update
@@ -1,207 +0,0 @@
|
|||||||
<!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="/assets/favicon.ico" sizes="any" />
|
|
||||||
<link
|
|
||||||
rel="apple-touch-icon"
|
|
||||||
sizes="180x180"
|
|
||||||
href="/assets/png/apple-touch-icon.png"
|
|
||||||
/>
|
|
||||||
<link
|
|
||||||
rel="icon"
|
|
||||||
type="image/png"
|
|
||||||
sizes="32x32"
|
|
||||||
href="assets/png/favicon-32x32.png"
|
|
||||||
/>
|
|
||||||
<link
|
|
||||||
rel="icon"
|
|
||||||
type="image/png"
|
|
||||||
sizes="16x16"
|
|
||||||
href="assets/png/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 -->
|
|
||||||
<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/about.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>
|
|
Before Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 93 KiB |
Before Width: | Height: | Size: 9.1 KiB |
Before Width: | Height: | Size: 37 KiB |
Before Width: | Height: | Size: 8.1 KiB |
Before Width: | Height: | Size: 586 B |
Before Width: | Height: | Size: 1.1 KiB |
@@ -1,33 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 318.32 108.98">
|
|
||||||
<defs>
|
|
||||||
<style>
|
|
||||||
.cls-1 {
|
|
||||||
fill: #fe0032;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cls-2 {
|
|
||||||
fill: #fff;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</defs>
|
|
||||||
<g>
|
|
||||||
<rect class="cls-1" x="7.1" y="7.8" width="5.84" height="5.48"/>
|
|
||||||
<rect class="cls-1" x="7.1" y="18.78" width="24.02" height="5.48"/>
|
|
||||||
<rect class="cls-1" x="7.1" y="29.77" width="42.85" height="5.48"/>
|
|
||||||
<rect class="cls-1" x="7.1" y="40.76" width="61.68" height="5.48"/>
|
|
||||||
<rect class="cls-1" x="7.1" y="51.75" width="80.88" height="5.48"/>
|
|
||||||
<rect class="cls-1" x="7.1" y="62.74" width="61.68" height="5.48"/>
|
|
||||||
<rect class="cls-1" x="7.1" y="73.73" width="42.85" height="5.48"/>
|
|
||||||
<rect class="cls-1" x="7.1" y="84.72" width="24.02" height="5.48"/>
|
|
||||||
<rect class="cls-1" x="7.1" y="95.71" width="5.84" height="5.48"/>
|
|
||||||
</g>
|
|
||||||
<g>
|
|
||||||
<path class="cls-2" d="M121.28,25.08v44.54c0,5.67,2.79,8.54,8.22,8.54h11.57c5.51,0,8.3-2.87,8.3-8.54v-13.09c0-5.67-2.79-8.46-8.3-8.46h-16.6v-5.83h17.72c8.7,0,13.01,4.47,13.01,13.33v15.01c0,8.86-4.31,13.33-13.01,13.33h-13.73c-8.7,0-13.01-4.47-13.01-13.33V25.08h5.83Z"/>
|
|
||||||
<path class="cls-2" d="M168.53,25.08v6.26h-5.83v-6.26h5.83ZM168.53,42.64v41.27h-5.83v-41.27h5.83Z"/>
|
|
||||||
<path class="cls-2" d="M191.36,25.08v17.16h9.02v5.19h-9.02v36.48h-5.83v-36.48h-9.82v-5.19h9.82v-17.16h5.83Z"/>
|
|
||||||
<path class="cls-2" d="M212.88,42.24l16.84,35.68,16.6-35.68h6.07l-19.56,41.67h-6.31l-20.04-41.67h6.39Z"/>
|
|
||||||
<path class="cls-2" d="M264.05,25.08v6.26h-5.83v-6.26h5.83ZM264.05,42.24v41.67h-5.83v-41.67h5.83Z"/>
|
|
||||||
<path class="cls-2" d="M311.21,25.08v45.5c0,8.86-4.31,13.33-13.01,13.33h-13.73c-8.7,0-13.01-4.47-13.01-13.33v-15.01c0-8.86,4.31-13.33,13.01-13.33h17.72v5.83h-16.6c-5.51,0-8.3,2.79-8.3,8.46v13.09c0,5.67,2.79,8.54,8.3,8.54h11.49c5.51,0,8.3-2.87,8.3-8.54V25.08h5.83Z"/>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
Before Width: | Height: | Size: 1.9 KiB |
@@ -1,33 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 318.32 108.98">
|
|
||||||
<defs>
|
|
||||||
<style>
|
|
||||||
.cls-1 {
|
|
||||||
fill: #fe0032;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cls-2 {
|
|
||||||
fill: #383838;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</defs>
|
|
||||||
<g>
|
|
||||||
<rect class="cls-1" x="7.1" y="7.8" width="5.84" height="5.48"/>
|
|
||||||
<rect class="cls-1" x="7.1" y="18.78" width="24.02" height="5.48"/>
|
|
||||||
<rect class="cls-1" x="7.1" y="29.77" width="42.85" height="5.48"/>
|
|
||||||
<rect class="cls-1" x="7.1" y="40.76" width="61.68" height="5.48"/>
|
|
||||||
<rect class="cls-1" x="7.1" y="51.75" width="80.88" height="5.48"/>
|
|
||||||
<rect class="cls-1" x="7.1" y="62.74" width="61.68" height="5.48"/>
|
|
||||||
<rect class="cls-1" x="7.1" y="73.73" width="42.85" height="5.48"/>
|
|
||||||
<rect class="cls-1" x="7.1" y="84.72" width="24.02" height="5.48"/>
|
|
||||||
<rect class="cls-1" x="7.1" y="95.71" width="5.84" height="5.48"/>
|
|
||||||
</g>
|
|
||||||
<g>
|
|
||||||
<path class="cls-2" d="M121.28,25.08v44.54c0,5.67,2.79,8.54,8.22,8.54h11.57c5.51,0,8.3-2.87,8.3-8.54v-13.09c0-5.67-2.79-8.46-8.3-8.46h-16.6v-5.83h17.72c8.7,0,13.01,4.47,13.01,13.33v15.01c0,8.86-4.31,13.33-13.01,13.33h-13.73c-8.7,0-13.01-4.47-13.01-13.33V25.08h5.83Z"/>
|
|
||||||
<path class="cls-2" d="M168.53,25.08v6.26h-5.83v-6.26h5.83ZM168.53,42.64v41.27h-5.83v-41.27h5.83Z"/>
|
|
||||||
<path class="cls-2" d="M191.36,25.08v17.16h9.02v5.19h-9.02v36.48h-5.83v-36.48h-9.82v-5.19h9.82v-17.16h5.83Z"/>
|
|
||||||
<path class="cls-2" d="M212.88,42.24l16.84,35.68,16.6-35.68h6.07l-19.56,41.67h-6.31l-20.04-41.67h6.39Z"/>
|
|
||||||
<path class="cls-2" d="M264.05,25.08v6.26h-5.83v-6.26h5.83ZM264.05,42.24v41.67h-5.83v-41.67h5.83Z"/>
|
|
||||||
<path class="cls-2" d="M311.21,25.08v45.5c0,8.86-4.31,13.33-13.01,13.33h-13.73c-8.7,0-13.01-4.47-13.01-13.33v-15.01c0-8.86,4.31-13.33,13.01-13.33h17.72v5.83h-16.6c-5.51,0-8.3,2.79-8.3,8.46v13.09c0,5.67,2.79,8.54,8.3,8.54h11.49c5.51,0,8.3-2.87,8.3-8.54V25.08h5.83Z"/>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
Before Width: | Height: | Size: 1.9 KiB |
@@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M24 13.616v-3.232c-1.651-.587-2.694-.752-3.219-2.019v-.001c-.527-1.271.1-2.134.847-3.707l-2.285-2.285c-1.561.742-2.433 1.375-3.707.847h-.001c-1.269-.526-1.435-1.576-2.019-3.219h-3.232c-.582 1.635-.749 2.692-2.019 3.219h-.001c-1.271.528-2.132-.098-3.707-.847l-2.285 2.285c.745 1.568 1.375 2.434.847 3.707-.527 1.271-1.584 1.438-3.219 2.02v3.232c1.632.58 2.692.749 3.219 2.019.53 1.282-.114 2.166-.847 3.707l2.285 2.286c1.562-.743 2.434-1.375 3.707-.847h.001c1.27.526 1.436 1.579 2.019 3.219h3.232c.582-1.636.75-2.69 2.027-3.222h.001c1.262-.524 2.12.101 3.698.851l2.285-2.286c-.744-1.563-1.375-2.433-.848-3.706.527-1.271 1.588-1.44 3.221-2.021zm-12 2.384c-2.209 0-4-1.791-4-4s1.791-4 4-4 4 1.791 4 4-1.791 4-4 4z"/></svg>
|
|
Before Width: | Height: | Size: 811 B |
@@ -1,99 +0,0 @@
|
|||||||
<!-- components/video-modal.html -->
|
|
||||||
<div id="playerModal" class="fixed inset-0 bg-black/90 z-50 hidden">
|
|
||||||
<div
|
|
||||||
class="modal-container h-full w-full flex items-start justify-center overflow-y-auto"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="modal-content bg-gray-900 w-full max-w-[90%] lg:max-w-6xl my-0 rounded-lg overflow-hidden relative"
|
|
||||||
>
|
|
||||||
<!-- Navigation bar - sliding at top -->
|
|
||||||
<div
|
|
||||||
id="modalNav"
|
|
||||||
class="sticky top-0 z-60 bg-gradient-to-b from-black/80 to-transparent transition-transform duration-300"
|
|
||||||
>
|
|
||||||
<div class="flex items-center px-6 py-4">
|
|
||||||
<button
|
|
||||||
id="closeModal"
|
|
||||||
class="back-button flex items-center justify-center w-10 h-10 rounded-full bg-black/50 hover:bg-black/70 transition-all duration-200 backdrop-blur"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
width="24"
|
|
||||||
height="24"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
stroke-width="2"
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
class="w-6 h-6 text-gray-300"
|
|
||||||
>
|
|
||||||
<path d="M15 18l-6-6 6-6" />
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="video-container w-full bg-black">
|
|
||||||
<video id="modalVideo" class="w-full aspect-video" controls></video>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="video-info p-6">
|
|
||||||
<!-- Rest of the content stays the same -->
|
|
||||||
<!-- Video Title -->
|
|
||||||
<h2 id="videoTitle" class="text-2xl font-bold mb-2 text-white"></h2>
|
|
||||||
|
|
||||||
<!-- Video Timestamp -->
|
|
||||||
<div
|
|
||||||
class="flex items-center justify-between text-sm text-gray-400 mb-4"
|
|
||||||
>
|
|
||||||
<span id="videoTimestamp">just now</span>
|
|
||||||
<div id="modalStatus" class="text-gray-300">
|
|
||||||
Initializing... Just give it a sec.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Creator info -->
|
|
||||||
<div class="flex items-center mb-4 p-4 bg-gray-800/50 rounded-lg">
|
|
||||||
<div class="w-12 h-12 rounded-full bg-gray-700 overflow-hidden">
|
|
||||||
<img
|
|
||||||
id="creatorAvatar"
|
|
||||||
src=""
|
|
||||||
alt="Creator"
|
|
||||||
class="w-full h-full object-cover"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="ml-4">
|
|
||||||
<h3 id="creatorName" class="font-medium text-lg text-white">
|
|
||||||
Creator Name
|
|
||||||
</h3>
|
|
||||||
<p id="creatorNpub" class="text-sm text-gray-400">npub...</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Video Description -->
|
|
||||||
<div class="bg-gray-800/50 rounded-lg p-4 mb-4">
|
|
||||||
<p id="videoDescription" class="text-gray-300 whitespace-pre-wrap">
|
|
||||||
No description available.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Torrent stats -->
|
|
||||||
<div class="bg-gray-800/50 rounded-lg p-4">
|
|
||||||
<div class="w-full bg-gray-700 rounded-full h-2 mb-2">
|
|
||||||
<div
|
|
||||||
class="bg-blue-500 h-2 rounded-full"
|
|
||||||
id="modalProgress"
|
|
||||||
style="width: 0%"
|
|
||||||
></div>
|
|
||||||
</div>
|
|
||||||
<div class="flex justify-between text-sm text-gray-400">
|
|
||||||
<span id="modalPeers">Peers: 0</span>
|
|
||||||
<span id="modalSpeed">Speed: 0 KB/s</span>
|
|
||||||
<span id="modalDownloaded">Downloaded: 0 MB / 0 MB</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
@@ -1,24 +0,0 @@
|
|||||||

|
|
||||||
|
|
||||||
# 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!
|
|
||||||
|
|
||||||
## What Makes bitvid Different?
|
|
||||||
|
|
||||||
- **You're in Control**: Your videos stay yours. No company owns or controls your content – you share it directly with your viewers.
|
|
||||||
- **Always Available**: Because videos are shared between viewers, popular content actually loads faster instead of slower. No more buffering during peak times!
|
|
||||||
- **Privacy First**: No need for email or password – you log in with a secure key that only you control.
|
|
||||||
- **Support Creators Directly**: Send tips to creators you love without a platform taking a big cut.
|
|
||||||
- **Free and Open**: bitvid's code is open source, meaning anyone can check how it works or help make it better.
|
|
||||||
|
|
||||||
## How Does It Work?
|
|
||||||
|
|
||||||
bitvid uses two main technologies to make this all possible:
|
|
||||||
|
|
||||||
1. **WebTorrent**: This lets viewers stream videos directly from other viewers, like a relay race passing the content from person to person.
|
|
||||||
2. **Nostr**: This handles your login and video details, like a digital ID card that works across many sites.
|
|
||||||
|
|
||||||
## Join the Revolution
|
|
||||||
|
|
||||||
Whether you're tired of traditional platforms controlling your content, care about privacy, or just want a better way to share videos, bitvid offers a fresh alternative. Come be part of the future of video sharing!
|
|
@@ -1,47 +0,0 @@
|
|||||||
# Getting Started with bitvid
|
|
||||||
|
|
||||||
Ready to jump in? Here's everything you need to know to start watching and sharing videos on bitvid.
|
|
||||||
|
|
||||||
## Watching Videos
|
|
||||||
|
|
||||||
1. Just visit [bitvid.network](https://bitvid.network) or one of our alternate sites like [bitvid.btc.us](https://bitvid.btc.us) and [bitvid.eth.limo](https://bitvid.eth.limo). We also have other instances via [IPNS](ipns.html) gateways you can try.
|
|
||||||
2. Browse the videos on the homepage
|
|
||||||
3. Click any video to start watching
|
|
||||||
That's it! No account needed to watch.
|
|
||||||
|
|
||||||
## Sharing Your Videos
|
|
||||||
|
|
||||||
### Step 1: Set Up Your Account
|
|
||||||
|
|
||||||
1. Install a [Nostr extension](https://nostrapps.com/#signers#all) (like Alby or Nos2x) in your browser
|
|
||||||
2. The extension creates your secure login key automatically
|
|
||||||
3. Click "Login" on bitvid to connect
|
|
||||||
|
|
||||||
### Step 2: Prepare Your Video
|
|
||||||
|
|
||||||
1. Download WebTorrent Desktop app from [webtorrent.io/desktop](https://webtorrent.io/desktop/)
|
|
||||||
2. Open your video file in WebTorrent Desktop
|
|
||||||
3. It will create a special "magnet link" for your video
|
|
||||||
4. Keep WebTorrent Desktop running to share your video
|
|
||||||
|
|
||||||
### Step 3: Share on bitvid
|
|
||||||
|
|
||||||
1. Click "Share a Video" on bitvid
|
|
||||||
2. Paste your video's magnet link
|
|
||||||
3. Add a title, description, and thumbnail
|
|
||||||
4. Click "Post" to share!
|
|
||||||
|
|
||||||
## Tips for Success
|
|
||||||
|
|
||||||
- Keep WebTorrent Desktop running while sharing videos
|
|
||||||
- Add eye-catching thumbnails to attract viewers
|
|
||||||
- Write clear descriptions to help people find your content
|
|
||||||
- Use the "Private" option if you only want to share with specific people
|
|
||||||
|
|
||||||
## Need Help?
|
|
||||||
|
|
||||||
- Visit our [GitHub](https://github.com/PR0M3TH3AN/bitvid) page for technical support
|
|
||||||
- Join our [community](https://primal.net/p/npub13yarr7j6vjqjjkahd63dmr27curypehx45ucue286ac7sft27y0srnpmpe) to connect with other users
|
|
||||||
- Report bugs to help us improve
|
|
||||||
|
|
||||||
Welcome to bitvid – let's start sharing!
|
|
@@ -1,19 +0,0 @@
|
|||||||
# 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.
|
|
@@ -1,47 +0,0 @@
|
|||||||
# Roadmap and Bug List
|
|
||||||
|
|
||||||
## UI Enhancements
|
|
||||||
|
|
||||||
- Add a copy Magnet button labeled "Seed".
|
|
||||||
- add community guidelines page
|
|
||||||
- add links to pop-up modal
|
|
||||||
- Convert "Logged in as" from public key to profile image and username (use npub as fallback).
|
|
||||||
- Add a sidebar for improved UI flexibility.
|
|
||||||
- Customize home screen content via algorithms for better feeds. (trending, new, for you etc.)
|
|
||||||
- Improve UI/UX and CSS.
|
|
||||||
- Add custom color themes and toggle between light and dark mode.
|
|
||||||
|
|
||||||
## Bug Fixes
|
|
||||||
|
|
||||||
- Fix public key wrapping issue on smaller screens.
|
|
||||||
- Fix video editing failures.
|
|
||||||
- Resolve issue where reopening the same video doesn't work after closing the video player.
|
|
||||||
- Address "Video playback error: MEDIA_ELEMENT_ERROR: Empty src attribute" error.
|
|
||||||
- Fix "Dev Mode" publishing "Live Mode" notes—add a flag for dev mode posts.
|
|
||||||
|
|
||||||
## Feature Additions
|
|
||||||
|
|
||||||
- Allow users to set custom relay settings, stored in local cache.
|
|
||||||
- Add a "Publish" step in the video editing process.
|
|
||||||
- Add comments to the video modal.
|
|
||||||
- Implement an "Adult Content" flag for note submissions.
|
|
||||||
- Enable custom hashtags in the submission spec and form.
|
|
||||||
- Allow multiple video resolutions with a selector in the video player.
|
|
||||||
- Add a block/unblock list with import/export functionality.
|
|
||||||
- Assign unique URLs to each video.
|
|
||||||
- Add a profile modal for each user/profile.
|
|
||||||
- Introduce a subscription mechanism with notifications.
|
|
||||||
- Add zaps to videos, profiles, and comments.
|
|
||||||
- Implement visibility filtering for videos:
|
|
||||||
- Show only videos whose magnet links have at least **one active peer online**.
|
|
||||||
- Integrate the filtering mechanism into the video list rendering process.
|
|
||||||
- Update the video list dynamically based on real-time peer availability.
|
|
||||||
- Add multi-language support for content and filtration.
|
|
||||||
- Create a settings menu for local account preferences, including relay, adult content, theme, and language.
|
|
||||||
|
|
||||||
## Long-Term Goals
|
|
||||||
|
|
||||||
- Add a system for creating high-quality, algorithm-driven content feeds.
|
|
||||||
- Thoroughly bug test the video editing and submission process.
|
|
||||||
|
|
||||||
If you find a new bug thats not listed here. DM me on [Nostr](https://primal.net/p/npub13yarr7j6vjqjjkahd63dmr27curypehx45ucue286ac7sft27y0srnpmpe).
|
|
@@ -1,111 +0,0 @@
|
|||||||
/* markdown.css */
|
|
||||||
|
|
||||||
/*
|
|
||||||
Give the markdown container a standard text color
|
|
||||||
that is easy to read on a white background
|
|
||||||
*/
|
|
||||||
#markdown-container {
|
|
||||||
color: #333;
|
|
||||||
line-height: 1.6;
|
|
||||||
/* Padding & border radius come from your inline Tailwind classes */
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Headings */
|
|
||||||
#markdown-container h1 {
|
|
||||||
font-size: 1.875rem; /* ~ text-2xl in Tailwind */
|
|
||||||
font-weight: 700;
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
margin-top: 0.5rem;
|
|
||||||
color: #111;
|
|
||||||
}
|
|
||||||
|
|
||||||
#markdown-container h2 {
|
|
||||||
font-size: 1.5rem; /* ~ text-xl */
|
|
||||||
font-weight: 700;
|
|
||||||
margin-bottom: 0.75rem;
|
|
||||||
margin-top: 1.25rem;
|
|
||||||
color: #222;
|
|
||||||
}
|
|
||||||
|
|
||||||
#markdown-container h3 {
|
|
||||||
font-size: 1.25rem; /* ~ text-lg */
|
|
||||||
font-weight: 600;
|
|
||||||
margin-bottom: 0.75rem;
|
|
||||||
margin-top: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
#markdown-container h4,
|
|
||||||
#markdown-container h5,
|
|
||||||
#markdown-container h6 {
|
|
||||||
font-weight: 600;
|
|
||||||
margin-top: 0.75rem;
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Paragraphs */
|
|
||||||
#markdown-container p {
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Lists */
|
|
||||||
#markdown-container ul,
|
|
||||||
#markdown-container ol {
|
|
||||||
margin-left: 1.25rem;
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
#markdown-container li {
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Blockquotes */
|
|
||||||
#markdown-container blockquote {
|
|
||||||
border-left: 4px solid #ccc;
|
|
||||||
padding-left: 1rem;
|
|
||||||
margin: 1rem 0;
|
|
||||||
color: #555;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Inline code */
|
|
||||||
#markdown-container code {
|
|
||||||
background-color: #f6f8fa;
|
|
||||||
color: #d63384;
|
|
||||||
padding: 0.2rem 0.4rem;
|
|
||||||
border-radius: 0.25rem;
|
|
||||||
font-family: Menlo, Monaco, "Courier New", monospace;
|
|
||||||
font-size: 0.875rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Code blocks */
|
|
||||||
#markdown-container pre {
|
|
||||||
background-color: #f6f8fa;
|
|
||||||
color: #333;
|
|
||||||
padding: 1rem;
|
|
||||||
border-radius: 0.25rem;
|
|
||||||
overflow-x: auto;
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Links */
|
|
||||||
#markdown-container a {
|
|
||||||
color: #bf5af2; /* pick a link color or use var(--color-primary) if you like */
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
#markdown-container a:hover {
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Horizontal Rule */
|
|
||||||
#markdown-container hr {
|
|
||||||
border: 0;
|
|
||||||
border-top: 1px solid #e5e7eb;
|
|
||||||
margin: 2rem 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Images */
|
|
||||||
#markdown-container img {
|
|
||||||
max-width: 100%;
|
|
||||||
height: auto;
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
}
|
|
@@ -1,423 +0,0 @@
|
|||||||
/* css/style.css */
|
|
||||||
|
|
||||||
:root {
|
|
||||||
--color-bg: #0f172a;
|
|
||||||
--color-card: #1e293b;
|
|
||||||
--color-primary: #8b5cf6;
|
|
||||||
--color-secondary: #f43f5e;
|
|
||||||
--color-text: #f8fafc;
|
|
||||||
--color-muted: #94a3b8;
|
|
||||||
--shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05);
|
|
||||||
--shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1);
|
|
||||||
--shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Core Styles */
|
|
||||||
body {
|
|
||||||
font-family: system-ui, -apple-system, sans-serif;
|
|
||||||
background-color: var(--color-bg);
|
|
||||||
color: var(--color-text);
|
|
||||||
line-height: 1.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
header {
|
|
||||||
margin-bottom: 2rem;
|
|
||||||
padding: 1rem 0;
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-start;
|
|
||||||
}
|
|
||||||
|
|
||||||
header img {
|
|
||||||
height: 6rem;
|
|
||||||
width: auto;
|
|
||||||
max-width: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.container {
|
|
||||||
max-width: 1480px;
|
|
||||||
margin: 0 auto;
|
|
||||||
padding: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Video Grid */
|
|
||||||
#videoList {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
|
||||||
gap: 2rem;
|
|
||||||
padding: 1.5rem 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Video Cards */
|
|
||||||
.video-card {
|
|
||||||
background-color: var(--color-card);
|
|
||||||
border-radius: 0.75rem;
|
|
||||||
overflow: hidden;
|
|
||||||
transition: transform 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
|
||||||
box-shadow: var(--shadow-md);
|
|
||||||
}
|
|
||||||
|
|
||||||
.video-card:hover {
|
|
||||||
transform: translateY(-4px);
|
|
||||||
box-shadow: var(--shadow-lg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.video-card .thumbnail-container {
|
|
||||||
position: relative;
|
|
||||||
padding-top: 56.25%;
|
|
||||||
background-color: #0f172a;
|
|
||||||
}
|
|
||||||
|
|
||||||
.video-card img.thumbnail {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
object-fit: cover;
|
|
||||||
}
|
|
||||||
|
|
||||||
.video-card .details {
|
|
||||||
padding: 1.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.video-card h3 {
|
|
||||||
font-size: 1.125rem;
|
|
||||||
font-weight: 600;
|
|
||||||
color: var(--color-text);
|
|
||||||
margin-bottom: 0.75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Modal Player */
|
|
||||||
#playerModal {
|
|
||||||
position: fixed;
|
|
||||||
inset: 0;
|
|
||||||
background-color: rgb(0 0 0 / 0.9);
|
|
||||||
z-index: 50;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
overflow-y: auto;
|
|
||||||
overscroll-behavior: contain;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Modal Content Container */
|
|
||||||
.modal-content {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
background-color: #0f172a;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Video Container */
|
|
||||||
.video-container {
|
|
||||||
width: 100%;
|
|
||||||
background-color: black;
|
|
||||||
position: sticky;
|
|
||||||
top: 0;
|
|
||||||
z-index: 51;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Modal Video */
|
|
||||||
#modalVideo {
|
|
||||||
width: 100%;
|
|
||||||
aspect-ratio: 16/9;
|
|
||||||
background-color: black;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Video Info Section */
|
|
||||||
.video-info {
|
|
||||||
padding: 1rem;
|
|
||||||
flex: 1;
|
|
||||||
overflow-y: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Responsive Adjustments */
|
|
||||||
@media (min-width: 768px) {
|
|
||||||
#playerModal {
|
|
||||||
padding: 2rem;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-content {
|
|
||||||
max-width: 64rem;
|
|
||||||
max-height: calc(100vh - 4rem); /* Account for padding */
|
|
||||||
border-radius: 0.5rem;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.video-container {
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Mobile-specific styles */
|
|
||||||
@media (max-width: 767px) {
|
|
||||||
#playerModal {
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-content {
|
|
||||||
border-radius: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Custom Scrollbar */
|
|
||||||
.video-info::-webkit-scrollbar {
|
|
||||||
width: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.video-info::-webkit-scrollbar-track {
|
|
||||||
background: rgba(255, 255, 255, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.video-info::-webkit-scrollbar-thumb {
|
|
||||||
background: rgba(255, 255, 255, 0.3);
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.video-info::-webkit-scrollbar-thumb:hover {
|
|
||||||
background: rgba(255, 255, 255, 0.4);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Progress Bars */
|
|
||||||
.progress-bar {
|
|
||||||
width: 100%;
|
|
||||||
height: 0.5rem;
|
|
||||||
background-color: rgb(255 255 255 / 0.1);
|
|
||||||
border-radius: 9999px;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.progress-bar-fill {
|
|
||||||
height: 100%;
|
|
||||||
background-color: var(--color-primary);
|
|
||||||
transition: width 0.3s ease-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Forms & Inputs */
|
|
||||||
input,
|
|
||||||
textarea {
|
|
||||||
width: 100%;
|
|
||||||
padding: 0.75rem;
|
|
||||||
background-color: var(--color-bg);
|
|
||||||
border: 1px solid rgb(255 255 255 / 0.1);
|
|
||||||
border-radius: 0.5rem;
|
|
||||||
color: var(--color-text);
|
|
||||||
transition: border-color 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
input:focus,
|
|
||||||
textarea:focus {
|
|
||||||
outline: none;
|
|
||||||
border-color: var(--color-primary);
|
|
||||||
ring: 2px var(--color-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Buttons */
|
|
||||||
button {
|
|
||||||
padding: 0.75rem 1.5rem;
|
|
||||||
background-color: var(--color-primary);
|
|
||||||
color: white;
|
|
||||||
border-radius: 0.5rem;
|
|
||||||
font-weight: 500;
|
|
||||||
transition: all 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
button:hover {
|
|
||||||
background-color: var(--color-secondary);
|
|
||||||
transform: translateY(-1px);
|
|
||||||
}
|
|
||||||
|
|
||||||
button:focus {
|
|
||||||
outline: none;
|
|
||||||
ring: 2px var(--color-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Utility Classes */
|
|
||||||
.line-clamp-2 {
|
|
||||||
display: -webkit-box;
|
|
||||||
-webkit-line-clamp: 2;
|
|
||||||
-webkit-box-orient: vertical;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hidden {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Notifications */
|
|
||||||
#errorContainer,
|
|
||||||
#successContainer {
|
|
||||||
border-radius: 0.5rem;
|
|
||||||
padding: 1rem;
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
#errorContainer {
|
|
||||||
background-color: rgb(220 38 38 / 0.1);
|
|
||||||
color: #fecaca;
|
|
||||||
border: 1px solid rgb(220 38 38 / 0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
#successContainer {
|
|
||||||
background-color: rgb(34 197 94 / 0.1);
|
|
||||||
color: #bbf7d0;
|
|
||||||
border: 1px solid rgb(34 197 94 / 0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Modal Display */
|
|
||||||
#playerSection,
|
|
||||||
#playerModal {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
#playerModal.flex {
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Responsive Design */
|
|
||||||
@media (max-width: 640px) {
|
|
||||||
#videoList {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
gap: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.container {
|
|
||||||
padding: 0.75rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
footer {
|
|
||||||
border-top: 1px solid rgba(255, 255, 255, 0.1);
|
|
||||||
margin-top: 4rem;
|
|
||||||
padding-top: 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
footer a {
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
footer a:hover {
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Disclaimer Modal Styles */
|
|
||||||
#disclaimerModal {
|
|
||||||
position: fixed;
|
|
||||||
inset: 0;
|
|
||||||
background-color: rgb(0 0 0 / 0.9);
|
|
||||||
z-index: 50;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
overflow-y: auto;
|
|
||||||
overscroll-behavior: contain;
|
|
||||||
}
|
|
||||||
|
|
||||||
#disclaimerModal .modal-content {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
background-color: var(--color-bg);
|
|
||||||
}
|
|
||||||
|
|
||||||
#disclaimerModal .modal-scroll {
|
|
||||||
padding: 1.5rem;
|
|
||||||
flex: 1;
|
|
||||||
overflow-y: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Disclaimer Modal Scrollbar */
|
|
||||||
#disclaimerModal .modal-scroll::-webkit-scrollbar {
|
|
||||||
width: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#disclaimerModal .modal-scroll::-webkit-scrollbar-track {
|
|
||||||
background: rgba(255, 255, 255, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
#disclaimerModal .modal-scroll::-webkit-scrollbar-thumb {
|
|
||||||
background: rgba(255, 255, 255, 0.3);
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#disclaimerModal .modal-scroll::-webkit-scrollbar-thumb:hover {
|
|
||||||
background: rgba(255, 255, 255, 0.4);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Disclaimer Modal Button Container */
|
|
||||||
#disclaimerModal .button-container {
|
|
||||||
padding: 1rem 1.5rem;
|
|
||||||
background-color: #1a2234;
|
|
||||||
border-top: 1px solid rgba(255, 255, 255, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Responsive Adjustments for Disclaimer Modal */
|
|
||||||
@media (min-width: 768px) {
|
|
||||||
#disclaimerModal {
|
|
||||||
padding: 2rem;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
#disclaimerModal .modal-content {
|
|
||||||
width: 100%;
|
|
||||||
height: auto;
|
|
||||||
max-width: 42rem;
|
|
||||||
max-height: 90vh;
|
|
||||||
border-radius: 0.5rem;
|
|
||||||
overflow: hidden;
|
|
||||||
margin: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
#disclaimerModal .modal-scroll {
|
|
||||||
max-height: calc(
|
|
||||||
90vh - 5rem
|
|
||||||
); /* Account for button container and padding */
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Mobile-specific styles for Disclaimer Modal */
|
|
||||||
@media (max-width: 767px) {
|
|
||||||
#disclaimerModal {
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
#disclaimerModal .modal-content {
|
|
||||||
min-height: 100vh;
|
|
||||||
border-radius: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Override default button styles for back button */
|
|
||||||
.back-button {
|
|
||||||
background: rgba(0, 0, 0, 0.5) !important;
|
|
||||||
padding: 0 !important;
|
|
||||||
color: rgb(209 213 219) !important;
|
|
||||||
transform: none !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.back-button:hover {
|
|
||||||
background: rgba(0, 0, 0, 0.7) !important;
|
|
||||||
color: white !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Modal Container */
|
|
||||||
.modal-container {
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 768px) {
|
|
||||||
.modal-container {
|
|
||||||
padding: 2rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Video info cards */
|
|
||||||
.video-info .bg-gray-800\/50 {
|
|
||||||
background-color: rgb(31 41 55 / 0.5);
|
|
||||||
backdrop-filter: blur(4px);
|
|
||||||
}
|
|
@@ -1,207 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
||||||
<title>bitvid | Getting Started</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.network" />
|
|
||||||
<meta property="og:type" content="website" />
|
|
||||||
<meta property="og:locale" content="en_US" />
|
|
||||||
|
|
||||||
<!-- App Icons -->
|
|
||||||
<link rel="icon" href="assets/favicon.ico" sizes="any" />
|
|
||||||
<link
|
|
||||||
rel="apple-touch-icon"
|
|
||||||
sizes="180x180"
|
|
||||||
href="assets/png/apple-touch-icon.png"
|
|
||||||
/>
|
|
||||||
<link
|
|
||||||
rel="icon"
|
|
||||||
type="image/png"
|
|
||||||
sizes="32x32"
|
|
||||||
href="assets/png/favicon-32x32.png"
|
|
||||||
/>
|
|
||||||
<link
|
|
||||||
rel="icon"
|
|
||||||
type="image/png"
|
|
||||||
sizes="16x16"
|
|
||||||
href="assets/png/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 -->
|
|
||||||
<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/getting-started.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>
|
|
@@ -1,448 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
||||||
<title>bitvid | Decentralized Video Sharing</title>
|
|
||||||
|
|
||||||
<!-- Open Graph Meta Tags -->
|
|
||||||
<meta
|
|
||||||
property="og:title"
|
|
||||||
content="BitVid - Decentralized Video Sharing"
|
|
||||||
/>
|
|
||||||
<meta
|
|
||||||
property="og:description"
|
|
||||||
content="Share videos and follow creators freely, in a truly decentralized way."
|
|
||||||
/>
|
|
||||||
<meta
|
|
||||||
property="og:image"
|
|
||||||
content="https://bitvid.netlify.app/assets/jpg/bitvid.jpg"
|
|
||||||
/>
|
|
||||||
<meta property="og:url" content="https://bitvid.network" />
|
|
||||||
<meta property="og:type" content="website" />
|
|
||||||
<meta property="og:locale" content="en_US" />
|
|
||||||
|
|
||||||
<!-- App Icons -->
|
|
||||||
<link rel="icon" href="assets/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="assets/png/favicon-32x32.png"
|
|
||||||
/>
|
|
||||||
<link
|
|
||||||
rel="icon"
|
|
||||||
type="image/png"
|
|
||||||
sizes="16x16"
|
|
||||||
href="assets/png/favicon-16x16.png"
|
|
||||||
/>
|
|
||||||
<link rel="manifest" href="/site.webmanifest" />
|
|
||||||
<meta name="theme-color" content="#0f172a" />
|
|
||||||
|
|
||||||
<!-- Styles -->
|
|
||||||
<link
|
|
||||||
href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css"
|
|
||||||
rel="stylesheet"
|
|
||||||
/>
|
|
||||||
<link href="css/style.css" rel="stylesheet" />
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<!-- Rest of your page content -->
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
|
|
||||||
<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">
|
|
||||||
<img
|
|
||||||
src="assets/svg/bitvid-logo-light-mode.svg"
|
|
||||||
alt="BitVid Logo"
|
|
||||||
class="h-16"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<!-- Login Section -->
|
|
||||||
<div id="loginSection" class="mb-8 flex items-center justify-between">
|
|
||||||
<div>
|
|
||||||
<button
|
|
||||||
id="loginButton"
|
|
||||||
class="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"
|
|
||||||
>
|
|
||||||
Login with Nostr
|
|
||||||
</button>
|
|
||||||
<p id="userStatus" class="mt-4 text-gray-500 hidden">
|
|
||||||
Logged in as: <span id="userPubKey"></span>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<button
|
|
||||||
id="logoutButton"
|
|
||||||
class="bg-red-500 text-white px-4 py-2 rounded-md hover:bg-red-600 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2 hidden"
|
|
||||||
>
|
|
||||||
Logout
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Error Container -->
|
|
||||||
<div
|
|
||||||
id="errorContainer"
|
|
||||||
class="hidden bg-red-100 text-red-800 p-4 rounded-md mb-4"
|
|
||||||
>
|
|
||||||
<!-- Error messages will appear here -->
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Success Container -->
|
|
||||||
<div
|
|
||||||
id="successContainer"
|
|
||||||
class="hidden bg-green-100 text-green-800 p-4 rounded-md mb-4"
|
|
||||||
>
|
|
||||||
<!-- Success messages will appear here -->
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Video Submission Form -->
|
|
||||||
<div
|
|
||||||
class="bg-white p-6 rounded-lg shadow-md mb-8 hidden"
|
|
||||||
id="videoFormContainer"
|
|
||||||
>
|
|
||||||
<h2 class="text-xl font-semibold mb-4">Share a Video</h2>
|
|
||||||
<form id="submitForm" class="space-y-4">
|
|
||||||
<div>
|
|
||||||
<label for="title" class="block text-sm font-medium text-gray-700"
|
|
||||||
>Title</label
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
id="title"
|
|
||||||
required
|
|
||||||
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label for="magnet" class="block text-sm font-medium text-gray-700"
|
|
||||||
>Magnet Link</label
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
id="magnet"
|
|
||||||
required
|
|
||||||
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label
|
|
||||||
for="thumbnail"
|
|
||||||
class="block text-sm font-medium text-gray-700"
|
|
||||||
>Thumbnail URL (optional)</label
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
type="url"
|
|
||||||
id="thumbnail"
|
|
||||||
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Description Field -->
|
|
||||||
<div>
|
|
||||||
<label
|
|
||||||
for="description"
|
|
||||||
class="block text-sm font-medium text-gray-700"
|
|
||||||
>Description (optional)</label
|
|
||||||
>
|
|
||||||
<textarea
|
|
||||||
id="description"
|
|
||||||
rows="3"
|
|
||||||
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500"
|
|
||||||
></textarea>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- ADDED FOR PRIVATE LISTINGS -->
|
|
||||||
<div class="flex items-center space-x-2">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
id="isPrivate"
|
|
||||||
class="form-checkbox h-5 w-5"
|
|
||||||
/>
|
|
||||||
<span class="text-sm font-medium text-gray-700"
|
|
||||||
>Private Listing (Encrypt Magnet)</span
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
<!-- END ADDED FOR PRIVATE LISTINGS -->
|
|
||||||
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
class="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"
|
|
||||||
>
|
|
||||||
Share Video
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Video Player Section -->
|
|
||||||
<div id="playerSection" class="mb-8 hidden">
|
|
||||||
<video id="video" controls class="w-full rounded-lg shadow-md"></video>
|
|
||||||
<!-- Status and Stats -->
|
|
||||||
<div class="mt-4">
|
|
||||||
<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="bg-blue-500 h-2 rounded-full"
|
|
||||||
id="progress"
|
|
||||||
style="width: 0%"
|
|
||||||
></div>
|
|
||||||
</div>
|
|
||||||
<div class="flex justify-between text-sm text-gray-600">
|
|
||||||
<span id="peers">Peers: 0</span>
|
|
||||||
<span id="speed">0 KB/s</span>
|
|
||||||
<span id="downloaded">0 MB / 0 MB</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Video List -->
|
|
||||||
<div class="mb-8">
|
|
||||||
<div
|
|
||||||
id="videoList"
|
|
||||||
class="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-8"
|
|
||||||
>
|
|
||||||
<!-- Videos will be dynamically inserted here -->
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Imported Video Player Modal -->
|
|
||||||
<div id="modalContainer"></div>
|
|
||||||
|
|
||||||
<div class="text-center mb-8">
|
|
||||||
<h2 class="text-2xl font-bold text-gray-500 tracking-wide">
|
|
||||||
seed. zap. subscribe.
|
|
||||||
</h2>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Disclaimer Modal -->
|
|
||||||
<div id="disclaimerModal" class="hidden">
|
|
||||||
<div class="modal-content">
|
|
||||||
<div class="modal-scroll">
|
|
||||||
<!-- 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 text-white">
|
|
||||||
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">
|
|
||||||
Early Access Status
|
|
||||||
</h3>
|
|
||||||
<p class="text-gray-400">
|
|
||||||
Currently, video posting is invite-only as we carefully
|
|
||||||
scale our platform. While anyone can watch videos, content
|
|
||||||
creation is limited to approved creators. This helps us
|
|
||||||
maintain quality content during our early stages.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="bg-gray-800 rounded-lg p-4">
|
|
||||||
<h3 class="text-white font-semibold mb-2">
|
|
||||||
Content Responsibility & Moderation
|
|
||||||
</h3>
|
|
||||||
<p class="text-gray-400">
|
|
||||||
While we don't host videos directly, we maintain community
|
|
||||||
standards through access control. Users who violate our
|
|
||||||
guidelines may be blocked from accessing the platform. All
|
|
||||||
content must follow local laws and platform guidelines.
|
|
||||||
</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>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Action Button in fixed container -->
|
|
||||||
<div class="button-container">
|
|
||||||
<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>
|
|
||||||
|
|
||||||
<!-- Footer -->
|
|
||||||
<footer class="mt-auto pb-8 text-center px-4">
|
|
||||||
<a
|
|
||||||
href="http://bitvid.network/"
|
|
||||||
class="text-gray-500 hover:text-gray-400 transition-colors duration-200"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
bitvid.network
|
|
||||||
</a>
|
|
||||||
|
|
|
||||||
<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>
|
|
||||||
|
|
|
||||||
<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>
|
|
||||||
|
|
||||||
<!-- Scripts -->
|
|
||||||
<!-- Load WebTorrent via CDN -->
|
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/webtorrent/1.9.7/webtorrent.min.js"></script>
|
|
||||||
<!-- Load Nostr library -->
|
|
||||||
<script src="js/libs/nostr.bundle.js"></script>
|
|
||||||
<!-- Load JavaScript Modules -->
|
|
||||||
<script src="js/libs/nostr.bundle.js"></script>
|
|
||||||
<script type="module" src="js/config.js"></script>
|
|
||||||
<script type="module" src="js/lists.js"></script>
|
|
||||||
<script type="module" src="js/accessControl.js"></script>
|
|
||||||
<script type="module" src="js/webtorrent.js"></script>
|
|
||||||
<script type="module" src="js/nostr.js"></script>
|
|
||||||
<script type="module" src="js/app.js"></script>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
@@ -1,206 +0,0 @@
|
|||||||
<!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="assets/favicon.ico" sizes="any" />
|
|
||||||
<link
|
|
||||||
rel="apple-touch-icon"
|
|
||||||
sizes="180x180"
|
|
||||||
href="assets/png/apple-touch-icon.png"
|
|
||||||
/>
|
|
||||||
<link
|
|
||||||
rel="icon"
|
|
||||||
type="image/png"
|
|
||||||
sizes="32x32"
|
|
||||||
href="assets/png/favicon-32x32.png"
|
|
||||||
/>
|
|
||||||
<link
|
|
||||||
rel="icon"
|
|
||||||
type="image/png"
|
|
||||||
sizes="16x16"
|
|
||||||
href="assets/png/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>
|
|
@@ -1,154 +0,0 @@
|
|||||||
// js/accessControl.js
|
|
||||||
|
|
||||||
import { isDevMode, isWhitelistEnabled } from "./config.js";
|
|
||||||
import { initialWhitelist, initialBlacklist } from "./lists.js";
|
|
||||||
|
|
||||||
class AccessControl {
|
|
||||||
constructor() {
|
|
||||||
// Debug logging for initialization
|
|
||||||
console.log("DEBUG: AccessControl constructor called");
|
|
||||||
console.log("DEBUG: initialWhitelist from import:", initialWhitelist);
|
|
||||||
console.log("DEBUG: typeof initialWhitelist:", typeof initialWhitelist);
|
|
||||||
console.log("DEBUG: initialWhitelist length:", initialWhitelist.length);
|
|
||||||
|
|
||||||
// Initialize empty sets
|
|
||||||
this.whitelist = new Set(initialWhitelist);
|
|
||||||
this.blacklist = new Set(initialBlacklist.filter((x) => x)); // Filter out empty strings
|
|
||||||
|
|
||||||
// Debug the sets
|
|
||||||
console.log("DEBUG: Whitelist after Set creation:", [...this.whitelist]);
|
|
||||||
console.log("DEBUG: Blacklist after Set creation:", [...this.blacklist]);
|
|
||||||
|
|
||||||
// Save to localStorage
|
|
||||||
this.saveWhitelist();
|
|
||||||
this.saveBlacklist();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Rest of the class remains the same...
|
|
||||||
loadWhitelist() {
|
|
||||||
try {
|
|
||||||
const stored = localStorage.getItem("bitvid_whitelist");
|
|
||||||
return stored ? JSON.parse(stored) : [];
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error loading whitelist:", error);
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
loadBlacklist() {
|
|
||||||
try {
|
|
||||||
const stored = localStorage.getItem("bitvid_blacklist");
|
|
||||||
return stored ? JSON.parse(stored) : [];
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error loading blacklist:", error);
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
saveWhitelist() {
|
|
||||||
try {
|
|
||||||
localStorage.setItem(
|
|
||||||
"bitvid_whitelist",
|
|
||||||
JSON.stringify([...this.whitelist])
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error saving whitelist:", error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
saveBlacklist() {
|
|
||||||
try {
|
|
||||||
localStorage.setItem(
|
|
||||||
"bitvid_blacklist",
|
|
||||||
JSON.stringify([...this.blacklist])
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error saving blacklist:", error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
addToWhitelist(npub) {
|
|
||||||
if (!this.isValidNpub(npub)) {
|
|
||||||
throw new Error("Invalid npub format");
|
|
||||||
}
|
|
||||||
this.whitelist.add(npub);
|
|
||||||
this.saveWhitelist();
|
|
||||||
if (isDevMode) console.log(`Added ${npub} to whitelist`);
|
|
||||||
}
|
|
||||||
|
|
||||||
removeFromWhitelist(npub) {
|
|
||||||
this.whitelist.delete(npub);
|
|
||||||
this.saveWhitelist();
|
|
||||||
if (isDevMode) console.log(`Removed ${npub} from whitelist`);
|
|
||||||
}
|
|
||||||
|
|
||||||
addToBlacklist(npub) {
|
|
||||||
if (!this.isValidNpub(npub)) {
|
|
||||||
throw new Error("Invalid npub format");
|
|
||||||
}
|
|
||||||
this.blacklist.add(npub);
|
|
||||||
this.saveBlacklist();
|
|
||||||
if (isDevMode) console.log(`Added ${npub} to blacklist`);
|
|
||||||
}
|
|
||||||
|
|
||||||
removeFromBlacklist(npub) {
|
|
||||||
this.blacklist.delete(npub);
|
|
||||||
this.saveBlacklist();
|
|
||||||
if (isDevMode) console.log(`Removed ${npub} from blacklist`);
|
|
||||||
}
|
|
||||||
|
|
||||||
isWhitelisted(npub) {
|
|
||||||
const result = this.whitelist.has(npub);
|
|
||||||
if (isDevMode)
|
|
||||||
console.log(
|
|
||||||
`Checking if ${npub} is whitelisted:`,
|
|
||||||
result,
|
|
||||||
"Current whitelist:",
|
|
||||||
[...this.whitelist]
|
|
||||||
);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
isBlacklisted(npub) {
|
|
||||||
return this.blacklist.has(npub);
|
|
||||||
}
|
|
||||||
|
|
||||||
canAccess(npub) {
|
|
||||||
if (this.isBlacklisted(npub)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const canAccess = !isWhitelistEnabled || this.isWhitelisted(npub);
|
|
||||||
if (isDevMode) console.log(`Checking access for ${npub}:`, canAccess);
|
|
||||||
return canAccess;
|
|
||||||
}
|
|
||||||
|
|
||||||
filterVideos(videos) {
|
|
||||||
return videos.filter((video) => {
|
|
||||||
try {
|
|
||||||
const npub = window.NostrTools.nip19.npubEncode(video.pubkey);
|
|
||||||
return !this.isBlacklisted(npub);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error filtering video:", error);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
isValidNpub(npub) {
|
|
||||||
try {
|
|
||||||
return npub.startsWith("npub1") && npub.length === 63;
|
|
||||||
} catch (error) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
getWhitelist() {
|
|
||||||
return [...this.whitelist];
|
|
||||||
}
|
|
||||||
|
|
||||||
getBlacklist() {
|
|
||||||
return [...this.blacklist];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const accessControl = new AccessControl();
|
|
@@ -1,724 +0,0 @@
|
|||||||
// js/app.js
|
|
||||||
|
|
||||||
import { nostrClient } from "./nostr.js";
|
|
||||||
import { torrentClient } from "./webtorrent.js";
|
|
||||||
import { isDevMode } from "./config.js";
|
|
||||||
import { disclaimerModal } from "./disclaimer.js";
|
|
||||||
import { videoPlayer } from "./components/VideoPlayer.js";
|
|
||||||
import { videoList } from "./components/VideoList.js";
|
|
||||||
import { formatTimeAgo } from "./utils/timeUtils.js";
|
|
||||||
|
|
||||||
class bitvidApp {
|
|
||||||
constructor() {
|
|
||||||
// Authentication Elements
|
|
||||||
this.loginButton = document.getElementById("loginButton");
|
|
||||||
this.logoutButton = document.getElementById("logoutButton");
|
|
||||||
this.userStatus = document.getElementById("userStatus");
|
|
||||||
this.userPubKey = document.getElementById("userPubKey");
|
|
||||||
|
|
||||||
// Form Elements
|
|
||||||
this.submitForm = document.getElementById("submitForm");
|
|
||||||
this.videoFormContainer = document.getElementById("videoFormContainer");
|
|
||||||
|
|
||||||
// Video Player Elements
|
|
||||||
this.playerSection = document.getElementById("playerSection");
|
|
||||||
this.videoElement = document.getElementById("video");
|
|
||||||
this.status = document.getElementById("status");
|
|
||||||
this.progressBar = document.getElementById("progress");
|
|
||||||
this.peers = document.getElementById("peers");
|
|
||||||
this.speed = document.getElementById("speed");
|
|
||||||
this.downloaded = document.getElementById("downloaded");
|
|
||||||
|
|
||||||
// Initialize these as null - they'll be set after modal loads
|
|
||||||
this.playerModal = null;
|
|
||||||
this.modalVideo = null;
|
|
||||||
this.modalStatus = null;
|
|
||||||
this.modalProgress = null;
|
|
||||||
this.modalPeers = null;
|
|
||||||
this.modalSpeed = null;
|
|
||||||
this.modalDownloaded = null;
|
|
||||||
this.closePlayerBtn = null;
|
|
||||||
this.videoTitle = null;
|
|
||||||
this.videoDescription = null;
|
|
||||||
this.videoTimestamp = null;
|
|
||||||
this.creatorAvatar = null;
|
|
||||||
this.creatorName = null;
|
|
||||||
this.creatorNpub = null;
|
|
||||||
|
|
||||||
// Notification Containers
|
|
||||||
this.errorContainer = document.getElementById("errorContainer");
|
|
||||||
this.successContainer = document.getElementById("successContainer");
|
|
||||||
|
|
||||||
this.pubkey = null;
|
|
||||||
this.currentMagnetUri = null;
|
|
||||||
|
|
||||||
// Private Video Checkbox
|
|
||||||
this.isPrivateCheckbox = document.getElementById("isPrivate");
|
|
||||||
}
|
|
||||||
|
|
||||||
async init() {
|
|
||||||
try {
|
|
||||||
// Hide and reset player states
|
|
||||||
if (this.playerSection) {
|
|
||||||
this.playerSection.style.display = "none";
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize Nostr client first
|
|
||||||
await nostrClient.init();
|
|
||||||
|
|
||||||
// Handle saved pubkey
|
|
||||||
const savedPubKey = localStorage.getItem("userPubKey");
|
|
||||||
if (savedPubKey) {
|
|
||||||
this.login(savedPubKey, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize modal
|
|
||||||
await videoPlayer.initModal();
|
|
||||||
|
|
||||||
// Initialize video list
|
|
||||||
await videoList.loadVideos();
|
|
||||||
|
|
||||||
// Initialize and show disclaimer modal
|
|
||||||
disclaimerModal.show();
|
|
||||||
|
|
||||||
// Set up event listeners after all initializations
|
|
||||||
this.setupEventListeners();
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Init failed:", error);
|
|
||||||
this.showError("Failed to connect to Nostr relay");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async initModal() {
|
|
||||||
try {
|
|
||||||
console.log("Starting modal initialization...");
|
|
||||||
const response = await fetch("components/video-modal.html");
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`HTTP error! status: ${response.status}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const html = await response.text();
|
|
||||||
console.log("Modal HTML loaded successfully");
|
|
||||||
|
|
||||||
const modalContainer = document.getElementById("modalContainer");
|
|
||||||
if (!modalContainer) {
|
|
||||||
throw new Error("Modal container element not found!");
|
|
||||||
}
|
|
||||||
|
|
||||||
modalContainer.innerHTML = html;
|
|
||||||
console.log("Modal HTML inserted into DOM");
|
|
||||||
|
|
||||||
// Set up modal close handler
|
|
||||||
const closeButton = document.getElementById("closeModal");
|
|
||||||
if (!closeButton) {
|
|
||||||
throw new Error("Close button element not found!");
|
|
||||||
}
|
|
||||||
|
|
||||||
closeButton.addEventListener("click", () => {
|
|
||||||
this.hideModal();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Set up scroll handler for nav show/hide
|
|
||||||
let lastScrollY = 0;
|
|
||||||
const modalNav = document.getElementById("modalNav");
|
|
||||||
const playerModal = document.getElementById("playerModal");
|
|
||||||
|
|
||||||
if (!modalNav || !playerModal) {
|
|
||||||
throw new Error("Modal navigation elements not found!");
|
|
||||||
}
|
|
||||||
|
|
||||||
playerModal.addEventListener("scroll", (e) => {
|
|
||||||
const currentScrollY = e.target.scrollTop;
|
|
||||||
const shouldShowNav =
|
|
||||||
currentScrollY <= lastScrollY || currentScrollY < 50;
|
|
||||||
modalNav.style.transform = shouldShowNav
|
|
||||||
? "translateY(0)"
|
|
||||||
: "translateY(-100%)";
|
|
||||||
lastScrollY = currentScrollY;
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log("Modal initialization completed successfully");
|
|
||||||
return true;
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Modal initialization failed:", error);
|
|
||||||
// You might want to show this error to the user
|
|
||||||
this.showError(`Failed to initialize video player: ${error.message}`);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
updateModalElements() {
|
|
||||||
// Update Modal Elements
|
|
||||||
this.playerModal = document.getElementById("playerModal");
|
|
||||||
this.modalVideo = document.getElementById("modalVideo");
|
|
||||||
this.modalStatus = document.getElementById("modalStatus");
|
|
||||||
this.modalProgress = document.getElementById("modalProgress");
|
|
||||||
this.modalPeers = document.getElementById("modalPeers");
|
|
||||||
this.modalSpeed = document.getElementById("modalSpeed");
|
|
||||||
this.modalDownloaded = document.getElementById("modalDownloaded");
|
|
||||||
this.closePlayerBtn = document.getElementById("closeModal");
|
|
||||||
|
|
||||||
// Update Video Info Elements
|
|
||||||
this.videoTitle = document.getElementById("videoTitle");
|
|
||||||
this.videoDescription = document.getElementById("videoDescription");
|
|
||||||
this.videoTimestamp = document.getElementById("videoTimestamp");
|
|
||||||
|
|
||||||
// Update Creator Info Elements
|
|
||||||
this.creatorAvatar = document.getElementById("creatorAvatar");
|
|
||||||
this.creatorName = document.getElementById("creatorName");
|
|
||||||
this.creatorNpub = document.getElementById("creatorNpub");
|
|
||||||
|
|
||||||
// Add scroll behavior for nav
|
|
||||||
let lastScrollY = 0;
|
|
||||||
const modalNav = document.getElementById("modalNav");
|
|
||||||
|
|
||||||
if (this.playerModal && modalNav) {
|
|
||||||
this.playerModal.addEventListener("scroll", (e) => {
|
|
||||||
const currentScrollY = e.target.scrollTop;
|
|
||||||
const shouldShowNav =
|
|
||||||
currentScrollY <= lastScrollY || currentScrollY < 50;
|
|
||||||
modalNav.style.transform = shouldShowNav
|
|
||||||
? "translateY(0)"
|
|
||||||
: "translateY(-100%)";
|
|
||||||
lastScrollY = currentScrollY;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets up event listeners for various UI interactions.
|
|
||||||
*/
|
|
||||||
setupEventListeners() {
|
|
||||||
// Login Button
|
|
||||||
this.loginButton.addEventListener("click", async () => {
|
|
||||||
try {
|
|
||||||
const pubkey = await nostrClient.login();
|
|
||||||
this.login(pubkey, true);
|
|
||||||
} catch (error) {
|
|
||||||
this.log("Login failed:", error);
|
|
||||||
this.showError("Failed to login. Please try again.");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Logout Button
|
|
||||||
this.logoutButton.addEventListener("click", () => {
|
|
||||||
this.logout();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Form submission
|
|
||||||
this.submitForm.addEventListener("submit", (e) => this.handleSubmit(e));
|
|
||||||
|
|
||||||
// Close Modal Button
|
|
||||||
if (this.closePlayerBtn) {
|
|
||||||
this.closePlayerBtn.addEventListener("click", async () => {
|
|
||||||
await this.hideModal();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close Modal by clicking outside content
|
|
||||||
if (this.playerModal) {
|
|
||||||
this.playerModal.addEventListener("click", async (e) => {
|
|
||||||
if (e.target === this.playerModal) {
|
|
||||||
await this.hideModal();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Video error handling
|
|
||||||
this.videoElement.addEventListener("error", (e) => {
|
|
||||||
const error = e.target.error;
|
|
||||||
this.log("Video error:", error);
|
|
||||||
if (error) {
|
|
||||||
this.showError(
|
|
||||||
`Video playback error: ${error.message || "Unknown error"}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Detailed Modal Video Event Listeners
|
|
||||||
if (this.modalVideo) {
|
|
||||||
this.modalVideo.addEventListener("error", (e) => {
|
|
||||||
const error = e.target.error;
|
|
||||||
this.log("Modal video error:", error);
|
|
||||||
if (error) {
|
|
||||||
this.log("Error code:", error.code);
|
|
||||||
this.log("Error message:", error.message);
|
|
||||||
this.showError(
|
|
||||||
`Video playback error: ${error.message || "Unknown error"}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
this.modalVideo.addEventListener("loadstart", () => {
|
|
||||||
this.log("Video loadstart event fired");
|
|
||||||
});
|
|
||||||
|
|
||||||
this.modalVideo.addEventListener("loadedmetadata", () => {
|
|
||||||
this.log("Video loadedmetadata event fired");
|
|
||||||
});
|
|
||||||
|
|
||||||
this.modalVideo.addEventListener("canplay", () => {
|
|
||||||
this.log("Video canplay event fired");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cleanup on page unload
|
|
||||||
window.addEventListener("beforeunload", async () => {
|
|
||||||
await this.cleanup();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles user login.
|
|
||||||
*/
|
|
||||||
login(pubkey, saveToStorage = true) {
|
|
||||||
this.pubkey = pubkey;
|
|
||||||
this.loginButton.classList.add("hidden");
|
|
||||||
this.logoutButton.classList.remove("hidden");
|
|
||||||
this.userStatus.classList.remove("hidden");
|
|
||||||
this.userPubKey.textContent = pubkey;
|
|
||||||
this.videoFormContainer.classList.remove("hidden");
|
|
||||||
this.log(`User logged in as: ${pubkey}`);
|
|
||||||
|
|
||||||
// ADD: Update videoList pubkey
|
|
||||||
videoList.setPubkey(pubkey);
|
|
||||||
|
|
||||||
if (saveToStorage) {
|
|
||||||
localStorage.setItem("userPubKey", pubkey);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles user logout.
|
|
||||||
*/
|
|
||||||
logout() {
|
|
||||||
nostrClient.logout();
|
|
||||||
this.pubkey = null;
|
|
||||||
this.loginButton.classList.remove("hidden");
|
|
||||||
this.logoutButton.classList.add("hidden");
|
|
||||||
this.userStatus.classList.add("hidden");
|
|
||||||
this.userPubKey.textContent = "";
|
|
||||||
this.videoFormContainer.classList.add("hidden");
|
|
||||||
localStorage.removeItem("userPubKey");
|
|
||||||
this.log("User logged out.");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Cleans up video player and torrents.
|
|
||||||
*/
|
|
||||||
async cleanup() {
|
|
||||||
try {
|
|
||||||
if (this.videoElement) {
|
|
||||||
this.videoElement.pause();
|
|
||||||
this.videoElement.src = "";
|
|
||||||
this.videoElement.load();
|
|
||||||
}
|
|
||||||
if (this.modalVideo) {
|
|
||||||
this.modalVideo.pause();
|
|
||||||
this.modalVideo.src = "";
|
|
||||||
this.modalVideo.load();
|
|
||||||
}
|
|
||||||
await torrentClient.cleanup();
|
|
||||||
} catch (error) {
|
|
||||||
this.log("Cleanup error:", error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Hides the video player section.
|
|
||||||
*/
|
|
||||||
async hideVideoPlayer() {
|
|
||||||
await this.cleanup();
|
|
||||||
this.playerSection.classList.add("hidden");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Hides the video modal.
|
|
||||||
*/
|
|
||||||
async hideModal() {
|
|
||||||
await this.cleanup();
|
|
||||||
this.playerModal.style.display = "none";
|
|
||||||
this.playerModal.classList.add("hidden");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles video submission (with version, private listing).
|
|
||||||
*/
|
|
||||||
async handleSubmit(e) {
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
if (!this.pubkey) {
|
|
||||||
this.showError("Please login to post a video.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const descriptionElement = document.getElementById("description");
|
|
||||||
|
|
||||||
// ADDED FOR VERSIONING/PRIVATE/DELETE:
|
|
||||||
// If you have a checkbox with id="isPrivate" in HTML
|
|
||||||
const isPrivate = this.isPrivateCheckbox
|
|
||||||
? this.isPrivateCheckbox.checked
|
|
||||||
: false;
|
|
||||||
|
|
||||||
const formData = {
|
|
||||||
version: 2, // We set the version to 2 for new posts
|
|
||||||
title: document.getElementById("title")?.value.trim() || "",
|
|
||||||
magnet: document.getElementById("magnet")?.value.trim() || "",
|
|
||||||
thumbnail: document.getElementById("thumbnail")?.value.trim() || "",
|
|
||||||
description: descriptionElement?.value.trim() || "",
|
|
||||||
mode: isDevMode ? "dev" : "live",
|
|
||||||
isPrivate, // new field to handle private listings
|
|
||||||
};
|
|
||||||
|
|
||||||
this.log("Form Data Collected:", formData);
|
|
||||||
|
|
||||||
if (!formData.title || !formData.magnet) {
|
|
||||||
this.showError("Title and Magnet URI are required.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
await nostrClient.publishVideo(formData, this.pubkey);
|
|
||||||
this.submitForm.reset();
|
|
||||||
|
|
||||||
// If the private checkbox was checked, reset it
|
|
||||||
if (this.isPrivateCheckbox) {
|
|
||||||
this.isPrivateCheckbox.checked = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// CHANGE: Use videoList component to refresh
|
|
||||||
await videoList.loadVideos(); // <-- Change this line
|
|
||||||
this.showSuccess("Video shared successfully!");
|
|
||||||
} catch (error) {
|
|
||||||
this.log("Failed to publish video:", error.message);
|
|
||||||
this.showError("Failed to share video. Please try again later.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets a user-friendly error message.
|
|
||||||
*/
|
|
||||||
getErrorMessage(error) {
|
|
||||||
if (error.message.includes("404")) {
|
|
||||||
return "Service worker not found. Please check server configuration.";
|
|
||||||
} else if (error.message.includes("Brave")) {
|
|
||||||
return "Please disable Brave Shields for this site to play videos.";
|
|
||||||
} else if (error.message.includes("timeout")) {
|
|
||||||
return "Connection timeout. Please check your internet connection.";
|
|
||||||
} else {
|
|
||||||
return "Failed to play video. Please try again.";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Shows an error message to the user.
|
|
||||||
*/
|
|
||||||
showError(message) {
|
|
||||||
if (this.errorContainer) {
|
|
||||||
this.errorContainer.textContent = message;
|
|
||||||
this.errorContainer.classList.remove("hidden");
|
|
||||||
setTimeout(() => {
|
|
||||||
this.errorContainer.classList.add("hidden");
|
|
||||||
this.errorContainer.textContent = "";
|
|
||||||
}, 5000);
|
|
||||||
} else {
|
|
||||||
alert(message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Shows a success message to the user.
|
|
||||||
*/
|
|
||||||
showSuccess(message) {
|
|
||||||
if (this.successContainer) {
|
|
||||||
this.successContainer.textContent = message;
|
|
||||||
this.successContainer.classList.remove("hidden");
|
|
||||||
setTimeout(() => {
|
|
||||||
this.successContainer.classList.add("hidden");
|
|
||||||
this.successContainer.textContent = "";
|
|
||||||
}, 5000);
|
|
||||||
} else {
|
|
||||||
alert(message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Logs messages to console.
|
|
||||||
*/
|
|
||||||
log(message) {
|
|
||||||
console.log(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Plays a video given its magnet URI.
|
|
||||||
* This method handles the logic to initiate torrent download and play the video.
|
|
||||||
*/
|
|
||||||
async playVideo(magnetURI) {
|
|
||||||
try {
|
|
||||||
if (!magnetURI) {
|
|
||||||
this.showError("Invalid Magnet URI.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const decodedMagnet = decodeURIComponent(magnetURI);
|
|
||||||
|
|
||||||
if (this.currentMagnetUri === decodedMagnet) {
|
|
||||||
this.log("Same video requested - already playing");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.currentMagnetUri = decodedMagnet;
|
|
||||||
|
|
||||||
this.playerModal.style.display = "flex";
|
|
||||||
this.playerModal.classList.remove("hidden");
|
|
||||||
|
|
||||||
// Re-fetch the latest from relays
|
|
||||||
const videos = await nostrClient.fetchVideos();
|
|
||||||
const video = videos.find((v) => v.magnet === decodedMagnet);
|
|
||||||
|
|
||||||
if (!video) {
|
|
||||||
this.showError("Video data not found.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Decrypt only once if user owns it
|
|
||||||
if (
|
|
||||||
video.isPrivate &&
|
|
||||||
video.pubkey === this.pubkey &&
|
|
||||||
!video.alreadyDecrypted
|
|
||||||
) {
|
|
||||||
this.log("User owns a private video => decrypting magnet link...");
|
|
||||||
video.magnet = fakeDecrypt(video.magnet);
|
|
||||||
// Mark it so we don't do it again
|
|
||||||
video.alreadyDecrypted = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
const finalMagnet = video.magnet;
|
|
||||||
|
|
||||||
// Profile fetch
|
|
||||||
let creatorProfile = {
|
|
||||||
name: "Unknown",
|
|
||||||
picture: `https://robohash.org/${video.pubkey}`,
|
|
||||||
};
|
|
||||||
try {
|
|
||||||
const userEvents = await nostrClient.pool.list(nostrClient.relays, [
|
|
||||||
{
|
|
||||||
kinds: [0],
|
|
||||||
authors: [video.pubkey],
|
|
||||||
limit: 1,
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Ensure userEvents isn't empty before accessing [0]
|
|
||||||
if (userEvents.length > 0 && userEvents[0]?.content) {
|
|
||||||
const profile = JSON.parse(userEvents[0].content);
|
|
||||||
creatorProfile = {
|
|
||||||
name: profile.name || profile.display_name || "Unknown",
|
|
||||||
picture: profile.picture || `https://robohash.org/${video.pubkey}`,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
this.log("Error fetching creator profile:", error);
|
|
||||||
}
|
|
||||||
|
|
||||||
let creatorNpub = "Unknown";
|
|
||||||
try {
|
|
||||||
creatorNpub = window.NostrTools.nip19.npubEncode(video.pubkey);
|
|
||||||
} catch (error) {
|
|
||||||
this.log("Error converting pubkey to npub:", error);
|
|
||||||
creatorNpub = video.pubkey;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.videoTitle.textContent = video.title || "Untitled";
|
|
||||||
this.videoDescription.textContent =
|
|
||||||
video.description || "No description available.";
|
|
||||||
this.videoTimestamp.textContent = formatTimeAgo(video.created_at);
|
|
||||||
|
|
||||||
this.creatorName.textContent = creatorProfile.name;
|
|
||||||
this.creatorNpub.textContent = `${creatorNpub.slice(
|
|
||||||
0,
|
|
||||||
8
|
|
||||||
)}...${creatorNpub.slice(-4)}`;
|
|
||||||
this.creatorAvatar.src = creatorProfile.picture;
|
|
||||||
this.creatorAvatar.alt = creatorProfile.name;
|
|
||||||
|
|
||||||
this.log("Starting video stream with:", finalMagnet);
|
|
||||||
await torrentClient.streamVideo(finalMagnet, this.modalVideo);
|
|
||||||
|
|
||||||
const updateInterval = setInterval(() => {
|
|
||||||
if (!document.body.contains(this.modalVideo)) {
|
|
||||||
clearInterval(updateInterval);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const status = document.getElementById("status");
|
|
||||||
const progress = document.getElementById("progress");
|
|
||||||
const peers = document.getElementById("peers");
|
|
||||||
const speed = document.getElementById("speed");
|
|
||||||
const downloaded = document.getElementById("downloaded");
|
|
||||||
|
|
||||||
if (status) this.modalStatus.textContent = status.textContent;
|
|
||||||
if (progress) this.modalProgress.style.width = progress.style.width;
|
|
||||||
if (peers) this.modalPeers.textContent = peers.textContent;
|
|
||||||
if (speed) this.modalSpeed.textContent = speed.textContent;
|
|
||||||
if (downloaded)
|
|
||||||
this.modalDownloaded.textContent = downloaded.textContent;
|
|
||||||
}, 1000);
|
|
||||||
} catch (error) {
|
|
||||||
this.log("Error in playVideo:", error);
|
|
||||||
this.showError(`Playback error: ${error.message}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
updateTorrentStatus(torrent) {
|
|
||||||
if (!torrent) return;
|
|
||||||
|
|
||||||
this.modalStatus.textContent = torrent.status;
|
|
||||||
this.modalProgress.style.width = `${(torrent.progress * 100).toFixed(2)}%`;
|
|
||||||
this.modalPeers.textContent = `Peers: ${torrent.numPeers}`;
|
|
||||||
this.modalSpeed.textContent = `${(torrent.downloadSpeed / 1024).toFixed(
|
|
||||||
2
|
|
||||||
)} KB/s`;
|
|
||||||
this.modalDownloaded.textContent = `${(
|
|
||||||
torrent.downloaded /
|
|
||||||
(1024 * 1024)
|
|
||||||
).toFixed(2)} MB / ${(torrent.length / (1024 * 1024)).toFixed(2)} MB`;
|
|
||||||
|
|
||||||
if (torrent.ready) {
|
|
||||||
this.modalStatus.textContent = "Ready to play";
|
|
||||||
} else {
|
|
||||||
setTimeout(() => this.updateTorrentStatus(torrent), 1000);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Allows the user to edit a video note (only if they are the owner).
|
|
||||||
* We reuse the note's existing d tag via nostrClient.editVideo.
|
|
||||||
*/
|
|
||||||
async handleEditVideo(index) {
|
|
||||||
try {
|
|
||||||
// CHANGE: Get videos through videoList component
|
|
||||||
const videos = await nostrClient.fetchVideos();
|
|
||||||
const video = videos[index];
|
|
||||||
|
|
||||||
if (!this.pubkey) {
|
|
||||||
this.showError("Please login to edit videos.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (video.pubkey !== this.pubkey) {
|
|
||||||
this.showError("You do not own this video.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Prompt for new fields or keep old
|
|
||||||
const newTitle = prompt(
|
|
||||||
"New Title? (Leave blank to keep existing)",
|
|
||||||
video.title
|
|
||||||
);
|
|
||||||
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
|
|
||||||
const wantPrivate = confirm("Make this video private? OK=Yes, Cancel=No");
|
|
||||||
|
|
||||||
// Fallback to old if user typed nothing
|
|
||||||
const title =
|
|
||||||
newTitle === null || newTitle.trim() === ""
|
|
||||||
? video.title
|
|
||||||
: 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
|
|
||||||
const updatedData = {
|
|
||||||
version: video.version || 2, // keep old version or set 2
|
|
||||||
isPrivate: wantPrivate,
|
|
||||||
title,
|
|
||||||
magnet,
|
|
||||||
thumbnail,
|
|
||||||
description,
|
|
||||||
mode: isDevMode ? "dev" : "live",
|
|
||||||
};
|
|
||||||
|
|
||||||
// Edit
|
|
||||||
const originalEvent = {
|
|
||||||
id: video.id,
|
|
||||||
pubkey: video.pubkey,
|
|
||||||
tags: video.tags,
|
|
||||||
};
|
|
||||||
await nostrClient.editVideo(originalEvent, updatedData, this.pubkey);
|
|
||||||
this.showSuccess("Video updated successfully!");
|
|
||||||
await videoList.loadVideos();
|
|
||||||
} catch (err) {
|
|
||||||
this.log("Failed to edit video:", err.message);
|
|
||||||
this.showError("Failed to edit video. Please try again later.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* ADDED FOR VERSIONING/PRIVATE/DELETE:
|
|
||||||
* Allows the user to delete (soft-delete) a video by marking it as deleted.
|
|
||||||
*/
|
|
||||||
async handleDeleteVideo(index) {
|
|
||||||
try {
|
|
||||||
// CHANGE: Get videos through videoList component
|
|
||||||
const videos = await nostrClient.fetchVideos();
|
|
||||||
const video = videos[index];
|
|
||||||
|
|
||||||
if (!this.pubkey) {
|
|
||||||
this.showError("Please login to delete videos.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (video.pubkey !== this.pubkey) {
|
|
||||||
this.showError("You do not own this video.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
!confirm(
|
|
||||||
`Are you sure you want to delete "${video.title}"? This action cannot be undone.`
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const originalEvent = {
|
|
||||||
id: video.id,
|
|
||||||
pubkey: video.pubkey,
|
|
||||||
tags: video.tags,
|
|
||||||
};
|
|
||||||
|
|
||||||
await nostrClient.deleteVideo(originalEvent, this.pubkey);
|
|
||||||
this.showSuccess("Video deleted (hidden) successfully!");
|
|
||||||
// CHANGE: Use videoList component to refresh
|
|
||||||
await videoList.loadVideos();
|
|
||||||
} catch (err) {
|
|
||||||
this.log("Failed to delete video:", err.message);
|
|
||||||
this.showError("Failed to delete video. Please try again later.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const app = new bitvidApp();
|
|
||||||
app.init();
|
|
||||||
window.app = app;
|
|
@@ -1,285 +0,0 @@
|
|||||||
// js/components/VideoList.js
|
|
||||||
import { nostrClient } from "../nostr.js";
|
|
||||||
import { formatTimeAgo } from "../utils/timeUtils.js";
|
|
||||||
import { escapeHTML } from "../utils/htmlUtils.js";
|
|
||||||
|
|
||||||
export class VideoList {
|
|
||||||
constructor() {
|
|
||||||
this.videoList = document.getElementById("videoList");
|
|
||||||
this.pubkey = null; // We'll need this for private video filtering
|
|
||||||
}
|
|
||||||
|
|
||||||
setPubkey(pubkey) {
|
|
||||||
this.pubkey = pubkey;
|
|
||||||
}
|
|
||||||
|
|
||||||
async loadVideos() {
|
|
||||||
console.log("Starting loadVideos...");
|
|
||||||
try {
|
|
||||||
const videos = await nostrClient.fetchVideos();
|
|
||||||
console.log("Raw videos from nostrClient:", videos);
|
|
||||||
|
|
||||||
if (!videos) {
|
|
||||||
console.log("No videos received");
|
|
||||||
throw new Error("No videos received from relays");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert to array if not already
|
|
||||||
const videosArray = Array.isArray(videos) ? videos : [videos];
|
|
||||||
|
|
||||||
// Filter private videos
|
|
||||||
const displayedVideos = videosArray.filter((video) => {
|
|
||||||
if (!video.isPrivate) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return this.pubkey && video.pubkey === this.pubkey;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (displayedVideos.length === 0) {
|
|
||||||
console.log("No valid videos found after filtering.");
|
|
||||||
this.renderEmptyState(
|
|
||||||
"No public videos available yet. Be the first to upload one!"
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("Processing filtered videos:", displayedVideos);
|
|
||||||
await this.renderVideoList(displayedVideos);
|
|
||||||
console.log(`Rendered ${displayedVideos.length} videos successfully`);
|
|
||||||
} catch (error) {
|
|
||||||
console.log("Failed to fetch videos:", error);
|
|
||||||
this.renderEmptyState(
|
|
||||||
"No videos available at the moment. Please try again later."
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
renderEmptyState(message) {
|
|
||||||
if (this.videoList) {
|
|
||||||
this.videoList.innerHTML = `
|
|
||||||
<p class="text-center text-gray-500">
|
|
||||||
${escapeHTML(message)}
|
|
||||||
</p>`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async renderVideoList(videos) {
|
|
||||||
try {
|
|
||||||
console.log("RENDER VIDEO LIST - Start", {
|
|
||||||
videosReceived: videos,
|
|
||||||
videosCount: videos ? videos.length : "N/A",
|
|
||||||
videosType: typeof videos,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!videos || videos.length === 0) {
|
|
||||||
this.renderEmptyState("No videos found.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sort by creation date
|
|
||||||
const videoArray = [...videos].sort(
|
|
||||||
(a, b) => b.created_at - a.created_at
|
|
||||||
);
|
|
||||||
|
|
||||||
// Fetch user profiles
|
|
||||||
const userProfiles = await this.fetchUserProfiles(videoArray);
|
|
||||||
|
|
||||||
// Build HTML for each video
|
|
||||||
const renderedVideos = videoArray
|
|
||||||
.map((video, index) => this.renderVideoCard(video, index, userProfiles))
|
|
||||||
.filter(Boolean);
|
|
||||||
|
|
||||||
if (renderedVideos.length === 0) {
|
|
||||||
this.renderEmptyState("No valid videos to display.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.videoList.innerHTML = renderedVideos.join("");
|
|
||||||
console.log("Videos rendered successfully");
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Rendering error:", error);
|
|
||||||
this.renderEmptyState("Error loading videos.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fetchUserProfiles(videos) {
|
|
||||||
const userProfiles = new Map();
|
|
||||||
const uniquePubkeys = [...new Set(videos.map((v) => v.pubkey))];
|
|
||||||
|
|
||||||
for (const pubkey of uniquePubkeys) {
|
|
||||||
try {
|
|
||||||
const profile = await nostrClient.fetchUserProfile(pubkey);
|
|
||||||
userProfiles.set(pubkey, profile);
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`Profile fetch error for ${pubkey}:`, error);
|
|
||||||
userProfiles.set(pubkey, {
|
|
||||||
name: "Unknown",
|
|
||||||
picture: `https://robohash.org/${pubkey}`,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return userProfiles;
|
|
||||||
}
|
|
||||||
|
|
||||||
renderVideoCard(video, index, userProfiles) {
|
|
||||||
try {
|
|
||||||
if (!this.validateVideo(video, index)) {
|
|
||||||
console.error(`Invalid video: ${video.title}`);
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
const profile = userProfiles.get(video.pubkey) || {
|
|
||||||
name: "Unknown",
|
|
||||||
picture: `https://robohash.org/${video.pubkey}`,
|
|
||||||
};
|
|
||||||
|
|
||||||
const canEdit = video.pubkey === this.pubkey;
|
|
||||||
const highlightClass =
|
|
||||||
video.isPrivate && canEdit
|
|
||||||
? "border-2 border-yellow-500"
|
|
||||||
: "border-none";
|
|
||||||
|
|
||||||
return `
|
|
||||||
<div class="video-card bg-gray-900 rounded-lg overflow-hidden shadow-lg hover:shadow-2xl transition-all duration-300 ${highlightClass}">
|
|
||||||
${this.renderThumbnail(video)}
|
|
||||||
${this.renderCardInfo(video, profile)}
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`Error processing video ${index}:`, error);
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
renderThumbnail(video) {
|
|
||||||
return `
|
|
||||||
<div
|
|
||||||
class="aspect-w-16 aspect-h-9 bg-gray-800 cursor-pointer relative group"
|
|
||||||
onclick="window.app.playVideo('${encodeURIComponent(video.magnet)}')"
|
|
||||||
>
|
|
||||||
${
|
|
||||||
video.thumbnail
|
|
||||||
? this.renderThumbnailImage(video)
|
|
||||||
: this.renderPlaceholderThumbnail()
|
|
||||||
}
|
|
||||||
<div class="absolute inset-0 bg-black bg-opacity-0 group-hover:bg-opacity-20 transition-opacity duration-300"></div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
renderThumbnailImage(video) {
|
|
||||||
return `
|
|
||||||
<img
|
|
||||||
src="${escapeHTML(video.thumbnail)}"
|
|
||||||
alt="${escapeHTML(video.title)}"
|
|
||||||
class="w-full h-full object-cover"
|
|
||||||
>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
renderPlaceholderThumbnail() {
|
|
||||||
return `
|
|
||||||
<div class="flex items-center justify-center h-full bg-gray-800">
|
|
||||||
<svg class="w-16 h-16 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
|
||||||
d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z" />
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
|
||||||
d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
renderCardInfo(video, profile) {
|
|
||||||
const timeAgo = formatTimeAgo(video.created_at);
|
|
||||||
const canEdit = video.pubkey === this.pubkey;
|
|
||||||
|
|
||||||
return `
|
|
||||||
<div class="p-4">
|
|
||||||
<h3 class="text-lg font-bold text-white line-clamp-2 hover:text-blue-400 cursor-pointer mb-3"
|
|
||||||
onclick="window.app.playVideo('${encodeURIComponent(
|
|
||||||
video.magnet
|
|
||||||
)}')">
|
|
||||||
${escapeHTML(video.title)}
|
|
||||||
</h3>
|
|
||||||
|
|
||||||
<div class="flex items-center justify-between">
|
|
||||||
<div class="flex items-center space-x-3">
|
|
||||||
<div class="w-8 h-8 rounded-full bg-gray-700 overflow-hidden">
|
|
||||||
<img src="${escapeHTML(profile.picture)}"
|
|
||||||
alt="${profile.name}"
|
|
||||||
class="w-full h-full object-cover">
|
|
||||||
</div>
|
|
||||||
<div class="min-w-0">
|
|
||||||
<p class="text-sm text-gray-400 hover:text-gray-300 cursor-pointer">
|
|
||||||
${escapeHTML(profile.name)}
|
|
||||||
</p>
|
|
||||||
<div class="flex items-center text-xs text-gray-500 mt-1">
|
|
||||||
<span>${timeAgo}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
${this.renderGearMenu(video, canEdit)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
renderGearMenu(video, canEdit) {
|
|
||||||
if (!canEdit) return "";
|
|
||||||
|
|
||||||
return `
|
|
||||||
<div class="relative inline-block ml-3 overflow-visible">
|
|
||||||
<button type="button"
|
|
||||||
class="inline-flex items-center p-2 rounded-full text-gray-400 hover:text-gray-200 hover:bg-gray-800 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
||||||
onclick="document.getElementById('settingsDropdown-${video.id}').classList.toggle('hidden')">
|
|
||||||
<img src="assets/svg/video-settings-gear.svg"
|
|
||||||
alt="Settings"
|
|
||||||
class="w-5 h-5"/>
|
|
||||||
</button>
|
|
||||||
<div id="settingsDropdown-${video.id}"
|
|
||||||
class="hidden absolute right-0 bottom-full mb-2 w-32 rounded-md shadow-lg bg-gray-800 ring-1 ring-black ring-opacity-5 z-50">
|
|
||||||
<div class="py-1">
|
|
||||||
<button class="block w-full text-left px-4 py-2 text-sm text-gray-100 hover:bg-gray-700"
|
|
||||||
onclick="app.handleEditVideo('${video.id}'); document.getElementById('settingsDropdown-${video.id}').classList.add('hidden');">
|
|
||||||
Edit
|
|
||||||
</button>
|
|
||||||
<button class="block w-full text-left px-4 py-2 text-sm text-red-400 hover:bg-red-700 hover:text-white"
|
|
||||||
onclick="app.handleDeleteVideo('${video.id}'); document.getElementById('settingsDropdown-${video.id}').classList.add('hidden');">
|
|
||||||
Delete
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
validateVideo(video, index) {
|
|
||||||
const validationResults = {
|
|
||||||
hasId: Boolean(video?.id),
|
|
||||||
isValidId: typeof video?.id === "string" && video.id.trim().length > 0,
|
|
||||||
hasVideo: Boolean(video),
|
|
||||||
hasTitle: Boolean(video?.title),
|
|
||||||
hasMagnet: Boolean(video?.magnet),
|
|
||||||
hasMode: Boolean(video?.mode),
|
|
||||||
hasPubkey: Boolean(video?.pubkey),
|
|
||||||
isValidTitle: typeof video?.title === "string" && video.title.length > 0,
|
|
||||||
isValidMagnet:
|
|
||||||
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);
|
|
||||||
console.log(
|
|
||||||
`Video ${video?.title} validation results:`,
|
|
||||||
validationResults,
|
|
||||||
passed ? "PASSED" : "FAILED"
|
|
||||||
);
|
|
||||||
|
|
||||||
return passed;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const videoList = new VideoList();
|
|
@@ -1,187 +0,0 @@
|
|||||||
// js/components/VideoPlayer.js
|
|
||||||
|
|
||||||
export class VideoPlayer {
|
|
||||||
constructor() {
|
|
||||||
// Initialize these as null - they'll be set after modal loads
|
|
||||||
this.playerModal = null;
|
|
||||||
this.modalVideo = null;
|
|
||||||
this.modalStatus = null;
|
|
||||||
this.modalProgress = null;
|
|
||||||
this.modalPeers = null;
|
|
||||||
this.modalSpeed = null;
|
|
||||||
this.modalDownloaded = null;
|
|
||||||
this.closePlayerBtn = null;
|
|
||||||
this.videoTitle = null;
|
|
||||||
this.videoDescription = null;
|
|
||||||
this.videoTimestamp = null;
|
|
||||||
this.creatorAvatar = null;
|
|
||||||
this.creatorName = null;
|
|
||||||
this.creatorNpub = null;
|
|
||||||
this.currentMagnetUri = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
async initModal() {
|
|
||||||
try {
|
|
||||||
console.log("Starting modal initialization...");
|
|
||||||
const response = await fetch("components/video-modal.html");
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`HTTP error! status: ${response.status}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const html = await response.text();
|
|
||||||
console.log("Modal HTML loaded successfully");
|
|
||||||
|
|
||||||
const modalContainer = document.getElementById("modalContainer");
|
|
||||||
if (!modalContainer) {
|
|
||||||
throw new Error("Modal container element not found!");
|
|
||||||
}
|
|
||||||
|
|
||||||
modalContainer.innerHTML = html;
|
|
||||||
console.log("Modal HTML inserted into DOM");
|
|
||||||
|
|
||||||
this.updateModalElements();
|
|
||||||
await this.setupEventListeners();
|
|
||||||
|
|
||||||
console.log("Modal initialization completed successfully");
|
|
||||||
return true;
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Modal initialization failed:", error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
updateModalElements() {
|
|
||||||
// Update Modal Elements
|
|
||||||
this.playerModal = document.getElementById("playerModal");
|
|
||||||
this.modalVideo = document.getElementById("modalVideo");
|
|
||||||
this.modalStatus = document.getElementById("modalStatus");
|
|
||||||
this.modalProgress = document.getElementById("modalProgress");
|
|
||||||
this.modalPeers = document.getElementById("modalPeers");
|
|
||||||
this.modalSpeed = document.getElementById("modalSpeed");
|
|
||||||
this.modalDownloaded = document.getElementById("modalDownloaded");
|
|
||||||
this.closePlayerBtn = document.getElementById("closeModal");
|
|
||||||
|
|
||||||
// Update Video Info Elements
|
|
||||||
this.videoTitle = document.getElementById("videoTitle");
|
|
||||||
this.videoDescription = document.getElementById("videoDescription");
|
|
||||||
this.videoTimestamp = document.getElementById("videoTimestamp");
|
|
||||||
|
|
||||||
// Update Creator Info Elements
|
|
||||||
this.creatorAvatar = document.getElementById("creatorAvatar");
|
|
||||||
this.creatorName = document.getElementById("creatorName");
|
|
||||||
this.creatorNpub = document.getElementById("creatorNpub");
|
|
||||||
|
|
||||||
this.setupScrollBehavior();
|
|
||||||
}
|
|
||||||
|
|
||||||
setupScrollBehavior() {
|
|
||||||
// Add scroll behavior for nav
|
|
||||||
let lastScrollY = 0;
|
|
||||||
const modalNav = document.getElementById("modalNav");
|
|
||||||
|
|
||||||
if (this.playerModal && modalNav) {
|
|
||||||
this.playerModal.addEventListener("scroll", (e) => {
|
|
||||||
const currentScrollY = e.target.scrollTop;
|
|
||||||
const shouldShowNav =
|
|
||||||
currentScrollY <= lastScrollY || currentScrollY < 50;
|
|
||||||
modalNav.style.transform = shouldShowNav
|
|
||||||
? "translateY(0)"
|
|
||||||
: "translateY(-100%)";
|
|
||||||
lastScrollY = currentScrollY;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async setupEventListeners() {
|
|
||||||
// Set up modal close handler
|
|
||||||
if (this.closePlayerBtn) {
|
|
||||||
this.closePlayerBtn.addEventListener("click", () => this.hide());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close Modal by clicking outside content
|
|
||||||
if (this.playerModal) {
|
|
||||||
this.playerModal.addEventListener("click", async (e) => {
|
|
||||||
if (e.target === this.playerModal) {
|
|
||||||
await this.hide();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Video error handling
|
|
||||||
if (this.modalVideo) {
|
|
||||||
this.setupVideoEventListeners();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setupVideoEventListeners() {
|
|
||||||
this.modalVideo.addEventListener("error", (e) => {
|
|
||||||
const error = e.target.error;
|
|
||||||
console.log("Modal video error:", error);
|
|
||||||
if (error) {
|
|
||||||
console.log("Error code:", error.code);
|
|
||||||
console.log("Error message:", error.message);
|
|
||||||
// You'll need to implement showError or pass it as a callback
|
|
||||||
// this.showError(`Video playback error: ${error.message || "Unknown error"}`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
this.modalVideo.addEventListener("loadstart", () => {
|
|
||||||
console.log("Video loadstart event fired");
|
|
||||||
});
|
|
||||||
|
|
||||||
this.modalVideo.addEventListener("loadedmetadata", () => {
|
|
||||||
console.log("Video loadedmetadata event fired");
|
|
||||||
});
|
|
||||||
|
|
||||||
this.modalVideo.addEventListener("canplay", () => {
|
|
||||||
console.log("Video canplay event fired");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async hide() {
|
|
||||||
await this.cleanup();
|
|
||||||
if (this.playerModal) {
|
|
||||||
this.playerModal.style.display = "none";
|
|
||||||
this.playerModal.classList.add("hidden");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async cleanup() {
|
|
||||||
if (this.modalVideo) {
|
|
||||||
this.modalVideo.pause();
|
|
||||||
this.modalVideo.src = "";
|
|
||||||
this.modalVideo.load();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
show() {
|
|
||||||
if (this.playerModal) {
|
|
||||||
this.playerModal.style.display = "flex";
|
|
||||||
this.playerModal.classList.remove("hidden");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
updateTorrentStatus(torrent) {
|
|
||||||
if (!torrent) return;
|
|
||||||
|
|
||||||
this.modalStatus.textContent = torrent.status;
|
|
||||||
this.modalProgress.style.width = `${(torrent.progress * 100).toFixed(2)}%`;
|
|
||||||
this.modalPeers.textContent = `Peers: ${torrent.numPeers}`;
|
|
||||||
this.modalSpeed.textContent = `${(torrent.downloadSpeed / 1024).toFixed(
|
|
||||||
2
|
|
||||||
)} KB/s`;
|
|
||||||
this.modalDownloaded.textContent = `${(
|
|
||||||
torrent.downloaded /
|
|
||||||
(1024 * 1024)
|
|
||||||
).toFixed(2)} MB / ${(torrent.length / (1024 * 1024)).toFixed(2)} MB`;
|
|
||||||
|
|
||||||
if (torrent.ready) {
|
|
||||||
this.modalStatus.textContent = "Ready to play";
|
|
||||||
} else {
|
|
||||||
setTimeout(() => this.updateTorrentStatus(torrent), 1000);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const videoPlayer = new VideoPlayer();
|
|
@@ -1,4 +0,0 @@
|
|||||||
// js/config.js
|
|
||||||
|
|
||||||
export const isDevMode = true; // Set to false for production
|
|
||||||
export const isWhitelistEnabled = true; // Set to false to allow all non-blacklisted users
|
|
@@ -1,29 +0,0 @@
|
|||||||
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();
|
|
@@ -1,13 +0,0 @@
|
|||||||
// js/lists.js
|
|
||||||
|
|
||||||
const npubs = [
|
|
||||||
"npub13yarr7j6vjqjjkahd63dmr27curypehx45ucue286ac7sft27y0srnpmpe",
|
|
||||||
"npub15jnttpymeytm80hatjqcvhhqhzrhx6gxp8pq0wn93rhnu8s9h9dsha32lx",
|
|
||||||
"npub1j37gc05qpqzyrmdc5vetsc9h5qtstas7tr25j0n9sdpqxghz6m4q2ej6n8",
|
|
||||||
"npub1epvnvv3kskvpnmpqgnm2atevsmdferhp7dg2s0yc7uc0hdmqmgssx09tu2",
|
|
||||||
];
|
|
||||||
|
|
||||||
console.log("DEBUG: lists.js loaded, npubs:", npubs);
|
|
||||||
|
|
||||||
export const initialWhitelist = npubs;
|
|
||||||
export const initialBlacklist = [""];
|
|
@@ -1,677 +0,0 @@
|
|||||||
// js/nostr.js
|
|
||||||
|
|
||||||
import { isDevMode } from "./config.js";
|
|
||||||
import { accessControl } from "./accessControl.js";
|
|
||||||
|
|
||||||
const RELAY_URLS = [
|
|
||||||
"wss://relay.damus.io",
|
|
||||||
"wss://nos.lol",
|
|
||||||
"wss://relay.snort.social",
|
|
||||||
"wss://nostr.wine",
|
|
||||||
];
|
|
||||||
|
|
||||||
// Rate limiting for error logs
|
|
||||||
let errorLogCount = 0;
|
|
||||||
const MAX_ERROR_LOGS = 100; // Adjust as needed
|
|
||||||
|
|
||||||
function logErrorOnce(message, eventContent = null) {
|
|
||||||
if (errorLogCount < MAX_ERROR_LOGS) {
|
|
||||||
console.error(message);
|
|
||||||
if (eventContent) {
|
|
||||||
console.log(`Event Content: ${eventContent}`);
|
|
||||||
}
|
|
||||||
errorLogCount++;
|
|
||||||
}
|
|
||||||
if (errorLogCount === MAX_ERROR_LOGS) {
|
|
||||||
console.error(
|
|
||||||
"Maximum error log limit reached. Further errors will be suppressed."
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A very naive "encryption" function that just reverses the string.
|
|
||||||
* In a real app, use a proper crypto library (AES-GCM, ECDH, etc.).
|
|
||||||
*/
|
|
||||||
function fakeEncrypt(magnet) {
|
|
||||||
return magnet.split("").reverse().join("");
|
|
||||||
}
|
|
||||||
function fakeDecrypt(encrypted) {
|
|
||||||
return encrypted.split("").reverse().join("");
|
|
||||||
}
|
|
||||||
|
|
||||||
class NostrClient {
|
|
||||||
constructor() {
|
|
||||||
this.pool = null;
|
|
||||||
this.pubkey = null;
|
|
||||||
this.relays = RELAY_URLS;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initializes the Nostr client by connecting to relays.
|
|
||||||
*/
|
|
||||||
async init() {
|
|
||||||
if (isDevMode) console.log("Connecting to relays...");
|
|
||||||
|
|
||||||
try {
|
|
||||||
this.pool = new window.NostrTools.SimplePool();
|
|
||||||
const results = await this.connectToRelays();
|
|
||||||
const successfulRelays = results
|
|
||||||
.filter((r) => r.success)
|
|
||||||
.map((r) => r.url);
|
|
||||||
|
|
||||||
if (successfulRelays.length === 0) throw new Error("No relays connected");
|
|
||||||
|
|
||||||
if (isDevMode)
|
|
||||||
console.log(`Connected to ${successfulRelays.length} relay(s)`);
|
|
||||||
} catch (err) {
|
|
||||||
console.error("Nostr init failed:", err);
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper method to handle relay connections
|
|
||||||
async connectToRelays() {
|
|
||||||
return Promise.all(
|
|
||||||
this.relays.map(
|
|
||||||
(url) =>
|
|
||||||
new Promise((resolve) => {
|
|
||||||
const sub = this.pool.sub([url], [{ kinds: [0], limit: 1 }]);
|
|
||||||
const timeout = setTimeout(() => {
|
|
||||||
sub.unsub();
|
|
||||||
resolve({ url, success: false });
|
|
||||||
}, 5000);
|
|
||||||
|
|
||||||
const succeed = () => {
|
|
||||||
clearTimeout(timeout);
|
|
||||||
sub.unsub();
|
|
||||||
resolve({ url, success: true });
|
|
||||||
};
|
|
||||||
|
|
||||||
sub.on("event", succeed);
|
|
||||||
sub.on("eose", succeed);
|
|
||||||
})
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Logs in the user using a Nostr extension or by entering an NSEC key.
|
|
||||||
*/
|
|
||||||
async login() {
|
|
||||||
try {
|
|
||||||
if (!window.nostr) {
|
|
||||||
console.log("No Nostr extension found");
|
|
||||||
throw new Error(
|
|
||||||
"Please install a Nostr extension (like Alby or nos2x)."
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const pubkey = await window.nostr.getPublicKey();
|
|
||||||
const npub = window.NostrTools.nip19.npubEncode(pubkey);
|
|
||||||
|
|
||||||
// Debug logs
|
|
||||||
if (isDevMode) {
|
|
||||||
console.log("Got pubkey:", pubkey);
|
|
||||||
console.log("Converted to npub:", npub);
|
|
||||||
console.log("Whitelist:", accessControl.getWhitelist());
|
|
||||||
console.log("Blacklist:", accessControl.getBlacklist());
|
|
||||||
console.log("Is whitelisted?", accessControl.isWhitelisted(npub));
|
|
||||||
console.log("Is blacklisted?", accessControl.isBlacklisted(npub));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check access control
|
|
||||||
if (!accessControl.canAccess(npub)) {
|
|
||||||
if (accessControl.isBlacklisted(npub)) {
|
|
||||||
throw new Error(
|
|
||||||
"Your account has been blocked from accessing this platform."
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
throw new Error(
|
|
||||||
"Access is currently restricted to whitelisted users only."
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.pubkey = pubkey;
|
|
||||||
if (isDevMode)
|
|
||||||
console.log(
|
|
||||||
"Successfully logged in with extension. Public key:",
|
|
||||||
this.pubkey
|
|
||||||
);
|
|
||||||
return this.pubkey;
|
|
||||||
} catch (e) {
|
|
||||||
console.error("Login error:", e);
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Logs out the user.
|
|
||||||
*/
|
|
||||||
logout() {
|
|
||||||
this.pubkey = null;
|
|
||||||
if (isDevMode) console.log("User logged out.");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Decodes an NSEC key.
|
|
||||||
*/
|
|
||||||
decodeNsec(nsec) {
|
|
||||||
try {
|
|
||||||
const { data } = window.NostrTools.nip19.decode(nsec);
|
|
||||||
return data;
|
|
||||||
} catch (error) {
|
|
||||||
throw new Error("Invalid NSEC key.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Publishes a new video event to all relays (creates a brand-new note).
|
|
||||||
*/
|
|
||||||
async publishVideo(videoData, pubkey) {
|
|
||||||
if (!pubkey) {
|
|
||||||
throw new Error("User is not logged in.");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isDevMode) {
|
|
||||||
console.log("Publishing video with data:", videoData);
|
|
||||||
}
|
|
||||||
|
|
||||||
// If user sets "isPrivate = true", encrypt the magnet
|
|
||||||
let finalMagnet = videoData.magnet;
|
|
||||||
if (videoData.isPrivate === true) {
|
|
||||||
finalMagnet = fakeEncrypt(finalMagnet);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Default version is 1 if not specified
|
|
||||||
const version = videoData.version ?? 1;
|
|
||||||
|
|
||||||
const uniqueD = `${Date.now()}-${Math.random()
|
|
||||||
.toString(36)
|
|
||||||
.substring(2, 10)}`;
|
|
||||||
|
|
||||||
// Always mark "deleted" false for new posts
|
|
||||||
const contentObject = {
|
|
||||||
version,
|
|
||||||
deleted: false,
|
|
||||||
isPrivate: videoData.isPrivate || false,
|
|
||||||
title: videoData.title,
|
|
||||||
magnet: finalMagnet,
|
|
||||||
thumbnail: videoData.thumbnail,
|
|
||||||
description: videoData.description,
|
|
||||||
mode: videoData.mode,
|
|
||||||
};
|
|
||||||
|
|
||||||
const event = {
|
|
||||||
kind: 30078,
|
|
||||||
pubkey,
|
|
||||||
created_at: Math.floor(Date.now() / 1000),
|
|
||||||
tags: [
|
|
||||||
["t", "video"],
|
|
||||||
["d", uniqueD],
|
|
||||||
],
|
|
||||||
content: JSON.stringify(contentObject),
|
|
||||||
};
|
|
||||||
|
|
||||||
if (isDevMode) {
|
|
||||||
console.log("Event content after stringify:", event.content);
|
|
||||||
console.log("Using d tag:", uniqueD);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const signedEvent = await window.nostr.signEvent(event);
|
|
||||||
if (isDevMode) {
|
|
||||||
console.log("Signed event:", signedEvent);
|
|
||||||
}
|
|
||||||
|
|
||||||
await Promise.all(
|
|
||||||
this.relays.map(async (url) => {
|
|
||||||
try {
|
|
||||||
await this.pool.publish([url], signedEvent);
|
|
||||||
if (isDevMode) {
|
|
||||||
console.log(`Event published to ${url}`);
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
if (isDevMode) {
|
|
||||||
console.error(`Failed to publish to ${url}:`, err.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
return signedEvent;
|
|
||||||
} catch (error) {
|
|
||||||
if (isDevMode) {
|
|
||||||
console.error("Failed to sign event:", error.message);
|
|
||||||
}
|
|
||||||
throw new Error("Failed to sign event.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Edits an existing video event by reusing the same "d" tag.
|
|
||||||
* Allows toggling isPrivate on/off and re-encrypting or decrypting the magnet.
|
|
||||||
*/
|
|
||||||
// Minimal fix: ensures we only ever encrypt once per edit operation
|
|
||||||
async editVideo(originalEvent, updatedVideoData, pubkey) {
|
|
||||||
if (!pubkey) {
|
|
||||||
throw new Error("User is not logged in.");
|
|
||||||
}
|
|
||||||
if (originalEvent.pubkey !== pubkey) {
|
|
||||||
throw new Error("You do not own this event (different pubkey).");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isDevMode) {
|
|
||||||
console.log("Editing video event:", originalEvent);
|
|
||||||
console.log("New video data:", updatedVideoData);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Grab the d tag from the original event
|
|
||||||
const dTag = originalEvent.tags.find((tag) => tag[0] === "d");
|
|
||||||
if (!dTag) {
|
|
||||||
throw new Error(
|
|
||||||
'This event has no "d" tag, cannot edit as addressable kind=30078.'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
const existingD = dTag[1];
|
|
||||||
|
|
||||||
// Parse old content
|
|
||||||
const oldContent = JSON.parse(originalEvent.content || "{}");
|
|
||||||
if (isDevMode) {
|
|
||||||
console.log("Old content:", oldContent);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Keep old version & deleted status
|
|
||||||
const oldVersion = oldContent.version ?? 1;
|
|
||||||
const oldDeleted = oldContent.deleted === true;
|
|
||||||
const newVersion = updatedVideoData.version ?? oldVersion;
|
|
||||||
|
|
||||||
const oldWasPrivate = oldContent.isPrivate === true;
|
|
||||||
|
|
||||||
// 1) If old was private, decrypt the old magnet once => oldPlainMagnet
|
|
||||||
let oldPlainMagnet = oldContent.magnet || "";
|
|
||||||
if (oldWasPrivate && oldPlainMagnet) {
|
|
||||||
oldPlainMagnet = fakeDecrypt(oldPlainMagnet);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2) If updatedVideoData.isPrivate is explicitly set, use that; else keep the old isPrivate
|
|
||||||
const newIsPrivate =
|
|
||||||
typeof updatedVideoData.isPrivate === "boolean"
|
|
||||||
? updatedVideoData.isPrivate
|
|
||||||
: oldContent.isPrivate ?? false;
|
|
||||||
|
|
||||||
// 3) The user might type a new magnet or keep oldPlainMagnet
|
|
||||||
const userTypedMagnet = (updatedVideoData.magnet || "").trim();
|
|
||||||
const finalPlainMagnet = userTypedMagnet || oldPlainMagnet;
|
|
||||||
|
|
||||||
// 4) If new is private => encrypt finalPlainMagnet once; otherwise store plaintext
|
|
||||||
let finalMagnet = finalPlainMagnet;
|
|
||||||
if (newIsPrivate) {
|
|
||||||
finalMagnet = fakeEncrypt(finalPlainMagnet);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build updated content
|
|
||||||
const contentObject = {
|
|
||||||
version: newVersion,
|
|
||||||
deleted: oldDeleted,
|
|
||||||
isPrivate: newIsPrivate,
|
|
||||||
title: updatedVideoData.title,
|
|
||||||
magnet: finalMagnet,
|
|
||||||
thumbnail: updatedVideoData.thumbnail,
|
|
||||||
description: updatedVideoData.description,
|
|
||||||
mode: updatedVideoData.mode,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (isDevMode) {
|
|
||||||
console.log("Building updated content object:", contentObject);
|
|
||||||
}
|
|
||||||
|
|
||||||
const event = {
|
|
||||||
kind: 30078,
|
|
||||||
pubkey,
|
|
||||||
created_at: Math.floor(Date.now() / 1000),
|
|
||||||
tags: [
|
|
||||||
["t", "video"],
|
|
||||||
["d", existingD],
|
|
||||||
],
|
|
||||||
content: JSON.stringify(contentObject),
|
|
||||||
};
|
|
||||||
|
|
||||||
if (isDevMode) {
|
|
||||||
console.log("Reusing d tag:", existingD);
|
|
||||||
console.log("Updated event content:", event.content);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const signedEvent = await window.nostr.signEvent(event);
|
|
||||||
if (isDevMode) {
|
|
||||||
console.log("Signed edited event:", signedEvent);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Publish to all relays
|
|
||||||
await Promise.all(
|
|
||||||
this.relays.map(async (url) => {
|
|
||||||
try {
|
|
||||||
await this.pool.publish([url], signedEvent);
|
|
||||||
if (isDevMode) {
|
|
||||||
console.log(
|
|
||||||
`Edited event published to ${url} (d="${existingD}")`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
if (isDevMode) {
|
|
||||||
console.error(
|
|
||||||
`Failed to publish edited event to ${url}:`,
|
|
||||||
err.message
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
return signedEvent;
|
|
||||||
} catch (error) {
|
|
||||||
if (isDevMode) {
|
|
||||||
console.error("Failed to sign edited event:", error.message);
|
|
||||||
}
|
|
||||||
throw new Error("Failed to sign edited event.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Soft-delete or hide an existing video by marking content as "deleted: true"
|
|
||||||
* and republishing with same (kind=30078, pubkey, d) address.
|
|
||||||
*/
|
|
||||||
async deleteVideo(originalEvent, pubkey) {
|
|
||||||
if (!pubkey) {
|
|
||||||
throw new Error("User is not logged in.");
|
|
||||||
}
|
|
||||||
if (originalEvent.pubkey !== pubkey) {
|
|
||||||
throw new Error("You do not own this event (different pubkey).");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isDevMode) {
|
|
||||||
console.log("Deleting video event:", originalEvent);
|
|
||||||
}
|
|
||||||
|
|
||||||
const dTag = originalEvent.tags.find((tag) => tag[0] === "d");
|
|
||||||
if (!dTag) {
|
|
||||||
throw new Error(
|
|
||||||
'This event has no "d" tag, cannot delete as addressable kind=30078.'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
const existingD = dTag[1];
|
|
||||||
|
|
||||||
const oldContent = JSON.parse(originalEvent.content || "{}");
|
|
||||||
const oldVersion = oldContent.version ?? 1;
|
|
||||||
|
|
||||||
const contentObject = {
|
|
||||||
version: oldVersion,
|
|
||||||
deleted: true,
|
|
||||||
title: oldContent.title || "",
|
|
||||||
magnet: "",
|
|
||||||
thumbnail: "",
|
|
||||||
description: "This video has been deleted.",
|
|
||||||
mode: oldContent.mode || "live",
|
|
||||||
isPrivate: oldContent.isPrivate || false,
|
|
||||||
};
|
|
||||||
|
|
||||||
const event = {
|
|
||||||
kind: 30078,
|
|
||||||
pubkey,
|
|
||||||
created_at: Math.floor(Date.now() / 1000),
|
|
||||||
tags: [
|
|
||||||
["t", "video"],
|
|
||||||
["d", existingD],
|
|
||||||
],
|
|
||||||
content: JSON.stringify(contentObject),
|
|
||||||
};
|
|
||||||
|
|
||||||
if (isDevMode) {
|
|
||||||
console.log("Reusing d tag for delete:", existingD);
|
|
||||||
console.log("Deleted event content:", event.content);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const signedEvent = await window.nostr.signEvent(event);
|
|
||||||
if (isDevMode) {
|
|
||||||
console.log("Signed deleted event:", signedEvent);
|
|
||||||
}
|
|
||||||
|
|
||||||
await Promise.all(
|
|
||||||
this.relays.map(async (url) => {
|
|
||||||
try {
|
|
||||||
await this.pool.publish([url], signedEvent);
|
|
||||||
if (isDevMode) {
|
|
||||||
console.log(
|
|
||||||
`Deleted event published to ${url} (d="${existingD}")`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
if (isDevMode) {
|
|
||||||
console.error(
|
|
||||||
`Failed to publish deleted event to ${url}:`,
|
|
||||||
err.message
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
return signedEvent;
|
|
||||||
} catch (error) {
|
|
||||||
if (isDevMode) {
|
|
||||||
console.error("Failed to sign deleted event:", error.message);
|
|
||||||
}
|
|
||||||
throw new Error("Failed to sign deleted event.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetches videos from all configured relays.
|
|
||||||
*/
|
|
||||||
async fetchVideos() {
|
|
||||||
const filter = {
|
|
||||||
kinds: [30078],
|
|
||||||
"#t": ["video"],
|
|
||||||
limit: 1000,
|
|
||||||
since: 0,
|
|
||||||
};
|
|
||||||
|
|
||||||
const videoEvents = new Map();
|
|
||||||
|
|
||||||
if (isDevMode) {
|
|
||||||
console.log("[fetchVideos] Starting fetch from all relays...");
|
|
||||||
console.log("[fetchVideos] Filter:", filter);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
await Promise.all(
|
|
||||||
this.relays.map(async (url) => {
|
|
||||||
if (isDevMode) console.log(`[fetchVideos] Querying relay: ${url}`);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const events = await this.pool.list([url], [filter]);
|
|
||||||
|
|
||||||
if (isDevMode) {
|
|
||||||
console.log(`Events from ${url}:`, events.length);
|
|
||||||
if (events.length > 0) {
|
|
||||||
events.forEach((evt, idx) => {
|
|
||||||
console.log(
|
|
||||||
`[fetchVideos] [${url}] Event[${idx}] ID: ${evt.id} | pubkey: ${evt.pubkey} | created_at: ${evt.created_at}`
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
events.forEach((event) => {
|
|
||||||
try {
|
|
||||||
const content = JSON.parse(event.content);
|
|
||||||
|
|
||||||
// If deleted == true, it overrides older notes
|
|
||||||
if (content.deleted === true) {
|
|
||||||
videoEvents.delete(event.id);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we haven't seen this event.id before, store it
|
|
||||||
if (!videoEvents.has(event.id)) {
|
|
||||||
videoEvents.set(event.id, {
|
|
||||||
id: event.id,
|
|
||||||
version: content.version ?? 1,
|
|
||||||
isPrivate: content.isPrivate ?? false,
|
|
||||||
title: content.title || "",
|
|
||||||
magnet: content.magnet || "",
|
|
||||||
thumbnail: content.thumbnail || "",
|
|
||||||
description: content.description || "",
|
|
||||||
mode: content.mode || "live",
|
|
||||||
pubkey: event.pubkey,
|
|
||||||
created_at: event.created_at,
|
|
||||||
tags: event.tags,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (parseError) {
|
|
||||||
if (isDevMode) {
|
|
||||||
console.error(
|
|
||||||
"[fetchVideos] Event parsing error:",
|
|
||||||
parseError
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} catch (relayError) {
|
|
||||||
if (isDevMode) {
|
|
||||||
console.error(
|
|
||||||
`[fetchVideos] Error fetching from ${url}:`,
|
|
||||||
relayError
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
const videos = Array.from(videoEvents.values()).sort(
|
|
||||||
(a, b) => b.created_at - a.created_at
|
|
||||||
);
|
|
||||||
|
|
||||||
// Apply access control filtering
|
|
||||||
const filteredVideos = accessControl.filterVideos(videos);
|
|
||||||
|
|
||||||
if (isDevMode) {
|
|
||||||
console.log("[fetchVideos] All relays have responded.");
|
|
||||||
console.log(
|
|
||||||
`[fetchVideos] Total unique video events: ${videoEvents.size}`
|
|
||||||
);
|
|
||||||
console.log(
|
|
||||||
`[fetchVideos] Videos after filtering: ${filteredVideos.length}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return filteredVideos;
|
|
||||||
} catch (error) {
|
|
||||||
if (isDevMode) {
|
|
||||||
console.error("FETCH VIDEOS ERROR:", error);
|
|
||||||
}
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetches a user profile given a pubkey.
|
|
||||||
* Returns an object with 'name' and 'picture' properties.
|
|
||||||
*/
|
|
||||||
async fetchUserProfile(pubkey) {
|
|
||||||
if (!pubkey) {
|
|
||||||
throw new Error("Invalid pubkey provided.");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isDevMode) {
|
|
||||||
console.log(`Fetching profile for pubkey: ${pubkey}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const filter = {
|
|
||||||
kinds: [0], // Profile events
|
|
||||||
authors: [pubkey],
|
|
||||||
limit: 1,
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
|
||||||
const events = await this.pool.list(this.relays, [filter]);
|
|
||||||
|
|
||||||
if (events.length === 0) {
|
|
||||||
if (isDevMode) {
|
|
||||||
console.log(`No profile found for pubkey: ${pubkey}`);
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
name: "Unknown",
|
|
||||||
picture: `https://robohash.org/${pubkey}`,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const profileContent = JSON.parse(events[0].content || "{}");
|
|
||||||
|
|
||||||
const profile = {
|
|
||||||
name:
|
|
||||||
profileContent.name ||
|
|
||||||
profileContent.display_name ||
|
|
||||||
`User ${pubkey.slice(0, 8)}...`,
|
|
||||||
picture: profileContent.picture || `https://robohash.org/${pubkey}`,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (isDevMode) {
|
|
||||||
console.log(`Fetched profile for ${pubkey}:`, profile);
|
|
||||||
}
|
|
||||||
|
|
||||||
return profile;
|
|
||||||
} catch (error) {
|
|
||||||
logErrorOnce(`Error fetching profile for ${pubkey}:`, error.message);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validates video content structure.
|
|
||||||
*/
|
|
||||||
isValidVideo(content) {
|
|
||||||
try {
|
|
||||||
const isValid =
|
|
||||||
content &&
|
|
||||||
typeof content === "object" &&
|
|
||||||
typeof content.title === "string" &&
|
|
||||||
content.title.length > 0 &&
|
|
||||||
typeof content.magnet === "string" &&
|
|
||||||
content.magnet.length > 0 &&
|
|
||||||
typeof content.mode === "string" &&
|
|
||||||
["dev", "live"].includes(content.mode) &&
|
|
||||||
(typeof content.thumbnail === "string" ||
|
|
||||||
typeof content.thumbnail === "undefined") &&
|
|
||||||
(typeof content.description === "string" ||
|
|
||||||
typeof content.description === "undefined");
|
|
||||||
|
|
||||||
if (isDevMode && !isValid) {
|
|
||||||
console.log("Invalid video content:", content);
|
|
||||||
console.log("Validation details:", {
|
|
||||||
hasTitle: typeof content.title === "string",
|
|
||||||
hasMagnet: typeof content.magnet === "string",
|
|
||||||
hasMode: typeof content.mode === "string",
|
|
||||||
validThumbnail:
|
|
||||||
typeof content.thumbnail === "string" ||
|
|
||||||
typeof content.thumbnail === "undefined",
|
|
||||||
validDescription:
|
|
||||||
typeof content.description === "string" ||
|
|
||||||
typeof content.description === "undefined",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return isValid;
|
|
||||||
} catch (error) {
|
|
||||||
if (isDevMode) {
|
|
||||||
console.error("Error validating video:", error);
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const nostrClient = new NostrClient();
|
|
@@ -1,9 +0,0 @@
|
|||||||
// js/utils/htmlUtils.js
|
|
||||||
export function escapeHTML(unsafe) {
|
|
||||||
return unsafe
|
|
||||||
.replace(/&/g, "&")
|
|
||||||
.replace(/</g, "<")
|
|
||||||
.replace(/>/g, ">")
|
|
||||||
.replace(/"/g, """)
|
|
||||||
.replace(/'/g, "'");
|
|
||||||
}
|
|
@@ -1,21 +0,0 @@
|
|||||||
// js/utils/timeUtils.js
|
|
||||||
export function formatTimeAgo(timestamp) {
|
|
||||||
const seconds = Math.floor(Date.now() / 1000 - timestamp);
|
|
||||||
const intervals = {
|
|
||||||
year: 31536000,
|
|
||||||
month: 2592000,
|
|
||||||
week: 604800,
|
|
||||||
day: 86400,
|
|
||||||
hour: 3600,
|
|
||||||
minute: 60,
|
|
||||||
};
|
|
||||||
|
|
||||||
for (const [unit, secondsInUnit] of Object.entries(intervals)) {
|
|
||||||
const interval = Math.floor(seconds / secondsInUnit);
|
|
||||||
if (interval >= 1) {
|
|
||||||
return `${interval} ${unit}${interval === 1 ? "" : "s"} ago`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return "just now";
|
|
||||||
}
|
|
@@ -1,286 +0,0 @@
|
|||||||
// js/webtorrent.js
|
|
||||||
|
|
||||||
import WebTorrent from 'https://esm.sh/webtorrent'
|
|
||||||
|
|
||||||
export class TorrentClient {
|
|
||||||
constructor() {
|
|
||||||
this.client = new WebTorrent()
|
|
||||||
this.currentTorrent = null
|
|
||||||
this.TIMEOUT_DURATION = 60000 // 60 seconds
|
|
||||||
this.statsInterval = null
|
|
||||||
}
|
|
||||||
|
|
||||||
log(msg) {
|
|
||||||
console.log(msg)
|
|
||||||
}
|
|
||||||
|
|
||||||
async isBrave() {
|
|
||||||
return (navigator.brave?.isBrave && await navigator.brave.isBrave()) || false
|
|
||||||
}
|
|
||||||
|
|
||||||
async waitForServiceWorkerActivation(registration) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const timeout = setTimeout(() => {
|
|
||||||
reject(new Error('Service worker activation timeout'))
|
|
||||||
}, this.TIMEOUT_DURATION)
|
|
||||||
|
|
||||||
this.log('Waiting for service worker activation...')
|
|
||||||
|
|
||||||
const checkActivation = () => {
|
|
||||||
if (registration.active) {
|
|
||||||
clearTimeout(timeout)
|
|
||||||
this.log('Service worker is active')
|
|
||||||
resolve(registration)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
if (checkActivation()) return
|
|
||||||
|
|
||||||
registration.addEventListener('activate', () => {
|
|
||||||
checkActivation()
|
|
||||||
})
|
|
||||||
|
|
||||||
if (registration.waiting) {
|
|
||||||
this.log('Service worker is waiting, sending skip waiting message')
|
|
||||||
registration.waiting.postMessage({ type: 'SKIP_WAITING' })
|
|
||||||
}
|
|
||||||
|
|
||||||
registration.addEventListener('statechange', () => {
|
|
||||||
checkActivation()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
async setupServiceWorker() {
|
|
||||||
try {
|
|
||||||
const isBraveBrowser = await this.isBrave()
|
|
||||||
|
|
||||||
if (!window.isSecureContext) {
|
|
||||||
throw new Error('HTTPS or localhost required')
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!('serviceWorker' in navigator) || !navigator.serviceWorker) {
|
|
||||||
throw new Error('Service Worker not supported or disabled')
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isBraveBrowser) {
|
|
||||||
this.log('Checking Brave configuration...')
|
|
||||||
|
|
||||||
if (!navigator.serviceWorker) {
|
|
||||||
throw new Error('Please enable Service Workers in Brave Shield settings')
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
|
|
||||||
throw new Error('Please enable WebRTC in Brave Shield settings')
|
|
||||||
}
|
|
||||||
|
|
||||||
const registrations = await navigator.serviceWorker.getRegistrations()
|
|
||||||
for (const registration of registrations) {
|
|
||||||
await registration.unregister()
|
|
||||||
}
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 1000))
|
|
||||||
}
|
|
||||||
|
|
||||||
const currentPath = window.location.pathname
|
|
||||||
const basePath = currentPath.substring(0, currentPath.lastIndexOf('/') + 1)
|
|
||||||
|
|
||||||
this.log('Registering service worker...')
|
|
||||||
const registration = await navigator.serviceWorker.register('./sw.min.js', {
|
|
||||||
scope: basePath,
|
|
||||||
updateViaCache: 'none'
|
|
||||||
})
|
|
||||||
this.log('Service worker registered')
|
|
||||||
|
|
||||||
if (registration.installing) {
|
|
||||||
this.log('Waiting for installation...')
|
|
||||||
await new Promise((resolve, reject) => {
|
|
||||||
const timeout = setTimeout(() => {
|
|
||||||
reject(new Error('Installation timeout'))
|
|
||||||
}, this.TIMEOUT_DURATION)
|
|
||||||
|
|
||||||
registration.installing.addEventListener('statechange', (e) => {
|
|
||||||
this.log('Service worker state:', e.target.state)
|
|
||||||
if (e.target.state === 'activated' || e.target.state === 'redundant') {
|
|
||||||
clearTimeout(timeout)
|
|
||||||
resolve()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.waitForServiceWorkerActivation(registration)
|
|
||||||
this.log('Service worker activated')
|
|
||||||
|
|
||||||
const readyRegistration = await Promise.race([
|
|
||||||
navigator.serviceWorker.ready,
|
|
||||||
new Promise((_, reject) =>
|
|
||||||
setTimeout(() => reject(new Error('Service worker ready timeout')), this.TIMEOUT_DURATION)
|
|
||||||
)
|
|
||||||
])
|
|
||||||
|
|
||||||
if (!readyRegistration.active) {
|
|
||||||
throw new Error('Service worker not active after ready state')
|
|
||||||
}
|
|
||||||
|
|
||||||
this.log('Service worker ready')
|
|
||||||
return registration
|
|
||||||
} catch (error) {
|
|
||||||
this.log('Service worker setup error:', error)
|
|
||||||
throw error
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
formatBytes(bytes) {
|
|
||||||
if (bytes === 0) return '0 B'
|
|
||||||
const k = 1024
|
|
||||||
const sizes = ['B', 'KB', 'MB', 'GB']
|
|
||||||
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
|
||||||
return `${(bytes / Math.pow(k, i)).toFixed(1)} ${sizes[i]}`
|
|
||||||
}
|
|
||||||
|
|
||||||
async streamVideo(magnetURI, videoElement) {
|
|
||||||
try {
|
|
||||||
// Setup service worker first
|
|
||||||
const registration = await this.setupServiceWorker()
|
|
||||||
|
|
||||||
if (!registration || !registration.active) {
|
|
||||||
throw new Error('Service worker setup failed')
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create WebTorrent server AFTER service worker is ready
|
|
||||||
this.client.createServer({ controller: registration })
|
|
||||||
this.log('WebTorrent server created')
|
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
this.log('Starting torrent download')
|
|
||||||
this.client.add(magnetURI, torrent => {
|
|
||||||
this.log('Torrent added: ' + torrent.name)
|
|
||||||
const status = document.getElementById('status')
|
|
||||||
const progress = document.getElementById('progress')
|
|
||||||
const peers = document.getElementById('peers')
|
|
||||||
const speed = document.getElementById('speed')
|
|
||||||
const downloaded = document.getElementById('downloaded')
|
|
||||||
|
|
||||||
if (status) status.textContent = `Loading ${torrent.name}...`
|
|
||||||
|
|
||||||
const file = torrent.files.find(file =>
|
|
||||||
file.name.endsWith('.mp4') ||
|
|
||||||
file.name.endsWith('.webm') ||
|
|
||||||
file.name.endsWith('.mkv')
|
|
||||||
)
|
|
||||||
|
|
||||||
if (!file) {
|
|
||||||
const error = new Error('No compatible video file found in torrent')
|
|
||||||
this.log(error.message)
|
|
||||||
if (status) status.textContent = 'Error: No video file found'
|
|
||||||
reject(error)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
videoElement.muted = true
|
|
||||||
videoElement.crossOrigin = 'anonymous'
|
|
||||||
|
|
||||||
videoElement.addEventListener('error', (e) => {
|
|
||||||
const error = e.target.error
|
|
||||||
this.log('Video error:', error)
|
|
||||||
if (error) {
|
|
||||||
this.log('Error code:', error.code)
|
|
||||||
this.log('Error message:', error.message)
|
|
||||||
}
|
|
||||||
if (status) status.textContent = 'Error playing video. Try disabling Brave Shields.'
|
|
||||||
})
|
|
||||||
|
|
||||||
videoElement.addEventListener('canplay', () => {
|
|
||||||
const playPromise = videoElement.play()
|
|
||||||
if (playPromise !== undefined) {
|
|
||||||
playPromise
|
|
||||||
.then(() => this.log('Autoplay started'))
|
|
||||||
.catch(err => {
|
|
||||||
this.log('Autoplay failed:', err)
|
|
||||||
if (status) status.textContent = 'Click to play video'
|
|
||||||
videoElement.addEventListener('click', () => {
|
|
||||||
videoElement.play()
|
|
||||||
.then(() => this.log('Play started by user'))
|
|
||||||
.catch(err => this.log('Play failed:', err))
|
|
||||||
}, { once: true })
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
videoElement.addEventListener('loadedmetadata', () => {
|
|
||||||
this.log('Video metadata loaded')
|
|
||||||
if (videoElement.duration === Infinity || isNaN(videoElement.duration)) {
|
|
||||||
this.log('Invalid duration, attempting to fix...')
|
|
||||||
videoElement.currentTime = 1e101
|
|
||||||
videoElement.currentTime = 0
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
try {
|
|
||||||
file.streamTo(videoElement)
|
|
||||||
this.log('Streaming started')
|
|
||||||
|
|
||||||
// Update stats every second
|
|
||||||
this.statsInterval = setInterval(() => {
|
|
||||||
if (!document.body.contains(videoElement)) {
|
|
||||||
clearInterval(this.statsInterval)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const percentage = torrent.progress * 100
|
|
||||||
if (progress) progress.style.width = `${percentage}%`
|
|
||||||
if (peers) peers.textContent = `Peers: ${torrent.numPeers}`
|
|
||||||
if (speed) speed.textContent = `${this.formatBytes(torrent.downloadSpeed)}/s`
|
|
||||||
if (downloaded) downloaded.textContent =
|
|
||||||
`${this.formatBytes(torrent.downloaded)} / ${this.formatBytes(torrent.length)}`
|
|
||||||
|
|
||||||
if (status) {
|
|
||||||
status.textContent = torrent.progress === 1
|
|
||||||
? `${torrent.name}`
|
|
||||||
: `Loading ${torrent.name}...`
|
|
||||||
}
|
|
||||||
}, 1000)
|
|
||||||
|
|
||||||
this.currentTorrent = torrent
|
|
||||||
resolve()
|
|
||||||
} catch (error) {
|
|
||||||
this.log('Streaming error:', error)
|
|
||||||
if (status) status.textContent = 'Error starting video stream'
|
|
||||||
reject(error)
|
|
||||||
}
|
|
||||||
|
|
||||||
torrent.on('error', err => {
|
|
||||||
this.log('Torrent error:', err)
|
|
||||||
if (status) status.textContent = 'Error loading video'
|
|
||||||
clearInterval(this.statsInterval)
|
|
||||||
reject(err)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
this.log('Failed to setup video streaming:', error)
|
|
||||||
throw error
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async cleanup() {
|
|
||||||
try {
|
|
||||||
if (this.statsInterval) {
|
|
||||||
clearInterval(this.statsInterval)
|
|
||||||
}
|
|
||||||
if (this.currentTorrent) {
|
|
||||||
this.currentTorrent.destroy()
|
|
||||||
}
|
|
||||||
if (this.client) {
|
|
||||||
await this.client.destroy()
|
|
||||||
this.client = new WebTorrent() // Create a new client for future use
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
this.log('Cleanup error:', error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const torrentClient = new TorrentClient()
|
|
@@ -1,185 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
||||||
<title>bitvid | Roadmap</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="assets/favicon.ico" sizes="any" />
|
|
||||||
<link
|
|
||||||
rel="apple-touch-icon"
|
|
||||||
sizes="180x180"
|
|
||||||
href="assets/png/apple-touch-icon.png"
|
|
||||||
/>
|
|
||||||
<link
|
|
||||||
rel="icon"
|
|
||||||
type="image/png"
|
|
||||||
sizes="32x32"
|
|
||||||
href="assets/png/favicon-32x32.png"
|
|
||||||
/>
|
|
||||||
<link
|
|
||||||
rel="icon"
|
|
||||||
type="image/png"
|
|
||||||
sizes="16x16"
|
|
||||||
href="assets/png/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>
|
|
||||||
<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: k51qzi5uqu5dgwr4oejq9rk41aoe9zcupenby6iqecsk5byc7rx48uecd133a1
|
|
||||||
</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/roadmap.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>
|
|
@@ -1,41 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "bitvid - Decentralized Video Sharing",
|
|
||||||
"short_name": "bitvid",
|
|
||||||
"description": "seed. zap. subscribe.",
|
|
||||||
"icons": [
|
|
||||||
{
|
|
||||||
"src": "/android-chrome-192x192.png",
|
|
||||||
"sizes": "192x192",
|
|
||||||
"type": "image/png"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"src": "/android-chrome-512x512.png",
|
|
||||||
"sizes": "512x512",
|
|
||||||
"type": "image/png"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"src": "/apple-touch-icon.png",
|
|
||||||
"sizes": "180x180",
|
|
||||||
"type": "image/png"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"src": "assets/png/favicon-32x32.png",
|
|
||||||
"sizes": "32x32",
|
|
||||||
"type": "image/png"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"src": "assets/png/favicon-16x16.png",
|
|
||||||
"sizes": "16x16",
|
|
||||||
"type": "image/png"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"start_url": "/index.html",
|
|
||||||
"display": "standalone",
|
|
||||||
"background_color": "#0f172a",
|
|
||||||
"theme_color": "#0f172a",
|
|
||||||
"orientation": "portrait-primary",
|
|
||||||
"scope": "/",
|
|
||||||
"categories": ["video", "entertainment", "decentralized", "streaming"],
|
|
||||||
"related_applications": [],
|
|
||||||
"lang": "en"
|
|
||||||
}
|
|
132
refactoring/sw.min.js
vendored
@@ -1,132 +0,0 @@
|
|||||||
(() => {
|
|
||||||
"use strict";
|
|
||||||
|
|
||||||
let cancelled = false;
|
|
||||||
|
|
||||||
// Handle skip waiting message
|
|
||||||
self.addEventListener('message', event => {
|
|
||||||
if (event.data && event.data.type === 'SKIP_WAITING') {
|
|
||||||
self.skipWaiting()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// Immediately install and activate
|
|
||||||
self.addEventListener("install", () => {
|
|
||||||
self.skipWaiting()
|
|
||||||
})
|
|
||||||
|
|
||||||
// Claim clients on activation
|
|
||||||
self.addEventListener('activate', event => {
|
|
||||||
event.waitUntil(
|
|
||||||
Promise.all([
|
|
||||||
clients.claim(),
|
|
||||||
self.skipWaiting(),
|
|
||||||
caches.keys().then(cacheNames =>
|
|
||||||
Promise.all(cacheNames.map(cacheName => caches.delete(cacheName)))
|
|
||||||
)
|
|
||||||
])
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
// Handle fetch events
|
|
||||||
self.addEventListener("fetch", s => {
|
|
||||||
const t = (s => {
|
|
||||||
const { url: t } = s.request;
|
|
||||||
|
|
||||||
// Only handle webtorrent requests
|
|
||||||
if (!t.includes(self.registration.scope + "webtorrent/")) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle keepalive requests
|
|
||||||
if (t.includes(self.registration.scope + "webtorrent/keepalive/")) {
|
|
||||||
return new Response();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle cancel requests
|
|
||||||
if (t.includes(self.registration.scope + "webtorrent/cancel/")) {
|
|
||||||
return new Response(new ReadableStream({
|
|
||||||
cancel() {
|
|
||||||
cancelled = true;
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle streaming requests
|
|
||||||
return async function({ request: s }) {
|
|
||||||
const { url: t, method: n, headers: o, destination: a } = s;
|
|
||||||
|
|
||||||
// Get all window clients
|
|
||||||
const l = await clients.matchAll({
|
|
||||||
type: "window",
|
|
||||||
includeUncontrolled: true
|
|
||||||
});
|
|
||||||
|
|
||||||
// Create message channel and wait for response
|
|
||||||
const [r, i] = await new Promise(e => {
|
|
||||||
for (const s of l) {
|
|
||||||
const l = new MessageChannel,
|
|
||||||
{ port1: r, port2: i } = l;
|
|
||||||
r.onmessage = ({ data: s }) => {
|
|
||||||
e([s, r])
|
|
||||||
};
|
|
||||||
s.postMessage({
|
|
||||||
url: t,
|
|
||||||
method: n,
|
|
||||||
headers: Object.fromEntries(o.entries()),
|
|
||||||
scope: self.registration.scope,
|
|
||||||
destination: a,
|
|
||||||
type: "webtorrent"
|
|
||||||
}, [i]);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
let c = null;
|
|
||||||
|
|
||||||
const d = () => {
|
|
||||||
i.postMessage(false);
|
|
||||||
clearTimeout(c);
|
|
||||||
i.onmessage = null;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Handle non-streaming response
|
|
||||||
if (r.body !== "STREAM") {
|
|
||||||
d();
|
|
||||||
return new Response(r.body, r);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle streaming response
|
|
||||||
return new Response(new ReadableStream({
|
|
||||||
pull: s => new Promise(t => {
|
|
||||||
i.onmessage = ({ data: e }) => {
|
|
||||||
if (e) {
|
|
||||||
s.enqueue(e);
|
|
||||||
} else {
|
|
||||||
d();
|
|
||||||
s.close();
|
|
||||||
}
|
|
||||||
t();
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!cancelled && a !== "document") {
|
|
||||||
clearTimeout(c);
|
|
||||||
c = setTimeout(() => {
|
|
||||||
d();
|
|
||||||
t();
|
|
||||||
}, 5000);
|
|
||||||
}
|
|
||||||
|
|
||||||
i.postMessage(true);
|
|
||||||
}),
|
|
||||||
cancel() {
|
|
||||||
d();
|
|
||||||
}
|
|
||||||
}), r);
|
|
||||||
}(s);
|
|
||||||
})(s);
|
|
||||||
|
|
||||||
if (t) {
|
|
||||||
s.respondWith(t);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
})();
|
|