From fe1ccc69661603d419a642a450e4ac9e1258adb0 Mon Sep 17 00:00:00 2001 From: Christian Cleberg Date: Mon, 13 Apr 2026 19:44:44 -0500 Subject: fix: centralize URLs, tighten models, and polish SwiftUI bindings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add shared HutchDeepLinkURL constants for app, widgets, and tests. - Bump marketing version to 3.1.2 and build to 70 (app + widget extension). - Point Hutch stats default base URL at HutchStatsAPI; add SRHTWebURL status incident feed and reuse it from SystemStatusService. - Group Project into Metadata and Resources; split ContributionStatsResponse into StatsWindow and StatsTotals with updated decoding and tests. - Replace @Bindable usage with explicit Bindings in Profile, Projects list, and repository ACL flows; simplify Home pinned-item helper; add no-op bodies on cancel alert buttons where the compiler requires a statement. - Move repository row build-status indicator next to the relative-updated caption and reserve a fixed 8×8 slot so the row does not jump when status loads. - Use NSString.lastPathComponent for build artifact filenames; collapse duplicate ACL error branches; minor HutchStats HTTP and XMLParserDelegate cleanups. - Point widgets at HutchDeepLinkURL helpers; align tests with the new response and URL types. --- Hutch/Models/ContributionCalendar.swift | 92 ++++++++++++++++++++------------- 1 file changed, 57 insertions(+), 35 deletions(-) (limited to 'Hutch/Models/ContributionCalendar.swift') diff --git a/Hutch/Models/ContributionCalendar.swift b/Hutch/Models/ContributionCalendar.swift index e2942dd..a429fa2 100644 --- a/Hutch/Models/ContributionCalendar.swift +++ b/Hutch/Models/ContributionCalendar.swift @@ -117,45 +117,67 @@ struct ContributionStatsResponse: Decodable, Sendable, Hashable { case currentStreak = "current_streak" } - init( - actor: String, - from: Date, - to: Date, - isIndexed: Bool, - lastPolledAt: Date?, - indexingState: ContributionIndexingState, - totalEvents: Int, - totalScore: Double, - activeDays: Int, - longestStreak: Int, - currentStreak: Int - ) { - self.actor = actor - self.from = from - self.to = to - self.isIndexed = isIndexed - self.lastPolledAt = lastPolledAt - self.indexingState = indexingState - self.totalEvents = totalEvents - self.totalScore = totalScore - self.activeDays = activeDays - self.longestStreak = longestStreak - self.currentStreak = currentStreak + struct StatsWindow: Sendable, Hashable { + let actor: String + let from: Date + let to: Date + let isIndexed: Bool + let lastPolledAt: Date? + let indexingState: ContributionIndexingState + } + + struct StatsTotals: Sendable, Hashable { + let totalEvents: Int + let totalScore: Double + let activeDays: Int + let longestStreak: Int + let currentStreak: Int + } + + init(window: StatsWindow, totals: StatsTotals) { + actor = window.actor + from = window.from + to = window.to + isIndexed = window.isIndexed + lastPolledAt = window.lastPolledAt + indexingState = window.indexingState + totalEvents = totals.totalEvents + totalScore = totals.totalScore + activeDays = totals.activeDays + longestStreak = totals.longestStreak + currentStreak = totals.currentStreak } init(from decoder: any Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) - actor = try container.decode(String.self, forKey: .actor) - from = try ContributionDateParser.decodeDateString(from: container, forKey: .from) - to = try ContributionDateParser.decodeDateString(from: container, forKey: .to) - isIndexed = try container.decodeIfPresent(Bool.self, forKey: .isIndexed) ?? false - lastPolledAt = try ContributionDateParser.decodeOptionalTimestamp(from: container, forKey: .lastPolledAt) - indexingState = try container.decodeIfPresent(ContributionIndexingState.self, forKey: .indexingState) ?? .indexed - totalEvents = try container.decode(Int.self, forKey: .totalEvents) - totalScore = try container.decode(Double.self, forKey: .totalScore) - activeDays = try container.decode(Int.self, forKey: .activeDays) - longestStreak = try container.decode(Int.self, forKey: .longestStreak) - currentStreak = try container.decode(Int.self, forKey: .currentStreak) + let actor = try container.decode(String.self, forKey: .actor) + let from = try ContributionDateParser.decodeDateString(from: container, forKey: .from) + let to = try ContributionDateParser.decodeDateString(from: container, forKey: .to) + let isIndexed = try container.decodeIfPresent(Bool.self, forKey: .isIndexed) ?? false + let lastPolledAt = try ContributionDateParser.decodeOptionalTimestamp(from: container, forKey: .lastPolledAt) + let indexingState = try container.decodeIfPresent(ContributionIndexingState.self, forKey: .indexingState) ?? .indexed + let totalEvents = try container.decode(Int.self, forKey: .totalEvents) + let totalScore = try container.decode(Double.self, forKey: .totalScore) + let activeDays = try container.decode(Int.self, forKey: .activeDays) + let longestStreak = try container.decode(Int.self, forKey: .longestStreak) + let currentStreak = try container.decode(Int.self, forKey: .currentStreak) + self.init( + window: .init( + actor: actor, + from: from, + to: to, + isIndexed: isIndexed, + lastPolledAt: lastPolledAt, + indexingState: indexingState + ), + totals: .init( + totalEvents: totalEvents, + totalScore: totalScore, + activeDays: activeDays, + longestStreak: longestStreak, + currentStreak: currentStreak + ) + ) } } -- cgit v1.2.3