Files
legado-ios/App/Features/Activity/ReadingStatisticsView.swift
T

176 lines
7.2 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import SwiftUI
struct ReadingStatisticsView: View {
@EnvironmentObject private var activity: ReadingActivityModel
@ScaledMetric(relativeTo: .largeTitle) private var durationFontSize = 42
@ScaledMetric(relativeTo: .caption2) private var chartHeight = 138
private var statistics: ReadingStatistics { activity.statistics }
var body: some View {
ScrollView {
VStack(spacing: 0) {
ReadingPageHeader(title: "阅读统计", subtitle: "每一页,都算数") { EmptyView() }
VStack(spacing: 16) {
todayCard
overviewGrid
weeklyCard
historyLink
}
.padding(.horizontal, 24)
.padding(.bottom, 32)
}
}
.background(ReadingTheme.canvas)
.navigationBarHidden(true)
}
private var todayCard: some View {
VStack(alignment: .leading, spacing: 10) {
HStack(alignment: .firstTextBaseline) {
Text("今日阅读")
.font(.subheadline)
Spacer()
Text(Date.now.formatted(.dateTime.locale(Locale(identifier: "zh_CN")).month().day()))
.font(.caption)
}
.foregroundStyle(ReadingTheme.onAccent.opacity(0.7))
Text(duration(statistics.todayReadTime, compact: false))
.font(.system(size: durationFontSize, weight: .medium))
.foregroundStyle(ReadingTheme.onAccent)
.readingNumericTransition()
.minimumScaleFactor(0.7)
.lineLimit(1)
Text(statistics.todayReadTime == 0 ? "今天还没有阅读记录" : "已记录今天的阅读时长")
.font(.caption)
.foregroundStyle(ReadingTheme.onAccent.opacity(0.65))
}
.frame(maxWidth: .infinity, alignment: .leading)
.padding(24)
.background(ReadingTheme.accent, in: RoundedRectangle(cornerRadius: 20))
}
private var overviewGrid: some View {
ReadingCard {
VStack(spacing: 18) {
HStack(alignment: .top, spacing: 20) {
statisticTile(title: "累计阅读", value: duration(statistics.totalReadTime, compact: true))
statisticTile(title: "阅读书籍", value: "\(statistics.bookCount) 本")
}
Rectangle().fill(ReadingTheme.separator).frame(height: 0.5)
HStack(alignment: .top, spacing: 20) {
statisticTile(
title: "近 7 天",
value: duration(statistics.recentDays.reduce(0) { $0 + $1.readTime }, compact: true)
)
statisticTile(title: "连续阅读", value: "\(statistics.currentStreak) 天")
}
}
}
}
private func statisticTile(title: String, value: String) -> some View {
VStack(alignment: .leading, spacing: 8) {
Text(title)
.font(.caption)
.foregroundStyle(.secondary)
Text(value)
.font(.title3.weight(.semibold))
.readingNumericTransition()
}
.frame(maxWidth: .infinity, alignment: .leading)
.accessibilityElement(children: .ignore)
.accessibilityLabel("\(title),\(value)")
}
private var weeklyCard: some View {
ReadingCard {
VStack(alignment: .leading, spacing: 18) {
HStack {
Text("最近 7 天")
.font(.headline)
Spacer()
Text("每日阅读时长")
.font(.caption)
.foregroundStyle(.secondary)
}
HStack(alignment: .bottom, spacing: 10) {
ForEach(statistics.recentDays, id: \.date) { day in
VStack(spacing: 7) {
Text(barLabel(day.readTime))
.font(.caption2.monospacedDigit())
.foregroundStyle(.secondary)
.lineLimit(1)
.minimumScaleFactor(0.7)
RoundedRectangle(cornerRadius: 3)
.fill(day.readTime > 0 ? ReadingTheme.accent.opacity(Calendar.current.isDateInToday(day.date) ? 1 : 0.22) : ReadingTheme.separator)
.frame(width: 22, height: barHeight(day.readTime))
Text(weekday(day.date))
.font(.caption2)
.foregroundStyle(Calendar.current.isDateInToday(day.date) ? ReadingTheme.accent : .secondary)
}
.frame(maxWidth: .infinity)
.accessibilityElement(children: .ignore)
.accessibilityLabel("\(weekday(day.date)),阅读 \(duration(day.readTime, compact: false))")
}
}
.frame(height: chartHeight, alignment: .bottom)
}
}
}
private var historyLink: some View {
NavigationLink {
ReadingHistoryView()
} label: {
ReadingCard {
HStack(spacing: 12) {
Image(systemName: "clock.arrow.circlepath").font(.title3)
Text("阅读记录").font(.subheadline.weight(.medium))
Spacer()
Image(systemName: "arrow.up.right").font(.subheadline)
}
.foregroundStyle(ReadingTheme.accent)
.frame(minHeight: 24)
.contentShape(Rectangle())
}
}
.buttonStyle(.plain)
}
private func barHeight(_ milliseconds: Int64) -> CGFloat {
let maximum = max(statistics.recentDays.map(\.readTime).max() ?? 0, 1)
guard milliseconds > 0 else { return 2 }
return 12 + 70 * CGFloat(milliseconds) / CGFloat(maximum)
}
private func barLabel(_ milliseconds: Int64) -> String {
guard milliseconds >= 60_000 else { return milliseconds > 0 ? "<1分" : "0" }
if milliseconds < 3_600_000 { return "\(milliseconds / 60_000)分" }
let hours = Double(milliseconds) / 3_600_000
return String(format: "%.1f时", hours)
}
private func weekday(_ date: Date) -> String {
let formatter = DateFormatter()
formatter.locale = Locale(identifier: "zh_CN")
formatter.dateFormat = "EEE"
return formatter.string(from: date)
}
private func duration(_ milliseconds: Int64, compact: Bool) -> String {
let value = max(milliseconds, 0)
let hours = value / 3_600_000
let minutes = value % 3_600_000 / 60_000
if compact {
if hours > 0 { return minutes > 0 ? "\(hours)时\(minutes)分" : "\(hours)小时" }
return "\(minutes)分钟"
}
let seconds = value % 60_000 / 1_000
if hours > 0 { return minutes > 0 ? "\(hours)小时 \(minutes)分钟" : "\(hours)小时" }
if minutes > 0 { return "\(minutes)分钟" }
return "\(seconds)秒"
}
}