diff --git a/Assets.xcassets/AppIcon.appiconset/Contents.json b/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..7b3bbd2 --- /dev/null +++ b/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,74 @@ +{ + "images": [ + { + "filename": "icon_40.png", + "idiom": "iphone", + "scale": "2x", + "size": "20x20" + }, + { + "filename": "icon_60.png", + "idiom": "iphone", + "scale": "3x", + "size": "20x20" + }, + { + "filename": "icon_58.png", + "idiom": "iphone", + "scale": "2x", + "size": "29x29" + }, + { + "filename": "icon_87.png", + "idiom": "iphone", + "scale": "3x", + "size": "29x29" + }, + { + "filename": "icon_80.png", + "idiom": "iphone", + "scale": "2x", + "size": "40x40" + }, + { + "filename": "icon_120.png", + "idiom": "iphone", + "scale": "3x", + "size": "40x40" + }, + { + "filename": "icon_120.png", + "idiom": "iphone", + "scale": "2x", + "size": "60x60" + }, + { + "filename": "icon_180.png", + "idiom": "iphone", + "scale": "3x", + "size": "60x60" + }, + { + "filename": "icon_152.png", + "idiom": "ipad", + "scale": "2x", + "size": "76x76" + }, + { + "filename": "icon_167.png", + "idiom": "ipad", + "scale": "2x", + "size": "83.5x83.5" + }, + { + "filename": "icon_1024.png", + "idiom": "ios-marketing", + "scale": "1x", + "size": "1024x1024" + } + ], + "info": { + "author": "MiniPlayer", + "version": 1 + } +} \ No newline at end of file diff --git a/Assets.xcassets/AppIcon.appiconset/icon_1024.png b/Assets.xcassets/AppIcon.appiconset/icon_1024.png new file mode 100644 index 0000000..0285f35 Binary files /dev/null and b/Assets.xcassets/AppIcon.appiconset/icon_1024.png differ diff --git a/Assets.xcassets/AppIcon.appiconset/icon_120.png b/Assets.xcassets/AppIcon.appiconset/icon_120.png new file mode 100644 index 0000000..9d5f9a8 Binary files /dev/null and b/Assets.xcassets/AppIcon.appiconset/icon_120.png differ diff --git a/Assets.xcassets/AppIcon.appiconset/icon_152.png b/Assets.xcassets/AppIcon.appiconset/icon_152.png new file mode 100644 index 0000000..c2ab899 Binary files /dev/null and b/Assets.xcassets/AppIcon.appiconset/icon_152.png differ diff --git a/Assets.xcassets/AppIcon.appiconset/icon_167.png b/Assets.xcassets/AppIcon.appiconset/icon_167.png new file mode 100644 index 0000000..8d4d35e Binary files /dev/null and b/Assets.xcassets/AppIcon.appiconset/icon_167.png differ diff --git a/Assets.xcassets/AppIcon.appiconset/icon_180.png b/Assets.xcassets/AppIcon.appiconset/icon_180.png new file mode 100644 index 0000000..0584068 Binary files /dev/null and b/Assets.xcassets/AppIcon.appiconset/icon_180.png differ diff --git a/Assets.xcassets/AppIcon.appiconset/icon_40.png b/Assets.xcassets/AppIcon.appiconset/icon_40.png new file mode 100644 index 0000000..d22550e Binary files /dev/null and b/Assets.xcassets/AppIcon.appiconset/icon_40.png differ diff --git a/Assets.xcassets/AppIcon.appiconset/icon_58.png b/Assets.xcassets/AppIcon.appiconset/icon_58.png new file mode 100644 index 0000000..f44e57e Binary files /dev/null and b/Assets.xcassets/AppIcon.appiconset/icon_58.png differ diff --git a/Assets.xcassets/AppIcon.appiconset/icon_60.png b/Assets.xcassets/AppIcon.appiconset/icon_60.png new file mode 100644 index 0000000..ca05232 Binary files /dev/null and b/Assets.xcassets/AppIcon.appiconset/icon_60.png differ diff --git a/Assets.xcassets/AppIcon.appiconset/icon_80.png b/Assets.xcassets/AppIcon.appiconset/icon_80.png new file mode 100644 index 0000000..61d351f Binary files /dev/null and b/Assets.xcassets/AppIcon.appiconset/icon_80.png differ diff --git a/Assets.xcassets/AppIcon.appiconset/icon_87.png b/Assets.xcassets/AppIcon.appiconset/icon_87.png new file mode 100644 index 0000000..07af970 Binary files /dev/null and b/Assets.xcassets/AppIcon.appiconset/icon_87.png differ diff --git a/Assets.xcassets/Contents.json b/Assets.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Sources/MiniPlayerApp.swift b/Sources/MiniPlayerApp.swift index 4e8a2cb..ea038fc 100644 --- a/Sources/MiniPlayerApp.swift +++ b/Sources/MiniPlayerApp.swift @@ -6,6 +6,7 @@ import SwiftBricks import AppKit #elseif os(iOS) import UIKit +import UniformTypeIdentifiers #endif #if os(macOS) @@ -456,6 +457,23 @@ struct BricksAppView: View { .sheet(isPresented: $bridge.showPlaylistSheet) { PlaylistWindowView(bridge: bridge) } + .sheet(isPresented: $bridge.showLibrarySheet) { + MediaLibraryView(library: bridge.library, bridge: bridge) + } + #if os(iOS) + .fileImporter( + isPresented: $bridge.showFileImporter, + allowedContentTypes: [.movie, .video, .audio, .mpeg4Movie, .quickTimeMovie, .mp3, .wav, .mpeg4Audio, .plainText, .item], + allowsMultipleSelection: true + ) { result in + switch result { + case .success(let urls): + bridge.handleFileImport(urls) + case .failure(let error): + bridge.showToastMsg(error.localizedDescription) + } + } + #endif } } @@ -486,8 +504,8 @@ struct ControlToolbar: View { // 按钮行 HStack(spacing: 10) { + // 录制 #if os(macOS) - // 录制(放最前面,更显眼) if bridge.screenRecorder.isRecording { HStack(spacing: 4) { Image(systemName: "stop.circle.fill") @@ -504,9 +522,26 @@ struct ControlToolbar: View { bridge.toggleRecording(); onTouch() } } + #elseif os(iOS) + if bridge.hlsRecorder.isRecording { + HStack(spacing: 4) { + Image(systemName: "stop.circle.fill") + .foregroundColor(.red) + .font(.system(size: 18)) + Text(bridge.hlsRecorder.durationText) + .font(.system(size: 12, design: .monospaced)) + .foregroundColor(.red) + } + .contentShape(Rectangle()) + .onTapGesture { bridge.toggleRecording(); onTouch() } + } else { + TB(icon: "record.circle") { + bridge.toggleRecording(); onTouch() + } + } + #endif Divider().frame(height: 20).padding(.horizontal, 4) - #endif TB(icon: "folder") { bridge.openFileDialog(); onTouch() } TB(icon: "link") { bridge.showURLDialog = true; onTouch() } diff --git a/Sources/PlayerBridge.swift b/Sources/PlayerBridge.swift index 65bb5fa..3e81f28 100644 --- a/Sources/PlayerBridge.swift +++ b/Sources/PlayerBridge.swift @@ -73,6 +73,8 @@ final class PlayerBridge: ObservableObject { @Published var showURLDialog = false @Published var showTrackDialog = false @Published var showQRScanner = false + @Published var showLibrarySheet = false + @Published var showFileImporter = false #if os(macOS) var qrScannerController: QRScannerWindowController? #endif @@ -115,6 +117,10 @@ final class PlayerBridge: ObservableObject { let screenRecorder = PlayerRecorder() #endif + #if os(iOS) + let hlsRecorder = HLSRecorder() + #endif + // MARK: - 初始化 func setup() { #if os(macOS) @@ -135,6 +141,15 @@ final class PlayerBridge: ObservableObject { self?.showToast(msg) } #endif + + #if os(iOS) + hlsRecorder.onRecordingSaved = { [weak self] url in + self?.showToast("Saved: \(url.lastPathComponent)") + } + hlsRecorder.onError = { [weak self] msg in + self?.showToast(msg) + } + #endif } // MARK: - 录制 @@ -150,6 +165,17 @@ final class PlayerBridge: ObservableObject { let item = queue[currentIndex] screenRecorder.startRecording(url: item.url) } + #elseif os(iOS) + if hlsRecorder.isRecording { + hlsRecorder.stopRecording() + } else { + guard !queue.isEmpty, currentIndex >= 0, currentIndex < queue.count else { + showToast("No media playing") + return + } + let item = queue[currentIndex] + hlsRecorder.startRecording(url: item.url) + } #endif } @@ -482,6 +508,8 @@ final class PlayerBridge: ObservableObject { addItem(url: url, name: url.lastPathComponent, type: "file") } } + #elseif os(iOS) + showFileImporter = true #endif } @@ -645,9 +673,23 @@ final class PlayerBridge: ObservableObject { } showToastMsg("\(L.imported) \(count) \(L.localFilesAdded)") } + #elseif os(iOS) + showFileImporter = true #endif } + /// 处理 iOS 文件选择器结果 + func handleFileImport(_ urls: [URL]) { + for url in urls { + // iOS: 需要 startAccessingSecurityScopedResource 获取沙盒文件访问权限 + let didStartAccessing = url.startAccessingSecurityScopedResource() + defer { + if didStartAccessing { url.stopAccessingSecurityScopedResource() } + } + addItem(url: url, name: url.lastPathComponent, type: "file") + } + } + // MARK: - 媒体库窗口 func toggleLibraryWindow() { @@ -668,6 +710,8 @@ final class PlayerBridge: ObservableObject { panel.contentView = NSHostingView(rootView: MediaLibraryView(library: library, bridge: self)) panel.makeKeyAndOrderFront(NSApp) libraryWindow = panel + #elseif os(iOS) + showLibrarySheet.toggle() #endif }