diff --git a/public/s/css/upload.css b/public/s/css/upload.css index fd1936c..6e70605 100644 --- a/public/s/css/upload.css +++ b/public/s/css/upload.css @@ -255,7 +255,6 @@ flex: 1; display: flex; flex-direction: column; - gap: 10px; overflow: hidden; min-width: 0; } @@ -1170,3 +1169,31 @@ .item-comment-input { min-height: 60px; } + +/* Media column wrapper for URL items in shitpost mode: + stacks the embed/icon above the url-type-badge (refetch trigger). + Mirrors the flex sizing of .preview-media-small so embed size is unchanged. */ +.item-media-col { + display: flex; + flex-direction: column; + align-items: stretch; + flex: 0 0 50%; + width: 50%; + min-height: 275px; + max-height: 350px; +} + +.item-media-col iframe, +.item-media-col .generic-file-icon { + width: 100%; + flex: 1 1 auto; + min-height: 120px; + max-height: 320px; + border-radius: 8px; + background: rgba(0, 0, 0, 0.3); +} + +/* Badge inside the col sits flush below the embed, no extra sizing */ +.item-media-col .url-type-badge { + flex-shrink: 0; +} diff --git a/public/s/js/upload.js b/public/s/js/upload.js index 2d22810..a2c2e7f 100644 --- a/public/s/js/upload.js +++ b/public/s/js/upload.js @@ -273,6 +273,47 @@ window.initUploadForm = (selector) => { // --- URL type detection & Metadata extraction --- let metaDebounce = null; let lastFetchedUrl = ''; + // Cache: url -> string[] of suggestion fields (populated by fetchMetadata) + const metaCache = new Map(); + + // Extracts a flat list of suggestion strings from a /api/v2/meta/fetch response meta object + const extractFieldsFromMeta = (meta) => { + const fields = []; + const push = (v) => { if (v && typeof v === 'string' && v.trim()) fields.push(v.trim()); }; + const pushArr = (arr) => { if (Array.isArray(arr)) arr.forEach(push); }; + + // Title (prefer og_title, strip site name suffix) + let bestTitle = meta.og_title || meta.twitter_title || meta.ld_headline || meta.title || null; + if (bestTitle && meta.site_name) { + const siteRegex = new RegExp(`\\s*[-|·]\\s*${meta.site_name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`, 'i'); + bestTitle = bestTitle.replace(siteRegex, '').trim(); + } + push(bestTitle); + push(meta.site_name); + push(meta.og_desc || meta.description || meta.twitter_desc || meta.ld_desc); + push(meta.author || meta.article_author || meta.book_author || meta.music_musician || meta.ld_author || meta.ld_creator); + push(meta.profile_name); + push(meta.profile_username); + pushArr(meta.keywords); + pushArr(meta.news_keywords); + pushArr(meta.ld_keywords); + pushArr(meta.article_tags); + push(meta.article_section); + push(meta.category); + push(meta.genre || meta.ld_genre); + push(meta.og_type); + pushArr(meta.video_tags); + push(meta.music_album); + push(meta.music_song); + pushArr(meta.book_tags); + pushArr(meta.twitter_labels); + push(meta.twitter_creator); + push(meta.ld_name); + push(meta.ld_about); + if (meta.price_amount) push(`${meta.price_amount}${meta.price_currency ? ' ' + meta.price_currency : ''}`); + push(meta.language); + return [...new Set(fields)]; + }; if (urlInput) { const fetchMetadata = async (url) => { @@ -296,67 +337,10 @@ window.initUploadForm = (selector) => { const resp = await fetch(`/api/v2/meta/fetch?url=${encodeURIComponent(currentVal)}`); const data = await resp.json(); if (data.success && data.meta) { - const meta = data.meta; - - const suggest = (v) => { if (v && typeof v === 'string' && v.trim()) addMetaSuggestion(v.trim()); }; - const suggestArr = (arr) => { if (Array.isArray(arr)) arr.forEach(suggest); }; - - // Title (prefer og_title, strip site name suffix) - let bestTitle = meta.og_title || meta.twitter_title || meta.ld_headline || meta.title || null; - if (bestTitle && meta.site_name) { - const siteRegex = new RegExp(`\\s*[-|·]\\s*${meta.site_name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`, 'i'); - bestTitle = bestTitle.replace(siteRegex, '').trim(); - } - suggest(bestTitle); - - // Site / source - suggest(meta.site_name); - - // Description / summary (og or standard — offer as single suggestion) - suggest(meta.og_desc || meta.description || meta.twitter_desc || meta.ld_desc); - - // Author - suggest(meta.author || meta.article_author || meta.book_author || meta.music_musician || meta.ld_author || meta.ld_creator); - suggest(meta.profile_name); - suggest(meta.profile_username); - - // Keywords (each separate) - suggestArr(meta.keywords); - suggestArr(meta.news_keywords); - suggestArr(meta.ld_keywords); - - // Article / content classification - suggestArr(meta.article_tags); - suggest(meta.article_section); - suggest(meta.category); - suggest(meta.genre || meta.ld_genre); - suggest(meta.og_type); - - // Video tags - suggestArr(meta.video_tags); - - // Music - suggest(meta.music_album); - suggest(meta.music_song); - - // Book tags - suggestArr(meta.book_tags); - - // Twitter labels (e.g. "Rating: 5 stars") - suggestArr(meta.twitter_labels); - - // Twitter handles - suggest(meta.twitter_creator); - - // JSON-LD extras - suggest(meta.ld_name); - suggest(meta.ld_about); - - // Price - if (meta.price_amount) suggest(`${meta.price_amount}${meta.price_currency ? ' ' + meta.price_currency : ''}`); - - // Language - suggest(meta.language); + const fields = extractFieldsFromMeta(data.meta); + // Cache the fields for instant use when item is committed + metaCache.set(currentVal, fields); + fields.forEach(v => addMetaSuggestion(v)); urlBadge.innerHTML = '✓ Metadata Fetched'; urlBadge.className = 'url-type-badge success'; @@ -685,6 +669,9 @@ window.initUploadForm = (selector) => { previewItem.className = 'file-preview-item' + (isUrl ? ' url-item' : ''); let mediaElem; + // For URL items: wrap media + badge in a column div + let mediaCol = null; + let itemBadge = null; if (isUrl) { const isYtMatch = item.url.match(ytRegex); if (isYtMatch && window.f0ckEnableYoutubeUpload !== false) { @@ -697,12 +684,34 @@ window.initUploadForm = (selector) => { mediaElem.allow = "accelerometer; clipboard-write; encrypted-media; gyroscope; picture-in-picture"; mediaElem.allowFullscreen = true; mediaElem.style.borderRadius = '4px'; - mediaElem.style.pointerEvents = 'auto'; // Ensure we can interact with it + mediaElem.style.pointerEvents = 'auto'; + + // Badge looks like the URL-tab YouTube badge + itemBadge = document.createElement('div'); + itemBadge.className = 'url-type-badge youtube'; + itemBadge.textContent = '▶ YouTube'; + itemBadge.title = window.f0ckI18n?.refetch_metadata || 'Click to re-fetch metadata'; + itemBadge.style.display = 'block'; + itemBadge.style.cursor = 'pointer'; + itemBadge.style.marginTop = '6px'; } else { mediaElem = document.createElement('div'); mediaElem.className = 'generic-file-icon'; mediaElem.innerHTML = ''; + + // Badge looks like the URL-tab direct badge + itemBadge = document.createElement('div'); + itemBadge.className = 'url-type-badge direct'; + itemBadge.textContent = 'URL'; + itemBadge.title = window.f0ckI18n?.refetch_metadata || 'Click to re-fetch metadata'; + itemBadge.style.display = 'block'; + itemBadge.style.cursor = 'pointer'; + itemBadge.style.marginTop = '6px'; } + + // Wrap media + badge in a column + mediaCol = document.createElement('div'); + mediaCol.className = 'item-media-col'; } else { const fileExt = file.name.split('.').pop().toLowerCase(); @@ -735,7 +744,16 @@ window.initUploadForm = (selector) => { mediaElem.innerHTML = ''; } } - mediaElem.classList.add('preview-media-small'); + // preview-media-small sizing only applies to standalone (non-col) media + if (!mediaCol) { + mediaElem.classList.add('preview-media-small'); + } + + // If URL item: finish building the media column (media + badge below) + if (mediaCol) { + mediaCol.appendChild(mediaElem); + if (itemBadge) mediaCol.appendChild(itemBadge); + } const infoRow = document.createElement('div'); infoRow.className = 'file-meta-row-small'; @@ -783,7 +801,9 @@ window.initUploadForm = (selector) => { } const fileNameStr = isUrl ? item.url : file.name; - const fileSizeStr = isUrl ? 'URL' : formatSize(file.size); + const fileSizeStr = isUrl + ? ((item.url.match(ytRegex) && window.f0ckEnableYoutubeUpload !== false) ? 'YouTube' : 'URL') + : formatSize(file.size); infoRow.innerHTML = `