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 // MARK: -
struct BricksAppView: View { struct PlayerContentView: View {
@ObservedObject var bridge: PlayerBridge @ObservedObject var bridge: PlayerBridge
@ObservedObject var engine: BricksEngine
@State private var showToolbar = false @State private var showToolbar = false
@State private var isHoveringIcon = false @State private var isHoveringIcon = false
@State private var hideTimer: Timer? @State private var hideTimer: Timer?
@State private var isHoveringToolbar = false @State private var isHoveringToolbar = false
@State private var jsonString: String?
private func resetHideTimer() { private func resetHideTimer() {
hideTimer?.invalidate() hideTimer?.invalidate()
@ -188,6 +186,12 @@ struct BricksAppView: View {
// AVPlayerLayer AVPlayerView // AVPlayerLayer AVPlayerView
PlayerLayerView(player: bridge.player) PlayerLayerView(player: bridge.player)
.frame(maxWidth: .infinity, maxHeight: .infinity) .frame(maxWidth: .infinity, maxHeight: .infinity)
.onTapGesture(count: 2) {
bridge.toggleFullscreen()
}
.onTapGesture {
bridge.togglePlayPause()
}
// //
MiniPlayerIcon() MiniPlayerIcon()
@ -239,24 +243,25 @@ struct BricksAppView: View {
.onDisappear { .onDisappear {
hideTimer?.invalidate() 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 { var body: some View {
if let url = Bundle.module.url(forResource: "miniplayer", withExtension: "ui"), PlayerContentView(bridge: bridge)
let data = try? Data(contentsOf: url), .sheet(isPresented: $bridge.showURLDialog) {
let str = String(data: data, encoding: .utf8) { URLInputDialog(bridge: bridge)
return str }
} .sheet(isPresented: $bridge.showTrackDialog) {
return "{\"widgettype\":\"VideoPlayer\",\"id\":\"videoPlayer\",\"options\":{\"width\":\"100%\",\"height\":\"100%\"}}" TrackSelectDialog(bridge: bridge)
}
.sheet(isPresented: $bridge.showPlaylistSheet) {
PlaylistWindowView(bridge: bridge)
}
} }
} }

View File

@ -279,7 +279,7 @@ final class PlayerBridge: ObservableObject {
// MARK: - // MARK: -
// orderFront/orderOut // orderFront/orderOut
// AVPlayerView CoreMedia weak listener // 使 NSHostingView + SwiftUI
func toggleFullscreen() { func toggleFullscreen() {
#if os(macOS) #if os(macOS)
if isFullscreen { if isFullscreen {
@ -294,7 +294,7 @@ final class PlayerBridge: ObservableObject {
return return
} }
// AVPlayerView // SwiftUI
if fullscreenWindow == nil { if fullscreenWindow == nil {
guard let screen = NSScreen.main else { return } guard let screen = NSScreen.main else { return }
let win = NSWindow(contentRect: screen.frame, styleMask: .borderless, backing: .buffered, defer: false) let win = NSWindow(contentRect: screen.frame, styleMask: .borderless, backing: .buffered, defer: false)
@ -304,11 +304,10 @@ final class PlayerBridge: ObservableObject {
win.hasShadow = false win.hasShadow = false
win.isReleasedWhenClosed = false win.isReleasedWhenClosed = false
let playerView = AVPlayerView(frame: screen.frame) // SwiftUI PlayerContentView +
playerView.player = player let hostingView = NSHostingView(rootView: PlayerContentView(bridge: self))
playerView.controlsStyle = .none hostingView.frame = screen.frame
playerView.videoGravity = .resizeAspect win.contentView = hostingView
win.contentView = playerView
fullscreenWindow = win fullscreenWindow = win
} }
@ -316,25 +315,13 @@ final class PlayerBridge: ObservableObject {
NSApp.activate(ignoringOtherApps: true) NSApp.activate(ignoringOtherApps: true)
isFullscreen = true isFullscreen = true
// 退/Escape退 // Escape 退
if fullscreenEventMonitor == nil { 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 let self = self, self.isFullscreen else { return event }
guard event.window === self.fullscreenWindow else { return event } guard event.window === self.fullscreenWindow else { return event }
if event.type == .leftMouseDown { if event.type == .keyDown && event.keyCode == 53 { // Escape
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 {
DispatchQueue.main.async { self.toggleFullscreen() } DispatchQueue.main.async { self.toggleFullscreen() }
return nil return nil
} }