@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
|
@@ -1,271 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* ImageDetailFooter — bottom navbar for the detail view: previous/next
|
|
3
|
-
* stepping through the captured list at the edges, per-image actions in the
|
|
4
|
-
* middle (hard reload / retry), with inline action feedback. Styling matches
|
|
5
|
-
* shared-ui's EventStepperFooter so it reads like the rest of Buoy.
|
|
6
|
-
*
|
|
7
|
-
* Mount with key={record.id} so action feedback resets when stepping.
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import { useState } from "react";
|
|
11
|
-
import { StyleSheet, Text, TouchableOpacity, View } from "react-native";
|
|
12
|
-
import {
|
|
13
|
-
ChevronLeft,
|
|
14
|
-
ChevronRight,
|
|
15
|
-
RefreshCw,
|
|
16
|
-
night,
|
|
17
|
-
useSafeAreaInsets,
|
|
18
|
-
} from "@buoy-gg/shared-ui";
|
|
19
|
-
import { hardReloadRecord, retryRecord } from "../capture/actions";
|
|
20
|
-
import type { ActionResult } from "../capture/actions";
|
|
21
|
-
import { clearOverride, flashRecord, setOverride } from "../capture/overrides";
|
|
22
|
-
import type { ImageRecord } from "../types";
|
|
23
|
-
|
|
24
|
-
interface ImageDetailFooterProps {
|
|
25
|
-
record: ImageRecord;
|
|
26
|
-
hasPrevious: boolean;
|
|
27
|
-
hasNext: boolean;
|
|
28
|
-
onPrevious: () => void;
|
|
29
|
-
onNext: () => void;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export function ImageDetailFooter({
|
|
33
|
-
record,
|
|
34
|
-
hasPrevious,
|
|
35
|
-
hasNext,
|
|
36
|
-
onPrevious,
|
|
37
|
-
onNext,
|
|
38
|
-
}: ImageDetailFooterProps) {
|
|
39
|
-
const insets = useSafeAreaInsets();
|
|
40
|
-
const [message, setMessage] = useState<string | null>(null);
|
|
41
|
-
|
|
42
|
-
const runAction = (action: () => Promise<ActionResult>) => {
|
|
43
|
-
setMessage("…");
|
|
44
|
-
action()
|
|
45
|
-
.then((result) => setMessage(result.message))
|
|
46
|
-
.catch((error) => setMessage(String(error?.message ?? error)));
|
|
47
|
-
};
|
|
48
|
-
|
|
49
|
-
const actionsDisabled = !record.mounted;
|
|
50
|
-
const overrideActive = record.overrideLabel != null;
|
|
51
|
-
|
|
52
|
-
const simulate = (
|
|
53
|
-
source: { type: "error" } | { type: "hang" } | { type: "blank" },
|
|
54
|
-
label: string
|
|
55
|
-
) => {
|
|
56
|
-
setOverride(record.id, { source, label });
|
|
57
|
-
setMessage(`${label} — Restore to undo`);
|
|
58
|
-
};
|
|
59
|
-
|
|
60
|
-
return (
|
|
61
|
-
<View style={[styles.footer, { paddingBottom: 12 + insets.bottom }]}>
|
|
62
|
-
{message ? (
|
|
63
|
-
<Text style={styles.message} numberOfLines={1}>
|
|
64
|
-
{message}
|
|
65
|
-
</Text>
|
|
66
|
-
) : actionsDisabled ? (
|
|
67
|
-
<Text style={styles.hint} numberOfLines={1}>
|
|
68
|
-
Instance unmounted — reload actions need the image on screen.
|
|
69
|
-
</Text>
|
|
70
|
-
) : null}
|
|
71
|
-
|
|
72
|
-
<View style={styles.row}>
|
|
73
|
-
<TouchableOpacity
|
|
74
|
-
onPress={onPrevious}
|
|
75
|
-
disabled={!hasPrevious}
|
|
76
|
-
style={[styles.navButton, !hasPrevious && styles.disabled]}
|
|
77
|
-
activeOpacity={0.7}
|
|
78
|
-
accessibilityLabel="Previous image"
|
|
79
|
-
>
|
|
80
|
-
<ChevronLeft
|
|
81
|
-
size={20}
|
|
82
|
-
color={hasPrevious ? night.color.accent : night.color.textTertiary}
|
|
83
|
-
/>
|
|
84
|
-
</TouchableOpacity>
|
|
85
|
-
|
|
86
|
-
<TouchableOpacity
|
|
87
|
-
onPress={() => runAction(() => hardReloadRecord(record))}
|
|
88
|
-
disabled={actionsDisabled}
|
|
89
|
-
style={[styles.actionButton, actionsDisabled && styles.disabled]}
|
|
90
|
-
activeOpacity={0.7}
|
|
91
|
-
>
|
|
92
|
-
<RefreshCw
|
|
93
|
-
size={13}
|
|
94
|
-
color={actionsDisabled ? night.color.textTertiary : night.color.info}
|
|
95
|
-
/>
|
|
96
|
-
<Text style={[styles.actionText, actionsDisabled && styles.actionTextDisabled]}>
|
|
97
|
-
Hard reload
|
|
98
|
-
</Text>
|
|
99
|
-
</TouchableOpacity>
|
|
100
|
-
|
|
101
|
-
<TouchableOpacity
|
|
102
|
-
onPress={() => runAction(() => retryRecord(record))}
|
|
103
|
-
disabled={actionsDisabled}
|
|
104
|
-
style={[styles.actionButton, actionsDisabled && styles.disabled]}
|
|
105
|
-
activeOpacity={0.7}
|
|
106
|
-
>
|
|
107
|
-
<Text style={[styles.actionText, actionsDisabled && styles.actionTextDisabled]}>
|
|
108
|
-
{record.status === "error" ? "Retry" : "Reload"}
|
|
109
|
-
</Text>
|
|
110
|
-
</TouchableOpacity>
|
|
111
|
-
|
|
112
|
-
<TouchableOpacity
|
|
113
|
-
onPress={onNext}
|
|
114
|
-
disabled={!hasNext}
|
|
115
|
-
style={[styles.navButton, !hasNext && styles.disabled]}
|
|
116
|
-
activeOpacity={0.7}
|
|
117
|
-
accessibilityLabel="Next image"
|
|
118
|
-
>
|
|
119
|
-
<ChevronRight
|
|
120
|
-
size={20}
|
|
121
|
-
color={hasNext ? night.color.accent : night.color.textTertiary}
|
|
122
|
-
/>
|
|
123
|
-
</TouchableOpacity>
|
|
124
|
-
</View>
|
|
125
|
-
|
|
126
|
-
<View style={styles.row}>
|
|
127
|
-
<SimButton
|
|
128
|
-
label="Error"
|
|
129
|
-
disabled={actionsDisabled}
|
|
130
|
-
onPress={() => simulate({ type: "error" }, "Forced error")}
|
|
131
|
-
/>
|
|
132
|
-
<SimButton
|
|
133
|
-
label="Loading"
|
|
134
|
-
disabled={actionsDisabled}
|
|
135
|
-
onPress={() => simulate({ type: "hang" }, "Forced loading")}
|
|
136
|
-
/>
|
|
137
|
-
<SimButton
|
|
138
|
-
label="Blank"
|
|
139
|
-
disabled={actionsDisabled}
|
|
140
|
-
onPress={() => simulate({ type: "blank" }, "Blanked")}
|
|
141
|
-
/>
|
|
142
|
-
<SimButton
|
|
143
|
-
label="Find"
|
|
144
|
-
disabled={actionsDisabled}
|
|
145
|
-
onPress={() => {
|
|
146
|
-
flashRecord(record.id);
|
|
147
|
-
setMessage("Flashing a red border on the image (2.5s)");
|
|
148
|
-
}}
|
|
149
|
-
/>
|
|
150
|
-
<SimButton
|
|
151
|
-
label="Restore"
|
|
152
|
-
accent
|
|
153
|
-
disabled={!overrideActive}
|
|
154
|
-
onPress={() => {
|
|
155
|
-
clearOverride(record.id);
|
|
156
|
-
setMessage("Override removed — original source restored");
|
|
157
|
-
}}
|
|
158
|
-
/>
|
|
159
|
-
</View>
|
|
160
|
-
</View>
|
|
161
|
-
);
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
function SimButton({
|
|
165
|
-
label,
|
|
166
|
-
onPress,
|
|
167
|
-
disabled,
|
|
168
|
-
accent,
|
|
169
|
-
}: {
|
|
170
|
-
label: string;
|
|
171
|
-
onPress: () => void;
|
|
172
|
-
disabled?: boolean;
|
|
173
|
-
accent?: boolean;
|
|
174
|
-
}) {
|
|
175
|
-
return (
|
|
176
|
-
<TouchableOpacity
|
|
177
|
-
onPress={onPress}
|
|
178
|
-
disabled={disabled}
|
|
179
|
-
style={[styles.simButton, disabled && styles.disabled]}
|
|
180
|
-
activeOpacity={0.7}
|
|
181
|
-
>
|
|
182
|
-
<Text
|
|
183
|
-
style={[
|
|
184
|
-
styles.simText,
|
|
185
|
-
accent && !disabled && styles.simTextAccent,
|
|
186
|
-
disabled && styles.actionTextDisabled,
|
|
187
|
-
]}
|
|
188
|
-
>
|
|
189
|
-
{label}
|
|
190
|
-
</Text>
|
|
191
|
-
</TouchableOpacity>
|
|
192
|
-
);
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
const styles = StyleSheet.create({
|
|
196
|
-
footer: {
|
|
197
|
-
paddingHorizontal: 12,
|
|
198
|
-
paddingTop: 10,
|
|
199
|
-
// Dark glass over the star field — matches EventStepperFooter's night bar.
|
|
200
|
-
backgroundColor: "rgba(5, 5, 5, 0.92)",
|
|
201
|
-
borderTopWidth: 1,
|
|
202
|
-
borderTopColor: night.color.border,
|
|
203
|
-
gap: 8,
|
|
204
|
-
},
|
|
205
|
-
row: {
|
|
206
|
-
flexDirection: "row",
|
|
207
|
-
alignItems: "center",
|
|
208
|
-
gap: 8,
|
|
209
|
-
},
|
|
210
|
-
navButton: {
|
|
211
|
-
paddingHorizontal: 10,
|
|
212
|
-
paddingVertical: 8,
|
|
213
|
-
borderRadius: 6,
|
|
214
|
-
backgroundColor: night.color.button,
|
|
215
|
-
alignItems: "center",
|
|
216
|
-
justifyContent: "center",
|
|
217
|
-
},
|
|
218
|
-
actionButton: {
|
|
219
|
-
flex: 1,
|
|
220
|
-
flexDirection: "row",
|
|
221
|
-
alignItems: "center",
|
|
222
|
-
justifyContent: "center",
|
|
223
|
-
gap: 6,
|
|
224
|
-
paddingVertical: 8,
|
|
225
|
-
borderRadius: 6,
|
|
226
|
-
backgroundColor: night.color.button,
|
|
227
|
-
borderWidth: StyleSheet.hairlineWidth,
|
|
228
|
-
borderColor: night.color.border,
|
|
229
|
-
},
|
|
230
|
-
actionText: {
|
|
231
|
-
fontSize: 11,
|
|
232
|
-
fontWeight: "600",
|
|
233
|
-
color: night.color.text,
|
|
234
|
-
fontFamily: "monospace",
|
|
235
|
-
textTransform: "uppercase",
|
|
236
|
-
},
|
|
237
|
-
actionTextDisabled: {
|
|
238
|
-
color: night.color.textTertiary,
|
|
239
|
-
},
|
|
240
|
-
disabled: {
|
|
241
|
-
opacity: 0.4,
|
|
242
|
-
},
|
|
243
|
-
simButton: {
|
|
244
|
-
flex: 1,
|
|
245
|
-
alignItems: "center",
|
|
246
|
-
paddingVertical: 7,
|
|
247
|
-
borderRadius: 6,
|
|
248
|
-
backgroundColor: night.color.button,
|
|
249
|
-
borderWidth: StyleSheet.hairlineWidth,
|
|
250
|
-
borderColor: night.color.border,
|
|
251
|
-
},
|
|
252
|
-
simText: {
|
|
253
|
-
fontSize: 10,
|
|
254
|
-
fontWeight: "600",
|
|
255
|
-
color: night.color.textSecondary,
|
|
256
|
-
fontFamily: "monospace",
|
|
257
|
-
textTransform: "uppercase",
|
|
258
|
-
},
|
|
259
|
-
simTextAccent: {
|
|
260
|
-
color: night.color.warning,
|
|
261
|
-
},
|
|
262
|
-
message: {
|
|
263
|
-
color: night.color.info,
|
|
264
|
-
fontSize: 10,
|
|
265
|
-
fontFamily: "monospace",
|
|
266
|
-
},
|
|
267
|
-
hint: {
|
|
268
|
-
color: night.color.textTertiary,
|
|
269
|
-
fontSize: 10,
|
|
270
|
-
},
|
|
271
|
-
});
|
|
@@ -1,148 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* ImageRow — one captured image in the list: thumbnail, filename, library +
|
|
3
|
-
* cache badges, load time, and the size verdict.
|
|
4
|
-
*
|
|
5
|
-
* Thumbnails carry the buoy-internal testID so the capture layer skips them —
|
|
6
|
-
* the tool must never appear in its own registry.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import { Image, StyleSheet, Text, TouchableOpacity, View } from "react-native";
|
|
10
|
-
import { macOSColors, night } from "@buoy-gg/shared-ui";
|
|
11
|
-
import { cacheBadge, formatMs, sizeVerdict, statusColor, uriTail } from "../format";
|
|
12
|
-
import type { ImageRecord } from "../types";
|
|
13
|
-
|
|
14
|
-
interface ImageRowProps {
|
|
15
|
-
record: ImageRecord;
|
|
16
|
-
onPress: (id: number) => void;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export function ImageRow({ record, onPress }: ImageRowProps) {
|
|
20
|
-
const verdict = sizeVerdict(record);
|
|
21
|
-
const cache = cacheBadge(record);
|
|
22
|
-
const showThumb =
|
|
23
|
-
record.sourceKind === "network" ||
|
|
24
|
-
record.sourceKind === "asset" ||
|
|
25
|
-
record.sourceKind === "file";
|
|
26
|
-
|
|
27
|
-
return (
|
|
28
|
-
<TouchableOpacity
|
|
29
|
-
style={styles.row}
|
|
30
|
-
testID={`buoy-image-row-${record.id}`}
|
|
31
|
-
onPress={() => onPress(record.id)}
|
|
32
|
-
>
|
|
33
|
-
<View style={styles.thumbBox}>
|
|
34
|
-
{showThumb && record.uri ? (
|
|
35
|
-
<Image
|
|
36
|
-
testID="buoy-internal-images-thumb"
|
|
37
|
-
source={{ uri: record.uri }}
|
|
38
|
-
style={styles.thumb}
|
|
39
|
-
resizeMode="cover"
|
|
40
|
-
/>
|
|
41
|
-
) : (
|
|
42
|
-
<Text style={styles.thumbFallback}>?</Text>
|
|
43
|
-
)}
|
|
44
|
-
<View style={[styles.statusDot, { backgroundColor: statusColor(record) }]} />
|
|
45
|
-
</View>
|
|
46
|
-
|
|
47
|
-
<View style={styles.main}>
|
|
48
|
-
<Text style={styles.name} numberOfLines={1}>
|
|
49
|
-
{uriTail(record.uri)}
|
|
50
|
-
</Text>
|
|
51
|
-
<View style={styles.metaRow}>
|
|
52
|
-
<Text style={[styles.badge, { color: night.color.textTertiary }]}>
|
|
53
|
-
{record.lib === "expo" ? "EXPO" : "RN"}
|
|
54
|
-
</Text>
|
|
55
|
-
<Text style={[styles.badge, { color: cache.color }]}>{cache.label}</Text>
|
|
56
|
-
{record.overrideLabel ? (
|
|
57
|
-
<Text style={[styles.badge, { color: night.color.warning }]}>SIM</Text>
|
|
58
|
-
) : null}
|
|
59
|
-
<Text style={styles.meta}>{formatMs(record.durationMs)}</Text>
|
|
60
|
-
{record.intrinsic ? (
|
|
61
|
-
<Text style={styles.meta}>
|
|
62
|
-
{record.intrinsic.width}×{record.intrinsic.height}
|
|
63
|
-
</Text>
|
|
64
|
-
) : null}
|
|
65
|
-
</View>
|
|
66
|
-
</View>
|
|
67
|
-
|
|
68
|
-
<View style={styles.right}>
|
|
69
|
-
{record.status === "error" ? (
|
|
70
|
-
<Text style={[styles.verdict, { color: macOSColors.semantic.error }]}>FAILED</Text>
|
|
71
|
-
) : verdict.label ? (
|
|
72
|
-
<Text style={[styles.verdict, { color: verdict.color }]}>{verdict.label}</Text>
|
|
73
|
-
) : null}
|
|
74
|
-
</View>
|
|
75
|
-
</TouchableOpacity>
|
|
76
|
-
);
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
const styles = StyleSheet.create({
|
|
80
|
-
row: {
|
|
81
|
-
flexDirection: "row",
|
|
82
|
-
alignItems: "center",
|
|
83
|
-
paddingHorizontal: 12,
|
|
84
|
-
paddingVertical: 8,
|
|
85
|
-
borderBottomWidth: StyleSheet.hairlineWidth,
|
|
86
|
-
borderBottomColor: night.color.border,
|
|
87
|
-
gap: 10,
|
|
88
|
-
},
|
|
89
|
-
thumbBox: {
|
|
90
|
-
width: 40,
|
|
91
|
-
height: 40,
|
|
92
|
-
borderRadius: 6,
|
|
93
|
-
backgroundColor: night.color.bg,
|
|
94
|
-
alignItems: "center",
|
|
95
|
-
justifyContent: "center",
|
|
96
|
-
overflow: "hidden",
|
|
97
|
-
},
|
|
98
|
-
thumb: {
|
|
99
|
-
width: 40,
|
|
100
|
-
height: 40,
|
|
101
|
-
},
|
|
102
|
-
thumbFallback: {
|
|
103
|
-
color: night.color.textTertiary,
|
|
104
|
-
fontSize: 16,
|
|
105
|
-
fontWeight: "700",
|
|
106
|
-
},
|
|
107
|
-
statusDot: {
|
|
108
|
-
position: "absolute",
|
|
109
|
-
right: 2,
|
|
110
|
-
bottom: 2,
|
|
111
|
-
width: 8,
|
|
112
|
-
height: 8,
|
|
113
|
-
borderRadius: 4,
|
|
114
|
-
borderWidth: 1,
|
|
115
|
-
borderColor: night.color.bg,
|
|
116
|
-
},
|
|
117
|
-
main: {
|
|
118
|
-
flex: 1,
|
|
119
|
-
gap: 2,
|
|
120
|
-
},
|
|
121
|
-
name: {
|
|
122
|
-
color: night.color.text,
|
|
123
|
-
fontSize: 12,
|
|
124
|
-
fontWeight: "600",
|
|
125
|
-
},
|
|
126
|
-
metaRow: {
|
|
127
|
-
flexDirection: "row",
|
|
128
|
-
alignItems: "center",
|
|
129
|
-
gap: 8,
|
|
130
|
-
},
|
|
131
|
-
badge: {
|
|
132
|
-
fontSize: 9,
|
|
133
|
-
fontWeight: "700",
|
|
134
|
-
letterSpacing: 0.5,
|
|
135
|
-
},
|
|
136
|
-
meta: {
|
|
137
|
-
color: night.color.textSecondary,
|
|
138
|
-
fontSize: 10,
|
|
139
|
-
fontVariant: ["tabular-nums"],
|
|
140
|
-
},
|
|
141
|
-
right: {
|
|
142
|
-
alignItems: "flex-end",
|
|
143
|
-
},
|
|
144
|
-
verdict: {
|
|
145
|
-
fontSize: 10,
|
|
146
|
-
fontWeight: "700",
|
|
147
|
-
},
|
|
148
|
-
});
|
|
@@ -1,77 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* ImagesHeader — shared ModalHeader wiring for the Images tool, matching the
|
|
3
|
-
* Console/Network header pattern. List mode shows aggregate stats, the shared
|
|
4
|
-
* ToolbarCopyButton (full registry report) and the shared ToolbarClearButton;
|
|
5
|
-
* detail mode shows back navigation plus the copy button for that record's
|
|
6
|
-
* report. Copy behavior (Pro gating, hint banner, success/error feedback)
|
|
7
|
-
* comes from shared-ui — identical to every other tool.
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import {
|
|
11
|
-
ModalHeader,
|
|
12
|
-
ToolbarClearButton,
|
|
13
|
-
ToolbarCopyButton,
|
|
14
|
-
} from "@buoy-gg/shared-ui";
|
|
15
|
-
import { formatBytes } from "../format";
|
|
16
|
-
import type { ImageStats } from "../types";
|
|
17
|
-
|
|
18
|
-
interface ImagesHeaderProps {
|
|
19
|
-
stats: ImageStats;
|
|
20
|
-
detailOpen: boolean;
|
|
21
|
-
/** Index of the open record in the list (-1 when unknown). */
|
|
22
|
-
detailIndex?: number;
|
|
23
|
-
detailTotal?: number;
|
|
24
|
-
onCloseDetail: () => void;
|
|
25
|
-
onClear: () => void;
|
|
26
|
-
/** Builds the mode-appropriate report (registry or single record). */
|
|
27
|
-
getReport: () => string;
|
|
28
|
-
/** App-stack back (from the floating menu's AppHost). */
|
|
29
|
-
onBack?: () => void;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export function ImagesHeader({
|
|
33
|
-
stats,
|
|
34
|
-
detailOpen,
|
|
35
|
-
detailIndex = -1,
|
|
36
|
-
detailTotal = 0,
|
|
37
|
-
onCloseDetail,
|
|
38
|
-
onClear,
|
|
39
|
-
getReport,
|
|
40
|
-
onBack,
|
|
41
|
-
}: ImagesHeaderProps) {
|
|
42
|
-
if (detailOpen) {
|
|
43
|
-
const counter =
|
|
44
|
-
detailIndex >= 0 && detailTotal > 0
|
|
45
|
-
? `${detailIndex + 1} of ${detailTotal}`
|
|
46
|
-
: undefined;
|
|
47
|
-
return (
|
|
48
|
-
<ModalHeader>
|
|
49
|
-
<ModalHeader.Navigation onBack={onCloseDetail} />
|
|
50
|
-
<ModalHeader.Content title="Image detail" subtitle={counter} />
|
|
51
|
-
<ModalHeader.Actions>
|
|
52
|
-
<ToolbarCopyButton value={getReport} />
|
|
53
|
-
</ModalHeader.Actions>
|
|
54
|
-
</ModalHeader>
|
|
55
|
-
);
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
const subtitleParts = [
|
|
59
|
-
`${stats.total} images`,
|
|
60
|
-
`${stats.errors} failed`,
|
|
61
|
-
`≈${formatBytes(stats.estDecodedBytes)} decoded`,
|
|
62
|
-
];
|
|
63
|
-
if (stats.estWastedBytes > 0) {
|
|
64
|
-
subtitleParts.push(`${formatBytes(stats.estWastedBytes)} wasted`);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
return (
|
|
68
|
-
<ModalHeader>
|
|
69
|
-
{onBack ? <ModalHeader.Navigation onBack={onBack} /> : null}
|
|
70
|
-
<ModalHeader.Content title="Images" subtitle={subtitleParts.join(" · ")} />
|
|
71
|
-
<ModalHeader.Actions>
|
|
72
|
-
<ToolbarCopyButton value={getReport} />
|
|
73
|
-
<ToolbarClearButton onPress={onClear} />
|
|
74
|
-
</ModalHeader.Actions>
|
|
75
|
-
</ModalHeader>
|
|
76
|
-
);
|
|
77
|
-
}
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* ImagesIcon — re-exported from the Buoy Icon Format.
|
|
3
|
-
*
|
|
4
|
-
* The artwork lives in shared/icons/images.json and is generated into
|
|
5
|
-
* @buoy-gg/floating-tools-core, so React Native, the desktop dashboard and
|
|
6
|
-
* Flutter all render the same drawing. Edit the JSON, then `pnpm icons`.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
export { ImagesIcon, IMAGES_ICON_COLOR } from "@buoy-gg/shared-ui";
|
|
@@ -1,194 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* ImagesListFooter — the list screen's action bar: mass simulations applied
|
|
3
|
-
* to EVERY mounted image at once (error / loading / blank), mass hard-reload,
|
|
4
|
-
* flash everything, restore all — plus the global network simulation modes
|
|
5
|
-
* (Normal / Offline / Cold). Styled to match the detail footer.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import { useState, useSyncExternalStore } from "react";
|
|
9
|
-
import { StyleSheet, Text, TouchableOpacity, View } from "react-native";
|
|
10
|
-
import { night, useSafeAreaInsets } from "@buoy-gg/shared-ui";
|
|
11
|
-
import { hardReloadAllMounted, subscribeActions } from "../capture/actions";
|
|
12
|
-
import {
|
|
13
|
-
clearAllOverrides,
|
|
14
|
-
flashAllMounted,
|
|
15
|
-
getGlobalModes,
|
|
16
|
-
setNetworkMode,
|
|
17
|
-
setOverrideForAllMounted,
|
|
18
|
-
} from "../capture/overrides";
|
|
19
|
-
import type { NetworkMode, OverrideSource } from "../capture/overrides";
|
|
20
|
-
|
|
21
|
-
const NETWORK_MODES: { key: NetworkMode; label: string }[] = [
|
|
22
|
-
{ key: "normal", label: "Normal" },
|
|
23
|
-
{ key: "offline", label: "Offline" },
|
|
24
|
-
{ key: "cold", label: "Cold" },
|
|
25
|
-
];
|
|
26
|
-
|
|
27
|
-
export function ImagesListFooter() {
|
|
28
|
-
const insets = useSafeAreaInsets();
|
|
29
|
-
const [message, setMessage] = useState<string | null>(null);
|
|
30
|
-
const network = useSyncExternalStore(
|
|
31
|
-
subscribeActions,
|
|
32
|
-
() => getGlobalModes().network
|
|
33
|
-
);
|
|
34
|
-
|
|
35
|
-
const massOverride = (source: OverrideSource, label: string) => {
|
|
36
|
-
const count = setOverrideForAllMounted(source, label);
|
|
37
|
-
setMessage(count > 0 ? `${label} on ${count} image${count === 1 ? "" : "s"} — Restore all to undo` : "No mounted images");
|
|
38
|
-
};
|
|
39
|
-
|
|
40
|
-
return (
|
|
41
|
-
<View style={[styles.footer, { paddingBottom: 10 + insets.bottom }]}>
|
|
42
|
-
{message ? (
|
|
43
|
-
<Text style={styles.message} numberOfLines={1}>
|
|
44
|
-
{message}
|
|
45
|
-
</Text>
|
|
46
|
-
) : (
|
|
47
|
-
<Text style={styles.caption}>MASS ACTIONS — every image on screen</Text>
|
|
48
|
-
)}
|
|
49
|
-
<View style={styles.row}>
|
|
50
|
-
<MassButton label="Error" onPress={() => massOverride({ type: "error" }, "Forced error")} />
|
|
51
|
-
<MassButton label="Loading" onPress={() => massOverride({ type: "hang" }, "Forced loading")} />
|
|
52
|
-
<MassButton label="Blank" onPress={() => massOverride({ type: "blank" }, "Blanked")} />
|
|
53
|
-
<MassButton
|
|
54
|
-
label="Reload"
|
|
55
|
-
onPress={() => {
|
|
56
|
-
setMessage("…");
|
|
57
|
-
hardReloadAllMounted().then((r) => setMessage(r.message));
|
|
58
|
-
}}
|
|
59
|
-
/>
|
|
60
|
-
<MassButton
|
|
61
|
-
label="Find"
|
|
62
|
-
onPress={() => {
|
|
63
|
-
const count = flashAllMounted();
|
|
64
|
-
setMessage(`Flashing ${count} image${count === 1 ? "" : "s"} (2.5s)`);
|
|
65
|
-
}}
|
|
66
|
-
/>
|
|
67
|
-
<MassButton
|
|
68
|
-
label="Restore"
|
|
69
|
-
accent
|
|
70
|
-
onPress={() => {
|
|
71
|
-
const count = clearAllOverrides();
|
|
72
|
-
setMessage(count > 0 ? `Restored ${count} image${count === 1 ? "" : "s"}` : "No overrides active");
|
|
73
|
-
}}
|
|
74
|
-
/>
|
|
75
|
-
</View>
|
|
76
|
-
|
|
77
|
-
<View style={styles.row}>
|
|
78
|
-
<Text style={styles.netLabel}>NET</Text>
|
|
79
|
-
{NETWORK_MODES.map((mode) => {
|
|
80
|
-
const active = network === mode.key;
|
|
81
|
-
return (
|
|
82
|
-
<TouchableOpacity
|
|
83
|
-
key={mode.key}
|
|
84
|
-
onPress={() => {
|
|
85
|
-
setNetworkMode(mode.key);
|
|
86
|
-
// Modes only affect loads that happen AFTER they're set — an
|
|
87
|
-
// on-screen image keeps its bitmap until something reloads it.
|
|
88
|
-
setMessage(
|
|
89
|
-
mode.key === "normal"
|
|
90
|
-
? "Network simulation off — tap RELOAD to load normally again"
|
|
91
|
-
: mode.key === "offline"
|
|
92
|
-
? "Offline: network images FAIL (bundled assets still load) — tap RELOAD to apply to on-screen images"
|
|
93
|
-
: "Cold (bypass caches) — tap RELOAD: everything refetches like first launch"
|
|
94
|
-
);
|
|
95
|
-
}}
|
|
96
|
-
style={[styles.netButton, active && styles.netButtonActive]}
|
|
97
|
-
activeOpacity={0.7}
|
|
98
|
-
>
|
|
99
|
-
<Text style={[styles.buttonText, active && styles.netTextActive]}>
|
|
100
|
-
{mode.label}
|
|
101
|
-
</Text>
|
|
102
|
-
</TouchableOpacity>
|
|
103
|
-
);
|
|
104
|
-
})}
|
|
105
|
-
</View>
|
|
106
|
-
</View>
|
|
107
|
-
);
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
function MassButton({
|
|
111
|
-
label,
|
|
112
|
-
onPress,
|
|
113
|
-
accent,
|
|
114
|
-
}: {
|
|
115
|
-
label: string;
|
|
116
|
-
onPress: () => void;
|
|
117
|
-
accent?: boolean;
|
|
118
|
-
}) {
|
|
119
|
-
return (
|
|
120
|
-
<TouchableOpacity onPress={onPress} style={styles.button} activeOpacity={0.7}>
|
|
121
|
-
<Text style={[styles.buttonText, accent && styles.buttonTextAccent]}>{label}</Text>
|
|
122
|
-
</TouchableOpacity>
|
|
123
|
-
);
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
const styles = StyleSheet.create({
|
|
127
|
-
footer: {
|
|
128
|
-
paddingHorizontal: 12,
|
|
129
|
-
paddingTop: 8,
|
|
130
|
-
// Dark glass over the star field — matches EventStepperFooter's night bar.
|
|
131
|
-
backgroundColor: "rgba(5, 5, 5, 0.92)",
|
|
132
|
-
borderTopWidth: 1,
|
|
133
|
-
borderTopColor: night.color.border,
|
|
134
|
-
gap: 8,
|
|
135
|
-
},
|
|
136
|
-
caption: {
|
|
137
|
-
color: night.color.textTertiary,
|
|
138
|
-
fontSize: 9,
|
|
139
|
-
fontWeight: "700",
|
|
140
|
-
letterSpacing: 0.8,
|
|
141
|
-
},
|
|
142
|
-
message: {
|
|
143
|
-
color: night.color.info,
|
|
144
|
-
fontSize: 10,
|
|
145
|
-
fontFamily: "monospace",
|
|
146
|
-
},
|
|
147
|
-
row: {
|
|
148
|
-
flexDirection: "row",
|
|
149
|
-
gap: 6,
|
|
150
|
-
},
|
|
151
|
-
button: {
|
|
152
|
-
flex: 1,
|
|
153
|
-
alignItems: "center",
|
|
154
|
-
paddingVertical: 8,
|
|
155
|
-
borderRadius: 6,
|
|
156
|
-
backgroundColor: night.color.button,
|
|
157
|
-
borderWidth: StyleSheet.hairlineWidth,
|
|
158
|
-
borderColor: night.color.border,
|
|
159
|
-
},
|
|
160
|
-
buttonText: {
|
|
161
|
-
fontSize: 10,
|
|
162
|
-
fontWeight: "600",
|
|
163
|
-
color: night.color.textSecondary,
|
|
164
|
-
fontFamily: "monospace",
|
|
165
|
-
textTransform: "uppercase",
|
|
166
|
-
},
|
|
167
|
-
buttonTextAccent: {
|
|
168
|
-
color: night.color.warning,
|
|
169
|
-
},
|
|
170
|
-
netLabel: {
|
|
171
|
-
color: night.color.textTertiary,
|
|
172
|
-
fontSize: 9,
|
|
173
|
-
fontWeight: "700",
|
|
174
|
-
letterSpacing: 0.8,
|
|
175
|
-
alignSelf: "center",
|
|
176
|
-
marginRight: 2,
|
|
177
|
-
},
|
|
178
|
-
netButton: {
|
|
179
|
-
flex: 1,
|
|
180
|
-
alignItems: "center",
|
|
181
|
-
paddingVertical: 6,
|
|
182
|
-
borderRadius: 6,
|
|
183
|
-
backgroundColor: night.color.button,
|
|
184
|
-
borderWidth: StyleSheet.hairlineWidth,
|
|
185
|
-
borderColor: night.color.border,
|
|
186
|
-
},
|
|
187
|
-
netButtonActive: {
|
|
188
|
-
backgroundColor: night.color.accentSoft,
|
|
189
|
-
borderColor: night.color.accentBorderStrong,
|
|
190
|
-
},
|
|
191
|
-
netTextActive: {
|
|
192
|
-
color: night.color.accent,
|
|
193
|
-
},
|
|
194
|
-
});
|