mirror of
https://github.com/Aminark/legado-ios.git
synced 2026-10-20 19:21:22 +08:00
176 lines
7.2 KiB
Swift
176 lines
7.2 KiB
Swift
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)秒"
|
||
}
|
||
}
|