jsdw_ios/QuickLocation/Section/Home/AppUsageReport/AppUsageReportViewModel.swift

188 lines
6.4 KiB
Swift

//
// AppUsageReportViewModel.swift
// QuickLocation
//
import Foundation
import RxSwift
import RxCocoa
import UIKit
enum AppUsageReportPeriod: String {
case week
case month
var title: String {
switch self {
case .week:
return ""
case .month:
return ""
}
}
var maximumUsageSeconds: Int {
switch self {
case .week:
return 24 * 7 * 60 * 60
case .month:
return 24 * 31 * 60 * 60
}
}
}
final class AppUsageReportViewModel {
enum ContentState {
case loading
case content
case empty
case failure
}
struct AppItem {
let rank: Int
let name: String
let iconURL: String
let usageTime: Int
let percentage: Int
let color: UIColor
}
struct Snapshot {
let state: ContentState
let period: AppUsageReportPeriod
let totalUsageTime: Int
let apps: [AppItem]
}
let members: [GroupMemberModel]
let selectedMemberId: BehaviorRelay<String>
let selectedPeriod = BehaviorRelay<AppUsageReportPeriod>(value: .week)
let snapshot = BehaviorRelay<Snapshot>(value: .init(state: .loading, period: .week, totalUsageTime: 0, apps: []))
private let groupKey: String
private let disposeBag = DisposeBag()
private var reportCache: [String: PhoneUsagePeriodModel] = [:]
private var requestIDs: [String: UUID] = [:]
init(members: [GroupMemberModel], selectedUserId: String, groupKey: String) {
self.members = members
self.groupKey = groupKey
self.selectedMemberId = BehaviorRelay(value: selectedUserId)
let selectedUser = selectedMemberId.asObservable()
let selectedUsagePeriod = selectedPeriod.asObservable()
let selection: Observable<(String, AppUsageReportPeriod)> = Observable.combineLatest(
selectedUser,
selectedUsagePeriod
)
selection
.distinctUntilChanged { previous, next in
previous.0 == next.0 && previous.1 == next.1
}
.subscribe(onNext: { [weak self] selection in
self?.refresh(userId: selection.0, period: selection.1)
})
.disposed(by: disposeBag)
}
private func refresh(userId: String, period: AppUsageReportPeriod) {
guard !userId.isEmpty, !groupKey.isEmpty else {
snapshot.accept(makeSnapshot(state: .empty, period: period, report: nil))
return
}
let cacheKey = cacheKey(userId: userId, period: period)
if let report = reportCache[cacheKey] {
snapshot.accept(makeSnapshot(state: report.favoriteApps.isEmpty ? .empty : .content, period: period, report: report))
return
}
snapshot.accept(makeSnapshot(state: .loading, period: period, report: nil))
guard requestIDs[cacheKey] == nil else { return }
let requestID = UUID()
requestIDs[cacheKey] = requestID
UserService.phoneUsagePeriod(userId: userId, groupKey: groupKey, period: period.rawValue)
.subscribe(onNext: { [weak self] response in
guard let self, self.requestIDs[cacheKey] == requestID else { return }
self.requestIDs.removeValue(forKey: cacheKey)
guard response.code == "0", let report = response.model else {
self.applyFailureIfCurrent(userId: userId, period: period)
return
}
self.reportCache[cacheKey] = report
guard self.selectedMemberId.value == userId, self.selectedPeriod.value == period else { return }
self.snapshot.accept(
self.makeSnapshot(
state: report.favoriteApps.isEmpty ? .empty : .content,
period: period,
report: report
)
)
}, onError: { [weak self] _ in
guard let self, self.requestIDs[cacheKey] == requestID else { return }
self.requestIDs.removeValue(forKey: cacheKey)
self.applyFailureIfCurrent(userId: userId, period: period)
})
.disposed(by: disposeBag)
}
private func applyFailureIfCurrent(userId: String, period: AppUsageReportPeriod) {
guard selectedMemberId.value == userId, selectedPeriod.value == period else { return }
snapshot.accept(makeSnapshot(state: .failure, period: period, report: nil))
}
private func cacheKey(userId: String, period: AppUsageReportPeriod) -> String {
"\(groupKey)|\(userId)|\(period.rawValue)"
}
private func makeSnapshot(
state: ContentState,
period: AppUsageReportPeriod,
report: PhoneUsagePeriodModel?
) -> Snapshot {
let rawApps = Array((report?.favoriteApps ?? []).filter { $0.usageTime > 0 }.prefix(5))
let totalUsageTime = max(report?.totalUsageTime ?? 0, 0)
let colors = Self.itemColors
let apps = rawApps.enumerated().map { index, app in
let usageTime = max(app.usageTime, 0)
let fallbackPercentage = totalUsageTime > 0
? Int((Double(usageTime) / Double(totalUsageTime) * 100).rounded())
: 0
let percentage = min(max(app.percentage ?? fallbackPercentage, 0), 100)
return AppItem(
rank: index + 1,
name: app.appName.isEmpty ? "未知应用" : app.appName,
iconURL: app.appIcon,
usageTime: usageTime,
percentage: percentage,
color: colors[index % colors.count]
)
}
let resolvedState: ContentState = apps.isEmpty && state == .content ? .empty : state
return Snapshot(state: resolvedState, period: period, totalUsageTime: totalUsageTime, apps: apps)
}
private static let itemColors = [
UIColor(hexStr: "#C3ECFF"),
UIColor(hexStr: "#E3FF9C"),
UIColor(hexStr: "#FFEAA4"),
UIColor(hexStr: "#C5FFB7"),
UIColor(hexStr: "#FFDCF5")
]
}
extension AppUsageReportViewModel.AppItem {
var durationText: String {
let hours = usageTime / 3600
let minutes = (usageTime % 3600) / 60
if hours > 0 {
return minutes > 0 ? "\(hours)h \(minutes)m" : "\(hours)h"
}
return "\(minutes)m"
}
}