refactor: adopt tokenized color utilities

This commit is contained in:
thePR0M3TH3AN committed 2025-10-11 12:59:37 -04:00
1 parent 3f8ad80db5
commit 19d971b49d
20 files changed
+798 -640

No files matched your search

+2 -2
View File
@@ -1,3 +1,3 @@
:root{--color-page:#fff;--color-page-alt:#f9fafb;--color-surface:#0f172a;--color-surface-alt:#111827;--color-bg:var(--color-surface);--color-card:#1e293b;--color-card-private:#3b1d74;--color-card-critical:#3f0b13;--color-panel:#1f2937;--color-panel-hover:#334155;--color-border:#374151;--color-border-subtle:#d1d5db;--color-primary:#fe0032;--color-secondary:#ff93a5;--color-info:#60a5fa;--color-info-strong:#2563eb;--color-info-pressed:#1d4ed8;--color-muted-strong:#6b7280;--color-text:#f8fafc;--color-text-strong:#0f172a;--color-text-muted:var(--color-muted);--color-muted:#94a3b8;--color-muted-alt:#9ca3af;--color-subtle-text:#e2e8f0;--color-subtle:#e5e7eb;--color-critical:#7f1d1d;--color-critical-strong:#991b1b;--color-warning:#facc15;--color-warning-strong:#d97706;--color-warning-surface:rgba(146,64,14,.24);--color-neutral-700:#3f3f46;--color-neutral-600:#52525b;--color-white:#fff;--color-black:#000;--color-overlay:#1a2234;--color-overlay-black:rgba(0,0,0,.9);--color-overlay-muted:rgba(15,23,42,.55);--color-overlay-strong:rgba(15,23,42,.75);--color-overlay-darker:rgba(15,23,42,.88);--color-overlay-panel:rgba(31,41,55,.9);--color-overlay-panel-soft:rgba(31,41,55,.75);--color-overlay-panel-tint:rgba(31,41,55,.5);--color-overlay-panel-glass:rgba(30,41,59,.85);--color-overlay-veil:rgba(11,17,32,.96);--color-overlay-banner:rgba(51,65,85,.95);--color-overlay-panel-gradient-start:rgba(30,41,59,.92);--color-overlay-panel-gradient-end:rgba(15,23,42,.92);--color-surface-tint-subtle:rgba(148,163,184,.08);--color-border-translucent:rgba(148,163,184,.18);--color-border-translucent-strong:rgba(148,163,184,.25);--color-border-translucent-bright:rgba(148,163,184,.32);--color-border-translucent-stronger:rgba(148,163,184,.35);--color-border-translucent-strongest:rgba(148,163,184,.4);--color-border-translucent-glow:rgba(148,163,184,.55);--color-border-translucent-medium:rgba(148,163,184,.2);--color-border-neutral-soft:rgba(229,231,235,.35);--color-border-info-soft:rgba(96,165,250,.35);--color-border-sidebar-strong:rgba(55,65,81,.9);--color-ring-private:rgba(168,85,247,.55);--color-ring-private-hover:rgba(192,132,252,.65);--color-ring-critical:rgba(239,68,68,.4);--color-ring-critical-soft:rgba(239,68,68,.35);--color-ring-critical-hover:rgba(239,68,68,.45);--color-ring-critical-strong:rgba(239,68,68,.55);--color-ring-alert:rgba(220,38,38,.6);--color-gradient-visibility-start:rgba(129,140,248,.4);--color-gradient-visibility-end:rgba(192,132,252,.32);--color-white-soft:hsla(0,0%,100%,.1);--color-white-softer:hsla(0,0%,100%,.3);--color-white-softest:hsla(0,0%,100%,.4);--color-shadow-floating:rgba(8,11,30,.55);--color-shadow-elevated:rgba(8,11,30,.45);--color-shadow-panel:rgba(15,23,42,.35);--color-shadow-notification:rgba(15,23,42,.3);--color-shadow-sidebar:rgba(0,0,0,.45);--color-divider-strong:rgba(15,23,42,.35);--color-text-frosted-strong:rgba(226,232,240,.94);--color-text-frosted:rgba(226,232,240,.9);--color-text-frosted-soft:rgba(226,232,240,.82);--space-2xs:0.25rem;--space-xs:0.5rem;--space-sm:0.75rem;--space-md:1rem;--space-lg:1.5rem;--space-xl:2rem;--space-2xl:3rem;--radius-sm:0.25rem;--radius-md:0.5rem;--radius-lg:0.75rem;--radius-xl:1rem;--radius-full:9999px;--shadow-sm:0 0.0625rem 0.125rem 0 rgb(0 0 0/5%);--shadow-md:0 0.25rem 0.375rem -0.0625rem rgba(0,0,0,.1);--shadow-lg:0 0.625rem 0.9375rem -0.1875rem rgba(0,0,0,.1);--shadow-modal:0 1.25rem 2.5rem -0.375rem rgba(15,23,42,.55);--font-family-sans:"Inter","Segoe UI",system-ui,-apple-system,sans-serif;--font-family-mono:"Fira Code","SFMono-Regular",ui-monospace,monospace;--font-size-xs:0.75rem;--font-size-sm:0.875rem;--font-size-base:1rem;--font-size-lg:1.125rem;--font-size-xl:1.5rem;--font-size-2xl:2rem;--line-height-tight:1.1;--line-height-snug:1.25;--line-height-base:1.5;--line-height-relaxed:1.75;--motion-duration-fast:120ms;--motion-duration-base:160ms;--motion-duration-slow:280ms;--motion-ease-out:cubic-bezier(0.16,1,0.3,1);--motion-ease-in-out:cubic-bezier(0.4,0,0.2,1);--modal-motion-duration:var(--motion-duration-base)}*,:after,:before{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tLine truncated
:root{--color-page:#fff;--color-page-alt:#f9fafb;--color-surface:#0f172a;--color-surface-alt:#111827;--color-bg:var(--color-surface);--color-card:#1e293b;--color-card-private:#3b1d74;--color-card-critical:#3f0b13;--color-panel:#1f2937;--color-panel-hover:#334155;--color-border:#374151;--color-border-subtle:#d1d5db;--color-primary:#fe0032;--color-secondary:#ff93a5;--color-info:#60a5fa;--color-info-strong:#2563eb;--color-info-pressed:#1d4ed8;--color-muted-strong:#6b7280;--color-text:#f8fafc;--color-text-strong:#0f172a;--color-text-muted:var(--color-muted);--color-muted:#94a3b8;--color-muted-alt:#9ca3af;--color-subtle-text:#e2e8f0;--color-subtle:#e5e7eb;--color-critical:#7f1d1d;--color-critical-strong:#991b1b;--color-warning:#facc15;--color-warning-strong:#d97706;--color-warning-surface:rgba(146,64,14,.24);--color-neutral-700:#3f3f46;--color-neutral-600:#52525b;--color-white:#fff;--color-black:#000;--color-overlay:#1a2234;--color-overlay-black:rgba(0,0,0,.9);--color-overlay-muted:rgba(15,23,42,.55);--color-overlay-strong:rgba(15,23,42,.75);--color-overlay-darker:rgba(15,23,42,.88);--color-overlay-panel:rgba(31,41,55,.9);--color-overlay-panel-soft:rgba(31,41,55,.75);--color-overlay-panel-tint:rgba(31,41,55,.5);--color-overlay-panel-glass:rgba(30,41,59,.85);--color-overlay-veil:rgba(11,17,32,.96);--color-overlay-banner:rgba(51,65,85,.95);--color-overlay-panel-gradient-start:rgba(30,41,59,.92);--color-overlay-panel-gradient-end:rgba(15,23,42,.92);--color-surface-tint-subtle:rgba(148,163,184,.08);--color-border-translucent:rgba(148,163,184,.18);--color-border-translucent-strong:rgba(148,163,184,.25);--color-border-translucent-bright:rgba(148,163,184,.32);--color-border-translucent-stronger:rgba(148,163,184,.35);--color-border-translucent-strongest:rgba(148,163,184,.4);--color-border-translucent-glow:rgba(148,163,184,.55);--color-border-translucent-medium:rgba(148,163,184,.2);--color-border-neutral-soft:rgba(229,231,235,.35);--color-border-info-soft:rgba(96,165,250,.35);--color-border-sidebar-strong:rgba(55,65,81,.9);--color-ring-private:rgba(168,85,247,.55);--color-ring-private-hover:rgba(192,132,252,.65);--color-ring-critical:rgba(239,68,68,.4);--color-ring-critical-soft:rgba(239,68,68,.35);--color-ring-critical-hover:rgba(239,68,68,.45);--color-ring-critical-strong:rgba(239,68,68,.55);--color-ring-alert:rgba(220,38,38,.6);--color-gradient-visibility-start:rgba(129,140,248,.4);--color-gradient-visibility-end:rgba(192,132,252,.32);--color-white-soft:hsla(0,0%,100%,.1);--color-white-softer:hsla(0,0%,100%,.3);--color-white-softest:hsla(0,0%,100%,.4);--color-shadow-floating:rgba(8,11,30,.55);--color-shadow-elevated:rgba(8,11,30,.45);--color-shadow-panel:rgba(15,23,42,.35);--color-shadow-notification:rgba(15,23,42,.3);--color-shadow-sidebar:rgba(0,0,0,.45);--color-divider-strong:rgba(15,23,42,.35);--color-text-frosted-strong:rgba(226,232,240,.94);--color-text-frosted:rgba(226,232,240,.9);--color-text-frosted-soft:rgba(226,232,240,.82);--color-blog-transparent:transparent;--color-blog-cyan-500:#06b6d4;--color-blog-sky-500:#0bf;--color-blog-teal-600:#0e7490;--color-blog-green-800:#14532d;--color-blog-green-700:#15803d;--color-blog-teal-700:#164e63;--color-blog-gray-900:#171717;--color-blog-coffee-900:#1c1917;--color-blog-green-500:#22c55e;--color-blog-gray-800:#2b2b2b;--color-blog-gray-700:#3b3b3b;--color-blog-teal-accent:#3e8b8a;--color-blog-gray-650:#404040;--color-blog-brown-700:#44403c;--color-blog-purple-900:#581c87;--color-blog-gray-500:#666;--color-blog-gray-450:#737373;--color-blog-gray-440:#747474;--color-blog-brown-500:#78716c;--color-blog-rust-700:#7c2d12;--color-blog-purple-700:#7e22ce;--color-blog-purple-accent:#92379c;--color-blog-purple-muted:#96609c;--color-blog-gray-300:#999;--color-blog-purple-500:#a855f7;--color-blog-gray-275:#a9a9a9;--color-blog-red-600:#b91c1c;--color-blog-rust-600:#c2410c;--color-blog-error:#ca1717;--color-blog-gray-220:#cbcbcb;--color-blog-gray-210:#ccc;--color-blog-gray-200:#dedede;--color-blog-amber-500:#e4984d;--color-blog-gray-190:#e0e0e0;--color-blog-orange-600:#e35428;--color-blog-gray-180:#e9e9e9;--color-blog-gray-170:#ebebeb;--color-blog-red-500:#ef4444;--color-blog-amber-400:#f6ad55;--color-blog-neutral-100:#f5f5f5;--color-blog-neutral-120:#f6f6f6;--color-blog-orange-500:#f97316;--color-blog-neutral-50:#fdfdfd;--space-2xs:0.25rem;--space-xs:0.5rem;--space-sm:0.75rem;--space-md:1rem;--space-lg:1.5rem;--space-xl:2rem;--space-2xl:3rem;--radius-sm:0.25rem;--radius-md:0.5rem;--radius-lg:0.75rem;--radius-xl:1rem;--radius-full:9999px;--shadow-sm:0 0.0625rem 0.125rem 0 rgb(0 0 0/5%);--shadow-md:0 0.25rem 0.375rem -0.0625rem rgba(0,0,0,.1);--shadow-lg:0 0.625rem 0.9375rem -0.1875rem rgba(0,0,0,.1);--shadow-modal:0 1.25rem 2.5rem -0.375rem rgba(15,23,42,.55);--font-family-sans:"Inter","Segoe UI",system-ui,-apple-system,sans-serif;--font-family-mono:"Fira Code","SFMono-Regular",ui-monospace,monospace;--font-size-xs:0.75rem;--font-size-sm:0.875rem;--font-size-base:1rem;--font-size-lLine truncated
/*! tailwindcss v3.4.17 | MIT License | https://tailwindcss.com*/*,:after,:before{box-sizing:border-box;border:0 solid #e5e7eb}:after,:before{--tw-content:""}:host,html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:var(--font-family-sans);font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:var(--font-family-mono);font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}.\!container{width:100%!important}.container{width:100%}@media (min-width:640px){.\!container{max-width:640px!important}.container{max-width:640px}}@media (min-width:768px){.\!container{max-width:768px!important}.container{max-width:768px}}@media (min-width:1024px){.\!container{max-width:1024px!important}.container{max-width:1024px}}@media (min-width:1280px){.\!container{max-width:1280px!important}.container{max-width:1280px}}@media (min-width:1536px){.\!container{max-width:1536px!important}.container{max-width:1536px}}.focus-ring{outline:2px solid transparent;outline-offset:2px}.focus-ring:focus-visible{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000);--tw-ring-color:var(--color-info);--tw-ring-opacity:0.7;--tw-ring-offset-width:2px;--tw-ring-offset-color:var(--color-surface);transition-property:box-shadow;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s;transition-duration:var(--motion-duration-fast);transition-timing-function:cubic-bezier(0,0,.2,1)}.btn{outline:2px solid transparent;outline-offset:2px}.btn:focus-visible{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000);--tw-ring-color:var(--color-info);--tw-ring-opacity:0.7;--tw-ring-offset-width:2px;--tw-ring-offset-color:var(--color-surface);transition-property:box-shadow;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s;transition-duration:var(--motion-duration-fast);transition-timing-function:cubic-bezier(0,0,.2,1)}.btn{display:inline-flex;align-items:center;justify-content:center;gap:.5rem;border-radius:var(--radius-lg);border-width:1px;border-color:transparent;background-color:var(--color-primary);padding:.5rem 1rem;font-size:var(--font-size-sm);line-height:var(--line-height-snug);font-weight:600;text-transform:uppercase;letter-spacing:.025em;color:var(--color-white);--tw-shadow:var(--shadow-md);--tw-shadow-colored:var(--shadow-md);transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-duration:.15s;transition-duration:var(--motion-duration-base);transition-timing-funLine truncated
/*! tailwindcss v3.4.17 | MIT License | https://tailwindcss.com*/*,:after,:before{box-sizing:border-box;border:0 solid #e5e7eb}:after,:before{--tw-content:""}:host,html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:var(--font-family-sans);font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:var(--font-family-mono);font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}.\!container{width:100%!important}.container{width:100%}@media (min-width:640px){.\!container{max-width:640px!important}.container{max-width:640px}}@media (min-width:768px){.\!container{max-width:768px!important}.container{max-width:768px}}@media (min-width:1024px){.\!container{max-width:1024px!important}.container{max-width:1024px}}@media (min-width:1280px){.\!container{max-width:1280px!important}.container{max-width:1280px}}@media (min-width:1536px){.\!container{max-width:1536px!important}.container{max-width:1536px}}.focus-ring{outline:2px solid transparent;outline-offset:2px}.focus-ring:focus-visible{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000);--tw-ring-color:var(--color-info);--tw-ring-opacity:0.7;--tw-ring-offset-width:2px;--tw-ring-offset-color:var(--color-surface);transition-property:box-shadow;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s;transition-duration:var(--motion-duration-fast);transition-timing-function:cubic-bezier(0,0,.2,1)}.popover{position:relative;display:inline-flex}.popover__panel{outline:2px solid transparent;outline-offset:2px}.popover__panel:focus-visible{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000);--tw-ring-color:var(--color-info);--tw-ring-opacity:0.7;--tw-ring-offset-width:2px;--tw-ring-offset-color:var(--color-surface);transition-property:box-shadow;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s;transition-duration:var(--motion-duration-fast);transition-timing-function:cubic-bezier(0,0,.2,1)}.popover__panel{pointer-events:none;position:absolute;left:0;top:100%;z-index:70;margin-top:.5rem;min-width:12rem;transform-origin:top;--tw-scale-x:.95;--tw-scale-y:.95;border-radius:var(--radius-lg);border-width:1px;border-color:var(--color-border);--tw-border-opacity:0.4;background-color:var(--color-panel);padding:.75rem 1rem;font-size:var(--font-size-sm);line-height:var(--line-height-snug);color:var(--color-text);opacity:0;--tw-shadow:var(--shadow-lg);--tw-shadow-colored:var(--shadow-lg);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)Line truncated
+25 -23
View File
@@ -1,4 +1,4 @@
<!DOCTYPE html>
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
@@ -81,11 +81,11 @@
<div
id="app"
class="px-4 py-8 min-h-screen flex flex-col fade-in"
class="fade-in flex min-h-screen flex-col px-4 py-8"
data-sidebar-state="collapsed"
>
<!-- Header -->
<header class="mb-8 flex items-center w-full">
<header class="mb-8 flex w-full items-center">
<!-- Mobile hamburger button (hidden on md+) -->
<!-- New button -->
<!--
@@ -95,7 +95,7 @@
-->
<button
id="mobileMenuBtn"
class="md:hidden ml-2 mr-4 flex items-center justify-center w-10 h-10 rounded-full bg-transparent hover:bg-transparent focus-ring z-[60] fade-in"
class="focus-ring fade-in z-[60] ml-2 mr-4 flex h-10 w-10 items-center justify-center rounded-full bg-transparent hover:bg-transparent md:hidden"
type="button"
aria-expanded="false"
aria-controls="sidebar"
@@ -104,7 +104,7 @@
<img
src="assets/svg/mobile-sidebar-menu-icon.svg"
alt="Mobile Sidebar Menu Icon"
class="w-6 h-6"
class="h-6 w-6"
/>
</button>
<!-- Logo -->
@@ -125,7 +125,9 @@
<!-- Header action buttons share the fade-in treatment for consistency. -->
<div class="ml-auto flex items-center space-x-4 fade-in fade-in-delay-200">
<div
class="fade-in fade-in-delay-200 ml-auto flex items-center space-x-4"
>
<!-- Login Button -->
<button
id="loginButton"
@@ -134,14 +136,14 @@
<img
src="assets/svg/default-profile.svg"
alt="Profile Icon"
class="w-9 h-9"
class="h-9 w-9"
/>
</button>
<!-- Upload Button (hidden by default) -->
<button
id="uploadButton"
class="hidden btn focus-ring h-12 w-12 rounded-full p-0 text-xl font-bold leading-none"
class="btn focus-ring hidden h-12 w-12 rounded-full p-0 text-xl font-bold leading-none"
>
+
</button>
@@ -149,14 +151,14 @@
<!-- Profile Button (hidden by default) -->
<button
id="profileButton"
class="hidden btn-ghost focus-ring h-12 w-12 rounded-full p-0"
class="btn-ghost focus-ring hidden h-12 w-12 rounded-full p-0"
>
<div class="w-10 h-10 rounded-full overflow-hidden">
<div class="h-10 w-10 overflow-hidden rounded-full">
<img
id="profileAvatar"
src="assets/svg/default-profile.svg"
alt="Profile"
class="w-full h-full object-cover"
class="h-full w-full object-cover"
/>
</div>
</button>
@@ -173,13 +175,13 @@
<!-- Error Container -->
<div
id="errorContainer"
class="hidden bg-red-100 text-red-900 p-4 rounded-md mb-4"
class="bg-critical/10 mb-4 hidden rounded-md p-4 text-critical-strong"
></div>
<!-- Status Container -->
<div
id="statusContainer"
class="hidden status-banner"
class="status-banner hidden"
role="status"
aria-live="polite"
>
@@ -190,12 +192,12 @@
<!-- Success Container -->
<div
id="successContainer"
class="hidden bg-green-100 text-green-900 p-4 rounded-md mb-4"
class="bg-info/10 mb-4 hidden rounded-md p-4 text-info-strong"
></div>
</div>
<!-- Dynamic views (like most-recent-videos.html, etc.) -->
<main id="viewContainer" class="flex-grow mb-8"></main>
<main id="viewContainer" class="mb-8 flex-grow"></main>
<!-- Modal Container (external modals) -->
<div id="modalContainer"></div>
@@ -204,8 +206,8 @@
<!-- Tagline fade matches the cards. -->
<div class="text-center mb-8 fade-in fade-in-delay-300">
<h2 class="text-2xl font-bold text-gray-500 tracking-wide">
<div class="fade-in fade-in-delay-300 mb-8 text-center">
<h2 class="text-2xl font-bold tracking-wide text-muted-strong">
seed. zap. subscribe.
</h2>
</div>
@@ -213,12 +215,12 @@
<!-- Additional Scripts -->
<!-- Bootstraps nostr-tools before other modules rely on nostrToolsReady -->
<script type="module" src="js/nostrToolsBootstrap.js?v=2024.10.29"></script>
<script type="module" src="js/config.js?v=2024.10.29"></script>
<script type="module" src="js/lists.js?v=2024.10.29"></script>
<script type="module" src="js/accessControl.js?v=2024.10.29"></script>
<script type="module" src="js/webtorrent.js?v=2024.10.29"></script>
<script type="module" src="js/nostr.js?v=2024.10.29"></script>
<script type="module" src="js/nostrToolsBootstrap.js?v=2024.10.29"></script>
<script type="module" src="js/config.js?v=2024.10.29"></script>
<script type="module" src="js/lists.js?v=2024.10.29"></script>
<script type="module" src="js/accessControl.js?v=2024.10.29"></script>
<script type="module" src="js/webtorrent.js?v=2024.10.29"></script>
<script type="module" src="js/nostr.js?v=2024.10.29"></script>
<script type="module" src="js/viewManager.js?v=2024.10.29"></script>
<script type="module" src="js/bootstrap.js?v=2024.10.29"></script>
<script type="module" src="js/disclaimer.js?v=2024.10.29"></script>
+181 -124
View File
@@ -3,7 +3,7 @@
import {
nostrClient,
DEFAULT_RELAY_URLS,
convertEventToVideo as sharedConvertEventToVideo,
convertEventToVideo as sharedConvertEventToVideo
} from "./nostr.js";
import { subscriptions } from "./subscriptions.js"; // <-- NEW import
import { attachHealthBadges } from "./gridHealth.js";
@@ -25,16 +25,19 @@ import {
normalizeLightningAddressKey,
rememberLightningMetadata,
setCachedPlatformLightningAddress,
validateInvoiceAmount,
validateInvoiceAmount
} from "./payments/zapSharedState.js";
import { splitAndZap } from "./payments/zapSplit.js";
import {
resolveLightningAddress,
fetchPayServiceData,
requestInvoice,
requestInvoice
} from "./payments/lnurl.js";
import { getPlatformLightningAddress } from "./payments/platformAddress.js";
import { ensureWallet, sendPayment as sendWalletPayment } from "./payments/nwcClient.js";
import {
ensureWallet,
sendPayment as sendWalletPayment
} from "./payments/nwcClient.js";
const getApp = () => getApplication();
@@ -57,8 +60,8 @@ const summarizeZapTracker = (tracker) =>
typeof entry?.error?.message === "string"
? entry.error.message
: entry?.error
? String(entry.error)
: undefined,
? String(entry.error)
: undefined
}))
: undefined;
@@ -84,21 +87,21 @@ function logZapError(stage, details = {}, error) {
details.walletSettings.type ||
details.walletSettings.name ||
details.walletSettings.client ||
undefined,
undefined
}
: undefined,
shares: details?.context?.shares
? {
total: details.context.shares.total,
creator: details.context.shares.creatorShare,
platform: details.context.shares.platformShare,
platform: details.context.shares.platformShare
}
: undefined,
tracker: summarizeZapTracker(details?.tracker),
retryAttempt:
Number.isInteger(details?.retryAttempt) && details.retryAttempt >= 0
? details.retryAttempt
: undefined,
: undefined
};
console.error("[zap] Channel zap failure", summary, error);
}
@@ -208,7 +211,10 @@ function setChannelZapVisibility(visible) {
}
function getChannelShareButton() {
if (cachedChannelShareButton && !document.body.contains(cachedChannelShareButton)) {
if (
cachedChannelShareButton &&
!document.body.contains(cachedChannelShareButton)
) {
cachedChannelShareButton = null;
}
if (!cachedChannelShareButton) {
@@ -467,30 +473,30 @@ function setZapStatus(message, tone = "neutral") {
const normalizedTone = typeof tone === "string" ? tone : "neutral";
statusEl.textContent = message || "";
statusEl.classList.remove(
"text-gray-300",
"text-gray-400",
"text-green-300",
"text-red-300",
"text-yellow-300"
"text-text",
"text-muted",
"text-info",
"text-critical",
"text-warning-strong"
);
if (!message) {
statusEl.classList.add("text-gray-400");
statusEl.classList.add("text-muted");
return;
}
switch (normalizedTone) {
case "success":
statusEl.classList.add("text-green-300");
statusEl.classList.add("text-info");
break;
case "error":
statusEl.classList.add("text-red-300");
statusEl.classList.add("text-critical");
break;
case "warning":
statusEl.classList.add("text-yellow-300");
statusEl.classList.add("text-warning-strong");
break;
default:
statusEl.classList.add("text-gray-300");
statusEl.classList.add("text-text");
break;
}
}
@@ -514,7 +520,8 @@ function renderZapReceipts(receipts, { partial = false } = {}) {
if (!Array.isArray(receipts) || receipts.length === 0) {
if (partial) {
const emptyItem = document.createElement("li");
emptyItem.className = "rounded border border-gray-700 bg-gray-800/70 p-3 text-gray-300";
emptyItem.className =
"rounded border border-border bg-panel/70 p-3 text-text";
emptyItem.textContent = "No receipts were returned for this attempt.";
list.appendChild(emptyItem);
}
@@ -527,10 +534,11 @@ function renderZapReceipts(receipts, { partial = false } = {}) {
}
const li = document.createElement("li");
li.className = "rounded border border-gray-700 bg-gray-800/70 p-3";
li.className = "rounded border border-border bg-panel/70 p-3";
const header = document.createElement("div");
header.className = "flex items-center justify-between text-xs font-semibold uppercase tracking-wide text-gray-200";
header.className =
"flex items-center justify-between text-xs font-semibold uppercase tracking-wide text-text";
const shareType = receipt.recipientType || receipt.type || "creator";
const shareLabel = document.createElement("span");
@@ -544,14 +552,14 @@ function renderZapReceipts(receipts, { partial = false } = {}) {
? receipt.status === "success"
: !receipt.error;
status.textContent = isSuccess ? "Success" : "Failed";
status.className = isSuccess ? "text-green-300" : "text-red-300";
status.className = isSuccess ? "text-info" : "text-critical";
header.appendChild(shareLabel);
header.appendChild(status);
li.appendChild(header);
const address = document.createElement("p");
address.className = "mt-1 text-xs text-gray-300 break-all";
address.className = "mt-1 text-xs text-text break-all";
const addressValue = receipt.address || "";
if (addressValue) {
address.textContent = addressValue;
@@ -559,7 +567,7 @@ function renderZapReceipts(receipts, { partial = false } = {}) {
}
const detail = document.createElement("p");
detail.className = "mt-2 text-xs text-gray-400";
detail.className = "mt-2 text-xs text-muted";
if (isSuccess) {
let detailMessage = "Invoice settled.";
const preimage = receipt.payment?.result?.preimage;
@@ -636,7 +644,7 @@ function getZapVideoEvent() {
tags: Array.isArray(baseEvent.tags) ? [...baseEvent.tags] : [],
content: typeof baseEvent.content === "string" ? baseEvent.content : "",
created_at: baseEvent.created_at || Math.floor(Date.now() / 1000),
lightningAddress,
lightningAddress
};
}
@@ -650,7 +658,9 @@ async function prepareLightningContext({ amount, overrideFee = null }) {
throw new Error("Enter a zap amount greater than zero.");
}
const creatorEntry = await fetchLightningMetadata(currentChannelLightningAddress);
const creatorEntry = await fetchLightningMetadata(
currentChannelLightningAddress
);
if (shares.creatorShare > 0) {
try {
validateInvoiceAmount(creatorEntry.metadata, shares.creatorShare);
@@ -665,7 +675,9 @@ async function prepareLightningContext({ amount, overrideFee = null }) {
if (shares.platformShare > 0) {
platformAddress = getCachedPlatformLightningAddress();
if (!platformAddress) {
platformAddress = await getPlatformLightningAddress({ forceRefresh: false });
platformAddress = await getPlatformLightningAddress({
forceRefresh: false
});
setCachedPlatformLightningAddress(platformAddress || "");
}
if (!platformAddress) {
@@ -687,7 +699,7 @@ async function prepareLightningContext({ amount, overrideFee = null }) {
shares,
creatorEntry,
platformEntry,
platformAddress,
platformAddress
};
}
@@ -696,7 +708,7 @@ function createZapDependencies({
platformEntry,
shares,
shareTracker,
walletSettings,
walletSettings
}) {
const creatorKey = normalizeLightningAddressKey(
creatorEntry?.address || currentChannelLightningAddress
@@ -731,7 +743,7 @@ function createZapDependencies({
key: normalized,
address: resolved.address || value,
resolved,
fetchedAt: Date.now(),
fetchedAt: Date.now()
});
}
return resolved;
@@ -747,13 +759,13 @@ function createZapDependencies({
rememberLightningMetadata({
...cached,
metadata,
fetchedAt: Date.now(),
fetchedAt: Date.now()
});
}
return metadata;
},
validateInvoiceAmount,
requestInvoice,
requestInvoice
},
wallet: {
ensureWallet: async (options) => {
@@ -765,7 +777,7 @@ function createZapDependencies({
{
tracker: shareTracker,
context: { shares },
walletSettings: options?.settings,
walletSettings: options?.settings
},
error
);
@@ -775,17 +787,18 @@ function createZapDependencies({
sendPayment: async (bolt11, params) => {
const shareType = activeShare || "unknown";
const shareAmount =
shareType === "platform"
? shares.platformShare
: shares.creatorShare;
shareType === "platform" ? shares.platformShare : shares.creatorShare;
const address =
shareType === "platform"
? platformEntry?.address || getCachedPlatformLightningAddress()
: creatorEntry?.address || currentChannelLightningAddress;
const normalizedParams = {
...(params || {}),
...(params || {})
};
if (!Number.isFinite(normalizedParams.amountSats) && Number.isFinite(shareAmount)) {
if (
!Number.isFinite(normalizedParams.amountSats) &&
Number.isFinite(shareAmount)
) {
normalizedParams.amountSats = shareAmount;
}
if (!normalizedParams.settings && walletSettings) {
@@ -799,7 +812,7 @@ function createZapDependencies({
status: "success",
amount: shareAmount,
address,
payment,
payment
});
}
return payment;
@@ -810,7 +823,7 @@ function createZapDependencies({
status: "error",
amount: shareAmount,
address,
error,
error
});
}
logZapError(
@@ -820,7 +833,7 @@ function createZapDependencies({
amount: shareAmount,
address,
tracker: shareTracker,
context: { shares },
context: { shares }
},
error
);
@@ -828,7 +841,7 @@ function createZapDependencies({
} finally {
activeShare = null;
}
},
}
},
platformAddress: {
getPlatformLightningAddress: async () => {
@@ -839,11 +852,13 @@ function createZapDependencies({
if (cachedAddress) {
return cachedAddress;
}
const fallback = await getPlatformLightningAddress({ forceRefresh: false });
const fallback = await getPlatformLightningAddress({
forceRefresh: false
});
setCachedPlatformLightningAddress(fallback || "");
return fallback;
},
},
}
}
};
}
@@ -864,7 +879,7 @@ function getWalletSettingsOrPrompt() {
}
return {
...settings,
nwcUri: normalizedUri,
nwcUri: normalizedUri
};
}
@@ -883,7 +898,7 @@ async function runZapAttempt({ amount, overrideFee = null, walletSettings }) {
{
amount,
overrideFee,
walletSettings: settings,
walletSettings: settings
},
error
);
@@ -893,13 +908,17 @@ async function runZapAttempt({ amount, overrideFee = null, walletSettings }) {
const dependencies = createZapDependencies({
...context,
shareTracker,
walletSettings: settings,
walletSettings: settings
});
const videoEvent = getZapVideoEvent();
let previousOverride;
const hasGlobal = typeof globalThis !== "undefined";
if (hasGlobal && typeof overrideFee === "number" && Number.isFinite(overrideFee)) {
if (
hasGlobal &&
typeof overrideFee === "number" &&
Number.isFinite(overrideFee)
) {
previousOverride = globalThis.__BITVID_PLATFORM_FEE_OVERRIDE__;
globalThis.__BITVID_PLATFORM_FEE_OVERRIDE__ = overrideFee;
}
@@ -909,7 +928,7 @@ async function runZapAttempt({ amount, overrideFee = null, walletSettings }) {
{
videoEvent,
amountSats: context.shares.total,
walletSettings: settings,
walletSettings: settings
},
dependencies
);
@@ -925,16 +944,23 @@ async function runZapAttempt({ amount, overrideFee = null, walletSettings }) {
overrideFee,
walletSettings: settings,
context,
tracker: shareTracker,
tracker: shareTracker
},
error
);
throw error;
} finally {
if (hasGlobal && typeof overrideFee === "number" && Number.isFinite(overrideFee)) {
if (
hasGlobal &&
typeof overrideFee === "number" &&
Number.isFinite(overrideFee)
) {
if (typeof previousOverride === "number") {
globalThis.__BITVID_PLATFORM_FEE_OVERRIDE__ = previousOverride;
} else if (globalThis && "__BITVID_PLATFORM_FEE_OVERRIDE__" in globalThis) {
} else if (
globalThis &&
"__BITVID_PLATFORM_FEE_OVERRIDE__" in globalThis
) {
delete globalThis.__BITVID_PLATFORM_FEE_OVERRIDE__;
}
}
@@ -970,7 +996,7 @@ async function executePendingRetry({ walletSettings }) {
const attempt = await runZapAttempt({
amount: share.amount,
overrideFee,
walletSettings,
walletSettings
});
if (!attempt) {
setZapStatus("", "neutral");
@@ -992,7 +1018,7 @@ async function executePendingRetry({ walletSettings }) {
amount: share?.amount,
address: share?.address,
walletSettings,
tracker,
tracker
},
error
);
@@ -1006,9 +1032,12 @@ async function executePendingRetry({ walletSettings }) {
const message = error?.message || "Retry failed.";
setZapStatus(message, "error");
app?.showError?.(message);
renderZapReceipts(aggregatedTracker.length ? aggregatedTracker : tracker, {
partial: true,
});
renderZapReceipts(
aggregatedTracker.length ? aggregatedTracker : tracker,
{
partial: true
}
);
return;
}
}
@@ -1137,9 +1166,10 @@ async function handleZapSend(event) {
walletSettings,
tracker,
retryAttempt:
Array.isArray(pendingZapRetry?.shares) && pendingZapRetry.shares.length
Array.isArray(pendingZapRetry?.shares) &&
pendingZapRetry.shares.length
? pendingZapRetry.shares.length
: undefined,
: undefined
},
error
);
@@ -1431,7 +1461,13 @@ function setupZapButton() {
const amountInput = getZapAmountInput();
const controls = getZapControlsContainer();
const zapForm = getZapFormElement();
if (!zapButton || !amountInput || !controls || !zapForm || !getZapSendButton()) {
if (
!zapButton ||
!amountInput ||
!controls ||
!zapForm ||
!getZapSendButton()
) {
return;
}
@@ -1460,7 +1496,10 @@ function setupZapButton() {
typeof app?.getActiveNwcSettings === "function"
? app.getActiveNwcSettings()
: {};
if (Number.isFinite(activeSettings?.defaultZap) && activeSettings.defaultZap > 0) {
if (
Number.isFinite(activeSettings?.defaultZap) &&
activeSettings.defaultZap > 0
) {
amountInput.value = Math.max(0, Math.round(activeSettings.defaultZap));
}
@@ -1542,7 +1581,9 @@ function normalizeChannelProfileMetadata(raw = {}) {
trimProfileString(raw.name) ||
"Unknown User";
const picture =
trimProfileString(raw.picture) || trimProfileString(raw.image) || FALLBACK_CHANNEL_AVATAR;
trimProfileString(raw.picture) ||
trimProfileString(raw.image) ||
FALLBACK_CHANNEL_AVATAR;
const about = trimProfileString(raw.about || raw.bio);
const website = trimProfileString(raw.website || raw.url);
const banner =
@@ -1560,7 +1601,7 @@ function normalizeChannelProfileMetadata(raw = {}) {
banner,
lud16,
lud06,
lightningAddress,
lightningAddress
};
}
@@ -1572,7 +1613,7 @@ function rememberChannelProfile(pubkey, { profile = {}, event = null } = {}) {
channelProfileMetadataCache.set(pubkey, {
timestamp: Date.now(),
profile: normalizeChannelProfileMetadata(profile),
event: event ? { ...event } : null,
event: event ? { ...event } : null
});
}
@@ -1592,7 +1633,7 @@ function getCachedChannelProfile(pubkey) {
return {
profile: entry.profile,
event: entry.event ? { ...entry.event } : null,
event: entry.event ? { ...entry.event } : null
};
}
@@ -1623,11 +1664,15 @@ function isValidRelayUrl(url) {
}
function getCachedChannelVideoEvents(pubkey) {
if (!nostrClient?.allEvents || typeof nostrClient.allEvents.forEach !== "function") {
if (
!nostrClient?.allEvents ||
typeof nostrClient.allEvents.forEach !== "function"
) {
return [];
}
const normalized = typeof pubkey === "string" ? pubkey.trim().toLowerCase() : "";
const normalized =
typeof pubkey === "string" ? pubkey.trim().toLowerCase() : "";
if (!normalized) {
return [];
}
@@ -1670,7 +1715,8 @@ function buildRenderableChannelVideos({ events = [], app } = {}) {
const uniqueVideos = Array.from(dedupedById.values());
const newestByRoot =
app?.dedupeVideosByRoot?.(uniqueVideos) ?? dedupeToNewestByRoot(uniqueVideos);
app?.dedupeVideosByRoot?.(uniqueVideos) ??
dedupeToNewestByRoot(uniqueVideos);
let videos = newestByRoot.filter((video) => !video.deleted);
videos = videos.filter((video) => {
@@ -1698,7 +1744,7 @@ function renderChannelVideosFromList({
container,
app,
loadToken,
allowEmptyMessage = true,
allowEmptyMessage = true
} = {}) {
if (!container || loadToken !== currentVideoLoadToken) {
return false;
@@ -1707,7 +1753,7 @@ function renderChannelVideosFromList({
if (!Array.isArray(videos) || videos.length === 0) {
if (allowEmptyMessage) {
container.dataset.hasChannelVideos = "false";
container.innerHTML = `<p class="text-gray-500">No videos to display.</p>`;
container.innerHTML = `<p class="text-muted-strong">No videos to display.</p>`;
return true;
}
return false;
@@ -1726,12 +1772,11 @@ function renderChannelVideosFromList({
: "This magnet link is missing a compatible BitTorrent v1 info hash.";
const fallbackNormalizePubkey = (value) =>
typeof value === "string" && value
? value.trim().toLowerCase()
: "";
typeof value === "string" && value ? value.trim().toLowerCase() : "";
const normalizePubkey =
typeof app?.normalizeHexPubkey === "function"
? (value) => app.normalizeHexPubkey(value) || fallbackNormalizePubkey(value)
? (value) =>
app.normalizeHexPubkey(value) || fallbackNormalizePubkey(value)
: fallbackNormalizePubkey;
const normalizedViewerPubkey = normalizePubkey(app?.pubkey);
let renderIndex = 0;
@@ -1745,16 +1790,21 @@ function renderChannelVideosFromList({
}
const element = trigger instanceof HTMLElement ? trigger : null;
const target = element?.closest("[data-play-url],[data-play-magnet]") || element;
const target =
element?.closest("[data-play-url],[data-play-magnet]") || element;
const rawUrlValue =
(target?.dataset && typeof target.dataset.playUrl === "string"
? target.dataset.playUrl
: null) ?? target?.getAttribute?.("data-play-url") ?? "";
: null) ??
target?.getAttribute?.("data-play-url") ??
"";
const rawMagnetValue =
(target?.dataset && typeof target.dataset.playMagnet === "string"
? target.dataset.playMagnet
: null) ?? target?.getAttribute?.("data-play-magnet") ?? "";
: null) ??
target?.getAttribute?.("data-play-magnet") ??
"";
let url = "";
if (rawUrlValue) {
@@ -1792,14 +1842,14 @@ function renderChannelVideosFromList({
url: detail.url,
magnet: detail.magnet,
title: detail.video?.title,
description: detail.video?.description,
description: detail.video?.description
})
).catch((error) => {
console.error("Failed to play channel video via event id:", error);
if (typeof app?.playVideoWithFallback === "function") {
app.playVideoWithFallback({
url: detail.url,
magnet: detail.magnet,
magnet: detail.magnet
});
}
});
@@ -1810,7 +1860,10 @@ function renderChannelVideosFromList({
Promise.resolve(
app.playVideoWithFallback({ url: detail.url, magnet: detail.magnet })
).catch((error) => {
console.error("Failed to start fallback playback for channel video:", error);
console.error(
"Failed to start fallback playback for channel video:",
error
);
});
}
};
@@ -1848,7 +1901,10 @@ function renderChannelVideosFromList({
typeof app?.deriveVideoPointerInfo === "function"
? app.deriveVideoPointerInfo(video)
: null;
if (pointerInfo && typeof app?.persistWatchHistoryMetadataForVideo === "function") {
if (
pointerInfo &&
typeof app?.persistWatchHistoryMetadataForVideo === "function"
) {
app.persistWatchHistoryMetadataForVideo(video, pointerInfo);
}
@@ -1885,22 +1941,22 @@ function renderChannelVideosFromList({
canManageBlacklist:
typeof app?.canCurrentUserManageBlacklist === "function"
? app.canCurrentUserManageBlacklist()
: false,
: false
},
nsfwContext: {
isNsfw: video?.isNsfw === true,
allowNsfw,
viewerIsOwner: canEdit,
viewerIsOwner: canEdit
},
helpers: {
escapeHtml: (value) => escapeHTML(value),
isMagnetSupported: (magnet) => app?.isMagnetUriSupported?.(magnet),
toLocaleString: (value) =>
typeof value === "number" ? value.toLocaleString() : value,
typeof value === "number" ? value.toLocaleString() : value
},
assets: {
fallbackThumbnailSrc: "assets/jpg/video-thumbnail-fallback.jpg",
unsupportedBtihMessage,
unsupportedBtihMessage
},
state: { loadedThumbnails },
ensureGlobalMoreMenuHandlers: () => app?.ensureGlobalMoreMenuHandlers?.(),
@@ -1909,8 +1965,8 @@ function renderChannelVideosFromList({
formatTimeAgo: (ts) =>
typeof app?.formatTimeAgo === "function"
? app.formatTimeAgo(ts)
: new Date(ts * 1000).toLocaleString(),
},
: new Date(ts * 1000).toLocaleString()
}
});
videoCard.onPlay = ({ event: domEvent, video: cardVideo }) => {
@@ -1923,21 +1979,21 @@ function renderChannelVideosFromList({
videoCard.onEdit = ({ video: editVideo, index: editIndex }) => {
app?.handleEditVideo?.({
eventId: editVideo?.id || "",
index: Number.isFinite(editIndex) ? editIndex : null,
index: Number.isFinite(editIndex) ? editIndex : null
});
};
videoCard.onRevert = ({ video: revertVideo, index: revertIndex }) => {
app?.handleRevertVideo?.({
eventId: revertVideo?.id || "",
index: Number.isFinite(revertIndex) ? revertIndex : null,
index: Number.isFinite(revertIndex) ? revertIndex : null
});
};
videoCard.onDelete = ({ video: deleteVideo, index: deleteIndex }) => {
app?.handleFullDeleteVideo?.({
eventId: deleteVideo?.id || "",
index: Number.isFinite(deleteIndex) ? deleteIndex : null,
index: Number.isFinite(deleteIndex) ? deleteIndex : null
});
};
@@ -1946,7 +2002,7 @@ function renderChannelVideosFromList({
const detail = {
...dataset,
eventId: dataset.eventId || video.id || "",
context: dataset.context || "channel-grid",
context: dataset.context || "channel-grid"
};
app?.handleMoreMenuAction?.(action || "copy-link", detail);
};
@@ -1967,7 +2023,7 @@ function renderChannelVideosFromList({
if (renderIndex === 0) {
if (allowEmptyMessage) {
container.dataset.hasChannelVideos = "false";
container.innerHTML = `<p class="text-gray-500">No videos to display.</p>`;
container.innerHTML = `<p class="text-muted-strong">No videos to display.</p>`;
return true;
}
return false;
@@ -1996,7 +2052,7 @@ function applyChannelProfileMetadata({
event = null,
pubkey = "",
npub = "",
loadToken = null,
loadToken = null
} = {}) {
if (loadToken !== null && loadToken !== currentProfileLoadToken) {
return;
@@ -2054,8 +2110,7 @@ function applyChannelProfileMetadata({
const lnEl = document.getElementById("channelLightning");
if (lnEl) {
lnEl.textContent =
lightningAddress || "No lightning address found.";
lnEl.textContent = lightningAddress || "No lightning address found.";
}
if (event) {
@@ -2084,7 +2139,7 @@ async function fetchChannelProfileFromRelays(pubkey) {
try {
const events = await nostrClient.pool.list(nostrClient.relays, [
{ kinds: [0], authors: [pubkey], limit: 1 },
{ kinds: [0], authors: [pubkey], limit: 1 }
]);
let newestEvent = null;
@@ -2129,7 +2184,7 @@ async function loadUserProfile(pubkey) {
event: cachedEntry.event,
pubkey,
npub: currentChannelNpub,
loadToken,
loadToken
});
} else {
const stateEntry =
@@ -2142,7 +2197,7 @@ async function loadUserProfile(pubkey) {
event: null,
pubkey,
npub: currentChannelNpub,
loadToken,
loadToken
});
} else {
applyChannelProfileMetadata({
@@ -2150,7 +2205,7 @@ async function loadUserProfile(pubkey) {
event: null,
pubkey,
npub: currentChannelNpub,
loadToken,
loadToken
});
}
}
@@ -2171,7 +2226,7 @@ async function loadUserProfile(pubkey) {
result.profile?.lightningAddress ||
result.profile?.lud16 ||
result.profile?.lud06 ||
"",
""
});
} catch (error) {
console.warn(
@@ -2186,7 +2241,7 @@ async function loadUserProfile(pubkey) {
event: result.event,
pubkey,
npub: currentChannelNpub,
loadToken,
loadToken
});
} catch (error) {
if (loadToken === currentProfileLoadToken) {
@@ -2219,7 +2274,7 @@ async function loadUserVideos(pubkey) {
if (!hadExistingContent) {
container.innerHTML = `
<div class="py-16 flex justify-center">
<span class="text-gray-400 animate-pulse">Loading videos…</span>
<span class="text-muted animate-pulse">Loading videos…</span>
</div>
`;
}
@@ -2231,7 +2286,7 @@ async function loadUserVideos(pubkey) {
if (cachedEvents.length) {
const cachedVideos = buildRenderableChannelVideos({
events: cachedEvents,
app,
app
});
if (cachedVideos.length) {
renderedFromCache =
@@ -2240,20 +2295,18 @@ async function loadUserVideos(pubkey) {
container,
app,
loadToken,
allowEmptyMessage: false,
allowEmptyMessage: false
}) || renderedFromCache;
}
}
}
const ensureAccessPromise = accessControl
.ensureReady()
.catch((error) => {
console.warn(
"Failed to ensure admin lists were loaded before channel fetch:",
error
);
});
const ensureAccessPromise = accessControl.ensureReady().catch((error) => {
console.warn(
"Failed to ensure admin lists were loaded before channel fetch:",
error
);
});
try {
// 1) Build filter for videos from this pubkey
@@ -2261,7 +2314,7 @@ async function loadUserVideos(pubkey) {
kinds: [30078],
authors: [pubkey],
"#t": ["video"],
limit: 200,
limit: 200
};
// 2) Collect raw events from all relays
@@ -2293,9 +2346,7 @@ async function loadUserVideos(pubkey) {
const relayUrl = relayList[index];
if (result.status === "fulfilled") {
const relayEvents = Array.isArray(result.value)
? result.value
: [];
const relayEvents = Array.isArray(result.value) ? result.value : [];
events.push(...relayEvents);
return;
}
@@ -2323,15 +2374,19 @@ async function loadUserVideos(pubkey) {
container,
app,
loadToken,
allowEmptyMessage: true,
allowEmptyMessage: true
});
renderedFromCache = rendered || renderedFromCache;
} catch (err) {
if (loadToken === currentVideoLoadToken && container && !renderedFromCache) {
if (
loadToken === currentVideoLoadToken &&
container &&
!renderedFromCache
) {
container.dataset.hasChannelVideos = "false";
container.innerHTML = `
<p class="text-red-400">Failed to load videos. Please try again.</p>
<p class="text-critical">Failed to load videos. Please try again.</p>
`;
}
console.error("Error loading user videos:", err);
@@ -2359,7 +2414,10 @@ window.addEventListener("bitvid:access-control-updated", () => {
}
loadUserVideos(currentChannelHex).catch((error) => {
console.error("Failed to refresh channel videos after admin update:", error);
console.error(
"Failed to refresh channel videos after admin update:",
error
);
});
});
@@ -2377,4 +2435,3 @@ function dedupeToNewestByRoot(videos) {
}
return Array.from(map.values());
}
+134 -102
View File
@@ -5,11 +5,11 @@ import {
pointerKey,
normalizePointerInput,
nostrClient,
updateWatchHistoryList,
updateWatchHistoryList
} from "./nostr.js";
import {
WATCH_HISTORY_BATCH_RESOLVE,
WATCH_HISTORY_BATCH_PAGE_SIZE,
WATCH_HISTORY_BATCH_PAGE_SIZE
} from "./config.js";
import { getApplication } from "./applicationContext.js";
@@ -34,8 +34,7 @@ const WATCH_HISTORY_BATCH_SIZE = (() => {
const FALLBACK_THUMBNAIL = "assets/svg/default-thumbnail.svg";
const FALLBACK_AVATAR = "assets/svg/default-profile.svg";
const isDevEnv =
typeof process !== "undefined" &&
process?.env?.NODE_ENV !== "production";
typeof process !== "undefined" && process?.env?.NODE_ENV !== "production";
function escapeSelector(value) {
if (typeof value !== "string") {
@@ -95,12 +94,13 @@ function buildWatchHistoryFeedRuntime({ actor, cursor = 0 } = {}) {
const runtime = {
blacklistedEventIds: blacklist,
isAuthorBlocked: (pubkey) =>
(typeof app?.isAuthorBlocked === "function" && app.isAuthorBlocked(pubkey)) ||
(typeof app?.isAuthorBlocked === "function" &&
app.isAuthorBlocked(pubkey)) ||
false,
watchHistory: {
actor: normalizedActor,
cursor: Number.isFinite(cursor) ? cursor : 0,
},
cursor: Number.isFinite(cursor) ? cursor : 0
}
};
if (normalizedActor) {
@@ -115,7 +115,7 @@ function safeLocaleDate(date) {
return date.toLocaleDateString(undefined, {
weekday: "long",
month: "long",
day: "numeric",
day: "numeric"
});
} catch (error) {
return date.toDateString();
@@ -160,7 +160,7 @@ function formatRelativeTime(timestampSeconds) {
{ label: "week", seconds: 604800 },
{ label: "day", seconds: 86400 },
{ label: "hour", seconds: 3600 },
{ label: "minute", seconds: 60 },
{ label: "minute", seconds: 60 }
];
for (const unit of units) {
const count = Math.floor(delta / unit.seconds);
@@ -208,15 +208,15 @@ function normalizeHistoryItems(rawItems) {
const watchedAtRaw = Number.isFinite(candidate?.watchedAt)
? candidate.watchedAt
: Number.isFinite(candidate?.timestamp)
? candidate.timestamp
: null;
? candidate.timestamp
: null;
const watchedAt =
watchedAtRaw !== null ? Math.max(0, Math.floor(watchedAtRaw)) : 0;
normalized.push({
pointer,
pointerKey: key,
watchedAt,
raw: candidate,
raw: candidate
});
}
normalized.sort((a, b) => {
@@ -232,7 +232,7 @@ async function defaultRemoveHandler({
actor,
items,
snapshot,
reason = "remove-item",
reason = "remove-item"
} = {}) {
const sanitized = Array.isArray(items)
? items
@@ -259,7 +259,7 @@ async function defaultRemoveHandler({
await updateWatchHistoryList(sanitized, {
actorPubkey: actor,
replace: true,
source: reason,
source: reason
});
}
} catch (error) {
@@ -338,12 +338,7 @@ function getPointerVideoId(video, pointer) {
return "";
}
export function buildHistoryCard({
item,
video,
profile,
metadataPreference,
}) {
export function buildHistoryCard({ item, video, profile, metadataPreference }) {
const article = document.createElement("article");
article.className = "watch-history-card";
article.dataset.pointerKey = item.pointerKey;
@@ -364,8 +359,8 @@ export function buildHistoryCard({
typeof video.magnet === "string"
? video.magnet.trim()
: typeof video.infoHash === "string"
? video.infoHash.trim()
: "";
? video.infoHash.trim()
: "";
return { url, magnet: magnetRaw };
})();
@@ -391,7 +386,7 @@ export function buildHistoryCard({
const thumbnailImg = document.createElement("img");
thumbnailImg.alt = video?.title || "Video thumbnail";
const thumbnailSrc =
(video && typeof video.thumbnail === "string" && video.thumbnail.trim())
video && typeof video.thumbnail === "string" && video.thumbnail.trim()
? video.thumbnail.trim()
: FALLBACK_THUMBNAIL;
thumbnailImg.src = thumbnailSrc;
@@ -421,7 +416,9 @@ export function buildHistoryCard({
const created = document.createElement("p");
created.className = "watch-history-card__created";
const createdAt = Number.isFinite(video?.created_at) ? video.created_at : null;
const createdAt = Number.isFinite(video?.created_at)
? video.created_at
: null;
created.textContent = createdAt
? `Published ${formatRelativeTime(createdAt)}`
: "Published date unavailable";
@@ -455,7 +452,7 @@ export function buildHistoryCard({
}
const avatarImg = document.createElement("img");
const avatarSrc =
(profile && typeof profile.picture === "string" && profile.picture.trim())
profile && typeof profile.picture === "string" && profile.picture.trim()
? profile.picture.trim()
: FALLBACK_AVATAR;
avatarImg.src = avatarSrc;
@@ -538,7 +535,7 @@ export function buildHistoryCard({
}
removeButton.setAttribute(
"aria-label",
"Remove this encrypted history entry (sync may take a moment).",
"Remove this encrypted history entry (sync may take a moment)."
);
removeButton.title =
"Removes this entry from encrypted history. Relay sync may take a moment.";
@@ -551,7 +548,7 @@ export function buildHistoryCard({
if (metadataPreference === "relay-opt-in") {
const hint = document.createElement("p");
hint.className = "text-xs text-blue-200";
hint.className = "text-xs text-info";
hint.textContent = "Metadata is shared with relays for this entry.";
meta.appendChild(hint);
}
@@ -570,12 +567,12 @@ export function createWatchHistoryRenderer(config = {}) {
if (engine && typeof engine.run === "function") {
const runtime = buildWatchHistoryFeedRuntime({
actor: actorInput,
cursor,
cursor
});
return engine.run("watch-history", { runtime });
}
const items = await watchHistoryService.loadLatest(actorInput, {
allowStale: true,
allowStale: true
});
const normalized = normalizeHistoryItems(items);
return { items: normalized, metadata: { engine: "service-fallback" } };
@@ -589,8 +586,8 @@ export function createWatchHistoryRenderer(config = {}) {
emptySelector = "#watchHistoryEmpty",
sentinelSelector = "#watchHistorySentinel",
loadMoreSelector = "#watchHistoryLoadMore",
clearButtonSelector = "[data-history-action=\"clear-cache\"]",
republishButtonSelector = "[data-history-action=\"republish\"]",
clearButtonSelector = '[data-history-action="clear-cache"]',
republishButtonSelector = '[data-history-action="republish"]',
privacyBannerSelector = "#watchHistoryPrivacyBanner",
privacyMessageSelector = "#watchHistoryPrivacyMessage",
privacyToggleSelector = "#watchHistoryPrivacyToggle",
@@ -615,7 +612,7 @@ export function createWatchHistoryRenderer(config = {}) {
return controller.handleWatchHistoryRemoval(payload);
}
return defaultRemoveHandler(payload);
},
}
} = config;
const syncEnabled =
@@ -655,7 +652,7 @@ export function createWatchHistoryRenderer(config = {}) {
syncEnabled,
localSupported,
localOnly,
featureEnabled: syncEnabled || localSupported,
featureEnabled: syncEnabled || localSupported
};
let rendererRef = null;
@@ -687,7 +684,7 @@ export function createWatchHistoryRenderer(config = {}) {
metadataToggle: null,
metadataThumb: null,
metadataLabel: null,
metadataDescription: null,
metadataDescription: null
};
let boundGridClickHandler = null;
@@ -725,7 +722,7 @@ export function createWatchHistoryRenderer(config = {}) {
metadataToggle: resolveElement(metadataToggleSelector),
metadataThumb: resolveElement(metadataThumbSelector),
metadataLabel: resolveElement(metadataLabelSelector),
metadataDescription: resolveElement(metadataDescriptionSelector),
metadataDescription: resolveElement(metadataDescriptionSelector)
};
}
@@ -737,7 +734,7 @@ export function createWatchHistoryRenderer(config = {}) {
if (isDevEnv) {
console.warn(
"[historyView] Failed to unsubscribe from watch history event:",
error,
error
);
}
}
@@ -764,11 +761,12 @@ export function createWatchHistoryRenderer(config = {}) {
if (!text) {
return;
}
const variantClass = {
info: "border-blue-500/40 bg-blue-500/10 text-blue-100",
warning: "border-amber-500/40 bg-amber-500/10 text-amber-100",
error: "border-red-500/40 bg-red-500/10 text-red-100",
}[variant] || "border-gray-600 bg-gray-800/70 text-gray-100";
const variantClass =
{
info: "border-info/40 bg-info/10 text-info",
warning: "border-warning-strong bg-warning-surface text-warning-strong",
error: "border-critical/40 bg-critical/10 text-critical"
}[variant] || "border-border bg-panel/70 text-text";
if (elements.toastRegion.childElementCount >= 3) {
const firstChild = elements.toastRegion.firstElementChild;
@@ -840,11 +838,14 @@ export function createWatchHistoryRenderer(config = {}) {
return;
}
const enabled = state.metadataStorageEnabled;
elements.metadataToggle.setAttribute("aria-checked", enabled ? "true" : "false");
elements.metadataToggle.classList.toggle("bg-blue-600", enabled);
elements.metadataToggle.classList.toggle("border-blue-500", enabled);
elements.metadataToggle.classList.toggle("bg-gray-700", !enabled);
elements.metadataToggle.classList.toggle("border-gray-600", !enabled);
elements.metadataToggle.setAttribute(
"aria-checked",
enabled ? "true" : "false"
);
elements.metadataToggle.classList.toggle("bg-info", enabled);
elements.metadataToggle.classList.toggle("border-info", enabled);
elements.metadataToggle.classList.toggle("bg-panel", !enabled);
elements.metadataToggle.classList.toggle("border-border", !enabled);
if (elements.metadataThumb instanceof HTMLElement) {
elements.metadataThumb.classList.toggle("translate-x-5", enabled);
elements.metadataThumb.classList.toggle("translate-x-1", !enabled);
@@ -862,7 +863,8 @@ export function createWatchHistoryRenderer(config = {}) {
? nostrClient.sessionActor.pubkey
: "";
const activeActor = typeof state.actor === "string" ? state.actor : "";
const fallbackActive = !nip07Pubkey && sessionPubkey && sessionPubkey === activeActor;
const fallbackActive =
!nip07Pubkey && sessionPubkey && sessionPubkey === activeActor;
state.sessionFallbackActive = Boolean(fallbackActive);
setHidden(elements.sessionWarning, !fallbackActive);
}
@@ -894,11 +896,7 @@ export function createWatchHistoryRenderer(config = {}) {
if (!payload) {
return;
}
if (
payload.actor &&
state.actor &&
payload.actor !== state.actor
) {
if (payload.actor && state.actor && payload.actor !== state.actor) {
return;
}
let message = "Republish retry scheduled.";
@@ -942,11 +940,7 @@ export function createWatchHistoryRenderer(config = {}) {
if (!payload) {
return;
}
if (
payload.actor &&
state.actor &&
payload.actor !== state.actor
) {
if (payload.actor && state.actor && payload.actor !== state.actor) {
return;
}
fingerprintRefreshQueued = true;
@@ -1049,7 +1043,7 @@ export function createWatchHistoryRenderer(config = {}) {
{
root: elements.scrollContainer || null,
rootMargin: "0px 0px 320px 0px",
threshold: 0.1,
threshold: 0.1
}
);
}
@@ -1139,7 +1133,10 @@ export function createWatchHistoryRenderer(config = {}) {
return result.trim();
}
} catch (error) {
console.warn("[historyView] Failed to resolve actor via getActor:", error);
console.warn(
"[historyView] Failed to resolve actor via getActor:",
error
);
}
}
if (watchHistoryService?.publishView) {
@@ -1160,20 +1157,30 @@ export function createWatchHistoryRenderer(config = {}) {
const pointerKeyValue = item.pointerKey || null;
const baseMetadata =
item && typeof item === "object" && item.metadata && typeof item.metadata === "object"
item &&
typeof item === "object" &&
item.metadata &&
typeof item.metadata === "object"
? { ...item.metadata }
: {};
let cached = pointerKeyValue ? state.metadataCache.get(pointerKeyValue) : null;
if (!cached && pointerKeyValue && typeof watchHistoryService.getLocalMetadata === "function") {
let cached = pointerKeyValue
? state.metadataCache.get(pointerKeyValue)
: null;
if (
!cached &&
pointerKeyValue &&
typeof watchHistoryService.getLocalMetadata === "function"
) {
try {
cached = watchHistoryService.getLocalMetadata(pointerKeyValue) || null;
cached =
watchHistoryService.getLocalMetadata(pointerKeyValue) || null;
} catch (error) {
if (isDevEnv) {
console.warn(
"[historyView] Failed to read stored metadata for pointer:",
pointerKeyValue,
error,
error
);
}
}
@@ -1190,30 +1197,32 @@ export function createWatchHistoryRenderer(config = {}) {
const metadata = {
...baseMetadata,
video: video || null,
profile: profile || null,
profile: profile || null
};
if (pointerKeyValue) {
state.metadataCache.set(pointerKeyValue, {
video: metadata.video,
profile: metadata.profile,
profile: metadata.profile
});
if (state.metadataStorageEnabled) {
try {
watchHistoryService.setLocalMetadata?.(pointerKeyValue, {
video: metadata.video,
profile: metadata.profile,
profile: metadata.profile
});
} catch (error) {
if (isDevEnv) {
console.warn(
"[historyView] Failed to persist metadata for pointer:",
pointerKeyValue,
error,
error
);
}
}
} else if (typeof watchHistoryService.removeLocalMetadata === "function") {
} else if (
typeof watchHistoryService.removeLocalMetadata === "function"
) {
try {
watchHistoryService.removeLocalMetadata(pointerKeyValue);
} catch (error) {
@@ -1221,7 +1230,7 @@ export function createWatchHistoryRenderer(config = {}) {
console.warn(
"[historyView] Failed to remove cached metadata for pointer:",
pointerKeyValue,
error,
error
);
}
}
@@ -1279,7 +1288,8 @@ export function createWatchHistoryRenderer(config = {}) {
section.dataset.historyDay = dayKey;
section.className = "space-y-4";
const header = document.createElement("h3");
header.className = "text-sm font-semibold uppercase tracking-wide text-gray-300";
header.className =
"text-sm font-semibold uppercase tracking-wide text-text";
header.textContent = label;
const list = document.createElement("div");
list.dataset.historyDayList = "true";
@@ -1308,7 +1318,7 @@ export function createWatchHistoryRenderer(config = {}) {
item: entry,
video: entry.metadata?.video || null,
profile: entry.metadata?.profile || null,
metadataPreference: state.metadataPreference,
metadataPreference: state.metadataPreference
});
container.list.appendChild(card);
});
@@ -1331,14 +1341,17 @@ export function createWatchHistoryRenderer(config = {}) {
elements.grid.removeEventListener("click", boundGridClickHandler);
}
boundGridClickHandler = async (event) => {
const trigger = event.target instanceof HTMLElement
? event.target.closest("[data-history-action]")
: null;
const trigger =
event.target instanceof HTMLElement
? event.target.closest("[data-history-action]")
: null;
if (!(trigger instanceof HTMLElement)) {
return;
}
const action = trigger.dataset.historyAction || "";
const pointerKeyAttr = trigger.dataset.pointerKey || (trigger.closest("[data-pointer-key]")?.dataset.pointerKey ?? "");
const pointerKeyAttr =
trigger.dataset.pointerKey ||
(trigger.closest("[data-pointer-key]")?.dataset.pointerKey ?? "");
if (!action) {
return;
}
@@ -1366,7 +1379,7 @@ export function createWatchHistoryRenderer(config = {}) {
if (app?.playVideoWithFallback) {
app.playVideoWithFallback({
url,
magnet: magnetAttr,
magnet: magnetAttr
});
}
break;
@@ -1430,7 +1443,7 @@ export function createWatchHistoryRenderer(config = {}) {
if (isDevEnv) {
console.warn(
"[historyView] Failed to clear stored metadata while resetting progress:",
error,
error
);
}
}
@@ -1451,7 +1464,10 @@ export function createWatchHistoryRenderer(config = {}) {
if (elements.republishButton) {
if (boundRepublishHandler) {
elements.republishButton.removeEventListener("click", boundRepublishHandler);
elements.republishButton.removeEventListener(
"click",
boundRepublishHandler
);
}
boundRepublishHandler = async (event) => {
event.preventDefault();
@@ -1459,7 +1475,7 @@ export function createWatchHistoryRenderer(config = {}) {
try {
const payload = state.items.map((entry) => ({
...entry.pointer,
watchedAt: entry.watchedAt,
watchedAt: entry.watchedAt
}));
await snapshot(payload, { actor, reason: "manual-republish" });
const app = getAppInstance();
@@ -1499,7 +1515,10 @@ export function createWatchHistoryRenderer(config = {}) {
updatePrivacyBanner();
renderer.render();
};
elements.privacyToggle.addEventListener("click", boundPrivacyToggleHandler);
elements.privacyToggle.addEventListener(
"click",
boundPrivacyToggleHandler
);
}
if (elements.privacyDismiss) {
@@ -1515,7 +1534,10 @@ export function createWatchHistoryRenderer(config = {}) {
writePreference(WATCH_HISTORY_PRIVACY_DISMISSED_KEY, "true");
updatePrivacyBanner();
};
elements.privacyDismiss.addEventListener("click", boundPrivacyDismissHandler);
elements.privacyDismiss.addEventListener(
"click",
boundPrivacyDismissHandler
);
}
}
@@ -1538,7 +1560,10 @@ export function createWatchHistoryRenderer(config = {}) {
}
} catch (error) {
if (isDevEnv) {
console.warn("[historyView] Failed to set metadata preference:", error);
console.warn(
"[historyView] Failed to set metadata preference:",
error
);
}
}
state.metadataStorageEnabled =
@@ -1553,7 +1578,7 @@ export function createWatchHistoryRenderer(config = {}) {
if (isDevEnv) {
console.warn(
"[historyView] Failed to clear stored metadata after disabling preference:",
error,
error
);
}
}
@@ -1561,7 +1586,10 @@ export function createWatchHistoryRenderer(config = {}) {
void renderer.render();
}
};
elements.metadataToggle.addEventListener("click", boundMetadataToggleHandler);
elements.metadataToggle.addEventListener(
"click",
boundMetadataToggleHandler
);
}
async function loadHistory({ force = false, actorOverride } = {}) {
@@ -1624,11 +1652,14 @@ export function createWatchHistoryRenderer(config = {}) {
const watchedAtRaw = Number.isFinite(entry?.watchedAt)
? entry.watchedAt
: Number.isFinite(entry?.metadata?.watchedAt)
? entry.metadata.watchedAt
: 0;
? entry.metadata.watchedAt
: 0;
const watchedAt = Math.max(0, Math.floor(Number(watchedAtRaw) || 0));
const baseMetadata =
entry && typeof entry === "object" && entry.metadata && typeof entry.metadata === "object"
entry &&
typeof entry === "object" &&
entry.metadata &&
typeof entry.metadata === "object"
? { ...entry.metadata }
: {};
const video = entry?.video || baseMetadata.video || null;
@@ -1640,18 +1671,18 @@ export function createWatchHistoryRenderer(config = {}) {
? baseMetadata.watchedAt
: watchedAt || null,
video: video || null,
profile: profile || null,
profile: profile || null
};
state.metadataCache.set(pointerKeyValue, {
video: metadata.video,
profile: metadata.profile,
profile: metadata.profile
});
normalized.push({
pointer,
pointerKey: pointerKeyValue,
watchedAt,
video: metadata.video,
metadata,
metadata
});
}
@@ -1717,10 +1748,8 @@ export function createWatchHistoryRenderer(config = {}) {
);
state.metadataPreference =
storedPreference === "relay-opt-in" ? "relay-opt-in" : "encrypted-only";
state.privacyDismissed = readPreference(
WATCH_HISTORY_PRIVACY_DISMISSED_KEY,
"false"
) === "true";
state.privacyDismissed =
readPreference(WATCH_HISTORY_PRIVACY_DISMISSED_KEY, "false") === "true";
updatePrivacyBanner();
state.initialized = true;
if (!state.featureEnabled) {
@@ -1771,7 +1800,7 @@ export function createWatchHistoryRenderer(config = {}) {
const { actor, force = true } = options;
const changed = await loadHistory({
force,
actorOverride: actor,
actorOverride: actor
});
if (!changed && state.items.length) {
updateLoadMoreVisibility();
@@ -1814,7 +1843,10 @@ export function createWatchHistoryRenderer(config = {}) {
boundClearHandler = null;
}
if (elements.republishButton && boundRepublishHandler) {
elements.republishButton.removeEventListener("click", boundRepublishHandler);
elements.republishButton.removeEventListener(
"click",
boundRepublishHandler
);
boundRepublishHandler = null;
}
if (elements.privacyToggle && boundPrivacyToggleHandler) {
@@ -1880,7 +1912,7 @@ export function createWatchHistoryRenderer(config = {}) {
console.warn(
"[historyView] Failed to drop cached metadata for pointer:",
pointerKeyValue,
error,
error
);
}
}
@@ -1904,7 +1936,7 @@ export function createWatchHistoryRenderer(config = {}) {
removed,
pointerKey: pointerKeyValue,
snapshot,
reason: "remove-item",
reason: "remove-item"
});
const app = getAppInstance();
if (!result?.handledToasts) {
@@ -1929,7 +1961,7 @@ export function createWatchHistoryRenderer(config = {}) {
if (isDevEnv) {
console.warn(
"[historyView] Failed to refresh watch history after removal:",
error,
error
);
}
}
@@ -1942,9 +1974,9 @@ export function createWatchHistoryRenderer(config = {}) {
getState() {
return {
...state,
items: state.items.slice(),
items: state.items.slice()
};
},
}
};
rendererRef = renderer;
+64 -70
View File
@@ -1,16 +1,16 @@
// js/subscriptions.js
import {
nostrClient,
convertEventToVideo as sharedConvertEventToVideo,
convertEventToVideo as sharedConvertEventToVideo
} from "./nostr.js";
import {
buildSubscriptionListEvent,
SUBSCRIPTION_LIST_IDENTIFIER,
SUBSCRIPTION_LIST_IDENTIFIER
} from "./nostrEventSchemas.js";
import { getSidebarLoadingMarkup } from "./sidebarLoading.js";
import {
publishEventToRelays,
assertAnyRelayAccepted,
assertAnyRelayAccepted
} from "./nostrPublish.js";
import { getApplication } from "./applicationContext.js";
import { VideoListView } from "./ui/views/VideoListView.js";
@@ -48,7 +48,7 @@ class SubscriptionsManager {
kinds: [30002],
authors: [userPubkey],
"#d": [SUBSCRIPTION_LIST_IDENTIFIER],
limit: 1,
limit: 1
};
const events = [];
@@ -180,7 +180,7 @@ class SubscriptionsManager {
const evt = buildSubscriptionListEvent({
pubkey: userPubkey,
created_at: Math.floor(Date.now() / 1000),
content: cipherText,
content: cipherText
});
let signedEvent;
@@ -200,7 +200,7 @@ class SubscriptionsManager {
let publishSummary;
try {
publishSummary = assertAnyRelayAccepted(publishResults, {
context: "subscription list",
context: "subscription list"
});
} catch (publishError) {
if (publishError?.relayFailures?.length) {
@@ -222,8 +222,8 @@ class SubscriptionsManager {
relayError instanceof Error
? relayError.message
: relayError
? String(relayError)
: "publish failed";
? String(relayError)
: "publish failed";
console.warn(
`[SubscriptionsManager] Subscription list not accepted by ${url}: ${reason}`,
relayError
@@ -251,9 +251,10 @@ class SubscriptionsManager {
options = {}
) {
const limitCandidate = Number(options?.limit);
const limit = Number.isFinite(limitCandidate) && limitCandidate > 0
? Math.floor(limitCandidate)
: null;
const limit =
Number.isFinite(limitCandidate) && limitCandidate > 0
? Math.floor(limitCandidate)
: null;
const reason =
typeof options?.reason === "string" && options.reason.trim()
? options.reason.trim()
@@ -265,7 +266,7 @@ class SubscriptionsManager {
if (!userPubkey) {
if (container) {
container.innerHTML =
"<p class='text-gray-500'>Please log in first.</p>";
"<p class='text-muted-strong'>Please log in first.</p>";
}
this.lastRunOptions = null;
this.lastResult = null;
@@ -281,18 +282,18 @@ class SubscriptionsManager {
this.lastRunOptions = {
actorPubkey: userPubkey,
limit,
containerId,
containerId
};
return null;
}
if (!channelHexes.length) {
container.innerHTML =
"<p class='text-gray-500'>No subscriptions found.</p>";
"<p class='text-muted-strong'>No subscriptions found.</p>";
this.lastRunOptions = {
actorPubkey: userPubkey,
limit,
containerId,
containerId
};
this.lastResult = { items: [], metadata: { reason: "no-subscriptions" } };
return this.lastResult;
@@ -304,14 +305,14 @@ class SubscriptionsManager {
actorPubkey: userPubkey,
limit,
containerId,
reason,
reason
};
this.ensureFeedRegistered();
const engine = this.getFeedEngine();
if (!engine || typeof engine.run !== "function") {
container.innerHTML =
"<p class='text-gray-500'>Subscriptions are unavailable right now.</p>";
"<p class='text-muted-strong'>Subscriptions are unavailable right now.</p>";
return null;
}
@@ -323,9 +324,9 @@ class SubscriptionsManager {
runtime,
hooks: {
subscriptions: {
resolveAuthors: () => this.getSubscribedAuthors(),
},
},
resolveAuthors: () => this.getSubscribedAuthors()
}
}
};
try {
@@ -352,7 +353,7 @@ class SubscriptionsManager {
error
);
container.innerHTML =
"<p class='text-gray-500'>Unable to load subscriptions right now.</p>";
"<p class='text-muted-strong'>Unable to load subscriptions right now.</p>";
this.lastResult = null;
return null;
}
@@ -396,7 +397,7 @@ class SubscriptionsManager {
subscriptionAuthors: normalizedAuthors,
authors: normalizedAuthors,
blacklistedEventIds: blacklist,
isAuthorBlocked,
isAuthorBlocked
};
}
@@ -418,9 +419,10 @@ class SubscriptionsManager {
: {};
const limitCandidate = Number(options?.limit);
const limit = Number.isFinite(limitCandidate) && limitCandidate > 0
? Math.floor(limitCandidate)
: null;
const limit =
Number.isFinite(limitCandidate) && limitCandidate > 0
? Math.floor(limitCandidate)
: null;
const limitedItems = limit ? items.slice(0, limit) : items;
const videos = limitedItems
@@ -429,7 +431,7 @@ class SubscriptionsManager {
if (!videos.length) {
container.innerHTML = `
<p class="flex justify-center items-center h-full w-full text-center text-gray-500">
<p class="flex justify-center items-center h-full w-full text-center text-muted-strong">
No videos available yet.
</p>`;
return;
@@ -438,7 +440,7 @@ class SubscriptionsManager {
const listView = this.getListView(container, app);
if (!listView) {
container.innerHTML = `
<p class="flex justify-center items-center h-full w-full text-center text-gray-500">
<p class="flex justify-center items-center h-full w-full text-center text-muted-strong">
Unable to render subscriptions feed.
</p>`;
return;
@@ -452,7 +454,7 @@ class SubscriptionsManager {
const enrichedMetadata = {
...metadata,
feed: "subscriptions",
feed: "subscriptions"
};
if (limit) {
enrichedMetadata.limit = limit;
@@ -479,7 +481,7 @@ class SubscriptionsManager {
const badgeHelpers = baseView?.badgeHelpers || {
attachHealthBadges: () => {},
attachUrlHealthBadges: () => {},
attachUrlHealthBadges: () => {}
};
const formatTimeAgo = (timestamp) => {
@@ -502,36 +504,36 @@ class SubscriptionsManager {
const assets = baseView?.assets || {
fallbackThumbnailSrc: "assets/jpg/video-thumbnail-fallback.jpg",
unsupportedBtihMessage:
"This magnet link is missing a compatible BitTorrent v1 info hash.",
"This magnet link is missing a compatible BitTorrent v1 info hash."
};
const loadedThumbnails =
app?.loadedThumbnails instanceof Map
? app.loadedThumbnails
: baseView?.state?.loadedThumbnails instanceof Map
? baseView.state.loadedThumbnails
: new Map();
? baseView.state.loadedThumbnails
: new Map();
const videosMap =
app?.videosMap instanceof Map
? app.videosMap
: baseView?.state?.videosMap instanceof Map
? baseView.state.videosMap
: new Map();
? baseView.state.videosMap
: new Map();
const urlHealthCache =
app?.urlHealthSnapshots instanceof Map
? app.urlHealthSnapshots
: baseView?.state?.urlHealthByVideoId instanceof Map
? baseView.state.urlHealthByVideoId
: new Map();
? baseView.state.urlHealthByVideoId
: new Map();
const streamHealthCache =
app?.streamHealthSnapshots instanceof Map
? app.streamHealthSnapshots
: baseView?.state?.streamHealthByVideoId instanceof Map
? baseView.state.streamHealthByVideoId
: new Map();
? baseView.state.streamHealthByVideoId
: new Map();
const listViewConfig = {
document: doc,
@@ -540,21 +542,21 @@ class SubscriptionsManager {
badgeHelpers,
formatters: {
formatTimeAgo,
formatViewCountLabel,
formatViewCountLabel
},
helpers: {
escapeHtml: (value) => app?.escapeHTML?.(value) ?? value,
isMagnetSupported: (magnet) =>
app?.isMagnetUriSupported?.(magnet) ?? false,
toLocaleString: (value) =>
typeof value === "number" ? value.toLocaleString() : value,
typeof value === "number" ? value.toLocaleString() : value
},
assets,
state: {
loadedThumbnails,
videosMap,
urlHealthByVideoId: urlHealthCache,
streamHealthByVideoId: streamHealthCache,
streamHealthByVideoId: streamHealthCache
},
utils: {
dedupeVideos: (videos) => (Array.isArray(videos) ? [...videos] : []),
@@ -574,24 +576,22 @@ class SubscriptionsManager {
app?.canCurrentUserManageBlacklist?.() ?? false,
canEditVideo: (video) => video?.pubkey === app?.pubkey,
canDeleteVideo: (video) => video?.pubkey === app?.pubkey,
batchFetchProfiles: (authorSet) =>
app?.batchFetchProfiles?.(authorSet),
batchFetchProfiles: (authorSet) => app?.batchFetchProfiles?.(authorSet),
bindThumbnailFallbacks: (target) =>
app?.bindThumbnailFallbacks?.(target),
handleUrlHealthBadge: (payload) =>
app?.handleUrlHealthBadge?.(payload),
handleUrlHealthBadge: (payload) => app?.handleUrlHealthBadge?.(payload),
refreshDiscussionCounts: (videosList, { container: root } = {}) =>
app?.refreshVideoDiscussionCounts?.(videosList, {
videoListRoot: root || container || null,
videoListRoot: root || container || null
}),
ensureGlobalMoreMenuHandlers: () =>
app?.ensureGlobalMoreMenuHandlers?.(),
closeAllMenus: () => app?.closeAllMoreMenus?.(),
closeAllMenus: () => app?.closeAllMoreMenus?.()
},
renderers: {
getLoadingMarkup: (message) => getSidebarLoadingMarkup(message),
getLoadingMarkup: (message) => getSidebarLoadingMarkup(message)
},
allowNsfw: ALLOW_NSFW_CONTENT === true,
allowNsfw: ALLOW_NSFW_CONTENT === true
};
const listView = new VideoListView(listViewConfig);
@@ -604,16 +604,14 @@ class SubscriptionsManager {
Promise.resolve(
app?.playVideoByEventId?.(detail.videoId, {
url: detail.url,
magnet: detail.magnet,
magnet: detail.magnet
})
).catch(
(error) => {
console.error(
"[SubscriptionsManager] Failed to play by event id:",
error
);
}
);
).catch((error) => {
console.error(
"[SubscriptionsManager] Failed to play by event id:",
error
);
});
return;
}
Promise.resolve(
@@ -632,7 +630,7 @@ class SubscriptionsManager {
}
app?.handleEditVideo?.({
eventId: video.id,
index: Number.isFinite(index) ? index : null,
index: Number.isFinite(index) ? index : null
});
});
@@ -642,7 +640,7 @@ class SubscriptionsManager {
}
app?.handleRevertVideo?.({
eventId: video.id,
index: Number.isFinite(index) ? index : null,
index: Number.isFinite(index) ? index : null
});
});
@@ -652,7 +650,7 @@ class SubscriptionsManager {
}
app?.handleFullDeleteVideo?.({
eventId: video.id,
index: Number.isFinite(index) ? index : null,
index: Number.isFinite(index) ? index : null
});
});
@@ -660,7 +658,7 @@ class SubscriptionsManager {
const detail = {
...(dataset || {}),
author: dataset?.author || video?.pubkey || "",
context: dataset?.context || "subscriptions",
context: dataset?.context || "subscriptions"
};
app?.handleMoreMenuAction?.("blacklist-author", detail);
});
@@ -670,11 +668,8 @@ class SubscriptionsManager {
const dataset = {
...(detail.dataset || {}),
eventId:
detail.eventId ||
detail.dataset?.eventId ||
detail.video?.id ||
"",
context: detail.dataset?.context || "subscriptions",
detail.eventId || detail.dataset?.eventId || detail.video?.id || "",
context: detail.dataset?.context || "subscriptions"
};
app?.handleMoreMenuAction?.(detail.action || "copy-link", dataset);
});
@@ -684,7 +679,7 @@ class SubscriptionsManager {
const dataset = {
...(detail.dataset || {}),
eventId: detail.dataset?.eventId || detail.video?.id || "",
context: detail.dataset?.context || "subscriptions",
context: detail.dataset?.context || "subscriptions"
};
app?.handleMoreMenuAction?.(detail.action, dataset);
});
@@ -710,14 +705,13 @@ class SubscriptionsManager {
return this.showSubscriptionVideos(actorPubkey, containerId, {
limit,
reason,
reason
});
}
convertEventToVideo(evt) {
return sharedConvertEventToVideo(evt);
}
}
export const subscriptions = new SubscriptionsManager();
+86 -47
View File
@@ -15,20 +15,29 @@ export class UploadModal {
getCurrentPubkey,
safeEncodeNpub,
eventTarget,
container,
container
} = {}) {
this.authService = authService || null;
this.r2Service = r2Service || null;
this.publishVideoNote = typeof publishVideoNote === "function" ? publishVideoNote : null;
this.publishVideoNote =
typeof publishVideoNote === "function" ? publishVideoNote : null;
this.removeTrackingScripts =
typeof removeTrackingScripts === "function" ? removeTrackingScripts : () => {};
typeof removeTrackingScripts === "function"
? removeTrackingScripts
: () => {};
this.setGlobalModalState =
typeof setGlobalModalState === "function" ? setGlobalModalState : () => {};
typeof setGlobalModalState === "function"
? setGlobalModalState
: () => {};
this.showError = typeof showError === "function" ? showError : () => {};
this.showSuccess = typeof showSuccess === "function" ? showSuccess : () => {};
this.getCurrentPubkey = typeof getCurrentPubkey === "function" ? getCurrentPubkey : null;
this.safeEncodeNpub = typeof safeEncodeNpub === "function" ? safeEncodeNpub : () => "";
this.eventTarget = eventTarget instanceof EventTarget ? eventTarget : new EventTarget();
this.showSuccess =
typeof showSuccess === "function" ? showSuccess : () => {};
this.getCurrentPubkey =
typeof getCurrentPubkey === "function" ? getCurrentPubkey : null;
this.safeEncodeNpub =
typeof safeEncodeNpub === "function" ? safeEncodeNpub : () => "";
this.eventTarget =
eventTarget instanceof EventTarget ? eventTarget : new EventTarget();
this.container = container || null;
this.root = null;
@@ -161,13 +170,15 @@ export class UploadModal {
cacheElements(context) {
const scope = this.root || context;
this.modalBackdrop = scope?.querySelector?.(".bv-modal-backdrop") || null;
this.modalPanel = scope?.querySelector?.(".bv-modal__panel") || scope || null;
this.modalPanel =
scope?.querySelector?.(".bv-modal__panel") || scope || null;
this.uploadModeButtons = Array.from(
context.querySelectorAll(".upload-mode-toggle[data-upload-mode]")
);
this.customSection = context.querySelector("#customUploadSection") || null;
this.cloudflareSection = context.querySelector("#cloudflareUploadSection") || null;
this.cloudflareSection =
context.querySelector("#cloudflareUploadSection") || null;
this.customForm = context.querySelector("#uploadForm") || null;
this.customFormInputs = {
@@ -181,36 +192,44 @@ export class UploadModal {
enableComments: context.querySelector("#uploadEnableComments") || null,
isNsfw: context.querySelector("#uploadIsNsfw") || null,
isForKids: context.querySelector("#uploadIsForKids") || null,
isPrivate: context.querySelector("#uploadIsPrivate") || null,
isPrivate: context.querySelector("#uploadIsPrivate") || null
};
this.closeButton = context.querySelector("#closeUploadModal") || null;
this.cloudflareSettingsForm = context.querySelector("#cloudflareSettingsForm") || null;
this.cloudflareSettingsForm =
context.querySelector("#cloudflareSettingsForm") || null;
this.cloudflareClearSettingsButton =
context.querySelector("#cloudflareClearSettings") || null;
this.cloudflareSettingsStatus =
context.querySelector("#cloudflareSettingsStatus") || null;
this.cloudflareBucketPreview =
context.querySelector("#cloudflareBucketPreview") || null;
this.cloudflareUploadForm = context.querySelector("#cloudflareUploadForm") || null;
this.cloudflareUploadForm =
context.querySelector("#cloudflareUploadForm") || null;
this.cloudflareFileInput = context.querySelector("#cloudflareFile") || null;
this.cloudflareUploadButton = context.querySelector("#cloudflareUploadButton") || null;
this.cloudflareUploadButton =
context.querySelector("#cloudflareUploadButton") || null;
this.cloudflareUploadStatus =
context.querySelector("#cloudflareUploadStatus") || null;
this.cloudflareProgressBar = context.querySelector("#cloudflareProgressBar") || null;
this.cloudflareProgressFill = context.querySelector("#cloudflareProgressFill") || null;
this.cloudflareTitleInput = context.querySelector("#cloudflareTitle") || null;
this.cloudflareProgressBar =
context.querySelector("#cloudflareProgressBar") || null;
this.cloudflareProgressFill =
context.querySelector("#cloudflareProgressFill") || null;
this.cloudflareTitleInput =
context.querySelector("#cloudflareTitle") || null;
this.cloudflareDescriptionInput =
context.querySelector("#cloudflareDescription") || null;
this.cloudflareThumbnailInput =
context.querySelector("#cloudflareThumbnail") || null;
this.cloudflareMagnetInput = context.querySelector("#cloudflareMagnet") || null;
this.cloudflareMagnetInput =
context.querySelector("#cloudflareMagnet") || null;
this.cloudflareWsInput = context.querySelector("#cloudflareWs") || null;
this.cloudflareXsInput = context.querySelector("#cloudflareXs") || null;
this.cloudflareEnableCommentsInput =
context.querySelector("#cloudflareEnableComments") || null;
this.cloudflareIsNsfwInput = context.querySelector("#cloudflareIsNsfw") || null;
this.cloudflareIsNsfwInput =
context.querySelector("#cloudflareIsNsfw") || null;
this.cloudflareIsForKidsInput =
context.querySelector("#cloudflareIsForKids") || null;
this.cloudflareAdvancedToggle =
@@ -223,7 +242,8 @@ export class UploadModal {
context.querySelector("#cloudflareAdvancedFields") || null;
this.r2AccountIdInput = context.querySelector("#r2AccountId") || null;
this.r2AccessKeyIdInput = context.querySelector("#r2AccessKeyId") || null;
this.r2SecretAccessKeyInput = context.querySelector("#r2SecretAccessKey") || null;
this.r2SecretAccessKeyInput =
context.querySelector("#r2SecretAccessKey") || null;
this.r2ApiTokenInput = context.querySelector("#r2ApiToken") || null;
this.r2ZoneIdInput = context.querySelector("#r2ZoneId") || null;
this.r2BaseDomainInput = context.querySelector("#r2BaseDomain") || null;
@@ -301,7 +321,7 @@ export class UploadModal {
image: [],
fallback: [],
service: [],
autoGenerated: true,
autoGenerated: true
};
}
@@ -351,12 +371,16 @@ export class UploadModal {
if (this.cloudflareAdvancedToggle) {
this.cloudflareAdvancedToggle.addEventListener("click", () => {
if (this.r2Service?.setCloudflareAdvancedVisibility &&
this.r2Service?.getCloudflareAdvancedVisibility) {
if (
this.r2Service?.setCloudflareAdvancedVisibility &&
this.r2Service?.getCloudflareAdvancedVisibility
) {
const nextState = !this.r2Service.getCloudflareAdvancedVisibility();
this.r2Service.setCloudflareAdvancedVisibility(nextState);
} else {
this.renderCloudflareAdvancedVisibility(!this.cloudflareAdvancedVisible);
this.renderCloudflareAdvancedVisibility(
!this.cloudflareAdvancedVisible
);
}
});
}
@@ -392,7 +416,7 @@ export class UploadModal {
root: this.root,
backdrop: this.modalBackdrop || this.root,
panel: this.modalPanel || this.root,
onRequestClose: () => this.close(),
onRequestClose: () => this.close()
});
this.cleanupHandlers.push(() => {
@@ -435,13 +459,21 @@ export class UploadModal {
register(
this.r2Service.on("settingsStatus", ({ message, variant } = {}) => {
this.applyCloudflareStatus(this.cloudflareSettingsStatus, message, variant);
this.applyCloudflareStatus(
this.cloudflareSettingsStatus,
message,
variant
);
})
);
register(
this.r2Service.on("uploadStatus", ({ message, variant } = {}) => {
this.applyCloudflareStatus(this.cloudflareUploadStatus, message, variant);
this.applyCloudflareStatus(
this.cloudflareUploadStatus,
message,
variant
);
})
);
@@ -548,7 +580,7 @@ export class UploadModal {
handleCustomSubmit() {
const audienceFlags = this.sanitizeAudienceFlags({
isNsfw: this.readCheckboxValue(this.customFormInputs.isNsfw, false),
isForKids: this.readCheckboxValue(this.customFormInputs.isForKids, false),
isForKids: this.readCheckboxValue(this.customFormInputs.isForKids, false)
});
const payload = {
title: this.customFormInputs.title?.value?.trim() || "",
@@ -562,7 +594,7 @@ export class UploadModal {
this.customFormInputs.enableComments,
true
),
...audienceFlags,
...audienceFlags
};
if (this.customFormInputs.isPrivate) {
@@ -587,7 +619,7 @@ export class UploadModal {
secretAccessKey: this.r2SecretAccessKeyInput?.value?.trim() || "",
apiToken: this.r2ApiTokenInput?.value?.trim() || "",
zoneId: this.r2ZoneIdInput?.value?.trim() || "",
baseDomain: this.r2BaseDomainInput?.value || "",
baseDomain: this.r2BaseDomainInput?.value || ""
};
}
@@ -598,24 +630,24 @@ export class UploadModal {
element.textContent = message || "";
element.classList.remove(
"text-green-400",
"text-red-400",
"text-yellow-400",
"text-gray-400"
"text-info-strong",
"text-critical",
"text-warning-strong",
"text-muted"
);
if (!message) {
element.classList.add("text-gray-400");
element.classList.add("text-muted");
return;
}
let className = "text-gray-400";
let className = "text-muted";
if (variant === "success") {
className = "text-green-400";
className = "text-info-strong";
} else if (variant === "error") {
className = "text-red-400";
className = "text-critical";
} else if (variant === "warning") {
className = "text-yellow-400";
className = "text-warning-strong";
}
element.classList.add(className);
}
@@ -646,7 +678,10 @@ export class UploadModal {
}
if (this.cloudflareAdvancedToggleIcon) {
this.cloudflareAdvancedToggleIcon.classList.toggle("rotate-90", isVisible);
this.cloudflareAdvancedToggleIcon.classList.toggle(
"rotate-90",
isVisible
);
}
}
@@ -744,7 +779,8 @@ export class UploadModal {
resetCloudflareUploadForm() {
if (this.cloudflareTitleInput) this.cloudflareTitleInput.value = "";
if (this.cloudflareDescriptionInput) this.cloudflareDescriptionInput.value = "";
if (this.cloudflareDescriptionInput)
this.cloudflareDescriptionInput.value = "";
if (this.cloudflareThumbnailInput) this.cloudflareThumbnailInput.value = "";
if (this.cloudflareMagnetInput) this.cloudflareMagnetInput.value = "";
if (this.cloudflareWsInput) this.cloudflareWsInput.value = "";
@@ -766,7 +802,8 @@ export class UploadModal {
if (this.customFormInputs.magnet) this.customFormInputs.magnet.value = "";
if (this.customFormInputs.ws) this.customFormInputs.ws.value = "";
if (this.customFormInputs.xs) this.customFormInputs.xs.value = "";
if (this.customFormInputs.thumbnail) this.customFormInputs.thumbnail.value = "";
if (this.customFormInputs.thumbnail)
this.customFormInputs.thumbnail.value = "";
if (this.customFormInputs.description)
this.customFormInputs.description.value = "";
if (this.customFormInputs.enableComments)
@@ -835,7 +872,7 @@ export class UploadModal {
const file = this.cloudflareFileInput?.files?.[0] || null;
const audienceFlags = this.sanitizeAudienceFlags({
isNsfw: this.readCheckboxValue(this.cloudflareIsNsfwInput, false),
isForKids: this.readCheckboxValue(this.cloudflareIsForKidsInput, false),
isForKids: this.readCheckboxValue(this.cloudflareIsForKidsInput, false)
});
const metadata = {
title: this.cloudflareTitleInput?.value?.trim() || "",
@@ -848,7 +885,7 @@ export class UploadModal {
this.cloudflareEnableCommentsInput,
true
),
...audienceFlags,
...audienceFlags
};
const nip71Metadata = this.nip71FormManager.collectSection("cloudflare");
@@ -862,7 +899,7 @@ export class UploadModal {
}
metadata.nip71 = {
...nip71Metadata,
imeta: imetaList,
imeta: imetaList
};
}
@@ -873,8 +910,10 @@ export class UploadModal {
metadata,
settingsInput: this.collectCloudflareSettingsFormValues(),
publishVideoNote: (payload, options) =>
this.publishVideoNote ? this.publishVideoNote(payload, options) : null,
onReset: () => this.resetCloudflareUploadForm(),
this.publishVideoNote
? this.publishVideoNote(payload, options)
: null,
onReset: () => this.resetCloudflareUploadForm()
});
} catch (error) {
console.error("[UploadModal] Cloudflare upload failed", error);
+177 -156
View File
@@ -16,7 +16,7 @@ export class VideoCard {
state = {},
ensureGlobalMoreMenuHandlers,
onRequestCloseAllMenus,
nsfwContext = null,
nsfwContext = null
} = {}) {
if (!doc) {
throw new Error("VideoCard requires a document reference.");
@@ -39,30 +39,32 @@ export class VideoCard {
canDelete: false,
canRevert: false,
canManageBlacklist: false,
...capabilities,
...capabilities
};
this.formatters = {
formatTimeAgo: formatters.formatTimeAgo,
formatNumber: formatters.formatNumber,
formatNumber: formatters.formatNumber
};
this.helpers = {
escapeHtml: helpers.escapeHtml,
isMagnetSupported: helpers.isMagnetSupported,
toLocaleString: helpers.toLocaleString,
toLocaleString: helpers.toLocaleString
};
this.assets = {
fallbackThumbnailSrc: assets.fallbackThumbnailSrc || "",
unsupportedBtihMessage: assets.unsupportedBtihMessage || "",
unsupportedBtihMessage: assets.unsupportedBtihMessage || ""
};
this.state = {
loadedThumbnails:
state.loadedThumbnails instanceof Map ? state.loadedThumbnails : null,
urlHealthByVideoId:
state.urlHealthByVideoId instanceof Map ? state.urlHealthByVideoId : null,
state.urlHealthByVideoId instanceof Map
? state.urlHealthByVideoId
: null,
streamHealthByVideoId:
state.streamHealthByVideoId instanceof Map
? state.streamHealthByVideoId
: null,
: null
};
this.ensureGlobalMoreMenuHandlers =
typeof ensureGlobalMoreMenuHandlers === "function"
@@ -76,7 +78,7 @@ export class VideoCard {
this.nsfwContext = {
isNsfw: Boolean(nsfwContext?.isNsfw),
allowNsfw: nsfwContext?.allowNsfw !== false,
viewerIsOwner: nsfwContext?.viewerIsOwner === true,
viewerIsOwner: nsfwContext?.viewerIsOwner === true
};
this.shouldMaskNsfwForOwner =
this.nsfwContext.isNsfw &&
@@ -89,7 +91,7 @@ export class VideoCard {
onRevert: null,
onDelete: null,
onMoreAction: null,
onAuthorNavigate: null,
onAuthorNavigate: null
};
this.root = null;
@@ -111,8 +113,7 @@ export class VideoCard {
this.authorNameEl = null;
this.timestampEl = null;
this.playbackUrl =
typeof video.url === "string" ? video.url.trim() : "";
this.playbackUrl = typeof video.url === "string" ? video.url.trim() : "";
const magnet =
(typeof video.magnet === "string" ? video.magnet.trim() : "") ||
(typeof video.infoHash === "string" ? video.infoHash.trim() : "");
@@ -129,7 +130,11 @@ export class VideoCard {
: null;
this.postedAt = normalizedPostedAt;
if (this.postedAt !== null && this.video && typeof this.video === "object") {
if (
this.postedAt !== null &&
this.video &&
typeof this.video === "object"
) {
this.video.rootCreatedAt = this.postedAt;
}
@@ -215,13 +220,13 @@ export class VideoCard {
const root = this.createElement("div", {
classNames: [
"video-card",
"bg-gray-900",
"bg-card",
"rounded-lg",
"shadow-lg",
"hover:shadow-2xl",
"transition-all",
"duration-300",
],
"duration-300"
]
});
this.applyClassListFromString(root, this.highlightClass);
@@ -245,17 +250,17 @@ export class VideoCard {
"relative",
"group",
"rounded-t-lg",
"overflow-hidden",
"overflow-hidden"
],
attrs: {
href: this.shareUrl,
},
href: this.shareUrl
}
});
anchor.dataset.videoId = this.video.id;
this.anchorEl = anchor;
const ratio = this.createElement("div", {
classNames: ["ratio-16-9"],
classNames: ["ratio-16-9"]
});
const thumbnail = this.buildThumbnail();
ratio.appendChild(thumbnail);
@@ -267,19 +272,19 @@ export class VideoCard {
classNames: [
"text-lg",
"font-bold",
"text-white",
"text-text",
"line-clamp-2",
"hover:text-blue-400",
"hover:text-info-strong",
"cursor-pointer",
"mb-3",
"mb-3"
],
textContent: this.video.title,
textContent: this.video.title
});
title.dataset.videoId = this.video.id;
this.titleEl = title;
const header = this.createElement("div", {
classNames: ["flex", "items-center", "justify-between"],
classNames: ["flex", "items-center", "justify-between"]
});
const authorSection = this.buildAuthorSection();
@@ -308,7 +313,7 @@ export class VideoCard {
classNames: ["mt-3", "text-xs", "text-amber-300"],
attrs: { title: this.assets.unsupportedBtihMessage || "" },
textContent:
"WebTorrent fallback unavailable (magnet missing btih info hash)",
"WebTorrent fallback unavailable (magnet missing btih info hash)"
});
warning.dataset.torrentStatus = "unsupported";
content.appendChild(warning);
@@ -395,7 +400,9 @@ export class VideoCard {
const fallbackAttr =
(typeof img.dataset.fallbackSrc === "string"
? img.dataset.fallbackSrc.trim()
: "") || fallbackSrc || "";
: "") ||
fallbackSrc ||
"";
const currentSrc = img.currentSrc || img.src || "";
const isFallback =
@@ -442,7 +449,7 @@ export class VideoCard {
buildAuthorSection() {
const wrapper = this.createElement("div", {
classNames: ["flex", "items-center", "space-x-3"],
classNames: ["flex", "items-center", "space-x-3"]
});
const avatarWrapper = this.createElement("div", {
@@ -450,19 +457,19 @@ export class VideoCard {
"w-8",
"h-8",
"rounded-full",
"bg-gray-700",
"bg-panel",
"overflow-hidden",
"flex",
"items-center",
"justify-center",
],
"justify-center"
]
});
const avatar = this.createElement("img", {
attrs: {
src: "assets/svg/default-profile.svg",
alt: "Placeholder",
},
alt: "Placeholder"
}
});
avatar.classList.add("author-pic");
if (this.video.pubkey) {
@@ -475,8 +482,8 @@ export class VideoCard {
const authorMeta = this.createElement("div", { classNames: ["min-w-0"] });
const authorName = this.createElement("p", {
classNames: ["text-sm", "text-gray-400", "author-name"],
textContent: "Loading name...",
classNames: ["text-sm", "text-muted", "author-name"],
textContent: "Loading name..."
});
if (this.video.pubkey) {
authorName.dataset.pubkey = this.video.pubkey;
@@ -484,25 +491,31 @@ export class VideoCard {
authorName.style.cursor = "pointer";
const metadata = this.createElement("div", {
classNames: ["flex", "items-center", "text-xs", "text-gray-500", "mt-1"],
classNames: [
"flex",
"items-center",
"text-xs",
"text-muted-strong",
"mt-1"
]
});
const timeEl = this.createElement("span", {
textContent: this.timeAgo,
textContent: this.timeAgo
});
metadata.appendChild(timeEl);
this.timestampEl = timeEl;
if (this.pointerInfo && this.pointerInfo.key) {
const dot = this.createElement("span", {
classNames: ["mx-1", "text-gray-600"],
textContent: "•",
classNames: ["mx-1", "text-muted-strong"],
textContent: "•"
});
dot.setAttribute("aria-hidden", "true");
const view = this.createElement("span", {
classNames: ["view-count-text"],
textContent: "– views",
textContent: "– views"
});
view.dataset.viewCount = "";
view.dataset.viewPointer = this.pointerInfo.key;
@@ -528,7 +541,7 @@ export class VideoCard {
buildControls() {
const doc = this.document;
const container = this.createElement("div", {
classNames: ["flex", "items-center"],
classNames: ["flex", "items-center"]
});
const moreMenu = this.buildMoreMenu();
@@ -538,24 +551,22 @@ export class VideoCard {
if (this.capabilities.canEdit) {
const wrapper = this.createElement("div", {
classNames: ["relative", "inline-block", "ml-3", "overflow-visible"],
classNames: ["relative", "inline-block", "ml-3", "overflow-visible"]
});
const button = this.createElement("button", {
classNames: [
"focus-ring",
"inline-flex",
"items-center",
"p-2",
"rounded-full",
"text-gray-400",
"hover:text-gray-200",
"hover:bg-gray-800",
"focus:outline-none",
"focus:ring-2",
"focus:ring-blue-500",
"text-muted",
"hover:text-text",
"hover:bg-panel"
],
attrs: {
type: "button",
},
type: "button"
}
});
button.dataset.settingsDropdown = String(this.index);
@@ -575,17 +586,17 @@ export class VideoCard {
"w-32",
"rounded-md",
"shadow-lg",
"bg-gray-800",
"bg-panel",
"ring-1",
"ring-black",
"ring-opacity-5",
"z-50",
],
"z-50"
]
});
dropdown.id = `settingsDropdown-${this.index}`;
const list = this.createElement("div", {
classNames: ["py-1"],
classNames: ["py-1"]
});
const editButton = this.createElement("button", {
@@ -596,10 +607,10 @@ export class VideoCard {
"px-4",
"py-2",
"text-sm",
"text-gray-100",
"hover:bg-gray-700",
"text-text",
"hover:bg-panel-hover"
],
textContent: "Edit",
textContent: "Edit"
});
editButton.dataset.editIndex = String(this.index);
editButton.dataset.editEventId = this.video.id;
@@ -615,11 +626,11 @@ export class VideoCard {
"px-4",
"py-2",
"text-sm",
"text-red-400",
"hover:bg-red-700",
"hover:text-white",
"text-critical",
"hover:bg-critical/20",
"hover:text-text"
],
textContent: "Revert",
textContent: "Revert"
});
revertButton.dataset.revertIndex = String(this.index);
revertButton.dataset.revertEventId = this.video.id;
@@ -636,11 +647,11 @@ export class VideoCard {
"px-4",
"py-2",
"text-sm",
"text-red-400",
"hover:bg-red-700",
"hover:text-white",
"text-critical",
"hover:bg-critical/20",
"hover:text-text"
],
textContent: "Delete All",
textContent: "Delete All"
});
deleteButton.dataset.deleteAllIndex = String(this.index);
deleteButton.dataset.deleteAllEventId = this.video.id;
@@ -663,12 +674,13 @@ export class VideoCard {
buildMoreMenu() {
const wrapper = this.createElement("div", {
classNames: ["popover", "ml-1"],
classNames: ["popover", "ml-1"]
});
wrapper.dataset.moreMenuWrapper = "true";
const button = this.createElement("button", {
classNames: [
"focus-ring",
"inline-flex",
"items-center",
"justify-center",
@@ -676,19 +688,16 @@ export class VideoCard {
"h-10",
"p-2",
"rounded-full",
"text-gray-400",
"hover:text-gray-200",
"hover:bg-gray-800",
"focus:outline-none",
"focus:ring-2",
"focus:ring-blue-500",
"text-muted",
"hover:text-text",
"hover:bg-panel"
],
attrs: {
type: "button",
"aria-haspopup": "true",
"aria-expanded": "false",
"aria-label": "More options",
},
"aria-label": "More options"
}
});
button.dataset.moreDropdown = String(this.index);
button.dataset.moreMenuToggleBound = "true";
@@ -704,8 +713,8 @@ export class VideoCard {
attrs: {
hidden: "",
"data-state": "closed",
"aria-hidden": "true",
},
"aria-hidden": "true"
}
});
dropdown.id = `moreDropdown-${this.index}`;
dropdown.dataset.moreMenu = "true";
@@ -713,21 +722,18 @@ export class VideoCard {
const list = this.createElement("div", {
classNames: ["menu"],
attrs: { role: "none" },
attrs: { role: "none" }
});
const addActionButton = (text, action, extraDataset = {}, options = {}) => {
const { variant = null } = options || {};
const btn = this.createElement("button", {
classNames: [
"menu__item",
"justify-start",
],
classNames: ["menu__item", "justify-start"],
textContent: text,
attrs: {
type: "button",
role: "menuitem",
},
role: "menuitem"
}
});
if (variant) {
btn.dataset.variant = variant;
@@ -744,11 +750,11 @@ export class VideoCard {
};
addActionButton("Open channel", "open-channel", {
author: this.video.pubkey || "",
author: this.video.pubkey || ""
});
addActionButton("Copy link", "copy-link", {
eventId: this.video.id || "",
eventId: this.video.id || ""
});
const pointer = Array.isArray(this.pointerInfo?.pointer)
@@ -764,7 +770,7 @@ export class VideoCard {
const baseBoostDataset = {
eventId: this.video.id || "",
author: this.video.pubkey || "",
author: this.video.pubkey || ""
};
if (pointerType && pointerValue) {
@@ -780,50 +786,41 @@ export class VideoCard {
const boostLabel = this.createElement("div", {
classNames: ["menu__heading"],
textContent: "Boost on Nostr…",
textContent: "Boost on Nostr…"
});
list.appendChild(boostLabel);
addActionButton(
"Repost (kind 6)",
"repost-event",
baseBoostDataset,
);
addActionButton("Repost (kind 6)", "repost-event", baseBoostDataset);
if (this.playbackUrl && this.video.isPrivate !== true) {
const mirrorDataset = {
...baseBoostDataset,
url: this.playbackUrl,
magnet: this.playbackMagnet || "",
thumbnail: typeof this.video.thumbnail === "string" ? this.video.thumbnail : "",
thumbnail:
typeof this.video.thumbnail === "string" ? this.video.thumbnail : "",
description:
typeof this.video.description === "string" ? this.video.description : "",
typeof this.video.description === "string"
? this.video.description
: "",
title: typeof this.video.title === "string" ? this.video.title : "",
isPrivate: this.video.isPrivate === true ? "true" : "false",
isPrivate: this.video.isPrivate === true ? "true" : "false"
};
addActionButton(
"Mirror (kind 1063)",
"mirror-video",
mirrorDataset,
);
addActionButton("Mirror (kind 1063)", "mirror-video", mirrorDataset);
}
const ensureDataset = {
...baseBoostDataset,
pubkey: this.video.pubkey || "",
pubkey: this.video.pubkey || ""
};
addActionButton(
"Rebroadcast",
"ensure-presence",
ensureDataset,
);
addActionButton("Rebroadcast", "ensure-presence", ensureDataset);
list.appendChild(
this.createElement("div", {
classNames: ["menu__separator"],
attrs: { role: "separator" },
attrs: { role: "separator" }
})
);
@@ -839,8 +836,8 @@ export class VideoCard {
pointerType: historyPointerType,
pointerValue: historyPointerValue,
pointerRelay: historyPointerRelay || "",
reason: "remove-item",
},
reason: "remove-item"
}
);
removeButton.setAttribute(
"title",
@@ -869,11 +866,7 @@ export class VideoCard {
{ variant: "critical" }
);
addActionButton(
"Report",
"report",
{ eventId: this.video.id || "" },
);
addActionButton("Report", "report", { eventId: this.video.id || "" });
dropdown.appendChild(list);
wrapper.appendChild(button);
@@ -898,8 +891,8 @@ export class VideoCard {
"py-1",
"rounded",
"transition-colors",
"duration-200",
],
"duration-200"
]
});
badge.setAttribute("aria-live", "polite");
badge.setAttribute("role", "status");
@@ -918,12 +911,15 @@ export class VideoCard {
"py-1",
"rounded",
"transition-colors",
"duration-200",
],
"duration-200"
]
});
badge.setAttribute("aria-live", "polite");
badge.setAttribute("role", "status");
this.applyStreamBadgeVisualState(badge, this.getCachedStreamHealthEntry());
this.applyStreamBadgeVisualState(
badge,
this.getCachedStreamHealthEntry()
);
this.torrentHealthBadgeEl = badge;
pieces.push(badge);
}
@@ -933,7 +929,7 @@ export class VideoCard {
}
const container = this.createElement("div", {
classNames: ["mt-3", "flex", "flex-wrap", "items-center", "gap-2"],
classNames: ["mt-3", "flex", "flex-wrap", "items-center", "gap-2"]
});
pieces.forEach((el) => container.appendChild(el));
return container;
@@ -1054,7 +1050,9 @@ export class VideoCard {
}
const status =
typeof entry?.status === "string" && entry.status ? entry.status : "checking";
typeof entry?.status === "string" && entry.status
? entry.status
: "checking";
const message =
typeof entry?.message === "string" && entry.message
? entry.message
@@ -1069,7 +1067,7 @@ export class VideoCard {
"py-1",
"rounded",
"transition-colors",
"duration-200",
"duration-200"
];
if (hadMargin) {
baseClasses.unshift("mt-3");
@@ -1082,13 +1080,13 @@ export class VideoCard {
};
if (status === "healthy") {
addClasses([...common, "bg-green-900", "text-green-200"]);
addClasses([...common, "bg-info/20", "text-info-strong"]);
} else if (status === "offline") {
addClasses([...common, "bg-red-900", "text-red-200"]);
addClasses([...common, "bg-critical/20", "text-critical"]);
} else if (status === "unknown" || status === "timeout") {
addClasses([...common, "bg-amber-900", "text-amber-200"]);
addClasses([...common, "bg-warning-surface", "text-warning-strong"]);
} else {
addClasses([...common, "bg-gray-800", "text-gray-300"]);
addClasses([...common, "bg-panel", "text-muted"]);
}
badge.dataset.urlHealthState = status;
@@ -1103,21 +1101,28 @@ export class VideoCard {
}
const state =
typeof entry?.state === "string" && entry.state ? entry.state : "checking";
typeof entry?.state === "string" && entry.state
? entry.state
: "checking";
const peersValue = Number.isFinite(entry?.peers)
? Math.max(0, Number(entry.peers))
: null;
const reason = typeof entry?.reason === "string" && entry.reason ? entry.reason : null;
const text = typeof entry?.text === "string" && entry.text ? entry.text : null;
const reason =
typeof entry?.reason === "string" && entry.reason ? entry.reason : null;
const text =
typeof entry?.text === "string" && entry.text ? entry.text : null;
const tooltip =
typeof entry?.tooltip === "string" && entry.tooltip ? entry.tooltip : null;
const role = entry?.role === "alert" || entry?.role === "status" ? entry.role : null;
typeof entry?.tooltip === "string" && entry.tooltip
? entry.tooltip
: null;
const role =
entry?.role === "alert" || entry?.role === "status" ? entry.role : null;
const ariaLive =
entry?.ariaLive === "assertive" || entry?.ariaLive === "polite"
? entry.ariaLive
: role === "alert"
? "assertive"
: "polite";
? "assertive"
: "polite";
const hadMargin = badge.classList.contains("mt-3");
const baseClasses = [
@@ -1128,7 +1133,7 @@ export class VideoCard {
"py-1",
"rounded",
"transition-colors",
"duration-200",
"duration-200"
];
if (hadMargin) {
baseClasses.unshift("mt-3");
@@ -1141,34 +1146,35 @@ export class VideoCard {
};
if (state === "healthy") {
addClasses([...common, "bg-green-900", "text-green-200"]);
addClasses([...common, "bg-info/20", "text-info-strong"]);
} else if (state === "unhealthy") {
addClasses([...common, "bg-red-900", "text-red-200"]);
addClasses([...common, "bg-critical/20", "text-critical"]);
} else {
addClasses([...common, "bg-gray-800", "text-gray-300"]);
addClasses([...common, "bg-panel", "text-muted"]);
}
const map = {
healthy: {
icon: "🟢",
aria: "WebTorrent peers available",
aria: "WebTorrent peers available"
},
unhealthy: {
icon: "🔴",
aria: "WebTorrent peers unavailable",
aria: "WebTorrent peers unavailable"
},
checking: {
icon: "⏳",
aria: "Checking WebTorrent peers",
aria: "Checking WebTorrent peers"
},
unknown: {
icon: "⚪",
aria: "WebTorrent status unknown",
},
aria: "WebTorrent status unknown"
}
};
const descriptor = map[state] || map.unknown;
const peersText = state === "healthy" && peersValue > 0 ? ` (${peersValue})` : "";
const peersText =
state === "healthy" && peersValue > 0 ? ` (${peersValue})` : "";
const iconPrefix = descriptor.icon ? `${descriptor.icon} ` : "";
const computedText = `${iconPrefix}WebTorrent${peersText}`;
badge.textContent = text || computedText;
@@ -1179,14 +1185,19 @@ export class VideoCard {
? descriptor.aria
: this.buildTorrentTooltip({
peers: peersValue,
checkedAt: Number.isFinite(entry?.checkedAt) ? entry.checkedAt : null,
reason,
checkedAt: Number.isFinite(entry?.checkedAt)
? entry.checkedAt
: null,
reason
}));
badge.setAttribute("aria-label", tooltipValue);
badge.setAttribute("title", tooltipValue);
badge.setAttribute("aria-live", ariaLive);
badge.setAttribute("role", role || (state === "unhealthy" ? "alert" : "status"));
badge.setAttribute(
"role",
role || (state === "unhealthy" ? "alert" : "status")
);
badge.dataset.streamHealthState = state;
if (reason) {
@@ -1234,19 +1245,25 @@ export class VideoCard {
: safeCount.toLocaleString();
const container = this.createElement("div", {
classNames: ["flex", "items-center", "text-xs", "text-gray-500", "mt-3"],
classNames: [
"flex",
"items-center",
"text-xs",
"text-muted-strong",
"mt-3"
]
});
container.dataset.discussionCount = this.video.id;
container.dataset.countState = "ready";
const valueEl = this.createElement("span", {
textContent: displayValue,
textContent: displayValue
});
valueEl.dataset.discussionCountValue = "";
const labelEl = this.createElement("span", {
classNames: ["ml-1"],
textContent: "notes",
textContent: "notes"
});
container.appendChild(valueEl);
@@ -1308,7 +1325,9 @@ export class VideoCard {
if (!this.root) {
return;
}
this.root.dataset.ownerIsViewer = this.capabilities.canEdit ? "true" : "false";
this.root.dataset.ownerIsViewer = this.capabilities.canEdit
? "true"
: "false";
if (this.video.pubkey) {
this.root.dataset.ownerPubkey = this.video.pubkey;
}
@@ -1339,7 +1358,8 @@ export class VideoCard {
const cachedStreamHealth = this.getCachedStreamHealthEntry();
if (this.magnetProvided && this.magnetSupported) {
const state =
typeof cachedStreamHealth?.state === "string" && cachedStreamHealth.state
typeof cachedStreamHealth?.state === "string" &&
cachedStreamHealth.state
? cachedStreamHealth.state
: "checking";
this.root.dataset.streamHealthState = state;
@@ -1460,7 +1480,8 @@ export class VideoCard {
}
const isMouseEvent =
typeof MouseEventCtor !== "undefined" && event instanceof MouseEventCtor;
typeof MouseEventCtor !== "undefined" &&
event instanceof MouseEventCtor;
if (isMouseEvent) {
const isPrimaryClick =
typeof event.button !== "number" || event.button === 0;
@@ -1507,7 +1528,7 @@ export class VideoCard {
event,
video: this.video,
index: this.index,
card: this,
card: this
});
}
});
@@ -1522,7 +1543,7 @@ export class VideoCard {
event,
video: this.video,
index: this.index,
card: this,
card: this
});
}
});
@@ -1537,7 +1558,7 @@ export class VideoCard {
event,
video: this.video,
index: this.index,
card: this,
card: this
});
}
});
@@ -1572,7 +1593,7 @@ export class VideoCard {
event,
video: this.video,
card: this,
dataset,
dataset
});
}
this.closeMoreMenu();
@@ -1590,7 +1611,7 @@ export class VideoCard {
event,
video: this.video,
card: this,
pubkey: this.video.pubkey || "",
pubkey: this.video.pubkey || ""
});
}
});
+42 -44
View File
@@ -5,7 +5,7 @@ export class VideoModal {
removeTrackingScripts,
setGlobalModalState,
document: doc,
logger,
logger
} = {}) {
if (!doc) {
throw new Error("VideoModal requires a document reference.");
@@ -225,14 +225,12 @@ export class VideoModal {
this.videoTitle = playerModal.querySelector("#videoTitle") || null;
this.videoDescription =
playerModal.querySelector("#videoDescription") || null;
this.videoTimestamp =
playerModal.querySelector("#videoTimestamp") || null;
this.videoTimestamp = playerModal.querySelector("#videoTimestamp") || null;
this.videoEditedTimestamp =
playerModal.querySelector("#videoEditedTimestamp") || null;
this.videoViewCountEl =
playerModal.querySelector("#videoViewCount") || null;
this.creatorAvatar =
playerModal.querySelector("#creatorAvatar") || null;
this.creatorAvatar = playerModal.querySelector("#creatorAvatar") || null;
this.creatorName = playerModal.querySelector("#creatorName") || null;
this.creatorNpub = playerModal.querySelector("#creatorNpub") || null;
this.copyMagnetBtn = playerModal.querySelector("#copyMagnetBtn") || null;
@@ -242,8 +240,7 @@ export class VideoModal {
this.modalMoreMenu =
playerModal.querySelector("#moreDropdown-modal") || null;
this.modalZapDialog =
playerModal.querySelector("#modalZapDialog") || null;
this.modalZapDialog = playerModal.querySelector("#modalZapDialog") || null;
this.modalZapForm = playerModal.querySelector("#modalZapForm") || null;
this.modalZapAmountInput =
playerModal.querySelector("#modalZapAmountInput") || null;
@@ -321,7 +318,7 @@ export class VideoModal {
document: this.document,
onRequestClose: () => {
this.dispatch("modal:close", { video: this.activeVideo });
},
}
});
this.bindVideoEvents();
@@ -345,13 +342,13 @@ export class VideoModal {
const loadedHandler = () => {
this.dispatch("playback:loadeddata", {
video: this.modalVideo,
active: this.activeVideo,
active: this.activeVideo
});
};
const playingHandler = () => {
this.dispatch("playback:playing", {
video: this.modalVideo,
active: this.activeVideo,
active: this.activeVideo
});
};
@@ -421,7 +418,7 @@ export class VideoModal {
this.dispatch("video:zap", {
video: this.activeVideo,
amount: this.getZapAmountValue(),
comment: this.getZapCommentValue(),
comment: this.getZapCommentValue()
});
});
}
@@ -439,7 +436,7 @@ export class VideoModal {
const amountHandler = () => {
this.dispatch("zap:amount-change", {
video: this.activeVideo,
amount: this.getZapAmountValue(),
amount: this.getZapAmountValue()
});
};
this.modalZapAmountInput.addEventListener("input", amountHandler);
@@ -450,7 +447,7 @@ export class VideoModal {
const commentHandler = () => {
this.dispatch("zap:comment-change", {
video: this.activeVideo,
comment: this.getZapCommentValue(),
comment: this.getZapCommentValue()
});
};
this.modalZapCommentInput.addEventListener("input", commentHandler);
@@ -458,7 +455,10 @@ export class VideoModal {
if (this.creatorAvatar) {
this.creatorAvatar.style.cursor = "pointer";
this.creatorAvatar.addEventListener("click", this.handleCreatorNavigation);
this.creatorAvatar.addEventListener(
"click",
this.handleCreatorNavigation
);
}
if (this.creatorName) {
this.creatorName.style.cursor = "pointer";
@@ -628,10 +628,7 @@ export class VideoModal {
return;
}
this.copyMagnetBtn.disabled = !enabled;
this.copyMagnetBtn.setAttribute(
"aria-disabled",
(!enabled).toString()
);
this.copyMagnetBtn.setAttribute("aria-disabled", (!enabled).toString());
this.copyMagnetBtn.classList.toggle("opacity-50", !enabled);
this.copyMagnetBtn.classList.toggle("cursor-not-allowed", !enabled);
}
@@ -641,10 +638,7 @@ export class VideoModal {
return;
}
this.shareBtn.disabled = !enabled;
this.shareBtn.setAttribute(
"aria-disabled",
(!enabled).toString()
);
this.shareBtn.setAttribute("aria-disabled", (!enabled).toString());
this.shareBtn.classList.toggle("opacity-50", !enabled);
this.shareBtn.classList.toggle("cursor-not-allowed", !enabled);
}
@@ -789,7 +783,8 @@ export class VideoModal {
return;
}
const message = typeof text === "string" ? text : "";
this.modalZapSplitSummary.textContent = message || "Enter an amount to view the split.";
this.modalZapSplitSummary.textContent =
message || "Enter an amount to view the split.";
}
setZapStatus(message, tone = "neutral") {
@@ -801,26 +796,26 @@ export class VideoModal {
const text = typeof message === "string" ? message : "";
this.modalZapStatusEl.textContent = text;
this.modalZapStatusEl.classList.remove(
"text-gray-300",
"text-gray-400",
"text-green-300",
"text-red-300",
"text-yellow-300"
"text-text",
"text-muted",
"text-info",
"text-critical",
"text-warning-strong"
);
if (!text) {
this.modalZapStatusEl.classList.add("text-gray-400");
this.modalZapStatusEl.classList.add("text-muted");
return;
}
if (normalizedTone === "success") {
this.modalZapStatusEl.classList.add("text-green-300");
this.modalZapStatusEl.classList.add("text-info");
} else if (normalizedTone === "error") {
this.modalZapStatusEl.classList.add("text-red-300");
this.modalZapStatusEl.classList.add("text-critical");
} else if (normalizedTone === "warning") {
this.modalZapStatusEl.classList.add("text-yellow-300");
this.modalZapStatusEl.classList.add("text-warning-strong");
} else {
this.modalZapStatusEl.classList.add("text-gray-300");
this.modalZapStatusEl.classList.add("text-text");
}
}
@@ -843,7 +838,7 @@ export class VideoModal {
if (!Array.isArray(receipts) || receipts.length === 0) {
if (partial) {
const empty = this.document.createElement("li");
empty.className = "text-sm text-gray-300";
empty.className = "text-sm text-text";
empty.textContent = "No zap receipts available.";
this.modalZapReceipts.appendChild(empty);
}
@@ -852,10 +847,11 @@ export class VideoModal {
receipts.forEach((receipt) => {
const li = this.document.createElement("li");
li.className = "rounded border border-gray-700 p-3 bg-gray-800/70";
li.className = "rounded border border-border p-3 bg-panel/70";
const header = this.document.createElement("div");
header.className = "flex items-center justify-between gap-2 text-xs text-gray-300";
header.className =
"flex items-center justify-between gap-2 text-xs text-text";
const shareType = receipt.recipientType || receipt.type || "creator";
const shareLabel = this.document.createElement("span");
@@ -863,8 +859,8 @@ export class VideoModal {
shareType === "platform"
? "Platform"
: shareType === "creator"
? "Creator"
: "Lightning";
? "Creator"
: "Lightning";
shareLabel.textContent = `${label} • ${Math.max(
0,
Math.round(Number(receipt.amount || 0))
@@ -875,21 +871,21 @@ export class VideoModal {
? receipt.status === "success"
: !receipt.error;
status.textContent = isSuccess ? "Success" : "Failed";
status.className = isSuccess ? "text-green-300" : "text-red-300";
status.className = isSuccess ? "text-info" : "text-critical";
header.appendChild(shareLabel);
header.appendChild(status);
li.appendChild(header);
const address = this.document.createElement("p");
address.className = "mt-1 text-xs text-gray-300 break-all";
address.className = "mt-1 text-xs text-text break-all";
if (receipt.address) {
address.textContent = receipt.address;
li.appendChild(address);
}
const detail = this.document.createElement("p");
detail.className = "mt-2 text-xs text-gray-400";
detail.className = "mt-2 text-xs text-muted";
if (isSuccess) {
let detailMessage = "Invoice settled.";
const preimage = receipt.payment?.result?.preimage;
@@ -1054,7 +1050,7 @@ export class VideoModal {
timestamp,
timestamps,
viewCount,
creator,
creator
} = {}) {
if (this.videoTitle && title !== undefined) {
this.videoTitle.textContent = title || "Untitled";
@@ -1161,7 +1157,8 @@ export class VideoModal {
Array.isArray(currentVideo?.pointer) &&
currentVideo.pointer.length >= 2
) {
const [pointerType, pointerValue, pointerRelay] = currentVideo.pointer;
const [pointerType, pointerValue, pointerRelay] =
currentVideo.pointer;
button.dataset.pointerType = pointerType || "";
button.dataset.pointerValue = pointerValue || "";
if (pointerRelay) {
@@ -1234,7 +1231,8 @@ export class VideoModal {
Array.isArray(currentVideo?.pointer) &&
currentVideo.pointer.length >= 2
) {
const [pointerType, pointerValue, pointerRelay] = currentVideo.pointer;
const [pointerType, pointerValue, pointerRelay] =
currentVideo.pointer;
button.dataset.pointerType = pointerType || "";
button.dataset.pointerValue = pointerValue || "";
if (pointerRelay) {
+4 -4
View File
@@ -4,15 +4,15 @@
This container uses a white background and dark text,
overriding your global “body { color: … }” rule
-->
<div class="bg-white p-6 rounded-lg shadow-md text-gray-900">
<h2 class="text-2xl font-bold mb-4 text-gray-700">About bitvid</h2>
<div class="rounded-lg bg-page p-6 text-text-strong shadow-md">
<h2 class="mb-4 text-2xl font-bold text-text-strong">About bitvid</h2>
<!--
The .markdown-body class ensures the Markdown styling
from your markdown.css can be applied here.
-->
<div id="markdown-container" class="markdown-body">
<p class="text-gray-500">Loading content...</p>
<p class="text-muted-strong">Loading content...</p>
</div>
</div>
</section>
@@ -44,7 +44,7 @@
const container = document.getElementById("markdown-container");
if (container) {
container.innerHTML = `
<p class="text-red-500">Error loading content. Please try again later.</p>
<p class="text-critical-strong">Error loading content. Please try again later.</p>
`;
}
}
+3 -3
View File
@@ -1,13 +1,13 @@
<!-- views/blog.html -->
<section class="mb-8">
<div class="bg-white p-6 rounded-lg shadow-md text-gray-900">
<h2 class="text-2xl font-bold mb-4 text-gray-700">Latest Blog Posts</h2>
<div class="rounded-lg bg-page p-6 text-text-strong shadow-md">
<h2 class="mb-4 text-2xl font-bold text-text-strong">Latest Blog Posts</h2>
<div class="relative overflow-hidden" style="height: 1600px">
<iframe
id="blog-iframe"
src="https://beta.bitvid.network/blog.html"
class="w-full h-full border-none"
class="h-full w-full border-none"
></iframe>
</div>
</div>
+18 -13
View File
@@ -4,25 +4,27 @@
<!-- Increase the default height classes -->
<img
id="channelBanner"
class="w-full h-48 md:h-64 object-cover rounded-lg"
class="h-48 w-full rounded-lg object-cover md:h-64"
src=""
alt="Banner"
/>
<div class="absolute inset-0 bg-black bg-opacity-50 rounded-lg"></div>
<div class="absolute inset-0 rounded-lg bg-black bg-opacity-50"></div>
<div
class="absolute bottom-4 left-4 right-4 flex flex-col gap-3 sm:flex-row sm:items-end sm:justify-between"
>
<div class="flex items-center">
<div class="w-16 h-16 rounded-full overflow-hidden border-2 border-white">
<div
class="h-16 w-16 overflow-hidden rounded-full border-2 border-border-translucent-bright"
>
<img
id="channelAvatar"
src="assets/svg/default-profile.svg"
alt="Avatar"
class="w-full h-full object-cover"
class="h-full w-full object-cover"
/>
</div>
<div class="ml-4 text-white">
<div class="ml-4 text-text">
<h2 id="channelName" class="text-xl font-bold">User Name</h2>
<p id="channelNpub" class="text-sm opacity-80">npub...</p>
</div>
@@ -120,7 +122,10 @@
role="status"
aria-live="polite"
></p>
<ul id="zapReceipts" class="space-y-2 text-xs text-muted"></ul>
<ul
id="zapReceipts"
class="space-y-2 text-xs text-muted"
></ul>
<div class="flex items-center justify-end gap-2">
<button
type="submit"
@@ -157,7 +162,7 @@
aria-label="Channel options"
data-more-dropdown="channel-profile"
>
<img src="assets/svg/ellipsis.svg" alt="More" class="w-5 h-5" />
<img src="assets/svg/ellipsis.svg" alt="More" class="h-5 w-5" />
</button>
<div
id="moreDropdown-channel-profile"
@@ -217,26 +222,26 @@
</div>
<!-- Basic profile details: about text, website, LN address -->
<div class="profile-details px-4 mt-4">
<p id="channelAbout" class="mb-2 text-gray-700"></p>
<div class="profile-details mt-4 px-4">
<p id="channelAbout" class="mb-2 text-text-strong"></p>
<a
id="channelWebsite"
href="#"
target="_blank"
class="block text-blue-500 hover:underline mb-2"
class="mb-2 block text-info-strong hover:underline"
>Website</a
>
<p id="channelLightning" class="text-sm text-gray-500"></p>
<p id="channelLightning" class="text-sm text-muted-strong"></p>
</div>
<!-- Area for Subscribe/Unsubscribe button -->
<div class="px-4 my-4">
<div class="my-4 px-4">
<div id="subscribeBtnArea" class="hidden"></div>
</div>
<hr class="my-6" />
<h3 class="text-lg font-bold mb-4 text-gray-700">Videos by This User</h3>
<h3 class="mb-4 text-lg font-bold text-text-strong">Videos by This User</h3>
<div id="channelVideoList" class="bv-grid-video">
<!-- User’s videos will go here -->
</div>
+9 -7
View File
@@ -1,18 +1,20 @@
<!-- views/community-guidelines.html -->
<section class="mb-8">
<!--
This card has a white background and shadow, similar to your other partials.
We also add text-gray-900 to override the global body text color.
<!--
This card has a light background and shadow, similar to your other partials.
We use text-text-strong to override the global body text color.
-->
<div class="bg-white p-6 rounded-lg shadow-md text-gray-900">
<h2 class="text-2xl font-bold mb-4 text-gray-700">Community Guidelines</h2>
<div class="rounded-lg bg-page p-6 text-text-strong shadow-md">
<h2 class="mb-4 text-2xl font-bold text-text-strong">
Community Guidelines
</h2>
<!--
The .markdown-body class can use your markdown.css
so headings, paragraphs, etc. are styled nicely.
-->
<div id="markdown-container" class="markdown-body">
<p class="text-gray-500">Loading content...</p>
<p class="text-muted-strong">Loading content...</p>
</div>
</div>
</section>
@@ -45,7 +47,7 @@
const container = document.getElementById("markdown-container");
if (container) {
container.innerHTML = `
<p class="text-red-500">Error loading content. Please try again later.</p>
<p class="text-critical-strong">Error loading content. Please try again later.</p>
`;
}
}
+2 -2
View File
@@ -1,5 +1,5 @@
<!-- views/explore.html -->
<section>
<h2 class="text-xl mb-4 text-gray-700">Explore</h2>
<p class="text-gray-600">Coming Soon...</p>
<h2 class="mb-4 text-xl text-text-strong">Explore</h2>
<p class="text-muted-strong">Coming Soon...</p>
</section>
+5 -5
View File
@@ -1,12 +1,12 @@
<!-- views/getting-started.html -->
<section class="mb-8">
<!-- White background, shadow, and dark text to override global site color -->
<div class="bg-white p-6 rounded-lg shadow-md text-gray-900">
<h2 class="text-2xl font-bold mb-4 text-gray-700">Getting Started</h2>
<!-- Light background, shadow, and dark text to override global site color -->
<div class="rounded-lg bg-page p-6 text-text-strong shadow-md">
<h2 class="mb-4 text-2xl font-bold text-text-strong">Getting Started</h2>
<!-- .markdown-body references your markdown.css to style headings, paragraphs, etc. -->
<div id="markdown-container" class="markdown-body">
<p class="text-gray-500">Loading content...</p>
<p class="text-muted-strong">Loading content...</p>
</div>
</div>
</section>
@@ -40,7 +40,7 @@
const container = document.getElementById("markdown-container");
if (container) {
container.innerHTML = `
<p class="text-red-500">Error loading content. Please try again later.</p>
<p class="text-critical-strong">Error loading content. Please try again later.</p>
`;
}
}
+21 -19
View File
@@ -5,8 +5,8 @@
class="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between"
>
<div>
<h2 class="text-xl font-semibold text-gray-100">Watch History</h2>
<p class="text-sm text-gray-400">
<h2 class="text-xl font-semibold text-text">Watch History</h2>
<p class="text-sm text-muted">
Review the videos you recently played, grouped by day.
</p>
</div>
@@ -14,14 +14,14 @@
<button
type="button"
data-history-action="clear-cache"
class="rounded-full bg-gray-800 px-4 py-2 text-sm font-medium text-gray-100 hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-blue-500"
class="focus-ring rounded-full bg-panel px-4 py-2 text-sm font-medium text-text hover:bg-panel-hover"
>
Clear local cache
</button>
<button
type="button"
data-history-action="republish"
class="rounded-full bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-300"
class="focus-ring rounded-full bg-info px-4 py-2 text-sm font-medium text-text hover:bg-info-strong focus-visible:bg-info-pressed"
>
Republish now
</button>
@@ -30,12 +30,14 @@
<div
id="watchHistoryPrivacyBanner"
class="hidden rounded-lg border border-blue-500/40 bg-blue-900/30 p-4"
class="border-info/40 bg-info/10 hidden rounded-lg border p-4"
role="status"
aria-live="polite"
>
<div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<p id="watchHistoryPrivacyMessage" class="text-sm text-blue-100">
<div
class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between"
>
<p id="watchHistoryPrivacyMessage" class="text-sm text-info">
Your history stays encrypted. Share metadata with relays to sync
thumbnails across devices?
</p>
@@ -43,14 +45,14 @@
<button
id="watchHistoryPrivacyToggle"
type="button"
class="rounded-full bg-blue-500 px-4 py-2 text-sm font-medium text-white hover:bg-blue-400 focus:outline-none focus:ring-2 focus:ring-blue-200"
class="focus-ring rounded-full bg-info px-4 py-2 text-sm font-medium text-text hover:bg-info-strong focus-visible:bg-info-pressed"
>
Share metadata
</button>
<button
id="watchHistoryPrivacyDismiss"
type="button"
class="rounded-full bg-transparent px-3 py-2 text-sm font-medium text-blue-200 hover:bg-blue-800/40 focus:outline-none focus:ring-2 focus:ring-blue-300"
class="focus-ring hover:bg-info/10 rounded-full bg-transparent px-3 py-2 text-sm font-medium text-info"
>
Dismiss
</button>
@@ -60,14 +62,14 @@
<div
id="watchHistoryInfo"
class="rounded-lg bg-gray-800/60 p-4 text-sm text-gray-200"
class="bg-panel/60 rounded-lg p-4 text-sm text-text"
>
Thumbnails and titles are hydrated locally from your device caches.
</div>
<div
id="watchHistoryError"
class="hidden rounded-lg border border-red-500/50 bg-red-900/40 p-3 text-sm text-red-100"
class="border-critical/50 bg-critical/40 hidden rounded-lg border p-3 text-sm text-critical"
role="alert"
></div>
</div>
@@ -88,20 +90,20 @@
</div>
</div>
<div
id="watchHistoryGrid"
class="grid-stack hidden"
data-variant="cards"
data-orientation="vertical"
></div>
<p id="watchHistoryEmpty" class="hidden text-gray-400 text-center py-12">
<div
id="watchHistoryGrid"
class="grid-stack hidden"
data-variant="cards"
data-orientation="vertical"
></div>
<p id="watchHistoryEmpty" class="hidden py-12 text-center text-muted">
Your watch history is empty. Watch some videos to populate this list.
</p>
<button
id="watchHistoryLoadMore"
type="button"
class="hidden w-full rounded-full bg-gray-800 px-4 py-2 text-sm font-medium text-gray-100 hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-blue-500"
class="focus-ring hidden w-full rounded-full bg-panel px-4 py-2 text-sm font-medium text-text hover:bg-panel-hover"
>
Load more
</button>
+7 -7
View File
@@ -1,18 +1,18 @@
<!-- views/ipns.html -->
<section class="mb-8">
<!--
Added "text-gray-900" here to ensure we override
any global white text color that might be inherited.
<!--
Added text-text-strong here to ensure we override
any global white text color that might be inherited.
-->
<div class="bg-white p-6 rounded-lg shadow-md text-gray-900">
<h2 class="text-2xl font-bold mb-4 text-gray-700">IPNS Gateways</h2>
<div class="rounded-lg bg-page p-6 text-text-strong shadow-md">
<h2 class="mb-4 text-2xl font-bold text-text-strong">IPNS Gateways</h2>
<!--
"markdown-body" can be any class name; it’s just so markdown.css
can style headings, lists, etc.
-->
<div id="markdown-container" class="markdown-body">
<p class="text-gray-500">Loading content...</p>
<p class="text-muted-strong">Loading content...</p>
</div>
</div>
</section>
@@ -44,7 +44,7 @@
const container = document.getElementById("markdown-container");
if (container) {
container.innerHTML = `
<p class="text-red-500">Error loading content. Please try again later.</p>
<p class="text-critical-strong">Error loading content. Please try again later.</p>
`;
}
}
+4 -4
View File
@@ -4,15 +4,15 @@
This container uses a white background and dark text,
overriding your global “body { color: … }” rule
-->
<div class="bg-white p-6 rounded-lg shadow-md text-gray-900">
<h2 class="text-2xl font-bold mb-4 text-gray-700">bitvid Links</h2>
<div class="rounded-lg bg-page p-6 text-text-strong shadow-md">
<h2 class="mb-4 text-2xl font-bold text-text-strong">bitvid Links</h2>
<!--
The .markdown-body class ensures the Markdown styling
from your markdown.css can be applied here.
-->
<div id="markdown-container" class="markdown-body">
<p class="text-gray-500">Loading content...</p>
<p class="text-muted-strong">Loading content...</p>
</div>
</div>
</section>
@@ -44,7 +44,7 @@
const container = document.getElementById("markdown-container");
if (container) {
container.innerHTML = `
<p class="text-red-500">Error loading content. Please try again later.</p>
<p class="text-critical-strong">Error loading content. Please try again later.</p>
`;
}
}
+5 -2
View File
@@ -1,13 +1,16 @@
<!-- views/most-recent-videos.html -->
<section>
<!-- You can have a heading if you want -->
<h2 class="text-xl mb-4 text-gray-700">Most Recent Videos</h2>
<h2 class="mb-4 text-xl text-text-strong">Most Recent Videos</h2>
<!-- Video List -->
<div id="videoList" class="bv-grid-video">
<div class="sidebar-loading-wrapper" role="status" aria-live="polite">
<div class="sidebar-loading-indicator">
<span class="status-spinner status-spinner--inline" aria-hidden="true"></span>
<span
class="status-spinner status-spinner--inline"
aria-hidden="true"
></span>
<span class="sidebar-loading-text">Fetching recent videos…</span>
</div>
</div>
+4 -4
View File
@@ -1,12 +1,12 @@
<!-- views/roadmap.html -->
<section class="mb-8">
<!-- White card, dark text to override global site color -->
<div class="bg-white p-6 rounded-lg shadow-md text-gray-900">
<h2 class="text-2xl font-bold mb-4 text-gray-700">Roadmap</h2>
<div class="rounded-lg bg-page p-6 text-text-strong shadow-md">
<h2 class="mb-4 text-2xl font-bold text-text-strong">Roadmap</h2>
<!-- .markdown-body uses your markdown.css rules for headings, etc. -->
<div id="markdown-container" class="markdown-body">
<p class="text-gray-500">Loading content...</p>
<p class="text-muted-strong">Loading content...</p>
</div>
</div>
</section>
@@ -39,7 +39,7 @@
const container = document.getElementById("markdown-container");
if (container) {
container.innerHTML = `
<p class="text-red-500">Error loading content. Please try again later.</p>
<p class="text-critical-strong">Error loading content. Please try again later.</p>
`;
}
}
+5 -2
View File
@@ -1,10 +1,13 @@
<!-- views/subscriptions.html -->
<section>
<h2 class="text-xl mb-4">Subscriptions</h2>
<h2 class="mb-4 text-xl">Subscriptions</h2>
<div id="subscriptionsVideoList" class="bv-grid-video">
<div class="sidebar-loading-wrapper" role="status" aria-live="polite">
<div class="sidebar-loading-indicator">
<span class="status-spinner status-spinner--inline" aria-hidden="true"></span>
<span
class="status-spinner status-spinner--inline"
aria-hidden="true"
></span>
<span class="sidebar-loading-text">Fetching subscriptions…</span>
</div>
</div>