From 6fb15c984af99a7c42c72a4d3932bfb24449e2f1 Mon Sep 17 00:00:00 2001 From: Ostap Korkuna Date: Fri, 27 Feb 2026 23:19:58 -0800 Subject: [PATCH 1/2] Time zone override --- LoopFollow/Controllers/Graphs.swift | 15 ++++++++++++++- LoopFollow/Helpers/Chart.swift | 5 +++++ LoopFollow/Settings/GraphSettingsView.swift | 14 ++++++++++++++ LoopFollow/Storage/Storage.swift | 2 ++ 4 files changed, 35 insertions(+), 1 deletion(-) diff --git a/LoopFollow/Controllers/Graphs.swift b/LoopFollow/Controllers/Graphs.swift index 33d654093..d67a9ae71 100644 --- a/LoopFollow/Controllers/Graphs.swift +++ b/LoopFollow/Controllers/Graphs.swift @@ -721,7 +721,15 @@ extension MainViewController { func createMidnightLines() { // Draw a line at midnight: useful when showing multiple days of data if Storage.shared.showMidnightLines.value { - var midnightTimeInterval = dateTimeUtils.getTimeIntervalMidnightToday() + var midnightTimeInterval: TimeInterval + if Storage.shared.graphTimeZoneEnabled.value, + let tz = TimeZone(identifier: Storage.shared.graphTimeZoneIdentifier.value) { + var cal = Calendar.current + cal.timeZone = tz + midnightTimeInterval = cal.startOfDay(for: Date()).timeIntervalSince1970 + } else { + midnightTimeInterval = dateTimeUtils.getTimeIntervalMidnightToday() + } let graphHours = 24 * Storage.shared.downloadDays.value let graphStart = dateTimeUtils.getTimeIntervalNHoursAgo(N: graphHours) while midnightTimeInterval > graphStart { @@ -1881,6 +1889,11 @@ extension MainViewController { dateFormatter.setLocalizedDateFormatFromTemplate("hh:mm") } + if Storage.shared.graphTimeZoneEnabled.value, + let tz = TimeZone(identifier: Storage.shared.graphTimeZoneIdentifier.value) { + dateFormatter.timeZone = tz + } + let wrappedLine1 = wrapText(line1, maxLineLength: 40) let date = Date(timeIntervalSince1970: time) diff --git a/LoopFollow/Helpers/Chart.swift b/LoopFollow/Helpers/Chart.swift index 3ccf637a5..46c350930 100644 --- a/LoopFollow/Helpers/Chart.swift +++ b/LoopFollow/Helpers/Chart.swift @@ -28,6 +28,11 @@ final class ChartXValueFormatter: AxisValueFormatter { dateFormatter.setLocalizedDateFormatFromTemplate("hh:mm") } + if Storage.shared.graphTimeZoneEnabled.value, + let tz = TimeZone(identifier: Storage.shared.graphTimeZoneIdentifier.value) { + dateFormatter.timeZone = tz + } + // let date = Date(timeIntervalSince1970: epochTimezoneOffset) let date = Date(timeIntervalSince1970: value) let formattedDate = dateFormatter.string(from: date) diff --git a/LoopFollow/Settings/GraphSettingsView.swift b/LoopFollow/Settings/GraphSettingsView.swift index 4ebc1896a..f46e4e556 100644 --- a/LoopFollow/Settings/GraphSettingsView.swift +++ b/LoopFollow/Settings/GraphSettingsView.swift @@ -13,6 +13,8 @@ struct GraphSettingsView: View { @ObservedObject private var show90MinLine = Storage.shared.show90MinLine @ObservedObject private var showMidnightLines = Storage.shared.showMidnightLines @ObservedObject private var smallGraphTreatments = Storage.shared.smallGraphTreatments + @ObservedObject private var graphTimeZoneEnabled = Storage.shared.graphTimeZoneEnabled + @ObservedObject private var graphTimeZoneIdentifier = Storage.shared.graphTimeZoneIdentifier @ObservedObject private var smallGraphHeight = Storage.shared.smallGraphHeight @ObservedObject private var predictionToLoad = Storage.shared.predictionToLoad @@ -48,6 +50,18 @@ struct GraphSettingsView: View { Toggle("Show Midnight Lines", isOn: $showMidnightLines.value) .onChange(of: showMidnightLines.value) { _ in markDirty() } + + Toggle("Time Zone Override", isOn: $graphTimeZoneEnabled.value) + .onChange(of: graphTimeZoneEnabled.value) { _ in markDirty() } + + if graphTimeZoneEnabled.value { + Picker("Time Zone", selection: $graphTimeZoneIdentifier.value) { + ForEach(TimeZone.knownTimeZoneIdentifiers.sorted(), id: \.self) { tz in + Text(tz).tag(tz) + } + } + .onChange(of: graphTimeZoneIdentifier.value) { _ in markDirty() } + } } // ── Treatments ─────────────────────────────────────────────── diff --git a/LoopFollow/Storage/Storage.swift b/LoopFollow/Storage/Storage.swift index 06bec90fb..e5e2a7ffc 100644 --- a/LoopFollow/Storage/Storage.swift +++ b/LoopFollow/Storage/Storage.swift @@ -98,6 +98,8 @@ class Storage { var lowLine = StorageValue(key: "lowLine", defaultValue: 70.0) var highLine = StorageValue(key: "highLine", defaultValue: 180.0) var downloadDays = StorageValue(key: "downloadDays", defaultValue: 1) + var graphTimeZoneEnabled = StorageValue(key: "graphTimeZoneEnabled", defaultValue: false) + var graphTimeZoneIdentifier = StorageValue(key: "graphTimeZoneIdentifier", defaultValue: TimeZone.current.identifier) // Graph Settings [END] // Calendar entries [BEGIN] From 639e18df7118f5cca78fa9b6473b105b0d7f030b Mon Sep 17 00:00:00 2001 From: Ostap Korkuna Date: Fri, 27 Feb 2026 23:33:39 -0800 Subject: [PATCH 2/2] Better tz sorting --- LoopFollow/Settings/GraphSettingsView.swift | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/LoopFollow/Settings/GraphSettingsView.swift b/LoopFollow/Settings/GraphSettingsView.swift index f46e4e556..2e5b82ac9 100644 --- a/LoopFollow/Settings/GraphSettingsView.swift +++ b/LoopFollow/Settings/GraphSettingsView.swift @@ -56,8 +56,8 @@ struct GraphSettingsView: View { if graphTimeZoneEnabled.value { Picker("Time Zone", selection: $graphTimeZoneIdentifier.value) { - ForEach(TimeZone.knownTimeZoneIdentifiers.sorted(), id: \.self) { tz in - Text(tz).tag(tz) + ForEach(Self.sortedTimeZones, id: \.identifier) { tz in + Text(Self.timeZoneLabel(tz)).tag(tz.identifier) } } .onChange(of: graphTimeZoneIdentifier.value) { _ in markDirty() } @@ -154,4 +154,19 @@ struct GraphSettingsView: View { private func markDirty() { Observable.shared.chartSettingsChanged.value = true } + + // MARK: - Time Zone Helpers + + private static let sortedTimeZones: [TimeZone] = { + TimeZone.knownTimeZoneIdentifiers + .compactMap { TimeZone(identifier: $0) } + .sorted { $0.secondsFromGMT() < $1.secondsFromGMT() } + }() + + private static func timeZoneLabel(_ tz: TimeZone) -> String { + let offsetMinutes = tz.secondsFromGMT() / 60 + let sign = offsetMinutes >= 0 ? "+" : "-" + let offsetString = String(format: "UTC%@%02d:%02d", sign, abs(offsetMinutes) / 60, abs(offsetMinutes) % 60) + return "(\(offsetString)) \(tz.identifier)" + } }