diff --git a/src/assets/favicon.ico b/assets/favicon.ico similarity index 100% rename from src/assets/favicon.ico rename to assets/favicon.ico diff --git a/src/assets/gif/please-stand-by.gif b/assets/gif/please-stand-by.gif similarity index 100% rename from src/assets/gif/please-stand-by.gif rename to assets/gif/please-stand-by.gif diff --git a/src/assets/jpg/bitvid.jpg b/assets/jpg/bitvid.jpg similarity index 100% rename from src/assets/jpg/bitvid.jpg rename to assets/jpg/bitvid.jpg diff --git a/src/assets/jpg/default-profile.jpg b/assets/jpg/default-profile.jpg similarity index 100% rename from src/assets/jpg/default-profile.jpg rename to assets/jpg/default-profile.jpg diff --git a/src/assets/jpg/video-thumbnail-fallback.jpg b/assets/jpg/video-thumbnail-fallback.jpg similarity index 100% rename from src/assets/jpg/video-thumbnail-fallback.jpg rename to assets/jpg/video-thumbnail-fallback.jpg diff --git a/src/assets/png/android-chrome-192x192.png b/assets/png/android-chrome-192x192.png similarity index 100% rename from src/assets/png/android-chrome-192x192.png rename to assets/png/android-chrome-192x192.png diff --git a/src/assets/png/android-chrome-512x512.png b/assets/png/android-chrome-512x512.png similarity index 100% rename from src/assets/png/android-chrome-512x512.png rename to assets/png/android-chrome-512x512.png diff --git a/src/assets/png/apple-touch-icon.png b/assets/png/apple-touch-icon.png similarity index 100% rename from src/assets/png/apple-touch-icon.png rename to assets/png/apple-touch-icon.png diff --git a/src/assets/png/bitvid-banner.png b/assets/png/bitvid-banner.png similarity index 100% rename from src/assets/png/bitvid-banner.png rename to assets/png/bitvid-banner.png diff --git a/src/assets/png/favicon-16x16.png b/assets/png/favicon-16x16.png similarity index 100% rename from src/assets/png/favicon-16x16.png rename to assets/png/favicon-16x16.png diff --git a/src/assets/png/favicon-32x32.png b/assets/png/favicon-32x32.png similarity index 100% rename from src/assets/png/favicon-32x32.png rename to assets/png/favicon-32x32.png diff --git a/assets/svg/about-icon.svg b/assets/svg/about-icon.svg new file mode 100644 index 0000000..855d3a2 --- /dev/null +++ b/assets/svg/about-icon.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/svg/beacon-icon.svg b/assets/svg/beacon-icon.svg new file mode 100644 index 0000000..5f8f37e --- /dev/null +++ b/assets/svg/beacon-icon.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/assets/svg/beta-icon.svg b/assets/svg/beta-icon.svg new file mode 100644 index 0000000..1fe3453 --- /dev/null +++ b/assets/svg/beta-icon.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/assets/svg/bitvid-logo-dark-mode.svg b/assets/svg/bitvid-logo-dark-mode.svg similarity index 100% rename from src/assets/svg/bitvid-logo-dark-mode.svg rename to assets/svg/bitvid-logo-dark-mode.svg diff --git a/src/assets/svg/bitvid-logo-light-mode.svg b/assets/svg/bitvid-logo-light-mode.svg similarity index 100% rename from src/assets/svg/bitvid-logo-light-mode.svg rename to assets/svg/bitvid-logo-light-mode.svg diff --git a/assets/svg/blog-icon.svg b/assets/svg/blog-icon.svg new file mode 100644 index 0000000..f415d7b --- /dev/null +++ b/assets/svg/blog-icon.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/assets/svg/copy-magnet.svg b/assets/svg/copy-magnet.svg similarity index 100% rename from src/assets/svg/copy-magnet.svg rename to assets/svg/copy-magnet.svg diff --git a/assets/svg/default-profile.svg b/assets/svg/default-profile.svg new file mode 100644 index 0000000..b31b553 --- /dev/null +++ b/assets/svg/default-profile.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/svg/dns-icon.svg b/assets/svg/dns-icon.svg new file mode 100644 index 0000000..6bedf6f --- /dev/null +++ b/assets/svg/dns-icon.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/svg/explore-icon.svg b/assets/svg/explore-icon.svg new file mode 100644 index 0000000..2d80429 --- /dev/null +++ b/assets/svg/explore-icon.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/svg/getting-started-icon.svg b/assets/svg/getting-started-icon.svg new file mode 100644 index 0000000..dcece7e --- /dev/null +++ b/assets/svg/getting-started-icon.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/svg/github-icon.svg b/assets/svg/github-icon.svg new file mode 100644 index 0000000..ffb9088 --- /dev/null +++ b/assets/svg/github-icon.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/svg/guidelines-icon.svg b/assets/svg/guidelines-icon.svg new file mode 100644 index 0000000..9933f81 --- /dev/null +++ b/assets/svg/guidelines-icon.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/svg/home-icon.svg b/assets/svg/home-icon.svg new file mode 100644 index 0000000..2c7c1df --- /dev/null +++ b/assets/svg/home-icon.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/svg/ipns-icon.svg b/assets/svg/ipns-icon.svg new file mode 100644 index 0000000..c6fe78a --- /dev/null +++ b/assets/svg/ipns-icon.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/svg/links-icon.svg b/assets/svg/links-icon.svg new file mode 100644 index 0000000..477a431 --- /dev/null +++ b/assets/svg/links-icon.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/svg/mobile-sidebar-menu-icon.svg b/assets/svg/mobile-sidebar-menu-icon.svg new file mode 100644 index 0000000..fa9a60c --- /dev/null +++ b/assets/svg/mobile-sidebar-menu-icon.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/svg/nostr-icon.svg b/assets/svg/nostr-icon.svg new file mode 100644 index 0000000..e32ea7b --- /dev/null +++ b/assets/svg/nostr-icon.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/assets/svg/roadmap-icon.svg b/assets/svg/roadmap-icon.svg new file mode 100644 index 0000000..6bd0948 --- /dev/null +++ b/assets/svg/roadmap-icon.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/assets/svg/share-video.svg b/assets/svg/share-video.svg similarity index 100% rename from src/assets/svg/share-video.svg rename to assets/svg/share-video.svg diff --git a/assets/svg/subscriptions-icon.svg b/assets/svg/subscriptions-icon.svg new file mode 100644 index 0000000..46ff49d --- /dev/null +++ b/assets/svg/subscriptions-icon.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/assets/svg/video-settings-gear.svg b/assets/svg/video-settings-gear.svg similarity index 100% rename from src/assets/svg/video-settings-gear.svg rename to assets/svg/video-settings-gear.svg diff --git a/bitvid_logo/beacon-icon.svg b/bitvid_logo/beacon-icon.svg new file mode 100644 index 0000000..5f8f37e --- /dev/null +++ b/bitvid_logo/beacon-icon.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/bitvid_logo/dns-icon.svg b/bitvid_logo/dns-icon.svg new file mode 100644 index 0000000..6bedf6f --- /dev/null +++ b/bitvid_logo/dns-icon.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/bitvid_logo/guidelines-icon.svg b/bitvid_logo/guidelines-icon.svg new file mode 100644 index 0000000..9933f81 --- /dev/null +++ b/bitvid_logo/guidelines-icon.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/bitvid_logo/home-icon.svg b/bitvid_logo/home-icon.svg new file mode 100644 index 0000000..2c7c1df --- /dev/null +++ b/bitvid_logo/home-icon.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/bitvid_logo/ipns-icon.svg b/bitvid_logo/ipns-icon.svg new file mode 100644 index 0000000..c6fe78a --- /dev/null +++ b/bitvid_logo/ipns-icon.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/blog.html b/blog.html new file mode 100644 index 0000000..bd41b46 --- /dev/null +++ b/blog.html @@ -0,0 +1,30444 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + diff --git a/src/components/application-form.html b/components/application-form.html similarity index 100% rename from src/components/application-form.html rename to components/application-form.html diff --git a/src/components/bug-fix-form.html b/components/bug-fix-form.html similarity index 100% rename from src/components/bug-fix-form.html rename to components/bug-fix-form.html diff --git a/src/components/content-appeals-form.html b/components/content-appeals-form.html similarity index 100% rename from src/components/content-appeals-form.html rename to components/content-appeals-form.html diff --git a/components/disclaimer.html b/components/disclaimer.html new file mode 100644 index 0000000..a190d9c --- /dev/null +++ b/components/disclaimer.html @@ -0,0 +1,151 @@ + + diff --git a/src/components/feature-request-form.html b/components/feature-request-form.html similarity index 100% rename from src/components/feature-request-form.html rename to components/feature-request-form.html diff --git a/src/components/general-feedback-form.html b/components/general-feedback-form.html similarity index 100% rename from src/components/general-feedback-form.html rename to components/general-feedback-form.html diff --git a/src/components/iframe_forms/iframe-application-form.html b/components/iframe_forms/iframe-application-form.html similarity index 100% rename from src/components/iframe_forms/iframe-application-form.html rename to components/iframe_forms/iframe-application-form.html diff --git a/src/components/iframe_forms/iframe-bug-fix-form.html b/components/iframe_forms/iframe-bug-fix-form.html similarity index 100% rename from src/components/iframe_forms/iframe-bug-fix-form.html rename to components/iframe_forms/iframe-bug-fix-form.html diff --git a/src/components/iframe_forms/iframe-content-appeals-form.html b/components/iframe_forms/iframe-content-appeals-form.html similarity index 100% rename from src/components/iframe_forms/iframe-content-appeals-form.html rename to components/iframe_forms/iframe-content-appeals-form.html diff --git a/src/components/iframe_forms/iframe-feedback-form.html b/components/iframe_forms/iframe-feedback-form.html similarity index 100% rename from src/components/iframe_forms/iframe-feedback-form.html rename to components/iframe_forms/iframe-feedback-form.html diff --git a/src/components/iframe_forms/iframe-request-form.html b/components/iframe_forms/iframe-request-form.html similarity index 100% rename from src/components/iframe_forms/iframe-request-form.html rename to components/iframe_forms/iframe-request-form.html diff --git a/src/components/login-modal.html b/components/login-modal.html similarity index 100% rename from src/components/login-modal.html rename to components/login-modal.html diff --git a/src/components/profile-modal.html b/components/profile-modal.html similarity index 98% rename from src/components/profile-modal.html rename to components/profile-modal.html index c453d32..1b9c5f9 100644 --- a/src/components/profile-modal.html +++ b/components/profile-modal.html @@ -54,7 +54,7 @@
Profile diff --git a/components/sidebar.html b/components/sidebar.html new file mode 100644 index 0000000..9756ccf --- /dev/null +++ b/components/sidebar.html @@ -0,0 +1,248 @@ + diff --git a/src/components/upload-modal.html b/components/upload-modal.html similarity index 100% rename from src/components/upload-modal.html rename to components/upload-modal.html diff --git a/src/components/video-modal.html b/components/video-modal.html similarity index 100% rename from src/components/video-modal.html rename to components/video-modal.html diff --git a/src/content/about.md b/content/about.md similarity index 95% rename from src/content/about.md rename to content/about.md index fcae05c..d61f257 100644 --- a/src/content/about.md +++ b/content/about.md @@ -1,7 +1,3 @@ -![](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? diff --git a/src/content/community-guidelines.md b/content/community-guidelines.md similarity index 99% rename from src/content/community-guidelines.md rename to content/community-guidelines.md index 7b3a771..adc097f 100644 --- a/src/content/community-guidelines.md +++ b/content/community-guidelines.md @@ -1,5 +1,3 @@ -# **bitvid Community Guidelines** - Welcome to **bitvid**, a decentralized video-sharing platform built on Nostr. These Community Guidelines outline the types of content allowed and prohibited on the platform. As bitvid is still in early access, enforcement will occur at the client level, meaning violations will result in content being blocked from display rather than removed from relays. These policies will evolve as we implement robust user blocking and reporting features. ## **1. Content Principles** diff --git a/src/content/getting-started.md b/content/getting-started.md similarity index 98% rename from src/content/getting-started.md rename to content/getting-started.md index 8104f9c..88e2c88 100644 --- a/src/content/getting-started.md +++ b/content/getting-started.md @@ -1,5 +1,3 @@ -# 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 diff --git a/src/content/ipns.md b/content/ipns.md similarity index 100% rename from src/content/ipns.md rename to content/ipns.md diff --git a/content/links.md b/content/links.md new file mode 100644 index 0000000..6d815b7 --- /dev/null +++ b/content/links.md @@ -0,0 +1,139 @@ +Here is a collection of links to forms and pages. + +--- + +## Forms + +--- + +### Application Form + +Main - [bitvid.network?modal=application](https://bitvid.network?modal=application) + +Beta - [beta.bitvid.network?modal=application](https://beta.bitvid.network?modal=application) + +--- + +### Bug Fix Form + +Main - [bitvid.network?modal=bug](https://bitvid.network?modal=bug) + +Beta - [beta.bitvid.network?modal=bug](https://beta.bitvid.network?modal=bug) + +--- + +### Content Appeals Form + +Main - [bitvid.network?modal=appeals](https://bitvid.network?modal=appeals) + +Beta - [beta.bitvid.network?modal=appeals](https://beta.bitvid.network?modal=appeals) + +--- + +### Feature Request Form + +Main - [bitvid.network?modal=feature](https://bitvid.network?modal=feature) + +Beta - [beta.bitvid.network?modal=feature](https://beta.bitvid.network?modal=feature) + +--- + +### General Feedback Form + +Main - [bitvid.network?modal=feedback](https://bitvid.network?modal=feedback) + +Beta - [beta.bitvid.network?modal=feedback](https://beta.bitvid.network?modal=feedback) + +--- + +## Views + +--- + +### Home + +Main - [bitvid.network#view=most-recent-videos](https://bitvid.network#view=most-recent-videos) + +Beta - [beta.bitvid.network#view=most-recent-videos](https://beta.bitvid.network#view=most-recent-videos) + +--- + +### Explore + +Main - [bitvid.network#view=explore](https://bitvid.network#view=explore) + +Beta - [beta.bitvid.network#view=explore](https://beta.bitvid.network#view=explore) + +--- + +### Subscriptions + +_Requires Nostr login_ + +Main - [bitvid.network#view=subscriptions](https://bitvid.network#view=subscriptions) + +Beta - [beta.bitvid.network#view=subscriptions](https://beta.bitvid.network#view=subscriptions) + +--- + +### About + +Main - [bitvid.network#view=about](https://bitvid.network#view=about) + +Beta - [beta.bitvid.network#view=about](https://beta.bitvid.network#view=about) + +--- + +### Guidelines + +Main - [bitvid.network#view=community-guidelines](https://bitvid.network#view=community-guidelines) + +Beta - [beta.bitvid.network#view=community-guidelines](https://beta.bitvid.network#view=community-guidelines) + +--- + +### Getting Started + +Main - [bitvid.network#view=getting-started](https://bitvid.network#view=getting-started) + +Beta - [beta.bitvid.network#view=getting-started](https://beta.bitvid.network#view=getting-started) + +--- + +### Roadmap + +Main - [bitvid.network#view=roadmap](https://bitvid.network#view=roadmap) + +Beta - [beta.bitvid.network#view=roadmap](https://beta.bitvid.network#view=roadmap) + +--- + +### βeacon + +Main - [bitvid.network/torrent/beacon.html](https://bitvid.network/torrent/beacon.html) + +Beta - [beta.bitvid.network/torrent/beacon.html](https://beta.bitvid.network/torrent/beacon.html) + +--- + +### Beta + +Main - [bitvid.network](https://bitvid.network) + +Beta - [beta.bitvid.network](https://beta.bitvid.network) + +--- + +### Links + +Main - [bitvid.network#view=links](https://bitvid.network#view=links) + +Beta - [beta.bitvid.network#view=links](https://beta.bitvid.network#view=links) + +--- + +### IPNS + +Main - [bitvid.network#view=ipns](https://bitvid.network#view=ipns) + +Beta - [beta.bitvid.network#view=ipns](https://beta.bitvid.network#view=ipns) diff --git a/src/content/roadmap.md b/content/roadmap.md similarity index 87% rename from src/content/roadmap.md rename to content/roadmap.md index 824c7a2..13cbe8a 100644 --- a/src/content/roadmap.md +++ b/content/roadmap.md @@ -1,5 +1,3 @@ -# Roadmap and Bug List - ## UI Enhancements - Add a copy Magnet button labeled "Seed". @@ -13,10 +11,6 @@ ## 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 diff --git a/src/content/roadmap/02_bitvid_Enhanced_Migration_of_Note_Spec_Logic.md b/content/roadmap/02_bitvid_Enhanced_Migration_of_Note_Spec_Logic.md similarity index 100% rename from src/content/roadmap/02_bitvid_Enhanced_Migration_of_Note_Spec_Logic.md rename to content/roadmap/02_bitvid_Enhanced_Migration_of_Note_Spec_Logic.md diff --git a/src/content/roadmap/03_bitvid_Enhanced_Nostr_Video_&_Audio_Note_Specification_Version 3.md b/content/roadmap/03_bitvid_Enhanced_Nostr_Video_&_Audio_Note_Specification_Version 3.md similarity index 100% rename from src/content/roadmap/03_bitvid_Enhanced_Nostr_Video_&_Audio_Note_Specification_Version 3.md rename to content/roadmap/03_bitvid_Enhanced_Nostr_Video_&_Audio_Note_Specification_Version 3.md diff --git a/src/content/roadmap/04_bitvid_Enhanced_Profile_Channel_Views_Specification.md b/content/roadmap/04_bitvid_Enhanced_Profile_Channel_Views_Specification.md similarity index 100% rename from src/content/roadmap/04_bitvid_Enhanced_Profile_Channel_Views_Specification.md rename to content/roadmap/04_bitvid_Enhanced_Profile_Channel_Views_Specification.md diff --git a/src/content/roadmap/05_bitvid_Enhanced_Block_Subscription_&_Reporting_Specification.md b/content/roadmap/05_bitvid_Enhanced_Block_Subscription_&_Reporting_Specification.md similarity index 100% rename from src/content/roadmap/05_bitvid_Enhanced_Block_Subscription_&_Reporting_Specification.md rename to content/roadmap/05_bitvid_Enhanced_Block_Subscription_&_Reporting_Specification.md diff --git a/src/content/roadmap/06_bitvid_Enhanced_Video_Comment_System_Specification.md b/content/roadmap/06_bitvid_Enhanced_Video_Comment_System_Specification.md similarity index 100% rename from src/content/roadmap/06_bitvid_Enhanced_Video_Comment_System_Specification.md rename to content/roadmap/06_bitvid_Enhanced_Video_Comment_System_Specification.md diff --git a/src/content/roadmap/07_bitvid_Enhanced_View_Rating_&_Retention_Penalty_Scoring.md b/content/roadmap/07_bitvid_Enhanced_View_Rating_&_Retention_Penalty_Scoring.md similarity index 100% rename from src/content/roadmap/07_bitvid_Enhanced_View_Rating_&_Retention_Penalty_Scoring.md rename to content/roadmap/07_bitvid_Enhanced_View_Rating_&_Retention_Penalty_Scoring.md diff --git a/src/content/roadmap/08_bitvid_Enhanced_Dynamic_Home_Page_&_Video_Tracking_Specification.md b/content/roadmap/08_bitvid_Enhanced_Dynamic_Home_Page_&_Video_Tracking_Specification.md similarity index 100% rename from src/content/roadmap/08_bitvid_Enhanced_Dynamic_Home_Page_&_Video_Tracking_Specification.md rename to content/roadmap/08_bitvid_Enhanced_Dynamic_Home_Page_&_Video_Tracking_Specification.md diff --git a/src/content/roadmap/09_bitvid_Enhanced_NIP-35_+_WebRTC_Check_Integration.md b/content/roadmap/09_bitvid_Enhanced_NIP-35_+_WebRTC_Check_Integration.md similarity index 100% rename from src/content/roadmap/09_bitvid_Enhanced_NIP-35_+_WebRTC_Check_Integration.md rename to content/roadmap/09_bitvid_Enhanced_NIP-35_+_WebRTC_Check_Integration.md diff --git a/src/css/markdown.css b/css/markdown.css similarity index 100% rename from src/css/markdown.css rename to css/markdown.css diff --git a/src/css/style.css b/css/style.css similarity index 87% rename from src/css/style.css rename to css/style.css index c8445d4..e8bec95 100644 --- a/src/css/style.css +++ b/css/style.css @@ -16,6 +16,9 @@ body { background-color: var(--color-bg); color: var(--color-text); line-height: 1.5; + margin: 0; + padding: 0; + overflow-x: hidden; /* Disable horizontal scrolling */ } header { @@ -470,3 +473,56 @@ footer a:hover { height: 100%; object-fit: cover; } + +/* Sidebar default states */ +#sidebar { + position: fixed; + top: 0; + left: 0; + width: 16rem; /* Tailwind's w-64 */ + height: 100vh; + background-color: #0f172a; + overflow-y: auto; + -webkit-overflow-scrolling: touch; + transition: transform 0.3s ease; +} + +/* Mobile (max-width: 767px): Hide sidebar by default */ +@media (max-width: 767px) { + #sidebar { + transform: translateX(-100%); + } + /* When the sidebar-open class is added, slide the sidebar in */ + #sidebar.sidebar-open { + transform: translateX(0); + } + + /* Optionally shift main content when sidebar is open */ + #app.sidebar-open { + transform: translateX(16rem); + transition: transform 0.3s ease; + } +} + +/* Desktop (min-width: 768px): Always show the sidebar */ +@media (min-width: 768px) { + #sidebar { + transform: translateX(0) !important; + } +} + +/* Collapsed/expanded classes if needed on desktop */ +.sidebar-collapsed { + width: 4rem; +} +.sidebar-expanded { + width: 16rem; +} +.sidebar-collapsed .sidebar-text { + display: none; +} + +/* Example: customizing the border & background in the sidebar */ +#sidebar hr { + border-color: rgba(255, 255, 255, 0.1); +} diff --git a/src/css/tailwind.min.css b/css/tailwind.min.css similarity index 100% rename from src/css/tailwind.min.css rename to css/tailwind.min.css diff --git a/index.html b/index.html new file mode 100644 index 0000000..e707cdd --- /dev/null +++ b/index.html @@ -0,0 +1,168 @@ + + + + + + bitvid | Decentralized Video Sharing + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + +
+ +
+ + + + + BitVid Logo + + +
+ + + + + + + + +
+
+ + + + + + + + +
+ + +
+ + +
+

+ seed. zap. subscribe. +

+
+
+ + + + + + + + + + + + + + + + + diff --git a/src/js/accessControl.js b/js/accessControl.js similarity index 100% rename from src/js/accessControl.js rename to js/accessControl.js diff --git a/src/js/app.js b/js/app.js similarity index 83% rename from src/js/app.js rename to js/app.js index 4891f11..e40a32f 100644 --- a/src/js/app.js +++ b/js/app.js @@ -4,7 +4,7 @@ import { loadView } from "./viewManager.js"; import { nostrClient } from "./nostr.js"; import { torrentClient } from "./webtorrent.js"; import { isDevMode } from "./config.js"; -import { disclaimerModal } from "./disclaimer.js"; +import disclaimerModal from "./disclaimer.js"; import { initialBlacklist, initialEventBlacklist } from "./lists.js"; /** @@ -14,6 +14,48 @@ function fakeDecrypt(str) { return str.split("").reverse().join(""); } +/** + * Simple IntersectionObserver-based lazy loader for images (or videos). + * + * Usage: + * const mediaLoader = new MediaLoader(); + * mediaLoader.observe(imgElement); + * + * This will load the real image source from `imgElement.dataset.lazy` + * once the image enters the viewport. + */ +class MediaLoader { + constructor(rootMargin = "50px") { + this.observer = new IntersectionObserver( + (entries) => { + for (const entry of entries) { + if (entry.isIntersecting) { + const el = entry.target; + const lazySrc = el.dataset.lazy; + if (lazySrc) { + el.src = lazySrc; + delete el.dataset.lazy; + } + // Stop observing once loaded + this.observer.unobserve(el); + } + } + }, + { rootMargin } + ); + } + + observe(el) { + if (el.dataset.lazy) { + this.observer.observe(el); + } + } + + disconnect() { + this.observer.disconnect(); + } +} + class bitvidApp { constructor() { // Basic auth/display elements @@ -22,6 +64,9 @@ class bitvidApp { this.userStatus = document.getElementById("userStatus") || null; this.userPubKey = document.getElementById("userPubKey") || null; + // Lazy-loading helper for images + this.mediaLoader = new MediaLoader(); + // Optional: a "profile" button or avatar (if used) this.profileButton = document.getElementById("profileButton") || null; this.profileAvatar = document.getElementById("profileAvatar") || null; @@ -65,6 +110,9 @@ class bitvidApp { this.copyMagnetBtn = null; this.shareBtn = null; + // Hide/Show Subscriptions Link + this.subscriptionsLink = null; + // Notification containers this.errorContainer = document.getElementById("errorContainer") || null; this.successContainer = document.getElementById("successContainer") || null; @@ -140,18 +188,38 @@ class bitvidApp { // 4. Connect to Nostr await nostrClient.init(); + + // Grab the "Subscriptions" link by its id in the sidebar + this.subscriptionsLink = document.getElementById("subscriptionsLink"); + const savedPubKey = localStorage.getItem("userPubKey"); if (savedPubKey) { // Auto-login if a pubkey was saved this.login(savedPubKey, false); + + // If the user was already logged in, show the Subscriptions link + if (this.subscriptionsLink) { + this.subscriptionsLink.classList.remove("hidden"); + } } // 5. Setup general event listeners, show disclaimers this.setupEventListeners(); disclaimerModal.show(); - // 6. Load the default view (most-recent-videos.html) - await loadView("views/most-recent-videos.html"); + // 6) Load the default view ONLY if there's no #view= already + if (!window.location.hash || !window.location.hash.startsWith("#view=")) { + console.log( + "[app.init()] No #view= in the URL, loading default home view" + ); + await loadView("views/most-recent-videos.html"); + } else { + console.log( + "[app.init()] Found hash:", + window.location.hash, + "so skipping default load" + ); + } // 7. Once loaded, get a reference to #videoList this.videoList = document.getElementById("videoList"); @@ -478,22 +546,36 @@ class bitvidApp { await this.cleanup(); }); - // 8) Handle back/forward nav => hide video modal + // 8) Handle back/forward navigation => hide video modal window.addEventListener("popstate", async () => { console.log("[popstate] user navigated back/forward; cleaning modal..."); await this.hideModal(); }); - // Event delegation for the “Application Form” button inside the login modal + // 9) Event delegation on the video list container for playing videos + if (this.videoList) { + this.videoList.addEventListener("click", (event) => { + const magnetTrigger = event.target.closest("[data-play-magnet]"); + if (magnetTrigger) { + // For a normal left-click (button 0, no Ctrl/Cmd), prevent navigation: + if (event.button === 0 && !event.ctrlKey && !event.metaKey) { + event.preventDefault(); // Stop browser from following the href + const magnet = magnetTrigger.dataset.playMagnet; + this.playVideo(magnet); + } + } + }); + } + + // 10) Event delegation for the “Application Form” button inside the login modal document.addEventListener("click", (event) => { if (event.target && event.target.id === "openApplicationModal") { - // 1) Hide the login modal + // Hide the login modal const loginModal = document.getElementById("loginModal"); if (loginModal) { loginModal.classList.add("hidden"); } - - // 2) Show the application modal + // Show the application modal const appModal = document.getElementById("nostrFormModal"); if (appModal) { appModal.classList.remove("hidden"); @@ -511,12 +593,12 @@ class bitvidApp { { kinds: [0], authors: [pubkey], limit: 1 }, ]); let displayName = "User"; - let picture = "assets/jpg/default-profile.jpg"; + let picture = "assets/svg/default-profile.svg"; if (events.length && events[0].content) { const data = JSON.parse(events[0].content); displayName = data.name || data.display_name || "User"; - picture = data.picture || "assets/jpg/default-profile.jpg"; + picture = data.picture || "assets/svg/default-profile.svg"; } // If you have a top-bar avatar (profileAvatar) @@ -535,6 +617,56 @@ class bitvidApp { } } + async fetchAndRenderProfile(pubkey, forceRefresh = false) { + const now = Date.now(); + + // 1) Check if we have a cached entry + const cacheEntry = this.profileCache.get(pubkey); + if (!forceRefresh && cacheEntry && now - cacheEntry.timestamp < 60000) { + // If it's less than 60 seconds old, just update DOM with it + this.updateProfileInDOM(pubkey, cacheEntry.profile); + return; + } + + // 2) Otherwise, fetch from Nostr + try { + const userEvents = await nostrClient.pool.list(nostrClient.relays, [ + { kinds: [0], authors: [pubkey], limit: 1 }, + ]); + if (userEvents.length > 0 && userEvents[0].content) { + const data = JSON.parse(userEvents[0].content); + const profile = { + name: data.name || data.display_name || "Unknown", + picture: data.picture || "assets/svg/default-profile.svg", + }; + + // Cache it + this.profileCache.set(pubkey, { profile, timestamp: now }); + // Update DOM + this.updateProfileInDOM(pubkey, profile); + } + } catch (err) { + console.error("Profile fetch error:", err); + } + } + + updateProfileInDOM(pubkey, profile) { + // For any .author-pic[data-pubkey=...] + const picEls = document.querySelectorAll( + `.author-pic[data-pubkey="${pubkey}"]` + ); + picEls.forEach((el) => { + el.src = profile.picture; + }); + // For any .author-name[data-pubkey=...] + const nameEls = document.querySelectorAll( + `.author-name[data-pubkey="${pubkey}"]` + ); + nameEls.forEach((el) => { + el.textContent = profile.name; + }); + } + /** * Actually handle the upload form submission. */ @@ -617,6 +749,11 @@ class bitvidApp { this.profileButton.classList.remove("hidden"); } + // Show the "Subscriptions" link if it exists + if (this.subscriptionsLink) { + this.subscriptionsLink.classList.remove("hidden"); + } + // (Optional) load the user's own Nostr profile this.loadOwnProfile(pubkey); @@ -663,6 +800,11 @@ class bitvidApp { this.profileButton.classList.add("hidden"); } + // Hide the Subscriptions link + if (this.subscriptionsLink) { + this.subscriptionsLink.classList.add("hidden"); + } + // Clear localStorage localStorage.removeItem("userPubKey"); @@ -701,35 +843,47 @@ class bitvidApp { * Hide the video modal. */ async hideModal() { - // 1) Clear intervals + // 1) Clear intervals, cleanup, etc. (unchanged) if (this.activeIntervals && this.activeIntervals.length) { this.activeIntervals.forEach((id) => clearInterval(id)); this.activeIntervals = []; } - // 2) Cleanup resources (this stops the torrent, etc.) + try { + await fetch("/webtorrent/cancel/", { mode: "no-cors" }); + } catch (err) { + // ignore + } await this.cleanup(); - // 3) Hide the modal + // 2) Hide the modal if (this.playerModal) { this.playerModal.style.display = "none"; this.playerModal.classList.add("hidden"); } this.currentMagnetUri = null; - // 4) Revert ?v= param in the URL - window.history.replaceState({}, "", window.location.pathname); + // 3) Remove only `?v=` but **keep** the hash + const url = new URL(window.location.href); + url.searchParams.delete("v"); // remove ?v= param + const newUrl = url.pathname + url.search + url.hash; + window.history.replaceState({}, "", newUrl); } /** * Subscribe to videos (older + new) and render them as they come in. */ - async loadVideos() { - console.log("Starting loadVideos..."); + async loadVideos(forceFetch = false) { + console.log("Starting loadVideos... (forceFetch =", forceFetch, ")"); - // We do NOT decode initialEventBlacklist here. - // That happens once in the constructor, creating this.blacklistedEventIds. + // If forceFetch is true, unsubscribe from the old subscription to start fresh + if (forceFetch && this.videoSubscription) { + // Call unsubscribe on the subscription object directly. + this.videoSubscription.unsub(); + this.videoSubscription = null; + } + // The rest of your existing logic: if (!this.videoSubscription) { if (this.videoList) { this.videoList.innerHTML = ` @@ -738,7 +892,7 @@ class bitvidApp {

`; } - // Create a single subscription + // Create a new subscription this.videoSubscription = nostrClient.subscribeVideos(() => { const updatedAll = nostrClient.getActiveVideos(); @@ -749,7 +903,7 @@ class bitvidApp { return false; } - // 2) Check author (if you’re also blacklisting authors by npub) + // 2) Check author if you’re blacklisting authors by npub const authorNpub = this.safeEncodeNpub(video.pubkey) || video.pubkey; if (initialBlacklist.includes(authorNpub)) { return false; @@ -760,6 +914,15 @@ class bitvidApp { this.renderVideoList(filteredVideos); }); + + // *** IMPORTANT ***: Unsubscribe once we get the historical EOSE + // so that we do not hold an open subscription forever: + if (this.videoSubscription) { + this.videoSubscription.on("eose", () => { + this.videoSubscription.unsub(); + console.log("[loadVideos] unsubscribed after EOSE"); + }); + } } else { // Already subscribed: just show what's cached const allCached = nostrClient.getActiveVideos(); @@ -794,10 +957,10 @@ class bitvidApp { return olderMatches.length > 0; } - // 4) Build the DOM for each video in newestActive async renderVideoList(videos) { if (!this.videoList) return; + // Check if there's anything to show if (!videos || videos.length === 0) { this.videoList.innerHTML = `

@@ -809,13 +972,14 @@ class bitvidApp { // Sort newest first videos.sort((a, b) => b.created_at - a.created_at); - // <-- NEW: Convert allEvents map => array to check older overshadowed events + // Convert allEvents to an array for checking older overshadowed events const fullAllEventsArray = Array.from(nostrClient.allEvents.values()); + const fragment = document.createDocumentFragment(); - const htmlList = videos.map((video, index) => { + videos.forEach((video, index) => { if (!video.id || !video.title) { console.error("Video missing ID/title:", video); - return ""; + return; } const nevent = window.NostrTools.nip19.neventEncode({ id: video.id }); @@ -829,32 +993,31 @@ class bitvidApp { : "border-none"; const timeAgo = this.formatTimeAgo(video.created_at); - // 1) Do we have an older version? + // Check if there's an older version (for revert button) let hasOlder = false; if (canEdit && video.videoRootId) { hasOlder = this.hasOlderVersion(video, fullAllEventsArray); } - // 2) If we do => show revert button const revertButton = hasOlder ? ` - - ` + + ` : ""; - // 3) Gear menu + // Gear menu (only shown if canEdit) const gearMenu = canEdit ? `

${revertButton} @@ -886,42 +1049,26 @@ class bitvidApp { ` : ""; - // 4) Build the card markup... + // Card markup const cardHtml = `
${this.escapeHTML(video.title)}

${this.escapeHTML(video.title)}

@@ -931,7 +1078,7 @@ class bitvidApp { Placeholder
@@ -953,132 +1100,83 @@ class bitvidApp {
`; - // Fire off a background fetch for the author's profile + // Turn the HTML into an element + const template = document.createElement("template"); + template.innerHTML = cardHtml.trim(); + const cardEl = template.content.firstElementChild; + + // Fetch the author's profile info in the background this.fetchAndRenderProfile(video.pubkey); - return cardHtml; + // Add the finished card to our fragment + fragment.appendChild(cardEl); }); - // Filter out any empty strings - const valid = htmlList.filter((x) => x.length > 0); - if (valid.length === 0) { - this.videoList.innerHTML = ` -

- No valid videos to display. -

`; - return; - } + // Clear the list and add our fragment + this.videoList.innerHTML = ""; + this.videoList.appendChild(fragment); - // Finally inject into DOM - this.videoList.innerHTML = valid.join(""); - } + // Lazy-load images + const lazyEls = this.videoList.querySelectorAll("[data-lazy]"); + lazyEls.forEach((el) => this.mediaLoader.observe(el)); - /** - * Retrieve the profile for a given pubkey (kind:0) and update the DOM. - */ - async fetchAndRenderProfile(pubkey, forceRefresh = false) { - const now = Date.now(); + // ------------------------------- + // Gear menu / button event listeners + // ------------------------------- - // Check if we already have a cached entry for this pubkey: - const cacheEntry = this.profileCache.get(pubkey); - - // If not forcing refresh, and we have a cache entry less than 60 sec old, use it: - if (!forceRefresh && cacheEntry && now - cacheEntry.timestamp < 60000) { - this.updateProfileInDOM(pubkey, cacheEntry.profile); - return; - } - - // Otherwise, go fetch from the relay - try { - const userEvents = await nostrClient.pool.list(nostrClient.relays, [ - { kinds: [0], authors: [pubkey], limit: 1 }, - ]); - if (userEvents.length > 0 && userEvents[0].content) { - const data = JSON.parse(userEvents[0].content); - const profile = { - name: data.name || data.display_name || "Unknown", - picture: data.picture || "assets/jpg/default-profile.jpg", - }; - - // Store into the cache with a timestamp - this.profileCache.set(pubkey, { - profile, - timestamp: now, - }); - - // Now update the DOM elements - this.updateProfileInDOM(pubkey, profile); - } - } catch (err) { - console.error("Profile fetch error for pubkey:", pubkey, err); - } - } - - /** - * Update all DOM elements that match this pubkey, e.g. .author-pic[data-pubkey=...] - */ - updateProfileInDOM(pubkey, profile) { - const picEls = document.querySelectorAll( - `.author-pic[data-pubkey="${pubkey}"]` + // Toggle the gear menu + const gearButtons = this.videoList.querySelectorAll( + "[data-settings-dropdown]" ); - picEls.forEach((el) => { - el.src = profile.picture; + gearButtons.forEach((button) => { + button.addEventListener("click", () => { + const index = button.getAttribute("data-settings-dropdown"); + const dropdown = document.getElementById(`settingsDropdown-${index}`); + if (dropdown) { + dropdown.classList.toggle("hidden"); + } + }); }); - const nameEls = document.querySelectorAll( - `.author-name[data-pubkey="${pubkey}"]` + + // Edit button + const editButtons = this.videoList.querySelectorAll("[data-edit-index]"); + editButtons.forEach((button) => { + button.addEventListener("click", () => { + const index = button.getAttribute("data-edit-index"); + const dropdown = document.getElementById(`settingsDropdown-${index}`); + if (dropdown) dropdown.classList.add("hidden"); + // Assuming you have a method like this in your code: + this.handleEditVideo(index); + }); + }); + + // Revert button + const revertButtons = this.videoList.querySelectorAll( + "[data-revert-index]" ); - nameEls.forEach((el) => { - el.textContent = profile.name; + revertButtons.forEach((button) => { + button.addEventListener("click", () => { + const index = button.getAttribute("data-revert-index"); + const dropdown = document.getElementById(`settingsDropdown-${index}`); + if (dropdown) dropdown.classList.add("hidden"); + // Assuming you have a method like this in your code: + this.handleRevertVideo(index); + }); }); - } - /** - * Plays a video given its magnet URI. - * We simply look up which event has this magnet - * and then delegate to playVideoByEventId for - * consistent modal and metadata handling. - */ - async playVideo(magnetURI) { - try { - if (!magnetURI) { - this.showError("Invalid Magnet URI."); - return; - } - - const decodedMagnet = decodeURIComponent(magnetURI); - - // If we are already playing this exact magnet, do nothing. - if (this.currentMagnetUri === decodedMagnet) { - this.log("Same video requested - already playing"); - return; - } - - // 1) Check local 'videosMap' or 'nostrClient.getActiveVideos()' - let matchedVideo = Array.from(this.videosMap.values()).find( - (v) => v.magnet === decodedMagnet - ); - if (!matchedVideo) { - // Instead of forcing a full `fetchVideos()`, - // try looking in the activeVideos from local cache: - const activeVideos = nostrClient.getActiveVideos(); - matchedVideo = activeVideos.find((v) => v.magnet === decodedMagnet); - } - - // If still not found, you can do a single event-based approach or just show an error: - if (!matchedVideo) { - this.showError("No matching video found in local cache."); - return; - } - - // Update tracking - this.currentMagnetUri = decodedMagnet; - - // Delegate to the main method - await this.playVideoByEventId(matchedVideo.id); - } catch (error) { - console.error("Error in playVideo:", error); - this.showError(`Playback error: ${error.message}`); - } + // Delete All button + const deleteAllButtons = this.videoList.querySelectorAll( + "[data-delete-all-index]" + ); + deleteAllButtons.forEach((button) => { + button.addEventListener("click", () => { + const index = button.getAttribute("data-delete-all-index"); + const dropdown = document.getElementById(`settingsDropdown-${index}`); + if (dropdown) dropdown.classList.add("hidden"); + // Assuming you have a method like this in your code: + this.handleFullDeleteVideo(index); + }); + }); } /** @@ -1350,55 +1448,48 @@ class bitvidApp { * Helper to open a video by event ID (like ?v=...). */ async playVideoByEventId(eventId) { - // First, check if this event is blacklisted by event ID if (this.blacklistedEventIds.has(eventId)) { this.showError("This content has been removed or is not allowed."); return; } try { - // 1) Check local subscription map let video = this.videosMap.get(eventId); - // 2) If not in local map, attempt fallback fetch from getOldEventById if (!video) { video = await this.getOldEventById(eventId); } - // 3) If still not found, show error and return if (!video) { this.showError("Video not found."); return; } - // **Check if video’s author is blacklisted** const authorNpub = this.safeEncodeNpub(video.pubkey) || video.pubkey; if (initialBlacklist.includes(authorNpub)) { this.showError("This content has been removed or is not allowed."); return; } - // 4) Decrypt magnet if private & owned if ( video.isPrivate && video.pubkey === this.pubkey && !video.alreadyDecrypted ) { - this.log("Decrypting private magnet link..."); video.magnet = fakeDecrypt(video.magnet); video.alreadyDecrypted = true; } - // 5) Show the modal this.currentVideo = video; this.currentMagnetUri = video.magnet; this.showModalWithPoster(); - // 6) Update ?v= param in the URL + // Update ?v= param in the URL const nevent = window.NostrTools.nip19.neventEncode({ id: eventId }); - const newUrl = - window.location.pathname + `?v=${encodeURIComponent(nevent)}`; + const newUrl = `${window.location.pathname}?v=${encodeURIComponent( + nevent + )}`; window.history.pushState({}, "", newUrl); - // 7) Optionally fetch the author profile + // Fetch author profile let creatorProfile = { name: "Unknown", picture: `https://robohash.org/${video.pubkey}`, @@ -1418,7 +1509,6 @@ class bitvidApp { this.log("Error fetching creator profile:", error); } - // 8) Render video details in modal const creatorNpub = this.safeEncodeNpub(video.pubkey) || video.pubkey; if (this.videoTitle) { this.videoTitle.textContent = video.title || "Untitled"; @@ -1444,28 +1534,49 @@ class bitvidApp { this.creatorAvatar.alt = creatorProfile.name; } - // 9) Clean up any existing torrent instance before starting a new stream await torrentClient.cleanup(); - // 10) Append a cache-busting parameter to the magnet URI const cacheBustedMagnet = video.magnet + "&ts=" + Date.now(); this.log("Starting video stream with:", cacheBustedMagnet); + // Autoplay preferences + const storedUnmuted = localStorage.getItem("unmutedAutoplay"); + const userWantsUnmuted = storedUnmuted === "true"; + this.modalVideo.muted = !userWantsUnmuted; + + this.modalVideo.addEventListener("volumechange", () => { + localStorage.setItem( + "unmutedAutoplay", + (!this.modalVideo.muted).toString() + ); + }); + const realTorrent = await torrentClient.streamVideo( cacheBustedMagnet, this.modalVideo ); - // 11) Start intervals to update stats + this.modalVideo.play().catch((err) => { + this.log("Autoplay failed:", err); + if (!this.modalVideo.muted) { + this.log("Falling back to muted autoplay."); + this.modalVideo.muted = true; + this.modalVideo.play().catch((err2) => { + this.log("Muted autoplay also failed:", err2); + }); + } + }); + + // Update torrent stats every 3s const updateInterval = setInterval(() => { if (!document.body.contains(this.modalVideo)) { clearInterval(updateInterval); return; } this.updateTorrentStatus(realTorrent); - }, 1000); + }, 3000); this.activeIntervals.push(updateInterval); - // (Optional) Mirror small inline stats into the modal + // Mirror stats into the modal if needed const mirrorInterval = setInterval(() => { if (!document.body.contains(this.modalVideo)) { clearInterval(mirrorInterval); @@ -1476,7 +1587,6 @@ class bitvidApp { const peers = document.getElementById("peers"); const speed = document.getElementById("speed"); const downloaded = document.getElementById("downloaded"); - if (status && this.modalStatus) { this.modalStatus.textContent = status.textContent; } @@ -1492,7 +1602,7 @@ class bitvidApp { if (downloaded && this.modalDownloaded) { this.modalDownloaded.textContent = downloaded.textContent; } - }, 1000); + }, 3000); this.activeIntervals.push(mirrorInterval); } catch (error) { this.log("Error in playVideoByEventId:", error); diff --git a/src/js/config.js b/js/config.js similarity index 100% rename from src/js/config.js rename to js/config.js diff --git a/js/disclaimer.js b/js/disclaimer.js new file mode 100644 index 0000000..e1a1398 --- /dev/null +++ b/js/disclaimer.js @@ -0,0 +1,39 @@ +// js/disclaimer.js + +class DisclaimerModal { + constructor() { + // Initialize elements when the disclaimer HTML is in the DOM. + this.init(); + } + + init() { + this.modal = document.getElementById("disclaimerModal"); + this.acceptButton = document.getElementById("acceptDisclaimer"); + if (this.acceptButton) { + this.acceptButton.addEventListener("click", () => { + this.hide(); + }); + } + } + + hide() { + if (this.modal) { + this.modal.classList.add("hidden"); + } + localStorage.setItem("hasSeenDisclaimer", "true"); + } + + show() { + // In case the modal hasn't been initialized yet. + if (!this.modal) { + this.init(); + } + if (!localStorage.getItem("hasSeenDisclaimer") && this.modal) { + this.modal.classList.remove("hidden"); + } + } +} + +// Create and export a default instance. +const disclaimerModal = new DisclaimerModal(); +export default disclaimerModal; diff --git a/js/index.js b/js/index.js new file mode 100644 index 0000000..4cba2d8 --- /dev/null +++ b/js/index.js @@ -0,0 +1,323 @@ +// js/index.js + +// 1) Load modals (login, application, etc.) +async function loadModal(url) { + try { + const response = await fetch(url); + if (!response.ok) { + throw new Error("Failed to load " + url); + } + const html = await response.text(); + document + .getElementById("modalContainer") + .insertAdjacentHTML("beforeend", html); + console.log(url, "loaded"); + } catch (err) { + console.error(err); + } +} + +// 2) Load sidebar +async function loadSidebar(url, containerId) { + try { + const response = await fetch(url); + if (!response.ok) { + throw new Error("Failed to load " + url); + } + const html = await response.text(); + document.getElementById(containerId).innerHTML = html; + console.log(url, "loaded into", containerId); + } catch (err) { + console.error(err); + } +} + +// 3) Load the disclaimer (now separate) +async function loadDisclaimer(url, containerId) { + try { + const response = await fetch(url); + if (!response.ok) { + throw new Error("Failed to load " + url); + } + const html = await response.text(); + document.getElementById(containerId).insertAdjacentHTML("beforeend", html); + console.log(url, "disclaimer loaded into", containerId); + } catch (err) { + console.error(err); + } +} + +// 4) Load everything: modals, sidebar, disclaimers +Promise.all([ + // Existing modals + loadModal("components/login-modal.html"), + loadModal("components/application-form.html"), + loadModal("components/content-appeals-form.html"), + + // New forms + loadModal("components/general-feedback-form.html"), + loadModal("components/feature-request-form.html"), + loadModal("components/bug-fix-form.html"), +]) + .then(() => { + console.log("Modals loaded."); + return loadSidebar("components/sidebar.html", "sidebarContainer"); + }) + .then(() => { + console.log("Sidebar loaded."); + + // Attach mobile menu button toggle logic (for sidebar) + const mobileMenuBtn = document.getElementById("mobileMenuBtn"); + const sidebar = document.getElementById("sidebar"); + const app = document.getElementById("app"); + if (mobileMenuBtn && sidebar && app) { + mobileMenuBtn.addEventListener("click", () => { + sidebar.classList.toggle("sidebar-open"); + app.classList.toggle("sidebar-open"); + }); + } + + // Attach "More" button toggle logic for footer links + const footerDropdownButton = document.getElementById( + "footerDropdownButton" + ); + if (footerDropdownButton) { + footerDropdownButton.addEventListener("click", () => { + const footerLinksContainer = document.getElementById( + "footerLinksContainer" + ); + if (!footerLinksContainer) return; + footerLinksContainer.classList.toggle("hidden"); + if (footerLinksContainer.classList.contains("hidden")) { + footerDropdownButton.innerHTML = "More ▼"; + } else { + footerDropdownButton.innerHTML = "Less ▲"; + } + }); + } + + // Load and set up sidebar navigation + return import("./sidebar.js").then((module) => { + module.setupSidebarNavigation(); + }); + }) + .then(() => { + // Now load the disclaimer + return loadDisclaimer("components/disclaimer.html", "modalContainer"); + }) + .then(() => { + console.log("Disclaimer loaded."); + + // 1) Login button => open login modal + const loginNavBtn = document.getElementById("loginButton"); + if (loginNavBtn) { + loginNavBtn.addEventListener("click", () => { + const loginModal = document.getElementById("loginModal"); + if (loginModal) { + loginModal.classList.remove("hidden"); + } + }); + } + + // 2) Close login modal + const closeLoginBtn = document.getElementById("closeLoginModal"); + if (closeLoginBtn) { + closeLoginBtn.addEventListener("click", () => { + const loginModal = document.getElementById("loginModal"); + if (loginModal) { + loginModal.classList.add("hidden"); + } + }); + } + + // 3) "Application Form" => open application form + const openAppFormBtn = document.getElementById("openApplicationModal"); + if (openAppFormBtn) { + openAppFormBtn.addEventListener("click", () => { + const loginModal = document.getElementById("loginModal"); + if (loginModal) { + loginModal.classList.add("hidden"); + } + const appModal = document.getElementById("nostrFormModal"); + if (appModal) { + appModal.classList.remove("hidden"); + } + }); + } + + // 4) Close application form + const closeNostrFormBtn = document.getElementById("closeNostrFormModal"); + if (closeNostrFormBtn) { + closeNostrFormBtn.addEventListener("click", () => { + const appModal = document.getElementById("nostrFormModal"); + if (appModal) { + appModal.classList.add("hidden"); + } + // If user hasn't seen disclaimer, show it + if (!localStorage.getItem("hasSeenDisclaimer")) { + const disclaimerModal = document.getElementById("disclaimerModal"); + if (disclaimerModal) { + disclaimerModal.classList.remove("hidden"); + } + } + }); + } + + // Once everything is loaded, handle the query params (modal? v?) & disclaimers + handleQueryParams(); + + // Listen for hash changes + window.addEventListener("hashchange", handleHashChange); + + // Also run once on initial load + handleHashChange(); + }); + +/* ------------------------------------------- + HELPER FUNCTIONS FOR QUERY AND HASH +-------------------------------------------- */ + +/** + * Sets the location.hash to "#view=", + * removing any ?modal=... or ?v=... from the query string. + */ +export function setHashView(viewName) { + const url = new URL(window.location.href); + url.searchParams.delete("modal"); + url.searchParams.delete("v"); + const newUrl = url.pathname + url.search + `#view=${viewName}`; + window.history.replaceState({}, "", newUrl); + handleHashChange(); +} + +/** + * Sets a query param (e.g. ?modal=xxx or ?v=yyy), + * removing any "#view=..." from the hash to avoid collisions. + */ +export function setQueryParam(key, value) { + const url = new URL(window.location.href); + url.hash = ""; + url.searchParams.set(key, value); + const newUrl = url.pathname + url.search; + window.history.replaceState({}, "", newUrl); + handleQueryParams(); +} + +/** + * Check the current URL for ?modal=..., ?v=..., etc. + * Open the correct modals or disclaimers as needed. + */ +function handleQueryParams() { + const urlParams = new URLSearchParams(window.location.search); + const modalParam = urlParams.get("modal"); + + if (modalParam === "appeals") { + const appealsModal = document.getElementById("contentAppealsModal"); + if (appealsModal) { + appealsModal.classList.remove("hidden"); + } + const closeAppealsBtn = document.getElementById("closeContentAppealsModal"); + if (closeAppealsBtn) { + closeAppealsBtn.addEventListener("click", () => { + const appealsModal = document.getElementById("contentAppealsModal"); + if (appealsModal) { + appealsModal.classList.add("hidden"); + } + if (!localStorage.getItem("hasSeenDisclaimer")) { + const disclaimerModal = document.getElementById("disclaimerModal"); + if (disclaimerModal) { + disclaimerModal.classList.remove("hidden"); + } + } + }); + } + } else if (modalParam === "application") { + const appModal = document.getElementById("nostrFormModal"); + if (appModal) { + appModal.classList.remove("hidden"); + } + } else { + const hasSeenDisclaimer = localStorage.getItem("hasSeenDisclaimer"); + if (!hasSeenDisclaimer) { + const disclaimerModal = document.getElementById("disclaimerModal"); + if (disclaimerModal) { + disclaimerModal.classList.remove("hidden"); + } + } + } + + if (modalParam === "feedback") { + const feedbackModal = document.getElementById("generalFeedbackModal"); + if (feedbackModal) { + feedbackModal.classList.remove("hidden"); + } + } else if (modalParam === "feature") { + const featureModal = document.getElementById("featureRequestModal"); + if (featureModal) { + featureModal.classList.remove("hidden"); + } + } else if (modalParam === "bug") { + const bugModal = document.getElementById("bugFixModal"); + if (bugModal) { + bugModal.classList.remove("hidden"); + } + } + + const closeFeedbackBtn = document.getElementById("closeGeneralFeedbackModal"); + if (closeFeedbackBtn) { + closeFeedbackBtn.addEventListener("click", () => { + const feedbackModal = document.getElementById("generalFeedbackModal"); + if (feedbackModal) { + feedbackModal.classList.add("hidden"); + } + }); + } + const closeFeatureBtn = document.getElementById("closeFeatureRequestModal"); + if (closeFeatureBtn) { + closeFeatureBtn.addEventListener("click", () => { + const featureModal = document.getElementById("featureRequestModal"); + if (featureModal) { + featureModal.classList.add("hidden"); + } + }); + } + const closeBugBtn = document.getElementById("closeBugFixModal"); + if (closeBugBtn) { + closeBugBtn.addEventListener("click", () => { + const bugModal = document.getElementById("bugFixModal"); + if (bugModal) { + bugModal.classList.add("hidden"); + } + }); + } +} + +/** + * Handle #view=... in the hash and load the correct partial view. + */ +function handleHashChange() { + console.log("handleHashChange called, current hash =", window.location.hash); + const hash = window.location.hash || ""; + const match = hash.match(/^#view=(.+)/); + if (!match || !match[1]) { + import("./viewManager.js").then(({ loadView, viewInitRegistry }) => { + loadView("views/most-recent-videos.html").then(() => { + const initFn = viewInitRegistry["most-recent-videos"]; + if (typeof initFn === "function") { + initFn(); + } + }); + }); + return; + } + const viewName = match[1]; + const viewUrl = `views/${viewName}.html`; + import("./viewManager.js").then(({ loadView, viewInitRegistry }) => { + loadView(viewUrl).then(() => { + const initFn = viewInitRegistry[viewName]; + if (typeof initFn === "function") { + initFn(); + } + }); + }); +} diff --git a/src/js/libs/nostr.bundle.js b/js/libs/nostr.bundle.js similarity index 100% rename from src/js/libs/nostr.bundle.js rename to js/libs/nostr.bundle.js diff --git a/src/js/lists.js b/js/lists.js similarity index 100% rename from src/js/lists.js rename to js/lists.js diff --git a/src/js/nostr.js b/js/nostr.js similarity index 99% rename from src/js/nostr.js rename to js/nostr.js index c78ff60..b3f44b4 100644 --- a/src/js/nostr.js +++ b/js/nostr.js @@ -544,9 +544,6 @@ class NostrClient { return true; } - /** - * subscribeVideos => old approach - */ /** * Subscribe to *all* videos (old and new) with a single subscription, * then call onVideo() each time a new or updated event arrives. @@ -612,6 +609,7 @@ class NostrClient { } }); + // Return the subscription object directly. return sub; } diff --git a/js/sidebar.js b/js/sidebar.js new file mode 100644 index 0000000..adb6f5c --- /dev/null +++ b/js/sidebar.js @@ -0,0 +1,29 @@ +// sidebar.js +import { setHashView } from "./index.js"; + +export function setupSidebarNavigation() { + const sidebarLinks = document.querySelectorAll('#sidebar a[href^="#view="]'); + sidebarLinks.forEach((link) => { + link.addEventListener("click", (e) => { + e.preventDefault(); + + // e.g. "#view=about" + const href = link.getAttribute("href") || ""; + const match = href.match(/^#view=(.+)/); + if (!match) return; + + const viewName = match[1]; // "about", "ipns", etc. + setHashView(viewName); // This changes the hash and loads the view. + + // --- NEW: if on mobile, close the sidebar automatically. --- + if (window.innerWidth < 768) { + const sidebar = document.getElementById("sidebar"); + const app = document.getElementById("app"); + if (sidebar && app) { + sidebar.classList.remove("sidebar-open"); + app.classList.remove("sidebar-open"); + } + } + }); + }); +} diff --git a/js/viewManager.js b/js/viewManager.js new file mode 100644 index 0000000..e6b5c5e --- /dev/null +++ b/js/viewManager.js @@ -0,0 +1,55 @@ +// js/viewManager.js + +// Load a partial view by URL into the #viewContainer +// js/viewManager.js +export async function loadView(viewUrl) { + try { + const res = await fetch(viewUrl); + if (!res.ok) { + throw new Error(`Failed to load view: ${res.status}`); + } + const text = await res.text(); + + // DOMParser, parse out the body, inject + const parser = new DOMParser(); + const doc = parser.parseFromString(text, "text/html"); + const container = document.getElementById("viewContainer"); + + container.innerHTML = doc.body.innerHTML; + + // Now copy and execute each script + const scriptTags = doc.querySelectorAll("script"); + scriptTags.forEach((oldScript) => { + const newScript = document.createElement("script"); + Array.from(oldScript.attributes).forEach((attr) => { + newScript.setAttribute(attr.name, attr.value); + }); + newScript.textContent = oldScript.textContent; + container.appendChild(newScript); + }); + } catch (err) { + console.error("View loading error:", err); + document.getElementById("viewContainer").innerHTML = + "

Failed to load content.

"; + } +} + +export const viewInitRegistry = { + "most-recent-videos": () => { + if (window.app && window.app.loadVideos) { + window.app.videoList = document.getElementById("videoList"); + window.app.loadVideos(); + } + // Force the profiles to update after the new view is in place. + if (window.app && window.app.forceRefreshAllProfiles) { + window.app.forceRefreshAllProfiles(); + } + }, + explore: () => { + console.log("Explore view loaded."); + }, + subscriptions: () => { + console.log("Subscriptions view loaded."); + }, + // Add additional view-specific functions here as needed. +}; diff --git a/src/js/webtorrent.js b/js/webtorrent.js similarity index 79% rename from src/js/webtorrent.js rename to js/webtorrent.js index 37519ac..65b8ea8 100644 --- a/src/js/webtorrent.js +++ b/js/webtorrent.js @@ -1,10 +1,19 @@ +//js/webtorrent.js + import WebTorrent from "./webtorrent.min.js"; export class TorrentClient { constructor() { - this.client = new WebTorrent(); + // Reusable objects and flags + this.client = null; this.currentTorrent = null; - this.TIMEOUT_DURATION = 60000; // 60 seconds + + // Service worker registration is cached + this.swRegistration = null; + this.serverCreated = false; // Indicates if we've called createServer on this.client + + // Timeout for SW operations + this.TIMEOUT_DURATION = 60000; } log(msg) { @@ -21,6 +30,22 @@ export class TorrentClient { return /firefox/i.test(window.navigator.userAgent); } + /** + * Makes sure we have exactly one WebTorrent client instance and one SW registration. + * Called once from streamVideo. + */ + async init() { + // 1) If the client doesn't exist, create it + if (!this.client) { + this.client = new WebTorrent(); + } + + // 2) If we haven’t registered the service worker yet, do it now + if (!this.swRegistration) { + this.swRegistration = await this.setupServiceWorker(); + } + } + async waitForServiceWorkerActivation(registration) { return new Promise((resolve, reject) => { const timeout = setTimeout(() => { @@ -67,6 +92,7 @@ export class TorrentClient { throw new Error("Service Worker not supported or disabled"); } + // Brave-specific logic if (isBraveBrowser) { this.log("Checking Brave configuration..."); if (!navigator.serviceWorker) { @@ -78,6 +104,7 @@ export class TorrentClient { throw new Error("Please enable WebRTC in Brave Shield settings"); } + // Unregister all existing service workers before installing a fresh one const registrations = await navigator.serviceWorker.getRegistrations(); for (const reg of registrations) { await reg.unregister(); @@ -87,12 +114,13 @@ export class TorrentClient { this.log("Registering service worker at /sw.min.js..."); const registration = await navigator.serviceWorker.register( - "./sw.min.js", + "/sw.min.js", { - scope: "./", + scope: "/", updateViaCache: "none", } ); + this.log("Service worker registered"); if (registration.installing) { @@ -134,8 +162,8 @@ export class TorrentClient { // Force the SW to check for updates registration.update(); - this.log("Service worker ready"); + return registration; } catch (error) { this.log("Service worker setup error:", error); @@ -143,7 +171,7 @@ export class TorrentClient { } } - // Minimal handleChromeTorrent + // Handle Chrome-based browsers handleChromeTorrent(torrent, videoElement, resolve, reject) { torrent.on("warning", (err) => { if (err && typeof err.message === "string") { @@ -203,7 +231,7 @@ export class TorrentClient { }); } - // Minimal handleFirefoxTorrent + // Handle Firefox-based browsers handleFirefoxTorrent(torrent, videoElement, resolve, reject) { const file = torrent.files.find((f) => /\.(mp4|webm|mkv)$/.test(f.name.toLowerCase()) @@ -226,7 +254,7 @@ export class TorrentClient { }); try { - file.streamTo(videoElement, { highWaterMark: 32 * 1024 }); + file.streamTo(videoElement, { highWaterMark: 256 * 1024 }); this.currentTorrent = torrent; resolve(torrent); } catch (err) { @@ -242,27 +270,27 @@ export class TorrentClient { /** * Initiates streaming of a torrent magnet to a