${this.escapeHTML(video.title)}
@@ -974,132 +1067,83 @@ class bitvidApp {
`;
- // Fire off a background fetch for the author's profile
+ // Turn the HTML into an element
+ const template = document.createElement("template");
+ template.innerHTML = cardHtml.trim();
+ const cardEl = template.content.firstElementChild;
+
+ // Fetch the author's profile info in the background
this.fetchAndRenderProfile(video.pubkey);
- return cardHtml;
+ // Add the finished card to our fragment
+ fragment.appendChild(cardEl);
});
- // Filter out any empty strings
- const valid = htmlList.filter((x) => x.length > 0);
- if (valid.length === 0) {
- this.videoList.innerHTML = `
-
- No valid videos to display.
-
`;
- return;
- }
+ // Clear the list and add our fragment
+ this.videoList.innerHTML = "";
+ this.videoList.appendChild(fragment);
- // Finally inject into DOM
- this.videoList.innerHTML = valid.join("");
- }
+ // Lazy-load images
+ const lazyEls = this.videoList.querySelectorAll("[data-lazy]");
+ lazyEls.forEach((el) => this.mediaLoader.observe(el));
- /**
- * Retrieve the profile for a given pubkey (kind:0) and update the DOM.
- */
- async fetchAndRenderProfile(pubkey, forceRefresh = false) {
- const now = Date.now();
+ // -------------------------------
+ // Gear menu / button event listeners
+ // -------------------------------
- // Check if we already have a cached entry for this pubkey:
- const cacheEntry = this.profileCache.get(pubkey);
-
- // If not forcing refresh, and we have a cache entry less than 60 sec old, use it:
- if (!forceRefresh && cacheEntry && now - cacheEntry.timestamp < 60000) {
- this.updateProfileInDOM(pubkey, cacheEntry.profile);
- return;
- }
-
- // Otherwise, go fetch from the relay
- try {
- const userEvents = await nostrClient.pool.list(nostrClient.relays, [
- { kinds: [0], authors: [pubkey], limit: 1 },
- ]);
- if (userEvents.length > 0 && userEvents[0].content) {
- const data = JSON.parse(userEvents[0].content);
- const profile = {
- name: data.name || data.display_name || "Unknown",
- picture: data.picture || "assets/svg/default-profile.svg",
- };
-
- // Store into the cache with a timestamp
- this.profileCache.set(pubkey, {
- profile,
- timestamp: now,
- });
-
- // Now update the DOM elements
- this.updateProfileInDOM(pubkey, profile);
- }
- } catch (err) {
- console.error("Profile fetch error for pubkey:", pubkey, err);
- }
- }
-
- /**
- * Update all DOM elements that match this pubkey, e.g. .author-pic[data-pubkey=...]
- */
- updateProfileInDOM(pubkey, profile) {
- const picEls = document.querySelectorAll(
- `.author-pic[data-pubkey="${pubkey}"]`
+ // Toggle the gear menu
+ const gearButtons = this.videoList.querySelectorAll(
+ "[data-settings-dropdown]"
);
- picEls.forEach((el) => {
- el.src = profile.picture;
+ gearButtons.forEach((button) => {
+ button.addEventListener("click", () => {
+ const index = button.getAttribute("data-settings-dropdown");
+ const dropdown = document.getElementById(`settingsDropdown-${index}`);
+ if (dropdown) {
+ dropdown.classList.toggle("hidden");
+ }
+ });
});
- const nameEls = document.querySelectorAll(
- `.author-name[data-pubkey="${pubkey}"]`
+
+ // Edit button
+ const editButtons = this.videoList.querySelectorAll("[data-edit-index]");
+ editButtons.forEach((button) => {
+ button.addEventListener("click", () => {
+ const index = button.getAttribute("data-edit-index");
+ const dropdown = document.getElementById(`settingsDropdown-${index}`);
+ if (dropdown) dropdown.classList.add("hidden");
+ // Assuming you have a method like this in your code:
+ this.handleEditVideo(index);
+ });
+ });
+
+ // Revert button
+ const revertButtons = this.videoList.querySelectorAll(
+ "[data-revert-index]"
);
- nameEls.forEach((el) => {
- el.textContent = profile.name;
+ revertButtons.forEach((button) => {
+ button.addEventListener("click", () => {
+ const index = button.getAttribute("data-revert-index");
+ const dropdown = document.getElementById(`settingsDropdown-${index}`);
+ if (dropdown) dropdown.classList.add("hidden");
+ // Assuming you have a method like this in your code:
+ this.handleRevertVideo(index);
+ });
});
- }
- /**
- * Plays a video given its magnet URI.
- * We simply look up which event has this magnet
- * and then delegate to playVideoByEventId for
- * consistent modal and metadata handling.
- */
- async playVideo(magnetURI) {
- try {
- if (!magnetURI) {
- this.showError("Invalid Magnet URI.");
- return;
- }
-
- const decodedMagnet = decodeURIComponent(magnetURI);
-
- // If we are already playing this exact magnet, do nothing.
- if (this.currentMagnetUri === decodedMagnet) {
- this.log("Same video requested - already playing");
- return;
- }
-
- // 1) Check local 'videosMap' or 'nostrClient.getActiveVideos()'
- let matchedVideo = Array.from(this.videosMap.values()).find(
- (v) => v.magnet === decodedMagnet
- );
- if (!matchedVideo) {
- // Instead of forcing a full `fetchVideos()`,
- // try looking in the activeVideos from local cache:
- const activeVideos = nostrClient.getActiveVideos();
- matchedVideo = activeVideos.find((v) => v.magnet === decodedMagnet);
- }
-
- // If still not found, you can do a single event-based approach or just show an error:
- if (!matchedVideo) {
- this.showError("No matching video found in local cache.");
- return;
- }
-
- // Update tracking
- this.currentMagnetUri = decodedMagnet;
-
- // Delegate to the main method
- await this.playVideoByEventId(matchedVideo.id);
- } catch (error) {
- console.error("Error in playVideo:", error);
- this.showError(`Playback error: ${error.message}`);
- }
+ // Delete All button
+ const deleteAllButtons = this.videoList.querySelectorAll(
+ "[data-delete-all-index]"
+ );
+ deleteAllButtons.forEach((button) => {
+ button.addEventListener("click", () => {
+ const index = button.getAttribute("data-delete-all-index");
+ const dropdown = document.getElementById(`settingsDropdown-${index}`);
+ if (dropdown) dropdown.classList.add("hidden");
+ // Assuming you have a method like this in your code:
+ this.handleFullDeleteVideo(index);
+ });
+ });
}
/**
@@ -1371,55 +1415,48 @@ class bitvidApp {
* Helper to open a video by event ID (like ?v=...).
*/
async playVideoByEventId(eventId) {
- // First, check if this event is blacklisted by event ID
if (this.blacklistedEventIds.has(eventId)) {
this.showError("This content has been removed or is not allowed.");
return;
}
try {
- // 1) Check local subscription map
let video = this.videosMap.get(eventId);
- // 2) If not in local map, attempt fallback fetch from getOldEventById
if (!video) {
video = await this.getOldEventById(eventId);
}
- // 3) If still not found, show error and return
if (!video) {
this.showError("Video not found.");
return;
}
- // **Check if video’s author is blacklisted**
const authorNpub = this.safeEncodeNpub(video.pubkey) || video.pubkey;
if (initialBlacklist.includes(authorNpub)) {
this.showError("This content has been removed or is not allowed.");
return;
}
- // 4) Decrypt magnet if private & owned
if (
video.isPrivate &&
video.pubkey === this.pubkey &&
!video.alreadyDecrypted
) {
- this.log("Decrypting private magnet link...");
video.magnet = fakeDecrypt(video.magnet);
video.alreadyDecrypted = true;
}
- // 5) Show the modal and set the "please stand by" poster
this.currentVideo = video;
this.currentMagnetUri = video.magnet;
this.showModalWithPoster();
- // 6) Update ?v= param in the URL
+ // Update ?v= param in the URL
const nevent = window.NostrTools.nip19.neventEncode({ id: eventId });
- const newUrl =
- window.location.pathname + `?v=${encodeURIComponent(nevent)}`;
+ const newUrl = `${window.location.pathname}?v=${encodeURIComponent(
+ nevent
+ )}`;
window.history.pushState({}, "", newUrl);
- // 7) Optionally fetch the author profile
+ // Fetch author profile
let creatorProfile = {
name: "Unknown",
picture: `https://robohash.org/${video.pubkey}`,
@@ -1439,7 +1476,6 @@ class bitvidApp {
this.log("Error fetching creator profile:", error);
}
- // 8) Render video details in modal
const creatorNpub = this.safeEncodeNpub(video.pubkey) || video.pubkey;
if (this.videoTitle) {
this.videoTitle.textContent = video.title || "Untitled";
@@ -1465,43 +1501,27 @@ class bitvidApp {
this.creatorAvatar.alt = creatorProfile.name;
}
- // 9) Clean up any existing torrent instance before starting a new stream
await torrentClient.cleanup();
- // 10) Append a cache-busting parameter to the magnet URI
const cacheBustedMagnet = video.magnet + "&ts=" + Date.now();
this.log("Starting video stream with:", cacheBustedMagnet);
- // 11) Set autoplay preferences:
- // Read user preference from localStorage (if not set, default to muted)
+ // Autoplay preferences
const storedUnmuted = localStorage.getItem("unmutedAutoplay");
const userWantsUnmuted = storedUnmuted === "true";
this.modalVideo.muted = !userWantsUnmuted;
- this.log(
- "Autoplay preference - unmuted:",
- userWantsUnmuted,
- "=> muted:",
- this.modalVideo.muted
- );
- // Attach a volumechange listener to update the stored preference
this.modalVideo.addEventListener("volumechange", () => {
localStorage.setItem(
"unmutedAutoplay",
(!this.modalVideo.muted).toString()
);
- this.log(
- "Volume changed, new unmuted preference:",
- !this.modalVideo.muted
- );
});
- // 12) Start torrent streaming
const realTorrent = await torrentClient.streamVideo(
cacheBustedMagnet,
this.modalVideo
);
- // 13) Attempt to autoplay; if unmuted autoplay fails, fall back to muted
this.modalVideo.play().catch((err) => {
this.log("Autoplay failed:", err);
if (!this.modalVideo.muted) {
@@ -1513,7 +1533,7 @@ class bitvidApp {
}
});
- // 14) Start intervals to update torrent stats (every 3 seconds)
+ // Update torrent stats every 3s
const updateInterval = setInterval(() => {
if (!document.body.contains(this.modalVideo)) {
clearInterval(updateInterval);
@@ -1523,7 +1543,7 @@ class bitvidApp {
}, 3000);
this.activeIntervals.push(updateInterval);
- // 15) (Optional) Mirror small inline stats into the modal
+ // Mirror stats into the modal if needed
const mirrorInterval = setInterval(() => {
if (!document.body.contains(this.modalVideo)) {
clearInterval(mirrorInterval);
@@ -1534,7 +1554,6 @@ class bitvidApp {
const peers = document.getElementById("peers");
const speed = document.getElementById("speed");
const downloaded = document.getElementById("downloaded");
-
if (status && this.modalStatus) {
this.modalStatus.textContent = status.textContent;
}
diff --git a/js/sidebar.js b/js/sidebar.js
index e2eacf9..c5c7808 100644
--- a/js/sidebar.js
+++ b/js/sidebar.js
@@ -1,3 +1,5 @@
+//js/sidebar.js
+
import { loadView } from "./viewManager.js";
import { viewInitRegistry } from "./viewManager.js";
diff --git a/js/webtorrent.js b/js/webtorrent.js
index 0e82b2e..65b8ea8 100644
--- a/js/webtorrent.js
+++ b/js/webtorrent.js
@@ -1,10 +1,19 @@
+//js/webtorrent.js
+
import WebTorrent from "./webtorrent.min.js";
export class TorrentClient {
constructor() {
- this.client = null; // Do NOT instantiate right away
+ // Reusable objects and flags
+ this.client = null;
this.currentTorrent = null;
- this.TIMEOUT_DURATION = 60000; // 60 seconds
+
+ // Service worker registration is cached
+ this.swRegistration = null;
+ this.serverCreated = false; // Indicates if we've called createServer on this.client
+
+ // Timeout for SW operations
+ this.TIMEOUT_DURATION = 60000;
}
log(msg) {
@@ -21,6 +30,22 @@ export class TorrentClient {
return /firefox/i.test(window.navigator.userAgent);
}
+ /**
+ * Makes sure we have exactly one WebTorrent client instance and one SW registration.
+ * Called once from streamVideo.
+ */
+ async init() {
+ // 1) If the client doesn't exist, create it
+ if (!this.client) {
+ this.client = new WebTorrent();
+ }
+
+ // 2) If we haven’t registered the service worker yet, do it now
+ if (!this.swRegistration) {
+ this.swRegistration = await this.setupServiceWorker();
+ }
+ }
+
async waitForServiceWorkerActivation(registration) {
return new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
@@ -67,6 +92,7 @@ export class TorrentClient {
throw new Error("Service Worker not supported or disabled");
}
+ // Brave-specific logic
if (isBraveBrowser) {
this.log("Checking Brave configuration...");
if (!navigator.serviceWorker) {
@@ -78,6 +104,7 @@ export class TorrentClient {
throw new Error("Please enable WebRTC in Brave Shield settings");
}
+ // Unregister all existing service workers before installing a fresh one
const registrations = await navigator.serviceWorker.getRegistrations();
for (const reg of registrations) {
await reg.unregister();
@@ -135,8 +162,8 @@ export class TorrentClient {
// Force the SW to check for updates
registration.update();
-
this.log("Service worker ready");
+
return registration;
} catch (error) {
this.log("Service worker setup error:", error);
@@ -144,7 +171,7 @@ export class TorrentClient {
}
}
- // Minimal handleChromeTorrent
+ // Handle Chrome-based browsers
handleChromeTorrent(torrent, videoElement, resolve, reject) {
torrent.on("warning", (err) => {
if (err && typeof err.message === "string") {
@@ -204,7 +231,7 @@ export class TorrentClient {
});
}
- // Minimal handleFirefoxTorrent
+ // Handle Firefox-based browsers
handleFirefoxTorrent(torrent, videoElement, resolve, reject) {
const file = torrent.files.find((f) =>
/\.(mp4|webm|mkv)$/.test(f.name.toLowerCase())
@@ -227,7 +254,7 @@ export class TorrentClient {
});
try {
- file.streamTo(videoElement, { highWaterMark: 32 * 1024 });
+ file.streamTo(videoElement, { highWaterMark: 256 * 1024 });
this.currentTorrent = torrent;
resolve(torrent);
} catch (err) {
@@ -243,32 +270,27 @@ export class TorrentClient {
/**
* Initiates streaming of a torrent magnet to a