gfds
This commit is contained in:
+244
-81
@@ -42,13 +42,13 @@
|
|||||||
--posts-meta-bg: #000000b8;
|
--posts-meta-bg: #000000b8;
|
||||||
--x2154: #215483;
|
--x2154: #215483;
|
||||||
--bg-canvas-opacity: 0.81;
|
--bg-canvas-opacity: 0.81;
|
||||||
--bg-canvas-blur: 151px;
|
--bg-canvas-blur: 30px; /* runtime value set by applyBackgroundOpacitySettings() */
|
||||||
--bg-canvas-zoom: 1.38;
|
--bg-canvas-zoom: 1.38;
|
||||||
--bg-canvas-color: rgba(0, 0, 0, 0.09);
|
--bg-canvas-color: rgba(0, 0, 0, 0.09);
|
||||||
--bg-canvas-color-opacity: 0.09;
|
--bg-canvas-color-opacity: 0.09;
|
||||||
--onara-bg-opacity: 0.4;
|
--onara-bg-opacity: 0.6;
|
||||||
--onara-backdrop-blur: 5px;
|
--onara-backdrop-blur: 5px;
|
||||||
--onara-grid-dim-opacity: 0.38;
|
--onara-grid-dim-opacity: 1.0;
|
||||||
}
|
}
|
||||||
|
|
||||||
html:not([theme]), html[theme=""] {
|
html:not([theme]), html[theme=""] {
|
||||||
@@ -1844,7 +1844,7 @@ html[theme="f0ck95"] #next {
|
|||||||
|
|
||||||
html[theme="4d"] {
|
html[theme="4d"] {
|
||||||
--accent: #f2ef0b;
|
--accent: #f2ef0b;
|
||||||
--accent-rgb: 31, 178, 176;
|
--accent-rgb: 242, 239, 11;
|
||||||
--bg: #242424;
|
--bg: #242424;
|
||||||
--black: #000;
|
--black: #000;
|
||||||
--white: #fff;
|
--white: #fff;
|
||||||
@@ -3215,7 +3215,7 @@ body.sidebar-right-hidden #sidebar-drag-zone {
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
background: linear-gradient(135deg, rgba(var(--accent-rgb, 153, 255, 0), 0.12), rgba(0, 0, 0, 0.45)), var(--nav-bg, #1e1e1e);
|
background: linear-gradient(135deg, rgba(var(--accent-rgb, 128, 128, 128), 0.12), rgba(0, 0, 0, 0.45)), var(--nav-bg, #1e1e1e);
|
||||||
color: var(--accent);
|
color: var(--accent);
|
||||||
font-size: 2em;
|
font-size: 2em;
|
||||||
}
|
}
|
||||||
@@ -3233,6 +3233,23 @@ body.sidebar-right-hidden #sidebar-drag-zone {
|
|||||||
background: #000000;
|
background: #000000;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.audio-cover-backdrop {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 0;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: center;
|
||||||
|
background-size: cover;
|
||||||
|
border-radius: inherit;
|
||||||
|
transition: opacity 0.25s ease, filter 0.25s ease;
|
||||||
|
will-change: opacity, transform, filter;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
.audio-reactive-bg {
|
.audio-reactive-bg {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
@@ -3242,8 +3259,9 @@ body.sidebar-right-hidden #sidebar-drag-zone {
|
|||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
z-index: 0;
|
z-index: 0;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transition: opacity 0.3s ease;
|
transition: opacity 0.35s ease;
|
||||||
will-change: background, opacity;
|
will-change: opacity, transform;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.audio-cover-circle {
|
.audio-cover-circle {
|
||||||
@@ -3256,8 +3274,8 @@ body.sidebar-right-hidden #sidebar-drag-zone {
|
|||||||
transform: translate(-50%, -50%);
|
transform: translate(-50%, -50%);
|
||||||
width: 125px;
|
width: 125px;
|
||||||
height: 125px;
|
height: 125px;
|
||||||
max-width: calc(100% - 40px);
|
max-width: none;
|
||||||
max-height: calc(100% - 40px);
|
max-height: none;
|
||||||
aspect-ratio: 1 / 1;
|
aspect-ratio: 1 / 1;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
background-size: cover;
|
background-size: cover;
|
||||||
@@ -3270,29 +3288,13 @@ body.sidebar-right-hidden #sidebar-drag-zone {
|
|||||||
outline: none !important;
|
outline: none !important;
|
||||||
overflow: visible;
|
overflow: visible;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
z-index: 1;
|
z-index: 2;
|
||||||
will-change: transform, box-shadow, border-color;
|
will-change: transform, box-shadow, border-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Motion trail ghost — faint blurred echo of the eye that smears on fast movement */
|
/* Motion trail ghost — disabled so cover art is not projected around the eye */
|
||||||
.eye-trail-ghost {
|
.eye-trail-ghost {
|
||||||
position: absolute;
|
display: none !important;
|
||||||
top: 50%;
|
|
||||||
left: 50%;
|
|
||||||
width: clamp(300px, 72vmin, 700px);
|
|
||||||
height: clamp(300px, 72vmin, 700px);
|
|
||||||
max-width: calc(100% - 60px);
|
|
||||||
max-height: calc(100% - 80px);
|
|
||||||
border-radius: 50%;
|
|
||||||
background-size: cover;
|
|
||||||
background-position: center;
|
|
||||||
background-repeat: no-repeat;
|
|
||||||
background-color: transparent;
|
|
||||||
pointer-events: none;
|
|
||||||
z-index: 0;
|
|
||||||
opacity: 0;
|
|
||||||
will-change: transform, opacity, filter;
|
|
||||||
mix-blend-mode: screen;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.audio-eye-inner-canvas {
|
.audio-eye-inner-canvas {
|
||||||
@@ -3345,6 +3347,21 @@ body.sidebar-right-hidden #sidebar-drag-zone {
|
|||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.thumb > .sidebar-media-placeholder .audio-cover-backdrop,
|
||||||
|
.album-thumb-item > .sidebar-media-placeholder .audio-cover-backdrop,
|
||||||
|
.item-preview .sidebar-media-placeholder .audio-cover-backdrop,
|
||||||
|
.sidebar-activity-item .sidebar-media-placeholder .audio-cover-backdrop,
|
||||||
|
.recent-comment-media .sidebar-media-placeholder .audio-cover-backdrop,
|
||||||
|
.posts .sidebar-media-placeholder .audio-cover-backdrop,
|
||||||
|
.thumb > .sidebar-media-placeholder .audio-reactive-bg,
|
||||||
|
.album-thumb-item > .sidebar-media-placeholder .audio-reactive-bg,
|
||||||
|
.item-preview .sidebar-media-placeholder .audio-reactive-bg,
|
||||||
|
.sidebar-activity-item .sidebar-media-placeholder .audio-reactive-bg,
|
||||||
|
.recent-comment-media .sidebar-media-placeholder .audio-reactive-bg,
|
||||||
|
.posts .sidebar-media-placeholder .audio-reactive-bg {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
.thumb > .sidebar-media-placeholder > i,
|
.thumb > .sidebar-media-placeholder > i,
|
||||||
.album-thumb-item > .sidebar-media-placeholder > i,
|
.album-thumb-item > .sidebar-media-placeholder > i,
|
||||||
.item-preview .sidebar-media-placeholder > i {
|
.item-preview .sidebar-media-placeholder > i {
|
||||||
@@ -6098,6 +6115,7 @@ html[theme="f0ck95"] .admin-search button {
|
|||||||
/* f0ck95dARK */
|
/* f0ck95dARK */
|
||||||
html[theme='f0ck95d'] {
|
html[theme='f0ck95d'] {
|
||||||
--accent: #fff;
|
--accent: #fff;
|
||||||
|
--accent-rgb: 255, 255, 255;
|
||||||
--bg: #0e0f0f;
|
--bg: #0e0f0f;
|
||||||
--black: #000;
|
--black: #000;
|
||||||
--white: #fff;
|
--white: #fff;
|
||||||
@@ -6524,6 +6542,9 @@ div.posts>a:not(.notif-item) {
|
|||||||
background-repeat: no-repeat;
|
background-repeat: no-repeat;
|
||||||
background-size: cover;
|
background-size: cover;
|
||||||
background-position: center;
|
background-position: center;
|
||||||
|
transition: box-shadow 0.25s cubic-bezier(0.16, 1, 0.3, 1),
|
||||||
|
border-color 0.25s cubic-bezier(0.16, 1, 0.3, 1),
|
||||||
|
outline-color 0.25s cubic-bezier(0.16, 1, 0.3, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
div.posts>a:not(.notif-item)::before {
|
div.posts>a:not(.notif-item)::before {
|
||||||
@@ -7119,11 +7140,7 @@ body.layout-modern.sidebar-right-hidden .pagination-container-fluid {
|
|||||||
.content.fade-in-zoom,
|
.content.fade-in-zoom,
|
||||||
._204863.fade-in-zoom,
|
._204863.fade-in-zoom,
|
||||||
.item_title.fade-in-zoom,
|
.item_title.fade-in-zoom,
|
||||||
.item-title.fade-in-zoom,
|
.item-title.fade-in-zoom {
|
||||||
#onara-modal .content.fade-in-zoom,
|
|
||||||
#onara-modal ._204863.fade-in-zoom,
|
|
||||||
#onara-modal .item_title.fade-in-zoom,
|
|
||||||
#onara-modal .item-title.fade-in-zoom {
|
|
||||||
animation: contentFadeInZoom 0.35s cubic-bezier(0.16, 1, 0.3, 1) both !important;
|
animation: contentFadeInZoom 0.35s cubic-bezier(0.16, 1, 0.3, 1) both !important;
|
||||||
will-change: transform, opacity;
|
will-change: transform, opacity;
|
||||||
}
|
}
|
||||||
@@ -7131,11 +7148,7 @@ body.layout-modern.sidebar-right-hidden .pagination-container-fluid {
|
|||||||
.content.fade-out-zoom,
|
.content.fade-out-zoom,
|
||||||
._204863.fade-out-zoom,
|
._204863.fade-out-zoom,
|
||||||
.item_title.fade-out-zoom,
|
.item_title.fade-out-zoom,
|
||||||
.item-title.fade-out-zoom,
|
.item-title.fade-out-zoom {
|
||||||
#onara-modal .content.fade-out-zoom,
|
|
||||||
#onara-modal ._204863.fade-out-zoom,
|
|
||||||
#onara-modal .item_title.fade-out-zoom,
|
|
||||||
#onara-modal .item-title.fade-out-zoom {
|
|
||||||
animation: contentFadeOutZoom 0.18s cubic-bezier(0.3, 0, 0.8, 1) forwards !important;
|
animation: contentFadeOutZoom 0.18s cubic-bezier(0.3, 0, 0.8, 1) forwards !important;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
will-change: transform, opacity;
|
will-change: transform, opacity;
|
||||||
@@ -10486,10 +10499,11 @@ button#togglebg {
|
|||||||
#bg.fader-out,
|
#bg.fader-out,
|
||||||
.fader-out {
|
.fader-out {
|
||||||
opacity: 0 !important;
|
opacity: 0 !important;
|
||||||
|
transition: opacity 0.22s cubic-bezier(0.25, 0, 0.4, 1) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.fast-fade {
|
.fast-fade {
|
||||||
transition: opacity 0.4s cubic-bezier(0.4, 0, 0.2, 1) !important;
|
transition: opacity 0.2s cubic-bezier(0.25, 0, 0.4, 1) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.grid-transition {
|
.grid-transition {
|
||||||
@@ -10780,7 +10794,6 @@ input#s_avatar {
|
|||||||
|
|
||||||
.tag-name {
|
.tag-name {
|
||||||
color: var(--white, #fff);
|
color: var(--white, #fff);
|
||||||
font-weight: bold;
|
|
||||||
font-size: 1.1em;
|
font-size: 1.1em;
|
||||||
font-family: var(--font, monospace);
|
font-family: var(--font, monospace);
|
||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
@@ -20818,19 +20831,22 @@ a.lazy-thumb.filtered-upload-ghost:hover::after {
|
|||||||
ONARA ALTERNATIVE FLAVOR: Fullscreen See-Through Modal Wrapper
|
ONARA ALTERNATIVE FLAVOR: Fullscreen See-Through Modal Wrapper
|
||||||
========================================================================== */
|
========================================================================== */
|
||||||
|
|
||||||
/* Prevent scrolling on background document when Onara modal is active */
|
/* Prevent scrolling on background document when Onara modal is active or closing */
|
||||||
body.onara-modal-open {
|
body.onara-modal-open,
|
||||||
|
body.onara-modal-closing {
|
||||||
overflow: hidden !important;
|
overflow: hidden !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Override html's scrollbar-gutter: stable so the page fills full viewport width */
|
/* Override html's scrollbar-gutter: stable so the page fills full viewport width */
|
||||||
html:has(body.onara-modal-open) {
|
html:has(body.onara-modal-open),
|
||||||
|
html:has(body.onara-modal-closing) {
|
||||||
overflow: hidden !important;
|
overflow: hidden !important;
|
||||||
scrollbar-gutter: auto !important;
|
scrollbar-gutter: auto !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Background main page elements and containers are transparent so #bg canvas and grid shine through */
|
/* Background main page elements and containers are transparent so #bg canvas and grid shine through */
|
||||||
body.onara-modal-open .pagewrapper {
|
body.onara-modal-open .pagewrapper,
|
||||||
|
body.onara-modal-closing .pagewrapper {
|
||||||
pointer-events: none !important;
|
pointer-events: none !important;
|
||||||
user-select: none !important;
|
user-select: none !important;
|
||||||
margin-top: var(--navbar-h, 50px) !important;
|
margin-top: var(--navbar-h, 50px) !important;
|
||||||
@@ -20838,21 +20854,32 @@ body.onara-modal-open .pagewrapper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
body.onara-modal-open .index-container,
|
body.onara-modal-open .index-container,
|
||||||
body.onara-modal-open div.posts {
|
body.onara-modal-closing .index-container,
|
||||||
|
body.onara-modal-open div.posts,
|
||||||
|
body.onara-modal-closing div.posts {
|
||||||
background: transparent !important;
|
background: transparent !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
body.onara-modal-open .pagewrapper > *:not(.index-layout-wrapper):not(#main),
|
body.onara-modal-open:not(.onara-modal-closing) .pagewrapper > *:not(.index-layout-wrapper):not(#main),
|
||||||
body.onara-modal-open .index-container > *:not(.posts),
|
body.onara-modal-open:not(.onara-modal-closing) .index-container > *:not(.posts),
|
||||||
body.onara-modal-open #footbar,
|
body.onara-modal-open:not(.onara-modal-closing) #footbar,
|
||||||
body.onara-modal-open .pagination-container-fluid {
|
body.onara-modal-open:not(.onara-modal-closing) .pagination-container-fluid {
|
||||||
opacity: var(--onara-grid-dim-opacity, 1) !important;
|
opacity: var(--onara-grid-dim-opacity, 1) !important;
|
||||||
filter: none !important;
|
filter: none !important;
|
||||||
transition: opacity 0.25s cubic-bezier(0.16, 1, 0.3, 1) !important;
|
transition: opacity 0.25s cubic-bezier(0.16, 1, 0.3, 1) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
body.onara-modal-closing .pagewrapper > *:not(.index-layout-wrapper):not(#main),
|
||||||
|
body.onara-modal-closing .index-container > *:not(.posts),
|
||||||
|
body.onara-modal-closing #footbar,
|
||||||
|
body.onara-modal-closing .pagination-container-fluid {
|
||||||
|
opacity: 1 !important;
|
||||||
|
filter: none !important;
|
||||||
|
transition: opacity 0.2s cubic-bezier(0.25, 0, 0.4, 1) !important;
|
||||||
|
}
|
||||||
|
|
||||||
/* All non-active background thumbnails shine through with the configured grid opacity */
|
/* All non-active background thumbnails shine through with the configured grid opacity */
|
||||||
body.onara-modal-open .posts > a.thumb:not(.onara-active) {
|
body.onara-modal-open:not(.onara-modal-closing) .posts > a.thumb:not(.onara-active) {
|
||||||
opacity: var(--onara-grid-dim-opacity, 1) !important;
|
opacity: var(--onara-grid-dim-opacity, 1) !important;
|
||||||
filter: none !important;
|
filter: none !important;
|
||||||
box-shadow: none !important;
|
box-shadow: none !important;
|
||||||
@@ -20860,36 +20887,71 @@ body.onara-modal-open .posts > a.thumb:not(.onara-active) {
|
|||||||
transition: opacity 0.25s cubic-bezier(0.16, 1, 0.3, 1) !important;
|
transition: opacity 0.25s cubic-bezier(0.16, 1, 0.3, 1) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
body.onara-modal-open .posts > a.thumb:not(.onara-active)::after {
|
body.onara-modal-closing .posts > a.thumb:not(.onara-active) {
|
||||||
|
opacity: 1 !important;
|
||||||
|
filter: none !important;
|
||||||
|
box-shadow: none !important;
|
||||||
|
outline: none !important;
|
||||||
|
transition: opacity 0.2s cubic-bezier(0.25, 0, 0.4, 1) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.onara-modal-open:not(.onara-modal-closing) .posts > a.thumb:not(.onara-active)::after {
|
||||||
|
visibility: hidden !important;
|
||||||
|
opacity: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.onara-modal-closing .posts > a.thumb:not(.onara-active)::after {
|
||||||
visibility: hidden !important;
|
visibility: hidden !important;
|
||||||
opacity: 0 !important;
|
opacity: 0 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ONLY the single currently active thumbnail in the background shines through with prominent glow */
|
/* ONLY the single currently active thumbnail in the background shines through with prominent glow */
|
||||||
body.onara-modal-open .posts > a.thumb.onara-active {
|
body.onara-modal-open:not(.onara-modal-closing) .posts > a.thumb.onara-active {
|
||||||
opacity: 1 !important;
|
opacity: 1 !important;
|
||||||
filter: none !important;
|
filter: none !important;
|
||||||
box-shadow: 0 0 0 3px var(--accent, #9f0), 0 0 20px var(--accent, #9f0), 0 0 35px rgba(255, 255, 255, 0.9) !important;
|
box-shadow: 0 0 0 3px var(--accent, #9f0), 0 0 20px var(--accent, #9f0), 0 0 35px rgba(255, 255, 255, 0.9) !important;
|
||||||
z-index: 50 !important;
|
z-index: 52 !important;
|
||||||
position: relative !important;
|
position: relative !important;
|
||||||
|
transition: box-shadow 0.28s cubic-bezier(0.16, 1, 0.3, 1), opacity 0.25s ease !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
body.onara-modal-open .posts > a.thumb.onara-active::after {
|
body.onara-modal-open:not(.onara-modal-closing) .posts > a.thumb.onara-active::after {
|
||||||
visibility: visible !important;
|
visibility: visible !important;
|
||||||
opacity: 1 !important;
|
opacity: 1 !important;
|
||||||
|
transition: opacity 0.25s ease !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
body.onara-modal-open .posts > a.thumb.loaded {
|
/* Smooth fade-out of active thumbnail border / glow when Onara modal closes */
|
||||||
|
body.onara-modal-closing .posts > a.thumb.onara-active,
|
||||||
|
body.onara-modal-closing .posts > a.thumb.loaded.onara-active {
|
||||||
|
opacity: 1 !important;
|
||||||
|
filter: none !important;
|
||||||
|
box-shadow: 0 0 0 0 transparent, 0 0 0 0 transparent, 0 0 0 0 transparent !important;
|
||||||
|
outline-color: transparent !important;
|
||||||
|
border-color: transparent !important;
|
||||||
|
z-index: 50 !important;
|
||||||
|
position: relative !important;
|
||||||
|
transition: box-shadow 0.2s cubic-bezier(0.25, 0, 0.4, 1),
|
||||||
|
outline-color 0.2s cubic-bezier(0.25, 0, 0.4, 1),
|
||||||
|
border-color 0.2s cubic-bezier(0.25, 0, 0.4, 1),
|
||||||
|
opacity 0.2s cubic-bezier(0.25, 0, 0.4, 1) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.onara-modal-closing .posts > a.thumb.onara-active::after {
|
||||||
|
opacity: 0 !important;
|
||||||
|
transition: opacity 0.2s cubic-bezier(0.25, 0, 0.4, 1) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.onara-modal-open .posts > a.thumb.loaded,
|
||||||
|
body.onara-modal-closing .posts > a.thumb.loaded {
|
||||||
opacity: 1 !important;
|
opacity: 1 !important;
|
||||||
filter: none !important;
|
filter: none !important;
|
||||||
z-index: 50 !important;
|
z-index: 50 !important;
|
||||||
position: relative !important;
|
position: relative !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
body.onara-modal-open #bg,
|
body.onara-modal-open:not(.onara-modal-closing) #bg,
|
||||||
body.onara-modal-open #bg.fader-in,
|
body.onara-modal-open:not(.onara-modal-closing) #bg.fader-in {
|
||||||
body.onara-modal-open #bg.fader-out,
|
|
||||||
body.onara-modal-open #bg.fast-fade {
|
|
||||||
z-index: 51 !important;
|
z-index: 51 !important;
|
||||||
cursor: pointer !important;
|
cursor: pointer !important;
|
||||||
transition: width 0.3s ease-in-out !important;
|
transition: width 0.3s ease-in-out !important;
|
||||||
@@ -20897,6 +20959,18 @@ body.onara-modal-open #bg.fast-fade {
|
|||||||
opacity: var(--bg-canvas-opacity, 1.0) !important;
|
opacity: var(--bg-canvas-opacity, 1.0) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
body.onara-modal-closing #bg,
|
||||||
|
body.onara-modal-closing #bg.fader-in,
|
||||||
|
body.onara-modal-closing #bg.fader-out,
|
||||||
|
body.onara-modal-closing #bg.fast-fade,
|
||||||
|
body.onara-modal-open #bg.fader-out {
|
||||||
|
z-index: 51 !important;
|
||||||
|
cursor: default !important;
|
||||||
|
pointer-events: none !important;
|
||||||
|
opacity: 0 !important;
|
||||||
|
transition: opacity 0.2s cubic-bezier(0.25, 0, 0.4, 1) !important;
|
||||||
|
}
|
||||||
|
|
||||||
/* Navbar and right sidebar stay fully visible, sharp, and interactive */
|
/* Navbar and right sidebar stay fully visible, sharp, and interactive */
|
||||||
body.onara-modal-open nav.navbar {
|
body.onara-modal-open nav.navbar {
|
||||||
opacity: 1 !important;
|
opacity: 1 !important;
|
||||||
@@ -20962,33 +21036,34 @@ body.onara-modal-open .admin-bar.open {
|
|||||||
overscroll-behavior-x: contain !important;
|
overscroll-behavior-x: contain !important;
|
||||||
box-sizing: border-box !important;
|
box-sizing: border-box !important;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transform: scale(0.99);
|
|
||||||
transition: opacity 0.28s cubic-bezier(0.16, 1, 0.3, 1),
|
|
||||||
transform 0.28s cubic-bezier(0.16, 1, 0.3, 1),
|
|
||||||
right 0.3s ease-in-out !important;
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
will-change: opacity, backdrop-filter;
|
||||||
|
transition: right 0.3s ease-in-out !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.onara-modal-wrapper::-webkit-scrollbar {
|
.onara-modal-wrapper::-webkit-scrollbar {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
body.onara-modal-open .onara-modal-wrapper,
|
/* Static server-load fallback when page is directly visited on an onara item URL */
|
||||||
.onara-modal-wrapper.onara-fade-in {
|
body.onara-modal-open .onara-modal-wrapper:not(.onara-fade-out):not(.onara-fade-in) {
|
||||||
display: flex !important;
|
display: flex !important;
|
||||||
opacity: 1 !important;
|
opacity: 1 !important;
|
||||||
transform: scale(1) !important;
|
|
||||||
pointer-events: auto !important;
|
pointer-events: auto !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Silky animated opening */
|
||||||
|
.onara-modal-wrapper.onara-fade-in {
|
||||||
|
display: flex !important;
|
||||||
|
animation: onaraModalFadeIn 0.28s cubic-bezier(0.16, 1, 0.3, 1) forwards !important;
|
||||||
|
pointer-events: auto !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Silky animated closing */
|
||||||
.onara-modal-wrapper.onara-fade-out {
|
.onara-modal-wrapper.onara-fade-out {
|
||||||
display: flex !important;
|
display: flex !important;
|
||||||
opacity: 0 !important;
|
animation: onaraModalFadeOut 0.22s cubic-bezier(0.25, 0, 0.4, 1) forwards !important;
|
||||||
transform: scale(0.99) !important;
|
|
||||||
pointer-events: none !important;
|
pointer-events: none !important;
|
||||||
transition: opacity 0.24s cubic-bezier(0.3, 0, 0.8, 1),
|
|
||||||
transform 0.24s cubic-bezier(0.3, 0, 0.8, 1),
|
|
||||||
right 0.3s ease-in-out !important;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Sidebar hidden: modal expands to right edge */
|
/* Sidebar hidden: modal expands to right edge */
|
||||||
@@ -21013,25 +21088,33 @@ body.sidebar-right-hidden .onara-modal-wrapper {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes onaraFadeIn {
|
@keyframes onaraModalFadeIn {
|
||||||
from {
|
0% {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transform: scale(0.99);
|
backdrop-filter: blur(0px);
|
||||||
|
-webkit-backdrop-filter: blur(0px);
|
||||||
|
background: rgba(0, 0, 0, 0);
|
||||||
}
|
}
|
||||||
to {
|
100% {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
transform: scale(1);
|
backdrop-filter: blur(var(--onara-backdrop-blur, 5px)) saturate(130%);
|
||||||
|
-webkit-backdrop-filter: blur(var(--onara-backdrop-blur, 5px)) saturate(130%);
|
||||||
|
background: rgba(0, 0, 0, var(--onara-bg-opacity, 0.6));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes onaraFadeOut {
|
@keyframes onaraModalFadeOut {
|
||||||
from {
|
0% {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
transform: scale(1);
|
backdrop-filter: blur(var(--onara-backdrop-blur, 5px)) saturate(130%);
|
||||||
|
-webkit-backdrop-filter: blur(var(--onara-backdrop-blur, 5px)) saturate(130%);
|
||||||
|
background: rgba(0, 0, 0, var(--onara-bg-opacity, 0.6));
|
||||||
}
|
}
|
||||||
to {
|
100% {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transform: scale(0.99);
|
backdrop-filter: blur(0px);
|
||||||
|
-webkit-backdrop-filter: blur(0px);
|
||||||
|
background: rgba(0, 0, 0, 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -21059,6 +21142,78 @@ body.sidebar-right-hidden .onara-modal-wrapper {
|
|||||||
padding: 20px 0 80px 0 !important;
|
padding: 20px 0 80px 0 !important;
|
||||||
box-sizing: border-box !important;
|
box-sizing: border-box !important;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
will-change: transform, opacity;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Smooth modal content elevation during entrance & exit */
|
||||||
|
.onara-modal-wrapper.onara-fade-in .onara-modal-content {
|
||||||
|
animation: onaraContentScaleIn 0.32s cubic-bezier(0.16, 1, 0.3, 1) forwards !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.onara-modal-wrapper.onara-fade-out .onara-modal-content {
|
||||||
|
animation: onaraContentScaleOut 0.2s cubic-bezier(0.25, 0, 0.4, 1) forwards !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes onaraContentScaleIn {
|
||||||
|
0% {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(16px) scale(0.985);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0) scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes onaraContentScaleOut {
|
||||||
|
0% {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0) scale(1);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(10px) scale(0.985);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Keep .blahlol completely static on new item load — never fade in or animate */
|
||||||
|
.blahlol,
|
||||||
|
.blahlol *,
|
||||||
|
#onara-item-mount .blahlol,
|
||||||
|
#onara-item-mount .blahlol * {
|
||||||
|
animation: none !important;
|
||||||
|
transition: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blahlol,
|
||||||
|
#onara-item-mount .blahlol {
|
||||||
|
opacity: 1 !important;
|
||||||
|
transform: none !important;
|
||||||
|
visibility: visible !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#onara-item-mount .item-layout-container {
|
||||||
|
animation: none !important;
|
||||||
|
transform: none !important;
|
||||||
|
opacity: 1 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Modern loading shimmer spinner inside mount */
|
||||||
|
.onara-mount-loading {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-height: 360px;
|
||||||
|
width: 100%;
|
||||||
|
color: var(--accent, #99ff00);
|
||||||
|
font-size: 2rem;
|
||||||
|
opacity: 0;
|
||||||
|
animation: onaraMountSpinnerFade 0.25s 0.12s cubic-bezier(0.16, 1, 0.3, 1) forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes onaraMountSpinnerFade {
|
||||||
|
from { opacity: 0; transform: scale(0.9); }
|
||||||
|
to { opacity: 0.75; transform: scale(1); }
|
||||||
}
|
}
|
||||||
|
|
||||||
#onara-item-mount {
|
#onara-item-mount {
|
||||||
@@ -22036,6 +22191,11 @@ div#flash,
|
|||||||
bottom: 58px;
|
bottom: 58px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* YouTube controls occupy ~56px at the bottom — lift strip above them */
|
||||||
|
.album-gallery-container.is-youtube-active .album-thumbnails-strip {
|
||||||
|
bottom: 62px;
|
||||||
|
}
|
||||||
|
|
||||||
.album-gallery-container.is-video-active .image-modal-btn,
|
.album-gallery-container.is-video-active .image-modal-btn,
|
||||||
.album-gallery-container.is-audio-active .image-modal-btn {
|
.album-gallery-container.is-audio-active .image-modal-btn {
|
||||||
display: none !important;
|
display: none !important;
|
||||||
@@ -22046,6 +22206,9 @@ div#flash,
|
|||||||
.album-gallery-container.is-audio-active .album-thumbnails-strip {
|
.album-gallery-container.is-audio-active .album-thumbnails-strip {
|
||||||
bottom: 48px;
|
bottom: 48px;
|
||||||
}
|
}
|
||||||
|
.album-gallery-container.is-youtube-active .album-thumbnails-strip {
|
||||||
|
bottom: 52px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.album-thumb-item {
|
.album-thumb-item {
|
||||||
|
|||||||
@@ -151,7 +151,7 @@
|
|||||||
if (activePostId) untaggedSpan.dataset.itemId = activePostId;
|
if (activePostId) untaggedSpan.dataset.itemId = activePostId;
|
||||||
const lbl = document.createElement("span");
|
const lbl = document.createElement("span");
|
||||||
lbl.className = "rating-label";
|
lbl.className = "rating-label";
|
||||||
lbl.textContent = "untagged";
|
lbl.textContent = "unrated";
|
||||||
untaggedSpan.appendChild(lbl);
|
untaggedSpan.appendChild(lbl);
|
||||||
inner.insertAdjacentElement("afterbegin", untaggedSpan);
|
inner.insertAdjacentElement("afterbegin", untaggedSpan);
|
||||||
}
|
}
|
||||||
|
|||||||
+938
-266
File diff suppressed because it is too large
Load Diff
@@ -556,7 +556,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ── Filter Presets CRUD ───────────────────────────────────────────────────
|
// ── Filter Presets CRUD ───────────────────────────────────────────────────
|
||||||
const PRESETS_LABELS = { mode: { 0: 'SFW', 1: 'NSFW', 2: 'Untagged', 3: 'All', 4: 'NSFL' } };
|
const PRESETS_LABELS = { mode: { 0: 'SFW', 1: 'NSFW', 2: 'Unrated', 3: 'All', 4: 'NSFL' } };
|
||||||
function getPresets() { try { return JSON.parse(localStorage.getItem(PRESETS_KEY) || '[]'); } catch { return []; } }
|
function getPresets() { try { return JSON.parse(localStorage.getItem(PRESETS_KEY) || '[]'); } catch { return []; } }
|
||||||
function savePresets(arr) { localStorage.setItem(PRESETS_KEY, JSON.stringify(arr)); }
|
function savePresets(arr) { localStorage.setItem(PRESETS_KEY, JSON.stringify(arr)); }
|
||||||
|
|
||||||
@@ -3060,7 +3060,7 @@
|
|||||||
if (addTagSendBtn) addTagSendBtn.addEventListener('click', submitTag);
|
if (addTagSendBtn) addTagSendBtn.addEventListener('click', submitTag);
|
||||||
|
|
||||||
// ── Filter panel ──────────────────────────────────────────────────────────
|
// ── Filter panel ──────────────────────────────────────────────────────────
|
||||||
const modeLabels = { 0: 'SFW', 1: 'NSFW', 2: 'Untagged', 3: 'All', 4: 'NSFL' };
|
const modeLabels = { 0: 'SFW', 1: 'NSFW', 2: 'Unrated', 3: 'All', 4: 'NSFL' };
|
||||||
|
|
||||||
filterOpenBtn.addEventListener('click', () => {
|
filterOpenBtn.addEventListener('click', () => {
|
||||||
pending = { ...applied, tags: [...applied.tags] }; syncPanelUI();
|
pending = { ...applied, tags: [...applied.tags] }; syncPanelUI();
|
||||||
|
|||||||
@@ -1498,7 +1498,7 @@
|
|||||||
} else {
|
} else {
|
||||||
document.documentElement.classList.remove('blur-untagged-active');
|
document.documentElement.classList.remove('blur-untagged-active');
|
||||||
}
|
}
|
||||||
showStatus(enabled ? 'Untagged blurring enabled!' : 'Untagged blurring disabled!', 'success');
|
showStatus(enabled ? 'Unrated blurring enabled!' : 'Unrated blurring disabled!', 'success');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
const blurDetailToggle = document.getElementById('blur_detail_toggle');
|
const blurDetailToggle = document.getElementById('blur_detail_toggle');
|
||||||
@@ -1596,10 +1596,16 @@
|
|||||||
onaraEnabledToggle.checked = stored !== null ? stored === 'true' : sessionOnara;
|
onaraEnabledToggle.checked = stored !== null ? stored === 'true' : sessionOnara;
|
||||||
// Reflect current state into window.onara so isOnaraActive() sees it
|
// Reflect current state into window.onara so isOnaraActive() sees it
|
||||||
window.onara = onaraEnabledToggle.checked;
|
window.onara = onaraEnabledToggle.checked;
|
||||||
|
try {
|
||||||
|
document.cookie = `f0ck_onara=${onaraEnabledToggle.checked ? '1' : '0'}; path=/; max-age=31536000; SameSite=Lax`;
|
||||||
|
} catch {}
|
||||||
|
|
||||||
onaraEnabledToggle.addEventListener('change', () => {
|
onaraEnabledToggle.addEventListener('change', () => {
|
||||||
const enabled = onaraEnabledToggle.checked;
|
const enabled = onaraEnabledToggle.checked;
|
||||||
localStorage.setItem(LS_KEY, String(enabled));
|
localStorage.setItem(LS_KEY, String(enabled));
|
||||||
|
try {
|
||||||
|
document.cookie = `f0ck_onara=${enabled ? '1' : '0'}; path=/; max-age=31536000; SameSite=Lax`;
|
||||||
|
} catch {}
|
||||||
window.onara = enabled;
|
window.onara = enabled;
|
||||||
showStatus('Onara viewer ' + (enabled ? 'enabled' : 'disabled') + '.', 'success');
|
showStatus('Onara viewer ' + (enabled ? 'enabled' : 'disabled') + '.', 'success');
|
||||||
});
|
});
|
||||||
|
|||||||
+273
-10
@@ -1272,14 +1272,19 @@
|
|||||||
|
|
||||||
const isTagFeed = options.isTagFeed || false;
|
const isTagFeed = options.isTagFeed || false;
|
||||||
const tagContext = options.tag || null;
|
const tagContext = options.tag || null;
|
||||||
|
const isFavsFeed = options.isFavsFeed || false;
|
||||||
|
const favsUser = options.favsUser || null;
|
||||||
const isStrict = isStrictMode();
|
const isStrict = isStrictMode();
|
||||||
const strictSuffix = (isTagFeed && isStrict) ? '?strict=1' : '';
|
const strictSuffix = (isTagFeed && isStrict) ? '?strict=1' : '';
|
||||||
const targetSubHash = video.target_subf0ck_slug ? `#${video.target_subf0ck_slug}` : '';
|
const targetSubHash = video.target_subf0ck_slug ? `#${video.target_subf0ck_slug}` : '';
|
||||||
const targetHref = (isTagFeed && tagContext)
|
const targetHref = (isFavsFeed && favsUser)
|
||||||
? `/tag/${encodeURIComponent(tagContext).replace(/%2C/g, ',').replace(/%20/g, ' ')}/${videoKey}${strictSuffix}${targetSubHash}`
|
? `/user/${encodeURIComponent(favsUser)}/favs/${videoKey}${targetSubHash}`
|
||||||
: `/${videoKey}${targetSubHash}`;
|
: (isTagFeed && tagContext)
|
||||||
|
? `/tag/${encodeURIComponent(tagContext).replace(/%2C/g, ',').replace(/%20/g, ' ')}/${videoKey}${strictSuffix}${targetSubHash}`
|
||||||
|
: `/${videoKey}${targetSubHash}`;
|
||||||
const activeClass = options.isActive ? ' active-tag-item' : '';
|
const activeClass = options.isActive ? ' active-tag-item' : '';
|
||||||
|
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<div class="sidebar-video-card${activeClass}" data-id="${video.id}" data-slug="${escapeHtml(video.slug || '')}" data-file="${escapeHtml(activeDest)}" data-mime="${escapeHtml(activeMime)}" data-ext="${escapeHtml(ext ? ext.toLowerCase() : '')}" data-mode="${rClass}" data-target-subf0ck="${escapeHtml(video.target_subf0ck_slug || '')}" data-personalized="${video.personalized ? 'true' : 'false'}"${devScoreAttr}>
|
<div class="sidebar-video-card${activeClass}" data-id="${video.id}" data-slug="${escapeHtml(video.slug || '')}" data-file="${escapeHtml(activeDest)}" data-mime="${escapeHtml(activeMime)}" data-ext="${escapeHtml(ext ? ext.toLowerCase() : '')}" data-mode="${rClass}" data-target-subf0ck="${escapeHtml(video.target_subf0ck_slug || '')}" data-personalized="${video.personalized ? 'true' : 'false'}"${devScoreAttr}>
|
||||||
<a href="${targetHref}" 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">
|
||||||
@@ -1554,12 +1559,22 @@
|
|||||||
if (txt) ids.add(txt);
|
if (txt) ids.add(txt);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. From URL pathname (/tag/:tag/:slugOrId or /:slugOrId)
|
// 2. From URL pathname (/tag/:tag/:slugOrId, /user/:user/favs/:slugOrId, /user/:user/:slugOrId, /:slugOrId)
|
||||||
const segments = window.location.pathname.split('/').filter(Boolean);
|
const segments = window.location.pathname.split('/').filter(Boolean);
|
||||||
if (segments.length >= 2 && segments[0] === 'tag') {
|
if (segments.length >= 2 && segments[0] === 'tag') {
|
||||||
if (segments[2]) {
|
if (segments[2]) {
|
||||||
ids.add(decodeURIComponent(segments[2]).trim());
|
ids.add(decodeURIComponent(segments[2]).trim());
|
||||||
}
|
}
|
||||||
|
} else if (segments.length >= 4 && segments[0] === 'user' && segments[2] === 'favs') {
|
||||||
|
// /user/:user/favs/:itemid
|
||||||
|
if (segments[3] !== 'p') {
|
||||||
|
ids.add(decodeURIComponent(segments[3]).trim());
|
||||||
|
}
|
||||||
|
} else if (segments.length >= 3 && segments[0] === 'user' && segments[2] !== 'favs') {
|
||||||
|
// /user/:user/:itemid (non-favs)
|
||||||
|
if (segments[2] !== 'p') {
|
||||||
|
ids.add(decodeURIComponent(segments[2]).trim());
|
||||||
|
}
|
||||||
} else if (segments.length === 1) {
|
} 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'];
|
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])) {
|
if (!forbidden.includes(segments[0])) {
|
||||||
@@ -1567,6 +1582,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
return Array.from(ids).filter(Boolean);
|
return Array.from(ids).filter(Boolean);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1840,6 +1856,237 @@
|
|||||||
card.classList.add('active-tag-item');
|
card.classList.add('active-tag-item');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
container.addEventListener('scroll', () => {
|
||||||
|
if (tagFeedLoading || !tagHasMore || !currentTag) return;
|
||||||
|
const nearBottom = container.scrollTop + container.clientHeight >= container.scrollHeight - 250;
|
||||||
|
if (nearBottom) {
|
||||||
|
loadMoreTagFeed();
|
||||||
|
}
|
||||||
|
}, { passive: true });
|
||||||
|
};
|
||||||
|
|
||||||
|
// ── Dedicated Favs Feed (mirrors Tag Feed for /user/x/favs context) ─────
|
||||||
|
let currentFavsUser = null;
|
||||||
|
let favsOffset = 0;
|
||||||
|
let favsTotal = 0;
|
||||||
|
let favsFeedLoaded = false;
|
||||||
|
let favsFeedLoading = false;
|
||||||
|
let favsFeedNeedsReload = false;
|
||||||
|
let favsHasMore = false;
|
||||||
|
let favsSentinel = null;
|
||||||
|
let favsObserver = null;
|
||||||
|
|
||||||
|
const getCurrentFavsUser = () => {
|
||||||
|
// Match /user/someuser/favs or /user/someuser/favs/itemid
|
||||||
|
const m = window.location.pathname.match(/^\/user\/([^/?#]+)\/favs/);
|
||||||
|
if (m) return decodeURIComponent(m[1]);
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
let isReCenteringFavsFeed = false;
|
||||||
|
|
||||||
|
const highlightActiveFavsCard = (scrollIntoView = false) => {
|
||||||
|
const identifiers = getCurrentItemIdentifiers();
|
||||||
|
const container = document.getElementById('sidebar-favs-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 isFavsTabActive = document.querySelector('.sidebar-tab.active')?.dataset.tab === 'favs' &&
|
||||||
|
container.style.display !== 'none';
|
||||||
|
if (isFavsTabActive) {
|
||||||
|
try {
|
||||||
|
const cr = container.getBoundingClientRect();
|
||||||
|
const mr = matchedCard.getBoundingClientRect();
|
||||||
|
container.scrollTo({ top: Math.max(0, container.scrollTop + (mr.top - cr.top) - (container.clientHeight / 2) + (mr.height / 2)), behavior: 'smooth' });
|
||||||
|
} catch (_) {
|
||||||
|
matchedCard.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (scrollIntoView && !matchedCard && identifiers.length > 0 && currentFavsUser && !isReCenteringFavsFeed && !favsFeedLoading) {
|
||||||
|
isReCenteringFavsFeed = true;
|
||||||
|
loadFavsFeed(true, identifiers[0]).finally(() => {
|
||||||
|
isReCenteringFavsFeed = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadFavsFeed = async (reset = false, focusId = null) => {
|
||||||
|
const container = document.getElementById('sidebar-favs-container');
|
||||||
|
const itemsContainer = container?.querySelector('.sidebar-favs-items-container');
|
||||||
|
if (!container || !itemsContainer || favsFeedLoading) return;
|
||||||
|
if (!currentFavsUser) return;
|
||||||
|
|
||||||
|
favsFeedLoading = true;
|
||||||
|
favsFeedNeedsReload = false;
|
||||||
|
|
||||||
|
if (reset) {
|
||||||
|
favsOffset = 0;
|
||||||
|
itemsContainer.innerHTML = `
|
||||||
|
<div class="sidebar-loading-state">
|
||||||
|
<i class="fa-solid fa-circle-notch fa-spin"></i>
|
||||||
|
<span>Loading favorites...</span>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const mode = typeof window.activeMode !== 'undefined' ? window.activeMode : '';
|
||||||
|
let url = `/api/v2/favs-feed?user=${encodeURIComponent(currentFavsUser)}&order=desc&offset=${favsOffset}&limit=20&mode=${mode}`;
|
||||||
|
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 || [];
|
||||||
|
favsTotal = data.total || 0;
|
||||||
|
favsOffset = (data.offset ?? favsOffset) + items.length;
|
||||||
|
favsHasMore = typeof data.hasMore !== 'undefined' ? !!data.hasMore : (favsOffset < favsTotal);
|
||||||
|
|
||||||
|
// Update header
|
||||||
|
const countEl = container.querySelector('.sidebar-favs-count');
|
||||||
|
if (countEl) countEl.textContent = `(${favsTotal})`;
|
||||||
|
|
||||||
|
const nameEl = container.querySelector('.sidebar-favs-name');
|
||||||
|
if (nameEl) nameEl.textContent = currentFavsUser + "'s favs";
|
||||||
|
|
||||||
|
if (data.success && items.length > 0) {
|
||||||
|
favsFeedLoaded = 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, { isFavsFeed: true, favsUser: currentFavsUser, isActive });
|
||||||
|
});
|
||||||
|
|
||||||
|
if (reset) {
|
||||||
|
itemsContainer.innerHTML = html;
|
||||||
|
setTimeout(() => highlightActiveFavsCard(true), 60);
|
||||||
|
} else {
|
||||||
|
itemsContainer.insertAdjacentHTML('beforeend', html);
|
||||||
|
highlightActiveFavsCard(false);
|
||||||
|
}
|
||||||
|
initFavsSentinel();
|
||||||
|
} 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-regular fa-heart" style="font-size:1.8em;opacity:0.4;margin-bottom:8px;display:block;"></i>
|
||||||
|
No favorites found
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[FAVS-FEED] Failed:', 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 favorites</div>`;
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
favsFeedLoading = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const initFavsSentinel = () => {
|
||||||
|
const container = document.getElementById('sidebar-favs-container');
|
||||||
|
const itemsContainer = container?.querySelector('.sidebar-favs-items-container');
|
||||||
|
if (!container || !itemsContainer) return;
|
||||||
|
|
||||||
|
if (favsSentinel && favsSentinel.parentNode) favsSentinel.remove();
|
||||||
|
if (favsObserver) { favsObserver.disconnect(); favsObserver = null; }
|
||||||
|
if (!favsHasMore) return;
|
||||||
|
|
||||||
|
favsSentinel = document.createElement('div');
|
||||||
|
favsSentinel.className = 'sidebar-tag-sentinel';
|
||||||
|
favsSentinel.style.cssText = 'height: 20px; margin: 10px 0;';
|
||||||
|
itemsContainer.appendChild(favsSentinel);
|
||||||
|
|
||||||
|
favsObserver = new IntersectionObserver((entries) => {
|
||||||
|
if (entries[0].isIntersecting && !favsFeedLoading && favsHasMore) {
|
||||||
|
loadFavsFeed(false);
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
root: container,
|
||||||
|
rootMargin: '200px'
|
||||||
|
});
|
||||||
|
favsObserver.observe(favsSentinel);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
const updateFavsTabVisibility = (forceSwitchToFavs = false) => {
|
||||||
|
const user = getCurrentFavsUser();
|
||||||
|
const favsBtn = document.getElementById('sidebar-tab-favs');
|
||||||
|
if (!favsBtn) return;
|
||||||
|
|
||||||
|
if (user) {
|
||||||
|
favsBtn.style.display = 'inline-flex';
|
||||||
|
favsBtn.title = `${user}'s favs`;
|
||||||
|
|
||||||
|
if (currentFavsUser !== user) {
|
||||||
|
currentFavsUser = user;
|
||||||
|
favsOffset = 0;
|
||||||
|
favsFeedLoaded = false;
|
||||||
|
favsFeedNeedsReload = true;
|
||||||
|
switchSidebarTab('favs');
|
||||||
|
} else {
|
||||||
|
if (forceSwitchToFavs || document.querySelector('.sidebar-tab.active')?.dataset.tab !== 'favs') {
|
||||||
|
switchSidebarTab('favs');
|
||||||
|
} else {
|
||||||
|
highlightActiveFavsCard(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
favsBtn.style.display = 'none';
|
||||||
|
currentFavsUser = null;
|
||||||
|
favsFeedLoaded = false;
|
||||||
|
|
||||||
|
const activeTab = document.querySelector('.sidebar-tab.active')?.dataset.tab;
|
||||||
|
if (activeTab === 'favs') {
|
||||||
|
// Fall back to tag tab if available, else recommendations
|
||||||
|
if (getCurrentTag()) {
|
||||||
|
switchSidebarTab('tag');
|
||||||
|
} else {
|
||||||
|
switchSidebarTab('recommendations');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const bindFavsContainerEvents = () => {
|
||||||
|
const container = document.getElementById('sidebar-favs-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');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
container.addEventListener('scroll', () => {
|
||||||
|
if (favsFeedLoading || !favsHasMore || !currentFavsUser) return;
|
||||||
|
const nearBottom = container.scrollTop + container.clientHeight >= container.scrollHeight - 250;
|
||||||
|
if (nearBottom) {
|
||||||
|
loadFavsFeed(false);
|
||||||
|
}
|
||||||
|
}, { passive: true });
|
||||||
};
|
};
|
||||||
|
|
||||||
const switchSidebarTab = (tabName) => {
|
const switchSidebarTab = (tabName) => {
|
||||||
@@ -1848,14 +2095,14 @@
|
|||||||
}
|
}
|
||||||
const normalizedTab = (tabName === 'videos' || tabName === 'recommendations')
|
const normalizedTab = (tabName === 'videos' || tabName === 'recommendations')
|
||||||
? 'recommendations'
|
? 'recommendations'
|
||||||
: (tabName === 'tag' ? 'tag' : tabName);
|
: (tabName === 'tag' ? 'tag' : (tabName === 'favs' ? 'favs' : 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')
|
const tTab = (t.dataset.tab === 'videos' || t.dataset.tab === 'recommendations')
|
||||||
? 'recommendations'
|
? 'recommendations'
|
||||||
: (t.dataset.tab === 'tag' ? 'tag' : t.dataset.tab);
|
: (t.dataset.tab === 'tag' ? 'tag' : (t.dataset.tab === 'favs' ? 'favs' : t.dataset.tab));
|
||||||
if (tTab === normalizedTab) {
|
if (tTab === normalizedTab) {
|
||||||
t.classList.add('active');
|
t.classList.add('active');
|
||||||
} else {
|
} else {
|
||||||
@@ -1866,7 +2113,7 @@
|
|||||||
contents.forEach(c => {
|
contents.forEach(c => {
|
||||||
const cTab = (c.dataset.tabContent === 'videos' || c.dataset.tabContent === 'recommendations')
|
const cTab = (c.dataset.tabContent === 'videos' || c.dataset.tabContent === 'recommendations')
|
||||||
? 'recommendations'
|
? 'recommendations'
|
||||||
: (c.dataset.tabContent === 'tag' ? 'tag' : c.dataset.tabContent);
|
: (c.dataset.tabContent === 'tag' ? 'tag' : (c.dataset.tabContent === 'favs' ? 'favs' : 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';
|
||||||
@@ -1890,6 +2137,12 @@
|
|||||||
} else {
|
} else {
|
||||||
setTimeout(() => highlightActiveTagCard(true), 60);
|
setTimeout(() => highlightActiveTagCard(true), 60);
|
||||||
}
|
}
|
||||||
|
} else if (normalizedTab === 'favs') {
|
||||||
|
if (favsFeedNeedsReload || !favsFeedLoaded) {
|
||||||
|
loadFavsFeed(true);
|
||||||
|
} else {
|
||||||
|
setTimeout(() => highlightActiveFavsCard(true), 60);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
window.switchSidebarTab = switchSidebarTab;
|
window.switchSidebarTab = switchSidebarTab;
|
||||||
@@ -1901,7 +2154,7 @@
|
|||||||
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') {
|
if (btn.dataset.tab !== 'tag' && btn.dataset.tab !== 'favs') {
|
||||||
userManuallySelectedNonTagTab = true;
|
userManuallySelectedNonTagTab = true;
|
||||||
} else {
|
} else {
|
||||||
userManuallySelectedNonTagTab = false;
|
userManuallySelectedNonTagTab = false;
|
||||||
@@ -1917,7 +2170,9 @@
|
|||||||
savedTab = localStorage.getItem('sidebar_active_tab') || 'comments';
|
savedTab = localStorage.getItem('sidebar_active_tab') || 'comments';
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
|
|
||||||
if (getCurrentTag()) {
|
if (getCurrentFavsUser()) {
|
||||||
|
switchSidebarTab('favs');
|
||||||
|
} else if (getCurrentTag()) {
|
||||||
switchSidebarTab('tag');
|
switchSidebarTab('tag');
|
||||||
} else if (savedTab === 'videos' || savedTab === 'recommendations') {
|
} else if (savedTab === 'videos' || savedTab === 'recommendations') {
|
||||||
switchSidebarTab('recommendations');
|
switchSidebarTab('recommendations');
|
||||||
@@ -2154,15 +2409,23 @@
|
|||||||
initSidebarTabs();
|
initSidebarTabs();
|
||||||
initTagOrderButton();
|
initTagOrderButton();
|
||||||
bindTagContainerEvents();
|
bindTagContainerEvents();
|
||||||
|
bindFavsContainerEvents();
|
||||||
updateTagTabVisibility();
|
updateTagTabVisibility();
|
||||||
|
updateFavsTabVisibility();
|
||||||
document.addEventListener('f0ck:contentLoaded', () => {
|
document.addEventListener('f0ck:contentLoaded', () => {
|
||||||
updateTagTabVisibility();
|
updateTagTabVisibility();
|
||||||
setTimeout(() => highlightActiveTagCard(true), 60);
|
updateFavsTabVisibility();
|
||||||
|
setTimeout(() => {
|
||||||
|
highlightActiveTagCard(true);
|
||||||
|
highlightActiveFavsCard(true);
|
||||||
|
}, 60);
|
||||||
});
|
});
|
||||||
window.addEventListener('popstate', () => {
|
window.addEventListener('popstate', () => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
updateTagTabVisibility();
|
updateTagTabVisibility();
|
||||||
|
updateFavsTabVisibility();
|
||||||
highlightActiveTagCard(true);
|
highlightActiveTagCard(true);
|
||||||
|
highlightActiveFavsCard(true);
|
||||||
}, 60);
|
}, 60);
|
||||||
});
|
});
|
||||||
bindRecommendationEvents();
|
bindRecommendationEvents();
|
||||||
|
|||||||
+96
-52
@@ -1266,40 +1266,70 @@ window.initUploadForm = (selector) => {
|
|||||||
badge.innerHTML = index === 0 ? '<i class="fa-solid fa-star"></i> Cover' : `#${index + 1}`;
|
badge.innerHTML = index === 0 ? '<i class="fa-solid fa-star"></i> Cover' : `#${index + 1}`;
|
||||||
card.appendChild(badge);
|
card.appendChild(badge);
|
||||||
|
|
||||||
const isVideo = (file.type && file.type.startsWith('video/')) || /\.(mp4|webm|mov|mkv)$/i.test(file.name || '');
|
// URL items (YouTube links etc.) — no File/Blob available
|
||||||
const isAudio = (file.type && file.type.startsWith('audio/')) || /\.(mp3|ogg|wav|flac|m4a|aac)$/i.test(file.name || '');
|
if (item.type === 'url') {
|
||||||
|
const urlStr = item.url || '';
|
||||||
if (isVideo) {
|
const ytMatch = urlStr.match(/(?:youtube\.com\/watch\?v=|youtu\.be\/)([A-Za-z0-9_-]{11})/);
|
||||||
const video = document.createElement('video');
|
if (ytMatch) {
|
||||||
video.src = URL.createObjectURL(file);
|
const thumb = document.createElement('img');
|
||||||
video.muted = true;
|
thumb.src = `https://img.youtube.com/vi/${ytMatch[1]}/mqdefault.jpg`;
|
||||||
video.playsInline = true;
|
thumb.alt = 'YouTube thumbnail';
|
||||||
video.autoplay = false;
|
thumb.style.cssText = 'width:100%;height:100%;object-fit:cover;';
|
||||||
video.preload = 'metadata';
|
card.appendChild(thumb);
|
||||||
card.appendChild(video);
|
const mimeBadge = document.createElement('span');
|
||||||
|
mimeBadge.className = 'album-stage-mime-badge';
|
||||||
const mimeBadge = document.createElement('span');
|
mimeBadge.innerHTML = '<i class="fa-brands fa-youtube"></i>';
|
||||||
mimeBadge.className = 'album-stage-mime-badge';
|
card.appendChild(mimeBadge);
|
||||||
mimeBadge.innerHTML = '<i class="fa-solid fa-play"></i>';
|
} else {
|
||||||
card.appendChild(mimeBadge);
|
const urlPreview = document.createElement('div');
|
||||||
} else if (isAudio) {
|
urlPreview.className = 'album-stage-audio-preview';
|
||||||
const audioPreview = document.createElement('div');
|
const shortUrl = urlStr.replace(/^https?:\/\//, '').substring(0, 40);
|
||||||
audioPreview.className = 'album-stage-audio-preview';
|
urlPreview.innerHTML = `
|
||||||
audioPreview.innerHTML = `
|
<i class="fa-solid fa-link"></i>
|
||||||
<i class="fa-solid fa-music"></i>
|
<span class="album-stage-audio-name" title="${urlStr}">${shortUrl}</span>
|
||||||
<span class="album-stage-audio-name" title="${file.name || 'Audio'}">${file.name || 'Audio'}</span>
|
`;
|
||||||
`;
|
card.appendChild(urlPreview);
|
||||||
card.appendChild(audioPreview);
|
const mimeBadge = document.createElement('span');
|
||||||
|
mimeBadge.className = 'album-stage-mime-badge';
|
||||||
const mimeBadge = document.createElement('span');
|
mimeBadge.innerHTML = '<i class="fa-solid fa-link"></i>';
|
||||||
mimeBadge.className = 'album-stage-mime-badge';
|
card.appendChild(mimeBadge);
|
||||||
mimeBadge.innerHTML = '<i class="fa-solid fa-music"></i>';
|
}
|
||||||
card.appendChild(mimeBadge);
|
|
||||||
} else {
|
} else {
|
||||||
const img = document.createElement('img');
|
const isVideo = (file.type && file.type.startsWith('video/')) || /\.(mp4|webm|mov|mkv)$/i.test(file.name || '');
|
||||||
img.src = URL.createObjectURL(file);
|
const isAudio = (file.type && file.type.startsWith('audio/')) || /\.(mp3|ogg|wav|flac|m4a|aac)$/i.test(file.name || '');
|
||||||
img.alt = file.name || `Subf0ck ${index + 1}`;
|
|
||||||
card.appendChild(img);
|
if (isVideo) {
|
||||||
|
const video = document.createElement('video');
|
||||||
|
video.src = URL.createObjectURL(file);
|
||||||
|
video.muted = true;
|
||||||
|
video.playsInline = true;
|
||||||
|
video.autoplay = false;
|
||||||
|
video.preload = 'metadata';
|
||||||
|
card.appendChild(video);
|
||||||
|
|
||||||
|
const mimeBadge = document.createElement('span');
|
||||||
|
mimeBadge.className = 'album-stage-mime-badge';
|
||||||
|
mimeBadge.innerHTML = '<i class="fa-solid fa-play"></i>';
|
||||||
|
card.appendChild(mimeBadge);
|
||||||
|
} else if (isAudio) {
|
||||||
|
const audioPreview = document.createElement('div');
|
||||||
|
audioPreview.className = 'album-stage-audio-preview';
|
||||||
|
audioPreview.innerHTML = `
|
||||||
|
<i class="fa-solid fa-music"></i>
|
||||||
|
<span class="album-stage-audio-name" title="${file.name || 'Audio'}">${file.name || 'Audio'}</span>
|
||||||
|
`;
|
||||||
|
card.appendChild(audioPreview);
|
||||||
|
|
||||||
|
const mimeBadge = document.createElement('span');
|
||||||
|
mimeBadge.className = 'album-stage-mime-badge';
|
||||||
|
mimeBadge.innerHTML = '<i class="fa-solid fa-music"></i>';
|
||||||
|
card.appendChild(mimeBadge);
|
||||||
|
} else {
|
||||||
|
const img = document.createElement('img');
|
||||||
|
img.src = URL.createObjectURL(file);
|
||||||
|
img.alt = file.name || `Subf0ck ${index + 1}`;
|
||||||
|
card.appendChild(img);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const actions = document.createElement('div');
|
const actions = document.createElement('div');
|
||||||
@@ -2972,8 +3002,21 @@ window.initUploadForm = (selector) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// URL uploads: always stay on current page — tracker panel shows progress
|
// URL uploads: redirect or stay based on user preference (pending/async jobs skip redirect)
|
||||||
// (no redirect here; the file upload path below handles its own redirect)
|
if (!lastData?.pending && !lastData?.manual_approval) {
|
||||||
|
const shouldRedirectToItem = redirectCheckbox
|
||||||
|
? redirectCheckbox.checked
|
||||||
|
: (localStorage.getItem('upload_redirect_to_item') !== 'false');
|
||||||
|
if (shouldRedirectToItem && lastData?.itemid) {
|
||||||
|
const targetUrl = lastData.slug ? `/${lastData.slug}` : `/${lastData.itemid}`;
|
||||||
|
if (typeof window.loadPageAjax === 'function') {
|
||||||
|
window.loadPageAjax(targetUrl, true, { bypassCache: true });
|
||||||
|
} else {
|
||||||
|
window.location.href = targetUrl;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// else: stay on current page
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
restoreBtn();
|
restoreBtn();
|
||||||
}
|
}
|
||||||
@@ -3006,9 +3049,15 @@ window.initUploadForm = (selector) => {
|
|||||||
if (comment) formData.append('comment', comment);
|
if (comment) formData.append('comment', comment);
|
||||||
|
|
||||||
for (let i = 0; i < selectedFiles.length; i++) {
|
for (let i = 0; i < selectedFiles.length; i++) {
|
||||||
const f = selectedFiles[i].file || selectedFiles[i];
|
const item = selectedFiles[i];
|
||||||
formData.append('files', f);
|
if (item.type === 'url') {
|
||||||
const subTags = selectedFiles[i].subTags || '';
|
// URL items (YouTube links etc.) — send as a URL field, not a binary file
|
||||||
|
formData.append(`subf0ck_url_${i}`, item.url || '');
|
||||||
|
} else {
|
||||||
|
const f = item.file || item;
|
||||||
|
formData.append('files', f);
|
||||||
|
}
|
||||||
|
const subTags = item.subTags || '';
|
||||||
if (subTags) {
|
if (subTags) {
|
||||||
formData.append(`subf0ck_tags_${i}`, subTags);
|
formData.append(`subf0ck_tags_${i}`, subTags);
|
||||||
}
|
}
|
||||||
@@ -3080,15 +3129,15 @@ window.initUploadForm = (selector) => {
|
|||||||
? redirectCheckbox.checked
|
? redirectCheckbox.checked
|
||||||
: (localStorage.getItem('upload_redirect_to_item') !== 'false');
|
: (localStorage.getItem('upload_redirect_to_item') !== 'false');
|
||||||
|
|
||||||
const targetUrl = shouldRedirectToItem
|
if (shouldRedirectToItem) {
|
||||||
? (res.slug ? `/${res.slug}` : (res.itemid ? `/${res.itemid}` : '/'))
|
const targetUrl = res.slug ? `/${res.slug}` : (res.itemid ? `/${res.itemid}` : '/');
|
||||||
: '/';
|
if (typeof window.loadPageAjax === 'function') {
|
||||||
|
window.loadPageAjax(targetUrl, true, { bypassCache: true });
|
||||||
if (typeof window.loadPageAjax === 'function') {
|
} else {
|
||||||
window.loadPageAjax(targetUrl, true, { bypassCache: true });
|
window.location.href = targetUrl;
|
||||||
} else {
|
}
|
||||||
window.location.href = targetUrl;
|
|
||||||
}
|
}
|
||||||
|
// else: stay on current page
|
||||||
return;
|
return;
|
||||||
} else {
|
} else {
|
||||||
const errMsg = res.msg || 'Upload failed';
|
const errMsg = res.msg || 'Upload failed';
|
||||||
@@ -3340,13 +3389,8 @@ window.initUploadForm = (selector) => {
|
|||||||
} else {
|
} else {
|
||||||
window.location.href = targetUrl;
|
window.location.href = targetUrl;
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
if (typeof window.loadPageAjax === 'function') {
|
|
||||||
window.loadPageAjax('/', true, { bypassCache: true });
|
|
||||||
} else {
|
|
||||||
window.location.href = '/';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
// else: stay on current page
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
restoreBtn();
|
restoreBtn();
|
||||||
|
|||||||
+1
-1
@@ -149,7 +149,7 @@
|
|||||||
if (activePostId) untaggedSpan.dataset.itemId = activePostId;
|
if (activePostId) untaggedSpan.dataset.itemId = activePostId;
|
||||||
const lbl = document.createElement("span");
|
const lbl = document.createElement("span");
|
||||||
lbl.className = "rating-label";
|
lbl.className = "rating-label";
|
||||||
lbl.textContent = "untagged";
|
lbl.textContent = "unrated";
|
||||||
untaggedSpan.appendChild(lbl);
|
untaggedSpan.appendChild(lbl);
|
||||||
inner.insertAdjacentElement("afterbegin", untaggedSpan);
|
inner.insertAdjacentElement("afterbegin", untaggedSpan);
|
||||||
}
|
}
|
||||||
|
|||||||
+19
-3
@@ -168,11 +168,27 @@ class v0ck {
|
|||||||
player.style.backgroundImage = 'none';
|
player.style.backgroundImage = 'none';
|
||||||
player.style.backgroundColor = 'transparent';
|
player.style.backgroundColor = 'transparent';
|
||||||
if (!isFallback) {
|
if (!isFallback) {
|
||||||
coverCircle.style.backgroundImage = `url('${poster}')`;
|
coverCircle._origBgImage = `url('${poster}')`;
|
||||||
ph.classList.add('has-cover');
|
if (typeof window.updateCoverArtSolidMode === 'function') {
|
||||||
|
window.updateCoverArtSolidMode();
|
||||||
|
} else {
|
||||||
|
const tuning = window.audioVisualizerTuning;
|
||||||
|
const showInEye = tuning ? (tuning.showCoverInEye !== undefined ? Number(tuning.showCoverInEye) === 1 : (tuning.showCoverArt !== undefined ? Number(tuning.showCoverArt) === 1 : Number(tuning.solidCover) === 0)) : false;
|
||||||
|
if (showInEye) {
|
||||||
|
coverCircle.style.backgroundImage = `url('${poster}')`;
|
||||||
|
ph.classList.add('has-cover');
|
||||||
|
} else {
|
||||||
|
coverCircle.style.backgroundImage = 'none';
|
||||||
|
ph.classList.remove('has-cover');
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
coverCircle.style.backgroundImage = '';
|
coverCircle._origBgImage = null;
|
||||||
|
coverCircle.style.backgroundImage = 'none';
|
||||||
ph.classList.remove('has-cover');
|
ph.classList.remove('has-cover');
|
||||||
|
if (typeof window.updateCoverArtSolidMode === 'function') {
|
||||||
|
window.updateCoverArtSolidMode();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -352,6 +352,23 @@ export default new class {
|
|||||||
order by (case when "tags".id = 1 then 0 when "tags".id = 2 then 1 when "tags".normalized = 'nsfl' then 2 else 3 end) asc, "tags".id asc
|
order by (case when "tags".id = 1 then 0 when "tags".id = 2 then 1 when "tags".normalized = 'nsfl' then 2 else 3 end) asc, "tags".id asc
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
// If album sub-item has no rating tag of its own, inherit the parent post's rating tag
|
||||||
|
const hasSubRating = tags.some(t => ['sfw', 'nsfw', 'nsfl'].includes(t.normalized) || t.id === 1 || t.id === 2);
|
||||||
|
if (!hasSubRating) {
|
||||||
|
const parentRatingTags = await db`
|
||||||
|
select "tags".id, "tags".tag, "tags".normalized${hasSession ? db`, "user".user, uo.display_name` : db``}
|
||||||
|
from "tags_assign"
|
||||||
|
left join "tags" on "tags".id = "tags_assign".tag_id
|
||||||
|
${hasSession ? db`left join "user" on "user".id = "tags_assign".user_id left join user_options uo on uo.user_id = "user".id` : db``}
|
||||||
|
where "tags_assign".item_id = ${+itemid}
|
||||||
|
and ("tags".id in (1, 2) or "tags".normalized in ('sfw', 'nsfw', 'nsfl'))
|
||||||
|
order by (case when "tags".id = 1 then 0 when "tags".id = 2 then 1 when "tags".normalized = 'nsfl' then 2 else 3 end) asc
|
||||||
|
limit 1
|
||||||
|
`;
|
||||||
|
if (parentRatingTags.length > 0) {
|
||||||
|
tags.unshift(parentRatingTags[0]);
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
tags = await db`
|
tags = await db`
|
||||||
select "tags".id, "tags".tag, "tags".normalized${hasSession ? db`, "user".user, uo.display_name` : db``}
|
select "tags".id, "tags".tag, "tags".normalized${hasSession ? db`, "user".user, uo.display_name` : db``}
|
||||||
|
|||||||
@@ -182,8 +182,8 @@
|
|||||||
"blur_nsfl_hint": "Blur NSFL-rated/shock thumbnails.",
|
"blur_nsfl_hint": "Blur NSFL-rated/shock thumbnails.",
|
||||||
"blur_sfw": "Blur SFW",
|
"blur_sfw": "Blur SFW",
|
||||||
"blur_sfw_hint": "Blur SFW-rated thumbnails.",
|
"blur_sfw_hint": "Blur SFW-rated thumbnails.",
|
||||||
"blur_untagged": "Blur Untagged",
|
"blur_untagged": "Blur Unrated",
|
||||||
"blur_untagged_hint": "Blur thumbnails with no tags or rating.",
|
"blur_untagged_hint": "Blur thumbnails with no rating.",
|
||||||
"blur_detail": "Click to reveal item on detail page",
|
"blur_detail": "Click to reveal item on detail page",
|
||||||
"blur_detail_hint": "Require clicking to reveal blurred media on the post detail page.",
|
"blur_detail_hint": "Require clicking to reveal blurred media on the post detail page.",
|
||||||
"render_emojis": "Render emojis in quote replies",
|
"render_emojis": "Render emojis in quote replies",
|
||||||
@@ -518,7 +518,7 @@
|
|||||||
"tag_stats": "Stats",
|
"tag_stats": "Stats",
|
||||||
"stat_total": "Total Items",
|
"stat_total": "Total Items",
|
||||||
"stat_tagged": "Tagged",
|
"stat_tagged": "Tagged",
|
||||||
"stat_untagged": "Untagged",
|
"stat_untagged": "Unrated",
|
||||||
"stat_sfw": "SFW content",
|
"stat_sfw": "SFW content",
|
||||||
"stat_nsfw": "NSFW content",
|
"stat_nsfw": "NSFW content",
|
||||||
"stat_nsfl": "NSFL content",
|
"stat_nsfl": "NSFL content",
|
||||||
@@ -743,7 +743,7 @@
|
|||||||
"reset_all": "Reset all",
|
"reset_all": "Reset all",
|
||||||
"rating": "Rating",
|
"rating": "Rating",
|
||||||
"all": "All",
|
"all": "All",
|
||||||
"untagged": "Untagged",
|
"untagged": "Unrated",
|
||||||
"media_type": "Media type",
|
"media_type": "Media type",
|
||||||
"video": "Video",
|
"video": "Video",
|
||||||
"image": "Image",
|
"image": "Image",
|
||||||
|
|||||||
@@ -37,8 +37,10 @@ export async function initPrivateItems() {
|
|||||||
if (r.id) _unavailableIds.set(Number(r.id), u);
|
if (r.id) _unavailableIds.set(Number(r.id), u);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (!_initialized) {
|
||||||
|
console.log(`[BOOT] Loaded ${_privateDests.size} private item(s) and ${_unavailableDests.size} unavailable item(s) into memory cache`);
|
||||||
|
}
|
||||||
_initialized = true;
|
_initialized = true;
|
||||||
console.log(`[BOOT] Loaded ${_privateDests.size} private item(s) and ${_unavailableDests.size} unavailable item(s) into memory cache`);
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('[BOOT] Failed to load private/unavailable items into cache:', err.message);
|
console.error('[BOOT] Failed to load private/unavailable items into cache:', err.message);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,26 +11,26 @@ import url from "url";
|
|||||||
const getGlobalfilter = (session) => {
|
const getGlobalfilter = (session) => {
|
||||||
if (!cfg.nsfp?.length) return null;
|
if (!cfg.nsfp?.length) return null;
|
||||||
let filteredTags = [...cfg.nsfp];
|
let filteredTags = [...cfg.nsfp];
|
||||||
// For anonymous users, respect their allowed_modes permissions
|
// allowed_modes controls access to NSFW/NSFL for anon/guests by stripping the RATING tags
|
||||||
|
// (tag_id 2 = NSFW, nsfl_tag_id = NSFL) from the nsfp filter.
|
||||||
|
// Any other nsfp tags (content-specific blocks) are never stripped — they always apply.
|
||||||
|
const nsflId = parseInt(cfg.nsfl_tag_id, 10) || 3;
|
||||||
if (session && isAnonSession(session)) {
|
if (session && isAnonSession(session)) {
|
||||||
const allowedModes = getAnonAllowedModes();
|
const allowedModes = getAnonAllowedModes();
|
||||||
if (allowedModes.includes('nsfw')) {
|
if (allowedModes.includes('nsfw')) {
|
||||||
filteredTags = filteredTags.filter(id => id !== 2);
|
filteredTags = filteredTags.filter(id => id !== 2);
|
||||||
}
|
}
|
||||||
const nsflId = parseInt(cfg.nsfl_tag_id, 10) || 3;
|
|
||||||
if (allowedModes.includes('nsfl')) {
|
if (allowedModes.includes('nsfl')) {
|
||||||
filteredTags = filteredTags.filter(id => id !== nsflId);
|
filteredTags = filteredTags.filter(id => id !== nsflId);
|
||||||
}
|
}
|
||||||
} else if (!session) {
|
} else if (!session) {
|
||||||
// Guest (not logged in): use public_nsfw setting
|
// Guest (not logged in): use public_nsfw / public_nsfl settings
|
||||||
if (cfg.websrv.public_nsfw) {
|
if (cfg.websrv.public_nsfw) {
|
||||||
filteredTags = filteredTags.filter(id => id !== 2);
|
filteredTags = filteredTags.filter(id => id !== 2);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Logged-in member: use public_nsfw setting
|
// Logged-in member: rating tags are not filtered by nsfp
|
||||||
if (cfg.websrv.public_nsfw) {
|
filteredTags = filteredTags.filter(id => id !== 2 && id !== nsflId);
|
||||||
filteredTags = filteredTags.filter(id => id !== 2);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return filteredTags.length ? filteredTags.map(n => `tag_id = ${n}`).join(" or ") : null;
|
return filteredTags.length ? filteredTags.map(n => `tag_id = ${n}`).join(" or ") : null;
|
||||||
};
|
};
|
||||||
@@ -363,7 +363,8 @@ const buildFeedFilters = async ({
|
|||||||
rawUserHallOwner,
|
rawUserHallOwner,
|
||||||
rawTagger
|
rawTagger
|
||||||
}) => {
|
}) => {
|
||||||
const user = rawUser ? lib.escapeLike(decodeURI(rawUser)) : null;
|
const user = rawUser ? decodeURI(rawUser) : null;
|
||||||
|
const userLike = user ? lib.escapeLike(user) : null;
|
||||||
|
|
||||||
// --- title: prefix — search items.title instead of the tags table ---
|
// --- title: prefix — search items.title instead of the tags table ---
|
||||||
const _decodedTag = rawTag ? decodeURIComponent(rawTag) : '';
|
const _decodedTag = rawTag ? decodeURIComponent(rawTag) : '';
|
||||||
@@ -485,6 +486,7 @@ const buildFeedFilters = async ({
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
user,
|
user,
|
||||||
|
userLike,
|
||||||
_decodedTag,
|
_decodedTag,
|
||||||
isTitleSearch,
|
isTitleSearch,
|
||||||
titleQuery,
|
titleQuery,
|
||||||
@@ -568,6 +570,7 @@ const f0cklib = {
|
|||||||
|
|
||||||
const {
|
const {
|
||||||
user,
|
user,
|
||||||
|
userLike,
|
||||||
mimeSQL,
|
mimeSQL,
|
||||||
excludedTags,
|
excludedTags,
|
||||||
newerThan,
|
newerThan,
|
||||||
@@ -591,8 +594,8 @@ const f0cklib = {
|
|||||||
${visibilityFilter}
|
${visibilityFilter}
|
||||||
${tagFilter}
|
${tagFilter}
|
||||||
${titleFilter}
|
${titleFilter}
|
||||||
${fav ? db`and (fav_u.user ilike ${user} or fav_u.login ilike ${user})` : db``}
|
${fav ? db`and (fav_u.user ilike ${userLike} or fav_u.login ilike ${userLike})` : db``}
|
||||||
${!fav && user ? db`and items.username ilike ${user}` : db``}
|
${!fav && user ? db`and items.username ilike ${userLike}` : db``}
|
||||||
${mimeSQL}
|
${mimeSQL}
|
||||||
${hallFilter}
|
${hallFilter}
|
||||||
${userHallFilter}
|
${userHallFilter}
|
||||||
@@ -617,8 +620,8 @@ const f0cklib = {
|
|||||||
${visibilityFilter}
|
${visibilityFilter}
|
||||||
${tagFilter}
|
${tagFilter}
|
||||||
${titleFilter}
|
${titleFilter}
|
||||||
${fav ? db`and (fav_u.user ilike ${user} or fav_u.login ilike ${user})` : db``}
|
${fav ? db`and (fav_u.user ilike ${userLike} or fav_u.login ilike ${userLike})` : db``}
|
||||||
${!fav && user ? db`and items.username ilike ${user}` : db``}
|
${!fav && user ? db`and items.username ilike ${userLike}` : db``}
|
||||||
${mimeSQL}
|
${mimeSQL}
|
||||||
${hallFilter}
|
${hallFilter}
|
||||||
${userHallFilter}
|
${userHallFilter}
|
||||||
@@ -680,6 +683,7 @@ const f0cklib = {
|
|||||||
|
|
||||||
const {
|
const {
|
||||||
user,
|
user,
|
||||||
|
userLike,
|
||||||
_decodedTag,
|
_decodedTag,
|
||||||
isTitleSearch,
|
isTitleSearch,
|
||||||
titleQuery,
|
titleQuery,
|
||||||
@@ -747,8 +751,8 @@ const f0cklib = {
|
|||||||
${tagFilter}
|
${tagFilter}
|
||||||
${titleFilter}
|
${titleFilter}
|
||||||
${idsFilter}
|
${idsFilter}
|
||||||
${fav && user ? db`and (fav_u.user ilike ${user} or fav_u.login ilike ${user})` : db``}
|
${fav && user ? db`and (fav_u.user ilike ${userLike} or fav_u.login ilike ${userLike})` : db``}
|
||||||
${!fav && user ? db`and items.username ilike ${user}` : db``}
|
${!fav && user ? db`and items.username ilike ${userLike}` : db``}
|
||||||
${mimeSQL}
|
${mimeSQL}
|
||||||
${hallFilter}
|
${hallFilter}
|
||||||
${userHallFilter}
|
${userHallFilter}
|
||||||
@@ -791,8 +795,8 @@ const f0cklib = {
|
|||||||
${tagFilter}
|
${tagFilter}
|
||||||
${titleFilter}
|
${titleFilter}
|
||||||
${idsFilter}
|
${idsFilter}
|
||||||
${fav && user ? db`and (fav_u.user ilike ${user} or fav_u.login ilike ${user})` : db``}
|
${fav && user ? db`and (fav_u.user ilike ${userLike} or fav_u.login ilike ${userLike})` : db``}
|
||||||
${!fav && user ? db`and items.username ilike ${user}` : db``}
|
${!fav && user ? db`and items.username ilike ${userLike}` : db``}
|
||||||
${mimeSQL}
|
${mimeSQL}
|
||||||
${hallFilter}
|
${hallFilter}
|
||||||
${userHallFilter}
|
${userHallFilter}
|
||||||
@@ -1089,7 +1093,8 @@ const f0cklib = {
|
|||||||
: (typeof ids === 'string' ? ids.split(',').map(Number).filter(n => Number.isInteger(n) && n > 0) : null);
|
: (typeof ids === 'string' ? ids.split(',').map(Number).filter(n => Number.isInteger(n) && n > 0) : null);
|
||||||
const idsFilter = (cleanIds && cleanIds.length > 0) ? db`and items.id = ANY(${cleanIds}::int[])` : db``;
|
const idsFilter = (cleanIds && cleanIds.length > 0) ? db`and items.id = ANY(${cleanIds}::int[])` : db``;
|
||||||
|
|
||||||
const user = rawUser ? lib.escapeLike(decodeURI(rawUser)) : null;
|
const user = rawUser ? decodeURI(rawUser) : null;
|
||||||
|
const userLike = user ? lib.escapeLike(user) : null;
|
||||||
|
|
||||||
// --- title: prefix — search items.title instead of the tags table ---
|
// --- title: prefix — search items.title instead of the tags table ---
|
||||||
const _decodedTag = rawTag ? decodeURIComponent(rawTag) : '';
|
const _decodedTag = rawTag ? decodeURIComponent(rawTag) : '';
|
||||||
@@ -1222,8 +1227,8 @@ const f0cklib = {
|
|||||||
${titleFilter}
|
${titleFilter}
|
||||||
${hallFilter}
|
${hallFilter}
|
||||||
${userHallFilter}
|
${userHallFilter}
|
||||||
${fav && user ? db`and "user"."user" ilike ${user}` : db``}
|
${fav && user ? db`and "user"."user" ilike ${userLike}` : db``}
|
||||||
${!fav && user ? db`and items.username ilike ${user}` : db``}
|
${!fav && user ? db`and items.username ilike ${userLike}` : db``}
|
||||||
${idsFilter}
|
${idsFilter}
|
||||||
${mimeSQL}
|
${mimeSQL}
|
||||||
${(!session || isAnonSession(session)) && getGlobalfilter(session) ? db`and not exists (select 1 from tags_assign where item_id = items.id and (${db.unsafe(getGlobalfilter(session))}))` : db``}
|
${(!session || isAnonSession(session)) && getGlobalfilter(session) ? db`and not exists (select 1 from tags_assign where item_id = items.id and (${db.unsafe(getGlobalfilter(session))}))` : db``}
|
||||||
@@ -1579,17 +1584,22 @@ const f0cklib = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const parentRatingTags = tags.filter(t => t.id == 1 || t.id == 2 || (cfg.enable_nsfl && (t.id == nsfl_id || t.normalized === 'nsfl')));
|
||||||
|
|
||||||
const isSubVisible = (subTags) => {
|
const isSubVisible = (subTags) => {
|
||||||
if (bypass_filter) return true;
|
if (bypass_filter) return true;
|
||||||
|
|
||||||
const subIsNsfl = cfg.enable_nsfl && subTags.some(t => t.id == nsfl_id || t.normalized === 'nsfl');
|
const hasSubRating = subTags.some(t => t.id == 1 || t.id == 2 || (cfg.enable_nsfl && (t.id == nsfl_id || t.normalized === 'nsfl')));
|
||||||
const subIsNsfw = subTags.some(t => t.id == 2);
|
const effectiveSubTags = hasSubRating ? subTags : [...parentRatingTags, ...subTags];
|
||||||
const subIsSfw = subTags.some(t => t.id == 1);
|
|
||||||
|
const subIsNsfl = cfg.enable_nsfl && effectiveSubTags.some(t => t.id == nsfl_id || t.normalized === 'nsfl');
|
||||||
|
const subIsNsfw = effectiveSubTags.some(t => t.id == 2);
|
||||||
|
const subIsSfw = effectiveSubTags.some(t => t.id == 1);
|
||||||
const subIsUntagged = !subIsSfw && !subIsNsfw && !subIsNsfl;
|
const subIsUntagged = !subIsSfw && !subIsNsfw && !subIsNsfl;
|
||||||
|
|
||||||
// Excluded tags check
|
// Excluded tags check
|
||||||
if (excludedTags && excludedTags.length > 0 && subTags.length > 0) {
|
if (excludedTags && excludedTags.length > 0 && effectiveSubTags.length > 0) {
|
||||||
if (subTags.some(t => excludedTags.includes(t.normalized) || excludedTags.includes(lib.slugify(t.tag)))) {
|
if (effectiveSubTags.some(t => excludedTags.includes(t.normalized) || excludedTags.includes(lib.slugify(t.tag)))) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1664,10 +1674,16 @@ const f0cklib = {
|
|||||||
album = await Promise.all(albumRows.map(async (r, idx) => {
|
album = await Promise.all(albumRows.map(async (r, idx) => {
|
||||||
const order = idx;
|
const order = idx;
|
||||||
const subSlug = r.slug || r.id;
|
const subSlug = r.slug || r.id;
|
||||||
|
const isYouTube = r.mime === 'video/youtube';
|
||||||
const subBase = r.dest.replace(/\.[^.]+$/, '');
|
const subBase = r.dest.replace(/\.[^.]+$/, '');
|
||||||
const isAudio = (r.mime || '').startsWith('audio/');
|
const isAudio = (r.mime || '').startsWith('audio/');
|
||||||
let subCover = null;
|
let subCover = null;
|
||||||
let subThumb = `${cfg.websrv.paths.thumbnails}/${subBase}.webp`;
|
// YouTube sub-items: thumbnail is stored by album_item row id (not by filename)
|
||||||
|
let subThumb = isYouTube
|
||||||
|
? `${cfg.websrv.paths.thumbnails}/${r.id}.webp`
|
||||||
|
: `${cfg.websrv.paths.thumbnails}/${subBase}.webp`;
|
||||||
|
// Also try YouTube CDN thumbnail as fallback
|
||||||
|
const ytVideoId = isYouTube ? r.dest.replace('yt:', '') : null;
|
||||||
|
|
||||||
if (isAudio) {
|
if (isAudio) {
|
||||||
const caFile = path.join(cfg.paths.ca, `${subBase}.webp`);
|
const caFile = path.join(cfg.paths.ca, `${subBase}.webp`);
|
||||||
@@ -1729,13 +1745,15 @@ const f0cklib = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let subTags = tagsBySubId.get(r.id) || [];
|
let subTags = tagsBySubId.get(r.id) || [];
|
||||||
|
const hasSubRating = subTags.some(t => t.id == 1 || t.id == 2 || (cfg.enable_nsfl && (t.id == nsfl_id || t.normalized === 'nsfl')));
|
||||||
|
const combinedSubTags = hasSubRating ? subTags : [...parentRatingTags, ...subTags];
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: r.id,
|
id: r.id,
|
||||||
slug: r.slug,
|
slug: r.slug,
|
||||||
subf0ck_id: subSlug,
|
subf0ck_id: subSlug,
|
||||||
dest: `${cfg.websrv.paths.images}/${r.dest}`,
|
dest: isYouTube ? r.dest : `${cfg.websrv.paths.images}/${r.dest}`,
|
||||||
src: `${cfg.websrv.paths.images}/${r.dest}`,
|
src: isYouTube ? `https://www.youtube.com/embed/${ytVideoId}?enablejsapi=1` : `${cfg.websrv.paths.images}/${r.dest}`,
|
||||||
filename: r.dest,
|
filename: r.dest,
|
||||||
mime: r.mime,
|
mime: r.mime,
|
||||||
size: lib.formatSize(subSizeNum),
|
size: lib.formatSize(subSizeNum),
|
||||||
@@ -1746,12 +1764,16 @@ const f0cklib = {
|
|||||||
display_index: order + 1,
|
display_index: order + 1,
|
||||||
is_first: order === 0,
|
is_first: order === 0,
|
||||||
is_video: (r.mime || '').startsWith('video/'),
|
is_video: (r.mime || '').startsWith('video/'),
|
||||||
|
is_youtube: isYouTube,
|
||||||
is_audio: isAudio,
|
is_audio: isAudio,
|
||||||
is_image: (r.mime || '').startsWith('image/'),
|
is_image: (r.mime || '').startsWith('image/'),
|
||||||
has_coverart: isAudio ? !!subCover : false,
|
has_coverart: isAudio ? !!subCover : false,
|
||||||
coverart: isAudio ? subCover : null,
|
coverart: isAudio ? subCover : null,
|
||||||
thumb: subThumb,
|
// Use YouTube CDN thumb as fallback for strip thumbnails
|
||||||
tags: subTags
|
thumb: isYouTube
|
||||||
|
? `https://img.youtube.com/vi/${ytVideoId}/mqdefault.jpg`
|
||||||
|
: subThumb,
|
||||||
|
tags: combinedSubTags
|
||||||
};
|
};
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@@ -1797,11 +1819,12 @@ const f0cklib = {
|
|||||||
actitem.coverart = targetSub.coverart;
|
actitem.coverart = targetSub.coverart;
|
||||||
}
|
}
|
||||||
if (targetSub.tags && targetSub.tags.length > 0) {
|
if (targetSub.tags && targetSub.tags.length > 0) {
|
||||||
effectiveItemTags = targetSub.tags;
|
const hasSubRating = targetSub.tags.some(t => t.id == 1 || t.id == 2 || (cfg.enable_nsfl && (t.id == nsfl_id || t.normalized === 'nsfl')));
|
||||||
|
effectiveItemTags = hasSubRating ? targetSub.tags : [...parentRatingTags, ...targetSub.tags];
|
||||||
} else if (reqIdx === 0) {
|
} else if (reqIdx === 0) {
|
||||||
effectiveItemTags = tags;
|
effectiveItemTags = tags;
|
||||||
} else {
|
} else {
|
||||||
effectiveItemTags = [];
|
effectiveItemTags = [...parentRatingTags];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1944,7 +1967,7 @@ const f0cklib = {
|
|||||||
})(),
|
})(),
|
||||||
// og_description: include rating + uploader for bots (Matrix, Discord, etc.)
|
// og_description: include rating + uploader for bots (Matrix, Discord, etc.)
|
||||||
og_description: (() => {
|
og_description: (() => {
|
||||||
const ratingLabel = isNsfl ? 'NSFL' : (isNsfw ? 'NSFW' : (isSfw ? 'SFW' : 'Untagged'));
|
const ratingLabel = isNsfl ? 'NSFL' : (isNsfw ? 'NSFW' : (isSfw ? 'SFW' : 'Unrated'));
|
||||||
const titlePart = actitem.title ? ` · "${actitem.title}"` : '';
|
const titlePart = actitem.title ? ` · "${actitem.title}"` : '';
|
||||||
return `${ratingLabel}${titlePart} · uploaded by ${actitem.username}`;
|
return `${ratingLabel}${titlePart} · uploaded by ${actitem.username}`;
|
||||||
})(),
|
})(),
|
||||||
@@ -2012,7 +2035,8 @@ const f0cklib = {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const user = rawUser ? lib.escapeLike(decodeURI(rawUser)) : null;
|
const user = rawUser ? decodeURI(rawUser) : null;
|
||||||
|
const userLike = user ? lib.escapeLike(user) : null;
|
||||||
const hall = rawHall || null;
|
const hall = rawHall || null;
|
||||||
|
|
||||||
// --- title: prefix — search items.title instead of the tags table ---
|
// --- title: prefix — search items.title instead of the tags table ---
|
||||||
@@ -2079,7 +2103,7 @@ const f0cklib = {
|
|||||||
left join tags on tags.id = tags_assign.tag_id
|
left join tags on tags.id = tags_assign.tag_id
|
||||||
where
|
where
|
||||||
${db.unsafe(modequery)}
|
${db.unsafe(modequery)}
|
||||||
and "user".user ilike ${user}
|
and "user".user ilike ${userLike}
|
||||||
and items.active = true
|
and items.active = true
|
||||||
and coalesce(items.visibility, 0) = 0
|
and coalesce(items.visibility, 0) = 0
|
||||||
${mimeSQL}
|
${mimeSQL}
|
||||||
@@ -2121,7 +2145,7 @@ const f0cklib = {
|
|||||||
and items.active = true
|
and items.active = true
|
||||||
and coalesce(items.visibility, 0) = 0
|
and coalesce(items.visibility, 0) = 0
|
||||||
${tagFilter}
|
${tagFilter}
|
||||||
${user ? db`and items.username ilike ${user}` : db``}
|
${user ? db`and items.username ilike ${userLike}` : db``}
|
||||||
${hall ? db`and items.id in (select item_id from halls_assign ha join halls h on h.id = ha.hall_id where h.slug = ${hall})` : db``}
|
${hall ? db`and items.id in (select item_id from halls_assign ha join halls h on h.id = ha.hall_id where h.slug = ${hall})` : db``}
|
||||||
${mimeSQL}
|
${mimeSQL}
|
||||||
${(!session || isAnonSession(session)) && getGlobalfilter(session) ? db`and not exists (select 1 from tags_assign where item_id = items.id and (${db.unsafe(getGlobalfilter(session))}))` : db``}
|
${(!session || isAnonSession(session)) && getGlobalfilter(session) ? db`and not exists (select 1 from tags_assign where item_id = items.id and (${db.unsafe(getGlobalfilter(session))}))` : db``}
|
||||||
@@ -2223,7 +2247,8 @@ const f0cklib = {
|
|||||||
return { items: [], total: 0 };
|
return { items: [], total: 0 };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const user = rawUser ? lib.escapeLike(decodeURI(rawUser)) : null;
|
const user = rawUser ? decodeURI(rawUser) : null;
|
||||||
|
const userLike = user ? lib.escapeLike(user) : null;
|
||||||
const hall = rawHall || null;
|
const hall = rawHall || null;
|
||||||
|
|
||||||
const _decodedTag = rawTag ? decodeURIComponent(rawTag) : '';
|
const _decodedTag = rawTag ? decodeURIComponent(rawTag) : '';
|
||||||
@@ -2277,7 +2302,7 @@ const f0cklib = {
|
|||||||
INNER JOIN items ON favorites.item_id = items.id
|
INNER JOIN items ON favorites.item_id = items.id
|
||||||
INNER JOIN "user" ON "user".id = favorites.user_id
|
INNER JOIN "user" ON "user".id = favorites.user_id
|
||||||
WHERE ${db.unsafe(modequery)}
|
WHERE ${db.unsafe(modequery)}
|
||||||
AND "user".user ILIKE ${user}
|
AND "user".user ILIKE ${userLike}
|
||||||
AND items.active = true AND coalesce(items.visibility, 0) = 0
|
AND items.active = true AND coalesce(items.visibility, 0) = 0
|
||||||
${mimeSQL} ${globalFilter}
|
${mimeSQL} ${globalFilter}
|
||||||
ORDER BY items.id
|
ORDER BY items.id
|
||||||
@@ -2310,7 +2335,7 @@ const f0cklib = {
|
|||||||
WHERE ${db.unsafe(modequery)}
|
WHERE ${db.unsafe(modequery)}
|
||||||
AND items.active = true AND coalesce(items.visibility, 0) = 0
|
AND items.active = true AND coalesce(items.visibility, 0) = 0
|
||||||
${tagFilter}
|
${tagFilter}
|
||||||
${user ? db`and items.username ilike ${user}` : db``}
|
${user ? db`and items.username ilike ${userLike}` : db``}
|
||||||
${hall ? db`and items.id in (select item_id from halls_assign ha join halls h on h.id = ha.hall_id where h.slug = ${hall})` : db``}
|
${hall ? db`and items.id in (select item_id from halls_assign ha join halls h on h.id = ha.hall_id where h.slug = ${hall})` : db``}
|
||||||
${mimeSQL} ${globalFilter} ${excludeFilter}
|
${mimeSQL} ${globalFilter} ${excludeFilter}
|
||||||
ORDER BY items.id
|
ORDER BY items.id
|
||||||
|
|||||||
+25
-4
@@ -101,7 +101,7 @@ export default (router, tpl) => {
|
|||||||
|
|
||||||
if (!data.success) {
|
if (!data.success) {
|
||||||
const { t: tErr } = createI18n(req.session?.language || req.lang || 'en');
|
const { t: tErr } = createI18n(req.session?.language || req.lang || 'en');
|
||||||
const modeLabels = { 0: 'SFW', 1: 'NSFW', 2: 'Untagged', 4: 'NSFL' };
|
const modeLabels = { 0: 'SFW', 1: 'NSFW', 2: 'Unrated', 4: 'NSFL' };
|
||||||
const errorModeLabel = reqMode !== 3 ? (modeLabels[reqMode] || 'SFW') : null;
|
const errorModeLabel = reqMode !== 3 ? (modeLabels[reqMode] || 'SFW') : null;
|
||||||
const errorHtml = tpl.render('error-partial', {
|
const errorHtml = tpl.render('error-partial', {
|
||||||
message: tErr('error.post_not_visible'),
|
message: tErr('error.post_not_visible'),
|
||||||
@@ -220,6 +220,27 @@ export default (router, tpl) => {
|
|||||||
// Detailed timing breakdown
|
// Detailed timing breakdown
|
||||||
console.log(`[AJAX-RANDOM] ${itemid} total=${tAjaxRender - tAjaxStart}ms | getRandom=${tRandom - tAjaxStart}ms | getf0ck=${tAjaxFetch - tRandom}ms | aux=${tAjaxAux - tAjaxFetch}ms | render=${tAjaxRender - tAjaxAux}ms`);
|
console.log(`[AJAX-RANDOM] ${itemid} total=${tAjaxRender - tAjaxStart}ms | getRandom=${tRandom - tAjaxStart}ms | getf0ck=${tAjaxFetch - tRandom}ms | aux=${tAjaxAux - tAjaxFetch}ms | render=${tAjaxRender - tAjaxAux}ms`);
|
||||||
|
|
||||||
|
let itemPage = null;
|
||||||
|
if (query.onara === '1' || req.cookies?.onara === '1' || req.cookies?.f0ck_onara === '1' || cfg.onara) {
|
||||||
|
try {
|
||||||
|
itemPage = await f0cklib.getItemPage({
|
||||||
|
targetItemId: data.item?.id || itemid,
|
||||||
|
targetItemPinned: data.item?.is_pinned,
|
||||||
|
user, tag, hall, userHall, userHallOwner, mime,
|
||||||
|
fav: isFav,
|
||||||
|
mode: reqMode,
|
||||||
|
ratings: ratingsArr && ratingsArr.length > 0 ? ratingsArr : null,
|
||||||
|
strict: isStrict,
|
||||||
|
session: req.session,
|
||||||
|
exclude: req.session ? (req.session.excluded_tags || []) : [],
|
||||||
|
user_id: req.session?.id,
|
||||||
|
is_admin: req.session?.admin,
|
||||||
|
minXdScore: req.url.qs?.min_xd !== undefined ? +req.url.qs.min_xd : (req.session?.min_xd_score || 0),
|
||||||
|
tagger: query.tagger || null
|
||||||
|
});
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
|
||||||
res.reply({
|
res.reply({
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
@@ -228,7 +249,7 @@ export default (router, tpl) => {
|
|||||||
title: data.title,
|
title: data.title,
|
||||||
id: itemid,
|
id: itemid,
|
||||||
slug: data.item?.slug || null,
|
slug: data.item?.slug || null,
|
||||||
page: null,
|
page: itemPage,
|
||||||
is_random: true
|
is_random: true
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
@@ -297,7 +318,7 @@ export default (router, tpl) => {
|
|||||||
|
|
||||||
if (!data.success) {
|
if (!data.success) {
|
||||||
const reqMode = (query.mode !== undefined ? +query.mode : req.mode) ?? 0;
|
const reqMode = (query.mode !== undefined ? +query.mode : req.mode) ?? 0;
|
||||||
const modeLabels = { 0: 'SFW', 1: 'NSFW', 2: 'Untagged', 4: 'NSFL' };
|
const modeLabels = { 0: 'SFW', 1: 'NSFW', 2: 'Unrated', 4: 'NSFL' };
|
||||||
const errorModeLabel = reqMode !== 3 ? (modeLabels[reqMode] || 'SFW') : null;
|
const errorModeLabel = reqMode !== 3 ? (modeLabels[reqMode] || 'SFW') : null;
|
||||||
const { t: tErr } = createI18n(req.session?.language || req.lang || 'en');
|
const { t: tErr } = createI18n(req.session?.language || req.lang || 'en');
|
||||||
const errorHtml = tpl.render('error-partial', {
|
const errorHtml = tpl.render('error-partial', {
|
||||||
@@ -443,7 +464,7 @@ export default (router, tpl) => {
|
|||||||
- Render: ${tAjaxRender - tAjaxAux}ms`);
|
- Render: ${tAjaxRender - tAjaxAux}ms`);
|
||||||
|
|
||||||
let itemPage = null;
|
let itemPage = null;
|
||||||
if (query.onara === '1' || req.cookies?.onara === '1' || cfg.onara || query.get_page === '1') {
|
if (query.onara === '1' || req.cookies?.onara === '1' || req.cookies?.f0ck_onara === '1' || cfg.onara || query.get_page === '1') {
|
||||||
try {
|
try {
|
||||||
itemPage = await f0cklib.getItemPage({
|
itemPage = await f0cklib.getItemPage({
|
||||||
targetItemId: data.item?.id || itemid,
|
targetItemId: data.item?.id || itemid,
|
||||||
|
|||||||
@@ -878,6 +878,9 @@ export default router => {
|
|||||||
});
|
});
|
||||||
console.log(`[RANDOM-POOL] ${pool.total} items in ${Date.now() - tStart}ms (mode=${mode} tag=${tag} hall=${hall} user=${user})`);
|
console.log(`[RANDOM-POOL] ${pool.total} items in ${Date.now() - tStart}ms (mode=${mode} tag=${tag} hall=${hall} user=${user})`);
|
||||||
|
|
||||||
|
// Include nsfp_version so clients can detect when the nsfp list changes and invalidate their pool
|
||||||
|
pool.nsfp_version = (cfg.nsfp || []).slice().sort((a, b) => a - b).join(',');
|
||||||
|
|
||||||
return res.json(pool);
|
return res.json(pool);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1037,6 +1040,102 @@ export default router => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
group.get(/\/favs-feed$/, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const user = req.url.qs?.user ? String(req.url.qs.user).trim() : null;
|
||||||
|
if (!user) {
|
||||||
|
return res.json({ success: false, error: 'user 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?.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 result = await f0cklib.getf0cks({
|
||||||
|
user,
|
||||||
|
fav: true,
|
||||||
|
mode,
|
||||||
|
ratings: ratingsArr,
|
||||||
|
session: req.session,
|
||||||
|
exclude: req.session?.excluded_tags || [],
|
||||||
|
user_id: req.session?.id,
|
||||||
|
is_admin: req.session?.admin,
|
||||||
|
limit,
|
||||||
|
page: Math.floor(offset / limit) + 1,
|
||||||
|
explicitTotal: null,
|
||||||
|
lang: req.lang
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!result || !result.success) {
|
||||||
|
return res.json({ success: false, error: 'not found', items: [], total: 0 });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle focus_id: if the focused item is on a different page, re-fetch centered on it
|
||||||
|
let items = result.items || [];
|
||||||
|
const total = result.total || 0;
|
||||||
|
let effectiveOffset = offset;
|
||||||
|
|
||||||
|
// If a focus_id is provided and not in current slice, try to find it
|
||||||
|
if (focusId && focusId !== 'favs' && items.length > 0 && !items.find(it => String(it.id) === focusId || it.slug === focusId)) {
|
||||||
|
const focusResult = await f0cklib.getItemPage({
|
||||||
|
targetItemId: focusId,
|
||||||
|
user,
|
||||||
|
fav: true,
|
||||||
|
mode,
|
||||||
|
ratings: ratingsArr,
|
||||||
|
session: req.session,
|
||||||
|
user_id: req.session?.id,
|
||||||
|
is_admin: req.session?.admin,
|
||||||
|
exclude: req.session?.excluded_tags || [],
|
||||||
|
limit
|
||||||
|
});
|
||||||
|
if (focusResult && focusResult > 0) {
|
||||||
|
const focusPage = focusResult;
|
||||||
|
effectiveOffset = (focusPage - 1) * limit;
|
||||||
|
const focusResult2 = await f0cklib.getf0cks({
|
||||||
|
user, fav: true, mode, ratings: ratingsArr,
|
||||||
|
session: req.session, exclude: req.session?.excluded_tags || [],
|
||||||
|
user_id: req.session?.id, is_admin: req.session?.admin,
|
||||||
|
limit, page: focusPage, explicitTotal: total, lang: req.lang
|
||||||
|
});
|
||||||
|
if (focusResult2?.success) {
|
||||||
|
items = focusResult2.items || [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const isAnonUser = isAnonymizeSession(req.session);
|
||||||
|
const outItems = isAnonUser
|
||||||
|
? items.map(it => ({ ...it, username: 'anonymous', display_name: 'anonymous', username_color: null }))
|
||||||
|
: items;
|
||||||
|
|
||||||
|
// Sort by order preference (items from getf0cks are newest-first by default)
|
||||||
|
if (order === 'asc') outItems.reverse();
|
||||||
|
|
||||||
|
return res.json({
|
||||||
|
success: true,
|
||||||
|
user,
|
||||||
|
order,
|
||||||
|
total,
|
||||||
|
offset: effectiveOffset,
|
||||||
|
limit,
|
||||||
|
items: outItems,
|
||||||
|
hasMore: (effectiveOffset + outItems.length) < total
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[FAVS-FEED] Error:', err);
|
||||||
|
return res.json({ success: false, error: 'Failed to load favs feed', items: [], total: 0 }, 500);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
group.get(/\/tag-feed$/, async (req, res) => {
|
group.get(/\/tag-feed$/, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const tag = req.url.qs?.tag ? String(req.url.qs.tag).trim() : null;
|
const tag = req.url.qs?.tag ? String(req.url.qs.tag).trim() : null;
|
||||||
@@ -2134,6 +2233,25 @@ export default router => {
|
|||||||
VALUES (${itemid}, ${newRatingId}, ${req.session.id})
|
VALUES (${itemid}, ${newRatingId}, ${req.session.id})
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Propagate rating to all album sub-items if this is an album
|
||||||
|
const albumCheck = await sql`SELECT id FROM items WHERE id = ${itemid} AND is_album = true LIMIT 1`;
|
||||||
|
if (albumCheck.length > 0) {
|
||||||
|
await sql`
|
||||||
|
DELETE FROM album_items_tags_assign
|
||||||
|
WHERE album_item_id IN (SELECT id FROM album_items WHERE item_id = ${itemid})
|
||||||
|
AND tag_id IN (1, 2, ${nsfl_id})
|
||||||
|
`;
|
||||||
|
if (newRatingId > 0) {
|
||||||
|
await sql`
|
||||||
|
INSERT INTO album_items_tags_assign (album_item_id, tag_id, user_id)
|
||||||
|
SELECT id, ${newRatingId}, ${req.session.id}
|
||||||
|
FROM album_items
|
||||||
|
WHERE item_id = ${itemid}
|
||||||
|
ON CONFLICT DO NOTHING
|
||||||
|
`.catch(() => {});
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure blurred thumbnail exists
|
// Ensure blurred thumbnail exists
|
||||||
|
|||||||
@@ -351,6 +351,27 @@ export default router => {
|
|||||||
VALUES (${+postid}, ${+tagid}, ${+req.session.id})
|
VALUES (${+postid}, ${+tagid}, ${+req.session.id})
|
||||||
ON CONFLICT DO NOTHING
|
ON CONFLICT DO NOTHING
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
// Ensure this album item also inherits parent's rating tag if it doesn't have one
|
||||||
|
const subRating = await db`
|
||||||
|
SELECT tag_id FROM album_items_tags_assign
|
||||||
|
WHERE album_item_id = ${+albumItemId} AND tag_id IN (1, 2, ${nsflId})
|
||||||
|
LIMIT 1
|
||||||
|
`;
|
||||||
|
if (subRating.length === 0) {
|
||||||
|
const parentRating = await db`
|
||||||
|
SELECT tag_id FROM tags_assign
|
||||||
|
WHERE item_id = ${+postid} AND tag_id IN (1, 2, ${nsflId})
|
||||||
|
LIMIT 1
|
||||||
|
`;
|
||||||
|
if (parentRating.length > 0) {
|
||||||
|
await db`
|
||||||
|
INSERT INTO album_items_tags_assign (album_item_id, tag_id, user_id)
|
||||||
|
VALUES (${+albumItemId}, ${parentRating[0].tag_id}, ${+req.session.id})
|
||||||
|
ON CONFLICT DO NOTHING
|
||||||
|
`.catch(() => {});
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
await db`
|
await db`
|
||||||
insert into "tags_assign" ${db({
|
insert into "tags_assign" ${db({
|
||||||
|
|||||||
@@ -494,7 +494,12 @@ export default (router, tpl) => {
|
|||||||
return res.reply({ body });
|
return res.reply({ body });
|
||||||
}
|
}
|
||||||
|
|
||||||
const isOnara = !!(cfg.onara !== undefined ? cfg.onara : cfg.websrv?.onara);
|
const cookieOnara = req.cookies?.f0ck_onara !== undefined
|
||||||
|
? (req.cookies.f0ck_onara === '1' || req.cookies.f0ck_onara === 'true')
|
||||||
|
: (req.cookies?.onara !== undefined ? (req.cookies.onara === '1' || req.cookies.onara === 'true') : null);
|
||||||
|
const isOnara = cookieOnara !== null
|
||||||
|
? cookieOnara
|
||||||
|
: !!(req.session?.onara || (cfg.onara !== undefined ? cfg.onara : cfg.websrv?.onara));
|
||||||
if (mode === 'item' && isOnara) {
|
if (mode === 'item' && isOnara) {
|
||||||
const itemHtml = tpl.render('ajax-item', data, req);
|
const itemHtml = tpl.render('ajax-item', data, req);
|
||||||
|
|
||||||
@@ -583,6 +588,7 @@ export default (router, tpl) => {
|
|||||||
|
|
||||||
gridData.session = data.session;
|
gridData.session = data.session;
|
||||||
gridData.is_onara_item = true;
|
gridData.is_onara_item = true;
|
||||||
|
gridData.onara = true;
|
||||||
gridData.onara_item_html = itemHtml;
|
gridData.onara_item_html = itemHtml;
|
||||||
gridData.item = data.item;
|
gridData.item = data.item;
|
||||||
gridData.page_meta = data.page_meta;
|
gridData.page_meta = data.page_meta;
|
||||||
|
|||||||
+132
-11
@@ -207,19 +207,35 @@ export const handleUpload = async (req, res, self) => {
|
|||||||
const title = rawTitle.length > 0 ? rawTitle.substring(0, 500) : null;
|
const title = rawTitle.length > 0 ? rawTitle.substring(0, 500) : null;
|
||||||
|
|
||||||
const is_oc = (parts.is_oc === true || parts.is_oc === 'true' || parts.is_oc === '1');
|
const is_oc = (parts.is_oc === true || parts.is_oc === 'true' || parts.is_oc === '1');
|
||||||
const isAlbumRequested = (parts.is_album === true || parts.is_album === 'true' || parts.is_album === '1') || (rawFiles.length > 1 && !parts.is_shitpost);
|
|
||||||
const is_album = isAlbumRequested && rawFiles.length > 1;
|
// Collect URL items submitted as subf0ck_url_{i} fields (e.g. YouTube links in album mode)
|
||||||
|
const albumUrlItems = [];
|
||||||
|
for (let i = 0; i < 101; i++) {
|
||||||
|
const urlVal = typeof parts[`subf0ck_url_${i}`] === 'string' ? parts[`subf0ck_url_${i}`].trim() : null;
|
||||||
|
if (urlVal && /^https?:\/\//i.test(urlVal)) {
|
||||||
|
albumUrlItems.push({ index: i, url: urlVal });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const totalAlbumItems = rawFiles.length + albumUrlItems.length;
|
||||||
|
const isAlbumRequested = (parts.is_album === true || parts.is_album === 'true' || parts.is_album === '1') || (totalAlbumItems > 1 && !parts.is_shitpost);
|
||||||
|
const is_album = isAlbumRequested && totalAlbumItems > 1;
|
||||||
const is_shitpost = !is_album && ((parts.is_shitpost === true || parts.is_shitpost === 'true' || parts.is_shitpost === '1') || cfg.websrv.shitpost_mode === true);
|
const is_shitpost = !is_album && ((parts.is_shitpost === true || parts.is_shitpost === 'true' || parts.is_shitpost === '1') || cfg.websrv.shitpost_mode === true);
|
||||||
|
|
||||||
const maxAlbumItems = cfg.websrv?.max_album_items || cfg.websrv?.max_album_images || 100;
|
const maxAlbumItems = cfg.websrv?.max_album_items || cfg.websrv?.max_album_images || 100;
|
||||||
if (is_album && rawFiles.length > maxAlbumItems) {
|
if (is_album && totalAlbumItems > maxAlbumItems) {
|
||||||
return sendJson(res, { success: false, msg: `Album exceeds maximum limit of ${maxAlbumItems} items` }, 400);
|
return sendJson(res, { success: false, msg: `Album exceeds maximum limit of ${maxAlbumItems} items` }, 400);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!file && !inputUrl) {
|
if (!file && !inputUrl && albumUrlItems.length === 0) {
|
||||||
return sendJson(res, { success: false, msg: 'No file or URL provided' }, 400);
|
return sendJson(res, { success: false, msg: 'No file or URL provided' }, 400);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// For URL-only albums: use the first URL as the primary inputUrl
|
||||||
|
if (!file && !inputUrl && albumUrlItems.length > 0) {
|
||||||
|
inputUrl = albumUrlItems[0].url;
|
||||||
|
}
|
||||||
|
|
||||||
if (inputUrl && cfg.websrv.web_url_upload === false) {
|
if (inputUrl && cfg.websrv.web_url_upload === false) {
|
||||||
return sendJson(res, { success: false, msg: 'URL uploads are disabled' }, 403);
|
return sendJson(res, { success: false, msg: 'URL uploads are disabled' }, 403);
|
||||||
}
|
}
|
||||||
@@ -316,7 +332,7 @@ export const handleUpload = async (req, res, self) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (is_album) {
|
if (is_album && rawFiles.length > 0) {
|
||||||
for (let i = 0; i < rawFiles.length; i++) {
|
for (let i = 0; i < rawFiles.length; i++) {
|
||||||
const f = rawFiles[i];
|
const f = rawFiles[i];
|
||||||
if (!f || !f.data || f.data.length === 0) {
|
if (!f || !f.data || f.data.length === 0) {
|
||||||
@@ -464,14 +480,70 @@ export const handleUpload = async (req, res, self) => {
|
|||||||
is_oc: !!is_oc,
|
is_oc: !!is_oc,
|
||||||
slug: itemSlug,
|
slug: itemSlug,
|
||||||
visibility: targetVisibility,
|
visibility: targetVisibility,
|
||||||
is_album: false,
|
is_album: !!is_album,
|
||||||
album_count: 0
|
album_count: is_album ? totalAlbumItems : 0
|
||||||
})})`;
|
})})`;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('[UPLOAD] YouTube new_item notify failed:', err);
|
console.error('[UPLOAD] YouTube new_item notify failed:', err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If this is a URL-only album, insert cover into album_items and process remaining URL sub-items
|
||||||
|
if (is_album && albumUrlItems.length > 0) {
|
||||||
|
const coverSubSlug = lib.generateSlug(11);
|
||||||
|
await db`
|
||||||
|
INSERT INTO album_items ${db({
|
||||||
|
item_id: itemid,
|
||||||
|
dest: filename,
|
||||||
|
mime: 'video/youtube',
|
||||||
|
size: 0,
|
||||||
|
checksum: `yt_${videoId}_cover_${Date.now()}`,
|
||||||
|
phash: null,
|
||||||
|
width: null,
|
||||||
|
height: null,
|
||||||
|
order_index: 0,
|
||||||
|
slug: coverSubSlug
|
||||||
|
}, 'item_id', 'dest', 'mime', 'size', 'checksum', 'phash', 'width', 'height', 'order_index', 'slug')}
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Update the parent item to mark it as an album
|
||||||
|
await db`UPDATE items SET is_album = true, album_count = ${totalAlbumItems} WHERE id = ${itemid}`;
|
||||||
|
|
||||||
|
const ytRegexAlbum = /(?:youtube\.com\/\S*(?:(?:\/e(?:mbed))?\/|watch\/?\?(?:\S*?&?v=))|youtu\.be\/)([a-zA-Z0-9_-]{6,11})/i;
|
||||||
|
for (let ui = 1; ui < albumUrlItems.length; ui++) {
|
||||||
|
const urlItem = albumUrlItems[ui];
|
||||||
|
const subYtMatch = urlItem.url.match(ytRegexAlbum);
|
||||||
|
if (!subYtMatch) continue;
|
||||||
|
const subVideoId = subYtMatch[1];
|
||||||
|
const subYtUrl = `https://www.youtube.com/watch?v=${subVideoId}`;
|
||||||
|
const subYtFilename = `yt:${subVideoId}`;
|
||||||
|
const subYtSlug = lib.generateSlug(11);
|
||||||
|
|
||||||
|
const insertedYtSub = await db`
|
||||||
|
INSERT INTO album_items ${db({
|
||||||
|
item_id: itemid,
|
||||||
|
dest: subYtFilename,
|
||||||
|
mime: 'video/youtube',
|
||||||
|
size: 0,
|
||||||
|
checksum: `yt_${subVideoId}_${Date.now()}_${ui}`,
|
||||||
|
phash: null,
|
||||||
|
width: null,
|
||||||
|
height: null,
|
||||||
|
order_index: ui,
|
||||||
|
slug: subYtSlug
|
||||||
|
}, 'item_id', 'dest', 'mime', 'size', 'checksum', 'phash', 'width', 'height', 'order_index', 'slug')}
|
||||||
|
RETURNING id
|
||||||
|
`;
|
||||||
|
const subYtItemId = insertedYtSub?.[0]?.id;
|
||||||
|
try {
|
||||||
|
await queue.genThumbnail(subYtFilename, 'video/youtube', subYtItemId || 0, subYtUrl, manualApproval);
|
||||||
|
} catch (_) {
|
||||||
|
const tDir = manualApproval ? path.join(cfg.paths.pending, 't') : cfg.paths.t;
|
||||||
|
await queue.spawn('magick', ['-size', '128x128', 'xc:#1a1a1a', '-gravity', 'center', '-fill', '#666', '-pointsize', '20', '-annotate', '0', 'YouTube', path.join(tDir, `${subYtSlug}.webp`)]).catch(() => {});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const itemRoute = itemSlug ? `/${itemSlug}` : `/${itemid}`;
|
const itemRoute = itemSlug ? `/${itemSlug}` : `/${itemid}`;
|
||||||
const successMsg = manualApproval ? 'Upload successful! Your upload is pending admin approval.' : 'Upload successful! Your upload is now live.';
|
const successMsg = manualApproval ? 'Upload successful! Your upload is pending admin approval.' : 'Upload successful! Your upload is now live.';
|
||||||
return sendJson(res, {
|
return sendJson(res, {
|
||||||
@@ -489,7 +561,9 @@ export const handleUpload = async (req, res, self) => {
|
|||||||
tag_id: effectiveRating ? (effectiveRating === 'sfw' ? 1 : (effectiveRating === 'nsfw' ? 2 : (cfg.nsfl_tag_id || 3))) : 0,
|
tag_id: effectiveRating ? (effectiveRating === 'sfw' ? 1 : (effectiveRating === 'nsfw' ? 2 : (cfg.nsfl_tag_id || 3))) : 0,
|
||||||
is_oc: !!is_oc,
|
is_oc: !!is_oc,
|
||||||
display_name: req.session.display_name || null,
|
display_name: req.session.display_name || null,
|
||||||
username: req.session.user
|
username: req.session.user,
|
||||||
|
is_album: !!is_album,
|
||||||
|
album_count: is_album ? totalAlbumItems : 0
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -803,7 +877,7 @@ export const handleUpload = async (req, res, self) => {
|
|||||||
expires_at: targetExpiresAt,
|
expires_at: targetExpiresAt,
|
||||||
uploader_ip: auditIp,
|
uploader_ip: auditIp,
|
||||||
is_album: !!is_album,
|
is_album: !!is_album,
|
||||||
album_count: is_album ? rawFiles.length : 0
|
album_count: is_album ? totalAlbumItems : 0
|
||||||
}, 'src', 'dest', 'mime', 'size', 'checksum', 'phash', 'username', 'userchannel', 'usernetwork', 'stamp', 'active', 'is_oc', 'original_filename', 'title', 'width', 'height', 'visibility', 'slug', 'expires_at', 'uploader_ip', 'is_album', 'album_count')}
|
}, 'src', 'dest', 'mime', 'size', 'checksum', 'phash', 'username', 'userchannel', 'usernetwork', 'stamp', 'active', 'is_oc', 'original_filename', 'title', 'width', 'height', 'visibility', 'slug', 'expires_at', 'uploader_ip', 'is_album', 'album_count')}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@@ -973,6 +1047,44 @@ export const handleUpload = async (req, res, self) => {
|
|||||||
console.warn(`[UPLOAD] Failed to generate thumbnail for album item ${subFilename}:`, tErr.message);
|
console.warn(`[UPLOAD] Failed to generate thumbnail for album item ${subFilename}:`, tErr.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Process URL sub-items (e.g. YouTube links added to the album)
|
||||||
|
const ytRegexAlbum = /(?:youtube\.com\/\S*(?:(?:\/e(?:mbed))?\/|watch\/?\?(?:\S*?&?v=))|youtu\.be\/)([a-zA-Z0-9_-]{6,11})/i;
|
||||||
|
for (const urlItem of albumUrlItems) {
|
||||||
|
// Skip if this was used as the cover (index 0, already inserted via YouTube flow above)
|
||||||
|
if (!file && urlItem.url === inputUrl && urlItem === albumUrlItems[0]) continue;
|
||||||
|
const subYtMatch = urlItem.url.match(ytRegexAlbum);
|
||||||
|
if (!subYtMatch) continue;
|
||||||
|
const subVideoId = subYtMatch[1];
|
||||||
|
const subYtUrl = `https://www.youtube.com/watch?v=${subVideoId}`;
|
||||||
|
const subYtFilename = `yt:${subVideoId}`;
|
||||||
|
const subYtSlug = lib.generateSlug(11);
|
||||||
|
|
||||||
|
const insertedYtSub = await db`
|
||||||
|
INSERT INTO album_items ${db({
|
||||||
|
item_id: itemid,
|
||||||
|
dest: subYtFilename,
|
||||||
|
mime: 'video/youtube',
|
||||||
|
size: 0,
|
||||||
|
checksum: `yt_${subVideoId}_${Date.now()}_${urlItem.index}`,
|
||||||
|
phash: null,
|
||||||
|
width: null,
|
||||||
|
height: null,
|
||||||
|
order_index: rawFiles.length + albumUrlItems.indexOf(urlItem),
|
||||||
|
slug: subYtSlug
|
||||||
|
}, 'item_id', 'dest', 'mime', 'size', 'checksum', 'phash', 'width', 'height', 'order_index', 'slug')}
|
||||||
|
RETURNING id
|
||||||
|
`;
|
||||||
|
const subYtItemId = insertedYtSub?.[0]?.id;
|
||||||
|
|
||||||
|
// Generate placeholder thumbnail
|
||||||
|
try {
|
||||||
|
await queue.genThumbnail(subYtFilename, 'video/youtube', subYtItemId || 0, subYtUrl, manualApproval);
|
||||||
|
} catch (_) {
|
||||||
|
const tDir = manualApproval ? path.join(cfg.paths.pending, 't') : cfg.paths.t;
|
||||||
|
await queue.spawn('magick', ['-size', '128x128', 'xc:#1a1a1a', '-gravity', 'center', '-fill', '#666', '-pointsize', '20', '-annotate', '0', 'YouTube', path.join(tDir, `${subYtSlug}.webp`)]).catch(() => {});
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (req.session?.is_anon) {
|
if (req.session?.is_anon) {
|
||||||
@@ -1067,6 +1179,15 @@ export const handleUpload = async (req, res, self) => {
|
|||||||
await db`
|
await db`
|
||||||
insert into tags_assign ${db({ item_id: itemid, tag_id: ratingTagId, user_id: req.session.id })}
|
insert into tags_assign ${db({ item_id: itemid, tag_id: ratingTagId, user_id: req.session.id })}
|
||||||
`;
|
`;
|
||||||
|
if (is_album) {
|
||||||
|
await db`
|
||||||
|
INSERT INTO album_items_tags_assign (album_item_id, tag_id, user_id)
|
||||||
|
SELECT id, ${ratingTagId}, ${req.session.id}
|
||||||
|
FROM album_items
|
||||||
|
WHERE item_id = ${itemid}
|
||||||
|
ON CONFLICT DO NOTHING
|
||||||
|
`.catch(() => {});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const tagName of tags) {
|
for (const tagName of tags) {
|
||||||
@@ -1231,7 +1352,7 @@ export const handleUpload = async (req, res, self) => {
|
|||||||
slug: itemSlug,
|
slug: itemSlug,
|
||||||
visibility: targetVisibility,
|
visibility: targetVisibility,
|
||||||
is_album: !!is_album,
|
is_album: !!is_album,
|
||||||
album_count: is_album ? rawFiles.length : 0
|
album_count: is_album ? totalAlbumItems : 0
|
||||||
})})`;
|
})})`;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('[UPLOAD] new_item notify failed:', err);
|
console.error('[UPLOAD] new_item notify failed:', err);
|
||||||
@@ -1305,7 +1426,7 @@ export const handleUpload = async (req, res, self) => {
|
|||||||
tag_id: effectiveRating ? (effectiveRating === 'sfw' ? 1 : (effectiveRating === 'nsfw' ? 2 : (cfg.nsfl_tag_id || 3))) : 0,
|
tag_id: effectiveRating ? (effectiveRating === 'sfw' ? 1 : (effectiveRating === 'nsfw' ? 2 : (cfg.nsfl_tag_id || 3))) : 0,
|
||||||
is_oc: !!is_oc,
|
is_oc: !!is_oc,
|
||||||
is_album: !!is_album,
|
is_album: !!is_album,
|
||||||
album_count: is_album ? rawFiles.length : 0,
|
album_count: is_album ? totalAlbumItems : 0,
|
||||||
display_name: req.session.display_name || null,
|
display_name: req.session.display_name || null,
|
||||||
username: req.session.user
|
username: req.session.user
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -103,7 +103,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@endif
|
@endif
|
||||||
@if(onara)
|
@if(onara || is_onara_item)
|
||||||
<div id="onara-modal" class="onara-modal-wrapper" @if(is_onara_item)style="display:flex;" aria-hidden="false"@else style="display:none;" aria-hidden="true"@endif>
|
<div id="onara-modal" class="onara-modal-wrapper" @if(is_onara_item)style="display:flex;" aria-hidden="false"@else style="display:none;" aria-hidden="true"@endif>
|
||||||
<div class="onara-backdrop" id="onara-backdrop"></div>
|
<div class="onara-backdrop" id="onara-backdrop"></div>
|
||||||
<div class="onara-modal-content" id="onara-modal-content">
|
<div class="onara-modal-content" id="onara-modal-content">
|
||||||
@@ -131,6 +131,9 @@
|
|||||||
<button type="button" class="sidebar-tab" id="sidebar-tab-tag" data-tab="tag" title="Tag Feed" aria-label="Tag Feed" style="display: none;">
|
<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>
|
<i class="fa-solid fa-hashtag"></i>
|
||||||
</button>
|
</button>
|
||||||
|
<button type="button" class="sidebar-tab" id="sidebar-tab-favs" data-tab="favs" title="Favorites" aria-label="Favorites" style="display: none;">
|
||||||
|
<i class="fa-solid fa-heart"></i>
|
||||||
|
</button>
|
||||||
<button type="button" class="sidebar-tab" id="sidebar-tab-tuner" data-tab="tuner" title="Tuner & Debug (Audio, Danmaku & Background)" aria-label="Tuner & Debug (Audio, Danmaku & Background)">
|
<button type="button" class="sidebar-tab" id="sidebar-tab-tuner" data-tab="tuner" title="Tuner & Debug (Audio, Danmaku & Background)" aria-label="Tuner & Debug (Audio, Danmaku & Background)">
|
||||||
<i class="fa-solid fa-sliders"></i>
|
<i class="fa-solid fa-sliders"></i>
|
||||||
</button>
|
</button>
|
||||||
@@ -165,6 +168,21 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div id="sidebar-favs-container" class="sidebar-recommendations-list sidebar-tab-content" data-tab-content="favs" style="display: none;">
|
||||||
|
<div class="sidebar-tag-header">
|
||||||
|
<div class="sidebar-tag-title">
|
||||||
|
<i class="fa-solid fa-heart"></i>
|
||||||
|
<span class="sidebar-favs-name"></span>
|
||||||
|
<span class="sidebar-favs-count"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="sidebar-favs-items-container">
|
||||||
|
<div class="sidebar-loading-state">
|
||||||
|
<i class="fa-solid fa-circle-notch fa-spin"></i>
|
||||||
|
<span>Loading favorites...</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div id="sidebar-tuner-container" class="sidebar-tuner-container sidebar-tab-content" data-tab-content="tuner" style="display: none;">
|
<div id="sidebar-tuner-container" class="sidebar-tuner-container sidebar-tab-content" data-tab-content="tuner" style="display: none;">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user