browser-cookie-bridge 1.2.0 β 1.4.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/README.md +14 -7
- package/bin/brave-codex-cookie-sync.js +10 -2
- package/extension-template/manifest.json +1 -1
- package/macos-app/Info.plist +2 -2
- package/macos-app/Sources/BraveCodexSyncApp/BraveCodexSyncApp.swift +152 -31
- package/macos-app/Sources/BraveCodexSyncApp/SyncModel.swift +339 -15
- package/package.json +2 -2
- package/src/browserless-preflight.js +113 -0
- package/src/browserless.js +184 -11
- package/src/cli.js +40 -6
- package/src/config.js +3 -0
package/README.md
CHANGED
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
|
|
45
45
|
---
|
|
46
46
|
|
|
47
|
-
> **Version 1.
|
|
47
|
+
> **Version 1.4.0 adds an optional one-click Codex restart flow.** When enabled for a manual sync, Browser Cookie Bridge force quits Codex, waits for its database to close, performs the verified local transfer, preserves the imported/skipped/failed counts, and reopens Codex only after success. The option is off by default.
|
|
48
48
|
|
|
49
49
|
## Why Browser Cookie Bridge
|
|
50
50
|
|
|
@@ -57,7 +57,8 @@ Browser Cookie Bridge gives those browser profiles a small, native control panel
|
|
|
57
57
|
- πͺ **Cookie and session transfer** β cookies are enabled by default, including supported domain, path, expiry, security, `SameSite`, and partition attributes.
|
|
58
58
|
- π **Seven Chromium browsers** β Brave, Chrome, Edge, Arc, Vivaldi, Opera, and Perplexity Comet can be sources or destinations.
|
|
59
59
|
- β¨ **ChatGPT Codex import** β merge selected local browser data into Codex's built-in browser; Codex is destination-only.
|
|
60
|
-
-
|
|
60
|
+
- π **Optional Codex restart** β manual Sync can force quit a running Codex instance and reopen it only after a successful local transfer; off by default.
|
|
61
|
+
- βοΈ **Optional Browserless upload** β create or refresh a Browserless authenticated profile with cookies, local storage, and IndexedDB; see a local size preflight, live progress, cancellation, and post-upload verification.
|
|
61
62
|
- π **Background automation** β sync when you sign in, at a fixed daily time, or whenever you choose.
|
|
62
63
|
- β **Native menu-bar app** β closing the window removes the Dock icon while the helper continues running.
|
|
63
64
|
- π§― **Backup and rollback** β Codex's database is backed up, modified on a separate copy, integrity-checked, and restored if replacement fails.
|
|
@@ -125,11 +126,11 @@ The final command compiles the native SwiftUI app, enables **Open at login**, **
|
|
|
125
126
|
No browser extension is needed for this path.
|
|
126
127
|
|
|
127
128
|
1. Select a source browser and **ChatGPT Codex** as the destination.
|
|
128
|
-
2. Quit Codex completely
|
|
129
|
+
2. Quit Codex completely, or enable **Restart Codex automatically**. Closing only its browser panel is not enough.
|
|
129
130
|
3. Choose **Cookies** and, optionally, **History URLs**.
|
|
130
|
-
4. Press **Sync now** and reopen Codex after the success message.
|
|
131
|
+
4. Press **Sync now** and reopen Codex after the success message if automatic restart is off.
|
|
131
132
|
|
|
132
|
-
|
|
133
|
+
By default, an open Codex blocks the transfer. The optional **Restart Codex automatically** setting applies only to a manual Sync: it force quits Codex, waits for its database to close, and reopens Codex only after the transfer succeeds. Scheduled and login syncs never force quit it.
|
|
133
134
|
|
|
134
135
|
### Browser β browser
|
|
135
136
|
|
|
@@ -151,7 +152,11 @@ This path uses the official Browserless CLI and is deliberately separate from lo
|
|
|
151
152
|
3. Quit the selected source browser so its profile can be copied consistently.
|
|
152
153
|
4. Review the cloud warning and click **Upload now**.
|
|
153
154
|
|
|
154
|
-
The upload creates the named Browserless profile the first time and refreshes it on later runs. It may contain cookies, local storage, and IndexedDB; history and saved passwords are excluded.
|
|
155
|
+
The upload creates the named Browserless profile the first time and refreshes it on later runs. It may contain cookies, local storage, and IndexedDB; history and saved passwords are excluded. The app measures the profile, IndexedDB, local storage, and available disk space locally before capture. Progress and elapsed time remain visible, Cancel upload terminates the isolated capture process group, and the dedicated temporary workspace is removed after success, failure, timeout, or cancellation.
|
|
156
|
+
|
|
157
|
+
Browserless currently caps the serialized authenticated-profile artifact at **2 MB**. A large on-disk IndexedDB does not mean all of it will be uploaded: the official CLI's `--auto-fit` behavior drops the heaviest origins until the artifact fits while keeping cookies. Browser Cookie Bridge reports those omissions in the final result and verifies that the named cloud profile can be read back after upload. Use the domain allowlist when you need specific sites or want a faster, smaller capture.
|
|
158
|
+
|
|
159
|
+
Browserless uploads never run from Daily sync or Sync at login. Browser Cookie Bridge disables Browserless CLI telemetry for this integration. Comet is not currently supported by the Browserless capture CLI. The default cloud timeout is 15 minutes; `--timeout` can override it.
|
|
155
160
|
|
|
156
161
|
The official CLI records the Browserless upload-disclaimer acceptance timestamp in `~/.browserless/config.json`. Browser Cookie Bridge does not store its API token there.
|
|
157
162
|
|
|
@@ -167,6 +172,7 @@ The official CLI records the Browserless upload-disclaimer acceptance timestamp
|
|
|
167
172
|
| **Sync at login** | Runs once whenever you sign in; on by default |
|
|
168
173
|
| **Open at login** | Starts the background app after macOS login; on by default |
|
|
169
174
|
| **Show in menu bar** | Keeps sync, status, updates, and support actions close at hand |
|
|
175
|
+
| **Restart Codex automatically** | Force quits Codex for a manual sync and reopens it only after success; off by default |
|
|
170
176
|
| **Check for updates** | Finds a newer GitHub release, verifies its DMG, and offers install + relaunch |
|
|
171
177
|
|
|
172
178
|
Automation uses the saved source, destination, and data choices for local transfers. A scheduled Codex sync safely exits without making changes when Codex is open. Browserless cloud uploads are always manual and require an explicit upload action.
|
|
@@ -176,8 +182,9 @@ Automation uses the saved source, destination, and data choices for local transf
|
|
|
176
182
|
```bash
|
|
177
183
|
browser-cookie-bridge install-app [--no-open]
|
|
178
184
|
browser-cookie-bridge setup [--hour 9] [--minute 0] [--no-schedule]
|
|
179
|
-
browser-cookie-bridge preferences --source brave --target codex --cookies on --history off
|
|
185
|
+
browser-cookie-bridge preferences --source brave --target codex --cookies on --history off --auto-restart-codex off
|
|
180
186
|
browser-cookie-bridge sync [--timeout 300] [--allow-cloud-upload]
|
|
187
|
+
browser-cookie-bridge browserless-preflight
|
|
181
188
|
browser-cookie-bridge doctor
|
|
182
189
|
browser-cookie-bridge enable-login-sync
|
|
183
190
|
browser-cookie-bridge disable-login-sync
|
|
@@ -2,7 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
import { main } from "../src/cli.js";
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
const controller = new AbortController();
|
|
6
|
+
const cancel = () => controller.abort();
|
|
7
|
+
process.once("SIGINT", cancel);
|
|
8
|
+
process.once("SIGTERM", cancel);
|
|
9
|
+
|
|
10
|
+
main(process.argv.slice(2), { signal: controller.signal }).catch((error) => {
|
|
6
11
|
console.error(`Error: ${error.message}`);
|
|
7
|
-
process.exitCode = 1;
|
|
12
|
+
process.exitCode = controller.signal.aborted ? 130 : 1;
|
|
13
|
+
}).finally(() => {
|
|
14
|
+
process.removeListener("SIGINT", cancel);
|
|
15
|
+
process.removeListener("SIGTERM", cancel);
|
|
8
16
|
});
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"manifest_version": 3,
|
|
3
3
|
"name": "Browser Cookie Bridge",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.4.0",
|
|
5
5
|
"description": "Transfers selected browser data locally between supported browsers and into ChatGPT Codex.",
|
|
6
6
|
"key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA5o6lDkXzU/23Jm2QRk4mfuxNnr3VlEIjRHcZr93Gdo7PI7asHJQhigaMGT6UOz0lkaTn1F+TGIPlsJo9GNBSsxbCfxWoBSXcFQFrPolY5LEfKaMtZQQ6eb4UAnp+bbUwb1den4On0piWU47Yzl6i0Jcdg4EG5H1kCSF/6nJsbKgF7OTaR/3drULu40yRvflzSXRhly3UDO7c5mgHJsaMwVV+VILsxbcPOoRINjuo3zDC737HFs67dpf38xF3tjSVL0HURAWKKobMvfABiDfVEm5O0Lvf+57d1Ib12QLT2qtVXNDy3iYwX+w2S0wH2vIMsfvoFPlRdmeqft70kZOqXwIDAQAB",
|
|
7
7
|
"permissions": ["alarms", "cookies", "history"],
|
package/macos-app/Info.plist
CHANGED
|
@@ -10,8 +10,8 @@
|
|
|
10
10
|
<key>CFBundleInfoDictionaryVersion</key><string>6.0</string>
|
|
11
11
|
<key>CFBundleName</key><string>Browser Cookie Bridge</string>
|
|
12
12
|
<key>CFBundlePackageType</key><string>APPL</string>
|
|
13
|
-
<key>CFBundleShortVersionString</key><string>1.
|
|
14
|
-
<key>CFBundleVersion</key><string>
|
|
13
|
+
<key>CFBundleShortVersionString</key><string>1.4.0</string>
|
|
14
|
+
<key>CFBundleVersion</key><string>28</string>
|
|
15
15
|
<key>LSMinimumSystemVersion</key><string>13.5</string>
|
|
16
16
|
<key>NSHighResolutionCapable</key><true/>
|
|
17
17
|
</dict>
|
|
@@ -35,6 +35,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSWindowDelegate {
|
|
|
35
35
|
weak var model: SyncModel?
|
|
36
36
|
private weak var mainWindow: NSWindow?
|
|
37
37
|
private var statusItem: NSStatusItem?
|
|
38
|
+
private var syncMenuItem: NSMenuItem?
|
|
38
39
|
private var updateMenuItem: NSMenuItem?
|
|
39
40
|
|
|
40
41
|
func applicationDidFinishLaunching(_ notification: Notification) {
|
|
@@ -56,6 +57,12 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSWindowDelegate {
|
|
|
56
57
|
name: .updateStateChanged,
|
|
57
58
|
object: nil
|
|
58
59
|
)
|
|
60
|
+
NotificationCenter.default.addObserver(
|
|
61
|
+
self,
|
|
62
|
+
selector: #selector(syncStateChanged(_:)),
|
|
63
|
+
name: .syncStateChanged,
|
|
64
|
+
object: nil
|
|
65
|
+
)
|
|
59
66
|
}
|
|
60
67
|
|
|
61
68
|
func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { false }
|
|
@@ -94,7 +101,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSWindowDelegate {
|
|
|
94
101
|
item.button?.imageScaling = .scaleProportionallyDown
|
|
95
102
|
let menu = NSMenu()
|
|
96
103
|
menu.addItem(withTitle: "Show Browser Cookie Bridge", action: #selector(showMainWindowAction), keyEquivalent: "")
|
|
97
|
-
menu.addItem(withTitle: "Sync now", action: #selector(syncNowAction), keyEquivalent: "")
|
|
104
|
+
syncMenuItem = menu.addItem(withTitle: "Sync now", action: #selector(syncNowAction), keyEquivalent: "")
|
|
98
105
|
let updateItem = menu.addItem(withTitle: "Check for Updatesβ¦", action: #selector(updateAction), keyEquivalent: "")
|
|
99
106
|
updateMenuItem = updateItem
|
|
100
107
|
addProjectLinks(to: menu)
|
|
@@ -106,6 +113,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSWindowDelegate {
|
|
|
106
113
|
} else if let statusItem {
|
|
107
114
|
NSStatusBar.system.removeStatusItem(statusItem)
|
|
108
115
|
self.statusItem = nil
|
|
116
|
+
syncMenuItem = nil
|
|
109
117
|
updateMenuItem = nil
|
|
110
118
|
}
|
|
111
119
|
}
|
|
@@ -116,7 +124,10 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSWindowDelegate {
|
|
|
116
124
|
}
|
|
117
125
|
|
|
118
126
|
@objc private func showMainWindowAction() { showMainWindow() }
|
|
119
|
-
@objc private func syncNowAction() {
|
|
127
|
+
@objc private func syncNowAction() {
|
|
128
|
+
if model?.isBrowserlessTarget == true, model?.isSyncing == true { model?.cancelSync() }
|
|
129
|
+
else { model?.syncNow(showMenuBarAlert: true) }
|
|
130
|
+
}
|
|
120
131
|
@objc private func updateAction() {
|
|
121
132
|
if model?.availableUpdateVersion != nil { model?.installAvailableUpdate() }
|
|
122
133
|
else { model?.checkForUpdates(showAlert: true) }
|
|
@@ -161,6 +172,20 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSWindowDelegate {
|
|
|
161
172
|
}
|
|
162
173
|
}
|
|
163
174
|
|
|
175
|
+
@objc private func syncStateChanged(_ notification: Notification) {
|
|
176
|
+
guard let payload = notification.object as? SyncMenuState else { return }
|
|
177
|
+
if payload.canceling {
|
|
178
|
+
syncMenuItem?.title = "Canceling uploadβ¦"
|
|
179
|
+
syncMenuItem?.isEnabled = false
|
|
180
|
+
} else if payload.uploading {
|
|
181
|
+
syncMenuItem?.title = "Cancel upload"
|
|
182
|
+
syncMenuItem?.isEnabled = true
|
|
183
|
+
} else {
|
|
184
|
+
syncMenuItem?.title = "Sync now"
|
|
185
|
+
syncMenuItem?.isEnabled = true
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
164
189
|
private func showMainWindow() {
|
|
165
190
|
NSApp.setActivationPolicy(.regular)
|
|
166
191
|
mainWindow?.makeKeyAndOrderFront(nil)
|
|
@@ -290,35 +315,57 @@ struct SyncPanel: View {
|
|
|
290
315
|
TargetPicker()
|
|
291
316
|
}
|
|
292
317
|
Divider().opacity(0.65)
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
Spacer()
|
|
304
|
-
Button {
|
|
305
|
-
if model.isBrowserlessTarget && !model.browserlessConfigured {
|
|
306
|
-
model.showingBrowserlessSetup = true
|
|
307
|
-
} else {
|
|
308
|
-
model.syncNow()
|
|
318
|
+
VStack(spacing: 8) {
|
|
319
|
+
HStack(spacing: 12) {
|
|
320
|
+
VStack(alignment: .leading, spacing: 2) {
|
|
321
|
+
Text(model.primaryStatus)
|
|
322
|
+
.font(.system(size: 13, weight: .semibold))
|
|
323
|
+
Text(model.secondaryStatus)
|
|
324
|
+
.font(.system(size: 10.5))
|
|
325
|
+
.foregroundStyle(.secondary)
|
|
326
|
+
.lineLimit(2)
|
|
327
|
+
.help(model.secondaryStatus)
|
|
309
328
|
}
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
if model.
|
|
313
|
-
|
|
314
|
-
|
|
329
|
+
Spacer()
|
|
330
|
+
Button {
|
|
331
|
+
if model.isBrowserlessTarget && model.isSyncing {
|
|
332
|
+
model.cancelSync()
|
|
333
|
+
} else if model.isBrowserlessTarget && !model.browserlessConfigured {
|
|
334
|
+
model.showingBrowserlessSetup = true
|
|
335
|
+
} else {
|
|
336
|
+
model.syncNow()
|
|
337
|
+
}
|
|
338
|
+
} label: {
|
|
339
|
+
HStack(spacing: 7) {
|
|
340
|
+
if model.isSyncing && !model.isBrowserlessTarget { ProgressView().controlSize(.small) }
|
|
341
|
+
else { Image(systemName: syncButtonIcon) }
|
|
342
|
+
Text(syncButtonTitle)
|
|
343
|
+
}
|
|
344
|
+
.frame(minWidth: model.syncBlocked ? 116 : 92)
|
|
345
|
+
}
|
|
346
|
+
.buttonStyle(.borderedProminent)
|
|
347
|
+
.tint(model.isBrowserlessTarget && model.isSyncing ? Theme.active : Theme.accent)
|
|
348
|
+
.disabled(
|
|
349
|
+
model.uploadCanceling
|
|
350
|
+
|| (model.isSyncing && !model.isBrowserlessTarget)
|
|
351
|
+
|| (!model.isSyncing && model.syncBlocked && !(model.isBrowserlessTarget && !model.browserlessConfigured))
|
|
352
|
+
)
|
|
353
|
+
.keyboardShortcut(.return, modifiers: .command)
|
|
354
|
+
}
|
|
355
|
+
if model.isBrowserlessTarget && model.isSyncing {
|
|
356
|
+
HStack(spacing: 9) {
|
|
357
|
+
ProgressView(value: model.uploadProgress, total: 1)
|
|
358
|
+
.progressViewStyle(.linear)
|
|
359
|
+
.tint(Theme.active)
|
|
360
|
+
Text(model.formattedUploadElapsed)
|
|
361
|
+
.font(.system(size: 9.5, weight: .semibold, design: .monospaced))
|
|
362
|
+
.foregroundStyle(.secondary)
|
|
363
|
+
.frame(width: 34, alignment: .trailing)
|
|
315
364
|
}
|
|
316
|
-
.
|
|
365
|
+
.accessibilityElement(children: .combine)
|
|
366
|
+
.accessibilityLabel("Browserless upload progress")
|
|
367
|
+
.accessibilityValue("\(Int(model.uploadProgress * 100)) percent, \(model.formattedUploadElapsed) elapsed")
|
|
317
368
|
}
|
|
318
|
-
.buttonStyle(.borderedProminent)
|
|
319
|
-
.tint(Theme.accent)
|
|
320
|
-
.disabled(model.isSyncing || (model.syncBlocked && !(model.isBrowserlessTarget && !model.browserlessConfigured)))
|
|
321
|
-
.keyboardShortcut(.return, modifiers: .command)
|
|
322
369
|
}
|
|
323
370
|
.padding(model.syncBlocked ? 7 : 0)
|
|
324
371
|
.background(
|
|
@@ -336,7 +383,9 @@ struct SyncPanel: View {
|
|
|
336
383
|
}
|
|
337
384
|
|
|
338
385
|
private var syncButtonTitle: String {
|
|
339
|
-
if model.
|
|
386
|
+
if model.isBrowserlessTarget && model.uploadCanceling { return "Cancelingβ¦" }
|
|
387
|
+
if model.isBrowserlessTarget && model.isSyncing { return "Cancel upload" }
|
|
388
|
+
if model.isSyncing { return "Syncingβ¦" }
|
|
340
389
|
if model.codexBlocked { return "Close Codex first" }
|
|
341
390
|
if model.isBrowserlessTarget && !model.browserlessConfigured { return "Connect Browserless" }
|
|
342
391
|
if model.isBrowserlessTarget && model.selectedSourceID == "comet" { return "Choose another browser" }
|
|
@@ -345,6 +394,7 @@ struct SyncPanel: View {
|
|
|
345
394
|
}
|
|
346
395
|
|
|
347
396
|
private var syncButtonIcon: String {
|
|
397
|
+
if model.isBrowserlessTarget && model.isSyncing { return "xmark.circle.fill" }
|
|
348
398
|
if model.syncBlocked { return model.isBrowserlessTarget && !model.browserlessConfigured ? "key.fill" : "xmark.circle.fill" }
|
|
349
399
|
return model.isBrowserlessTarget ? "icloud.and.arrow.up.fill" : "arrow.triangle.2.circlepath"
|
|
350
400
|
}
|
|
@@ -499,6 +549,21 @@ struct PreferencesPanel: View {
|
|
|
499
549
|
}
|
|
500
550
|
.controlSize(.small)
|
|
501
551
|
}
|
|
552
|
+
RowDivider()
|
|
553
|
+
PreferenceRow(icon: "externaldrive.badge.magnifyingglass", color: Theme.accent, title: "Local profile preflight", detail: browserlessPreflightDetail) {
|
|
554
|
+
HStack(spacing: 7) {
|
|
555
|
+
ProfileSizeBadge(severity: model.browserlessAssessment?.severity, scanning: model.isInspectingBrowserlessProfile)
|
|
556
|
+
Button {
|
|
557
|
+
model.refreshBrowserlessPreflight(force: true)
|
|
558
|
+
} label: {
|
|
559
|
+
Image(systemName: "arrow.clockwise")
|
|
560
|
+
}
|
|
561
|
+
.buttonStyle(.plain)
|
|
562
|
+
.foregroundStyle(.secondary)
|
|
563
|
+
.disabled(model.isInspectingBrowserlessProfile || model.isSyncing)
|
|
564
|
+
.help("Rescan local profile size")
|
|
565
|
+
}
|
|
566
|
+
}
|
|
502
567
|
} else {
|
|
503
568
|
PreferenceRow(icon: "network", color: Theme.accent, title: "Cookies", detail: "Site sessions and sign-ins") {
|
|
504
569
|
Toggle("", isOn: Binding(get: { model.cookiesEnabled }, set: { model.setCookiesEnabled($0) }))
|
|
@@ -513,6 +578,18 @@ struct PreferencesPanel: View {
|
|
|
513
578
|
PreferenceRow(icon: "key.slash", color: .secondary, title: "Passwords", detail: "Browser extensions cannot access passwords", muted: true) {
|
|
514
579
|
FixedBadge("Unavailable")
|
|
515
580
|
}
|
|
581
|
+
if model.selectedTargetID == "codex" {
|
|
582
|
+
RowDivider()
|
|
583
|
+
PreferenceRow(
|
|
584
|
+
icon: "arrow.clockwise.circle",
|
|
585
|
+
color: Theme.accent,
|
|
586
|
+
title: "Restart Codex automatically",
|
|
587
|
+
detail: "Force quit before manual sync; reopen only after success"
|
|
588
|
+
) {
|
|
589
|
+
Toggle("", isOn: Binding(get: { model.autoRestartCodex }, set: { model.setAutoRestartCodex($0) }))
|
|
590
|
+
.labelsHidden().toggleStyle(.switch).tint(Theme.active).disabled(model.isWorking || model.isSyncing)
|
|
591
|
+
}
|
|
592
|
+
}
|
|
516
593
|
}
|
|
517
594
|
|
|
518
595
|
SectionLabel(title: "Automation", detail: "Runs in the background", separated: true)
|
|
@@ -579,6 +656,16 @@ struct PreferencesPanel: View {
|
|
|
579
656
|
}
|
|
580
657
|
}
|
|
581
658
|
}
|
|
659
|
+
|
|
660
|
+
private var browserlessPreflightDetail: String {
|
|
661
|
+
if model.isInspectingBrowserlessProfile { return "Measuring profile, local storage, and IndexedDB without reading their contents" }
|
|
662
|
+
if let assessment = model.browserlessAssessment {
|
|
663
|
+
return assessment.temporarySpaceWarning
|
|
664
|
+
? "\(assessment.summary) Β· free space may be too low for the temporary copy"
|
|
665
|
+
: assessment.summary
|
|
666
|
+
}
|
|
667
|
+
return "Calculated locally before upload; Browserless cloud artifacts are capped at 2 MB"
|
|
668
|
+
}
|
|
582
669
|
}
|
|
583
670
|
|
|
584
671
|
struct FixedBadge: View {
|
|
@@ -595,6 +682,40 @@ struct FixedBadge: View {
|
|
|
595
682
|
}
|
|
596
683
|
}
|
|
597
684
|
|
|
685
|
+
struct ProfileSizeBadge: View {
|
|
686
|
+
let severity: String?
|
|
687
|
+
let scanning: Bool
|
|
688
|
+
|
|
689
|
+
var body: some View {
|
|
690
|
+
Text(label)
|
|
691
|
+
.font(.system(size: 9.5, weight: .semibold))
|
|
692
|
+
.foregroundStyle(color)
|
|
693
|
+
.padding(.horizontal, 8).padding(.vertical, 4)
|
|
694
|
+
.background(color.opacity(0.10), in: Capsule())
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
private var label: String {
|
|
698
|
+
if scanning { return "Scanning" }
|
|
699
|
+
switch severity {
|
|
700
|
+
case "elevated": return "Large"
|
|
701
|
+
case "high": return "Very large"
|
|
702
|
+
case "extreme": return "Extreme"
|
|
703
|
+
case "normal": return "Ready"
|
|
704
|
+
default: return "Not scanned"
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
private var color: Color {
|
|
709
|
+
if scanning { return .secondary }
|
|
710
|
+
switch severity {
|
|
711
|
+
case "elevated": return .orange
|
|
712
|
+
case "high", "extreme": return Theme.active
|
|
713
|
+
case "normal": return Theme.accent
|
|
714
|
+
default: return .secondary
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
|
|
598
719
|
struct BrowserlessSetupSheet: View {
|
|
599
720
|
@EnvironmentObject private var model: SyncModel
|
|
600
721
|
@Environment(\.dismiss) private var dismiss
|
|
@@ -904,11 +1025,11 @@ struct StatusIndicator: View {
|
|
|
904
1025
|
}
|
|
905
1026
|
|
|
906
1027
|
private var label: String {
|
|
907
|
-
switch state { case .ready: "Ready"; case .syncing: "Syncing"; case .success: "Synced"; case .warning: "Partial"; case .error: "Needs action" }
|
|
1028
|
+
switch state { case .ready: "Ready"; case .syncing: "Syncing"; case .success: "Synced"; case .canceled: "Canceled"; case .warning: "Partial"; case .error: "Needs action" }
|
|
908
1029
|
}
|
|
909
1030
|
|
|
910
1031
|
private var color: Color {
|
|
911
|
-
switch state { case .ready, .syncing, .success: Theme.accent; case .warning: .orange; case .error: .red }
|
|
1032
|
+
switch state { case .ready, .syncing, .success: Theme.accent; case .canceled: .secondary; case .warning: .orange; case .error: .red }
|
|
912
1033
|
}
|
|
913
1034
|
}
|
|
914
1035
|
|