diff --git a/DynamicIsland/ContentView.swift b/DynamicIsland/ContentView.swift index 30d33cd..146cb11 100644 --- a/DynamicIsland/ContentView.swift +++ b/DynamicIsland/ContentView.swift @@ -87,6 +87,12 @@ struct ContentView: View { @Default(.showNotHumanFace) var showNotHumanFace @Default(.useModernCloseAnimation) var useModernCloseAnimation @Default(.enableMinimalisticUI) var enableMinimalisticUI + private var dynamicNotchResizeAnimation: Animation? { + if enableMinimalisticUI && reminderManager.activeWindowReminders.isEmpty == false { + return nil + } + return .easeInOut(duration: 0.4) + } private let extendedHoverPadding: CGFloat = 30 private let zeroHeightHoverPadding: CGFloat = 10 @@ -344,8 +350,8 @@ struct ContentView: View { // .keyboardShortcut("E", modifiers: .command) } } - .frame(maxWidth: dynamicNotchSize.width, maxHeight: dynamicNotchSize.height, alignment: .top) - .animation(.easeInOut(duration: 0.4), value: dynamicNotchSize) + .frame(maxWidth: dynamicNotchSize.width, maxHeight: dynamicNotchSize.height, alignment: .top) + .animation(dynamicNotchResizeAnimation, value: dynamicNotchSize) .animation(.easeInOut(duration: 0.4), value: coordinator.currentView) .environmentObject(privacyManager) .onChange(of: dynamicNotchSize) { oldSize, newSize in @@ -357,9 +363,11 @@ struct ContentView: View { isViewTransitioning = true let workItem = DispatchWorkItem { isViewTransitioning = false + vm.shouldRecheckHover.toggle() } sizeChangeWorkItem = workItem - DispatchQueue.main.asyncAfter(deadline: .now() + 1.0, execute: workItem) // Frame animation + buffer + let delay: TimeInterval = dynamicNotchResizeAnimation == nil ? 0.25 : 1.0 + DispatchQueue.main.asyncAfter(deadline: .now() + delay, execute: workItem) } } .shadow(color: ((vm.notchState == .open || isHovering) && Defaults[.enableShadow]) ? .black.opacity(0.6) : .clear, radius: Defaults[.cornerRadiusScaling] ? 10 : 5) @@ -736,11 +744,15 @@ struct ContentView: View { } } } else { + if vm.notchState == .open && isViewTransitioning { + return + } hoverTask = Task { try? await Task.sleep(for: .milliseconds(100)) guard !Task.isCancelled else { return } await MainActor.run { + guard !self.isViewTransitioning else { return } withAnimation(.bouncy.speed(1.2)) { self.isHovering = false } @@ -800,7 +812,7 @@ struct ContentView: View { let stillNotHovering = !isHovering let stillOpen = vm.notchState == .open let stillNoPopovers = !hasAnyActivePopovers() - let notTransitioning = !isStatsTransitioning && !isSwitchingToStats + let notTransitioning = !isStatsTransitioning && !isSwitchingToStats && !isViewTransitioning if stillNotHovering && stillOpen && stillNoPopovers && notTransitioning { vm.close() diff --git a/DynamicIsland/DynamicIslandApp.swift b/DynamicIsland/DynamicIslandApp.swift index 0b1646a..a6f2e48 100644 --- a/DynamicIsland/DynamicIslandApp.swift +++ b/DynamicIsland/DynamicIslandApp.swift @@ -79,6 +79,12 @@ struct DynamicNotchApp: App { } } +extension AppDelegate { + static var shared: AppDelegate? { + NSApplication.shared.delegate as? AppDelegate + } +} + class AppDelegate: NSObject, NSApplicationDelegate { var statusItem: NSStatusItem? var windows: [NSScreen: NSWindow] = [:] @@ -249,69 +255,8 @@ class AppDelegate: NSObject, NSApplicationDelegate { private func updateWindowSizeIfNeeded() { // Calculate required size based on current state let requiredSize = calculateRequiredNotchSize() - - if Defaults[.showOnAllDisplays] { - // Update all windows if size has changed (multi-display mode) - for (screen, window) in windows { - if window.frame.size != requiredSize { - // Calculate center position BEFORE any changes - let screenFrame = screen.frame - let centerX = screenFrame.origin.x + (screenFrame.width / 2) - let newX = centerX - (requiredSize.width / 2) - let newY = screenFrame.origin.y + screenFrame.height - requiredSize.height - - // Stop any existing animations first - NSAnimationContext.runAnimationGroup { _ in - window.animator().setFrame(window.frame, display: false) - } - - // Animate window resize smoothly - NSAnimationContext.runAnimationGroup { context in - context.duration = 0.25 - context.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut) - context.allowsImplicitAnimation = true - window.animator().setFrame(NSRect( - x: newX, - y: newY, - width: requiredSize.width, - height: requiredSize.height - ), display: true) - } - } - } - } else { - // Update single window if size has changed (single display mode) - if let window = window, window.frame.size != requiredSize { - // Find the screen this window is on - let currentScreen = NSScreen.screens.first { screen in - screen.frame.intersects(window.frame) - } ?? NSScreen.main ?? NSScreen.screens.first! - - // Calculate center position BEFORE any changes - let screenFrame = currentScreen.frame - let centerX = screenFrame.origin.x + (screenFrame.width / 2) - let newX = centerX - (requiredSize.width / 2) - let newY = screenFrame.origin.y + screenFrame.height - requiredSize.height - - // Stop any existing animations first - NSAnimationContext.runAnimationGroup { _ in - window.animator().setFrame(window.frame, display: false) - } - - // Animate window resize smoothly - NSAnimationContext.runAnimationGroup { context in - context.duration = 0.25 - context.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut) - context.allowsImplicitAnimation = true - window.animator().setFrame(NSRect( - x: newX, - y: newY, - width: requiredSize.width, - height: requiredSize.height - ), display: true) - } - } - } + let animateResize = shouldAnimateResize(for: requiredSize) + resizeWindows(to: requiredSize, animated: animateResize, force: false) } private func calculateRequiredNotchSize() -> CGSize { @@ -364,6 +309,54 @@ class AppDelegate: NSObject, NSApplicationDelegate { // Width stays constant - no horizontal expansion return CGSize(width: baseSize.width, height: requiredHeight) } + + func ensureWindowSize(_ size: CGSize, animated: Bool, force: Bool = false) { + resizeWindows(to: size, animated: animated, force: force) + } + + private func resizeWindows(to size: CGSize, animated: Bool, force: Bool) { + guard size.width > 0, size.height > 0 else { return } + + if Defaults[.showOnAllDisplays] { + for (screen, window) in windows { + if force || window.frame.size != size { + resizeWindow(window, on: screen, to: size, animated: animated) + } + } + } else if let window { + let screen = window.screen ?? NSScreen.screens.first { $0.frame.intersects(window.frame) } ?? NSScreen.main ?? NSScreen.screens.first + guard let screen else { return } + if force || window.frame.size != size { + resizeWindow(window, on: screen, to: size, animated: animated) + } + } + } + + private func resizeWindow(_ window: NSWindow, on screen: NSScreen, to size: CGSize, animated: Bool) { + let screenFrame = screen.frame + let centerX = screenFrame.midX + let newX = centerX - (size.width / 2) + let newY = screenFrame.origin.y + screenFrame.height - size.height + let targetFrame = NSRect(x: newX, y: newY, width: size.width, height: size.height) + + if animated { + NSAnimationContext.runAnimationGroup { context in + context.duration = 0.25 + context.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut) + context.allowsImplicitAnimation = true + window.animator().setFrame(targetFrame, display: true) + } + } else { + window.setFrame(targetFrame, display: true) + } + } + + private func shouldAnimateResize(for newSize: CGSize) -> Bool { + if Defaults[.enableMinimalisticUI] && !ReminderLiveActivityManager.shared.activeWindowReminders.isEmpty { + return false + } + return true + } func applicationDidFinishLaunching(_ notification: Notification) { diff --git a/DynamicIsland/components/Notch/MinimalisticMusicPlayerView.swift b/DynamicIsland/components/Notch/MinimalisticMusicPlayerView.swift index 22eb4a3..5f8b8ee 100644 --- a/DynamicIsland/components/Notch/MinimalisticMusicPlayerView.swift +++ b/DynamicIsland/components/Notch/MinimalisticMusicPlayerView.swift @@ -65,10 +65,7 @@ struct MinimalisticMusicPlayerView: View { playbackControls .padding(.top, 4) - if shouldShowReminderList { - MinimalisticReminderEventListView(reminders: reminderEntries, currentDate: reminderManager.currentDate) - .transition(.opacity.combined(with: .move(edge: .bottom))) - } + reminderList } .padding(.horizontal, 12) .padding(.top, -15) @@ -83,6 +80,18 @@ struct MinimalisticMusicPlayerView: View { private var shouldShowReminderList: Bool { enableReminderLiveActivity && !reminderEntries.isEmpty } + + private var reminderListHeight: CGFloat { + ReminderLiveActivityManager.additionalHeight(forRowCount: reminderEntries.count) + } + + private var reminderList: some View { + MinimalisticReminderEventListView(reminders: reminderEntries, currentDate: reminderManager.currentDate) + .frame(maxWidth: .infinity, alignment: .leading) + .frame(height: reminderListHeight, alignment: .top) + .opacity(shouldShowReminderList ? 1 : 0) + .animation(.easeInOut(duration: 0.18), value: shouldShowReminderList) + } private struct MinimalisticReminderEventListView: View { diff --git a/DynamicIsland/models/DynamicIslandViewModel.swift b/DynamicIsland/models/DynamicIslandViewModel.swift index dcfb9b5..d068625 100644 --- a/DynamicIsland/models/DynamicIslandViewModel.swift +++ b/DynamicIsland/models/DynamicIslandViewModel.swift @@ -44,6 +44,7 @@ class DynamicIslandViewModel: NSObject, ObservableObject { @Published var notchSize: CGSize = getClosedNotchSize() @Published var closedNotchSize: CGSize = getClosedNotchSize() + private var minimalisticReminderRowCount: Int = 0 deinit { destroy() @@ -71,6 +72,19 @@ class DynamicIslandViewModel: NSObject, ObservableObject { .store(in: &cancellables) setupDetectorObserver() + + ReminderLiveActivityManager.shared.$activeWindowReminders + .removeDuplicates() + .receive(on: RunLoop.main) + .sink { [weak self] entries in + guard let self else { return } + self.minimalisticReminderRowCount = entries.count + let updatedTarget = self.calculateDynamicNotchSize() + guard self.notchState == .open else { return } + guard self.notchSize != updatedTarget else { return } + self.notchSize = updatedTarget + } + .store(in: &cancellables) } private func setupDetectorObserver() { @@ -124,8 +138,21 @@ class DynamicIslandViewModel: NSObject, ObservableObject { } func open() { + let targetSize = calculateDynamicNotchSize() + + let applyWindowResize: () -> Void = { + guard let delegate = AppDelegate.shared else { return } + delegate.ensureWindowSize(targetSize, animated: false, force: true) + } + + if Thread.isMainThread { + applyWindowResize() + } else { + DispatchQueue.main.async(execute: applyWindowResize) + } + withAnimation(.bouncy) { - self.notchSize = calculateDynamicNotchSize() + self.notchSize = targetSize self.notchState = .open } @@ -138,8 +165,7 @@ class DynamicIslandViewModel: NSObject, ObservableObject { var baseSize = Defaults[.enableMinimalisticUI] ? minimalisticOpenNotchSize : openNotchSize if Defaults[.enableMinimalisticUI] { - let reminderCount = ReminderLiveActivityManager.shared.activeWindowReminders.count - let extraHeight = ReminderLiveActivityManager.additionalHeight(forRowCount: reminderCount) + let extraHeight = ReminderLiveActivityManager.additionalHeight(forRowCount: minimalisticReminderRowCount) baseSize.height += extraHeight }