| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116 |
- import Foundation
- import SwiftData
- @Model
- final class CelestiaSession {
- var id: UUID
- var title: String
- var startTime: Date
- var endTime: Date?
- var localAudioPath: String?
- var isSynced: Bool
- var cloudSessionId: String?
- var ownerUserID: String?
- // Defaults must live on the persisted properties so SwiftData can backfill
- // existing rows during a lightweight migration. Initializer defaults only
- // apply to newly-created model instances.
- var serverRevision: Int64 = 0
- // Timestamp of the newest server detail whose metadata and assets have
- // both been fully applied locally. Nil forces one initial detail refresh.
- var serverUpdatedAt: Date?
- var syncStateRaw: String = SessionSyncState.localOnly.rawValue
- var lastSyncError: String?
- var lastSyncedAt: Date?
-
- @Relationship(deleteRule: .cascade, inverse: \CelestiaTimelineEvent.session)
- var events: [CelestiaTimelineEvent]
-
- @Relationship(deleteRule: .cascade, inverse: \AudioChunk.session)
- var audioChunks: [AudioChunk]
-
- var durationMs: Int64 {
- guard let end = endTime else { return 0 }
- return Int64(end.timeIntervalSince(startTime) * 1000)
- }
-
- var durationFormatted: String {
- guard let end = endTime else { return "正在记录..." }
- let interval = end.timeIntervalSince(startTime)
- let hours = Int(interval) / 3600
- let minutes = (Int(interval) % 3600) / 60
- let seconds = Int(interval) % 60
- if hours > 0 {
- return String(format: "%d:%02d:%02d", hours, minutes, seconds)
- }
- return String(format: "%02d:%02d", minutes, seconds)
- }
-
- var photoCount: Int {
- events.filter { $0.eventType == "PHOTO" }.count
- }
-
- var noteCount: Int {
- events.filter {
- $0.eventType == "NOTE"
- || ($0.eventType == "MARKER" && !$0.isContinuationMarker)
- }.count
- }
-
- var syncState: SessionSyncState {
- get { SessionSyncState(rawValue: syncStateRaw) ?? .localOnly }
- set { syncStateRaw = newValue.rawValue }
- }
- /// A server-confirmed sync requires both the legacy completion flag and a
- /// remote identifier. This remains compatible with records created before
- /// `syncStateRaw` was added, whose migrated state defaults to `localOnly`.
- var hasConfirmedCloudSync: Bool {
- isSynced && cloudSessionId != nil
- }
- var needsCloudSync: Bool {
- !hasConfirmedCloudSync
- }
- func markContentModified() {
- isSynced = false
- syncState = .pending
- lastSyncError = nil
- }
- /// The revision represented by the current local content.
- /// Unsynced edits form the next revision based on the last known server revision.
- var localRevision: Int64 {
- if hasConfirmedCloudSync {
- return max(serverRevision, 1)
- }
- return max(serverRevision + 1, 1)
- }
- init(id: UUID = UUID(), title: String, localAudioPath: String? = nil, startTime: Date = Date()) {
- self.id = id
- self.title = title
- self.startTime = startTime
- self.endTime = nil
- self.localAudioPath = localAudioPath
- self.isSynced = false
- self.cloudSessionId = nil
- self.ownerUserID = nil
- self.serverRevision = 0
- self.serverUpdatedAt = nil
- self.syncStateRaw = SessionSyncState.localOnly.rawValue
- self.lastSyncError = nil
- self.lastSyncedAt = nil
- self.events = []
- self.audioChunks = []
- }
- }
- enum SessionSyncState: String, Codable {
- case localOnly
- case pending
- case syncing
- case synced
- case failed
- case conflict
- }
|