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
|
import AppKit
|
||||||
#elseif os(iOS)
|
#elseif os(iOS)
|
||||||
import UIKit
|
import UIKit
|
||||||
|
import UniformTypeIdentifiers
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
@ -456,6 +457,23 @@ struct BricksAppView: View {
|
|||||||
.sheet(isPresented: $bridge.showPlaylistSheet) {
|
.sheet(isPresented: $bridge.showPlaylistSheet) {
|
||||||
PlaylistWindowView(bridge: bridge)
|
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) {
|
HStack(spacing: 10) {
|
||||||
|
// 录制
|
||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
// 录制(放最前面,更显眼)
|
|
||||||
if bridge.screenRecorder.isRecording {
|
if bridge.screenRecorder.isRecording {
|
||||||
HStack(spacing: 4) {
|
HStack(spacing: 4) {
|
||||||
Image(systemName: "stop.circle.fill")
|
Image(systemName: "stop.circle.fill")
|
||||||
@ -504,9 +522,26 @@ struct ControlToolbar: View {
|
|||||||
bridge.toggleRecording(); onTouch()
|
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)
|
Divider().frame(height: 20).padding(.horizontal, 4)
|
||||||
#endif
|
|
||||||
|
|
||||||
TB(icon: "folder") { bridge.openFileDialog(); onTouch() }
|
TB(icon: "folder") { bridge.openFileDialog(); onTouch() }
|
||||||
TB(icon: "link") { bridge.showURLDialog = true; onTouch() }
|
TB(icon: "link") { bridge.showURLDialog = true; onTouch() }
|
||||||
|
|||||||
@ -73,6 +73,8 @@ final class PlayerBridge: ObservableObject {
|
|||||||
@Published var showURLDialog = false
|
@Published var showURLDialog = false
|
||||||
@Published var showTrackDialog = false
|
@Published var showTrackDialog = false
|
||||||
@Published var showQRScanner = false
|
@Published var showQRScanner = false
|
||||||
|
@Published var showLibrarySheet = false
|
||||||
|
@Published var showFileImporter = false
|
||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
var qrScannerController: QRScannerWindowController?
|
var qrScannerController: QRScannerWindowController?
|
||||||
#endif
|
#endif
|
||||||
@ -115,6 +117,10 @@ final class PlayerBridge: ObservableObject {
|
|||||||
let screenRecorder = PlayerRecorder()
|
let screenRecorder = PlayerRecorder()
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#if os(iOS)
|
||||||
|
let hlsRecorder = HLSRecorder()
|
||||||
|
#endif
|
||||||
|
|
||||||
// MARK: - 初始化
|
// MARK: - 初始化
|
||||||
func setup() {
|
func setup() {
|
||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
@ -135,6 +141,15 @@ final class PlayerBridge: ObservableObject {
|
|||||||
self?.showToast(msg)
|
self?.showToast(msg)
|
||||||
}
|
}
|
||||||
#endif
|
#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: - 录制
|
// MARK: - 录制
|
||||||
@ -150,6 +165,17 @@ final class PlayerBridge: ObservableObject {
|
|||||||
let item = queue[currentIndex]
|
let item = queue[currentIndex]
|
||||||
screenRecorder.startRecording(url: item.url)
|
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
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -482,6 +508,8 @@ final class PlayerBridge: ObservableObject {
|
|||||||
addItem(url: url, name: url.lastPathComponent, type: "file")
|
addItem(url: url, name: url.lastPathComponent, type: "file")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
#elseif os(iOS)
|
||||||
|
showFileImporter = true
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -645,9 +673,23 @@ final class PlayerBridge: ObservableObject {
|
|||||||
}
|
}
|
||||||
showToastMsg("\(L.imported) \(count) \(L.localFilesAdded)")
|
showToastMsg("\(L.imported) \(count) \(L.localFilesAdded)")
|
||||||
}
|
}
|
||||||
|
#elseif os(iOS)
|
||||||
|
showFileImporter = true
|
||||||
#endif
|
#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: - 媒体库窗口
|
// MARK: - 媒体库窗口
|
||||||
|
|
||||||
func toggleLibraryWindow() {
|
func toggleLibraryWindow() {
|
||||||
@ -668,6 +710,8 @@ final class PlayerBridge: ObservableObject {
|
|||||||
panel.contentView = NSHostingView(rootView: MediaLibraryView(library: library, bridge: self))
|
panel.contentView = NSHostingView(rootView: MediaLibraryView(library: library, bridge: self))
|
||||||
panel.makeKeyAndOrderFront(NSApp)
|
panel.makeKeyAndOrderFront(NSApp)
|
||||||
libraryWindow = panel
|
libraryWindow = panel
|
||||||
|
#elseif os(iOS)
|
||||||
|
showLibrarySheet.toggle()
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||