import SwiftUI import SwiftBricks import AVFoundation @main struct MiniPlayerApp: App { @StateObject private var bridge = PlayerBridge() var body: some Scene { WindowGroup { ContentView(bridge: bridge) .frame(minWidth: 900, minHeight: 600) .onAppear { bridge.setup() } } #if os(macOS) .commands { CommandGroup(replacing: .newItem) {} CommandMenu("播放") { Button("播放/暂停") { bridge.togglePlayPause() } .keyboardShortcut(" ", modifiers: []) Divider() Button("上一首") { bridge.playPrev() } .keyboardShortcut("[", modifiers: []) Button("下一首") { bridge.playNext() } .keyboardShortcut("]", modifiers: []) Divider() Button("全屏") { bridge.toggleFullscreen() } .keyboardShortcut("f", modifiers: .command) Divider() Button("循环模式") { bridge.cycleRepeatMode() } .keyboardShortcut("r", modifiers: .command) } CommandMenu("文件") { Button("打开文件...") { bridge.openFileDialog() } .keyboardShortcut("o", modifiers: .command) Divider() Button("添加URL...") { bridge.showURLDialog = true } .keyboardShortcut("u", modifiers: .command) } } #endif } } // MARK: - 主内容视图 struct ContentView: View { @ObservedObject var bridge: PlayerBridge var body: some View { ZStack { // 视频背景 if let engine = bridge.engine, let schema = bridge.schema { BricksView(schema: schema, engine: engine) } else { Color.black } // Logo 图标(左上角) VStack { HStack { Button(action: { bridge.showToolbar.toggle() }) { Text("🎬") .font(.system(size: 28)) .padding(8) .background(.black.opacity(0.4)) .cornerRadius(10) } .buttonStyle(.plain) Spacer() } .padding(.leading, 16) .padding(.top, 12) Spacer() } // 底部 Toolbar(半透明) if bridge.showToolbar { VStack { Spacer() ControlToolbar(bridge: bridge) .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) } } } .ignoresSafeArea() .sheet(isPresented: $bridge.showURLDialog) { URLInputDialog(bridge: bridge) } #if os(macOS) .sheet(isPresented: $bridge.showTrackDialog) { TrackSelectDialog(bridge: bridge) } #endif .animation(.easeInOut(duration: 0.25), value: bridge.showToolbar) } } // MARK: - 底部控制栏 struct ControlToolbar: View { @ObservedObject var bridge: PlayerBridge var body: some View { VStack(spacing: 6) { // 进度条行 HStack(spacing: 8) { Text(bridge.currentTimeText) .font(.system(size: 12, design: .monospaced)) .foregroundColor(.white) .frame(width: 44, alignment: .trailing) ProgressSliderWidget(bridge: bridge, schema: dummySchema, engine: bridge.engine!) .frame(height: 20) Text(bridge.totalTimeText) .font(.system(size: 12, design: .monospaced)) .foregroundColor(.white) .frame(width: 44, alignment: .leading) } // 按钮行 HStack(spacing: 10) { ToolbarButton(label: "📂 打开") { bridge.openFileDialog() } ToolbarButton(label: "⏮") { bridge.playPrev() } ToolbarButton(label: bridge.isPlaying ? "⏸" : "▶️") { bridge.togglePlayPause() } ToolbarButton(label: "⏭") { bridge.playNext() } Spacer() Text(bridge.currentTrackLabel) .font(.system(size: 11)) .foregroundColor(.white.opacity(0.7)) ToolbarButton(label: "🎵 音轨") { bridge.showTrackDialog = true } ToolbarButton(label: "\(bridge.repeatMode.icon) \(bridge.repeatMode.rawValue)") { bridge.cycleRepeatMode() } ToolbarButton(label: "⛶") { bridge.toggleFullscreen() } ToolbarButton(label: "📋 列表 (\(bridge.queue.count))") { bridge.togglePlaylistWindow() } } } .padding(.horizontal, 16) .padding(.vertical, 8) .background(.black.opacity(0.55)) } private var dummySchema: ControlSchema { ControlSchema(id: "progress_slider", widgettype: "ProgressSlider", options: ControlOptions(), binds: nil, subwidgets: nil) } } // MARK: - 半透明按钮 struct ToolbarButton: View { let label: String let action: () -> Void @State private var hovering = false var body: some View { Button(action: action) { Text(label) .font(.system(size: 13)) .foregroundColor(.white) .padding(.horizontal, 8) .padding(.vertical, 4) .background(hovering ? .white.opacity(0.15) : .clear) .cornerRadius(4) } .buttonStyle(.plain) .onHover { hovering = $0 } } } // MARK: - 播放列表弹窗 struct PlaylistWindowView: View { @ObservedObject var bridge: PlayerBridge var body: some View { VStack(spacing: 0) { // 顶部操作栏 HStack { Button("📂 添加文件") { bridge.openFileDialog() } Button("🔗 添加URL") { bridge.showURLDialog = true } Spacer() Text("\(bridge.queue.count) 项") .font(.caption) .foregroundColor(.secondary) } .padding(12) .background(.regularMaterial) Divider() // 列表 if bridge.queue.isEmpty { VStack { Spacer() Text("暂无媒体,请添加文件或URL") .foregroundColor(.secondary) Spacer() } } else { ScrollView { LazyVStack(spacing: 2) { ForEach(Array(bridge.queue.enumerated()), id: \.element.id) { idx, item in HStack { if idx == bridge.currentIndex { Text("▶").foregroundColor(.accentColor).font(.caption) } VStack(alignment: .leading, spacing: 2) { Text(item.name) .font(.system(size: 13)) .lineLimit(1) Text(item.mediaType) .font(.caption) .foregroundColor(.secondary) } Spacer() Button(role: .destructive) { bridge.removeItem(at: idx) } label: { Image(systemName: "trash") .font(.caption) } .buttonStyle(.plain) } .padding(.horizontal, 12) .padding(.vertical, 8) .background(idx == bridge.currentIndex ? Color.accentColor.opacity(0.15) : Color.clear) .contentShape(Rectangle()) .onTapGesture { bridge.playIndex(idx) } } } } } } .frame(minWidth: 350, minHeight: 400) } } // MARK: - URL输入弹窗 struct URLInputDialog: View { @ObservedObject var bridge: PlayerBridge @State private var url = "" @Environment(\.dismiss) private var dismiss var body: some View { VStack(spacing: 16) { Text("添加媒体URL").font(.headline) TextField("https://example.com/video.m3u8", text: $url) .textFieldStyle(.roundedBorder) HStack { Button("取消") { dismiss() } Spacer() Button("添加") { if !url.isEmpty { bridge.addURL(url) dismiss() } } .keyboardShortcut(.defaultAction) .disabled(url.isEmpty) } } .padding(20) .frame(width: 400) } } // MARK: - 音轨选择弹窗 struct TrackSelectDialog: View { @ObservedObject var bridge: PlayerBridge @Environment(\.dismiss) private var dismiss var body: some View { VStack(spacing: 12) { Text("选择音轨").font(.headline) ForEach(Array(bridge.availableTracks.enumerated()), id: \.offset) { idx, track in Button(action: { bridge.selectTrack(index: idx) dismiss() }) { HStack { Text("Track \(idx + 1)") Spacer() if idx == bridge.currentTrackIndex { Text("✓").foregroundColor(.green) } } .padding(8) .contentShape(Rectangle()) } .buttonStyle(.plain) } Divider() Button("关闭") { dismiss() } } .padding(20) .frame(minWidth: 250) } }