fix(iOS): move pendingExportURL to PlayerBridge — SwiftUI can now observe it and trigger save sheet
This commit is contained in:
parent
3ed5696ae0
commit
1412c234f8
@ -10,7 +10,6 @@ import Photos
|
|||||||
final class HLSRecorder: ObservableObject {
|
final class HLSRecorder: ObservableObject {
|
||||||
@Published var isRecording = false
|
@Published var isRecording = false
|
||||||
@Published var durationText = "00:00"
|
@Published var durationText = "00:00"
|
||||||
@Published var pendingExportURL: URL?
|
|
||||||
|
|
||||||
var onRecordingSaved: ((URL) -> Void)?
|
var onRecordingSaved: ((URL) -> Void)?
|
||||||
var onError: ((String) -> Void)?
|
var onError: ((String) -> Void)?
|
||||||
@ -42,7 +41,6 @@ final class HLSRecorder: ObservableObject {
|
|||||||
self.isRecording = false
|
self.isRecording = false
|
||||||
self.timer?.invalidate()
|
self.timer?.invalidate()
|
||||||
self.onRecordingSaved?(outputURL)
|
self.onRecordingSaved?(outputURL)
|
||||||
self.showExportSheet(url: outputURL)
|
|
||||||
}
|
}
|
||||||
} catch is CancellationError {
|
} catch is CancellationError {
|
||||||
NSLog("[MiniPlayer] Recording task cancelled")
|
NSLog("[MiniPlayer] Recording task cancelled")
|
||||||
@ -294,108 +292,6 @@ final class HLSRecorder: ObservableObject {
|
|||||||
return outputMP4
|
return outputMP4
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - 导出/分享
|
|
||||||
|
|
||||||
private func showExportSheet(url: URL) {
|
|
||||||
NSLog("[MiniPlayer] showExportSheet: \(url.path)")
|
|
||||||
|
|
||||||
// 1. 保存到相册(使用 PHPhotoLibrary,有回调通知结果)
|
|
||||||
if url.pathExtension.lowercased() == "mp4" || url.pathExtension.lowercased() == "mov" {
|
|
||||||
saveToPhotoLibrary(url: url)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. 通过 SwiftUI sheet 弹出分享面板(而非直接 present UIViewController)
|
|
||||||
DispatchQueue.main.async { [weak self] in
|
|
||||||
self?.pendingExportURL = url
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func saveToPhotoLibrary(url: URL) {
|
|
||||||
let status = PHPhotoLibrary.authorizationStatus(for: .addOnly)
|
|
||||||
|
|
||||||
if status == .authorized || status == .limited {
|
|
||||||
performSave(url: url)
|
|
||||||
} else if status == .notDetermined {
|
|
||||||
PHPhotoLibrary.requestAuthorization(for: .addOnly) { [weak self] newStatus in
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
if newStatus == .authorized || newStatus == .limited {
|
|
||||||
self?.performSave(url: url)
|
|
||||||
} else {
|
|
||||||
self?.onError?("Photo library access denied")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
onError?("Photo library access denied. Please enable in Settings.")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func performSave(url: URL) {
|
|
||||||
PHPhotoLibrary.shared().performChanges {
|
|
||||||
PHAssetCreationRequest.forAsset().addResource(with: .video, fileURL: url, options: nil)
|
|
||||||
} completionHandler: { [weak self] success, error in
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
if success {
|
|
||||||
self?.onRecordingSaved?(url)
|
|
||||||
NSLog("[MiniPlayer] Video saved to Photos")
|
|
||||||
} else {
|
|
||||||
self?.onError?("Save to Photos failed: \(error?.localizedDescription ?? "unknown")")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func presentActivitySheet(url: URL) {
|
|
||||||
NSLog("[MiniPlayer] presentActivitySheet called for: %@", url.lastPathComponent)
|
|
||||||
|
|
||||||
guard let windowScene = UIApplication.shared.connectedScenes
|
|
||||||
.compactMap({ $0 as? UIWindowScene })
|
|
||||||
.first(where: { $0.activationState == .foregroundActive }) ?? UIApplication.shared.connectedScenes.compactMap({ $0 as? UIWindowScene }).first else {
|
|
||||||
NSLog("[MiniPlayer] No window scene found")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 找最顶层可见的 VC:先用 keyWindow,再 fallback 到任意 window
|
|
||||||
var topVC: UIViewController?
|
|
||||||
if let keyWindow = windowScene.keyWindow {
|
|
||||||
topVC = keyWindow.rootViewController
|
|
||||||
}
|
|
||||||
if topVC == nil {
|
|
||||||
for window in windowScene.windows where !window.isHidden && window.bounds.size != .zero {
|
|
||||||
topVC = window.rootViewController
|
|
||||||
if topVC != nil { break }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if topVC == nil {
|
|
||||||
topVC = windowScene.windows.first?.rootViewController
|
|
||||||
}
|
|
||||||
|
|
||||||
// 沿着 presentedViewController 链找到最顶层
|
|
||||||
while let presented = topVC?.presentedViewController {
|
|
||||||
topVC = presented
|
|
||||||
}
|
|
||||||
|
|
||||||
guard let vc = topVC else {
|
|
||||||
NSLog("[MiniPlayer] No view controller found to present share sheet")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
NSLog("[MiniPlayer] Presenting from VC: %@", String(describing: type(of: vc)))
|
|
||||||
|
|
||||||
let activityVC = UIActivityViewController(activityItems: [url], applicationActivities: nil)
|
|
||||||
|
|
||||||
// iPad 适配
|
|
||||||
if let popover = activityVC.popoverPresentationController {
|
|
||||||
popover.sourceView = vc.view
|
|
||||||
popover.sourceRect = CGRect(x: vc.view?.bounds.midX ?? 0, y: vc.view?.bounds.midY ?? 0, width: 0, height: 0)
|
|
||||||
popover.permittedArrowDirections = []
|
|
||||||
}
|
|
||||||
|
|
||||||
vc.present(activityVC, animated: true) {
|
|
||||||
NSLog("[MiniPlayer] Share sheet presented successfully")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Helpers
|
// MARK: - Helpers
|
||||||
|
|
||||||
private func resolveURL(_ urlString: String, baseURL: URL) -> URL {
|
private func resolveURL(_ urlString: String, baseURL: URL) -> URL {
|
||||||
|
|||||||
@ -582,12 +582,12 @@ struct BricksAppView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.sheet(isPresented: Binding<Bool>(
|
.sheet(isPresented: Binding<Bool>(
|
||||||
get: { bridge.hlsRecorder.pendingExportURL != nil },
|
get: { bridge.pendingExportURL != nil },
|
||||||
set: { if !$0 { bridge.hlsRecorder.pendingExportURL = nil } }
|
set: { if !$0 { bridge.pendingExportURL = nil } }
|
||||||
)) {
|
)) {
|
||||||
if let url = bridge.hlsRecorder.pendingExportURL {
|
if let url = bridge.pendingExportURL {
|
||||||
ActivityShareSheet(url: url) {
|
ActivityShareSheet(url: url) {
|
||||||
bridge.hlsRecorder.pendingExportURL = nil
|
bridge.pendingExportURL = nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -122,7 +122,8 @@ final class PlayerBridge: ObservableObject {
|
|||||||
#if os(iOS)
|
#if os(iOS)
|
||||||
let hlsRecorder = HLSRecorder()
|
let hlsRecorder = HLSRecorder()
|
||||||
let pipController = PiPController()
|
let pipController = PiPController()
|
||||||
|
@Published var pendingExportURL: URL?
|
||||||
|
#endif
|
||||||
// 诊断日志
|
// 诊断日志
|
||||||
private var diagLogURL: URL?
|
private var diagLogURL: URL?
|
||||||
private var diagLogHandle: FileHandle?
|
private var diagLogHandle: FileHandle?
|
||||||
@ -144,7 +145,6 @@ final class PlayerBridge: ObservableObject {
|
|||||||
try? diagLogHandle?.write(contentsOf: data)
|
try? diagLogHandle?.write(contentsOf: data)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endif
|
|
||||||
|
|
||||||
// MARK: - 初始化
|
// MARK: - 初始化
|
||||||
func setup() {
|
func setup() {
|
||||||
@ -178,6 +178,7 @@ final class PlayerBridge: ObservableObject {
|
|||||||
#if os(iOS)
|
#if os(iOS)
|
||||||
hlsRecorder.onRecordingSaved = { [weak self] url in
|
hlsRecorder.onRecordingSaved = { [weak self] url in
|
||||||
self?.showToast("Saved: \(url.lastPathComponent)")
|
self?.showToast("Saved: \(url.lastPathComponent)")
|
||||||
|
self?.pendingExportURL = url
|
||||||
}
|
}
|
||||||
hlsRecorder.onError = { [weak self] msg in
|
hlsRecorder.onError = { [weak self] msg in
|
||||||
self?.showToast(msg)
|
self?.showToast(msg)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user