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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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