@buoy-gg/images 7.0.29 → 7.0.30

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.
@@ -1,466 +0,0 @@
1
- /**
2
- * ImageDetail — full record view: preview, source, load timeline, dimensions
3
- * with the oversize verdict + estimated decoded/wasted memory, cache verdict,
4
- * and error details (including HTTP status/headers when the platform provides
5
- * them). Cache actions live here too (expo-image global clears).
6
- */
7
-
8
- import { useState, type ReactNode } from "react";
9
- import {
10
- Image,
11
- ScrollView,
12
- StyleSheet,
13
- Text,
14
- TextInput,
15
- TouchableOpacity,
16
- View,
17
- } from "react-native";
18
- import { macOSColors, night } from "@buoy-gg/shared-ui";
19
- import { clearOverride, setOverride } from "../capture/overrides";
20
- import { proveSavings } from "../capture/savings";
21
- import type { SavingsResult } from "../capture/savings";
22
- import { remoteImagesDispatch } from "../remote";
23
- import {
24
- effectivePixelRatio,
25
- estDecodedBytes,
26
- estWastedBytes,
27
- neededPixels,
28
- } from "../store/imageEventStore";
29
- import { cacheBadge, formatBytes, formatMs, sizeVerdict } from "../format";
30
- import type { ImageRecord } from "../types";
31
-
32
- interface ImageDetailProps {
33
- record: ImageRecord;
34
- }
35
-
36
- function Section({ title, children }: { title: string; children: ReactNode }) {
37
- return (
38
- <View style={styles.section}>
39
- <Text style={styles.sectionTitle}>{title}</Text>
40
- <View style={styles.sectionBody}>{children}</View>
41
- </View>
42
- );
43
- }
44
-
45
- function Row({ label, value, color }: { label: string; value: string; color?: string }) {
46
- return (
47
- <View style={styles.kvRow}>
48
- <Text style={styles.kvLabel}>{label}</Text>
49
- <Text style={[styles.kvValue, color ? { color } : null]} selectable>
50
- {value}
51
- </Text>
52
- </View>
53
- );
54
- }
55
-
56
- /** expo-image global cache clears (no-ops if expo-image is absent). */
57
- function clearExpoCache(kind: "memory" | "disk"): void {
58
- // Mirror mode: the caches live on the synced device.
59
- const remote = remoteImagesDispatch();
60
- if (remote) {
61
- void remote("clearExpoCaches", {
62
- memory: kind === "memory",
63
- disk: kind === "disk",
64
- });
65
- return;
66
- }
67
- try {
68
- const expo = require("expo-image");
69
- if (kind === "memory") void expo.Image.clearMemoryCache();
70
- else void expo.Image.clearDiskCache();
71
- } catch {
72
- // expo-image not installed
73
- }
74
- }
75
-
76
- /** Free-text source swap: test another URL/format/CDN param on the live view. */
77
- function SwapUrlSection({ record }: { record: ImageRecord }) {
78
- const [url, setUrl] = useState(record.uri);
79
- const trimmed = url.trim();
80
- const canApply = record.mounted && trimmed.length > 0 && trimmed !== record.uri;
81
-
82
- return (
83
- <Section title="SWAP SOURCE URL">
84
- <TextInput
85
- style={styles.urlInput}
86
- value={url}
87
- onChangeText={setUrl}
88
- placeholder="https://…"
89
- placeholderTextColor={night.color.placeholder}
90
- autoCapitalize="none"
91
- autoCorrect={false}
92
- multiline
93
- />
94
- <View style={styles.actionsRow}>
95
- <TouchableOpacity
96
- style={[styles.actionButton, !canApply && styles.actionDisabled]}
97
- disabled={!canApply}
98
- onPress={() =>
99
- setOverride(record.id, {
100
- source: { type: "url", uri: trimmed },
101
- label: "URL swapped",
102
- })
103
- }
104
- >
105
- <Text style={styles.actionText}>Apply</Text>
106
- </TouchableOpacity>
107
- <TouchableOpacity
108
- style={styles.actionButton}
109
- onPress={() => setUrl(record.uri)}
110
- >
111
- <Text style={styles.actionText}>Reset field</Text>
112
- </TouchableOpacity>
113
- </View>
114
- </Section>
115
- );
116
- }
117
-
118
- /**
119
- * Real, on-device savings proof: re-encode at displayed size as WebP via
120
- * expo-image-manipulator, report actual bytes, offer an in-place preview.
121
- */
122
- function SavingsSection({ record }: { record: ImageRecord }) {
123
- const [busy, setBusy] = useState(false);
124
- const [result, setResult] = useState<SavingsResult | null>(null);
125
-
126
- const run = () => {
127
- setBusy(true);
128
- proveSavings(record)
129
- .then(setResult)
130
- .catch((error) => setResult({ ok: false, message: String(error?.message ?? error) }))
131
- .finally(() => setBusy(false));
132
- };
133
-
134
- return (
135
- <Section title="PROVE SAVINGS (re-encode on device)">
136
- {result == null ? (
137
- <View style={styles.actionsRow}>
138
- <TouchableOpacity style={styles.actionButton} onPress={run} disabled={busy}>
139
- <Text style={styles.actionText}>
140
- {busy ? "Re-encoding…" : "Re-encode at displayed size (WebP)"}
141
- </Text>
142
- </TouchableOpacity>
143
- </View>
144
- ) : (
145
- <>
146
- <Row
147
- label="Result"
148
- value={result.message || (result.ok ? "done" : "failed")}
149
- color={result.ok ? macOSColors.semantic.success : macOSColors.semantic.error}
150
- />
151
- {result.originalBytes != null ? (
152
- <Row
153
- label="Original"
154
- value={`${formatBytes(result.originalBytes)} (${result.originalBytesSource})`}
155
- />
156
- ) : null}
157
- {result.optimizedBytes != null ? (
158
- <Row
159
- label="Optimized"
160
- value={`${formatBytes(result.optimizedBytes)} — ${result.optimizedDims?.width}×${result.optimizedDims?.height} WebP`}
161
- color={macOSColors.semantic.success}
162
- />
163
- ) : null}
164
- {result.savedBytes != null ? (
165
- <Row
166
- label="Saved"
167
- value={`${formatBytes(result.savedBytes)} (${result.savedPct}%)`}
168
- color={macOSColors.semantic.success}
169
- />
170
- ) : null}
171
- <View style={styles.actionsRow}>
172
- {result.ok && result.optimizedUri && record.mounted ? (
173
- <TouchableOpacity
174
- style={styles.actionButton}
175
- onPress={() =>
176
- setOverride(record.id, {
177
- source: { type: "url", uri: result.optimizedUri! },
178
- label: "Optimized preview",
179
- })
180
- }
181
- >
182
- <Text style={styles.actionText}>Preview in place</Text>
183
- </TouchableOpacity>
184
- ) : null}
185
- <TouchableOpacity style={styles.actionButton} onPress={run} disabled={busy}>
186
- <Text style={styles.actionText}>{busy ? "Re-encoding…" : "Re-run"}</Text>
187
- </TouchableOpacity>
188
- </View>
189
- </>
190
- )}
191
- </Section>
192
- );
193
- }
194
-
195
- export function ImageDetail({ record }: ImageDetailProps) {
196
- const verdict = sizeVerdict(record);
197
- const cache = cacheBadge(record);
198
- const needed = neededPixels(record);
199
- const decoded = estDecodedBytes(record);
200
- const wasted = estWastedBytes(record);
201
-
202
- return (
203
- <ScrollView style={styles.container} contentContainerStyle={styles.content}>
204
- {record.overrideLabel ? (
205
- <View style={styles.overrideBanner}>
206
- <Text style={styles.overrideText}>
207
- Simulation active: {record.overrideLabel} — stats below describe the
208
- override, not the original source.
209
- </Text>
210
- <TouchableOpacity onPress={() => clearOverride(record.id)}>
211
- <Text style={styles.overrideRestore}>RESTORE</Text>
212
- </TouchableOpacity>
213
- </View>
214
- ) : null}
215
-
216
- {record.uri && record.sourceKind !== "data" ? (
217
- <View style={styles.previewBox}>
218
- <Image
219
- testID="buoy-internal-images-preview"
220
- source={{ uri: record.uri }}
221
- style={styles.preview}
222
- resizeMode="contain"
223
- />
224
- </View>
225
- ) : null}
226
-
227
- <Section title="SOURCE">
228
- <Row label="URI" value={record.uri || "(none)"} />
229
- <Row label="Kind" value={record.sourceKind} />
230
- <Row label="Library" value={record.lib === "expo" ? "expo-image" : "RN <Image>"} />
231
- {record.hasAltText === false ? (
232
- <Row
233
- label="Alt text"
234
- value="missing — add alt/accessibilityLabel"
235
- color={macOSColors.semantic.warning}
236
- />
237
- ) : null}
238
- {record.mediaType != null ? <Row label="Media type" value={String(record.mediaType)} /> : null}
239
- {record.isAnimated != null ? (
240
- <Row label="Animated" value={record.isAnimated ? "yes" : "no"} />
241
- ) : null}
242
- </Section>
243
-
244
- <Section title="LOAD">
245
- <Row
246
- label="Status"
247
- value={record.status}
248
- color={
249
- record.status === "error"
250
- ? macOSColors.semantic.error
251
- : record.status === "loaded"
252
- ? macOSColors.semantic.success
253
- : macOSColors.semantic.warning
254
- }
255
- />
256
- <Row label="Duration" value={formatMs(record.durationMs)} />
257
- {record.displayedAt && record.loadedAt ? (
258
- <Row
259
- label="Decode → display"
260
- value={formatMs(Math.max(0, record.displayedAt - record.loadedAt))}
261
- />
262
- ) : null}
263
- <Row label="Cache" value={`${cache.label}`} color={cache.color} />
264
- {record.cacheVerdictSource ? (
265
- <Row
266
- label="Cache source"
267
- value={
268
- record.cacheVerdictSource === "event"
269
- ? "reported by expo-image onLoad"
270
- : "Image.queryCache after load"
271
- }
272
- />
273
- ) : null}
274
- {record.progressSeen ? (
275
- <Row
276
- label="Downloaded"
277
- value={
278
- record.bytesTotal
279
- ? `${formatBytes(record.bytesLoaded ?? 0)} / ${formatBytes(record.bytesTotal)}`
280
- : "network transfer observed"
281
- }
282
- />
283
- ) : null}
284
- <Row label="Load cycles" value={String(record.loadCount)} />
285
- <Row label="Still mounted" value={record.mounted ? "yes" : "no"} />
286
- </Section>
287
-
288
- <Section title="DIMENSIONS">
289
- {record.intrinsic ? (
290
- <Row
291
- label="Decoded"
292
- value={`${record.intrinsic.width}×${record.intrinsic.height} px`}
293
- />
294
- ) : (
295
- <Row label="Decoded" value="unknown" />
296
- )}
297
- {record.layout ? (
298
- <Row
299
- label="Displayed"
300
- value={`${Math.round(record.layout.width)}×${Math.round(record.layout.height)} dp (needs ${
301
- needed ? `${needed.width}×${needed.height}` : "?"
302
- } px @${Math.round(effectivePixelRatio() * 100) / 100}x)`}
303
- />
304
- ) : (
305
- <Row label="Displayed" value="no layout yet" />
306
- )}
307
- {verdict.label ? <Row label="Verdict" value={verdict.label} color={verdict.color} /> : null}
308
- {decoded > 0 ? <Row label="Est. decoded memory" value={formatBytes(decoded)} /> : null}
309
- {wasted > 0 ? (
310
- <Row
311
- label="Est. wasted"
312
- value={`${formatBytes(wasted)} — resize the source to ${
313
- needed ? `${needed.width}×${needed.height}` : "the displayed size"
314
- }`}
315
- color={macOSColors.semantic.warning}
316
- />
317
- ) : null}
318
- {(record.layoutShifts ?? 0) > 0 ? (
319
- <Row
320
- label="Layout shifts"
321
- value={`${record.layoutShifts} — box resized after load; reserve explicit width/height`}
322
- color={macOSColors.semantic.warning}
323
- />
324
- ) : null}
325
- </Section>
326
-
327
- {record.sourceKind === "network" || record.sourceKind === "asset" ? (
328
- <SavingsSection key={`savings-${record.id}`} record={record} />
329
- ) : null}
330
-
331
- {record.status === "error" ? (
332
- <Section title="ERROR">
333
- <Row label="Message" value={record.error ?? "unknown"} color={macOSColors.semantic.error} />
334
- {record.errorCode != null ? <Row label="HTTP status" value={String(record.errorCode)} /> : null}
335
- {record.errorHeaders
336
- ? Object.entries(record.errorHeaders).map(([k, v]) => (
337
- <Row key={k} label={k} value={String(v)} />
338
- ))
339
- : null}
340
- </Section>
341
- ) : null}
342
-
343
- {record.sourceKind === "network" && record.mounted ? (
344
- <SwapUrlSection key={record.id} record={record} />
345
- ) : null}
346
-
347
- {record.lib === "expo" ? (
348
- <Section title="CACHE ACTIONS (expo-image, global)">
349
- <View style={styles.actionsRow}>
350
- <TouchableOpacity style={styles.actionButton} onPress={() => clearExpoCache("memory")}>
351
- <Text style={styles.actionText}>Clear memory cache</Text>
352
- </TouchableOpacity>
353
- <TouchableOpacity style={styles.actionButton} onPress={() => clearExpoCache("disk")}>
354
- <Text style={styles.actionText}>Clear disk cache</Text>
355
- </TouchableOpacity>
356
- </View>
357
- </Section>
358
- ) : null}
359
- </ScrollView>
360
- );
361
- }
362
-
363
- const styles = StyleSheet.create({
364
- container: {
365
- flex: 1,
366
- },
367
- content: {
368
- padding: 12,
369
- gap: 12,
370
- },
371
- previewBox: {
372
- height: 140,
373
- borderRadius: 8,
374
- backgroundColor: night.color.bg,
375
- overflow: "hidden",
376
- },
377
- preview: {
378
- width: "100%",
379
- height: "100%",
380
- },
381
- section: {
382
- gap: 6,
383
- },
384
- sectionTitle: {
385
- color: night.color.textTertiary,
386
- fontSize: 10,
387
- fontWeight: "700",
388
- letterSpacing: 0.8,
389
- },
390
- sectionBody: {
391
- backgroundColor: night.color.surface,
392
- borderRadius: 8,
393
- borderWidth: StyleSheet.hairlineWidth,
394
- borderColor: night.color.border,
395
- paddingHorizontal: 10,
396
- paddingVertical: 4,
397
- },
398
- kvRow: {
399
- flexDirection: "row",
400
- paddingVertical: 5,
401
- gap: 12,
402
- },
403
- kvLabel: {
404
- color: night.color.textSecondary,
405
- fontSize: 11,
406
- width: 110,
407
- },
408
- kvValue: {
409
- color: night.color.text,
410
- fontSize: 11,
411
- flex: 1,
412
- },
413
- actionsRow: {
414
- flexDirection: "row",
415
- gap: 8,
416
- paddingVertical: 8,
417
- },
418
- actionButton: {
419
- backgroundColor: night.color.surfaceElevated,
420
- borderRadius: 6,
421
- paddingHorizontal: 10,
422
- paddingVertical: 6,
423
- borderWidth: StyleSheet.hairlineWidth,
424
- borderColor: night.color.border,
425
- },
426
- actionText: {
427
- color: night.color.text,
428
- fontSize: 11,
429
- fontWeight: "600",
430
- },
431
- actionDisabled: {
432
- opacity: 0.4,
433
- },
434
- urlInput: {
435
- color: night.color.text,
436
- fontSize: 11,
437
- fontFamily: "monospace",
438
- backgroundColor: night.color.surfaceElevated,
439
- borderRadius: 6,
440
- borderWidth: StyleSheet.hairlineWidth,
441
- borderColor: night.color.border,
442
- paddingHorizontal: 8,
443
- paddingVertical: 6,
444
- marginTop: 8,
445
- },
446
- overrideBanner: {
447
- flexDirection: "row",
448
- alignItems: "center",
449
- gap: 10,
450
- padding: 10,
451
- borderRadius: 8,
452
- backgroundColor: night.color.warningSoft,
453
- },
454
- overrideText: {
455
- flex: 1,
456
- color: night.color.warning,
457
- fontSize: 11,
458
- lineHeight: 15,
459
- },
460
- overrideRestore: {
461
- color: night.color.warning,
462
- fontSize: 11,
463
- fontWeight: "700",
464
- letterSpacing: 0.5,
465
- },
466
- });