This commit is contained in:
Keep Creating Online
2025-01-08 22:06:26 -05:00
parent a6b66f9ca3
commit 71e95c395e
8 changed files with 1222 additions and 766 deletions

View File

@@ -69,6 +69,7 @@
</div> </div>
</main> </main>
<!-- Footer -->
<!-- Footer --> <!-- Footer -->
<footer class="mt-auto pb-8 text-center px-4"> <footer class="mt-auto pb-8 text-center px-4">
<a <a
@@ -79,6 +80,24 @@
> >
bitvid.btc.us bitvid.btc.us
</a> </a>
|
<a
href="https://bitvid.eth.limo"
class="text-gray-500 hover:text-gray-400 transition-colors duration-200"
target="_blank"
rel="noopener noreferrer"
>
bitvid.eth.limo
</a>
|
<a
href="https://bitvid.netlify.app"
class="text-gray-500 hover:text-gray-400 transition-colors duration-200"
target="_blank"
rel="noopener noreferrer"
>
bitvid.netlify.app
</a>
<div class="mt-2 space-x-4"> <div class="mt-2 space-x-4">
<a <a
href="https://github.com/PR0M3TH3AN/bitvid" href="https://github.com/PR0M3TH3AN/bitvid"
@@ -132,7 +151,10 @@
<p <p
class="mt-2 text-xs text-gray-600 font-mono break-all max-w-full overflow-hidden" class="mt-2 text-xs text-gray-600 font-mono break-all max-w-full overflow-hidden"
> >
IPNS: k51qzi5uqu5dgwr4oejq9rk41aoe9zcupenby6iqecsk5byc7rx48uecd133a1 IPNS:
<a href="ipns.html" class="text-blue-600 underline">
k51qzi5uqu5dgwr4oejq9rk41aoe9zcupenby6iqecsk5byc7rx48uecd133a1
</a>
</p> </p>
</footer> </footer>
</div> </div>

View File

@@ -1,11 +1,5 @@
![](https://bitvid.netlify.app/assets/jpg/bitvid.jpg) ![](https://bitvid.netlify.app/assets/jpg/bitvid.jpg)
BTC DNS: [bitvid.btc.us](https://bitvid.btc.us)
ETH DNS: [bitvid.eth.limo](https://bitvid.eth.limo)
**_IPNS: k51qzi5uqu5dgwr4oejq9rk41aoe9zcupenby6iqecsk5byc7rx48uecd133a1_**
# About bitvid # About bitvid
Welcome to bitvid, a new kind of video platform that puts you in control. Unlike traditional video sites that keep your content on their servers, bitvid lets videos flow directly between creators and viewers. Think of it like a digital potluck where everyone brings and shares content directly with each other! Welcome to bitvid, a new kind of video platform that puts you in control. Unlike traditional video sites that keep your content on their servers, bitvid lets videos flow directly between creators and viewers. Think of it like a digital potluck where everyone brings and shares content directly with each other!

19
src/content/ipns.md Normal file
View File

@@ -0,0 +1,19 @@
# IPNS Gateways
Below is a list of available IPNS gateways you can use with the hash `k51qzi5uqu5dgwr4oejq9rk41aoe9zcupenby6iqecsk5byc7rx48uecd133a1`:
1. **[FLK IPFS Gateway](https://k51qzi5uqu5dgwr4oejq9rk41aoe9zcupenby6iqecsk5byc7rx48uecd133a1.ipns.flk-ipfs.xyz/)**
A public gateway that resolves the provided IPNS hash.
2. **[Aragon IPFS Gateway](https://ipfs.eth.aragon.network/ipfs/bafybeih2ebj55ki3wvasj5i3rhwgjn6e72f6vxsrlrjfqvzezot2eoeqz4/)**
A gateway hosted by Aragon for IPFS content resolution.
3. **[Dweb.link Gateway](https://dweb.link/ipns/k51qzi5uqu5dgwr4oejq9rk41aoe9zcupenby6iqecsk5byc7rx48uecd133a1)**
A subdomain resolution gateway provided by Protocol Labs.
4. **[IPFS.io Gateway](https://ipfs.io/ipns/k51qzi5uqu5dgwr4oejq9rk41aoe9zcupenby6iqecsk5byc7rx48uecd133a1)**
A public gateway operated by Protocol Labs.
---
**Note:** The availability and performance of these gateways may vary.

View File

@@ -69,6 +69,7 @@
</div> </div>
</main> </main>
<!-- Footer -->
<!-- Footer --> <!-- Footer -->
<footer class="mt-auto pb-8 text-center px-4"> <footer class="mt-auto pb-8 text-center px-4">
<a <a
@@ -79,6 +80,24 @@
> >
bitvid.btc.us bitvid.btc.us
</a> </a>
|
<a
href="https://bitvid.eth.limo"
class="text-gray-500 hover:text-gray-400 transition-colors duration-200"
target="_blank"
rel="noopener noreferrer"
>
bitvid.eth.limo
</a>
|
<a
href="https://bitvid.netlify.app"
class="text-gray-500 hover:text-gray-400 transition-colors duration-200"
target="_blank"
rel="noopener noreferrer"
>
bitvid.netlify.app
</a>
<div class="mt-2 space-x-4"> <div class="mt-2 space-x-4">
<a <a
href="https://github.com/PR0M3TH3AN/bitvid" href="https://github.com/PR0M3TH3AN/bitvid"
@@ -132,7 +151,10 @@
<p <p
class="mt-2 text-xs text-gray-600 font-mono break-all max-w-full overflow-hidden" class="mt-2 text-xs text-gray-600 font-mono break-all max-w-full overflow-hidden"
> >
IPNS: k51qzi5uqu5dgwr4oejq9rk41aoe9zcupenby6iqecsk5byc7rx48uecd133a1 IPNS:
<a href="ipns.html" class="text-blue-600 underline">
k51qzi5uqu5dgwr4oejq9rk41aoe9zcupenby6iqecsk5byc7rx48uecd133a1
</a>
</p> </p>
</footer> </footer>
</div> </div>

View File

@@ -198,7 +198,9 @@
<video id="video" controls class="w-full rounded-lg shadow-md"></video> <video id="video" controls class="w-full rounded-lg shadow-md"></video>
<!-- Status and Stats --> <!-- Status and Stats -->
<div class="mt-4"> <div class="mt-4">
<div id="status" class="text-gray-700 mb-2">Initializing...</div> <div id="status" class="text-gray-700 mb-2">
Initializing... Just give it a sec.
</div>
<div class="w-full bg-gray-300 rounded-full h-2 mb-2"> <div class="w-full bg-gray-300 rounded-full h-2 mb-2">
<div <div
class="bg-blue-500 h-2 rounded-full" class="bg-blue-500 h-2 rounded-full"
@@ -232,27 +234,6 @@
<div <div
class="bg-gray-900 rounded-lg max-w-4xl w-full relative overflow-hidden" class="bg-gray-900 rounded-lg max-w-4xl w-full relative overflow-hidden"
> >
<!-- Close button -->
<button
id="closePlayer"
class="absolute top-4 right-4 z-50 text-white bg-gray-800 hover:bg-gray-700 rounded-full p-2"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M6 18L18 6M6 6l12 12"
/>
</svg>
</button>
<!-- Video container --> <!-- Video container -->
<div class="aspect-w-16 aspect-h-9"> <div class="aspect-w-16 aspect-h-9">
<video id="modalVideo" controls class="w-full rounded-t-lg"></video> <video id="modalVideo" controls class="w-full rounded-t-lg"></video>
@@ -268,7 +249,9 @@
class="flex items-center justify-between text-sm text-gray-400 mb-4" class="flex items-center justify-between text-sm text-gray-400 mb-4"
> >
<span id="videoTimestamp">just now</span> <span id="videoTimestamp">just now</span>
<div id="modalStatus" class="text-gray-300">Initializing...</div> <div id="modalStatus" class="text-gray-300">
Initializing... Just give it a sec.
</div>
</div> </div>
<!-- Creator info --> <!-- Creator info -->
@@ -320,6 +303,108 @@
seed. zap. subscribe. seed. zap. subscribe.
</h2> </h2>
</div> </div>
<!-- Disclaimer Modal -->
<div
id="disclaimerModal"
class="fixed inset-0 bg-black bg-opacity-75 flex items-center justify-center z-50 p-4"
>
<div
class="bg-gray-900 rounded-lg max-w-2xl w-full relative overflow-hidden"
>
<!-- Content -->
<div class="p-6 text-white">
<!-- Logo -->
<div class="flex justify-center mb-8">
<img
src="assets/svg/bitvid-logo-dark-mode.svg"
alt="BitVid Logo"
class="h-16"
/>
</div>
<h2 class="text-2xl font-bold mb-4 text-center">
Welcome to bitvid
</h2>
<!-- Warning Alert -->
<div
class="bg-yellow-900/20 border border-yellow-700/50 rounded-lg p-4 mb-6 flex items-start"
>
<svg
class="h-5 w-5 text-yellow-500 mt-0.5 mr-3 flex-shrink-0"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
/>
</svg>
<p class="text-yellow-200">
This platform is still in development. You may encounter bugs or
missing features.
</p>
</div>
<!-- Main Content -->
<div class="space-y-6 text-gray-300">
<p>
bitvid is a decentralized video platform where content is shared
directly between users. We want you to understand a few
important points before you continue:
</p>
<div class="space-y-4">
<div class="bg-gray-800 rounded-lg p-4">
<h3 class="text-white font-semibold mb-2">
Content Responsibility
</h3>
<p class="text-gray-400">
We don't host or control any videos shared on bitvid. All
content is the responsibility of the creators who share it.
Please follow your local laws and use the platform
responsibly.
</p>
</div>
<div class="bg-gray-800 rounded-lg p-4">
<h3 class="text-white font-semibold mb-2">Platform Status</h3>
<p class="text-gray-400">
bitvid is a work in progress. Features may change or break,
and security improvements are ongoing. Your feedback and
patience help us build a better platform.
</p>
</div>
<div class="bg-gray-800 rounded-lg p-4">
<h3 class="text-white font-semibold mb-2">Get Involved</h3>
<p class="text-gray-400">
Are you a developer? We'd love your help! Visit our GitHub
repository to contribute to building the future of
decentralized video sharing.
</p>
</div>
</div>
</div>
<!-- Action Button -->
<div class="mt-6">
<button
id="acceptDisclaimer"
class="w-full bg-blue-500 text-white px-4 py-2 rounded-md hover:bg-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 focus:ring-offset-gray-900 transition-colors duration-200"
>
I Understand
</button>
</div>
</div>
</div>
</div>
<!-- Footer -->
<footer class="mt-auto pb-8 text-center px-4"> <footer class="mt-auto pb-8 text-center px-4">
<a <a
href="https://bitvid.btc.us" href="https://bitvid.btc.us"
@@ -329,6 +414,24 @@
> >
bitvid.btc.us bitvid.btc.us
</a> </a>
|
<a
href="https://bitvid.eth.limo"
class="text-gray-500 hover:text-gray-400 transition-colors duration-200"
target="_blank"
rel="noopener noreferrer"
>
bitvid.eth.limo
</a>
|
<a
href="https://bitvid.netlify.app"
class="text-gray-500 hover:text-gray-400 transition-colors duration-200"
target="_blank"
rel="noopener noreferrer"
>
bitvid.netlify.app
</a>
<div class="mt-2 space-x-4"> <div class="mt-2 space-x-4">
<a <a
href="https://github.com/PR0M3TH3AN/bitvid" href="https://github.com/PR0M3TH3AN/bitvid"
@@ -382,7 +485,10 @@
<p <p
class="mt-2 text-xs text-gray-600 font-mono break-all max-w-full overflow-hidden" class="mt-2 text-xs text-gray-600 font-mono break-all max-w-full overflow-hidden"
> >
IPNS: k51qzi5uqu5dgwr4oejq9rk41aoe9zcupenby6iqecsk5byc7rx48uecd133a1 IPNS:
<a href="ipns.html" class="text-blue-600 underline">
k51qzi5uqu5dgwr4oejq9rk41aoe9zcupenby6iqecsk5byc7rx48uecd133a1
</a>
</p> </p>
</footer> </footer>

192
src/ipns.html Normal file
View File

@@ -0,0 +1,192 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>bitvid | About</title>
<!-- Open Graph Meta Tags -->
<meta property="og:title" content="BitVid - Markdown Viewer" />
<meta
property="og:description"
content="View and render markdown content dynamically."
/>
<meta
property="og:image"
content="https://bitvid.netlify.app/assets/jpg/bitvid.jpg"
/>
<meta property="og:url" content="https://bitvid.btc.us" />
<meta property="og:type" content="website" />
<meta property="og:locale" content="en_US" />
<!-- App Icons -->
<link rel="icon" href="/favicon.ico" sizes="any" />
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" />
<link rel="manifest" href="/site.webmanifest" />
<meta name="theme-color" content="#0f172a" />
<!-- Tailwind CSS -->
<link
href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css"
rel="stylesheet"
/>
<!-- Main Layout Styles -->
<link href="css/style.css" rel="stylesheet" />
<!-- Markdown-Specific Styles -->
<link href="css/markdown.css" rel="stylesheet" />
</head>
<body class="bg-gray-100">
<div
id="app"
class="container mx-auto px-4 py-8 min-h-screen flex flex-col"
>
<!-- Header -->
<header class="mb-8">
<div class="flex items-start">
<!-- Logo links back to index.html (or "/") -->
<a href="index.html">
<img
src="assets/svg/bitvid-logo-light-mode.svg"
alt="BitVid Logo"
class="h-16"
/>
</a>
</div>
</header>
<!-- Markdown Content Section -->
<main>
<!--
We give this section a white background and a shadow
just like you originally had for other cards.
-->
<div id="markdown-container" class="bg-white p-6 rounded-lg shadow-md">
<h2 class="text-2xl font-bold mb-4">Loading Content...</h2>
</div>
</main>
<!-- Footer -->
<footer class="mt-auto pb-8 text-center px-4">
<a
href="https://bitvid.btc.us"
class="text-gray-500 hover:text-gray-400 transition-colors duration-200"
target="_blank"
rel="noopener noreferrer"
>
bitvid.btc.us
</a>
|
<a
href="https://bitvid.eth.limo"
class="text-gray-500 hover:text-gray-400 transition-colors duration-200"
target="_blank"
rel="noopener noreferrer"
>
bitvid.eth.limo
</a>
|
<a
href="https://bitvid.netlify.app"
class="text-gray-500 hover:text-gray-400 transition-colors duration-200"
target="_blank"
rel="noopener noreferrer"
>
bitvid.netlify.app
</a>
<div class="mt-2 space-x-4">
<a
href="https://github.com/PR0M3TH3AN/bitvid"
class="text-gray-500 hover:text-gray-400 transition-colors duration-200"
target="_blank"
rel="noopener noreferrer"
>
GitHub
</a>
<a
href="https://primal.net/p/npub13yarr7j6vjqjjkahd63dmr27curypehx45ucue286ac7sft27y0srnpmpe"
class="text-gray-500 hover:text-gray-400 transition-colors duration-200"
target="_blank"
rel="noopener noreferrer"
>
Nostr
</a>
<a
href="https://habla.news/p/nprofile1qyv8wumn8ghj7un9d3shjtnndehhyapwwdhkx6tpdsq3gamnwvaz7tmjv4kxz7fwv3sk6atn9e5k7qgdwaehxw309ahx7uewd3hkcqgswaehxw309ahx7um5wgh8w6twv5q3yamnwvaz7tm0venxx6rpd9hzuur4vgqzpzf6x8a95eyp99dmwm4zmkx4a3cxgrnwdtfe3ej504m3aqjk4ugldyww3a"
class="text-gray-500 hover:text-gray-400 transition-colors duration-200"
target="_blank"
rel="noopener noreferrer"
>
Blog
</a>
<a
href="getting-started.html"
class="text-gray-500 hover:text-gray-400 transition-colors duration-200"
target="_self"
rel="noopener noreferrer"
>
Getting Started
</a>
<a
href="about.html"
class="text-gray-500 hover:text-gray-400 transition-colors duration-200"
target="_self"
rel="noopener noreferrer"
>
About
</a>
<a
href="roadmap.html"
class="text-gray-500 hover:text-gray-400 transition-colors duration-200"
target="_self"
rel="noopener noreferrer"
>
Roadmap
</a>
</div>
<p
class="mt-2 text-xs text-gray-600 font-mono break-all max-w-full overflow-hidden"
>
IPNS:
<a href="ipns.html" class="text-blue-600 underline">
k51qzi5uqu5dgwr4oejq9rk41aoe9zcupenby6iqecsk5byc7rx48uecd133a1
</a>
</p>
</footer>
</div>
<!-- Marked.js (for converting markdown to HTML) -->
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<!-- Highlight.js (optional for code block highlighting) -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/highlight.min.js"></script>
<script>
async function loadMarkdown() {
const response = await fetch("content/ipns.md");
if (response.ok) {
const markdown = await response.text();
const container = document.getElementById("markdown-container");
// Convert markdown to HTML
const html = marked.parse(markdown);
// Insert the HTML into the container
container.innerHTML = html;
// (Optional) Highlight code blocks
document.querySelectorAll("pre code").forEach((block) => {
hljs.highlightBlock(block);
});
} else {
document.getElementById("markdown-container").innerHTML =
'<p class="text-red-500">Error loading content. Please try again later.</p>';
}
}
loadMarkdown();
</script>
</body>
</html>

View File

@@ -1,63 +1,66 @@
// js/app.js // js/app.js
import { nostrClient } from './nostr.js'; import { nostrClient } from "./nostr.js";
import { torrentClient } from './webtorrent.js'; import { torrentClient } from "./webtorrent.js";
import { isDevMode } from './config.js'; import { isDevMode } from "./config.js";
import { disclaimerModal } from "./disclaimer.js";
class bitvidApp { class bitvidApp {
constructor() { constructor() {
// Authentication Elements // Authentication Elements
this.loginButton = document.getElementById('loginButton'); this.loginButton = document.getElementById("loginButton");
this.logoutButton = document.getElementById('logoutButton'); this.logoutButton = document.getElementById("logoutButton");
this.userStatus = document.getElementById('userStatus'); this.userStatus = document.getElementById("userStatus");
this.userPubKey = document.getElementById('userPubKey'); this.userPubKey = document.getElementById("userPubKey");
// Form Elements // Form Elements
this.submitForm = document.getElementById('submitForm'); this.submitForm = document.getElementById("submitForm");
this.videoFormContainer = document.getElementById('videoFormContainer'); this.videoFormContainer = document.getElementById("videoFormContainer");
// Video List Element // Video List Element
this.videoList = document.getElementById('videoList'); this.videoList = document.getElementById("videoList");
// Video Player Elements // Video Player Elements
this.playerSection = document.getElementById('playerSection'); this.playerSection = document.getElementById("playerSection");
this.videoElement = document.getElementById('video'); this.videoElement = document.getElementById("video");
this.status = document.getElementById('status'); this.status = document.getElementById("status");
this.progressBar = document.getElementById('progress'); this.progressBar = document.getElementById("progress");
this.peers = document.getElementById('peers'); this.peers = document.getElementById("peers");
this.speed = document.getElementById('speed'); this.speed = document.getElementById("speed");
this.downloaded = document.getElementById('downloaded'); this.downloaded = document.getElementById("downloaded");
// Modal Elements // Modal Elements
this.playerModal = document.getElementById('playerModal'); this.playerModal = document.getElementById("playerModal");
this.modalVideo = document.getElementById('modalVideo'); this.modalVideo = document.getElementById("modalVideo");
this.modalStatus = document.getElementById('modalStatus'); this.modalStatus = document.getElementById("modalStatus");
this.modalProgress = document.getElementById('modalProgress'); this.modalProgress = document.getElementById("modalProgress");
this.modalPeers = document.getElementById('modalPeers'); this.modalPeers = document.getElementById("modalPeers");
this.modalSpeed = document.getElementById('modalSpeed'); this.modalSpeed = document.getElementById("modalSpeed");
this.modalDownloaded = document.getElementById('modalDownloaded'); this.modalDownloaded = document.getElementById("modalDownloaded");
this.closePlayerBtn = document.getElementById('closePlayer'); this.closePlayerBtn = document.getElementById("closePlayer");
// Video Info Elements // Video Info Elements
this.videoTitle = document.getElementById('videoTitle'); this.videoTitle = document.getElementById("videoTitle");
this.videoDescription = document.getElementById('videoDescription'); this.videoDescription = document.getElementById("videoDescription");
this.videoTimestamp = document.getElementById('videoTimestamp'); this.videoTimestamp = document.getElementById("videoTimestamp");
// Creator Info Elements // Creator Info Elements
this.creatorAvatar = document.getElementById('creatorAvatar').querySelector('img'); this.creatorAvatar = document
this.creatorName = document.getElementById('creatorName'); .getElementById("creatorAvatar")
this.creatorNpub = document.getElementById('creatorNpub'); .querySelector("img");
this.creatorName = document.getElementById("creatorName");
this.creatorNpub = document.getElementById("creatorNpub");
// Notification Containers // Notification Containers
this.errorContainer = document.getElementById('errorContainer'); this.errorContainer = document.getElementById("errorContainer");
this.successContainer = document.getElementById('successContainer'); this.successContainer = document.getElementById("successContainer");
this.pubkey = null; this.pubkey = null;
this.currentMagnetUri = null; this.currentMagnetUri = null;
// ADDED FOR VERSIONING/PRIVATE/DELETE: // ADDED FOR VERSIONING/PRIVATE/DELETE:
// If you created an <input type="checkbox" id="isPrivate" /> in your HTML form: // If you created an <input type="checkbox" id="isPrivate" /> in your HTML form:
this.isPrivateCheckbox = document.getElementById('isPrivate'); this.isPrivateCheckbox = document.getElementById("isPrivate");
} }
/** /**
@@ -67,22 +70,23 @@ class bitvidApp {
async init() { async init() {
try { try {
// Hide and reset player states // Hide and reset player states
this.playerSection.style.display = 'none'; this.playerSection.style.display = "none";
this.playerModal.style.display = 'none'; this.playerModal.style.display = "none";
this.currentMagnetUri = null; this.currentMagnetUri = null;
// Initialize Nostr and check login // Initialize Nostr and check login
await nostrClient.init(); await nostrClient.init();
const savedPubKey = localStorage.getItem('userPubKey'); const savedPubKey = localStorage.getItem("userPubKey");
if (savedPubKey) { if (savedPubKey) {
this.login(savedPubKey, false); this.login(savedPubKey, false);
} }
this.setupEventListeners(); this.setupEventListeners();
disclaimerModal.show(); // Add this line here
await this.loadVideos(); await this.loadVideos();
} catch (error) { } catch (error) {
console.error('Init failed:', error); console.error("Init failed:", error);
this.showError('Failed to connect to Nostr relay'); this.showError("Failed to connect to Nostr relay");
} }
} }
@@ -90,24 +94,24 @@ class bitvidApp {
* Formats a timestamp into a "time ago" format. * Formats a timestamp into a "time ago" format.
*/ */
formatTimeAgo(timestamp) { formatTimeAgo(timestamp) {
const seconds = Math.floor((Date.now() / 1000) - timestamp); const seconds = Math.floor(Date.now() / 1000 - timestamp);
const intervals = { const intervals = {
year: 31536000, year: 31536000,
month: 2592000, month: 2592000,
week: 604800, week: 604800,
day: 86400, day: 86400,
hour: 3600, hour: 3600,
minute: 60 minute: 60,
}; };
for (const [unit, secondsInUnit] of Object.entries(intervals)) { for (const [unit, secondsInUnit] of Object.entries(intervals)) {
const interval = Math.floor(seconds / secondsInUnit); const interval = Math.floor(seconds / secondsInUnit);
if (interval >= 1) { if (interval >= 1) {
return `${interval} ${unit}${interval === 1 ? '' : 's'} ago`; return `${interval} ${unit}${interval === 1 ? "" : "s"} ago`;
} }
} }
return 'just now'; return "just now";
} }
/** /**
@@ -115,34 +119,34 @@ class bitvidApp {
*/ */
setupEventListeners() { setupEventListeners() {
// Login Button // Login Button
this.loginButton.addEventListener('click', async () => { this.loginButton.addEventListener("click", async () => {
try { try {
const pubkey = await nostrClient.login(); const pubkey = await nostrClient.login();
this.login(pubkey, true); this.login(pubkey, true);
} catch (error) { } catch (error) {
this.log('Login failed:', error); this.log("Login failed:", error);
this.showError('Failed to login. Please try again.'); this.showError("Failed to login. Please try again.");
} }
}); });
// Logout Button // Logout Button
this.logoutButton.addEventListener('click', () => { this.logoutButton.addEventListener("click", () => {
this.logout(); this.logout();
}); });
// Form submission // Form submission
this.submitForm.addEventListener('submit', (e) => this.handleSubmit(e)); this.submitForm.addEventListener("submit", (e) => this.handleSubmit(e));
// Close Modal Button // Close Modal Button
if (this.closePlayerBtn) { if (this.closePlayerBtn) {
this.closePlayerBtn.addEventListener('click', async () => { this.closePlayerBtn.addEventListener("click", async () => {
await this.hideModal(); await this.hideModal();
}); });
} }
// Close Modal by clicking outside content // Close Modal by clicking outside content
if (this.playerModal) { if (this.playerModal) {
this.playerModal.addEventListener('click', async (e) => { this.playerModal.addEventListener("click", async (e) => {
if (e.target === this.playerModal) { if (e.target === this.playerModal) {
await this.hideModal(); await this.hideModal();
} }
@@ -150,41 +154,45 @@ class bitvidApp {
} }
// Video error handling // Video error handling
this.videoElement.addEventListener('error', (e) => { this.videoElement.addEventListener("error", (e) => {
const error = e.target.error; const error = e.target.error;
this.log('Video error:', error); this.log("Video error:", error);
if (error) { if (error) {
this.showError(`Video playback error: ${error.message || 'Unknown error'}`); this.showError(
`Video playback error: ${error.message || "Unknown error"}`
);
} }
}); });
// Detailed Modal Video Event Listeners // Detailed Modal Video Event Listeners
if (this.modalVideo) { if (this.modalVideo) {
this.modalVideo.addEventListener('error', (e) => { this.modalVideo.addEventListener("error", (e) => {
const error = e.target.error; const error = e.target.error;
this.log('Modal video error:', error); this.log("Modal video error:", error);
if (error) { if (error) {
this.log('Error code:', error.code); this.log("Error code:", error.code);
this.log('Error message:', error.message); this.log("Error message:", error.message);
this.showError(`Video playback error: ${error.message || 'Unknown error'}`); this.showError(
`Video playback error: ${error.message || "Unknown error"}`
);
} }
}); });
this.modalVideo.addEventListener('loadstart', () => { this.modalVideo.addEventListener("loadstart", () => {
this.log('Video loadstart event fired'); this.log("Video loadstart event fired");
}); });
this.modalVideo.addEventListener('loadedmetadata', () => { this.modalVideo.addEventListener("loadedmetadata", () => {
this.log('Video loadedmetadata event fired'); this.log("Video loadedmetadata event fired");
}); });
this.modalVideo.addEventListener('canplay', () => { this.modalVideo.addEventListener("canplay", () => {
this.log('Video canplay event fired'); this.log("Video canplay event fired");
}); });
} }
// Cleanup on page unload // Cleanup on page unload
window.addEventListener('beforeunload', async () => { window.addEventListener("beforeunload", async () => {
await this.cleanup(); await this.cleanup();
}); });
} }
@@ -194,15 +202,15 @@ class bitvidApp {
*/ */
login(pubkey, saveToStorage = true) { login(pubkey, saveToStorage = true) {
this.pubkey = pubkey; this.pubkey = pubkey;
this.loginButton.classList.add('hidden'); this.loginButton.classList.add("hidden");
this.logoutButton.classList.remove('hidden'); this.logoutButton.classList.remove("hidden");
this.userStatus.classList.remove('hidden'); this.userStatus.classList.remove("hidden");
this.userPubKey.textContent = pubkey; this.userPubKey.textContent = pubkey;
this.videoFormContainer.classList.remove('hidden'); this.videoFormContainer.classList.remove("hidden");
this.log(`User logged in as: ${pubkey}`); this.log(`User logged in as: ${pubkey}`);
if (saveToStorage) { if (saveToStorage) {
localStorage.setItem('userPubKey', pubkey); localStorage.setItem("userPubKey", pubkey);
} }
} }
@@ -212,13 +220,13 @@ class bitvidApp {
logout() { logout() {
nostrClient.logout(); nostrClient.logout();
this.pubkey = null; this.pubkey = null;
this.loginButton.classList.remove('hidden'); this.loginButton.classList.remove("hidden");
this.logoutButton.classList.add('hidden'); this.logoutButton.classList.add("hidden");
this.userStatus.classList.add('hidden'); this.userStatus.classList.add("hidden");
this.userPubKey.textContent = ''; this.userPubKey.textContent = "";
this.videoFormContainer.classList.add('hidden'); this.videoFormContainer.classList.add("hidden");
localStorage.removeItem('userPubKey'); localStorage.removeItem("userPubKey");
this.log('User logged out.'); this.log("User logged out.");
} }
/** /**
@@ -228,17 +236,17 @@ class bitvidApp {
try { try {
if (this.videoElement) { if (this.videoElement) {
this.videoElement.pause(); this.videoElement.pause();
this.videoElement.src = ''; this.videoElement.src = "";
this.videoElement.load(); this.videoElement.load();
} }
if (this.modalVideo) { if (this.modalVideo) {
this.modalVideo.pause(); this.modalVideo.pause();
this.modalVideo.src = ''; this.modalVideo.src = "";
this.modalVideo.load(); this.modalVideo.load();
} }
await torrentClient.cleanup(); await torrentClient.cleanup();
} catch (error) { } catch (error) {
this.log('Cleanup error:', error); this.log("Cleanup error:", error);
} }
} }
@@ -247,7 +255,7 @@ class bitvidApp {
*/ */
async hideVideoPlayer() { async hideVideoPlayer() {
await this.cleanup(); await this.cleanup();
this.playerSection.classList.add('hidden'); this.playerSection.classList.add("hidden");
} }
/** /**
@@ -255,8 +263,8 @@ class bitvidApp {
*/ */
async hideModal() { async hideModal() {
await this.cleanup(); await this.cleanup();
this.playerModal.style.display = 'none'; this.playerModal.style.display = "none";
this.playerModal.classList.add('hidden'); this.playerModal.classList.add("hidden");
} }
/** /**
@@ -266,11 +274,11 @@ class bitvidApp {
e.preventDefault(); e.preventDefault();
if (!this.pubkey) { if (!this.pubkey) {
this.showError('Please login to post a video.'); this.showError("Please login to post a video.");
return; return;
} }
const descriptionElement = document.getElementById('description'); const descriptionElement = document.getElementById("description");
// ADDED FOR VERSIONING/PRIVATE/DELETE: // ADDED FOR VERSIONING/PRIVATE/DELETE:
// If you have a checkbox with id="isPrivate" in HTML // If you have a checkbox with id="isPrivate" in HTML
@@ -280,18 +288,18 @@ class bitvidApp {
const formData = { const formData = {
version: 2, // We set the version to 2 for new posts version: 2, // We set the version to 2 for new posts
title: document.getElementById('title')?.value.trim() || '', title: document.getElementById("title")?.value.trim() || "",
magnet: document.getElementById('magnet')?.value.trim() || '', magnet: document.getElementById("magnet")?.value.trim() || "",
thumbnail: document.getElementById('thumbnail')?.value.trim() || '', thumbnail: document.getElementById("thumbnail")?.value.trim() || "",
description: descriptionElement?.value.trim() || '', description: descriptionElement?.value.trim() || "",
mode: isDevMode ? 'dev' : 'live', mode: isDevMode ? "dev" : "live",
isPrivate // new field to handle private listings isPrivate, // new field to handle private listings
}; };
this.log('Form Data Collected:', formData); this.log("Form Data Collected:", formData);
if (!formData.title || !formData.magnet) { if (!formData.title || !formData.magnet) {
this.showError('Title and Magnet URI are required.'); this.showError("Title and Magnet URI are required.");
return; return;
} }
@@ -305,10 +313,10 @@ class bitvidApp {
} }
await this.loadVideos(); await this.loadVideos();
this.showSuccess('Video shared successfully!'); this.showSuccess("Video shared successfully!");
} catch (error) { } catch (error) {
this.log('Failed to publish video:', error.message); this.log("Failed to publish video:", error.message);
this.showError('Failed to share video. Please try again later.'); this.showError("Failed to share video. Please try again later.");
} }
} }
@@ -318,11 +326,11 @@ class bitvidApp {
async loadVideos() { async loadVideos() {
try { try {
const videos = await nostrClient.fetchVideos(); const videos = await nostrClient.fetchVideos();
this.log('Fetched videos (raw):', videos); this.log("Fetched videos (raw):", videos);
if (!videos) { if (!videos) {
this.log('No videos received'); this.log("No videos received");
throw new Error('No videos received from relays'); throw new Error("No videos received from relays");
} }
// Convert to array if not already // Convert to array if not already
@@ -331,17 +339,17 @@ class bitvidApp {
// **Filter** so we only show: // **Filter** so we only show:
// - isPrivate === false (public videos) // - isPrivate === false (public videos)
// - or isPrivate === true but pubkey === this.pubkey // - or isPrivate === true but pubkey === this.pubkey
const displayedVideos = videosArray.filter(video => { const displayedVideos = videosArray.filter((video) => {
if (!video.isPrivate) { if (!video.isPrivate) {
// Public video => show it // Public video => show it
return true; return true;
} }
// Else it's private; only show if it's owned by the logged-in user // Else it's private; only show if it's owned by the logged-in user
return (this.pubkey && video.pubkey === this.pubkey); return this.pubkey && video.pubkey === this.pubkey;
}); });
if (displayedVideos.length === 0) { if (displayedVideos.length === 0) {
this.log('No valid videos found after filtering.'); this.log("No valid videos found after filtering.");
this.videoList.innerHTML = ` this.videoList.innerHTML = `
<p class="text-center text-gray-500"> <p class="text-center text-gray-500">
No public videos available yet. Be the first to upload one! No public videos available yet. Be the first to upload one!
@@ -349,7 +357,7 @@ class bitvidApp {
return; return;
} }
this.log('Processing filtered videos:', displayedVideos); this.log("Processing filtered videos:", displayedVideos);
displayedVideos.forEach((video, index) => { displayedVideos.forEach((video, index) => {
this.log(`Video ${index} details:`, { this.log(`Video ${index} details:`, {
@@ -358,7 +366,7 @@ class bitvidApp {
magnet: video.magnet, magnet: video.magnet,
isPrivate: video.isPrivate, isPrivate: video.isPrivate,
pubkey: video.pubkey, pubkey: video.pubkey,
created_at: video.created_at created_at: video.created_at,
}); });
}); });
@@ -366,8 +374,10 @@ class bitvidApp {
this.renderVideoList(displayedVideos); this.renderVideoList(displayedVideos);
this.log(`Rendered ${displayedVideos.length} videos successfully`); this.log(`Rendered ${displayedVideos.length} videos successfully`);
} catch (error) { } catch (error) {
this.log('Failed to fetch videos:', error); this.log("Failed to fetch videos:", error);
this.showError('An error occurred while loading videos. Please try again later.'); this.showError(
"An error occurred while loading videos. Please try again later."
);
this.videoList.innerHTML = ` this.videoList.innerHTML = `
<p class="text-center text-gray-500"> <p class="text-center text-gray-500">
No videos available at the moment. Please try again later. No videos available at the moment. Please try again later.
@@ -381,14 +391,14 @@ class bitvidApp {
*/ */
async renderVideoList(videos) { async renderVideoList(videos) {
try { try {
console.log('RENDER VIDEO LIST - Start', { console.log("RENDER VIDEO LIST - Start", {
videosReceived: videos, videosReceived: videos,
videosCount: videos ? videos.length : 'N/A', videosCount: videos ? videos.length : "N/A",
videosType: typeof videos videosType: typeof videos,
}); });
if (!videos) { if (!videos) {
console.error('NO VIDEOS RECEIVED'); console.error("NO VIDEOS RECEIVED");
this.videoList.innerHTML = `<p class="text-center text-gray-500">No videos found.</p>`; this.videoList.innerHTML = `<p class="text-center text-gray-500">No videos found.</p>`;
return; return;
} }
@@ -396,7 +406,7 @@ class bitvidApp {
const videoArray = Array.isArray(videos) ? videos : [videos]; const videoArray = Array.isArray(videos) ? videos : [videos];
if (videoArray.length === 0) { if (videoArray.length === 0) {
console.error('VIDEO ARRAY IS EMPTY'); console.error("VIDEO ARRAY IS EMPTY");
this.videoList.innerHTML = `<p class="text-center text-gray-500">No videos available.</p>`; this.videoList.innerHTML = `<p class="text-center text-gray-500">No videos available.</p>`;
return; return;
} }
@@ -406,58 +416,62 @@ class bitvidApp {
// Prepare to fetch user profiles // Prepare to fetch user profiles
const userProfiles = new Map(); const userProfiles = new Map();
const uniquePubkeys = [...new Set(videoArray.map(v => v.pubkey))]; const uniquePubkeys = [...new Set(videoArray.map((v) => v.pubkey))];
for (const pubkey of uniquePubkeys) { for (const pubkey of uniquePubkeys) {
try { try {
const userEvents = await nostrClient.pool.list(nostrClient.relays, [{ const userEvents = await nostrClient.pool.list(nostrClient.relays, [
{
kinds: [0], kinds: [0],
authors: [pubkey], authors: [pubkey],
limit: 1 limit: 1,
}]); },
]);
if (userEvents[0]?.content) { if (userEvents[0]?.content) {
const profile = JSON.parse(userEvents[0].content); const profile = JSON.parse(userEvents[0].content);
userProfiles.set(pubkey, { userProfiles.set(pubkey, {
name: profile.name || profile.display_name || 'Unknown', name: profile.name || profile.display_name || "Unknown",
picture: profile.picture || `https://robohash.org/${pubkey}` picture: profile.picture || `https://robohash.org/${pubkey}`,
}); });
} else { } else {
userProfiles.set(pubkey, { userProfiles.set(pubkey, {
name: 'Unknown', name: "Unknown",
picture: `https://robohash.org/${pubkey}` picture: `https://robohash.org/${pubkey}`,
}); });
} }
} catch (error) { } catch (error) {
console.error(`Profile fetch error for ${pubkey}:`, error); console.error(`Profile fetch error for ${pubkey}:`, error);
userProfiles.set(pubkey, { userProfiles.set(pubkey, {
name: 'Unknown', name: "Unknown",
picture: `https://robohash.org/${pubkey}` picture: `https://robohash.org/${pubkey}`,
}); });
} }
} }
// Build HTML for each video // Build HTML for each video
const renderedVideos = videoArray.map((video, index) => { const renderedVideos = videoArray
.map((video, index) => {
try { try {
if (!this.validateVideo(video, index)) { if (!this.validateVideo(video, index)) {
console.error(`Invalid video: ${video.title}`); console.error(`Invalid video: ${video.title}`);
return ''; return "";
} }
const profile = userProfiles.get(video.pubkey) || { const profile = userProfiles.get(video.pubkey) || {
name: 'Unknown', name: "Unknown",
picture: `https://robohash.org/${video.pubkey}` picture: `https://robohash.org/${video.pubkey}`,
}; };
const timeAgo = this.formatTimeAgo(video.created_at); const timeAgo = this.formatTimeAgo(video.created_at);
// If user is the owner // If user is the owner
const canEdit = (video.pubkey === this.pubkey); const canEdit = video.pubkey === this.pubkey;
// If it's private + user owns it => highlight with a special border // If it's private + user owns it => highlight with a special border
const highlightClass = (video.isPrivate && canEdit) const highlightClass =
? 'border-2 border-yellow-500' video.isPrivate && canEdit
: 'border-none'; // normal case ? "border-2 border-yellow-500"
: "border-none"; // normal case
// Gear menu (unchanged) // Gear menu (unchanged)
const gearMenu = canEdit const gearMenu = canEdit
@@ -496,7 +510,7 @@ class bitvidApp {
</div> </div>
</div> </div>
` `
: ''; : "";
return ` return `
<div class="video-card bg-gray-900 rounded-lg overflow-hidden shadow-lg hover:shadow-2xl transition-all duration-300 ${highlightClass}"> <div class="video-card bg-gray-900 rounded-lg overflow-hidden shadow-lg hover:shadow-2xl transition-all duration-300 ${highlightClass}">
@@ -504,7 +518,9 @@ class bitvidApp {
<!-- VIDEO THUMBNAIL --> <!-- VIDEO THUMBNAIL -->
<div <div
class="aspect-w-16 aspect-h-9 bg-gray-800 cursor-pointer relative group" class="aspect-w-16 aspect-h-9 bg-gray-800 cursor-pointer relative group"
onclick="app.playVideo('${encodeURIComponent(video.magnet)}')" onclick="app.playVideo('${encodeURIComponent(
video.magnet
)}')"
> >
${ ${
video.thumbnail video.thumbnail
@@ -530,7 +546,9 @@ class bitvidApp {
<!-- TITLE --> <!-- TITLE -->
<h3 <h3
class="text-lg font-bold text-white line-clamp-2 hover:text-blue-400 cursor-pointer mb-3" class="text-lg font-bold text-white line-clamp-2 hover:text-blue-400 cursor-pointer mb-3"
onclick="app.playVideo('${encodeURIComponent(video.magnet)}')" onclick="app.playVideo('${encodeURIComponent(
video.magnet
)}')"
> >
${this.escapeHTML(video.title)} ${this.escapeHTML(video.title)}
</h3> </h3>
@@ -541,7 +559,9 @@ class bitvidApp {
<div class="flex items-center space-x-3"> <div class="flex items-center space-x-3">
<div class="w-8 h-8 rounded-full bg-gray-700 overflow-hidden"> <div class="w-8 h-8 rounded-full bg-gray-700 overflow-hidden">
<img <img
src="${this.escapeHTML(profile.picture)}" src="${this.escapeHTML(
profile.picture
)}"
alt="${profile.name}" alt="${profile.name}"
class="w-full h-full object-cover" class="w-full h-full object-cover"
> >
@@ -563,22 +583,22 @@ class bitvidApp {
`; `;
} catch (error) { } catch (error) {
console.error(`Error processing video ${index}:`, error); console.error(`Error processing video ${index}:`, error);
return ''; return "";
} }
}).filter(html => html.length > 0); })
.filter((html) => html.length > 0);
console.log('Rendered videos:', renderedVideos.length); console.log("Rendered videos:", renderedVideos.length);
if (renderedVideos.length === 0) { if (renderedVideos.length === 0) {
this.videoList.innerHTML = `<p class="text-center text-gray-500">No valid videos to display.</p>`; this.videoList.innerHTML = `<p class="text-center text-gray-500">No valid videos to display.</p>`;
return; return;
} }
this.videoList.innerHTML = renderedVideos.join(''); this.videoList.innerHTML = renderedVideos.join("");
console.log('Videos rendered successfully'); console.log("Videos rendered successfully");
} catch (error) { } catch (error) {
console.error('Rendering error:', error); console.error("Rendering error:", error);
this.videoList.innerHTML = `<p class="text-center text-gray-500">Error loading videos.</p>`; this.videoList.innerHTML = `<p class="text-center text-gray-500">Error loading videos.</p>`;
} }
} }
@@ -589,19 +609,25 @@ class bitvidApp {
validateVideo(video, index) { validateVideo(video, index) {
const validationResults = { const validationResults = {
hasId: Boolean(video?.id), hasId: Boolean(video?.id),
isValidId: typeof video?.id === 'string' && video.id.trim().length > 0, isValidId: typeof video?.id === "string" && video.id.trim().length > 0,
hasVideo: Boolean(video), hasVideo: Boolean(video),
hasTitle: Boolean(video?.title), hasTitle: Boolean(video?.title),
hasMagnet: Boolean(video?.magnet), hasMagnet: Boolean(video?.magnet),
hasMode: Boolean(video?.mode), hasMode: Boolean(video?.mode),
hasPubkey: Boolean(video?.pubkey), hasPubkey: Boolean(video?.pubkey),
isValidTitle: typeof video?.title === 'string' && video.title.length > 0, isValidTitle: typeof video?.title === "string" && video.title.length > 0,
isValidMagnet: typeof video?.magnet === 'string' && video.magnet.length > 0, isValidMagnet:
isValidMode: typeof video?.mode === 'string' && ['dev', 'live'].includes(video.mode) typeof video?.magnet === "string" && video.magnet.length > 0,
isValidMode:
typeof video?.mode === "string" && ["dev", "live"].includes(video.mode),
}; };
const passed = Object.values(validationResults).every(Boolean); const passed = Object.values(validationResults).every(Boolean);
console.log(`Video ${video?.title} validation results:`, validationResults, passed ? 'PASSED' : 'FAILED'); console.log(
`Video ${video?.title} validation results:`,
validationResults,
passed ? "PASSED" : "FAILED"
);
return passed; return passed;
} }
@@ -610,14 +636,14 @@ class bitvidApp {
* Gets a user-friendly error message. * Gets a user-friendly error message.
*/ */
getErrorMessage(error) { getErrorMessage(error) {
if (error.message.includes('404')) { if (error.message.includes("404")) {
return 'Service worker not found. Please check server configuration.'; return "Service worker not found. Please check server configuration.";
} else if (error.message.includes('Brave')) { } else if (error.message.includes("Brave")) {
return 'Please disable Brave Shields for this site to play videos.'; return "Please disable Brave Shields for this site to play videos.";
} else if (error.message.includes('timeout')) { } else if (error.message.includes("timeout")) {
return 'Connection timeout. Please check your internet connection.'; return "Connection timeout. Please check your internet connection.";
} else { } else {
return 'Failed to play video. Please try again.'; return "Failed to play video. Please try again.";
} }
} }
@@ -627,10 +653,10 @@ class bitvidApp {
showError(message) { showError(message) {
if (this.errorContainer) { if (this.errorContainer) {
this.errorContainer.textContent = message; this.errorContainer.textContent = message;
this.errorContainer.classList.remove('hidden'); this.errorContainer.classList.remove("hidden");
setTimeout(() => { setTimeout(() => {
this.errorContainer.classList.add('hidden'); this.errorContainer.classList.add("hidden");
this.errorContainer.textContent = ''; this.errorContainer.textContent = "";
}, 5000); }, 5000);
} else { } else {
alert(message); alert(message);
@@ -643,10 +669,10 @@ class bitvidApp {
showSuccess(message) { showSuccess(message) {
if (this.successContainer) { if (this.successContainer) {
this.successContainer.textContent = message; this.successContainer.textContent = message;
this.successContainer.classList.remove('hidden'); this.successContainer.classList.remove("hidden");
setTimeout(() => { setTimeout(() => {
this.successContainer.classList.add('hidden'); this.successContainer.classList.add("hidden");
this.successContainer.textContent = ''; this.successContainer.textContent = "";
}, 5000); }, 5000);
} else { } else {
alert(message); alert(message);
@@ -679,33 +705,37 @@ class bitvidApp {
async playVideo(magnetURI) { async playVideo(magnetURI) {
try { try {
if (!magnetURI) { if (!magnetURI) {
this.showError('Invalid Magnet URI.'); this.showError("Invalid Magnet URI.");
return; return;
} }
const decodedMagnet = decodeURIComponent(magnetURI); const decodedMagnet = decodeURIComponent(magnetURI);
if (this.currentMagnetUri === decodedMagnet) { if (this.currentMagnetUri === decodedMagnet) {
this.log('Same video requested - already playing'); this.log("Same video requested - already playing");
return; return;
} }
this.currentMagnetUri = decodedMagnet; this.currentMagnetUri = decodedMagnet;
this.playerModal.style.display = 'flex'; this.playerModal.style.display = "flex";
this.playerModal.classList.remove('hidden'); this.playerModal.classList.remove("hidden");
// Re-fetch the latest from relays // Re-fetch the latest from relays
const videos = await nostrClient.fetchVideos(); const videos = await nostrClient.fetchVideos();
const video = videos.find(v => v.magnet === decodedMagnet); const video = videos.find((v) => v.magnet === decodedMagnet);
if (!video) { if (!video) {
this.showError('Video data not found.'); this.showError("Video data not found.");
return; return;
} }
// Decrypt only once if user owns it // Decrypt only once if user owns it
if (video.isPrivate && video.pubkey === this.pubkey && !video.alreadyDecrypted) { if (
this.log('User owns a private video => decrypting magnet link...'); video.isPrivate &&
video.pubkey === this.pubkey &&
!video.alreadyDecrypted
) {
this.log("User owns a private video => decrypting magnet link...");
video.magnet = fakeDecrypt(video.magnet); video.magnet = fakeDecrypt(video.magnet);
// Mark it so we don't do it again // Mark it so we don't do it again
video.alreadyDecrypted = true; video.alreadyDecrypted = true;
@@ -714,44 +744,53 @@ class bitvidApp {
const finalMagnet = video.magnet; const finalMagnet = video.magnet;
// Profile fetch // Profile fetch
let creatorProfile = { name: 'Unknown', picture: `https://robohash.org/${video.pubkey}` }; let creatorProfile = {
name: "Unknown",
picture: `https://robohash.org/${video.pubkey}`,
};
try { try {
const userEvents = await nostrClient.pool.list(nostrClient.relays, [{ const userEvents = await nostrClient.pool.list(nostrClient.relays, [
{
kinds: [0], kinds: [0],
authors: [video.pubkey], authors: [video.pubkey],
limit: 1 limit: 1,
}]); },
]);
// Ensure userEvents isn't empty before accessing [0] // Ensure userEvents isn't empty before accessing [0]
if (userEvents.length > 0 && userEvents[0]?.content) { if (userEvents.length > 0 && userEvents[0]?.content) {
const profile = JSON.parse(userEvents[0].content); const profile = JSON.parse(userEvents[0].content);
creatorProfile = { creatorProfile = {
name: profile.name || profile.display_name || 'Unknown', name: profile.name || profile.display_name || "Unknown",
picture: profile.picture || `https://robohash.org/${video.pubkey}` picture: profile.picture || `https://robohash.org/${video.pubkey}`,
}; };
} }
} catch (error) { } catch (error) {
this.log('Error fetching creator profile:', error); this.log("Error fetching creator profile:", error);
} }
let creatorNpub = 'Unknown'; let creatorNpub = "Unknown";
try { try {
creatorNpub = window.NostrTools.nip19.npubEncode(video.pubkey); creatorNpub = window.NostrTools.nip19.npubEncode(video.pubkey);
} catch (error) { } catch (error) {
this.log('Error converting pubkey to npub:', error); this.log("Error converting pubkey to npub:", error);
creatorNpub = video.pubkey; creatorNpub = video.pubkey;
} }
this.videoTitle.textContent = video.title || 'Untitled'; this.videoTitle.textContent = video.title || "Untitled";
this.videoDescription.textContent = video.description || 'No description available.'; this.videoDescription.textContent =
video.description || "No description available.";
this.videoTimestamp.textContent = this.formatTimeAgo(video.created_at); this.videoTimestamp.textContent = this.formatTimeAgo(video.created_at);
this.creatorName.textContent = creatorProfile.name; this.creatorName.textContent = creatorProfile.name;
this.creatorNpub.textContent = `${creatorNpub.slice(0, 8)}...${creatorNpub.slice(-4)}`; this.creatorNpub.textContent = `${creatorNpub.slice(
0,
8
)}...${creatorNpub.slice(-4)}`;
this.creatorAvatar.src = creatorProfile.picture; this.creatorAvatar.src = creatorProfile.picture;
this.creatorAvatar.alt = creatorProfile.name; this.creatorAvatar.alt = creatorProfile.name;
this.log('Starting video stream with:', finalMagnet); this.log("Starting video stream with:", finalMagnet);
await torrentClient.streamVideo(finalMagnet, this.modalVideo); await torrentClient.streamVideo(finalMagnet, this.modalVideo);
const updateInterval = setInterval(() => { const updateInterval = setInterval(() => {
@@ -760,21 +799,21 @@ class bitvidApp {
return; return;
} }
const status = document.getElementById('status'); const status = document.getElementById("status");
const progress = document.getElementById('progress'); const progress = document.getElementById("progress");
const peers = document.getElementById('peers'); const peers = document.getElementById("peers");
const speed = document.getElementById('speed'); const speed = document.getElementById("speed");
const downloaded = document.getElementById('downloaded'); const downloaded = document.getElementById("downloaded");
if (status) this.modalStatus.textContent = status.textContent; if (status) this.modalStatus.textContent = status.textContent;
if (progress) this.modalProgress.style.width = progress.style.width; if (progress) this.modalProgress.style.width = progress.style.width;
if (peers) this.modalPeers.textContent = peers.textContent; if (peers) this.modalPeers.textContent = peers.textContent;
if (speed) this.modalSpeed.textContent = speed.textContent; if (speed) this.modalSpeed.textContent = speed.textContent;
if (downloaded) this.modalDownloaded.textContent = downloaded.textContent; if (downloaded)
this.modalDownloaded.textContent = downloaded.textContent;
}, 1000); }, 1000);
} catch (error) { } catch (error) {
this.log('Error in playVideo:', error); this.log("Error in playVideo:", error);
this.showError(`Playback error: ${error.message}`); this.showError(`Playback error: ${error.message}`);
} }
} }
@@ -785,11 +824,16 @@ class bitvidApp {
this.modalStatus.textContent = torrent.status; this.modalStatus.textContent = torrent.status;
this.modalProgress.style.width = `${(torrent.progress * 100).toFixed(2)}%`; this.modalProgress.style.width = `${(torrent.progress * 100).toFixed(2)}%`;
this.modalPeers.textContent = `Peers: ${torrent.numPeers}`; this.modalPeers.textContent = `Peers: ${torrent.numPeers}`;
this.modalSpeed.textContent = `${(torrent.downloadSpeed / 1024).toFixed(2)} KB/s`; this.modalSpeed.textContent = `${(torrent.downloadSpeed / 1024).toFixed(
this.modalDownloaded.textContent = `${(torrent.downloaded / (1024 * 1024)).toFixed(2)} MB / ${(torrent.length / (1024 * 1024)).toFixed(2)} MB`; 2
)} KB/s`;
this.modalDownloaded.textContent = `${(
torrent.downloaded /
(1024 * 1024)
).toFixed(2)} MB / ${(torrent.length / (1024 * 1024)).toFixed(2)} MB`;
if (torrent.ready) { if (torrent.ready) {
this.modalStatus.textContent = 'Ready to play'; this.modalStatus.textContent = "Ready to play";
} else { } else {
setTimeout(() => this.updateTorrentStatus(torrent), 1000); setTimeout(() => this.updateTorrentStatus(torrent), 1000);
} }
@@ -805,28 +849,52 @@ class bitvidApp {
const video = videos[index]; const video = videos[index];
if (!this.pubkey) { if (!this.pubkey) {
this.showError('Please login to edit videos.'); this.showError("Please login to edit videos.");
return; return;
} }
if (video.pubkey !== this.pubkey) { if (video.pubkey !== this.pubkey) {
this.showError('You do not own this video.'); this.showError("You do not own this video.");
return; return;
} }
// Prompt for new fields or keep old // Prompt for new fields or keep old
const newTitle = prompt('New Title? (Leave blank to keep existing)', video.title); const newTitle = prompt(
const newMagnet = prompt('New Magnet? (Leave blank to keep existing)', video.magnet); "New Title? (Leave blank to keep existing)",
const newThumbnail = prompt('New Thumbnail? (Leave blank to keep existing)', video.thumbnail); video.title
const newDescription = prompt('New Description? (Leave blank to keep existing)', video.description); );
const newMagnet = prompt(
"New Magnet? (Leave blank to keep existing)",
video.magnet
);
const newThumbnail = prompt(
"New Thumbnail? (Leave blank to keep existing)",
video.thumbnail
);
const newDescription = prompt(
"New Description? (Leave blank to keep existing)",
video.description
);
// Ask user if they want the note private or public // Ask user if they want the note private or public
const wantPrivate = confirm('Make this video private? OK=Yes, Cancel=No'); const wantPrivate = confirm("Make this video private? OK=Yes, Cancel=No");
// Fallback to old if user typed nothing // Fallback to old if user typed nothing
const title = (newTitle === null || newTitle.trim() === '') ? video.title : newTitle.trim(); const title =
const magnet = (newMagnet === null || newMagnet.trim() === '') ? video.magnet : newMagnet.trim(); newTitle === null || newTitle.trim() === ""
const thumbnail = (newThumbnail === null || newThumbnail.trim() === '') ? video.thumbnail : newThumbnail.trim(); ? video.title
const description = (newDescription === null || newDescription.trim() === '') ? video.description : newDescription.trim(); : newTitle.trim();
const magnet =
newMagnet === null || newMagnet.trim() === ""
? video.magnet
: newMagnet.trim();
const thumbnail =
newThumbnail === null || newThumbnail.trim() === ""
? video.thumbnail
: newThumbnail.trim();
const description =
newDescription === null || newDescription.trim() === ""
? video.description
: newDescription.trim();
// Build final updated data // Build final updated data
const updatedData = { const updatedData = {
@@ -836,21 +904,21 @@ class bitvidApp {
magnet, magnet,
thumbnail, thumbnail,
description, description,
mode: isDevMode ? 'dev' : 'live' mode: isDevMode ? "dev" : "live",
}; };
// Edit // Edit
const originalEvent = { const originalEvent = {
id: video.id, id: video.id,
pubkey: video.pubkey, pubkey: video.pubkey,
tags: video.tags tags: video.tags,
}; };
await nostrClient.editVideo(originalEvent, updatedData, this.pubkey); await nostrClient.editVideo(originalEvent, updatedData, this.pubkey);
this.showSuccess('Video updated successfully!'); this.showSuccess("Video updated successfully!");
await this.loadVideos(); await this.loadVideos();
} catch (err) { } catch (err) {
this.log('Failed to edit video:', err.message); this.log("Failed to edit video:", err.message);
this.showError('Failed to edit video. Please try again later.'); this.showError("Failed to edit video. Please try again later.");
} }
} }
@@ -864,30 +932,34 @@ class bitvidApp {
const video = videos[index]; const video = videos[index];
if (!this.pubkey) { if (!this.pubkey) {
this.showError('Please login to delete videos.'); this.showError("Please login to delete videos.");
return; return;
} }
if (video.pubkey !== this.pubkey) { if (video.pubkey !== this.pubkey) {
this.showError('You do not own this video.'); this.showError("You do not own this video.");
return; return;
} }
if (!confirm(`Are you sure you want to delete "${video.title}"? This action cannot be undone.`)) { if (
!confirm(
`Are you sure you want to delete "${video.title}"? This action cannot be undone.`
)
) {
return; return;
} }
const originalEvent = { const originalEvent = {
id: video.id, id: video.id,
pubkey: video.pubkey, pubkey: video.pubkey,
tags: video.tags tags: video.tags,
}; };
await nostrClient.deleteVideo(originalEvent, this.pubkey); await nostrClient.deleteVideo(originalEvent, this.pubkey);
this.showSuccess('Video deleted (hidden) successfully!'); this.showSuccess("Video deleted (hidden) successfully!");
await this.loadVideos(); await this.loadVideos();
} catch (err) { } catch (err) {
this.log('Failed to delete video:', err.message); this.log("Failed to delete video:", err.message);
this.showError('Failed to delete video. Please try again later.'); this.showError("Failed to delete video. Please try again later.");
} }
} }
} }

29
src/js/disclaimer.js Normal file
View File

@@ -0,0 +1,29 @@
class DisclaimerModal {
constructor() {
this.modal = document.getElementById("disclaimerModal");
this.acceptButton = document.getElementById("acceptDisclaimer");
this.hasSeenDisclaimer = localStorage.getItem("hasSeenDisclaimer");
this.setupEventListeners();
}
setupEventListeners() {
const closeModal = () => {
this.modal.style.display = "none";
document.body.style.overflow = "unset";
localStorage.setItem("hasSeenDisclaimer", "true");
};
// Only keep the accept button event listener
this.acceptButton.addEventListener("click", closeModal);
}
show() {
if (!this.hasSeenDisclaimer) {
this.modal.style.display = "flex";
document.body.style.overflow = "hidden";
}
}
}
export const disclaimerModal = new DisclaimerModal();