Files
XStats/Widget/RestWidget.swift

125 lines
5.5 KiB
Swift

// Copyright (C) 2026 ysicing
// SPDX-License-Identifier: AGPL-3.0-or-later
import Localization
import SwiftUI
import WidgetKit
private struct RestEntry: TimelineEntry {
let date: Date
let enabled: Bool
let isResting: Bool
let running: Bool
let remaining: TimeInterval
let deadline: Date
let duration: TimeInterval
}
private struct RestProvider: TimelineProvider {
func placeholder(in context: Context) -> RestEntry {
RestEntry(date: .now, enabled: true, isResting: false, running: true, remaining: 20 * 60,
deadline: .now.addingTimeInterval(20 * 60), duration: 20 * 60)
}
func getSnapshot(in context: Context, completion: @escaping (RestEntry) -> Void) {
completion(context.isPreview ? placeholder(in: context) : read())
}
func getTimeline(in context: Context, completion: @escaping (Timeline<RestEntry>) -> Void) {
let entry = read()
guard entry.enabled, entry.running, entry.deadline > entry.date else {
completion(Timeline(entries: [entry], policy: .after(entry.date.addingTimeInterval(3600))))
return
}
// 倒计时文字可由系统更新;圆环需要在阶段内提供新的 TimelineEntry。
var entries = [entry]
var date = entry.date
while date < entry.deadline {
date = min(date.addingTimeInterval(60), entry.deadline)
entries.append(RestEntry(date: date, enabled: entry.enabled, isResting: entry.isResting,
running: entry.running, remaining: entry.remaining,
deadline: entry.deadline, duration: entry.duration))
}
completion(Timeline(entries: entries, policy: .after(entry.deadline)))
}
private func read() -> RestEntry {
let defaults = UserDefaults(suiteName: "group.work.12306.xstats")
if let language = defaults?.string(forKey: "rest.language").flatMap(AppLanguage.init(rawValue:)) {
L10n.configure(language)
}
let now = Date()
return RestEntry(date: now, enabled: defaults?.bool(forKey: "rest.enabled") ?? false,
isResting: ["rest", "longRest"].contains(defaults?.string(forKey: "rest.phase") ?? ""),
running: defaults?.bool(forKey: "rest.running") ?? false,
remaining: defaults?.double(forKey: "rest.remaining") ?? 0,
deadline: Date(timeIntervalSince1970: defaults?.double(forKey: "rest.deadline") ?? 0),
duration: TimeInterval(defaults?.double(forKey: "rest.duration") ?? 0))
}
}
private struct RestWidgetView: View {
@Environment(\.widgetFamily) private var family
let entry: RestEntry
var body: some View {
HStack(spacing: 12) {
ZStack {
Circle().stroke(Color.cyan.opacity(0.17), lineWidth: 8)
Circle().trim(from: 0, to: progress)
.stroke(.cyan, style: StrokeStyle(lineWidth: 8, lineCap: .round))
.rotationEffect(.degrees(-90))
Image(systemName: entry.isResting ? "eye" : "timer")
.font(.system(size: family == .systemSmall ? 28 : 36, weight: .ultraLight))
}
.frame(width: family == .systemSmall ? 76 : 96, height: family == .systemSmall ? 76 : 96)
VStack(alignment: .leading, spacing: 6) {
Text(tr(entry.enabled ? (entry.running ? (entry.isResting ? "休息中" : "专注中") : "已暂停")
: "护眼休息未开启"))
.font(.system(size: 13, weight: .medium))
.foregroundStyle(.secondary)
if entry.enabled {
if entry.running {
Text(entry.deadline, style: .timer)
.font(.system(size: 22, weight: .semibold, design: .rounded))
.monospacedDigit()
} else {
Text(String(format: "%02d:%02d", max(0, Int(entry.remaining)) / 60,
max(0, Int(entry.remaining)) % 60))
.font(.system(size: 22, weight: .semibold, design: .rounded))
.monospacedDigit()
}
}
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.containerBackground(.background, for: .widget)
.environment(\.locale, L10n.locale(for: L10n.language))
.environment(\.layoutDirection, L10n.language.isRightToLeft ? .rightToLeft : .leftToRight)
}
private var progress: Double {
guard entry.enabled, entry.duration > 0 else { return 0 }
let remaining = entry.running ? max(0, entry.deadline.timeIntervalSince(entry.date)) : entry.remaining
return min(1, max(0, 1 - remaining / entry.duration))
}
}
struct RestWidget: Widget {
init() {
let defaults = UserDefaults(suiteName: "group.work.12306.xstats")
let language = defaults?.string(forKey: "rest.language").flatMap(AppLanguage.init(rawValue:)) ?? .system
L10n.configure(language)
}
var body: some WidgetConfiguration {
StaticConfiguration(kind: "rest", provider: RestProvider()) { entry in
RestWidgetView(entry: entry)
}
.configurationDisplayName(tr("番茄钟"))
.description(tr("查看下一次休息或当前休息倒计时。"))
.supportedFamilies([.systemSmall, .systemMedium])
}
}