import SwiftUI 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.adjustVolume(by: 0.1) } .keyboardShortcut("=", modifiers: .command) Button("音量-") { bridge.adjustVolume(by: -0.1) } .keyboardShortcut("-", 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: - 主内容视图(直接SwiftUI,不经过BricksView) struct ContentView: View { @ObservedObject var bridge: PlayerBridge 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() } #if os(macOS) .onTapGesture(count: 2) { bridge.toggleFullscreen() } #endif .ignoresSafeArea() // 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(半透明,点击logo才显示) 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) } } } .sheet(isPresented: $bridge.showURLDialog) { URLInputDialog(bridge: bridge) } .sheet(isPresented: $bridge.showTrackDialog) { TrackSelectDialog(bridge: bridge) } .sheet(isPresented: $bridge.showPlaylistSheet) { PlaylistWindowView(bridge: bridge) } .animation(.easeInOut(duration: 0.25), value: bridge.showToolbar) .onReceive(NotificationCenter.default.publisher(for: .init("ExitFullscreen"))) { _ in bridge.toggleFullscreen() } } } // 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: 50, alignment: .trailing) ProgressSlider(player: bridge.player, bridge: bridge) .frame(height: 20) Text(bridge.totalTimeText) .font(.system(size: 12, design: .monospaced)) .foregroundColor(.white) .frame(width: 50, alignment: .leading) } // 按钮行 HStack(spacing: 10) { #if os(macOS) ToolbarButton(label: "📂 打开") { bridge.openFileDialog() } #endif ToolbarButton(label: "⏮") { bridge.playPrev() } ToolbarButton(label: bridge.isPlaying ? "⏸" : "▶️") { bridge.togglePlayPause() } ToolbarButton(label: "⏭") { bridge.playNext() } Spacer() // 音量控制 ToolbarButton(label: "🔈") { bridge.adjustVolume(by: -0.1) } // 音量条 GeometryReader { geo in ZStack(alignment: .leading) { RoundedRectangle(cornerRadius: 2) .fill(Color.white.opacity(0.2)) .frame(height: 4) RoundedRectangle(cornerRadius: 2) .fill(Color.accentColor) .frame(width: geo.size.width * CGFloat(bridge.volume), height: 4) } .contentShape(Rectangle()) .gesture( DragGesture(minimumDistance: 0) .onChanged { value in let ratio = max(0, min(1, value.location.x / geo.size.width)) bridge.setVolume(Float(ratio)) } ) } .frame(width: 80, height: 20) Text("\(Int(bridge.volume * 100))") .font(.system(size: 11, design: .monospaced)) .foregroundColor(.white.opacity(0.7)) .frame(width: 30) ToolbarButton(label: "🔊") { bridge.adjustVolume(by: 0.1) } 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)) } } // 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("暂无媒体").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) : .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) } }