browser-cookie-bridge 1.1.0 → 1.3.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 +35 -13
- package/THIRD_PARTY_NOTICES.md +11 -0
- 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/Resources/BrowserIcons/browserless.svg +6 -0
- package/macos-app/Sources/BraveCodexSyncApp/BraveCodexSyncApp.swift +342 -62
- package/macos-app/Sources/BraveCodexSyncApp/SyncModel.swift +400 -34
- package/package.json +11 -6
- package/src/browserless-preflight.js +113 -0
- package/src/browserless-runner.js +13 -0
- package/src/browserless.js +281 -0
- package/src/cli.js +76 -6
- package/src/config.js +38 -4
- package/src/paths.js +1 -1
|
@@ -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)
|
|
@@ -213,6 +238,9 @@ struct ContentView: View {
|
|
|
213
238
|
.sheet(isPresented: $showingSetup) {
|
|
214
239
|
ExtensionSetupSheet().environmentObject(model)
|
|
215
240
|
}
|
|
241
|
+
.sheet(isPresented: $model.showingBrowserlessSetup) {
|
|
242
|
+
BrowserlessSetupSheet().environmentObject(model)
|
|
243
|
+
}
|
|
216
244
|
}
|
|
217
245
|
|
|
218
246
|
private var header: some View {
|
|
@@ -238,7 +266,10 @@ struct ContentView: View {
|
|
|
238
266
|
|
|
239
267
|
private var footer: some View {
|
|
240
268
|
HStack(spacing: 10) {
|
|
241
|
-
Label(
|
|
269
|
+
Label(
|
|
270
|
+
model.isBrowserlessTarget ? "Cloud upload only when you choose it" : "Data stays on this Mac",
|
|
271
|
+
systemImage: model.isBrowserlessTarget ? "cloud.fill" : "lock.shield.fill"
|
|
272
|
+
)
|
|
242
273
|
.foregroundStyle(.secondary)
|
|
243
274
|
Spacer()
|
|
244
275
|
if let version = model.availableUpdateVersion {
|
|
@@ -262,7 +293,7 @@ struct ContentView: View {
|
|
|
262
293
|
.buttonStyle(.plain)
|
|
263
294
|
.foregroundStyle(.secondary)
|
|
264
295
|
.help("Refresh status")
|
|
265
|
-
if model.selectedTargetID != "codex" {
|
|
296
|
+
if model.selectedTargetID != "codex" && !model.isBrowserlessTarget {
|
|
266
297
|
Button("Extension setup…") { showingSetup = true }
|
|
267
298
|
.controlSize(.small)
|
|
268
299
|
}
|
|
@@ -284,37 +315,65 @@ struct SyncPanel: View {
|
|
|
284
315
|
TargetPicker()
|
|
285
316
|
}
|
|
286
317
|
Divider().opacity(0.65)
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
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)
|
|
328
|
+
}
|
|
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)
|
|
296
354
|
}
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
Text(model.
|
|
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)
|
|
303
364
|
}
|
|
304
|
-
.
|
|
365
|
+
.accessibilityElement(children: .combine)
|
|
366
|
+
.accessibilityLabel("Browserless upload progress")
|
|
367
|
+
.accessibilityValue("\(Int(model.uploadProgress * 100)) percent, \(model.formattedUploadElapsed) elapsed")
|
|
305
368
|
}
|
|
306
|
-
.buttonStyle(.borderedProminent)
|
|
307
|
-
.tint(Theme.accent)
|
|
308
|
-
.disabled(model.isSyncing || model.codexBlocked)
|
|
309
|
-
.keyboardShortcut(.return, modifiers: .command)
|
|
310
369
|
}
|
|
311
|
-
.padding(model.
|
|
370
|
+
.padding(model.syncBlocked ? 7 : 0)
|
|
312
371
|
.background(
|
|
313
|
-
model.
|
|
372
|
+
model.syncBlocked ? Theme.active.opacity(0.10) : Color.clear,
|
|
314
373
|
in: RoundedRectangle(cornerRadius: 9, style: .continuous)
|
|
315
374
|
)
|
|
316
375
|
.overlay {
|
|
317
|
-
if model.
|
|
376
|
+
if model.syncBlocked {
|
|
318
377
|
RoundedRectangle(cornerRadius: 9, style: .continuous)
|
|
319
378
|
.stroke(Theme.active.opacity(0.38), lineWidth: 1)
|
|
320
379
|
}
|
|
@@ -323,6 +382,23 @@ struct SyncPanel: View {
|
|
|
323
382
|
}
|
|
324
383
|
}
|
|
325
384
|
|
|
385
|
+
private var syncButtonTitle: String {
|
|
386
|
+
if model.isBrowserlessTarget && model.uploadCanceling { return "Canceling…" }
|
|
387
|
+
if model.isBrowserlessTarget && model.isSyncing { return "Cancel upload" }
|
|
388
|
+
if model.isSyncing { return "Syncing…" }
|
|
389
|
+
if model.codexBlocked { return "Close Codex first" }
|
|
390
|
+
if model.isBrowserlessTarget && !model.browserlessConfigured { return "Connect Browserless" }
|
|
391
|
+
if model.isBrowserlessTarget && model.selectedSourceID == "comet" { return "Choose another browser" }
|
|
392
|
+
if model.isBrowserlessTarget && model.sourceBrowserRunning { return "Close \(model.selectedBrowser.name) first" }
|
|
393
|
+
return model.isBrowserlessTarget ? "Upload now" : "Sync now"
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
private var syncButtonIcon: String {
|
|
397
|
+
if model.isBrowserlessTarget && model.isSyncing { return "xmark.circle.fill" }
|
|
398
|
+
if model.syncBlocked { return model.isBrowserlessTarget && !model.browserlessConfigured ? "key.fill" : "xmark.circle.fill" }
|
|
399
|
+
return model.isBrowserlessTarget ? "icloud.and.arrow.up.fill" : "arrow.triangle.2.circlepath"
|
|
400
|
+
}
|
|
401
|
+
|
|
326
402
|
}
|
|
327
403
|
|
|
328
404
|
struct SourcePicker: View {
|
|
@@ -378,7 +454,12 @@ struct TargetPicker: View {
|
|
|
378
454
|
VStack(spacing: 5) {
|
|
379
455
|
HStack(spacing: 5) {
|
|
380
456
|
ForEach(Array(model.browsers.prefix(4))) { browser in targetButton(browser) }
|
|
381
|
-
|
|
457
|
+
EndpointButton(
|
|
458
|
+
icon: model.browserlessIcon,
|
|
459
|
+
name: "Browserless Cloud",
|
|
460
|
+
selected: model.selectedTargetID == "browserless",
|
|
461
|
+
disabled: model.isWorking || model.isSyncing || model.selectedSourceID == "comet"
|
|
462
|
+
) { model.selectTarget("browserless") }
|
|
382
463
|
}
|
|
383
464
|
HStack(spacing: 5) {
|
|
384
465
|
ForEach(Array(model.browsers.dropFirst(4))) { browser in targetButton(browser) }
|
|
@@ -449,44 +530,84 @@ struct PreferencesPanel: View {
|
|
|
449
530
|
Surface(padding: 0) {
|
|
450
531
|
VStack(spacing: 0) {
|
|
451
532
|
SectionLabel(title: "Sync data", detail: "Saved automatically")
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
533
|
+
if model.isBrowserlessTarget {
|
|
534
|
+
PreferenceRow(icon: "person.crop.circle.badge.checkmark", color: Theme.accent, title: "Authenticated state", detail: "Cookies, local storage, and IndexedDB") {
|
|
535
|
+
FixedBadge("Included")
|
|
536
|
+
}
|
|
537
|
+
RowDivider()
|
|
538
|
+
PreferenceRow(icon: "clock.arrow.circlepath", color: .secondary, title: "History URLs", detail: "Not included in Browserless profiles", muted: true) {
|
|
539
|
+
FixedBadge("Excluded")
|
|
540
|
+
}
|
|
541
|
+
RowDivider()
|
|
542
|
+
PreferenceRow(icon: "key.slash", color: .secondary, title: "Saved passwords", detail: "Never read or uploaded", muted: true) {
|
|
543
|
+
FixedBadge("Excluded")
|
|
544
|
+
}
|
|
545
|
+
RowDivider()
|
|
546
|
+
PreferenceRow(icon: "cloud.fill", color: Theme.accent, title: "Browserless connection", detail: model.browserlessConfigured ? "\(model.browserlessProfileName) · \(model.browserlessRegion.uppercased()) · token in Keychain" : "Not connected") {
|
|
547
|
+
Button(model.browserlessConfigured ? "Configure…" : "Connect…") {
|
|
548
|
+
model.showingBrowserlessSetup = true
|
|
549
|
+
}
|
|
550
|
+
.controlSize(.small)
|
|
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
|
+
}
|
|
567
|
+
} else {
|
|
568
|
+
PreferenceRow(icon: "network", color: Theme.accent, title: "Cookies", detail: "Site sessions and sign-ins") {
|
|
569
|
+
Toggle("", isOn: Binding(get: { model.cookiesEnabled }, set: { model.setCookiesEnabled($0) }))
|
|
570
|
+
.labelsHidden().toggleStyle(.switch).tint(Theme.active).disabled(model.isWorking)
|
|
571
|
+
}
|
|
572
|
+
RowDivider()
|
|
573
|
+
PreferenceRow(icon: "clock.arrow.circlepath", color: Theme.accent, title: "History URLs", detail: "Original visit times are not preserved") {
|
|
574
|
+
Toggle("", isOn: Binding(get: { model.historyEnabled }, set: { model.setHistoryEnabled($0) }))
|
|
575
|
+
.labelsHidden().toggleStyle(.switch).tint(Theme.active).disabled(model.isWorking)
|
|
576
|
+
}
|
|
577
|
+
RowDivider()
|
|
578
|
+
PreferenceRow(icon: "key.slash", color: .secondary, title: "Passwords", detail: "Browser extensions cannot access passwords", muted: true) {
|
|
579
|
+
FixedBadge("Unavailable")
|
|
580
|
+
}
|
|
468
581
|
}
|
|
469
582
|
|
|
470
583
|
SectionLabel(title: "Automation", detail: "Runs in the background", separated: true)
|
|
471
|
-
PreferenceRow(icon: "clock.badge.checkmark", color: Theme.accent, title: "Daily sync", detail: model.dailyEnabled ? "At the selected local time" : "Off") {
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
584
|
+
PreferenceRow(icon: "clock.badge.checkmark", color: Theme.accent, title: "Daily sync", detail: model.isBrowserlessTarget ? "Unavailable for cloud uploads" : (model.dailyEnabled ? "At the selected local time" : "Off")) {
|
|
585
|
+
if model.isBrowserlessTarget {
|
|
586
|
+
FixedBadge("Manual only")
|
|
587
|
+
} else {
|
|
588
|
+
HStack(spacing: 8) {
|
|
589
|
+
DatePicker("", selection: $model.scheduleTime, displayedComponents: .hourAndMinute)
|
|
590
|
+
.labelsHidden()
|
|
591
|
+
.datePickerStyle(.field)
|
|
592
|
+
.controlSize(.regular)
|
|
593
|
+
.disabled(!model.dailyEnabled || model.isWorking)
|
|
594
|
+
.frame(width: 112, alignment: .center)
|
|
595
|
+
.onChange(of: model.scheduleTime) { _ in
|
|
596
|
+
if model.dailyEnabled && !model.isWorking { model.saveSchedule() }
|
|
597
|
+
}
|
|
598
|
+
Toggle("", isOn: Binding(get: { model.dailyEnabled }, set: { model.setDailyEnabled($0) }))
|
|
599
|
+
.labelsHidden().toggleStyle(.switch).tint(Theme.active).disabled(model.isWorking)
|
|
600
|
+
}
|
|
484
601
|
}
|
|
485
602
|
}
|
|
486
603
|
RowDivider()
|
|
487
|
-
PreferenceRow(icon: "sunrise.fill", color: Theme.accent, title: "Sync at login", detail: model.loginSyncEnabled ? "Once whenever you sign in" : "Off") {
|
|
488
|
-
|
|
489
|
-
|
|
604
|
+
PreferenceRow(icon: "sunrise.fill", color: Theme.accent, title: "Sync at login", detail: model.isBrowserlessTarget ? "Unavailable for cloud uploads" : (model.loginSyncEnabled ? "Once whenever you sign in" : "Off")) {
|
|
605
|
+
if model.isBrowserlessTarget {
|
|
606
|
+
FixedBadge("Manual only")
|
|
607
|
+
} else {
|
|
608
|
+
Toggle("", isOn: Binding(get: { model.loginSyncEnabled }, set: { model.setLoginSyncEnabled($0) }))
|
|
609
|
+
.labelsHidden().toggleStyle(.switch).tint(Theme.active).disabled(model.isWorking)
|
|
610
|
+
}
|
|
490
611
|
}
|
|
491
612
|
RowDivider()
|
|
492
613
|
PreferenceRow(icon: "power", color: Theme.accent, title: "Open at login", detail: "Start the app after you sign in") {
|
|
@@ -523,6 +644,165 @@ struct PreferencesPanel: View {
|
|
|
523
644
|
}
|
|
524
645
|
}
|
|
525
646
|
}
|
|
647
|
+
|
|
648
|
+
private var browserlessPreflightDetail: String {
|
|
649
|
+
if model.isInspectingBrowserlessProfile { return "Measuring profile, local storage, and IndexedDB without reading their contents" }
|
|
650
|
+
if let assessment = model.browserlessAssessment {
|
|
651
|
+
return assessment.temporarySpaceWarning
|
|
652
|
+
? "\(assessment.summary) · free space may be too low for the temporary copy"
|
|
653
|
+
: assessment.summary
|
|
654
|
+
}
|
|
655
|
+
return "Calculated locally before upload; Browserless cloud artifacts are capped at 2 MB"
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
struct FixedBadge: View {
|
|
660
|
+
let title: String
|
|
661
|
+
|
|
662
|
+
init(_ title: String) { self.title = title }
|
|
663
|
+
|
|
664
|
+
var body: some View {
|
|
665
|
+
Text(title)
|
|
666
|
+
.font(.system(size: 10, weight: .semibold))
|
|
667
|
+
.foregroundStyle(.tertiary)
|
|
668
|
+
.padding(.horizontal, 8).padding(.vertical, 4)
|
|
669
|
+
.background(Color.secondary.opacity(0.08), in: Capsule())
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
struct ProfileSizeBadge: View {
|
|
674
|
+
let severity: String?
|
|
675
|
+
let scanning: Bool
|
|
676
|
+
|
|
677
|
+
var body: some View {
|
|
678
|
+
Text(label)
|
|
679
|
+
.font(.system(size: 9.5, weight: .semibold))
|
|
680
|
+
.foregroundStyle(color)
|
|
681
|
+
.padding(.horizontal, 8).padding(.vertical, 4)
|
|
682
|
+
.background(color.opacity(0.10), in: Capsule())
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
private var label: String {
|
|
686
|
+
if scanning { return "Scanning" }
|
|
687
|
+
switch severity {
|
|
688
|
+
case "elevated": return "Large"
|
|
689
|
+
case "high": return "Very large"
|
|
690
|
+
case "extreme": return "Extreme"
|
|
691
|
+
case "normal": return "Ready"
|
|
692
|
+
default: return "Not scanned"
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
private var color: Color {
|
|
697
|
+
if scanning { return .secondary }
|
|
698
|
+
switch severity {
|
|
699
|
+
case "elevated": return .orange
|
|
700
|
+
case "high", "extreme": return Theme.active
|
|
701
|
+
case "normal": return Theme.accent
|
|
702
|
+
default: return .secondary
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
struct BrowserlessSetupSheet: View {
|
|
708
|
+
@EnvironmentObject private var model: SyncModel
|
|
709
|
+
@Environment(\.dismiss) private var dismiss
|
|
710
|
+
@State private var token = ""
|
|
711
|
+
@State private var profileName = "browser-cookie-bridge"
|
|
712
|
+
@State private var region = "sfo"
|
|
713
|
+
@State private var onlyDomains = ""
|
|
714
|
+
@State private var cloudConsent = false
|
|
715
|
+
|
|
716
|
+
var body: some View {
|
|
717
|
+
VStack(alignment: .leading, spacing: 17) {
|
|
718
|
+
HStack(alignment: .top, spacing: 12) {
|
|
719
|
+
Image(nsImage: model.browserlessIcon)
|
|
720
|
+
.resizable()
|
|
721
|
+
.scaledToFit()
|
|
722
|
+
.frame(width: 24, height: 24)
|
|
723
|
+
.frame(width: 40, height: 40)
|
|
724
|
+
.background(Theme.accent.opacity(0.11), in: RoundedRectangle(cornerRadius: 11, style: .continuous))
|
|
725
|
+
VStack(alignment: .leading, spacing: 3) {
|
|
726
|
+
Text("Connect Browserless Cloud")
|
|
727
|
+
.font(.system(size: 18, weight: .bold, design: .rounded))
|
|
728
|
+
Text("Optional authenticated-profile upload")
|
|
729
|
+
.font(.system(size: 11.5)).foregroundStyle(.secondary)
|
|
730
|
+
}
|
|
731
|
+
Spacer()
|
|
732
|
+
FixedBadge(model.browserlessConfigured ? "Connected" : "Not connected")
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
VStack(alignment: .leading, spacing: 12) {
|
|
736
|
+
LabeledContent("API token") {
|
|
737
|
+
SecureField(model.browserlessConfigured ? "Saved in Keychain — leave blank to keep" : "Browserless API token", text: $token)
|
|
738
|
+
.textFieldStyle(.roundedBorder)
|
|
739
|
+
.frame(width: 270)
|
|
740
|
+
}
|
|
741
|
+
LabeledContent("Cloud profile") {
|
|
742
|
+
TextField("browser-cookie-bridge", text: $profileName)
|
|
743
|
+
.textFieldStyle(.roundedBorder)
|
|
744
|
+
.frame(width: 270)
|
|
745
|
+
}
|
|
746
|
+
LabeledContent("Region") {
|
|
747
|
+
Picker("", selection: $region) {
|
|
748
|
+
Text("San Francisco (SFO)").tag("sfo")
|
|
749
|
+
Text("London (LON)").tag("lon")
|
|
750
|
+
Text("Amsterdam (AMS)").tag("ams")
|
|
751
|
+
}
|
|
752
|
+
.labelsHidden()
|
|
753
|
+
.frame(width: 270)
|
|
754
|
+
}
|
|
755
|
+
LabeledContent("Only domains") {
|
|
756
|
+
TextField("Optional: example.com, app.example.com", text: $onlyDomains)
|
|
757
|
+
.textFieldStyle(.roundedBorder)
|
|
758
|
+
.frame(width: 270)
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
.font(.system(size: 11.5, weight: .medium))
|
|
762
|
+
.padding(14)
|
|
763
|
+
.background(Color(nsColor: .controlBackgroundColor).opacity(0.72), in: RoundedRectangle(cornerRadius: 12, style: .continuous))
|
|
764
|
+
.overlay(RoundedRectangle(cornerRadius: 12, style: .continuous).stroke(Color.primary.opacity(0.08)))
|
|
765
|
+
|
|
766
|
+
HStack(alignment: .top, spacing: 10) {
|
|
767
|
+
Image(systemName: "exclamationmark.cloud.fill")
|
|
768
|
+
.foregroundStyle(Theme.active)
|
|
769
|
+
Text("This destination is not local. Clicking Upload sends cookies, local storage, and IndexedDB from a temporary profile copy to Browserless. Upload only profiles you are authorized to share; use the domain allowlist for sensitive profiles. Background and login sync never perform cloud uploads. CLI telemetry is disabled.")
|
|
770
|
+
.font(.system(size: 10.5))
|
|
771
|
+
.foregroundStyle(.secondary)
|
|
772
|
+
.fixedSize(horizontal: false, vertical: true)
|
|
773
|
+
}
|
|
774
|
+
.padding(12)
|
|
775
|
+
.background(Theme.active.opacity(0.08), in: RoundedRectangle(cornerRadius: 10, style: .continuous))
|
|
776
|
+
|
|
777
|
+
Toggle("I understand the upload contents, have authority to share them, and accept responsibility for the Browserless copy.", isOn: $cloudConsent)
|
|
778
|
+
.toggleStyle(.checkbox)
|
|
779
|
+
.font(.system(size: 10.5, weight: .medium))
|
|
780
|
+
|
|
781
|
+
HStack {
|
|
782
|
+
Link("Get a Browserless API token", destination: URL(string: "https://www.browserless.io/account")!)
|
|
783
|
+
.font(.system(size: 10.5, weight: .medium))
|
|
784
|
+
Link("Privacy", destination: URL(string: "https://www.browserless.io/privacy-policy")!)
|
|
785
|
+
.font(.system(size: 10.5, weight: .medium))
|
|
786
|
+
if model.browserlessConfigured {
|
|
787
|
+
Button("Disconnect", role: .destructive) { model.disconnectBrowserless() }
|
|
788
|
+
}
|
|
789
|
+
Spacer()
|
|
790
|
+
Button("Cancel") { dismiss() }
|
|
791
|
+
Button("Save connection") {
|
|
792
|
+
model.saveBrowserlessSettings(token: token, profileName: profileName, region: region, onlyDomains: onlyDomains)
|
|
793
|
+
}
|
|
794
|
+
.keyboardShortcut(.defaultAction)
|
|
795
|
+
.disabled(profileName.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty || !cloudConsent)
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
.padding(22)
|
|
799
|
+
.frame(width: 520)
|
|
800
|
+
.onAppear {
|
|
801
|
+
profileName = model.browserlessProfileName
|
|
802
|
+
region = model.browserlessRegion
|
|
803
|
+
onlyDomains = model.browserlessOnlyDomains
|
|
804
|
+
}
|
|
805
|
+
}
|
|
526
806
|
}
|
|
527
807
|
|
|
528
808
|
struct ExtensionSetupSheet: View {
|
|
@@ -733,11 +1013,11 @@ struct StatusIndicator: View {
|
|
|
733
1013
|
}
|
|
734
1014
|
|
|
735
1015
|
private var label: String {
|
|
736
|
-
switch state { case .ready: "Ready"; case .syncing: "Syncing"; case .success: "Synced"; case .warning: "Partial"; case .error: "Needs action" }
|
|
1016
|
+
switch state { case .ready: "Ready"; case .syncing: "Syncing"; case .success: "Synced"; case .canceled: "Canceled"; case .warning: "Partial"; case .error: "Needs action" }
|
|
737
1017
|
}
|
|
738
1018
|
|
|
739
1019
|
private var color: Color {
|
|
740
|
-
switch state { case .ready, .syncing, .success: Theme.accent; case .warning: .orange; case .error: .red }
|
|
1020
|
+
switch state { case .ready, .syncing, .success: Theme.accent; case .canceled: .secondary; case .warning: .orange; case .error: .red }
|
|
741
1021
|
}
|
|
742
1022
|
}
|
|
743
1023
|
|