-1
package.json
-1
package.json
···
50
50
"@solidjs/meta": "^0.29.4",
51
51
"@solidjs/router": "^0.15.3",
52
52
"codemirror": "^6.0.2",
53
-
"hls.js": "^1.6.13",
54
53
"solid-js": "^1.9.9"
55
54
},
56
55
"packageManager": "pnpm@10.12.2+sha512.a32540185b964ee30bb4e979e405adc6af59226b438ee4cc19f9e8773667a66d302f5bfee60a39d3cac69e35e4b96e708a71dd002b7e9359c4112a1722ac323f"
-8
pnpm-lock.yaml
-8
pnpm-lock.yaml
···
95
95
codemirror:
96
96
specifier: ^6.0.2
97
97
version: 6.0.2
98
-
hls.js:
99
-
specifier: ^1.6.13
100
-
version: 1.6.13
101
98
solid-js:
102
99
specifier: ^1.9.9
103
100
version: 1.9.9
···
1038
1035
1039
1036
graceful-fs@4.2.11:
1040
1037
resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
1041
-
1042
-
hls.js@1.6.13:
1043
-
resolution: {integrity: sha512-hNEzjZNHf5bFrUNvdS4/1RjIanuJ6szpWNfTaX5I6WfGynWXGT7K/YQLYtemSvFExzeMdgdE4SsyVLJbd5PcZA==}
1044
1038
1045
1039
html-entities@2.3.3:
1046
1040
resolution: {integrity: sha512-DV5Ln36z34NNTDgnz0EWGBLZENelNAtkiFA4kyNOG2tDI6Mz1uSWiq1wAKdyjnJwyDiDO7Fa2SO1CTxPXL8VxA==}
···
2274
2268
globals@15.15.0: {}
2275
2269
2276
2270
graceful-fs@4.2.11: {}
2277
-
2278
-
hls.js@1.6.13: {}
2279
2271
2280
2272
html-entities@2.3.3: {}
2281
2273
+21
-16
src/components/json.tsx
+21
-16
src/components/json.tsx
···
1
-
import { A } from "@solidjs/router";
2
-
import { createEffect, createSignal, For, Show } from "solid-js";
1
+
import { A, useParams } from "@solidjs/router";
2
+
import { createEffect, createSignal, ErrorBoundary, For, Show } from "solid-js";
3
3
import { hideMedia } from "../views/settings";
4
4
import { pds } from "./navbar";
5
5
import Tooltip from "./tooltip";
···
77
77
};
78
78
79
79
const JSONObject = ({ data, repo }: { data: { [x: string]: JSONType }; repo: string }) => {
80
-
const [hide, setHide] = createSignal(localStorage.hideMedia === "true");
80
+
const params = useParams();
81
+
const [hide, setHide] = createSignal(
82
+
localStorage.hideMedia === "true" || params.rkey === undefined,
83
+
);
81
84
82
-
createEffect(() => setHide(hideMedia()));
85
+
createEffect(() => {
86
+
if (hideMedia()) setHide(hideMedia());
87
+
});
83
88
84
89
const Obj = ({ key, value }: { key: string; value: JSONType }) => {
85
90
const [show, setShow] = createSignal(true);
···
132
137
<>
133
138
<span class="flex gap-x-1">
134
139
<Show when={blob.mimeType.startsWith("image/") && !hide()}>
135
-
<a
136
-
href={`https://cdn.bsky.app/img/feed_thumbnail/plain/${repo}/${blob.ref.$link}@jpeg`}
137
-
target="_blank"
138
-
>
139
-
<img
140
-
class="max-h-[16rem] w-full max-w-[16rem]"
141
-
src={`https://cdn.bsky.app/img/feed_thumbnail/plain/${repo}/${blob.ref.$link}@jpeg`}
142
-
/>
143
-
</a>
140
+
<img
141
+
class="max-h-[16rem] w-fit max-w-[16rem]"
142
+
src={`https://${pds()}/xrpc/com.atproto.sync.getBlob?did=${repo}&cid=${blob.ref.$link}`}
143
+
/>
144
144
</Show>
145
145
<Show when={blob.mimeType === "video/mp4" && !hide()}>
146
-
<VideoPlayer did={repo} cid={blob.ref.$link} />
146
+
<ErrorBoundary fallback={() => <span>Failed to load video</span>}>
147
+
<VideoPlayer did={repo} cid={blob.ref.$link} />
148
+
</ErrorBoundary>
147
149
</Show>
148
150
<span
149
-
classList={{ "flex items-center justify-between gap-1": true, "flex-col": !hide() }}
151
+
classList={{
152
+
"flex items-center justify-between gap-1": true,
153
+
"flex-col": !hide(),
154
+
}}
150
155
>
151
156
<Show when={blob.mimeType.startsWith("image/") || blob.mimeType === "video/mp4"}>
152
157
<Tooltip text={hide() ? "Show" : "Hide"}>
···
161
166
</Tooltip>
162
167
</Show>
163
168
<Show when={pds()}>
164
-
<Tooltip text="Blob PDS link">
169
+
<Tooltip text="Blob on PDS">
165
170
<a
166
171
href={`https://${pds()}/xrpc/com.atproto.sync.getBlob?did=${repo}&cid=${blob.ref.$link}`}
167
172
target="_blank"
+13
-68
src/components/video-player.tsx
+13
-68
src/components/video-player.tsx
···
1
-
// courtesy of the best 🐇, my lovely sister mary
2
-
import Hls from "hls.js";
3
-
import { createEffect, createSignal, onCleanup, Show } from "solid-js";
1
+
import { onMount } from "solid-js";
2
+
import { pds } from "./navbar";
4
3
5
4
export interface VideoPlayerProps {
6
-
/** Expected to be static */
7
5
did: string;
8
6
cid: string;
9
7
}
10
8
11
9
const VideoPlayer = ({ did, cid }: VideoPlayerProps) => {
12
-
const [playing, setPlaying] = createSignal(false);
13
-
const [error, setError] = createSignal(false);
10
+
let video!: HTMLVideoElement;
14
11
15
-
const hls = new Hls({
16
-
capLevelToPlayerSize: true,
17
-
startLevel: 1,
18
-
xhrSetup(xhr, urlString) {
19
-
const url = new URL(urlString);
20
-
21
-
// Just in case it fails, we'll remove `session_id` everywhere
22
-
url.searchParams.delete("session_id");
23
-
24
-
xhr.open("get", url.toString());
25
-
},
12
+
onMount(async () => {
13
+
// thanks bf <3
14
+
const res = await fetch(`https://${pds()}/xrpc/com.atproto.sync.getBlob?did=${did}&cid=${cid}`);
15
+
if (!res.ok) throw new Error(res.statusText);
16
+
const blob = await res.blob();
17
+
const url = URL.createObjectURL(blob);
18
+
if (video) video.src = url;
26
19
});
27
20
28
-
onCleanup(() => hls.destroy());
29
-
30
-
hls.loadSource(`https://video.cdn.bsky.app/hls/${did}/${cid}/playlist.m3u8`);
31
-
hls.on(Hls.Events.ERROR, () => setError(true));
32
-
33
21
return (
34
-
<div class="max-w-xs">
35
-
<Show when={!error()}>
36
-
<video
37
-
ref={(node) => {
38
-
hls.attachMedia(node);
39
-
40
-
createEffect(() => {
41
-
if (!playing()) {
42
-
return;
43
-
}
44
-
45
-
const observer = new IntersectionObserver(
46
-
(entries) => {
47
-
const entry = entries[0];
48
-
if (!entry.isIntersecting) {
49
-
node.pause();
50
-
}
51
-
},
52
-
{ threshold: 0.5 },
53
-
);
54
-
55
-
onCleanup(() => observer.disconnect());
56
-
57
-
observer.observe(node);
58
-
});
59
-
}}
60
-
controls
61
-
playsinline
62
-
onPlay={() => setPlaying(true)}
63
-
onPause={() => setPlaying(false)}
64
-
onLoadedMetadata={(ev) => {
65
-
const video = ev.currentTarget;
66
-
67
-
const hasAudio =
68
-
// @ts-expect-error: Mozilla-specific
69
-
video.mozHasAudio ||
70
-
// @ts-expect-error: WebKit/Blink-specific
71
-
!!video.webkitAudioDecodedByteCount ||
72
-
// @ts-expect-error: WebKit-specific
73
-
!!(video.audioTracks && video.audioTracks.length);
74
-
75
-
video.loop = !hasAudio || video.duration <= 6;
76
-
}}
77
-
/>
78
-
</Show>
79
-
</div>
22
+
<video ref={video} class="max-w-xs" controls playsinline>
23
+
<source type="video/mp4" />
24
+
</video>
80
25
);
81
26
};
82
27