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 {
49 Section { 49 Section {
50 Toggle("Swipe actions", isOn: $swipeActionsEnabled) 50 Toggle("Swipe actions", isOn: $swipeActionsEnabled)
51 Toggle("Contribution graphs", isOn: $contributionGraphsEnabled) 51 Toggle("Contribution graphs", isOn: $contributionGraphsEnabled)
52 .onChange(of: contributionGraphsEnabled) { _, newValue in
53 ContributionWidgetContextStore.setEnabled(newValue)
54 }
52 } header: { 55 } header: {
53 Text("Behavior") 56 Text("Behavior")
54 } footer: { 57 } footer: {
HutchWidgetExtension/ContributionGraphWidget.swift +42 −4
@@ -9,6 +9,7 @@ struct ContributionGraphEntry: TimelineEntry {
9 9
10 enum State { 10 enum State {
11 case placeholder 11 case placeholder
12 case disabled
12 case unavailable 13 case unavailable
13 case empty 14 case empty
14 case indexing 15 case indexing
@@ -41,6 +42,10 @@ struct ContributionGraphTimelineProvider: TimelineProvider {
41 } 42 }
42 43
43 private func loadEntry() async -> ContributionGraphEntry { 44 private func loadEntry() async -> ContributionGraphEntry {
45 guard ContributionWidgetContextStore.isEnabled() else {
46 return ContributionGraphEntry(date: .now, actor: nil, state: .disabled, weeks: [])
47 }
48
44 guard let actor = ContributionWidgetContextStore.loadActor(), !actor.isEmpty else { 49 guard let actor = ContributionWidgetContextStore.loadActor(), !actor.isEmpty else {
45 return ContributionGraphEntry(date: .now, actor: nil, state: .unavailable, weeks: []) 50 return ContributionGraphEntry(date: .now, actor: nil, state: .unavailable, weeks: [])
46 } 51 }
@@ -91,6 +96,29 @@ private struct ContributionGraphWidgetView: View {
91 @Environment(\.widgetFamily) private var family 96 @Environment(\.widgetFamily) private var family
92 97
93 var body: some View { 98 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 {
94 GeometryReader { geometry in 122 GeometryReader { geometry in
95 let cellSize = ContributionGraphSizing.baseCellSize(availableHeight: geometry.size.height) 123 let cellSize = ContributionGraphSizing.baseCellSize(availableHeight: geometry.size.height)
96 let layout = ContributionGraphSizing.layout(availableSize: geometry.size, cellSize: cellSize) 124 let layout = ContributionGraphSizing.layout(availableSize: geometry.size, cellSize: cellSize)
@@ -108,17 +136,27 @@ private struct ContributionGraphWidgetView: View {
108 .frame(width: actualSize.width, height: actualSize.height) 136 .frame(width: actualSize.width, height: actualSize.height)
109 .frame(maxWidth: .infinity, maxHeight: .infinity) 137 .frame(maxWidth: .infinity, maxHeight: .infinity)
110 } 138 }
111 .widgetURL(URL(string: "hutch://home")) 139 }
112 .containerBackground(for: .widget) { 140
113 Color(.systemBackground) 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)
114 } 150 }
151 .padding()
152 .frame(maxWidth: .infinity, maxHeight: .infinity)
115 } 153 }
116 154
117 private func displayedWeeks(columnCount: Int) -> [ContributionGraphWeek] { 155 private func displayedWeeks(columnCount: Int) -> [ContributionGraphWeek] {
118 var baseWeeks = switch entry.state { 156 var baseWeeks = switch entry.state {
119 case .populated, .indexing, .empty: 157 case .populated, .indexing, .empty:
120 entry.weeks 158 entry.weeks
121 case .placeholder, .unavailable: 159 case .placeholder, .disabled, .unavailable:
122 ContributionGraphSampleData.placeholderWeeks 160 ContributionGraphSampleData.placeholderWeeks
123 } 161 }
124 162
Shared/ContributionWidgetContext.swift +10
@@ -9,11 +9,21 @@ enum ContributionGraphWidgetConfiguration {
9 9
10enum ContributionWidgetContextStore { 10enum ContributionWidgetContextStore {
11 private static let actorKey = "contributionWidget.actor" 11 private static let actorKey = "contributionWidget.actor"
12 private static let enabledKey = "contributionWidget.enabled"
12 13
13 static func loadActor(defaults: UserDefaults? = sharedDefaults()) -> String? { 14 static func loadActor(defaults: UserDefaults? = sharedDefaults()) -> String? {
14 defaults?.string(forKey: actorKey) 15 defaults?.string(forKey: actorKey)
15 } 16 }
16 17
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
17 static func saveActor(_ actor: String, defaults: UserDefaults? = sharedDefaults()) { 27 static func saveActor(_ actor: String, defaults: UserDefaults? = sharedDefaults()) {
18 defaults?.set(actor, forKey: actorKey) 28 defaults?.set(actor, forKey: actorKey)
19 reloadWidgetTimelines() 29 reloadWidgetTimelines()