// // DynamicIslandViewModel.swift // DynamicIsland // // Created by Harsh Vardhan Goswami on 04/08/24. // import Combine import Defaults import SwiftUI import TheBoringWorkerNotifier class DynamicIslandViewModel: NSObject, ObservableObject { @ObservedObject var coordinator = DynamicIslandViewCoordinator.shared @ObservedObject var detector = FullscreenMediaDetector.shared let animationLibrary: DynamicIslandAnimations = .init() let animation: Animation? @Published var contentType: ContentType = .normal @Published private(set) var notchState: NotchState = .closed @Published var dragDetectorTargeting: Bool = false @Published var dropZoneTargeting: Bool = false @Published var dropEvent: Bool = false @Published var anyDropZoneTargeting: Bool = false var cancellables: Set = [] @Published var hideOnClosed: Bool = true @Published var isHoveringCalendar: Bool = false @Published var isBatteryPopoverActive: Bool = false @Published var screen: String? @Published var notchSize: CGSize = getClosedNotchSize() @Published var closedNotchSize: CGSize = getClosedNotchSize() deinit { destroy() } func destroy() { cancellables.forEach { $0.cancel() } cancellables.removeAll() } init(screen: String? = nil) { animation = animationLibrary.animation super.init() self.screen = screen notchSize = getClosedNotchSize(screen: screen) closedNotchSize = notchSize Publishers.CombineLatest($dropZoneTargeting, $dragDetectorTargeting) .map { value1, value2 in value1 || value2 } .assign(to: \.anyDropZoneTargeting, on: self) .store(in: &cancellables) setupDetectorObserver() } private func setupDetectorObserver() { // 1) Publisher for the user’s fullscreen detection setting let enabledPublisher = Defaults .publisher(.enableFullscreenMediaDetection) .map(\.newValue) // 2) For each non‑nil screen name, map to a Bool publisher for that screen's status let statusPublisher = $screen .compactMap { $0 } .removeDuplicates() .map { screenName in self.detector.$fullscreenStatus .map { $0[screenName] ?? false } .removeDuplicates() } .switchToLatest() // 3) Combine enabled & status, animate only on changes Publishers.CombineLatest(statusPublisher, enabledPublisher) .map { status, enabled in enabled && status } .removeDuplicates() .receive(on: RunLoop.main) .sink { [weak self] shouldHide in withAnimation(.smooth) { self?.hideOnClosed = shouldHide } } .store(in: &cancellables) } // Computed property for effective notch height var effectiveClosedNotchHeight: CGFloat { let currentScreen = NSScreen.screens.first { $0.localizedName == screen } let noNotchAndFullscreen = hideOnClosed && (currentScreen?.safeAreaInsets.top ?? 0 <= 0 || currentScreen == nil) return noNotchAndFullscreen ? 0 : closedNotchSize.height } func isMouseHovering(position: NSPoint = NSEvent.mouseLocation) -> Bool { let screenFrame = getScreenFrame(screen) if let frame = screenFrame { let baseY = frame.maxY - notchSize.height let baseX = frame.midX - notchSize.width / 2 return position.y >= baseY && position.x >= baseX && position.x <= baseX + notchSize.width } return false } func open() { withAnimation(.bouncy) { self.notchSize = openNotchSize self.notchState = .open } // Force music information update when notch is opened MusicManager.shared.forceUpdate() } func close() { withAnimation(.smooth) { [weak self] in guard let self = self else { return } self.notchSize = getClosedNotchSize(screen: self.screen) self.closedNotchSize = self.notchSize self.notchState = .closed } // Set the current view to shelf if it contains files and the user enables openShelfByDefault // Otherwise, if the user has not enabled openLastShelfByDefault, set the view to home if !TrayDrop.shared.isEmpty && Defaults[.openShelfByDefault] { coordinator.currentView = .shelf } else if !coordinator.openLastTabByDefault { coordinator.currentView = .home } } func closeHello() { DispatchQueue.main.asyncAfter(deadline: .now() + 3.5) { [weak self] in self?.coordinator.firstLaunch = false withAnimation(self?.animationLibrary.animation) { self?.close() } } } }