Bläddra i källkod

Refine Google sign-in button styling and layout.

Move auth control to top-right, add polished dynamic sizing and spacing, use high-resolution transparent Google logo assets, and add account popup logout behavior with stable build fixes.

Made-with: Cursor
huzaifahayat12 3 månader sedan
förälder
incheckning
156b04e0e3

+ 27 - 0
meetings_app/Assets.xcassets/GoogleGLogo.imageset/Contents.json

@@ -0,0 +1,27 @@
+{
+  "images" : [
+    {
+      "filename" : "google_g.png",
+      "idiom" : "universal",
+      "scale" : "1x"
+    },
+    {
+      "filename" : "google_g@2x.png",
+      "idiom" : "universal",
+      "scale" : "2x"
+    },
+    {
+      "filename" : "google_g@3x.png",
+      "idiom" : "universal",
+      "scale" : "3x"
+    }
+  ],
+  "info" : {
+    "author" : "xcode",
+    "version" : 1
+  },
+  "properties" : {
+    "preserves-vector-representation" : false,
+    "template-rendering-intent" : "original"
+  }
+}

BIN
meetings_app/Assets.xcassets/GoogleGLogo.imageset/google_g.png


BIN
meetings_app/Assets.xcassets/GoogleGLogo.imageset/google_g@2x.png


BIN
meetings_app/Assets.xcassets/GoogleGLogo.imageset/google_g@3x.png


+ 144 - 89
meetings_app/ViewController.swift

@@ -85,10 +85,8 @@ final class ViewController: NSViewController {
     private weak var scheduleScrollRightButton: NSView?
     private weak var scheduleFilterDropdown: NSPopUpButton?
     private weak var scheduleGoogleAuthButton: NSButton?
-    private weak var scheduleProfileContainer: NSView?
-    private weak var scheduleProfileNameLabel: NSTextField?
-    private weak var scheduleProfileEmailLabel: NSTextField?
-    private weak var scheduleProfileImageView: NSImageView?
+    private var scheduleGoogleAuthButtonWidthConstraint: NSLayoutConstraint?
+    private var scheduleCurrentProfile: GoogleProfileDisplay?
     private var scheduleProfileImageTask: Task<Void, Never>?
 
     /// In-app browser navigation: `.allowAll` or `.whitelist(hostSuffixes:)` (e.g. `["google.com"]` matches `meet.google.com`).
@@ -932,6 +930,11 @@ private extension ViewController {
         contentStack.spacing = 14
         contentStack.alignment = .leading
 
+        contentStack.addArrangedSubview(scheduleTopAuthRow())
+        if let authRow = contentStack.arrangedSubviews.last {
+            contentStack.setCustomSpacing(20, after: authRow)
+        }
+
         let joinActions = meetJoinActionsRow()
         contentStack.addArrangedSubview(textLabel("Join Meetings", font: typography.pageTitle, color: palette.textPrimary))
         contentStack.addArrangedSubview(meetJoinSectionRow())
@@ -1817,18 +1820,28 @@ private extension ViewController {
 
         row.addArrangedSubview(makeScheduleRefreshButton())
 
-        let profileBadge = makeScheduleProfileBadge()
-        scheduleProfileContainer = profileBadge
-        row.addArrangedSubview(profileBadge)
+        row.addArrangedSubview(makeScheduleFilterDropdown())
+        row.widthAnchor.constraint(greaterThanOrEqualToConstant: 780).isActive = true
+        return row
+    }
+
+    private func scheduleTopAuthRow() -> NSView {
+        let row = NSStackView()
+        row.translatesAutoresizingMaskIntoConstraints = false
+        row.orientation = .horizontal
+        row.alignment = .centerY
+        row.spacing = 10
+
+        let spacer = NSView()
+        spacer.translatesAutoresizingMaskIntoConstraints = false
+        row.addArrangedSubview(spacer)
+        spacer.setContentHuggingPriority(.defaultLow, for: .horizontal)
 
-        let connectButton = makeSchedulePillButton(title: googleOAuth.loadTokens() == nil ? "Login with Google" : "Logout")
-        connectButton.target = self
-        connectButton.action = #selector(scheduleConnectButtonPressed(_:))
-        scheduleGoogleAuthButton = connectButton
+        let authButton = makeGoogleAuthButton()
+        scheduleGoogleAuthButton = authButton
         updateGoogleAuthButtonTitle()
-        row.addArrangedSubview(connectButton)
+        row.addArrangedSubview(authButton)
 
-        row.addArrangedSubview(makeScheduleFilterDropdown())
         row.widthAnchor.constraint(greaterThanOrEqualToConstant: 780).isActive = true
         return row
     }
@@ -1881,52 +1894,26 @@ private extension ViewController {
         return button
     }
 
-    private func makeScheduleProfileBadge() -> NSView {
-        let chip = roundedContainer(cornerRadius: 16, color: palette.inputBackground)
-        chip.translatesAutoresizingMaskIntoConstraints = false
-        styleSurface(chip, borderColor: palette.inputBorder, borderWidth: 1, shadow: false)
-        chip.heightAnchor.constraint(equalToConstant: 34).isActive = true
-        chip.widthAnchor.constraint(equalToConstant: 230).isActive = true
-        chip.isHidden = true
-
-        let avatar = NSImageView()
-        avatar.translatesAutoresizingMaskIntoConstraints = false
-        avatar.wantsLayer = true
-        avatar.layer?.cornerRadius = 11
-        avatar.layer?.masksToBounds = true
-        avatar.imageScaling = .scaleAxesIndependently
-        avatar.widthAnchor.constraint(equalToConstant: 22).isActive = true
-        avatar.heightAnchor.constraint(equalToConstant: 22).isActive = true
-        avatar.image = NSImage(systemSymbolName: "person.crop.circle.fill", accessibilityDescription: "Profile")
-        avatar.contentTintColor = palette.textSecondary
-
-        let name = textLabel("Google User", font: NSFont.systemFont(ofSize: 11, weight: .semibold), color: palette.textPrimary)
-        let email = textLabel("Signed in", font: NSFont.systemFont(ofSize: 10, weight: .regular), color: palette.textMuted)
-        name.lineBreakMode = .byTruncatingTail
-        email.lineBreakMode = .byTruncatingTail
-        name.maximumNumberOfLines = 1
-        email.maximumNumberOfLines = 1
-
-        let textStack = NSStackView(views: [name, email])
-        textStack.translatesAutoresizingMaskIntoConstraints = false
-        textStack.orientation = .vertical
-        textStack.spacing = 0
-        textStack.alignment = .leading
-
-        chip.addSubview(avatar)
-        chip.addSubview(textStack)
-        NSLayoutConstraint.activate([
-            avatar.leadingAnchor.constraint(equalTo: chip.leadingAnchor, constant: 8),
-            avatar.centerYAnchor.constraint(equalTo: chip.centerYAnchor),
-            textStack.leadingAnchor.constraint(equalTo: avatar.trailingAnchor, constant: 7),
-            textStack.trailingAnchor.constraint(equalTo: chip.trailingAnchor, constant: -8),
-            textStack.centerYAnchor.constraint(equalTo: chip.centerYAnchor)
-        ])
-
-        scheduleProfileNameLabel = name
-        scheduleProfileEmailLabel = email
-        scheduleProfileImageView = avatar
-        return chip
+    private func makeGoogleAuthButton() -> NSButton {
+        let button = NSButton(title: "", target: self, action: #selector(scheduleConnectButtonPressed(_:)))
+        button.translatesAutoresizingMaskIntoConstraints = false
+        button.isBordered = false
+        button.bezelStyle = .regularSquare
+        button.wantsLayer = true
+        button.layer?.cornerRadius = 21
+        button.layer?.borderWidth = 1
+        button.font = NSFont.systemFont(ofSize: 14, weight: .semibold)
+        button.imagePosition = .imageLeading
+        button.alignment = .left
+        button.imageHugsTitle = false
+        button.lineBreakMode = .byTruncatingTail
+        button.contentTintColor = palette.textPrimary
+        button.imageScaling = .scaleNone
+        button.heightAnchor.constraint(equalToConstant: 42).isActive = true
+        let widthConstraint = button.widthAnchor.constraint(equalToConstant: 248)
+        widthConstraint.isActive = true
+        scheduleGoogleAuthButtonWidthConstraint = widthConstraint
+        return button
     }
 
     private func makeScheduleRefreshButton() -> NSButton {
@@ -2909,15 +2896,7 @@ private extension ViewController {
             guard let self else { return }
             do {
                 if self.googleOAuth.loadTokens() != nil {
-                    try self.googleOAuth.signOut()
-                    await MainActor.run {
-                        self.updateGoogleAuthButtonTitle()
-                        self.applyGoogleProfile(nil)
-                        self.scheduleDateHeadingLabel?.stringValue = "Connect Google to see meetings"
-                        if let stack = self.scheduleCardsStack {
-                            self.renderScheduleCards(into: stack, meetings: [])
-                        }
-                    }
+                    await MainActor.run { self.showGoogleAccountMenu() }
                     return
                 }
 
@@ -2936,12 +2915,75 @@ private extension ViewController {
         }
     }
 
+    private func showGoogleAccountMenu() {
+        guard let button = scheduleGoogleAuthButton else { return }
+        let menu = NSMenu()
+
+        let name = scheduleCurrentProfile?.name ?? "Google account"
+        let email = scheduleCurrentProfile?.email ?? "Signed in"
+        let accountItem = NSMenuItem(title: "\(name) (\(email))", action: nil, keyEquivalent: "")
+        accountItem.isEnabled = false
+        menu.addItem(accountItem)
+        menu.addItem(.separator())
+
+        let logoutItem = NSMenuItem(title: "Logout", action: #selector(scheduleLogoutSelected(_:)), keyEquivalent: "")
+        logoutItem.target = self
+        menu.addItem(logoutItem)
+
+        let point = NSPoint(x: 0, y: button.bounds.height + 2)
+        menu.popUp(positioning: nil, at: point, in: button)
+    }
+
+    @objc private func scheduleLogoutSelected(_ sender: NSMenuItem) {
+        do {
+            try googleOAuth.signOut()
+            updateGoogleAuthButtonTitle()
+            applyGoogleProfile(nil)
+            scheduleDateHeadingLabel?.stringValue = "Connect Google to see meetings"
+            if let stack = scheduleCardsStack {
+                renderScheduleCards(into: stack, meetings: [])
+            }
+        } catch {
+            showSimpleError("Couldn’t logout Google account.", error: error)
+        }
+    }
+
     private func updateGoogleAuthButtonTitle() {
         let signedIn = (googleOAuth.loadTokens() != nil)
-        scheduleGoogleAuthButton?.title = signedIn ? "Logout" : "Login with Google"
-        scheduleGoogleAuthButton?.image = NSImage(systemSymbolName: signedIn ? "rectangle.portrait.and.arrow.right" : "person.crop.circle.badge.checkmark", accessibilityDescription: "Google Auth")
-        scheduleGoogleAuthButton?.imagePosition = .imageLeading
-        scheduleGoogleAuthButton?.symbolConfiguration = NSImage.SymbolConfiguration(pointSize: 12, weight: .semibold)
+        guard let button = scheduleGoogleAuthButton else { return }
+        let iconLeftInset: CGFloat = 12
+
+        let profileName = scheduleCurrentProfile?.name ?? "Google account"
+        let profileEmail = scheduleCurrentProfile?.email ?? "Sign in with Google"
+        let title = signedIn ? "\(profileName)  ·  \(profileEmail)" : "Sign in with Google"
+        let titleFont = NSFont.systemFont(ofSize: 14, weight: .medium)
+        let titleColor = darkModeEnabled ? NSColor(calibratedWhite: 0.96, alpha: 1) : NSColor(calibratedRed: 0.13, green: 0.14, blue: 0.16, alpha: 1)
+        button.attributedTitle = NSAttributedString(string: title, attributes: [
+            .font: titleFont,
+            .foregroundColor: titleColor
+        ])
+        let textWidth = (title as NSString).size(withAttributes: [.font: titleFont]).width
+        let idealWidth = ceil(textWidth + 74) // icon + extra left inset + cleaner side padding
+        scheduleGoogleAuthButtonWidthConstraint?.constant = min(320, max(188, idealWidth))
+
+        if signedIn {
+            let symbol = NSImage(systemSymbolName: "person.crop.circle.fill", accessibilityDescription: "Profile")
+            button.image = symbol.flatMap { paddedImage($0, iconSize: NSSize(width: 16, height: 16), leftInset: iconLeftInset) }
+            button.symbolConfiguration = NSImage.SymbolConfiguration(pointSize: 16, weight: .regular)
+            button.contentTintColor = palette.textPrimary
+        } else {
+            if let g = NSImage(named: "GoogleGLogo") {
+                button.image = paddedImage(g, iconSize: NSSize(width: 16, height: 16), leftInset: iconLeftInset)
+            } else {
+                button.image = nil
+            }
+            button.contentTintColor = nil
+        }
+        button.contentTintColor = signedIn ? palette.textPrimary : nil
+
+        let isDark = darkModeEnabled
+        button.layer?.backgroundColor = isDark ? NSColor(calibratedRed: 8.0 / 255.0, green: 14.0 / 255.0, blue: 24.0 / 255.0, alpha: 1).cgColor : NSColor.white.cgColor
+        button.layer?.borderColor = (isDark ? NSColor(calibratedWhite: 0.50, alpha: 1) : NSColor(calibratedWhite: 0.72, alpha: 1)).cgColor
     }
 
     private func makeGoogleProfileDisplay(from profile: GoogleUserProfile) -> GoogleProfileDisplay {
@@ -2957,31 +2999,19 @@ private extension ViewController {
     private func applyGoogleProfile(_ profile: GoogleProfileDisplay?) {
         scheduleProfileImageTask?.cancel()
         scheduleProfileImageTask = nil
+        scheduleCurrentProfile = profile
 
-        guard let profile else {
-            scheduleProfileContainer?.isHidden = true
-            scheduleProfileNameLabel?.stringValue = "Google User"
-            scheduleProfileEmailLabel?.stringValue = "Signed in"
-            scheduleProfileImageView?.image = NSImage(systemSymbolName: "person.crop.circle.fill", accessibilityDescription: "Profile")
-            scheduleProfileImageView?.contentTintColor = palette.textSecondary
-            return
-        }
-
-        scheduleProfileContainer?.isHidden = false
-        scheduleProfileNameLabel?.stringValue = profile.name
-        scheduleProfileEmailLabel?.stringValue = profile.email
-        scheduleProfileImageView?.image = NSImage(systemSymbolName: "person.crop.circle.fill", accessibilityDescription: "Profile")
-        scheduleProfileImageView?.contentTintColor = palette.textSecondary
+        updateGoogleAuthButtonTitle()
 
-        guard let pictureURL = profile.pictureURL else { return }
+        guard let profile, let pictureURL = profile.pictureURL else { return }
         scheduleProfileImageTask = Task { [weak self] in
             do {
                 let (data, _) = try await URLSession.shared.data(from: pictureURL)
                 if Task.isCancelled { return }
                 guard let image = NSImage(data: data) else { return }
                 await MainActor.run {
-                    self?.scheduleProfileImageView?.image = image
-                    self?.scheduleProfileImageView?.contentTintColor = nil
+                    self?.scheduleGoogleAuthButton?.image = self?.paddedImage(image, iconSize: NSSize(width: 16, height: 16), leftInset: 12)
+                    self?.scheduleGoogleAuthButton?.contentTintColor = nil
                 }
             } catch {
                 // Keep placeholder avatar if image fetch fails.
@@ -2989,6 +3019,31 @@ private extension ViewController {
         }
     }
 
+    private func resizedImage(_ image: NSImage, to size: NSSize) -> NSImage {
+        let result = NSImage(size: size)
+        result.lockFocus()
+        image.draw(in: NSRect(origin: .zero, size: size),
+                   from: NSRect(origin: .zero, size: image.size),
+                   operation: .copy,
+                   fraction: 1.0)
+        result.unlockFocus()
+        result.isTemplate = false
+        return result
+    }
+
+    private func paddedImage(_ image: NSImage, iconSize: NSSize, leftInset: CGFloat) -> NSImage {
+        let canvasSize = NSSize(width: iconSize.width + leftInset, height: iconSize.height)
+        let result = NSImage(size: canvasSize)
+        result.lockFocus()
+        image.draw(in: NSRect(x: leftInset, y: 0, width: iconSize.width, height: iconSize.height),
+                   from: NSRect(origin: .zero, size: image.size),
+                   operation: .copy,
+                   fraction: 1.0)
+        result.unlockFocus()
+        result.isTemplate = false
+        return result
+    }
+
     @MainActor
     func ensureGoogleClientIdConfigured(presentingWindow: NSWindow?) async throws {
         if googleOAuth.configuredClientId() != nil, googleOAuth.configuredClientSecret() != nil { return }