@buoy-gg/images 7.0.28 → 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.
- package/lib/commonjs/components/ImagesModal.js +2 -1
- package/lib/commonjs/components/ImagesModal.js.map +1 -1
- package/lib/module/components/ImagesModal.js +2 -1
- package/lib/module/components/ImagesModal.js.map +1 -1
- package/package.json +5 -6
- package/src/capture/actions.ts +0 -215
- package/src/capture/install.ts +0 -40
- package/src/capture/installExpoImage.ts +0 -115
- package/src/capture/installRnImage.tsx +0 -133
- package/src/capture/overrides.ts +0 -332
- package/src/capture/savings.ts +0 -204
- package/src/capture/shared.ts +0 -387
- package/src/components/ImageDetail.tsx +0 -466
- package/src/components/ImageDetailFooter.tsx +0 -271
- package/src/components/ImageRow.tsx +0 -148
- package/src/components/ImagesHeader.tsx +0 -77
- package/src/components/ImagesIcon.tsx +0 -9
- package/src/components/ImagesListFooter.tsx +0 -194
- package/src/components/ImagesModal.tsx +0 -265
- package/src/components/ImagesRoot.tsx +0 -18
- package/src/components/InsightsBar.tsx +0 -51
- package/src/export.ts +0 -100
- package/src/format.ts +0 -85
- package/src/hooks/useImageRecords.ts +0 -24
- package/src/index.tsx +0 -120
- package/src/preset.tsx +0 -35
- package/src/register.ts +0 -17
- package/src/remote.ts +0 -42
- package/src/state.ts +0 -103
- package/src/store/imageEventStore.ts +0 -236
- package/src/store/insights.ts +0 -66
- package/src/sync/__tests__/imagesSyncAdapter.test.ts +0 -114
- package/src/sync/imagesSyncAdapter.ts +0 -312
- package/src/types.ts +0 -155
package/src/capture/shared.ts
DELETED
|
@@ -1,387 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Capture core shared by the RN <Image> decorator and the expo-image patch.
|
|
3
|
-
*
|
|
4
|
-
* ImageInstanceTracker: one per live image component instance. It rolls a new
|
|
5
|
-
* ImageRecord whenever the instance's source URI changes, and exposes wrapped
|
|
6
|
-
* event handlers that feed the store while still calling through to whatever
|
|
7
|
-
* handlers the app passed.
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import type { CacheVerdict, ImageLib, SourceKind } from "../types";
|
|
11
|
-
import { imagesState } from "../state";
|
|
12
|
-
import { dropActionState, settleReloadRequest } from "./actions";
|
|
13
|
-
import {
|
|
14
|
-
createRecord,
|
|
15
|
-
getRecord,
|
|
16
|
-
updateRecord,
|
|
17
|
-
} from "../store/imageEventStore";
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Images rendered by Buoy's own UI (e.g. thumbnails inside the Images modal)
|
|
21
|
-
* carry this testID prefix and are excluded from capture, so the tool doesn't
|
|
22
|
-
* pollute its own registry.
|
|
23
|
-
*/
|
|
24
|
-
export const INTERNAL_TEST_ID_PREFIX = "buoy-internal";
|
|
25
|
-
|
|
26
|
-
export function isInternalImage(testID: unknown): boolean {
|
|
27
|
-
return typeof testID === "string" && testID.startsWith(INTERNAL_TEST_ID_PREFIX);
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* React Native's own devtools UI loads images too (LogBox icons resolve to
|
|
32
|
-
* Metro asset URLs under Libraries/LogBox). They're noise in an app-focused
|
|
33
|
-
* registry — skip them.
|
|
34
|
-
*/
|
|
35
|
-
export function isNoiseUri(uri: string): boolean {
|
|
36
|
-
return (
|
|
37
|
-
uri.includes("Libraries%2FLogBox") ||
|
|
38
|
-
uri.includes("Libraries/LogBox") ||
|
|
39
|
-
uri.includes("LogBoxImages")
|
|
40
|
-
);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
export function classifyUri(uri: string): SourceKind {
|
|
44
|
-
if (uri.startsWith("http://") || uri.startsWith("https://")) return "network";
|
|
45
|
-
if (uri.startsWith("file://") || uri.startsWith("asset://")) return "file";
|
|
46
|
-
if (uri.startsWith("data:")) return "data";
|
|
47
|
-
return "other";
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Best-effort URI extraction from any source prop shape (RN or expo-image):
|
|
52
|
-
* string, number (Metro asset), {uri}, arrays of those, ImageRef.
|
|
53
|
-
*/
|
|
54
|
-
export function resolveSourceUri(source: unknown): {
|
|
55
|
-
uri: string;
|
|
56
|
-
kind: SourceKind;
|
|
57
|
-
} {
|
|
58
|
-
if (source == null) return { uri: "", kind: "other" };
|
|
59
|
-
if (Array.isArray(source)) {
|
|
60
|
-
return resolveSourceUri(source.length > 0 ? source[0] : null);
|
|
61
|
-
}
|
|
62
|
-
if (typeof source === "string") {
|
|
63
|
-
return { uri: source, kind: classifyUri(source) };
|
|
64
|
-
}
|
|
65
|
-
if (typeof source === "number") {
|
|
66
|
-
// Metro asset id. Image is guaranteed evaluated by the time an instance
|
|
67
|
-
// renders, so requiring it lazily here can never latch the decorator late.
|
|
68
|
-
try {
|
|
69
|
-
const rn = require("react-native");
|
|
70
|
-
const resolved = rn.Image?.resolveAssetSource?.(source);
|
|
71
|
-
if (resolved?.uri) return { uri: resolved.uri, kind: "asset" };
|
|
72
|
-
} catch {
|
|
73
|
-
// fall through
|
|
74
|
-
}
|
|
75
|
-
return { uri: `asset:#${source}`, kind: "asset" };
|
|
76
|
-
}
|
|
77
|
-
if (typeof source === "object") {
|
|
78
|
-
const uri = (source as { uri?: unknown }).uri;
|
|
79
|
-
if (typeof uri === "string") {
|
|
80
|
-
const kind = classifyUri(uri);
|
|
81
|
-
// require()'d assets resolve to Metro dev-server URLs in dev — still
|
|
82
|
-
// report them as assets when they carry the packager marker.
|
|
83
|
-
if ((source as { __packager_asset?: boolean }).__packager_asset) {
|
|
84
|
-
return { uri, kind: "asset" };
|
|
85
|
-
}
|
|
86
|
-
return { uri, kind };
|
|
87
|
-
}
|
|
88
|
-
// expo-image ImageRef (native shared object) has width/height but no uri.
|
|
89
|
-
const width = (source as { width?: unknown }).width;
|
|
90
|
-
if (typeof width === "number") return { uri: "(ImageRef)", kind: "other" };
|
|
91
|
-
}
|
|
92
|
-
return { uri: "", kind: "other" };
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
/** The event-handler subset we intercept, normalized across both libraries. */
|
|
96
|
-
export interface OriginalHandlers {
|
|
97
|
-
onLoadStart?: (event: unknown) => void;
|
|
98
|
-
onProgress?: (event: unknown) => void;
|
|
99
|
-
onLoad?: (event: unknown) => void;
|
|
100
|
-
onError?: (event: unknown) => void;
|
|
101
|
-
onLoadEnd?: (event: unknown) => void;
|
|
102
|
-
onDisplay?: (event: unknown) => void;
|
|
103
|
-
onLayout?: (event: unknown) => void;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
export type WrappedHandlers = Required<
|
|
107
|
-
Pick<
|
|
108
|
-
OriginalHandlers,
|
|
109
|
-
"onLoadStart" | "onProgress" | "onLoad" | "onError" | "onLayout"
|
|
110
|
-
>
|
|
111
|
-
> &
|
|
112
|
-
Pick<OriginalHandlers, "onLoadEnd" | "onDisplay">;
|
|
113
|
-
|
|
114
|
-
/** RN events arrive as {nativeEvent}; expo-image passes the payload directly. */
|
|
115
|
-
function payloadOf(event: unknown): Record<string, unknown> {
|
|
116
|
-
if (event && typeof event === "object") {
|
|
117
|
-
const native = (event as { nativeEvent?: unknown }).nativeEvent;
|
|
118
|
-
if (native && typeof native === "object") {
|
|
119
|
-
return native as Record<string, unknown>;
|
|
120
|
-
}
|
|
121
|
-
return event as Record<string, unknown>;
|
|
122
|
-
}
|
|
123
|
-
return {};
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
/**
|
|
127
|
-
* After an RN-core load, classify the cache via Image.queryCache.
|
|
128
|
-
*
|
|
129
|
-
* Delayed + retried: NSURLCache commits fresh downloads asynchronously, so a
|
|
130
|
-
* query at the instant of onLoad reports a miss for images that ARE being
|
|
131
|
-
* cached (observed live). First probe after 600ms, one re-probe at 3s to
|
|
132
|
-
* catch slow cache writes.
|
|
133
|
-
*/
|
|
134
|
-
function queryRnCache(recordId: number, uri: string): void {
|
|
135
|
-
const probe = () => {
|
|
136
|
-
try {
|
|
137
|
-
const rn = require("react-native");
|
|
138
|
-
const queryCache: undefined | ((uris: string[]) => Promise<Record<string, string>>) =
|
|
139
|
-
rn.Image?.queryCache;
|
|
140
|
-
if (typeof queryCache !== "function" || !uri) return;
|
|
141
|
-
queryCache([uri])
|
|
142
|
-
.then((result) => {
|
|
143
|
-
const verdict = result?.[uri] as CacheVerdict | undefined;
|
|
144
|
-
if (verdict) {
|
|
145
|
-
updateRecord(recordId, {
|
|
146
|
-
cacheVerdict: verdict,
|
|
147
|
-
cacheVerdictSource: "queryCache",
|
|
148
|
-
});
|
|
149
|
-
}
|
|
150
|
-
})
|
|
151
|
-
.catch(() => {});
|
|
152
|
-
} catch {
|
|
153
|
-
// queryCache unavailable (web) — leave verdict unknown.
|
|
154
|
-
}
|
|
155
|
-
};
|
|
156
|
-
setTimeout(probe, 600);
|
|
157
|
-
setTimeout(() => {
|
|
158
|
-
const record = getRecord(recordId);
|
|
159
|
-
if (record && record.cacheVerdictSource !== "queryCache") probe();
|
|
160
|
-
}, 3000);
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
export class ImageInstanceTracker {
|
|
164
|
-
private readonly lib: ImageLib;
|
|
165
|
-
private recordId: number | null = null;
|
|
166
|
-
private uri: string | null = null;
|
|
167
|
-
private handlerCache: { key: unknown[]; out: WrappedHandlers } | null = null;
|
|
168
|
-
|
|
169
|
-
constructor(lib: ImageLib) {
|
|
170
|
-
this.lib = lib;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
getRecordId(): number | null {
|
|
174
|
-
return this.recordId;
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
/** Record accessibility presence (called per render; writes only on change). */
|
|
178
|
-
noteA11y(hasAltText: boolean): void {
|
|
179
|
-
if (this.recordId == null) return;
|
|
180
|
-
const record = getRecord(this.recordId);
|
|
181
|
-
if (record && record.hasAltText !== hasAltText) {
|
|
182
|
-
updateRecord(this.recordId, { hasAltText });
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
/** Call on every render with the current source prop. Rolls records on change. */
|
|
187
|
-
syncSource(source: unknown): void {
|
|
188
|
-
const { uri, kind } = resolveSourceUri(source);
|
|
189
|
-
if (uri === this.uri) return;
|
|
190
|
-
this.uri = uri;
|
|
191
|
-
this.handlerCache = null; // handlers close over recordId via getter, but roll anyway
|
|
192
|
-
if (this.recordId != null) dropActionState(this.recordId);
|
|
193
|
-
if (!uri || isNoiseUri(uri)) {
|
|
194
|
-
this.recordId = null;
|
|
195
|
-
return;
|
|
196
|
-
}
|
|
197
|
-
const record = createRecord({ lib: this.lib, uri, sourceKind: kind });
|
|
198
|
-
this.recordId = record.id;
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
unmount(): void {
|
|
202
|
-
if (this.recordId != null) {
|
|
203
|
-
updateRecord(this.recordId, { mounted: false });
|
|
204
|
-
dropActionState(this.recordId);
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
/**
|
|
209
|
-
* Wrapped handlers for the current originals. Cached by handler identity so
|
|
210
|
-
* re-renders with unchanged props reuse the same functions.
|
|
211
|
-
*/
|
|
212
|
-
handlers(orig: OriginalHandlers): WrappedHandlers {
|
|
213
|
-
const key = [
|
|
214
|
-
orig.onLoadStart,
|
|
215
|
-
orig.onProgress,
|
|
216
|
-
orig.onLoad,
|
|
217
|
-
orig.onError,
|
|
218
|
-
orig.onLoadEnd,
|
|
219
|
-
orig.onDisplay,
|
|
220
|
-
orig.onLayout,
|
|
221
|
-
this.recordId,
|
|
222
|
-
];
|
|
223
|
-
const cached = this.handlerCache;
|
|
224
|
-
if (cached && cached.key.length === key.length && cached.key.every((k, i) => k === key[i])) {
|
|
225
|
-
return cached.out;
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
const recordId = this.recordId;
|
|
229
|
-
const lib = this.lib;
|
|
230
|
-
const uri = this.uri ?? "";
|
|
231
|
-
|
|
232
|
-
const out: WrappedHandlers = {
|
|
233
|
-
onLoadStart: (event) => {
|
|
234
|
-
if (recordId != null) {
|
|
235
|
-
const record = getRecord(recordId);
|
|
236
|
-
const now = Date.now();
|
|
237
|
-
// Out-of-order defense (seen live on iOS remounts/cache hits AND
|
|
238
|
-
// instant file:// failures): the native onLoadStart of a burst can
|
|
239
|
-
// arrive AFTER its onLoad/onError. A "start" right after a settled
|
|
240
|
-
// outcome belongs to that same burst — don't regress the record.
|
|
241
|
-
const settledAt = Math.max(record?.loadedAt ?? 0, record?.erroredAt ?? 0);
|
|
242
|
-
const settledJustNow = settledAt > 0 && now - settledAt < 300;
|
|
243
|
-
updateRecord(recordId, {
|
|
244
|
-
...(settledJustNow ? {} : { status: "loading", error: undefined, errorCode: undefined, errorHeaders: undefined }),
|
|
245
|
-
startedAt: now,
|
|
246
|
-
loadCount: (record?.loadCount ?? 0) + 1,
|
|
247
|
-
});
|
|
248
|
-
}
|
|
249
|
-
orig.onLoadStart?.(event);
|
|
250
|
-
},
|
|
251
|
-
onProgress: (event) => {
|
|
252
|
-
if (recordId != null) {
|
|
253
|
-
const p = payloadOf(event);
|
|
254
|
-
updateRecord(recordId, {
|
|
255
|
-
progressSeen: true,
|
|
256
|
-
bytesLoaded: typeof p.loaded === "number" ? p.loaded : undefined,
|
|
257
|
-
bytesTotal: typeof p.total === "number" ? p.total : undefined,
|
|
258
|
-
});
|
|
259
|
-
}
|
|
260
|
-
orig.onProgress?.(event);
|
|
261
|
-
},
|
|
262
|
-
onLoad: (event) => {
|
|
263
|
-
if (recordId != null) {
|
|
264
|
-
const p = payloadOf(event);
|
|
265
|
-
const source = (p.source ?? {}) as Record<string, unknown>;
|
|
266
|
-
const now = Date.now();
|
|
267
|
-
const record = getRecord(recordId);
|
|
268
|
-
// Duration only counts from a start belonging to THIS load cycle: a
|
|
269
|
-
// start newer than the previous completion. Without one (instant
|
|
270
|
-
// cache hit whose onLoadStart arrives late/never), report 0ms.
|
|
271
|
-
const start =
|
|
272
|
-
record?.startedAt != null &&
|
|
273
|
-
record.startedAt <= now &&
|
|
274
|
-
record.startedAt > (record.loadedAt ?? record.createdAt - 1)
|
|
275
|
-
? record.startedAt
|
|
276
|
-
: record?.loadedAt == null
|
|
277
|
-
? (record?.startedAt ?? record?.createdAt ?? now)
|
|
278
|
-
: now;
|
|
279
|
-
const width = typeof source.width === "number" ? source.width : 0;
|
|
280
|
-
const height = typeof source.height === "number" ? source.height : 0;
|
|
281
|
-
updateRecord(recordId, {
|
|
282
|
-
status: "loaded",
|
|
283
|
-
loadedAt: now,
|
|
284
|
-
durationMs: Math.max(0, now - start),
|
|
285
|
-
error: undefined,
|
|
286
|
-
errorCode: undefined,
|
|
287
|
-
errorHeaders: undefined,
|
|
288
|
-
intrinsic: width > 0 && height > 0 ? { width, height } : record?.intrinsic,
|
|
289
|
-
cacheVerdict:
|
|
290
|
-
typeof p.cacheType === "string"
|
|
291
|
-
? (p.cacheType as CacheVerdict)
|
|
292
|
-
: record?.cacheVerdict,
|
|
293
|
-
cacheVerdictSource:
|
|
294
|
-
typeof p.cacheType === "string" ? "event" : record?.cacheVerdictSource,
|
|
295
|
-
mediaType:
|
|
296
|
-
typeof source.mediaType === "string" || source.mediaType === null
|
|
297
|
-
? (source.mediaType as string | null)
|
|
298
|
-
: record?.mediaType,
|
|
299
|
-
isAnimated:
|
|
300
|
-
typeof source.isAnimated === "boolean"
|
|
301
|
-
? source.isAnimated
|
|
302
|
-
: record?.isAnimated,
|
|
303
|
-
});
|
|
304
|
-
settleReloadRequest(recordId);
|
|
305
|
-
if (lib === "rn") queryRnCache(recordId, uri);
|
|
306
|
-
}
|
|
307
|
-
orig.onLoad?.(event);
|
|
308
|
-
},
|
|
309
|
-
onError: (event) => {
|
|
310
|
-
if (recordId != null) {
|
|
311
|
-
const p = payloadOf(event);
|
|
312
|
-
let message =
|
|
313
|
-
typeof p.error === "string"
|
|
314
|
-
? p.error
|
|
315
|
-
: String((p.error as { message?: string })?.message ?? "unknown error");
|
|
316
|
-
// Simulation sources produce raw file-not-found noise — translate to
|
|
317
|
-
// an honest label (state-based, so it works for both libs' error
|
|
318
|
-
// texts), in our record AND in the event the app receives.
|
|
319
|
-
const record0 = getRecord(recordId);
|
|
320
|
-
const s = imagesState();
|
|
321
|
-
const activeOverride = s.overrides.get(recordId) as
|
|
322
|
-
| { source?: { type?: string } }
|
|
323
|
-
| undefined;
|
|
324
|
-
if (activeOverride?.source?.type === "error") {
|
|
325
|
-
message = "Forced error (Buoy Images simulation)";
|
|
326
|
-
if (typeof p.error === "string") p.error = message;
|
|
327
|
-
} else if (
|
|
328
|
-
s.globalModes.network === "offline" &&
|
|
329
|
-
record0?.sourceKind === "network"
|
|
330
|
-
) {
|
|
331
|
-
message = "Offline simulation (Buoy Images): network image blocked";
|
|
332
|
-
if (typeof p.error === "string") p.error = message;
|
|
333
|
-
}
|
|
334
|
-
updateRecord(recordId, {
|
|
335
|
-
status: "error",
|
|
336
|
-
erroredAt: Date.now(),
|
|
337
|
-
error: message,
|
|
338
|
-
errorCode: typeof p.responseCode === "number" ? p.responseCode : undefined,
|
|
339
|
-
errorHeaders:
|
|
340
|
-
p.httpResponseHeaders && typeof p.httpResponseHeaders === "object"
|
|
341
|
-
? (p.httpResponseHeaders as Record<string, string>)
|
|
342
|
-
: undefined,
|
|
343
|
-
});
|
|
344
|
-
settleReloadRequest(recordId);
|
|
345
|
-
}
|
|
346
|
-
orig.onError?.(event);
|
|
347
|
-
},
|
|
348
|
-
onLayout: (event) => {
|
|
349
|
-
if (recordId != null) {
|
|
350
|
-
const layout = (payloadOf(event).layout ?? {}) as Record<string, unknown>;
|
|
351
|
-
const width = typeof layout.width === "number" ? layout.width : 0;
|
|
352
|
-
const height = typeof layout.height === "number" ? layout.height : 0;
|
|
353
|
-
if (width > 0 && height > 0) {
|
|
354
|
-
const record = getRecord(recordId);
|
|
355
|
-
const prev = record?.layout;
|
|
356
|
-
// Size change AFTER the image loaded = layout-shift suspect (the
|
|
357
|
-
// image arriving resized its box — reserve explicit dimensions).
|
|
358
|
-
const shifted =
|
|
359
|
-
prev != null &&
|
|
360
|
-
record?.loadedAt != null &&
|
|
361
|
-
(Math.abs(prev.width - width) > 1 || Math.abs(prev.height - height) > 1);
|
|
362
|
-
updateRecord(recordId, {
|
|
363
|
-
layout: { width, height },
|
|
364
|
-
...(shifted ? { layoutShifts: (record?.layoutShifts ?? 0) + 1 } : {}),
|
|
365
|
-
});
|
|
366
|
-
}
|
|
367
|
-
}
|
|
368
|
-
orig.onLayout?.(event);
|
|
369
|
-
},
|
|
370
|
-
};
|
|
371
|
-
|
|
372
|
-
// Only forward-wrap the optional ones so we don't attach handlers a
|
|
373
|
-
// library doesn't support (onDisplay is expo-only; onLoadEnd is RN-only).
|
|
374
|
-
out.onLoadEnd = orig.onLoadEnd
|
|
375
|
-
? (event) => orig.onLoadEnd?.(event)
|
|
376
|
-
: undefined;
|
|
377
|
-
if (this.lib === "expo") {
|
|
378
|
-
out.onDisplay = (event) => {
|
|
379
|
-
if (recordId != null) updateRecord(recordId, { displayedAt: Date.now() });
|
|
380
|
-
orig.onDisplay?.(event);
|
|
381
|
-
};
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
this.handlerCache = { key, out };
|
|
385
|
-
return out;
|
|
386
|
-
}
|
|
387
|
-
}
|