gsdf
This commit is contained in:
+124
-80
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 */
|
||||||
|
|||||||
+115
-20
@@ -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 (val !== null) document.documentElement.setAttribute(attr, val);
|
if (attr === 'theme') {
|
||||||
else document.documentElement.removeAttribute(attr);
|
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);
|
||||||
|
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') {
|
||||||
e.preventDefault();
|
if (e.key === 'Enter') {
|
||||||
const container = e.target.closest('.item_title') || document;
|
e.preventDefault();
|
||||||
const saveBtn = container.querySelector('#info-title-save') || document.getElementById('info-title-save');
|
const container = e.target.closest('.item_title') || document;
|
||||||
if (saveBtn) saveBtn.click();
|
const saveBtn = container.querySelector('#info-title-save') || document.getElementById('info-title-save');
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
+456
-32
@@ -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;
|
||||||
|
|
||||||
if (modeChanged) {
|
window.f0ckDebug("Sidebar Activity: Page transition detected", modeChanged ? "(Mode changed)" : "", mimeChanged ? "(Mime changed)" : "");
|
||||||
window._sidebarActivityCache = [];
|
|
||||||
lastRenderedIds = '';
|
if (modeChanged || mimeChanged) {
|
||||||
currentPage = 1;
|
if (modeChanged) {
|
||||||
hasMore = true;
|
window._sidebarActivityCache = [];
|
||||||
loadActivity(false); // Force reload with loading state
|
lastRenderedIds = '';
|
||||||
|
currentPage = 1;
|
||||||
|
hasMore = true;
|
||||||
|
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,13 +1894,19 @@
|
|||||||
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;
|
||||||
let savedTab = 'comments';
|
const tag = getCurrentTag();
|
||||||
try { savedTab = localStorage.getItem('sidebar_active_tab'); } catch (_) {}
|
if (tag) {
|
||||||
if (savedTab === 'videos' || savedTab === 'recommendations') {
|
userManuallySelectedNonTagTab = false;
|
||||||
loadRecommendations(false);
|
switchSidebarTab('tag');
|
||||||
|
} else {
|
||||||
|
let savedTab = 'comments';
|
||||||
|
try { savedTab = localStorage.getItem('sidebar_active_tab'); } catch (_) {}
|
||||||
|
if (savedTab === 'videos' || savedTab === 'recommendations') {
|
||||||
|
loadRecommendations(false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -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();
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -804,6 +819,67 @@ export default router => {
|
|||||||
return res.json({ success: false, error: "Tracking failed" }, 500);
|
return res.json({ success: false, error: "Tracking failed" }, 500);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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 {
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
@@ -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); }
|
||||||
|
|||||||
@@ -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,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
|
||||||
|
|||||||
@@ -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,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>
|
||||||
|
|||||||
Reference in New Issue
Block a user