diff --git a/apps/mobile/metro-now/metro-now Watch App/pages/platform/platform-detail.view.swift b/apps/mobile/metro-now/metro-now Watch App/pages/platform/platform-detail.view.swift index af322ce4..0e9dc0bc 100644 --- a/apps/mobile/metro-now/metro-now Watch App/pages/platform/platform-detail.view.swift +++ b/apps/mobile/metro-now/metro-now Watch App/pages/platform/platform-detail.view.swift @@ -78,7 +78,11 @@ struct PlatformDetailView: View { func getDepartures() { NetworkManager.shared - .getDepartures(stopIds: [], platformIds: [platformId]) { result in + .getDepartures( + includeVehicle: .METRO, + excludeMetro: false, + stopIds: [], platformIds: [platformId] + ) { result in DispatchQueue.main.async { switch result { case let .success(departures): diff --git a/apps/mobile/metro-now/metro-now/ContentView.swift b/apps/mobile/metro-now/metro-now/ContentView.swift index e05d8ec6..568ed841 100644 --- a/apps/mobile/metro-now/metro-now/ContentView.swift +++ b/apps/mobile/metro-now/metro-now/ContentView.swift @@ -6,73 +6,54 @@ import SwiftUI struct ContentView: View { @StateObject private var locationManager = LocationManager() - @State var metroStops: [ApiStop]? = nil @State var allStops: [ApiStop]? = nil - @State var departures: [ApiDeparture]? = nil - private let timer = Timer.publish(every: 2, on: .main, in: .common).autoconnect() + @State var metroStops: [ApiStop]? = nil var body: some View { - NavigationStack { - List { - if let location = locationManager.location, - let metroStops, - let closestMetroStop = findClosestStop(to: location, stops: metroStops) - { - Section(header: Text("Metro")) { - ClosestMetroStopSectionView( - closestStop: closestMetroStop, - departures: departures - ) - .navigationTitle(closestMetroStop.name) + if let location = locationManager.location { + NavigationStack { + if let metroStops { + List { + Section(header: Text("Metro")) { + MetroDeparturesView( + location: location, + stops: metroStops + ) + } + + if let allStops { + NonMetroDeparturesView( + location: location, + stops: allStops + ) + } } + .navigationTitle( + findClosestStop( + to: location, + stops: metroStops + )?.name ?? + "Loading..." + ) } else { ProgressView() } } - } - .onAppear { - getAllMetroStops() - getAllStops() - } - .onReceive(timer) { _ in - getAllMetroStops() - getAllStops() - getStopDepartures() - } - } - - func findClosestStop(to location: CLLocation, stops: [ApiStop]) -> ApiStop? { - var closestStop: ApiStop? - var closestDistance: CLLocationDistance? - - for stop in stops { - let stopLocation = CLLocation(latitude: stop.avgLatitude, longitude: stop.avgLongitude) - - let distance = location.distance(from: stopLocation) - - guard closestDistance != nil else { - closestStop = stop - closestDistance = distance - continue - } - - if distance < closestDistance! { - closestStop = stop - closestDistance = distance + .onAppear { + getMetroStops() + getAllStops() } + } else { + NoLocationView() } - - return closestStop } - func getAllMetroStops() { - NetworkManager.shared.getMetroStops { result in + func getAllStops() { + NetworkManager.shared.getStops(metroOnly: false) { result in DispatchQueue.main.async { switch result { case let .success(stops): - - metroStops = stops - + allStops = stops case let .failure(error): print(error.localizedDescription) } @@ -80,44 +61,18 @@ struct ContentView: View { } } - func getAllStops() { - NetworkManager.shared.getAllStops { result in + func getMetroStops() { + NetworkManager.shared.getStops(metroOnly: true) { result in DispatchQueue.main.async { switch result { case let .success(stops): - - allStops = stops - + metroStops = stops case let .failure(error): print(error.localizedDescription) } } } } - - func getStopDepartures() { - guard - let location = locationManager.location, - let metroStops, - let closestStop = findClosestStop(to: location, stops: metroStops) - else { - return - } - - NetworkManager.shared - .getDepartures(stopIds: [closestStop.id], platformIds: []) { result in - DispatchQueue.main.async { - switch result { - case let .success(departures): - - self.departures = departures - - case let .failure(error): - print(error.localizedDescription) - } - } - } - } } #Preview { diff --git a/apps/mobile/metro-now/metro-now/pages/closest-stop/closest-stop-page-list-item-placeholder.view.swift b/apps/mobile/metro-now/metro-now/components/departure-list-item/departure-list-item-placeholder.view.swift similarity index 53% rename from apps/mobile/metro-now/metro-now/pages/closest-stop/closest-stop-page-list-item-placeholder.view.swift rename to apps/mobile/metro-now/metro-now/components/departure-list-item/departure-list-item-placeholder.view.swift index 09987464..dc07f69e 100644 --- a/apps/mobile/metro-now/metro-now/pages/closest-stop/closest-stop-page-list-item-placeholder.view.swift +++ b/apps/mobile/metro-now/metro-now/components/departure-list-item/departure-list-item-placeholder.view.swift @@ -4,19 +4,48 @@ import SwiftUI struct ClosestStopPageListItemPlaceholderView: View { - let routeLabel: String + let routeLabel: String? let routeLabelBackground: Color var body: some View { - ClosestStopPageListItemView( - routeLabel: routeLabel, - routeLabelBackground: routeLabelBackground, - headsign: "Loading...", - departure: .now + 10 * 60, - nextHeadsign: "Loading..", - nextDeparture: .now + 15 * 60 - ) - .redacted(reason: .placeholder) + HStack( + alignment: .top, + spacing: 8 + ) { + if let routeLabel { + RouteNameIconView( + label: routeLabel, + background: routeLabelBackground + ) + } else { + RouteNameIconView( + label: "X", + background: routeLabelBackground + ) + .redacted(reason: .placeholder) + } + + VStack(alignment: .trailing, spacing: 4) { + HStack { + Text("Loading...") + Spacer() + CountdownView(targetDate: .now + 10 * 60) + } + + HStack { + Spacer() + CountdownView( + targetDate: .now + 15 * 60 + ) { + "Also in \($0)" + } + } + .foregroundStyle(.secondary) + .font(.footnote) + } + .fontWeight(.semibold) + .redacted(reason: .placeholder) + } } } diff --git a/apps/mobile/metro-now/metro-now/pages/closest-stop/closest-stop-page-list-item.view.swift b/apps/mobile/metro-now/metro-now/components/departure-list-item/departure-list-item.view.swift similarity index 100% rename from apps/mobile/metro-now/metro-now/pages/closest-stop/closest-stop-page-list-item.view.swift rename to apps/mobile/metro-now/metro-now/components/departure-list-item/departure-list-item.view.swift diff --git a/apps/mobile/metro-now/metro-now/pages/closest-stop/closest-metro-stop-section.view.swift b/apps/mobile/metro-now/metro-now/pages/closest-stop/metro/metro-departures.view.swift similarity index 55% rename from apps/mobile/metro-now/metro-now/pages/closest-stop/closest-metro-stop-section.view.swift rename to apps/mobile/metro-now/metro-now/pages/closest-stop/metro/metro-departures.view.swift index 00cc2efd..f53bc89c 100644 --- a/apps/mobile/metro-now/metro-now/pages/closest-stop/closest-metro-stop-section.view.swift +++ b/apps/mobile/metro-now/metro-now/pages/closest-stop/metro/metro-departures.view.swift @@ -1,6 +1,7 @@ // metro-now // https://github.com/krystxf/metro-now +import CoreLocation import SwiftUI struct ClosestMetroStopSectionView: View { @@ -56,3 +57,62 @@ struct PlatformDeparturesView: View { } } } + +struct MetroDeparturesView: View { + let location: CLLocation + let stops: [ApiStop] + + init( + location: CLLocation, + stops: [ApiStop] + + ) { + self.location = location + self.stops = stops + } + + @State var departures: [ApiDeparture]? = nil + private let timer = Timer.publish(every: 5, on: .main, in: .common).autoconnect() + + var body: some View { + if let closestMetroStop = findClosestStop(to: location, stops: stops) { + ClosestMetroStopSectionView( + closestStop: closestMetroStop, + departures: departures + ) + } else { + ProgressView() + } + + EmptyView() + .onAppear { + getStopDepartures() + } + .onReceive(timer) { _ in + getStopDepartures() + } + } + + func getStopDepartures() { + guard let closestStop = findClosestStop(to: location, stops: stops) else { + return + } + + NetworkManager.shared + .getDepartures( + includeVehicle: .METRO, + excludeMetro: false, + stopIds: [closestStop.id], + platformIds: [] + ) { result in + DispatchQueue.main.async { + switch result { + case let .success(departures): + self.departures = departures + case let .failure(error): + print(error.localizedDescription) + } + } + } + } +} diff --git a/apps/mobile/metro-now/metro-now/pages/closest-stop/non-metro/non-metro-departures.view.swift b/apps/mobile/metro-now/metro-now/pages/closest-stop/non-metro/non-metro-departures.view.swift new file mode 100644 index 00000000..9c1bb72f --- /dev/null +++ b/apps/mobile/metro-now/metro-now/pages/closest-stop/non-metro/non-metro-departures.view.swift @@ -0,0 +1,149 @@ +// metro-now +// https://github.com/krystxf/metro-now + +import CoreLocation +import SwiftUI + +struct PlatformSectionListView: View { + let departures: [[ApiDeparture]] + + var body: some View { + ForEach(departures, id: \.first?.id) { deps in + let departure = deps.count > 0 ? deps[0] : nil + let nextDeparture = deps.count > 1 ? deps[1] : nil + + if let departure { + ClosestStopPageListItemView( + routeLabel: departure.route, + routeLabelBackground: getColorByRouteName(departure.route), + headsign: departure.headsign, + departure: departure.departure.predicted, + nextHeadsign: nextDeparture?.headsign, + nextDeparture: nextDeparture?.departure.predicted + ) + } else { + Text("Loading") + } + } + } +} + +struct PlatformSectionListPlaceholderView: View { + let routes: [ApiRoute] + let maxItems: Int = 3 + + var body: some View { + ForEach(routes.prefix(maxItems), id: \.id) { route in + ClosestStopPageListItemPlaceholderView( + routeLabel: route.name, + routeLabelBackground: getColorByRouteName(route.name) + ) + } + } +} + +struct PlatformSectionView: View { + let platform: ApiPlatform + let departures: [[ApiDeparture]]? + + init(platform: ApiPlatform, departures: [ApiDeparture]?) { + self.platform = platform + + guard let departures else { + self.departures = nil + return + } + + let filteredDepartures = departures + .filter { + platform.id == $0.platformId + } + + let departuresByRoute = Dictionary( + grouping: filteredDepartures, + by: { $0.route } + ) + + self.departures = Array( + departuresByRoute + .map(\.value) + .sorted(by: { + $0.first!.departure.predicted < $1.first!.departure.predicted + } + ) + ) + } + + var body: some View { + if departures == nil || departures!.count > 0 { + Section(header: Text(getPlatformLabel(platform))) { + if let departures { + PlatformSectionListView(departures: departures) + } else { + PlatformSectionListPlaceholderView(routes: platform.routes) + .redacted(reason: .placeholder) + } + } + } else { + EmptyView() + } + } +} + +struct NonMetroDeparturesView: View { + let location: CLLocation + let stops: [ApiStop] + + @StateObject private var locationManager = LocationManager() + @State var departures: [ApiDeparture]? = nil + private let timer = Timer.publish(every: 5, on: .main, in: .common).autoconnect() + + var body: some View { + if let closestStop = findClosestStop(to: location, stops: stops) { + let platforms = closestStop.platforms.filter { !$0.isMetro }.sorted( + by: { getPlatformLabel($0) < getPlatformLabel($1) } + ) + + ForEach(platforms, id: \.id) { platform in + PlatformSectionView( + platform: platform, + departures: departures + ) + } + + } else { + ProgressView() + } + + EmptyView() + .onAppear { +// getStopDepartures() + } + .onReceive(timer) { _ in + getStopDepartures() + } + } + + func getStopDepartures() { + let closestStop = findClosestStop(to: location, stops: stops)! + + NetworkManager.shared + .getDepartures( + includeVehicle: .ALL, + excludeMetro: true, + stopIds: [closestStop.id], + platformIds: [] + ) { result in + DispatchQueue.main.async { + switch result { + case let .success(departures): + + self.departures = departures + + case let .failure(error): + print(error.localizedDescription) + } + } + } + } +}