diff --git a/Package.swift b/Package.swift index 4fb3587..2d0c188 100644 --- a/Package.swift +++ b/Package.swift @@ -8,14 +8,15 @@ let package = Package( .iOS(.v17), .macOS(.v14) ], - products: [ - .executable(name: "MiniPlayer", targets: ["MiniPlayer"]) + dependencies: [ + .package(path: "../swiftbricks") ], - dependencies: [], targets: [ .executableTarget( name: "MiniPlayer", - dependencies: [], + dependencies: [ + .product(name: "SwiftBricks", package: "swiftbricks") + ], resources: [.process("Resources")] ) ] diff --git a/Resources/miniplayer.json b/Resources/miniplayer.json new file mode 100644 index 0000000..9603d38 --- /dev/null +++ b/Resources/miniplayer.json @@ -0,0 +1,124 @@ +{ + "widgettype": "VBox", + "options": { + "width": "100%", + "height": "100%", + "bgcolor": "#000000" + }, + "subwidgets": [ + { + "widgettype": "VideoPlayer", + "id": "videoPlayer", + "options": { + "width": "100%", + "height": "85%" + } + }, + { + "widgettype": "HBox", + "options": { + "width": "100%", + "height": "15%", + "bgcolor": "#1a1a1a", + "spacing": 10, + "padding": "10px" + }, + "subwidgets": [ + { + "widgettype": "Button", + "id": "playBtn", + "options": { + "label": "Play", + "icon": "play.fill", + "cwidth": 8, + "cheight": 4, + "bgcolor": "#007aff", + "color": "#ffffff" + }, + "binds": [ + { + "wid": "self", + "event": "click", + "actiontype": "event", + "target": "videoPlayer.play" + } + ] + }, + { + "widgettype": "Button", + "id": "pauseBtn", + "options": { + "label": "Pause", + "icon": "pause.fill", + "cwidth": 8, + "cheight": 4, + "bgcolor": "#ff9500", + "color": "#ffffff" + }, + "binds": [ + { + "wid": "self", + "event": "click", + "actiontype": "event", + "target": "videoPlayer.pause" + } + ] + }, + { + "widgettype": "Button", + "id": "toggleBtn", + "options": { + "label": "Toggle", + "icon": "playpause.fill", + "cwidth": 10, + "cheight": 4, + "bgcolor": "#34c759", + "color": "#ffffff" + }, + "binds": [ + { + "wid": "self", + "event": "click", + "actiontype": "event", + "target": "videoPlayer.toggle" + } + ] + }, + { + "widgettype": "Filler" + }, + { + "widgettype": "Input", + "id": "videoUrlInput", + "options": { + "name": "url", + "placeholder": "Enter video URL", + "cwidth": 30, + "cheight": 4 + } + }, + { + "widgettype": "Button", + "id": "loadBtn", + "options": { + "label": "Load", + "icon": "arrow.down.circle.fill", + "cwidth": 8, + "cheight": 4, + "bgcolor": "#5856d6", + "color": "#ffffff" + }, + "binds": [ + { + "wid": "self", + "event": "click", + "actiontype": "method", + "target": "videoPlayer", + "method": "setURL" + } + ] + } + ] + } + ] +} diff --git a/Sources/MiniPlayerApp.swift b/Sources/MiniPlayerApp.swift index 47454dd..102b1fa 100644 --- a/Sources/MiniPlayerApp.swift +++ b/Sources/MiniPlayerApp.swift @@ -1,16 +1,21 @@ import SwiftUI import AVFoundation import Combine +import SwiftBricks @main struct MiniPlayerApp: App { @StateObject private var bridge = PlayerBridge() + @StateObject private var engine = BricksEngine() var body: some Scene { WindowGroup { - ContentView(bridge: bridge) + BricksAppView(bridge: bridge, engine: engine) .frame(minWidth: 900, minHeight: 600) - .onAppear { bridge.setup() } + .onAppear { + bridge.setup() + setupBridgeEvents() + } } #if os(macOS) .commands { @@ -45,79 +50,81 @@ struct MiniPlayerApp: App { } #endif } + + // 桥接 BricksEngine 事件到 PlayerBridge + private func setupBridgeEvents() { + // 将 PlayerBridge 的 AVPlayer 注入 Store,让 VideoPlayerControl 共享 + engine.store.setValue(id: "videoPlayer.player", value: bridge.player) + + // 播放控制 — 从 Store 读取 Input 值或事件数据 + engine.eventBus.on("videoPlayer.setURL") { [weak bridge, weak engine] data in + guard let bridge, let engine else { return } + // 优先从事件数据取 URL,否则从 Input widget 的 store 值取 + var url: String? = data["url"] as? String + if url == nil || url?.isEmpty == true { + url = engine.store.getValue(id: "videoUrlInput") as? String + } + if let url, !url.isEmpty { + bridge.addURL(url) + } + } + + // seek — 直接操作共享 AVPlayer + engine.eventBus.on("videoPlayer.seek") { [weak bridge] data in + guard let bridge else { return } + if let timeStr = data["time"] as? String, let time = Double(timeStr) { + bridge.player.seek(to: CMTime(seconds: time, preferredTimescale: 600)) + } + } + + // 音量 + engine.eventBus.on("videoPlayer.setVolume") { [weak bridge] data in + guard let bridge else { return } + if let volStr = data["volume"] as? String, let vol = Float(volStr) { + bridge.setVolume(vol) + } + } + + // 全屏 + engine.eventBus.on("videoPlayer.fullscreen") { [weak bridge] data in + guard let bridge else { return } + bridge.toggleFullscreen() + } + + // 上一曲/下一曲 + engine.eventBus.on("videoPlayer.prev") { [weak bridge] data in + guard let bridge else { return } + bridge.playPrev() + } + + engine.eventBus.on("videoPlayer.next") { [weak bridge] data in + guard let bridge else { return } + bridge.playNext() + } + + // 打开文件 + engine.eventBus.on("videoPlayer.openFile") { [weak bridge] data in + guard let bridge else { return } + bridge.openFileDialog() + } + } } -// MARK: - 主内容视图 -struct ContentView: View { +// MARK: - BricksView wrapper +struct BricksAppView: View { @ObservedObject var bridge: PlayerBridge - // 自动隐藏:本地状态,不经过 bridge 的 @Published - @State private var toolbarVisible = false - @State private var isHoveringToolbar = false - @State private var lastInteraction = Date() - private let hideTimer = Timer.publish(every: 2, on: .main, in: .common).autoconnect() - private let autoHideInterval: TimeInterval = 60 + @ObservedObject var engine: BricksEngine + @State private var jsonString: String? var body: some View { ZStack { - // 视频占满全部 - VideoPlayerRepresentable(player: bridge.player) - .background(Color.black) - .frame(maxWidth: .infinity, maxHeight: .infinity) - .ignoresSafeArea() - - // 单击播放/暂停 + 双击全屏 - Color.clear - .contentShape(Rectangle()) - .onTapGesture { - bridge.togglePlayPause() - touchInteraction() - } - #if os(macOS) - .onTapGesture(count: 2) { bridge.toggleFullscreen() } - #endif - .ignoresSafeArea() - - // Logo(左上角,始终显示) - VStack { - HStack { - Button(action: { - toolbarVisible.toggle() - if toolbarVisible { touchInteraction() } - }) { - MiniPlayerIcon() - .frame(width: 36, height: 36) - .padding(6) - .background(.black.opacity(0.4)) - .cornerRadius(10) + if let json = jsonString { + BricksView(json: json, engine: engine) + } else { + ProgressView("Loading UI...") + .task { + jsonString = loadJSONString() } - .buttonStyle(.plain) - Spacer() - } - .padding(.leading, 16) - .padding(.top, 12) - Spacer() - } - - // 底部 Toolbar(半透明) - if toolbarVisible { - VStack { - Spacer() - ControlToolbar(bridge: bridge, isHovering: $isHoveringToolbar, onTouch: touchInteraction) - .transition(.move(edge: .bottom).combined(with: .opacity)) - } - } - - // Toast - if let msg = bridge.toastMessage { - VStack { - Spacer() - Text(msg) - .padding(8) - .background(.black.opacity(0.7)) - .foregroundColor(.white) - .cornerRadius(6) - .padding(.bottom, 100) - } } } .sheet(isPresented: $bridge.showURLDialog) { @@ -129,23 +136,24 @@ struct ContentView: View { .sheet(isPresented: $bridge.showPlaylistSheet) { PlaylistWindowView(bridge: bridge) } - .animation(.easeInOut(duration: 0.25), value: toolbarVisible) - // 自动隐藏 Timer:每2秒检查一次 - .onReceive(hideTimer) { _ in - guard toolbarVisible else { return } - if isHoveringToolbar { lastInteraction = Date(); return } - if Date().timeIntervalSince(lastInteraction) >= autoHideInterval { - toolbarVisible = false - } - } - } - private func touchInteraction() { - lastInteraction = Date() + private func loadJSONString() -> String { + // 从 Resources 加载 miniplayer.json + if let url = Bundle.module.url(forResource: "miniplayer", withExtension: "json"), + let data = try? Data(contentsOf: url), + let str = String(data: data, encoding: .utf8) { + return str + } + + // Fallback: 最小化 schema + return """ + {"widgettype":"VBox","options":{"width":"100%","height":"100%"},"subwidgets":[{"widgettype":"Text","options":{"text":"Failed to load UI schema"}}]} + """ } } + // MARK: - 底部控制栏 struct ControlToolbar: View { @ObservedObject var bridge: PlayerBridge diff --git a/Sources/Resources/miniplayer.json b/Sources/Resources/miniplayer.json new file mode 100644 index 0000000..de8f1b5 --- /dev/null +++ b/Sources/Resources/miniplayer.json @@ -0,0 +1,164 @@ +{ + "widgettype": "VBox", + "options": { + "width": "100%", + "height": "100%", + "bgcolor": "#000000" + }, + "subwidgets": [ + { + "widgettype": "VideoPlayer", + "id": "videoPlayer", + "options": { + "width": "100%", + "height": "85%" + } + }, + { + "widgettype": "HBox", + "options": { + "width": "100%", + "height": "15%", + "bgcolor": "#1a1a1a", + "spacing": 6, + "padding": "8px 12px" + }, + "subwidgets": [ + { + "widgettype": "Button", + "id": "openBtn", + "options": { + "label": "Open", + "icon": "folder", + "cwidth": 8, + "cheight": 4, + "bgcolor": "#555555", + "color": "#ffffff" + }, + "binds": [ + { + "wid": "self", + "event": "click", + "actiontype": "event", + "target": "videoPlayer.openFile" + } + ] + }, + { + "widgettype": "Button", + "id": "prevBtn", + "options": { + "label": "Prev", + "icon": "backward.fill", + "cwidth": 8, + "cheight": 4, + "bgcolor": "#333333", + "color": "#ffffff" + }, + "binds": [ + { + "wid": "self", + "event": "click", + "actiontype": "event", + "target": "videoPlayer.prev" + } + ] + }, + { + "widgettype": "Button", + "id": "toggleBtn", + "options": { + "label": "Play/Pause", + "icon": "playpause.fill", + "cwidth": 12, + "cheight": 4, + "bgcolor": "#007aff", + "color": "#ffffff" + }, + "binds": [ + { + "wid": "self", + "event": "click", + "actiontype": "event", + "target": "videoPlayer.toggle" + } + ] + }, + { + "widgettype": "Button", + "id": "nextBtn", + "options": { + "label": "Next", + "icon": "forward.fill", + "cwidth": 8, + "cheight": 4, + "bgcolor": "#333333", + "color": "#ffffff" + }, + "binds": [ + { + "wid": "self", + "event": "click", + "actiontype": "event", + "target": "videoPlayer.next" + } + ] + }, + { + "widgettype": "Filler" + }, + { + "widgettype": "Input", + "id": "videoUrlInput", + "options": { + "name": "url", + "placeholder": "Enter video URL...", + "cwidth": 30, + "cheight": 4 + } + }, + { + "widgettype": "Button", + "id": "loadBtn", + "options": { + "label": "Load", + "icon": "arrow.down.circle.fill", + "cwidth": 8, + "cheight": 4, + "bgcolor": "#5856d6", + "color": "#ffffff" + }, + "binds": [ + { + "wid": "self", + "event": "click", + "actiontype": "method", + "target": "videoPlayer", + "method": "setURL" + } + ] + }, + { + "widgettype": "Button", + "id": "fullscreenBtn", + "options": { + "label": "Fullscreen", + "icon": "arrow.up.left.and.arrow.down.right", + "cwidth": 10, + "cheight": 4, + "bgcolor": "#ff9500", + "color": "#ffffff" + }, + "binds": [ + { + "wid": "self", + "event": "click", + "actiontype": "event", + "target": "videoPlayer.fullscreen" + } + ] + } + ] + } + ] +}