mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-28 00:12:25 +00:00
fix(embed): implement robust EmbedPlayerModal and guard resetTorrentStats
- Add `js/ui/components/EmbedPlayerModal.js` to implement the `VideoModal` interface for embed contexts (including `resetStats`). - Update `js/ui/ModalManager.js` to detect embed mode and use `EmbedPlayerModal`. - Clean up `js/embed.js` to remove local modal definition and add `postMessage` diagnostic logging. - Update `js/app.js` `resetTorrentStats` log message and verify guard. - Add unit tests for `EmbedPlayerModal` and `resetTorrentStats` logic. - Add E2E reproduction script `tests/e2e/repro_issue.spec.ts`.
This commit is contained in:
1 parent
d71d2812f4
commit
981fac785a
7 files changed
+379
-117
No files matched your search
@@ -4906,7 +4906,7 @@ class Application {
|
||||
this.videoModal.resetStats();
|
||||
} else {
|
||||
devLogger.info(
|
||||
"[Application] resetTorrentStats: videoModal.resetStats not available in this context — skipping."
|
||||
"[Application] resetTorrentStats: videoModal.resetStats not available — skipping."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+48
-97
@@ -5,13 +5,52 @@ import { resolveVideoPointer } from "./utils/videoPointer.js";
|
||||
import { devLogger, userLogger } from "./utils/logger.js";
|
||||
import { nostrToolsReady } from "./nostrToolsBootstrap.js";
|
||||
|
||||
// Diagnostic forwarder for embed debugging
|
||||
function postToParent(type, payload) {
|
||||
try {
|
||||
if (window.parent !== window) {
|
||||
window.parent.postMessage({ __bitvid_debug: true, type, payload }, "*");
|
||||
}
|
||||
} catch (e) {
|
||||
/* no-op */
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener("error", (ev) => {
|
||||
postToParent("error", {
|
||||
message: ev.message,
|
||||
filename: ev.filename,
|
||||
lineno: ev.lineno,
|
||||
colno: ev.colno,
|
||||
stack: ev.error && ev.error.stack,
|
||||
});
|
||||
});
|
||||
|
||||
window.addEventListener("unhandledrejection", (ev) => {
|
||||
postToParent("unhandledrejection", {
|
||||
reason: (ev.reason && ev.reason.stack) || String(ev.reason),
|
||||
});
|
||||
});
|
||||
|
||||
["log", "info", "warn", "error", "debug"].forEach((level) => {
|
||||
if (console[level]) {
|
||||
const orig = console[level].bind(console);
|
||||
console[level] = (...args) => {
|
||||
postToParent("console", {
|
||||
level,
|
||||
args: args.map((a) => (typeof a === "string" ? a : String(a))),
|
||||
});
|
||||
orig(...args);
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
const POINTER_PARAM = "pointer";
|
||||
const PLAYBACK_PARAM = "playback";
|
||||
const HEX64_REGEX = /^[0-9a-f]{64}$/i;
|
||||
|
||||
const statusEl = document.getElementById("embedStatus");
|
||||
const videoEl = document.getElementById("embedVideo");
|
||||
const rootEl = document.getElementById("embedRoot");
|
||||
|
||||
const toneClasses = {
|
||||
default: "text-muted",
|
||||
@@ -24,8 +63,7 @@ const setStatus = (message, tone = "default") => {
|
||||
return;
|
||||
}
|
||||
|
||||
const normalizedMessage =
|
||||
typeof message === "string" ? message.trim() : "";
|
||||
const normalizedMessage = typeof message === "string" ? message.trim() : "";
|
||||
statusEl.textContent = normalizedMessage;
|
||||
|
||||
Object.values(toneClasses).forEach((className) => {
|
||||
@@ -40,102 +78,15 @@ const setStatus = (message, tone = "default") => {
|
||||
}
|
||||
};
|
||||
|
||||
class EmbedVideoModal {
|
||||
constructor({ root, video, status } = {}) {
|
||||
this.root = root || null;
|
||||
this.video = video || null;
|
||||
this.status = status || null;
|
||||
this.eventTarget = new EventTarget();
|
||||
}
|
||||
|
||||
addEventListener(...args) {
|
||||
this.eventTarget.addEventListener(...args);
|
||||
}
|
||||
|
||||
removeEventListener(...args) {
|
||||
this.eventTarget.removeEventListener(...args);
|
||||
}
|
||||
|
||||
dispatchEvent(event) {
|
||||
return this.eventTarget.dispatchEvent(event);
|
||||
}
|
||||
|
||||
getRoot() {
|
||||
return this.root;
|
||||
}
|
||||
|
||||
getVideoElement() {
|
||||
return this.video;
|
||||
}
|
||||
|
||||
setVideoElement(videoElement) {
|
||||
if (videoElement instanceof HTMLVideoElement) {
|
||||
this.video = videoElement;
|
||||
}
|
||||
return this.video;
|
||||
}
|
||||
|
||||
resetStats() {
|
||||
// No-op for embed view
|
||||
}
|
||||
|
||||
updateStatus(message) {
|
||||
setStatus(message, "default");
|
||||
}
|
||||
|
||||
applyLoadingPoster() {
|
||||
setStatus("Preparing playback…", "default");
|
||||
}
|
||||
|
||||
forceRemovePoster() {
|
||||
return false;
|
||||
}
|
||||
|
||||
clearPosterCleanup() {}
|
||||
|
||||
setTorrentStatsVisibility(isVisible) {
|
||||
if (!this.root) {
|
||||
return;
|
||||
}
|
||||
this.root.dataset.torrentStats = isVisible ? "true" : "false";
|
||||
}
|
||||
|
||||
open() {
|
||||
return this.root;
|
||||
}
|
||||
|
||||
close() {}
|
||||
|
||||
async load() {
|
||||
if (!this.video || !(this.video instanceof HTMLVideoElement)) {
|
||||
throw new Error("Embed video element is missing.");
|
||||
}
|
||||
return this.root;
|
||||
}
|
||||
}
|
||||
|
||||
const embedModal = new EmbedVideoModal({
|
||||
root: rootEl,
|
||||
video: videoEl,
|
||||
status: statusEl,
|
||||
});
|
||||
|
||||
const app = new Application({
|
||||
ui: {
|
||||
videoModal: embedModal,
|
||||
},
|
||||
});
|
||||
// ModalManager will instantiate EmbedPlayerModal automatically when running in embed mode.
|
||||
const app = new Application();
|
||||
|
||||
app.showError = (message) => {
|
||||
const normalizedMessage =
|
||||
typeof message === "string" ? message.trim() : "";
|
||||
const normalizedMessage = typeof message === "string" ? message.trim() : "";
|
||||
if (normalizedMessage) {
|
||||
userLogger.error(normalizedMessage);
|
||||
}
|
||||
setStatus(
|
||||
normalizedMessage || "Playback failed. Please try again.",
|
||||
"error",
|
||||
);
|
||||
setStatus(normalizedMessage || "Playback failed. Please try again.", "error");
|
||||
};
|
||||
|
||||
app.showStatus = (message) => {
|
||||
@@ -228,8 +179,8 @@ const buildRelayList = (relays) => {
|
||||
new Set(
|
||||
combined
|
||||
.map((relay) => (typeof relay === "string" ? relay.trim() : ""))
|
||||
.filter(Boolean),
|
||||
),
|
||||
.filter(Boolean)
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
@@ -308,7 +259,7 @@ const resolveEventFromPointer = async (pointer) => {
|
||||
if (Array.isArray(events) && events.length) {
|
||||
const flattened = events.flat().filter(Boolean);
|
||||
rawEvent = flattened.sort(
|
||||
(a, b) => (b.created_at || 0) - (a.created_at || 0),
|
||||
(a, b) => (b.created_at || 0) - (a.created_at || 0)
|
||||
)[0];
|
||||
}
|
||||
} catch (error) {
|
||||
|
||||
+27
-19
@@ -7,6 +7,7 @@ import { VideoModal } from "./components/VideoModal.js";
|
||||
import { RevertModal } from "./components/RevertModal.js";
|
||||
import { ShareNostrModal } from "./components/ShareNostrModal.js";
|
||||
import { EmbedVideoModal } from "./components/EmbedVideoModal.js";
|
||||
import { EmbedPlayerModal } from "./components/EmbedPlayerModal.js";
|
||||
import initUploadModal from "./initUploadModal.js";
|
||||
import initEditModal from "./initEditModal.js";
|
||||
import initDeleteModal from "./initDeleteModal.js";
|
||||
@@ -195,29 +196,36 @@ export default class ModalManager {
|
||||
});
|
||||
app.embedVideoModal = this.embedVideoModal;
|
||||
|
||||
const isEmbedMode = typeof window !== "undefined" && window.location.pathname.endsWith("/embed.html");
|
||||
|
||||
this.videoModal =
|
||||
(typeof this.ui.videoModal === "function"
|
||||
? this.ui.videoModal({ app })
|
||||
: this.ui.videoModal) ||
|
||||
new VideoModal({
|
||||
removeTrackingScripts,
|
||||
setGlobalModalState,
|
||||
document: doc,
|
||||
logger: {
|
||||
log: (message, ...args) => app.log(message, ...args),
|
||||
},
|
||||
mediaLoader: app.mediaLoader,
|
||||
assets: {
|
||||
fallbackThumbnailSrc: this.assets.fallbackThumbnailSrc,
|
||||
},
|
||||
state: {
|
||||
loadedThumbnails: app.loadedThumbnails,
|
||||
},
|
||||
helpers: {
|
||||
safeEncodeNpub: (pubkey) => app.safeEncodeNpub(pubkey),
|
||||
formatShortNpub: (value) => formatShortNpub(value),
|
||||
},
|
||||
});
|
||||
(isEmbedMode
|
||||
? new EmbedPlayerModal({
|
||||
document: doc,
|
||||
logger: { log: (...a) => app.log(...a) },
|
||||
})
|
||||
: new VideoModal({
|
||||
removeTrackingScripts,
|
||||
setGlobalModalState,
|
||||
document: doc,
|
||||
logger: {
|
||||
log: (message, ...args) => app.log(message, ...args),
|
||||
},
|
||||
mediaLoader: app.mediaLoader,
|
||||
assets: {
|
||||
fallbackThumbnailSrc: this.assets.fallbackThumbnailSrc,
|
||||
},
|
||||
state: {
|
||||
loadedThumbnails: app.loadedThumbnails,
|
||||
},
|
||||
helpers: {
|
||||
safeEncodeNpub: (pubkey) => app.safeEncodeNpub(pubkey),
|
||||
formatShortNpub: (value) => formatShortNpub(value),
|
||||
},
|
||||
}));
|
||||
|
||||
if (
|
||||
this.videoModal &&
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
export class EmbedPlayerModal {
|
||||
constructor({ document: doc, logger } = {}) {
|
||||
this.document = doc || (typeof document !== "undefined" ? document : null);
|
||||
this.logger = logger || console;
|
||||
this.root = null;
|
||||
this.video = null;
|
||||
this.status = null;
|
||||
this.eventTarget = new EventTarget();
|
||||
|
||||
this.toneClasses = {
|
||||
default: "text-muted",
|
||||
error: "text-danger",
|
||||
success: "text-success",
|
||||
};
|
||||
}
|
||||
|
||||
load() {
|
||||
if (this.document) {
|
||||
this.root = this.document.getElementById("embedRoot");
|
||||
this.video = this.document.getElementById("embedVideo");
|
||||
this.status = this.document.getElementById("embedStatus");
|
||||
}
|
||||
return Promise.resolve(this.root);
|
||||
}
|
||||
|
||||
getRoot() {
|
||||
return this.root;
|
||||
}
|
||||
|
||||
getVideoElement() {
|
||||
return this.video;
|
||||
}
|
||||
|
||||
setVideoElement(videoElement) {
|
||||
if (videoElement) {
|
||||
this.video = videoElement;
|
||||
}
|
||||
return this.video;
|
||||
}
|
||||
|
||||
resetStats() {
|
||||
// no-op for embed view
|
||||
}
|
||||
|
||||
setStatus(message, tone = "default") {
|
||||
if (!this.status) {
|
||||
return;
|
||||
}
|
||||
|
||||
const normalizedMessage = typeof message === "string" ? message.trim() : "";
|
||||
this.status.textContent = normalizedMessage;
|
||||
|
||||
Object.values(this.toneClasses).forEach((className) => {
|
||||
if (className) {
|
||||
this.status.classList.remove(className);
|
||||
}
|
||||
});
|
||||
|
||||
const toneClass = this.toneClasses[tone] || this.toneClasses.default;
|
||||
if (toneClass) {
|
||||
this.status.classList.add(toneClass);
|
||||
}
|
||||
}
|
||||
|
||||
updateStatus(message) {
|
||||
this.setStatus(message, "default");
|
||||
}
|
||||
|
||||
applyLoadingPoster() {
|
||||
this.setStatus("Preparing playback…", "default");
|
||||
}
|
||||
|
||||
forceRemovePoster() {
|
||||
return false;
|
||||
}
|
||||
|
||||
clearPosterCleanup() {}
|
||||
|
||||
setTorrentStatsVisibility(isVisible) {
|
||||
if (!this.root) {
|
||||
return;
|
||||
}
|
||||
this.root.dataset.torrentStats = isVisible ? "true" : "false";
|
||||
}
|
||||
|
||||
open() {
|
||||
return this.root;
|
||||
}
|
||||
|
||||
close() {}
|
||||
|
||||
updateMetadata() {}
|
||||
setShareEnabled() {}
|
||||
|
||||
addEventListener(type, listener, options) {
|
||||
this.eventTarget.addEventListener(type, listener, options);
|
||||
}
|
||||
|
||||
removeEventListener(type, listener, options) {
|
||||
this.eventTarget.removeEventListener(type, listener, options);
|
||||
}
|
||||
|
||||
dispatch(type, detail) {
|
||||
return this.eventTarget.dispatchEvent(new CustomEvent(type, { detail }));
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user