beautifying the new shitpost upload process
This commit is contained in:
@@ -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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
Reference in New Issue
Block a user