mirror of
https://github.com/FBukovina/Atoll.git
synced 2026-10-27 15:14:33 +00:00
commit
1a2e3c3920
20 changed files with 1207 additions and 143 deletions
|
|
@ -25,6 +25,7 @@
|
||||||
7E8840DB2E9A347800C5CB8C /* Orange Cat Peeping.json in Resources */ = {isa = PBXBuildFile; fileRef = 7E8840DA2E9A347800C5CB8C /* Orange Cat Peeping.json */; };
|
7E8840DB2E9A347800C5CB8C /* Orange Cat Peeping.json in Resources */ = {isa = PBXBuildFile; fileRef = 7E8840DA2E9A347800C5CB8C /* Orange Cat Peeping.json */; };
|
||||||
7E9911B02EFD7D1B002EDFD5 /* Reindeer.json in Resources */ = {isa = PBXBuildFile; fileRef = 7E9911AF2EFD7D1B002EDFD5 /* Reindeer.json */; };
|
7E9911B02EFD7D1B002EDFD5 /* Reindeer.json in Resources */ = {isa = PBXBuildFile; fileRef = 7E9911AF2EFD7D1B002EDFD5 /* Reindeer.json */; };
|
||||||
7E9918722EFDAE5B002EDFD5 /* MediaRemoteAdapter.framework in Resources */ = {isa = PBXBuildFile; fileRef = F4C1DD09EC134302938E84F6 /* MediaRemoteAdapter.framework */; };
|
7E9918722EFDAE5B002EDFD5 /* MediaRemoteAdapter.framework in Resources */ = {isa = PBXBuildFile; fileRef = F4C1DD09EC134302938E84F6 /* MediaRemoteAdapter.framework */; };
|
||||||
|
7ECFE0C62F4A2323000DD9BD /* MijickCalendarView in Frameworks */ = {isa = PBXBuildFile; productRef = 7ECFE0C52F4A2323000DD9BD /* MijickCalendarView */; };
|
||||||
7EE1D73D2E6B1E0B00997219 /* ApplicationServices.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7EE1D73C2E6B1E0B00997219 /* ApplicationServices.framework */; };
|
7EE1D73D2E6B1E0B00997219 /* ApplicationServices.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7EE1D73C2E6B1E0B00997219 /* ApplicationServices.framework */; };
|
||||||
7EE1D73F2E6B1E1200997219 /* IOKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7EE1D73E2E6B1E1200997219 /* IOKit.framework */; };
|
7EE1D73F2E6B1E1200997219 /* IOKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7EE1D73E2E6B1E1200997219 /* IOKit.framework */; };
|
||||||
7EF1D4B92FFFCE3C008D2C32 /* ExtensionEventBridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7EF1D4B82FFFCE3C008D2C32 /* ExtensionEventBridge.swift */; };
|
7EF1D4B92FFFCE3C008D2C32 /* ExtensionEventBridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7EF1D4B82FFFCE3C008D2C32 /* ExtensionEventBridge.swift */; };
|
||||||
|
|
@ -71,6 +72,7 @@
|
||||||
7E6118172E24071E0050006C /* Collections in Frameworks */,
|
7E6118172E24071E0050006C /* Collections in Frameworks */,
|
||||||
7E6118162E24071E0050006C /* KeyboardShortcuts in Frameworks */,
|
7E6118162E24071E0050006C /* KeyboardShortcuts in Frameworks */,
|
||||||
7E6118152E24071E0050006C /* Sparkle in Frameworks */,
|
7E6118152E24071E0050006C /* Sparkle in Frameworks */,
|
||||||
|
7ECFE0C62F4A2323000DD9BD /* MijickCalendarView in Frameworks */,
|
||||||
7E288D122F1659EF0020C117 /* AtollExtensionKit in Frameworks */,
|
7E288D122F1659EF0020C117 /* AtollExtensionKit in Frameworks */,
|
||||||
7E6118142E24071E0050006C /* LaunchAtLogin in Frameworks */,
|
7E6118142E24071E0050006C /* LaunchAtLogin in Frameworks */,
|
||||||
);
|
);
|
||||||
|
|
@ -171,6 +173,7 @@
|
||||||
146BC4362D366E9B0047C66A /* MacroVisionKit */,
|
146BC4362D366E9B0047C66A /* MacroVisionKit */,
|
||||||
7E61181E2E24071E0050006C /* SkyLightWindow */,
|
7E61181E2E24071E0050006C /* SkyLightWindow */,
|
||||||
7E288D112F1659EF0020C117 /* AtollExtensionKit */,
|
7E288D112F1659EF0020C117 /* AtollExtensionKit */,
|
||||||
|
7ECFE0C52F4A2323000DD9BD /* MijickCalendarView */,
|
||||||
);
|
);
|
||||||
productName = DynamicIsland;
|
productName = DynamicIsland;
|
||||||
productReference = F54406E72CD4987A00C7AFD9 /* Atoll.app */;
|
productReference = F54406E72CD4987A00C7AFD9 /* Atoll.app */;
|
||||||
|
|
@ -216,6 +219,7 @@
|
||||||
7E61181F2E24071E0050006C /* XCRemoteSwiftPackageReference "SkyLightWindow" */,
|
7E61181F2E24071E0050006C /* XCRemoteSwiftPackageReference "SkyLightWindow" */,
|
||||||
7E09D5962EB7B497005AF41D /* XCRemoteSwiftPackageReference "open-meteo" */,
|
7E09D5962EB7B497005AF41D /* XCRemoteSwiftPackageReference "open-meteo" */,
|
||||||
7E288D102F1659EF0020C117 /* XCRemoteSwiftPackageReference "AtollExtensionKit" */,
|
7E288D102F1659EF0020C117 /* XCRemoteSwiftPackageReference "AtollExtensionKit" */,
|
||||||
|
7ECFE0C42F4A2323000DD9BD /* XCRemoteSwiftPackageReference "CalendarView" */,
|
||||||
);
|
);
|
||||||
preferredProjectObjectVersion = 77;
|
preferredProjectObjectVersion = 77;
|
||||||
productRefGroup = F54406E82CD4987A00C7AFD9 /* Products */;
|
productRefGroup = F54406E82CD4987A00C7AFD9 /* Products */;
|
||||||
|
|
@ -589,6 +593,14 @@
|
||||||
kind = branch;
|
kind = branch;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
7ECFE0C42F4A2323000DD9BD /* XCRemoteSwiftPackageReference "CalendarView" */ = {
|
||||||
|
isa = XCRemoteSwiftPackageReference;
|
||||||
|
repositoryURL = "https://github.com/Mijick/CalendarView";
|
||||||
|
requirement = {
|
||||||
|
kind = upToNextMajorVersion;
|
||||||
|
minimumVersion = 1.1.1;
|
||||||
|
};
|
||||||
|
};
|
||||||
9A987A112C73CAA1005CA465 /* XCRemoteSwiftPackageReference "Pow" */ = {
|
9A987A112C73CAA1005CA465 /* XCRemoteSwiftPackageReference "Pow" */ = {
|
||||||
isa = XCRemoteSwiftPackageReference;
|
isa = XCRemoteSwiftPackageReference;
|
||||||
repositoryURL = "https://github.com/EmergeTools/Pow";
|
repositoryURL = "https://github.com/EmergeTools/Pow";
|
||||||
|
|
@ -665,6 +677,11 @@
|
||||||
package = 7E61181F2E24071E0050006C /* XCRemoteSwiftPackageReference "SkyLightWindow" */;
|
package = 7E61181F2E24071E0050006C /* XCRemoteSwiftPackageReference "SkyLightWindow" */;
|
||||||
productName = SkyLightWindow;
|
productName = SkyLightWindow;
|
||||||
};
|
};
|
||||||
|
7ECFE0C52F4A2323000DD9BD /* MijickCalendarView */ = {
|
||||||
|
isa = XCSwiftPackageProductDependency;
|
||||||
|
package = 7ECFE0C42F4A2323000DD9BD /* XCRemoteSwiftPackageReference "CalendarView" */;
|
||||||
|
productName = MijickCalendarView;
|
||||||
|
};
|
||||||
9A987A0F2C73CA8D005CA465 /* Collections */ = {
|
9A987A0F2C73CA8D005CA465 /* Collections */ = {
|
||||||
isa = XCSwiftPackageProductDependency;
|
isa = XCSwiftPackageProductDependency;
|
||||||
package = F54407082CD4A23F00C7AFD9 /* XCRemoteSwiftPackageReference "swift-collections" */;
|
package = F54407082CD4A23F00C7AFD9 /* XCRemoteSwiftPackageReference "swift-collections" */;
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"originHash" : "6fbfa8cda0c383746edd66361426fdc2bd266fc082867294374a1818f02798f3",
|
"originHash" : "802e15fea0dccd2d325dc082d8d36162007adcd2fa301070eb26448d0a2cf6c5",
|
||||||
"pins" : [
|
"pins" : [
|
||||||
{
|
{
|
||||||
"identity" : "atollextensionkit",
|
"identity" : "atollextensionkit",
|
||||||
|
|
@ -10,6 +10,15 @@
|
||||||
"revision" : "ebe35277572a349fad26abeb6fc552cb5251d584"
|
"revision" : "ebe35277572a349fad26abeb6fc552cb5251d584"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"identity" : "calendarview",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/Mijick/CalendarView",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "168cf14a50d33684caa2a411bbd0b3f988e9dff6",
|
||||||
|
"version" : "1.1.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"identity" : "defaults",
|
"identity" : "defaults",
|
||||||
"kind" : "remoteSourceControl",
|
"kind" : "remoteSourceControl",
|
||||||
|
|
|
||||||
|
|
@ -180,10 +180,26 @@ struct ContentView: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
private let zeroHeightHoverPadding: CGFloat = 10
|
private let zeroHeightHoverPadding: CGFloat = 10
|
||||||
private let statsAdditionalRowHeight: CGFloat = 110
|
private let statsAdditionalRowHeight: CGFloat = statsSecondRowContentHeight + statsGridSpacingHeight
|
||||||
private let musicControlPauseGrace: TimeInterval = 5
|
private let musicControlPauseGrace: TimeInterval = 5
|
||||||
private let musicControlResumeDelay: TimeInterval = 0.24
|
private let musicControlResumeDelay: TimeInterval = 0.24
|
||||||
|
|
||||||
|
// MARK: - Tab switch direction for smooth transitions
|
||||||
|
|
||||||
|
private var tabSwitchTransition: AnyTransition {
|
||||||
|
if coordinator.tabSwitchForward {
|
||||||
|
return .asymmetric(
|
||||||
|
insertion: .move(edge: .trailing).combined(with: .opacity),
|
||||||
|
removal: .move(edge: .leading).combined(with: .opacity)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
return .asymmetric(
|
||||||
|
insertion: .move(edge: .leading).combined(with: .opacity),
|
||||||
|
removal: .move(edge: .trailing).combined(with: .opacity)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private var standardMediaControlsActive: Bool {
|
private var standardMediaControlsActive: Bool {
|
||||||
showStandardMediaControls && !enableMinimalisticUI
|
showStandardMediaControls && !enableMinimalisticUI
|
||||||
}
|
}
|
||||||
|
|
@ -398,6 +414,7 @@ struct ContentView: View {
|
||||||
maxHeight: dynamicNotchSize.height + currentShadowPadding,
|
maxHeight: dynamicNotchSize.height + currentShadowPadding,
|
||||||
alignment: .top
|
alignment: .top
|
||||||
)
|
)
|
||||||
|
.frame(maxHeight: .infinity, alignment: .top)
|
||||||
.environmentObject(privacyManager)
|
.environmentObject(privacyManager)
|
||||||
.onChange(of: dynamicNotchSize) { oldSize, newSize in
|
.onChange(of: dynamicNotchSize) { oldSize, newSize in
|
||||||
guard oldSize != newSize else { return }
|
guard oldSize != newSize else { return }
|
||||||
|
|
@ -753,13 +770,16 @@ struct ContentView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.id(coordinator.currentView) // Force SwiftUI to treat each view as unique
|
.id(coordinator.currentView)
|
||||||
|
.transition(tabSwitchTransition)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.clipped()
|
||||||
.zIndex(1)
|
.zIndex(1)
|
||||||
.allowsHitTesting(vm.notchState == .open)
|
.allowsHitTesting(vm.notchState == .open)
|
||||||
.blur(radius: abs(gestureProgress) > 0.3 ? min(abs(gestureProgress), 8) : 0)
|
.blur(radius: abs(gestureProgress) > 0.3 ? min(abs(gestureProgress), 8) : 0)
|
||||||
.opacity(abs(gestureProgress) > 0.3 ? min(abs(gestureProgress * 2), 0.8) : 1)
|
.opacity(abs(gestureProgress) > 0.3 ? min(abs(gestureProgress * 2), 0.8) : 1)
|
||||||
|
.animation(.smooth(duration: 0.3), value: coordinator.currentView)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -70,6 +70,7 @@ struct DynamicNotchApp: App {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@CommandsBuilder
|
||||||
var commands: some Commands {
|
var commands: some Commands {
|
||||||
CommandGroup(replacing: .appSettings) {
|
CommandGroup(replacing: .appSettings) {
|
||||||
Button("Settings…") {
|
Button("Settings…") {
|
||||||
|
|
@ -116,6 +117,8 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
||||||
private var cancellables = Set<AnyCancellable>()
|
private var cancellables = Set<AnyCancellable>()
|
||||||
private var windowsHiddenForLock = false
|
private var windowsHiddenForLock = false
|
||||||
private var optionalShortcutHandlersRegistered = false
|
private var optionalShortcutHandlersRegistered = false
|
||||||
|
private weak var focusWithoutDevToolsMenuItem: NSMenuItem?
|
||||||
|
private weak var focusUseDevToolsMenuItem: NSMenuItem?
|
||||||
|
|
||||||
// Debouncing mechanism for window size updates
|
// Debouncing mechanism for window size updates
|
||||||
private var windowSizeUpdateWorkItem: DispatchWorkItem?
|
private var windowSizeUpdateWorkItem: DispatchWorkItem?
|
||||||
|
|
@ -147,6 +150,10 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func applicationDidBecomeActive(_ notification: Notification) {
|
||||||
|
installTopMenuItemsIfNeeded()
|
||||||
|
}
|
||||||
|
|
||||||
func applicationWillTerminate(_ notification: Notification) {
|
func applicationWillTerminate(_ notification: Notification) {
|
||||||
// Cancel any pending window size updates
|
// Cancel any pending window size updates
|
||||||
windowSizeUpdateWorkItem?.cancel()
|
windowSizeUpdateWorkItem?.cancel()
|
||||||
|
|
@ -370,6 +377,13 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
||||||
idleAnimationManager.initializeDefaultAnimations()
|
idleAnimationManager.initializeDefaultAnimations()
|
||||||
|
|
||||||
applySelectedAppIcon()
|
applySelectedAppIcon()
|
||||||
|
installTopMenuItemsIfNeeded()
|
||||||
|
|
||||||
|
Defaults.publisher(.focusMonitoringMode, options: [])
|
||||||
|
.sink { [weak self] _ in
|
||||||
|
self?.updateFocusMenuState()
|
||||||
|
}
|
||||||
|
.store(in: &cancellables)
|
||||||
|
|
||||||
// Setup SystemHUD Manager
|
// Setup SystemHUD Manager
|
||||||
SystemHUDManager.shared.setup(coordinator: coordinator)
|
SystemHUDManager.shared.setup(coordinator: coordinator)
|
||||||
|
|
@ -531,9 +545,11 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
||||||
forName: Notification.Name.automaticallySwitchDisplayChanged, object: nil, queue: nil
|
forName: Notification.Name.automaticallySwitchDisplayChanged, object: nil, queue: nil
|
||||||
) { [weak self] _ in
|
) { [weak self] _ in
|
||||||
guard let self = self, let window = self.window else { return }
|
guard let self = self, let window = self.window else { return }
|
||||||
|
DispatchQueue.main.async {
|
||||||
window.alphaValue =
|
window.alphaValue =
|
||||||
self.coordinator.selectedScreen == self.coordinator.preferredScreen ? 1 : 0
|
self.coordinator.selectedScreen == self.coordinator.preferredScreen ? 1 : 0
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
NotificationCenter.default.addObserver(
|
NotificationCenter.default.addObserver(
|
||||||
forName: Notification.Name.showOnAllDisplaysChanged, object: nil, queue: nil
|
forName: Notification.Name.showOnAllDisplaysChanged, object: nil, queue: nil
|
||||||
|
|
@ -644,6 +660,155 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func installTopMenuItemsIfNeeded() {
|
||||||
|
guard let mainMenu = NSApp.mainMenu else { return }
|
||||||
|
if mainMenu.items.contains(where: { $0.identifier?.rawValue == "Atoll.Focus.Menu" }) {
|
||||||
|
updateFocusMenuState()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let insertionIndex = preferredMenuInsertionIndex(in: mainMenu)
|
||||||
|
|
||||||
|
let focusMenuItem = NSMenuItem(title: "Focus", action: nil, keyEquivalent: "")
|
||||||
|
focusMenuItem.identifier = NSUserInterfaceItemIdentifier("Atoll.Focus.Menu")
|
||||||
|
let focusSubmenu = NSMenu(title: "Focus")
|
||||||
|
|
||||||
|
let withoutDevTools = NSMenuItem(
|
||||||
|
title: "Use without DevTools",
|
||||||
|
action: #selector(selectFocusWithoutDevTools),
|
||||||
|
keyEquivalent: ""
|
||||||
|
)
|
||||||
|
withoutDevTools.target = self
|
||||||
|
|
||||||
|
let useDevTools = NSMenuItem(
|
||||||
|
title: "Use DevTools",
|
||||||
|
action: #selector(selectFocusUseDevTools),
|
||||||
|
keyEquivalent: ""
|
||||||
|
)
|
||||||
|
useDevTools.target = self
|
||||||
|
|
||||||
|
focusSubmenu.addItem(withoutDevTools)
|
||||||
|
focusSubmenu.addItem(useDevTools)
|
||||||
|
focusMenuItem.submenu = focusSubmenu
|
||||||
|
mainMenu.insertItem(focusMenuItem, at: insertionIndex)
|
||||||
|
|
||||||
|
focusWithoutDevToolsMenuItem = withoutDevTools
|
||||||
|
focusUseDevToolsMenuItem = useDevTools
|
||||||
|
|
||||||
|
let accessibilityMenuItem = NSMenuItem(title: "Accessibility", action: nil, keyEquivalent: "")
|
||||||
|
accessibilityMenuItem.identifier = NSUserInterfaceItemIdentifier("Atoll.Accessibility.Menu")
|
||||||
|
let accessibilitySubmenu = NSMenu(title: "Accessibility")
|
||||||
|
|
||||||
|
let requestAccessibility = NSMenuItem(
|
||||||
|
title: "Request Accessibility Access",
|
||||||
|
action: #selector(requestAccessibilityAccess),
|
||||||
|
keyEquivalent: ""
|
||||||
|
)
|
||||||
|
requestAccessibility.target = self
|
||||||
|
|
||||||
|
let openAccessibility = NSMenuItem(
|
||||||
|
title: "Open Accessibility Settings",
|
||||||
|
action: #selector(openAccessibilitySettings),
|
||||||
|
keyEquivalent: ""
|
||||||
|
)
|
||||||
|
openAccessibility.target = self
|
||||||
|
|
||||||
|
accessibilitySubmenu.addItem(requestAccessibility)
|
||||||
|
accessibilitySubmenu.addItem(openAccessibility)
|
||||||
|
accessibilityMenuItem.submenu = accessibilitySubmenu
|
||||||
|
mainMenu.insertItem(accessibilityMenuItem, at: insertionIndex + 1)
|
||||||
|
|
||||||
|
let permissionsMenuItem = NSMenuItem(title: "Permissions", action: nil, keyEquivalent: "")
|
||||||
|
permissionsMenuItem.identifier = NSUserInterfaceItemIdentifier("Atoll.Permissions.Menu")
|
||||||
|
let permissionsSubmenu = NSMenu(title: "Permissions")
|
||||||
|
|
||||||
|
let requestFullDisk = NSMenuItem(
|
||||||
|
title: "Request Full Disk Access",
|
||||||
|
action: #selector(requestFullDiskAccess),
|
||||||
|
keyEquivalent: ""
|
||||||
|
)
|
||||||
|
requestFullDisk.target = self
|
||||||
|
|
||||||
|
let openFullDisk = NSMenuItem(
|
||||||
|
title: "Open Full Disk Access Settings",
|
||||||
|
action: #selector(openFullDiskAccessSettings),
|
||||||
|
keyEquivalent: ""
|
||||||
|
)
|
||||||
|
openFullDisk.target = self
|
||||||
|
|
||||||
|
let openDevTools = NSMenuItem(
|
||||||
|
title: "Open Developer Tools Settings",
|
||||||
|
action: #selector(openDeveloperToolsSettingsFromMenu),
|
||||||
|
keyEquivalent: ""
|
||||||
|
)
|
||||||
|
openDevTools.target = self
|
||||||
|
|
||||||
|
permissionsSubmenu.addItem(requestFullDisk)
|
||||||
|
permissionsSubmenu.addItem(openFullDisk)
|
||||||
|
permissionsSubmenu.addItem(NSMenuItem.separator())
|
||||||
|
permissionsSubmenu.addItem(openDevTools)
|
||||||
|
permissionsMenuItem.submenu = permissionsSubmenu
|
||||||
|
mainMenu.insertItem(permissionsMenuItem, at: insertionIndex + 2)
|
||||||
|
|
||||||
|
updateFocusMenuState()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func preferredMenuInsertionIndex(in mainMenu: NSMenu) -> Int {
|
||||||
|
if let index = mainMenu.items.firstIndex(where: { $0.title == "Window" }) {
|
||||||
|
return index
|
||||||
|
}
|
||||||
|
if let index = mainMenu.items.firstIndex(where: { $0.title == "Help" }) {
|
||||||
|
return index
|
||||||
|
}
|
||||||
|
return max(mainMenu.numberOfItems, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func updateFocusMenuState() {
|
||||||
|
let mode = Defaults[.focusMonitoringMode]
|
||||||
|
focusWithoutDevToolsMenuItem?.state = mode == .withoutDevTools ? .on : .off
|
||||||
|
focusUseDevToolsMenuItem?.state = mode == .useDevTools ? .on : .off
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func selectFocusWithoutDevTools() {
|
||||||
|
Defaults[.focusMonitoringMode] = .withoutDevTools
|
||||||
|
updateFocusMenuState()
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func selectFocusUseDevTools() {
|
||||||
|
Defaults[.focusMonitoringMode] = .useDevTools
|
||||||
|
updateFocusMenuState()
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func requestAccessibilityAccess() {
|
||||||
|
AccessibilityPermissionStore.shared.requestAuthorizationPrompt()
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func openAccessibilitySettings() {
|
||||||
|
AccessibilityPermissionStore.shared.openSystemSettings()
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func requestFullDiskAccess() {
|
||||||
|
FullDiskAccessPermissionStore.shared.requestAccessPrompt()
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func openFullDiskAccessSettings() {
|
||||||
|
FullDiskAccessPermissionStore.shared.openSystemSettings()
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func openDeveloperToolsSettingsFromMenu() {
|
||||||
|
let urls = [
|
||||||
|
"x-apple.systempreferences:com.apple.preference.security?Privacy_DevTools",
|
||||||
|
"x-apple.systempreferences:com.apple.preference.security"
|
||||||
|
]
|
||||||
|
|
||||||
|
for candidate in urls {
|
||||||
|
guard let url = URL(string: candidate) else { continue }
|
||||||
|
if NSWorkspace.shared.open(url) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private func registerOptionalShortcutHandlers() {
|
private func registerOptionalShortcutHandlers() {
|
||||||
guard !optionalShortcutHandlersRegistered else { return }
|
guard !optionalShortcutHandlersRegistered else { return }
|
||||||
optionalShortcutHandlersRegistered = true
|
optionalShortcutHandlersRegistered = true
|
||||||
|
|
|
||||||
|
|
@ -102,12 +102,21 @@ class DynamicIslandViewCoordinator: ObservableObject {
|
||||||
static let shared = DynamicIslandViewCoordinator()
|
static let shared = DynamicIslandViewCoordinator()
|
||||||
private var cancellables = Set<AnyCancellable>()
|
private var cancellables = Set<AnyCancellable>()
|
||||||
|
|
||||||
|
private static let tabOrder: [NotchViews] = [.home, .shelf, .timer, .stats, .colorPicker, .notes, .clipboard, .extensionExperience]
|
||||||
|
|
||||||
|
/// Direction of the most recent tab switch (true = forward/right, false = backward/left)
|
||||||
|
@Published var tabSwitchForward: Bool = true
|
||||||
|
|
||||||
@Published var currentView: NotchViews = .home {
|
@Published var currentView: NotchViews = .home {
|
||||||
didSet {
|
didSet {
|
||||||
if Defaults[.enableMinimalisticUI] && currentView != .home {
|
if Defaults[.enableMinimalisticUI] && currentView != .home {
|
||||||
currentView = .home
|
currentView = .home
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
// Track direction before SwiftUI re-renders
|
||||||
|
let oldIdx = Self.tabOrder.firstIndex(of: oldValue) ?? 0
|
||||||
|
let newIdx = Self.tabOrder.firstIndex(of: currentView) ?? 0
|
||||||
|
tabSwitchForward = newIdx >= oldIdx
|
||||||
handleStatsTabTransition(from: oldValue, to: currentView)
|
handleStatsTabTransition(from: oldValue, to: currentView)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1591,10 +1591,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"±10 Seconds" : {
|
|
||||||
"comment" : "Name of the music skip behavior that skips the current track within ±10 seconds.",
|
|
||||||
"isCommentAutoGenerated" : true
|
|
||||||
},
|
|
||||||
"0" : {
|
"0" : {
|
||||||
"comment" : "A label displayed in a circular gauge to indicate the minimum battery level.",
|
"comment" : "A label displayed in a circular gauge to indicate the minimum battery level.",
|
||||||
"isCommentAutoGenerated" : true,
|
"isCommentAutoGenerated" : true,
|
||||||
|
|
@ -1911,10 +1907,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Accent" : {
|
|
||||||
"comment" : "Name of the tint mode for the caps lock indicator that uses the accent color.",
|
|
||||||
"isCommentAutoGenerated" : true
|
|
||||||
},
|
|
||||||
"Accent color" : {
|
"Accent color" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"ar" : {
|
"ar" : {
|
||||||
|
|
@ -2260,10 +2252,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Adaptive gradient" : {
|
|
||||||
"comment" : "Name of the timer icon color mode option that uses an adaptive gradient.",
|
|
||||||
"isCommentAutoGenerated" : true
|
|
||||||
},
|
|
||||||
"Add" : {
|
"Add" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"ar" : {
|
"ar" : {
|
||||||
|
|
@ -3853,10 +3841,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Apple Music" : {
|
|
||||||
"comment" : "Names for the different media player types available in the app.",
|
|
||||||
"isCommentAutoGenerated" : true
|
|
||||||
},
|
|
||||||
"Applies the notch-style parallax effect to the lock screen media widget album art." : {
|
"Applies the notch-style parallax effect to the lock screen media widget album art." : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"pt-BR" : {
|
"pt-BR" : {
|
||||||
|
|
@ -4268,10 +4252,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Bar" : {
|
|
||||||
"comment" : "Localized name for \"Bar\" timer progress style.",
|
|
||||||
"isCommentAutoGenerated" : true
|
|
||||||
},
|
|
||||||
"Battery" : {
|
"Battery" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"ar" : {
|
"ar" : {
|
||||||
|
|
@ -7617,6 +7597,9 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"Custom Liquid is available on macOS 26 or later." : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Custom Liquid settings require the Liquid Glass material." : {
|
"Custom Liquid settings require the Liquid Glass material." : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
|
|
@ -7627,6 +7610,9 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"Custom liquid variant" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Custom music live activity animation" : {
|
"Custom music live activity animation" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
|
|
@ -8505,10 +8491,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Digital" : {
|
|
||||||
"comment" : "Name for the \"Digital\" style of reminder presentation.",
|
|
||||||
"isCommentAutoGenerated" : true
|
|
||||||
},
|
|
||||||
"Dimensions" : {
|
"Dimensions" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"pt-BR" : {
|
"pt-BR" : {
|
||||||
|
|
@ -11000,6 +10982,10 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Focus detection mode" : {
|
||||||
|
"comment" : "The title of a confirmation dialog that lets users choose between using the focus detection mode with or without the use of the SwiftUI Inspector (DevTools).",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
"Focus Status" : {
|
"Focus Status" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"pt-BR" : {
|
"pt-BR" : {
|
||||||
|
|
@ -11875,10 +11861,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Green" : {
|
|
||||||
"comment" : "Display name for the \"Green\" caps lock indicator tint mode.",
|
|
||||||
"isCommentAutoGenerated" : true
|
|
||||||
},
|
|
||||||
"Height:" : {
|
"Height:" : {
|
||||||
"extractionState" : "stale",
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
|
|
@ -15545,6 +15527,7 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Minutes" : {
|
"Minutes" : {
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"fr" : {
|
"fr" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
|
|
@ -17754,10 +17737,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Now Playing" : {
|
|
||||||
"comment" : "Localized name for each media controller type.",
|
|
||||||
"isCommentAutoGenerated" : true
|
|
||||||
},
|
|
||||||
"Off" : {
|
"Off" : {
|
||||||
"comment" : "Text displayed in a status label at the top of the screen indicating whether low power mode is on or off.",
|
"comment" : "Text displayed in a status label at the top of the screen indicating whether low power mode is on or off.",
|
||||||
"isCommentAutoGenerated" : true
|
"isCommentAutoGenerated" : true
|
||||||
|
|
@ -18124,10 +18103,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Panel" : {
|
|
||||||
"comment" : "Display name for the \"Panel\" clipboard display mode.",
|
|
||||||
"isCommentAutoGenerated" : true
|
|
||||||
},
|
|
||||||
"Panel mode shows clipboard in a floating window near the notch." : {
|
"Panel mode shows clipboard in a floating window near the notch." : {
|
||||||
"comment" : "A description of the \"Panel\" clipboard display mode.",
|
"comment" : "A description of the \"Panel\" clipboard display mode.",
|
||||||
"isCommentAutoGenerated" : true,
|
"isCommentAutoGenerated" : true,
|
||||||
|
|
@ -19722,10 +19697,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Ring" : {
|
|
||||||
"comment" : "Name of the timer progress style that shows a ring animation.",
|
|
||||||
"isCommentAutoGenerated" : true
|
|
||||||
},
|
|
||||||
"Rotation:" : {
|
"Rotation:" : {
|
||||||
"extractionState" : "stale",
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
|
|
@ -20546,10 +20517,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Separate Tab" : {
|
|
||||||
"comment" : "Name of the clipboard display mode option that opens the clipboard in a separate tab.",
|
|
||||||
"isCommentAutoGenerated" : true
|
|
||||||
},
|
|
||||||
"Separate Tab mode integrates Copied Items and Notes into a single view. If both are enabled, Notes appear on the right and Clipboard on the left." : {
|
"Separate Tab mode integrates Copied Items and Notes into a single view. If both are enabled, Notes appear on the right and Clipboard on the left." : {
|
||||||
"comment" : "A description of the \"Separate Tab\" clipboard display mode.",
|
"comment" : "A description of the \"Separate Tab\" clipboard display mode.",
|
||||||
"isCommentAutoGenerated" : true,
|
"isCommentAutoGenerated" : true,
|
||||||
|
|
@ -21604,14 +21571,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Shows screen assistant as a dropdown attached to the AI button" : {
|
|
||||||
"comment" : "Description of the \"Popover\" screen assistant display mode.",
|
|
||||||
"isCommentAutoGenerated" : true
|
|
||||||
},
|
|
||||||
"Shows screen assistant in a floating panel near the notch" : {
|
|
||||||
"comment" : "Description of the \"panel\" display mode for the screen assistant.",
|
|
||||||
"isCommentAutoGenerated" : true
|
|
||||||
},
|
|
||||||
"Shows the model's reasoning process before the final answer" : {
|
"Shows the model's reasoning process before the final answer" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"fr" : {
|
"fr" : {
|
||||||
|
|
@ -21674,10 +21633,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Skip forward or backward by ten seconds" : {
|
|
||||||
"comment" : "Description of the \"Skip forward or backward by ten seconds\" music skip behavior option.",
|
|
||||||
"isCommentAutoGenerated" : true
|
|
||||||
},
|
|
||||||
"Slider color" : {
|
"Slider color" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"ar" : {
|
"ar" : {
|
||||||
|
|
@ -22245,10 +22200,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Spotify" : {
|
|
||||||
"comment" : "Name of a media controller type for Spotify.",
|
|
||||||
"isCommentAutoGenerated" : true
|
|
||||||
},
|
|
||||||
"Square" : {
|
"Square" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"ar" : {
|
"ar" : {
|
||||||
|
|
@ -22357,10 +22308,6 @@
|
||||||
},
|
},
|
||||||
"Standard notch media controls are hidden. Re-enable the toggle above to restore them." : {
|
"Standard notch media controls are hidden. Re-enable the toggle above to restore them." : {
|
||||||
|
|
||||||
},
|
|
||||||
"Standard previous/next track controls" : {
|
|
||||||
"comment" : "Description of a music skip behavior option that uses standard previous/next track controls.",
|
|
||||||
"isCommentAutoGenerated" : true
|
|
||||||
},
|
},
|
||||||
"Start" : {
|
"Start" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
|
|
@ -23158,6 +23105,9 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"This is optional. You can change it any time from the menu bar." : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"This permanently removes every saved note and its attachments." : {
|
"This permanently removes every saved note and its attachments." : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
|
|
@ -23707,10 +23657,6 @@
|
||||||
},
|
},
|
||||||
"shouldTranslate" : false
|
"shouldTranslate" : false
|
||||||
},
|
},
|
||||||
"Track Skip" : {
|
|
||||||
"comment" : "Display name for the \"Track Skip\" music skip behavior option.",
|
|
||||||
"isCommentAutoGenerated" : true
|
|
||||||
},
|
|
||||||
"Transform" : {
|
"Transform" : {
|
||||||
"extractionState" : "stale",
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
|
|
@ -24415,6 +24361,10 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Use DevTools" : {
|
||||||
|
"comment" : "One option in the confirmation dialog in the `OnboardingView` that lets the user choose to use the developer tools for focus monitoring.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
"Use music visualizer spectrogram" : {
|
"Use music visualizer spectrogram" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"ar" : {
|
"ar" : {
|
||||||
|
|
@ -24537,6 +24487,10 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Use without DevTools" : {
|
||||||
|
"comment" : "An option in the onboarding flow that lets users choose to use focus detection without the option provided by the developer tools.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
"Used" : {
|
"Used" : {
|
||||||
"comment" : "A label describing the percentage of memory that is currently in use.",
|
"comment" : "A label describing the percentage of memory that is currently in use.",
|
||||||
"isCommentAutoGenerated" : true,
|
"isCommentAutoGenerated" : true,
|
||||||
|
|
@ -25613,10 +25567,6 @@
|
||||||
"comment" : "A footer note explaining the app's funding.",
|
"comment" : "A footer note explaining the app's funding.",
|
||||||
"isCommentAutoGenerated" : true
|
"isCommentAutoGenerated" : true
|
||||||
},
|
},
|
||||||
"Youtube Music" : {
|
|
||||||
"comment" : "Name for the \"Youtube Music\" media controller type in settings.",
|
|
||||||
"isCommentAutoGenerated" : true
|
|
||||||
},
|
|
||||||
"YouTube Music requires this third-party app to be installed: " : {
|
"YouTube Music requires this third-party app to be installed: " : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"ar" : {
|
"ar" : {
|
||||||
|
|
|
||||||
|
|
@ -251,6 +251,290 @@ struct CalendarView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct StandaloneCalendarView: View {
|
||||||
|
@EnvironmentObject var vm: DynamicIslandViewModel
|
||||||
|
@ObservedObject private var calendarManager = CalendarManager.shared
|
||||||
|
@State private var selectedDate = Date()
|
||||||
|
@State private var displayedMonth = Date()
|
||||||
|
@State private var datePickerScrollTarget: Date?
|
||||||
|
@Default(.hideAllDayEvents) private var hideAllDayEvents
|
||||||
|
@Default(.hideCompletedReminders) private var hideCompletedReminders
|
||||||
|
|
||||||
|
private let calendar = Calendar.current
|
||||||
|
private let weekdaySymbols = Calendar.current.shortWeekdaySymbols
|
||||||
|
|
||||||
|
private var monthTitle: String {
|
||||||
|
displayedMonth.formatted(.dateTime.month(.wide))
|
||||||
|
}
|
||||||
|
|
||||||
|
private var yearTitle: String {
|
||||||
|
displayedMonth.formatted(.dateTime.year())
|
||||||
|
}
|
||||||
|
|
||||||
|
private var monthDays: [Date] {
|
||||||
|
guard let monthInterval = calendar.dateInterval(of: .month, for: displayedMonth),
|
||||||
|
let firstWeekInterval = calendar.dateInterval(of: .weekOfMonth, for: monthInterval.start),
|
||||||
|
let lastDay = calendar.date(byAdding: .day, value: -1, to: monthInterval.end),
|
||||||
|
let lastWeekInterval = calendar.dateInterval(of: .weekOfMonth, for: lastDay)
|
||||||
|
else { return [] }
|
||||||
|
|
||||||
|
var days: [Date] = []
|
||||||
|
var current = firstWeekInterval.start
|
||||||
|
while current < lastWeekInterval.end {
|
||||||
|
days.append(current)
|
||||||
|
guard let next = calendar.date(byAdding: .day, value: 1, to: current) else { break }
|
||||||
|
current = next
|
||||||
|
}
|
||||||
|
return days
|
||||||
|
}
|
||||||
|
|
||||||
|
private var filteredEvents: [EventModel] {
|
||||||
|
EventListView.filteredEvents(
|
||||||
|
events: calendarManager.events,
|
||||||
|
hideCompletedReminders: hideCompletedReminders,
|
||||||
|
hideAllDayEvents: hideAllDayEvents
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var resolvedNotchHeight: CGFloat {
|
||||||
|
let height = vm.notchSize.height
|
||||||
|
return height > 0 ? height : openNotchSize.height
|
||||||
|
}
|
||||||
|
|
||||||
|
private var headerHeight: CGFloat {
|
||||||
|
max(24, vm.effectiveClosedNotchHeight)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var maxTabContentHeight: CGFloat {
|
||||||
|
let available = resolvedNotchHeight - headerHeight - 36
|
||||||
|
return max(130, available)
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
GeometryReader { geometry in
|
||||||
|
let paneSpacing: CGFloat = 12
|
||||||
|
let paneWidth = max((geometry.size.width - paneSpacing) / 2, 0)
|
||||||
|
let paneHeight = max(0, geometry.size.height)
|
||||||
|
|
||||||
|
HStack(alignment: .top, spacing: paneSpacing) {
|
||||||
|
leftPickerPane
|
||||||
|
.frame(width: paneWidth, alignment: .topLeading)
|
||||||
|
.frame(height: paneHeight, alignment: .topLeading)
|
||||||
|
.layoutPriority(1)
|
||||||
|
|
||||||
|
rightEventsPane
|
||||||
|
.frame(width: paneWidth, alignment: .topLeading)
|
||||||
|
.frame(height: paneHeight, alignment: .topLeading)
|
||||||
|
.layoutPriority(1)
|
||||||
|
}
|
||||||
|
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
|
||||||
|
.clipped()
|
||||||
|
}
|
||||||
|
.frame(height: maxTabContentHeight)
|
||||||
|
.clipped()
|
||||||
|
.onAppear {
|
||||||
|
selectedDate = Date.now
|
||||||
|
displayedMonth = selectedDate.startOfMonth
|
||||||
|
requestDatePickerCenterOnCurrentDate()
|
||||||
|
Task {
|
||||||
|
await calendarManager.updateCurrentDate(selectedDate)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onChange(of: selectedDate) { _, newDate in
|
||||||
|
withAnimation(.smooth(duration: 0.22)) {
|
||||||
|
displayedMonth = newDate.startOfMonth
|
||||||
|
}
|
||||||
|
Task {
|
||||||
|
await calendarManager.updateCurrentDate(newDate)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onChange(of: vm.notchState) { _, newState in
|
||||||
|
guard newState == .open else { return }
|
||||||
|
selectedDate = Date.now
|
||||||
|
displayedMonth = selectedDate.startOfMonth
|
||||||
|
requestDatePickerCenterOnCurrentDate()
|
||||||
|
Task {
|
||||||
|
await calendarManager.updateCurrentDate(selectedDate)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var leftPickerPane: some View {
|
||||||
|
GeometryReader { geometry in
|
||||||
|
let pickerViewportHeight = max(96, geometry.size.height - 56)
|
||||||
|
|
||||||
|
VStack(alignment: .leading, spacing: 10) {
|
||||||
|
HStack(alignment: .center) {
|
||||||
|
HStack(alignment: .firstTextBaseline, spacing: 4) {
|
||||||
|
Text(monthTitle)
|
||||||
|
.font(.title3)
|
||||||
|
.fontWeight(.semibold)
|
||||||
|
.foregroundStyle(.white)
|
||||||
|
Text(yearTitle)
|
||||||
|
.font(.subheadline)
|
||||||
|
.fontWeight(.medium)
|
||||||
|
.foregroundStyle(Color(white: 0.65))
|
||||||
|
}
|
||||||
|
Spacer()
|
||||||
|
HStack(spacing: 6) {
|
||||||
|
Button(action: showPreviousMonth) {
|
||||||
|
Image(systemName: "chevron.left")
|
||||||
|
.font(.system(size: 11, weight: .bold))
|
||||||
|
.frame(width: 24, height: 24)
|
||||||
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
|
||||||
|
Button(action: showNextMonth) {
|
||||||
|
Image(systemName: "chevron.right")
|
||||||
|
.font(.system(size: 11, weight: .bold))
|
||||||
|
.frame(width: 24, height: 24)
|
||||||
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
}
|
||||||
|
.foregroundStyle(.white)
|
||||||
|
}
|
||||||
|
|
||||||
|
ScrollViewReader { proxy in
|
||||||
|
VStack(spacing: 6) {
|
||||||
|
LazyVGrid(columns: Array(repeating: GridItem(.flexible(minimum: 14), spacing: 6), count: 7), spacing: 6) {
|
||||||
|
ForEach(weekdaySymbols, id: \.self) { symbol in
|
||||||
|
Text(symbol.prefix(1))
|
||||||
|
.font(.caption2)
|
||||||
|
.fontWeight(.semibold)
|
||||||
|
.foregroundStyle(Color(white: 0.55))
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ZStack {
|
||||||
|
ScrollView(.vertical, showsIndicators: false) {
|
||||||
|
LazyVGrid(columns: Array(repeating: GridItem(.flexible(minimum: 14), spacing: 6), count: 7), spacing: 6) {
|
||||||
|
ForEach(monthDays, id: \.self) { day in
|
||||||
|
dayCell(for: day)
|
||||||
|
.id(calendar.startOfDay(for: day))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(.bottom, 2)
|
||||||
|
}
|
||||||
|
.onChange(of: datePickerScrollTarget) { _, target in
|
||||||
|
guard let target else { return }
|
||||||
|
centerDatePicker(on: target, proxy: proxy)
|
||||||
|
}
|
||||||
|
|
||||||
|
LinearGradient(colors: [Color.black.opacity(0.65), .clear], startPoint: .top, endPoint: .bottom)
|
||||||
|
.frame(height: 16)
|
||||||
|
.allowsHitTesting(false)
|
||||||
|
.frame(maxHeight: .infinity, alignment: .top)
|
||||||
|
|
||||||
|
LinearGradient(colors: [.clear, Color.black.opacity(0.65)], startPoint: .top, endPoint: .bottom)
|
||||||
|
.frame(height: 16)
|
||||||
|
.allowsHitTesting(false)
|
||||||
|
.frame(maxHeight: .infinity, alignment: .bottom)
|
||||||
|
}
|
||||||
|
.frame(height: max(0, pickerViewportHeight - 22))
|
||||||
|
.clipped()
|
||||||
|
}
|
||||||
|
.frame(height: pickerViewportHeight)
|
||||||
|
}
|
||||||
|
.frame(height: pickerViewportHeight)
|
||||||
|
.clipped()
|
||||||
|
}
|
||||||
|
.frame(maxHeight: .infinity, alignment: .top)
|
||||||
|
}
|
||||||
|
.padding(.horizontal, 6)
|
||||||
|
.padding(.top, 4)
|
||||||
|
.clipped()
|
||||||
|
}
|
||||||
|
|
||||||
|
private var rightEventsPane: some View {
|
||||||
|
Group {
|
||||||
|
if filteredEvents.isEmpty {
|
||||||
|
EmptyEventsView(selectedDate: selectedDate)
|
||||||
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||||
|
} else {
|
||||||
|
StandaloneEventCardList(
|
||||||
|
events: filteredEvents,
|
||||||
|
showFullEventTitles: Defaults[.showFullEventTitles],
|
||||||
|
onToggleReminder: { reminderID, completed in
|
||||||
|
Task {
|
||||||
|
await calendarManager.setReminderCompleted(reminderID: reminderID, completed: completed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||||
|
.clipped()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func dayCell(for day: Date) -> some View {
|
||||||
|
let isCurrentMonth = calendar.isDate(day, equalTo: displayedMonth, toGranularity: .month)
|
||||||
|
let isSelected = calendar.isDate(day, inSameDayAs: selectedDate)
|
||||||
|
let isToday = calendar.isDateInToday(day)
|
||||||
|
|
||||||
|
return Button {
|
||||||
|
withAnimation(.smooth(duration: 0.18)) {
|
||||||
|
selectedDate = day
|
||||||
|
}
|
||||||
|
} label: {
|
||||||
|
ZStack {
|
||||||
|
if isSelected {
|
||||||
|
Circle()
|
||||||
|
.fill(Color.effectiveAccent)
|
||||||
|
.frame(width: 28, height: 28)
|
||||||
|
}
|
||||||
|
|
||||||
|
Text(day.formatted(.dateTime.day()))
|
||||||
|
.font(.system(size: 12, weight: isSelected ? .semibold : .medium))
|
||||||
|
.foregroundStyle(dayTextColor(isCurrentMonth: isCurrentMonth, isSelected: isSelected, isToday: isToday))
|
||||||
|
}
|
||||||
|
.frame(maxWidth: .infinity, minHeight: 30)
|
||||||
|
.contentShape(Rectangle())
|
||||||
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func dayTextColor(isCurrentMonth: Bool, isSelected: Bool, isToday: Bool) -> Color {
|
||||||
|
if isSelected { return .white }
|
||||||
|
if !isCurrentMonth { return Color(white: 0.35) }
|
||||||
|
if isToday { return Color.effectiveAccent }
|
||||||
|
return .white
|
||||||
|
}
|
||||||
|
|
||||||
|
private func showPreviousMonth() {
|
||||||
|
guard let newMonth = calendar.date(byAdding: .month, value: -1, to: displayedMonth) else { return }
|
||||||
|
withAnimation(.smooth(duration: 0.22)) {
|
||||||
|
displayedMonth = newMonth.startOfMonth
|
||||||
|
selectedDate = newMonth.startOfMonth
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func showNextMonth() {
|
||||||
|
guard let newMonth = calendar.date(byAdding: .month, value: 1, to: displayedMonth) else { return }
|
||||||
|
withAnimation(.smooth(duration: 0.22)) {
|
||||||
|
displayedMonth = newMonth.startOfMonth
|
||||||
|
selectedDate = newMonth.startOfMonth
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func requestDatePickerCenterOnCurrentDate() {
|
||||||
|
datePickerScrollTarget = calendar.startOfDay(for: selectedDate)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func centerDatePicker(on target: Date, proxy: ScrollViewProxy) {
|
||||||
|
let normalizedTarget = calendar.startOfDay(for: target)
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
withAnimation(.smooth(duration: 0.24)) {
|
||||||
|
proxy.scrollTo(normalizedTarget, anchor: .center)
|
||||||
|
}
|
||||||
|
if datePickerScrollTarget == normalizedTarget {
|
||||||
|
datePickerScrollTarget = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
struct EmptyEventsView: View {
|
struct EmptyEventsView: View {
|
||||||
let selectedDate: Date
|
let selectedDate: Date
|
||||||
|
|
||||||
|
|
@ -269,6 +553,170 @@ struct EmptyEventsView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private extension Date {
|
||||||
|
var startOfMonth: Date {
|
||||||
|
Calendar.current.date(from: Calendar.current.dateComponents([.year, .month], from: self)) ?? self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct StandaloneEventCardList: View {
|
||||||
|
@Environment(\.openURL) private var openURL
|
||||||
|
let events: [EventModel]
|
||||||
|
let showFullEventTitles: Bool
|
||||||
|
let onToggleReminder: (String, Bool) -> Void
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
ZStack {
|
||||||
|
ScrollView {
|
||||||
|
LazyVStack(spacing: 8) {
|
||||||
|
ForEach(events) { event in
|
||||||
|
eventCard(event)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(.vertical, 2)
|
||||||
|
}
|
||||||
|
.clipped()
|
||||||
|
|
||||||
|
LinearGradient(colors: [Color.black.opacity(0.65), .clear], startPoint: .top, endPoint: .bottom)
|
||||||
|
.frame(height: 16)
|
||||||
|
.allowsHitTesting(false)
|
||||||
|
.frame(maxHeight: .infinity, alignment: .top)
|
||||||
|
|
||||||
|
LinearGradient(colors: [.clear, Color.black.opacity(0.65)], startPoint: .top, endPoint: .bottom)
|
||||||
|
.frame(height: 16)
|
||||||
|
.allowsHitTesting(false)
|
||||||
|
.frame(maxHeight: .infinity, alignment: .bottom)
|
||||||
|
}
|
||||||
|
.clipped()
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
private func eventCard(_ event: EventModel) -> some View {
|
||||||
|
if event.type.isReminder {
|
||||||
|
Button {
|
||||||
|
if let url = event.calendarAppURL() {
|
||||||
|
openURL(url)
|
||||||
|
}
|
||||||
|
} label: {
|
||||||
|
reminderCard(event)
|
||||||
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
.padding(.horizontal, 2)
|
||||||
|
} else {
|
||||||
|
calendarEventCard(event)
|
||||||
|
.contentShape(Rectangle())
|
||||||
|
.onTapGesture {
|
||||||
|
if let url = event.calendarAppURL() {
|
||||||
|
openURL(url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(.horizontal, 2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func reminderCard(_ event: EventModel) -> some View {
|
||||||
|
let isCompleted: Bool
|
||||||
|
if case .reminder(let completed) = event.type {
|
||||||
|
isCompleted = completed
|
||||||
|
} else {
|
||||||
|
isCompleted = false
|
||||||
|
}
|
||||||
|
|
||||||
|
return HStack(spacing: 10) {
|
||||||
|
ReminderToggle(
|
||||||
|
isOn: Binding(
|
||||||
|
get: { isCompleted },
|
||||||
|
set: { newValue in onToggleReminder(event.id, newValue) }
|
||||||
|
),
|
||||||
|
color: Color(event.calendar.color)
|
||||||
|
)
|
||||||
|
|
||||||
|
VStack(alignment: .leading, spacing: 4) {
|
||||||
|
Text(event.title)
|
||||||
|
.font(.callout)
|
||||||
|
.fontWeight(.medium)
|
||||||
|
.foregroundColor(.white)
|
||||||
|
.lineLimit(showFullEventTitles ? nil : 2)
|
||||||
|
|
||||||
|
if event.isAllDay {
|
||||||
|
Text("All-day")
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundColor(Color(white: 0.65))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(minLength: 8)
|
||||||
|
|
||||||
|
if !event.isAllDay {
|
||||||
|
Text(event.start, style: .time)
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundColor(Color(white: 0.75))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(.horizontal, 11)
|
||||||
|
.padding(.vertical, 8)
|
||||||
|
.frame(maxWidth: .infinity, minHeight: 50, alignment: .leading)
|
||||||
|
.background(
|
||||||
|
RoundedRectangle(cornerRadius: 14, style: .continuous)
|
||||||
|
.fill(Color.white.opacity(0.05))
|
||||||
|
)
|
||||||
|
.opacity(isCompleted ? 0.55 : 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func calendarEventCard(_ event: EventModel) -> some View {
|
||||||
|
HStack(alignment: .top, spacing: 10) {
|
||||||
|
RoundedRectangle(cornerRadius: 2)
|
||||||
|
.fill(Color(event.calendar.color))
|
||||||
|
.frame(width: 4, height: 36)
|
||||||
|
|
||||||
|
VStack(alignment: .leading, spacing: 4) {
|
||||||
|
Text(event.title)
|
||||||
|
.font(.callout)
|
||||||
|
.fontWeight(.semibold)
|
||||||
|
.foregroundColor(.white)
|
||||||
|
.lineLimit(showFullEventTitles ? nil : 2)
|
||||||
|
|
||||||
|
if let location = event.location, !location.isEmpty {
|
||||||
|
Text(location)
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundColor(Color(white: 0.65))
|
||||||
|
.lineLimit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if let conferenceURL = event.conferenceURL {
|
||||||
|
ConferenceJoinButton(url: conferenceURL, event: event)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(minLength: 8)
|
||||||
|
|
||||||
|
VStack(alignment: .trailing, spacing: 2) {
|
||||||
|
if event.isAllDay {
|
||||||
|
Text("All-day")
|
||||||
|
.font(.caption)
|
||||||
|
.fontWeight(.medium)
|
||||||
|
.foregroundColor(.white)
|
||||||
|
} else {
|
||||||
|
Text(event.start, style: .time)
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundColor(.white)
|
||||||
|
Text(event.end, style: .time)
|
||||||
|
.font(.caption2)
|
||||||
|
.foregroundColor(Color(white: 0.65))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(.horizontal, 11)
|
||||||
|
.padding(.vertical, 8)
|
||||||
|
.frame(maxWidth: .infinity, minHeight: 50, alignment: .leading)
|
||||||
|
.background(
|
||||||
|
RoundedRectangle(cornerRadius: 14, style: .continuous)
|
||||||
|
.fill(Color.white.opacity(0.05))
|
||||||
|
)
|
||||||
|
.opacity(event.eventStatus == .ended && Calendar.current.isDateInToday(event.start) ? 0.6 : 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
struct EventListView: View {
|
struct EventListView: View {
|
||||||
@Environment(\.openURL) private var openURL
|
@Environment(\.openURL) private var openURL
|
||||||
@ObservedObject private var calendarManager = CalendarManager.shared
|
@ObservedObject private var calendarManager = CalendarManager.shared
|
||||||
|
|
|
||||||
|
|
@ -132,11 +132,9 @@ struct ExtensionLockScreenWidgetView: View {
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private func liquidGlassBackground(shape: RoundedRectangle) -> some View {
|
private func liquidGlassBackground(shape: RoundedRectangle) -> some View {
|
||||||
TimelineView(.periodic(from: .now, by: 1.0 / 30.0)) { context in
|
LiquidGlassBackground(variant: requestedLiquidGlassVariant, cornerRadius: descriptor.cornerRadius) {
|
||||||
LiquidGlassBackground(variant: requestedLiquidGlassVariant, cornerRadius: descriptor.cornerRadius, trigger: context.date.timeIntervalSinceReferenceDate) {
|
|
||||||
Color.black.opacity(0.08)
|
Color.black.opacity(0.08)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
.overlay {
|
.overlay {
|
||||||
tintOverlay(shape: shape)
|
tintOverlay(shape: shape)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -356,12 +356,17 @@ struct LockScreenMusicPanel: View {
|
||||||
collapseWorkItem = nil
|
collapseWorkItem = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var isProgressTimelinePaused: Bool {
|
||||||
|
!musicManager.isPlaying || musicManager.isLiveStream || musicManager.playbackRate <= 0
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Progress Bar
|
// MARK: - Progress Bar
|
||||||
|
|
||||||
private var progressBar: some View {
|
private var progressBar: some View {
|
||||||
TimelineView(
|
TimelineView(
|
||||||
.animation(
|
.animation(
|
||||||
minimumInterval: (!musicManager.isLiveStream && musicManager.playbackRate > 0) ? 0.1 : nil
|
minimumInterval: 0.1,
|
||||||
|
paused: isProgressTimelinePaused
|
||||||
)
|
)
|
||||||
) { timeline in
|
) { timeline in
|
||||||
MusicSliderView(
|
MusicSliderView(
|
||||||
|
|
@ -792,11 +797,9 @@ struct LockScreenMusicPanel: View {
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private var customLiquidPanelBackdrop: some View {
|
private var customLiquidPanelBackdrop: some View {
|
||||||
TimelineView(.periodic(from: .now, by: 1.0 / 30.0)) { context in
|
LiquidGlassBackground(variant: musicGlassVariant, cornerRadius: panelCornerRadius) {
|
||||||
LiquidGlassBackground(variant: musicGlassVariant, cornerRadius: panelCornerRadius, trigger: context.date.timeIntervalSinceReferenceDate) {
|
|
||||||
Color.white.opacity(0.04)
|
Color.white.opacity(0.04)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||||
.allowsHitTesting(false)
|
.allowsHitTesting(false)
|
||||||
.accessibilityHidden(true)
|
.accessibilityHidden(true)
|
||||||
|
|
@ -873,12 +876,10 @@ struct LockScreenMusicPanel: View {
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private func customLiquidAlbumArtBackground(cornerRadius: CGFloat) -> some View {
|
private func customLiquidAlbumArtBackground(cornerRadius: CGFloat) -> some View {
|
||||||
TimelineView(.periodic(from: .now, by: 1.0 / 30.0)) { context in
|
LiquidGlassBackground(variant: musicGlassVariant, cornerRadius: cornerRadius) {
|
||||||
LiquidGlassBackground(variant: musicGlassVariant, cornerRadius: cornerRadius, trigger: context.date.timeIntervalSinceReferenceDate) {
|
|
||||||
Color.clear
|
Color.clear
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private func logGlassState(reason: String) {
|
private func logGlassState(reason: String) {
|
||||||
let snapshot = GlassLogSnapshot(
|
let snapshot = GlassLogSnapshot(
|
||||||
|
|
|
||||||
|
|
@ -154,12 +154,10 @@ struct LockScreenTimerWidget: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
private var customLiquidBackground: some View {
|
private var customLiquidBackground: some View {
|
||||||
TimelineView(.periodic(from: .now, by: 1.0 / 30.0)) { context in
|
LiquidGlassBackground(variant: timerGlassVariant, cornerRadius: Self.cornerRadius) {
|
||||||
LiquidGlassBackground(variant: timerGlassVariant, cornerRadius: Self.cornerRadius, trigger: context.date.timeIntervalSinceReferenceDate) {
|
|
||||||
Color.black.opacity(0.12)
|
Color.black.opacity(0.12)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private var frostedBackground: some View {
|
private var frostedBackground: some View {
|
||||||
RoundedRectangle(cornerRadius: Self.cornerRadius, style: .continuous)
|
RoundedRectangle(cornerRadius: Self.cornerRadius, style: .continuous)
|
||||||
|
|
|
||||||
|
|
@ -552,7 +552,14 @@ struct NotchHomeView: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
if Defaults[.showCalendar] {
|
if Defaults[.showCalendar] {
|
||||||
|
Group {
|
||||||
|
if showStandardMediaControls {
|
||||||
CalendarView()
|
CalendarView()
|
||||||
|
} else {
|
||||||
|
StandaloneCalendarView()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
.onHover { isHovering in
|
.onHover { isHovering in
|
||||||
vm.isHoveringCalendar = isHovering
|
vm.isHoveringCalendar = isHovering
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,8 @@ struct CustomOSDView: View {
|
||||||
@Binding var icon: String
|
@Binding var icon: String
|
||||||
|
|
||||||
@Default(.osdMaterial) var osdMaterial
|
@Default(.osdMaterial) var osdMaterial
|
||||||
|
@Default(.osdLiquidGlassCustomizationMode) var osdLiquidGlassCustomizationMode
|
||||||
|
@Default(.osdLiquidGlassVariant) var osdLiquidGlassVariant
|
||||||
@Default(.osdIconColorStyle) var osdIconColorStyle
|
@Default(.osdIconColorStyle) var osdIconColorStyle
|
||||||
@Environment(\.colorScheme) var colorScheme
|
@Environment(\.colorScheme) var colorScheme
|
||||||
|
|
||||||
|
|
@ -160,11 +162,20 @@ struct CustomOSDView: View {
|
||||||
.fill(.ultraThinMaterial)
|
.fill(.ultraThinMaterial)
|
||||||
case .liquid:
|
case .liquid:
|
||||||
if #available(macOS 26.0, *) {
|
if #available(macOS 26.0, *) {
|
||||||
|
if osdLiquidGlassCustomizationMode == .customLiquid {
|
||||||
|
LiquidGlassBackground(
|
||||||
|
variant: osdLiquidGlassVariant,
|
||||||
|
cornerRadius: 18
|
||||||
|
) {
|
||||||
|
Color.white.opacity(0.04)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
RoundedRectangle(cornerRadius: 18, style: .continuous)
|
RoundedRectangle(cornerRadius: 18, style: .continuous)
|
||||||
.glassEffect(
|
.glassEffect(
|
||||||
.clear.interactive(),
|
.clear.interactive(),
|
||||||
in: .rect(cornerRadius: 18)
|
in: .rect(cornerRadius: 18)
|
||||||
)
|
)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
RoundedRectangle(cornerRadius: 18, style: .continuous)
|
RoundedRectangle(cornerRadius: 18, style: .continuous)
|
||||||
.fill(.ultraThinMaterial)
|
.fill(.ultraThinMaterial)
|
||||||
|
|
|
||||||
|
|
@ -46,6 +46,9 @@ struct VerticalHUDView: View {
|
||||||
@Default(.verticalHUDHeight) var hudHeight
|
@Default(.verticalHUDHeight) var hudHeight
|
||||||
@Default(.verticalHUDWidth) var hudWidth
|
@Default(.verticalHUDWidth) var hudWidth
|
||||||
@Default(.verticalHUDUseAccentColor) var useAccentColor
|
@Default(.verticalHUDUseAccentColor) var useAccentColor
|
||||||
|
@Default(.verticalHUDMaterial) var verticalHUDMaterial
|
||||||
|
@Default(.verticalHUDLiquidGlassCustomizationMode) var verticalHUDLiquidGlassCustomizationMode
|
||||||
|
@Default(.verticalHUDLiquidGlassVariant) var verticalHUDLiquidGlassVariant
|
||||||
@Default(.verticalHUDInteractive) var isInteractive
|
@Default(.verticalHUDInteractive) var isInteractive
|
||||||
|
|
||||||
@Environment(\.colorScheme) var colorScheme
|
@Environment(\.colorScheme) var colorScheme
|
||||||
|
|
@ -68,8 +71,8 @@ struct VerticalHUDView: View {
|
||||||
// The Actual HUD Pill
|
// The Actual HUD Pill
|
||||||
ZStack {
|
ZStack {
|
||||||
// Background
|
// Background
|
||||||
Capsule()
|
verticalBackground
|
||||||
.fill(.ultraThinMaterial)
|
.clipShape(Capsule())
|
||||||
.overlay {
|
.overlay {
|
||||||
Capsule()
|
Capsule()
|
||||||
.stroke(.white.opacity(0.1), lineWidth: 1)
|
.stroke(.white.opacity(0.1), lineWidth: 1)
|
||||||
|
|
@ -253,5 +256,39 @@ struct VerticalHUDView: View {
|
||||||
}
|
}
|
||||||
return .secondary
|
return .secondary
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
private var verticalBackground: some View {
|
||||||
|
switch verticalHUDMaterial {
|
||||||
|
case .frosted:
|
||||||
|
Capsule().fill(.ultraThinMaterial)
|
||||||
|
case .liquid:
|
||||||
|
if #available(macOS 26.0, *) {
|
||||||
|
if verticalHUDLiquidGlassCustomizationMode == .customLiquid {
|
||||||
|
LiquidGlassBackground(
|
||||||
|
variant: verticalHUDLiquidGlassVariant,
|
||||||
|
cornerRadius: max(currentWidth, hudWidth)
|
||||||
|
) {
|
||||||
|
Color.white.opacity(0.04)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Capsule()
|
||||||
|
.fill(.clear)
|
||||||
|
.glassEffect(
|
||||||
|
.clear.interactive(),
|
||||||
|
in: .capsule
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Capsule().fill(.ultraThinMaterial)
|
||||||
|
}
|
||||||
|
case .solidDark:
|
||||||
|
Capsule().fill(Color.black.opacity(0.85))
|
||||||
|
case .solidLight:
|
||||||
|
Capsule().fill(Color.white.opacity(0.85))
|
||||||
|
case .solidAuto:
|
||||||
|
Capsule().fill((colorScheme == .dark ? Color.black : Color.white).opacity(0.85))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@
|
||||||
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import AVFoundation
|
import AVFoundation
|
||||||
|
import Defaults
|
||||||
|
|
||||||
enum OnboardingStep {
|
enum OnboardingStep {
|
||||||
case welcome
|
case welcome
|
||||||
|
|
@ -36,6 +37,8 @@ private let calendarService = CalendarService()
|
||||||
|
|
||||||
struct OnboardingView: View {
|
struct OnboardingView: View {
|
||||||
@State private var step: OnboardingStep = .welcome
|
@State private var step: OnboardingStep = .welcome
|
||||||
|
@State private var showFocusMonitoringChoice = false
|
||||||
|
@State private var didPresentFocusMonitoringChoice = false
|
||||||
let onFinish: () -> Void
|
let onFinish: () -> Void
|
||||||
let onOpenSettings: () -> Void
|
let onOpenSettings: () -> Void
|
||||||
|
|
||||||
|
|
@ -120,6 +123,28 @@ struct OnboardingView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.frame(width: 400, height: 600)
|
.frame(width: 400, height: 600)
|
||||||
|
.onAppear {
|
||||||
|
guard !didPresentFocusMonitoringChoice else { return }
|
||||||
|
didPresentFocusMonitoringChoice = true
|
||||||
|
showFocusMonitoringChoice = true
|
||||||
|
}
|
||||||
|
.confirmationDialog(
|
||||||
|
"Focus detection mode",
|
||||||
|
isPresented: $showFocusMonitoringChoice,
|
||||||
|
titleVisibility: .visible
|
||||||
|
) {
|
||||||
|
Button("Use DevTools") {
|
||||||
|
Defaults[.focusMonitoringMode] = .useDevTools
|
||||||
|
}
|
||||||
|
|
||||||
|
Button("Use without DevTools") {
|
||||||
|
Defaults[.focusMonitoringMode] = .withoutDevTools
|
||||||
|
}
|
||||||
|
|
||||||
|
Button("Later", role: .cancel) {}
|
||||||
|
} message: {
|
||||||
|
Text("This is optional. You can change it any time from the menu bar.")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Permission Request Logic
|
// MARK: - Permission Request Logic
|
||||||
|
|
|
||||||
|
|
@ -1369,6 +1369,9 @@ private struct HUDAndOSDSettingsView: View {
|
||||||
@Default(.verticalHUDWidth) var verticalHUDWidth
|
@Default(.verticalHUDWidth) var verticalHUDWidth
|
||||||
@Default(.verticalHUDPadding) var verticalHUDPadding
|
@Default(.verticalHUDPadding) var verticalHUDPadding
|
||||||
@Default(.verticalHUDUseAccentColor) var verticalHUDUseAccentColor
|
@Default(.verticalHUDUseAccentColor) var verticalHUDUseAccentColor
|
||||||
|
@Default(.verticalHUDMaterial) var verticalHUDMaterial
|
||||||
|
@Default(.verticalHUDLiquidGlassCustomizationMode) var verticalHUDLiquidGlassCustomizationMode
|
||||||
|
@Default(.verticalHUDLiquidGlassVariant) var verticalHUDLiquidGlassVariant
|
||||||
|
|
||||||
// Circular HUD Props
|
// Circular HUD Props
|
||||||
@Default(.circularHUDShowValue) var circularHUDShowValue
|
@Default(.circularHUDShowValue) var circularHUDShowValue
|
||||||
|
|
@ -1391,6 +1394,27 @@ private struct HUDAndOSDSettingsView: View {
|
||||||
Color(nsColor: .controlBackgroundColor)
|
Color(nsColor: .controlBackgroundColor)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var liquidVariantRange: ClosedRange<Double> {
|
||||||
|
Double(LiquidGlassVariant.supportedRange.lowerBound)...Double(LiquidGlassVariant.supportedRange.upperBound)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var availableVerticalMaterials: [OSDMaterial] {
|
||||||
|
if #available(macOS 26.0, *) {
|
||||||
|
return OSDMaterial.allCases
|
||||||
|
}
|
||||||
|
return OSDMaterial.allCases.filter { $0 != .liquid }
|
||||||
|
}
|
||||||
|
|
||||||
|
private var verticalLiquidVariantBinding: Binding<Double> {
|
||||||
|
Binding(
|
||||||
|
get: { Double(verticalHUDLiquidGlassVariant.rawValue) },
|
||||||
|
set: { newValue in
|
||||||
|
let raw = Int(newValue.rounded())
|
||||||
|
verticalHUDLiquidGlassVariant = LiquidGlassVariant.clamped(raw)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(spacing: 20) {
|
VStack(spacing: 20) {
|
||||||
HStack(spacing: 16) {
|
HStack(spacing: 16) {
|
||||||
|
|
@ -1608,6 +1632,39 @@ private struct HUDAndOSDSettingsView: View {
|
||||||
Toggle("Show Percentage", isOn: $verticalHUDShowValue)
|
Toggle("Show Percentage", isOn: $verticalHUDShowValue)
|
||||||
Toggle("Use Accent Color", isOn: $verticalHUDUseAccentColor)
|
Toggle("Use Accent Color", isOn: $verticalHUDUseAccentColor)
|
||||||
Toggle("Interactive (Drag to Change)", isOn: $verticalHUDInteractive)
|
Toggle("Interactive (Drag to Change)", isOn: $verticalHUDInteractive)
|
||||||
|
Picker("Material", selection: $verticalHUDMaterial) {
|
||||||
|
ForEach(availableVerticalMaterials, id: \.self) { material in
|
||||||
|
Text(material.rawValue).tag(material)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if verticalHUDMaterial == .liquid {
|
||||||
|
if #available(macOS 26.0, *) {
|
||||||
|
Picker("Glass mode", selection: $verticalHUDLiquidGlassCustomizationMode) {
|
||||||
|
ForEach(LockScreenGlassCustomizationMode.allCases) { mode in
|
||||||
|
Text(mode.rawValue).tag(mode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.pickerStyle(.segmented)
|
||||||
|
|
||||||
|
if verticalHUDLiquidGlassCustomizationMode == .customLiquid {
|
||||||
|
VStack(alignment: .leading, spacing: 6) {
|
||||||
|
HStack {
|
||||||
|
Text("Custom liquid variant")
|
||||||
|
Spacer()
|
||||||
|
Text("v\(verticalHUDLiquidGlassVariant.rawValue)")
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
}
|
||||||
|
Slider(value: verticalLiquidVariantBinding, in: liquidVariantRange, step: 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Text("Custom Liquid is available on macOS 26 or later.")
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
}
|
||||||
|
}
|
||||||
Defaults.Toggle("Color-coded Volume", key: .useColorCodedVolumeDisplay)
|
Defaults.Toggle("Color-coded Volume", key: .useColorCodedVolumeDisplay)
|
||||||
if Defaults[.useColorCodedVolumeDisplay] {
|
if Defaults[.useColorCodedVolumeDisplay] {
|
||||||
Defaults.Toggle("Smooth color transitions", key: .useSmoothColorGradient)
|
Defaults.Toggle("Smooth color transitions", key: .useSmoothColorGradient)
|
||||||
|
|
@ -1719,6 +1776,12 @@ private struct HUDAndOSDSettingsView: View {
|
||||||
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
|
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
|
||||||
.background(paneBackgroundColor)
|
.background(paneBackgroundColor)
|
||||||
.navigationTitle("Controls")
|
.navigationTitle("Controls")
|
||||||
|
.onAppear {
|
||||||
|
if #unavailable(macOS 26.0), verticalHUDMaterial == .liquid {
|
||||||
|
verticalHUDMaterial = .frosted
|
||||||
|
verticalHUDLiquidGlassCustomizationMode = .standard
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -4457,7 +4520,6 @@ private struct LockScreenGlassVariantPreviewCell: View {
|
||||||
private let cornerRadius: CGFloat = 16
|
private let cornerRadius: CGFloat = 16
|
||||||
private let previewCornerRadius: CGFloat = 14
|
private let previewCornerRadius: CGFloat = 14
|
||||||
private let previewSize = CGSize(width: 190, height: 96)
|
private let previewSize = CGSize(width: 190, height: 96)
|
||||||
@State private var previewToken: Double = 0
|
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ZStack {
|
ZStack {
|
||||||
|
|
@ -4486,7 +4548,6 @@ private struct LockScreenGlassVariantPreviewCell: View {
|
||||||
Logger.log("Lock screen glass preview disappeared", category: .performance)
|
Logger.log("Lock screen glass preview disappeared", category: .performance)
|
||||||
}
|
}
|
||||||
.onChange(of: variant) { _, newValue in
|
.onChange(of: variant) { _, newValue in
|
||||||
previewToken = Date().timeIntervalSinceReferenceDate
|
|
||||||
Logger.log("Lock screen glass preview variant changed to v\(newValue.rawValue)", category: .performance)
|
Logger.log("Lock screen glass preview variant changed to v\(newValue.rawValue)", category: .performance)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -4494,9 +4555,7 @@ private struct LockScreenGlassVariantPreviewCell: View {
|
||||||
private var liquidGlassPreview: some View {
|
private var liquidGlassPreview: some View {
|
||||||
LiquidGlassBackground(
|
LiquidGlassBackground(
|
||||||
variant: variant,
|
variant: variant,
|
||||||
cornerRadius: previewCornerRadius,
|
cornerRadius: previewCornerRadius
|
||||||
trigger: previewToken,
|
|
||||||
jitterEnabled: false
|
|
||||||
) {
|
) {
|
||||||
Color.white.opacity(0.04)
|
Color.white.opacity(0.04)
|
||||||
}
|
}
|
||||||
|
|
@ -5608,34 +5667,34 @@ struct TimerSettings: View {
|
||||||
let nextIndex = timerPresets.count + 1
|
let nextIndex = timerPresets.count + 1
|
||||||
let defaultColor = Defaults[.accentColor]
|
let defaultColor = Defaults[.accentColor]
|
||||||
let newPreset = TimerPreset(name: "Preset \(nextIndex)", duration: 5 * 60, color: defaultColor)
|
let newPreset = TimerPreset(name: "Preset \(nextIndex)", duration: 5 * 60, color: defaultColor)
|
||||||
withAnimation(.smooth) {
|
_ = withAnimation(.smooth) {
|
||||||
timerPresets.append(newPreset)
|
timerPresets.append(newPreset)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func movePresetUp(_ index: Int) {
|
private func movePresetUp(_ index: Int) {
|
||||||
guard index > timerPresets.startIndex else { return }
|
guard index > timerPresets.startIndex else { return }
|
||||||
withAnimation(.smooth) {
|
_ = withAnimation(.smooth) {
|
||||||
timerPresets.swapAt(index, index - 1)
|
timerPresets.swapAt(index, index - 1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func movePresetDown(_ index: Int) {
|
private func movePresetDown(_ index: Int) {
|
||||||
guard index < timerPresets.index(before: timerPresets.endIndex) else { return }
|
guard index < timerPresets.index(before: timerPresets.endIndex) else { return }
|
||||||
withAnimation(.smooth) {
|
_ = withAnimation(.smooth) {
|
||||||
timerPresets.swapAt(index, index + 1)
|
timerPresets.swapAt(index, index + 1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func removePreset(_ index: Int) {
|
private func removePreset(_ index: Int) {
|
||||||
guard timerPresets.indices.contains(index) else { return }
|
guard timerPresets.indices.contains(index) else { return }
|
||||||
withAnimation(.smooth) {
|
_ = withAnimation(.smooth) {
|
||||||
timerPresets.remove(at: index)
|
timerPresets.remove(at: index)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func resetPresets() {
|
private func resetPresets() {
|
||||||
withAnimation(.smooth) {
|
_ = withAnimation(.smooth) {
|
||||||
timerPresets = TimerPreset.defaultPresets
|
timerPresets = TimerPreset.defaultPresets
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -6550,6 +6609,8 @@ struct CustomOSDSettings: View {
|
||||||
@Default(.enableOSDBrightness) var enableOSDBrightness
|
@Default(.enableOSDBrightness) var enableOSDBrightness
|
||||||
@Default(.enableOSDKeyboardBacklight) var enableOSDKeyboardBacklight
|
@Default(.enableOSDKeyboardBacklight) var enableOSDKeyboardBacklight
|
||||||
@Default(.osdMaterial) var osdMaterial
|
@Default(.osdMaterial) var osdMaterial
|
||||||
|
@Default(.osdLiquidGlassCustomizationMode) var osdLiquidGlassCustomizationMode
|
||||||
|
@Default(.osdLiquidGlassVariant) var osdLiquidGlassVariant
|
||||||
@Default(.osdIconColorStyle) var osdIconColorStyle
|
@Default(.osdIconColorStyle) var osdIconColorStyle
|
||||||
@Default(.enableSystemHUD) var enableSystemHUD
|
@Default(.enableSystemHUD) var enableSystemHUD
|
||||||
@ObservedObject private var accessibilityPermission = AccessibilityPermissionStore.shared
|
@ObservedObject private var accessibilityPermission = AccessibilityPermissionStore.shared
|
||||||
|
|
@ -6566,6 +6627,27 @@ struct CustomOSDSettings: View {
|
||||||
accessibilityPermission.isAuthorized
|
accessibilityPermission.isAuthorized
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var availableOSDMaterials: [OSDMaterial] {
|
||||||
|
if #available(macOS 26.0, *) {
|
||||||
|
return OSDMaterial.allCases
|
||||||
|
}
|
||||||
|
return OSDMaterial.allCases.filter { $0 != .liquid }
|
||||||
|
}
|
||||||
|
|
||||||
|
private var liquidVariantRange: ClosedRange<Double> {
|
||||||
|
Double(LiquidGlassVariant.supportedRange.lowerBound)...Double(LiquidGlassVariant.supportedRange.upperBound)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var osdLiquidVariantBinding: Binding<Double> {
|
||||||
|
Binding(
|
||||||
|
get: { Double(osdLiquidGlassVariant.rawValue) },
|
||||||
|
set: { newValue in
|
||||||
|
let raw = Int(newValue.rounded())
|
||||||
|
osdLiquidGlassVariant = LiquidGlassVariant.clamped(raw)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Form {
|
Form {
|
||||||
if !hasAccessibilityPermission {
|
if !hasAccessibilityPermission {
|
||||||
|
|
@ -6598,7 +6680,7 @@ struct CustomOSDSettings: View {
|
||||||
|
|
||||||
Section {
|
Section {
|
||||||
Picker("Material", selection: $osdMaterial) {
|
Picker("Material", selection: $osdMaterial) {
|
||||||
ForEach(OSDMaterial.allCases, id: \.self) { material in
|
ForEach(availableOSDMaterials, id: \.self) { material in
|
||||||
Text(material.rawValue).tag(material)
|
Text(material.rawValue).tag(material)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -6607,6 +6689,34 @@ struct CustomOSDSettings: View {
|
||||||
previewValue = previewValue == 0.65 ? 0.651 : 0.65
|
previewValue = previewValue == 0.65 ? 0.651 : 0.65
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if osdMaterial == .liquid {
|
||||||
|
if #available(macOS 26.0, *) {
|
||||||
|
Picker("Glass mode", selection: $osdLiquidGlassCustomizationMode) {
|
||||||
|
ForEach(LockScreenGlassCustomizationMode.allCases) { mode in
|
||||||
|
Text(mode.rawValue).tag(mode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.pickerStyle(.segmented)
|
||||||
|
|
||||||
|
if osdLiquidGlassCustomizationMode == .customLiquid {
|
||||||
|
VStack(alignment: .leading, spacing: 6) {
|
||||||
|
HStack {
|
||||||
|
Text("Custom liquid variant")
|
||||||
|
Spacer()
|
||||||
|
Text("v\(osdLiquidGlassVariant.rawValue)")
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
}
|
||||||
|
Slider(value: osdLiquidVariantBinding, in: liquidVariantRange, step: 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Text("Custom Liquid is available on macOS 26 or later.")
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Picker("Icon & Progress Color", selection: $osdIconColorStyle) {
|
Picker("Icon & Progress Color", selection: $osdIconColorStyle) {
|
||||||
ForEach(OSDIconColorStyle.allCases, id: \.self) { style in
|
ForEach(OSDIconColorStyle.allCases, id: \.self) { style in
|
||||||
Text(style.rawValue).tag(style)
|
Text(style.rawValue).tag(style)
|
||||||
|
|
@ -6682,6 +6792,10 @@ struct CustomOSDSettings: View {
|
||||||
.navigationTitle("Custom OSD")
|
.navigationTitle("Custom OSD")
|
||||||
.onAppear {
|
.onAppear {
|
||||||
accessibilityPermission.refreshStatus()
|
accessibilityPermission.refreshStatus()
|
||||||
|
if #unavailable(macOS 26.0), osdMaterial == .liquid {
|
||||||
|
osdMaterial = .frosted
|
||||||
|
osdLiquidGlassCustomizationMode = .standard
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.onChange(of: accessibilityPermission.isAuthorized) { _, granted in
|
.onChange(of: accessibilityPermission.isAuthorized) { _, granted in
|
||||||
if !granted {
|
if !granted {
|
||||||
|
|
|
||||||
|
|
@ -59,9 +59,10 @@ class SettingsWindowController: NSWindowController {
|
||||||
window.titleVisibility = .visible
|
window.titleVisibility = .visible
|
||||||
window.toolbarStyle = .unified
|
window.toolbarStyle = .unified
|
||||||
window.isMovableByWindowBackground = true
|
window.isMovableByWindowBackground = true
|
||||||
|
window.level = .normal
|
||||||
|
|
||||||
// Make it behave like a regular app window with proper Spaces support
|
// Make it behave like a regular app window with proper Spaces support
|
||||||
window.collectionBehavior = [.managed, .participatesInCycle, .fullScreenAuxiliary]
|
window.collectionBehavior = [.managed, .participatesInCycle]
|
||||||
|
|
||||||
// Ensure proper window behavior
|
// Ensure proper window behavior
|
||||||
window.hidesOnDeactivate = false
|
window.hidesOnDeactivate = false
|
||||||
|
|
@ -86,6 +87,10 @@ class SettingsWindowController: NSWindowController {
|
||||||
// Ensure window exists
|
// Ensure window exists
|
||||||
_ = window
|
_ = window
|
||||||
|
|
||||||
|
// Reassert regular window semantics in case any prior state mutated this window.
|
||||||
|
window?.level = .normal
|
||||||
|
window?.collectionBehavior = [.managed, .participatesInCycle]
|
||||||
|
|
||||||
// If window is already visible, bring it to front properly
|
// If window is already visible, bring it to front properly
|
||||||
if window?.isVisible == true {
|
if window?.isVisible == true {
|
||||||
NSApp.activate(ignoringOtherApps: true)
|
NSApp.activate(ignoringOtherApps: true)
|
||||||
|
|
|
||||||
|
|
@ -99,9 +99,6 @@ struct TabSelectionView: View {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
DispatchQueue.main.async {
|
|
||||||
ensureValidSelection(with: tabsArray)
|
|
||||||
}
|
|
||||||
return tabsArray
|
return tabsArray
|
||||||
}
|
}
|
||||||
var body: some View {
|
var body: some View {
|
||||||
|
|
@ -110,7 +107,7 @@ struct TabSelectionView: View {
|
||||||
let isSelected = isSelected(tab)
|
let isSelected = isSelected(tab)
|
||||||
let activeAccent = tab.accentColor ?? .white
|
let activeAccent = tab.accentColor ?? .white
|
||||||
TabButton(label: tab.label, icon: tab.icon, selected: isSelected) {
|
TabButton(label: tab.label, icon: tab.icon, selected: isSelected) {
|
||||||
withAnimation(.smooth) {
|
withAnimation(.smooth(duration: 0.3)) {
|
||||||
if tab.view == .extensionExperience {
|
if tab.view == .extensionExperience {
|
||||||
coordinator.selectedExtensionExperienceID = tab.experienceID
|
coordinator.selectedExtensionExperienceID = tab.experienceID
|
||||||
}
|
}
|
||||||
|
|
@ -135,6 +132,9 @@ struct TabSelectionView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.clipShape(Capsule())
|
.clipShape(Capsule())
|
||||||
|
.onAppear {
|
||||||
|
ensureValidSelection(with: tabs)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var extensionTabsEnabled: Bool {
|
private var extensionTabsEnabled: Bool {
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,108 @@ import SwiftUI
|
||||||
import AppKit
|
import AppKit
|
||||||
import Defaults
|
import Defaults
|
||||||
|
|
||||||
|
private final class LiquidGlassContainerView: NSView {
|
||||||
|
weak var glassView: NSView?
|
||||||
|
var hostingView: NSHostingView<AnyView>?
|
||||||
|
|
||||||
|
private var observedBackdropLayers: [CALayer] = []
|
||||||
|
private var hasScheduledBackdropSetup = false
|
||||||
|
private let windowServerAwareKeyPath = "windowServerAware"
|
||||||
|
private let scaleKeyPath = "scale"
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
removeBackdropObservers()
|
||||||
|
}
|
||||||
|
|
||||||
|
override func removeFromSuperview() {
|
||||||
|
removeBackdropObservers()
|
||||||
|
super.removeFromSuperview()
|
||||||
|
}
|
||||||
|
|
||||||
|
override func viewDidMoveToWindow() {
|
||||||
|
super.viewDidMoveToWindow()
|
||||||
|
scheduleBackdropSetup()
|
||||||
|
}
|
||||||
|
|
||||||
|
override func layout() {
|
||||||
|
super.layout()
|
||||||
|
scheduleBackdropSetup()
|
||||||
|
}
|
||||||
|
|
||||||
|
func scheduleBackdropSetup() {
|
||||||
|
guard !hasScheduledBackdropSetup else { return }
|
||||||
|
hasScheduledBackdropSetup = true
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) { [weak self] in
|
||||||
|
guard let self else { return }
|
||||||
|
self.hasScheduledBackdropSetup = false
|
||||||
|
self.configureBackdropLayers()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func configureBackdropLayers() {
|
||||||
|
guard let glassView else { return }
|
||||||
|
guard let rootLayer = glassView.layer else {
|
||||||
|
scheduleBackdropSetup()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setBackdropProperties(in: rootLayer)
|
||||||
|
let newBackdropLayers = collectBackdropLayers(in: rootLayer)
|
||||||
|
|
||||||
|
removeBackdropObservers()
|
||||||
|
observedBackdropLayers = newBackdropLayers
|
||||||
|
for backdrop in observedBackdropLayers {
|
||||||
|
backdrop.addObserver(self, forKeyPath: windowServerAwareKeyPath, options: [.old, .new], context: nil)
|
||||||
|
backdrop.addObserver(self, forKeyPath: scaleKeyPath, options: [.old, .new], context: nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func setBackdropProperties(in layer: CALayer) {
|
||||||
|
if NSStringFromClass(type(of: layer)).contains("CABackdropLayer") {
|
||||||
|
layer.setValue(true, forKey: windowServerAwareKeyPath)
|
||||||
|
layer.setValue(1.0, forKey: scaleKeyPath)
|
||||||
|
}
|
||||||
|
layer.sublayers?.forEach { setBackdropProperties(in: $0) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private func collectBackdropLayers(in layer: CALayer) -> [CALayer] {
|
||||||
|
var results: [CALayer] = []
|
||||||
|
if NSStringFromClass(type(of: layer)).contains("CABackdropLayer") {
|
||||||
|
results.append(layer)
|
||||||
|
}
|
||||||
|
layer.sublayers?.forEach { results.append(contentsOf: collectBackdropLayers(in: $0)) }
|
||||||
|
return results
|
||||||
|
}
|
||||||
|
|
||||||
|
override func observeValue(
|
||||||
|
forKeyPath keyPath: String?,
|
||||||
|
of object: Any?,
|
||||||
|
change: [NSKeyValueChangeKey: Any]?,
|
||||||
|
context: UnsafeMutableRawPointer?
|
||||||
|
) {
|
||||||
|
if keyPath == windowServerAwareKeyPath {
|
||||||
|
if change?[.newKey] as? Bool == false {
|
||||||
|
configureBackdropLayers()
|
||||||
|
}
|
||||||
|
} else if keyPath == scaleKeyPath {
|
||||||
|
guard let layer = object as? CALayer else { return }
|
||||||
|
if let newScale = (change?[.newKey] as? NSNumber)?.doubleValue, newScale != 1.0 {
|
||||||
|
layer.setValue(1.0, forKey: scaleKeyPath)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func removeBackdropObservers() {
|
||||||
|
for layer in observedBackdropLayers {
|
||||||
|
layer.removeObserver(self, forKeyPath: windowServerAwareKeyPath)
|
||||||
|
layer.removeObserver(self, forKeyPath: scaleKeyPath)
|
||||||
|
}
|
||||||
|
observedBackdropLayers.removeAll()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// All 20 available liquid‑glass variants.
|
/// All 20 available liquid‑glass variants.
|
||||||
/// Apple does not publicly describe how each value looks so experiment and pick the one you like!
|
/// Apple does not publicly describe how each value looks so experiment and pick the one you like!
|
||||||
public enum LiquidGlassVariant: Int, CaseIterable, Identifiable, Defaults.Serializable, Sendable {
|
public enum LiquidGlassVariant: Int, CaseIterable, Identifiable, Defaults.Serializable, Sendable {
|
||||||
|
|
@ -57,8 +159,6 @@ public struct LiquidGlassBackground<Content: View>: NSViewRepresentable {
|
||||||
private let content: Content
|
private let content: Content
|
||||||
private let cornerRadius: CGFloat
|
private let cornerRadius: CGFloat
|
||||||
private let variant: LiquidGlassVariant
|
private let variant: LiquidGlassVariant
|
||||||
private let trigger: Double
|
|
||||||
private let jitterEnabled: Bool
|
|
||||||
/// Creates a new liquid‑glass container.
|
/// Creates a new liquid‑glass container.
|
||||||
/// - Parameters:
|
/// - Parameters:
|
||||||
/// - variant: Any ``LiquidGlassVariant`` (0–19). Defaults to `.v11`, which is visually super pleasing
|
/// - variant: Any ``LiquidGlassVariant`` (0–19). Defaults to `.v11`, which is visually super pleasing
|
||||||
|
|
@ -67,14 +167,10 @@ public struct LiquidGlassBackground<Content: View>: NSViewRepresentable {
|
||||||
public init(
|
public init(
|
||||||
variant: LiquidGlassVariant = .defaultVariant,
|
variant: LiquidGlassVariant = .defaultVariant,
|
||||||
cornerRadius: CGFloat = 10,
|
cornerRadius: CGFloat = 10,
|
||||||
trigger: Double = 0,
|
|
||||||
jitterEnabled: Bool = true,
|
|
||||||
@ViewBuilder content: () -> Content
|
@ViewBuilder content: () -> Content
|
||||||
) {
|
) {
|
||||||
self.variant = variant
|
self.variant = variant
|
||||||
self.cornerRadius = cornerRadius
|
self.cornerRadius = cornerRadius
|
||||||
self.trigger = trigger
|
|
||||||
self.jitterEnabled = jitterEnabled
|
|
||||||
self.content = content()
|
self.content = content()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -116,14 +212,30 @@ public struct LiquidGlassBackground<Content: View>: NSViewRepresentable {
|
||||||
public func makeNSView(context: Context) -> NSView {
|
public func makeNSView(context: Context) -> NSView {
|
||||||
// `NSGlassEffectView` is private. Look it up dynamically to avoid compile‑time coupling.
|
// `NSGlassEffectView` is private. Look it up dynamically to avoid compile‑time coupling.
|
||||||
if let glassType = NSClassFromString("NSGlassEffectView") as? NSView.Type {
|
if let glassType = NSClassFromString("NSGlassEffectView") as? NSView.Type {
|
||||||
|
let container = LiquidGlassContainerView(frame: .zero)
|
||||||
|
container.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
|
||||||
let glass = glassType.init(frame: .zero)
|
let glass = glassType.init(frame: .zero)
|
||||||
|
glass.translatesAutoresizingMaskIntoConstraints = false
|
||||||
glass.setValue(cornerRadius, forKey: "cornerRadius")
|
glass.setValue(cornerRadius, forKey: "cornerRadius")
|
||||||
callPrivateVariantSetter(on: glass, value: variant.rawValue)
|
callPrivateVariantSetter(on: glass, value: variant.rawValue)
|
||||||
|
|
||||||
let hosting = NSHostingView(rootView: content)
|
let hosting = NSHostingView(rootView: AnyView(content))
|
||||||
hosting.translatesAutoresizingMaskIntoConstraints = false
|
hosting.translatesAutoresizingMaskIntoConstraints = false
|
||||||
glass.setValue(hosting, forKey: "contentView")
|
glass.setValue(hosting, forKey: "contentView")
|
||||||
return glass
|
|
||||||
|
container.addSubview(glass)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
glass.leadingAnchor.constraint(equalTo: container.leadingAnchor),
|
||||||
|
glass.trailingAnchor.constraint(equalTo: container.trailingAnchor),
|
||||||
|
glass.topAnchor.constraint(equalTo: container.topAnchor),
|
||||||
|
glass.bottomAnchor.constraint(equalTo: container.bottomAnchor)
|
||||||
|
])
|
||||||
|
|
||||||
|
container.glassView = glass
|
||||||
|
container.hostingView = hosting
|
||||||
|
container.scheduleBackdropSetup()
|
||||||
|
return container
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback for earlier macOS – use an ordinary blur.
|
// Fallback for earlier macOS – use an ordinary blur.
|
||||||
|
|
@ -143,20 +255,17 @@ public struct LiquidGlassBackground<Content: View>: NSViewRepresentable {
|
||||||
}
|
}
|
||||||
|
|
||||||
public func updateNSView(_ nsView: NSView, context: Context) {
|
public func updateNSView(_ nsView: NSView, context: Context) {
|
||||||
if let hosting = nsView.value(forKey: "contentView") as? NSHostingView<Content> {
|
if let container = nsView as? LiquidGlassContainerView,
|
||||||
|
let glass = container.glassView {
|
||||||
|
container.hostingView?.rootView = AnyView(content)
|
||||||
|
glass.setValue(cornerRadius, forKey: "cornerRadius")
|
||||||
|
callPrivateVariantSetter(on: glass, value: variant.rawValue)
|
||||||
|
container.scheduleBackdropSetup()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if let hosting = nsView.subviews.first as? NSHostingView<Content> {
|
||||||
hosting.rootView = content
|
hosting.rootView = content
|
||||||
}
|
}
|
||||||
nsView.setValue(cornerRadius, forKey: "cornerRadius")
|
|
||||||
callPrivateVariantSetter(on: nsView, value: variant.rawValue)
|
|
||||||
|
|
||||||
if jitterEnabled {
|
|
||||||
// Micro-jitter: imperceptibly adjust opacity so WindowServer re-samples the wallpaper every frame.
|
|
||||||
let jitter = sin(trigger * 100) * 0.000001
|
|
||||||
nsView.alphaValue = 1.0 - CGFloat(abs(jitter))
|
|
||||||
} else if nsView.alphaValue != 1.0 {
|
|
||||||
nsView.alphaValue = 1.0
|
|
||||||
}
|
|
||||||
|
|
||||||
nsView.needsDisplay = true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -32,12 +32,31 @@ final class DoNotDisturbManager: ObservableObject {
|
||||||
|
|
||||||
private let notificationCenter = DistributedNotificationCenter.default()
|
private let notificationCenter = DistributedNotificationCenter.default()
|
||||||
private let metadataExtractionQueue = DispatchQueue(label: "com.dynamicisland.focus.metadata", qos: .userInitiated)
|
private let metadataExtractionQueue = DispatchQueue(label: "com.dynamicisland.focus.metadata", qos: .userInitiated)
|
||||||
|
private let pollingQueue = DispatchQueue(label: "com.dynamicisland.focus.polling", qos: .utility)
|
||||||
private let focusLogStream = FocusLogStream()
|
private let focusLogStream = FocusLogStream()
|
||||||
|
private let assertionsURL = FileManager.default.homeDirectoryForCurrentUser
|
||||||
|
.appendingPathComponent("Library/DoNotDisturb/DB/Assertions.json")
|
||||||
|
private var pollingSource: DispatchSourceTimer?
|
||||||
|
private var lastAssertionsModificationDate: Date?
|
||||||
|
private var modeCancellable: AnyCancellable?
|
||||||
|
|
||||||
|
@Published private(set) var monitoringMode: FocusMonitoringMode = Defaults[.focusMonitoringMode]
|
||||||
|
|
||||||
private init() {
|
private init() {
|
||||||
focusLogStream.onMetadataUpdate = { [weak self] identifier, name in
|
focusLogStream.onMetadataUpdate = { [weak self] identifier, name in
|
||||||
self?.handleLogMetadataUpdate(identifier: identifier, name: name)
|
self?.handleLogMetadataUpdate(identifier: identifier, name: name)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
modeCancellable = Defaults.publisher(.focusMonitoringMode, options: [])
|
||||||
|
.sink { [weak self] change in
|
||||||
|
guard let self else { return }
|
||||||
|
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.monitoringMode = change.newValue
|
||||||
|
}
|
||||||
|
|
||||||
|
self.applyMonitoringModeChange(change.newValue)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
deinit {
|
deinit {
|
||||||
|
|
@ -63,7 +82,8 @@ final class DoNotDisturbManager: ObservableObject {
|
||||||
suspensionBehavior: .deliverImmediately
|
suspensionBehavior: .deliverImmediately
|
||||||
)
|
)
|
||||||
|
|
||||||
focusLogStream.start()
|
startAssertionsPolling()
|
||||||
|
applyMonitoringModeChange(Defaults[.focusMonitoringMode])
|
||||||
isMonitoring = true
|
isMonitoring = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -74,6 +94,7 @@ final class DoNotDisturbManager: ObservableObject {
|
||||||
notificationCenter.removeObserver(self, name: .focusModeDisabled, object: nil)
|
notificationCenter.removeObserver(self, name: .focusModeDisabled, object: nil)
|
||||||
|
|
||||||
focusLogStream.stop()
|
focusLogStream.stop()
|
||||||
|
stopAssertionsPolling()
|
||||||
isMonitoring = false
|
isMonitoring = false
|
||||||
|
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
|
|
@ -159,6 +180,8 @@ final class DoNotDisturbManager: ObservableObject {
|
||||||
}
|
}
|
||||||
|
|
||||||
private func handleLogMetadataUpdate(identifier: String?, name: String?) {
|
private func handleLogMetadataUpdate(identifier: String?, name: String?) {
|
||||||
|
guard Defaults[.focusMonitoringMode] == .useDevTools else { return }
|
||||||
|
|
||||||
metadataExtractionQueue.async { [weak self] in
|
metadataExtractionQueue.async { [weak self] in
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
let trimmedIdentifier = identifier?.trimmingCharacters(in: .whitespacesAndNewlines)
|
let trimmedIdentifier = identifier?.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
|
@ -308,6 +331,90 @@ private extension Notification.Name {
|
||||||
static let focusModeDisabled = Notification.Name("_NSDoNotDisturbDisabledNotification")
|
static let focusModeDisabled = Notification.Name("_NSDoNotDisturbDisabledNotification")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private extension DoNotDisturbManager {
|
||||||
|
func applyMonitoringModeChange(_ mode: FocusMonitoringMode) {
|
||||||
|
guard isMonitoring else { return }
|
||||||
|
|
||||||
|
if mode == .useDevTools {
|
||||||
|
focusLogStream.start()
|
||||||
|
} else {
|
||||||
|
focusLogStream.stop()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func startAssertionsPolling() {
|
||||||
|
stopAssertionsPolling()
|
||||||
|
lastAssertionsModificationDate = nil
|
||||||
|
|
||||||
|
let timer = DispatchSource.makeTimerSource(queue: pollingQueue)
|
||||||
|
timer.schedule(deadline: .now() + .seconds(1), repeating: .seconds(2), leeway: .milliseconds(250))
|
||||||
|
timer.setEventHandler { [weak self] in
|
||||||
|
self?.pollAssertionsState()
|
||||||
|
}
|
||||||
|
timer.resume()
|
||||||
|
pollingSource = timer
|
||||||
|
}
|
||||||
|
|
||||||
|
func stopAssertionsPolling() {
|
||||||
|
pollingSource?.cancel()
|
||||||
|
pollingSource = nil
|
||||||
|
lastAssertionsModificationDate = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func pollAssertionsState() {
|
||||||
|
guard FullDiskAccessAuthorization.hasPermission() else { return }
|
||||||
|
|
||||||
|
if let attributes = try? FileManager.default.attributesOfItem(atPath: assertionsURL.path),
|
||||||
|
let modifiedAt = attributes[.modificationDate] as? Date,
|
||||||
|
let lastObservedModificationDate = lastAssertionsModificationDate,
|
||||||
|
modifiedAt <= lastObservedModificationDate {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let data = try? Data(contentsOf: assertionsURL),
|
||||||
|
!data.isEmpty,
|
||||||
|
let root = (try? JSONSerialization.jsonObject(with: data, options: [])) as? [String: Any],
|
||||||
|
let dataArray = root["data"] as? [[String: Any]],
|
||||||
|
let firstItem = dataArray.first else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let assertions = (firstItem["storeAssertionRecords"] as? [Any]) ?? []
|
||||||
|
let isActive = !assertions.isEmpty
|
||||||
|
|
||||||
|
if let attributes = try? FileManager.default.attributesOfItem(atPath: assertionsURL.path),
|
||||||
|
let modifiedAt = attributes[.modificationDate] as? Date {
|
||||||
|
lastAssertionsModificationDate = modifiedAt
|
||||||
|
}
|
||||||
|
|
||||||
|
let identifierKeys = [
|
||||||
|
"modeIdentifier",
|
||||||
|
"FocusModeIdentifier",
|
||||||
|
"focusModeIdentifier",
|
||||||
|
"identifier",
|
||||||
|
"Identifier",
|
||||||
|
"focusModeUUID",
|
||||||
|
"UUID",
|
||||||
|
"uuid"
|
||||||
|
]
|
||||||
|
|
||||||
|
let nameKeys = [
|
||||||
|
"activityDisplayName",
|
||||||
|
"displayName",
|
||||||
|
"FocusModeName",
|
||||||
|
"focusModeName",
|
||||||
|
"focusMode",
|
||||||
|
"name",
|
||||||
|
"Name"
|
||||||
|
]
|
||||||
|
|
||||||
|
let identifier = isActive ? firstMatch(for: identifierKeys, in: assertions) : nil
|
||||||
|
let name = isActive ? firstMatch(for: nameKeys, in: assertions) : nil
|
||||||
|
|
||||||
|
publishMetadata(identifier: identifier, name: name, isActive: isActive, source: "assertions-poll")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Focus Mode Types
|
// MARK: - Focus Mode Types
|
||||||
|
|
||||||
enum FocusModeType: String, CaseIterable {
|
enum FocusModeType: String, CaseIterable {
|
||||||
|
|
|
||||||
|
|
@ -498,6 +498,22 @@ enum TimerProgressStyle: String, CaseIterable, Identifiable, Defaults.Serializab
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum FocusMonitoringMode: String, CaseIterable, Identifiable, Defaults.Serializable {
|
||||||
|
case withoutDevTools = "withoutDevTools"
|
||||||
|
case useDevTools = "useDevTools"
|
||||||
|
|
||||||
|
var id: String { rawValue }
|
||||||
|
|
||||||
|
var displayName: String {
|
||||||
|
switch self {
|
||||||
|
case .withoutDevTools:
|
||||||
|
return "Use without DevTools"
|
||||||
|
case .useDevTools:
|
||||||
|
return "Use DevTools"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
enum ReminderPresentationStyle: String, CaseIterable, Identifiable, Defaults.Serializable {
|
enum ReminderPresentationStyle: String, CaseIterable, Identifiable, Defaults.Serializable {
|
||||||
case ringCountdown = "Ring"
|
case ringCountdown = "Ring"
|
||||||
case digital = "Digital"
|
case digital = "Digital"
|
||||||
|
|
@ -973,6 +989,15 @@ extension Defaults.Keys {
|
||||||
static let verticalHUDWidth = Key<CGFloat>("verticalHUDWidth", default: 36)
|
static let verticalHUDWidth = Key<CGFloat>("verticalHUDWidth", default: 36)
|
||||||
static let verticalHUDPadding = Key<CGFloat>("verticalHUDPadding", default: 24)
|
static let verticalHUDPadding = Key<CGFloat>("verticalHUDPadding", default: 24)
|
||||||
static let verticalHUDUseAccentColor = Key<Bool>("verticalHUDUseAccentColor", default: false)
|
static let verticalHUDUseAccentColor = Key<Bool>("verticalHUDUseAccentColor", default: false)
|
||||||
|
static let verticalHUDMaterial = Key<OSDMaterial>("verticalHUDMaterial", default: .frosted)
|
||||||
|
static let verticalHUDLiquidGlassCustomizationMode = Key<LockScreenGlassCustomizationMode>(
|
||||||
|
"verticalHUDLiquidGlassCustomizationMode",
|
||||||
|
default: .standard
|
||||||
|
)
|
||||||
|
static let verticalHUDLiquidGlassVariant = Key<LiquidGlassVariant>(
|
||||||
|
"verticalHUDLiquidGlassVariant",
|
||||||
|
default: .defaultVariant
|
||||||
|
)
|
||||||
|
|
||||||
// Circular HUD Customization
|
// Circular HUD Customization
|
||||||
static let circularHUDShowValue = Key<Bool>("circularHUDShowValue", default: true)
|
static let circularHUDShowValue = Key<Bool>("circularHUDShowValue", default: true)
|
||||||
|
|
@ -985,6 +1010,14 @@ extension Defaults.Keys {
|
||||||
static let enableOSDBrightness = Key<Bool>("enableOSDBrightness", default: true)
|
static let enableOSDBrightness = Key<Bool>("enableOSDBrightness", default: true)
|
||||||
static let enableOSDKeyboardBacklight = Key<Bool>("enableOSDKeyboardBacklight", default: true)
|
static let enableOSDKeyboardBacklight = Key<Bool>("enableOSDKeyboardBacklight", default: true)
|
||||||
static let osdMaterial = Key<OSDMaterial>("osdMaterial", default: .frosted)
|
static let osdMaterial = Key<OSDMaterial>("osdMaterial", default: .frosted)
|
||||||
|
static let osdLiquidGlassCustomizationMode = Key<LockScreenGlassCustomizationMode>(
|
||||||
|
"osdLiquidGlassCustomizationMode",
|
||||||
|
default: .standard
|
||||||
|
)
|
||||||
|
static let osdLiquidGlassVariant = Key<LiquidGlassVariant>(
|
||||||
|
"osdLiquidGlassVariant",
|
||||||
|
default: .defaultVariant
|
||||||
|
)
|
||||||
static let osdIconColorStyle = Key<OSDIconColorStyle>("osdIconColorStyle", default: .white)
|
static let osdIconColorStyle = Key<OSDIconColorStyle>("osdIconColorStyle", default: .white)
|
||||||
|
|
||||||
// MARK: Screen Recording Detection Feature
|
// MARK: Screen Recording Detection Feature
|
||||||
|
|
@ -995,6 +1028,7 @@ extension Defaults.Keys {
|
||||||
|
|
||||||
// MARK: Focus / Do Not Disturb Detection
|
// MARK: Focus / Do Not Disturb Detection
|
||||||
static let enableDoNotDisturbDetection = Key<Bool>("enableDoNotDisturbDetection", default: true)
|
static let enableDoNotDisturbDetection = Key<Bool>("enableDoNotDisturbDetection", default: true)
|
||||||
|
static let focusMonitoringMode = Key<FocusMonitoringMode>("focusMonitoringMode", default: .withoutDevTools)
|
||||||
static let showDoNotDisturbIndicator = Key<Bool>("showDoNotDisturbIndicator", default: true)
|
static let showDoNotDisturbIndicator = Key<Bool>("showDoNotDisturbIndicator", default: true)
|
||||||
static let showDoNotDisturbLabel = Key<Bool>("showDoNotDisturbLabel", default: true)
|
static let showDoNotDisturbLabel = Key<Bool>("showDoNotDisturbLabel", default: true)
|
||||||
static let focusIndicatorNonPersistent = Key<Bool>("focusIndicatorNonPersistent", default: false)
|
static let focusIndicatorNonPersistent = Key<Bool>("focusIndicatorNonPersistent", default: false)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue