将会话、权限、位置、收款与排队存储拆分为独立模块,同步更新各 ViewModel 与单元测试,并补充素材管理 UI 与个人空间设置交互。 Co-authored-by: Cursor <cursoragent@cursor.com>
192 lines
6.1 KiB
Swift
192 lines
6.1 KiB
Swift
//
|
||
// HomeLocationStateStore.swift
|
||
// suixinkan
|
||
//
|
||
|
||
import Foundation
|
||
|
||
/// 首页在线状态与 2 小时倒计时存储,对齐 Android `LocationStateRepository`。
|
||
final class HomeLocationStateStore {
|
||
|
||
static let shared = HomeLocationStateStore(store: .shared)
|
||
|
||
static let onlineDurationMillis: Int64 = 2 * 60 * 60 * 1000
|
||
|
||
private(set) var isOnline = false
|
||
private(set) var elapsedSeconds: Int64 = 0
|
||
private(set) var nextReportCountdownSeconds: Int64 = 0
|
||
private(set) var reminderMinutes = 0
|
||
|
||
private let store: AppStore
|
||
private var countdownTask: Task<Void, Never>?
|
||
private var anchorTimestamp: Int64?
|
||
private var observers: [ObjectIdentifier: StateObserver] = [:]
|
||
|
||
init(store: AppStore = .shared) {
|
||
self.store = store
|
||
reminderMinutes = store.location.reminderMinutes
|
||
isOnline = store.location.onlineStatus
|
||
}
|
||
|
||
deinit {
|
||
countdownTask?.cancel()
|
||
}
|
||
|
||
/// 监听状态变化,允许首页与独立位置上报页同时刷新。
|
||
func observe(_ owner: AnyObject, handler: @escaping () -> Void) {
|
||
observers[ObjectIdentifier(owner)] = StateObserver(owner: owner, handler: handler)
|
||
}
|
||
|
||
/// 移除指定监听者。
|
||
func removeObserver(_ owner: AnyObject) {
|
||
observers.removeValue(forKey: ObjectIdentifier(owner))
|
||
}
|
||
|
||
/// 启动时恢复在线倒计时。
|
||
func restoreStateIfNeeded() {
|
||
reminderMinutes = store.location.reminderMinutes
|
||
guard store.location.onlineStatus else {
|
||
isOnline = false
|
||
stopCountdown()
|
||
notifyChange()
|
||
return
|
||
}
|
||
let lastTime = store.location.lastReportTime
|
||
guard lastTime > 0 else {
|
||
isOnline = false
|
||
store.location.onlineStatus = false
|
||
store.location.clearLastReportTime()
|
||
stopCountdown()
|
||
notifyChange()
|
||
return
|
||
}
|
||
|
||
let now = Int64(Date().timeIntervalSince1970 * 1000)
|
||
let elapsed = now - lastTime
|
||
if elapsed >= Self.onlineDurationMillis {
|
||
updateOnlineStatus(false)
|
||
store.location.clearLastReportTime()
|
||
notifyChange()
|
||
return
|
||
}
|
||
|
||
anchorTimestamp = lastTime
|
||
let remaining = Self.onlineDurationMillis - elapsed
|
||
elapsedSeconds = max(0, elapsed / 1000)
|
||
nextReportCountdownSeconds = max(0, remaining / 1000)
|
||
startCountdown(anchorTime: lastTime)
|
||
notifyChange()
|
||
}
|
||
|
||
/// 更新在线状态并持久化。
|
||
func updateOnlineStatus(_ online: Bool) {
|
||
isOnline = online
|
||
store.location.onlineStatus = online
|
||
if !online {
|
||
stopCountdown()
|
||
store.location.clearLastReportTime()
|
||
}
|
||
notifyChange()
|
||
}
|
||
|
||
/// 更新提醒分钟数。
|
||
func updateReminderMinutes(_ minutes: Int) {
|
||
reminderMinutes = minutes
|
||
store.location.reminderMinutes = minutes
|
||
notifyChange()
|
||
}
|
||
|
||
/// 开始新的位置上报会话。
|
||
func startLocationReport(at timestamp: Int64 = Int64(Date().timeIntervalSince1970 * 1000)) {
|
||
anchorTimestamp = timestamp
|
||
store.location.lastReportTime = timestamp
|
||
isOnline = true
|
||
store.location.onlineStatus = true
|
||
elapsedSeconds = 0
|
||
nextReportCountdownSeconds = Self.onlineDurationMillis / 1000
|
||
startCountdown(anchorTime: timestamp)
|
||
notifyChange()
|
||
}
|
||
|
||
var countdownDisplayText: String {
|
||
formattedCountdown(from: nextReportCountdownSeconds)
|
||
}
|
||
|
||
/// Android 独立页倒计时格式 `H:MM:SS`。
|
||
var countdownDisplayTextSingleHour: String {
|
||
formattedCountdownSingleHour(from: nextReportCountdownSeconds)
|
||
}
|
||
|
||
/// 格式化倒计时文本 `HH:MM:SS`。
|
||
func formattedCountdown(from seconds: Int64) -> String {
|
||
let hours = seconds / 3600
|
||
let minutes = (seconds % 3600) / 60
|
||
let secs = seconds % 60
|
||
return String(format: "%02d:%02d:%02d", hours, minutes, secs)
|
||
}
|
||
|
||
/// 格式化倒计时文本 `H:MM:SS`。
|
||
func formattedCountdownSingleHour(from seconds: Int64) -> String {
|
||
let hours = seconds / 3600
|
||
let minutes = (seconds % 3600) / 60
|
||
let secs = seconds % 60
|
||
return String(format: "%d:%02d:%02d", hours, minutes, secs)
|
||
}
|
||
|
||
/// 是否到达提前提醒窗口。
|
||
func shouldTriggerTimeoutReminder() -> Bool {
|
||
guard reminderMinutes > 0, isOnline else { return false }
|
||
let reminderSeconds = Int64(reminderMinutes * 60)
|
||
return nextReportCountdownSeconds > 0 && nextReportCountdownSeconds <= reminderSeconds
|
||
}
|
||
|
||
private func startCountdown(anchorTime: Int64) {
|
||
countdownTask?.cancel()
|
||
countdownTask = Task { [weak self] in
|
||
while !Task.isCancelled {
|
||
guard let self else { return }
|
||
guard self.isOnline else { return }
|
||
|
||
let now = Int64(Date().timeIntervalSince1970 * 1000)
|
||
let elapsed = now - anchorTime
|
||
let remaining = Self.onlineDurationMillis - elapsed
|
||
let elapsedSeconds = max(0, elapsed / 1000)
|
||
|
||
self.elapsedSeconds = elapsedSeconds
|
||
if remaining <= 0 {
|
||
self.nextReportCountdownSeconds = 0
|
||
self.updateOnlineStatus(false)
|
||
self.store.location.clearLastReportTime()
|
||
return
|
||
}
|
||
self.nextReportCountdownSeconds = max(0, remaining / 1000)
|
||
self.notifyChange()
|
||
try? await Task.sleep(nanoseconds: 1_000_000_000)
|
||
}
|
||
}
|
||
}
|
||
|
||
private func stopCountdown() {
|
||
countdownTask?.cancel()
|
||
countdownTask = nil
|
||
anchorTimestamp = nil
|
||
elapsedSeconds = 0
|
||
nextReportCountdownSeconds = 0
|
||
}
|
||
|
||
private func notifyChange() {
|
||
observers = observers.filter { $0.value.owner != nil }
|
||
observers.values.forEach { $0.handler() }
|
||
}
|
||
}
|
||
|
||
private final class StateObserver {
|
||
weak var owner: AnyObject?
|
||
let handler: () -> Void
|
||
|
||
init(owner: AnyObject, handler: @escaping () -> Void) {
|
||
self.owner = owner
|
||
self.handler = handler
|
||
}
|
||
}
|