mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 16:02:26 +00:00
579 lines
16 KiB
JavaScript
579 lines
16 KiB
JavaScript
export class VideoModal {
|
|
constructor({
|
|
removeTrackingScripts,
|
|
setGlobalModalState,
|
|
document: doc,
|
|
logger,
|
|
} = {}) {
|
|
if (!doc) {
|
|
throw new Error("VideoModal requires a document reference.");
|
|
}
|
|
if (typeof setGlobalModalState !== "function") {
|
|
throw new Error("VideoModal requires setGlobalModalState helper.");
|
|
}
|
|
|
|
this.document = doc;
|
|
this.window = doc.defaultView || globalThis;
|
|
this.removeTrackingScripts =
|
|
typeof removeTrackingScripts === "function"
|
|
? removeTrackingScripts
|
|
: () => {};
|
|
this.setGlobalModalState = setGlobalModalState;
|
|
this.logger = logger || console;
|
|
this.eventTarget = new EventTarget();
|
|
|
|
this.loaded = false;
|
|
|
|
this.playerModal = null;
|
|
this.scrollRegion = null;
|
|
this.modalVideo = null;
|
|
this.modalStatus = null;
|
|
this.modalProgress = null;
|
|
this.modalPeers = null;
|
|
this.modalSpeed = null;
|
|
this.modalDownloaded = null;
|
|
this.videoTitle = null;
|
|
this.videoDescription = null;
|
|
this.videoTimestamp = null;
|
|
this.videoViewCountEl = null;
|
|
this.creatorAvatar = null;
|
|
this.creatorName = null;
|
|
this.creatorNpub = null;
|
|
this.copyMagnetBtn = null;
|
|
this.shareBtn = null;
|
|
this.modalZapBtn = null;
|
|
this.modalMoreBtn = null;
|
|
this.modalMoreMenu = null;
|
|
|
|
this.modalPosterCleanup = null;
|
|
this.videoEventCleanup = null;
|
|
|
|
this.activeVideo = null;
|
|
|
|
this.handleCopyRequest = this.handleCopyRequest.bind(this);
|
|
this.handleShareRequest = this.handleShareRequest.bind(this);
|
|
this.handleCreatorNavigation = this.handleCreatorNavigation.bind(this);
|
|
|
|
this.MODAL_LOADING_POSTER = "assets/gif/please-stand-by.gif";
|
|
}
|
|
|
|
log(message, ...args) {
|
|
if (!message) {
|
|
return;
|
|
}
|
|
if (this.logger && typeof this.logger.log === "function") {
|
|
this.logger.log(message, ...args);
|
|
return;
|
|
}
|
|
if (typeof this.logger === "function") {
|
|
this.logger(message, ...args);
|
|
return;
|
|
}
|
|
console.log(message, ...args);
|
|
}
|
|
|
|
addEventListener(type, listener, options) {
|
|
this.eventTarget.addEventListener(type, listener, options);
|
|
}
|
|
|
|
removeEventListener(type, listener, options) {
|
|
this.eventTarget.removeEventListener(type, listener, options);
|
|
}
|
|
|
|
dispatch(type, detail) {
|
|
const event = new CustomEvent(type, { detail });
|
|
this.eventTarget.dispatchEvent(event);
|
|
}
|
|
|
|
getRoot() {
|
|
return this.playerModal;
|
|
}
|
|
|
|
getVideoElement() {
|
|
return this.modalVideo;
|
|
}
|
|
|
|
setVideoElement(videoElement) {
|
|
this.detachVideoEvents();
|
|
this.clearPosterCleanup();
|
|
|
|
if (
|
|
this.window &&
|
|
typeof this.window.HTMLVideoElement !== "undefined" &&
|
|
videoElement instanceof this.window.HTMLVideoElement
|
|
) {
|
|
this.modalVideo = videoElement;
|
|
this.bindVideoEvents();
|
|
} else {
|
|
this.modalVideo = null;
|
|
}
|
|
|
|
return this.modalVideo;
|
|
}
|
|
|
|
async load() {
|
|
if (this.loaded && this.playerModal && this.playerModal.isConnected) {
|
|
return this.playerModal;
|
|
}
|
|
|
|
const existing = this.document.getElementById("playerModal");
|
|
if (existing) {
|
|
this.hydrate(existing);
|
|
this.loaded = true;
|
|
return this.playerModal;
|
|
}
|
|
|
|
const response = await fetch("components/video-modal.html");
|
|
if (!response.ok) {
|
|
throw new Error(`HTTP error! status: ${response.status}`);
|
|
}
|
|
|
|
const html = await response.text();
|
|
const container = this.document.getElementById("modalContainer");
|
|
if (!container) {
|
|
throw new Error("Modal container element not found!");
|
|
}
|
|
|
|
const wrapper = this.document.createElement("div");
|
|
wrapper.innerHTML = html;
|
|
this.removeTrackingScripts(wrapper);
|
|
container.appendChild(wrapper);
|
|
|
|
const playerModal = wrapper.querySelector("#playerModal");
|
|
if (!playerModal) {
|
|
throw new Error("Player modal root not found in markup.");
|
|
}
|
|
|
|
this.hydrate(playerModal);
|
|
this.loaded = true;
|
|
return this.playerModal;
|
|
}
|
|
|
|
hydrate(playerModal) {
|
|
this.playerModal = playerModal;
|
|
this.scrollRegion =
|
|
playerModal.querySelector(".player-modal__content") || playerModal;
|
|
|
|
this.modalVideo = playerModal.querySelector("#modalVideo") || null;
|
|
this.modalStatus = playerModal.querySelector("#modalStatus") || null;
|
|
this.modalProgress = playerModal.querySelector("#modalProgress") || null;
|
|
this.modalPeers = playerModal.querySelector("#modalPeers") || null;
|
|
this.modalSpeed = playerModal.querySelector("#modalSpeed") || null;
|
|
this.modalDownloaded =
|
|
playerModal.querySelector("#modalDownloaded") || null;
|
|
this.videoTitle = playerModal.querySelector("#videoTitle") || null;
|
|
this.videoDescription =
|
|
playerModal.querySelector("#videoDescription") || null;
|
|
this.videoTimestamp =
|
|
playerModal.querySelector("#videoTimestamp") || null;
|
|
this.videoViewCountEl =
|
|
playerModal.querySelector("#videoViewCount") || 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;
|
|
this.shareBtn = playerModal.querySelector("#shareBtn") || null;
|
|
this.modalZapBtn = playerModal.querySelector("#modalZapBtn") || null;
|
|
this.modalMoreBtn = playerModal.querySelector("#modalMoreBtn") || null;
|
|
this.modalMoreMenu =
|
|
playerModal.querySelector("#moreDropdown-modal") || null;
|
|
|
|
const closeButton = playerModal.querySelector("#closeModal");
|
|
if (closeButton) {
|
|
closeButton.addEventListener("click", () => {
|
|
this.dispatch("modal:close", { video: this.activeVideo });
|
|
});
|
|
}
|
|
|
|
const modalNav = playerModal.querySelector("#modalNav");
|
|
if (modalNav && this.scrollRegion) {
|
|
let lastScrollY = 0;
|
|
this.scrollRegion.addEventListener("scroll", () => {
|
|
const currentScrollY = this.scrollRegion.scrollTop;
|
|
const shouldShowNav =
|
|
currentScrollY <= lastScrollY || currentScrollY < 50;
|
|
modalNav.style.transform = shouldShowNav
|
|
? "translateY(0)"
|
|
: "translateY(-100%)";
|
|
lastScrollY = currentScrollY;
|
|
});
|
|
}
|
|
|
|
this.bindVideoEvents();
|
|
this.bindActionButtons();
|
|
this.setZapVisibility(false);
|
|
this.setCopyEnabled(false);
|
|
this.setShareEnabled(false);
|
|
this.resetStats();
|
|
}
|
|
|
|
bindVideoEvents() {
|
|
if (
|
|
!this.modalVideo ||
|
|
!this.window ||
|
|
typeof this.window.HTMLVideoElement === "undefined" ||
|
|
!(this.modalVideo instanceof this.window.HTMLVideoElement)
|
|
) {
|
|
return;
|
|
}
|
|
|
|
const loadedHandler = () => {
|
|
this.dispatch("playback:loadeddata", {
|
|
video: this.modalVideo,
|
|
active: this.activeVideo,
|
|
});
|
|
};
|
|
const playingHandler = () => {
|
|
this.dispatch("playback:playing", {
|
|
video: this.modalVideo,
|
|
active: this.activeVideo,
|
|
});
|
|
};
|
|
|
|
this.modalVideo.addEventListener("loadeddata", loadedHandler);
|
|
this.modalVideo.addEventListener("playing", playingHandler);
|
|
|
|
this.videoEventCleanup = () => {
|
|
if (!this.modalVideo) {
|
|
return;
|
|
}
|
|
this.modalVideo.removeEventListener("loadeddata", loadedHandler);
|
|
this.modalVideo.removeEventListener("playing", playingHandler);
|
|
this.videoEventCleanup = null;
|
|
};
|
|
}
|
|
|
|
detachVideoEvents() {
|
|
if (typeof this.videoEventCleanup === "function") {
|
|
this.videoEventCleanup();
|
|
}
|
|
this.videoEventCleanup = null;
|
|
}
|
|
|
|
bindActionButtons() {
|
|
if (this.copyMagnetBtn) {
|
|
this.copyMagnetBtn.addEventListener("click", this.handleCopyRequest);
|
|
}
|
|
if (this.shareBtn) {
|
|
this.shareBtn.addEventListener("click", this.handleShareRequest);
|
|
}
|
|
|
|
if (this.modalZapBtn) {
|
|
this.modalZapBtn.addEventListener("click", (event) => {
|
|
event?.preventDefault?.();
|
|
this.dispatch("video:zap", { video: this.activeVideo });
|
|
});
|
|
}
|
|
|
|
if (this.creatorAvatar) {
|
|
this.creatorAvatar.style.cursor = "pointer";
|
|
this.creatorAvatar.addEventListener("click", this.handleCreatorNavigation);
|
|
}
|
|
if (this.creatorName) {
|
|
this.creatorName.style.cursor = "pointer";
|
|
this.creatorName.addEventListener("click", this.handleCreatorNavigation);
|
|
}
|
|
}
|
|
|
|
handleCopyRequest(event) {
|
|
event?.preventDefault?.();
|
|
if (this.copyMagnetBtn?.disabled) {
|
|
return;
|
|
}
|
|
this.dispatch("video:copy-magnet", { video: this.activeVideo });
|
|
}
|
|
|
|
handleShareRequest(event) {
|
|
event?.preventDefault?.();
|
|
if (this.shareBtn?.disabled) {
|
|
return;
|
|
}
|
|
this.dispatch("video:share", { video: this.activeVideo });
|
|
}
|
|
|
|
handleCreatorNavigation(event) {
|
|
event?.preventDefault?.();
|
|
this.dispatch("creator:navigate", { video: this.activeVideo });
|
|
}
|
|
|
|
open(video) {
|
|
this.activeVideo = video || null;
|
|
if (!this.playerModal) {
|
|
return;
|
|
}
|
|
|
|
this.playerModal.style.display = "flex";
|
|
this.playerModal.classList.remove("hidden");
|
|
this.document.body.classList.add("modal-open");
|
|
this.document.documentElement.classList.add("modal-open");
|
|
if (this.scrollRegion) {
|
|
this.scrollRegion.scrollTop = 0;
|
|
}
|
|
this.setGlobalModalState("player", true);
|
|
this.applyLoadingPoster();
|
|
}
|
|
|
|
close() {
|
|
this.activeVideo = null;
|
|
if (this.playerModal) {
|
|
this.playerModal.style.display = "none";
|
|
this.playerModal.classList.add("hidden");
|
|
}
|
|
this.document.body.classList.remove("modal-open");
|
|
this.document.documentElement.classList.remove("modal-open");
|
|
this.setGlobalModalState("player", false);
|
|
this.forceRemovePoster("close");
|
|
}
|
|
|
|
applyLoadingPoster() {
|
|
if (!this.modalVideo) {
|
|
return;
|
|
}
|
|
|
|
this.clearPosterCleanup();
|
|
|
|
const clearPoster = () => {
|
|
this.forceRemovePoster("playback-event");
|
|
};
|
|
|
|
this.modalVideo.addEventListener("loadeddata", clearPoster);
|
|
this.modalVideo.addEventListener("playing", clearPoster);
|
|
this.modalVideo.poster = this.MODAL_LOADING_POSTER;
|
|
|
|
this.modalPosterCleanup = () => {
|
|
if (!this.modalVideo) {
|
|
return;
|
|
}
|
|
this.modalVideo.removeEventListener("loadeddata", clearPoster);
|
|
this.modalVideo.removeEventListener("playing", clearPoster);
|
|
this.modalPosterCleanup = null;
|
|
};
|
|
}
|
|
|
|
clearPosterCleanup() {
|
|
if (typeof this.modalPosterCleanup === "function") {
|
|
this.modalPosterCleanup();
|
|
}
|
|
this.modalPosterCleanup = null;
|
|
}
|
|
|
|
forceRemovePoster(reason = "manual-clear") {
|
|
if (!this.modalVideo) {
|
|
return false;
|
|
}
|
|
|
|
this.clearPosterCleanup();
|
|
|
|
const videoEl = this.modalVideo;
|
|
const hadPoster =
|
|
videoEl.hasAttribute("poster") ||
|
|
(typeof videoEl.poster === "string" && videoEl.poster !== "");
|
|
|
|
if (!hadPoster) {
|
|
return false;
|
|
}
|
|
|
|
videoEl.poster = "";
|
|
if (videoEl.hasAttribute("poster")) {
|
|
videoEl.removeAttribute("poster");
|
|
}
|
|
|
|
this.log(`[VideoModal] Cleared loading poster (${reason}).`);
|
|
return true;
|
|
}
|
|
|
|
resetStats() {
|
|
this.updatePeers("");
|
|
this.updateSpeed("");
|
|
this.updateDownloaded("");
|
|
this.updateProgress("0%");
|
|
}
|
|
|
|
updateStatus(message) {
|
|
if (this.modalStatus) {
|
|
this.modalStatus.textContent = message || "";
|
|
}
|
|
}
|
|
|
|
updatePeers(text) {
|
|
if (this.modalPeers) {
|
|
this.modalPeers.textContent = text || "";
|
|
}
|
|
}
|
|
|
|
updateSpeed(text) {
|
|
if (this.modalSpeed) {
|
|
this.modalSpeed.textContent = text || "";
|
|
}
|
|
}
|
|
|
|
updateDownloaded(text) {
|
|
if (this.modalDownloaded) {
|
|
this.modalDownloaded.textContent = text || "";
|
|
}
|
|
}
|
|
|
|
updateProgress(value) {
|
|
if (!this.modalProgress) {
|
|
return;
|
|
}
|
|
if (typeof value === "number" && Number.isFinite(value)) {
|
|
this.modalProgress.style.width = `${Math.max(0, value)}%`;
|
|
return;
|
|
}
|
|
if (typeof value === "string") {
|
|
this.modalProgress.style.width = value;
|
|
return;
|
|
}
|
|
this.modalProgress.style.width = "0%";
|
|
}
|
|
|
|
setCopyEnabled(enabled) {
|
|
if (!this.copyMagnetBtn) {
|
|
return;
|
|
}
|
|
this.copyMagnetBtn.disabled = !enabled;
|
|
this.copyMagnetBtn.setAttribute(
|
|
"aria-disabled",
|
|
(!enabled).toString()
|
|
);
|
|
this.copyMagnetBtn.classList.toggle("opacity-50", !enabled);
|
|
this.copyMagnetBtn.classList.toggle("cursor-not-allowed", !enabled);
|
|
}
|
|
|
|
setShareEnabled(enabled) {
|
|
if (!this.shareBtn) {
|
|
return;
|
|
}
|
|
this.shareBtn.disabled = !enabled;
|
|
this.shareBtn.setAttribute(
|
|
"aria-disabled",
|
|
(!enabled).toString()
|
|
);
|
|
this.shareBtn.classList.toggle("opacity-50", !enabled);
|
|
this.shareBtn.classList.toggle("cursor-not-allowed", !enabled);
|
|
}
|
|
|
|
setZapVisibility(visible) {
|
|
if (!this.modalZapBtn) {
|
|
return;
|
|
}
|
|
this.modalZapBtn.classList.toggle("hidden", !visible);
|
|
this.modalZapBtn.disabled = !visible;
|
|
this.modalZapBtn.setAttribute("aria-disabled", (!visible).toString());
|
|
this.modalZapBtn.setAttribute("aria-hidden", (!visible).toString());
|
|
if (visible) {
|
|
this.modalZapBtn.removeAttribute("tabindex");
|
|
} else {
|
|
this.modalZapBtn.setAttribute("tabindex", "-1");
|
|
}
|
|
}
|
|
|
|
getViewCountElement() {
|
|
return this.videoViewCountEl;
|
|
}
|
|
|
|
updateViewCountLabel(text) {
|
|
if (this.videoViewCountEl) {
|
|
this.videoViewCountEl.textContent = text || "";
|
|
}
|
|
}
|
|
|
|
setViewCountPointer(pointerKey) {
|
|
if (!this.videoViewCountEl) {
|
|
return;
|
|
}
|
|
if (pointerKey) {
|
|
this.videoViewCountEl.dataset.viewPointer = pointerKey;
|
|
} else if (this.videoViewCountEl.dataset?.viewPointer) {
|
|
delete this.videoViewCountEl.dataset.viewPointer;
|
|
}
|
|
}
|
|
|
|
updateMetadata({
|
|
title,
|
|
description,
|
|
timestamp,
|
|
viewCount,
|
|
creator,
|
|
} = {}) {
|
|
if (this.videoTitle) {
|
|
this.videoTitle.textContent = title || "Untitled";
|
|
}
|
|
if (this.videoDescription) {
|
|
this.videoDescription.textContent = description || "";
|
|
}
|
|
if (this.videoTimestamp) {
|
|
this.videoTimestamp.textContent = timestamp || "";
|
|
}
|
|
if (typeof viewCount === "string") {
|
|
this.updateViewCountLabel(viewCount);
|
|
}
|
|
if (creator) {
|
|
this.updateCreator(creator);
|
|
}
|
|
}
|
|
|
|
updateCreator({ name, avatarUrl, npub } = {}) {
|
|
if (this.creatorName) {
|
|
this.creatorName.textContent = name || "Unknown";
|
|
}
|
|
if (this.creatorAvatar) {
|
|
this.creatorAvatar.src = avatarUrl || "assets/svg/default-profile.svg";
|
|
this.creatorAvatar.alt = name || "Unknown";
|
|
}
|
|
if (this.creatorNpub) {
|
|
this.creatorNpub.textContent = npub || "";
|
|
}
|
|
}
|
|
|
|
syncMoreMenuData({ currentVideo, canManageBlacklist }) {
|
|
if (!this.modalMoreMenu) {
|
|
return;
|
|
}
|
|
|
|
const buttons = this.modalMoreMenu.querySelectorAll("button[data-action]");
|
|
const HTMLElementCtor =
|
|
this.window && typeof this.window.HTMLElement !== "undefined"
|
|
? this.window.HTMLElement
|
|
: null;
|
|
|
|
buttons.forEach((button) => {
|
|
if (HTMLElementCtor && !(button instanceof HTMLElementCtor)) {
|
|
return;
|
|
}
|
|
|
|
const action = button.dataset.action || "";
|
|
if (action === "blacklist-author") {
|
|
if (canManageBlacklist && currentVideo?.pubkey) {
|
|
button.dataset.author = currentVideo.pubkey;
|
|
button.classList.remove("hidden");
|
|
button.setAttribute("aria-hidden", "false");
|
|
} else {
|
|
delete button.dataset.author;
|
|
button.classList.add("hidden");
|
|
button.setAttribute("aria-hidden", "true");
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (action === "open-channel" || action === "block-author") {
|
|
if (currentVideo?.pubkey) {
|
|
button.dataset.author = currentVideo.pubkey;
|
|
} else {
|
|
delete button.dataset.author;
|
|
}
|
|
}
|
|
|
|
if (action === "copy-link" || action === "report") {
|
|
if (currentVideo?.id) {
|
|
button.dataset.eventId = currentVideo.id;
|
|
} else {
|
|
delete button.dataset.eventId;
|
|
}
|
|
}
|
|
});
|
|
}
|
|
}
|