From 40f87167b9ea8c4f783dfb04bcd6acaa031525e6 Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Thu, 25 Sep 2025 19:31:41 -0400 Subject: [PATCH] Align torrent badge styling with CDN indicator --- css/style.css | 38 ------------------ js/app.js | 94 ++++++++++++++++++++++++++++++++++---------- js/channelProfile.js | 19 +++++++-- js/gridHealth.js | 80 ++++++++++++++++++++++++++----------- js/subscriptions.js | 36 +++++++++-------- 5 files changed, 168 insertions(+), 99 deletions(-) diff --git a/css/style.css b/css/style.css index bd9cdd74..208e5552 100644 --- a/css/style.css +++ b/css/style.css @@ -74,44 +74,6 @@ header img { box-shadow: var(--shadow-md); } -.video-card .stream-health { - display: inline-flex; - align-items: center; - justify-content: center; - min-width: 2rem; - font-size: 1.1rem; - line-height: 1; - transition: transform 150ms ease, filter 150ms ease, opacity 150ms ease; -} - -.video-card .stream-health[data-stream-health-state="good"] { - filter: drop-shadow(0 0 6px rgba(34, 197, 94, 0.45)); -} - -.video-card .stream-health[data-stream-health-state="none"] { - opacity: 0.85; -} - -.video-card .stream-health[data-stream-health-state="noresp"], -.video-card .stream-health[data-stream-health-state="unknown"] { - opacity: 0.6; -} - -.video-card .stream-health[data-stream-health-state="checking"] { - animation: stream-health-pulse 1.2s ease-in-out infinite alternate; -} - -@keyframes stream-health-pulse { - from { - transform: scale(0.95); - opacity: 0.65; - } - to { - transform: scale(1.05); - opacity: 1; - } -} - .video-card--enter { opacity: 0; animation: video-card-fade-in 220ms ease-out forwards; diff --git a/js/app.js b/js/app.js index 2be08ceb..ffbe72ba 100644 --- a/js/app.js +++ b/js/app.js @@ -2132,10 +2132,28 @@ class bitvidApp { * so they can re-use the exact same badge skeleton. Keeping the markup in * one place avoids subtle mismatches when we tweak copy or classes later. */ - getUrlHealthPlaceholderMarkup() { + getUrlHealthPlaceholderMarkup(options = {}) { + const includeMargin = options?.includeMargin !== false; + const classes = [ + "url-health-badge", + "text-xs", + "font-semibold", + "px-2", + "py-1", + "rounded", + "inline-flex", + "items-center", + "gap-1", + "bg-gray-800", + "text-gray-300", + ]; + if (includeMargin) { + classes.splice(1, 0, "mt-3"); + } + return `
+ ⏳ Torrent +
+ `; + } + + isMagnetUriSupported(magnet) { + return isValidMagnetUri(magnet); + } + getCachedUrlHealth(eventId, url) { return readUrlHealthFromCache(eventId, url); } @@ -2480,7 +2535,7 @@ class bitvidApp { const playbackMagnet = magnetCandidate; const showUnsupportedTorrentBadge = !trimmedUrl && magnetProvided && !magnetSupported; - const torrentBadge = showUnsupportedTorrentBadge + const torrentWarningHtml = showUnsupportedTorrentBadge ? `

+ ${urlBadgeHtml}${torrentHealthBadgeHtml} + + ` + : ""; const rawThumbnail = typeof video.thumbnail === "string" ? video.thumbnail.trim() : ""; @@ -2546,19 +2613,6 @@ class bitvidApp {

-
- - Streamable? - - - 🟦 - -

${gearMenu}

- ${urlStatusHtml} - ${torrentBadge} + ${connectionBadgesHtml} + ${torrentWarningHtml} `; diff --git a/js/channelProfile.js b/js/channelProfile.js index 8922b649..0292ba96 100644 --- a/js/channelProfile.js +++ b/js/channelProfile.js @@ -367,9 +367,22 @@ async function loadUserVideos(pubkey) { typeof video.url === "string" ? video.url : ""; const trimmedUrl = playbackUrl ? playbackUrl.trim() : ""; const playbackMagnet = rawMagnet || legacyInfoHash || ""; - const urlStatusHtml = trimmedUrl - ? app.getUrlHealthPlaceholderMarkup() + const magnetSupported = app.isMagnetUriSupported(playbackMagnet); + const urlBadgeHtml = trimmedUrl + ? app.getUrlHealthPlaceholderMarkup({ includeMargin: false }) : ""; + const torrentHealthBadgeHtml = + magnetSupported && playbackMagnet + ? app.getTorrentHealthBadgeMarkup({ includeMargin: false }) + : ""; + const connectionBadgesHtml = + urlBadgeHtml || torrentHealthBadgeHtml + ? ` +
+ ${urlBadgeHtml}${torrentHealthBadgeHtml} +
+ ` + : ""; cardEl.innerHTML = `
${gearMenu}
- ${urlStatusHtml} + ${connectionBadgesHtml} `; diff --git a/js/gridHealth.js b/js/gridHealth.js index c05ba294..be1f976d 100644 --- a/js/gridHealth.js +++ b/js/gridHealth.js @@ -54,46 +54,82 @@ function toVisual(health) { return "noresp"; } -function formatCount(health) { - if (!health || !Number.isFinite(health.seeders) || health.seeders <= 0) { - return ""; - } - return ` (${health.seeders})`; -} - function setBadge(card, visual, health) { - const el = card.querySelector(".stream-health"); - if (!el) { + const badge = card.querySelector(".torrent-health-badge"); + if (!badge) { return; } + const hadMargin = badge.classList.contains("mt-3"); + + const baseClasses = [ + "torrent-health-badge", + "text-xs", + "font-semibold", + "px-2", + "py-1", + "rounded", + "inline-flex", + "items-center", + "gap-1", + "transition-colors", + "duration-200", + ]; + if (hadMargin) { + baseClasses.unshift("mt-3"); + } + badge.className = baseClasses.join(" "); + const map = { good: { - text: "🟢", - aria: "Streamable: seeders available", + icon: "✅", + aria: "WebTorrent fallback ready", + classes: ["bg-green-900", "text-green-200"], + role: "status", }, none: { - text: "🟡", + icon: "⚠️", aria: "No seeders reported by trackers", + classes: ["bg-amber-900", "text-amber-200"], + role: "status", }, noresp: { - text: "⚫", + icon: "❌", aria: "No tracker response", + classes: ["bg-red-900", "text-red-200"], + role: "alert", }, checking: { - text: "🟦", - aria: "Checking stream availability", + icon: "⏳", + aria: "Checking Torrent availability", + classes: ["bg-gray-800", "text-gray-300"], + role: "status", }, unknown: { - text: "⚪", - aria: "Unknown stream availability", + icon: "⚠️", + aria: "Torrent availability unknown", + classes: ["bg-amber-900", "text-amber-200"], + role: "status", }, }; + const entry = map[visual] || map.unknown; - const countText = formatCount(health); - el.textContent = `${entry.text}${countText}`; - el.setAttribute("aria-label", countText ? `${entry.aria}${countText}` : entry.aria); - el.setAttribute("title", countText ? `${entry.aria}${countText}` : entry.aria); - el.dataset.streamHealthState = visual; + entry.classes.forEach((cls) => badge.classList.add(cls)); + + const seederCount = + health && Number.isFinite(health.seeders) && health.seeders > 0 + ? health.seeders + : null; + const countText = seederCount ? ` (${seederCount})` : ""; + const ariaCount = seederCount ? ` with ${seederCount} seeders` : ""; + + const iconPrefix = entry.icon ? `${entry.icon} ` : ""; + badge.textContent = `${iconPrefix}Torrent${countText}`; + const ariaLabel = `${entry.aria}${ariaCount}`; + badge.setAttribute("aria-label", ariaLabel); + badge.setAttribute("title", ariaLabel); + badge.setAttribute("aria-live", "polite"); + badge.setAttribute("role", entry.role); + badge.dataset.streamHealthState = visual; } function applyHealth(card, health) { diff --git a/js/subscriptions.js b/js/subscriptions.js index 50ed39cb..68b42102 100644 --- a/js/subscriptions.js +++ b/js/subscriptions.js @@ -388,9 +388,26 @@ class SubscriptionsManager { const magnetCandidate = trimmedMagnet || legacyInfoHash; const playbackMagnet = magnetCandidate; const magnetProvided = magnetCandidate.length > 0; - const urlStatusHtml = trimmedUrl - ? window.app?.getUrlHealthPlaceholderMarkup?.() ?? "" + const magnetSupported = + window.app?.isMagnetUriSupported?.(magnetCandidate) ?? false; + const urlBadgeHtml = trimmedUrl + ? window.app?.getUrlHealthPlaceholderMarkup?.({ includeMargin: false }) ?? + "" : ""; + const torrentHealthBadgeHtml = + magnetProvided && magnetSupported + ? window.app?.getTorrentHealthBadgeMarkup?.({ + includeMargin: false, + }) ?? "" + : ""; + const connectionBadgesHtml = + urlBadgeHtml || torrentHealthBadgeHtml + ? ` +
+ ${urlBadgeHtml}${torrentHealthBadgeHtml} +
+ ` + : ""; const cardHtml = `
-
- - Streamable? - - - 🟦 - -

${gearMenu}

- ${urlStatusHtml} + ${connectionBadgesHtml}
`;