|
|
@@ -362,6 +362,8 @@ final class ViewController: NSViewController {
|
|
362
|
362
|
private weak var calendarPageDaySummaryLabel: NSTextField?
|
|
363
|
363
|
private var calendarPageActionPopover: NSPopover?
|
|
364
|
364
|
private var calendarPageCreatePopover: NSPopover?
|
|
|
365
|
+ private weak var topToastView: NSVisualEffectView?
|
|
|
366
|
+ private var topToastHideWorkItem: DispatchWorkItem?
|
|
365
|
367
|
|
|
366
|
368
|
/// In-app browser navigation: `.allowAll` or `.whitelist(hostSuffixes:)` (e.g. `["google.com"]` matches `meet.google.com`).
|
|
367
|
369
|
private let inAppBrowserDefaultPolicy: InAppBrowserURLPolicy = .allowAll
|
|
|
@@ -867,6 +869,95 @@ private extension ViewController {
|
|
867
|
869
|
alert.runModal()
|
|
868
|
870
|
}
|
|
869
|
871
|
|
|
|
872
|
+ private func showTopToast(message: String, isError: Bool) {
|
|
|
873
|
+ topToastHideWorkItem?.cancel()
|
|
|
874
|
+ topToastHideWorkItem = nil
|
|
|
875
|
+ topToastView?.removeFromSuperview()
|
|
|
876
|
+ topToastView = nil
|
|
|
877
|
+
|
|
|
878
|
+ let toast = NSVisualEffectView()
|
|
|
879
|
+ toast.translatesAutoresizingMaskIntoConstraints = false
|
|
|
880
|
+ toast.material = darkModeEnabled ? .hudWindow : .popover
|
|
|
881
|
+ toast.blendingMode = .withinWindow
|
|
|
882
|
+ toast.state = .active
|
|
|
883
|
+ toast.wantsLayer = true
|
|
|
884
|
+ toast.layer?.cornerRadius = 10
|
|
|
885
|
+ toast.layer?.masksToBounds = true
|
|
|
886
|
+ toast.layer?.borderWidth = 1
|
|
|
887
|
+ toast.layer?.borderColor = NSColor.white.withAlphaComponent(darkModeEnabled ? 0.10 : 0.18).cgColor
|
|
|
888
|
+ toast.layer?.backgroundColor = NSColor.black.withAlphaComponent(darkModeEnabled ? 0.68 : 0.78).cgColor
|
|
|
889
|
+ toast.alphaValue = 0
|
|
|
890
|
+
|
|
|
891
|
+ let row = NSStackView()
|
|
|
892
|
+ row.translatesAutoresizingMaskIntoConstraints = false
|
|
|
893
|
+ row.orientation = .horizontal
|
|
|
894
|
+ row.alignment = .centerY
|
|
|
895
|
+ row.spacing = 8
|
|
|
896
|
+ row.distribution = .fill
|
|
|
897
|
+
|
|
|
898
|
+ let icon = NSImageView()
|
|
|
899
|
+ icon.translatesAutoresizingMaskIntoConstraints = false
|
|
|
900
|
+ icon.imageScaling = .scaleProportionallyDown
|
|
|
901
|
+ icon.image = NSImage(
|
|
|
902
|
+ systemSymbolName: isError ? "xmark.circle.fill" : "checkmark.circle.fill",
|
|
|
903
|
+ accessibilityDescription: isError ? "Error" : "Success"
|
|
|
904
|
+ )
|
|
|
905
|
+ icon.symbolConfiguration = NSImage.SymbolConfiguration(pointSize: 16, weight: .semibold)
|
|
|
906
|
+ icon.contentTintColor = isError ? NSColor.systemRed : NSColor.systemGreen
|
|
|
907
|
+
|
|
|
908
|
+ let label = textLabel(
|
|
|
909
|
+ message,
|
|
|
910
|
+ font: NSFont.systemFont(ofSize: 13, weight: .semibold),
|
|
|
911
|
+ color: NSColor.white
|
|
|
912
|
+ )
|
|
|
913
|
+ label.alignment = .left
|
|
|
914
|
+ label.maximumNumberOfLines = 2
|
|
|
915
|
+ label.lineBreakMode = .byWordWrapping
|
|
|
916
|
+ row.addArrangedSubview(icon)
|
|
|
917
|
+ row.addArrangedSubview(label)
|
|
|
918
|
+ toast.addSubview(row)
|
|
|
919
|
+
|
|
|
920
|
+ view.addSubview(toast)
|
|
|
921
|
+ NSLayoutConstraint.activate([
|
|
|
922
|
+ toast.topAnchor.constraint(equalTo: view.topAnchor, constant: 14),
|
|
|
923
|
+ toast.centerXAnchor.constraint(equalTo: view.centerXAnchor),
|
|
|
924
|
+ toast.widthAnchor.constraint(lessThanOrEqualTo: view.widthAnchor, constant: -40),
|
|
|
925
|
+ toast.heightAnchor.constraint(greaterThanOrEqualToConstant: 40),
|
|
|
926
|
+
|
|
|
927
|
+ icon.widthAnchor.constraint(equalToConstant: 16),
|
|
|
928
|
+ icon.heightAnchor.constraint(equalToConstant: 16),
|
|
|
929
|
+
|
|
|
930
|
+ row.leadingAnchor.constraint(equalTo: toast.leadingAnchor, constant: 14),
|
|
|
931
|
+ row.trailingAnchor.constraint(equalTo: toast.trailingAnchor, constant: -14),
|
|
|
932
|
+ row.topAnchor.constraint(equalTo: toast.topAnchor, constant: 10),
|
|
|
933
|
+ row.bottomAnchor.constraint(equalTo: toast.bottomAnchor, constant: -10)
|
|
|
934
|
+ ])
|
|
|
935
|
+
|
|
|
936
|
+ topToastView = toast
|
|
|
937
|
+
|
|
|
938
|
+ NSAnimationContext.runAnimationGroup { context in
|
|
|
939
|
+ context.duration = 0.18
|
|
|
940
|
+ context.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
|
|
|
941
|
+ toast.animator().alphaValue = 1
|
|
|
942
|
+ }
|
|
|
943
|
+
|
|
|
944
|
+ let hideWorkItem = DispatchWorkItem { [weak self, weak toast] in
|
|
|
945
|
+ guard let self, let toast else { return }
|
|
|
946
|
+ NSAnimationContext.runAnimationGroup({ context in
|
|
|
947
|
+ context.duration = 0.2
|
|
|
948
|
+ context.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
|
|
|
949
|
+ toast.animator().alphaValue = 0
|
|
|
950
|
+ }, completionHandler: {
|
|
|
951
|
+ toast.removeFromSuperview()
|
|
|
952
|
+ if self.topToastView === toast {
|
|
|
953
|
+ self.topToastView = nil
|
|
|
954
|
+ }
|
|
|
955
|
+ })
|
|
|
956
|
+ }
|
|
|
957
|
+ topToastHideWorkItem = hideWorkItem
|
|
|
958
|
+ DispatchQueue.main.asyncAfter(deadline: .now() + 2.0, execute: hideWorkItem)
|
|
|
959
|
+ }
|
|
|
960
|
+
|
|
870
|
961
|
private func confirmPremiumUpgrade() -> Bool {
|
|
871
|
962
|
let alert = NSAlert()
|
|
872
|
963
|
alert.messageText = "Already Premium"
|
|
|
@@ -4686,9 +4777,12 @@ private extension ViewController {
|
|
4686
|
4777
|
self.calendarPageMonthLabel?.stringValue = self.calendarMonthTitleText(for: self.calendarPageMonthAnchor)
|
|
4687
|
4778
|
self.renderCalendarMonthGrid()
|
|
4688
|
4779
|
self.renderCalendarSelectedDay()
|
|
|
4780
|
+ self.showTopToast(message: "Meeting added successfully.", isError: false)
|
|
4689
|
4781
|
}
|
|
4690
|
4782
|
} catch {
|
|
4691
|
|
- self.showSimpleError("Couldn’t create meeting.", error: error)
|
|
|
4783
|
+ await MainActor.run {
|
|
|
4784
|
+ self.showTopToast(message: "An issue occurred. Please try again.", isError: true)
|
|
|
4785
|
+ }
|
|
4692
|
4786
|
}
|
|
4693
|
4787
|
}
|
|
4694
|
4788
|
}
|