├── CODEOWNERS
├── api
├── .gitignore
└── src
│ ├── commonMain
│ └── kotlin
│ │ └── com
│ │ └── sixbynine
│ │ └── transit
│ │ └── path
│ │ ├── api
│ │ ├── NetworkException.kt
│ │ ├── PathApiException.kt
│ │ ├── Coordinates.kt
│ │ ├── StationChoice.kt
│ │ ├── HttpClientFactory.kt
│ │ ├── schedule
│ │ │ ├── GithubSchedules.kt
│ │ │ └── GithubScheduleRepository.kt
│ │ ├── alerts
│ │ │ ├── github
│ │ │ │ ├── GithubAlerts.kt
│ │ │ │ └── GithubAlertsRepository.kt
│ │ │ ├── everbridge
│ │ │ │ └── EverbridgeAlertsRepository.kt
│ │ │ └── AlertsRepository.kt
│ │ ├── UpcomingDepartures.kt
│ │ ├── Line.kt
│ │ ├── PathApi.kt
│ │ ├── Route.kt
│ │ ├── DepartingTrain.kt
│ │ ├── State.kt
│ │ └── templine
│ │ │ ├── HobClosureConfigRepository.kt
│ │ │ └── HobClosureConfig.kt
│ │ ├── location
│ │ └── Location.kt
│ │ ├── util
│ │ ├── TestRemoteFileProvider.kt
│ │ ├── Serialization.kt
│ │ ├── RemoteFileReading.kt
│ │ ├── RemoteFileRepository.kt
│ │ └── DataSource.kt
│ │ └── model
│ │ ├── Season.kt
│ │ └── Colors.kt
│ ├── jvmMain
│ └── kotlin
│ │ └── com
│ │ └── sixbynine
│ │ └── transit
│ │ └── path
│ │ └── api
│ │ └── HttpClientFactory.jvm.kt
│ ├── iosMain
│ └── kotlin
│ │ └── com
│ │ └── sixbynine
│ │ └── transit
│ │ └── path
│ │ └── api
│ │ └── IosHttpClientFactory.kt
│ ├── commonTest
│ └── kotlin
│ │ └── com
│ │ └── sixbynine
│ │ └── transit
│ │ └── path
│ │ ├── HobClosureRepoTest.kt
│ │ └── api
│ │ └── StationsTest.kt
│ ├── androidMain
│ └── kotlin
│ │ └── com
│ │ └── sixbynine
│ │ └── transit
│ │ └── path
│ │ └── api
│ │ └── AndroidHttpClientFactory.kt
│ └── androidUnitTest
│ └── resources
│ └── com
│ └── sixbynine
│ └── transit
│ └── path
│ └── everbridge_alert_jan18.json
├── flipper
├── .gitignore
├── src
│ ├── androidRelease
│ │ └── kotlin
│ │ │ └── com
│ │ │ └── sixbynine
│ │ │ └── transit
│ │ │ └── path
│ │ │ └── flipper
│ │ │ └── FlipperUtil.kt
│ └── androidDebug
│ │ └── kotlin
│ │ └── com
│ │ └── sixbynine
│ │ └── transit
│ │ └── path
│ │ └── flipper
│ │ └── FlipperUtil.kt
└── build.gradle.kts
├── logging
├── .gitignore
├── src
│ └── commonMain
│ │ └── kotlin
│ │ └── com
│ │ └── sixbynine
│ │ └── transit
│ │ └── path
│ │ └── NonFatalReporter.kt
└── build.gradle.kts
├── platform
├── .gitignore
├── src
│ ├── commonMain
│ │ └── kotlin
│ │ │ └── com
│ │ │ └── sixbynine
│ │ │ └── transit
│ │ │ └── path
│ │ │ ├── util
│ │ │ ├── IsTest.kt
│ │ │ ├── Staleness.kt
│ │ │ ├── TimestampedValue.kt
│ │ │ ├── CoroutineScopes.kt
│ │ │ ├── AgedValue.kt
│ │ │ ├── NonEmptyList.kt
│ │ │ └── GlobalExtensions.kt
│ │ │ ├── platform
│ │ │ └── Platform.kt
│ │ │ ├── network
│ │ │ └── NetworkManager.kt
│ │ │ ├── time
│ │ │ ├── UserPreferenceDayOfWeekComparator.kt
│ │ │ └── TimeUtils.kt
│ │ │ └── preferences
│ │ │ └── IntPersistable.kt
│ ├── jvmMain
│ │ └── kotlin
│ │ │ └── com
│ │ │ └── sixbynine
│ │ │ └── transit
│ │ │ └── path
│ │ │ ├── platform
│ │ │ └── Platform.jvm.kt
│ │ │ ├── preferences
│ │ │ └── Preferences.jvm.kt
│ │ │ ├── network
│ │ │ └── NetworkManager.jvm.kt
│ │ │ ├── time
│ │ │ └── TimeUtils.jvm.kt
│ │ │ └── util
│ │ │ └── GlobalDataStore.jvm.kt
│ ├── iosMain
│ │ └── kotlin
│ │ │ └── com
│ │ │ └── sixbynine
│ │ │ └── transit
│ │ │ └── path
│ │ │ ├── platform
│ │ │ └── Platform.ios.kt
│ │ │ ├── network
│ │ │ └── IosNetworkManager.kt
│ │ │ ├── time
│ │ │ └── NativeTimeUtils.kt
│ │ │ └── util
│ │ │ └── GlobalDataStore.ios.kt
│ ├── androidMain
│ │ ├── kotlin
│ │ │ └── com
│ │ │ │ └── sixbynine
│ │ │ │ └── transit
│ │ │ │ └── path
│ │ │ │ ├── platform
│ │ │ │ └── Platform.android.kt
│ │ │ │ ├── PreviewContext.kt
│ │ │ │ ├── PathApplication.kt
│ │ │ │ ├── time
│ │ │ │ └── AndroidTimeUtils.kt
│ │ │ │ └── network
│ │ │ │ └── AndroidNetworkManager.kt
│ │ └── AndroidManifest.xml
│ └── commonTest
│ │ └── kotlin
│ │ └── com
│ │ └── sixbynine
│ │ └── transit
│ │ └── path
│ │ └── time
│ │ └── UserPreferenceDayOfWeekComparatorTest.kt
└── build.gradle.kts
├── schedule
├── .gitignore
├── src
│ ├── commonMain
│ │ └── kotlin
│ │ │ └── com
│ │ │ └── sixbynine
│ │ │ └── transit
│ │ │ └── path
│ │ │ └── schedule
│ │ │ └── Schedule.kt
│ └── commonTest
│ │ └── kotlin
│ │ ├── Departures.kt
│ │ └── DeparturesParsing.kt
├── generator
│ ├── src
│ │ ├── test
│ │ │ └── kotlin
│ │ │ │ └── com
│ │ │ │ └── sixbynine
│ │ │ │ └── transit
│ │ │ │ └── path
│ │ │ │ └── schedule
│ │ │ │ └── generator
│ │ │ │ ├── ScheduleParserTest.kt
│ │ │ │ └── ScheduleHtmlParserTest.kt
│ │ └── main
│ │ │ └── kotlin
│ │ │ └── com
│ │ │ └── sixbynine
│ │ │ └── transit
│ │ │ └── path
│ │ │ └── schedule
│ │ │ └── generator
│ │ │ └── ScheduleGenerator.kt
│ └── build.gradle.kts
└── build.gradle.kts
├── schedule-generator
├── .gitignore
└── requirements.txt
├── composeApp
├── .gitignore
├── path_app_icon.icns
├── path_app_icon.ico
└── src
│ ├── androidMain
│ ├── res
│ │ ├── values-es
│ │ │ └── strings.xml
│ │ ├── mipmap-hdpi
│ │ │ ├── ic_launcher.png
│ │ │ └── ic_launcher_round.png
│ │ ├── mipmap-mdpi
│ │ │ ├── ic_launcher.png
│ │ │ └── ic_launcher_round.png
│ │ ├── mipmap-xhdpi
│ │ │ ├── ic_launcher.png
│ │ │ └── ic_launcher_round.png
│ │ ├── xml
│ │ │ ├── file_paths.xml
│ │ │ ├── locales_config.xml
│ │ │ └── departure_widget.xml
│ │ ├── mipmap-xxhdpi
│ │ │ ├── ic_launcher.png
│ │ │ └── ic_launcher_round.png
│ │ ├── mipmap-xxxhdpi
│ │ │ ├── ic_launcher.png
│ │ │ └── ic_launcher_round.png
│ │ ├── values
│ │ │ ├── strings.xml
│ │ │ ├── styles.xml
│ │ │ ├── colors.xml
│ │ │ └── themes.xml
│ │ ├── drawable-nodpi
│ │ │ └── widget_preview.png
│ │ ├── values-en
│ │ │ └── widget_strings.xml
│ │ ├── values-night
│ │ │ ├── themes.xml
│ │ │ └── colors.xml
│ │ ├── drawable
│ │ │ ├── ic_edit_inset.xml
│ │ │ ├── ic_refresh_inset.xml
│ │ │ ├── ic_warning_inset.xml
│ │ │ ├── circle.xml
│ │ │ ├── preview_circle_hob.xml
│ │ │ ├── preview_circle_nwk.xml
│ │ │ ├── circle_border.xml
│ │ │ ├── circle_right.xml
│ │ │ ├── ic_filter.xml
│ │ │ ├── ic_sort.xml
│ │ │ ├── ic_arrow_up.xml
│ │ │ ├── ic_arrow_down.xml
│ │ │ ├── ic_down.xml
│ │ │ ├── ic_open_in_new.xml
│ │ │ ├── ic_station.xml
│ │ │ ├── ic_warning.xml
│ │ │ ├── ic_edit.xml
│ │ │ ├── ic_refresh.xml
│ │ │ ├── ic_one_column.xml
│ │ │ └── ic_launcher_foreground.xml
│ │ ├── mipmap-anydpi-v26
│ │ │ ├── ic_launcher.xml
│ │ │ └── ic_launcher_round.xml
│ │ ├── layout
│ │ │ └── widget_loading.xml
│ │ └── xml-v27
│ │ │ └── departure_widget.xml
│ ├── kotlin
│ │ ├── Platform.android.kt
│ │ └── com
│ │ │ └── sixbynine
│ │ │ └── transit
│ │ │ └── path
│ │ │ ├── widget
│ │ │ ├── ui
│ │ │ │ └── WidgetState.kt
│ │ │ ├── UpdateWidgetAction.kt
│ │ │ ├── glance
│ │ │ │ ├── GlanceResources.kt
│ │ │ │ ├── Text.kt
│ │ │ │ ├── Theme.kt
│ │ │ │ ├── ImageButton.kt
│ │ │ │ └── Typography.kt
│ │ │ ├── StationDataComparator.kt
│ │ │ ├── configuration
│ │ │ │ └── StoredWidgetConfiguration.kt
│ │ │ ├── StartConfigurationActivityAction.kt
│ │ │ ├── GlanceExtensions.kt
│ │ │ └── WidgetMeasurements.kt
│ │ │ ├── app
│ │ │ ├── ui
│ │ │ │ ├── setup
│ │ │ │ │ └── SetupScreenPreview.kt
│ │ │ │ ├── advancessettings
│ │ │ │ │ └── AdvancedSettingsPreview.kt
│ │ │ │ └── settings
│ │ │ │ │ └── SettingsScreenPreview.kt
│ │ │ └── settings
│ │ │ │ └── DevOptionsExport.android.kt
│ │ │ ├── analytics
│ │ │ └── AndroidAnalytics.kt
│ │ │ ├── BaseActivity.kt
│ │ │ ├── MainActivity.kt
│ │ │ └── MobilePathApplication.kt
│ └── .gitignore
│ ├── commonMain
│ ├── kotlin
│ │ ├── com
│ │ │ └── sixbynine
│ │ │ │ └── transit
│ │ │ │ └── path
│ │ │ │ ├── api
│ │ │ │ ├── impl
│ │ │ │ │ └── GithubAlertHelper.kt
│ │ │ │ ├── LocationSetting.kt
│ │ │ │ ├── StationSort.kt
│ │ │ │ └── LineExtensions.kt
│ │ │ │ ├── app
│ │ │ │ ├── ui
│ │ │ │ │ ├── SizeWrapper.kt
│ │ │ │ │ ├── sheet
│ │ │ │ │ │ └── BottomSheetTitle.kt
│ │ │ │ │ ├── FontInfo.kt
│ │ │ │ │ ├── AppUiScope.kt
│ │ │ │ │ ├── theme
│ │ │ │ │ │ └── Dimensions.kt
│ │ │ │ │ ├── setup
│ │ │ │ │ │ ├── SetupScreenContract.kt
│ │ │ │ │ │ └── SetupScreenViewModel.kt
│ │ │ │ │ ├── layout
│ │ │ │ │ │ └── LayoutOption.kt
│ │ │ │ │ ├── common
│ │ │ │ │ │ └── AppUiTrainData.kt
│ │ │ │ │ ├── ColorRect.kt
│ │ │ │ │ ├── station
│ │ │ │ │ │ └── StationContract.kt
│ │ │ │ │ ├── BaseViewModel.kt
│ │ │ │ │ ├── settings
│ │ │ │ │ │ ├── StationLimitBottomSheet.kt
│ │ │ │ │ │ ├── TrainFilterBottomSheet.kt
│ │ │ │ │ │ ├── TimeDisplayBottomSheet.kt
│ │ │ │ │ │ ├── LineFilterBottomSheet.kt
│ │ │ │ │ │ └── StationSortBottomSheet.kt
│ │ │ │ │ ├── home
│ │ │ │ │ │ └── TrainLine.kt
│ │ │ │ │ ├── PathViewModel.kt
│ │ │ │ │ ├── ViewModelScreen.kt
│ │ │ │ │ └── icon
│ │ │ │ │ │ └── NativeIcon.kt
│ │ │ │ ├── settings
│ │ │ │ │ ├── DevOptionsExport.kt
│ │ │ │ │ ├── TimeDisplay.kt
│ │ │ │ │ ├── AvoidMissingTrains.kt
│ │ │ │ │ ├── StationLimit.kt
│ │ │ │ │ ├── AppSettings.kt
│ │ │ │ │ └── CommutingConfiguration.kt
│ │ │ │ ├── station
│ │ │ │ │ └── StationSelection.kt
│ │ │ │ ├── lifecycle
│ │ │ │ │ └── AppLifecycleObserver.kt
│ │ │ │ └── external
│ │ │ │ │ └── ExternalRoutingManager.kt
│ │ │ │ ├── widget
│ │ │ │ ├── WidgetReloader.kt
│ │ │ │ ├── StationByDisplayNameComparator.kt
│ │ │ │ └── PathWidgetConfiguration.kt
│ │ │ │ ├── util
│ │ │ │ ├── ComposeExtensions.kt
│ │ │ │ ├── CollectionExtensions.kt
│ │ │ │ ├── TimeUtilities.kt
│ │ │ │ └── Localization.kt
│ │ │ │ └── native
│ │ │ │ └── NativeHolder.kt
│ │ ├── Greeting.kt
│ │ └── Platform.kt
│ └── composeResources
│ │ └── drawable
│ │ └── train_track.png
│ ├── iosMain
│ └── kotlin
│ │ ├── MainViewController.kt
│ │ ├── com
│ │ └── sixbynine
│ │ │ └── transit
│ │ │ └── path
│ │ │ └── analytics
│ │ │ └── IosAnalytics.kt
│ │ └── Platform.ios.kt
│ ├── desktopMain
│ ├── kotlin
│ │ ├── com
│ │ │ └── sixbynine
│ │ │ │ └── transit
│ │ │ │ └── path
│ │ │ │ ├── app
│ │ │ │ ├── settings
│ │ │ │ │ └── DevOptionsExport.jvm.kt
│ │ │ │ └── external
│ │ │ │ │ └── ExternalRoutingManager.jvm.kt
│ │ │ │ ├── analytics
│ │ │ │ └── Analytics.jvm.kt
│ │ │ │ └── location
│ │ │ │ └── LocationProvider.jvm.kt
│ │ ├── Platform.jvm.kt
│ │ └── main.kt
│ └── composeResources
│ │ └── drawable
│ │ ├── ic_filter.xml
│ │ ├── ic_sort.xml
│ │ ├── ic_arrow_up.xml
│ │ ├── ic_arrow_down.xml
│ │ ├── ic_down.xml
│ │ ├── ic_open_in_new.xml
│ │ ├── ic_station.xml
│ │ └── ic_one_column.xml
│ └── commonTest
│ └── kotlin
│ └── com
│ └── sixbynine
│ └── transit
│ └── path
│ ├── app
│ └── ui
│ │ └── ColorsTest.kt
│ └── api
│ └── impl
│ └── CheckpointMapTest.kt
├── .fleet
├── settings.json
└── receipt.json
├── iosApp
├── Configuration
│ └── Config.xcconfig
├── iosApp
│ ├── Assets.xcassets
│ │ ├── Contents.json
│ │ └── AppIcon.appiconset
│ │ │ ├── path_app_icon.png
│ │ │ └── Contents.json
│ ├── Media.xcassets
│ │ ├── Contents.json
│ │ └── AppIcon.appiconset
│ │ │ ├── path_app_icon.png
│ │ │ └── Contents.json
│ ├── LegacyAssets.xcassets
│ │ ├── Contents.json
│ │ ├── AppIcon.appiconset
│ │ │ ├── path_app_icon.png
│ │ │ └── Contents.json
│ │ └── AccentColor.colorset
│ │ │ └── Contents.json
│ ├── LegacyAssets copy.xcassets
│ │ ├── Contents.json
│ │ ├── AppIcon.appiconset
│ │ │ ├── path_app_icon.png
│ │ │ └── Contents.json
│ │ └── AccentColor.colorset
│ │ │ └── Contents.json
│ ├── Preview Content
│ │ └── Preview Assets.xcassets
│ │ │ ├── Contents.json
│ │ │ └── AppIcon.appiconset
│ │ │ ├── path_app_icon.png
│ │ │ └── Contents.json
│ ├── Localizable.strings
│ ├── iOSApp.swift
│ ├── iosApp.entitlements
│ ├── WidgetReloader.swift
│ ├── HomeView.swift
│ ├── ContentView.swift
│ └── InfoPlist.xcstrings
├── widget
│ ├── Assets.xcassets
│ │ ├── Contents.json
│ │ ├── AppIcon.appiconset
│ │ │ ├── path_app_icon.png
│ │ │ └── Contents.json
│ │ ├── FallBackground.imageset
│ │ │ ├── image 39.png
│ │ │ └── Contents.json
│ │ ├── SpringBackground.imageset
│ │ │ ├── image 37.png
│ │ │ └── Contents.json
│ │ ├── SummerBackground.imageset
│ │ │ ├── image 38.png
│ │ │ └── Contents.json
│ │ ├── WinterBackground.imageset
│ │ │ ├── image 40.png
│ │ │ └── Contents.json
│ │ ├── WidgetBackground.colorset
│ │ │ └── Contents.json
│ │ └── AccentColor.colorset
│ │ │ └── Contents.json
│ ├── Media.xcassets
│ │ ├── Contents.json
│ │ └── AppIcon.appiconset
│ │ │ ├── path_app_icon.png
│ │ │ └── Contents.json
│ ├── FetchResult.swift
│ ├── Info.plist
│ ├── RefreshIntent.swift
│ ├── WidgetConfigurationUtils.swift
│ ├── EmptyDepartureBoardView.swift
│ ├── PathWidgetBundle.swift
│ ├── EntryView.swift
│ ├── StationTitle.swift
│ ├── ColorCircle.swift
│ ├── SeasonalUtils.swift
│ └── Interop.swift
└── widgetExtension.entitlements
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── test
├── src
│ └── commonMain
│ │ └── kotlin
│ │ └── com
│ │ └── sixbynine
│ │ └── transit
│ │ └── path
│ │ └── test
│ │ ├── Ridepath.kt
│ │ ├── TestSetupHelper.kt
│ │ ├── TestPreferences.kt
│ │ └── TestRemoteFileProviderImpl.kt
└── build.gradle.kts
├── hob_closure.json
├── .gitignore
├── gradle.properties
├── settings.gradle.kts
├── .github
└── workflows
│ └── update-schedule.yml
└── README.md
/CODEOWNERS:
--------------------------------------------------------------------------------
1 | * @steviek
--------------------------------------------------------------------------------
/api/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/flipper/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/logging/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/platform/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/schedule/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/schedule-generator/.gitignore:
--------------------------------------------------------------------------------
1 | __pycache__/
--------------------------------------------------------------------------------
/composeApp/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 | /release
3 |
--------------------------------------------------------------------------------
/.fleet/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "editor.formatOnSave": true
3 | }
--------------------------------------------------------------------------------
/schedule-generator/requirements.txt:
--------------------------------------------------------------------------------
1 | beautifulsoup4
2 | requests
3 |
--------------------------------------------------------------------------------
/composeApp/path_app_icon.icns:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/steviek/PathWidgetXplat/HEAD/composeApp/path_app_icon.icns
--------------------------------------------------------------------------------
/composeApp/path_app_icon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/steviek/PathWidgetXplat/HEAD/composeApp/path_app_icon.ico
--------------------------------------------------------------------------------
/iosApp/Configuration/Config.xcconfig:
--------------------------------------------------------------------------------
1 | TEAM_ID=
2 | BUNDLE_ID=com.sixbynine.transit.path
3 | APP_NAME=Departures for PATH
4 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/steviek/PathWidgetXplat/HEAD/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/iosApp/iosApp/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/iosApp/iosApp/Media.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/iosApp/widget/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/iosApp/widget/Media.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/iosApp/iosApp/LegacyAssets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/iosApp/iosApp/LegacyAssets copy.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/platform/src/commonMain/kotlin/com/sixbynine/transit/path/util/IsTest.kt:
--------------------------------------------------------------------------------
1 | package com.sixbynine.transit.path.util
2 |
3 | var IsTest = false
4 |
--------------------------------------------------------------------------------
/iosApp/iosApp/Preview Content/Preview Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/platform/src/commonMain/kotlin/com/sixbynine/transit/path/platform/Platform.kt:
--------------------------------------------------------------------------------
1 | package com.sixbynine.transit.path.platform
2 |
3 | expect val IsDebug: Boolean
4 |
--------------------------------------------------------------------------------
/platform/src/jvmMain/kotlin/com/sixbynine/transit/path/platform/Platform.jvm.kt:
--------------------------------------------------------------------------------
1 | package com.sixbynine.transit.path.platform
2 |
3 | actual val IsDebug = true
4 |
--------------------------------------------------------------------------------
/platform/src/iosMain/kotlin/com/sixbynine/transit/path/platform/Platform.ios.kt:
--------------------------------------------------------------------------------
1 | package com.sixbynine.transit.path.platform
2 |
3 | actual val IsDebug: Boolean = false
--------------------------------------------------------------------------------
/composeApp/src/androidMain/res/values-es/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | Mostrar las salidas de los trenes PATH
3 |
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/com/sixbynine/transit/path/api/impl/GithubAlertHelper.kt:
--------------------------------------------------------------------------------
1 | package com.sixbynine.transit.path.api.impl
2 |
3 | class GithubAlertHelper {
4 | }
--------------------------------------------------------------------------------
/composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/steviek/PathWidgetXplat/HEAD/composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/steviek/PathWidgetXplat/HEAD/composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/steviek/PathWidgetXplat/HEAD/composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/composeApp/src/androidMain/res/xml/file_paths.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/steviek/PathWidgetXplat/HEAD/composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/steviek/PathWidgetXplat/HEAD/composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/composeApp/src/androidMain/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | Display the upcoming departure times for PATH trains
3 |
--------------------------------------------------------------------------------
/composeApp/src/iosMain/kotlin/MainViewController.kt:
--------------------------------------------------------------------------------
1 | import androidx.compose.ui.window.ComposeUIViewController
2 |
3 | fun MainViewController() = ComposeUIViewController { App() }
4 |
--------------------------------------------------------------------------------
/composeApp/src/androidMain/res/drawable-nodpi/widget_preview.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/steviek/PathWidgetXplat/HEAD/composeApp/src/androidMain/res/drawable-nodpi/widget_preview.png
--------------------------------------------------------------------------------
/composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/steviek/PathWidgetXplat/HEAD/composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/steviek/PathWidgetXplat/HEAD/composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/steviek/PathWidgetXplat/HEAD/composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/steviek/PathWidgetXplat/HEAD/composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/steviek/PathWidgetXplat/HEAD/composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/composeApp/src/androidMain/res/values-en/widget_strings.xml:
--------------------------------------------------------------------------------
1 |
2 | Display the upcoming departure times for PATH trains
3 |
--------------------------------------------------------------------------------
/composeApp/src/commonMain/composeResources/drawable/train_track.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/steviek/PathWidgetXplat/HEAD/composeApp/src/commonMain/composeResources/drawable/train_track.png
--------------------------------------------------------------------------------
/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/path_app_icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/steviek/PathWidgetXplat/HEAD/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/path_app_icon.png
--------------------------------------------------------------------------------
/iosApp/iosApp/Localizable.strings:
--------------------------------------------------------------------------------
1 | /*
2 | Localizable.strings
3 | iosApp
4 |
5 | Created by Steven Kideckel on 2024-03-24.
6 | Copyright © 2024 orgName. All rights reserved.
7 | */
8 |
--------------------------------------------------------------------------------
/iosApp/iosApp/Media.xcassets/AppIcon.appiconset/path_app_icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/steviek/PathWidgetXplat/HEAD/iosApp/iosApp/Media.xcassets/AppIcon.appiconset/path_app_icon.png
--------------------------------------------------------------------------------
/iosApp/widget/Assets.xcassets/AppIcon.appiconset/path_app_icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/steviek/PathWidgetXplat/HEAD/iosApp/widget/Assets.xcassets/AppIcon.appiconset/path_app_icon.png
--------------------------------------------------------------------------------
/iosApp/widget/Assets.xcassets/FallBackground.imageset/image 39.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/steviek/PathWidgetXplat/HEAD/iosApp/widget/Assets.xcassets/FallBackground.imageset/image 39.png
--------------------------------------------------------------------------------
/iosApp/widget/Media.xcassets/AppIcon.appiconset/path_app_icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/steviek/PathWidgetXplat/HEAD/iosApp/widget/Media.xcassets/AppIcon.appiconset/path_app_icon.png
--------------------------------------------------------------------------------
/platform/src/androidMain/kotlin/com/sixbynine/transit/path/platform/Platform.android.kt:
--------------------------------------------------------------------------------
1 | package com.sixbynine.transit.path.platform
2 |
3 | actual val IsDebug: Boolean = BuildConfig.DEBUG
4 |
--------------------------------------------------------------------------------
/test/src/commonMain/kotlin/com/sixbynine/transit/path/test/Ridepath.kt:
--------------------------------------------------------------------------------
1 | package com.sixbynine.transit.path.test
2 |
3 | internal val RidePath = """
4 | {"results":[]}
5 | """.trimIndent()
--------------------------------------------------------------------------------
/api/src/commonMain/kotlin/com/sixbynine/transit/path/api/NetworkException.kt:
--------------------------------------------------------------------------------
1 | package com.sixbynine.transit.path.api
2 |
3 | class NetworkException(message: String) : RuntimeException(message)
4 |
--------------------------------------------------------------------------------
/api/src/commonMain/kotlin/com/sixbynine/transit/path/location/Location.kt:
--------------------------------------------------------------------------------
1 | package com.sixbynine.transit.path.location
2 |
3 | data class Location(val latitude: Double, val longitude: Double)
4 |
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/com/sixbynine/transit/path/app/ui/SizeWrapper.kt:
--------------------------------------------------------------------------------
1 | package com.sixbynine.transit.path.app.ui
2 |
3 | data class SizeWrapper(val width: Double, val height: Double)
4 |
--------------------------------------------------------------------------------
/iosApp/widget/Assets.xcassets/SpringBackground.imageset/image 37.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/steviek/PathWidgetXplat/HEAD/iosApp/widget/Assets.xcassets/SpringBackground.imageset/image 37.png
--------------------------------------------------------------------------------
/iosApp/widget/Assets.xcassets/SummerBackground.imageset/image 38.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/steviek/PathWidgetXplat/HEAD/iosApp/widget/Assets.xcassets/SummerBackground.imageset/image 38.png
--------------------------------------------------------------------------------
/iosApp/widget/Assets.xcassets/WinterBackground.imageset/image 40.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/steviek/PathWidgetXplat/HEAD/iosApp/widget/Assets.xcassets/WinterBackground.imageset/image 40.png
--------------------------------------------------------------------------------
/logging/src/commonMain/kotlin/com/sixbynine/transit/path/NonFatalReporter.kt:
--------------------------------------------------------------------------------
1 | package com.sixbynine.transit.path
2 |
3 | fun interface NonFatalReporter {
4 | fun report(e: Throwable)
5 | }
6 |
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/Greeting.kt:
--------------------------------------------------------------------------------
1 | class Greeting {
2 | private val platform = getPlatform()
3 |
4 | fun greet(): String {
5 | return "Hello, ${platform.name}!"
6 | }
7 | }
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/com/sixbynine/transit/path/widget/WidgetReloader.kt:
--------------------------------------------------------------------------------
1 | package com.sixbynine.transit.path.widget
2 |
3 | interface WidgetReloader {
4 | fun reloadWidgets()
5 | }
6 |
--------------------------------------------------------------------------------
/iosApp/iosApp/LegacyAssets.xcassets/AppIcon.appiconset/path_app_icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/steviek/PathWidgetXplat/HEAD/iosApp/iosApp/LegacyAssets.xcassets/AppIcon.appiconset/path_app_icon.png
--------------------------------------------------------------------------------
/composeApp/src/androidMain/res/values-night/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/iosApp/iosApp/LegacyAssets copy.xcassets/AppIcon.appiconset/path_app_icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/steviek/PathWidgetXplat/HEAD/iosApp/iosApp/LegacyAssets copy.xcassets/AppIcon.appiconset/path_app_icon.png
--------------------------------------------------------------------------------
/hob_closure.json:
--------------------------------------------------------------------------------
1 | {"tempLineInfo":{"displayName":"33rd Street ⇆ World Trade Center","codes":["WTC-33","33-WTC"],"lightColor":"65C100","darkColor":null},"validFrom":"2025-01-18T23:59","validTo":"2025-02-28T05:00"}
2 |
--------------------------------------------------------------------------------
/platform/src/androidMain/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/platform/src/jvmMain/kotlin/com/sixbynine/transit/path/preferences/Preferences.jvm.kt:
--------------------------------------------------------------------------------
1 | package com.sixbynine.transit.path.preferences
2 |
3 | actual fun createPreferences(): Preferences {
4 | return InMemoryPreferences
5 | }
6 |
--------------------------------------------------------------------------------
/api/src/commonMain/kotlin/com/sixbynine/transit/path/api/PathApiException.kt:
--------------------------------------------------------------------------------
1 | package com.sixbynine.transit.path.api
2 |
3 | abstract class PathApiException : RuntimeException() {
4 | data object NoResults : PathApiException()
5 | }
6 |
--------------------------------------------------------------------------------
/composeApp/src/androidMain/res/drawable/ic_edit_inset.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/iosApp/iosApp/Preview Content/Preview Assets.xcassets/AppIcon.appiconset/path_app_icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/steviek/PathWidgetXplat/HEAD/iosApp/iosApp/Preview Content/Preview Assets.xcassets/AppIcon.appiconset/path_app_icon.png
--------------------------------------------------------------------------------
/iosApp/widget/Assets.xcassets/WidgetBackground.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "idiom" : "universal"
5 | }
6 | ],
7 | "info" : {
8 | "author" : "xcode",
9 | "version" : 1
10 | }
11 | }
--------------------------------------------------------------------------------
/composeApp/src/androidMain/res/drawable/ic_refresh_inset.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/composeApp/src/androidMain/res/drawable/ic_warning_inset.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/com/sixbynine/transit/path/app/settings/DevOptionsExport.kt:
--------------------------------------------------------------------------------
1 | package com.sixbynine.transit.path.app.settings
2 |
3 | import com.sixbynine.transit.path.LogRecord
4 |
5 | expect fun exportDevLogs(logs: List)
6 |
--------------------------------------------------------------------------------
/api/src/commonMain/kotlin/com/sixbynine/transit/path/api/Coordinates.kt:
--------------------------------------------------------------------------------
1 | package com.sixbynine.transit.path.api
2 |
3 | import kotlinx.serialization.Serializable
4 |
5 | @Serializable
6 | data class Coordinates(val latitude: Double, val longitude: Double)
7 |
--------------------------------------------------------------------------------
/composeApp/src/androidMain/res/xml/locales_config.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/iosApp/iosApp/iOSApp.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | @main
4 | struct iOSApp: App {
5 | @UIApplicationDelegateAdaptor(AppDelegate.self) var delegate
6 |
7 | var body: some Scene {
8 | WindowGroup {
9 | ContentView()
10 | }
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/platform/src/commonMain/kotlin/com/sixbynine/transit/path/util/Staleness.kt:
--------------------------------------------------------------------------------
1 | package com.sixbynine.transit.path.util
2 |
3 | import kotlin.time.Duration
4 |
5 | data class Staleness(
6 | val staleAfter: Duration,
7 | val invalidAfter: Duration,
8 | )
9 |
--------------------------------------------------------------------------------
/api/src/commonMain/kotlin/com/sixbynine/transit/path/api/StationChoice.kt:
--------------------------------------------------------------------------------
1 | package com.sixbynine.transit.path.api
2 |
3 | sealed interface StationChoice {
4 | data class Fixed(val station: Station) : StationChoice
5 | data object Closest : StationChoice
6 | }
7 |
--------------------------------------------------------------------------------
/platform/src/commonMain/kotlin/com/sixbynine/transit/path/network/NetworkManager.kt:
--------------------------------------------------------------------------------
1 | package com.sixbynine.transit.path.network
2 |
3 | interface NetworkManager {
4 | fun isConnectedToInternet(): Boolean
5 | }
6 |
7 | expect fun NetworkManager(): NetworkManager
8 |
--------------------------------------------------------------------------------
/composeApp/src/androidMain/res/drawable/circle.xml:
--------------------------------------------------------------------------------
1 |
3 |
4 |
7 |
--------------------------------------------------------------------------------
/flipper/src/androidRelease/kotlin/com/sixbynine/transit/path/flipper/FlipperUtil.kt:
--------------------------------------------------------------------------------
1 | package com.sixbynine.transit.path.flipper
2 |
3 | import android.content.Context
4 |
5 | object FlipperUtil {
6 | fun initialize(context: Context) = Unit
7 |
8 | fun interceptor(): Any? = null
9 | }
--------------------------------------------------------------------------------
/platform/src/jvmMain/kotlin/com/sixbynine/transit/path/network/NetworkManager.jvm.kt:
--------------------------------------------------------------------------------
1 | package com.sixbynine.transit.path.network
2 |
3 | actual fun NetworkManager() = object : NetworkManager {
4 | override fun isConnectedToInternet(): Boolean {
5 | return true
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/composeApp/src/androidMain/res/drawable/preview_circle_hob.xml:
--------------------------------------------------------------------------------
1 |
3 |
4 |
7 |
--------------------------------------------------------------------------------
/composeApp/src/androidMain/res/drawable/preview_circle_nwk.xml:
--------------------------------------------------------------------------------
1 |
3 |
4 |
7 |
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/Platform.kt:
--------------------------------------------------------------------------------
1 | interface Platform {
2 | val name: String
3 |
4 | val type: PlatformType
5 | }
6 |
7 | expect fun getPlatform(): Platform
8 |
9 | expect val IsDebug: Boolean
10 |
11 | enum class PlatformType {
12 | ANDROID, IOS, DESKTOP
13 | }
--------------------------------------------------------------------------------
/platform/src/androidMain/kotlin/com/sixbynine/transit/path/PreviewContext.kt:
--------------------------------------------------------------------------------
1 | package com.sixbynine.transit.path
2 |
3 | import android.annotation.SuppressLint
4 | import android.content.Context
5 |
6 | @SuppressLint("StaticFieldLeak") // Just for previews
7 | var PreviewContext: Context? = null
--------------------------------------------------------------------------------
/composeApp/src/desktopMain/kotlin/com/sixbynine/transit/path/app/settings/DevOptionsExport.jvm.kt:
--------------------------------------------------------------------------------
1 | package com.sixbynine.transit.path.app.settings
2 |
3 | import com.sixbynine.transit.path.LogRecord
4 |
5 | actual fun exportDevLogs(logs: List) {
6 | TODO("Not implemented on jvm")
7 | }
8 |
--------------------------------------------------------------------------------
/api/src/commonMain/kotlin/com/sixbynine/transit/path/api/HttpClientFactory.kt:
--------------------------------------------------------------------------------
1 | package com.sixbynine.transit.path.api
2 |
3 | import io.ktor.client.HttpClient
4 | import io.ktor.client.HttpClientConfig
5 |
6 | expect fun createHttpClient(
7 | block: HttpClientConfig<*>.() -> Unit = {}
8 | ): HttpClient
9 |
--------------------------------------------------------------------------------
/composeApp/src/androidMain/res/drawable/circle_border.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionBase=GRADLE_USER_HOME
2 | distributionPath=wrapper/dists
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip
4 | networkTimeout=10000
5 | validateDistributionUrl=true
6 | zipStoreBase=GRADLE_USER_HOME
7 | zipStorePath=wrapper/dists
8 |
--------------------------------------------------------------------------------
/api/src/commonMain/kotlin/com/sixbynine/transit/path/api/schedule/GithubSchedules.kt:
--------------------------------------------------------------------------------
1 | package com.sixbynine.transit.path.api.schedule
2 |
3 | import com.sixbynine.transit.path.schedule.Timetables
4 |
5 | data class ScheduleAndOverride(
6 | val regularSchedule: Timetables,
7 | val override: Timetables?,
8 | )
9 |
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/com/sixbynine/transit/path/app/settings/TimeDisplay.kt:
--------------------------------------------------------------------------------
1 | package com.sixbynine.transit.path.app.settings
2 |
3 | import com.sixbynine.transit.path.preferences.IntPersistable
4 |
5 | enum class TimeDisplay(override val number: Int) : IntPersistable {
6 | Relative(1), Clock(2)
7 | }
8 |
--------------------------------------------------------------------------------
/composeApp/src/desktopMain/kotlin/Platform.jvm.kt:
--------------------------------------------------------------------------------
1 |
2 | object JvmPlatform : Platform {
3 | override val name = "Java ${System.getProperty("java.version")}"
4 | override val type = PlatformType.DESKTOP
5 | }
6 |
7 | actual fun getPlatform(): Platform = JvmPlatform
8 | actual val IsDebug: Boolean get() = true
9 |
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/com/sixbynine/transit/path/app/station/StationSelection.kt:
--------------------------------------------------------------------------------
1 | package com.sixbynine.transit.path.app.station
2 |
3 | import com.sixbynine.transit.path.api.Station
4 |
5 | data class StationSelection(
6 | val selectedStations: List,
7 | val unselectedStations: List
8 | )
9 |
--------------------------------------------------------------------------------
/api/src/commonMain/kotlin/com/sixbynine/transit/path/util/TestRemoteFileProvider.kt:
--------------------------------------------------------------------------------
1 | package com.sixbynine.transit.path.util
2 |
3 | interface TestRemoteFileProvider {
4 | fun getText(url: String): Result
5 |
6 | companion object {
7 | var instance: TestRemoteFileProvider? = null
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/api/src/jvmMain/kotlin/com/sixbynine/transit/path/api/HttpClientFactory.jvm.kt:
--------------------------------------------------------------------------------
1 | package com.sixbynine.transit.path.api
2 |
3 | import io.ktor.client.HttpClient
4 | import io.ktor.client.HttpClientConfig
5 |
6 | actual fun createHttpClient(block: HttpClientConfig<*>.() -> Unit): HttpClient {
7 | return HttpClient(block)
8 | }
9 |
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/com/sixbynine/transit/path/app/ui/sheet/BottomSheetTitle.kt:
--------------------------------------------------------------------------------
1 | package com.sixbynine.transit.path.app.ui.sheet
2 |
3 | import androidx.compose.material3.Text
4 | import androidx.compose.runtime.Composable
5 |
6 | @Composable
7 | fun BottomSheetTitle(text: String) {
8 | Text(text)
9 | }
10 |
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/com/sixbynine/transit/path/util/ComposeExtensions.kt:
--------------------------------------------------------------------------------
1 | package com.sixbynine.transit.path.util
2 |
3 | import androidx.compose.ui.Modifier
4 |
5 | inline fun Modifier.conditional(condition: Boolean, transform: Modifier.() -> Modifier): Modifier {
6 | return if (condition) transform() else this
7 | }
8 |
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/com/sixbynine/transit/path/api/LocationSetting.kt:
--------------------------------------------------------------------------------
1 | package com.sixbynine.transit.path.api
2 |
3 | import com.sixbynine.transit.path.preferences.IntPersistable
4 |
5 | enum class LocationSetting(override val number: Int) : IntPersistable {
6 | Enabled(1), Disabled(2), EnabledPendingPermission(3);
7 | }
8 |
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/com/sixbynine/transit/path/app/settings/AvoidMissingTrains.kt:
--------------------------------------------------------------------------------
1 | package com.sixbynine.transit.path.app.settings
2 |
3 | import com.sixbynine.transit.path.preferences.IntPersistable
4 |
5 | enum class AvoidMissingTrains(override val number: Int) : IntPersistable {
6 | Disabled(1), OffPeak(2), Always(3)
7 | }
--------------------------------------------------------------------------------
/composeApp/src/androidMain/res/drawable/circle_right.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "path_app_icon.png",
5 | "idiom" : "universal",
6 | "platform" : "ios",
7 | "size" : "1024x1024"
8 | }
9 | ],
10 | "info" : {
11 | "author" : "xcode",
12 | "version" : 1
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/iosApp/iosApp/Media.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "path_app_icon.png",
5 | "idiom" : "universal",
6 | "platform" : "ios",
7 | "size" : "1024x1024"
8 | }
9 | ],
10 | "info" : {
11 | "author" : "xcode",
12 | "version" : 1
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/iosApp/widget/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "path_app_icon.png",
5 | "idiom" : "universal",
6 | "platform" : "ios",
7 | "size" : "1024x1024"
8 | }
9 | ],
10 | "info" : {
11 | "author" : "xcode",
12 | "version" : 1
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/iosApp/widget/Media.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "path_app_icon.png",
5 | "idiom" : "universal",
6 | "platform" : "ios",
7 | "size" : "1024x1024"
8 | }
9 | ],
10 | "info" : {
11 | "author" : "xcode",
12 | "version" : 1
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/iosApp/iosApp/LegacyAssets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "path_app_icon.png",
5 | "idiom" : "universal",
6 | "platform" : "ios",
7 | "size" : "1024x1024"
8 | }
9 | ],
10 | "info" : {
11 | "author" : "xcode",
12 | "version" : 1
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/schedule/src/commonMain/kotlin/com/sixbynine/transit/path/schedule/Schedule.kt:
--------------------------------------------------------------------------------
1 | package com.sixbynine.transit.path.schedule
2 |
3 | import kotlinx.datetime.LocalDateTime
4 |
5 | /** Schedule that can be either active or inactive given a specific calendar time. */
6 | interface Schedule {
7 | fun isActiveAt(dateTime: LocalDateTime): Boolean
8 | }
9 |
--------------------------------------------------------------------------------
/api/src/iosMain/kotlin/com/sixbynine/transit/path/api/IosHttpClientFactory.kt:
--------------------------------------------------------------------------------
1 | package com.sixbynine.transit.path.api
2 |
3 | import io.ktor.client.HttpClient
4 | import io.ktor.client.HttpClientConfig
5 |
6 | actual fun createHttpClient(
7 | block: HttpClientConfig<*>.() -> Unit
8 | ): HttpClient {
9 | return HttpClient(block)
10 | }
11 |
--------------------------------------------------------------------------------
/iosApp/iosApp/LegacyAssets copy.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "path_app_icon.png",
5 | "idiom" : "universal",
6 | "platform" : "ios",
7 | "size" : "1024x1024"
8 | }
9 | ],
10 | "info" : {
11 | "author" : "xcode",
12 | "version" : 1
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/platform/src/iosMain/kotlin/com/sixbynine/transit/path/network/IosNetworkManager.kt:
--------------------------------------------------------------------------------
1 | package com.sixbynine.transit.path.network
2 |
3 | object IosNetworkManager : NetworkManager {
4 | override fun isConnectedToInternet(): Boolean {
5 | return true
6 | }
7 | }
8 |
9 | actual fun NetworkManager(): NetworkManager = IosNetworkManager
10 |
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/com/sixbynine/transit/path/app/settings/StationLimit.kt:
--------------------------------------------------------------------------------
1 | package com.sixbynine.transit.path.app.settings
2 |
3 | import com.sixbynine.transit.path.preferences.IntPersistable
4 |
5 | enum class StationLimit(override val number: Int) : IntPersistable {
6 | None(1), Four(2), Six(3), OnePerLine(4), TwoPerLine(5), ThreePerLine(6)
7 | }
8 |
--------------------------------------------------------------------------------
/iosApp/iosApp/Preview Content/Preview Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "path_app_icon.png",
5 | "idiom" : "universal",
6 | "platform" : "ios",
7 | "size" : "1024x1024"
8 | }
9 | ],
10 | "info" : {
11 | "author" : "xcode",
12 | "version" : 1
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/iosApp/iosApp/iosApp.entitlements:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | com.apple.security.application-groups
6 |
7 | group.com.sixbynine.transit.path
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/iosApp/widgetExtension.entitlements:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | com.apple.security.application-groups
6 |
7 | group.com.sixbynine.transit.path
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/composeApp/src/androidMain/res/drawable/ic_filter.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/api/src/commonMain/kotlin/com/sixbynine/transit/path/api/alerts/github/GithubAlerts.kt:
--------------------------------------------------------------------------------
1 | package com.sixbynine.transit.path.api.alerts.github
2 |
3 | import com.sixbynine.transit.path.api.alerts.Alert
4 | import kotlinx.serialization.Serializable
5 |
6 | @Serializable
7 | data class GithubAlerts(val alerts: List) {
8 | constructor(vararg alerts: Alert) : this(alerts.toList())
9 | }
10 |
--------------------------------------------------------------------------------
/composeApp/src/desktopMain/kotlin/com/sixbynine/transit/path/analytics/Analytics.jvm.kt:
--------------------------------------------------------------------------------
1 | package com.sixbynine.transit.path.analytics
2 |
3 | actual fun AnalyticsStrategy(): AnalyticsStrategy = object : AnalyticsStrategy {
4 | override fun logEvent(
5 | name: String,
6 | params: Map
7 | ) {
8 | println("Event: $name, Params: $params")
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/platform/src/commonMain/kotlin/com/sixbynine/transit/path/util/TimestampedValue.kt:
--------------------------------------------------------------------------------
1 | package com.sixbynine.transit.path.util
2 |
3 | import kotlinx.datetime.Instant
4 |
5 | data class TimestampedValue(val timestamp: Instant, val value: T)
6 |
7 | fun TimestampedValue.toAgedValue(now: Instant): AgedValue {
8 | val age = now - timestamp
9 | return AgedValue(age, value)
10 | }
11 |
--------------------------------------------------------------------------------
/api/src/commonMain/kotlin/com/sixbynine/transit/path/api/UpcomingDepartures.kt:
--------------------------------------------------------------------------------
1 | package com.sixbynine.transit.path.api
2 |
3 | class UpcomingDepartures(
4 | private val stationToTrains: Map>,
5 | val scheduleName: String?,
6 | ) {
7 | fun getTrainsAt(station: Station): List? {
8 | return stationToTrains[station.pathApiName]
9 | }
10 | }
--------------------------------------------------------------------------------
/iosApp/widget/FetchResult.swift:
--------------------------------------------------------------------------------
1 | //
2 | // FetchResult.swift
3 | // widget2Extension
4 | //
5 | // Created by Steven Kideckel on 2023-10-23.
6 | // Copyright © 2023 orgName. All rights reserved.
7 | //
8 |
9 | import ComposeApp
10 |
11 | struct FetchResult {
12 | let data: DepartureBoardData?
13 | let hadInternet: Bool
14 | let hasError: Bool
15 | let hasPathError: Bool
16 | }
17 |
--------------------------------------------------------------------------------
/composeApp/src/androidMain/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/composeApp/src/desktopMain/composeResources/drawable/ic_filter.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/platform/src/commonMain/kotlin/com/sixbynine/transit/path/util/CoroutineScopes.kt:
--------------------------------------------------------------------------------
1 | package com.sixbynine.transit.path.util
2 |
3 | import kotlinx.coroutines.CoroutineScope
4 | import kotlinx.coroutines.Dispatchers
5 | import kotlinx.coroutines.IO
6 |
7 | val ApplicationScope = CoroutineScope(Dispatchers.Main)
8 | val IoScope = CoroutineScope(Dispatchers.IO)
9 | val BackgroundScope = CoroutineScope(Dispatchers.Default)
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/com/sixbynine/transit/path/util/CollectionExtensions.kt:
--------------------------------------------------------------------------------
1 | package com.sixbynine.transit.path.util
2 |
3 | fun List.secondOrNull(): T? {
4 | return getOrNull(1)
5 | }
6 |
7 | fun Set.withElementPresent(element: T, present: Boolean): Set {
8 | return if (present) {
9 | plus(element)
10 | } else {
11 | minus(element)
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/composeApp/src/androidMain/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/composeApp/src/androidMain/res/drawable/ic_sort.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/test/src/commonMain/kotlin/com/sixbynine/transit/path/test/TestSetupHelper.kt:
--------------------------------------------------------------------------------
1 | package com.sixbynine.transit.path.test
2 |
3 | import com.sixbynine.transit.path.util.IsTest
4 | import com.sixbynine.transit.path.util.TestRemoteFileProvider
5 |
6 | object TestSetupHelper {
7 | fun setUp() {
8 | IsTest = true
9 | TestRemoteFileProvider.install()
10 | TestPreferences.install()
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/composeApp/src/androidMain/res/drawable/ic_arrow_up.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/composeApp/src/desktopMain/composeResources/drawable/ic_sort.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/iosApp/iosApp/WidgetReloader.swift:
--------------------------------------------------------------------------------
1 | //
2 | // WidgetReloader.swift
3 | // iosApp
4 | //
5 | // Created by Steven Kideckel on 2024-12-12.
6 | // Copyright © 2024 orgName. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import WidgetKit
11 | import ComposeApp
12 |
13 | class IosWidgetReloader : WidgetReloader {
14 | func reloadWidgets() {
15 | WidgetCenter.shared.reloadAllTimelines()
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/composeApp/src/androidMain/res/drawable/ic_arrow_down.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/composeApp/src/desktopMain/composeResources/drawable/ic_arrow_up.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | **/build/
4 | xcuserdata
5 | !src/**/build/
6 | local.properties
7 | .idea
8 | .DS_Store
9 | captures
10 | .externalNativeBuild
11 | .cxx
12 | *.xcodeproj/*
13 | !*.xcodeproj/project.pbxproj
14 | !*.xcodeproj/xcshareddata/
15 | !*.xcodeproj/project.xcworkspace/
16 | !*.xcworkspace/contents.xcworkspacedata
17 | **/xcshareddata/WorkspaceSettings.xcsettings
18 | google-services.json
19 | GoogleService-Info.plist
20 |
--------------------------------------------------------------------------------
/composeApp/src/androidMain/kotlin/Platform.android.kt:
--------------------------------------------------------------------------------
1 | import PlatformType.ANDROID
2 | import android.os.Build
3 | import com.sixbynine.transit.path.BuildConfig
4 |
5 | class AndroidPlatform : Platform {
6 | override val name: String = "Android ${Build.VERSION.SDK_INT}"
7 |
8 | override val type = ANDROID
9 | }
10 |
11 | actual fun getPlatform(): Platform = AndroidPlatform()
12 |
13 | actual val IsDebug: Boolean = BuildConfig.DEBUG
14 |
--------------------------------------------------------------------------------
/composeApp/src/androidMain/kotlin/com/sixbynine/transit/path/widget/ui/WidgetState.kt:
--------------------------------------------------------------------------------
1 | package com.sixbynine.transit.path.widget.ui
2 |
3 | import com.sixbynine.transit.path.util.DataResult
4 | import com.sixbynine.transit.path.model.DepartureBoardData
5 | import kotlinx.datetime.Instant
6 |
7 | data class WidgetState(
8 | val result: DataResult,
9 | val updateTime: Instant,
10 | val needsSetup: Boolean,
11 | )
12 |
--------------------------------------------------------------------------------
/composeApp/src/desktopMain/composeResources/drawable/ic_arrow_down.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/platform/src/jvmMain/kotlin/com/sixbynine/transit/path/time/TimeUtils.jvm.kt:
--------------------------------------------------------------------------------
1 | package com.sixbynine.transit.path.time
2 |
3 | import kotlinx.datetime.DayOfWeek
4 |
5 | actual fun getPlatformTimeUtils(): PlatformTimeUtils = object : PlatformTimeUtils{
6 | override fun is24HourClock(): Boolean {
7 | return true
8 | }
9 |
10 | override fun getFirstDayOfWeek(): DayOfWeek {
11 | return DayOfWeek.MONDAY
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/composeApp/src/androidMain/kotlin/com/sixbynine/transit/path/app/ui/setup/SetupScreenPreview.kt:
--------------------------------------------------------------------------------
1 | package com.sixbynine.transit.path.app.ui.setup
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.compose.ui.tooling.preview.Preview
5 | import com.sixbynine.transit.path.PreviewTheme
6 |
7 | @Preview
8 | @Composable
9 | fun SetupScreenPreview() {
10 | PreviewTheme {
11 | SetupScreen(SetupScreenContract.State(), {})
12 | }
13 | }
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/com/sixbynine/transit/path/api/StationSort.kt:
--------------------------------------------------------------------------------
1 | package com.sixbynine.transit.path.api
2 |
3 | import com.sixbynine.transit.path.preferences.IntPersistable
4 |
5 | enum class StationSort(override val number: Int) : IntPersistable {
6 | Alphabetical(1), NjAm(2), NyAm(3), Proximity(4);
7 |
8 | companion object {
9 | fun isProximityEnabled(): Boolean {
10 | return false
11 | }
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/composeApp/src/androidMain/res/values-night/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | @android:color/black
4 | @color/path_blue_200
5 | @android:color/white
6 | @android:color/darker_gray
7 | @android:color/white
8 |
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/com/sixbynine/transit/path/app/ui/FontInfo.kt:
--------------------------------------------------------------------------------
1 | package com.sixbynine.transit.path.app.ui
2 |
3 | data class FontInfo(
4 | val size: Double,
5 | val isBold: Boolean = false,
6 | val isMonospacedDigit: Boolean = false
7 | ) {
8 | constructor(
9 | size: Int,
10 | isBold: Boolean = false,
11 | isMonospacedDigit: Boolean = false
12 | ) : this(size.toDouble(), isBold, isMonospacedDigit)
13 | }
14 |
--------------------------------------------------------------------------------
/iosApp/widget/Assets.xcassets/FallBackground.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "image 39.png",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "author" : "xcode",
19 | "version" : 1
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/iosApp/widget/Assets.xcassets/SpringBackground.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "image 37.png",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "author" : "xcode",
19 | "version" : 1
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/iosApp/widget/Assets.xcassets/SummerBackground.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "image 38.png",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "author" : "xcode",
19 | "version" : 1
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/iosApp/widget/Assets.xcassets/WinterBackground.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "image 40.png",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "author" : "xcode",
19 | "version" : 1
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | kotlin.code.style=official
2 |
3 | #Gradle
4 | org.gradle.jvmargs=-Xmx2048M -Dfile.encoding=UTF-8 -Dkotlin.daemon.jvm.options\="-Xmx2048M"
5 |
6 |
7 | #Compose
8 | org.jetbrains.compose.experimental.uikit.enabled=true
9 |
10 | #Android
11 | android.useAndroidX=true
12 | android.nonTransitiveRClass=true
13 |
14 | #MPP
15 | kotlin.mpp.androidSourceSetLayoutVersion=2
16 | kotlin.mpp.enableCInteropCommonization=true
17 |
18 | #Development
19 | development=true
--------------------------------------------------------------------------------
/iosApp/widget/Assets.xcassets/AccentColor.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "color" : {
5 | "color-space" : "srgb",
6 | "components" : {
7 | "alpha" : "1.000",
8 | "blue" : "0xD1",
9 | "green" : "0x95",
10 | "red" : "0x17"
11 | }
12 | },
13 | "idiom" : "universal"
14 | }
15 | ],
16 | "info" : {
17 | "author" : "xcode",
18 | "version" : 1
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/composeApp/src/androidMain/res/drawable/ic_down.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/iosApp/iosApp/LegacyAssets.xcassets/AccentColor.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "color" : {
5 | "color-space" : "srgb",
6 | "components" : {
7 | "alpha" : "1.000",
8 | "blue" : "0xD1",
9 | "green" : "0x96",
10 | "red" : "0x18"
11 | }
12 | },
13 | "idiom" : "universal"
14 | }
15 | ],
16 | "info" : {
17 | "author" : "xcode",
18 | "version" : 1
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/api/src/commonTest/kotlin/com/sixbynine/transit/path/HobClosureRepoTest.kt:
--------------------------------------------------------------------------------
1 | package com.sixbynine.transit.path
2 |
3 | import com.sixbynine.transit.path.api.templine.HobClosureConfig
4 | import kotlinx.serialization.encodeToString
5 | import kotlinx.serialization.json.Json
6 | import kotlin.test.Test
7 |
8 | class HobClosureRepoTest {
9 | @Test
10 | fun foo() {
11 | val config = HobClosureConfig.fallback
12 | println(Json.encodeToString(config))
13 | }
14 | }
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/com/sixbynine/transit/path/app/ui/AppUiScope.kt:
--------------------------------------------------------------------------------
1 | package com.sixbynine.transit.path.app.ui
2 |
3 | import LocalIsTablet
4 | import androidx.compose.runtime.Composable
5 | import androidx.compose.ui.unit.Dp
6 | import com.sixbynine.transit.path.app.ui.theme.Dimensions
7 |
8 | interface AppUiScope {
9 | val isTablet: Boolean
10 | }
11 |
12 | @Composable
13 | fun gutter(): Dp {
14 | return Dimensions.gutter(isTablet = LocalIsTablet.current)
15 | }
--------------------------------------------------------------------------------
/iosApp/iosApp/LegacyAssets copy.xcassets/AccentColor.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "color" : {
5 | "color-space" : "srgb",
6 | "components" : {
7 | "alpha" : "1.000",
8 | "blue" : "0xD1",
9 | "green" : "0x96",
10 | "red" : "0x18"
11 | }
12 | },
13 | "idiom" : "universal"
14 | }
15 | ],
16 | "info" : {
17 | "author" : "xcode",
18 | "version" : 1
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/composeApp/src/desktopMain/composeResources/drawable/ic_down.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.fleet/receipt.json:
--------------------------------------------------------------------------------
1 | // Project generated by Kotlin Multiplatform Wizard
2 | {
3 | "spec": {
4 | "template_id": "kmt",
5 | "targets": {
6 | "android": {
7 | "ui": [
8 | "compose"
9 | ]
10 | },
11 | "ios": {
12 | "ui": [
13 | "compose"
14 | ]
15 | }
16 | }
17 | },
18 | "timestamp": "2023-11-28T02:16:51.041143273Z"
19 | }
--------------------------------------------------------------------------------
/composeApp/src/androidMain/res/drawable/ic_open_in_new.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/composeApp/src/desktopMain/composeResources/drawable/ic_open_in_new.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/platform/src/androidMain/kotlin/com/sixbynine/transit/path/PathApplication.kt:
--------------------------------------------------------------------------------
1 | package com.sixbynine.transit.path
2 |
3 | import android.app.Application
4 | import com.sixbynine.transit.path.flipper.FlipperUtil
5 |
6 | abstract class PathApplication : Application() {
7 |
8 | override fun onCreate() {
9 | super.onCreate()
10 | instance = this
11 | FlipperUtil.initialize(this)
12 | }
13 |
14 | companion object {
15 | lateinit var instance: PathApplication
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/api/src/commonTest/kotlin/com/sixbynine/transit/path/api/StationsTest.kt:
--------------------------------------------------------------------------------
1 | package com.sixbynine.transit.path.api
2 |
3 | import com.sixbynine.transit.path.location.Location
4 | import kotlin.test.Test
5 | import kotlin.test.assertEquals
6 |
7 | class StationsTest {
8 | @Test
9 | fun `random pin`() {
10 | val location = Location(40.742114, -73.990503)
11 | val closestStations = Stations.byProximityTo(location)
12 |
13 | assertEquals(Stations.TwentyThirdStreet, closestStations.first())
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/composeApp/src/androidMain/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 |
10 |
--------------------------------------------------------------------------------
/composeApp/src/androidMain/res/drawable/ic_station.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/composeApp/src/androidMain/res/drawable/ic_warning.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/composeApp/src/androidMain/res/layout/widget_loading.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
12 |
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/com/sixbynine/transit/path/app/ui/theme/Dimensions.kt:
--------------------------------------------------------------------------------
1 | package com.sixbynine.transit.path.app.ui.theme
2 |
3 | import androidx.compose.foundation.layout.BoxWithConstraintsScope
4 | import androidx.compose.ui.unit.Dp
5 | import androidx.compose.ui.unit.dp
6 |
7 | object Dimensions {
8 | fun gutter(isTablet: Boolean): Dp {
9 | return if (isTablet) 64.dp else 16.dp
10 | }
11 |
12 | fun BoxWithConstraintsScope.isTablet(): Boolean {
13 | return minOf(maxWidth, maxHeight) >= 480.dp
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/composeApp/src/desktopMain/composeResources/drawable/ic_station.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/iosApp/widget/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | NSExtension
6 |
7 | NSExtensionPointIdentifier
8 | com.apple.widgetkit-extension
9 |
10 | CFBundleLocalizations
11 |
12 | en
13 | es
14 |
15 | NSWidgetWantsLocation
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/composeApp/src/androidMain/res/drawable/ic_edit.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/api/src/commonMain/kotlin/com/sixbynine/transit/path/api/Line.kt:
--------------------------------------------------------------------------------
1 | package com.sixbynine.transit.path.api
2 |
3 | import com.sixbynine.transit.path.preferences.IntPersistable
4 |
5 | enum class Line(override val number: Int) : IntPersistable {
6 | NewarkWtc(1), HobokenWtc(2), JournalSquare33rd(3), Hoboken33rd(4);
7 |
8 | companion object {
9 | val permanentLines: List =
10 | listOf(NewarkWtc, HobokenWtc, JournalSquare33rd, Hoboken33rd)
11 |
12 | val permanentLinesForWtc33rd = listOf(HobokenWtc, JournalSquare33rd)
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/composeApp/src/androidMain/res/drawable/ic_refresh.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/api/src/commonMain/kotlin/com/sixbynine/transit/path/api/PathApi.kt:
--------------------------------------------------------------------------------
1 | package com.sixbynine.transit.path.api
2 |
3 | import com.sixbynine.transit.path.api.impl.PathApiImpl
4 | import com.sixbynine.transit.path.util.FetchWithPrevious
5 | import com.sixbynine.transit.path.util.Staleness
6 | import kotlinx.datetime.Instant
7 |
8 | interface PathApi {
9 |
10 | fun getUpcomingDepartures(
11 | now: Instant,
12 | staleness: Staleness,
13 | ): FetchWithPrevious
14 |
15 | companion object {
16 | val instance: PathApi = PathApiImpl()
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/composeApp/src/iosMain/kotlin/com/sixbynine/transit/path/analytics/IosAnalytics.kt:
--------------------------------------------------------------------------------
1 | package com.sixbynine.transit.path.analytics
2 |
3 | import platform.Foundation.NSNotificationCenter
4 |
5 | object IosAnalytics : AnalyticsStrategy {
6 | override fun logEvent(name: String, params: Map) {
7 | NSNotificationCenter.defaultCenter.postNotificationName(
8 | "logEvent",
9 | `object` = null,
10 | userInfo = params + ("event_name" to name)
11 | )
12 | }
13 | }
14 |
15 | actual fun AnalyticsStrategy(): AnalyticsStrategy = IosAnalytics
16 |
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/com/sixbynine/transit/path/app/ui/setup/SetupScreenContract.kt:
--------------------------------------------------------------------------------
1 | package com.sixbynine.transit.path.app.ui.setup
2 |
3 | import com.sixbynine.transit.path.api.Station
4 |
5 | object SetupScreenContract {
6 | data class State(val selectedStations: Set = emptySet())
7 |
8 | sealed interface Intent {
9 | data class StationCheckedChanged(val station: Station, val isChecked: Boolean) : Intent
10 | data object ConfirmClicked : Intent
11 | }
12 |
13 | sealed interface Effect {
14 | data object NavigateToHome : Effect
15 | }
16 | }
--------------------------------------------------------------------------------
/composeApp/src/androidMain/res/xml/departure_widget.xml:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/platform/src/commonMain/kotlin/com/sixbynine/transit/path/time/UserPreferenceDayOfWeekComparator.kt:
--------------------------------------------------------------------------------
1 | package com.sixbynine.transit.path.time
2 |
3 | import kotlinx.datetime.DayOfWeek
4 |
5 | class UserPreferenceDayOfWeekComparator(
6 | private val firstDayOfWeek: DayOfWeek = getPlatformTimeUtils().getFirstDayOfWeek()
7 | ) : Comparator {
8 |
9 | override fun compare(a: DayOfWeek, b: DayOfWeek): Int {
10 | val aIndex = (a.ordinal - firstDayOfWeek.ordinal + 7) % 7
11 | val bIndex = (b.ordinal - firstDayOfWeek.ordinal + 7) % 7
12 | return aIndex - bIndex
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/platform/src/commonMain/kotlin/com/sixbynine/transit/path/util/AgedValue.kt:
--------------------------------------------------------------------------------
1 | package com.sixbynine.transit.path.util
2 |
3 | import kotlin.time.Duration
4 |
5 | data class AgedValue(val age: Duration, val value: T)
6 |
7 | fun AgedValue.map(transform: (T) -> R): AgedValue {
8 | return AgedValue(age, transform(value))
9 | }
10 |
11 | inline fun AgedValue.combine(
12 | other: AgedValue,
13 | transform: (A, B) -> C
14 | ): AgedValue {
15 | return AgedValue(
16 | age = maxOf(age, other.age),
17 | value = transform(value, other.value),
18 | )
19 | }
20 |
--------------------------------------------------------------------------------
/composeApp/src/iosMain/kotlin/Platform.ios.kt:
--------------------------------------------------------------------------------
1 | import com.sixbynine.transit.path.time.IosPlatformTimeUtils
2 | import platform.UIKit.UIDevice
3 |
4 | class IOSPlatform: Platform {
5 | override val name: String = UIDevice.currentDevice.systemName() + " " + UIDevice.currentDevice.systemVersion
6 |
7 | override val type = PlatformType.IOS
8 |
9 | fun setFirstDayOfWeek(firstDayOfWeek: String?) {
10 | IosPlatformTimeUtils.setFirstDayOfWeek(firstDayOfWeek)
11 | }
12 | }
13 |
14 | actual fun getPlatform(): Platform = IOSPlatform()
15 |
16 | actual val IsDebug: Boolean = kotlin.native.Platform.isDebugBinary
17 |
--------------------------------------------------------------------------------
/iosApp/widget/RefreshIntent.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RefreshIntent.swift
3 | // iosApp
4 | //
5 | // Created by Steven Kideckel on 2023-10-22.
6 | // Copyright © 2023 orgName. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import AppIntents
11 | import WidgetKit
12 |
13 | struct RefreshIntent : AppIntent {
14 | static var title: LocalizedStringResource = "Refresh"
15 | static var description = IntentDescription("Updates all the widgets")
16 |
17 | func perform() async throws -> some IntentResult {
18 | WidgetCenter.shared.reloadAllTimelines()
19 | return .result()
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/api/src/commonMain/kotlin/com/sixbynine/transit/path/api/Route.kt:
--------------------------------------------------------------------------------
1 | package com.sixbynine.transit.path.api
2 |
3 | import com.sixbynine.transit.path.util.NonEmptyList
4 |
5 | /**
6 | * A route is a journey taken by a train. It may be considered part of multiple lines.
7 | *
8 | * e.g. 'NWK-WTC' is both a line and a route. However, 'JSQ-33S via HOB' is only a route, and
9 | * includes the 'JSQ-33S' and 'HOB-33S' lines.
10 | */
11 | data class Route(val lines: NonEmptyList, val stops: NonEmptyList)
12 |
13 | val Route.origin: Station get() = stops.first()
14 | val Route.destination: Station get() = stops.last()
15 |
--------------------------------------------------------------------------------
/composeApp/src/androidMain/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | /local.properties
4 | /.idea/caches
5 | /.idea/dictionaries
6 | /.idea/libraries
7 | /.idea/modules.xml
8 | /.idea/workspace.xml
9 | /.idea/navEditor.xml
10 | /.idea/assetWizardSettings.xml
11 | .DS_Store
12 | /build
13 | /captures
14 | .externalNativeBuild
15 | .cxx
16 |
17 | # Built application files
18 | *.apk
19 | *.ap_
20 |
21 | # Files for the ART/Dalvik VM
22 | *.dex
23 |
24 | # Java class files
25 | *.class
26 |
27 | # Generated files
28 | bin/
29 | gen/
30 | out/
31 |
32 | proguard/
33 |
34 | *.log
35 |
36 | .navigation/
37 |
38 | google-services.json
39 |
40 | *.jks
--------------------------------------------------------------------------------
/composeApp/src/androidMain/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #64c6ff
4 | #1896D1
5 | @color/path_blue_500
6 | @android:color/white
7 | @color/path_blue_500
8 | @android:color/black
9 | @android:color/darker_gray
10 | @android:color/transparent
11 |
--------------------------------------------------------------------------------
/composeApp/src/desktopMain/kotlin/main.kt:
--------------------------------------------------------------------------------
1 |
2 | import androidx.compose.runtime.DisposableEffect
3 | import androidx.compose.ui.window.Window
4 | import androidx.compose.ui.window.application
5 | import com.sixbynine.transit.path.app.lifecycle.AppLifecycleObserver
6 |
7 | fun main() = application {
8 | DisposableEffect(Unit) {
9 | AppLifecycleObserver.setAppIsActive(true)
10 |
11 | onDispose {
12 | AppLifecycleObserver.setAppIsActive(false)
13 | }
14 | }
15 | Window(
16 | onCloseRequest = ::exitApplication,
17 | title = "Departures for PATH",
18 | ) {
19 | App()
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/schedule/generator/src/test/kotlin/com/sixbynine/transit/path/schedule/generator/ScheduleParserTest.kt:
--------------------------------------------------------------------------------
1 | package com.sixbynine.transit.path.schedule.generator
2 |
3 | import kotlinx.serialization.encodeToString
4 | import kotlinx.serialization.json.Json
5 | import kotlin.test.Test
6 |
7 | class ScheduleParserTest {
8 |
9 | private val json = Json {
10 |
11 | }
12 |
13 | @Test
14 | fun parsing() {
15 | val text = ScheduleParserTest::class.java.getResource("pathModel.json")!!.readText()
16 |
17 | val schedules = ScheduleParser.parse(text, "regular")
18 |
19 | val json = json.encodeToString(schedules)
20 |
21 | println(json)
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/composeApp/src/androidMain/kotlin/com/sixbynine/transit/path/widget/UpdateWidgetAction.kt:
--------------------------------------------------------------------------------
1 | package com.sixbynine.transit.path.widget
2 |
3 | import android.content.Context
4 | import androidx.glance.GlanceId
5 | import androidx.glance.action.ActionParameters
6 | import androidx.glance.appwidget.action.ActionCallback
7 |
8 | class UpdateWidgetAction : ActionCallback {
9 | override suspend fun onAction(
10 | context: Context,
11 | glanceId: GlanceId,
12 | parameters: ActionParameters
13 | ) {
14 | AndroidWidgetDataRepository.refreshWidgetData(
15 | force = true,
16 | canRefreshLocation = true,
17 | isBackgroundUpdate = false
18 | )
19 | }
20 | }
--------------------------------------------------------------------------------
/settings.gradle.kts:
--------------------------------------------------------------------------------
1 | rootProject.name = "PathWidgetXplat"
2 | enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS")
3 |
4 | pluginManagement {
5 | repositories {
6 | maven("https://maven.pkg.jetbrains.space/public/p/compose/dev")
7 | google()
8 | gradlePluginPortal()
9 | mavenCentral()
10 | }
11 | }
12 |
13 | dependencyResolutionManagement {
14 | repositories {
15 | google()
16 | mavenCentral()
17 | maven("https://maven.pkg.jetbrains.space/public/p/compose/dev")
18 | }
19 | }
20 |
21 | include(":composeApp")
22 | include(":logging")
23 | include(":api")
24 | include(":platform")
25 | include(":flipper")
26 | include(":schedule")
27 | include(":schedule:generator")
28 | include(":test")
--------------------------------------------------------------------------------
/api/src/androidMain/kotlin/com/sixbynine/transit/path/api/AndroidHttpClientFactory.kt:
--------------------------------------------------------------------------------
1 | package com.sixbynine.transit.path.api
2 |
3 | import com.sixbynine.transit.path.flipper.FlipperUtil
4 | import io.ktor.client.HttpClient
5 | import io.ktor.client.HttpClientConfig
6 | import io.ktor.client.engine.okhttp.OkHttp
7 | import okhttp3.Interceptor
8 |
9 | actual fun createHttpClient(
10 | block: HttpClientConfig<*>.() -> Unit
11 | ): HttpClient {
12 | return HttpClient(OkHttp) {
13 | block()
14 |
15 | engine {
16 | val debugInterceptor = FlipperUtil.interceptor()
17 | if (debugInterceptor is Interceptor) {
18 | addInterceptor(debugInterceptor)
19 | }
20 | }
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/test/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | alias(libs.plugins.kotlinMultiplatform)
3 | alias(libs.plugins.androidLibrary)
4 | }
5 |
6 | kotlin {
7 | applyDefaultHierarchyTemplate()
8 |
9 | androidTarget()
10 |
11 | jvm()
12 |
13 | iosX64()
14 | iosArm64()
15 | iosSimulatorArm64()
16 |
17 | sourceSets {
18 | commonMain.dependencies {
19 | implementation(projects.api)
20 | implementation(projects.platform)
21 | }
22 | }
23 | }
24 |
25 | android {
26 | namespace = "com.sixbynine.transit.path.test"
27 |
28 | defaultConfig {
29 | minSdk = libs.versions.android.minSdk.get().toInt()
30 | }
31 |
32 | buildFeatures {
33 | buildConfig = true
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/composeApp/src/androidMain/res/xml-v27/departure_widget.xml:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/api/src/commonMain/kotlin/com/sixbynine/transit/path/api/DepartingTrain.kt:
--------------------------------------------------------------------------------
1 | package com.sixbynine.transit.path.api
2 |
3 | import com.sixbynine.transit.path.model.ColorWrapper
4 | import kotlinx.datetime.Instant
5 | import kotlinx.serialization.Serializable
6 |
7 | data class DepartingTrain(
8 | val headsign: String,
9 | val projectedArrival: Instant,
10 | val lineColors: List,
11 | val isDelayed: Boolean,
12 | val backfillSource: BackfillSource?,
13 | val directionState: State?,
14 | val lines: Set,
15 | )
16 |
17 | @Serializable
18 | data class BackfillSource(
19 | val station: Station,
20 | val projectedArrival: Instant,
21 | )
22 |
23 | val DepartingTrain.terminalStation: Station? get() = Stations.fromHeadSign(headsign)
24 |
--------------------------------------------------------------------------------
/composeApp/src/desktopMain/kotlin/com/sixbynine/transit/path/app/external/ExternalRoutingManager.jvm.kt:
--------------------------------------------------------------------------------
1 | package com.sixbynine.transit.path.app.external
2 |
3 | object JvmExternalRoutingManager : ExternalRoutingManager {
4 | override suspend fun openEmail(): Boolean {
5 | TODO("Not yet implemented")
6 | }
7 |
8 | override suspend fun openUrl(url: String): Boolean {
9 | TODO("Not yet implemented")
10 | }
11 |
12 | override suspend fun shareTextToSystem(text: String): Boolean {
13 | TODO("Not yet implemented")
14 | }
15 |
16 | override suspend fun launchAppRating(): Boolean {
17 | TODO("Not yet implemented")
18 | }
19 | }
20 |
21 | actual fun ExternalRoutingManager(): ExternalRoutingManager = JvmExternalRoutingManager
22 |
--------------------------------------------------------------------------------
/composeApp/src/androidMain/kotlin/com/sixbynine/transit/path/widget/glance/GlanceResources.kt:
--------------------------------------------------------------------------------
1 | package com.sixbynine.transit.path.widget.glance
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.glance.GlanceComposable
5 | import kotlinx.coroutines.runBlocking
6 | import org.jetbrains.compose.resources.StringResource
7 | import org.jetbrains.compose.resources.getString
8 |
9 | // Glance runs on a background thread, so blocking is ok here.
10 | @GlanceComposable
11 | @Composable
12 | fun stringResource(resource: StringResource): String {
13 | return runBlocking { getString(resource) }
14 | }
15 |
16 | @GlanceComposable
17 | @Composable
18 | fun stringResource(resource: StringResource, vararg args: Any): String {
19 | return runBlocking { getString(resource, *args) }
20 | }
--------------------------------------------------------------------------------
/flipper/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | alias(libs.plugins.kotlinMultiplatform)
3 | alias(libs.plugins.androidLibrary)
4 | }
5 |
6 | kotlin {
7 | applyDefaultHierarchyTemplate()
8 |
9 | androidTarget()
10 |
11 | jvm()
12 |
13 | iosX64()
14 | iosArm64()
15 | iosSimulatorArm64()
16 | }
17 |
18 | android {
19 | namespace = "com.sixbynine.transit.path.flipper"
20 |
21 | defaultConfig {
22 | minSdk = libs.versions.android.minSdk.get().toInt()
23 | }
24 |
25 | buildFeatures {
26 | buildConfig = true
27 | }
28 |
29 | dependencies {
30 | debugImplementation(libs.facebook.flipper)
31 | debugImplementation(libs.facebook.soloader)
32 | debugImplementation(libs.facebook.flipper.network.plugin)
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/test/src/commonMain/kotlin/com/sixbynine/transit/path/test/TestPreferences.kt:
--------------------------------------------------------------------------------
1 | package com.sixbynine.transit.path.test
2 |
3 | import com.sixbynine.transit.path.preferences.Preferences
4 | import com.sixbynine.transit.path.preferences.PreferencesKey
5 | import com.sixbynine.transit.path.preferences.testInstance
6 |
7 | object TestPreferences : Preferences {
8 | private val data = mutableMapOf, Any?>()
9 |
10 | override fun set(key: PreferencesKey, value: T?) {
11 | data[key] = value
12 | }
13 |
14 | override fun get(key: PreferencesKey): T? {
15 | return data[key] as T?
16 | }
17 |
18 | override fun clear() {
19 | data.clear()
20 | }
21 |
22 | fun install() {
23 | testInstance = this
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/com/sixbynine/transit/path/app/settings/AppSettings.kt:
--------------------------------------------------------------------------------
1 | package com.sixbynine.transit.path.app.settings
2 |
3 | import com.sixbynine.transit.path.api.Line
4 | import com.sixbynine.transit.path.api.LocationSetting
5 | import com.sixbynine.transit.path.api.StationSort
6 | import com.sixbynine.transit.path.api.TrainFilter
7 |
8 | data class AppSettings(
9 | val locationSetting: LocationSetting,
10 | val trainFilter: TrainFilter,
11 | val lineFilters: Set,
12 | val timeDisplay: TimeDisplay,
13 | val stationLimit: StationLimit,
14 | val stationSort: StationSort,
15 | val displayPresumedTrains: Boolean,
16 | val avoidMissingTrains: AvoidMissingTrains,
17 | val commutingConfiguration: CommutingConfiguration,
18 | val groupTrains: Boolean,
19 | )
20 |
--------------------------------------------------------------------------------
/api/src/commonMain/kotlin/com/sixbynine/transit/path/api/alerts/github/GithubAlertsRepository.kt:
--------------------------------------------------------------------------------
1 | package com.sixbynine.transit.path.api.alerts.github
2 |
3 | import com.sixbynine.transit.path.util.FetchWithPrevious
4 | import com.sixbynine.transit.path.util.RemoteFileRepository
5 | import kotlinx.datetime.Instant
6 | import kotlin.time.Duration.Companion.minutes
7 |
8 | object GithubAlertsRepository {
9 |
10 | private val helper = RemoteFileRepository(
11 | keyPrefix = "github_alerts",
12 | url = "https://raw.githubusercontent.com/steviek/PathWidgetXplat/main/alerts.json",
13 | serializer = GithubAlerts.serializer(),
14 | maxAge = 30.minutes
15 | )
16 |
17 | fun getAlerts(now: Instant): FetchWithPrevious {
18 | return helper.get(now)
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/schedule/src/commonTest/kotlin/Departures.kt:
--------------------------------------------------------------------------------
1 | import kotlinx.datetime.LocalTime
2 |
3 | internal interface Departures {
4 | val nwkWtc: String
5 |
6 | val wtcNewark: String
7 |
8 | val jsq33s: String get() = ""
9 |
10 | val s33Jsq: String get() = ""
11 |
12 | val jsqHob33s: String
13 |
14 | val s33HobJsq: String
15 |
16 | val hob33s: String get() = ""
17 |
18 | val s33Hob: String get() = ""
19 |
20 | val hobWtc: String get() = ""
21 |
22 | val wtcHob: String get() = ""
23 |
24 | val wtcJsq: String get() = ""
25 |
26 | val jsqWtc: String get() = ""
27 |
28 | val nwkHar: String get() = ""
29 |
30 | val harNwk: String get() = ""
31 |
32 | val firstSlowDepartureTime: LocalTime? get() = null
33 |
34 | val lastSlowDepartureTime: LocalTime? get() = null
35 | }
36 |
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/com/sixbynine/transit/path/app/lifecycle/AppLifecycleObserver.kt:
--------------------------------------------------------------------------------
1 | package com.sixbynine.transit.path.app.lifecycle
2 |
3 | import kotlinx.coroutines.flow.MutableStateFlow
4 | import kotlinx.coroutines.flow.asStateFlow
5 | import kotlinx.coroutines.flow.first
6 | import kotlinx.coroutines.withTimeoutOrNull
7 | import kotlin.time.Duration
8 |
9 | object AppLifecycleObserver {
10 |
11 | private val _isActive = MutableStateFlow(false)
12 | val isActive = _isActive.asStateFlow()
13 |
14 | fun setAppIsActive(isActive: Boolean) {
15 | _isActive.value = isActive
16 | }
17 |
18 | suspend fun awaitActive() = isActive.first { it }
19 |
20 | suspend fun awaitInactive(timeout: Duration = Duration.INFINITE) {
21 | withTimeoutOrNull(timeout) { isActive.first { !it } }
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/api/src/commonMain/kotlin/com/sixbynine/transit/path/api/alerts/everbridge/EverbridgeAlertsRepository.kt:
--------------------------------------------------------------------------------
1 | package com.sixbynine.transit.path.api.alerts.everbridge
2 |
3 | import com.sixbynine.transit.path.util.FetchWithPrevious
4 | import com.sixbynine.transit.path.util.RemoteFileRepository
5 | import kotlinx.datetime.Instant
6 | import kotlin.time.Duration.Companion.minutes
7 |
8 | object EverbridgeAlertsRepository {
9 |
10 | private val helper = RemoteFileRepository(
11 | keyPrefix = "everbridge_alerts",
12 | url = "https://panynj.gov/bin/portauthority/everbridge/incidents?status=All&department=Path",
13 | serializer = EverbridgeAlerts.serializer(),
14 | maxAge = 2.minutes
15 | )
16 |
17 | fun getAlerts(now: Instant): FetchWithPrevious {
18 | return helper.get(now)
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/schedule/generator/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | kotlin("jvm")
3 | alias(libs.plugins.kotlinSerialization)
4 | application
5 | }
6 |
7 | kotlin {
8 | jvmToolchain(21)
9 | }
10 |
11 | dependencies {
12 | implementation(projects.api)
13 | implementation(projects.logging)
14 | implementation(projects.schedule)
15 | implementation(projects.platform)
16 |
17 | implementation(libs.kotlin.coroutines)
18 | implementation(libs.kotlin.serialization.json)
19 | implementation(libs.slf4j)
20 | implementation(libs.ksoup)
21 |
22 | testImplementation(projects.test)
23 | testImplementation(libs.kotlin.test)
24 | }
25 |
26 | application {
27 | mainClass.set("com.sixbynine.transit.path.schedule.generator.ScheduleGeneratorKt")
28 | }
29 |
30 | tasks.named("run") {
31 | outputs.dir("build/outputs")
32 | }
33 |
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/com/sixbynine/transit/path/app/ui/layout/LayoutOption.kt:
--------------------------------------------------------------------------------
1 | package com.sixbynine.transit.path.app.ui.layout
2 |
3 | import com.sixbynine.transit.path.preferences.IntPreferencesKey
4 | import com.sixbynine.transit.path.preferences.persisting
5 |
6 | enum class LayoutOption(val number: Int) {
7 | OneColumn(1), TwoColumns(2), ThreeColumns(3)
8 | }
9 |
10 | object LayoutOptionManager {
11 | private val LayoutOptionKey = IntPreferencesKey("layout_option")
12 | private var storedLayoutOption by persisting(LayoutOptionKey)
13 |
14 | var layoutOption: LayoutOption?
15 | get() =
16 | storedLayoutOption
17 | ?.let { storedOption -> LayoutOption.entries.find { it.number == storedOption } }
18 | set(value) {
19 | storedLayoutOption = value?.number
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/iosApp/widget/WidgetConfigurationUtils.swift:
--------------------------------------------------------------------------------
1 | //
2 | // WidgetConfigurationUtils.swift
3 | // widget2Extension
4 | //
5 | // Created by Steven Kideckel on 2023-10-18.
6 | // Copyright © 2023 orgName. All rights reserved.
7 | //
8 |
9 | import WidgetKit
10 |
11 | class WidgetConfigurationUtils {
12 | static func getWidgetLimit(family: WidgetFamily) -> Int {
13 | return switch (family) {
14 | case .systemSmall:
15 | 1
16 | case .systemMedium:
17 | 2
18 | case .systemLarge:
19 | 4
20 | case .systemExtraLarge:
21 | 4
22 | case .accessoryCircular:
23 | 1
24 | case .accessoryRectangular:
25 | 1
26 | case .accessoryInline:
27 | 1
28 | @unknown default:
29 | 1
30 | }
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/composeApp/src/androidMain/res/drawable/ic_one_column.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/logging/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | alias(libs.plugins.kotlinMultiplatform)
3 | alias(libs.plugins.androidLibrary)
4 | alias(libs.plugins.kotlinSerialization)
5 | }
6 |
7 | kotlin {
8 | applyDefaultHierarchyTemplate()
9 |
10 | androidTarget()
11 |
12 | jvm()
13 |
14 | iosX64()
15 | iosArm64()
16 | iosSimulatorArm64()
17 |
18 | sourceSets {
19 | commonMain.dependencies {
20 | implementation(projects.platform)
21 | implementation(libs.kotlin.coroutines)
22 | implementation(libs.kotlin.date.time)
23 | implementation(libs.kotlin.serialization.json)
24 | implementation(libs.napier)
25 | }
26 | }
27 | }
28 |
29 | android {
30 | namespace = "com.sixbynine.transit.path.logging"
31 |
32 | buildFeatures {
33 | buildConfig = true
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/composeApp/src/androidMain/kotlin/com/sixbynine/transit/path/analytics/AndroidAnalytics.kt:
--------------------------------------------------------------------------------
1 | package com.sixbynine.transit.path.analytics
2 |
3 | import com.google.firebase.Firebase
4 | import com.google.firebase.analytics.analytics
5 | import com.google.firebase.analytics.logEvent
6 |
7 | actual fun AnalyticsStrategy(): AnalyticsStrategy = object : AnalyticsStrategy {
8 | override fun logEvent(name: String, params: Map) {
9 | Firebase.analytics.logEvent(name) {
10 | params.forEach { (key, value) ->
11 | when (value) {
12 | is Long -> param(key, value)
13 | is String -> param(key, value)
14 | is Int -> param(key, value.toLong())
15 | is Boolean -> param(key, if (value) "true" else "false")
16 | }
17 | }
18 | }
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/composeApp/src/desktopMain/composeResources/drawable/ic_one_column.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/api/src/commonMain/kotlin/com/sixbynine/transit/path/api/alerts/AlertsRepository.kt:
--------------------------------------------------------------------------------
1 | package com.sixbynine.transit.path.api.alerts
2 |
3 | import com.sixbynine.transit.path.api.alerts.everbridge.EverbridgeAlertsRepository
4 | import com.sixbynine.transit.path.api.alerts.everbridge.toCommonAlert
5 | import com.sixbynine.transit.path.api.alerts.github.GithubAlertsRepository
6 | import com.sixbynine.transit.path.util.FetchWithPrevious
7 | import com.sixbynine.transit.path.util.combine
8 | import kotlinx.datetime.Instant
9 |
10 | object AlertsRepository {
11 | fun getAlerts(now: Instant): FetchWithPrevious> {
12 | return combine(
13 | GithubAlertsRepository.getAlerts(now),
14 | EverbridgeAlertsRepository.getAlerts(now)
15 | ) { githubAlerts, everbridgeAlerts ->
16 | githubAlerts.alerts + everbridgeAlerts.data.map { it.toCommonAlert() }
17 | }
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/test/src/commonMain/kotlin/com/sixbynine/transit/path/test/TestRemoteFileProviderImpl.kt:
--------------------------------------------------------------------------------
1 | package com.sixbynine.transit.path.test
2 |
3 | import com.sixbynine.transit.path.util.TestRemoteFileProvider
4 |
5 | object TestRemoteFileProviderImpl : TestRemoteFileProvider {
6 | override fun getText(url: String): Result {
7 | val lastSlash = url.lastIndexOf('/')
8 | val path = url.substring(lastSlash + 1)
9 | val json = when (path) {
10 | "alerts.json" -> Alerts
11 | "schedule.json" -> Schedule
12 | "schedule_override.json" -> ScheduleOverride
13 | "ridepath.json" -> RidePath
14 | else -> return Result.failure(IllegalArgumentException("No test file for $path"))
15 | }
16 | return Result.success(json)
17 | }
18 | }
19 |
20 | fun TestRemoteFileProvider.Companion.install() {
21 | instance = TestRemoteFileProviderImpl
22 | }
23 |
--------------------------------------------------------------------------------
/composeApp/src/androidMain/res/drawable/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
7 |
11 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/composeApp/src/androidMain/res/values/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
17 |
--------------------------------------------------------------------------------
/schedule/src/commonTest/kotlin/DeparturesParsing.kt:
--------------------------------------------------------------------------------
1 | internal fun parseDeparturesFromNwk(raw: String): String {
2 | val lines = raw.split("\n")
3 | return lines.filter { !it.trim().startsWith("-") }.joinToString(separator = "\n")
4 | }
5 |
6 | internal fun parseDeparturesToNwk(raw: String): String {
7 | val lines = raw.split("\n")
8 | return lines.filter { !it.trim().endsWith("-") }.joinToString(separator = "\n")
9 | }
10 |
11 | internal fun parseDeparturesFromJsq(raw: String): String {
12 | val lines = raw.split("\n")
13 | return lines
14 | .filter { it.trim().startsWith("-") }.joinToString(separator = "\n") { line ->
15 | val firstDigit = line.indexOfFirst { it.isDigit() }
16 | line.substring(startIndex = firstDigit)
17 | }
18 | }
19 |
20 | internal fun parseDeparturesToJsq(raw: String): String {
21 | val lines = raw.split("\n")
22 | return lines.filter { it.trim().endsWith("-") }.joinToString(separator = "\n")
23 | }
24 |
--------------------------------------------------------------------------------
/platform/src/commonMain/kotlin/com/sixbynine/transit/path/util/NonEmptyList.kt:
--------------------------------------------------------------------------------
1 | package com.sixbynine.transit.path.util
2 |
3 | class NonEmptyList internal constructor(private val list: List) : List by list {
4 | init {
5 | require(list.isNotEmpty())
6 | }
7 |
8 | override fun equals(other: Any?): Boolean {
9 | if (other is NonEmptyList<*>) {
10 | return list == other.list
11 | } else {
12 | return list == other
13 | }
14 | }
15 |
16 | override fun hashCode(): Int {
17 | return list.hashCode()
18 | }
19 |
20 | override fun toString(): String {
21 | return list.toString()
22 | }
23 | }
24 |
25 | fun List.toNonEmptyList(): NonEmptyList? {
26 | return if (this.isNotEmpty()) NonEmptyList(this) else null
27 | }
28 |
29 | fun nonEmptyListOf(first: T, vararg others: T): NonEmptyList {
30 | val list = listOf(first) + others
31 | return NonEmptyList(list)
32 | }
33 |
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/com/sixbynine/transit/path/app/ui/common/AppUiTrainData.kt:
--------------------------------------------------------------------------------
1 | package com.sixbynine.transit.path.app.ui.common
2 |
3 | import com.sixbynine.transit.path.api.BackfillSource
4 | import com.sixbynine.transit.path.api.Station
5 | import com.sixbynine.transit.path.model.ColorWrapper
6 | import kotlinx.datetime.Instant
7 |
8 | data class AppUiTrainData(
9 | val id: String,
10 | val title: String,
11 | val colors: List,
12 | val projectedArrival: Instant,
13 | val displayText: String,
14 | val isDelayed: Boolean = false,
15 | val backfill: AppUiBackfillSource? = null,
16 | ) {
17 | val isBackfilled: Boolean
18 | get() = backfill != null
19 | }
20 |
21 | data class AppUiBackfillSource(
22 | val source: BackfillSource,
23 | val displayText: String,
24 | ) {
25 | val projectedArrival: Instant
26 | get() = source.projectedArrival
27 |
28 | val station: Station
29 | get() = source.station
30 | }
31 |
--------------------------------------------------------------------------------
/schedule/generator/src/test/kotlin/com/sixbynine/transit/path/schedule/generator/ScheduleHtmlParserTest.kt:
--------------------------------------------------------------------------------
1 | package com.sixbynine.transit.path.schedule.generator
2 |
3 | import kotlin.test.Test
4 | import kotlin.test.assertNotNull
5 |
6 | class ScheduleHtmlParserTest {
7 | @Test
8 | fun `parse nwk wtc`() {
9 | parseAndPrint("nwkWtcAccordion.html")
10 | }
11 |
12 | @Test
13 | fun `parse jsq hob 33s`() {
14 | parseAndPrint("jsq33sAccordion.html")
15 | }
16 |
17 | private fun parseAndPrint(filename: String) {
18 | val html = ScheduleHtmlParserTest::class.java.getResource(filename)!!.readText()
19 | val result = ScheduleHtmlParser.parseDepartures(html, verboseLogging = false)
20 | assertNotNull(result)
21 |
22 | println("Route:")
23 | println("\tLines:${result.first.lines}")
24 | println("\tStations:${result.first.stops.map { it.pathApiName }}")
25 | println()
26 | println("Departures:")
27 | println(result.second)
28 | }
29 | }
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/com/sixbynine/transit/path/util/TimeUtilities.kt:
--------------------------------------------------------------------------------
1 | package com.sixbynine.transit.path.util
2 |
3 | import kotlinx.datetime.DateTimeUnit
4 | import kotlinx.datetime.Instant
5 | import kotlinx.datetime.LocalDateTime
6 | import kotlinx.datetime.TimeZone
7 | import kotlinx.datetime.plus
8 | import kotlinx.datetime.toInstant
9 | import kotlinx.datetime.toLocalDateTime
10 |
11 | class TimeUtilities {
12 | fun getStartOfNextMinute(time: Instant): Instant {
13 | val timeZone = TimeZone.currentSystemDefault()
14 | return time
15 | .plus(1, DateTimeUnit.MINUTE)
16 | .toLocalDateTime(timeZone)
17 | .floorToMinute()
18 | .toInstant(timeZone)
19 | }
20 |
21 | private fun LocalDateTime.floorToMinute(): LocalDateTime {
22 | return LocalDateTime(
23 | year = year,
24 | monthNumber = monthNumber,
25 | dayOfMonth = dayOfMonth,
26 | hour = hour,
27 | minute = minute
28 | )
29 | }
30 | }
--------------------------------------------------------------------------------
/.github/workflows/update-schedule.yml:
--------------------------------------------------------------------------------
1 | name: schedule-updater
2 |
3 | on:
4 | schedule:
5 | - cron: '0 */12 * * *'
6 | workflow_dispatch:
7 |
8 | jobs:
9 | build:
10 | runs-on: ubuntu-latest
11 | steps:
12 | - name: get repo content
13 | uses: actions/checkout@v2
14 |
15 | - name: setup java
16 | uses: actions/setup-java@v4
17 | with:
18 | distribution: 'temurin'
19 | java-version: '21'
20 |
21 | - name: run schedule generator
22 | run: ./gradlew :schedule:generator:run --no-daemon -Pkotlin.native.ignoreDisabledTargets=true
23 |
24 | - name: put schedule in place
25 | run: |
26 | mv schedule/generator/build/outputs/schedule.json .
27 |
28 | - name: commit
29 | uses: stefanzweifel/git-auto-commit-action@v4
30 | with:
31 | commit_message: 'chore: update schedules'
32 | commit_author: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
33 | file_pattern: 'schedule*.json'
--------------------------------------------------------------------------------
/api/src/commonMain/kotlin/com/sixbynine/transit/path/util/Serialization.kt:
--------------------------------------------------------------------------------
1 | package com.sixbynine.transit.path.util
2 |
3 | import kotlinx.datetime.Instant
4 | import kotlinx.serialization.KSerializer
5 | import kotlinx.serialization.descriptors.PrimitiveKind.LONG
6 | import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
7 | import kotlinx.serialization.encoding.Decoder
8 | import kotlinx.serialization.encoding.Encoder
9 | import kotlinx.serialization.json.Json
10 |
11 | val JsonFormat = Json {
12 | ignoreUnknownKeys = true
13 | explicitNulls = false
14 | isLenient = true
15 | }
16 |
17 | class InstantAsEpochMillisSerializer : KSerializer {
18 | override val descriptor = PrimitiveSerialDescriptor("InstantAsEpochMillis", LONG)
19 |
20 | override fun serialize(encoder: Encoder, value: Instant) {
21 | encoder.encodeLong(value.toEpochMilliseconds())
22 | }
23 |
24 | override fun deserialize(decoder: Decoder): Instant {
25 | return Instant.fromEpochMilliseconds(decoder.decodeLong())
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/com/sixbynine/transit/path/app/ui/ColorRect.kt:
--------------------------------------------------------------------------------
1 | package com.sixbynine.transit.path.app.ui
2 |
3 | import androidx.compose.foundation.background
4 | import androidx.compose.foundation.layout.Box
5 | import androidx.compose.foundation.layout.Column
6 | import androidx.compose.foundation.layout.width
7 | import androidx.compose.runtime.Composable
8 | import androidx.compose.ui.Modifier
9 | import androidx.compose.ui.unit.Dp
10 | import androidx.compose.ui.unit.dp
11 | import com.sixbynine.transit.path.model.ColorWrapper
12 | import com.sixbynine.transit.path.model.unwrap
13 |
14 | @Composable
15 | fun ColorRect(
16 | colors: List,
17 | modifier: Modifier = Modifier
18 | ) {
19 | Column(
20 | modifier
21 | .width(12.dp)
22 | ) {
23 | colors.take(3).forEach { color ->
24 | Box(
25 | Modifier
26 | .width(12.dp)
27 | .weight(1f)
28 | .background(color.unwrap())
29 | )
30 | }
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/api/src/commonMain/kotlin/com/sixbynine/transit/path/api/State.kt:
--------------------------------------------------------------------------------
1 | package com.sixbynine.transit.path.api
2 |
3 | import com.sixbynine.transit.path.api.State.NewJersey
4 | import com.sixbynine.transit.path.api.State.NewYork
5 | import com.sixbynine.transit.path.location.Location
6 |
7 | enum class State {
8 | NewJersey, NewYork
9 | }
10 |
11 | fun State.other(): State = when(this) {
12 | NewJersey -> NewYork
13 | NewYork -> NewJersey
14 | }
15 |
16 | val Station.state: State
17 | get() = if (coordinates.longitude > -74.020) State.NewYork else State.NewJersey
18 |
19 | val Location.state: State
20 | get() = if (longitude > -74.020) State.NewYork else State.NewJersey
21 |
22 | infix fun Station.isEastOf(other: Station): Boolean {
23 | return coordinates.longitude > other.coordinates.longitude
24 | }
25 |
26 | infix fun Station.isWestOf(other: Station): Boolean {
27 | return coordinates.longitude < other.coordinates.longitude
28 | }
29 |
30 | val Station.isInNewJersey get() = state == State.NewJersey
31 | val Station.isInNewYork get() = state == State.NewYork
--------------------------------------------------------------------------------
/iosApp/widget/EmptyDepartureBoardView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // EmptyDepartureBoardView.swift
3 | // widget2Extension
4 | //
5 | // Created by Steven Kideckel on 2023-10-21.
6 | // Copyright © 2023 orgName. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 | import ComposeApp
11 |
12 | struct EmptyDepartureBoardView: View {
13 | let isError: Bool
14 | let isPathError: Bool
15 |
16 | var body: some View {
17 | ZStack(alignment: .center) {
18 | Text(getText())
19 | .font(Font.system(size: (isError ? 12 : 16)))
20 | .multilineTextAlignment(.center)
21 | }
22 | .frame(maxWidth: .infinity, maxHeight: .infinity)
23 | }
24 |
25 | private func getText() -> String {
26 | if isError {
27 | return IosResourceProvider().getEmptyErrorMessage(isPathApiError: isPathError)
28 | } else {
29 | return IosResourceProvider().getEmptyStateString()
30 | }
31 | }
32 | }
33 |
34 | #Preview {
35 | EmptyDepartureBoardView(isError: false, isPathError: false)
36 | }
37 |
--------------------------------------------------------------------------------
/composeApp/src/androidMain/kotlin/com/sixbynine/transit/path/widget/StationDataComparator.kt:
--------------------------------------------------------------------------------
1 | package com.sixbynine.transit.path.widget
2 |
3 | import com.sixbynine.transit.path.api.StationSort
4 | import com.sixbynine.transit.path.api.Stations
5 | import com.sixbynine.transit.path.time.now
6 | import com.sixbynine.transit.path.model.DepartureBoardData.StationData
7 | import kotlinx.datetime.LocalDateTime
8 | import kotlinx.datetime.TimeZone
9 | import kotlinx.datetime.toLocalDateTime
10 |
11 | class StationDataComparator(
12 | order: StationSort?,
13 | now: LocalDateTime = now().toLocalDateTime(TimeZone.currentSystemDefault()),
14 | ) : Comparator {
15 |
16 | // TODO: Fix this
17 | private val delegate = StationComparator(order, null, now)
18 |
19 | override fun compare(data1: StationData, data2: StationData): Int {
20 | val first = Stations.All.firstOrNull { it.pathApiName == data1.id } ?: return 0
21 | val second = Stations.All.firstOrNull { it.pathApiName == data2.id } ?: return 0
22 | return delegate.compare(first, second)
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/iosApp/widget/PathWidgetBundle.swift:
--------------------------------------------------------------------------------
1 | //
2 | // widgetBundle.swift
3 | // widget
4 | //
5 | // Created by Steven Kideckel on 2023-11-27.
6 | // Copyright © 2023 orgName. All rights reserved.
7 | //
8 |
9 | import WidgetKit
10 | import SwiftUI
11 | import ComposeApp
12 |
13 | @main
14 | struct PathWidgetBundle: WidgetBundle {
15 |
16 | init() {
17 | let locationHelper = LocationHelper()
18 | locationHelper.isWidget = true
19 | IosLocationProvider().requestDelegate = locationHelper
20 |
21 | var firstDayOfWeek: String? = nil
22 | if #available(iOS 16, *) {
23 | firstDayOfWeek = Locale.current.firstDayOfWeek.rawValue
24 | }
25 | IOSPlatform().setFirstDayOfWeek(firstDayOfWeek: firstDayOfWeek)
26 |
27 | NativeHolder().initialize(
28 | widgetReloader: IosWidgetReloader(),
29 | nonFatalReporter: { e in
30 |
31 | }
32 | )
33 | }
34 |
35 | var body: some Widget {
36 | CommuteWidget()
37 | DepartureWidget()
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/composeApp/src/androidMain/kotlin/com/sixbynine/transit/path/widget/glance/Text.kt:
--------------------------------------------------------------------------------
1 | package com.sixbynine.transit.path.widget.glance
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.compose.ui.unit.TextUnit
5 | import androidx.glance.GlanceModifier
6 | import androidx.glance.text.TextAlign
7 | import androidx.glance.text.TextDefaults
8 | import androidx.glance.text.TextStyle
9 | import androidx.glance.unit.ColorProvider
10 |
11 | @Composable
12 | fun Text(
13 | text: String,
14 | modifier: GlanceModifier = GlanceModifier,
15 | style: TextStyle = TextDefaults.defaultTextStyle,
16 | maxLines: Int = Int.MAX_VALUE,
17 | fontSize: TextUnit? = null,
18 | textAlign: TextAlign? = null,
19 | color: ColorProvider? = null,
20 | ) {
21 | androidx.glance.text.Text(
22 | text = text,
23 | modifier = modifier,
24 | style = style.copy(
25 | color = color ?: style.color,
26 | fontSize = fontSize ?: style.fontSize,
27 | textAlign = textAlign ?: style.textAlign,
28 | ),
29 | maxLines = maxLines,
30 | )
31 | }
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/com/sixbynine/transit/path/app/ui/station/StationContract.kt:
--------------------------------------------------------------------------------
1 | package com.sixbynine.transit.path.app.ui.station
2 |
3 | import com.sixbynine.transit.path.app.settings.TimeDisplay
4 | import com.sixbynine.transit.path.app.ui.ScreenScope
5 | import com.sixbynine.transit.path.app.ui.common.AppUiTrainData
6 | import com.sixbynine.transit.path.app.ui.home.HomeScreenContract.StationData
7 | import com.sixbynine.transit.path.app.ui.station.StationContract.Intent
8 | import com.sixbynine.transit.path.app.ui.station.StationContract.State
9 |
10 | object StationContract {
11 | data class State(
12 | val station: StationData? = null,
13 | val trainsMatchingFilters: List,
14 | val otherTrains: List,
15 | val timeDisplay: TimeDisplay,
16 | val groupByDestination: Boolean,
17 | )
18 |
19 | sealed interface Intent {
20 | data object BackClicked : Intent
21 |
22 | }
23 |
24 | sealed interface Effect {
25 | data object GoBack : Effect
26 | }
27 | }
28 |
29 | typealias StationScope = ScreenScope
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/com/sixbynine/transit/path/app/ui/BaseViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.sixbynine.transit.path.app.ui
2 |
3 | import kotlinx.coroutines.channels.Channel
4 | import kotlinx.coroutines.flow.MutableStateFlow
5 | import kotlinx.coroutines.flow.asStateFlow
6 | import kotlinx.coroutines.flow.receiveAsFlow
7 | import kotlinx.coroutines.flow.update
8 | import kotlinx.coroutines.launch
9 | import kotlinx.coroutines.sync.Mutex
10 |
11 | abstract class BaseViewModel(
12 | initialState: State
13 | ) : PathViewModel() {
14 |
15 | private val _state = MutableStateFlow(initialState)
16 | final override val state = _state.asStateFlow()
17 | private val stateMutex = Mutex()
18 |
19 |
20 |
21 | private val _effects = Channel()
22 | final override val effects = _effects.receiveAsFlow()
23 |
24 | protected fun sendEffect(effect: Effect) {
25 | lightweightScope.launch { _effects.send(effect) }
26 | }
27 |
28 | protected fun updateState(block: State.() -> State) {
29 | _state.update(block)
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/iosApp/iosApp/HomeView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // HomeView.swift
3 | // iosApp
4 | //
5 | // Created by Steven Kideckel on 2023-10-23.
6 | // Copyright © 2023 orgName. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 |
11 | struct HomeView: View {
12 | var body: some View {
13 | ZStack {
14 | VStack {
15 | Spacer()
16 | Text(getString(strings().welcome_text))
17 | .multilineTextAlignment(.center)
18 | Spacer()
19 | }
20 |
21 |
22 | VStack {
23 | Spacer()
24 | Button(action: emailSupport) {
25 | Text(getString(strings().report_a_problem))
26 | }
27 | .padding()
28 | Spacer().frame(height: 8)
29 | }
30 | }
31 | .padding()
32 | }
33 |
34 | private func emailSupport() {
35 | if let url = URL(string: "mailto:sixbynineapps@gmail.com") {
36 | UIApplication.shared.open(url)
37 | }
38 | }
39 | }
40 |
41 | #Preview {
42 | HomeView()
43 | }
44 |
--------------------------------------------------------------------------------
/api/src/commonMain/kotlin/com/sixbynine/transit/path/util/RemoteFileReading.kt:
--------------------------------------------------------------------------------
1 | package com.sixbynine.transit.path.util
2 |
3 | import com.sixbynine.transit.path.api.NetworkException
4 | import com.sixbynine.transit.path.api.createHttpClient
5 | import io.ktor.client.request.get
6 | import io.ktor.client.statement.bodyAsText
7 | import io.ktor.http.isSuccess
8 | import kotlinx.coroutines.Dispatchers
9 | import kotlinx.coroutines.IO
10 | import kotlinx.coroutines.withContext
11 | import kotlinx.coroutines.withTimeout
12 | import kotlin.time.Duration.Companion.seconds
13 |
14 | private val httpClient by lazy { createHttpClient() }
15 |
16 | suspend fun readRemoteFile(url: String): Result = suspendRunCatching {
17 | TestRemoteFileProvider.instance
18 | ?.getText(url)
19 | ?.let { return@suspendRunCatching it.getOrThrow() }
20 |
21 | withContext(Dispatchers.IO) {
22 | val response = withTimeout(5.seconds) { httpClient.get(url) }
23 |
24 | if (!response.status.isSuccess()) {
25 | throw NetworkException(response.status.toString())
26 | }
27 |
28 | response.bodyAsText()
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/api/src/commonMain/kotlin/com/sixbynine/transit/path/api/templine/HobClosureConfigRepository.kt:
--------------------------------------------------------------------------------
1 | package com.sixbynine.transit.path.api.templine
2 |
3 | import com.sixbynine.transit.path.time.now
4 | import com.sixbynine.transit.path.util.RemoteFileRepository
5 | import com.sixbynine.transit.path.util.await
6 | import kotlinx.coroutines.GlobalScope
7 | import kotlinx.coroutines.delay
8 | import kotlinx.coroutines.launch
9 | import kotlin.time.Duration.Companion.hours
10 | import kotlin.time.Duration.Companion.seconds
11 |
12 | object HobClosureConfigRepository {
13 |
14 | init {
15 | GlobalScope.launch {
16 | delay(2.seconds)
17 | helper.get(now()).await()
18 | }
19 | }
20 |
21 | private val helper = RemoteFileRepository(
22 | keyPrefix = "hob_closue_config",
23 | url = "https://raw.githubusercontent.com/steviek/PathWidgetXplat/main/hob_closure.json",
24 | serializer = HobClosureConfig.serializer(),
25 | maxAge = 1.hours,
26 | )
27 |
28 | fun getConfig(): HobClosureConfig {
29 | return helper.get(now()).previous?.value ?: HobClosureConfig.fallback
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/composeApp/src/androidMain/kotlin/com/sixbynine/transit/path/widget/glance/Theme.kt:
--------------------------------------------------------------------------------
1 | package com.sixbynine.transit.path.widget.glance
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.compose.runtime.CompositionLocalProvider
5 | import androidx.compose.runtime.ReadOnlyComposable
6 | import androidx.glance.GlanceComposable
7 | import androidx.glance.color.ColorProviders
8 |
9 | object GlanceTheme {
10 | val colors: ColorProviders
11 | @GlanceComposable
12 | @Composable
13 | @ReadOnlyComposable
14 | get() = androidx.glance.GlanceTheme.colors
15 |
16 | val typography: Typography
17 | @GlanceComposable
18 | @Composable
19 | @ReadOnlyComposable
20 | get() = LocalTypography.current
21 | }
22 |
23 | @Composable
24 | fun GlanceTheme(
25 | colors: ColorProviders = androidx.glance.GlanceTheme.colors,
26 | content: @GlanceComposable @Composable () -> Unit
27 | ) {
28 | androidx.glance.GlanceTheme(colors) {
29 | CompositionLocalProvider(
30 | LocalTypography provides createTypography(),
31 | content = content
32 | )
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/composeApp/src/androidMain/kotlin/com/sixbynine/transit/path/widget/configuration/StoredWidgetConfiguration.kt:
--------------------------------------------------------------------------------
1 | package com.sixbynine.transit.path.widget.configuration
2 |
3 | import com.sixbynine.transit.path.api.Line
4 | import com.sixbynine.transit.path.api.StationSort
5 | import com.sixbynine.transit.path.api.TrainFilter
6 | import com.sixbynine.transit.path.preferences.IntPersistable
7 | import kotlinx.serialization.Serializable
8 | import kotlin.contracts.contract
9 |
10 | /** The persisted data for a widget. */
11 | @Serializable
12 | data class StoredWidgetConfiguration(
13 | val fixedStations: Set? = null,
14 | private val linesBitmask: Int? = null,
15 | val useClosestStation: Boolean = false,
16 | val sortOrder: StationSort? = null,
17 | val filter: TrainFilter? = null,
18 | val version: Int = 1,
19 | ) {
20 | val lines: Set
21 | get() = IntPersistable.fromBitmask(linesBitmask ?: 0)
22 | }
23 |
24 | fun StoredWidgetConfiguration?.needsSetup(): Boolean {
25 | contract { returns(false) implies (this@needsSetup != null) }
26 | return this == null || (fixedStations.isNullOrEmpty() && !useClosestStation)
27 | }
28 |
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/com/sixbynine/transit/path/native/NativeHolder.kt:
--------------------------------------------------------------------------------
1 | package com.sixbynine.transit.path.native
2 |
3 | import com.sixbynine.transit.path.Logging
4 | import com.sixbynine.transit.path.NonFatalReporter
5 | import com.sixbynine.transit.path.api.templine.HobClosureConfigRepository
6 | import com.sixbynine.transit.path.widget.WidgetReloader
7 | import kotlinx.coroutines.flow.MutableStateFlow
8 | import kotlinx.coroutines.flow.StateFlow
9 |
10 | object NativeHolder {
11 |
12 | val widgetReloader = MutableStateFlow(null)
13 |
14 | fun initialize(
15 | widgetReloader: WidgetReloader,
16 | nonFatalReporter: (Throwable) -> Unit,
17 | ) {
18 | this.widgetReloader.value = widgetReloader
19 |
20 | // kick start some initialization here
21 | HobClosureConfigRepository.getConfig()
22 |
23 | Logging.nonFatalReporter = NonFatalReporter { e -> nonFatalReporter(e) }
24 | }
25 | }
26 |
27 | val widgetReloader: WidgetReloader
28 | get() = getInitialized(NativeHolder.widgetReloader)
29 |
30 | private fun getInitialized(ref: StateFlow): T {
31 | return checkNotNull(ref.value) { "not initialized" }
32 | }
33 |
--------------------------------------------------------------------------------
/platform/src/commonMain/kotlin/com/sixbynine/transit/path/util/GlobalExtensions.kt:
--------------------------------------------------------------------------------
1 | package com.sixbynine.transit.path.util
2 |
3 | import kotlinx.datetime.LocalDateTime
4 | import kotlinx.datetime.LocalTime
5 | import kotlinx.datetime.atTime
6 |
7 | inline fun T?.orElse(other: () -> T): T {
8 | return this ?: other()
9 | }
10 |
11 | inline fun T.runIf(condition: Boolean, block: T.() -> T): T {
12 | return if (condition) {
13 | block()
14 | } else {
15 | this
16 | }
17 | }
18 |
19 | inline fun T.runUnless(condition: Boolean, block: T.() -> T) = runIf(!condition) { block() }
20 |
21 | inline fun ifNotNull(first: A?, second: B?, transform: (A, B) -> C): C? {
22 | first ?: return null
23 | second ?: return null
24 | return transform(first, second)
25 | }
26 |
27 | fun Result>.flatten(): Result {
28 | return fold(
29 | onSuccess = { it },
30 | onFailure = { Result.failure(it) }
31 | )
32 | }
33 |
34 | fun LocalDateTime.dropSubSeconds(): LocalDateTime {
35 | return date.atTime(time.dropSubSeconds())
36 | }
37 |
38 | fun LocalTime.dropSubSeconds(): LocalTime {
39 | return LocalTime(hour, minute, second)
40 | }
41 |
--------------------------------------------------------------------------------
/composeApp/src/commonTest/kotlin/com/sixbynine/transit/path/app/ui/ColorsTest.kt:
--------------------------------------------------------------------------------
1 | package com.sixbynine.transit.path.app.ui
2 |
3 | import com.sixbynine.transit.path.model.Colors
4 | import com.sixbynine.transit.path.model.Colors.approxEquals
5 | import kotlin.test.Test
6 | import kotlin.test.assertFalse
7 | import kotlin.test.assertTrue
8 |
9 | class ColorsTest {
10 | @Test
11 | fun approxEquals() {
12 | val c1 = Colors.parse("#4287f5")
13 | val c2 = Colors.parse("#3a80f0")
14 | assertTrue(c1 approxEquals c2)
15 |
16 | val c3 = Colors.parse("#053685")
17 | assertFalse(c1 approxEquals c3)
18 |
19 | val c4 = Colors.parse("#d00dd6")
20 | assertFalse(c1 approxEquals c4)
21 | assertFalse(c3 approxEquals c4)
22 |
23 | val c5 = Colors.parse("#782C94")
24 | val c6 = Colors.parse("#6b128c")
25 | assertTrue(c5 approxEquals c6)
26 | assertFalse(c4 approxEquals c6)
27 |
28 | val c7 = Colors.parse("#19bf32")
29 | assertFalse(c7 approxEquals c6)
30 | assertFalse(c7 approxEquals c5)
31 | assertFalse(c7 approxEquals c4)
32 | assertFalse(c7 approxEquals c3)
33 | assertFalse(c7 approxEquals c2)
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/platform/src/commonTest/kotlin/com/sixbynine/transit/path/time/UserPreferenceDayOfWeekComparatorTest.kt:
--------------------------------------------------------------------------------
1 | package com.sixbynine.transit.path.time
2 |
3 | import kotlinx.datetime.DayOfWeek.MONDAY
4 | import kotlinx.datetime.DayOfWeek.SATURDAY
5 | import kotlinx.datetime.DayOfWeek.SUNDAY
6 | import kotlinx.datetime.DayOfWeek.TUESDAY
7 | import kotlin.test.Test
8 | import kotlin.test.assertTrue
9 |
10 | class UserPreferenceDayOfWeekComparatorTest {
11 | @Test
12 | fun `monday as start`() {
13 | val comparator = UserPreferenceDayOfWeekComparator(MONDAY)
14 | assertTrue { comparator.compare(MONDAY, TUESDAY) < 0 }
15 | assertTrue { comparator.compare(SUNDAY, TUESDAY) > 0 }
16 | assertTrue { comparator.compare(MONDAY, SUNDAY) < 0 }
17 | }
18 |
19 | @Test
20 | fun `sunday as start`() {
21 | val comparator = UserPreferenceDayOfWeekComparator(SUNDAY)
22 | assertTrue { comparator.compare(MONDAY, TUESDAY) < 0 }
23 | assertTrue { comparator.compare(SUNDAY, TUESDAY) < 0 }
24 | assertTrue { comparator.compare(SUNDAY, SATURDAY) < 0 }
25 | assertTrue { comparator.compare(MONDAY, SUNDAY) > 0 }
26 | assertTrue { comparator.compare(SATURDAY, SATURDAY) == 0 }
27 | }
28 | }
--------------------------------------------------------------------------------
/composeApp/src/androidMain/kotlin/com/sixbynine/transit/path/widget/StartConfigurationActivityAction.kt:
--------------------------------------------------------------------------------
1 | package com.sixbynine.transit.path.widget
2 |
3 | import android.appwidget.AppWidgetManager
4 | import android.content.Intent
5 | import androidx.compose.runtime.Composable
6 | import androidx.glance.LocalContext
7 | import androidx.glance.LocalGlanceId
8 | import androidx.glance.action.Action
9 | import androidx.glance.appwidget.GlanceAppWidgetManager
10 | import androidx.glance.appwidget.action.actionStartActivity
11 | import com.sixbynine.transit.path.widget.setup.WidgetSetupActivity
12 |
13 | @Composable
14 | fun startConfigurationActivityAction(): Action {
15 | val context = LocalContext.current
16 | val appWidgetManager = GlanceAppWidgetManager(context)
17 | val appWidgetId = appWidgetManager.getAppWidgetId(LocalGlanceId.current)
18 | val configurationIntent =
19 | Intent(AppWidgetManager.ACTION_APPWIDGET_CONFIGURE)
20 | .setClass(LocalContext.current, WidgetSetupActivity::class.java)
21 | .putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId)
22 | .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK)
23 | return actionStartActivity(configurationIntent)
24 | }
--------------------------------------------------------------------------------
/iosApp/iosApp/ContentView.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 | import SwiftUI
3 | import ComposeApp
4 |
5 | struct ComposeView: UIViewControllerRepresentable {
6 | func makeUIViewController(context: Context) -> UIViewController {
7 | MainViewControllerKt.MainViewController()
8 | }
9 |
10 | func updateUIViewController(_ uiViewController: UIViewController, context: Context) {}
11 | }
12 |
13 | struct ContentView: View {
14 | var body: some View {
15 | ComposeView()
16 | .onAppear {
17 | AppLifecycleObserver().setAppIsActive(isActive: true)
18 | }
19 | .onDisappear {
20 | AppLifecycleObserver().setAppIsActive(isActive: false)
21 | }
22 | .onReceive(NotificationCenter.default.publisher(for: UIApplication.didBecomeActiveNotification)) { _ in
23 | AppLifecycleObserver().setAppIsActive(isActive: true)
24 | }
25 | .onReceive(NotificationCenter.default.publisher(for: UIApplication.willResignActiveNotification)) { _ in
26 | AppLifecycleObserver().setAppIsActive(isActive: false)
27 | }
28 | .ignoresSafeArea(.all) // Compose has own keyboard handler
29 | }
30 | }
31 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/iosApp/widget/EntryView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // EntryView.swift
3 | // widgetExtension
4 | //
5 | // Created by Steven Kideckel on 2024-06-19.
6 | // Copyright © 2024 orgName. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 | import ComposeApp
11 |
12 | protocol EntryView : View {
13 | var entry: DepartureBoardWidgetEntry { get }
14 | }
15 |
16 | extension EntryView {
17 | func measureTextHeight(text: String, font: UIFont) -> CGFloat {
18 | measureTextSize(text: text, font: font).height
19 | }
20 |
21 | func measureTextWidth(text: String, font: UIFont) -> CGFloat {
22 | measureTextSize(text: text, font: font).width
23 | }
24 |
25 | func measureTextSize(text: String, font: UIFont) -> CGRect {
26 | measureTextSize(maxSize: entry.size, text: text, font: font)
27 | }
28 |
29 | func formatTrainTime(_ time: Kotlinx_datetimeInstant) -> String {
30 | if (entry.configuration.timeDisplay == .clock) {
31 | return WidgetDataFormatter().formatTime(instant: time)
32 | } else {
33 | return WidgetDataFormatter().formatRelativeTime(
34 | now: entry.date.toKotlinInstant(),
35 | time: time
36 | )
37 | }
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/platform/src/commonMain/kotlin/com/sixbynine/transit/path/preferences/IntPersistable.kt:
--------------------------------------------------------------------------------
1 | package com.sixbynine.transit.path.preferences
2 |
3 | import kotlin.enums.EnumEntries
4 | import kotlin.enums.enumEntries
5 |
6 | interface IntPersistable {
7 | val number: Int
8 |
9 | companion object {
10 | fun fromPersistence(
11 | number: Int,
12 | entries: EnumEntries
13 | ): E? where E : Enum, E : IntPersistable {
14 | return entries.find { it.number == number }
15 | }
16 |
17 | inline fun fromPersistence(
18 | number: Int
19 | ): E? where E : Enum, E : IntPersistable {
20 | return fromPersistence(number, enumEntries())
21 | }
22 |
23 | inline fun createBitmask(
24 | values: Collection
25 | ): Int where E : Enum, E : IntPersistable {
26 | return values.fold(0) { acc, e -> acc or (1 shl e.number) }
27 | }
28 |
29 | inline fun fromBitmask(
30 | mask: Int
31 | ): Set where E : Enum, E : IntPersistable {
32 | return enumEntries().filter { (1 shl it.number) and mask != 0 }.toSet()
33 | }
34 | }
35 | }
36 |
37 |
--------------------------------------------------------------------------------
/platform/src/androidMain/kotlin/com/sixbynine/transit/path/time/AndroidTimeUtils.kt:
--------------------------------------------------------------------------------
1 | package com.sixbynine.transit.path.time
2 |
3 | import android.text.format.DateFormat
4 | import androidx.core.text.util.LocalePreferences
5 | import androidx.core.text.util.LocalePreferences.FirstDayOfWeek
6 | import com.sixbynine.transit.path.PathApplication
7 | import com.sixbynine.transit.path.PreviewContext
8 | import kotlinx.datetime.DayOfWeek
9 |
10 | object AndroidPlatformTimeUtils : PlatformTimeUtils {
11 | override fun is24HourClock(): Boolean {
12 | return DateFormat.is24HourFormat(PreviewContext ?: PathApplication.instance)
13 | }
14 |
15 | override fun getFirstDayOfWeek(): DayOfWeek = when (LocalePreferences.getFirstDayOfWeek()) {
16 | FirstDayOfWeek.MONDAY -> DayOfWeek.MONDAY
17 | FirstDayOfWeek.TUESDAY -> DayOfWeek.TUESDAY
18 | FirstDayOfWeek.WEDNESDAY -> DayOfWeek.WEDNESDAY
19 | FirstDayOfWeek.THURSDAY -> DayOfWeek.THURSDAY
20 | FirstDayOfWeek.FRIDAY -> DayOfWeek.FRIDAY
21 | FirstDayOfWeek.SATURDAY -> DayOfWeek.SATURDAY
22 | FirstDayOfWeek.SUNDAY -> DayOfWeek.SUNDAY
23 | else -> DayOfWeek.SUNDAY
24 | }
25 | }
26 |
27 | actual fun getPlatformTimeUtils(): PlatformTimeUtils {
28 | return AndroidPlatformTimeUtils
29 | }
30 |
--------------------------------------------------------------------------------
/composeApp/src/androidMain/kotlin/com/sixbynine/transit/path/app/ui/advancessettings/AdvancedSettingsPreview.kt:
--------------------------------------------------------------------------------
1 | package com.sixbynine.transit.path.app.ui.advancessettings
2 |
3 | import androidx.compose.runtime.Composable
4 | import com.sixbynine.transit.path.PathWidgetPreview
5 | import com.sixbynine.transit.path.PreviewTheme
6 | import com.sixbynine.transit.path.app.settings.AvoidMissingTrains.OffPeak
7 | import com.sixbynine.transit.path.app.settings.CommutingConfiguration
8 | import com.sixbynine.transit.path.app.settings.StationLimit.Six
9 | import com.sixbynine.transit.path.app.settings.TimeDisplay.Clock
10 | import com.sixbynine.transit.path.app.ui.advancedsettings.AdvancedSettingsContract.State
11 | import com.sixbynine.transit.path.app.ui.advancedsettings.AdvancedSettingsScreen
12 |
13 | @PathWidgetPreview
14 | @Composable
15 | fun AdvancedSettingsPreview() {
16 | PreviewTheme {
17 | AdvancedSettingsScreen(
18 | state = State(
19 | avoidMissingTrains = OffPeak,
20 | stationLimit = Six,
21 | timeDisplay = Clock,
22 | groupTrains = true,
23 | commutingConfiguration = CommutingConfiguration.default()
24 | ),
25 | onIntent = {
26 |
27 | }
28 | )
29 | }
30 | }
--------------------------------------------------------------------------------
/iosApp/widget/StationTitle.swift:
--------------------------------------------------------------------------------
1 | //
2 | // StationTitle.swift
3 | // widgetExtension
4 | //
5 | // Created by Steven Kideckel on 2024-06-19.
6 | // Copyright © 2024 orgName. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 | import ComposeApp
11 |
12 | struct StationTitle: View {
13 | let title: String
14 | let width: CGFloat
15 | let maxHeight: CGFloat
16 |
17 | var body: some View {
18 | HStack(spacing: 0) {
19 | Spacer()
20 | Text(
21 | WidgetDataFormatter().formatHeadSign(
22 | title: title,
23 | fits: {
24 | let titleSpace = width - 16
25 | let textWidth = measureTextWidth(
26 | maxSize: CGSize(width: width, height: maxHeight),
27 | text: $0,
28 | font: UIFont.systemFont(ofSize: 14, weight: .bold)
29 | )
30 | return (textWidth <= titleSpace).toKotlinBoolean()
31 | }
32 | )
33 | )
34 | .multilineTextAlignment(.center)
35 | .font(Font.system(size: 14))
36 | .fontWeight(.bold)
37 | Spacer()
38 | }
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/com/sixbynine/transit/path/widget/StationByDisplayNameComparator.kt:
--------------------------------------------------------------------------------
1 | package com.sixbynine.transit.path.widget
2 |
3 | import com.sixbynine.transit.path.api.Station
4 |
5 | /** Comparator that sorts [Station]s by their display names. */
6 | object StationByDisplayNameComparator : Comparator {
7 | override fun compare(first: Station, second: Station): Int {
8 | // Station names that start with digits should come at the end.
9 | val firstIsDigit = first.displayName.first().isDigit()
10 | val secondIsDigit = second.displayName.first().isDigit()
11 | if (firstIsDigit && !secondIsDigit) {
12 | return 1
13 | }
14 |
15 | if (secondIsDigit && !firstIsDigit) {
16 | return -1
17 | }
18 |
19 | if (!firstIsDigit) {
20 | return first.displayName.compareTo(second.displayName)
21 | }
22 |
23 | // If both the names start with digits, compare by the numerical value. This ensures that i.e.
24 | // '9th St' comes before '14th St'.
25 | val firstNumber = first.displayName.takeWhile { it.isDigit() }.toInt()
26 | val secondNumber = second.displayName.takeWhile { it.isDigit() }.toInt()
27 | return firstNumber.compareTo(secondNumber)
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/composeApp/src/androidMain/kotlin/com/sixbynine/transit/path/BaseActivity.kt:
--------------------------------------------------------------------------------
1 | package com.sixbynine.transit.path
2 |
3 | import android.Manifest.permission.ACCESS_COARSE_LOCATION
4 | import android.Manifest.permission.ACCESS_FINE_LOCATION
5 | import android.os.Bundle
6 | import androidx.activity.ComponentActivity
7 | import androidx.activity.result.ActivityResultLauncher
8 | import androidx.activity.result.contract.ActivityResultContracts.RequestMultiplePermissions
9 | import com.sixbynine.transit.path.location.AndroidLocationProvider
10 |
11 | abstract class BaseActivity : ComponentActivity() {
12 |
13 | private lateinit var locationPermissionRequest: ActivityResultLauncher>
14 |
15 | override fun onCreate(savedInstanceState: Bundle?) {
16 | super.onCreate(savedInstanceState)
17 | locationPermissionRequest =
18 | registerForActivityResult(RequestMultiplePermissions()) { permissions ->
19 | AndroidLocationProvider.onLocationPermissionResult(
20 | permissions[ACCESS_FINE_LOCATION] == true ||
21 | permissions[ACCESS_COARSE_LOCATION] == true
22 | )
23 | }
24 | }
25 |
26 | fun requestLocationPermissions() {
27 | locationPermissionRequest.launch(arrayOf(ACCESS_FINE_LOCATION, ACCESS_COARSE_LOCATION))
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/composeApp/src/androidMain/kotlin/com/sixbynine/transit/path/widget/GlanceExtensions.kt:
--------------------------------------------------------------------------------
1 | package com.sixbynine.transit.path.widget
2 |
3 | import androidx.annotation.ColorInt
4 | import androidx.compose.ui.graphics.Color
5 | import androidx.datastore.preferences.core.MutablePreferences
6 | import androidx.glance.GlanceId
7 | import androidx.glance.appwidget.GlanceAppWidget
8 | import androidx.glance.appwidget.GlanceAppWidgetManager
9 | import androidx.glance.appwidget.state.updateAppWidgetState
10 | import com.sixbynine.transit.path.MobilePathApplication
11 |
12 | suspend inline fun updateAppWidgetStates(
13 | crossinline updateState: suspend (MutablePreferences, GlanceId) -> Unit,
14 | ) {
15 | val context = MobilePathApplication.instance
16 | GlanceAppWidgetManager(context)
17 | .getGlanceIds(T::class.java)
18 | .forEach { glanceId ->
19 | updateAppWidgetState(context, glanceId) { preferences ->
20 | updateState(preferences, glanceId)
21 | }
22 | }
23 | }
24 |
25 | fun @receiver:ColorInt Int.toColor(): Color {
26 | return Color(
27 | red = android.graphics.Color.red(this),
28 | green = android.graphics.Color.green(this),
29 | blue = android.graphics.Color.blue(this),
30 | alpha = android.graphics.Color.alpha(this)
31 | )
32 | }
33 |
--------------------------------------------------------------------------------
/composeApp/src/androidMain/kotlin/com/sixbynine/transit/path/widget/WidgetMeasurements.kt:
--------------------------------------------------------------------------------
1 | package com.sixbynine.transit.path.widget
2 |
3 | import android.content.Context
4 | import android.hardware.display.DisplayManager
5 | import android.os.Build.VERSION
6 | import android.view.Display
7 | import android.widget.TextView
8 | import androidx.compose.ui.unit.Dp
9 | import androidx.compose.ui.unit.TextUnit
10 | import androidx.compose.ui.unit.dp
11 | import kotlin.math.ceil
12 |
13 | /** Best guess at how wide [text] is when drawn with [size]. */
14 | fun estimateTextWidth(context: Context, text: String, size: TextUnit): Dp {
15 | val textView = TextView(context.asUiContext())
16 | if (VERSION.SDK_INT >= 29) {
17 | textView.setTextAppearance(android.R.style.Theme_DeviceDefault_DayNight)
18 | }
19 | require(size.isSp)
20 | textView.textSize = size.value
21 | return context.pxToDp(textView.paint.measureText(text))
22 | }
23 |
24 | private fun Context.asUiContext(): Context {
25 | val displayManager = getSystemService(Context.DISPLAY_SERVICE) as DisplayManager
26 | val defaultDisplay = displayManager.getDisplay(Display.DEFAULT_DISPLAY)
27 | return createDisplayContext(defaultDisplay)
28 | }
29 |
30 | private fun Context.pxToDp(px: Float): Dp {
31 | val density = resources.displayMetrics.density
32 | return ceil(px / density).dp
33 | }
--------------------------------------------------------------------------------
/platform/src/jvmMain/kotlin/com/sixbynine/transit/path/util/GlobalDataStore.jvm.kt:
--------------------------------------------------------------------------------
1 | package com.sixbynine.transit.path.util
2 |
3 | import com.sixbynine.transit.path.preferences.BooleanPreferencesKey
4 | import com.sixbynine.transit.path.preferences.LongPreferencesKey
5 | import com.sixbynine.transit.path.preferences.StringPreferencesKey
6 | import com.sixbynine.transit.path.preferences.createPreferences
7 |
8 | actual fun globalDataStore(): GlobalDataStore {
9 | val preferences = createPreferences()
10 | return object : GlobalDataStore {
11 | override fun set(key: String, value: String?) {
12 | preferences[StringPreferencesKey(key)] = value
13 | }
14 |
15 | override fun set(key: String, value: Boolean?) {
16 | preferences[BooleanPreferencesKey(key)] = value
17 | }
18 |
19 | override fun set(key: String, value: Long?) {
20 | preferences[LongPreferencesKey(key)] = value
21 | }
22 |
23 | override fun getString(key: String): String? {
24 | return preferences[StringPreferencesKey(key)]
25 | }
26 |
27 | override fun getBoolean(key: String): Boolean? {
28 | return preferences[BooleanPreferencesKey(key)]
29 | }
30 |
31 | override fun getLong(key: String): Long? {
32 | return preferences[LongPreferencesKey(key)]
33 | }
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/composeApp/src/androidMain/kotlin/com/sixbynine/transit/path/widget/glance/ImageButton.kt:
--------------------------------------------------------------------------------
1 | package com.sixbynine.transit.path.widget.glance
2 |
3 | import androidx.annotation.DrawableRes
4 | import androidx.compose.runtime.Composable
5 | import androidx.compose.ui.unit.dp
6 | import androidx.glance.ColorFilter
7 | import androidx.glance.GlanceComposable
8 | import androidx.glance.GlanceModifier
9 | import androidx.glance.GlanceTheme
10 | import androidx.glance.Image
11 | import androidx.glance.ImageProvider
12 | import androidx.glance.action.Action
13 | import androidx.glance.action.clickable
14 | import androidx.glance.appwidget.cornerRadius
15 | import androidx.glance.unit.ColorProvider
16 | import org.jetbrains.compose.resources.StringResource
17 |
18 | @GlanceComposable
19 | @Composable
20 | fun ImageButton(
21 | modifier: GlanceModifier = GlanceModifier,
22 | @DrawableRes srcResId: Int,
23 | contentDesc: StringResource,
24 | onClick: Action,
25 | isClickable: Boolean = true,
26 | tintColor: ColorProvider = GlanceTheme.colors.primary
27 | ) {
28 | Image(
29 | modifier = modifier
30 | .let { if (isClickable) it.clickable(onClick) else it }
31 | .cornerRadius(200.dp),
32 | provider = ImageProvider(srcResId),
33 | contentDescription = stringResource(contentDesc),
34 | colorFilter = ColorFilter.tint(tintColor),
35 | )
36 | }
37 |
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/com/sixbynine/transit/path/util/Localization.kt:
--------------------------------------------------------------------------------
1 | package com.sixbynine.transit.path.util
2 |
3 | import androidx.compose.ui.text.intl.LocaleList
4 | import kotlinx.coroutines.runBlocking
5 | import org.jetbrains.compose.resources.getString
6 | import pathwidgetxplat.composeapp.generated.resources.Res.string
7 | import pathwidgetxplat.composeapp.generated.resources.language_code
8 |
9 | private val locale by lazy {
10 | runCatching { runBlocking { getString(string.language_code) } }
11 | .getOrElse {
12 | // Apparently, getString can crash sometimes? Maybe only when called from non-compose
13 | // code. Let's try this, even though it's also compose logic. We can hand-roll even more
14 | // if needed.
15 | val localeList = LocaleList.current
16 | localeList
17 | .firstOrNull { it.language == "es" || it.language == "en" }
18 | ?.language
19 | ?: localeList.firstOrNull()?.language
20 | ?: "en"
21 | }
22 | }
23 |
24 | fun localizedString(en: () -> String, es: () -> String): String {
25 | return when (locale) {
26 | "es" -> es()
27 | else -> en()
28 | }
29 | }
30 |
31 | fun localizedString(en: String, es: String): String {
32 | return when (locale) {
33 | "es" -> es
34 | else -> en
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/composeApp/src/androidMain/kotlin/com/sixbynine/transit/path/widget/glance/Typography.kt:
--------------------------------------------------------------------------------
1 | package com.sixbynine.transit.path.widget.glance
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.compose.runtime.ProvidableCompositionLocal
5 | import androidx.compose.runtime.staticCompositionLocalOf
6 | import androidx.compose.ui.unit.sp
7 | import androidx.glance.text.FontWeight
8 | import androidx.glance.text.TextStyle
9 |
10 | data class Typography(val header: TextStyle, val primary: TextStyle, val secondary: TextStyle)
11 |
12 | @Composable
13 | fun createTypography(
14 | header: TextStyle = TextStyle(
15 | color = GlanceTheme.colors.onSurface,
16 | fontSize = 18.sp,
17 | fontWeight = FontWeight.Bold,
18 | ),
19 | primary: TextStyle = TextStyle(
20 | color = GlanceTheme.colors.onSurface,
21 | fontSize = 14.sp,
22 | fontWeight = FontWeight.Bold,
23 | ),
24 | secondary: TextStyle = TextStyle(
25 | color = GlanceTheme.colors.onSurfaceVariant,
26 | fontSize = 14.sp,
27 | fontWeight = FontWeight.Normal,
28 | ),
29 | ): Typography {
30 | return Typography(
31 | header = header,
32 | primary = primary,
33 | secondary = secondary,
34 | )
35 | }
36 |
37 | internal val LocalTypography: ProvidableCompositionLocal =
38 | staticCompositionLocalOf { error("no typography") }
39 |
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/com/sixbynine/transit/path/app/ui/settings/StationLimitBottomSheet.kt:
--------------------------------------------------------------------------------
1 | package com.sixbynine.transit.path.app.ui.settings
2 |
3 | import androidx.compose.foundation.layout.padding
4 | import androidx.compose.runtime.Composable
5 | import androidx.compose.ui.Modifier
6 | import androidx.compose.ui.unit.dp
7 | import com.sixbynine.transit.path.app.settings.StationLimit
8 | import com.sixbynine.transit.path.app.ui.PathBottomSheet
9 | import org.jetbrains.compose.resources.stringResource
10 | import pathwidgetxplat.composeapp.generated.resources.Res.string
11 | import pathwidgetxplat.composeapp.generated.resources.settings_header_station_filter
12 |
13 | @Composable
14 | fun StationLimitBottomSheet(
15 | isShown: Boolean,
16 | limit: StationLimit,
17 | onLimitClicked: (StationLimit) -> Unit,
18 | onDismiss: () -> Unit,
19 | ) {
20 | PathBottomSheet(
21 | isShown = isShown,
22 | onDismissRequest = onDismiss,
23 | title = stringResource(string.settings_header_station_filter)
24 | ) {
25 | RadioSection(modifier = Modifier.padding(bottom = 16.dp)) {
26 | StationLimit.entries.forEach {
27 | item(
28 | text = stringResource(it.displayName),
29 | selected = it == limit,
30 | onClick = { onLimitClicked(it) }
31 | )
32 | }
33 | }
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/com/sixbynine/transit/path/app/ui/settings/TrainFilterBottomSheet.kt:
--------------------------------------------------------------------------------
1 | package com.sixbynine.transit.path.app.ui.settings
2 |
3 | import androidx.compose.foundation.layout.padding
4 | import androidx.compose.runtime.Composable
5 | import androidx.compose.ui.Modifier
6 | import androidx.compose.ui.unit.dp
7 | import com.sixbynine.transit.path.api.TrainFilter
8 | import com.sixbynine.transit.path.app.ui.PathBottomSheet
9 | import org.jetbrains.compose.resources.stringResource
10 | import pathwidgetxplat.composeapp.generated.resources.Res.string
11 | import pathwidgetxplat.composeapp.generated.resources.filter
12 |
13 | @Composable
14 | fun TrainFilterBottomSheet(
15 | isShown: Boolean,
16 | filter: TrainFilter,
17 | onFilterClicked: (TrainFilter) -> Unit,
18 | onDismiss: () -> Unit,
19 | ) {
20 | PathBottomSheet(
21 | isShown = isShown,
22 | onDismissRequest = onDismiss,
23 | title = stringResource(string.filter)
24 | ) {
25 | RadioSection(modifier = Modifier.padding(bottom = 16.dp)) {
26 | TrainFilter.entries.forEach {
27 | item(
28 | text = stringResource(it.title),
29 | subtext = it.subtext?.let { stringResource(it) },
30 | selected = it == filter,
31 | onClick = { onFilterClicked(it) }
32 | )
33 | }
34 | }
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/api/src/commonMain/kotlin/com/sixbynine/transit/path/api/templine/HobClosureConfig.kt:
--------------------------------------------------------------------------------
1 | package com.sixbynine.transit.path.api.templine
2 |
3 | import com.sixbynine.transit.path.schedule.LocalDateTimeSerializer
4 | import kotlinx.datetime.LocalDateTime
5 | import kotlinx.datetime.Month.FEBRUARY
6 | import kotlinx.datetime.Month.JANUARY
7 | import kotlinx.serialization.Serializable
8 |
9 | @Serializable
10 | data class HobClosureConfig(
11 | val tempLineInfo: TempLineInfo,
12 | @Serializable(with = LocalDateTimeSerializer::class) val validFrom: LocalDateTime?,
13 | @Serializable(with = LocalDateTimeSerializer::class) val validTo: LocalDateTime?,
14 | ) {
15 | companion object {
16 | val fallback get() = HobClosureConfig(
17 | tempLineInfo = TempLineInfo.fallback,
18 | validFrom = LocalDateTime(2025, JANUARY, 18, 23, 59),
19 | validTo = LocalDateTime(2025, FEBRUARY, 28, 5, 0),
20 | )
21 | }
22 | }
23 |
24 | @Serializable
25 | data class TempLineInfo(
26 | val displayName: String,
27 | val codes: List,
28 | val lightColor: String,
29 | val darkColor: String?,
30 | ) {
31 | companion object {
32 | val fallback get() = TempLineInfo(
33 | displayName = "33rd Street ⇆ World Trade Center",
34 | codes = listOf("WTC-33", "33-WTC"),
35 | lightColor = "65C100",
36 | darkColor = null,
37 | )
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/api/src/commonMain/kotlin/com/sixbynine/transit/path/api/schedule/GithubScheduleRepository.kt:
--------------------------------------------------------------------------------
1 | package com.sixbynine.transit.path.api.schedule
2 |
3 | import com.sixbynine.transit.path.schedule.Timetables
4 | import com.sixbynine.transit.path.util.FetchWithPrevious
5 | import com.sixbynine.transit.path.util.RemoteFileRepository
6 | import com.sixbynine.transit.path.util.combine
7 | import kotlinx.datetime.Instant
8 | import kotlin.time.Duration.Companion.days
9 | import kotlin.time.Duration.Companion.minutes
10 |
11 | object GithubScheduleRepository {
12 | private val scheduleRepo = RemoteFileRepository(
13 | keyPrefix = "github_schedule",
14 | url = "https://raw.githubusercontent.com/steviek/PathWidgetXplat/main/schedule.json",
15 | serializer = Timetables.serializer(),
16 | maxAge = 3.days
17 | )
18 |
19 | private val scheduleOverrideRepo = RemoteFileRepository(
20 | keyPrefix = "github_schedule_override",
21 | url = "https://raw.githubusercontent.com/steviek/PathWidgetXplat/main/schedule_override.json",
22 | serializer = Timetables.serializer(),
23 | maxAge = 30.minutes
24 | )
25 |
26 | fun getSchedules(now: Instant): FetchWithPrevious {
27 | return combine(scheduleRepo.get(now), scheduleOverrideRepo.get(now)) { schedule, override ->
28 | ScheduleAndOverride(regularSchedule = schedule, override = override)
29 | }
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/composeApp/src/desktopMain/kotlin/com/sixbynine/transit/path/location/LocationProvider.jvm.kt:
--------------------------------------------------------------------------------
1 | package com.sixbynine.transit.path.location
2 |
3 | import com.sixbynine.transit.path.util.DataResult
4 | import com.sixbynine.transit.path.util.stateFlowOf
5 | import kotlinx.coroutines.flow.MutableSharedFlow
6 | import kotlinx.coroutines.flow.SharedFlow
7 | import kotlinx.coroutines.flow.StateFlow
8 | import kotlinx.coroutines.flow.asSharedFlow
9 | import kotlin.time.Duration
10 |
11 | object JvmLocationProvider : LocationProvider {
12 | override val isLocationSupportedByDeviceFlow: StateFlow>
13 | get() = stateFlowOf(DataResult.success(false))
14 |
15 | private val _locationPermissionResults = MutableSharedFlow(replay = 1)
16 | override val locationPermissionResults: SharedFlow
17 | get() = _locationPermissionResults.asSharedFlow()
18 |
19 | override fun hasLocationPermission() = false
20 |
21 | override fun requestLocationPermission() {
22 | _locationPermissionResults.tryEmit(LocationPermissionRequestResult.Denied)
23 | }
24 |
25 | override suspend fun tryToGetLocation(): LocationCheckResult {
26 | return LocationCheckResult.NoProvider
27 | }
28 |
29 | override val defaultLocationCheckTimeout: Duration
30 | get() = Duration.INFINITE
31 | }
32 |
33 | actual fun LocationProvider() : LocationProvider = JvmLocationProvider
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/com/sixbynine/transit/path/app/ui/settings/TimeDisplayBottomSheet.kt:
--------------------------------------------------------------------------------
1 | package com.sixbynine.transit.path.app.ui.settings
2 |
3 | import androidx.compose.foundation.layout.padding
4 | import androidx.compose.runtime.Composable
5 | import androidx.compose.ui.Modifier
6 | import androidx.compose.ui.unit.dp
7 | import com.sixbynine.transit.path.app.settings.TimeDisplay
8 | import com.sixbynine.transit.path.app.ui.PathBottomSheet
9 | import org.jetbrains.compose.resources.stringResource
10 | import pathwidgetxplat.composeapp.generated.resources.Res.string
11 | import pathwidgetxplat.composeapp.generated.resources.setting_header_time_display
12 |
13 | @Composable
14 | fun TimeDisplayBottomSheet(
15 | isShown: Boolean,
16 | display: TimeDisplay,
17 | onTimeDisplayClicked: (TimeDisplay) -> Unit,
18 | onDismiss: () -> Unit,
19 | ) {
20 | PathBottomSheet(
21 | isShown = isShown,
22 | onDismissRequest = onDismiss,
23 | title = stringResource(string.setting_header_time_display)
24 | ) {
25 | RadioSection(modifier = Modifier.padding(bottom = 16.dp)) {
26 | TimeDisplay.entries.forEach {
27 | item(
28 | text = stringResource(it.title),
29 | subtext = it.subtitle(),
30 | selected = it == display,
31 | onClick = { onTimeDisplayClicked(it) }
32 | )
33 | }
34 | }
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/api/src/commonMain/kotlin/com/sixbynine/transit/path/util/RemoteFileRepository.kt:
--------------------------------------------------------------------------------
1 | package com.sixbynine.transit.path.util
2 |
3 | import com.sixbynine.transit.path.preferences.StringPreferencesKey
4 | import com.sixbynine.transit.path.time.now
5 | import kotlinx.datetime.Instant
6 | import kotlinx.serialization.KSerializer
7 | import kotlin.time.Duration
8 |
9 | class RemoteFileRepository(
10 | keyPrefix: String,
11 | private val url: String,
12 | maxAge: Duration,
13 | private val serializer: KSerializer
14 | ) {
15 | private var storedJson by persistingGlobally(StringPreferencesKey(keyPrefix))
16 | private var storedTime by persistingInstantGlobally("{$keyPrefix}_time")
17 |
18 | private val dataSource = DataSource(
19 | getCached = getCached@{
20 | val storedDataTime = storedTime ?: return@getCached null
21 | val storedDataJson = storedJson ?: return@getCached null
22 | val deserialized = JsonFormat.decodeFromString(serializer, storedDataJson)
23 | TimestampedValue(storedDataTime, deserialized)
24 | },
25 | fetch = {
26 | val responseText = readRemoteFile(url).getOrThrow()
27 |
28 | storedJson = responseText
29 | storedTime = now()
30 | JsonFormat.decodeFromString(serializer, responseText)
31 | },
32 | maxAge = maxAge
33 | )
34 |
35 | fun get(now: Instant): FetchWithPrevious {
36 | return dataSource.get(now)
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/composeApp/src/androidMain/kotlin/com/sixbynine/transit/path/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package com.sixbynine.transit.path
2 |
3 | import App
4 | import android.content.Intent
5 | import android.os.Bundle
6 | import androidx.activity.compose.setContent
7 | import androidx.compose.runtime.Composable
8 | import androidx.compose.ui.tooling.preview.Preview
9 | import androidx.glance.action.Action
10 | import androidx.glance.appwidget.action.actionStartActivity
11 | import androidx.glance.appwidget.updateAll
12 | import androidx.lifecycle.lifecycleScope
13 | import com.sixbynine.transit.path.widget.DepartureBoardWidget
14 | import kotlinx.coroutines.launch
15 |
16 | class MainActivity : BaseActivity() {
17 | override fun onCreate(savedInstanceState: Bundle?) {
18 | super.onCreate(savedInstanceState)
19 |
20 | if (savedInstanceState == null) {
21 | lifecycleScope.launch {
22 | DepartureBoardWidget().updateAll(MobilePathApplication.instance)
23 | }
24 | }
25 |
26 | setContent {
27 | App()
28 | }
29 | }
30 |
31 | companion object {
32 | fun createAppWidgetLaunchAction(): Action {
33 | return Intent(MobilePathApplication.instance, MainActivity::class.java)
34 | .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK)
35 | .let { actionStartActivity(it) }
36 | }
37 | }
38 | }
39 |
40 | @Preview
41 | @Composable
42 | fun AppAndroidPreview() {
43 | App()
44 | }
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/com/sixbynine/transit/path/app/ui/settings/LineFilterBottomSheet.kt:
--------------------------------------------------------------------------------
1 | package com.sixbynine.transit.path.app.ui.settings
2 |
3 | import androidx.compose.foundation.layout.Column
4 | import androidx.compose.foundation.layout.fillMaxWidth
5 | import androidx.compose.foundation.layout.padding
6 | import androidx.compose.runtime.Composable
7 | import androidx.compose.ui.Modifier
8 | import androidx.compose.ui.unit.dp
9 | import com.sixbynine.transit.path.api.Line
10 | import com.sixbynine.transit.path.app.ui.PathBottomSheet
11 | import org.jetbrains.compose.resources.stringResource
12 | import pathwidgetxplat.composeapp.generated.resources.Res.string
13 | import pathwidgetxplat.composeapp.generated.resources.lines
14 |
15 | @Composable
16 | fun LineFilterBottomSheet(
17 | isShown: Boolean,
18 | lines: Set,
19 | onLineCheckedChange: (Line, Boolean) -> Unit,
20 | onDismiss: () -> Unit,
21 | ) {
22 | PathBottomSheet(
23 | isShown = isShown,
24 | onDismissRequest = onDismiss,
25 | title = stringResource(string.lines)
26 | ) {
27 | Column(Modifier.fillMaxWidth().padding(bottom = 24.dp)) {
28 | Line.permanentLines.forEach { line ->
29 | TrainLineCheckboxRow(
30 | line = line,
31 | checked = line in lines,
32 | onCheckedChange = { isChecked -> onLineCheckedChange(line, isChecked) },
33 | )
34 | }
35 | }
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/schedule/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | alias(libs.plugins.kotlinMultiplatform)
3 | alias(libs.plugins.androidLibrary)
4 | alias(libs.plugins.kotlinSerialization)
5 | }
6 |
7 | kotlin {
8 | applyDefaultHierarchyTemplate()
9 |
10 | androidTarget()
11 |
12 | jvm()
13 |
14 | iosX64()
15 | iosArm64()
16 | iosSimulatorArm64()
17 |
18 | sourceSets {
19 | commonMain.dependencies {
20 | implementation(libs.kotlin.coroutines)
21 | implementation(libs.kotlin.date.time)
22 | implementation(libs.kotlin.serialization.json)
23 | implementation(projects.platform)
24 | }
25 |
26 | commonTest.dependencies {
27 | implementation(libs.kotlin.test)
28 | }
29 |
30 | androidMain.dependencies {
31 | implementation(libs.androidx.core.ktx)
32 | }
33 |
34 | all {
35 | languageSettings.optIn("kotlin.ExperimentalStdlibApi")
36 | languageSettings.optIn("kotlin.contracts.ExperimentalContracts")
37 | }
38 | }
39 | }
40 |
41 | android {
42 | namespace = "com.sixbynine.transit.path.schedule"
43 |
44 | defaultConfig {
45 | minSdk = libs.versions.android.minSdk.get().toInt()
46 | }
47 |
48 | buildFeatures {
49 | buildConfig = true
50 | }
51 |
52 | compileOptions {
53 | isCoreLibraryDesugaringEnabled = true
54 | }
55 |
56 | dependencies {
57 | coreLibraryDesugaring(libs.android.tools.desugar)
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/platform/src/androidMain/kotlin/com/sixbynine/transit/path/network/AndroidNetworkManager.kt:
--------------------------------------------------------------------------------
1 | package com.sixbynine.transit.path.network
2 |
3 | import android.content.Context
4 | import android.net.ConnectivityManager
5 | import android.net.NetworkCapabilities
6 | import android.os.Build.VERSION
7 | import com.sixbynine.transit.path.PathApplication
8 | import com.sixbynine.transit.path.util.IsTest
9 |
10 | object AndroidNetworkManager : NetworkManager {
11 | private val connectivityManager: ConnectivityManager
12 | get() {
13 | return PathApplication.instance.getSystemService(Context.CONNECTIVITY_SERVICE)
14 | as ConnectivityManager
15 | }
16 |
17 | override fun isConnectedToInternet(): Boolean {
18 | if (IsTest) return true
19 | if (VERSION.SDK_INT < 23) {
20 | return connectivityManager.activeNetworkInfo?.isConnectedOrConnecting == true
21 | }
22 |
23 | val network = connectivityManager.activeNetwork ?: return false
24 | val capabilities = connectivityManager.getNetworkCapabilities(network) ?: return false
25 | return capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) ||
26 | capabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) ||
27 | capabilities.hasTransport(NetworkCapabilities.TRANSPORT_BLUETOOTH) ||
28 | capabilities.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET)
29 | }
30 | }
31 |
32 | actual fun NetworkManager(): NetworkManager = AndroidNetworkManager
33 |
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/com/sixbynine/transit/path/app/external/ExternalRoutingManager.kt:
--------------------------------------------------------------------------------
1 | package com.sixbynine.transit.path.app.external
2 |
3 | import PlatformType
4 | import getPlatform
5 | import org.jetbrains.compose.resources.getString
6 | import pathwidgetxplat.composeapp.generated.resources.Res.string
7 | import pathwidgetxplat.composeapp.generated.resources.sharing_message
8 |
9 | interface ExternalRoutingManager {
10 | suspend fun openEmail(): Boolean
11 |
12 | suspend fun openUrl(url: String): Boolean
13 |
14 | suspend fun shareTextToSystem(text: String): Boolean
15 |
16 | suspend fun launchAppRating(): Boolean
17 | }
18 |
19 | expect fun ExternalRoutingManager(): ExternalRoutingManager
20 |
21 | suspend fun ExternalRoutingManager.shareAppToSystem(): Boolean {
22 | val text = StringBuilder()
23 |
24 | text.appendLine(getString(string.sharing_message))
25 | text.appendLine()
26 |
27 | listOf(AndroidSharingLine, "", IosSharingLine)
28 | .let { if (getPlatform().type == PlatformType.ANDROID) it else it.reversed() }
29 | .forEach { text.appendLine(it) }
30 |
31 | return shareTextToSystem(text.toString())
32 | }
33 |
34 | const val FeedbackEmail = "sixbynineapps@gmail.com"
35 |
36 | const val AppStoreUrl = "https://apps.apple.com/id/app/departures-widget-for-path/id6470330823"
37 |
38 | private const val IosSharingLine = "iOS: $AppStoreUrl"
39 |
40 | private const val AndroidSharingLine =
41 | "Android: https://play.google.com/store/apps/details?id=com.sixbynine.transit.path"
42 |
--------------------------------------------------------------------------------
/composeApp/src/androidMain/kotlin/com/sixbynine/transit/path/MobilePathApplication.kt:
--------------------------------------------------------------------------------
1 | package com.sixbynine.transit.path
2 |
3 | import com.google.firebase.Firebase
4 | import com.google.firebase.crashlytics.crashlytics
5 | import com.sixbynine.transit.path.analytics.Analytics
6 | import com.sixbynine.transit.path.app.ui.ActivityRegistry
7 | import com.sixbynine.transit.path.native.NativeHolder
8 | import com.sixbynine.transit.path.widget.WidgetRefreshWorker
9 | import com.sixbynine.transit.path.widget.WidgetReloader
10 | import kotlinx.coroutines.GlobalScope
11 | import kotlinx.coroutines.delay
12 | import kotlinx.coroutines.launch
13 | import kotlin.time.Duration.Companion.seconds
14 |
15 | class MobilePathApplication : PathApplication() {
16 | override fun onCreate() {
17 | super.onCreate()
18 | instance = this
19 |
20 | Analytics.appLaunched()
21 |
22 | NativeHolder.initialize(
23 | object : WidgetReloader {
24 | override fun reloadWidgets() {
25 | GlobalScope.launch {
26 | WidgetRefreshWorker.scheduleOneTime()
27 | }
28 | }
29 | },
30 | Firebase.crashlytics::recordException
31 | )
32 |
33 | ActivityRegistry.register(this)
34 |
35 | GlobalScope.launch {
36 | delay(1.seconds)
37 | WidgetRefreshWorker.schedule()
38 | }
39 | }
40 |
41 | companion object {
42 | lateinit var instance: MobilePathApplication
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/platform/src/commonMain/kotlin/com/sixbynine/transit/path/time/TimeUtils.kt:
--------------------------------------------------------------------------------
1 | package com.sixbynine.transit.path.time
2 |
3 | import kotlinx.datetime.Clock
4 | import kotlinx.datetime.DayOfWeek
5 | import kotlinx.datetime.Instant
6 | import kotlinx.datetime.LocalDate
7 | import kotlinx.datetime.TimeZone
8 | import kotlinx.datetime.toLocalDateTime
9 |
10 | interface PlatformTimeUtils {
11 | fun is24HourClock(): Boolean
12 |
13 | fun getFirstDayOfWeek(): DayOfWeek
14 | }
15 |
16 | expect fun getPlatformTimeUtils(): PlatformTimeUtils
17 |
18 | fun is24HourClock() = getPlatformTimeUtils().is24HourClock()
19 |
20 | fun now(): Instant = Clock.System.now()
21 |
22 | fun today(): LocalDate = now().toLocalDateTime(TimeZone.currentSystemDefault()).date
23 |
24 | val NewYorkTimeZone = TimeZone.of("America/New_York")
25 |
26 | fun DayOfWeek.plusDays(days: Int): DayOfWeek {
27 | var newOrdinal = ordinal + days
28 | while (newOrdinal < 0) {
29 | newOrdinal += 7
30 | }
31 | while (newOrdinal >= 7) {
32 | newOrdinal -= 7
33 | }
34 | return DayOfWeek.entries[newOrdinal]
35 | }
36 |
37 | fun DayOfWeek.minusDays(days: Int): DayOfWeek = plusDays(-days)
38 |
39 | fun DayOfWeek.previous() = minusDays(1)
40 | fun DayOfWeek.next() = plusDays(1)
41 |
42 | fun closedDayOfWeekSet(start: DayOfWeek, end: DayOfWeek): Set {
43 | return buildSet {
44 | var current = start
45 | while (current != end) {
46 | add(current)
47 | current = current.next()
48 | }
49 | add(end)
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/api/src/commonMain/kotlin/com/sixbynine/transit/path/util/DataSource.kt:
--------------------------------------------------------------------------------
1 | package com.sixbynine.transit.path.util
2 |
3 | import kotlinx.coroutines.CoroutineStart.LAZY
4 | import kotlinx.coroutines.Deferred
5 | import kotlinx.coroutines.async
6 | import kotlinx.coroutines.sync.Mutex
7 | import kotlinx.coroutines.sync.withLock
8 | import kotlinx.datetime.Instant
9 | import kotlin.time.Duration
10 |
11 | internal class DataSource(
12 | private val getCached: () -> TimestampedValue?,
13 | private val fetch: suspend () -> T,
14 | private val maxAge: Duration,
15 | ) {
16 |
17 | private val fetchMutex = Mutex()
18 | private var ongoingFetch: Deferred>? = null
19 |
20 | fun get(now: Instant): FetchWithPrevious {
21 | val lastResult = runCatching { getCached() }.getOrNull()?.toAgedValue(now)
22 |
23 | if (lastResult != null && lastResult.age <= maxAge) {
24 | return FetchWithPrevious(lastResult)
25 | }
26 |
27 | val fetch = startOrJoinFetch()
28 | return FetchWithPrevious(previous = lastResult, fetch = fetch)
29 | }
30 |
31 | private fun startOrJoinFetch(): Deferred> {
32 | return IoScope.async(start = LAZY) {
33 | val asyncFetch = fetchMutex.withLock {
34 | ongoingFetch?.takeIf { it.isActive }?.let { return@withLock it }
35 | IoScope.asyncCatchingDataResult { fetch() }.also { ongoingFetch = it }
36 | }
37 |
38 | asyncFetch.await().also { ongoingFetch = null }
39 | }
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/flipper/src/androidDebug/kotlin/com/sixbynine/transit/path/flipper/FlipperUtil.kt:
--------------------------------------------------------------------------------
1 | package com.sixbynine.transit.path.flipper
2 |
3 | import android.content.Context
4 | import com.facebook.flipper.android.AndroidFlipperClient
5 | import com.facebook.flipper.plugins.inspector.DescriptorMapping
6 | import com.facebook.flipper.plugins.inspector.InspectorFlipperPlugin
7 | import com.facebook.flipper.plugins.network.FlipperOkhttpInterceptor
8 | import com.facebook.flipper.plugins.network.NetworkFlipperPlugin
9 | import com.facebook.flipper.plugins.sharedpreferences.SharedPreferencesFlipperPlugin
10 | import com.facebook.flipper.plugins.sharedpreferences.SharedPreferencesFlipperPlugin.SharedPreferencesDescriptor
11 | import com.facebook.soloader.SoLoader
12 |
13 | object FlipperUtil {
14 |
15 | private val networkPlugin = NetworkFlipperPlugin()
16 |
17 | fun initialize(context: Context) {
18 | SoLoader.init(context, false)
19 |
20 | val client = AndroidFlipperClient.getInstance(context)
21 |
22 | val sharedPreferencesDescriptors =
23 | listOf("path", "widget_data")
24 | .map { SharedPreferencesDescriptor(it, Context.MODE_PRIVATE) }
25 |
26 | client.addPlugin(InspectorFlipperPlugin(context, DescriptorMapping.withDefaults()))
27 | client.addPlugin(SharedPreferencesFlipperPlugin(context, sharedPreferencesDescriptors))
28 | client.addPlugin(networkPlugin)
29 |
30 | client.start()
31 | }
32 |
33 | fun interceptor(): Any? {
34 | return FlipperOkhttpInterceptor(networkPlugin)
35 | }
36 | }
--------------------------------------------------------------------------------
/iosApp/widget/ColorCircle.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ColorCircle.swift
3 | // widgetExtension
4 | //
5 | // Created by Steven Kideckel on 2024-06-19.
6 | // Copyright © 2024 orgName. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 | import ComposeApp
11 |
12 | struct ColorCircle: View {
13 |
14 | @Environment(\.colorScheme) var colorScheme
15 |
16 | let size: CGFloat
17 | let colors: [ColorWrapper]
18 |
19 | var body: some View {
20 | let isDark = colorScheme == .dark
21 | let color1 = colors.first?.toColor(isDark: isDark) ?? Color.black
22 | let color2 = colors.count > 1 ? colors[1].toColor(isDark: isDark) : color1
23 | ZStack {
24 | Circle()
25 | .fill(color1)
26 | .frame(width: size, height: size)
27 |
28 | SemiCircle()
29 | .fill(color2)
30 | .rotationEffect(.degrees(90))
31 | .frame(width: size, height: size)
32 | }
33 | .overlay(isDark ? Circle().stroke(Color.white, lineWidth: 1) : nil)
34 | }
35 |
36 | private struct SemiCircle: Shape {
37 | func path(in rect: CGRect) -> Path {
38 | var path = Path()
39 |
40 | path.move(to: CGPoint(x: rect.minX, y: rect.midY))
41 | path.addArc(center: CGPoint(x: rect.midX, y: rect.midY), radius: rect.width / 2, startAngle: .degrees(0), endAngle: .degrees(180), clockwise: true)
42 | path.closeSubpath()
43 |
44 | return path
45 | }
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Departures App for PATH
2 |
3 | This is a [Kotlin Multiplatform](https://www.jetbrains.com/help/kotlin-multiplatform-dev/get-started.html) project targeting Android and iOS.
4 |
5 | The App UI is written in [Compose Multiplatform](https://www.jetbrains.com/compose-multiplatform/) with minimal platform-specific code.
6 |
7 | The widgets are native by necessity, and written in [SwiftUI](https://developer.apple.com/documentation/widgetkit/swiftui-views) on iOS and [Jetpack Glance](https://developer.android.com/develop/ui/compose/glance) for Android.
8 |
9 | ## Setup
10 |
11 | You can use any of the following options to work with the code here:
12 |
13 | - [Android Studio](https://developer.android.com/studio) Standard IDE for editing kotlin/Android code. With the [kotlin multiplatform plugin](https://plugins.jetbrains.com/plugin/14936-kotlin-multiplatform), you can run the code on both Android and iOS
14 | - [Xcode](https://developer.apple.com/xcode/) allows for running the code on iOS, managing iOS simulators, and editing Swift code
15 |
16 |
17 | ## App store links
18 |
19 | [Android](https://play.google.com/store/apps/details?id=com.sixbynine.transit.path)
20 |
21 | [iOS](https://apps.apple.com/id/app/departures-widget-for-path/id6470330823?platform=iphone)
22 |
23 | ## Localizations
24 |
25 | English and Spanish currently. If you want to contribute something else feel free. Translations are found in the following files:
26 |
27 | - composeApp/src/commonMain/composeResources/values-{locale}/strings.xml
28 | - iosApp/widget/Localizable.xcstrings (edit in Xcode)
29 | - iosApp/iosApp/InfoPlist.xcstrings (edit in Xcode)
30 |
--------------------------------------------------------------------------------
/platform/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | alias(libs.plugins.kotlinMultiplatform)
3 | alias(libs.plugins.androidLibrary)
4 | alias(libs.plugins.kotlinSerialization)
5 | }
6 |
7 | kotlin {
8 | applyDefaultHierarchyTemplate()
9 |
10 | androidTarget()
11 |
12 | jvm()
13 |
14 | iosX64()
15 | iosArm64()
16 | iosSimulatorArm64()
17 |
18 | sourceSets {
19 | commonMain.dependencies {
20 | implementation(projects.flipper)
21 |
22 | implementation(libs.kotlin.coroutines)
23 | implementation(libs.kotlin.date.time)
24 | implementation(libs.kotlin.serialization.json)
25 | }
26 |
27 | commonTest.dependencies {
28 | implementation(libs.kotlin.test)
29 | }
30 |
31 | androidMain.dependencies {
32 | implementation(libs.androidx.core.ktx)
33 | }
34 |
35 | all {
36 | languageSettings.optIn("kotlin.ExperimentalStdlibApi")
37 | languageSettings.optIn("kotlin.contracts.ExperimentalContracts")
38 | languageSettings.optIn("kotlinx.coroutines.ExperimentalCoroutinesApi")
39 | }
40 | }
41 | }
42 |
43 | android {
44 | namespace = "com.sixbynine.transit.path.platform"
45 |
46 | defaultConfig {
47 | minSdk = libs.versions.android.minSdk.get().toInt()
48 | }
49 |
50 | buildFeatures {
51 | buildConfig = true
52 | }
53 |
54 | compileOptions {
55 | isCoreLibraryDesugaringEnabled = true
56 | }
57 |
58 | dependencies {
59 | coreLibraryDesugaring(libs.android.tools.desugar)
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/composeApp/src/commonTest/kotlin/com/sixbynine/transit/path/api/impl/CheckpointMapTest.kt:
--------------------------------------------------------------------------------
1 | package com.sixbynine.transit.path.api.impl
2 |
3 | import com.sixbynine.transit.path.api.Stations.Harrison
4 | import com.sixbynine.transit.path.api.Stations.Hoboken
5 | import com.sixbynine.transit.path.api.Stations.JournalSquare
6 | import com.sixbynine.transit.path.api.Stations.Newark
7 | import kotlin.test.Test
8 | import kotlin.test.assertEquals
9 | import kotlin.test.assertNull
10 | import kotlin.time.Duration.Companion.minutes
11 |
12 | class CheckpointMapTest {
13 |
14 | @Test
15 | fun `basic functions`() {
16 | assertEquals(0.minutes, Map[Newark])
17 | assertEquals(1.minutes, Map[Harrison])
18 | assertEquals(2.minutes, Map[JournalSquare])
19 | assertNull(Map[Hoboken])
20 |
21 | assertEquals(0, Map.getPosition(Newark))
22 | assertEquals(1, Map.getPosition(Harrison))
23 | assertEquals(2, Map.getPosition(JournalSquare))
24 | assertNull(Map.getPosition(Hoboken))
25 | }
26 |
27 | @Test
28 | fun `filtering middle`() {
29 | val filtered = Map.without(Harrison)
30 |
31 | assertEquals(checkpointMapOf(Newark to 0.minutes, JournalSquare to 2.minutes), filtered)
32 | }
33 |
34 | @Test
35 | fun `filtering start`() {
36 | val filtered = Map.without(Newark)
37 |
38 | assertEquals(checkpointMapOf(Harrison to 0.minutes, JournalSquare to 1.minutes), filtered)
39 | }
40 |
41 | companion object {
42 | val Map =
43 | checkpointMapOf(Newark to 0.minutes, Harrison to 1.minutes, JournalSquare to 2.minutes)
44 | }
45 | }
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/com/sixbynine/transit/path/app/ui/settings/StationSortBottomSheet.kt:
--------------------------------------------------------------------------------
1 | package com.sixbynine.transit.path.app.ui.settings
2 |
3 | import androidx.compose.foundation.layout.padding
4 | import androidx.compose.runtime.Composable
5 | import androidx.compose.ui.Modifier
6 | import androidx.compose.ui.unit.dp
7 | import com.sixbynine.transit.path.api.StationSort
8 | import com.sixbynine.transit.path.api.StationSort.Proximity
9 | import com.sixbynine.transit.path.app.ui.PathBottomSheet
10 | import org.jetbrains.compose.resources.stringResource
11 | import pathwidgetxplat.composeapp.generated.resources.Res.string
12 | import pathwidgetxplat.composeapp.generated.resources.station_order
13 |
14 | @Composable
15 | fun StationSortBottomSheet(
16 | isShown: Boolean,
17 | sort: StationSort,
18 | onSortClicked: (StationSort) -> Unit,
19 | onDismiss: () -> Unit,
20 | ) {
21 | PathBottomSheet(
22 | isShown = isShown,
23 | onDismissRequest = onDismiss,
24 | title = stringResource(string.station_order)
25 | ) {
26 | RadioSection(modifier = Modifier.padding(bottom = 16.dp)) {
27 | StationSort.entries.forEach {
28 | if (it == Proximity && !StationSort.isProximityEnabled()) {
29 | return@forEach
30 | }
31 |
32 | item(
33 | text = stringResource(it.title),
34 | subtext = it.subtitle?.let { stringResource(it) },
35 | selected = it == sort,
36 | onClick = { onSortClicked(it) }
37 | )
38 | }
39 | }
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/iosApp/widget/SeasonalUtils.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SeasonalUtils.swift
3 | // widgetExtension
4 | //
5 | // Created by Assistant on 2024-12-19.
6 | // Copyright © 2024 orgName. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 | import Foundation
11 | import ComposeApp
12 |
13 | struct SeasonalUtils {
14 |
15 | /// Returns the appropriate background image name for the current season
16 | static func getSeasonalBackgroundName(for date: Date) -> String {
17 | let season = SeasonUtils().getSeasonForInstant(instant: date.toKotlinInstant())
18 | switch season {
19 | case .spring:
20 | return "SpringBackground"
21 | case .summer:
22 | return "SummerBackground"
23 | case .fall:
24 | return "FallBackground"
25 | case .winter:
26 | return "WinterBackground"
27 | default:
28 | return "WinterBackground"
29 | }
30 | }
31 |
32 | /// Returns the appropriate text color for the current season
33 | /// Summer and Fall: white, Spring and Winter: custom dark colors
34 | static func getSeasonalTextColor(for date: Date) -> Color {
35 | let season = SeasonUtils().getSeasonForInstant(instant: date.toKotlinInstant())
36 | switch season {
37 | case .spring:
38 | return Color(red: 0.173, green: 0.184, blue: 0.118)
39 | case .summer:
40 | return .white
41 | case .fall:
42 | return .white
43 | case .winter:
44 | return Color(red: 0.294, green: 0.239, blue: 0.043)
45 | default:
46 | return .white
47 | }
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/composeApp/src/androidMain/kotlin/com/sixbynine/transit/path/app/ui/settings/SettingsScreenPreview.kt:
--------------------------------------------------------------------------------
1 | package com.sixbynine.transit.path.app.ui.settings
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.compose.runtime.getValue
5 | import androidx.compose.runtime.mutableStateOf
6 | import androidx.compose.runtime.remember
7 | import androidx.compose.runtime.setValue
8 | import com.sixbynine.transit.path.PathWidgetPreview
9 | import com.sixbynine.transit.path.PreviewTheme
10 | import com.sixbynine.transit.path.api.Line
11 | import com.sixbynine.transit.path.api.StationSort
12 | import com.sixbynine.transit.path.api.TrainFilter.Interstate
13 | import com.sixbynine.transit.path.app.settings.TimeDisplay.Relative
14 | import com.sixbynine.transit.path.app.ui.settings.SettingsContract.LocationSettingState
15 | import com.sixbynine.transit.path.app.ui.settings.SettingsContract.State
16 |
17 | @PathWidgetPreview
18 | @Composable
19 | fun SettingsScreenPreview() {
20 | PreviewTheme {
21 | var timeDisplay by remember { mutableStateOf(Relative) }
22 | val settingsScope = SettingsScope(
23 | state = State(
24 | locationSetting = LocationSettingState.Disabled,
25 | trainFilter = Interstate,
26 | lines = Line.permanentLines.toSet(),
27 | stationSort = StationSort.Alphabetical,
28 | showPresumedTrains = false,
29 | hasLocationPermission = false,
30 | devOptionsEnabled = false,
31 | ),
32 | onIntent = { intent ->
33 |
34 | }
35 | )
36 | settingsScope.Content()
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/platform/src/iosMain/kotlin/com/sixbynine/transit/path/time/NativeTimeUtils.kt:
--------------------------------------------------------------------------------
1 | package com.sixbynine.transit.path.time
2 |
3 | import kotlinx.coroutines.flow.MutableStateFlow
4 | import kotlinx.datetime.DayOfWeek
5 | import platform.Foundation.NSDateFormatter
6 | import platform.Foundation.NSLocale
7 | import platform.Foundation.currentLocale
8 |
9 | object IosPlatformTimeUtils : PlatformTimeUtils {
10 |
11 | private val firstDayOfWeek = MutableStateFlow(null)
12 |
13 | fun setFirstDayOfWeek(firstDayOfWeek: String?) {
14 | this.firstDayOfWeek.value = firstDayOfWeek
15 | }
16 |
17 | override fun is24HourClock(): Boolean {
18 | val dateFormat = NSDateFormatter.dateFormatFromTemplate(
19 | tmplate = "j",
20 | options = 0U,
21 | locale = NSLocale.currentLocale
22 | )
23 | return dateFormat?.indexOf("a")?.let { it < 0 } ?: true
24 | }
25 |
26 | override fun getFirstDayOfWeek(): DayOfWeek {
27 | val value = firstDayOfWeek.value?.lowercase()
28 | return when {
29 | value == null || value.startsWith("su") -> DayOfWeek.SUNDAY
30 | value.startsWith("sa") -> DayOfWeek.SATURDAY
31 | value.startsWith("m") -> DayOfWeek.MONDAY
32 | value.startsWith("tu") -> DayOfWeek.TUESDAY
33 | value.startsWith("w") -> DayOfWeek.WEDNESDAY
34 | value.startsWith("th") -> DayOfWeek.THURSDAY
35 | value.startsWith("f") -> DayOfWeek.FRIDAY
36 | else -> DayOfWeek.SUNDAY
37 | }
38 | }
39 | }
40 |
41 | actual fun getPlatformTimeUtils(): PlatformTimeUtils {
42 | return IosPlatformTimeUtils
43 | }
44 |
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/com/sixbynine/transit/path/widget/PathWidgetConfiguration.kt:
--------------------------------------------------------------------------------
1 | package com.sixbynine.transit.path.widget
2 |
3 | import com.sixbynine.transit.path.api.Line
4 | import com.sixbynine.transit.path.api.StationChoice
5 | import com.sixbynine.transit.path.api.StationSort
6 | import com.sixbynine.transit.path.api.Stations
7 | import com.sixbynine.transit.path.api.TrainFilter
8 |
9 | sealed interface PathWidgetConfiguration {
10 | val stationLimit: Int
11 |
12 | data class DepartureBoard(
13 | override val stationLimit: Int,
14 | val stationChoices: List,
15 | val lines: Collection,
16 | val sort: StationSort,
17 | val filter: TrainFilter,
18 | ) : PathWidgetConfiguration
19 |
20 | data class Commute(
21 | val origin: StationChoice,
22 | val destination: StationChoice,
23 | ) : PathWidgetConfiguration {
24 | override val stationLimit: Int = 1
25 | }
26 |
27 | companion object {
28 | fun allData(
29 | includeClosestStation: Boolean,
30 | sort: StationSort = StationSort.Alphabetical
31 | ): DepartureBoard {
32 | return DepartureBoard(
33 | stationLimit = Int.MAX_VALUE,
34 | stationChoices = buildList {
35 | if (includeClosestStation) add(StationChoice.Closest)
36 | Stations.All.forEach { add(StationChoice.Fixed(it)) }
37 | },
38 | lines = Line.entries,
39 | sort = StationSort.Alphabetical,
40 | filter = TrainFilter.All,
41 | )
42 | }
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/com/sixbynine/transit/path/app/settings/CommutingConfiguration.kt:
--------------------------------------------------------------------------------
1 | package com.sixbynine.transit.path.app.settings
2 |
3 | import com.sixbynine.transit.path.schedule.DailySchedule
4 | import com.sixbynine.transit.path.schedule.Schedule
5 | import kotlinx.datetime.DayOfWeek
6 | import kotlinx.datetime.DayOfWeek.FRIDAY
7 | import kotlinx.datetime.DayOfWeek.MONDAY
8 | import kotlinx.datetime.DayOfWeek.THURSDAY
9 | import kotlinx.datetime.DayOfWeek.TUESDAY
10 | import kotlinx.datetime.DayOfWeek.WEDNESDAY
11 | import kotlinx.datetime.LocalDateTime
12 | import kotlinx.datetime.LocalTime
13 | import kotlinx.serialization.Serializable
14 |
15 | @Serializable
16 | data class CommutingConfiguration(
17 | val schedules: List,
18 | ) : Schedule {
19 | override fun isActiveAt(dateTime: LocalDateTime): Boolean {
20 | return activeSchedule.isActiveAt(dateTime)
21 | }
22 |
23 | companion object {
24 | val DefaultSchedule = CommutingSchedule(
25 | days = setOf(MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY),
26 | start = LocalTime(12, 0),
27 | end = LocalTime(3, 0),
28 | )
29 |
30 | fun default(): CommutingConfiguration {
31 | return CommutingConfiguration(schedules = listOf(DefaultSchedule))
32 | }
33 | }
34 | }
35 |
36 | val CommutingConfiguration.activeSchedule: CommutingSchedule
37 | get() = schedules.firstOrNull() ?: CommutingConfiguration.DefaultSchedule
38 |
39 | @Serializable
40 | data class CommutingSchedule(
41 | override val days: Set,
42 | override val start: LocalTime,
43 | override val end: LocalTime,
44 | ) : DailySchedule
45 |
--------------------------------------------------------------------------------
/iosApp/widget/Interop.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Interop.swift
3 | // widget2Extension
4 | //
5 | // Created by Steven Kideckel on 2023-10-18.
6 | // Copyright © 2023 orgName. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import ComposeApp
11 | import SwiftUI
12 | import WidgetKit
13 |
14 | extension Date {
15 | func toKotlinInstant() -> Kotlinx_datetimeInstant {
16 | Kotlinx_datetimeInstant.Companion().fromEpochMilliseconds(
17 | epochMilliseconds: Int64(self.timeIntervalSince1970 * 1000)
18 | )
19 | }
20 | }
21 |
22 | extension Kotlinx_datetimeInstant {
23 | func toDate() -> Date {
24 | Date(timeIntervalSince1970: Double(self.toEpochMilliseconds()) / 1000.0)
25 | }
26 | }
27 |
28 | extension Bool {
29 | func toKotlinBoolean() -> KotlinBoolean {
30 | KotlinBoolean(bool: self)
31 | }
32 | }
33 |
34 | extension KotlinBoolean {
35 | func toBool() -> Bool {
36 | self.boolValue
37 | }
38 | }
39 |
40 | extension ColorWrapper {
41 | func toColor(isDark: Bool) -> SwiftUI.Color {
42 | let adjusted = adjustForDarkMode(isDark: isDark)
43 | return SwiftUI.Color(
44 | red: Double(adjusted.red),
45 | green: Double(adjusted.green),
46 | blue: Double(adjusted.blue)
47 | )
48 | }
49 | }
50 |
51 | extension TimeDisplay {
52 | func toKotlinTimeDisplay() -> ComposeApp.TimeDisplay {
53 | switch self {
54 | case .clock:
55 | return ComposeApp.TimeDisplay.clock
56 | case .relative:
57 | return ComposeApp.TimeDisplay.relative
58 | default:
59 | return ComposeApp.TimeDisplay.clock
60 | }
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/api/src/commonMain/kotlin/com/sixbynine/transit/path/model/Season.kt:
--------------------------------------------------------------------------------
1 | package com.sixbynine.transit.path.model
2 |
3 | import kotlinx.datetime.Instant
4 | import kotlinx.datetime.LocalDate
5 | import kotlinx.datetime.Month
6 | import kotlinx.datetime.Month.DECEMBER
7 | import kotlinx.datetime.Month.JUNE
8 | import kotlinx.datetime.Month.MARCH
9 | import kotlinx.datetime.Month.SEPTEMBER
10 | import kotlinx.datetime.TimeZone
11 | import kotlinx.datetime.toLocalDateTime
12 |
13 | enum class Season {
14 | Spring,
15 | Summer,
16 | Fall,
17 | Winter
18 | }
19 |
20 | object SeasonUtils {
21 | /**
22 | * Determines the current season based on equinoxes and solstices.
23 | * Uses the local system timezone for date calculations.
24 | *
25 | * Approximate dates:
26 | * - Spring Equinox: March 20
27 | * - Summer Solstice: June 21
28 | * - Fall Equinox: September 22
29 | * - Winter Solstice: December 21
30 | */
31 | fun getSeasonForInstant(instant: Instant): Season {
32 | val localDate = instant.toLocalDateTime(TimeZone.currentSystemDefault()).date
33 | val year = localDate.year
34 |
35 | val springEquinox = LocalDate(year, MARCH, 20)
36 | val summerSolstice = LocalDate(year, JUNE, 21)
37 | val fallEquinox = LocalDate(year, SEPTEMBER, 22)
38 | val winterSolstice = LocalDate(year, DECEMBER, 21)
39 |
40 | return when {
41 | localDate >= winterSolstice -> Season.Winter
42 | localDate >= fallEquinox -> Season.Fall
43 | localDate >= summerSolstice -> Season.Summer
44 | localDate >= springEquinox -> Season.Spring
45 | else -> Season.Winter
46 | }
47 | }
48 | }
49 |
50 |
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/com/sixbynine/transit/path/app/ui/setup/SetupScreenViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.sixbynine.transit.path.app.ui.setup
2 |
3 | import com.sixbynine.transit.path.app.ui.setup.SetupScreenContract.Effect
4 | import com.sixbynine.transit.path.app.ui.setup.SetupScreenContract.Intent.ConfirmClicked
5 | import com.sixbynine.transit.path.app.ui.setup.SetupScreenContract.Intent.StationCheckedChanged
6 | import dev.icerock.moko.mvvm.viewmodel.ViewModel
7 | import kotlinx.coroutines.channels.Channel
8 | import kotlinx.coroutines.flow.MutableStateFlow
9 | import kotlinx.coroutines.flow.asStateFlow
10 | import kotlinx.coroutines.flow.receiveAsFlow
11 | import kotlinx.coroutines.flow.update
12 | import kotlinx.coroutines.launch
13 |
14 | class SetupScreenViewModel : ViewModel() {
15 | private val _state = MutableStateFlow(SetupScreenContract.State())
16 | val state = _state.asStateFlow()
17 |
18 | private val _effects = Channel()
19 | val effects = _effects.receiveAsFlow()
20 |
21 | fun onIntent(intent: SetupScreenContract.Intent) {
22 | when (intent) {
23 | is StationCheckedChanged -> {
24 | _state.update {
25 | it.copy(
26 | selectedStations = if (intent.isChecked) {
27 | it.selectedStations + intent.station
28 | } else {
29 | it.selectedStations - intent.station
30 | }
31 | )
32 | }
33 | }
34 |
35 | ConfirmClicked -> {
36 | viewModelScope.launch {
37 | _effects.send(Effect.NavigateToHome)
38 | }
39 | }
40 | }
41 | }
42 | }
--------------------------------------------------------------------------------
/iosApp/iosApp/InfoPlist.xcstrings:
--------------------------------------------------------------------------------
1 | {
2 | "sourceLanguage" : "en",
3 | "strings" : {
4 | "CFBundleDisplayName" : {
5 | "comment" : "Bundle display name",
6 | "extractionState" : "extracted_with_value",
7 | "localizations" : {
8 | "en" : {
9 | "stringUnit" : {
10 | "state" : "new",
11 | "value" : "Departures for PATH"
12 | }
13 | },
14 | "es" : {
15 | "stringUnit" : {
16 | "state" : "translated",
17 | "value" : "Salidas de PATH"
18 | }
19 | }
20 | }
21 | },
22 | "CFBundleName" : {
23 | "comment" : "Bundle name",
24 | "extractionState" : "extracted_with_value",
25 | "localizations" : {
26 | "en" : {
27 | "stringUnit" : {
28 | "state" : "new",
29 | "value" : "Departures for PATH"
30 | }
31 | },
32 | "es" : {
33 | "stringUnit" : {
34 | "state" : "translated",
35 | "value" : "Salidas"
36 | }
37 | }
38 | }
39 | },
40 | "NSLocationWhenInUseUsageDescription" : {
41 | "comment" : "Privacy - Location When In Use Usage Description",
42 | "extractionState" : "extracted_with_value",
43 | "localizations" : {
44 | "en" : {
45 | "stringUnit" : {
46 | "state" : "new",
47 | "value" : "Your location will be used to identify the closest PATH station"
48 | }
49 | },
50 | "es" : {
51 | "stringUnit" : {
52 | "state" : "needs_review",
53 | "value" : "Su ubicación se utilizará para identificar la estación de PATH más cercana."
54 | }
55 | }
56 | }
57 | }
58 | },
59 | "version" : "1.0"
60 | }
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/com/sixbynine/transit/path/app/ui/home/TrainLine.kt:
--------------------------------------------------------------------------------
1 | package com.sixbynine.transit.path.app.ui.home
2 |
3 | import androidx.compose.material3.MaterialTheme
4 | import androidx.compose.runtime.Composable
5 | import androidx.compose.ui.Modifier
6 | import androidx.compose.ui.graphics.Color
7 | import androidx.compose.ui.text.TextStyle
8 | import com.sixbynine.transit.path.app.ui.common.AppUiTrainData
9 |
10 | @Composable
11 | fun HomeScreenScope.TrainLineContent(
12 | data: AppUiTrainData,
13 | modifier: Modifier = Modifier,
14 | textStyle: TextStyle = MaterialTheme.typography.titleMedium,
15 | subtitleTextStyle: TextStyle = MaterialTheme.typography.bodyLarge,
16 | textColor: Color = MaterialTheme.colorScheme.onSurface,
17 | fullWidth: Boolean = true,
18 | ) {
19 | TrainLineContent(
20 | data = listOf(data),
21 | modifier = modifier,
22 | textStyle = textStyle,
23 | subtitleTextStyle = subtitleTextStyle,
24 | textColor = textColor,
25 | fullWidth = fullWidth,
26 | )
27 | }
28 |
29 | @Composable
30 | fun HomeScreenScope.TrainLineContent(
31 | data: List,
32 | modifier: Modifier = Modifier,
33 | textStyle: TextStyle = MaterialTheme.typography.titleMedium,
34 | subtitleTextStyle: TextStyle = MaterialTheme.typography.bodyLarge,
35 | textColor: Color = MaterialTheme.colorScheme.onSurface,
36 | fullWidth: Boolean = true,
37 | ) {
38 | com.sixbynine.transit.path.app.ui.common.TrainLineContent(
39 | data = data,
40 | timeDisplay = state.timeDisplay,
41 | modifier = modifier,
42 | textStyle = textStyle,
43 | subtitleTextStyle = subtitleTextStyle,
44 | textColor = textColor,
45 | fullWidth = fullWidth,
46 | )
47 | }
48 |
--------------------------------------------------------------------------------
/schedule/generator/src/main/kotlin/com/sixbynine/transit/path/schedule/generator/ScheduleGenerator.kt:
--------------------------------------------------------------------------------
1 | package com.sixbynine.transit.path.schedule.generator
2 |
3 | import com.sixbynine.transit.path.Logging
4 | import com.sixbynine.transit.path.util.readRemoteFile
5 | import kotlinx.coroutines.CompletableDeferred
6 | import kotlinx.coroutines.CoroutineScope
7 | import kotlinx.coroutines.Dispatchers
8 | import kotlinx.coroutines.launch
9 | import kotlinx.coroutines.runBlocking
10 | import kotlinx.serialization.encodeToString
11 | import kotlinx.serialization.json.Json
12 | import java.io.File
13 | import kotlin.system.exitProcess
14 |
15 | private val jobComplete = CompletableDeferred()
16 |
17 | fun main() {
18 | val taskScope = CoroutineScope(Dispatchers.Default)
19 | taskScope.launch { ScheduleGenerator.generateSchedule() }
20 |
21 | runBlocking {
22 | jobComplete.await()
23 | exitProcess(0)
24 | }
25 | }
26 |
27 | object ScheduleGenerator {
28 | private val json = Json { }
29 |
30 | suspend fun generateSchedule() {
31 | val response =
32 | readRemoteFile("https://www.panynj.gov/content/path/en.model.json")
33 | .getOrElse {
34 | Logging.e("Failed to reach path model", it)
35 | exitProcess(1)
36 | }
37 |
38 | val schedules = ScheduleParser.parse(response, "regular")
39 |
40 | val outputDir = File("build/outputs")
41 | outputDir.mkdirs()
42 |
43 | val outputFile = File(outputDir, "schedule.json")
44 | val jsonString = json.encodeToString(schedules)
45 | outputFile.writeText(jsonString)
46 |
47 | println("Schedule written to: ${outputFile.absolutePath} successfully.")
48 |
49 | jobComplete.complete(Unit)
50 | }
51 | }
52 |
53 |
--------------------------------------------------------------------------------
/platform/src/iosMain/kotlin/com/sixbynine/transit/path/util/GlobalDataStore.ios.kt:
--------------------------------------------------------------------------------
1 | package com.sixbynine.transit.path.util
2 |
3 | import platform.Foundation.NSUserDefaults
4 |
5 | object NativeGlobalDataStore : GlobalDataStore {
6 |
7 | private val defaults: NSUserDefaults =
8 | NSUserDefaults(suiteName = "group.com.sixbynine.transit.path")
9 |
10 | override fun set(key: String, value: String?) {
11 | if (value == null) {
12 | remove(key)
13 | return
14 | }
15 | defaults.setObject(value, key)
16 | defaults.synchronize()
17 | }
18 |
19 | override fun getString(key: String): String? {
20 | return defaults.stringForKey(key)
21 | }
22 |
23 | override fun set(key: String, value: Boolean?) {
24 | if (value == null) {
25 | remove(key)
26 | return
27 | }
28 | defaults.setBool(value, key)
29 | defaults.synchronize()
30 | }
31 |
32 | override fun getBoolean(key: String): Boolean? {
33 | if (defaults.objectForKey(key) == null) {
34 | return null
35 | }
36 | return defaults.boolForKey(key)
37 | }
38 |
39 | override fun set(key: String, value: Long?) {
40 | if (value == null) {
41 | remove(key)
42 | return
43 | }
44 | defaults.setInteger(value, key)
45 | defaults.synchronize()
46 | }
47 |
48 | override fun getLong(key: String): Long? {
49 | if (defaults.objectForKey(key) == null) {
50 | return null
51 | }
52 | return defaults.integerForKey(key)
53 | }
54 |
55 | private fun remove(key: String) {
56 | defaults.removeObjectForKey(key)
57 | }
58 | }
59 |
60 | actual fun globalDataStore(): GlobalDataStore = NativeGlobalDataStore
61 |
--------------------------------------------------------------------------------
/api/src/androidUnitTest/resources/com/sixbynine/transit/path/everbridge_alert_jan18.json:
--------------------------------------------------------------------------------
1 | {
2 | "status": "Success",
3 | "data": [
4 | {
5 | "incidentMessage": {
6 | "subject": "Hack River Bridge Lift Fri 01-17-2025",
7 | "preMessage": "Plan Ahead: Fri 01-17-2025 12:30pm: Mandatory Hack River Bridge lift scheduled. Time subject to change. Possible NWK-WTC impacts. Please allow extra travel time.",
8 | "formVariableItems": [
9 | {
10 | "val": [
11 | "NWK-WTC"
12 | ],
13 | "variableName": "Lines",
14 | "isRequired": true,
15 | "seq": 1,
16 | "variableId": 367103739691127
17 | },
18 | {
19 | "val": [
20 | "01-17-2025"
21 | ],
22 | "variableName": "Date",
23 | "isRequired": true,
24 | "seq": 2,
25 | "variableId": 392942262944022,
26 | "prefixName": "1"
27 | },
28 | {
29 | "val": [
30 | "Fri"
31 | ],
32 | "variableName": "Day of the Week",
33 | "isRequired": true,
34 | "seq": 3,
35 | "variableId": 392942262944016,
36 | "prefixName": "1"
37 | },
38 | {
39 | "val": [
40 | "12:30pm"
41 | ],
42 | "variableName": "Time Range 1",
43 | "isRequired": true,
44 | "seq": 4,
45 | "variableId": 2626187817910379,
46 | "prefixName": "1"
47 | }
48 | ],
49 | "sysVarTodayDateFormat": "mm-dd-yyyy",
50 | "sysVarCurrentTimeFormat": "HH:mm:ss"
51 | },
52 | "CreatedDate": "1737118615522",
53 | "ModifiedDate": "1737118615522"
54 | }
55 | ]
56 | }
--------------------------------------------------------------------------------
/composeApp/src/androidMain/kotlin/com/sixbynine/transit/path/app/settings/DevOptionsExport.android.kt:
--------------------------------------------------------------------------------
1 | package com.sixbynine.transit.path.app.settings
2 |
3 | import android.content.Intent
4 | import androidx.core.content.FileProvider
5 | import com.sixbynine.transit.path.LogRecord
6 | import com.sixbynine.transit.path.PathApplication
7 | import com.sixbynine.transit.path.app.ui.ActivityRegistry
8 | import com.sixbynine.transit.path.time.NewYorkTimeZone
9 | import kotlinx.datetime.toLocalDateTime
10 | import java.io.BufferedWriter
11 | import java.io.File
12 | import java.io.FileWriter
13 |
14 | actual fun exportDevLogs(logs: List) {
15 | val context = PathApplication.instance
16 | val activity = ActivityRegistry.peekCreatedActivity() ?: return
17 | val filename = "logs.tsv"
18 | val logsDir = File(context.cacheDir, "logs")
19 | logsDir.mkdirs()
20 | val file = File(logsDir, filename)
21 | file.createNewFile()
22 |
23 | BufferedWriter(FileWriter(file)).use { writer ->
24 | writer.write("timestamp\tlevel\tmessage")
25 | writer.newLine()
26 |
27 | logs.asReversed().forEach { log ->
28 | writer.write(log.timestamp.toLocalDateTime(NewYorkTimeZone).toString())
29 | writer.write("\t")
30 | writer.write(log.level.toString())
31 | writer.write("\t")
32 | writer.write(log.message)
33 | writer.newLine()
34 | }
35 | }
36 |
37 | val uri = FileProvider.getUriForFile(context, context.packageName + ".provider", file)
38 | val shareIntent = Intent(Intent.ACTION_SEND).apply {
39 | type = "text/tsv"
40 | putExtra(Intent.EXTRA_STREAM, uri)
41 | addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
42 | }
43 | activity.startActivity(Intent.createChooser(shareIntent, "Share TSV"))
44 | }
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/com/sixbynine/transit/path/app/ui/PathViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.sixbynine.transit.path.app.ui
2 |
3 | import com.sixbynine.transit.path.util.launchAndReturnUnit
4 | import dev.icerock.moko.mvvm.viewmodel.ViewModel
5 | import kotlinx.coroutines.CoroutineScope
6 | import kotlinx.coroutines.Dispatchers
7 | import kotlinx.coroutines.cancel
8 | import kotlinx.coroutines.flow.Flow
9 | import kotlinx.coroutines.flow.StateFlow
10 | import kotlin.time.ComparableTimeMark
11 | import kotlin.time.Duration.Companion.milliseconds
12 | import kotlin.time.TimeSource.Monotonic
13 |
14 | abstract class PathViewModel : ViewModel() {
15 |
16 | private var lastIntent: Intent? = null
17 | private var lastIntentTime: ComparableTimeMark? = null
18 |
19 | protected val lightweightScope = CoroutineScope(Dispatchers.Default)
20 |
21 | protected open val rateLimitedIntents: Set = emptySet()
22 |
23 | override fun onCleared() {
24 | super.onCleared()
25 | lightweightScope.cancel()
26 | }
27 |
28 | abstract val state: StateFlow
29 | abstract val effects: Flow
30 |
31 | protected abstract suspend fun performIntent(intent: Intent)
32 |
33 | fun onIntent(intent: Intent) = lightweightScope.launchAndReturnUnit {
34 | val lastIntent = lastIntent
35 | val lastIntentTime = lastIntentTime
36 | val now = Monotonic.markNow()
37 | if (lastIntent == intent &&
38 | lastIntentTime != null &&
39 | lastIntentTime > (now - 500.milliseconds) &&
40 | intent in rateLimitedIntents
41 | ) {
42 | return@launchAndReturnUnit
43 | }
44 | this.lastIntent = intent
45 | this.lastIntentTime = now
46 | performIntent(intent)
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/com/sixbynine/transit/path/app/ui/ViewModelScreen.kt:
--------------------------------------------------------------------------------
1 | package com.sixbynine.transit.path.app.ui
2 |
3 | import LocalNavigator
4 | import androidx.compose.runtime.Composable
5 | import androidx.compose.runtime.LaunchedEffect
6 | import androidx.compose.runtime.collectAsState
7 | import androidx.compose.runtime.getValue
8 | import androidx.compose.ui.platform.LocalUriHandler
9 | import androidx.compose.ui.platform.UriHandler
10 | import dev.icerock.moko.mvvm.compose.getViewModel
11 | import dev.icerock.moko.mvvm.compose.viewModelFactory
12 | import kotlinx.coroutines.flow.Flow
13 | import moe.tlaster.precompose.navigation.Navigator
14 |
15 | @Composable
16 | fun ViewModelScreen(
17 | viewModelKey: String,
18 | createViewModel: () -> PathViewModel,
19 | onEffect: suspend HandleEffectsScope.(Effect) -> Unit,
20 | content: @Composable ScreenScope.() -> Unit,
21 | ) {
22 | val viewModel = getViewModel(
23 | key = viewModelKey,
24 | factory = viewModelFactory { createViewModel() }
25 | )
26 | HandleEffects(viewModel.effects) { effect ->
27 | onEffect(effect)
28 | }
29 | val state by viewModel.state.collectAsState()
30 | val scope = ScreenScope(state, viewModel::onIntent)
31 | scope.content()
32 | }
33 |
34 | @Composable
35 | fun HandleEffects(flow: Flow, onEffect: suspend HandleEffectsScope.(T) -> Unit) {
36 | val scope = HandleEffectsScope(LocalNavigator.current, LocalUriHandler.current)
37 | LaunchedEffect(Unit) {
38 | flow.collect { scope.onEffect(it) }
39 | }
40 | }
41 |
42 | data class HandleEffectsScope(val navigator: Navigator, val uriHandler: UriHandler)
43 |
44 | data class ScreenScope(val state: State, val onIntent: (Intent) -> Unit)
45 |
--------------------------------------------------------------------------------
/api/src/commonMain/kotlin/com/sixbynine/transit/path/model/Colors.kt:
--------------------------------------------------------------------------------
1 | package com.sixbynine.transit.path.model
2 |
3 | import androidx.compose.ui.graphics.Color
4 | import com.sixbynine.transit.path.Logging
5 | import com.sixbynine.transit.path.api.templine.HobClosureConfigRepository
6 |
7 | object Colors {
8 | fun parse(hexString: String): Color {
9 | return Color(hexString.removePrefix("#").toLong(16) or 0x00000000FF000000)
10 | }
11 |
12 | val Path: ColorWrapper
13 | get() = ColorWrapper(Color(0xFF1896D1))
14 |
15 | val NwkWtcSingle = Color(red = 0xd9, green = 0x3a, blue = 0x30).wrap()
16 | val Jsq33sSingle = Color(red = 0xff, green = 0x99, blue = 0x00).wrap()
17 | val Hob33sSingle = Color(red = 0x4d, green = 0x92, blue = 0xfb).wrap()
18 | val HobWtcSingle = Color(red = 0x65, green = 0xc1, blue = 0x00).wrap()
19 | val Wtc33sSingle by lazy {
20 | HobClosureConfigRepository.getConfig().tempLineInfo.lightColor
21 | .let { parse(it) }
22 | .wrap()
23 | }
24 | val NwkWtc = listOf(NwkWtcSingle)
25 | val Jsq33s = listOf(Jsq33sSingle)
26 | val Hob33s = listOf(Hob33sSingle)
27 | val HobWtc = listOf(HobWtcSingle)
28 | val Wtc33s by lazy { listOf(Wtc33sSingle) }
29 |
30 | fun background(isDark: Boolean): ColorWrapper {
31 | Logging.initialize()
32 | return if (isDark) {
33 | Color(0xFF191C1E).wrap()
34 | } else {
35 | Color(0xFFFCFCFF).wrap()
36 | }
37 | }
38 |
39 | fun Color.wrap(): ColorWrapper {
40 | return ColorWrapper(this)
41 | }
42 |
43 | infix fun Color.approxEquals(other: Color): Boolean {
44 | val dR = red - other.red
45 | val dG = green - other.green
46 | val dB = blue - other.blue
47 | val delta = (dR * dR) + (dG * dG) + (dB * dB)
48 | return delta < .1f
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/com/sixbynine/transit/path/api/LineExtensions.kt:
--------------------------------------------------------------------------------
1 | package com.sixbynine.transit.path.api
2 |
3 | import com.sixbynine.transit.path.api.Line.Hoboken33rd
4 | import com.sixbynine.transit.path.api.Line.HobokenWtc
5 | import com.sixbynine.transit.path.api.Line.JournalSquare33rd
6 | import com.sixbynine.transit.path.api.Line.NewarkWtc
7 | import com.sixbynine.transit.path.api.impl.LineComputer
8 | import com.sixbynine.transit.path.app.ui.common.AppUiTrainData
9 | import com.sixbynine.transit.path.model.ColorWrapper
10 | import com.sixbynine.transit.path.model.Colors
11 | import com.sixbynine.transit.path.model.DepartureBoardData.SignData
12 | import com.sixbynine.transit.path.model.DepartureBoardData.TrainData
13 |
14 | val Line.colors: List
15 | get() = when (this) {
16 | NewarkWtc -> Colors.NwkWtc
17 | HobokenWtc -> Colors.HobWtc
18 | JournalSquare33rd -> Colors.Jsq33s
19 | Hoboken33rd -> Colors.Hob33s
20 | }
21 |
22 | private fun Line.matches(
23 | colors: Collection,
24 | target: String,
25 | stationId: String
26 | ): Boolean {
27 | return this in LineComputer.computeLines(station = stationId, target = target, colors = colors)
28 | }
29 |
30 | fun Line.matches(data: TrainData, stationId: String): Boolean {
31 | return matches(data.colors, data.title, stationId)
32 | }
33 |
34 | fun Collection.anyMatch(data: TrainData, stationId: String): Boolean {
35 | if (containsAll(Line.permanentLines)) return true
36 | return any { it.matches(data, stationId) }
37 | }
38 |
39 | fun Collection.anyMatch(data: SignData, stationId: String): Boolean {
40 | if (containsAll(Line.permanentLines)) return true
41 | return any { it.matches(data.colors, data.title, stationId) }
42 | }
43 |
44 | fun Line.matches(data: AppUiTrainData, stationId: String): Boolean {
45 | return matches(data.colors, data.title, stationId)
46 | }
47 |
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/com/sixbynine/transit/path/app/ui/icon/NativeIcon.kt:
--------------------------------------------------------------------------------
1 | package com.sixbynine.transit.path.app.ui.icon
2 |
3 | import PlatformType.ANDROID
4 | import PlatformType.DESKTOP
5 | import PlatformType.IOS
6 | import androidx.compose.foundation.layout.size
7 | import androidx.compose.material3.Icon
8 | import androidx.compose.material3.IconButton
9 | import androidx.compose.material3.LocalContentColor
10 | import androidx.compose.runtime.Composable
11 | import androidx.compose.ui.Modifier
12 | import androidx.compose.ui.graphics.Color
13 | import androidx.compose.ui.graphics.isSpecified
14 | import androidx.compose.ui.graphics.painter.Painter
15 | import androidx.compose.ui.unit.Dp
16 | import androidx.compose.ui.unit.dp
17 | import getPlatform
18 |
19 | enum class IconType {
20 | Edit, Station, Filter, Sort, LayoutOneColumn, ArrowUp, ArrowDown, Settings, Delete, Back,
21 | ExpandDown, Internet
22 | }
23 |
24 | @Composable
25 | expect fun IconPainter(icon: IconType): Painter
26 |
27 | @Composable
28 | fun NativeIconButton(
29 | icon: IconType,
30 | contentDescription: String?,
31 | onClick: () -> Unit,
32 | modifier: Modifier = Modifier,
33 | tint: Color = Color.Unspecified,
34 | enabled: Boolean = true,
35 | buttonSize: Dp = 48.dp,
36 | iconSize: Dp = 24.dp,
37 | ) {
38 | val actualIconSize = when (getPlatform().type) {
39 | ANDROID, DESKTOP -> iconSize
40 | IOS -> iconSize - 4.dp
41 | }
42 | val painter = IconPainter(icon)
43 | return IconButton(
44 | onClick = onClick,
45 | modifier = modifier.size(buttonSize),
46 | enabled = enabled,
47 | ) {
48 | Icon(
49 | painter = painter,
50 | contentDescription = contentDescription,
51 | modifier = Modifier.size(actualIconSize),
52 | tint = if (tint.isSpecified) tint else LocalContentColor.current,
53 | )
54 | }
55 | }
56 |
--------------------------------------------------------------------------------