Procházet zdrojové kódy

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 před 1 týdnem
rodič
revize
156b04e0e3

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

@@ -0,0 +1,27 @@
1
+{
2
+  "images" : [
3
+    {
4
+      "filename" : "google_g.png",
5
+      "idiom" : "universal",
6
+      "scale" : "1x"
7
+    },
8
+    {
9
+      "filename" : "google_g@2x.png",
10
+      "idiom" : "universal",
11
+      "scale" : "2x"
12
+    },
13
+    {
14
+      "filename" : "google_g@3x.png",
15
+      "idiom" : "universal",
16
+      "scale" : "3x"
17
+    }
18
+  ],
19
+  "info" : {
20
+    "author" : "xcode",
21
+    "version" : 1
22
+  },
23
+  "properties" : {
24
+    "preserves-vector-representation" : false,
25
+    "template-rendering-intent" : "original"
26
+  }
27
+}

binární
meetings_app/Assets.xcassets/GoogleGLogo.imageset/google_g.png


binární
meetings_app/Assets.xcassets/GoogleGLogo.imageset/google_g@2x.png


binární
meetings_app/Assets.xcassets/GoogleGLogo.imageset/google_g@3x.png


+ 144 - 89
meetings_app/ViewController.swift

@@ -85,10 +85,8 @@ final class ViewController: NSViewController {
85 85
     private weak var scheduleScrollRightButton: NSView?
86 86
     private weak var scheduleFilterDropdown: NSPopUpButton?
87 87
     private weak var scheduleGoogleAuthButton: NSButton?
88
-    private weak var scheduleProfileContainer: NSView?
89
-    private weak var scheduleProfileNameLabel: NSTextField?
90
-    private weak var scheduleProfileEmailLabel: NSTextField?
91
-    private weak var scheduleProfileImageView: NSImageView?
88
+    private var scheduleGoogleAuthButtonWidthConstraint: NSLayoutConstraint?
89
+    private var scheduleCurrentProfile: GoogleProfileDisplay?
92 90
     private var scheduleProfileImageTask: Task<Void, Never>?
93 91
 
94 92
     /// In-app browser navigation: `.allowAll` or `.whitelist(hostSuffixes:)` (e.g. `["google.com"]` matches `meet.google.com`).
@@ -932,6 +930,11 @@ private extension ViewController {
932 930
         contentStack.spacing = 14
933 931
         contentStack.alignment = .leading
934 932
 
933
+        contentStack.addArrangedSubview(scheduleTopAuthRow())
934
+        if let authRow = contentStack.arrangedSubviews.last {
935
+            contentStack.setCustomSpacing(20, after: authRow)
936
+        }
937
+
935 938
         let joinActions = meetJoinActionsRow()
936 939
         contentStack.addArrangedSubview(textLabel("Join Meetings", font: typography.pageTitle, color: palette.textPrimary))
937 940
         contentStack.addArrangedSubview(meetJoinSectionRow())
@@ -1817,18 +1820,28 @@ private extension ViewController {
1817 1820
 
1818 1821
         row.addArrangedSubview(makeScheduleRefreshButton())
1819 1822
 
1820
-        let profileBadge = makeScheduleProfileBadge()
1821
-        scheduleProfileContainer = profileBadge
1822
-        row.addArrangedSubview(profileBadge)
1823
+        row.addArrangedSubview(makeScheduleFilterDropdown())
1824
+        row.widthAnchor.constraint(greaterThanOrEqualToConstant: 780).isActive = true
1825
+        return row
1826
+    }
1827
+
1828
+    private func scheduleTopAuthRow() -> NSView {
1829
+        let row = NSStackView()
1830
+        row.translatesAutoresizingMaskIntoConstraints = false
1831
+        row.orientation = .horizontal
1832
+        row.alignment = .centerY
1833
+        row.spacing = 10
1834
+
1835
+        let spacer = NSView()
1836
+        spacer.translatesAutoresizingMaskIntoConstraints = false
1837
+        row.addArrangedSubview(spacer)
1838
+        spacer.setContentHuggingPriority(.defaultLow, for: .horizontal)
1823 1839
 
1824
-        let connectButton = makeSchedulePillButton(title: googleOAuth.loadTokens() == nil ? "Login with Google" : "Logout")
1825
-        connectButton.target = self
1826
-        connectButton.action = #selector(scheduleConnectButtonPressed(_:))
1827
-        scheduleGoogleAuthButton = connectButton
1840
+        let authButton = makeGoogleAuthButton()
1841
+        scheduleGoogleAuthButton = authButton
1828 1842
         updateGoogleAuthButtonTitle()
1829
-        row.addArrangedSubview(connectButton)
1843
+        row.addArrangedSubview(authButton)
1830 1844
 
1831
-        row.addArrangedSubview(makeScheduleFilterDropdown())
1832 1845
         row.widthAnchor.constraint(greaterThanOrEqualToConstant: 780).isActive = true
1833 1846
         return row
1834 1847
     }
@@ -1881,52 +1894,26 @@ private extension ViewController {
1881 1894
         return button
1882 1895
     }
1883 1896
 
1884
-    private func makeScheduleProfileBadge() -> NSView {
1885
-        let chip = roundedContainer(cornerRadius: 16, color: palette.inputBackground)
1886
-        chip.translatesAutoresizingMaskIntoConstraints = false
1887
-        styleSurface(chip, borderColor: palette.inputBorder, borderWidth: 1, shadow: false)
1888
-        chip.heightAnchor.constraint(equalToConstant: 34).isActive = true
1889
-        chip.widthAnchor.constraint(equalToConstant: 230).isActive = true
1890
-        chip.isHidden = true
1891
-
1892
-        let avatar = NSImageView()
1893
-        avatar.translatesAutoresizingMaskIntoConstraints = false
1894
-        avatar.wantsLayer = true
1895
-        avatar.layer?.cornerRadius = 11
1896
-        avatar.layer?.masksToBounds = true
1897
-        avatar.imageScaling = .scaleAxesIndependently
1898
-        avatar.widthAnchor.constraint(equalToConstant: 22).isActive = true
1899
-        avatar.heightAnchor.constraint(equalToConstant: 22).isActive = true
1900
-        avatar.image = NSImage(systemSymbolName: "person.crop.circle.fill", accessibilityDescription: "Profile")
1901
-        avatar.contentTintColor = palette.textSecondary
1902
-
1903
-        let name = textLabel("Google User", font: NSFont.systemFont(ofSize: 11, weight: .semibold), color: palette.textPrimary)
1904
-        let email = textLabel("Signed in", font: NSFont.systemFont(ofSize: 10, weight: .regular), color: palette.textMuted)
1905
-        name.lineBreakMode = .byTruncatingTail
1906
-        email.lineBreakMode = .byTruncatingTail
1907
-        name.maximumNumberOfLines = 1
1908
-        email.maximumNumberOfLines = 1
1909
-
1910
-        let textStack = NSStackView(views: [name, email])
1911
-        textStack.translatesAutoresizingMaskIntoConstraints = false
1912
-        textStack.orientation = .vertical
1913
-        textStack.spacing = 0
1914
-        textStack.alignment = .leading
1915
-
1916
-        chip.addSubview(avatar)
1917
-        chip.addSubview(textStack)
1918
-        NSLayoutConstraint.activate([
1919
-            avatar.leadingAnchor.constraint(equalTo: chip.leadingAnchor, constant: 8),
1920
-            avatar.centerYAnchor.constraint(equalTo: chip.centerYAnchor),
1921
-            textStack.leadingAnchor.constraint(equalTo: avatar.trailingAnchor, constant: 7),
1922
-            textStack.trailingAnchor.constraint(equalTo: chip.trailingAnchor, constant: -8),
1923
-            textStack.centerYAnchor.constraint(equalTo: chip.centerYAnchor)
1924
-        ])
1925
-
1926
-        scheduleProfileNameLabel = name
1927
-        scheduleProfileEmailLabel = email
1928
-        scheduleProfileImageView = avatar
1929
-        return chip
1897
+    private func makeGoogleAuthButton() -> NSButton {
1898
+        let button = NSButton(title: "", target: self, action: #selector(scheduleConnectButtonPressed(_:)))
1899
+        button.translatesAutoresizingMaskIntoConstraints = false
1900
+        button.isBordered = false
1901
+        button.bezelStyle = .regularSquare
1902
+        button.wantsLayer = true
1903
+        button.layer?.cornerRadius = 21
1904
+        button.layer?.borderWidth = 1
1905
+        button.font = NSFont.systemFont(ofSize: 14, weight: .semibold)
1906
+        button.imagePosition = .imageLeading
1907
+        button.alignment = .left
1908
+        button.imageHugsTitle = false
1909
+        button.lineBreakMode = .byTruncatingTail
1910
+        button.contentTintColor = palette.textPrimary
1911
+        button.imageScaling = .scaleNone
1912
+        button.heightAnchor.constraint(equalToConstant: 42).isActive = true
1913
+        let widthConstraint = button.widthAnchor.constraint(equalToConstant: 248)
1914
+        widthConstraint.isActive = true
1915
+        scheduleGoogleAuthButtonWidthConstraint = widthConstraint
1916
+        return button
1930 1917
     }
1931 1918
 
1932 1919
     private func makeScheduleRefreshButton() -> NSButton {
@@ -2909,15 +2896,7 @@ private extension ViewController {
2909 2896
             guard let self else { return }
2910 2897
             do {
2911 2898
                 if self.googleOAuth.loadTokens() != nil {
2912
-                    try self.googleOAuth.signOut()
2913
-                    await MainActor.run {
2914
-                        self.updateGoogleAuthButtonTitle()
2915
-                        self.applyGoogleProfile(nil)
2916
-                        self.scheduleDateHeadingLabel?.stringValue = "Connect Google to see meetings"
2917
-                        if let stack = self.scheduleCardsStack {
2918
-                            self.renderScheduleCards(into: stack, meetings: [])
2919
-                        }
2920
-                    }
2899
+                    await MainActor.run { self.showGoogleAccountMenu() }
2921 2900
                     return
2922 2901
                 }
2923 2902
 
@@ -2936,12 +2915,75 @@ private extension ViewController {
2936 2915
         }
2937 2916
     }
2938 2917
 
2918
+    private func showGoogleAccountMenu() {
2919
+        guard let button = scheduleGoogleAuthButton else { return }
2920
+        let menu = NSMenu()
2921
+
2922
+        let name = scheduleCurrentProfile?.name ?? "Google account"
2923
+        let email = scheduleCurrentProfile?.email ?? "Signed in"
2924
+        let accountItem = NSMenuItem(title: "\(name) (\(email))", action: nil, keyEquivalent: "")
2925
+        accountItem.isEnabled = false
2926
+        menu.addItem(accountItem)
2927
+        menu.addItem(.separator())
2928
+
2929
+        let logoutItem = NSMenuItem(title: "Logout", action: #selector(scheduleLogoutSelected(_:)), keyEquivalent: "")
2930
+        logoutItem.target = self
2931
+        menu.addItem(logoutItem)
2932
+
2933
+        let point = NSPoint(x: 0, y: button.bounds.height + 2)
2934
+        menu.popUp(positioning: nil, at: point, in: button)
2935
+    }
2936
+
2937
+    @objc private func scheduleLogoutSelected(_ sender: NSMenuItem) {
2938
+        do {
2939
+            try googleOAuth.signOut()
2940
+            updateGoogleAuthButtonTitle()
2941
+            applyGoogleProfile(nil)
2942
+            scheduleDateHeadingLabel?.stringValue = "Connect Google to see meetings"
2943
+            if let stack = scheduleCardsStack {
2944
+                renderScheduleCards(into: stack, meetings: [])
2945
+            }
2946
+        } catch {
2947
+            showSimpleError("Couldn’t logout Google account.", error: error)
2948
+        }
2949
+    }
2950
+
2939 2951
     private func updateGoogleAuthButtonTitle() {
2940 2952
         let signedIn = (googleOAuth.loadTokens() != nil)
2941
-        scheduleGoogleAuthButton?.title = signedIn ? "Logout" : "Login with Google"
2942
-        scheduleGoogleAuthButton?.image = NSImage(systemSymbolName: signedIn ? "rectangle.portrait.and.arrow.right" : "person.crop.circle.badge.checkmark", accessibilityDescription: "Google Auth")
2943
-        scheduleGoogleAuthButton?.imagePosition = .imageLeading
2944
-        scheduleGoogleAuthButton?.symbolConfiguration = NSImage.SymbolConfiguration(pointSize: 12, weight: .semibold)
2953
+        guard let button = scheduleGoogleAuthButton else { return }
2954
+        let iconLeftInset: CGFloat = 12
2955
+
2956
+        let profileName = scheduleCurrentProfile?.name ?? "Google account"
2957
+        let profileEmail = scheduleCurrentProfile?.email ?? "Sign in with Google"
2958
+        let title = signedIn ? "\(profileName)  ·  \(profileEmail)" : "Sign in with Google"
2959
+        let titleFont = NSFont.systemFont(ofSize: 14, weight: .medium)
2960
+        let titleColor = darkModeEnabled ? NSColor(calibratedWhite: 0.96, alpha: 1) : NSColor(calibratedRed: 0.13, green: 0.14, blue: 0.16, alpha: 1)
2961
+        button.attributedTitle = NSAttributedString(string: title, attributes: [
2962
+            .font: titleFont,
2963
+            .foregroundColor: titleColor
2964
+        ])
2965
+        let textWidth = (title as NSString).size(withAttributes: [.font: titleFont]).width
2966
+        let idealWidth = ceil(textWidth + 74) // icon + extra left inset + cleaner side padding
2967
+        scheduleGoogleAuthButtonWidthConstraint?.constant = min(320, max(188, idealWidth))
2968
+
2969
+        if signedIn {
2970
+            let symbol = NSImage(systemSymbolName: "person.crop.circle.fill", accessibilityDescription: "Profile")
2971
+            button.image = symbol.flatMap { paddedImage($0, iconSize: NSSize(width: 16, height: 16), leftInset: iconLeftInset) }
2972
+            button.symbolConfiguration = NSImage.SymbolConfiguration(pointSize: 16, weight: .regular)
2973
+            button.contentTintColor = palette.textPrimary
2974
+        } else {
2975
+            if let g = NSImage(named: "GoogleGLogo") {
2976
+                button.image = paddedImage(g, iconSize: NSSize(width: 16, height: 16), leftInset: iconLeftInset)
2977
+            } else {
2978
+                button.image = nil
2979
+            }
2980
+            button.contentTintColor = nil
2981
+        }
2982
+        button.contentTintColor = signedIn ? palette.textPrimary : nil
2983
+
2984
+        let isDark = darkModeEnabled
2985
+        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
2986
+        button.layer?.borderColor = (isDark ? NSColor(calibratedWhite: 0.50, alpha: 1) : NSColor(calibratedWhite: 0.72, alpha: 1)).cgColor
2945 2987
     }
2946 2988
 
2947 2989
     private func makeGoogleProfileDisplay(from profile: GoogleUserProfile) -> GoogleProfileDisplay {
@@ -2957,31 +2999,19 @@ private extension ViewController {
2957 2999
     private func applyGoogleProfile(_ profile: GoogleProfileDisplay?) {
2958 3000
         scheduleProfileImageTask?.cancel()
2959 3001
         scheduleProfileImageTask = nil
3002
+        scheduleCurrentProfile = profile
2960 3003
 
2961
-        guard let profile else {
2962
-            scheduleProfileContainer?.isHidden = true
2963
-            scheduleProfileNameLabel?.stringValue = "Google User"
2964
-            scheduleProfileEmailLabel?.stringValue = "Signed in"
2965
-            scheduleProfileImageView?.image = NSImage(systemSymbolName: "person.crop.circle.fill", accessibilityDescription: "Profile")
2966
-            scheduleProfileImageView?.contentTintColor = palette.textSecondary
2967
-            return
2968
-        }
2969
-
2970
-        scheduleProfileContainer?.isHidden = false
2971
-        scheduleProfileNameLabel?.stringValue = profile.name
2972
-        scheduleProfileEmailLabel?.stringValue = profile.email
2973
-        scheduleProfileImageView?.image = NSImage(systemSymbolName: "person.crop.circle.fill", accessibilityDescription: "Profile")
2974
-        scheduleProfileImageView?.contentTintColor = palette.textSecondary
3004
+        updateGoogleAuthButtonTitle()
2975 3005
 
2976
-        guard let pictureURL = profile.pictureURL else { return }
3006
+        guard let profile, let pictureURL = profile.pictureURL else { return }
2977 3007
         scheduleProfileImageTask = Task { [weak self] in
2978 3008
             do {
2979 3009
                 let (data, _) = try await URLSession.shared.data(from: pictureURL)
2980 3010
                 if Task.isCancelled { return }
2981 3011
                 guard let image = NSImage(data: data) else { return }
2982 3012
                 await MainActor.run {
2983
-                    self?.scheduleProfileImageView?.image = image
2984
-                    self?.scheduleProfileImageView?.contentTintColor = nil
3013
+                    self?.scheduleGoogleAuthButton?.image = self?.paddedImage(image, iconSize: NSSize(width: 16, height: 16), leftInset: 12)
3014
+                    self?.scheduleGoogleAuthButton?.contentTintColor = nil
2985 3015
                 }
2986 3016
             } catch {
2987 3017
                 // Keep placeholder avatar if image fetch fails.
@@ -2989,6 +3019,31 @@ private extension ViewController {
2989 3019
         }
2990 3020
     }
2991 3021
 
3022
+    private func resizedImage(_ image: NSImage, to size: NSSize) -> NSImage {
3023
+        let result = NSImage(size: size)
3024
+        result.lockFocus()
3025
+        image.draw(in: NSRect(origin: .zero, size: size),
3026
+                   from: NSRect(origin: .zero, size: image.size),
3027
+                   operation: .copy,
3028
+                   fraction: 1.0)
3029
+        result.unlockFocus()
3030
+        result.isTemplate = false
3031
+        return result
3032
+    }
3033
+
3034
+    private func paddedImage(_ image: NSImage, iconSize: NSSize, leftInset: CGFloat) -> NSImage {
3035
+        let canvasSize = NSSize(width: iconSize.width + leftInset, height: iconSize.height)
3036
+        let result = NSImage(size: canvasSize)
3037
+        result.lockFocus()
3038
+        image.draw(in: NSRect(x: leftInset, y: 0, width: iconSize.width, height: iconSize.height),
3039
+                   from: NSRect(origin: .zero, size: image.size),
3040
+                   operation: .copy,
3041
+                   fraction: 1.0)
3042
+        result.unlockFocus()
3043
+        result.isTemplate = false
3044
+        return result
3045
+    }
3046
+
2992 3047
     @MainActor
2993 3048
     func ensureGoogleClientIdConfigured(presentingWindow: NSWindow?) async throws {
2994 3049
         if googleOAuth.configuredClientId() != nil, googleOAuth.configuredClientSecret() != nil { return }