diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 00000000..8900a770 --- /dev/null +++ b/app/.gitignore @@ -0,0 +1,92 @@ +# Xcode +# +# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore + +## User settings +xcuserdata/ + +## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) +*.xcscmblueprint +*.xccheckout + +## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) +build/ +DerivedData/ +*.moved-aside +*.pbxuser +!default.pbxuser +*.mode1v3 +!default.mode1v3 +*.mode2v3 +!default.mode2v3 +*.perspectivev3 +!default.perspectivev3 + +## Obj-C/Swift specific +*.hmap + +## App packaging +*.ipa +*.dSYM.zip +*.dSYM + +## Playgrounds +timeline.xctimeline +playground.xcworkspace + +# Swift Package Manager +# +# Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. +# Packages/ +# Package.pins +# Package.resolved +# *.xcodeproj +# +# Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata +# hence it is not needed unless you have added a package configuration file to your project +# .swiftpm + +.build/ + +# CocoaPods +# +# We recommend against adding the Pods directory to your .gitignore. However +# you should judge for yourself, the pros and cons are mentioned at: +# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control +# +# Pods/ +# +# Add this line if you want to avoid checking in source code from the Xcode workspace +# *.xcworkspace + +# Carthage +# +# Add this line if you want to avoid checking in source code from Carthage dependencies. +# Carthage/Checkouts + +Carthage/Build/ + +# Accio dependency management +Dependencies/ +.accio/ + +# fastlane +# +# It is recommended to not store the screenshots in the git repo. +# Instead, use fastlane to re-generate the screenshots whenever they are needed. +# For more information about the recommended setup visit: +# https://docs.fastlane.tools/best-practices/source-control/#source-control + +fastlane/report.xml +fastlane/Preview.html +fastlane/screenshots/**/*.png +fastlane/test_output + +# Code Injection +# +# After new code Injection tools there's a generated folder /iOSInjectionProject +# https://github.com/johnno1962/injectionforxcode + +iOSInjectionProject/ + +env.swift \ No newline at end of file diff --git a/app/MetroMate Watch App/Assets.xcassets/AccentColor.colorset/Contents.json b/app/MetroMate Watch App/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 00000000..eb878970 --- /dev/null +++ b/app/MetroMate Watch App/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/app/MetroMate Watch App/Assets.xcassets/AppIcon.appiconset/Contents.json b/app/MetroMate Watch App/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000..110a3e31 --- /dev/null +++ b/app/MetroMate Watch App/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,14 @@ +{ + "images" : [ + { + "filename" : "icon.png", + "idiom" : "universal", + "platform" : "watchos", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/app/MetroMate Watch App/Assets.xcassets/AppIcon.appiconset/icon.png b/app/MetroMate Watch App/Assets.xcassets/AppIcon.appiconset/icon.png new file mode 100644 index 00000000..0761338d Binary files /dev/null and b/app/MetroMate Watch App/Assets.xcassets/AppIcon.appiconset/icon.png differ diff --git a/app/MetroMate Watch App/Assets.xcassets/Contents.json b/app/MetroMate Watch App/Assets.xcassets/Contents.json new file mode 100644 index 00000000..73c00596 --- /dev/null +++ b/app/MetroMate Watch App/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/app/MetroMate Watch App/MetroMateApp.swift b/app/MetroMate Watch App/MetroMateApp.swift new file mode 100644 index 00000000..2a67d93f --- /dev/null +++ b/app/MetroMate Watch App/MetroMateApp.swift @@ -0,0 +1,17 @@ +// +// MetroMateApp.swift +// MetroMate Watch App +// +// Created by Kryštof Krátký on 31.03.2024. +// + +import SwiftUI + +@main +struct MetroMate_Watch_AppApp: App { + var body: some Scene { + WindowGroup { + ContentView() + } + } +} diff --git a/app/MetroMate Watch App/Preview Content/Preview Assets.xcassets/Contents.json b/app/MetroMate Watch App/Preview Content/Preview Assets.xcassets/Contents.json new file mode 100644 index 00000000..73c00596 --- /dev/null +++ b/app/MetroMate Watch App/Preview Content/Preview Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/app/MetroMate Watch App/departures/departure.swift b/app/MetroMate Watch App/departures/departure.swift new file mode 100644 index 00000000..ef77b9fc --- /dev/null +++ b/app/MetroMate Watch App/departures/departure.swift @@ -0,0 +1,48 @@ +// +// departure.swift +// MetroMate +// +// Created by Kryštof Krátký on 01.04.2024. +// + +import Foundation + +struct Departure: Codable, Identifiable, Hashable { + let departureTimestamp: DepartureTimestamp + let trip: DepartureTrip + let delay: DepartureDelay? + let stop: DepartureStop + let route: DepartureRoute + + var id: String { return trip.id } + + func hash(into hasher: inout Hasher) { + hasher.combine(trip.id) + } + + static func == (lhs: Departure, rhs: Departure) -> Bool { + return lhs.trip.id == rhs.trip.id + } +} + +struct DepartureStop: Codable { + let id: String +} + +struct DepartureRoute: Codable { + let shortName: String +} + +struct DepartureTimestamp: Codable { + let predicted: String + let scheduled: String +} + +struct DepartureTrip: Codable { + let headsign: String + let id: String +} + +struct DepartureDelay: Codable { + let minutes: Int? +} diff --git a/app/MetroMate Watch App/departures/fetch-departures.swift b/app/MetroMate Watch App/departures/fetch-departures.swift new file mode 100644 index 00000000..accb1ad4 --- /dev/null +++ b/app/MetroMate Watch App/departures/fetch-departures.swift @@ -0,0 +1,94 @@ +// +// fetch-departures.swift +// MetroMate +// +// Created by Kryštof Krátký on 01.04.2024. +// + +import Foundation + +let ENDPOINT_URL = "https://api.golemio.cz/v2/pid/departureboards" +let REQUEST_PARAMETERS = [ + URLQueryItem(name: "includeMetroTrains", value: "true"), + URLQueryItem(name: "preferredTimezone", value: "Europe_Prague"), + URLQueryItem(name: "mode", value: "departures"), + URLQueryItem(name: "order", value: "real"), + URLQueryItem(name: "filter", value: "none"), + URLQueryItem(name: "minutesBefore", value: String(2)), + URLQueryItem(name: "minutesAfter", value: String(360)) + +] + +struct DepartureBoardResponse: Codable { + let departures: [Departure] +} + +func fetchDepartureBoardData( + platformIDs: [String], // gtfsIDs + completion: @escaping (Result<[Departure], Error>) -> Void +) { + guard let baseURL = URL(string: ENDPOINT_URL) else { + print("Invalid base URL") + return + } + + var components = URLComponents( + url: baseURL, + resolvingAgainstBaseURL: false + ) + components?.queryItems = REQUEST_PARAMETERS + platformIDs.map { platformID in + URLQueryItem(name: "ids[]", value: platformID) + } + + guard let url = components?.url else { + print("Failed to construct URL") + return + } + + var request = URLRequest(url: url) + request.httpMethod = "GET" + request.setValue( + GOLEMIO_API_KEY, + forHTTPHeaderField: "X-Access-Token" + ) + + let task = URLSession.shared.dataTask(with: request) { data, response, error in + if let error = error { + completion(.failure(error)) + return + } + + guard let httpResponse = response as? HTTPURLResponse, + (200 ... 299).contains(httpResponse.statusCode) + else { + completion( + .failure( + NSError( + domain: "InvalidResponse", + code: 0, + userInfo: nil + ) + ) + ) + return + } + + if let data = data { + do { + let decoder = JSONDecoder() + decoder.keyDecodingStrategy = .convertFromSnakeCase + + let decodedResponse = try decoder.decode(DepartureBoardResponse.self, from: data) + + completion( + .success(decodedResponse.departures) + ) + } catch { + completion( + .failure(error) + ) + } + } + } + task.resume() +} diff --git a/app/MetroMate Watch App/lines.utils.swift b/app/MetroMate Watch App/lines.utils.swift new file mode 100644 index 00000000..8723f5e6 --- /dev/null +++ b/app/MetroMate Watch App/lines.utils.swift @@ -0,0 +1,22 @@ +// +// lines.utils.swift +// MetroMate +// +// Created by Kryštof Krátký on 31.03.2024. +// + +import Foundation +import SwiftUI + +func getLineColor(line: String) -> Color { + switch line { + case "A": + return .green + case "B": + return .yellow + case "C": + return .red + default: + return .white + } +} diff --git a/app/MetroMate Watch App/location/location.manager.swift b/app/MetroMate Watch App/location/location.manager.swift new file mode 100644 index 00000000..7dce8447 --- /dev/null +++ b/app/MetroMate Watch App/location/location.manager.swift @@ -0,0 +1,50 @@ +// +// location.manager.swift +// MetroMate +// +// Created by Kryštof Krátký on 31.03.2024. +// + +import CoreLocation +import Foundation + +class LocationManager: NSObject, ObservableObject { + private let manager = CLLocationManager() + @Published var userLocation: CLLocation? + static let shared = LocationManager() + + override init() { + super.init() + manager.delegate = self + manager.desiredAccuracy = kCLLocationAccuracyHundredMeters + manager.startUpdatingLocation() + } + + func requestLocation() { + manager.requestWhenInUseAuthorization() + } +} + +extension LocationManager: CLLocationManagerDelegate { + func locationManager(_: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) { + switch status { + case .notDetermined: + print("location manager: notDetermined") + case .restricted: + print("location manager: restricted") + case .denied: + print("location manager: denied") + case .authorizedAlways: + print("location manager: authorizedAlways") + case .authorizedWhenInUse: + print("location manager: authorizedWhenInUse") + @unknown default: + break + } + } + + func locationManager(_: CLLocationManager, didUpdateLocations locations: [CLLocation]) { + guard let location = locations.last else { return } + userLocation = location + } +} diff --git a/app/MetroMate Watch App/location/location.utils.swift b/app/MetroMate Watch App/location/location.utils.swift new file mode 100644 index 00000000..708f7e5f --- /dev/null +++ b/app/MetroMate Watch App/location/location.utils.swift @@ -0,0 +1,70 @@ +// +// location.utils.swift +// MetroMate +// +// Created by Kryštof Krátký on 01.04.2024. +// + +import Foundation + +let EARTH_RADIUS_KM = 6371.0 + +// Calculate distance with maximum accuracy just for fun +func getDistanceBetweenCoordinates(lat1: Double, lon1: Double, lat2: Double, lon2: Double) -> Double { + // Convert latitude and longitude from degrees to radians + let lat1Rad = lat1 * .pi / 180.0 + let lon1Rad = lon1 * .pi / 180.0 + let lat2Rad = lat2 * .pi / 180.0 + let lon2Rad = lon2 * .pi / 180.0 + + // Calculate differences in latitude and longitude + let deltaLat = lat2Rad - lat1Rad + let deltaLon = lon2Rad - lon1Rad + + // Haversine formula + let a = sin(deltaLat / 2) * sin(deltaLat / 2) + + cos(lat1Rad) * cos(lat2Rad) * + sin(deltaLon / 2) * sin(deltaLon / 2) + let c = 2 * atan2(sqrt(a), sqrt(1 - a)) + let distance = EARTH_RADIUS_KM * c + + return distance +} + +func getDistanceToStation(lat:Double, lon:Double,station:Station)->Double{ + let distanceToStation = getDistanceBetweenCoordinates(lat1: lat, lon1: lon, lat2: station.avgLat, lon2: station.avgLon) + + return distanceToStation +} + +func getClosestStation(lat: Double, lon: Double) -> Station { + let stations = Station.allStations + + var closestStationIndex: Int = 0 + var distanceToClosestStation = Double.infinity + + for (index, station) in stations.enumerated() { + let distanceToStation = getDistanceToStation(lat:lat, lon:lon, station:station) + + if distanceToStation < distanceToClosestStation { + distanceToClosestStation = distanceToStation + closestStationIndex = index + } + } + + return stations[closestStationIndex] +} + +// stations array sorted from the closest to the farthest +func getStationsSortedByDistance(lat: Double, lon: Double) -> [Station] { + let stations = Station.allStations + + let sortedStations = stations.sorted{station1,station2 in + let distanceToStation1 = getDistanceToStation(lat: lat, lon: lon, station: station1) + let distanceToStation2 = getDistanceToStation(lat: lat, lon: lon, station: station2) + + return distanceToStation1 < distanceToStation2 + } + + return []; +} diff --git a/app/MetroMate Watch App/name.utils.swift b/app/MetroMate Watch App/name.utils.swift new file mode 100644 index 00000000..6a1c80bb --- /dev/null +++ b/app/MetroMate Watch App/name.utils.swift @@ -0,0 +1,43 @@ +// +// name.utils.swift +// MetroMate +// +// Created by Kryštof Krátký on 31.03.2024. +// + +import Foundation + +func getShortenStationName(_ name: String) -> String { + // Exceptions: + switch name { + case "Černý Most": + return "Č. Most" + case "Nemocnice Motol": + return "Motol" + case "Depo Hostivař": + return "Hostivař" + case "Jiřího z Poděbrad": + return "J. z Poděbrad" + case "Hlavní nádraží": + return "Hl. nádraží" + case "Pražského povstání": + return "P. povstání" + case "I. P. Pavlova": + return "I. P. Pavl." + case "Nové Butovice": + return "N. Butovice" + default: + break + } + + var shortened = name + + // Replace frequent woeds in station names + shortened = shortened.replacing(/^Nádraží/, with: "Nádr.") + shortened = shortened.replacing(/nádraží$/, with: "nádr.") + + shortened = shortened.replacing(/^Náměstí/, with: "Nám.") + shortened = shortened.replacing(/náměstí$/, with: "nám.") + + return shortened +} diff --git a/app/MetroMate Watch App/stations/stations.json b/app/MetroMate Watch App/stations/stations.json new file mode 100644 index 00000000..7a852b85 --- /dev/null +++ b/app/MetroMate Watch App/stations/stations.json @@ -0,0 +1,993 @@ +[ + { + "name": "Anděl", + "avgLat": 50.07126, + "avgLon": 14.4033651, + "platforms": [ + { + "gtfsId": "U1040Z101P", + "name": "B", + "direction": "Zličín" + }, + { + "gtfsId": "U1040Z102P", + "name": "B", + "direction": "Černý Most" + } + ] + }, + { + "name": "Bořislavka", + "avgLat": 50.09862, + "avgLon": 14.36417, + "platforms": [ + { + "gtfsId": "U157Z101P", + "name": "A", + "direction": "Depo Hostivař" + }, + { + "gtfsId": "U157Z102P", + "name": "A", + "direction": "Nemocnice Motol" + } + ] + }, + { + "name": "Budějovická", + "avgLat": 50.0448837, + "avgLon": 14.4482613, + "platforms": [ + { + "gtfsId": "U50Z101P", + "name": "C", + "direction": "Letňany" + }, + { + "gtfsId": "U50Z102P", + "name": "C", + "direction": "Háje" + } + ] + }, + { + "name": "Černý Most", + "avgLat": 50.10898, + "avgLon": 14.5774412, + "platforms": [ + { + "gtfsId": "U897Z101P", + "name": "B", + "direction": "Zličín" + } + ] + }, + { + "name": "Českomoravská", + "avgLat": 50.1059647, + "avgLon": 14.4922371, + "platforms": [ + { + "gtfsId": "U510Z101P", + "name": "B", + "direction": "Zličín" + }, + { + "gtfsId": "U510Z102P", + "name": "B", + "direction": "Černý Most" + } + ] + }, + { + "name": "Dejvická", + "avgLat": 50.1009, + "avgLon": 14.393199, + "platforms": [ + { + "gtfsId": "U321Z101P", + "name": "A", + "direction": "Depo Hostivař" + }, + { + "gtfsId": "U321Z102P", + "name": "A", + "direction": "Nemocnice Motol" + } + ] + }, + { + "name": "Depo Hostivař", + "avgLat": 50.07664, + "avgLon": 14.5167828, + "platforms": [ + { + "gtfsId": "U1071Z101P", + "name": "A", + "direction": "Nemocnice Motol" + }, + { + "gtfsId": "U1071Z102P", + "name": "A", + "direction": "Nemocnice Motol" + } + ] + }, + { + "name": "Flora", + "avgLat": 50.0781326, + "avgLon": 14.4617405, + "platforms": [ + { + "gtfsId": "U118Z101P", + "name": "A", + "direction": "Depo Hostivař" + }, + { + "gtfsId": "U118Z102P", + "name": "A", + "direction": "Nemocnice Motol" + } + ] + }, + { + "name": "Florenc", + "avgLat": 50.0905075, + "avgLon": 14.438818, + "platforms": [ + { + "gtfsId": "U689Z101P", + "name": "B", + "direction": "Zličín" + }, + { + "gtfsId": "U689Z102P", + "name": "B", + "direction": "Černý Most" + }, + { + "gtfsId": "U689Z121P", + "name": "C", + "direction": "Letňany" + }, + { + "gtfsId": "U689Z122P", + "name": "C", + "direction": "Háje" + } + ] + }, + { + "name": "Háje", + "avgLat": 50.0305176, + "avgLon": 14.5269022, + "platforms": [ + { + "gtfsId": "U286Z101P", + "name": "C", + "direction": "Letňany" + } + ] + }, + { + "name": "Hlavní nádraží", + "avgLat": 50.0838, + "avgLon": 14.4347754, + "platforms": [ + { + "gtfsId": "U142Z101P", + "name": "C", + "direction": "Letňany" + }, + { + "gtfsId": "U142Z102P", + "name": "C", + "direction": "Háje" + } + ] + }, + { + "name": "Hloubětín", + "avgLat": 50.1062622, + "avgLon": 14.5371313, + "platforms": [ + { + "gtfsId": "U135Z101P", + "name": "B", + "direction": "Zličín" + }, + { + "gtfsId": "U135Z102P", + "name": "B", + "direction": "Černý Most" + } + ] + }, + { + "name": "Hradčanská", + "avgLat": 50.0975037, + "avgLon": 14.4043093, + "platforms": [ + { + "gtfsId": "U163Z101P", + "name": "A", + "direction": "Depo Hostivař" + }, + { + "gtfsId": "U163Z102P", + "name": "A", + "direction": "Nemocnice Motol" + } + ] + }, + { + "name": "Hůrka", + "avgLat": 50.0496368, + "avgLon": 14.3416481, + "platforms": [ + { + "gtfsId": "U1154Z101P", + "name": "B", + "direction": "Zličín" + }, + { + "gtfsId": "U1154Z102P", + "name": "B", + "direction": "Černý Most" + } + ] + }, + { + "name": "Chodov", + "avgLat": 50.03112, + "avgLon": 14.4914255, + "platforms": [ + { + "gtfsId": "U52Z102P", + "name": "C", + "direction": "Háje" + }, + { + "gtfsId": "U52Z101P", + "name": "C", + "direction": "Letňany" + } + ] + }, + { + "name": "I. P. Pavlova", + "avgLat": 50.07518, + "avgLon": 14.4305162, + "platforms": [ + { + "gtfsId": "U190Z101P", + "name": "C", + "direction": "Letňany" + }, + { + "gtfsId": "U190Z102P", + "name": "C", + "direction": "Háje" + } + ] + }, + { + "name": "Invalidovna", + "avgLat": 50.097393, + "avgLon": 14.4643278, + "platforms": [ + { + "gtfsId": "U655Z101P", + "name": "B", + "direction": "Zličín" + }, + { + "gtfsId": "U655Z102P", + "name": "B", + "direction": "Černý Most" + } + ] + }, + { + "name": "Jinonice", + "avgLat": 50.0544548, + "avgLon": 14.3706169, + "platforms": [ + { + "gtfsId": "U685Z101P", + "name": "B", + "direction": "Zličín" + }, + { + "gtfsId": "U685Z102P", + "name": "B", + "direction": "Černý Most" + } + ] + }, + { + "name": "Jiřího z Poděbrad", + "avgLat": 50.0775261, + "avgLon": 14.44971, + "platforms": [ + { + "gtfsId": "U209Z101P", + "name": "A", + "direction": "Depo Hostivař" + }, + { + "gtfsId": "U209Z102P", + "name": "A", + "direction": "Nemocnice Motol" + } + ] + }, + { + "name": "Kačerov", + "avgLat": 50.0418549, + "avgLon": 14.4603252, + "platforms": [ + { + "gtfsId": "U228Z101P", + "name": "C", + "direction": "Letňany" + }, + { + "gtfsId": "U228Z102P", + "name": "C", + "direction": "Háje" + } + ] + }, + { + "name": "Karlovo náměstí", + "avgLat": 50.07529, + "avgLon": 14.4185162, + "platforms": [ + { + "gtfsId": "U237Z101P", + "name": "B", + "direction": "Zličín" + }, + { + "gtfsId": "U237Z102P", + "name": "B", + "direction": "Černý Most" + } + ] + }, + { + "name": "Kobylisy", + "avgLat": 50.12433, + "avgLon": 14.4544983, + "platforms": [ + { + "gtfsId": "U675Z101P", + "name": "C", + "direction": "Letňany" + }, + { + "gtfsId": "U675Z102P", + "name": "C", + "direction": "Háje" + } + ] + }, + { + "name": "Kolbenova", + "avgLat": 50.11049, + "avgLon": 14.5162048, + "platforms": [ + { + "gtfsId": "U75Z101P", + "name": "B", + "direction": "Zličín" + }, + { + "gtfsId": "U75Z102P", + "name": "B", + "direction": "Černý Most" + } + ] + }, + { + "name": "Křižíkova", + "avgLat": 50.09329, + "avgLon": 14.4513893, + "platforms": [ + { + "gtfsId": "U758Z101P", + "name": "B", + "direction": "Zličín" + }, + { + "gtfsId": "U758Z102P", + "name": "B", + "direction": "Černý Most" + } + ] + }, + { + "name": "Ládví", + "avgLat": 50.12643, + "avgLon": 14.4697676, + "platforms": [ + { + "gtfsId": "U78Z101P", + "name": "C", + "direction": "Letňany" + }, + { + "gtfsId": "U78Z102P", + "name": "C", + "direction": "Háje" + } + ] + }, + { + "name": "Letňany", + "avgLat": 50.1256142, + "avgLon": 14.51615, + "platforms": [ + { + "gtfsId": "U1000Z102P", + "name": "C", + "direction": "Háje" + } + ] + }, + { + "name": "Luka", + "avgLat": 50.0454369, + "avgLon": 14.3213024, + "platforms": [ + { + "gtfsId": "U1007Z101P", + "name": "B", + "direction": "Zličín" + }, + { + "gtfsId": "U1007Z102P", + "name": "B", + "direction": "Černý Most" + } + ] + }, + { + "name": "Lužiny", + "avgLat": 50.0444946, + "avgLon": 14.3304424, + "platforms": [ + { + "gtfsId": "U258Z101P", + "name": "B", + "direction": "Zličín" + }, + { + "gtfsId": "U258Z102P", + "name": "B", + "direction": "Černý Most" + } + ] + }, + { + "name": "Malostranská", + "avgLat": 50.09125, + "avgLon": 14.4099207, + "platforms": [ + { + "gtfsId": "U360Z101P", + "name": "A", + "direction": "Depo Hostivař" + }, + { + "gtfsId": "U360Z102P", + "name": "A", + "direction": "Nemocnice Motol" + } + ] + }, + { + "name": "Můstek", + "avgLat": 50.08347, + "avgLon": 14.4239731, + "platforms": [ + { + "gtfsId": "U1072Z101P", + "name": "A", + "direction": "Depo Hostivař" + }, + { + "gtfsId": "U1072Z102P", + "name": "A", + "direction": "Nemocnice Motol" + }, + { + "gtfsId": "U1072Z121P", + "name": "B", + "direction": "Zličín" + }, + { + "gtfsId": "U1072Z122P", + "name": "B", + "direction": "Černý Most" + } + ] + }, + { + "name": "Muzeum", + "avgLat": 50.07953, + "avgLon": 14.4318123, + "platforms": [ + { + "gtfsId": "U400Z101P", + "name": "A", + "direction": "Depo Hostivař" + }, + { + "gtfsId": "U400Z102P", + "name": "A", + "direction": "Nemocnice Motol" + }, + { + "gtfsId": "U400Z121P", + "name": "C", + "direction": "Letňany" + }, + { + "gtfsId": "U400Z122P", + "name": "C", + "direction": "Háje" + } + ] + }, + { + "name": "Nádraží Holešovice", + "avgLat": 50.10902, + "avgLon": 14.4394522, + "platforms": [ + { + "gtfsId": "U115Z101P", + "name": "C", + "direction": "Letňany" + }, + { + "gtfsId": "U115Z102P", + "name": "C", + "direction": "Háje" + } + ] + }, + { + "name": "Nádraží Veleslavín", + "avgLat": 50.095726, + "avgLon": 14.3468428, + "platforms": [ + { + "gtfsId": "U462Z101P", + "name": "A", + "direction": "Depo Hostivař" + }, + { + "gtfsId": "U462Z102P", + "name": "A", + "direction": "Nemocnice Motol" + } + ] + }, + { + "name": "Náměstí Míru", + "avgLat": 50.07506, + "avgLon": 14.4380789, + "platforms": [ + { + "gtfsId": "U476Z101P", + "name": "A", + "direction": "Depo Hostivař" + }, + { + "gtfsId": "U476Z102P", + "name": "A", + "direction": "Nemocnice Motol" + } + ] + }, + { + "name": "Náměstí Republiky", + "avgLat": 50.0889244, + "avgLon": 14.429677, + "platforms": [ + { + "gtfsId": "U480Z101P", + "name": "B", + "direction": "Zličín" + }, + { + "gtfsId": "U480Z102P", + "name": "B", + "direction": "Černý Most" + } + ] + }, + { + "name": "Národní třída", + "avgLat": 50.0805855, + "avgLon": 14.41992, + "platforms": [ + { + "gtfsId": "U539Z101P", + "name": "B", + "direction": "Zličín" + }, + { + "gtfsId": "U539Z102P", + "name": "B", + "direction": "Černý Most" + } + ] + }, + { + "name": "Nemocnice Motol", + "avgLat": 50.0749741, + "avgLon": 14.3407869, + "platforms": [ + { + "gtfsId": "U306Z101P", + "name": "A", + "direction": "Depo Hostivař" + } + ] + }, + { + "name": "Nové Butovice", + "avgLat": 50.0509758, + "avgLon": 14.3522758, + "platforms": [ + { + "gtfsId": "U602Z101P", + "name": "B", + "direction": "Zličín" + }, + { + "gtfsId": "U602Z102P", + "name": "B", + "direction": "Černý Most" + } + ] + }, + { + "name": "Opatov", + "avgLat": 50.0276947, + "avgLon": 14.5084009, + "platforms": [ + { + "gtfsId": "U106Z101P", + "name": "C", + "direction": "Letňany" + }, + { + "gtfsId": "U106Z102P", + "name": "C", + "direction": "Háje" + } + ] + }, + { + "name": "Palmovka", + "avgLat": 50.1038132, + "avgLon": 14.4749022, + "platforms": [ + { + "gtfsId": "U529Z101P", + "name": "B", + "direction": "Zličín" + }, + { + "gtfsId": "U529Z102P", + "name": "B", + "direction": "Černý Most" + } + ] + }, + { + "name": "Pankrác", + "avgLat": 50.0511665, + "avgLon": 14.4392538, + "platforms": [ + { + "gtfsId": "U385Z101P", + "name": "C", + "direction": "Letňany" + }, + { + "gtfsId": "U385Z102P", + "name": "C", + "direction": "Háje" + } + ] + }, + { + "name": "Petřiny", + "avgLat": 50.0875053, + "avgLon": 14.344923, + "platforms": [ + { + "gtfsId": "U507Z101P", + "name": "A", + "direction": "Depo Hostivař" + }, + { + "gtfsId": "U507Z102P", + "name": "A", + "direction": "Nemocnice Motol" + } + ] + }, + { + "name": "Pražského povstání", + "avgLat": 50.05617, + "avgLon": 14.43422, + "platforms": [ + { + "gtfsId": "U597Z101P", + "name": "C", + "direction": "Letňany" + }, + { + "gtfsId": "U597Z102P", + "name": "C", + "direction": "Háje" + } + ] + }, + { + "name": "Prosek", + "avgLat": 50.1192856, + "avgLon": 14.4982214, + "platforms": [ + { + "gtfsId": "U603Z101P", + "name": "C", + "direction": "Letňany" + }, + { + "gtfsId": "U603Z102P", + "name": "C", + "direction": "Háje" + } + ] + }, + { + "name": "Radlická", + "avgLat": 50.0584869, + "avgLon": 14.3888187, + "platforms": [ + { + "gtfsId": "U957Z101P", + "name": "B", + "direction": "Zličín" + }, + { + "gtfsId": "U957Z102P", + "name": "B", + "direction": "Černý Most" + } + ] + }, + { + "name": "Rajská zahrada", + "avgLat": 50.1065674, + "avgLon": 14.5598669, + "platforms": [ + { + "gtfsId": "U818Z101P", + "name": "B", + "direction": "Zličín" + }, + { + "gtfsId": "U818Z102P", + "name": "B", + "direction": "Černý Most" + } + ] + }, + { + "name": "Roztyly", + "avgLat": 50.0376625, + "avgLon": 14.477663, + "platforms": [ + { + "gtfsId": "U601Z101P", + "name": "C", + "direction": "Letňany" + }, + { + "gtfsId": "U601Z102P", + "name": "C", + "direction": "Háje" + } + ] + }, + { + "name": "Skalka", + "avgLat": 50.068222, + "avgLon": 14.5077934, + "platforms": [ + { + "gtfsId": "U953Z101P", + "name": "A", + "direction": "Depo Hostivař" + }, + { + "gtfsId": "U953Z102P", + "name": "A", + "direction": "Nemocnice Motol" + } + ] + }, + { + "name": "Smíchovské nádraží", + "avgLat": 50.06086, + "avgLon": 14.4091272, + "platforms": [ + { + "gtfsId": "U458Z101P", + "name": "B", + "direction": "Zličín" + }, + { + "gtfsId": "U458Z102P", + "name": "B", + "direction": "Černý Most" + } + ] + }, + { + "name": "Staroměstská", + "avgLat": 50.0886536, + "avgLon": 14.4163837, + "platforms": [ + { + "gtfsId": "U703Z101P", + "name": "A", + "direction": "Depo Hostivař" + }, + { + "gtfsId": "U703Z102P", + "name": "A", + "direction": "Nemocnice Motol" + } + ] + }, + { + "name": "Stodůlky", + "avgLat": 50.0466423, + "avgLon": 14.307148, + "platforms": [ + { + "gtfsId": "U1140Z101P", + "name": "B", + "direction": "Zličín" + }, + { + "gtfsId": "U1140Z102P", + "name": "B", + "direction": "Černý Most" + } + ] + }, + { + "name": "Strašnická", + "avgLat": 50.0726929, + "avgLon": 14.4910736, + "platforms": [ + { + "gtfsId": "U713Z101P", + "name": "A", + "direction": "Depo Hostivař" + }, + { + "gtfsId": "U713Z102P", + "name": "A", + "direction": "Nemocnice Motol" + } + ] + }, + { + "name": "Střížkov", + "avgLat": 50.1263466, + "avgLon": 14.4892445, + "platforms": [ + { + "gtfsId": "U332Z101P", + "name": "C", + "direction": "Letňany" + }, + { + "gtfsId": "U332Z102P", + "name": "C", + "direction": "Háje" + } + ] + }, + { + "name": "Vltavská", + "avgLat": 50.0988846, + "avgLon": 14.4383736, + "platforms": [ + { + "gtfsId": "U100Z101P", + "name": "C", + "direction": "Letňany" + }, + { + "gtfsId": "U100Z102P", + "name": "C", + "direction": "Háje" + } + ] + }, + { + "name": "Vysočanská", + "avgLat": 50.11072, + "avgLon": 14.5022192, + "platforms": [ + { + "gtfsId": "U474Z101P", + "name": "B", + "direction": "Zličín" + }, + { + "gtfsId": "U474Z102P", + "name": "B", + "direction": "Černý Most" + } + ] + }, + { + "name": "Vyšehrad", + "avgLat": 50.06116, + "avgLon": 14.4305353, + "platforms": [ + { + "gtfsId": "U527Z101P", + "name": "C", + "direction": "Letňany" + }, + { + "gtfsId": "U527Z102P", + "name": "C", + "direction": "Háje" + } + ] + }, + { + "name": "Zličín", + "avgLat": 50.0546761, + "avgLon": 14.2901535, + "platforms": [ + { + "gtfsId": "U1141Z102P", + "name": "B", + "direction": "Černý Most" + } + ] + }, + { + "name": "Želivského", + "avgLat": 50.0785637, + "avgLon": 14.4737558, + "platforms": [ + { + "gtfsId": "U921Z101P", + "name": "A", + "direction": "Depo Hostivař" + }, + { + "gtfsId": "U921Z102P", + "name": "A", + "direction": "Nemocnice Motol" + } + ] + } +] diff --git a/app/MetroMate Watch App/stations/stations.swift b/app/MetroMate Watch App/stations/stations.swift new file mode 100644 index 00000000..e560e227 --- /dev/null +++ b/app/MetroMate Watch App/stations/stations.swift @@ -0,0 +1,52 @@ +// +// stations.swift +// MetroMate Watch App +// +// Created by Kryštof Krátký on 31.03.2024. +// + +import Foundation + +struct Station: Codable { + let name: String + let avgLat, avgLon: Double + /** + * there are three possible lengths of stops array + * 1 - final station + * 2 - normal station + * 4 - transit station (Můstek, Muzeum, Florenc) + */ + let platforms: [Platform] + + static let allStations: [Station] = Bundle.main.decode(file: "stations") +} + +struct Platform: Codable, Identifiable { + var id: String { return gtfsId } + + let gtfsId: String + let name: String // "A" / "B" / "C" + let direction: String +} + + + +extension Bundle { + func decode(file: String) -> T { + guard let url = url(forResource: file, withExtension: "json") else { + fatalError("Could not find \(file) file") + } + + guard let data = try? Data(contentsOf: url) else { + fatalError("Could not load \(file) file") + } + + let decoder = JSONDecoder() + + guard let loadedData = try? decoder.decode(T.self, from: data) else { + fatalError("Could not decode \(file) file") + } + + return loadedData + } +} diff --git a/app/MetroMate Watch App/views/ContentView.swift b/app/MetroMate Watch App/views/ContentView.swift new file mode 100644 index 00000000..c3c51202 --- /dev/null +++ b/app/MetroMate Watch App/views/ContentView.swift @@ -0,0 +1,223 @@ +// +// ContentView.swift +// MetroMate Watch App +// +// Created by Kryštof Krátký on 31.03.2024. +// + +import os +import SwiftUI +import CoreLocation + + +struct ContentView: View { +// let logger = Logger(subsystem: "com.apple.liveUpdatesSample", category: "DemoView") +// @ObservedObject var locationsHandler = LocationsHandler.shared + + @ObservedObject var locationManager = LocationManager.shared + @State private var isLoading: Bool = true + @State private var station: Station? + @State private var allDepartures: [Departure] = [] + @State var selectedDeparture: Departure.ID? + + @State var selectedPlatform: Platform.ID? + + + + func populateStation() { + let userCoordinates = locationManager.userLocation?.coordinate + let lat = userCoordinates?.latitude + let lon = userCoordinates?.longitude + + if let lat = lat, let lon = lon { + station = getClosestStation(lat: lat, lon: lon) + } + } + + func populateDepartures(platformIDs: [String]) { + fetchDepartureBoardData(platformIDs: platformIDs) { result in + switch result { + case let .success(data): + allDepartures = data + isLoading = false + case let .failure(error): + print("Error: \(error)") + } + } + } + + func populateAll() { + populateStation() + + let platformIDs = station?.platforms.map { $0.gtfsId } + if let platformIDs = platformIDs { + populateDepartures(platformIDs: platformIDs) + } + } + +// var body: some View { +// VStack { +// Spacer() +// Text("Location: \(self.locationsHandler.lastLocation)") +// .padding(10) +// Text("Count: \(self.locationsHandler.count)") +// Text("isStationary:") +// Rectangle() +// .fill(self.locationsHandler.isStationary ? .green : .red) +// .frame(width: 100, height: 100, alignment: .center) +// Spacer() +// Button(self.locationsHandler.updatesStarted ? "Stop Location Updates" : "Start Location Updates") { +// self.locationsHandler.updatesStarted ? self.locationsHandler.stopLocationUpdates() : self.locationsHandler.startLocationUpdates() +// } +// .buttonStyle(.bordered) +// Button(self.locationsHandler.backgroundActivity ? "Stop BG Activity Session" : "Start BG Activity Session") { +// self.locationsHandler.backgroundActivity.toggle() +// } +// .buttonStyle(.bordered) +// } +// } + + var body: some View { + if locationManager.userLocation == nil { + RequestAccessToLocationView() + } else if isLoading { + LoadingView( + stationName: station?.name + ).onAppear { + populateAll() + + Timer.scheduledTimer( + withTimeInterval: 20, + repeats: true + ) { _ in + populateAll() + } + } + } else if allDepartures.count == 0 { + NoDeparturesView() + } else { + NavigationSplitView { + TabView { + let platformIDs = station?.platforms.map { $0.gtfsId } + let departures = platformIDs?.compactMap { platformID in + allDepartures.first { departureBoardRecord in + departureBoardRecord.stop.id == platformID + } + } + + List( station!.platforms, selection: $selectedPlatform) { platform in + let departure = allDepartures.first(where: { + $0.stop.id == platform.gtfsId + })! + + HStack { + Text(getShortenStationName(departure.trip.headsign)).fontWeight(.semibold) + Spacer() + CountdownView(countdownViewModel: CountdownViewModel(targetDateString: departure.departureTimestamp.predicted)) + } + .tag(platform.gtfsId) + .listItemTint( getLineColor(line: departure.route.shortName)) + } + .navigationTitle( + getShortenStationName(station?.name ?? String()) + ) + } + .toolbar { + ToolbarItem(placement: .topBarLeading) { NavigationLink { + SettingsView() + } label: { + Label( + "Settings", systemImage: "gear" + ) + } + } + } + } + detail: { + TabView { + let departureRecord = allDepartures.first { + $0.stop.id == selectedPlatform + } + + + if let departureRecord = departureRecord { + let filteredDepartures = allDepartures + .filter { + $0.stop.id == departureRecord.stop.id + } + + VStack { + Label( + getShortenStationName(departureRecord.trip.headsign), + systemImage: "arrowshape.right.fill" + ) + .font(.title2) + .padding(.bottom, 5) + CountdownView(countdownViewModel: CountdownViewModel(targetDateString: departureRecord.departureTimestamp.predicted)).font(.title) + HStack { + if filteredDepartures.count >= 2 { + Text("Also in") + CountdownView(countdownViewModel: CountdownViewModel(targetDateString: filteredDepartures[1].departureTimestamp.predicted)) + } + } + + }.containerBackground( + getLineColor(line: departureRecord.route.shortName).gradient, + for: .tabView + ) + + if filteredDepartures.count >= 2 { + List { + ForEach(allDepartures + .filter { + $0.stop.id == departureRecord.stop.id + }.dropFirst() + ) { + dep in + HStack { + Text(getShortenStationName(dep.trip.headsign)) + Spacer() + CountdownView(countdownViewModel: CountdownViewModel(targetDateString: dep.departureTimestamp.predicted)) + } + } + .containerBackground( + getLineColor( + line: departureRecord.route.shortName + ) + .gradient, + for: .tabView + ) + } + } + } + } + .tabViewStyle(.verticalPage) + .navigationTitle( + getShortenStationName( + station?.name ?? String() + ) + ) + .toolbar { + let departureRecord = allDepartures.first { + $0.id == selectedDeparture + } + + if let departureRecord = departureRecord { + ToolbarItem(placement: .topBarTrailing) { + Button {} label: { Label( + "Metro Line \(departureRecord.route.shortName)", systemImage: "\(departureRecord.route.shortName.lowercased()).circle" + ) } + .foregroundStyle( + .white + ) + } + } + } + } + } + } +} + +#Preview { + ContentView() +} diff --git a/app/MetroMate Watch App/views/CountdownView.swift b/app/MetroMate Watch App/views/CountdownView.swift new file mode 100644 index 00000000..3a63f3d2 --- /dev/null +++ b/app/MetroMate Watch App/views/CountdownView.swift @@ -0,0 +1,80 @@ +// +// CountdownView.swift +// MetroMate +// +// Created by Kryštof Krátký on 01.04.2024. +// + +import Foundation +import SwiftUI + +let SECONDS_IN_MINUTE = 60 +let SECONDS_IN_HOUR = 60 * SECONDS_IN_MINUTE + +func formatTime(seconds: Int) -> String { + if seconds < 0 { + return "\(seconds)s" + } + + let (h, m, s) = (seconds / SECONDS_IN_HOUR, (seconds % SECONDS_IN_HOUR) / SECONDS_IN_MINUTE, (seconds % SECONDS_IN_HOUR) % SECONDS_IN_MINUTE) + + var output: [String] = [] + + if h > 0 { + output.append("\(h)h") + } + if m > 0 { + output.append("\(m)m") + } + // don't show seconds if wait time is over 1 hour + if h == 0 && s > 0 { + output.append("\(s)s") + } + + return output.joined(separator: " ") +} + +struct CountdownView: View { + @StateObject var countdownViewModel: CountdownViewModel + + var body: some View { + VStack { + Text(formatTime(seconds: countdownViewModel.remainingTimeInSeconds)) + } + .onAppear { + countdownViewModel.startCountdown() + } + } +} + + let dateFormatter = ISO8601DateFormatter() + +class CountdownViewModel: ObservableObject { + @Published var remainingTimeInSeconds: Int = 0 + + private var timer: Timer? + + + private let targetDate: Date + + init(targetDateString: String) { + dateFormatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds] + guard let date = ISO8601DateFormatter().date(from: targetDateString) else { + fatalError("Invalid date format") + } + targetDate = date + } + + func startCountdown() { + timer = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { [weak self] _ in + guard let self = self else { return } + let currentDate = Date() + let remainingTime = Int(self.targetDate.timeIntervalSince(currentDate)) + self.remainingTimeInSeconds = remainingTime + } + } + + deinit { + timer?.invalidate() + } +} diff --git a/app/MetroMate Watch App/views/DetailView.swift b/app/MetroMate Watch App/views/DetailView.swift new file mode 100644 index 00000000..6eb37a51 --- /dev/null +++ b/app/MetroMate Watch App/views/DetailView.swift @@ -0,0 +1,74 @@ +// +// DetailView.swift +// MetroMate +// +// Created by Kryštof Krátký on 01.04.2024. +// + +import SwiftUI + +struct DetailView: View { + @State var allDepartures: [Departure] + @State var selectedDeparture: Departure.ID? + + + @State private var critical: Bool = false + + + var body: some View { + let departureRecord = allDepartures.first { + $0.id == selectedDeparture + } + + if let departureRecord = departureRecord { + let filteredDepartures = allDepartures + .filter { + $0.stop.id == departureRecord.stop.id + } + + VStack { + Label( + getShortenStationName(departureRecord.trip.headsign), + systemImage: "arrowshape.right.fill" + ) + .foregroundStyle(.secondary) + .font(.title3).bold() + CountdownView(countdownViewModel: CountdownViewModel(targetDateString: departureRecord.departureTimestamp.predicted)).font(.title) + HStack { + if filteredDepartures.count >= 2 { + Text("Also in") + CountdownView(countdownViewModel: CountdownViewModel(targetDateString: filteredDepartures[1].departureTimestamp.predicted)) + } } + .foregroundStyle(.tertiary) + } + .onAppear { + Timer.scheduledTimer(withTimeInterval: 0.25, repeats: true) { timer in + dateFormatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds] + guard let targetDate = ISO8601DateFormatter().date(from: departureRecord.departureTimestamp.predicted) else { + fatalError("Invalid date format") + } + + let currentDate = Date() + let remainingTime = Int(targetDate.timeIntervalSince(currentDate)) + + if remainingTime < 20 { + critical = !critical + } + else { + critical = false + } + + } + } + .containerBackground( + + (critical ? .black : + getLineColor(line: departureRecord.route.shortName)).gradient, + for: .tabView + ) + } + + } + +} + diff --git a/app/MetroMate Watch App/views/LoadingView.swift b/app/MetroMate Watch App/views/LoadingView.swift new file mode 100644 index 00000000..df3324da --- /dev/null +++ b/app/MetroMate Watch App/views/LoadingView.swift @@ -0,0 +1,34 @@ +// +// LoadingView.swift +// MetroMate +// +// Created by Kryštof Krátký on 31.03.2024. +// + +import SwiftUI + +struct LoadingView: View { + var stationName: String? + + var body: some View { + NavigationView { + ProgressView() + .toolbar { + ToolbarItem(placement: .topBarLeading) { NavigationLink { + SettingsView() + } label: { + Label( + "Settings", systemImage: "gear" + ) + } + .navigationTitle(getShortenStationName(stationName ?? String())) + } + } + } + } +} + +#Preview { + LoadingView( + stationName: "Hlavní nádraží") +} diff --git a/app/MetroMate Watch App/views/NoDeparturesView.swift b/app/MetroMate Watch App/views/NoDeparturesView.swift new file mode 100644 index 00000000..550dcfcc --- /dev/null +++ b/app/MetroMate Watch App/views/NoDeparturesView.swift @@ -0,0 +1,36 @@ +// +// NoDeparturesView.swift +// MetroMate +// +// Created by Kryštof Krátký on 01.04.2024. +// + +import SwiftUI + +struct NoDeparturesView: View { + @State var degreesRotating = 0.0 + + var body: some View { + VStack(spacing: 20) { + Image(systemName: "gauge.with.needle").rotationEffect(.degrees(degreesRotating)).font(.system(size: 50)) + .foregroundStyle(.tint) + .onAppear { + withAnimation( + .linear(duration: 1) + .speed(0.05).repeatForever(autoreverses: false) + ) { + degreesRotating = 360.0 + } + } + Text( + "Damn, that was the last one. But no worries, metro starts again at 5:00 😴") + .foregroundStyle(.tint) + .font(.caption) + .multilineTextAlignment(.center) + } + } +} + +#Preview { + NoDeparturesView() +} diff --git a/app/MetroMate Watch App/views/RequestAccessToLocationView.swift b/app/MetroMate Watch App/views/RequestAccessToLocationView.swift new file mode 100644 index 00000000..a5bc5c78 --- /dev/null +++ b/app/MetroMate Watch App/views/RequestAccessToLocationView.swift @@ -0,0 +1,33 @@ +// +// RequestAccessToLocationView.swift +// MetroMate +// +// Created by Kryštof Krátký on 31.03.2024. +// + +import SwiftUI + +let DESCRIPTION = "App needs access to your location to determine closest metro station" + +struct RequestAccessToLocationView: View { + @ObservedObject var locationManager = LocationManager.shared + + var body: some View { + VStack { + Text(DESCRIPTION).font(.caption2) + + Spacer().frame(height: 20) + + Button { + LocationManager.shared.requestLocation() + } label: { + Label("Allow", systemImage: "location.circle") + } + .tint(.blue) + } + } +} + +#Preview { + RequestAccessToLocationView() +} diff --git a/app/MetroMate Watch App/views/SettingsView.swift b/app/MetroMate Watch App/views/SettingsView.swift new file mode 100644 index 00000000..0f4d5dea --- /dev/null +++ b/app/MetroMate Watch App/views/SettingsView.swift @@ -0,0 +1,33 @@ +// +// SettingsView.swift +// MetroMate +// +// Created by Kryštof Krátký on 31.03.2024. +// + +import SwiftUI + +struct SettingsView: View { + // TODO: + + @State private var shortenNames = true + @State private var showLineNames = true + @State private var allowFlashingEffects = true + @State private var allowVibrations = true + @State private var allowSounds = false + + var body: some View { + List { + Toggle("Shorten station names", isOn: $shortenNames) + Toggle("Show line names", isOn: $showLineNames) + Toggle("Allow flashing effects", isOn: $allowFlashingEffects) + Toggle("Allow vibrations", isOn: $allowVibrations) + Toggle("Allow sounds", isOn: $allowSounds) + } + .navigationTitle("Settings") + } +} + +#Preview { + SettingsView() +} diff --git a/app/MetroMate.xcodeproj/project.pbxproj b/app/MetroMate.xcodeproj/project.pbxproj new file mode 100644 index 00000000..9e70cb70 --- /dev/null +++ b/app/MetroMate.xcodeproj/project.pbxproj @@ -0,0 +1,552 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 56; + objects = { + +/* Begin PBXBuildFile section */ + 2D27FFB22BBB200F0040EF9A /* DetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D27FFB12BBB200F0040EF9A /* DetailView.swift */; }; + 2D29A9962BB9FB7D00DB0F7E /* Metro Now.app in Embed Watch Content */ = {isa = PBXBuildFile; fileRef = 2D29A9952BB9FB7D00DB0F7E /* Metro Now.app */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; + 2D29A99B2BB9FB7D00DB0F7E /* MetroMateApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D29A99A2BB9FB7D00DB0F7E /* MetroMateApp.swift */; }; + 2D29A99D2BB9FB7D00DB0F7E /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D29A99C2BB9FB7D00DB0F7E /* ContentView.swift */; }; + 2D29A99F2BB9FB7E00DB0F7E /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 2D29A99E2BB9FB7E00DB0F7E /* Assets.xcassets */; }; + 2D29A9A22BB9FB7E00DB0F7E /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 2D29A9A12BB9FB7E00DB0F7E /* Preview Assets.xcassets */; }; + 2D29A9AF2BB9FC3600DB0F7E /* stations.json in Resources */ = {isa = PBXBuildFile; fileRef = 2D29A9AE2BB9FC3600DB0F7E /* stations.json */; }; + 2D29A9B02BB9FC3600DB0F7E /* stations.json in Resources */ = {isa = PBXBuildFile; fileRef = 2D29A9AE2BB9FC3600DB0F7E /* stations.json */; }; + 2D29A9B62BB9FE6300DB0F7E /* stations.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D29A9B52BB9FE6300DB0F7E /* stations.swift */; }; + 2D29A9B82BB9FEE200DB0F7E /* lines.utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D29A9B72BB9FEE200DB0F7E /* lines.utils.swift */; }; + 2D29A9BA2BBA01A000DB0F7E /* name.utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D29A9B92BBA01A000DB0F7E /* name.utils.swift */; }; + 2D29A9BD2BBA053A00DB0F7E /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D29A9BC2BBA053A00DB0F7E /* SettingsView.swift */; }; + 2D29A9C12BBA09AF00DB0F7E /* location.manager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D29A9C02BBA09AF00DB0F7E /* location.manager.swift */; }; + 2D29A9C32BBA0A7C00DB0F7E /* RequestAccessToLocationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D29A9C22BBA0A7C00DB0F7E /* RequestAccessToLocationView.swift */; }; + 2D29A9C52BBA0EBD00DB0F7E /* LoadingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D29A9C42BBA0EBD00DB0F7E /* LoadingView.swift */; }; + 2D29A9C72BBA167B00DB0F7E /* location.utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D29A9C62BBA167B00DB0F7E /* location.utils.swift */; }; + 2D29A9CB2BBA1A7D00DB0F7E /* departure.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D29A9CA2BBA1A7D00DB0F7E /* departure.swift */; }; + 2D29A9CD2BBA1B9900DB0F7E /* env.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D29A9CC2BBA1B9900DB0F7E /* env.swift */; }; + 2D29A9CF2BBA1D7F00DB0F7E /* fetch-departures.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D29A9CE2BBA1D7F00DB0F7E /* fetch-departures.swift */; }; + 2D29A9D32BBA26F300DB0F7E /* NoDeparturesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D29A9D22BBA26F300DB0F7E /* NoDeparturesView.swift */; }; + 2D29A9D52BBA2CC400DB0F7E /* CountdownView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D29A9D42BBA2CC400DB0F7E /* CountdownView.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 2D29A9972BB9FB7D00DB0F7E /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 2D29A9892BB9FB7D00DB0F7E /* Project object */; + proxyType = 1; + remoteGlobalIDString = 2D29A9942BB9FB7D00DB0F7E; + remoteInfo = "MetroMate Watch App"; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 2D29A9A82BB9FB7E00DB0F7E /* Embed Watch Content */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = "$(CONTENTS_FOLDER_PATH)/Watch"; + dstSubfolderSpec = 16; + files = ( + 2D29A9962BB9FB7D00DB0F7E /* Metro Now.app in Embed Watch Content */, + ); + name = "Embed Watch Content"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 2D27FFB12BBB200F0040EF9A /* DetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetailView.swift; sourceTree = ""; }; + 2D29A98F2BB9FB7D00DB0F7E /* MetroMate.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = MetroMate.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 2D29A9952BB9FB7D00DB0F7E /* Metro Now.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Metro Now.app"; sourceTree = BUILT_PRODUCTS_DIR; }; + 2D29A99A2BB9FB7D00DB0F7E /* MetroMateApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MetroMateApp.swift; sourceTree = ""; }; + 2D29A99C2BB9FB7D00DB0F7E /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; + 2D29A99E2BB9FB7E00DB0F7E /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 2D29A9A12BB9FB7E00DB0F7E /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; + 2D29A9AE2BB9FC3600DB0F7E /* stations.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = stations.json; sourceTree = ""; }; + 2D29A9B52BB9FE6300DB0F7E /* stations.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = stations.swift; sourceTree = ""; }; + 2D29A9B72BB9FEE200DB0F7E /* lines.utils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = lines.utils.swift; sourceTree = ""; }; + 2D29A9B92BBA01A000DB0F7E /* name.utils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = name.utils.swift; sourceTree = ""; }; + 2D29A9BC2BBA053A00DB0F7E /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = ""; }; + 2D29A9C02BBA09AF00DB0F7E /* location.manager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = location.manager.swift; sourceTree = ""; }; + 2D29A9C22BBA0A7C00DB0F7E /* RequestAccessToLocationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RequestAccessToLocationView.swift; sourceTree = ""; }; + 2D29A9C42BBA0EBD00DB0F7E /* LoadingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingView.swift; sourceTree = ""; }; + 2D29A9C62BBA167B00DB0F7E /* location.utils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = location.utils.swift; sourceTree = ""; }; + 2D29A9CA2BBA1A7D00DB0F7E /* departure.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = departure.swift; sourceTree = ""; }; + 2D29A9CC2BBA1B9900DB0F7E /* env.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = env.swift; sourceTree = ""; }; + 2D29A9CE2BBA1D7F00DB0F7E /* fetch-departures.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "fetch-departures.swift"; sourceTree = ""; }; + 2D29A9D22BBA26F300DB0F7E /* NoDeparturesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoDeparturesView.swift; sourceTree = ""; }; + 2D29A9D42BBA2CC400DB0F7E /* CountdownView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CountdownView.swift; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 2D29A9922BB9FB7D00DB0F7E /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 2D29A9882BB9FB7D00DB0F7E = { + isa = PBXGroup; + children = ( + 2D29A9CC2BBA1B9900DB0F7E /* env.swift */, + 2D29A9992BB9FB7D00DB0F7E /* MetroMate Watch App */, + 2D29A9902BB9FB7D00DB0F7E /* Products */, + ); + sourceTree = ""; + }; + 2D29A9902BB9FB7D00DB0F7E /* Products */ = { + isa = PBXGroup; + children = ( + 2D29A98F2BB9FB7D00DB0F7E /* MetroMate.app */, + 2D29A9952BB9FB7D00DB0F7E /* Metro Now.app */, + ); + name = Products; + sourceTree = ""; + }; + 2D29A9992BB9FB7D00DB0F7E /* MetroMate Watch App */ = { + isa = PBXGroup; + children = ( + 2D29A99A2BB9FB7D00DB0F7E /* MetroMateApp.swift */, + 2D29A9C92BBA1A4900DB0F7E /* departures */, + 2D29A9BB2BBA050D00DB0F7E /* views */, + 2D29A9C82BBA169200DB0F7E /* location */, + 2D29A9B12BB9FDDC00DB0F7E /* stations */, + 2D29A9B72BB9FEE200DB0F7E /* lines.utils.swift */, + 2D29A9B92BBA01A000DB0F7E /* name.utils.swift */, + 2D29A99E2BB9FB7E00DB0F7E /* Assets.xcassets */, + 2D29A9A02BB9FB7E00DB0F7E /* Preview Content */, + ); + path = "MetroMate Watch App"; + sourceTree = ""; + }; + 2D29A9A02BB9FB7E00DB0F7E /* Preview Content */ = { + isa = PBXGroup; + children = ( + 2D29A9A12BB9FB7E00DB0F7E /* Preview Assets.xcassets */, + ); + path = "Preview Content"; + sourceTree = ""; + }; + 2D29A9B12BB9FDDC00DB0F7E /* stations */ = { + isa = PBXGroup; + children = ( + 2D29A9AE2BB9FC3600DB0F7E /* stations.json */, + 2D29A9B52BB9FE6300DB0F7E /* stations.swift */, + ); + path = stations; + sourceTree = ""; + }; + 2D29A9BB2BBA050D00DB0F7E /* views */ = { + isa = PBXGroup; + children = ( + 2D29A9C42BBA0EBD00DB0F7E /* LoadingView.swift */, + 2D29A99C2BB9FB7D00DB0F7E /* ContentView.swift */, + 2D27FFB12BBB200F0040EF9A /* DetailView.swift */, + 2D29A9D42BBA2CC400DB0F7E /* CountdownView.swift */, + 2D29A9D22BBA26F300DB0F7E /* NoDeparturesView.swift */, + 2D29A9BC2BBA053A00DB0F7E /* SettingsView.swift */, + 2D29A9C22BBA0A7C00DB0F7E /* RequestAccessToLocationView.swift */, + ); + path = views; + sourceTree = ""; + }; + 2D29A9C82BBA169200DB0F7E /* location */ = { + isa = PBXGroup; + children = ( + 2D29A9C02BBA09AF00DB0F7E /* location.manager.swift */, + 2D29A9C62BBA167B00DB0F7E /* location.utils.swift */, + ); + path = location; + sourceTree = ""; + }; + 2D29A9C92BBA1A4900DB0F7E /* departures */ = { + isa = PBXGroup; + children = ( + 2D29A9CA2BBA1A7D00DB0F7E /* departure.swift */, + 2D29A9CE2BBA1D7F00DB0F7E /* fetch-departures.swift */, + ); + path = departures; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 2D29A98E2BB9FB7D00DB0F7E /* MetroMate */ = { + isa = PBXNativeTarget; + buildConfigurationList = 2D29A9A92BB9FB7E00DB0F7E /* Build configuration list for PBXNativeTarget "MetroMate" */; + buildPhases = ( + 2D29A98D2BB9FB7D00DB0F7E /* Resources */, + 2D29A9A82BB9FB7E00DB0F7E /* Embed Watch Content */, + ); + buildRules = ( + ); + dependencies = ( + 2D29A9982BB9FB7D00DB0F7E /* PBXTargetDependency */, + ); + name = MetroMate; + productName = MetroMate; + productReference = 2D29A98F2BB9FB7D00DB0F7E /* MetroMate.app */; + productType = "com.apple.product-type.application.watchapp2-container"; + }; + 2D29A9942BB9FB7D00DB0F7E /* MetroMate Watch App */ = { + isa = PBXNativeTarget; + buildConfigurationList = 2D29A9A52BB9FB7E00DB0F7E /* Build configuration list for PBXNativeTarget "MetroMate Watch App" */; + buildPhases = ( + 2D29A9912BB9FB7D00DB0F7E /* Sources */, + 2D29A9922BB9FB7D00DB0F7E /* Frameworks */, + 2D29A9932BB9FB7D00DB0F7E /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = "MetroMate Watch App"; + productName = "MetroMate Watch App"; + productReference = 2D29A9952BB9FB7D00DB0F7E /* Metro Now.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 2D29A9892BB9FB7D00DB0F7E /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 1530; + LastUpgradeCheck = 1530; + TargetAttributes = { + 2D29A98E2BB9FB7D00DB0F7E = { + CreatedOnToolsVersion = 15.3; + LastSwiftMigration = 1530; + }; + 2D29A9942BB9FB7D00DB0F7E = { + CreatedOnToolsVersion = 15.3; + }; + }; + }; + buildConfigurationList = 2D29A98C2BB9FB7D00DB0F7E /* Build configuration list for PBXProject "MetroMate" */; + compatibilityVersion = "Xcode 14.0"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 2D29A9882BB9FB7D00DB0F7E; + productRefGroup = 2D29A9902BB9FB7D00DB0F7E /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 2D29A98E2BB9FB7D00DB0F7E /* MetroMate */, + 2D29A9942BB9FB7D00DB0F7E /* MetroMate Watch App */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 2D29A98D2BB9FB7D00DB0F7E /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 2D29A9AF2BB9FC3600DB0F7E /* stations.json in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 2D29A9932BB9FB7D00DB0F7E /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 2D29A9A22BB9FB7E00DB0F7E /* Preview Assets.xcassets in Resources */, + 2D29A99F2BB9FB7E00DB0F7E /* Assets.xcassets in Resources */, + 2D29A9B02BB9FC3600DB0F7E /* stations.json in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 2D29A9912BB9FB7D00DB0F7E /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 2D29A9C12BBA09AF00DB0F7E /* location.manager.swift in Sources */, + 2D29A99D2BB9FB7D00DB0F7E /* ContentView.swift in Sources */, + 2D29A9C32BBA0A7C00DB0F7E /* RequestAccessToLocationView.swift in Sources */, + 2D29A9D32BBA26F300DB0F7E /* NoDeparturesView.swift in Sources */, + 2D29A9CF2BBA1D7F00DB0F7E /* fetch-departures.swift in Sources */, + 2D29A9B82BB9FEE200DB0F7E /* lines.utils.swift in Sources */, + 2D29A9D52BBA2CC400DB0F7E /* CountdownView.swift in Sources */, + 2D29A9CB2BBA1A7D00DB0F7E /* departure.swift in Sources */, + 2D29A9CD2BBA1B9900DB0F7E /* env.swift in Sources */, + 2D27FFB22BBB200F0040EF9A /* DetailView.swift in Sources */, + 2D29A9BA2BBA01A000DB0F7E /* name.utils.swift in Sources */, + 2D29A9C72BBA167B00DB0F7E /* location.utils.swift in Sources */, + 2D29A9BD2BBA053A00DB0F7E /* SettingsView.swift in Sources */, + 2D29A9B62BB9FE6300DB0F7E /* stations.swift in Sources */, + 2D29A99B2BB9FB7D00DB0F7E /* MetroMateApp.swift in Sources */, + 2D29A9C52BBA0EBD00DB0F7E /* LoadingView.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 2D29A9982BB9FB7D00DB0F7E /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 2D29A9942BB9FB7D00DB0F7E /* MetroMate Watch App */; + targetProxy = 2D29A9972BB9FB7D00DB0F7E /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin XCBuildConfiguration section */ + 2D29A9A32BB9FB7E00DB0F7E /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 2D29A9A42BB9FB7E00DB0F7E /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SWIFT_COMPILATION_MODE = wholemodule; + }; + name = Release; + }; + 2D29A9A62BB9FB7E00DB0F7E /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_ASSET_PATHS = "\"MetroMate Watch App/Preview Content\""; + DEVELOPMENT_TEAM = R6WU5ABNG2; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "Location is used to determine closest metro station"; + INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; + INFOPLIST_KEY_WKWatchOnly = YES; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.krystof.metronow.watchkitapp; + PRODUCT_NAME = "Metro Now"; + SDKROOT = watchos; + SKIP_INSTALL = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = 4; + WATCHOS_DEPLOYMENT_TARGET = 10.4; + }; + name = Debug; + }; + 2D29A9A72BB9FB7E00DB0F7E /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_ASSET_PATHS = "\"MetroMate Watch App/Preview Content\""; + DEVELOPMENT_TEAM = R6WU5ABNG2; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "Location is used to determine closest metro station"; + INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; + INFOPLIST_KEY_WKWatchOnly = YES; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.krystof.metronow.watchkitapp; + PRODUCT_NAME = "Metro Now"; + SDKROOT = watchos; + SKIP_INSTALL = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = 4; + VALIDATE_PRODUCT = YES; + WATCHOS_DEPLOYMENT_TARGET = 10.4; + }; + name = Release; + }; + 2D29A9AA2BB9FB7E00DB0F7E /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = R6WU5ABNG2; + INFOPLIST_KEY_CFBundleDisplayName = MetroMate; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.krystof.metronow; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + SWIFT_OBJC_BRIDGING_HEADER = "MetroMate Watch App/stations/MetroMate-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + 2D29A9AB2BB9FB7E00DB0F7E /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = R6WU5ABNG2; + INFOPLIST_KEY_CFBundleDisplayName = MetroMate; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.krystof.metronow; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + SWIFT_OBJC_BRIDGING_HEADER = "MetroMate Watch App/stations/MetroMate-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 2D29A98C2BB9FB7D00DB0F7E /* Build configuration list for PBXProject "MetroMate" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 2D29A9A32BB9FB7E00DB0F7E /* Debug */, + 2D29A9A42BB9FB7E00DB0F7E /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 2D29A9A52BB9FB7E00DB0F7E /* Build configuration list for PBXNativeTarget "MetroMate Watch App" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 2D29A9A62BB9FB7E00DB0F7E /* Debug */, + 2D29A9A72BB9FB7E00DB0F7E /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 2D29A9A92BB9FB7E00DB0F7E /* Build configuration list for PBXNativeTarget "MetroMate" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 2D29A9AA2BB9FB7E00DB0F7E /* Debug */, + 2D29A9AB2BB9FB7E00DB0F7E /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 2D29A9892BB9FB7D00DB0F7E /* Project object */; +} diff --git a/app/MetroMate.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/app/MetroMate.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 00000000..919434a6 --- /dev/null +++ b/app/MetroMate.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/app/MetroMate.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/app/MetroMate.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 00000000..18d98100 --- /dev/null +++ b/app/MetroMate.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/app/env.example.swift b/app/env.example.swift new file mode 100644 index 00000000..8f94ec8e --- /dev/null +++ b/app/env.example.swift @@ -0,0 +1,11 @@ +// +// env.swift +// MetroMate Watch App +// +// Created by Kryštof Krátký on 01.04.2024. +// + +import Foundation + +// get yours @ https://api.golemio.cz/api-keys/auth/sign-up +let GOLEMIO_API_KEY = ""