feat: fullscreen uses SwiftUI with icon+toolbar, double-click toggle, single-click play/pause

This commit is contained in:
yumoqing 2026-06-23 09:02:08 +08:00
parent 4879f56ef1
commit f303832a1b
2 changed files with 34 additions and 42 deletions

View File

@ -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)
}
}
}
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%\"}}"
// MARK: - BricksView wrapper
struct BricksAppView: View {
@ObservedObject var bridge: PlayerBridge
@ObservedObject var engine: BricksEngine
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)
}
}
}

View File

@ -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
}