Merge pull request #2518 from PR0M3TH3AN/agents/weekly/refactor-agent-video-modal-13827376005693961609

Refactor Video Modal Event Handling to Controller
This commit is contained in:
thePR0M3TH3AN authored and GitHub committed 2026-02-14 20:12:48 -05:00
commit e032f9b23e
4 files changed
+385 -124

No files matched your search

@@ -0,0 +1,32 @@
# Weekly Agent Task Completion: refactor-agent
- **Agent Name**: refactor-agent
- **Cadence**: weekly
- **Date**: 2026-02-15
- **Status**: Completed
## Summary of Changes
Refactored `js/app.js` to extract video modal event handling logic into `js/ui/videoModalController.js`.
### Key Changes
- Modified `js/ui/videoModalController.js`:
- Added `bindEvents()` method to attach event listeners to the video modal component.
- Implemented handler methods: `handleShareNostr`, `handleCopyCdn`, `handleCopyMagnetCallback`, `handleSourceSwitch`.
- Updated constructor to accept necessary callbacks and dependencies (`showSuccess`, `playVideoWithFallback`, etc.).
- Modified `js/app.js`:
- Removed `_bindVideoModalEvents` method.
- Updated `VideoModalController` instantiation to pass new callbacks.
- Called `this.videoModalController.bindEvents()` in `_initModals`.
- Added `tests/unit/ui/videoModalController.test.mjs` to test the new controller logic.
## Verification
- Ran `npm run lint`: Passed.
- Ran `npm run test:unit:shard1`: Passed.
- Ran new unit tests: Passed.
## AGENTS.md Checklist Compliance
- **Separation of Concerns**: Moved DOM event handling logic from `bitvidApp` to `VideoModalController`.
- **State Management**: Controller reads state via getters (`getCurrentVideo`, `getPlaySource`) passed from App.
- **Execution Flow**: Controller invokes callbacks (`playVideoWithFallback`) provided by App.
- **Atomicity**: Change is limited to Video Modal event handling extraction.
+13 -123
View File
@@ -309,9 +309,19 @@ class Application {
getVideoModal: () => this.videoModal,
callbacks: {
showError: (msg) => this.showError(msg),
showSuccess: (msg) => this.showSuccess(msg),
showStatus: (msg, opts) => this.showStatus(msg, opts),
getLastModalTrigger: () => this.lastModalTrigger,
setLastModalTrigger: (val) => this.setLastModalTrigger(val),
getCurrentVideo: () => this.currentVideo,
getPlaySource: () => this.playSource,
getStreamHealthSnapshots: () => this.streamHealthSnapshots,
getCachedUrlHealth: (id, url) => this.getCachedUrlHealth(id, url),
handleCopyMagnet: () => this.handleCopyMagnet(),
openShareNostrModal: (opts) => this.openShareNostrModal(opts),
playVideoWithFallback: (opts) => this.playVideoWithFallback(opts),
attachMoreMenuHandlers: (container) =>
this.attachMoreMenuHandlers(container),
},
});
@@ -850,131 +860,11 @@ class Application {
);
}
_bindVideoModalEvents() {
const modalRoot = this.videoModal.getRoot();
if (modalRoot) {
this.attachMoreMenuHandlers(modalRoot);
}
if (
this.videoModal &&
typeof this.videoModal.addEventListener === "function"
) {
this.videoModal.addEventListener("video:share-nostr", (event) => {
this.openShareNostrModal({
video: event?.detail?.video || null,
triggerElement: event?.detail?.trigger || null,
});
});
this.videoModal.addEventListener("video:copy-cdn", (event) => {
const video = event?.detail?.video || this.currentVideo;
const url = video?.url || "";
if (!url) {
this.showError("No CDN link available to copy.");
return;
}
navigator.clipboard
.writeText(url)
.then(() => this.showSuccess("CDN link copied to clipboard!"))
.catch(() => this.showError("Failed to copy CDN link."));
});
this.videoModal.addEventListener("video:copy-magnet", () => {
this.handleCopyMagnet();
});
this.videoModal.addEventListener("playback:switch-source", (event) => {
const detail = event?.detail || {};
const { source } = detail;
if (!source) {
return;
}
const modalVideo = detail?.video || null;
const fallbackVideo = this.currentVideo || null;
const video = {
...(fallbackVideo || {}),
...(modalVideo || {}),
};
const urlCandidate =
typeof video.url === "string" ? video.url.trim() : "";
const magnetCandidate =
typeof video.magnet === "string" ? video.magnet.trim() : "";
if (!modalVideo && !fallbackVideo) {
devLogger.warn("[app] Playback source switch missing video data.");
return;
}
const magnetAvailable = Boolean(magnetCandidate);
const cachedStreamHealth =
video?.id && this.streamHealthSnapshots instanceof Map
? this.streamHealthSnapshots.get(video.id)
: null;
const cachedPeers = Number.isFinite(cachedStreamHealth?.peers)
? cachedStreamHealth.peers
: null;
const hasActivePeers =
cachedPeers === null ? null : cachedPeers > 0;
const cachedUrlHealth =
video?.id && urlCandidate
? this.getCachedUrlHealth(video.id, urlCandidate)
: null;
const cdnUnavailable =
!urlCandidate ||
["offline", "timeout"].includes(cachedUrlHealth?.status);
if (source === "torrent" && !magnetAvailable) {
userLogger.warn(
"[app] Unable to switch to torrent playback: missing magnet.",
);
this.showError(
"Torrent playback is unavailable for this video. No magnet was provided.",
);
return;
}
if (source === "torrent" && hasActivePeers === false) {
userLogger.warn(
"[app] Switching to torrent playback despite 0 active peers detected.",
);
this.showStatus(
"Warning: No peers detected. Playback may fail or stall.",
{ autoHideMs: 5000 }
);
// Proceed anyway
}
if (source === "url" && cdnUnavailable) {
userLogger.warn(
"[app] Unable to switch to CDN playback: URL unavailable.",
);
this.showError(
"CDN playback is unavailable right now, staying on the torrent stream.",
);
return;
}
if (this.playSource && source === this.playSource) {
return;
}
this.playVideoWithFallback({
url: urlCandidate,
magnet: magnetCandidate,
forcedSource: source,
}).catch((error) => {
devLogger.warn(
"[app] Failed to switch playback source:",
error,
);
});
});
}
}
_initModals() {
const videoModalPromise = this.videoModal.load().then(() => {
this._bindVideoModalEvents();
if (this.videoModalController) {
this.videoModalController.bindEvents();
}
});
const uploadModalPromise = this.uploadModal.load().catch((error) => {
+191 -1
View File
@@ -1,20 +1,38 @@
import { devLogger } from "../utils/logger.js";
import { devLogger, userLogger } from "../utils/logger.js";
export default class VideoModalController {
constructor({
getVideoModal,
callbacks: {
showError,
showSuccess,
showStatus,
getLastModalTrigger,
setLastModalTrigger,
getCurrentVideo,
getPlaySource,
getStreamHealthSnapshots,
getCachedUrlHealth,
handleCopyMagnet,
openShareNostrModal,
playVideoWithFallback,
attachMoreMenuHandlers,
} = {},
}) {
this.getVideoModal = getVideoModal;
this.showError = showError;
this.showSuccess = showSuccess;
this.showStatus = showStatus;
this.getLastModalTrigger = getLastModalTrigger;
this.setLastModalTrigger = setLastModalTrigger;
this.getCurrentVideo = getCurrentVideo;
this.getPlaySource = getPlaySource;
this.getStreamHealthSnapshots = getStreamHealthSnapshots;
this.getCachedUrlHealth = getCachedUrlHealth;
this.handleCopyMagnetCallback = handleCopyMagnet;
this.openShareNostrModalCallback = openShareNostrModal;
this.playVideoWithFallbackCallback = playVideoWithFallback;
this.attachMoreMenuHandlersCallback = attachMoreMenuHandlers;
this.videoModalReadyPromise = null;
}
@@ -27,6 +45,178 @@ export default class VideoModalController {
devLogger.log("[VideoModalController]", message, ...args);
}
bindEvents() {
if (!this.videoModal) {
return;
}
const modalRoot =
typeof this.videoModal.getRoot === "function"
? this.videoModal.getRoot()
: null;
if (modalRoot && this.attachMoreMenuHandlersCallback) {
this.attachMoreMenuHandlersCallback(modalRoot);
}
if (typeof this.videoModal.addEventListener === "function") {
this.videoModal.addEventListener("video:share-nostr", (event) => {
this.handleShareNostr(event);
});
this.videoModal.addEventListener("video:copy-cdn", (event) => {
this.handleCopyCdn(event);
});
this.videoModal.addEventListener("video:copy-magnet", () => {
if (this.handleCopyMagnetCallback) {
this.handleCopyMagnetCallback();
}
});
this.videoModal.addEventListener("playback:switch-source", (event) => {
this.handleSourceSwitch(event);
});
}
}
handleShareNostr(event) {
if (this.openShareNostrModalCallback) {
this.openShareNostrModalCallback({
video: event?.detail?.video || null,
triggerElement: event?.detail?.trigger || null,
});
}
}
handleCopyCdn(event) {
const video =
event?.detail?.video ||
(this.getCurrentVideo ? this.getCurrentVideo() : null);
const url = video?.url || "";
if (!url) {
if (this.showError) {
this.showError("No CDN link available to copy.");
}
return;
}
navigator.clipboard
.writeText(url)
.then(() => {
if (this.showSuccess) {
this.showSuccess("CDN link copied to clipboard!");
}
})
.catch(() => {
if (this.showError) {
this.showError("Failed to copy CDN link.");
}
});
}
handleSourceSwitch(event) {
const detail = event?.detail || {};
const { source } = detail;
if (!source) {
return;
}
const modalVideo = detail?.video || null;
const fallbackVideo = this.getCurrentVideo ? this.getCurrentVideo() : null;
const video = {
...(fallbackVideo || {}),
...(modalVideo || {}),
};
const urlCandidate =
typeof video.url === "string" ? video.url.trim() : "";
const magnetCandidate =
typeof video.magnet === "string" ? video.magnet.trim() : "";
if (!modalVideo && !fallbackVideo) {
devLogger.warn(
"[VideoModalController] Playback source switch missing video data.",
);
return;
}
const magnetAvailable = Boolean(magnetCandidate);
const streamHealthSnapshots = this.getStreamHealthSnapshots
? this.getStreamHealthSnapshots()
: null;
const cachedStreamHealth =
video?.id && streamHealthSnapshots instanceof Map
? streamHealthSnapshots.get(video.id)
: null;
const cachedPeers = Number.isFinite(cachedStreamHealth?.peers)
? cachedStreamHealth.peers
: null;
const hasActivePeers = cachedPeers === null ? null : cachedPeers > 0;
const cachedUrlHealth =
video?.id && urlCandidate && this.getCachedUrlHealth
? this.getCachedUrlHealth(video.id, urlCandidate)
: null;
const cdnUnavailable =
!urlCandidate ||
["offline", "timeout"].includes(cachedUrlHealth?.status);
if (source === "torrent" && !magnetAvailable) {
userLogger.warn(
"[VideoModalController] Unable to switch to torrent playback: missing magnet.",
);
if (this.showError) {
this.showError(
"Torrent playback is unavailable for this video. No magnet was provided.",
);
}
return;
}
if (source === "torrent" && hasActivePeers === false) {
userLogger.warn(
"[VideoModalController] Switching to torrent playback despite 0 active peers detected.",
);
if (this.showStatus) {
this.showStatus(
"Warning: No peers detected. Playback may fail or stall.",
{ autoHideMs: 5000 },
);
}
// Proceed anyway
}
if (source === "url" && cdnUnavailable) {
userLogger.warn(
"[VideoModalController] Unable to switch to CDN playback: URL unavailable.",
);
if (this.showError) {
this.showError(
"CDN playback is unavailable right now, staying on the torrent stream.",
);
}
return;
}
const currentSource = this.getPlaySource ? this.getPlaySource() : null;
if (currentSource && source === currentSource) {
return;
}
if (this.playVideoWithFallbackCallback) {
this.playVideoWithFallbackCallback({
url: urlCandidate,
magnet: magnetCandidate,
forcedSource: source,
}).catch((error) => {
devLogger.warn(
"[VideoModalController] Failed to switch playback source:",
error,
);
});
}
}
/**
* Show the modal and set the "Please stand by" poster on the video.
*/
+149
View File
@@ -0,0 +1,149 @@
import test from "node:test";
import assert from "node:assert/strict";
// Mock globals before importing modules that might use them
if (!globalThis.window) {
globalThis.window = {
addEventListener: () => {},
removeEventListener: () => {},
};
}
if (!globalThis.navigator) {
globalThis.navigator = {
userAgent: "node",
clipboard: {
writeText: async () => {},
}
};
}
if (!globalThis.localStorage) {
globalThis.localStorage = {
getItem: () => null,
setItem: () => {},
};
}
// Now import the controller
import VideoModalController from "../../../js/ui/videoModalController.js";
test("VideoModalController bindEvents attaches listeners", (t) => {
const listeners = {};
const mockVideoModal = {
addEventListener: (event, handler) => {
listeners[event] = handler;
},
getRoot: () => ({}),
};
const controller = new VideoModalController({
getVideoModal: () => mockVideoModal,
callbacks: {
attachMoreMenuHandlers: () => {},
}
});
controller.bindEvents();
assert.ok(listeners["video:share-nostr"], "should attach share handler");
assert.ok(listeners["video:copy-cdn"], "should attach copy-cdn handler");
assert.ok(listeners["video:copy-magnet"], "should attach copy-magnet handler");
assert.ok(listeners["playback:switch-source"], "should attach switch-source handler");
});
test("VideoModalController handleShareNostr triggers callback", (t) => {
let called = false;
let args = null;
const controller = new VideoModalController({
getVideoModal: () => ({}),
callbacks: {
openShareNostrModal: (opts) => {
called = true;
args = opts;
},
},
});
controller.handleShareNostr({ detail: { video: { id: "1" } } });
assert.equal(called, true);
assert.equal(args.video.id, "1");
});
test("VideoModalController handleCopyCdn triggers clipboard write", async (t) => {
let clipboardText = "";
if (!globalThis.navigator.clipboard) {
Object.defineProperty(globalThis.navigator, 'clipboard', {
value: {},
writable: true,
configurable: true
});
}
const originalWriteText = globalThis.navigator.clipboard.writeText;
globalThis.navigator.clipboard.writeText = async (text) => {
clipboardText = text;
};
t.after(() => {
if (originalWriteText) {
globalThis.navigator.clipboard.writeText = originalWriteText;
} else {
delete globalThis.navigator.clipboard.writeText;
}
});
let successCalled = false;
const controller = new VideoModalController({
getVideoModal: () => ({}),
callbacks: {
getCurrentVideo: () => ({ url: "https://example.com/video.mp4" }),
showSuccess: () => { successCalled = true; }
}
});
await controller.handleCopyCdn({});
assert.equal(clipboardText, "https://example.com/video.mp4");
// We can't easily await the promise chain inside handleCopyCdn because it doesn't return the promise.
// But since the mock writeText is synchronous (returns resolved promise), the callback should fire in microtask.
// We can wait a bit.
await new Promise(resolve => setTimeout(resolve, 0));
assert.equal(successCalled, true);
});
test("VideoModalController handleCopyMagnet triggers callback", (t) => {
let called = false;
const controller = new VideoModalController({
getVideoModal: () => ({}),
callbacks: {
handleCopyMagnet: () => { called = true; }
}
});
// Manually trigger the logic that would be called by the event listener
if (controller.handleCopyMagnetCallback) {
controller.handleCopyMagnetCallback();
}
assert.equal(called, true);
});
test("VideoModalController handleSourceSwitch calls playVideoWithFallback", async (t) => {
let called = false;
let args = null;
const controller = new VideoModalController({
getVideoModal: () => ({}),
callbacks: {
getCurrentVideo: () => ({ id: "vid1", url: "http://url", magnet: "magnet:?" }),
getStreamHealthSnapshots: () => new Map([["vid1", { peers: 5 }]]),
getCachedUrlHealth: () => ({ status: "ok" }),
playVideoWithFallback: async (opts) => {
called = true;
args = opts;
}
}
});
controller.handleSourceSwitch({ detail: { source: "torrent", video: { id: "vid1" } } });
await new Promise(resolve => setTimeout(resolve, 0));
assert.equal(called, true);
assert.equal(args.forcedSource, "torrent");
});