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}
`;