gfsd
This commit is contained in:
@@ -21,3 +21,14 @@ CREATE TABLE IF NOT EXISTS public.album_items (
|
|||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_album_items_item_id ON public.album_items(item_id, order_index ASC);
|
CREATE INDEX IF NOT EXISTS idx_album_items_item_id ON public.album_items(item_id, order_index ASC);
|
||||||
CREATE INDEX IF NOT EXISTS idx_album_items_slug ON public.album_items(slug);
|
CREATE INDEX IF NOT EXISTS idx_album_items_slug ON public.album_items(slug);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS public.album_items_tags_assign (
|
||||||
|
album_item_id INTEGER NOT NULL REFERENCES public.album_items(id) ON DELETE CASCADE,
|
||||||
|
tag_id INTEGER NOT NULL REFERENCES public.tags(id) ON DELETE CASCADE,
|
||||||
|
user_id INTEGER REFERENCES public."user"(id) ON DELETE SET DEFAULT,
|
||||||
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||||
|
PRIMARY KEY (album_item_id, tag_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_album_items_tags_assign_tag_id ON public.album_items_tags_assign(tag_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_album_items_tags_assign_album_item_id ON public.album_items_tags_assign(album_item_id);
|
||||||
|
|||||||
+280
-3
@@ -3160,6 +3160,50 @@ body.sidebar-right-hidden #sidebar-drag-zone {
|
|||||||
text-shadow: 0 0 16px rgba(var(--accent-rgb, 153, 255, 0), 0.35);
|
text-shadow: 0 0 16px rgba(var(--accent-rgb, 153, 255, 0), 0.35);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.sidebar-media-placeholder.audio.has-cover {
|
||||||
|
background: radial-gradient(circle at center, rgba(0, 0, 0, 0.45) 0%, rgba(0, 0, 0, 0.82) 85%), var(--nav-bg, #1e1e1e);
|
||||||
|
}
|
||||||
|
|
||||||
|
.audio-cover-circle {
|
||||||
|
display: none;
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
width: 155px;
|
||||||
|
height: 155px;
|
||||||
|
max-width: calc(100% - 40px);
|
||||||
|
max-height: calc(100% - 40px);
|
||||||
|
aspect-ratio: 1 / 1;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-size: cover;
|
||||||
|
background-position: center;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.88), 0 0 35px rgba(var(--accent-rgb, 153, 255, 0), 0.28), inset 0 0 20px rgba(0, 0, 0, 0.65);
|
||||||
|
border: 2px solid rgba(var(--accent-rgb, 153, 255, 0), 0.4);
|
||||||
|
overflow: hidden;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 1;
|
||||||
|
will-change: transform, box-shadow, border-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.audio-cover-circle::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: radial-gradient(circle at center, transparent 72%, rgba(0, 0, 0, 0.35) 92%, rgba(0, 0, 0, 0.75) 100%);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-media-placeholder.audio.has-cover .audio-cover-circle {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-media-placeholder.audio.has-cover i {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
.sidebar-media-placeholder.hidden {
|
.sidebar-media-placeholder.hidden {
|
||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
@@ -3170,12 +3214,223 @@ body.sidebar-right-hidden #sidebar-drag-zone {
|
|||||||
border-radius: inherit;
|
border-radius: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.album-thumb-item > .sidebar-media-placeholder.audio {
|
||||||
|
z-index: 1;
|
||||||
|
font-size: 1.25rem;
|
||||||
|
border-radius: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
.thumb:has(.sidebar-media-placeholder),
|
.thumb:has(.sidebar-media-placeholder),
|
||||||
.thumb.thumb-fallback:has(.sidebar-media-placeholder) {
|
.thumb.thumb-fallback:has(.sidebar-media-placeholder) {
|
||||||
opacity: 1 !important;
|
opacity: 1 !important;
|
||||||
background-image: none !important;
|
background-image: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.v0ck > .sidebar-media-placeholder.audio,
|
||||||
|
.album-audio-wrapper > .sidebar-media-placeholder.audio,
|
||||||
|
.embed-responsive > .sidebar-media-placeholder.audio {
|
||||||
|
z-index: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
font-size: clamp(2rem, 5.5vw, 3.8rem);
|
||||||
|
border-radius: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v0ck:fullscreen > .sidebar-media-placeholder.audio,
|
||||||
|
.v0ck.v0ck_fullscreen > .sidebar-media-placeholder.audio,
|
||||||
|
#main:fullscreen .v0ck.v0ck_fullscreen > .sidebar-media-placeholder.audio,
|
||||||
|
#onara-modal:fullscreen .v0ck.v0ck_fullscreen > .sidebar-media-placeholder.audio {
|
||||||
|
font-size: clamp(3rem, 7.5vw, 5.5rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
.v0ck:fullscreen .audio-cover-circle,
|
||||||
|
.v0ck.v0ck_fullscreen .audio-cover-circle,
|
||||||
|
#main:fullscreen .v0ck.v0ck_fullscreen .audio-cover-circle,
|
||||||
|
#onara-modal:fullscreen .v0ck.v0ck_fullscreen .audio-cover-circle {
|
||||||
|
width: clamp(260px, 56vmin, 560px);
|
||||||
|
height: clamp(260px, 56vmin, 560px);
|
||||||
|
max-width: calc(100% - 80px);
|
||||||
|
max-height: calc(100% - 100px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-media-placeholder.audio i {
|
||||||
|
position: relative;
|
||||||
|
z-index: 2;
|
||||||
|
display: inline-block;
|
||||||
|
transform-origin: center center;
|
||||||
|
will-change: transform, filter;
|
||||||
|
transition: transform 0.015s linear;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Audio Reactivity Tuner Overlay */
|
||||||
|
.f0ck-tuner-toggle {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 65px;
|
||||||
|
right: 20px;
|
||||||
|
z-index: 99998;
|
||||||
|
background: var(--nav-bg, #1a1a1a);
|
||||||
|
color: var(--accent, #99ff00);
|
||||||
|
border: 1.5px solid var(--accent, #99ff00);
|
||||||
|
border-radius: 20px;
|
||||||
|
padding: 6px 13px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 700;
|
||||||
|
cursor: pointer;
|
||||||
|
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.65);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
transition: transform 0.15s ease, background-color 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.f0ck-tuner-toggle:hover {
|
||||||
|
transform: scale(1.05);
|
||||||
|
background: rgba(var(--accent-rgb, 153, 255, 0), 0.18);
|
||||||
|
}
|
||||||
|
|
||||||
|
.f0ck-audio-tuner-panel {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 110px;
|
||||||
|
right: 20px;
|
||||||
|
z-index: 99999;
|
||||||
|
width: 320px;
|
||||||
|
max-height: 80vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
background: rgba(18, 18, 22, 0.95);
|
||||||
|
backdrop-filter: blur(16px);
|
||||||
|
-webkit-backdrop-filter: blur(16px);
|
||||||
|
border: 1px solid rgba(var(--accent-rgb, 153, 255, 0), 0.45);
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.9);
|
||||||
|
padding: 14px 16px;
|
||||||
|
color: #e6e6e6;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.f0ck-audio-tuner-panel.hidden {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.f0ck-tuner-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
padding-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.f0ck-tuner-section-title {
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 700;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.6px;
|
||||||
|
color: var(--accent, #99ff00);
|
||||||
|
margin: 12px 0 6px 0;
|
||||||
|
padding-bottom: 3px;
|
||||||
|
border-bottom: 1px solid rgba(var(--accent-rgb, 153, 255, 0), 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
.f0ck-tuner-section-title:first-child {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.f0ck-tuner-title {
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--accent, #99ff00);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.f0ck-tuner-close {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: #888;
|
||||||
|
font-size: 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 2px 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.f0ck-tuner-close:hover {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.f0ck-tuner-row {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.f0ck-tuner-row-label {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
color: #bbb;
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.f0ck-tuner-row-label span:last-child {
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--accent, #99ff00);
|
||||||
|
font-family: monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.f0ck-tuner-row input[type="range"] {
|
||||||
|
width: 100%;
|
||||||
|
accent-color: var(--accent, #99ff00);
|
||||||
|
cursor: pointer;
|
||||||
|
margin: 0;
|
||||||
|
height: 4px;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.f0ck-tuner-footer {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: 14px;
|
||||||
|
border-top: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
padding-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.f0ck-tuner-footer button {
|
||||||
|
flex: 1;
|
||||||
|
padding: 8px 10px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 700;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: filter 0.15s ease, background 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.f0ck-tuner-btn-copy {
|
||||||
|
background: var(--accent, #99ff00);
|
||||||
|
color: #000;
|
||||||
|
border: none;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.f0ck-tuner-btn-copy:hover {
|
||||||
|
filter: brightness(1.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.f0ck-tuner-btn-reset {
|
||||||
|
background: rgba(255, 255, 255, 0.08);
|
||||||
|
color: #ccc;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.18);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.f0ck-tuner-btn-reset:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.15);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
.sidebar-video-details {
|
.sidebar-video-details {
|
||||||
display: flex;
|
display: flex;
|
||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
@@ -8347,16 +8602,18 @@ video.emoji {
|
|||||||
|
|
||||||
|
|
||||||
/* visualizer */
|
/* visualizer */
|
||||||
canvas {
|
canvas.audio-visualizer,
|
||||||
|
.v0ck canvas.audio-visualizer,
|
||||||
|
.embed-responsive canvas.audio-visualizer {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 1px;
|
top: 0;
|
||||||
/* This removes the ugly 1px line seen at the bottom when you hover over the audio element */
|
|
||||||
left: 0;
|
left: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
|
z-index: 1 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* tags */
|
/* tags */
|
||||||
@@ -13617,6 +13874,26 @@ body.layout-legacy .navbar-header {
|
|||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.subf0ck-tag-scope {
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
color: var(--color-muted, #888);
|
||||||
|
margin-bottom: 6px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subf0ck-tag-scope code {
|
||||||
|
font-size: 11px;
|
||||||
|
color: #fff;
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
padding: 1px 4px;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
/* ===== layout-modern: flexible by default, viewport-locked for item view ===== */
|
/* ===== layout-modern: flexible by default, viewport-locked for item view ===== */
|
||||||
@media (min-width: 1000px) {
|
@media (min-width: 1000px) {
|
||||||
body.layout-modern {
|
body.layout-modern {
|
||||||
|
|||||||
@@ -2012,3 +2012,26 @@
|
|||||||
opacity: 0.7;
|
opacity: 0.7;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.album-stage-tags-wrap {
|
||||||
|
width: 100%;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.album-stage-tags-input {
|
||||||
|
width: 100%;
|
||||||
|
background: rgba(0, 0, 0, 0.45);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.15);
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 3px 6px;
|
||||||
|
font-size: 0.68rem;
|
||||||
|
color: #fff;
|
||||||
|
outline: none;
|
||||||
|
box-sizing: border-box;
|
||||||
|
transition: border-color 0.2s ease, background 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.album-stage-tags-input:focus {
|
||||||
|
border-color: var(--accent);
|
||||||
|
background: rgba(0, 0, 0, 0.65);
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -52,6 +52,18 @@
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.v0ck canvas.audio-visualizer {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
bottom: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 1 !important;
|
||||||
|
}
|
||||||
|
|
||||||
.v0ck_overlay {
|
.v0ck_overlay {
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
|||||||
+51
-15
@@ -12,8 +12,14 @@
|
|||||||
const tagsContainer = document.querySelector("#tags");
|
const tagsContainer = document.querySelector("#tags");
|
||||||
const inner = tagsContainer ? (tagsContainer.querySelector(".tags-inner") || tagsContainer) : null;
|
const inner = tagsContainer ? (tagsContainer.querySelector(".tags-inner") || tagsContainer) : null;
|
||||||
const usernameEl = document.querySelector("a#a_username");
|
const usernameEl = document.querySelector("a#a_username");
|
||||||
|
const currentSub = (window.albumGallery && typeof window.albumGallery.getCurrentSubf0ck === 'function')
|
||||||
|
? window.albumGallery.getCurrentSubf0ck()
|
||||||
|
: null;
|
||||||
|
const subf0ck_id = currentSub ? (currentSub.slug || currentSub.id) : (tagsContainer?.dataset?.subf0ckSlug || tagsContainer?.dataset?.subf0ckId || null);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
postid: /^\d+$/.test(String(rawId).trim()) ? parseInt(rawId, 10) : rawId.trim(),
|
postid: /^\d+$/.test(String(rawId).trim()) ? parseInt(rawId, 10) : rawId.trim(),
|
||||||
|
subf0ck_id,
|
||||||
// data-username holds the raw DB username; data-author-id holds the user's numeric ID.
|
// data-username holds the raw DB username; data-author-id holds the user's numeric ID.
|
||||||
// Never fall back to innerText — it may be a display name or the literal string 'unknown'.
|
// Never fall back to innerText — it may be a display name or the literal string 'unknown'.
|
||||||
poster: (usernameEl?.dataset?.username || '').trim() || null,
|
poster: (usernameEl?.dataset?.username || '').trim() || null,
|
||||||
@@ -81,7 +87,7 @@
|
|||||||
contentEl.textContent = tag.tag;
|
contentEl.textContent = tag.tag;
|
||||||
} else {
|
} else {
|
||||||
contentEl = document.createElement("a");
|
contentEl = document.createElement("a");
|
||||||
contentEl.href = `/tag/${tag.normalized}`;
|
contentEl.href = "/tag/" + tag.normalized;
|
||||||
contentEl.style = "color: inherit !important";
|
contentEl.style = "color: inherit !important";
|
||||||
contentEl.textContent = tag.tag;
|
contentEl.textContent = tag.tag;
|
||||||
}
|
}
|
||||||
@@ -102,19 +108,27 @@
|
|||||||
span.classList.add('can-cycle');
|
span.classList.add('can-cycle');
|
||||||
}
|
}
|
||||||
} else if (!window.f0ckSession?.is_anonymized && window.f0ckSession?.logged_in && !window.f0ckSession?.is_anon && (tag.display_name || tag.user)) {
|
} else if (!window.f0ckSession?.is_anonymized && window.f0ckSession?.logged_in && !window.f0ckSession?.is_anon && (tag.display_name || tag.user)) {
|
||||||
span.setAttribute('tooltip', tag.display_name || tag.user);
|
span.setAttribute("tooltip", tag.display_name || tag.user);
|
||||||
}
|
}
|
||||||
|
|
||||||
span.appendChild(contentEl);
|
span.appendChild(contentEl);
|
||||||
|
|
||||||
|
if (!isRating && tag.can_exclude) {
|
||||||
|
const excludeBtn = document.createElement("button");
|
||||||
|
excludeBtn.type = "button";
|
||||||
|
excludeBtn.className = "tag-exclude-btn";
|
||||||
|
excludeBtn.setAttribute("title", tag.is_excluded ? "Excluded (click to unexclude)" : "Exclude tag");
|
||||||
|
excludeBtn.setAttribute("aria-label", "Exclude tag");
|
||||||
|
excludeBtn.innerHTML = `<i class="fa-solid ${tag.is_excluded ? 'fa-circle-check' : 'fa-ban'}"></i>`;
|
||||||
|
span.appendChild(excludeBtn);
|
||||||
|
}
|
||||||
|
|
||||||
if (!isRating) {
|
if (!isRating) {
|
||||||
const delbutton = document.createElement("a");
|
const delbutton = document.createElement("a");
|
||||||
delbutton.innerHTML = '<i class="fa-solid fa-xmark"></i>';
|
delbutton.href = "#";
|
||||||
delbutton.href = "javascript:void(0)";
|
|
||||||
// Class for delegation
|
|
||||||
delbutton.classList.add("admin-deltag", "removetag");
|
delbutton.classList.add("admin-deltag", "removetag");
|
||||||
|
delbutton.innerHTML = '<i class="fa-solid fa-xmark"></i>';
|
||||||
span.appendChild(document.createTextNode('\u00A0'));
|
span.appendChild(document.createTextNode(" "));
|
||||||
span.appendChild(delbutton);
|
span.appendChild(delbutton);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -188,7 +202,7 @@
|
|||||||
e.stopImmediatePropagation();
|
e.stopImmediatePropagation();
|
||||||
const ctx = getContext();
|
const ctx = getContext();
|
||||||
if (!ctx) return;
|
if (!ctx) return;
|
||||||
const { postid } = ctx;
|
const { postid, subf0ck_id } = ctx;
|
||||||
|
|
||||||
let target = e.target;
|
let target = e.target;
|
||||||
if (target.nodeType === 3) target = target.parentElement;
|
if (target.nodeType === 3) target = target.parentElement;
|
||||||
@@ -202,8 +216,12 @@
|
|||||||
if (typeof ModAction === 'undefined') return alert('Error: ModAction module not loaded');
|
if (typeof ModAction === 'undefined') return alert('Error: ModAction module not loaded');
|
||||||
|
|
||||||
ModAction.confirm((window.f0ckI18n && window.f0ckI18n.tag_delete_title) || 'Delete Tag', `${(window.f0ckI18n && window.f0ckI18n.tag_delete_confirm) || 'Are you sure you want to delete the tag'} <strong style="color:#d9534f">${tagname}</strong>?`, async (reason) => {
|
ModAction.confirm((window.f0ckI18n && window.f0ckI18n.tag_delete_title) || 'Delete Tag', `${(window.f0ckI18n && window.f0ckI18n.tag_delete_confirm) || 'Are you sure you want to delete the tag'} <strong style="color:#d9534f">${tagname}</strong>?`, async (reason) => {
|
||||||
// Send reason via query param for DELETE request
|
const qs = new URLSearchParams();
|
||||||
const res = await (await fetch("/api/v2/tags/" + postid + "/" + encodeURIComponent(tagname) + (reason ? "?reason=" + encodeURIComponent(reason) : ""), {
|
if (reason) qs.set('reason', reason);
|
||||||
|
if (subf0ck_id) qs.set('subf0ck_id', subf0ck_id);
|
||||||
|
const qsStr = qs.toString();
|
||||||
|
|
||||||
|
const res = await (await fetch("/api/v2/tags/" + postid + "/" + encodeURIComponent(tagname) + (qsStr ? "?" + qsStr : ""), {
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
headers: { "X-CSRF-Token": window.f0ckSession?.csrf_token }
|
headers: { "X-CSRF-Token": window.f0ckSession?.csrf_token }
|
||||||
})).json();
|
})).json();
|
||||||
@@ -211,6 +229,10 @@
|
|||||||
if (!res.success) {
|
if (!res.success) {
|
||||||
throw new Error(res.msg || "Error deleting tag");
|
throw new Error(res.msg || "Error deleting tag");
|
||||||
}
|
}
|
||||||
|
if (window.albumGallery && typeof window.albumGallery.getCurrentSubf0ck === 'function') {
|
||||||
|
const cur = window.albumGallery.getCurrentSubf0ck();
|
||||||
|
if (cur && res.tags) cur.tags = res.tags;
|
||||||
|
}
|
||||||
renderTags(res.tags);
|
renderTags(res.tags);
|
||||||
if (window.flashMessage) window.flashMessage((window.f0ckI18n?.tag_deleted_success) || 'Tag deleted', 2500, 'success');
|
if (window.flashMessage) window.flashMessage((window.f0ckI18n?.tag_deleted_success) || 'Tag deleted', 2500, 'success');
|
||||||
}, { allowEmpty: window.f0ckSession?.is_admin });
|
}, { allowEmpty: window.f0ckSession?.is_admin });
|
||||||
@@ -220,7 +242,7 @@
|
|||||||
if (e) e.preventDefault();
|
if (e) e.preventDefault();
|
||||||
const ctx = getContext();
|
const ctx = getContext();
|
||||||
if (!ctx) return;
|
if (!ctx) return;
|
||||||
const { postid, tags } = ctx;
|
const { postid, subf0ck_id, tags } = ctx;
|
||||||
const anchor = document.querySelector("a#a_addtag");
|
const anchor = document.querySelector("a#a_addtag");
|
||||||
if (!anchor) return;
|
if (!anchor) return;
|
||||||
|
|
||||||
@@ -229,13 +251,27 @@
|
|||||||
existingTags: tags,
|
existingTags: tags,
|
||||||
anchorEl: anchor,
|
anchorEl: anchor,
|
||||||
onSubmit: async (tag) => {
|
onSubmit: async (tag) => {
|
||||||
const res = await post("/api/v2/tags/" + postid, { tagname: tag });
|
const payload = { tagname: tag };
|
||||||
if (res.success && window.invalidateItemCache) {
|
if (subf0ck_id) payload.subf0ck_id = subf0ck_id;
|
||||||
window.invalidateItemCache(postid);
|
const res = await post("/api/v2/tags/" + postid, payload);
|
||||||
|
if (res.success) {
|
||||||
|
if (window.albumGallery && typeof window.albumGallery.getCurrentSubf0ck === 'function') {
|
||||||
|
const cur = window.albumGallery.getCurrentSubf0ck();
|
||||||
|
if (cur && res.tags) cur.tags = res.tags;
|
||||||
|
}
|
||||||
|
if (window.invalidateItemCache) {
|
||||||
|
window.invalidateItemCache(postid);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return res;
|
return res;
|
||||||
},
|
},
|
||||||
renderTags
|
renderTags: (newTags, hl) => {
|
||||||
|
if (window.albumGallery && typeof window.albumGallery.getCurrentSubf0ck === 'function') {
|
||||||
|
const cur = window.albumGallery.getCurrentSubf0ck();
|
||||||
|
if (cur) cur.tags = newTags;
|
||||||
|
}
|
||||||
|
renderTags(newTags, hl);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
+549
-87
@@ -374,27 +374,49 @@ window.cancelAnimFrame = (function () {
|
|||||||
// Update audio cover in player if viewing this audio item
|
// Update audio cover in player if viewing this audio item
|
||||||
const audioCover = document.getElementById('f0ck-audio-cover');
|
const audioCover = document.getElementById('f0ck-audio-cover');
|
||||||
if (audioCover && currentId === idStr) {
|
if (audioCover && currentId === idStr) {
|
||||||
|
const parent = audioCover.parentElement;
|
||||||
|
const audioEl = document.querySelector('audio#my-video');
|
||||||
|
let ph = parent ? parent.querySelector(':scope > .sidebar-media-placeholder.audio') : null;
|
||||||
|
if (parent && !ph) {
|
||||||
|
ph = document.createElement('div');
|
||||||
|
ph.className = 'sidebar-media-placeholder audio';
|
||||||
|
ph.innerHTML = '<i class="fa-solid fa-music"></i>';
|
||||||
|
parent.prepend(ph);
|
||||||
|
}
|
||||||
|
let coverCircle = ph ? ph.querySelector('.audio-cover-circle') : null;
|
||||||
if (hasCoverart === true || (hasCoverart === undefined && audioCover.src && audioCover.src.includes('/ca/'))) {
|
if (hasCoverart === true || (hasCoverart === undefined && audioCover.src && audioCover.src.includes('/ca/'))) {
|
||||||
const coverUrl = `/ca/${idStr}.webp?t=${timestamp}`;
|
const coverUrl = `/ca/${idStr}.webp?t=${timestamp}`;
|
||||||
audioCover.src = coverUrl;
|
audioCover.src = coverUrl;
|
||||||
const parent = audioCover.parentElement;
|
|
||||||
if (parent) {
|
if (parent) {
|
||||||
parent.style.background = `url('${coverUrl}') no-repeat center / contain black`;
|
parent.style.background = 'none';
|
||||||
|
if (ph) {
|
||||||
|
if (!coverCircle) {
|
||||||
|
coverCircle = document.createElement('div');
|
||||||
|
coverCircle.className = 'audio-cover-circle';
|
||||||
|
ph.insertBefore(coverCircle, ph.firstChild);
|
||||||
|
}
|
||||||
|
coverCircle.style.backgroundImage = `url('${coverUrl}')`;
|
||||||
|
ph.classList.add('has-cover');
|
||||||
|
ph.style.display = '';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
const audioEl = document.querySelector('audio#my-video');
|
|
||||||
if (audioEl) {
|
if (audioEl) {
|
||||||
audioEl.setAttribute('poster', coverUrl);
|
audioEl.setAttribute('poster', coverUrl);
|
||||||
}
|
}
|
||||||
} else if (hasCoverart === false) {
|
} else if (hasCoverart === false) {
|
||||||
const fallbackUrl = '/s/img/audio.webp';
|
audioCover.removeAttribute('src');
|
||||||
audioCover.src = fallbackUrl;
|
|
||||||
const parent = audioCover.parentElement;
|
|
||||||
if (parent) {
|
if (parent) {
|
||||||
parent.style.background = `url('${fallbackUrl}') no-repeat center / contain black`;
|
parent.style.background = 'none';
|
||||||
|
if (ph) {
|
||||||
|
if (coverCircle) {
|
||||||
|
coverCircle.style.backgroundImage = 'none';
|
||||||
|
}
|
||||||
|
ph.classList.remove('has-cover');
|
||||||
|
ph.style.display = '';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
const audioEl = document.querySelector('audio#my-video');
|
|
||||||
if (audioEl) {
|
if (audioEl) {
|
||||||
audioEl.setAttribute('poster', fallbackUrl);
|
audioEl.removeAttribute('poster');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1684,10 +1706,10 @@ window.cancelAnimFrame = (function () {
|
|||||||
} catch {}
|
} catch {}
|
||||||
}
|
}
|
||||||
if (!targetThumb && slug) {
|
if (!targetThumb && slug) {
|
||||||
targetThumb = document.querySelector(`.posts > a.thumb[href$="/${slug}"]`);
|
targetThumb = document.querySelector(`.posts > a.thumb[href$="/${slug}"], .posts > a.thumb[href*="/${slug}#"]`);
|
||||||
}
|
}
|
||||||
if (!targetThumb && itemid) {
|
if (!targetThumb && itemid) {
|
||||||
targetThumb = document.querySelector(`.posts > a.thumb[href$="/${itemid}"], .posts > a.thumb[data-bg*="/${itemid}."]`);
|
targetThumb = document.querySelector(`.posts > a.thumb[data-item-id="${itemid}"], .posts > a.thumb[href$="/${itemid}"], .posts > a.thumb[href*="/${itemid}#"], .posts > a.thumb[data-bg*="/${itemid}."]`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Blur any other active element so browser native focus cannot highlight a second thumbnail
|
// Blur any other active element so browser native focus cannot highlight a second thumbnail
|
||||||
@@ -1856,7 +1878,8 @@ window.cancelAnimFrame = (function () {
|
|||||||
const gridObj = new URL(window._onaraCurrentGridUrl, window.location.origin);
|
const gridObj = new URL(window._onaraCurrentGridUrl, window.location.origin);
|
||||||
const gridBasePath = gridObj.pathname.replace(/\/p\/\d+/, '').replace(/\/+$/, '');
|
const gridBasePath = gridObj.pathname.replace(/\/p\/\d+/, '').replace(/\/+$/, '');
|
||||||
if (gridBasePath === path) {
|
if (gridBasePath === path) {
|
||||||
return gridObj.pathname + (gridObj.search || '') + (gridObj.hash || '');
|
const safeGridHash = (gridObj.hash && !gridObj.hash.match(/^#[a-zA-Z0-9_-]{1,60}$/)) ? gridObj.hash : '';
|
||||||
|
return gridObj.pathname + (gridObj.search || '') + safeGridHash;
|
||||||
}
|
}
|
||||||
} catch {}
|
} catch {}
|
||||||
}
|
}
|
||||||
@@ -1872,7 +1895,8 @@ window.cancelAnimFrame = (function () {
|
|||||||
path = (path ? path : '') + '/p/' + currentPage;
|
path = (path ? path : '') + '/p/' + currentPage;
|
||||||
}
|
}
|
||||||
if (!path) path = '/';
|
if (!path) path = '/';
|
||||||
return path + (urlObj.search || '') + (urlObj.hash || '');
|
const safeHash = (urlObj.hash && !urlObj.hash.match(/^#[a-zA-Z0-9_-]{1,60}$/)) ? urlObj.hash : '';
|
||||||
|
return path + (urlObj.search || '') + safeHash;
|
||||||
} catch {
|
} catch {
|
||||||
return '/';
|
return '/';
|
||||||
}
|
}
|
||||||
@@ -2731,7 +2755,18 @@ window.cancelAnimFrame = (function () {
|
|||||||
window._currentActiveAlbumGallery = null;
|
window._currentActiveAlbumGallery = null;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (container._f0ckAlbumInit) return;
|
|
||||||
|
const getHashSubf0ckId = () => {
|
||||||
|
return (window.location.hash || '').replace(/^#/, '').trim();
|
||||||
|
};
|
||||||
|
|
||||||
|
if (container._f0ckAlbumInit) {
|
||||||
|
const initialHash = getHashSubf0ckId() || container.getAttribute('data-requested-subf0ck') || '';
|
||||||
|
if (initialHash && window._currentActiveAlbumGallery && typeof window._currentActiveAlbumGallery.showBySlug === 'function') {
|
||||||
|
window._currentActiveAlbumGallery.showBySlug(initialHash);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
container._f0ckAlbumInit = true;
|
container._f0ckAlbumInit = true;
|
||||||
|
|
||||||
let albumData = null;
|
let albumData = null;
|
||||||
@@ -2789,12 +2824,15 @@ window.cancelAnimFrame = (function () {
|
|||||||
|
|
||||||
let currentIndex = 0;
|
let currentIndex = 0;
|
||||||
|
|
||||||
const getHashSubf0ckId = () => {
|
|
||||||
return (window.location.hash || '').replace(/^#/, '').trim();
|
|
||||||
};
|
|
||||||
|
|
||||||
const initialHash = getHashSubf0ckId() || container.getAttribute('data-requested-subf0ck') || '';
|
const initialHash = getHashSubf0ckId() || container.getAttribute('data-requested-subf0ck') || '';
|
||||||
if (initialHash) {
|
if (initialHash) {
|
||||||
|
if (!getHashSubf0ckId()) {
|
||||||
|
history.replaceState(null, '', window.location.pathname + window.location.search + '#' + initialHash);
|
||||||
|
}
|
||||||
|
const locEl = document.querySelector('.location');
|
||||||
|
if (locEl && !locEl.textContent.includes('#')) {
|
||||||
|
locEl.textContent = locEl.textContent.replace(/#.*$/, '') + '#' + initialHash;
|
||||||
|
}
|
||||||
const foundIdx = albumData.findIndex(item =>
|
const foundIdx = albumData.findIndex(item =>
|
||||||
String(item.slug || '') === initialHash ||
|
String(item.slug || '') === initialHash ||
|
||||||
String(item.subf0ck_id || '') === initialHash ||
|
String(item.subf0ck_id || '') === initialHash ||
|
||||||
@@ -2904,6 +2942,10 @@ window.cancelAnimFrame = (function () {
|
|||||||
if (updateHash && subKey) {
|
if (updateHash && subKey) {
|
||||||
history.replaceState(null, '', window.location.pathname + window.location.search + '#' + subKey);
|
history.replaceState(null, '', window.location.pathname + window.location.search + '#' + subKey);
|
||||||
}
|
}
|
||||||
|
const locEl = document.querySelector('.location');
|
||||||
|
if (locEl && subKey) {
|
||||||
|
locEl.textContent = locEl.textContent.replace(/#.*$/, '') + '#' + subKey;
|
||||||
|
}
|
||||||
|
|
||||||
// Pause any playing audio or video
|
// Pause any playing audio or video
|
||||||
if (videoEl && !videoEl.paused) {
|
if (videoEl && !videoEl.paused) {
|
||||||
@@ -2962,16 +3004,42 @@ window.cancelAnimFrame = (function () {
|
|||||||
|
|
||||||
if (audioWrapper) {
|
if (audioWrapper) {
|
||||||
audioWrapper.style.display = 'block';
|
audioWrapper.style.display = 'block';
|
||||||
const coverUrl = item.coverart || item.thumb || '/s/img/audio.webp';
|
const hasCover = item.has_coverart && item.coverart && !item.coverart.includes('audio.webp');
|
||||||
audioWrapper.style.backgroundImage = `url('${coverUrl}')`;
|
const coverUrl = hasCover ? (item.coverart || item.thumb) : null;
|
||||||
audioWrapper.style.backgroundRepeat = 'no-repeat';
|
let ph = audioWrapper.querySelector(':scope > .sidebar-media-placeholder.audio');
|
||||||
audioWrapper.style.backgroundPosition = 'center';
|
if (!ph) {
|
||||||
audioWrapper.style.backgroundSize = 'contain';
|
ph = document.createElement('div');
|
||||||
audioWrapper.style.backgroundColor = 'black';
|
ph.className = 'sidebar-media-placeholder audio';
|
||||||
|
ph.innerHTML = '<i class="fa-solid fa-music"></i>';
|
||||||
|
audioWrapper.prepend(ph);
|
||||||
|
}
|
||||||
|
let coverCircle = ph.querySelector('.audio-cover-circle');
|
||||||
|
if (!coverCircle) {
|
||||||
|
coverCircle = document.createElement('div');
|
||||||
|
coverCircle.className = 'audio-cover-circle';
|
||||||
|
ph.insertBefore(coverCircle, ph.firstChild);
|
||||||
|
}
|
||||||
|
audioWrapper.style.backgroundImage = 'none';
|
||||||
|
audioWrapper.style.backgroundColor = 'transparent';
|
||||||
|
if (coverUrl) {
|
||||||
|
coverCircle.style.backgroundImage = `url('${coverUrl}')`;
|
||||||
|
ph.classList.add('has-cover');
|
||||||
|
} else {
|
||||||
|
coverCircle.style.backgroundImage = 'none';
|
||||||
|
ph.classList.remove('has-cover');
|
||||||
|
}
|
||||||
|
ph.style.display = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (audioEl) {
|
if (audioEl) {
|
||||||
audioEl.style.display = 'block';
|
audioEl.style.display = 'block';
|
||||||
|
const hasCover = item.has_coverart && item.coverart && !item.coverart.includes('audio.webp');
|
||||||
|
const coverUrl = hasCover ? (item.coverart || item.thumb) : null;
|
||||||
|
if (coverUrl) {
|
||||||
|
audioEl.setAttribute('poster', coverUrl);
|
||||||
|
} else {
|
||||||
|
audioEl.removeAttribute('poster');
|
||||||
|
}
|
||||||
|
|
||||||
if (audioEl.src !== newSrc && !audioEl.src.endsWith(newSrc)) {
|
if (audioEl.src !== newSrc && !audioEl.src.endsWith(newSrc)) {
|
||||||
audioEl.src = newSrc;
|
audioEl.src = newSrc;
|
||||||
@@ -2981,6 +3049,12 @@ window.cancelAnimFrame = (function () {
|
|||||||
initAlbumAudioV0ck();
|
initAlbumAudioV0ck();
|
||||||
video = audioEl;
|
video = audioEl;
|
||||||
|
|
||||||
|
if (window.initVisualizer) window.initVisualizer();
|
||||||
|
|
||||||
|
audioEl.addEventListener('play', () => audioWrapper?.classList.add('is-playing'));
|
||||||
|
audioEl.addEventListener('pause', () => audioWrapper?.classList.remove('is-playing'));
|
||||||
|
audioEl.addEventListener('ended', () => audioWrapper?.classList.remove('is-playing'));
|
||||||
|
|
||||||
if (item.size && audioWrapper) {
|
if (item.size && audioWrapper) {
|
||||||
const dlBtn = audioWrapper.querySelector('#v0ck_download');
|
const dlBtn = audioWrapper.querySelector('#v0ck_download');
|
||||||
if (dlBtn) dlBtn.textContent = `Download (${item.size})`;
|
if (dlBtn) dlBtn.textContent = `Download (${item.size})`;
|
||||||
@@ -3041,11 +3115,44 @@ window.cancelAnimFrame = (function () {
|
|||||||
});
|
});
|
||||||
|
|
||||||
updateInfoModal();
|
updateInfoModal();
|
||||||
|
updateAlbumTags();
|
||||||
|
|
||||||
preload(currentIndex + 1);
|
preload(currentIndex + 1);
|
||||||
preload(currentIndex - 1);
|
preload(currentIndex - 1);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const updateAlbumTags = () => {
|
||||||
|
const sub = albumData[currentIndex];
|
||||||
|
if (!sub) return;
|
||||||
|
|
||||||
|
const tagsContainer = document.querySelector('#tags');
|
||||||
|
if (!tagsContainer) return;
|
||||||
|
|
||||||
|
const subSlug = sub.slug || sub.subf0ck_id || sub.id;
|
||||||
|
tagsContainer.dataset.subf0ckId = String(sub.id);
|
||||||
|
tagsContainer.dataset.subf0ckSlug = String(subSlug);
|
||||||
|
tagsContainer.dataset.subf0ckIndex = String(currentIndex);
|
||||||
|
|
||||||
|
let tagScopeEl = document.querySelector('#subf0ck-tag-scope');
|
||||||
|
if (!tagScopeEl) {
|
||||||
|
const sidebarCont = document.querySelector('.sidebar-tags-container');
|
||||||
|
if (sidebarCont) {
|
||||||
|
tagScopeEl = document.createElement('div');
|
||||||
|
tagScopeEl.id = 'subf0ck-tag-scope';
|
||||||
|
tagScopeEl.className = 'subf0ck-tag-scope';
|
||||||
|
sidebarCont.insertBefore(tagScopeEl, tagsContainer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (tagScopeEl) {
|
||||||
|
tagScopeEl.innerHTML = `<i class="fa-solid fa-layer-group"></i> Subf0ck <code>${subSlug}</code> (${currentIndex + 1}/${albumData.length}):`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const subTags = Array.isArray(sub.tags) ? sub.tags : [];
|
||||||
|
if (typeof window.renderTags === 'function') {
|
||||||
|
window.renderTags(subTags);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const updateInfoModal = () => {
|
const updateInfoModal = () => {
|
||||||
const modal = document.getElementById('info-modal');
|
const modal = document.getElementById('info-modal');
|
||||||
if (!modal) return;
|
if (!modal) return;
|
||||||
@@ -3168,63 +3275,81 @@ window.cancelAnimFrame = (function () {
|
|||||||
let stripTimeout = null;
|
let stripTimeout = null;
|
||||||
container.addEventListener('click', (e) => {
|
container.addEventListener('click', (e) => {
|
||||||
if (stripEl && !e.target.closest('.album-thumbnails-strip, .v0ck_player_controls, .v0ck_settings_menu, .v0ck_hud, .album-btn')) {
|
if (stripEl && !e.target.closest('.album-thumbnails-strip, .v0ck_player_controls, .v0ck_settings_menu, .v0ck_hud, .album-btn')) {
|
||||||
stripEl.classList.add('is-visible');
|
stripEl.classList.add('strip-peek');
|
||||||
clearTimeout(stripTimeout);
|
if (stripTimeout) clearTimeout(stripTimeout);
|
||||||
stripTimeout = setTimeout(() => {
|
stripTimeout = setTimeout(() => {
|
||||||
stripEl.classList.remove('is-visible');
|
stripEl.classList.remove('strip-peek');
|
||||||
}, 3500);
|
}, 3000);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Hashchange listener for forward/backward browser navigation
|
// Handle touch swipe for mobile gallery navigation
|
||||||
const hashChangeHandler = () => {
|
let touchStartX = null;
|
||||||
const newHash = getHashSubf0ckId();
|
let touchStartY = null;
|
||||||
if (!newHash) return;
|
|
||||||
const targetIdx = albumData.findIndex(item =>
|
|
||||||
String(item.slug || '') === newHash ||
|
|
||||||
String(item.subf0ck_id || '') === newHash ||
|
|
||||||
String(item.id) === newHash ||
|
|
||||||
String(item.order_index + 1) === newHash
|
|
||||||
);
|
|
||||||
if (targetIdx !== -1 && targetIdx !== currentIndex) {
|
|
||||||
showImage(targetIdx, 'none', false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
window.addEventListener('hashchange', hashChangeHandler);
|
|
||||||
|
|
||||||
// Touch swipe support on album container
|
|
||||||
let touchStartX = 0;
|
|
||||||
let touchStartY = 0;
|
|
||||||
container.addEventListener('touchstart', (e) => {
|
container.addEventListener('touchstart', (e) => {
|
||||||
if (e.target.closest('.v0ck_player_controls, .v0ck_settings_menu, input[type="range"]')) return;
|
if (e.touches.length === 1) {
|
||||||
if (e.touches && e.touches.length === 1) {
|
|
||||||
touchStartX = e.touches[0].clientX;
|
touchStartX = e.touches[0].clientX;
|
||||||
touchStartY = e.touches[0].clientY;
|
touchStartY = e.touches[0].clientY;
|
||||||
}
|
}
|
||||||
}, { passive: true });
|
}, { passive: true });
|
||||||
|
|
||||||
container.addEventListener('touchend', (e) => {
|
container.addEventListener('touchend', (e) => {
|
||||||
if (e.target.closest('.v0ck_player_controls, .v0ck_settings_menu, input[type="range"]')) return;
|
if (touchStartX === null || touchStartY === null) return;
|
||||||
if (e.changedTouches && e.changedTouches.length === 1) {
|
const touchEndX = e.changedTouches[0].clientX;
|
||||||
const diffX = e.changedTouches[0].clientX - touchStartX;
|
const touchEndY = e.changedTouches[0].clientY;
|
||||||
const diffY = e.changedTouches[0].clientY - touchStartY;
|
const diffX = touchEndX - touchStartX;
|
||||||
if (Math.abs(diffX) > 40 && Math.abs(diffX) > Math.abs(diffY) * 1.5) {
|
const diffY = touchEndY - touchStartY;
|
||||||
if (diffX > 0) {
|
|
||||||
showImage(currentIndex - 1, 'prev');
|
// Minimum swipe distance of 50px and predominantly horizontal
|
||||||
} else {
|
if (Math.abs(diffX) > 50 && Math.abs(diffX) > Math.abs(diffY) * 1.5) {
|
||||||
showImage(currentIndex + 1, 'next');
|
if (diffX < 0) {
|
||||||
}
|
showImage(currentIndex + 1, 'next');
|
||||||
|
} else {
|
||||||
|
showImage(currentIndex - 1, 'prev');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
touchStartX = null;
|
||||||
|
touchStartY = null;
|
||||||
}, { passive: true });
|
}, { passive: true });
|
||||||
|
|
||||||
|
// Handle hash change if user navigates back/forward to another subf0ck
|
||||||
|
window.addEventListener('hashchange', () => {
|
||||||
|
const newHash = getHashSubf0ckId();
|
||||||
|
if (!newHash) return;
|
||||||
|
const foundIdx = albumData.findIndex((item) =>
|
||||||
|
String(item.slug || '') === newHash ||
|
||||||
|
String(item.subf0ck_id || '') === newHash ||
|
||||||
|
String(item.id) === newHash ||
|
||||||
|
String(item.order_index + 1) === newHash
|
||||||
|
);
|
||||||
|
if (foundIdx !== -1 && foundIdx !== currentIndex) {
|
||||||
|
showImage(foundIdx, 'none', false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
window._currentActiveAlbumGallery = {
|
window._currentActiveAlbumGallery = {
|
||||||
prev: () => showImage(currentIndex - 1, 'prev'),
|
prev: () => showImage(currentIndex - 1, 'prev'),
|
||||||
next: () => showImage(currentIndex + 1, 'next'),
|
next: () => showImage(currentIndex + 1, 'next'),
|
||||||
|
showBySlug: (subKey) => {
|
||||||
|
if (!subKey || !Array.isArray(albumData)) return false;
|
||||||
|
const foundIdx = albumData.findIndex((item) =>
|
||||||
|
String(item.slug || '') === String(subKey) ||
|
||||||
|
String(item.subf0ck_id || '') === String(subKey) ||
|
||||||
|
String(item.id) === String(subKey) ||
|
||||||
|
String(item.order_index + 1) === String(subKey)
|
||||||
|
);
|
||||||
|
if (foundIdx !== -1) {
|
||||||
|
showImage(foundIdx, 'none', true);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
},
|
||||||
updateInfoModal: updateInfoModal,
|
updateInfoModal: updateInfoModal,
|
||||||
|
updateAlbumTags: updateAlbumTags,
|
||||||
getCurrentSubf0ck: () => albumData[currentIndex],
|
getCurrentSubf0ck: () => albumData[currentIndex],
|
||||||
isHovered: false
|
isHovered: false
|
||||||
};
|
};
|
||||||
|
window.albumGallery = window._currentActiveAlbumGallery;
|
||||||
|
|
||||||
container.addEventListener('mouseenter', () => {
|
container.addEventListener('mouseenter', () => {
|
||||||
if (window._currentActiveAlbumGallery) window._currentActiveAlbumGallery.isHovered = true;
|
if (window._currentActiveAlbumGallery) window._currentActiveAlbumGallery.isHovered = true;
|
||||||
@@ -3234,8 +3359,25 @@ window.cancelAnimFrame = (function () {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const syncLocationSubf0ck = () => {
|
||||||
|
const layout = document.querySelector('.item-layout-container');
|
||||||
|
const reqSub = layout ? (layout.getAttribute('data-requested-subf0ck') || '') : '';
|
||||||
|
const hash = (window.location.hash || '').replace(/^#/, '').trim();
|
||||||
|
const targetSub = hash || reqSub;
|
||||||
|
if (targetSub) {
|
||||||
|
if (!hash && reqSub) {
|
||||||
|
history.replaceState(null, '', window.location.pathname + window.location.search + '#' + reqSub);
|
||||||
|
}
|
||||||
|
const locEl = document.querySelector('.location');
|
||||||
|
if (locEl && !locEl.textContent.includes('#')) {
|
||||||
|
locEl.textContent = locEl.textContent.replace(/#.*$/, '') + '#' + targetSub;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const setupMedia = () => {
|
const setupMedia = () => {
|
||||||
window._currentActiveAlbumGallery = null;
|
window._currentActiveAlbumGallery = null;
|
||||||
|
window.albumGallery = null;
|
||||||
const elem = document.querySelector("#my-video") || document.querySelector("audio#my-video");
|
const elem = document.querySelector("#my-video") || document.querySelector("audio#my-video");
|
||||||
if (elem) {
|
if (elem) {
|
||||||
video = new v0ck(elem);
|
video = new v0ck(elem);
|
||||||
@@ -3243,8 +3385,12 @@ window.cancelAnimFrame = (function () {
|
|||||||
video = null;
|
video = null;
|
||||||
}
|
}
|
||||||
initAlbumGallery();
|
initAlbumGallery();
|
||||||
|
syncLocationSubf0ck();
|
||||||
};
|
};
|
||||||
document.addEventListener('f0ck:contentLoaded', initAlbumGallery);
|
document.addEventListener('f0ck:contentLoaded', () => {
|
||||||
|
initAlbumGallery();
|
||||||
|
syncLocationSubf0ck();
|
||||||
|
});
|
||||||
|
|
||||||
const initOnaraInitialState = () => {
|
const initOnaraInitialState = () => {
|
||||||
if (!isOnaraActive()) return;
|
if (!isOnaraActive()) return;
|
||||||
@@ -3524,13 +3670,155 @@ window.cancelAnimFrame = (function () {
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
// Audio Visualizer Reactivity Tuner
|
||||||
|
const DEFAULT_AUDIO_TUNING = {
|
||||||
|
bassGain: 0.5,
|
||||||
|
bassPower: 2.0,
|
||||||
|
scaleBounce: 0.4,
|
||||||
|
bounceBoost: 2.5,
|
||||||
|
glowIntensity: 250,
|
||||||
|
attackSpeed: 1.0,
|
||||||
|
releaseSpeed: 0.6,
|
||||||
|
coverSize: 155,
|
||||||
|
barHeight: 0.4,
|
||||||
|
smoothing: 0.84
|
||||||
|
};
|
||||||
|
|
||||||
|
let savedTuning = null;
|
||||||
|
try {
|
||||||
|
const raw = localStorage.getItem('f0ck_audio_tuning');
|
||||||
|
if (raw) savedTuning = JSON.parse(raw);
|
||||||
|
} catch (e) {}
|
||||||
|
|
||||||
|
window.audioVisualizerTuning = Object.assign({}, DEFAULT_AUDIO_TUNING, savedTuning || {});
|
||||||
|
|
||||||
|
const initAudioTunerUI = () => {
|
||||||
|
if (document.getElementById('f0ck-audio-tuner-panel')) return;
|
||||||
|
|
||||||
|
const panel = document.createElement('div');
|
||||||
|
panel.id = 'f0ck-audio-tuner-panel';
|
||||||
|
panel.className = 'f0ck-audio-tuner-panel hidden';
|
||||||
|
|
||||||
|
const sliders = [
|
||||||
|
// Disc & Glow Section
|
||||||
|
{ section: 'Cover Art & Glow Reactivity', key: 'scaleBounce', label: 'Disc Bounce (Scale)', min: 0.00, max: 5.00, step: 0.05, unit: 'x' },
|
||||||
|
{ section: 'Cover Art & Glow Reactivity', key: 'bounceBoost', label: 'Bounce Reactivity Boost', min: 1.0, max: 30.0, step: 0.5, unit: 'x' },
|
||||||
|
{ section: 'Cover Art & Glow Reactivity', key: 'bassGain', label: 'Bass Gain (Multiplier)', min: 0.5, max: 6.0, step: 0.1, unit: 'x' },
|
||||||
|
{ section: 'Cover Art & Glow Reactivity', key: 'bassPower', label: 'Sensitivity Curve (Power)', min: 0.30, max: 2.00, step: 0.02, unit: '' },
|
||||||
|
{ section: 'Cover Art & Glow Reactivity', key: 'glowIntensity', label: 'Glow Aura Reach', min: 10, max: 250, step: 5, unit: 'px' },
|
||||||
|
{ section: 'Cover Art & Glow Reactivity', key: 'attackSpeed', label: 'Attack Speed (Snap)', min: 0.10, max: 1.00, step: 0.02, unit: '' },
|
||||||
|
{ section: 'Cover Art & Glow Reactivity', key: 'releaseSpeed', label: 'Release Speed (Decay)', min: 0.05, max: 0.60, step: 0.01, unit: '' },
|
||||||
|
{ section: 'Cover Art & Glow Reactivity', key: 'coverSize', label: 'Cover Disc Diameter', min: 120, max: 450, step: 5, unit: 'px' },
|
||||||
|
|
||||||
|
// Visualizer Bars Section
|
||||||
|
{ section: 'Bottom Visualizer Bars', key: 'barHeight', label: 'Visualizer Bar Height', min: 0.05, max: 2.00, step: 0.05, unit: 'x' },
|
||||||
|
{ section: 'Bottom Visualizer Bars', key: 'smoothing', label: 'Visualizer Bar Smoothing', min: 0.10, max: 0.95, step: 0.02, unit: '' }
|
||||||
|
];
|
||||||
|
|
||||||
|
let rowsHtml = '';
|
||||||
|
let currentSection = '';
|
||||||
|
sliders.forEach(s => {
|
||||||
|
if (s.section && s.section !== currentSection) {
|
||||||
|
currentSection = s.section;
|
||||||
|
rowsHtml += `<div class="f0ck-tuner-section-title">${currentSection}</div>`;
|
||||||
|
}
|
||||||
|
const val = window.audioVisualizerTuning[s.key] !== undefined ? window.audioVisualizerTuning[s.key] : DEFAULT_AUDIO_TUNING[s.key];
|
||||||
|
rowsHtml += `
|
||||||
|
<div class="f0ck-tuner-row" data-key="${s.key}">
|
||||||
|
<div class="f0ck-tuner-row-label">
|
||||||
|
<span>${s.label}</span>
|
||||||
|
<span id="val-${s.key}">${val}${s.unit}</span>
|
||||||
|
</div>
|
||||||
|
<input type="range" id="input-${s.key}" min="${s.min}" max="${s.max}" step="${s.step}" value="${val}" />
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
});
|
||||||
|
|
||||||
|
panel.innerHTML = `
|
||||||
|
<div class="f0ck-tuner-header">
|
||||||
|
<span class="f0ck-tuner-title"><i class="fa-solid fa-sliders"></i> Audio Reactivity Tuner</span>
|
||||||
|
<button type="button" class="f0ck-tuner-close" id="f0ck-tuner-close" title="Close">✕</button>
|
||||||
|
</div>
|
||||||
|
<div class="f0ck-tuner-body">
|
||||||
|
${rowsHtml}
|
||||||
|
</div>
|
||||||
|
<div class="f0ck-tuner-footer">
|
||||||
|
<button type="button" id="f0ck-tuner-copy" class="f0ck-tuner-btn-copy"><i class="fa-solid fa-copy"></i> Copy Settings</button>
|
||||||
|
<button type="button" id="f0ck-tuner-reset" class="f0ck-tuner-btn-reset"><i class="fa-solid fa-rotate-left"></i> Reset</button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const toggleBtn = document.createElement('button');
|
||||||
|
toggleBtn.id = 'f0ck-tuner-toggle';
|
||||||
|
toggleBtn.type = 'button';
|
||||||
|
toggleBtn.className = 'f0ck-tuner-toggle';
|
||||||
|
toggleBtn.innerHTML = '<i class="fa-solid fa-sliders"></i> Tuner';
|
||||||
|
toggleBtn.title = 'Open Live Audio Reactivity Tuner';
|
||||||
|
|
||||||
|
document.body.appendChild(panel);
|
||||||
|
document.body.appendChild(toggleBtn);
|
||||||
|
|
||||||
|
toggleBtn.addEventListener('click', () => {
|
||||||
|
panel.classList.toggle('hidden');
|
||||||
|
});
|
||||||
|
|
||||||
|
panel.querySelector('#f0ck-tuner-close').addEventListener('click', () => {
|
||||||
|
panel.classList.add('hidden');
|
||||||
|
});
|
||||||
|
|
||||||
|
sliders.forEach(s => {
|
||||||
|
const input = panel.querySelector(`#input-${s.key}`);
|
||||||
|
const valEl = panel.querySelector(`#val-${s.key}`);
|
||||||
|
input.addEventListener('input', () => {
|
||||||
|
const parsed = parseFloat(input.value);
|
||||||
|
window.audioVisualizerTuning[s.key] = parsed;
|
||||||
|
valEl.textContent = `${parsed}${s.unit}`;
|
||||||
|
try {
|
||||||
|
localStorage.setItem('f0ck_audio_tuning', JSON.stringify(window.audioVisualizerTuning));
|
||||||
|
} catch (e) {}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
panel.querySelector('#f0ck-tuner-copy').addEventListener('click', () => {
|
||||||
|
const copyBtn = panel.querySelector('#f0ck-tuner-copy');
|
||||||
|
const text = JSON.stringify(window.audioVisualizerTuning, null, 2);
|
||||||
|
navigator.clipboard.writeText(text).then(() => {
|
||||||
|
copyBtn.innerHTML = '<i class="fa-solid fa-check"></i> Copied!';
|
||||||
|
if (typeof window.flashMessage === 'function') {
|
||||||
|
window.flashMessage('Settings copied to clipboard! Paste them into the chat.', 3500, 'success');
|
||||||
|
}
|
||||||
|
setTimeout(() => {
|
||||||
|
copyBtn.innerHTML = '<i class="fa-solid fa-copy"></i> Copy Settings';
|
||||||
|
}, 2000);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
panel.querySelector('#f0ck-tuner-reset').addEventListener('click', () => {
|
||||||
|
Object.assign(window.audioVisualizerTuning, DEFAULT_AUDIO_TUNING);
|
||||||
|
try {
|
||||||
|
localStorage.removeItem('f0ck_audio_tuning');
|
||||||
|
} catch (e) {}
|
||||||
|
sliders.forEach(s => {
|
||||||
|
const input = panel.querySelector(`#input-${s.key}`);
|
||||||
|
const valEl = panel.querySelector(`#val-${s.key}`);
|
||||||
|
input.value = DEFAULT_AUDIO_TUNING[s.key];
|
||||||
|
valEl.textContent = `${DEFAULT_AUDIO_TUNING[s.key]}${s.unit}`;
|
||||||
|
});
|
||||||
|
if (typeof window.flashMessage === 'function') {
|
||||||
|
window.flashMessage('Tuner reset to defaults', 2000, 'info');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
window.initVisualizer = () => {
|
window.initVisualizer = () => {
|
||||||
const audioElement = document.querySelector("audio");
|
const audioElement = document.querySelector("audio#my-video, audio#f0ck-album-audio, audio");
|
||||||
if (audioElement) {
|
if (audioElement) {
|
||||||
|
// Ensure Tuner UI is mounted
|
||||||
|
initAudioTunerUI();
|
||||||
|
|
||||||
// Cleanup existing visualizer
|
// Cleanup existing visualizer
|
||||||
if (visualizerRafId) window.cancelAnimFrame(visualizerRafId);
|
if (visualizerRafId) window.cancelAnimFrame(visualizerRafId);
|
||||||
const existingCanvas = document.querySelector(".v0ck > canvas.audio-visualizer");
|
document.querySelectorAll("canvas.audio-visualizer").forEach(c => c.remove());
|
||||||
if (existingCanvas) existingCanvas.remove();
|
|
||||||
|
|
||||||
const canvas = document.createElement("canvas");
|
const canvas = document.createElement("canvas");
|
||||||
canvas.className = "audio-visualizer";
|
canvas.className = "audio-visualizer";
|
||||||
@@ -3538,39 +3826,195 @@ window.cancelAnimFrame = (function () {
|
|||||||
canvas.width = 1920;
|
canvas.width = 1920;
|
||||||
canvas.height = 1080;
|
canvas.height = 1080;
|
||||||
|
|
||||||
setTimeout(() => {
|
const attachCanvas = () => {
|
||||||
const v0ckContainer = document.querySelector(".v0ck");
|
const v0ckContainer = audioElement.closest('.v0ck') || audioElement.parentElement || document.querySelector('.v0ck');
|
||||||
if (v0ckContainer) v0ckContainer.insertAdjacentElement("afterbegin", canvas);
|
if (v0ckContainer && !v0ckContainer.contains(canvas)) {
|
||||||
}, 400);
|
const controls = v0ckContainer.querySelector('.v0ck_player_controls');
|
||||||
|
if (controls) {
|
||||||
|
v0ckContainer.insertBefore(canvas, controls);
|
||||||
|
} else {
|
||||||
|
v0ckContainer.appendChild(canvas);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
attachCanvas();
|
||||||
|
setTimeout(attachCanvas, 50);
|
||||||
|
setTimeout(attachCanvas, 250);
|
||||||
|
setTimeout(attachCanvas, 600);
|
||||||
|
|
||||||
if (!audioCtx) {
|
if (!audioCtx) {
|
||||||
audioCtx = new (window.AudioContext || window.webkitAudioContext)();
|
audioCtx = new (window.AudioContext || window.webkitAudioContext)();
|
||||||
}
|
}
|
||||||
|
|
||||||
const analyser = audioCtx.createAnalyser();
|
let source = audioElement._mediaElementSource;
|
||||||
analyser.fftSize = 2048;
|
let analyser = audioElement._audioAnalyser;
|
||||||
|
|
||||||
try {
|
if (!source) {
|
||||||
const source = audioCtx.createMediaElementSource(audioElement);
|
try {
|
||||||
source.connect(analyser);
|
source = audioCtx.createMediaElementSource(audioElement);
|
||||||
source.connect(audioCtx.destination);
|
audioElement._mediaElementSource = source;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn("Visualizer Source creation failed (already connected?):", e);
|
console.warn("Visualizer Source creation failed:", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const cfgInit = window.audioVisualizerTuning || DEFAULT_AUDIO_TUNING;
|
||||||
|
if (!analyser) {
|
||||||
|
analyser = audioCtx.createAnalyser();
|
||||||
|
analyser.fftSize = 2048;
|
||||||
|
analyser.smoothingTimeConstant = cfgInit.smoothing !== undefined ? cfgInit.smoothing : 0.80;
|
||||||
|
audioElement._audioAnalyser = analyser;
|
||||||
|
if (source) {
|
||||||
|
source.connect(analyser);
|
||||||
|
source.connect(audioCtx.destination);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
analyser.fftSize = 2048;
|
||||||
|
analyser.smoothingTimeConstant = cfgInit.smoothing !== undefined ? cfgInit.smoothing : 0.80;
|
||||||
}
|
}
|
||||||
|
|
||||||
let data = new Uint8Array(analyser.frequencyBinCount);
|
let data = new Uint8Array(analyser.frequencyBinCount);
|
||||||
|
|
||||||
|
let noteIcon = null;
|
||||||
|
let smoothScale = 1;
|
||||||
|
let smoothGlow = 0;
|
||||||
|
|
||||||
const draw = (data) => {
|
const draw = (data) => {
|
||||||
data = [...data];
|
|
||||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||||
ctx.fillStyle = getComputedStyle(document.body).getPropertyValue("--accent") || "#9f0";
|
const accent = getComputedStyle(document.body).getPropertyValue("--accent")?.trim() || "#99ff00";
|
||||||
data.forEach((value, i) => {
|
ctx.fillStyle = accent;
|
||||||
const percent = value / 256;
|
const count = analyser.frequencyBinCount;
|
||||||
const height = (canvas.height * percent / 2) - 40;
|
const barWidth = canvas.width / count;
|
||||||
const offset = canvas.height - height - 1;
|
const cfg = window.audioVisualizerTuning || DEFAULT_AUDIO_TUNING;
|
||||||
const barWidth = canvas.width / analyser.frequencyBinCount;
|
const barMult = cfg.barHeight !== undefined ? cfg.barHeight : 0.85;
|
||||||
ctx.fillRect(i * barWidth, offset, barWidth, height);
|
|
||||||
});
|
for (let i = 0; i < count; i++) {
|
||||||
|
const value = data[i];
|
||||||
|
if (value > 0) {
|
||||||
|
const percent = (value / 256) * barMult;
|
||||||
|
const height = (canvas.height * percent / 2) - 30;
|
||||||
|
if (height > 0) {
|
||||||
|
const offset = canvas.height - height - 1;
|
||||||
|
ctx.fillRect(i * barWidth, offset, barWidth, height);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cfg.smoothing !== undefined && analyser.smoothingTimeConstant !== cfg.smoothing) {
|
||||||
|
analyser.smoothingTimeConstant = cfg.smoothing;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Animate the music note and/or cover art circle to react dynamically to the music
|
||||||
|
const v0ckContainer = audioElement.closest('.v0ck') || audioElement.parentElement || document.querySelector('.v0ck');
|
||||||
|
const ph = v0ckContainer ? v0ckContainer.querySelector('.sidebar-media-placeholder.audio') : null;
|
||||||
|
if (!noteIcon || !noteIcon.isConnected) {
|
||||||
|
noteIcon = ph ? ph.querySelector('i') : null;
|
||||||
|
}
|
||||||
|
const coverCircle = ph ? ph.querySelector('.audio-cover-circle') : null;
|
||||||
|
|
||||||
|
if (coverCircle && cfg.coverSize) {
|
||||||
|
coverCircle.style.width = cfg.coverSize + 'px';
|
||||||
|
coverCircle.style.height = cfg.coverSize + 'px';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (noteIcon || coverCircle) {
|
||||||
|
if (!audioElement.paused && audioCtx.state === 'running') {
|
||||||
|
// Bass energy from kick/sub-bass bins (1 to 10): peak transient + body
|
||||||
|
let bassMax = 0;
|
||||||
|
let bassSum = 0;
|
||||||
|
const bassBins = Math.min(10, count);
|
||||||
|
for (let b = 1; b <= bassBins; b++) {
|
||||||
|
const val = data[b];
|
||||||
|
if (val > bassMax) bassMax = val;
|
||||||
|
bassSum += val;
|
||||||
|
}
|
||||||
|
const bassPeak = bassMax / 255;
|
||||||
|
const bassAvg = bassSum / (bassBins * 255);
|
||||||
|
// Kick-driven punch: transient peak dominant for instant beat response
|
||||||
|
const kickEnergy = Math.min(1, (bassPeak * 0.75 + bassAvg * 0.25) * (cfg.bassGain || 3.0));
|
||||||
|
|
||||||
|
// Midrange melodic energy
|
||||||
|
let midMax = 0;
|
||||||
|
let midSum = 0;
|
||||||
|
const midStart = 11;
|
||||||
|
const midEnd = Math.min(32, count);
|
||||||
|
for (let m = midStart; m < midEnd; m++) {
|
||||||
|
const val = data[m];
|
||||||
|
if (val > midMax) midMax = val;
|
||||||
|
midSum += val;
|
||||||
|
}
|
||||||
|
const midEnergy = Math.min(1, ((midMax / 255) * 0.65 + (midSum / ((midEnd - midStart) * 255)) * 0.35) * 2.2);
|
||||||
|
|
||||||
|
// High-frequency snap (snares, hi-hats, percs)
|
||||||
|
let highMax = 0;
|
||||||
|
const highStart = 33;
|
||||||
|
const highEnd = Math.min(70, count);
|
||||||
|
for (let h = highStart; h < highEnd; h++) {
|
||||||
|
if (data[h] > highMax) highMax = data[h];
|
||||||
|
}
|
||||||
|
const highEnergy = Math.min(1, (highMax / 255) * 2.0);
|
||||||
|
|
||||||
|
// Dynamic punch with power curve
|
||||||
|
const punch = Math.pow(kickEnergy, (cfg.bassPower !== undefined ? cfg.bassPower : 1.5));
|
||||||
|
const bounceMultiplier = cfg.bounceBoost !== undefined ? cfg.bounceBoost : 1.5;
|
||||||
|
// Target scale jumps based on slider and bounce boost
|
||||||
|
const targetScale = 1 + (punch * (cfg.scaleBounce !== undefined ? cfg.scaleBounce : 0.50) * bounceMultiplier) + (midEnergy * 0.05);
|
||||||
|
const targetGlow = punch * (cfg.glowIntensity !== undefined ? cfg.glowIntensity : 20) + midEnergy * 20;
|
||||||
|
|
||||||
|
// Attack & release smoothing
|
||||||
|
const cfgAttack = cfg.attackSpeed !== undefined ? cfg.attackSpeed : 0.85;
|
||||||
|
const cfgRelease = cfg.releaseSpeed !== undefined ? cfg.releaseSpeed : 0.16;
|
||||||
|
const attackSpeed = targetScale > smoothScale ? cfgAttack : cfgRelease;
|
||||||
|
smoothScale += (targetScale - smoothScale) * attackSpeed;
|
||||||
|
smoothGlow += (targetGlow - smoothGlow) * attackSpeed;
|
||||||
|
|
||||||
|
// Rhythmic tilt based on balance
|
||||||
|
const tilt = (midEnergy - kickEnergy * 0.6) * 18;
|
||||||
|
|
||||||
|
if (noteIcon) {
|
||||||
|
noteIcon.style.transform = `scale(${smoothScale.toFixed(3)}) rotate(${tilt.toFixed(2)}deg)`;
|
||||||
|
noteIcon.style.filter = `drop-shadow(0 0 ${Math.round(14 + smoothGlow * 0.5)}px rgba(var(--accent-rgb, 153, 255, 0), ${(0.45 + (smoothScale - 1) * 1.5).toFixed(2)}))`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (coverCircle) {
|
||||||
|
// Direct scale bounce: the disc visibly thumps like a speaker cone
|
||||||
|
const circleScale = smoothScale;
|
||||||
|
const glow1 = Math.round(16 + smoothGlow * 1.1);
|
||||||
|
const glow2 = Math.round(38 + smoothGlow * 2.3);
|
||||||
|
const glow3 = Math.round(75 + smoothGlow * 3.6);
|
||||||
|
const alpha1 = Math.min(0.98, (0.42 + (smoothScale - 1) * 2.4)).toFixed(2);
|
||||||
|
const alpha2 = Math.min(0.85, (0.24 + (smoothScale - 1) * 1.8)).toFixed(2);
|
||||||
|
const alpha3 = Math.min(0.60, (0.10 + (smoothScale - 1) * 1.2)).toFixed(2);
|
||||||
|
coverCircle.style.transform = `translate(-50%, -50%) scale(${circleScale.toFixed(3)})`;
|
||||||
|
coverCircle.style.boxShadow = `0 14px 45px rgba(0, 0, 0, 0.9), 0 0 ${glow1}px rgba(var(--accent-rgb, 153, 255, 0), ${alpha1}), 0 0 ${glow2}px rgba(var(--accent-rgb, 153, 255, 0), ${alpha2}), 0 0 ${glow3}px rgba(var(--accent-rgb, 153, 255, 0), ${alpha3}), inset 0 0 20px rgba(0, 0, 0, 0.65)`;
|
||||||
|
coverCircle.style.borderColor = `rgba(var(--accent-rgb, 153, 255, 0), ${(0.42 + (smoothScale - 1) * 2.0).toFixed(2)})`;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Decay smoothly back to neutral state when paused or idle
|
||||||
|
if (smoothScale > 1.002) {
|
||||||
|
smoothScale += (1 - smoothScale) * 0.16;
|
||||||
|
if (noteIcon) {
|
||||||
|
noteIcon.style.transform = `scale(${smoothScale.toFixed(3)})`;
|
||||||
|
noteIcon.style.filter = '';
|
||||||
|
}
|
||||||
|
if (coverCircle) {
|
||||||
|
coverCircle.style.transform = `translate(-50%, -50%) scale(${smoothScale.toFixed(3)})`;
|
||||||
|
}
|
||||||
|
} else if (smoothScale !== 1) {
|
||||||
|
smoothScale = 1;
|
||||||
|
if (noteIcon) {
|
||||||
|
noteIcon.style.transform = '';
|
||||||
|
noteIcon.style.filter = '';
|
||||||
|
}
|
||||||
|
if (coverCircle) {
|
||||||
|
coverCircle.style.transform = 'translate(-50%, -50%)';
|
||||||
|
coverCircle.style.boxShadow = '';
|
||||||
|
coverCircle.style.borderColor = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const loopingFunction = () => {
|
const loopingFunction = () => {
|
||||||
@@ -3581,11 +4025,19 @@ window.cancelAnimFrame = (function () {
|
|||||||
|
|
||||||
visualizerRafId = requestAnimationFrame(loopingFunction);
|
visualizerRafId = requestAnimationFrame(loopingFunction);
|
||||||
|
|
||||||
audioElement.onplay = () => {
|
const resumeAudio = () => {
|
||||||
if (audioCtx.state === 'suspended') {
|
if (audioCtx && audioCtx.state === 'suspended') {
|
||||||
audioCtx.resume();
|
audioCtx.resume();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
audioElement.addEventListener('play', resumeAudio);
|
||||||
|
audioElement.addEventListener('playing', resumeAudio);
|
||||||
|
const playerWrap = audioElement.closest('.v0ck') || audioElement.parentElement;
|
||||||
|
if (playerWrap) {
|
||||||
|
playerWrap.addEventListener('click', resumeAudio, { passive: true });
|
||||||
|
playerWrap.addEventListener('pointerdown', resumeAudio, { passive: true });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -5339,6 +5791,8 @@ window.cancelAnimFrame = (function () {
|
|||||||
|
|
||||||
const params = new URLSearchParams();
|
const params = new URLSearchParams();
|
||||||
params.append('mode', window.activeMode);
|
params.append('mode', window.activeMode);
|
||||||
|
const targetHash = new URL(url, window.location.origin).hash.replace(/^#/, '').trim();
|
||||||
|
if (targetHash) params.append('subf0ck', targetHash);
|
||||||
if (tag) params.append('tag', tag);
|
if (tag) params.append('tag', tag);
|
||||||
if (hall) params.append('hall', hall);
|
if (hall) params.append('hall', hall);
|
||||||
if (userHall && userHallOwner) {
|
if (userHall && userHallOwner) {
|
||||||
@@ -6586,13 +7040,18 @@ window.cancelAnimFrame = (function () {
|
|||||||
? window.getCsrfToken()
|
? window.getCsrfToken()
|
||||||
: ((window.f0ckSession && window.f0ckSession.csrf_token) || document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') || '');
|
: ((window.f0ckSession && window.f0ckSession.csrf_token) || document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') || '');
|
||||||
|
|
||||||
|
const currentSub = (window.albumGallery && typeof window.albumGallery.getCurrentSubf0ck === 'function')
|
||||||
|
? window.albumGallery.getCurrentSubf0ck()
|
||||||
|
: null;
|
||||||
|
const subf0ck_id = currentSub ? (currentSub.slug || currentSub.id) : (ratingEl.dataset.subf0ckSlug || ratingEl.dataset.subf0ckId || document.querySelector('#tags')?.dataset?.subf0ckSlug || null);
|
||||||
|
|
||||||
fetch(`/api/v2/item/${postid}/rating`, {
|
fetch(`/api/v2/item/${postid}/rating`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
"X-CSRF-Token": csrf
|
"X-CSRF-Token": csrf
|
||||||
},
|
},
|
||||||
body: JSON.stringify({ rating: targetRating, csrf_token: csrf })
|
body: JSON.stringify({ rating: targetRating, subf0ck_id, csrf_token: csrf })
|
||||||
})
|
})
|
||||||
.then(r => r.json())
|
.then(r => r.json())
|
||||||
.then(res => {
|
.then(res => {
|
||||||
@@ -6611,6 +7070,9 @@ window.cancelAnimFrame = (function () {
|
|||||||
|
|
||||||
if (ratingEl._lastCycleReqId !== reqId) return; // ignore stale responses
|
if (ratingEl._lastCycleReqId !== reqId) return; // ignore stale responses
|
||||||
if (res.success) {
|
if (res.success) {
|
||||||
|
if (res.tags && currentSub) {
|
||||||
|
currentSub.tags = res.tags;
|
||||||
|
}
|
||||||
if (res.tags && window.renderTags) {
|
if (res.tags && window.renderTags) {
|
||||||
window.renderTags(res.tags);
|
window.renderTags(res.tags);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1169,9 +1169,10 @@
|
|||||||
const isVideo = mime.startsWith('video/');
|
const isVideo = mime.startsWith('video/');
|
||||||
const isImage = mime.startsWith('image/');
|
const isImage = mime.startsWith('image/');
|
||||||
|
|
||||||
let thumbUrl = `/t/${video.id}.webp`;
|
let thumbUrl = video.thumb || video.matching_sub_thumb || `/t/${video.id}.webp`;
|
||||||
if (isBlurred) {
|
if (isBlurred) {
|
||||||
thumbUrl = `/t/${video.id}_blur.webp`;
|
const baseThumb = thumbUrl.replace(/\.webp$/, '');
|
||||||
|
thumbUrl = `${baseThumb}_blur.webp`;
|
||||||
}
|
}
|
||||||
if (window.applyThumbCacheBust) thumbUrl = window.applyThumbCacheBust(thumbUrl);
|
if (window.applyThumbCacheBust) thumbUrl = window.applyThumbCacheBust(thumbUrl);
|
||||||
|
|
||||||
@@ -1207,7 +1208,9 @@
|
|||||||
? `<span class="sidebar-video-xd xd-tier-${video.xd_tier || 0}">xD ${video.xd_score}</span>`
|
? `<span class="sidebar-video-xd xd-tier-${video.xd_tier || 0}">xD ${video.xd_score}</span>`
|
||||||
: '';
|
: '';
|
||||||
|
|
||||||
const ext = (video.mime ? video.mime.split('/')[1] : '')
|
const activeMime = video.matching_sub_mime || video.mime || '';
|
||||||
|
const activeDest = video.matching_sub_dest || video.dest || '';
|
||||||
|
const ext = (activeMime ? activeMime.split('/')[1] : '')
|
||||||
.replace('jpeg', 'jpg')
|
.replace('jpeg', 'jpg')
|
||||||
.replace('x-shockwave-flash', 'flash')
|
.replace('x-shockwave-flash', 'flash')
|
||||||
.replace('x-flac', 'flac')
|
.replace('x-flac', 'flac')
|
||||||
@@ -1253,15 +1256,16 @@
|
|||||||
const tagContext = options.tag || null;
|
const tagContext = options.tag || 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 targetHref = (isTagFeed && tagContext)
|
const targetHref = (isTagFeed && tagContext)
|
||||||
? `/tag/${encodeURIComponent(tagContext).replace(/%2C/g, ',').replace(/%20/g, ' ')}/${videoKey}${strictSuffix}`
|
? `/tag/${encodeURIComponent(tagContext).replace(/%2C/g, ',').replace(/%20/g, ' ')}/${videoKey}${strictSuffix}${targetSubHash}`
|
||||||
: `/${videoKey}`;
|
: `/${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(video.dest || '')}" data-mime="${escapeHtml(video.mime || '')}" data-ext="${escapeHtml(ext ? ext.toLowerCase() : '')}" data-mode="${rClass}" 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">
|
||||||
<div class="sidebar-video-thumb-wrap" data-file="${escapeHtml(video.dest || '')}" data-mime="${escapeHtml(video.mime || '')}" data-ext="${escapeHtml(ext ? ext.toLowerCase() : '')}" data-mode="${rClass}">
|
<div class="sidebar-video-thumb-wrap" data-file="${escapeHtml(activeDest)}" data-mime="${escapeHtml(activeMime)}" data-ext="${escapeHtml(ext ? ext.toLowerCase() : '')}" data-mode="${rClass}">
|
||||||
${thumbContentHtml}
|
${thumbContentHtml}
|
||||||
<div class="sidebar-video-badges">
|
<div class="sidebar-video-badges">
|
||||||
${formatBadge}
|
${formatBadge}
|
||||||
|
|||||||
+18
-1
@@ -1364,9 +1364,22 @@ window.initUploadForm = (selector) => {
|
|||||||
const caption = document.createElement('div');
|
const caption = document.createElement('div');
|
||||||
caption.className = 'album-stage-caption';
|
caption.className = 'album-stage-caption';
|
||||||
caption.title = file.name;
|
caption.title = file.name;
|
||||||
caption.textContent = `${file.name} (${formatSize(file.size)})`;
|
|
||||||
card.appendChild(caption);
|
card.appendChild(caption);
|
||||||
|
|
||||||
|
const tagWrap = document.createElement('div');
|
||||||
|
tagWrap.className = 'album-stage-tags-wrap';
|
||||||
|
const tagInput = document.createElement('input');
|
||||||
|
tagInput.type = 'text';
|
||||||
|
tagInput.className = 'album-stage-tags-input';
|
||||||
|
tagInput.placeholder = (window.f0ckI18n && window.f0ckI18n.album_subf0ck_tags_placeholder) || 'Tags for this subf0ck (optional)';
|
||||||
|
tagInput.value = item.subTags || '';
|
||||||
|
tagInput.addEventListener('input', () => {
|
||||||
|
item.subTags = tagInput.value;
|
||||||
|
});
|
||||||
|
tagInput.addEventListener('click', (e) => e.stopPropagation());
|
||||||
|
tagWrap.appendChild(tagInput);
|
||||||
|
card.appendChild(tagWrap);
|
||||||
|
|
||||||
grid.appendChild(card);
|
grid.appendChild(card);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -2982,6 +2995,10 @@ window.initUploadForm = (selector) => {
|
|||||||
for (let i = 0; i < selectedFiles.length; i++) {
|
for (let i = 0; i < selectedFiles.length; i++) {
|
||||||
const f = selectedFiles[i].file || selectedFiles[i];
|
const f = selectedFiles[i].file || selectedFiles[i];
|
||||||
formData.append('files', f);
|
formData.append('files', f);
|
||||||
|
const subTags = selectedFiles[i].subTags || '';
|
||||||
|
if (subTags) {
|
||||||
|
formData.append(`subf0ck_tags_${i}`, subTags);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
+25
-5
@@ -11,8 +11,14 @@
|
|||||||
|
|
||||||
const tagsContainer = document.querySelector("#tags");
|
const tagsContainer = document.querySelector("#tags");
|
||||||
const inner = tagsContainer ? (tagsContainer.querySelector(".tags-inner") || tagsContainer) : null;
|
const inner = tagsContainer ? (tagsContainer.querySelector(".tags-inner") || tagsContainer) : null;
|
||||||
|
const currentSub = (window.albumGallery && typeof window.albumGallery.getCurrentSubf0ck === 'function')
|
||||||
|
? window.albumGallery.getCurrentSubf0ck()
|
||||||
|
: null;
|
||||||
|
const subf0ck_id = currentSub ? (currentSub.slug || currentSub.id) : (tagsContainer?.dataset?.subf0ckSlug || tagsContainer?.dataset?.subf0ckId || null);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
postid: /^\d+$/.test(String(rawId).trim()) ? parseInt(rawId, 10) : rawId.trim(),
|
postid: /^\d+$/.test(String(rawId).trim()) ? parseInt(rawId, 10) : rawId.trim(),
|
||||||
|
subf0ck_id,
|
||||||
poster: document.querySelector("a#a_username")?.innerText,
|
poster: document.querySelector("a#a_username")?.innerText,
|
||||||
tags: inner ? [...inner.querySelectorAll(".badge")].map(t => t.innerText.slice(0, -2)) : []
|
tags: inner ? [...inner.querySelectorAll(".badge")].map(t => t.innerText.slice(0, -2)) : []
|
||||||
};
|
};
|
||||||
@@ -182,7 +188,7 @@
|
|||||||
if (e) e.preventDefault();
|
if (e) e.preventDefault();
|
||||||
const ctx = getContext();
|
const ctx = getContext();
|
||||||
if (!ctx) return;
|
if (!ctx) return;
|
||||||
const { postid, tags } = ctx;
|
const { postid, subf0ck_id, tags } = ctx;
|
||||||
const anchor = document.querySelector("a#a_addtag");
|
const anchor = document.querySelector("a#a_addtag");
|
||||||
if (!anchor) return;
|
if (!anchor) return;
|
||||||
|
|
||||||
@@ -191,13 +197,27 @@
|
|||||||
existingTags: tags,
|
existingTags: tags,
|
||||||
anchorEl: anchor,
|
anchorEl: anchor,
|
||||||
onSubmit: async (tag) => {
|
onSubmit: async (tag) => {
|
||||||
const res = await post("/api/v2/tags/" + postid, { tagname: tag });
|
const payload = { tagname: tag };
|
||||||
if (res.success && window.invalidateItemCache) {
|
if (subf0ck_id) payload.subf0ck_id = subf0ck_id;
|
||||||
window.invalidateItemCache(postid);
|
const res = await post("/api/v2/tags/" + postid, payload);
|
||||||
|
if (res.success) {
|
||||||
|
if (window.albumGallery && typeof window.albumGallery.getCurrentSubf0ck === 'function') {
|
||||||
|
const cur = window.albumGallery.getCurrentSubf0ck();
|
||||||
|
if (cur && res.tags) cur.tags = res.tags;
|
||||||
|
}
|
||||||
|
if (window.invalidateItemCache) {
|
||||||
|
window.invalidateItemCache(postid);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return res;
|
return res;
|
||||||
},
|
},
|
||||||
renderTags
|
renderTags: (newTags, hl) => {
|
||||||
|
if (window.albumGallery && typeof window.albumGallery.getCurrentSubf0ck === 'function') {
|
||||||
|
const cur = window.albumGallery.getCurrentSubf0ck();
|
||||||
|
if (cur) cur.tags = newTags;
|
||||||
|
}
|
||||||
|
renderTags(newTags, hl);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
+28
-3
@@ -133,9 +133,34 @@ class v0ck {
|
|||||||
window.f0ckDebug("[v0ck] Player initialized for", tagName);
|
window.f0ckDebug("[v0ck] Player initialized for", tagName);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tagName === "audio" && elem.hasAttribute('poster')) { // set cover
|
if (tagName === "audio") {
|
||||||
const player = elem.closest('.v0ck') || document.querySelector('.v0ck');
|
const poster = elem.getAttribute('poster');
|
||||||
if (player) player.style.backgroundImage = `url('${elem.getAttribute('poster')}')`;
|
const player = elem.closest('.v0ck') || elem.parentElement;
|
||||||
|
const isFallback = !poster || poster === '/s/img/200.gif' || poster.includes('audio.webp') || poster.includes('music.webp');
|
||||||
|
if (player) {
|
||||||
|
let ph = player.querySelector(':scope > .sidebar-media-placeholder.audio');
|
||||||
|
if (!ph) {
|
||||||
|
ph = document.createElement('div');
|
||||||
|
ph.className = 'sidebar-media-placeholder audio';
|
||||||
|
ph.innerHTML = '<div class="audio-cover-circle"></div><i class="fa-solid fa-music"></i>';
|
||||||
|
player.prepend(ph);
|
||||||
|
}
|
||||||
|
let coverCircle = ph.querySelector('.audio-cover-circle');
|
||||||
|
if (!coverCircle) {
|
||||||
|
coverCircle = document.createElement('div');
|
||||||
|
coverCircle.className = 'audio-cover-circle';
|
||||||
|
ph.insertBefore(coverCircle, ph.firstChild);
|
||||||
|
}
|
||||||
|
player.style.backgroundImage = 'none';
|
||||||
|
player.style.backgroundColor = 'transparent';
|
||||||
|
if (!isFallback) {
|
||||||
|
coverCircle.style.backgroundImage = `url('${poster}')`;
|
||||||
|
ph.classList.add('has-cover');
|
||||||
|
} else {
|
||||||
|
coverCircle.style.backgroundImage = 'none';
|
||||||
|
ph.classList.remove('has-cover');
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
|||||||
+57
-9
@@ -284,17 +284,65 @@ export default new class {
|
|||||||
|
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
async getTags(itemid, session = null) {
|
async getTags(itemid, session = null, subf0ckId = null) {
|
||||||
const isAnonymized = isAnonymizeSession(session);
|
const isAnonymized = isAnonymizeSession(session);
|
||||||
const hasSession = !isAnonymized && !!(session && !session.is_anon && (typeof session === 'object' ? (session.id || session.user) : session));
|
const hasSession = !isAnonymized && !!(session && !session.is_anon && (typeof session === 'object' ? (session.id || session.user) : session));
|
||||||
const tags = await db`
|
|
||||||
select "tags".id, "tags".tag, "tags".normalized${hasSession ? db`, "user".user, uo.display_name` : db``}
|
let albumItemId = null;
|
||||||
from "tags_assign"
|
if (subf0ckId !== null && subf0ckId !== undefined && subf0ckId !== '') {
|
||||||
left join "tags" on "tags".id = "tags_assign".tag_id
|
if (Number.isInteger(+subf0ckId) && +subf0ckId > 0) {
|
||||||
${hasSession ? db`left join "user" on "user".id = "tags_assign".user_id left join user_options uo on uo.user_id = "user".id` : db``}
|
const row = await db`
|
||||||
where "tags_assign".item_id = ${+itemid}
|
SELECT id FROM album_items
|
||||||
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
|
WHERE (id = ${+subf0ckId} OR (item_id = ${+itemid} AND order_index = ${+subf0ckId}))
|
||||||
`;
|
AND item_id = ${+itemid}
|
||||||
|
LIMIT 1
|
||||||
|
`;
|
||||||
|
albumItemId = row?.[0]?.id || null;
|
||||||
|
}
|
||||||
|
if (!albumItemId) {
|
||||||
|
const row = await db`
|
||||||
|
SELECT id FROM album_items
|
||||||
|
WHERE slug = ${String(subf0ckId)}
|
||||||
|
AND item_id = ${+itemid}
|
||||||
|
LIMIT 1
|
||||||
|
`;
|
||||||
|
albumItemId = row?.[0]?.id || null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let tags;
|
||||||
|
if (albumItemId) {
|
||||||
|
tags = await db`
|
||||||
|
select "tags".id, "tags".tag, "tags".normalized${hasSession ? db`, "user".user, uo.display_name` : db``}
|
||||||
|
from "album_items_tags_assign"
|
||||||
|
left join "tags" on "tags".id = "album_items_tags_assign".tag_id
|
||||||
|
${hasSession ? db`left join "user" on "user".id = "album_items_tags_assign".user_id left join user_options uo on uo.user_id = "user".id` : db``}
|
||||||
|
where "album_items_tags_assign".album_item_id = ${+albumItemId}
|
||||||
|
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 (tags.length === 0) {
|
||||||
|
const isOrderZero = await db`SELECT order_index FROM album_items WHERE id = ${albumItemId} LIMIT 1`;
|
||||||
|
if (isOrderZero?.[0]?.order_index === 0) {
|
||||||
|
tags = 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}
|
||||||
|
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
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
tags = 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}
|
||||||
|
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
|
||||||
|
`;
|
||||||
|
}
|
||||||
let hasRating = false;
|
let hasRating = false;
|
||||||
const cleanTags = [];
|
const cleanTags = [];
|
||||||
const excludedTagIds = (session && Array.isArray(session.excluded_tags)) ? session.excluded_tags : [];
|
const excludedTagIds = (session && Array.isArray(session.excluded_tags)) ? session.excluded_tags : [];
|
||||||
|
|||||||
@@ -849,6 +849,8 @@
|
|||||||
"add_more": "Weitere Subf0cks hinzufügen",
|
"add_more": "Weitere Subf0cks hinzufügen",
|
||||||
"drop_hint": "Wähle oder ziehe mehrere Dateien (Subf0cks) hierher, um ein Album zu erstellen",
|
"drop_hint": "Wähle oder ziehe mehrere Dateien (Subf0cks) hierher, um ein Album zu erstellen",
|
||||||
"select_pictures": "Dateien für Album auswählen",
|
"select_pictures": "Dateien für Album auswählen",
|
||||||
"min_pictures": "Mindestens 2 Subf0cks für ein Album erforderlich"
|
"min_pictures": "Mindestens 2 Subf0cks für ein Album erforderlich",
|
||||||
|
"subf0ck_tags": "Tags für diesen Subf0ck",
|
||||||
|
"subf0ck_tags_placeholder": "Tags für diesen Subf0ck (optional)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -849,6 +849,8 @@
|
|||||||
"add_more": "Add more subf0cks",
|
"add_more": "Add more subf0cks",
|
||||||
"drop_hint": "Select or drop multiple files (subf0cks) to create an Album",
|
"drop_hint": "Select or drop multiple files (subf0cks) to create an Album",
|
||||||
"select_pictures": "Select files for album",
|
"select_pictures": "Select files for album",
|
||||||
"min_pictures": "Add at least 2 subf0cks for an album"
|
"min_pictures": "Add at least 2 subf0cks for an album",
|
||||||
|
"subf0ck_tags": "Tags for this subf0ck",
|
||||||
|
"subf0ck_tags_placeholder": "Tags for this subf0ck (optional)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -839,6 +839,8 @@
|
|||||||
"add_more": "Meer afbeeldingen toevoegen",
|
"add_more": "Meer afbeeldingen toevoegen",
|
||||||
"drop_hint": "Selecteer of sleep meerdere afbeeldingen om een album te maken",
|
"drop_hint": "Selecteer of sleep meerdere afbeeldingen om een album te maken",
|
||||||
"select_pictures": "Selecteer afbeeldingen voor album",
|
"select_pictures": "Selecteer afbeeldingen voor album",
|
||||||
"min_pictures": "Voeg minimaal 2 afbeeldingen toe voor een album"
|
"min_pictures": "Voeg minimaal 2 afbeeldingen toe voor een album",
|
||||||
|
"subf0ck_tags": "Tags voor deze subf0ck",
|
||||||
|
"subf0ck_tags_placeholder": "Tags voor deze subf0ck (optioneel)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -839,6 +839,8 @@
|
|||||||
"add_more": "Weitere Bildnisse hinzufügen",
|
"add_more": "Weitere Bildnisse hinzufügen",
|
||||||
"drop_hint": "Wähle oder droppe mehrere Bilder für 1 Album",
|
"drop_hint": "Wähle oder droppe mehrere Bilder für 1 Album",
|
||||||
"select_pictures": "Bildnisse fürs Album auswählen",
|
"select_pictures": "Bildnisse fürs Album auswählen",
|
||||||
"min_pictures": "Mindestens 2 Bildnisse fürs Album nötig"
|
"min_pictures": "Mindestens 2 Bildnisse fürs Album nötig",
|
||||||
|
"subf0ck_tags": "Tags für dies Unterf0ck",
|
||||||
|
"subf0ck_tags_placeholder": "Tags für dies Unterf0ck (optional)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+5
-5
@@ -684,10 +684,10 @@ export default new class queue {
|
|||||||
const ptSize = String(Math.round(thumbSize * 0.35));
|
const ptSize = String(Math.round(thumbSize * 0.35));
|
||||||
try {
|
try {
|
||||||
await this.spawn('magick', [
|
await this.spawn('magick', [
|
||||||
'-size', thumbSpec, 'radial-gradient:#334a15-#1e1e1e',
|
'-size', thumbSpec, 'radial-gradient:#2c2c2c-#181818',
|
||||||
'(', '+clone', '-font', faFont, '-pointsize', ptSize, '-gravity', 'center', '-fill', 'rgba(153,255,0,0.5)', '-annotate', '0', '\uf001', '-blur', '0x12', ')',
|
'(', '+clone', '-font', faFont, '-pointsize', ptSize, '-gravity', 'center', '-fill', 'rgba(255,255,255,0.3)', '-annotate', '0', '\uf001', '-blur', '0x12', ')',
|
||||||
'-composite',
|
'-composite',
|
||||||
'-font', faFont, '-pointsize', ptSize, '-gravity', 'center', '-fill', '#99ff00', '-annotate', '0', '\uf001',
|
'-font', faFont, '-pointsize', ptSize, '-gravity', 'center', '-fill', 'rgba(240,240,240,0.88)', '-annotate', '0', '\uf001',
|
||||||
targetFile
|
targetFile
|
||||||
]);
|
]);
|
||||||
return true;
|
return true;
|
||||||
@@ -698,8 +698,8 @@ export default new class queue {
|
|||||||
return true;
|
return true;
|
||||||
} catch (copyErr) {
|
} catch (copyErr) {
|
||||||
await this.spawn('magick', [
|
await this.spawn('magick', [
|
||||||
'-size', thumbSpec, 'radial-gradient:#334a15-#1e1e1e',
|
'-size', thumbSpec, 'radial-gradient:#2c2c2c-#181818',
|
||||||
'-gravity', 'center', '-fill', '#99ff00', '-pointsize', '60', '-annotate', '0', '♪',
|
'-gravity', 'center', '-fill', 'rgba(240,240,240,0.88)', '-pointsize', '60', '-annotate', '0', '♪',
|
||||||
targetFile
|
targetFile
|
||||||
]).catch(() => {});
|
]).catch(() => {});
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
+267
-26
@@ -394,13 +394,20 @@ const buildFeedFilters = async ({
|
|||||||
const terms = tag.split(',').map(t => t.trim()).filter(Boolean);
|
const terms = tag.split(',').map(t => t.trim()).filter(Boolean);
|
||||||
if (terms.length > 0) {
|
if (terms.length > 0) {
|
||||||
const conditions = terms.map(term => {
|
const conditions = terms.map(term => {
|
||||||
return db`and items.id in (
|
return db`and (items.id in (
|
||||||
select ta.item_id from tags_assign ta
|
select ta.item_id from tags_assign ta
|
||||||
join tags t on t.id = ta.tag_id
|
join tags t on t.id = ta.tag_id
|
||||||
join "user" u on u.id = ta.user_id
|
join "user" u on u.id = ta.user_id
|
||||||
where t.normalized like '%' || slugify(${term}) || '%'
|
where t.normalized like '%' || slugify(${term}) || '%'
|
||||||
and u.user ilike ${tagger}
|
and u.user ilike ${tagger}
|
||||||
)`;
|
) or items.id in (
|
||||||
|
select ai.item_id from album_items ai
|
||||||
|
join album_items_tags_assign aita on aita.album_item_id = ai.id
|
||||||
|
join tags t on t.id = aita.tag_id
|
||||||
|
join "user" u on u.id = aita.user_id
|
||||||
|
where t.normalized like '%' || slugify(${term}) || '%'
|
||||||
|
and u.user ilike ${tagger}
|
||||||
|
))`;
|
||||||
});
|
});
|
||||||
tagFilter = db`${conditions}`;
|
tagFilter = db`${conditions}`;
|
||||||
}
|
}
|
||||||
@@ -408,17 +415,29 @@ const buildFeedFilters = async ({
|
|||||||
const terms = tag.split(',').map(t => t.trim()).filter(Boolean);
|
const terms = tag.split(',').map(t => t.trim()).filter(Boolean);
|
||||||
if (terms.length > 0) {
|
if (terms.length > 0) {
|
||||||
if (isStrict) {
|
if (isStrict) {
|
||||||
tagFilter = db`and items.id in (
|
tagFilter = db`and (items.id in (
|
||||||
select ta.item_id
|
select ta.item_id
|
||||||
from tags_assign ta
|
from tags_assign ta
|
||||||
join tags t on t.id = ta.tag_id
|
join tags t on t.id = ta.tag_id
|
||||||
where t.normalized = ANY(ARRAY(SELECT slugify(x) FROM unnest(${terms}::text[]) AS x))
|
where t.normalized = ANY(ARRAY(SELECT slugify(x) FROM unnest(${terms}::text[]) AS x))
|
||||||
group by ta.item_id
|
group by ta.item_id
|
||||||
having count(distinct t.normalized) = ${terms.length}
|
having count(distinct t.normalized) = ${terms.length}
|
||||||
)`;
|
) or items.id in (
|
||||||
|
select ai.item_id
|
||||||
|
from album_items ai
|
||||||
|
join album_items_tags_assign aita on aita.album_item_id = ai.id
|
||||||
|
join tags t on t.id = aita.tag_id
|
||||||
|
where t.normalized = ANY(ARRAY(SELECT slugify(x) FROM unnest(${terms}::text[]) AS x))
|
||||||
|
group by ai.item_id
|
||||||
|
having count(distinct t.normalized) = ${terms.length}
|
||||||
|
))`;
|
||||||
} else {
|
} else {
|
||||||
const conditions = terms.map(term => {
|
const conditions = terms.map(term => {
|
||||||
return db`and items.id in (select ta.item_id from tags_assign ta join tags t on t.id = ta.tag_id where t.normalized like '%' || slugify(${term}) || '%')`;
|
return db`and (items.id in (
|
||||||
|
select ta.item_id from tags_assign ta join tags t on t.id = ta.tag_id where t.normalized like '%' || slugify(${term}) || '%'
|
||||||
|
) or items.id in (
|
||||||
|
select ai.item_id from album_items ai join album_items_tags_assign aita on aita.album_item_id = ai.id join tags t on t.id = aita.tag_id where t.normalized like '%' || slugify(${term}) || '%'
|
||||||
|
))`;
|
||||||
});
|
});
|
||||||
tagFilter = db`${conditions}`;
|
tagFilter = db`${conditions}`;
|
||||||
}
|
}
|
||||||
@@ -802,6 +821,73 @@ const f0cklib = {
|
|||||||
row.is_audio = !!(row.mime && row.mime.startsWith('audio/'));
|
row.is_audio = !!(row.mime && row.mime.startsWith('audio/'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (tag && !isTitleSearch && rows.some(r => r.is_album)) {
|
||||||
|
const albumIds = rows.filter(r => r.is_album).map(r => r.id);
|
||||||
|
const terms = tag.split(',').map(t => t.trim()).filter(Boolean);
|
||||||
|
if (terms.length > 0 && albumIds.length > 0) {
|
||||||
|
try {
|
||||||
|
const matchingSubs = await db`
|
||||||
|
SELECT
|
||||||
|
ai.item_id,
|
||||||
|
ai.id as subf0ck_id,
|
||||||
|
ai.slug as subf0ck_slug,
|
||||||
|
ai.dest as subf0ck_dest,
|
||||||
|
ai.mime as subf0ck_mime,
|
||||||
|
ai.order_index
|
||||||
|
FROM album_items ai
|
||||||
|
JOIN album_items_tags_assign aita ON aita.album_item_id = ai.id
|
||||||
|
JOIN tags t ON t.id = aita.tag_id
|
||||||
|
${tagger ? db`JOIN "user" u ON u.id = aita.user_id` : db``}
|
||||||
|
WHERE ai.item_id = ANY(${albumIds}::int[])
|
||||||
|
${tagger ? db`AND u.user ILIKE ${tagger}` : db``}
|
||||||
|
AND (${isStrict
|
||||||
|
? db`t.normalized = ANY(ARRAY(SELECT slugify(x) FROM unnest(${terms}::text[]) AS x))`
|
||||||
|
: db`${terms.map(term => db`t.normalized LIKE '%' || slugify(${term}) || '%'`).reduce((a, b) => db`${a} OR ${b}`)}`
|
||||||
|
})
|
||||||
|
ORDER BY ai.item_id, ai.order_index ASC
|
||||||
|
`;
|
||||||
|
|
||||||
|
const matchMap = new Map();
|
||||||
|
for (const ms of matchingSubs) {
|
||||||
|
if (!matchMap.has(ms.item_id)) {
|
||||||
|
matchMap.set(ms.item_id, {
|
||||||
|
first: ms,
|
||||||
|
matchingIds: new Set([ms.subf0ck_id])
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
matchMap.get(ms.item_id).matchingIds.add(ms.subf0ck_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const row of rows) {
|
||||||
|
if (matchMap.has(row.id)) {
|
||||||
|
const info = matchMap.get(row.id);
|
||||||
|
const ms = info.first;
|
||||||
|
row.target_subf0ck_slug = ms.subf0ck_slug || ms.subf0ck_id;
|
||||||
|
if (ms.subf0ck_dest) {
|
||||||
|
const subBase = ms.subf0ck_dest.replace(/\.[^.]+$/, '');
|
||||||
|
row.thumb = `/t/${subBase}.webp`;
|
||||||
|
row.matching_sub_thumb = `/t/${subBase}.webp`;
|
||||||
|
row.matching_sub_dest = ms.subf0ck_dest;
|
||||||
|
row.dest = ms.subf0ck_dest;
|
||||||
|
}
|
||||||
|
if (ms.subf0ck_mime) {
|
||||||
|
row.matching_sub_mime = ms.subf0ck_mime;
|
||||||
|
row.mime = ms.subf0ck_mime;
|
||||||
|
}
|
||||||
|
const mCount = info.matchingIds.size;
|
||||||
|
row.album_count = mCount;
|
||||||
|
if (mCount <= 1) {
|
||||||
|
row.is_album = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('[FEED] Error resolving matching subf0cks for tag search:', err.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Dynamic thumb sizing: applies to the main feed including mime/rating filters.
|
// Dynamic thumb sizing: applies to the main feed including mime/rating filters.
|
||||||
// Only per-user profiles, tag searches, halls, and favorites disable it.
|
// Only per-user profiles, tag searches, halls, and favorites disable it.
|
||||||
const isMainFeed = cfg.websrv.enable_dynamic_thumbs
|
const isMainFeed = cfg.websrv.enable_dynamic_thumbs
|
||||||
@@ -864,7 +950,7 @@ const f0cklib = {
|
|||||||
view_mode: fav ? 'favs' : 'uploads'
|
view_mode: fav ? 'favs' : 'uploads'
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
getf0ck: async ({ user: rawUser, tag: rawTag, hall: rawHall, mime: rawMime, itemid: rawItemid, mode, ratings, session, strict, exclude, user_id, is_admin, fav, random, userHall: rawUserHall, userHallOwner: rawUserHallOwner, lang, ids } = {}) => {
|
getf0ck: async ({ user: rawUser, tag: rawTag, hall: rawHall, mime: rawMime, itemid: rawItemid, mode, ratings, session, strict, exclude, user_id, is_admin, fav, random, userHall: rawUserHall, userHallOwner: rawUserHallOwner, lang, ids, subf0ck } = {}) => {
|
||||||
if (fav && rawUser) {
|
if (fav && rawUser) {
|
||||||
const { isPrivate, isAllowed } = await checkFavoritesAccess(rawUser, { session, user_id, is_admin });
|
const { isPrivate, isAllowed } = await checkFavoritesAccess(rawUser, { session, user_id, is_admin });
|
||||||
if (isPrivate && !isAllowed) {
|
if (isPrivate && !isAllowed) {
|
||||||
@@ -920,7 +1006,7 @@ const f0cklib = {
|
|||||||
|
|
||||||
const isNumeric = /^\d+$/.test(String(rawIdOrSlug));
|
const isNumeric = /^\d+$/.test(String(rawIdOrSlug));
|
||||||
let itemLookup = isNumeric ? db`items.id = ${+rawIdOrSlug}` : db`items.slug = ${String(rawIdOrSlug)}`;
|
let itemLookup = isNumeric ? db`items.id = ${+rawIdOrSlug}` : db`items.slug = ${String(rawIdOrSlug)}`;
|
||||||
let requestedSubf0ckSlug = null;
|
let requestedSubf0ckSlug = subf0ck || null;
|
||||||
|
|
||||||
if (!isNumeric) {
|
if (!isNumeric) {
|
||||||
const itemRow = await db`SELECT id FROM items WHERE slug = ${String(rawIdOrSlug)} LIMIT 1`;
|
const itemRow = await db`SELECT id FROM items WHERE slug = ${String(rawIdOrSlug)} LIMIT 1`;
|
||||||
@@ -1299,20 +1385,58 @@ const f0cklib = {
|
|||||||
}
|
}
|
||||||
const coverartUrl = hasCoverart
|
const coverartUrl = hasCoverart
|
||||||
? `${cfg.websrv.paths.coverarts}/${actitem.id}.webp`
|
? `${cfg.websrv.paths.coverarts}/${actitem.id}.webp`
|
||||||
: `/s/img/music.webp`;
|
: null;
|
||||||
|
|
||||||
let album = [];
|
let album = [];
|
||||||
|
let effectiveItemTags = tags;
|
||||||
if (actitem.is_album) {
|
if (actitem.is_album) {
|
||||||
try {
|
try {
|
||||||
const albumRows = await db`
|
let albumRows = await db`
|
||||||
SELECT id, dest, mime, size, width, height, order_index, slug, checksum
|
SELECT id, dest, mime, size, width, height, order_index, slug, checksum
|
||||||
FROM album_items
|
FROM album_items
|
||||||
WHERE item_id = ${itemid}
|
WHERE item_id = ${itemid}
|
||||||
ORDER BY order_index ASC
|
ORDER BY order_index ASC
|
||||||
`;
|
`;
|
||||||
if (albumRows.length > 0) {
|
if (albumRows.length > 0) {
|
||||||
|
const itemIds = albumRows.map(r => r.id);
|
||||||
|
const subTagsRows = await db`
|
||||||
|
SELECT aita.album_item_id, t.id, t.tag, t.normalized
|
||||||
|
FROM album_items_tags_assign aita
|
||||||
|
JOIN tags t ON t.id = aita.tag_id
|
||||||
|
WHERE aita.album_item_id IN ${db(itemIds)}
|
||||||
|
ORDER BY (case when t.id = 1 then 0 when t.id = 2 then 1 when t.normalized = 'nsfl' then 2 else 3 end) asc, t.id asc
|
||||||
|
`.catch(() => []);
|
||||||
|
const tagsBySubId = new Map();
|
||||||
|
for (const st of subTagsRows) {
|
||||||
|
if (!tagsBySubId.has(st.album_item_id)) tagsBySubId.set(st.album_item_id, []);
|
||||||
|
st.badge = lib.getBadge(st);
|
||||||
|
tagsBySubId.get(st.album_item_id).push(st);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tag && !isTitleSearch) {
|
||||||
|
const terms = tag.split(',').map(t => t.trim()).filter(Boolean);
|
||||||
|
if (terms.length > 0 && tagsBySubId.size > 0) {
|
||||||
|
const matching = albumRows.filter((r, idx) => {
|
||||||
|
let st = tagsBySubId.get(r.id) || [];
|
||||||
|
if (st.length === 0 && (r.order_index === 0 || idx === 0)) {
|
||||||
|
st = tags;
|
||||||
|
}
|
||||||
|
if (isStrict) {
|
||||||
|
return strictParams.every(sp => st.some(t => t.normalized === sp));
|
||||||
|
}
|
||||||
|
return terms.some(term => {
|
||||||
|
const slugTerm = lib.slugify(term);
|
||||||
|
return st.some(t => t.normalized && t.normalized.includes(slugTerm));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
if (matching.length > 0) {
|
||||||
|
albumRows = matching;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
album = await Promise.all(albumRows.map(async (r, idx) => {
|
album = await Promise.all(albumRows.map(async (r, idx) => {
|
||||||
const order = r.order_index !== undefined && r.order_index !== null ? r.order_index : idx;
|
const order = idx;
|
||||||
const subSlug = r.slug || r.id;
|
const subSlug = r.slug || r.id;
|
||||||
const subBase = r.dest.replace(/\.[^.]+$/, '');
|
const subBase = r.dest.replace(/\.[^.]+$/, '');
|
||||||
const isAudio = (r.mime || '').startsWith('audio/');
|
const isAudio = (r.mime || '').startsWith('audio/');
|
||||||
@@ -1363,11 +1487,16 @@ const f0cklib = {
|
|||||||
subThumb = subCover;
|
subThumb = subCover;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
subCover = '/s/img/audio.webp';
|
subCover = null;
|
||||||
subThumb = '/s/img/audio.webp';
|
subThumb = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let subTags = tagsBySubId.get(r.id) || [];
|
||||||
|
if (subTags.length === 0 && (r.order_index === 0 || idx === 0)) {
|
||||||
|
subTags = tags;
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: r.id,
|
id: r.id,
|
||||||
slug: r.slug,
|
slug: r.slug,
|
||||||
@@ -1386,11 +1515,49 @@ const f0cklib = {
|
|||||||
is_video: (r.mime || '').startsWith('video/'),
|
is_video: (r.mime || '').startsWith('video/'),
|
||||||
is_audio: isAudio,
|
is_audio: isAudio,
|
||||||
is_image: (r.mime || '').startsWith('image/'),
|
is_image: (r.mime || '').startsWith('image/'),
|
||||||
has_coverart: isAudio ? (subCover && subCover !== '/s/img/audio.webp') : false,
|
has_coverart: isAudio ? !!subCover : false,
|
||||||
coverart: isAudio ? subCover : null,
|
coverart: isAudio ? subCover : null,
|
||||||
thumb: subThumb
|
thumb: subThumb,
|
||||||
|
tags: subTags
|
||||||
};
|
};
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
let reqIdx = 0;
|
||||||
|
if (requestedSubf0ckSlug && album.length > 0) {
|
||||||
|
const found = album.findIndex(s => String(s.slug || '') === String(requestedSubf0ckSlug) || String(s.subf0ck_id || '') === String(requestedSubf0ckSlug) || String(s.id) === String(requestedSubf0ckSlug));
|
||||||
|
if (found !== -1) {
|
||||||
|
reqIdx = found;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (album.length > 0 && album[reqIdx]) {
|
||||||
|
album.forEach((s, i) => { s.is_first = (i === reqIdx); });
|
||||||
|
const targetSub = album[reqIdx];
|
||||||
|
if (requestedSubf0ckSlug || tag) {
|
||||||
|
requestedSubf0ckSlug = targetSub.slug || targetSub.id;
|
||||||
|
} else {
|
||||||
|
requestedSubf0ckSlug = null;
|
||||||
|
}
|
||||||
|
actitem.dest = targetSub.filename || targetSub.dest.replace(/^\/b\//, '');
|
||||||
|
actitem.mime = targetSub.mime;
|
||||||
|
actitem.width = targetSub.width;
|
||||||
|
actitem.height = targetSub.height;
|
||||||
|
if (targetSub.size) {
|
||||||
|
actitem.size = targetSub.size;
|
||||||
|
}
|
||||||
|
if (targetSub.coverart) {
|
||||||
|
actitem.coverart = targetSub.coverart;
|
||||||
|
}
|
||||||
|
if (targetSub.tags && targetSub.tags.length > 0) {
|
||||||
|
effectiveItemTags = targetSub.tags;
|
||||||
|
if (!effectiveItemTags.some(t => t.id === 1 || t.id === 2 || t.normalized === 'nsfl')) {
|
||||||
|
const ratingTag = tags.find(t => t.id === 1 || t.id === 2 || t.normalized === 'nsfl');
|
||||||
|
if (ratingTag) {
|
||||||
|
effectiveItemTags = [ratingTag, ...effectiveItemTags];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('[GETF0CK] Failed to fetch album items:', err.message);
|
console.error('[GETF0CK] Failed to fetch album items:', err.message);
|
||||||
@@ -1400,10 +1567,10 @@ const f0cklib = {
|
|||||||
const duration = Date.now() - startTime;
|
const duration = Date.now() - startTime;
|
||||||
console.log(`[${new Date().toISOString()}] [GETF0CK_OPT] Fetch complete in ${duration}ms`);
|
console.log(`[${new Date().toISOString()}] [GETF0CK_OPT] Fetch complete in ${duration}ms`);
|
||||||
|
|
||||||
const isNsfl = cfg.enable_nsfl && tags.some(t => t.id == nsfl_id);
|
const isNsfl = cfg.enable_nsfl && effectiveItemTags.some(t => t.id == nsfl_id);
|
||||||
const isNsfw = tags.some(t => t.id == 2);
|
const isNsfw = effectiveItemTags.some(t => t.id == 2);
|
||||||
const isSfw = tags.some(t => t.id == 1);
|
const isSfw = effectiveItemTags.some(t => t.id == 1);
|
||||||
const isTagged = tags.length > 0;
|
const isTagged = effectiveItemTags.length > 0;
|
||||||
const isUntagged = !isSfw && !isNsfw && !isNsfl;
|
const isUntagged = !isSfw && !isNsfw && !isNsfl;
|
||||||
// Guest rating restriction check for public uploads (visibility === 0)
|
// Guest rating restriction check for public uploads (visibility === 0)
|
||||||
if (!session && !isOwnerOrAdmin && (actitem.visibility || 0) === 0) {
|
if (!session && !isOwnerOrAdmin && (actitem.visibility || 0) === 0) {
|
||||||
@@ -1506,6 +1673,8 @@ const f0cklib = {
|
|||||||
return `${ratingLabel}${titlePart} · uploaded by ${actitem.username}`;
|
return `${ratingLabel}${titlePart} · uploaded by ${actitem.username}`;
|
||||||
})(),
|
})(),
|
||||||
coverart: coverartUrl,
|
coverart: coverartUrl,
|
||||||
|
has_coverart: !!hasCoverart,
|
||||||
|
is_audio: (actitem.mime || '').startsWith('audio/'),
|
||||||
dest: (() => {
|
dest: (() => {
|
||||||
if (actitem.mime !== 'video/youtube') return `${cfg.websrv.paths.images}/${actitem.dest}`;
|
if (actitem.mime !== 'video/youtube') return `${cfg.websrv.paths.images}/${actitem.dest}`;
|
||||||
if (actitem.dest && actitem.dest.startsWith('yt:')) return actitem.dest;
|
if (actitem.dest && actitem.dest.startsWith('yt:')) return actitem.dest;
|
||||||
@@ -1522,7 +1691,7 @@ const f0cklib = {
|
|||||||
timefull: new Date(actitem.stamp * 1e3).toISOString()
|
timefull: new Date(actitem.stamp * 1e3).toISOString()
|
||||||
},
|
},
|
||||||
favorites: favorites,
|
favorites: favorites,
|
||||||
tags: tags,
|
tags: effectiveItemTags,
|
||||||
halls: itemHalls,
|
halls: itemHalls,
|
||||||
user_halls: userHallsForItem,
|
user_halls: userHallsForItem,
|
||||||
is_nsfw: isNsfw,
|
is_nsfw: isNsfw,
|
||||||
@@ -2898,22 +3067,36 @@ const f0cklib = {
|
|||||||
const isStrict = !!strict || (tag && tag.includes(','));
|
const isStrict = !!strict || (tag && tag.includes(','));
|
||||||
let tagConditions;
|
let tagConditions;
|
||||||
if (isStrict) {
|
if (isStrict) {
|
||||||
tagConditions = terms.length > 0 ? [db`and items.id in (
|
tagConditions = terms.length > 0 ? [db`and (items.id in (
|
||||||
select ta.item_id
|
select ta.item_id
|
||||||
from tags_assign ta
|
from tags_assign ta
|
||||||
join tags t on t.id = ta.tag_id
|
join tags t on t.id = ta.tag_id
|
||||||
where t.normalized = ANY(ARRAY(SELECT slugify(x) FROM unnest(${terms}::text[]) AS x))
|
where t.normalized = ANY(ARRAY(SELECT slugify(x) FROM unnest(${terms}::text[]) AS x))
|
||||||
group by ta.item_id
|
group by ta.item_id
|
||||||
having count(distinct t.normalized) = ${terms.length}
|
having count(distinct t.normalized) = ${terms.length}
|
||||||
)`] : [];
|
) or items.id in (
|
||||||
|
select ai.item_id
|
||||||
|
from album_items ai
|
||||||
|
join album_items_tags_assign aita on aita.album_item_id = ai.id
|
||||||
|
join tags t on t.id = aita.tag_id
|
||||||
|
where t.normalized = ANY(ARRAY(SELECT slugify(x) FROM unnest(${terms}::text[]) AS x))
|
||||||
|
group by ai.item_id
|
||||||
|
having count(distinct t.normalized) = ${terms.length}
|
||||||
|
))` ] : [];
|
||||||
} else {
|
} else {
|
||||||
tagConditions = terms.map(term => {
|
tagConditions = terms.map(term => {
|
||||||
return db`and items.id in (
|
return db`and (items.id in (
|
||||||
select ta.item_id
|
select ta.item_id
|
||||||
from tags_assign ta
|
from tags_assign ta
|
||||||
join tags t on t.id = ta.tag_id
|
join tags t on t.id = ta.tag_id
|
||||||
where t.normalized like '%' || slugify(${term}) || '%'
|
where t.normalized like '%' || slugify(${term}) || '%'
|
||||||
)`;
|
) or items.id in (
|
||||||
|
select ai.item_id
|
||||||
|
from album_items ai
|
||||||
|
join album_items_tags_assign aita on aita.album_item_id = ai.id
|
||||||
|
join tags t on t.id = aita.tag_id
|
||||||
|
where t.normalized like '%' || slugify(${term}) || '%'
|
||||||
|
))`;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2975,7 +3158,7 @@ const f0cklib = {
|
|||||||
|
|
||||||
const rows = await db`
|
const rows = await db`
|
||||||
WITH matched_items AS (
|
WITH matched_items AS (
|
||||||
SELECT items.id, items.title, items.slug, items.mime, items.dest, items.username, items.stamp, items.xd_score, items.width, items.height, items.has_coverart
|
SELECT items.id, items.title, items.slug, items.mime, items.dest, items.username, items.stamp, items.xd_score, items.width, items.height, items.has_coverart, items.is_album
|
||||||
FROM items
|
FROM items
|
||||||
WHERE items.active = true
|
WHERE items.active = true
|
||||||
AND (items.is_deleted IS NOT TRUE)
|
AND (items.is_deleted IS NOT TRUE)
|
||||||
@@ -3009,6 +3192,59 @@ const f0cklib = {
|
|||||||
ORDER BY ${sortOrderMi}
|
ORDER BY ${sortOrderMi}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
if (rows.some(r => r.is_album)) {
|
||||||
|
const albumIds = rows.filter(r => r.is_album).map(r => r.id);
|
||||||
|
if (terms.length > 0 && albumIds.length > 0) {
|
||||||
|
try {
|
||||||
|
const matchingSubs = await db`
|
||||||
|
SELECT
|
||||||
|
ai.item_id,
|
||||||
|
ai.id as subf0ck_id,
|
||||||
|
ai.slug as subf0ck_slug,
|
||||||
|
ai.dest as subf0ck_dest,
|
||||||
|
ai.mime as subf0ck_mime,
|
||||||
|
ai.order_index
|
||||||
|
FROM album_items ai
|
||||||
|
JOIN album_items_tags_assign aita ON aita.album_item_id = ai.id
|
||||||
|
JOIN tags t ON t.id = aita.tag_id
|
||||||
|
WHERE ai.item_id = ANY(${albumIds}::int[])
|
||||||
|
AND (${isStrict
|
||||||
|
? db`t.normalized = ANY(ARRAY(SELECT slugify(x) FROM unnest(${terms}::text[]) AS x))`
|
||||||
|
: db`${terms.map(term => db`t.normalized LIKE '%' || slugify(${term}) || '%'`).reduce((a, b) => db`${a} OR ${b}`)}`
|
||||||
|
})
|
||||||
|
ORDER BY ai.item_id, ai.order_index ASC
|
||||||
|
`;
|
||||||
|
|
||||||
|
const matchMap = new Map();
|
||||||
|
for (const ms of matchingSubs) {
|
||||||
|
if (!matchMap.has(ms.item_id)) {
|
||||||
|
matchMap.set(ms.item_id, ms);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const row of rows) {
|
||||||
|
if (matchMap.has(row.id)) {
|
||||||
|
const ms = matchMap.get(row.id);
|
||||||
|
row.target_subf0ck_slug = ms.subf0ck_slug || ms.subf0ck_id;
|
||||||
|
if (ms.subf0ck_dest) {
|
||||||
|
const subBase = ms.subf0ck_dest.replace(/\.[^.]+$/, '');
|
||||||
|
row.thumb = `/t/${subBase}.webp`;
|
||||||
|
row.matching_sub_thumb = `/t/${subBase}.webp`;
|
||||||
|
row.matching_sub_dest = ms.subf0ck_dest;
|
||||||
|
row.dest = ms.subf0ck_dest;
|
||||||
|
}
|
||||||
|
if (ms.subf0ck_mime) {
|
||||||
|
row.matching_sub_mime = ms.subf0ck_mime;
|
||||||
|
row.mime = ms.subf0ck_mime;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('[TAG-FEED] Error resolving matching subf0cks for tag feed:', err.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const items = rows.map(r => {
|
const items = rows.map(r => {
|
||||||
const meta = xdScoreMeta(r.xd_score);
|
const meta = xdScoreMeta(r.xd_score);
|
||||||
const tagId = r.rating_tag_id;
|
const tagId = r.rating_tag_id;
|
||||||
@@ -3017,8 +3253,13 @@ const f0cklib = {
|
|||||||
id: r.id,
|
id: r.id,
|
||||||
title: r.title || null,
|
title: r.title || null,
|
||||||
slug: r.slug || null,
|
slug: r.slug || null,
|
||||||
mime: r.mime,
|
mime: r.matching_sub_mime || r.mime,
|
||||||
dest: r.dest,
|
dest: r.matching_sub_dest || r.dest,
|
||||||
|
thumb: r.thumb || `/t/${r.id}.webp`,
|
||||||
|
matching_sub_dest: r.matching_sub_dest || null,
|
||||||
|
matching_sub_mime: r.matching_sub_mime || null,
|
||||||
|
matching_sub_thumb: r.matching_sub_thumb || null,
|
||||||
|
target_subf0ck_slug: r.target_subf0ck_slug || null,
|
||||||
username: r.username,
|
username: r.username,
|
||||||
display_name: r.display_name || r.username,
|
display_name: r.display_name || r.username,
|
||||||
username_color: r.username_color || null,
|
username_color: r.username_color || null,
|
||||||
|
|||||||
@@ -59,7 +59,8 @@ export default (router, tpl) => {
|
|||||||
strict: query.strict === '1' || query.strict === 'true' || req.session?.strict_mode,
|
strict: query.strict === '1' || query.strict === 'true' || req.session?.strict_mode,
|
||||||
explicitStrict: query.strict === '1' || query.strict === 'true',
|
explicitStrict: query.strict === '1' || query.strict === 'true',
|
||||||
exclude: req.session ? (req.session.excluded_tags || []) : [],
|
exclude: req.session ? (req.session.excluded_tags || []) : [],
|
||||||
user_id: req.session?.id
|
user_id: req.session?.id,
|
||||||
|
subf0ck: query.subf0ck || null
|
||||||
});
|
});
|
||||||
const tAjaxFetch = Date.now();
|
const tAjaxFetch = Date.now();
|
||||||
|
|
||||||
|
|||||||
@@ -1820,19 +1820,42 @@ export default router => {
|
|||||||
let newRatingId;
|
let newRatingId;
|
||||||
let currentRatingId = null;
|
let currentRatingId = null;
|
||||||
|
|
||||||
|
const subf0ck = req.body?.subf0ck_id || req.post?.subf0ck_id || req.url?.qs?.subf0ck_id || null;
|
||||||
|
let albumItemId = null;
|
||||||
|
if (subf0ck) {
|
||||||
|
if (Number.isInteger(+subf0ck) && +subf0ck > 0) {
|
||||||
|
const row = await db`SELECT id FROM album_items WHERE (id = ${+subf0ck} OR (item_id = ${itemid} AND order_index = ${+subf0ck})) AND item_id = ${itemid} LIMIT 1`;
|
||||||
|
albumItemId = row?.[0]?.id || null;
|
||||||
|
}
|
||||||
|
if (!albumItemId) {
|
||||||
|
const row = await db`SELECT id FROM album_items WHERE slug = ${String(subf0ck)} AND item_id = ${itemid} LIMIT 1`;
|
||||||
|
albumItemId = row?.[0]?.id || null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
await db.begin(async sql => {
|
await db.begin(async sql => {
|
||||||
// Lock the item row exclusively so any concurrent rating update for this post MUST wait
|
// Lock the item row exclusively so any concurrent rating update for this post MUST wait
|
||||||
await sql`SELECT id FROM items WHERE id = ${itemid} FOR UPDATE`;
|
await sql`SELECT id FROM items WHERE id = ${itemid} FOR UPDATE`;
|
||||||
|
|
||||||
const existingRating = await sql`
|
if (albumItemId) {
|
||||||
SELECT tag_id FROM tags_assign
|
const existingRating = await sql`
|
||||||
WHERE item_id = ${itemid}
|
SELECT tag_id FROM album_items_tags_assign
|
||||||
AND (tag_id IN (1, 2, ${nsfl_id}) OR tag_id IN (SELECT id FROM tags WHERE normalized IN ('sfw', 'nsfw', 'nsfl')))
|
WHERE album_item_id = ${albumItemId}
|
||||||
ORDER BY tag_id DESC
|
AND tag_id IN (1, 2, ${nsfl_id})
|
||||||
LIMIT 1
|
LIMIT 1
|
||||||
`;
|
`;
|
||||||
|
currentRatingId = existingRating.length > 0 ? existingRating[0].tag_id : null;
|
||||||
|
} else {
|
||||||
|
const existingRating = await sql`
|
||||||
|
SELECT tag_id FROM tags_assign
|
||||||
|
WHERE item_id = ${itemid}
|
||||||
|
AND (tag_id IN (1, 2, ${nsfl_id}) OR tag_id IN (SELECT id FROM tags WHERE normalized IN ('sfw', 'nsfw', 'nsfl')))
|
||||||
|
ORDER BY tag_id DESC
|
||||||
|
LIMIT 1
|
||||||
|
`;
|
||||||
|
currentRatingId = existingRating.length > 0 ? existingRating[0].tag_id : null;
|
||||||
|
}
|
||||||
|
|
||||||
currentRatingId = existingRating.length > 0 ? existingRating[0].tag_id : null;
|
|
||||||
const reqRating = req.body?.rating || req.post?.rating || req.url?.qs?.rating;
|
const reqRating = req.body?.rating || req.post?.rating || req.url?.qs?.rating;
|
||||||
if (reqRating === 'sfw') {
|
if (reqRating === 'sfw') {
|
||||||
newRatingId = 1;
|
newRatingId = 1;
|
||||||
@@ -1851,19 +1874,32 @@ export default router => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove ALL existing rating tags for this item atomically
|
if (albumItemId) {
|
||||||
await sql`
|
|
||||||
DELETE FROM tags_assign
|
|
||||||
WHERE item_id = ${itemid}
|
|
||||||
AND (tag_id IN (1, 2, ${nsfl_id}) OR tag_id IN (SELECT id FROM tags WHERE normalized IN ('sfw', 'nsfw', 'nsfl')))
|
|
||||||
`;
|
|
||||||
|
|
||||||
// Insert new rating tag
|
|
||||||
if (newRatingId > 0) {
|
|
||||||
await sql`
|
await sql`
|
||||||
INSERT INTO tags_assign (item_id, tag_id, user_id)
|
DELETE FROM album_items_tags_assign
|
||||||
VALUES (${itemid}, ${newRatingId}, ${req.session.id})
|
WHERE album_item_id = ${albumItemId} 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)
|
||||||
|
VALUES (${albumItemId}, ${newRatingId}, ${req.session.id})
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Remove ALL existing rating tags for this item atomically
|
||||||
|
await sql`
|
||||||
|
DELETE FROM tags_assign
|
||||||
|
WHERE item_id = ${itemid}
|
||||||
|
AND (tag_id IN (1, 2, ${nsfl_id}) OR tag_id IN (SELECT id FROM tags WHERE normalized IN ('sfw', 'nsfw', 'nsfl')))
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Insert new rating tag
|
||||||
|
if (newRatingId > 0) {
|
||||||
|
await sql`
|
||||||
|
INSERT INTO tags_assign (item_id, tag_id, user_id)
|
||||||
|
VALUES (${itemid}, ${newRatingId}, ${req.session.id})
|
||||||
|
`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure blurred thumbnail exists
|
// Ensure blurred thumbnail exists
|
||||||
@@ -1875,15 +1911,16 @@ export default router => {
|
|||||||
const newRating = newRatingId === 1 ? 'sfw' : (newRatingId === 2 ? 'nsfw' : 'nsfl');
|
const newRating = newRatingId === 1 ? 'sfw' : (newRatingId === 2 ? 'nsfw' : 'nsfl');
|
||||||
const ratingLabels = { 1: 'sfw', 2: 'nsfw', [nsfl_id]: 'nsfl' };
|
const ratingLabels = { 1: 'sfw', 2: 'nsfw', [nsfl_id]: 'nsfl' };
|
||||||
const oldRating = currentRatingId ? ratingLabels[currentRatingId] : 'none';
|
const oldRating = currentRatingId ? ratingLabels[currentRatingId] : 'none';
|
||||||
audit.log(req.session.id, 'update_rating', 'item', itemid, { old: oldRating, new: newRating }).catch(() => {});
|
audit.log(req.session.id, 'update_rating', 'item', itemid, { old: oldRating, new: newRating, subf0ck_id: subf0ck }).catch(() => {});
|
||||||
|
|
||||||
// Notify that tags have changed
|
// Notify that tags have changed
|
||||||
const freshTags = await lib.getTags(itemid);
|
const freshTags = await lib.getTags(itemid, req.session, subf0ck);
|
||||||
db.notify('tags', JSON.stringify({ item_id: itemid, fresh: true, tags: freshTags })).catch(() => {});
|
db.notify('tags', JSON.stringify({ item_id: itemid, subf0ck_id: subf0ck, fresh: true, tags: freshTags })).catch(() => {});
|
||||||
|
|
||||||
return res.json({
|
return res.json({
|
||||||
success: true,
|
success: true,
|
||||||
itemid: itemid,
|
itemid: itemid,
|
||||||
|
subf0ck_id: subf0ck,
|
||||||
rating: newRating,
|
rating: newRating,
|
||||||
tags: freshTags
|
tags: freshTags
|
||||||
});
|
});
|
||||||
|
|||||||
+173
-62
@@ -272,9 +272,10 @@ export default router => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const subf0ck = req.url?.qs?.subf0ck_id || req.url?.qs?.subf0ck || null;
|
||||||
return res.json({
|
return res.json({
|
||||||
success: true,
|
success: true,
|
||||||
tags: await lib.getTags(+req.params.postid, req.session)
|
tags: await lib.getTags(+req.params.postid, req.session, subf0ck)
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -309,6 +310,19 @@ export default router => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const subf0ck = req.post?.subf0ck_id || req.body?.subf0ck_id || req.post?.subf0ck || req.body?.subf0ck || null;
|
||||||
|
let albumItemId = null;
|
||||||
|
if (subf0ck) {
|
||||||
|
if (Number.isInteger(+subf0ck) && +subf0ck > 0) {
|
||||||
|
const row = await db`SELECT id FROM album_items WHERE (id = ${+subf0ck} OR (item_id = ${postid} AND order_index = ${+subf0ck})) AND item_id = ${postid} LIMIT 1`;
|
||||||
|
albumItemId = row?.[0]?.id || null;
|
||||||
|
}
|
||||||
|
if (!albumItemId) {
|
||||||
|
const row = await db`SELECT id FROM album_items WHERE slug = ${String(subf0ck)} AND item_id = ${postid} LIMIT 1`;
|
||||||
|
albumItemId = row?.[0]?.id || null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let tagid = (await db`
|
let tagid = (await db`
|
||||||
select id
|
select id
|
||||||
@@ -326,37 +340,51 @@ export default router => {
|
|||||||
`)[0].id;
|
`)[0].id;
|
||||||
}
|
}
|
||||||
|
|
||||||
await db`
|
if (albumItemId) {
|
||||||
insert into "tags_assign" ${db({
|
await db`
|
||||||
tag_id: +tagid,
|
INSERT INTO "album_items_tags_assign" (album_item_id, tag_id, user_id)
|
||||||
item_id: +postid,
|
VALUES (${+albumItemId}, ${+tagid}, ${+req.session.id})
|
||||||
user_id: +req.session.id
|
ON CONFLICT DO NOTHING
|
||||||
})
|
`;
|
||||||
}
|
await db`
|
||||||
`;
|
INSERT INTO "tags_assign" (item_id, tag_id, user_id)
|
||||||
|
VALUES (${+postid}, ${+tagid}, ${+req.session.id})
|
||||||
|
ON CONFLICT DO NOTHING
|
||||||
|
`;
|
||||||
|
} else {
|
||||||
|
await db`
|
||||||
|
insert into "tags_assign" ${db({
|
||||||
|
tag_id: +tagid,
|
||||||
|
item_id: +postid,
|
||||||
|
user_id: +req.session.id
|
||||||
|
})
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const isDuplicate = err.code === '23505' || err.constraint?.includes('tags_assign');
|
const isDuplicate = err.code === '23505' || err.constraint?.includes('tags_assign');
|
||||||
return res.json({
|
return res.json({
|
||||||
success: false,
|
success: false,
|
||||||
msg: isDuplicate ? 'Tag already exists' : 'Failed to add tag',
|
msg: isDuplicate ? 'Tag already exists' : 'Failed to add tag',
|
||||||
tags: await lib.getTags(postid)
|
tags: await lib.getTags(postid, req.session, subf0ck)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const freshTags = await lib.getTags(postid);
|
const freshTags = await lib.getTags(postid, req.session, subf0ck);
|
||||||
if (req.session?.is_anon) {
|
if (req.session?.is_anon) {
|
||||||
await logAnonActivity(req, {
|
await logAnonActivity(req, {
|
||||||
action: 'tag',
|
action: 'tag',
|
||||||
targetId: postid,
|
targetId: postid,
|
||||||
details: { tag: tagname }
|
details: { tag: tagname, subf0ck_id: subf0ck }
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
console.log(`[API] Notifying 'tags' for item ${postid} with ${freshTags.length} tags`);
|
console.log(`[API] Notifying 'tags' for item ${postid} (subf0ck: ${subf0ck || 'none'}) with ${freshTags.length} tags`);
|
||||||
await db.notify('tags', JSON.stringify({ item_id: postid, fresh: true, tags: freshTags }));
|
await db.notify('tags', JSON.stringify({ item_id: postid, subf0ck_id: subf0ck, fresh: true, tags: freshTags }));
|
||||||
|
|
||||||
return res.json({
|
return res.json({
|
||||||
success: true,
|
success: true,
|
||||||
postid: postid,
|
postid: postid,
|
||||||
|
subf0ck_id: subf0ck,
|
||||||
tag: tagname,
|
tag: tagname,
|
||||||
tags: freshTags
|
tags: freshTags
|
||||||
});
|
});
|
||||||
@@ -391,44 +419,90 @@ export default router => {
|
|||||||
let nextTagId;
|
let nextTagId;
|
||||||
let ratingTagId = 0;
|
let ratingTagId = 0;
|
||||||
|
|
||||||
|
const subf0ck = req.body?.subf0ck_id || req.post?.subf0ck_id || req.url?.qs?.subf0ck_id || null;
|
||||||
|
let albumItemId = null;
|
||||||
|
if (subf0ck) {
|
||||||
|
if (Number.isInteger(+subf0ck) && +subf0ck > 0) {
|
||||||
|
const row = await db`SELECT id FROM album_items WHERE (id = ${+subf0ck} OR (item_id = ${postid} AND order_index = ${+subf0ck})) AND item_id = ${postid} LIMIT 1`;
|
||||||
|
albumItemId = row?.[0]?.id || null;
|
||||||
|
}
|
||||||
|
if (!albumItemId) {
|
||||||
|
const row = await db`SELECT id FROM album_items WHERE slug = ${String(subf0ck)} AND item_id = ${postid} LIMIT 1`;
|
||||||
|
albumItemId = row?.[0]?.id || null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await db.begin(async sql => {
|
await db.begin(async sql => {
|
||||||
// Lock the item row exclusively
|
// Lock the item row exclusively
|
||||||
await sql`SELECT id FROM items WHERE id = ${postid} FOR UPDATE`;
|
await sql`SELECT id FROM items WHERE id = ${postid} FOR UPDATE`;
|
||||||
|
|
||||||
const existingRating = await sql`
|
if (albumItemId) {
|
||||||
SELECT tag_id FROM tags_assign
|
const existingRating = await sql`
|
||||||
WHERE item_id = ${postid}
|
SELECT tag_id FROM album_items_tags_assign
|
||||||
AND (tag_id IN (1, 2, ${nsflId}) OR tag_id IN (SELECT id FROM tags WHERE normalized IN ('sfw', 'nsfw', 'nsfl')))
|
WHERE album_item_id = ${albumItemId}
|
||||||
ORDER BY tag_id DESC
|
AND tag_id IN (1, 2, ${nsflId})
|
||||||
LIMIT 1
|
LIMIT 1
|
||||||
`;
|
|
||||||
|
|
||||||
ratingTagId = existingRating.length > 0 ? existingRating[0].tag_id : 0;
|
|
||||||
const reqRating = req.body?.rating || req.post?.rating || req.url?.qs?.rating;
|
|
||||||
if (reqRating === 'sfw') {
|
|
||||||
nextTagId = 1;
|
|
||||||
} else if (reqRating === 'nsfw') {
|
|
||||||
nextTagId = 2;
|
|
||||||
} else if (reqRating === 'nsfl') {
|
|
||||||
nextTagId = nsflId;
|
|
||||||
} else {
|
|
||||||
const cycleIdx = cycle.indexOf(ratingTagId); // -1 if untagged → (−1+1)%3 = 0 → SFW
|
|
||||||
nextTagId = cycle[(cycleIdx + 1) % cycle.length];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove ALL existing rating tags for this item atomically
|
|
||||||
await sql`
|
|
||||||
DELETE FROM tags_assign
|
|
||||||
WHERE item_id = ${postid}
|
|
||||||
AND (tag_id IN (1, 2, ${nsflId}) OR tag_id IN (SELECT id FROM tags WHERE normalized IN ('sfw', 'nsfw', 'nsfl')))
|
|
||||||
`;
|
|
||||||
|
|
||||||
if (nextTagId > 0) {
|
|
||||||
await sql`
|
|
||||||
INSERT INTO tags_assign (item_id, tag_id, user_id)
|
|
||||||
VALUES (${postid}, ${nextTagId}, ${+req.session.id})
|
|
||||||
`;
|
`;
|
||||||
|
ratingTagId = existingRating.length > 0 ? existingRating[0].tag_id : 0;
|
||||||
|
const reqRating = req.body?.rating || req.post?.rating || req.url?.qs?.rating;
|
||||||
|
if (reqRating === 'sfw') {
|
||||||
|
nextTagId = 1;
|
||||||
|
} else if (reqRating === 'nsfw') {
|
||||||
|
nextTagId = 2;
|
||||||
|
} else if (reqRating === 'nsfl') {
|
||||||
|
nextTagId = nsflId;
|
||||||
|
} else {
|
||||||
|
const cycleIdx = cycle.indexOf(ratingTagId);
|
||||||
|
nextTagId = cycle[(cycleIdx + 1) % cycle.length];
|
||||||
|
}
|
||||||
|
|
||||||
|
await sql`
|
||||||
|
DELETE FROM album_items_tags_assign
|
||||||
|
WHERE album_item_id = ${albumItemId}
|
||||||
|
AND tag_id IN (1, 2, ${nsflId})
|
||||||
|
`;
|
||||||
|
if (nextTagId > 0) {
|
||||||
|
await sql`
|
||||||
|
INSERT INTO album_items_tags_assign (album_item_id, tag_id, user_id)
|
||||||
|
VALUES (${albumItemId}, ${nextTagId}, ${+req.session.id})
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const existingRating = await sql`
|
||||||
|
SELECT tag_id FROM tags_assign
|
||||||
|
WHERE item_id = ${postid}
|
||||||
|
AND (tag_id IN (1, 2, ${nsflId}) OR tag_id IN (SELECT id FROM tags WHERE normalized IN ('sfw', 'nsfw', 'nsfl')))
|
||||||
|
ORDER BY tag_id DESC
|
||||||
|
LIMIT 1
|
||||||
|
`;
|
||||||
|
|
||||||
|
ratingTagId = existingRating.length > 0 ? existingRating[0].tag_id : 0;
|
||||||
|
const reqRating = req.body?.rating || req.post?.rating || req.url?.qs?.rating;
|
||||||
|
if (reqRating === 'sfw') {
|
||||||
|
nextTagId = 1;
|
||||||
|
} else if (reqRating === 'nsfw') {
|
||||||
|
nextTagId = 2;
|
||||||
|
} else if (reqRating === 'nsfl') {
|
||||||
|
nextTagId = nsflId;
|
||||||
|
} else {
|
||||||
|
const cycleIdx = cycle.indexOf(ratingTagId); // -1 if untagged → (−1+1)%3 = 0 → SFW
|
||||||
|
nextTagId = cycle[(cycleIdx + 1) % cycle.length];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove ALL existing rating tags for this item atomically
|
||||||
|
await sql`
|
||||||
|
DELETE FROM tags_assign
|
||||||
|
WHERE item_id = ${postid}
|
||||||
|
AND (tag_id IN (1, 2, ${nsflId}) OR tag_id IN (SELECT id FROM tags WHERE normalized IN ('sfw', 'nsfw', 'nsfl')))
|
||||||
|
`;
|
||||||
|
|
||||||
|
if (nextTagId > 0) {
|
||||||
|
await sql`
|
||||||
|
INSERT INTO tags_assign (item_id, tag_id, user_id)
|
||||||
|
VALUES (${postid}, ${nextTagId}, ${+req.session.id})
|
||||||
|
`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Automatically generate/verify blurred thumbnail on cycle
|
// Automatically generate/verify blurred thumbnail on cycle
|
||||||
@@ -534,46 +608,83 @@ export default router => {
|
|||||||
const postid = +req.params.postid;
|
const postid = +req.params.postid;
|
||||||
const tagname = decodeURIComponent(req.params.tagname);
|
const tagname = decodeURIComponent(req.params.tagname);
|
||||||
|
|
||||||
const tags = await lib.getTags(postid);
|
const subf0ck = req.post?.subf0ck_id || req.body?.subf0ck_id || req.url?.qs?.subf0ck_id || req.url?.qs?.subf0ck || null;
|
||||||
|
let albumItemId = null;
|
||||||
|
if (subf0ck) {
|
||||||
|
if (Number.isInteger(+subf0ck) && +subf0ck > 0) {
|
||||||
|
const row = await db`SELECT id FROM album_items WHERE (id = ${+subf0ck} OR (item_id = ${postid} AND order_index = ${+subf0ck})) AND item_id = ${postid} LIMIT 1`;
|
||||||
|
albumItemId = row?.[0]?.id || null;
|
||||||
|
}
|
||||||
|
if (!albumItemId) {
|
||||||
|
const row = await db`SELECT id FROM album_items WHERE slug = ${String(subf0ck)} AND item_id = ${postid} LIMIT 1`;
|
||||||
|
albumItemId = row?.[0]?.id || null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const tagid = tags.filter(t => t.tag === tagname)[0]?.id ?? null;
|
const tags = await lib.getTags(postid, req.session, subf0ck);
|
||||||
|
|
||||||
|
const tagid = tags.filter(t => t.tag === tagname || t.normalized === tagname.toLowerCase())[0]?.id ?? null;
|
||||||
|
|
||||||
if (!tagid) {
|
if (!tagid) {
|
||||||
return res.json({
|
return res.json({
|
||||||
success: false,
|
success: false,
|
||||||
msg: 'tag is not assigned',
|
msg: 'tag is not assigned',
|
||||||
tags: await lib.getTags(postid)
|
tags: await lib.getTags(postid, req.session, subf0ck)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
let q = await db`
|
let reply = false;
|
||||||
delete from "tags_assign"
|
if (albumItemId) {
|
||||||
where tag_id = ${+tagid}
|
const q = await db`
|
||||||
and item_id = ${+postid}
|
DELETE FROM album_items_tags_assign
|
||||||
`;
|
WHERE album_item_id = ${+albumItemId}
|
||||||
const reply = !!q;
|
AND tag_id = ${+tagid}
|
||||||
|
`;
|
||||||
|
reply = !!q;
|
||||||
|
|
||||||
|
const otherUses = await db`
|
||||||
|
SELECT 1 FROM album_items_tags_assign aita
|
||||||
|
JOIN album_items ai ON ai.id = aita.album_item_id
|
||||||
|
WHERE ai.item_id = ${postid} AND aita.tag_id = ${+tagid}
|
||||||
|
LIMIT 1
|
||||||
|
`;
|
||||||
|
if (otherUses.length === 0) {
|
||||||
|
await db`
|
||||||
|
DELETE FROM tags_assign
|
||||||
|
WHERE item_id = ${postid} AND tag_id = ${+tagid}
|
||||||
|
`.catch(() => {});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let q = await db`
|
||||||
|
delete from "tags_assign"
|
||||||
|
where tag_id = ${+tagid}
|
||||||
|
and item_id = ${+postid}
|
||||||
|
`;
|
||||||
|
reply = !!q;
|
||||||
|
}
|
||||||
|
|
||||||
if (reply) {
|
if (reply) {
|
||||||
const reason = req.post.reason || req.url.qs.reason || 'No reason provided';
|
const reason = req.post?.reason || req.url?.qs?.reason || 'No reason provided';
|
||||||
await audit.log(req.session.id, 'delete_tag', 'item', postid, { tag: tagname, reason });
|
await audit.log(req.session.id, 'delete_tag', 'item', postid, { tag: tagname, subf0ck_id: subf0ck, reason });
|
||||||
if (req.session?.is_anon) {
|
if (req.session?.is_anon) {
|
||||||
await logAnonActivity(req, {
|
await logAnonActivity(req, {
|
||||||
action: 'delete_tag',
|
action: 'delete_tag',
|
||||||
targetId: postid,
|
targetId: postid,
|
||||||
details: { tag: tagname, reason }
|
details: { tag: tagname, subf0ck_id: subf0ck, reason }
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const freshTags = await lib.getTags(postid);
|
const freshTags = await lib.getTags(postid, req.session, subf0ck);
|
||||||
console.log(`[API] Notifying 'tags' (delete) for item ${postid}`);
|
console.log(`[API] Notifying 'tags' (delete) for item ${postid} (subf0ck: ${subf0ck || 'none'})`);
|
||||||
await db.notify('tags', JSON.stringify({ item_id: postid, fresh: true, tags: freshTags }));
|
await db.notify('tags', JSON.stringify({ item_id: postid, subf0ck_id: subf0ck, fresh: true, tags: freshTags }));
|
||||||
|
|
||||||
return res.json({
|
return res.json({
|
||||||
success: reply,
|
success: reply,
|
||||||
tagid,
|
tagid,
|
||||||
|
subf0ck_id: subf0ck,
|
||||||
tags: freshTags
|
tags: freshTags
|
||||||
})
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -285,7 +285,8 @@ export default (router, tpl) => {
|
|||||||
random: req.cookies.random_mode === '1',
|
random: req.cookies.random_mode === '1',
|
||||||
minXdScore: req.params.itemid ? 0 : (req.url.qs?.min_xd !== undefined ? +req.url.qs.min_xd : (req.session?.min_xd_score || 0)),
|
minXdScore: req.params.itemid ? 0 : (req.url.qs?.min_xd !== undefined ? +req.url.qs.min_xd : (req.session?.min_xd_score || 0)),
|
||||||
lang: req.lang,
|
lang: req.lang,
|
||||||
tagger: req.url.qs?.tagger || null
|
tagger: req.url.qs?.tagger || null,
|
||||||
|
subf0ck: req.query?.subf0ck || req.url.qs?.subf0ck || null
|
||||||
});
|
});
|
||||||
console.log(`[DEBUG] Checking strict mode: query=${req.query?.strict}, session=${req.session?.strict_mode}, effective=${!!(req.query?.strict || req.url.qs?.strict || req.session?.strict_mode)}`);
|
console.log(`[DEBUG] Checking strict mode: query=${req.query?.strict}, session=${req.session?.strict_mode}, effective=${!!(req.query?.strict || req.url.qs?.strict || req.session?.strict_mode)}`);
|
||||||
console.log(`[${new Date().toISOString()}] [ROUTE] Data fetch complete in ${Date.now() - tRouteStart}ms`);
|
console.log(`[${new Date().toISOString()}] [ROUTE] Data fetch complete in ${Date.now() - tRouteStart}ms`);
|
||||||
|
|||||||
@@ -274,6 +274,17 @@ export default (router, tpl) => {
|
|||||||
if (f0ck[0].mime.startsWith('audio')) {
|
if (f0ck[0].mime.startsWith('audio')) {
|
||||||
await moveSafe(p.ca, caDst);
|
await moveSafe(p.ca, caDst);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (f0ck[0].is_album) {
|
||||||
|
try {
|
||||||
|
const subItems = await db`SELECT dest FROM album_items WHERE item_id = ${id}`;
|
||||||
|
for (const sub of subItems) {
|
||||||
|
const subBase = sub.dest.replace(/\.[^.]+$/, '');
|
||||||
|
await moveSafe(path.join(cfg.paths.pending, 'b', sub.dest), path.join(cfg.paths.b, sub.dest));
|
||||||
|
await moveSafe(path.join(cfg.paths.pending, 't', `${subBase}.webp`), path.join(cfg.paths.t, `${subBase}.webp`));
|
||||||
|
}
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
} catch (e) { }
|
} catch (e) { }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import cfg from "../config.mjs";
|
import cfg from "../config.mjs";
|
||||||
import fs from "fs/promises";
|
import fs from "fs/promises";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
|
import db from "../sql.mjs";
|
||||||
|
import queue from "../queue.mjs";
|
||||||
|
|
||||||
export default (router, tpl) => {
|
export default (router, tpl) => {
|
||||||
router.static({
|
router.static({
|
||||||
@@ -61,6 +63,52 @@ export default (router, tpl) => {
|
|||||||
return res.end(content);
|
return res.end(content);
|
||||||
}
|
}
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
|
|
||||||
|
// Dynamic generation or fallback for subf0cks / albums
|
||||||
|
const base = file.replace(/\.[^.]+$/, '').replace(/_blur$/, '');
|
||||||
|
try {
|
||||||
|
const subRow = await db`SELECT id, item_id, dest, mime FROM album_items WHERE dest LIKE ${base + '.%'} LIMIT 1`;
|
||||||
|
if (subRow.length > 0) {
|
||||||
|
const sub = subRow[0];
|
||||||
|
const srcFile = path.join(cfg.paths.b, sub.dest);
|
||||||
|
const thumbDest = path.join(cfg.paths.t, `${base}.webp`);
|
||||||
|
const srcStat = await fs.stat(srcFile).catch(() => null);
|
||||||
|
if (srcStat && srcStat.size > 0) {
|
||||||
|
if (sub.mime.startsWith('image/')) {
|
||||||
|
await queue.spawn('magick', [srcFile + '[0]', '-resize', '256x256^', '-gravity', 'center', '-crop', '256x256+0+0', '+repage', thumbDest]);
|
||||||
|
} else if (sub.mime.startsWith('video/')) {
|
||||||
|
await queue.spawn('ffmpegthumbnailer', ['-i', srcFile, '-s', '256', '-o', thumbDest]).catch(async () => {
|
||||||
|
await queue.spawn('ffmpeg', ['-y', '-ss', '00:00:01', '-i', srcFile, '-vframes', '1', '-vf', 'scale=256:256:force_original_aspect_ratio=increase,crop=256:256', thumbDest]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const genStat = await fs.stat(thumbDest).catch(() => null);
|
||||||
|
if (genStat && genStat.size > 0) {
|
||||||
|
const content = await fs.readFile(thumbDest);
|
||||||
|
res.writeHead(200, {
|
||||||
|
'Content-Length': genStat.size,
|
||||||
|
'Content-Type': 'image/webp',
|
||||||
|
'Cache-Control': 'public, max-age=3600'
|
||||||
|
});
|
||||||
|
return res.end(content);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Fallback to parent album thumbnail if subf0ck media file is missing
|
||||||
|
const parentThumb = path.join(cfg.paths.t, `${sub.item_id}.webp`);
|
||||||
|
const pStat = await fs.stat(parentThumb).catch(() => null);
|
||||||
|
if (pStat && pStat.size > 0) {
|
||||||
|
const content = await fs.readFile(parentThumb);
|
||||||
|
res.writeHead(200, {
|
||||||
|
'Content-Length': pStat.size,
|
||||||
|
'Content-Type': 'image/webp',
|
||||||
|
'Cache-Control': 'public, max-age=3600'
|
||||||
|
});
|
||||||
|
return res.end(content);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('[STATIC /t/] Dynamic subf0ck thumbnail generation error:', e.message);
|
||||||
|
}
|
||||||
|
|
||||||
res.writeHead(404, { 'Content-Type': 'text/plain' });
|
res.writeHead(404, { 'Content-Type': 'text/plain' });
|
||||||
return res.end('404 - file not found.');
|
return res.end('404 - file not found.');
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -545,6 +545,17 @@ process.on('uncaughtException', err => {
|
|||||||
await runMigration(db`CREATE INDEX IF NOT EXISTS idx_album_items_item_id ON album_items(item_id, order_index ASC)`);
|
await runMigration(db`CREATE INDEX IF NOT EXISTS idx_album_items_item_id ON album_items(item_id, order_index ASC)`);
|
||||||
await runMigration(db`ALTER TABLE album_items ADD COLUMN IF NOT EXISTS slug character varying(60) DEFAULT NULL`);
|
await runMigration(db`ALTER TABLE album_items ADD COLUMN IF NOT EXISTS slug character varying(60) DEFAULT NULL`);
|
||||||
await runMigration(db`CREATE INDEX IF NOT EXISTS idx_album_items_slug ON album_items(slug)`);
|
await runMigration(db`CREATE INDEX IF NOT EXISTS idx_album_items_slug ON album_items(slug)`);
|
||||||
|
await runMigration(db`
|
||||||
|
CREATE TABLE IF NOT EXISTS album_items_tags_assign (
|
||||||
|
album_item_id INTEGER NOT NULL REFERENCES album_items(id) ON DELETE CASCADE,
|
||||||
|
tag_id INTEGER NOT NULL REFERENCES tags(id) ON DELETE CASCADE,
|
||||||
|
user_id INTEGER REFERENCES "user"(id) ON DELETE SET DEFAULT,
|
||||||
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||||
|
PRIMARY KEY (album_item_id, tag_id)
|
||||||
|
)
|
||||||
|
`);
|
||||||
|
await runMigration(db`CREATE INDEX IF NOT EXISTS idx_album_items_tags_assign_tag_id ON album_items_tags_assign(tag_id)`);
|
||||||
|
await runMigration(db`CREATE INDEX IF NOT EXISTS idx_album_items_tags_assign_album_item_id ON album_items_tags_assign(album_item_id)`);
|
||||||
|
|
||||||
// Initial halls cache (only if halls are enabled)
|
// Initial halls cache (only if halls are enabled)
|
||||||
if (cfg.websrv.halls_enabled !== false) {
|
if (cfg.websrv.halls_enabled !== false) {
|
||||||
|
|||||||
+51
-1
@@ -881,7 +881,7 @@ export const handleUpload = async (req, res, self) => {
|
|||||||
const subSize = subFile.data.length;
|
const subSize = subFile.data.length;
|
||||||
const subItemSlug = lib.generateSlug(11);
|
const subItemSlug = lib.generateSlug(11);
|
||||||
|
|
||||||
await db`
|
const insertedSub = await db`
|
||||||
INSERT INTO album_items ${db({
|
INSERT INTO album_items ${db({
|
||||||
item_id: itemid,
|
item_id: itemid,
|
||||||
dest: subFilename,
|
dest: subFilename,
|
||||||
@@ -894,7 +894,57 @@ export const handleUpload = async (req, res, self) => {
|
|||||||
order_index: i,
|
order_index: i,
|
||||||
slug: subItemSlug
|
slug: subItemSlug
|
||||||
}, 'item_id', 'dest', 'mime', 'size', 'checksum', 'phash', 'width', 'height', 'order_index', 'slug')}
|
}, 'item_id', 'dest', 'mime', 'size', 'checksum', 'phash', 'width', 'height', 'order_index', 'slug')}
|
||||||
|
RETURNING id
|
||||||
`;
|
`;
|
||||||
|
const subItemId = insertedSub?.[0]?.id;
|
||||||
|
|
||||||
|
// Process any per-subf0ck tags passed in parts
|
||||||
|
const rawSubTags = parts[`subf0ck_tags_${i}`] || parts[`subtags_${i}`];
|
||||||
|
if (subItemId && rawSubTags) {
|
||||||
|
const parsedSubTags = String(rawSubTags)
|
||||||
|
.split(/[,;\n]+/)
|
||||||
|
.map(t => t.trim())
|
||||||
|
.filter(t => t.length > 0 && t.length <= 85 && !['sfw', 'nsfw', 'nsfl'].includes(t.toLowerCase()));
|
||||||
|
|
||||||
|
for (const sTag of parsedSubTags) {
|
||||||
|
try {
|
||||||
|
let tagRow = await db`SELECT id FROM tags WHERE normalized = slugify(${sTag}) LIMIT 1`;
|
||||||
|
if (tagRow.length === 0) {
|
||||||
|
await db`INSERT INTO tags ${db({ tag: sTag }, 'tag')}`;
|
||||||
|
tagRow = await db`SELECT id FROM tags WHERE normalized = slugify(${sTag}) LIMIT 1`;
|
||||||
|
}
|
||||||
|
if (tagRow.length > 0) {
|
||||||
|
const sTagId = tagRow[0].id;
|
||||||
|
await db`
|
||||||
|
INSERT INTO album_items_tags_assign (album_item_id, tag_id, user_id)
|
||||||
|
VALUES (${subItemId}, ${sTagId}, ${req.session.id})
|
||||||
|
ON CONFLICT DO NOTHING
|
||||||
|
`;
|
||||||
|
await db`
|
||||||
|
INSERT INTO tags_assign (item_id, tag_id, user_id)
|
||||||
|
VALUES (${itemid}, ${sTagId}, ${req.session.id})
|
||||||
|
ON CONFLICT DO NOTHING
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
} catch (stErr) {
|
||||||
|
console.warn(`[UPLOAD] Error assigning tag "${sTag}" to subf0ck ${subItemId}:`, stErr.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (subItemId && i === 0 && tags && tags.length > 0) {
|
||||||
|
// Backfill initial post tags to cover subf0ck
|
||||||
|
for (const sTag of tags) {
|
||||||
|
try {
|
||||||
|
let tagRow = await db`SELECT id FROM tags WHERE normalized = slugify(${sTag}) LIMIT 1`;
|
||||||
|
if (tagRow.length > 0) {
|
||||||
|
await db`
|
||||||
|
INSERT INTO album_items_tags_assign (album_item_id, tag_id, user_id)
|
||||||
|
VALUES (${subItemId}, ${tagRow[0].id}, ${req.session.id})
|
||||||
|
ON CONFLICT DO NOTHING
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Generate thumbnail for album item filmstrip
|
// Generate thumbnail for album item filmstrip
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -3,8 +3,8 @@
|
|||||||
@include(snippets/page-title)
|
@include(snippets/page-title)
|
||||||
<div class="posts" data-current-page="{{ pagination.current }}" data-has-more="{{ pagination.next ? 'true' : 'false' }}">
|
<div class="posts" data-current-page="{{ pagination.current }}" data-has-more="{{ pagination.next ? 'true' : 'false' }}">
|
||||||
@each(items as item)
|
@each(items as item)
|
||||||
<a href="{{ link.main }}{{ (enable_item_slugs && item.slug) ? item.slug : item.id }}" class="{{ item.is_pinned ? 'anim-boxshadow ' : '' }}thumb lazy-thumb {{ item.has_notification ? 'has-notif' : '' }} {{ item.is_pinned ? 'is-pinned' : '' }} {{ item.is_onara_active ? 'onara-active' : '' }}" data-item-id="{{ item.id }}" data-file="{{ item.dest }}" data-mime="{{ item.mime }}" data-user="@if(!is_anonymized){!! item.display_name || item.username !!}@else anonymous@endif" data-ext="{{ item.mime.split('/')[1].replace('youtube', 'yt').replace('x-shockwave-flash', 'flash').replace('vnd.adobe.flash.movie', 'flash').toUpperCase() }}" data-mode="{{ item.tag_id == nsfl_tag_id ? 'nsfl' : (item.tag_id == 2 ? 'nsfw' : (item.tag_id == 1 ? 'sfw' : 'null')) }}" data-bg="/t/{{ item.id }}.webp" data-size="{{ enable_dynamic_thumbs ? (item.thumb_size || 1) : 1 }}">
|
<a href="{{ link.main }}{{ (enable_item_slugs && item.slug) ? item.slug : item.id }}{{ item.target_subf0ck_slug ? ('#' + item.target_subf0ck_slug) : '' }}" class="{{ item.is_pinned ? 'anim-boxshadow ' : '' }}thumb lazy-thumb {{ item.has_notification ? 'has-notif' : '' }} {{ item.is_pinned ? 'is-pinned' : '' }} {{ item.is_onara_active ? 'onara-active' : '' }}" data-item-id="{{ item.id }}" data-file="{{ item.matching_sub_dest || item.dest }}" data-mime="{{ item.matching_sub_mime || item.mime }}" data-user="@if(!is_anonymized){!! item.display_name || item.username !!}@else anonymous@endif" data-ext="{{ (item.matching_sub_mime || item.mime).split('/')[1].replace('youtube', 'yt').replace('x-shockwave-flash', 'flash').replace('vnd.adobe.flash.movie', 'flash').replace('x-zip-compressed', 'zip').replace('x-rar-compressed', 'rar').replace('vnd.rar', 'rar').replace('x-7z-compressed', '7z').replace('x-tar', 'tar').replace('x-bzip2', 'bz2').replace('x-xz', 'xz').toUpperCase() }}" data-mode="{{ item.tag_id == nsfl_tag_id ? 'nsfl' : (item.tag_id == 2 ? 'nsfw' : (item.tag_id == 1 ? 'sfw' : 'null')) }}" data-bg="{{ item.thumb || ('/t/' + item.id + '.webp') }}" data-target-subf0ck="{{ item.target_subf0ck_slug || '' }}" data-size="{{ enable_dynamic_thumbs ? (item.thumb_size || 1) : 1 }}">
|
||||||
@if(item.is_audio && !item.has_coverart)
|
@if(item.is_audio && !item.has_coverart && !item.thumb)
|
||||||
<div class="sidebar-media-placeholder audio">
|
<div class="sidebar-media-placeholder audio">
|
||||||
<i class="fa-solid fa-music"></i>
|
<i class="fa-solid fa-music"></i>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
{{-- LEGACY LAYOUT — 2-column (main + fixed right sidebar) --}}
|
{{-- LEGACY LAYOUT — 2-column (main + fixed right sidebar) --}}
|
||||||
{{-- Used by: members with use_new_layout = false --}}
|
{{-- Used by: members with use_new_layout = false --}}
|
||||||
<div class="item-layout-container" data-item-id="{{ item.id }}">
|
<div class="item-layout-container" data-item-id="{{ item.id }}" data-requested-subf0ck="{{ item.requested_subf0ck_slug || '' }}">
|
||||||
|
|
||||||
{{-- MAIN CONTENT: media + navigation + metadata + comments --}}
|
{{-- MAIN CONTENT: media + navigation + metadata + comments --}}
|
||||||
<div class="item-main-content">
|
<div class="item-main-content">
|
||||||
|
|
||||||
<div class="_204863">
|
<div class="_204863">
|
||||||
<div class="location">{{ link.mainDisplay || link.main }}{{ (enable_item_slugs && item.slug) ? item.slug : item.id }}{{ link.suffix }}</div>
|
<div class="location">{{ link.mainDisplay || link.main }}{{ (enable_item_slugs && item.slug) ? item.slug : item.id }}{{ link.suffix }}{{ item.requested_subf0ck_slug ? ('#' + item.requested_subf0ck_slug) : '' }}</div>
|
||||||
<div class="gapLeft"></div>
|
<div class="gapLeft"></div>
|
||||||
</div>
|
</div>
|
||||||
@if(enable_item_title)
|
@if(enable_item_title)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{{-- MODERN LAYOUT — 3-column grid --}}
|
{{-- MODERN LAYOUT — 3-column grid --}}
|
||||||
{{-- Used by: guests (no session) + members with use_new_layout = true --}}
|
{{-- Used by: guests (no session) + members with use_new_layout = true --}}
|
||||||
<div class="item-layout-container" data-item-id="{{ item.id }}">
|
<div class="item-layout-container" data-item-id="{{ item.id }}" data-requested-subf0ck="{{ item.requested_subf0ck_slug || '' }}">
|
||||||
|
|
||||||
{{-- LEFT SIDEBAR: comments + tags --}}
|
{{-- LEFT SIDEBAR: comments + tags --}}
|
||||||
<div class="item-sidebar-left">
|
<div class="item-sidebar-left">
|
||||||
@@ -70,7 +70,7 @@
|
|||||||
<div class="item-main-content">
|
<div class="item-main-content">
|
||||||
|
|
||||||
<div class="_204863">
|
<div class="_204863">
|
||||||
<div class="location">{{ link.mainDisplay || link.main }}{{ (enable_item_slugs && item.slug) ? item.slug : item.id }}{{ link.suffix }}</div>
|
<div class="location">{{ link.mainDisplay || link.main }}{{ (enable_item_slugs && item.slug) ? item.slug : item.id }}{{ link.suffix }}{{ item.requested_subf0ck_slug ? ('#' + item.requested_subf0ck_slug) : '' }}</div>
|
||||||
<div class="gapLeft"></div>
|
<div class="gapLeft"></div>
|
||||||
</div>
|
</div>
|
||||||
@if(enable_item_title)
|
@if(enable_item_title)
|
||||||
|
|||||||
@@ -686,6 +686,7 @@
|
|||||||
album_add_more: "{{ t('album.add_more') || 'Add more pictures' }}",
|
album_add_more: "{{ t('album.add_more') || 'Add more pictures' }}",
|
||||||
album_select_pictures: "{{ t('album.select_pictures') || 'Select pictures for album' }}",
|
album_select_pictures: "{{ t('album.select_pictures') || 'Select pictures for album' }}",
|
||||||
album_min_pictures: "{{ t('album.min_pictures') || 'Add at least 2 pictures for an album' }}",
|
album_min_pictures: "{{ t('album.min_pictures') || 'Add at least 2 pictures for an album' }}",
|
||||||
|
album_subf0ck_tags_placeholder: "{{ t('album.subf0ck_tags_placeholder') || 'Tags for this subf0ck (optional)' }}",
|
||||||
// favorites
|
// favorites
|
||||||
no_favs: "{{ t('profile.no_favs') || 'no favorites' }}",
|
no_favs: "{{ t('profile.no_favs') || 'no favorites' }}",
|
||||||
favs_label: "{{ t('profile.favs_label') || 'Favorites' }}",
|
favs_label: "{{ t('profile.favs_label') || 'Favorites' }}",
|
||||||
|
|||||||
@@ -8,8 +8,12 @@
|
|||||||
<div id="f0ck-album-video-wrapper" class="album-video-wrapper embed-responsive embed-responsive-16by9" style="display: none; position: absolute; inset: 0; width: 100%; height: 100%;">
|
<div id="f0ck-album-video-wrapper" class="album-video-wrapper embed-responsive embed-responsive-16by9" style="display: none; position: absolute; inset: 0; width: 100%; height: 100%;">
|
||||||
<video id="f0ck-album-video" class="embed-responsive-item" preload="auto" loop playsinline></video>
|
<video id="f0ck-album-video" class="embed-responsive-item" preload="auto" loop playsinline></video>
|
||||||
</div>
|
</div>
|
||||||
<div id="f0ck-album-audio-wrapper" class="album-audio-wrapper embed-responsive embed-responsive-16by9" style="display: none; position: absolute; inset: 0; width: 100%; height: 100%; background: url('/s/img/audio.webp') no-repeat center / contain black;">
|
<div id="f0ck-album-audio-wrapper" class="album-audio-wrapper embed-responsive embed-responsive-16by9" style="display: none; position: absolute; inset: 0; width: 100%; height: 100%;">
|
||||||
<audio id="f0ck-album-audio" class="embed-responsive-item" preload="auto" loop poster="/s/img/audio.webp"></audio>
|
<div class="sidebar-media-placeholder audio">
|
||||||
|
<div class="audio-cover-circle"></div>
|
||||||
|
<i class="fa-solid fa-music"></i>
|
||||||
|
</div>
|
||||||
|
<audio id="f0ck-album-audio" class="embed-responsive-item" preload="auto" loop></audio>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- Album Gallery Controls Inside View -->
|
<!-- Album Gallery Controls Inside View -->
|
||||||
@@ -29,7 +33,13 @@
|
|||||||
<div class="album-thumbnails-strip">
|
<div class="album-thumbnails-strip">
|
||||||
@each(item.album as img)
|
@each(item.album as img)
|
||||||
<button type="button" class="album-thumb-item {{ img.is_first ? 'active' : '' }}" data-index="{{ img.order_index }}" data-subf0ck-id="{{ img.slug || img.id }}" data-subf0ck-slug="{{ img.slug || img.id }}" data-src="{{ img.src }}" data-mime="{{ img.mime }}" title="Subf0ck {{ img.slug || img.id }} ({{ img.display_index }}/{{ item.album.length }})">
|
<button type="button" class="album-thumb-item {{ img.is_first ? 'active' : '' }}" data-index="{{ img.order_index }}" data-subf0ck-id="{{ img.slug || img.id }}" data-subf0ck-slug="{{ img.slug || img.id }}" data-src="{{ img.src }}" data-mime="{{ img.mime }}" title="Subf0ck {{ img.slug || img.id }} ({{ img.display_index }}/{{ item.album.length }})">
|
||||||
|
@if(img.is_audio && !img.has_coverart)
|
||||||
|
<div class="sidebar-media-placeholder audio">
|
||||||
|
<i class="fa-solid fa-music"></i>
|
||||||
|
</div>
|
||||||
|
@else
|
||||||
<img src="{{ img.thumb || img.src }}" loading="lazy" alt="Subf0ck {{ img.slug || img.id }}" />
|
<img src="{{ img.thumb || img.src }}" loading="lazy" alt="Subf0ck {{ img.slug || img.id }}" />
|
||||||
|
@endif
|
||||||
@if(img.is_video)
|
@if(img.is_video)
|
||||||
<span class="album-thumb-mime-badge"><i class="fa-solid fa-play"></i></span>
|
<span class="album-thumb-mime-badge"><i class="fa-solid fa-play"></i></span>
|
||||||
@elseif(img.is_audio)
|
@elseif(img.is_audio)
|
||||||
@@ -49,9 +59,13 @@
|
|||||||
<video id="my-video" class="embed-responsive-item" width="640" height="360" src="{{ item.dest }}" preload="auto" data-size="{{ item.size }}" loop playsinline></video>
|
<video id="my-video" class="embed-responsive-item" width="640" height="360" src="{{ item.dest }}" preload="auto" data-size="{{ item.size }}" loop playsinline></video>
|
||||||
</div>
|
</div>
|
||||||
@elseif(item.mime.startsWith("audio"))
|
@elseif(item.mime.startsWith("audio"))
|
||||||
<div class="embed-responsive embed-responsive-16by9" style="background: url('@if(item.coverart){{ item.coverart }}@else/s/img/200.gif@endif') no-repeat center / contain black;">
|
<div class="embed-responsive embed-responsive-16by9">
|
||||||
<audio id="my-video" class="embed-responsive-item" preload="auto" loop src="{{ item.dest }}" data-setup="{}" data-size="{{ item.size }}" poster="@if(item.coverart){{ item.coverart }}@else/s/img/200.gif@endif" type="{{ item.mime }}"></audio>
|
<div class="sidebar-media-placeholder audio @if(item.coverart)has-cover@endif">
|
||||||
<img id="f0ck-audio-cover" src="@if(item.coverart){{ item.coverart }}@else/s/img/200.gif@endif" style="display: none;">
|
<div class="audio-cover-circle" @if(item.coverart)style="background-image: url('{{ item.coverart }}');"@endif></div>
|
||||||
|
<i class="fa-solid fa-music"></i>
|
||||||
|
</div>
|
||||||
|
<audio id="my-video" class="embed-responsive-item" preload="auto" loop src="{{ item.dest }}" data-setup="{}" data-size="{{ item.size }}" @if(item.coverart)poster="{{ item.coverart }}"@endif type="{{ item.mime }}"></audio>
|
||||||
|
<img id="f0ck-audio-cover" @if(item.coverart)src="{{ item.coverart }}"@endif style="display: none;">
|
||||||
</div>
|
</div>
|
||||||
@elseif(item.mime.startsWith("image"))
|
@elseif(item.mime.startsWith("image"))
|
||||||
<div class="embed-responsive embed-responsive-16by9">
|
<div class="embed-responsive embed-responsive-16by9">
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
@each(items as item)
|
@each(items as item)
|
||||||
<a href="{{ link.main }}{{ (enable_item_slugs && item.slug) ? item.slug : item.id }}" class="{{ item.is_pinned ? 'anim-boxshadow ' : '' }}thumb lazy-thumb {{ item.has_notification ? 'has-notif' : '' }} {{ item.is_pinned ? 'is-pinned' : '' }}" data-item-id="{{ item.id }}" data-file="{{ item.dest }}" data-mime="{{ item.mime }}" data-user="@if(!is_anonymized){!! item.display_name || item.username !!}@else anonymous@endif" data-ext="{{ item.mime.split('/')[1].replace('youtube', 'yt').replace('x-shockwave-flash', 'flash').replace('vnd.adobe.flash.movie', 'flash').replace('x-zip-compressed', 'zip').replace('x-rar-compressed', 'rar').replace('vnd.rar', 'rar').replace('x-7z-compressed', '7z').replace('x-tar', 'tar').replace('x-bzip2', 'bz2').replace('x-xz', 'xz').toUpperCase() }}" data-mode="{{ item.tag_id == nsfl_tag_id ? 'nsfl' : (item.tag_id == 2 ? 'nsfw' : (item.tag_id == 1 ? 'sfw' : 'null')) }}" data-bg="/t/{{ item.id }}.webp" data-size="{{ enable_dynamic_thumbs ? (item.thumb_size || 1) : 1 }}">
|
<a href="{{ link.main }}{{ (enable_item_slugs && item.slug) ? item.slug : item.id }}{{ item.target_subf0ck_slug ? ('#' + item.target_subf0ck_slug) : '' }}" class="{{ item.is_pinned ? 'anim-boxshadow ' : '' }}thumb lazy-thumb {{ item.has_notification ? 'has-notif' : '' }} {{ item.is_pinned ? 'is-pinned' : '' }}" data-item-id="{{ item.id }}" data-file="{{ item.matching_sub_dest || item.dest }}" data-mime="{{ item.matching_sub_mime || item.mime }}" data-user="@if(!is_anonymized){!! item.display_name || item.username !!}@else anonymous@endif" data-ext="{{ (item.matching_sub_mime || item.mime).split('/')[1].replace('youtube', 'yt').replace('x-shockwave-flash', 'flash').replace('vnd.adobe.flash.movie', 'flash').replace('x-zip-compressed', 'zip').replace('x-rar-compressed', 'rar').replace('vnd.rar', 'rar').replace('x-7z-compressed', '7z').replace('x-tar', 'tar').replace('x-bzip2', 'bz2').replace('x-xz', 'xz').toUpperCase() }}" data-mode="{{ item.tag_id == nsfl_tag_id ? 'nsfl' : (item.tag_id == 2 ? 'nsfw' : (item.tag_id == 1 ? 'sfw' : 'null')) }}" data-bg="{{ item.thumb || ('/t/' + item.id + '.webp') }}" data-target-subf0ck="{{ item.target_subf0ck_slug || '' }}" data-size="{{ enable_dynamic_thumbs ? (item.thumb_size || 1) : 1 }}">
|
||||||
@if(item.is_audio && !item.has_coverart)
|
@if(item.is_audio && !item.has_coverart && !item.thumb)
|
||||||
<div class="sidebar-media-placeholder audio">
|
<div class="sidebar-media-placeholder audio">
|
||||||
<i class="fa-solid fa-music"></i>
|
<i class="fa-solid fa-music"></i>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -123,8 +123,8 @@
|
|||||||
@if(count.f0cks)
|
@if(count.f0cks)
|
||||||
<div class="posts no-infinite-scroll">
|
<div class="posts no-infinite-scroll">
|
||||||
@each(f0cks.items as item)
|
@each(f0cks.items as item)
|
||||||
<a href="{{ f0cks.link.main }}{{ (enable_item_slugs && item.slug) ? item.slug : item.id }}" class="{{ item.is_pinned ? 'anim-boxshadow ' : '' }}thumb lazy-thumb {{ item.is_pinned ? 'is-pinned' : '' }}" data-item-id="{{ item.id }}" data-file="{{ item.dest }}" data-mime="{{ item.mime }}" data-user="{!! item.display_name || item.username !!}" data-ext="{{ item.mime.split('/')[1].replace('youtube', 'yt').replace('x-shockwave-flash', 'flash').replace('vnd.adobe.flash.movie', 'flash').toUpperCase() }}" data-mode="{{ item.tag_id == nsfl_tag_id ? 'nsfl' : (item.tag_id == 2 ? 'nsfw' : (item.tag_id == 1 ? 'sfw' : 'null')) }}" data-bg="/t/{{ item.id }}.webp">
|
<a href="{{ f0cks.link.main }}{{ (enable_item_slugs && item.slug) ? item.slug : item.id }}{{ item.target_subf0ck_slug ? ('#' + item.target_subf0ck_slug) : '' }}" class="{{ item.is_pinned ? 'anim-boxshadow ' : '' }}thumb lazy-thumb {{ item.is_pinned ? 'is-pinned' : '' }}" data-item-id="{{ item.id }}" data-file="{{ item.matching_sub_dest || item.dest }}" data-mime="{{ item.matching_sub_mime || item.mime }}" data-user="{!! item.display_name || item.username !!}" data-ext="{{ (item.matching_sub_mime || item.mime).split('/')[1].replace('youtube', 'yt').replace('x-shockwave-flash', 'flash').replace('vnd.adobe.flash.movie', 'flash').toUpperCase() }}" data-mode="{{ item.tag_id == nsfl_tag_id ? 'nsfl' : (item.tag_id == 2 ? 'nsfw' : (item.tag_id == 1 ? 'sfw' : 'null')) }}" data-bg="{{ item.thumb || ('/t/' + item.id + '.webp') }}" data-target-subf0ck="{{ item.target_subf0ck_slug || '' }}">
|
||||||
@if(item.is_audio && !item.has_coverart)
|
@if(item.is_audio && !item.has_coverart && !item.thumb)
|
||||||
<div class="sidebar-media-placeholder audio">
|
<div class="sidebar-media-placeholder audio">
|
||||||
<i class="fa-solid fa-music"></i>
|
<i class="fa-solid fa-music"></i>
|
||||||
</div>
|
</div>
|
||||||
@@ -162,8 +162,8 @@
|
|||||||
@if(count.favs)
|
@if(count.favs)
|
||||||
<div class="posts no-infinite-scroll">
|
<div class="posts no-infinite-scroll">
|
||||||
@each(favs.items as item)
|
@each(favs.items as item)
|
||||||
<a href="{{ favs.link.main }}{{ (enable_item_slugs && item.slug) ? item.slug : item.id }}" class="{{ item.is_pinned ? 'anim-boxshadow ' : '' }}thumb lazy-thumb {{ item.is_pinned ? 'is-pinned' : '' }}" data-item-id="{{ item.id }}" data-file="{{ item.dest }}" data-mime="{{ item.mime }}" data-user="{!! item.display_name || item.username !!}" data-ext="{{ item.mime.split('/')[1].replace('youtube', 'yt').replace('x-shockwave-flash', 'flash').replace('vnd.adobe.flash.movie', 'flash').toUpperCase() }}" data-mode="{{ item.tag_id == nsfl_tag_id ? 'nsfl' : (item.tag_id == 2 ? 'nsfw' : (item.tag_id == 1 ? 'sfw' : 'null')) }}" data-bg="/t/{{ item.id }}.webp">
|
<a href="{{ favs.link.main }}{{ (enable_item_slugs && item.slug) ? item.slug : item.id }}{{ item.target_subf0ck_slug ? ('#' + item.target_subf0ck_slug) : '' }}" class="{{ item.is_pinned ? 'anim-boxshadow ' : '' }}thumb lazy-thumb {{ item.is_pinned ? 'is-pinned' : '' }}" data-item-id="{{ item.id }}" data-file="{{ item.matching_sub_dest || item.dest }}" data-mime="{{ item.matching_sub_mime || item.mime }}" data-user="{!! item.display_name || item.username !!}" data-ext="{{ (item.matching_sub_mime || item.mime).split('/')[1].replace('youtube', 'yt').replace('x-shockwave-flash', 'flash').replace('vnd.adobe.flash.movie', 'flash').toUpperCase() }}" data-mode="{{ item.tag_id == nsfl_tag_id ? 'nsfl' : (item.tag_id == 2 ? 'nsfw' : (item.tag_id == 1 ? 'sfw' : 'null')) }}" data-bg="{{ item.thumb || ('/t/' + item.id + '.webp') }}" data-target-subf0ck="{{ item.target_subf0ck_slug || '' }}">
|
||||||
@if(item.is_audio && !item.has_coverart)
|
@if(item.is_audio && !item.has_coverart && !item.thumb)
|
||||||
<div class="sidebar-media-placeholder audio">
|
<div class="sidebar-media-placeholder audio">
|
||||||
<i class="fa-solid fa-music"></i>
|
<i class="fa-solid fa-music"></i>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user