This commit is contained in:
2025-01-19 19:42:39 -05:00
parent 1d7f616963
commit 035ce99bc2
36 changed files with 0 additions and 13328 deletions

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 586 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -1,33 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 318.32 108.98">
<defs>
<style>
.cls-1 {
fill: #fe0032;
}
.cls-2 {
fill: #fff;
}
</style>
</defs>
<g>
<rect class="cls-1" x="7.1" y="7.8" width="5.84" height="5.48"/>
<rect class="cls-1" x="7.1" y="18.78" width="24.02" height="5.48"/>
<rect class="cls-1" x="7.1" y="29.77" width="42.85" height="5.48"/>
<rect class="cls-1" x="7.1" y="40.76" width="61.68" height="5.48"/>
<rect class="cls-1" x="7.1" y="51.75" width="80.88" height="5.48"/>
<rect class="cls-1" x="7.1" y="62.74" width="61.68" height="5.48"/>
<rect class="cls-1" x="7.1" y="73.73" width="42.85" height="5.48"/>
<rect class="cls-1" x="7.1" y="84.72" width="24.02" height="5.48"/>
<rect class="cls-1" x="7.1" y="95.71" width="5.84" height="5.48"/>
</g>
<g>
<path class="cls-2" d="M121.28,25.08v44.54c0,5.67,2.79,8.54,8.22,8.54h11.57c5.51,0,8.3-2.87,8.3-8.54v-13.09c0-5.67-2.79-8.46-8.3-8.46h-16.6v-5.83h17.72c8.7,0,13.01,4.47,13.01,13.33v15.01c0,8.86-4.31,13.33-13.01,13.33h-13.73c-8.7,0-13.01-4.47-13.01-13.33V25.08h5.83Z"/>
<path class="cls-2" d="M168.53,25.08v6.26h-5.83v-6.26h5.83ZM168.53,42.64v41.27h-5.83v-41.27h5.83Z"/>
<path class="cls-2" d="M191.36,25.08v17.16h9.02v5.19h-9.02v36.48h-5.83v-36.48h-9.82v-5.19h9.82v-17.16h5.83Z"/>
<path class="cls-2" d="M212.88,42.24l16.84,35.68,16.6-35.68h6.07l-19.56,41.67h-6.31l-20.04-41.67h6.39Z"/>
<path class="cls-2" d="M264.05,25.08v6.26h-5.83v-6.26h5.83ZM264.05,42.24v41.67h-5.83v-41.67h5.83Z"/>
<path class="cls-2" d="M311.21,25.08v45.5c0,8.86-4.31,13.33-13.01,13.33h-13.73c-8.7,0-13.01-4.47-13.01-13.33v-15.01c0-8.86,4.31-13.33,13.01-13.33h17.72v5.83h-16.6c-5.51,0-8.3,2.79-8.3,8.46v13.09c0,5.67,2.79,8.54,8.3,8.54h11.49c5.51,0,8.3-2.87,8.3-8.54V25.08h5.83Z"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.9 KiB

View File

@@ -1,33 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 318.32 108.98">
<defs>
<style>
.cls-1 {
fill: #fe0032;
}
.cls-2 {
fill: #383838;
}
</style>
</defs>
<g>
<rect class="cls-1" x="7.1" y="7.8" width="5.84" height="5.48"/>
<rect class="cls-1" x="7.1" y="18.78" width="24.02" height="5.48"/>
<rect class="cls-1" x="7.1" y="29.77" width="42.85" height="5.48"/>
<rect class="cls-1" x="7.1" y="40.76" width="61.68" height="5.48"/>
<rect class="cls-1" x="7.1" y="51.75" width="80.88" height="5.48"/>
<rect class="cls-1" x="7.1" y="62.74" width="61.68" height="5.48"/>
<rect class="cls-1" x="7.1" y="73.73" width="42.85" height="5.48"/>
<rect class="cls-1" x="7.1" y="84.72" width="24.02" height="5.48"/>
<rect class="cls-1" x="7.1" y="95.71" width="5.84" height="5.48"/>
</g>
<g>
<path class="cls-2" d="M121.28,25.08v44.54c0,5.67,2.79,8.54,8.22,8.54h11.57c5.51,0,8.3-2.87,8.3-8.54v-13.09c0-5.67-2.79-8.46-8.3-8.46h-16.6v-5.83h17.72c8.7,0,13.01,4.47,13.01,13.33v15.01c0,8.86-4.31,13.33-13.01,13.33h-13.73c-8.7,0-13.01-4.47-13.01-13.33V25.08h5.83Z"/>
<path class="cls-2" d="M168.53,25.08v6.26h-5.83v-6.26h5.83ZM168.53,42.64v41.27h-5.83v-41.27h5.83Z"/>
<path class="cls-2" d="M191.36,25.08v17.16h9.02v5.19h-9.02v36.48h-5.83v-36.48h-9.82v-5.19h9.82v-17.16h5.83Z"/>
<path class="cls-2" d="M212.88,42.24l16.84,35.68,16.6-35.68h6.07l-19.56,41.67h-6.31l-20.04-41.67h6.39Z"/>
<path class="cls-2" d="M264.05,25.08v6.26h-5.83v-6.26h5.83ZM264.05,42.24v41.67h-5.83v-41.67h5.83Z"/>
<path class="cls-2" d="M311.21,25.08v45.5c0,8.86-4.31,13.33-13.01,13.33h-13.73c-8.7,0-13.01-4.47-13.01-13.33v-15.01c0-8.86,4.31-13.33,13.01-13.33h17.72v5.83h-16.6c-5.51,0-8.3,2.79-8.3,8.46v13.09c0,5.67,2.79,8.54,8.3,8.54h11.49c5.51,0,8.3-2.87,8.3-8.54V25.08h5.83Z"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.9 KiB

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M24 13.616v-3.232c-1.651-.587-2.694-.752-3.219-2.019v-.001c-.527-1.271.1-2.134.847-3.707l-2.285-2.285c-1.561.742-2.433 1.375-3.707.847h-.001c-1.269-.526-1.435-1.576-2.019-3.219h-3.232c-.582 1.635-.749 2.692-2.019 3.219h-.001c-1.271.528-2.132-.098-3.707-.847l-2.285 2.285c.745 1.568 1.375 2.434.847 3.707-.527 1.271-1.584 1.438-3.219 2.02v3.232c1.632.58 2.692.749 3.219 2.019.53 1.282-.114 2.166-.847 3.707l2.285 2.286c1.562-.743 2.434-1.375 3.707-.847h.001c1.27.526 1.436 1.579 2.019 3.219h3.232c.582-1.636.75-2.69 2.027-3.222h.001c1.262-.524 2.12.101 3.698.851l2.285-2.286c-.744-1.563-1.375-2.433-.848-3.706.527-1.271 1.588-1.44 3.221-2.021zm-12 2.384c-2.209 0-4-1.791-4-4s1.791-4 4-4 4 1.791 4 4-1.791 4-4 4z"/></svg>

Before

Width:  |  Height:  |  Size: 811 B

View File

@@ -1,99 +0,0 @@
<!-- components/video-modal.html -->
<div id="playerModal" class="fixed inset-0 bg-black/90 z-50 hidden">
<div
class="modal-container h-full w-full flex items-start justify-center overflow-y-auto"
>
<div
class="modal-content bg-gray-900 w-full max-w-[90%] lg:max-w-6xl my-0 rounded-lg overflow-hidden relative"
>
<!-- Navigation bar - sliding at top -->
<div
id="modalNav"
class="sticky top-0 z-60 bg-gradient-to-b from-black/80 to-transparent transition-transform duration-300"
>
<div class="flex items-center px-6 py-4">
<button
id="closeModal"
class="back-button flex items-center justify-center w-10 h-10 rounded-full bg-black/50 hover:bg-black/70 transition-all duration-200 backdrop-blur"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="w-6 h-6 text-gray-300"
>
<path d="M15 18l-6-6 6-6" />
</svg>
</button>
</div>
</div>
<div class="video-container w-full bg-black">
<video id="modalVideo" class="w-full aspect-video" controls></video>
</div>
<div class="video-info p-6">
<!-- Rest of the content stays the same -->
<!-- Video Title -->
<h2 id="videoTitle" class="text-2xl font-bold mb-2 text-white"></h2>
<!-- Video Timestamp -->
<div
class="flex items-center justify-between text-sm text-gray-400 mb-4"
>
<span id="videoTimestamp">just now</span>
<div id="modalStatus" class="text-gray-300">
Initializing... Just give it a sec.
</div>
</div>
<!-- Creator info -->
<div class="flex items-center mb-4 p-4 bg-gray-800/50 rounded-lg">
<div class="w-12 h-12 rounded-full bg-gray-700 overflow-hidden">
<img
id="creatorAvatar"
src=""
alt="Creator"
class="w-full h-full object-cover"
/>
</div>
<div class="ml-4">
<h3 id="creatorName" class="font-medium text-lg text-white">
Creator Name
</h3>
<p id="creatorNpub" class="text-sm text-gray-400">npub...</p>
</div>
</div>
<!-- Video Description -->
<div class="bg-gray-800/50 rounded-lg p-4 mb-4">
<p id="videoDescription" class="text-gray-300 whitespace-pre-wrap">
No description available.
</p>
</div>
<!-- Torrent stats -->
<div class="bg-gray-800/50 rounded-lg p-4">
<div class="w-full bg-gray-700 rounded-full h-2 mb-2">
<div
class="bg-blue-500 h-2 rounded-full"
id="modalProgress"
style="width: 0%"
></div>
</div>
<div class="flex justify-between text-sm text-gray-400">
<span id="modalPeers">Peers: 0</span>
<span id="modalSpeed">Speed: 0 KB/s</span>
<span id="modalDownloaded">Downloaded: 0 MB / 0 MB</span>
</div>
</div>
</div>
</div>
</div>
</div>

View File

@@ -1,24 +0,0 @@
![](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

@@ -1,47 +0,0 @@
# Getting Started with bitvid
Ready to jump in? Here's everything you need to know to start watching and sharing videos on bitvid.
## Watching Videos
1. Just visit [bitvid.network](https://bitvid.network) or one of our alternate sites like [bitvid.btc.us](https://bitvid.btc.us) and [bitvid.eth.limo](https://bitvid.eth.limo). We also have other instances via [IPNS](ipns.html) gateways you can try.
2. Browse the videos on the homepage
3. Click any video to start watching
That's it! No account needed to watch.
## Sharing Your Videos
### Step 1: Set Up Your Account
1. Install a [Nostr extension](https://nostrapps.com/#signers#all) (like Alby or Nos2x) in your browser
2. The extension creates your secure login key automatically
3. Click "Login" on bitvid to connect
### Step 2: Prepare Your Video
1. Download WebTorrent Desktop app from [webtorrent.io/desktop](https://webtorrent.io/desktop/)
2. Open your video file in WebTorrent Desktop
3. It will create a special "magnet link" for your video
4. Keep WebTorrent Desktop running to share your video
### Step 3: Share on bitvid
1. Click "Share a Video" on bitvid
2. Paste your video's magnet link
3. Add a title, description, and thumbnail
4. Click "Post" to share!
## Tips for Success
- Keep WebTorrent Desktop running while sharing videos
- Add eye-catching thumbnails to attract viewers
- Write clear descriptions to help people find your content
- Use the "Private" option if you only want to share with specific people
## Need Help?
- Visit our [GitHub](https://github.com/PR0M3TH3AN/bitvid) page for technical support
- Join our [community](https://primal.net/p/npub13yarr7j6vjqjjkahd63dmr27curypehx45ucue286ac7sft27y0srnpmpe) to connect with other users
- Report bugs to help us improve
Welcome to bitvid let's start sharing!

View File

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

View File

@@ -1,47 +0,0 @@
# Roadmap and Bug List
## UI Enhancements
- Add a copy Magnet button labeled "Seed".
- add community guidelines page
- add links to pop-up modal
- Convert "Logged in as" from public key to profile image and username (use npub as fallback).
- Add a sidebar for improved UI flexibility.
- Customize home screen content via algorithms for better feeds. (trending, new, for you etc.)
- Improve UI/UX and CSS.
- Add custom color themes and toggle between light and dark mode.
## Bug Fixes
- Fix public key wrapping issue on smaller screens.
- Fix video editing failures.
- Resolve issue where reopening the same video doesn't work after closing the video player.
- Address "Video playback error: MEDIA_ELEMENT_ERROR: Empty src attribute" error.
- Fix "Dev Mode" publishing "Live Mode" notes—add a flag for dev mode posts.
## Feature Additions
- Allow users to set custom relay settings, stored in local cache.
- Add a "Publish" step in the video editing process.
- Add comments to the video modal.
- Implement an "Adult Content" flag for note submissions.
- Enable custom hashtags in the submission spec and form.
- Allow multiple video resolutions with a selector in the video player.
- Add a block/unblock list with import/export functionality.
- Assign unique URLs to each video.
- Add a profile modal for each user/profile.
- Introduce a subscription mechanism with notifications.
- Add zaps to videos, profiles, and comments.
- Implement visibility filtering for videos:
- Show only videos whose magnet links have at least **one active peer online**.
- Integrate the filtering mechanism into the video list rendering process.
- Update the video list dynamically based on real-time peer availability.
- Add multi-language support for content and filtration.
- Create a settings menu for local account preferences, including relay, adult content, theme, and language.
## Long-Term Goals
- Add a system for creating high-quality, algorithm-driven content feeds.
- Thoroughly bug test the video editing and submission process.
If you find a new bug thats not listed here. DM me on [Nostr](https://primal.net/p/npub13yarr7j6vjqjjkahd63dmr27curypehx45ucue286ac7sft27y0srnpmpe).

View File

@@ -1,111 +0,0 @@
/* markdown.css */
/*
Give the markdown container a standard text color
that is easy to read on a white background
*/
#markdown-container {
color: #333;
line-height: 1.6;
/* Padding & border radius come from your inline Tailwind classes */
}
/* Headings */
#markdown-container h1 {
font-size: 1.875rem; /* ~ text-2xl in Tailwind */
font-weight: 700;
margin-bottom: 1rem;
margin-top: 0.5rem;
color: #111;
}
#markdown-container h2 {
font-size: 1.5rem; /* ~ text-xl */
font-weight: 700;
margin-bottom: 0.75rem;
margin-top: 1.25rem;
color: #222;
}
#markdown-container h3 {
font-size: 1.25rem; /* ~ text-lg */
font-weight: 600;
margin-bottom: 0.75rem;
margin-top: 1rem;
}
#markdown-container h4,
#markdown-container h5,
#markdown-container h6 {
font-weight: 600;
margin-top: 0.75rem;
margin-bottom: 0.5rem;
}
/* Paragraphs */
#markdown-container p {
margin-bottom: 1rem;
}
/* Lists */
#markdown-container ul,
#markdown-container ol {
margin-left: 1.25rem;
margin-bottom: 1rem;
}
#markdown-container li {
margin-bottom: 0.5rem;
}
/* Blockquotes */
#markdown-container blockquote {
border-left: 4px solid #ccc;
padding-left: 1rem;
margin: 1rem 0;
color: #555;
}
/* Inline code */
#markdown-container code {
background-color: #f6f8fa;
color: #d63384;
padding: 0.2rem 0.4rem;
border-radius: 0.25rem;
font-family: Menlo, Monaco, "Courier New", monospace;
font-size: 0.875rem;
}
/* Code blocks */
#markdown-container pre {
background-color: #f6f8fa;
color: #333;
padding: 1rem;
border-radius: 0.25rem;
overflow-x: auto;
margin-bottom: 1rem;
}
/* Links */
#markdown-container a {
color: #bf5af2; /* pick a link color or use var(--color-primary) if you like */
text-decoration: none;
}
#markdown-container a:hover {
text-decoration: underline;
}
/* Horizontal Rule */
#markdown-container hr {
border: 0;
border-top: 1px solid #e5e7eb;
margin: 2rem 0;
}
/* Images */
#markdown-container img {
max-width: 100%;
height: auto;
margin-bottom: 1rem;
}

View File

@@ -1,423 +0,0 @@
/* css/style.css */
:root {
--color-bg: #0f172a;
--color-card: #1e293b;
--color-primary: #8b5cf6;
--color-secondary: #f43f5e;
--color-text: #f8fafc;
--color-muted: #94a3b8;
--shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05);
--shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1);
--shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1);
}
/* Core Styles */
body {
font-family: system-ui, -apple-system, sans-serif;
background-color: var(--color-bg);
color: var(--color-text);
line-height: 1.5;
}
header {
margin-bottom: 2rem;
padding: 1rem 0;
display: flex;
justify-content: flex-start;
}
header img {
height: 6rem;
width: auto;
max-width: none;
}
.container {
max-width: 1480px;
margin: 0 auto;
padding: 1rem;
}
/* Video Grid */
#videoList {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
gap: 2rem;
padding: 1.5rem 0;
}
/* Video Cards */
.video-card {
background-color: var(--color-card);
border-radius: 0.75rem;
overflow: hidden;
transition: transform 0.2s cubic-bezier(0.4, 0, 0.2, 1);
box-shadow: var(--shadow-md);
}
.video-card:hover {
transform: translateY(-4px);
box-shadow: var(--shadow-lg);
}
.video-card .thumbnail-container {
position: relative;
padding-top: 56.25%;
background-color: #0f172a;
}
.video-card img.thumbnail {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
object-fit: cover;
}
.video-card .details {
padding: 1.25rem;
}
.video-card h3 {
font-size: 1.125rem;
font-weight: 600;
color: var(--color-text);
margin-bottom: 0.75rem;
}
/* Modal Player */
#playerModal {
position: fixed;
inset: 0;
background-color: rgb(0 0 0 / 0.9);
z-index: 50;
display: flex;
flex-direction: column;
overflow-y: auto;
overscroll-behavior: contain;
}
/* Modal Content Container */
.modal-content {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
background-color: #0f172a;
}
/* Video Container */
.video-container {
width: 100%;
background-color: black;
position: sticky;
top: 0;
z-index: 51;
}
/* Modal Video */
#modalVideo {
width: 100%;
aspect-ratio: 16/9;
background-color: black;
}
/* Video Info Section */
.video-info {
padding: 1rem;
flex: 1;
overflow-y: auto;
}
/* Responsive Adjustments */
@media (min-width: 768px) {
#playerModal {
padding: 2rem;
align-items: center;
justify-content: center;
}
.modal-content {
max-width: 64rem;
max-height: calc(100vh - 4rem); /* Account for padding */
border-radius: 0.5rem;
overflow: hidden;
}
.video-container {
position: relative;
}
}
/* Mobile-specific styles */
@media (max-width: 767px) {
#playerModal {
padding: 0;
}
.modal-content {
border-radius: 0;
}
}
/* Custom Scrollbar */
.video-info::-webkit-scrollbar {
width: 8px;
}
.video-info::-webkit-scrollbar-track {
background: rgba(255, 255, 255, 0.1);
}
.video-info::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.3);
border-radius: 4px;
}
.video-info::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.4);
}
/* Progress Bars */
.progress-bar {
width: 100%;
height: 0.5rem;
background-color: rgb(255 255 255 / 0.1);
border-radius: 9999px;
overflow: hidden;
}
.progress-bar-fill {
height: 100%;
background-color: var(--color-primary);
transition: width 0.3s ease-out;
}
/* Forms & Inputs */
input,
textarea {
width: 100%;
padding: 0.75rem;
background-color: var(--color-bg);
border: 1px solid rgb(255 255 255 / 0.1);
border-radius: 0.5rem;
color: var(--color-text);
transition: border-color 0.2s;
}
input:focus,
textarea:focus {
outline: none;
border-color: var(--color-primary);
ring: 2px var(--color-primary);
}
/* Buttons */
button {
padding: 0.75rem 1.5rem;
background-color: var(--color-primary);
color: white;
border-radius: 0.5rem;
font-weight: 500;
transition: all 0.2s;
}
button:hover {
background-color: var(--color-secondary);
transform: translateY(-1px);
}
button:focus {
outline: none;
ring: 2px var(--color-primary);
}
/* Utility Classes */
.line-clamp-2 {
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.hidden {
display: none;
}
/* Notifications */
#errorContainer,
#successContainer {
border-radius: 0.5rem;
padding: 1rem;
margin-bottom: 1rem;
display: flex;
align-items: center;
}
#errorContainer {
background-color: rgb(220 38 38 / 0.1);
color: #fecaca;
border: 1px solid rgb(220 38 38 / 0.2);
}
#successContainer {
background-color: rgb(34 197 94 / 0.1);
color: #bbf7d0;
border: 1px solid rgb(34 197 94 / 0.2);
}
/* Modal Display */
#playerSection,
#playerModal {
display: none;
}
#playerModal.flex {
display: flex;
}
/* Responsive Design */
@media (max-width: 640px) {
#videoList {
grid-template-columns: 1fr;
gap: 1rem;
}
.container {
padding: 0.75rem;
}
}
footer {
border-top: 1px solid rgba(255, 255, 255, 0.1);
margin-top: 4rem;
padding-top: 2rem;
}
footer a {
text-decoration: none;
}
footer a:hover {
text-decoration: underline;
}
/* Disclaimer Modal Styles */
#disclaimerModal {
position: fixed;
inset: 0;
background-color: rgb(0 0 0 / 0.9);
z-index: 50;
display: flex;
flex-direction: column;
overflow-y: auto;
overscroll-behavior: contain;
}
#disclaimerModal .modal-content {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
background-color: var(--color-bg);
}
#disclaimerModal .modal-scroll {
padding: 1.5rem;
flex: 1;
overflow-y: auto;
}
/* Disclaimer Modal Scrollbar */
#disclaimerModal .modal-scroll::-webkit-scrollbar {
width: 8px;
}
#disclaimerModal .modal-scroll::-webkit-scrollbar-track {
background: rgba(255, 255, 255, 0.1);
}
#disclaimerModal .modal-scroll::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.3);
border-radius: 4px;
}
#disclaimerModal .modal-scroll::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.4);
}
/* Disclaimer Modal Button Container */
#disclaimerModal .button-container {
padding: 1rem 1.5rem;
background-color: #1a2234;
border-top: 1px solid rgba(255, 255, 255, 0.1);
}
/* Responsive Adjustments for Disclaimer Modal */
@media (min-width: 768px) {
#disclaimerModal {
padding: 2rem;
align-items: center;
justify-content: center;
}
#disclaimerModal .modal-content {
width: 100%;
height: auto;
max-width: 42rem;
max-height: 90vh;
border-radius: 0.5rem;
overflow: hidden;
margin: auto;
}
#disclaimerModal .modal-scroll {
max-height: calc(
90vh - 5rem
); /* Account for button container and padding */
}
}
/* Mobile-specific styles for Disclaimer Modal */
@media (max-width: 767px) {
#disclaimerModal {
padding: 0;
}
#disclaimerModal .modal-content {
min-height: 100vh;
border-radius: 0;
}
}
/* Override default button styles for back button */
.back-button {
background: rgba(0, 0, 0, 0.5) !important;
padding: 0 !important;
color: rgb(209 213 219) !important;
transform: none !important;
}
.back-button:hover {
background: rgba(0, 0, 0, 0.7) !important;
color: white !important;
}
/* Modal Container */
.modal-container {
padding: 0;
}
@media (min-width: 768px) {
.modal-container {
padding: 2rem;
}
}
/* Video info cards */
.video-info .bg-gray-800\/50 {
background-color: rgb(31 41 55 / 0.5);
backdrop-filter: blur(4px);
}

View File

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

View File

@@ -1,448 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>bitvid | Decentralized Video Sharing</title>
<!-- Open Graph Meta Tags -->
<meta
property="og:title"
content="BitVid - Decentralized Video Sharing"
/>
<meta
property="og:description"
content="Share videos and follow creators freely, in a truly decentralized way."
/>
<meta
property="og:image"
content="https://bitvid.netlify.app/assets/jpg/bitvid.jpg"
/>
<meta property="og:url" content="https://bitvid.network" />
<meta property="og:type" content="website" />
<meta property="og:locale" content="en_US" />
<!-- App Icons -->
<link rel="icon" href="assets/favicon.ico" sizes="any" />
<link
rel="apple-touch-icon"
sizes="180x180"
href="/apple-touch-icon.png"
/>
<link
rel="icon"
type="image/png"
sizes="32x32"
href="assets/png/favicon-32x32.png"
/>
<link
rel="icon"
type="image/png"
sizes="16x16"
href="assets/png/favicon-16x16.png"
/>
<link rel="manifest" href="/site.webmanifest" />
<meta name="theme-color" content="#0f172a" />
<!-- Styles -->
<link
href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css"
rel="stylesheet"
/>
<link href="css/style.css" rel="stylesheet" />
</head>
<body>
<!-- Rest of your page content -->
</body>
</html>
<body class="bg-gray-100">
<div
id="app"
class="container mx-auto px-4 py-8 min-h-screen flex flex-col"
>
<!-- Header -->
<header class="mb-8">
<div class="flex items-start">
<img
src="assets/svg/bitvid-logo-light-mode.svg"
alt="BitVid Logo"
class="h-16"
/>
</div>
</header>
<!-- Login Section -->
<div id="loginSection" class="mb-8 flex items-center justify-between">
<div>
<button
id="loginButton"
class="bg-blue-500 text-white px-4 py-2 rounded-md hover:bg-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2"
>
Login with Nostr
</button>
<p id="userStatus" class="mt-4 text-gray-500 hidden">
Logged in as: <span id="userPubKey"></span>
</p>
</div>
<div>
<button
id="logoutButton"
class="bg-red-500 text-white px-4 py-2 rounded-md hover:bg-red-600 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2 hidden"
>
Logout
</button>
</div>
</div>
<!-- Error Container -->
<div
id="errorContainer"
class="hidden bg-red-100 text-red-800 p-4 rounded-md mb-4"
>
<!-- Error messages will appear here -->
</div>
<!-- Success Container -->
<div
id="successContainer"
class="hidden bg-green-100 text-green-800 p-4 rounded-md mb-4"
>
<!-- Success messages will appear here -->
</div>
<!-- Video Submission Form -->
<div
class="bg-white p-6 rounded-lg shadow-md mb-8 hidden"
id="videoFormContainer"
>
<h2 class="text-xl font-semibold mb-4">Share a Video</h2>
<form id="submitForm" class="space-y-4">
<div>
<label for="title" class="block text-sm font-medium text-gray-700"
>Title</label
>
<input
type="text"
id="title"
required
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500"
/>
</div>
<div>
<label for="magnet" class="block text-sm font-medium text-gray-700"
>Magnet Link</label
>
<input
type="text"
id="magnet"
required
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500"
/>
</div>
<div>
<label
for="thumbnail"
class="block text-sm font-medium text-gray-700"
>Thumbnail URL (optional)</label
>
<input
type="url"
id="thumbnail"
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500"
/>
</div>
<!-- Description Field -->
<div>
<label
for="description"
class="block text-sm font-medium text-gray-700"
>Description (optional)</label
>
<textarea
id="description"
rows="3"
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500"
></textarea>
</div>
<!-- ADDED FOR PRIVATE LISTINGS -->
<div class="flex items-center space-x-2">
<input
type="checkbox"
id="isPrivate"
class="form-checkbox h-5 w-5"
/>
<span class="text-sm font-medium text-gray-700"
>Private Listing (Encrypt Magnet)</span
>
</div>
<!-- END ADDED FOR PRIVATE LISTINGS -->
<button
type="submit"
class="bg-blue-500 text-white px-4 py-2 rounded-md hover:bg-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2"
>
Share Video
</button>
</form>
</div>
<!-- Video Player Section -->
<div id="playerSection" class="mb-8 hidden">
<video id="video" controls class="w-full rounded-lg shadow-md"></video>
<!-- Status and Stats -->
<div class="mt-4">
<div id="status" class="text-gray-700 mb-2">
Initializing... Just give it a sec.
</div>
<div class="w-full bg-gray-300 rounded-full h-2 mb-2">
<div
class="bg-blue-500 h-2 rounded-full"
id="progress"
style="width: 0%"
></div>
</div>
<div class="flex justify-between text-sm text-gray-600">
<span id="peers">Peers: 0</span>
<span id="speed">0 KB/s</span>
<span id="downloaded">0 MB / 0 MB</span>
</div>
</div>
</div>
<!-- Video List -->
<div class="mb-8">
<div
id="videoList"
class="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-8"
>
<!-- Videos will be dynamically inserted here -->
</div>
</div>
<!-- Imported Video Player Modal -->
<div id="modalContainer"></div>
<div class="text-center mb-8">
<h2 class="text-2xl font-bold text-gray-500 tracking-wide">
seed. zap. subscribe.
</h2>
</div>
<!-- Disclaimer Modal -->
<div id="disclaimerModal" class="hidden">
<div class="modal-content">
<div class="modal-scroll">
<!-- Logo -->
<div class="flex justify-center mb-8">
<img
src="assets/svg/bitvid-logo-dark-mode.svg"
alt="BitVid Logo"
class="h-16"
/>
</div>
<h2 class="text-2xl font-bold mb-4 text-center text-white">
Welcome to bitvid
</h2>
<!-- Warning Alert -->
<div
class="bg-yellow-900/20 border border-yellow-700/50 rounded-lg p-4 mb-6 flex items-start"
>
<svg
class="h-5 w-5 text-yellow-500 mt-0.5 mr-3 flex-shrink-0"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
/>
</svg>
<p class="text-yellow-200">
This platform is still in development. You may encounter bugs or
missing features.
</p>
</div>
<!-- Main Content -->
<div class="space-y-6 text-gray-300">
<p>
bitvid is a decentralized video platform where content is shared
directly between users. We want you to understand a few
important points before you continue:
</p>
<div class="space-y-4">
<div class="bg-gray-800 rounded-lg p-4">
<h3 class="text-white font-semibold mb-2">
Early Access Status
</h3>
<p class="text-gray-400">
Currently, video posting is invite-only as we carefully
scale our platform. While anyone can watch videos, content
creation is limited to approved creators. This helps us
maintain quality content during our early stages.
</p>
</div>
<div class="bg-gray-800 rounded-lg p-4">
<h3 class="text-white font-semibold mb-2">
Content Responsibility & Moderation
</h3>
<p class="text-gray-400">
While we don't host videos directly, we maintain community
standards through access control. Users who violate our
guidelines may be blocked from accessing the platform. All
content must follow local laws and platform guidelines.
</p>
</div>
<div class="bg-gray-800 rounded-lg p-4">
<h3 class="text-white font-semibold mb-2">Platform Status</h3>
<p class="text-gray-400">
bitvid is a work in progress. Features may change or break,
and security improvements are ongoing. Your feedback and
patience help us build a better platform.
</p>
</div>
<div class="bg-gray-800 rounded-lg p-4">
<h3 class="text-white font-semibold mb-2">Get Involved</h3>
<p class="text-gray-400">
Are you a developer? We'd love your help! Visit our GitHub
repository to contribute to building the future of
decentralized video sharing.
</p>
</div>
</div>
</div>
</div>
<!-- Action Button in fixed container -->
<div class="button-container">
<button
id="acceptDisclaimer"
class="w-full bg-blue-500 text-white px-4 py-2 rounded-md hover:bg-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 focus:ring-offset-gray-900 transition-colors duration-200"
>
I Understand
</button>
</div>
</div>
</div>
<!-- Footer -->
<footer class="mt-auto pb-8 text-center px-4">
<a
href="http://bitvid.network/"
class="text-gray-500 hover:text-gray-400 transition-colors duration-200"
target="_blank"
rel="noopener noreferrer"
>
bitvid.network
</a>
|
<a
href="https://bitvid.btc.us"
class="text-gray-500 hover:text-gray-400 transition-colors duration-200"
target="_blank"
rel="noopener noreferrer"
>
bitvid.btc.us
</a>
|
<a
href="https://bitvid.eth.limo"
class="text-gray-500 hover:text-gray-400 transition-colors duration-200"
target="_blank"
rel="noopener noreferrer"
>
bitvid.eth.limo
</a>
|
<div class="mt-2 space-x-4">
<a
href="https://github.com/PR0M3TH3AN/bitvid"
class="text-gray-500 hover:text-gray-400 transition-colors duration-200"
target="_blank"
rel="noopener noreferrer"
>
GitHub
</a>
<a
href="https://primal.net/p/npub13yarr7j6vjqjjkahd63dmr27curypehx45ucue286ac7sft27y0srnpmpe"
class="text-gray-500 hover:text-gray-400 transition-colors duration-200"
target="_blank"
rel="noopener noreferrer"
>
Nostr
</a>
<a
href="https://habla.news/p/nprofile1qyv8wumn8ghj7un9d3shjtnndehhyapwwdhkx6tpdsq3gamnwvaz7tmjv4kxz7fwv3sk6atn9e5k7qgdwaehxw309ahx7uewd3hkcqgswaehxw309ahx7um5wgh8w6twv5q3yamnwvaz7tm0venxx6rpd9hzuur4vgqzpzf6x8a95eyp99dmwm4zmkx4a3cxgrnwdtfe3ej504m3aqjk4ugldyww3a"
class="text-gray-500 hover:text-gray-400 transition-colors duration-200"
target="_blank"
rel="noopener noreferrer"
>
Blog
</a>
<a
href="getting-started.html"
class="text-gray-500 hover:text-gray-400 transition-colors duration-200"
target="_self"
rel="noopener noreferrer"
>
Getting Started
</a>
<a
href="about.html"
class="text-gray-500 hover:text-gray-400 transition-colors duration-200"
target="_self"
rel="noopener noreferrer"
>
About
</a>
<a
href="roadmap.html"
class="text-gray-500 hover:text-gray-400 transition-colors duration-200"
target="_self"
rel="noopener noreferrer"
>
Roadmap
</a>
</div>
<p
class="mt-2 text-xs text-gray-600 font-mono break-all max-w-full overflow-hidden"
>
IPNS:
<a href="ipns.html" class="text-blue-600 underline">
k51qzi5uqu5dgwr4oejq9rk41aoe9zcupenby6iqecsk5byc7rx48uecd133a1
</a>
</p>
</footer>
<!-- Scripts -->
<!-- Load WebTorrent via CDN -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/webtorrent/1.9.7/webtorrent.min.js"></script>
<!-- Load Nostr library -->
<script src="js/libs/nostr.bundle.js"></script>
<!-- Load JavaScript Modules -->
<script src="js/libs/nostr.bundle.js"></script>
<script type="module" src="js/config.js"></script>
<script type="module" src="js/lists.js"></script>
<script type="module" src="js/accessControl.js"></script>
<script type="module" src="js/webtorrent.js"></script>
<script type="module" src="js/nostr.js"></script>
<script type="module" src="js/app.js"></script>
</div>
</body>
</html>

View File

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

View File

@@ -1,154 +0,0 @@
// js/accessControl.js
import { isDevMode, isWhitelistEnabled } from "./config.js";
import { initialWhitelist, initialBlacklist } from "./lists.js";
class AccessControl {
constructor() {
// Debug logging for initialization
console.log("DEBUG: AccessControl constructor called");
console.log("DEBUG: initialWhitelist from import:", initialWhitelist);
console.log("DEBUG: typeof initialWhitelist:", typeof initialWhitelist);
console.log("DEBUG: initialWhitelist length:", initialWhitelist.length);
// Initialize empty sets
this.whitelist = new Set(initialWhitelist);
this.blacklist = new Set(initialBlacklist.filter((x) => x)); // Filter out empty strings
// Debug the sets
console.log("DEBUG: Whitelist after Set creation:", [...this.whitelist]);
console.log("DEBUG: Blacklist after Set creation:", [...this.blacklist]);
// Save to localStorage
this.saveWhitelist();
this.saveBlacklist();
}
// Rest of the class remains the same...
loadWhitelist() {
try {
const stored = localStorage.getItem("bitvid_whitelist");
return stored ? JSON.parse(stored) : [];
} catch (error) {
console.error("Error loading whitelist:", error);
return [];
}
}
loadBlacklist() {
try {
const stored = localStorage.getItem("bitvid_blacklist");
return stored ? JSON.parse(stored) : [];
} catch (error) {
console.error("Error loading blacklist:", error);
return [];
}
}
saveWhitelist() {
try {
localStorage.setItem(
"bitvid_whitelist",
JSON.stringify([...this.whitelist])
);
} catch (error) {
console.error("Error saving whitelist:", error);
}
}
saveBlacklist() {
try {
localStorage.setItem(
"bitvid_blacklist",
JSON.stringify([...this.blacklist])
);
} catch (error) {
console.error("Error saving blacklist:", error);
}
}
addToWhitelist(npub) {
if (!this.isValidNpub(npub)) {
throw new Error("Invalid npub format");
}
this.whitelist.add(npub);
this.saveWhitelist();
if (isDevMode) console.log(`Added ${npub} to whitelist`);
}
removeFromWhitelist(npub) {
this.whitelist.delete(npub);
this.saveWhitelist();
if (isDevMode) console.log(`Removed ${npub} from whitelist`);
}
addToBlacklist(npub) {
if (!this.isValidNpub(npub)) {
throw new Error("Invalid npub format");
}
this.blacklist.add(npub);
this.saveBlacklist();
if (isDevMode) console.log(`Added ${npub} to blacklist`);
}
removeFromBlacklist(npub) {
this.blacklist.delete(npub);
this.saveBlacklist();
if (isDevMode) console.log(`Removed ${npub} from blacklist`);
}
isWhitelisted(npub) {
const result = this.whitelist.has(npub);
if (isDevMode)
console.log(
`Checking if ${npub} is whitelisted:`,
result,
"Current whitelist:",
[...this.whitelist]
);
return result;
}
isBlacklisted(npub) {
return this.blacklist.has(npub);
}
canAccess(npub) {
if (this.isBlacklisted(npub)) {
return false;
}
const canAccess = !isWhitelistEnabled || this.isWhitelisted(npub);
if (isDevMode) console.log(`Checking access for ${npub}:`, canAccess);
return canAccess;
}
filterVideos(videos) {
return videos.filter((video) => {
try {
const npub = window.NostrTools.nip19.npubEncode(video.pubkey);
return !this.isBlacklisted(npub);
} catch (error) {
console.error("Error filtering video:", error);
return false;
}
});
}
isValidNpub(npub) {
try {
return npub.startsWith("npub1") && npub.length === 63;
} catch (error) {
return false;
}
}
getWhitelist() {
return [...this.whitelist];
}
getBlacklist() {
return [...this.blacklist];
}
}
export const accessControl = new AccessControl();

View File

@@ -1,724 +0,0 @@
// js/app.js
import { nostrClient } from "./nostr.js";
import { torrentClient } from "./webtorrent.js";
import { isDevMode } from "./config.js";
import { disclaimerModal } from "./disclaimer.js";
import { videoPlayer } from "./components/VideoPlayer.js";
import { videoList } from "./components/VideoList.js";
import { formatTimeAgo } from "./utils/timeUtils.js";
class bitvidApp {
constructor() {
// Authentication Elements
this.loginButton = document.getElementById("loginButton");
this.logoutButton = document.getElementById("logoutButton");
this.userStatus = document.getElementById("userStatus");
this.userPubKey = document.getElementById("userPubKey");
// Form Elements
this.submitForm = document.getElementById("submitForm");
this.videoFormContainer = document.getElementById("videoFormContainer");
// Video Player Elements
this.playerSection = document.getElementById("playerSection");
this.videoElement = document.getElementById("video");
this.status = document.getElementById("status");
this.progressBar = document.getElementById("progress");
this.peers = document.getElementById("peers");
this.speed = document.getElementById("speed");
this.downloaded = document.getElementById("downloaded");
// Initialize these as null - they'll be set after modal loads
this.playerModal = null;
this.modalVideo = null;
this.modalStatus = null;
this.modalProgress = null;
this.modalPeers = null;
this.modalSpeed = null;
this.modalDownloaded = null;
this.closePlayerBtn = null;
this.videoTitle = null;
this.videoDescription = null;
this.videoTimestamp = null;
this.creatorAvatar = null;
this.creatorName = null;
this.creatorNpub = null;
// Notification Containers
this.errorContainer = document.getElementById("errorContainer");
this.successContainer = document.getElementById("successContainer");
this.pubkey = null;
this.currentMagnetUri = null;
// Private Video Checkbox
this.isPrivateCheckbox = document.getElementById("isPrivate");
}
async init() {
try {
// Hide and reset player states
if (this.playerSection) {
this.playerSection.style.display = "none";
}
// Initialize Nostr client first
await nostrClient.init();
// Handle saved pubkey
const savedPubKey = localStorage.getItem("userPubKey");
if (savedPubKey) {
this.login(savedPubKey, false);
}
// Initialize modal
await videoPlayer.initModal();
// Initialize video list
await videoList.loadVideos();
// Initialize and show disclaimer modal
disclaimerModal.show();
// Set up event listeners after all initializations
this.setupEventListeners();
} catch (error) {
console.error("Init failed:", error);
this.showError("Failed to connect to Nostr relay");
}
}
async initModal() {
try {
console.log("Starting modal initialization...");
const response = await fetch("components/video-modal.html");
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const html = await response.text();
console.log("Modal HTML loaded successfully");
const modalContainer = document.getElementById("modalContainer");
if (!modalContainer) {
throw new Error("Modal container element not found!");
}
modalContainer.innerHTML = html;
console.log("Modal HTML inserted into DOM");
// Set up modal close handler
const closeButton = document.getElementById("closeModal");
if (!closeButton) {
throw new Error("Close button element not found!");
}
closeButton.addEventListener("click", () => {
this.hideModal();
});
// Set up scroll handler for nav show/hide
let lastScrollY = 0;
const modalNav = document.getElementById("modalNav");
const playerModal = document.getElementById("playerModal");
if (!modalNav || !playerModal) {
throw new Error("Modal navigation elements not found!");
}
playerModal.addEventListener("scroll", (e) => {
const currentScrollY = e.target.scrollTop;
const shouldShowNav =
currentScrollY <= lastScrollY || currentScrollY < 50;
modalNav.style.transform = shouldShowNav
? "translateY(0)"
: "translateY(-100%)";
lastScrollY = currentScrollY;
});
console.log("Modal initialization completed successfully");
return true;
} catch (error) {
console.error("Modal initialization failed:", error);
// You might want to show this error to the user
this.showError(`Failed to initialize video player: ${error.message}`);
return false;
}
}
updateModalElements() {
// Update Modal Elements
this.playerModal = document.getElementById("playerModal");
this.modalVideo = document.getElementById("modalVideo");
this.modalStatus = document.getElementById("modalStatus");
this.modalProgress = document.getElementById("modalProgress");
this.modalPeers = document.getElementById("modalPeers");
this.modalSpeed = document.getElementById("modalSpeed");
this.modalDownloaded = document.getElementById("modalDownloaded");
this.closePlayerBtn = document.getElementById("closeModal");
// Update Video Info Elements
this.videoTitle = document.getElementById("videoTitle");
this.videoDescription = document.getElementById("videoDescription");
this.videoTimestamp = document.getElementById("videoTimestamp");
// Update Creator Info Elements
this.creatorAvatar = document.getElementById("creatorAvatar");
this.creatorName = document.getElementById("creatorName");
this.creatorNpub = document.getElementById("creatorNpub");
// Add scroll behavior for nav
let lastScrollY = 0;
const modalNav = document.getElementById("modalNav");
if (this.playerModal && modalNav) {
this.playerModal.addEventListener("scroll", (e) => {
const currentScrollY = e.target.scrollTop;
const shouldShowNav =
currentScrollY <= lastScrollY || currentScrollY < 50;
modalNav.style.transform = shouldShowNav
? "translateY(0)"
: "translateY(-100%)";
lastScrollY = currentScrollY;
});
}
}
/**
* Sets up event listeners for various UI interactions.
*/
setupEventListeners() {
// Login Button
this.loginButton.addEventListener("click", async () => {
try {
const pubkey = await nostrClient.login();
this.login(pubkey, true);
} catch (error) {
this.log("Login failed:", error);
this.showError("Failed to login. Please try again.");
}
});
// Logout Button
this.logoutButton.addEventListener("click", () => {
this.logout();
});
// Form submission
this.submitForm.addEventListener("submit", (e) => this.handleSubmit(e));
// Close Modal Button
if (this.closePlayerBtn) {
this.closePlayerBtn.addEventListener("click", async () => {
await this.hideModal();
});
}
// Close Modal by clicking outside content
if (this.playerModal) {
this.playerModal.addEventListener("click", async (e) => {
if (e.target === this.playerModal) {
await this.hideModal();
}
});
}
// Video error handling
this.videoElement.addEventListener("error", (e) => {
const error = e.target.error;
this.log("Video error:", error);
if (error) {
this.showError(
`Video playback error: ${error.message || "Unknown error"}`
);
}
});
// Detailed Modal Video Event Listeners
if (this.modalVideo) {
this.modalVideo.addEventListener("error", (e) => {
const error = e.target.error;
this.log("Modal video error:", error);
if (error) {
this.log("Error code:", error.code);
this.log("Error message:", error.message);
this.showError(
`Video playback error: ${error.message || "Unknown error"}`
);
}
});
this.modalVideo.addEventListener("loadstart", () => {
this.log("Video loadstart event fired");
});
this.modalVideo.addEventListener("loadedmetadata", () => {
this.log("Video loadedmetadata event fired");
});
this.modalVideo.addEventListener("canplay", () => {
this.log("Video canplay event fired");
});
}
// Cleanup on page unload
window.addEventListener("beforeunload", async () => {
await this.cleanup();
});
}
/**
* Handles user login.
*/
login(pubkey, saveToStorage = true) {
this.pubkey = pubkey;
this.loginButton.classList.add("hidden");
this.logoutButton.classList.remove("hidden");
this.userStatus.classList.remove("hidden");
this.userPubKey.textContent = pubkey;
this.videoFormContainer.classList.remove("hidden");
this.log(`User logged in as: ${pubkey}`);
// ADD: Update videoList pubkey
videoList.setPubkey(pubkey);
if (saveToStorage) {
localStorage.setItem("userPubKey", pubkey);
}
}
/**
* Handles user logout.
*/
logout() {
nostrClient.logout();
this.pubkey = null;
this.loginButton.classList.remove("hidden");
this.logoutButton.classList.add("hidden");
this.userStatus.classList.add("hidden");
this.userPubKey.textContent = "";
this.videoFormContainer.classList.add("hidden");
localStorage.removeItem("userPubKey");
this.log("User logged out.");
}
/**
* Cleans up video player and torrents.
*/
async cleanup() {
try {
if (this.videoElement) {
this.videoElement.pause();
this.videoElement.src = "";
this.videoElement.load();
}
if (this.modalVideo) {
this.modalVideo.pause();
this.modalVideo.src = "";
this.modalVideo.load();
}
await torrentClient.cleanup();
} catch (error) {
this.log("Cleanup error:", error);
}
}
/**
* Hides the video player section.
*/
async hideVideoPlayer() {
await this.cleanup();
this.playerSection.classList.add("hidden");
}
/**
* Hides the video modal.
*/
async hideModal() {
await this.cleanup();
this.playerModal.style.display = "none";
this.playerModal.classList.add("hidden");
}
/**
* Handles video submission (with version, private listing).
*/
async handleSubmit(e) {
e.preventDefault();
if (!this.pubkey) {
this.showError("Please login to post a video.");
return;
}
const descriptionElement = document.getElementById("description");
// ADDED FOR VERSIONING/PRIVATE/DELETE:
// If you have a checkbox with id="isPrivate" in HTML
const isPrivate = this.isPrivateCheckbox
? this.isPrivateCheckbox.checked
: false;
const formData = {
version: 2, // We set the version to 2 for new posts
title: document.getElementById("title")?.value.trim() || "",
magnet: document.getElementById("magnet")?.value.trim() || "",
thumbnail: document.getElementById("thumbnail")?.value.trim() || "",
description: descriptionElement?.value.trim() || "",
mode: isDevMode ? "dev" : "live",
isPrivate, // new field to handle private listings
};
this.log("Form Data Collected:", formData);
if (!formData.title || !formData.magnet) {
this.showError("Title and Magnet URI are required.");
return;
}
try {
await nostrClient.publishVideo(formData, this.pubkey);
this.submitForm.reset();
// If the private checkbox was checked, reset it
if (this.isPrivateCheckbox) {
this.isPrivateCheckbox.checked = false;
}
// CHANGE: Use videoList component to refresh
await videoList.loadVideos(); // <-- Change this line
this.showSuccess("Video shared successfully!");
} catch (error) {
this.log("Failed to publish video:", error.message);
this.showError("Failed to share video. Please try again later.");
}
}
/**
* Gets a user-friendly error message.
*/
getErrorMessage(error) {
if (error.message.includes("404")) {
return "Service worker not found. Please check server configuration.";
} else if (error.message.includes("Brave")) {
return "Please disable Brave Shields for this site to play videos.";
} else if (error.message.includes("timeout")) {
return "Connection timeout. Please check your internet connection.";
} else {
return "Failed to play video. Please try again.";
}
}
/**
* Shows an error message to the user.
*/
showError(message) {
if (this.errorContainer) {
this.errorContainer.textContent = message;
this.errorContainer.classList.remove("hidden");
setTimeout(() => {
this.errorContainer.classList.add("hidden");
this.errorContainer.textContent = "";
}, 5000);
} else {
alert(message);
}
}
/**
* Shows a success message to the user.
*/
showSuccess(message) {
if (this.successContainer) {
this.successContainer.textContent = message;
this.successContainer.classList.remove("hidden");
setTimeout(() => {
this.successContainer.classList.add("hidden");
this.successContainer.textContent = "";
}, 5000);
} else {
alert(message);
}
}
/**
* Logs messages to console.
*/
log(message) {
console.log(message);
}
/**
* Plays a video given its magnet URI.
* This method handles the logic to initiate torrent download and play the video.
*/
async playVideo(magnetURI) {
try {
if (!magnetURI) {
this.showError("Invalid Magnet URI.");
return;
}
const decodedMagnet = decodeURIComponent(magnetURI);
if (this.currentMagnetUri === decodedMagnet) {
this.log("Same video requested - already playing");
return;
}
this.currentMagnetUri = decodedMagnet;
this.playerModal.style.display = "flex";
this.playerModal.classList.remove("hidden");
// Re-fetch the latest from relays
const videos = await nostrClient.fetchVideos();
const video = videos.find((v) => v.magnet === decodedMagnet);
if (!video) {
this.showError("Video data not found.");
return;
}
// Decrypt only once if user owns it
if (
video.isPrivate &&
video.pubkey === this.pubkey &&
!video.alreadyDecrypted
) {
this.log("User owns a private video => decrypting magnet link...");
video.magnet = fakeDecrypt(video.magnet);
// Mark it so we don't do it again
video.alreadyDecrypted = true;
}
const finalMagnet = video.magnet;
// Profile fetch
let creatorProfile = {
name: "Unknown",
picture: `https://robohash.org/${video.pubkey}`,
};
try {
const userEvents = await nostrClient.pool.list(nostrClient.relays, [
{
kinds: [0],
authors: [video.pubkey],
limit: 1,
},
]);
// Ensure userEvents isn't empty before accessing [0]
if (userEvents.length > 0 && userEvents[0]?.content) {
const profile = JSON.parse(userEvents[0].content);
creatorProfile = {
name: profile.name || profile.display_name || "Unknown",
picture: profile.picture || `https://robohash.org/${video.pubkey}`,
};
}
} catch (error) {
this.log("Error fetching creator profile:", error);
}
let creatorNpub = "Unknown";
try {
creatorNpub = window.NostrTools.nip19.npubEncode(video.pubkey);
} catch (error) {
this.log("Error converting pubkey to npub:", error);
creatorNpub = video.pubkey;
}
this.videoTitle.textContent = video.title || "Untitled";
this.videoDescription.textContent =
video.description || "No description available.";
this.videoTimestamp.textContent = formatTimeAgo(video.created_at);
this.creatorName.textContent = creatorProfile.name;
this.creatorNpub.textContent = `${creatorNpub.slice(
0,
8
)}...${creatorNpub.slice(-4)}`;
this.creatorAvatar.src = creatorProfile.picture;
this.creatorAvatar.alt = creatorProfile.name;
this.log("Starting video stream with:", finalMagnet);
await torrentClient.streamVideo(finalMagnet, this.modalVideo);
const updateInterval = setInterval(() => {
if (!document.body.contains(this.modalVideo)) {
clearInterval(updateInterval);
return;
}
const status = document.getElementById("status");
const progress = document.getElementById("progress");
const peers = document.getElementById("peers");
const speed = document.getElementById("speed");
const downloaded = document.getElementById("downloaded");
if (status) this.modalStatus.textContent = status.textContent;
if (progress) this.modalProgress.style.width = progress.style.width;
if (peers) this.modalPeers.textContent = peers.textContent;
if (speed) this.modalSpeed.textContent = speed.textContent;
if (downloaded)
this.modalDownloaded.textContent = downloaded.textContent;
}, 1000);
} catch (error) {
this.log("Error in playVideo:", error);
this.showError(`Playback error: ${error.message}`);
}
}
updateTorrentStatus(torrent) {
if (!torrent) return;
this.modalStatus.textContent = torrent.status;
this.modalProgress.style.width = `${(torrent.progress * 100).toFixed(2)}%`;
this.modalPeers.textContent = `Peers: ${torrent.numPeers}`;
this.modalSpeed.textContent = `${(torrent.downloadSpeed / 1024).toFixed(
2
)} KB/s`;
this.modalDownloaded.textContent = `${(
torrent.downloaded /
(1024 * 1024)
).toFixed(2)} MB / ${(torrent.length / (1024 * 1024)).toFixed(2)} MB`;
if (torrent.ready) {
this.modalStatus.textContent = "Ready to play";
} else {
setTimeout(() => this.updateTorrentStatus(torrent), 1000);
}
}
/**
* Allows the user to edit a video note (only if they are the owner).
* We reuse the note's existing d tag via nostrClient.editVideo.
*/
async handleEditVideo(index) {
try {
// CHANGE: Get videos through videoList component
const videos = await nostrClient.fetchVideos();
const video = videos[index];
if (!this.pubkey) {
this.showError("Please login to edit videos.");
return;
}
if (video.pubkey !== this.pubkey) {
this.showError("You do not own this video.");
return;
}
// Prompt for new fields or keep old
const newTitle = prompt(
"New Title? (Leave blank to keep existing)",
video.title
);
const newMagnet = prompt(
"New Magnet? (Leave blank to keep existing)",
video.magnet
);
const newThumbnail = prompt(
"New Thumbnail? (Leave blank to keep existing)",
video.thumbnail
);
const newDescription = prompt(
"New Description? (Leave blank to keep existing)",
video.description
);
// Ask user if they want the note private or public
const wantPrivate = confirm("Make this video private? OK=Yes, Cancel=No");
// Fallback to old if user typed nothing
const title =
newTitle === null || newTitle.trim() === ""
? video.title
: newTitle.trim();
const magnet =
newMagnet === null || newMagnet.trim() === ""
? video.magnet
: newMagnet.trim();
const thumbnail =
newThumbnail === null || newThumbnail.trim() === ""
? video.thumbnail
: newThumbnail.trim();
const description =
newDescription === null || newDescription.trim() === ""
? video.description
: newDescription.trim();
// Build final updated data
const updatedData = {
version: video.version || 2, // keep old version or set 2
isPrivate: wantPrivate,
title,
magnet,
thumbnail,
description,
mode: isDevMode ? "dev" : "live",
};
// Edit
const originalEvent = {
id: video.id,
pubkey: video.pubkey,
tags: video.tags,
};
await nostrClient.editVideo(originalEvent, updatedData, this.pubkey);
this.showSuccess("Video updated successfully!");
await videoList.loadVideos();
} catch (err) {
this.log("Failed to edit video:", err.message);
this.showError("Failed to edit video. Please try again later.");
}
}
/**
* ADDED FOR VERSIONING/PRIVATE/DELETE:
* Allows the user to delete (soft-delete) a video by marking it as deleted.
*/
async handleDeleteVideo(index) {
try {
// CHANGE: Get videos through videoList component
const videos = await nostrClient.fetchVideos();
const video = videos[index];
if (!this.pubkey) {
this.showError("Please login to delete videos.");
return;
}
if (video.pubkey !== this.pubkey) {
this.showError("You do not own this video.");
return;
}
if (
!confirm(
`Are you sure you want to delete "${video.title}"? This action cannot be undone.`
)
) {
return;
}
const originalEvent = {
id: video.id,
pubkey: video.pubkey,
tags: video.tags,
};
await nostrClient.deleteVideo(originalEvent, this.pubkey);
this.showSuccess("Video deleted (hidden) successfully!");
// CHANGE: Use videoList component to refresh
await videoList.loadVideos();
} catch (err) {
this.log("Failed to delete video:", err.message);
this.showError("Failed to delete video. Please try again later.");
}
}
}
export const app = new bitvidApp();
app.init();
window.app = app;

View File

@@ -1,285 +0,0 @@
// js/components/VideoList.js
import { nostrClient } from "../nostr.js";
import { formatTimeAgo } from "../utils/timeUtils.js";
import { escapeHTML } from "../utils/htmlUtils.js";
export class VideoList {
constructor() {
this.videoList = document.getElementById("videoList");
this.pubkey = null; // We'll need this for private video filtering
}
setPubkey(pubkey) {
this.pubkey = pubkey;
}
async loadVideos() {
console.log("Starting loadVideos...");
try {
const videos = await nostrClient.fetchVideos();
console.log("Raw videos from nostrClient:", videos);
if (!videos) {
console.log("No videos received");
throw new Error("No videos received from relays");
}
// Convert to array if not already
const videosArray = Array.isArray(videos) ? videos : [videos];
// Filter private videos
const displayedVideos = videosArray.filter((video) => {
if (!video.isPrivate) {
return true;
}
return this.pubkey && video.pubkey === this.pubkey;
});
if (displayedVideos.length === 0) {
console.log("No valid videos found after filtering.");
this.renderEmptyState(
"No public videos available yet. Be the first to upload one!"
);
return;
}
console.log("Processing filtered videos:", displayedVideos);
await this.renderVideoList(displayedVideos);
console.log(`Rendered ${displayedVideos.length} videos successfully`);
} catch (error) {
console.log("Failed to fetch videos:", error);
this.renderEmptyState(
"No videos available at the moment. Please try again later."
);
}
}
renderEmptyState(message) {
if (this.videoList) {
this.videoList.innerHTML = `
<p class="text-center text-gray-500">
${escapeHTML(message)}
</p>`;
}
}
async renderVideoList(videos) {
try {
console.log("RENDER VIDEO LIST - Start", {
videosReceived: videos,
videosCount: videos ? videos.length : "N/A",
videosType: typeof videos,
});
if (!videos || videos.length === 0) {
this.renderEmptyState("No videos found.");
return;
}
// Sort by creation date
const videoArray = [...videos].sort(
(a, b) => b.created_at - a.created_at
);
// Fetch user profiles
const userProfiles = await this.fetchUserProfiles(videoArray);
// Build HTML for each video
const renderedVideos = videoArray
.map((video, index) => this.renderVideoCard(video, index, userProfiles))
.filter(Boolean);
if (renderedVideos.length === 0) {
this.renderEmptyState("No valid videos to display.");
return;
}
this.videoList.innerHTML = renderedVideos.join("");
console.log("Videos rendered successfully");
} catch (error) {
console.error("Rendering error:", error);
this.renderEmptyState("Error loading videos.");
}
}
async fetchUserProfiles(videos) {
const userProfiles = new Map();
const uniquePubkeys = [...new Set(videos.map((v) => v.pubkey))];
for (const pubkey of uniquePubkeys) {
try {
const profile = await nostrClient.fetchUserProfile(pubkey);
userProfiles.set(pubkey, profile);
} catch (error) {
console.error(`Profile fetch error for ${pubkey}:`, error);
userProfiles.set(pubkey, {
name: "Unknown",
picture: `https://robohash.org/${pubkey}`,
});
}
}
return userProfiles;
}
renderVideoCard(video, index, userProfiles) {
try {
if (!this.validateVideo(video, index)) {
console.error(`Invalid video: ${video.title}`);
return "";
}
const profile = userProfiles.get(video.pubkey) || {
name: "Unknown",
picture: `https://robohash.org/${video.pubkey}`,
};
const canEdit = video.pubkey === this.pubkey;
const highlightClass =
video.isPrivate && canEdit
? "border-2 border-yellow-500"
: "border-none";
return `
<div class="video-card bg-gray-900 rounded-lg overflow-hidden shadow-lg hover:shadow-2xl transition-all duration-300 ${highlightClass}">
${this.renderThumbnail(video)}
${this.renderCardInfo(video, profile)}
</div>
`;
} catch (error) {
console.error(`Error processing video ${index}:`, error);
return "";
}
}
renderThumbnail(video) {
return `
<div
class="aspect-w-16 aspect-h-9 bg-gray-800 cursor-pointer relative group"
onclick="window.app.playVideo('${encodeURIComponent(video.magnet)}')"
>
${
video.thumbnail
? this.renderThumbnailImage(video)
: this.renderPlaceholderThumbnail()
}
<div class="absolute inset-0 bg-black bg-opacity-0 group-hover:bg-opacity-20 transition-opacity duration-300"></div>
</div>
`;
}
renderThumbnailImage(video) {
return `
<img
src="${escapeHTML(video.thumbnail)}"
alt="${escapeHTML(video.title)}"
class="w-full h-full object-cover"
>
`;
}
renderPlaceholderThumbnail() {
return `
<div class="flex items-center justify-center h-full bg-gray-800">
<svg class="w-16 h-16 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</div>
`;
}
renderCardInfo(video, profile) {
const timeAgo = formatTimeAgo(video.created_at);
const canEdit = video.pubkey === this.pubkey;
return `
<div class="p-4">
<h3 class="text-lg font-bold text-white line-clamp-2 hover:text-blue-400 cursor-pointer mb-3"
onclick="window.app.playVideo('${encodeURIComponent(
video.magnet
)}')">
${escapeHTML(video.title)}
</h3>
<div class="flex items-center justify-between">
<div class="flex items-center space-x-3">
<div class="w-8 h-8 rounded-full bg-gray-700 overflow-hidden">
<img src="${escapeHTML(profile.picture)}"
alt="${profile.name}"
class="w-full h-full object-cover">
</div>
<div class="min-w-0">
<p class="text-sm text-gray-400 hover:text-gray-300 cursor-pointer">
${escapeHTML(profile.name)}
</p>
<div class="flex items-center text-xs text-gray-500 mt-1">
<span>${timeAgo}</span>
</div>
</div>
</div>
${this.renderGearMenu(video, canEdit)}
</div>
</div>
`;
}
renderGearMenu(video, canEdit) {
if (!canEdit) return "";
return `
<div class="relative inline-block ml-3 overflow-visible">
<button type="button"
class="inline-flex items-center p-2 rounded-full text-gray-400 hover:text-gray-200 hover:bg-gray-800 focus:outline-none focus:ring-2 focus:ring-blue-500"
onclick="document.getElementById('settingsDropdown-${video.id}').classList.toggle('hidden')">
<img src="assets/svg/video-settings-gear.svg"
alt="Settings"
class="w-5 h-5"/>
</button>
<div id="settingsDropdown-${video.id}"
class="hidden absolute right-0 bottom-full mb-2 w-32 rounded-md shadow-lg bg-gray-800 ring-1 ring-black ring-opacity-5 z-50">
<div class="py-1">
<button class="block w-full text-left px-4 py-2 text-sm text-gray-100 hover:bg-gray-700"
onclick="app.handleEditVideo('${video.id}'); document.getElementById('settingsDropdown-${video.id}').classList.add('hidden');">
Edit
</button>
<button class="block w-full text-left px-4 py-2 text-sm text-red-400 hover:bg-red-700 hover:text-white"
onclick="app.handleDeleteVideo('${video.id}'); document.getElementById('settingsDropdown-${video.id}').classList.add('hidden');">
Delete
</button>
</div>
</div>
</div>
`;
}
validateVideo(video, index) {
const validationResults = {
hasId: Boolean(video?.id),
isValidId: typeof video?.id === "string" && video.id.trim().length > 0,
hasVideo: Boolean(video),
hasTitle: Boolean(video?.title),
hasMagnet: Boolean(video?.magnet),
hasMode: Boolean(video?.mode),
hasPubkey: Boolean(video?.pubkey),
isValidTitle: typeof video?.title === "string" && video.title.length > 0,
isValidMagnet:
typeof video?.magnet === "string" && video.magnet.length > 0,
isValidMode:
typeof video?.mode === "string" && ["dev", "live"].includes(video.mode),
};
const passed = Object.values(validationResults).every(Boolean);
console.log(
`Video ${video?.title} validation results:`,
validationResults,
passed ? "PASSED" : "FAILED"
);
return passed;
}
}
export const videoList = new VideoList();

View File

@@ -1,187 +0,0 @@
// js/components/VideoPlayer.js
export class VideoPlayer {
constructor() {
// Initialize these as null - they'll be set after modal loads
this.playerModal = null;
this.modalVideo = null;
this.modalStatus = null;
this.modalProgress = null;
this.modalPeers = null;
this.modalSpeed = null;
this.modalDownloaded = null;
this.closePlayerBtn = null;
this.videoTitle = null;
this.videoDescription = null;
this.videoTimestamp = null;
this.creatorAvatar = null;
this.creatorName = null;
this.creatorNpub = null;
this.currentMagnetUri = null;
}
async initModal() {
try {
console.log("Starting modal initialization...");
const response = await fetch("components/video-modal.html");
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const html = await response.text();
console.log("Modal HTML loaded successfully");
const modalContainer = document.getElementById("modalContainer");
if (!modalContainer) {
throw new Error("Modal container element not found!");
}
modalContainer.innerHTML = html;
console.log("Modal HTML inserted into DOM");
this.updateModalElements();
await this.setupEventListeners();
console.log("Modal initialization completed successfully");
return true;
} catch (error) {
console.error("Modal initialization failed:", error);
throw error;
}
}
updateModalElements() {
// Update Modal Elements
this.playerModal = document.getElementById("playerModal");
this.modalVideo = document.getElementById("modalVideo");
this.modalStatus = document.getElementById("modalStatus");
this.modalProgress = document.getElementById("modalProgress");
this.modalPeers = document.getElementById("modalPeers");
this.modalSpeed = document.getElementById("modalSpeed");
this.modalDownloaded = document.getElementById("modalDownloaded");
this.closePlayerBtn = document.getElementById("closeModal");
// Update Video Info Elements
this.videoTitle = document.getElementById("videoTitle");
this.videoDescription = document.getElementById("videoDescription");
this.videoTimestamp = document.getElementById("videoTimestamp");
// Update Creator Info Elements
this.creatorAvatar = document.getElementById("creatorAvatar");
this.creatorName = document.getElementById("creatorName");
this.creatorNpub = document.getElementById("creatorNpub");
this.setupScrollBehavior();
}
setupScrollBehavior() {
// Add scroll behavior for nav
let lastScrollY = 0;
const modalNav = document.getElementById("modalNav");
if (this.playerModal && modalNav) {
this.playerModal.addEventListener("scroll", (e) => {
const currentScrollY = e.target.scrollTop;
const shouldShowNav =
currentScrollY <= lastScrollY || currentScrollY < 50;
modalNav.style.transform = shouldShowNav
? "translateY(0)"
: "translateY(-100%)";
lastScrollY = currentScrollY;
});
}
}
async setupEventListeners() {
// Set up modal close handler
if (this.closePlayerBtn) {
this.closePlayerBtn.addEventListener("click", () => this.hide());
}
// Close Modal by clicking outside content
if (this.playerModal) {
this.playerModal.addEventListener("click", async (e) => {
if (e.target === this.playerModal) {
await this.hide();
}
});
}
// Video error handling
if (this.modalVideo) {
this.setupVideoEventListeners();
}
}
setupVideoEventListeners() {
this.modalVideo.addEventListener("error", (e) => {
const error = e.target.error;
console.log("Modal video error:", error);
if (error) {
console.log("Error code:", error.code);
console.log("Error message:", error.message);
// You'll need to implement showError or pass it as a callback
// this.showError(`Video playback error: ${error.message || "Unknown error"}`);
}
});
this.modalVideo.addEventListener("loadstart", () => {
console.log("Video loadstart event fired");
});
this.modalVideo.addEventListener("loadedmetadata", () => {
console.log("Video loadedmetadata event fired");
});
this.modalVideo.addEventListener("canplay", () => {
console.log("Video canplay event fired");
});
}
async hide() {
await this.cleanup();
if (this.playerModal) {
this.playerModal.style.display = "none";
this.playerModal.classList.add("hidden");
}
}
async cleanup() {
if (this.modalVideo) {
this.modalVideo.pause();
this.modalVideo.src = "";
this.modalVideo.load();
}
}
show() {
if (this.playerModal) {
this.playerModal.style.display = "flex";
this.playerModal.classList.remove("hidden");
}
}
updateTorrentStatus(torrent) {
if (!torrent) return;
this.modalStatus.textContent = torrent.status;
this.modalProgress.style.width = `${(torrent.progress * 100).toFixed(2)}%`;
this.modalPeers.textContent = `Peers: ${torrent.numPeers}`;
this.modalSpeed.textContent = `${(torrent.downloadSpeed / 1024).toFixed(
2
)} KB/s`;
this.modalDownloaded.textContent = `${(
torrent.downloaded /
(1024 * 1024)
).toFixed(2)} MB / ${(torrent.length / (1024 * 1024)).toFixed(2)} MB`;
if (torrent.ready) {
this.modalStatus.textContent = "Ready to play";
} else {
setTimeout(() => this.updateTorrentStatus(torrent), 1000);
}
}
}
export const videoPlayer = new VideoPlayer();

View File

@@ -1,4 +0,0 @@
// js/config.js
export const isDevMode = true; // Set to false for production
export const isWhitelistEnabled = true; // Set to false to allow all non-blacklisted users

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -1,13 +0,0 @@
// js/lists.js
const npubs = [
"npub13yarr7j6vjqjjkahd63dmr27curypehx45ucue286ac7sft27y0srnpmpe",
"npub15jnttpymeytm80hatjqcvhhqhzrhx6gxp8pq0wn93rhnu8s9h9dsha32lx",
"npub1j37gc05qpqzyrmdc5vetsc9h5qtstas7tr25j0n9sdpqxghz6m4q2ej6n8",
"npub1epvnvv3kskvpnmpqgnm2atevsmdferhp7dg2s0yc7uc0hdmqmgssx09tu2",
];
console.log("DEBUG: lists.js loaded, npubs:", npubs);
export const initialWhitelist = npubs;
export const initialBlacklist = [""];

View File

@@ -1,677 +0,0 @@
// js/nostr.js
import { isDevMode } from "./config.js";
import { accessControl } from "./accessControl.js";
const RELAY_URLS = [
"wss://relay.damus.io",
"wss://nos.lol",
"wss://relay.snort.social",
"wss://nostr.wine",
];
// Rate limiting for error logs
let errorLogCount = 0;
const MAX_ERROR_LOGS = 100; // Adjust as needed
function logErrorOnce(message, eventContent = null) {
if (errorLogCount < MAX_ERROR_LOGS) {
console.error(message);
if (eventContent) {
console.log(`Event Content: ${eventContent}`);
}
errorLogCount++;
}
if (errorLogCount === MAX_ERROR_LOGS) {
console.error(
"Maximum error log limit reached. Further errors will be suppressed."
);
}
}
/**
* A very naive "encryption" function that just reverses the string.
* In a real app, use a proper crypto library (AES-GCM, ECDH, etc.).
*/
function fakeEncrypt(magnet) {
return magnet.split("").reverse().join("");
}
function fakeDecrypt(encrypted) {
return encrypted.split("").reverse().join("");
}
class NostrClient {
constructor() {
this.pool = null;
this.pubkey = null;
this.relays = RELAY_URLS;
}
/**
* Initializes the Nostr client by connecting to relays.
*/
async init() {
if (isDevMode) console.log("Connecting to relays...");
try {
this.pool = new window.NostrTools.SimplePool();
const results = await this.connectToRelays();
const successfulRelays = results
.filter((r) => r.success)
.map((r) => r.url);
if (successfulRelays.length === 0) throw new Error("No relays connected");
if (isDevMode)
console.log(`Connected to ${successfulRelays.length} relay(s)`);
} catch (err) {
console.error("Nostr init failed:", err);
throw err;
}
}
// Helper method to handle relay connections
async connectToRelays() {
return Promise.all(
this.relays.map(
(url) =>
new Promise((resolve) => {
const sub = this.pool.sub([url], [{ kinds: [0], limit: 1 }]);
const timeout = setTimeout(() => {
sub.unsub();
resolve({ url, success: false });
}, 5000);
const succeed = () => {
clearTimeout(timeout);
sub.unsub();
resolve({ url, success: true });
};
sub.on("event", succeed);
sub.on("eose", succeed);
})
)
);
}
/**
* Logs in the user using a Nostr extension or by entering an NSEC key.
*/
async login() {
try {
if (!window.nostr) {
console.log("No Nostr extension found");
throw new Error(
"Please install a Nostr extension (like Alby or nos2x)."
);
}
const pubkey = await window.nostr.getPublicKey();
const npub = window.NostrTools.nip19.npubEncode(pubkey);
// Debug logs
if (isDevMode) {
console.log("Got pubkey:", pubkey);
console.log("Converted to npub:", npub);
console.log("Whitelist:", accessControl.getWhitelist());
console.log("Blacklist:", accessControl.getBlacklist());
console.log("Is whitelisted?", accessControl.isWhitelisted(npub));
console.log("Is blacklisted?", accessControl.isBlacklisted(npub));
}
// Check access control
if (!accessControl.canAccess(npub)) {
if (accessControl.isBlacklisted(npub)) {
throw new Error(
"Your account has been blocked from accessing this platform."
);
} else {
throw new Error(
"Access is currently restricted to whitelisted users only."
);
}
}
this.pubkey = pubkey;
if (isDevMode)
console.log(
"Successfully logged in with extension. Public key:",
this.pubkey
);
return this.pubkey;
} catch (e) {
console.error("Login error:", e);
throw e;
}
}
/**
* Logs out the user.
*/
logout() {
this.pubkey = null;
if (isDevMode) console.log("User logged out.");
}
/**
* Decodes an NSEC key.
*/
decodeNsec(nsec) {
try {
const { data } = window.NostrTools.nip19.decode(nsec);
return data;
} catch (error) {
throw new Error("Invalid NSEC key.");
}
}
/**
* Publishes a new video event to all relays (creates a brand-new note).
*/
async publishVideo(videoData, pubkey) {
if (!pubkey) {
throw new Error("User is not logged in.");
}
if (isDevMode) {
console.log("Publishing video with data:", videoData);
}
// If user sets "isPrivate = true", encrypt the magnet
let finalMagnet = videoData.magnet;
if (videoData.isPrivate === true) {
finalMagnet = fakeEncrypt(finalMagnet);
}
// Default version is 1 if not specified
const version = videoData.version ?? 1;
const uniqueD = `${Date.now()}-${Math.random()
.toString(36)
.substring(2, 10)}`;
// Always mark "deleted" false for new posts
const contentObject = {
version,
deleted: false,
isPrivate: videoData.isPrivate || false,
title: videoData.title,
magnet: finalMagnet,
thumbnail: videoData.thumbnail,
description: videoData.description,
mode: videoData.mode,
};
const event = {
kind: 30078,
pubkey,
created_at: Math.floor(Date.now() / 1000),
tags: [
["t", "video"],
["d", uniqueD],
],
content: JSON.stringify(contentObject),
};
if (isDevMode) {
console.log("Event content after stringify:", event.content);
console.log("Using d tag:", uniqueD);
}
try {
const signedEvent = await window.nostr.signEvent(event);
if (isDevMode) {
console.log("Signed event:", signedEvent);
}
await Promise.all(
this.relays.map(async (url) => {
try {
await this.pool.publish([url], signedEvent);
if (isDevMode) {
console.log(`Event published to ${url}`);
}
} catch (err) {
if (isDevMode) {
console.error(`Failed to publish to ${url}:`, err.message);
}
}
})
);
return signedEvent;
} catch (error) {
if (isDevMode) {
console.error("Failed to sign event:", error.message);
}
throw new Error("Failed to sign event.");
}
}
/**
* Edits an existing video event by reusing the same "d" tag.
* Allows toggling isPrivate on/off and re-encrypting or decrypting the magnet.
*/
// Minimal fix: ensures we only ever encrypt once per edit operation
async editVideo(originalEvent, updatedVideoData, pubkey) {
if (!pubkey) {
throw new Error("User is not logged in.");
}
if (originalEvent.pubkey !== pubkey) {
throw new Error("You do not own this event (different pubkey).");
}
if (isDevMode) {
console.log("Editing video event:", originalEvent);
console.log("New video data:", updatedVideoData);
}
// Grab the d tag from the original event
const dTag = originalEvent.tags.find((tag) => tag[0] === "d");
if (!dTag) {
throw new Error(
'This event has no "d" tag, cannot edit as addressable kind=30078.'
);
}
const existingD = dTag[1];
// Parse old content
const oldContent = JSON.parse(originalEvent.content || "{}");
if (isDevMode) {
console.log("Old content:", oldContent);
}
// Keep old version & deleted status
const oldVersion = oldContent.version ?? 1;
const oldDeleted = oldContent.deleted === true;
const newVersion = updatedVideoData.version ?? oldVersion;
const oldWasPrivate = oldContent.isPrivate === true;
// 1) If old was private, decrypt the old magnet once => oldPlainMagnet
let oldPlainMagnet = oldContent.magnet || "";
if (oldWasPrivate && oldPlainMagnet) {
oldPlainMagnet = fakeDecrypt(oldPlainMagnet);
}
// 2) If updatedVideoData.isPrivate is explicitly set, use that; else keep the old isPrivate
const newIsPrivate =
typeof updatedVideoData.isPrivate === "boolean"
? updatedVideoData.isPrivate
: oldContent.isPrivate ?? false;
// 3) The user might type a new magnet or keep oldPlainMagnet
const userTypedMagnet = (updatedVideoData.magnet || "").trim();
const finalPlainMagnet = userTypedMagnet || oldPlainMagnet;
// 4) If new is private => encrypt finalPlainMagnet once; otherwise store plaintext
let finalMagnet = finalPlainMagnet;
if (newIsPrivate) {
finalMagnet = fakeEncrypt(finalPlainMagnet);
}
// Build updated content
const contentObject = {
version: newVersion,
deleted: oldDeleted,
isPrivate: newIsPrivate,
title: updatedVideoData.title,
magnet: finalMagnet,
thumbnail: updatedVideoData.thumbnail,
description: updatedVideoData.description,
mode: updatedVideoData.mode,
};
if (isDevMode) {
console.log("Building updated content object:", contentObject);
}
const event = {
kind: 30078,
pubkey,
created_at: Math.floor(Date.now() / 1000),
tags: [
["t", "video"],
["d", existingD],
],
content: JSON.stringify(contentObject),
};
if (isDevMode) {
console.log("Reusing d tag:", existingD);
console.log("Updated event content:", event.content);
}
try {
const signedEvent = await window.nostr.signEvent(event);
if (isDevMode) {
console.log("Signed edited event:", signedEvent);
}
// Publish to all relays
await Promise.all(
this.relays.map(async (url) => {
try {
await this.pool.publish([url], signedEvent);
if (isDevMode) {
console.log(
`Edited event published to ${url} (d="${existingD}")`
);
}
} catch (err) {
if (isDevMode) {
console.error(
`Failed to publish edited event to ${url}:`,
err.message
);
}
}
})
);
return signedEvent;
} catch (error) {
if (isDevMode) {
console.error("Failed to sign edited event:", error.message);
}
throw new Error("Failed to sign edited event.");
}
}
/**
* Soft-delete or hide an existing video by marking content as "deleted: true"
* and republishing with same (kind=30078, pubkey, d) address.
*/
async deleteVideo(originalEvent, pubkey) {
if (!pubkey) {
throw new Error("User is not logged in.");
}
if (originalEvent.pubkey !== pubkey) {
throw new Error("You do not own this event (different pubkey).");
}
if (isDevMode) {
console.log("Deleting video event:", originalEvent);
}
const dTag = originalEvent.tags.find((tag) => tag[0] === "d");
if (!dTag) {
throw new Error(
'This event has no "d" tag, cannot delete as addressable kind=30078.'
);
}
const existingD = dTag[1];
const oldContent = JSON.parse(originalEvent.content || "{}");
const oldVersion = oldContent.version ?? 1;
const contentObject = {
version: oldVersion,
deleted: true,
title: oldContent.title || "",
magnet: "",
thumbnail: "",
description: "This video has been deleted.",
mode: oldContent.mode || "live",
isPrivate: oldContent.isPrivate || false,
};
const event = {
kind: 30078,
pubkey,
created_at: Math.floor(Date.now() / 1000),
tags: [
["t", "video"],
["d", existingD],
],
content: JSON.stringify(contentObject),
};
if (isDevMode) {
console.log("Reusing d tag for delete:", existingD);
console.log("Deleted event content:", event.content);
}
try {
const signedEvent = await window.nostr.signEvent(event);
if (isDevMode) {
console.log("Signed deleted event:", signedEvent);
}
await Promise.all(
this.relays.map(async (url) => {
try {
await this.pool.publish([url], signedEvent);
if (isDevMode) {
console.log(
`Deleted event published to ${url} (d="${existingD}")`
);
}
} catch (err) {
if (isDevMode) {
console.error(
`Failed to publish deleted event to ${url}:`,
err.message
);
}
}
})
);
return signedEvent;
} catch (error) {
if (isDevMode) {
console.error("Failed to sign deleted event:", error.message);
}
throw new Error("Failed to sign deleted event.");
}
}
/**
* Fetches videos from all configured relays.
*/
async fetchVideos() {
const filter = {
kinds: [30078],
"#t": ["video"],
limit: 1000,
since: 0,
};
const videoEvents = new Map();
if (isDevMode) {
console.log("[fetchVideos] Starting fetch from all relays...");
console.log("[fetchVideos] Filter:", filter);
}
try {
await Promise.all(
this.relays.map(async (url) => {
if (isDevMode) console.log(`[fetchVideos] Querying relay: ${url}`);
try {
const events = await this.pool.list([url], [filter]);
if (isDevMode) {
console.log(`Events from ${url}:`, events.length);
if (events.length > 0) {
events.forEach((evt, idx) => {
console.log(
`[fetchVideos] [${url}] Event[${idx}] ID: ${evt.id} | pubkey: ${evt.pubkey} | created_at: ${evt.created_at}`
);
});
}
}
events.forEach((event) => {
try {
const content = JSON.parse(event.content);
// If deleted == true, it overrides older notes
if (content.deleted === true) {
videoEvents.delete(event.id);
return;
}
// If we haven't seen this event.id before, store it
if (!videoEvents.has(event.id)) {
videoEvents.set(event.id, {
id: event.id,
version: content.version ?? 1,
isPrivate: content.isPrivate ?? false,
title: content.title || "",
magnet: content.magnet || "",
thumbnail: content.thumbnail || "",
description: content.description || "",
mode: content.mode || "live",
pubkey: event.pubkey,
created_at: event.created_at,
tags: event.tags,
});
}
} catch (parseError) {
if (isDevMode) {
console.error(
"[fetchVideos] Event parsing error:",
parseError
);
}
}
});
} catch (relayError) {
if (isDevMode) {
console.error(
`[fetchVideos] Error fetching from ${url}:`,
relayError
);
}
}
})
);
const videos = Array.from(videoEvents.values()).sort(
(a, b) => b.created_at - a.created_at
);
// Apply access control filtering
const filteredVideos = accessControl.filterVideos(videos);
if (isDevMode) {
console.log("[fetchVideos] All relays have responded.");
console.log(
`[fetchVideos] Total unique video events: ${videoEvents.size}`
);
console.log(
`[fetchVideos] Videos after filtering: ${filteredVideos.length}`
);
}
return filteredVideos;
} catch (error) {
if (isDevMode) {
console.error("FETCH VIDEOS ERROR:", error);
}
return [];
}
}
/**
* Fetches a user profile given a pubkey.
* Returns an object with 'name' and 'picture' properties.
*/
async fetchUserProfile(pubkey) {
if (!pubkey) {
throw new Error("Invalid pubkey provided.");
}
if (isDevMode) {
console.log(`Fetching profile for pubkey: ${pubkey}`);
}
const filter = {
kinds: [0], // Profile events
authors: [pubkey],
limit: 1,
};
try {
const events = await this.pool.list(this.relays, [filter]);
if (events.length === 0) {
if (isDevMode) {
console.log(`No profile found for pubkey: ${pubkey}`);
}
return {
name: "Unknown",
picture: `https://robohash.org/${pubkey}`,
};
}
const profileContent = JSON.parse(events[0].content || "{}");
const profile = {
name:
profileContent.name ||
profileContent.display_name ||
`User ${pubkey.slice(0, 8)}...`,
picture: profileContent.picture || `https://robohash.org/${pubkey}`,
};
if (isDevMode) {
console.log(`Fetched profile for ${pubkey}:`, profile);
}
return profile;
} catch (error) {
logErrorOnce(`Error fetching profile for ${pubkey}:`, error.message);
throw error;
}
}
/**
* Validates video content structure.
*/
isValidVideo(content) {
try {
const isValid =
content &&
typeof content === "object" &&
typeof content.title === "string" &&
content.title.length > 0 &&
typeof content.magnet === "string" &&
content.magnet.length > 0 &&
typeof content.mode === "string" &&
["dev", "live"].includes(content.mode) &&
(typeof content.thumbnail === "string" ||
typeof content.thumbnail === "undefined") &&
(typeof content.description === "string" ||
typeof content.description === "undefined");
if (isDevMode && !isValid) {
console.log("Invalid video content:", content);
console.log("Validation details:", {
hasTitle: typeof content.title === "string",
hasMagnet: typeof content.magnet === "string",
hasMode: typeof content.mode === "string",
validThumbnail:
typeof content.thumbnail === "string" ||
typeof content.thumbnail === "undefined",
validDescription:
typeof content.description === "string" ||
typeof content.description === "undefined",
});
}
return isValid;
} catch (error) {
if (isDevMode) {
console.error("Error validating video:", error);
}
return false;
}
}
}
export const nostrClient = new NostrClient();

View File

@@ -1,9 +0,0 @@
// js/utils/htmlUtils.js
export function escapeHTML(unsafe) {
return unsafe
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#039;");
}

View File

@@ -1,21 +0,0 @@
// js/utils/timeUtils.js
export function formatTimeAgo(timestamp) {
const seconds = Math.floor(Date.now() / 1000 - timestamp);
const intervals = {
year: 31536000,
month: 2592000,
week: 604800,
day: 86400,
hour: 3600,
minute: 60,
};
for (const [unit, secondsInUnit] of Object.entries(intervals)) {
const interval = Math.floor(seconds / secondsInUnit);
if (interval >= 1) {
return `${interval} ${unit}${interval === 1 ? "" : "s"} ago`;
}
}
return "just now";
}

View File

@@ -1,286 +0,0 @@
// js/webtorrent.js
import WebTorrent from 'https://esm.sh/webtorrent'
export class TorrentClient {
constructor() {
this.client = new WebTorrent()
this.currentTorrent = null
this.TIMEOUT_DURATION = 60000 // 60 seconds
this.statsInterval = null
}
log(msg) {
console.log(msg)
}
async isBrave() {
return (navigator.brave?.isBrave && await navigator.brave.isBrave()) || false
}
async waitForServiceWorkerActivation(registration) {
return new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
reject(new Error('Service worker activation timeout'))
}, this.TIMEOUT_DURATION)
this.log('Waiting for service worker activation...')
const checkActivation = () => {
if (registration.active) {
clearTimeout(timeout)
this.log('Service worker is active')
resolve(registration)
return true
}
return false
}
if (checkActivation()) return
registration.addEventListener('activate', () => {
checkActivation()
})
if (registration.waiting) {
this.log('Service worker is waiting, sending skip waiting message')
registration.waiting.postMessage({ type: 'SKIP_WAITING' })
}
registration.addEventListener('statechange', () => {
checkActivation()
})
})
}
async setupServiceWorker() {
try {
const isBraveBrowser = await this.isBrave()
if (!window.isSecureContext) {
throw new Error('HTTPS or localhost required')
}
if (!('serviceWorker' in navigator) || !navigator.serviceWorker) {
throw new Error('Service Worker not supported or disabled')
}
if (isBraveBrowser) {
this.log('Checking Brave configuration...')
if (!navigator.serviceWorker) {
throw new Error('Please enable Service Workers in Brave Shield settings')
}
if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
throw new Error('Please enable WebRTC in Brave Shield settings')
}
const registrations = await navigator.serviceWorker.getRegistrations()
for (const registration of registrations) {
await registration.unregister()
}
await new Promise(resolve => setTimeout(resolve, 1000))
}
const currentPath = window.location.pathname
const basePath = currentPath.substring(0, currentPath.lastIndexOf('/') + 1)
this.log('Registering service worker...')
const registration = await navigator.serviceWorker.register('./sw.min.js', {
scope: basePath,
updateViaCache: 'none'
})
this.log('Service worker registered')
if (registration.installing) {
this.log('Waiting for installation...')
await new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
reject(new Error('Installation timeout'))
}, this.TIMEOUT_DURATION)
registration.installing.addEventListener('statechange', (e) => {
this.log('Service worker state:', e.target.state)
if (e.target.state === 'activated' || e.target.state === 'redundant') {
clearTimeout(timeout)
resolve()
}
})
})
}
await this.waitForServiceWorkerActivation(registration)
this.log('Service worker activated')
const readyRegistration = await Promise.race([
navigator.serviceWorker.ready,
new Promise((_, reject) =>
setTimeout(() => reject(new Error('Service worker ready timeout')), this.TIMEOUT_DURATION)
)
])
if (!readyRegistration.active) {
throw new Error('Service worker not active after ready state')
}
this.log('Service worker ready')
return registration
} catch (error) {
this.log('Service worker setup error:', error)
throw error
}
}
formatBytes(bytes) {
if (bytes === 0) return '0 B'
const k = 1024
const sizes = ['B', 'KB', 'MB', 'GB']
const i = Math.floor(Math.log(bytes) / Math.log(k))
return `${(bytes / Math.pow(k, i)).toFixed(1)} ${sizes[i]}`
}
async streamVideo(magnetURI, videoElement) {
try {
// Setup service worker first
const registration = await this.setupServiceWorker()
if (!registration || !registration.active) {
throw new Error('Service worker setup failed')
}
// Create WebTorrent server AFTER service worker is ready
this.client.createServer({ controller: registration })
this.log('WebTorrent server created')
return new Promise((resolve, reject) => {
this.log('Starting torrent download')
this.client.add(magnetURI, torrent => {
this.log('Torrent added: ' + torrent.name)
const status = document.getElementById('status')
const progress = document.getElementById('progress')
const peers = document.getElementById('peers')
const speed = document.getElementById('speed')
const downloaded = document.getElementById('downloaded')
if (status) status.textContent = `Loading ${torrent.name}...`
const file = torrent.files.find(file =>
file.name.endsWith('.mp4') ||
file.name.endsWith('.webm') ||
file.name.endsWith('.mkv')
)
if (!file) {
const error = new Error('No compatible video file found in torrent')
this.log(error.message)
if (status) status.textContent = 'Error: No video file found'
reject(error)
return
}
videoElement.muted = true
videoElement.crossOrigin = 'anonymous'
videoElement.addEventListener('error', (e) => {
const error = e.target.error
this.log('Video error:', error)
if (error) {
this.log('Error code:', error.code)
this.log('Error message:', error.message)
}
if (status) status.textContent = 'Error playing video. Try disabling Brave Shields.'
})
videoElement.addEventListener('canplay', () => {
const playPromise = videoElement.play()
if (playPromise !== undefined) {
playPromise
.then(() => this.log('Autoplay started'))
.catch(err => {
this.log('Autoplay failed:', err)
if (status) status.textContent = 'Click to play video'
videoElement.addEventListener('click', () => {
videoElement.play()
.then(() => this.log('Play started by user'))
.catch(err => this.log('Play failed:', err))
}, { once: true })
})
}
})
videoElement.addEventListener('loadedmetadata', () => {
this.log('Video metadata loaded')
if (videoElement.duration === Infinity || isNaN(videoElement.duration)) {
this.log('Invalid duration, attempting to fix...')
videoElement.currentTime = 1e101
videoElement.currentTime = 0
}
})
try {
file.streamTo(videoElement)
this.log('Streaming started')
// Update stats every second
this.statsInterval = setInterval(() => {
if (!document.body.contains(videoElement)) {
clearInterval(this.statsInterval)
return
}
const percentage = torrent.progress * 100
if (progress) progress.style.width = `${percentage}%`
if (peers) peers.textContent = `Peers: ${torrent.numPeers}`
if (speed) speed.textContent = `${this.formatBytes(torrent.downloadSpeed)}/s`
if (downloaded) downloaded.textContent =
`${this.formatBytes(torrent.downloaded)} / ${this.formatBytes(torrent.length)}`
if (status) {
status.textContent = torrent.progress === 1
? `${torrent.name}`
: `Loading ${torrent.name}...`
}
}, 1000)
this.currentTorrent = torrent
resolve()
} catch (error) {
this.log('Streaming error:', error)
if (status) status.textContent = 'Error starting video stream'
reject(error)
}
torrent.on('error', err => {
this.log('Torrent error:', err)
if (status) status.textContent = 'Error loading video'
clearInterval(this.statsInterval)
reject(err)
})
})
})
} catch (error) {
this.log('Failed to setup video streaming:', error)
throw error
}
}
async cleanup() {
try {
if (this.statsInterval) {
clearInterval(this.statsInterval)
}
if (this.currentTorrent) {
this.currentTorrent.destroy()
}
if (this.client) {
await this.client.destroy()
this.client = new WebTorrent() // Create a new client for future use
}
} catch (error) {
this.log('Cleanup error:', error)
}
}
}
export const torrentClient = new TorrentClient()

View File

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

View File

@@ -1,41 +0,0 @@
{
"name": "bitvid - Decentralized Video Sharing",
"short_name": "bitvid",
"description": "seed. zap. subscribe.",
"icons": [
{
"src": "/android-chrome-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/android-chrome-512x512.png",
"sizes": "512x512",
"type": "image/png"
},
{
"src": "/apple-touch-icon.png",
"sizes": "180x180",
"type": "image/png"
},
{
"src": "assets/png/favicon-32x32.png",
"sizes": "32x32",
"type": "image/png"
},
{
"src": "assets/png/favicon-16x16.png",
"sizes": "16x16",
"type": "image/png"
}
],
"start_url": "/index.html",
"display": "standalone",
"background_color": "#0f172a",
"theme_color": "#0f172a",
"orientation": "portrait-primary",
"scope": "/",
"categories": ["video", "entertainment", "decentralized", "streaming"],
"related_applications": [],
"lang": "en"
}

132
refactoring/sw.min.js vendored
View File

@@ -1,132 +0,0 @@
(() => {
"use strict";
let cancelled = false;
// Handle skip waiting message
self.addEventListener('message', event => {
if (event.data && event.data.type === 'SKIP_WAITING') {
self.skipWaiting()
}
})
// Immediately install and activate
self.addEventListener("install", () => {
self.skipWaiting()
})
// Claim clients on activation
self.addEventListener('activate', event => {
event.waitUntil(
Promise.all([
clients.claim(),
self.skipWaiting(),
caches.keys().then(cacheNames =>
Promise.all(cacheNames.map(cacheName => caches.delete(cacheName)))
)
])
)
})
// Handle fetch events
self.addEventListener("fetch", s => {
const t = (s => {
const { url: t } = s.request;
// Only handle webtorrent requests
if (!t.includes(self.registration.scope + "webtorrent/")) {
return null;
}
// Handle keepalive requests
if (t.includes(self.registration.scope + "webtorrent/keepalive/")) {
return new Response();
}
// Handle cancel requests
if (t.includes(self.registration.scope + "webtorrent/cancel/")) {
return new Response(new ReadableStream({
cancel() {
cancelled = true;
}
}));
}
// Handle streaming requests
return async function({ request: s }) {
const { url: t, method: n, headers: o, destination: a } = s;
// Get all window clients
const l = await clients.matchAll({
type: "window",
includeUncontrolled: true
});
// Create message channel and wait for response
const [r, i] = await new Promise(e => {
for (const s of l) {
const l = new MessageChannel,
{ port1: r, port2: i } = l;
r.onmessage = ({ data: s }) => {
e([s, r])
};
s.postMessage({
url: t,
method: n,
headers: Object.fromEntries(o.entries()),
scope: self.registration.scope,
destination: a,
type: "webtorrent"
}, [i]);
}
});
let c = null;
const d = () => {
i.postMessage(false);
clearTimeout(c);
i.onmessage = null;
};
// Handle non-streaming response
if (r.body !== "STREAM") {
d();
return new Response(r.body, r);
}
// Handle streaming response
return new Response(new ReadableStream({
pull: s => new Promise(t => {
i.onmessage = ({ data: e }) => {
if (e) {
s.enqueue(e);
} else {
d();
s.close();
}
t();
};
if (!cancelled && a !== "document") {
clearTimeout(c);
c = setTimeout(() => {
d();
t();
}, 5000);
}
i.postMessage(true);
}),
cancel() {
d();
}
}), r);
}(s);
})(s);
if (t) {
s.respondWith(t);
}
});
})();