@banou/ripple 0.0.22 → 0.0.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +102 -187
- package/fkn.json +1 -1
- package/index.js +110 -73
- package/package.json +3 -1
- package/sw.js +1 -1
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2023 Banou
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,212 +1,127 @@
|
|
|
1
|
-
# Ripple
|
|
1
|
+
# Ripple
|
|
2
|
+
|
|
3
|
+
Download and stream torrents in your browser. Ripple runs a real BitTorrent engine
|
|
4
|
+
([libtorrent](https://www.libtorrent.org), compiled to WebAssembly) in the page, reaches peers
|
|
5
|
+
through the [FKN](https://fkn.app) relay, and plays a video while it downloads. There is nothing to
|
|
6
|
+
install and no Ripple server.
|
|
7
|
+
|
|
8
|
+
- Live at [torrent.fkn.app](https://torrent.fkn.app), and inside FKN at
|
|
9
|
+
[fkn.app/app/npm:@banou/ripple](https://fkn.app/app/npm:@banou/ripple)
|
|
10
|
+
- Published on npm as [`@banou/ripple`](https://www.npmjs.com/package/@banou/ripple)
|
|
11
|
+
|
|
12
|
+
## Features
|
|
13
|
+
|
|
14
|
+
- **Add a magnet or a `.torrent`.** Paste it, drop it, or open it from your system once Ripple is
|
|
15
|
+
installed as an app, which then opens `magnet:` links and `.torrent` files.
|
|
16
|
+
- **Watch while it downloads**, with subtitles, a choice of audio track and a Normalize volume
|
|
17
|
+
switch.
|
|
18
|
+
- **Pick the files you want** before the download starts.
|
|
19
|
+
- **Keep files in the browser or in a folder on your disk**, and save one file, or a zip of
|
|
20
|
+
several.
|
|
21
|
+
- **Create a torrent** from files or a folder (v1, v2 or hybrid, private with a source tag, web
|
|
22
|
+
seeds) and seed it straight from those files.
|
|
23
|
+
- **Your library on every device.** Signed in to FKN, your list and its thumbnails sync, encrypted
|
|
24
|
+
in your browser before they leave it.
|
|
25
|
+
- **Share links** that open the player or a download page, which other sites can embed.
|
|
26
|
+
- **One engine for all your tabs.** One tab runs it and the others use it.
|
|
27
|
+
- **Speed limits** for everything or per torrent, and the peers, trackers, files and all-time share
|
|
28
|
+
ratio of each torrent.
|
|
29
|
+
- **No Ripple account and no analytics.**
|
|
30
|
+
|
|
31
|
+
## How it is built
|
|
32
|
+
|
|
33
|
+
- **UI**: React 19, routed with React Router 7 and styled with Emotion, in `src/router` and
|
|
34
|
+
`src/components`.
|
|
35
|
+
- **Engine**: [`libtorrent-wasm`](https://www.npmjs.com/package/libtorrent-wasm) in a Web Worker
|
|
36
|
+
(`src/torrent/worker.ts`), driven from the page by `src/torrent/client.ts`. One tab holds it
|
|
37
|
+
under a Web Lock and the others reach it over a BroadcastChannel (`engine-election.ts`,
|
|
38
|
+
`engine-share.ts`).
|
|
39
|
+
- **Network**: `@fkn/lib/net` and `@fkn/lib/dgram`, carried through the FKN relay (`relayWorker`
|
|
40
|
+
bridges the worker). Inbound peers reach a port the relay reserves.
|
|
41
|
+
- **Storage**: OPFS, or a folder you grant (`src/torrent/hybrid-storage.ts`). The list, resume data
|
|
42
|
+
and thumbnails are in IndexedDB.
|
|
43
|
+
- **Player**: [`@banou/media-player`](https://www.npmjs.com/package/@banou/media-player) at
|
|
44
|
+
`/watch` (`src/router/embed.tsx`), with libav-wasm and jassub.
|
|
45
|
+
- **Sync**: `src/torrent/use-cloud-backup.ts` and `src/torrent/thumbnail-sync.ts`, under `ripple/`
|
|
46
|
+
in the FKN account's storage.
|
|
47
|
+
|
|
48
|
+
## Development
|
|
49
|
+
|
|
50
|
+
Use Node 24 (`.node-version` names 24.21.0).
|
|
2
51
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
| --- | --- | --- |
|
|
12
|
-
| `m` | the packed magnet, base64url | what Ripple writes today |
|
|
13
|
-
| `magnet` | base64 of the magnet URI | the original form, read forever |
|
|
14
|
-
| `fileIndex` | a file index | the file `/watch` plays; the fallback `/download` uses |
|
|
15
|
-
| `files` | `all`, `3`, `0-4`, `0,2,5` | what `/download` delivers |
|
|
16
|
-
| `f` | a packed file list | optional preview, `/download` only |
|
|
17
|
-
|
|
18
|
-
One of `m` or `magnet` is required. `m` wins if both are present.
|
|
19
|
-
|
|
20
|
-
There is no `mode` parameter. It used to choose the page and is now neither written nor read, so a
|
|
21
|
-
link still carrying one gets the page its PATH names and the parameter is ignored.
|
|
22
|
-
|
|
23
|
-
### The two magnet forms
|
|
24
|
-
|
|
25
|
-
`magnet=<base64 of the magnet URI>` is the original, and it is **permanent**. Every link ever handed
|
|
26
|
-
out with it keeps working, and an embedder that finds it easier to write can keep writing it. Nothing
|
|
27
|
-
about it has changed.
|
|
28
|
-
|
|
29
|
-
`m=` is the same torrent packed smaller: the infohash as raw bytes rather than 40 hex characters, and
|
|
30
|
-
the rest of the query deflated against a fixed table of the announce URLs that public magnets
|
|
31
|
-
overwhelmingly share. It is base64url, so a query string carries it without escaping anything. Across
|
|
32
|
-
a corpus covering every magnet form Ripple accepts, the median link is **69% shorter**; a five-tracker
|
|
33
|
-
release goes from a 549-character URL to 148. A magnet using only private trackers has nothing to
|
|
34
|
-
match against and still comes out around 42% shorter, on the strength of the packed infohash alone.
|
|
35
|
-
|
|
36
|
-
Ripple writes whichever of the two is shorter, which is almost always `m`. The digit is a version:
|
|
37
|
-
the table it compresses against can never be edited, so if it ever needs to change the parameter
|
|
38
|
-
becomes `m2` and `m` keeps decoding the way it always did.
|
|
39
|
-
|
|
40
|
-
### Parameter order
|
|
41
|
-
|
|
42
|
-
Order never matters when READING a link. When writing one, Ripple puts the plainest parts first:
|
|
43
|
-
|
|
44
|
-
```
|
|
45
|
-
/watch?fileIndex=3&m=AQAIraWnphg6rh4J2DHfZ0jVZglaEAMWq8GZeSWpORSmTXxFBwA
|
|
46
|
-
/download?files=2&m=AQAIraWnphg6rh4J2DHfZ0jVZglaEAMWq8GZeSWpORSmTXxFBwA
|
|
47
|
-
/download?m=AQAIraWnphg6rh4J2DHfZ0jVZglaEAMWq8GZeSWpORSmTXxFBwA&f=AWNSC87MK0nN0YdQ
|
|
52
|
+
```sh
|
|
53
|
+
npm ci
|
|
54
|
+
npm run dev # http://localhost:4560
|
|
55
|
+
npm run build # the site and the npm package, into build/
|
|
56
|
+
npm test # unit tests (vitest, node)
|
|
57
|
+
npm run test:browser # component tests in Chrome
|
|
58
|
+
npm run typecheck
|
|
59
|
+
npm run lint # vp lint
|
|
48
60
|
```
|
|
49
61
|
|
|
50
|
-
The
|
|
51
|
-
it was the only thing in the link; now that everything around it is base64url or an index, leading
|
|
52
|
-
with it buried the one part a person can read behind forty characters of noise. A URL is read from
|
|
53
|
-
the left and truncated from the right, so `f`, the longest parameter and the one a reader cares
|
|
54
|
-
least about, goes last.
|
|
55
|
-
|
|
56
|
-
### `f`, the file list
|
|
57
|
-
|
|
58
|
-
A magnet names a torrent and nothing else, so a download link normally opens on "Reading the torrent
|
|
59
|
-
from the network" for as long as metadata takes. `f` closes that gap by carrying the file list the
|
|
60
|
-
sender already had, and the page shows it immediately, marked **from the link**.
|
|
62
|
+
The Playwright suites build first, then drive the built site in Chrome:
|
|
61
63
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
64
|
+
| command | what it covers |
|
|
65
|
+
| --- | --- |
|
|
66
|
+
| `npm run test:e2e:local` | the app, needing nothing but the browser (and ffmpeg) |
|
|
67
|
+
| `npm run test:e2e:machine` | live figures keep their width, which depends on the machine's fonts |
|
|
68
|
+
| `npm run test:e2e:swarm` | the engine against real public swarms |
|
|
69
|
+
| `npm run test:e2e:rig` | the whole chain against a local swarm, see [tests/swarm-rig](./tests/swarm-rig/README.md) |
|
|
67
70
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
left off entirely rather than pushing the link past what a chat message will carry. Absent, `f`
|
|
71
|
-
changes nothing, so a link without it behaves exactly as it always has.
|
|
71
|
+
The swarm and rig suites need a headful Chrome, because headless Chromium stalls the engine at
|
|
72
|
+
0 B/s. Run them under `xvfb-run -a -s "-screen 0 1280x720x24"` so no window opens.
|
|
72
73
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
file list is the part that is both small and worth having.
|
|
74
|
+
A push to `main` deploys [torrent.fkn.app](https://torrent.fkn.app) (Cloudflare Pages), and
|
|
75
|
+
publishes to npm when `package.json` names a version the registry does not have yet
|
|
76
|
+
(`.github/workflows/publish-lib.yml`). Each published version is signed for FKN with `fkn-sign`.
|
|
77
77
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
Plays `fileIndex` (0 if absent) in the media player, with the filename, peer count and transfer
|
|
81
|
-
rates drawn over the video.
|
|
82
|
-
|
|
83
|
-
The mode used to be a parameter, and for a while Ripple wrote `mode=watch` out in full so that a
|
|
84
|
-
link said what it did rather than being told apart by an absence. The path says it for free and in
|
|
85
|
-
the part of a URL that is read first, so the parameter is gone from both halves and a link still
|
|
86
|
-
carrying one gets the page its path names.
|
|
78
|
+
## Embedding
|
|
87
79
|
|
|
88
|
-
|
|
89
|
-
|
|
80
|
+
`/watch` and `/download` are the pages another site puts in an iframe. Both take a magnet and the
|
|
81
|
+
same query; the path chooses which one renders. `/watch` plays a file, `/download` delivers files.
|
|
90
82
|
|
|
91
|
-
|
|
83
|
+
| param | value | meaning |
|
|
84
|
+
| --- | --- | --- |
|
|
85
|
+
| `magnet` | base64 of the magnet URI | the form to write; permanent |
|
|
86
|
+
| `m` | the packed magnet, base64url | what Ripple's own share links write |
|
|
87
|
+
| `fileIndex` | a file index | the file `/watch` plays (0 if absent); the fallback `/download` uses |
|
|
88
|
+
| `files` | `all`, `3`, `0-4`, `0,2,5` | what `/download` delivers |
|
|
92
89
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
90
|
+
One of `magnet` or `m` is required, and `m` is read first when both are present. `magnet=` is the
|
|
91
|
+
original form and every link written with it keeps working. `m=` is the same torrent packed into a
|
|
92
|
+
much shorter link.
|
|
96
93
|
|
|
97
94
|
```
|
|
98
|
-
/
|
|
99
|
-
/download?magnet=<base64
|
|
100
|
-
/download?magnet=<base64>&files=
|
|
101
|
-
/download?magnet=<base64>&files=0
|
|
102
|
-
/download?magnet=<base64>&
|
|
103
|
-
|
|
95
|
+
/watch?magnet=<base64>&fileIndex=3 plays file 3
|
|
96
|
+
/download?magnet=<base64> the whole torrent
|
|
97
|
+
/download?magnet=<base64>&files=3 just file 3
|
|
98
|
+
/download?magnet=<base64>&files=0-4 files 0 to 4 inclusive, as a zip
|
|
99
|
+
/download?magnet=<base64>&files=0,2,5 those three, as a zip
|
|
100
|
+
/download?magnet=<base64>&fileIndex=3 same as files=3
|
|
104
101
|
```
|
|
105
102
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
### What an embedder has to grant
|
|
103
|
+
One file is delivered as that file, and more than one as a single `.zip`. `files` outranks
|
|
104
|
+
`fileIndex`. Indices the torrent does not have are dropped, and a selection that resolves to
|
|
105
|
+
nothing says so instead of downloading the whole torrent.
|
|
110
106
|
|
|
111
|
-
**
|
|
112
|
-
|
|
113
|
-
restored from inside, and Chrome then refuses the download *silently*: the navigation is dropped, no
|
|
114
|
-
event fires and nothing throws. There is no `downloads` feature in Permissions-Policy, so `allow=`
|
|
115
|
-
is not a lever here; the `sandbox` attribute is the only one.
|
|
107
|
+
**A sandboxed frame needs `allow-downloads`**, or the browser drops the download without an error.
|
|
108
|
+
Leaving out `sandbox` works too.
|
|
116
109
|
|
|
117
110
|
```html
|
|
118
111
|
<iframe src="https://torrent.fkn.app/download?magnet=..."
|
|
119
112
|
sandbox="allow-scripts allow-same-origin allow-downloads"></iframe>
|
|
120
113
|
```
|
|
121
114
|
|
|
122
|
-
|
|
123
|
-
offers a link to open itself top-level, which is the way out if the embedder cannot grant the flag.
|
|
124
|
-
|
|
125
|
-
Two other consequences of being framed, both handled: Chrome refuses `showSaveFilePicker` in a
|
|
126
|
-
cross-origin frame, so the page does not ask for one, and delivery goes through the service worker
|
|
127
|
-
instead.
|
|
115
|
+
Why each part works the way it does is in [docs/embedding.md](./docs/embedding.md).
|
|
128
116
|
|
|
129
|
-
##
|
|
130
|
-
|
|
131
|
-
| command | what it covers | cost |
|
|
132
|
-
| --- | --- | --- |
|
|
133
|
-
| `npm test` | pure logic in node | ~1s |
|
|
134
|
-
| `npm run test:browser` | components in real Chrome | ~2s |
|
|
135
|
-
| `npx tsc --noEmit` | types | ~1s |
|
|
136
|
-
| `npx vp lint` | oxlint, type aware | ~1s |
|
|
137
|
-
| `npm run test:download` | the download page against a real torrent, end to end | ~30s |
|
|
138
|
-
| `npm run test:e2e` | the engine against real swarms | minutes, headful |
|
|
139
|
-
| `npm run test:e2e:rig` | the whole chain against a local swarm, see below | ~90s, headful under Xvfb |
|
|
117
|
+
## Privacy
|
|
140
118
|
|
|
141
|
-
|
|
142
|
-
|
|
119
|
+
Ripple needs no account and runs no analytics. Your list and your files stay in your browser, or in
|
|
120
|
+
a folder you choose. Signed in to FKN, your list and its thumbnails sync to your FKN account,
|
|
121
|
+
encrypted in your browser first. Peers see the FKN relay's address, not yours. The
|
|
122
|
+
[privacy page](https://torrent.fkn.app/privacy) lists what Ripple keeps
|
|
123
|
+
(`src/router/privacy.tsx`).
|
|
143
124
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
`e2e:rig` downloads a torrent that exists only on this machine. The seeders are transmission daemons
|
|
147
|
-
on `127.0.0.2` and up, the relay is a local webvpn build, and the broker is a local fkn-client web
|
|
148
|
-
build, so the only source of a byte is the fleet the magnet names. The public swarm cannot give that:
|
|
149
|
-
byte-identical code measured 14.7 s to 73.4 s to first frame there.
|
|
150
|
-
|
|
151
|
-
| piece | where | port |
|
|
152
|
-
| --- | --- | --- |
|
|
153
|
-
| broker (fkn-client `web`, development build) | `http://localhost:5234/api` | 5234 |
|
|
154
|
-
| relay (webvpn release build) | WebTransport, HTTP, WebSocket on `127.0.0.1` | 5433, 5434, 5443 |
|
|
155
|
-
| ripple (`build/`, served) | `http://localhost:5460` | 5460 |
|
|
156
|
-
| seeders (transmission 4) | `127.0.0.2:52600` and up, rpc `9400` and up | |
|
|
157
|
-
|
|
158
|
-
None of it overlaps fkn/local's rig (1234, 3000, 4433, 4560, 8443), so the two run side by side. The
|
|
159
|
-
ports live in `tests/swarm-rig/topology.json`.
|
|
160
|
-
|
|
161
|
-
It needs three checkouts, and an fkn-client worktree of its own, because the rig rewrites that
|
|
162
|
-
checkout's `.env.local` files and build output:
|
|
163
|
-
|
|
164
|
-
```sh
|
|
165
|
-
git -C ~/dev/horionsoftware/fkn-client worktree add --detach ~/dev/fkn-client-rig origin/main
|
|
166
|
-
(cd ~/dev/horionsoftware/webvpn && cargo build --release)
|
|
167
|
-
|
|
168
|
-
export RIPPLE_RIG_FKN_CLIENT=~/dev/fkn-client-rig
|
|
169
|
-
export RIPPLE_RIG_WEBVPN=~/dev/horionsoftware/webvpn
|
|
170
|
-
export RIPPLE_RIG_LOCAL=~/dev/horionsoftware/local
|
|
171
|
-
export RIPPLE_CHROME=$(command -v google-chrome-stable)
|
|
172
|
-
export RIG_TRANSMISSION=$(nix build --no-link --print-out-paths 'nixpkgs#transmission_4')
|
|
173
|
-
|
|
174
|
-
nix shell nixpkgs#xvfb-run -c env -u WAYLAND_DISPLAY -u NIXOS_OZONE_WL \
|
|
175
|
-
xvfb-run -a -s "-screen 0 1280x720x24" npm run test:e2e:rig
|
|
176
|
-
```
|
|
125
|
+
## License
|
|
177
126
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
1. `scripts/rig-prepare.mjs` builds `@fkn/lib` with `VITE_WEB_ORIGIN=http://localhost:5234` and the
|
|
181
|
-
web app as the broker, then checks what each build baked in. It refuses a main worktree.
|
|
182
|
-
2. `npm run build`, which sees `RIPPLE_RIG_FKN_CLIENT` and resolves `@fkn/lib` to that local build
|
|
183
|
-
(the published lib has `https://fkn.app` baked in, and no setting of ripple's can move it). The
|
|
184
|
-
build warns when it does this. Afterwards `build/` points at the rig's broker, so rebuild without
|
|
185
|
-
the variable before serving it for anything else.
|
|
186
|
-
3. `playwright.rig.config.ts` starts the relay (the environment `byo.sh webvpn --export` prints, moved
|
|
187
|
-
to the rig's ports, with a fresh certificate and `FREE_RATE_BYTES_PER_SEC` at 1 GiB/s), the broker
|
|
188
|
-
and ripple, then runs `tests/swarm-rig.spec.ts`.
|
|
189
|
-
|
|
190
|
-
The spec has three tests, run in order:
|
|
191
|
-
|
|
192
|
-
- **Configuration.** The relay answers. The RUNNING relay's environment allows private targets and
|
|
193
|
-
carries the raised rate. `/api` serves the broker's own document, not the SPA fallback.
|
|
194
|
-
- **The swarm arm.** It makes a 60 s 720p fixture with ffmpeg (no private flag, which would stop
|
|
195
|
-
seeders serving metadata to a magnet). It starts N seeders with staggered adds and opens `/watch`
|
|
196
|
-
for the magnet in a fresh regular profile. It then asserts that the engine held the whole file,
|
|
197
|
-
that OPFS holds it piece for piece and by SHA-256, that a frame was painted, and that the seeders
|
|
198
|
-
uploaded at least the file. It prints time to first byte, first frame and completion from the add.
|
|
199
|
-
- **The control.** The same fleet comes up and goes down, and the same visit runs for 30 s. It must
|
|
200
|
-
see 0 bytes in the engine and in OPFS, and the swarm arm's own assertion must FAIL on it.
|
|
201
|
-
|
|
202
|
-
The seeders run under a shell that kills them when the test process is gone, so a run killed
|
|
203
|
-
outright leaves nothing on their addresses. Without that shell, all four outlived a SIGKILL.
|
|
204
|
-
|
|
205
|
-
Knobs: `RIPPLE_RIG_SEEDERS` (default 4), `RIPPLE_RIG_BUDGET_MS` (swarm arm, 120000),
|
|
206
|
-
`RIPPLE_RIG_CONTROL_MS` (30000), `RIPPLE_RIG_STATE` (fixture and seeder state, default
|
|
207
|
-
`$TMPDIR/ripple-swarm-rig`). The config refuses to start when `WAYLAND_DISPLAY` or `NIXOS_OZONE_WL`
|
|
208
|
-
is set, because the Nix Chrome wrapper then opens a real window even under `xvfb-run`.
|
|
209
|
-
|
|
210
|
-
The engine's DHT is on, and it announces the infohash through the relay. So the control can see a
|
|
211
|
-
stray peer (1 peer in two of three runs, 0 bytes each time). This is why the seeders' own upload
|
|
212
|
-
count is part of the claim.
|
|
127
|
+
MIT, see [LICENSE](./LICENSE).
|
package/fkn.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"v":0,"app":"fkn:app:1c7clnsv53zt7dr7q7rcbt455hxauykaxl4gibkuoao24dfmk2eqq","name":"Ripple","version":"0.0.
|
|
1
|
+
{"v":0,"app":"fkn:app:1c7clnsv53zt7dr7q7rcbt455hxauykaxl4gibkuoao24dfmk2eqq","name":"Ripple","version":"0.0.23","key":"vB+5jsZMRya/m8nc1Ba6qNELaua1jUjbGj90dEzuaF0=","issuedAt":1792086367,"perSource":["npm:@banou/ripple"],"url":"https://torrent.fkn.app/","contentsDigest":"TsanK8KS0pclvVi0y/XgBdZhX7HP+xrg6InO5vmfCqU=","sig":"ATtFoey2Db21mRqpz+kIrLOnsnSvTHUgXd8uszgaW1PuQVgz+e631o6/8Ex4aItvJqLfjX1G6O4SP7ZyrnmACQ==","contents":{"name":"@banou/ripple","version":"0.0.23","alg":"sha256","files":{"LICENSE":"ltuFZrZFyUvlPEfixC2u36goYudRNNWKEccYye+T9yY=","README.md":"si/2UKFBl2/DeGj/bcVo9jCbmxSgEpyTU/ysagk9zcM=","_headers":"/3GnajpvIIUp907PK8pIPx9H2wEmJgfEbrC3BKNLlgo=","app.webmanifest":"gIWmbT8RLQZNhLkGKZKO8zLWTEX3F1Dj7RAjIr1zEcQ=","assets/empty-BvRk9kiK.js":"47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=","assets/libtorrent-DY7aMUNo.js":"iL3qzB3N3U7HOI1KoUvlK9MxK43UlFsSEz9qVOG4Qi0=","assets/logo.png":"/u2ZiOWTOJxhO6AxbamaptNtn+kUxlrAiPncAEyqwIM=","assets/logo.svg":"3Ciuaqc0v2MjXzWy8x8yLaYwZsDdM0m1H42lzdQPKTg=","assets/sintel.torrent":"TI/a0EFLR2dUag+S/j1mCmbtwyRxh056HPqXEgMXuEo=","assets/worker-DqSNLC3T.js":"FuWv4sMcQhm+PGsvtCLbv3facgy5FNQ20fzwWe9v3qI=","build-B7te3F-u.js":"R7BWxWTweMWYj6fVhUIDPAcGhMkmlWoq2zyRTe9wFN4=","index.html":"NzDOFOXPmq6jpxfz7igvOO2ti9cLkmZSJm1K3NhBxNE=","index.js":"mdrDb1FqfowHUvuk/33vgi6a0x+qzdIRBBuiFfZG6zs=","jassub/default.woff2":"iGkpkDcHxbsosHzS7taZIbPV71xJxzoN0eGH6/ZUakw=","jassub/fonts.js":"JFrcfQn7ONAEVBPA8HdbKDW9YM3BYoIKvVpwHPdwMhw=","jassub/jassub-assets.js":"LkOyTls8XUtOqkH3PzaW5tDCgxL/e+3exAOIUP0F53Q=","jassub/jassub-worker-modern.wasm":"2ryQh6ZZOS3g/IbRZ8h6CF/HUA8xRabX9L8kMsxlDAM=","jassub/jassub-worker.js":"bakeHatafNtt0otborw+KQ/nnI5yRZ9aH7AUU9dnFS4=","jassub/jassub-worker.wasm":"IEZIYFRcqKpiSk7Fbv9FqzgDd89yR8MfQDY0IHeDbtE=","jassub/worker.js":"GkJ90cst1LkPx1b3tixhjcZBxzQ/o0h3VQMtYQJ3hA4=","libav-jspi.wasm":"2UHGqYWee4PUfv9bW+YRWXeZvhxkqSLVjNKt0Q6bgoE=","libav-worker.js":"UcXP9IK8X+1BraCfqCNNEIVE33wn5EYf9P10KNeZupY=","libav.wasm":"0GJptWeBO93kFB18dlf1YWZspC0QjZ60hEv+4O4fPGU=","package.json":"P67kFT/TCXkXmEyldBbd8IOqedrWZf8wHmBg1KNVwx8=","remote-DvdN47iT.js":"sGSSUKnjoHkfVqnVZ5r1E1NPaQgilhGahOZLnnhXesw=","sw.js":"HehHawnlUVYWifbIdz6Lea/4fh0ouRDqO8FNuIn7sbk=","volume-normalizer-worklet.js":"uERAQN4GaSzVMTY5hfsqvFH25HBOohimVr3HwLUM3WM="},"types":{"LICENSE":"application/octet-stream","README.md":"text/markdown","_headers":"application/octet-stream","app.webmanifest":"application/manifest+json","assets/empty-BvRk9kiK.js":"text/javascript","assets/libtorrent-DY7aMUNo.js":"text/javascript","assets/logo.png":"image/png","assets/logo.svg":"image/svg+xml","assets/sintel.torrent":"application/octet-stream","assets/worker-DqSNLC3T.js":"text/javascript","build-B7te3F-u.js":"text/javascript","index.html":"text/html","index.js":"text/javascript","jassub/default.woff2":"font/woff2","jassub/fonts.js":"text/javascript","jassub/jassub-assets.js":"text/javascript","jassub/jassub-worker-modern.wasm":"application/wasm","jassub/jassub-worker.js":"text/javascript","jassub/jassub-worker.wasm":"application/wasm","jassub/worker.js":"text/javascript","libav-jspi.wasm":"application/wasm","libav-worker.js":"text/javascript","libav.wasm":"application/wasm","package.json":"application/json","remote-DvdN47iT.js":"text/javascript","sw.js":"text/javascript","volume-normalizer-worklet.js":"text/javascript"}},"keys":{"app":"fkn:app:1c7clnsv53zt7dr7q7rcbt455hxauykaxl4gibkuoao24dfmk2eqq","serial":1790278249316,"issuedAt":1790278249,"founder":{"kind":"fkn","seed":"fsqkqngttg4s2gpm23qiz5nonk"},"signer":"fkn-1","name":"Ripple","members":[{"key":"vB+5jsZMRya/m8nc1Ba6qNELaua1jUjbGj90dEzuaF0=","since":1790278249}],"sig":"1ulbdoxt48ngFVZw6Oufds/QhcKE2+KRG7ZQqShFVPeiPyoLGH2IR3m8CmeSOxe3tA7oTmAGKSc4vWZ6pAuFDQ=="}}
|
package/index.js
CHANGED
|
@@ -19077,6 +19077,7 @@ var ky = {
|
|
|
19077
19077
|
audioTracks: [],
|
|
19078
19078
|
selectedAudioTrack: void 0,
|
|
19079
19079
|
selectAudioTrack: () => {},
|
|
19080
|
+
quality: null,
|
|
19080
19081
|
hideUI: !1,
|
|
19081
19082
|
setHideUI: () => {},
|
|
19082
19083
|
hasMedia: !1,
|
|
@@ -20850,65 +20851,74 @@ ${$b}
|
|
|
20850
20851
|
}
|
|
20851
20852
|
|
|
20852
20853
|
`, dx = () => {
|
|
20853
|
-
let e = Y(), t = Y((e) => e.playbackRate), n = Y((e) => e.audioTracks), r = Y((e) => e.selectedAudioTrack), i = Y((e) => e.selectAudioTrack), a = Y((e) => e.volumeNormalizer), [
|
|
20854
|
-
e.setPlaybackRate(t),
|
|
20855
|
-
},
|
|
20856
|
-
e !== void 0 &&
|
|
20857
|
-
},
|
|
20858
|
-
|
|
20854
|
+
let e = Y(), t = Y((e) => e.playbackRate), n = Y((e) => e.audioTracks), r = Y((e) => e.selectedAudioTrack), i = Y((e) => e.selectAudioTrack), a = Y((e) => e.quality), o = Y((e) => e.volumeNormalizer), [s, c] = (0, v.useState)(0), { open: l, toggle: u, containerRef: d, pending: f, failed: p, runSelect: m } = tx({ onReset: () => c(0) }), h = (t) => {
|
|
20855
|
+
e.setPlaybackRate(t), c(0);
|
|
20856
|
+
}, g = (e) => {
|
|
20857
|
+
e !== void 0 && m(e, () => i(e));
|
|
20858
|
+
}, _ = (e) => {
|
|
20859
|
+
a && m(e ?? null, () => a.select(e === void 0 ? null : String(e)));
|
|
20860
|
+
}, y = a ? a.options.length + +!!a.offLabel : 0, b = a?.selectedId == null ? a?.offLabel : a.options.find(({ id: e }) => e === a.selectedId)?.label, x = (e) => () => {
|
|
20861
|
+
c(e);
|
|
20859
20862
|
};
|
|
20860
20863
|
return /* @__PURE__ */ G("div", {
|
|
20861
20864
|
css: ux,
|
|
20862
|
-
ref:
|
|
20865
|
+
ref: d,
|
|
20863
20866
|
children: [
|
|
20864
20867
|
/* @__PURE__ */ W(xb, {
|
|
20865
20868
|
id: "settings",
|
|
20866
|
-
disabled:
|
|
20869
|
+
disabled: l,
|
|
20867
20870
|
text: /* @__PURE__ */ W("button", {
|
|
20868
20871
|
className: "settings",
|
|
20869
20872
|
type: "button",
|
|
20870
|
-
onClick:
|
|
20873
|
+
onClick: u,
|
|
20871
20874
|
"aria-label": "Settings",
|
|
20872
|
-
"aria-expanded":
|
|
20875
|
+
"aria-expanded": l,
|
|
20873
20876
|
children: /* @__PURE__ */ W(Lo, {})
|
|
20874
20877
|
}),
|
|
20875
20878
|
toolTipText: /* @__PURE__ */ W("span", { children: "Settings" })
|
|
20876
20879
|
}),
|
|
20877
|
-
|
|
20880
|
+
l && s === 0 && /* @__PURE__ */ G("div", {
|
|
20878
20881
|
className: "popover menu",
|
|
20879
20882
|
children: [
|
|
20880
20883
|
n.length > 1 ? /* @__PURE__ */ G("div", {
|
|
20881
|
-
onClick:
|
|
20884
|
+
onClick: x(2),
|
|
20882
20885
|
children: [/* @__PURE__ */ W("div", { children: "Audio" }), /* @__PURE__ */ W("div", { children: /* @__PURE__ */ W(go, {}) })]
|
|
20883
20886
|
}) : null,
|
|
20887
|
+
y > 1 ? /* @__PURE__ */ G("div", {
|
|
20888
|
+
onClick: x(3),
|
|
20889
|
+
children: [/* @__PURE__ */ W("div", { children: "Quality" }), /* @__PURE__ */ G("div", { children: [b ? /* @__PURE__ */ W("span", {
|
|
20890
|
+
className: "secondary",
|
|
20891
|
+
children: b
|
|
20892
|
+
}) : null, /* @__PURE__ */ W(go, {})] })]
|
|
20893
|
+
}) : null,
|
|
20884
20894
|
/* @__PURE__ */ G("div", {
|
|
20885
|
-
onClick:
|
|
20895
|
+
onClick: x(1),
|
|
20886
20896
|
children: [/* @__PURE__ */ W("div", { children: "Playback speed" }), /* @__PURE__ */ G("div", { children: [/* @__PURE__ */ W("span", {
|
|
20887
20897
|
className: "secondary",
|
|
20888
20898
|
children: t === 1 ? "(Default)" : t.toFixed(2)
|
|
20889
20899
|
}), /* @__PURE__ */ W(go, {})] })]
|
|
20890
20900
|
}),
|
|
20891
|
-
|
|
20901
|
+
o ? /* @__PURE__ */ G("div", {
|
|
20892
20902
|
role: "switch",
|
|
20893
|
-
"aria-checked":
|
|
20894
|
-
"aria-disabled": !
|
|
20895
|
-
className:
|
|
20903
|
+
"aria-checked": o.available && o.enabled,
|
|
20904
|
+
"aria-disabled": !o.available,
|
|
20905
|
+
className: o.available ? void 0 : "no-hover unavailable",
|
|
20896
20906
|
onClick: () => {
|
|
20897
|
-
|
|
20907
|
+
o.available && o.setEnabled(!o.enabled);
|
|
20898
20908
|
},
|
|
20899
|
-
children: [/* @__PURE__ */ W("div", { children: "Normalize volume" }), /* @__PURE__ */ W("div", { children:
|
|
20909
|
+
children: [/* @__PURE__ */ W("div", { children: "Normalize volume" }), /* @__PURE__ */ W("div", { children: o.available ? /* @__PURE__ */ W("span", { className: o.enabled ? "switch on" : "switch" }) : /* @__PURE__ */ W("span", {
|
|
20900
20910
|
className: "secondary",
|
|
20901
20911
|
children: "Unavailable"
|
|
20902
20912
|
}) })]
|
|
20903
20913
|
}) : null
|
|
20904
20914
|
]
|
|
20905
20915
|
}),
|
|
20906
|
-
|
|
20916
|
+
l && s === 1 && /* @__PURE__ */ G("div", {
|
|
20907
20917
|
className: "popover playback-rate",
|
|
20908
20918
|
children: [
|
|
20909
20919
|
/* @__PURE__ */ G("div", {
|
|
20910
20920
|
className: "back",
|
|
20911
|
-
onClick:
|
|
20921
|
+
onClick: x(0),
|
|
20912
20922
|
children: [/* @__PURE__ */ W(ho, {}), /* @__PURE__ */ W("span", { children: "Playback speed" })]
|
|
20913
20923
|
}),
|
|
20914
20924
|
/* @__PURE__ */ W("div", {
|
|
@@ -20919,33 +20929,43 @@ ${$b}
|
|
|
20919
20929
|
className: "options no-hover",
|
|
20920
20930
|
children: [
|
|
20921
20931
|
/* @__PURE__ */ W("div", {
|
|
20922
|
-
onClick: () =>
|
|
20932
|
+
onClick: () => h(.5),
|
|
20923
20933
|
children: "0.5x"
|
|
20924
20934
|
}),
|
|
20925
20935
|
/* @__PURE__ */ W("div", {
|
|
20926
|
-
onClick: () =>
|
|
20936
|
+
onClick: () => h(1),
|
|
20927
20937
|
children: "1x"
|
|
20928
20938
|
}),
|
|
20929
20939
|
/* @__PURE__ */ W("div", {
|
|
20930
|
-
onClick: () =>
|
|
20940
|
+
onClick: () => h(1.5),
|
|
20931
20941
|
children: "1.5x"
|
|
20932
20942
|
}),
|
|
20933
20943
|
/* @__PURE__ */ W("div", {
|
|
20934
|
-
onClick: () =>
|
|
20944
|
+
onClick: () => h(2),
|
|
20935
20945
|
children: "2x"
|
|
20936
20946
|
})
|
|
20937
20947
|
]
|
|
20938
20948
|
})
|
|
20939
20949
|
]
|
|
20940
20950
|
}),
|
|
20941
|
-
|
|
20951
|
+
l && s === 2 && /* @__PURE__ */ W(ex, {
|
|
20942
20952
|
title: "Audio",
|
|
20943
20953
|
tracks: n,
|
|
20944
20954
|
selected: r,
|
|
20945
|
-
onSelect:
|
|
20946
|
-
onBack:
|
|
20947
|
-
pending:
|
|
20948
|
-
failed:
|
|
20955
|
+
onSelect: g,
|
|
20956
|
+
onBack: x(0),
|
|
20957
|
+
pending: f,
|
|
20958
|
+
failed: p
|
|
20959
|
+
}),
|
|
20960
|
+
l && s === 3 && a && /* @__PURE__ */ W(ex, {
|
|
20961
|
+
title: "Quality",
|
|
20962
|
+
tracks: a.options,
|
|
20963
|
+
selected: a.selectedId ?? void 0,
|
|
20964
|
+
onSelect: _,
|
|
20965
|
+
onBack: x(0),
|
|
20966
|
+
offLabel: a.offLabel,
|
|
20967
|
+
pending: f,
|
|
20968
|
+
failed: p
|
|
20949
20969
|
})
|
|
20950
20970
|
]
|
|
20951
20971
|
});
|
|
@@ -21388,6 +21408,7 @@ ${$b}
|
|
|
21388
21408
|
e = Date.now() + Sx;
|
|
21389
21409
|
};
|
|
21390
21410
|
}, [D]), (0, v.useEffect)(() => () => clearInterval(y.current), []), /* @__PURE__ */ G("div", {
|
|
21411
|
+
className: "control-bar",
|
|
21391
21412
|
css: Cx,
|
|
21392
21413
|
style: { ...c ? {
|
|
21393
21414
|
opacity: "0",
|
|
@@ -21718,47 +21739,51 @@ ${$b}
|
|
|
21718
21739
|
let s = Y(), c = Y((e) => e.hideUI), l = Y((e) => e.setHideUI), u = Y((e) => e.hasMedia), d = Y((e) => e.passThroughPictureInPicture), f = (0, v.useRef)(void 0), p = (0, v.useRef)("mouse"), m = (0, v.useRef)({
|
|
21719
21740
|
x: -1,
|
|
21720
21741
|
y: -1
|
|
21721
|
-
}), h = (0, v.useRef)(null);
|
|
21742
|
+
}), h = (0, v.useRef)(!1), g = (0, v.useRef)(null);
|
|
21722
21743
|
(0, v.useEffect)(() => () => clearTimeout(f.current), []), (0, v.useEffect)(() => {
|
|
21723
21744
|
u || (clearTimeout(f.current), l(!1));
|
|
21724
21745
|
}, [u, l]);
|
|
21725
|
-
let
|
|
21746
|
+
let _ = () => {
|
|
21726
21747
|
u && l(!0);
|
|
21727
|
-
},
|
|
21728
|
-
|
|
21729
|
-
},
|
|
21748
|
+
}, y = (t) => {
|
|
21749
|
+
g.current = t, typeof e == "function" ? e(t) : e && (e.current = t);
|
|
21750
|
+
}, b = () => {
|
|
21730
21751
|
if (s.passThroughPictureInPicture?.armed) return !0;
|
|
21731
21752
|
if (p.current !== "mouse") return !1;
|
|
21732
21753
|
let { x: e, y: t } = m.current;
|
|
21733
|
-
if (!
|
|
21754
|
+
if (!g.current || e < 0) return !1;
|
|
21734
21755
|
let n = document.elementFromPoint(e, t);
|
|
21735
|
-
return
|
|
21736
|
-
},
|
|
21737
|
-
if (
|
|
21738
|
-
f.current = setTimeout(
|
|
21756
|
+
return !n || !g.current.contains(n) || n.closest(".video") ? !1 : !n.closest(".control-bar") || h.current || !!g.current.querySelector(".control-bar [aria-expanded=\"true\"]");
|
|
21757
|
+
}, x = () => {
|
|
21758
|
+
if (b()) {
|
|
21759
|
+
f.current = setTimeout(x, Px);
|
|
21739
21760
|
return;
|
|
21740
21761
|
}
|
|
21741
|
-
|
|
21742
|
-
},
|
|
21743
|
-
l(!1), clearTimeout(f.current), f.current = setTimeout(
|
|
21744
|
-
}
|
|
21762
|
+
_();
|
|
21763
|
+
}, S = () => {
|
|
21764
|
+
l(!1), clearTimeout(f.current), f.current = setTimeout(x, Px);
|
|
21765
|
+
}, C = (e) => {
|
|
21766
|
+
p.current = e.pointerType, e.pointerType === "mouse" && (m.current = {
|
|
21767
|
+
x: e.clientX,
|
|
21768
|
+
y: e.clientY
|
|
21769
|
+
}, S());
|
|
21770
|
+
}, w = (e) => {
|
|
21771
|
+
p.current = e.pointerType, h.current = !0;
|
|
21772
|
+
}, T = () => {
|
|
21773
|
+
h.current = !1;
|
|
21774
|
+
}, E = (0, v.useRef)(!0);
|
|
21745
21775
|
return /* @__PURE__ */ G("div", {
|
|
21746
21776
|
css: Lx,
|
|
21747
|
-
ref:
|
|
21748
|
-
onPointerMove:
|
|
21749
|
-
|
|
21750
|
-
|
|
21751
|
-
|
|
21752
|
-
}, x());
|
|
21753
|
-
},
|
|
21754
|
-
onPointerDown: (e) => {
|
|
21755
|
-
p.current = e.pointerType;
|
|
21756
|
-
},
|
|
21777
|
+
ref: y,
|
|
21778
|
+
onPointerMove: C,
|
|
21779
|
+
onPointerDown: w,
|
|
21780
|
+
onPointerUp: T,
|
|
21781
|
+
onPointerCancel: T,
|
|
21757
21782
|
onMouseOut: (e) => {
|
|
21758
21783
|
let t = e.relatedTarget;
|
|
21759
21784
|
if (t && e.currentTarget.contains(t)) return;
|
|
21760
21785
|
let { left: n, right: r, top: i, bottom: a } = e.currentTarget.getBoundingClientRect();
|
|
21761
|
-
e.clientX >= n && e.clientX < r && e.clientY >= i && e.clientY < a || (clearTimeout(f.current),
|
|
21786
|
+
e.clientX >= n && e.clientX < r && e.clientY >= i && e.clientY < a || (clearTimeout(f.current), _());
|
|
21762
21787
|
},
|
|
21763
21788
|
className: c ? "hide" : "",
|
|
21764
21789
|
tabIndex: a === "player" ? -1 : void 0,
|
|
@@ -21775,23 +21800,27 @@ ${$b}
|
|
|
21775
21800
|
}, e)),
|
|
21776
21801
|
i === !1 ? null : /* @__PURE__ */ W(wx, {
|
|
21777
21802
|
shortcuts: a,
|
|
21778
|
-
root:
|
|
21803
|
+
root: g
|
|
21779
21804
|
}),
|
|
21780
21805
|
/* @__PURE__ */ W(Dx, {}),
|
|
21781
21806
|
/* @__PURE__ */ W(Nx, {}),
|
|
21782
|
-
d?.armed &&
|
|
21807
|
+
d?.armed && g.current ? /* @__PURE__ */ W("div", {
|
|
21783
21808
|
className: "pass-through",
|
|
21784
|
-
style: { clipPath: Ix(d.armed,
|
|
21809
|
+
style: { clipPath: Ix(d.armed, g.current.getBoundingClientRect()) },
|
|
21785
21810
|
onPointerOver: d.disarm
|
|
21786
21811
|
}) : null,
|
|
21787
21812
|
/* @__PURE__ */ G("div", {
|
|
21788
21813
|
className: "video",
|
|
21789
|
-
onClick: () => {
|
|
21814
|
+
onClick: (e) => {
|
|
21790
21815
|
if (p.current === "mouse") {
|
|
21791
|
-
|
|
21816
|
+
if (e.detail > 1) return;
|
|
21817
|
+
E.current = s.paused, s.togglePaused();
|
|
21792
21818
|
return;
|
|
21793
21819
|
}
|
|
21794
|
-
c ?
|
|
21820
|
+
c ? S() : (clearTimeout(f.current), _());
|
|
21821
|
+
},
|
|
21822
|
+
onDoubleClick: () => {
|
|
21823
|
+
p.current === "mouse" && (E.current ? s.pause() : s.play().catch(() => {}), s.toggleFullscreen());
|
|
21795
21824
|
},
|
|
21796
21825
|
children: [t ? /* @__PURE__ */ W("video", {
|
|
21797
21826
|
ref: t,
|
|
@@ -21882,7 +21911,7 @@ ${$b}
|
|
|
21882
21911
|
V({ passThroughPictureInPicture: B });
|
|
21883
21912
|
}, [V, B]);
|
|
21884
21913
|
let te = n?.subtitles?.selection, ne = n?.audioTracks?.selection;
|
|
21885
|
-
|
|
21914
|
+
(0, v.useEffect)(() => {
|
|
21886
21915
|
te && V({
|
|
21887
21916
|
subtitleTracks: te.options.map(({ id: e, label: t, disabled: n }) => ({
|
|
21888
21917
|
id: e,
|
|
@@ -21903,7 +21932,11 @@ ${$b}
|
|
|
21903
21932
|
selectedAudioTrack: ne.selectedId ?? void 0,
|
|
21904
21933
|
selectAudioTrack: (e) => ne.select(String(e))
|
|
21905
21934
|
});
|
|
21906
|
-
}, [V, ne])
|
|
21935
|
+
}, [V, ne]);
|
|
21936
|
+
let re = n?.quality?.selection ?? null;
|
|
21937
|
+
return (0, v.useEffect)(() => {
|
|
21938
|
+
V({ quality: re });
|
|
21939
|
+
}, [V, re]), (0, v.useEffect)(() => {
|
|
21907
21940
|
a && V({ chapters: a });
|
|
21908
21941
|
}, [V, a]), /* @__PURE__ */ W(zx, {
|
|
21909
21942
|
ref: m,
|
|
@@ -44756,15 +44789,15 @@ var nK = () => window.location.origin + "/?magnet=%s", rK = () => typeof window
|
|
|
44756
44789
|
}),
|
|
44757
44790
|
/* @__PURE__ */ G("a", {
|
|
44758
44791
|
className: "build",
|
|
44759
|
-
href: "https://github.com/banou26/ripple/commit/
|
|
44792
|
+
href: "https://github.com/banou26/ripple/commit/a812ac3ef4e115708bf4407c58d4962c60ccbaa3",
|
|
44760
44793
|
target: "_blank",
|
|
44761
44794
|
rel: "noreferrer",
|
|
44762
|
-
...mV("commit
|
|
44795
|
+
...mV("commit a812ac3ef4e115708bf4407c58d4962c60ccbaa3"),
|
|
44763
44796
|
children: [
|
|
44764
44797
|
"v",
|
|
44765
|
-
"0.0.
|
|
44798
|
+
"0.0.23",
|
|
44766
44799
|
" · ",
|
|
44767
|
-
"
|
|
44800
|
+
"a812ac3"
|
|
44768
44801
|
]
|
|
44769
44802
|
}),
|
|
44770
44803
|
/* @__PURE__ */ G("div", {
|
|
@@ -44994,13 +45027,15 @@ var nK = () => window.location.origin + "/?magnet=%s", rK = () => typeof window
|
|
|
44994
45027
|
/* @__PURE__ */ W("h1", { children: "Privacy" }),
|
|
44995
45028
|
/* @__PURE__ */ W("div", {
|
|
44996
45029
|
className: "updated",
|
|
44997
|
-
children: "Last updated
|
|
45030
|
+
children: "Last updated 16 October 2026"
|
|
44998
45031
|
}),
|
|
44999
45032
|
/* @__PURE__ */ W("p", { children: "Ripple needs no account and runs no analytics. Used without an account, it keeps everything on your device and stores nothing about you on any server." }),
|
|
45000
45033
|
/* @__PURE__ */ W("h2", { children: "What Ripple stores on your device" }),
|
|
45001
|
-
/* @__PURE__ */ W("p", { children: "Your torrent list
|
|
45002
|
-
/* @__PURE__ */ W("
|
|
45003
|
-
/* @__PURE__ */ W("
|
|
45034
|
+
/* @__PURE__ */ W("p", { children: "Your torrent list, each torrent’s resume data and thumbnail, and your settings (such as speed limits and how the list is sorted) live in your browser’s storage (IndexedDB and local storage). Downloaded files live in your browser’s storage (OPFS). If you choose a folder on your disk, finished downloads are saved there. Once you sign in, Ripple also remembers which FKN account your list belongs to." }),
|
|
45035
|
+
/* @__PURE__ */ W("p", { children: "Ripple never stores your files on FKN or on any server. Like any BitTorrent app, while a torrent is active it shares the pieces it has with the other peers of that torrent." }),
|
|
45036
|
+
/* @__PURE__ */ W("h2", { children: "Library sync when signed in" }),
|
|
45037
|
+
/* @__PURE__ */ W("p", { children: "If you connect an FKN account, Ripple keeps a copy of your library in that account’s FKN cloud storage, so it follows you across devices. For each torrent that is its magnet link, name, size and file list, where it is saved, when you added it, and its totals (data downloaded and uploaded, time active and seeding). It also keeps the torrents you removed and when, so other devices remove them too, and one thumbnail picture per torrent. It never syncs the downloaded files themselves." }),
|
|
45038
|
+
/* @__PURE__ */ W("p", { children: "All of it is encrypted in your browser before it is uploaded, so FKN stores it without being able to read it. File names stay visible to FKN, and each thumbnail’s file name is its torrent’s info hash. Without an account connected, nothing is synced." }),
|
|
45004
45039
|
/* @__PURE__ */ W("h2", { children: "The relay and your IP address" }),
|
|
45005
45040
|
/* @__PURE__ */ G("p", { children: [
|
|
45006
45041
|
"Peer connections are tunneled through the FKN relay rather than opened directly, so the other peers in a swarm see the relay’s IP address, not yours. The relay forwards traffic without storing it. Usage metering for the free and premium tiers is anonymized with daily-rotating keys and purged within about a day, so past transfers cannot be mapped back to a person, by us or anyone else. The full details are in the",
|
|
@@ -45014,8 +45049,9 @@ var nK = () => window.location.origin + "/?magnet=%s", rK = () => typeof window
|
|
|
45014
45049
|
"."
|
|
45015
45050
|
] }),
|
|
45016
45051
|
/* @__PURE__ */ W("h2", { children: "Your control" }),
|
|
45052
|
+
/* @__PURE__ */ W("p", { children: "Removing a torrent deletes its data from your browser’s storage. Files in a folder on your disk, and the files you made a torrent from, stay where they are. Clearing this site’s data in your browser removes everything Ripple stored in your browser." }),
|
|
45017
45053
|
/* @__PURE__ */ G("p", { children: [
|
|
45018
|
-
"
|
|
45054
|
+
"Ripple itself has no account to delete. Disconnecting your FKN account stops the sync. Removing a torrent also removes it from the synced list, but its synced thumbnail stays in your account. Deleting your FKN account at",
|
|
45019
45055
|
" ",
|
|
45020
45056
|
/* @__PURE__ */ W("a", {
|
|
45021
45057
|
href: "https://fkn.app",
|
|
@@ -45023,7 +45059,8 @@ var nK = () => window.location.origin + "/?magnet=%s", rK = () => typeof window
|
|
|
45023
45059
|
rel: "noreferrer noopener",
|
|
45024
45060
|
children: "fkn.app"
|
|
45025
45061
|
}),
|
|
45026
|
-
"
|
|
45062
|
+
" ",
|
|
45063
|
+
"deletes everything Ripple stored in it."
|
|
45027
45064
|
] }),
|
|
45028
45065
|
/* @__PURE__ */ W("h2", { children: "Contact" }),
|
|
45029
45066
|
/* @__PURE__ */ G("p", { children: [
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@banou/ripple",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.23",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -10,6 +10,8 @@
|
|
|
10
10
|
"fkn",
|
|
11
11
|
"fkn-type:app"
|
|
12
12
|
],
|
|
13
|
+
"license": "MIT",
|
|
14
|
+
"description": "Download and stream torrents in your browser",
|
|
13
15
|
"fkn": {
|
|
14
16
|
"app": "fkn:app:1c7clnsv53zt7dr7q7rcbt455hxauykaxl4gibkuoao24dfmk2eqq",
|
|
15
17
|
"signed": true,
|
package/sw.js
CHANGED
|
@@ -8,7 +8,7 @@ const live = new Map()
|
|
|
8
8
|
/* Stamped at build time. These are the dev defaults and must never reach production: a worker whose
|
|
9
9
|
BUILD never changes is never reinstalled, so the update this feature announces would never fire. */
|
|
10
10
|
const MODE = "cache" // @stamp:mode
|
|
11
|
-
const BUILD = "
|
|
11
|
+
const BUILD = "415e9074236b44ec" // @stamp:build
|
|
12
12
|
const MANIFEST = [["/assets/empty-BvRk9kiK.js","e3b0c44298fc1c14"],["/assets/libtorrent-DY7aMUNo.js","88bdeacc1dcddd4e"],["/assets/worker-DqSNLC3T.js","16e5afe2c31c4219"],["/build-B7te3F-u.js","47b056c564f078c5"],["/remote-DvdN47iT.js","b0649250a9e3a079"],["/volume-normalizer-worklet.js","b8444040de06692c"]] // @stamp:manifest
|
|
13
13
|
|
|
14
14
|
const CACHE = 'ripple-assets-' + BUILD
|