@buoy-gg/time-machine 7.0.22 → 7.0.23
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/TimeMachineBody.js +147 -677
- package/lib/commonjs/components/TimeMachineBody.js.map +1 -1
- package/lib/commonjs/components/TimeMachineModal.js +10 -11
- package/lib/commonjs/components/TimeMachineModal.js.map +1 -1
- package/lib/commonjs/components/confirm.js +39 -0
- package/lib/commonjs/components/confirm.js.map +1 -0
- package/lib/commonjs/components/format.js +24 -0
- package/lib/commonjs/components/format.js.map +1 -0
- package/lib/commonjs/components/list/CaptureBar.js +95 -0
- package/lib/commonjs/components/list/CaptureBar.js.map +1 -0
- package/lib/commonjs/components/list/RestoreOutcomeCard.js +162 -0
- package/lib/commonjs/components/list/RestoreOutcomeCard.js.map +1 -0
- package/lib/commonjs/components/list/SnapshotRow.js +141 -0
- package/lib/commonjs/components/list/SnapshotRow.js.map +1 -0
- package/lib/commonjs/components/list/SourcesStrip.js +126 -0
- package/lib/commonjs/components/list/SourcesStrip.js.map +1 -0
- package/lib/commonjs/components/list/UndoDeleteBar.js +81 -0
- package/lib/commonjs/components/list/UndoDeleteBar.js.map +1 -0
- package/lib/commonjs/components/list/WipeAllRow.js +46 -0
- package/lib/commonjs/components/list/WipeAllRow.js.map +1 -0
- package/lib/commonjs/components/preview/ItemDiffPanel.js +77 -0
- package/lib/commonjs/components/preview/ItemDiffPanel.js.map +1 -0
- package/lib/commonjs/components/preview/PreviewFlow.js +51 -0
- package/lib/commonjs/components/preview/PreviewFlow.js.map +1 -0
- package/lib/commonjs/components/preview/RestorePointNameRow.js +83 -0
- package/lib/commonjs/components/preview/RestorePointNameRow.js.map +1 -0
- package/lib/commonjs/components/preview/RestorePreviewScreen.js +549 -635
- package/lib/commonjs/components/preview/RestorePreviewScreen.js.map +1 -1
- package/lib/commonjs/components/preview/verdictMeta.js +8 -8
- package/lib/commonjs/components/preview/verdictMeta.js.map +1 -1
- package/lib/commonjs/components/sources.js +72 -0
- package/lib/commonjs/components/sources.js.map +1 -0
- package/lib/commonjs/store/snapshotVault.js +93 -9
- package/lib/commonjs/store/snapshotVault.js.map +1 -1
- package/lib/commonjs/store/timeMachineStore.js +125 -6
- package/lib/commonjs/store/timeMachineStore.js.map +1 -1
- package/lib/commonjs/sync/timeMachineSyncAdapter.js +41 -0
- package/lib/commonjs/sync/timeMachineSyncAdapter.js.map +1 -1
- package/lib/commonjs/ui/SwipeActionsRow.js +167 -0
- package/lib/commonjs/ui/SwipeActionsRow.js.map +1 -0
- package/lib/commonjs/ui/primitives.js +164 -0
- package/lib/commonjs/ui/primitives.js.map +1 -0
- package/lib/commonjs/ui/tmTheme.js +103 -0
- package/lib/commonjs/ui/tmTheme.js.map +1 -0
- package/lib/module/components/TimeMachineBody.js +150 -680
- package/lib/module/components/TimeMachineBody.js.map +1 -1
- package/lib/module/components/TimeMachineModal.js +11 -12
- package/lib/module/components/TimeMachineModal.js.map +1 -1
- package/lib/module/components/confirm.js +34 -0
- package/lib/module/components/confirm.js.map +1 -0
- package/lib/module/components/format.js +19 -0
- package/lib/module/components/format.js.map +1 -0
- package/lib/module/components/list/CaptureBar.js +90 -0
- package/lib/module/components/list/CaptureBar.js.map +1 -0
- package/lib/module/components/list/RestoreOutcomeCard.js +156 -0
- package/lib/module/components/list/RestoreOutcomeCard.js.map +1 -0
- package/lib/module/components/list/SnapshotRow.js +138 -0
- package/lib/module/components/list/SnapshotRow.js.map +1 -0
- package/lib/module/components/list/SourcesStrip.js +121 -0
- package/lib/module/components/list/SourcesStrip.js.map +1 -0
- package/lib/module/components/list/UndoDeleteBar.js +78 -0
- package/lib/module/components/list/UndoDeleteBar.js.map +1 -0
- package/lib/module/components/list/WipeAllRow.js +41 -0
- package/lib/module/components/list/WipeAllRow.js.map +1 -0
- package/lib/module/components/preview/ItemDiffPanel.js +72 -0
- package/lib/module/components/preview/ItemDiffPanel.js.map +1 -0
- package/lib/module/components/preview/PreviewFlow.js +46 -0
- package/lib/module/components/preview/PreviewFlow.js.map +1 -0
- package/lib/module/components/preview/RestorePointNameRow.js +78 -0
- package/lib/module/components/preview/RestorePointNameRow.js.map +1 -0
- package/lib/module/components/preview/RestorePreviewScreen.js +552 -638
- package/lib/module/components/preview/RestorePreviewScreen.js.map +1 -1
- package/lib/module/components/preview/verdictMeta.js +9 -8
- package/lib/module/components/preview/verdictMeta.js.map +1 -1
- package/lib/module/components/sources.js +66 -0
- package/lib/module/components/sources.js.map +1 -0
- package/lib/module/store/snapshotVault.js +87 -9
- package/lib/module/store/snapshotVault.js.map +1 -1
- package/lib/module/store/timeMachineStore.js +125 -7
- package/lib/module/store/timeMachineStore.js.map +1 -1
- package/lib/module/sync/timeMachineSyncAdapter.js +41 -0
- package/lib/module/sync/timeMachineSyncAdapter.js.map +1 -1
- package/lib/module/ui/SwipeActionsRow.js +162 -0
- package/lib/module/ui/SwipeActionsRow.js.map +1 -0
- package/lib/module/ui/primitives.js +156 -0
- package/lib/module/ui/primitives.js.map +1 -0
- package/lib/module/ui/tmTheme.js +99 -0
- package/lib/module/ui/tmTheme.js.map +1 -0
- package/lib/typescript/components/TimeMachineBody.d.ts.map +1 -1
- package/lib/typescript/components/TimeMachineModal.d.ts.map +1 -1
- package/lib/typescript/components/confirm.d.ts +2 -0
- package/lib/typescript/components/confirm.d.ts.map +1 -0
- package/lib/typescript/components/format.d.ts +4 -0
- package/lib/typescript/components/format.d.ts.map +1 -0
- package/lib/typescript/components/list/CaptureBar.d.ts +6 -0
- package/lib/typescript/components/list/CaptureBar.d.ts.map +1 -0
- package/lib/typescript/components/list/RestoreOutcomeCard.d.ts +11 -0
- package/lib/typescript/components/list/RestoreOutcomeCard.d.ts.map +1 -0
- package/lib/typescript/components/list/SnapshotRow.d.ts +12 -0
- package/lib/typescript/components/list/SnapshotRow.d.ts.map +1 -0
- package/lib/typescript/components/list/SourcesStrip.d.ts +5 -0
- package/lib/typescript/components/list/SourcesStrip.d.ts.map +1 -0
- package/lib/typescript/components/list/UndoDeleteBar.d.ts +8 -0
- package/lib/typescript/components/list/UndoDeleteBar.d.ts.map +1 -0
- package/lib/typescript/components/list/WipeAllRow.d.ts +5 -0
- package/lib/typescript/components/list/WipeAllRow.d.ts.map +1 -0
- package/lib/typescript/components/preview/ItemDiffPanel.d.ts +5 -0
- package/lib/typescript/components/preview/ItemDiffPanel.d.ts.map +1 -0
- package/lib/typescript/components/preview/PreviewFlow.d.ts +9 -0
- package/lib/typescript/components/preview/PreviewFlow.d.ts.map +1 -0
- package/lib/typescript/components/preview/RestorePointNameRow.d.ts +5 -0
- package/lib/typescript/components/preview/RestorePointNameRow.d.ts.map +1 -0
- package/lib/typescript/components/preview/RestorePreviewScreen.d.ts +1 -5
- package/lib/typescript/components/preview/RestorePreviewScreen.d.ts.map +1 -1
- package/lib/typescript/components/preview/verdictMeta.d.ts +5 -0
- package/lib/typescript/components/preview/verdictMeta.d.ts.map +1 -1
- package/lib/typescript/components/sources.d.ts +36 -0
- package/lib/typescript/components/sources.d.ts.map +1 -0
- package/lib/typescript/store/snapshotVault.d.ts +48 -0
- package/lib/typescript/store/snapshotVault.d.ts.map +1 -1
- package/lib/typescript/store/timeMachineStore.d.ts +35 -1
- package/lib/typescript/store/timeMachineStore.d.ts.map +1 -1
- package/lib/typescript/sync/timeMachineSyncAdapter.d.ts +17 -0
- package/lib/typescript/sync/timeMachineSyncAdapter.d.ts.map +1 -1
- package/lib/typescript/ui/SwipeActionsRow.d.ts +32 -0
- package/lib/typescript/ui/SwipeActionsRow.d.ts.map +1 -0
- package/lib/typescript/ui/primitives.d.ts +57 -0
- package/lib/typescript/ui/primitives.d.ts.map +1 -0
- package/lib/typescript/ui/tmTheme.d.ts +67 -0
- package/lib/typescript/ui/tmTheme.d.ts.map +1 -0
- package/package.json +7 -7
- package/src/components/TimeMachineBody.tsx +183 -817
- package/src/components/TimeMachineModal.tsx +10 -11
- package/src/components/confirm.ts +35 -0
- package/src/components/format.ts +17 -0
- package/src/components/list/CaptureBar.tsx +100 -0
- package/src/components/list/RestoreOutcomeCard.tsx +159 -0
- package/src/components/list/SnapshotRow.tsx +155 -0
- package/src/components/list/SourcesStrip.tsx +124 -0
- package/src/components/list/UndoDeleteBar.tsx +76 -0
- package/src/components/list/WipeAllRow.tsx +42 -0
- package/src/components/preview/ItemDiffPanel.tsx +77 -0
- package/src/components/preview/PreviewFlow.tsx +70 -0
- package/src/components/preview/RestorePointNameRow.tsx +80 -0
- package/src/components/preview/RestorePreviewScreen.tsx +535 -535
- package/src/components/preview/verdictMeta.ts +8 -8
- package/src/components/sources.ts +92 -0
- package/src/store/snapshotVault.ts +131 -9
- package/src/store/timeMachineStore.ts +136 -5
- package/src/sync/timeMachineSyncAdapter.ts +30 -0
- package/src/ui/SwipeActionsRow.tsx +199 -0
- package/src/ui/primitives.tsx +157 -0
- package/src/ui/tmTheme.ts +107 -0
- package/lib/commonjs/components/preview/ItemDetailScreen.js +0 -189
- package/lib/commonjs/components/preview/ItemDetailScreen.js.map +0 -1
- package/lib/module/components/preview/ItemDetailScreen.js +0 -184
- package/lib/module/components/preview/ItemDetailScreen.js.map +0 -1
- package/lib/typescript/components/preview/ItemDetailScreen.d.ts +0 -11
- package/lib/typescript/components/preview/ItemDetailScreen.d.ts.map +0 -1
- package/src/components/preview/ItemDetailScreen.tsx +0 -172
|
@@ -1,296 +1,110 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Time Machine body —
|
|
3
|
-
* the Restore Preview flow (tap a snapshot row → item-level diff of what a
|
|
4
|
-
* restore would change; tap an item → tree/split detail).
|
|
2
|
+
* Time Machine body — the screen router plus the restore-point list.
|
|
5
3
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
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.
|
|
9
13
|
*/
|
|
10
14
|
import { useState } from "react";
|
|
11
|
-
import {
|
|
12
|
-
|
|
13
|
-
Platform,
|
|
14
|
-
ScrollView,
|
|
15
|
-
StyleSheet,
|
|
16
|
-
Text,
|
|
17
|
-
TextInput,
|
|
18
|
-
TouchableOpacity,
|
|
19
|
-
View,
|
|
20
|
-
} from "react-native";
|
|
21
|
-
import {
|
|
22
|
-
Plus,
|
|
23
|
-
EventPickerModal,
|
|
24
|
-
Home,
|
|
25
|
-
Trash2,
|
|
26
|
-
buoyColors,
|
|
27
|
-
} from "@buoy-gg/shared-ui";
|
|
28
|
-
import {
|
|
29
|
-
JotaiIcon,
|
|
30
|
-
QueryIcon,
|
|
31
|
-
ReduxIcon,
|
|
32
|
-
RoutesIcon,
|
|
33
|
-
StorageIcon,
|
|
34
|
-
TimeMachineIcon,
|
|
35
|
-
ZustandIcon,
|
|
36
|
-
} from "@buoy-gg/floating-tools-core";
|
|
37
|
-
import type {
|
|
38
|
-
EventPickerItem,
|
|
39
|
-
StateSnapshotPreviewItem,
|
|
40
|
-
} from "@buoy-gg/shared-ui";
|
|
15
|
+
import { ScrollView, StyleSheet, Text, View } from "react-native";
|
|
16
|
+
import { useSafeAreaInsets, useLimit } from "@buoy-gg/shared-ui";
|
|
41
17
|
import { useToolHost } from "@buoy-gg/core";
|
|
42
18
|
import { useIsPro } from "@buoy-gg/license";
|
|
43
|
-
import {
|
|
19
|
+
import { useTimeMachineState } from "../hooks/useTimeMachine";
|
|
44
20
|
import { timeMachineStore } from "../store/timeMachineStore";
|
|
45
|
-
import type { PreviewSideRef, RestoreOutcome } from "../store/timeMachineStore";
|
|
46
|
-
import { canNavigate, currentRoute } from "../store/routeRestore";
|
|
47
21
|
import type { SnapshotMeta } from "../store/snapshotVault";
|
|
48
|
-
import {
|
|
49
|
-
import {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
route: RoutesIcon,
|
|
63
|
-
};
|
|
64
|
-
|
|
65
|
-
/** Id of the route tile — a device capability, not a snapshot provider. */
|
|
66
|
-
const ROUTE_SOURCE_ID = "route";
|
|
67
|
-
|
|
68
|
-
function timeAgo(ts: number): string {
|
|
69
|
-
const s = Math.max(0, Math.round((Date.now() - ts) / 1000));
|
|
70
|
-
if (s < 60) return `${s}s ago`;
|
|
71
|
-
const m = Math.round(s / 60);
|
|
72
|
-
if (m < 60) return `${m}m ago`;
|
|
73
|
-
const h = Math.round(m / 60);
|
|
74
|
-
if (h < 24) return `${h}h ago`;
|
|
75
|
-
return `${Math.round(h / 24)}d ago`;
|
|
76
|
-
}
|
|
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";
|
|
77
36
|
|
|
78
|
-
|
|
79
|
-
if (bytes < 1024) return `${bytes} B`;
|
|
80
|
-
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
81
|
-
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
82
|
-
}
|
|
37
|
+
type Screen = { kind: "list" } | { kind: "preview" };
|
|
83
38
|
|
|
84
|
-
function
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
);
|
|
89
|
-
}
|
|
39
|
+
export function TimeMachineBody() {
|
|
40
|
+
const [screen, setScreen] = useState<Screen>({ kind: "list" });
|
|
41
|
+
const { snapshots, busy, preview } = useTimeMachineState();
|
|
42
|
+
const sources = useSourceStatuses();
|
|
90
43
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
confirmLabel: string,
|
|
96
|
-
onConfirm: () => void
|
|
97
|
-
) {
|
|
98
|
-
if (Platform.OS === "web") {
|
|
99
|
-
const confirmFn = (globalThis as { confirm?: (msg: string) => boolean }).confirm;
|
|
100
|
-
if (!confirmFn || confirmFn(`${title}\n\n${message}`)) onConfirm();
|
|
101
|
-
return;
|
|
102
|
-
}
|
|
103
|
-
Alert.alert(title, message, [
|
|
104
|
-
{ text: "Cancel", style: "cancel" },
|
|
105
|
-
{ text: confirmLabel, style: "destructive", onPress: onConfirm },
|
|
106
|
-
]);
|
|
107
|
-
}
|
|
44
|
+
const backToList = () => {
|
|
45
|
+
timeMachineStore.clearPreview();
|
|
46
|
+
setScreen({ kind: "list" });
|
|
47
|
+
};
|
|
108
48
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
function confirmRestoreRoute(
|
|
120
|
-
snapshotName: string,
|
|
121
|
-
href: string,
|
|
122
|
-
onChoose: (withRoute: boolean) => void
|
|
123
|
-
) {
|
|
124
|
-
const message = `“${snapshotName}” was captured on ${href}. Go back to that screen too, or restore state only?`;
|
|
125
|
-
if (Platform.OS === "web") {
|
|
126
|
-
const confirmFn = (globalThis as { confirm?: (msg: string) => boolean })
|
|
127
|
-
.confirm;
|
|
128
|
-
onChoose(
|
|
129
|
-
confirmFn
|
|
130
|
-
? confirmFn(`Also return to ${href}?\n\n${message}`)
|
|
131
|
-
: false
|
|
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
|
+
/>
|
|
132
59
|
);
|
|
133
|
-
return;
|
|
134
|
-
}
|
|
135
|
-
Alert.alert("Restore route too?", message, [
|
|
136
|
-
{ text: "Cancel", style: "cancel" },
|
|
137
|
-
{ text: "State only", onPress: () => onChoose(false) },
|
|
138
|
-
{ text: `Go to ${href}`, onPress: () => onChoose(true) },
|
|
139
|
-
]);
|
|
140
|
-
}
|
|
141
60
|
|
|
142
|
-
/**
|
|
143
|
-
* Route leg of a restore: prompt when the snapshot has a captured route we
|
|
144
|
-
* aren't already sitting on, otherwise restore straight away. On Buoy Desktop
|
|
145
|
-
* the device's live route is unknown (no local router), so we always ask.
|
|
146
|
-
*/
|
|
147
|
-
function restoreWithRoutePrompt(
|
|
148
|
-
meta: SnapshotMeta,
|
|
149
|
-
run: (restoreRoute: boolean) => void
|
|
150
|
-
) {
|
|
151
|
-
const route = meta.route;
|
|
152
|
-
if (!route) {
|
|
153
|
-
run(false);
|
|
154
|
-
return;
|
|
155
|
-
}
|
|
156
|
-
if (currentRoute()?.href === route.href) {
|
|
157
|
-
run(false); // already on that screen — nothing to ask
|
|
158
|
-
return;
|
|
159
|
-
}
|
|
160
|
-
confirmRestoreRoute(meta.name, route.href, run);
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
function OutcomePanel({ outcome }: { outcome: RestoreOutcome }) {
|
|
164
|
-
const entries = Object.entries(outcome.results);
|
|
165
61
|
return (
|
|
166
|
-
<
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
onPress={() => timeMachineStore.clearLastOutcome()}
|
|
174
|
-
hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}
|
|
175
|
-
>
|
|
176
|
-
<Text style={styles.outcomeDismiss}>✕</Text>
|
|
177
|
-
</TouchableOpacity>
|
|
178
|
-
</View>
|
|
179
|
-
{entries.length === 0 && (
|
|
180
|
-
<Text style={styles.outcomeLine}>no sources restored</Text>
|
|
181
|
-
)}
|
|
182
|
-
{outcome.route && (
|
|
183
|
-
<Text
|
|
184
|
-
style={[
|
|
185
|
-
styles.outcomeLine,
|
|
186
|
-
(outcome.route.status === "failed" ||
|
|
187
|
-
outcome.route.status === "unavailable") &&
|
|
188
|
-
styles.outcomeLineBad,
|
|
189
|
-
]}
|
|
190
|
-
>
|
|
191
|
-
{outcome.route.status === "navigated"
|
|
192
|
-
? `✓ route — ${outcome.route.href}`
|
|
193
|
-
: outcome.route.status === "deferred"
|
|
194
|
-
? `↻ route — ${outcome.route.href} after reload`
|
|
195
|
-
: `✕ route — ${outcome.route.error ?? outcome.route.status}`}
|
|
196
|
-
</Text>
|
|
197
|
-
)}
|
|
198
|
-
{entries.map(([sourceId, r]) => (
|
|
199
|
-
<View key={sourceId} style={styles.outcomeSource}>
|
|
200
|
-
<Text style={[styles.outcomeLine, !r.ok && styles.outcomeLineBad]}>
|
|
201
|
-
{r.ok ? "✓" : "✕"} {sourceId} — applied {r.applied}
|
|
202
|
-
{r.skipped.length > 0 ? `, skipped ${r.skipped.length}` : ""}
|
|
203
|
-
</Text>
|
|
204
|
-
{r.skipped.slice(0, 3).map((skip) => (
|
|
205
|
-
<Text key={skip.id} style={styles.outcomeDetail}>
|
|
206
|
-
· {skip.id}: {skip.reason}
|
|
207
|
-
</Text>
|
|
208
|
-
))}
|
|
209
|
-
{r.warnings.slice(0, 3).map((warning) => (
|
|
210
|
-
<Text key={warning} style={styles.outcomeDetail}>
|
|
211
|
-
⚠ {warning}
|
|
212
|
-
</Text>
|
|
213
|
-
))}
|
|
214
|
-
</View>
|
|
215
|
-
))}
|
|
216
|
-
</View>
|
|
62
|
+
<RestorePointList
|
|
63
|
+
sources={sources}
|
|
64
|
+
onOpenPreview={(meta) => {
|
|
65
|
+
void timeMachineStore.buildPreview({ snapshotId: meta.id });
|
|
66
|
+
setScreen({ kind: "preview" });
|
|
67
|
+
}}
|
|
68
|
+
/>
|
|
217
69
|
);
|
|
218
70
|
}
|
|
219
71
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
export function TimeMachineBody() {
|
|
231
|
-
const {
|
|
232
|
-
snapshots,
|
|
233
|
-
hasLoaded,
|
|
234
|
-
busy,
|
|
235
|
-
lastOutcome,
|
|
236
|
-
preview,
|
|
237
|
-
remoteProviders,
|
|
238
|
-
remoteRoute,
|
|
239
|
-
} = useTimeMachineState();
|
|
240
|
-
const localProviders = useSnapshotProviders();
|
|
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();
|
|
241
81
|
const isPro = useIsPro();
|
|
82
|
+
const snapshotLimit = useLimit("timeMachine.snapshots");
|
|
242
83
|
const host = useToolHost();
|
|
243
|
-
const
|
|
244
|
-
const [screen, setScreen] = useState<Screen>({ kind: "list" });
|
|
245
|
-
const [picker, setPicker] = useState<"left" | "right" | null>(null);
|
|
84
|
+
const insets = useSafeAreaInsets({ minBottom: 16 });
|
|
246
85
|
|
|
247
|
-
// Header search (when hosted by the core shell) filters the
|
|
248
|
-
const
|
|
249
|
-
const
|
|
250
|
-
? snapshots.filter((s) => s.name.toLowerCase().includes(
|
|
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))
|
|
251
90
|
: snapshots;
|
|
252
91
|
|
|
253
|
-
//
|
|
254
|
-
|
|
255
|
-
const
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
canCapture: routeAvailable,
|
|
270
|
-
canRestore: routeAvailable,
|
|
271
|
-
// No live route in the caption: this screen only re-renders on store
|
|
272
|
-
// changes, so a pathname printed here goes stale the moment you navigate
|
|
273
|
-
// with the tool still open. Each restore point shows the route it holds.
|
|
274
|
-
detail: routeAvailable
|
|
275
|
-
? "recorded with every snapshot — restore can take you back"
|
|
276
|
-
: "no Expo Router detected — snapshots record no route",
|
|
277
|
-
};
|
|
278
|
-
const sourceTiles = [...providers, routeSource];
|
|
279
|
-
|
|
280
|
-
const atFreeLimit = !isPro && snapshots.length >= FREE_SNAPSHOT_LIMIT;
|
|
281
|
-
const captureDisabled = busy !== null || atFreeLimit;
|
|
282
|
-
|
|
283
|
-
const capture = () => {
|
|
284
|
-
if (captureDisabled) return;
|
|
285
|
-
void timeMachineStore.capture(name);
|
|
286
|
-
setName("");
|
|
287
|
-
};
|
|
288
|
-
|
|
289
|
-
const restore = (
|
|
290
|
-
meta: SnapshotMeta,
|
|
291
|
-
mode: "live" | "reload",
|
|
292
|
-
excludeKeys?: string[]
|
|
293
|
-
) => {
|
|
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) => {
|
|
294
108
|
if (busy) return;
|
|
295
109
|
if (meta.baseline) {
|
|
296
110
|
confirmDestructive(
|
|
@@ -301,570 +115,122 @@ export function TimeMachineBody() {
|
|
|
301
115
|
);
|
|
302
116
|
return;
|
|
303
117
|
}
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
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();
|
|
308
122
|
});
|
|
309
123
|
};
|
|
310
124
|
|
|
311
|
-
const remove = (meta: SnapshotMeta) => {
|
|
312
|
-
confirmDestructive(
|
|
313
|
-
"Delete snapshot?",
|
|
314
|
-
`“${meta.name}” cannot be recovered.`,
|
|
315
|
-
"Delete",
|
|
316
|
-
() => void timeMachineStore.delete(meta.id)
|
|
317
|
-
);
|
|
318
|
-
};
|
|
319
|
-
|
|
320
|
-
const wipeAll = () => {
|
|
321
|
-
if (busy) return;
|
|
322
|
-
confirmDestructive(
|
|
323
|
-
"Wipe all app state?",
|
|
324
|
-
"This deletes all app storage and reloads the app — like a fresh install.",
|
|
325
|
-
"Wipe & reload",
|
|
326
|
-
() => void timeMachineStore.wipeAll()
|
|
327
|
-
);
|
|
328
|
-
};
|
|
329
|
-
|
|
330
|
-
const openPreview = (meta: SnapshotMeta) => {
|
|
331
|
-
void timeMachineStore.buildPreview({ snapshotId: meta.id });
|
|
332
|
-
setScreen({ kind: "preview" });
|
|
333
|
-
};
|
|
334
|
-
|
|
335
|
-
const closePreview = () => {
|
|
336
|
-
timeMachineStore.clearPreview();
|
|
337
|
-
setScreen({ kind: "list" });
|
|
338
|
-
};
|
|
339
|
-
|
|
340
|
-
// ── Preview / item screens ──
|
|
341
|
-
if (screen.kind === "item" && preview) {
|
|
342
|
-
return (
|
|
343
|
-
<ItemDetailScreen
|
|
344
|
-
sourceLabel={screen.sourceLabel}
|
|
345
|
-
item={screen.item}
|
|
346
|
-
leftLabel={
|
|
347
|
-
preview.left === "current"
|
|
348
|
-
? "CURRENT"
|
|
349
|
-
: snapshots.find(
|
|
350
|
-
(s) => s.id === (preview.left as { snapshotId: string }).snapshotId
|
|
351
|
-
)?.name ?? "snapshot"
|
|
352
|
-
}
|
|
353
|
-
rightLabel={
|
|
354
|
-
preview.right === "current"
|
|
355
|
-
? "CURRENT"
|
|
356
|
-
: snapshots.find(
|
|
357
|
-
(s) =>
|
|
358
|
-
s.id === (preview.right as { snapshotId: string }).snapshotId
|
|
359
|
-
)?.name ?? "snapshot"
|
|
360
|
-
}
|
|
361
|
-
onBack={() => setScreen({ kind: "preview" })}
|
|
362
|
-
/>
|
|
363
|
-
);
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
if (screen.kind === "preview" && preview) {
|
|
367
|
-
const rightMeta =
|
|
368
|
-
preview.right === "current"
|
|
369
|
-
? undefined
|
|
370
|
-
: snapshots.find(
|
|
371
|
-
(s) => s.id === (preview.right as { snapshotId: string }).snapshotId
|
|
372
|
-
);
|
|
373
|
-
|
|
374
|
-
const pickerItems: EventPickerItem[] = [];
|
|
375
|
-
if (picker) {
|
|
376
|
-
const otherRef = picker === "left" ? preview.right : preview.left;
|
|
377
|
-
const otherId =
|
|
378
|
-
otherRef === "current" ? null : (otherRef as { snapshotId: string }).snapshotId;
|
|
379
|
-
if (picker === "left") {
|
|
380
|
-
pickerItems.push({
|
|
381
|
-
index: 0,
|
|
382
|
-
label: "Current state",
|
|
383
|
-
timestamp: "live",
|
|
384
|
-
relativeTime: "now",
|
|
385
|
-
selected: preview.left === "current",
|
|
386
|
-
});
|
|
387
|
-
}
|
|
388
|
-
snapshots.forEach((s, i) => {
|
|
389
|
-
const ref = { snapshotId: s.id };
|
|
390
|
-
const current = picker === "left" ? preview.left : preview.right;
|
|
391
|
-
pickerItems.push({
|
|
392
|
-
index: picker === "left" ? i + 1 : i,
|
|
393
|
-
label: s.baseline ? `⌂ ${s.name}` : s.name,
|
|
394
|
-
timestamp: new Date(s.createdAt).toLocaleTimeString(),
|
|
395
|
-
relativeTime: timeAgo(s.createdAt),
|
|
396
|
-
disabled: s.id === otherId || (picker === "right" && s.baseline),
|
|
397
|
-
selected:
|
|
398
|
-
current !== "current" &&
|
|
399
|
-
(current as { snapshotId: string }).snapshotId === s.id,
|
|
400
|
-
});
|
|
401
|
-
});
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
const onPickerSelect = (index: number) => {
|
|
405
|
-
const side = picker;
|
|
406
|
-
setPicker(null);
|
|
407
|
-
if (!side) return;
|
|
408
|
-
let ref: PreviewSideRef;
|
|
409
|
-
if (side === "left") {
|
|
410
|
-
ref = index === 0 ? "current" : { snapshotId: snapshots[index - 1].id };
|
|
411
|
-
void timeMachineStore.buildPreview(preview.right, ref);
|
|
412
|
-
} else {
|
|
413
|
-
ref = { snapshotId: snapshots[index].id };
|
|
414
|
-
void timeMachineStore.buildPreview(ref, preview.left);
|
|
415
|
-
}
|
|
416
|
-
};
|
|
417
|
-
|
|
418
|
-
return (
|
|
419
|
-
<View style={styles.screenFill}>
|
|
420
|
-
<RestorePreviewScreen
|
|
421
|
-
preview={preview}
|
|
422
|
-
snapshots={snapshots}
|
|
423
|
-
busy={busy !== null}
|
|
424
|
-
onBack={closePreview}
|
|
425
|
-
onOpenItem={(sourceId, item) => {
|
|
426
|
-
const source = preview.sources.find((s) => s.id === sourceId);
|
|
427
|
-
setScreen({
|
|
428
|
-
kind: "item",
|
|
429
|
-
sourceId,
|
|
430
|
-
sourceLabel: source?.label ?? sourceId,
|
|
431
|
-
item,
|
|
432
|
-
});
|
|
433
|
-
}}
|
|
434
|
-
onRestore={(mode, excludeKeys) => {
|
|
435
|
-
if (!rightMeta || busy) return;
|
|
436
|
-
if (rightMeta.baseline) {
|
|
437
|
-
confirmDestructive(
|
|
438
|
-
"Restore fresh install?",
|
|
439
|
-
"This wipes all app storage and reloads the app.",
|
|
440
|
-
"Wipe & reload",
|
|
441
|
-
() => void timeMachineStore.restore(rightMeta.id, "reload")
|
|
442
|
-
);
|
|
443
|
-
return;
|
|
444
|
-
}
|
|
445
|
-
// Land back on the list, where the outcome panel reports the
|
|
446
|
-
// results — the preview's diff is stale the moment restore runs.
|
|
447
|
-
restoreWithRoutePrompt(rightMeta, (restoreRoute) => {
|
|
448
|
-
void timeMachineStore
|
|
449
|
-
.restore(rightMeta.id, mode, excludeKeys, { restoreRoute })
|
|
450
|
-
.then(() => closePreview());
|
|
451
|
-
});
|
|
452
|
-
}}
|
|
453
|
-
onPickSide={setPicker}
|
|
454
|
-
onRefresh={() =>
|
|
455
|
-
void timeMachineStore.buildPreview(preview.right, preview.left)
|
|
456
|
-
}
|
|
457
|
-
/>
|
|
458
|
-
{picker && (
|
|
459
|
-
<EventPickerModal
|
|
460
|
-
isOpen
|
|
461
|
-
onClose={() => setPicker(null)}
|
|
462
|
-
title={picker === "left" ? "Compare from" : "Compare to"}
|
|
463
|
-
position={picker}
|
|
464
|
-
items={pickerItems}
|
|
465
|
-
onSelect={onPickerSelect}
|
|
466
|
-
/>
|
|
467
|
-
)}
|
|
468
|
-
</View>
|
|
469
|
-
);
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
// ── List screen ──
|
|
473
|
-
const unavailable = sourceTiles.filter(
|
|
474
|
-
(p) => p.detail && (!p.canCapture || !p.canRestore)
|
|
475
|
-
);
|
|
476
|
-
|
|
477
125
|
return (
|
|
478
|
-
<ScrollView
|
|
479
|
-
|
|
126
|
+
<ScrollView
|
|
127
|
+
style={styles.fill}
|
|
128
|
+
contentContainerStyle={[styles.content, { paddingBottom: insets.bottom }]}
|
|
129
|
+
>
|
|
130
|
+
{/* Only when the restore did something the app itself won't show you. */}
|
|
131
|
+
{lastOutcome && restoreNeedsAttention(lastOutcome) ? (
|
|
132
|
+
<RestoreOutcomeCard outcome={lastOutcome} />
|
|
133
|
+
) : null}
|
|
134
|
+
|
|
135
|
+
{recentlyDeleted ? (
|
|
136
|
+
<UndoDeleteBar
|
|
137
|
+
name={recentlyDeleted.stored.meta.name}
|
|
138
|
+
at={recentlyDeleted.at}
|
|
139
|
+
onUndo={() => void timeMachineStore.undoDelete()}
|
|
140
|
+
onExpire={() => timeMachineStore.clearRecentlyDeleted()}
|
|
141
|
+
/>
|
|
142
|
+
) : null}
|
|
480
143
|
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
<View style={styles.captureField}>
|
|
484
|
-
<Plus size={14} color={buoyColors.textMuted} />
|
|
485
|
-
<TextInput
|
|
486
|
-
style={styles.captureInput}
|
|
487
|
-
value={name}
|
|
488
|
-
onChangeText={setName}
|
|
489
|
-
placeholder="Name this restore point…"
|
|
490
|
-
placeholderTextColor={buoyColors.textMuted}
|
|
491
|
-
editable={!captureDisabled}
|
|
492
|
-
onSubmitEditing={capture}
|
|
493
|
-
returnKeyType="done"
|
|
494
|
-
testID="time-machine-name-input"
|
|
495
|
-
/>
|
|
496
|
-
</View>
|
|
497
|
-
<TouchableOpacity
|
|
498
|
-
style={[styles.captureButton, captureDisabled && styles.buttonDisabled]}
|
|
499
|
-
onPress={capture}
|
|
144
|
+
<View>
|
|
145
|
+
<CaptureBar
|
|
500
146
|
disabled={captureDisabled}
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
{busy === "capture" ? "…" : "CAPTURE"}
|
|
505
|
-
</Text>
|
|
506
|
-
</TouchableOpacity>
|
|
507
|
-
</View>
|
|
508
|
-
{atFreeLimit && (
|
|
509
|
-
<Text style={styles.freeLimitNote}>
|
|
510
|
-
Free plan: {FREE_SNAPSHOT_LIMIT} restore points. Upgrade to Pro for
|
|
511
|
-
unlimited snapshots.
|
|
512
|
-
</Text>
|
|
513
|
-
)}
|
|
514
|
-
|
|
515
|
-
{/* Sources — detection at a glance */}
|
|
516
|
-
<View style={styles.sectionRow}>
|
|
517
|
-
<Text style={styles.sectionTitle}>Sources</Text>
|
|
518
|
-
<View style={styles.sourceTiles}>
|
|
519
|
-
{sourceTiles.map((p) => {
|
|
520
|
-
const IconComp = SOURCE_ICONS[p.id] ?? TimeMachineIcon;
|
|
521
|
-
const dotColor = !p.canCapture
|
|
522
|
-
? buoyColors.textMuted
|
|
523
|
-
: p.canRestore
|
|
524
|
-
? buoyColors.success
|
|
525
|
-
: buoyColors.warning;
|
|
526
|
-
return (
|
|
527
|
-
<View
|
|
528
|
-
key={p.id}
|
|
529
|
-
style={styles.sourceTile}
|
|
530
|
-
testID={`time-machine-source-${p.id}`}
|
|
531
|
-
accessibilityLabel={`${p.label}: ${
|
|
532
|
-
p.canCapture
|
|
533
|
-
? p.canRestore
|
|
534
|
-
? "capture and restore"
|
|
535
|
-
: "capture only"
|
|
536
|
-
: "unavailable"
|
|
537
|
-
}`}
|
|
538
|
-
>
|
|
539
|
-
<IconComp size={16} />
|
|
540
|
-
<View
|
|
541
|
-
style={[styles.sourceTileDot, { backgroundColor: dotColor }]}
|
|
542
|
-
/>
|
|
543
|
-
</View>
|
|
544
|
-
);
|
|
545
|
-
})}
|
|
546
|
-
{providers.length === 0 && !routeAvailable && (
|
|
547
|
-
<Text style={styles.emptyText}>none registered</Text>
|
|
548
|
-
)}
|
|
549
|
-
</View>
|
|
147
|
+
busy={busy === "capture"}
|
|
148
|
+
onCapture={(name) => void timeMachineStore.capture(name)}
|
|
149
|
+
/>
|
|
550
150
|
</View>
|
|
551
|
-
{
|
|
552
|
-
<
|
|
553
|
-
{
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
{visibleSnapshots.map((meta) => {
|
|
574
|
-
const warnings = warningCount(meta);
|
|
575
|
-
return (
|
|
576
|
-
<TouchableOpacity
|
|
151
|
+
{atCaptureLimit ? (
|
|
152
|
+
<Footnote tone="warning">
|
|
153
|
+
Free plan keeps {snapshotLimit} restore points. Upgrade to Pro
|
|
154
|
+
for unlimited snapshots.
|
|
155
|
+
</Footnote>
|
|
156
|
+
) : null}
|
|
157
|
+
|
|
158
|
+
<SectionLabel>Sources</SectionLabel>
|
|
159
|
+
<SourcesStrip sources={sources} />
|
|
160
|
+
{/* A source that can't fully participate says why, right where its
|
|
161
|
+
tile is — the strip only has room for the dot. */}
|
|
162
|
+
{sources
|
|
163
|
+
.filter((s) => s.detail && !(s.canCapture && s.canRestore))
|
|
164
|
+
.map((s) => (
|
|
165
|
+
<Footnote key={s.id}>
|
|
166
|
+
{s.label}: {s.detail}
|
|
167
|
+
</Footnote>
|
|
168
|
+
))}
|
|
169
|
+
<SectionLabel>Restore Points</SectionLabel>
|
|
170
|
+
<Group>
|
|
171
|
+
{visible.map((meta) => (
|
|
172
|
+
<SnapshotRow
|
|
577
173
|
key={meta.id}
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
</TouchableOpacity>
|
|
617
|
-
) : (
|
|
618
|
-
<TouchableOpacity
|
|
619
|
-
style={[styles.rowAction, busy && styles.buttonDisabled]}
|
|
620
|
-
onPress={() => restore(meta, "reload")}
|
|
621
|
-
disabled={busy !== null}
|
|
622
|
-
accessibilityLabel="Wipe and reload"
|
|
623
|
-
>
|
|
624
|
-
<Text style={styles.rowActionLabelDanger}>WIPE</Text>
|
|
625
|
-
</TouchableOpacity>
|
|
626
|
-
)}
|
|
627
|
-
<TouchableOpacity
|
|
628
|
-
style={styles.rowAction}
|
|
629
|
-
onPress={() => remove(meta)}
|
|
630
|
-
hitSlop={{ top: 8, bottom: 8, left: 4, right: 8 }}
|
|
631
|
-
accessibilityLabel="Delete"
|
|
632
|
-
>
|
|
633
|
-
<Trash2 size={14} color={buoyColors.error} />
|
|
634
|
-
</TouchableOpacity>
|
|
635
|
-
</TouchableOpacity>
|
|
636
|
-
);
|
|
637
|
-
})}
|
|
638
|
-
|
|
639
|
-
{/* Destructive utility — deliberately last and understated. */}
|
|
640
|
-
<TouchableOpacity
|
|
641
|
-
style={[styles.wipeAllButton, busy && styles.buttonDisabled]}
|
|
642
|
-
onPress={wipeAll}
|
|
643
|
-
disabled={busy !== null}
|
|
644
|
-
testID="time-machine-wipe-all"
|
|
645
|
-
>
|
|
646
|
-
<Trash2 size={13} color={buoyColors.error} />
|
|
647
|
-
<Text style={styles.wipeAllText}>Wipe all app state…</Text>
|
|
648
|
-
</TouchableOpacity>
|
|
649
|
-
<Text style={styles.wipeAllCaption}>
|
|
174
|
+
meta={meta}
|
|
175
|
+
busy={busy !== null}
|
|
176
|
+
onOpen={() => onOpenPreview(meta)}
|
|
177
|
+
onRestore={() => restore(meta)}
|
|
178
|
+
onDelete={() => void timeMachineStore.delete(meta.id)}
|
|
179
|
+
onDuplicate={() => void timeMachineStore.duplicate(meta.id)}
|
|
180
|
+
/>
|
|
181
|
+
))}
|
|
182
|
+
{visible.length === 0 ? (
|
|
183
|
+
<Text style={styles.empty}>
|
|
184
|
+
{!hasLoaded
|
|
185
|
+
? "Loading restore points…"
|
|
186
|
+
: snapshots.length > 0
|
|
187
|
+
? "No restore point matches that name."
|
|
188
|
+
: "Set up the state you want to come back to, then capture it."}
|
|
189
|
+
</Text>
|
|
190
|
+
) : null}
|
|
191
|
+
</Group>
|
|
192
|
+
{visible.length > 0 ? (
|
|
193
|
+
<Footnote>Swipe a restore point to rename, copy or delete it.</Footnote>
|
|
194
|
+
) : null}
|
|
195
|
+
|
|
196
|
+
<View style={styles.spacer} />
|
|
197
|
+
|
|
198
|
+
<Group>
|
|
199
|
+
<WipeAllRow
|
|
200
|
+
disabled={busy !== null}
|
|
201
|
+
onWipe={() =>
|
|
202
|
+
confirmDestructive(
|
|
203
|
+
"Wipe all app state?",
|
|
204
|
+
"This deletes all app storage and reloads the app — like a fresh install.",
|
|
205
|
+
"Wipe & reload",
|
|
206
|
+
() => void timeMachineStore.wipeAll()
|
|
207
|
+
)
|
|
208
|
+
}
|
|
209
|
+
/>
|
|
210
|
+
</Group>
|
|
211
|
+
<Footnote centered>
|
|
650
212
|
Deletes all app storage and reloads — like a fresh install.
|
|
651
|
-
</
|
|
213
|
+
</Footnote>
|
|
652
214
|
</ScrollView>
|
|
653
215
|
);
|
|
654
216
|
}
|
|
655
217
|
|
|
656
218
|
const styles = StyleSheet.create({
|
|
657
|
-
|
|
658
|
-
flex: 1,
|
|
659
|
-
},
|
|
660
|
-
container: {
|
|
661
|
-
padding: 12,
|
|
662
|
-
gap: 8,
|
|
663
|
-
},
|
|
664
|
-
captureCard: {
|
|
665
|
-
flexDirection: "row",
|
|
666
|
-
alignItems: "center",
|
|
667
|
-
gap: 8,
|
|
668
|
-
},
|
|
669
|
-
captureField: {
|
|
670
|
-
flex: 1,
|
|
671
|
-
flexDirection: "row",
|
|
672
|
-
alignItems: "center",
|
|
673
|
-
gap: 8,
|
|
674
|
-
backgroundColor: buoyColors.card,
|
|
675
|
-
borderColor: buoyColors.border,
|
|
676
|
-
borderWidth: 1,
|
|
677
|
-
borderRadius: 10,
|
|
678
|
-
paddingHorizontal: 10,
|
|
679
|
-
},
|
|
680
|
-
captureInput: {
|
|
681
|
-
flex: 1,
|
|
682
|
-
color: buoyColors.text,
|
|
683
|
-
fontSize: 13,
|
|
684
|
-
paddingVertical: 9,
|
|
685
|
-
},
|
|
686
|
-
captureButton: {
|
|
687
|
-
backgroundColor: buoyColors.primary,
|
|
688
|
-
borderRadius: 10,
|
|
689
|
-
paddingHorizontal: 16,
|
|
690
|
-
paddingVertical: 10,
|
|
691
|
-
justifyContent: "center",
|
|
692
|
-
},
|
|
693
|
-
captureButtonText: {
|
|
694
|
-
color: "#0A0A0A",
|
|
695
|
-
fontSize: 12,
|
|
696
|
-
fontWeight: "700",
|
|
697
|
-
letterSpacing: 0.5,
|
|
698
|
-
},
|
|
699
|
-
buttonDisabled: {
|
|
700
|
-
opacity: 0.4,
|
|
701
|
-
},
|
|
702
|
-
freeLimitNote: {
|
|
703
|
-
color: buoyColors.warning,
|
|
704
|
-
fontSize: 11,
|
|
705
|
-
},
|
|
706
|
-
sectionRow: {
|
|
707
|
-
flexDirection: "row",
|
|
708
|
-
alignItems: "center",
|
|
709
|
-
justifyContent: "space-between",
|
|
710
|
-
marginTop: 12,
|
|
711
|
-
gap: 10,
|
|
712
|
-
},
|
|
713
|
-
sectionTitle: {
|
|
714
|
-
color: buoyColors.text,
|
|
715
|
-
fontSize: 14,
|
|
716
|
-
fontWeight: "700",
|
|
717
|
-
},
|
|
718
|
-
sectionSpacing: {
|
|
719
|
-
marginTop: 14,
|
|
720
|
-
},
|
|
721
|
-
sourceTiles: {
|
|
722
|
-
flexDirection: "row",
|
|
723
|
-
gap: 6,
|
|
724
|
-
flexWrap: "wrap",
|
|
725
|
-
justifyContent: "flex-end",
|
|
726
|
-
flex: 1,
|
|
727
|
-
},
|
|
728
|
-
sourceTile: {
|
|
729
|
-
width: 32,
|
|
730
|
-
height: 32,
|
|
731
|
-
borderRadius: 9,
|
|
732
|
-
backgroundColor: buoyColors.card,
|
|
733
|
-
borderColor: buoyColors.border,
|
|
734
|
-
borderWidth: 1,
|
|
735
|
-
alignItems: "center",
|
|
736
|
-
justifyContent: "center",
|
|
737
|
-
},
|
|
738
|
-
sourceTileDot: {
|
|
739
|
-
position: "absolute",
|
|
740
|
-
top: 3,
|
|
741
|
-
right: 3,
|
|
742
|
-
width: 6,
|
|
743
|
-
height: 6,
|
|
744
|
-
borderRadius: 3,
|
|
745
|
-
},
|
|
746
|
-
providerDetail: {
|
|
747
|
-
color: buoyColors.textMuted,
|
|
748
|
-
fontSize: 10,
|
|
749
|
-
marginTop: 4,
|
|
750
|
-
},
|
|
751
|
-
emptyText: {
|
|
752
|
-
color: buoyColors.textMuted,
|
|
753
|
-
fontSize: 12,
|
|
754
|
-
marginTop: 8,
|
|
755
|
-
},
|
|
756
|
-
snapshotCard: {
|
|
757
|
-
flexDirection: "row",
|
|
758
|
-
alignItems: "center",
|
|
759
|
-
backgroundColor: buoyColors.card,
|
|
760
|
-
borderColor: buoyColors.border,
|
|
761
|
-
borderWidth: 1,
|
|
762
|
-
borderRadius: 12,
|
|
763
|
-
paddingHorizontal: 12,
|
|
764
|
-
paddingVertical: 10,
|
|
765
|
-
marginTop: 8,
|
|
766
|
-
gap: 10,
|
|
767
|
-
},
|
|
768
|
-
snapshotIconTile: {
|
|
769
|
-
width: 36,
|
|
770
|
-
height: 36,
|
|
771
|
-
borderRadius: 10,
|
|
772
|
-
backgroundColor: buoyColors.hover,
|
|
773
|
-
alignItems: "center",
|
|
774
|
-
justifyContent: "center",
|
|
775
|
-
},
|
|
776
|
-
snapshotInfo: {
|
|
219
|
+
fill: {
|
|
777
220
|
flex: 1,
|
|
778
|
-
|
|
779
|
-
gap: 2,
|
|
780
|
-
},
|
|
781
|
-
snapshotName: {
|
|
782
|
-
color: buoyColors.text,
|
|
783
|
-
fontSize: 14,
|
|
784
|
-
fontWeight: "700",
|
|
785
|
-
},
|
|
786
|
-
snapshotMeta: {
|
|
787
|
-
color: buoyColors.textMuted,
|
|
788
|
-
fontSize: 10,
|
|
789
|
-
fontVariant: ["tabular-nums"],
|
|
790
|
-
},
|
|
791
|
-
snapshotRoute: {
|
|
792
|
-
color: buoyColors.textSecondary,
|
|
793
|
-
fontSize: 10,
|
|
794
|
-
},
|
|
795
|
-
rowAction: {
|
|
796
|
-
padding: 4,
|
|
797
|
-
justifyContent: "center",
|
|
798
|
-
alignItems: "center",
|
|
799
|
-
},
|
|
800
|
-
rowActionLabel: {
|
|
801
|
-
color: buoyColors.primary,
|
|
802
|
-
fontSize: 12,
|
|
803
|
-
fontWeight: "600",
|
|
804
|
-
},
|
|
805
|
-
rowActionLabelDanger: {
|
|
806
|
-
color: buoyColors.error,
|
|
807
|
-
fontSize: 12,
|
|
808
|
-
fontWeight: "600",
|
|
809
|
-
},
|
|
810
|
-
wipeAllButton: {
|
|
811
|
-
flexDirection: "row",
|
|
812
|
-
alignItems: "center",
|
|
813
|
-
justifyContent: "center",
|
|
814
|
-
gap: 7,
|
|
815
|
-
borderColor: `${buoyColors.error}55`,
|
|
816
|
-
borderWidth: 1,
|
|
817
|
-
borderRadius: 10,
|
|
818
|
-
paddingVertical: 10,
|
|
819
|
-
marginTop: 18,
|
|
820
|
-
},
|
|
821
|
-
wipeAllText: {
|
|
822
|
-
color: buoyColors.error,
|
|
823
|
-
fontSize: 12.5,
|
|
824
|
-
fontWeight: "600",
|
|
825
|
-
},
|
|
826
|
-
wipeAllCaption: {
|
|
827
|
-
color: buoyColors.textMuted,
|
|
828
|
-
fontSize: 10,
|
|
829
|
-
textAlign: "center",
|
|
830
|
-
marginTop: 6,
|
|
831
|
-
},
|
|
832
|
-
outcomePanel: {
|
|
833
|
-
backgroundColor: buoyColors.card,
|
|
834
|
-
borderColor: buoyColors.borderStrong,
|
|
835
|
-
borderWidth: 1,
|
|
836
|
-
borderRadius: 8,
|
|
837
|
-
padding: 10,
|
|
838
|
-
gap: 4,
|
|
839
|
-
},
|
|
840
|
-
outcomeHeaderRow: {
|
|
841
|
-
flexDirection: "row",
|
|
842
|
-
justifyContent: "space-between",
|
|
843
|
-
alignItems: "center",
|
|
844
|
-
},
|
|
845
|
-
outcomeTitle: {
|
|
846
|
-
color: buoyColors.text,
|
|
847
|
-
fontSize: 12,
|
|
848
|
-
fontWeight: "700",
|
|
849
|
-
flex: 1,
|
|
850
|
-
},
|
|
851
|
-
outcomeDismiss: {
|
|
852
|
-
color: buoyColors.textMuted,
|
|
853
|
-
fontSize: 13,
|
|
854
|
-
},
|
|
855
|
-
outcomeSource: {
|
|
856
|
-
gap: 2,
|
|
221
|
+
backgroundColor: tm.color.sheet,
|
|
857
222
|
},
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
fontSize: 11,
|
|
223
|
+
content: {
|
|
224
|
+
paddingTop: 14,
|
|
861
225
|
},
|
|
862
|
-
|
|
863
|
-
color:
|
|
226
|
+
empty: {
|
|
227
|
+
color: tm.color.labelSecondary,
|
|
228
|
+
fontSize: tm.font.footnote,
|
|
229
|
+
lineHeight: tm.font.footnote * 1.4,
|
|
230
|
+
paddingHorizontal: tm.textInset,
|
|
231
|
+
paddingVertical: 16,
|
|
864
232
|
},
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
fontSize: 10,
|
|
868
|
-
paddingLeft: 12,
|
|
233
|
+
spacer: {
|
|
234
|
+
height: 24,
|
|
869
235
|
},
|
|
870
236
|
});
|