Files
suixinkan_ios_new/suixinkan/Features/TravelAlbum/Services/WiredTransferPhotoStore.swift

272 lines
9.7 KiB
Swift
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//
// WiredTransferPhotoStore.swift
// suixinkan
//
// Created by Codex on 2026/6/29.
//
import Foundation
/// 线 UserDefaults
final class WiredTransferPhotoStore {
private let defaults: UserDefaults
private let accountPrefixProvider: () -> String
private let userIDProvider: () -> String
private static let keyPrefix = "wired_transfer_photos_"
private static let deletedSuffix = "_deleted"
private static let bindingsSuffix = "_photo_bindings"
private static let transferModeKeySuffix = "wired_transfer_mode_option"
static let modeLiveCapture = "边拍边传"
static let modePostShootTransfer = "拍后传输"
///
func loadTransferModeOption() -> String? {
let value = defaults.string(forKey: transferModeKey())?
.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
switch value {
case Self.modeLiveCapture, Self.modePostShootTransfer:
return value
default:
return nil
}
}
///
func saveTransferModeOption(_ option: String) {
switch option {
case Self.modeLiveCapture, Self.modePostShootTransfer:
defaults.set(option, forKey: transferModeKey())
default:
break
}
}
/// ID
init(
defaults: UserDefaults = .standard,
accountPrefixProvider: @escaping () -> String,
userIDProvider: @escaping () -> String
) {
self.defaults = defaults
self.accountPrefixProvider = accountPrefixProvider
self.userIDProvider = userIDProvider
}
/// ID
func albumID(forPhoto photoID: String) -> Int? {
guard !photoID.isEmpty else { return nil }
return loadBindings().first { $0.photoID == photoID }?.albumID
}
///
func bindPhotoToAlbum(photoID: String, albumID: Int) {
guard !photoID.isEmpty, albumID > 0, !userIDProvider().isEmpty else { return }
var bindings = loadBindings().filter { $0.photoID != photoID }
bindings.append(PhotoAlbumBinding(photoID: photoID, albumID: albumID))
saveBindings(bindings)
}
/// ID
func loadDeletedIDs(albumID: Int) -> Set<String> {
guard albumID > 0, !userIDProvider().isEmpty else { return [] }
let ids = defaults.stringArray(forKey: deletedKey(albumID)) ?? []
return Set(ids)
}
///
func load(albumID: Int) -> [WiredTransferPhotoRecord] {
guard albumID > 0 else { return [] }
let userID = userIDProvider()
guard !userID.isEmpty else { return [] }
let deletedIDs = loadDeletedIDs(albumID: albumID)
guard let data = defaults.data(forKey: storageKey(albumID)),
let records = try? JSONDecoder().decode([WiredTransferPhotoRecord].self, from: data)
else { return [] }
let filteredRecords = records.filter { record in
!deletedIDs.contains(record.id)
&& record.userId == userID
&& record.albumId == albumID
}
let normalizedRecords = filteredRecords.map { $0.normalizeInterruptedTransfer() }
return normalizedRecords
.filter(\.isDisplayable)
.sorted { $0.capturedAt > $1.capturedAt }
.also { items in
items.forEach { bindPhotoToAlbum(photoID: $0.id, albumID: albumID) }
}
}
///
func save(albumID: Int, records: [WiredTransferPhotoRecord]) {
guard albumID > 0 else { return }
let userID = userIDProvider()
guard !userID.isEmpty else { return }
let normalized = records
.filter { $0.userId == userID && $0.albumId == albumID }
.sorted { $0.capturedAt > $1.capturedAt }
normalized.forEach { bindPhotoToAlbum(photoID: $0.id, albumID: albumID) }
if let data = try? JSONEncoder().encode(normalized) {
defaults.set(data, forKey: storageKey(albumID))
}
}
///
func remove(albumID: Int, photoID: String) {
guard albumID > 0, !photoID.isEmpty else { return }
markDeleted(albumID: albumID, photoID: photoID)
let deletedIDs = loadDeletedIDs(albumID: albumID)
guard let data = defaults.data(forKey: storageKey(albumID)),
var records = try? JSONDecoder().decode([WiredTransferPhotoRecord].self, from: data)
else { return }
records
.filter { $0.id == photoID && $0.userId == userIDProvider() && $0.albumId == albumID }
.forEach { deleteRecordFiles($0) }
records = records.filter { record in
!deletedIDs.contains(record.id)
&& record.userId == userIDProvider()
&& record.albumId == albumID
}
save(albumID: albumID, records: records)
}
///
func markDeleted(albumID: Int, photoID: String) {
guard albumID > 0, !photoID.isEmpty, !userIDProvider().isEmpty else { return }
let updated = loadDeletedIDs(albumID: albumID).union([photoID])
defaults.set(Array(updated), forKey: deletedKey(albumID))
}
///
func clearAlbum(albumID: Int) -> Set<String> {
guard albumID > 0 else { return [] }
let userID = userIDProvider()
guard !userID.isEmpty else { return [] }
guard let data = defaults.data(forKey: storageKey(albumID)),
let records = try? JSONDecoder().decode([WiredTransferPhotoRecord].self, from: data)
else {
CameraDownloadStorage.removeScopeDirectory(for: downloadScope(albumID: albumID))
defaults.removeObject(forKey: storageKey(albumID))
defaults.removeObject(forKey: deletedKey(albumID))
return []
}
let userRecords = records.filter { $0.userId == userID }
userRecords.forEach { deleteRecordFiles($0) }
CameraDownloadStorage.removeScopeDirectory(for: downloadScope(albumID: albumID))
let bindings = loadBindings().filter { $0.albumID != albumID }
saveBindings(bindings)
defaults.removeObject(forKey: storageKey(albumID))
defaults.removeObject(forKey: deletedKey(albumID))
return Set(userRecords.map(\.id))
}
private func deleteRecordFiles(_ record: WiredTransferPhotoRecord) {
deleteFileIfExists(record.localPath)
deleteFileIfExists(record.thumbnailPath)
}
private func deleteFileIfExists(_ path: String) {
guard !path.isEmpty else { return }
guard let url = CameraDownloadStorage.resolveLocalURL(from: path) else { return }
if FileManager.default.fileExists(atPath: url.path) {
try? FileManager.default.removeItem(at: url)
}
}
private func storageKey(_ albumID: Int) -> String {
"\(accountPrefixProvider())\(Self.keyPrefix)\(albumID)"
}
private func deletedKey(_ albumID: Int) -> String {
"\(storageKey(albumID))\(Self.deletedSuffix)"
}
private func bindingsKey() -> String {
"\(accountPrefixProvider())\(Self.bindingsSuffix)"
}
private func transferModeKey() -> String {
"\(accountPrefixProvider())\(Self.transferModeKeySuffix)"
}
private func downloadScope(albumID: Int) -> CameraDownloadStorage.Scope {
CameraDownloadStorage.Scope(accountKey: accountPrefixProvider(), albumID: albumID)
}
private struct PhotoAlbumBinding: Codable {
let photoID: String
let albumID: Int
}
private func loadBindings() -> [PhotoAlbumBinding] {
guard let data = defaults.data(forKey: bindingsKey()),
let bindings = try? JSONDecoder().decode([PhotoAlbumBinding].self, from: data)
else { return [] }
return bindings
}
private func saveBindings(_ bindings: [PhotoAlbumBinding]) {
if let data = try? JSONEncoder().encode(bindings) {
defaults.set(data, forKey: bindingsKey())
}
}
}
private extension WiredTransferPhotoRecord {
var isDisplayable: Bool {
if !remoteURL.isEmpty { return true }
if let url = CameraDownloadStorage.resolveLocalURL(from: localPath),
FileManager.default.fileExists(atPath: url.path) {
return true
}
if let url = CameraDownloadStorage.resolveLocalURL(from: thumbnailPath),
FileManager.default.fileExists(atPath: url.path) {
return true
}
return false
}
func normalizeInterruptedTransfer() -> WiredTransferPhotoRecord {
guard status == WiredTransferUploadStatus.transferring.rawValue
|| status == WiredTransferUploadStatus.uploading.rawValue
else { return self }
var copy = self
copy = WiredTransferPhotoRecord(
id: id,
sourceId: sourceId,
fileName: fileName,
localPath: localPath,
thumbnailPath: thumbnailPath,
capturedAt: capturedAt,
fileSizeBytes: fileSizeBytes,
status: remoteURL.isEmpty ? WiredTransferUploadStatus.pending.rawValue : WiredTransferUploadStatus.uploaded.rawValue,
progress: remoteURL.isEmpty ? 0 : 100,
errorMessage: errorMessage,
albumId: albumId,
userId: userId,
remoteURL: remoteURL,
updatedAt: updatedAt
)
return copy
}
}
private extension Array {
@discardableResult
func also(_ body: (Self) -> Void) -> Self {
body(self)
return self
}
}