import SwiftUI import WidgetKit // MARK: - Dot-grid progress (day / week / month / year) enum TimePeriod { case day, week, month, year } struct ProgressDotView: View { let period: TimePeriod let accent: Color @Environment(\.widgetFamily) private var family private var label: String { switch period { case .day: let f = DateFormatter(); f.dateFormat = "EEEE d"; return f.string(from: Date()) case .week: return "This week" case .month: let f = DateFormatter(); f.dateFormat = "MMMM"; return f.string(from: Date()) case .year: let f = DateFormatter(); f.dateFormat = "yyyy"; return f.string(from: Date()) } } private var progress: Double { let cal = Calendar.current; let now = Date() switch period { case .day: let sod = cal.startOfDay(for: now) return (now.timeIntervalSince(sod)) / 86400 case .week: let comps = cal.dateComponents([.weekday], from: now) let wd = (comps.weekday ?? 1) - 1 let secIntoDay = now.timeIntervalSince(cal.startOfDay(for: now)) return (Double(wd) * 86400 + secIntoDay) / (7 * 86400) case .month: let range = cal.range(of: .day, in: .month, for: now)! let day = cal.component(.day, from: now) - 1 let secIntoDay = now.timeIntervalSince(cal.startOfDay(for: now)) return (Double(day) * 86400 + secIntoDay) / (Double(range.count) * 86400) case .year: let start = cal.date(from: cal.dateComponents([.year], from: now))! let end = cal.date(byAdding: .year, value: 1, to: start)! return now.timeIntervalSince(start) / end.timeIntervalSince(start) } } private var subLabel: String { let pct = Int(progress * 100) return "\(pct)%" } var body: some View { let isMedium = family == .systemMedium VStack(alignment: .leading, spacing: isMedium ? 10 : 6) { HStack(alignment: .firstTextBaseline) { Text(label) .font(.system(size: isMedium ? 22 : 14, weight: .bold)) .foregroundColor(.white) .lineLimit(1) Spacer() Text(subLabel) .font(.system(size: isMedium ? 22 : 13, weight: .bold)) .foregroundColor(Color(white: 0.5)) } DotGrid(total: 100, filled: Int(progress * 100), color: accent) } .padding(.horizontal, isMedium ? 4 : 0) .containerBackground(.black, for: .widget) } } // MARK: - Tasks completed today struct TasksCompleteView: View { @Environment(\.widgetFamily) private var family private let teal = Color(red: 0.48, green: 0.87, blue: 0.80) private var stats: (done: Int, total: Int) { todayTaskCompletion() } private var percent: Int { let s = stats guard s.total > 0 else { return 0 } return Int((Double(s.done) / Double(s.total) * 100).rounded()) } var body: some View { let s = stats let isMedium = family == .systemMedium VStack(alignment: .leading, spacing: isMedium ? 10 : 6) { HStack(alignment: .firstTextBaseline) { Text("Today's Tasks") .font(.system(size: isMedium ? 22 : 14, weight: .bold)) .foregroundColor(.white) .lineLimit(1) Spacer() Text("\(percent)%") .font(.system(size: isMedium ? 22 : 13, weight: .bold)) .foregroundColor(Color(white: 0.5)) } if s.total == 0 { Spacer(minLength: 0) Text("No tasks due today") .font(.system(size: 12)) .foregroundColor(Color(white: 0.5)) Spacer(minLength: 0) } else { Text("\(s.done) of \(s.total) done") .font(.system(size: 12)) .foregroundColor(teal) Spacer(minLength: 2) DotGrid(total: s.total, filled: s.done, color: teal) } } .padding(.horizontal, isMedium ? 4 : 0) .containerBackground(.black, for: .widget) } } // MARK: - Circular dot grid (event countdown motif) struct DotGrid: View { let total: Int let filled: Int let color: Color var gap: CGFloat = 3 var body: some View { GeometryReader { geo in let l = layout(for: total, in: geo.size) VStack(spacing: gap) { ForEach(0.. some View { if filled { Circle().fill(color).frame(width: size, height: size) } else { Circle() .strokeBorder(color.opacity(0.55), lineWidth: max(0.8, size * 0.13)) .frame(width: size, height: size) } } /// Pick the column count that yields the largest uniform circle that still fits /// every dot inside the available area — an even grid that fills the widget. private func layout(for count: Int, in size: CGSize) -> (cols: Int, rows: Int, size: CGFloat) { guard count > 0, size.width > 1, size.height > 1 else { return (1, 1, 6) } var best = (cols: 1, rows: count, size: CGFloat(0)) for cols in 1...count { let rows = Int(ceil(Double(count) / Double(cols))) let w = (size.width - gap * CGFloat(cols - 1)) / CGFloat(cols) let h = (size.height - gap * CGFloat(rows - 1)) / CGFloat(rows) let s = min(w, h) if s > best.size { best = (cols, rows, s) } } return best } } // MARK: - Event Countdown Widget (small + medium) struct EventCountdownView: View { let entry: EventEntry @Environment(\.widgetFamily) var family private let teal = Color(red: 0.48, green: 0.87, blue: 0.80) private var percent: Int { Int((entry.progress * 100).rounded()) } var body: some View { Group { if family == .systemSmall { small } else { medium } } .containerBackground(.black, for: .widget) } // Wide layout that mirrors the reference: title + percent, then a dot grid. private var medium: some View { VStack(alignment: .leading, spacing: 10) { HStack(alignment: .firstTextBaseline) { Text(LocalizedStringKey(entry.eventName)) .font(.system(size: 22, weight: .bold)) .foregroundColor(.white) .lineLimit(1) Spacer() Text("\(percent)%") .font(.system(size: 22, weight: .bold)) .foregroundColor(Color(white: 0.5)) } DotGrid(total: entry.totalDays, filled: Int(entry.progress * Double(entry.totalDays)), color: teal) } .padding(.horizontal, 4) } private var small: some View { VStack(alignment: .leading, spacing: 6) { Text(LocalizedStringKey(entry.eventName)) .font(.system(size: 14, weight: .bold)) .foregroundColor(.white) .lineLimit(2) HStack(alignment: .firstTextBaseline, spacing: 4) { Text("\(entry.daysLeft)") .font(.system(size: 28, weight: .bold, design: .rounded)) .foregroundColor(teal) Text(entry.daysLeft == 1 ? "day" : "days") .font(.system(size: 12)) .foregroundColor(Color(white: 0.5)) } Spacer(minLength: 2) DotGrid(total: entry.totalDays, filled: Int(entry.progress * Double(entry.totalDays)), color: teal) } } } // MARK: - Lock Screen Widget (accessoryRectangular) struct LockScreenRectView: View { let task: WidgetTask var body: some View { HStack(spacing: 10) { VStack(alignment: .leading, spacing: 2) { Text(task.title) .font(.system(size: 13, weight: .semibold)) .lineLimit(1) Text(task.timeLeftLabel) .font(.system(size: 11)) .foregroundColor(.secondary) } Spacer() if !task.isComplete, let days = task.daysLeft, days <= 365 { ProgressView(value: Double(365 - days), total: 365) .progressViewStyle(.circular) .frame(width: 22, height: 22) .tint(.white) } else if task.isComplete { Image(systemName: "checkmark.circle.fill") .foregroundColor(.green) } } .containerBackground(.thinMaterial, for: .widget) } } // MARK: - Lock Screen Circular struct LockScreenCircularView: View { let task: WidgetTask var body: some View { ZStack { if !task.isComplete, let days = task.daysLeft, days <= 365 { ProgressView(value: Double(365 - days), total: 365) .progressViewStyle(.circular) .tint(.white) } VStack(spacing: 0) { if let days = task.daysLeft { Text("\(days)").font(.system(size: 14, weight: .bold)) Text("days").font(.system(size: 7)) } else { Image(systemName: "checkmark").font(.system(size: 14, weight: .bold)) } } } .containerBackground(.thinMaterial, for: .widget) } }