feat(iOS): add recording, media library, and local file import
- Wire up HLSRecorder for iOS stream recording (toggleRecording) - Add MediaLibraryView sheet for iOS (toggleLibraryWindow) - Add .fileImporter for iOS local file playback (openFileDialog) - Add Assets.xcassets with app icons - Add UniformTypeIdentifiers import for iOS - Add handleFileImport() with security-scoped resource access
74
Assets.xcassets/AppIcon.appiconset/Contents.json
Normal file
@ -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
|
||||
}
|
||||
}
|
||||
BIN
Assets.xcassets/AppIcon.appiconset/icon_1024.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
Assets.xcassets/AppIcon.appiconset/icon_120.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
Assets.xcassets/AppIcon.appiconset/icon_152.png
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
Assets.xcassets/AppIcon.appiconset/icon_167.png
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
Assets.xcassets/AppIcon.appiconset/icon_180.png
Normal file
|
After Width: | Height: | Size: 2.0 KiB |
BIN
Assets.xcassets/AppIcon.appiconset/icon_40.png
Normal file
|
After Width: | Height: | Size: 503 B |
BIN
Assets.xcassets/AppIcon.appiconset/icon_58.png
Normal file
|
After Width: | Height: | Size: 683 B |
BIN
Assets.xcassets/AppIcon.appiconset/icon_60.png
Normal file
|
After Width: | Height: | Size: 696 B |
BIN
Assets.xcassets/AppIcon.appiconset/icon_80.png
Normal file
|
After Width: | Height: | Size: 903 B |
BIN
Assets.xcassets/AppIcon.appiconset/icon_87.png
Normal file
|
After Width: | Height: | Size: 1.0 KiB |
6
Assets.xcassets/Contents.json
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
@ -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() }
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
|
||||