mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 07:52:24 +00:00
fix(beacon): stop the processing spinner hanging forever (#28)
The torrent beacon's processing overlay was gated on WebTorrent's metadata-ready
callback (client.add/seed), which only fires once metadata arrives from peers. A
magnet with no live seeders never resolved it, so the spinner span forever, and a
torrent that emitted its own 'error' didn't always reach the client-level handler.
- beginProcessing() arms a 30s watchdog that drops the overlay and warns ("still
searching for peers — it'll keep trying in the background") instead of hanging;
the torrent stays in the table and keeps announcing.
- handleTorrentReady, the client error handler, and destroy() clear the watchdog.
- addMagnet/seedFiles wrap add/seed in try/catch and attach a per-torrent 'error'
listener so bad/duplicate magnets fail visibly.
- Tests: tests/torrent/beacon-watchdog.test.mjs (JSDOM).
Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
1 parent
1c256808ff
commit
fdcf034464
4 files changed
+222
-15
No files matched your search
@@ -0,0 +1,123 @@
|
||||
import { strict as assert } from "node:assert";
|
||||
import { afterEach, beforeEach, describe, it } from "node:test";
|
||||
import { JSDOM } from "jsdom";
|
||||
|
||||
import { createBeaconApp } from "../../torrent/app.js";
|
||||
|
||||
// The processing overlay is the only element these scenarios assert on; every
|
||||
// other element createBeaconApp queries is optional and guarded.
|
||||
const BODY = '<div data-beacon="processing-overlay" hidden></div>';
|
||||
const MAGNET = `magnet:?xt=urn:btih:${"a".repeat(40)}`;
|
||||
|
||||
function makeFakeTorrent() {
|
||||
return { infoHash: "a".repeat(40), name: "fake", progress: 0, files: [], once() {} };
|
||||
}
|
||||
|
||||
// A WebTorrent stub whose add() never invokes the ready callback — exactly what
|
||||
// a magnet with no live seeders does, which used to spin the overlay forever.
|
||||
class StuckWebTorrent {
|
||||
constructor() {
|
||||
this.torrents = [];
|
||||
this.downloadSpeed = 0;
|
||||
this.uploadSpeed = 0;
|
||||
this.ratio = 0;
|
||||
}
|
||||
on() {}
|
||||
removeListener() {}
|
||||
add() {
|
||||
const torrent = makeFakeTorrent();
|
||||
this.torrents.push(torrent);
|
||||
return torrent; // intentionally never calls the ready callback
|
||||
}
|
||||
seed() {
|
||||
const torrent = makeFakeTorrent();
|
||||
this.torrents.push(torrent);
|
||||
return torrent;
|
||||
}
|
||||
destroy() {}
|
||||
}
|
||||
StuckWebTorrent.WEBRTC_SUPPORT = true;
|
||||
|
||||
// A WebTorrent stub whose add() resolves immediately (metadata arrives).
|
||||
class ReadyWebTorrent extends StuckWebTorrent {
|
||||
add(magnet, options, cb) {
|
||||
const torrent = makeFakeTorrent();
|
||||
this.torrents.push(torrent);
|
||||
if (typeof cb === "function") cb(torrent);
|
||||
return torrent;
|
||||
}
|
||||
}
|
||||
ReadyWebTorrent.WEBRTC_SUPPORT = true;
|
||||
|
||||
let dom;
|
||||
let firedTimeouts;
|
||||
|
||||
function setupDom() {
|
||||
dom = new JSDOM(`<!doctype html><html><body>${BODY}</body></html>`, {
|
||||
url: `https://beacon.test/#${MAGNET}`,
|
||||
pretendToBeVisual: true,
|
||||
});
|
||||
const view = dom.window;
|
||||
view.requestAnimationFrame = (cb) => cb();
|
||||
// Capture the watchdog timeout so the scenario controls when it "fires",
|
||||
// and neutralise the 1s render interval so no real timers run.
|
||||
firedTimeouts = [];
|
||||
view.setTimeout = (fn) => {
|
||||
firedTimeouts.push(fn);
|
||||
return firedTimeouts.length;
|
||||
};
|
||||
view.clearTimeout = () => {};
|
||||
view.setInterval = () => 0;
|
||||
view.clearInterval = () => {};
|
||||
return view.document;
|
||||
}
|
||||
|
||||
function overlay(documentRef) {
|
||||
return documentRef.querySelector('[data-beacon="processing-overlay"]');
|
||||
}
|
||||
|
||||
describe("torrent/app beacon processing watchdog", () => {
|
||||
beforeEach(() => {});
|
||||
|
||||
afterEach(() => {
|
||||
if (dom) {
|
||||
dom.window.close();
|
||||
dom = null;
|
||||
}
|
||||
});
|
||||
|
||||
it("drops the spinner (and warns) when a magnet never finds peers", () => {
|
||||
const documentRef = setupDom();
|
||||
const app = createBeaconApp({ documentRef, WebTorrentCtor: StuckWebTorrent });
|
||||
|
||||
app.mount(); // reads location.hash → addMagnet(MAGNET) → spinner on
|
||||
|
||||
assert.equal(overlay(documentRef).hidden, false, "spinner shows while connecting");
|
||||
assert.equal(firedTimeouts.length, 1, "a watchdog timeout was armed");
|
||||
|
||||
// Simulate the timeout elapsing with the torrent still not ready.
|
||||
firedTimeouts[0]();
|
||||
|
||||
assert.equal(
|
||||
overlay(documentRef).hidden,
|
||||
true,
|
||||
"spinner is dismissed instead of hanging forever",
|
||||
);
|
||||
const toast = documentRef.querySelector(".torrent-toast--warn");
|
||||
assert.ok(toast, "a warning toast explains it's still trying in the background");
|
||||
});
|
||||
|
||||
it("clears the spinner and the watchdog as soon as metadata arrives", () => {
|
||||
const documentRef = setupDom();
|
||||
const app = createBeaconApp({ documentRef, WebTorrentCtor: ReadyWebTorrent });
|
||||
|
||||
app.mount(); // ready callback fires synchronously → handleTorrentReady
|
||||
|
||||
assert.equal(overlay(documentRef).hidden, true, "spinner resolves on success");
|
||||
// Firing a stale watchdog after success must not re-hide / re-toast.
|
||||
const before = documentRef.querySelectorAll(".torrent-toast--warn").length;
|
||||
firedTimeouts.forEach((fn) => fn());
|
||||
const after = documentRef.querySelectorAll(".torrent-toast--warn").length;
|
||||
assert.equal(after, before, "a resolved op does not fire a stale watchdog warning");
|
||||
});
|
||||
});
|
||||
@@ -516,12 +516,26 @@ SILENT one: `handleEventDetailsAction` only opened the modal `if (… && payload
|
||||
- [ ] **VERIFY live**: click ⋯ → Event Details AND ⋯ → Popularity in the video
|
||||
modal AND on a grid/channel card; both should open the correct video.
|
||||
|
||||
### 28. Beacon torrent app stuck — spinner never resolves (BUG)
|
||||
- [ ] The torrent beacon app (`scripts/build:beacon` / `torrent/` integration) shows
|
||||
a spinner that never goes away. Trace the beacon's init/connect path: what the
|
||||
spinner is waiting on (tracker/handshake/WebTorrent ready), whether it errors
|
||||
silently, and whether it's related to the vendored `webtorrent.min.js`
|
||||
`null.fill` crash (#10). Make it either resolve or fail visibly.
|
||||
### 28. Beacon torrent app stuck — spinner never resolves (BUG) — FIXED 2026-06-24
|
||||
- [x] **Root cause:** the processing overlay (`torrent/app.js`) was gated on
|
||||
WebTorrent's metadata-ready callback (`client.add(magnet, opts, cb)` /
|
||||
`client.seed`). That callback only fires once metadata arrives from peers, so
|
||||
a magnet with **no live seeders** never resolved it and the spinner span
|
||||
forever — and a torrent that emits its own `'error'` (bad/duplicate magnet)
|
||||
didn't always reach the client-level handler. (NOT the `null.fill` crash
|
||||
(#10); the client constructs fine — this was a missing timeout/failure path.)
|
||||
- [x] **Fix:** added a watchdog around the overlay. `beginProcessing()` arms a
|
||||
30s `PROCESSING_TIMEOUT_MS` timer; if the op hasn't completed it drops the
|
||||
overlay and warns ("still searching for peers — it'll keep trying in the
|
||||
background") while the torrent stays in the table and keeps announcing.
|
||||
`handleTorrentReady`, the client error handler, and `destroy()` all clear the
|
||||
watchdog. `addMagnet`/`seedFiles` now wrap `add`/`seed` in try/catch and
|
||||
attach a per-torrent `'error'` listener so bad magnets fail visibly.
|
||||
- [x] **Tests:** `tests/torrent/beacon-watchdog.test.mjs` (JSDOM) — a no-peers
|
||||
magnet drops the spinner + warns; a ready torrent resolves it and a stale
|
||||
watchdog can't re-fire.
|
||||
- [ ] **VERIFY live**: open the beacon, paste a dead magnet → spinner should clear
|
||||
with a warning after ~30s instead of hanging; paste a live magnet → resolves.
|
||||
|
||||
### 29. Admin-whitelisted users bypass the Web-of-Trust (anti-abuse)
|
||||
- [ ] When an admin **whitelists** a user, that user's content should **bypass the
|
||||
|
||||
+78
-8
@@ -12,6 +12,12 @@ const TRACKERS = [
|
||||
const TORRENT_OPTIONS = { announce: TRACKERS };
|
||||
const TRACKER_OPTIONS = { announce: TRACKERS };
|
||||
|
||||
// A magnet with no live seeders never fires WebTorrent's metadata-ready
|
||||
// callback, so the processing overlay would otherwise spin forever. After this
|
||||
// long we drop the overlay and tell the user it's still trying in the
|
||||
// background (the torrent stays in the table and keeps announcing).
|
||||
const PROCESSING_TIMEOUT_MS = 30000;
|
||||
|
||||
function formatBytes(num, speed = false) {
|
||||
if (typeof num !== "number" || Number.isNaN(num)) {
|
||||
return "";
|
||||
@@ -208,11 +214,37 @@ export function createBeaconApp({
|
||||
|
||||
const cleanupTasks = [];
|
||||
const intervalHandles = [];
|
||||
let processingTimeoutId = null;
|
||||
|
||||
function addCleanup(task) {
|
||||
cleanupTasks.push(task);
|
||||
}
|
||||
|
||||
function clearProcessingWatchdog() {
|
||||
if (processingTimeoutId !== null) {
|
||||
view.clearTimeout(processingTimeoutId);
|
||||
processingTimeoutId = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Show the processing overlay and arm a watchdog so it can never hang
|
||||
// forever: if the operation hasn't completed in time, drop the overlay and
|
||||
// warn instead of leaving a dead spinner.
|
||||
function beginProcessing() {
|
||||
setProcessing(true);
|
||||
clearProcessingWatchdog();
|
||||
processingTimeoutId = view.setTimeout(() => {
|
||||
processingTimeoutId = null;
|
||||
if (!state.processing) {
|
||||
return;
|
||||
}
|
||||
setProcessing(false);
|
||||
toast?.warn(
|
||||
"Still searching for peers — this is taking a while. It'll keep trying in the background.",
|
||||
);
|
||||
}, PROCESSING_TIMEOUT_MS);
|
||||
}
|
||||
|
||||
function setProcessing(flag) {
|
||||
state.processing = Boolean(flag);
|
||||
|
||||
@@ -359,6 +391,7 @@ export function createBeaconApp({
|
||||
}
|
||||
|
||||
function handleTorrentReady(torrent, { isSeed = false } = {}) {
|
||||
clearProcessingWatchdog();
|
||||
setProcessing(false);
|
||||
prepareTorrentFiles(torrent);
|
||||
|
||||
@@ -380,10 +413,20 @@ export function createBeaconApp({
|
||||
return;
|
||||
}
|
||||
|
||||
setProcessing(true);
|
||||
client.add(trimmed, TORRENT_OPTIONS, (torrent) => {
|
||||
handleTorrentReady(torrent, { isSeed: false });
|
||||
});
|
||||
beginProcessing();
|
||||
let torrent;
|
||||
try {
|
||||
torrent = client.add(trimmed, TORRENT_OPTIONS, (added) => {
|
||||
handleTorrentReady(added, { isSeed: false });
|
||||
});
|
||||
} catch (error) {
|
||||
clearProcessingWatchdog();
|
||||
setProcessing(false);
|
||||
userLogger.error("[beacon] Failed to add magnet", error);
|
||||
toast?.error(error?.message || "Couldn't add that magnet.");
|
||||
return;
|
||||
}
|
||||
watchTorrentForFailure(torrent);
|
||||
}
|
||||
|
||||
function seedFiles(files) {
|
||||
@@ -391,10 +434,35 @@ export function createBeaconApp({
|
||||
return;
|
||||
}
|
||||
|
||||
setProcessing(true);
|
||||
client.seed(files, TORRENT_OPTIONS, (torrent) => {
|
||||
handleTorrentReady(torrent, { isSeed: true });
|
||||
toast.success(`Seeding ${torrent.files.length} file(s)`);
|
||||
beginProcessing();
|
||||
let torrent;
|
||||
try {
|
||||
torrent = client.seed(files, TORRENT_OPTIONS, (seeded) => {
|
||||
handleTorrentReady(seeded, { isSeed: true });
|
||||
toast.success(`Seeding ${seeded.files.length} file(s)`);
|
||||
});
|
||||
} catch (error) {
|
||||
clearProcessingWatchdog();
|
||||
setProcessing(false);
|
||||
userLogger.error("[beacon] Failed to seed files", error);
|
||||
toast?.error(error?.message || "Couldn't seed those files.");
|
||||
return;
|
||||
}
|
||||
watchTorrentForFailure(torrent);
|
||||
}
|
||||
|
||||
// A torrent can emit its own 'error' (bad/duplicate magnet) without the
|
||||
// client-level handler firing; surface it and drop the overlay so the add
|
||||
// fails visibly instead of spinning.
|
||||
function watchTorrentForFailure(torrent) {
|
||||
if (!torrent || typeof torrent.once !== "function") {
|
||||
return;
|
||||
}
|
||||
torrent.once("error", (error) => {
|
||||
clearProcessingWatchdog();
|
||||
setProcessing(false);
|
||||
userLogger.error("[beacon] Torrent error", error);
|
||||
toast?.error(error?.message || "That torrent could not be loaded.");
|
||||
});
|
||||
}
|
||||
|
||||
@@ -572,6 +640,7 @@ export function createBeaconApp({
|
||||
const errorHandler = (error) => {
|
||||
userLogger.error("[beacon] Torrent client error", error);
|
||||
toast?.error(error?.message || String(error));
|
||||
clearProcessingWatchdog();
|
||||
setProcessing(false);
|
||||
};
|
||||
client.on("error", errorHandler);
|
||||
@@ -610,6 +679,7 @@ export function createBeaconApp({
|
||||
}
|
||||
|
||||
function destroy() {
|
||||
clearProcessingWatchdog();
|
||||
intervalHandles.forEach((id) => view.clearInterval(id));
|
||||
intervalHandles.length = 0;
|
||||
|
||||
|
||||
Vendored
+1
-1
@@ -1 +1 @@
|
||||
import{IS_DEV_MODE as ve,IS_VERBOSE_DEV_MODE as Ie,IS_LOCKDOWN_MODE as Oe,LOCKDOWN_TITLE as _t,LOCKDOWN_MESSAGE as St,ADMIN_SUPER_NPUB as wt,ADMIN_DM_IMAGE_URL as Lt,BITVID_WEBSITE_URL as At,BLOG_URL as Ct,COMMUNITY_URL as xt,NOSTR_URL as vt,GITHUB_URL as It,BETA_URL as Ot,DNS_URL as Bt,TIP_JAR_URL as Nt,MAX_WALLET_DEFAULT_ZAP as Mt,PLATFORM_FEE_PERCENT as Dt,PLATFORM_LUD16_OVERRIDE as Rt,DEFAULT_RELAY_URLS_OVERRIDE as Ut,ADMIN_WHITELIST_MODE_STORAGE_KEY as Ht,DEFAULT_WHITELIST_MODE_ENABLED as kt,ALLOW_NSFW_CONTENT as Ft,DEFAULT_TRUST_SEED_NPUBS as Vt,DEFAULT_BLUR_THRESHOLD as Pt,DEFAULT_AUTOPLAY_BLOCK_THRESHOLD as Wt,DEFAULT_TRUSTED_MUTE_HIDE_THRESHOLD as qt,DEFAULT_TRUSTED_SPAM_HIDE_THRESHOLD as jt,WATCH_HISTORY_KIND as $t,WATCH_HISTORY_LIST_IDENTIFIER as Yt,WATCH_HISTORY_MAX_ITEMS as Kt,WATCH_HISTORY_BATCH_RESOLVE as zt,WATCH_HISTORY_BATCH_PAGE_SIZE as Gt,WATCH_HISTORY_PAYLOAD_MAX_BYTES as Xt,WATCH_HISTORY_FETCH_EVENT_LIMIT as Zt,WATCH_HISTORY_CACHE_TTL_MS as Jt,VIEW_COUNT_DEDUPE_WINDOW_SECONDS as Qt,VIEW_COUNT_BACKFILL_MAX_DAYS as er,VIEW_COUNT_CACHE_TTL_MS as tr,ENSURE_PRESENCE_REBROADCAST_COOLDOWN_SECONDS as rr,DEFAULT_PLAYBACK_SOURCE as nr,DEFAULT_PLAYBACK_START_TIMEOUT as or}from"../config/instance-config.js";var re=typeof globalThis>"u"?null:globalThis,ne=e=>{if(typeof e=="boolean")return e;if(typeof e=="string"){let t=e.trim().toLowerCase();if(t==="true")return!0;if(t==="false")return!1}return null},ee=ne(re?.__BITVID_DEV_MODE_OVERRIDE__),te=ne(re?.__BITVID_VERBOSE_DEV_MODE_OVERRIDE__),U=ee===null?!!ve:ee;var Be=te===null?!!Ie:te;var Ne=!!Oe;typeof window<"u"&&(window.__BITVID_DEV_MODE__=U,window.__BITVID_VERBOSE_DEV_MODE__=Be,window.__BITVID_LOCKDOWN_MODE__=Ne);var k=()=>{};function Me(){return typeof console>"u"||console===null?null:console}function De(){let e=Me();if(!e)return{log:k,info:k,debug:k,warn:k,error:k};let t=typeof e.log=="function"?e.log.bind(e):k;return{log:typeof e.log=="function"?e.log.bind(e):t,info:typeof e.info=="function"?e.info.bind(e):t,debug:typeof e.debug=="function"?e.debug.bind(e):t,warn:typeof e.warn=="function"?e.warn.bind(e):t,error:typeof e.error=="function"?e.error.bind(e):t}}var _=De(),M="[bitvid]";function oe(e){if(e.length===0)return{args:e,force:!1};let t=e[e.length-1];if(t&&typeof t=="object"&&!Array.isArray(t)&&Object.prototype.hasOwnProperty.call(t,"force")){let r=[...e],o=r.pop();return{args:r,force:!!o.force}}return{args:e,force:!1}}var S=Object.freeze({log:(...e)=>{U&&_.log(...e)},info:(...e)=>{U&&_.info(...e)},debug:(...e)=>{U&&_.debug(...e)},warn:(...e)=>{U&&_.warn(...e)},error:(...e)=>{U&&_.error(...e)}}),b=Object.freeze({log:(...e)=>{let{args:t,force:r}=oe(e);if(!r&&t.length===0)return;if(r){_.log(M,...t);return}let[o,...n]=t;if(typeof o=="string"&&n.length===0){_.log(`${M} ${o}`);return}o!==void 0&&_.log(M,o)},info:(...e)=>{let{args:t,force:r}=oe(e);if(!r&&t.length===0)return;if(r){_.info(M,...t);return}let[o,...n]=t;if(typeof o=="string"&&n.length===0){_.info(`${M} ${o}`);return}o!==void 0&&_.info(M,o)},warn:(...e)=>{_.warn(M,...e)},error:(...e)=>{_.error(M,...e)}}),ae=Object.freeze({dev:S,user:b});function Re(e){return!e||typeof e!="object"?!1:Object.entries({dev:["log","info","debug","warn","error"],user:["log","info","warn","error"]}).every(([r,o])=>{let n=e[r];return!n||typeof n!="object"?!1:o.every(c=>typeof n[c]=="function")})}typeof window<"u"&&(Re(window.bitvidLogger)||(window.bitvidLogger=ae));Object.freeze(ae);import{THEME_ACCENT_OVERRIDES as se}from"../config/instance-config.js";var G="bitvid:theme",v="dark",Ue=new Set(["light","dark"]),ie={dark:"rgb(15, 23, 42)",light:"rgb(248, 250, 252)"},ce=["[data-theme-toggle]",'[data-action="toggle-theme"]',".js-theme-toggle","#themeToggle"],ue=Object.freeze({accent:"--color-accent",accentStrong:"--color-accent-strong",accentPressed:"--color-accent-pressed"}),le=new WeakSet,W=new Set,D=null,de=!1,F=null,I=()=>typeof window<"u"&&typeof document<"u",q=e=>{if(typeof e!="string")return null;let t=e.trim().toLowerCase();return t==="default"||t===""?v:Ue.has(t)?t:null},He=()=>{if(!I())return null;try{let e=window.localStorage.getItem(G);return q(e)}catch(e){return b.warn("Unable to read stored theme preference:",e),null}},ke=e=>{if(I())try{window.localStorage.setItem(G,e)}catch(t){b.warn("Unable to persist theme preference:",t)}},K=()=>{if(!I())return null;let{documentElement:e}=document;return e instanceof HTMLElement?e:null},Fe=e=>typeof e=="string"?e.trim():"",Ve=e=>{if(!e||typeof e!="object")return null;let t=Object.entries(e).reduce((r,[o,n])=>{if(!n||typeof n!="object")return r;let c=Object.entries(n).reduce((a,[u,d])=>(Object.prototype.hasOwnProperty.call(ue,u)&&(a[u]=d),a),{});return Object.keys(c).length>0&&(r[o]=c),r},{});return Object.keys(t).length>0?t:null},fe=e=>{F=Ve(e)};fe(typeof se<"u"?se:null);var pe=(e,t)=>{if(!e)return;let r=F&&typeof F=="object"&&F[t]&&typeof F[t]=="object"?F[t]:null,o=!1;Object.entries(ue).forEach(([n,c])=>{let a=r?r[n]:null,u=Fe(a);u?(e.style.setProperty(c,u),o=!0)Line truncated
|
||||
import{IS_DEV_MODE as Be,IS_VERBOSE_DEV_MODE as Me,IS_LOCKDOWN_MODE as De,LOCKDOWN_TITLE as Ct,LOCKDOWN_MESSAGE as xt,ADMIN_SUPER_NPUB as It,ADMIN_DM_IMAGE_URL as vt,BITVID_WEBSITE_URL as Ot,BLOG_URL as Nt,COMMUNITY_URL as Bt,NOSTR_URL as Mt,GITHUB_URL as Dt,BETA_URL as Rt,DNS_URL as Ut,TIP_JAR_URL as Ht,MAX_WALLET_DEFAULT_ZAP as kt,PLATFORM_FEE_PERCENT as Ft,PLATFORM_LUD16_OVERRIDE as Pt,DEFAULT_RELAY_URLS_OVERRIDE as Vt,ADMIN_WHITELIST_MODE_STORAGE_KEY as Wt,DEFAULT_WHITELIST_MODE_ENABLED as qt,ALLOW_NSFW_CONTENT as jt,FEATURE_NIP71_INGEST as $t,DEFAULT_TRUST_SEED_NPUBS as Yt,DEFAULT_BLUR_THRESHOLD as Gt,DEFAULT_AUTOPLAY_BLOCK_THRESHOLD as Kt,DEFAULT_TRUSTED_MUTE_HIDE_THRESHOLD as zt,DEFAULT_TRUSTED_SPAM_HIDE_THRESHOLD as Xt,WATCH_HISTORY_KIND as Zt,WATCH_HISTORY_LIST_IDENTIFIER as Jt,WATCH_HISTORY_MAX_ITEMS as Qt,WATCH_HISTORY_BATCH_RESOLVE as er,WATCH_HISTORY_BATCH_PAGE_SIZE as tr,WATCH_HISTORY_PAYLOAD_MAX_BYTES as rr,WATCH_HISTORY_FETCH_EVENT_LIMIT as nr,WATCH_HISTORY_CACHE_TTL_MS as or,VIEW_COUNT_DEDUPE_WINDOW_SECONDS as ar,VIEW_COUNT_BACKFILL_MAX_DAYS as sr,VIEW_COUNT_CACHE_TTL_MS as ir,ENSURE_PRESENCE_REBROADCAST_COOLDOWN_SECONDS as cr,DEFAULT_PLAYBACK_SOURCE as lr,DEFAULT_PLAYBACK_START_TIMEOUT as dr,ENABLE_NIP17_RELAY_WARNING as ur}from"../config/instance-config.js";var oe=typeof globalThis>"u"?null:globalThis,ae=e=>{if(typeof e=="boolean")return e;if(typeof e=="string"){let t=e.trim().toLowerCase();if(t==="true")return!0;if(t==="false")return!1}return null},re=ae(oe?.__BITVID_DEV_MODE_OVERRIDE__),ne=ae(oe?.__BITVID_VERBOSE_DEV_MODE_OVERRIDE__),U=re===null?!!Be:re;var Re=ne===null?!!Me:ne;var Ue=!!De;typeof window<"u"&&(window.__BITVID_DEV_MODE__=U,window.__BITVID_VERBOSE_DEV_MODE__=Re,window.__BITVID_LOCKDOWN_MODE__=Ue);var k=()=>{};function He(){return typeof console>"u"||console===null?null:console}function ke(){let e=He();if(!e)return{log:k,info:k,debug:k,warn:k,error:k};let t=typeof e.log=="function"?e.log.bind(e):k;return{log:typeof e.log=="function"?e.log.bind(e):t,info:typeof e.info=="function"?e.info.bind(e):t,debug:typeof e.debug=="function"?e.debug.bind(e):t,warn:typeof e.warn=="function"?e.warn.bind(e):t,error:typeof e.error=="function"?e.error.bind(e):t}}var w=ke(),M="[bitvid]";function se(e){if(e.length===0)return{args:e,force:!1};let t=e[e.length-1];if(t&&typeof t=="object"&&!Array.isArray(t)&&Object.prototype.hasOwnProperty.call(t,"force")){let r=[...e],o=r.pop();return{args:r,force:!!o.force}}return{args:e,force:!1}}var A=Object.freeze({log:(...e)=>{U&&w.log(...e)},info:(...e)=>{U&&w.info(...e)},debug:(...e)=>{U&&w.debug(...e)},warn:(...e)=>{U&&w.warn(...e)},error:(...e)=>{U&&w.error(...e)}}),m=Object.freeze({log:(...e)=>{let{args:t,force:r}=se(e);if(!r&&t.length===0)return;if(r){w.log(M,...t);return}let[o,...n]=t;if(typeof o=="string"&&n.length===0){w.log(`${M} ${o}`);return}o!==void 0&&w.log(M,o)},info:(...e)=>{let{args:t,force:r}=se(e);if(!r&&t.length===0)return;if(r){w.info(M,...t);return}let[o,...n]=t;if(typeof o=="string"&&n.length===0){w.info(`${M} ${o}`);return}o!==void 0&&w.info(M,o)},warn:(...e)=>{w.warn(M,...e)},error:(...e)=>{w.error(M,...e)}}),ie=Object.freeze({dev:A,user:m});function Fe(e){return!e||typeof e!="object"?!1:Object.entries({dev:["log","info","debug","warn","error"],user:["log","info","warn","error"]}).every(([r,o])=>{let n=e[r];return!n||typeof n!="object"?!1:o.every(c=>typeof n[c]=="function")})}typeof window<"u"&&(Fe(window.bitvidLogger)||(window.bitvidLogger=ie));Object.freeze(ie);import{THEME_ACCENT_OVERRIDES as ce}from"../config/instance-config.js";var Z="bitvid:theme",O="dark",Pe=new Set(["light","dark"]),le={dark:"rgb(15, 23, 42)",light:"rgb(248, 250, 252)"},de=["[data-theme-toggle]",'[data-action="toggle-theme"]',".js-theme-toggle","#themeToggle"],pe=Object.freeze({accent:"--color-accent",accentStrong:"--color-accent-strong",accentPressed:"--color-accent-pressed"}),ue=new WeakSet,V=new Set,D=null,fe=!1,F=null,N=()=>typeof window<"u"&&typeof document<"u",W=e=>{if(typeof e!="string")return null;let t=e.trim().toLowerCase();return t==="default"||t===""?O:Pe.has(t)?t:null},Ve=()=>{if(!N())return null;try{let e=window.localStorage.getItem(Z);return W(e)}catch(e){return m.warn("Unable to read stored theme preference:",e),null}},We=e=>{if(N())try{window.localStorage.setItem(Z,e)}catch(t){m.warn("Unable to persist theme preference:",t)}},X=()=>{if(!N())return null;let{documentElement:e}=document;return e instanceof HTMLElement?e:null},qe=e=>typeof e=="string"?e.trim():"",je=e=>{if(!e||typeof e!="object")return null;let t=Object.entries(e).reduce((r,[o,n])=>{if(!n||typeof n!="object")return r;let c=Object.entries(n).reduce((a,[p,d])=>(Object.prototype.hasOwnProperty.call(pe,p)&&(a[p]=d),a),{});return Object.keys(c).length>0&&(r[o]=c),r},{});return Object.keys(t).length>0?t:null},me=e=>{F=je(e)};me(typeof ce<"u"?ce:null);var he=(e,t)=>{if(!e)return;let r=F&&typeof F=="object"&&F[t]&&typeof F[t]=="object"?F[t]:null,o=!1;Object.entries(pe).forEach(([n,c])=>Line truncated
|
||||
Reference in new issue
Block a user