Files
xianyan/ios/XianyanWidget/XianyanWidget.swift
Developer 27672343b8 520
2026-05-20 01:40:09 +08:00

587 lines
20 KiB
Swift

import WidgetKit
import SwiftUI
struct DailySentenceEntry: TimelineEntry {
let date: Date
let sentence: String
let author: String
let isDark: Bool
}
struct ReadlaterEntry: TimelineEntry {
let date: Date
let count: Int
let previewText: String
let previewAuthor: String
let isDark: Bool
}
struct DailyCardEntry: TimelineEntry {
let date: Date
let sentence: String
let author: String
let isDark: Bool
}
struct FortuneEntry: TimelineEntry {
let date: Date
let text: String
let keyword: String
let isDark: Bool
}
struct CountdownEntry: TimelineEntry {
let date: Date
let title: String
let daysLeft: Int
let isDark: Bool
}
struct PomodoroEntry: TimelineEntry {
let date: Date
let remaining: Int
let state: String
let isDark: Bool
}
struct SolarTermEntry: TimelineEntry {
let date: Date
let name: String
let poem: String
let isDark: Bool
}
struct CheckinEntry: TimelineEntry {
let date: Date
let days: Int
let todayDone: Bool
let isDark: Bool
}
private func isDarkMode() -> Bool {
return UserDefaults(suiteName: "group.apps.xy.xianyan.share")?.string(forKey: "widget_theme_mode") == "dark"
}
struct DailySentenceProvider: TimelineProvider {
func placeholder(in context: Context) -> DailySentenceEntry {
DailySentenceEntry(date: Date(), sentence: "生活不止眼前的苟且", author: "高晓松", isDark: false)
}
func getSnapshot(in context: Context, completion: @escaping (DailySentenceEntry) -> Void) {
let defaults = UserDefaults(suiteName: "group.apps.xy.xianyan.share")
let entry = DailySentenceEntry(
date: Date(),
sentence: defaults?.string(forKey: "daily_sentence") ?? "生活不止眼前的苟且",
author: defaults?.string(forKey: "daily_sentence_author") ?? "高晓松",
isDark: isDarkMode()
)
completion(entry)
}
func getTimeline(in context: Context, completion: @escaping (Timeline<DailySentenceEntry>) -> Void) {
let defaults = UserDefaults(suiteName: "group.apps.xy.xianyan.share")
let entry = DailySentenceEntry(
date: Date(),
sentence: defaults?.string(forKey: "daily_sentence") ?? "暂无句子",
author: defaults?.string(forKey: "daily_sentence_author") ?? "",
isDark: isDarkMode()
)
let timeline = Timeline(entries: [entry], policy: .atEnd)
completion(timeline)
}
}
struct ReadlaterProvider: TimelineProvider {
func placeholder(in context: Context) -> ReadlaterEntry {
ReadlaterEntry(date: Date(), count: 0, previewText: "", previewAuthor: "", isDark: false)
}
func getSnapshot(in context: Context, completion: @escaping (ReadlaterEntry) -> Void) {
let defaults = UserDefaults(suiteName: "group.apps.xy.xianyan.share")
let entry = ReadlaterEntry(
date: Date(),
count: defaults?.integer(forKey: "readlater_count") ?? 0,
previewText: defaults?.string(forKey: "readlater_preview_text") ?? "",
previewAuthor: defaults?.string(forKey: "readlater_preview_author") ?? "",
isDark: isDarkMode()
)
completion(entry)
}
func getTimeline(in context: Context, completion: @escaping (Timeline<ReadlaterEntry>) -> Void) {
let defaults = UserDefaults(suiteName: "group.apps.xy.xianyan.share")
let entry = ReadlaterEntry(
date: Date(),
count: defaults?.integer(forKey: "readlater_count") ?? 0,
previewText: defaults?.string(forKey: "readlater_preview_text") ?? "",
previewAuthor: defaults?.string(forKey: "readlater_preview_author") ?? "",
isDark: isDarkMode()
)
let timeline = Timeline(entries: [entry], policy: .atEnd)
completion(timeline)
}
}
struct DailyCardProvider: TimelineProvider {
func placeholder(in context: Context) -> DailyCardEntry {
DailyCardEntry(date: Date(), sentence: "生活不止眼前的苟且", author: "高晓松", isDark: false)
}
func getSnapshot(in context: Context, completion: @escaping (DailyCardEntry) -> Void) {
let defaults = UserDefaults(suiteName: "group.apps.xy.xianyan.share")
let entry = DailyCardEntry(
date: Date(),
sentence: defaults?.string(forKey: "daily_sentence") ?? "暂无句子",
author: defaults?.string(forKey: "daily_sentence_author") ?? "",
isDark: isDarkMode()
)
completion(entry)
}
func getTimeline(in context: Context, completion: @escaping (Timeline<DailyCardEntry>) -> Void) {
let defaults = UserDefaults(suiteName: "group.apps.xy.xianyan.share")
let entry = DailyCardEntry(
date: Date(),
sentence: defaults?.string(forKey: "daily_sentence") ?? "暂无句子",
author: defaults?.string(forKey: "daily_sentence_author") ?? "",
isDark: isDarkMode()
)
let timeline = Timeline(entries: [entry], policy: .atEnd)
completion(timeline)
}
}
struct FortuneProvider: TimelineProvider {
func placeholder(in context: Context) -> FortuneEntry {
FortuneEntry(date: Date(), text: "今日运势不错", keyword: "", isDark: false)
}
func getSnapshot(in context: Context, completion: @escaping (FortuneEntry) -> Void) {
let defaults = UserDefaults(suiteName: "group.apps.xy.xianyan.share")
let entry = FortuneEntry(
date: Date(),
text: defaults?.string(forKey: "fortune_text") ?? "今日运势不错",
keyword: defaults?.string(forKey: "fortune_keyword") ?? "",
isDark: isDarkMode()
)
completion(entry)
}
func getTimeline(in context: Context, completion: @escaping (Timeline<FortuneEntry>) -> Void) {
let defaults = UserDefaults(suiteName: "group.apps.xy.xianyan.share")
let entry = FortuneEntry(
date: Date(),
text: defaults?.string(forKey: "fortune_text") ?? "今日运势不错",
keyword: defaults?.string(forKey: "fortune_keyword") ?? "",
isDark: isDarkMode()
)
let timeline = Timeline(entries: [entry], policy: .atEnd)
completion(timeline)
}
}
struct CountdownProvider: TimelineProvider {
func placeholder(in context: Context) -> CountdownEntry {
CountdownEntry(date: Date(), title: "倒计时", daysLeft: 0, isDark: false)
}
func getSnapshot(in context: Context, completion: @escaping (CountdownEntry) -> Void) {
let defaults = UserDefaults(suiteName: "group.apps.xy.xianyan.share")
let title = defaults?.string(forKey: "countdown_title") ?? "倒计时"
let targetStr = defaults?.string(forKey: "countdown_target") ?? ""
let days = Self.calculateDays(from: targetStr)
completion(CountdownEntry(date: Date(), title: title, daysLeft: days, isDark: isDarkMode()))
}
func getTimeline(in context: Context, completion: @escaping (Timeline<CountdownEntry>) -> Void) {
let defaults = UserDefaults(suiteName: "group.apps.xy.xianyan.share")
let title = defaults?.string(forKey: "countdown_title") ?? "倒计时"
let targetStr = defaults?.string(forKey: "countdown_target") ?? ""
let days = Self.calculateDays(from: targetStr)
let entry = CountdownEntry(date: Date(), title: title, daysLeft: days, isDark: isDarkMode())
let timeline = Timeline(entries: [entry], policy: .atEnd)
completion(timeline)
}
private static func calculateDays(from isoString: String) -> Int {
guard !isoString.isEmpty else { return 0 }
let formatter = ISO8601DateFormatter()
formatter.formatOptions = [.withFullDate]
guard let target = formatter.date(from: String(isoString.prefix(10))) else { return 0 }
return Calendar.current.dateComponents([.day], from: Date(), to: target).day ?? 0
}
}
struct PomodoroProvider: TimelineProvider {
func placeholder(in context: Context) -> PomodoroEntry {
PomodoroEntry(date: Date(), remaining: 1500, state: "idle", isDark: false)
}
func getSnapshot(in context: Context, completion: @escaping (PomodoroEntry) -> Void) {
let defaults = UserDefaults(suiteName: "group.apps.xy.xianyan.share")
let entry = PomodoroEntry(
date: Date(),
remaining: defaults?.integer(forKey: "pomodoro_remaining") ?? 1500,
state: defaults?.string(forKey: "pomodoro_state") ?? "idle",
isDark: isDarkMode()
)
completion(entry)
}
func getTimeline(in context: Context, completion: @escaping (Timeline<PomodoroEntry>) -> Void) {
let defaults = UserDefaults(suiteName: "group.apps.xy.xianyan.share")
let entry = PomodoroEntry(
date: Date(),
remaining: defaults?.integer(forKey: "pomodoro_remaining") ?? 1500,
state: defaults?.string(forKey: "pomodoro_state") ?? "idle",
isDark: isDarkMode()
)
let timeline = Timeline(entries: [entry], policy: .atEnd)
completion(timeline)
}
}
struct SolarTermProvider: TimelineProvider {
func placeholder(in context: Context) -> SolarTermEntry {
SolarTermEntry(date: Date(), name: "立春", poem: "春到人间草木知", isDark: false)
}
func getSnapshot(in context: Context, completion: @escaping (SolarTermEntry) -> Void) {
let defaults = UserDefaults(suiteName: "group.apps.xy.xianyan.share")
let entry = SolarTermEntry(
date: Date(),
name: defaults?.string(forKey: "solar_term_name") ?? "立春",
poem: defaults?.string(forKey: "solar_term_poem") ?? "春到人间草木知",
isDark: isDarkMode()
)
completion(entry)
}
func getTimeline(in context: Context, completion: @escaping (Timeline<SolarTermEntry>) -> Void) {
let defaults = UserDefaults(suiteName: "group.apps.xy.xianyan.share")
let entry = SolarTermEntry(
date: Date(),
name: defaults?.string(forKey: "solar_term_name") ?? "立春",
poem: defaults?.string(forKey: "solar_term_poem") ?? "春到人间草木知",
isDark: isDarkMode()
)
let timeline = Timeline(entries: [entry], policy: .atEnd)
completion(timeline)
}
}
struct CheckinProvider: TimelineProvider {
func placeholder(in context: Context) -> CheckinEntry {
CheckinEntry(date: Date(), days: 0, todayDone: false, isDark: false)
}
func getSnapshot(in context: Context, completion: @escaping (CheckinEntry) -> Void) {
let defaults = UserDefaults(suiteName: "group.apps.xy.xianyan.share")
let entry = CheckinEntry(
date: Date(),
days: defaults?.integer(forKey: "checkin_days") ?? 0,
todayDone: defaults?.bool(forKey: "checkin_today") ?? false,
isDark: isDarkMode()
)
completion(entry)
}
func getTimeline(in context: Context, completion: @escaping (Timeline<CheckinEntry>) -> Void) {
let defaults = UserDefaults(suiteName: "group.apps.xy.xianyan.share")
let entry = CheckinEntry(
date: Date(),
days: defaults?.integer(forKey: "checkin_days") ?? 0,
todayDone: defaults?.bool(forKey: "checkin_today") ?? false,
isDark: isDarkMode()
)
let timeline = Timeline(entries: [entry], policy: .atEnd)
completion(timeline)
}
}
struct WidgetColors {
let bg: Color
let primary: Color
let secondary: Color
init(isDark: Bool) {
if isDark {
bg = Color(red: 28/255, green: 28/255, blue: 30/255)
primary = Color(red: 224/255, green: 224/255, blue: 224/255)
secondary = Color(red: 170/255, green: 170/255, blue: 170/255)
} else {
bg = Color.white
primary = Color(red: 51/255, green: 51/255, blue: 51/255)
secondary = Color(red: 136/255, green: 136/255, blue: 136/255)
}
}
}
struct DailySentenceWidgetEntryView: View {
var entry: DailySentenceEntry
var body: some View {
let colors = WidgetColors(isDark: entry.isDark)
VStack(alignment: .leading, spacing: 8) {
Text(entry.sentence)
.font(.body)
.foregroundColor(colors.primary)
.lineLimit(3)
.multilineTextAlignment(.leading)
Spacer()
Text("\(entry.author)")
.font(.caption)
.foregroundColor(colors.secondary)
.frame(maxWidth: .infinity, alignment: .trailing)
}
.padding()
.containerBackground(for: .widget) { colors.bg }
}
}
struct ReadlaterWidgetEntryView: View {
var entry: ReadlaterEntry
var body: some View {
let colors = WidgetColors(isDark: entry.isDark)
VStack(alignment: .leading, spacing: 6) {
HStack {
Text("\(entry.count)条未读")
.font(.headline)
.foregroundColor(colors.primary)
Spacer()
}
if !entry.previewText.isEmpty {
Text(entry.previewText)
.font(.caption)
.foregroundColor(colors.secondary)
.lineLimit(2)
}
}
.padding()
.containerBackground(for: .widget) { colors.bg }
}
}
struct DailyCardWidgetEntryView: View {
var entry: DailyCardEntry
var body: some View {
let colors = WidgetColors(isDark: entry.isDark)
VStack(spacing: 8) {
Spacer()
Text(entry.sentence)
.font(.body)
.foregroundColor(colors.primary)
.lineLimit(4)
.multilineTextAlignment(.center)
Text("\(entry.author)")
.font(.caption)
.foregroundColor(colors.secondary)
Spacer()
}
.padding()
.containerBackground(for: .widget) { colors.bg }
}
}
struct FortuneWidgetEntryView: View {
var entry: FortuneEntry
var body: some View {
let colors = WidgetColors(isDark: entry.isDark)
VStack(alignment: .leading, spacing: 6) {
Text(entry.keyword)
.font(.title2)
Text(entry.text)
.font(.caption)
.foregroundColor(colors.primary)
.lineLimit(2)
}
.padding()
.containerBackground(for: .widget) { colors.bg }
}
}
struct CountdownWidgetEntryView: View {
var entry: CountdownEntry
var body: some View {
let colors = WidgetColors(isDark: entry.isDark)
VStack(spacing: 4) {
Text(entry.title)
.font(.caption)
.foregroundColor(colors.secondary)
Text("\(entry.daysLeft)")
.font(.system(size: 36, weight: .bold, design: .rounded))
.foregroundColor(colors.primary)
Text("")
.font(.caption)
.foregroundColor(colors.secondary)
}
.padding()
.containerBackground(for: .widget) { colors.bg }
}
}
struct PomodoroWidgetEntryView: View {
var entry: PomodoroEntry
var body: some View {
let colors = WidgetColors(isDark: entry.isDark)
let min = entry.remaining / 60
let sec = entry.remaining % 60
VStack(spacing: 4) {
Text(String(format: "%02d:%02d", min, sec))
.font(.system(size: 32, weight: .bold, design: .monospaced))
.foregroundColor(colors.primary)
Text(entry.state == "running" ? "专注中" : entry.state == "paused" ? "已暂停" : "准备开始")
.font(.caption2)
.foregroundColor(colors.secondary)
}
.padding()
.containerBackground(for: .widget) { colors.bg }
}
}
struct SolarTermWidgetEntryView: View {
var entry: SolarTermEntry
var body: some View {
let colors = WidgetColors(isDark: entry.isDark)
VStack(alignment: .leading, spacing: 6) {
Text(entry.name)
.font(.title3)
.fontWeight(.bold)
.foregroundColor(colors.primary)
Text(entry.poem)
.font(.caption)
.foregroundColor(colors.secondary)
.lineLimit(2)
}
.padding()
.containerBackground(for: .widget) { colors.bg }
}
}
struct CheckinWidgetEntryView: View {
var entry: CheckinEntry
var body: some View {
let colors = WidgetColors(isDark: entry.isDark)
VStack(spacing: 4) {
Text("连续\(entry.days)")
.font(.title3)
.fontWeight(.bold)
.foregroundColor(colors.primary)
Text(entry.todayDone ? "✅ 今日已签" : "📝 点击签到")
.font(.caption)
.foregroundColor(colors.secondary)
}
.padding()
.containerBackground(for: .widget) { colors.bg }
}
}
struct DailySentenceWidget: Widget {
let kind: String = "DailySentenceWidget"
var body: some WidgetConfiguration {
StaticConfiguration(kind: kind, provider: DailySentenceProvider()) { entry in
DailySentenceWidgetEntryView(entry: entry)
}
.configurationDisplayName("每日一句")
.description("每日推送精选句子")
.supportedFamilies([.systemSmall, .systemMedium])
}
}
struct ReadlaterWidget: Widget {
let kind: String = "ReadlaterWidget"
var body: some WidgetConfiguration {
StaticConfiguration(kind: kind, provider: ReadlaterProvider()) { entry in
ReadlaterWidgetEntryView(entry: entry)
}
.configurationDisplayName("稍后读")
.description("未读数量和最新预览")
.supportedFamilies([.systemSmall, .systemMedium])
}
}
struct DailyCardWidget: Widget {
let kind: String = "DailyCardWidget"
var body: some WidgetConfiguration {
StaticConfiguration(kind: kind, provider: DailyCardProvider()) { entry in
DailyCardWidgetEntryView(entry: entry)
}
.configurationDisplayName("日签卡片")
.description("精美日签含日期和句子")
.supportedFamilies([.systemMedium, .systemLarge])
}
}
struct FortuneWidget: Widget {
let kind: String = "FortuneWidget"
var body: some WidgetConfiguration {
StaticConfiguration(kind: kind, provider: FortuneProvider()) { entry in
FortuneWidgetEntryView(entry: entry)
}
.configurationDisplayName("每日运势")
.description("今日运势和幸运关键词")
.supportedFamilies([.systemSmall])
}
}
struct CountdownWidget: Widget {
let kind: String = "CountdownWidget"
var body: some WidgetConfiguration {
StaticConfiguration(kind: kind, provider: CountdownProvider()) { entry in
CountdownWidgetEntryView(entry: entry)
}
.configurationDisplayName("倒计时")
.description("自定义倒计时事件")
.supportedFamilies([.systemSmall, .systemMedium])
}
}
struct PomodoroWidget: Widget {
let kind: String = "PomodoroWidget"
var body: some WidgetConfiguration {
StaticConfiguration(kind: kind, provider: PomodoroProvider()) { entry in
PomodoroWidgetEntryView(entry: entry)
}
.configurationDisplayName("番茄钟")
.description("桌面快捷专注计时")
.supportedFamilies([.systemSmall])
}
}
struct SolarTermWidget: Widget {
let kind: String = "SolarTermWidget"
var body: some WidgetConfiguration {
StaticConfiguration(kind: kind, provider: SolarTermProvider()) { entry in
SolarTermWidgetEntryView(entry: entry)
}
.configurationDisplayName("节气诗词")
.description("当前节气与对应诗词")
.supportedFamilies([.systemSmall, .systemMedium])
}
}
struct CheckinWidget: Widget {
let kind: String = "CheckinWidget"
var body: some WidgetConfiguration {
StaticConfiguration(kind: kind, provider: CheckinProvider()) { entry in
CheckinWidgetEntryView(entry: entry)
}
.configurationDisplayName("每日签到")
.description("连续签到天数和快捷签到")
.supportedFamilies([.systemSmall])
}
}
@main
struct XianyanWidgetBundle: WidgetBundle {
var body: some Widget {
DailySentenceWidget()
ReadlaterWidget()
DailyCardWidget()
FortuneWidget()
CountdownWidget()
PomodoroWidget()
SolarTermWidget()
CheckinWidget()
}
}