mirror of
https://github.com/FBukovina/Atoll.git
synced 2026-10-27 15:14:33 +00:00
Applied latest changes from boring.notch
This commit is contained in:
parent
bad481588b
commit
628d76b41c
18 changed files with 940 additions and 339 deletions
|
|
@ -15,7 +15,7 @@ import SwiftUIIntrospect
|
||||||
|
|
||||||
struct ContentView: View {
|
struct ContentView: View {
|
||||||
@EnvironmentObject var vm: DynamicIslandViewModel
|
@EnvironmentObject var vm: DynamicIslandViewModel
|
||||||
@StateObject var webcamManager: WebcamManager = .init()
|
@EnvironmentObject var webcamManager: WebcamManager
|
||||||
|
|
||||||
@ObservedObject var coordinator = DynamicIslandViewCoordinator.shared
|
@ObservedObject var coordinator = DynamicIslandViewCoordinator.shared
|
||||||
@ObservedObject var musicManager = MusicManager.shared
|
@ObservedObject var musicManager = MusicManager.shared
|
||||||
|
|
|
||||||
|
|
@ -7,27 +7,32 @@
|
||||||
|
|
||||||
import AVFoundation
|
import AVFoundation
|
||||||
import Combine
|
import Combine
|
||||||
|
import Defaults
|
||||||
import KeyboardShortcuts
|
import KeyboardShortcuts
|
||||||
import Sparkle
|
import Sparkle
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import Defaults
|
|
||||||
|
|
||||||
@main
|
@main
|
||||||
struct DynamicNotchApp: App {
|
struct DynamicNotchApp: App {
|
||||||
@NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
|
@NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
|
||||||
@Default(.menubarIcon) var showMenuBarIcon
|
@Default(.menubarIcon) var showMenuBarIcon
|
||||||
@Environment(\.openWindow) var openWindow
|
@Environment(\.openWindow) var openWindow
|
||||||
|
|
||||||
let updaterController: SPUStandardUpdaterController
|
let updaterController: SPUStandardUpdaterController
|
||||||
|
|
||||||
init() {
|
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 {
|
var body: some Scene {
|
||||||
MenuBarExtra("dynamic.island", systemImage: "sparkle", isInserted: $showMenuBarIcon) {
|
MenuBarExtra("dynamic.island", systemImage: "sparkle", isInserted: $showMenuBarIcon) {
|
||||||
SettingsLink(label: {
|
Button("Settings") {
|
||||||
Text("Settings")
|
SettingsWindowController.shared.showWindow()
|
||||||
})
|
}
|
||||||
.keyboardShortcut(KeyEquivalent(","), modifiers: .command)
|
.keyboardShortcut(KeyEquivalent(","), modifiers: .command)
|
||||||
if false {
|
if false {
|
||||||
Button("Activate License") {
|
Button("Activate License") {
|
||||||
|
|
@ -37,37 +42,27 @@ struct DynamicNotchApp: App {
|
||||||
CheckForUpdatesView(updater: updaterController.updater)
|
CheckForUpdatesView(updater: updaterController.updater)
|
||||||
Divider()
|
Divider()
|
||||||
Button("Restart Dynamic Island") {
|
Button("Restart Dynamic Island") {
|
||||||
guard let bundleIdentifier = Bundle.main.bundleIdentifier else { return }
|
guard let bundleIdentifier = Bundle.main.bundleIdentifier else { return }
|
||||||
|
|
||||||
let workspace = NSWorkspace.shared
|
let workspace = NSWorkspace.shared
|
||||||
|
|
||||||
if let appURL = workspace.urlForApplication(withBundleIdentifier: bundleIdentifier) {
|
if let appURL = workspace.urlForApplication(withBundleIdentifier: bundleIdentifier)
|
||||||
|
{
|
||||||
let configuration = NSWorkspace.OpenConfiguration()
|
|
||||||
configuration.createsNewApplicationInstance = true
|
let configuration = NSWorkspace.OpenConfiguration()
|
||||||
|
configuration.createsNewApplicationInstance = true
|
||||||
workspace.openApplication(at: appURL, configuration: configuration)
|
|
||||||
}
|
workspace.openApplication(at: appURL, configuration: configuration)
|
||||||
|
}
|
||||||
NSApplication.shared.terminate(nil)
|
|
||||||
|
NSApplication.shared.terminate(nil)
|
||||||
}
|
}
|
||||||
Button("Quit", role: .destructive) {
|
Button("Quit", role: .destructive) {
|
||||||
NSApp.terminate(nil)
|
NSApp.terminate(nil)
|
||||||
}
|
}
|
||||||
.keyboardShortcut(KeyEquivalent("Q"), modifiers: .command)
|
.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") {
|
Window("Activation", id: "activation") {
|
||||||
ActivationWindow()
|
ActivationWindow()
|
||||||
}
|
}
|
||||||
|
|
@ -85,11 +80,12 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
||||||
@ObservedObject var coordinator = DynamicIslandViewCoordinator.shared
|
@ObservedObject var coordinator = DynamicIslandViewCoordinator.shared
|
||||||
var whatsNewWindow: NSWindow?
|
var whatsNewWindow: NSWindow?
|
||||||
var timer: Timer?
|
var timer: Timer?
|
||||||
let calenderManager = CalendarManager()
|
let calendarManager = CalendarManager()
|
||||||
|
let webcamManager = WebcamManager.shared
|
||||||
var closeNotchWorkItem: DispatchWorkItem?
|
var closeNotchWorkItem: DispatchWorkItem?
|
||||||
private var previousScreens: [NSScreen]?
|
private var previousScreens: [NSScreen]?
|
||||||
@Environment(\.openWindow) var openWindow
|
private var onboardingWindowController: NSWindowController?
|
||||||
|
|
||||||
func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
|
func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
@ -125,10 +121,13 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
||||||
self.window = nil
|
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(
|
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],
|
styleMask: [.borderless, .nonactivatingPanel, .utilityWindow, .hudWindow],
|
||||||
backing: .buffered,
|
backing: .buffered,
|
||||||
defer: false
|
defer: false
|
||||||
|
|
@ -137,14 +136,16 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
||||||
window.contentView = NSHostingView(
|
window.contentView = NSHostingView(
|
||||||
rootView: ContentView()
|
rootView: ContentView()
|
||||||
.environmentObject(viewModel)
|
.environmentObject(viewModel)
|
||||||
|
.environmentObject(webcamManager)
|
||||||
)
|
)
|
||||||
|
|
||||||
window.orderFrontRegardless()
|
window.orderFrontRegardless()
|
||||||
NotchSpaceManager.shared.notchSpace.windows.insert(window)
|
NotchSpaceManager.shared.notchSpace.windows.insert(window)
|
||||||
return 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 {
|
if changeAlpha {
|
||||||
window.alphaValue = 0
|
window.alphaValue = 0
|
||||||
}
|
}
|
||||||
|
|
@ -152,45 +153,56 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
||||||
DispatchQueue.main.async { [weak window] in
|
DispatchQueue.main.async { [weak window] in
|
||||||
guard let window = window else { return }
|
guard let window = window else { return }
|
||||||
let screenFrame = screen.frame
|
let screenFrame = screen.frame
|
||||||
window.setFrameOrigin(NSPoint(
|
window.setFrameOrigin(
|
||||||
x: screenFrame.origin.x + (screenFrame.width / 2) - window.frame.width / 2,
|
NSPoint(
|
||||||
y: screenFrame.origin.y + screenFrame.height - window.frame.height
|
x: screenFrame.origin.x + (screenFrame.width / 2) - window.frame.width / 2,
|
||||||
))
|
y: screenFrame.origin.y + screenFrame.height - window.frame.height
|
||||||
|
))
|
||||||
window.alphaValue = 1
|
window.alphaValue = 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func applicationDidFinishLaunching(_ notification: Notification) {
|
func applicationDidFinishLaunching(_ notification: Notification) {
|
||||||
|
|
||||||
coordinator.setupWorkersNotificationObservers();
|
coordinator.setupWorkersNotificationObservers()
|
||||||
|
|
||||||
NotificationCenter.default.addObserver(
|
NotificationCenter.default.addObserver(
|
||||||
self,
|
self,
|
||||||
selector: #selector(screenConfigurationDidChange),
|
selector: #selector(screenConfigurationDidChange),
|
||||||
name: NSApplication.didChangeScreenParametersNotification,
|
name: NSApplication.didChangeScreenParametersNotification,
|
||||||
object: nil
|
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)
|
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()
|
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 }
|
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 }
|
guard let self = self else { return }
|
||||||
self.cleanupWindows(shouldInvert: true)
|
self.cleanupWindows(shouldInvert: true)
|
||||||
|
|
||||||
if(!Defaults[.showOnAllDisplays]) {
|
if !Defaults[.showOnAllDisplays] {
|
||||||
let viewModel = self.vm
|
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.window = window
|
||||||
self.adjustWindowPosition(changeAlpha: true)
|
self.adjustWindowPosition(changeAlpha: true)
|
||||||
} else {
|
} 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(
|
||||||
DistributedNotificationCenter.default().addObserver(self, selector: #selector(onScreenUnlocked(_:)), name: NSNotification.Name(rawValue: "com.apple.screenIsUnlocked"), object: nil)
|
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
|
KeyboardShortcuts.onKeyDown(for: .toggleSneakPeek) { [weak self] in
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
|
|
@ -216,8 +232,8 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
||||||
let mouseLocation = NSEvent.mouseLocation
|
let mouseLocation = NSEvent.mouseLocation
|
||||||
|
|
||||||
var viewModel = self.vm
|
var viewModel = self.vm
|
||||||
|
|
||||||
if(Defaults[.showOnAllDisplays]) {
|
if Defaults[.showOnAllDisplays] {
|
||||||
for screen in NSScreen.screens {
|
for screen in NSScreen.screens {
|
||||||
if screen.frame.contains(mouseLocation) {
|
if screen.frame.contains(mouseLocation) {
|
||||||
if let screenViewModel = self.viewModels[screen] {
|
if let screenViewModel = self.viewModels[screen] {
|
||||||
|
|
@ -248,7 +264,8 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
||||||
|
|
||||||
if !Defaults[.showOnAllDisplays] {
|
if !Defaults[.showOnAllDisplays] {
|
||||||
let viewModel = self.vm
|
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
|
self.window = window
|
||||||
adjustWindowPosition(changeAlpha: true)
|
adjustWindowPosition(changeAlpha: true)
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -257,7 +274,7 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
||||||
|
|
||||||
if coordinator.firstLaunch {
|
if coordinator.firstLaunch {
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
self.openWindow(id: "onboarding")
|
self.showOnboardingWindow()
|
||||||
}
|
}
|
||||||
playWelcomeSound()
|
playWelcomeSound()
|
||||||
}
|
}
|
||||||
|
|
@ -283,11 +300,13 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
||||||
|
|
||||||
@objc func screenConfigurationDidChange() {
|
@objc func screenConfigurationDidChange() {
|
||||||
let currentScreens = NSScreen.screens
|
let currentScreens = NSScreen.screens
|
||||||
|
|
||||||
let screensChanged = currentScreens.count != previousScreens?.count ||
|
let screensChanged =
|
||||||
Set(currentScreens.map { $0.localizedName }) != Set(previousScreens?.map { $0.localizedName } ?? []) ||
|
currentScreens.count != previousScreens?.count
|
||||||
Set(currentScreens.map { $0.frame }) != Set(previousScreens?.map { $0.frame } ?? [])
|
|| Set(currentScreens.map { $0.localizedName })
|
||||||
|
!= Set(previousScreens?.map { $0.localizedName } ?? [])
|
||||||
|
|| Set(currentScreens.map { $0.frame }) != Set(previousScreens?.map { $0.frame } ?? [])
|
||||||
|
|
||||||
previousScreens = currentScreens
|
previousScreens = currentScreens
|
||||||
|
|
||||||
if screensChanged {
|
if screensChanged {
|
||||||
|
|
@ -330,8 +349,10 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
let selectedScreen: NSScreen
|
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
|
coordinator.selectedScreen = coordinator.preferredScreen
|
||||||
selectedScreen = preferredScreen
|
selectedScreen = preferredScreen
|
||||||
} else if Defaults[.automaticallySwitchDisplay], let mainScreen = NSScreen.main {
|
} else if Defaults[.automaticallySwitchDisplay], let mainScreen = NSScreen.main {
|
||||||
|
|
@ -376,6 +397,42 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
||||||
@objc func quitAction() {
|
@objc func quitAction() {
|
||||||
NSApplication.shared.terminate(nil)
|
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 {
|
extension Notification.Name {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,9 @@
|
||||||
{
|
{
|
||||||
"sourceLanguage" : "en",
|
"sourceLanguage" : "en",
|
||||||
"strings" : {
|
"strings" : {
|
||||||
|
"" : {
|
||||||
|
|
||||||
|
},
|
||||||
" – %lld" : {
|
" – %lld" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
@ -55,6 +58,7 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Accent color" : {
|
"Accent color" : {
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en" : {
|
"en" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
|
|
@ -96,6 +100,9 @@
|
||||||
},
|
},
|
||||||
"All-day" : {
|
"All-day" : {
|
||||||
|
|
||||||
|
},
|
||||||
|
"Allow Access" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Always hide in fullscreen" : {
|
"Always hide in fullscreen" : {
|
||||||
|
|
||||||
|
|
@ -225,6 +232,9 @@
|
||||||
},
|
},
|
||||||
"Check for Updates…" : {
|
"Check for Updates…" : {
|
||||||
|
|
||||||
|
},
|
||||||
|
"Choose a Music Source" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Circle" : {
|
"Circle" : {
|
||||||
|
|
||||||
|
|
@ -241,8 +251,12 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"Continue" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Custom color" : {
|
"Custom color" : {
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en" : {
|
"en" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
|
|
@ -263,6 +277,9 @@
|
||||||
},
|
},
|
||||||
"Custom vizualizers (Lottie)" : {
|
"Custom vizualizers (Lottie)" : {
|
||||||
|
|
||||||
|
},
|
||||||
|
"Customize in Settings" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Default" : {
|
"Default" : {
|
||||||
|
|
||||||
|
|
@ -314,6 +331,9 @@
|
||||||
},
|
},
|
||||||
"Drop files here" : {
|
"Drop files here" : {
|
||||||
|
|
||||||
|
},
|
||||||
|
"Dynamic Island" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"dynamic.island" : {
|
"dynamic.island" : {
|
||||||
|
|
||||||
|
|
@ -382,6 +402,9 @@
|
||||||
},
|
},
|
||||||
"Extensions" : {
|
"Extensions" : {
|
||||||
|
|
||||||
|
},
|
||||||
|
"Finish" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"General" : {
|
"General" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
|
|
@ -475,7 +498,6 @@
|
||||||
|
|
||||||
},
|
},
|
||||||
"Made with 🫶🏻 by not so boring not.people" : {
|
"Made with 🫶🏻 by not so boring not.people" : {
|
||||||
"extractionState" : "stale",
|
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en" : {
|
"en" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
|
|
@ -484,9 +506,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"Made with 🫶🏻 by the DynamicIsland team" : {
|
|
||||||
|
|
||||||
},
|
},
|
||||||
"Match menubar height" : {
|
"Match menubar height" : {
|
||||||
|
|
||||||
|
|
@ -605,6 +624,9 @@
|
||||||
},
|
},
|
||||||
"Non-notch display height" : {
|
"Non-notch display height" : {
|
||||||
|
|
||||||
|
},
|
||||||
|
"Not Now" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Notch behavior" : {
|
"Notch behavior" : {
|
||||||
|
|
||||||
|
|
@ -617,9 +639,6 @@
|
||||||
},
|
},
|
||||||
"OK" : {
|
"OK" : {
|
||||||
|
|
||||||
},
|
|
||||||
"Onboarding" : {
|
|
||||||
|
|
||||||
},
|
},
|
||||||
"One or more files failed to load" : {
|
"One or more files failed to load" : {
|
||||||
|
|
||||||
|
|
@ -732,6 +751,9 @@
|
||||||
},
|
},
|
||||||
"Select Calendars" : {
|
"Select Calendars" : {
|
||||||
|
|
||||||
|
},
|
||||||
|
"Select the music source you want to use. You can change this later in the app settings." : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"selected" : {
|
"selected" : {
|
||||||
|
|
||||||
|
|
@ -901,14 +923,20 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"View on GitHub: th-ch/youtube-music" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Welcome" : {
|
"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: " : {
|
"YouTube Music requires this third-party app to be installed: " : {
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
// YouTubeMusicController.swift
|
// YouTubeMusicController.swift
|
||||||
// DynamicIsland
|
// DynamicIsland
|
||||||
//
|
//
|
||||||
// Created By Alexander Greco on 2025-03-30.
|
// Created By Alexander on 2025-03-30.
|
||||||
//
|
//
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
@ -64,7 +64,7 @@ class YouTubeMusicController: MediaControllerProtocol {
|
||||||
}
|
}
|
||||||
|
|
||||||
private func getAccessToken(completion: @escaping (Bool) -> Void) {
|
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)
|
completion(false)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -80,7 +80,7 @@ class YouTubeMusicController: MediaControllerProtocol {
|
||||||
.receive(on: DispatchQueue.main)
|
.receive(on: DispatchQueue.main)
|
||||||
.sink(receiveCompletion: { completionStatus in
|
.sink(receiveCompletion: { completionStatus in
|
||||||
switch completionStatus {
|
switch completionStatus {
|
||||||
case .failure(let error):
|
case .failure(_):
|
||||||
completion(false)
|
completion(false)
|
||||||
case .finished:
|
case .finished:
|
||||||
break
|
break
|
||||||
|
|
|
||||||
302
DynamicIsland/Providers/CalendarServiceProviding.swift
Normal file
302
DynamicIsland/Providers/CalendarServiceProviding.swift
Normal file
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -5,9 +5,9 @@
|
||||||
// Created by Harsh Vardhan Goswami on 08/09/24.
|
// Created by Harsh Vardhan Goswami on 08/09/24.
|
||||||
//
|
//
|
||||||
|
|
||||||
import EventKit
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import Defaults
|
import Defaults
|
||||||
|
import EventKit
|
||||||
|
|
||||||
struct Config: Equatable {
|
struct Config: Equatable {
|
||||||
// var count: Int = 10 // 3 days past + today + 7 days future
|
// var count: Int = 10 // 3 days past + today + 7 days future
|
||||||
|
|
@ -45,7 +45,9 @@ struct WheelPicker: View {
|
||||||
withAnimation{
|
withAnimation{
|
||||||
scrollPosition = indexForDate(date, offset: offset) - config.offset
|
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 {
|
private func dayText(date: String, isSelected: Bool) -> some View {
|
||||||
Text(date)
|
Text(date)
|
||||||
.font(.caption2)
|
.font(.caption2)
|
||||||
.foregroundStyle(
|
.foregroundStyle(isSelected ? Color.accentColor : .gray)
|
||||||
isSelected ? Defaults[.accentColor] : .gray
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private func dateCircle(date: Date, isSelected: Bool) -> some View {
|
private func dateCircle(date: Date, isSelected: Bool) -> some View {
|
||||||
ZStack {
|
ZStack {
|
||||||
Circle()
|
Circle()
|
||||||
.fill(isSelected ? Defaults[.accentColor] : .clear)
|
.fill(isSelected ? Color.accentColor : .clear)
|
||||||
.frame(width: 24, height: 24)
|
.frame(width: 24, height: 24)
|
||||||
Text("\(date.date)")
|
Text("\(date.date)")
|
||||||
.font(.title3)
|
.font(.title3)
|
||||||
|
|
@ -105,15 +105,17 @@ struct WheelPicker: View {
|
||||||
let todayIndex = indexForDate(Date(), offset: offset)
|
let todayIndex = indexForDate(Date(), offset: offset)
|
||||||
guard let newIndex = newValue else { return }
|
guard let newIndex = newValue else { return }
|
||||||
let targetDateIndex = newIndex + config.offset
|
let targetDateIndex = newIndex + config.offset
|
||||||
switch targetDateIndex{
|
switch targetDateIndex {
|
||||||
case todayIndex-config.past..<todayIndex+config.future:
|
case todayIndex - config.past ..< todayIndex + config.future:
|
||||||
selectedDate = dateForIndex(targetDateIndex, offset: offset)
|
selectedDate = dateForIndex(targetDateIndex, offset: offset)
|
||||||
haptics.toggle()
|
if (Defaults[.enableHaptics]) {
|
||||||
|
haptics.toggle()
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func scrollToToday(config: Config) {
|
private func scrollToToday(config: Config) {
|
||||||
let today = Date()
|
let today = Date()
|
||||||
let todayIndex = indexForDate(today, offset: -config.offset - config.past)
|
let todayIndex = indexForDate(today, offset: -config.offset - config.past)
|
||||||
|
|
@ -121,7 +123,7 @@ struct WheelPicker: View {
|
||||||
scrollPosition = todayIndex - config.offset
|
scrollPosition = todayIndex - config.offset
|
||||||
selectedDate = today
|
selectedDate = today
|
||||||
}
|
}
|
||||||
|
|
||||||
private func indexForDate(_ date: Date, offset: Int) -> Int {
|
private func indexForDate(_ date: Date, offset: Int) -> Int {
|
||||||
let calendar = Calendar.current
|
let calendar = Calendar.current
|
||||||
let startDate = calendar.startOfDay(for: calendar.date(byAdding: .day, value: offset, to: Date()) ?? Date())
|
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
|
let daysDifference = calendar.dateComponents([.day], from: startDate, to: targetDate).day ?? 0
|
||||||
return daysDifference
|
return daysDifference
|
||||||
}
|
}
|
||||||
|
|
||||||
private func dateForIndex(_ index: Int, offset: Int) -> Date {
|
private func dateForIndex(_ index: Int, offset: Int) -> Date {
|
||||||
let startDate = Calendar.current.date(byAdding: .day, value: offset, to: Date()) ?? Date()
|
let startDate = Calendar.current.date(byAdding: .day, value: offset, to: Date()) ?? Date()
|
||||||
return Calendar.current.date(byAdding: .day, value: index, to: startDate) ?? 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 {
|
private func dateToString(for date: Date) -> String {
|
||||||
let formatter = DateFormatter()
|
let formatter = DateFormatter()
|
||||||
formatter.dateFormat = "E"
|
formatter.dateFormat = "E"
|
||||||
return formatter.string(from: date)
|
return formatter.string(from: date)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func isDateSelected(_ index: Int, offset: Int) -> Bool {
|
private func isDateSelected(_ index: Int, offset: Int) -> Bool {
|
||||||
Calendar.current.isDate(dateForIndex(index, offset: offset), inSameDayAs: selectedDate)
|
Calendar.current.isDate(dateForIndex(index, offset: offset), inSameDayAs: selectedDate)
|
||||||
}
|
}
|
||||||
|
|
@ -155,7 +152,7 @@ struct CalendarView: View {
|
||||||
@EnvironmentObject var vm: DynamicIslandViewModel
|
@EnvironmentObject var vm: DynamicIslandViewModel
|
||||||
@StateObject private var calendarManager = CalendarManager()
|
@StateObject private var calendarManager = CalendarManager()
|
||||||
@State private var selectedDate = Date()
|
@State private var selectedDate = Date()
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(spacing: 8) {
|
VStack(spacing: 8) {
|
||||||
HStack {
|
HStack {
|
||||||
|
|
@ -185,13 +182,19 @@ struct CalendarView: View {
|
||||||
}
|
}
|
||||||
.listRowBackground(Color.clear)
|
.listRowBackground(Color.clear)
|
||||||
.onChange(of: selectedDate) { _, newDate in
|
.onChange(of: selectedDate) { _, newDate in
|
||||||
calendarManager.updateCurrentDate(newDate)
|
Task {
|
||||||
|
await calendarManager.updateCurrentDate(newDate)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.onChange(of: vm.notchState) { _, _ in
|
.onChange(of: vm.notchState) { _, _ in
|
||||||
calendarManager.updateCurrentDate(Date.now)
|
Task {
|
||||||
|
await calendarManager.updateCurrentDate(Date.now)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.onAppear {
|
.onAppear {
|
||||||
calendarManager.updateCurrentDate(Date.now)
|
Task {
|
||||||
|
await calendarManager.updateCurrentDate(Date.now)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -211,7 +214,7 @@ struct EmptyEventsView: View {
|
||||||
|
|
||||||
struct EventListView: View {
|
struct EventListView: View {
|
||||||
@Environment(\.openURL) private var openURL
|
@Environment(\.openURL) private var openURL
|
||||||
let events: [EKEvent]
|
let events: [EventModel]
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ScrollView(showsIndicators: false) {
|
ScrollView(showsIndicators: false) {
|
||||||
|
|
@ -219,13 +222,11 @@ struct EventListView: View {
|
||||||
VStack(alignment: .trailing, spacing: 5) {
|
VStack(alignment: .trailing, spacing: 5) {
|
||||||
ForEach(events.indices, id: \.self) { index in
|
ForEach(events.indices, id: \.self) { index in
|
||||||
VStack(alignment: .trailing) {
|
VStack(alignment: .trailing) {
|
||||||
if isAllDayEvent(
|
if events[index].isAllDay {
|
||||||
start: events[index].startDate, end: events[index].endDate)
|
|
||||||
{
|
|
||||||
Text("All-day")
|
Text("All-day")
|
||||||
} else {
|
} else {
|
||||||
Text("\(events[index].startDate, style: .time)")
|
Text("\(events[index].start, style: .time)")
|
||||||
Text("\(events[index].endDate, style: .time)")
|
Text("\(events[index].end, style: .time)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.multilineTextAlignment(.trailing)
|
.multilineTextAlignment(.trailing)
|
||||||
|
|
@ -239,10 +240,10 @@ struct EventListView: View {
|
||||||
HStack(alignment: .top) {
|
HStack(alignment: .top) {
|
||||||
VStack(spacing: 5) {
|
VStack(spacing: 5) {
|
||||||
Image(
|
Image(
|
||||||
systemName: isEventEnded(events[index].endDate)
|
systemName: events[index].eventStatus == .ended
|
||||||
? "checkmark.circle" : "circle"
|
? "checkmark.circle" : "circle"
|
||||||
)
|
)
|
||||||
.foregroundColor(isEventEnded(events[index].endDate) ? .green : .gray)
|
.foregroundColor(events[index].eventStatus == .ended ? .green : .gray)
|
||||||
.font(.footnote)
|
.font(.footnote)
|
||||||
Rectangle()
|
Rectangle()
|
||||||
.frame(width: 1)
|
.frame(width: 1)
|
||||||
|
|
@ -252,7 +253,7 @@ struct EventListView: View {
|
||||||
.padding(.top, 1)
|
.padding(.top, 1)
|
||||||
|
|
||||||
Button {
|
Button {
|
||||||
if let url = generateEventURL(for: events[index]) {
|
if let url = events[index].calendarAppURL() {
|
||||||
openURL(url)
|
openURL(url)
|
||||||
}
|
}
|
||||||
} label: {
|
} label: {
|
||||||
|
|
@ -264,7 +265,7 @@ struct EventListView: View {
|
||||||
|
|
||||||
Spacer(minLength: 0)
|
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)
|
.scrollIndicators(.never)
|
||||||
.scrollTargetBehavior(.viewAligned)
|
.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 {
|
#Preview {
|
||||||
CalendarView()
|
CalendarView()
|
||||||
.frame(width: 250)
|
.frame(width: 250)
|
||||||
.padding(.horizontal)
|
.padding(.horizontal)
|
||||||
.background(.black)
|
.background(.black)
|
||||||
.environmentObject(DynamicIslandViewModel())
|
.environmentObject(DynamicIslandViewModel.init())
|
||||||
}
|
}
|
||||||
|
|
@ -2,10 +2,6 @@
|
||||||
// DynamicIslandExtrasMenu.swift
|
// DynamicIslandExtrasMenu.swift
|
||||||
// DynamicIsland
|
// DynamicIsland
|
||||||
//
|
//
|
||||||
// Created by Harsh Vardhan Goswami on 02/08/24.
|
|
||||||
// DynamicIslandExtrasMenu.swift
|
|
||||||
// DynamicIsland
|
|
||||||
//
|
|
||||||
// Created by Harsh Vardhan Goswami on 04/08/24.
|
// Created by Harsh Vardhan Goswami on 04/08/24.
|
||||||
//
|
//
|
||||||
|
|
||||||
|
|
@ -65,7 +61,9 @@ struct DynamicIslandExtrasMenu : View {
|
||||||
}
|
}
|
||||||
|
|
||||||
var settings: some View {
|
var settings: some View {
|
||||||
SettingsLink(label: {
|
Button(action: {
|
||||||
|
SettingsWindowController.shared.showWindow()
|
||||||
|
}) {
|
||||||
ZStack {
|
ZStack {
|
||||||
RoundedRectangle(cornerRadius: 12.0).fill(.black).frame(width: 70, height: 70)
|
RoundedRectangle(cornerRadius: 12.0).fill(.black).frame(width: 70, height: 70)
|
||||||
VStack(spacing: 8) {
|
VStack(spacing: 8) {
|
||||||
|
|
@ -74,7 +72,7 @@ struct DynamicIslandExtrasMenu : View {
|
||||||
Text("Settings").font(.body)
|
Text("Settings").font(.body)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
.buttonStyle(PlainButtonStyle()).shadow(color: .black.opacity(0.5), radius: 10)
|
.buttonStyle(PlainButtonStyle()).shadow(color: .black.opacity(0.5), radius: 10)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -107,5 +105,5 @@ struct DynamicIslandExtrasMenu : View {
|
||||||
|
|
||||||
|
|
||||||
#Preview {
|
#Preview {
|
||||||
DynamicIslandExtrasMenu(vm: .init())
|
DynamicIslandExtrasMenu(vm: DynamicIslandViewModel())
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ import SwiftUI
|
||||||
|
|
||||||
struct DynamicIslandHeader: View {
|
struct DynamicIslandHeader: View {
|
||||||
@EnvironmentObject var vm: DynamicIslandViewModel
|
@EnvironmentObject var vm: DynamicIslandViewModel
|
||||||
|
@EnvironmentObject var webcamManager: WebcamManager
|
||||||
@ObservedObject var batteryModel = BatteryStatusViewModel.shared
|
@ObservedObject var batteryModel = BatteryStatusViewModel.shared
|
||||||
@ObservedObject var coordinator = DynamicIslandViewCoordinator.shared
|
@ObservedObject var coordinator = DynamicIslandViewCoordinator.shared
|
||||||
@StateObject var tvm = TrayDrop.shared
|
@StateObject var tvm = TrayDrop.shared
|
||||||
|
|
@ -40,8 +41,30 @@ struct DynamicIslandHeader: View {
|
||||||
|
|
||||||
HStack(spacing: 4) {
|
HStack(spacing: 4) {
|
||||||
if vm.notchState == .open {
|
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] {
|
if Defaults[.settingsIconInNotch] {
|
||||||
SettingsLink(label: {
|
Button(action: {
|
||||||
|
SettingsWindowController.shared.showWindow()
|
||||||
|
}) {
|
||||||
Capsule()
|
Capsule()
|
||||||
.fill(.black)
|
.fill(.black)
|
||||||
.frame(width: 30, height: 30)
|
.frame(width: 30, height: 30)
|
||||||
|
|
@ -51,7 +74,7 @@ struct DynamicIslandHeader: View {
|
||||||
.padding()
|
.padding()
|
||||||
.imageScale(.medium)
|
.imageScale(.medium)
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
.buttonStyle(PlainButtonStyle())
|
.buttonStyle(PlainButtonStyle())
|
||||||
}
|
}
|
||||||
if Defaults[.showBatteryIndicator] {
|
if Defaults[.showBatteryIndicator] {
|
||||||
|
|
@ -81,5 +104,7 @@ struct DynamicIslandHeader: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
#Preview {
|
#Preview {
|
||||||
DynamicIslandHeader().environmentObject(DynamicIslandViewModel())
|
DynamicIslandHeader()
|
||||||
|
.environmentObject(DynamicIslandViewModel())
|
||||||
|
.environmentObject(WebcamManager.shared)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
// DynamicIsland
|
// DynamicIsland
|
||||||
//
|
//
|
||||||
// Created by Hugo Persson on 2024-08-18.
|
// 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
|
import Combine
|
||||||
|
|
@ -179,11 +179,11 @@ struct MusicControlsView: View {
|
||||||
|
|
||||||
struct NotchHomeView: View {
|
struct NotchHomeView: View {
|
||||||
@EnvironmentObject var vm: DynamicIslandViewModel
|
@EnvironmentObject var vm: DynamicIslandViewModel
|
||||||
@EnvironmentObject var webcamManager: WebcamManager
|
@ObservedObject var webcamManager = WebcamManager.shared
|
||||||
@ObservedObject var batteryModel = BatteryStatusViewModel.shared
|
@ObservedObject var batteryModel = BatteryStatusViewModel.shared
|
||||||
@ObservedObject var coordinator = DynamicIslandViewCoordinator.shared
|
@ObservedObject var coordinator = DynamicIslandViewCoordinator.shared
|
||||||
let albumArtNamespace: Namespace.ID
|
let albumArtNamespace: Namespace.ID
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Group {
|
Group {
|
||||||
if !coordinator.firstLaunch {
|
if !coordinator.firstLaunch {
|
||||||
|
|
@ -196,16 +196,18 @@ struct NotchHomeView: View {
|
||||||
private var mainContent: some View {
|
private var mainContent: some View {
|
||||||
HStack(alignment: .top, spacing: 20) {
|
HStack(alignment: .top, spacing: 20) {
|
||||||
MusicPlayerView(albumArtNamespace: albumArtNamespace)
|
MusicPlayerView(albumArtNamespace: albumArtNamespace)
|
||||||
|
|
||||||
if Defaults[.showCalendar] {
|
if Defaults[.showCalendar] {
|
||||||
CalendarView()
|
CalendarView()
|
||||||
.onHover { isHovering in
|
.onHover { isHovering in
|
||||||
vm.isHoveringCalendar = isHovering
|
vm.isHoveringCalendar = isHovering
|
||||||
}
|
}
|
||||||
.environmentObject(vm)
|
.environmentObject(vm)
|
||||||
}
|
}
|
||||||
|
|
||||||
if Defaults[.showMirror] && webcamManager.cameraAvailable {
|
if Defaults[.showMirror],
|
||||||
|
webcamManager.cameraAvailable,
|
||||||
|
vm.notchState == .open {
|
||||||
CameraPreviewView(webcamManager: webcamManager)
|
CameraPreviewView(webcamManager: webcamManager)
|
||||||
.scaledToFit()
|
.scaledToFit()
|
||||||
.opacity(vm.notchState == .closed ? 0 : 1)
|
.opacity(vm.notchState == .closed ? 0 : 1)
|
||||||
|
|
@ -249,7 +251,7 @@ struct MusicSliderView: View {
|
||||||
range: 0 ... duration,
|
range: 0 ... duration,
|
||||||
color: Defaults[.sliderColor] == SliderColorEnum.albumArt ? Color(
|
color: Defaults[.sliderColor] == SliderColorEnum.albumArt ? Color(
|
||||||
nsColor: 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,
|
dragging: $dragging,
|
||||||
lastDragged: $lastDragged,
|
lastDragged: $lastDragged,
|
||||||
onValueChange: onValueChange
|
onValueChange: onValueChange
|
||||||
|
|
@ -336,7 +338,8 @@ struct CustomSlider: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
#Preview {
|
#Preview {
|
||||||
NotchHomeView(albumArtNamespace: Namespace().wrappedValue)
|
NotchHomeView(
|
||||||
.environmentObject(DynamicIslandViewModel())
|
albumArtNamespace: Namespace().wrappedValue
|
||||||
.environmentObject(WebcamManager())
|
)
|
||||||
|
.environmentObject(DynamicIslandViewModel())
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
//
|
//
|
||||||
// Created by Kai Azim on 2023-08-24.
|
// Created by Kai Azim on 2023-08-24.
|
||||||
// Original source: https://github.com/MrKai77/DynamicNotchKit
|
// Original source: https://github.com/MrKai77/DynamicNotchKit
|
||||||
// Modified by Alexander Greco on 2025-05-18.
|
// Modified by Alexander on 2025-05-18.
|
||||||
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,95 +16,120 @@ import SwiftUI
|
||||||
import SwiftUIIntrospect
|
import SwiftUIIntrospect
|
||||||
|
|
||||||
struct SettingsView: View {
|
struct SettingsView: View {
|
||||||
@Environment(\.scenePhase) private var scenePhase
|
|
||||||
@StateObject var extensionManager = DynamicIslandExtensionManager()
|
@StateObject var extensionManager = DynamicIslandExtensionManager()
|
||||||
let updaterController: SPUStandardUpdaterController
|
|
||||||
@StateObject private var calendarManager = CalendarManager()
|
@StateObject private var calendarManager = CalendarManager()
|
||||||
|
|
||||||
@State private var selectedTab = "General"
|
@State private var selectedTab = "General"
|
||||||
|
|
||||||
|
let updaterController: SPUStandardUpdaterController?
|
||||||
|
|
||||||
|
init(updaterController: SPUStandardUpdaterController? = nil) {
|
||||||
|
self.updaterController = updaterController
|
||||||
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
NavigationSplitView {
|
NavigationSplitView {
|
||||||
List(selection: $selectedTab) {
|
List(selection: $selectedTab) {
|
||||||
NavigationLink(destination: GeneralSettings()) {
|
NavigationLink(value: "General") {
|
||||||
Label("General", systemImage: "gear")
|
Label("General", systemImage: "gear")
|
||||||
}
|
}
|
||||||
NavigationLink(destination: Appearance()) {
|
NavigationLink(value: "Appearance") {
|
||||||
Label("Appearance", systemImage: "eye")
|
Label("Appearance", systemImage: "eye")
|
||||||
}
|
}
|
||||||
NavigationLink(destination: Media()) {
|
NavigationLink(value: "Media") {
|
||||||
Label("Media", systemImage: "play.laptopcomputer")
|
Label("Media", systemImage: "play.laptopcomputer")
|
||||||
}
|
}
|
||||||
NavigationLink(destination: CalendarSettings()) {
|
NavigationLink(value: "Calendar") {
|
||||||
Label("Calendar", systemImage: "calendar")
|
Label("Calendar", systemImage: "calendar")
|
||||||
}
|
}
|
||||||
if extensionManager.installedExtensions
|
if extensionManager.installedExtensions
|
||||||
.contains(
|
.contains(where: { $0.bundleIdentifier == hudExtension }) {
|
||||||
where: { $0.bundleIdentifier == hudExtension
|
NavigationLink(value: "HUD") {
|
||||||
})
|
|
||||||
{
|
|
||||||
NavigationLink(destination: HUD()) {
|
|
||||||
Label("HUDs", systemImage: "dial.medium.fill")
|
Label("HUDs", systemImage: "dial.medium.fill")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
NavigationLink(destination: Charge()) {
|
NavigationLink(value: "Battery") {
|
||||||
Label("Battery", systemImage: "battery.100.bolt")
|
Label("Battery", systemImage: "battery.100.bolt")
|
||||||
}
|
}
|
||||||
if extensionManager.installedExtensions
|
if extensionManager.installedExtensions
|
||||||
.contains(
|
.contains(where: { $0.bundleIdentifier == downloadManagerExtension }) {
|
||||||
where: { $0.bundleIdentifier == downloadManagerExtension
|
NavigationLink(value: "Downloads") {
|
||||||
})
|
|
||||||
{
|
|
||||||
NavigationLink(destination: Downloads()) {
|
|
||||||
Label("Downloads", systemImage: "square.and.arrow.down")
|
Label("Downloads", systemImage: "square.and.arrow.down")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
NavigationLink(destination: Shelf()) {
|
NavigationLink(value: "Shelf") {
|
||||||
Label("Shelf", systemImage: "books.vertical")
|
Label("Shelf", systemImage: "books.vertical")
|
||||||
}
|
}
|
||||||
NavigationLink(destination: Shortcuts()) {
|
NavigationLink(value: "Shortcuts") {
|
||||||
Label("Shortcuts", systemImage: "keyboard")
|
Label("Shortcuts", systemImage: "keyboard")
|
||||||
}
|
}
|
||||||
NavigationLink(destination: Extensions()) {
|
NavigationLink(value: "Extensions") {
|
||||||
Label("Extensions", systemImage: "puzzlepiece.extension")
|
Label("Extensions", systemImage: "puzzlepiece.extension")
|
||||||
}
|
}
|
||||||
NavigationLink(
|
NavigationLink(value: "About") {
|
||||||
destination: About(updaterController: updaterController)
|
|
||||||
) {
|
|
||||||
Label("About", systemImage: "info.circle")
|
Label("About", systemImage: "info.circle")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.tint(Defaults[.accentColor])
|
.listStyle(SidebarListStyle())
|
||||||
|
.toolbar(removing: .sidebarToggle)
|
||||||
|
.navigationSplitViewColumnWidth(200)
|
||||||
} detail: {
|
} 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(extensionManager)
|
||||||
.environmentObject(calendarManager)
|
.environmentObject(calendarManager)
|
||||||
.formStyle(.grouped)
|
.formStyle(.grouped)
|
||||||
.onChange(of: scenePhase) {
|
.frame(width: 700)
|
||||||
if scenePhase == .active {
|
.background(Color(NSColor.windowBackgroundColor))
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct GeneralSettings: View {
|
struct GeneralSettings: View {
|
||||||
@State private var screens: [String] = NSScreen.screens.compactMap { $0.localizedName }
|
@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
|
@EnvironmentObject var vm: DynamicIslandViewModel
|
||||||
@ObservedObject var coordinator = DynamicIslandViewCoordinator.shared
|
@ObservedObject var coordinator = DynamicIslandViewCoordinator.shared
|
||||||
@Default(.accentColor) var accentColor
|
|
||||||
@Default(.mirrorShape) var mirrorShape
|
@Default(.mirrorShape) var mirrorShape
|
||||||
@Default(.showEmojis) var showEmojis
|
@Default(.showEmojis) var showEmojis
|
||||||
@Default(.gestureSensitivity) var gestureSensitivity
|
@Default(.gestureSensitivity) var gestureSensitivity
|
||||||
|
|
@ -121,38 +146,6 @@ struct GeneralSettings: View {
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Form {
|
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 {
|
Section {
|
||||||
Defaults.Toggle("Menubar icon", key: .menubarIcon)
|
Defaults.Toggle("Menubar icon", key: .menubarIcon)
|
||||||
LaunchAtLogin.Toggle("Launch at login")
|
LaunchAtLogin.Toggle("Launch at login")
|
||||||
|
|
@ -244,7 +237,6 @@ struct GeneralSettings: View {
|
||||||
|
|
||||||
gestureControls()
|
gestureControls()
|
||||||
}
|
}
|
||||||
.tint(Defaults[.accentColor])
|
|
||||||
.toolbar {
|
.toolbar {
|
||||||
Button("Quit app") {
|
Button("Quit app") {
|
||||||
NSApp.terminate(self)
|
NSApp.terminate(self)
|
||||||
|
|
@ -293,7 +285,7 @@ struct GeneralSettings: View {
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
func NotchBehaviour() -> some View {
|
func NotchBehaviour() -> some View {
|
||||||
Section {
|
Section {
|
||||||
Defaults.Toggle("Extend hover area", key: .extendHoverArea)
|
Defaults.Toggle("Extend hover area", key: .extendHoverArea)
|
||||||
Defaults.Toggle("Enable haptics", key: .enableHaptics)
|
Defaults.Toggle("Enable haptics", key: .enableHaptics)
|
||||||
Defaults.Toggle("Open notch on hover", key: .openNotchOnHover)
|
Defaults.Toggle("Open notch on hover", key: .openNotchOnHover)
|
||||||
Toggle("Remember last tab", isOn: $coordinator.openLastTabByDefault)
|
Toggle("Remember last tab", isOn: $coordinator.openLastTabByDefault)
|
||||||
|
|
@ -332,7 +324,6 @@ struct Charge: View {
|
||||||
Text("Battery Information")
|
Text("Battery Information")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.tint(Defaults[.accentColor])
|
|
||||||
.navigationTitle("Battery")
|
.navigationTitle("Battery")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -407,7 +398,6 @@ struct Downloads: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.tint(Defaults[.accentColor])
|
|
||||||
.navigationTitle("Downloads")
|
.navigationTitle("Downloads")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -453,7 +443,6 @@ struct HUD: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.tint(Defaults[.accentColor])
|
|
||||||
.navigationTitle("HUDs")
|
.navigationTitle("HUDs")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -537,7 +526,6 @@ struct Media: View {
|
||||||
Defaults[.enableFullscreenMediaDetection] = newValue != .never
|
Defaults[.enableFullscreenMediaDetection] = newValue != .never
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.tint(Defaults[.accentColor])
|
|
||||||
.navigationTitle("Media")
|
.navigationTitle("Media")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -571,11 +559,13 @@ struct CalendarSettings: View {
|
||||||
Toggle("Show calendar", isOn: $showCalendar)
|
Toggle("Show calendar", isOn: $showCalendar)
|
||||||
Section(header: Text("Select Calendars")) {
|
Section(header: Text("Select Calendars")) {
|
||||||
List {
|
List {
|
||||||
ForEach(calendarManager.allCalendars, id: \.calendarIdentifier) { calendar in
|
ForEach(calendarManager.allCalendars, id: \.id) { calendar in
|
||||||
Toggle(isOn: Binding(
|
Toggle(isOn: Binding(
|
||||||
get: { calendarManager.getCalendarSelected(calendar) },
|
get: { calendarManager.getCalendarSelected(calendar) },
|
||||||
set: { isSelected in
|
set: { isSelected in
|
||||||
calendarManager.setCalendarSelected(calendar, isSelected: isSelected)
|
Task {
|
||||||
|
await calendarManager.setCalendarSelected(calendar, isSelected: isSelected)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
)) {
|
)) {
|
||||||
Text(calendar.title)
|
Text(calendar.title)
|
||||||
|
|
@ -586,8 +576,12 @@ struct CalendarSettings: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.onAppear {
|
.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) {
|
VStack(spacing: 0) {
|
||||||
Divider()
|
Divider()
|
||||||
Text("Made with 🫶🏻 by the DynamicIsland team")
|
Text("Made with 🫶🏻 by not so boring not.people")
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
.padding(.top, 5)
|
.padding(.top, 5)
|
||||||
.padding(.bottom, 7)
|
.padding(.bottom, 7)
|
||||||
|
|
@ -669,13 +663,12 @@ struct About: View {
|
||||||
.frame(maxWidth: .infinity, alignment: .center)
|
.frame(maxWidth: .infinity, alignment: .center)
|
||||||
}
|
}
|
||||||
.toolbar {
|
.toolbar {
|
||||||
Button("Welcome window") {
|
// Button("Welcome window") {
|
||||||
openWindow(id: "onboarding")
|
// openWindow(id: "onboarding")
|
||||||
}
|
// }
|
||||||
.controlSize(.extraLarge)
|
// .controlSize(.extraLarge)
|
||||||
CheckForUpdatesView(updater: updaterController.updater)
|
CheckForUpdatesView(updater: updaterController.updater)
|
||||||
}
|
}
|
||||||
.tint(Defaults[.accentColor])
|
|
||||||
.navigationTitle("About")
|
.navigationTitle("About")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -692,7 +685,6 @@ struct Shelf: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.tint(Defaults[.accentColor])
|
|
||||||
.navigationTitle("Shelf")
|
.navigationTitle("Shelf")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -810,7 +802,6 @@ struct Extensions: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.tint(Defaults[.accentColor])
|
|
||||||
.navigationTitle("Extensions")
|
.navigationTitle("Extensions")
|
||||||
// TipsView()
|
// TipsView()
|
||||||
// .padding(.horizontal, 19)
|
// .padding(.horizontal, 19)
|
||||||
|
|
@ -913,7 +904,7 @@ struct Appearance: View {
|
||||||
.buttonStyle(PlainButtonStyle())
|
.buttonStyle(PlainButtonStyle())
|
||||||
.padding(.vertical, 2)
|
.padding(.vertical, 2)
|
||||||
.background(
|
.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)
|
in: RoundedRectangle(cornerRadius: 5)
|
||||||
)
|
)
|
||||||
.contentShape(Rectangle())
|
.contentShape(Rectangle())
|
||||||
|
|
@ -1027,7 +1018,7 @@ struct Appearance: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
Section {
|
Section {
|
||||||
Defaults.Toggle("Enable dynamic mirror", key: .showMirror)
|
Defaults.Toggle("Enable boring mirror", key: .showMirror)
|
||||||
.disabled(!checkVideoInput())
|
.disabled(!checkVideoInput())
|
||||||
Picker("Mirror shape", selection: $mirrorShape) {
|
Picker("Mirror shape", selection: $mirrorShape) {
|
||||||
Text("Circle")
|
Text("Circle")
|
||||||
|
|
@ -1053,7 +1044,7 @@ struct Appearance: View {
|
||||||
.background(
|
.background(
|
||||||
RoundedRectangle(cornerRadius: 20, style: .circular)
|
RoundedRectangle(cornerRadius: 20, style: .circular)
|
||||||
.strokeBorder(
|
.strokeBorder(
|
||||||
icon == selectedIcon ? Defaults[.accentColor] : .clear,
|
icon == selectedIcon ? Color.accentColor : .clear,
|
||||||
lineWidth: 2.5
|
lineWidth: 2.5
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
@ -1066,7 +1057,7 @@ struct Appearance: View {
|
||||||
.padding(.vertical, 3)
|
.padding(.vertical, 3)
|
||||||
.background(
|
.background(
|
||||||
Capsule()
|
Capsule()
|
||||||
.fill(icon == selectedIcon ? Defaults[.accentColor] : .clear)
|
.fill(icon == selectedIcon ? Color.accentColor : .clear)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
.onTapGesture {
|
.onTapGesture {
|
||||||
|
|
@ -1086,7 +1077,6 @@ struct Appearance: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.tint(Defaults[.accentColor])
|
|
||||||
.navigationTitle("Appearance")
|
.navigationTitle("Appearance")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1116,7 +1106,6 @@ struct Shortcuts: View {
|
||||||
KeyboardShortcuts.Recorder("Toggle Notch Open:", name: .toggleNotchOpen)
|
KeyboardShortcuts.Recorder("Toggle Notch Open:", name: .toggleNotchOpen)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.tint(Defaults[.accentColor])
|
|
||||||
.navigationTitle("Shortcuts")
|
.navigationTitle("Shortcuts")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
125
DynamicIsland/components/Settings/SettingsWindowController.swift
Normal file
125
DynamicIsland/components/Settings/SettingsWindowController.swift
Normal file
|
|
@ -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) {
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -118,5 +118,5 @@ struct CameraPreviewLayerView: NSViewRepresentable {
|
||||||
}
|
}
|
||||||
|
|
||||||
#Preview {
|
#Preview {
|
||||||
CameraPreviewView(webcamManager: WebcamManager())
|
CameraPreviewView(webcamManager: WebcamManager.shared)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
// AppleScriptHelper.swift
|
// AppleScriptHelper.swift
|
||||||
// DynamicIsland
|
// DynamicIsland
|
||||||
//
|
//
|
||||||
// Created by Alexander Greco on 2025-03-29.
|
// Created by Alexander on 2025-03-29.
|
||||||
//
|
//
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
//
|
//
|
||||||
// CalenderManager.swift
|
// CalendarManager.swift
|
||||||
// DynamicIsland
|
// DynamicIsland
|
||||||
//
|
//
|
||||||
// Created by Harsh Vardhan Goswami on 08/09/24.
|
// Created by Harsh Vardhan Goswami on 08/09/24.
|
||||||
|
|
@ -11,39 +11,37 @@ import Defaults
|
||||||
|
|
||||||
// MARK: - CalendarManager
|
// MARK: - CalendarManager
|
||||||
|
|
||||||
|
@MainActor
|
||||||
class CalendarManager: ObservableObject {
|
class CalendarManager: ObservableObject {
|
||||||
@Published var currentWeekStartDate: Date
|
@Published var currentWeekStartDate: Date
|
||||||
@Published var events: [EKEvent] = []
|
@Published var events: [EventModel] = []
|
||||||
@Published var allCalendars: [EKCalendar] = []
|
@Published var allCalendars: [CalendarModel] = []
|
||||||
private var selectedCalendars: [EKCalendar] = []
|
|
||||||
private let eventStore = EKEventStore()
|
|
||||||
@Published var authorizationStatus: EKAuthorizationStatus = .notDetermined
|
@Published var authorizationStatus: EKAuthorizationStatus = .notDetermined
|
||||||
|
private var selectedCalendars: [CalendarModel] = []
|
||||||
|
private let calendarService = CalendarService()
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
self.currentWeekStartDate = CalendarManager.startOfDay(Date())
|
self.currentWeekStartDate = CalendarManager.startOfDay(Date())
|
||||||
if(Defaults[.showCalendar]) {
|
|
||||||
checkCalendarAuthorization()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func checkCalendarAuthorization() {
|
func checkCalendarAuthorization() async {
|
||||||
let status = EKEventStore.authorizationStatus(for: .event)
|
let status = EKEventStore.authorizationStatus(for: .event)
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
print("📅 Current calendar authorization status: \(status)")
|
print("📅 Current calendar authorization status: \(status)")
|
||||||
self.authorizationStatus = status
|
self.authorizationStatus = status
|
||||||
}
|
}
|
||||||
|
|
||||||
switch status {
|
switch status {
|
||||||
case .notDetermined:
|
case .notDetermined:
|
||||||
requestCalendarAccess()
|
self.authorizationStatus = await calendarService.requestAccess() ? .fullAccess : .denied
|
||||||
case .restricted, .denied:
|
case .restricted, .denied:
|
||||||
// Handle the case where the user has denied or restricted access
|
// Handle the case where the user has denied or restricted access
|
||||||
NSLog("Calendar access denied or restricted")
|
NSLog("Calendar access denied or restricted")
|
||||||
case .fullAccess:
|
case .fullAccess:
|
||||||
NSLog("Full access")
|
NSLog("Full access")
|
||||||
self.allCalendars = eventStore.calendars(for: .event)
|
allCalendars = await calendarService.calendars()
|
||||||
updateSelectedCalendars()
|
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:
|
case .writeOnly:
|
||||||
NSLog("Write only")
|
NSLog("Write only")
|
||||||
@unknown default:
|
@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() {
|
func updateSelectedCalendars() {
|
||||||
selectedCalendars = allCalendars.filter { getCalendarSelected($0) }
|
selectedCalendars = allCalendars.filter { getCalendarSelected($0) }
|
||||||
}
|
}
|
||||||
|
|
||||||
func getCalendarSelected(_ calendar: EKCalendar) -> Bool {
|
func getCalendarSelected(_ calendar: CalendarModel) -> Bool {
|
||||||
switch Defaults[.calendarSelectionState] {
|
switch Defaults[.calendarSelectionState] {
|
||||||
case .all:
|
case .all:
|
||||||
return true
|
return true
|
||||||
case .selected(let identifiers):
|
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]
|
var selectionState = Defaults[.calendarSelectionState]
|
||||||
|
|
||||||
switch selectionState {
|
switch selectionState {
|
||||||
case .all:
|
case .all:
|
||||||
if !isSelected {
|
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)
|
selectionState = .selected(identifiers)
|
||||||
}
|
}
|
||||||
|
|
||||||
case .selected(var identifiers):
|
case .selected(var identifiers):
|
||||||
if isSelected {
|
if isSelected {
|
||||||
identifiers.insert(calendar.calendarIdentifier)
|
identifiers.insert(calendar.id)
|
||||||
} else {
|
} 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
|
Defaults[.calendarSelectionState] = selectionState
|
||||||
fetchEvents()
|
updateSelectedCalendars()
|
||||||
}
|
events = await calendarService.events(from: currentWeekStartDate, to: Calendar.current.date(byAdding: .day, value: 1, to: currentWeekStartDate)!, calendars: selectedCalendars.map{$0.id})
|
||||||
|
|
||||||
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")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static func startOfDay(_ date: Date) -> Date {
|
static func startOfDay(_ date: Date) -> Date {
|
||||||
return Calendar.current.startOfDay(for: date)
|
return Calendar.current.startOfDay(for: date)
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateCurrentDate(_ date: Date) {
|
func updateCurrentDate(_ date: Date) async {
|
||||||
print("📅 Updating current date to: \(date)")
|
|
||||||
currentWeekStartDate = Calendar.current.startOfDay(for: date)
|
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})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,8 @@ import AVFoundation
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
class WebcamManager: NSObject, ObservableObject {
|
class WebcamManager: NSObject, ObservableObject {
|
||||||
|
static let shared = WebcamManager()
|
||||||
|
|
||||||
@Published var previewLayer: AVCaptureVideoPreviewLayer? {
|
@Published var previewLayer: AVCaptureVideoPreviewLayer? {
|
||||||
didSet {
|
didSet {
|
||||||
objectWillChange.send()
|
objectWillChange.send()
|
||||||
|
|
@ -58,9 +60,8 @@ class WebcamManager: NSObject, ObservableObject {
|
||||||
|
|
||||||
// MARK: - Properties
|
// MARK: - Properties
|
||||||
|
|
||||||
override init() {
|
private override init() {
|
||||||
super.init()
|
super.init()
|
||||||
checkAndRequestVideoAuthorization()
|
|
||||||
NotificationCenter.default.addObserver(self, selector: #selector(deviceWasDisconnected), name: .AVCaptureDeviceWasDisconnected, object: nil)
|
NotificationCenter.default.addObserver(self, selector: #selector(deviceWasDisconnected), name: .AVCaptureDeviceWasDisconnected, object: nil)
|
||||||
NotificationCenter.default.addObserver(self, selector: #selector(deviceWasConnected), name: .AVCaptureDeviceWasConnected, object: nil)
|
NotificationCenter.default.addObserver(self, selector: #selector(deviceWasConnected), name: .AVCaptureDeviceWasConnected, object: nil)
|
||||||
checkCameraAvailability()
|
checkCameraAvailability()
|
||||||
|
|
|
||||||
17
DynamicIsland/models/CalendarModel.swift
Normal file
17
DynamicIsland/models/CalendarModel.swift
Normal file
|
|
@ -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
|
||||||
|
}
|
||||||
125
DynamicIsland/models/EventModel.swift
Normal file
125
DynamicIsland/models/EventModel.swift
Normal file
|
|
@ -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
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue