diff --git a/Front Row/FrontRowApp.swift b/Front Row/FrontRowApp.swift index 85536ed..f57514c 100644 --- a/Front Row/FrontRowApp.swift +++ b/Front Row/FrontRowApp.swift @@ -5,6 +5,7 @@ // Created by Joshua Park on 3/4/24. // +import AVKit import Sparkle import SwiftUI @@ -37,7 +38,12 @@ struct FrontRowApp: App { Window("Front Row", id: "main") { ContentView() .preferredColorScheme(.dark) + .ignoresSafeArea() .environment(playEngine) + .navigationTitle(playEngine.fileURL?.lastPathComponent ?? "Front Row") + .if(playEngine.isLocalFile) { view in + view.navigationDocument(playEngine.fileURL!) + } .sheet(isPresented: $presentedViewManager.isPresentingOpenURLView) { OpenURLView() .frame(minWidth: 600) @@ -68,7 +74,6 @@ struct FrontRowApp: App { windowController.setIsFullscreen(false) } } - .windowStyle(.hiddenTitleBar) .restorationBehavior(.disabled) .commands { AppCommands(updater: updaterController.updater) diff --git a/Front Row/Support/Extensions.swift b/Front Row/Support/Extensions.swift index 47d71d7..fadd57b 100644 --- a/Front Row/Support/Extensions.swift +++ b/Front Row/Support/Extensions.swift @@ -9,6 +9,17 @@ import AVKit import Foundation import SwiftUI +extension View { + @ViewBuilder + func `if`(_ condition: Bool, transform: (Self) -> Content) -> some View { + if condition { + transform(self) + } else { + self + } + } +} + struct AnyDropDelegate: DropDelegate { var isTargeted: Binding? var onValidate: ((DropInfo) -> Bool)? diff --git a/Front Row/Support/PlayEngine.swift b/Front Row/Support/PlayEngine.swift index 8f383a1..ba83c87 100644 --- a/Front Row/Support/PlayEngine.swift +++ b/Front Row/Support/PlayEngine.swift @@ -39,6 +39,8 @@ import SwiftUI private(set) var isLocalFile = false + private(set) var fileURL: URL? + private var _currentTime: TimeInterval = 0.0 var currentTime: Double { @@ -229,6 +231,7 @@ import SwiftUI isLoaded = true isLocalFile = FileManager.default.fileExists( atPath: url.path(percentEncoded: false)) + fileURL = url NowPlayable.shared.setNowPlayingMetadata( NowPlayableStaticMetadata( assetURL: url, mediaType: videoSize == CGSize.zero ? .audio : .video, @@ -237,6 +240,7 @@ import SwiftUI case .failed: isLoaded = false isLocalFile = false + fileURL = nil NowPlayable.shared.sessionEnd() default: break diff --git a/Front Row/Support/WindowController.swift b/Front Row/Support/WindowController.swift index 1cad76d..48b0db7 100644 --- a/Front Row/Support/WindowController.swift +++ b/Front Row/Support/WindowController.swift @@ -11,6 +11,54 @@ import SwiftUI static let shared = WindowController() + private var mouseMovedMonitor: Any? + + // MARK: - Mouse Tracking + + private(set) var isMouseInTitleBar = false + var isMouseInPlayerControls = false + + init() { + setupMouseTracking() + } + + deinit { + if let monitor = mouseMovedMonitor { + NSEvent.removeMonitor(monitor) + } + } + + private func setupMouseTracking() { + mouseMovedMonitor = NSEvent.addLocalMonitorForEvents(matching: [.mouseMoved]) { + [weak self] event in + self?.updateMousePosition() + return event + } + } + + private func updateMousePosition() { + guard let window = NSApp.mainWindow else { + isMouseInTitleBar = false + return + } + + let mouseLocation = NSEvent.mouseLocation + let windowFrame = window.frame + + guard windowFrame.contains(mouseLocation) else { + isMouseInTitleBar = false + return + } + + // Convert screen point to window coordinates + let windowPoint = window.convertPoint(fromScreen: mouseLocation) + // contentLayoutRect excludes the title bar area + let contentRect = window.contentLayoutRect + + // Mouse is in title bar if it's above the content rect + isMouseInTitleBar = windowPoint.y > contentRect.maxY + } + // MARK: - Fullscreen private(set) var isFullscreen = false diff --git a/Front Row/Views/ContentView.swift b/Front Row/Views/ContentView.swift index 41a5bd2..50b7e3b 100644 --- a/Front Row/Views/ContentView.swift +++ b/Front Row/Views/ContentView.swift @@ -50,6 +50,18 @@ struct ContentView: View { } PlayerControlsView() + .onContinuousHover { phase in + switch phase { + case .active: + WindowController.shared.isMouseInPlayerControls = true + resetMouseIdleTimer() + showPlayerControls() + WindowController.shared.showTitlebar() + WindowController.shared.showCursor() + case .ended: + WindowController.shared.isMouseInPlayerControls = false + } + } .animation(.linear(duration: 0.4), value: playerControlsShown) .opacity(playerControlsShown ? 1.0 : 0.0) } @@ -66,11 +78,25 @@ struct ContentView: View { WindowController.shared.showCursor() case .ended: mouseInsideWindow = false - hidePlayerControls() - WindowController.shared.hideTitlebar() + // Only hide if mouse is not hovering over title bar or controls + let isHoveringInteractiveArea = + WindowController.shared.isMouseInTitleBar + || WindowController.shared.isMouseInPlayerControls + if !isHoveringInteractiveArea { + hidePlayerControls() + WindowController.shared.hideTitlebar() + } WindowController.shared.showCursor() } } + .onChange(of: WindowController.shared.isMouseInTitleBar) { _, isInTitleBar in + // When mouse enters title bar, show controls and reset idle timer + if isInTitleBar { + showPlayerControls() + WindowController.shared.showTitlebar() + resetMouseIdleTimer() + } + } } private func hidePlayerControls() { @@ -97,9 +123,17 @@ struct ContentView: View { } private func mouseIdleTimerAction(_ sender: Timer) { - hidePlayerControls() - WindowController.shared.hideTitlebar() - if mouseInsideWindow { + let isHoveringInteractiveArea = + WindowController.shared.isMouseInTitleBar + || WindowController.shared.isMouseInPlayerControls + + // Only hide controls if mouse is not hovering over title bar or controls + if !isHoveringInteractiveArea { + hidePlayerControls() + WindowController.shared.hideTitlebar() + } + // Only hide cursor if mouse is in content area (not title bar or controls) + if mouseInsideWindow && !isHoveringInteractiveArea { WindowController.shared.hideCursor() } }