skeleleleleleeleltons

This commit is contained in:
2026-05-14 14:24:33 +02:00
parent db0c4cdc6c
commit 320ff03c81
4 changed files with 172 additions and 21 deletions

View File

@@ -1992,6 +1992,79 @@ body.sidebar-right-hidden .global-sidebar-right {
margin-bottom: 4px; margin-bottom: 4px;
} }
/* ─── Sidebar Skeleton Loader ─────────────────────────────────────────────── */
@keyframes skeleton-shimmer {
0% { background-position: -300px 0; }
100% { background-position: 300px 0; }
}
.sidebar-skeleton-item {
padding: 8px 5px 10px;
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
}
.sidebar-skeleton-item:last-child {
border-bottom: none;
}
.skeleton-header {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 8px;
}
.skeleton-meta {
display: flex;
flex-direction: column;
gap: 5px;
flex: 1;
min-width: 0;
}
/* Shared shimmer base */
.skeleton-avatar,
.skeleton-line {
background: linear-gradient(
90deg,
rgba(255,255,255,0.04) 0%,
rgba(255,255,255,0.10) 40%,
rgba(255,255,255,0.04) 80%
);
background-size: 600px 100%;
animation: skeleton-shimmer 1.6s ease-in-out infinite;
border-radius: 3px;
}
.skeleton-avatar {
width: 24px;
height: 24px;
flex-shrink: 0;
border-radius: 2px;
}
.skeleton-line {
height: 10px;
}
.skeleton-name { width: 55%; }
.skeleton-time { width: 35%; height: 8px; opacity: 0.65; }
.skeleton-text-long { width: 90%; margin-bottom: 6px; }
.skeleton-text-medium { width: 70%; margin-bottom: 6px; }
.skeleton-text-short { width: 45%; margin-bottom: 0; }
/* Stagger the shimmer phase so each item feels alive individually (cycles every 5) */
.sidebar-skeleton-item:nth-child(5n+2) .skeleton-avatar,
.sidebar-skeleton-item:nth-child(5n+2) .skeleton-line { animation-delay: 0.2s; }
.sidebar-skeleton-item:nth-child(5n+3) .skeleton-avatar,
.sidebar-skeleton-item:nth-child(5n+3) .skeleton-line { animation-delay: 0.4s; }
.sidebar-skeleton-item:nth-child(5n+4) .skeleton-avatar,
.sidebar-skeleton-item:nth-child(5n+4) .skeleton-line { animation-delay: 0.6s; }
.sidebar-skeleton-item:nth-child(5n+5) .skeleton-avatar,
.sidebar-skeleton-item:nth-child(5n+5) .skeleton-line { animation-delay: 0.8s; }
/* Mobile Stacking for Legacy Mode (max-width: 999px) */ /* Mobile Stacking for Legacy Mode (max-width: 999px) */
@media (max-width: 999px) { @media (max-width: 999px) {
.item-layout-container { .item-layout-container {
@@ -6223,6 +6296,23 @@ button#togglebg {
animation: none; animation: none;
} }
/* ─── Posts Grid Skeleton: shimmer on real items while thumbnail loads ─────── */
div.posts > a.lazy-thumb:not(.loaded) {
background: linear-gradient(
90deg,
rgba(255,255,255,0.04) 0%,
rgba(255,255,255,0.10) 40%,
rgba(255,255,255,0.04) 80%
);
background-size: 600px 100%;
animation: skeleton-shimmer 1.6s ease-in-out infinite;
}
/* Stagger shimmer phases so items feel alive individually */
div.posts > a.lazy-thumb:not(.loaded):nth-child(3n+2) { animation-delay: 0.2s; }
div.posts > a.lazy-thumb:not(.loaded):nth-child(3n+3) { animation-delay: 0.4s; }
/* Make individual item entry subtler and faster for infinite scroll */ /* Make individual item entry subtler and faster for infinite scroll */
@keyframes fadeInFX { @keyframes fadeInFX {
0% { 0% {

View File

@@ -1,3 +1,8 @@
// Safe wrapper — window.f0ckDebug may not be defined yet when this file first executes
// (comments.js loads before the footer script block that sets window.f0ckDebug).
// Calling through this helper defers the lookup to invocation time, not parse time.
const _f0ckDebug = (...args) => (typeof window.f0ckDebug === 'function' ? window.f0ckDebug(...args) : void 0);
class CommentSystem { class CommentSystem {
constructor() { constructor() {
this.container = document.getElementById('comments-container'); this.container = document.getElementById('comments-container');
@@ -62,11 +67,11 @@ class CommentSystem {
return; return;
} }
if (this.container.dataset.commentSystemInit) { if (this.container.dataset.commentSystemInit) {
window.f0ckDebug('[CommentSystem] Already initialized for this container'); _f0ckDebug('[CommentSystem] Already initialized for this container');
return; return;
} }
this.container.dataset.commentSystemInit = 'true'; this.container.dataset.commentSystemInit = 'true';
window.f0ckDebug('[CommentSystem] Initializing for item:', this.itemId); _f0ckDebug('[CommentSystem] Initializing for item:', this.itemId);
this.loadComments(); this.loadComments();
this.setupGlobalListeners(); this.setupGlobalListeners();
@@ -190,7 +195,7 @@ class CommentSystem {
this.stabilizationObserver.disconnect(); this.stabilizationObserver.disconnect();
} }
this.stopStabilization(); this.stopStabilization();
window.f0ckDebug('[CommentSystem] Instance destroyed'); _f0ckDebug('[CommentSystem] Instance destroyed');
} }
async loadEmojis() { async loadEmojis() {
@@ -212,7 +217,7 @@ class CommentSystem {
this.customEmojis[e.name] = e.url; this.customEmojis[e.name] = e.url;
}); });
CommentSystem.emojiCache = this.customEmojis; CommentSystem.emojiCache = this.customEmojis;
window.f0ckDebug('Loaded Emojis:', this.customEmojis); _f0ckDebug('Loaded Emojis:', this.customEmojis);
// Preload images to prevent NS Binding Aborted errors // Preload images to prevent NS Binding Aborted errors
this.preloadEmojiImages(); this.preloadEmojiImages();
@@ -265,7 +270,7 @@ class CommentSystem {
// ... // ...
renderEmoji(match, name) { renderEmoji(match, name) {
// window.f0ckDebug('Rendering Emoji:', name, this.customEmojis ? this.customEmojis[name] : 'No list'); // _f0ckDebug('Rendering Emoji:', name, this.customEmojis ? this.customEmojis[name] : 'No list');
if (this.customEmojis && this.customEmojis[name]) { if (this.customEmojis && this.customEmojis[name]) {
return `<img src="${this.customEmojis[name]}" style="height:60px;vertical-align:middle;" alt="${name}">`; return `<img src="${this.customEmojis[name]}" style="height:60px;vertical-align:middle;" alt="${name}">`;
} }
@@ -374,7 +379,7 @@ class CommentSystem {
// However, if we ARE currently loading comments from server, skip re-render. // However, if we ARE currently loading comments from server, skip re-render.
if (this.initialLoadDone === false && this.lastData.length === 0) { if (this.initialLoadDone === false && this.lastData.length === 0) {
window.f0ckDebug('[CommentSystem] Live comment skipped - initial comments load in progress.'); _f0ckDebug('[CommentSystem] Live comment skipped - initial comments load in progress.');
return; return;
} }
@@ -599,7 +604,7 @@ class CommentSystem {
// 1. Early Bail-out: If data is bit-for-bit identical, do nothing. // 1. Early Bail-out: If data is bit-for-bit identical, do nothing.
// This is the primary defense against tab-switch reloads when nothing changed. // This is the primary defense against tab-switch reloads when nothing changed.
if (preserveScroll && this._isDeeplyIdentical(data.comments, data.user_id, data.is_subscribed)) { if (preserveScroll && this._isDeeplyIdentical(data.comments, data.user_id, data.is_subscribed)) {
window.f0ckDebug('[CommentSystem] Sync: Data identical, bailing early to protect media.'); _f0ckDebug('[CommentSystem] Sync: Data identical, bailing early to protect media.');
this.restoreState(state); this.restoreState(state);
this.preservingScroll = false; this.preservingScroll = false;
return; return;
@@ -607,7 +612,7 @@ class CommentSystem {
// 2. Reconciliation: If data changed but we want to preserve media. // 2. Reconciliation: If data changed but we want to preserve media.
if (preserveScroll && this.lastData && this.lastData.length > 0) { if (preserveScroll && this.lastData && this.lastData.length > 0) {
window.f0ckDebug('[CommentSystem] Sync: Data changed, reconciling DOM.'); _f0ckDebug('[CommentSystem] Sync: Data changed, reconciling DOM.');
this.reconcile(data.comments, data.user_id, data.is_subscribed); this.reconcile(data.comments, data.user_id, data.is_subscribed);
this.initialLoadDone = true; this.initialLoadDone = true;
this.restoreState(state); this.restoreState(state);
@@ -715,7 +720,7 @@ class CommentSystem {
this.startStabilization(id); this.startStabilization(id);
} }
} else if (retries > 0) { } else if (retries > 0) {
window.f0ckDebug(`[CommentSystem] Scroll target #c${id} not found, retrying... (${retries} left)`); _f0ckDebug(`[CommentSystem] Scroll target #c${id} not found, retrying... (${retries} left)`);
setTimeout(() => this.scrollToComment(id, retries - 1), 200); setTimeout(() => this.scrollToComment(id, retries - 1), 200);
} }
}; };
@@ -739,7 +744,7 @@ class CommentSystem {
const scrollKeys = ['ArrowUp', 'ArrowDown', 'PageUp', 'PageDown', ' ', 'Home', 'End']; const scrollKeys = ['ArrowUp', 'ArrowDown', 'PageUp', 'PageDown', ' ', 'Home', 'End'];
if (!scrollKeys.includes(e.key)) return; if (!scrollKeys.includes(e.key)) return;
} }
window.f0ckDebug(`[CommentSystem] Stabilization aborted due to ${e.type}`); _f0ckDebug(`[CommentSystem] Stabilization aborted due to ${e.type}`);
this.isUserInteracting = true; this.isUserInteracting = true;
this.stopStabilization(); this.stopStabilization();
}; };
@@ -762,7 +767,7 @@ class CommentSystem {
// If it shifted more than 10px (e.g. media loaded), re-scroll // If it shifted more than 10px (e.g. media loaded), re-scroll
if (diff > 10 && checks < maxChecks) { if (diff > 10 && checks < maxChecks) {
window.f0ckDebug(`[CommentSystem] Layout shift detected (${Math.round(diff)}px), re-stabilizing scroll...`); _f0ckDebug(`[CommentSystem] Layout shift detected (${Math.round(diff)}px), re-stabilizing scroll...`);
this.scrollToComment(id, 0, true); this.scrollToComment(id, 0, true);
lastTop = currentEl.getBoundingClientRect().top; lastTop = currentEl.getBoundingClientRect().top;
} }
@@ -1236,7 +1241,7 @@ class CommentSystem {
// Check for edits or state changes using robust data-attributes // Check for edits or state changes using robust data-attributes
const contentEl = el.querySelector('.comment-content'); const contentEl = el.querySelector('.comment-content');
if (contentEl && contentEl.dataset.raw !== incoming.content) { if (contentEl && contentEl.dataset.raw !== incoming.content) {
window.f0ckDebug(`[CommentSystem] Reconcile: Updating content for #c${id}`); _f0ckDebug(`[CommentSystem] Reconcile: Updating content for #c${id}`);
contentEl.innerHTML = this.renderCommentContent(incoming.content, incoming.id); contentEl.innerHTML = this.renderCommentContent(incoming.content, incoming.id);
contentEl.dataset.raw = incoming.content; contentEl.dataset.raw = incoming.content;
} }
@@ -1260,7 +1265,7 @@ class CommentSystem {
const idStr = String(c.id); const idStr = String(c.id);
if (document.getElementById('c' + idStr)) return; if (document.getElementById('c' + idStr)) return;
window.f0ckDebug(`[CommentSystem] Reconcile: Injecting new flat comment #c${idStr}`); _f0ckDebug(`[CommentSystem] Reconcile: Injecting new flat comment #c${idStr}`);
const html = this.renderComment(c, currentUserId, false, true); const html = this.renderComment(c, currentUserId, false, true);
const tmp = document.createElement('div'); const tmp = document.createElement('div');
tmp.innerHTML = html; tmp.innerHTML = html;
@@ -1322,7 +1327,7 @@ class CommentSystem {
let el = document.getElementById('c' + idStr); let el = document.getElementById('c' + idStr);
if (!el) { if (!el) {
window.f0ckDebug(`[CommentSystem] Reconcile: Injecting new comment #c${idStr}`); _f0ckDebug(`[CommentSystem] Reconcile: Injecting new comment #c${idStr}`);
const html = this.renderComment(c, currentUserId, isReply); const html = this.renderComment(c, currentUserId, isReply);
const tmp = document.createElement('div'); const tmp = document.createElement('div');
tmp.innerHTML = html; tmp.innerHTML = html;
@@ -1957,7 +1962,7 @@ class CommentSystem {
} }
setupDelegatedEvents() { setupDelegatedEvents() {
window.f0ckDebug('[DEBUG] Setting up delegated events for container:', this.container); _f0ckDebug('[DEBUG] Setting up delegated events for container:', this.container);
if (!this.container) return; if (!this.container) return;
// Ctrl+Enter to submit comment // Ctrl+Enter to submit comment
@@ -1985,7 +1990,7 @@ class CommentSystem {
// Single Click Listener for Everything // Single Click Listener for Everything
this.container.addEventListener('click', async (e) => { this.container.addEventListener('click', async (e) => {
window.f0ckDebug('[DEBUG] Click on container:', e.target); _f0ckDebug('[DEBUG] Click on container:', e.target);
const target = e.target; const target = e.target;
@@ -2503,7 +2508,7 @@ class CommentSystem {
retryCount++; retryCount++;
// Randomized exponential backoff // Randomized exponential backoff
const delay = Math.min(1000 * Math.pow(1.5, retryCount) + (Math.random() * 1000), 10000); const delay = Math.min(1000 * Math.pow(1.5, retryCount) + (Math.random() * 1000), 10000);
window.f0ckDebug(`[CommentSystem] Retrying in ${Math.round(delay)}ms...`); _f0ckDebug(`[CommentSystem] Retrying in ${Math.round(delay)}ms...`);
setTimeout(attemptSubmit, delay); setTimeout(attemptSubmit, delay);
} else { } else {
alert('Failed to send comment after multiple attempts. Please check your connection.'); alert('Failed to send comment after multiple attempts. Please check your connection.');

View File

@@ -435,6 +435,42 @@
} }
}); });
const SIDEBAR_SKELETON_COUNT = 15;
const showSkeletons = () => {
const container = document.getElementById('sidebar-activity-container');
if (!container) return;
const variants = [
`<div class="skeleton-line skeleton-text-long"></div>
<div class="skeleton-line skeleton-text-medium"></div>
<div class="skeleton-line skeleton-text-short"></div>`,
`<div class="skeleton-line skeleton-text-long"></div>
<div class="skeleton-line skeleton-text-short"></div>`,
`<div class="skeleton-line skeleton-text-medium"></div>
<div class="skeleton-line skeleton-text-long"></div>
<div class="skeleton-line skeleton-text-short"></div>`,
`<div class="skeleton-line skeleton-text-long"></div>
<div class="skeleton-line skeleton-text-medium"></div>`,
`<div class="skeleton-line skeleton-text-short"></div>
<div class="skeleton-line skeleton-text-long"></div>`,
];
let html = '';
for (let i = 0; i < SIDEBAR_SKELETON_COUNT; i++) {
html += `
<div class="sidebar-skeleton-item">
<div class="skeleton-header">
<div class="skeleton-avatar"></div>
<div class="skeleton-meta">
<div class="skeleton-line skeleton-name"></div>
<div class="skeleton-line skeleton-time"></div>
</div>
</div>
${variants[i % 5]}
</div>`;
}
container.innerHTML = html;
};
const renderFromCache = () => { const renderFromCache = () => {
const container = document.getElementById('sidebar-activity-container'); const container = document.getElementById('sidebar-activity-container');
if (!container || window._sidebarActivityCache.length === 0) return false; if (!container || window._sidebarActivityCache.length === 0) return false;
@@ -461,10 +497,14 @@
if (!container || loading) return; if (!container || loading) return;
const hasCache = renderFromCache(); const hasCache = renderFromCache();
// If no cache and not silent: show skeletons while we fetch.
// On the very first page load the server-rendered skeletons are already there;
// on subsequent loads (e.g. mode change) we inject them programmatically.
if (!hasCache && !silent) { if (!hasCache && !silent) {
container.innerHTML = '<div class="loading">Loading activity...</div>'; showSkeletons();
} }
loading = true; loading = true;
currentPage = 1; currentPage = 1;
hasMore = true; hasMore = true;
@@ -484,13 +524,13 @@
renderFromCache(); renderFromCache();
// Also check after a delay to account for image/emoji loading shifts // Also check after a delay to account for image/emoji loading shifts
setTimeout(checkOverflow, 500); setTimeout(checkOverflow, 500);
} else if (container.innerHTML.includes('loading')) { } else if (!hasCache) {
container.innerHTML = '<div style="text-align:center;padding:20px;color:#888;">No recent activity.</div>'; container.innerHTML = '<div style="text-align:center;padding:20px;color:#888;">No recent activity.</div>';
hasMore = false; hasMore = false;
} }
} catch (e) { } catch (e) {
console.error("Sidebar Activity: Failed to load activity", e); console.error("Sidebar Activity: Failed to load activity", e);
if (container.innerHTML.includes('loading')) { if (!hasCache) {
container.innerHTML = '<div style="text-align:center;padding:20px;color:#888;">Failed to load.</div>'; container.innerHTML = '<div style="text-align:center;padding:20px;color:#888;">Failed to load.</div>';
} }
hasMore = false; hasMore = false;
@@ -499,6 +539,7 @@
} }
}; };
const loadMoreActivity = async () => { const loadMoreActivity = async () => {
const container = document.getElementById('sidebar-activity-container'); const container = document.getElementById('sidebar-activity-container');
if (!container || loading || loadingMore || !hasMore) return; if (!container || loading || loadingMore || !hasMore) return;

View File

@@ -106,7 +106,21 @@
<div class="global-sidebar-right"> <div class="global-sidebar-right">
<div class="sidebar-activity"> <div class="sidebar-activity">
<div id="sidebar-activity-container" class="sidebar-comments-list"> <div id="sidebar-activity-container" class="sidebar-comments-list">
<div class="loading">{{ t('sidebar.loading_activity') }}</div> <div class="sidebar-skeleton-item"><div class="skeleton-header"><div class="skeleton-avatar"></div><div class="skeleton-meta"><div class="skeleton-line skeleton-name"></div><div class="skeleton-line skeleton-time"></div></div></div><div class="skeleton-line skeleton-text-long"></div><div class="skeleton-line skeleton-text-medium"></div><div class="skeleton-line skeleton-text-short"></div></div>
<div class="sidebar-skeleton-item"><div class="skeleton-header"><div class="skeleton-avatar"></div><div class="skeleton-meta"><div class="skeleton-line skeleton-name"></div><div class="skeleton-line skeleton-time"></div></div></div><div class="skeleton-line skeleton-text-long"></div><div class="skeleton-line skeleton-text-short"></div></div>
<div class="sidebar-skeleton-item"><div class="skeleton-header"><div class="skeleton-avatar"></div><div class="skeleton-meta"><div class="skeleton-line skeleton-name"></div><div class="skeleton-line skeleton-time"></div></div></div><div class="skeleton-line skeleton-text-medium"></div><div class="skeleton-line skeleton-text-long"></div><div class="skeleton-line skeleton-text-short"></div></div>
<div class="sidebar-skeleton-item"><div class="skeleton-header"><div class="skeleton-avatar"></div><div class="skeleton-meta"><div class="skeleton-line skeleton-name"></div><div class="skeleton-line skeleton-time"></div></div></div><div class="skeleton-line skeleton-text-long"></div><div class="skeleton-line skeleton-text-medium"></div></div>
<div class="sidebar-skeleton-item"><div class="skeleton-header"><div class="skeleton-avatar"></div><div class="skeleton-meta"><div class="skeleton-line skeleton-name"></div><div class="skeleton-line skeleton-time"></div></div></div><div class="skeleton-line skeleton-text-short"></div><div class="skeleton-line skeleton-text-long"></div></div>
<div class="sidebar-skeleton-item"><div class="skeleton-header"><div class="skeleton-avatar"></div><div class="skeleton-meta"><div class="skeleton-line skeleton-name"></div><div class="skeleton-line skeleton-time"></div></div></div><div class="skeleton-line skeleton-text-long"></div><div class="skeleton-line skeleton-text-medium"></div><div class="skeleton-line skeleton-text-short"></div></div>
<div class="sidebar-skeleton-item"><div class="skeleton-header"><div class="skeleton-avatar"></div><div class="skeleton-meta"><div class="skeleton-line skeleton-name"></div><div class="skeleton-line skeleton-time"></div></div></div><div class="skeleton-line skeleton-text-long"></div><div class="skeleton-line skeleton-text-short"></div></div>
<div class="sidebar-skeleton-item"><div class="skeleton-header"><div class="skeleton-avatar"></div><div class="skeleton-meta"><div class="skeleton-line skeleton-name"></div><div class="skeleton-line skeleton-time"></div></div></div><div class="skeleton-line skeleton-text-medium"></div><div class="skeleton-line skeleton-text-long"></div><div class="skeleton-line skeleton-text-short"></div></div>
<div class="sidebar-skeleton-item"><div class="skeleton-header"><div class="skeleton-avatar"></div><div class="skeleton-meta"><div class="skeleton-line skeleton-name"></div><div class="skeleton-line skeleton-time"></div></div></div><div class="skeleton-line skeleton-text-long"></div><div class="skeleton-line skeleton-text-medium"></div></div>
<div class="sidebar-skeleton-item"><div class="skeleton-header"><div class="skeleton-avatar"></div><div class="skeleton-meta"><div class="skeleton-line skeleton-name"></div><div class="skeleton-line skeleton-time"></div></div></div><div class="skeleton-line skeleton-text-short"></div><div class="skeleton-line skeleton-text-long"></div></div>
<div class="sidebar-skeleton-item"><div class="skeleton-header"><div class="skeleton-avatar"></div><div class="skeleton-meta"><div class="skeleton-line skeleton-name"></div><div class="skeleton-line skeleton-time"></div></div></div><div class="skeleton-line skeleton-text-long"></div><div class="skeleton-line skeleton-text-medium"></div><div class="skeleton-line skeleton-text-short"></div></div>
<div class="sidebar-skeleton-item"><div class="skeleton-header"><div class="skeleton-avatar"></div><div class="skeleton-meta"><div class="skeleton-line skeleton-name"></div><div class="skeleton-line skeleton-time"></div></div></div><div class="skeleton-line skeleton-text-long"></div><div class="skeleton-line skeleton-text-short"></div></div>
<div class="sidebar-skeleton-item"><div class="skeleton-header"><div class="skeleton-avatar"></div><div class="skeleton-meta"><div class="skeleton-line skeleton-name"></div><div class="skeleton-line skeleton-time"></div></div></div><div class="skeleton-line skeleton-text-medium"></div><div class="skeleton-line skeleton-text-long"></div></div>
<div class="sidebar-skeleton-item"><div class="skeleton-header"><div class="skeleton-avatar"></div><div class="skeleton-meta"><div class="skeleton-line skeleton-name"></div><div class="skeleton-line skeleton-time"></div></div></div><div class="skeleton-line skeleton-text-long"></div><div class="skeleton-line skeleton-text-medium"></div></div>
<div class="sidebar-skeleton-item"><div class="skeleton-header"><div class="skeleton-avatar"></div><div class="skeleton-meta"><div class="skeleton-line skeleton-name"></div><div class="skeleton-line skeleton-time"></div></div></div><div class="skeleton-line skeleton-text-short"></div><div class="skeleton-line skeleton-text-long"></div></div>
</div> </div>
</div> </div>
<div class="global-sidebar-right-footer"> <div class="global-sidebar-right-footer">
@@ -402,6 +416,7 @@
sidebar_read_more: "{{ t('sidebar.read_more') }}", sidebar_read_more: "{{ t('sidebar.read_more') }}",
sidebar_see_less: "{{ t('sidebar.see_less') }}", sidebar_see_less: "{{ t('sidebar.see_less') }}",
sidebar_show_full_comment: "{{ t('sidebar.show_full_comment') }}", sidebar_show_full_comment: "{{ t('sidebar.show_full_comment') }}",
sidebar_loading_activity: "{{ t('sidebar.loading_activity') }}",
select_file: "{{ t('upload_btn.select_file') }}", select_file: "{{ t('upload_btn.select_file') }}",
enter_url: "{{ t('upload_btn.enter_url') }}", enter_url: "{{ t('upload_btn.enter_url') }}",
tags_required: "{{ t('upload_btn.tags_required') }}", tags_required: "{{ t('upload_btn.tags_required') }}",