This commit is contained in:
Keep Creating Online
2025-01-12 09:15:25 -05:00
parent 0d0b1d7e26
commit dcae8bb82f
57 changed files with 15403 additions and 0 deletions

193
refactoring/about.html Normal file
View 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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

View 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

View 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

View 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

View 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>

View File

@@ -0,0 +1,24 @@
![](https://bitvid.netlify.app/assets/jpg/bitvid.jpg)
# 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!

View 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!

View File

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

View File

@@ -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).

View 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
View 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);
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 586 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

BIN
refactoring/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View 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
View 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
View File

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

View File

@@ -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

File diff suppressed because it is too large Load Diff

View File

View File

View File

View File

View 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
View 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

View File

View File

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

File diff suppressed because it is too large Load Diff

13
refactoring/js/lists.js Normal file
View 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 = [""];

View File

624
refactoring/js/nostr.js Normal file
View 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();

View 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

File diff suppressed because it is too large Load Diff

View 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

View File

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

View 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
View 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();

View 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()

View File

View File

View File

View File

View File

View File

View File

View File

View 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
View 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>

View 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
View 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);
}
});
})();