From 628d76b41ce3ffb65a51ac30e19b23b45d5d39bd Mon Sep 17 00:00:00 2001 From: Hariharan Mudaliar Date: Sun, 13 Jul 2025 23:17:43 +0530 Subject: [PATCH] Applied latest changes from boring.notch --- DynamicIsland/ContentView.swift | 2 +- DynamicIsland/DynamicIslandApp.swift | 199 ++++++++---- DynamicIsland/Localizable.xcstrings | 46 ++- .../YouTubeMusicController.swift | 6 +- .../Providers/CalendarServiceProviding.swift | 302 ++++++++++++++++++ .../DynamicIslandCalendar.swift | 103 +++--- .../Notch/DynamicIslandExtrasMenu.swift | 12 +- .../Notch/DynamicIslandHeader.swift | 31 +- .../components/Notch/NotchHomeView.swift | 31 +- .../components/Notch/NotchShape.swift | 2 +- .../components/Settings/SettingsView.swift | 183 +++++------ .../Settings/SettingsWindowController.swift | 125 ++++++++ .../components/Webcam/WebcamView.swift | 2 +- DynamicIsland/helpers/AppleScriptHelper.swift | 2 +- DynamicIsland/managers/CalendarManager.swift | 86 ++--- DynamicIsland/managers/WebcamManager.swift | 5 +- DynamicIsland/models/CalendarModel.swift | 17 + DynamicIsland/models/EventModel.swift | 125 ++++++++ 18 files changed, 940 insertions(+), 339 deletions(-) create mode 100644 DynamicIsland/Providers/CalendarServiceProviding.swift rename DynamicIsland/components/{Calender => Calendar}/DynamicIslandCalendar.swift (76%) create mode 100644 DynamicIsland/components/Settings/SettingsWindowController.swift create mode 100644 DynamicIsland/models/CalendarModel.swift create mode 100644 DynamicIsland/models/EventModel.swift diff --git a/DynamicIsland/ContentView.swift b/DynamicIsland/ContentView.swift index 1467975..fbb7861 100644 --- a/DynamicIsland/ContentView.swift +++ b/DynamicIsland/ContentView.swift @@ -15,7 +15,7 @@ import SwiftUIIntrospect struct ContentView: View { @EnvironmentObject var vm: DynamicIslandViewModel - @StateObject var webcamManager: WebcamManager = .init() + @EnvironmentObject var webcamManager: WebcamManager @ObservedObject var coordinator = DynamicIslandViewCoordinator.shared @ObservedObject var musicManager = MusicManager.shared diff --git a/DynamicIsland/DynamicIslandApp.swift b/DynamicIsland/DynamicIslandApp.swift index f1ae4b8..3e7cae2 100644 --- a/DynamicIsland/DynamicIslandApp.swift +++ b/DynamicIsland/DynamicIslandApp.swift @@ -7,27 +7,32 @@ import AVFoundation import Combine +import Defaults import KeyboardShortcuts import Sparkle import SwiftUI -import Defaults @main struct DynamicNotchApp: App { @NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate @Default(.menubarIcon) var showMenuBarIcon @Environment(\.openWindow) var openWindow + let updaterController: SPUStandardUpdaterController - + init() { - updaterController = SPUStandardUpdaterController(startingUpdater: true, updaterDelegate: nil, userDriverDelegate: nil) + updaterController = SPUStandardUpdaterController( + startingUpdater: true, updaterDelegate: nil, userDriverDelegate: nil) + + // Initialize the settings window controller with the updater controller + SettingsWindowController.shared.setUpdaterController(updaterController) } - + var body: some Scene { MenuBarExtra("dynamic.island", systemImage: "sparkle", isInserted: $showMenuBarIcon) { - SettingsLink(label: { - Text("Settings") - }) + Button("Settings") { + SettingsWindowController.shared.showWindow() + } .keyboardShortcut(KeyEquivalent(","), modifiers: .command) if false { Button("Activate License") { @@ -37,37 +42,27 @@ struct DynamicNotchApp: App { CheckForUpdatesView(updater: updaterController.updater) Divider() Button("Restart Dynamic Island") { - guard let bundleIdentifier = Bundle.main.bundleIdentifier else { return } - - let workspace = NSWorkspace.shared - - if let appURL = workspace.urlForApplication(withBundleIdentifier: bundleIdentifier) { - - let configuration = NSWorkspace.OpenConfiguration() - configuration.createsNewApplicationInstance = true - - workspace.openApplication(at: appURL, configuration: configuration) - } - - NSApplication.shared.terminate(nil) + guard let bundleIdentifier = Bundle.main.bundleIdentifier else { return } + + let workspace = NSWorkspace.shared + + if let appURL = workspace.urlForApplication(withBundleIdentifier: bundleIdentifier) + { + + let configuration = NSWorkspace.OpenConfiguration() + configuration.createsNewApplicationInstance = true + + workspace.openApplication(at: appURL, configuration: configuration) + } + + NSApplication.shared.terminate(nil) } Button("Quit", role: .destructive) { NSApp.terminate(nil) } .keyboardShortcut(KeyEquivalent("Q"), modifiers: .command) } - - Settings { - SettingsView(updaterController: updaterController) - } - .defaultSize(CGSize(width: 750, height: 700)) - - Window("Onboarding", id: "onboarding") { - ProOnboard() - } - .windowStyle(.hiddenTitleBar) - .windowResizability(.contentSize) - + Window("Activation", id: "activation") { ActivationWindow() } @@ -85,11 +80,12 @@ class AppDelegate: NSObject, NSApplicationDelegate { @ObservedObject var coordinator = DynamicIslandViewCoordinator.shared var whatsNewWindow: NSWindow? var timer: Timer? - let calenderManager = CalendarManager() + let calendarManager = CalendarManager() + let webcamManager = WebcamManager.shared var closeNotchWorkItem: DispatchWorkItem? private var previousScreens: [NSScreen]? - @Environment(\.openWindow) var openWindow - + private var onboardingWindowController: NSWindowController? + func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { return false } @@ -125,10 +121,13 @@ class AppDelegate: NSObject, NSApplicationDelegate { self.window = nil } } - - private func createDynamicIslandWindow(for screen: NSScreen, with viewModel: DynamicIslandViewModel) -> NSWindow { + + private func createDynamicIslandWindow(for screen: NSScreen, with viewModel: DynamicIslandViewModel) + -> NSWindow + { let window = DynamicIslandWindow( - contentRect: NSRect(x: 0, y: 0, width: openNotchSize.width, height: openNotchSize.height), + contentRect: NSRect( + x: 0, y: 0, width: openNotchSize.width, height: openNotchSize.height), styleMask: [.borderless, .nonactivatingPanel, .utilityWindow, .hudWindow], backing: .buffered, defer: false @@ -137,14 +136,16 @@ class AppDelegate: NSObject, NSApplicationDelegate { window.contentView = NSHostingView( rootView: ContentView() .environmentObject(viewModel) + .environmentObject(webcamManager) ) window.orderFrontRegardless() NotchSpaceManager.shared.notchSpace.windows.insert(window) return window } - - private func positionWindow(_ window: NSWindow, on screen: NSScreen, changeAlpha: Bool = false) { + + private func positionWindow(_ window: NSWindow, on screen: NSScreen, changeAlpha: Bool = false) + { if changeAlpha { window.alphaValue = 0 } @@ -152,45 +153,56 @@ class AppDelegate: NSObject, NSApplicationDelegate { DispatchQueue.main.async { [weak window] in guard let window = window else { return } let screenFrame = screen.frame - window.setFrameOrigin(NSPoint( - x: screenFrame.origin.x + (screenFrame.width / 2) - window.frame.width / 2, - y: screenFrame.origin.y + screenFrame.height - window.frame.height - )) + window.setFrameOrigin( + NSPoint( + x: screenFrame.origin.x + (screenFrame.width / 2) - window.frame.width / 2, + y: screenFrame.origin.y + screenFrame.height - window.frame.height + )) window.alphaValue = 1 } } func applicationDidFinishLaunching(_ notification: Notification) { - - coordinator.setupWorkersNotificationObservers(); - + + coordinator.setupWorkersNotificationObservers() + NotificationCenter.default.addObserver( self, selector: #selector(screenConfigurationDidChange), name: NSApplication.didChangeScreenParametersNotification, object: nil ) - - NotificationCenter.default.addObserver(forName: Notification.Name.selectedScreenChanged, object: nil, queue: nil) { [weak self] _ in + + NotificationCenter.default.addObserver( + forName: Notification.Name.selectedScreenChanged, object: nil, queue: nil + ) { [weak self] _ in self?.adjustWindowPosition(changeAlpha: true) } - - NotificationCenter.default.addObserver(forName: Notification.Name.notchHeightChanged, object: nil, queue: nil) { [weak self] _ in + + NotificationCenter.default.addObserver( + forName: Notification.Name.notchHeightChanged, object: nil, queue: nil + ) { [weak self] _ in self?.adjustWindowPosition() } - - NotificationCenter.default.addObserver(forName: Notification.Name.automaticallySwitchDisplayChanged, object: nil, queue: nil) { [weak self] _ in + + NotificationCenter.default.addObserver( + forName: Notification.Name.automaticallySwitchDisplayChanged, object: nil, queue: nil + ) { [weak self] _ in guard let self = self, let window = self.window else { return } - window.alphaValue = self.coordinator.selectedScreen == self.coordinator.preferredScreen ? 1 : 0 + window.alphaValue = + self.coordinator.selectedScreen == self.coordinator.preferredScreen ? 1 : 0 } - NotificationCenter.default.addObserver(forName: Notification.Name.showOnAllDisplaysChanged, object: nil, queue: nil) { [weak self] _ in + NotificationCenter.default.addObserver( + forName: Notification.Name.showOnAllDisplaysChanged, object: nil, queue: nil + ) { [weak self] _ in guard let self = self else { return } self.cleanupWindows(shouldInvert: true) - - if(!Defaults[.showOnAllDisplays]) { + + if !Defaults[.showOnAllDisplays] { let viewModel = self.vm - let window = self.createDynamicIslandWindow(for: NSScreen.main ?? NSScreen.screens.first!, with: viewModel) + let window = self.createDynamicIslandWindow( + for: NSScreen.main ?? NSScreen.screens.first!, with: viewModel) self.window = window self.adjustWindowPosition(changeAlpha: true) } else { @@ -198,8 +210,12 @@ class AppDelegate: NSObject, NSApplicationDelegate { } } - DistributedNotificationCenter.default().addObserver(self, selector: #selector(onScreenLocked(_:)), name: NSNotification.Name(rawValue: "com.apple.screenIsLocked"), object: nil) - DistributedNotificationCenter.default().addObserver(self, selector: #selector(onScreenUnlocked(_:)), name: NSNotification.Name(rawValue: "com.apple.screenIsUnlocked"), object: nil) + DistributedNotificationCenter.default().addObserver( + self, selector: #selector(onScreenLocked(_:)), + name: NSNotification.Name(rawValue: "com.apple.screenIsLocked"), object: nil) + DistributedNotificationCenter.default().addObserver( + self, selector: #selector(onScreenUnlocked(_:)), + name: NSNotification.Name(rawValue: "com.apple.screenIsUnlocked"), object: nil) KeyboardShortcuts.onKeyDown(for: .toggleSneakPeek) { [weak self] in guard let self = self else { return } @@ -216,8 +232,8 @@ class AppDelegate: NSObject, NSApplicationDelegate { let mouseLocation = NSEvent.mouseLocation var viewModel = self.vm - - if(Defaults[.showOnAllDisplays]) { + + if Defaults[.showOnAllDisplays] { for screen in NSScreen.screens { if screen.frame.contains(mouseLocation) { if let screenViewModel = self.viewModels[screen] { @@ -248,7 +264,8 @@ class AppDelegate: NSObject, NSApplicationDelegate { if !Defaults[.showOnAllDisplays] { let viewModel = self.vm - let window = createDynamicIslandWindow(for: NSScreen.main ?? NSScreen.screens.first!, with: viewModel) + let window = createDynamicIslandWindow( + for: NSScreen.main ?? NSScreen.screens.first!, with: viewModel) self.window = window adjustWindowPosition(changeAlpha: true) } else { @@ -257,7 +274,7 @@ class AppDelegate: NSObject, NSApplicationDelegate { if coordinator.firstLaunch { DispatchQueue.main.async { - self.openWindow(id: "onboarding") + self.showOnboardingWindow() } playWelcomeSound() } @@ -283,11 +300,13 @@ class AppDelegate: NSObject, NSApplicationDelegate { @objc func screenConfigurationDidChange() { let currentScreens = NSScreen.screens - - let screensChanged = currentScreens.count != previousScreens?.count || - Set(currentScreens.map { $0.localizedName }) != Set(previousScreens?.map { $0.localizedName } ?? []) || - Set(currentScreens.map { $0.frame }) != Set(previousScreens?.map { $0.frame } ?? []) - + + let screensChanged = + currentScreens.count != previousScreens?.count + || Set(currentScreens.map { $0.localizedName }) + != Set(previousScreens?.map { $0.localizedName } ?? []) + || Set(currentScreens.map { $0.frame }) != Set(previousScreens?.map { $0.frame } ?? []) + previousScreens = currentScreens if screensChanged { @@ -330,8 +349,10 @@ class AppDelegate: NSObject, NSApplicationDelegate { } } else { let selectedScreen: NSScreen - - if let preferredScreen = NSScreen.screens.first(where: { $0.localizedName == coordinator.preferredScreen }) { + + if let preferredScreen = NSScreen.screens.first(where: { + $0.localizedName == coordinator.preferredScreen + }) { coordinator.selectedScreen = coordinator.preferredScreen selectedScreen = preferredScreen } else if Defaults[.automaticallySwitchDisplay], let mainScreen = NSScreen.main { @@ -376,6 +397,42 @@ class AppDelegate: NSObject, NSApplicationDelegate { @objc func quitAction() { NSApplication.shared.terminate(nil) } + + private func showOnboardingWindow() { + if onboardingWindowController == nil { + let window = NSWindow( + contentRect: NSRect(x: 0, y: 0, width: 400, height: 600), + styleMask: [.titled, .fullSizeContentView], + backing: .buffered, + defer: false + ) + window.center() + window.title = "Onboarding" + window.titlebarAppearsTransparent = true + window.titleVisibility = .hidden + window.contentView = NSHostingView(rootView: OnboardingView( + onFinish: { + window.orderOut(nil) + NSApp.setActivationPolicy(.accessory) + window.close() + NSApp.deactivate() + }, + onOpenSettings: { + window.close() + SettingsWindowController.shared.showWindow() + } + )) + window.isRestorable = false + window.identifier = NSUserInterfaceItemIdentifier("OnboardingWindow") + + onboardingWindowController = NSWindowController(window: window) + } + + NSApp.setActivationPolicy(.regular) + NSApp.activate(ignoringOtherApps: true) + onboardingWindowController?.window?.makeKeyAndOrderFront(nil) + onboardingWindowController?.window?.orderFrontRegardless() + } } extension Notification.Name { diff --git a/DynamicIsland/Localizable.xcstrings b/DynamicIsland/Localizable.xcstrings index c506974..4404e98 100644 --- a/DynamicIsland/Localizable.xcstrings +++ b/DynamicIsland/Localizable.xcstrings @@ -1,6 +1,9 @@ { "sourceLanguage" : "en", "strings" : { + "" : { + + }, " – %lld" : { }, @@ -55,6 +58,7 @@ } }, "Accent color" : { + "extractionState" : "stale", "localizations" : { "en" : { "stringUnit" : { @@ -96,6 +100,9 @@ }, "All-day" : { + }, + "Allow Access" : { + }, "Always hide in fullscreen" : { @@ -225,6 +232,9 @@ }, "Check for Updates…" : { + }, + "Choose a Music Source" : { + }, "Circle" : { @@ -241,8 +251,12 @@ } } } + }, + "Continue" : { + }, "Custom color" : { + "extractionState" : "stale", "localizations" : { "en" : { "stringUnit" : { @@ -263,6 +277,9 @@ }, "Custom vizualizers (Lottie)" : { + }, + "Customize in Settings" : { + }, "Default" : { @@ -314,6 +331,9 @@ }, "Drop files here" : { + }, + "Dynamic Island" : { + }, "dynamic.island" : { @@ -382,6 +402,9 @@ }, "Extensions" : { + }, + "Finish" : { + }, "General" : { "localizations" : { @@ -475,7 +498,6 @@ }, "Made with 🫢🏻 by not so boring not.people" : { - "extractionState" : "stale", "localizations" : { "en" : { "stringUnit" : { @@ -484,9 +506,6 @@ } } } - }, - "Made with 🫢🏻 by the DynamicIsland team" : { - }, "Match menubar height" : { @@ -605,6 +624,9 @@ }, "Non-notch display height" : { + }, + "Not Now" : { + }, "Notch behavior" : { @@ -617,9 +639,6 @@ }, "OK" : { - }, - "Onboarding" : { - }, "One or more files failed to load" : { @@ -732,6 +751,9 @@ }, "Select Calendars" : { + }, + "Select the music source you want to use. You can change this later in the app settings." : { + }, "selected" : { @@ -901,14 +923,20 @@ } } } + }, + "View on GitHub: th-ch/youtube-music" : { + }, "Welcome" : { }, - "Welcome window" : { + "What's New" : { }, - "What's New" : { + "You can now enjoy the app. If you want to tweak things further, you can always visit the settings." : { + + }, + "You're All Set!" : { }, "YouTube Music requires this third-party app to be installed: " : { diff --git a/DynamicIsland/MediaControllers/YouTubeMusicController.swift b/DynamicIsland/MediaControllers/YouTubeMusicController.swift index 293539d..d5cb82d 100644 --- a/DynamicIsland/MediaControllers/YouTubeMusicController.swift +++ b/DynamicIsland/MediaControllers/YouTubeMusicController.swift @@ -2,7 +2,7 @@ // YouTubeMusicController.swift // DynamicIsland // -// Created By Alexander Greco on 2025-03-30. +// Created By Alexander on 2025-03-30. // import Foundation @@ -64,7 +64,7 @@ class YouTubeMusicController: MediaControllerProtocol { } private func getAccessToken(completion: @escaping (Bool) -> Void) { - guard let url = URL(string: "\(baseURL)/auth/dynamicIsland") else { + guard let url = URL(string: "\(baseURL)/auth/DynamicIsland") else { completion(false) return } @@ -80,7 +80,7 @@ class YouTubeMusicController: MediaControllerProtocol { .receive(on: DispatchQueue.main) .sink(receiveCompletion: { completionStatus in switch completionStatus { - case .failure(let error): + case .failure(_): completion(false) case .finished: break diff --git a/DynamicIsland/Providers/CalendarServiceProviding.swift b/DynamicIsland/Providers/CalendarServiceProviding.swift new file mode 100644 index 0000000..8b76d79 --- /dev/null +++ b/DynamicIsland/Providers/CalendarServiceProviding.swift @@ -0,0 +1,302 @@ +// +// CalendarServiceProvider.swift +// Calendr +// +// Created by Paker on 31/12/20. +// Original source: Original source: https://github.com/pakerwreah/Calendr +// Modified by Alexander on 08/06/25 +// + +import Foundation +import EventKit + +protocol CalendarServiceProviding { + func requestAccess() async -> Bool + func calendars() async -> [CalendarModel] + func events(from start: Date, to end: Date, calendars: [String]) async -> [EventModel] +} + +class CalendarService: CalendarServiceProviding { + private let store = EKEventStore() + + @MainActor + func requestAccess() async -> Bool { + do { + let eventsAccess = try await requestAccess(to: .event) + let remindersAccess = try await requestAccess(to: .reminder) + return eventsAccess || remindersAccess // At least one should work + } catch { + print("Calendar access error: \(error)") + return false + } + } + + private func requestAccess(to type: EKEntityType) async throws -> Bool { + if #available(macOS 14.0, *) { + switch type { + case .event: + return try await store.requestFullAccessToEvents() + case .reminder: + return try await store.requestFullAccessToReminders() + @unknown default: + return false + } + } else { + return try await store.requestAccess(to: type) + } + } + + private func hasAccess(to entityType: EKEntityType) -> Bool { + let status = EKEventStore.authorizationStatus(for: entityType) + if #available(macOS 14.0, *) { + return status == .fullAccess + } else { + return status == .authorized + } + } + + func calendars() async -> [CalendarModel] { + var calendars: [EKCalendar] = [] + + for type in [EKEntityType.event, .reminder] where hasAccess(to: type) { + calendars.append(contentsOf: store.calendars(for: type)) + } + + return calendars.map { CalendarModel(from: $0) } + } + + func events(from start: Date, to end: Date, calendars ids: [String]) async -> [EventModel] { + let allCalendars = await self.calendars() + let filteredCalendars = allCalendars.filter { ids.isEmpty || ids.contains($0.id) } + let ekCalendars = filteredCalendars.compactMap { calendarModel in + store.calendars(for: .event).first { $0.calendarIdentifier == calendarModel.id } ?? + store.calendars(for: .reminder).first { $0.calendarIdentifier == calendarModel.id } + } + + var events: [EventModel] = [] + + // Fetch regular events + if hasAccess(to: .event) { + let eventCalendars = ekCalendars.filter { store.calendars(for: .event).contains($0) } + let predicate = store.predicateForEvents(withStart: start, end: end, calendars: eventCalendars) + let ekEvents = store.events(matching: predicate) + events.append(contentsOf: ekEvents.compactMap { EventModel(from: $0) }) + } + + // Fetch reminders + if hasAccess(to: .reminder) { + let reminderCalendars = ekCalendars.filter { store.calendars(for: .reminder).contains($0) } + events.append(contentsOf: await fetchReminders(from: start, to: end, calendars: reminderCalendars)) + } + + return events.sorted { $0.start < $1.start } + } + + private func fetchReminders(from start: Date, to end: Date, calendars: [EKCalendar]) async -> [EventModel] { + await withTaskGroup(of: [EKReminder].self) { group in + var allReminders: [EKReminder] = [] + + // Fetch incomplete reminders + group.addTask { + await withCheckedContinuation { continuation in + let predicate = self.store.predicateForIncompleteReminders( + withDueDateStarting: start, + ending: end, + calendars: calendars + ) + self.store.fetchReminders(matching: predicate) { reminders in + continuation.resume(returning: reminders ?? []) + } + } + } + + // Fetch completed reminders + group.addTask { + await withCheckedContinuation { continuation in + let predicate = self.store.predicateForCompletedReminders( + withCompletionDateStarting: start, + ending: end, + calendars: calendars + ) + self.store.fetchReminders(matching: predicate) { reminders in + continuation.resume(returning: reminders ?? []) + } + } + } + + for await reminders in group { + allReminders.append(contentsOf: reminders) + } + + // Remove duplicates and convert to EventModel + let uniqueReminders = Dictionary(grouping: allReminders, by: \.calendarItemIdentifier) + .compactMapValues { $0.first } + .values + + return Array(uniqueReminders.compactMap { EventModel(from: $0) }) + } + } +} + +// MARK: - Model Extensions + +extension CalendarModel { + init(from calendar: EKCalendar) { + self.init( + id: calendar.calendarIdentifier, + account: calendar.accountTitle, + title: calendar.title, + color: calendar.color, + isSubscribed: calendar.isSubscribed || calendar.isDelegate + ) + } +} + +extension EventModel { + init?(from event: EKEvent) { + guard let calendar = event.calendar else { return nil } + + self.init( + id: event.calendarItemIdentifier, + start: event.startDate, + end: event.endDate, + title: event.title ?? "", + location: event.location, + notes: event.notes, + url: event.url, + isAllDay: event.shouldBeAllDay, + type: .init(from: event), + calendar: .init(from: calendar), + participants: .init(from: event), + timeZone: calendar.isSubscribed || calendar.isDelegate ? nil : event.timeZone, + hasRecurrenceRules: event.hasRecurrenceRules || event.isDetached, + priority: nil + ) + } + + init?(from reminder: EKReminder) { + guard let calendar = reminder.calendar, + let dueDateComponents = reminder.dueDateComponents, + let date = Calendar.current.date(from: dueDateComponents) + else { return nil } + + self.init( + id: reminder.calendarItemIdentifier, + start: date, + end: Calendar.current.endOfDay(for: date), + title: reminder.title ?? "", + location: reminder.location, + notes: reminder.notes, + url: reminder.url, + isAllDay: dueDateComponents.hour == nil, + type: .reminder(completed: reminder.isCompleted), + calendar: .init(from: calendar), + participants: [], + timeZone: calendar.isSubscribed || calendar.isDelegate ? nil : reminder.timeZone, + hasRecurrenceRules: reminder.hasRecurrenceRules, + priority: .init(from: reminder.priority) + ) + } +} + +extension EventType { + init(from event: EKEvent) { + self = event.birthdayContactIdentifier != nil ? .birthday : .event(.init(from: event.currentUser?.participantStatus)) + } +} + +extension AttendanceStatus { + init(from status: EKParticipantStatus?) { + switch status { + case .accepted: + self = .accepted + case .tentative: + self = .maybe + case .declined: + self = .declined + case .pending: + self = .pending + default: + self = .unknown + } + } +} + +extension Array where Element == Participant { + init(from event: EKEvent) { + var participants = event.attendees ?? [] + if let organizer = event.organizer, !participants.contains(where: { $0.url == organizer.url }) { + participants.append(organizer) + } + self.init( + participants.map { .init(from: $0, isOrganizer: $0.url == event.organizer?.url) } + ) + } +} + +extension Participant { + init(from participant: EKParticipant, isOrganizer: Bool) { + self.init( + name: participant.name ?? participant.url.absoluteString.replacingOccurrences(of: "mailto:", with: ""), + status: .init(from: participant.participantStatus), + isOrganizer: isOrganizer, + isCurrentUser: participant.isCurrentUser + ) + } +} + +extension Priority { + init?(from p: Int) { + switch p { + case 1...4: + self = .high + case 5: + self = .medium + case 6...9: + self = .low + default: + return nil + } + } +} + +// MARK: - Helper Extensions + +private extension EKCalendar { + var accountTitle: String { + switch source.sourceType { + case .local, .subscribed, .birthdays: + return "Other" + default: + return source.title + } + } + + var isDelegate: Bool { + if #available(macOS 13.0, *) { + return source.isDelegate + } else { + return false + } + } +} + +private extension EKEvent { + var currentUser: EKParticipant? { + attendees?.first(where: \.isCurrentUser) + } + + var shouldBeAllDay: Bool { + guard !isAllDay else { return true } + let calendar = Calendar.current + let startOfDay = calendar.startOfDay(for: startDate) + let endOfDay = calendar.dateInterval(of: .day, for: endDate)?.end + return startDate == startOfDay && endDate == endOfDay + } +} + +private extension Calendar { + func endOfDay(for date: Date) -> Date { + dateInterval(of: .day, for: date)?.end ?? date + } +} diff --git a/DynamicIsland/components/Calender/DynamicIslandCalendar.swift b/DynamicIsland/components/Calendar/DynamicIslandCalendar.swift similarity index 76% rename from DynamicIsland/components/Calender/DynamicIslandCalendar.swift rename to DynamicIsland/components/Calendar/DynamicIslandCalendar.swift index 272dfe7..4dfd91f 100644 --- a/DynamicIsland/components/Calender/DynamicIslandCalendar.swift +++ b/DynamicIsland/components/Calendar/DynamicIslandCalendar.swift @@ -5,9 +5,9 @@ // Created by Harsh Vardhan Goswami on 08/09/24. // -import EventKit import SwiftUI import Defaults +import EventKit struct Config: Equatable { // var count: Int = 10 // 3 days past + today + 7 days future @@ -45,7 +45,9 @@ struct WheelPicker: View { withAnimation{ scrollPosition = indexForDate(date, offset: offset) - config.offset } - //haptics.toggle() // Causes double haptic when click + if (Defaults[.enableHaptics]) { + haptics.toggle() + } } } } @@ -84,15 +86,13 @@ struct WheelPicker: View { private func dayText(date: String, isSelected: Bool) -> some View { Text(date) .font(.caption2) - .foregroundStyle( - isSelected ? Defaults[.accentColor] : .gray - ) + .foregroundStyle(isSelected ? Color.accentColor : .gray) } - + private func dateCircle(date: Date, isSelected: Bool) -> some View { ZStack { Circle() - .fill(isSelected ? Defaults[.accentColor] : .clear) + .fill(isSelected ? Color.accentColor : .clear) .frame(width: 24, height: 24) Text("\(date.date)") .font(.title3) @@ -105,15 +105,17 @@ struct WheelPicker: View { let todayIndex = indexForDate(Date(), offset: offset) guard let newIndex = newValue else { return } let targetDateIndex = newIndex + config.offset - switch targetDateIndex{ - case todayIndex-config.past.. Int { let calendar = Calendar.current let startDate = calendar.startOfDay(for: calendar.date(byAdding: .day, value: offset, to: Date()) ?? Date()) @@ -129,23 +131,18 @@ struct WheelPicker: View { let daysDifference = calendar.dateComponents([.day], from: startDate, to: targetDate).day ?? 0 return daysDifference } - + private func dateForIndex(_ index: Int, offset: Int) -> Date { let startDate = Calendar.current.date(byAdding: .day, value: offset, to: Date()) ?? Date() return Calendar.current.date(byAdding: .day, value: index, to: startDate) ?? Date() } - - private func dayForIndex(_ index: Int, offset: Int) -> String { - let date = dateForIndex(index, offset: offset) - return dateToString(for: date) - } - + private func dateToString(for date: Date) -> String { let formatter = DateFormatter() formatter.dateFormat = "E" return formatter.string(from: date) } - + private func isDateSelected(_ index: Int, offset: Int) -> Bool { Calendar.current.isDate(dateForIndex(index, offset: offset), inSameDayAs: selectedDate) } @@ -155,7 +152,7 @@ struct CalendarView: View { @EnvironmentObject var vm: DynamicIslandViewModel @StateObject private var calendarManager = CalendarManager() @State private var selectedDate = Date() - + var body: some View { VStack(spacing: 8) { HStack { @@ -185,13 +182,19 @@ struct CalendarView: View { } .listRowBackground(Color.clear) .onChange(of: selectedDate) { _, newDate in - calendarManager.updateCurrentDate(newDate) + Task { + await calendarManager.updateCurrentDate(newDate) + } } .onChange(of: vm.notchState) { _, _ in - calendarManager.updateCurrentDate(Date.now) + Task { + await calendarManager.updateCurrentDate(Date.now) + } } .onAppear { - calendarManager.updateCurrentDate(Date.now) + Task { + await calendarManager.updateCurrentDate(Date.now) + } } } } @@ -211,7 +214,7 @@ struct EmptyEventsView: View { struct EventListView: View { @Environment(\.openURL) private var openURL - let events: [EKEvent] + let events: [EventModel] var body: some View { ScrollView(showsIndicators: false) { @@ -219,13 +222,11 @@ struct EventListView: View { VStack(alignment: .trailing, spacing: 5) { ForEach(events.indices, id: \.self) { index in VStack(alignment: .trailing) { - if isAllDayEvent( - start: events[index].startDate, end: events[index].endDate) - { + if events[index].isAllDay { Text("All-day") } else { - Text("\(events[index].startDate, style: .time)") - Text("\(events[index].endDate, style: .time)") + Text("\(events[index].start, style: .time)") + Text("\(events[index].end, style: .time)") } } .multilineTextAlignment(.trailing) @@ -239,10 +240,10 @@ struct EventListView: View { HStack(alignment: .top) { VStack(spacing: 5) { Image( - systemName: isEventEnded(events[index].endDate) + systemName: events[index].eventStatus == .ended ? "checkmark.circle" : "circle" ) - .foregroundColor(isEventEnded(events[index].endDate) ? .green : .gray) + .foregroundColor(events[index].eventStatus == .ended ? .green : .gray) .font(.footnote) Rectangle() .frame(width: 1) @@ -252,7 +253,7 @@ struct EventListView: View { .padding(.top, 1) Button { - if let url = generateEventURL(for: events[index]) { + if let url = events[index].calendarAppURL() { openURL(url) } } label: { @@ -264,7 +265,7 @@ struct EventListView: View { Spacer(minLength: 0) } - .opacity(isEventEnded(events[index].endDate) ? 0.6 : 1) + .opacity((events[index].eventStatus == .ended) ? 0.6 : 1) } } } @@ -272,44 +273,12 @@ struct EventListView: View { .scrollIndicators(.never) .scrollTargetBehavior(.viewAligned) } - - private func isAllDayEvent(start: Date, end: Date) -> Bool { - let calendar = Calendar.current - - let startComponents = calendar.dateComponents([.hour, .minute], from: start) - let endComponents = calendar.dateComponents([.hour, .minute], from: end) - - return startComponents.hour == 0 && startComponents.minute == 0 && endComponents.hour == 23 - && endComponents.minute == 59 - } - - private func isEventEnded(_ end: Date) -> Bool { - return Date.now > end - } - - private func generateEventURL(for event: EKEvent) -> URL? { - var dateComponent = "" - if event.hasRecurrenceRules { - if let startDate = event.startDate { - let formatter = DateFormatter() - formatter.dateFormat = "yyyyMMdd'T'HHmmss'Z'" - formatter.timeZone = TimeZone.current - if !event.isAllDay { - formatter.timeZone = TimeZone(secondsFromGMT: 0) - } - dateComponent = "/\(formatter.string(from: startDate))" - } - } - return URL(string: "ical://ekevent\(dateComponent)/\(event.calendarItemIdentifier)?method=show&options=more") - } } -// Keep the CalendarManager, EmptyEventsView, and EventListView as they were in the previous implementation - #Preview { CalendarView() .frame(width: 250) .padding(.horizontal) .background(.black) - .environmentObject(DynamicIslandViewModel()) + .environmentObject(DynamicIslandViewModel.init()) } diff --git a/DynamicIsland/components/Notch/DynamicIslandExtrasMenu.swift b/DynamicIsland/components/Notch/DynamicIslandExtrasMenu.swift index 4a8e7a6..b546207 100644 --- a/DynamicIsland/components/Notch/DynamicIslandExtrasMenu.swift +++ b/DynamicIsland/components/Notch/DynamicIslandExtrasMenu.swift @@ -2,10 +2,6 @@ // DynamicIslandExtrasMenu.swift // DynamicIsland // -// Created by Harsh Vardhan Goswami on 02/08/24. -// DynamicIslandExtrasMenu.swift -// DynamicIsland -// // Created by Harsh Vardhan Goswami on 04/08/24. // @@ -65,7 +61,9 @@ struct DynamicIslandExtrasMenu : View { } var settings: some View { - SettingsLink(label: { + Button(action: { + SettingsWindowController.shared.showWindow() + }) { ZStack { RoundedRectangle(cornerRadius: 12.0).fill(.black).frame(width: 70, height: 70) VStack(spacing: 8) { @@ -74,7 +72,7 @@ struct DynamicIslandExtrasMenu : View { Text("Settings").font(.body) } } - }) + } .buttonStyle(PlainButtonStyle()).shadow(color: .black.opacity(0.5), radius: 10) } @@ -107,5 +105,5 @@ struct DynamicIslandExtrasMenu : View { #Preview { - DynamicIslandExtrasMenu(vm: .init()) + DynamicIslandExtrasMenu(vm: DynamicIslandViewModel()) } diff --git a/DynamicIsland/components/Notch/DynamicIslandHeader.swift b/DynamicIsland/components/Notch/DynamicIslandHeader.swift index 4c1177c..ed6fd55 100644 --- a/DynamicIsland/components/Notch/DynamicIslandHeader.swift +++ b/DynamicIsland/components/Notch/DynamicIslandHeader.swift @@ -10,6 +10,7 @@ import SwiftUI struct DynamicIslandHeader: View { @EnvironmentObject var vm: DynamicIslandViewModel + @EnvironmentObject var webcamManager: WebcamManager @ObservedObject var batteryModel = BatteryStatusViewModel.shared @ObservedObject var coordinator = DynamicIslandViewCoordinator.shared @StateObject var tvm = TrayDrop.shared @@ -40,8 +41,30 @@ struct DynamicIslandHeader: View { HStack(spacing: 4) { if vm.notchState == .open { + if Defaults[.showMirror] { + Button(action: { + if webcamManager.isSessionRunning { + webcamManager.stopSession() + } else { + webcamManager.startSession() + } + }) { + Capsule() + .fill(.black) + .frame(width: 30, height: 30) + .overlay { + Image(systemName: "web.camera") + .foregroundColor(.white) + .padding() + .imageScale(.medium) + } + } + .buttonStyle(PlainButtonStyle()) + } if Defaults[.settingsIconInNotch] { - SettingsLink(label: { + Button(action: { + SettingsWindowController.shared.showWindow() + }) { Capsule() .fill(.black) .frame(width: 30, height: 30) @@ -51,7 +74,7 @@ struct DynamicIslandHeader: View { .padding() .imageScale(.medium) } - }) + } .buttonStyle(PlainButtonStyle()) } if Defaults[.showBatteryIndicator] { @@ -81,5 +104,7 @@ struct DynamicIslandHeader: View { } #Preview { - DynamicIslandHeader().environmentObject(DynamicIslandViewModel()) + DynamicIslandHeader() + .environmentObject(DynamicIslandViewModel()) + .environmentObject(WebcamManager.shared) } diff --git a/DynamicIsland/components/Notch/NotchHomeView.swift b/DynamicIsland/components/Notch/NotchHomeView.swift index 781e56e..aeeaf64 100644 --- a/DynamicIsland/components/Notch/NotchHomeView.swift +++ b/DynamicIsland/components/Notch/NotchHomeView.swift @@ -3,7 +3,7 @@ // DynamicIsland // // Created by Hugo Persson on 2024-08-18. -// Modified by Harsh Vardhan Goswami & Richard Kunkli +// Modified by Harsh Vardhan Goswami & Richard Kunkli & Mustafa Ramadan // import Combine @@ -179,11 +179,11 @@ struct MusicControlsView: View { struct NotchHomeView: View { @EnvironmentObject var vm: DynamicIslandViewModel - @EnvironmentObject var webcamManager: WebcamManager + @ObservedObject var webcamManager = WebcamManager.shared @ObservedObject var batteryModel = BatteryStatusViewModel.shared @ObservedObject var coordinator = DynamicIslandViewCoordinator.shared let albumArtNamespace: Namespace.ID - + var body: some View { Group { if !coordinator.firstLaunch { @@ -196,16 +196,18 @@ struct NotchHomeView: View { private var mainContent: some View { HStack(alignment: .top, spacing: 20) { MusicPlayerView(albumArtNamespace: albumArtNamespace) - + if Defaults[.showCalendar] { CalendarView() - .onHover { isHovering in - vm.isHoveringCalendar = isHovering - } - .environmentObject(vm) + .onHover { isHovering in + vm.isHoveringCalendar = isHovering + } + .environmentObject(vm) } - - if Defaults[.showMirror] && webcamManager.cameraAvailable { + + if Defaults[.showMirror], + webcamManager.cameraAvailable, + vm.notchState == .open { CameraPreviewView(webcamManager: webcamManager) .scaledToFit() .opacity(vm.notchState == .closed ? 0 : 1) @@ -249,7 +251,7 @@ struct MusicSliderView: View { range: 0 ... duration, color: Defaults[.sliderColor] == SliderColorEnum.albumArt ? Color( nsColor: color - ).ensureMinimumBrightness(factor: 0.8) : Defaults[.sliderColor] == SliderColorEnum.accent ? Defaults[.accentColor] : .white, + ).ensureMinimumBrightness(factor: 0.8) : Defaults[.sliderColor] == SliderColorEnum.accent ? .accentColor : .white, dragging: $dragging, lastDragged: $lastDragged, onValueChange: onValueChange @@ -336,7 +338,8 @@ struct CustomSlider: View { } #Preview { - NotchHomeView(albumArtNamespace: Namespace().wrappedValue) - .environmentObject(DynamicIslandViewModel()) - .environmentObject(WebcamManager()) + NotchHomeView( + albumArtNamespace: Namespace().wrappedValue + ) + .environmentObject(DynamicIslandViewModel()) } diff --git a/DynamicIsland/components/Notch/NotchShape.swift b/DynamicIsland/components/Notch/NotchShape.swift index d1950e5..55997b1 100644 --- a/DynamicIsland/components/Notch/NotchShape.swift +++ b/DynamicIsland/components/Notch/NotchShape.swift @@ -4,7 +4,7 @@ // // Created by Kai Azim on 2023-08-24. // Original source: https://github.com/MrKai77/DynamicNotchKit -// Modified by Alexander Greco on 2025-05-18. +// Modified by Alexander on 2025-05-18. import SwiftUI diff --git a/DynamicIsland/components/Settings/SettingsView.swift b/DynamicIsland/components/Settings/SettingsView.swift index 4965ba7..a45fbbd 100644 --- a/DynamicIsland/components/Settings/SettingsView.swift +++ b/DynamicIsland/components/Settings/SettingsView.swift @@ -16,95 +16,120 @@ import SwiftUI import SwiftUIIntrospect struct SettingsView: View { - @Environment(\.scenePhase) private var scenePhase @StateObject var extensionManager = DynamicIslandExtensionManager() - let updaterController: SPUStandardUpdaterController @StateObject private var calendarManager = CalendarManager() @State private var selectedTab = "General" + let updaterController: SPUStandardUpdaterController? + + init(updaterController: SPUStandardUpdaterController? = nil) { + self.updaterController = updaterController + } + var body: some View { NavigationSplitView { List(selection: $selectedTab) { - NavigationLink(destination: GeneralSettings()) { + NavigationLink(value: "General") { Label("General", systemImage: "gear") } - NavigationLink(destination: Appearance()) { + NavigationLink(value: "Appearance") { Label("Appearance", systemImage: "eye") } - NavigationLink(destination: Media()) { + NavigationLink(value: "Media") { Label("Media", systemImage: "play.laptopcomputer") } - NavigationLink(destination: CalendarSettings()) { + NavigationLink(value: "Calendar") { Label("Calendar", systemImage: "calendar") } if extensionManager.installedExtensions - .contains( - where: { $0.bundleIdentifier == hudExtension - }) - { - NavigationLink(destination: HUD()) { + .contains(where: { $0.bundleIdentifier == hudExtension }) { + NavigationLink(value: "HUD") { Label("HUDs", systemImage: "dial.medium.fill") } } - NavigationLink(destination: Charge()) { + NavigationLink(value: "Battery") { Label("Battery", systemImage: "battery.100.bolt") } if extensionManager.installedExtensions - .contains( - where: { $0.bundleIdentifier == downloadManagerExtension - }) - { - NavigationLink(destination: Downloads()) { + .contains(where: { $0.bundleIdentifier == downloadManagerExtension }) { + NavigationLink(value: "Downloads") { Label("Downloads", systemImage: "square.and.arrow.down") } } - NavigationLink(destination: Shelf()) { + NavigationLink(value: "Shelf") { Label("Shelf", systemImage: "books.vertical") } - NavigationLink(destination: Shortcuts()) { + NavigationLink(value: "Shortcuts") { Label("Shortcuts", systemImage: "keyboard") } - NavigationLink(destination: Extensions()) { + NavigationLink(value: "Extensions") { Label("Extensions", systemImage: "puzzlepiece.extension") } - NavigationLink( - destination: About(updaterController: updaterController) - ) { + NavigationLink(value: "About") { Label("About", systemImage: "info.circle") } } - .tint(Defaults[.accentColor]) + .listStyle(SidebarListStyle()) + .toolbar(removing: .sidebarToggle) + .navigationSplitViewColumnWidth(200) } detail: { - GeneralSettings() + Group { + switch selectedTab { + case "General": + GeneralSettings() + case "Appearance": + Appearance() + case "Media": + Media() + case "Calendar": + CalendarSettings() + case "HUD": + HUD() + case "Battery": + Charge() + case "Downloads": + Downloads() + case "Shelf": + Shelf() + case "Shortcuts": + Shortcuts() + case "Extensions": + Extensions() + case "About": + if let controller = updaterController { + About(updaterController: controller) + } else { + // Fallback with a default controller + About(updaterController: SPUStandardUpdaterController(startingUpdater: false, updaterDelegate: nil, userDriverDelegate: nil)) + } + default: + GeneralSettings() + } + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + } + .navigationSplitViewStyle(.balanced) + .toolbar(removing: .sidebarToggle) + .toolbar { + Button("") {} // Empty label, does nothing + .controlSize(.extraLarge) + .opacity(0) // Invisible, but reserves space for a consistent look between tabs + .disabled(true) } .environmentObject(extensionManager) .environmentObject(calendarManager) .formStyle(.grouped) - .onChange(of: scenePhase) { - if scenePhase == .active { - NSApp.setActivationPolicy(.regular) - NSApp.activate(ignoringOtherApps: true) - NSApp.keyWindow?.makeKeyAndOrderFront(nil) - } - } - .onReceive(NotificationCenter.default.publisher(for: NSApplication.willResignActiveNotification)) { _ in - NSApp.setActivationPolicy(.accessory) - NSApp.deactivate() - } - .introspect(.window, on: .macOS(.v14, .v15)) { window in - window.toolbarStyle = .unified - window.styleMask.update(with: .resizable) - } + .frame(width: 700) + .background(Color(NSColor.windowBackgroundColor)) } } struct GeneralSettings: View { @State private var screens: [String] = NSScreen.screens.compactMap { $0.localizedName } - let accentColors: [Color] = [.blue, .purple, .pink, .red, .orange, .yellow, .green, .gray] @EnvironmentObject var vm: DynamicIslandViewModel @ObservedObject var coordinator = DynamicIslandViewCoordinator.shared - @Default(.accentColor) var accentColor + @Default(.mirrorShape) var mirrorShape @Default(.showEmojis) var showEmojis @Default(.gestureSensitivity) var gestureSensitivity @@ -121,38 +146,6 @@ struct GeneralSettings: View { var body: some View { Form { - Section { - HStack { - ForEach(accentColors, id: \.self) { color in - Button(action: { - withAnimation { - Defaults[.accentColor] = color - } - }) { - Circle() - .fill(color) - .frame(width: 20, height: 20) - .overlay( - Circle() - .stroke(Color.white, lineWidth: Defaults[.accentColor] == color ? 2 : 0) - .overlay { - Circle() - .fill(.white) - .frame(width: 7, height: 7) - .opacity(Defaults[.accentColor] == color ? 1 : 0) - } - ) - } - .buttonStyle(PlainButtonStyle()) - } - Spacer() - ColorPicker("Custom color", selection: $accentColor) - .labelsHidden() - } - } header: { - Text("Accent color") - } - Section { Defaults.Toggle("Menubar icon", key: .menubarIcon) LaunchAtLogin.Toggle("Launch at login") @@ -244,7 +237,6 @@ struct GeneralSettings: View { gestureControls() } - .tint(Defaults[.accentColor]) .toolbar { Button("Quit app") { NSApp.terminate(self) @@ -293,7 +285,7 @@ struct GeneralSettings: View { @ViewBuilder func NotchBehaviour() -> some View { Section { - Defaults.Toggle("Extend hover area", key: .extendHoverArea) + Defaults.Toggle("Extend hover area", key: .extendHoverArea) Defaults.Toggle("Enable haptics", key: .enableHaptics) Defaults.Toggle("Open notch on hover", key: .openNotchOnHover) Toggle("Remember last tab", isOn: $coordinator.openLastTabByDefault) @@ -332,7 +324,6 @@ struct Charge: View { Text("Battery Information") } } - .tint(Defaults[.accentColor]) .navigationTitle("Battery") } } @@ -407,7 +398,6 @@ struct Downloads: View { } } } - .tint(Defaults[.accentColor]) .navigationTitle("Downloads") } } @@ -453,7 +443,6 @@ struct HUD: View { } } } - .tint(Defaults[.accentColor]) .navigationTitle("HUDs") } } @@ -537,7 +526,6 @@ struct Media: View { Defaults[.enableFullscreenMediaDetection] = newValue != .never } } - .tint(Defaults[.accentColor]) .navigationTitle("Media") } @@ -571,11 +559,13 @@ struct CalendarSettings: View { Toggle("Show calendar", isOn: $showCalendar) Section(header: Text("Select Calendars")) { List { - ForEach(calendarManager.allCalendars, id: \.calendarIdentifier) { calendar in + ForEach(calendarManager.allCalendars, id: \.id) { calendar in Toggle(isOn: Binding( get: { calendarManager.getCalendarSelected(calendar) }, set: { isSelected in - calendarManager.setCalendarSelected(calendar, isSelected: isSelected) + Task { + await calendarManager.setCalendarSelected(calendar, isSelected: isSelected) + } } )) { Text(calendar.title) @@ -586,8 +576,12 @@ struct CalendarSettings: View { } } .onAppear { - calendarManager.checkCalendarAuthorization() + Task { + await calendarManager.checkCalendarAuthorization() + } } + // Add navigation title if it's missing or adjust as needed + .navigationTitle("Calendar") } } @@ -659,7 +653,7 @@ struct About: View { } VStack(spacing: 0) { Divider() - Text("Made with 🫢🏻 by the DynamicIsland team") + Text("Made with 🫢🏻 by not so boring not.people") .foregroundStyle(.secondary) .padding(.top, 5) .padding(.bottom, 7) @@ -669,13 +663,12 @@ struct About: View { .frame(maxWidth: .infinity, alignment: .center) } .toolbar { - Button("Welcome window") { - openWindow(id: "onboarding") - } - .controlSize(.extraLarge) +// Button("Welcome window") { +// openWindow(id: "onboarding") +// } +// .controlSize(.extraLarge) CheckForUpdatesView(updater: updaterController.updater) } - .tint(Defaults[.accentColor]) .navigationTitle("About") } } @@ -692,7 +685,6 @@ struct Shelf: View { } } } - .tint(Defaults[.accentColor]) .navigationTitle("Shelf") } } @@ -810,7 +802,6 @@ struct Extensions: View { } } } - .tint(Defaults[.accentColor]) .navigationTitle("Extensions") // TipsView() // .padding(.horizontal, 19) @@ -913,7 +904,7 @@ struct Appearance: View { .buttonStyle(PlainButtonStyle()) .padding(.vertical, 2) .background( - selectedListVisualizer != nil ? selectedListVisualizer == visualizer ? Defaults[.accentColor] : Color.clear : Color.clear, + selectedListVisualizer != nil ? selectedListVisualizer == visualizer ? Color.accentColor : Color.clear : Color.clear, in: RoundedRectangle(cornerRadius: 5) ) .contentShape(Rectangle()) @@ -1027,7 +1018,7 @@ struct Appearance: View { } Section { - Defaults.Toggle("Enable dynamic mirror", key: .showMirror) + Defaults.Toggle("Enable boring mirror", key: .showMirror) .disabled(!checkVideoInput()) Picker("Mirror shape", selection: $mirrorShape) { Text("Circle") @@ -1053,7 +1044,7 @@ struct Appearance: View { .background( RoundedRectangle(cornerRadius: 20, style: .circular) .strokeBorder( - icon == selectedIcon ? Defaults[.accentColor] : .clear, + icon == selectedIcon ? Color.accentColor : .clear, lineWidth: 2.5 ) ) @@ -1066,7 +1057,7 @@ struct Appearance: View { .padding(.vertical, 3) .background( Capsule() - .fill(icon == selectedIcon ? Defaults[.accentColor] : .clear) + .fill(icon == selectedIcon ? Color.accentColor : .clear) ) } .onTapGesture { @@ -1086,7 +1077,6 @@ struct Appearance: View { } } } - .tint(Defaults[.accentColor]) .navigationTitle("Appearance") } @@ -1116,7 +1106,6 @@ struct Shortcuts: View { KeyboardShortcuts.Recorder("Toggle Notch Open:", name: .toggleNotchOpen) } } - .tint(Defaults[.accentColor]) .navigationTitle("Shortcuts") } } diff --git a/DynamicIsland/components/Settings/SettingsWindowController.swift b/DynamicIsland/components/Settings/SettingsWindowController.swift new file mode 100644 index 0000000..c7e99da --- /dev/null +++ b/DynamicIsland/components/Settings/SettingsWindowController.swift @@ -0,0 +1,125 @@ +// +// SettingsWindowController.swift +// DynamicIsland +// +// Created by Alexander on 2025-06-14. +// + +import AppKit +import SwiftUI +import Defaults +import Sparkle + +class SettingsWindowController: NSWindowController { + static let shared = SettingsWindowController() + private var updaterController: SPUStandardUpdaterController? + + private init() { + let window = NSWindow( + contentRect: NSRect(x: 0, y: 0, width: 700, height: 600), + styleMask: [.titled, .closable, .miniaturizable, .resizable, .fullSizeContentView], + backing: .buffered, + defer: false + ) + + super.init(window: window) + + setupWindow() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func setUpdaterController(_ controller: SPUStandardUpdaterController) { + self.updaterController = controller + // Recreate the content view with the proper updater controller + setupWindow() + } + + private func setupWindow() { + guard let window = window else { return } + + window.title = "Dynamic Island Settings" + window.titlebarAppearsTransparent = false + window.titleVisibility = .visible + window.toolbarStyle = .unified + window.isMovableByWindowBackground = true + + // Make it behave like a regular app window with proper Spaces support + window.collectionBehavior = [.managed, .participatesInCycle, .fullScreenAuxiliary] + + // Ensure proper window behavior + window.hidesOnDeactivate = false + window.isExcludedFromWindowsMenu = false + + // Configure window to be a standard document-style window + window.isRestorable = true + window.identifier = NSUserInterfaceItemIdentifier("DynamicIslandSettingsWindow") + + // Create the SwiftUI content + let settingsView = SettingsView(updaterController: updaterController) + let hostingView = NSHostingView(rootView: settingsView) + window.contentView = hostingView + + // Handle window closing + window.delegate = self + } + + func showWindow() { + // Set app to regular mode first + NSApp.setActivationPolicy(.regular) + + // If window is already visible, bring it to front properly + if window?.isVisible == true { + NSApp.activate(ignoringOtherApps: true) + window?.orderFrontRegardless() + window?.makeKeyAndOrderFront(nil) + return + } + + // Show the window with proper ordering + window?.orderFrontRegardless() + window?.makeKeyAndOrderFront(nil) + window?.center() + + // Activate the app and ensure window gets focus + NSApp.activate(ignoringOtherApps: true) + + // Force window to front after activation + DispatchQueue.main.async { [weak self] in + self?.window?.makeKeyAndOrderFront(nil) + } + } + + override func close() { + super.close() + relinquishFocus() + } + + private func relinquishFocus() { + window?.orderOut(nil) + + // Set app back to accessory mode immediately + NSApp.setActivationPolicy(.accessory) + } +} + +extension SettingsWindowController: NSWindowDelegate { + func windowWillClose(_ notification: Notification) { + relinquishFocus() + } + + func windowShouldClose(_ sender: NSWindow) -> Bool { + return true + } + + func windowDidBecomeKey(_ notification: Notification) { + // Ensure app is in regular mode when window becomes key + NSApp.setActivationPolicy(.regular) + } + + func windowDidResignKey(_ notification: Notification) { + } + +} diff --git a/DynamicIsland/components/Webcam/WebcamView.swift b/DynamicIsland/components/Webcam/WebcamView.swift index eea1462..ca1d821 100644 --- a/DynamicIsland/components/Webcam/WebcamView.swift +++ b/DynamicIsland/components/Webcam/WebcamView.swift @@ -118,5 +118,5 @@ struct CameraPreviewLayerView: NSViewRepresentable { } #Preview { - CameraPreviewView(webcamManager: WebcamManager()) + CameraPreviewView(webcamManager: WebcamManager.shared) } diff --git a/DynamicIsland/helpers/AppleScriptHelper.swift b/DynamicIsland/helpers/AppleScriptHelper.swift index 719a5b3..50312ed 100644 --- a/DynamicIsland/helpers/AppleScriptHelper.swift +++ b/DynamicIsland/helpers/AppleScriptHelper.swift @@ -2,7 +2,7 @@ // AppleScriptHelper.swift // DynamicIsland // -// Created by Alexander Greco on 2025-03-29. +// Created by Alexander on 2025-03-29. // import Foundation diff --git a/DynamicIsland/managers/CalendarManager.swift b/DynamicIsland/managers/CalendarManager.swift index 821fe85..7afc23e 100644 --- a/DynamicIsland/managers/CalendarManager.swift +++ b/DynamicIsland/managers/CalendarManager.swift @@ -1,5 +1,5 @@ // -// CalenderManager.swift +// CalendarManager.swift // DynamicIsland // // Created by Harsh Vardhan Goswami on 08/09/24. @@ -11,39 +11,37 @@ import Defaults // MARK: - CalendarManager +@MainActor class CalendarManager: ObservableObject { @Published var currentWeekStartDate: Date - @Published var events: [EKEvent] = [] - @Published var allCalendars: [EKCalendar] = [] - private var selectedCalendars: [EKCalendar] = [] - private let eventStore = EKEventStore() + @Published var events: [EventModel] = [] + @Published var allCalendars: [CalendarModel] = [] @Published var authorizationStatus: EKAuthorizationStatus = .notDetermined - + private var selectedCalendars: [CalendarModel] = [] + private let calendarService = CalendarService() + init() { self.currentWeekStartDate = CalendarManager.startOfDay(Date()) - if(Defaults[.showCalendar]) { - checkCalendarAuthorization() - } } - - func checkCalendarAuthorization() { + + func checkCalendarAuthorization() async { let status = EKEventStore.authorizationStatus(for: .event) DispatchQueue.main.async { print("πŸ“… Current calendar authorization status: \(status)") self.authorizationStatus = status } - + switch status { case .notDetermined: - requestCalendarAccess() + self.authorizationStatus = await calendarService.requestAccess() ? .fullAccess : .denied case .restricted, .denied: // Handle the case where the user has denied or restricted access NSLog("Calendar access denied or restricted") case .fullAccess: NSLog("Full access") - self.allCalendars = eventStore.calendars(for: .event) + allCalendars = await calendarService.calendars() updateSelectedCalendars() - fetchEvents() + events = await calendarService.events(from: currentWeekStartDate, to: Calendar.current.date(byAdding: .day, value: 1, to: currentWeekStartDate)!, calendars: selectedCalendars.map{$0.id}) case .writeOnly: NSLog("Write only") @unknown default: @@ -51,86 +49,50 @@ class CalendarManager: ObservableObject { } } - func requestCalendarAccess() { - eventStore.requestFullAccessToEvents { [weak self] granted, error in - DispatchQueue.main.async { - if let error = error { - print("πŸ“… Calendar access error: \(error.localizedDescription)") - } - - self?.authorizationStatus = granted ? .fullAccess : .denied - if granted { - print("πŸ“… Calendar access granted") - self?.fetchEvents() - } else { - print("πŸ“… Calendar access denied") - } - } - } - } - func updateSelectedCalendars() { selectedCalendars = allCalendars.filter { getCalendarSelected($0) } } - func getCalendarSelected(_ calendar: EKCalendar) -> Bool { + func getCalendarSelected(_ calendar: CalendarModel) -> Bool { switch Defaults[.calendarSelectionState] { case .all: return true case .selected(let identifiers): - return identifiers.contains(calendar.calendarIdentifier) + return identifiers.contains(calendar.id) } } - func setCalendarSelected(_ calendar: EKCalendar, isSelected: Bool) { + func setCalendarSelected(_ calendar: CalendarModel, isSelected: Bool) async { var selectionState = Defaults[.calendarSelectionState] switch selectionState { case .all: if !isSelected { - let identifiers = Set(allCalendars.map { $0.calendarIdentifier }).subtracting([calendar.calendarIdentifier]) + let identifiers = Set(allCalendars.map { $0.id }).subtracting([calendar.id]) selectionState = .selected(identifiers) } case .selected(var identifiers): if isSelected { - identifiers.insert(calendar.calendarIdentifier) + identifiers.insert(calendar.id) } else { - identifiers.remove(calendar.calendarIdentifier) + identifiers.remove(calendar.id) } - selectionState = identifiers.count == allCalendars.count ? .all : .selected(identifiers) + selectionState = identifiers.isEmpty ? .all : identifiers.count == allCalendars.count ? .all : .selected(identifiers) // if empty, select all } Defaults[.calendarSelectionState] = selectionState - fetchEvents() - } - - func fetchEvents() { - guard !self.selectedCalendars.isEmpty else { - DispatchQueue.main.async { - self.events = [] - } - return - } - - let endOfWeek = Calendar.current.date(byAdding: .day, value: 1, to: currentWeekStartDate)! - let predicate = eventStore.predicateForEvents(withStart: currentWeekStartDate, end: endOfWeek, calendars: self.selectedCalendars) - let fetchedEvents = eventStore.events(matching: predicate) - - DispatchQueue.main.async { - self.events = fetchedEvents.sorted { $0.startDate < $1.startDate } - print("πŸ“… Fetched \(self.events.count) calendar events") - } + updateSelectedCalendars() + events = await calendarService.events(from: currentWeekStartDate, to: Calendar.current.date(byAdding: .day, value: 1, to: currentWeekStartDate)!, calendars: selectedCalendars.map{$0.id}) } static func startOfDay(_ date: Date) -> Date { return Calendar.current.startOfDay(for: date) } - func updateCurrentDate(_ date: Date) { - print("πŸ“… Updating current date to: \(date)") + func updateCurrentDate(_ date: Date) async { currentWeekStartDate = Calendar.current.startOfDay(for: date) - fetchEvents() + events = await calendarService.events(from: currentWeekStartDate, to: Calendar.current.date(byAdding: .day, value: 1, to: currentWeekStartDate)!, calendars: selectedCalendars.map{$0.id}) } } diff --git a/DynamicIsland/managers/WebcamManager.swift b/DynamicIsland/managers/WebcamManager.swift index cee5aee..ea4fc83 100644 --- a/DynamicIsland/managers/WebcamManager.swift +++ b/DynamicIsland/managers/WebcamManager.swift @@ -8,6 +8,8 @@ import AVFoundation import SwiftUI class WebcamManager: NSObject, ObservableObject { + static let shared = WebcamManager() + @Published var previewLayer: AVCaptureVideoPreviewLayer? { didSet { objectWillChange.send() @@ -58,9 +60,8 @@ class WebcamManager: NSObject, ObservableObject { // MARK: - Properties - override init() { + private override init() { super.init() - checkAndRequestVideoAuthorization() NotificationCenter.default.addObserver(self, selector: #selector(deviceWasDisconnected), name: .AVCaptureDeviceWasDisconnected, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(deviceWasConnected), name: .AVCaptureDeviceWasConnected, object: nil) checkCameraAvailability() diff --git a/DynamicIsland/models/CalendarModel.swift b/DynamicIsland/models/CalendarModel.swift new file mode 100644 index 0000000..26418fc --- /dev/null +++ b/DynamicIsland/models/CalendarModel.swift @@ -0,0 +1,17 @@ +// +// CalendarModel.swift +// Calendr +// +// Created by Paker on 31/12/20. +// Original source: https://github.com/pakerwreah/Calendr +// + +import Cocoa + +struct CalendarModel: Equatable { + let id: String + let account: String + let title: String + let color: NSColor + let isSubscribed: Bool +} diff --git a/DynamicIsland/models/EventModel.swift b/DynamicIsland/models/EventModel.swift new file mode 100644 index 0000000..40e7574 --- /dev/null +++ b/DynamicIsland/models/EventModel.swift @@ -0,0 +1,125 @@ +// +// EventModel.swift +// Calendr +// +// Created by Paker on 24/12/20. +// Original source: https://github.com/pakerwreah/Calendr +// Modified by Alexander on 2025-05-18. +// + +import Foundation + +struct EventModel: Equatable { + let id: String + let start: Date + let end: Date + let title: String + let location: String? + let notes: String? + let url: URL? + let isAllDay: Bool + let type: EventType + let calendar: CalendarModel + let participants: [Participant] + let timeZone: TimeZone? + let hasRecurrenceRules: Bool + let priority: Priority? +} + +enum AttendanceStatus: Comparable { + case accepted + case maybe + case pending + case declined + case unknown + + private var comparisonValue: Int { + switch self { + case .accepted: return 1 + case .maybe: return 2 + case .declined: return 3 + case .pending: return 4 + case .unknown: return 5 + } + } + + static func < (lhs: Self, rhs: Self) -> Bool { + return lhs.comparisonValue < rhs.comparisonValue + } +} + +enum EventType: Equatable { + case event(AttendanceStatus) + case birthday + case reminder(completed: Bool) +} + +enum EventStatus: Equatable { + case upcoming + case inProgress + case ended +} + +extension EventType { + var isEvent: Bool { if case .event = self { return true } else { return false } } + var isBirthday: Bool { self ~= .birthday } + var isReminder: Bool { if case .reminder = self { return true } else { return false } } +} + +extension EventModel { + + var eventStatus: EventStatus { + if start > Date() { + return .upcoming + } else if end > Date() { + return .inProgress + } else { + return .ended + } + } + + var attendance: AttendanceStatus { if case .event(let attendance) = type { return attendance } else { return .unknown } } + + var isMeeting: Bool { !participants.isEmpty } + + func calendarAppURL() -> URL? { + + guard let id = id.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) else { + return nil + } + + guard !type.isReminder else { + return URL(string: "x-apple-reminderkit://remcdreminder/\(id)") + } + + let date: String + if hasRecurrenceRules { + let formatter = DateFormatter() + formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZ" + if !isAllDay { + formatter.timeZone = .init(secondsFromGMT: 0) + } + if let formattedDate = formatter.string(for: start) { + date = "/\(formattedDate)" + } else { + return nil + } + } else { + date = "" + } + return URL(string: "ical://ekevent\(date)/\(id)?method=show&options=more") + } +} + +struct Participant: Hashable { + let name: String + let status: AttendanceStatus + let isOrganizer: Bool + let isCurrentUser: Bool +} + +enum Priority { + case high + case medium + case low +}