188 lines
6.4 KiB
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"
|
|
}
|
|
}
|