From f303832a1b69d481c8cf3eac74bdbdd3b86dcdad Mon Sep 17 00:00:00 2001 From: yumoqing Date: Tue, 23 Jun 2026 09:02:08 +0800 Subject: [PATCH] feat: fullscreen uses SwiftUI with icon+toolbar, double-click toggle, single-click play/pause --- Sources/MiniPlayerApp.swift | 45 ++++++++++++++++++++----------------- Sources/PlayerBridge.swift | 31 ++++++++----------------- 2 files changed, 34 insertions(+), 42 deletions(-) diff --git a/Sources/MiniPlayerApp.swift b/Sources/MiniPlayerApp.swift index f336ac4..ce2b68b 100644 --- a/Sources/MiniPlayerApp.swift +++ b/Sources/MiniPlayerApp.swift @@ -162,15 +162,13 @@ class PlayerLayerHostView: NSView { } } -// MARK: - BricksView wrapper -struct BricksAppView: View { +// MARK: - 可复用的播放器内容视图(主窗口和全屏共用) +struct PlayerContentView: View { @ObservedObject var bridge: PlayerBridge - @ObservedObject var engine: BricksEngine @State private var showToolbar = false @State private var isHoveringIcon = false @State private var hideTimer: Timer? @State private var isHoveringToolbar = false - @State private var jsonString: String? private func resetHideTimer() { hideTimer?.invalidate() @@ -188,6 +186,12 @@ struct BricksAppView: View { // 直接用 AVPlayerLayer 渲染视频(彻底绕过 AVPlayerView 内置控制栏) PlayerLayerView(player: bridge.player) .frame(maxWidth: .infinity, maxHeight: .infinity) + .onTapGesture(count: 2) { + bridge.toggleFullscreen() + } + .onTapGesture { + bridge.togglePlayPause() + } // 左上角应用图标 — 点击切换控制栏 MiniPlayerIcon() @@ -239,24 +243,25 @@ struct BricksAppView: View { .onDisappear { hideTimer?.invalidate() } - .sheet(isPresented: $bridge.showURLDialog) { - URLInputDialog(bridge: bridge) - } - .sheet(isPresented: $bridge.showTrackDialog) { - TrackSelectDialog(bridge: bridge) - } - .sheet(isPresented: $bridge.showPlaylistSheet) { - PlaylistWindowView(bridge: bridge) - } } +} + +// MARK: - BricksView wrapper +struct BricksAppView: View { + @ObservedObject var bridge: PlayerBridge + @ObservedObject var engine: BricksEngine - private func loadJSONString() -> String { - if let url = Bundle.module.url(forResource: "miniplayer", withExtension: "ui"), - let data = try? Data(contentsOf: url), - let str = String(data: data, encoding: .utf8) { - return str - } - return "{\"widgettype\":\"VideoPlayer\",\"id\":\"videoPlayer\",\"options\":{\"width\":\"100%\",\"height\":\"100%\"}}" + var body: some View { + PlayerContentView(bridge: bridge) + .sheet(isPresented: $bridge.showURLDialog) { + URLInputDialog(bridge: bridge) + } + .sheet(isPresented: $bridge.showTrackDialog) { + TrackSelectDialog(bridge: bridge) + } + .sheet(isPresented: $bridge.showPlaylistSheet) { + PlaylistWindowView(bridge: bridge) + } } } diff --git a/Sources/PlayerBridge.swift b/Sources/PlayerBridge.swift index 3485b56..50416e1 100644 --- a/Sources/PlayerBridge.swift +++ b/Sources/PlayerBridge.swift @@ -279,7 +279,7 @@ final class PlayerBridge: ObservableObject { // MARK: - 全屏(视频内容全屏,非窗口全屏) // 全屏窗口只创建一次,之后只用 orderFront/orderOut 切换显示 - // AVPlayerView 永不销毁,彻底避免 CoreMedia weak listener 竞态 + // 使用 NSHostingView + SwiftUI,与主窗口共享相同的图标和控制栏 func toggleFullscreen() { #if os(macOS) if isFullscreen { @@ -294,7 +294,7 @@ final class PlayerBridge: ObservableObject { return } - // 首次全屏:创建窗口和 AVPlayerView(仅此一次) + // 首次全屏:创建窗口和 SwiftUI 内容(仅此一次) if fullscreenWindow == nil { guard let screen = NSScreen.main else { return } let win = NSWindow(contentRect: screen.frame, styleMask: .borderless, backing: .buffered, defer: false) @@ -304,11 +304,10 @@ final class PlayerBridge: ObservableObject { win.hasShadow = false win.isReleasedWhenClosed = false - let playerView = AVPlayerView(frame: screen.frame) - playerView.player = player - playerView.controlsStyle = .none - playerView.videoGravity = .resizeAspect - win.contentView = playerView + // 用 SwiftUI PlayerContentView 渲染全屏(含图标+控制栏) + let hostingView = NSHostingView(rootView: PlayerContentView(bridge: self)) + hostingView.frame = screen.frame + win.contentView = hostingView fullscreenWindow = win } @@ -316,25 +315,13 @@ final class PlayerBridge: ObservableObject { NSApp.activate(ignoringOtherApps: true) isFullscreen = true - // 事件监控:双击退出、单击暂停/播放、Escape退出 + // Escape 键退出全屏 if fullscreenEventMonitor == nil { - fullscreenEventMonitor = NSEvent.addLocalMonitorForEvents(matching: [.leftMouseDown, .keyDown]) { [weak self] event in + fullscreenEventMonitor = NSEvent.addLocalMonitorForEvents(matching: [.keyDown]) { [weak self] event in guard let self = self, self.isFullscreen else { return event } guard event.window === self.fullscreenWindow else { return event } - if event.type == .leftMouseDown { - if event.clickCount >= 2 { - DispatchQueue.main.async { self.toggleFullscreen() } - return nil - } else { - if self.player.timeControlStatus == .playing { - self.player.pause() - } else { - self.player.play() - } - return nil - } - } else if event.type == .keyDown && event.keyCode == 53 { + if event.type == .keyDown && event.keyCode == 53 { // Escape DispatchQueue.main.async { self.toggleFullscreen() } return nil }