mirror of
https://github.com/FBukovina/Atoll.git
synced 2026-10-28 23:54:32 +00:00
Fix: DevTools Access
This commit is contained in:
parent
e553ccf667
commit
53a9fdb112
5 changed files with 332 additions and 3 deletions
|
|
@ -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?
|
||||||
|
|
@ -146,6 +149,10 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
||||||
func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
|
func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
|
||||||
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
|
||||||
|
|
@ -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,8 +545,10 @@ 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 }
|
||||||
window.alphaValue =
|
DispatchQueue.main.async {
|
||||||
self.coordinator.selectedScreen == self.coordinator.preferredScreen ? 1 : 0
|
window.alphaValue =
|
||||||
|
self.coordinator.selectedScreen == self.coordinator.preferredScreen ? 1 : 0
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
NotificationCenter.default.addObserver(
|
NotificationCenter.default.addObserver(
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -11006,6 +11006,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" : {
|
||||||
|
|
@ -23164,6 +23168,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" : {
|
||||||
|
|
@ -24421,6 +24428,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" : {
|
||||||
|
|
@ -24543,6 +24554,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,
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
@ -1012,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