|
|
@@ -6,6 +6,7 @@
|
|
6
|
6
|
//
|
|
7
|
7
|
|
|
8
|
8
|
import Cocoa
|
|
|
9
|
+import QuartzCore
|
|
9
|
10
|
import WebKit
|
|
10
|
11
|
import AuthenticationServices
|
|
11
|
12
|
|
|
|
@@ -85,6 +86,9 @@ final class ViewController: NSViewController {
|
|
85
|
86
|
private weak var scheduleScrollRightButton: NSView?
|
|
86
|
87
|
private weak var scheduleFilterDropdown: NSPopUpButton?
|
|
87
|
88
|
private weak var scheduleGoogleAuthButton: NSButton?
|
|
|
89
|
+ private weak var scheduleGoogleAuthHostView: GoogleProfileAuthHostView?
|
|
|
90
|
+ private var scheduleGoogleAuthHostPadWidthConstraint: NSLayoutConstraint?
|
|
|
91
|
+ private var scheduleGoogleAuthHostPadHeightConstraint: NSLayoutConstraint?
|
|
88
|
92
|
private var scheduleGoogleAuthButtonWidthConstraint: NSLayoutConstraint?
|
|
89
|
93
|
private var scheduleGoogleAuthButtonHeightConstraint: NSLayoutConstraint?
|
|
90
|
94
|
/// Circular avatar size when signed in (top-right, Google-style).
|
|
|
@@ -1847,10 +1851,25 @@ private extension ViewController {
|
|
1847
|
1851
|
row.addArrangedSubview(spacer)
|
|
1848
|
1852
|
spacer.setContentHuggingPriority(.defaultLow, for: .horizontal)
|
|
1849
|
1853
|
|
|
|
1854
|
+ let host = GoogleProfileAuthHostView()
|
|
|
1855
|
+ host.translatesAutoresizingMaskIntoConstraints = false
|
|
1850
|
1856
|
let authButton = makeGoogleAuthButton()
|
|
|
1857
|
+ host.authButton = authButton
|
|
|
1858
|
+ scheduleGoogleAuthHostView = host
|
|
1851
|
1859
|
scheduleGoogleAuthButton = authButton
|
|
|
1860
|
+ host.addSubview(authButton)
|
|
|
1861
|
+ NSLayoutConstraint.activate([
|
|
|
1862
|
+ authButton.centerXAnchor.constraint(equalTo: host.centerXAnchor),
|
|
|
1863
|
+ authButton.centerYAnchor.constraint(equalTo: host.centerYAnchor)
|
|
|
1864
|
+ ])
|
|
|
1865
|
+ let hostPadW = host.widthAnchor.constraint(equalTo: authButton.widthAnchor, constant: 0)
|
|
|
1866
|
+ let hostPadH = host.heightAnchor.constraint(equalTo: authButton.heightAnchor, constant: 0)
|
|
|
1867
|
+ hostPadW.isActive = true
|
|
|
1868
|
+ hostPadH.isActive = true
|
|
|
1869
|
+ scheduleGoogleAuthHostPadWidthConstraint = hostPadW
|
|
|
1870
|
+ scheduleGoogleAuthHostPadHeightConstraint = hostPadH
|
|
1852
|
1871
|
updateGoogleAuthButtonTitle()
|
|
1853
|
|
- row.addArrangedSubview(authButton)
|
|
|
1872
|
+ row.addArrangedSubview(host)
|
|
1854
|
1873
|
|
|
1855
|
1874
|
row.widthAnchor.constraint(greaterThanOrEqualToConstant: 780).isActive = true
|
|
1856
|
1875
|
return row
|
|
|
@@ -1928,6 +1947,7 @@ private extension ViewController {
|
|
1928
|
1947
|
scheduleGoogleAuthButtonWidthConstraint = widthConstraint
|
|
1929
|
1948
|
button.onHoverChanged = { [weak self] hovering in
|
|
1930
|
1949
|
self?.scheduleGoogleAuthHovering = hovering
|
|
|
1950
|
+ self?.scheduleGoogleAuthHostView?.setProfileHoverActive(hovering)
|
|
1931
|
1951
|
self?.applyGoogleAuthButtonSurface()
|
|
1932
|
1952
|
}
|
|
1933
|
1953
|
button.onHoverChanged?(false)
|
|
|
@@ -2164,6 +2184,103 @@ extension ViewController: NSWindowDelegate {
|
|
2164
|
2184
|
}
|
|
2165
|
2185
|
}
|
|
2166
|
2186
|
|
|
|
2187
|
+/// Wraps the Google auth control and draws a circular accent ring with a light pulse while the signed-in avatar is hovered.
|
|
|
2188
|
+private final class GoogleProfileAuthHostView: NSView {
|
|
|
2189
|
+ weak var authButton: NSButton? {
|
|
|
2190
|
+ didSet { needsLayout = true }
|
|
|
2191
|
+ }
|
|
|
2192
|
+
|
|
|
2193
|
+ private let ringLayer = CAShapeLayer()
|
|
|
2194
|
+ private var avatarRingMode = false
|
|
|
2195
|
+ private static let ringLineWidth: CGFloat = 2.25
|
|
|
2196
|
+
|
|
|
2197
|
+ override init(frame frameRect: NSRect) {
|
|
|
2198
|
+ super.init(frame: frameRect)
|
|
|
2199
|
+ wantsLayer = true
|
|
|
2200
|
+ layer?.masksToBounds = false
|
|
|
2201
|
+ ringLayer.fillColor = nil
|
|
|
2202
|
+ ringLayer.strokeColor = NSColor.clear.cgColor
|
|
|
2203
|
+ ringLayer.lineWidth = Self.ringLineWidth
|
|
|
2204
|
+ ringLayer.lineCap = .round
|
|
|
2205
|
+ ringLayer.opacity = 0
|
|
|
2206
|
+ ringLayer.anchorPoint = CGPoint(x: 0.5, y: 0.5)
|
|
|
2207
|
+ layer?.insertSublayer(ringLayer, at: 0)
|
|
|
2208
|
+ }
|
|
|
2209
|
+
|
|
|
2210
|
+ @available(*, unavailable)
|
|
|
2211
|
+ required init?(coder: NSCoder) {
|
|
|
2212
|
+ nil
|
|
|
2213
|
+ }
|
|
|
2214
|
+
|
|
|
2215
|
+ func setAvatarRingMode(_ enabled: Bool) {
|
|
|
2216
|
+ avatarRingMode = enabled
|
|
|
2217
|
+ if enabled == false {
|
|
|
2218
|
+ ringLayer.removeAllAnimations()
|
|
|
2219
|
+ ringLayer.opacity = 0
|
|
|
2220
|
+ ringLayer.lineWidth = Self.ringLineWidth
|
|
|
2221
|
+ }
|
|
|
2222
|
+ needsLayout = true
|
|
|
2223
|
+ }
|
|
|
2224
|
+
|
|
|
2225
|
+ func updateRingAppearance(isDark: Bool, accent: NSColor) {
|
|
|
2226
|
+ let stroke = isDark
|
|
|
2227
|
+ ? accent.blended(withFraction: 0.22, of: NSColor.white) ?? accent
|
|
|
2228
|
+ : accent
|
|
|
2229
|
+ CATransaction.begin()
|
|
|
2230
|
+ CATransaction.setDisableActions(true)
|
|
|
2231
|
+ ringLayer.strokeColor = stroke.withAlphaComponent(0.95).cgColor
|
|
|
2232
|
+ CATransaction.commit()
|
|
|
2233
|
+ }
|
|
|
2234
|
+
|
|
|
2235
|
+ func setProfileHoverActive(_ active: Bool) {
|
|
|
2236
|
+ guard avatarRingMode else { return }
|
|
|
2237
|
+ ringLayer.removeAnimation(forKey: "pulse")
|
|
|
2238
|
+ if active {
|
|
|
2239
|
+ layoutRingPathIfNeeded()
|
|
|
2240
|
+ CATransaction.begin()
|
|
|
2241
|
+ CATransaction.setAnimationDuration(0.22)
|
|
|
2242
|
+ ringLayer.opacity = 1
|
|
|
2243
|
+ CATransaction.commit()
|
|
|
2244
|
+ let pulse = CABasicAnimation(keyPath: "lineWidth")
|
|
|
2245
|
+ pulse.fromValue = Self.ringLineWidth * 0.88
|
|
|
2246
|
+ pulse.toValue = Self.ringLineWidth * 1.45
|
|
|
2247
|
+ pulse.duration = 0.72
|
|
|
2248
|
+ pulse.autoreverses = true
|
|
|
2249
|
+ pulse.repeatCount = .infinity
|
|
|
2250
|
+ pulse.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
|
|
|
2251
|
+ ringLayer.add(pulse, forKey: "pulse")
|
|
|
2252
|
+ } else {
|
|
|
2253
|
+ CATransaction.begin()
|
|
|
2254
|
+ CATransaction.setAnimationDuration(0.18)
|
|
|
2255
|
+ ringLayer.opacity = 0
|
|
|
2256
|
+ CATransaction.commit()
|
|
|
2257
|
+ ringLayer.lineWidth = Self.ringLineWidth
|
|
|
2258
|
+ }
|
|
|
2259
|
+ }
|
|
|
2260
|
+
|
|
|
2261
|
+ private func layoutRingPathIfNeeded() {
|
|
|
2262
|
+ guard avatarRingMode, let btn = authButton else { return }
|
|
|
2263
|
+ let f = btn.frame
|
|
|
2264
|
+ guard f.width > 1, f.height > 1 else { return }
|
|
|
2265
|
+ let center = CGPoint(x: f.midX, y: f.midY)
|
|
|
2266
|
+ let avatarR = min(f.width, f.height) / 2
|
|
|
2267
|
+ let gap: CGFloat = 3.5
|
|
|
2268
|
+ let ringRadius = avatarR + gap
|
|
|
2269
|
+ let d = ringRadius * 2
|
|
|
2270
|
+ CATransaction.begin()
|
|
|
2271
|
+ CATransaction.setDisableActions(true)
|
|
|
2272
|
+ ringLayer.bounds = CGRect(x: 0, y: 0, width: d, height: d)
|
|
|
2273
|
+ ringLayer.position = center
|
|
|
2274
|
+ ringLayer.path = CGPath(ellipseIn: CGRect(origin: .zero, size: CGSize(width: d, height: d)), transform: nil)
|
|
|
2275
|
+ CATransaction.commit()
|
|
|
2276
|
+ }
|
|
|
2277
|
+
|
|
|
2278
|
+ override func layout() {
|
|
|
2279
|
+ super.layout()
|
|
|
2280
|
+ layoutRingPathIfNeeded()
|
|
|
2281
|
+ }
|
|
|
2282
|
+}
|
|
|
2283
|
+
|
|
2167
|
2284
|
/// Ensures `NSClickGestureRecognizer` on the row receives clicks instead of child label/image views swallowing them.
|
|
2168
|
2285
|
private class RowHitTestView: NSView {
|
|
2169
|
2286
|
override func hitTest(_ point: NSPoint) -> NSView? {
|
|
|
@@ -3238,6 +3355,14 @@ private extension ViewController {
|
|
3238
|
3355
|
guard let button = scheduleGoogleAuthButton else { return }
|
|
3239
|
3356
|
|
|
3240
|
3357
|
let profileName = scheduleCurrentProfile?.name ?? "Google account"
|
|
|
3358
|
+ let ringHostInset: CGFloat = signedIn ? 14 : 0
|
|
|
3359
|
+ scheduleGoogleAuthHostPadWidthConstraint?.constant = ringHostInset
|
|
|
3360
|
+ scheduleGoogleAuthHostPadHeightConstraint?.constant = ringHostInset
|
|
|
3361
|
+ scheduleGoogleAuthHostView?.setAvatarRingMode(signedIn)
|
|
|
3362
|
+ scheduleGoogleAuthHostView?.updateRingAppearance(isDark: darkModeEnabled, accent: palette.primaryBlue)
|
|
|
3363
|
+ if signedIn == false {
|
|
|
3364
|
+ scheduleGoogleAuthHostView?.setProfileHoverActive(false)
|
|
|
3365
|
+ }
|
|
3241
|
3366
|
|
|
3242
|
3367
|
if signedIn {
|
|
3243
|
3368
|
button.setAccessibilityLabel("\(profileName), Google account")
|
|
|
@@ -3360,7 +3485,14 @@ private extension ViewController {
|
|
3360
|
3485
|
|
|
3361
|
3486
|
private func applyGoogleAuthButtonSurface() {
|
|
3362
|
3487
|
guard let button = scheduleGoogleAuthButton else { return }
|
|
|
3488
|
+ let signedIn = (googleOAuth.loadTokens() != nil)
|
|
3363
|
3489
|
let isDark = darkModeEnabled
|
|
|
3490
|
+ if signedIn {
|
|
|
3491
|
+ button.layer?.backgroundColor = NSColor.clear.cgColor
|
|
|
3492
|
+ button.layer?.borderWidth = 0
|
|
|
3493
|
+ scheduleGoogleAuthHostView?.updateRingAppearance(isDark: isDark, accent: palette.primaryBlue)
|
|
|
3494
|
+ return
|
|
|
3495
|
+ }
|
|
3364
|
3496
|
let baseBackground = isDark
|
|
3365
|
3497
|
? NSColor(calibratedRed: 8.0 / 255.0, green: 14.0 / 255.0, blue: 24.0 / 255.0, alpha: 1)
|
|
3366
|
3498
|
: NSColor.white
|
|
|
@@ -3372,6 +3504,7 @@ private extension ViewController {
|
|
3372
|
3504
|
let hoverBorder = isDark
|
|
3373
|
3505
|
? NSColor(calibratedWhite: 0.62, alpha: 1)
|
|
3374
|
3506
|
: NSColor(calibratedWhite: 0.56, alpha: 1)
|
|
|
3507
|
+ button.layer?.borderWidth = 1
|
|
3375
|
3508
|
button.layer?.backgroundColor = (scheduleGoogleAuthHovering ? hoverBackground : baseBackground).cgColor
|
|
3376
|
3509
|
button.layer?.borderColor = (scheduleGoogleAuthHovering ? hoverBorder : baseBorder).cgColor
|
|
3377
|
3510
|
}
|