@rightkit/system-ui 0.0.0-stage → 0.1.0
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/CHANGELOG.md +5 -0
- package/LICENSE-APACHE +201 -0
- package/LICENSE-MIT +21 -0
- package/README.md +36 -2
- package/dist/activityLease.d.ts +1 -0
- package/dist/activityLease.js +20 -0
- package/dist/api.d.ts +1011 -0
- package/dist/api.js +296 -0
- package/dist/chart.d.ts +14 -0
- package/dist/chart.js +94 -0
- package/dist/components/Chart.d.ts +40 -0
- package/dist/components/Chart.js +109 -0
- package/dist/env.d.ts +60 -0
- package/dist/index.d.ts +26 -0
- package/dist/index.js +28 -0
- package/dist/kept.d.ts +3 -0
- package/dist/kept.js +21 -0
- package/dist/lib/chatKind.d.ts +4 -0
- package/dist/lib/chatKind.js +15 -0
- package/dist/lib/legal.d.ts +26 -0
- package/dist/lib/legal.js +39 -0
- package/dist/lib/onboardingSettings.d.ts +2 -0
- package/dist/lib/onboardingSettings.js +17 -0
- package/dist/lib/qr.d.ts +9 -0
- package/dist/lib/qr.js +286 -0
- package/dist/lib/rk.d.ts +6 -0
- package/dist/lib/rk.js +30 -0
- package/dist/metrics.d.ts +24 -0
- package/dist/metrics.js +76 -0
- package/dist/onboarding/flow.d.ts +31 -0
- package/dist/onboarding/flow.js +81 -0
- package/dist/onboarding/gate.d.ts +8 -0
- package/dist/onboarding/gate.js +52 -0
- package/dist/styles.css +8 -0
- package/dist/views/Activity.d.ts +3 -0
- package/dist/views/Activity.js +111 -0
- package/dist/views/Apps.d.ts +1 -0
- package/dist/views/Apps.js +478 -0
- package/dist/views/Chat.d.ts +1 -0
- package/dist/views/Chat.js +255 -0
- package/dist/views/ChromeSnapshots.d.ts +6 -0
- package/dist/views/ChromeSnapshots.js +19 -0
- package/dist/views/Cleanup.d.ts +4 -0
- package/dist/views/Cleanup.js +291 -0
- package/dist/views/DriveHealth.d.ts +14 -0
- package/dist/views/DriveHealth.js +81 -0
- package/dist/views/Duplicates.d.ts +2 -0
- package/dist/views/Duplicates.js +99 -0
- package/dist/views/LauncherSettings.d.ts +63 -0
- package/dist/views/LauncherSettings.js +242 -0
- package/dist/views/NearbySettings.d.ts +117 -0
- package/dist/views/NearbySettings.js +237 -0
- package/dist/views/Storage.d.ts +9 -0
- package/dist/views/Storage.js +916 -0
- package/dist/views/activity.css +94 -0
- package/dist/views/apps.css +98 -0
- package/dist/views/chat.css +61 -0
- package/dist/views/cleanup.css +49 -0
- package/dist/views/health.css +17 -0
- package/dist/views/membrane.css +23 -0
- package/dist/views/monitor.css +99 -0
- package/dist/views/settings.css +176 -0
- package/dist/views/settingsKit.d.ts +14 -0
- package/dist/views/settingsKit.js +43 -0
- package/dist/views/storage.css +121 -0
- package/dist/views/storageEstimate.d.ts +6 -0
- package/dist/views/storageEstimate.js +7 -0
- package/dist/visible.d.ts +3 -0
- package/dist/visible.js +42 -0
- package/package.json +110 -3
|
@@ -0,0 +1,478 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useMemo, useRef, useState } from "react";
|
|
3
|
+
import { Button, Checkbox, ConfirmDialog, Notice, SegmentedControl, Input } from "@rightkit/react-shell/react";
|
|
4
|
+
import { invoke } from "@tauri-apps/api/core";
|
|
5
|
+
import { listen } from "@tauri-apps/api/event";
|
|
6
|
+
import { AppWindow, ChevronLeft, Search } from "lucide-react";
|
|
7
|
+
import { ago, api, appsApi, bytes, isWindows, } from "../api.js";
|
|
8
|
+
import { readKept, writeKept } from "../kept.js";
|
|
9
|
+
import { upTo } from "./storageEstimate.js";
|
|
10
|
+
const DAY = 86_400;
|
|
11
|
+
const UNUSED_DAYS = 90;
|
|
12
|
+
/** The app whose detail was open when the hub closed; the web view is destroyed on window close. */
|
|
13
|
+
const KEPT_APP = "pulse.hub.apps.open";
|
|
14
|
+
/** The open app's ticked uninstall items, kept across a hub close (#302): `{ app, picked, touched }`. */
|
|
15
|
+
const KEPT_PICKED = "pulse.hub.apps.picked";
|
|
16
|
+
/** Uninstalls that left failed paths, kept after Back and across a hub close until dismissed. */
|
|
17
|
+
const KEPT_PENDING = "pulse.hub.apps.pending";
|
|
18
|
+
function lastUsedText(epoch) {
|
|
19
|
+
if (epoch == null)
|
|
20
|
+
return "Unknown";
|
|
21
|
+
const days = Math.floor((Date.now() / 1000 - epoch) / DAY);
|
|
22
|
+
if (days <= 0)
|
|
23
|
+
return "Today";
|
|
24
|
+
if (days === 1)
|
|
25
|
+
return "Yesterday";
|
|
26
|
+
if (days < 60)
|
|
27
|
+
return `${days} days ago`;
|
|
28
|
+
return `${Math.floor(days / 30)} months ago`;
|
|
29
|
+
}
|
|
30
|
+
function byPath(apps) {
|
|
31
|
+
return Object.fromEntries(apps.map((a) => [a.path, a]));
|
|
32
|
+
}
|
|
33
|
+
function upsertApp(list, row) {
|
|
34
|
+
const index = list.findIndex((a) => a.path === row.path);
|
|
35
|
+
if (index < 0)
|
|
36
|
+
return [...list, row];
|
|
37
|
+
const next = list.slice();
|
|
38
|
+
next[index] = row;
|
|
39
|
+
return next;
|
|
40
|
+
}
|
|
41
|
+
export function Apps() {
|
|
42
|
+
const [apps, setApps] = useState([]);
|
|
43
|
+
const [loaded, setLoaded] = useState(false);
|
|
44
|
+
const [refreshing, setRefreshing] = useState(false);
|
|
45
|
+
const [updates, setUpdates] = useState({});
|
|
46
|
+
const [updatesAt, setUpdatesAt] = useState(null);
|
|
47
|
+
const [checking, setChecking] = useState(false);
|
|
48
|
+
const [icons, setIcons] = useState({});
|
|
49
|
+
const [jobs, setJobs] = useState({});
|
|
50
|
+
const [query, setQuery] = useState("");
|
|
51
|
+
const [filter, setFilter] = useState("all");
|
|
52
|
+
const [open, setOpen] = useState(null);
|
|
53
|
+
const [error, setError] = useState(null);
|
|
54
|
+
// Failed leftovers stay listed here after Back: the app's own detail is gone once it is left.
|
|
55
|
+
const [pending, setPending] = useState(() => readKept(KEPT_PENDING) ?? []);
|
|
56
|
+
const iconsAsked = useRef(new Set());
|
|
57
|
+
// Per-app leftover choices survive Back and reopening.
|
|
58
|
+
const retained = useRef(new Map());
|
|
59
|
+
/** Opening or closing an app is kept, so reopening the hub returns to that app. */
|
|
60
|
+
const openApp = (entry) => {
|
|
61
|
+
setOpen(entry);
|
|
62
|
+
writeKept(KEPT_APP, entry?.path ?? null);
|
|
63
|
+
};
|
|
64
|
+
const refreshList = () => {
|
|
65
|
+
setRefreshing(true);
|
|
66
|
+
setError(null);
|
|
67
|
+
appsApi.refresh().catch((e) => {
|
|
68
|
+
setRefreshing(false);
|
|
69
|
+
setError(String(e));
|
|
70
|
+
});
|
|
71
|
+
};
|
|
72
|
+
const checkUpdates = (force) => {
|
|
73
|
+
setChecking(true);
|
|
74
|
+
appsApi.updatesRefresh(force).catch((e) => {
|
|
75
|
+
setChecking(false);
|
|
76
|
+
setError(String(e));
|
|
77
|
+
});
|
|
78
|
+
};
|
|
79
|
+
const setJob = (path, job) => setJobs((prev) => ({ ...prev, [path]: job }));
|
|
80
|
+
useEffect(() => {
|
|
81
|
+
writeKept(KEPT_PENDING, pending.length ? pending : null);
|
|
82
|
+
}, [pending]);
|
|
83
|
+
const keepFailed = (entry, result) => {
|
|
84
|
+
if (result.failed.length === 0)
|
|
85
|
+
return;
|
|
86
|
+
setPending((prev) => [
|
|
87
|
+
...prev.filter((p) => p.path !== entry.path),
|
|
88
|
+
{ path: entry.path, name: entry.name, result },
|
|
89
|
+
]);
|
|
90
|
+
};
|
|
91
|
+
const startUpdate = (path) => {
|
|
92
|
+
setJob(path, { path, state: "running", message: "Starting…" });
|
|
93
|
+
appsApi
|
|
94
|
+
.update(path)
|
|
95
|
+
.then((outcome) => {
|
|
96
|
+
if (outcome === "running")
|
|
97
|
+
return;
|
|
98
|
+
setJob(path, {
|
|
99
|
+
path,
|
|
100
|
+
state: "done",
|
|
101
|
+
message: outcome === "store" ? "Opened in the App Store." : "Opened. Use the app's own updater.",
|
|
102
|
+
});
|
|
103
|
+
})
|
|
104
|
+
.catch((e) => setJob(path, { path, state: "failed", message: String(e) }));
|
|
105
|
+
};
|
|
106
|
+
// Listeners first, then the saved data, then the background refreshes.
|
|
107
|
+
useEffect(() => {
|
|
108
|
+
const subs = [
|
|
109
|
+
listen("apps-row", (e) => setApps((prev) => upsertApp(prev, e.payload))),
|
|
110
|
+
listen("apps-inventory", (e) => {
|
|
111
|
+
setApps(e.payload);
|
|
112
|
+
setRefreshing(false);
|
|
113
|
+
setLoaded(true);
|
|
114
|
+
}),
|
|
115
|
+
listen("apps-update-row", (e) => setUpdates((prev) => ({ ...prev, [e.payload.path]: e.payload }))),
|
|
116
|
+
listen("apps-updates-done", (e) => {
|
|
117
|
+
setUpdates(byPath(e.payload.apps));
|
|
118
|
+
setUpdatesAt(e.payload.checked_at);
|
|
119
|
+
setChecking(false);
|
|
120
|
+
}),
|
|
121
|
+
listen("apps-icon", (e) => setIcons((prev) => ({ ...prev, [e.payload.path]: e.payload.data_url }))),
|
|
122
|
+
listen("apps-update-job", (e) => {
|
|
123
|
+
setJob(e.payload.path, e.payload);
|
|
124
|
+
// Homebrew changed an app: check again so its row reflects the new version.
|
|
125
|
+
if (e.payload.state === "done")
|
|
126
|
+
checkUpdates(false);
|
|
127
|
+
}),
|
|
128
|
+
];
|
|
129
|
+
let alive = true;
|
|
130
|
+
Promise.all(subs).then(async () => {
|
|
131
|
+
const saved = await appsApi.cached().catch(() => null);
|
|
132
|
+
if (alive && saved && saved.apps.length > 0)
|
|
133
|
+
setApps(saved.apps);
|
|
134
|
+
if (alive)
|
|
135
|
+
refreshList();
|
|
136
|
+
if (alive)
|
|
137
|
+
setLoaded(true);
|
|
138
|
+
const savedUpdates = await appsApi.updatesCached().catch(() => null);
|
|
139
|
+
if (alive && savedUpdates) {
|
|
140
|
+
setUpdates(byPath(savedUpdates.apps));
|
|
141
|
+
setUpdatesAt(savedUpdates.checked_at);
|
|
142
|
+
}
|
|
143
|
+
if (alive)
|
|
144
|
+
checkUpdates(false);
|
|
145
|
+
// `--app <path>` opens straight to that app's detail; otherwise the app that was open last.
|
|
146
|
+
const first = await invoke("initial_app").catch(() => null);
|
|
147
|
+
const target = first ?? readKept(KEPT_APP);
|
|
148
|
+
if (alive && target) {
|
|
149
|
+
const entry = await appsApi.summary(target).catch((e) => {
|
|
150
|
+
if (first)
|
|
151
|
+
setError(String(e));
|
|
152
|
+
else
|
|
153
|
+
writeKept(KEPT_APP, null); // The kept app is gone (uninstalled or moved): show the list.
|
|
154
|
+
return null;
|
|
155
|
+
});
|
|
156
|
+
if (alive && entry)
|
|
157
|
+
openApp(entry);
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
return () => {
|
|
161
|
+
alive = false;
|
|
162
|
+
subs.forEach((p) => p.then((off) => off()));
|
|
163
|
+
};
|
|
164
|
+
}, []);
|
|
165
|
+
// Icons: ask once per app; cached ones come back now, the rest as events.
|
|
166
|
+
useEffect(() => {
|
|
167
|
+
const wanted = apps.map((a) => a.path).filter((p) => !iconsAsked.current.has(p));
|
|
168
|
+
if (wanted.length === 0)
|
|
169
|
+
return;
|
|
170
|
+
wanted.forEach((p) => iconsAsked.current.add(p));
|
|
171
|
+
appsApi
|
|
172
|
+
.icons(wanted)
|
|
173
|
+
.then((ready) => setIcons((prev) => ({ ...prev, ...ready })))
|
|
174
|
+
.catch(() => { });
|
|
175
|
+
}, [apps]);
|
|
176
|
+
const shown = useMemo(() => {
|
|
177
|
+
const now = Date.now() / 1000;
|
|
178
|
+
const q = query.trim().toLowerCase();
|
|
179
|
+
return apps
|
|
180
|
+
.filter((a) => (!q || a.name.toLowerCase().includes(q) || (a.bundle_id ?? "").toLowerCase().includes(q)) &&
|
|
181
|
+
// Unknown last-used is never treated as unused.
|
|
182
|
+
(filter !== "unused" || (a.last_used != null && now - a.last_used >= UNUSED_DAYS * DAY)) &&
|
|
183
|
+
(filter !== "updates" || updates[a.path]?.state === "available"))
|
|
184
|
+
.sort((a, b) => b.size_bytes - a.size_bytes);
|
|
185
|
+
}, [apps, query, filter, updates]);
|
|
186
|
+
const updateCount = useMemo(() => apps.filter((a) => updates[a.path]?.state === "available").length, [apps, updates]);
|
|
187
|
+
if (open) {
|
|
188
|
+
return (_jsx(Detail, { app: open, update: updates[open.path], job: jobs[open.path], retained: retained.current, onStartUpdate: () => startUpdate(open.path), onUninstalled: (r) => keepFailed(open, r), onBack: () => {
|
|
189
|
+
openApp(null);
|
|
190
|
+
refreshList();
|
|
191
|
+
} }, open.path));
|
|
192
|
+
}
|
|
193
|
+
const largest = Math.max(1, ...shown.map((a) => a.size_bytes));
|
|
194
|
+
const filtered = query.trim() !== "" || filter !== "all";
|
|
195
|
+
return (_jsxs("div", { className: "view ck-apps", children: [_jsxs("div", { className: "ck-apps-tools", children: [_jsxs("label", { className: "ck-apps-search", children: [_jsx(Search, { size: 15, strokeWidth: 1.75, "aria-hidden": "true" }), _jsx(Input, { type: "search", "aria-label": "Search apps", placeholder: "Search apps by name or bundle id", value: query, onChange: (e) => setQuery(e.target.value) })] }), _jsx(Button, { size: "sm", variant: "secondary", disabled: refreshing, onClick: refreshList, children: refreshing ? "Refreshing…" : "Refresh" }), _jsx(Button, { size: "sm", variant: "secondary", disabled: checking, onClick: () => checkUpdates(true), children: checking ? "Checking…" : "Check updates" })] }), _jsxs("div", { className: "ck-apps-filters", children: [_jsx(SegmentedControl, { label: "Show apps", value: filter, options: [
|
|
196
|
+
{ value: "all", label: "All" },
|
|
197
|
+
{ value: "unused", label: `Unused ${UNUSED_DAYS}+ days` },
|
|
198
|
+
{ value: "updates", label: updateCount > 0 ? `Updates (${updateCount})` : "Updates" },
|
|
199
|
+
], onChange: (v) => setFilter(v) }), apps.length > 0 && (_jsxs("span", { className: "ck-apps-summary", role: "status", children: [shown.length, " of ", apps.length, " apps \u00B7 ", bytes(shown.reduce((n, a) => n + a.size_bytes, 0)), " \u00B7 largest first", refreshing ? " · sizes updating…" : "", checking
|
|
200
|
+
? " · checking for updates…"
|
|
201
|
+
: updatesAt != null
|
|
202
|
+
? ` · updates checked ${ago(updatesAt)}`
|
|
203
|
+
: ""] }))] }), error && (_jsx("div", { className: "ck-apps-error", role: "alert", children: error })), pending.map((p) => (_jsxs(Notice, { tone: "warn", children: [_jsxs("strong", { children: [p.name, ": ", p.result.failed.length, " failed and still listed"] }), p.result.failed.map((f) => (_jsxs("div", { className: "ck-apps-failed", title: f.path, children: [f.path.replace(/^\/Users\/[^/]+/, "~"), ": ", f.error, " ", _jsx(Button, { size: "sm", variant: "secondary", "aria-label": `Reveal ${f.path}`, onClick: () => api.reveal(f.path).catch((e) => setError(String(e))), children: "Reveal" })] }, f.path))), _jsx(Button, { size: "sm", variant: "secondary", onClick: () => setPending((prev) => prev.filter((x) => x.path !== p.path)), children: "Dismiss" })] }, p.path))), _jsxs("div", { className: "ck-apps-card", tabIndex: 0, "aria-label": "Installed apps", children: [apps.length === 0 && (refreshing || !loaded) && (_jsxs("div", { className: "ck-apps-state", role: "status", children: [_jsx("strong", { children: "Reading your applications\u2026" }), _jsx("span", { children: "Checking sizes and last use" })] })), apps.length === 0 && loaded && !refreshing && (_jsxs("div", { className: "ck-apps-state", children: [_jsx("strong", { children: "No applications found" }), _jsx("span", { children: "Refresh to read them again." })] })), apps.length > 0 && shown.length === 0 && (_jsxs("div", { className: "ck-apps-state", children: [_jsx("strong", { children: "No matching apps" }), _jsx("span", { children: "Clear the search or filter, or refresh the list." }), filtered && (_jsx(Button, { size: "sm", variant: "secondary", onClick: () => {
|
|
204
|
+
setQuery("");
|
|
205
|
+
setFilter("all");
|
|
206
|
+
}, children: "Clear filters" }))] })), shown.map((a) => (_jsx(AppRow, { app: a, largest: largest, icon: icons[a.path], update: updates[a.path], job: jobs[a.path], onOpen: () => openApp(a), onUpdate: () => startUpdate(a.path) }, a.path)))] }), _jsxs("details", { className: "ck-apps-about", children: [_jsx("summary", { children: "About these numbers" }), _jsxs("p", { children: ["Sizes are measured on disk and keep updating while the list refreshes. Unused means not opened for", " ", UNUSED_DAYS, "+ days; an unknown last use never counts as unused. Updates show what the last check found. Apps that update themselves open their own updater. ", isWindows
|
|
207
|
+
? "Removing an app runs its own uninstaller; leftover folders you pick go to the Recycle Bin. Last used comes from what Windows recorded when you launched the app from Explorer or Start, so many apps show Unknown."
|
|
208
|
+
: "Removing an app moves it to the Trash."] })] }), _jsx(StartsByItself, {})] }));
|
|
209
|
+
}
|
|
210
|
+
const SCOPES = [
|
|
211
|
+
{ scope: "user", label: "Your account" },
|
|
212
|
+
{ scope: "all users", label: "All users" },
|
|
213
|
+
{ scope: "system", label: "System" },
|
|
214
|
+
];
|
|
215
|
+
/** Middle-truncates long text; the full value stays in the title. */
|
|
216
|
+
function middle(text, max = 60) {
|
|
217
|
+
if (text.length <= max)
|
|
218
|
+
return text;
|
|
219
|
+
const head = Math.ceil((max - 1) / 2);
|
|
220
|
+
const tail = Math.floor((max - 1) / 2);
|
|
221
|
+
return `${text.slice(0, head)}…${text.slice(text.length - tail)}`;
|
|
222
|
+
}
|
|
223
|
+
function StateText({ enabled }) {
|
|
224
|
+
if (enabled === true)
|
|
225
|
+
return _jsx("span", { className: "ck-apps-badge ck-apps-badge--ok", children: "On" });
|
|
226
|
+
if (enabled === false)
|
|
227
|
+
return _jsx("span", { className: "ck-apps-badge", children: "Off" });
|
|
228
|
+
return _jsx("span", { className: "ck-apps-badge", children: "Unknown" });
|
|
229
|
+
}
|
|
230
|
+
/** Login and background items that start by themselves. Read only; loaded when opened, not on a timer. */
|
|
231
|
+
function StartsByItself() {
|
|
232
|
+
const [report, setReport] = useState(null);
|
|
233
|
+
const [loading, setLoading] = useState(false);
|
|
234
|
+
const [error, setError] = useState(null);
|
|
235
|
+
const load = () => {
|
|
236
|
+
setLoading(true);
|
|
237
|
+
setError(null);
|
|
238
|
+
appsApi
|
|
239
|
+
.startupItems()
|
|
240
|
+
.then(setReport)
|
|
241
|
+
.catch((e) => setError(String(e)))
|
|
242
|
+
.finally(() => setLoading(false));
|
|
243
|
+
};
|
|
244
|
+
return (_jsxs("details", { className: "ck-apps-startup", onToggle: (e) => {
|
|
245
|
+
if (e.currentTarget.open && report === null && !loading)
|
|
246
|
+
load();
|
|
247
|
+
}, children: [_jsx("summary", { children: "Starts by itself" }), _jsxs("div", { className: "ck-apps-startup-body", children: [_jsxs("div", { className: "ck-apps-startup-tools", children: [_jsx("span", { className: "ck-apps-sub", role: "status", children: loading ? "Reading…" : report ? `${report.rows.length} items` : "" }), _jsx(Button, { size: "sm", variant: "secondary", disabled: loading, onClick: load, children: loading ? "Reading…" : "Refresh" })] }), error && (_jsx("div", { className: "ck-apps-error", role: "alert", children: error })), loading && !report && (_jsx("div", { className: "ck-apps-state", role: "status", children: _jsx("strong", { children: "Reading startup items\u2026" }) })), report && report.rows.length === 0 && (_jsxs("div", { className: "ck-apps-state", children: [_jsx("strong", { children: "Nothing starts by itself" }), _jsx("span", { children: "No launch items or Run entries were found." })] })), report &&
|
|
248
|
+
SCOPES.map(({ scope, label }) => {
|
|
249
|
+
const rows = report.rows.filter((r) => r.scope === scope);
|
|
250
|
+
if (rows.length === 0)
|
|
251
|
+
return null;
|
|
252
|
+
return (_jsxs("div", { className: "ck-apps-startup-group", role: "group", "aria-label": label, children: [_jsx("h3", { className: "ck-apps-startup-scope", children: label }), rows.map((row, index) => (_jsxs("div", { className: "ck-apps-startrow", children: [_jsxs("span", { className: "ck-apps-text", children: [_jsx("span", { className: "ck-apps-name", children: row.name }), _jsx("span", { className: "ck-apps-sub", children: row.kind })] }), _jsx("span", { className: "ck-apps-startwhat", title: row.program ?? row.path ?? undefined, children: row.program ? middle(row.program) : "Not known" }), _jsx(StateText, { enabled: row.enabled }), _jsx(Button, { size: "sm", variant: "secondary", disabled: !row.path, title: row.path ? undefined : "No file to reveal for this item", "aria-label": `Reveal ${row.name}`, onClick: () => {
|
|
253
|
+
if (row.path)
|
|
254
|
+
api.reveal(row.path).catch((e) => setError(String(e)));
|
|
255
|
+
}, children: "Reveal" })] }, `${scope}:${row.kind}:${row.name}:${index}`)))] }, scope));
|
|
256
|
+
}), report && report.notes.length > 0 && (_jsx("div", { className: "ck-apps-note", children: report.notes.map((note) => (_jsx("p", { children: note }, note))) }))] })] }));
|
|
257
|
+
}
|
|
258
|
+
function AppIcon({ src }) {
|
|
259
|
+
if (src)
|
|
260
|
+
return _jsx("img", { className: "ck-apps-icon", src: src, alt: "" });
|
|
261
|
+
return (_jsx("span", { className: "ck-apps-icon ck-apps-icon--fallback", "aria-hidden": "true", children: _jsx(AppWindow, { size: 16, strokeWidth: 1.75 }) }));
|
|
262
|
+
}
|
|
263
|
+
/** The update badge and action for one app. It sits beside the open button, never inside it. */
|
|
264
|
+
function UpdateControl({ update, job, onUpdate, }) {
|
|
265
|
+
if (job?.state === "running") {
|
|
266
|
+
return (_jsx("span", { className: "ck-apps-update", role: "status", children: _jsx("span", { className: "ck-apps-status", children: job.message }) }));
|
|
267
|
+
}
|
|
268
|
+
if (job?.state === "failed") {
|
|
269
|
+
return (_jsx("span", { className: "ck-apps-update", role: "alert", children: _jsx("span", { className: "ck-apps-status ck-apps-status--bad", title: job.message, children: "Update failed" }) }));
|
|
270
|
+
}
|
|
271
|
+
if (job?.state === "done") {
|
|
272
|
+
return (_jsx("span", { className: "ck-apps-update", role: "status", children: _jsx("span", { className: "ck-apps-status ck-apps-status--ok", title: job.message, children: job.message }) }));
|
|
273
|
+
}
|
|
274
|
+
if (update?.state === "available") {
|
|
275
|
+
return (_jsxs("span", { className: "ck-apps-update", children: [_jsxs("span", { className: "ck-apps-badge ck-apps-badge--accent", children: ["Update available", update.latest_version ? ` ${update.latest_version}` : ""] }), update.reason ? (_jsx("span", { className: "ck-apps-status", title: update.reason, children: update.reason })) : null, _jsx(Button, { size: "sm", variant: "secondary", onClick: onUpdate, children: "Update" })] }));
|
|
276
|
+
}
|
|
277
|
+
if (update?.state === "app_store") {
|
|
278
|
+
return (_jsx("span", { className: "ck-apps-update", children: _jsx(Button, { size: "sm", variant: "secondary", onClick: onUpdate, children: "Open in App Store" }) }));
|
|
279
|
+
}
|
|
280
|
+
return null;
|
|
281
|
+
}
|
|
282
|
+
function AppRow({ app, largest, icon, update, job, onOpen, onUpdate, }) {
|
|
283
|
+
return (_jsxs("div", { className: "ck-apps-row", children: [_jsxs(Button, { variant: "ghost", className: "ck-apps-open", onClick: onOpen, title: app.path, children: [_jsx(AppIcon, { src: icon }), _jsxs("span", { className: "ck-apps-text", children: [_jsxs("span", { className: "ck-apps-name", children: [app.name, app.running && _jsx("span", { className: "ck-apps-badge ck-apps-badge--ok", children: "Running" }), app.protected && _jsx("span", { className: "ck-apps-badge", children: "Protected" })] }), _jsxs("span", { className: "ck-apps-sub", children: [app.version ? `${app.version} · ` : "", "Last used: ", lastUsedText(app.last_used)] })] })] }), _jsx(UpdateControl, { update: update, job: job, onUpdate: onUpdate }), _jsxs("span", { className: "ck-apps-size", children: [bytes(app.size_bytes), _jsx("span", { className: "ck-apps-bar", "aria-hidden": "true", children: _jsx("i", { style: { width: `${(app.size_bytes / largest) * 100}%` } }) })] })] }));
|
|
284
|
+
}
|
|
285
|
+
const LOCATIONS = [
|
|
286
|
+
"Application",
|
|
287
|
+
"User Library",
|
|
288
|
+
"System Library",
|
|
289
|
+
"Your app data",
|
|
290
|
+
"Shared app data",
|
|
291
|
+
"Installer receipt",
|
|
292
|
+
];
|
|
293
|
+
const CONFIDENCE = {
|
|
294
|
+
exact: "Exact id",
|
|
295
|
+
helper: "Helper id",
|
|
296
|
+
group: "App group",
|
|
297
|
+
prefix: "Id prefix",
|
|
298
|
+
team: "Team id",
|
|
299
|
+
name: "Name",
|
|
300
|
+
receipt: "Receipt",
|
|
301
|
+
install: "Registered app",
|
|
302
|
+
publisher: "Publisher and product",
|
|
303
|
+
};
|
|
304
|
+
/** The bundle row from the list's facts, shown before the bundle part arrives. */
|
|
305
|
+
function bundleRow(app) {
|
|
306
|
+
return {
|
|
307
|
+
path: app.path,
|
|
308
|
+
label: "Application",
|
|
309
|
+
location: "Application",
|
|
310
|
+
exact: true,
|
|
311
|
+
confidence: "exact",
|
|
312
|
+
reason: "The application bundle",
|
|
313
|
+
admin: false,
|
|
314
|
+
size_bytes: app.size_bytes,
|
|
315
|
+
preselected: app.protected == null,
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
function mergeBackground(prev, more) {
|
|
319
|
+
const seen = new Set(prev.map((b) => `${b.kind}:${b.label}`));
|
|
320
|
+
const out = [...prev];
|
|
321
|
+
for (const b of more) {
|
|
322
|
+
const key = `${b.kind}:${b.label}`;
|
|
323
|
+
if (!seen.has(key)) {
|
|
324
|
+
seen.add(key);
|
|
325
|
+
out.push(b);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
return out;
|
|
329
|
+
}
|
|
330
|
+
function Detail({ app, update, job, retained, onStartUpdate, onUninstalled, onBack, }) {
|
|
331
|
+
const [items, setItems] = useState(() => new Map([[app.path, bundleRow(app)]]));
|
|
332
|
+
const [background, setBackground] = useState([]);
|
|
333
|
+
const [receipts, setReceipts] = useState([]);
|
|
334
|
+
// The ticks from before the hub window closed (its web view is destroyed), for this app only.
|
|
335
|
+
const keptPicked = useRef((() => {
|
|
336
|
+
const k = readKept(KEPT_PICKED);
|
|
337
|
+
return k && k.app === app.path ? k : null;
|
|
338
|
+
})());
|
|
339
|
+
const [picked, setPicked] = useState(() => new Set(retained.get(app.path)?.picked ?? keptPicked.current?.picked ?? (app.protected == null ? [app.path] : [])));
|
|
340
|
+
// Paths the user has changed: leftovers arriving later never overrule them.
|
|
341
|
+
const touched = useRef(new Set(retained.get(app.path)?.touched ?? keptPicked.current?.touched));
|
|
342
|
+
const [status, setStatus] = useState("loading");
|
|
343
|
+
const [leftoverError, setLeftoverError] = useState(null);
|
|
344
|
+
const [confirm, setConfirm] = useState(false);
|
|
345
|
+
const [busy, setBusy] = useState(false);
|
|
346
|
+
const [result, setResult] = useState(null);
|
|
347
|
+
const [error, setError] = useState(null);
|
|
348
|
+
const mergePart = (part) => {
|
|
349
|
+
setItems((prev) => {
|
|
350
|
+
const next = new Map(prev);
|
|
351
|
+
for (const item of part.items) {
|
|
352
|
+
const old = next.get(item.path);
|
|
353
|
+
if (!old || part.source === "bundle" || (item.preselected && !old.preselected)) {
|
|
354
|
+
next.set(item.path, item);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
return next;
|
|
358
|
+
});
|
|
359
|
+
const auto = part.items
|
|
360
|
+
.filter((i) => i.preselected && !touched.current.has(i.path))
|
|
361
|
+
.map((i) => i.path);
|
|
362
|
+
if (auto.length > 0)
|
|
363
|
+
setPicked((prev) => new Set([...prev, ...auto]));
|
|
364
|
+
if (part.background.length > 0)
|
|
365
|
+
setBackground((prev) => mergeBackground(prev, part.background));
|
|
366
|
+
if (part.receipts.length > 0)
|
|
367
|
+
setReceipts((prev) => [...new Set([...prev, ...part.receipts])]);
|
|
368
|
+
};
|
|
369
|
+
useEffect(() => {
|
|
370
|
+
const sub = listen("apps-leftovers", (e) => {
|
|
371
|
+
const event = e.payload;
|
|
372
|
+
if (event.path !== app.path)
|
|
373
|
+
return;
|
|
374
|
+
if (event.kind === "part") {
|
|
375
|
+
mergePart(event.part);
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
setLeftoverError(event.error);
|
|
379
|
+
setStatus(event.error ? "error" : "done");
|
|
380
|
+
});
|
|
381
|
+
sub
|
|
382
|
+
.then(() => appsApi.leftovers(app.path))
|
|
383
|
+
.catch((err) => {
|
|
384
|
+
setLeftoverError(String(err));
|
|
385
|
+
setStatus("error");
|
|
386
|
+
});
|
|
387
|
+
return () => {
|
|
388
|
+
sub.then((off) => off());
|
|
389
|
+
};
|
|
390
|
+
}, [app.path]);
|
|
391
|
+
const all = [...items.values()];
|
|
392
|
+
// Installer files under the app or a Library item are already listed there.
|
|
393
|
+
const taken = [
|
|
394
|
+
app.path,
|
|
395
|
+
...all.filter((i) => i.location === "User Library" || i.location === "System Library").map((i) => i.path),
|
|
396
|
+
];
|
|
397
|
+
const visible = all.filter((i) => i.location !== "Installer receipt" || !taken.some((t) => i.path === t || i.path.startsWith(`${t}/`)));
|
|
398
|
+
const pickedVisible = visible.filter((i) => picked.has(i.path));
|
|
399
|
+
const total = pickedVisible.reduce((n, i) => n + i.size_bytes, 0);
|
|
400
|
+
const adminPicked = pickedVisible.filter((i) => i.admin).length;
|
|
401
|
+
const set = (paths, on) => {
|
|
402
|
+
for (const p of paths)
|
|
403
|
+
touched.current.add(p);
|
|
404
|
+
setPicked((prev) => {
|
|
405
|
+
const next = new Set(prev);
|
|
406
|
+
for (const p of paths) {
|
|
407
|
+
if (on)
|
|
408
|
+
next.add(p);
|
|
409
|
+
else
|
|
410
|
+
next.delete(p);
|
|
411
|
+
}
|
|
412
|
+
return next;
|
|
413
|
+
});
|
|
414
|
+
};
|
|
415
|
+
const run = async () => {
|
|
416
|
+
setConfirm(false);
|
|
417
|
+
setBusy(true);
|
|
418
|
+
setError(null);
|
|
419
|
+
try {
|
|
420
|
+
const r = await api.uninstall(app.path, app.bundle_id, pickedVisible.map((i) => i.path));
|
|
421
|
+
setResult(r);
|
|
422
|
+
onUninstalled(r);
|
|
423
|
+
const gone = new Set(r.moved.map((m) => m.path));
|
|
424
|
+
setItems((prev) => {
|
|
425
|
+
const next = new Map(prev);
|
|
426
|
+
for (const p of gone)
|
|
427
|
+
next.delete(p);
|
|
428
|
+
return next;
|
|
429
|
+
});
|
|
430
|
+
setPicked(new Set());
|
|
431
|
+
}
|
|
432
|
+
catch (e) {
|
|
433
|
+
setError(String(e));
|
|
434
|
+
}
|
|
435
|
+
finally {
|
|
436
|
+
setBusy(false);
|
|
437
|
+
}
|
|
438
|
+
};
|
|
439
|
+
useEffect(() => {
|
|
440
|
+
retained.set(app.path, { picked, touched: touched.current });
|
|
441
|
+
writeKept(KEPT_PICKED, { app: app.path, picked: [...picked], touched: [...touched.current] });
|
|
442
|
+
}, [picked]);
|
|
443
|
+
// Once the leftover scan has finished, a kept tick whose item is gone is dropped. Uninstall
|
|
444
|
+
// still asks for confirmation.
|
|
445
|
+
useEffect(() => {
|
|
446
|
+
if (status !== "done")
|
|
447
|
+
return;
|
|
448
|
+
setPicked((prev) => {
|
|
449
|
+
const next = new Set([...prev].filter((p) => items.has(p)));
|
|
450
|
+
return next.size === prev.size ? prev : next;
|
|
451
|
+
});
|
|
452
|
+
}, [status]);
|
|
453
|
+
const reveal = (path) => {
|
|
454
|
+
api.reveal(path).catch((e) => setError(String(e)));
|
|
455
|
+
};
|
|
456
|
+
const group = (location) => {
|
|
457
|
+
const rows = visible.filter((i) => i.location === location);
|
|
458
|
+
if (rows.length === 0)
|
|
459
|
+
return null;
|
|
460
|
+
const paths = rows.map((i) => i.path);
|
|
461
|
+
const allOn = paths.every((p) => picked.has(p));
|
|
462
|
+
return (_jsxs("section", { className: "ck-apps-group", "aria-label": location, children: [_jsxs("div", { className: "ck-apps-group-head", children: [_jsxs("span", { children: [location, " ", _jsxs("span", { className: "muted", children: ["\u00B7 ", rows.length, " \u00B7 ", bytes(rows.reduce((n, i) => n + i.size_bytes, 0))] })] }), rows.length > 1 && (_jsx(Button, { size: "sm", variant: "secondary", disabled: busy, onClick: () => set(paths, !allOn), children: allOn ? "Deselect group" : "Select group" }))] }), rows.map((i) => (_jsxs("div", { className: "ck-apps-item", title: i.path, onClick: (e) => {
|
|
463
|
+
// The whole row toggles, as the label it replaces did; its own checkbox and Reveal handle their own clicks.
|
|
464
|
+
if (busy || e.target.closest("button, .rk-check"))
|
|
465
|
+
return;
|
|
466
|
+
set([i.path], !picked.has(i.path));
|
|
467
|
+
}, children: [_jsx(Checkbox, { checked: picked.has(i.path), disabled: busy, onChange: (on) => set([i.path], on), label: _jsxs("span", { className: "ov-vh", children: ["Select ", i.path.replace(/^\/Users\/[^/]+/, "~")] }) }), _jsxs("span", { className: "ck-apps-item-main", children: [_jsx("span", { className: "ck-apps-path", children: i.path.replace(/^\/Users\/[^/]+/, "~") }), _jsx("span", { className: "ck-apps-sub", children: i.reason }), _jsxs("span", { className: "ck-apps-tags", children: [_jsxs("span", { className: "ck-apps-badge", children: ["Match: ", CONFIDENCE[i.confidence] ?? i.confidence] }), i.admin && _jsx("span", { className: "ck-apps-badge ck-apps-badge--warn", children: "Admin required" }), !i.preselected && _jsx("span", { className: "ck-apps-badge ck-apps-badge--warn", children: "Review before moving" }), _jsx(Button, { size: "sm", variant: "secondary", disabled: !i.path, title: i.path ? undefined : "No file to reveal for this item", "aria-label": `Reveal ${i.path}`, onClick: () => reveal(i.path), children: "Reveal" })] })] }), _jsx("span", { className: "ck-apps-size", children: bytes(i.size_bytes) })] }, i.path)))] }, location));
|
|
468
|
+
};
|
|
469
|
+
return (_jsxs("div", { className: "view ck-apps", children: [_jsxs("div", { className: "ck-apps-head", children: [_jsx(Button, { size: "sm", variant: "secondary", onClick: onBack, children: _jsxs("span", { className: "ck-apps-back", children: [_jsx(ChevronLeft, { size: 14, strokeWidth: 1.75, "aria-hidden": "true" }), "Back to apps"] }) }), _jsxs("div", { className: "ck-apps-head-title", children: [_jsx("h2", { children: app.name }), _jsx("div", { className: "ck-apps-sub", children: [app.version, app.bundle_id, `Last used: ${lastUsedText(app.last_used)}`].filter(Boolean).join(" · ") })] }), _jsx(UpdateControl, { update: update, job: job, onUpdate: onStartUpdate }), _jsx(Button, { size: "sm", variant: "secondary", disabled: !app.path, title: app.path ? undefined : "This entry has no file or folder to reveal", "aria-label": `Reveal ${app.name}`, onClick: () => reveal(app.path), children: "Reveal" })] }), app.protected && _jsxs(Notice, { tone: "warn", children: [app.protected, " Uninstall is not offered."] }), app.running && !app.protected && (_jsx(Notice, { tone: "warn", children: "Running. Pulse will ask it to quit first, and will not remove anything if it stays open." })), status === "loading" && (_jsx(Notice, { children: "Finding leftovers\u2026" })), leftoverError && (_jsx("div", { className: "ck-apps-error", role: "alert", children: leftoverError })), error && (_jsx("div", { className: "ck-apps-error", role: "alert", children: error })), result && (_jsxs(Notice, { tone: result.failed.length > 0 ? "warn" : "ok", children: [isWindows
|
|
470
|
+
? `Started the uninstaller${result.moved.length > 0 ? ` · moved ${result.moved.length} leftover${result.moved.length === 1 ? "" : "s"} to the Recycle Bin: ${upTo(result.bytes_moved)} on disk · restore from the Recycle Bin · Space is not freed until the Recycle Bin is emptied.` : ""}`
|
|
471
|
+
: `Moved ${result.moved.length} to the Trash: ${upTo(result.bytes_moved)} on disk · restore with Put Back in Finder · Space is not freed until the Trash is emptied.`, result.failed.length > 0 && _jsxs("strong", { children: [" \u00B7 ", result.failed.length, " failed and still listed"] }), result.failed.map((f) => (_jsxs("div", { className: "ck-apps-failed", title: f.path, children: [f.path.replace(/^\/Users\/[^/]+/, "~"), ": ", f.error] }, f.path)))] })), _jsxs("div", { className: "ck-apps-card", tabIndex: 0, "aria-label": "Associated files", children: [LOCATIONS.map(group), background.length > 0 && (_jsxs("div", { className: "ck-apps-extra", children: [_jsx("h3", { children: "Login and background items" }), background.map((b) => (_jsxs("div", { title: b.path ?? "", children: [b.kind, " \u00B7 ", b.label] }, `${b.kind}:${b.label}`)))] })), receipts.length > 0 && _jsxs("div", { className: "ck-apps-extra", children: ["Installer packages: ", receipts.join(", ")] }), _jsxs("details", { className: "ck-apps-about", children: [_jsx("summary", { children: "About these numbers" }), _jsxs("p", { children: ["Sizes are measured on disk. Match shows how the file was tied to the app; items marked for review are left unchecked. ", isWindows
|
|
472
|
+
? "Leftover folders go to the Recycle Bin and can be restored. Registry and startup entries are listed only; Pulse never removes them."
|
|
473
|
+
: "Everything goes to the Trash and can be put back."] })] })] }), !app.protected && (_jsxs("div", { className: "ck-apps-foot", children: [_jsxs("div", { className: "ck-apps-foot-copy", children: [_jsxs("strong", { children: [pickedVisible.length, " selected \u00B7 ", bytes(total)] }), _jsxs("span", { className: "ck-apps-sub", children: [adminPicked > 0 && `${adminPicked} need an administrator password · `, app.running ? "Must quit first · " : "", isWindows
|
|
474
|
+
? "Runs the app's uninstaller; leftovers go to the Recycle Bin"
|
|
475
|
+
: "Goes to Trash, restore with Put Back"] })] }), _jsx(Button, { size: "sm", variant: "danger", disabled: busy || status === "loading" || pickedVisible.length === 0, onClick: () => setConfirm(true), children: busy ? "Working…" : isWindows ? "Uninstall" : "Move to Trash" })] })), confirm && (_jsx(ConfirmDialog, { danger: true, title: isWindows ? `Uninstall ${app.name}?` : `Move ${pickedVisible.length} item${pickedVisible.length === 1 ? "" : "s"} to Trash?`, description: isWindows
|
|
476
|
+
? `${app.name}'s own uninstaller opens and may ask for administrator rights. Once it has finished, any leftover folders you selected go to the Recycle Bin. If the uninstaller is cancelled, none are touched.`
|
|
477
|
+
: `${app.name} and the selected files, ${bytes(total)} in all. Everything goes to the Trash, so it can be put back.${app.running ? " The app will be asked to quit first." : ""}${adminPicked > 0 ? " Root-owned items make Finder ask for your password." : ""}`, confirmLabel: isWindows ? "Uninstall" : "Move to Trash", onConfirm: run, onCancel: () => setConfirm(false) }))] }));
|
|
478
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function Chat(): import("react").JSX.Element;
|