@buoy-gg/images 7.0.13 → 7.0.15
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/capture/actions.js +15 -0
- package/lib/commonjs/capture/actions.js.map +1 -1
- package/lib/commonjs/capture/install.js +5 -1
- package/lib/commonjs/capture/install.js.map +1 -1
- package/lib/commonjs/capture/overrides.js +70 -10
- package/lib/commonjs/capture/overrides.js.map +1 -1
- package/lib/commonjs/capture/savings.js +16 -0
- package/lib/commonjs/capture/savings.js.map +1 -1
- package/lib/commonjs/components/ImageDetail.js +11 -1
- package/lib/commonjs/components/ImageDetail.js.map +1 -1
- package/lib/commonjs/components/ImagesModal.js +103 -38
- package/lib/commonjs/components/ImagesModal.js.map +1 -1
- package/lib/commonjs/index.js +26 -1
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/remote.js +42 -0
- package/lib/commonjs/remote.js.map +1 -0
- package/lib/commonjs/state.js +5 -1
- package/lib/commonjs/state.js.map +1 -1
- package/lib/commonjs/store/imageEventStore.js +79 -1
- package/lib/commonjs/store/imageEventStore.js.map +1 -1
- package/lib/commonjs/sync/imagesSyncAdapter.js.map +1 -1
- package/lib/module/capture/actions.js +15 -0
- package/lib/module/capture/actions.js.map +1 -1
- package/lib/module/capture/install.js +5 -1
- package/lib/module/capture/install.js.map +1 -1
- package/lib/module/capture/overrides.js +70 -10
- package/lib/module/capture/overrides.js.map +1 -1
- package/lib/module/capture/savings.js +16 -0
- package/lib/module/capture/savings.js.map +1 -1
- package/lib/module/components/ImageDetail.js +13 -3
- package/lib/module/components/ImageDetail.js.map +1 -1
- package/lib/module/components/ImagesModal.js +104 -40
- package/lib/module/components/ImagesModal.js.map +1 -1
- package/lib/module/index.js +6 -0
- package/lib/module/index.js.map +1 -1
- package/lib/module/remote.js +36 -0
- package/lib/module/remote.js.map +1 -0
- package/lib/module/state.js +5 -1
- package/lib/module/state.js.map +1 -1
- package/lib/module/store/imageEventStore.js +76 -1
- package/lib/module/store/imageEventStore.js.map +1 -1
- package/lib/module/sync/imagesSyncAdapter.js.map +1 -1
- package/lib/typescript/capture/actions.d.ts.map +1 -1
- package/lib/typescript/capture/install.d.ts.map +1 -1
- package/lib/typescript/capture/overrides.d.ts.map +1 -1
- package/lib/typescript/capture/savings.d.ts.map +1 -1
- package/lib/typescript/components/ImageDetail.d.ts.map +1 -1
- package/lib/typescript/components/ImagesModal.d.ts +9 -1
- package/lib/typescript/components/ImagesModal.d.ts.map +1 -1
- package/lib/typescript/index.d.ts +5 -0
- package/lib/typescript/index.d.ts.map +1 -1
- package/lib/typescript/remote.d.ts +16 -0
- package/lib/typescript/remote.d.ts.map +1 -0
- package/lib/typescript/state.d.ts +9 -1
- package/lib/typescript/state.d.ts.map +1 -1
- package/lib/typescript/store/imageEventStore.d.ts +15 -1
- package/lib/typescript/store/imageEventStore.d.ts.map +1 -1
- package/lib/typescript/sync/imagesSyncAdapter.d.ts +3 -70
- package/lib/typescript/sync/imagesSyncAdapter.d.ts.map +1 -1
- package/lib/typescript/types.d.ts +53 -0
- package/lib/typescript/types.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/capture/actions.ts +9 -0
- package/src/capture/install.ts +5 -1
- package/src/capture/overrides.ts +59 -9
- package/src/capture/savings.ts +11 -0
- package/src/components/ImageDetail.tsx +12 -2
- package/src/components/ImagesModal.tsx +107 -39
- package/src/index.tsx +12 -0
- package/src/remote.ts +42 -0
- package/src/state.ts +16 -1
- package/src/store/imageEventStore.ts +76 -2
- package/src/sync/imagesSyncAdapter.ts +2 -2
- package/src/types.ts +43 -0
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* instead of silently empty.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import { useState } from "react";
|
|
8
|
+
import { useEffect, useState } from "react";
|
|
9
9
|
import { FlatList, StyleSheet, Text, View } from "react-native";
|
|
10
10
|
import { JsModal, macOSColors } from "@buoy-gg/shared-ui";
|
|
11
11
|
import type { ModalState } from "@buoy-gg/shared-ui";
|
|
@@ -13,7 +13,8 @@ import { getCaptureStatus, installImageCapture } from "../capture/install";
|
|
|
13
13
|
import { getGlobalModes } from "../capture/overrides";
|
|
14
14
|
import { formatRecordMarkdown, formatRegistryMarkdown } from "../export";
|
|
15
15
|
import { useImageRecords } from "../hooks/useImageRecords";
|
|
16
|
-
import {
|
|
16
|
+
import { remoteImagesDispatch } from "../remote";
|
|
17
|
+
import { clearRecords, getRecord, updateRecord } from "../store/imageEventStore";
|
|
17
18
|
import { ImageDetail } from "./ImageDetail";
|
|
18
19
|
import { ImageDetailFooter } from "./ImageDetailFooter";
|
|
19
20
|
import { ImageRow } from "./ImageRow";
|
|
@@ -29,6 +30,8 @@ interface ImagesModalProps {
|
|
|
29
30
|
onBack?: () => void;
|
|
30
31
|
onMinimize?: (modalState: ModalState) => void;
|
|
31
32
|
persistenceKey?: string;
|
|
33
|
+
/** Render as a plain header-bar + body stack instead of a JsModal (desktop). */
|
|
34
|
+
inline?: boolean;
|
|
32
35
|
}
|
|
33
36
|
|
|
34
37
|
function CaptureNotice() {
|
|
@@ -72,6 +75,7 @@ export function ImagesModal({
|
|
|
72
75
|
onBack,
|
|
73
76
|
onMinimize,
|
|
74
77
|
persistenceKey,
|
|
78
|
+
inline = false,
|
|
75
79
|
}: ImagesModalProps) {
|
|
76
80
|
// Safety net: ensures capture is live even without the early register
|
|
77
81
|
// import (expo-image capture is timing-immune, so this still gets most data).
|
|
@@ -81,6 +85,30 @@ export function ImagesModal({
|
|
|
81
85
|
const [selectedId, setSelectedId] = useState<number | null>(null);
|
|
82
86
|
const selected = selectedId != null ? getRecord(selectedId) : undefined;
|
|
83
87
|
|
|
88
|
+
// Mirror mode: the synced wire records omit errorHeaders (detail-only data)
|
|
89
|
+
// — pull them from the device the first time a failed record is opened.
|
|
90
|
+
const wantHeadersFor =
|
|
91
|
+
selected?.status === "error" && selected.errorHeaders == null
|
|
92
|
+
? selected.id
|
|
93
|
+
: null;
|
|
94
|
+
useEffect(() => {
|
|
95
|
+
const remote = remoteImagesDispatch();
|
|
96
|
+
if (wantHeadersFor == null || !remote) return;
|
|
97
|
+
let cancelled = false;
|
|
98
|
+
remote("getDetail", { id: wantHeadersFor })
|
|
99
|
+
.then((detail) => {
|
|
100
|
+
if (cancelled) return;
|
|
101
|
+
const headers = (detail as { errorHeaders?: Record<string, string> | null })
|
|
102
|
+
?.errorHeaders;
|
|
103
|
+
// `{}` (not undefined) so a headerless error doesn't refetch forever.
|
|
104
|
+
updateRecord(wantHeadersFor, { errorHeaders: headers ?? {} });
|
|
105
|
+
})
|
|
106
|
+
.catch(() => {});
|
|
107
|
+
return () => {
|
|
108
|
+
cancelled = true;
|
|
109
|
+
};
|
|
110
|
+
}, [wantHeadersFor]);
|
|
111
|
+
|
|
84
112
|
// Stepping is relative to the live list (newest first): previous = newer.
|
|
85
113
|
const selectedIndex = selected
|
|
86
114
|
? records.findIndex((r) => r.id === selected.id)
|
|
@@ -104,6 +132,58 @@ export function ImagesModal({
|
|
|
104
132
|
getGlobalModes()
|
|
105
133
|
);
|
|
106
134
|
|
|
135
|
+
const headerContent = (
|
|
136
|
+
<ImagesHeader
|
|
137
|
+
stats={stats}
|
|
138
|
+
detailOpen={selected != null}
|
|
139
|
+
detailIndex={selectedIndex}
|
|
140
|
+
detailTotal={records.length}
|
|
141
|
+
onCloseDetail={() => setSelectedId(null)}
|
|
142
|
+
onClear={clearRecords}
|
|
143
|
+
getReport={getReport}
|
|
144
|
+
onBack={onBack}
|
|
145
|
+
/>
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
const body = selected ? (
|
|
149
|
+
<View style={styles.detailContainer}>
|
|
150
|
+
<ImageDetail record={selected} />
|
|
151
|
+
<ImageDetailFooter
|
|
152
|
+
key={selected.id}
|
|
153
|
+
record={selected}
|
|
154
|
+
hasPrevious={selectedIndex > 0}
|
|
155
|
+
hasNext={selectedIndex >= 0 && selectedIndex < records.length - 1}
|
|
156
|
+
onPrevious={() => stepTo(selectedIndex - 1)}
|
|
157
|
+
onNext={() => stepTo(selectedIndex + 1)}
|
|
158
|
+
/>
|
|
159
|
+
</View>
|
|
160
|
+
) : (
|
|
161
|
+
<View style={styles.listContainer}>
|
|
162
|
+
<InsightsBar insights={computeInsights(records)} />
|
|
163
|
+
<CaptureNotice />
|
|
164
|
+
<FlatList
|
|
165
|
+
data={records}
|
|
166
|
+
keyExtractor={(item: ImageRecord) => String(item.id)}
|
|
167
|
+
renderItem={({ item }) => <ImageRow record={item} onPress={setSelectedId} />}
|
|
168
|
+
ListEmptyComponent={<EmptyState />}
|
|
169
|
+
/>
|
|
170
|
+
<ImagesListFooter />
|
|
171
|
+
</View>
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
// The ONLY difference between the phone and the desktop dashboard is which
|
|
175
|
+
// shell the header and body go in. Everything above this is shared.
|
|
176
|
+
if (inline) {
|
|
177
|
+
return (
|
|
178
|
+
<View style={styles.inlineRoot}>
|
|
179
|
+
{/* Flex-less wrapper — `ModalHeader` is `flex: 1` for JsModal's
|
|
180
|
+
ROW-direction header slot and would grow vertically here. */}
|
|
181
|
+
<View style={styles.inlineHeader}>{headerContent}</View>
|
|
182
|
+
{body}
|
|
183
|
+
</View>
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
|
|
107
187
|
return (
|
|
108
188
|
<JsModal
|
|
109
189
|
visible={visible}
|
|
@@ -112,53 +192,41 @@ export function ImagesModal({
|
|
|
112
192
|
persistenceKey={persistenceKey}
|
|
113
193
|
header={{
|
|
114
194
|
showToggleButton: true,
|
|
115
|
-
customContent:
|
|
116
|
-
<ImagesHeader
|
|
117
|
-
stats={stats}
|
|
118
|
-
detailOpen={selected != null}
|
|
119
|
-
detailIndex={selectedIndex}
|
|
120
|
-
detailTotal={records.length}
|
|
121
|
-
onCloseDetail={() => setSelectedId(null)}
|
|
122
|
-
onClear={clearRecords}
|
|
123
|
-
getReport={getReport}
|
|
124
|
-
onBack={onBack}
|
|
125
|
-
/>
|
|
126
|
-
),
|
|
195
|
+
customContent: headerContent,
|
|
127
196
|
}}
|
|
128
197
|
enablePersistence
|
|
129
198
|
initialMode="bottomSheet"
|
|
130
199
|
disableScrollWrapper
|
|
131
200
|
>
|
|
132
|
-
{
|
|
133
|
-
<View style={styles.detailContainer}>
|
|
134
|
-
<ImageDetail record={selected} />
|
|
135
|
-
<ImageDetailFooter
|
|
136
|
-
key={selected.id}
|
|
137
|
-
record={selected}
|
|
138
|
-
hasPrevious={selectedIndex > 0}
|
|
139
|
-
hasNext={selectedIndex >= 0 && selectedIndex < records.length - 1}
|
|
140
|
-
onPrevious={() => stepTo(selectedIndex - 1)}
|
|
141
|
-
onNext={() => stepTo(selectedIndex + 1)}
|
|
142
|
-
/>
|
|
143
|
-
</View>
|
|
144
|
-
) : (
|
|
145
|
-
<View style={styles.listContainer}>
|
|
146
|
-
<InsightsBar insights={computeInsights(records)} />
|
|
147
|
-
<CaptureNotice />
|
|
148
|
-
<FlatList
|
|
149
|
-
data={records}
|
|
150
|
-
keyExtractor={(item: ImageRecord) => String(item.id)}
|
|
151
|
-
renderItem={({ item }) => <ImageRow record={item} onPress={setSelectedId} />}
|
|
152
|
-
ListEmptyComponent={<EmptyState />}
|
|
153
|
-
/>
|
|
154
|
-
<ImagesListFooter />
|
|
155
|
-
</View>
|
|
156
|
-
)}
|
|
201
|
+
{body}
|
|
157
202
|
</JsModal>
|
|
158
203
|
);
|
|
159
204
|
}
|
|
160
205
|
|
|
206
|
+
/**
|
|
207
|
+
* The same Images tool as `ImagesModal`, rendered INLINE for hosts that place
|
|
208
|
+
* tools in a panel rather than a modal (the desktop dashboard). Exists so
|
|
209
|
+
* there is exactly one implementation of the tool's header, list and detail.
|
|
210
|
+
*/
|
|
211
|
+
export function ImagesToolView(
|
|
212
|
+
props: Omit<ImagesModalProps, "visible" | "onClose" | "inline"> = {}
|
|
213
|
+
) {
|
|
214
|
+
return <ImagesModal {...props} inline visible onClose={() => {}} />;
|
|
215
|
+
}
|
|
216
|
+
|
|
161
217
|
const styles = StyleSheet.create({
|
|
218
|
+
inlineRoot: {
|
|
219
|
+
flex: 1,
|
|
220
|
+
minHeight: 0,
|
|
221
|
+
backgroundColor: macOSColors.background.base,
|
|
222
|
+
},
|
|
223
|
+
inlineHeader: {
|
|
224
|
+
paddingHorizontal: 12,
|
|
225
|
+
paddingVertical: 8,
|
|
226
|
+
borderBottomWidth: 1,
|
|
227
|
+
borderBottomColor: macOSColors.border.default,
|
|
228
|
+
backgroundColor: macOSColors.background.card,
|
|
229
|
+
},
|
|
162
230
|
listContainer: {
|
|
163
231
|
flex: 1,
|
|
164
232
|
},
|
package/src/index.tsx
CHANGED
|
@@ -79,6 +79,18 @@ export { formatRecordMarkdown, formatRegistryMarkdown } from "./export";
|
|
|
79
79
|
// =============================================================================
|
|
80
80
|
export { imagesSyncAdapter } from "./sync/imagesSyncAdapter";
|
|
81
81
|
|
|
82
|
+
// =============================================================================
|
|
83
|
+
// DASHBOARD MIRROR (Buoy Desktop renders ImagesToolView fed by these)
|
|
84
|
+
// =============================================================================
|
|
85
|
+
export { ImagesToolView } from "./components/ImagesModal";
|
|
86
|
+
export {
|
|
87
|
+
disableImagesCapture,
|
|
88
|
+
replaceImagesFromSnapshot,
|
|
89
|
+
} from "./store/imageEventStore";
|
|
90
|
+
export { setRemoteImagesHandler } from "./remote";
|
|
91
|
+
export type { ImagesRemoteDispatch } from "./remote";
|
|
92
|
+
export type { ImageRecordWire, ImagesSnapshotPayload } from "./types";
|
|
93
|
+
|
|
82
94
|
// =============================================================================
|
|
83
95
|
// STORE + HOOKS
|
|
84
96
|
// =============================================================================
|
package/src/remote.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Remote-dispatch seam for dashboard mirrors (Buoy Desktop).
|
|
3
|
+
*
|
|
4
|
+
* On device this stays null and every action in capture/ runs locally. A
|
|
5
|
+
* dashboard sets a handler that forwards the action name + params over the
|
|
6
|
+
* external-sync protocol to the SAME adapter actions the MCP uses
|
|
7
|
+
* (sync/imagesSyncAdapter.ts), so one action vocabulary serves phone UI,
|
|
8
|
+
* desktop UI, and LLM automation.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { imagesState } from "./state";
|
|
12
|
+
import type { ActionResult } from "./capture/actions";
|
|
13
|
+
|
|
14
|
+
export type ImagesRemoteDispatch = (
|
|
15
|
+
action: string,
|
|
16
|
+
params?: Record<string, unknown>
|
|
17
|
+
) => Promise<unknown>;
|
|
18
|
+
|
|
19
|
+
export function setRemoteImagesHandler(
|
|
20
|
+
handler: ImagesRemoteDispatch | null
|
|
21
|
+
): void {
|
|
22
|
+
imagesState().remoteDispatch = handler;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function remoteImagesDispatch(): ImagesRemoteDispatch | null {
|
|
26
|
+
return imagesState().remoteDispatch;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Coerce an adapter action's `{ ok, message }` reply into an ActionResult. */
|
|
30
|
+
export async function remoteActionResult(
|
|
31
|
+
call: Promise<unknown>
|
|
32
|
+
): Promise<ActionResult> {
|
|
33
|
+
try {
|
|
34
|
+
const result = (await call) as { ok?: unknown; message?: unknown } | null;
|
|
35
|
+
return {
|
|
36
|
+
ok: !!result?.ok,
|
|
37
|
+
message: String(result?.message ?? ""),
|
|
38
|
+
};
|
|
39
|
+
} catch (error) {
|
|
40
|
+
return { ok: false, message: String((error as Error)?.message ?? error) };
|
|
41
|
+
}
|
|
42
|
+
}
|
package/src/state.ts
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* every copy operate on the same data.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
import type { ImageRecord } from "./types";
|
|
13
|
+
import type { CaptureStatus, ImageRecord } from "./types";
|
|
14
14
|
|
|
15
15
|
export interface ImagesGlobalState {
|
|
16
16
|
// store
|
|
@@ -51,6 +51,17 @@ export interface ImagesGlobalState {
|
|
|
51
51
|
versionCounter: number;
|
|
52
52
|
/** App-wide simulation modes. */
|
|
53
53
|
globalModes: { network: "normal" | "offline" | "cold"; blank: boolean };
|
|
54
|
+
// mirror mode (desktop dashboard)
|
|
55
|
+
/** True on dashboards: local capture must never install (mirror only). */
|
|
56
|
+
captureDisabled: boolean;
|
|
57
|
+
/** Forwards actions to the synced device instead of running them locally. */
|
|
58
|
+
remoteDispatch:
|
|
59
|
+
| ((action: string, params?: Record<string, unknown>) => Promise<unknown>)
|
|
60
|
+
| null;
|
|
61
|
+
/** Device PixelRatio derived from the synced snapshot (layout → neededPx). */
|
|
62
|
+
mirrorPixelRatio: number | null;
|
|
63
|
+
/** Device-reported capture status (overrides the local probe on mirrors). */
|
|
64
|
+
mirrorCaptureStatus: CaptureStatus | null;
|
|
54
65
|
}
|
|
55
66
|
|
|
56
67
|
const KEY = Symbol.for("buoy.images.globalState.v1");
|
|
@@ -82,6 +93,10 @@ export function imagesState(): ImagesGlobalState {
|
|
|
82
93
|
globalVersion: 0,
|
|
83
94
|
versionCounter: 0,
|
|
84
95
|
globalModes: { network: "normal", blank: false },
|
|
96
|
+
captureDisabled: false,
|
|
97
|
+
remoteDispatch: null,
|
|
98
|
+
mirrorPixelRatio: null,
|
|
99
|
+
mirrorCaptureStatus: null,
|
|
85
100
|
};
|
|
86
101
|
}
|
|
87
102
|
return g[KEY];
|
|
@@ -10,8 +10,9 @@
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import { PixelRatio } from "react-native";
|
|
13
|
+
import { remoteImagesDispatch } from "../remote";
|
|
13
14
|
import { imagesState } from "../state";
|
|
14
|
-
import type { ImageRecord, ImageStats } from "../types";
|
|
15
|
+
import type { ImageRecord, ImageStats, ImagesSnapshotPayload } from "../types";
|
|
15
16
|
|
|
16
17
|
/** Max records retained before the oldest are dropped. */
|
|
17
18
|
const MAX_RECORDS = 500;
|
|
@@ -83,6 +84,9 @@ export function subscribe(listener: () => void): () => void {
|
|
|
83
84
|
}
|
|
84
85
|
|
|
85
86
|
export function clearRecords(): void {
|
|
87
|
+
// Mirror mode: clear the device's registry too, or the next snapshot would
|
|
88
|
+
// refill this store straight away.
|
|
89
|
+
void remoteImagesDispatch()?.("clearRecords");
|
|
86
90
|
const s = imagesState();
|
|
87
91
|
// Keep records still actively loading so in-flight events don't orphan.
|
|
88
92
|
s.records = s.records.filter(
|
|
@@ -92,14 +96,84 @@ export function clearRecords(): void {
|
|
|
92
96
|
invalidate();
|
|
93
97
|
}
|
|
94
98
|
|
|
99
|
+
// ---------------------------------------------------------------------------
|
|
100
|
+
// Mirror mode (desktop dashboard)
|
|
101
|
+
// ---------------------------------------------------------------------------
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Dashboards never instrument their own images — the store is purely a mirror
|
|
105
|
+
* of the synced device. Makes installImageCapture() a permanent no-op.
|
|
106
|
+
*/
|
|
107
|
+
export function disableImagesCapture(): void {
|
|
108
|
+
imagesState().captureDisabled = true;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Replace the whole store from a synced adapter snapshot (wire records are
|
|
113
|
+
* newest-first; the store keeps oldest-first and useImageRecords reverses).
|
|
114
|
+
* Also adopts the device's global modes / capture status and derives the
|
|
115
|
+
* DEVICE's PixelRatio so oversize math doesn't use the dashboard display's.
|
|
116
|
+
*/
|
|
117
|
+
export function replaceImagesFromSnapshot(payload: ImagesSnapshotPayload): void {
|
|
118
|
+
const s = imagesState();
|
|
119
|
+
const wire = payload.records ?? [];
|
|
120
|
+
|
|
121
|
+
// neededPx was computed on device as round(layout * PixelRatio.get()), so
|
|
122
|
+
// the widest record recovers the device scale with the least rounding error.
|
|
123
|
+
let bestWidth = 0;
|
|
124
|
+
for (const w of wire) {
|
|
125
|
+
if (w.layout && w.neededPx && w.layout.width > bestWidth) {
|
|
126
|
+
bestWidth = w.layout.width;
|
|
127
|
+
s.mirrorPixelRatio = w.neededPx.width / w.layout.width;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const now = Date.now();
|
|
132
|
+
s.records = [...wire].reverse().map((w) => ({
|
|
133
|
+
id: w.id,
|
|
134
|
+
lib: w.lib,
|
|
135
|
+
uri: w.uri,
|
|
136
|
+
sourceKind: w.kind,
|
|
137
|
+
status: w.status,
|
|
138
|
+
mounted: w.mounted,
|
|
139
|
+
createdAt: now - w.ageMs,
|
|
140
|
+
durationMs: w.ms ?? undefined,
|
|
141
|
+
intrinsic: w.intrinsic ?? undefined,
|
|
142
|
+
layout: w.layout ?? undefined,
|
|
143
|
+
cacheVerdict: w.cache ?? undefined,
|
|
144
|
+
cacheVerdictSource: w.cacheSource ?? undefined,
|
|
145
|
+
progressSeen: w.progressSeen,
|
|
146
|
+
// The wire carries only the total; a settled download loaded all of it.
|
|
147
|
+
bytesLoaded: w.status === "loaded" ? (w.bytesTotal ?? undefined) : undefined,
|
|
148
|
+
bytesTotal: w.bytesTotal ?? undefined,
|
|
149
|
+
error: w.error ?? undefined,
|
|
150
|
+
errorCode: w.errorCode ?? undefined,
|
|
151
|
+
loadCount: w.loadCount,
|
|
152
|
+
overrideLabel: w.overrideLabel ?? undefined,
|
|
153
|
+
hasAltText: w.hasAltText ?? undefined,
|
|
154
|
+
layoutShifts: w.layoutShifts,
|
|
155
|
+
}));
|
|
156
|
+
s.byId = new Map(s.records.map((r) => [r.id, r]));
|
|
157
|
+
if (payload.globalModes) s.globalModes = { ...payload.globalModes };
|
|
158
|
+
if (payload.captureStatus) s.mirrorCaptureStatus = payload.captureStatus;
|
|
159
|
+
invalidate();
|
|
160
|
+
// Mode toggles render through the actions bus, not the record store.
|
|
161
|
+
for (const l of s.actionListeners) l();
|
|
162
|
+
}
|
|
163
|
+
|
|
95
164
|
// ---------------------------------------------------------------------------
|
|
96
165
|
// Derived math (shared by rows, detail view, and stats)
|
|
97
166
|
// ---------------------------------------------------------------------------
|
|
98
167
|
|
|
168
|
+
/** Device PixelRatio on device; the SYNCED device's ratio on a mirror. */
|
|
169
|
+
export function effectivePixelRatio(): number {
|
|
170
|
+
return imagesState().mirrorPixelRatio ?? PixelRatio.get();
|
|
171
|
+
}
|
|
172
|
+
|
|
99
173
|
/** Physical pixels actually needed to fill the laid-out view 1:1. */
|
|
100
174
|
export function neededPixels(record: ImageRecord): { width: number; height: number } | null {
|
|
101
175
|
if (!record.layout || record.layout.width <= 0 || record.layout.height <= 0) return null;
|
|
102
|
-
const scale =
|
|
176
|
+
const scale = effectivePixelRatio();
|
|
103
177
|
return {
|
|
104
178
|
width: Math.round(record.layout.width * scale),
|
|
105
179
|
height: Math.round(record.layout.height * scale),
|
|
@@ -40,10 +40,10 @@ import {
|
|
|
40
40
|
oversizeFactor,
|
|
41
41
|
subscribe,
|
|
42
42
|
} from "../store/imageEventStore";
|
|
43
|
-
import type { ImageRecord } from "../types";
|
|
43
|
+
import type { ImageRecord, ImageRecordWire } from "../types";
|
|
44
44
|
|
|
45
45
|
/** Slim wire form of a record (heavy/derived fields precomputed + rounded). */
|
|
46
|
-
function toWire(record: ImageRecord) {
|
|
46
|
+
function toWire(record: ImageRecord): ImageRecordWire {
|
|
47
47
|
const factor = oversizeFactor(record);
|
|
48
48
|
return {
|
|
49
49
|
id: record.id,
|
package/src/types.ts
CHANGED
|
@@ -97,6 +97,49 @@ export interface ImageStats {
|
|
|
97
97
|
estWastedBytes: number;
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
+
/**
|
|
101
|
+
* Slim wire form of a record as pushed by the external-sync adapter (heavy
|
|
102
|
+
* derived math precomputed on device). This is what dashboards receive and
|
|
103
|
+
* what `replaceFromSnapshot` reconstructs ImageRecords from — keep it in
|
|
104
|
+
* lockstep with `toWire` in sync/imagesSyncAdapter.ts (and the Flutter port).
|
|
105
|
+
*/
|
|
106
|
+
export interface ImageRecordWire {
|
|
107
|
+
id: number;
|
|
108
|
+
lib: ImageLib;
|
|
109
|
+
uri: string;
|
|
110
|
+
kind: SourceKind;
|
|
111
|
+
status: ImageStatus;
|
|
112
|
+
mounted: boolean;
|
|
113
|
+
cache: CacheVerdict | null;
|
|
114
|
+
cacheSource: "event" | "queryCache" | null;
|
|
115
|
+
ms: number | null;
|
|
116
|
+
intrinsic: { width: number; height: number } | null;
|
|
117
|
+
layout: { width: number; height: number } | null;
|
|
118
|
+
neededPx: { width: number; height: number } | null;
|
|
119
|
+
oversizeFactor: number | null;
|
|
120
|
+
decodedKB: number;
|
|
121
|
+
wastedKB: number;
|
|
122
|
+
progressSeen: boolean;
|
|
123
|
+
bytesTotal: number | null;
|
|
124
|
+
error: string | null;
|
|
125
|
+
errorCode: number | null;
|
|
126
|
+
loadCount: number;
|
|
127
|
+
overrideLabel: string | null;
|
|
128
|
+
hasAltText: boolean | null;
|
|
129
|
+
layoutShifts: number;
|
|
130
|
+
ageMs: number;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** Full snapshot payload the adapter pushes to watching dashboards. */
|
|
134
|
+
export interface ImagesSnapshotPayload {
|
|
135
|
+
stats?: ImageStats;
|
|
136
|
+
captureStatus?: CaptureStatus;
|
|
137
|
+
globalModes?: { network: "normal" | "offline" | "cold"; blank: boolean };
|
|
138
|
+
insights?: ImageInsights;
|
|
139
|
+
/** Newest first, capped at 100. */
|
|
140
|
+
records?: ImageRecordWire[];
|
|
141
|
+
}
|
|
142
|
+
|
|
100
143
|
/** Capture capability report — what the interceptors managed to hook. */
|
|
101
144
|
export interface CaptureStatus {
|
|
102
145
|
installed: boolean;
|