Commit 380ad71149

380ad71149ab78e0457fe96441ce79c01f58d626

parent: 3ded38a6ab

Unsigned

cmc <hello@cleberg.net> · 2026-04-12 00:55 UTC

fix: respect contribution graphs toggle in widget

Mirror the contributionGraphsEnabled setting to shared app group
defaults so the widget extension can read it. Show a descriptive
message when contributions are disabled instead of placeholder data.

Layout: unified · split

Hutch/Views/Settings/SettingsView.swift +3
@@ -49,6 +49,9 @@ struct SettingsView: View {
4949 Section {
5050 Toggle("Swipe actions", isOn: $swipeActionsEnabled)
5151 Toggle("Contribution graphs", isOn: $contributionGraphsEnabled)
52 .onChange(of: contributionGraphsEnabled) { _, newValue in
53 ContributionWidgetContextStore.setEnabled(newValue)
54 }
5255 } header: {
5356 Text("Behavior")
5457 } footer: {
HutchWidgetExtension/ContributionGraphWidget.swift +42 −4
@@ -9,6 +9,7 @@ struct ContributionGraphEntry: TimelineEntry {
99
1010 enum State {
1111 case placeholder
12 case disabled
1213 case unavailable
1314 case empty
1415 case indexing
@@ -41,6 +42,10 @@ struct ContributionGraphTimelineProvider: TimelineProvider {
4142 }
4243
4344 private func loadEntry() async -> ContributionGraphEntry {
45 guard ContributionWidgetContextStore.isEnabled() else {
46 return ContributionGraphEntry(date: .now, actor: nil, state: .disabled, weeks: [])
47 }
48
4449 guard let actor = ContributionWidgetContextStore.loadActor(), !actor.isEmpty else {
4550 return ContributionGraphEntry(date: .now, actor: nil, state: .unavailable, weeks: [])
4651 }
@@ -91,6 +96,29 @@ private struct ContributionGraphWidgetView: View {
9196 @Environment(\.widgetFamily) private var family
9297
9398 var body: some View {
99 Group {
100 switch entry.state {
101 case .disabled:
102 messageView(
103 title: "Contributions Disabled",
104 detail: "Enable contribution graphs in Hutch settings to use this widget."
105 )
106 case .unavailable:
107 messageView(
108 title: "Open Hutch",
109 detail: "Sign in and open your profile to load contribution data."
110 )
111 default:
112 graphView
113 }
114 }
115 .widgetURL(URL(string: "hutch://home"))
116 .containerBackground(for: .widget) {
117 Color(.systemBackground)
118 }
119 }
120
121 private var graphView: some View {
94122 GeometryReader { geometry in
95123 let cellSize = ContributionGraphSizing.baseCellSize(availableHeight: geometry.size.height)
96124 let layout = ContributionGraphSizing.layout(availableSize: geometry.size, cellSize: cellSize)
@@ -108,17 +136,27 @@ private struct ContributionGraphWidgetView: View {
108136 .frame(width: actualSize.width, height: actualSize.height)
109137 .frame(maxWidth: .infinity, maxHeight: .infinity)
110138 }
111 .widgetURL(URL(string: "hutch://home"))
112 .containerBackground(for: .widget) {
113 Color(.systemBackground)
139 }
140
141 private func messageView(title: String, detail: String) -> some View {
142 VStack(spacing: 6) {
143 Text(title)
144 .font(family == .systemSmall ? .headline : .title3.weight(.semibold))
145 .multilineTextAlignment(.center)
146 Text(detail)
147 .font(.caption)
148 .foregroundStyle(.secondary)
149 .multilineTextAlignment(.center)
114150 }
151 .padding()
152 .frame(maxWidth: .infinity, maxHeight: .infinity)
115153 }
116154
117155 private func displayedWeeks(columnCount: Int) -> [ContributionGraphWeek] {
118156 var baseWeeks = switch entry.state {
119157 case .populated, .indexing, .empty:
120158 entry.weeks
121 case .placeholder, .unavailable:
159 case .placeholder, .disabled, .unavailable:
122160 ContributionGraphSampleData.placeholderWeeks
123161 }
124162
Shared/ContributionWidgetContext.swift +10
@@ -9,11 +9,21 @@ enum ContributionGraphWidgetConfiguration {
99
1010enum ContributionWidgetContextStore {
1111 private static let actorKey = "contributionWidget.actor"
12 private static let enabledKey = "contributionWidget.enabled"
1213
1314 static func loadActor(defaults: UserDefaults? = sharedDefaults()) -> String? {
1415 defaults?.string(forKey: actorKey)
1516 }
1617
18 static func isEnabled(defaults: UserDefaults? = sharedDefaults()) -> Bool {
19 defaults?.object(forKey: enabledKey) == nil ? true : defaults?.bool(forKey: enabledKey) ?? true
20 }
21
22 static func setEnabled(_ enabled: Bool, defaults: UserDefaults? = sharedDefaults()) {
23 defaults?.set(enabled, forKey: enabledKey)
24 reloadWidgetTimelines()
25 }
26
1727 static func saveActor(_ actor: String, defaults: UserDefaults? = sharedDefaults()) {
1828 defaults?.set(actor, forKey: actorKey)
1929 reloadWidgetTimelines()