|
@@ -6,6 +6,7 @@
|
|
|
//
|
|
//
|
|
|
|
|
|
|
|
import Cocoa
|
|
import Cocoa
|
|
|
|
|
+import QuartzCore
|
|
|
import WebKit
|
|
import WebKit
|
|
|
import AuthenticationServices
|
|
import AuthenticationServices
|
|
|
|
|
|
|
@@ -85,6 +86,9 @@ final class ViewController: NSViewController {
|
|
|
private weak var scheduleScrollRightButton: NSView?
|
|
private weak var scheduleScrollRightButton: NSView?
|
|
|
private weak var scheduleFilterDropdown: NSPopUpButton?
|
|
private weak var scheduleFilterDropdown: NSPopUpButton?
|
|
|
private weak var scheduleGoogleAuthButton: NSButton?
|
|
private weak var scheduleGoogleAuthButton: NSButton?
|
|
|
|
|
+ private weak var scheduleGoogleAuthHostView: GoogleProfileAuthHostView?
|
|
|
|
|
+ private var scheduleGoogleAuthHostPadWidthConstraint: NSLayoutConstraint?
|
|
|
|
|
+ private var scheduleGoogleAuthHostPadHeightConstraint: NSLayoutConstraint?
|
|
|
private var scheduleGoogleAuthButtonWidthConstraint: NSLayoutConstraint?
|
|
private var scheduleGoogleAuthButtonWidthConstraint: NSLayoutConstraint?
|
|
|
private var scheduleGoogleAuthButtonHeightConstraint: NSLayoutConstraint?
|
|
private var scheduleGoogleAuthButtonHeightConstraint: NSLayoutConstraint?
|
|
|
/// Circular avatar size when signed in (top-right, Google-style).
|
|
/// Circular avatar size when signed in (top-right, Google-style).
|
|
@@ -1847,10 +1851,25 @@ private extension ViewController {
|
|
|
row.addArrangedSubview(spacer)
|
|
row.addArrangedSubview(spacer)
|
|
|
spacer.setContentHuggingPriority(.defaultLow, for: .horizontal)
|
|
spacer.setContentHuggingPriority(.defaultLow, for: .horizontal)
|
|
|
|
|
|
|
|
|
|
+ let host = GoogleProfileAuthHostView()
|
|
|
|
|
+ host.translatesAutoresizingMaskIntoConstraints = false
|
|
|
let authButton = makeGoogleAuthButton()
|
|
let authButton = makeGoogleAuthButton()
|
|
|
|
|
+ host.authButton = authButton
|
|
|
|
|
+ scheduleGoogleAuthHostView = host
|
|
|
scheduleGoogleAuthButton = authButton
|
|
scheduleGoogleAuthButton = authButton
|
|
|
|
|
+ host.addSubview(authButton)
|
|
|
|
|
+ NSLayoutConstraint.activate([
|
|
|
|
|
+ authButton.centerXAnchor.constraint(equalTo: host.centerXAnchor),
|
|
|
|
|
+ authButton.centerYAnchor.constraint(equalTo: host.centerYAnchor)
|
|
|
|
|
+ ])
|
|
|
|
|
+ let hostPadW = host.widthAnchor.constraint(equalTo: authButton.widthAnchor, constant: 0)
|
|
|
|
|
+ let hostPadH = host.heightAnchor.constraint(equalTo: authButton.heightAnchor, constant: 0)
|
|
|
|
|
+ hostPadW.isActive = true
|
|
|
|
|
+ hostPadH.isActive = true
|
|
|
|
|
+ scheduleGoogleAuthHostPadWidthConstraint = hostPadW
|
|
|
|
|
+ scheduleGoogleAuthHostPadHeightConstraint = hostPadH
|
|
|
updateGoogleAuthButtonTitle()
|
|
updateGoogleAuthButtonTitle()
|
|
|
- row.addArrangedSubview(authButton)
|
|
|
|
|
|
|
+ row.addArrangedSubview(host)
|
|
|
|
|
|
|
|
row.widthAnchor.constraint(greaterThanOrEqualToConstant: 780).isActive = true
|
|
row.widthAnchor.constraint(greaterThanOrEqualToConstant: 780).isActive = true
|
|
|
return row
|
|
return row
|
|
@@ -1928,6 +1947,7 @@ private extension ViewController {
|
|
|
scheduleGoogleAuthButtonWidthConstraint = widthConstraint
|
|
scheduleGoogleAuthButtonWidthConstraint = widthConstraint
|
|
|
button.onHoverChanged = { [weak self] hovering in
|
|
button.onHoverChanged = { [weak self] hovering in
|
|
|
self?.scheduleGoogleAuthHovering = hovering
|
|
self?.scheduleGoogleAuthHovering = hovering
|
|
|
|
|
+ self?.scheduleGoogleAuthHostView?.setProfileHoverActive(hovering)
|
|
|
self?.applyGoogleAuthButtonSurface()
|
|
self?.applyGoogleAuthButtonSurface()
|
|
|
}
|
|
}
|
|
|
button.onHoverChanged?(false)
|
|
button.onHoverChanged?(false)
|
|
@@ -2164,6 +2184,103 @@ extension ViewController: NSWindowDelegate {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+/// Wraps the Google auth control and draws a circular accent ring with a light pulse while the signed-in avatar is hovered.
|
|
|
|
|
+private final class GoogleProfileAuthHostView: NSView {
|
|
|
|
|
+ weak var authButton: NSButton? {
|
|
|
|
|
+ didSet { needsLayout = true }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private let ringLayer = CAShapeLayer()
|
|
|
|
|
+ private var avatarRingMode = false
|
|
|
|
|
+ private static let ringLineWidth: CGFloat = 2.25
|
|
|
|
|
+
|
|
|
|
|
+ override init(frame frameRect: NSRect) {
|
|
|
|
|
+ super.init(frame: frameRect)
|
|
|
|
|
+ wantsLayer = true
|
|
|
|
|
+ layer?.masksToBounds = false
|
|
|
|
|
+ ringLayer.fillColor = nil
|
|
|
|
|
+ ringLayer.strokeColor = NSColor.clear.cgColor
|
|
|
|
|
+ ringLayer.lineWidth = Self.ringLineWidth
|
|
|
|
|
+ ringLayer.lineCap = .round
|
|
|
|
|
+ ringLayer.opacity = 0
|
|
|
|
|
+ ringLayer.anchorPoint = CGPoint(x: 0.5, y: 0.5)
|
|
|
|
|
+ layer?.insertSublayer(ringLayer, at: 0)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @available(*, unavailable)
|
|
|
|
|
+ required init?(coder: NSCoder) {
|
|
|
|
|
+ nil
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ func setAvatarRingMode(_ enabled: Bool) {
|
|
|
|
|
+ avatarRingMode = enabled
|
|
|
|
|
+ if enabled == false {
|
|
|
|
|
+ ringLayer.removeAllAnimations()
|
|
|
|
|
+ ringLayer.opacity = 0
|
|
|
|
|
+ ringLayer.lineWidth = Self.ringLineWidth
|
|
|
|
|
+ }
|
|
|
|
|
+ needsLayout = true
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ func updateRingAppearance(isDark: Bool, accent: NSColor) {
|
|
|
|
|
+ let stroke = isDark
|
|
|
|
|
+ ? accent.blended(withFraction: 0.22, of: NSColor.white) ?? accent
|
|
|
|
|
+ : accent
|
|
|
|
|
+ CATransaction.begin()
|
|
|
|
|
+ CATransaction.setDisableActions(true)
|
|
|
|
|
+ ringLayer.strokeColor = stroke.withAlphaComponent(0.95).cgColor
|
|
|
|
|
+ CATransaction.commit()
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ func setProfileHoverActive(_ active: Bool) {
|
|
|
|
|
+ guard avatarRingMode else { return }
|
|
|
|
|
+ ringLayer.removeAnimation(forKey: "pulse")
|
|
|
|
|
+ if active {
|
|
|
|
|
+ layoutRingPathIfNeeded()
|
|
|
|
|
+ CATransaction.begin()
|
|
|
|
|
+ CATransaction.setAnimationDuration(0.22)
|
|
|
|
|
+ ringLayer.opacity = 1
|
|
|
|
|
+ CATransaction.commit()
|
|
|
|
|
+ let pulse = CABasicAnimation(keyPath: "lineWidth")
|
|
|
|
|
+ pulse.fromValue = Self.ringLineWidth * 0.88
|
|
|
|
|
+ pulse.toValue = Self.ringLineWidth * 1.45
|
|
|
|
|
+ pulse.duration = 0.72
|
|
|
|
|
+ pulse.autoreverses = true
|
|
|
|
|
+ pulse.repeatCount = .infinity
|
|
|
|
|
+ pulse.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
|
|
|
|
|
+ ringLayer.add(pulse, forKey: "pulse")
|
|
|
|
|
+ } else {
|
|
|
|
|
+ CATransaction.begin()
|
|
|
|
|
+ CATransaction.setAnimationDuration(0.18)
|
|
|
|
|
+ ringLayer.opacity = 0
|
|
|
|
|
+ CATransaction.commit()
|
|
|
|
|
+ ringLayer.lineWidth = Self.ringLineWidth
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private func layoutRingPathIfNeeded() {
|
|
|
|
|
+ guard avatarRingMode, let btn = authButton else { return }
|
|
|
|
|
+ let f = btn.frame
|
|
|
|
|
+ guard f.width > 1, f.height > 1 else { return }
|
|
|
|
|
+ let center = CGPoint(x: f.midX, y: f.midY)
|
|
|
|
|
+ let avatarR = min(f.width, f.height) / 2
|
|
|
|
|
+ let gap: CGFloat = 3.5
|
|
|
|
|
+ let ringRadius = avatarR + gap
|
|
|
|
|
+ let d = ringRadius * 2
|
|
|
|
|
+ CATransaction.begin()
|
|
|
|
|
+ CATransaction.setDisableActions(true)
|
|
|
|
|
+ ringLayer.bounds = CGRect(x: 0, y: 0, width: d, height: d)
|
|
|
|
|
+ ringLayer.position = center
|
|
|
|
|
+ ringLayer.path = CGPath(ellipseIn: CGRect(origin: .zero, size: CGSize(width: d, height: d)), transform: nil)
|
|
|
|
|
+ CATransaction.commit()
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ override func layout() {
|
|
|
|
|
+ super.layout()
|
|
|
|
|
+ layoutRingPathIfNeeded()
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
/// Ensures `NSClickGestureRecognizer` on the row receives clicks instead of child label/image views swallowing them.
|
|
/// Ensures `NSClickGestureRecognizer` on the row receives clicks instead of child label/image views swallowing them.
|
|
|
private class RowHitTestView: NSView {
|
|
private class RowHitTestView: NSView {
|
|
|
override func hitTest(_ point: NSPoint) -> NSView? {
|
|
override func hitTest(_ point: NSPoint) -> NSView? {
|
|
@@ -3238,6 +3355,14 @@ private extension ViewController {
|
|
|
guard let button = scheduleGoogleAuthButton else { return }
|
|
guard let button = scheduleGoogleAuthButton else { return }
|
|
|
|
|
|
|
|
let profileName = scheduleCurrentProfile?.name ?? "Google account"
|
|
let profileName = scheduleCurrentProfile?.name ?? "Google account"
|
|
|
|
|
+ let ringHostInset: CGFloat = signedIn ? 14 : 0
|
|
|
|
|
+ scheduleGoogleAuthHostPadWidthConstraint?.constant = ringHostInset
|
|
|
|
|
+ scheduleGoogleAuthHostPadHeightConstraint?.constant = ringHostInset
|
|
|
|
|
+ scheduleGoogleAuthHostView?.setAvatarRingMode(signedIn)
|
|
|
|
|
+ scheduleGoogleAuthHostView?.updateRingAppearance(isDark: darkModeEnabled, accent: palette.primaryBlue)
|
|
|
|
|
+ if signedIn == false {
|
|
|
|
|
+ scheduleGoogleAuthHostView?.setProfileHoverActive(false)
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
if signedIn {
|
|
if signedIn {
|
|
|
button.setAccessibilityLabel("\(profileName), Google account")
|
|
button.setAccessibilityLabel("\(profileName), Google account")
|
|
@@ -3360,7 +3485,14 @@ private extension ViewController {
|
|
|
|
|
|
|
|
private func applyGoogleAuthButtonSurface() {
|
|
private func applyGoogleAuthButtonSurface() {
|
|
|
guard let button = scheduleGoogleAuthButton else { return }
|
|
guard let button = scheduleGoogleAuthButton else { return }
|
|
|
|
|
+ let signedIn = (googleOAuth.loadTokens() != nil)
|
|
|
let isDark = darkModeEnabled
|
|
let isDark = darkModeEnabled
|
|
|
|
|
+ if signedIn {
|
|
|
|
|
+ button.layer?.backgroundColor = NSColor.clear.cgColor
|
|
|
|
|
+ button.layer?.borderWidth = 0
|
|
|
|
|
+ scheduleGoogleAuthHostView?.updateRingAppearance(isDark: isDark, accent: palette.primaryBlue)
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
let baseBackground = isDark
|
|
let baseBackground = isDark
|
|
|
? NSColor(calibratedRed: 8.0 / 255.0, green: 14.0 / 255.0, blue: 24.0 / 255.0, alpha: 1)
|
|
? NSColor(calibratedRed: 8.0 / 255.0, green: 14.0 / 255.0, blue: 24.0 / 255.0, alpha: 1)
|
|
|
: NSColor.white
|
|
: NSColor.white
|
|
@@ -3372,6 +3504,7 @@ private extension ViewController {
|
|
|
let hoverBorder = isDark
|
|
let hoverBorder = isDark
|
|
|
? NSColor(calibratedWhite: 0.62, alpha: 1)
|
|
? NSColor(calibratedWhite: 0.62, alpha: 1)
|
|
|
: NSColor(calibratedWhite: 0.56, alpha: 1)
|
|
: NSColor(calibratedWhite: 0.56, alpha: 1)
|
|
|
|
|
+ button.layer?.borderWidth = 1
|
|
|
button.layer?.backgroundColor = (scheduleGoogleAuthHovering ? hoverBackground : baseBackground).cgColor
|
|
button.layer?.backgroundColor = (scheduleGoogleAuthHovering ? hoverBackground : baseBackground).cgColor
|
|
|
button.layer?.borderColor = (scheduleGoogleAuthHovering ? hoverBorder : baseBorder).cgColor
|
|
button.layer?.borderColor = (scheduleGoogleAuthHovering ? hoverBorder : baseBorder).cgColor
|
|
|
}
|
|
}
|