This commit is contained in:
2026-09-12 06:01:50 +02:00
parent 2913b03d50
commit 4f6223640f
15 changed files with 991 additions and 159 deletions
+124 -80
View File
@@ -33,10 +33,24 @@
--pagination-background: #2b2b2b; --pagination-background: #2b2b2b;
--metadata-bg: #2b2b2b; --metadata-bg: #2b2b2b;
--badge-bg: #171717; --badge-bg: #171717;
--badge-sfw: #02500b;
--badge-nsfw: #E10DC3;
--badge-nsfl: #660000;
--badge-untagged: #555555;
--badge-tag: #090909;
--posts-meta-bg: #000000b8; --posts-meta-bg: #000000b8;
--x2154: #215483; --x2154: #215483;
} }
html:not([theme]), html[theme=""] {
--badge-bg: #171717;
--badge-sfw: #02500b;
--badge-nsfw: #E10DC3;
--badge-nsfl: #660000;
--badge-untagged: #555555;
--badge-tag: #090909;
}
html, body { html, body {
touch-action: pan-x pan-y; touch-action: pan-x pan-y;
} }
@@ -3017,45 +3031,15 @@ body.sidebar-right-hidden #sidebar-drag-zone {
transition: background-color 0.2s ease, border-color 0.2s ease, transform 0.2s ease; transition: background-color 0.2s ease, border-color 0.2s ease, transform 0.2s ease;
} }
.sidebar-video-card:hover { .sidebar-video-card:hover,
.sidebar-video-card.touch-active {
background: rgba(255, 255, 255, 0.06); background: rgba(255, 255, 255, 0.06);
border-color: rgba(255, 255, 255, 0.12); border-color: rgba(255, 255, 255, 0.12);
transform: translateY(-1px); transform: translateY(-1px);
} }
.sidebar-video-card:active {
transform: scale(0.99);
}
.sidebar-video-card.sidebar-card-swapping { .sidebar-video-card.sidebar-card-swapping {
opacity: 0.35; opacity: 0.5;
filter: grayscale(0.6);
transition: opacity 0.5s ease, filter 0.5s ease;
}
.sidebar-video-card.sidebar-card-entering {
animation: sidebarCardEnter 2s cubic-bezier(0.16, 1, 0.3, 1) forwards;
}
@keyframes sidebarCardEnter {
0% {
opacity: 0;
transform: scale(0.91) translateY(8px);
filter: brightness(1.2) contrast(0.95);
}
40% {
opacity: 0.75;
transform: scale(0.97) translateY(3px);
}
75% {
opacity: 0.95;
transform: scale(0.995) translateY(0.5px);
}
100% {
opacity: 1;
transform: scale(1) translateY(0);
filter: brightness(1) contrast(1);
}
} }
.sidebar-video-card .sidebar-video-link { .sidebar-video-card .sidebar-video-link {
@@ -3080,43 +3064,15 @@ body.sidebar-right-hidden #sidebar-drag-zone {
object-fit: cover; object-fit: cover;
display: block; display: block;
transition: transform 0.3s ease; transition: transform 0.3s ease;
-webkit-user-drag: none;
user-select: none;
-webkit-user-select: none;
} }
.sidebar-video-card:hover .sidebar-video-thumb { .sidebar-video-card:hover .sidebar-video-thumb {
transform: scale(1.04); transform: scale(1.04);
} }
.sidebar-video-play-overlay {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
background: rgba(0, 0, 0, 0.25);
color: #fff;
font-size: 1.2em;
opacity: 0;
transition: opacity 0.2s ease, background 0.2s ease;
}
.sidebar-video-play-overlay i {
width: 38px;
height: 38px;
border-radius: 50%;
background: rgba(0, 0, 0, 0.65);
display: flex;
align-items: center;
justify-content: center;
padding-left: 3px;
border: 1px solid rgba(255, 255, 255, 0.2);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.5);
}
.sidebar-video-card:hover .sidebar-video-play-overlay {
opacity: 1;
background: rgba(0, 0, 0, 0.35);
}
.sidebar-video-badge { .sidebar-video-badge {
position: absolute; position: absolute;
bottom: 6px; bottom: 6px;
@@ -3131,20 +3087,21 @@ body.sidebar-right-hidden #sidebar-drag-zone {
color: #fff; color: #fff;
text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.8); text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.8);
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.4); box-shadow: 0 1px 3px rgba(0, 0, 0, 0.4);
z-index: 10;
} }
.sidebar-video-badge.rating-sfw { .sidebar-video-badge.rating-sfw {
background-color: var(--badge-sfw); background-color: var(--badge-sfw, #02500b);
border: 1px solid rgba(0, 0, 0, 0.2); border: 1px solid rgba(0, 0, 0, 0.2);
} }
.sidebar-video-badge.rating-nsfw { .sidebar-video-badge.rating-nsfw {
background-color: var(--badge-nsfw); background-color: var(--badge-nsfw, #E10DC3);
border: 1px solid rgba(0, 0, 0, 0.2); border: 1px solid rgba(0, 0, 0, 0.2);
} }
.sidebar-video-badge.rating-nsfl { .sidebar-video-badge.rating-nsfl {
background-color: var(--badge-nsfl); background-color: var(--badge-nsfl, #660000);
border: 1px solid rgba(0, 0, 0, 0.2); border: 1px solid rgba(0, 0, 0, 0.2);
} }
@@ -3167,6 +3124,7 @@ body.sidebar-right-hidden #sidebar-drag-zone {
backdrop-filter: blur(4px); backdrop-filter: blur(4px);
border: 1px solid rgba(255, 255, 255, 0.15); border: 1px solid rgba(255, 255, 255, 0.15);
line-height: 1; line-height: 1;
z-index: 10;
} }
.sidebar-media-placeholder { .sidebar-media-placeholder {
@@ -3313,11 +3271,6 @@ html[theme='paper'] .sidebar-video-channel {
color: #64748b; color: #64748b;
} }
html[theme='light'] .sidebar-video-play-overlay,
html[theme='paper'] .sidebar-video-play-overlay {
background: rgba(0, 0, 0, 0.2);
}
/* Theme 95 */ /* Theme 95 */
html[theme='95'] .sidebar-tabs { html[theme='95'] .sidebar-tabs {
background: #c0c0c0; background: #c0c0c0;
@@ -3346,6 +3299,68 @@ html[theme='95'] .sidebar-video-title {
font-family: 'MS Sans Serif', sans-serif; font-family: 'MS Sans Serif', sans-serif;
} }
/* Sidebar Tag Feed Header & Active Card Highlighting */
.sidebar-tag-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px 12px;
background: rgba(18, 18, 24, 0.95);
backdrop-filter: blur(8px);
border-bottom: 1px solid var(--border-color, rgba(255, 255, 255, 0.08));
margin-bottom: 6px;
position: sticky;
top: -6px;
z-index: 10;
}
.sidebar-tag-title {
display: flex;
align-items: center;
gap: 6px;
font-size: 0.85em;
font-weight: 700;
color: var(--accent, #a78bfa);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.sidebar-tag-title i {
opacity: 0.85;
}
.sidebar-tag-count {
font-size: 0.85em;
font-weight: 500;
color: var(--text-muted, #888);
margin-left: 4px;
}
.sidebar-tag-order-btn {
background: rgba(255, 255, 255, 0.06);
border: 1px solid var(--border-color, rgba(255, 255, 255, 0.1));
color: var(--text-color, #ccc);
border-radius: 4px;
padding: 3px 8px;
font-size: 0.82em;
cursor: pointer;
display: flex;
align-items: center;
gap: 5px;
transition: all 0.15s ease;
}
.sidebar-tag-order-btn:hover {
background: rgba(255, 255, 255, 0.12);
color: #fff;
border-color: var(--accent, #a78bfa);
}
.sidebar-video-card.active-tag-item {
border-color: var(--accent, #a78bfa) !important;
}
#sidebar-activity-container.sidebar-comments-list { #sidebar-activity-container.sidebar-comments-list {
flex: 1; flex: 1;
overflow-y: auto; overflow-y: auto;
@@ -5646,11 +5661,16 @@ div.posts {
margin-bottom: 35px; margin-bottom: 35px;
} }
/* Mobile: Ensure at least 3 columns unless user picked a larger size */ /* Mobile: Ensure at least 3 columns for S/M, and at least 2 columns for L/XL */
@media (max-width: 600px) { @media (max-width: 600px) {
html:not([data-thumb-size="l"]):not([data-thumb-size="xl"]) div.posts { html:not([data-thumb-size="l"]):not([data-thumb-size="xl"]) div.posts {
grid-template-columns: repeat(3, 1fr); grid-template-columns: repeat(3, 1fr);
} }
html[data-thumb-size="l"] div.posts,
html[data-thumb-size="xl"] div.posts {
grid-template-columns: repeat(2, 1fr);
}
} }
div.posts>a:not(.notif-item) { div.posts>a:not(.notif-item) {
@@ -6476,6 +6496,28 @@ body.layout-modern.sidebar-right-hidden .pagination-container-fluid {
overflow: visible; overflow: visible;
} }
.item_title:has(.info-title-edit-wrap) .item_title_text {
cursor: pointer;
display: inline-block;
max-width: 100%;
transition: color 0.15s ease, opacity 0.15s ease;
}
.item_title:has(.info-title-edit-wrap) .item_title_text:hover {
color: #fff;
}
.item_title_text.no-title,
.item_title_text em {
font-style: italic;
color: var(--text-muted, rgba(255, 255, 255, 0.45));
}
.item_title:has(.info-title-edit-wrap) .item_title_text.no-title:hover em,
.item_title:has(.info-title-edit-wrap) .item_title_text:hover em {
color: var(--accent, #9f0);
}
/* .location { /* .location {
padding-left: 5px; padding-left: 5px;
} */ } */
@@ -7339,7 +7381,7 @@ span#tags:not(.tags-expanded) .tags-inner>span:nth-child(n+11) {
.badge-success { .badge-success {
color: var(--white); color: var(--white);
background-color: var(--badge-sfw); background-color: var(--badge-sfw, #02500b);
padding-right: 5px; padding-right: 5px;
padding-left: 5px; padding-left: 5px;
border-radius: 3px; border-radius: 3px;
@@ -7349,7 +7391,7 @@ span#tags:not(.tags-expanded) .tags-inner>span:nth-child(n+11) {
.badge-danger { .badge-danger {
color: var(--white); color: var(--white);
background-color: var(--badge-nsfw); background-color: var(--badge-nsfw, #E10DC3);
padding-right: 5px; padding-right: 5px;
padding-left: 5px; padding-left: 5px;
border-radius: 3px; border-radius: 3px;
@@ -12982,7 +13024,8 @@ div.posts>a.thumb[data-size="2"] {
grid-row: span 2; grid-row: span 2;
} }
.thumb>.preview-video { .thumb>.preview-video,
.sidebar-video-thumb-wrap>.preview-video {
position: absolute; position: absolute;
top: 0; top: 0;
left: 0; left: 0;
@@ -12996,7 +13039,8 @@ div.posts>a.thumb[data-size="2"] {
transition: opacity 0.2s ease-in; transition: opacity 0.2s ease-in;
} }
.thumb>.preview-video.playing { .thumb>.preview-video.playing,
.sidebar-video-thumb-wrap>.preview-video.playing {
opacity: 1; opacity: 1;
} }
@@ -13562,17 +13606,17 @@ body.layout-modern>.pagewrapper:not(:has(.index-layout-wrapper)):not(:has(.item-
} }
#a_toggle.rating-btn.is-sfw { #a_toggle.rating-btn.is-sfw {
background-color: var(--badge-sfw); background-color: var(--badge-sfw, #02500b);
border-color: rgba(0, 0, 0, 0.15); border-color: rgba(0, 0, 0, 0.15);
} }
#a_toggle.rating-btn.is-nsfw { #a_toggle.rating-btn.is-nsfw {
background-color: var(--badge-nsfw); background-color: var(--badge-nsfw, #E10DC3);
border-color: rgba(0, 0, 0, 0.15); border-color: rgba(0, 0, 0, 0.15);
} }
#a_toggle.rating-btn.is-nsfl { #a_toggle.rating-btn.is-nsfl {
background-color: var(--badge-nsfl); background-color: var(--badge-nsfl, #660000);
border-color: rgba(0, 0, 0, 0.15); border-color: rgba(0, 0, 0, 0.15);
} }
+6 -6
View File
@@ -404,21 +404,21 @@
} }
.item-rating-option input:checked + .item-rating-label.sfw { .item-rating-option input:checked + .item-rating-label.sfw {
background: var(--badge-sfw); background: var(--badge-sfw, #02500b);
color: #fff; color: #fff;
border-color: var(--badge-sfw); border-color: var(--badge-sfw, #02500b);
} }
.item-rating-option input:checked + .item-rating-label.nsfw { .item-rating-option input:checked + .item-rating-label.nsfw {
background: var(--badge-nsfw); background: var(--badge-nsfw, #E10DC3);
color: #fff; color: #fff;
border-color: var(--badge-nsfw); border-color: var(--badge-nsfw, #E10DC3);
} }
.item-rating-option input:checked + .item-rating-label.nsfl { .item-rating-option input:checked + .item-rating-label.nsfl {
background: var(--badge-nsfl); background: var(--badge-nsfl, #660000);
color: #fff; color: #fff;
border-color: var(--badge-nsfl); border-color: var(--badge-nsfl, #660000);
} }
/* Visibility Container - Only checked option is colored, unchecked options are gray */ /* Visibility Container - Only checked option is colored, unchecked options are gray */
+109 -14
View File
@@ -3340,8 +3340,18 @@ window.cancelAnimFrame = (function () {
// Full page: Sync attributes (CRITICAL for fullscreen and theme state) // Full page: Sync attributes (CRITICAL for fullscreen and theme state)
['theme', 'res'].forEach(attr => { ['theme', 'res'].forEach(attr => {
const val = doc.documentElement.getAttribute(attr); const val = doc.documentElement.getAttribute(attr);
if (attr === 'theme') {
if (val && val.trim()) {
document.documentElement.setAttribute('theme', val.trim());
} else if (!document.documentElement.getAttribute('theme')) {
const cookieTheme = document.cookie.match(/(?:^|; )theme=([^;]*)/)?.[1];
const fallback = (cookieTheme && decodeURIComponent(cookieTheme)) || window.f0ckDefaultTheme || 'amoled';
document.documentElement.setAttribute('theme', fallback);
}
} else {
if (val !== null) document.documentElement.setAttribute(attr, val); if (val !== null) document.documentElement.setAttribute(attr, val);
else document.documentElement.removeAttribute(attr); else document.documentElement.removeAttribute(attr);
}
}); });
// Sync body classes (CRITICAL for layout triggers) // Sync body classes (CRITICAL for layout triggers)
@@ -4837,6 +4847,12 @@ window.cancelAnimFrame = (function () {
loadItemAjax(`/user/${encodeURIComponent(wUserHallOwner)}/hall/${encodeURIComponent(wUserHall)}/${targetKey}`, true, { transition: 'fade-zoom' }); loadItemAjax(`/user/${encodeURIComponent(wUserHallOwner)}/hall/${encodeURIComponent(wUserHall)}/${targetKey}`, true, { transition: 'fade-zoom' });
} else if (wFavsUser) { } else if (wFavsUser) {
loadItemAjax(`/user/${encodeURIComponent(wFavsUser)}/favs/${targetKey}`, true, { transition: 'fade-zoom' }); loadItemAjax(`/user/${encodeURIComponent(wFavsUser)}/favs/${targetKey}`, true, { transition: 'fade-zoom' });
} else if (wTagMatch) {
const wTag = decodeURIComponent(wTagMatch[1]);
loadItemAjax(`/tag/${encodeURIComponent(wTag).replace(/%2C/g, ',').replace(/%20/g, ' ')}/${targetKey}`, true, { transition: 'fade-zoom' });
} else if (wHallMatch) {
const wHall = decodeURIComponent(wHallMatch[1]);
loadItemAjax(`/h/${encodeURIComponent(wHall).replace(/%20/g, ' ')}/${targetKey}`, true, { transition: 'fade-zoom' });
} else { } else {
loadItemAjax(`/${targetKey}`, true, { transition: 'fade-zoom' }); loadItemAjax(`/${targetKey}`, true, { transition: 'fade-zoom' });
} }
@@ -10369,6 +10385,8 @@ document.addEventListener("DOMContentLoaded", function () {
// Sync UI immediately (Before AJAX) // Sync UI immediately (Before AJAX)
if (window.updateMimeLabel) window.updateMimeLabel(); if (window.updateMimeLabel) window.updateMimeLabel();
document.dispatchEvent(new CustomEvent('f0ck:mimeChanged', { detail: { mime: newMime } }));
// Trigger reload context-aware // Trigger reload context-aware
const fromModal = !!menu.closest('#excluded-tags-overlay'); const fromModal = !!menu.closest('#excluded-tags-overlay');
const postsEl = document.querySelector('.posts, .tags-grid'); const postsEl = document.querySelector('.posts, .tags-grid');
@@ -10656,6 +10674,8 @@ document.addEventListener('DOMContentLoaded', () => {
if (activeThumb) { if (activeThumb) {
activeThumb.classList.remove('previewing'); activeThumb.classList.remove('previewing');
activeThumb.classList.remove('touch-active'); activeThumb.classList.remove('touch-active');
const card = activeThumb.closest('.sidebar-video-card');
if (card) card.classList.remove('previewing');
activeThumb = null; activeThumb = null;
} }
}; };
@@ -10677,7 +10697,7 @@ document.addEventListener('DOMContentLoaded', () => {
if (mode === 'nsfw') shouldBlurThis = blurNsfw; if (mode === 'nsfw') shouldBlurThis = blurNsfw;
else if (mode === 'nsfl') shouldBlurThis = blurNsfl; else if (mode === 'nsfl') shouldBlurThis = blurNsfl;
else if (mode === 'sfw') shouldBlurThis = blurSfw; else if (mode === 'sfw') shouldBlurThis = blurSfw;
else if (mode === 'null' || !mode) shouldBlurThis = blurUntagged; else if (mode === 'null' || mode === 'untagged' || !mode) shouldBlurThis = blurUntagged;
if (shouldBlurThis && !thumb.classList.contains('revealed')) { if (shouldBlurThis && !thumb.classList.contains('revealed')) {
return; return;
@@ -10700,6 +10720,8 @@ document.addEventListener('DOMContentLoaded', () => {
if (thumb !== activeThumb) return; if (thumb !== activeThumb) return;
activeThumb.classList.add('previewing'); activeThumb.classList.add('previewing');
const card = activeThumb.closest('.sidebar-video-card');
if (card) card.classList.add('previewing');
const video = document.createElement('video'); const video = document.createElement('video');
video.src = `/b/${file}`; video.src = `/b/${file}`;
@@ -10716,11 +10738,11 @@ document.addEventListener('DOMContentLoaded', () => {
video.classList.add('playing'); video.classList.add('playing');
}; };
thumb.appendChild(video); const container = thumb.classList.contains('sidebar-video-thumb-wrap')
? thumb
: (thumb.querySelector('.sidebar-video-thumb-wrap') || thumb);
container.appendChild(video);
activeVideo = video; activeVideo = video;
// Note: Hover prefetching of the first 512 KB has been removed to prevent
// Chromium cache locks and validation loop stalls on subsequent playback.
}; };
if (delay === 0) { if (delay === 0) {
@@ -10732,7 +10754,14 @@ document.addEventListener('DOMContentLoaded', () => {
window.startHoverPreview = startPreview; window.startHoverPreview = startPreview;
const handleEnter = (e) => { const handleEnter = (e) => {
const thumb = e.target.closest('.thumb'); let thumb = e.target.closest('.thumb');
if (!thumb) {
// The playback can ONLY start when exactly hovering class="sidebar-video-thumb"
const imgThumb = e.target.closest('.sidebar-video-thumb');
if (imgThumb) {
thumb = imgThumb.closest('.sidebar-video-thumb-wrap');
}
}
if (thumb && thumb !== activeThumb) { if (thumb && thumb !== activeThumb) {
startPreview(thumb, 150); startPreview(thumb, 150);
} }
@@ -10741,7 +10770,7 @@ document.addEventListener('DOMContentLoaded', () => {
document.addEventListener('focusin', handleEnter); document.addEventListener('focusin', handleEnter);
const handleLeave = (e) => { const handleLeave = (e) => {
const thumb = e.target.closest('.thumb'); const thumb = e.target.closest('.thumb, .sidebar-video-thumb-wrap');
if (thumb) { if (thumb) {
if (e.type === 'mouseout' && e.relatedTarget && thumb.contains(e.relatedTarget)) return; if (e.type === 'mouseout' && e.relatedTarget && thumb.contains(e.relatedTarget)) return;
clearPreview(); clearPreview();
@@ -10757,7 +10786,13 @@ document.addEventListener('DOMContentLoaded', () => {
let touchBlockClick = false; let touchBlockClick = false;
document.addEventListener('touchstart', (e) => { document.addEventListener('touchstart', (e) => {
const thumb = e.target.closest('.thumb'); let thumb = e.target.closest('.thumb');
if (!thumb) {
const imgThumb = e.target.closest('.sidebar-video-thumb');
if (imgThumb) {
thumb = imgThumb.closest('.sidebar-video-thumb-wrap');
}
}
if (thumb) { if (thumb) {
// Only apply logic for items we want to preview // Only apply logic for items we want to preview
@@ -10782,7 +10817,7 @@ document.addEventListener('DOMContentLoaded', () => {
// Capture phase click listener to stop navigation if we just activated // Capture phase click listener to stop navigation if we just activated
document.addEventListener('click', (e) => { document.addEventListener('click', (e) => {
if (touchBlockClick) { if (touchBlockClick) {
const thumb = e.target.closest('.thumb'); const thumb = e.target.closest('.thumb, .sidebar-video-thumb-wrap');
if (thumb && thumb === activeThumb) { if (thumb && thumb === activeThumb) {
e.preventDefault(); e.preventDefault();
e.stopImmediatePropagation(); e.stopImmediatePropagation();
@@ -12768,6 +12803,40 @@ document.addEventListener('click', (e) => {
return; return;
} }
// Title text click to edit
const titleText = e.target.closest('.item_title_text');
if (titleText && !document.body.classList.contains('preview-as-user')) {
const container = titleText.closest('.item_title');
if (container) {
const editWrap = container.querySelector('.info-title-edit-wrap');
const input = container.querySelector('#info-title-input');
if (editWrap && input) {
e.preventDefault();
titleText.style.display = 'none';
editWrap.style.display = 'flex';
input.dataset.origVal = input.value;
input.focus();
if (input.value) {
input.select();
}
return;
}
}
}
// Close title editor when clicking outside .item_title
const openTitleWrap = document.querySelector('.item_title .info-title-edit-wrap');
if (openTitleWrap && openTitleWrap.style.display !== 'none' && !e.target.closest('.item_title')) {
const container = openTitleWrap.closest('.item_title');
const textEl = container?.querySelector('.item_title_text');
const input = container?.querySelector('#info-title-input');
openTitleWrap.style.display = 'none';
if (textEl) textEl.style.display = '';
if (input && input.dataset.origVal !== undefined) {
input.value = input.dataset.origVal;
}
}
// Title save button // Title save button
const saveBtn = e.target.closest('#info-title-save'); const saveBtn = e.target.closest('#info-title-save');
if (saveBtn) { if (saveBtn) {
@@ -12798,13 +12867,26 @@ document.addEventListener('click', (e) => {
saveBtn.disabled = false; saveBtn.disabled = false;
saveBtn.innerHTML = origIcon; saveBtn.innerHTML = origIcon;
if (data.success) { if (data.success) {
// Keep input form intact and update its value const savedTitle = (data.title || '').trim();
input.value = data.title || ''; input.value = savedTitle;
input.dataset.origVal = savedTitle;
// Update any plain-text title display element // Update any plain-text title display element
const textEl = container.querySelector('.item_title_text') || document.querySelector('.item_title_text'); const textEl = container.querySelector('.item_title_text') || document.querySelector('.item_title_text');
if (textEl) { if (textEl) {
textEl.textContent = data.title || ''; if (savedTitle) {
textEl.textContent = savedTitle;
textEl.classList.remove('no-title');
} else {
textEl.innerHTML = '<em>Add a title</em>';
textEl.classList.add('no-title');
}
textEl.style.display = '';
}
const editWrap = container.querySelector('.info-title-edit-wrap');
if (editWrap) {
editWrap.style.display = 'none';
} }
// Invalidate cache so navigating next/prev fetches fresh HTML from server // Invalidate cache so navigating next/prev fetches fresh HTML from server
@@ -12977,13 +13059,26 @@ document.addEventListener('click', (e) => {
} }
}); });
// Handle Enter key on title input // Handle Enter and Escape key on title input
document.addEventListener('keydown', (e) => { document.addEventListener('keydown', (e) => {
if (e.key === 'Enter' && e.target && e.target.id === 'info-title-input') { if (e.target && e.target.id === 'info-title-input') {
if (e.key === 'Enter') {
e.preventDefault(); e.preventDefault();
const container = e.target.closest('.item_title') || document; const container = e.target.closest('.item_title') || document;
const saveBtn = container.querySelector('#info-title-save') || document.getElementById('info-title-save'); const saveBtn = container.querySelector('#info-title-save') || document.getElementById('info-title-save');
if (saveBtn) saveBtn.click(); if (saveBtn) saveBtn.click();
} else if (e.key === 'Escape') {
e.preventDefault();
const container = e.target.closest('.item_title') || document;
const editWrap = container.querySelector('.info-title-edit-wrap');
const textEl = container.querySelector('.item_title_text');
const input = e.target;
if (editWrap) editWrap.style.display = 'none';
if (textEl) textEl.style.display = '';
if (input && input.dataset.origVal !== undefined) {
input.value = input.dataset.origVal;
}
}
} }
}); });
+446 -22
View File
@@ -1030,26 +1030,53 @@
let lastBoundMode = typeof window.activeMode !== 'undefined' ? window.activeMode : null; let lastBoundMode = typeof window.activeMode !== 'undefined' ? window.activeMode : null;
const getCurrentMimeFilter = () => {
const urlParams = new URLSearchParams(window.location.search);
const qMime = urlParams.get('mime');
if (qMime !== null) return qMime.trim();
const cookieMime = document.cookie.split('; ').find(row => row.startsWith('mime='));
if (cookieMime) {
const val = cookieMime.split('=')[1];
if (typeof val === 'string') return decodeURIComponent(val).trim();
}
return '';
};
let lastBoundMime = getCurrentMimeFilter();
// Handle AJAX item loads // Handle AJAX item loads
document.addEventListener('f0ck:contentLoaded', () => { document.addEventListener('f0ck:contentLoaded', () => {
const currentMode = typeof window.activeMode !== 'undefined' ? window.activeMode : null; const currentMode = typeof window.activeMode !== 'undefined' ? window.activeMode : null;
const modeChanged = lastBoundMode !== null && lastBoundMode !== currentMode; const modeChanged = lastBoundMode !== null && lastBoundMode !== currentMode;
lastBoundMode = currentMode; lastBoundMode = currentMode;
window.f0ckDebug("Sidebar Activity: Page transition detected", modeChanged ? "(Mode changed)" : ""); const currentMime = getCurrentMimeFilter();
const mimeChanged = lastBoundMime !== null && lastBoundMime !== currentMime;
lastBoundMime = currentMime;
window.f0ckDebug("Sidebar Activity: Page transition detected", modeChanged ? "(Mode changed)" : "", mimeChanged ? "(Mime changed)" : "");
if (modeChanged || mimeChanged) {
if (modeChanged) { if (modeChanged) {
window._sidebarActivityCache = []; window._sidebarActivityCache = [];
lastRenderedIds = ''; lastRenderedIds = '';
currentPage = 1; currentPage = 1;
hasMore = true; hasMore = true;
loadActivity(false); // Force reload with loading state loadActivity(false); // Force reload with loading state
}
recommendationsLoaded = false; recommendationsLoaded = false;
tagFeedLoaded = false;
tagFeedNeedsReload = true;
const activeTabEl = document.querySelector('.sidebar-tab.active')?.dataset.tab;
let savedTab = 'comments'; let savedTab = 'comments';
try { savedTab = localStorage.getItem('sidebar_active_tab'); } catch (_) {} try { savedTab = localStorage.getItem('sidebar_active_tab'); } catch (_) {}
if (savedTab === 'videos') { const currentTabName = activeTabEl || savedTab;
loadVideoRecommendations(false);
if (currentTabName === 'videos' || currentTabName === 'recommendations') {
loadRecommendations(false);
} else if (currentTabName === 'tag' && currentTag) {
loadTagFeed(true);
} }
} else { } else {
// Immediately render from cache to avoid flicker // Immediately render from cache to avoid flicker
@@ -1059,6 +1086,27 @@
} }
}); });
document.addEventListener('f0ck:mimeChanged', (e) => {
const newMime = (e.detail && typeof e.detail.mime !== 'undefined') ? e.detail.mime : getCurrentMimeFilter();
window.f0ckDebug("Sidebar Activity: MIME filter changed", newMime);
lastBoundMime = newMime;
recommendationsLoaded = false;
tagFeedLoaded = false;
tagFeedNeedsReload = true;
const activeTabEl = document.querySelector('.sidebar-tab.active')?.dataset.tab;
let savedTab = 'comments';
try { savedTab = localStorage.getItem('sidebar_active_tab'); } catch (_) {}
const currentTabName = activeTabEl || savedTab;
if (currentTabName === 'videos' || currentTabName === 'recommendations') {
loadRecommendations(false);
} else if (currentTabName === 'tag' && currentTag) {
loadTagFeed(true);
}
});
// Sync sidebar and comments-list layout on initial page load (Legacy View Only) // Sync sidebar and comments-list layout on initial page load (Legacy View Only)
if (typeof syncSidebarAndComments === 'function') { if (typeof syncSidebarAndComments === 'function') {
syncSidebarAndComments(); syncSidebarAndComments();
@@ -1074,7 +1122,7 @@
let recIoSentinel = null; let recIoSentinel = null;
let recObserver = null; let recObserver = null;
const renderVideoCard = (video) => { const renderVideoCard = (video, options = {}) => {
const videoKey = (window.f0ckSession?.enable_item_slugs && video.slug) ? video.slug : video.id; const videoKey = (window.f0ckSession?.enable_item_slugs && video.slug) ? video.slug : video.id;
const rClass = video.rating_class || 'untagged'; const rClass = video.rating_class || 'untagged';
const blurNsfw = localStorage.getItem('blurNsfw') === 'true'; const blurNsfw = localStorage.getItem('blurNsfw') === 'true';
@@ -1140,7 +1188,7 @@
`; `;
} else { } else {
thumbContentHtml = ` thumbContentHtml = `
<img src="${thumbUrl}" class="sidebar-video-thumb" alt="${escapeHtml(displayTitle)}" loading="lazy" onerror="this.style.display='none'; if(this.nextElementSibling) this.nextElementSibling.classList.remove('hidden');" /> <img src="${thumbUrl}" class="sidebar-video-thumb" alt="${escapeHtml(displayTitle)}" loading="lazy" draggable="false" onerror="this.style.display='none'; if(this.nextElementSibling) this.nextElementSibling.classList.remove('hidden');" />
<div class="sidebar-media-placeholder ${isAudio ? 'audio' : ''} hidden"> <div class="sidebar-media-placeholder ${isAudio ? 'audio' : ''} hidden">
<i class="${overlayIcon}"></i> <i class="${overlayIcon}"></i>
</div> </div>
@@ -1151,12 +1199,18 @@
? `<span class="sidebar-personalized-pill" title="Personalized recommendation based on your interests"><i class="fa-solid fa-wand-magic-sparkles"></i> For You</span>` ? `<span class="sidebar-personalized-pill" title="Personalized recommendation based on your interests"><i class="fa-solid fa-wand-magic-sparkles"></i> For You</span>`
: ''; : '';
const isTagFeed = options.isTagFeed || false;
const tagContext = options.tag || null;
const targetHref = (isTagFeed && tagContext)
? `/tag/${encodeURIComponent(tagContext).replace(/%2C/g, ',').replace(/%20/g, ' ')}/${videoKey}`
: `/${videoKey}`;
const activeClass = options.isActive ? ' active-tag-item' : '';
return ` return `
<div class="sidebar-video-card" data-id="${video.id}" data-slug="${escapeHtml(video.slug || '')}" data-personalized="${video.personalized ? 'true' : 'false'}"> <div class="sidebar-video-card${activeClass}" data-id="${video.id}" data-slug="${escapeHtml(video.slug || '')}" data-file="${escapeHtml(video.dest || '')}" data-mime="${escapeHtml(video.mime || '')}" data-ext="${escapeHtml(ext ? ext.toLowerCase() : '')}" data-mode="${rClass}" data-personalized="${video.personalized ? 'true' : 'false'}">
<a href="/${videoKey}" class="sidebar-video-link" data-mode="${rClass}" data-inherit-context="false"> <a href="${targetHref}" class="sidebar-video-link" data-mode="${rClass}" data-inherit-context="false">
<div class="sidebar-video-thumb-wrap"> <div class="sidebar-video-thumb-wrap" data-file="${escapeHtml(video.dest || '')}" data-mime="${escapeHtml(video.mime || '')}" data-ext="${escapeHtml(ext ? ext.toLowerCase() : '')}" data-mode="${rClass}">
${thumbContentHtml} ${thumbContentHtml}
<div class="sidebar-video-play-overlay"><i class="${overlayIcon}"></i></div>
${formatBadge} ${formatBadge}
<span class="sidebar-video-badge rating-${rClass}">${rClass.toUpperCase()}</span> <span class="sidebar-video-badge rating-${rClass}">${rClass.toUpperCase()}</span>
</div> </div>
@@ -1215,7 +1269,9 @@
seenRecommendationIds.clear(); seenRecommendationIds.clear();
const mode = typeof window.activeMode !== 'undefined' ? window.activeMode : ''; const mode = typeof window.activeMode !== 'undefined' ? window.activeMode : '';
const affParams = getSessionAffinityParams(); const affParams = getSessionAffinityParams();
const res = await fetch(`/api/v2/recommendations?limit=${RECOMMENDATIONS_LIMIT}&mode=${mode}${affParams}`, { const mime = getCurrentMimeFilter();
const mimeParam = mime ? `&mime=${encodeURIComponent(mime)}` : '';
const res = await fetch(`/api/v2/recommendations?limit=${RECOMMENDATIONS_LIMIT}&mode=${mode}${mimeParam}${affParams}`, {
headers: { 'X-Requested-With': 'XMLHttpRequest' } headers: { 'X-Requested-With': 'XMLHttpRequest' }
}); });
const data = await res.json(); const data = await res.json();
@@ -1272,7 +1328,9 @@
const mode = typeof window.activeMode !== 'undefined' ? window.activeMode : ''; const mode = typeof window.activeMode !== 'undefined' ? window.activeMode : '';
const excludeArr = Array.from(seenRecommendationIds).slice(-100); const excludeArr = Array.from(seenRecommendationIds).slice(-100);
const affParams = getSessionAffinityParams(); const affParams = getSessionAffinityParams();
const res = await fetch(`/api/v2/recommendations?limit=15&mode=${mode}&exclude_ids=${excludeArr.join(',')}${affParams}`, { const mime = getCurrentMimeFilter();
const mimeParam = mime ? `&mime=${encodeURIComponent(mime)}` : '';
const res = await fetch(`/api/v2/recommendations?limit=15&mode=${mode}${mimeParam}&exclude_ids=${excludeArr.join(',')}${affParams}`, {
headers: { 'X-Requested-With': 'XMLHttpRequest' } headers: { 'X-Requested-With': 'XMLHttpRequest' }
}); });
const data = await res.json(); const data = await res.json();
@@ -1349,13 +1407,335 @@
const loadVideoRecommendations = loadRecommendations; const loadVideoRecommendations = loadRecommendations;
// ── Dedicated Tag Feed (Ordered for Tag Views) ───────────────────────
let currentTag = null;
let tagOrder = 'desc'; // 'desc' = newest first (default), 'asc' = oldest first (chronological)
let tagOffset = 0;
let tagTotal = 0;
let tagFeedLoaded = false;
let tagFeedLoading = false;
let tagFeedNeedsReload = false;
let tagHasMore = false;
let tagSentinel = null;
let tagObserver = null;
const getCurrentTag = () => {
const pathMatch = window.location.pathname.match(/^\/tag\/([^/?#]+)/);
if (pathMatch) return decodeURIComponent(pathMatch[1]);
const searchParams = new URLSearchParams(window.location.search);
if (searchParams.get('tag')) return searchParams.get('tag');
if (window.currentTag) return window.currentTag;
return null;
};
const getCurrentItemIdentifiers = () => {
const ids = new Set();
// 1. From DOM elements on the page (primary/direct)
const idElem = document.querySelector('[data-item-id]');
if (idElem && idElem.dataset.itemId) {
ids.add(String(idElem.dataset.itemId).trim());
}
const idLink = document.querySelector('.id-link');
if (idLink) {
if (idLink.dataset.itemId) ids.add(String(idLink.dataset.itemId).trim());
const txt = idLink.textContent.trim();
if (txt) ids.add(txt);
}
// 2. From URL pathname (/tag/:tag/:slugOrId or /:slugOrId)
const segments = window.location.pathname.split('/').filter(Boolean);
if (segments.length >= 2 && segments[0] === 'tag') {
if (segments[2]) {
ids.add(decodeURIComponent(segments[2]).trim());
}
} else if (segments.length === 1) {
const forbidden = ['s', 'b', 't', 'ca', 'a', 'login', 'register', 'settings', 'about', 'terms', 'rules', 'api', 'logout', 'auth', 'admin', 'mod', 'comments', 'notifications', 'feed', 'upload', 'tags', 'halls', 'ranking', 'abyss', 'random', 'scroller', 'p'];
if (!forbidden.includes(segments[0])) {
ids.add(decodeURIComponent(segments[0]).trim());
}
}
return Array.from(ids).filter(Boolean);
};
const getCurrentItemKey = () => {
const ids = getCurrentItemIdentifiers();
return ids.length > 0 ? ids[0] : null;
};
let isReCenteringFeed = false;
const highlightActiveTagCard = (scrollIntoView = false) => {
const identifiers = getCurrentItemIdentifiers();
const container = document.getElementById('sidebar-tag-container');
if (!container) return;
let matchedCard = null;
container.querySelectorAll('.sidebar-video-card').forEach(card => {
const cardId = card.dataset.id ? String(card.dataset.id).trim() : '';
const cardSlug = card.dataset.slug ? String(card.dataset.slug).trim() : '';
const isMatch = identifiers.length > 0 && (
(cardId && identifiers.includes(cardId)) ||
(cardSlug && identifiers.includes(cardSlug))
);
if (isMatch) {
card.classList.add('active-tag-item');
if (!matchedCard) matchedCard = card;
} else {
card.classList.remove('active-tag-item');
}
});
if (matchedCard && scrollIntoView) {
const isTagTabActive = document.querySelector('.sidebar-tab.active')?.dataset.tab === 'tag' &&
container.style.display !== 'none';
if (isTagTabActive) {
try {
const containerRect = container.getBoundingClientRect();
const cardRect = matchedCard.getBoundingClientRect();
const targetScrollTop = container.scrollTop + (cardRect.top - containerRect.top) - (container.clientHeight / 2) + (cardRect.height / 2);
container.scrollTo({ top: Math.max(0, targetScrollTop), behavior: 'smooth' });
} catch (_) {
matchedCard.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
}
} else if (!matchedCard && identifiers.length > 0 && currentTag && !isReCenteringFeed && !tagFeedLoading) {
// Active item belongs to current tag context but is not in currently rendered slice
// (e.g. after pressing Random in tag). Center feed around this active item!
isReCenteringFeed = true;
loadTagFeed(true, identifiers[0]).finally(() => {
isReCenteringFeed = false;
});
}
};
let userManuallySelectedNonTagTab = false;
const updateTagTabVisibility = (forceSwitchToTag = false) => {
const tag = getCurrentTag();
const tagBtn = document.getElementById('sidebar-tab-tag');
if (!tagBtn) return;
if (tag) {
tagBtn.style.display = 'inline-flex';
tagBtn.title = `#${tag}`;
tagBtn.setAttribute('aria-label', `#${tag}`);
const titleNameEl = document.querySelector('.sidebar-tag-name');
if (titleNameEl) titleNameEl.textContent = `#${tag}`;
if (currentTag !== tag) {
currentTag = tag;
tagOffset = 0;
tagFeedLoaded = false;
tagFeedNeedsReload = true;
userManuallySelectedNonTagTab = false;
// When entering or switching tag view (e.g. /tag/cat), automatically jump to the tag tab
switchSidebarTab('tag');
} else {
if (forceSwitchToTag || (!userManuallySelectedNonTagTab && document.querySelector('.sidebar-tab.active')?.dataset.tab !== 'tag')) {
switchSidebarTab('tag');
} else {
highlightActiveTagCard(true);
}
}
} else {
tagBtn.style.display = 'none';
currentTag = null;
tagFeedLoaded = false;
userManuallySelectedNonTagTab = false;
const activeTab = document.querySelector('.sidebar-tab.active')?.dataset.tab;
if (activeTab === 'tag') {
switchSidebarTab('recommendations');
}
}
};
const loadTagFeed = async (reset = false, focusId = null) => {
const container = document.getElementById('sidebar-tag-container');
const itemsContainer = container?.querySelector('.sidebar-tag-items-container');
if (!container || !itemsContainer || tagFeedLoading) return;
if (!currentTag) return;
tagFeedLoading = true;
tagFeedNeedsReload = false;
if (reset) {
tagOffset = 0;
itemsContainer.innerHTML = `
<div class="sidebar-loading-state">
<i class="fa-solid fa-circle-notch fa-spin"></i>
<span>Loading #${escapeHtml(currentTag)} items...</span>
</div>
`;
}
try {
const mode = typeof window.activeMode !== 'undefined' ? window.activeMode : '';
const mime = getCurrentMimeFilter();
let url = `/api/v2/tag-feed?tag=${encodeURIComponent(currentTag)}&order=${tagOrder}&offset=${tagOffset}&limit=20&mode=${mode}`;
if (mime) {
url += `&mime=${encodeURIComponent(mime)}`;
}
if (focusId) {
url += `&focus_id=${encodeURIComponent(focusId)}`;
}
const res = await fetch(url, {
headers: { 'X-Requested-With': 'XMLHttpRequest' }
});
const data = await res.json();
const items = data.items || [];
tagTotal = data.total || 0;
tagOffset = (data.offset ?? tagOffset) + items.length;
tagHasMore = typeof data.hasMore !== 'undefined' ? !!data.hasMore : (tagOffset < tagTotal);
// Update counter in header
const countEl = container.querySelector('.sidebar-tag-count');
if (countEl) countEl.textContent = `(${tagTotal})`;
// Update order button icon and tooltip
const orderBtn = container.querySelector('.sidebar-tag-order-btn');
if (orderBtn) {
if (tagOrder === 'asc') {
orderBtn.innerHTML = '<i class="fa-solid fa-arrow-down-1-9"></i>';
orderBtn.title = 'Chronological (Oldest first) — Click for Newest first';
} else {
orderBtn.innerHTML = '<i class="fa-solid fa-arrow-down-9-1"></i>';
orderBtn.title = 'Newest first (default) — Click for Oldest first';
}
}
if (data.success && items.length > 0) {
tagFeedLoaded = true;
const identifiers = getCurrentItemIdentifiers();
let html = '';
items.forEach(v => {
const vId = String(v.id).trim();
const vSlug = String(v.slug || '').trim();
const isActive = identifiers.length > 0 && (
(vId && identifiers.includes(vId)) ||
(vSlug && identifiers.includes(vSlug))
);
html += renderVideoCard(v, { isTagFeed: true, tag: currentTag, isActive });
});
if (reset) {
itemsContainer.innerHTML = html;
setTimeout(() => highlightActiveTagCard(true), 60);
} else {
itemsContainer.insertAdjacentHTML('beforeend', html);
highlightActiveTagCard(false);
}
initTagSentinel();
} else if (reset) {
itemsContainer.innerHTML = `
<div class="sidebar-empty-state" style="text-align:center;padding:30px 15px;color:var(--text-muted,#888);font-size:0.88em;">
<i class="fa-solid fa-tag" style="font-size:1.8em;opacity:0.4;margin-bottom:8px;display:block;"></i>
No uploads found for #${escapeHtml(currentTag)}
</div>
`;
}
} catch (err) {
console.error('[TAG-FEED] Failed to load tag items:', err);
if (reset) {
itemsContainer.innerHTML = `
<div class="sidebar-empty-state" style="text-align:center;padding:25px;color:var(--danger,#ff4444);font-size:0.88em;">
Failed to load tag items
</div>
`;
}
} finally {
tagFeedLoading = false;
}
};
const loadMoreTagFeed = async () => {
if (tagFeedLoading || !tagHasMore || !currentTag) return;
tagOffset += 20;
await loadTagFeed(false);
};
const initTagSentinel = () => {
const container = document.getElementById('sidebar-tag-container');
const itemsContainer = container?.querySelector('.sidebar-tag-items-container');
if (!container || !itemsContainer) return;
if (tagSentinel && tagSentinel.parentNode) {
tagSentinel.remove();
}
if (!tagHasMore) return;
tagSentinel = document.createElement('div');
tagSentinel.className = 'sidebar-tag-sentinel';
tagSentinel.style.cssText = 'height: 20px; margin: 10px 0;';
itemsContainer.appendChild(tagSentinel);
if (tagObserver) tagObserver.disconnect();
tagObserver = new IntersectionObserver((entries) => {
if (entries[0].isIntersecting && !tagFeedLoading && tagHasMore) {
loadMoreTagFeed();
}
}, {
root: container,
rootMargin: '200px'
});
tagObserver.observe(tagSentinel);
};
const initTagOrderButton = () => {
const container = document.getElementById('sidebar-tag-container');
if (!container || container.dataset.orderBound === 'true') return;
container.dataset.orderBound = 'true';
const orderBtn = container.querySelector('.sidebar-tag-order-btn');
if (orderBtn) {
orderBtn.addEventListener('click', (e) => {
e.preventDefault();
tagOrder = (tagOrder === 'desc') ? 'asc' : 'desc';
loadTagFeed(true);
});
}
};
const bindTagContainerEvents = () => {
const container = document.getElementById('sidebar-tag-container');
if (!container || container.dataset.eventsBound === 'true') return;
container.dataset.eventsBound = 'true';
container.addEventListener('click', (e) => {
const card = e.target.closest('.sidebar-video-card');
if (card) {
container.querySelectorAll('.sidebar-video-card').forEach(c => c.classList.remove('active-tag-item'));
card.classList.add('active-tag-item');
}
});
};
const switchSidebarTab = (tabName) => { const switchSidebarTab = (tabName) => {
const normalizedTab = (tabName === 'videos' || tabName === 'recommendations') ? 'recommendations' : tabName; if (window.clearHoverPreview) {
window.clearHoverPreview();
}
const normalizedTab = (tabName === 'videos' || tabName === 'recommendations')
? 'recommendations'
: (tabName === 'tag' ? 'tag' : tabName);
const tabs = document.querySelectorAll('.sidebar-tab'); const tabs = document.querySelectorAll('.sidebar-tab');
const contents = document.querySelectorAll('.sidebar-tab-content'); const contents = document.querySelectorAll('.sidebar-tab-content');
tabs.forEach(t => { tabs.forEach(t => {
const tTab = (t.dataset.tab === 'videos' || t.dataset.tab === 'recommendations') ? 'recommendations' : t.dataset.tab; const tTab = (t.dataset.tab === 'videos' || t.dataset.tab === 'recommendations')
? 'recommendations'
: (t.dataset.tab === 'tag' ? 'tag' : t.dataset.tab);
if (tTab === normalizedTab) { if (tTab === normalizedTab) {
t.classList.add('active'); t.classList.add('active');
} else { } else {
@@ -1364,7 +1744,9 @@
}); });
contents.forEach(c => { contents.forEach(c => {
const cTab = (c.dataset.tabContent === 'videos' || c.dataset.tabContent === 'recommendations') ? 'recommendations' : c.dataset.tabContent; const cTab = (c.dataset.tabContent === 'videos' || c.dataset.tabContent === 'recommendations')
? 'recommendations'
: (c.dataset.tabContent === 'tag' ? 'tag' : c.dataset.tabContent);
if (cTab === normalizedTab) { if (cTab === normalizedTab) {
c.classList.add('active'); c.classList.add('active');
c.style.display = 'block'; c.style.display = 'block';
@@ -1382,6 +1764,12 @@
if (!recommendationsLoaded) { if (!recommendationsLoaded) {
loadRecommendations(); loadRecommendations();
} }
} else if (normalizedTab === 'tag') {
if (tagFeedNeedsReload || !tagFeedLoaded) {
loadTagFeed(true);
} else {
setTimeout(() => highlightActiveTagCard(true), 60);
}
} }
}; };
@@ -1392,6 +1780,11 @@
const btn = e.target.closest('.sidebar-tab'); const btn = e.target.closest('.sidebar-tab');
if (btn && btn.dataset.tab) { if (btn && btn.dataset.tab) {
e.preventDefault(); e.preventDefault();
if (btn.dataset.tab !== 'tag') {
userManuallySelectedNonTagTab = true;
} else {
userManuallySelectedNonTagTab = false;
}
switchSidebarTab(btn.dataset.tab); switchSidebarTab(btn.dataset.tab);
} }
}); });
@@ -1403,13 +1796,20 @@
savedTab = localStorage.getItem('sidebar_active_tab') || 'comments'; savedTab = localStorage.getItem('sidebar_active_tab') || 'comments';
} catch (_) {} } catch (_) {}
if (savedTab === 'videos' || savedTab === 'recommendations') { if (getCurrentTag()) {
switchSidebarTab('tag');
} else if (savedTab === 'videos' || savedTab === 'recommendations') {
switchSidebarTab('recommendations'); switchSidebarTab('recommendations');
} else {
switchSidebarTab(savedTab);
} }
}; };
const replaceCardWithNewRandom = async (card) => { const replaceCardWithNewRandom = async (card) => {
if (!card || card.dataset.swapping === 'true') return; if (!card || card.dataset.swapping === 'true') return;
if (window.clearHoverPreview) {
window.clearHoverPreview();
}
card.dataset.swapping = 'true'; card.dataset.swapping = 'true';
card.classList.add('sidebar-card-swapping'); card.classList.add('sidebar-card-swapping');
@@ -1436,7 +1836,9 @@
const excludeArr = Array.from(excludeSet).slice(-120); const excludeArr = Array.from(excludeSet).slice(-120);
const affParams = getSessionAffinityParams(); const affParams = getSessionAffinityParams();
const res = await fetch(`/api/v2/recommendations?limit=1&mode=${mode}&exclude_ids=${excludeArr.join(',')}${affParams}`, { const mime = getCurrentMimeFilter();
const mimeParam = mime ? `&mime=${encodeURIComponent(mime)}` : '';
const res = await fetch(`/api/v2/recommendations?limit=1&mode=${mode}${mimeParam}&exclude_ids=${excludeArr.join(',')}${affParams}`, {
headers: { 'X-Requested-With': 'XMLHttpRequest' } headers: { 'X-Requested-With': 'XMLHttpRequest' }
}); });
const data = await res.json(); const data = await res.json();
@@ -1458,11 +1860,7 @@
const newCard = temp.firstElementChild; const newCard = temp.firstElementChild;
if (newCard && card.parentNode) { if (newCard && card.parentNode) {
newCard.classList.add('sidebar-card-entering');
card.replaceWith(newCard); card.replaceWith(newCard);
setTimeout(() => {
newCard.classList.remove('sidebar-card-entering');
}, 2200);
} }
} else { } else {
card.classList.remove('sidebar-card-swapping'); card.classList.remove('sidebar-card-swapping');
@@ -1496,14 +1894,20 @@
container.addEventListener('auxclick', handleCardAction); container.addEventListener('auxclick', handleCardAction);
}; };
// Reload recommendations when user triggers Random (#random, #nav-random, or 'r' key) // Reload recommendations or sync tag feed when user triggers Random (#random, #nav-random, or 'r' key)
const handleRandomAction = () => { const handleRandomAction = () => {
recommendationsLoaded = false; recommendationsLoaded = false;
const tag = getCurrentTag();
if (tag) {
userManuallySelectedNonTagTab = false;
switchSidebarTab('tag');
} else {
let savedTab = 'comments'; let savedTab = 'comments';
try { savedTab = localStorage.getItem('sidebar_active_tab'); } catch (_) {} try { savedTab = localStorage.getItem('sidebar_active_tab'); } catch (_) {}
if (savedTab === 'videos' || savedTab === 'recommendations') { if (savedTab === 'videos' || savedTab === 'recommendations') {
loadRecommendations(false); loadRecommendations(false);
} }
}
}; };
document.addEventListener('f0ck:randomTriggered', handleRandomAction); document.addEventListener('f0ck:randomTriggered', handleRandomAction);
@@ -1525,10 +1929,17 @@
loadActivity(false); loadActivity(false);
recommendationsLoaded = false; recommendationsLoaded = false;
tagFeedLoaded = false;
tagFeedNeedsReload = true;
const activeTabEl = document.querySelector('.sidebar-tab.active')?.dataset.tab;
let savedTab = 'comments'; let savedTab = 'comments';
try { savedTab = localStorage.getItem('sidebar_active_tab'); } catch (_) {} try { savedTab = localStorage.getItem('sidebar_active_tab'); } catch (_) {}
if (savedTab === 'videos' || savedTab === 'recommendations') { const currentTabName = activeTabEl || savedTab;
if (currentTabName === 'videos' || currentTabName === 'recommendations') {
loadRecommendations(false); loadRecommendations(false);
} else if (currentTabName === 'tag' && currentTag) {
loadTagFeed(true);
} }
}); });
@@ -1573,6 +1984,19 @@
const _origInit = init; const _origInit = init;
const initWithScroll = async () => { const initWithScroll = async () => {
initSidebarTabs(); initSidebarTabs();
initTagOrderButton();
bindTagContainerEvents();
updateTagTabVisibility();
document.addEventListener('f0ck:contentLoaded', () => {
updateTagTabVisibility();
setTimeout(() => highlightActiveTagCard(true), 60);
});
window.addEventListener('popstate', () => {
setTimeout(() => {
updateTagTabVisibility();
highlightActiveTagCard(true);
}, 60);
});
bindRecommendationEvents(); bindRecommendationEvents();
bindRecommendationScrollListener(); bindRecommendationScrollListener();
await _origInit(); await _origInit();
+172
View File
@@ -2517,6 +2517,178 @@ const f0cklib = {
return combined.slice(0, maxLimit); return combined.slice(0, maxLimit);
}, },
getTagFeedItems: async ({
tag,
order = 'desc', // 'desc' = newest first (default), 'asc' = oldest first (chronological)
offset = 0,
limit = 20,
focus_id = null,
mode,
ratings,
session,
exclude,
user_id,
is_admin,
mime
} = {}) => {
if (!tag || !tag.trim()) return { items: [], total: 0 };
const ratingsArr = (Array.isArray(ratings) && ratings.length > 0) ? ratings : null;
const modequery = computeBaseMode(mode, ratingsArr, session);
const globalfilter = !session ? getGlobalfilter() : null;
const excludedTags = session && exclude ? (exclude || []) : [];
const maxLimit = Math.min(Math.max(1, Number(limit) || 20), 50);
const numOffset = Math.max(0, Number(offset) || 0);
const isOwnerOrAdmin = session && is_admin;
const visibilityFilter = isOwnerOrAdmin
? db``
: (session && user_id
? db`AND (COALESCE(items.visibility, 0) = 0 OR items.username = (SELECT "user" FROM "user" WHERE id = ${user_id}))`
: db`AND COALESCE(items.visibility, 0) = 0`);
const mimeParts = (mime || "").split(',').filter(m => ['video', 'audio', 'image', 'flash', 'pdf'].includes(m));
const mimeSQL = mimeParts.length > 0
? db`and (${mimeParts.map(m => m === 'flash'
? (flashMimes.length > 0
? flashMimes.map(fm => db`items.mime = ${fm}`).reduce((a, b) => db`${a} or ${b}`)
: db`false`)
: (m === 'pdf' ? db`items.mime = 'application/pdf'` : db`items.mime ilike ${m + '/%'}`)).reduce((a, b) => db`${a} or ${b}`)})`
: db``;
const terms = tag.split(',').map(t => t.trim()).filter(Boolean);
const tagConditions = terms.map(term => {
return db`and items.id in (
select ta.item_id
from tags_assign ta
join tags t on t.id = ta.tag_id
where t.normalized like '%' || slugify(${term}) || '%'
)`;
});
const isDesc = String(order).toLowerCase() === 'desc';
const sortOrderItems = isDesc ? db`items.id desc` : db`items.id asc`;
const sortOrderMi = isDesc ? db`mi.id desc` : db`mi.id asc`;
let computedOffset = numOffset;
let computedLimit = maxLimit;
if (focus_id) {
const numFocusId = await resolveNumericItemId(focus_id);
if (numFocusId) {
try {
const rankRes = await db`
WITH ordered AS (
SELECT items.id, ROW_NUMBER() OVER (ORDER BY ${sortOrderItems}) - 1 as row_num
FROM items
WHERE items.active = true
AND (items.is_deleted IS NOT TRUE)
${visibilityFilter}
${mimeSQL}
AND ${db.unsafe(modequery)}
${globalfilter ? db`AND NOT EXISTS (SELECT 1 FROM tags_assign WHERE item_id = items.id AND (${db.unsafe(globalfilter)}))` : db``}
${excludedTags.length > 0 ? db`AND NOT EXISTS (SELECT 1 FROM tags_assign WHERE item_id = items.id AND tag_id = ANY(${excludedTags}::int[]))` : db``}
${tagConditions}
)
SELECT row_num FROM ordered WHERE id = ${numFocusId} LIMIT 1
`;
if (rankRes.length > 0) {
const rowNum = parseInt(rankRes[0].row_num, 10);
if (rowNum < 30) {
computedOffset = 0;
computedLimit = Math.min(50, Math.max(20, rowNum + 10));
} else {
computedOffset = Math.max(0, rowNum - 6);
computedLimit = 25;
}
}
} catch (err) {
console.error('[TAG-FEED] Error computing focus_id offset:', err);
}
}
}
const countRes = await db`
SELECT count(distinct items.id) as total
FROM items
WHERE items.active = true
AND (items.is_deleted IS NOT TRUE)
${visibilityFilter}
${mimeSQL}
AND ${db.unsafe(modequery)}
${globalfilter ? db`AND NOT EXISTS (SELECT 1 FROM tags_assign WHERE item_id = items.id AND (${db.unsafe(globalfilter)}))` : db``}
${excludedTags.length > 0 ? db`AND NOT EXISTS (SELECT 1 FROM tags_assign WHERE item_id = items.id AND tag_id = ANY(${excludedTags}::int[]))` : db``}
${tagConditions}
`;
const total = parseInt(countRes[0]?.total || 0, 10);
const rows = await db`
WITH matched_items AS (
SELECT items.id, items.title, items.slug, items.mime, items.dest, items.username, items.stamp, items.xd_score, items.width, items.height, items.has_coverart
FROM items
WHERE items.active = true
AND (items.is_deleted IS NOT TRUE)
${visibilityFilter}
${mimeSQL}
AND ${db.unsafe(modequery)}
${globalfilter ? db`AND NOT EXISTS (SELECT 1 FROM tags_assign WHERE item_id = items.id AND (${db.unsafe(globalfilter)}))` : db``}
${excludedTags.length > 0 ? db`AND NOT EXISTS (SELECT 1 FROM tags_assign WHERE item_id = items.id AND tag_id = ANY(${excludedTags}::int[]))` : db``}
${tagConditions}
ORDER BY ${sortOrderItems}
OFFSET ${computedOffset}
LIMIT ${computedLimit}
)
SELECT
mi.*,
uo.display_name,
uo.username_color,
uo.avatar,
uo.avatar_file,
(SELECT ta.tag_id FROM tags_assign ta WHERE ta.item_id = mi.id AND ta.tag_id = ANY(${[1, 2, cfg.nsfl_tag_id || 3]}::int[]) LIMIT 1) as rating_tag_id,
ARRAY(
SELECT t.tag
FROM tags_assign ta
JOIN tags t ON t.id = ta.tag_id
WHERE ta.item_id = mi.id AND ta.tag_id NOT IN (1, 2, 3)
LIMIT 3
) as tags
FROM matched_items mi
LEFT JOIN "user" u ON LOWER(u."user") = LOWER(mi.username)
LEFT JOIN user_options uo ON uo.user_id = u.id
ORDER BY ${sortOrderMi}
`;
const items = rows.map(r => {
const meta = xdScoreMeta(r.xd_score);
const tagId = r.rating_tag_id;
const ratingClass = tagId === 1 ? 'sfw' : (tagId === 2 ? 'nsfw' : (tagId === 3 ? 'nsfl' : 'untagged'));
return {
id: r.id,
title: r.title || null,
slug: r.slug || null,
mime: r.mime,
dest: r.dest,
username: r.username,
display_name: r.display_name || r.username,
username_color: r.username_color || null,
avatar: r.avatar || null,
avatar_file: r.avatar_file || null,
stamp: r.stamp,
tags: r.tags || [],
rating_tag_id: tagId,
rating_class: ratingClass,
xd_score: r.xd_score,
xd_tier: meta.tier,
xd_label: meta.label,
width: r.width,
height: r.height,
has_coverart: !!r.has_coverart
};
});
return { items, total, offset: computedOffset, limit: computedLimit };
},
computeBaseMode, computeBaseMode,
getGlobalfilter, getGlobalfilter,
+82 -6
View File
@@ -718,7 +718,10 @@ export default router => {
const ratingsRaw = req.cookies.ratings; const ratingsRaw = req.cookies.ratings;
const ratingsArr = ratingsRaw ? decodeURIComponent(ratingsRaw).split(/[|,]/).filter(r => ['sfw','nsfw','nsfl','untagged'].includes(r)) : null; const ratingsArr = ratingsRaw ? decodeURIComponent(ratingsRaw).split(/[|,]/).filter(r => ['sfw','nsfw','nsfl','untagged'].includes(r)) : null;
const mime = req.params?.type === 'videos' ? 'video' : (req.url.qs?.mime || null); const cookieMime = req.cookies?.mime ? decodeURIComponent(req.cookies.mime).trim() : null;
const mime = (typeof req.url.qs?.mime !== 'undefined')
? (req.url.qs.mime || null)
: (cookieMime || (req.params?.type === 'videos' ? 'video' : null));
let excludeIds = []; let excludeIds = [];
if (req.url.qs?.exclude_ids) { if (req.url.qs?.exclude_ids) {
@@ -744,15 +747,27 @@ export default router => {
res.json({ res.json({
success: true, success: true,
items, items: items.map(item => ({
videos: items id: item.id,
title: item.title,
slug: (getEnableItemSlugs() && item.slug) ? item.slug : null,
dest: item.dest,
mime: item.mime,
stamp: item.stamp,
username: item.username,
display_name: item.display_name,
username_color: item.username_color,
has_coverart: item.has_coverart,
rating_class: item.rating_class,
xd_score: item.xd_score,
xd_tier: item.xd_tier,
personalized: !!item.personalized
}))
}); });
} catch (err) { } catch (err) {
console.error("[RECOMMENDATIONS] Failed to fetch personalized recommendations:", err); console.error("[RECOMMENDATIONS] Failed to fetch personalized recommendations:", err);
res.json({ res.status(500).json({
success: false, success: false,
items: [],
videos: [],
error: "Failed to load recommendations" error: "Failed to load recommendations"
}); });
} }
@@ -805,6 +820,67 @@ export default router => {
} }
}); });
group.get(/\/tag-feed$/, async (req, res) => {
try {
const tag = req.url.qs?.tag ? String(req.url.qs.tag).trim() : null;
if (!tag) {
return res.json({ success: false, error: "Tag is required", items: [], total: 0 }, 400);
}
const limit = Math.min(+(req.url.qs?.limit || 20), 50);
const offset = Math.max(+(req.url.qs?.offset || 0), 0);
const order = req.url.qs?.order === 'asc' ? 'asc' : 'desc';
const focusId = req.url.qs?.focus_id ? String(req.url.qs.focus_id).trim() : null;
let mode = req.mode ?? 0;
if (req.url.qs && req.url.qs.mode && ['0', '1', '2', '3'].includes(req.url.qs.mode)) {
mode = parseInt(req.url.qs.mode);
}
const ratingsRaw = req.cookies.ratings;
const ratingsArr = ratingsRaw ? decodeURIComponent(ratingsRaw).split(/[|,]/).filter(r => ['sfw','nsfw','nsfl','untagged'].includes(r)) : null;
const cookieMime = req.cookies?.mime ? decodeURIComponent(req.cookies.mime).trim() : null;
const mime = (typeof req.url.qs?.mime !== 'undefined')
? (req.url.qs.mime || null)
: (cookieMime || null);
const result = await f0cklib.getTagFeedItems({
tag,
order,
offset,
limit,
focus_id: focusId,
mode,
ratings: ratingsArr,
session: !!req.session,
exclude: req.session?.excluded_tags || [],
user_id: req.session?.id,
is_admin: req.session?.admin,
mime
});
const effectiveOffset = result.offset ?? offset;
return res.json({
success: true,
tag,
order,
total: result.total,
offset: effectiveOffset,
limit: result.limit ?? limit,
items: result.items,
hasMore: (effectiveOffset + result.items.length) < result.total
});
} catch (err) {
console.error("[TAG-FEED] Error loading tag feed items:", err);
return res.json({
success: false,
error: "Failed to load tag feed",
items: [],
total: 0
}, 500);
}
});
group.get(/\/orakel\/user$/, async (req, res) => { group.get(/\/orakel\/user$/, async (req, res) => {
try { try {
const now = ~~(Date.now() / 1000); const now = ~~(Date.now() / 1000);
+1 -2
View File
@@ -116,7 +116,7 @@ export default (router, tpl) => {
if (req.headers['x-requested-with'] === 'XMLHttpRequest' || query.ajax) { if (req.headers['x-requested-with'] === 'XMLHttpRequest' || query.ajax) {
return res.json({ return res.json({
success: true, success: true,
html: tpl.render('tag-cards', { toptags: tags, user: req.session?.user ? { user: req.session.user } : null, session: (req.session && req.session.user) ? { ...req.session } : false }, req), html: tpl.render('tag-cards', { toptags: tags, session: (req.session && req.session.user) ? { ...req.session } : false }, req),
currentPage: page, currentPage: page,
hasMore: tags.length === TAGS_PER_PAGE hasMore: tags.length === TAGS_PER_PAGE
}); });
@@ -149,7 +149,6 @@ export default (router, tpl) => {
phrase, phrase,
tmp: null, tmp: null,
hidePagination: true, hidePagination: true,
user: req.session?.user ? { user: req.session.user } : null,
session: (req.session && req.session.user) ? { ...req.session } : false, session: (req.session && req.session.user) ? { ...req.session } : false,
page_meta: { page_meta: {
title: 'Tags', title: 'Tags',
+2 -1
View File
@@ -141,7 +141,7 @@ export default (router, tpl) => {
return res.json({ return res.json({
success: true, success: true,
html: tpl.render('tag-cards', { toptags: tags, user: { user: userName }, session: (req.session && req.session.user) ? { ...req.session } : false }, req), html: tpl.render('tag-cards', { toptags: tags, tagger: userName, user: { user: userName }, session: (req.session && req.session.user) ? { ...req.session } : false }, req),
currentPage: page, currentPage: page,
pagination: paginationHtml, pagination: paginationHtml,
hasMore: tags.length === TAGS_PER_PAGE hasMore: tags.length === TAGS_PER_PAGE
@@ -188,6 +188,7 @@ export default (router, tpl) => {
const data = { const data = {
user: userRow, user: userRow,
tagger: userRow.user,
toptags: toptags, toptags: toptags,
tmp: null, tmp: null,
hidePagination: false, hidePagination: false,
+2 -2
View File
@@ -11,9 +11,9 @@
</div> </div>
@if(enable_item_title) @if(enable_item_title)
<div class="item_title item-title"> <div class="item_title item-title">
<span class="item_title_text" @if(can_manage_item) style="display:none;" @endif>{!! item.title || '' !!}</span> <span class="item_title_text@if(!item.title) no-title@endif"@if(can_manage_item) title="Click to edit"@endif>@if(item.title){!! item.title !!}@elseif(can_manage_item)<em>Add a title</em>@endif</span>
@if(can_manage_item) @if(can_manage_item)
<div class="info-title-edit-wrap"> <div class="info-title-edit-wrap" style="display:none;">
<input type="text" id="info-title-input" class="info-title-input" value="{!! item.title || '' !!}" placeholder="Add a title…" maxlength="500" data-item-id="{{ item.id }}" /> <input type="text" id="info-title-input" class="info-title-input" value="{!! item.title || '' !!}" placeholder="Add a title…" maxlength="500" data-item-id="{{ item.id }}" />
<button type="button" id="info-title-save" class="info-title-save-btn" title="Save Title"><i class="fa-solid fa-check"></i></button> <button type="button" id="info-title-save" class="info-title-save-btn" title="Save Title"><i class="fa-solid fa-check"></i></button>
</div> </div>
+2 -2
View File
@@ -75,9 +75,9 @@
</div> </div>
@if(enable_item_title) @if(enable_item_title)
<div class="item_title item-title"> <div class="item_title item-title">
<span class="item_title_text" @if(can_manage_item) style="display:none;" @endif>{!! item.title || '' !!}</span> <span class="item_title_text@if(!item.title) no-title@endif"@if(can_manage_item) title="Click to edit"@endif>@if(item.title){!! item.title !!}@elseif(can_manage_item)<em>Add a title</em>@endif</span>
@if(can_manage_item) @if(can_manage_item)
<div class="info-title-edit-wrap"> <div class="info-title-edit-wrap" style="display:none;">
<input type="text" id="info-title-input" class="info-title-input" value="{!! item.title || '' !!}" placeholder="Add a title…" maxlength="500" data-item-id="{{ item.id }}" /> <input type="text" id="info-title-input" class="info-title-input" value="{!! item.title || '' !!}" placeholder="Add a title…" maxlength="500" data-item-id="{{ item.id }}" />
<button type="button" id="info-title-save" class="info-title-save-btn" title="Save Title"><i class="fa-solid fa-check"></i></button> <button type="button" id="info-title-save" class="info-title-save-btn" title="Save Title"><i class="fa-solid fa-check"></i></button>
</div> </div>
+4 -4
View File
@@ -1,5 +1,5 @@
<!doctype html> <!doctype html>
<html lang="{{ lang || 'en' }}" theme="@if(typeof theme !== 'undefined'){{ theme }}@endif"> <html lang="{{ lang || 'en' }}" theme="@if(typeof theme !== 'undefined' && theme){{ theme }}@elseif(typeof req !== 'undefined' && req.theme){{ req.theme }}@else{{ default_theme || 'f0ck' }}@endif">
<head> <head>
<title>{{ domain }} — doomscroll</title> <title>{{ domain }} — doomscroll</title>
<meta charset="utf-8" /> <meta charset="utf-8" />
@@ -321,9 +321,9 @@
.scroll-user-link { pointer-events: all; position: relative; z-index: 11; text-decoration: none; color: inherit; } .scroll-user-link { pointer-events: all; position: relative; z-index: 11; text-decoration: none; color: inherit; }
.scroll-badges { display: flex; gap: 6px; align-items: center; margin-bottom: 4px; pointer-events: all; position: relative; z-index: 11; } .scroll-badges { display: flex; gap: 6px; align-items: center; margin-bottom: 4px; pointer-events: all; position: relative; z-index: 11; }
.scroll-rating { display: inline-flex; align-items: center; padding: 2px 8px; border-radius: 4px; font-size: .67rem; font-weight: 700; text-transform: uppercase; letter-spacing: .04em; } .scroll-rating { display: inline-flex; align-items: center; padding: 2px 8px; border-radius: 4px; font-size: .67rem; font-weight: 700; text-transform: uppercase; letter-spacing: .04em; }
.scroll-rating.sfw { background: var(--badge-sfw); color: #fff; } .scroll-rating.sfw { background: var(--badge-sfw, #4caf50); color: #fff; }
.scroll-rating.nsfw { background: var(--badge-nsfw); color: #fff; } .scroll-rating.nsfw { background: var(--badge-nsfw, #e10dc3); color: #fff; }
.scroll-rating.nsfl { background: var(--badge-nsfl); color: #fff; } .scroll-rating.nsfl { background: var(--badge-nsfl, #660000); color: #fff; }
.scroll-rating.untagged { background: transparent; color: rgba(255,255,255,.5); border: 1px dashed rgba(255,255,255,.3); } .scroll-rating.untagged { background: transparent; color: rgba(255,255,255,.5); border: 1px dashed rgba(255,255,255,.3); }
.scroll-rating.can-cycle { cursor: pointer; pointer-events: all; position: relative; z-index: 11; } .scroll-rating.can-cycle { cursor: pointer; pointer-events: all; position: relative; z-index: 11; }
.scroll-rating.can-cycle:hover { filter: brightness(1.2); } .scroll-rating.can-cycle:hover { filter: brightness(1.2); }
+21
View File
@@ -127,6 +127,9 @@
<button type="button" class="sidebar-tab" data-tab="recommendations" title="{{ t('sidebar.recommendations') }}" aria-label="{{ t('sidebar.recommendations') }}"> <button type="button" class="sidebar-tab" data-tab="recommendations" title="{{ t('sidebar.recommendations') }}" aria-label="{{ t('sidebar.recommendations') }}">
<i class="fa-solid fa-compass"></i> <i class="fa-solid fa-compass"></i>
</button> </button>
<button type="button" class="sidebar-tab" id="sidebar-tab-tag" data-tab="tag" title="Tag Feed" aria-label="Tag Feed" style="display: none;">
<i class="fa-solid fa-hashtag"></i>
</button>
</div> </div>
<div id="sidebar-activity-container" class="sidebar-comments-list sidebar-tab-content active" data-tab-content="comments"> <div id="sidebar-activity-container" class="sidebar-comments-list sidebar-tab-content active" data-tab-content="comments">
<div class="sidebar-loading-state"> <div class="sidebar-loading-state">
@@ -140,6 +143,24 @@
<span>{{ t('sidebar.loading_recommendations') }}</span> <span>{{ t('sidebar.loading_recommendations') }}</span>
</div> </div>
</div> </div>
<div id="sidebar-tag-container" class="sidebar-recommendations-list sidebar-tab-content" data-tab-content="tag" style="display: none;">
<div class="sidebar-tag-header">
<div class="sidebar-tag-title">
<i class="fa-solid fa-hashtag"></i>
<span class="sidebar-tag-name"></span>
<span class="sidebar-tag-count"></span>
</div>
<button type="button" class="sidebar-tag-order-btn" title="Newest first (default) — Click for Oldest first">
<i class="fa-solid fa-arrow-down-9-1"></i>
</button>
</div>
<div class="sidebar-tag-items-container">
<div class="sidebar-loading-state">
<i class="fa-solid fa-circle-notch fa-spin"></i>
<span>Loading tag items...</span>
</div>
</div>
</div>
</div> </div>
<div class="global-sidebar-right-footer"> <div class="global-sidebar-right-footer">
<a href="/ranking">{{ t('footer.ranking') }}</a> <a href="/ranking">{{ t('footer.ranking') }}</a>
+1 -1
View File
@@ -1,5 +1,5 @@
<!doctype html> <!doctype html>
<html lang="{{ lang || 'en' }}" theme="@if(typeof theme !== 'undefined'){{ theme }}@endif" res="@if(typeof fullscreen !== 'undefined'){{ fullscreen == 1 ? 'fullscreen' : '' }}@endif" data-thumb-size="{{ default_thumb_size || 'm' }}"> <html lang="{{ lang || 'en' }}" theme="@if(typeof theme !== 'undefined' && theme){{ theme }}@elseif(typeof req !== 'undefined' && req.theme){{ req.theme }}@else{{ default_theme || 'f0ck' }}@endif" res="@if(typeof fullscreen !== 'undefined'){{ fullscreen == 1 ? 'fullscreen' : '' }}@endif" data-thumb-size="{{ default_thumb_size || 'm' }}">
<head> <head>
@if(typeof page_meta !== 'undefined' && page_meta.title)<title>{{ domain }} - {{ page_meta.title }}</title>@elseif(typeof item !== 'undefined')<title>{{ domain }} - {{ (enable_item_slugs && item.slug) ? item.slug : item.id }}</title>@else<title>{{ domain }}</title>@endif @if(typeof page_meta !== 'undefined' && page_meta.title)<title>{{ domain }} - {{ page_meta.title }}</title>@elseif(typeof item !== 'undefined')<title>{{ domain }} - {{ (enable_item_slugs && item.slug) ? item.slug : item.id }}</title>@else<title>{{ domain }}</title>@endif
+2 -2
View File
@@ -928,8 +928,8 @@ body.preview-as-user .admin-edit-btn,
body.preview-as-user .admin-pin-btn, body.preview-as-user .admin-pin-btn,
body.preview-as-user .delete-btn, body.preview-as-user .delete-btn,
body.preview-as-user .poll-delete-btn, body.preview-as-user .poll-delete-btn,
body.preview-as-user [data-is-admin] .admin-only { display: none !important; } body.preview-as-user .item_title_text { display: inline !important; cursor: default !important; pointer-events: none !important; }
body.preview-as-user .item_title_text { display: inline !important; } body.preview-as-user .item_title_text.no-title { display: none !important; }
/* Hide admin-only actions in mod preview (keep delete — mods can delete) */ /* Hide admin-only actions in mod preview (keep delete — mods can delete) */
body.preview-as-mod #a_rethumb, body.preview-as-mod #a_rethumb,
+1 -1
View File
@@ -1,5 +1,5 @@
@each(toptags as toptag) @each(toptags as toptag)
<a href="/tag/{{ toptag.safe_tag }}@if(user && user.user)?tagger={!! encodeURIComponent(user.user) !!}@endif" class="tag-card"> <a href="/tag/{{ toptag.safe_tag }}@if(typeof tagger !== 'undefined' && tagger)?tagger={!! encodeURIComponent(tagger) !!}@endif" class="tag-card">
<div class="tag-card-image"> <div class="tag-card-image">
<img src="/tag_image/{{ toptag.encoded_tag }}?m={{ session.mode }}" loading="lazy" alt="{!! toptag.tag !!}"> <img src="/tag_image/{{ toptag.encoded_tag }}?m={{ session.mode }}" loading="lazy" alt="{!! toptag.tag !!}">
</div> </div>