fix(iOS): move pendingExportURL to PlayerBridge — SwiftUI can now observe it and trigger save sheet

This commit is contained in:
yumoqing 2026-07-03 08:29:02 +08:00
parent 3ed5696ae0
commit 1412c234f8
3 changed files with 7 additions and 110 deletions

View File

@ -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 {

View File

@ -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
} }
} }
} }

View File

@ -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)