@buoy-gg/time-machine 7.0.29 → 7.0.30
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +10 -11
- package/src/components/HeaderReloadButton.tsx +0 -45
- package/src/components/TimeMachineBody.tsx +0 -246
- package/src/components/TimeMachineModal.tsx +0 -75
- package/src/components/confirm.ts +0 -35
- package/src/components/format.ts +0 -17
- package/src/components/list/CaptureBar.tsx +0 -100
- package/src/components/list/RestoreOutcomeCard.tsx +0 -181
- package/src/components/list/SnapshotRow.tsx +0 -164
- package/src/components/list/SourcesStrip.tsx +0 -127
- package/src/components/list/UndoDeleteBar.tsx +0 -76
- package/src/components/list/WipeAllRow.tsx +0 -42
- package/src/components/preview/ItemDiffPanel.tsx +0 -83
- package/src/components/preview/PreviewFlow.tsx +0 -70
- package/src/components/preview/RestorePointNameRow.tsx +0 -80
- package/src/components/preview/RestorePreviewScreen.tsx +0 -1149
- package/src/components/preview/verdictMeta.ts +0 -40
- package/src/components/sources.ts +0 -92
- package/src/hooks/useTimeMachine.ts +0 -26
- package/src/index.tsx +0 -68
- package/src/preset.tsx +0 -47
- package/src/store/routeRestore.ts +0 -194
- package/src/store/snapshotVault.ts +0 -274
- package/src/store/timeMachineStore.ts +0 -881
- package/src/sync/timeMachineSyncAdapter.ts +0 -241
- package/src/ui/SwipeActionsRow.tsx +0 -199
- package/src/ui/primitives.tsx +0 -174
- package/src/ui/tmTheme.ts +0 -109
package/package.json
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@buoy-gg/time-machine",
|
|
3
|
-
"version": "7.0.
|
|
3
|
+
"version": "7.0.30",
|
|
4
4
|
"description": "Time Machine - snapshot & restore app state (storage, redux, zustand, jotai, react-query) for React Native DevTools",
|
|
5
|
-
"main": "lib/commonjs/index",
|
|
6
|
-
"module": "lib/module/index",
|
|
5
|
+
"main": "lib/commonjs/index.js",
|
|
6
|
+
"module": "lib/module/index.js",
|
|
7
7
|
"types": "lib/typescript/index.d.ts",
|
|
8
|
-
"react-native": "
|
|
8
|
+
"react-native": "lib/module/index.js",
|
|
9
9
|
"source": "src/index.tsx",
|
|
10
10
|
"files": [
|
|
11
|
-
"src",
|
|
12
11
|
"lib"
|
|
13
12
|
],
|
|
14
13
|
"sideEffects": [
|
|
@@ -17,21 +16,21 @@
|
|
|
17
16
|
"./lib/module/index.js"
|
|
18
17
|
],
|
|
19
18
|
"dependencies": {
|
|
20
|
-
"@buoy-gg/floating-tools-core": "^7.0.
|
|
21
|
-
"@buoy-gg/shared-ui": "^7.0.
|
|
19
|
+
"@buoy-gg/floating-tools-core": "^7.0.30",
|
|
20
|
+
"@buoy-gg/shared-ui": "^7.0.30"
|
|
22
21
|
},
|
|
23
22
|
"peerDependencies": {
|
|
24
|
-
"@buoy-gg/license": "7.0.
|
|
23
|
+
"@buoy-gg/license": "7.0.30",
|
|
25
24
|
"react": "*",
|
|
26
25
|
"react-native": "*",
|
|
27
|
-
"@buoy-gg/core": "^7.0.
|
|
26
|
+
"@buoy-gg/core": "^7.0.30"
|
|
28
27
|
},
|
|
29
28
|
"devDependencies": {
|
|
30
29
|
"@types/react": "^19.1.0",
|
|
31
30
|
"@types/react-native": "^0.73.0",
|
|
32
31
|
"typescript": "~5.8.3",
|
|
33
|
-
"@buoy-gg/license": "^7.0.
|
|
34
|
-
"@buoy-gg/core": "^7.0.
|
|
32
|
+
"@buoy-gg/license": "^7.0.30",
|
|
33
|
+
"@buoy-gg/core": "^7.0.30"
|
|
35
34
|
},
|
|
36
35
|
"react-native-builder-bob": {
|
|
37
36
|
"source": "src",
|
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Labeled "RELOAD APP" header action — the one reload affordance in the tool.
|
|
3
|
-
* Plain JS-bundle reload (no restore): in-memory state rebuilds from whatever
|
|
4
|
-
* is persisted. Registered as the preset's default headerActions, so it shows
|
|
5
|
-
* on every screen unless a screen overrides the header actions.
|
|
6
|
-
*/
|
|
7
|
-
import { StyleSheet, Text, TouchableOpacity } from "react-native";
|
|
8
|
-
import { RefreshCw, night, scheduleReloadApp } from "@buoy-gg/shared-ui";
|
|
9
|
-
|
|
10
|
-
export function HeaderReloadButton() {
|
|
11
|
-
const onPress = () => {
|
|
12
|
-
try {
|
|
13
|
-
scheduleReloadApp({ reason: "buoy time-machine reload app" });
|
|
14
|
-
} catch {
|
|
15
|
-
// No reload mechanism available (bare production build) — nothing to do.
|
|
16
|
-
}
|
|
17
|
-
};
|
|
18
|
-
return (
|
|
19
|
-
<TouchableOpacity
|
|
20
|
-
style={styles.button}
|
|
21
|
-
onPress={onPress}
|
|
22
|
-
hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}
|
|
23
|
-
accessibilityLabel="Reload app"
|
|
24
|
-
accessibilityRole="button"
|
|
25
|
-
testID="time-machine-reload-app"
|
|
26
|
-
>
|
|
27
|
-
<RefreshCw size={12} color={night.color.textSecondary} />
|
|
28
|
-
<Text style={styles.label}>RELOAD APP</Text>
|
|
29
|
-
</TouchableOpacity>
|
|
30
|
-
);
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
const styles = StyleSheet.create({
|
|
34
|
-
button: {
|
|
35
|
-
flexDirection: "row",
|
|
36
|
-
alignItems: "center",
|
|
37
|
-
gap: 6,
|
|
38
|
-
padding: 4,
|
|
39
|
-
},
|
|
40
|
-
label: {
|
|
41
|
-
color: night.color.textSecondary,
|
|
42
|
-
fontSize: 12,
|
|
43
|
-
fontWeight: "600",
|
|
44
|
-
},
|
|
45
|
-
});
|
|
@@ -1,246 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Time Machine body — the screen router plus the restore-point list.
|
|
3
|
-
*
|
|
4
|
-
* Screens: list → preview. Each is its own component that mounts and
|
|
5
|
-
* unmounts whole (no branching inside a single tree), and each owns its own
|
|
6
|
-
* scrolling — the host must NOT wrap this in a scroll view, or the preview's
|
|
7
|
-
* virtualized FlatList is defeated. Item diffs expand inline in that list
|
|
8
|
-
* rather than being a third screen.
|
|
9
|
-
*
|
|
10
|
-
* The list follows the grouped-inset surface in ../ui: capture bar, the
|
|
11
|
-
* Sources strip, one card of restore points with swipe-to-delete, and the
|
|
12
|
-
* app-wide wipe kept last and understated.
|
|
13
|
-
*/
|
|
14
|
-
import { useState } from "react";
|
|
15
|
-
import { ScrollView, StyleSheet, Text, View } from "react-native";
|
|
16
|
-
import { useSafeAreaInsets, useLimit } from "@buoy-gg/shared-ui";
|
|
17
|
-
import { useToolHost } from "@buoy-gg/core";
|
|
18
|
-
import { useIsPro } from "@buoy-gg/license";
|
|
19
|
-
import { useTimeMachineState } from "../hooks/useTimeMachine";
|
|
20
|
-
import { timeMachineStore } from "../store/timeMachineStore";
|
|
21
|
-
import type { SnapshotMeta } from "../store/snapshotVault";
|
|
22
|
-
import { Footnote, Group, SectionLabel } from "../ui/primitives";
|
|
23
|
-
import { tm } from "../ui/tmTheme";
|
|
24
|
-
import { useSourceStatuses, type SourceStatus } from "./sources";
|
|
25
|
-
import { confirmDestructive } from "./confirm";
|
|
26
|
-
import { CaptureBar } from "./list/CaptureBar";
|
|
27
|
-
import {
|
|
28
|
-
RestoreOutcomeCard,
|
|
29
|
-
restoreNeedsAttention,
|
|
30
|
-
} from "./list/RestoreOutcomeCard";
|
|
31
|
-
import { SnapshotRow } from "./list/SnapshotRow";
|
|
32
|
-
import { UndoDeleteBar } from "./list/UndoDeleteBar";
|
|
33
|
-
import { SourcesStrip } from "./list/SourcesStrip";
|
|
34
|
-
import { WipeAllRow } from "./list/WipeAllRow";
|
|
35
|
-
import { PreviewScreen } from "./preview/PreviewFlow";
|
|
36
|
-
|
|
37
|
-
type Screen = { kind: "list" } | { kind: "preview" };
|
|
38
|
-
|
|
39
|
-
export function TimeMachineBody() {
|
|
40
|
-
const [screen, setScreen] = useState<Screen>({ kind: "list" });
|
|
41
|
-
const { snapshots, busy, preview } = useTimeMachineState();
|
|
42
|
-
const sources = useSourceStatuses();
|
|
43
|
-
|
|
44
|
-
const backToList = () => {
|
|
45
|
-
timeMachineStore.clearPreview();
|
|
46
|
-
setScreen({ kind: "list" });
|
|
47
|
-
};
|
|
48
|
-
|
|
49
|
-
// The preview can't render without a built diff (clearPreview races a
|
|
50
|
-
// back-navigation) — fall through to the list rather than an empty shell.
|
|
51
|
-
if (screen.kind === "preview" && preview)
|
|
52
|
-
return (
|
|
53
|
-
<PreviewScreen
|
|
54
|
-
preview={preview}
|
|
55
|
-
snapshots={snapshots}
|
|
56
|
-
busy={busy !== null}
|
|
57
|
-
onClose={backToList}
|
|
58
|
-
/>
|
|
59
|
-
);
|
|
60
|
-
|
|
61
|
-
return (
|
|
62
|
-
<RestorePointList
|
|
63
|
-
sources={sources}
|
|
64
|
-
onOpenPreview={(meta) => {
|
|
65
|
-
void timeMachineStore.buildPreview({ snapshotId: meta.id });
|
|
66
|
-
setScreen({ kind: "preview" });
|
|
67
|
-
}}
|
|
68
|
-
/>
|
|
69
|
-
);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
function RestorePointList({
|
|
73
|
-
sources,
|
|
74
|
-
onOpenPreview,
|
|
75
|
-
}: {
|
|
76
|
-
sources: SourceStatus[];
|
|
77
|
-
onOpenPreview: (meta: SnapshotMeta) => void;
|
|
78
|
-
}) {
|
|
79
|
-
const { snapshots, hasLoaded, busy, lastOutcome, recentlyDeleted } =
|
|
80
|
-
useTimeMachineState();
|
|
81
|
-
const isPro = useIsPro();
|
|
82
|
-
const snapshotLimit = useLimit("timeMachine.snapshots");
|
|
83
|
-
const host = useToolHost();
|
|
84
|
-
const insets = useSafeAreaInsets({ minBottom: 16 });
|
|
85
|
-
|
|
86
|
-
// Header search (when hosted by the core shell) filters the list.
|
|
87
|
-
const query = host.search.trim().toLowerCase();
|
|
88
|
-
const visible = query
|
|
89
|
-
? snapshots.filter((s) => s.name.toLowerCase().includes(query))
|
|
90
|
-
: snapshots;
|
|
91
|
-
|
|
92
|
-
// Infinity for Pro, so this is simply false for them.
|
|
93
|
-
const atCaptureLimit = snapshots.length >= snapshotLimit;
|
|
94
|
-
const captureDisabled = busy !== null || atCaptureLimit;
|
|
95
|
-
|
|
96
|
-
/**
|
|
97
|
-
* Restoring is the last thing anyone wants from this screen, so the tool
|
|
98
|
-
* gets out of the way the moment it worked — with no toast and no banner.
|
|
99
|
-
* The restored app IS the confirmation, and anything announcing it is one
|
|
100
|
-
* more thing to dismiss on a flow people run dozens of times an hour.
|
|
101
|
-
*
|
|
102
|
-
* It MINIMIZES rather than closes: the list, the search and the screen you
|
|
103
|
-
* were on all survive, and coming back is one tap on the dock. A partial
|
|
104
|
-
* restore stays open instead, because the outcome card is the only place
|
|
105
|
-
* the per-source failures are reported.
|
|
106
|
-
*/
|
|
107
|
-
const restore = (meta: SnapshotMeta) => {
|
|
108
|
-
if (busy) return;
|
|
109
|
-
if (meta.baseline) {
|
|
110
|
-
confirmDestructive(
|
|
111
|
-
"Restore fresh install?",
|
|
112
|
-
"This wipes all app storage and reloads the app.",
|
|
113
|
-
"Wipe & reload",
|
|
114
|
-
() => void timeMachineStore.restore(meta.id, "reload")
|
|
115
|
-
);
|
|
116
|
-
return;
|
|
117
|
-
}
|
|
118
|
-
void timeMachineStore.restore(meta.id, "live").then((outcome) => {
|
|
119
|
-
const clean = Object.values(outcome.results).every((r) => r.ok);
|
|
120
|
-
if (!clean || outcome.willReload) return;
|
|
121
|
-
host.minimize();
|
|
122
|
-
});
|
|
123
|
-
};
|
|
124
|
-
|
|
125
|
-
return (
|
|
126
|
-
<ScrollView
|
|
127
|
-
style={styles.fill}
|
|
128
|
-
contentContainerStyle={[styles.content, { paddingBottom: insets.bottom }]}
|
|
129
|
-
testID="time-machine-scroll"
|
|
130
|
-
// Without this, the first tap after typing a name only DISMISSES the
|
|
131
|
-
// keyboard — Capture never fires and the restore point is silently not
|
|
132
|
-
// taken. Every other Buoy screen with a field inside a scroller sets it;
|
|
133
|
-
// this one didn't, and only the XCTest lane could see it (Detox's
|
|
134
|
-
// replaceText never opens a keyboard, so the tap always landed).
|
|
135
|
-
keyboardShouldPersistTaps="handled"
|
|
136
|
-
>
|
|
137
|
-
{/* Only when the restore did something the app itself won't show you. */}
|
|
138
|
-
{lastOutcome && restoreNeedsAttention(lastOutcome) ? (
|
|
139
|
-
<RestoreOutcomeCard outcome={lastOutcome} />
|
|
140
|
-
) : null}
|
|
141
|
-
|
|
142
|
-
{recentlyDeleted ? (
|
|
143
|
-
<UndoDeleteBar
|
|
144
|
-
name={recentlyDeleted.stored.meta.name}
|
|
145
|
-
at={recentlyDeleted.at}
|
|
146
|
-
onUndo={() => void timeMachineStore.undoDelete()}
|
|
147
|
-
onExpire={() => timeMachineStore.clearRecentlyDeleted()}
|
|
148
|
-
/>
|
|
149
|
-
) : null}
|
|
150
|
-
|
|
151
|
-
<View>
|
|
152
|
-
<CaptureBar
|
|
153
|
-
disabled={captureDisabled}
|
|
154
|
-
busy={busy === "capture"}
|
|
155
|
-
onCapture={(name) => void timeMachineStore.capture(name)}
|
|
156
|
-
/>
|
|
157
|
-
</View>
|
|
158
|
-
{atCaptureLimit ? (
|
|
159
|
-
<Footnote tone="warning" testID="time-machine-limit">
|
|
160
|
-
Free plan keeps {snapshotLimit} restore points. Upgrade to Pro
|
|
161
|
-
for unlimited snapshots.
|
|
162
|
-
</Footnote>
|
|
163
|
-
) : null}
|
|
164
|
-
|
|
165
|
-
<SectionLabel>Sources</SectionLabel>
|
|
166
|
-
<SourcesStrip sources={sources} />
|
|
167
|
-
{/* A source that can't fully participate says why, right where its
|
|
168
|
-
tile is — the strip only has room for the dot. */}
|
|
169
|
-
{sources
|
|
170
|
-
.filter((s) => s.detail && !(s.canCapture && s.canRestore))
|
|
171
|
-
.map((s) => (
|
|
172
|
-
<Footnote key={s.id} testID={`time-machine-source-${s.id}-detail`}>
|
|
173
|
-
{s.label}: {s.detail}
|
|
174
|
-
</Footnote>
|
|
175
|
-
))}
|
|
176
|
-
<SectionLabel>Restore Points</SectionLabel>
|
|
177
|
-
<Group>
|
|
178
|
-
{visible.map((meta) => (
|
|
179
|
-
<SnapshotRow
|
|
180
|
-
key={meta.id}
|
|
181
|
-
meta={meta}
|
|
182
|
-
busy={busy !== null}
|
|
183
|
-
onOpen={() => onOpenPreview(meta)}
|
|
184
|
-
onRestore={() => restore(meta)}
|
|
185
|
-
onDelete={() => void timeMachineStore.delete(meta.id)}
|
|
186
|
-
onDuplicate={() => void timeMachineStore.duplicate(meta.id)}
|
|
187
|
-
/>
|
|
188
|
-
))}
|
|
189
|
-
{visible.length === 0 ? (
|
|
190
|
-
<Text style={styles.empty} testID="time-machine-empty">
|
|
191
|
-
{!hasLoaded
|
|
192
|
-
? "Loading restore points…"
|
|
193
|
-
: snapshots.length > 0
|
|
194
|
-
? "No restore point matches that name."
|
|
195
|
-
: "Set up the state you want to come back to, then capture it."}
|
|
196
|
-
</Text>
|
|
197
|
-
) : null}
|
|
198
|
-
</Group>
|
|
199
|
-
{visible.length > 0 ? (
|
|
200
|
-
<Footnote testID="time-machine-swipe-hint">
|
|
201
|
-
Swipe a restore point to copy or delete it.
|
|
202
|
-
</Footnote>
|
|
203
|
-
) : null}
|
|
204
|
-
|
|
205
|
-
<View style={styles.spacer} />
|
|
206
|
-
|
|
207
|
-
<Group>
|
|
208
|
-
<WipeAllRow
|
|
209
|
-
disabled={busy !== null}
|
|
210
|
-
onWipe={() =>
|
|
211
|
-
confirmDestructive(
|
|
212
|
-
"Wipe all app state?",
|
|
213
|
-
"This deletes all app storage and reloads the app — like a fresh install.",
|
|
214
|
-
"Wipe & reload",
|
|
215
|
-
() => void timeMachineStore.wipeAll()
|
|
216
|
-
)
|
|
217
|
-
}
|
|
218
|
-
/>
|
|
219
|
-
</Group>
|
|
220
|
-
<Footnote centered>
|
|
221
|
-
Deletes all app storage and reloads — like a fresh install.
|
|
222
|
-
</Footnote>
|
|
223
|
-
</ScrollView>
|
|
224
|
-
);
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
const styles = StyleSheet.create({
|
|
228
|
-
fill: {
|
|
229
|
-
flex: 1,
|
|
230
|
-
// Transparent so JsModal's ToolBackground shows through (the modal paints opaque base beneath it).
|
|
231
|
-
backgroundColor: "transparent",
|
|
232
|
-
},
|
|
233
|
-
content: {
|
|
234
|
-
paddingTop: 14,
|
|
235
|
-
},
|
|
236
|
-
empty: {
|
|
237
|
-
color: tm.color.labelSecondary,
|
|
238
|
-
fontSize: tm.font.footnote,
|
|
239
|
-
lineHeight: tm.font.footnote * 1.4,
|
|
240
|
-
paddingHorizontal: tm.textInset,
|
|
241
|
-
paddingVertical: 16,
|
|
242
|
-
},
|
|
243
|
-
spacer: {
|
|
244
|
-
height: 24,
|
|
245
|
-
},
|
|
246
|
-
});
|
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* TimeMachineModal — bottom-sheet wrapper rendered by FloatingDevTools when
|
|
3
|
-
* the Time Machine tool is opened. TimeMachineToolView is the inline variant
|
|
4
|
-
* for Buoy Desktop.
|
|
5
|
-
*/
|
|
6
|
-
import { StyleSheet, Text, View } from "react-native";
|
|
7
|
-
import { JsModal } from "@buoy-gg/shared-ui";
|
|
8
|
-
import type { ModalState } from "@buoy-gg/shared-ui";
|
|
9
|
-
import { tm } from "../ui/tmTheme";
|
|
10
|
-
import { TimeMachineBody } from "./TimeMachineBody";
|
|
11
|
-
|
|
12
|
-
export interface TimeMachineModalProps {
|
|
13
|
-
visible: boolean;
|
|
14
|
-
onClose: () => void;
|
|
15
|
-
onBack?: () => void;
|
|
16
|
-
onMinimize?: (modalState: ModalState) => void;
|
|
17
|
-
persistenceKey?: string;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
function Header() {
|
|
21
|
-
return (
|
|
22
|
-
<View style={styles.header}>
|
|
23
|
-
<Text style={styles.headerTitle}>Time Machine</Text>
|
|
24
|
-
<Text style={styles.headerSubtitle}>Snapshot & restore app state</Text>
|
|
25
|
-
</View>
|
|
26
|
-
);
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export function TimeMachineModal({
|
|
30
|
-
visible,
|
|
31
|
-
onClose,
|
|
32
|
-
onBack: _onBack,
|
|
33
|
-
onMinimize,
|
|
34
|
-
persistenceKey,
|
|
35
|
-
}: TimeMachineModalProps) {
|
|
36
|
-
return (
|
|
37
|
-
<JsModal
|
|
38
|
-
visible={visible}
|
|
39
|
-
onClose={onClose}
|
|
40
|
-
onMinimize={onMinimize}
|
|
41
|
-
persistenceKey={persistenceKey}
|
|
42
|
-
header={{
|
|
43
|
-
showToggleButton: true,
|
|
44
|
-
customContent: <Header />,
|
|
45
|
-
}}
|
|
46
|
-
enablePersistence
|
|
47
|
-
initialMode="bottomSheet"
|
|
48
|
-
disableScrollWrapper
|
|
49
|
-
>
|
|
50
|
-
<TimeMachineBody />
|
|
51
|
-
</JsModal>
|
|
52
|
-
);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/** Inline variant (no modal chrome) — used by Buoy Desktop. */
|
|
56
|
-
export function TimeMachineToolView() {
|
|
57
|
-
return <TimeMachineBody />;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
const styles = StyleSheet.create({
|
|
61
|
-
header: {
|
|
62
|
-
paddingHorizontal: 4,
|
|
63
|
-
gap: 2,
|
|
64
|
-
},
|
|
65
|
-
headerTitle: {
|
|
66
|
-
color: tm.color.label,
|
|
67
|
-
fontSize: 22,
|
|
68
|
-
fontWeight: "700",
|
|
69
|
-
letterSpacing: 0.3,
|
|
70
|
-
},
|
|
71
|
-
headerSubtitle: {
|
|
72
|
-
color: tm.color.labelSecondary,
|
|
73
|
-
fontSize: tm.font.footnote,
|
|
74
|
-
},
|
|
75
|
-
});
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Confirmation dialogs for the destructive / branching Time Machine actions.
|
|
3
|
-
*
|
|
4
|
-
* RN-web's Alert renders no buttons at all, so the web path (Buoy Desktop,
|
|
5
|
-
* the web example) uses `window.confirm`.
|
|
6
|
-
*
|
|
7
|
-
* There is deliberately NO "restore the route too?" prompt: that question is
|
|
8
|
-
* answered once per restore point by the route toggle on its row, because
|
|
9
|
-
* asking it on every restore meant answering it thirty times a day.
|
|
10
|
-
*/
|
|
11
|
-
import { Alert, Platform } from "react-native";
|
|
12
|
-
|
|
13
|
-
function webConfirm(message: string): boolean {
|
|
14
|
-
const confirmFn = (globalThis as { confirm?: (msg: string) => boolean })
|
|
15
|
-
.confirm;
|
|
16
|
-
// No dialog available at all (headless web): don't silently swallow the
|
|
17
|
-
// user's intent — treat it as confirmed, same as the previous behavior.
|
|
18
|
-
return confirmFn ? confirmFn(message) : true;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export function confirmDestructive(
|
|
22
|
-
title: string,
|
|
23
|
-
message: string,
|
|
24
|
-
confirmLabel: string,
|
|
25
|
-
onConfirm: () => void
|
|
26
|
-
) {
|
|
27
|
-
if (Platform.OS === "web") {
|
|
28
|
-
if (webConfirm(`${title}\n\n${message}`)) onConfirm();
|
|
29
|
-
return;
|
|
30
|
-
}
|
|
31
|
-
Alert.alert(title, message, [
|
|
32
|
-
{ text: "Cancel", style: "cancel" },
|
|
33
|
-
{ text: confirmLabel, style: "destructive", onPress: onConfirm },
|
|
34
|
-
]);
|
|
35
|
-
}
|
package/src/components/format.ts
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
/** Display formatting shared by the Time Machine screens. */
|
|
2
|
-
|
|
3
|
-
export function timeAgo(ts: number): string {
|
|
4
|
-
const s = Math.max(0, Math.round((Date.now() - ts) / 1000));
|
|
5
|
-
if (s < 60) return `${s}s ago`;
|
|
6
|
-
const m = Math.round(s / 60);
|
|
7
|
-
if (m < 60) return `${m}m ago`;
|
|
8
|
-
const h = Math.round(m / 60);
|
|
9
|
-
if (h < 24) return `${h}h ago`;
|
|
10
|
-
return `${Math.round(h / 24)}d ago`;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export function formatBytes(bytes: number): string {
|
|
14
|
-
if (bytes < 1024) return `${bytes} B`;
|
|
15
|
-
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
16
|
-
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
17
|
-
}
|
|
@@ -1,100 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Capture bar — the single field-plus-pill control at the top of the list.
|
|
3
|
-
*
|
|
4
|
-
* Owns the draft name itself so keystrokes re-render this component and not
|
|
5
|
-
* the snapshot list underneath it. The parent only hears about a finished
|
|
6
|
-
* capture.
|
|
7
|
-
*/
|
|
8
|
-
import { useState } from "react";
|
|
9
|
-
import {
|
|
10
|
-
StyleSheet,
|
|
11
|
-
Text,
|
|
12
|
-
TextInput,
|
|
13
|
-
TouchableOpacity,
|
|
14
|
-
View,
|
|
15
|
-
} from "react-native";
|
|
16
|
-
import { tm } from "../../ui/tmTheme";
|
|
17
|
-
|
|
18
|
-
export function CaptureBar({
|
|
19
|
-
disabled,
|
|
20
|
-
busy,
|
|
21
|
-
onCapture,
|
|
22
|
-
}: {
|
|
23
|
-
disabled: boolean;
|
|
24
|
-
busy: boolean;
|
|
25
|
-
onCapture: (name: string) => void;
|
|
26
|
-
}) {
|
|
27
|
-
const [name, setName] = useState("");
|
|
28
|
-
|
|
29
|
-
const capture = () => {
|
|
30
|
-
if (disabled) return;
|
|
31
|
-
onCapture(name);
|
|
32
|
-
setName("");
|
|
33
|
-
};
|
|
34
|
-
|
|
35
|
-
return (
|
|
36
|
-
<View style={styles.bar}>
|
|
37
|
-
<TextInput
|
|
38
|
-
style={styles.input}
|
|
39
|
-
value={name}
|
|
40
|
-
onChangeText={setName}
|
|
41
|
-
placeholder="Name this restore point…"
|
|
42
|
-
placeholderTextColor={tm.color.labelTertiary}
|
|
43
|
-
editable={!disabled}
|
|
44
|
-
onSubmitEditing={capture}
|
|
45
|
-
returnKeyType="done"
|
|
46
|
-
testID="time-machine-name-input"
|
|
47
|
-
/>
|
|
48
|
-
<TouchableOpacity
|
|
49
|
-
style={[styles.button, disabled ? styles.buttonDisabled : null]}
|
|
50
|
-
onPress={capture}
|
|
51
|
-
disabled={disabled}
|
|
52
|
-
accessibilityRole="button"
|
|
53
|
-
accessibilityLabel="Capture restore point"
|
|
54
|
-
testID="time-machine-capture"
|
|
55
|
-
>
|
|
56
|
-
<Text style={styles.buttonText}>{busy ? "Capturing…" : "Capture"}</Text>
|
|
57
|
-
</TouchableOpacity>
|
|
58
|
-
</View>
|
|
59
|
-
);
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
const styles = StyleSheet.create({
|
|
63
|
-
bar: {
|
|
64
|
-
flexDirection: "row",
|
|
65
|
-
alignItems: "center",
|
|
66
|
-
gap: 8,
|
|
67
|
-
marginHorizontal: tm.gutter,
|
|
68
|
-
backgroundColor: tm.color.group,
|
|
69
|
-
borderRadius: tm.radius.group,
|
|
70
|
-
paddingLeft: tm.textInset,
|
|
71
|
-
padding: 6,
|
|
72
|
-
},
|
|
73
|
-
input: {
|
|
74
|
-
flex: 1,
|
|
75
|
-
minWidth: 0,
|
|
76
|
-
color: tm.color.label,
|
|
77
|
-
fontSize: tm.font.row,
|
|
78
|
-
letterSpacing: tm.tracking.row,
|
|
79
|
-
// Zero out the platform chrome so the field reads as part of the card.
|
|
80
|
-
padding: 0,
|
|
81
|
-
backgroundColor: "transparent",
|
|
82
|
-
},
|
|
83
|
-
button: {
|
|
84
|
-
height: 34,
|
|
85
|
-
paddingHorizontal: 16,
|
|
86
|
-
borderRadius: tm.radius.pill,
|
|
87
|
-
backgroundColor: tm.color.accent,
|
|
88
|
-
alignItems: "center",
|
|
89
|
-
justifyContent: "center",
|
|
90
|
-
},
|
|
91
|
-
buttonDisabled: {
|
|
92
|
-
opacity: 0.4,
|
|
93
|
-
},
|
|
94
|
-
buttonText: {
|
|
95
|
-
color: tm.color.onAccent,
|
|
96
|
-
fontSize: tm.font.control,
|
|
97
|
-
fontWeight: "600",
|
|
98
|
-
letterSpacing: tm.tracking.control,
|
|
99
|
-
},
|
|
100
|
-
});
|