Merge pull request #312 from Ebullioscopic/bugfix/stats

Bugfix/stats
This commit is contained in:
Hariharan Mudaliar 2026-03-01 02:03:06 +05:30 • committed by GitHub
commit 1a2e3c3920
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
20 changed files with 1207 additions and 143 deletions

View file

@ -25,6 +25,7 @@
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 */; };
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 */; };
7EE1D73F2E6B1E1200997219 /* IOKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7EE1D73E2E6B1E1200997219 /* IOKit.framework */; };
7EF1D4B92FFFCE3C008D2C32 /* ExtensionEventBridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7EF1D4B82FFFCE3C008D2C32 /* ExtensionEventBridge.swift */; };
@ -71,6 +72,7 @@
7E6118172E24071E0050006C /* Collections in Frameworks */,
7E6118162E24071E0050006C /* KeyboardShortcuts in Frameworks */,
7E6118152E24071E0050006C /* Sparkle in Frameworks */,
7ECFE0C62F4A2323000DD9BD /* MijickCalendarView in Frameworks */,
7E288D122F1659EF0020C117 /* AtollExtensionKit in Frameworks */,
7E6118142E24071E0050006C /* LaunchAtLogin in Frameworks */,
);
@ -171,6 +173,7 @@
146BC4362D366E9B0047C66A /* MacroVisionKit */,
7E61181E2E24071E0050006C /* SkyLightWindow */,
7E288D112F1659EF0020C117 /* AtollExtensionKit */,
7ECFE0C52F4A2323000DD9BD /* MijickCalendarView */,
);
productName = DynamicIsland;
productReference = F54406E72CD4987A00C7AFD9 /* Atoll.app */;
@ -216,6 +219,7 @@
7E61181F2E24071E0050006C /* XCRemoteSwiftPackageReference "SkyLightWindow" */,
7E09D5962EB7B497005AF41D /* XCRemoteSwiftPackageReference "open-meteo" */,
7E288D102F1659EF0020C117 /* XCRemoteSwiftPackageReference "AtollExtensionKit" */,
7ECFE0C42F4A2323000DD9BD /* XCRemoteSwiftPackageReference "CalendarView" */,
);
preferredProjectObjectVersion = 77;
productRefGroup = F54406E82CD4987A00C7AFD9 /* Products */;
@ -589,6 +593,14 @@
kind = branch;
};
};
7ECFE0C42F4A2323000DD9BD /* XCRemoteSwiftPackageReference "CalendarView" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/Mijick/CalendarView";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 1.1.1;
};
};
9A987A112C73CAA1005CA465 /* XCRemoteSwiftPackageReference "Pow" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/EmergeTools/Pow";
@ -665,6 +677,11 @@
package = 7E61181F2E24071E0050006C /* XCRemoteSwiftPackageReference "SkyLightWindow" */;
productName = SkyLightWindow;
};
7ECFE0C52F4A2323000DD9BD /* MijickCalendarView */ = {
isa = XCSwiftPackageProductDependency;
package = 7ECFE0C42F4A2323000DD9BD /* XCRemoteSwiftPackageReference "CalendarView" */;
productName = MijickCalendarView;
};
9A987A0F2C73CA8D005CA465 /* Collections */ = {
isa = XCSwiftPackageProductDependency;
package = F54407082CD4A23F00C7AFD9 /* XCRemoteSwiftPackageReference "swift-collections" */;

View file

@ -1,5 +1,5 @@
{
"originHash" : "6fbfa8cda0c383746edd66361426fdc2bd266fc082867294374a1818f02798f3",
"originHash" : "802e15fea0dccd2d325dc082d8d36162007adcd2fa301070eb26448d0a2cf6c5",
"pins" : [
{
"identity" : "atollextensionkit",
@ -10,6 +10,15 @@
"revision" : "ebe35277572a349fad26abeb6fc552cb5251d584"
}
},
{
"identity" : "calendarview",
"kind" : "remoteSourceControl",
"location" : "https://github.com/Mijick/CalendarView",
"state" : {
"revision" : "168cf14a50d33684caa2a411bbd0b3f988e9dff6",
"version" : "1.1.1"
}
},
{
"identity" : "defaults",
"kind" : "remoteSourceControl",

View file

@ -180,10 +180,26 @@ struct ContentView: View {
}
private let zeroHeightHoverPadding: CGFloat = 10
private let statsAdditionalRowHeight: CGFloat = 110
private let statsAdditionalRowHeight: CGFloat = statsSecondRowContentHeight + statsGridSpacingHeight
private let musicControlPauseGrace: TimeInterval = 5
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 {
showStandardMediaControls && !enableMinimalisticUI
}
@ -398,6 +414,7 @@ struct ContentView: View {
maxHeight: dynamicNotchSize.height + currentShadowPadding,
alignment: .top
)
.frame(maxHeight: .infinity, alignment: .top)
.environmentObject(privacyManager)
.onChange(of: dynamicNotchSize) { oldSize, newSize in
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)
.allowsHitTesting(vm.notchState == .open)
.blur(radius: abs(gestureProgress) > 0.3 ? min(abs(gestureProgress), 8) : 0)
.opacity(abs(gestureProgress) > 0.3 ? min(abs(gestureProgress * 2), 0.8) : 1)
.animation(.smooth(duration: 0.3), value: coordinator.currentView)
}
}

View file

@ -70,6 +70,7 @@ struct DynamicNotchApp: App {
}
}
@CommandsBuilder
var commands: some Commands {
CommandGroup(replacing: .appSettings) {
Button("Settings…") {
@ -116,6 +117,8 @@ class AppDelegate: NSObject, NSApplicationDelegate {
private var cancellables = Set<AnyCancellable>()
private var windowsHiddenForLock = false
private var optionalShortcutHandlersRegistered = false
private weak var focusWithoutDevToolsMenuItem: NSMenuItem?
private weak var focusUseDevToolsMenuItem: NSMenuItem?
// Debouncing mechanism for window size updates
private var windowSizeUpdateWorkItem: DispatchWorkItem?
@ -147,6 +150,10 @@ class AppDelegate: NSObject, NSApplicationDelegate {
return false
}
func applicationDidBecomeActive(_ notification: Notification) {
installTopMenuItemsIfNeeded()
}
func applicationWillTerminate(_ notification: Notification) {
// Cancel any pending window size updates
windowSizeUpdateWorkItem?.cancel()
@ -370,6 +377,13 @@ class AppDelegate: NSObject, NSApplicationDelegate {
idleAnimationManager.initializeDefaultAnimations()
applySelectedAppIcon()
installTopMenuItemsIfNeeded()
Defaults.publisher(.focusMonitoringMode, options: [])
.sink { [weak self] _ in
self?.updateFocusMenuState()
}
.store(in: &cancellables)
// Setup SystemHUD Manager
SystemHUDManager.shared.setup(coordinator: coordinator)
@ -531,9 +545,11 @@ class AppDelegate: NSObject, NSApplicationDelegate {
forName: Notification.Name.automaticallySwitchDisplayChanged, object: nil, queue: nil
) { [weak self] _ in
guard let self = self, let window = self.window else { return }
DispatchQueue.main.async {
window.alphaValue =
self.coordinator.selectedScreen == self.coordinator.preferredScreen ? 1 : 0
}
}
NotificationCenter.default.addObserver(
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() {
guard !optionalShortcutHandlersRegistered else { return }
optionalShortcutHandlersRegistered = true

View file

@ -102,12 +102,21 @@ class DynamicIslandViewCoordinator: ObservableObject {
static let shared = DynamicIslandViewCoordinator()
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 {
didSet {
if Defaults[.enableMinimalisticUI] && currentView != .home {
currentView = .home
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)
}
}

View file

@ -1591,10 +1591,6 @@
}
}
},
"±10 Seconds" : {
"comment" : "Name of the music skip behavior that skips the current track within ±10 seconds.",
"isCommentAutoGenerated" : true
},
"0" : {
"comment" : "A label displayed in a circular gauge to indicate the minimum battery level.",
"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" : {
"localizations" : {
"ar" : {
@ -2260,10 +2252,6 @@
}
}
},
"Adaptive gradient" : {
"comment" : "Name of the timer icon color mode option that uses an adaptive gradient.",
"isCommentAutoGenerated" : true
},
"Add" : {
"localizations" : {
"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." : {
"localizations" : {
"pt-BR" : {
@ -4268,10 +4252,6 @@
}
}
},
"Bar" : {
"comment" : "Localized name for \"Bar\" timer progress style.",
"isCommentAutoGenerated" : true
},
"Battery" : {
"localizations" : {
"ar" : {
@ -7617,6 +7597,9 @@
}
}
}
},
"Custom Liquid is available on macOS 26 or later." : {
},
"Custom Liquid settings require the Liquid Glass material." : {
"localizations" : {
@ -7627,6 +7610,9 @@
}
}
}
},
"Custom liquid variant" : {
},
"Custom music live activity animation" : {
"localizations" : {
@ -8505,10 +8491,6 @@
}
}
},
"Digital" : {
"comment" : "Name for the \"Digital\" style of reminder presentation.",
"isCommentAutoGenerated" : true
},
"Dimensions" : {
"localizations" : {
"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" : {
"localizations" : {
"pt-BR" : {
@ -11875,10 +11861,6 @@
}
}
},
"Green" : {
"comment" : "Display name for the \"Green\" caps lock indicator tint mode.",
"isCommentAutoGenerated" : true
},
"Height:" : {
"extractionState" : "stale",
"localizations" : {
@ -15545,6 +15527,7 @@
}
},
"Minutes" : {
"extractionState" : "stale",
"localizations" : {
"fr" : {
"stringUnit" : {
@ -17754,10 +17737,6 @@
}
}
},
"Now Playing" : {
"comment" : "Localized name for each media controller type.",
"isCommentAutoGenerated" : true
},
"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
@ -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." : {
"comment" : "A description of the \"Panel\" clipboard display mode.",
"isCommentAutoGenerated" : true,
@ -19722,10 +19697,6 @@
}
}
},
"Ring" : {
"comment" : "Name of the timer progress style that shows a ring animation.",
"isCommentAutoGenerated" : true
},
"Rotation:" : {
"extractionState" : "stale",
"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." : {
"comment" : "A description of the \"Separate Tab\" clipboard display mode.",
"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" : {
"localizations" : {
"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" : {
"localizations" : {
"ar" : {
@ -22245,10 +22200,6 @@
}
}
},
"Spotify" : {
"comment" : "Name of a media controller type for Spotify.",
"isCommentAutoGenerated" : true
},
"Square" : {
"localizations" : {
"ar" : {
@ -22357,10 +22308,6 @@
},
"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" : {
"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." : {
"localizations" : {
@ -23707,10 +23657,6 @@
},
"shouldTranslate" : false
},
"Track Skip" : {
"comment" : "Display name for the \"Track Skip\" music skip behavior option.",
"isCommentAutoGenerated" : true
},
"Transform" : {
"extractionState" : "stale",
"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" : {
"localizations" : {
"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" : {
"comment" : "A label describing the percentage of memory that is currently in use.",
"isCommentAutoGenerated" : true,
@ -25613,10 +25567,6 @@
"comment" : "A footer note explaining the app's funding.",
"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: " : {
"localizations" : {
"ar" : {

View file

@ -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 {
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 {
@Environment(\.openURL) private var openURL
@ObservedObject private var calendarManager = CalendarManager.shared

View file

@ -132,11 +132,9 @@ struct ExtensionLockScreenWidgetView: View {
@ViewBuilder
private func liquidGlassBackground(shape: RoundedRectangle) -> some View {
TimelineView(.periodic(from: .now, by: 1.0 / 30.0)) { context in
LiquidGlassBackground(variant: requestedLiquidGlassVariant, cornerRadius: descriptor.cornerRadius, trigger: context.date.timeIntervalSinceReferenceDate) {
LiquidGlassBackground(variant: requestedLiquidGlassVariant, cornerRadius: descriptor.cornerRadius) {
Color.black.opacity(0.08)
}
}
.overlay {
tintOverlay(shape: shape)
}

View file

@ -356,12 +356,17 @@ struct LockScreenMusicPanel: View {
collapseWorkItem = nil
}
private var isProgressTimelinePaused: Bool {
!musicManager.isPlaying || musicManager.isLiveStream || musicManager.playbackRate <= 0
}
// MARK: - Progress Bar
private var progressBar: some View {
TimelineView(
.animation(
minimumInterval: (!musicManager.isLiveStream && musicManager.playbackRate > 0) ? 0.1 : nil
minimumInterval: 0.1,
paused: isProgressTimelinePaused
)
) { timeline in
MusicSliderView(
@ -792,11 +797,9 @@ struct LockScreenMusicPanel: View {
@ViewBuilder
private var customLiquidPanelBackdrop: some View {
TimelineView(.periodic(from: .now, by: 1.0 / 30.0)) { context in
LiquidGlassBackground(variant: musicGlassVariant, cornerRadius: panelCornerRadius, trigger: context.date.timeIntervalSinceReferenceDate) {
LiquidGlassBackground(variant: musicGlassVariant, cornerRadius: panelCornerRadius) {
Color.white.opacity(0.04)
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.allowsHitTesting(false)
.accessibilityHidden(true)
@ -873,12 +876,10 @@ struct LockScreenMusicPanel: View {
@ViewBuilder
private func customLiquidAlbumArtBackground(cornerRadius: CGFloat) -> some View {
TimelineView(.periodic(from: .now, by: 1.0 / 30.0)) { context in
LiquidGlassBackground(variant: musicGlassVariant, cornerRadius: cornerRadius, trigger: context.date.timeIntervalSinceReferenceDate) {
LiquidGlassBackground(variant: musicGlassVariant, cornerRadius: cornerRadius) {
Color.clear
}
}
}
private func logGlassState(reason: String) {
let snapshot = GlassLogSnapshot(

View file

@ -154,12 +154,10 @@ struct LockScreenTimerWidget: View {
}
private var customLiquidBackground: some View {
TimelineView(.periodic(from: .now, by: 1.0 / 30.0)) { context in
LiquidGlassBackground(variant: timerGlassVariant, cornerRadius: Self.cornerRadius, trigger: context.date.timeIntervalSinceReferenceDate) {
LiquidGlassBackground(variant: timerGlassVariant, cornerRadius: Self.cornerRadius) {
Color.black.opacity(0.12)
}
}
}
private var frostedBackground: some View {
RoundedRectangle(cornerRadius: Self.cornerRadius, style: .continuous)

View file

@ -552,7 +552,14 @@ struct NotchHomeView: View {
}
if Defaults[.showCalendar] {
Group {
if showStandardMediaControls {
CalendarView()
} else {
StandaloneCalendarView()
}
}
.frame(maxWidth: .infinity, alignment: .leading)
.onHover { isHovering in
vm.isHoveringCalendar = isHovering
}

View file

@ -28,6 +28,8 @@ struct CustomOSDView: View {
@Binding var icon: String
@Default(.osdMaterial) var osdMaterial
@Default(.osdLiquidGlassCustomizationMode) var osdLiquidGlassCustomizationMode
@Default(.osdLiquidGlassVariant) var osdLiquidGlassVariant
@Default(.osdIconColorStyle) var osdIconColorStyle
@Environment(\.colorScheme) var colorScheme
@ -160,11 +162,20 @@ struct CustomOSDView: View {
.fill(.ultraThinMaterial)
case .liquid:
if #available(macOS 26.0, *) {
if osdLiquidGlassCustomizationMode == .customLiquid {
LiquidGlassBackground(
variant: osdLiquidGlassVariant,
cornerRadius: 18
) {
Color.white.opacity(0.04)
}
} else {
RoundedRectangle(cornerRadius: 18, style: .continuous)
.glassEffect(
.clear.interactive(),
in: .rect(cornerRadius: 18)
)
}
} else {
RoundedRectangle(cornerRadius: 18, style: .continuous)
.fill(.ultraThinMaterial)

View file

@ -46,6 +46,9 @@ struct VerticalHUDView: View {
@Default(.verticalHUDHeight) var hudHeight
@Default(.verticalHUDWidth) var hudWidth
@Default(.verticalHUDUseAccentColor) var useAccentColor
@Default(.verticalHUDMaterial) var verticalHUDMaterial
@Default(.verticalHUDLiquidGlassCustomizationMode) var verticalHUDLiquidGlassCustomizationMode
@Default(.verticalHUDLiquidGlassVariant) var verticalHUDLiquidGlassVariant
@Default(.verticalHUDInteractive) var isInteractive
@Environment(\.colorScheme) var colorScheme
@ -68,8 +71,8 @@ struct VerticalHUDView: View {
// The Actual HUD Pill
ZStack {
// Background
Capsule()
.fill(.ultraThinMaterial)
verticalBackground
.clipShape(Capsule())
.overlay {
Capsule()
.stroke(.white.opacity(0.1), lineWidth: 1)
@ -253,5 +256,39 @@ struct VerticalHUDView: View {
}
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

View file

@ -22,6 +22,7 @@
import SwiftUI
import AVFoundation
import Defaults
enum OnboardingStep {
case welcome
@ -36,6 +37,8 @@ private let calendarService = CalendarService()
struct OnboardingView: View {
@State private var step: OnboardingStep = .welcome
@State private var showFocusMonitoringChoice = false
@State private var didPresentFocusMonitoringChoice = false
let onFinish: () -> Void
let onOpenSettings: () -> Void
@ -120,6 +123,28 @@ struct OnboardingView: View {
}
}
.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

View file

@ -1369,6 +1369,9 @@ private struct HUDAndOSDSettingsView: View {
@Default(.verticalHUDWidth) var verticalHUDWidth
@Default(.verticalHUDPadding) var verticalHUDPadding
@Default(.verticalHUDUseAccentColor) var verticalHUDUseAccentColor
@Default(.verticalHUDMaterial) var verticalHUDMaterial
@Default(.verticalHUDLiquidGlassCustomizationMode) var verticalHUDLiquidGlassCustomizationMode
@Default(.verticalHUDLiquidGlassVariant) var verticalHUDLiquidGlassVariant
// Circular HUD Props
@Default(.circularHUDShowValue) var circularHUDShowValue
@ -1391,6 +1394,27 @@ private struct HUDAndOSDSettingsView: View {
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 {
VStack(spacing: 20) {
HStack(spacing: 16) {
@ -1608,6 +1632,39 @@ private struct HUDAndOSDSettingsView: View {
Toggle("Show Percentage", isOn: $verticalHUDShowValue)
Toggle("Use Accent Color", isOn: $verticalHUDUseAccentColor)
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)
if Defaults[.useColorCodedVolumeDisplay] {
Defaults.Toggle("Smooth color transitions", key: .useSmoothColorGradient)
@ -1719,6 +1776,12 @@ private struct HUDAndOSDSettingsView: View {
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
.background(paneBackgroundColor)
.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 previewCornerRadius: CGFloat = 14
private let previewSize = CGSize(width: 190, height: 96)
@State private var previewToken: Double = 0
var body: some View {
ZStack {
@ -4486,7 +4548,6 @@ private struct LockScreenGlassVariantPreviewCell: View {
Logger.log("Lock screen glass preview disappeared", category: .performance)
}
.onChange(of: variant) { _, newValue in
previewToken = Date().timeIntervalSinceReferenceDate
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 {
LiquidGlassBackground(
variant: variant,
cornerRadius: previewCornerRadius,
trigger: previewToken,
jitterEnabled: false
cornerRadius: previewCornerRadius
) {
Color.white.opacity(0.04)
}
@ -5608,34 +5667,34 @@ struct TimerSettings: View {
let nextIndex = timerPresets.count + 1
let defaultColor = Defaults[.accentColor]
let newPreset = TimerPreset(name: "Preset \(nextIndex)", duration: 5 * 60, color: defaultColor)
withAnimation(.smooth) {
_ = withAnimation(.smooth) {
timerPresets.append(newPreset)
}
}
private func movePresetUp(_ index: Int) {
guard index > timerPresets.startIndex else { return }
withAnimation(.smooth) {
_ = withAnimation(.smooth) {
timerPresets.swapAt(index, index - 1)
}
}
private func movePresetDown(_ index: Int) {
guard index < timerPresets.index(before: timerPresets.endIndex) else { return }
withAnimation(.smooth) {
_ = withAnimation(.smooth) {
timerPresets.swapAt(index, index + 1)
}
}
private func removePreset(_ index: Int) {
guard timerPresets.indices.contains(index) else { return }
withAnimation(.smooth) {
_ = withAnimation(.smooth) {
timerPresets.remove(at: index)
}
}
private func resetPresets() {
withAnimation(.smooth) {
_ = withAnimation(.smooth) {
timerPresets = TimerPreset.defaultPresets
}
}
@ -6550,6 +6609,8 @@ struct CustomOSDSettings: View {
@Default(.enableOSDBrightness) var enableOSDBrightness
@Default(.enableOSDKeyboardBacklight) var enableOSDKeyboardBacklight
@Default(.osdMaterial) var osdMaterial
@Default(.osdLiquidGlassCustomizationMode) var osdLiquidGlassCustomizationMode
@Default(.osdLiquidGlassVariant) var osdLiquidGlassVariant
@Default(.osdIconColorStyle) var osdIconColorStyle
@Default(.enableSystemHUD) var enableSystemHUD
@ObservedObject private var accessibilityPermission = AccessibilityPermissionStore.shared
@ -6566,6 +6627,27 @@ struct CustomOSDSettings: View {
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 {
Form {
if !hasAccessibilityPermission {
@ -6598,7 +6680,7 @@ struct CustomOSDSettings: View {
Section {
Picker("Material", selection: $osdMaterial) {
ForEach(OSDMaterial.allCases, id: \.self) { material in
ForEach(availableOSDMaterials, id: \.self) { material in
Text(material.rawValue).tag(material)
}
}
@ -6607,6 +6689,34 @@ struct CustomOSDSettings: View {
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) {
ForEach(OSDIconColorStyle.allCases, id: \.self) { style in
Text(style.rawValue).tag(style)
@ -6682,6 +6792,10 @@ struct CustomOSDSettings: View {
.navigationTitle("Custom OSD")
.onAppear {
accessibilityPermission.refreshStatus()
if #unavailable(macOS 26.0), osdMaterial == .liquid {
osdMaterial = .frosted
osdLiquidGlassCustomizationMode = .standard
}
}
.onChange(of: accessibilityPermission.isAuthorized) { _, granted in
if !granted {

View file

@ -59,9 +59,10 @@ class SettingsWindowController: NSWindowController {
window.titleVisibility = .visible
window.toolbarStyle = .unified
window.isMovableByWindowBackground = true
window.level = .normal
// 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
window.hidesOnDeactivate = false
@ -86,6 +87,10 @@ class SettingsWindowController: NSWindowController {
// Ensure window exists
_ = 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?.isVisible == true {
NSApp.activate(ignoringOtherApps: true)

View file

@ -99,9 +99,6 @@ struct TabSelectionView: View {
)
}
}
DispatchQueue.main.async {
ensureValidSelection(with: tabsArray)
}
return tabsArray
}
var body: some View {
@ -110,7 +107,7 @@ struct TabSelectionView: View {
let isSelected = isSelected(tab)
let activeAccent = tab.accentColor ?? .white
TabButton(label: tab.label, icon: tab.icon, selected: isSelected) {
withAnimation(.smooth) {
withAnimation(.smooth(duration: 0.3)) {
if tab.view == .extensionExperience {
coordinator.selectedExtensionExperienceID = tab.experienceID
}
@ -135,6 +132,9 @@ struct TabSelectionView: View {
}
}
.clipShape(Capsule())
.onAppear {
ensureValidSelection(with: tabs)
}
}
private var extensionTabsEnabled: Bool {

View file

@ -20,6 +20,108 @@ import SwiftUI
import AppKit
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.
/// 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 {
@ -57,8 +159,6 @@ public struct LiquidGlassBackground<Content: View>: NSViewRepresentable {
private let content: Content
private let cornerRadius: CGFloat
private let variant: LiquidGlassVariant
private let trigger: Double
private let jitterEnabled: Bool
/// Creates a new liquid‑glass container.
/// - Parameters:
/// - 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(
variant: LiquidGlassVariant = .defaultVariant,
cornerRadius: CGFloat = 10,
trigger: Double = 0,
jitterEnabled: Bool = true,
@ViewBuilder content: () -> Content
) {
self.variant = variant
self.cornerRadius = cornerRadius
self.trigger = trigger
self.jitterEnabled = jitterEnabled
self.content = content()
}
@ -116,14 +212,30 @@ public struct LiquidGlassBackground<Content: View>: NSViewRepresentable {
public func makeNSView(context: Context) -> NSView {
// `NSGlassEffectView` is private. Look it up dynamically to avoid compile‑time coupling.
if let glassType = NSClassFromString("NSGlassEffectView") as? NSView.Type {
let container = LiquidGlassContainerView(frame: .zero)
container.translatesAutoresizingMaskIntoConstraints = false
let glass = glassType.init(frame: .zero)
glass.translatesAutoresizingMaskIntoConstraints = false
glass.setValue(cornerRadius, forKey: "cornerRadius")
callPrivateVariantSetter(on: glass, value: variant.rawValue)
let hosting = NSHostingView(rootView: content)
let hosting = NSHostingView(rootView: AnyView(content))
hosting.translatesAutoresizingMaskIntoConstraints = false
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.
@ -143,20 +255,17 @@ public struct LiquidGlassBackground<Content: View>: NSViewRepresentable {
}
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
}
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
}
}

View file

@ -32,12 +32,31 @@ final class DoNotDisturbManager: ObservableObject {
private let notificationCenter = DistributedNotificationCenter.default()
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 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() {
focusLogStream.onMetadataUpdate = { [weak self] identifier, name in
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 {
@ -63,7 +82,8 @@ final class DoNotDisturbManager: ObservableObject {
suspensionBehavior: .deliverImmediately
)
focusLogStream.start()
startAssertionsPolling()
applyMonitoringModeChange(Defaults[.focusMonitoringMode])
isMonitoring = true
}
@ -74,6 +94,7 @@ final class DoNotDisturbManager: ObservableObject {
notificationCenter.removeObserver(self, name: .focusModeDisabled, object: nil)
focusLogStream.stop()
stopAssertionsPolling()
isMonitoring = false
DispatchQueue.main.async {
@ -159,6 +180,8 @@ final class DoNotDisturbManager: ObservableObject {
}
private func handleLogMetadataUpdate(identifier: String?, name: String?) {
guard Defaults[.focusMonitoringMode] == .useDevTools else { return }
metadataExtractionQueue.async { [weak self] in
guard let self = self else { return }
let trimmedIdentifier = identifier?.trimmingCharacters(in: .whitespacesAndNewlines)
@ -308,6 +331,90 @@ private extension Notification.Name {
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
enum FocusModeType: String, CaseIterable {

View file

@ -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 {
case ringCountdown = "Ring"
case digital = "Digital"
@ -973,6 +989,15 @@ extension Defaults.Keys {
static let verticalHUDWidth = Key<CGFloat>("verticalHUDWidth", default: 36)
static let verticalHUDPadding = Key<CGFloat>("verticalHUDPadding", default: 24)
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
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 enableOSDKeyboardBacklight = Key<Bool>("enableOSDKeyboardBacklight", default: true)
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)
// MARK: Screen Recording Detection Feature
@ -995,6 +1028,7 @@ extension Defaults.Keys {
// MARK: Focus / Do Not Disturb Detection
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 showDoNotDisturbLabel = Key<Bool>("showDoNotDisturbLabel", default: true)
static let focusIndicatorNonPersistent = Key<Bool>("focusIndicatorNonPersistent", default: false)