From 5ca40b9580229b7a33f953cf84f201c3812a87a6 Mon Sep 17 00:00:00 2001 From: Krystof Date: Tue, 3 Dec 2024 16:58:49 +0100 Subject: [PATCH] feat(widgets): prototype --- .../metro-now.xcodeproj/project.pbxproj | 10 + .../metro-now/metro-now/metro_nowApp.swift | 4 + .../settings/subpages/settings-about.swift | 3 +- apps/mobile/metro-now/widgets/Info.plist | 4 + .../widgets/departures/DeparturesWidget.swift | 40 +++ .../departures/DeparturesWidgetManager.swift | 91 +++++++ .../DeparturesWidgetTimelineEntry.swift | 12 + .../DeparturesWidgetTimelineProvider.swift | 53 ++++ .../departures/DeparturesWidgetView.swift | 49 ++++ .../widgets/frequency/FrequencyWidget.swift | 35 +++ .../frequency/FrequencyWidgetManager.swift | 91 +++++++ .../FrequencyWidgetTimelineEntry.swift | 10 + .../FrequencyWidgetTimelineProvider.swift | 59 +++++ .../frequency/FrequencyWidgetView.swift | 43 ++++ apps/mobile/metro-now/widgets/widgets.swift | 232 ------------------ .../metro-now/widgets/widgetsBundle.swift | 10 +- 16 files changed, 505 insertions(+), 241 deletions(-) create mode 100644 apps/mobile/metro-now/widgets/departures/DeparturesWidget.swift create mode 100644 apps/mobile/metro-now/widgets/departures/DeparturesWidgetManager.swift create mode 100644 apps/mobile/metro-now/widgets/departures/DeparturesWidgetTimelineEntry.swift create mode 100644 apps/mobile/metro-now/widgets/departures/DeparturesWidgetTimelineProvider.swift create mode 100644 apps/mobile/metro-now/widgets/departures/DeparturesWidgetView.swift create mode 100644 apps/mobile/metro-now/widgets/frequency/FrequencyWidget.swift create mode 100644 apps/mobile/metro-now/widgets/frequency/FrequencyWidgetManager.swift create mode 100644 apps/mobile/metro-now/widgets/frequency/FrequencyWidgetTimelineEntry.swift create mode 100644 apps/mobile/metro-now/widgets/frequency/FrequencyWidgetTimelineProvider.swift create mode 100644 apps/mobile/metro-now/widgets/frequency/FrequencyWidgetView.swift delete mode 100644 apps/mobile/metro-now/widgets/widgets.swift diff --git a/apps/mobile/metro-now/metro-now.xcodeproj/project.pbxproj b/apps/mobile/metro-now/metro-now.xcodeproj/project.pbxproj index 74739536..08f8b44c 100644 --- a/apps/mobile/metro-now/metro-now.xcodeproj/project.pbxproj +++ b/apps/mobile/metro-now/metro-now.xcodeproj/project.pbxproj @@ -13,6 +13,7 @@ 2DD9D1792CF3B8A70037CB95 /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2DD9D1782CF3B8A70037CB95 /* WidgetKit.framework */; }; 2DD9D17B2CF3B8A70037CB95 /* SwiftUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2DD9D17A2CF3B8A70037CB95 /* SwiftUI.framework */; }; 2DD9D1862CF3B8A90037CB95 /* widgetsExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 2DD9D1762CF3B8A70037CB95 /* widgetsExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; + 2DEE771C2CFF5CD000F24AAD /* Alamofire in Frameworks */ = {isa = PBXBuildFile; productRef = 2DEE771B2CFF5CD000F24AAD /* Alamofire */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -128,6 +129,8 @@ "components/route-label-view/get-color-by-route-name.utils.swift", "components/route-label-view/route-name.view.swift", "components/route-label-view/route-type.enum.swift", + const/api.const.swift, + "types/api-types.swift", utils/color.utils.swift, ); target = 2DD9D1752CF3B8A70037CB95 /* widgetsExtension */; @@ -198,6 +201,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 2DEE771C2CFF5CD000F24AAD /* Alamofire in Frameworks */, 2DD9D17B2CF3B8A70037CB95 /* SwiftUI.framework in Frameworks */, 2DD9D1792CF3B8A70037CB95 /* WidgetKit.framework in Frameworks */, ); @@ -333,6 +337,7 @@ ); name = widgetsExtension; packageProductDependencies = ( + 2DEE771B2CFF5CD000F24AAD /* Alamofire */, ); productName = widgetsExtension; productReference = 2DD9D1762CF3B8A70037CB95 /* widgetsExtension.appex */; @@ -893,6 +898,11 @@ package = 2D87C85D2CE8BACA00209DE6 /* XCRemoteSwiftPackageReference "Alamofire" */; productName = Alamofire; }; + 2DEE771B2CFF5CD000F24AAD /* Alamofire */ = { + isa = XCSwiftPackageProductDependency; + package = 2D87C85D2CE8BACA00209DE6 /* XCRemoteSwiftPackageReference "Alamofire" */; + productName = Alamofire; + }; /* End XCSwiftPackageProductDependency section */ }; rootObject = 2D001BA02CC8099B00C6B4F8 /* Project object */; diff --git a/apps/mobile/metro-now/metro-now/metro_nowApp.swift b/apps/mobile/metro-now/metro-now/metro_nowApp.swift index 26076c8d..c819f0b7 100644 --- a/apps/mobile/metro-now/metro-now/metro_nowApp.swift +++ b/apps/mobile/metro-now/metro-now/metro_nowApp.swift @@ -2,12 +2,16 @@ // https://github.com/krystxf/metro-now import SwiftUI +import WidgetKit @main struct metro_nowApp: App { var body: some Scene { WindowGroup { ContentView() + .task { + WidgetCenter.shared.reloadAllTimelines() + } } } } diff --git a/apps/mobile/metro-now/metro-now/pages/settings/subpages/settings-about.swift b/apps/mobile/metro-now/metro-now/pages/settings/subpages/settings-about.swift index 106a389f..0189a38b 100644 --- a/apps/mobile/metro-now/metro-now/pages/settings/subpages/settings-about.swift +++ b/apps/mobile/metro-now/metro-now/pages/settings/subpages/settings-about.swift @@ -26,10 +26,9 @@ struct SettingsAboutPageView: View { .frame(maxWidth: .infinity, alignment: .center) Text(""" The app is still in development. Stay tuned to see what's next! - """) + """) .multilineTextAlignment(.center) } - } if let appStoreUrl { diff --git a/apps/mobile/metro-now/widgets/Info.plist b/apps/mobile/metro-now/widgets/Info.plist index 0f118fb7..d987ab47 100644 --- a/apps/mobile/metro-now/widgets/Info.plist +++ b/apps/mobile/metro-now/widgets/Info.plist @@ -2,6 +2,10 @@ + NSWidgetWantsLocation + + NSLocationUsageDescription + Show the closest metro stop NSExtension NSExtensionPointIdentifier diff --git a/apps/mobile/metro-now/widgets/departures/DeparturesWidget.swift b/apps/mobile/metro-now/widgets/departures/DeparturesWidget.swift new file mode 100644 index 00000000..4fefa922 --- /dev/null +++ b/apps/mobile/metro-now/widgets/departures/DeparturesWidget.swift @@ -0,0 +1,40 @@ +// metro-now +// https://github.com/krystxf/metro-now + +import CoreLocation +import SwiftUI +import WidgetKit + +struct DeparturesWidget: Widget { + let kind: String = "Widgets" + + var body: some WidgetConfiguration { + StaticConfiguration(kind: kind, provider: DeparturesWidgetTimelineProvider()) { entry in + if #available(iOS 17.0, *) { + DeparturesWidgetView(entry: entry) + .containerBackground(.fill.tertiary, for: .widget) + } else { + DeparturesWidgetView(entry: entry) + .padding() + .background() + } + } + .configurationDisplayName("Metro Departures") + .description("Show the closest metro stop dynamically.") + .supportedFamilies([.systemLarge]) + } +} + +#Preview("large", as: .systemLarge) { + DeparturesWidget() +} timeline: { + DeparturesWidgetTimelineEntry( + date: .now, + closestStop: "Muzeum", + departures: [ + "Route A to Station 1 at 12:45 PM", + "Route B to Station 2 at 12:50 PM", + ], + location: CLLocation(latitude: 50.08, longitude: 14.43) + ) +} diff --git a/apps/mobile/metro-now/widgets/departures/DeparturesWidgetManager.swift b/apps/mobile/metro-now/widgets/departures/DeparturesWidgetManager.swift new file mode 100644 index 00000000..7c4ebd16 --- /dev/null +++ b/apps/mobile/metro-now/widgets/departures/DeparturesWidgetManager.swift @@ -0,0 +1,91 @@ +// metro-now +// https://github.com/krystxf/metro-now + +import Alamofire +import CoreLocation + +class DeparturesWidgetManager: NSObject, ObservableObject, CLLocationManagerDelegate { + private let locationManager = CLLocationManager() + @Published var location: CLLocation? + @Published var metroStops: [ApiStop]? + @Published var closestMetroStop: ApiStop? + @Published var nearestDepartures: [ApiDeparture] = [] + + override init() { + super.init() + locationManager.delegate = self + locationManager.requestWhenInUseAuthorization() + locationManager.startUpdatingLocation() + fetchMetroStops() + } + + func locationManager(_: CLLocationManager, didUpdateLocations locations: [CLLocation]) { + guard let location = locations.last else { return } + self.location = location + updateClosestMetroStop() + fetchDeparturesForClosestStop() + } + + private func fetchMetroStops() { + let request = AF.request( + "\(API_URL)/v1/stop/all", + method: .get, + parameters: ["metroOnly": "true"] + ) + + request.validate().responseDecodable(of: [ApiStop].self) { response in + switch response.result { + case let .success(stops): + DispatchQueue.main.async { + self.metroStops = stops + self.updateClosestMetroStop() + } + case let .failure(error): + print("Failed to fetch metro stops: \(error)") + } + } + } + + private func updateClosestMetroStop() { + guard let location, let metroStops else { return } + closestMetroStop = metroStops.min(by: { + let distance1 = location.distance(from: CLLocation(latitude: $0.avgLatitude, longitude: $0.avgLongitude)) + let distance2 = location.distance(from: CLLocation(latitude: $1.avgLatitude, longitude: $1.avgLongitude)) + return distance1 < distance2 + }) + } + + private func fetchDeparturesForClosestStop() { + guard let closestStop = closestMetroStop else { return } + + // Collecting stop and platform ids for the request + let stopIds = closestStop.id + let platformIds = closestStop.platforms.map(\.id) + + // Construct the API request URL with array-like syntax for `stop[]` and `platform[]` + let stopQuery = stopIds.map { "stop[]=\($0)" }.joined(separator: "&") + let platformQuery = platformIds.map { "platform[]=\($0)" }.joined(separator: "&") + + let url = "\(API_URL)/v2/departure?\(stopQuery)&\(platformQuery)&limit=4&minutesBefore=1&minutesAfter=\(3 * 60)" + + // Print the full API request URL for debugging + print("API Request URL for Departures: \(url)") + + // API request to fetch departures + let request = AF.request(url, method: .get) + + let decoder = JSONDecoder() + decoder.dateDecodingStrategy = .iso8601 + + request.validate().responseDecodable(of: [ApiDeparture].self, decoder: decoder) { response in + switch response.result { + case let .success(departures): + DispatchQueue.main.async { + self.nearestDepartures = departures + } + case let .failure(error): + print("Failed to fetch departures: \(error)") + } + } + } +} diff --git a/apps/mobile/metro-now/widgets/departures/DeparturesWidgetTimelineEntry.swift b/apps/mobile/metro-now/widgets/departures/DeparturesWidgetTimelineEntry.swift new file mode 100644 index 00000000..19d857a0 --- /dev/null +++ b/apps/mobile/metro-now/widgets/departures/DeparturesWidgetTimelineEntry.swift @@ -0,0 +1,12 @@ +// metro-now +// https://github.com/krystxf/metro-now + +import CoreLocation +import WidgetKit + +struct DeparturesWidgetTimelineEntry: TimelineEntry { + let date: Date + let closestStop: String + let departures: [String] + let location: CLLocation? +} diff --git a/apps/mobile/metro-now/widgets/departures/DeparturesWidgetTimelineProvider.swift b/apps/mobile/metro-now/widgets/departures/DeparturesWidgetTimelineProvider.swift new file mode 100644 index 00000000..c5b6ad7c --- /dev/null +++ b/apps/mobile/metro-now/widgets/departures/DeparturesWidgetTimelineProvider.swift @@ -0,0 +1,53 @@ +// metro-now +// https://github.com/krystxf/metro-now + +import WidgetKit + +struct DeparturesWidgetTimelineProvider: TimelineProvider { + private let stopManager = DeparturesWidgetManager() + + func placeholder(in _: Context) -> DeparturesWidgetTimelineEntry { + DeparturesWidgetTimelineEntry(date: Date(), closestStop: "Loading...", departures: [], location: nil) + } + + func getSnapshot(in _: Context, completion: @escaping (DeparturesWidgetTimelineEntry) -> Void) { + let entry = DeparturesWidgetTimelineEntry(date: Date(), closestStop: "Loading...", departures: [], location: nil) + completion(entry) + } + + func getTimeline(in _: Context, completion: @escaping (Timeline) -> Void) { + // Wait for data to be fetched + DispatchQueue.main.asyncAfter(deadline: .now() + 1) { [weak stopManager] in + guard let stopManager, + let closestStop = stopManager.closestMetroStop, + let location = stopManager.location + else { + let entry = DeparturesWidgetTimelineEntry(date: Date(), closestStop: "Unknown", departures: [], location: nil) + let timeline = Timeline(entries: [entry], policy: .atEnd) + completion(timeline) + return + } + + let departures = stopManager.nearestDepartures.map { departure in + "\(departure.route) to \(departure.headsign) at \(formattedDepartureTime(departure.departure.predicted))" + } + + // Create a timeline entry + let entry = DeparturesWidgetTimelineEntry( + date: Date(), + closestStop: closestStop.name, + departures: departures, + location: location + ) + + let timeline = Timeline(entries: [entry], policy: .atEnd) + completion(timeline) + } + } + + private func formattedDepartureTime(_ departureDate: Date) -> String { + let formatter = DateFormatter() + formatter.timeStyle = .short + return formatter.string(from: departureDate) + } +} diff --git a/apps/mobile/metro-now/widgets/departures/DeparturesWidgetView.swift b/apps/mobile/metro-now/widgets/departures/DeparturesWidgetView.swift new file mode 100644 index 00000000..5602e725 --- /dev/null +++ b/apps/mobile/metro-now/widgets/departures/DeparturesWidgetView.swift @@ -0,0 +1,49 @@ +// metro-now +// https://github.com/krystxf/metro-now + +import SwiftUI + +struct DeparturesWidgetView: View { + var entry: DeparturesWidgetTimelineProvider.Entry + @Environment(\.widgetFamily) var widgetFamily + + var body: some View { + VStack(alignment: .leading) { + Text("Closest Metro Stop:") + .font(.headline) + Text(entry.closestStop) + .font(.title2) + .fontWeight(.bold) + + if let location = entry.location { + Text("Current Location:") + .font(.subheadline) + .foregroundColor(.secondary) + Text("Lat: \(location.coordinate.latitude, specifier: "%.4f"), Lon: \(location.coordinate.longitude, specifier: "%.4f")") + .font(.footnote) + } else { + Text("Location not available") + .font(.subheadline) + .foregroundColor(.red) + } + + Divider() + + Text("Next Departures:") + .font(.subheadline) + .foregroundColor(.secondary) + + ForEach(entry.departures, id: \.self) { departure in + Text(departure) + .font(.footnote) + } + + Spacer() + + Text("Last refreshed: \(entry.date, style: .time)") + .foregroundStyle(.tertiary) + .font(.footnote) + } + .padding() + } +} diff --git a/apps/mobile/metro-now/widgets/frequency/FrequencyWidget.swift b/apps/mobile/metro-now/widgets/frequency/FrequencyWidget.swift new file mode 100644 index 00000000..8d6a0999 --- /dev/null +++ b/apps/mobile/metro-now/widgets/frequency/FrequencyWidget.swift @@ -0,0 +1,35 @@ +// metro-now +// https://github.com/krystxf/metro-now + +import SwiftUI +import WidgetKit + +struct FrequencyWidget: Widget { + let kind: String = "Widgets" + + var body: some WidgetConfiguration { + StaticConfiguration(kind: kind, provider: FrequencyWidgetTimelineProvider()) { entry in + if #available(iOS 17.0, *) { + FrequencyWidgetView(entry: entry) + .containerBackground(.background, for: .widget) + } else { + FrequencyWidgetView(entry: entry) + .padding() + .background() + } + } + .configurationDisplayName("Metro Frequency") + .description("Show the frequency of metro departures.") + .supportedFamilies([.systemSmall]) + } +} + +#Preview("One metro line", as: .systemSmall) { + FrequencyWidget() +} timeline: { + FrequencyWidgetTimelineEntry( + date: .now, + stopName: "Muzeum", + frequency: 2 * 60 + 50 + ) +} diff --git a/apps/mobile/metro-now/widgets/frequency/FrequencyWidgetManager.swift b/apps/mobile/metro-now/widgets/frequency/FrequencyWidgetManager.swift new file mode 100644 index 00000000..b33f7de0 --- /dev/null +++ b/apps/mobile/metro-now/widgets/frequency/FrequencyWidgetManager.swift @@ -0,0 +1,91 @@ +// metro-now +// https://github.com/krystxf/metro-now + +import Alamofire +import CoreLocation + +class FrequencyWidgetManager: NSObject, ObservableObject, CLLocationManagerDelegate { + private let locationManager = CLLocationManager() + @Published var location: CLLocation? + @Published var metroStops: [ApiStop]? + @Published var closestMetroStop: ApiStop? + @Published var nearestDepartures: [ApiDeparture] = [] + + override init() { + super.init() + locationManager.delegate = self + locationManager.requestWhenInUseAuthorization() + locationManager.startUpdatingLocation() + fetchMetroStops() + } + + func locationManager(_: CLLocationManager, didUpdateLocations locations: [CLLocation]) { + guard let location = locations.last else { return } + self.location = location + updateClosestMetroStop() + fetchDeparturesForClosestStop() + } + + private func fetchMetroStops() { + let request = AF.request( + "\(API_URL)/v1/stop/all", + method: .get, + parameters: ["metroOnly": "true"] + ) + + request.validate().responseDecodable(of: [ApiStop].self) { response in + switch response.result { + case let .success(stops): + DispatchQueue.main.async { + self.metroStops = stops + self.updateClosestMetroStop() + } + case let .failure(error): + print("Failed to fetch metro stops: \(error)") + } + } + } + + private func updateClosestMetroStop() { + guard let location, let metroStops else { return } + closestMetroStop = metroStops.min(by: { + let distance1 = location.distance(from: CLLocation(latitude: $0.avgLatitude, longitude: $0.avgLongitude)) + let distance2 = location.distance(from: CLLocation(latitude: $1.avgLatitude, longitude: $1.avgLongitude)) + return distance1 < distance2 + }) + } + + private func fetchDeparturesForClosestStop() { + guard let closestStop = closestMetroStop else { return } + + // Collecting stop and platform ids for the request + let stopIds = closestStop.id + let platformIds = closestStop.platforms.map(\.id) + + // Construct the API request URL with array-like syntax for `stop[]` and `platform[]` + let stopQuery = stopIds.map { "stop[]=\($0)" }.joined(separator: "&") + let platformQuery = platformIds.map { "platform[]=\($0)" }.joined(separator: "&") + + let url = "\(API_URL)/v2/departure?\(stopQuery)&\(platformQuery)&limit=4&minutesBefore=1&minutesAfter=\(3 * 60)" + + // Print the full API request URL for debugging + print("API Request URL for Departures: \(url)") + + // API request to fetch departures + let request = AF.request(url, method: .get) + + let decoder = JSONDecoder() + decoder.dateDecodingStrategy = .iso8601 + + request.validate().responseDecodable(of: [ApiDeparture].self, decoder: decoder) { response in + switch response.result { + case let .success(departures): + DispatchQueue.main.async { + self.nearestDepartures = departures + } + case let .failure(error): + print("Failed to fetch departures: \(error)") + } + } + } +} diff --git a/apps/mobile/metro-now/widgets/frequency/FrequencyWidgetTimelineEntry.swift b/apps/mobile/metro-now/widgets/frequency/FrequencyWidgetTimelineEntry.swift new file mode 100644 index 00000000..738b2ab6 --- /dev/null +++ b/apps/mobile/metro-now/widgets/frequency/FrequencyWidgetTimelineEntry.swift @@ -0,0 +1,10 @@ +// metro-now +// https://github.com/krystxf/metro-now + +import WidgetKit + +struct FrequencyWidgetTimelineEntry: TimelineEntry { + let date: Date + let stopName: String + let frequency: TimeInterval // in seconds +} diff --git a/apps/mobile/metro-now/widgets/frequency/FrequencyWidgetTimelineProvider.swift b/apps/mobile/metro-now/widgets/frequency/FrequencyWidgetTimelineProvider.swift new file mode 100644 index 00000000..3388b1af --- /dev/null +++ b/apps/mobile/metro-now/widgets/frequency/FrequencyWidgetTimelineProvider.swift @@ -0,0 +1,59 @@ +// metro-now +// https://github.com/krystxf/metro-now + +import WidgetKit + +struct FrequencyWidgetTimelineProvider: TimelineProvider { + private let stopManager = FrequencyWidgetManager() + + func placeholder(in _: Context) -> FrequencyWidgetTimelineEntry { + FrequencyWidgetTimelineEntry(date: Date(), stopName: "Muzeum", frequency: 2 * 60) + } + + func getSnapshot(in _: Context, completion: @escaping (FrequencyWidgetTimelineEntry) -> Void) { + let entry = FrequencyWidgetTimelineEntry( + date: Date(), + stopName: "Muzeum", + frequency: 2 * 60 + ) + + completion(entry) + } + + func getTimeline( + in _: Context, + completion: @escaping (Timeline) -> Void + ) { + // Wait for data to be fetched + DispatchQueue.main.asyncAfter(deadline: .now() + 1) { [weak stopManager] in + guard let stopManager, + let closestStop = stopManager.closestMetroStop, + let location = stopManager.location + else { + let entry = FrequencyWidgetTimelineEntry( + date: Date(), + stopName: "Muzeum", + frequency: 2 * 60 + ) + + let timeline = Timeline(entries: [entry], policy: .atEnd) + completion(timeline) + return + } + +// let departures = stopManager.nearestDepartures.map { departure in +// "\(departure.route) to \(departure.headsign) at \(formattedDepartureTime(departure.departure.predicted))" +// } + + // Create a timeline entry + let entry = FrequencyWidgetTimelineEntry( + date: Date(), + stopName: closestStop.name, + frequency: 2 * 60 + ) + + let timeline = Timeline(entries: [entry], policy: .atEnd) + completion(timeline) + } + } +} diff --git a/apps/mobile/metro-now/widgets/frequency/FrequencyWidgetView.swift b/apps/mobile/metro-now/widgets/frequency/FrequencyWidgetView.swift new file mode 100644 index 00000000..1a055c29 --- /dev/null +++ b/apps/mobile/metro-now/widgets/frequency/FrequencyWidgetView.swift @@ -0,0 +1,43 @@ +// metro-now +// https://github.com/krystxf/metro-now + +import SwiftUI + +struct FrequencyWidgetView: View { + private var entry: FrequencyWidgetTimelineProvider.Entry + private let formatter: DateComponentsFormatter + + init(entry: FrequencyWidgetTimelineProvider.Entry) { + self.entry = entry + formatter = DateComponentsFormatter() + + formatter.unitsStyle = .brief + formatter.allowedUnits = [.hour, .minute, .second] + formatter.maximumUnitCount = 2 + } + + var body: some View { + VStack(alignment: .leading) { + VStack(alignment: .leading, spacing: 4) { + Text(entry.stopName) + .font(.headline) + .frame(maxWidth: .infinity, alignment: .leading) + } + + VStack(spacing: 6) { + Text("Every") + Text( + formatter.string(from: entry.frequency)! + ) + .font(.title3) + .fontWeight(.bold) + } + .padding(.vertical) + .frame(maxWidth: .infinity, alignment: .center) + + Spacer() + Text(entry.date, format: .dateTime) + .font(.callout) + } + } +} diff --git a/apps/mobile/metro-now/widgets/widgets.swift b/apps/mobile/metro-now/widgets/widgets.swift deleted file mode 100644 index 9c77edac..00000000 --- a/apps/mobile/metro-now/widgets/widgets.swift +++ /dev/null @@ -1,232 +0,0 @@ -// metro-now -// https://github.com/krystxf/metro-now - -import SwiftUI -import WidgetKit - -struct Provider: TimelineProvider { - func placeholder(in _: Context) -> SimpleEntry { - SimpleEntry(date: Date()) - } - - func getSnapshot(in _: Context, completion: @escaping (SimpleEntry) -> Void) { - let entry = SimpleEntry(date: Date()) - completion(entry) - } - - func getTimeline(in _: Context, completion: @escaping (Timeline) -> Void) { - var entries: [SimpleEntry] = [] - - let currentDate = Date() - for hourOffset in 0 ..< 5 { - let entryDate = Calendar.current.date(byAdding: .hour, value: hourOffset, to: currentDate)! - let entry = SimpleEntry(date: entryDate) - entries.append(entry) - } - - let timeline = Timeline(entries: entries, policy: .atEnd) - completion(timeline) - } -} - -struct SimpleEntry: TimelineEntry { - let date: Date -} - - - - - -struct LargeWidgetListItemView: View { - let routeName: String - - let headsign: String - let departure: Date - - let nextHeadsing: String? - let nextDeparture: Date? - - var body: some View { - HStack{ - RouteNameIconView( - label: routeName, - background: getRouteColor(routeName) - ) - - VStack{ - HStack{ - Text(headsign) - Spacer() - Text(departure,style: .offset) - .fontDesign(.monospaced) - .multilineTextAlignment(.trailing) - } - - - if let nextHeadsing, let nextDeparture { - - HStack{ - Text(headsign == nextHeadsing ? "Also in" : nextHeadsing) - Spacer() - Text(nextDeparture,style: .offset) - .fontDesign(.monospaced) - .multilineTextAlignment(.trailing) - - } - .font(.caption) - } - } - } - } -} - -struct WidgetsEntryPlaceholderView : View { - var entry: Provider.Entry - - var body: some View { - Text("Muzeum") - .font(.headline) - - VStack{ - LargeWidgetListItemView( - routeName: "a", - headsign: "D. Hostivar", - departure: .now, - nextHeadsing: "Skalka", - nextDeparture: .now - ) - Divider() - LargeWidgetListItemView( - routeName: "a", - headsign: "N. Motol",departure: .now, - nextHeadsing: "N. Motol", - nextDeparture: .now - ) - Divider() - LargeWidgetListItemView( - routeName: "c", - headsign: "Letnany",departure: .now, - nextHeadsing: "Letnany", - nextDeparture: .now - ) - Divider() - LargeWidgetListItemView( - routeName: "c", - headsign: "Haje",departure: .now, - nextHeadsing: "Kacerov", - nextDeparture: .now - ) - Divider() - Spacer() - - Text("Last refreshed: \(entry.date, style: .time)") - .foregroundStyle(.tertiary) - .font(.footnote) - } - - - } -} - -struct LargeWidgetView : View { - var entry: Provider.Entry - - var body: some View { - Text("Muzeum") - .font(.headline) - - VStack{ - LargeWidgetListItemView( - routeName: "a", - headsign: "D. Hostivar", - departure: .now, - nextHeadsing: "Skalka", - nextDeparture: .now - ) - Divider() - LargeWidgetListItemView( - routeName: "a", - headsign: "N. Motol",departure: .now, - nextHeadsing: "N. Motol", - nextDeparture: .now - ) - Divider() - LargeWidgetListItemView( - routeName: "c", - headsign: "Letnany",departure: .now, - nextHeadsing: "Letnany", - nextDeparture: .now - ) - Divider() - LargeWidgetListItemView( - routeName: "c", - headsign: "Haje",departure: .now, - nextHeadsing: "Kacerov", - nextDeparture: .now - ) - Divider() - Spacer() - - Text("Last refreshed: \(entry.date, style: .time)") - .foregroundStyle(.tertiary) - .font(.footnote) - } - } -} - -struct widgetsEntryView: View { - var entry: Provider.Entry - @Environment(\.widgetFamily) var widgetFamily - - var body: some View { - switch widgetFamily { - case .systemLarge, .systemExtraLarge: - LargeWidgetView(entry: entry) - default: - LargeWidgetView(entry: entry) - } - } -} - -struct widgets: Widget { - let kind: String = "widgets" - - var body: some WidgetConfiguration { - StaticConfiguration(kind: kind, provider: Provider()) { entry in - if #available(iOS 17.0, *) { - widgetsEntryView(entry: entry) - .containerBackground(.fill.tertiary, for: .widget) - } else { - widgetsEntryView(entry: entry) - .padding() - .background() - } - } - .configurationDisplayName("Metro Departures") - .description("Show metro departures from selected stop.") - .supportedFamilies([ - .systemSmall, - .systemMedium, - .systemLarge, - .systemExtraLarge - ]) - } -} - -#Preview("small", as: .systemSmall) { - widgets() -} timeline: { - SimpleEntry(date: .now) -} - -#Preview("medium", as: .systemMedium) { - widgets() -} timeline: { - SimpleEntry(date: .now) -} - -#Preview("large", as: .systemLarge) { - widgets() -} timeline: { - SimpleEntry(date: .now) -} diff --git a/apps/mobile/metro-now/widgets/widgetsBundle.swift b/apps/mobile/metro-now/widgets/widgetsBundle.swift index 6bc7049c..1362fb7f 100644 --- a/apps/mobile/metro-now/widgets/widgetsBundle.swift +++ b/apps/mobile/metro-now/widgets/widgetsBundle.swift @@ -1,9 +1,5 @@ -// -// widgetsBundle.swift -// widgets -// -// Created by Kryštof Krátký on 24.11.2024. -// +// metro-now +// https://github.com/krystxf/metro-now import SwiftUI import WidgetKit @@ -11,6 +7,6 @@ import WidgetKit @main struct widgetsBundle: WidgetBundle { var body: some Widget { - widgets() + FrequencyWidget() } }