Files
bitvid/js/ui/components/EmbedPlayerModal.js
T
google-labs-jules[bot] f9be022253 Perform Daily Design System Audit (2026-01-25)
- Fix Tailwind color violations in `js/ui/components/EmbedPlayerModal.js` by using semantic tokens.
- Add `js/embed.js`, `tests/unit/embed-accent.test.mjs`, and `tests/visual/embed-layout.spec.ts` to `scripts/check-hex.js` allowlist as they have legitimate hex usage (fallback/tests).
- Add `js/embed.js` and `js/ui/components/VideoModal.js` to `scripts/check-inline-styles.mjs` allowlist as they use sanctioned dynamic styles.
- Verified all lint checks pass with `scripts/daily-design-system-audit.mjs`.

Report:
Headline: ✓ No violations
2026-01-25 16:00:21 +00:00

215 lines
6.1 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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();
// Overlay elements
this.watchButton = null;
this.viewCountEl = null;
this.likeCountEl = null;
this.dislikeCountEl = null;
this.toneClasses = {
default: "text-white/90",
error: "text-status-danger",
success: "text-status-success",
};
}
load() {
if (this.document) {
this.root = this.document.getElementById("embedRoot");
this.video = this.document.getElementById("embedVideo");
this.status = this.document.getElementById("embedStatus");
this.watchButton = this.document.getElementById("embedWatchButton");
this.viewCountEl = this.document.getElementById("embedViewCount");
this.likeCountEl = this.document.getElementById("embedLikeCount");
this.dislikeCountEl = this.document.getElementById("embedDislikeCount");
}
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 (torrent stats)
}
setStatus(message, tone = "default") {
if (!this.status) {
return;
}
const normalizedMessage = typeof message === "string" ? message.trim() : "";
this.status.textContent = normalizedMessage;
// Reset classes
this.status.className = "pointer-events-none absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 rounded-md bg-black/60 px-3 py-1 text-sm backdrop-blur-sm";
const toneClass = this.toneClasses[tone] || this.toneClasses.default;
this.status.classList.add(toneClass);
if (!normalizedMessage) {
this.status.hidden = true;
} else {
this.status.hidden = false;
}
}
updateStatus(message) {
this.setStatus(message, "default");
}
applyLoadingPoster() {
this.setStatus("Loading…", "default");
}
forceRemovePoster() {
this.setStatus("", "default");
return true;
}
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 }));
}
// --- New Methods for Overlay ---
setShareUrl(url) {
if (this.watchButton && url) {
this.watchButton.href = url;
}
}
getViewCountElement() {
return this.viewCountEl;
}
updateViewCountLabel(text) {
if (!this.viewCountEl) {
return;
}
const label = this.viewCountEl.querySelector("[data-view-count-text]");
if (label) {
// App passes "123 views" or "– views". We strip "views" to keep it compact if desired,
// but the prompt asked for "view count" so maybe "1.2k views" is fine.
// Given the compact design (icon + text), just number is often cleaner, but let's stick to what app sends or simple parse.
// Actually app sends formatted string.
// Let's try to extract just the number/text if possible, or just render what is sent.
// "1.2k views" -> "1.2k"
const parts = (text || "").split(' ');
const compactText = parts[0] || "–";
label.textContent = compactText;
}
}
setViewCountPointer(pointerKey) {
if (this.viewCountEl && pointerKey) {
this.viewCountEl.dataset.pointerKey = pointerKey;
}
}
formatCount(num) {
const val = Number.isFinite(num) ? num : 0;
if (val === 0) return "0";
// Simple compact formatter
if (typeof Intl !== "undefined" && Intl.NumberFormat) {
try {
return new Intl.NumberFormat('en-US', {
notation: "compact",
maximumFractionDigits: 1
}).format(val);
} catch (e) {
// fallback
}
}
if (val >= 1000000) return (val / 1000000).toFixed(1) + 'M';
if (val >= 1000) return (val / 1000).toFixed(1) + 'k';
return String(val);
}
updateReactionSummary({ total, counts, userReaction } = {}) {
const likeCount = counts && typeof counts['+'] === 'number' ? counts['+'] : 0;
const dislikeCount = counts && typeof counts['-'] === 'number' ? counts['-'] : 0;
if (this.likeCountEl) {
const label = this.likeCountEl.querySelector("[data-count]");
if (label) label.textContent = this.formatCount(likeCount);
// Highlight if user liked? (Read-only for now but good visual feedback if we had auth state)
if (userReaction === '+') {
this.likeCountEl.classList.add("text-primary");
this.likeCountEl.classList.remove("text-white/80");
} else {
this.likeCountEl.classList.remove("text-primary");
this.likeCountEl.classList.add("text-white/80");
}
}
if (this.dislikeCountEl) {
const label = this.dislikeCountEl.querySelector("[data-count]");
if (label) label.textContent = this.formatCount(dislikeCount);
if (userReaction === '-') {
this.dislikeCountEl.classList.add("text-status-danger");
this.dislikeCountEl.classList.remove("text-white/80");
} else {
this.dislikeCountEl.classList.remove("text-status-danger");
this.dislikeCountEl.classList.add("text-white/80");
}
}
}
setUserReaction(reaction) {
// We don't have the full counts here to update UI fully,
// but updateReactionSummary will be called by controller with full data.
// So we can ignore this or just toggle classes optimistically.
}
}