Implement permission-driven home tab aligned with Android.

Load role-permission on first visit to drive menus, role-based layout, store card, and location reporting with account-scoped persistence and unit tests.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-06 17:27:54 +08:00
parent 75d0cb1f9a
commit 3b17b7f7f0
27 changed files with 2573 additions and 10 deletions

View File

@ -0,0 +1,139 @@
//
// HomeLocationStateStore.swift
// suixinkan
//
import Foundation
/// 线 2 Android `LocationStateRepository`
final class HomeLocationStateStore {
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
var onStateChange: (() -> Void)?
private let store: AppStore
private var countdownTask: Task<Void, Never>?
private var anchorTimestamp: Int64?
init(store: AppStore = .shared) {
self.store = store
reminderMinutes = store.locationReminderMinutes
isOnline = store.onlineStatus
}
/// 线
func restoreStateIfNeeded() {
guard store.onlineStatus else { return }
let lastTime = store.lastLocationReportTime
guard lastTime > 0 else { return }
let now = Int64(Date().timeIntervalSince1970 * 1000)
let elapsed = now - lastTime
if elapsed >= Self.onlineDurationMillis {
updateOnlineStatus(false)
store.clearLastLocationReportTime()
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.onlineStatus = online
if !online {
stopCountdown()
store.clearLastLocationReportTime()
}
notifyChange()
}
///
func updateReminderMinutes(_ minutes: Int) {
reminderMinutes = minutes
store.locationReminderMinutes = minutes
notifyChange()
}
///
func startLocationReport(at timestamp: Int64 = Int64(Date().timeIntervalSince1970 * 1000)) {
anchorTimestamp = timestamp
store.lastLocationReportTime = timestamp
isOnline = true
store.onlineStatus = true
elapsedSeconds = 0
nextReportCountdownSeconds = Self.onlineDurationMillis / 1000
startCountdown(anchorTime: timestamp)
notifyChange()
}
/// `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)
}
var countdownDisplayText: String {
formattedCountdown(from: nextReportCountdownSeconds)
}
///
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.clearLastLocationReportTime()
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() {
onStateChange?()
}
}