@rockhall/electron-offline-content 0.4.0
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/CHANGELOG.md +384 -0
- package/LICENSE +21 -0
- package/README.md +794 -0
- package/dist/internal/asset-file-name.cjs +13 -0
- package/dist/internal/asset-file-name.cjs.map +1 -0
- package/dist/internal/asset-file-name.d.cts +6 -0
- package/dist/internal/asset-file-name.d.cts.map +1 -0
- package/dist/internal/asset-file-name.d.ts +6 -0
- package/dist/internal/asset-file-name.d.ts.map +1 -0
- package/dist/internal/asset-file-name.js +12 -0
- package/dist/internal/asset-file-name.js.map +1 -0
- package/dist/internal/asset-key.cjs +30 -0
- package/dist/internal/asset-key.cjs.map +1 -0
- package/dist/internal/asset-key.d.cts +19 -0
- package/dist/internal/asset-key.d.cts.map +1 -0
- package/dist/internal/asset-key.d.ts +19 -0
- package/dist/internal/asset-key.d.ts.map +1 -0
- package/dist/internal/asset-key.js +27 -0
- package/dist/internal/asset-key.js.map +1 -0
- package/dist/internal/log-format.cjs +98 -0
- package/dist/internal/log-format.cjs.map +1 -0
- package/dist/internal/log-format.d.cts +10 -0
- package/dist/internal/log-format.d.cts.map +1 -0
- package/dist/internal/log-format.d.ts +10 -0
- package/dist/internal/log-format.d.ts.map +1 -0
- package/dist/internal/log-format.js +97 -0
- package/dist/internal/log-format.js.map +1 -0
- package/dist/internal/media-kind.cjs +46 -0
- package/dist/internal/media-kind.cjs.map +1 -0
- package/dist/internal/media-kind.d.cts +20 -0
- package/dist/internal/media-kind.d.cts.map +1 -0
- package/dist/internal/media-kind.d.ts +20 -0
- package/dist/internal/media-kind.d.ts.map +1 -0
- package/dist/internal/media-kind.js +45 -0
- package/dist/internal/media-kind.js.map +1 -0
- package/dist/internal/url-warn.cjs +14 -0
- package/dist/internal/url-warn.cjs.map +1 -0
- package/dist/internal/url-warn.d.cts +10 -0
- package/dist/internal/url-warn.d.cts.map +1 -0
- package/dist/internal/url-warn.d.ts +10 -0
- package/dist/internal/url-warn.d.ts.map +1 -0
- package/dist/internal/url-warn.js +13 -0
- package/dist/internal/url-warn.js.map +1 -0
- package/dist/internal/validation.cjs +222 -0
- package/dist/internal/validation.cjs.map +1 -0
- package/dist/internal/validation.d.cts +78 -0
- package/dist/internal/validation.d.cts.map +1 -0
- package/dist/internal/validation.d.ts +78 -0
- package/dist/internal/validation.d.ts.map +1 -0
- package/dist/internal/validation.js +196 -0
- package/dist/internal/validation.js.map +1 -0
- package/dist/main/asset-download.cjs +265 -0
- package/dist/main/asset-download.cjs.map +1 -0
- package/dist/main/asset-download.d.cts +12 -0
- package/dist/main/asset-download.d.cts.map +1 -0
- package/dist/main/asset-download.d.ts +12 -0
- package/dist/main/asset-download.d.ts.map +1 -0
- package/dist/main/asset-download.js +263 -0
- package/dist/main/asset-download.js.map +1 -0
- package/dist/main/database.cjs +473 -0
- package/dist/main/database.cjs.map +1 -0
- package/dist/main/database.d.cts +81 -0
- package/dist/main/database.d.cts.map +1 -0
- package/dist/main/database.d.ts +81 -0
- package/dist/main/database.d.ts.map +1 -0
- package/dist/main/database.js +472 -0
- package/dist/main/database.js.map +1 -0
- package/dist/main/index.cjs +22 -0
- package/dist/main/index.d.cts +7 -0
- package/dist/main/index.d.ts +7 -0
- package/dist/main/index.js +7 -0
- package/dist/main/media-cache.cjs +862 -0
- package/dist/main/media-cache.cjs.map +1 -0
- package/dist/main/media-cache.d.cts +134 -0
- package/dist/main/media-cache.d.cts.map +1 -0
- package/dist/main/media-cache.d.ts +134 -0
- package/dist/main/media-cache.d.ts.map +1 -0
- package/dist/main/media-cache.js +854 -0
- package/dist/main/media-cache.js.map +1 -0
- package/dist/main/storage-root-lock.cjs +124 -0
- package/dist/main/storage-root-lock.cjs.map +1 -0
- package/dist/main/storage-root-lock.d.cts +11 -0
- package/dist/main/storage-root-lock.d.cts.map +1 -0
- package/dist/main/storage-root-lock.d.ts +11 -0
- package/dist/main/storage-root-lock.d.ts.map +1 -0
- package/dist/main/storage-root-lock.js +120 -0
- package/dist/main/storage-root-lock.js.map +1 -0
- package/dist/main/store.cjs +197 -0
- package/dist/main/store.cjs.map +1 -0
- package/dist/main/store.d.cts +83 -0
- package/dist/main/store.d.cts.map +1 -0
- package/dist/main/store.d.ts +83 -0
- package/dist/main/store.d.ts.map +1 -0
- package/dist/main/store.js +195 -0
- package/dist/main/store.js.map +1 -0
- package/dist/preload/index.cjs +36 -0
- package/dist/preload/index.cjs.map +1 -0
- package/dist/preload/index.d.cts +14 -0
- package/dist/preload/index.d.cts.map +1 -0
- package/dist/preload/index.d.ts +14 -0
- package/dist/preload/index.d.ts.map +1 -0
- package/dist/preload/index.js +34 -0
- package/dist/preload/index.js.map +1 -0
- package/dist/react/index.cjs +199 -0
- package/dist/react/index.cjs.map +1 -0
- package/dist/react/index.d.cts +50 -0
- package/dist/react/index.d.cts.map +1 -0
- package/dist/react/index.d.ts +50 -0
- package/dist/react/index.d.ts.map +1 -0
- package/dist/react/index.js +191 -0
- package/dist/react/index.js.map +1 -0
- package/dist/renderer/helpers.cjs +36 -0
- package/dist/renderer/helpers.cjs.map +1 -0
- package/dist/renderer/helpers.d.cts +11 -0
- package/dist/renderer/helpers.d.cts.map +1 -0
- package/dist/renderer/helpers.d.ts +11 -0
- package/dist/renderer/helpers.d.ts.map +1 -0
- package/dist/renderer/helpers.js +35 -0
- package/dist/renderer/helpers.js.map +1 -0
- package/dist/renderer/index.cjs +20 -0
- package/dist/renderer/index.cjs.map +1 -0
- package/dist/renderer/index.d.cts +14 -0
- package/dist/renderer/index.d.cts.map +1 -0
- package/dist/renderer/index.d.ts +14 -0
- package/dist/renderer/index.d.ts.map +1 -0
- package/dist/renderer/index.js +14 -0
- package/dist/renderer/index.js.map +1 -0
- package/dist/renderer/runtime.cjs +278 -0
- package/dist/renderer/runtime.cjs.map +1 -0
- package/dist/renderer/runtime.d.cts +35 -0
- package/dist/renderer/runtime.d.cts.map +1 -0
- package/dist/renderer/runtime.d.ts +35 -0
- package/dist/renderer/runtime.d.ts.map +1 -0
- package/dist/renderer/runtime.js +273 -0
- package/dist/renderer/runtime.js.map +1 -0
- package/dist/renderer/window-globals.d.cts +9 -0
- package/dist/renderer/window-globals.d.cts.map +1 -0
- package/dist/renderer/window-globals.d.ts +9 -0
- package/dist/renderer/window-globals.d.ts.map +1 -0
- package/dist/shared/errors.cjs +102 -0
- package/dist/shared/errors.cjs.map +1 -0
- package/dist/shared/errors.d.cts +45 -0
- package/dist/shared/errors.d.cts.map +1 -0
- package/dist/shared/errors.d.ts +45 -0
- package/dist/shared/errors.d.ts.map +1 -0
- package/dist/shared/errors.js +93 -0
- package/dist/shared/errors.js.map +1 -0
- package/dist/shared/ipc.cjs +14 -0
- package/dist/shared/ipc.cjs.map +1 -0
- package/dist/shared/ipc.d.cts +12 -0
- package/dist/shared/ipc.d.cts.map +1 -0
- package/dist/shared/ipc.d.ts +12 -0
- package/dist/shared/ipc.d.ts.map +1 -0
- package/dist/shared/ipc.js +13 -0
- package/dist/shared/ipc.js.map +1 -0
- package/dist/shared/normalize.cjs +19 -0
- package/dist/shared/normalize.cjs.map +1 -0
- package/dist/shared/normalize.d.cts +11 -0
- package/dist/shared/normalize.d.cts.map +1 -0
- package/dist/shared/normalize.d.ts +11 -0
- package/dist/shared/normalize.d.ts.map +1 -0
- package/dist/shared/normalize.js +18 -0
- package/dist/shared/normalize.js.map +1 -0
- package/dist/shared/pagination.cjs +32 -0
- package/dist/shared/pagination.cjs.map +1 -0
- package/dist/shared/pagination.d.cts +14 -0
- package/dist/shared/pagination.d.cts.map +1 -0
- package/dist/shared/pagination.d.ts +14 -0
- package/dist/shared/pagination.d.ts.map +1 -0
- package/dist/shared/pagination.js +28 -0
- package/dist/shared/pagination.js.map +1 -0
- package/dist/shared/stem.cjs +16 -0
- package/dist/shared/stem.cjs.map +1 -0
- package/dist/shared/stem.d.cts +6 -0
- package/dist/shared/stem.d.cts.map +1 -0
- package/dist/shared/stem.d.ts +6 -0
- package/dist/shared/stem.d.ts.map +1 -0
- package/dist/shared/stem.js +14 -0
- package/dist/shared/stem.js.map +1 -0
- package/dist/shared/types.cjs +15 -0
- package/dist/shared/types.cjs.map +1 -0
- package/dist/shared/types.d.cts +234 -0
- package/dist/shared/types.d.cts.map +1 -0
- package/dist/shared/types.d.ts +234 -0
- package/dist/shared/types.d.ts.map +1 -0
- package/dist/shared/types.js +14 -0
- package/dist/shared/types.js.map +1 -0
- package/package.json +120 -0
- package/skills/authenticated-downloads/SKILL.md +203 -0
- package/skills/cache-configuration/SKILL.md +357 -0
- package/skills/cache-configuration/references/options.md +356 -0
- package/skills/getting-started/SKILL.md +407 -0
- package/skills/production-checklist/SKILL.md +397 -0
- package/skills/react-rendering/SKILL.md +424 -0
- package/skills/react-rendering/references/hooks.md +443 -0
- package/skills/store-authoring/SKILL.md +369 -0
|
@@ -0,0 +1,424 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: react-rendering
|
|
3
|
+
description: >
|
|
4
|
+
React bindings for @rockhall/electron-offline-content:
|
|
5
|
+
MediaCacheProvider context, useMediaAsset for single asset lookups,
|
|
6
|
+
useMediaByIndex for index-based queries, useMediaBridge and
|
|
7
|
+
useMediaCacheStatus for sync phase and progress, useFileStemMatch
|
|
8
|
+
for filename search, useMediaCacheReady for download gates, and
|
|
9
|
+
useMediaCacheErrors for aggregated error display. AsyncState shape,
|
|
10
|
+
refetchOnSyncComplete, rendering media:// URLs in video/img/audio/track
|
|
11
|
+
elements.
|
|
12
|
+
type: framework
|
|
13
|
+
library: electron-offline-content
|
|
14
|
+
framework: react
|
|
15
|
+
library_version: "0.4.0"
|
|
16
|
+
requires:
|
|
17
|
+
- getting-started
|
|
18
|
+
sources:
|
|
19
|
+
- "rockhallweb/electron-offline-content:src/react/index.tsx"
|
|
20
|
+
- "rockhallweb/electron-offline-content:src/shared/types.ts"
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
This skill builds on getting-started. Read it first for full main → preload → renderer wiring.
|
|
24
|
+
|
|
25
|
+
## Setup
|
|
26
|
+
|
|
27
|
+
Wrap your renderer entry with `MediaCacheProvider`. The bridge is auto-detected from `window.mediaCache` when omitted.
|
|
28
|
+
|
|
29
|
+
```tsx
|
|
30
|
+
import { MediaCacheProvider } from "@rockhall/electron-offline-content/react";
|
|
31
|
+
|
|
32
|
+
function App() {
|
|
33
|
+
return (
|
|
34
|
+
<MediaCacheProvider>
|
|
35
|
+
<KioskShell />
|
|
36
|
+
</MediaCacheProvider>
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Gate content rendering on first sync completion, then render assets:
|
|
42
|
+
|
|
43
|
+
```tsx
|
|
44
|
+
import { useMediaByIndex, useMediaCacheReady } from "@rockhall/electron-offline-content/react";
|
|
45
|
+
|
|
46
|
+
function KioskShell() {
|
|
47
|
+
const ready = useMediaCacheReady();
|
|
48
|
+
const videos = useMediaByIndex("category", "videos", { limit: 50 });
|
|
49
|
+
|
|
50
|
+
if (ready.loading || !ready.data?.ready) {
|
|
51
|
+
return <div>Preparing content…</div>;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (videos.loading) return <div>Loading videos…</div>;
|
|
55
|
+
|
|
56
|
+
return (
|
|
57
|
+
<ul>
|
|
58
|
+
{videos.data?.items.map((asset) => (
|
|
59
|
+
<li key={asset.key}>
|
|
60
|
+
<video src={asset.url} controls />
|
|
61
|
+
<span>{asset.metadata.title as string}</span>
|
|
62
|
+
</li>
|
|
63
|
+
))}
|
|
64
|
+
</ul>
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Hooks and Components
|
|
70
|
+
|
|
71
|
+
### Loading gate with useMediaCacheReady
|
|
72
|
+
|
|
73
|
+
Returns `AsyncState<MediaCacheReadyState>` where `MediaCacheReadyState` has `{ ready, syncing, phase, activeGenerationId, syncError }`.
|
|
74
|
+
|
|
75
|
+
Use as a gate before rendering any content that depends on cached media.
|
|
76
|
+
|
|
77
|
+
```tsx
|
|
78
|
+
import { useMediaCacheReady } from "@rockhall/electron-offline-content/react";
|
|
79
|
+
|
|
80
|
+
function LoadingGate({ children }: { children: React.ReactNode }) {
|
|
81
|
+
const { data, loading } = useMediaCacheReady();
|
|
82
|
+
|
|
83
|
+
if (loading || !data?.ready) {
|
|
84
|
+
return (
|
|
85
|
+
<div className="loading-screen">
|
|
86
|
+
<p>Preparing content…</p>
|
|
87
|
+
{data?.syncing && <p>Downloading assets…</p>}
|
|
88
|
+
</div>
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return <>{children}</>;
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### Single asset lookup with useMediaAsset
|
|
97
|
+
|
|
98
|
+
Returns `AsyncState<ResolvedMediaAsset>` for a single asset by key.
|
|
99
|
+
|
|
100
|
+
```tsx
|
|
101
|
+
import { useMediaAsset } from "@rockhall/electron-offline-content/react";
|
|
102
|
+
|
|
103
|
+
function WelcomeVideo() {
|
|
104
|
+
const { data: asset, loading } = useMediaAsset("video/welcome");
|
|
105
|
+
|
|
106
|
+
if (loading || !asset) return <p>Loading…</p>;
|
|
107
|
+
|
|
108
|
+
return <video src={asset.url} controls />;
|
|
109
|
+
}
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Use `useMediaAsset` when you know the exact asset key. Asset keys come from the `key` field passed to `store.add()` during `resolveStore`.
|
|
113
|
+
|
|
114
|
+
### Index-based queries with useMediaByIndex
|
|
115
|
+
|
|
116
|
+
Returns `AsyncState<PaginationResult<ResolvedMediaAsset>>` for assets matching an index value.
|
|
117
|
+
|
|
118
|
+
```tsx
|
|
119
|
+
import { useMediaByIndex } from "@rockhall/electron-offline-content/react";
|
|
120
|
+
|
|
121
|
+
function VideoList() {
|
|
122
|
+
const { data, loading, error, refresh } = useMediaByIndex("category", "videos", {
|
|
123
|
+
limit: 20,
|
|
124
|
+
refetchOnSyncComplete: true,
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
if (loading) return <p>Loading…</p>;
|
|
128
|
+
if (error) return <p>Error: {error.message}</p>;
|
|
129
|
+
|
|
130
|
+
return (
|
|
131
|
+
<ul>
|
|
132
|
+
{data?.items.map((asset) => (
|
|
133
|
+
<li key={asset.key}>{asset.metadata.title as string}</li>
|
|
134
|
+
))}
|
|
135
|
+
</ul>
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
Query by any index defined in `resolveStore`:
|
|
141
|
+
|
|
142
|
+
```tsx
|
|
143
|
+
function FloorExhibits({ floor }: { floor: string }) {
|
|
144
|
+
const { data, loading } = useMediaByIndex("floor", floor, {
|
|
145
|
+
limit: 100,
|
|
146
|
+
refetchOnSyncComplete: true,
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
if (loading || !data) return null;
|
|
150
|
+
|
|
151
|
+
return (
|
|
152
|
+
<div>
|
|
153
|
+
{data.items.map((asset) => (
|
|
154
|
+
<figure key={asset.key}>
|
|
155
|
+
<img src={asset.url} alt={asset.metadata.title as string} />
|
|
156
|
+
<figcaption>{asset.metadata.title as string}</figcaption>
|
|
157
|
+
</figure>
|
|
158
|
+
))}
|
|
159
|
+
</div>
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
### Sync progress with useMediaCacheStatus
|
|
165
|
+
|
|
166
|
+
Returns `AsyncState<MediaCacheStatus>` with `phase`, `progress`, `storageRoot`, `activeGenerationId`, `lastRun`, and `error`.
|
|
167
|
+
|
|
168
|
+
```tsx
|
|
169
|
+
import { useMediaCacheStatus } from "@rockhall/electron-offline-content/react";
|
|
170
|
+
|
|
171
|
+
function SyncOverlay() {
|
|
172
|
+
const { data: status, loading } = useMediaCacheStatus();
|
|
173
|
+
|
|
174
|
+
if (loading || !status) return null;
|
|
175
|
+
if (status.phase !== "syncing") return null;
|
|
176
|
+
|
|
177
|
+
const { progress } = status;
|
|
178
|
+
|
|
179
|
+
return (
|
|
180
|
+
<div className="sync-overlay">
|
|
181
|
+
<p>Syncing…</p>
|
|
182
|
+
{progress && <progress value={progress.completedAssets} max={progress.totalAssets} />}
|
|
183
|
+
{progress && (
|
|
184
|
+
<p>
|
|
185
|
+
{progress.completedAssets}/{progress.totalAssets} assets (
|
|
186
|
+
{(progress.bytesDownloaded / 1_048_576).toFixed(1)} MB)
|
|
187
|
+
</p>
|
|
188
|
+
)}
|
|
189
|
+
</div>
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
### Imperative bridge access with useMediaBridge
|
|
195
|
+
|
|
196
|
+
Returns bridge methods together with shared `status` and aggregated `errors`.
|
|
197
|
+
|
|
198
|
+
```tsx
|
|
199
|
+
import { useMediaBridge } from "@rockhall/electron-offline-content/react";
|
|
200
|
+
|
|
201
|
+
function DownloadButton() {
|
|
202
|
+
const { syncNow, status, errors } = useMediaBridge();
|
|
203
|
+
|
|
204
|
+
return (
|
|
205
|
+
<button
|
|
206
|
+
type="button"
|
|
207
|
+
disabled={status.data?.phase === "syncing"}
|
|
208
|
+
onClick={() => void syncNow()}
|
|
209
|
+
>
|
|
210
|
+
{errors.hasError ? `Retry sync (${errors.primaryError?.message})` : "Sync now"}
|
|
211
|
+
</button>
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
### Error aggregation with useMediaCacheErrors
|
|
217
|
+
|
|
218
|
+
Returns `MediaCacheErrors` with `{ hasError, primaryError, syncError, statusError, queryErrors }`.
|
|
219
|
+
|
|
220
|
+
```tsx
|
|
221
|
+
import { useMediaByIndex, useMediaCacheErrors } from "@rockhall/electron-offline-content/react";
|
|
222
|
+
|
|
223
|
+
function ExhibitPage() {
|
|
224
|
+
const videos = useMediaByIndex("category", "videos", { limit: 50 });
|
|
225
|
+
const images = useMediaByIndex("category", "images", { limit: 100 });
|
|
226
|
+
const errors = useMediaCacheErrors();
|
|
227
|
+
|
|
228
|
+
if (errors.hasError) {
|
|
229
|
+
return (
|
|
230
|
+
<div className="error-banner">
|
|
231
|
+
<p>Something went wrong: {errors.primaryError?.message}</p>
|
|
232
|
+
{errors.syncError && <p>Sync failed — content may be stale.</p>}
|
|
233
|
+
</div>
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return <div>{/* render videos.data and images.data */}</div>;
|
|
238
|
+
}
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
### File stem matching with useFileStemMatch
|
|
242
|
+
|
|
243
|
+
Returns `AsyncState<PaginationResult<FileStemMatch>>`. Searches cached content by filename stem across all assets.
|
|
244
|
+
|
|
245
|
+
```tsx
|
|
246
|
+
import { useFileStemMatch } from "@rockhall/electron-offline-content/react";
|
|
247
|
+
|
|
248
|
+
function AssetSearch({ query }: { query: string }) {
|
|
249
|
+
const { data, loading } = useFileStemMatch(query, {
|
|
250
|
+
limit: 25,
|
|
251
|
+
refetchOnSyncComplete: true,
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
if (loading || !data) return <p>Searching…</p>;
|
|
255
|
+
|
|
256
|
+
return (
|
|
257
|
+
<ul>
|
|
258
|
+
{data.items.map((match) => (
|
|
259
|
+
<li key={match.asset.key}>{match.asset.key}</li>
|
|
260
|
+
))}
|
|
261
|
+
</ul>
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
### Rendering media:// URLs
|
|
267
|
+
|
|
268
|
+
URLs from hook results work directly in `src` attributes. In offline mode they resolve through the `media://` protocol handler registered in main. In `devPassthrough` mode they are remote HTTPS URLs. Never construct these URLs manually.
|
|
269
|
+
|
|
270
|
+
```tsx
|
|
271
|
+
function MediaPlayer({ asset }: { asset: ResolvedMediaAsset }) {
|
|
272
|
+
return <video src={asset.url} controls autoPlay muted />;
|
|
273
|
+
}
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
Audio assets work the same way:
|
|
277
|
+
|
|
278
|
+
```tsx
|
|
279
|
+
function AudioPlayer({ asset }: { asset: ResolvedMediaAsset }) {
|
|
280
|
+
return <audio src={asset.url} controls />;
|
|
281
|
+
}
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
For related assets (e.g. a video with a poster and subtitles), look up each asset by key:
|
|
285
|
+
|
|
286
|
+
```tsx
|
|
287
|
+
function VideoWithExtras({ videoKey }: { videoKey: string }) {
|
|
288
|
+
const video = useMediaAsset(videoKey);
|
|
289
|
+
const poster = useMediaAsset(`${videoKey}/poster`);
|
|
290
|
+
const subs = useMediaAsset(`${videoKey}/subs-en`);
|
|
291
|
+
|
|
292
|
+
if (video.loading || !video.data) return <p>Loading…</p>;
|
|
293
|
+
|
|
294
|
+
return (
|
|
295
|
+
<video src={video.data.url} poster={poster.data?.url} controls>
|
|
296
|
+
{subs.data && <track src={subs.data.url} kind="subtitles" srcLang="en" default />}
|
|
297
|
+
</video>
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
## Common Mistakes
|
|
303
|
+
|
|
304
|
+
### HIGH: Accessing data before loading completes
|
|
305
|
+
|
|
306
|
+
All hooks return `AsyncState<T>` where `data` is `null` until the first successful load. Accessing nested properties without a null check causes a `TypeError` at runtime.
|
|
307
|
+
|
|
308
|
+
```tsx
|
|
309
|
+
// WRONG — TypeError when data is null
|
|
310
|
+
function Broken() {
|
|
311
|
+
const videos = useMediaByIndex("category", "videos");
|
|
312
|
+
return (
|
|
313
|
+
<ul>
|
|
314
|
+
{videos.data.items.map((asset) => (
|
|
315
|
+
<li key={asset.key}>{asset.metadata.title as string}</li>
|
|
316
|
+
))}
|
|
317
|
+
</ul>
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// CORRECT — guard on loading and null
|
|
322
|
+
function Fixed() {
|
|
323
|
+
const videos = useMediaByIndex("category", "videos");
|
|
324
|
+
if (videos.loading || !videos.data) return <p>Loading…</p>;
|
|
325
|
+
return (
|
|
326
|
+
<ul>
|
|
327
|
+
{videos.data.items.map((asset) => (
|
|
328
|
+
<li key={asset.key}>{asset.metadata.title as string}</li>
|
|
329
|
+
))}
|
|
330
|
+
</ul>
|
|
331
|
+
);
|
|
332
|
+
}
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
Source: `react/index.tsx` — `AsyncState` type definition
|
|
336
|
+
|
|
337
|
+
### HIGH: Fetching remote URLs instead of rendering directly
|
|
338
|
+
|
|
339
|
+
Hook URLs are ready to render. Do not `fetch()` them in the renderer — it bypasses the protocol handler in offline mode and adds unnecessary complexity.
|
|
340
|
+
|
|
341
|
+
```tsx
|
|
342
|
+
// WRONG — redundant fetch, breaks offline
|
|
343
|
+
function Broken({ asset }: { asset: ResolvedMediaAsset }) {
|
|
344
|
+
const [src, setSrc] = useState("");
|
|
345
|
+
useEffect(() => {
|
|
346
|
+
fetch(asset.url)
|
|
347
|
+
.then((r) => r.blob())
|
|
348
|
+
.then((b) => setSrc(URL.createObjectURL(b)));
|
|
349
|
+
}, [asset]);
|
|
350
|
+
return <video src={src} />;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// CORRECT — pass URL directly to src
|
|
354
|
+
function Fixed({ asset }: { asset: ResolvedMediaAsset }) {
|
|
355
|
+
return <video src={asset.url} controls />;
|
|
356
|
+
}
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
Source: Maintainer interview
|
|
360
|
+
|
|
361
|
+
### HIGH: Using removed useMedia hooks
|
|
362
|
+
|
|
363
|
+
`useMedia({ kind: "item", ... })` and `useMedia({ kind: "list", ... })` were removed in 0.4.0. Use `useMediaAsset` and `useMediaByIndex` instead.
|
|
364
|
+
|
|
365
|
+
```tsx
|
|
366
|
+
// WRONG — removed API
|
|
367
|
+
const item = useMedia({ kind: "item", namespace: "videos", id: "welcome" });
|
|
368
|
+
const list = useMedia({ kind: "list", namespace: "videos", limit: 20 });
|
|
369
|
+
|
|
370
|
+
// CORRECT
|
|
371
|
+
const asset = useMediaAsset("video/welcome");
|
|
372
|
+
const videos = useMediaByIndex("category", "videos", { limit: 20 });
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
Source: `CHANGELOG.md` 0.4.0; `react/index.tsx`
|
|
376
|
+
|
|
377
|
+
### MEDIUM: Splitting imperative bridge state across multiple hooks
|
|
378
|
+
|
|
379
|
+
If a component needs `syncNow()`, status, and errors together, prefer `useMediaBridge()` over manually combining separate bridge, status, and error hooks. The combined hook matches the provider runtime and keeps imperative UI code simpler.
|
|
380
|
+
|
|
381
|
+
```tsx
|
|
382
|
+
// WRONG — imperative bridge UI spread across separate hooks
|
|
383
|
+
function SyncButton() {
|
|
384
|
+
const status = useMediaCacheStatus();
|
|
385
|
+
const errors = useMediaCacheErrors();
|
|
386
|
+
// some other hook supplies syncNow()
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// CORRECT — one hook for imperative bridge access
|
|
390
|
+
function SyncButton() {
|
|
391
|
+
const { syncNow, status, errors } = useMediaBridge();
|
|
392
|
+
return (
|
|
393
|
+
<button onClick={() => void syncNow()}>
|
|
394
|
+
{status.data?.phase ?? errors.primaryError?.message}
|
|
395
|
+
</button>
|
|
396
|
+
);
|
|
397
|
+
}
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
Source: `react/index.tsx` — `useMediaCacheErrors` JSDoc
|
|
401
|
+
|
|
402
|
+
### MEDIUM: Hardcoding media:// URLs instead of using hook data
|
|
403
|
+
|
|
404
|
+
URLs differ between offline mode (`media://`) and `devPassthrough` mode (remote HTTPS). Hardcoded URLs break in one of the two modes.
|
|
405
|
+
|
|
406
|
+
```tsx
|
|
407
|
+
// WRONG — hardcoded protocol URL
|
|
408
|
+
<video src="media://asset/video%2Fwelcome" />;
|
|
409
|
+
|
|
410
|
+
// CORRECT — URL from hook result
|
|
411
|
+
const { data: asset } = useMediaAsset("video/welcome");
|
|
412
|
+
<video src={asset?.url} />;
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
Source: README — devPassthrough documentation
|
|
416
|
+
|
|
417
|
+
---
|
|
418
|
+
|
|
419
|
+
See also: getting-started/SKILL.md — Full main → preload → renderer wiring
|
|
420
|
+
See also: store-authoring/SKILL.md — Index definitions determine how hooks query content
|
|
421
|
+
|
|
422
|
+
## References
|
|
423
|
+
|
|
424
|
+
- [Complete hooks API reference](references/hooks.md)
|