Merge pull request #227 from Ebullioscopic/feature/liveactivity

Feature/liveactivity
This commit is contained in:
Hariharan Mudaliar 2026-01-02 17:36:06 +05:30 • committed by GitHub
commit 7bbbcaeabf
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 296 additions and 21 deletions

View file

@ -1824,10 +1824,6 @@
},
"Accessibility" : {
},
"Accessibility permission required" : {
"comment" : "A label explaining that accessibility permissions are required.",
"isCommentAutoGenerated" : true
},
"Actions" : {
"localizations" : {
@ -14994,10 +14990,6 @@
},
"Open Model Settings" : {
},
"Open Settings" : {
"comment" : "A button that opens system settings when pressed.",
"isCommentAutoGenerated" : true
},
"Open System Settings" : {

View file

@ -2402,6 +2402,7 @@ struct Shelf: View {
@Default(.copyOnDrag) var copyOnDrag
@Default(.autoRemoveShelfItems) var autoRemoveShelfItems
@StateObject private var quickShareService = QuickShareService.shared
@ObservedObject private var fullDiskAccessPermission = FullDiskAccessPermissionStore.shared
private var selectedProvider: QuickShareProvider? {
quickShareService.availableProviders.first(where: { $0.id == quickShareProvider })
@ -2417,8 +2418,26 @@ struct Shelf: View {
var body: some View {
Form {
if !fullDiskAccessPermission.isAuthorized {
Section {
SettingsPermissionCallout(
title: "Full Disk Access required",
message: "Grant Full Disk Access so the Shelf can index and move files outside the app sandbox.",
icon: "externaldrive.fill",
iconColor: .purple,
requestButtonTitle: "Request Full Disk Access",
openSettingsButtonTitle: "Open Privacy & Security",
requestAction: { fullDiskAccessPermission.requestAccessPrompt() },
openSettingsAction: { fullDiskAccessPermission.openSystemSettings() }
)
} header: {
Text("Permissions")
}
}
Section {
Defaults.Toggle("Enable shelf", key: .dynamicShelf)
.disabled(!fullDiskAccessPermission.isAuthorized)
.settingsHighlight(id: highlightID("Enable shelf"))
Defaults.Toggle("Open shelf tab by default if items added", key: .openShelfByDefault)
@ -2503,6 +2522,9 @@ struct Shelf: View {
}
.accentColor(.effectiveAccent)
.navigationTitle("Shelf")
.onAppear {
fullDiskAccessPermission.refreshStatus()
}
}
}
@ -2511,6 +2533,7 @@ struct LiveActivitiesSettings: View {
@ObservedObject var recordingManager = ScreenRecordingManager.shared
@ObservedObject var privacyManager = PrivacyIndicatorManager.shared
@ObservedObject var doNotDisturbManager = DoNotDisturbManager.shared
@ObservedObject private var fullDiskAccessPermission = FullDiskAccessPermissionStore.shared
@Default(.enableScreenRecordingDetection) var enableScreenRecordingDetection
@Default(.enableDoNotDisturbDetection) var enableDoNotDisturbDetection
@ -2555,6 +2578,19 @@ struct LiveActivitiesSettings: View {
}
Section {
if !fullDiskAccessPermission.isAuthorized {
SettingsPermissionCallout(
title: "Custom Focus metadata",
message: "Full Disk Access unlocks custom Focus icons, colors, and labels. Standard Focus detection still works without it—grant access only if you need personalized indicators.",
icon: "externaldrive.fill",
iconColor: .purple,
requestButtonTitle: "Request Full Disk Access",
openSettingsButtonTitle: "Open Privacy & Security",
requestAction: { fullDiskAccessPermission.requestAccessPrompt() },
openSettingsAction: { fullDiskAccessPermission.openSystemSettings() }
)
}
Defaults.Toggle("Enable Focus Detection", key: .enableDoNotDisturbDetection)
.settingsHighlight(id: highlightID("Enable Focus Detection"))
@ -2672,6 +2708,9 @@ struct LiveActivitiesSettings: View {
}
}
.navigationTitle("Live Activities")
.onAppear {
fullDiskAccessPermission.refreshStatus()
}
}
}
@ -5217,27 +5256,52 @@ struct CustomOSDSettings: View {
}
struct SettingsPermissionCallout: View {
let title: String
let message: String
let icon: String
let iconColor: Color
let requestButtonTitle: String
let openSettingsButtonTitle: String
let requestAction: () -> Void
let openSettingsAction: () -> Void
init(
title: String = "Accessibility permission required",
message: String,
icon: String = "exclamationmark.triangle.fill",
iconColor: Color = .orange,
requestButtonTitle: String = "Request Access",
openSettingsButtonTitle: String = "Open Settings",
requestAction: @escaping () -> Void,
openSettingsAction: @escaping () -> Void
) {
self.title = title
self.message = message
self.icon = icon
self.iconColor = iconColor
self.requestButtonTitle = requestButtonTitle
self.openSettingsButtonTitle = openSettingsButtonTitle
self.requestAction = requestAction
self.openSettingsAction = openSettingsAction
}
var body: some View {
VStack(alignment: .leading, spacing: 8) {
Label("Accessibility permission required", systemImage: "exclamationmark.triangle.fill")
Label(title, systemImage: icon)
.font(.subheadline.weight(.semibold))
.foregroundStyle(.orange)
.foregroundStyle(iconColor)
Text(message)
.font(.caption)
.foregroundStyle(.secondary)
HStack(spacing: 8) {
Button("Request Access") {
Button(requestButtonTitle) {
requestAction()
}
.buttonStyle(.borderedProminent)
Button("Open Settings") {
Button(openSettingsButtonTitle) {
openSettingsAction()
}
.buttonStyle(.bordered)

View file

@ -0,0 +1,112 @@
import AppKit
import Combine
import Foundation
enum FullDiskAccessAuthorization {
private static let probeURLs: [URL] = [
FileManager.default.homeDirectoryForCurrentUser
.appendingPathComponent("Library/Application Support/com.apple.TCC/TCC.db"),
FileManager.default.homeDirectoryForCurrentUser
.appendingPathComponent("Library/DoNotDisturb/DB/ModeConfigurations.json")
]
static func hasPermission() -> Bool {
for url in probeURLs {
if canReadProtectedResource(at: url) {
return true
}
}
return false
}
private static func canReadProtectedResource(at url: URL) -> Bool {
var isDirectory: ObjCBool = false
guard FileManager.default.fileExists(atPath: url.path, isDirectory: &isDirectory), !isDirectory.boolValue else {
return false
}
do {
let handle = try FileHandle(forReadingFrom: url)
try handle.close()
return true
} catch {
return false
}
}
}
@MainActor
final class FullDiskAccessPermissionStore: ObservableObject {
static let shared = FullDiskAccessPermissionStore()
@Published private(set) var isAuthorized: Bool = FullDiskAccessAuthorization.hasPermission()
private var pollingTask: Task<Void, Never>?
private init() {}
deinit {
pollingTask?.cancel()
}
func refreshStatus() {
updateAuthorizationStatus(to: FullDiskAccessAuthorization.hasPermission())
}
func requestAccessPrompt() {
#if os(macOS)
let alert = NSAlert()
alert.messageText = "Full Disk Access Required"
alert.informativeText = "Dynamic Island needs Full Disk Access to detect custom Focus indicators and power the Shelf. Click Continue to open Full Disk Access settings, then press the + button and select Dynamic Island (we'll reveal it in Finder for you)."
alert.alertStyle = .informational
alert.addButton(withTitle: "Continue")
alert.addButton(withTitle: "Cancel")
if alert.runModal() == .alertFirstButtonReturn {
openSystemSettings()
revealAppBundleInFinder()
}
#endif
beginPollingForStatusChanges()
}
func openSystemSettings() {
#if os(macOS)
if let url = URL(string: "x-apple.systempreferences:com.apple.preference.security?Privacy_AllFiles") {
NSWorkspace.shared.open(url)
}
#endif
}
private func revealAppBundleInFinder() {
#if os(macOS)
let bundleURL = Bundle.main.bundleURL
NSWorkspace.shared.activateFileViewerSelecting([bundleURL])
#endif
}
private func beginPollingForStatusChanges() {
pollingTask?.cancel()
pollingTask = Task { [weak self] in
guard let self else { return }
for _ in 0..<40 {
try? await Task.sleep(nanoseconds: 500_000_000)
let status = FullDiskAccessAuthorization.hasPermission()
await MainActor.run {
self.updateAuthorizationStatus(to: status)
}
if status {
break
}
}
}
}
private func updateAuthorizationStatus(to newValue: Bool) {
guard newValue != isAuthorized else { return }
isAuthorized = newValue
}
}

View file

@ -131,6 +131,9 @@ final class DoNotDisturbManager: ObservableObject {
if nameChanged {
self.currentFocusModeName = finalName
.localizedCaseInsensitiveContains(
"Reduce Interruptions"
) ? "Reduce Interr." : finalName
}
if identifierChanged || nameChanged || shouldToggleActive {
@ -307,6 +310,7 @@ enum FocusModeType: String, CaseIterable {
case gaming = "com.apple.focus.gaming"
case mindfulness = "com.apple.focus.mindfulness"
case reading = "com.apple.focus.reading"
case reduceInterruptions = "com.apple.focus.reduce-interruptions"
case custom = "com.apple.focus.custom"
case unknown = ""
@ -321,6 +325,7 @@ enum FocusModeType: String, CaseIterable {
case .gaming: return "Gaming"
case .mindfulness: return "Mindfulness"
case .reading: return "Reading"
case .reduceInterruptions: return "Reduce Interr."
case .custom: return "Focus"
case .unknown: return "Focus Mode"
}
@ -337,6 +342,7 @@ enum FocusModeType: String, CaseIterable {
case .gaming: return "gamecontroller.fill"
case .mindfulness: return "circle.hexagongrid"
case .reading: return "book.closed.fill"
case .reduceInterruptions: return "apple.intelligence"
case .custom: return "app.badge"
case .unknown: return "moon.fill"
}
@ -362,7 +368,11 @@ enum FocusModeType: String, CaseIterable {
return image
}
return Image(systemName: sfSymbol)
return Image(
systemName: self == .custom
? self.getCustomIconFromFile()
: sfSymbol
)
}
var accentColor: Color {
@ -385,8 +395,10 @@ enum FocusModeType: String, CaseIterable {
return Color(red: 0.361, green: 0.898, blue: 0.883, opacity: 1.0)
case .reading:
return Color(red: 1.000, green: 0.622, blue: 0.044, opacity: 1.0)
case .reduceInterruptions:
return Color(red: 0.686, green: 0.322, blue: 0.871, opacity: 1.0)
case .custom:
return Color(red: 0.513, green: 0.478, blue: 0.965)
return self.getCustomAccentColorFromFile()
case .unknown:
return Color(red: 0.370, green: 0.360, blue: 0.902)
}
@ -425,7 +437,7 @@ extension FocusModeType {
return
}
if normalizedLowercased.hasPrefix("com.apple.focus.") {
if normalizedLowercased.hasPrefix("com.apple.focus") {
self = .custom
return
}
@ -434,23 +446,31 @@ extension FocusModeType {
}
static func resolve(identifier: String?, name: String?) -> FocusModeType {
let trimmedName = name?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
if !trimmedName.isEmpty {
if let name, !name.isEmpty {
if let match = FocusModeType.allCases.first(where: {
guard !$0.displayName.isEmpty else { return false }
return $0.displayName.compare(trimmedName, options: [.caseInsensitive, .diacriticInsensitive]) == .orderedSame
return $0.displayName.compare(name, options: [.caseInsensitive, .diacriticInsensitive]) == .orderedSame
}) {
return match
}
}
let trimmedIdentifier = identifier?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
if !trimmedIdentifier.isEmpty {
return FocusModeType(identifier: trimmedIdentifier)
if let identifier, !identifier.isEmpty {
return FocusModeType(identifier: identifier)
}
return .doNotDisturb
}
func getCustomIconFromFile() -> String {
return FocusMetadataReader.shared
.getIcon(for: DoNotDisturbManager.shared.currentFocusModeName)
}
func getCustomAccentColorFromFile() -> Color {
return FocusMetadataReader.shared
.getAccentColor(for: DoNotDisturbManager.shared.currentFocusModeName)
}
}
// MARK: - Metadata helpers
@ -943,3 +963,90 @@ private enum FocusMetadataDecoder {
return nil
}
}
private final class FocusMetadataReader {
private let pathToDatabase:URL = FileManager.default.homeDirectoryForCurrentUser.appendingPathComponent("Library/DoNotDisturb/DB/ModeConfigurations.json")
struct DNDConfigRoot: Codable {
let data: [DNDDataEntry]
}
struct DNDDataEntry: Codable {
let modeConfigurations: [String: DNDModeWrapper]
}
struct DNDModeWrapper: Codable {
let mode: DNDMode
}
struct DNDMode: Codable {
let name: String
let symbolImageName: String?
let tintColorName: String?
}
private init(){}
static let shared = FocusMetadataReader()
private func getModeConfig(for focusName: String) -> DNDMode? {
guard FullDiskAccessAuthorization.hasPermission() else { return nil }
do {
let data = try Data(contentsOf: pathToDatabase)
let root = try JSONDecoder().decode(DNDConfigRoot.self, from: data)
for entry in root.data {
for wrapper in entry.modeConfigurations.values {
if wrapper.mode.name
.localizedCaseInsensitiveCompare(focusName) == .orderedSame {
return wrapper.mode
}
}
}
} catch {
print("JSON Error: \(error)")
}
return nil
}
/// Fetch the icon for the current focus from disk. If the focus is not found return the placeholder `app.badge`
/// - Returns A string representing the sfSymbol of the current focus
func getIcon(for focus: String) -> String {
guard let mode = getModeConfig(for: focus) else { return "app.badge" }
return mode.symbolImageName ?? "app.badge"
}
/// Fetch the accent color for the current focus from disk. If the focus is not found return the placeholder `Color.indigo`
/// - Returns A Color representing the accent color for the current focus
func getAccentColor(for focus: String) -> Color {
guard let mode = getModeConfig(for: focus),
let colorName = mode.tintColorName else { return .indigo }
return Color.stringToColor(for: colorName)
}
}
extension Color {
static func stringToColor(for string:String) -> Color {
let cleanName = string.lowercased()
.replacingOccurrences(of: "system", with: "")
.replacingOccurrences(of: "color", with: "")
switch cleanName {
case "red": return .red
case "orange": return .orange
case "yellow": return .yellow
case "green": return .green
case "mint": return .mint
case "teal": return .teal
case "cyan": return .cyan
case "blue": return .blue
case "indigo": return .indigo
case "purple": return .purple
case "pink": return .pink
case "gray", "grey": return .gray
default: return .indigo
}
}
}