Widgets: bars countdown widget + unified slate/orange theme (KC-15)

- New EventBarsWidget ("Event Countdown (Bars)") using BarGrid + EventBarsView,
  with a fine "X hr, Y min left" label; registered in the bundle.
- Re-themed every home-screen widget to the shared WidgetTheme (dark slate
  background + brand orange): Day Progress, Tasks Done Today, Event Countdown
  (dots), My Tasks. Lock-screen widgets keep .thinMaterial (system-tinted).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
kutesir
2026-06-10 17:31:55 +03:00
parent 063df4ba03
commit 0362d586e5
5 changed files with 91 additions and 9 deletions

View File

@@ -458,3 +458,27 @@ Files: `ExerciseModels.swift`, `WorkoutView.swift`.
Stats are computed from the app's own per-day logs. Health (steps/kcal/resting HR + Stats are computed from the app's own per-day logs. Health (steps/kcal/resting HR +
workout dates) is already read on the Today dashboard and merged into the streak; workout dates) is already read on the Today dashboard and merged into the streak;
a Health overlay on this screen could be added later. a Health overlay on this screen could be added later.
---
## KC-15 — Bars countdown widget + unified slate/orange widget theme
**Status:** In Progress (implemented & build-verified, pending device build)
**Reported by:** User
**Area:** Widgets
### Description
Add a bar-meter event-countdown widget (per reference screenshot) and re-theme
every widget to one look: dark slate background + brand orange, matching the app.
### Fix
- `WidgetTheme` (slate background `RGB(0.224,0.255,0.31)`, brand orange accent).
- `BarGrid` vertical-bar progress view; new `EventBarsView` + `EventBarsWidget`
("Event Countdown (Bars)", medium) reusing the existing event config/provider.
- `EventEntry.fineTimeLeft` (days → "X hr, Y min left") for the bars label.
- Re-themed all home-screen widgets to slate bg + orange: Day Progress, Tasks Done
Today, Event Countdown (dots), My Tasks. Lock-screen accessory widgets stay on
`.thinMaterial` (system-tinted on the Lock Screen).
Files: `WidgetViews.swift`, `EventCountdownWidget.swift`, `MyTasksWidget.swift`,
`KisaniCalWidgets.swift`.

View File

@@ -151,6 +151,18 @@ struct EventEntry: TimelineEntry {
return max(0, d) return max(0, d)
} }
/// Granular countdown for the bars widget: days hr min as it nears.
var fineTimeLeft: String {
if target <= date { return "Today" }
let secs = target.timeIntervalSince(date)
let days = Int(secs / 86400)
let hrs = Int(secs.truncatingRemainder(dividingBy: 86400) / 3600)
let mins = Int(secs.truncatingRemainder(dividingBy: 3600) / 60)
if days >= 1 { return hrs > 0 ? "\(days)d, \(hrs) hr left" : "\(days) day\(days == 1 ? "" : "s") left" }
if hrs >= 1 { return "\(hrs) hr, \(mins) min left" }
return "\(mins) min left"
}
/// Countdown from the current date. Tapping the label cycles the unit: /// Countdown from the current date. Tapping the label cycles the unit:
/// days weeks time remaining. /// days weeks time remaining.
var timeLeftLabel: String { var timeLeftLabel: String {
@@ -254,3 +266,18 @@ struct EventCountdownWidget: Widget {
.supportedFamilies([.systemMedium, .systemSmall]) .supportedFamilies([.systemMedium, .systemSmall])
} }
} }
// MARK: - Event Countdown (Bars)
struct EventBarsWidget: Widget {
let kind = "KisaniEventBars"
var body: some WidgetConfiguration {
AppIntentConfiguration(kind: kind, intent: EventConfigIntent.self, provider: EventProvider()) { entry in
EventBarsView(entry: entry)
}
.configurationDisplayName("Event Countdown (Bars)")
.description("Count down to an event with a bar meter.")
.supportedFamilies([.systemMedium])
}
}

View File

@@ -40,7 +40,7 @@ struct DayProgressWidget: Widget {
let kind = "KisaniDayProgress" let kind = "KisaniDayProgress"
var body: some WidgetConfiguration { var body: some WidgetConfiguration {
StaticConfiguration(kind: kind, provider: KisaniProvider()) { _ in StaticConfiguration(kind: kind, provider: KisaniProvider()) { _ in
ProgressDotView(period: .day, accent: Color(red: 0.48, green: 0.87, blue: 0.80)) ProgressDotView(period: .day, accent: WidgetTheme.accent)
} }
.configurationDisplayName("Day Progress") .configurationDisplayName("Day Progress")
.description("How much of today has elapsed, as a dot grid.") .description("How much of today has elapsed, as a dot grid.")
@@ -106,6 +106,7 @@ struct LockScreenCircularWidget: Widget {
struct KisaniWidgetBundle: WidgetBundle { struct KisaniWidgetBundle: WidgetBundle {
var body: some Widget { var body: some Widget {
EventCountdownWidget() EventCountdownWidget()
EventBarsWidget()
MyTasksWidget() MyTasksWidget()
DayProgressWidget() DayProgressWidget()
TasksCompleteWidget() TasksCompleteWidget()

View File

@@ -96,7 +96,7 @@ struct MyTasksView: View {
HStack(spacing: 6) { HStack(spacing: 6) {
Text("Today") Text("Today")
.font(.system(size: 18, weight: .bold)) .font(.system(size: 18, weight: .bold))
.foregroundColor(Color(red: 0.42, green: 0.5, blue: 0.96)) .foregroundColor(WidgetTheme.accent)
Text("\(entry.openCount)") Text("\(entry.openCount)")
.font(.system(size: 15, weight: .semibold)) .font(.system(size: 15, weight: .semibold))
.foregroundColor(Color(white: 0.5)) .foregroundColor(Color(white: 0.5))
@@ -104,7 +104,7 @@ struct MyTasksView: View {
Button(intent: AddTaskFromWidgetIntent()) { Button(intent: AddTaskFromWidgetIntent()) {
Image(systemName: "plus") Image(systemName: "plus")
.font(.system(size: 16, weight: .semibold)) .font(.system(size: 16, weight: .semibold))
.foregroundColor(Color(red: 0.42, green: 0.5, blue: 0.96)) .foregroundColor(WidgetTheme.accent)
} }
.buttonStyle(.plain) .buttonStyle(.plain)
} }
@@ -143,6 +143,6 @@ struct MyTasksView: View {
Spacer(minLength: 0) Spacer(minLength: 0)
} }
} }
.containerBackground(.black, for: .widget) .containerBackground(WidgetTheme.background, for: .widget)
} }
} }

View File

@@ -98,7 +98,7 @@ struct ProgressDotView: View {
DotGrid(total: 100, filled: Int(progress * 100), color: accent) DotGrid(total: 100, filled: Int(progress * 100), color: accent)
} }
.padding(.horizontal, isMedium ? 4 : 0) .padding(.horizontal, isMedium ? 4 : 0)
.containerBackground(.black, for: .widget) .containerBackground(WidgetTheme.background, for: .widget)
} }
} }
@@ -106,7 +106,7 @@ struct ProgressDotView: View {
struct TasksCompleteView: View { struct TasksCompleteView: View {
@Environment(\.widgetFamily) private var family @Environment(\.widgetFamily) private var family
private let teal = Color(red: 0.48, green: 0.87, blue: 0.80) private let teal = WidgetTheme.accent
private var stats: (done: Int, total: Int) { todayTaskCompletion() } private var stats: (done: Int, total: Int) { todayTaskCompletion() }
private var percent: Int { private var percent: Int {
@@ -144,7 +144,7 @@ struct TasksCompleteView: View {
} }
} }
.padding(.horizontal, isMedium ? 4 : 0) .padding(.horizontal, isMedium ? 4 : 0)
.containerBackground(.black, for: .widget) .containerBackground(WidgetTheme.background, for: .widget)
} }
} }
@@ -211,7 +211,7 @@ struct EventCountdownView: View {
let entry: EventEntry let entry: EventEntry
@Environment(\.widgetFamily) var family @Environment(\.widgetFamily) var family
private let teal = Color(red: 0.48, green: 0.87, blue: 0.80) private let teal = WidgetTheme.accent
private var percent: Int { Int((entry.progress * 100).rounded()) } private var percent: Int { Int((entry.progress * 100).rounded()) }
@@ -223,7 +223,7 @@ struct EventCountdownView: View {
medium medium
} }
} }
.containerBackground(.black, for: .widget) .containerBackground(WidgetTheme.background, for: .widget)
} }
private var filled: Int { Int(entry.progress * Double(entry.totalDays)) } private var filled: Int { Int(entry.progress * Double(entry.totalDays)) }
@@ -274,6 +274,36 @@ struct EventCountdownView: View {
} }
} }
// MARK: - Event Countdown Bars style (medium)
struct EventBarsView: View {
let entry: EventEntry
var body: some View {
VStack(alignment: .leading, spacing: 0) {
HStack(alignment: .firstTextBaseline, spacing: 10) {
Text(LocalizedStringKey(entry.eventName))
.font(.system(size: 22, weight: .heavy))
.foregroundColor(WidgetTheme.accent)
.lineLimit(1)
Button(intent: CycleCountdownUnitIntent()) {
Text(entry.fineTimeLeft)
.font(.system(size: 16, weight: .semibold))
.foregroundColor(WidgetTheme.textDim)
.lineLimit(1)
}
.buttonStyle(.plain)
Spacer(minLength: 0)
}
Spacer(minLength: 8)
BarGrid(progress: entry.progress, color: WidgetTheme.accent)
.frame(maxHeight: .infinity)
}
.padding(.horizontal, 4).padding(.vertical, 2)
.containerBackground(WidgetTheme.background, for: .widget)
}
}
// MARK: - Lock Screen Widget (accessoryRectangular) // MARK: - Lock Screen Widget (accessoryRectangular)
struct LockScreenRectView: View { struct LockScreenRectView: View {