Files
suixinkan_ios_new/suixinkan/Features/TravelAlbum/Models/TravelAlbumModels.swift
汉秋 d2fe5d71e4 Add TravelAlbum, ProfileSpace, and wired camera transfer modules.
Introduce travel album entry with Sony PTP tethering pipeline, profile space settings page, home routing updates, Launch Screen storyboard, and related tests/docs.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-30 09:41:05 +08:00

454 lines
13 KiB
Swift
Raw 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.

//
// TravelAlbumModels.swift
// suixinkan
//
// Created by Codex on 2026/6/29.
//
import Foundation
///
struct TravelAlbumUser: Decodable, Equatable, Hashable {
let id: Int
let phone: String
enum CodingKeys: String, CodingKey {
case id, phone
}
init(id: Int = 0, phone: String = "") {
self.id = id
self.phone = phone
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
id = container.lossyInt(forKey: .id)
phone = container.lossyString(forKey: .phone)
}
}
///
struct TravelAlbumItem: Decodable, Identifiable, Equatable, Hashable {
let id: Int
let storeUserId: Int
let name: String
/// 1 2
let type: Int
let orderNumber: String
let materialNum: Int
let materialPrice: Int
let materialPackagePrice: Int
let photoPrice: Int
let coverURL: String
let userId: Int
let status: Int
let createdAt: String
let updatedAt: String
let user: TravelAlbumUser?
enum CodingKeys: String, CodingKey {
case id
case storeUserId = "store_user_id"
case name, type
case orderNumber = "order_number"
case materialNum = "material_num"
case materialPrice = "material_price"
case materialPackagePrice = "material_package_price"
case photoPrice = "photo_price"
case coverURL = "cover_url"
case userId = "user_id"
case status
case createdAt = "created_at"
case updatedAt = "updated_at"
case user
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
id = container.lossyInt(forKey: .id)
storeUserId = container.lossyInt(forKey: .storeUserId)
name = container.lossyString(forKey: .name)
type = container.lossyInt(forKey: .type)
orderNumber = container.lossyString(forKey: .orderNumber)
materialNum = container.lossyInt(forKey: .materialNum)
materialPrice = container.lossyInt(forKey: .materialPrice)
materialPackagePrice = container.lossyInt(forKey: .materialPackagePrice)
photoPrice = container.lossyInt(forKey: .photoPrice)
coverURL = container.lossyString(forKey: .coverURL)
userId = container.lossyInt(forKey: .userId)
status = container.lossyInt(forKey: .status)
createdAt = container.lossyString(forKey: .createdAt)
updatedAt = container.lossyString(forKey: .updatedAt)
user = try? container.decodeIfPresent(TravelAlbumUser.self, forKey: .user)
}
var displayPhone: String {
user?.phone ?? ""
}
}
///
struct TravelAlbumAvailableOrder: Decodable, Identifiable, Equatable, Hashable {
let projectName: String
let orderNumber: String
let userPhone: String
let userId: Int
var id: String { orderNumber }
enum CodingKeys: String, CodingKey {
case projectName = "project_name"
case orderNumber = "order_number"
case userPhone = "user_phone"
case userId = "user_id"
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
projectName = container.lossyString(forKey: .projectName)
orderNumber = container.lossyString(forKey: .orderNumber)
userPhone = container.lossyString(forKey: .userPhone)
userId = container.lossyInt(forKey: .userId)
}
}
///
struct TravelAlbumMaterial: Decodable, Identifiable, Equatable {
let id: Int
let userEquityTravelId: Int
let status: Int
let orderNumber: String
let userId: Int
let fileName: String
let fileType: Int
let fileURL: String
let fileSize: Int
let coverURL: String
let isPurchased: Bool
let createdAt: String
let updatedAt: String
enum CodingKeys: String, CodingKey {
case id
case userEquityTravelId = "user_equity_travel_id"
case status
case orderNumber = "order_number"
case userId = "user_id"
case fileName = "file_name"
case fileType = "file_type"
case fileURL = "file_url"
case fileSize = "file_size"
case coverURL = "cover_url"
case isPurchased = "is_purchased"
case createdAt = "created_at"
case updatedAt = "updated_at"
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
id = container.lossyInt(forKey: .id)
userEquityTravelId = container.lossyInt(forKey: .userEquityTravelId)
status = container.lossyInt(forKey: .status)
orderNumber = container.lossyString(forKey: .orderNumber)
userId = container.lossyInt(forKey: .userId)
fileName = container.lossyString(forKey: .fileName)
fileType = container.lossyInt(forKey: .fileType)
fileURL = container.lossyString(forKey: .fileURL)
fileSize = container.lossyInt(forKey: .fileSize)
coverURL = container.lossyString(forKey: .coverURL)
isPurchased = container.lossyBool(forKey: .isPurchased)
createdAt = container.lossyString(forKey: .createdAt)
updatedAt = container.lossyString(forKey: .updatedAt)
}
}
///
struct TravelAlbumCreateRequest: Encodable {
let name: String
let type: Int
let orderNumber: String?
let materialNum: Int?
let materialPrice: Double?
let materialPackagePrice: Double?
let photoPrice: Double?
enum CodingKeys: String, CodingKey {
case name, type
case orderNumber = "order_number"
case materialNum = "material_num"
case materialPrice = "material_price"
case materialPackagePrice = "material_package_price"
case photoPrice = "photo_price"
}
}
///
struct TravelAlbumCreateResponse: Decodable {
let id: Int
init(id: Int) {
self.id = id
}
enum CodingKeys: String, CodingKey {
case id
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
id = container.lossyInt(forKey: .id)
}
}
///
struct TravelAlbumUploadMaterialRequest: Encodable {
let userEquityTravelId: Int
let fileName: String
let fileURL: String
enum CodingKeys: String, CodingKey {
case userEquityTravelId = "user_equity_travel_id"
case fileName = "file_name"
case fileURL = "file_url"
}
}
///
struct TravelAlbumDeleteRequest: Encodable {
let id: Int
}
///
struct TravelAlbumEditRequest: Encodable {
let id: Int
let name: String?
let materialPackagePrice: Double?
enum CodingKeys: String, CodingKey {
case id, name
case materialPackagePrice = "material_package_price"
}
}
///
struct TravelAlbumDeleteMaterialRequest: Encodable {
let id: Int
}
///
struct TravelAlbumMpCodeResponse: Decodable {
let mpCodeOssURL: String
init(mpCodeOssURL: String) {
self.mpCodeOssURL = mpCodeOssURL
}
enum CodingKeys: String, CodingKey {
case mpCodeOssURL = "mp_code_oss_url"
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
mpCodeOssURL = container.lossyString(forKey: .mpCodeOssURL)
}
}
/// 线
struct WiredTransferContext: Hashable {
let albumId: Int
let albumName: String
let phone: String
let orderNumber: String
}
/// 线
enum WiredTransferUploadStatus: String, Codable, Equatable {
case uploaded = "UPLOADED"
case pending = "PENDING"
case transferring = "TRANSFERRING"
case uploading = "UPLOADING"
case failed = "FAILED"
var displayLabel: String {
switch self {
case .uploaded: "已上传"
case .pending: "待上传"
case .transferring: "传输中"
case .uploading: "上传中"
case .failed: "上传失败"
}
}
}
/// 线
struct WiredTransferPhotoItem: Identifiable, Equatable {
let id: String
let sourceId: String
let fileName: String
let thumbnailURL: String
let capturedAt: String
let fileSizeText: String
let status: WiredTransferUploadStatus
let progress: Int
let errorMessage: String?
init(
id: String,
sourceId: String? = nil,
fileName: String,
thumbnailURL: String,
capturedAt: String,
fileSizeText: String,
status: WiredTransferUploadStatus,
progress: Int = 0,
errorMessage: String? = nil
) {
self.id = id
self.sourceId = sourceId ?? id
self.fileName = fileName
self.thumbnailURL = thumbnailURL
self.capturedAt = capturedAt
self.fileSizeText = fileSizeText
self.status = status
self.progress = progress
self.errorMessage = errorMessage
}
var canSelectForSpecifyUpload: Bool {
status == .pending
}
var isNotUploaded: Bool {
status == .pending || status == .failed
}
}
/// 线
struct WiredTransferPhotoRecord: Codable, Equatable {
let id: String
let sourceId: String
let fileName: String
let localPath: String
let thumbnailPath: String
let capturedAt: String
let fileSizeBytes: Int64
let status: String
let progress: Int
let errorMessage: String?
let albumId: Int
let userId: String
let remoteURL: String
let updatedAt: TimeInterval
func toPhotoItem() -> WiredTransferPhotoItem {
let previewPath = [thumbnailPath, localPath, remoteURL]
.first { path in
!path.isEmpty && (path.hasPrefix("http") || FileManager.default.fileExists(atPath: path))
} ?? ""
let thumbnailURL: String
if previewPath.hasPrefix("http") {
thumbnailURL = previewPath
} else if !previewPath.isEmpty {
thumbnailURL = "file://\(previewPath)"
} else {
thumbnailURL = ""
}
return WiredTransferPhotoItem(
id: id,
sourceId: sourceId,
fileName: fileName,
thumbnailURL: thumbnailURL,
capturedAt: capturedAt,
fileSizeText: Self.formatFileSize(fileSizeBytes),
status: WiredTransferUploadStatus(rawValue: status) ?? .pending,
progress: progress,
errorMessage: errorMessage
)
}
static func formatFileSize(_ bytes: Int64) -> String {
guard bytes > 0 else { return "--" }
let mb = Double(bytes) / 1024.0 / 1024.0
if mb >= 1024 {
return String(format: "%.2f GB", mb / 1024.0)
}
return String(format: "%.2f MB", mb)
}
}
extension WiredTransferPhotoItem {
///
func toRecord(
albumId: Int,
userId: String,
localPath: String,
thumbnailPath: String = "",
fileSizeBytes: Int64,
remoteURL: String = ""
) -> WiredTransferPhotoRecord {
WiredTransferPhotoRecord(
id: id,
sourceId: sourceId,
fileName: fileName,
localPath: localPath,
thumbnailPath: thumbnailPath,
capturedAt: capturedAt,
fileSizeBytes: fileSizeBytes,
status: status.rawValue,
progress: progress,
errorMessage: errorMessage,
albumId: albumId,
userId: userId,
remoteURL: remoteURL,
updatedAt: Date().timeIntervalSince1970
)
}
}
extension Array where Element == WiredTransferPhotoItem {
/// Tab 0 1 2
func filtered(byTabIndex index: Int) -> [WiredTransferPhotoItem] {
switch index {
case 1: filter { $0.status == .uploaded }
case 2: filter { $0.status == .failed }
default: self
}
}
/// Tab
var transferTabCounts: [Int] {
[count, filter { $0.status == .uploaded }.count, filter { $0.status == .failed }.count]
}
/// photo id
var selectablePendingPhotoIDs: Set<String> {
Set(filter { $0.canSelectForSpecifyUpload }.map(\.id))
}
}
private extension KeyedDecodingContainer {
func lossyString(forKey key: Key) -> String {
if let value = try? decode(String.self, forKey: key) { return value }
if let value = try? decode(Int.self, forKey: key) { return "\(value)" }
if let value = try? decode(Double.self, forKey: key) { return "\(value)" }
return ""
}
func lossyInt(forKey key: Key) -> Int {
if let value = try? decode(Int.self, forKey: key) { return value }
if let value = try? decode(String.self, forKey: key), let intValue = Int(value) { return intValue }
if let value = try? decode(Double.self, forKey: key) { return Int(value) }
return 0
}
func lossyBool(forKey key: Key) -> Bool {
if let value = try? decode(Bool.self, forKey: key) { return value }
if let value = try? decode(Int.self, forKey: key) { return value != 0 }
if let value = try? decode(String.self, forKey: key) {
return value == "1" || value.lowercased() == "true"
}
return false
}
}