diff --git a/public/s/css/f0ckm.css b/public/s/css/f0ckm.css
index 9bcb4ea..3910eb1 100644
--- a/public/s/css/f0ckm.css
+++ b/public/s/css/f0ckm.css
@@ -701,39 +701,96 @@ html[theme='paper'] .metadata table th {
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 svg.iconset,
html[theme='light'] .gapRight > .iconset,
+html[theme='light'] .gapRight button,
html[theme='paper'] .gapRight i.iconset,
html[theme='paper'] .gapRight svg.iconset,
-html[theme='paper'] .gapRight > .iconset {
- color: #000 !important;
- fill: #000 !important;
- background: #f1f5f9 !important;
+html[theme='paper'] .gapRight > .iconset,
+html[theme='paper'] .gapRight button,
+[theme='light'] .gapRight i.iconset,
+[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-radius: 4px;
+ border-radius: 3px !important;
+ color: #334155 !important;
+ fill: #334155 !important;
}
html[theme='light'] .gapRight i.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 svg.iconset:hover {
- background: #e2e8f0 !important;
- color: #000 !important;
- fill: #000 !important;
+html[theme='paper'] .gapRight svg.iconset:hover,
+html[theme='paper'] .gapRight > .iconset:hover,
+html[theme='paper'] .gapRight button:hover,
+[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='paper'] .gapRight #a_favo {
+html[theme='light'] .gapRight #a_favo.fa-solid,
+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;
fill: #e11d48 !important;
+ border-color: rgba(225, 29, 72, 0.4) !important;
}
html[theme='light'] i.iconset#a_oc,
html[theme='paper'] i.iconset#a_oc {
- color: #000 !important;
- fill: #000 !important;
+ color: inherit !important;
}
/* a_addtag and tag controls */
@@ -3155,23 +3212,41 @@ body.sidebar-right-hidden #sidebar-drag-zone {
}
.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);
- 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 {
- 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 {
- display: none;
+ display: flex;
+ align-items: center;
+ justify-content: center;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
- width: 155px;
- height: 155px;
+ width: 125px;
+ height: 125px;
max-width: calc(100% - 40px);
max-height: calc(100% - 40px);
aspect-ratio: 1 / 1;
@@ -3179,31 +3254,63 @@ body.sidebar-right-hidden #sidebar-drag-zone {
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;
+ background-color: #000000;
+ background-image: none;
+ 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;
z-index: 1;
will-change: transform, box-shadow, border-color;
}
-.audio-cover-circle::after {
- content: '';
+.audio-eye-inner-canvas {
position: absolute;
- inset: 0;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -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;
+ z-index: 2;
}
-.sidebar-media-placeholder.audio.has-cover .audio-cover-circle {
- display: block;
+.audio-cover-circle i {
+ 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;
}
+.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 {
display: none !important;
}
@@ -3233,6 +3340,13 @@ body.sidebar-right-hidden #sidebar-drag-zone {
pointer-events: none;
font-size: clamp(2rem, 5.5vw, 3.8rem);
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,
@@ -3252,6 +3366,13 @@ body.sidebar-right-hidden #sidebar-drag-zone {
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 {
position: relative;
z-index: 2;
@@ -3311,6 +3432,50 @@ body.sidebar-right-hidden #sidebar-drag-zone {
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 {
display: flex;
justify-content: space-between;
@@ -3431,6 +3596,126 @@ body.sidebar-right-hidden #sidebar-drag-zone {
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 {
display: flex;
margin-top: 8px;
@@ -6725,8 +7010,10 @@ body.layout-modern.sidebar-right-hidden .pagination-container-fluid {
.gapRight {
align-items: center;
- display: flex;
-
+ display: inline-flex;
+ gap: 6px;
+ flex-wrap: wrap;
+ vertical-align: middle;
}
.admin-lock-btn {
@@ -6938,7 +7225,8 @@ body:has(.index-container) > .pagewrapper > #main {
overflow-y: auto !important;
overflow-x: hidden !important;
-webkit-overflow-scrolling: touch;
- overscroll-behavior-y: contain;
+ overscroll-behavior-y: auto;
+ overscroll-behavior-x: contain;
box-sizing: border-box;
padding: 5px 20px 75px 20px;
margin: 0;
@@ -7772,7 +8060,7 @@ a.removetag i {
background: transparent;
border: none;
padding: 0;
- margin: 0;
+ margin: 0 0 0 5px;
cursor: pointer;
color: inherit;
display: inline-flex;
@@ -7783,44 +8071,15 @@ a.removetag i {
vertical-align: middle;
border-radius: 50%;
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);
}
-@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:focus-visible {
opacity: 1;
@@ -7845,12 +8104,6 @@ a.removetag i {
.tag-badge.tag-is-excluded .tag-exclude-btn {
opacity: 0.95;
- width: 14px;
- max-width: 14px;
- margin-left: 5px;
- overflow: visible;
- pointer-events: auto;
- transform: scale(1);
color: #ff4757;
}
@@ -9674,15 +9927,18 @@ table img {
}
}
-/* gapRight icon colors: white for all, pink for favorites */
-.gapRight i.iconset {
- color: #fff !important;
- fill: #fff !important;
+/* gapRight icon colors & active states */
+.gapRight i.iconset,
+.gapRight svg.iconset,
+.gapRight button {
+ color: var(--white, #fff);
+ fill: var(--white, #fff);
}
-.gapRight #a_favo {
+.gapRight #a_favo.fa-solid {
color: #ff6b9d !important;
fill: #ff6b9d !important;
+ border-color: rgba(255, 107, 157, 0.4) !important;
}
/* pin icon */
@@ -9701,10 +9957,6 @@ table img {
transform: rotate(-45deg);
}
-.iconset#subscribe-btn {
- border-style: outset;
-}
-
.imageDoor:hover:after {
content: "";
background-image: var(--hover-image);
@@ -14695,6 +14947,42 @@ body.layout-modern .tag-ac-input {
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
═══════════════════════════════════════════════════════════ */
@@ -16347,37 +16635,109 @@ span.gchat-post-card--loading {
.gapRight,
body.layout-modern .gapRight,
body.layout-legacy .gapRight {
+ display: inline-flex;
+ align-items: center;
justify-content: center;
+ gap: 6px;
+ flex-wrap: wrap;
+ vertical-align: middle;
}
.gapRight>svg.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>i.iconset,
+body.layout-modern .gapRight>button,
+body.layout-modern .gapRight>.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;
+ min-width: 30px;
width: 30px;
- margin: 5px;
- padding: 5px;
- right: 0px;
- background: var(--badge-tag);
+ padding: 0;
+ box-sizing: border-box;
display: inline-flex;
align-items: center;
justify-content: center;
- font-size: 18px;
- box-sizing: border-box;
+ gap: 5px;
+ 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,
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 {
- color: #fff !important;
- fill: #fff !important;
+ color: inherit;
+ fill: inherit;
}
/* OC Feature */
@@ -20246,7 +20606,8 @@ body.onara-modal-open .admin-bar.open {
overflow-x: hidden !important;
-webkit-overflow-scrolling: touch;
scrollbar-width: none !important;
- overscroll-behavior: contain !important;
+ overscroll-behavior-y: auto !important;
+ overscroll-behavior-x: contain !important;
box-sizing: border-box !important;
opacity: 0;
transform: scale(0.99);
@@ -21026,6 +21387,8 @@ div#flash,
.album-gallery-container {
position: relative;
overflow: hidden;
+ max-width: 100%;
+ box-sizing: border-box;
}
.album-active-image {
@@ -21082,7 +21445,8 @@ div#flash,
top: 14px;
left: 50%;
transform: translateX(-50%);
- pointer-events: auto;
+ pointer-events: none;
+ opacity: 0;
background: rgba(16, 16, 20, 0.75);
border: 1px solid rgba(255, 255, 255, 0.2);
backdrop-filter: blur(12px);
@@ -21098,6 +21462,24 @@ div#flash,
box-shadow: 0 4px 14px rgba(0, 0, 0, 0.4);
letter-spacing: 0.03em;
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 {
@@ -21111,6 +21493,7 @@ div#flash,
left: 50%;
transform: translateX(-50%) translateY(10px);
max-width: calc(100% - 32px);
+ box-sizing: border-box;
display: flex;
align-items: center;
gap: 8px;
@@ -21122,16 +21505,101 @@ div#flash,
border-radius: 12px;
z-index: 22;
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;
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.45);
opacity: 0;
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);
}
-.album-gallery-container:hover .album-thumbnails-strip,
-.album-gallery-container:focus-within .album-thumbnails-strip,
-.album-thumbnails-strip.is-visible {
+.album-thumbnails-strip.is-dragging {
+ cursor: grabbing !important;
+ 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;
pointer-events: auto;
transform: translateX(-50%) translateY(0);
@@ -21141,8 +21609,7 @@ div#flash,
display: none;
}
-.album-video-wrapper,
-.album-audio-wrapper {
+.album-video-wrapper {
position: absolute;
inset: 0;
width: 100%;
@@ -21150,6 +21617,14 @@ div#flash,
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-audio-active .album-thumbnails-strip {
bottom: 58px;
@@ -21180,6 +21655,9 @@ div#flash,
padding: 0;
margin: 0;
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);
}
@@ -21261,5 +21739,20 @@ div#flash,
bottom: 8px;
padding: 4px 8px;
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;
}
}
\ No newline at end of file
diff --git a/public/s/css/v0ck.css b/public/s/css/v0ck.css
index 2b09610..8153898 100644
--- a/public/s/css/v0ck.css
+++ b/public/s/css/v0ck.css
@@ -6,7 +6,7 @@
background-size: contain;
background-repeat: no-repeat;
background-position: center center;
- touch-action: none; /* Prevent pull-to-refresh and scroll while interacting */
+ touch-action: pan-y;
z-index: 0;
}
@@ -556,8 +556,8 @@
transform: translate(-50%, 0);
}
-/* Hide mouse cursor on inactivity in fullscreen */
-.v0ck.v0ck_fullscreen:not(.v0ck_hover),
-.v0ck.v0ck_fullscreen:not(.v0ck_hover) * {
+/* Hide mouse cursor on inactivity when player controls auto-hide */
+.v0ck:not(.v0ck_hover),
+.v0ck:not(.v0ck_hover) * {
cursor: none !important;
}
\ No newline at end of file
diff --git a/public/s/js/comments.js b/public/s/js/comments.js
index b12ebc4..a9a7387 100644
--- a/public/s/js/comments.js
+++ b/public/s/js/comments.js
@@ -27,6 +27,8 @@ class CommentSystem {
if (this.displayMode === 1) this.sort = 'old';
this.customEmojis = CommentSystem.emojiCache || {};
+ this._selfPostedCommentIds = new Set();
+ this._pendingSelfCommentTexts = new Set();
this.icons = {
reply: ``,
@@ -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) {
if (!this.container || !this.itemId) return;
// 1. Check if comment belongs to this item
@@ -537,14 +567,20 @@ class CommentSystem {
// Danmaku: fire one-shot for other users' comments.
// Own comment is handled by the optimistic submit path (fire + addItem).
- // The _loadDanmaku re-render will add this comment to the items rotation.
- const currentUser = window.f0ckSession?.user;
- if (window.danmakuInstance && data.username !== currentUser) {
+ const isSelf = this._isSelfComment(data);
+ if (window.danmakuInstance && !isSelf) {
window.danmakuInstance.fire(
data.body,
data.display_name || data.username || '?',
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
@@ -3448,6 +3484,7 @@ class CommentSystem {
} else {
this.isMainSubmitting = true;
}
+ this._pendingSelfCommentTexts.add(text);
let retryCount = 0;
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)
const json = await res.json().catch(() => ({}));
alert('Error: ' + (json.message || `Status ${res.status}`));
- this._finishSubmit(submitBtn, originalBtnHtml, parentId);
+ this._finishSubmit(submitBtn, originalBtnHtml, parentId, text);
return;
}
@@ -3692,6 +3729,11 @@ class CommentSystem {
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
if (window.danmakuInstance) {
window.danmakuInstance.fire(
@@ -3700,6 +3742,7 @@ class CommentSystem {
session.username_color || null
);
window.danmakuInstance.addItem({
+ id: newComment.id,
content: text,
video_time: newComment.video_time ?? null,
display_name: session.display_name || currentUsername || '?',
@@ -3810,10 +3853,10 @@ class CommentSystem {
}
this._silentSync();
- this._finishSubmit(submitBtn, originalBtnHtml, parentId);
+ this._finishSubmit(submitBtn, originalBtnHtml, parentId, text);
} else {
alert('Error: ' + json.message);
- this._finishSubmit(submitBtn, originalBtnHtml, parentId);
+ this._finishSubmit(submitBtn, originalBtnHtml, parentId, text);
}
} catch (err) {
console.warn(`[CommentSystem] Submit attempt ${retryCount + 1} failed:`, err);
@@ -3825,7 +3868,7 @@ class CommentSystem {
setTimeout(attemptSubmit, delay);
} else {
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'));
}
- _finishSubmit(btn, originalHtml, parentId) {
+ _finishSubmit(btn, originalHtml, parentId, submittedText = null) {
+ if (submittedText) {
+ this._pendingSelfCommentTexts?.delete(submittedText);
+ }
if (parentId) {
this.pendingSubmissions.delete(parentId);
} else {
diff --git a/public/s/js/danmaku.js b/public/s/js/danmaku.js
index a527abf..10078d7 100644
--- a/public/s/js/danmaku.js
+++ b/public/s/js/danmaku.js
@@ -195,6 +195,7 @@ class Danmaku {
const mapped = comments
.filter(c => !c.is_deleted && c.content)
.map(c => ({
+ id: c.id || null,
text: this._prepareText(c.content),
username: c.display_name || c.username || '?',
color: c.username_color || null,
@@ -288,6 +289,7 @@ class Danmaku {
*/
addItem(comment) {
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 hasDuration = isFinite(duration) && duration > 0;
@@ -307,6 +309,7 @@ class Danmaku {
}
const item = {
+ id: comment.id || null,
video_time: t,
text: this._prepareText(comment.content),
username: comment.display_name || comment.username || '?',
@@ -314,6 +317,13 @@ class Danmaku {
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
const idx = this.items.findIndex(i => i.video_time > t);
if (idx === -1) this.items.push(item);
diff --git a/public/s/js/f0ckm.js b/public/s/js/f0ckm.js
index 9e8fc65..43c8da6 100644
--- a/public/s/js/f0ckm.js
+++ b/public/s/js/f0ckm.js
@@ -393,7 +393,14 @@ window.cancelAnimFrame = (function () {
if (!coverCircle) {
coverCircle = document.createElement('div');
coverCircle.className = 'audio-cover-circle';
+ coverCircle.innerHTML = '';
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', '');
+ }
}
coverCircle.style.backgroundImage = `url('${coverUrl}')`;
ph.classList.add('has-cover');
@@ -406,12 +413,24 @@ window.cancelAnimFrame = (function () {
} else if (hasCoverart === false) {
audioCover.removeAttribute('src');
if (parent) {
- parent.style.background = 'none';
+ parent.style.background = '#000000';
if (ph) {
- if (coverCircle) {
+ if (!coverCircle) {
+ coverCircle = document.createElement('div');
+ coverCircle.className = 'audio-cover-circle';
+ coverCircle.innerHTML = '';
+ 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', '');
+ }
coverCircle.style.backgroundImage = 'none';
+ coverCircle.style.backgroundColor = '#000000';
}
ph.classList.remove('has-cover');
+ ph.style.backgroundColor = '#000000';
ph.style.display = '';
}
}
@@ -2807,6 +2826,8 @@ window.cancelAnimFrame = (function () {
const idx = parseInt(thumb.getAttribute('data-index'), 10);
const src = thumb.getAttribute('data-src') || thumb.querySelector('img')?.src || '';
const subSlug = thumb.getAttribute('data-subf0ck-slug') || thumb.getAttribute('data-subf0ck-id') || '';
+ const mime = (thumb.getAttribute('data-mime') || '').toLowerCase();
+ const isAudio = mime.startsWith('audio/') || !!thumb.querySelector('.sidebar-media-placeholder.audio');
return {
order_index: isNaN(idx) ? i : idx,
display_index: (isNaN(idx) ? i : idx) + 1,
@@ -2814,7 +2835,10 @@ window.cancelAnimFrame = (function () {
dest: src,
slug: subSlug,
subf0ck_id: subSlug,
- id: subSlug
+ id: subSlug,
+ mime: mime || (isAudio ? 'audio/mpeg' : ''),
+ is_audio: isAudio,
+ is_video: mime.startsWith('video/')
};
});
}
@@ -2857,6 +2881,17 @@ window.cancelAnimFrame = (function () {
const thumbItems = container.querySelectorAll('.album-thumb-item');
const stripEl = container.querySelector('.album-thumbnails-strip');
+ let stripTimeout = null;
+ function showStripPeek(duration = 3000) {
+ if (stripTimeout) clearTimeout(stripTimeout);
+ container.classList.add('strip-peek');
+ if (stripEl) stripEl.classList.add('strip-peek');
+ stripTimeout = setTimeout(() => {
+ container.classList.remove('strip-peek');
+ if (stripEl) stripEl.classList.remove('strip-peek');
+ }, duration);
+ }
+
if (totalCountEl) totalCountEl.textContent = albumData.length;
let albumVideoV0ck = null;
@@ -2958,6 +2993,8 @@ window.cancelAnimFrame = (function () {
if (mime.startsWith('video/')) {
container.classList.add('is-video-active');
container.classList.remove('is-audio-active');
+ const badge = container.querySelector('.audio-track-info-badge');
+ if (badge) badge.classList.remove('is-visible');
if (imgEl) imgEl.style.display = 'none';
if (linkEl) linkEl.style.display = 'none';
if (audioWrapper) audioWrapper.style.display = 'none';
@@ -3010,29 +3047,39 @@ window.cancelAnimFrame = (function () {
if (!ph) {
ph = document.createElement('div');
ph.className = 'sidebar-media-placeholder audio';
- ph.innerHTML = '';
+ ph.innerHTML = '
';
audioWrapper.prepend(ph);
}
let coverCircle = ph.querySelector('.audio-cover-circle');
if (!coverCircle) {
coverCircle = document.createElement('div');
coverCircle.className = 'audio-cover-circle';
+ coverCircle.innerHTML = '';
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', '');
+ }
}
audioWrapper.style.backgroundImage = 'none';
- audioWrapper.style.backgroundColor = 'transparent';
+ audioWrapper.style.backgroundColor = '#000000';
if (coverUrl) {
coverCircle.style.backgroundImage = `url('${coverUrl}')`;
ph.classList.add('has-cover');
} else {
coverCircle.style.backgroundImage = 'none';
+ coverCircle.style.backgroundColor = '#000000';
ph.classList.remove('has-cover');
+ ph.style.backgroundColor = '#000000';
}
ph.style.display = '';
}
if (audioEl) {
audioEl.style.display = 'block';
+ audioEl.crossOrigin = 'anonymous';
const hasCover = item.has_coverart && item.coverart && !item.coverart.includes('audio.webp');
const coverUrl = hasCover ? (item.coverart || item.thumb) : null;
if (coverUrl) {
@@ -3043,13 +3090,15 @@ window.cancelAnimFrame = (function () {
if (audioEl.src !== newSrc && !audioEl.src.endsWith(newSrc)) {
audioEl.src = newSrc;
+ audioEl.setAttribute('src', newSrc);
audioEl.load();
}
initAlbumAudioV0ck();
video = audioEl;
- if (window.initVisualizer) window.initVisualizer();
+ if (window.initVisualizer) window.initVisualizer(audioEl);
+ if (window.updateAudioTrackBadge) window.updateAudioTrackBadge(audioEl, item.id || item.slug, newSrc);
audioEl.addEventListener('play', () => audioWrapper?.classList.add('is-playing'));
audioEl.addEventListener('pause', () => audioWrapper?.classList.remove('is-playing'));
@@ -3076,6 +3125,8 @@ window.cancelAnimFrame = (function () {
} else {
// Image
container.classList.remove('is-video-active', 'is-audio-active');
+ const badge = container.querySelector('.audio-track-info-badge');
+ if (badge) badge.classList.remove('is-visible');
if (videoEl) {
try { videoEl.pause(); } catch {}
}
@@ -3109,11 +3160,24 @@ window.cancelAnimFrame = (function () {
thumbItems.forEach((btn, idx) => {
const isActive = idx === currentIndex;
btn.classList.toggle('active', isActive);
- if (isActive) {
- btn.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'center' });
+ if (isActive && stripEl && stripEl.clientWidth > 0) {
+ const stripRect = stripEl.getBoundingClientRect();
+ const btnRect = btn.getBoundingClientRect();
+ const offsetWithinStrip = (btnRect.left - stripRect.left) + stripEl.scrollLeft;
+ const targetLeft = offsetWithinStrip - (stripEl.clientWidth - btn.clientWidth) / 2;
+ stripEl.scrollTo({
+ left: Math.max(0, targetLeft),
+ behavior: 'smooth'
+ });
}
});
+ if (window.scrollX > 0) {
+ window.scrollTo(0, window.scrollY);
+ }
+
+ showStripPeek(3000);
+
updateInfoModal();
updateAlbumTags();
@@ -3176,7 +3240,16 @@ window.cancelAnimFrame = (function () {
const fileSizeEl = modal.querySelector('#info-file-size');
if (fileSizeEl) {
- fileSizeEl.textContent = sub.size || '';
+ let displaySize = sub.size || '';
+ if (displaySize === 'NaN B' || displaySize === 'NaN') displaySize = '';
+ if (displaySize && !isNaN(displaySize)) {
+ const num = Number(displaySize);
+ if (num > 0) {
+ const i = Math.min(4, Math.max(0, ~~(Math.log(num) / Math.log(1024))));
+ displaySize = (num / Math.pow(1024, i)).toFixed(2) * 1 + ' ' + ['B', 'kB', 'MB', 'GB', 'TB'][i];
+ }
+ }
+ fileSizeEl.textContent = displaySize;
}
const dimsCard = modal.querySelector('#info-card-dimensions');
@@ -3260,8 +3333,157 @@ window.cancelAnimFrame = (function () {
});
}
+ let hasDraggedStrip = false;
+ if (stripEl && !stripEl._dragInitialized) {
+ stripEl._dragInitialized = true;
+ let isDown = false;
+ let startX = 0;
+ let startScrollLeft = 0;
+ let momentumID = null;
+ let velX = 0;
+ let lastX = 0;
+ let lastTime = 0;
+
+ stripEl.addEventListener('mousedown', (e) => {
+ if (e.button !== 0) return;
+ isDown = true;
+ hasDraggedStrip = false;
+ startX = e.pageX;
+ startScrollLeft = stripEl.scrollLeft;
+ lastX = e.pageX;
+ lastTime = performance.now();
+ velX = 0;
+ cancelAnimationFrame(momentumID);
+ });
+
+ window.addEventListener('mousemove', (e) => {
+ if (!isDown) return;
+ const now = performance.now();
+ const walk = e.pageX - startX;
+ if (Math.abs(walk) > 6) {
+ hasDraggedStrip = true;
+ stripEl.classList.add('is-dragging');
+ e.preventDefault();
+ stripEl.scrollLeft = startScrollLeft - walk;
+
+ const dt = Math.max(1, now - lastTime);
+ velX = (e.pageX - lastX) / dt;
+ lastX = e.pageX;
+ lastTime = now;
+ }
+ });
+
+ window.addEventListener('mouseup', () => {
+ if (!isDown) return;
+ isDown = false;
+ stripEl.classList.remove('is-dragging');
+ showStripPeek(6000);
+
+ if (hasDraggedStrip && Math.abs(velX) > 0.15) {
+ let currentVel = velX * 18;
+ const glide = () => {
+ if (Math.abs(currentVel) > 0.5) {
+ stripEl.scrollLeft -= currentVel;
+ currentVel *= 0.93;
+ momentumID = requestAnimationFrame(glide);
+ }
+ };
+ momentumID = requestAnimationFrame(glide);
+ }
+
+ setTimeout(() => {
+ hasDraggedStrip = false;
+ }, 100);
+ });
+
+ // Touch drag / swipe support for mobile
+ let touchStartX = 0;
+ let touchStartScrollLeft = 0;
+ let isTouchingStrip = false;
+
+ stripEl.addEventListener('touchstart', (e) => {
+ if (e.touches.length === 1) {
+ e.stopPropagation();
+ if (stripTimeout) clearTimeout(stripTimeout);
+ container.classList.add('strip-peek');
+ stripEl.classList.add('strip-peek');
+ isTouchingStrip = true;
+ hasDraggedStrip = false;
+ touchStartX = e.touches[0].clientX;
+ touchStartScrollLeft = stripEl.scrollLeft;
+ lastX = e.touches[0].clientX;
+ lastTime = performance.now();
+ velX = 0;
+ cancelAnimationFrame(momentumID);
+ }
+ }, { passive: true });
+
+ stripEl.addEventListener('touchmove', (e) => {
+ if (!isTouchingStrip || e.touches.length !== 1) return;
+ e.stopPropagation();
+ const currentX = e.touches[0].clientX;
+ const walk = currentX - touchStartX;
+ if (Math.abs(walk) > 3) {
+ hasDraggedStrip = true;
+ stripEl.classList.add('is-dragging');
+ stripEl.scrollLeft = touchStartScrollLeft - walk;
+ const now = performance.now();
+ const dt = Math.max(1, now - lastTime);
+ velX = (currentX - lastX) / dt;
+ lastX = currentX;
+ lastTime = now;
+ }
+ }, { passive: true });
+
+ stripEl.addEventListener('touchend', (e) => {
+ if (!isTouchingStrip) return;
+ e.stopPropagation();
+ isTouchingStrip = false;
+ stripEl.classList.remove('is-dragging');
+ showStripPeek(6000);
+ if (hasDraggedStrip) {
+ if (Math.abs(velX) > 0.05) {
+ let currentVel = velX * 24;
+ const glide = () => {
+ if (Math.abs(currentVel) > 0.5) {
+ stripEl.scrollLeft -= currentVel;
+ currentVel *= 0.93;
+ momentumID = requestAnimationFrame(glide);
+ }
+ };
+ momentumID = requestAnimationFrame(glide);
+ }
+ setTimeout(() => {
+ hasDraggedStrip = false;
+ }, 150);
+ }
+ }, { passive: true });
+
+ stripEl.addEventListener('touchcancel', (e) => {
+ e.stopPropagation();
+ isTouchingStrip = false;
+ stripEl.classList.remove('is-dragging');
+ showStripPeek(6000);
+ setTimeout(() => {
+ hasDraggedStrip = false;
+ }, 100);
+ }, { passive: true });
+
+ stripEl.addEventListener('wheel', (e) => {
+ if (e.deltaY !== 0) {
+ e.preventDefault();
+ stripEl.scrollLeft += e.deltaY;
+ }
+ }, { passive: false });
+ }
+
thumbItems.forEach((btn) => {
btn.addEventListener('click', (e) => {
+ if (hasDraggedStrip) {
+ e.preventDefault();
+ e.stopPropagation();
+ return;
+ }
e.preventDefault();
e.stopPropagation();
const targetIdx = parseInt(btn.getAttribute('data-index'), 10);
@@ -3272,29 +3494,31 @@ window.cancelAnimFrame = (function () {
});
// Touch tap on container reveals thumbnail strip briefly on touch devices
- let stripTimeout = null;
container.addEventListener('click', (e) => {
- if (stripEl && !e.target.closest('.album-thumbnails-strip, .v0ck_player_controls, .v0ck_settings_menu, .v0ck_hud, .album-btn')) {
- stripEl.classList.add('strip-peek');
- if (stripTimeout) clearTimeout(stripTimeout);
- stripTimeout = setTimeout(() => {
- stripEl.classList.remove('strip-peek');
- }, 3000);
+ if (!e.target.closest('.v0ck_settings_menu, .v0ck_hud, .album-btn')) {
+ showStripPeek(3000);
}
});
// Handle touch swipe for mobile gallery navigation
let touchStartX = null;
let touchStartY = null;
+ let touchIgnored = false;
container.addEventListener('touchstart', (e) => {
if (e.touches.length === 1) {
+ touchIgnored = !!e.target.closest('.album-thumbnails-strip, .v0ck_player_controls, .v0ck_settings_menu, .v0ck_hud, .album-btn');
touchStartX = e.touches[0].clientX;
touchStartY = e.touches[0].clientY;
}
}, { passive: true });
container.addEventListener('touchend', (e) => {
- if (touchStartX === null || touchStartY === null) return;
+ if (touchStartX === null || touchStartY === null || touchIgnored) {
+ touchStartX = null;
+ touchStartY = null;
+ touchIgnored = false;
+ return;
+ }
const touchEndX = e.changedTouches[0].clientX;
const touchEndY = e.changedTouches[0].clientY;
const diffX = touchEndX - touchStartX;
@@ -3310,6 +3534,7 @@ window.cancelAnimFrame = (function () {
}
touchStartX = null;
touchStartY = null;
+ touchIgnored = false;
}, { passive: true });
// Handle hash change if user navigates back/forward to another subf0ck
@@ -3672,16 +3897,70 @@ window.cancelAnimFrame = (function () {
// Audio Visualizer Reactivity Tuner
const DEFAULT_AUDIO_TUNING = {
+ useCustomColor: 0,
+ visualizerColor: "#241f31",
+ enableBeatHue: 0,
+ beatHueThreshold: 0.3,
+ beatHueStep: 45,
+ beatHueSmooth: 0.65,
+ beatHueIdleDrift: 0.2,
+ beatHueCooldown: 150,
+ coverSize: 110,
+ glowIntensity: 500,
+ coverGlowBase: 128,
+ glowBrightness: 2.05,
+ glowSensitivity: 1,
+ glowDynamism: 0.85,
+ glowSmoothness: 0.95,
+ solidCover: 1,
+ coverColor: "#000000",
+ coverOpacity: 0,
bassGain: 0.5,
- bassPower: 2.0,
- scaleBounce: 0.4,
- bounceBoost: 2.5,
- glowIntensity: 250,
- attackSpeed: 1.0,
+ bassPower: 0.3,
+ scaleBounce: 0.85,
+ bounceBoost: 1,
+ attackSpeed: 1,
releaseSpeed: 0.6,
- coverSize: 155,
- barHeight: 0.4,
- smoothing: 0.84
+ enableBars: 0,
+ enableInnerBars: 1,
+ useCustomInnerColor: 0,
+ innerBarColor: "#ff0000",
+ innerBarsMode: 4,
+ innerRadius: 150,
+ innerPupilRadius: 46,
+ innerPupilRingOpacity: 0,
+ innerBarCount: 73,
+ innerBarWidth: 80,
+ innerBarHeight: 2.65,
+ innerBarOpacity: 0.13,
+ outerRingOpacity: 0,
+ innerHighBoost: 0.162,
+ innerRadialRotation: 90,
+ innerBarGlow: 0,
+ innerHideNote: 1,
+ barHeight: 0.9,
+ barWidth: 34,
+ barGap: 9,
+ barRadius: 12,
+ barOpacity: 0.1,
+ barGlow: 0,
+ smoothing: 0.86,
+ followMouse: 1,
+ followSpeed: 0.1,
+ followRadius: 15,
+ tiltEffect: 1,
+ glowAttack: 1,
+ glowDecay: 0.5,
+ enableBgGradient: 1,
+ bgGradientStyle: 1,
+ bgGradientOpacity: 0.4,
+ bgGradientReactivity: 1.2,
+ bgGradientSpread: 75,
+ useCustomBgColor: 0,
+ bgGradientColor: "#3a1c71",
+ enableBlink: 0,
+ blinkInterval: 12,
+ beatHueSpeed: 0.1
};
let savedTuning = null;
@@ -3692,29 +3971,256 @@ window.cancelAnimFrame = (function () {
window.audioVisualizerTuning = Object.assign({}, DEFAULT_AUDIO_TUNING, savedTuning || {});
+ // Eye Cursor Follower & 3D Tracking
+ const mousePos = { x: null, y: null, active: false };
+ let isEyeActive = false;
+ let eyeInactivityTimer = null;
+
+ const isCursorNearV0ck = (e) => {
+ const margin = 10;
+ const players = document.querySelectorAll('.v0ck, .sidebar-media-placeholder.audio');
+ for (const p of players) {
+ const r = p.getBoundingClientRect();
+ if (r.width > 0 && r.height > 0 &&
+ e.clientX >= r.left - margin && e.clientX <= r.right + margin &&
+ e.clientY >= r.top - margin && e.clientY <= r.bottom + margin) {
+ return true;
+ }
+ }
+ return false;
+ };
+
+ const handlePointerMove = (e) => {
+ mousePos.x = e.clientX;
+ mousePos.y = e.clientY;
+
+ const nearPlayer = isCursorNearV0ck(e);
+ if (nearPlayer) {
+ isEyeActive = true;
+ mousePos.active = true;
+ clearTimeout(eyeInactivityTimer);
+ eyeInactivityTimer = setTimeout(() => {
+ isEyeActive = false;
+ mousePos.active = false;
+ }, 2500);
+ } else {
+ // More than 10px outside of player: stop following and return to center
+ isEyeActive = false;
+ mousePos.active = false;
+ clearTimeout(eyeInactivityTimer);
+ }
+ };
+
+ window.addEventListener('pointermove', handlePointerMove, { passive: true });
+ window.addEventListener('pointerdown', (e) => {
+ if (isCursorNearV0ck(e)) handlePointerMove(e);
+ }, { passive: true });
+
+ document.addEventListener('mouseleave', () => {
+ isEyeActive = false;
+ mousePos.active = false;
+ clearTimeout(eyeInactivityTimer);
+ });
+
+ window.addEventListener('pointerup', () => {
+ if (window.matchMedia && window.matchMedia('(hover: none)').matches) {
+ isEyeActive = false;
+ mousePos.active = false;
+ clearTimeout(eyeInactivityTimer);
+ }
+ }, { passive: true });
+
+ const updateEyeTracking = () => {
+ const cfg = window.audioVisualizerTuning || DEFAULT_AUDIO_TUNING;
+ const isEnabled = cfg.followMouse !== undefined ? cfg.followMouse === 1 : true;
+ const speed = cfg.followSpeed !== undefined ? cfg.followSpeed : 0.04;
+ const allowTilt = cfg.tiltEffect !== undefined ? cfg.tiltEffect === 1 : true;
+
+ const circles = document.querySelectorAll('.sidebar-media-placeholder.audio .audio-cover-circle');
+ circles.forEach(circle => {
+ const ph = circle.closest('.sidebar-media-placeholder.audio');
+ if (!ph) return;
+ const rect = ph.getBoundingClientRect();
+ if (rect.width === 0 || rect.height === 0) return;
+
+ const v0ckPlayer = ph.closest('.v0ck') || ph;
+ const pRect = v0ckPlayer.getBoundingClientRect();
+ const margin = 10;
+
+ const isNearThisPlayer = (
+ mousePos.x !== null && mousePos.y !== null &&
+ mousePos.x >= pRect.left - margin && mousePos.x <= pRect.right + margin &&
+ mousePos.y >= pRect.top - margin && mousePos.y <= pRect.bottom + margin
+ );
+
+ let isCursorHiddenOnPlayer = false;
+ if (v0ckPlayer && !v0ckPlayer.classList.contains('v0ck_hover')) {
+ if (mousePos.active && mousePos.x >= pRect.left && mousePos.x <= pRect.right &&
+ mousePos.y >= pRect.top && mousePos.y <= pRect.bottom) {
+ isCursorHiddenOnPlayer = true;
+ }
+ }
+
+ let targetX = 0;
+ let targetY = 0;
+ let tiltX = 0;
+ let tiltY = 0;
+
+ if (isEnabled && isEyeActive && mousePos.active && isNearThisPlayer && !isCursorHiddenOnPlayer) {
+ const centerX = rect.left + rect.width / 2;
+ const centerY = rect.top + rect.height / 2;
+ const dx = mousePos.x - centerX;
+ const dy = mousePos.y - centerY;
+
+ const maxRadius = cfg.followRadius !== undefined ? cfg.followRadius : 20;
+ const dist = Math.hypot(dx, dy);
+
+ if (dist > 0) {
+ const clampedDist = Math.min(dist, maxRadius);
+ targetX = (dx / dist) * clampedDist;
+ targetY = (dy / dist) * clampedDist;
+ }
+
+ if (allowTilt) {
+ tiltX = -(targetY / (maxRadius || 1)) * 20;
+ tiltY = (targetX / (maxRadius || 1)) * 20;
+ }
+ }
+
+ circle._eyeX = circle._eyeX || 0;
+ circle._eyeY = circle._eyeY || 0;
+ circle._eyeTiltX = circle._eyeTiltX || 0;
+ circle._eyeTiltY = circle._eyeTiltY || 0;
+
+ circle._eyeX += (targetX - circle._eyeX) * speed;
+ circle._eyeY += (targetY - circle._eyeY) * speed;
+ circle._eyeTiltX += (tiltX - circle._eyeTiltX) * speed;
+ circle._eyeTiltY += (tiltY - circle._eyeTiltY) * speed;
+
+ // If audio visualizer is NOT actively driving the transform this frame, update it here
+ if (!circle._visualizerDriving) {
+ const curX = circle._eyeX.toFixed(1);
+ const curY = circle._eyeY.toFixed(1);
+ const curTiltX = circle._eyeTiltX.toFixed(1);
+ const curTiltY = circle._eyeTiltY.toFixed(1);
+ circle.style.transform = `translate(calc(-50% + ${curX}px), calc(-50% + ${curY}px)) perspective(600px) rotateX(${curTiltX}deg) rotateY(${curTiltY}deg) scale(1)`;
+ }
+ });
+
+ requestAnimationFrame(updateEyeTracking);
+ };
+
+ requestAnimationFrame(updateEyeTracking);
+
const initAudioTunerUI = () => {
if (document.getElementById('f0ck-audio-tuner-panel')) return;
+ const sidebarContainer = document.getElementById('sidebar-tuner-container');
const panel = document.createElement('div');
panel.id = 'f0ck-audio-tuner-panel';
- panel.className = 'f0ck-audio-tuner-panel hidden';
+ panel.className = sidebarContainer ? 'f0ck-audio-tuner-panel in-sidebar' : 'f0ck-audio-tuner-panel hidden';
const sliders = [
- // Disc & Glow Section
+ // Visualizer Color Section
+ { section: 'Visualizer Color & Theme', key: 'useCustomColor', label: 'Use Custom Color (1=Custom, 0=Site Accent)', min: 0, max: 1, step: 1, unit: '' },
+ { section: 'Visualizer Color & Theme', key: 'visualizerColor', label: 'Custom Visualizer Color', type: 'color' },
+ { section: 'Visualizer Color & Theme', key: 'enableBeatHue', label: 'Beat-Reactive Hue Color (1=On, 0=Off)', min: 0, max: 1, step: 1, unit: '' },
+ { section: 'Visualizer Color & Theme', key: 'beatHueThreshold', label: 'Beat Trigger Threshold', min: 0.00, max: 1.00, step: 0.01, unit: '' },
+ { section: 'Visualizer Color & Theme', key: 'beatHueStep', label: 'Color Shift Angle per Beat', min: 1, max: 180, step: 1, unit: '°' },
+ { section: 'Visualizer Color & Theme', key: 'beatHueSmooth', label: 'Color Morph Smoothness (0=Snap, 1=Glide)', min: 0.00, max: 1.00, step: 0.01, unit: '' },
+ { section: 'Visualizer Color & Theme', key: 'beatHueIdleDrift', label: 'Idle Ambient Hue Drift', min: 0.00, max: 2.00, step: 0.01, unit: 'x' },
+ { section: 'Visualizer Color & Theme', key: 'beatHueCooldown', label: 'Min Time Between Jumps', min: 50, max: 800, step: 25, unit: 'ms' },
+
+ // Cover Art & Glow Section
+ { section: 'Cover Art & Glow Reactivity', key: 'coverSize', label: 'Cover Art Size (Diameter)', min: 40, max: 600, step: 5, unit: 'px' },
+ { section: 'Cover Art & Glow Reactivity', key: 'solidCover', label: 'Solid Color Cover (1=On, 0=Off)', min: 0, max: 1, step: 1, unit: '' },
+ { section: 'Cover Art & Glow Reactivity', key: 'coverColor', label: 'Solid Cover Color', type: 'color' },
+ { section: 'Cover Art & Glow Reactivity', key: 'coverOpacity', label: 'Cover / Disc Opacity', min: 0.00, max: 1.00, step: 0.01, unit: '' },
+ { section: 'Cover Art & Glow Reactivity', key: 'glowIntensity', label: 'Cover Art Glow Size (Dynamic)', min: 0, max: 500, step: 5, unit: 'px' },
+ { section: 'Cover Art & Glow Reactivity', key: 'coverGlowBase', label: 'Cover Art Base Glow (Ambient)', min: 0, max: 150, step: 2, unit: 'px' },
+ { section: 'Cover Art & Glow Reactivity', key: 'glowBrightness', label: 'Cover Art Glow Brightness', min: 0.0, max: 3.0, step: 0.05, unit: 'x' },
+ { section: 'Cover Art & Glow Reactivity', key: 'glowSensitivity', label: 'Glow Beat Reactivity / Sensitivity', min: 0.10, max: 5.00, step: 0.05, unit: 'x' },
+ { section: 'Cover Art & Glow Reactivity', key: 'glowDynamism', label: 'Glow Dynamism (Dynamic Punch)', min: 0.00, max: 3.00, step: 0.05, unit: 'x' },
+ { section: 'Cover Art & Glow Reactivity', key: 'glowSmoothness', label: 'Glow Smoothness (Liquid Glow)', min: 0.00, max: 1.00, step: 0.01, unit: '' },
{ 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: 'bounceBoost', label: 'Bounce Reactivity Boost', min: 1.0, max: 100.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' },
+ { section: 'Cover Art & Glow Reactivity', key: 'bassPower', label: 'Sensitivity Curve (Power)', min: 0.10, max: 2.00, step: 0.01, unit: '' },
+ { section: 'Cover Art & Glow Reactivity', key: 'attackSpeed', label: 'Attack Speed (Snap)', min: 0.05, max: 1.00, step: 0.01, unit: '' },
+ { section: 'Cover Art & Glow Reactivity', key: 'releaseSpeed', label: 'Release Speed (Decay)', min: 0.01, max: 1.00, step: 0.01, unit: '' },
+ { section: 'Cover Art & Glow Reactivity', key: 'glowAttack', label: 'Glow Attack Speed (Swell)', min: 0.01, max: 1.00, step: 0.01, unit: '' },
+ { section: 'Cover Art & Glow Reactivity', key: 'glowDecay', label: 'Glow Decay Speed (Dissipate)', min: 0.01, max: 1.00, step: 0.01, unit: '' },
+
+ // Reactive Background Gradient Section
+ { section: 'Reactive Background Gradient', key: 'enableBgGradient', label: 'Background Gradient (1=On, 0=Off)', min: 0, max: 1, step: 1, unit: '' },
+ { section: 'Reactive Background Gradient', key: 'bgGradientStyle', label: 'Gradient Style (1=Radial Aura, 2=Dual Fog, 3=Conic Vortex, 4=Horizon Flare)', min: 1, max: 4, step: 1, unit: '' },
+ { section: 'Reactive Background Gradient', key: 'bgGradientOpacity', label: 'Gradient Base Opacity', min: 0.00, max: 1.00, step: 0.01, unit: '' },
+ { section: 'Reactive Background Gradient', key: 'bgGradientReactivity', label: 'Beat Reactivity (Pulse Strength)', min: 0.00, max: 3.00, step: 0.05, unit: 'x' },
+ { section: 'Reactive Background Gradient', key: 'bgGradientSpread', label: 'Gradient Radius / Spread', min: 10, max: 200, step: 2, unit: '%' },
+ { section: 'Reactive Background Gradient', key: 'useCustomBgColor', label: 'Custom Gradient Color (1=On, 0=Accent)', min: 0, max: 1, step: 1, unit: '' },
+ { section: 'Reactive Background Gradient', key: 'bgGradientColor', label: 'Custom Gradient Color', type: 'color' },
+
+ // Inner Eye Visualizer Section
+ { section: 'Inner Eye Visualizer (Pupil HUD)', key: 'enableInnerBars', label: 'Inner Visualizer (1=On, 0=Off)', min: 0, max: 1, step: 1, unit: '' },
+ { section: 'Inner Eye Visualizer (Pupil HUD)', key: 'innerBarsMode', label: 'Inner Style (1=Center, 2=Outward Iris, 3=Arc, 4=Inverted Iris, 5=Inverted Rev, 6=Dual Stargate, 7=360° Radar)', min: 1, max: 7, step: 1, unit: '' },
+ { section: 'Inner Eye Visualizer (Pupil HUD)', key: 'innerRadius', label: 'Inner Visualizer Size (Outer Radius)', min: 30, max: 800, step: 2, unit: 'px' },
+ { section: 'Inner Eye Visualizer (Pupil HUD)', key: 'innerPupilRadius', label: 'Center Pupil Gap (Inner Radius)', min: 0, max: 600, step: 2, unit: 'px' },
+ { section: 'Inner Eye Visualizer (Pupil HUD)', key: 'useCustomInnerColor', label: 'Solid Custom Inner Color (1=On, 0=Off)', min: 0, max: 1, step: 1, unit: '' },
+ { section: 'Inner Eye Visualizer (Pupil HUD)', key: 'innerBarColor', label: 'Solid Inner Bar Color', type: 'color' },
+ { section: 'Inner Eye Visualizer (Pupil HUD)', key: 'innerBarCount', label: 'Spectrum Density (Note Count)', min: 2, max: 512, step: 1, unit: '' },
+ { section: 'Inner Eye Visualizer (Pupil HUD)', key: 'innerHighBoost', label: 'Melody & Treble Sensitivity', min: 0.000, max: 2.000, step: 0.001, unit: 'x' },
+ { section: 'Inner Eye Visualizer (Pupil HUD)', key: 'innerBarHeight', label: 'Inner Bar Height / Scale', min: 0.05, max: 100.00, step: 0.05, unit: 'x' },
+ { section: 'Inner Eye Visualizer (Pupil HUD)', key: 'innerBarWidth', label: 'Inner Bar Width', min: 1, max: 250, step: 1, unit: 'px' },
+ { section: 'Inner Eye Visualizer (Pupil HUD)', key: 'innerBarOpacity', label: 'Inner Bar Opacity', min: 0.00, max: 1.00, step: 0.01, unit: '' },
+ { section: 'Inner Eye Visualizer (Pupil HUD)', key: 'outerRingOpacity', label: 'Outer Ring Opacity', min: 0.00, max: 1.00, step: 0.01, unit: '' },
+ { section: 'Inner Eye Visualizer (Pupil HUD)', key: 'innerPupilRingOpacity', label: 'Center Pupil Gap Opacity', min: 0.00, max: 1.00, step: 0.01, unit: '' },
+ { section: 'Inner Eye Visualizer (Pupil HUD)', key: 'innerRadialRotation', label: 'Radial Iris Rotation Angle', min: 0, max: 360, step: 5, unit: '°' },
+ { section: 'Inner Eye Visualizer (Pupil HUD)', key: 'innerBarGlow', label: 'Inner Bar Glow', min: 0, max: 30, step: 1, unit: 'px' },
+ { section: 'Inner Eye Visualizer (Pupil HUD)', key: 'innerHideNote', label: 'Hide Music Note Icon (1=On, 0=Off)', min: 0, max: 1, step: 1, unit: '' },
// 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: '' }
+ { section: 'Bottom Visualizer Bars', key: 'enableBars', label: 'Show Visualizer Bars (1=On, 0=Off)', min: 0, max: 1, step: 1, unit: '' },
+ { section: 'Bottom Visualizer Bars', key: 'barHeight', label: 'Visualizer Bar Height', min: 0.05, max: 2.50, step: 0.05, unit: 'x' },
+ { section: 'Bottom Visualizer Bars', key: 'barWidth', label: 'Bar Width', min: 1, max: 35, step: 1, unit: 'px' },
+ { section: 'Bottom Visualizer Bars', key: 'barGap', label: 'Bar Spacing (Gap)', min: 0, max: 15, step: 1, unit: 'px' },
+ { section: 'Bottom Visualizer Bars', key: 'barRadius', label: 'Bar Top Rounding', min: 0, max: 12, step: 1, unit: 'px' },
+ { section: 'Bottom Visualizer Bars', key: 'barOpacity', label: 'Bar Opacity', min: 0.00, max: 1.00, step: 0.01, unit: '' },
+ { section: 'Bottom Visualizer Bars', key: 'barGlow', label: 'Bar Glow Aura', min: 0, max: 30, step: 1, unit: 'px' },
+ { section: 'Bottom Visualizer Bars', key: 'smoothing', label: 'Visualizer Bar Smoothing', min: 0.00, max: 1.00, step: 0.01, unit: '' },
+
+ // Mouse Follow & Tilt Simulation Section
+ { section: 'Eye Simulation & Mouse Follow', key: 'followMouse', label: 'Follow Mouse Cursor (1=On, 0=Off)', min: 0, max: 1, step: 1, unit: '' },
+ { section: 'Eye Simulation & Mouse Follow', key: 'followSpeed', label: 'Eye Follow Speed', min: 0.01, max: 0.40, step: 0.01, unit: '' },
+ { section: 'Eye Simulation & Mouse Follow', key: 'followRadius', label: 'Eye Follow Range (Radius)', min: 5, max: 100, step: 1, unit: 'px' },
+ { section: 'Eye Simulation & Mouse Follow', key: 'tiltEffect', label: '3D Eye Tilt (1=On, 0=Off)', min: 0, max: 1, step: 1, unit: '' }
];
+ const updateCoverArtSolidMode = () => {
+ const cfg = window.audioVisualizerTuning || DEFAULT_AUDIO_TUNING;
+ const isSolid = Number(cfg.solidCover) === 1;
+ const color = cfg.coverColor || '#000000';
+ const coverOp = cfg.coverOpacity !== undefined ? Math.min(1, Math.max(0, Number(cfg.coverOpacity))) : 1.0;
+ const bgCol = coverOp <= 0.001 ? 'transparent' : (coverOp >= 0.999 ? color : `color-mix(in srgb, ${color} ${Math.round(coverOp * 100)}%, transparent)`);
+
+ document.querySelectorAll('.audio-cover-circle').forEach(c => {
+ if (isSolid) {
+ if (c.style.backgroundImage && c.style.backgroundImage !== 'none' && !c._origBgImage) {
+ c._origBgImage = c.style.backgroundImage;
+ }
+ c.style.backgroundImage = 'none';
+ c.style.backgroundColor = bgCol;
+ } else {
+ if (c._origBgImage) {
+ c.style.backgroundImage = c._origBgImage;
+ c.style.backgroundColor = bgCol;
+ } else {
+ c.style.backgroundImage = 'none';
+ c.style.backgroundColor = bgCol;
+ }
+ }
+ });
+ };
+
let rowsHtml = '';
let currentSection = '';
sliders.forEach(s => {
@@ -3723,15 +4229,27 @@ window.cancelAnimFrame = (function () {
rowsHtml += `${currentSection}
`;
}
const val = window.audioVisualizerTuning[s.key] !== undefined ? window.audioVisualizerTuning[s.key] : DEFAULT_AUDIO_TUNING[s.key];
- rowsHtml += `
-
-
-
${s.label}
-
${val}${s.unit}
+ if (s.type === 'color') {
+ rowsHtml += `
+
+
+ ${s.label}
+ ${val}
+
+
-
-
- `;
+ `;
+ } else {
+ rowsHtml += `
+
+
+ ${s.label}
+ ${val}${s.unit || ''}
+
+
+
+ `;
+ }
});
panel.innerHTML = `
@@ -3740,6 +4258,17 @@ window.cancelAnimFrame = (function () {
+
+
+
+
+
+
+
+
+
+
+
${rowsHtml}
`;
- const toggleBtn = document.createElement('button');
- toggleBtn.id = 'f0ck-tuner-toggle';
- toggleBtn.type = 'button';
- toggleBtn.className = 'f0ck-tuner-toggle';
- toggleBtn.innerHTML = ' Tuner';
- toggleBtn.title = 'Open Live Audio Reactivity Tuner';
+ const BUILTIN_PRESETS = {
+ 'Default': DEFAULT_AUDIO_TUNING,
+ 'Reactor': {
+ useCustomColor: 0,
+ visualizerColor: "#241f31",
+ enableBeatHue: 1,
+ beatHueThreshold: 1,
+ beatHueStep: 1,
+ beatHueSmooth: 1,
+ beatHueIdleDrift: 0.21,
+ beatHueCooldown: 800,
+ coverSize: 180,
+ glowIntensity: 500,
+ coverGlowBase: 150,
+ glowBrightness: 1.05,
+ glowSensitivity: 1.8,
+ glowDynamism: 0.85,
+ glowSmoothness: 0.95,
+ solidCover: 1,
+ coverColor: "#000000",
+ coverOpacity: 1,
+ bassGain: 0.5,
+ bassPower: 2,
+ scaleBounce: 0.85,
+ bounceBoost: 3.5,
+ attackSpeed: 1,
+ releaseSpeed: 1,
+ enableBars: 0,
+ enableInnerBars: 1,
+ useCustomInnerColor: 0,
+ innerBarColor: "#ff0000",
+ innerBarsMode: 4,
+ innerRadius: 156,
+ innerPupilRadius: 0,
+ innerPupilRingOpacity: 0,
+ innerBarCount: 512,
+ innerBarWidth: 15,
+ innerBarHeight: 5.4,
+ innerBarOpacity: 0.02,
+ outerRingOpacity: 0,
+ innerHighBoost: 0.066,
+ innerRadialRotation: 270,
+ innerBarGlow: 0,
+ innerHideNote: 1,
+ barHeight: 0.9,
+ barWidth: 34,
+ barGap: 9,
+ barRadius: 12,
+ barOpacity: 0.1,
+ barGlow: 0,
+ smoothing: 0.86,
+ followMouse: 1,
+ followSpeed: 0.1,
+ followRadius: 15,
+ tiltEffect: 1,
+ glowAttack: 1,
+ glowDecay: 0.73,
+ enableBlink: 0,
+ blinkInterval: 12,
+ beatHueSpeed: 0.1
+ },
+ 'Butterfly': {
+ useCustomColor: 0,
+ visualizerColor: "#241f31",
+ enableBeatHue: 1,
+ beatHueThreshold: 1,
+ beatHueStep: 1,
+ beatHueSmooth: 1,
+ beatHueIdleDrift: 0.21,
+ beatHueCooldown: 800,
+ coverSize: 205,
+ glowIntensity: 500,
+ coverGlowBase: 150,
+ glowBrightness: 0,
+ glowSensitivity: 0.1,
+ glowDynamism: 0,
+ glowSmoothness: 1,
+ solidCover: 1,
+ coverColor: "#000000",
+ coverOpacity: 0,
+ bassGain: 0.7,
+ bassPower: 1.51,
+ scaleBounce: 0.4,
+ bounceBoost: 3,
+ attackSpeed: 1,
+ releaseSpeed: 1,
+ enableBars: 0,
+ enableInnerBars: 1,
+ useCustomInnerColor: 0,
+ innerBarColor: "#ff0000",
+ innerBarsMode: 6,
+ innerRadius: 158,
+ innerPupilRadius: 0,
+ innerPupilRingOpacity: 0,
+ innerBarCount: 512,
+ innerBarWidth: 2,
+ innerBarHeight: 5.4,
+ innerBarOpacity: 0.94,
+ outerRingOpacity: 0,
+ innerHighBoost: 0.066,
+ innerRadialRotation: 270,
+ innerBarGlow: 0,
+ innerHideNote: 1,
+ barHeight: 0.9,
+ barWidth: 34,
+ barGap: 9,
+ barRadius: 12,
+ barOpacity: 0.1,
+ barGlow: 0,
+ smoothing: 0.86,
+ followMouse: 1,
+ followSpeed: 0.1,
+ followRadius: 15,
+ tiltEffect: 1,
+ glowAttack: 1,
+ glowDecay: 0.73,
+ enableBgGradient: 1,
+ bgGradientStyle: 1,
+ bgGradientOpacity: 1,
+ bgGradientReactivity: 0,
+ bgGradientSpread: 158,
+ useCustomBgColor: 0,
+ bgGradientColor: "#3a1c71",
+ enableBlink: 0,
+ blinkInterval: 12,
+ beatHueSpeed: 0.1
+ },
+ 'Hyper Laser 512': {
+ useCustomColor: 0,
+ visualizerColor: '#241f31',
+ enableBeatHue: 0,
+ coverSize: 105,
+ glowIntensity: 500,
+ coverGlowBase: 35,
+ glowBrightness: 2.2,
+ glowSensitivity: 1.2,
+ glowDynamism: 2.2,
+ glowSmoothness: 0.65,
+ solidCover: 1,
+ coverColor: '#000000',
+ coverOpacity: 1,
+ bassGain: 1.4,
+ bassPower: 1.1,
+ scaleBounce: 0,
+ bounceBoost: 24.5,
+ attackSpeed: 1,
+ releaseSpeed: 0.57,
+ enableBars: 0,
+ enableInnerBars: 1,
+ useCustomInnerColor: 0,
+ innerBarColor: '#000000',
+ innerBarsMode: 5,
+ innerRadius: 318,
+ innerPupilRadius: 292,
+ innerPupilRingOpacity: 0,
+ innerBarCount: 512,
+ innerBarWidth: 1,
+ innerBarHeight: 5,
+ innerBarOpacity: 0.85,
+ outerRingOpacity: 0,
+ innerHighBoost: 0.365,
+ innerRadialRotation: 90,
+ innerBarGlow: 0,
+ innerHideNote: 1,
+ smoothing: 0.34,
+ followMouse: 1,
+ followSpeed: 0.02,
+ followRadius: 20,
+ tiltEffect: 1,
+ glowAttack: 0.85,
+ glowDecay: 0.35,
+ enableBlink: 0,
+ blinkInterval: 12
+ },
+ 'Deep Bass Pulse': {
+ useCustomColor: 0,
+ coverSize: 110,
+ glowIntensity: 450,
+ coverGlowBase: 30,
+ glowBrightness: 2.5,
+ glowSensitivity: 1.5,
+ glowDynamism: 2.4,
+ glowSmoothness: 0.75,
+ solidCover: 1,
+ coverColor: '#000000',
+ coverOpacity: 1,
+ bassGain: 1.6,
+ bassPower: 1.2,
+ scaleBounce: 0.85,
+ bounceBoost: 2.0,
+ attackSpeed: 1,
+ releaseSpeed: 0.25,
+ enableBars: 1,
+ enableInnerBars: 1,
+ innerBarsMode: 4,
+ innerRadius: 180,
+ innerPupilRadius: 0,
+ innerPupilRingOpacity: 0,
+ innerBarCount: 180,
+ innerBarWidth: 2,
+ innerBarHeight: 2.2,
+ innerBarOpacity: 0.8,
+ outerRingOpacity: 0.3,
+ innerHighBoost: 0.2,
+ innerRadialRotation: 90,
+ innerBarGlow: 8,
+ innerHideNote: 1,
+ barHeight: 0.8,
+ barWidth: 24,
+ barGap: 8,
+ barRadius: 8,
+ barOpacity: 0.7,
+ barGlow: 15,
+ smoothing: 0.80
+ },
+ 'Stargate Strobe': {
+ useCustomColor: 0,
+ coverSize: 115,
+ glowIntensity: 400,
+ coverGlowBase: 40,
+ glowBrightness: 2.0,
+ glowSensitivity: 1.3,
+ glowDynamism: 2.0,
+ glowSmoothness: 0.50,
+ solidCover: 1,
+ coverColor: '#000000',
+ coverOpacity: 1,
+ bassGain: 1.3,
+ bassPower: 1.0,
+ scaleBounce: 0.40,
+ bounceBoost: 1.5,
+ enableBars: 0,
+ enableInnerBars: 1,
+ innerBarsMode: 6,
+ innerRadius: 240,
+ innerPupilRadius: 70,
+ innerPupilRingOpacity: 0.5,
+ innerBarCount: 256,
+ innerBarWidth: 2,
+ innerBarHeight: 2.8,
+ innerBarOpacity: 0.9,
+ outerRingOpacity: 0.5,
+ innerHighBoost: 0.25,
+ innerRadialRotation: 90,
+ innerBarGlow: 12,
+ innerHideNote: 1,
+ smoothing: 0.70
+ },
+ 'Minimal Bars': {
+ useCustomColor: 0,
+ coverSize: 100,
+ glowIntensity: 200,
+ coverGlowBase: 25,
+ glowBrightness: 1.2,
+ glowSensitivity: 1.0,
+ glowDynamism: 1.5,
+ solidCover: 0,
+ coverOpacity: 1,
+ scaleBounce: 0.50,
+ bounceBoost: 1.2,
+ enableBars: 1,
+ enableInnerBars: 0,
+ barHeight: 0.65,
+ barWidth: 28,
+ barGap: 8,
+ barRadius: 10,
+ barOpacity: 0.85,
+ barGlow: 10,
+ smoothing: 0.80
+ }
+ };
- document.body.appendChild(panel);
- document.body.appendChild(toggleBtn);
+ const loadUserPresets = () => {
+ try {
+ const raw = localStorage.getItem('f0ck_audio_presets');
+ return raw ? JSON.parse(raw) : {};
+ } catch (e) {
+ return {};
+ }
+ };
- toggleBtn.addEventListener('click', () => {
- panel.classList.toggle('hidden');
+ const saveUserPresets = (p) => {
+ try {
+ localStorage.setItem('f0ck_audio_presets', JSON.stringify(p));
+ } catch (e) {}
+ };
+
+ const presetSelect = panel.querySelector('#f0ck-tuner-preset-select');
+ const deleteBtn = panel.querySelector('#f0ck-tuner-preset-delete');
+ const saveBtn = panel.querySelector('#f0ck-tuner-preset-save');
+ const importBtn = panel.querySelector('#f0ck-tuner-preset-import');
+ const nameInput = panel.querySelector('#f0ck-tuner-preset-name');
+
+ const renderPresetsDropdown = (selectedVal = '') => {
+ const userPresets = loadUserPresets();
+ let html = ``;
+
+ html += ``;
+
+ const userKeys = Object.keys(userPresets);
+ if (userKeys.length > 0) {
+ html += ``;
+ }
+
+ presetSelect.innerHTML = html;
+ deleteBtn.disabled = !selectedVal || !selectedVal.startsWith('user:');
+ };
+
+ const applyTuningConfig = (newCfg, presetName) => {
+ Object.assign(window.audioVisualizerTuning, newCfg);
+ try {
+ localStorage.setItem('f0ck_audio_tuning', JSON.stringify(window.audioVisualizerTuning));
+ } catch (e) {}
+
+ sliders.forEach(s => {
+ const input = panel.querySelector(`#input-${s.key}`);
+ const valEl = panel.querySelector(`#val-${s.key}`);
+ if (!input || !valEl) return;
+ const val = window.audioVisualizerTuning[s.key] !== undefined ? window.audioVisualizerTuning[s.key] : DEFAULT_AUDIO_TUNING[s.key];
+ input.value = val;
+ if (s.type === 'color') {
+ valEl.textContent = val;
+ } else {
+ const num = Number(val);
+ const displayVal = Number.isInteger(num) ? num : parseFloat(num.toFixed(3));
+ valEl.textContent = `${displayVal}${s.unit || ''}`;
+ }
+ });
+
+ const cfg = window.audioVisualizerTuning;
+ const isCustom = Number(cfg.useCustomColor) === 1;
+ const activeColor = isCustom ? (cfg.visualizerColor || '#99ff00') : '';
+ document.querySelectorAll('.audio-cover-circle i').forEach(note => {
+ note.style.color = activeColor;
+ note.style.textShadow = activeColor ? `0 0 16px color-mix(in srgb, ${activeColor} 75%, transparent), 0 0 35px color-mix(in srgb, ${activeColor} 40%, transparent)` : '';
+ note.style.display = Number(cfg.innerHideNote) === 1 ? 'none' : '';
+ });
+ document.querySelectorAll('.audio-cover-circle').forEach(c => {
+ if (cfg.coverSize) {
+ c.style.width = cfg.coverSize + 'px';
+ c.style.height = cfg.coverSize + 'px';
+ }
+ const note = c.querySelector('i');
+ if (note && cfg.coverSize) note.style.fontSize = Math.round(cfg.coverSize * 0.35) + 'px';
+ });
+ updateCoverArtSolidMode();
+
+ if (presetName && typeof window.flashMessage === 'function') {
+ window.flashMessage(`Loaded preset: "${presetName}"`, 2200, 'info');
+ }
+ };
+
+ renderPresetsDropdown();
+
+ presetSelect.addEventListener('change', () => {
+ const val = presetSelect.value;
+ if (!val) return;
+ if (val.startsWith('builtin:')) {
+ const name = val.replace('builtin:', '');
+ if (BUILTIN_PRESETS[name]) {
+ applyTuningConfig(BUILTIN_PRESETS[name], name);
+ }
+ deleteBtn.disabled = true;
+ } else if (val.startsWith('user:')) {
+ const name = val.replace('user:', '');
+ const userPresets = loadUserPresets();
+ if (userPresets[name]) {
+ applyTuningConfig(userPresets[name], name);
+ }
+ deleteBtn.disabled = false;
+ }
});
- panel.querySelector('#f0ck-tuner-close').addEventListener('click', () => {
- panel.classList.add('hidden');
+ const handleSavePreset = () => {
+ let name = (nameInput.value || '').trim();
+ if (!name) {
+ name = prompt('Enter a name for this preset:');
+ if (!name) return;
+ name = name.trim();
+ }
+ if (!name) return;
+
+ const userPresets = loadUserPresets();
+ userPresets[name] = Object.assign({}, window.audioVisualizerTuning);
+ saveUserPresets(userPresets);
+
+ nameInput.value = '';
+ renderPresetsDropdown('user:' + name);
+
+ if (typeof window.flashMessage === 'function') {
+ window.flashMessage(`Preset "${name}" saved!`, 2500, 'success');
+ }
+ };
+
+ saveBtn.addEventListener('click', handleSavePreset);
+ nameInput.addEventListener('keydown', (e) => {
+ if (e.key === 'Enter') {
+ e.preventDefault();
+ handleSavePreset();
+ }
});
+ deleteBtn.addEventListener('click', () => {
+ const val = presetSelect.value;
+ if (!val || !val.startsWith('user:')) return;
+ const name = val.replace('user:', '');
+ if (!confirm(`Delete preset "${name}"?`)) return;
+
+ const userPresets = loadUserPresets();
+ delete userPresets[name];
+ saveUserPresets(userPresets);
+
+ renderPresetsDropdown('');
+ if (typeof window.flashMessage === 'function') {
+ window.flashMessage(`Preset "${name}" deleted`, 2200, 'info');
+ }
+ });
+
+ importBtn.addEventListener('click', () => {
+ const raw = prompt('Paste your visualizer preset JSON:');
+ if (!raw) return;
+ try {
+ const parsed = JSON.parse(raw.trim());
+ if (typeof parsed !== 'object' || parsed === null) {
+ throw new Error('Not an object');
+ }
+ const defaultName = 'Preset ' + new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
+ let name = prompt('Save preset as:', defaultName);
+ name = name ? name.trim() : defaultName;
+
+ const userPresets = loadUserPresets();
+ userPresets[name] = Object.assign({}, DEFAULT_AUDIO_TUNING, parsed);
+ saveUserPresets(userPresets);
+
+ applyTuningConfig(userPresets[name], name);
+ renderPresetsDropdown('user:' + name);
+
+ if (typeof window.flashMessage === 'function') {
+ window.flashMessage(`Imported and loaded "${name}"!`, 3000, 'success');
+ }
+ } catch (err) {
+ alert('Invalid JSON! Please paste valid preset settings.');
+ }
+ });
+
+ if (sidebarContainer) {
+ sidebarContainer.appendChild(panel);
+ } else {
+ const toggleBtn = document.createElement('button');
+ toggleBtn.id = 'f0ck-tuner-toggle';
+ toggleBtn.type = 'button';
+ toggleBtn.className = 'f0ck-tuner-toggle';
+ toggleBtn.innerHTML = ' Tuner';
+ toggleBtn.title = 'Open Live Audio Reactivity Tuner';
+
+ document.body.appendChild(panel);
+ document.body.appendChild(toggleBtn);
+
+ toggleBtn.addEventListener('click', () => {
+ panel.classList.toggle('hidden');
+ });
+ }
+
+ const closeBtn = panel.querySelector('#f0ck-tuner-close');
+ if (closeBtn) {
+ closeBtn.addEventListener('click', () => {
+ if (sidebarContainer && typeof window.switchSidebarTab === 'function') {
+ window.switchSidebarTab('comments');
+ } else {
+ 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}`;
+ if (s.type === 'color') {
+ window.audioVisualizerTuning[s.key] = input.value;
+ valEl.textContent = input.value;
+ } else {
+ const parsed = parseFloat(input.value);
+ window.audioVisualizerTuning[s.key] = parsed;
+ const displayVal = Number.isInteger(parsed) ? parsed : parseFloat(parsed.toFixed(3));
+ valEl.textContent = `${displayVal}${s.unit || ''}`;
+ }
+
+ if (s.key === 'useCustomColor' || s.key === 'visualizerColor') {
+ const cfg = window.audioVisualizerTuning;
+ const isCustom = Number(cfg.useCustomColor) === 1;
+ const activeColor = isCustom ? (cfg.visualizerColor || '#99ff00') : '';
+ document.querySelectorAll('.audio-cover-circle i').forEach(note => {
+ note.style.color = activeColor;
+ note.style.textShadow = activeColor ? `0 0 16px color-mix(in srgb, ${activeColor} 75%, transparent), 0 0 35px color-mix(in srgb, ${activeColor} 40%, transparent)` : '';
+ });
+ } else if (s.key === 'solidCover' || s.key === 'coverColor' || s.key === 'coverOpacity') {
+ updateCoverArtSolidMode();
+ } else if (s.key === 'enableBars') {
+ if (Number(input.value) === 0) {
+ document.querySelectorAll('.v0ck-audio-visualizer-canvas, .audio-visualizer-canvas').forEach(c => {
+ const cCtx = c.getContext('2d');
+ if (cCtx) cCtx.clearRect(0, 0, c.width, c.height);
+ });
+ }
+ } else if (s.key === 'enableInnerBars') {
+ if (Number(input.value) === 0) {
+ document.querySelectorAll('.audio-eye-inner-canvas').forEach(c => {
+ const inCtx = c.getContext('2d');
+ if (inCtx) inCtx.clearRect(0, 0, c.width, c.height);
+ });
+ }
+ } else if (s.key === 'innerHideNote') {
+ const hide = Number(input.value) === 1;
+ document.querySelectorAll('.audio-cover-circle i').forEach(note => {
+ note.style.display = hide ? 'none' : '';
+ });
+ } else if (s.key === 'coverSize') {
+ const parsed = parseFloat(input.value);
+ document.querySelectorAll('.audio-cover-circle').forEach(c => {
+ c.style.width = parsed + 'px';
+ c.style.height = parsed + 'px';
+ const note = c.querySelector('i');
+ if (note) note.style.fontSize = Math.round(parsed * 0.35) + 'px';
+ });
+ } else if (s.key === 'coverGlowBase' || s.key === 'glowBrightness') {
+ const cfg = window.audioVisualizerTuning;
+ const baseGlow = cfg.coverGlowBase !== undefined ? cfg.coverGlowBase : 35;
+ const glowMult = cfg.glowBrightness !== undefined ? cfg.glowBrightness : 1.0;
+ document.querySelectorAll('.audio-cover-circle').forEach(c => {
+ if (!c._visualizerDriving) {
+ if (glowMult <= 0) {
+ c.style.boxShadow = `0 12px 40px rgba(0, 0, 0, 0.88)`;
+ } else {
+ c.style.boxShadow = `0 12px 40px rgba(0, 0, 0, 0.88), 0 0 ${baseGlow}px color-mix(in srgb, var(--accent, #99ff00) ${Math.round(28 * Math.min(1.5, glowMult))}%, transparent)`;
+ }
+ }
+ });
+ }
+
try {
localStorage.setItem('f0ck_audio_tuning', JSON.stringify(window.audioVisualizerTuning));
} catch (e) {}
@@ -3794,27 +4848,293 @@ window.cancelAnimFrame = (function () {
});
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}`;
- });
+ applyTuningConfig(DEFAULT_AUDIO_TUNING, 'Default');
+ renderPresetsDropdown('builtin:Default');
if (typeof window.flashMessage === 'function') {
window.flashMessage('Tuner reset to defaults', 2000, 'info');
}
});
+
+ updateCoverArtSolidMode();
};
- window.initVisualizer = () => {
- const audioElement = document.querySelector("audio#my-video, audio#f0ck-album-audio, audio");
+ document.addEventListener('click', (e) => {
+ const tunerTab = e.target.closest('#sidebar-tab-tuner, [data-tab="tuner"]');
+ if (tunerTab) {
+ initAudioTunerUI();
+ }
+ });
+
+ if (document.readyState === 'loading') {
+ document.addEventListener('DOMContentLoaded', initAudioTunerUI);
+ } else {
+ setTimeout(initAudioTunerUI, 50);
+ }
+
+ const audioMetaMemoryCache = new Map();
+
+ // Fast client-side binary tag parser (FLAC Vorbis Comment & ID3v2)
+ const parseAudioTagsFromBuffer = (buffer) => {
+ const bytes = new Uint8Array(buffer);
+ const result = { artist: '', title: '', album: '' };
+
+ // 1. Check FLAC: "fLaC" (0x66, 0x4C, 0x61, 0x63)
+ if (bytes.length > 4 && bytes[0] === 0x66 && bytes[1] === 0x4C && bytes[2] === 0x61 && bytes[3] === 0x63) {
+ let offset = 4;
+ const view = new DataView(buffer);
+ while (offset + 4 < bytes.length) {
+ const header = bytes[offset];
+ const isLast = (header & 0x80) !== 0;
+ const type = header & 0x7F;
+ const length = (bytes[offset + 1] << 16) | (bytes[offset + 2] << 8) | bytes[offset + 3];
+ offset += 4;
+ if (type === 4 && offset + length <= bytes.length) { // VORBIS_COMMENT
+ let p = offset;
+ const vendorLen = view.getUint32(p, true); p += 4 + vendorLen;
+ if (p + 4 <= offset + length) {
+ const commentCount = view.getUint32(p, true); p += 4;
+ const decoder = new TextDecoder('utf-8');
+ for (let i = 0; i < commentCount && p + 4 <= offset + length; i++) {
+ const cLen = view.getUint32(p, true); p += 4;
+ if (p + cLen <= offset + length) {
+ const commentStr = decoder.decode(bytes.subarray(p, p + cLen));
+ p += cLen;
+ const eqIdx = commentStr.indexOf('=');
+ if (eqIdx > 0) {
+ const key = commentStr.slice(0, eqIdx).toUpperCase().replace(/[-_\s]/g, '');
+ const val = commentStr.slice(eqIdx + 1).trim();
+ if (key === 'TITLE' && !result.title) result.title = val;
+ else if ((key === 'ARTIST' || key === 'ALBUMARTIST' || key === 'PERFORMER' || key === 'AUTHOR') && !result.artist) result.artist = val;
+ else if (key === 'ALBUM' && !result.album) result.album = val;
+ }
+ }
+ }
+ }
+ break;
+ }
+ offset += length;
+ if (isLast) break;
+ }
+ }
+
+ // 2. Check ID3v2: "ID3" (73, 68, 51)
+ if (bytes.length > 10 && bytes[0] === 0x49 && bytes[1] === 0x44 && bytes[2] === 0x33) {
+ const version = bytes[3];
+ const tagSize = ((bytes[6] & 0x7F) << 21) | ((bytes[7] & 0x7F) << 14) | ((bytes[8] & 0x7F) << 7) | (bytes[9] & 0x7F);
+ let offset = 10;
+ const end = Math.min(bytes.length, 10 + tagSize);
+
+ while (offset + 10 <= end) {
+ let frameId = '';
+ let frameSize = 0;
+
+ if (version >= 3) {
+ frameId = String.fromCharCode(bytes[offset], bytes[offset + 1], bytes[offset + 2], bytes[offset + 3]);
+ if (version === 4) {
+ frameSize = ((bytes[offset + 4] & 0x7F) << 21) | ((bytes[offset + 5] & 0x7F) << 14) | ((bytes[offset + 6] & 0x7F) << 7) | (bytes[offset + 7] & 0x7F);
+ } else {
+ frameSize = (bytes[offset + 4] << 24) | (bytes[offset + 5] << 16) | (bytes[offset + 6] << 8) | bytes[offset + 7];
+ }
+ offset += 10;
+ } else if (version === 2) {
+ frameId = String.fromCharCode(bytes[offset], bytes[offset + 1], bytes[offset + 2]);
+ frameSize = (bytes[offset + 3] << 16) | (bytes[offset + 4] << 8) | bytes[offset + 5];
+ offset += 6;
+ }
+
+ if (!frameId || frameId.charCodeAt(0) === 0 || frameSize <= 0 || offset + frameSize > end) break;
+
+ if (frameId === 'TIT2' || frameId === 'TT2' || frameId === 'TPE1' || frameId === 'TP1' || frameId === 'TPE2' || frameId === 'TALB') {
+ const encoding = bytes[offset];
+ const frameData = bytes.subarray(offset + 1, offset + frameSize);
+ let text = '';
+ try {
+ if (encoding === 1 || encoding === 2) {
+ text = new TextDecoder('utf-16').decode(frameData);
+ } else {
+ text = new TextDecoder('utf-8').decode(frameData);
+ }
+ text = text.replace(/\0+$/, '').trim();
+ } catch (_) {}
+
+ if ((frameId === 'TIT2' || frameId === 'TT2') && !result.title) result.title = text;
+ else if ((frameId === 'TPE1' || frameId === 'TP1') && !result.artist) result.artist = text;
+ else if (frameId === 'TPE2' && !result.artist) result.artist = text;
+ else if (frameId === 'TALB' && !result.album) result.album = text;
+ }
+
+ offset += frameSize;
+ }
+ }
+
+ return result;
+ };
+
+ let badgeUpdateSeq = 0;
+ window.updateAudioTrackBadge = async (audioElement, itemId, explicitSrc) => {
+ if (!audioElement) return;
+ const playerWrap = audioElement.closest('.album-gallery-container, .album-audio-wrapper, .v0ck, .embed-responsive') || audioElement.parentElement;
+ if (!playerWrap) return;
+
+ let badge = playerWrap.querySelector('.album-audio-wrapper .audio-track-info-badge, .sidebar-media-placeholder.audio > .audio-track-info-badge, .audio-track-info-badge');
+ if (!badge) {
+ const ph = playerWrap.querySelector('.album-audio-wrapper .sidebar-media-placeholder.audio, .sidebar-media-placeholder.audio') || playerWrap;
+ ph.insertAdjacentHTML('afterbegin', `
+
+ `);
+ badge = playerWrap.querySelector('.audio-track-info-badge');
+ }
+ if (!badge) return;
+
+ badge.classList.remove('is-playing');
+ if (audioElement._badgePlayStateListener) {
+ audioElement.removeEventListener('play', audioElement._badgePlayStateListener);
+ audioElement.removeEventListener('pause', audioElement._badgePlayStateListener);
+ audioElement.removeEventListener('ended', audioElement._badgePlayStateListener);
+ audioElement._badgePlayStateListener = null;
+ }
+
+ const artistEl = badge.querySelector('.audio-track-artist');
+ const titleEl = badge.querySelector('.audio-track-title');
+
+ const src = explicitSrc ||
+ (audioElement.getAttribute ? audioElement.getAttribute('src') : null) ||
+ audioElement.src ||
+ audioElement.currentSrc ||
+ '';
+ if (!src || src === window.location.href || src.endsWith('#')) {
+ badge.classList.remove('is-visible');
+ return;
+ }
+
+ const cleanFilename = src.split('?')[0].split('/').pop();
+ const cacheKey = cleanFilename || `item_${itemId || ''}`;
+
+ const thisReqId = ++badgeUpdateSeq;
+ badge._lastReqId = thisReqId;
+
+ let meta = audioMetaMemoryCache.get(cacheKey);
+
+ const renderMeta = (m) => {
+ if (badge._lastReqId !== thisReqId) return;
+ if (!m || (!m.artist && !m.title)) {
+ badge.classList.remove('is-visible');
+ return;
+ }
+ if (artistEl) {
+ artistEl.textContent = m.artist || '';
+ artistEl.style.display = m.artist ? '' : 'none';
+ }
+ if (titleEl) {
+ titleEl.textContent = m.title || '';
+ titleEl.style.display = m.title ? '' : 'none';
+ }
+ const fullTooltip = `${m.artist ? m.artist + ' – ' : ''}${m.title || ''}${m.album ? ' (' + m.album + ')' : ''}`;
+ badge.setAttribute('title', fullTooltip);
+ badge.classList.add('is-visible');
+
+ if ('mediaSession' in navigator && window.MediaMetadata) {
+ try {
+ navigator.mediaSession.metadata = new MediaMetadata({
+ title: m.title || '',
+ artist: m.artist || '',
+ album: m.album || ''
+ });
+ } catch (_) {}
+ }
+ };
+
+ if (meta) {
+ renderMeta(meta);
+ return;
+ }
+
+ // Immediately clear previous track text while fetching new track metadata
+ if (artistEl) artistEl.textContent = '';
+ if (titleEl) titleEl.textContent = '';
+ badge.classList.remove('is-visible');
+
+ // 1. Fetch from server endpoint /api/v2/audio-metadata (ffprobe format tags + DB fallback)
+ try {
+ const q = new URLSearchParams();
+ if (cleanFilename) q.set('src', cleanFilename);
+ if (itemId) q.set('id', itemId);
+ const apiResp = await fetch(`/api/v2/audio-metadata?${q.toString()}`);
+ if (badge._lastReqId !== thisReqId) return;
+ if (apiResp.ok) {
+ const data = await apiResp.json();
+ if (badge._lastReqId !== thisReqId) return;
+ if (data.success && (data.artist || data.title)) {
+ meta = { artist: data.artist, title: data.title, album: data.album };
+ audioMetaMemoryCache.set(cacheKey, meta);
+ renderMeta(meta);
+ return;
+ }
+ }
+ } catch (_) {}
+
+ // 2. Client-side range fetch fallback (first 64KB)
+ try {
+ if (badge._lastReqId !== thisReqId) return;
+ const resp = await fetch(src, {
+ headers: { Range: 'bytes=0-65535' }
+ });
+ if (badge._lastReqId !== thisReqId) return;
+ if (resp.ok || resp.status === 206) {
+ const buf = await resp.arrayBuffer();
+ if (badge._lastReqId !== thisReqId) return;
+ const parsed = parseAudioTagsFromBuffer(buf);
+ if (parsed.artist || parsed.title) {
+ meta = parsed;
+ audioMetaMemoryCache.set(cacheKey, meta);
+ renderMeta(meta);
+ return;
+ }
+ }
+ } catch (_) {}
+
+ // 3. Fallback: parse from filename if "Artist - Title" pattern
+ if (badge._lastReqId !== thisReqId) return;
+ const rawName = decodeURIComponent(cleanFilename.replace(/\.[^/.]+$/, ''));
+ const match = rawName.match(/^(.+?)\s*[-–—_]\s*(.+)$/);
+ if (match && !rawName.startsWith('subf0ck')) {
+ meta = {
+ artist: match[1].replace(/^[0-9]+[\s._-]+/, '').trim(),
+ title: match[2].trim(),
+ album: ''
+ };
+ audioMetaMemoryCache.set(cacheKey, meta);
+ renderMeta(meta);
+ } else {
+ badge.classList.remove('is-visible');
+ }
+ };
+
+ window.initVisualizer = (targetAudio) => {
+ let audioElement = targetAudio;
+ if (!audioElement || !audioElement.isConnected) {
+ const albumAudio = document.querySelector(".album-gallery-container.is-audio-active audio, #f0ck-album-audio-wrapper audio, audio#f0ck-album-audio");
+ const standaloneAudio = document.querySelector("audio#my-video");
+ const container = document.querySelector(".album-gallery-container");
+ if (container && container.classList.contains('is-audio-active') && albumAudio) {
+ audioElement = albumAudio;
+ } else if (standaloneAudio && standaloneAudio.isConnected) {
+ audioElement = standaloneAudio;
+ } else {
+ audioElement = albumAudio || standaloneAudio || document.querySelector("audio");
+ }
+ }
if (audioElement) {
// Ensure Tuner UI is mounted
initAudioTunerUI();
+ if (window.updateAudioTrackBadge) {
+ window.updateAudioTrackBadge(audioElement, null, audioElement.getAttribute('src') || audioElement.src);
+ }
// Cleanup existing visualizer
if (visualizerRafId) window.cancelAnimFrame(visualizerRafId);
@@ -3827,7 +5147,7 @@ window.cancelAnimFrame = (function () {
canvas.height = 1080;
const attachCanvas = () => {
- const v0ckContainer = audioElement.closest('.v0ck') || audioElement.parentElement || document.querySelector('.v0ck');
+ const v0ckContainer = audioElement.closest('.v0ck, .album-audio-wrapper, .embed-responsive') || audioElement.parentElement;
if (v0ckContainer && !v0ckContainer.contains(canvas)) {
const controls = v0ckContainer.querySelector('.v0ck_player_controls');
if (controls) {
@@ -3850,55 +5170,163 @@ window.cancelAnimFrame = (function () {
let source = audioElement._mediaElementSource;
let analyser = audioElement._audioAnalyser;
- if (!source) {
- try {
- source = audioCtx.createMediaElementSource(audioElement);
- audioElement._mediaElementSource = source;
- } catch (e) {
- console.warn("Visualizer Source creation failed:", e);
+ const setupAudioSource = () => {
+ if (!audioElement.src || audioElement.src === window.location.href || audioElement.src.endsWith('#')) {
+ return false;
}
- }
+ if (!source) {
+ try {
+ source = audioCtx.createMediaElementSource(audioElement);
+ audioElement._mediaElementSource = source;
+ } catch (e) {
+ console.warn("Visualizer Source creation failed:", e);
+ }
+ }
+ if (source && analyser && !source._connectedToAnalyser) {
+ try {
+ source.connect(analyser);
+ source.connect(audioCtx.destination);
+ source._connectedToAnalyser = true;
+ } catch (e) {
+ console.warn("Visualizer connect failed:", e);
+ }
+ }
+ return !!source;
+ };
const cfgInit = window.audioVisualizerTuning || DEFAULT_AUDIO_TUNING;
if (!analyser) {
analyser = audioCtx.createAnalyser();
analyser.fftSize = 2048;
- analyser.smoothingTimeConstant = cfgInit.smoothing !== undefined ? cfgInit.smoothing : 0.80;
+ analyser.smoothingTimeConstant = Math.min(0.99, Math.max(0, cfgInit.smoothing !== undefined ? Number(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;
+ analyser.smoothingTimeConstant = Math.min(0.99, Math.max(0, cfgInit.smoothing !== undefined ? Number(cfgInit.smoothing) : 0.80));
}
+ setupAudioSource();
+
let data = new Uint8Array(analyser.frequencyBinCount);
let noteIcon = null;
let smoothScale = 1;
let smoothGlow = 0;
+ let currentBeatHue = 0;
+ let targetBeatHue = 0;
+ let lastBeatJumpTime = 0;
const draw = (data) => {
ctx.clearRect(0, 0, canvas.width, canvas.height);
- const accent = getComputedStyle(document.body).getPropertyValue("--accent")?.trim() || "#99ff00";
- ctx.fillStyle = accent;
const count = analyser.frequencyBinCount;
- const barWidth = canvas.width / count;
const cfg = window.audioVisualizerTuning || DEFAULT_AUDIO_TUNING;
- const barMult = cfg.barHeight !== undefined ? cfg.barHeight : 0.85;
- 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 (!audioElement.paused && Number(cfg.enableBeatHue) === 1) {
+ const now = performance.now();
+ const threshold = cfg.beatHueThreshold !== undefined ? Number(cfg.beatHueThreshold) : 0.30;
+ const stepAngle = cfg.beatHueStep !== undefined ? Math.max(1, Number(cfg.beatHueStep)) : 45;
+ const idleDrift = cfg.beatHueIdleDrift !== undefined ? Number(cfg.beatHueIdleDrift) : 0.20;
+ const cooldown = cfg.beatHueCooldown !== undefined ? Number(cfg.beatHueCooldown) : 150;
+ const morphSmooth = cfg.beatHueSmooth !== undefined ? Math.min(0.98, Math.max(0, Number(cfg.beatHueSmooth))) : 0.65;
+
+ let bMax = 0, bSum = 0;
+ const kBins = Math.min(10, count);
+ for (let b = 1; b <= kBins; b++) {
+ const v = data[b] || 0;
+ if (v > bMax) bMax = v;
+ bSum += v;
+ }
+ const bPeak = bMax / 255;
+ const bAvg = bSum / (kBins * 255);
+ const kEnergy = Math.min(1, (bPeak * 0.75 + bAvg * 0.25) * (cfg.bassGain || 0.5));
+ const pwr = Math.pow(kEnergy, (cfg.bassPower !== undefined ? cfg.bassPower : 0.3));
+
+ // Continuous subtle ambient drift
+ targetBeatHue = (targetBeatHue + idleDrift * 0.4) % 360;
+
+ // Beat threshold & cooldown check
+ if (pwr >= threshold && (now - lastBeatJumpTime) >= cooldown) {
+ const excess = (pwr - threshold) / Math.max(0.1, 1 - threshold);
+ const jump = stepAngle * (1 + excess * 0.35);
+ targetBeatHue = (targetBeatHue + jump) % 360;
+ lastBeatJumpTime = now;
+ }
+
+ // Interpolate currentBeatHue towards targetBeatHue
+ if (morphSmooth <= 0.02) {
+ currentBeatHue = targetBeatHue;
+ } else {
+ let diff = (targetBeatHue - currentBeatHue) % 360;
+ if (diff < -180) diff += 360;
+ if (diff > 180) diff -= 360;
+ currentBeatHue = (currentBeatHue + diff * (1 - morphSmooth)) % 360;
+ if (currentBeatHue < 0) currentBeatHue += 360;
+ }
+ }
+
+ let accent = getComputedStyle(document.body).getPropertyValue("--accent")?.trim() || "#99ff00";
+ if (Number(cfg.enableBeatHue) === 1) {
+ accent = `hsl(${Math.round(currentBeatHue)}, 100%, 55%)`;
+ } else if (Number(cfg.useCustomColor) === 1 && cfg.visualizerColor) {
+ accent = cfg.visualizerColor;
+ }
+ const barMult = cfg.barHeight !== undefined ? cfg.barHeight : 0.40;
+ const bWidth = Math.max(1, cfg.barWidth !== undefined ? cfg.barWidth : 4);
+ const bGap = Math.max(0, cfg.barGap !== undefined ? cfg.barGap : 2);
+ const bRadius = Math.max(0, cfg.barRadius !== undefined ? cfg.barRadius : 2);
+ const bOpacity = Math.min(1, Math.max(0, cfg.barOpacity !== undefined ? Number(cfg.barOpacity) : 0.85));
+ const bGlow = Math.max(0, cfg.barGlow !== undefined ? cfg.barGlow : 8);
+
+ const showBars = cfg.enableBars !== undefined ? Number(cfg.enableBars) === 1 : true;
+ if (showBars) {
+ const totalBarStep = bWidth + bGap;
+ const numBars = Math.max(8, Math.floor((canvas.width + bGap) / totalBarStep));
+ const totalUsedWidth = numBars * totalBarStep - bGap;
+ const startX = Math.max(0, (canvas.width - totalUsedWidth) / 2);
+
+ if (bGlow > 0) {
+ ctx.shadowBlur = bGlow;
+ ctx.shadowColor = accent;
+ } else {
+ ctx.shadowBlur = 0;
+ }
+
+ ctx.fillStyle = `color-mix(in srgb, ${accent} ${Math.round(bOpacity * 100)}%, transparent)`;
+
+ for (let i = 0; i < numBars; i++) {
+ const normIdx = i / numBars;
+ const nextNormIdx = (i + 1) / numBars;
+ const startBin = Math.floor(Math.pow(normIdx, 1.7) * (count * 0.78));
+ const endBin = Math.min(count - 1, Math.max(startBin, Math.floor(Math.pow(nextNormIdx, 1.7) * (count * 0.78))));
+
+ let valMax = 0;
+ let valSum = 0;
+ let valCount = 0;
+ for (let b = startBin; b <= endBin; b++) {
+ const v = data[b] || 0;
+ if (v > valMax) valMax = v;
+ valSum += v;
+ valCount++;
+ }
+ const val = valCount > 0 ? (valMax * 0.7 + (valSum / valCount) * 0.3) : (data[startBin] || 0);
+
+ if (val > 0) {
+ const percent = (val / 256) * barMult;
+ const height = (canvas.height * percent / 2) - 15;
+ if (height > 1) {
+ const x = startX + i * totalBarStep;
+ const y = canvas.height - height;
+ if (bRadius > 0 && typeof ctx.roundRect === 'function') {
+ ctx.beginPath();
+ ctx.roundRect(x, y, bWidth, height, [bRadius, bRadius, 0, 0]);
+ ctx.fill();
+ } else {
+ ctx.fillRect(x, y, bWidth, height);
+ }
+ }
}
}
+ ctx.shadowBlur = 0;
}
if (cfg.smoothing !== undefined && analyser.smoothingTimeConstant !== cfg.smoothing) {
@@ -3906,20 +5334,66 @@ window.cancelAnimFrame = (function () {
}
// 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;
+ const v0ckContainer = audioElement.closest('.v0ck, .album-audio-wrapper, .embed-responsive') || audioElement.parentElement;
+ const ph = v0ckContainer ? v0ckContainer.querySelector('.sidebar-media-placeholder.audio') : document.querySelector('.album-gallery-container.is-audio-active .sidebar-media-placeholder.audio, .sidebar-media-placeholder.audio');
if (!noteIcon || !noteIcon.isConnected) {
noteIcon = ph ? ph.querySelector('i') : null;
}
const coverCircle = ph ? ph.querySelector('.audio-cover-circle') : null;
+ let bgEl = ph ? ph.querySelector('.audio-reactive-bg') : null;
+ if (ph && !bgEl) {
+ bgEl = document.createElement('div');
+ bgEl.className = 'audio-reactive-bg';
+ ph.insertBefore(bgEl, ph.firstChild);
+ }
- if (coverCircle && cfg.coverSize) {
- coverCircle.style.width = cfg.coverSize + 'px';
- coverCircle.style.height = cfg.coverSize + 'px';
+ if (coverCircle) {
+ if (cfg.coverSize) {
+ coverCircle.style.width = cfg.coverSize + 'px';
+ coverCircle.style.height = cfg.coverSize + 'px';
+ if (noteIcon && coverCircle.contains(noteIcon)) {
+ noteIcon.style.fontSize = Math.round(cfg.coverSize * 0.35) + 'px';
+ }
+ }
+ const coverOp = cfg.coverOpacity !== undefined ? Math.min(1, Math.max(0, Number(cfg.coverOpacity))) : 1.0;
+ const solidCol = cfg.coverColor || '#000000';
+ const bgCol = coverOp <= 0.001 ? 'transparent' : (coverOp >= 0.999 ? solidCol : `color-mix(in srgb, ${solidCol} ${Math.round(coverOp * 100)}%, transparent)`);
+
+ if (Number(cfg.solidCover) === 1) {
+ if (coverCircle.style.backgroundImage && coverCircle.style.backgroundImage !== 'none' && !coverCircle._origBgImage) {
+ coverCircle._origBgImage = coverCircle.style.backgroundImage;
+ }
+ if (coverCircle.style.backgroundImage !== 'none') {
+ coverCircle.style.backgroundImage = 'none';
+ }
+ coverCircle.style.backgroundColor = bgCol;
+ } else if (coverCircle._origBgImage && coverCircle.style.backgroundImage === 'none') {
+ coverCircle.style.backgroundImage = coverCircle._origBgImage;
+ coverCircle.style.backgroundColor = bgCol;
+ } else if (!coverCircle._origBgImage && (!coverCircle.style.backgroundImage || coverCircle.style.backgroundImage === 'none')) {
+ coverCircle.style.backgroundImage = 'none';
+ coverCircle.style.backgroundColor = bgCol;
+ }
+
+ let innerCanvas = coverCircle.querySelector('.audio-eye-inner-canvas');
+ if (!innerCanvas) {
+ innerCanvas = document.createElement('canvas');
+ innerCanvas.className = 'audio-eye-inner-canvas';
+ innerCanvas.width = 300;
+ innerCanvas.height = 300;
+ coverCircle.insertBefore(innerCanvas, coverCircle.firstChild);
+ }
+
+ if (noteIcon) {
+ noteIcon.style.display = Number(cfg.innerHideNote) === 1 ? 'none' : '';
+ }
}
if (noteIcon || coverCircle) {
- if (!audioElement.paused && audioCtx.state === 'running') {
+ if (!audioElement.paused) {
+ if (audioCtx && audioCtx.state !== 'running') {
+ audioCtx.resume().catch(() => {});
+ }
// Bass energy from kick/sub-bass bins (1 to 10): peak transient + body
let bassMax = 0;
let bassSum = 0;
@@ -3960,58 +5434,625 @@ window.cancelAnimFrame = (function () {
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;
+ // Normalized composite glow energy:
+ // Combines strong kick/bass transient impact (70%) with melodic bloom (20%) and high sparkle (10%)
+ const dynamism = cfg.glowDynamism !== undefined ? cfg.glowDynamism : 1.50;
+ const glowSens = cfg.glowSensitivity !== undefined ? Number(cfg.glowSensitivity) : 1.0;
+ const rawGlowEnergy = Math.min(1.0, (punch * 0.70 + midEnergy * 0.20 + highEnergy * 0.10) * glowSens);
+ const targetGlowNorm = Math.pow(rawGlowEnergy, Math.max(0.4, dynamism));
- // Attack & release smoothing
+ // Scale smoothing (instant snap & decay for speaker thump)
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;
+
+ // Dedicated glow smoothing (butter-smooth swell & organic dissipation)
+ const smoothFactor = cfg.glowSmoothness !== undefined ? Number(cfg.glowSmoothness) : 0.70;
+ const speedScale = Math.max(0.08, 1.0 - smoothFactor * 0.70);
+ const glowAttack = (cfg.glowAttack !== undefined ? Number(cfg.glowAttack) : 0.85) * speedScale;
+ const glowRelease = (cfg.glowDecay !== undefined ? Number(cfg.glowDecay) : (cfg.glowRelease !== undefined ? Number(cfg.glowRelease) : 0.35)) * speedScale;
+ const glowSpeed = targetGlowNorm > smoothGlow ? glowAttack : glowRelease;
+ smoothGlow += (targetGlowNorm - smoothGlow) * Math.min(1.0, Math.max(0.01, glowSpeed));
// 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)}))`;
+ const notePct = Math.min(100, Math.round((0.55 + (smoothScale - 1) * 1.5) * 100));
+ if (Number(cfg.enableBeatHue) === 1) {
+ noteIcon.style.color = accent;
+ }
+ if (coverCircle && coverCircle.contains(noteIcon)) {
+ // Note is inside the eye circle; inherits scale from circle, add tilt & glow
+ noteIcon.style.transform = `rotate(${tilt.toFixed(2)}deg)`;
+ noteIcon.style.filter = `drop-shadow(0 0 ${Math.round(12 + smoothGlow * 20)}px color-mix(in srgb, ${accent} ${notePct}%, transparent))`;
+ } else {
+ noteIcon.style.transform = `scale(${smoothScale.toFixed(3)}) rotate(${tilt.toFixed(2)}deg)`;
+ noteIcon.style.filter = `drop-shadow(0 0 ${Math.round(14 + smoothGlow * 25)}px color-mix(in srgb, ${accent} ${notePct}%, transparent))`;
+ }
}
if (coverCircle) {
- // Direct scale bounce: the disc visibly thumps like a speaker cone
+ coverCircle._visualizerDriving = true;
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)})`;
+ const baseGlow = cfg.coverGlowBase !== undefined ? cfg.coverGlowBase : 35;
+ const glowReach = cfg.glowIntensity !== undefined ? cfg.glowIntensity : 250;
+ const glowMult = cfg.glowBrightness !== undefined ? cfg.glowBrightness : 1.0;
+
+ // Dynamic glow spread: smoothGlow expands outward from resting base to dynamic reach
+ const dynSpread = smoothGlow * glowReach;
+ const glow1 = Math.round(baseGlow * 0.20 + dynSpread * 0.18);
+ const glow2 = Math.round(baseGlow * 0.50 + dynSpread * 0.50);
+ const glow3 = Math.round(baseGlow * 1.00 + dynSpread * 0.95);
+ const glow4 = Math.round(baseGlow * 1.60 + dynSpread * 1.55);
+
+ // Continuous breathing opacities with high dynamic contrast
+ const bMult = Math.min(1.4, Math.max(0.2, glowMult * 0.35));
+ const pct1 = Math.min(100, Math.max(0, Math.round((0.36 + smoothGlow * 0.64) * bMult * 100)));
+ const pct2 = Math.min(100, Math.max(0, Math.round((0.22 + smoothGlow * 0.72) * bMult * 100)));
+ const pct3 = Math.min(100, Math.max(0, Math.round((0.10 + smoothGlow * 0.68) * bMult * 100)));
+ const pct4 = Math.min(100, Math.max(0, Math.round((0.03 + smoothGlow * 0.55) * bMult * 100)));
+ const ringOp = cfg.outerRingOpacity !== undefined ? Math.min(1, Math.max(0, Number(cfg.outerRingOpacity))) : 1.0;
+ const borderPct = Math.min(100, Math.max(0, Math.round((0.35 + smoothGlow * 0.65) * 100 * ringOp)));
+
+ const curX = (coverCircle._eyeX || 0).toFixed(1);
+ const curY = (coverCircle._eyeY || 0).toFixed(1);
+ const curTiltX = (coverCircle._eyeTiltX || 0).toFixed(1);
+ const curTiltY = (coverCircle._eyeTiltY || 0).toFixed(1);
+ coverCircle.style.transform = `translate(calc(-50% + ${curX}px), calc(-50% + ${curY}px)) perspective(600px) rotateX(${curTiltX}deg) rotateY(${curTiltY}deg) scale(${circleScale.toFixed(3)})`;
+ if (glowMult <= 0) {
+ coverCircle.style.boxShadow = `0 14px 45px rgba(0, 0, 0, 0.9)`;
+ } else {
+ coverCircle.style.boxShadow = `0 14px 45px rgba(0, 0, 0, 0.9), 0 0 ${glow1}px color-mix(in srgb, ${accent} ${pct1}%, transparent), 0 0 ${glow2}px color-mix(in srgb, ${accent} ${pct2}%, transparent), 0 0 ${glow3}px color-mix(in srgb, ${accent} ${pct3}%, transparent), 0 0 ${glow4}px color-mix(in srgb, ${accent} ${pct4}%, transparent)`;
+ }
+ if (ringOp <= 0.001) {
+ coverCircle.style.borderColor = 'transparent';
+ } else {
+ coverCircle.style.borderColor = `color-mix(in srgb, ${accent} ${borderPct}%, transparent)`;
+ }
+
+ // Dynamic Background Gradient Layer Rendering
+ if (bgEl) {
+ const enableBg = Number(cfg.enableBgGradient !== undefined ? cfg.enableBgGradient : 1) === 1;
+ if (!enableBg || audioElement.paused) {
+ bgEl.style.opacity = '0';
+ } else {
+ const baseOp = cfg.bgGradientOpacity !== undefined ? Number(cfg.bgGradientOpacity) : 0.40;
+ const reactMult = cfg.bgGradientReactivity !== undefined ? Number(cfg.bgGradientReactivity) : 1.20;
+ const spreadPct = cfg.bgGradientSpread !== undefined ? Number(cfg.bgGradientSpread) : 75;
+ const styleMode = cfg.bgGradientStyle !== undefined ? Number(cfg.bgGradientStyle) : 1;
+ const bgCol = (Number(cfg.useCustomBgColor) === 1 && cfg.bgGradientColor) ? cfg.bgGradientColor : accent;
+
+ const pulse = Math.min(1.0, (smoothGlow * 0.70 + kickEnergy * 0.30) * reactMult);
+ const dynamicOp = Math.min(1.0, Math.max(0.0, baseOp + pulse * (1.0 - baseOp * 0.4)));
+ bgEl.style.opacity = dynamicOp.toFixed(3);
+
+ const cX = coverCircle && coverCircle._eyeX ? Number(coverCircle._eyeX) : 0;
+ const cY = coverCircle && coverCircle._eyeY ? Number(coverCircle._eyeY) : 0;
+ const focalX = `calc(50% + ${(cX * 0.5).toFixed(1)}px)`;
+ const focalY = `calc(50% + ${(cY * 0.5).toFixed(1)}px)`;
+ const dynamicSpread = Math.round(spreadPct * (1.0 + pulse * 0.45));
+
+ if (styleMode === 1) {
+ // Style 1: Radial Beat Aura (breathing spherical bloom from behind disc)
+ const corePct = Math.min(100, Math.round(25 + pulse * 45));
+ const midPct = Math.min(100, Math.round(10 + pulse * 30));
+ bgEl.style.background = `radial-gradient(circle at ${focalX} ${focalY}, color-mix(in srgb, ${bgCol} ${corePct}%, transparent) 0%, color-mix(in srgb, ${bgCol} ${midPct}%, transparent) ${Math.round(dynamicSpread * 0.5)}%, transparent ${dynamicSpread}%)`;
+ } else if (styleMode === 2) {
+ // Style 2: Dual Atmospheric Fog (cosmic corners with reactive dark center)
+ const edgePct = Math.min(100, Math.round(15 + pulse * 50));
+ bgEl.style.background = `radial-gradient(ellipse at 50% 120%, color-mix(in srgb, ${bgCol} ${edgePct}%, transparent) 0%, transparent 65%), radial-gradient(ellipse at 50% -20%, color-mix(in srgb, ${bgCol} ${Math.round(edgePct * 0.7)}%, transparent) 0%, transparent 60%)`;
+ } else if (styleMode === 3) {
+ // Style 3: Conic Sweep / Stargate Vortex
+ const rotAngle = Math.round(((performance.now() * 0.04) % 360));
+ const corePct = Math.min(100, Math.round(18 + pulse * 42));
+ bgEl.style.background = `conic-gradient(from ${rotAngle}deg at ${focalX} ${focalY}, transparent 0deg, color-mix(in srgb, ${bgCol} ${corePct}%, transparent) 90deg, transparent 180deg, color-mix(in srgb, ${bgCol} ${corePct}%, transparent) 270deg, transparent 360deg)`;
+ } else if (styleMode === 4) {
+ // Style 4: Horizontal Horizon Flare (cinematic anamorphic flare beam)
+ const beamHeight = Math.max(8, Math.round(18 + pulse * 35));
+ const beamPct = Math.min(100, Math.round(25 + pulse * 55));
+ bgEl.style.background = `radial-gradient(ellipse 90% ${beamHeight}% at ${focalX} ${focalY}, color-mix(in srgb, ${bgCol} ${beamPct}%, transparent) 0%, color-mix(in srgb, ${bgCol} ${Math.round(beamPct * 0.35)}%, transparent) 55%, transparent 85%)`;
+ }
+ }
+ }
+
+ // Inner Eye Visualizer Rendering
+ const innerCanvas = coverCircle.querySelector('.audio-eye-inner-canvas');
+ if (innerCanvas) {
+ const rMax = Math.max(30, Math.min(800, cfg.innerRadius !== undefined ? Number(cfg.innerRadius) : (cfg.innerSize !== undefined ? Number(cfg.innerSize) : 150)));
+ const rMin = Math.max(0, Math.min(rMax - 6, cfg.innerPupilRadius !== undefined ? Number(cfg.innerPupilRadius) : 0));
+
+ const targetDim = Math.max(300, Math.round(rMax * 2 + 20));
+ if (innerCanvas.width !== targetDim || innerCanvas.height !== targetDim) {
+ innerCanvas.width = targetDim;
+ innerCanvas.height = targetDim;
+ }
+
+ const currentCoverSize = Math.max(40, cfg.coverSize || 110);
+ const canvasDisplaySize = Math.round(currentCoverSize * (rMax / 150));
+ const displayPx = canvasDisplaySize + 'px';
+ if (innerCanvas.style.width !== displayPx) {
+ innerCanvas.style.width = displayPx;
+ innerCanvas.style.height = displayPx;
+ }
+
+ const inCtx = innerCanvas.getContext('2d');
+ inCtx.clearRect(0, 0, innerCanvas.width, innerCanvas.height);
+
+ const showInner = cfg.enableInnerBars !== undefined ? Number(cfg.enableInnerBars) === 1 : true;
+ if (showInner && !audioElement.paused) {
+ const mode = cfg.innerBarsMode !== undefined ? Number(cfg.innerBarsMode) : 1;
+ const iGlow = cfg.innerBarGlow !== undefined ? cfg.innerBarGlow : 12;
+ const iHMult = cfg.innerBarHeight !== undefined ? cfg.innerBarHeight : 1.0;
+ const iBW = Math.max(1, cfg.innerBarWidth !== undefined ? cfg.innerBarWidth : 6);
+ const cw = innerCanvas.width;
+ const ch = innerCanvas.height;
+ const cx = cw / 2;
+ const cy = ch / 2;
+
+ const innerAccent = (Number(cfg.useCustomInnerColor) === 1 && cfg.innerBarColor) ? cfg.innerBarColor : accent;
+ if (iGlow > 0) {
+ inCtx.shadowBlur = iGlow;
+ inCtx.shadowColor = innerAccent;
+ } else {
+ inCtx.shadowBlur = 0;
+ }
+ inCtx.fillStyle = innerAccent;
+ inCtx.strokeStyle = innerAccent;
+ const inOpacity = cfg.innerBarOpacity !== undefined ? Math.min(1, Math.max(0, Number(cfg.innerBarOpacity))) : 1.0;
+ inCtx.globalAlpha = inOpacity;
+ const pupilRingOp = cfg.innerPupilRingOpacity !== undefined ? Math.min(1, Math.max(0, Number(cfg.innerPupilRingOpacity))) : (cfg.outerRingOpacity !== undefined ? Number(cfg.outerRingOpacity) : 0);
+
+ const numInnerBars = Math.max(2, Math.min(512, cfg.innerBarCount !== undefined ? Number(cfg.innerBarCount) : 32));
+ const hBoost = cfg.innerHighBoost !== undefined ? Number(cfg.innerHighBoost) : 0.1;
+ const sRate = (audioCtx && audioCtx.sampleRate) || 44100;
+ const binHz = sRate / 2048; // ~21.5 Hz per FFT bin
+ const fMin = 28; // Deep Sub-Bass
+ const fMax = 15000; // Crisp Air & Sizzle
+
+ const maxLen = (rMax - rMin) * iHMult;
+ const rotDeg = cfg.innerRadialRotation !== undefined ? Number(cfg.innerRadialRotation) : 90;
+ const rotRad = (rotDeg * Math.PI) / 180;
+
+ if (mode === 1) {
+ // Mode 1: Mirrored Full-Spectrum Horizon Equalizer across eye disc
+ const spanW = Math.max(40, Math.round(rMax * 1.74));
+ const barGap = Math.max(2, Math.floor((spanW - numInnerBars * iBW) / Math.max(1, numInnerBars - 1)));
+ const totalW = numInnerBars * iBW + (numInnerBars - 1) * barGap;
+ const startX = (cw - totalW) / 2;
+
+ for (let i = 0; i < numInnerBars; i++) {
+ const normIdx = i / Math.max(1, numInnerBars - 1);
+ // Logarithmic musical semitone / 1/3-octave band sampling
+ const f1 = fMin * Math.pow(fMax / fMin, i / numInnerBars);
+ const f2 = fMin * Math.pow(fMax / fMin, (i + 1) / numInnerBars);
+ const sBin = Math.max(1, Math.min(count - 1, Math.floor(f1 / binHz)));
+ const eBin = Math.max(sBin, Math.min(count - 1, Math.ceil(f2 / binHz)));
+
+ let bMax = 0;
+ let bSum = 0;
+ let bCnt = 0;
+ for (let b = sBin; b <= eBin; b++) {
+ const val = data[b] || 0;
+ if (val > bMax) bMax = val;
+ bSum += val;
+ bCnt++;
+ }
+ const rawVal = bCnt > 0 ? (bMax * 0.70 + (bSum / bCnt) * 0.30) : (data[sBin] || 0);
+
+ // Equal-energy pink noise compensation (allows melody, keys, vocals, and hi-hats to dance)
+ const tilt = Math.max(0.05, Math.min(1.0, hBoost) + Math.pow(normIdx, 0.72) * (hBoost * 3.5));
+ const bVal = Math.min(255, rawVal * tilt);
+ const bPct = bVal / 255;
+
+ const x = startX + i * (iBW + barGap);
+ const barCenterX = x + iBW / 2;
+ const distX = Math.abs(barCenterX - cx);
+ const maxInsideCircle = distX < (rMax - 2) ? Math.sqrt(Math.max(0, (rMax - 2) * (rMax - 2) - distX * distX)) * 2 - 12 : 24;
+ const maxH = Math.min(maxInsideCircle, (rMax - 5) * iHMult);
+ const barH = Math.max(4, bPct * maxH);
+ const y = cy - barH / 2;
+ const radius = Math.min(iBW / 2, barH / 2);
+
+ inCtx.beginPath();
+ if (typeof inCtx.roundRect === 'function') {
+ inCtx.roundRect(x, y, iBW, barH, [radius, radius, radius, radius]);
+ } else {
+ inCtx.rect(x, y, iBW, barH);
+ }
+ inCtx.fill();
+ }
+ } else if (mode === 2) {
+ // Mode 2: 360° Full-Spectrum Outward Radial Iris
+ const numSpokes = Math.max(4, Math.min(1024, numInnerBars * 2));
+
+ for (let i = 0; i < numSpokes; i++) {
+ const angle = (i / numSpokes) * Math.PI * 2 + rotRad;
+ // Symmetrical musical octave distribution around iris circle
+ const sym = 1 - Math.abs((i / numSpokes) * 2 - 1);
+ const f1 = fMin * Math.pow(fMax / fMin, sym);
+ const f2 = fMin * Math.pow(fMax / fMin, Math.min(1.0, sym + 1.25 / numSpokes));
+ const sBin = Math.max(1, Math.min(count - 1, Math.floor(f1 / binHz)));
+ const eBin = Math.max(sBin, Math.min(count - 1, Math.ceil(f2 / binHz)));
+
+ let bMax = 0;
+ let bSum = 0;
+ let bCnt = 0;
+ for (let b = sBin; b <= eBin; b++) {
+ const val = data[b] || 0;
+ if (val > bMax) bMax = val;
+ bSum += val;
+ bCnt++;
+ }
+ const rawVal = bCnt > 0 ? (bMax * 0.70 + (bSum / bCnt) * 0.30) : (data[sBin] || 0);
+ const tilt = Math.max(0.05, Math.min(1.0, hBoost) + Math.pow(sym, 0.72) * (hBoost * 3.5));
+ const bVal = Math.min(255, rawVal * tilt);
+ const len = Math.max(3, (bVal / 255) * maxLen);
+
+ const x1 = cx + Math.cos(angle) * rMin;
+ const y1 = cy + Math.sin(angle) * rMin;
+ const x2 = cx + Math.cos(angle) * (rMin + len);
+ const y2 = cy + Math.sin(angle) * (rMin + len);
+
+ inCtx.beginPath();
+ inCtx.lineWidth = Math.max(1, iBW);
+ inCtx.lineCap = 'round';
+ inCtx.moveTo(x1, y1);
+ inCtx.lineTo(x2, y2);
+ inCtx.stroke();
+ }
+
+ if (pupilRingOp > 0.001 && rMin > 3) {
+ inCtx.save();
+ inCtx.globalAlpha = inOpacity * pupilRingOp;
+ inCtx.beginPath();
+ inCtx.arc(cx, cy, rMin - 2, 0, Math.PI * 2);
+ inCtx.lineWidth = 1.5;
+ inCtx.stroke();
+ inCtx.restore();
+ }
+ } else if (mode === 3) {
+ // Mode 3: Bottom Arc Full-Spectrum Equalizer
+ const spanW = Math.max(40, Math.round(rMax * 1.74));
+ const barGap = Math.max(2, Math.floor((spanW - numInnerBars * iBW) / Math.max(1, numInnerBars - 1)));
+ const totalW = numInnerBars * iBW + (numInnerBars - 1) * barGap;
+ const startX = (cw - totalW) / 2;
+ const baseY = cy + rMax * 0.75;
+
+ for (let i = 0; i < numInnerBars; i++) {
+ const normIdx = i / Math.max(1, numInnerBars - 1);
+ const f1 = fMin * Math.pow(fMax / fMin, i / numInnerBars);
+ const f2 = fMin * Math.pow(fMax / fMin, (i + 1) / numInnerBars);
+ const sBin = Math.max(1, Math.min(count - 1, Math.floor(f1 / binHz)));
+ const eBin = Math.max(sBin, Math.min(count - 1, Math.ceil(f2 / binHz)));
+
+ let bMax = 0;
+ let bSum = 0;
+ let bCnt = 0;
+ for (let b = sBin; b <= eBin; b++) {
+ const val = data[b] || 0;
+ if (val > bMax) bMax = val;
+ bSum += val;
+ bCnt++;
+ }
+ const rawVal = bCnt > 0 ? (bMax * 0.70 + (bSum / bCnt) * 0.30) : (data[sBin] || 0);
+ const tilt = Math.max(0.05, Math.min(1.0, hBoost) + Math.pow(normIdx, 0.72) * (hBoost * 3.5));
+ const bVal = Math.min(255, rawVal * tilt);
+ const bPct = bVal / 255;
+
+ const x = startX + i * (iBW + barGap);
+ const barCenterX = x + iBW / 2;
+ const distX = Math.abs(barCenterX - cx);
+ const maxInside = distX < (rMax - 5) ? Math.sqrt(Math.max(0, (rMax - 5) * (rMax - 5) - distX * distX)) : 20;
+ const maxH = Math.min(maxInside + 25, rMax * iHMult);
+ const barH = Math.max(4, bPct * maxH);
+ const y = baseY - barH;
+ const radius = Math.min(iBW / 2, barH / 2);
+
+ inCtx.beginPath();
+ if (typeof inCtx.roundRect === 'function') {
+ inCtx.roundRect(x, y, iBW, barH, [radius, radius, 0, 0]);
+ } else {
+ inCtx.rect(x, y, iBW, barH);
+ }
+ inCtx.fill();
+ }
+ } else if (mode === 4) {
+ // Mode 4: 360° Inverted Inward Radial Iris (spikes fire inward from outer rim)
+ const numSpokes = Math.max(4, Math.min(1024, numInnerBars * 2));
+
+ for (let i = 0; i < numSpokes; i++) {
+ const angle = (i / numSpokes) * Math.PI * 2 + rotRad;
+ const sym = 1 - Math.abs((i / numSpokes) * 2 - 1);
+ const f1 = fMin * Math.pow(fMax / fMin, sym);
+ const f2 = fMin * Math.pow(fMax / fMin, Math.min(1.0, sym + 1.25 / numSpokes));
+ const sBin = Math.max(1, Math.min(count - 1, Math.floor(f1 / binHz)));
+ const eBin = Math.max(sBin, Math.min(count - 1, Math.ceil(f2 / binHz)));
+
+ let bMax = 0, bSum = 0, bCnt = 0;
+ for (let b = sBin; b <= eBin; b++) {
+ const val = data[b] || 0;
+ if (val > bMax) bMax = val;
+ bSum += val;
+ bCnt++;
+ }
+ const rawVal = bCnt > 0 ? (bMax * 0.70 + (bSum / bCnt) * 0.30) : (data[sBin] || 0);
+ const tilt = Math.max(0.05, Math.min(1.0, hBoost) + Math.pow(sym, 0.72) * (hBoost * 3.5));
+ const bVal = Math.min(255, rawVal * tilt);
+ const len = Math.max(3, (bVal / 255) * maxLen);
+
+ const x1 = cx + Math.cos(angle) * rMax;
+ const y1 = cy + Math.sin(angle) * rMax;
+ const x2 = cx + Math.cos(angle) * (rMax - len);
+ const y2 = cy + Math.sin(angle) * (rMax - len);
+
+ inCtx.beginPath();
+ inCtx.lineWidth = Math.max(1, iBW);
+ inCtx.lineCap = 'round';
+ inCtx.moveTo(x1, y1);
+ inCtx.lineTo(x2, y2);
+ inCtx.stroke();
+ }
+
+ // Outer glowing rim ring
+ if (ringOp > 0.001) {
+ inCtx.save();
+ inCtx.globalAlpha = inOpacity * ringOp;
+ inCtx.beginPath();
+ inCtx.arc(cx, cy, rMax, 0, Math.PI * 2);
+ inCtx.lineWidth = 1.5;
+ inCtx.stroke();
+ inCtx.restore();
+ }
+
+ if (pupilRingOp > 0.001 && rMin > 3) {
+ inCtx.save();
+ inCtx.globalAlpha = inOpacity * pupilRingOp;
+ inCtx.beginPath();
+ inCtx.arc(cx, cy, rMin - 2, 0, Math.PI * 2);
+ inCtx.lineWidth = 1.5;
+ inCtx.stroke();
+ inCtx.restore();
+ }
+ } else if (mode === 5) {
+ // Mode 5: Reversed Inverted Radial Iris (inward spikes + reversed frequency order)
+ const numSpokes = Math.max(4, Math.min(1024, numInnerBars * 2));
+
+ for (let i = 0; i < numSpokes; i++) {
+ const angle = (i / numSpokes) * Math.PI * 2 + rotRad;
+ // Inverted / reversed frequency symmetry
+ const sym = Math.abs((i / numSpokes) * 2 - 1);
+ const f1 = fMin * Math.pow(fMax / fMin, sym);
+ const f2 = fMin * Math.pow(fMax / fMin, Math.min(1.0, sym + 1.25 / numSpokes));
+ const sBin = Math.max(1, Math.min(count - 1, Math.floor(f1 / binHz)));
+ const eBin = Math.max(sBin, Math.min(count - 1, Math.ceil(f2 / binHz)));
+
+ let bMax = 0, bSum = 0, bCnt = 0;
+ for (let b = sBin; b <= eBin; b++) {
+ const val = data[b] || 0;
+ if (val > bMax) bMax = val;
+ bSum += val;
+ bCnt++;
+ }
+ const rawVal = bCnt > 0 ? (bMax * 0.70 + (bSum / bCnt) * 0.30) : (data[sBin] || 0);
+ const tilt = Math.max(0.05, Math.min(1.0, hBoost) + Math.pow(sym, 0.72) * (hBoost * 3.5));
+ const bVal = Math.min(255, rawVal * tilt);
+ const len = Math.max(3, (bVal / 255) * maxLen);
+
+ const x1 = cx + Math.cos(angle) * rMax;
+ const y1 = cy + Math.sin(angle) * rMax;
+ const x2 = cx + Math.cos(angle) * (rMax - len);
+ const y2 = cy + Math.sin(angle) * (rMax - len);
+
+ inCtx.beginPath();
+ inCtx.lineWidth = Math.max(1, iBW);
+ inCtx.lineCap = 'round';
+ inCtx.moveTo(x1, y1);
+ inCtx.lineTo(x2, y2);
+ inCtx.stroke();
+ }
+
+ // Outer glowing rim ring & inner pupil ring
+ if (ringOp > 0.001) {
+ inCtx.save();
+ inCtx.globalAlpha = inOpacity * ringOp;
+ inCtx.beginPath();
+ inCtx.arc(cx, cy, rMax, 0, Math.PI * 2);
+ inCtx.lineWidth = 1.5;
+ inCtx.stroke();
+ inCtx.restore();
+ }
+
+ if (pupilRingOp > 0.001 && rMin > 3) {
+ inCtx.save();
+ inCtx.globalAlpha = inOpacity * pupilRingOp;
+ inCtx.beginPath();
+ inCtx.arc(cx, cy, rMin - 2, 0, Math.PI * 2);
+ inCtx.lineWidth = 1.5;
+ inCtx.stroke();
+ inCtx.restore();
+ }
+ } else if (mode === 6) {
+ // Mode 6: Dual Bi-Directional Stargate Iris (spikes pulse inward from rim AND outward from pupil!)
+ const numSpokes = Math.max(4, Math.min(1024, numInnerBars * 2));
+ const halfLen = maxLen * 0.52;
+
+ for (let i = 0; i < numSpokes; i++) {
+ const angle = (i / numSpokes) * Math.PI * 2 + rotRad;
+ const sym = 1 - Math.abs((i / numSpokes) * 2 - 1);
+ const f1 = fMin * Math.pow(fMax / fMin, sym);
+ const f2 = fMin * Math.pow(fMax / fMin, Math.min(1.0, sym + 1.25 / numSpokes));
+ const sBin = Math.max(1, Math.min(count - 1, Math.floor(f1 / binHz)));
+ const eBin = Math.max(sBin, Math.min(count - 1, Math.ceil(f2 / binHz)));
+
+ let bMax = 0, bSum = 0, bCnt = 0;
+ for (let b = sBin; b <= eBin; b++) {
+ const val = data[b] || 0;
+ if (val > bMax) bMax = val;
+ bSum += val;
+ bCnt++;
+ }
+ const rawVal = bCnt > 0 ? (bMax * 0.70 + (bSum / bCnt) * 0.30) : (data[sBin] || 0);
+ const tilt = Math.max(0.05, Math.min(1.0, hBoost) + Math.pow(sym, 0.72) * (hBoost * 3.5));
+ const bVal = Math.min(255, rawVal * tilt);
+ const len = Math.max(3, (bVal / 255) * halfLen);
+
+ inCtx.beginPath();
+ inCtx.lineWidth = Math.max(1, iBW);
+ inCtx.lineCap = 'round';
+ // Outward spike from pupil
+ inCtx.moveTo(cx + Math.cos(angle) * rMin, cy + Math.sin(angle) * rMin);
+ inCtx.lineTo(cx + Math.cos(angle) * (rMin + len), cy + Math.sin(angle) * (rMin + len));
+ // Inward spike from outer rim
+ inCtx.moveTo(cx + Math.cos(angle) * rMax, cy + Math.sin(angle) * rMax);
+ inCtx.lineTo(cx + Math.cos(angle) * (rMax - len), cy + Math.sin(angle) * (rMax - len));
+ inCtx.stroke();
+ }
+
+ if (pupilRingOp > 0.001 && rMin > 3) {
+ inCtx.save();
+ inCtx.globalAlpha = inOpacity * pupilRingOp;
+ inCtx.beginPath();
+ inCtx.arc(cx, cy, rMin - 2, 0, Math.PI * 2);
+ inCtx.lineWidth = 1.5;
+ inCtx.stroke();
+ inCtx.restore();
+ }
+
+ if (ringOp > 0.001) {
+ inCtx.save();
+ inCtx.globalAlpha = inOpacity * ringOp;
+ inCtx.beginPath();
+ inCtx.arc(cx, cy, rMax, 0, Math.PI * 2);
+ inCtx.lineWidth = 1.5;
+ inCtx.stroke();
+ inCtx.restore();
+ }
+ } else if (mode === 7) {
+ // Mode 7: 360° Continuous Radar Iris (seamless full-circle chromatic spectrum)
+ const numSpokes = Math.max(4, Math.min(1024, numInnerBars * 2));
+
+ for (let i = 0; i < numSpokes; i++) {
+ const angle = (i / numSpokes) * Math.PI * 2 + rotRad;
+ const sym = i / numSpokes;
+ const f1 = fMin * Math.pow(fMax / fMin, sym);
+ const f2 = fMin * Math.pow(fMax / fMin, Math.min(1.0, sym + 1.25 / numSpokes));
+ const sBin = Math.max(1, Math.min(count - 1, Math.floor(f1 / binHz)));
+ const eBin = Math.max(sBin, Math.min(count - 1, Math.ceil(f2 / binHz)));
+
+ let bMax = 0, bSum = 0, bCnt = 0;
+ for (let b = sBin; b <= eBin; b++) {
+ const val = data[b] || 0;
+ if (val > bMax) bMax = val;
+ bSum += val;
+ bCnt++;
+ }
+ const rawVal = bCnt > 0 ? (bMax * 0.70 + (bSum / bCnt) * 0.30) : (data[sBin] || 0);
+ const tilt = Math.max(0.05, Math.min(1.0, hBoost) + Math.pow(sym, 0.72) * (hBoost * 3.5));
+ const bVal = Math.min(255, rawVal * tilt);
+ const len = Math.max(3, (bVal / 255) * maxLen);
+
+ const x1 = cx + Math.cos(angle) * rMin;
+ const y1 = cy + Math.sin(angle) * rMin;
+ const x2 = cx + Math.cos(angle) * (rMin + len);
+ const y2 = cy + Math.sin(angle) * (rMin + len);
+
+ inCtx.beginPath();
+ inCtx.lineWidth = Math.max(1, iBW);
+ inCtx.lineCap = 'round';
+ inCtx.moveTo(x1, y1);
+ inCtx.lineTo(x2, y2);
+ inCtx.stroke();
+ }
+
+ if (pupilRingOp > 0.001 && rMin > 3) {
+ inCtx.save();
+ inCtx.globalAlpha = inOpacity * pupilRingOp;
+ inCtx.beginPath();
+ inCtx.arc(cx, cy, rMin - 2, 0, Math.PI * 2);
+ inCtx.lineWidth = 1.5;
+ inCtx.stroke();
+ inCtx.restore();
+ }
+ }
+ inCtx.shadowBlur = 0;
+ inCtx.globalAlpha = 1.0;
+ }
+ }
}
} else {
// Decay smoothly back to neutral state when paused or idle
- if (smoothScale > 1.002) {
+ if (bgEl) {
+ bgEl.style.opacity = '0';
+ }
+ if (coverCircle) {
+ const inC = coverCircle.querySelector('.audio-eye-inner-canvas');
+ if (inC) {
+ const inCtx = inC.getContext('2d');
+ if (inCtx) inCtx.clearRect(0, 0, inC.width, inC.height);
+ }
+ }
+ if (smoothScale > 1.002 || smoothGlow > 0.005) {
smoothScale += (1 - smoothScale) * 0.16;
+ smoothGlow += (0 - smoothGlow) * 0.09;
if (noteIcon) {
- noteIcon.style.transform = `scale(${smoothScale.toFixed(3)})`;
- noteIcon.style.filter = '';
+ if (coverCircle && coverCircle.contains(noteIcon)) {
+ noteIcon.style.transform = '';
+ noteIcon.style.filter = '';
+ } else {
+ noteIcon.style.transform = `scale(${smoothScale.toFixed(3)})`;
+ noteIcon.style.filter = '';
+ }
}
if (coverCircle) {
- coverCircle.style.transform = `translate(-50%, -50%) scale(${smoothScale.toFixed(3)})`;
+ const curX = (coverCircle._eyeX || 0).toFixed(1);
+ const curY = (coverCircle._eyeY || 0).toFixed(1);
+ const curTiltX = (coverCircle._eyeTiltX || 0).toFixed(1);
+ const curTiltY = (coverCircle._eyeTiltY || 0).toFixed(1);
+ coverCircle.style.transform = `translate(calc(-50% + ${curX}px), calc(-50% + ${curY}px)) perspective(600px) rotateX(${curTiltX}deg) rotateY(${curTiltY}deg) scale(${smoothScale.toFixed(3)})`;
+ const baseGlow = cfg.coverGlowBase !== undefined ? cfg.coverGlowBase : 35;
+ const glowReach = cfg.glowIntensity !== undefined ? cfg.glowIntensity : 250;
+ const glowMult = cfg.glowBrightness !== undefined ? cfg.glowBrightness : 1.0;
+ const dynSpread = smoothGlow * glowReach;
+ const glow1 = Math.round(baseGlow * 0.20 + dynSpread * 0.18);
+ const glow2 = Math.round(baseGlow * 0.50 + dynSpread * 0.50);
+ const glow3 = Math.round(baseGlow * 1.00 + dynSpread * 0.95);
+ const bMult = Math.min(1.4, Math.max(0.2, glowMult * 0.35));
+ const pct1 = Math.min(100, Math.max(0, Math.round((0.36 + smoothGlow * 0.64) * bMult * 100)));
+ const pct2 = Math.min(100, Math.max(0, Math.round((0.22 + smoothGlow * 0.72) * bMult * 100)));
+ const pct3 = Math.min(100, Math.max(0, Math.round((0.10 + smoothGlow * 0.68) * bMult * 100)));
+ if (glowMult <= 0) {
+ coverCircle.style.boxShadow = `0 14px 45px rgba(0, 0, 0, 0.9)`;
+ } else {
+ coverCircle.style.boxShadow = `0 14px 45px rgba(0, 0, 0, 0.9), 0 0 ${glow1}px color-mix(in srgb, ${accent} ${pct1}%, transparent), 0 0 ${glow2}px color-mix(in srgb, ${accent} ${pct2}%, transparent), 0 0 ${glow3}px color-mix(in srgb, ${accent} ${pct3}%, transparent)`;
+ }
}
- } else if (smoothScale !== 1) {
+ } else if (smoothScale !== 1 || smoothGlow !== 0) {
smoothScale = 1;
+ smoothGlow = 0;
if (noteIcon) {
noteIcon.style.transform = '';
noteIcon.style.filter = '';
}
if (coverCircle) {
- coverCircle.style.transform = 'translate(-50%, -50%)';
- coverCircle.style.boxShadow = '';
- coverCircle.style.borderColor = '';
+ coverCircle._visualizerDriving = false;
+ const baseGlow = cfg.coverGlowBase !== undefined ? cfg.coverGlowBase : 35;
+ const glowMult = cfg.glowBrightness !== undefined ? cfg.glowBrightness : 1.0;
+ if (glowMult <= 0) {
+ coverCircle.style.boxShadow = `0 12px 40px rgba(0, 0, 0, 0.88)`;
+ } else {
+ coverCircle.style.boxShadow = `0 12px 40px rgba(0, 0, 0, 0.88), 0 0 ${baseGlow}px color-mix(in srgb, ${accent} ${Math.round(28 * Math.min(1.5, glowMult))}%, transparent)`;
+ }
+ const ringOp = cfg.outerRingOpacity !== undefined ? Math.min(1, Math.max(0, Number(cfg.outerRingOpacity))) : 1.0;
+ if (ringOp <= 0.001) {
+ coverCircle.style.borderColor = 'transparent';
+ } else {
+ coverCircle.style.borderColor = `color-mix(in srgb, ${accent} ${Math.round(40 * ringOp)}%, transparent)`;
+ }
}
+ } else if (coverCircle) {
+ coverCircle._visualizerDriving = false;
}
}
}
@@ -4026,18 +6067,22 @@ window.cancelAnimFrame = (function () {
visualizerRafId = requestAnimationFrame(loopingFunction);
const resumeAudio = () => {
- if (audioCtx && audioCtx.state === 'suspended') {
- audioCtx.resume();
+ setupAudioSource();
+ if (audioCtx && audioCtx.state !== 'running') {
+ audioCtx.resume().catch(() => {});
}
};
audioElement.addEventListener('play', resumeAudio);
audioElement.addEventListener('playing', resumeAudio);
- const playerWrap = audioElement.closest('.v0ck') || audioElement.parentElement;
+ const playerWrap = audioElement.closest('.v0ck, .album-audio-wrapper, .embed-responsive') || audioElement.parentElement;
if (playerWrap) {
playerWrap.addEventListener('click', resumeAudio, { passive: true });
playerWrap.addEventListener('pointerdown', resumeAudio, { passive: true });
}
+ ['click', 'pointerdown', 'keydown', 'touchstart'].forEach(evt => {
+ window.addEventListener(evt, resumeAudio, { passive: true });
+ });
}
};
@@ -5792,7 +7837,7 @@ window.cancelAnimFrame = (function () {
const params = new URLSearchParams();
params.append('mode', window.activeMode);
const targetHash = new URL(url, window.location.origin).hash.replace(/^#/, '').trim();
- if (targetHash) params.append('subf0ck', targetHash);
+ if (targetHash && !targetHash.startsWith('c')) params.append('subf0ck', targetHash);
if (tag) params.append('tag', tag);
if (hall) params.append('hall', hall);
if (userHall && userHallOwner) {
@@ -6592,27 +8637,69 @@ window.cancelAnimFrame = (function () {
const targetUrlBase = targetUrl.split('#')[0];
const hash = anyLink.hash;
- // If it's a same-page hash link, just jump and don't AJAX reload
- if (targetUrlBase === currentUrl && hash) {
+ // Check if it's a same-item / same-page hash link
+ const isCurrentItem = isItemPath(window.location.href);
+ const isTargetItem = isItemPath(targetUrl);
+ const getItemKey = (u) => {
+ try {
+ const p = new URL(u, window.location.origin).pathname;
+ const parts = p.split('/').filter(Boolean);
+ const keys = parts.filter(s => /^\d+$/.test(s) || /^[a-zA-Z0-9_-]{11}$/.test(s));
+ return keys.length ? keys[keys.length - 1] : null;
+ } catch {
+ return null;
+ }
+ };
+ const currentItemKey = isCurrentItem ? getItemKey(window.location.href) : null;
+ const targetItemKey = isTargetItem ? getItemKey(targetUrl) : null;
+ const isSameItem = isCurrentItem && isTargetItem && currentItemKey && currentItemKey === targetItemKey;
+ const isSamePageHash = (targetUrlBase === currentUrl) || isSameItem;
+
+ // If it's a same-page hash link, just jump / switch subf0ck and don't AJAX reload
+ if (isSamePageHash && hash) {
e.preventDefault();
+ let handled = false;
// If it's a comment link, use the robust CommentSystem logic
if (hash.startsWith('#c') && window.commentSystem) {
const id = hash.substring(2);
window.commentSystem.scrollToComment(id);
+ handled = true;
} else {
- const targetEl = document.querySelector(hash);
- if (targetEl) {
- targetEl.scrollIntoView({ behavior: 'smooth', block: 'center' });
- // Clear any previous highlight, then permanently mark this element
- document.querySelectorAll('.anchor-target').forEach(el => el.classList.remove('anchor-target'));
- targetEl.classList.add('anchor-target');
+ // If it's an album / subf0ck link
+ const subKey = hash.replace(/^#/, '').trim();
+ if (window._currentActiveAlbumGallery && typeof window._currentActiveAlbumGallery.showBySlug === 'function') {
+ handled = window._currentActiveAlbumGallery.showBySlug(subKey);
+ if (handled) {
+ const mediaContainer = document.querySelector('.content, .album-container, .v0ck, #my-video, .main-image');
+ if (mediaContainer) {
+ mediaContainer.scrollIntoView({ behavior: 'smooth', block: 'center' });
+ }
+ }
+ }
+
+ if (!handled) {
+ try {
+ const targetEl = document.querySelector(hash);
+ if (targetEl) {
+ targetEl.scrollIntoView({ behavior: 'smooth', block: 'center' });
+ // Clear any previous highlight, then permanently mark this element
+ document.querySelectorAll('.anchor-target').forEach(el => el.classList.remove('anchor-target'));
+ targetEl.classList.add('anchor-target');
+ handled = true;
+ }
+ } catch (err) {
+ // Ignore invalid querySelector for non-CSS selectors
+ }
}
}
- // Update URL without reload
- history.replaceState(null, null, targetUrl);
- return;
+ if (handled || targetUrlBase === currentUrl) {
+ // Update URL without reload
+ history.replaceState(null, null, targetUrl);
+ window.dispatchEvent(new Event('hashchange'));
+ return;
+ }
}
// Intercept /register and /login — handle entirely client-side
@@ -9174,6 +11261,19 @@ window.cancelAnimFrame = (function () {
swipeRT.yDiff = 0;
swipeRT.isDraggingSidebar = false;
swipeRT.startedInSidebarZone = false;
+ let inStrip = !!(e.target && e.target.closest('.album-thumbnails-strip'));
+ const stripEl = document.querySelector('.album-thumbnails-strip');
+ if (!inStrip && stripEl && stripEl.offsetParent !== null) {
+ const str = stripEl.getBoundingClientRect();
+ const touch0 = e.touches[0];
+ if (
+ touch0.clientX >= str.left && touch0.clientX <= str.right &&
+ touch0.clientY >= str.top && touch0.clientY <= str.bottom
+ ) {
+ inStrip = true;
+ }
+ }
+ swipeRT.startedInStrip = inStrip;
// Coordinate-based steuerung detection — more reliable than e.target.closest() on iOS Safari
// where e.target can be misreported for touch events on complex DOM hierarchies
@@ -9199,15 +11299,15 @@ window.cancelAnimFrame = (function () {
// Mark as candidate — direction will be confirmed on first touchmove
if (isHidden) {
- // Only set sidebar candidate if NOT starting inside .steuerung
- if (touch.clientX > window.innerWidth - 100 && !swipeRT.startedInSteuerung) {
+ // Only set sidebar candidate if NOT starting inside .steuerung or .album-thumbnails-strip
+ if (touch.clientX > window.innerWidth - 100 && !swipeRT.startedInSteuerung && !swipeRT.startedInStrip) {
swipeRT.isSidebarCandidate = true;
swipeRT.startedInSidebarZone = true;
}
} else {
// Sidebar is open: candidate if touch starts inside sidebar bounding box
// Use coordinates, not e.target — iOS Safari can mis-report target in scrollable containers
- if (!swipeRT.startedInSteuerung) {
+ if (!swipeRT.startedInSteuerung && !swipeRT.startedInStrip) {
const sr = sidebar.getBoundingClientRect();
if (touch.clientX >= sr.left && touch.clientX <= sr.right &&
touch.clientY >= sr.top && touch.clientY <= sr.bottom) {
@@ -9223,6 +11323,7 @@ window.cancelAnimFrame = (function () {
document.addEventListener('touchmove', e => {
if (window.f0ckSession?.disable_swiping) return;
if (document.body.classList.contains('modal-open')) return;
+ if (swipeRT.startedInStrip) return;
if (!swipeRT.xDown || !swipeRT.yDown) return;
const touch = e.touches[0];
@@ -9376,8 +11477,8 @@ window.cancelAnimFrame = (function () {
}
if (Math.abs(swipeRT.xDiff) > Math.abs(swipeRT.yDiff)) {
- // Skip navigation if this touch started in the sidebar zone
- if (!swipeRT.startedInSidebarZone) {
+ // Skip navigation if this touch started in the sidebar zone or album thumbnail strip
+ if (!swipeRT.startedInSidebarZone && !swipeRT.startedInStrip) {
// On item pages with .steuerung, only navigate if swipe started inside it
const steuerung = document.querySelector('.steuerung');
const allowNav = !steuerung || swipeRT.startedInSteuerung;
@@ -12169,11 +14270,6 @@ class NotificationSystem {
}
handleActivity(data) {
- // 1. Dispatch to global CommentSystem if active (for Item Page)
- if (window.commentSystem && typeof window.commentSystem.handleLiveComment === 'function') {
- window.commentSystem.handleLiveComment(data);
- }
-
// Respect mode filter
if (typeof window.activeMode !== 'undefined' && window.activeMode !== 3) { // 3 is ALL
if (window.activeMode === 0 && data.tag_id !== 1) return; // SFW mode, item is not SFW
@@ -12303,6 +14399,15 @@ class NotificationSystem {
`;
+ } else if (c.mime.startsWith('audio')) {
+ if (c.has_coverart) {
+ mediaHtml = `
`;
+ } else {
+ mediaHtml = `
+ `;
+ }
} else {
mediaHtml = `
diff --git a/public/s/js/sidebar-activity.js b/public/s/js/sidebar-activity.js
index 66c14ed..fa16b46 100644
--- a/public/s/js/sidebar-activity.js
+++ b/public/s/js/sidebar-activity.js
@@ -532,14 +532,32 @@
else if (rClass === 'sfw' && blurSfw) 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) {
- thumbUrl = `/t/${c.item_id}_blur.webp`;
+ const baseThumb = thumbUrl.replace(/\.webp$/, '');
+ thumbUrl = `${baseThumb}_blur.webp`;
}
if (window.applyThumbCacheBust) thumbUrl = window.applyThumbCacheBust(thumbUrl);
- mediaHtml = `

`;
+ if (isAudio && !c.has_coverart) {
+ mediaHtml = `
+
+ `;
+ } else {
+ mediaHtml = `
+

+
+ `;
+ }
itemPreview = `
@@ -1869,6 +1887,7 @@
}
}
};
+ window.switchSidebarTab = switchSidebarTab;
const initSidebarTabs = () => {
const tabsContainer = document.querySelector('.sidebar-tabs');
diff --git a/public/s/js/v0ck.js b/public/s/js/v0ck.js
index 63f7b23..29d199b 100644
--- a/public/s/js/v0ck.js
+++ b/public/s/js/v0ck.js
@@ -88,6 +88,8 @@ const updateHoverStates = () => {
const isOver = mouseX >= rect.left && mouseX <= rect.right &&
mouseY >= rect.top && mouseY <= rect.bottom;
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 &&
mouseY >= rect.top && mouseY <= rect.bottom) {
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
setTimeout(() => parent.classList.remove("v0ck_no_transition"), 50);
}
@@ -142,14 +146,24 @@ class v0ck {
if (!ph) {
ph = document.createElement('div');
ph.className = 'sidebar-media-placeholder audio';
- ph.innerHTML = '
';
+ ph.innerHTML = '
';
player.prepend(ph);
}
let coverCircle = ph.querySelector('.audio-cover-circle');
if (!coverCircle) {
coverCircle = document.createElement('div');
coverCircle.className = 'audio-cover-circle';
+ coverCircle.innerHTML = '
';
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', '
');
+ }
+ }
}
player.style.backgroundImage = 'none';
player.style.backgroundColor = 'transparent';
@@ -157,7 +171,7 @@ class v0ck {
coverCircle.style.backgroundImage = `url('${poster}')`;
ph.classList.add('has-cover');
} else {
- coverCircle.style.backgroundImage = 'none';
+ coverCircle.style.backgroundImage = '';
ph.classList.remove('has-cover');
}
}
@@ -203,6 +217,23 @@ class v0ck {
let wasPausedWhenStarted = false;
// Mobile tap-to-show-controls: true when this touch revealed the controls bar
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');
// (mouse position is now tracked via docMouseX/docMouseY in resetControlsTimer block)
@@ -343,7 +374,7 @@ class v0ck {
if (isMobile && controlsJustShown) {
// First tap: controls were just revealed by this touch — don't toggle play
controlsJustShown = false;
- player.classList.add('v0ck_hover');
+ setHover(true);
return;
}
controlsJustShown = false;
@@ -353,7 +384,7 @@ class v0ck {
toggle.addEventListener('click', togglePlay);
overlay.addEventListener('click', e => {
e.stopPropagation();
- player.classList.add('v0ck_hover');
+ setHover(true);
togglePlay();
});
video.addEventListener('play', updatePlayIcon);
@@ -831,7 +862,7 @@ class v0ck {
}
if (isMobile && !isInsidePlayer && !isFlashYankUI) {
- player.classList.remove('v0ck_hover');
+ setHover(false);
}
});
@@ -928,7 +959,7 @@ class v0ck {
const isFullscreen = player.classList.contains('v0ck_fullscreen');
if (!video.paused || isFullscreen) {
controlsTimer = setTimeout(() => {
- player.classList.remove('v0ck_hover');
+ setHover(false);
if (settingsMenu && !settingsMenu.classList.contains('v0ck_hidden')) {
settingsMenu.classList.add('v0ck_hidden');
document.dispatchEvent(new CustomEvent('v0ck_settings_closed'));
@@ -950,7 +981,7 @@ class v0ck {
docMouseX = e.clientX;
docMouseY = e.clientY;
}
- player.classList.add('v0ck_hover');
+ setHover(true);
resetControlsTimer();
}
@@ -998,7 +1029,7 @@ class v0ck {
}
docMouseX = -1;
docMouseY = -1;
- player.classList.remove('v0ck_hover');
+ setHover(false);
clearTimeout(controlsTimer);
});
diff --git a/src/inc/lib.mjs b/src/inc/lib.mjs
index cf5b46a..a923a6a 100644
--- a/src/inc/lib.mjs
+++ b/src/inc/lib.mjs
@@ -49,7 +49,18 @@ export default new class {
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];
};
calcSpeed(b, s) {
diff --git a/src/inc/routeinc/f0cklib.mjs b/src/inc/routeinc/f0cklib.mjs
index b4601c0..81b33d3 100644
--- a/src/inc/routeinc/f0cklib.mjs
+++ b/src/inc/routeinc/f0cklib.mjs
@@ -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) || [];
if (subTags.length === 0 && (r.order_index === 0 || idx === 0)) {
subTags = tags;
@@ -1505,7 +1515,7 @@ const f0cklib = {
src: `${cfg.websrv.paths.images}/${r.dest}`,
filename: r.dest,
mime: r.mime,
- size: lib.formatSize(r.size),
+ size: lib.formatSize(subSizeNum),
width: r.width,
height: r.height,
checksum: r.checksum,
diff --git a/src/inc/routes/apiv2/index.mjs b/src/inc/routes/apiv2/index.mjs
index 8922f95..71d27bc 100644
--- a/src/inc/routes/apiv2/index.mjs
+++ b/src/inc/routes/apiv2/index.mjs
@@ -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.
// Guests may read from the cache (in-memory or DB); only authenticated users trigger real outbound fetches.
group.get(/\/meta\/fetch$/, async (req, res) => {
diff --git a/src/inc/routes/comments.mjs b/src/inc/routes/comments.mjs
index aa31e7e..d10974a 100644
--- a/src/inc/routes/comments.mjs
+++ b/src/inc/routes/comments.mjs
@@ -624,6 +624,9 @@ export default (router, tpl) => {
const itemQuery = await db`
SELECT
i.slug,
+ i.mime,
+ i.dest,
+ i.has_coverart,
i.xd_score,
COALESCE(i.visibility, 0) as visibility,
(SELECT ta.tag_id FROM tags_assign ta
@@ -684,6 +687,10 @@ export default (router, tpl) => {
db.notify('activity', JSON.stringify({
user_id: req.session.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',
body: notifyBody,
id: commentId,
@@ -1104,6 +1111,7 @@ export default (router, tpl) => {
i.id as item_id,
i.slug as item_slug,
i.dest as item_dest,
+ i.has_coverart,
(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[])
ORDER BY ta.tag_id LIMIT 1) AS rating_tag_id,
diff --git a/views/snippets/footer.html b/views/snippets/footer.html
index 10d6272..952403a 100644
--- a/views/snippets/footer.html
+++ b/views/snippets/footer.html
@@ -131,6 +131,9 @@
+
+
@elseif(item.mime.startsWith("audio"))
-