@banou/ripple 0.0.4 → 0.0.7
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/README.md +141 -3
- package/build/_headers +14 -0
- package/build/api-Jq4OSTp1-B4uxQLLk.js +338 -0
- package/build/app.webmanifest +50 -65
- package/build/assets/empty-BvRk9kiK.js +0 -0
- package/build/assets/libtorrent-DS_9Yo8M.js +6 -0
- package/build/assets/sintel.torrent +0 -0
- package/build/assets/worker-SJZDGuG-.js +23 -0
- package/build/build-DEOZ3A7n.js +1846 -0
- package/build/index.js +44138 -33306
- package/build/jassub/default.woff2 +0 -0
- package/build/jassub/fonts.js +1 -0
- package/build/jassub/jassub-assets.js +0 -0
- package/build/jassub/jassub-worker-modern.wasm +0 -0
- package/build/jassub/jassub-worker.js +1 -0
- package/build/jassub/jassub-worker.wasm +0 -0
- package/build/jassub/worker.js +148 -0
- package/build/libav-jspi.wasm +0 -0
- package/build/libav-worker.js +4823 -1564
- package/build/libav.wasm +0 -0
- package/build/remote-BLaEXLm6.js +209 -0
- package/build/sw.js +192 -0
- package/package.json +96 -81
- package/build/assets/index-be95001c.js +0 -211
- package/build/assets/index-d5b720ec.js +0 -109
- package/build/assets/io-worker-bde41815.js +0 -17681
- package/build/index-3576f032.js +0 -14881
- package/build/index-65b2197a.js +0 -116
- package/build/index-9a320571.js +0 -107
- package/build/io-worker-190cc8d7.js +0 -2862
- package/build/jassub-worker-modern.wasm +0 -0
- package/build/jassub-worker.js +0 -15
- package/build/shared-worker.js +0 -13
- package/build/worker.js +0 -38
package/README.md
CHANGED
|
@@ -1,3 +1,141 @@
|
|
|
1
|
-
# Ripple, the torrent app that respect your privacy
|
|
2
|
-
|
|
3
|
-
THE app that allows you to download torrents and stream video files from the safety of your browser!
|
|
1
|
+
# Ripple, the torrent app that respect your privacy
|
|
2
|
+
|
|
3
|
+
THE app that allows you to download torrents and stream video files from the safety of your browser!
|
|
4
|
+
|
|
5
|
+
## Embedding
|
|
6
|
+
|
|
7
|
+
`/watch` and `/download` are the pages another site puts in an iframe. Both take a magnet and the
|
|
8
|
+
same query; the path chooses which one renders. `/watch` plays the file, `/download` delivers it.
|
|
9
|
+
|
|
10
|
+
| param | value | meaning |
|
|
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
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
The path, then which files, then the packed torrent, then `f`. The torrent used to lead, from when
|
|
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**.
|
|
61
|
+
|
|
62
|
+
It is a preview and Ripple treats it as one. It never decides what gets downloaded: the button stays
|
|
63
|
+
disabled until real metadata arrives, the download itself is resolved against that metadata, and the
|
|
64
|
+
per-file buttons are not rendered at all while the list is only the link's claim. A link that
|
|
65
|
+
describes a torrent inaccurately therefore costs a reader a wrong line on screen for a few seconds,
|
|
66
|
+
and can never cost them the wrong file on disk.
|
|
67
|
+
|
|
68
|
+
Ripple writes it only on `/download`, only when it has the list, and only when it fits: a
|
|
69
|
+
12-episode season costs about 172 characters and a 48-file season about 416. Past a budget it is
|
|
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.
|
|
72
|
+
|
|
73
|
+
Putting the whole `.torrent` in the URL instead does not work, and the reason is arithmetic rather
|
|
74
|
+
than encoding: piece hashes are around 94% of a torrent and are 20 bytes of SHA1 per piece, so they
|
|
75
|
+
are incompressible. A 12-episode season is a 28,512-character URL and a 40 GB remux is 68,676. The
|
|
76
|
+
file list is the part that is both small and worth having.
|
|
77
|
+
|
|
78
|
+
### `/watch`
|
|
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.
|
|
87
|
+
|
|
88
|
+
**This route was `/embed` until 2026-09-09 and the old path is gone rather than redirected.** A link
|
|
89
|
+
written against it 404s to the SPA shell; rewrite it as `/watch` with the query untouched.
|
|
90
|
+
|
|
91
|
+
### `/download`
|
|
92
|
+
|
|
93
|
+
A download page: the release name, the size of the selection, and one button. One file is delivered
|
|
94
|
+
as that file; anything more is delivered as a single `.zip`, written straight through to the
|
|
95
|
+
browser's own downloader without ever being held in memory.
|
|
96
|
+
|
|
97
|
+
```
|
|
98
|
+
/download?magnet=<base64> the whole torrent, as a zip
|
|
99
|
+
/download?magnet=<base64>&files=3 just file 3
|
|
100
|
+
/download?magnet=<base64>&files=0-4 files 0 to 4 inclusive, as a zip
|
|
101
|
+
/download?magnet=<base64>&files=0,2,5 those three, as a zip
|
|
102
|
+
/download?magnet=<base64>&fileIndex=3 same as files=3, so a watch URL becomes a download by
|
|
103
|
+
swapping /watch for /download and nothing else
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
`files` outranks `fileIndex`. Indices the torrent does not have are dropped rather than clamped, and
|
|
107
|
+
a selection that resolves to nothing says so instead of quietly downloading the whole torrent.
|
|
108
|
+
|
|
109
|
+
### What an embedder has to grant
|
|
110
|
+
|
|
111
|
+
**The frame needs `allow-downloads` if it is sandboxed at all.** Sandbox flags on a nested browsing
|
|
112
|
+
context are the union of the parent's set and the child's, so a flag the embedder withheld cannot be
|
|
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.
|
|
116
|
+
|
|
117
|
+
```html
|
|
118
|
+
<iframe src="https://torrent.fkn.app/download?magnet=..."
|
|
119
|
+
sandbox="allow-scripts allow-same-origin allow-downloads"></iframe>
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Omitting `sandbox` entirely works too. When the page detects it is framed by another origin it
|
|
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.
|
|
128
|
+
|
|
129
|
+
## Tests
|
|
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
|
+
|
|
140
|
+
The playwright suites run headful on purpose: headless Chromium stalls the engine at a flat 0 B/s in
|
|
141
|
+
every topology, which makes anything torrent-shaped unmeasurable rather than merely slow.
|
package/build/_headers
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# Cloudflare Pages serves this verbatim. Path order matters: the first matching block wins.
|
|
2
|
+
|
|
3
|
+
# /add asks a person to agree to something on behalf of whatever site linked them here, so it must
|
|
4
|
+
# never render inside a frame. A page that can size and position an invisible iframe can put the Add
|
|
5
|
+
# button under the visitor's cursor while they believe they are clicking something else, and nothing
|
|
6
|
+
# written inside the frame helps, because the surrounding page chooses what is visible.
|
|
7
|
+
#
|
|
8
|
+
# The page checks `window.top !== window.self` as well. This header is the half that works before any
|
|
9
|
+
# script runs, and the check is the half that survives a host serving these files without it.
|
|
10
|
+
/add
|
|
11
|
+
X-Frame-Options: DENY
|
|
12
|
+
Content-Security-Policy: frame-ancestors 'none'
|
|
13
|
+
|
|
14
|
+
# NOT /watch or /download, deliberately: being framed by another site is the entire point of both.
|
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
import { a as e } from "./build-DEOZ3A7n.js";
|
|
2
|
+
//#region \0rolldown/runtime.js
|
|
3
|
+
var t = Object.create, n = Object.defineProperty, r = Object.getOwnPropertyDescriptor, i = Object.getOwnPropertyNames, a = Object.getPrototypeOf, o = Object.prototype.hasOwnProperty, s = (e, t) => () => (t || (e((t = { exports: {} }).exports, t), e = null), t.exports), c = (e, t) => {
|
|
4
|
+
let r = {};
|
|
5
|
+
for (var i in e) n(r, i, {
|
|
6
|
+
get: e[i],
|
|
7
|
+
enumerable: !0
|
|
8
|
+
});
|
|
9
|
+
return t || n(r, Symbol.toStringTag, { value: "Module" }), r;
|
|
10
|
+
}, l = (e, t, a, s) => {
|
|
11
|
+
if (t && typeof t == "object" || typeof t == "function") for (var c = i(t), l = 0, u = c.length, d; l < u; l++) d = c[l], !o.call(e, d) && d !== a && n(e, d, {
|
|
12
|
+
get: ((e) => t[e]).bind(null, d),
|
|
13
|
+
enumerable: !(s = r(t, d)) || s.enumerable
|
|
14
|
+
});
|
|
15
|
+
return e;
|
|
16
|
+
}, u = (e, r, i) => (i = e == null ? {} : t(a(e)), l(r || !e || !e.__esModule ? n(i, "default", {
|
|
17
|
+
value: e,
|
|
18
|
+
enumerable: !0
|
|
19
|
+
}) : i, e)), d = Object.defineProperty, f = (e, t) => {
|
|
20
|
+
let n = {};
|
|
21
|
+
for (var r in e) d(n, r, {
|
|
22
|
+
get: e[r],
|
|
23
|
+
enumerable: !0
|
|
24
|
+
});
|
|
25
|
+
return t || d(n, Symbol.toStringTag, { value: "Module" }), n;
|
|
26
|
+
}, ee = /* @__PURE__ */ c({
|
|
27
|
+
a: () => Q,
|
|
28
|
+
i: () => Y,
|
|
29
|
+
n: () => Ce,
|
|
30
|
+
o: () => J,
|
|
31
|
+
r: () => $,
|
|
32
|
+
t: () => Z
|
|
33
|
+
}), te = 1e4, ne = 16, p = (e) => {
|
|
34
|
+
let t;
|
|
35
|
+
try {
|
|
36
|
+
t = e.contentWindow;
|
|
37
|
+
} catch {
|
|
38
|
+
return !0;
|
|
39
|
+
}
|
|
40
|
+
if (!t) return !0;
|
|
41
|
+
try {
|
|
42
|
+
let e = t.location.href;
|
|
43
|
+
return e !== "" && e !== "about:blank";
|
|
44
|
+
} catch {
|
|
45
|
+
return !0;
|
|
46
|
+
}
|
|
47
|
+
}, m = (e, t = te) => new Promise((n) => {
|
|
48
|
+
if (p(e)) return n();
|
|
49
|
+
let r = () => {
|
|
50
|
+
clearInterval(i), clearTimeout(a), e.removeEventListener("load", r), n();
|
|
51
|
+
}, i = setInterval(() => {
|
|
52
|
+
p(e) && r();
|
|
53
|
+
}, ne), a = setTimeout(r, t);
|
|
54
|
+
e.addEventListener("load", r);
|
|
55
|
+
}), re = 32, ie = 8, ae = 120, h = typeof CSS < "u" && CSS.supports("clip-path", "path('M0 0H1V1H0Z')"), g = typeof CSS < "u" && CSS.supports("clip-path", "polygon(nonzero, 0px 0px, 1px 0px, 1px 1px)"), oe = (e) => {
|
|
56
|
+
let t = e.style;
|
|
57
|
+
t.setProperty("display", "block", "important"), t.setProperty("position", "fixed", "important"), t.setProperty("top", "0", "important"), t.setProperty("left", "0", "important"), t.setProperty("width", "100%", "important"), t.setProperty("height", "100%", "important"), t.setProperty("border", "none", "important"), t.setProperty("padding", "0", "important"), t.setProperty("box-sizing", "border-box", "important"), t.setProperty("clip-path", "inset(100%)", "important"), t.setProperty("background", "transparent"), t.setProperty("color-scheme", "normal"), t.setProperty("z-index", "2147483647"), e.setAttribute("title", "FKN");
|
|
58
|
+
}, se = ({ x: e, y: t, width: n, height: r, radius: i }) => {
|
|
59
|
+
if (!i) return `M${e} ${t}h${n}v${r}h${-n}Z`;
|
|
60
|
+
let a = Math.min(n, r) / 2, [o, s, c, l] = i.map((e) => Math.max(0, Math.min(e, a)));
|
|
61
|
+
return `M${e + o} ${t}h${n - o - s}a${s} ${s} 0 0 1 ${s} ${s}v${r - s - c}a${c} ${c} 0 0 1 ${-c} ${c}h${-(n - c - l)}a${l} ${l} 0 0 1 ${-l} ${-l}v${-(r - l - o)}a${o} ${o} 0 0 1 ${o} ${-o}Z`;
|
|
62
|
+
}, ce = () => h && g, _ = ({ x: e, y: t, width: n, height: r }) => [
|
|
63
|
+
[e, t],
|
|
64
|
+
[e + n, t],
|
|
65
|
+
[e + n, t + r],
|
|
66
|
+
[e, t + r],
|
|
67
|
+
[e, t]
|
|
68
|
+
], le = (e) => {
|
|
69
|
+
let t = e[0];
|
|
70
|
+
if (!t) return [];
|
|
71
|
+
let n = _(t)[0], r = [];
|
|
72
|
+
return e.forEach((t, i) => {
|
|
73
|
+
r.push(..._(t)), i > 0 && i < e.length - 1 && r.push(n);
|
|
74
|
+
}), r;
|
|
75
|
+
}, v = (e) => `polygon(nonzero, ${le(e).map(([e, t]) => `${e}px ${t}px`).join(", ")})`, ue = ({ x: e, y: t, width: n, height: r, radius: i }, a) => {
|
|
76
|
+
let o = `${t}px ${a.width - e - n}px ${a.height - t - r}px ${e}px`;
|
|
77
|
+
return i ? `inset(${o} round ${i.map((e) => `${e}px`).join(" ")})` : `inset(${o})`;
|
|
78
|
+
}, y = (e, t, n) => {
|
|
79
|
+
if (t) return "none";
|
|
80
|
+
if (e.length === 0) return "inset(100%)";
|
|
81
|
+
let r = e.length === 1 ? e[0] : void 0;
|
|
82
|
+
return r ? ue(r, n) : g && e.length <= 5 && !e.some((e) => e.radius) ? v(e) : `path('${e.map(se).join("")}')`;
|
|
83
|
+
}, de = (e) => {
|
|
84
|
+
if (e.length < 2) return null;
|
|
85
|
+
let t = Math.min(...e.map((e) => e.x)), n = Math.min(...e.map((e) => e.y));
|
|
86
|
+
return {
|
|
87
|
+
x: t,
|
|
88
|
+
y: n,
|
|
89
|
+
width: Math.max(...e.map((e) => e.x + e.width)) - t,
|
|
90
|
+
height: Math.max(...e.map((e) => e.y + e.height)) - n
|
|
91
|
+
};
|
|
92
|
+
}, b = (e, t, n) => t >= e.x && t < e.x + e.width && n >= e.y && n < e.y + e.height, x = 16, S = (e, t, n) => t === 1 ? e * x : t === 2 ? e * n : e, C = (e, t, n) => {
|
|
93
|
+
let r = getComputedStyle(e);
|
|
94
|
+
return n !== 0 && /auto|scroll/.test(r.overflowY) && e.scrollHeight > e.clientHeight ? !0 : t !== 0 && /auto|scroll/.test(r.overflowX) && e.scrollWidth > e.clientWidth;
|
|
95
|
+
}, fe = (e, t, n) => {
|
|
96
|
+
for (let r = e; r; r = r.parentElement) if (C(r, t, n)) return r;
|
|
97
|
+
return document.scrollingElement;
|
|
98
|
+
}, w = null, T = null, E = (e) => ({
|
|
99
|
+
width: e.offsetWidth || window.innerWidth,
|
|
100
|
+
height: e.offsetHeight || window.innerHeight
|
|
101
|
+
}), D = (e) => {
|
|
102
|
+
if (!T || !w || !b(T, e.clientX, e.clientY)) return;
|
|
103
|
+
let t = document.elementFromPoint(e.clientX, e.clientY);
|
|
104
|
+
if (!t || t === w) return;
|
|
105
|
+
let n = S(e.deltaX, e.deltaMode, window.innerWidth), r = S(e.deltaY, e.deltaMode, window.innerHeight), i = fe(t, n, r);
|
|
106
|
+
i && (i.scrollBy({
|
|
107
|
+
left: n,
|
|
108
|
+
top: r,
|
|
109
|
+
behavior: "instant"
|
|
110
|
+
}), e.cancelable && e.preventDefault());
|
|
111
|
+
}, pe = (e) => {
|
|
112
|
+
let t = T !== null;
|
|
113
|
+
T = e, t !== (e !== null) && (e ? window.addEventListener("wheel", D, {
|
|
114
|
+
capture: !0,
|
|
115
|
+
passive: !1
|
|
116
|
+
}) : window.removeEventListener("wheel", D, { capture: !0 }));
|
|
117
|
+
}, O = 0, k = (e) => {
|
|
118
|
+
if (e === O) return;
|
|
119
|
+
O = e;
|
|
120
|
+
let t = document.documentElement.style;
|
|
121
|
+
e > 0 ? (t.setProperty("margin-top", `${e}px`, "important"), t.setProperty("--fkn-inset-top", `${e}px`)) : (t.removeProperty("margin-top"), t.removeProperty("--fkn-inset-top"));
|
|
122
|
+
}, A = (e, { modal: t, rects: n, inset: r }, i) => {
|
|
123
|
+
if (k(r), !h) {
|
|
124
|
+
let r = t || n.length > 0;
|
|
125
|
+
e.style.setProperty("pointer-events", r ? "auto" : "none", "important"), e.style.setProperty("visibility", r ? "visible" : "hidden", "important");
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
let a = y(n, t, i), o = a === "none" || CSS.supports("clip-path", a) ? a : "inset(100%)";
|
|
129
|
+
e.style.setProperty("clip-path", o, "important"), pe(t || o !== a ? null : de(n));
|
|
130
|
+
}, j = (e) => {
|
|
131
|
+
if (typeof e != "object" || !e) return 0;
|
|
132
|
+
let { top: t } = e;
|
|
133
|
+
return typeof t != "number" || !Number.isFinite(t) || t <= 0 ? 0 : Math.min(Math.round(t), ae);
|
|
134
|
+
}, M = (e) => {
|
|
135
|
+
if (!(!Array.isArray(e) || e.length !== 4) && e.every((e) => typeof e == "number" && Number.isFinite(e) && e >= 0)) return e.some((e) => e > 0) ? e : void 0;
|
|
136
|
+
}, N = (e, t) => {
|
|
137
|
+
if (typeof e != "object" || !e) return null;
|
|
138
|
+
let { x: n, y: r, width: i, height: a } = e;
|
|
139
|
+
if (![
|
|
140
|
+
n,
|
|
141
|
+
r,
|
|
142
|
+
i,
|
|
143
|
+
a
|
|
144
|
+
].every((e) => typeof e == "number" && Number.isFinite(e))) return null;
|
|
145
|
+
let o = t.width, s = t.height, c = Math.max(0, Math.min(Math.round(n), o)), l = Math.max(0, Math.min(Math.round(r), s)), u = Math.max(c, Math.min(Math.round(n + i), o)), d = Math.max(l, Math.min(Math.round(r + a), s)), f = M(e.radius);
|
|
146
|
+
return {
|
|
147
|
+
x: c,
|
|
148
|
+
y: l,
|
|
149
|
+
width: u - c,
|
|
150
|
+
height: d - l,
|
|
151
|
+
...f ? { radius: f } : {}
|
|
152
|
+
};
|
|
153
|
+
}, P = {
|
|
154
|
+
"not-rendered": "this page is not painting it at all, so it or an ancestor is display:none, or the frame was moved out of the layout",
|
|
155
|
+
clipped: "it is clipped away or positioned outside the viewport",
|
|
156
|
+
"not-shown": "it is rendered but would not be seen, so either something is drawn over it, or it or an ancestor has opacity below 1, visibility:hidden, or a filter"
|
|
157
|
+
}, F = (e) => Array.isArray(e) ? e.slice(0, ie).flatMap((e) => {
|
|
158
|
+
if (typeof e != "object" || !e) return [];
|
|
159
|
+
let { kind: t, cause: n, level: r } = e;
|
|
160
|
+
return typeof t != "string" || t.length > 40 || typeof n != "string" || !(n in P) || r !== "full" && r !== "liveness" ? [] : [{
|
|
161
|
+
kind: t,
|
|
162
|
+
cause: n,
|
|
163
|
+
level: r
|
|
164
|
+
}];
|
|
165
|
+
}) : [], I = /* @__PURE__ */ new Set(), L = (e) => {
|
|
166
|
+
for (let { kind: t, cause: n, level: r } of e) I.has(t) || (I.add(t), console.error(`[FKN] The FKN ${JSON.stringify(t)} is not visible on this page: ${P[n]}. Nobody using this page can see or answer it.
|
|
167
|
+
@fkn/lib draws its UI inside <iframe title="FKN"> and sets display, position, top, left, width, height, border, padding, box-sizing, clip-path and z-index on that element. Leave those as they are, then check for an ancestor with opacity, visibility, display or a clipping transform, and for an element stacked above it.` + (r === "liveness" ? "\nThis browser reports limited detail, so the cause above is a best guess." : "")));
|
|
168
|
+
}, R = (e, t) => {
|
|
169
|
+
if (typeof e != "object" || !e) return null;
|
|
170
|
+
let n = e, r = (Array.isArray(n.rects) ? n.rects.slice(0, re) : []).map((e) => N(e, t)).filter((e) => e !== null && e.width > 0 && e.height > 0);
|
|
171
|
+
return {
|
|
172
|
+
modal: n.modal === !0,
|
|
173
|
+
rects: r,
|
|
174
|
+
hidden: F(n.hidden),
|
|
175
|
+
inset: j(n.inset)
|
|
176
|
+
};
|
|
177
|
+
}, z = () => w !== null, B = null, V = null, H = () => {
|
|
178
|
+
if (!w || B === null) return;
|
|
179
|
+
let e = E(w), t = R(B, e);
|
|
180
|
+
t && A(w, t, e);
|
|
181
|
+
}, me = (e) => {
|
|
182
|
+
typeof ResizeObserver > "u" || (V ??= new ResizeObserver(H), V.disconnect(), V.observe(e));
|
|
183
|
+
}, he = (e) => {
|
|
184
|
+
oe(e), A(e, {
|
|
185
|
+
modal: !1,
|
|
186
|
+
rects: [],
|
|
187
|
+
hidden: [],
|
|
188
|
+
inset: 0
|
|
189
|
+
}, E(e)), w = e, me(e);
|
|
190
|
+
}, ge = (e) => {
|
|
191
|
+
if (!w) return;
|
|
192
|
+
let t = E(w), n = R(e, t);
|
|
193
|
+
n && (B = e, A(w, n, t), L(n.hidden));
|
|
194
|
+
}, U = "https://fkn.app", W = typeof globalThis < "u" && globalThis.document ? globalThis.document : void 0, G = typeof globalThis < "u" && globalThis.crossOriginIsolated === !0, K = `${U}/api${G ? "?coi=1" : ""}`, _e = "fkn-api-port", ve = "fkn-api-port-ack", ye = 2e3, be = () => {
|
|
195
|
+
if (!W || typeof window > "u" || window.parent === window) return Promise.resolve(void 0);
|
|
196
|
+
let e;
|
|
197
|
+
try {
|
|
198
|
+
e = window.parent;
|
|
199
|
+
} catch {
|
|
200
|
+
return Promise.resolve(void 0);
|
|
201
|
+
}
|
|
202
|
+
let t = new MessageChannel();
|
|
203
|
+
return new Promise((n) => {
|
|
204
|
+
let r = (e) => {
|
|
205
|
+
if (clearTimeout(a), t.port1.removeEventListener("message", i), !e) try {
|
|
206
|
+
t.port1.close();
|
|
207
|
+
} catch {}
|
|
208
|
+
n(e);
|
|
209
|
+
}, i = (e) => {
|
|
210
|
+
e.data?.type === ve && r(t.port1);
|
|
211
|
+
}, a = setTimeout(() => r(void 0), ye);
|
|
212
|
+
t.port1.addEventListener("message", i), t.port1.start();
|
|
213
|
+
try {
|
|
214
|
+
e.postMessage({ type: _e }, U, [t.port2]);
|
|
215
|
+
} catch {
|
|
216
|
+
r(void 0);
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
}, q, xe = () => {
|
|
220
|
+
if (q || !W) return q;
|
|
221
|
+
let e = W.querySelector(`iframe[src="${K}"]`);
|
|
222
|
+
if (e) q = e;
|
|
223
|
+
else {
|
|
224
|
+
let e = W.createElement("iframe");
|
|
225
|
+
G && e.setAttribute("allow", "cross-origin-isolated"), e.src = K, W.body.appendChild(e), q = e;
|
|
226
|
+
}
|
|
227
|
+
return he(q), q;
|
|
228
|
+
}, J = (async () => {
|
|
229
|
+
if (!W) return;
|
|
230
|
+
let e = await be();
|
|
231
|
+
if (e) return {
|
|
232
|
+
receive: e,
|
|
233
|
+
emit: e
|
|
234
|
+
};
|
|
235
|
+
let t = xe();
|
|
236
|
+
if (!t) return;
|
|
237
|
+
await m(t);
|
|
238
|
+
let n = t.contentWindow;
|
|
239
|
+
return n ? {
|
|
240
|
+
receive: window,
|
|
241
|
+
emit: n
|
|
242
|
+
} : void 0;
|
|
243
|
+
})(), Se = (e) => {
|
|
244
|
+
let t, n = () => {}, r = new Promise((e) => {
|
|
245
|
+
n = e;
|
|
246
|
+
}), i = /* @__PURE__ */ new Set(), a = /* @__PURE__ */ new Set(), o = 0, s = 0;
|
|
247
|
+
(async () => {
|
|
248
|
+
for await (let r of e) {
|
|
249
|
+
let e = (t?.epoch ?? 0) + 1;
|
|
250
|
+
t = {
|
|
251
|
+
remote: r,
|
|
252
|
+
epoch: e
|
|
253
|
+
}, e === 1 && n(r);
|
|
254
|
+
let o = [...a];
|
|
255
|
+
a.clear();
|
|
256
|
+
let s = /* @__PURE__ */ Error("FKN: the broker was replaced while this call was pending; retry it");
|
|
257
|
+
for (let e of o) try {
|
|
258
|
+
e(s);
|
|
259
|
+
} catch {}
|
|
260
|
+
for (let t of [...i]) try {
|
|
261
|
+
t(r, e);
|
|
262
|
+
} catch {}
|
|
263
|
+
}
|
|
264
|
+
})().catch(() => {});
|
|
265
|
+
let c = async (e, n) => {
|
|
266
|
+
let i = t?.remote ?? await r;
|
|
267
|
+
for (let t of e.slice(0, -1)) i = i[t];
|
|
268
|
+
let c = i[e[e.length - 1] ?? ""];
|
|
269
|
+
o = Date.now(), s += 1;
|
|
270
|
+
let l = () => {}, u = new Promise((e, t) => {
|
|
271
|
+
l = t;
|
|
272
|
+
});
|
|
273
|
+
a.add(l);
|
|
274
|
+
try {
|
|
275
|
+
return await Promise.race([Promise.resolve(c.apply(i, n)), u]);
|
|
276
|
+
} finally {
|
|
277
|
+
--s, a.delete(l), o = Date.now();
|
|
278
|
+
}
|
|
279
|
+
}, l = /* @__PURE__ */ new Map(), u = (e) => {
|
|
280
|
+
let n = e.join("\0"), r = l.get(n);
|
|
281
|
+
if (r) return r;
|
|
282
|
+
let i = new Proxy(function() {}, {
|
|
283
|
+
get: (n, r) => {
|
|
284
|
+
if (typeof r != "string" || r === "then") return;
|
|
285
|
+
let i = t;
|
|
286
|
+
if (i) {
|
|
287
|
+
let t = i.remote;
|
|
288
|
+
for (let n of e) t = t?.[n];
|
|
289
|
+
if (t?.[r] === void 0) return;
|
|
290
|
+
}
|
|
291
|
+
return u([...e, r]);
|
|
292
|
+
},
|
|
293
|
+
apply: (t, n, r) => c(e, r)
|
|
294
|
+
});
|
|
295
|
+
return l.set(n, i), i;
|
|
296
|
+
};
|
|
297
|
+
return {
|
|
298
|
+
facade: u([]),
|
|
299
|
+
first: r.then(() => {}),
|
|
300
|
+
current: () => t,
|
|
301
|
+
onEpoch: (e) => {
|
|
302
|
+
if (i.add(e), t) try {
|
|
303
|
+
e(t.remote, t.epoch);
|
|
304
|
+
} catch {}
|
|
305
|
+
return () => {
|
|
306
|
+
i.delete(e);
|
|
307
|
+
};
|
|
308
|
+
},
|
|
309
|
+
lastCall: () => o,
|
|
310
|
+
inFlight: () => s
|
|
311
|
+
};
|
|
312
|
+
}, Ce = /* @__PURE__ */ f({
|
|
313
|
+
apiPromise: () => Z,
|
|
314
|
+
currentApiEpoch: () => $,
|
|
315
|
+
hasTransport: () => Y,
|
|
316
|
+
onApiEpoch: () => Q
|
|
317
|
+
}), Y = typeof window < "u" || typeof self < "u", we = async () => {
|
|
318
|
+
if (typeof window < "u") return J;
|
|
319
|
+
if (typeof self < "u") return {
|
|
320
|
+
receive: self,
|
|
321
|
+
emit: self
|
|
322
|
+
};
|
|
323
|
+
}, X = Se({ async *[Symbol.asyncIterator]() {
|
|
324
|
+
let t = await we();
|
|
325
|
+
if (!t) {
|
|
326
|
+
await new Promise(() => {});
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
yield* e({}, {
|
|
330
|
+
key: "fkn-api",
|
|
331
|
+
transport: t
|
|
332
|
+
});
|
|
333
|
+
} }), Z = X.first.then(() => X.facade), Q = X.onEpoch, $ = X.current;
|
|
334
|
+
X.lastCall, X.inFlight, Q((e) => {
|
|
335
|
+
z() && e.overlay.setHost(ge, { exactClips: ce() }).catch(() => {});
|
|
336
|
+
});
|
|
337
|
+
//#endregion
|
|
338
|
+
export { Z as a, s as c, Y as i, c as l, J as n, ee as o, Q as r, f as s, $ as t, u };
|
package/build/app.webmanifest
CHANGED
|
@@ -1,65 +1,50 @@
|
|
|
1
|
-
{
|
|
2
|
-
"short_name": "Ripple",
|
|
3
|
-
"name": "Ripple
|
|
4
|
-
"description": "
|
|
5
|
-
"icons": [
|
|
6
|
-
{
|
|
7
|
-
"src": "./assets/logo.
|
|
8
|
-
"sizes": "
|
|
9
|
-
"type": "image/
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
"
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
"enctype": "multipart/form-data",
|
|
52
|
-
"params": {
|
|
53
|
-
"files": [
|
|
54
|
-
{
|
|
55
|
-
"name": "file",
|
|
56
|
-
"accept": [
|
|
57
|
-
"application/vnd.ripple+json",
|
|
58
|
-
"application/json",
|
|
59
|
-
".ripple"
|
|
60
|
-
]
|
|
61
|
-
}
|
|
62
|
-
]
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"short_name": "Ripple",
|
|
3
|
+
"name": "Ripple",
|
|
4
|
+
"description": "Download and stream torrents in your browser",
|
|
5
|
+
"icons": [
|
|
6
|
+
{
|
|
7
|
+
"src": "./assets/logo.svg",
|
|
8
|
+
"sizes": "any",
|
|
9
|
+
"type": "image/svg+xml",
|
|
10
|
+
"purpose": "any"
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
"src": "./assets/logo.png",
|
|
14
|
+
"sizes": "554x561",
|
|
15
|
+
"type": "image/png",
|
|
16
|
+
"purpose": "any"
|
|
17
|
+
}
|
|
18
|
+
],
|
|
19
|
+
"id": "/?source=pwa",
|
|
20
|
+
"start_url": "/?source=pwa",
|
|
21
|
+
"background_color": "#0f0f0f",
|
|
22
|
+
"display": "standalone",
|
|
23
|
+
"scope": "/",
|
|
24
|
+
"theme_color": "#0f0f0f",
|
|
25
|
+
"launch_handler": {
|
|
26
|
+
"client_mode": "focus-existing"
|
|
27
|
+
},
|
|
28
|
+
"file_handlers": [
|
|
29
|
+
{
|
|
30
|
+
"action": "/",
|
|
31
|
+
"accept": {
|
|
32
|
+
"application/x-bittorrent": [".torrent"]
|
|
33
|
+
},
|
|
34
|
+
"icons": [
|
|
35
|
+
{
|
|
36
|
+
"src": "./assets/logo.png",
|
|
37
|
+
"sizes": "554x561",
|
|
38
|
+
"type": "image/png"
|
|
39
|
+
}
|
|
40
|
+
],
|
|
41
|
+
"launch_type": "single-client"
|
|
42
|
+
}
|
|
43
|
+
],
|
|
44
|
+
"protocol_handlers": [
|
|
45
|
+
{
|
|
46
|
+
"protocol": "magnet",
|
|
47
|
+
"url": "/?magnet=%s"
|
|
48
|
+
}
|
|
49
|
+
]
|
|
50
|
+
}
|
|
File without changes
|