beautifying the new shitpost upload process

This commit is contained in:
2026-05-13 12:34:51 +02:00
parent e59e038a8c
commit ba2e20679a
2 changed files with 205 additions and 72 deletions

View File

@@ -255,7 +255,6 @@
flex: 1; flex: 1;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 10px;
overflow: hidden; overflow: hidden;
min-width: 0; min-width: 0;
} }
@@ -1170,3 +1169,31 @@
.item-comment-input { .item-comment-input {
min-height: 60px; 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;
}

View File

@@ -273,6 +273,47 @@ window.initUploadForm = (selector) => {
// --- URL type detection & Metadata extraction --- // --- URL type detection & Metadata extraction ---
let metaDebounce = null; let metaDebounce = null;
let lastFetchedUrl = ''; 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) { if (urlInput) {
const fetchMetadata = async (url) => { const fetchMetadata = async (url) => {
@@ -296,67 +337,10 @@ window.initUploadForm = (selector) => {
const resp = await fetch(`/api/v2/meta/fetch?url=${encodeURIComponent(currentVal)}`); const resp = await fetch(`/api/v2/meta/fetch?url=${encodeURIComponent(currentVal)}`);
const data = await resp.json(); const data = await resp.json();
if (data.success && data.meta) { if (data.success && data.meta) {
const meta = data.meta; const fields = extractFieldsFromMeta(data.meta);
// Cache the fields for instant use when item is committed
const suggest = (v) => { if (v && typeof v === 'string' && v.trim()) addMetaSuggestion(v.trim()); }; metaCache.set(currentVal, fields);
const suggestArr = (arr) => { if (Array.isArray(arr)) arr.forEach(suggest); }; fields.forEach(v => addMetaSuggestion(v));
// 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);
urlBadge.innerHTML = '✓ Metadata Fetched'; urlBadge.innerHTML = '✓ Metadata Fetched';
urlBadge.className = 'url-type-badge success'; urlBadge.className = 'url-type-badge success';
@@ -685,6 +669,9 @@ window.initUploadForm = (selector) => {
previewItem.className = 'file-preview-item' + (isUrl ? ' url-item' : ''); previewItem.className = 'file-preview-item' + (isUrl ? ' url-item' : '');
let mediaElem; let mediaElem;
// For URL items: wrap media + badge in a column div
let mediaCol = null;
let itemBadge = null;
if (isUrl) { if (isUrl) {
const isYtMatch = item.url.match(ytRegex); const isYtMatch = item.url.match(ytRegex);
if (isYtMatch && window.f0ckEnableYoutubeUpload !== false) { if (isYtMatch && window.f0ckEnableYoutubeUpload !== false) {
@@ -697,12 +684,34 @@ window.initUploadForm = (selector) => {
mediaElem.allow = "accelerometer; clipboard-write; encrypted-media; gyroscope; picture-in-picture"; mediaElem.allow = "accelerometer; clipboard-write; encrypted-media; gyroscope; picture-in-picture";
mediaElem.allowFullscreen = true; mediaElem.allowFullscreen = true;
mediaElem.style.borderRadius = '4px'; 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 { } else {
mediaElem = document.createElement('div'); mediaElem = document.createElement('div');
mediaElem.className = 'generic-file-icon'; mediaElem.className = 'generic-file-icon';
mediaElem.innerHTML = '<i class="fa-solid fa-link"></i>'; mediaElem.innerHTML = '<i class="fa-solid fa-link"></i>';
// 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 { } else {
const fileExt = file.name.split('.').pop().toLowerCase(); const fileExt = file.name.split('.').pop().toLowerCase();
@@ -735,7 +744,16 @@ window.initUploadForm = (selector) => {
mediaElem.innerHTML = '<i class="fa-solid fa-file"></i>'; mediaElem.innerHTML = '<i class="fa-solid fa-file"></i>';
} }
} }
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'); const infoRow = document.createElement('div');
infoRow.className = 'file-meta-row-small'; infoRow.className = 'file-meta-row-small';
@@ -783,7 +801,9 @@ window.initUploadForm = (selector) => {
} }
const fileNameStr = isUrl ? item.url : file.name; 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 = ` infoRow.innerHTML = `
<div class="file-info-small"> <div class="file-info-small">
@@ -874,17 +894,98 @@ window.initUploadForm = (selector) => {
// Trigger Extraction for this item // Trigger Extraction for this item
if (isUrl) { if (isUrl) {
(async () => { // Helper: populate suggestions for this item from a fields array
const populateSuggestionsFromFields = (fields) => {
fields.forEach(val => addMetaSuggestionToItem(val));
};
// The itemBadge IS the refetch button — same badge as the URL tab
const isYt = ytRegex.test(item.url) && window.f0ckEnableYoutubeUpload !== false;
const badgeLabel = isYt ? '▶ YouTube' : 'URL';
const badgeClass = isYt ? 'url-type-badge youtube' : 'url-type-badge direct';
// Helper: drive the badge through fetching → success → restore
const setBadgeFetching = () => {
if (!itemBadge) return;
itemBadge.innerHTML = '<span class="loading-spinner"></span> Fetching...';
itemBadge.className = 'url-type-badge fetching';
itemBadge.style.display = 'flex';
itemBadge.style.pointerEvents = 'none';
};
const setBadgeSuccess = () => {
if (!itemBadge) return;
itemBadge.innerHTML = '✓ Metadata Fetched';
itemBadge.className = 'url-type-badge success';
itemBadge.style.display = 'block';
setTimeout(() => {
if (itemBadge) {
itemBadge.textContent = badgeLabel;
itemBadge.className = badgeClass;
itemBadge.style.pointerEvents = 'auto';
}
}, 2500);
};
const setBadgeRestored = () => {
if (!itemBadge) return;
itemBadge.textContent = badgeLabel;
itemBadge.className = badgeClass;
itemBadge.style.display = 'block';
itemBadge.style.pointerEvents = 'auto';
};
// Helper: fetch metadata for this item
const fetchItemMeta = async (forceFetch = false) => {
if (suggCont) suggCont.style.display = 'flex';
// Cache hit → instant, no network call needed
if (!forceFetch && metaCache.has(item.url)) {
populateSuggestionsFromFields(metaCache.get(item.url));
setBadgeSuccess();
return;
}
// Dim existing suggestions in-place — no removal, no layout jump
const staleChips = suggCont
? Array.from(suggCont.querySelectorAll('.item-meta-suggestion'))
: [];
staleChips.forEach(s => { s.style.opacity = '0.3'; s.style.pointerEvents = 'none'; });
setBadgeFetching();
try { try {
const resp = await fetch('/api/v2/meta/extract-url?url=' + encodeURIComponent(item.url), { const resp = await fetch(`/api/v2/meta/fetch?url=${encodeURIComponent(item.url)}`, {
headers: { 'X-Requested-With': 'XMLHttpRequest' } headers: { 'X-Requested-With': 'XMLHttpRequest' }
}); });
const data = await resp.json(); const data = await resp.json();
if (data.success && data.fields) { if (data.success && data.meta) {
data.fields.forEach(val => addMetaSuggestionToItem(val)); const fields = extractFieldsFromMeta(data.meta);
metaCache.set(item.url, fields);
// Now it's safe to clear — new content is ready to fill the space
staleChips.forEach(s => s.remove());
populateSuggestionsFromFields(fields);
setBadgeSuccess();
} else {
// Restore stale chips on failure
staleChips.forEach(s => { s.style.opacity = ''; s.style.pointerEvents = ''; });
setBadgeRestored();
} }
} catch (e) { console.warn('[URL ITEM META SYNC ERROR]', e.message); } } catch (e) {
})(); console.warn('[URL ITEM META SYNC ERROR]', e.message);
staleChips.forEach(s => { s.style.opacity = ''; s.style.pointerEvents = ''; });
setBadgeRestored();
}
};
// Badge click = re-fetch
if (itemBadge) {
itemBadge.addEventListener('click', (e) => {
e.stopPropagation();
metaCache.delete(item.url);
fetchItemMeta(true);
});
}
// Populate suggestions (instant if cached, otherwise network)
fetchItemMeta(false);
} else { } else {
// Add filename as first suggestion // Add filename as first suggestion
if (file.name) { if (file.name) {
@@ -931,7 +1032,12 @@ window.initUploadForm = (selector) => {
handleFile(); // Rebuild UI handleFile(); // Rebuild UI
}; };
previewItem.appendChild(mediaElem); // Append media column (URL items) or raw media element (file items)
if (mediaCol) {
previewItem.appendChild(mediaCol);
} else {
previewItem.appendChild(mediaElem);
}
previewItem.appendChild(infoRow); previewItem.appendChild(infoRow);
previewItem.appendChild(removeBtn); previewItem.appendChild(removeBtn);
if (filePreview) filePreview.appendChild(previewItem); if (filePreview) filePreview.appendChild(previewItem);