Update OTG transfer workflow

This commit is contained in:
2026-07-10 11:02:00 +08:00
parent 9d446b4bc3
commit 98dc810455
11 changed files with 1047 additions and 121 deletions
@@ -15,8 +15,8 @@ final class CanonRemoteCaptureService {
private var albumID: Int?
private let photoRepository: PhotoRepositoryProtocol
/// 下载成功时回调文件名,供 ConnectionManager 转发给 ViewModel。
var onShotSaved: ((String) -> Void)?
/// 下载成功时回调本地照片记录,供 ConnectionManager 转发给 ViewModel。
var onShotSaved: ((TravelAlbumOTGPhotoRecord) -> Void)?
private var eventPollTimer: Timer?
private var catalogPollTimer: Timer?
@@ -175,14 +175,14 @@ final class CanonRemoteCaptureService {
lastDownloadedSignature = objectID
guard await saveToAlbum(data: data, filename: filename, reason: reason) else {
guard let record = await saveToAlbum(data: data, filename: filename, reason: reason) else {
knownCatalogIDs.remove(objectID)
return
}
savedShotCount += 1
OTGLog.info(.canon, "Live shot saved (#\(savedShotCount)): \(filename), bytes=\(data.count)")
onShotSaved?(filename)
onShotSaved?(record)
} catch {
knownCatalogIDs.remove(objectID)
OTGLog.error(.canon, "\(reason): catalog download failed \(filename): \(error.localizedDescription)")
@@ -417,23 +417,23 @@ final class CanonRemoteCaptureService {
lastDownloadedSignature = signature
knownDownloadedHandles.insert(item.handle)
guard await saveToAlbum(data: result.data, filename: result.filename, reason: item.reason) else {
guard let record = await saveToAlbum(data: result.data, filename: result.filename, reason: item.reason) else {
return false
}
savedShotCount += 1
OTGLog.info(.canon, "Live shot saved (#\(savedShotCount)): \(result.filename), bytes=\(result.data.count)")
onShotSaved?(result.filename)
onShotSaved?(record)
return true
}
return false
}
private func saveToAlbum(data: Data, filename: String, reason: String) async -> Bool {
private func saveToAlbum(data: Data, filename: String, reason: String) async -> TravelAlbumOTGPhotoRecord? {
guard let albumID else {
OTGLog.warning(.canon, "\(reason): live transfer album not configured, skip save")
return false
return nil
}
do {
@@ -442,15 +442,14 @@ final class CanonRemoteCaptureService {
filename: filename,
albumID: albumID
)
try photoRepository.addPhoto(
return try photoRepository.addPhoto(
albumID: albumID,
localPath: fileURL.path,
createdAt: Date()
)
return true
} catch {
OTGLog.error(.canon, "\(reason): save failed: \(error.localizedDescription)")
return false
return nil
}
}
}
@@ -15,7 +15,8 @@ final class NikonCatalogLiveCaptureService {
private var albumID: Int?
private let photoRepository: PhotoRepositoryProtocol
var onShotSaved: ((String) -> Void)?
/// 下载成功时回调本地照片记录,供 ConnectionManager 转发给 ViewModel。
var onShotSaved: ((TravelAlbumOTGPhotoRecord) -> Void)?
private var catalogPollTimer: Timer?
private var isDownloading = false
@@ -140,14 +141,14 @@ final class NikonCatalogLiveCaptureService {
lastDownloadedSignature = objectID
guard await saveToAlbum(data: data, filename: filename, reason: reason) else {
guard let record = await saveToAlbum(data: data, filename: filename, reason: reason) else {
knownCatalogIDs.remove(objectID)
return
}
savedShotCount += 1
OTGLog.info(.nikon, "Live shot saved (#\(savedShotCount)): \(filename), bytes=\(data.count)")
onShotSaved?(filename)
onShotSaved?(record)
} catch {
knownCatalogIDs.remove(objectID)
OTGLog.error(.nikon, "\(reason): catalog download failed \(filename): \(error.localizedDescription)")
@@ -184,10 +185,10 @@ final class NikonCatalogLiveCaptureService {
// MARK: - Save
private func saveToAlbum(data: Data, filename: String, reason: String) async -> Bool {
private func saveToAlbum(data: Data, filename: String, reason: String) async -> TravelAlbumOTGPhotoRecord? {
guard let albumID else {
OTGLog.warning(.nikon, "\(reason): live transfer album not configured, skip save")
return false
return nil
}
do {
@@ -196,15 +197,14 @@ final class NikonCatalogLiveCaptureService {
filename: filename,
albumID: albumID
)
try photoRepository.addPhoto(
return try photoRepository.addPhoto(
albumID: albumID,
localPath: fileURL.path,
createdAt: Date()
)
return true
} catch {
OTGLog.error(.nikon, "\(reason): save failed: \(error.localizedDescription)")
return false
return nil
}
}
}
@@ -15,8 +15,8 @@ final class SonyRemoteCaptureService {
private var albumID: Int?
private let photoRepository: PhotoRepositoryProtocol
/// 下载成功时回调文件名,供 ConnectionManager 转发给 ViewModel。
var onShotSaved: ((String) -> Void)?
/// 下载成功时回调本地照片记录,供 ConnectionManager 转发给 ViewModel。
var onShotSaved: ((TravelAlbumOTGPhotoRecord) -> Void)?
private var pollTimer: Timer?
private var propertyCheckTask: Task<Void, Never>?
@@ -298,7 +298,7 @@ final class SonyRemoteCaptureService {
filename: downloaded.filename,
albumID: albumID
)
try photoRepository.addPhoto(
let record = try photoRepository.addPhoto(
albumID: albumID,
localPath: fileURL.path,
createdAt: Date()
@@ -307,7 +307,7 @@ final class SonyRemoteCaptureService {
.sony,
"Live shot saved (#\(savedShotCount + 1)): \(downloaded.filename), bytes=\(downloaded.data.count), path=\(fileURL.lastPathComponent)"
)
onShotSaved?(downloaded.filename)
onShotSaved?(record)
return true
} catch {
OTGLog.error(.sony, "\(reason): save failed: \(error.localizedDescription)")
@@ -15,14 +15,14 @@ protocol ConnectionManagerDelegate: AnyObject {
func connectionManager(_ manager: ConnectionManager, didFail error: Error)
func connectionManager(_ manager: ConnectionManager, contentCatalogDidBecomeReady driver: CameraDriver)
func connectionManagerDidSuggestReplug(_ manager: ConnectionManager)
/// 边拍边传:单张照片已下载并写入目标相册。
func connectionManager(_ manager: ConnectionManager, didSaveLiveShot filename: String, albumID: Int)
/// 边拍边传:单张照片已下载并写入目标相册本地缓存。
func connectionManager(_ manager: ConnectionManager, didSaveLiveShot record: TravelAlbumOTGPhotoRecord)
}
extension ConnectionManagerDelegate {
func connectionManager(_ manager: ConnectionManager, contentCatalogDidBecomeReady driver: CameraDriver) {}
func connectionManagerDidSuggestReplug(_ manager: ConnectionManager) {}
func connectionManager(_ manager: ConnectionManager, didSaveLiveShot filename: String, albumID: Int) {}
func connectionManager(_ manager: ConnectionManager, didSaveLiveShot record: TravelAlbumOTGPhotoRecord) {}
}
/// 有线相机连接管理抽象,便于页面 ViewModel 测试连接与目录就绪状态。
@@ -553,14 +553,14 @@ private extension ConnectionManager {
nikonLiveCapture = nil
}
private func makeLiveShotSavedHandler() -> (String) -> Void {
{ [weak self] filename in
private func makeLiveShotSavedHandler() -> (TravelAlbumOTGPhotoRecord) -> Void {
{ [weak self] record in
guard let self, let albumID = self.liveTransferAlbumID else { return }
guard record.albumId == albumID else { return }
self.notify {
self.delegate?.connectionManager(
self,
didSaveLiveShot: filename,
albumID: albumID
didSaveLiveShot: record
)
}
}
@@ -67,6 +67,44 @@ enum TravelAlbumOTGTransferTab: Int, CaseIterable, Sendable {
}
}
/// OTG 传输模式,决定新照片传入 App 后是否立即上传 OSS。
enum TravelAlbumOTGTransferMode: String, CaseIterable, Sendable {
case liveUpload
case postTransfer
/// 展示标题。
var title: String {
switch self {
case .liveUpload: return "边拍边传"
case .postTransfer: return "拍后传输"
}
}
/// 是否在新照片导入 App 后自动上传。
var shouldAutoUploadNewImports: Bool {
self == .liveUpload
}
/// 根据展示标题解析传输模式。
static func option(title: String) -> TravelAlbumOTGTransferMode? {
allCases.first { $0.title == title }
}
}
/// OTG 指定上传弹窗选项。
enum TravelAlbumOTGSpecifyUploadOption: String, CaseIterable, Sendable {
case allPending
case todayCaptured
/// 展示标题。
var title: String {
switch self {
case .allPending: return "上传所有未上传的照片"
case .todayCaptured: return "上传所有今日拍摄的照片"
}
}
}
extension TravelAlbumOTGPhotoItem {
/// 将 Android 同款 `yyyy-MM-dd HH:mm:ss` 拍摄时间转为本地日期。
func capturedDate() -> Date? {
@@ -89,7 +89,8 @@ protocol PhotoRepositoryProtocol {
func fetchPhotos(albumID: Int) throws -> [TravelAlbumOTGPhotoRecord]
/// 添加一张已下载到本地的照片。
func addPhoto(albumID: Int, localPath: String, createdAt: Date) throws
@discardableResult
func addPhoto(albumID: Int, localPath: String, createdAt: Date) throws -> TravelAlbumOTGPhotoRecord
/// 删除指定照片及本地文件。
func deletePhoto(photoID: String, albumID: Int) throws
@@ -110,12 +111,15 @@ final class TravelAlbumOTGPhotoRepository: PhotoRepositoryProtocol {
}
/// 添加一张已下载到本地的照片。
func addPhoto(albumID: Int, localPath: String, createdAt: Date) throws {
@discardableResult
func addPhoto(albumID: Int, localPath: String, createdAt: Date) throws -> TravelAlbumOTGPhotoRecord {
let fileURL = URL(fileURLWithPath: localPath)
let attributes = try? FileManager.default.attributesOfItem(atPath: localPath)
let size = (attributes?[.size] as? NSNumber)?.int64Value ?? 0
let userId = storage.context.userId
guard !userId.isEmpty else { return }
guard !userId.isEmpty else {
throw CameraError.connectionFailed("用户信息缺失,无法保存照片")
}
let record = TravelAlbumOTGPhotoRecord(
id: TravelAlbumOTGPhotoStore.photoId(for: fileURL.lastPathComponent, createdAt: createdAt),
@@ -128,6 +132,7 @@ final class TravelAlbumOTGPhotoRepository: PhotoRepositoryProtocol {
userId: userId
)
storage.upsert(record, albumId: albumID)
return record
}
/// 删除指定照片及本地文件。