|
|
@@ -86,9 +86,15 @@ final class ViewController: NSViewController {
|
|
86
|
86
|
private weak var scheduleFilterDropdown: NSPopUpButton?
|
|
87
|
87
|
private weak var scheduleGoogleAuthButton: NSButton?
|
|
88
|
88
|
private var scheduleGoogleAuthButtonWidthConstraint: NSLayoutConstraint?
|
|
|
89
|
+ private var scheduleGoogleAuthButtonHeightConstraint: NSLayoutConstraint?
|
|
|
90
|
+ /// Circular avatar size when signed in (top-right, Google-style).
|
|
|
91
|
+ private let scheduleGoogleSignedInAvatarSize: CGFloat = 36
|
|
89
|
92
|
private var scheduleGoogleAuthHovering = false
|
|
90
|
93
|
private var scheduleCurrentProfile: GoogleProfileDisplay?
|
|
|
94
|
+ /// Larger copy of the header avatar for the account popover (optional).
|
|
|
95
|
+ private var scheduleProfileMenuAvatar: NSImage?
|
|
91
|
96
|
private var scheduleProfileImageTask: Task<Void, Never>?
|
|
|
97
|
+ private var googleAccountPopover: NSPopover?
|
|
92
|
98
|
|
|
93
|
99
|
/// In-app browser navigation: `.allowAll` or `.whitelist(hostSuffixes:)` (e.g. `["google.com"]` matches `meet.google.com`).
|
|
94
|
100
|
private let inAppBrowserDefaultPolicy: InAppBrowserURLPolicy = .allowAll
|
|
|
@@ -436,6 +442,9 @@ private extension ViewController {
|
|
436
|
442
|
paywallPlanViews.removeAll()
|
|
437
|
443
|
premiumPlanByView.removeAll()
|
|
438
|
444
|
|
|
|
445
|
+ googleAccountPopover?.performClose(nil)
|
|
|
446
|
+ googleAccountPopover = nil
|
|
|
447
|
+
|
|
439
|
448
|
mainContentHost = nil
|
|
440
|
449
|
view.subviews.forEach { $0.removeFromSuperview() }
|
|
441
|
450
|
setupRootView()
|
|
|
@@ -1910,7 +1919,10 @@ private extension ViewController {
|
|
1910
|
1919
|
button.lineBreakMode = .byTruncatingTail
|
|
1911
|
1920
|
button.contentTintColor = palette.textPrimary
|
|
1912
|
1921
|
button.imageScaling = .scaleNone
|
|
1913
|
|
- button.heightAnchor.constraint(equalToConstant: 42).isActive = true
|
|
|
1922
|
+ button.layer?.masksToBounds = true
|
|
|
1923
|
+ let heightConstraint = button.heightAnchor.constraint(equalToConstant: 42)
|
|
|
1924
|
+ heightConstraint.isActive = true
|
|
|
1925
|
+ scheduleGoogleAuthButtonHeightConstraint = heightConstraint
|
|
1914
|
1926
|
let widthConstraint = button.widthAnchor.constraint(equalToConstant: 248)
|
|
1915
|
1927
|
widthConstraint.isActive = true
|
|
1916
|
1928
|
scheduleGoogleAuthButtonWidthConstraint = widthConstraint
|
|
|
@@ -2293,6 +2305,259 @@ private final class HoverButton: NSButton {
|
|
2293
|
2305
|
}
|
|
2294
|
2306
|
}
|
|
2295
|
2307
|
|
|
|
2308
|
+private func circularNSImage(_ image: NSImage, diameter: CGFloat) -> NSImage {
|
|
|
2309
|
+ let size = NSSize(width: diameter, height: diameter)
|
|
|
2310
|
+ let result = NSImage(size: size)
|
|
|
2311
|
+ result.lockFocus()
|
|
|
2312
|
+ if let ctx = NSGraphicsContext.current {
|
|
|
2313
|
+ ctx.imageInterpolation = .high
|
|
|
2314
|
+ }
|
|
|
2315
|
+ let rect = NSRect(origin: .zero, size: size)
|
|
|
2316
|
+ let path = NSBezierPath(ovalIn: rect)
|
|
|
2317
|
+ path.addClip()
|
|
|
2318
|
+ let src = image.size.width > 0 && image.size.height > 0
|
|
|
2319
|
+ ? NSRect(origin: .zero, size: image.size)
|
|
|
2320
|
+ : NSRect(origin: .zero, size: size)
|
|
|
2321
|
+ image.draw(in: rect, from: src, operation: .copy, fraction: 1.0)
|
|
|
2322
|
+ result.unlockFocus()
|
|
|
2323
|
+ result.isTemplate = false
|
|
|
2324
|
+ return result
|
|
|
2325
|
+}
|
|
|
2326
|
+
|
|
|
2327
|
+private final class GoogleAccountMenuViewController: NSViewController {
|
|
|
2328
|
+ private let palette: Palette
|
|
|
2329
|
+ private let darkModeEnabled: Bool
|
|
|
2330
|
+ private let displayName: String
|
|
|
2331
|
+ private let email: String
|
|
|
2332
|
+ private let avatar: NSImage?
|
|
|
2333
|
+ private let onSignOut: () -> Void
|
|
|
2334
|
+
|
|
|
2335
|
+ init(
|
|
|
2336
|
+ palette: Palette,
|
|
|
2337
|
+ darkModeEnabled: Bool,
|
|
|
2338
|
+ displayName: String,
|
|
|
2339
|
+ email: String,
|
|
|
2340
|
+ avatar: NSImage?,
|
|
|
2341
|
+ onSignOut: @escaping () -> Void
|
|
|
2342
|
+ ) {
|
|
|
2343
|
+ self.palette = palette
|
|
|
2344
|
+ self.darkModeEnabled = darkModeEnabled
|
|
|
2345
|
+ self.displayName = displayName
|
|
|
2346
|
+ self.email = email
|
|
|
2347
|
+ self.avatar = avatar
|
|
|
2348
|
+ self.onSignOut = onSignOut
|
|
|
2349
|
+ super.init(nibName: nil, bundle: nil)
|
|
|
2350
|
+ view = makeContentView()
|
|
|
2351
|
+ view.appearance = NSAppearance(named: darkModeEnabled ? .darkAqua : .aqua)
|
|
|
2352
|
+ preferredContentSize = NSSize(width: 300, height: 158)
|
|
|
2353
|
+ }
|
|
|
2354
|
+
|
|
|
2355
|
+ @available(*, unavailable)
|
|
|
2356
|
+ required init?(coder: NSCoder) {
|
|
|
2357
|
+ nil
|
|
|
2358
|
+ }
|
|
|
2359
|
+
|
|
|
2360
|
+ private func makeContentView() -> NSView {
|
|
|
2361
|
+ let root = NSView()
|
|
|
2362
|
+ root.translatesAutoresizingMaskIntoConstraints = false
|
|
|
2363
|
+
|
|
|
2364
|
+ let card = NSView()
|
|
|
2365
|
+ card.translatesAutoresizingMaskIntoConstraints = false
|
|
|
2366
|
+ card.wantsLayer = true
|
|
|
2367
|
+ card.layer?.cornerRadius = 14
|
|
|
2368
|
+ card.layer?.backgroundColor = palette.sectionCard.cgColor
|
|
|
2369
|
+ card.layer?.borderColor = palette.inputBorder.cgColor
|
|
|
2370
|
+ card.layer?.borderWidth = 1
|
|
|
2371
|
+ card.layer?.shadowColor = NSColor.black.cgColor
|
|
|
2372
|
+ card.layer?.shadowOpacity = darkModeEnabled ? 0.5 : 0.2
|
|
|
2373
|
+ card.layer?.shadowOffset = CGSize(width: 0, height: 6)
|
|
|
2374
|
+ card.layer?.shadowRadius = 18
|
|
|
2375
|
+ card.layer?.masksToBounds = false
|
|
|
2376
|
+
|
|
|
2377
|
+ root.addSubview(card)
|
|
|
2378
|
+ NSLayoutConstraint.activate([
|
|
|
2379
|
+ card.leadingAnchor.constraint(equalTo: root.leadingAnchor, constant: 8),
|
|
|
2380
|
+ card.trailingAnchor.constraint(equalTo: root.trailingAnchor, constant: -8),
|
|
|
2381
|
+ card.topAnchor.constraint(equalTo: root.topAnchor, constant: 8),
|
|
|
2382
|
+ card.bottomAnchor.constraint(equalTo: root.bottomAnchor, constant: -8),
|
|
|
2383
|
+ root.widthAnchor.constraint(equalToConstant: 300)
|
|
|
2384
|
+ ])
|
|
|
2385
|
+
|
|
|
2386
|
+ let inner = NSStackView()
|
|
|
2387
|
+ inner.translatesAutoresizingMaskIntoConstraints = false
|
|
|
2388
|
+ inner.orientation = .vertical
|
|
|
2389
|
+ inner.spacing = 0
|
|
|
2390
|
+ inner.alignment = .leading
|
|
|
2391
|
+ card.addSubview(inner)
|
|
|
2392
|
+
|
|
|
2393
|
+ NSLayoutConstraint.activate([
|
|
|
2394
|
+ inner.leadingAnchor.constraint(equalTo: card.leadingAnchor, constant: 18),
|
|
|
2395
|
+ inner.trailingAnchor.constraint(equalTo: card.trailingAnchor, constant: -18),
|
|
|
2396
|
+ inner.topAnchor.constraint(equalTo: card.topAnchor, constant: 18),
|
|
|
2397
|
+ inner.bottomAnchor.constraint(equalTo: card.bottomAnchor, constant: -10)
|
|
|
2398
|
+ ])
|
|
|
2399
|
+
|
|
|
2400
|
+ let headerRow = NSView()
|
|
|
2401
|
+ headerRow.translatesAutoresizingMaskIntoConstraints = false
|
|
|
2402
|
+
|
|
|
2403
|
+ let avatarBox = NSView()
|
|
|
2404
|
+ avatarBox.translatesAutoresizingMaskIntoConstraints = false
|
|
|
2405
|
+ avatarBox.wantsLayer = true
|
|
|
2406
|
+ avatarBox.layer?.cornerRadius = 24
|
|
|
2407
|
+ avatarBox.layer?.masksToBounds = true
|
|
|
2408
|
+ avatarBox.layer?.borderColor = palette.inputBorder.cgColor
|
|
|
2409
|
+ avatarBox.layer?.borderWidth = 1
|
|
|
2410
|
+
|
|
|
2411
|
+ let avatarView = NSImageView()
|
|
|
2412
|
+ avatarView.translatesAutoresizingMaskIntoConstraints = false
|
|
|
2413
|
+ avatarView.imageScaling = .scaleAxesIndependently
|
|
|
2414
|
+ avatarView.image = resolvedAvatarImage()
|
|
|
2415
|
+
|
|
|
2416
|
+ avatarBox.addSubview(avatarView)
|
|
|
2417
|
+ NSLayoutConstraint.activate([
|
|
|
2418
|
+ avatarBox.widthAnchor.constraint(equalToConstant: 48),
|
|
|
2419
|
+ avatarBox.heightAnchor.constraint(equalToConstant: 48),
|
|
|
2420
|
+ avatarView.leadingAnchor.constraint(equalTo: avatarBox.leadingAnchor),
|
|
|
2421
|
+ avatarView.trailingAnchor.constraint(equalTo: avatarBox.trailingAnchor),
|
|
|
2422
|
+ avatarView.topAnchor.constraint(equalTo: avatarBox.topAnchor),
|
|
|
2423
|
+ avatarView.bottomAnchor.constraint(equalTo: avatarBox.bottomAnchor)
|
|
|
2424
|
+ ])
|
|
|
2425
|
+
|
|
|
2426
|
+ let textColumn = NSStackView()
|
|
|
2427
|
+ textColumn.translatesAutoresizingMaskIntoConstraints = false
|
|
|
2428
|
+ textColumn.orientation = .vertical
|
|
|
2429
|
+ textColumn.spacing = 3
|
|
|
2430
|
+ textColumn.alignment = .leading
|
|
|
2431
|
+
|
|
|
2432
|
+ let nameField = NSTextField(labelWithString: displayName)
|
|
|
2433
|
+ nameField.translatesAutoresizingMaskIntoConstraints = false
|
|
|
2434
|
+ nameField.font = NSFont.systemFont(ofSize: 15, weight: .semibold)
|
|
|
2435
|
+ nameField.textColor = palette.textPrimary
|
|
|
2436
|
+ nameField.lineBreakMode = .byTruncatingTail
|
|
|
2437
|
+ nameField.maximumNumberOfLines = 1
|
|
|
2438
|
+ nameField.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
|
|
|
2439
|
+
|
|
|
2440
|
+ let emailField = NSTextField(labelWithString: email)
|
|
|
2441
|
+ emailField.translatesAutoresizingMaskIntoConstraints = false
|
|
|
2442
|
+ emailField.font = NSFont.systemFont(ofSize: 12, weight: .regular)
|
|
|
2443
|
+ emailField.textColor = palette.textTertiary
|
|
|
2444
|
+ emailField.lineBreakMode = .byTruncatingTail
|
|
|
2445
|
+ emailField.maximumNumberOfLines = 1
|
|
|
2446
|
+ emailField.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
|
|
|
2447
|
+
|
|
|
2448
|
+ textColumn.addArrangedSubview(nameField)
|
|
|
2449
|
+ textColumn.addArrangedSubview(emailField)
|
|
|
2450
|
+
|
|
|
2451
|
+ headerRow.addSubview(avatarBox)
|
|
|
2452
|
+ headerRow.addSubview(textColumn)
|
|
|
2453
|
+
|
|
|
2454
|
+ NSLayoutConstraint.activate([
|
|
|
2455
|
+ avatarBox.leadingAnchor.constraint(equalTo: headerRow.leadingAnchor),
|
|
|
2456
|
+ avatarBox.topAnchor.constraint(equalTo: headerRow.topAnchor),
|
|
|
2457
|
+ avatarBox.bottomAnchor.constraint(lessThanOrEqualTo: headerRow.bottomAnchor),
|
|
|
2458
|
+
|
|
|
2459
|
+ textColumn.leadingAnchor.constraint(equalTo: avatarBox.trailingAnchor, constant: 14),
|
|
|
2460
|
+ textColumn.trailingAnchor.constraint(equalTo: headerRow.trailingAnchor),
|
|
|
2461
|
+ textColumn.centerYAnchor.constraint(equalTo: avatarBox.centerYAnchor),
|
|
|
2462
|
+ textColumn.topAnchor.constraint(greaterThanOrEqualTo: headerRow.topAnchor),
|
|
|
2463
|
+ textColumn.bottomAnchor.constraint(lessThanOrEqualTo: headerRow.bottomAnchor)
|
|
|
2464
|
+ ])
|
|
|
2465
|
+
|
|
|
2466
|
+ inner.addArrangedSubview(headerRow)
|
|
|
2467
|
+ headerRow.widthAnchor.constraint(equalTo: inner.widthAnchor).isActive = true
|
|
|
2468
|
+
|
|
|
2469
|
+ inner.setCustomSpacing(14, after: headerRow)
|
|
|
2470
|
+
|
|
|
2471
|
+ let separator = NSView()
|
|
|
2472
|
+ separator.translatesAutoresizingMaskIntoConstraints = false
|
|
|
2473
|
+ separator.wantsLayer = true
|
|
|
2474
|
+ separator.layer?.backgroundColor = palette.separator.cgColor
|
|
|
2475
|
+ separator.heightAnchor.constraint(equalToConstant: 1).isActive = true
|
|
|
2476
|
+ inner.addArrangedSubview(separator)
|
|
|
2477
|
+ separator.widthAnchor.constraint(equalTo: inner.widthAnchor).isActive = true
|
|
|
2478
|
+
|
|
|
2479
|
+ inner.setCustomSpacing(6, after: separator)
|
|
|
2480
|
+
|
|
|
2481
|
+ let signOutRow = HoverTrackingView()
|
|
|
2482
|
+ signOutRow.translatesAutoresizingMaskIntoConstraints = false
|
|
|
2483
|
+ signOutRow.heightAnchor.constraint(equalToConstant: 44).isActive = true
|
|
|
2484
|
+ signOutRow.wantsLayer = true
|
|
|
2485
|
+ signOutRow.layer?.cornerRadius = 10
|
|
|
2486
|
+
|
|
|
2487
|
+ let signOutIcon = NSImageView()
|
|
|
2488
|
+ signOutIcon.translatesAutoresizingMaskIntoConstraints = false
|
|
|
2489
|
+ signOutIcon.imageScaling = .scaleProportionallyDown
|
|
|
2490
|
+ if let sym = NSImage(systemSymbolName: "rectangle.portrait.and.arrow.right", accessibilityDescription: nil) {
|
|
|
2491
|
+ signOutIcon.image = sym
|
|
|
2492
|
+ signOutIcon.contentTintColor = palette.textSecondary
|
|
|
2493
|
+ signOutIcon.symbolConfiguration = NSImage.SymbolConfiguration(pointSize: 14, weight: .medium)
|
|
|
2494
|
+ }
|
|
|
2495
|
+
|
|
|
2496
|
+ let signOutLabel = NSTextField(labelWithString: "Log out")
|
|
|
2497
|
+ signOutLabel.translatesAutoresizingMaskIntoConstraints = false
|
|
|
2498
|
+ signOutLabel.font = NSFont.systemFont(ofSize: 14, weight: .medium)
|
|
|
2499
|
+ signOutLabel.textColor = palette.textPrimary
|
|
|
2500
|
+
|
|
|
2501
|
+ signOutRow.addSubview(signOutIcon)
|
|
|
2502
|
+ signOutRow.addSubview(signOutLabel)
|
|
|
2503
|
+
|
|
|
2504
|
+ NSLayoutConstraint.activate([
|
|
|
2505
|
+ signOutIcon.leadingAnchor.constraint(equalTo: signOutRow.leadingAnchor, constant: 10),
|
|
|
2506
|
+ signOutIcon.centerYAnchor.constraint(equalTo: signOutRow.centerYAnchor),
|
|
|
2507
|
+ signOutIcon.widthAnchor.constraint(equalToConstant: 20),
|
|
|
2508
|
+ signOutIcon.heightAnchor.constraint(equalToConstant: 20),
|
|
|
2509
|
+
|
|
|
2510
|
+ signOutLabel.leadingAnchor.constraint(equalTo: signOutIcon.trailingAnchor, constant: 10),
|
|
|
2511
|
+ signOutLabel.centerYAnchor.constraint(equalTo: signOutRow.centerYAnchor),
|
|
|
2512
|
+ signOutLabel.trailingAnchor.constraint(lessThanOrEqualTo: signOutRow.trailingAnchor, constant: -10)
|
|
|
2513
|
+ ])
|
|
|
2514
|
+
|
|
|
2515
|
+ let signOutClick = NSClickGestureRecognizer(target: self, action: #selector(signOutClicked))
|
|
|
2516
|
+ signOutRow.addGestureRecognizer(signOutClick)
|
|
|
2517
|
+
|
|
|
2518
|
+ signOutRow.onHoverChanged = { [weak self] hovering in
|
|
|
2519
|
+ guard let self else { return }
|
|
|
2520
|
+ signOutRow.layer?.backgroundColor = (hovering ? self.palette.inputBackground : NSColor.clear).cgColor
|
|
|
2521
|
+ }
|
|
|
2522
|
+ signOutRow.onHoverChanged?(false)
|
|
|
2523
|
+
|
|
|
2524
|
+ inner.addArrangedSubview(signOutRow)
|
|
|
2525
|
+ signOutRow.widthAnchor.constraint(equalTo: inner.widthAnchor).isActive = true
|
|
|
2526
|
+
|
|
|
2527
|
+ return root
|
|
|
2528
|
+ }
|
|
|
2529
|
+
|
|
|
2530
|
+ private func resolvedAvatarImage() -> NSImage {
|
|
|
2531
|
+ if let a = avatar {
|
|
|
2532
|
+ return circularNSImage(a, diameter: 48)
|
|
|
2533
|
+ }
|
|
|
2534
|
+ return initialLetterAvatar()
|
|
|
2535
|
+ }
|
|
|
2536
|
+
|
|
|
2537
|
+ private func initialLetterAvatar() -> NSImage {
|
|
|
2538
|
+ let d: CGFloat = 48
|
|
|
2539
|
+ let letter = displayName.trimmingCharacters(in: .whitespacesAndNewlines).first.map { String($0).uppercased() } ?? "?"
|
|
|
2540
|
+ let img = NSImage(size: NSSize(width: d, height: d))
|
|
|
2541
|
+ img.lockFocus()
|
|
|
2542
|
+ palette.primaryBlue.setFill()
|
|
|
2543
|
+ NSBezierPath(ovalIn: NSRect(x: 0, y: 0, width: d, height: d)).fill()
|
|
|
2544
|
+ let attrs: [NSAttributedString.Key: Any] = [
|
|
|
2545
|
+ .font: NSFont.systemFont(ofSize: 20, weight: .semibold),
|
|
|
2546
|
+ .foregroundColor: NSColor.white
|
|
|
2547
|
+ ]
|
|
|
2548
|
+ let sz = (letter as NSString).size(withAttributes: attrs)
|
|
|
2549
|
+ let origin = NSPoint(x: (d - sz.width) / 2, y: (d - sz.height) / 2)
|
|
|
2550
|
+ (letter as NSString).draw(at: origin, withAttributes: attrs)
|
|
|
2551
|
+ img.unlockFocus()
|
|
|
2552
|
+ img.isTemplate = false
|
|
|
2553
|
+ return img
|
|
|
2554
|
+ }
|
|
|
2555
|
+
|
|
|
2556
|
+ @objc private func signOutClicked() {
|
|
|
2557
|
+ onSignOut()
|
|
|
2558
|
+ }
|
|
|
2559
|
+}
|
|
|
2560
|
+
|
|
2296
|
2561
|
private final class SettingsMenuViewController: NSViewController {
|
|
2297
|
2562
|
private let palette: Palette
|
|
2298
|
2563
|
private let typography: Typography
|
|
|
@@ -2923,27 +3188,41 @@ private extension ViewController {
|
|
2923
|
3188
|
|
|
2924
|
3189
|
private func showGoogleAccountMenu() {
|
|
2925
|
3190
|
guard let button = scheduleGoogleAuthButton else { return }
|
|
2926
|
|
- let menu = NSMenu()
|
|
|
3191
|
+ if googleAccountPopover?.isShown == true {
|
|
|
3192
|
+ googleAccountPopover?.performClose(nil)
|
|
|
3193
|
+ googleAccountPopover = nil
|
|
|
3194
|
+ return
|
|
|
3195
|
+ }
|
|
|
3196
|
+
|
|
|
3197
|
+ let popover = NSPopover()
|
|
|
3198
|
+ popover.behavior = .transient
|
|
|
3199
|
+ popover.animates = true
|
|
|
3200
|
+ popover.appearance = NSAppearance(named: darkModeEnabled ? .darkAqua : .aqua)
|
|
2927
|
3201
|
|
|
2928
|
3202
|
let name = scheduleCurrentProfile?.name ?? "Google account"
|
|
2929
|
3203
|
let email = scheduleCurrentProfile?.email ?? "Signed in"
|
|
2930
|
|
- let accountItem = NSMenuItem(title: "\(name) (\(email))", action: nil, keyEquivalent: "")
|
|
2931
|
|
- accountItem.isEnabled = false
|
|
2932
|
|
- menu.addItem(accountItem)
|
|
2933
|
|
- menu.addItem(.separator())
|
|
|
3204
|
+ let avatar = scheduleProfileMenuAvatar
|
|
2934
|
3205
|
|
|
2935
|
|
- let logoutItem = NSMenuItem(title: "Logout", action: #selector(scheduleLogoutSelected(_:)), keyEquivalent: "")
|
|
2936
|
|
- logoutItem.target = self
|
|
2937
|
|
- menu.addItem(logoutItem)
|
|
|
3206
|
+ popover.contentViewController = GoogleAccountMenuViewController(
|
|
|
3207
|
+ palette: palette,
|
|
|
3208
|
+ darkModeEnabled: darkModeEnabled,
|
|
|
3209
|
+ displayName: name,
|
|
|
3210
|
+ email: email,
|
|
|
3211
|
+ avatar: avatar,
|
|
|
3212
|
+ onSignOut: { [weak self] in
|
|
|
3213
|
+ self?.googleAccountPopover?.performClose(nil)
|
|
|
3214
|
+ self?.googleAccountPopover = nil
|
|
|
3215
|
+ self?.performGoogleSignOut()
|
|
|
3216
|
+ }
|
|
|
3217
|
+ )
|
|
2938
|
3218
|
|
|
2939
|
|
- let point = NSPoint(x: 0, y: button.bounds.height + 2)
|
|
2940
|
|
- menu.popUp(positioning: nil, at: point, in: button)
|
|
|
3219
|
+ googleAccountPopover = popover
|
|
|
3220
|
+ popover.show(relativeTo: button.bounds, of: button, preferredEdge: .minY)
|
|
2941
|
3221
|
}
|
|
2942
|
3222
|
|
|
2943
|
|
- @objc private func scheduleLogoutSelected(_ sender: NSMenuItem) {
|
|
|
3223
|
+ private func performGoogleSignOut() {
|
|
2944
|
3224
|
do {
|
|
2945
|
3225
|
try googleOAuth.signOut()
|
|
2946
|
|
- updateGoogleAuthButtonTitle()
|
|
2947
|
3226
|
applyGoogleProfile(nil)
|
|
2948
|
3227
|
scheduleDateHeadingLabel?.stringValue = "Connect Google to see meetings"
|
|
2949
|
3228
|
if let stack = scheduleCardsStack {
|
|
|
@@ -2959,24 +3238,44 @@ private extension ViewController {
|
|
2959
|
3238
|
guard let button = scheduleGoogleAuthButton else { return }
|
|
2960
|
3239
|
|
|
2961
|
3240
|
let profileName = scheduleCurrentProfile?.name ?? "Google account"
|
|
2962
|
|
- let profileEmail = scheduleCurrentProfile?.email ?? "Sign in with Google"
|
|
2963
|
|
- let title = signedIn ? "\(profileName) · \(profileEmail)" : "Sign in with Google"
|
|
2964
|
|
- let titleFont = NSFont.systemFont(ofSize: 14, weight: .medium)
|
|
2965
|
|
- let titleColor = darkModeEnabled ? NSColor(calibratedWhite: 0.96, alpha: 1) : NSColor(calibratedRed: 0.13, green: 0.14, blue: 0.16, alpha: 1)
|
|
2966
|
|
- button.attributedTitle = NSAttributedString(string: title, attributes: [
|
|
2967
|
|
- .font: titleFont,
|
|
2968
|
|
- .foregroundColor: titleColor
|
|
2969
|
|
- ])
|
|
2970
|
|
- let textWidth = (title as NSString).size(withAttributes: [.font: titleFont]).width
|
|
2971
|
|
- let idealWidth = ceil(textWidth + 80) // icon + spacing + side padding
|
|
2972
|
|
- scheduleGoogleAuthButtonWidthConstraint?.constant = min(320, max(188, idealWidth))
|
|
2973
|
3241
|
|
|
2974
|
3242
|
if signedIn {
|
|
|
3243
|
+ button.setAccessibilityLabel("\(profileName), Google account")
|
|
|
3244
|
+ button.attributedTitle = NSAttributedString(string: "")
|
|
|
3245
|
+ button.imagePosition = .imageOnly
|
|
|
3246
|
+ button.imageScaling = .scaleProportionallyDown
|
|
|
3247
|
+ button.symbolConfiguration = nil
|
|
|
3248
|
+ scheduleGoogleAuthButtonHeightConstraint?.constant = scheduleGoogleSignedInAvatarSize
|
|
|
3249
|
+ scheduleGoogleAuthButtonWidthConstraint?.constant = scheduleGoogleSignedInAvatarSize
|
|
|
3250
|
+ button.layer?.cornerRadius = scheduleGoogleSignedInAvatarSize / 2
|
|
|
3251
|
+
|
|
2975
|
3252
|
let symbol = NSImage(systemSymbolName: "person.crop.circle.fill", accessibilityDescription: "Profile")
|
|
2976
|
|
- button.image = symbol.flatMap { paddedTrailingImage($0, iconSize: NSSize(width: 22, height: 22), trailingPadding: 8) }
|
|
2977
|
|
- button.symbolConfiguration = NSImage.SymbolConfiguration(pointSize: 22, weight: .regular)
|
|
2978
|
|
- button.contentTintColor = palette.textPrimary
|
|
|
3253
|
+ if let symbol {
|
|
|
3254
|
+ let sized = resizedImage(symbol, to: NSSize(width: scheduleGoogleSignedInAvatarSize, height: scheduleGoogleSignedInAvatarSize))
|
|
|
3255
|
+ button.image = sized
|
|
|
3256
|
+ button.contentTintColor = palette.textSecondary
|
|
|
3257
|
+ } else {
|
|
|
3258
|
+ button.image = nil
|
|
|
3259
|
+ button.contentTintColor = nil
|
|
|
3260
|
+ }
|
|
|
3261
|
+ scheduleProfileMenuAvatar = button.image
|
|
2979
|
3262
|
} else {
|
|
|
3263
|
+ button.setAccessibilityLabel("Sign in with Google")
|
|
|
3264
|
+ let title = "Sign in with Google"
|
|
|
3265
|
+ let titleFont = NSFont.systemFont(ofSize: 14, weight: .medium)
|
|
|
3266
|
+ let titleColor = darkModeEnabled ? NSColor(calibratedWhite: 0.96, alpha: 1) : NSColor(calibratedRed: 0.13, green: 0.14, blue: 0.16, alpha: 1)
|
|
|
3267
|
+ button.attributedTitle = NSAttributedString(string: title, attributes: [
|
|
|
3268
|
+ .font: titleFont,
|
|
|
3269
|
+ .foregroundColor: titleColor
|
|
|
3270
|
+ ])
|
|
|
3271
|
+ let textWidth = (title as NSString).size(withAttributes: [.font: titleFont]).width
|
|
|
3272
|
+ let idealWidth = ceil(textWidth + 80)
|
|
|
3273
|
+ scheduleGoogleAuthButtonWidthConstraint?.constant = min(320, max(188, idealWidth))
|
|
|
3274
|
+ scheduleGoogleAuthButtonHeightConstraint?.constant = 42
|
|
|
3275
|
+ button.layer?.cornerRadius = 21
|
|
|
3276
|
+
|
|
|
3277
|
+ button.imagePosition = .imageLeading
|
|
|
3278
|
+ button.imageScaling = .scaleNone
|
|
2980
|
3279
|
if let g = NSImage(named: "GoogleGLogo") {
|
|
2981
|
3280
|
button.image = paddedTrailingImage(g, iconSize: NSSize(width: 22, height: 22), trailingPadding: 8)
|
|
2982
|
3281
|
} else {
|
|
|
@@ -2984,7 +3283,6 @@ private extension ViewController {
|
|
2984
|
3283
|
}
|
|
2985
|
3284
|
button.contentTintColor = nil
|
|
2986
|
3285
|
}
|
|
2987
|
|
- button.contentTintColor = signedIn ? palette.textPrimary : nil
|
|
2988
|
3286
|
|
|
2989
|
3287
|
applyGoogleAuthButtonSurface()
|
|
2990
|
3288
|
}
|
|
|
@@ -3002,19 +3300,26 @@ private extension ViewController {
|
|
3002
|
3300
|
private func applyGoogleProfile(_ profile: GoogleProfileDisplay?) {
|
|
3003
|
3301
|
scheduleProfileImageTask?.cancel()
|
|
3004
|
3302
|
scheduleProfileImageTask = nil
|
|
|
3303
|
+ if profile == nil {
|
|
|
3304
|
+ scheduleProfileMenuAvatar = nil
|
|
|
3305
|
+ }
|
|
3005
|
3306
|
scheduleCurrentProfile = profile
|
|
3006
|
3307
|
|
|
3007
|
3308
|
updateGoogleAuthButtonTitle()
|
|
3008
|
3309
|
|
|
3009
|
3310
|
guard let profile, let pictureURL = profile.pictureURL else { return }
|
|
|
3311
|
+ let avatarDiameter = scheduleGoogleSignedInAvatarSize
|
|
3010
|
3312
|
scheduleProfileImageTask = Task { [weak self] in
|
|
3011
|
3313
|
do {
|
|
3012
|
3314
|
let (data, _) = try await URLSession.shared.data(from: pictureURL)
|
|
3013
|
3315
|
if Task.isCancelled { return }
|
|
3014
|
3316
|
guard let image = NSImage(data: data) else { return }
|
|
3015
|
|
- await MainActor.run {
|
|
3016
|
|
- self?.scheduleGoogleAuthButton?.image = self?.paddedTrailingImage(image, iconSize: NSSize(width: 22, height: 22), trailingPadding: 8)
|
|
3017
|
|
- self?.scheduleGoogleAuthButton?.contentTintColor = nil
|
|
|
3317
|
+ await MainActor.run { [weak self] in
|
|
|
3318
|
+ guard let self else { return }
|
|
|
3319
|
+ let rounded = self.circularProfileImage(image, diameter: avatarDiameter)
|
|
|
3320
|
+ self.scheduleProfileMenuAvatar = circularNSImage(rounded, diameter: 48)
|
|
|
3321
|
+ self.scheduleGoogleAuthButton?.image = rounded
|
|
|
3322
|
+ self.scheduleGoogleAuthButton?.contentTintColor = nil
|
|
3018
|
3323
|
}
|
|
3019
|
3324
|
} catch {
|
|
3020
|
3325
|
// Keep placeholder avatar if image fetch fails.
|
|
|
@@ -3034,6 +3339,11 @@ private extension ViewController {
|
|
3034
|
3339
|
return result
|
|
3035
|
3340
|
}
|
|
3036
|
3341
|
|
|
|
3342
|
+ /// Clips a photo to a circle for the signed-in avatar (Google userinfo `picture` URLs are usually square).
|
|
|
3343
|
+ private func circularProfileImage(_ image: NSImage, diameter: CGFloat) -> NSImage {
|
|
|
3344
|
+ circularNSImage(image, diameter: diameter)
|
|
|
3345
|
+ }
|
|
|
3346
|
+
|
|
3037
|
3347
|
private func paddedTrailingImage(_ image: NSImage, iconSize: NSSize, trailingPadding: CGFloat) -> NSImage {
|
|
3038
|
3348
|
let base = resizedImage(image, to: iconSize)
|
|
3039
|
3349
|
let canvas = NSSize(width: iconSize.width + trailingPadding, height: iconSize.height)
|