update
193
refactoring/about.html
Normal file
@@ -0,0 +1,193 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>bitvid | About</title>
|
||||
|
||||
<!-- Open Graph Meta Tags -->
|
||||
<meta property="og:title" content="BitVid - Markdown Viewer" />
|
||||
<meta
|
||||
property="og:description"
|
||||
content="View and render markdown content dynamically."
|
||||
/>
|
||||
<meta
|
||||
property="og:image"
|
||||
content="https://bitvid.netlify.app/assets/jpg/bitvid.jpg"
|
||||
/>
|
||||
<meta property="og:url" content="https://bitvid.btc.us" />
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:locale" content="en_US" />
|
||||
|
||||
<!-- App Icons -->
|
||||
<link rel="icon" href="/favicon.ico" sizes="any" />
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" />
|
||||
<link rel="manifest" href="/site.webmanifest" />
|
||||
<meta name="theme-color" content="#0f172a" />
|
||||
|
||||
<!-- Tailwind CSS -->
|
||||
<link
|
||||
href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
|
||||
<!-- Main Layout Styles -->
|
||||
<link href="css/style.css" rel="stylesheet" />
|
||||
|
||||
<!-- Markdown-Specific Styles -->
|
||||
<link href="css/markdown.css" rel="stylesheet" />
|
||||
</head>
|
||||
<body class="bg-gray-100">
|
||||
<div
|
||||
id="app"
|
||||
class="container mx-auto px-4 py-8 min-h-screen flex flex-col"
|
||||
>
|
||||
<!-- Header -->
|
||||
<header class="mb-8">
|
||||
<div class="flex items-start">
|
||||
<!-- Logo links back to index.html (or "/") -->
|
||||
<a href="index.html">
|
||||
<img
|
||||
src="assets/svg/bitvid-logo-light-mode.svg"
|
||||
alt="BitVid Logo"
|
||||
class="h-16"
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Markdown Content Section -->
|
||||
<main>
|
||||
<!--
|
||||
We give this section a white background and a shadow
|
||||
just like you originally had for other cards.
|
||||
-->
|
||||
<div id="markdown-container" class="bg-white p-6 rounded-lg shadow-md">
|
||||
<h2 class="text-2xl font-bold mb-4">Loading Content...</h2>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- Footer -->
|
||||
<!-- Footer -->
|
||||
<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>
|
BIN
refactoring/android-chrome-192x192.png
Normal file
After Width: | Height: | Size: 9.1 KiB |
BIN
refactoring/android-chrome-512x512.png
Normal file
After Width: | Height: | Size: 37 KiB |
BIN
refactoring/apple-touch-icon.png
Normal file
After Width: | Height: | Size: 8.1 KiB |
BIN
refactoring/assets/jpg/bitvid.jpg
Normal file
After Width: | Height: | Size: 93 KiB |
33
refactoring/assets/svg/bitvid-logo-dark-mode.svg
Normal file
@@ -0,0 +1,33 @@
|
||||
<?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>
|
After Width: | Height: | Size: 1.9 KiB |
33
refactoring/assets/svg/bitvid-logo-light-mode.svg
Normal file
@@ -0,0 +1,33 @@
|
||||
<?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>
|
After Width: | Height: | Size: 1.9 KiB |
1
refactoring/assets/svg/video-settings-gear.svg
Normal file
@@ -0,0 +1 @@
|
||||
<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>
|
After Width: | Height: | Size: 811 B |
99
refactoring/components/video-modal.html
Normal file
@@ -0,0 +1,99 @@
|
||||
<!-- 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>
|
24
refactoring/content/about.md
Normal file
@@ -0,0 +1,24 @@
|
||||

|
||||
|
||||
# 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!
|
47
refactoring/content/getting-started.md
Normal file
@@ -0,0 +1,47 @@
|
||||
# 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!
|
19
refactoring/content/ipns.md
Normal file
@@ -0,0 +1,19 @@
|
||||
# IPNS Gateways
|
||||
|
||||
Below is a list of available IPNS gateways you can use with the hash `k51qzi5uqu5dgwr4oejq9rk41aoe9zcupenby6iqecsk5byc7rx48uecd133a1`:
|
||||
|
||||
1. **[FLK IPFS Gateway](https://k51qzi5uqu5dgwr4oejq9rk41aoe9zcupenby6iqecsk5byc7rx48uecd133a1.ipns.flk-ipfs.xyz/)**
|
||||
A public gateway that resolves the provided IPNS hash.
|
||||
|
||||
2. **[Aragon IPFS Gateway](https://ipfs.eth.aragon.network/ipfs/bafybeih2ebj55ki3wvasj5i3rhwgjn6e72f6vxsrlrjfqvzezot2eoeqz4/)**
|
||||
A gateway hosted by Aragon for IPFS content resolution.
|
||||
|
||||
3. **[Dweb.link Gateway](https://dweb.link/ipns/k51qzi5uqu5dgwr4oejq9rk41aoe9zcupenby6iqecsk5byc7rx48uecd133a1)**
|
||||
A subdomain resolution gateway provided by Protocol Labs.
|
||||
|
||||
4. **[IPFS.io Gateway](https://ipfs.io/ipns/k51qzi5uqu5dgwr4oejq9rk41aoe9zcupenby6iqecsk5byc7rx48uecd133a1)**
|
||||
A public gateway operated by Protocol Labs.
|
||||
|
||||
---
|
||||
|
||||
**Note:** The availability and performance of these gateways may vary.
|
47
refactoring/content/roadmap.md
Normal file
@@ -0,0 +1,47 @@
|
||||
# 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).
|
111
refactoring/css/markdown.css
Normal file
@@ -0,0 +1,111 @@
|
||||
/* 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;
|
||||
}
|
423
refactoring/css/style.css
Normal file
@@ -0,0 +1,423 @@
|
||||
/* 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);
|
||||
}
|
BIN
refactoring/favicon-16x16.png
Normal file
After Width: | Height: | Size: 586 B |
BIN
refactoring/favicon-32x32.png
Normal file
After Width: | Height: | Size: 1.1 KiB |
BIN
refactoring/favicon.ico
Normal file
After Width: | Height: | Size: 15 KiB |
193
refactoring/getting-started.html
Normal file
@@ -0,0 +1,193 @@
|
||||
<!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="/favicon.ico" sizes="any" />
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" />
|
||||
<link rel="manifest" href="/site.webmanifest" />
|
||||
<meta name="theme-color" content="#0f172a" />
|
||||
|
||||
<!-- Tailwind CSS -->
|
||||
<link
|
||||
href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
|
||||
<!-- Main Layout Styles -->
|
||||
<link href="css/style.css" rel="stylesheet" />
|
||||
|
||||
<!-- Markdown-Specific Styles -->
|
||||
<link href="css/markdown.css" rel="stylesheet" />
|
||||
</head>
|
||||
<body class="bg-gray-100">
|
||||
<div
|
||||
id="app"
|
||||
class="container mx-auto px-4 py-8 min-h-screen flex flex-col"
|
||||
>
|
||||
<!-- Header -->
|
||||
<header class="mb-8">
|
||||
<div class="flex items-start">
|
||||
<!-- Logo links back to index.html (or "/") -->
|
||||
<a href="index.html">
|
||||
<img
|
||||
src="assets/svg/bitvid-logo-light-mode.svg"
|
||||
alt="BitVid Logo"
|
||||
class="h-16"
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Markdown Content Section -->
|
||||
<main>
|
||||
<!--
|
||||
We give this section a white background and a shadow
|
||||
just like you originally had for other cards.
|
||||
-->
|
||||
<div id="markdown-container" class="bg-white p-6 rounded-lg shadow-md">
|
||||
<h2 class="text-2xl font-bold mb-4">Loading Content...</h2>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- Footer -->
|
||||
<!-- Footer -->
|
||||
<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>
|
448
refactoring/index.html
Normal file
@@ -0,0 +1,448 @@
|
||||
<!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="/favicon.ico" sizes="any" />
|
||||
<link
|
||||
rel="apple-touch-icon"
|
||||
sizes="180x180"
|
||||
href="/apple-touch-icon.png"
|
||||
/>
|
||||
<link
|
||||
rel="icon"
|
||||
type="image/png"
|
||||
sizes="32x32"
|
||||
href="/favicon-32x32.png"
|
||||
/>
|
||||
<link
|
||||
rel="icon"
|
||||
type="image/png"
|
||||
sizes="16x16"
|
||||
href="/favicon-16x16.png"
|
||||
/>
|
||||
<link rel="manifest" href="/site.webmanifest" />
|
||||
<meta name="theme-color" content="#0f172a" />
|
||||
|
||||
<!-- 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>
|
192
refactoring/ipns.html
Normal file
@@ -0,0 +1,192 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>bitvid | About</title>
|
||||
|
||||
<!-- Open Graph Meta Tags -->
|
||||
<meta property="og:title" content="BitVid - Markdown Viewer" />
|
||||
<meta
|
||||
property="og:description"
|
||||
content="View and render markdown content dynamically."
|
||||
/>
|
||||
<meta
|
||||
property="og:image"
|
||||
content="https://bitvid.netlify.app/assets/jpg/bitvid.jpg"
|
||||
/>
|
||||
<meta property="og:url" content="https://bitvid.btc.us" />
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:locale" content="en_US" />
|
||||
|
||||
<!-- App Icons -->
|
||||
<link rel="icon" href="/favicon.ico" sizes="any" />
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" />
|
||||
<link rel="manifest" href="/site.webmanifest" />
|
||||
<meta name="theme-color" content="#0f172a" />
|
||||
|
||||
<!-- Tailwind CSS -->
|
||||
<link
|
||||
href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
|
||||
<!-- Main Layout Styles -->
|
||||
<link href="css/style.css" rel="stylesheet" />
|
||||
|
||||
<!-- Markdown-Specific Styles -->
|
||||
<link href="css/markdown.css" rel="stylesheet" />
|
||||
</head>
|
||||
<body class="bg-gray-100">
|
||||
<div
|
||||
id="app"
|
||||
class="container mx-auto px-4 py-8 min-h-screen flex flex-col"
|
||||
>
|
||||
<!-- Header -->
|
||||
<header class="mb-8">
|
||||
<div class="flex items-start">
|
||||
<!-- Logo links back to index.html (or "/") -->
|
||||
<a href="index.html">
|
||||
<img
|
||||
src="assets/svg/bitvid-logo-light-mode.svg"
|
||||
alt="BitVid Logo"
|
||||
class="h-16"
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Markdown Content Section -->
|
||||
<main>
|
||||
<!--
|
||||
We give this section a white background and a shadow
|
||||
just like you originally had for other cards.
|
||||
-->
|
||||
<div id="markdown-container" class="bg-white p-6 rounded-lg shadow-md">
|
||||
<h2 class="text-2xl font-bold mb-4">Loading Content...</h2>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- Footer -->
|
||||
<footer class="mt-auto pb-8 text-center px-4">
|
||||
<a
|
||||
href="https://bitvid.btc.us"
|
||||
class="text-gray-500 hover:text-gray-400 transition-colors duration-200"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
bitvid.btc.us
|
||||
</a>
|
||||
|
|
||||
<a
|
||||
href="https://bitvid.eth.limo"
|
||||
class="text-gray-500 hover:text-gray-400 transition-colors duration-200"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
bitvid.eth.limo
|
||||
</a>
|
||||
|
|
||||
<a
|
||||
href="https://bitvid.netlify.app"
|
||||
class="text-gray-500 hover:text-gray-400 transition-colors duration-200"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
bitvid.netlify.app
|
||||
</a>
|
||||
<div class="mt-2 space-x-4">
|
||||
<a
|
||||
href="https://github.com/PR0M3TH3AN/bitvid"
|
||||
class="text-gray-500 hover:text-gray-400 transition-colors duration-200"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
GitHub
|
||||
</a>
|
||||
<a
|
||||
href="https://primal.net/p/npub13yarr7j6vjqjjkahd63dmr27curypehx45ucue286ac7sft27y0srnpmpe"
|
||||
class="text-gray-500 hover:text-gray-400 transition-colors duration-200"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Nostr
|
||||
</a>
|
||||
<a
|
||||
href="https://habla.news/p/nprofile1qyv8wumn8ghj7un9d3shjtnndehhyapwwdhkx6tpdsq3gamnwvaz7tmjv4kxz7fwv3sk6atn9e5k7qgdwaehxw309ahx7uewd3hkcqgswaehxw309ahx7um5wgh8w6twv5q3yamnwvaz7tm0venxx6rpd9hzuur4vgqzpzf6x8a95eyp99dmwm4zmkx4a3cxgrnwdtfe3ej504m3aqjk4ugldyww3a"
|
||||
class="text-gray-500 hover:text-gray-400 transition-colors duration-200"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Blog
|
||||
</a>
|
||||
<a
|
||||
href="getting-started.html"
|
||||
class="text-gray-500 hover:text-gray-400 transition-colors duration-200"
|
||||
target="_self"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Getting Started
|
||||
</a>
|
||||
<a
|
||||
href="about.html"
|
||||
class="text-gray-500 hover:text-gray-400 transition-colors duration-200"
|
||||
target="_self"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
About
|
||||
</a>
|
||||
<a
|
||||
href="roadmap.html"
|
||||
class="text-gray-500 hover:text-gray-400 transition-colors duration-200"
|
||||
target="_self"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Roadmap
|
||||
</a>
|
||||
</div>
|
||||
<p
|
||||
class="mt-2 text-xs text-gray-600 font-mono break-all max-w-full overflow-hidden"
|
||||
>
|
||||
IPNS:
|
||||
<a href="ipns.html" class="text-blue-600 underline">
|
||||
k51qzi5uqu5dgwr4oejq9rk41aoe9zcupenby6iqecsk5byc7rx48uecd133a1
|
||||
</a>
|
||||
</p>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<!-- Marked.js (for converting markdown to HTML) -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
||||
<!-- Highlight.js (optional for code block highlighting) -->
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/highlight.min.js"></script>
|
||||
|
||||
<script>
|
||||
async function loadMarkdown() {
|
||||
const response = await fetch("content/ipns.md");
|
||||
if (response.ok) {
|
||||
const markdown = await response.text();
|
||||
const container = document.getElementById("markdown-container");
|
||||
|
||||
// Convert markdown to HTML
|
||||
const html = marked.parse(markdown);
|
||||
|
||||
// Insert the HTML into the container
|
||||
container.innerHTML = html;
|
||||
|
||||
// (Optional) Highlight code blocks
|
||||
document.querySelectorAll("pre code").forEach((block) => {
|
||||
hljs.highlightBlock(block);
|
||||
});
|
||||
} else {
|
||||
document.getElementById("markdown-container").innerHTML =
|
||||
'<p class="text-red-500">Error loading content. Please try again later.</p>';
|
||||
}
|
||||
}
|
||||
|
||||
loadMarkdown();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
154
refactoring/js/accessControl.js
Normal file
@@ -0,0 +1,154 @@
|
||||
// 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();
|
1051
refactoring/js/app.js
Normal file
0
refactoring/js/components/Navigation.js
Normal file
0
refactoring/js/components/UserManager.js
Normal file
0
refactoring/js/components/VideoForm.js
Normal file
0
refactoring/js/components/VideoList.js
Normal file
187
refactoring/js/components/VideoPlayer.js
Normal file
@@ -0,0 +1,187 @@
|
||||
// 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();
|
4
refactoring/js/config.js
Normal file
@@ -0,0 +1,4 @@
|
||||
// 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
|
0
refactoring/js/config/config.js
Normal file
29
refactoring/js/disclaimer.js
Normal file
@@ -0,0 +1,29 @@
|
||||
class DisclaimerModal {
|
||||
constructor() {
|
||||
this.modal = document.getElementById("disclaimerModal");
|
||||
this.acceptButton = document.getElementById("acceptDisclaimer");
|
||||
this.hasSeenDisclaimer = localStorage.getItem("hasSeenDisclaimer");
|
||||
|
||||
this.setupEventListeners();
|
||||
}
|
||||
|
||||
setupEventListeners() {
|
||||
const closeModal = () => {
|
||||
this.modal.style.display = "none";
|
||||
document.body.style.overflow = "unset";
|
||||
localStorage.setItem("hasSeenDisclaimer", "true");
|
||||
};
|
||||
|
||||
// Only keep the accept button event listener
|
||||
this.acceptButton.addEventListener("click", closeModal);
|
||||
}
|
||||
|
||||
show() {
|
||||
if (!this.hasSeenDisclaimer) {
|
||||
this.modal.style.display = "flex";
|
||||
document.body.style.overflow = "hidden";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const disclaimerModal = new DisclaimerModal();
|
8676
refactoring/js/libs/nostr.bundle.js
Normal file
13
refactoring/js/lists.js
Normal file
@@ -0,0 +1,13 @@
|
||||
// js/lists.js
|
||||
|
||||
const npubs = [
|
||||
"npub13yarr7j6vjqjjkahd63dmr27curypehx45ucue286ac7sft27y0srnpmpe",
|
||||
"npub15jnttpymeytm80hatjqcvhhqhzrhx6gxp8pq0wn93rhnu8s9h9dsha32lx",
|
||||
"npub1j37gc05qpqzyrmdc5vetsc9h5qtstas7tr25j0n9sdpqxghz6m4q2ej6n8",
|
||||
"npub1epvnvv3kskvpnmpqgnm2atevsmdferhp7dg2s0yc7uc0hdmqmgssx09tu2",
|
||||
];
|
||||
|
||||
console.log("DEBUG: lists.js loaded, npubs:", npubs);
|
||||
|
||||
export const initialWhitelist = npubs;
|
||||
export const initialBlacklist = [""];
|
0
refactoring/js/models/VideoModel.js
Normal file
624
refactoring/js/nostr.js
Normal file
@@ -0,0 +1,624 @@
|
||||
// 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 [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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();
|
154
refactoring/js/old/accessControl.js
Normal file
@@ -0,0 +1,154 @@
|
||||
// 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();
|
1062
refactoring/js/old/app.js
Normal file
4
refactoring/js/old/config.js
Normal file
@@ -0,0 +1,4 @@
|
||||
// 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
|
29
refactoring/js/old/disclaimer.js
Normal file
@@ -0,0 +1,29 @@
|
||||
class DisclaimerModal {
|
||||
constructor() {
|
||||
this.modal = document.getElementById("disclaimerModal");
|
||||
this.acceptButton = document.getElementById("acceptDisclaimer");
|
||||
this.hasSeenDisclaimer = localStorage.getItem("hasSeenDisclaimer");
|
||||
|
||||
this.setupEventListeners();
|
||||
}
|
||||
|
||||
setupEventListeners() {
|
||||
const closeModal = () => {
|
||||
this.modal.style.display = "none";
|
||||
document.body.style.overflow = "unset";
|
||||
localStorage.setItem("hasSeenDisclaimer", "true");
|
||||
};
|
||||
|
||||
// Only keep the accept button event listener
|
||||
this.acceptButton.addEventListener("click", closeModal);
|
||||
}
|
||||
|
||||
show() {
|
||||
if (!this.hasSeenDisclaimer) {
|
||||
this.modal.style.display = "flex";
|
||||
document.body.style.overflow = "hidden";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const disclaimerModal = new DisclaimerModal();
|
13
refactoring/js/old/lists.js
Normal file
@@ -0,0 +1,13 @@
|
||||
// js/lists.js
|
||||
|
||||
const npubs = [
|
||||
"npub13yarr7j6vjqjjkahd63dmr27curypehx45ucue286ac7sft27y0srnpmpe",
|
||||
"npub15jnttpymeytm80hatjqcvhhqhzrhx6gxp8pq0wn93rhnu8s9h9dsha32lx",
|
||||
"npub1j37gc05qpqzyrmdc5vetsc9h5qtstas7tr25j0n9sdpqxghz6m4q2ej6n8",
|
||||
"npub1epvnvv3kskvpnmpqgnm2atevsmdferhp7dg2s0yc7uc0hdmqmgssx09tu2",
|
||||
];
|
||||
|
||||
console.log("DEBUG: lists.js loaded, npubs:", npubs);
|
||||
|
||||
export const initialWhitelist = npubs;
|
||||
export const initialBlacklist = [""];
|
624
refactoring/js/old/nostr.js
Normal file
@@ -0,0 +1,624 @@
|
||||
// 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 [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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();
|
286
refactoring/js/old/webtorrent.js
Normal file
@@ -0,0 +1,286 @@
|
||||
// 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()
|
0
refactoring/js/services/nostr/client.js
Normal file
0
refactoring/js/services/nostr/events.js
Normal file
0
refactoring/js/services/nostr/profiles.js
Normal file
0
refactoring/js/services/nostr/relays.js
Normal file
0
refactoring/js/services/torrent/client.js
Normal file
0
refactoring/js/services/torrent/stats.js
Normal file
0
refactoring/js/services/torrent/stream.js
Normal file
0
refactoring/js/utils/Notifications.js
Normal file
0
refactoring/js/utils/htmlUtils.js
Normal file
0
refactoring/js/utils/logger.js
Normal file
0
refactoring/js/utils/timeUtils.js
Normal file
286
refactoring/js/webtorrent.js
Normal file
@@ -0,0 +1,286 @@
|
||||
// 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()
|
171
refactoring/roadmap.html
Normal file
@@ -0,0 +1,171 @@
|
||||
<!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="/favicon.ico" sizes="any" />
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" />
|
||||
<link rel="manifest" href="/site.webmanifest" />
|
||||
<meta name="theme-color" content="#0f172a" />
|
||||
|
||||
<!-- Tailwind CSS -->
|
||||
<link
|
||||
href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
|
||||
<!-- Main Layout Styles -->
|
||||
<link href="css/style.css" rel="stylesheet" />
|
||||
|
||||
<!-- Markdown-Specific Styles -->
|
||||
<link href="css/markdown.css" rel="stylesheet" />
|
||||
</head>
|
||||
<body class="bg-gray-100">
|
||||
<div
|
||||
id="app"
|
||||
class="container mx-auto px-4 py-8 min-h-screen flex flex-col"
|
||||
>
|
||||
<!-- Header -->
|
||||
<header class="mb-8">
|
||||
<div class="flex items-start">
|
||||
<!-- Logo links back to index.html (or "/") -->
|
||||
<a href="index.html">
|
||||
<img
|
||||
src="assets/svg/bitvid-logo-light-mode.svg"
|
||||
alt="BitVid Logo"
|
||||
class="h-16"
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Markdown Content Section -->
|
||||
<main>
|
||||
<!--
|
||||
We give this section a white background and a shadow
|
||||
just like you originally had for other cards.
|
||||
-->
|
||||
<div id="markdown-container" class="bg-white p-6 rounded-lg shadow-md">
|
||||
<h2 class="text-2xl font-bold mb-4">Loading Content...</h2>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- Footer -->
|
||||
<footer class="mt-auto pb-8 text-center px-4">
|
||||
<a
|
||||
href="https://bitvid.btc.us"
|
||||
class="text-gray-500 hover:text-gray-400 transition-colors duration-200"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
bitvid.btc.us
|
||||
</a>
|
||||
<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>
|
41
refactoring/site.webmanifest
Normal file
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"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": "/favicon-32x32.png",
|
||||
"sizes": "32x32",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "/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
Normal file
@@ -0,0 +1,132 @@
|
||||
(() => {
|
||||
"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);
|
||||
}
|
||||
});
|
||||
})();
|