This commit is contained in:
2026-09-14 15:51:11 +02:00
parent ba5bc18b98
commit 22ffc3b51a
13 changed files with 3162 additions and 284 deletions
+595 -102
View File
@@ -701,39 +701,96 @@ html[theme='paper'] .metadata table th {
border-color: #e2e8f0; border-color: #e2e8f0;
} }
/* Icons & GapRight - high contrast black colored */ /* Icons & GapRight - aligned with .input-actions button */
html[theme='light'] .gapRight i.iconset, html[theme='light'] .gapRight i.iconset,
html[theme='light'] .gapRight svg.iconset, html[theme='light'] .gapRight svg.iconset,
html[theme='light'] .gapRight > .iconset, html[theme='light'] .gapRight > .iconset,
html[theme='light'] .gapRight button,
html[theme='paper'] .gapRight i.iconset, html[theme='paper'] .gapRight i.iconset,
html[theme='paper'] .gapRight svg.iconset, html[theme='paper'] .gapRight svg.iconset,
html[theme='paper'] .gapRight > .iconset { html[theme='paper'] .gapRight > .iconset,
color: #000 !important; html[theme='paper'] .gapRight button,
fill: #000 !important; [theme='light'] .gapRight i.iconset,
background: #f1f5f9 !important; [theme='light'] .gapRight svg.iconset,
[theme='light'] .gapRight > .iconset,
[theme='light'] .gapRight button,
[theme='paper'] .gapRight i.iconset,
[theme='paper'] .gapRight svg.iconset,
[theme='paper'] .gapRight > .iconset,
[theme='paper'] .gapRight button {
background: #ffffff !important;
border: 1px solid #cbd5e1 !important; border: 1px solid #cbd5e1 !important;
border-radius: 4px; border-radius: 3px !important;
color: #334155 !important;
fill: #334155 !important;
} }
html[theme='light'] .gapRight i.iconset:hover, html[theme='light'] .gapRight i.iconset:hover,
html[theme='light'] .gapRight svg.iconset:hover, html[theme='light'] .gapRight svg.iconset:hover,
html[theme='light'] .gapRight > .iconset:hover,
html[theme='light'] .gapRight button:hover,
html[theme='paper'] .gapRight i.iconset:hover, html[theme='paper'] .gapRight i.iconset:hover,
html[theme='paper'] .gapRight svg.iconset:hover { html[theme='paper'] .gapRight svg.iconset:hover,
background: #e2e8f0 !important; html[theme='paper'] .gapRight > .iconset:hover,
color: #000 !important; html[theme='paper'] .gapRight button:hover,
fill: #000 !important; [theme='light'] .gapRight i.iconset:hover,
[theme='light'] .gapRight svg.iconset:hover,
[theme='light'] .gapRight > .iconset:hover,
[theme='light'] .gapRight button:hover,
[theme='paper'] .gapRight i.iconset:hover,
[theme='paper'] .gapRight svg.iconset:hover,
[theme='paper'] .gapRight > .iconset:hover,
[theme='paper'] .gapRight button:hover,
html[theme='light'] .gapRight i.iconset.active,
html[theme='light'] .gapRight svg.iconset.active,
html[theme='light'] .gapRight > .iconset.active,
html[theme='light'] .gapRight button.active,
html[theme='paper'] .gapRight i.iconset.active,
html[theme='paper'] .gapRight svg.iconset.active,
html[theme='paper'] .gapRight > .iconset.active,
html[theme='paper'] .gapRight button.active,
[theme='light'] .gapRight i.iconset.active,
[theme='light'] .gapRight svg.iconset.active,
[theme='light'] .gapRight > .iconset.active,
[theme='light'] .gapRight button.active,
[theme='paper'] .gapRight i.iconset.active,
[theme='paper'] .gapRight svg.iconset.active,
[theme='paper'] .gapRight > .iconset.active,
[theme='paper'] .gapRight button.active,
html[theme='light'] .gapRight i.iconset.is-active,
html[theme='light'] .gapRight svg.iconset.is-active,
html[theme='light'] .gapRight > .iconset.is-active,
html[theme='light'] .gapRight button.is-active,
html[theme='paper'] .gapRight i.iconset.is-active,
html[theme='paper'] .gapRight svg.iconset.is-active,
html[theme='paper'] .gapRight > .iconset.is-active,
html[theme='paper'] .gapRight button.is-active,
[theme='light'] .gapRight i.iconset.is-active,
[theme='light'] .gapRight svg.iconset.is-active,
[theme='light'] .gapRight > .iconset.is-active,
[theme='light'] .gapRight button.is-active,
[theme='paper'] .gapRight i.iconset.is-active,
[theme='paper'] .gapRight svg.iconset.is-active,
[theme='paper'] .gapRight > .iconset.is-active,
[theme='paper'] .gapRight button.is-active {
background: #f1f5f9 !important;
border-color: var(--accent) !important;
color: var(--accent) !important;
fill: var(--accent) !important;
} }
html[theme='light'] .gapRight #a_favo, html[theme='light'] .gapRight #a_favo.fa-solid,
html[theme='paper'] .gapRight #a_favo { html[theme='paper'] .gapRight #a_favo.fa-solid,
[theme='light'] .gapRight #a_favo.fa-solid,
[theme='paper'] .gapRight #a_favo.fa-solid {
color: #e11d48 !important; color: #e11d48 !important;
fill: #e11d48 !important; fill: #e11d48 !important;
border-color: rgba(225, 29, 72, 0.4) !important;
} }
html[theme='light'] i.iconset#a_oc, html[theme='light'] i.iconset#a_oc,
html[theme='paper'] i.iconset#a_oc { html[theme='paper'] i.iconset#a_oc {
color: #000 !important; color: inherit !important;
fill: #000 !important;
} }
/* a_addtag and tag controls */ /* a_addtag and tag controls */
@@ -3155,23 +3212,41 @@ body.sidebar-right-hidden #sidebar-drag-zone {
} }
.sidebar-media-placeholder.audio { .sidebar-media-placeholder.audio {
background: radial-gradient(circle at center, rgba(var(--accent-rgb, 153, 255, 0), 0.22) 0%, rgba(var(--accent-rgb, 153, 255, 0), 0.05) 70%), var(--nav-bg, #1e1e1e); background: #000000;
color: var(--accent); color: var(--accent);
text-shadow: 0 0 16px rgba(var(--accent-rgb, 153, 255, 0), 0.35); text-shadow: 0 0 16px color-mix(in srgb, var(--accent, #99ff00) 35%, transparent);
perspective: 800px;
overflow: hidden;
outline: none !important;
} }
.sidebar-media-placeholder.audio.has-cover { .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); background: #000000;
}
.audio-reactive-bg {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
z-index: 0;
opacity: 0;
transition: opacity 0.3s ease;
will-change: background, opacity;
} }
.audio-cover-circle { .audio-cover-circle {
display: none; display: flex;
align-items: center;
justify-content: center;
position: absolute; position: absolute;
top: 50%; top: 50%;
left: 50%; left: 50%;
transform: translate(-50%, -50%); transform: translate(-50%, -50%);
width: 155px; width: 125px;
height: 155px; height: 125px;
max-width: calc(100% - 40px); max-width: calc(100% - 40px);
max-height: calc(100% - 40px); max-height: calc(100% - 40px);
aspect-ratio: 1 / 1; aspect-ratio: 1 / 1;
@@ -3179,31 +3254,63 @@ body.sidebar-right-hidden #sidebar-drag-zone {
background-size: cover; background-size: cover;
background-position: center; background-position: center;
background-repeat: no-repeat; 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); background-color: #000000;
border: 2px solid rgba(var(--accent-rgb, 153, 255, 0), 0.4); background-image: none;
overflow: hidden; box-shadow: 0 12px 40px rgba(0, 0, 0, 0.88), 0 0 150px color-mix(in srgb, var(--accent, #99ff00) 45%, transparent);
border: 2px solid color-mix(in srgb, var(--accent, #99ff00) 40%, transparent);
outline: none !important;
overflow: visible;
pointer-events: none; pointer-events: none;
z-index: 1; z-index: 1;
will-change: transform, box-shadow, border-color; will-change: transform, box-shadow, border-color;
} }
.audio-cover-circle::after { .audio-eye-inner-canvas {
content: '';
position: absolute; position: absolute;
inset: 0; top: 50%;
left: 50%;
transform: translate(-50%, -50%);
border-radius: 50%; 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; pointer-events: none;
z-index: 2;
} }
.sidebar-media-placeholder.audio.has-cover .audio-cover-circle { .audio-cover-circle i {
display: block; font-size: 3.4rem;
color: var(--accent);
text-shadow: 0 0 16px color-mix(in srgb, var(--accent, #99ff00) 75%, transparent), 0 0 35px color-mix(in srgb, var(--accent, #99ff00) 40%, transparent);
z-index: 2;
position: relative;
pointer-events: none;
display: inline-block;
will-change: transform, filter;
transition: transform 0.04s linear;
} }
.sidebar-media-placeholder.audio.has-cover i { .sidebar-media-placeholder.audio.has-cover .audio-cover-circle i {
display: none !important; display: none !important;
} }
.sidebar-media-placeholder.audio:has(.audio-cover-circle) > i {
display: none !important;
}
.sidebar-media-placeholder.audio .audio-cover-circle {
display: flex;
}
.thumb > .sidebar-media-placeholder .audio-cover-circle,
.album-thumb-item > .sidebar-media-placeholder .audio-cover-circle,
.item-preview .sidebar-media-placeholder .audio-cover-circle {
display: none !important;
}
.thumb > .sidebar-media-placeholder > i,
.album-thumb-item > .sidebar-media-placeholder > i,
.item-preview .sidebar-media-placeholder > i {
display: flex !important;
}
.sidebar-media-placeholder.hidden { .sidebar-media-placeholder.hidden {
display: none !important; display: none !important;
} }
@@ -3233,6 +3340,13 @@ body.sidebar-right-hidden #sidebar-drag-zone {
pointer-events: none; pointer-events: none;
font-size: clamp(2rem, 5.5vw, 3.8rem); font-size: clamp(2rem, 5.5vw, 3.8rem);
border-radius: inherit; border-radius: inherit;
background: #000000 !important;
}
.album-gallery-container.is-audio-active,
.album-audio-wrapper,
.embed-responsive:has(.sidebar-media-placeholder.audio) {
background: #000000 !important;
} }
.v0ck:fullscreen > .sidebar-media-placeholder.audio, .v0ck:fullscreen > .sidebar-media-placeholder.audio,
@@ -3252,6 +3366,13 @@ body.sidebar-right-hidden #sidebar-drag-zone {
max-height: calc(100% - 100px); max-height: calc(100% - 100px);
} }
.v0ck:fullscreen .audio-cover-circle i,
.v0ck.v0ck_fullscreen .audio-cover-circle i,
#main:fullscreen .v0ck.v0ck_fullscreen .audio-cover-circle i,
#onara-modal:fullscreen .v0ck.v0ck_fullscreen .audio-cover-circle i {
font-size: clamp(5rem, 14vmin, 12rem);
}
.sidebar-media-placeholder.audio i { .sidebar-media-placeholder.audio i {
position: relative; position: relative;
z-index: 2; z-index: 2;
@@ -3311,6 +3432,50 @@ body.sidebar-right-hidden #sidebar-drag-zone {
display: none !important; display: none !important;
} }
/* Embedded in Right Sidebar */
.sidebar-tuner-container {
display: none;
flex: 1;
min-height: 0;
overflow-y: auto;
scrollbar-width: thin;
scrollbar-color: rgba(255, 255, 255, 0.15) transparent;
padding: 0;
touch-action: pan-y;
}
.sidebar-tuner-container::-webkit-scrollbar {
width: 5px;
}
.sidebar-tuner-container::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.15);
border-radius: 4px;
}
.sidebar-tuner-container.active {
display: block;
}
.f0ck-audio-tuner-panel.in-sidebar {
position: static;
width: 100%;
max-height: none;
overflow-y: visible;
background: transparent;
backdrop-filter: none;
-webkit-backdrop-filter: none;
border: none;
border-radius: 0;
box-shadow: none;
padding: 14px 14px 40px 14px;
box-sizing: border-box;
}
.f0ck-audio-tuner-panel.in-sidebar .f0ck-tuner-close {
display: none;
}
.f0ck-tuner-header { .f0ck-tuner-header {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
@@ -3431,6 +3596,126 @@ body.sidebar-right-hidden #sidebar-drag-zone {
color: #fff; color: #fff;
} }
.f0ck-tuner-presets-bar {
background: rgba(255, 255, 255, 0.04);
border: 1px solid rgba(255, 255, 255, 0.10);
border-radius: 8px;
padding: 10px;
margin-bottom: 14px;
display: flex;
flex-direction: column;
gap: 8px;
}
.f0ck-tuner-presets-select-wrap {
display: flex;
gap: 6px;
align-items: center;
}
.f0ck-tuner-preset-select {
flex: 1;
background: rgba(0, 0, 0, 0.45);
border: 1px solid rgba(255, 255, 255, 0.18);
border-radius: 6px;
color: #fff;
padding: 6px 8px;
font-size: 11px;
font-family: inherit;
cursor: pointer;
outline: none;
}
.f0ck-tuner-preset-select:focus {
border-color: var(--accent, #99ff00);
}
.f0ck-tuner-preset-select optgroup {
color: var(--accent, #99ff00);
background: #111;
font-weight: bold;
}
.f0ck-tuner-preset-select option {
color: #eee;
background: #181818;
}
.f0ck-tuner-btn-icon {
background: rgba(255, 255, 255, 0.06);
border: 1px solid rgba(255, 255, 255, 0.14);
color: #bbb;
border-radius: 6px;
padding: 6px 9px;
font-size: 11px;
cursor: pointer;
transition: all 0.15s ease;
display: flex;
align-items: center;
justify-content: center;
}
.f0ck-tuner-btn-icon:disabled {
opacity: 0.3;
cursor: not-allowed;
}
.f0ck-tuner-btn-icon:not(:disabled):hover {
background: rgba(255, 50, 50, 0.25);
border-color: rgba(255, 50, 50, 0.5);
color: #ff5555;
}
.f0ck-tuner-presets-actions {
display: flex;
gap: 6px;
align-items: center;
}
.f0ck-tuner-preset-input {
flex: 1;
background: rgba(0, 0, 0, 0.35);
border: 1px solid rgba(255, 255, 255, 0.14);
border-radius: 6px;
color: #fff;
padding: 5px 8px;
font-size: 11px;
font-family: inherit;
outline: none;
}
.f0ck-tuner-preset-input:focus {
border-color: var(--accent, #99ff00);
}
.f0ck-tuner-btn-save-preset,
.f0ck-tuner-btn-import-preset {
background: rgba(255, 255, 255, 0.08);
border: 1px solid rgba(255, 255, 255, 0.18);
color: #eee;
border-radius: 6px;
padding: 5px 10px;
font-size: 11px;
font-weight: 600;
cursor: pointer;
transition: all 0.15s ease;
display: flex;
align-items: center;
gap: 4px;
white-space: nowrap;
}
.f0ck-tuner-btn-save-preset:hover {
background: var(--accent, #99ff00);
border-color: var(--accent, #99ff00);
color: #000;
}
.f0ck-tuner-btn-import-preset:hover {
background: rgba(255, 255, 255, 0.2);
color: #fff;
}
.sidebar-video-details { .sidebar-video-details {
display: flex; display: flex;
margin-top: 8px; margin-top: 8px;
@@ -6725,8 +7010,10 @@ body.layout-modern.sidebar-right-hidden .pagination-container-fluid {
.gapRight { .gapRight {
align-items: center; align-items: center;
display: flex; display: inline-flex;
gap: 6px;
flex-wrap: wrap;
vertical-align: middle;
} }
.admin-lock-btn { .admin-lock-btn {
@@ -6938,7 +7225,8 @@ body:has(.index-container) > .pagewrapper > #main {
overflow-y: auto !important; overflow-y: auto !important;
overflow-x: hidden !important; overflow-x: hidden !important;
-webkit-overflow-scrolling: touch; -webkit-overflow-scrolling: touch;
overscroll-behavior-y: contain; overscroll-behavior-y: auto;
overscroll-behavior-x: contain;
box-sizing: border-box; box-sizing: border-box;
padding: 5px 20px 75px 20px; padding: 5px 20px 75px 20px;
margin: 0; margin: 0;
@@ -7772,7 +8060,7 @@ a.removetag i {
background: transparent; background: transparent;
border: none; border: none;
padding: 0; padding: 0;
margin: 0; margin: 0 0 0 5px;
cursor: pointer; cursor: pointer;
color: inherit; color: inherit;
display: inline-flex; display: inline-flex;
@@ -7783,44 +8071,15 @@ a.removetag i {
vertical-align: middle; vertical-align: middle;
border-radius: 50%; border-radius: 50%;
outline: none; outline: none;
opacity: 0.85;
width: 14px;
max-width: 14px;
overflow: visible;
pointer-events: auto;
transform: scale(1);
transition: all 0.22s cubic-bezier(0.34, 1.56, 0.64, 1); transition: all 0.22s cubic-bezier(0.34, 1.56, 0.64, 1);
} }
@media (hover: hover) {
.tag-badge:not(.tag-is-excluded) .tag-exclude-btn {
opacity: 0;
width: 0;
max-width: 0;
margin-left: 0;
overflow: hidden;
transform: scale(0.5);
pointer-events: none;
}
.tag-badge:hover .tag-exclude-btn,
.tag-badge:focus-within .tag-exclude-btn {
opacity: 0.85;
width: 14px;
max-width: 14px;
margin-left: 5px;
overflow: visible;
transform: scale(1);
pointer-events: auto;
}
}
@media (hover: none) {
.tag-badge .tag-exclude-btn {
opacity: 0.7;
width: 14px;
max-width: 14px;
margin-left: 4px;
overflow: visible;
pointer-events: auto;
transform: scale(1);
}
}
.tag-badge .tag-exclude-btn:hover, .tag-badge .tag-exclude-btn:hover,
.tag-badge .tag-exclude-btn:focus-visible { .tag-badge .tag-exclude-btn:focus-visible {
opacity: 1; opacity: 1;
@@ -7845,12 +8104,6 @@ a.removetag i {
.tag-badge.tag-is-excluded .tag-exclude-btn { .tag-badge.tag-is-excluded .tag-exclude-btn {
opacity: 0.95; opacity: 0.95;
width: 14px;
max-width: 14px;
margin-left: 5px;
overflow: visible;
pointer-events: auto;
transform: scale(1);
color: #ff4757; color: #ff4757;
} }
@@ -9674,15 +9927,18 @@ table img {
} }
} }
/* gapRight icon colors: white for all, pink for favorites */ /* gapRight icon colors & active states */
.gapRight i.iconset { .gapRight i.iconset,
color: #fff !important; .gapRight svg.iconset,
fill: #fff !important; .gapRight button {
color: var(--white, #fff);
fill: var(--white, #fff);
} }
.gapRight #a_favo { .gapRight #a_favo.fa-solid {
color: #ff6b9d !important; color: #ff6b9d !important;
fill: #ff6b9d !important; fill: #ff6b9d !important;
border-color: rgba(255, 107, 157, 0.4) !important;
} }
/* pin icon */ /* pin icon */
@@ -9701,10 +9957,6 @@ table img {
transform: rotate(-45deg); transform: rotate(-45deg);
} }
.iconset#subscribe-btn {
border-style: outset;
}
.imageDoor:hover:after { .imageDoor:hover:after {
content: ""; content: "";
background-image: var(--hover-image); background-image: var(--hover-image);
@@ -14695,6 +14947,42 @@ body.layout-modern .tag-ac-input {
margin: 5px; margin: 5px;
} }
.item-preview .sidebar-thumb-link {
position: relative;
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 5px;
margin: 5px;
overflow: hidden;
flex-shrink: 0;
text-decoration: none;
}
.item-preview .sidebar-media-placeholder {
position: absolute;
inset: 0;
width: 32px;
height: 32px;
font-size: 14px;
border-radius: inherit;
display: flex;
align-items: center;
justify-content: center;
}
.item-preview .sidebar-media-placeholder.audio {
background: #000000;
color: var(--accent);
}
.item-preview .sidebar-media-placeholder.audio > i {
display: flex !important;
font-size: 14px;
}
/* /*
PRIVATE MESSAGES / DM SYSTEM PRIVATE MESSAGES / DM SYSTEM
*/ */
@@ -16347,37 +16635,109 @@ span.gchat-post-card--loading {
.gapRight, .gapRight,
body.layout-modern .gapRight, body.layout-modern .gapRight,
body.layout-legacy .gapRight { body.layout-legacy .gapRight {
display: inline-flex;
align-items: center;
justify-content: center; justify-content: center;
gap: 6px;
flex-wrap: wrap;
vertical-align: middle;
} }
.gapRight>svg.iconset, .gapRight>svg.iconset,
.gapRight>i.iconset, .gapRight>i.iconset,
.gapRight>button,
.gapRight>.iconset,
.gapRight i.iconset,
.gapRight svg.iconset,
.gapRight button,
body.layout-modern .gapRight>svg.iconset, body.layout-modern .gapRight>svg.iconset,
body.layout-modern .gapRight>i.iconset, body.layout-modern .gapRight>i.iconset,
body.layout-modern .gapRight>button,
body.layout-modern .gapRight>.iconset,
body.layout-legacy .gapRight>svg.iconset, body.layout-legacy .gapRight>svg.iconset,
body.layout-legacy .gapRight>i.iconset { body.layout-legacy .gapRight>i.iconset,
body.layout-legacy .gapRight>button,
body.layout-legacy .gapRight>.iconset {
height: 30px; height: 30px;
min-width: 30px;
width: 30px; width: 30px;
margin: 5px; padding: 0;
padding: 5px; box-sizing: border-box;
right: 0px;
background: var(--badge-tag);
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
font-size: 18px; gap: 5px;
box-sizing: border-box; border-radius: 3px;
border: 1px solid var(--nav-border-color, rgba(255, 255, 255, 0.15));
background: var(--navigation-links-bg, rgba(255, 255, 255, 0.08));
color: var(--white, #fff);
fill: var(--white, #fff);
font-family: inherit;
font-size: 13px;
font-weight: bold;
line-height: 1;
cursor: pointer;
margin: 0;
position: static;
right: auto;
vertical-align: middle;
text-decoration: none;
transition: background-color 0.15s ease, border-color 0.15s ease, color 0.15s ease, opacity 0.15s ease;
}
.gapRight>svg.iconset:hover,
.gapRight>i.iconset:hover,
.gapRight>button:hover,
.gapRight>.iconset:hover,
.gapRight i.iconset:hover,
.gapRight svg.iconset:hover,
.gapRight button:hover,
.gapRight>svg.iconset.active,
.gapRight>i.iconset.active,
.gapRight>button.active,
.gapRight>.iconset.active,
.gapRight i.iconset.active,
.gapRight svg.iconset.active,
.gapRight button.active,
.gapRight>svg.iconset.is-active,
.gapRight>i.iconset.is-active,
.gapRight>button.is-active,
.gapRight>.iconset.is-active,
.gapRight i.iconset.is-active,
.gapRight svg.iconset.is-active,
.gapRight button.is-active,
body.layout-modern .gapRight>svg.iconset:hover,
body.layout-modern .gapRight>i.iconset:hover,
body.layout-modern .gapRight>button:hover,
body.layout-modern .gapRight>.iconset:hover,
body.layout-modern .gapRight>svg.iconset.active,
body.layout-modern .gapRight>i.iconset.active,
body.layout-modern .gapRight>button.active,
body.layout-modern .gapRight>.iconset.active,
body.layout-legacy .gapRight>svg.iconset:hover,
body.layout-legacy .gapRight>i.iconset:hover,
body.layout-legacy .gapRight>button:hover,
body.layout-legacy .gapRight>.iconset:hover,
body.layout-legacy .gapRight>svg.iconset.active,
body.layout-legacy .gapRight>i.iconset.active,
body.layout-legacy .gapRight>button.active,
body.layout-legacy .gapRight>.iconset.active {
background: var(--nav-link-hover-bg, rgba(255, 255, 255, 0.15)) !important;
border-color: var(--accent) !important;
color: var(--accent) !important;
fill: var(--accent) !important;
text-decoration: none;
} }
.gapRight>.iconset#a_favo, .gapRight>.iconset#a_favo,
body.layout-legacy .gapRight>.iconset#a_favo { body.layout-legacy .gapRight>.iconset#a_favo {
fill: #ff009b; fill: inherit;
} }
/* OC Toggle Icon - now i element, color inherited from .gapRight i.iconset */ /* OC Toggle Icon - inherits from .gapRight i.iconset */
i.iconset#a_oc { i.iconset#a_oc {
color: #fff !important; color: inherit;
fill: #fff !important; fill: inherit;
} }
/* OC Feature */ /* OC Feature */
@@ -20246,7 +20606,8 @@ body.onara-modal-open .admin-bar.open {
overflow-x: hidden !important; overflow-x: hidden !important;
-webkit-overflow-scrolling: touch; -webkit-overflow-scrolling: touch;
scrollbar-width: none !important; scrollbar-width: none !important;
overscroll-behavior: contain !important; overscroll-behavior-y: auto !important;
overscroll-behavior-x: contain !important;
box-sizing: border-box !important; box-sizing: border-box !important;
opacity: 0; opacity: 0;
transform: scale(0.99); transform: scale(0.99);
@@ -21026,6 +21387,8 @@ div#flash,
.album-gallery-container { .album-gallery-container {
position: relative; position: relative;
overflow: hidden; overflow: hidden;
max-width: 100%;
box-sizing: border-box;
} }
.album-active-image { .album-active-image {
@@ -21082,7 +21445,8 @@ div#flash,
top: 14px; top: 14px;
left: 50%; left: 50%;
transform: translateX(-50%); transform: translateX(-50%);
pointer-events: auto; pointer-events: none;
opacity: 0;
background: rgba(16, 16, 20, 0.75); background: rgba(16, 16, 20, 0.75);
border: 1px solid rgba(255, 255, 255, 0.2); border: 1px solid rgba(255, 255, 255, 0.2);
backdrop-filter: blur(12px); backdrop-filter: blur(12px);
@@ -21098,6 +21462,24 @@ div#flash,
box-shadow: 0 4px 14px rgba(0, 0, 0, 0.4); box-shadow: 0 4px 14px rgba(0, 0, 0, 0.4);
letter-spacing: 0.03em; letter-spacing: 0.03em;
user-select: none; user-select: none;
transition: opacity 0.25s cubic-bezier(0.4, 0, 0.2, 1), transform 0.25s cubic-bezier(0.4, 0, 0.2, 1);
}
@media (hover: hover) {
.album-gallery-container:hover .album-counter-pill,
.album-gallery-container:focus-within .album-counter-pill {
opacity: 1;
pointer-events: auto;
}
}
.album-gallery-container.v0ck_hover .album-counter-pill,
.album-gallery-container.strip-peek .album-counter-pill,
.album-gallery-container:has(.v0ck.v0ck_hover) .album-counter-pill,
.album-counter-pill.is-visible,
.album-counter-pill.strip-peek {
opacity: 1;
pointer-events: auto;
} }
.album-counter-pill i { .album-counter-pill i {
@@ -21111,6 +21493,7 @@ div#flash,
left: 50%; left: 50%;
transform: translateX(-50%) translateY(10px); transform: translateX(-50%) translateY(10px);
max-width: calc(100% - 32px); max-width: calc(100% - 32px);
box-sizing: border-box;
display: flex; display: flex;
align-items: center; align-items: center;
gap: 8px; gap: 8px;
@@ -21122,16 +21505,101 @@ div#flash,
border-radius: 12px; border-radius: 12px;
z-index: 22; z-index: 22;
overflow-x: auto; overflow-x: auto;
overflow-y: hidden;
overscroll-behavior: contain;
overscroll-behavior-x: contain;
overscroll-behavior-y: none;
-webkit-overflow-scrolling: touch;
touch-action: pan-x;
scrollbar-width: none; scrollbar-width: none;
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.45); box-shadow: 0 6px 20px rgba(0, 0, 0, 0.45);
opacity: 0; opacity: 0;
pointer-events: none; pointer-events: none;
cursor: grab;
user-select: none;
-webkit-user-select: none;
scroll-behavior: smooth;
transition: opacity 0.25s cubic-bezier(0.4, 0, 0.2, 1), transform 0.25s cubic-bezier(0.4, 0, 0.2, 1); transition: opacity 0.25s cubic-bezier(0.4, 0, 0.2, 1), transform 0.25s cubic-bezier(0.4, 0, 0.2, 1);
} }
.album-gallery-container:hover .album-thumbnails-strip, .album-thumbnails-strip.is-dragging {
.album-gallery-container:focus-within .album-thumbnails-strip, cursor: grabbing !important;
.album-thumbnails-strip.is-visible { scroll-behavior: auto !important;
}
.album-thumb-item {
cursor: pointer;
}
.album-thumb-item img {
-webkit-user-drag: none;
user-select: none;
}
/* Audio Track Info Badge (Artist & Song Title on top-left) */
.audio-track-info-badge {
position: absolute;
top: 5px;
left: 10px;
text-align: left;
z-index: 25;
pointer-events: none;
opacity: 0;
transition: opacity 0.2s ease;
}
.audio-track-info-badge.is-visible {
opacity: 1;
}
.audio-track-info-details {
display: flex;
flex-direction: column;
align-items: flex-start;
text-align: left;
min-width: 0;
line-height: 1.25;
}
.audio-track-artist {
font-size: 0.72rem;
font-weight: 700;
letter-spacing: 0.07em;
text-transform: uppercase;
color: var(--accent);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
text-align: left;
align-self: flex-start;
}
.audio-track-title {
font-size: 0.88rem;
font-weight: 700;
color: #fff;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
text-shadow: 0 1px 4px rgba(0, 0, 0, 0.8);
text-align: left;
align-self: flex-start;
}
@media (hover: hover) {
.album-gallery-container:hover .album-thumbnails-strip,
.album-gallery-container:focus-within .album-thumbnails-strip {
opacity: 1;
pointer-events: auto;
transform: translateX(-50%) translateY(0);
}
}
.album-gallery-container.v0ck_hover .album-thumbnails-strip,
.album-gallery-container.strip-peek .album-thumbnails-strip,
.album-gallery-container:has(.v0ck.v0ck_hover) .album-thumbnails-strip,
.album-thumbnails-strip.is-visible,
.album-thumbnails-strip.strip-peek {
opacity: 1; opacity: 1;
pointer-events: auto; pointer-events: auto;
transform: translateX(-50%) translateY(0); transform: translateX(-50%) translateY(0);
@@ -21141,8 +21609,7 @@ div#flash,
display: none; display: none;
} }
.album-video-wrapper, .album-video-wrapper {
.album-audio-wrapper {
position: absolute; position: absolute;
inset: 0; inset: 0;
width: 100%; width: 100%;
@@ -21150,6 +21617,14 @@ div#flash,
background: #000; background: #000;
} }
.album-audio-wrapper {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
background: transparent;
}
.album-gallery-container.is-video-active .album-thumbnails-strip, .album-gallery-container.is-video-active .album-thumbnails-strip,
.album-gallery-container.is-audio-active .album-thumbnails-strip { .album-gallery-container.is-audio-active .album-thumbnails-strip {
bottom: 58px; bottom: 58px;
@@ -21180,6 +21655,9 @@ div#flash,
padding: 0; padding: 0;
margin: 0; margin: 0;
background: #000; background: #000;
touch-action: pan-x;
user-select: none;
-webkit-user-select: none;
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
} }
@@ -21261,5 +21739,20 @@ div#flash,
bottom: 8px; bottom: 8px;
padding: 4px 8px; padding: 4px 8px;
gap: 6px; gap: 6px;
max-width: calc(100% - 16px);
box-sizing: border-box;
}
.audio-track-info-badge {
top: 8px;
left: 8px;
padding: 4px 8px;
gap: 6px;
max-width: calc(100% - 70px);
}
.audio-track-title {
font-size: 0.78rem;
}
.audio-track-artist {
font-size: 0.68rem;
} }
} }
+4 -4
View File
@@ -6,7 +6,7 @@
background-size: contain; background-size: contain;
background-repeat: no-repeat; background-repeat: no-repeat;
background-position: center center; background-position: center center;
touch-action: none; /* Prevent pull-to-refresh and scroll while interacting */ touch-action: pan-y;
z-index: 0; z-index: 0;
} }
@@ -556,8 +556,8 @@
transform: translate(-50%, 0); transform: translate(-50%, 0);
} }
/* Hide mouse cursor on inactivity in fullscreen */ /* Hide mouse cursor on inactivity when player controls auto-hide */
.v0ck.v0ck_fullscreen:not(.v0ck_hover), .v0ck:not(.v0ck_hover),
.v0ck.v0ck_fullscreen:not(.v0ck_hover) * { .v0ck:not(.v0ck_hover) * {
cursor: none !important; cursor: none !important;
} }
+54 -8
View File
@@ -27,6 +27,8 @@ class CommentSystem {
if (this.displayMode === 1) this.sort = 'old'; if (this.displayMode === 1) this.sort = 'old';
this.customEmojis = CommentSystem.emojiCache || {}; this.customEmojis = CommentSystem.emojiCache || {};
this._selfPostedCommentIds = new Set();
this._pendingSelfCommentTexts = new Set();
this.icons = { this.icons = {
reply: `<i class="fa-solid fa-reply"></i>`, reply: `<i class="fa-solid fa-reply"></i>`,
@@ -472,6 +474,34 @@ class CommentSystem {
} }
} }
_isSelfComment(data) {
if (!data) return false;
if (data.id && this._selfPostedCommentIds?.has(data.id)) return true;
if (data.body && this._pendingSelfCommentTexts?.has(data.body)) return true;
const session = window.f0ckSession || {};
const currentUserId = session.id || session.user_id;
if (currentUserId && data.user_id && parseInt(data.user_id, 10) === parseInt(currentUserId, 10)) {
return true;
}
const currentUsername = session.user || this.user;
if (currentUsername && data.username && currentUsername.toLowerCase() === data.username.toLowerCase()) {
return true;
}
if (session.is_anon && data.is_anon) {
if (data.anon_fingerprint && session.fingerprint && data.anon_fingerprint === session.fingerprint) {
return true;
}
if (data.anon_short_fingerprint && session.fingerprint && session.fingerprint.includes(data.anon_short_fingerprint)) {
return true;
}
}
return false;
}
handleLiveComment(data) { handleLiveComment(data) {
if (!this.container || !this.itemId) return; if (!this.container || !this.itemId) return;
// 1. Check if comment belongs to this item // 1. Check if comment belongs to this item
@@ -537,14 +567,20 @@ class CommentSystem {
// Danmaku: fire one-shot for other users' comments. // Danmaku: fire one-shot for other users' comments.
// Own comment is handled by the optimistic submit path (fire + addItem). // Own comment is handled by the optimistic submit path (fire + addItem).
// The _loadDanmaku re-render will add this comment to the items rotation. const isSelf = this._isSelfComment(data);
const currentUser = window.f0ckSession?.user; if (window.danmakuInstance && !isSelf) {
if (window.danmakuInstance && data.username !== currentUser) {
window.danmakuInstance.fire( window.danmakuInstance.fire(
data.body, data.body,
data.display_name || data.username || '?', data.display_name || data.username || '?',
data.username_color || null data.username_color || null
); );
window.danmakuInstance.addItem({
id: data.id,
content: data.body,
video_time: data.video_time ?? null,
display_name: data.display_name || data.username || '?',
username_color: data.username_color || null
});
} }
// Update backlinks for live comment // Update backlinks for live comment
@@ -3448,6 +3484,7 @@ class CommentSystem {
} else { } else {
this.isMainSubmitting = true; this.isMainSubmitting = true;
} }
this._pendingSelfCommentTexts.add(text);
let retryCount = 0; let retryCount = 0;
const maxRetries = 20; // Allow several minutes of retrying during restart const maxRetries = 20; // Allow several minutes of retrying during restart
@@ -3534,7 +3571,7 @@ class CommentSystem {
// For 4xx errors, we stop and show the error to user (likely validation or auth) // For 4xx errors, we stop and show the error to user (likely validation or auth)
const json = await res.json().catch(() => ({})); const json = await res.json().catch(() => ({}));
alert('Error: ' + (json.message || `Status ${res.status}`)); alert('Error: ' + (json.message || `Status ${res.status}`));
this._finishSubmit(submitBtn, originalBtnHtml, parentId); this._finishSubmit(submitBtn, originalBtnHtml, parentId, text);
return; return;
} }
@@ -3692,6 +3729,11 @@ class CommentSystem {
poll: null poll: null
}; };
if (json.comment?.id) {
this._selfPostedCommentIds.add(json.comment.id);
}
this._pendingSelfCommentTexts.delete(text);
// Danmaku: fire immediately (one-shot) + add to future rotation // Danmaku: fire immediately (one-shot) + add to future rotation
if (window.danmakuInstance) { if (window.danmakuInstance) {
window.danmakuInstance.fire( window.danmakuInstance.fire(
@@ -3700,6 +3742,7 @@ class CommentSystem {
session.username_color || null session.username_color || null
); );
window.danmakuInstance.addItem({ window.danmakuInstance.addItem({
id: newComment.id,
content: text, content: text,
video_time: newComment.video_time ?? null, video_time: newComment.video_time ?? null,
display_name: session.display_name || currentUsername || '?', display_name: session.display_name || currentUsername || '?',
@@ -3810,10 +3853,10 @@ class CommentSystem {
} }
this._silentSync(); this._silentSync();
this._finishSubmit(submitBtn, originalBtnHtml, parentId); this._finishSubmit(submitBtn, originalBtnHtml, parentId, text);
} else { } else {
alert('Error: ' + json.message); alert('Error: ' + json.message);
this._finishSubmit(submitBtn, originalBtnHtml, parentId); this._finishSubmit(submitBtn, originalBtnHtml, parentId, text);
} }
} catch (err) { } catch (err) {
console.warn(`[CommentSystem] Submit attempt ${retryCount + 1} failed:`, err); console.warn(`[CommentSystem] Submit attempt ${retryCount + 1} failed:`, err);
@@ -3825,7 +3868,7 @@ class CommentSystem {
setTimeout(attemptSubmit, delay); setTimeout(attemptSubmit, delay);
} else { } else {
alert('Failed to send comment after multiple attempts. Please check your connection.'); alert('Failed to send comment after multiple attempts. Please check your connection.');
this._finishSubmit(submitBtn, originalBtnHtml, parentId); this._finishSubmit(submitBtn, originalBtnHtml, parentId, text);
} }
} }
}; };
@@ -3857,7 +3900,10 @@ class CommentSystem {
console.log('[ensureBtn] done, button in DOM:', !!contentEl.querySelector('.load-full-comment-btn')); console.log('[ensureBtn] done, button in DOM:', !!contentEl.querySelector('.load-full-comment-btn'));
} }
_finishSubmit(btn, originalHtml, parentId) { _finishSubmit(btn, originalHtml, parentId, submittedText = null) {
if (submittedText) {
this._pendingSelfCommentTexts?.delete(submittedText);
}
if (parentId) { if (parentId) {
this.pendingSubmissions.delete(parentId); this.pendingSubmissions.delete(parentId);
} else { } else {
+10
View File
@@ -195,6 +195,7 @@ class Danmaku {
const mapped = comments const mapped = comments
.filter(c => !c.is_deleted && c.content) .filter(c => !c.is_deleted && c.content)
.map(c => ({ .map(c => ({
id: c.id || null,
text: this._prepareText(c.content), text: this._prepareText(c.content),
username: c.display_name || c.username || '?', username: c.display_name || c.username || '?',
color: c.username_color || null, color: c.username_color || null,
@@ -288,6 +289,7 @@ class Danmaku {
*/ */
addItem(comment) { addItem(comment) {
if (!comment || !comment.content) return; if (!comment || !comment.content) return;
if (comment.id && this.items && this.items.some(i => i.id === comment.id)) return;
const duration = this.media.duration; const duration = this.media.duration;
const hasDuration = isFinite(duration) && duration > 0; const hasDuration = isFinite(duration) && duration > 0;
@@ -307,6 +309,7 @@ class Danmaku {
} }
const item = { const item = {
id: comment.id || null,
video_time: t, video_time: t,
text: this._prepareText(comment.content), text: this._prepareText(comment.content),
username: comment.display_name || comment.username || '?', username: comment.display_name || comment.username || '?',
@@ -314,6 +317,13 @@ class Danmaku {
fired: true // mark as already fired — caller handles any immediate one-shot fired: true // mark as already fired — caller handles any immediate one-shot
}; };
if (this._synthClock && this._flashPool) {
if (!this._flashPool.some(i => (comment.id && i.id === comment.id) || (i.text === item.text && i.username === item.username))) {
this._flashPool.push(item);
}
return;
}
// Insert in sorted order // Insert in sorted order
const idx = this.items.findIndex(i => i.video_time > t); const idx = this.items.findIndex(i => i.video_time > t);
if (idx === -1) this.items.push(item); if (idx === -1) this.items.push(item);
+2255 -150
View File
File diff suppressed because it is too large Load Diff
+22 -3
View File
@@ -532,14 +532,32 @@
else if (rClass === 'sfw' && blurSfw) isBlurred = true; else if (rClass === 'sfw' && blurSfw) isBlurred = true;
else if (rClass === 'untagged' && blurUntagged) isBlurred = true; else if (rClass === 'untagged' && blurUntagged) isBlurred = true;
let thumbUrl = `/t/${c.item_id}.webp`; const mime = c.mime || c.item_mime || '';
const activeDest = c.item_dest || c.dest || '';
const isAudio = mime.startsWith('audio/') || (!mime && activeDest.match(/\.(mp3|wav|ogg|flac|m4a|aac|opus)$/i));
let thumbUrl = c.thumb || `/t/${c.item_id}.webp`;
if (isBlurred) { if (isBlurred) {
thumbUrl = `/t/${c.item_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);
mediaHtml = `<img src="${thumbUrl}" style="width: 32px; height: 32px; object-fit: cover; border-radius: 2px;" loading="lazy" onerror="this.style.display='none'" />`; if (isAudio && !c.has_coverart) {
mediaHtml = `
<div class="sidebar-media-placeholder audio">
<i class="fa-solid fa-music"></i>
</div>
`;
} else {
mediaHtml = `
<img src="${thumbUrl}" style="width: 32px; height: 32px; object-fit: cover; border-radius: 2px;" loading="lazy" onerror="this.style.display='none'; if(this.nextElementSibling) this.nextElementSibling.classList.remove('hidden');" />
<div class="sidebar-media-placeholder ${isAudio ? 'audio' : ''} hidden">
<i class="${isAudio ? 'fa-solid fa-music' : 'fa-solid fa-image'}"></i>
</div>
`;
}
itemPreview = ` itemPreview = `
<div class="item-preview"> <div class="item-preview">
@@ -1869,6 +1887,7 @@
} }
} }
}; };
window.switchSidebarTab = switchSidebarTab;
const initSidebarTabs = () => { const initSidebarTabs = () => {
const tabsContainer = document.querySelector('.sidebar-tabs'); const tabsContainer = document.querySelector('.sidebar-tabs');
+39 -8
View File
@@ -88,6 +88,8 @@ const updateHoverStates = () => {
const isOver = mouseX >= rect.left && mouseX <= rect.right && const isOver = mouseX >= rect.left && mouseX <= rect.right &&
mouseY >= rect.top && mouseY <= rect.bottom; mouseY >= rect.top && mouseY <= rect.bottom;
p.classList.toggle("v0ck_hover", isOver); p.classList.toggle("v0ck_hover", isOver);
const gal = p.closest('.album-gallery-container');
if (gal) gal.classList.toggle("v0ck_hover", isOver);
}); });
}; };
@@ -117,6 +119,8 @@ class v0ck {
if (mouseX >= rect.left && mouseX <= rect.right && if (mouseX >= rect.left && mouseX <= rect.right &&
mouseY >= rect.top && mouseY <= rect.bottom) { mouseY >= rect.top && mouseY <= rect.bottom) {
parent.classList.add("v0ck_hover", "v0ck_no_transition"); parent.classList.add("v0ck_hover", "v0ck_no_transition");
const gal = parent.closest('.album-gallery-container');
if (gal) gal.classList.add("v0ck_hover");
// Remove no-transition after a frame // Remove no-transition after a frame
setTimeout(() => parent.classList.remove("v0ck_no_transition"), 50); setTimeout(() => parent.classList.remove("v0ck_no_transition"), 50);
} }
@@ -142,14 +146,24 @@ class v0ck {
if (!ph) { if (!ph) {
ph = document.createElement('div'); ph = document.createElement('div');
ph.className = 'sidebar-media-placeholder audio'; ph.className = 'sidebar-media-placeholder audio';
ph.innerHTML = '<div class="audio-cover-circle"></div><i class="fa-solid fa-music"></i>'; ph.innerHTML = '<div class="audio-cover-circle"><i class="fa-solid fa-music"></i></div>';
player.prepend(ph); player.prepend(ph);
} }
let coverCircle = ph.querySelector('.audio-cover-circle'); let coverCircle = ph.querySelector('.audio-cover-circle');
if (!coverCircle) { if (!coverCircle) {
coverCircle = document.createElement('div'); coverCircle = document.createElement('div');
coverCircle.className = 'audio-cover-circle'; coverCircle.className = 'audio-cover-circle';
coverCircle.innerHTML = '<i class="fa-solid fa-music"></i>';
ph.insertBefore(coverCircle, ph.firstChild); ph.insertBefore(coverCircle, ph.firstChild);
} else {
if (!coverCircle.querySelector('i')) {
const existingI = ph.querySelector(':scope > i');
if (existingI) {
coverCircle.appendChild(existingI);
} else {
coverCircle.insertAdjacentHTML('beforeend', '<i class="fa-solid fa-music"></i>');
}
}
} }
player.style.backgroundImage = 'none'; player.style.backgroundImage = 'none';
player.style.backgroundColor = 'transparent'; player.style.backgroundColor = 'transparent';
@@ -157,7 +171,7 @@ class v0ck {
coverCircle.style.backgroundImage = `url('${poster}')`; coverCircle.style.backgroundImage = `url('${poster}')`;
ph.classList.add('has-cover'); ph.classList.add('has-cover');
} else { } else {
coverCircle.style.backgroundImage = 'none'; coverCircle.style.backgroundImage = '';
ph.classList.remove('has-cover'); ph.classList.remove('has-cover');
} }
} }
@@ -203,6 +217,23 @@ class v0ck {
let wasPausedWhenStarted = false; let wasPausedWhenStarted = false;
// Mobile tap-to-show-controls: true when this touch revealed the controls bar // Mobile tap-to-show-controls: true when this touch revealed the controls bar
let controlsJustShown = false; let controlsJustShown = false;
const setHover = (active) => {
if (active) {
player.classList.add('v0ck_hover');
const gal = player.closest('.album-gallery-container');
if (gal) gal.classList.add('v0ck_hover');
} else {
player.classList.remove('v0ck_hover');
const gal = player.closest('.album-gallery-container');
if (gal) {
gal.classList.remove('v0ck_hover');
gal.classList.remove('strip-peek');
const strip = gal.querySelector('.album-thumbnails-strip');
if (strip) strip.classList.remove('strip-peek');
}
}
};
const speedIndicator = player.querySelector('.v0ck_speed_indicator'); const speedIndicator = player.querySelector('.v0ck_speed_indicator');
// (mouse position is now tracked via docMouseX/docMouseY in resetControlsTimer block) // (mouse position is now tracked via docMouseX/docMouseY in resetControlsTimer block)
@@ -343,7 +374,7 @@ class v0ck {
if (isMobile && controlsJustShown) { if (isMobile && controlsJustShown) {
// First tap: controls were just revealed by this touch — don't toggle play // First tap: controls were just revealed by this touch — don't toggle play
controlsJustShown = false; controlsJustShown = false;
player.classList.add('v0ck_hover'); setHover(true);
return; return;
} }
controlsJustShown = false; controlsJustShown = false;
@@ -353,7 +384,7 @@ class v0ck {
toggle.addEventListener('click', togglePlay); toggle.addEventListener('click', togglePlay);
overlay.addEventListener('click', e => { overlay.addEventListener('click', e => {
e.stopPropagation(); e.stopPropagation();
player.classList.add('v0ck_hover'); setHover(true);
togglePlay(); togglePlay();
}); });
video.addEventListener('play', updatePlayIcon); video.addEventListener('play', updatePlayIcon);
@@ -831,7 +862,7 @@ class v0ck {
} }
if (isMobile && !isInsidePlayer && !isFlashYankUI) { if (isMobile && !isInsidePlayer && !isFlashYankUI) {
player.classList.remove('v0ck_hover'); setHover(false);
} }
}); });
@@ -928,7 +959,7 @@ class v0ck {
const isFullscreen = player.classList.contains('v0ck_fullscreen'); const isFullscreen = player.classList.contains('v0ck_fullscreen');
if (!video.paused || isFullscreen) { if (!video.paused || isFullscreen) {
controlsTimer = setTimeout(() => { controlsTimer = setTimeout(() => {
player.classList.remove('v0ck_hover'); setHover(false);
if (settingsMenu && !settingsMenu.classList.contains('v0ck_hidden')) { if (settingsMenu && !settingsMenu.classList.contains('v0ck_hidden')) {
settingsMenu.classList.add('v0ck_hidden'); settingsMenu.classList.add('v0ck_hidden');
document.dispatchEvent(new CustomEvent('v0ck_settings_closed')); document.dispatchEvent(new CustomEvent('v0ck_settings_closed'));
@@ -950,7 +981,7 @@ class v0ck {
docMouseX = e.clientX; docMouseX = e.clientX;
docMouseY = e.clientY; docMouseY = e.clientY;
} }
player.classList.add('v0ck_hover'); setHover(true);
resetControlsTimer(); resetControlsTimer();
} }
@@ -998,7 +1029,7 @@ class v0ck {
} }
docMouseX = -1; docMouseX = -1;
docMouseY = -1; docMouseY = -1;
player.classList.remove('v0ck_hover'); setHover(false);
clearTimeout(controlsTimer); clearTimeout(controlsTimer);
}); });
+12 -1
View File
@@ -49,7 +49,18 @@ export default new class {
return slug; return slug;
} }
formatSize(size, i = ~~(Math.log(size) / Math.log(1024))) { formatSize(size) {
if (size === undefined || size === null || size === '') return '0 B';
if (typeof size === 'string') {
if (/^\d+(\.\d+)?\s*(B|kB|KB|MB|GB|TB)$/i.test(size.trim())) {
return size.trim();
}
const num = Number(size);
if (isNaN(num)) return '0 B';
size = num;
}
if (isNaN(size) || size <= 0) return '0 B';
const i = Math.min(4, Math.max(0, ~~(Math.log(size) / Math.log(1024))));
return (size / Math.pow(1024, i)).toFixed(2) * 1 + " " + ["B", "kB", "MB", "GB", "TB"][i]; return (size / Math.pow(1024, i)).toFixed(2) * 1 + " " + ["B", "kB", "MB", "GB", "TB"][i];
}; };
calcSpeed(b, s) { calcSpeed(b, s) {
+11 -1
View File
@@ -1492,6 +1492,16 @@ const f0cklib = {
} }
} }
let subSizeNum = r.size;
if (!subSizeNum || isNaN(subSizeNum) || Number(subSizeNum) <= 0) {
try {
const bFile = path.join(cfg.paths.b, r.dest);
if (fs.existsSync(bFile)) {
subSizeNum = fs.statSync(bFile).size;
}
} catch (_) {}
}
let subTags = tagsBySubId.get(r.id) || []; let subTags = tagsBySubId.get(r.id) || [];
if (subTags.length === 0 && (r.order_index === 0 || idx === 0)) { if (subTags.length === 0 && (r.order_index === 0 || idx === 0)) {
subTags = tags; subTags = tags;
@@ -1505,7 +1515,7 @@ const f0cklib = {
src: `${cfg.websrv.paths.images}/${r.dest}`, src: `${cfg.websrv.paths.images}/${r.dest}`,
filename: r.dest, filename: r.dest,
mime: r.mime, mime: r.mime,
size: lib.formatSize(r.size), size: lib.formatSize(subSizeNum),
width: r.width, width: r.width,
height: r.height, height: r.height,
checksum: r.checksum, checksum: r.checksum,
+125
View File
@@ -348,6 +348,131 @@ export default router => {
} }
}); });
const audioMetaCache = new Map();
group.get(/\/audio-metadata$/, async (req, res) => {
try {
const rawSrc = req.url.qs?.src || req.url.qs?.file || '';
const itemId = req.url.qs?.id || '';
if (!rawSrc && !itemId) {
return res.json({ success: false, msg: 'Missing src or id' }, 400);
}
let filename = '';
if (rawSrc) {
filename = path.basename(rawSrc.split('?')[0]);
}
if (filename && (filename.includes('/') || filename.includes('\\') || filename.includes('..'))) {
return res.json({ success: false, msg: 'Invalid filename' }, 400);
}
const cacheKey = filename || `item_${itemId}`;
if (audioMetaCache.has(cacheKey)) {
return res.json(audioMetaCache.get(cacheKey));
}
let fullPath = filename ? path.join(cfg.paths.b, filename) : null;
let originalFilename = '';
if (itemId) {
const isNumeric = /^\d+$/.test(itemId);
const rows = isNumeric
? await db`SELECT dest, original_filename, title FROM items WHERE id = ${itemId}`
: await db`SELECT dest, original_filename, title FROM items WHERE slug = ${itemId}`;
if (rows && rows[0]) {
if (!fullPath) fullPath = path.join(cfg.paths.b, rows[0].dest);
originalFilename = rows[0].original_filename || '';
} else {
const subRows = isNumeric
? await db`SELECT dest, original_filename FROM sub_items WHERE id = ${itemId}`
: await db`SELECT dest, original_filename FROM sub_items WHERE slug = ${itemId}`;
if (subRows && subRows[0]) {
if (!fullPath) fullPath = path.join(cfg.paths.b, subRows[0].dest);
originalFilename = subRows[0].original_filename || '';
}
}
}
if (!fullPath) {
return res.json({ success: false, msg: 'File not found' }, 404);
}
try {
await fs.access(fullPath);
} catch {
return res.json({ success: false, msg: 'File not found on disk' }, 404);
}
const metadata = await queue.getVideoMetadata(fullPath);
const tags = metadata || {};
// Normalize all tag keys to lowercase without punctuation (e.g. 'Title' -> 'title', 'ALBUM_ARTIST' -> 'albumartist')
const lowerTags = {};
for (const [k, v] of Object.entries(tags)) {
if (v != null && v !== '') {
const normKey = k.toLowerCase().replace(/[-_\s]/g, '');
lowerTags[normKey] = typeof v === 'string' ? v.trim() : String(v).trim();
}
}
let title = lowerTags.title ||
lowerTags.tracktitle ||
lowerTags.song ||
lowerTags.songname ||
lowerTags.name ||
'';
let artist = lowerTags.artist ||
lowerTags.albumartist ||
lowerTags.author ||
lowerTags.performer ||
lowerTags.creator ||
lowerTags.composer ||
lowerTags.byline ||
'';
let album = lowerTags.album ||
lowerTags.albumtitle ||
'';
let genre = lowerTags.genre || '';
let date = lowerTags.date || lowerTags.year || '';
if (!title && !artist) {
const nameToParse = originalFilename || path.parse(fullPath).name;
const match = nameToParse.match(/^(.+?)\s*[-–—_]\s*(.+)$/);
if (match) {
artist = match[1].replace(/^[0-9]+[\s._-]+/, '').trim();
title = match[2].trim();
} else if (nameToParse && !nameToParse.startsWith('subf0ck') && nameToParse.length < 60) {
title = nameToParse.trim();
}
}
const result = {
success: true,
artist: String(artist || '').trim(),
title: String(title || '').trim(),
album: String(album || '').trim(),
genre: String(genre || '').trim(),
date: String(date || '').trim(),
has_meta: !!(artist || title)
};
if (audioMetaCache.size > 1000) {
const firstKey = audioMetaCache.keys().next().value;
audioMetaCache.delete(firstKey);
}
audioMetaCache.set(cacheKey, result);
return res.json(result);
} catch (err) {
console.error('[API-V2-AUDIO-METADATA] Error:', err);
return res.json({ success: false, msg: 'Failed to extract audio metadata' }, 500);
}
});
// F-002 Security: Require authentication to prevent SSRF via arbitrary URL fetching. // F-002 Security: Require authentication to prevent SSRF via arbitrary URL fetching.
// Guests may read from the cache (in-memory or DB); only authenticated users trigger real outbound fetches. // Guests may read from the cache (in-memory or DB); only authenticated users trigger real outbound fetches.
group.get(/\/meta\/fetch$/, async (req, res) => { group.get(/\/meta\/fetch$/, async (req, res) => {
+8
View File
@@ -624,6 +624,9 @@ export default (router, tpl) => {
const itemQuery = await db` const itemQuery = await db`
SELECT SELECT
i.slug, i.slug,
i.mime,
i.dest,
i.has_coverart,
i.xd_score, i.xd_score,
COALESCE(i.visibility, 0) as visibility, COALESCE(i.visibility, 0) as visibility,
(SELECT ta.tag_id FROM tags_assign ta (SELECT ta.tag_id FROM tags_assign ta
@@ -684,6 +687,10 @@ export default (router, tpl) => {
db.notify('activity', JSON.stringify({ db.notify('activity', JSON.stringify({
user_id: req.session.id, user_id: req.session.id,
item_id: item_id, item_id: item_id,
item_slug: (getEnableItemSlugs() && itemQuery[0]?.slug) ? itemQuery[0].slug : null,
mime: itemQuery[0]?.mime || null,
dest: itemQuery[0]?.dest || null,
has_coverart: !!itemQuery[0]?.has_coverart,
type: 'comment', type: 'comment',
body: notifyBody, body: notifyBody,
id: commentId, id: commentId,
@@ -1104,6 +1111,7 @@ export default (router, tpl) => {
i.id as item_id, i.id as item_id,
i.slug as item_slug, i.slug as item_slug,
i.dest as item_dest, i.dest as item_dest,
i.has_coverart,
(SELECT ta.tag_id FROM tags_assign ta (SELECT ta.tag_id FROM tags_assign ta
WHERE ta.item_id = i.id AND ta.tag_id = ANY(${[1, 2, cfg.nsfl_tag_id || 3]}::int[]) WHERE ta.item_id = i.id AND ta.tag_id = ANY(${[1, 2, cfg.nsfl_tag_id || 3]}::int[])
ORDER BY ta.tag_id LIMIT 1) AS rating_tag_id, ORDER BY ta.tag_id LIMIT 1) AS rating_tag_id,
+5
View File
@@ -131,6 +131,9 @@
<button type="button" class="sidebar-tab" id="sidebar-tab-tag" data-tab="tag" title="Tag Feed" aria-label="Tag Feed" style="display: none;"> <button type="button" class="sidebar-tab" id="sidebar-tab-tag" data-tab="tag" title="Tag Feed" aria-label="Tag Feed" style="display: none;">
<i class="fa-solid fa-hashtag"></i> <i class="fa-solid fa-hashtag"></i>
</button> </button>
<button type="button" class="sidebar-tab" id="sidebar-tab-tuner" data-tab="tuner" title="Audio Reactivity Tuner" aria-label="Audio Reactivity Tuner">
<i class="fa-solid fa-sliders"></i>
</button>
</div> </div>
<div id="sidebar-activity-container" class="sidebar-comments-list sidebar-tab-content active" data-tab-content="comments"> <div id="sidebar-activity-container" class="sidebar-comments-list sidebar-tab-content active" data-tab-content="comments">
<div class="sidebar-loading-state"> <div class="sidebar-loading-state">
@@ -162,6 +165,8 @@
</div> </div>
</div> </div>
</div> </div>
<div id="sidebar-tuner-container" class="sidebar-tuner-container sidebar-tab-content" data-tab-content="tuner" style="display: none;">
</div>
</div> </div>
<div class="global-sidebar-right-footer"> <div class="global-sidebar-right-footer">
<a href="/ranking">{{ t('footer.ranking') }}</a> <a href="/ranking">{{ t('footer.ranking') }}</a>
+22 -7
View File
@@ -8,12 +8,19 @@
<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%;"> <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: #000;">
<div class="sidebar-media-placeholder audio"> <div class="sidebar-media-placeholder audio">
<div class="audio-cover-circle"></div> <div class="audio-track-info-badge">
<i class="fa-solid fa-music"></i> <div class="audio-track-info-details">
<span class="audio-track-artist"></span>
<span class="audio-track-title"></span>
</div>
</div>
<div class="audio-cover-circle">
<i class="fa-solid fa-music"></i>
</div>
</div> </div>
<audio id="f0ck-album-audio" class="embed-responsive-item" preload="auto" loop></audio> <audio id="f0ck-album-audio" class="embed-responsive-item" preload="auto" loop crossorigin="anonymous"></audio>
</div> </div>
</div> </div>
<!-- Album Gallery Controls Inside View --> <!-- Album Gallery Controls Inside View -->
@@ -59,10 +66,18 @@
<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"> <div class="embed-responsive embed-responsive-16by9" style="background: #000;">
<div class="sidebar-media-placeholder audio @if(item.coverart)has-cover@endif"> <div class="sidebar-media-placeholder audio @if(item.coverart)has-cover@endif">
<div class="audio-cover-circle" @if(item.coverart)style="background-image: url('{{ item.coverart }}');"@endif></div> <div class="audio-reactive-bg"></div>
<i class="fa-solid fa-music"></i> <div class="audio-track-info-badge">
<div class="audio-track-info-details">
<span class="audio-track-artist"></span>
<span class="audio-track-title"></span>
</div>
</div>
<div class="audio-cover-circle" @if(item.coverart)style="background-image: url('{{ item.coverart }}');"@endif>
<i class="fa-solid fa-music"></i>
</div>
</div> </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> <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;"> <img id="f0ck-audio-cover" @if(item.coverart)src="{{ item.coverart }}"@endif style="display: none;">