@buoy-gg/images 7.0.41 → 7.0.43
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 +11 -70
- package/lib/commonjs/autoCapture.js +1 -4
- package/lib/commonjs/autoCapture.web.js +1 -6
- package/lib/commonjs/capture/actions.js +1 -243
- package/lib/commonjs/capture/install.js +1 -45
- package/lib/commonjs/capture/install.web.js +1 -250
- package/lib/commonjs/capture/installExpoImage.js +1 -99
- package/lib/commonjs/capture/installRnImage.js +1 -132
- package/lib/commonjs/capture/nativeImageModules.js +1 -30
- package/lib/commonjs/capture/nativeImageModules.web.js +1 -18
- package/lib/commonjs/capture/overrides.js +1 -346
- package/lib/commonjs/capture/rnDecoratorEarly.js +1 -55
- package/lib/commonjs/capture/savings.js +1 -209
- package/lib/commonjs/capture/savings.web.js +1 -71
- package/lib/commonjs/capture/shared.js +1 -378
- package/lib/commonjs/components/ImageDetail.js +1 -455
- package/lib/commonjs/components/ImageDetailFooter.js +1 -158
- package/lib/commonjs/components/ImageRow.js +1 -156
- package/lib/commonjs/components/ImagesHeader.js +1 -63
- package/lib/commonjs/components/ImagesIcon.js +1 -19
- package/lib/commonjs/components/ImagesListFooter.js +1 -126
- package/lib/commonjs/components/ImagesModal.js +1 -240
- package/lib/commonjs/components/ImagesRoot.js +1 -24
- package/lib/commonjs/components/ImagesRoot.web.js +1 -13
- package/lib/commonjs/components/InsightsBar.js +1 -59
- package/lib/commonjs/export.js +1 -81
- package/lib/commonjs/format.js +1 -117
- package/lib/commonjs/hooks/useImageRecords.js +1 -20
- package/lib/commonjs/index.js +1 -261
- package/lib/commonjs/preset.js +1 -39
- package/lib/commonjs/register.js +1 -19
- package/lib/commonjs/registerEarly.js +1 -14
- package/lib/commonjs/remote.js +1 -42
- package/lib/commonjs/state.js +1 -58
- package/lib/commonjs/store/imageEventStore.js +1 -238
- package/lib/commonjs/store/insights.js +1 -76
- package/lib/commonjs/sync/imagesSyncAdapter.js +1 -361
- package/lib/commonjs/types.js +0 -1
- package/lib/module/autoCapture.js +1 -4
- package/lib/module/autoCapture.web.js +1 -5
- package/lib/module/capture/actions.js +1 -230
- package/lib/module/capture/install.js +1 -40
- package/lib/module/capture/install.web.js +1 -243
- package/lib/module/capture/installExpoImage.js +1 -94
- package/lib/module/capture/installRnImage.js +1 -125
- package/lib/module/capture/nativeImageModules.js +1 -24
- package/lib/module/capture/nativeImageModules.web.js +1 -12
- package/lib/module/capture/overrides.js +1 -329
- package/lib/module/capture/rnDecoratorEarly.js +1 -50
- package/lib/module/capture/savings.js +1 -205
- package/lib/module/capture/savings.web.js +1 -67
- package/lib/module/capture/shared.js +1 -369
- package/lib/module/components/ImageDetail.js +1 -451
- package/lib/module/components/ImageDetailFooter.js +1 -154
- package/lib/module/components/ImageRow.js +1 -152
- package/lib/module/components/ImagesHeader.js +1 -59
- package/lib/module/components/ImagesIcon.js +1 -12
- package/lib/module/components/ImagesListFooter.js +1 -122
- package/lib/module/components/ImagesModal.js +1 -235
- package/lib/module/components/ImagesRoot.js +1 -20
- package/lib/module/components/ImagesRoot.web.js +1 -9
- package/lib/module/components/InsightsBar.js +1 -55
- package/lib/module/export.js +1 -76
- package/lib/module/format.js +1 -108
- package/lib/module/hooks/useImageRecords.js +1 -16
- package/lib/module/index.js +1 -79
- package/lib/module/preset.js +1 -35
- package/lib/module/register.js +1 -19
- package/lib/module/registerEarly.js +1 -14
- package/lib/module/remote.js +1 -36
- package/lib/module/state.js +1 -54
- package/lib/module/store/imageEventStore.js +1 -221
- package/lib/module/store/insights.js +1 -71
- package/lib/module/sync/imagesSyncAdapter.js +1 -356
- package/lib/module/types.js +0 -1
- package/lib/web/chunks/chunk-TTS5EH4R.mjs +1 -2
- package/lib/web/index.mjs +3 -4
- package/lib/web/register.mjs +1 -2
- package/package.json +3 -7
- package/register.js +1 -15
- package/lib/commonjs/autoCapture.js.map +0 -1
- package/lib/commonjs/autoCapture.web.js.map +0 -1
- package/lib/commonjs/capture/actions.js.map +0 -1
- package/lib/commonjs/capture/install.js.map +0 -1
- package/lib/commonjs/capture/install.web.js.map +0 -1
- package/lib/commonjs/capture/installExpoImage.js.map +0 -1
- package/lib/commonjs/capture/installRnImage.js.map +0 -1
- package/lib/commonjs/capture/nativeImageModules.js.map +0 -1
- package/lib/commonjs/capture/nativeImageModules.web.js.map +0 -1
- package/lib/commonjs/capture/overrides.js.map +0 -1
- package/lib/commonjs/capture/rnDecoratorEarly.js.map +0 -1
- package/lib/commonjs/capture/savings.js.map +0 -1
- package/lib/commonjs/capture/savings.web.js.map +0 -1
- package/lib/commonjs/capture/shared.js.map +0 -1
- package/lib/commonjs/components/ImageDetail.js.map +0 -1
- package/lib/commonjs/components/ImageDetailFooter.js.map +0 -1
- package/lib/commonjs/components/ImageRow.js.map +0 -1
- package/lib/commonjs/components/ImagesHeader.js.map +0 -1
- package/lib/commonjs/components/ImagesIcon.js.map +0 -1
- package/lib/commonjs/components/ImagesListFooter.js.map +0 -1
- package/lib/commonjs/components/ImagesModal.js.map +0 -1
- package/lib/commonjs/components/ImagesRoot.js.map +0 -1
- package/lib/commonjs/components/ImagesRoot.web.js.map +0 -1
- package/lib/commonjs/components/InsightsBar.js.map +0 -1
- package/lib/commonjs/export.js.map +0 -1
- package/lib/commonjs/format.js.map +0 -1
- package/lib/commonjs/hooks/useImageRecords.js.map +0 -1
- package/lib/commonjs/index.js.map +0 -1
- package/lib/commonjs/preset.js.map +0 -1
- package/lib/commonjs/register.js.map +0 -1
- package/lib/commonjs/registerEarly.js.map +0 -1
- package/lib/commonjs/remote.js.map +0 -1
- package/lib/commonjs/state.js.map +0 -1
- package/lib/commonjs/store/imageEventStore.js.map +0 -1
- package/lib/commonjs/store/insights.js.map +0 -1
- package/lib/commonjs/sync/imagesSyncAdapter.js.map +0 -1
- package/lib/commonjs/types.js.map +0 -1
- package/lib/module/autoCapture.js.map +0 -1
- package/lib/module/autoCapture.web.js.map +0 -1
- package/lib/module/capture/actions.js.map +0 -1
- package/lib/module/capture/install.js.map +0 -1
- package/lib/module/capture/install.web.js.map +0 -1
- package/lib/module/capture/installExpoImage.js.map +0 -1
- package/lib/module/capture/installRnImage.js.map +0 -1
- package/lib/module/capture/nativeImageModules.js.map +0 -1
- package/lib/module/capture/nativeImageModules.web.js.map +0 -1
- package/lib/module/capture/overrides.js.map +0 -1
- package/lib/module/capture/rnDecoratorEarly.js.map +0 -1
- package/lib/module/capture/savings.js.map +0 -1
- package/lib/module/capture/savings.web.js.map +0 -1
- package/lib/module/capture/shared.js.map +0 -1
- package/lib/module/components/ImageDetail.js.map +0 -1
- package/lib/module/components/ImageDetailFooter.js.map +0 -1
- package/lib/module/components/ImageRow.js.map +0 -1
- package/lib/module/components/ImagesHeader.js.map +0 -1
- package/lib/module/components/ImagesIcon.js.map +0 -1
- package/lib/module/components/ImagesListFooter.js.map +0 -1
- package/lib/module/components/ImagesModal.js.map +0 -1
- package/lib/module/components/ImagesRoot.js.map +0 -1
- package/lib/module/components/ImagesRoot.web.js.map +0 -1
- package/lib/module/components/InsightsBar.js.map +0 -1
- package/lib/module/export.js.map +0 -1
- package/lib/module/format.js.map +0 -1
- package/lib/module/hooks/useImageRecords.js.map +0 -1
- package/lib/module/index.js.map +0 -1
- package/lib/module/preset.js.map +0 -1
- package/lib/module/register.js.map +0 -1
- package/lib/module/registerEarly.js.map +0 -1
- package/lib/module/remote.js.map +0 -1
- package/lib/module/state.js.map +0 -1
- package/lib/module/store/imageEventStore.js.map +0 -1
- package/lib/module/store/insights.js.map +0 -1
- package/lib/module/sync/imagesSyncAdapter.js.map +0 -1
- package/lib/module/types.js.map +0 -1
- package/lib/typescript/autoCapture.d.ts.map +0 -1
- package/lib/typescript/autoCapture.web.d.ts.map +0 -1
- package/lib/typescript/capture/actions.d.ts.map +0 -1
- package/lib/typescript/capture/install.d.ts.map +0 -1
- package/lib/typescript/capture/install.web.d.ts.map +0 -1
- package/lib/typescript/capture/installExpoImage.d.ts.map +0 -1
- package/lib/typescript/capture/installRnImage.d.ts.map +0 -1
- package/lib/typescript/capture/nativeImageModules.d.ts.map +0 -1
- package/lib/typescript/capture/nativeImageModules.web.d.ts.map +0 -1
- package/lib/typescript/capture/overrides.d.ts.map +0 -1
- package/lib/typescript/capture/rnDecoratorEarly.d.ts.map +0 -1
- package/lib/typescript/capture/savings.d.ts.map +0 -1
- package/lib/typescript/capture/savings.web.d.ts.map +0 -1
- package/lib/typescript/capture/shared.d.ts.map +0 -1
- package/lib/typescript/components/ImageDetail.d.ts.map +0 -1
- package/lib/typescript/components/ImageDetailFooter.d.ts.map +0 -1
- package/lib/typescript/components/ImageRow.d.ts.map +0 -1
- package/lib/typescript/components/ImagesHeader.d.ts.map +0 -1
- package/lib/typescript/components/ImagesIcon.d.ts.map +0 -1
- package/lib/typescript/components/ImagesListFooter.d.ts.map +0 -1
- package/lib/typescript/components/ImagesModal.d.ts.map +0 -1
- package/lib/typescript/components/ImagesRoot.d.ts.map +0 -1
- package/lib/typescript/components/ImagesRoot.web.d.ts.map +0 -1
- package/lib/typescript/components/InsightsBar.d.ts.map +0 -1
- package/lib/typescript/export.d.ts.map +0 -1
- package/lib/typescript/format.d.ts.map +0 -1
- package/lib/typescript/hooks/useImageRecords.d.ts.map +0 -1
- package/lib/typescript/index.d.ts.map +0 -1
- package/lib/typescript/preset.d.ts.map +0 -1
- package/lib/typescript/register.d.ts.map +0 -1
- package/lib/typescript/registerEarly.d.ts.map +0 -1
- package/lib/typescript/remote.d.ts.map +0 -1
- package/lib/typescript/state.d.ts.map +0 -1
- package/lib/typescript/store/imageEventStore.d.ts.map +0 -1
- package/lib/typescript/store/insights.d.ts.map +0 -1
- package/lib/typescript/sync/imagesSyncAdapter.d.ts.map +0 -1
- package/lib/typescript/types.d.ts.map +0 -1
- package/lib/web/chunks/chunk-7CWLRQ2L.mjs +0 -2
- package/lib/web/chunks/chunk-B5A6CYSS.mjs +0 -2
- package/lib/web/chunks/chunk-LKTR6X2V.mjs +0 -2
|
@@ -1,329 +1 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Simulation overrides — the "we own the props" tier of actions.
|
|
5
|
-
*
|
|
6
|
-
* Because the RN decorator and the expo-image render patch control every
|
|
7
|
-
* mounted image's props, anything the component accepts is a simulation:
|
|
8
|
-
* forced errors (instant, offline-safe file:// source), forced loading
|
|
9
|
-
* (blackhole-IP source that never resolves), URL swaps, blanking, style
|
|
10
|
-
* merges (locate-flash border), and app-wide modes (offline images /
|
|
11
|
-
* cold-start / blank images).
|
|
12
|
-
*
|
|
13
|
-
* Re-render plumbing: every mutation bumps a per-record or global version.
|
|
14
|
-
* RN wrappers subscribe via useSyncExternalStore with a per-record snapshot
|
|
15
|
-
* (only the touched image re-renders); expo class instances are re-rendered
|
|
16
|
-
* directly via forceUpdate() from the per-record instance registry.
|
|
17
|
-
*/
|
|
18
|
-
|
|
19
|
-
import { remoteImagesDispatch } from "../remote";
|
|
20
|
-
import { imagesState } from "../state";
|
|
21
|
-
import { getRecord, getSnapshot, updateRecord } from "../store/imageEventStore";
|
|
22
|
-
|
|
23
|
-
// ---------------------------------------------------------------------------
|
|
24
|
-
// Override model
|
|
25
|
-
// ---------------------------------------------------------------------------
|
|
26
|
-
|
|
27
|
-
const FORCED_ERROR_URI = "file:///buoy-images-forced-error.png";
|
|
28
|
-
const HANG_URI = "https://10.255.255.1/buoy-images-hang.png";
|
|
29
|
-
/** Offline mode replaces network sources with this (fails immediately). */
|
|
30
|
-
const OFFLINE_URI = "file:///buoy-images-offline-no-network.png";
|
|
31
|
-
const FLASH_STYLE = {
|
|
32
|
-
borderWidth: 3,
|
|
33
|
-
borderColor: "#FF453A"
|
|
34
|
-
};
|
|
35
|
-
/**
|
|
36
|
-
* RN core has NO safe "empty source": undefined resolves to the app bundle
|
|
37
|
-
* path and [] does too on RN 0.85 — both fire a bogus onError ("The file …
|
|
38
|
-
* couldn't be opened", observed live). So RN images blank via opacity
|
|
39
|
-
* instead (visual-only; the decoded image stays resident). expo-image
|
|
40
|
-
* supports a true `source={null}` and keeps showing its placeholder.
|
|
41
|
-
*/
|
|
42
|
-
const RN_BLANK_STYLE = {
|
|
43
|
-
opacity: 0
|
|
44
|
-
};
|
|
45
|
-
// ---------------------------------------------------------------------------
|
|
46
|
-
// Version bus (re-render triggers)
|
|
47
|
-
// ---------------------------------------------------------------------------
|
|
48
|
-
|
|
49
|
-
function emit() {
|
|
50
|
-
for (const l of imagesState().actionListeners) l();
|
|
51
|
-
}
|
|
52
|
-
function forceUpdateExpo(recordId) {
|
|
53
|
-
const s = imagesState();
|
|
54
|
-
if (recordId != null) {
|
|
55
|
-
const instance = s.expoInstances.get(recordId);
|
|
56
|
-
instance?.forceUpdate?.();
|
|
57
|
-
return;
|
|
58
|
-
}
|
|
59
|
-
for (const instance of s.expoInstances.values()) {
|
|
60
|
-
instance?.forceUpdate?.();
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/** Bump one record's render version (re-renders just that image). */
|
|
65
|
-
export function touchRecord(recordId) {
|
|
66
|
-
const s = imagesState();
|
|
67
|
-
s.recordVersions.set(recordId, ++s.versionCounter);
|
|
68
|
-
emit();
|
|
69
|
-
forceUpdateExpo(recordId);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
/** Bump the global render version (re-renders every wrapped image). */
|
|
73
|
-
export function touchGlobal() {
|
|
74
|
-
const s = imagesState();
|
|
75
|
-
s.globalVersion = ++s.versionCounter;
|
|
76
|
-
emit();
|
|
77
|
-
forceUpdateExpo();
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
/** Snapshot for the RN wrapper's useSyncExternalStore. */
|
|
81
|
-
export function getRenderVersion(recordId) {
|
|
82
|
-
const s = imagesState();
|
|
83
|
-
const record = recordId != null ? s.recordVersions.get(recordId) ?? 0 : 0;
|
|
84
|
-
return Math.max(record, s.globalVersion);
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
// ---------------------------------------------------------------------------
|
|
88
|
-
// Per-record overrides
|
|
89
|
-
// ---------------------------------------------------------------------------
|
|
90
|
-
|
|
91
|
-
/** Remote param shape for the adapter's `setOverride` action. */
|
|
92
|
-
function overrideParams(recordId, source) {
|
|
93
|
-
return source.type === "url" ? {
|
|
94
|
-
id: recordId,
|
|
95
|
-
kind: "url",
|
|
96
|
-
uri: source.uri
|
|
97
|
-
} : {
|
|
98
|
-
id: recordId,
|
|
99
|
-
kind: source.type
|
|
100
|
-
};
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
/**
|
|
104
|
-
* Local bookkeeping only (no remote forward) — mirrors keep this as an
|
|
105
|
-
* optimistic UI update; the next device snapshot is the source of truth.
|
|
106
|
-
*/
|
|
107
|
-
function setOverrideLocal(recordId, override) {
|
|
108
|
-
imagesState().overrides.set(recordId, override);
|
|
109
|
-
updateRecord(recordId, {
|
|
110
|
-
overrideLabel: override.label
|
|
111
|
-
});
|
|
112
|
-
touchRecord(recordId);
|
|
113
|
-
}
|
|
114
|
-
export function setOverride(recordId, override) {
|
|
115
|
-
if (override.source) {
|
|
116
|
-
void remoteImagesDispatch()?.("setOverride", overrideParams(recordId, override.source));
|
|
117
|
-
}
|
|
118
|
-
setOverrideLocal(recordId, override);
|
|
119
|
-
}
|
|
120
|
-
function clearOverrideLocal(recordId) {
|
|
121
|
-
imagesState().overrides.delete(recordId);
|
|
122
|
-
updateRecord(recordId, {
|
|
123
|
-
overrideLabel: undefined
|
|
124
|
-
});
|
|
125
|
-
touchRecord(recordId);
|
|
126
|
-
}
|
|
127
|
-
export function clearOverride(recordId) {
|
|
128
|
-
void remoteImagesDispatch()?.("clearOverride", {
|
|
129
|
-
id: recordId
|
|
130
|
-
});
|
|
131
|
-
clearOverrideLocal(recordId);
|
|
132
|
-
}
|
|
133
|
-
export function getOverride(recordId) {
|
|
134
|
-
if (recordId == null) return undefined;
|
|
135
|
-
return imagesState().overrides.get(recordId);
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
// ---------------------------------------------------------------------------
|
|
139
|
-
// Mass actions (every mounted image at once)
|
|
140
|
-
// ---------------------------------------------------------------------------
|
|
141
|
-
|
|
142
|
-
/** Adapter `massAction` kind for a mass override (per-record 'hang' is mass 'loading'). */
|
|
143
|
-
function massKind(source) {
|
|
144
|
-
return source.type === "hang" ? "loading" : source.type;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
/** Apply one override to every mounted record. Returns how many were hit. */
|
|
148
|
-
export function setOverrideForAllMounted(source, label) {
|
|
149
|
-
// One mass action on the device, not N per-record forwards.
|
|
150
|
-
void remoteImagesDispatch()?.("massAction", {
|
|
151
|
-
kind: massKind(source)
|
|
152
|
-
});
|
|
153
|
-
let count = 0;
|
|
154
|
-
for (const record of getSnapshot()) {
|
|
155
|
-
if (!record.mounted) continue;
|
|
156
|
-
setOverrideLocal(record.id, {
|
|
157
|
-
source,
|
|
158
|
-
label
|
|
159
|
-
});
|
|
160
|
-
count++;
|
|
161
|
-
}
|
|
162
|
-
return count;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
/** Remove every active override. Returns how many were cleared. */
|
|
166
|
-
export function clearAllOverrides() {
|
|
167
|
-
void remoteImagesDispatch()?.("massAction", {
|
|
168
|
-
kind: "restore"
|
|
169
|
-
});
|
|
170
|
-
// Mirrors track labels on records, not the overrides map — clear both.
|
|
171
|
-
const ids = new Set(imagesState().overrides.keys());
|
|
172
|
-
for (const record of getSnapshot()) {
|
|
173
|
-
if (record.overrideLabel != null) ids.add(record.id);
|
|
174
|
-
}
|
|
175
|
-
for (const id of ids) clearOverrideLocal(id);
|
|
176
|
-
return ids.size;
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
/** Flash every mounted image (locate everything the tool tracks). */
|
|
180
|
-
export function flashAllMounted(durationMs = 2500) {
|
|
181
|
-
void remoteImagesDispatch()?.("massAction", {
|
|
182
|
-
kind: "flash"
|
|
183
|
-
});
|
|
184
|
-
let count = 0;
|
|
185
|
-
for (const record of getSnapshot()) {
|
|
186
|
-
if (!record.mounted) continue;
|
|
187
|
-
flashRecordLocal(record.id, durationMs);
|
|
188
|
-
count++;
|
|
189
|
-
}
|
|
190
|
-
return count;
|
|
191
|
-
}
|
|
192
|
-
function flashRecordLocal(recordId, durationMs = 2500) {
|
|
193
|
-
const s = imagesState();
|
|
194
|
-
s.flashUntil.set(recordId, Date.now() + durationMs);
|
|
195
|
-
touchRecord(recordId);
|
|
196
|
-
setTimeout(() => {
|
|
197
|
-
imagesState().flashUntil.delete(recordId);
|
|
198
|
-
touchRecord(recordId);
|
|
199
|
-
}, durationMs + 50);
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
/** Flash a border on the on-screen image so it can be located visually. */
|
|
203
|
-
export function flashRecord(recordId, durationMs = 2500) {
|
|
204
|
-
void remoteImagesDispatch()?.("flash", {
|
|
205
|
-
id: recordId
|
|
206
|
-
});
|
|
207
|
-
flashRecordLocal(recordId, durationMs);
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
// ---------------------------------------------------------------------------
|
|
211
|
-
// Global simulation modes
|
|
212
|
-
// ---------------------------------------------------------------------------
|
|
213
|
-
|
|
214
|
-
/**
|
|
215
|
-
* offline — every NETWORK image load fails immediately (both libs, cache or
|
|
216
|
-
* not); bundled/local assets still load, like a real offline device showing
|
|
217
|
-
* shipped images. Blunt on purpose: toggle + reload = images visibly gone.
|
|
218
|
-
* (The subtler `only-if-cached` variant was correct-but-confusing, and its
|
|
219
|
-
* uncached requests HANG on iOS instead of erroring — bad for QA.)
|
|
220
|
-
* cold — every image bypasses caches (RN `cache:'reload'`, expo
|
|
221
|
-
* `cachePolicy:'none'`): first-launch behavior without clearing anything.
|
|
222
|
-
*/
|
|
223
|
-
export function setNetworkMode(mode) {
|
|
224
|
-
void remoteImagesDispatch()?.("setNetworkMode", {
|
|
225
|
-
mode
|
|
226
|
-
});
|
|
227
|
-
imagesState().globalModes.network = mode;
|
|
228
|
-
touchGlobal();
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
/** Chrome-style "disable images": render every image without a source. */
|
|
232
|
-
export function setBlankImages(enabled) {
|
|
233
|
-
void remoteImagesDispatch()?.("setBlankImages", {
|
|
234
|
-
enabled
|
|
235
|
-
});
|
|
236
|
-
imagesState().globalModes.blank = enabled;
|
|
237
|
-
touchGlobal();
|
|
238
|
-
}
|
|
239
|
-
export function getGlobalModes() {
|
|
240
|
-
return imagesState().globalModes;
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
// ---------------------------------------------------------------------------
|
|
244
|
-
// Application (called from the RN wrapper + expo render patch)
|
|
245
|
-
// ---------------------------------------------------------------------------
|
|
246
|
-
|
|
247
|
-
/** Inject a `cache` policy into an RN source (object or array shape). */
|
|
248
|
-
export function patchSourceCache(source, cache) {
|
|
249
|
-
if (Array.isArray(source)) {
|
|
250
|
-
return source.map(entry => entry && typeof entry === "object" && typeof entry.uri === "string" ? {
|
|
251
|
-
...entry,
|
|
252
|
-
cache
|
|
253
|
-
} : entry);
|
|
254
|
-
}
|
|
255
|
-
if (source && typeof source === "object" && typeof source.uri === "string") {
|
|
256
|
-
return {
|
|
257
|
-
...source,
|
|
258
|
-
cache
|
|
259
|
-
};
|
|
260
|
-
}
|
|
261
|
-
return source; // numeric assets etc. — nothing to patch
|
|
262
|
-
}
|
|
263
|
-
function resolveOverrideSource(source, lib) {
|
|
264
|
-
switch (source.type) {
|
|
265
|
-
case "url":
|
|
266
|
-
return {
|
|
267
|
-
uri: source.uri
|
|
268
|
-
};
|
|
269
|
-
case "error":
|
|
270
|
-
return {
|
|
271
|
-
uri: FORCED_ERROR_URI
|
|
272
|
-
};
|
|
273
|
-
case "hang":
|
|
274
|
-
return {
|
|
275
|
-
uri: HANG_URI
|
|
276
|
-
};
|
|
277
|
-
case "blank":
|
|
278
|
-
// Only reached for expo — RN blanking is style-based (see RN_BLANK_STYLE).
|
|
279
|
-
return null;
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
/**
|
|
284
|
-
* Merge global modes + per-record override + locate-flash into an
|
|
285
|
-
* already-copied props object. Mutates and returns `out`.
|
|
286
|
-
*/
|
|
287
|
-
export function applyOverrides(out, recordId, lib) {
|
|
288
|
-
const s = imagesState();
|
|
289
|
-
const modes = s.globalModes;
|
|
290
|
-
if (modes.network === "offline") {
|
|
291
|
-
// Hard offline: network sources fail outright (both libs); bundled/local
|
|
292
|
-
// assets keep loading like a real offline device.
|
|
293
|
-
const record = recordId != null ? getRecord(recordId) : undefined;
|
|
294
|
-
if (record?.sourceKind === "network") {
|
|
295
|
-
out.source = {
|
|
296
|
-
uri: OFFLINE_URI
|
|
297
|
-
};
|
|
298
|
-
}
|
|
299
|
-
} else if (modes.network === "cold") {
|
|
300
|
-
if (lib === "rn") {
|
|
301
|
-
out.source = patchSourceCache(out.source, "reload");
|
|
302
|
-
} else {
|
|
303
|
-
out.cachePolicy = "none";
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
if (modes.blank) {
|
|
307
|
-
if (lib === "expo") out.source = null;else out.style = [out.style, RN_BLANK_STYLE];
|
|
308
|
-
}
|
|
309
|
-
const override = getOverride(recordId);
|
|
310
|
-
if (override) {
|
|
311
|
-
if (override.source) {
|
|
312
|
-
if (override.source.type === "blank" && lib === "rn") {
|
|
313
|
-
out.style = [out.style, RN_BLANK_STYLE];
|
|
314
|
-
} else {
|
|
315
|
-
out.source = resolveOverrideSource(override.source, lib);
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
if (override.extraProps) Object.assign(out, override.extraProps);
|
|
319
|
-
if (override.style) out.style = [out.style, override.style];
|
|
320
|
-
}
|
|
321
|
-
if (recordId != null) {
|
|
322
|
-
const until = s.flashUntil.get(recordId);
|
|
323
|
-
if (until != null && until > Date.now()) {
|
|
324
|
-
out.style = [out.style, FLASH_STYLE];
|
|
325
|
-
}
|
|
326
|
-
}
|
|
327
|
-
return out;
|
|
328
|
-
}
|
|
329
|
-
//# sourceMappingURL=overrides.js.map
|
|
1
|
+
"use strict";import{remoteImagesDispatch as i}from"../remote";import{imagesState as o}from"../state";import{getRecord as y,getSnapshot as c,updateRecord as a}from"../store/imageEventStore";const x="file:///buoy-images-forced-error.png";const b="https://10.255.255.1/buoy-images-hang.png";const h="file:///buoy-images-offline-no-network.png";const k={borderWidth:3,borderColor:"#FF453A"};const u={opacity:0};function p(){for(const e of o().actionListeners)e()}function d(e){const n=o();if(e!=null){const t=n.expoInstances.get(e);t?.forceUpdate?.();return}for(const t of n.expoInstances.values()){t?.forceUpdate?.()}}export function touchRecord(e){const n=o();n.recordVersions.set(e,++n.versionCounter);p();d(e)}export function touchGlobal(){const e=o();e.globalVersion=++e.versionCounter;p();d()}export function getRenderVersion(e){const n=o();const t=e!=null?n.recordVersions.get(e)??0:0;return Math.max(t,n.globalVersion)}function O(e,n){return n.type==="url"?{id:e,kind:"url",uri:n.uri}:{id:e,kind:n.type}}function g(e,n){o().overrides.set(e,n);a(e,{overrideLabel:n.label});touchRecord(e)}export function setOverride(e,n){if(n.source){void i()?.("setOverride",O(e,n.source))}g(e,n)}function v(e){o().overrides.delete(e);a(e,{overrideLabel:void 0});touchRecord(e)}export function clearOverride(e){void i()?.("clearOverride",{id:e});v(e)}export function getOverride(e){if(e==null)return void 0;return o().overrides.get(e)}function R(e){return e.type==="hang"?"loading":e.type}export function setOverrideForAllMounted(e,n){void i()?.("massAction",{kind:R(e)});let t=0;for(const s of c()){if(!s.mounted)continue;g(s.id,{source:e,label:n});t++}return t}export function clearAllOverrides(){void i()?.("massAction",{kind:"restore"});const e=new Set(o().overrides.keys());for(const n of c()){if(n.overrideLabel!=null)e.add(n.id)}for(const n of e)v(n);return e.size}export function flashAllMounted(e=2500){void i()?.("massAction",{kind:"flash"});let n=0;for(const t of c()){if(!t.mounted)continue;m(t.id,e);n++}return n}function m(e,n=2500){const t=o();t.flashUntil.set(e,Date.now()+n);touchRecord(e);setTimeout(()=>{o().flashUntil.delete(e);touchRecord(e)},n+50)}export function flashRecord(e,n=2500){void i()?.("flash",{id:e});m(e,n)}export function setNetworkMode(e){void i()?.("setNetworkMode",{mode:e});o().globalModes.network=e;touchGlobal()}export function setBlankImages(e){void i()?.("setBlankImages",{enabled:e});o().globalModes.blank=e;touchGlobal()}export function getGlobalModes(){return o().globalModes}export function patchSourceCache(e,n){if(Array.isArray(e)){return e.map(t=>t&&typeof t==="object"&&typeof t.uri==="string"?{...t,cache:n}:t)}if(e&&typeof e==="object"&&typeof e.uri==="string"){return{...e,cache:n}}return e}function A(e,n){switch(e.type){case"url":return{uri:e.uri};case"error":return{uri:x};case"hang":return{uri:b};case"blank":return null}}export function applyOverrides(e,n,t){const s=o();const f=s.globalModes;if(f.network==="offline"){const l=n!=null?y(n):void 0;if(l?.sourceKind==="network"){e.source={uri:h}}}else if(f.network==="cold"){if(t==="rn"){e.source=patchSourceCache(e.source,"reload")}else{e.cachePolicy="none"}}if(f.blank){if(t==="expo")e.source=null;else e.style=[e.style,u]}const r=getOverride(n);if(r){if(r.source){if(r.source.type==="blank"&&t==="rn"){e.style=[e.style,u]}else{e.source=A(r.source,t)}}if(r.extraProps)Object.assign(e,r.extraProps);if(r.style)e.style=[e.style,r.style]}if(n!=null){const l=s.flashUntil.get(n);if(l!=null&&l>Date.now()){e.style=[e.style,k]}}return e}
|
|
@@ -1,50 +1 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Minimal RN <Image> decorator install, safe to run before React Native's
|
|
5
|
-
* InitializeCore.
|
|
6
|
-
*
|
|
7
|
-
* RN latches the decorator once, when `Image.ios.js` / `Image.android.js` is
|
|
8
|
-
* first evaluated. In development builds that happens during InitializeCore:
|
|
9
|
-
* AppRegistry pulls in the LogBox inspector, which imports Image. So even the
|
|
10
|
-
* first import of the app's entry file (`@buoy-gg/images/register`) is too
|
|
11
|
-
* late there, and the tool reports "missed the module-load race".
|
|
12
|
-
*
|
|
13
|
-
* `register` itself can't run before InitializeCore, because its imports touch
|
|
14
|
-
* globals InitializeCore installs (FormData, for one). This module imports only
|
|
15
|
-
* React, the ImageInjection hook and the globalThis state, and loads the
|
|
16
|
-
* capture hook lazily on the first render, by which time everything exists.
|
|
17
|
-
* Run it as a Metro pre-main module ahead of InitializeCore (see README).
|
|
18
|
-
*/
|
|
19
|
-
|
|
20
|
-
import * as React from "react";
|
|
21
|
-
import { imagesState } from "../state";
|
|
22
|
-
import { jsx as _jsx } from "react/jsx-runtime";
|
|
23
|
-
export function installRnImageDecoratorEarly() {
|
|
24
|
-
const s = imagesState();
|
|
25
|
-
if (s.decoratorRegistered) return;
|
|
26
|
-
try {
|
|
27
|
-
const injection = require("react-native/Libraries/Image/ImageInjection");
|
|
28
|
-
const setDecorator = injection.unstable_setImageComponentDecorator;
|
|
29
|
-
if (typeof setDecorator !== "function") return;
|
|
30
|
-
setDecorator(BaseImage => {
|
|
31
|
-
imagesState().decoratorInvoked = true;
|
|
32
|
-
return /*#__PURE__*/React.forwardRef(function BuoyCapturedImage(props, ref) {
|
|
33
|
-
// Same module every render, so the hook order stays stable.
|
|
34
|
-
const useImageCapture = require("./installRnImage").useImageCapture;
|
|
35
|
-
const {
|
|
36
|
-
props: instrumented,
|
|
37
|
-
reloadKey
|
|
38
|
-
} = useImageCapture(props);
|
|
39
|
-
return /*#__PURE__*/_jsx(BaseImage, {
|
|
40
|
-
...instrumented,
|
|
41
|
-
ref: ref
|
|
42
|
-
}, `buoy-r${reloadKey}`);
|
|
43
|
-
});
|
|
44
|
-
});
|
|
45
|
-
s.decoratorRegistered = true;
|
|
46
|
-
} catch {
|
|
47
|
-
// Internal RN path moved: the regular install reports the decorator inactive.
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
//# sourceMappingURL=rnDecoratorEarly.js.map
|
|
1
|
+
"use strict";import*as m from"react";import{imagesState as t}from"../state";import{jsx as f}from"react/jsx-runtime";export function installRnImageDecoratorEarly(){const e=t();if(e.decoratorRegistered)return;try{const o=require("react-native/Libraries/Image/ImageInjection");const r=o.unstable_setImageComponentDecorator;if(typeof r!=="function")return;r(a=>{t().decoratorInvoked=true;return m.forwardRef(function g(n,s){const i=require("./installRnImage").useImageCapture;const{props:c,reloadKey:u}=i(n);return f(a,{...c,ref:s},`buoy-r${u}`)})});e.decoratorRegistered=true}catch{}}
|
|
@@ -1,205 +1 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Savings prover — Squoosh-style, on device.
|
|
5
|
-
*
|
|
6
|
-
* Instead of estimating what an oversized source wastes, PROVE it: re-encode
|
|
7
|
-
* the image at the displayed size (× pixel ratio) as WebP via
|
|
8
|
-
* expo-image-manipulator (verified: chainable resize → saveAsync WebP on BOTH
|
|
9
|
-
* platforms, available in Expo Go), stat the output file, and report real
|
|
10
|
-
* bytes vs the original. The optimized file:// URI can then be A/B'd in place
|
|
11
|
-
* through the existing swap-url override.
|
|
12
|
-
*
|
|
13
|
-
* Original size is resolved in tiers: observed download bytes (onProgress
|
|
14
|
-
* total) → expo disk-cache file size → HEAD Content-Length.
|
|
15
|
-
*/
|
|
16
|
-
|
|
17
|
-
import { remoteImagesDispatch } from "../remote";
|
|
18
|
-
import { neededPixels } from "../store/imageEventStore";
|
|
19
|
-
function loadLegacyFs() {
|
|
20
|
-
try {
|
|
21
|
-
const mod = require("expo-file-system/legacy");
|
|
22
|
-
const fs = mod?.default || mod;
|
|
23
|
-
if (fs?.getInfoAsync) return fs;
|
|
24
|
-
} catch {
|
|
25
|
-
// try main import
|
|
26
|
-
}
|
|
27
|
-
try {
|
|
28
|
-
const mod = require("expo-file-system");
|
|
29
|
-
const fs = mod?.default || mod;
|
|
30
|
-
if (fs?.getInfoAsync) return fs;
|
|
31
|
-
} catch {
|
|
32
|
-
// not installed
|
|
33
|
-
}
|
|
34
|
-
return null;
|
|
35
|
-
}
|
|
36
|
-
async function fileSize(uri) {
|
|
37
|
-
const fs = loadLegacyFs();
|
|
38
|
-
if (!fs?.getInfoAsync) return null;
|
|
39
|
-
try {
|
|
40
|
-
const normalized = uri.startsWith("file://") ? uri : `file://${uri}`;
|
|
41
|
-
const info = await fs.getInfoAsync(normalized, {
|
|
42
|
-
size: true
|
|
43
|
-
});
|
|
44
|
-
return info.exists && typeof info.size === "number" ? info.size : null;
|
|
45
|
-
} catch {
|
|
46
|
-
return null;
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
async function resolveOriginalBytes(record) {
|
|
50
|
-
if (record.bytesTotal && record.bytesTotal > 0) {
|
|
51
|
-
return {
|
|
52
|
-
bytes: record.bytesTotal,
|
|
53
|
-
source: "download"
|
|
54
|
-
};
|
|
55
|
-
}
|
|
56
|
-
// expo disk cache file
|
|
57
|
-
if (record.lib === "expo") {
|
|
58
|
-
try {
|
|
59
|
-
const expo = require("expo-image");
|
|
60
|
-
const path = await expo.Image.getCachePathAsync(record.uri);
|
|
61
|
-
if (path) {
|
|
62
|
-
const size = await fileSize(path);
|
|
63
|
-
if (size != null) return {
|
|
64
|
-
bytes: size,
|
|
65
|
-
source: "cacheFile"
|
|
66
|
-
};
|
|
67
|
-
}
|
|
68
|
-
} catch {
|
|
69
|
-
// fall through
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
// HEAD request (network sources only)
|
|
73
|
-
if (record.sourceKind === "network") {
|
|
74
|
-
try {
|
|
75
|
-
const response = await fetch(record.uri, {
|
|
76
|
-
method: "HEAD"
|
|
77
|
-
});
|
|
78
|
-
const length = Number(response.headers.get("content-length"));
|
|
79
|
-
if (Number.isFinite(length) && length > 0) return {
|
|
80
|
-
bytes: length,
|
|
81
|
-
source: "head"
|
|
82
|
-
};
|
|
83
|
-
} catch {
|
|
84
|
-
// unknown original size is fine
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
return null;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
/**
|
|
91
|
-
* Re-encode `record`'s source at its displayed size as WebP and measure it.
|
|
92
|
-
* Requires expo-image-manipulator (graceful message when absent).
|
|
93
|
-
*/
|
|
94
|
-
export async function proveSavings(record) {
|
|
95
|
-
// Mirror mode: the re-encode must happen ON DEVICE (that's the whole point —
|
|
96
|
-
// real bytes from the real encoder); forward and relay the result.
|
|
97
|
-
const remote = remoteImagesDispatch();
|
|
98
|
-
if (remote) {
|
|
99
|
-
try {
|
|
100
|
-
return await remote("proveSavings", {
|
|
101
|
-
id: record.id
|
|
102
|
-
});
|
|
103
|
-
} catch (error) {
|
|
104
|
-
return {
|
|
105
|
-
ok: false,
|
|
106
|
-
message: String(error?.message ?? error)
|
|
107
|
-
};
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
if (!record.uri || record.sourceKind !== "network" && record.sourceKind !== "asset" && record.sourceKind !== "file") {
|
|
111
|
-
return {
|
|
112
|
-
ok: false,
|
|
113
|
-
message: "No loadable source URI on this record"
|
|
114
|
-
};
|
|
115
|
-
}
|
|
116
|
-
const needed = neededPixels(record);
|
|
117
|
-
if (!needed) {
|
|
118
|
-
return {
|
|
119
|
-
ok: false,
|
|
120
|
-
message: "No layout yet — the image must render once to know its displayed size"
|
|
121
|
-
};
|
|
122
|
-
}
|
|
123
|
-
let manipulator;
|
|
124
|
-
try {
|
|
125
|
-
manipulator = require("expo-image-manipulator");
|
|
126
|
-
} catch {
|
|
127
|
-
return {
|
|
128
|
-
ok: false,
|
|
129
|
-
message: "expo-image-manipulator not installed — add it to enable real savings measurement"
|
|
130
|
-
};
|
|
131
|
-
}
|
|
132
|
-
const webp = manipulator.SaveFormat?.WEBP ?? "webp";
|
|
133
|
-
const targetWidth = Math.min(needed.width, record.intrinsic?.width ?? needed.width);
|
|
134
|
-
let original = null;
|
|
135
|
-
let optimized = null;
|
|
136
|
-
try {
|
|
137
|
-
[original, optimized] = await Promise.all([resolveOriginalBytes(record), (async () => {
|
|
138
|
-
// Preferred: context API (current). Fallback: deprecated manipulateAsync.
|
|
139
|
-
if (manipulator.ImageManipulator?.manipulate) {
|
|
140
|
-
const context = manipulator.ImageManipulator.manipulate(record.uri);
|
|
141
|
-
try {
|
|
142
|
-
const ref = await context.resize({
|
|
143
|
-
width: targetWidth
|
|
144
|
-
}).renderAsync();
|
|
145
|
-
try {
|
|
146
|
-
return await ref.saveAsync({
|
|
147
|
-
format: webp,
|
|
148
|
-
compress: 0.8
|
|
149
|
-
});
|
|
150
|
-
} finally {
|
|
151
|
-
ref.release?.();
|
|
152
|
-
}
|
|
153
|
-
} finally {
|
|
154
|
-
context.release?.();
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
if (manipulator.manipulateAsync) {
|
|
158
|
-
return manipulator.manipulateAsync(record.uri, [{
|
|
159
|
-
resize: {
|
|
160
|
-
width: targetWidth
|
|
161
|
-
}
|
|
162
|
-
}], {
|
|
163
|
-
compress: 0.8,
|
|
164
|
-
format: webp
|
|
165
|
-
});
|
|
166
|
-
}
|
|
167
|
-
return null;
|
|
168
|
-
})()]);
|
|
169
|
-
} catch (error) {
|
|
170
|
-
return {
|
|
171
|
-
ok: false,
|
|
172
|
-
message: `Re-encode failed: ${String(error?.message ?? error)}`
|
|
173
|
-
};
|
|
174
|
-
}
|
|
175
|
-
if (!optimized?.uri) {
|
|
176
|
-
return {
|
|
177
|
-
ok: false,
|
|
178
|
-
message: "Re-encode produced no output (unsupported source?)"
|
|
179
|
-
};
|
|
180
|
-
}
|
|
181
|
-
const optimizedBytes = await fileSize(optimized.uri);
|
|
182
|
-
const result = {
|
|
183
|
-
ok: true,
|
|
184
|
-
message: "",
|
|
185
|
-
optimizedUri: optimized.uri,
|
|
186
|
-
optimizedBytes: optimizedBytes ?? undefined,
|
|
187
|
-
optimizedDims: {
|
|
188
|
-
width: optimized.width,
|
|
189
|
-
height: optimized.height
|
|
190
|
-
},
|
|
191
|
-
originalBytes: original?.bytes,
|
|
192
|
-
originalBytesSource: original?.source
|
|
193
|
-
};
|
|
194
|
-
if (original?.bytes != null && optimizedBytes != null) {
|
|
195
|
-
result.savedBytes = Math.max(0, original.bytes - optimizedBytes);
|
|
196
|
-
result.savedPct = original.bytes > 0 ? Math.round(result.savedBytes / original.bytes * 100) : 0;
|
|
197
|
-
result.message = `Right-sized WebP is ${result.savedPct}% smaller (real bytes, encoded on device)`;
|
|
198
|
-
} else if (optimizedBytes != null) {
|
|
199
|
-
result.message = "Optimized variant encoded — original size unknown (no download bytes, cache entry, or Content-Length)";
|
|
200
|
-
} else {
|
|
201
|
-
result.message = "Optimized variant encoded — could not stat the output file";
|
|
202
|
-
}
|
|
203
|
-
return result;
|
|
204
|
-
}
|
|
205
|
-
//# sourceMappingURL=savings.js.map
|
|
1
|
+
"use strict";import{remoteImagesDispatch as d}from"../remote";import{neededPixels as y}from"../store/imageEventStore";function g(){try{const e=require("expo-file-system/legacy");const t=e?.default||e;if(t?.getInfoAsync)return t}catch{}try{const e=require("expo-file-system");const t=e?.default||e;if(t?.getInfoAsync)return t}catch{}return null}async function f(e){const t=g();if(!t?.getInfoAsync)return null;try{const n=e.startsWith("file://")?e:`file://${e}`;const s=await t.getInfoAsync(n,{size:true});return s.exists&&typeof s.size==="number"?s.size:null}catch{return null}}async function p(e){if(e.bytesTotal&&e.bytesTotal>0){return{bytes:e.bytesTotal,source:"download"}}if(e.lib==="expo"){try{const t=require("expo-image");const n=await t.Image.getCachePathAsync(e.uri);if(n){const s=await f(n);if(s!=null)return{bytes:s,source:"cacheFile"}}}catch{}}if(e.sourceKind==="network"){try{const t=await fetch(e.uri,{method:"HEAD"});const n=Number(t.headers.get("content-length"));if(Number.isFinite(n)&&n>0)return{bytes:n,source:"head"}}catch{}}return null}export async function proveSavings(e){const t=d();if(t){try{return await t("proveSavings",{id:e.id})}catch(a){return{ok:false,message:String(a?.message??a)}}}if(!e.uri||e.sourceKind!=="network"&&e.sourceKind!=="asset"&&e.sourceKind!=="file"){return{ok:false,message:"No loadable source URI on this record"}}const n=y(e);if(!n){return{ok:false,message:"No layout yet \u2014 the image must render once to know its displayed size"}}let s;try{s=require("expo-image-manipulator")}catch{return{ok:false,message:"expo-image-manipulator not installed \u2014 add it to enable real savings measurement"}}const u=s.SaveFormat?.WEBP??"webp";const c=Math.min(n.width,e.intrinsic?.width??n.width);let r=null;let o=null;try{[r,o]=await Promise.all([p(e),(async()=>{if(s.ImageManipulator?.manipulate){const a=s.ImageManipulator.manipulate(e.uri);try{const m=await a.resize({width:c}).renderAsync();try{return await m.saveAsync({format:u,compress:.8})}finally{m.release?.()}}finally{a.release?.()}}if(s.manipulateAsync){return s.manipulateAsync(e.uri,[{resize:{width:c}}],{compress:.8,format:u})}return null})()])}catch(a){return{ok:false,message:`Re-encode failed: ${String(a?.message??a)}`}}if(!o?.uri){return{ok:false,message:"Re-encode produced no output (unsupported source?)"}}const l=await f(o.uri);const i={ok:true,message:"",optimizedUri:o.uri,optimizedBytes:l??void 0,optimizedDims:{width:o.width,height:o.height},originalBytes:r?.bytes,originalBytesSource:r?.source};if(r?.bytes!=null&&l!=null){i.savedBytes=Math.max(0,r.bytes-l);i.savedPct=r.bytes>0?Math.round(i.savedBytes/r.bytes*100):0;i.message=`Right-sized WebP is ${i.savedPct}% smaller (real bytes, encoded on device)`}else if(l!=null){i.message="Optimized variant encoded \u2014 original size unknown (no download bytes, cache entry, or Content-Length)"}else{i.message="Optimized variant encoded \u2014 could not stat the output file"}return i}
|
|
@@ -1,67 +1 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
|
|
3
|
-
import { remoteImagesDispatch } from '../remote';
|
|
4
|
-
import { neededPixels } from '../store/imageEventStore';
|
|
5
|
-
const urls = new Map();
|
|
6
|
-
export async function proveSavings(record) {
|
|
7
|
-
const remote = remoteImagesDispatch();
|
|
8
|
-
if (remote) return await remote('proveSavings', {
|
|
9
|
-
id: record.id
|
|
10
|
-
});
|
|
11
|
-
const dimensions = neededPixels(record);
|
|
12
|
-
if (!dimensions) return {
|
|
13
|
-
ok: false,
|
|
14
|
-
message: 'Measure the displayed image before optimizing it.'
|
|
15
|
-
};
|
|
16
|
-
try {
|
|
17
|
-
const response = await fetch(record.uri);
|
|
18
|
-
if (!response.ok) throw new Error(`Image download returned HTTP ${response.status}.`);
|
|
19
|
-
const original = await response.blob();
|
|
20
|
-
const bitmap = await createImageBitmap(original);
|
|
21
|
-
const canvas = document.createElement('canvas');
|
|
22
|
-
canvas.width = Math.max(1, Math.min(bitmap.width, Math.round(dimensions.width)));
|
|
23
|
-
canvas.height = Math.max(1, Math.min(bitmap.height, Math.round(dimensions.height)));
|
|
24
|
-
const context = canvas.getContext('2d');
|
|
25
|
-
if (!context) {
|
|
26
|
-
bitmap.close();
|
|
27
|
-
throw new Error('Canvas is unavailable.');
|
|
28
|
-
}
|
|
29
|
-
context.drawImage(bitmap, 0, 0, canvas.width, canvas.height);
|
|
30
|
-
bitmap.close();
|
|
31
|
-
const optimized = await new Promise((resolve, reject) => canvas.toBlob(blob => blob ? resolve(blob) : reject(new Error('Image encoding failed.')), 'image/webp', 0.8));
|
|
32
|
-
if (optimized.type !== 'image/webp') return {
|
|
33
|
-
ok: false,
|
|
34
|
-
message: 'This browser cannot encode WebP images.'
|
|
35
|
-
};
|
|
36
|
-
const previous = urls.get(record.id);
|
|
37
|
-
if (previous) URL.revokeObjectURL(previous);
|
|
38
|
-
const uri = URL.createObjectURL(optimized);
|
|
39
|
-
urls.set(record.id, uri);
|
|
40
|
-
while (urls.size > 30) {
|
|
41
|
-
const [id, value] = urls.entries().next().value;
|
|
42
|
-
URL.revokeObjectURL(value);
|
|
43
|
-
urls.delete(id);
|
|
44
|
-
}
|
|
45
|
-
const savedBytes = original.size - optimized.size;
|
|
46
|
-
return {
|
|
47
|
-
ok: true,
|
|
48
|
-
message: 'WebP preview created.',
|
|
49
|
-
originalBytes: original.size,
|
|
50
|
-
originalBytesSource: 'download',
|
|
51
|
-
optimizedBytes: optimized.size,
|
|
52
|
-
optimizedUri: uri,
|
|
53
|
-
optimizedDims: {
|
|
54
|
-
width: canvas.width,
|
|
55
|
-
height: canvas.height
|
|
56
|
-
},
|
|
57
|
-
savedBytes,
|
|
58
|
-
savedPct: original.size ? savedBytes / original.size * 100 : 0
|
|
59
|
-
};
|
|
60
|
-
} catch (error) {
|
|
61
|
-
return {
|
|
62
|
-
ok: false,
|
|
63
|
-
message: `Image optimization failed: ${error instanceof Error ? error.message : String(error)}`
|
|
64
|
-
};
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
//# sourceMappingURL=savings.web.js.map
|
|
1
|
+
"use strict";import{remoteImagesDispatch as u}from"../remote";import{neededPixels as f}from"../store/imageEventStore";const o=new Map;export async function proveSavings(i){const d=u();if(d)return await d("proveSavings",{id:i.id});const r=f(i);if(!r)return{ok:false,message:"Measure the displayed image before optimizing it."};try{const t=await fetch(i.uri);if(!t.ok)throw new Error(`Image download returned HTTP ${t.status}.`);const a=await t.blob();const s=await createImageBitmap(a);const e=document.createElement("canvas");e.width=Math.max(1,Math.min(s.width,Math.round(r.width)));e.height=Math.max(1,Math.min(s.height,Math.round(r.height)));const h=e.getContext("2d");if(!h){s.close();throw new Error("Canvas is unavailable.")}h.drawImage(s,0,0,e.width,e.height);s.close();const n=await new Promise((m,c)=>e.toBlob(p=>p?m(p):c(new Error("Image encoding failed.")),"image/webp",.8));if(n.type!=="image/webp")return{ok:false,message:"This browser cannot encode WebP images."};const g=o.get(i.id);if(g)URL.revokeObjectURL(g);const w=URL.createObjectURL(n);o.set(i.id,w);while(o.size>30){const[m,c]=o.entries().next().value;URL.revokeObjectURL(c);o.delete(m)}const l=a.size-n.size;return{ok:true,message:"WebP preview created.",originalBytes:a.size,originalBytesSource:"download",optimizedBytes:n.size,optimizedUri:w,optimizedDims:{width:e.width,height:e.height},savedBytes:l,savedPct:a.size?l/a.size*100:0}}catch(t){return{ok:false,message:`Image optimization failed: ${t instanceof Error?t.message:String(t)}`}}}
|