Author: Adam Evyčędo <git@apiote.xyz>
merge develop into master for version 3.3.0
%!v(PANIC=String method: strings: negative Repeat count)
diff --git a/CHANGELOG.adoc b/CHANGELOG.adoc index e3c5a8f241db4dcc20a981fbc9b060c28283ad16..115cb1a406cd760a463bc2ee73f41809c5a42379 100644 --- a/CHANGELOG.adoc +++ b/CHANGELOG.adoc @@ -13,7 +13,28 @@ == Unreleased * Travel planning * Offline timetable + +== [3.3] – 2024-05-16 + +=== Added + * Alerts +* Selecting date +* Filtering by time or line +* Automatically adding locales from build.gradle + +=== Changed + +* Cached localities have lower alpha +* Updated deprecated methods +* Updated dependencies + +=== Fixed + +* Landscape about screen +* Vehicle capabilities on map +* Lines directions other than 2 +* Feed info is cached, not latest response == [3.2] – 2024-03-13 diff --git a/README.adoc b/README.adoc index 38eaec80f70222d212a80400c87d9782157e7096..d0ddcbe2c696355c6b278213be5538fd8556182e 100644 --- a/README.adoc +++ b/README.adoc @@ -4,7 +4,7 @@ // SPDX-License-Identifier: GPL-3.0-or-later = Bimba Adam Evyčędo <me@apiote.xyz> -v3.2 2024-03-13 +v3.3 2024-05-16 :toc: Bimba is a FLOSS public transport passenger companion; a timetable in your pocket. @@ -65,4 +65,3 @@ * https://github.com/tebriz159 for new logo * https://fonts.google.com/icons[Material Icons], © Google Apache 2.0 -* https://github.com/mancj/MaterialSearchBar[Search bar], © mancj MIT diff --git a/app/build.gradle b/app/build.gradle index 8c58bf222e861cffd0db7c5e995856f2e498b6f4..74c83573693cf4ef41816d56aaaf424ba0785c77 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -11,6 +11,7 @@ id 'com.android.application' id 'org.jetbrains.kotlin.android' id "org.jetbrains.kotlin.plugin.parcelize" id 'org.jetbrains.kotlin.plugin.serialization' version '1.9.22' + id 'com.mermake.locale-resource-generator' version '0.1' } android { @@ -20,15 +21,16 @@ defaultConfig { applicationId "xyz.apiote.bimba.czwek" minSdk 21 targetSdk 34 - versionCode 24 - versionName "3.2.0" + versionCode 25 + versionName "3.3.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" - resourceConfigurations += ["en", "pl", "it"] + resourceConfigurations += ["en", "pl", "it", "de"] } applicationVariants.configureEach { variant -> variant.resValue "string", "versionName", variant.versionName + variant.resValue "string", "applicationId", variant.applicationId } buildTypes { @@ -50,12 +52,12 @@ buildToolsVersion = '34.0.0' } dependencies { - implementation 'androidx.core:core-ktx:1.12.0' + implementation 'androidx.core:core-ktx:1.13.1' implementation 'androidx.appcompat:appcompat:1.6.1' - implementation 'com.google.android.material:material:1.11.0' + implementation 'com.google.android.material:material:1.12.0' implementation 'androidx.constraintlayout:constraintlayout:2.1.4' - implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.7.0' - implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.7.0' + implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.8.0' + implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.0' implementation 'androidx.navigation:navigation-fragment-ktx:2.7.7' implementation 'androidx.navigation:navigation-ui-ktx:2.7.7' implementation 'androidx.legacy:legacy-support-v4:1.0.0' @@ -63,7 +65,7 @@ implementation 'androidx.core:core-splashscreen:1.0.1' implementation 'com.google.openlocationcode:openlocationcode:1.0.4' implementation 'org.osmdroid:osmdroid-android:6.1.18' implementation 'org.yaml:snakeyaml:2.2' - implementation 'androidx.activity:activity-ktx:1.8.2' + implementation 'androidx.activity:activity-ktx:1.9.0' implementation 'com.google.openlocationcode:openlocationcode:1.0.4' implementation 'com.otaliastudios:zoomlayout:1.9.0' implementation 'dev.bandb.graphview:graphview:0.8.1' diff --git a/app/src/debug/java/xyz/apiote/bimba/czwek/api/responses/DevResponses.kt b/app/src/debug/java/xyz/apiote/bimba/czwek/api/responses/DevResponses.kt new file mode 100644 index 0000000000000000000000000000000000000000..e7e36dc7168480786df2c912ee01962d0bfb3826 --- /dev/null +++ b/app/src/debug/java/xyz/apiote/bimba/czwek/api/responses/DevResponses.kt @@ -0,0 +1,123 @@ +// SPDX-FileCopyrightText: Adam Evyčędo +// +// SPDX-License-Identifier: GPL-3.0-or-later + +package xyz.apiote.bimba.czwek.api.responses + +import xyz.apiote.bimba.czwek.api.AlertV1 +import xyz.apiote.bimba.czwek.api.DepartureV4 +import xyz.apiote.bimba.czwek.api.LineV3 +import xyz.apiote.bimba.czwek.api.LocatableV3 +import xyz.apiote.bimba.czwek.api.QueryableV4 +import xyz.apiote.bimba.czwek.api.StopV2 +import xyz.apiote.bimba.czwek.api.UnknownResourceVersionException +import xyz.apiote.bimba.czwek.api.VehicleV3 +import xyz.apiote.bimba.czwek.api.structs.FeedInfoV2 +import xyz.apiote.fruchtfleisch.Reader +import java.io.InputStream + +data class DeparturesResponseDev( + val alerts: List<AlertV1>, + val departures: List<DepartureV4>, + val stop: StopV2 +) : DeparturesResponse { + companion object { + fun unmarshal(stream: InputStream): DeparturesResponseDev { + val alerts = mutableListOf<AlertV1>() + val departures = mutableListOf<DepartureV4>() + + val reader = Reader(stream) + val alertsNum = reader.readUInt().toULong() + for (i in 0UL until alertsNum) { + val alert = AlertV1.unmarshal(stream) + alerts.add(alert) + } + val departuresNum = reader.readUInt().toULong() + for (i in 0UL until departuresNum) { + val departure = DepartureV4.unmarshal(stream) + departures.add(departure) + } + + return DeparturesResponseDev(alerts, departures, StopV2.unmarshal(stream)) + } + } +} + +data class FeedsResponseDev( + val feeds: List<FeedInfoV2> +) : FeedsResponse { + companion object { + fun unmarshal(stream: InputStream): FeedsResponseDev { + val feeds = mutableListOf<FeedInfoV2>() + val reader = Reader(stream) + val n = reader.readUInt().toULong() + for (i in 0UL until n) { + feeds.add(FeedInfoV2.unmarshal(stream)) + } + return FeedsResponseDev(feeds) + } + } +} + +data class LineResponseDev( + val line: LineV3 +) : LineResponse { + companion object { + fun unmarshal(stream: InputStream): LineResponseDev { + return LineResponseDev(LineV3.unmarshal(stream)) + } + } +} + +data class LocatablesResponseDev(val locatables: List<LocatableV3>) : LocatablesResponse { + companion object { + fun unmarshal(stream: InputStream): LocatablesResponseDev { + val locatables = mutableListOf<LocatableV3>() + val reader = Reader(stream) + val n = reader.readUInt().toULong() + for (i in 0UL until n) { + when (val r = reader.readUInt().toULong()) { + 0UL -> { + locatables.add(StopV2.unmarshal(stream)) + } + + 1UL -> { + locatables.add(VehicleV3.unmarshal(stream)) + } + + else -> { + throw UnknownResourceVersionException("Locatable/$r", 0u) + } + } + } + return LocatablesResponseDev(locatables) + } + } +} + + +data class QueryablesResponseDev(val queryables: List<QueryableV4>) : QueryablesResponse { + companion object { + fun unmarshal(stream: InputStream): QueryablesResponseDev { + val queryables = mutableListOf<QueryableV4>() + val reader = Reader(stream) + val n = reader.readUInt().toULong() + for (i in 0UL until n) { + when (val r = reader.readUInt().toULong()) { + 0UL -> { + queryables.add(StopV2.unmarshal(stream)) + } + + 1UL -> { + queryables.add(LineV3.unmarshal(stream)) + } + + else -> { + throw UnknownResourceVersionException("Queryable/$r", 0u) + } + } + } + return QueryablesResponseDev(queryables) + } + } +} diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 2714bb3942edb3cf3ad94de74d9cacc8b8a5eca8..f7c0314f0b78f5e5a72bccc33f8d7b940088a0d9 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -22,7 +22,7 @@ android:enableOnBackInvokedCallback="true" android:fullBackupContent="@xml/backup_rules" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" - android:localeConfig="@xml/locales_config" + android:localeConfig="@xml/locale_config" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/Theme.Bimba.Style" diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/api/Api.kt b/app/src/main/java/xyz/apiote/bimba/czwek/api/Api.kt index 8a942fd52c4c7aed749d85e8dc70c1953f7bdc22..3ebf1676b286d06d1505a26a162af9843274a88a 100644 --- a/app/src/main/java/xyz/apiote/bimba/czwek/api/Api.kt +++ b/app/src/main/java/xyz/apiote/bimba/czwek/api/Api.kt @@ -7,6 +7,8 @@ import android.content.Context import android.content.Context.MODE_PRIVATE import android.net.ConnectivityManager +import android.net.NetworkCapabilities +import android.os.Build import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import xyz.apiote.bimba.czwek.R @@ -17,6 +19,8 @@ import java.net.HttpURLConnection import java.net.MalformedURLException import java.net.URL import java.net.URLEncoder +import java.time.LocalDate +import java.time.format.DateTimeFormatter // todo [3.2] constants // todo [3.2] split api files to classes files @@ -45,20 +49,20 @@ data class Result(val stream: InputStream?, val error: Error?) data class Error(val statusCode: Int, val stringResource: Int, val imageResource: Int) -suspend fun getBimba(cm: ConnectivityManager, server: Server): Result { +suspend fun getBimba(context: Context, server: Server): Result { return try { rawRequest( - URL("${hostWithScheme(server.host)}/.well-known/traffic-api"), server, cm, emptyArray() + URL("${hostWithScheme(server.host)}/.well-known/traffic-api"), server, context, emptyArray() ) } catch (e: MalformedURLException) { Result(null, Error(0, R.string.error_url, R.drawable.error_url)) } } -suspend fun getFeeds(cm: ConnectivityManager, server: Server): Result { +suspend fun getFeeds(context: Context, server: Server): Result { return try { rawRequest( - URL("${server.apiPath}/"), server, cm, arrayOf(1u, 2u) + URL("${server.apiPath}/"), server, context, arrayOf(1u, 2u) ) } catch (_: MalformedURLException) { Result(null, Error(0, R.string.error_url, R.drawable.error_url)) @@ -66,7 +70,7 @@ } } suspend fun queryQueryables( - cm: ConnectivityManager, server: Server, query: String, limit: Int? = null + context: Context, server: Server, query: String, limit: Int? = null ): Result { val params = mutableMapOf("q" to query) if (limit != null) { @@ -77,61 +81,67 @@ server, "queryables", null, params, - cm, - arrayOf(1u, 2u, 3u), + context, + arrayOf(1u, 2u, 3u, 4u), null ) } -suspend fun locateQueryables(cm: ConnectivityManager, server: Server, near: PositionV1): Result { +suspend fun locateQueryables(context: Context, server: Server, near: PositionV1): Result { return request( server, "queryables", null, mapOf("near" to near.toString()), - cm, - arrayOf(1u, 2u, 3u), + context, + arrayOf(1u, 2u, 3u, 4u), null ) } suspend fun getLocatablesIn( - cm: ConnectivityManager, server: Server, bl: PositionV1, tr: PositionV1 + context: Context, server: Server, bl: PositionV1, tr: PositionV1 ): Result { return request( server, "locatables", null, mapOf("lb" to bl.toString(), "rt" to tr.toString()), - cm, + context, arrayOf(1u, 2u, 3u), null ) } -suspend fun getLine(cm: ConnectivityManager, server: Server, feedID: String, line: String): Result { +suspend fun getLine( + context: Context, + server: Server, + feedID: String, + lineName: String, + lineID: String +): Result { return request( server, "lines", - line, - mapOf(), - cm, - arrayOf(1u, 2u), + lineName, + mapOf("line" to lineID), + context, + arrayOf(1u, 2u, 3u), feedID ) } suspend fun getDepartures( - cm: ConnectivityManager, + context: Context, server: Server, feedID: String, stop: String, - line: String? = null, + date: LocalDate?, limit: Int? = null ): Result { val params = mutableMapOf("code" to stop) - if (line != null) { - params["line"] = line + if (date != null) { + params["date"] = date.format(DateTimeFormatter.ISO_LOCAL_DATE) } if (limit != null) { params["limit"] = limit.toString(10) @@ -141,8 +151,8 @@ server, "departures", null, params, - cm, - arrayOf(1u, 2u, 3u), + context, + arrayOf(1u, 2u, 3u, 4u), feedID ) } @@ -165,14 +175,17 @@ } } suspend fun rawRequest( - url: URL, server: Server, cm: ConnectivityManager, responseVersion: Array<UInt> + url: URL, server: Server, context: Context, responseVersion: Array<UInt> ): Result { - @Suppress("DEPRECATION") // fixme later(API_29, API_23) https://developer.android.com/reference/android/net/ConnectivityManager#getActiveNetwork() - if (cm.activeNetworkInfo == null) { + if (!isNetworkAvailable(context)) { return Result(null, Error(0, R.string.error_offline, R.drawable.error_net)) } return withContext(Dispatchers.IO) { val c = (url.openConnection() as HttpURLConnection).apply { + setRequestProperty( + "User-Agent", + "${context.getString(R.string.applicationId)}/${context.getString(R.string.versionName)} (${Build.VERSION.SDK_INT})" + ) setRequestProperty("X-Bimba-Token", server.token) responseVersion.forEach { addRequestProperty("Accept", "application/$it+bare") } } @@ -189,12 +202,30 @@ } } } +private fun isNetworkAvailable(context: Context): Boolean { + val connectivityManager = + context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager + + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + connectivityManager.activeNetwork?.let {network -> + connectivityManager.getNetworkCapabilities(network)?.let {capabilities -> + capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) || capabilities.hasTransport( + NetworkCapabilities.TRANSPORT_CELLULAR + ) || capabilities.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET) + } + }?: false + } else { + @Suppress("DEPRECATION") + connectivityManager.activeNetworkInfo?.isConnected ?: false + } +} + suspend fun request( server: Server, resource: String, item: String?, params: Map<String, String>, - cm: ConnectivityManager, + context: Context, responseVersion: Array<UInt>, feeds: String? ): Result { @@ -204,7 +235,7 @@ "${server.apiPath}/${feeds?.ifEmpty { server.feeds.getIDs() } ?: server.feeds.getIDs()}/$resource${ if (item == null) { "" } else { - "/$item" + "/${URLEncoder.encode(item, "utf-8")}" } }${ params.map { @@ -216,7 +247,7 @@ }" }.joinToString("&", "?") }" ) - rawRequest(url, server, cm, responseVersion) + rawRequest(url, server, context, responseVersion) } } diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/api/Interfaces.kt b/app/src/main/java/xyz/apiote/bimba/czwek/api/Interfaces.kt index ca416f8c472519ef800011d579d02f1d735e6bf6..4bfc2f5c505cd365ec5a973bc01a4558856f35c9 100644 --- a/app/src/main/java/xyz/apiote/bimba/czwek/api/Interfaces.kt +++ b/app/src/main/java/xyz/apiote/bimba/czwek/api/Interfaces.kt @@ -7,6 +7,7 @@ interface QueryableV1 interface QueryableV2 interface QueryableV3 +interface QueryableV4 interface LocatableV1 interface LocatableV2 interface LocatableV3 \ No newline at end of file diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/api/Structs.kt b/app/src/main/java/xyz/apiote/bimba/czwek/api/Structs.kt index 5a1468c35018ec56820e55c0129b1983e8aaf534..9e604c62e01918732d5d03565aa5492cb5989dd5 100644 --- a/app/src/main/java/xyz/apiote/bimba/czwek/api/Structs.kt +++ b/app/src/main/java/xyz/apiote/bimba/czwek/api/Structs.kt @@ -11,11 +11,6 @@ import org.yaml.snakeyaml.Yaml import xyz.apiote.bimba.czwek.api.structs.VehicleStatusV1 import xyz.apiote.fruchtfleisch.Reader import java.io.InputStream -import java.time.LocalDate -import java.time.LocalDateTime -import java.time.LocalTime -import java.time.ZoneId -import java.time.ZonedDateTime import kotlin.reflect.KClass class TrafficFormatException(override val message: String) : IllegalArgumentException() @@ -200,16 +195,6 @@ reader.readI8(), reader.readString() ) } - } - - fun toDateTime(): ZonedDateTime { - return ZonedDateTime.of( - LocalDateTime.of( - LocalDate.now().plusDays(DayOffset.toLong()), - LocalTime.of(Hour.toInt(), Minute.toInt(), Second.toInt()) - ), - ZoneId.of(Zone) - ) } } @@ -456,6 +441,35 @@ } } } +data class DepartureV4( + val ID: String, + val time: Time, + val status: VehicleStatusV1, + val isRealtime: Boolean, + val vehicle: VehicleV3, + val boarding: UByte, + val alerts: List<AlertV1> +) { + + companion object { + fun unmarshal(stream: InputStream): DepartureV4 { + val reader = Reader(stream) + val id = reader.readString() + val time = Time.unmarshal(stream) + val status = VehicleStatusV1.of(reader.readUInt().toULong().toUInt()) + val isRealtime = reader.readBoolean() + val vehicle = VehicleV3.unmarshal(stream) + val boarding = reader.readU8() + val alertsNum = reader.readUInt().toULong() + val alerts = mutableListOf<AlertV1>() + for (i in 0UL until alertsNum) { + alerts.add(AlertV1.unmarshal(stream)) + } + return DepartureV4(id, time, status, isRealtime, vehicle, boarding, alerts) + } + } +} + @Parcelize data class StopV2( val code: String, @@ -465,7 +479,7 @@ val zone: String, val feedID: String, val position: PositionV1, val changeOptions: List<ChangeOptionV1> -) : QueryableV2, Parcelable, LocatableV2, QueryableV3, LocatableV3 { +) : QueryableV2, Parcelable, LocatableV2, QueryableV3, LocatableV3, QueryableV4 { companion object { fun unmarshal(stream: InputStream): StopV2 { val reader = Reader(stream) @@ -599,6 +613,54 @@ for (i in 0UL until directionsNum) { graphs.add(LineGraph.unmarshal(stream)) } return LineV2( + name = name, + colour = colour, + type = LineTypeV3.of(type.toULong().toUInt()), + feedID = feedID, + headsigns = headsigns, + graphs = graphs + ) + } + } +} + +data class LineV3( + val id: String, + val name: String, + val colour: ColourV1, + val type: LineTypeV3, + val feedID: String, + val headsigns: List<List<String>>, + val graphs: List<LineGraph>, +) : QueryableV4 { + override fun toString(): String { + return "$name ($type) [$colour]\n${headsigns.map { "-> ${it.joinToString()}" }}" + } + + companion object { + fun unmarshal(stream: InputStream): LineV3 { + val reader = Reader(stream) + val id = reader.readString() + val name = reader.readString() + val colour = ColourV1.unmarshal(stream) + val type = reader.readUInt() + val feedID = reader.readString() + var directionsNum = reader.readUInt().toULong() + val headsigns = (0UL until directionsNum).map { + val headsignsNum = reader.readUInt().toULong() + val headsignsDir = mutableListOf<String>() + for (j in 0UL until headsignsNum) { + headsignsDir.add(reader.readString()) + } + headsignsDir + } + directionsNum = reader.readUInt().toULong() + val graphs = mutableListOf<LineGraph>() + for (i in 0UL until directionsNum) { + graphs.add(LineGraph.unmarshal(stream)) + } + return LineV3( + id = id, name = name, colour = colour, type = LineTypeV3.of(type.toULong().toUInt()), diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/api/responses/Departures.kt b/app/src/main/java/xyz/apiote/bimba/czwek/api/responses/Departures.kt index ea9d6279fd223e940cb9df6f90889e221c2e435d..0d053162b88f3d4f2a65975d1d2cff92e2b979a4 100644 --- a/app/src/main/java/xyz/apiote/bimba/czwek/api/responses/Departures.kt +++ b/app/src/main/java/xyz/apiote/bimba/czwek/api/responses/Departures.kt @@ -8,9 +8,9 @@ import xyz.apiote.bimba.czwek.api.AlertV1 import xyz.apiote.bimba.czwek.api.DepartureV1 import xyz.apiote.bimba.czwek.api.DepartureV2 import xyz.apiote.bimba.czwek.api.DepartureV3 +import xyz.apiote.bimba.czwek.api.DepartureV4 import xyz.apiote.bimba.czwek.api.StopV1 import xyz.apiote.bimba.czwek.api.StopV2 -import xyz.apiote.bimba.czwek.api.responses.UnknownResponseVersion import xyz.apiote.fruchtfleisch.Reader import java.io.InputStream @@ -19,25 +19,26 @@ companion object { fun unmarshal(stream: InputStream): DeparturesResponse { val reader = Reader(stream) return when (val v = reader.readUInt().toULong()) { - 0UL -> DeparturesResponseDev.unmarshal(stream) + // 0UL -> DeparturesResponseDev.unmarshal(stream) 1UL -> DeparturesResponseV1.unmarshal(stream) 2UL -> DeparturesResponseV2.unmarshal(stream) 3UL -> DeparturesResponseV3.unmarshal(stream) + 4UL -> DeparturesResponseV4.unmarshal(stream) else -> throw UnknownResponseVersion("Departures", v) } } } } -data class DeparturesResponseDev( +data class DeparturesResponseV4( val alerts: List<AlertV1>, - val departures: List<DepartureV3>, + val departures: List<DepartureV4>, val stop: StopV2 ) : DeparturesResponse { companion object { - fun unmarshal(stream: InputStream): DeparturesResponseDev { + fun unmarshal(stream: InputStream): DeparturesResponseV4 { val alerts = mutableListOf<AlertV1>() - val departures = mutableListOf<DepartureV3>() + val departures = mutableListOf<DepartureV4>() val reader = Reader(stream) val alertsNum = reader.readUInt().toULong() @@ -47,11 +48,11 @@ alerts.add(alert) } val departuresNum = reader.readUInt().toULong() for (i in 0UL until departuresNum) { - val departure = DepartureV3.unmarshal(stream) + val departure = DepartureV4.unmarshal(stream) departures.add(departure) } - return DeparturesResponseDev(alerts, departures, StopV2.unmarshal(stream)) + return DeparturesResponseV4(alerts, departures, StopV2.unmarshal(stream)) } } } @@ -62,7 +63,7 @@ val departures: List, val stop: StopV2 ) : DeparturesResponse { companion object { - fun unmarshal(stream: InputStream): DeparturesResponseDev { + fun unmarshal(stream: InputStream): DeparturesResponseV3 { val alerts = mutableListOf<AlertV1>() val departures = mutableListOf<DepartureV3>() @@ -78,7 +79,7 @@ val departure = DepartureV3.unmarshal(stream) departures.add(departure) } - return DeparturesResponseDev(alerts, departures, StopV2.unmarshal(stream)) + return DeparturesResponseV3(alerts, departures, StopV2.unmarshal(stream)) } } } diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/api/responses/Feeds.kt b/app/src/main/java/xyz/apiote/bimba/czwek/api/responses/Feeds.kt index 2db68ec32cf82ff44a86c514e5111cf2142f5c6f..2bb1c7b23d5db5ea1184c2a787c42124f2ce0211 100644 --- a/app/src/main/java/xyz/apiote/bimba/czwek/api/responses/Feeds.kt +++ b/app/src/main/java/xyz/apiote/bimba/czwek/api/responses/Feeds.kt @@ -4,7 +4,6 @@ // SPDX-License-Identifier: GPL-3.0-or-later package xyz.apiote.bimba.czwek.api.responses -import xyz.apiote.bimba.czwek.api.responses.UnknownResponseVersion import xyz.apiote.bimba.czwek.api.structs.FeedInfoV1 import xyz.apiote.bimba.czwek.api.structs.FeedInfoV2 import xyz.apiote.fruchtfleisch.Reader @@ -15,27 +14,11 @@ companion object { fun unmarshal(stream: InputStream): FeedsResponse { val reader = Reader(stream) return when (val v = reader.readUInt().toULong()) { - 0UL -> FeedsResponseDev.unmarshal(stream) + // 0UL -> FeedsResponseDev.unmarshal(stream) 1UL -> FeedsResponseV1.unmarshal(stream) 2UL -> FeedsResponseV2.unmarshal(stream) else -> throw UnknownResponseVersion("Feeds", v) } - } - } -} - -data class FeedsResponseDev( - val feeds: List<FeedInfoV2> -) : FeedsResponse { - companion object { - fun unmarshal(stream: InputStream): FeedsResponseDev { - val feeds = mutableListOf<FeedInfoV2>() - val reader = Reader(stream) - val n = reader.readUInt().toULong() - for (i in 0UL until n) { - feeds.add(FeedInfoV2.unmarshal(stream)) - } - return FeedsResponseDev(feeds) } } } diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/api/responses/Line.kt b/app/src/main/java/xyz/apiote/bimba/czwek/api/responses/Line.kt index 1a4528959aac80bc5f0887ea98a32120318ce351..2ea9f2c732a68df8155e79559e7a23741de253c5 100644 --- a/app/src/main/java/xyz/apiote/bimba/czwek/api/responses/Line.kt +++ b/app/src/main/java/xyz/apiote/bimba/czwek/api/responses/Line.kt @@ -6,7 +6,7 @@ package xyz.apiote.bimba.czwek.api.responses import xyz.apiote.bimba.czwek.api.LineV1 import xyz.apiote.bimba.czwek.api.LineV2 -import xyz.apiote.bimba.czwek.api.responses.UnknownResponseVersion +import xyz.apiote.bimba.czwek.api.LineV3 import xyz.apiote.fruchtfleisch.Reader import java.io.InputStream @@ -15,21 +15,22 @@ companion object { fun unmarshal(stream: InputStream): LineResponse { val reader = Reader(stream) return when (val v = reader.readUInt().toULong()) { - 0UL -> LineResponseDev.unmarshal(stream) + // 0UL -> LineResponseDev.unmarshal(stream) 1UL -> LineResponseV1.unmarshal(stream) 2UL -> LineResponseV2.unmarshal(stream) + 3UL -> LineResponseV3.unmarshal(stream) else -> throw UnknownResponseVersion("Line", v) } } } } -data class LineResponseDev( - val line: LineV2 +data class LineResponseV3( + val line: LineV3 ) : LineResponse { companion object { - fun unmarshal(stream: InputStream): LineResponseDev { - return LineResponseDev(LineV2.unmarshal(stream)) + fun unmarshal(stream: InputStream): LineResponseV3 { + return LineResponseV3(LineV3.unmarshal(stream)) } } } @@ -38,8 +39,8 @@ data class LineResponseV2( val line: LineV2 ) : LineResponse { companion object { - fun unmarshal(stream: InputStream): LineResponseDev { - return LineResponseDev(LineV2.unmarshal(stream)) + fun unmarshal(stream: InputStream): LineResponseV2 { + return LineResponseV2(LineV2.unmarshal(stream)) } } } diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/api/responses/Locatables.kt b/app/src/main/java/xyz/apiote/bimba/czwek/api/responses/Locatables.kt index f126d564f35d713d84edb60e202ef413e0622e4c..a176bd2dff780abb44aa27b89af2679233679fe7 100644 --- a/app/src/main/java/xyz/apiote/bimba/czwek/api/responses/Locatables.kt +++ b/app/src/main/java/xyz/apiote/bimba/czwek/api/responses/Locatables.kt @@ -10,7 +10,6 @@ import xyz.apiote.bimba.czwek.api.LocatableV3 import xyz.apiote.bimba.czwek.api.StopV1 import xyz.apiote.bimba.czwek.api.StopV2 import xyz.apiote.bimba.czwek.api.UnknownResourceVersionException -import xyz.apiote.bimba.czwek.api.responses.UnknownResponseVersion import xyz.apiote.bimba.czwek.api.VehicleV1 import xyz.apiote.bimba.czwek.api.VehicleV2 import xyz.apiote.bimba.czwek.api.VehicleV3 @@ -22,7 +21,7 @@ companion object { fun unmarshal(stream: InputStream): LocatablesResponse { val reader = Reader(stream) return when (val v = reader.readUInt().toULong()) { - 0UL -> LocatablesResponseDev.unmarshal(stream) + // 0UL -> LocatablesResponseDev.unmarshal(stream) 1UL -> LocatablesResponseV1.unmarshal(stream) 2UL -> LocatablesResponseV2.unmarshal(stream) 3UL -> LocatablesResponseV3.unmarshal(stream) @@ -32,31 +31,6 @@ } } } -data class LocatablesResponseDev(val locatables: List<LocatableV3>) : LocatablesResponse { - companion object { - fun unmarshal(stream: InputStream): LocatablesResponseDev { - val locatables = mutableListOf<LocatableV3>() - val reader = Reader(stream) - val n = reader.readUInt().toULong() - for (i in 0UL until n) { - when (val r = reader.readUInt().toULong()) { - 0UL -> { - locatables.add(StopV2.unmarshal(stream)) - } - - 1UL -> { - locatables.add(VehicleV3.unmarshal(stream)) - } - - else -> { - throw UnknownResourceVersionException("Locatable/$r", 0u) - } - } - } - return LocatablesResponseDev(locatables) - } - } -} data class LocatablesResponseV3(val locatables: List<LocatableV3>) : LocatablesResponse { companion object { diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/api/responses/Queryables.kt b/app/src/main/java/xyz/apiote/bimba/czwek/api/responses/Queryables.kt index 9e5d0a467f6b052d867a9777d3f7ce0e5a9a8a14..8e7cc8f8c7a63204b4c2d69604ae33f2fe4ac768 100644 --- a/app/src/main/java/xyz/apiote/bimba/czwek/api/responses/Queryables.kt +++ b/app/src/main/java/xyz/apiote/bimba/czwek/api/responses/Queryables.kt @@ -6,13 +6,14 @@ package xyz.apiote.bimba.czwek.api.responses import xyz.apiote.bimba.czwek.api.LineV1 import xyz.apiote.bimba.czwek.api.LineV2 +import xyz.apiote.bimba.czwek.api.LineV3 import xyz.apiote.bimba.czwek.api.QueryableV1 import xyz.apiote.bimba.czwek.api.QueryableV2 import xyz.apiote.bimba.czwek.api.QueryableV3 +import xyz.apiote.bimba.czwek.api.QueryableV4 import xyz.apiote.bimba.czwek.api.StopV1 import xyz.apiote.bimba.czwek.api.StopV2 import xyz.apiote.bimba.czwek.api.UnknownResourceVersionException -import xyz.apiote.bimba.czwek.api.responses.UnknownResponseVersion import xyz.apiote.fruchtfleisch.Reader import java.io.InputStream @@ -21,20 +22,21 @@ companion object { fun unmarshal(stream: InputStream): QueryablesResponse { val reader = Reader(stream) return when (val v = reader.readUInt().toULong()) { - 0UL -> QueryablesResponseDev.unmarshal(stream) + // 0UL -> QueryablesResponseDev.unmarshal(stream) 1UL -> QueryablesResponseV1.unmarshal(stream) 2UL -> QueryablesResponseV2.unmarshal(stream) 3UL -> QueryablesResponseV3.unmarshal(stream) + 4UL -> QueryablesResponseV4.unmarshal(stream) else -> throw UnknownResponseVersion("Queryables", v) } } } } -data class QueryablesResponseDev(val queryables: List<QueryableV3>) : QueryablesResponse { +data class QueryablesResponseV4(val queryables: List<QueryableV4>) : QueryablesResponse { companion object { - fun unmarshal(stream: InputStream): QueryablesResponseDev { - val queryables = mutableListOf<QueryableV3>() + fun unmarshal(stream: InputStream): QueryablesResponseV4 { + val queryables = mutableListOf<QueryableV4>() val reader = Reader(stream) val n = reader.readUInt().toULong() for (i in 0UL until n) { @@ -44,7 +46,7 @@ queryables.add(StopV2.unmarshal(stream)) } 1UL -> { - queryables.add(LineV2.unmarshal(stream)) + queryables.add(LineV3.unmarshal(stream)) } else -> { @@ -52,14 +54,14 @@ throw UnknownResourceVersionException("Queryable/$r", 0u) } } } - return QueryablesResponseDev(queryables) + return QueryablesResponseV4(queryables) } } } data class QueryablesResponseV3(val queryables: List<QueryableV3>) : QueryablesResponse { companion object { - fun unmarshal(stream: InputStream): QueryablesResponseDev { + fun unmarshal(stream: InputStream): QueryablesResponseV3 { val queryables = mutableListOf<QueryableV3>() val reader = Reader(stream) val n = reader.readUInt().toULong() @@ -78,7 +80,7 @@ throw UnknownResourceVersionException("Queryable/$r", 0u) } } } - return QueryablesResponseDev(queryables) + return QueryablesResponseV3(queryables) } } } diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/dashboard/MainActivity.kt b/app/src/main/java/xyz/apiote/bimba/czwek/dashboard/MainActivity.kt index 76d0caa48af1122b8b7f89b2beab8543cedfd5d9..090e870cac6a76658ce555f4a8e7a0eac3f98355 100644 --- a/app/src/main/java/xyz/apiote/bimba/czwek/dashboard/MainActivity.kt +++ b/app/src/main/java/xyz/apiote/bimba/czwek/dashboard/MainActivity.kt @@ -44,7 +44,7 @@ private lateinit var binding: ActivityMainBinding private lateinit var locationPermissionRequest: ActivityResultLauncher<Array<String>> private lateinit var permissionAsker: Fragment - var locationPermissionDialogShown = false + private var locationPermissionDialogShown = false override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/dashboard/ui/home/HomeViewModel.kt b/app/src/main/java/xyz/apiote/bimba/czwek/dashboard/ui/home/HomeViewModel.kt index 38afc1078fe260f2e80a6553869cc720b446b48e..d42be675e2f6c4b569a608c3c0e66e7598f1b4ab 100644 --- a/app/src/main/java/xyz/apiote/bimba/czwek/dashboard/ui/home/HomeViewModel.kt +++ b/app/src/main/java/xyz/apiote/bimba/czwek/dashboard/ui/home/HomeViewModel.kt @@ -5,7 +5,6 @@ package xyz.apiote.bimba.czwek.dashboard.ui.home import android.content.Context -import android.net.ConnectivityManager import android.os.Handler import android.os.Looper import android.text.Editable diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/dashboard/ui/map/MapFragment.kt b/app/src/main/java/xyz/apiote/bimba/czwek/dashboard/ui/map/MapFragment.kt index 20f56bd9673a6a06a4c62de22349b81b694a365a..bd2f1614e6497260be727826f03a5a6e3ccf2b9e 100644 --- a/app/src/main/java/xyz/apiote/bimba/czwek/dashboard/ui/map/MapFragment.kt +++ b/app/src/main/java/xyz/apiote/bimba/czwek/dashboard/ui/map/MapFragment.kt @@ -5,12 +5,12 @@ package xyz.apiote.bimba.czwek.dashboard.ui.map import android.annotation.SuppressLint -import android.content.Context import android.content.Context.MODE_PRIVATE import android.content.SharedPreferences -import android.content.res.Configuration.* +import android.content.res.Configuration.UI_MODE_NIGHT_MASK +import android.content.res.Configuration.UI_MODE_NIGHT_UNDEFINED +import android.content.res.Configuration.UI_MODE_NIGHT_YES import android.graphics.Bitmap -import android.net.ConnectivityManager import android.os.Bundle import android.os.Handler import android.os.Looper @@ -103,11 +103,13 @@ } binding.map.addMapListener(object : MapListener { override fun onScroll(event: ScrollEvent?): Boolean { - return onMapMove() + onMapMove() + return true } override fun onZoom(event: ZoomEvent?): Boolean { - return onMapMove() + onMapMove() + return true } }) @@ -119,18 +121,17 @@ return root } - private fun onMapMove(): Boolean { + private fun onMapMove() { snack?.dismiss() - return delayGetLocatables() + delayGetLocatables() } - private fun delayGetLocatables(delay: Long = 1000): Boolean { + private fun delayGetLocatables(delay: Long = 1000) { handler.removeCallbacks(workRunnable) workRunnable = Runnable { getLocatables() } handler.postDelayed(workRunnable, delay) - return true } private fun observeLocatables() { @@ -197,8 +198,7 @@ Position(it.latNorth, it.lonEast) ) } context?.let { - val cm = it.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager - mapViewModel.getLocatablesIn(cm, bl, tr, it) + mapViewModel.getLocatablesIn(bl, tr, it) } delayGetLocatables(30000) } diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/dashboard/ui/map/MapViewModel.kt b/app/src/main/java/xyz/apiote/bimba/czwek/dashboard/ui/map/MapViewModel.kt index a129db6bbe44d613664c6861c8de3daea78e6a44..f53541d8f75292860a260fd389f57fe047e1bf32 100644 --- a/app/src/main/java/xyz/apiote/bimba/czwek/dashboard/ui/map/MapViewModel.kt +++ b/app/src/main/java/xyz/apiote/bimba/czwek/dashboard/ui/map/MapViewModel.kt @@ -7,7 +7,6 @@ import android.content.ActivityNotFoundException import android.content.Context import android.content.Intent -import android.net.ConnectivityManager import android.net.Uri import android.os.Bundle import android.util.Log @@ -16,17 +15,21 @@ import android.view.View import android.view.ViewGroup import android.widget.Button import android.widget.ImageView +import android.widget.LinearLayout import android.widget.TextView import android.widget.Toast -import androidx.constraintlayout.widget.Group +import androidx.appcompat.widget.TooltipCompat import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.google.android.material.bottomsheet.BottomSheetDialogFragment import kotlinx.coroutines.launch +import org.osmdroid.views.MapView import xyz.apiote.bimba.czwek.R import xyz.apiote.bimba.czwek.departures.DeparturesActivity +import xyz.apiote.bimba.czwek.repo.CongestionLevel import xyz.apiote.bimba.czwek.repo.Locatable +import xyz.apiote.bimba.czwek.repo.OccupancyStatus import xyz.apiote.bimba.czwek.repo.OnlineRepository import xyz.apiote.bimba.czwek.repo.Position import xyz.apiote.bimba.czwek.repo.Stop @@ -38,12 +41,12 @@ private val _locatables = MutableLiveData<List<Locatable>>() val locatables: MutableLiveData<List<Locatable>> = _locatables - fun getLocatablesIn(cm: ConnectivityManager, bl: Position, tr: Position, context: Context) { + fun getLocatablesIn(bl: Position, tr: Position, context: Context) { viewModelScope.launch { viewModelScope.launch { try { val repository = OnlineRepository() - _locatables.value = repository.getLocatablesIn(cm, bl, tr, context) ?: emptyList() + _locatables.value = repository.getLocatablesIn(context, bl, tr) ?: emptyList() } catch (e: TrafficResponseException) { Log.w("Map", "$e") } @@ -58,11 +61,8 @@ const val TAG = "MapBottomSheet" } private fun showVehicle(content: View, vehicle: Vehicle) { - content.findViewById<Group>(R.id.stop_group).visibility = View.GONE - content.findViewById<Group>(R.id.vehicle_group).visibility = View.VISIBLE - context?.let { ctx -> - content.findViewById<TextView>(R.id.title).apply { + content.findViewById<TextView>(R.id.line).apply { text = ctx.getString(R.string.vehicle_headsign, vehicle.Line.name, vehicle.Headsign) contentDescription = ctx.getString( R.string.vehicle_headsign_content_description, @@ -70,51 +70,95 @@ vehicle.Line.name, vehicle.Headsign ) } + + content.findViewById<TextView>(R.id.time).visibility = View.GONE + + content.findViewById<MapView>(R.id.map).visibility = View.GONE + content.findViewById<LinearLayout>(R.id.boarding).visibility = View.GONE + content.findViewById<ImageView>(R.id.rt_icon).visibility = View.GONE + // TODO vehicle accessible + content.findViewById<ImageView>(R.id.wheelchair_icon).visibility = View.GONE + // todo units -- [3.2] settings or system-based content.findViewById<TextView>(R.id.speed_text).text = ctx.getString(R.string.speed_in_km_per_h, vehicle.Speed * 3.6) + + content.findViewById<LinearLayout>(R.id.congestion).visibility = + if (vehicle.congestionLevel == CongestionLevel.UNKNOWN) View.GONE else View.VISIBLE content.findViewById<TextView>(R.id.congestion_text).text = vehicle.congestion(ctx) + + content.findViewById<LinearLayout>(R.id.occupancy).visibility = + if (vehicle.occupancyStatus == OccupancyStatus.UNKNOWN) View.GONE else View.VISIBLE content.findViewById<TextView>(R.id.occupancy_text).text = vehicle.occupancy(ctx) - content.findViewById<ImageView>(R.id.ac).visibility = - if (vehicle.getCapability(Vehicle.Capability.AC)) { - View.VISIBLE - } else { - View.GONE - } - content.findViewById<ImageView>(R.id.bike).visibility = - if (vehicle.getCapability(Vehicle.Capability.BIKE)) { - View.VISIBLE - } else { - View.GONE - } - content.findViewById<ImageView>(R.id.voice).visibility = - if (vehicle.getCapability(Vehicle.Capability.VOICE)) { - View.VISIBLE - } else { - View.GONE - } - content.findViewById<ImageView>(R.id.ticket).visibility = - if (vehicle.let { - it.getCapability(Vehicle.Capability.TICKET_DRIVER) || it.getCapability(Vehicle.Capability.TICKET_MACHINE) - }) { - View.VISIBLE - } else { - View.GONE - } - content.findViewById<ImageView>(R.id.usb).visibility = - if (vehicle.getCapability(Vehicle.Capability.USB_CHARGING)) { - View.VISIBLE - } else { - View.GONE - } + + content.findViewById<ImageView>(R.id.ac).let { + TooltipCompat.setTooltipText( + it, + getString(R.string.air_condition_content_description) + ) + it.visibility = + if (vehicle.getCapability(Vehicle.Capability.AC)) { + View.VISIBLE + } else { + View.GONE + } + } + content.findViewById<ImageView>(R.id.bike).let { + TooltipCompat.setTooltipText( + it, + getString(R.string.bicycles_allowed_content_description) + ) + it.visibility = + if (vehicle.getCapability(Vehicle.Capability.BIKE)) { + View.VISIBLE + } else { + View.GONE + } + } + content.findViewById<ImageView>(R.id.voice).let { + TooltipCompat.setTooltipText( + it, + getString(R.string.voice_announcements_content_description) + ) + it.visibility = + if (vehicle.getCapability(Vehicle.Capability.VOICE)) { + View.VISIBLE + } else { + View.GONE + } + } + content.findViewById<ImageView>(R.id.ticket).let { ticketImage -> + TooltipCompat.setTooltipText( + ticketImage, + getString(R.string.tickets_sold_content_description) + ) + ticketImage.visibility = + if (vehicle.let { + it.getCapability(Vehicle.Capability.TICKET_DRIVER) || it.getCapability(Vehicle.Capability.TICKET_MACHINE) + }) { + View.VISIBLE + } else { + View.GONE + } + } + content.findViewById<ImageView>(R.id.usb).let { + TooltipCompat.setTooltipText( + it, + getString(R.string.usb_charging_content_description) + ) + it.visibility = + if (vehicle.getCapability(Vehicle.Capability.USB_CHARGING)) { + View.VISIBLE + } else { + View.GONE + } + } } } private fun showStop(content: View, stop: Stop) { context?.let { ctx -> - content.findViewById<Group>(R.id.stop_group).visibility = View.VISIBLE - content.findViewById<Group>(R.id.vehicle_group).visibility = View.GONE - content.findViewById<TextView>(R.id.title).text = stop.name + content.findViewById<TextView>(R.id.stop_name).text = stop.name content.findViewById<Button>(R.id.departures_button).setOnClickListener { val intent = Intent(ctx, DeparturesActivity::class.java).apply { putExtra("code", stop.code) @@ -151,20 +195,22 @@ inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View { - val content = inflater.inflate(R.layout.map_bottom_sheet, container, false) - content.apply { - when (locatable) { - is Vehicle -> { + return when (locatable) { + is Vehicle -> { + inflater.inflate(R.layout.departure_bottom_sheet, container, false).apply { showVehicle(this, locatable) } + } - is Stop -> { + is Stop -> { + inflater.inflate(R.layout.stop_bottom_sheet, container, false).apply { showStop(this, locatable) } } + + else -> { + throw IllegalStateException("locatable is neither a vehicle nor a stop") + } } - //(dialog as BottomSheetDialog).behavior.peekHeight = dpToPixelI(90f) - - return content } } \ No newline at end of file diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/departures/Departures.kt b/app/src/main/java/xyz/apiote/bimba/czwek/departures/Departures.kt index 04c4d0a1eab4505ce9237ecf6e031cf6c5beeb22..7b334c3d4efecdfa7b18e8d98c1de5bb9d8e1e6c 100644 --- a/app/src/main/java/xyz/apiote/bimba/czwek/departures/Departures.kt +++ b/app/src/main/java/xyz/apiote/bimba/czwek/departures/Departures.kt @@ -15,11 +15,15 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.ImageView +import android.widget.LinearLayout import android.widget.TextView +import androidx.appcompat.widget.TooltipCompat import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView import com.google.android.material.bottomsheet.BottomSheetDialog import com.google.android.material.bottomsheet.BottomSheetDialogFragment +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import com.google.android.material.textview.MaterialTextView import org.osmdroid.tileprovider.tilesource.TileSourceFactory import org.osmdroid.util.GeoPoint import org.osmdroid.views.CustomZoomButtonsController @@ -29,7 +33,9 @@ import org.osmdroid.views.overlay.TilesOverlay import org.osmdroid.views.overlay.gestures.RotationGestureOverlay import xyz.apiote.bimba.czwek.R import xyz.apiote.bimba.czwek.dpToPixelI +import xyz.apiote.bimba.czwek.repo.CongestionLevel import xyz.apiote.bimba.czwek.repo.Departure +import xyz.apiote.bimba.czwek.repo.OccupancyStatus import xyz.apiote.bimba.czwek.repo.Vehicle import java.time.ZoneId import java.time.ZonedDateTime @@ -47,6 +53,7 @@ fun bind( departure: Departure, holder: BimbaDepartureViewHolder?, context: Context?, + showAsTime: Boolean, onClickListener: (Departure) -> Unit ) { holder?.root?.setOnClickListener { @@ -63,7 +70,7 @@ R.string.departure_headsign_content_description, departure.vehicle.Headsign ) - holder?.departureTime?.text = departure.statusText(context) + holder?.departureTime?.text = departure.statusText(context, showAsTime) } } } @@ -78,10 +85,12 @@ RecyclerView.Adapter () { var lastUpdate: ZonedDateTime = ZonedDateTime.of(0, 1, 1, 0, 0, 0, 0, ZoneId.systemDefault()) private set + private var showAsTime: Boolean = false inner class DiffUtilCallback( private val oldDepartures: List<Departure>, - private val newDepartures: List<Departure> + private val newDepartures: List<Departure>, + private val showAsTimeChanged: Boolean ) : DiffUtil.Callback() { override fun getOldListSize() = oldDepartures.size @@ -94,7 +103,10 @@ override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { val oldDeparture = oldDepartures[oldItemPosition] val newDeparture = newDepartures[newItemPosition] return oldDeparture.vehicle.Line == newDeparture.vehicle.Line && oldDeparture.vehicle.Headsign == newDeparture.vehicle.Headsign && - oldDeparture.statusText(context, lastUpdate) == newDeparture.statusText(context) + oldDeparture.statusText(context, false, lastUpdate) == newDeparture.statusText( + context, + false + ) && !showAsTimeChanged } } @@ -112,13 +124,19 @@ return BimbaDepartureViewHolder(rowView) } override fun onBindViewHolder(holder: BimbaDepartureViewHolder, position: Int) { - BimbaDepartureViewHolder.bind(departures[position], holder, context, onClickListener) + BimbaDepartureViewHolder.bind( + departures[position], + holder, + context, + showAsTime, + onClickListener + ) } override fun getItemCount(): Int = departures.size - fun get(ID: String): Departure? { - val position = departuresPositions[ID] + fun get(id: String): Departure? { + val position = departuresPositions[id] return if (position == null) { null } else { @@ -126,12 +144,14 @@ departures[position] } } - fun update(departures: List<Departure>, areNewObserved: Boolean = false) { + fun update(departures: List<Departure>, showAsTime: Boolean, areNewObserved: Boolean = false) { val newPositions: MutableMap<String, Int> = HashMap() departures.forEachIndexed { i, departure -> newPositions[departure.ID] = i } - val diff = DiffUtil.calculateDiff(DiffUtilCallback(this.departures, departures)) + val diff = DiffUtil.calculateDiff(DiffUtilCallback(this.departures, departures, this.showAsTime != showAsTime)) + + this.showAsTime = showAsTime this.departures = departures departuresPositions = newPositions @@ -142,7 +162,7 @@ diff.dispatchUpdatesTo(this) } fun refreshItems() { - update(this.departures) + update(this.departures, showAsTime) } } @@ -211,41 +231,94 @@ findViewById (R.id.boarding_text).text = departure.boardingText(ctx) // todo units -- [3.2] settings or system-based findViewById<TextView>(R.id.speed_text).text = getString(R.string.speed_in_km_per_h, departure.vehicle.Speed * 3.6) + + findViewById<LinearLayout>(R.id.congestion).visibility = + if (departure.vehicle.congestionLevel == CongestionLevel.UNKNOWN) View.GONE else View.VISIBLE findViewById<TextView>(R.id.congestion_text).text = departure.vehicle.congestion(ctx) + + findViewById<LinearLayout>(R.id.occupancy).visibility = + if (departure.vehicle.occupancyStatus == OccupancyStatus.UNKNOWN) View.GONE else View.VISIBLE findViewById<TextView>(R.id.occupancy_text).text = departure.vehicle.occupancy(ctx) - findViewById<ImageView>(R.id.ac).visibility = - if (departure.vehicle.getCapability(Vehicle.Capability.AC)) { - View.VISIBLE - } else { - View.GONE - } - findViewById<ImageView>(R.id.bike).visibility = - if (departure.vehicle.getCapability(Vehicle.Capability.BIKE)) { - View.VISIBLE - } else { - View.GONE - } - findViewById<ImageView>(R.id.voice).visibility = - if (departure.vehicle.getCapability(Vehicle.Capability.VOICE)) { - View.VISIBLE - } else { - View.GONE - } - findViewById<ImageView>(R.id.ticket).visibility = - if (departure.vehicle.let { + findViewById<ImageView>(R.id.ac).let { + TooltipCompat.setTooltipText( + it, + getString(R.string.air_condition_content_description) + ) + it.visibility = + if (departure.vehicle.getCapability(Vehicle.Capability.AC)) View.VISIBLE else View.GONE + } + + findViewById<ImageView>(R.id.bike).let { + TooltipCompat.setTooltipText( + it, + getString(R.string.bicycles_allowed_content_description) + ) + it.visibility = + if (departure.vehicle.getCapability(Vehicle.Capability.BIKE)) { + View.VISIBLE + } else { + View.GONE + } + } + + findViewById<ImageView>(R.id.voice).let { + TooltipCompat.setTooltipText( + it, + getString(R.string.voice_announcements_content_description) + ) + it.visibility = + if (departure.vehicle.getCapability(Vehicle.Capability.VOICE)) { + View.VISIBLE + } else { + View.GONE + } + } + findViewById<ImageView>(R.id.ticket).let { ticketImage -> + TooltipCompat.setTooltipText( + ticketImage, + getString(R.string.tickets_sold_content_description) + ) + ticketImage.visibility = if (departure.vehicle.let { it.getCapability(Vehicle.Capability.TICKET_DRIVER) || it.getCapability(Vehicle.Capability.TICKET_MACHINE) }) { View.VISIBLE } else { View.GONE } - findViewById<ImageView>(R.id.usb).visibility = - if (departure.vehicle.getCapability(Vehicle.Capability.USB_CHARGING)) { - View.VISIBLE - } else { - View.GONE + } + findViewById<ImageView>(R.id.usb).let { + TooltipCompat.setTooltipText( + it, + getString(R.string.usb_charging_content_description) + ) + it.visibility = + if (departure.vehicle.getCapability(Vehicle.Capability.USB_CHARGING)) { + View.VISIBLE + } else { + View.GONE + } + } + + if (departure.alerts.isNotEmpty()) { + findViewById<MaterialTextView>(R.id.alerts_text).text = departure.alerts.map { + it.header.ifEmpty { + getString(R.string.alert_header) + } + }.toSet().joinToString(separator = "\n") + findViewById<LinearLayout>(R.id.alerts).apply { + visibility = View.VISIBLE + setOnClickListener { + MaterialAlertDialogBuilder(context) + .setTitle("Alerts") + .setPositiveButton(R.string.ok) { _, _ -> } + .setMessage(departure.alerts.map { it.description }.filter { it != "" } + .joinToString(separator = "\n")) + .show() + } } + } + findViewById<MapView>(R.id.map).let { map -> if (departure.vehicle.Position.isZero()) { map.visibility = View.GONE @@ -255,13 +328,13 @@ map.controller.apply { GeoPoint( departure.vehicle.location().latitude, departure.vehicle.location().longitude - ).let { + ).let { geoPoint -> if (updating) { animateTo( - it, 19.0f.toDouble(), 3 * 1000 + geoPoint, 19.0f.toDouble(), 3 * 1000 ) } else { - setCenter(it) + setCenter(geoPoint) setZoom(19f.toDouble()) } } diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/departures/DeparturesActivity.kt b/app/src/main/java/xyz/apiote/bimba/czwek/departures/DeparturesActivity.kt index f47185c9f0a0948217cb80a2aa3dbbb6db37e310..3ace64fd176fe6079d89e6ecd848ed72e9a42ec6 100644 --- a/app/src/main/java/xyz/apiote/bimba/czwek/departures/DeparturesActivity.kt +++ b/app/src/main/java/xyz/apiote/bimba/czwek/departures/DeparturesActivity.kt @@ -5,12 +5,17 @@ package xyz.apiote.bimba.czwek.departures import android.content.Intent +import android.net.ConnectivityManager +import android.net.ConnectivityManager.NetworkCallback +import android.net.Network +import android.net.NetworkCapabilities +import android.net.NetworkRequest +import android.os.Build import android.os.Bundle import android.os.Handler import android.os.Looper import android.text.format.DateUtils import android.text.format.DateUtils.MINUTE_IN_MILLIS -import android.util.Log import android.view.View import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.content.res.AppCompatResources @@ -19,13 +24,21 @@ import androidx.core.view.WindowCompat import androidx.lifecycle.ViewModelProvider import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView +import com.google.android.material.datepicker.MaterialDatePicker +import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.snackbar.Snackbar +import com.google.android.material.timepicker.MaterialTimePicker +import com.google.android.material.timepicker.TimeFormat import kotlinx.coroutines.Runnable import xyz.apiote.bimba.czwek.R import xyz.apiote.bimba.czwek.api.Error import xyz.apiote.bimba.czwek.databinding.ActivityDeparturesBinding import xyz.apiote.bimba.czwek.repo.Departure import xyz.apiote.bimba.czwek.repo.Stop +import java.time.Instant +import java.time.LocalDate +import java.time.LocalTime +import java.time.ZoneId import java.time.ZonedDateTime class DeparturesActivity : AppCompatActivity() { @@ -42,6 +55,20 @@ private lateinit var snackbar: Snackbar private lateinit var viewModel: DeparturesViewModel + private val datePicker = + MaterialDatePicker.Builder.datePicker().setTitleText(R.string.title_select_date) + .setNegativeButtonText(R.string.clear_date_selection) + .build() + private var timePickerStart: MaterialTimePicker? = null + private var timePickerEnd: MaterialTimePicker? = null + private var linePicker: MaterialAlertDialogBuilder? = null + private var date: LocalDate? = null + private val linesFilter = mutableMapOf<String, Boolean>() + private val linesFilterTemporary = mutableMapOf<String, Boolean>() + private var startTime: LocalTime = LocalTime.MIN + private var endTime: LocalTime = LocalTime.MAX + private var alertDescriptions: String = "" + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) _binding = ActivityDeparturesBinding.inflate(layoutInflater) @@ -49,20 +76,150 @@ setContentView(binding.root) viewModel = ViewModelProvider(this)[DeparturesViewModel::class.java] + linePicker = MaterialAlertDialogBuilder(this) + .setTitle(resources.getString(R.string.title_select_line)) + .setNegativeButton(R.string.clear_date_selection) { _, _ -> + linesFilter.clear() + getDepartures() + } + .setPositiveButton(R.string.ok) { _, _ -> + linesFilterTemporary.forEach { linesFilter[it.key] = it.value } + getDepartures() + } + + binding.moreButton.setOnClickListener { + MaterialAlertDialogBuilder(this) + .setTitle("Alerts") + .setPositiveButton(R.string.ok) { _, _ -> } + .setMessage(alertDescriptions) + .show() + } + viewModel.departures.observe(this) { stopDepartures -> - updateItems(stopDepartures.departures, stopDepartures.stop) + if (stopDepartures.alerts.isNotEmpty()) { + binding.alerts.visibility = View.VISIBLE + binding.alertsText.text = stopDepartures.alerts.map { + it.header.ifEmpty { + getString(R.string.alert_header) + } + }.toSet().joinToString(separator = "\n") + alertDescriptions = stopDepartures.alerts.map { it.description }.filter { it != "" } + .joinToString(separator = "\n") + binding.moreButton.visibility = if (alertDescriptions == "") View.GONE else View.VISIBLE + + } else { + binding.alerts.visibility = View.GONE + } + updateItems( + stopDepartures.departures + .filter { d -> + linesFilter.values.all { !it } or (linesFilter[d.vehicle.Line.name] ?: false) + } + .filter { d -> + val t = LocalTime.of(d.time.Hour.toInt(), d.time.Minute.toInt()) + t >= startTime && t <= endTime + }, + stopDepartures.stop + ) openBottomSheet?.departureID()?.let { adapter.get(it) }?.let { openBottomSheet?.update(it) } + + val lines = stopDepartures.departures.map { it.vehicle.Line.name }.sortedWith { s1, s2 -> + val s1n = s1.toIntOrNull() + val s2n = s2.toIntOrNull() + if (s1n != null && s2n != null) { + s1.toInt() - s2.toInt() + } else { + s1.compareTo(s2) + } + }.toSet().toTypedArray() + val selections = lines.map { linesFilter.getOrDefault(it, false) }.toBooleanArray() + + linePicker?.setMultiChoiceItems(lines, selections) { _, which, checked -> + linesFilterTemporary[lines[which]] = checked + } } viewModel.error.observe(this) { showError(it) } + binding.departuresAppBar.menu.findItem(R.id.departures_filter_bytime).setEnabled(false) + + + datePicker.addOnNegativeButtonClickListener { + date = null + startTime = LocalTime.MIN + endTime = LocalTime.MAX + binding.departuresAppBar.menu.findItem(R.id.departures_filter_bytime).setEnabled(false) + getDepartures(true) + } + datePicker.addOnPositiveButtonClickListener { + if (date == null) { + startTime = LocalTime.MIN + endTime = LocalTime.MAX + } + date = Instant.ofEpochMilli(it).atZone(ZoneId.systemDefault()) + .toLocalDate() + binding.departuresAppBar.menu.findItem(R.id.departures_filter_bytime).setEnabled(true) + getDepartures(true) + } + binding.collapsingLayout.apply { title = getName() val tf = ResourcesCompat.getFont(this@DeparturesActivity, R.font.yellowcircle8) setCollapsedTitleTypeface(tf) setExpandedTitleTypeface(tf) } + binding.departuresAppBar.setOnMenuItemClickListener { + when (it.itemId) { + R.id.departures_calendar -> { + datePicker.show(supportFragmentManager, "datePicker") + true + } + + R.id.departures_filter_byline -> { + linesFilterTemporary.clear() + linesFilter.forEach { filter -> linesFilterTemporary[filter.key] = filter.value } + linePicker?.show() + true + } + + R.id.departures_filter_bytime -> { + timePickerStart = + MaterialTimePicker.Builder().setTitleText(R.string.title_select_time_start) + .setTimeFormat(TimeFormat.CLOCK_24H) + .setHour(startTime.hour) + .setMinute(startTime.minute) + .setNegativeButtonText(R.string.clear_date_selection) + .build() + timePickerEnd = MaterialTimePicker.Builder().setTitleText(R.string.title_select_time_end) + .setTimeFormat(TimeFormat.CLOCK_24H) + .setHour(endTime.hour) + .setMinute(endTime.minute) + .setNegativeButtonText(R.string.clear_date_selection) + .build() + timePickerEnd!!.addOnPositiveButtonClickListener { + endTime = LocalTime.of(timePickerEnd!!.hour, timePickerEnd!!.minute) + getDepartures(true) + } + timePickerEnd!!.addOnNegativeButtonClickListener { + endTime = LocalTime.MAX + getDepartures(true) + } + timePickerStart!!.addOnPositiveButtonClickListener { + startTime = LocalTime.of(timePickerStart!!.hour, timePickerStart!!.minute) + timePickerEnd!!.show(supportFragmentManager, "timePickerEnd") + } + timePickerStart!!.addOnNegativeButtonClickListener { + startTime = LocalTime.MIN + timePickerEnd!!.show(supportFragmentManager, "timePickerEnd") + } + timePickerStart!!.show(supportFragmentManager, "timePickerStart") + true + } + + else -> super.onOptionsItemSelected(it) + } + } binding.departuresRecycler.layoutManager = LinearLayoutManager(this) binding.departuresRecycler.addOnScrollListener( @@ -91,6 +248,33 @@ binding.departuresRecycler.adapter = adapter WindowCompat.setDecorFitsSystemWindows(window, false) snackbar = Snackbar.make(binding.root, "", Snackbar.LENGTH_INDEFINITE) + + val networkCallback: NetworkCallback = object : NetworkCallback() { + override fun onAvailable(network: Network) { + getDepartures() + } + + override fun onLost(network: Network) { + } + } + + val connectivityManager = getSystemService(CONNECTIVITY_SERVICE) as ConnectivityManager + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + connectivityManager.registerDefaultNetworkCallback(networkCallback) + } else { + val request = NetworkRequest.Builder() + .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET).build() + connectivityManager.registerNetworkCallback(request, networkCallback) + } + } + + override fun onStart() { + super.onStart() + linesFilter.clear() + getLine()?.let { + linesFilter[it] = true + } } override fun onResume() { @@ -103,6 +287,12 @@ super.onPause() handler.removeCallbacks(runnable) } + override fun onStop() { + super.onStop() + handler.removeCallbacks(runnable) + handler.removeCallbacksAndMessages(null) + } + private fun getName(): String { return when (intent?.action) { Intent.ACTION_VIEW -> getString(R.string.stop_from_qr_code) @@ -118,10 +308,14 @@ else -> null } } - fun getDepartures() { - adapter.refreshItems() - setupSnackbar() - viewModel.getDepartures(this, getLine()) + fun getDepartures(force: Boolean = false) { + if (force) { + showLoading() + } else { + adapter.refreshItems() + setupSnackbar() + } + viewModel.getDepartures(this, date, force) handler.removeCallbacks(runnable) runnable = Runnable { getDepartures() } handler.postDelayed(runnable, 30 * 1000) @@ -130,12 +324,16 @@ private fun setupSnackbar() { val lastUpdateAgo = ZonedDateTime.now().toEpochSecond() - adapter.lastUpdate.toEpochSecond() if (lastUpdateAgo > 59 && adapter.lastUpdate.year != 0) { - snackbar.setText(getString(R.string.departures_snackbar, - DateUtils.getRelativeTimeSpanString( - adapter.lastUpdate.toEpochSecond() * 1000, - ZonedDateTime.now().toEpochSecond() * 1000, - MINUTE_IN_MILLIS, - DateUtils.FORMAT_ABBREV_RELATIVE)) + snackbar.setText( + getString( + R.string.departures_snackbar, + DateUtils.getRelativeTimeSpanString( + adapter.lastUpdate.toEpochSecond() * 1000, + ZonedDateTime.now().toEpochSecond() * 1000, + MINUTE_IN_MILLIS, + DateUtils.FORMAT_ABBREV_RELATIVE + ) + ) ).show() } else { snackbar.dismiss() @@ -149,13 +347,26 @@ binding.errorImage.visibility = View.VISIBLE binding.errorText.visibility = View.VISIBLE binding.errorText.text = getString(error.stringResource) - binding.errorImage.setImageDrawable(AppCompatResources.getDrawable(this, error.imageResource)) + binding.errorImage.setImageDrawable( + AppCompatResources.getDrawable( + this, + error.imageResource + ) + ) + } + + private fun showLoading() { + binding.departuresOverlay.visibility = View.VISIBLE + binding.departuresProgress.visibility = View.VISIBLE + binding.departuresRecycler.visibility = View.GONE + binding.errorImage.visibility = View.GONE + binding.errorText.visibility = View.GONE } private fun updateItems(departures: List<Departure>, stop: Stop) { setupSnackbar() binding.departuresProgress.visibility = View.GONE - adapter.update(departures, true) + adapter.update(departures, date != null, true) binding.collapsingLayout.apply { title = stop.name } @@ -179,7 +390,6 @@ binding.errorImage.visibility = View.GONE binding.errorText.visibility = View.GONE binding.departuresRecycler.visibility = View.VISIBLE } - // todo [3.2; traffic] alerts // todo [3.2; traffic] stop info } } \ No newline at end of file diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/departures/DeparturesViewModel.kt b/app/src/main/java/xyz/apiote/bimba/czwek/departures/DeparturesViewModel.kt index 57f6fb5bfc9505ba2262d3cfe16cf7e48345f627..b973174eeb0b9325f3c5282aaa682545ac5187e4 100644 --- a/app/src/main/java/xyz/apiote/bimba/czwek/departures/DeparturesViewModel.kt +++ b/app/src/main/java/xyz/apiote/bimba/czwek/departures/DeparturesViewModel.kt @@ -22,6 +22,7 @@ import xyz.apiote.bimba.czwek.repo.OnlineRepository import xyz.apiote.bimba.czwek.repo.QrLocation import xyz.apiote.bimba.czwek.repo.StopDepartures import xyz.apiote.bimba.czwek.repo.TrafficResponseException +import java.time.LocalDate class DeparturesViewModel : ViewModel() { private val _departures = MutableLiveData<StopDepartures>() @@ -33,7 +34,7 @@ var allItemsRequested = false private var feed: FeedInfo? = null private lateinit var code: String - fun getDepartures(context: Context, line: String?) { + fun getDepartures(context: Context, date: LocalDate?, force: Boolean) { MainScope().launch { try { if (feed == null) { @@ -46,19 +47,19 @@ val stopDepartures = repository.getDepartures( feed!!.id, code, - line, + date, context, requestedItemsNumber ) stopDepartures?.let { - if (stopDepartures.departures.isEmpty()) { + if (stopDepartures.departures.isEmpty()) { // TODO other error for empty than not-found val (string, image) = mapHttpError(404) throw TrafficResponseException(404, "", Error(404, string, image)) } _departures.value = it } } catch (e: TrafficResponseException) { - if (!departures.isInitialized) { + if (!departures.isInitialized || force) { _error.value = e.error } Log.w("Departures", "$e") @@ -71,6 +72,9 @@ val intent = (context as Activity).intent var feeds = OfflineRepository().getFeeds(context) if (feeds.isNullOrEmpty()) { feeds = OnlineRepository().getFeeds(context) + if (feeds != null) { + OfflineRepository().saveFeedCache(context, feeds) + } } return when (intent.action) { Intent.ACTION_VIEW -> { diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/repo/Departure.kt b/app/src/main/java/xyz/apiote/bimba/czwek/repo/Departure.kt index 617515c72577de45af91523cd643eb14e07a9760..358a7bbd7ed88d19af4e2e9fdb6a26ff7774cb6f 100644 --- a/app/src/main/java/xyz/apiote/bimba/czwek/repo/Departure.kt +++ b/app/src/main/java/xyz/apiote/bimba/czwek/repo/Departure.kt @@ -13,11 +13,13 @@ import xyz.apiote.bimba.czwek.api.AlertV1 import xyz.apiote.bimba.czwek.api.DepartureV1 import xyz.apiote.bimba.czwek.api.DepartureV2 import xyz.apiote.bimba.czwek.api.DepartureV3 +import xyz.apiote.bimba.czwek.api.DepartureV4 import xyz.apiote.bimba.czwek.api.Time import xyz.apiote.bimba.czwek.api.UnknownResourceVersionException import java.time.Instant import java.time.ZoneId import java.time.ZonedDateTime +import java.time.format.DateTimeFormatter import java.time.temporal.ChronoUnit enum class AlertCause { @@ -89,7 +91,8 @@ val time: Time, val status: ULong, val isRealtime: Boolean, val vehicle: Vehicle, - val boarding: UByte + val boarding: UByte, + val alerts: List<Alert> ) { constructor(d: DepartureV1) : this( @@ -98,7 +101,8 @@ d.time, d.status, d.isRealtime, Vehicle(d.vehicle), - d.boarding + d.boarding, + emptyList() ) constructor(d: DepartureV2) : this( @@ -107,7 +111,8 @@ d.time, d.status, d.isRealtime, Vehicle(d.vehicle), - d.boarding + d.boarding, + emptyList() ) constructor(d: DepartureV3) : this( @@ -116,15 +121,29 @@ d.time, d.status.ordinal.toULong(), // TODO VehicleStatus d.isRealtime, Vehicle(d.vehicle), - d.boarding + d.boarding, + emptyList() + ) + + constructor(d: DepartureV4) : this( + d.ID, + d.time, + d.status.ordinal.toULong(), // TODO VehicleStatus + d.isRealtime, + Vehicle(d.vehicle), + d.boarding, + d.alerts.map { Alert(it) } ) - fun statusText(context: Context?, at: ZonedDateTime? = null): String { + fun statusText(context: Context?, showAsTime: Boolean, at: ZonedDateTime? = null): String { val now = at ?: Instant.now().atZone(ZoneId.systemDefault()) val departureTime = ZonedDateTime.of( now.year, now.monthValue, now.dayOfMonth, time.Hour.toInt(), time.Minute.toInt(), time.Second.toInt(), 0, ZoneId.of(time.Zone) ).plus(time.DayOffset.toLong(), ChronoUnit.DAYS) + if (showAsTime) { + return departureTime.format(DateTimeFormatter.ofPattern("HH:mm")) + } var r = status.toUInt() if (departureTime.isBefore(now) && r < 3u) { r = 0u @@ -164,4 +183,4 @@ boarding.and(0b0011_0011u) == (0b0001_0001).toUByte() -> context.getString(R.string.boarding) else -> context.getString(R.string.on_demand) } } -} \ No newline at end of file +} diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/repo/FeedInfo.kt b/app/src/main/java/xyz/apiote/bimba/czwek/repo/FeedInfo.kt index 4c3211555a8fc4219845e9562e824ca6195c25f5..3d645e655861a1f67f3694aa12da1ab90eb1d675 100644 --- a/app/src/main/java/xyz/apiote/bimba/czwek/repo/FeedInfo.kt +++ b/app/src/main/java/xyz/apiote/bimba/czwek/repo/FeedInfo.kt @@ -6,11 +6,24 @@ package xyz.apiote.bimba.czwek.repo import xyz.apiote.bimba.czwek.api.structs.FeedInfoV1 import xyz.apiote.bimba.czwek.api.structs.FeedInfoV2 +import xyz.apiote.bimba.czwek.api.structs.QrLocationV1 +import xyz.apiote.fruchtfleisch.Reader +import xyz.apiote.fruchtfleisch.Writer +import java.io.InputStream +import java.io.OutputStream import java.time.LocalDate import java.time.format.DateTimeFormatter import java.time.format.FormatStyle import java.util.Locale +class FeedInfoPrev { + companion object { + fun unmarshal(stream: InputStream): FeedInfo { + return FeedInfo(FeedInfoPrev()) + } + } +} + data class FeedInfo( val id: String, val name: String, @@ -24,6 +37,41 @@ val validSince: LocalDate?, val validTill: LocalDate?, val cached: Boolean ) { + companion object { + const val VERSION = 100u + private fun parseDate(dateString: String) = + LocalDate.parse(dateString, DateTimeFormatter.ISO_LOCAL_DATE) + + + fun unmarshal(stream: InputStream): FeedInfo { + val reader = Reader(stream) + val id = reader.readString() + val name = reader.readString() + val attribution = reader.readString() + val description = reader.readString() + val lastUpdate = parseDate(reader.readString()) + val qrHost = reader.readString() + val qrIn = QrLocation.of(QrLocationV1.of(reader.readUInt().toULong().toUInt())) + val qrSelector = reader.readString() + val validSince = reader.readString() + val validTill = reader.readString() + + return FeedInfo( + id, + name, + attribution, + description, + lastUpdate, + qrHost, + qrIn, + qrSelector, + if (validSince != "") parseDate(validSince) else null, + if (validTill != "") parseDate(validTill) else null, + true + ) + } + } + constructor(f: FeedInfoV2, cached: Boolean = false) : this( f.id, f.name, @@ -52,6 +100,38 @@ null, cached ) + constructor(f: FeedInfoPrev) : this( + "", + "", + "", + "", + LocalDate.MIN, + "", + QrLocation.UNKNOWN, + "", + null, + null, + false + ) + + fun marshal(stream: OutputStream) { + val writer = Writer(stream) + writer.writeString(id) + writer.writeString(name) + writer.writeString(attribution) + writer.writeString(description) + writer.writeString(formatDateMarshal(lastUpdate)) + writer.writeString(qrHost) + writer.writeUInt(qrIn.value().toULong()) + writer.writeString(qrSelector) + writer.writeString(if (validSince == null) "" else formatDateMarshal(validSince)) + writer.writeString(if (validTill == null) "" else formatDateMarshal(validTill)) + } + + private fun formatDateMarshal(date: LocalDate): String { + return date.format(DateTimeFormatter.ISO_LOCAL_DATE) + } + fun formatDate(): String { return lastUpdate.format( DateTimeFormatter.ofLocalizedDate(FormatStyle.FULL).withLocale(Locale.getDefault()) @@ -74,8 +154,12 @@ if (lastUpdate.isAfter(other.lastUpdate)) lastUpdate else other.lastUpdate, other.qrHost, other.qrIn, other.qrSelector, - if (other.validSince == null || (validSince?:LocalDate.MIN).isAfter(other.validSince)) validSince else other.validSince, - if (other.validTill == null || (validTill?:LocalDate.MIN).isAfter(other.validTill)) validTill else other.validTill, + if (other.validSince == null || (validSince + ?: LocalDate.MIN).isAfter(other.validSince) + ) validSince else other.validSince, + if (other.validTill == null || (validTill + ?: LocalDate.MIN).isAfter(other.validTill) + ) validTill else other.validTill, this.cached && other.cached ) } diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/repo/Interfaces.kt b/app/src/main/java/xyz/apiote/bimba/czwek/repo/Interfaces.kt index dc8bdba932bf7327316bd331ae92791002adce35..7d70dbc8aa3e2cedc903e6e19ea399841623f2a1 100644 --- a/app/src/main/java/xyz/apiote/bimba/czwek/repo/Interfaces.kt +++ b/app/src/main/java/xyz/apiote/bimba/czwek/repo/Interfaces.kt @@ -6,8 +6,8 @@ package xyz.apiote.bimba.czwek.repo import android.content.Context import android.graphics.drawable.Drawable -import android.net.ConnectivityManager import xyz.apiote.bimba.czwek.api.Server +import java.time.LocalDate interface Queryable interface Locatable { @@ -25,23 +25,22 @@ suspend fun getDepartures( feedID: String, stop: String, - line: String?, + date: LocalDate?, context: Context, limit: Int? ): StopDepartures? suspend fun getLocatablesIn( - cm: ConnectivityManager, + context: Context, bl: Position, tr: Position, - context: Context ): List<Locatable>? suspend fun getLine( - cm: ConnectivityManager, + context: Context, feedID: String, - line: String, - context: Context + lineName: String, + lineID: String, ): Line? suspend fun queryQueryables( diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/repo/Line.kt b/app/src/main/java/xyz/apiote/bimba/czwek/repo/Line.kt index cdffec7beddc8f0caee7728aa408c9bb8204092b..7a28e2b3e2128782b096ffc2744719151997fe65 100644 --- a/app/src/main/java/xyz/apiote/bimba/czwek/repo/Line.kt +++ b/app/src/main/java/xyz/apiote/bimba/czwek/repo/Line.kt @@ -9,8 +9,10 @@ import android.graphics.drawable.BitmapDrawable import android.graphics.drawable.Drawable import xyz.apiote.bimba.czwek.api.LineV1 import xyz.apiote.bimba.czwek.api.LineV2 +import xyz.apiote.bimba.czwek.api.LineV3 data class Line( + val id: String, val name: String, val colour: Colour, val type: LineType, @@ -21,6 +23,7 @@ ) : Queryable, LineAbstract { constructor(line: LineV1) : this( line.name, + line.name, Colour(line.colour), LineType.of(line.type), line.feedID, @@ -28,6 +31,17 @@ line.headsigns, line.graphs.map{LineGraph(it)} ) constructor(line: LineV2) : this( + line.name, + line.name, + Colour(line.colour), + LineType.of(line.type), + line.feedID, + line.headsigns, + line.graphs.map{LineGraph(it)} + ) + + constructor(line: LineV3) : this( + line.id, line.name, Colour(line.colour), LineType.of(line.type), diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/repo/LineAbstract.kt b/app/src/main/java/xyz/apiote/bimba/czwek/repo/LineAbstract.kt index 527ba30671fdb54696be7f43caf2c228762faf68..d5ecc9bbbba7111c55e31d0015ce15f696e17274 100644 --- a/app/src/main/java/xyz/apiote/bimba/czwek/repo/LineAbstract.kt +++ b/app/src/main/java/xyz/apiote/bimba/czwek/repo/LineAbstract.kt @@ -17,6 +17,7 @@ import xyz.apiote.bimba.czwek.R import xyz.apiote.bimba.czwek.dpToPixel import xyz.apiote.bimba.czwek.dpToPixelI import kotlin.math.abs +import kotlin.math.cbrt import kotlin.math.pow interface LineAbstract { @@ -88,10 +89,10 @@ val radiusToPow = (radius * radius * radius).toDouble() val path = Path() path.moveTo(-radius.toFloat(), 0f) for (x in -radius..radius) path.lineTo( - x.toFloat(), Math.cbrt(radiusToPow - abs(x * x * x)).toFloat() + x.toFloat(), cbrt(radiusToPow - abs(x * x * x)).toFloat() ) for (x in radius downTo -radius) path.lineTo( - x.toFloat(), -Math.cbrt(radiusToPow - abs(x * x * x)).toFloat() + x.toFloat(), -cbrt(radiusToPow - abs(x * x * x)).toFloat() ) path.close() val matrix = Matrix() diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/repo/OfflineRepository.kt b/app/src/main/java/xyz/apiote/bimba/czwek/repo/OfflineRepository.kt index 0a92c37cd563b5560cd44f559b7ff8ce787d82bb..6f9849cfc8b086ec4809afba0ad14e6218b4f58f 100644 --- a/app/src/main/java/xyz/apiote/bimba/czwek/repo/OfflineRepository.kt +++ b/app/src/main/java/xyz/apiote/bimba/czwek/repo/OfflineRepository.kt @@ -5,60 +5,72 @@ package xyz.apiote.bimba.czwek.repo import android.content.Context -import android.net.ConnectivityManager +import androidx.core.content.edit import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import xyz.apiote.bimba.czwek.api.Server -import xyz.apiote.bimba.czwek.api.responses.FeedsResponse -import xyz.apiote.bimba.czwek.api.responses.FeedsResponseDev -import xyz.apiote.bimba.czwek.api.responses.FeedsResponseV1 -import xyz.apiote.bimba.czwek.api.responses.FeedsResponseV2 +import xyz.apiote.fruchtfleisch.Reader +import xyz.apiote.fruchtfleisch.Writer import java.io.File -import java.io.FileInputStream import java.net.URLEncoder +import java.time.LocalDate class OfflineRepository : Repository { + fun saveFeedCache(context: Context, feedInfos: Map<String, FeedInfo>) { + val file = File( + context.filesDir, URLEncoder.encode(Server.get(context).apiPath, "utf-8") + ) + context.getSharedPreferences("offlineFeeds", Context.MODE_PRIVATE).edit { + putInt("version", FeedInfo.VERSION.toInt()) + } + val stream = file.outputStream() + val writer = Writer(stream) + writer.writeUInt(feedInfos.size.toULong()) + feedInfos.forEach { + it.value.marshal(stream) + } + stream.flush() + stream.close() + } + + @Suppress("RedundantNullableReturnType") override suspend fun getFeeds( context: Context, server: Server ): Map<String, FeedInfo>? { val file = File( - context.filesDir, URLEncoder.encode(server.apiPath, "utf-8") + context.filesDir, withContext(Dispatchers.IO) { + URLEncoder.encode(server.apiPath, "utf-8") + } ) if (!file.exists()) { return emptyMap() } - return when (val response = - withContext(Dispatchers.IO) { FeedsResponse.unmarshal(FileInputStream(file)) }) { - is FeedsResponseDev -> response.feeds.associate { - Pair( - it.id, - FeedInfo(it).copy(cached = true) - ) - } - is FeedsResponseV2 -> response.feeds.associate { - Pair( - it.id, - FeedInfo(it).copy(cached = true) - ) - } - - is FeedsResponseV1 -> response.feeds.associate { - Pair( - it.id, - FeedInfo(it).copy(cached = true) - ) + val version = + context.getSharedPreferences("offlineFeeds", Context.MODE_PRIVATE).getInt("version", -1) + if (version < 0) { + return emptyMap() + } + val unmarshaller = + if (version.toUInt() == FeedInfo.VERSION) FeedInfo::unmarshal else FeedInfoPrev::unmarshal + val stream = file.inputStream() + val feeds = mutableMapOf<String, FeedInfo>() + val n = Reader(stream).readUInt().toULong().toInt() + repeat(n) { + val feed = unmarshaller(stream) + feeds[feed.id] = feed + if (version.toUInt() != FeedInfo.VERSION) { + saveFeedCache(context, feeds) } - - else -> null } + return feeds } override suspend fun getDepartures( feedID: String, stop: String, - line: String?, + date: LocalDate?, context: Context, limit: Int? ): StopDepartures? { @@ -66,19 +78,18 @@ TODO("Not yet implemented") } override suspend fun getLocatablesIn( - cm: ConnectivityManager, + context: Context, bl: Position, tr: Position, - context: Context ): List<Locatable>? { TODO("Not yet implemented") } override suspend fun getLine( - cm: ConnectivityManager, + context: Context, feedID: String, - line: String, - context: Context + lineName: String, + lineID: String, ): Line? { TODO("Not yet implemented") } diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/repo/OnlineRepository.kt b/app/src/main/java/xyz/apiote/bimba/czwek/repo/OnlineRepository.kt index e25943d37a5bb1d941a759ad6cd56a29f23b20ba..8432f937e713904e92c77440c6d634607f8167fe 100644 --- a/app/src/main/java/xyz/apiote/bimba/czwek/repo/OnlineRepository.kt +++ b/app/src/main/java/xyz/apiote/bimba/czwek/repo/OnlineRepository.kt @@ -5,11 +5,11 @@ package xyz.apiote.bimba.czwek.repo import android.content.Context -import android.net.ConnectivityManager import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import xyz.apiote.bimba.czwek.api.LineV1 import xyz.apiote.bimba.czwek.api.LineV2 +import xyz.apiote.bimba.czwek.api.LineV3 import xyz.apiote.bimba.czwek.api.PositionV1 import xyz.apiote.bimba.czwek.api.Server import xyz.apiote.bimba.czwek.api.StopV1 @@ -23,6 +23,7 @@ import xyz.apiote.bimba.czwek.api.responses.DeparturesResponseDev import xyz.apiote.bimba.czwek.api.responses.DeparturesResponseV1 import xyz.apiote.bimba.czwek.api.responses.DeparturesResponseV2 import xyz.apiote.bimba.czwek.api.responses.DeparturesResponseV3 +import xyz.apiote.bimba.czwek.api.responses.DeparturesResponseV4 import xyz.apiote.bimba.czwek.api.responses.ErrorResponse import xyz.apiote.bimba.czwek.api.responses.FeedsResponse import xyz.apiote.bimba.czwek.api.responses.FeedsResponseDev @@ -32,6 +33,7 @@ import xyz.apiote.bimba.czwek.api.responses.LineResponse import xyz.apiote.bimba.czwek.api.responses.LineResponseDev import xyz.apiote.bimba.czwek.api.responses.LineResponseV1 import xyz.apiote.bimba.czwek.api.responses.LineResponseV2 +import xyz.apiote.bimba.czwek.api.responses.LineResponseV3 import xyz.apiote.bimba.czwek.api.responses.LocatablesResponse import xyz.apiote.bimba.czwek.api.responses.LocatablesResponseDev import xyz.apiote.bimba.czwek.api.responses.LocatablesResponseV1 @@ -42,26 +44,18 @@ import xyz.apiote.bimba.czwek.api.responses.QueryablesResponseDev import xyz.apiote.bimba.czwek.api.responses.QueryablesResponseV1 import xyz.apiote.bimba.czwek.api.responses.QueryablesResponseV2 import xyz.apiote.bimba.czwek.api.responses.QueryablesResponseV3 -import java.io.File -import java.net.URLEncoder +import xyz.apiote.bimba.czwek.api.responses.QueryablesResponseV4 +import java.time.LocalDate // todo [3.2] in Repository check if responses are BARE or HTML class OnlineRepository : Repository { - private fun saveFeedCache(server: Server, context: Context, rawResponse: ByteArray) { - val file = File( - context.filesDir, URLEncoder.encode(server.apiPath, "utf-8") - ) - file.writeBytes(rawResponse) - } - override suspend fun getFeeds( context: Context, server: Server ): Map<String, FeedInfo>? { - val cm = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager val result = - xyz.apiote.bimba.czwek.api.getFeeds(cm, server) + xyz.apiote.bimba.czwek.api.getFeeds(context, server) if (result.error != null) { if (result.stream != null) { val response = withContext(Dispatchers.IO) { ErrorResponse.unmarshal(result.stream) } @@ -71,7 +65,6 @@ throw TrafficResponseException(result.error.statusCode, "", result.error) } } else { val rawResponse = result.stream!!.readBytes() - saveFeedCache(server, context, rawResponse) return when (val response = withContext(Dispatchers.IO) { FeedsResponse.unmarshal(rawResponse.inputStream()) }) { is FeedsResponseDev -> response.feeds.associate { Pair(it.id, FeedInfo(it)) } @@ -86,13 +79,19 @@ override suspend fun getDepartures( feedID: String, stop: String, - line: String?, + date: LocalDate?, context: Context, limit: Int? ): StopDepartures? { - val cm = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager val result = - xyz.apiote.bimba.czwek.api.getDepartures(cm, Server.get(context), feedID, stop, line, limit) + xyz.apiote.bimba.czwek.api.getDepartures( + context, + Server.get(context), + feedID, + stop, + date, + limit + ) if (result.error != null) { if (result.stream != null) { val response = withContext(Dispatchers.IO) { ErrorResponse.unmarshal(result.stream) } @@ -104,6 +103,11 @@ } else { return when (val response = withContext(Dispatchers.IO) { DeparturesResponse.unmarshal(result.stream!!) }) { is DeparturesResponseDev -> StopDepartures( + response.departures.map { Departure(it) }, + Stop(response.stop), + response.alerts.map { Alert(it) }) + + is DeparturesResponseV4 -> StopDepartures( response.departures.map { Departure(it) }, Stop(response.stop), response.alerts.map { Alert(it) }) @@ -129,13 +133,12 @@ } } override suspend fun getLocatablesIn( - cm: ConnectivityManager, + context: Context, bl: Position, tr: Position, - context: Context ): List<Locatable>? { val result = xyz.apiote.bimba.czwek.api.getLocatablesIn( - cm, + context, Server.get(context), PositionV1(bl.latitude, bl.longitude), PositionV1(tr.latitude, tr.longitude) @@ -188,9 +191,10 @@ } } override suspend fun getLine( - cm: ConnectivityManager, feedID: String, line: String, context: Context + context: Context, feedID: String, lineName: String, lineID: String ): Line? { - val result = xyz.apiote.bimba.czwek.api.getLine(cm, Server.get(context), feedID, line) + val result = + xyz.apiote.bimba.czwek.api.getLine(context, Server.get(context), feedID, lineName, lineID) if (result.error != null) { if (result.stream != null) { val response = withContext(Dispatchers.IO) { ErrorResponse.unmarshal(result.stream) } @@ -204,6 +208,7 @@ withContext(Dispatchers.IO) { LineResponse.unmarshal(result.stream!!) }) { is LineResponseDev -> Line(response.line) is LineResponseV1 -> Line(response.line) is LineResponseV2 -> Line(response.line) + is LineResponseV3 -> Line(response.line) else -> null } } @@ -224,14 +229,18 @@ private suspend fun getQueryables( query: String?, position: Position?, context: Context, type: String ): List<Queryable>? { - val cm = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager val result = when (type) { "query" -> { - xyz.apiote.bimba.czwek.api.queryQueryables(cm, Server.get(context), query!!, limit = 12) + xyz.apiote.bimba.czwek.api.queryQueryables( + context, + Server.get(context), + query!!, + limit = 12 + ) } "locate" -> xyz.apiote.bimba.czwek.api.locateQueryables( - cm, Server.get(context), PositionV1(position!!.latitude, position.longitude) + context, Server.get(context), PositionV1(position!!.latitude, position.longitude) ) else -> throw RuntimeException("Unknown query type $type") @@ -249,8 +258,8 @@ withContext(Dispatchers.IO) { QueryablesResponse.unmarshal(result.stream!!) }) { is QueryablesResponseDev -> response.queryables.map { when (it) { is StopV2 -> Stop(it) - is LineV2 -> Line(it) - else -> throw UnknownResourceException("queryablesV2", it::class) + is LineV3 -> Line(it) + else -> throw UnknownResourceException("queryablesV4", it::class) } } @@ -273,7 +282,15 @@ is QueryablesResponseV3 -> response.queryables.map { when (it) { is StopV2 -> Stop(it) is LineV2 -> Line(it) - else -> throw UnknownResourceException("queryablesV2", it::class) + else -> throw UnknownResourceException("queryablesV3", it::class) + } + } + + is QueryablesResponseV4 -> response.queryables.map { + when (it) { + is StopV2 -> Stop(it) + is LineV3 -> Line(it) + else -> throw UnknownResourceException("queryablesV4", it::class) } } diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/repo/QrLocation.kt b/app/src/main/java/xyz/apiote/bimba/czwek/repo/QrLocation.kt index 93d5932ed09463d59ff583a66727df435461f03e..108b0077fdb81df22d1e537ed9e696f3bc6d4f92 100644 --- a/app/src/main/java/xyz/apiote/bimba/czwek/repo/QrLocation.kt +++ b/app/src/main/java/xyz/apiote/bimba/czwek/repo/QrLocation.kt @@ -20,4 +20,13 @@ QrLocationV1.QUERY -> QUERY } } } + + fun value(): UInt { + return when (this) { + UNKNOWN -> 0u + NONE -> 1u + PATH -> 2u + QUERY -> 3u + } + } } diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/repo/Stop.kt b/app/src/main/java/xyz/apiote/bimba/czwek/repo/Stop.kt index 3c79dffea5278fa9f9090bd4cbd18c8f098a3a79..0c6909006f92fe6a5bc7add619c14d4b7cc7a0a4 100644 --- a/app/src/main/java/xyz/apiote/bimba/czwek/repo/Stop.kt +++ b/app/src/main/java/xyz/apiote/bimba/czwek/repo/Stop.kt @@ -5,20 +5,10 @@ package xyz.apiote.bimba.czwek.repo import android.content.Context -import android.content.res.Configuration -import android.content.res.TypedArray -import android.graphics.Bitmap -import android.graphics.drawable.BitmapDrawable import android.graphics.drawable.Drawable -import androidx.appcompat.content.res.AppCompatResources -import androidx.core.graphics.ColorUtils -import androidx.core.graphics.drawable.toBitmap import xyz.apiote.bimba.czwek.R import xyz.apiote.bimba.czwek.api.StopV1 import xyz.apiote.bimba.czwek.api.StopV2 -import xyz.apiote.bimba.czwek.dpToPixelI -import java.util.zip.Adler32 - data class Stop( val code: String, @@ -28,32 +18,10 @@ val zone: String, val feedID: String?, val position: Position, val changeOptions: List<ChangeOption> -) : Queryable, Locatable { +) : Queryable, Locatable, StopAbstract { override fun icon(context: Context, scale: Float): Drawable { - val md = Adler32().let { - it.update(nodeName.toByteArray()) - it.value - } - val h = md % 359f - val s = 1.0f - val a: TypedArray = context.theme.obtainStyledAttributes( - R.style.Theme_Bimba, intArrayOf(R.attr.randomColourLightness) - ) - val l = a.getFloat(0, when (context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK) { - Configuration.UI_MODE_NIGHT_YES -> 1f - Configuration.UI_MODE_NIGHT_NO -> 0f - Configuration.UI_MODE_NIGHT_UNDEFINED -> 0f - else -> 0f - }) - a.recycle() - val bg = AppCompatResources.getDrawable(context, R.drawable.stop)!!.mutate().apply { - setTint(ColorUtils.HSLToColor(floatArrayOf(h, s, l))) - } - return BitmapDrawable( - context.resources, - bg.toBitmap(dpToPixelI(24f / scale), dpToPixelI(24f / scale), Bitmap.Config.ARGB_8888) - ) + return super.icon(context, nodeName, scale) } override fun id(): String = code diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/repo/StopAbstract.kt b/app/src/main/java/xyz/apiote/bimba/czwek/repo/StopAbstract.kt new file mode 100644 index 0000000000000000000000000000000000000000..5a50f42e09d60e52d10100d188243eaa51cae669 --- /dev/null +++ b/app/src/main/java/xyz/apiote/bimba/czwek/repo/StopAbstract.kt @@ -0,0 +1,46 @@ +// SPDX-FileCopyrightText: Adam Evyčędo +// +// SPDX-License-Identifier: GPL-3.0-or-later + +package xyz.apiote.bimba.czwek.repo + +import android.content.Context +import android.content.res.Configuration +import android.content.res.TypedArray +import android.graphics.Bitmap +import android.graphics.drawable.BitmapDrawable +import android.graphics.drawable.Drawable +import androidx.appcompat.content.res.AppCompatResources +import androidx.core.graphics.ColorUtils +import androidx.core.graphics.drawable.toBitmap +import xyz.apiote.bimba.czwek.R +import xyz.apiote.bimba.czwek.dpToPixelI +import java.util.zip.Adler32 + +interface StopAbstract{ + fun icon(context: Context, nodeName: String, scale: Float): Drawable { + val md = Adler32().let { + it.update(nodeName.toByteArray()) + it.value + } + val h = md % 359f + val s = 1.0f + val a: TypedArray = context.theme.obtainStyledAttributes( + R.style.Theme_Bimba, intArrayOf(R.attr.randomColourLightness) + ) + val l = a.getFloat(0, when (context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK) { + Configuration.UI_MODE_NIGHT_YES -> 1f + Configuration.UI_MODE_NIGHT_NO -> 0f + Configuration.UI_MODE_NIGHT_UNDEFINED -> 0f + else -> 0f + }) + a.recycle() + val bg = AppCompatResources.getDrawable(context, R.drawable.stop)!!.mutate().apply { + setTint(ColorUtils.HSLToColor(floatArrayOf(h, s, l))) + } + return BitmapDrawable( + context.resources, + bg.toBitmap(dpToPixelI(24f / scale), dpToPixelI(24f / scale), Bitmap.Config.ARGB_8888) + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/repo/StopStub.kt b/app/src/main/java/xyz/apiote/bimba/czwek/repo/StopStub.kt index b3536dc60d5d3c69af3f9dab98b919110aa0856e..361b093c3f8dd836f328f608dda785f7db11d186 100644 --- a/app/src/main/java/xyz/apiote/bimba/czwek/repo/StopStub.kt +++ b/app/src/main/java/xyz/apiote/bimba/czwek/repo/StopStub.kt @@ -5,24 +5,14 @@ package xyz.apiote.bimba.czwek.repo import android.content.Context -import android.content.res.Configuration -import android.content.res.TypedArray -import android.graphics.Bitmap -import android.graphics.drawable.BitmapDrawable import android.graphics.drawable.Drawable import android.os.Parcelable -import androidx.appcompat.content.res.AppCompatResources -import androidx.core.graphics.ColorUtils -import androidx.core.graphics.drawable.toBitmap import kotlinx.parcelize.Parcelize -import xyz.apiote.bimba.czwek.R import xyz.apiote.bimba.czwek.api.StopStub -import xyz.apiote.bimba.czwek.dpToPixelI -import java.util.zip.Adler32 @Parcelize data class StopStub(val name: String, val nodeName: String, val code: String, val zone: String, val onDemand: Boolean) : - Parcelable { + Parcelable, StopAbstract { constructor(stopStub: StopStub) : this( stopStub.name, stopStub.nodeName, @@ -31,29 +21,6 @@ stopStub.zone, stopStub.onDemand ) fun icon(context: Context, scale: Float = 1f): Drawable { - // TODO same is in Stop - val md = Adler32().let { - it.update(nodeName.toByteArray()) - it.value - } - val h = md % 359f - val s = 1.0f - val a: TypedArray = context.theme.obtainStyledAttributes( - R.style.Theme_Bimba, intArrayOf(R.attr.randomColourLightness) - ) - val l = a.getFloat(0, when (context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK) { - Configuration.UI_MODE_NIGHT_YES -> 1f - Configuration.UI_MODE_NIGHT_NO -> 0f - Configuration.UI_MODE_NIGHT_UNDEFINED -> 0f - else -> 0f - }) - a.recycle() - val bg = AppCompatResources.getDrawable(context, R.drawable.stop)!!.mutate().apply { - setTint(ColorUtils.HSLToColor(floatArrayOf(h, s, l))) - } - return BitmapDrawable( - context.resources, - bg.toBitmap(dpToPixelI(24f / scale), dpToPixelI(24f / scale), Bitmap.Config.ARGB_8888) - ) + return super.icon(context, nodeName, scale) } } \ No newline at end of file diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/search/LineGraphActivity.kt b/app/src/main/java/xyz/apiote/bimba/czwek/search/LineGraphActivity.kt index df0662fa875ec77a21b4c94b520104eb546c9949..128a2199a685110c6bfd442bf81ed6509cbda12f 100644 --- a/app/src/main/java/xyz/apiote/bimba/czwek/search/LineGraphActivity.kt +++ b/app/src/main/java/xyz/apiote/bimba/czwek/search/LineGraphActivity.kt @@ -4,8 +4,6 @@ // SPDX-License-Identifier: GPL-3.0-or-later package xyz.apiote.bimba.czwek.search -import android.content.Context -import android.net.ConnectivityManager import android.os.Bundle import android.util.Log import android.view.View @@ -31,22 +29,22 @@ binding = ActivityLineGraphBinding.inflate(layoutInflater) setContentView(binding.root) - val lineName = intent.getStringExtra("line")!! + val lineName = intent.getStringExtra("lineName")!! + val lineID = intent.getStringExtra("lineID")!! val feedID = intent.getStringExtra("feedID")!! - val cm = getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager binding.title.text = lineName - getGraph(lineName, feedID, cm) + getGraph(lineName, lineID, feedID) } private fun getGraph( lineName: String, + lineID: String, feedID: String, - cm: ConnectivityManager, ) { MainScope().launch { try { val repository = OnlineRepository() - val line = repository.getLine(cm, feedID, lineName, this@LineGraphActivity) + val line = repository.getLine(this@LineGraphActivity, feedID, lineName, lineID) line?.let { sectionsPagerAdapter = SectionsPagerAdapter(supportFragmentManager, it) val viewPager: ViewPager = binding.viewPager diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/search/Results.kt b/app/src/main/java/xyz/apiote/bimba/czwek/search/Results.kt index ef4757d062459420ae3714d74bce0c0592bcb441..d18cb129731d39ea38fdf1878605754b52088caa 100644 --- a/app/src/main/java/xyz/apiote/bimba/czwek/search/Results.kt +++ b/app/src/main/java/xyz/apiote/bimba/czwek/search/Results.kt @@ -226,7 +226,8 @@ } is Line -> { val intent = Intent(context, LineGraphActivity::class.java).apply { - putExtra("line", it.name) + putExtra("lineName", it.name) + putExtra("lineID", it.id) putExtra("feedID", it.feedID) } context!!.startActivity(intent) diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/search/ResultsActivity.kt b/app/src/main/java/xyz/apiote/bimba/czwek/search/ResultsActivity.kt index 6243e61e18928bc6551514c3f3195c511222738c..95054348f2de4faf524e27245737e784f1222b45 100644 --- a/app/src/main/java/xyz/apiote/bimba/czwek/search/ResultsActivity.kt +++ b/app/src/main/java/xyz/apiote/bimba/czwek/search/ResultsActivity.kt @@ -8,7 +8,6 @@ import android.content.Context import android.location.Location import android.location.LocationListener import android.location.LocationManager -import android.net.ConnectivityManager import android.os.Build import android.os.Bundle import android.os.Handler diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/search/ui/LineGraphFragment.kt b/app/src/main/java/xyz/apiote/bimba/czwek/search/ui/LineGraphFragment.kt index 88c8d402f9f5bd10433e93faf71794962ea269a3..84c6285e11884e3a4ff364dbe0b27f128f519785 100644 --- a/app/src/main/java/xyz/apiote/bimba/czwek/search/ui/LineGraphFragment.kt +++ b/app/src/main/java/xyz/apiote/bimba/czwek/search/ui/LineGraphFragment.kt @@ -38,6 +38,7 @@ override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) adapter = LineGraphAdapter( + arguments?.getString("lineID", "") ?: "", arguments?.getString("lineName", "") ?: "", arguments?.getString("feedID", "") ?: "" ) @@ -87,10 +88,16 @@ } companion object { @JvmStatic - fun newInstance(lineGraph: LineGraph, lineName: String, feedID: String): LineGraphFragment { + fun newInstance( + lineGraph: LineGraph, + lineID: String, + lineName: String, + feedID: String + ): LineGraphFragment { return LineGraphFragment().apply { arguments = Bundle().apply { putParcelable("graph", lineGraph) + putString("lineID", lineID) putString("lineName", lineName) putString("feedID", feedID) } @@ -104,7 +111,11 @@ _binding = null } } -class LineGraphAdapter(private val lineName: String, private val feedID: String) : +class LineGraphAdapter( + private val lineID: String, + private val lineName: String, + private val feedID: String +) : AbstractGraphAdapter<BimbaViewHolder>() { private lateinit var context: Context override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BimbaViewHolder { @@ -120,9 +131,10 @@ val intent = Intent(context, DeparturesActivity::class.java).apply { putExtra("code", it.code) putExtra("name", it.name) putExtra("line", lineName) + putExtra("lineID", lineID) putExtra("feedID", feedID) } context.startActivity(intent) } } -} \ No newline at end of file +} diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/search/ui/SectionsPagerAdapter.kt b/app/src/main/java/xyz/apiote/bimba/czwek/search/ui/SectionsPagerAdapter.kt index 4233f8e172aef60ad7224c25771165cf556fd4c4..ec1c7b9130af99eef56553f45c9014948f705c95 100644 --- a/app/src/main/java/xyz/apiote/bimba/czwek/search/ui/SectionsPagerAdapter.kt +++ b/app/src/main/java/xyz/apiote/bimba/czwek/search/ui/SectionsPagerAdapter.kt @@ -13,7 +13,7 @@ class SectionsPagerAdapter(fm: FragmentManager, val line: Line) : FragmentPagerAdapter(fm, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) { override fun getItem(position: Int): Fragment { - return LineGraphFragment.newInstance(line.graphs[position], line.name, line.feedID) + return LineGraphFragment.newInstance(line.graphs[position], line.id, line.name, line.feedID) } override fun getPageTitle(position: Int): CharSequence { @@ -21,6 +21,6 @@ return line.headsigns[position].joinToString() } override fun getCount(): Int { - return 2 + return line.headsigns.size } -} \ No newline at end of file +} diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/settings/ServerChooserActivity.kt b/app/src/main/java/xyz/apiote/bimba/czwek/settings/ServerChooserActivity.kt index 7a7c268eae257dddc5e7be27a5230ff270e3c569..2780cb83d4dea6903fe6a720c6ad8c95622ddc0b 100644 --- a/app/src/main/java/xyz/apiote/bimba/czwek/settings/ServerChooserActivity.kt +++ b/app/src/main/java/xyz/apiote/bimba/czwek/settings/ServerChooserActivity.kt @@ -134,7 +134,7 @@ private fun checkServer(isSimple: Boolean) { val cm = getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager MainScope().launch { - val result = getBimba(cm, Server.get(this@ServerChooserActivity)) + val result = getBimba(this@ServerChooserActivity, Server.get(this@ServerChooserActivity)) if (result.error != null) { showDialog(R.string.error, result.error.stringResource, result.error.imageResource, null) Log.w( diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/settings/feeds/FeedChooserActivity.kt b/app/src/main/java/xyz/apiote/bimba/czwek/settings/feeds/FeedChooserActivity.kt index 9a0d146235b441b3dba102139db061d33fa0ef89..c01d13f54ea0addd2b246d3d070d96aac3aed5f0 100644 --- a/app/src/main/java/xyz/apiote/bimba/czwek/settings/feeds/FeedChooserActivity.kt +++ b/app/src/main/java/xyz/apiote/bimba/czwek/settings/feeds/FeedChooserActivity.kt @@ -84,7 +84,6 @@ BimbaFeedInfoAdapter( layoutInflater, (viewModel.feeds.value ?: emptyMap()).map { it.value }.sortedBy { it.name }, viewModel.settings.value!!, - this, { showBottomSheet(it) }, diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/settings/feeds/FeedInfos.kt b/app/src/main/java/xyz/apiote/bimba/czwek/settings/feeds/FeedInfos.kt index aca53807906122b038f7b86289157d6176d1daaa..411804efd7acd137d2fb6b463d7794c3cf1395a5 100644 --- a/app/src/main/java/xyz/apiote/bimba/czwek/settings/feeds/FeedInfos.kt +++ b/app/src/main/java/xyz/apiote/bimba/czwek/settings/feeds/FeedInfos.kt @@ -4,7 +4,6 @@ // SPDX-License-Identifier: GPL-3.0-or-later package xyz.apiote.bimba.czwek.settings.feeds -import android.content.Context import android.content.DialogInterface import android.os.Bundle import android.view.LayoutInflater @@ -33,20 +32,11 @@ fun bind( feed: FeedInfo, feedSettings: FeedSettings?, holder: BimbaFeedInfoViewHolder?, - context: Context, onClickListener: (String) -> Unit, onCheckedChangeListener: (String, Boolean) -> Unit ) { - val colorAttr = if (feed.cached) { - com.google.android.material.R.attr.colorOnSurfaceVariant - } else { - com.google.android.material.R.attr.colorOnSurface - } - holder?.name?.setTextColor( - context.theme.obtainStyledAttributes( - R.style.Theme_Bimba, intArrayOf(colorAttr) - ).getColor(0, 0) - ) + holder?.name?.alpha = if (feed.cached) { .5f } else { 1f } + holder?.root?.setOnClickListener { onClickListener(feed.id) } @@ -70,7 +60,6 @@ class BimbaFeedInfoAdapter( private val inflater: LayoutInflater, private var feeds: List<FeedInfo>, private var feedsSettings: FeedsSettings, - private val context: Context, private val onClickListener: ((String) -> Unit), private val onEnabledChangedListener: ((String, Boolean) -> Unit) ) : @@ -109,7 +98,6 @@ BimbaFeedInfoViewHolder.bind( feed, feedsSettings.settings[feed.id], holder, - context, onClickListener, onEnabledChangedListener ) @@ -162,7 +150,7 @@ ): View { val content = inflater.inflate(R.layout.feed_bottom_sheet, container, false) val feed = feeds[feedID]!! var settings = feedsSettings.settings[feedID] - content.findViewById<MaterialTextView>(R.id.title).text = feed.name + content.findViewById<MaterialTextView>(R.id.feed_name).text = feed.name content.findViewById<MaterialTextView>(R.id.description).text = feed.description content.findViewById<MaterialTextView>(R.id.outdated_info_warning).visibility = if (feed.cached) { diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/settings/feeds/FeedsViewModel.kt b/app/src/main/java/xyz/apiote/bimba/czwek/settings/feeds/FeedsViewModel.kt index 4b651ced88475df9b4e0d3ae1982b9eb49d26500..6d5fb983e6baf603ce0237922088462d9450f328 100644 --- a/app/src/main/java/xyz/apiote/bimba/czwek/settings/feeds/FeedsViewModel.kt +++ b/app/src/main/java/xyz/apiote/bimba/czwek/settings/feeds/FeedsViewModel.kt @@ -11,6 +11,7 @@ import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import kotlinx.coroutines.MainScope import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import xyz.apiote.bimba.czwek.api.Error import xyz.apiote.bimba.czwek.repo.FeedInfo import xyz.apiote.bimba.czwek.repo.OfflineRepository @@ -44,25 +45,36 @@ setSettings(feedID, feedSettings?.copy(enabled = enabled) ?: FeedSettings(enabled, true)) } fun loadFeeds(context: Context) { + var offlineFeeds: Map<String, FeedInfo>? = null + var onlineFeeds: Map<String, FeedInfo>? = null + var error: Error? = null MainScope().launch { - val offlineRepository = OfflineRepository() - val offlineFeeds = - offlineRepository.getFeeds(context) - if (!offlineFeeds.isNullOrEmpty()) { - _feeds.value = offlineFeeds!! + withContext(coroutineContext) { + launch { + offlineFeeds = + OfflineRepository().getFeeds(context) + if (!offlineFeeds.isNullOrEmpty()) { + _feeds.value = offlineFeeds!! + } + } + launch { + try { + val repository = OnlineRepository() + onlineFeeds = + repository.getFeeds(context) + } catch (e: TrafficResponseException) { + error = e.error + Log.e("Feeds", "$e") + } + } } - try { - val repository = OnlineRepository() - val onlineFeeds = - repository.getFeeds(context) + if (offlineFeeds.isNullOrEmpty() && error != null) { + _error.value = error!! + } else{ joinFeeds(offlineFeeds, onlineFeeds).let { joinedFeeds -> _feeds.value = joinedFeeds + OfflineRepository().saveFeedCache(context, joinedFeeds) } - } catch (e: TrafficResponseException) { - if (offlineFeeds.isNullOrEmpty()) { - _error.value = e.error - } - Log.e("Feeds", "$e") } } } @@ -82,6 +94,8 @@ if (feeds2.isNullOrEmpty()) { return feeds1 } - return feeds1.keys.union(feeds2.keys).associateWith { feeds1[it].join(feeds2[it]) } + return feeds1.keys.union(feeds2.keys).associateWith { + feeds1[it].join(feeds2[it]) + } } } diff --git a/app/src/main/res/drawable/calendar.xml b/app/src/main/res/drawable/calendar.xml new file mode 100644 index 0000000000000000000000000000000000000000..135a57526208e28cfa5c7032fab1b7387b2692f9 --- /dev/null +++ b/app/src/main/res/drawable/calendar.xml @@ -0,0 +1,11 @@ +<!-- +SPDX-FileCopyrightText: Google + +SPDX-License-Identifier: Apache-2.0 +--> + +<vector android:height="24dp" android:tint="?attr/colorOnSurface" + android:viewportHeight="24" android:viewportWidth="24" + android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android"> + <path android:fillColor="@android:color/white" android:pathData="M19,4h-1V2h-2v2H8V2H6v2H5C3.89,4 3.01,4.9 3.01,6L3,20c0,1.1 0.89,2 2,2h14c1.1,0 2,-0.9 2,-2V6C21,4.9 20.1,4 19,4zM19,20H5V10h14V20zM9,14H7v-2h2V14zM13,14h-2v-2h2V14zM17,14h-2v-2h2V14zM9,18H7v-2h2V18zM13,18h-2v-2h2V18zM17,18h-2v-2h2V18z"/> +</vector> diff --git a/app/src/main/res/drawable/filter.xml b/app/src/main/res/drawable/filter.xml new file mode 100644 index 0000000000000000000000000000000000000000..c391d75031b1c369760e31b44a0e7506cacf94b4 --- /dev/null +++ b/app/src/main/res/drawable/filter.xml @@ -0,0 +1,11 @@ +<!-- +SPDX-FileCopyrightText: Google + +SPDX-License-Identifier: Apache-2.0 +--> + +<vector android:height="24dp" android:tint="?attr/colorOnSurface" + android:viewportHeight="24" android:viewportWidth="24" + android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android"> + <path android:fillColor="@android:color/white" android:pathData="M10,18h4v-2h-4v2zM3,6v2h18L21,6L3,6zM6,13h12v-2L6,11v2z"/> +</vector> diff --git a/app/src/main/res/drawable/warning.xml b/app/src/main/res/drawable/warning.xml new file mode 100644 index 0000000000000000000000000000000000000000..5b2e32cdd6384ca1445fd00351b7b0e6bcb62565 --- /dev/null +++ b/app/src/main/res/drawable/warning.xml @@ -0,0 +1,16 @@ +<!-- +SPDX-FileCopyrightText: Google + +SPDX-License-Identifier: Apache-2.0 +--> + +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="960" + android:viewportHeight="960" + android:tint="?attr/colorOnSurface"> + <path + android:fillColor="@android:color/white" + android:pathData="M40,840L480,80L920,840L40,840ZM178,760L782,760L480,240L178,760ZM480,720Q497,720 508.5,708.5Q520,697 520,680Q520,663 508.5,651.5Q497,640 480,640Q463,640 451.5,651.5Q440,663 440,680Q440,697 451.5,708.5Q463,720 480,720ZM440,600L520,600L520,400L440,400L440,600ZM480,500L480,500L480,500L480,500Z"/> +</vector> diff --git a/app/src/main/res/layout/activity_departures.xml b/app/src/main/res/layout/activity_departures.xml index d71bb1c201f10214475335f515a81489fd690a3b..4744dc352f27edaf2b35495e8d89e7775388ef95 100644 --- a/app/src/main/res/layout/activity_departures.xml +++ b/app/src/main/res/layout/activity_departures.xml @@ -13,6 +13,67 @@ android:layout_width="match_parent" android:layout_height="match_parent" android:paddingBottom="16dp"> + <androidx.recyclerview.widget.RecyclerView + android:id="@+id/departures_recycler" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:clipToPadding="false" + android:fitsSystemWindows="true" + android:visibility="gone" + app:layout_behavior="@string/appbar_scrolling_view_behavior" /> + + <com.google.android.material.card.MaterialCardView + android:id="@+id/alerts" + android:layout_width="match_parent" + android:layout_height="100dp" + android:backgroundTint="@color/safety" + android:visibility="gone" + app:layout_anchor="@id/app_bar_layout" + app:layout_anchorGravity="bottom"> + + <androidx.constraintlayout.widget.ConstraintLayout + android:layout_width="match_parent" + android:layout_height="match_parent"> + + <ImageView + android:id="@+id/imageView" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginStart="8dp" + android:importantForAccessibility="no" + android:src="@drawable/warning" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="@+id/alerts_text" + app:tint="@color/black" /> + + <com.google.android.material.textview.MaterialTextView + android:id="@+id/alerts_text" + android:layout_width="0dp" + android:layout_height="0dp" + android:layout_marginStart="8dp" + android:layout_marginTop="58dp" + android:layout_marginEnd="8dp" + android:ellipsize="end" + android:maxLines="2" + android:textColor="@color/black" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toStartOf="@+id/more_button" + app:layout_constraintStart_toEndOf="@+id/imageView" + app:layout_constraintTop_toTopOf="parent" + tool:text="Warning: Serious blockade on Piastowska towards Wojska Polskiego. Lines 5, 14, 163 diverted. Change for other means of transport, e.g. lines \n\naaaaa" /> + + <com.google.android.material.button.MaterialButton + android:id="@+id/more_button" + style="@style/Widget.Material3.Button.TextButton" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@string/more" + android:textColor="@color/link" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" /> + </androidx.constraintlayout.widget.ConstraintLayout> + </com.google.android.material.card.MaterialCardView> + <androidx.constraintlayout.widget.ConstraintLayout android:id="@+id/departures_overlay" android:layout_width="match_parent" @@ -57,6 +118,7 @@ tool:text="No connection" /> </androidx.constraintlayout.widget.ConstraintLayout> <com.google.android.material.appbar.AppBarLayout + android:id="@+id/app_bar_layout" android:layout_width="match_parent" android:layout_height="wrap_content" android:fitsSystemWindows="true"> @@ -74,19 +136,11 @@ android:id="@+id/departures_app_bar" android:layout_width="match_parent" android:layout_height="?attr/actionBarSize" android:elevation="0dp" - app:layout_collapseMode="pin" /> + app:layout_collapseMode="pin" + app:menu="@menu/departures_menu" /> </com.google.android.material.appbar.CollapsingToolbarLayout> </com.google.android.material.appbar.AppBarLayout> - - <androidx.recyclerview.widget.RecyclerView - android:id="@+id/departures_recycler" - android:layout_width="match_parent" - android:layout_height="match_parent" - android:clipToPadding="false" - android:fitsSystemWindows="true" - android:visibility="gone" - app:layout_behavior="@string/appbar_scrolling_view_behavior" /> </androidx.coordinatorlayout.widget.CoordinatorLayout> \ No newline at end of file diff --git a/app/src/main/res/layout/departure_bottom_sheet.xml b/app/src/main/res/layout/departure_bottom_sheet.xml index 07fdfe3f032d1d913bd62072817aef68823c0c42..5d5c7b3acb6ae7a0b5a21c7c50f67771ed7fc176 100644 --- a/app/src/main/res/layout/departure_bottom_sheet.xml +++ b/app/src/main/res/layout/departure_bottom_sheet.xml @@ -23,11 +23,11 @@ android:id="@+id/time" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_marginTop="48dp" + android:layout_marginTop="0dp" android:textAppearance="@style/TextAppearance.Material3.DisplaySmall" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toTopOf="parent" + app:layout_constraintTop_toBottomOf="@+id/drag_handle" tool:text="at 12:10:30" /> <com.google.android.material.textview.MaterialTextView @@ -75,105 +75,113 @@ app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/time" /> - <ImageView - android:id="@+id/boarding_icon" - android:layout_width="16dp" - android:layout_height="16dp" - android:layout_marginEnd="8dp" - android:importantForAccessibility="no" - app:layout_constraintBottom_toBottomOf="@+id/boarding_text" - app:layout_constraintEnd_toStartOf="@+id/boarding_text" - app:layout_constraintTop_toTopOf="@+id/boarding_text" - app:srcCompat="@drawable/transfer" /> + <androidx.constraintlayout.helper.widget.Flow + android:id="@+id/info" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginStart="48dp" + android:layout_marginTop="48dp" + android:layout_marginEnd="48dp" + app:constraint_referenced_ids="boarding,speed,congestion,occupancy" + app:flow_horizontalGap="4dp" + app:flow_horizontalStyle="spread_inside" + app:flow_verticalGap="4dp" + app:flow_wrapMode="chain" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/line" /> - <com.google.android.material.textview.MaterialTextView - android:id="@+id/boarding_text" + <LinearLayout + android:id="@+id/boarding" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_marginStart="8dp" - android:layout_marginTop="180dp" - android:layout_marginEnd="8dp" - android:textAppearance="@style/TextAppearance.Material3.BodyLarge" - app:layout_constraintEnd_toEndOf="@id/middle" - app:layout_constraintTop_toTopOf="parent" - tool:text="on demand" /> + android:gravity="center_vertical"> - <ImageView - android:id="@+id/speed_icon" - android:layout_width="16dp" - android:layout_height="16dp" - android:layout_marginEnd="8dp" - android:importantForAccessibility="no" - app:layout_constraintBottom_toBottomOf="@+id/speed_text" - app:layout_constraintEnd_toStartOf="@+id/speed_text" - app:layout_constraintTop_toTopOf="@+id/speed_text" - app:srcCompat="@drawable/speed" /> + <ImageView + android:id="@+id/boarding_icon" + android:layout_width="16dp" + android:layout_height="16dp" + android:layout_marginEnd="8dp" + android:importantForAccessibility="no" + app:srcCompat="@drawable/transfer" /> + + <com.google.android.material.textview.MaterialTextView + android:id="@+id/boarding_text" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginEnd="8dp" + android:textAppearance="@style/TextAppearance.Material3.BodyLarge" + tool:text="on demand" /> + </LinearLayout> - <com.google.android.material.textview.MaterialTextView - android:id="@+id/speed_text" + <LinearLayout + android:id="@+id/speed" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_marginTop="8dp" - android:layout_marginEnd="8dp" - android:textAppearance="@style/TextAppearance.Material3.BodyLarge" - app:layout_constraintEnd_toStartOf="@+id/middle" - app:layout_constraintTop_toBottomOf="@id/boarding_text" - tool:text="10 Vl" /> + android:gravity="center_vertical"> - <ImageView - android:id="@+id/congestion_icon" - android:layout_width="16dp" - android:layout_height="16dp" - android:layout_marginStart="8dp" - android:layout_marginEnd="8dp" - android:importantForAccessibility="no" - app:layout_constraintBottom_toBottomOf="@+id/congestion_text" - app:layout_constraintStart_toStartOf="@+id/middle" - app:layout_constraintTop_toTopOf="@+id/congestion_text" - app:srcCompat="@drawable/traffic" /> + <ImageView + android:id="@+id/speed_icon" + android:layout_width="16dp" + android:layout_height="16dp" + android:layout_marginEnd="8dp" + android:importantForAccessibility="no" + app:srcCompat="@drawable/speed" /> - <com.google.android.material.textview.MaterialTextView - android:id="@+id/congestion_text" + <com.google.android.material.textview.MaterialTextView + android:id="@+id/speed_text" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginEnd="8dp" + android:textAppearance="@style/TextAppearance.Material3.BodyLarge" + tool:text="10 Vl" /> + </LinearLayout> + + <LinearLayout + android:id="@+id/congestion" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_marginStart="8dp" - android:layout_marginTop="180dp" - android:layout_marginEnd="8dp" - android:textAppearance="@style/TextAppearance.Material3.BodyLarge" - app:layout_constraintStart_toEndOf="@id/congestion_icon" - app:layout_constraintTop_toTopOf="parent" - tool:text="smooth traffic" /> + android:gravity="center_vertical"> + + <ImageView + android:id="@+id/congestion_icon" + android:layout_width="16dp" + android:layout_height="16dp" + android:layout_marginEnd="8dp" + android:importantForAccessibility="no" + app:srcCompat="@drawable/traffic" /> - <ImageView - android:id="@+id/occupancy_icon" - android:layout_width="16dp" - android:layout_height="16dp" - android:layout_marginStart="8dp" - android:layout_marginEnd="8dp" - android:importantForAccessibility="no" - app:layout_constraintBottom_toBottomOf="@+id/occupancy_text" - app:layout_constraintStart_toStartOf="@+id/middle" - app:layout_constraintTop_toTopOf="@+id/occupancy_text" - app:srcCompat="@drawable/crowd" /> + <com.google.android.material.textview.MaterialTextView + android:id="@+id/congestion_text" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginEnd="8dp" + android:textAppearance="@style/TextAppearance.Material3.BodyLarge" + tool:text="smooth traffic" /> + </LinearLayout> - <com.google.android.material.textview.MaterialTextView - android:id="@+id/occupancy_text" + <LinearLayout + android:id="@+id/occupancy" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_marginStart="8dp" - android:layout_marginTop="8dp" - android:layout_marginEnd="8dp" - android:textAppearance="@style/TextAppearance.Material3.BodyLarge" - app:layout_constraintStart_toEndOf="@id/occupancy_icon" - app:layout_constraintTop_toBottomOf="@id/congestion_text" - tool:text="empty vehicle" /> + android:gravity="center_vertical"> + + <ImageView + android:id="@+id/occupancy_icon" + android:layout_width="16dp" + android:layout_height="16dp" + android:layout_marginEnd="8dp" + android:importantForAccessibility="no" + app:srcCompat="@drawable/crowd" /> - <androidx.constraintlayout.widget.Guideline - android:id="@+id/middle" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:orientation="vertical" - app:layout_constraintGuide_percent=".5" /> + <com.google.android.material.textview.MaterialTextView + android:id="@+id/occupancy_text" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginEnd="8dp" + android:textAppearance="@style/TextAppearance.Material3.BodyLarge" + tool:text="empty vehicle" /> + </LinearLayout> <androidx.constraintlayout.helper.widget.Flow android:id="@+id/capabilities" @@ -189,7 +197,7 @@ app:flow_verticalGap="4dp" app:flow_wrapMode="chain" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@+id/occupancy_text" /> + app:layout_constraintTop_toBottomOf="@+id/info" /> <ImageView android:id="@+id/ac" @@ -231,6 +239,34 @@ android:contentDescription="@string/usb_charging_content_description" app:srcCompat="@drawable/usb" tool:ignore="MissingConstraints" /> + <LinearLayout + android:layout_marginTop="8dp" + android:id="@+id/alerts" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:gravity="center_vertical" + android:visibility="gone" + android:background="@color/safety" + app:layout_constraintTop_toBottomOf="@+id/capabilities"> + + <ImageView + android:layout_marginStart="8dp" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + app:tint="@color/black" + android:importantForAccessibility="no" + android:src="@drawable/warning" /> + + <com.google.android.material.textview.MaterialTextView + android:id="@+id/alerts_text" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_margin="8dp" + android:padding="8dp" + android:textColor="@color/black" + tool:text="Severe stops on Metropolitan line towards Tower Hill through Victoria" /> + </LinearLayout> + <org.osmdroid.views.MapView android:id="@+id/map" android:layout_width="match_parent" @@ -238,5 +274,5 @@ android:layout_height="250dp" android:layout_margin="16dp" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@+id/capabilities" /> + app:layout_constraintTop_toBottomOf="@+id/alerts" /> </androidx.constraintlayout.widget.ConstraintLayout> diff --git a/app/src/main/res/layout/feed_bottom_sheet.xml b/app/src/main/res/layout/feed_bottom_sheet.xml index bcb47b534c804342efbcdece3b2dee15fa746216..97c3bd552d9fd51347bba2fa92c98a0068714e9a 100644 --- a/app/src/main/res/layout/feed_bottom_sheet.xml +++ b/app/src/main/res/layout/feed_bottom_sheet.xml @@ -20,7 +20,7 @@ android:layout_height="wrap_content" app:layout_constraintTop_toTopOf="parent" /> <com.google.android.material.textview.MaterialTextView - android:id="@+id/title" + android:id="@+id/feed_name" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_marginStart="8dp" @@ -54,7 +54,7 @@ android:text="@string/use_online_feed" android:textAppearance="@style/TextAppearance.Material3.HeadlineMedium" app:layout_constraintEnd_toStartOf="@+id/onlineSwitch" app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@+id/title" /> + app:layout_constraintTop_toBottomOf="@+id/feed_name" /> <com.google.android.material.divider.MaterialDivider android:id="@+id/onlineOfflineDivider" diff --git a/app/src/main/res/layout/map_bottom_sheet.xml b/app/src/main/res/layout/map_bottom_sheet.xml deleted file mode 100644 index 24a9d0f75191b3c2fb2a4dec332d713083618041..0000000000000000000000000000000000000000 --- a/app/src/main/res/layout/map_bottom_sheet.xml +++ /dev/null @@ -1,215 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> - -<!-- -SPDX-FileCopyrightText: Adam Evyčędo - -SPDX-License-Identifier: GPL-3.0-or-later ---> - -<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" - xmlns:app="http://schemas.android.com/apk/res-auto" - xmlns:tool="http://schemas.android.com/tools" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:paddingBottom="16dp"> - - <com.google.android.material.bottomsheet.BottomSheetDragHandleView - android:id="@+id/drag_handle" - android:layout_width="match_parent" - android:layout_height="wrap_content" - app:layout_constraintTop_toTopOf="parent" /> - - - <com.google.android.material.textview.MaterialTextView - android:id="@+id/title" - android:layout_width="0dp" - android:layout_height="wrap_content" - android:layout_marginStart="8dp" - android:layout_marginTop="48dp" - android:layout_marginEnd="8dp" - android:textAlignment="center" - android:textAppearance="@style/TextAppearance.Material3.HeadlineSmall" - app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toTopOf="parent" /> - - <androidx.constraintlayout.widget.Group - android:id="@+id/stop_group" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - app:constraint_referenced_ids="change_options,departures_button,navigation_button" /> - - <com.google.android.material.textview.MaterialTextView - android:id="@+id/change_options" - android:layout_width="0dp" - android:layout_height="wrap_content" - android:layout_margin="16dp" - android:textAppearance="@style/TextAppearance.Material3.BodyMedium" - app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@+id/title" /> - - <Button - android:id="@+id/departures_button" - style="@style/Widget.Material3.Button.TextButton.Icon" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_margin="4dp" - android:text="@string/show_departures" - app:icon="@drawable/departure" - app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@+id/change_options" /> - - <Button - android:id="@+id/navigation_button" - style="@style/Widget.Material3.Button.TextButton.Icon" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_margin="4dp" - android:text="@string/open_in_maps_app" - app:icon="@drawable/open_outside" - app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@+id/departures_button" /> - - <androidx.constraintlayout.widget.Group - android:id="@+id/vehicle_group" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - app:constraint_referenced_ids="speed_icon,speed_text,congestion_icon,congestion_text,occupancy_icon,occupancy_text,ac,bike,voice,ticket,usb" /> - - <ImageView - android:id="@+id/speed_icon" - android:layout_width="16dp" - android:layout_height="16dp" - android:layout_marginEnd="8dp" - android:importantForAccessibility="no" - app:layout_constraintBottom_toBottomOf="@+id/speed_text" - app:layout_constraintEnd_toStartOf="@+id/speed_text" - app:layout_constraintTop_toTopOf="@+id/speed_text" - app:srcCompat="@drawable/speed" /> - - <com.google.android.material.textview.MaterialTextView - android:id="@+id/speed_text" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_marginTop="8dp" - android:layout_marginEnd="8dp" - android:textAppearance="@style/TextAppearance.Material3.BodyLarge" - app:layout_constraintEnd_toStartOf="@+id/middle" - app:layout_constraintTop_toBottomOf="@id/title" - tool:text="10 Vl" /> - - <ImageView - android:id="@+id/congestion_icon" - android:layout_width="16dp" - android:layout_height="16dp" - android:layout_marginStart="8dp" - android:layout_marginEnd="8dp" - android:importantForAccessibility="no" - app:layout_constraintBottom_toBottomOf="@+id/congestion_text" - app:layout_constraintStart_toStartOf="@+id/middle" - app:layout_constraintTop_toTopOf="@+id/congestion_text" - app:srcCompat="@drawable/traffic" /> - - <com.google.android.material.textview.MaterialTextView - android:id="@+id/congestion_text" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_marginStart="8dp" - android:layout_marginTop="8dp" - android:layout_marginEnd="8dp" - android:textAppearance="@style/TextAppearance.Material3.BodyLarge" - app:layout_constraintStart_toEndOf="@id/congestion_icon" - app:layout_constraintTop_toBottomOf="@id/title" - tool:text="smooth traffic" /> - - <ImageView - android:id="@+id/occupancy_icon" - android:layout_width="16dp" - android:layout_height="16dp" - android:layout_marginStart="8dp" - android:layout_marginEnd="8dp" - android:importantForAccessibility="no" - app:layout_constraintBottom_toBottomOf="@+id/occupancy_text" - app:layout_constraintStart_toStartOf="@+id/middle" - app:layout_constraintTop_toTopOf="@+id/occupancy_text" - app:srcCompat="@drawable/crowd" /> - - <com.google.android.material.textview.MaterialTextView - android:id="@+id/occupancy_text" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_marginStart="8dp" - android:layout_marginTop="8dp" - android:layout_marginEnd="8dp" - android:textAppearance="@style/TextAppearance.Material3.BodyLarge" - app:layout_constraintStart_toEndOf="@id/occupancy_icon" - app:layout_constraintTop_toBottomOf="@id/congestion_text" - tool:text="empty vehicle" /> - - <androidx.constraintlayout.widget.Guideline - android:id="@+id/middle" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:orientation="vertical" - app:layout_constraintGuide_percent=".5" /> - - <androidx.constraintlayout.helper.widget.Flow - android:layout_width="0dp" - android:layout_height="wrap_content" - android:layout_marginStart="8dp" - android:layout_marginTop="16dp" - android:layout_marginEnd="8dp" - app:constraint_referenced_ids="ac,bike,voice,ticket,usb" - app:flow_horizontalGap="4dp" - app:flow_horizontalStyle="packed" - app:flow_verticalGap="4dp" - app:flow_wrapMode="chain" - app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@+id/occupancy_text" /> - - <ImageView - android:id="@+id/ac" - android:layout_width="24dp" - android:layout_height="24dp" - android:contentDescription="@string/air_condition_content_description" - app:srcCompat="@drawable/ac" - tool:ignore="MissingConstraints" /> - - <ImageView - android:id="@+id/bike" - android:layout_width="24dp" - android:layout_height="24dp" - android:contentDescription="@string/bicycles_allowed_content_description" - app:srcCompat="@drawable/bike" - tool:ignore="MissingConstraints" /> - - <ImageView - android:id="@+id/voice" - android:layout_width="24dp" - android:layout_height="24dp" - android:contentDescription="@string/voice_announcements_content_description" - app:srcCompat="@drawable/voice" - tool:ignore="MissingConstraints" /> - - <ImageView - android:id="@+id/ticket" - android:layout_width="24dp" - android:layout_height="24dp" - android:contentDescription="@string/tickets_sold_content_description" - app:srcCompat="@drawable/ticket" - tool:ignore="MissingConstraints" /> - - <ImageView - android:id="@+id/usb" - android:layout_width="24dp" - android:layout_height="24dp" - android:contentDescription="@string/usb_charging_content_description" - app:srcCompat="@drawable/usb" - tool:ignore="MissingConstraints" /> - - -</androidx.constraintlayout.widget.ConstraintLayout> diff --git a/app/src/main/res/layout/stop_bottom_sheet.xml b/app/src/main/res/layout/stop_bottom_sheet.xml new file mode 100644 index 0000000000000000000000000000000000000000..8b7419d9a9dfa5803e59a1c207e287a826656b2b --- /dev/null +++ b/app/src/main/res/layout/stop_bottom_sheet.xml @@ -0,0 +1,69 @@ +<?xml version="1.0" encoding="utf-8"?> + +<!-- +SPDX-FileCopyrightText: Adam Evyčędo + +SPDX-License-Identifier: GPL-3.0-or-later +--> + +<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:paddingBottom="16dp"> + + <com.google.android.material.bottomsheet.BottomSheetDragHandleView + android:id="@+id/drag_handle" + android:layout_width="match_parent" + android:layout_height="wrap_content" + app:layout_constraintTop_toTopOf="parent" /> + + + <com.google.android.material.textview.MaterialTextView + android:id="@+id/stop_name" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginStart="8dp" + android:layout_marginTop="48dp" + android:layout_marginEnd="8dp" + android:textAlignment="center" + android:textAppearance="@style/TextAppearance.Material3.HeadlineSmall" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" /> + + <com.google.android.material.textview.MaterialTextView + android:id="@+id/change_options" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_margin="16dp" + android:textAppearance="@style/TextAppearance.Material3.BodyMedium" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/stop_name" /> + + <Button + android:id="@+id/departures_button" + style="@style/Widget.Material3.Button.TextButton.Icon" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_margin="4dp" + android:text="@string/show_departures" + app:icon="@drawable/departure" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/change_options" /> + + <Button + android:id="@+id/navigation_button" + style="@style/Widget.Material3.Button.TextButton.Icon" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_margin="4dp" + android:text="@string/open_in_maps_app" + app:icon="@drawable/open_outside" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/departures_button" /> + +</androidx.constraintlayout.widget.ConstraintLayout> diff --git a/app/src/main/res/layout-land/activity_about.xml b/app/src/main/res/layout-land/activity_about.xml new file mode 100644 index 0000000000000000000000000000000000000000..6e1131b7366a7fa3430448c54fc8f7ad38b78368 --- /dev/null +++ b/app/src/main/res/layout-land/activity_about.xml @@ -0,0 +1,135 @@ +<?xml version="1.0" encoding="utf-8"?> + +<!-- +SPDX-FileCopyrightText: Adam Evyčędo + +SPDX-License-Identifier: GPL-3.0-or-later +--> + +<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="match_parent" + tools:context=".AboutActivity"> + + <com.google.android.material.imageview.ShapeableImageView + android:id="@+id/logo" + android:layout_width="100dp" + android:layout_height="100dp" + android:layout_marginTop="32dp" + android:background="@color/bimba_grey" + app:layout_constraintEnd_toStartOf="@+id/middle" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" + app:shapeAppearanceOverlay="@style/roundedImageView" + app:srcCompat="@drawable/ic_launcher_foreground" /> + + <TextView + android:id="@+id/app_name" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginTop="16dp" + android:text="@string/app_name" + android:textAppearance="@style/TextAppearance.AppCompat.Display1" + app:layout_constraintEnd_toEndOf="@+id/logo" + app:layout_constraintStart_toStartOf="@+id/logo" + app:layout_constraintTop_toBottomOf="@+id/logo" /> + + <TextView + android:id="@+id/version" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginTop="8dp" + android:text="@string/versionName" + app:layout_constraintEnd_toEndOf="@+id/app_name" + app:layout_constraintStart_toStartOf="@+id/app_name" + app:layout_constraintTop_toBottomOf="@+id/app_name" /> + + <TextView + android:id="@+id/description" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginStart="8dp" + android:layout_marginTop="48dp" + android:layout_marginEnd="8dp" + android:text="@string/app_description" + android:textAppearance="@style/TextAppearance.AppCompat.Body1" + app:layout_constraintEnd_toEndOf="@id/middle" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/app_name" /> + + <androidx.constraintlayout.helper.widget.Flow + android:id="@+id/links" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginStart="8dp" + android:layout_marginTop="32dp" + android:layout_marginEnd="8dp" + app:constraint_referenced_ids="website,code,translate,mastodon" + app:flow_horizontalGap="16dp" + app:flow_horizontalStyle="packed" + app:flow_verticalGap="4dp" + app:flow_wrapMode="chain" + app:layout_constraintEnd_toEndOf="@id/middle" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/description" /> + + <Button + android:contentDescription="@string/website_button_description" + android:id="@+id/website" + style="@style/Widget.Material3.Button.IconButton" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + tools:ignore="MissingConstraints" + app:icon="@drawable/website" /> + + <Button + android:contentDescription="@string/code_button_description" + android:id="@+id/code" + style="@style/Widget.Material3.Button.IconButton" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + tools:ignore="MissingConstraints" + app:icon="@drawable/code" /> + + <Button + android:contentDescription="@string/mastodon_button_description" + android:id="@+id/mastodon" + style="@style/Widget.Material3.Button.IconButton" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + tools:ignore="MissingConstraints" + app:icon="@drawable/mastodon" /> + + <Button + android:contentDescription="@string/translation_button_description" + android:id="@+id/translate" + style="@style/Widget.Material3.Button.IconButton" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + tools:ignore="MissingConstraints" + app:icon="@drawable/translate" /> + + <androidx.constraintlayout.widget.Guideline + android:id="@+id/middle" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:orientation="vertical" + app:layout_constraintGuide_percent=".5" /> + + <TextView + android:id="@+id/credits" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginStart="8dp" + android:layout_marginTop="32dp" + android:layout_marginEnd="8dp" + android:autoLink="web" + android:text="@string/credits" + android:textAppearance="@style/TextAppearance.AppCompat.Body1" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="@id/middle" + app:layout_constraintTop_toTopOf="parent" /> + +</androidx.constraintlayout.widget.ConstraintLayout> diff --git a/app/src/main/res/menu/departures_menu.xml b/app/src/main/res/menu/departures_menu.xml new file mode 100644 index 0000000000000000000000000000000000000000..2a4d18f7c839b628af7e21dcae639ed287fb05e5 --- /dev/null +++ b/app/src/main/res/menu/departures_menu.xml @@ -0,0 +1,36 @@ +<?xml version="1.0" encoding="utf-8"?> + +<!-- +SPDX-FileCopyrightText: Adam Evyčędo + +SPDX-License-Identifier: GPL-3.0-or-later +--> + +<menu xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto"> + <item + android:id="@+id/departures_filter" + android:icon="@drawable/filter" + app:showAsAction="ifRoom" + android:contentDescription="@string/title_filter" + android:title="@string/title_filter" > + <menu> + <item + android:id="@+id/departures_filter_byline" + app:showAsAction="never" + android:contentDescription="@string/title_filter_byline" + android:title="@string/title_filter_byline" /> + <item + android:id="@+id/departures_filter_bytime" + app:showAsAction="never" + android:contentDescription="@string/title_filter_bytime" + android:title="@string/title_filter_bytime" /> + </menu> + </item> + <item + android:id="@+id/departures_calendar" + android:icon="@drawable/calendar" + app:showAsAction="ifRoom" + android:contentDescription="@string/title_select_date" + android:title="@string/title_select_date"/> +</menu> diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 338d4237c2978af3fe102d073f412fb1d23d9c81..6f5f27b1f333372ff398c544e20ae0630f122041 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -16,6 +16,8 @@ #197f00 <color name="bimba_orange">#be7e3e</color> <color name="black">#FF000000</color> <color name="white">#FFFFFFFF</color> + <color name="safety">#eeD202</color> + <color name="link">#0000ff</color> <color name="seed">#54af39</color> <color name="md_theme_light_primary">#1A6D00</color> <!-- 40 --> diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 8b0524c6126083cb284dacaa638ca95ba4efe2b3..7c8b6eb99c2812fbdceb7a75eec84c1f2c7bb2c2 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -103,7 +103,7 @@» %1$s <string name="credits">Font yellowcircle8 (https://git.apiote.xyz/yellowcircle8.git) based on Railway Sans © Greg Fleming, OFL-1.1 https://github.com/davelab6/Railway-Sans\n\n Mastodon icon (https://github.com/mastodon/joinmastodon) © Mastodon contributors, AGPL-3.0-or-later\n\n Bimba logo created by https://github.com/tebriz159\n\n Material icons © Google, Apache-2.0\n\n Map data © OpenStreetMap contributors, ODbL-1.0</string> <string name="title_about">About</string> <string name="translation_button_description">link to translations service</string> - <string name="app_description">FLOSS public transport passenger companion; a timetable in your pocket.</string> + <string name="app_description">FLOSS public transport passenger companion; a timetable in your pocket.</string> <string name="website_button_description">link to website</string> <string name="code_button_description">link to source code</string> <string name="mastodon_button_description">link to Mastodon</string> @@ -114,5 +114,15 @@App version is not compatible with the server <string name="filter_localities">filter localities</string> <string name="error_41">This locality is not supported by the server</string> <string name="stop_from_qr_code">QR code stop</string> - <string name="departures_snackbar">Last update: %1$s</string> -</resources> \ No newline at end of file + <string name="departures_snackbar">Last update: %1$s</string> + <string name="title_select_date">Select day of departures</string> + <string name="title_select_line">Select line</string> + <string name="clear_date_selection">Clear</string> + <string name="title_filter">Filter</string> + <string name="title_filter_byline">Filter by line</string> + <string name="title_filter_bytime">Filter by time</string> + <string name="title_select_time_start">Select start time</string> + <string name="title_select_time_end">Select end time</string> + <string name="more">More</string> + <string name="alert_header">Status updates</string> +</resources> diff --git a/app/src/main/res/values-en-rGB/strings.xml b/app/src/main/res/values-en-rGB/strings.xml index a29ad3303af3a290263427ff3950ee82602572bf..68de08ea5bcbaf161327f182bc53ee5445d4beb6 100644 --- a/app/src/main/res/values-en-rGB/strings.xml +++ b/app/src/main/res/values-en-rGB/strings.xml @@ -1,9 +1,14 @@ <?xml version="1.0" encoding="utf-8"?> - <!-- SPDX-FileCopyrightText: Adam Evyčędo and contributors using Weblate SPDX-License-Identifier: GPL-3.0-or-later ---> - -<resources></resources> +--><resources> + <string name="app_name">Bimba</string> + <string name="title_home">Home</string> + <string name="title_map">Map</string> + <string name="title_voyage">Journey</string> + <string name="title_activity_results">Results</string> + <string name="cont">Continue</string> + <string name="save">Save</string> +</resources> \ No newline at end of file diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index a5365ed11a1f0e220bc23586d5ac812c8d39d5f5..06a8812c2b57108762c4e9d8f5ff0308501f0996 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -1,120 +1,127 @@ <?xml version="1.0" encoding="utf-8"?> - <!-- SPDX-FileCopyrightText: Adam Evyčędo SPDX-License-Identifier: GPL-3.0-or-later ---> - -<resources> - <string name="app_name">Bimba</string> - <string name="title_home">Casa</string> - <string name="title_map">Cartina</string> - <string name="title_voyage">Viaggio</string> - <string name="home_fab_description">icona GPS</string> - <string name="search_placeholder">fermate, linee o codici OLC</string> - <string name="title_activity_results">Risultati</string> - <string name="cont">Continua</string> - <string name="save">Salva</string> - <string name="error_400">L’app ha fatto una richiesta malformata</string> - <string name="error_401">Un gettone è necessario per usare questo server</string> - <string name="error_403">Il gettone è sbagliato</string> - <string name="error_404">Non trovato</string> - <string name="error_429">Superato il limite di frequenza. Prova di nuovo più tardi</string> - <string name="error_50x">C’era un errore sul sever. Prova di nuovo più tardi</string> - <string name="error_unknown">Un errrore sconosciutto è successo</string> - <string name="error_connecting">Errore di collegamento al server. Prova di nuovo più tardi</string> - <string name="error_offline">Sei offline. Collega al’Internet</string> - <string name="error_gps">Non è possibile ottenere la posizione corrente</string> - <string name="no_departures">Nessune partenze</string> - <string name="waiting_position">In attesa della posizione</string> - <string name="vehicle_headsign">%1$s » %2$s</string> - <string name="vehicle_headsign_content_description">%1$s verso %2$s</string> - <string name="speed_in_km_per_h">%1$.3f km/h</string> - <string name="congestion_unknown">sconosciuta</string> - <string name="congestion_smooth">fluido</string> - <string name="congestion_stop_and_go">fermarsi e andare</string> - <string name="congestion_congestion">congestione</string> - <string name="congestion_jams">ingorghi</string> - <string name="occupancy_unknown">sconosciuta</string> - <string name="occupancy_empty">vouto</string> - <string name="occupancy_many_seats">molte sedie</string> - <string name="occupancy_few_seats">pochi sedie</string> - <string name="occupancy_standing_only">solo in piedi</string> - <string name="occupancy_crowded">affollato</string> - <string name="occupancy_full">pieno</string> - <string name="occupancy_wont_let">non fa entrare</string> - <string name="no_map_app">Nessuna app cartine</string> - <string name="departure_headsign">» %1$s</string> - <string name="departure_headsign_content_description">verso %1$s</string> - <string name="departure_momentarily">presto</string> - <string name="departure_departed">partito</string> - <string name="departure_now">adesso</string> - <string name="at_time">alle %1$02d:%2$02d</string> - <string name="at_time_realtime">alle %1$02d:%2$02d:%3$02d</string> - <string name="on_demand">su richiesta</string> - <string name="no_boarding">nessun imbarco</string> - <string name="on_boarding">salire</string> - <string name="off_boarding">scendere</string> - <string name="boarding">imbarco</string> - <string name="line_headsign">» %1$s</string> - <string name="line_headsign_content_description">verso %1$s</string> - <string name="line_headsigns">%1$s «» %2$s</string> - <string name="line_headsigns_content_description">tra %1$s e %2$s</string> - <string name="stops_nearby">Fermate vicine</string> - <string name="results_for">Risultati per «%1$s»</string> - <string name="bimba_server_address_hint">Server</string> - <string name="bimba_server_token_hint">Gettone</string> - <string name="bimba_server_continue_button">Continua</string> - <string name="realtime_content_description">la partenza è in tempo reale</string> - <string name="wheelchair_content_description">il veicolo è accessibile alle sedie a rotelle</string> - <string name="air_condition_content_description">climatizzazione</string> - <string name="bicycles_allowed_content_description">bici permesse</string> - <string name="voice_announcements_content_description">avvisi vocali</string> - <string name="tickets_sold_content_description">biglietti venduti a bordo</string> - <string name="usb_charging_content_description">ricarica USB</string> - <string name="show_departures">Mostra le partenze</string> - <string name="open_in_maps_app">Apri nell’app cartine</string> - <string name="stop_content_description">fermata</string> - <string name="seatbelts_everyone">Allacciate le cinture!</string> - <string name="onboarding_question">Come comminciamo?</string> - <string name="onboarding_simple">Semplicemente</string> - <string name="onboarding_simple_action">scegli località</string> - <string name="onboarding_advanced">Avanzato</string> - <string name="onboarding_advanced_action">scegli server</string> - <string name="cancel">Anulla</string> - <string name="error">Errore</string> - <string name="rate_limit">Limite di frequenza</string> - <string name="server_rate_limited_question">Il server è a frequenza limitata e non è stato fornito alcun gettone. Vuoi continuare?</string> - <string name="server_private_question">Il server è privato e non è stato fornito alcun gettone</string> - <string name="last_update">Aggiornamento più recente: %1$s</string> - <string name="title_feeds">Orari</string> - <string name="title_servers">Server</string> - <string name="title_cities">Località</string> - <string name="error_url">URL malformato fornito</string> - <string name="error_traffic_spec">Impossibile verificare il server</string> - <string name="stops_near_code">Fermate vicino a %1$s</string> - <string name="code_is_not_full">Il codice non è pieno</string> - <string name="choose_server">Sceglii la varietà del server</string> - <string name="ok">OK</string> - <string name="no_location_access">Accesso alla posizione non fornito</string> - <string name="no_location_message">È necessaria l’autorizzazione all’uso della posizione per trovare le fermate vicine e per mostrare la posizione corrente sulla mappa. Le altre funzioni funzionano anche senza l’autorizzazione. Può essere attivata e disattivata in qualsiasi momento nelle impostazioni di sistema.</string> - <string name="stop_stub_on_demand_in_zone">Fermata su richiesta nella zona %1$s</string> - <string name="stop_stub_on_demand">Fermata su richiesta</string> - <string name="stop_stub_in_zone">Fermata nella zona %1$s</string> - <string name="title_about">Che cos\'è</string> - <string name="translation_button_description">link al servizio di traduzioni</string> - <string name="app_description">Compagno FLOSS di passeggero di trasport pubblico; un orario nella tasca</string> - <string name="website_button_description">link al sito web</string> - <string name="code_button_description">link al codice sorgente</string> - <string name="mastodon_button_description">link a Mastodon</string> - <string name="use_online_feed">Usa online</string> - <string name="information_may_be_outdated">Le informazioni possono essere obsolete</string> - <string name="current_timetable_validity">Orario attuale valido: %1$s fino alla %1$s</string> - <string name="error_406">La versione dell’app non è compatibile con il server</string> - <string name="filter_localities">filtra le località</string> - <string name="error_41">Questa località non è supportata dal server</string> - <string name="credits">Font yellowcircle8 (https://git.apiote.xyz/yellowcircle8.git) basato su Railway Sans © Greg Fleming, OFL-1.1 https://github.com/davelab6/Railway-Sans\n\n L’icona Mastodona (https://github.com/mastodon/joinmastodon) © Mastodon contributors, AGPL-3.0-or-later\n\n Logo Bimby stworzone przez https://github.com/tebriz159\n\n Material icons © Google, Apache-2.0\n\n Dane mapy © kontrybutorzy OpenStreetMap, ODbL-1.0</string> - <string name="stop_from_qr_code">Stop del codice QR</string> - <string name="departures_snackbar">L’ultimo aggiornamento: %1$s</string> +--><resources> + <string name="app_name">Bimba</string> + <string name="title_home">Casa</string> + <string name="title_map">Cartina</string> + <string name="title_voyage">Viaggio</string> + <string name="home_fab_description">icona GPS</string> + <string name="search_placeholder">fermate, linee o codici OLC</string> + <string name="title_activity_results">Risultati</string> + <string name="cont">Continua</string> + <string name="save">Salva</string> + <string name="error_400">L’app ha fatto una richiesta malformata</string> + <string name="error_401">Un gettone è necessario per usare questo server</string> + <string name="error_403">Il gettone è sbagliato</string> + <string name="error_404">Non trovato</string> + <string name="error_429">Superato il limite di frequenza. Prova di nuovo più tardi</string> + <string name="error_50x">C’era un errore sul sever. Prova di nuovo più tardi</string> + <string name="error_unknown">Un errrore sconosciutto è successo</string> + <string name="error_connecting">Errore di collegamento al server. Prova di nuovo più tardi</string> + <string name="error_offline">Sei offline. Collega al’Internet</string> + <string name="error_gps">Non è possibile ottenere la posizione corrente</string> + <string name="no_departures">Nessune partenze</string> + <string name="waiting_position">In attesa della posizione</string> + <string name="vehicle_headsign">%1$s » %2$s</string> + <string name="vehicle_headsign_content_description">%1$s verso %2$s</string> + <string name="speed_in_km_per_h">%1$.3f km/h</string> + <string name="congestion_unknown">sconosciuta</string> + <string name="congestion_smooth">fluido</string> + <string name="congestion_stop_and_go">fermarsi e andare</string> + <string name="congestion_congestion">congestione</string> + <string name="congestion_jams">ingorghi</string> + <string name="occupancy_unknown">sconosciuta</string> + <string name="occupancy_empty">vouto</string> + <string name="occupancy_many_seats">molte sedie</string> + <string name="occupancy_few_seats">pochi sedie</string> + <string name="occupancy_standing_only">solo in piedi</string> + <string name="occupancy_crowded">affollato</string> + <string name="occupancy_full">pieno</string> + <string name="occupancy_wont_let">non fa entrare</string> + <string name="no_map_app">Nessuna app cartine</string> + <string name="departure_headsign">» %1$s</string> + <string name="departure_headsign_content_description">verso %1$s</string> + <string name="departure_momentarily">presto</string> + <string name="departure_departed">partito</string> + <string name="departure_now">adesso</string> + <string name="at_time">alle %1$02d:%2$02d</string> + <string name="at_time_realtime">alle %1$02d:%2$02d:%3$02d</string> + <string name="on_demand">su richiesta</string> + <string name="no_boarding">nessun imbarco</string> + <string name="on_boarding">salire</string> + <string name="off_boarding">scendere</string> + <string name="boarding">imbarco</string> + <string name="line_headsign">» %1$s</string> + <string name="line_headsign_content_description">verso %1$s</string> + <string name="line_headsigns">%1$s «» %2$s</string> + <string name="line_headsigns_content_description">tra %1$s e %2$s</string> + <string name="stops_nearby">Fermate vicine</string> + <string name="results_for">Risultati per «%1$s»</string> + <string name="bimba_server_address_hint">Server</string> + <string name="bimba_server_token_hint">Gettone</string> + <string name="bimba_server_continue_button">Continua</string> + <string name="realtime_content_description">la partenza è in tempo reale</string> + <string name="wheelchair_content_description">il veicolo è accessibile alle sedie a rotelle</string> + <string name="air_condition_content_description">climatizzazione</string> + <string name="bicycles_allowed_content_description">bici permesse</string> + <string name="voice_announcements_content_description">avvisi vocali</string> + <string name="tickets_sold_content_description">biglietti venduti a bordo</string> + <string name="usb_charging_content_description">ricarica USB</string> + <string name="show_departures">Mostra le partenze</string> + <string name="open_in_maps_app">Apri nell’app cartine</string> + <string name="stop_content_description">fermata</string> + <string name="seatbelts_everyone">Allacciate le cinture!</string> + <string name="onboarding_question">Come comminciamo?</string> + <string name="onboarding_simple">Semplicemente</string> + <string name="onboarding_simple_action">scegli località</string> + <string name="onboarding_advanced">Avanzato</string> + <string name="onboarding_advanced_action">scegli server</string> + <string name="cancel">Anulla</string> + <string name="error">Errore</string> + <string name="rate_limit">Limite di frequenza</string> + <string name="server_rate_limited_question">Il server è a frequenza limitata e non è stato fornito alcun gettone. Vuoi continuare?</string> + <string name="server_private_question">Il server è privato e non è stato fornito alcun gettone</string> + <string name="last_update">Aggiornamento più recente: %1$s</string> + <string name="title_feeds">Orari</string> + <string name="title_servers">Server</string> + <string name="title_cities">Località</string> + <string name="error_url">URL malformato fornito</string> + <string name="error_traffic_spec">Impossibile verificare il server</string> + <string name="stops_near_code">Fermate vicino a %1$s</string> + <string name="code_is_not_full">Il codice non è pieno</string> + <string name="choose_server">Scegli la varietà del server</string> + <string name="ok">OK</string> + <string name="no_location_access">Accesso alla posizione non fornito</string> + <string name="no_location_message">È necessaria l’autorizzazione all’uso della posizione per trovare le fermate vicine e per mostrare la posizione corrente sulla mappa. Le altre funzioni funzionano anche senza l’autorizzazione. Può essere attivata e disattivata in qualsiasi momento nelle impostazioni di sistema.</string> + <string name="stop_stub_on_demand_in_zone">Fermata su richiesta nella zona %1$s</string> + <string name="stop_stub_on_demand">Fermata su richiesta</string> + <string name="stop_stub_in_zone">Fermata nella zona %1$s</string> + <string name="title_about">Che cos\'è</string> + <string name="translation_button_description">link al servizio di traduzioni</string> + <string name="app_description">Compagno FLOSS di passeggero di trasport pubblico; un orario nella tasca</string> + <string name="website_button_description">link al sito web</string> + <string name="code_button_description">link al codice sorgente</string> + <string name="mastodon_button_description">link a Mastodon</string> + <string name="use_online_feed">Usa online</string> + <string name="information_may_be_outdated">Le informazioni possono essere obsolete</string> + <string name="current_timetable_validity">Orario attuale valido: %1$s fino alla %2$s</string> + <string name="error_406">La versione dell’app non è compatibile con il server</string> + <string name="filter_localities">filtra le località</string> + <string name="error_41">Questa località non è supportata dal server</string> + <string name="credits">Font yellowcircle8 (https://git.apiote.xyz/yellowcircle8.git) basato su Railway Sans © Greg Fleming, OFL-1.1 https://github.com/davelab6/Railway-Sans\n\n L’icona Mastodona (https://github.com/mastodon/joinmastodon) © Mastodon contributors, AGPL-3.0-or-later\n\n Logo Bimby stworzone przez https://github.com/tebriz159\n\n Material icons © Google, Apache-2.0\n\n Dane mapy © kontrybutorzy OpenStreetMap, ODbL-1.0</string> + <string name="stop_from_qr_code">Stop del codice QR</string> + <string name="departures_snackbar">L’ultimo aggiornamento: %1$s</string> + <string name="title_select_date">Scegli il giorno della partenza</string> + <string name="title_select_line">Scegli la linea</string> + <string name="clear_date_selection">Azzera</string> + <string name="title_filter">Fìltra</string> + <string name="title_filter_byline">Filtra per linee</string> + <string name="title_filter_bytime">Filtra per orari</string> + <string name="title_select_time_start">Scegli l’inizio</string> + <string name="title_select_time_end">Scegli il fine</string> + <string name="more">Più</string> + <string name="alert_header">Informazioni tempestive</string> </resources> \ No newline at end of file diff --git a/app/src/main/res/values-nb-rNO/strings.xml b/app/src/main/res/values-nb-rNO/strings.xml index a29ad3303af3a290263427ff3950ee82602572bf..4c87604b47f664e0663bbc07cdf7d3db1016c94b 100644 --- a/app/src/main/res/values-nb-rNO/strings.xml +++ b/app/src/main/res/values-nb-rNO/strings.xml @@ -1,7 +1,7 @@ <?xml version="1.0" encoding="utf-8"?> <!-- -SPDX-FileCopyrightText: Adam Evyčędo and contributors using Weblate +SPDX-FileCopyrightText: Adam Evyčędo SPDX-License-Identifier: GPL-3.0-or-later --> diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index ce4ad730166f84d7c71819866e2c96ae4393295d..4bcbeaae1cf21379a118eedf1fbe3a10b98db137 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -1,120 +1,127 @@ <?xml version="1.0" encoding="utf-8"?> - <!-- SPDX-FileCopyrightText: Adam Evyčędo SPDX-License-Identifier: GPL-3.0-or-later ---> - -<resources> - <string name="app_name">Bimba</string> - <string name="title_home">Dom</string> - <string name="title_map">Mapa</string> - <string name="title_voyage">Podróż</string> - <string name="home_fab_description">ikona lokalizacji</string> - <string name="search_placeholder">przystanki, linie lub kody OLC</string> - <string name="title_activity_results">Wyniki</string> - <string name="cont">Kontynuuj</string> - <string name="save">Zapisz</string> - <string name="error_400">Aplikacja wykonała niepoprawne żądanie</string> - <string name="error_401">Żeton jest wymagany, aby używać tego serwera</string> - <string name="error_403">Żeton jest niepoprawny</string> - <string name="error_404">Nie znaleziono</string> - <string name="error_429">Przekroczono limit prób. Spróbuj ponownie później</string> - <string name="error_50x">Błąd serwera. Spróbuj ponownie później</string> - <string name="error_unknown">Wystąpił nieznany błąd</string> - <string name="error_connecting">Błąd połączenia z serwerem. Spróbuj ponownie później</string> - <string name="error_offline">Brak połączenia z Internetem</string> - <string name="error_gps">Nie można uzyskać bierzącej pozycji</string> - <string name="no_departures">Brak odjazdów</string> - <string name="waiting_position">oczekiwanie na pozycję</string> - <string name="vehicle_headsign">%1$s » %2$s</string> - <string name="vehicle_headsign_content_description">%1$s w kierunku przystanku %2$s</string> - <string name="speed_in_km_per_h">%1$.3f km/h</string> - <string name="congestion_unknown">nieznane</string> - <string name="congestion_smooth">płynne</string> - <string name="congestion_stop_and_go">przestoje</string> - <string name="congestion_congestion">korki</string> - <string name="congestion_jams">zatory</string> - <string name="occupancy_unknown">nieznana</string> - <string name="occupancy_empty">pusty</string> - <string name="occupancy_many_seats">wiele miejsc</string> - <string name="occupancy_few_seats">kilka miejsc</string> - <string name="occupancy_standing_only">tylko stojące</string> - <string name="occupancy_crowded">zatłoczony</string> - <string name="occupancy_full">pełny</string> - <string name="occupancy_wont_let">nie wpuszcza</string> - <string name="no_map_app">Brak aplikacji map</string> - <string name="departure_headsign">» %1$s</string> - <string name="departure_headsign_content_description">w kierunku przystanku %1$s</string> - <string name="departure_momentarily">za moment</string> - <string name="departure_departed">odjechał</string> - <string name="departure_now">teraz</string> - <string name="at_time">o %1$02d:%2$02d</string> - <string name="at_time_realtime">o %1$02d:%2$02d:%3$02d</string> - <string name="on_demand">na żądanie</string> - <string name="no_boarding">brak</string> - <string name="on_boarding">wsiadanie</string> - <string name="off_boarding">wysiadanie</string> - <string name="boarding">standard</string> - <string name="line_headsign">» %1$s</string> - <string name="line_headsign_content_description">w kierunku przystanku %1$s</string> - <string name="line_headsigns">%1$s «» %2$s</string> - <string name="line_headsigns_content_description">pomiędzy przystankami %1$s i %2$s</string> - <string name="stops_nearby">Przystanki w pobliżu</string> - <string name="results_for">Wyniki dla „%1$s”</string> - <string name="bimba_server_address_hint">Serwer</string> - <string name="bimba_server_token_hint">Żeton</string> - <string name="bimba_server_continue_button">Kontynuuj</string> - <string name="realtime_content_description">odjazd w czasie rzeczywistym</string> - <string name="wheelchair_content_description">pojazd ma niską podłogę</string> - <string name="air_condition_content_description">klimatyzacja</string> - <string name="bicycles_allowed_content_description">przewóz rowerów dozwolony</string> - <string name="voice_announcements_content_description">komunikaty głosowe</string> - <string name="tickets_sold_content_description">możliwość kupienia biletów na pokładzie</string> - <string name="usb_charging_content_description">ładowarki USB</string> - <string name="show_departures">Pokaż odjazdy</string> - <string name="open_in_maps_app">Otwórz w aplikacji map</string> - <string name="stop_content_description">przystanek</string> - <string name="seatbelts_everyone">Zajmujcie miejsca!</string> - <string name="onboarding_question">Jak zaczynamy?</string> - <string name="onboarding_simple">Prosto</string> - <string name="onboarding_simple_action">wybór lokalizacji</string> - <string name="onboarding_advanced">Zaawansowane</string> - <string name="onboarding_advanced_action">wybór serwerów</string> - <string name="cancel">Anuluj</string> - <string name="error">Błąd</string> - <string name="rate_limit">Limit żądań</string> - <string name="server_rate_limited_question">Nie podano żetona, a serwer limituje żądania. Czy chcesz kontynuować?</string> - <string name="server_private_question">Nie podano żetona, a serwer jest prywatny</string> - <string name="last_update">Ostatnia aktualizacja: %1$s</string> - <string name="title_feeds">Rozkłady</string> - <string name="title_servers">Serwery</string> - <string name="title_cities">Lokalizacje</string> - <string name="error_url">Podano błędny URL</string> - <string name="error_traffic_spec">Nie można zweryfikować serwera</string> - <string name="stops_near_code">Przystanki w pobliżu %1$s</string> - <string name="code_is_not_full">Kod nie jest pełen</string> - <string name="choose_server">Wybierz rodzaj serwera</string> - <string name="ok">OK</string> - <string name="no_location_access">Brak uprawnień do lokalizacji</string> - <string name="no_location_message">Uprawnienia do używania lokalizacji są wymagane, aby znaleźć przystanki w pobliżu i pokazać aktualną pozycję na mapie. Pozostałe funkcje będą działały bez tych uprawnień. Mogą być one w każdym momencie nadane i odebrane w ustawieniach systemowych.</string> - <string name="stop_stub_on_demand_in_zone">Przystanek na żądanie w strefie %1$s</string> - <string name="stop_stub_on_demand">Przystanek na żądanie</string> - <string name="stop_stub_in_zone">Przystanek w strefie %1$s</string> - <string name="credits">Font yellowcircle8 (https://git.apiote.xyz/yellowcircle8.git) na podstawie Railway Sans © Greg Fleming, OFL-1.1 https://github.com/davelab6/Railway-Sans\n\n Ikona Mastodona (https://github.com/mastodon/joinmastodon) © Mastodon contributors, AGPL-3.0-or-later\n\n Logo Bimby stworzone przez https://github.com/tebriz159\n\n Material icons © Google, Apache-2.0\n\n Dane mapy © kontrybutorzy OpenStreetMap, ODbL-1.0</string> - <string name="title_about">O Bimbie</string> - <string name="translation_button_description">link do narzędzia do tłumaczeń</string> - <string name="app_description">Wolny i otwarty kompan pasażerów transportu publicznego; rozkład jazdy w twojej kieszeni</string> - <string name="website_button_description">link do strony internetowej</string> - <string name="code_button_description">link do kodu źródłowego</string> - <string name="mastodon_button_description">link do Mastodona</string> - <string name="use_online_feed">Używaj online</string> - <string name="information_may_be_outdated">Informacje mogą być nieaktualne</string> - <string name="current_timetable_validity">Aktualny rozkła ważny: %1$s do %2$s</string> - <string name="error_406">Wersja aplikacji jest niekompatybilna z serwerem</string> - <string name="filter_localities">filtruj lokalizacje</string> - <string name="error_41">Ta lokalizacja nie jest obsługiwana przez serwer</string> - <string name="stop_from_qr_code">Przystanek z kodem QR</string> - <string name="departures_snackbar">Ostatnia aktualizacja: %1$s</string> +--><resources> + <string name="app_name">Bimba</string> + <string name="title_home">Dom</string> + <string name="title_map">Mapa</string> + <string name="title_voyage">Podróż</string> + <string name="home_fab_description">ikona lokalizacji</string> + <string name="search_placeholder">przystanki, linie lub kody OLC</string> + <string name="title_activity_results">Wyniki</string> + <string name="cont">Kontynuuj</string> + <string name="save">Zapisz</string> + <string name="error_400">Aplikacja wykonała niepoprawne żądanie</string> + <string name="error_401">Żeton jest wymagany, aby używać tego serwera</string> + <string name="error_403">Żeton jest niepoprawny</string> + <string name="error_404">Nie znaleziono</string> + <string name="error_429">Przekroczono limit prób. Spróbuj ponownie później</string> + <string name="error_50x">Błąd serwera. Spróbuj ponownie później</string> + <string name="error_unknown">Wystąpił nieznany błąd</string> + <string name="error_connecting">Błąd połączenia z serwerem. Spróbuj ponownie później</string> + <string name="error_offline">Brak połączenia z Internetem</string> + <string name="error_gps">Nie można uzyskać bierzącej pozycji</string> + <string name="no_departures">Brak odjazdów</string> + <string name="waiting_position">oczekiwanie na pozycję</string> + <string name="vehicle_headsign">%1$s » %2$s</string> + <string name="vehicle_headsign_content_description">%1$s w kierunku przystanku %2$s</string> + <string name="speed_in_km_per_h">%1$.3f km/h</string> + <string name="congestion_unknown">nieznane</string> + <string name="congestion_smooth">płynne</string> + <string name="congestion_stop_and_go">przestoje</string> + <string name="congestion_congestion">korki</string> + <string name="congestion_jams">zatory</string> + <string name="occupancy_unknown">nieznana</string> + <string name="occupancy_empty">pusty</string> + <string name="occupancy_many_seats">wiele miejsc</string> + <string name="occupancy_few_seats">kilka miejsc</string> + <string name="occupancy_standing_only">tylko stojące</string> + <string name="occupancy_crowded">zatłoczony</string> + <string name="occupancy_full">pełny</string> + <string name="occupancy_wont_let">nie wpuszcza</string> + <string name="no_map_app">Brak aplikacji map</string> + <string name="departure_headsign">» %1$s</string> + <string name="departure_headsign_content_description">w kierunku przystanku %1$s</string> + <string name="departure_momentarily">za moment</string> + <string name="departure_departed">odjechał</string> + <string name="departure_now">teraz</string> + <string name="at_time">o %1$02d:%2$02d</string> + <string name="at_time_realtime">o %1$02d:%2$02d:%3$02d</string> + <string name="on_demand">na żądanie</string> + <string name="no_boarding">brak</string> + <string name="on_boarding">wsiadanie</string> + <string name="off_boarding">wysiadanie</string> + <string name="boarding">standard</string> + <string name="line_headsign">» %1$s</string> + <string name="line_headsign_content_description">w kierunku przystanku %1$s</string> + <string name="line_headsigns">%1$s «» %2$s</string> + <string name="line_headsigns_content_description">pomiędzy przystankami %1$s i %2$s</string> + <string name="stops_nearby">Przystanki w pobliżu</string> + <string name="results_for">Wyniki dla „%1$s”</string> + <string name="bimba_server_address_hint">Serwer</string> + <string name="bimba_server_token_hint">Żeton</string> + <string name="bimba_server_continue_button">Kontynuuj</string> + <string name="realtime_content_description">odjazd w czasie rzeczywistym</string> + <string name="wheelchair_content_description">pojazd ma niską podłogę</string> + <string name="air_condition_content_description">klimatyzacja</string> + <string name="bicycles_allowed_content_description">przewóz rowerów dozwolony</string> + <string name="voice_announcements_content_description">komunikaty głosowe</string> + <string name="tickets_sold_content_description">możliwość kupienia biletów na pokładzie</string> + <string name="usb_charging_content_description">ładowarki USB</string> + <string name="show_departures">Pokaż odjazdy</string> + <string name="open_in_maps_app">Otwórz w aplikacji map</string> + <string name="stop_content_description">przystanek</string> + <string name="seatbelts_everyone">Zajmujcie miejsca!</string> + <string name="onboarding_question">Jak zaczynamy?</string> + <string name="onboarding_simple">Prosto</string> + <string name="onboarding_simple_action">wybór lokalizacji</string> + <string name="onboarding_advanced">Zaawansowane</string> + <string name="onboarding_advanced_action">wybór serwerów</string> + <string name="cancel">Anuluj</string> + <string name="error">Błąd</string> + <string name="rate_limit">Limit żądań</string> + <string name="server_rate_limited_question">Nie podano żetona, a serwer limituje żądania. Czy chcesz kontynuować?</string> + <string name="server_private_question">Nie podano żetona, a serwer jest prywatny</string> + <string name="last_update">Ostatnia aktualizacja: %1$s</string> + <string name="title_feeds">Rozkłady</string> + <string name="title_servers">Serwery</string> + <string name="title_cities">Lokalizacje</string> + <string name="error_url">Podano błędny URL</string> + <string name="error_traffic_spec">Nie można zweryfikować serwera</string> + <string name="stops_near_code">Przystanki w pobliżu %1$s</string> + <string name="code_is_not_full">Kod nie jest pełen</string> + <string name="choose_server">Wybierz rodzaj serwera</string> + <string name="ok">OK</string> + <string name="no_location_access">Brak uprawnień do lokalizacji</string> + <string name="no_location_message">Uprawnienia do używania lokalizacji są wymagane, aby znaleźć przystanki w pobliżu i pokazać aktualną pozycję na mapie. Pozostałe funkcje będą działały bez tych uprawnień. Mogą być one w każdym momencie nadane i odebrane w ustawieniach systemowych.</string> + <string name="stop_stub_on_demand_in_zone">Przystanek na żądanie w strefie %1$s</string> + <string name="stop_stub_on_demand">Przystanek na żądanie</string> + <string name="stop_stub_in_zone">Przystanek w strefie %1$s</string> + <string name="credits">Font yellowcircle8 (https://git.apiote.xyz/yellowcircle8.git) na podstawie Railway Sans © Greg Fleming, OFL-1.1 https://github.com/davelab6/Railway-Sans\n\n Ikona Mastodona (https://github.com/mastodon/joinmastodon) © Mastodon contributors, AGPL-3.0-or-later\n\n Logo Bimby stworzone przez https://github.com/tebriz159\n\n Material icons © Google, Apache-2.0\n\n Dane mapy © kontrybutorzy OpenStreetMap, ODbL-1.0</string> + <string name="title_about">O Bimbie</string> + <string name="translation_button_description">link do narzędzia do tłumaczeń</string> + <string name="app_description">Wolny i otwarty kompan pasażerów transportu publicznego; rozkład jazdy w twojej kieszeni</string> + <string name="website_button_description">link do strony internetowej</string> + <string name="code_button_description">link do kodu źródłowego</string> + <string name="mastodon_button_description">link do Mastodona</string> + <string name="use_online_feed">Używaj online</string> + <string name="information_may_be_outdated">Informacje mogą być nieaktualne</string> + <string name="current_timetable_validity">Aktualny rozkład ważny: %1$s do %2$s</string> + <string name="error_406">Wersja aplikacji jest niekompatybilna z serwerem</string> + <string name="filter_localities">filtruj lokalizacje</string> + <string name="error_41">Ta lokalizacja nie jest obsługiwana przez serwer</string> + <string name="stop_from_qr_code">Przystanek z kodem QR</string> + <string name="departures_snackbar">Ostatnia aktualizacja: %1$s</string> + <string name="title_select_date">Wybierz dzień odjazdów</string> + <string name="title_select_line">Wybierz linię</string> + <string name="clear_date_selection">Wyczyść</string> + <string name="title_filter">Filter</string> + <string name="title_filter_byline">Filtrowanie po liniach</string> + <string name="title_filter_bytime">Filtrowanie po czasie</string> + <string name="title_select_time_start">Wybierz początkowy czas</string> + <string name="title_select_time_end">Wybierz końcowy czas</string> + <string name="more">Więcej</string> + <string name="alert_header">Komunikaty</string> </resources> \ No newline at end of file diff --git a/app/src/main/res/xml/locales_config.xml b/app/src/main/res/xml/locales_config.xml deleted file mode 100644 index 7d619e622b1039ed7082483a13c74300c254de7e..0000000000000000000000000000000000000000 --- a/app/src/main/res/xml/locales_config.xml +++ /dev/null @@ -1,13 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> - -<!-- -SPDX-FileCopyrightText: Adam Evyčędo - -SPDX-License-Identifier: GPL-3.0-or-later ---> - -<locale-config xmlns:android="http://schemas.android.com/apk/res/android"> - <locale android:name="en" /> - <locale android:name="pl" /> - <locale android:name="it" /> -</locale-config> \ No newline at end of file diff --git a/app/src/release/java/xyz/apiote/bimba/czwek/api/responses/DevResponses.kt b/app/src/release/java/xyz/apiote/bimba/czwek/api/responses/DevResponses.kt new file mode 100644 index 0000000000000000000000000000000000000000..8290582612b62aedbdcfdc18846921dd774b2d60 --- /dev/null +++ b/app/src/release/java/xyz/apiote/bimba/czwek/api/responses/DevResponses.kt @@ -0,0 +1,69 @@ +// SPDX-FileCopyrightText: Adam Evyčędo +// +// SPDX-License-Identifier: GPL-3.0-or-later + +package xyz.apiote.bimba.czwek.api.responses + +import xyz.apiote.bimba.czwek.api.AlertV1 +import xyz.apiote.bimba.czwek.api.DepartureV4 +import xyz.apiote.bimba.czwek.api.LineV3 +import xyz.apiote.bimba.czwek.api.ColourV1 +import xyz.apiote.bimba.czwek.api.LineTypeV3 +import xyz.apiote.bimba.czwek.api.LocatableV3 +import xyz.apiote.bimba.czwek.api.QueryableV4 +import xyz.apiote.bimba.czwek.api.StopV2 +import xyz.apiote.bimba.czwek.api.PositionV1 +import xyz.apiote.bimba.czwek.api.UnknownResourceVersionException +import xyz.apiote.bimba.czwek.api.VehicleV3 +import xyz.apiote.bimba.czwek.api.structs.FeedInfoV2 +import xyz.apiote.fruchtfleisch.Reader +import java.io.InputStream + +data class DeparturesResponseDev( + val alerts: List<AlertV1>, + val departures: List<DepartureV4>, + val stop: StopV2 +) : DeparturesResponse { + companion object { + private fun unmarshal(stream: InputStream): DeparturesResponseDev { + return DeparturesResponseDev(listOf(), listOf(), StopV2("","","","","", PositionV1(0.0, 0.0), listOf())) + } + } +} + +data class FeedsResponseDev( + val feeds: List<FeedInfoV2> +) : FeedsResponse { + companion object { + private fun unmarshal(stream: InputStream): FeedsResponseDev { + return FeedsResponseDev(listOf()) + } + } +} + +data class LineResponseDev( + val line: LineV3 +) : LineResponse { + companion object { + private fun unmarshal(stream: InputStream): LineResponseDev { + return LineResponseDev(LineV3("","",ColourV1(0u,0u,0u),LineTypeV3.UNKNOWN,"",listOf(),listOf())) + } + } +} + +data class LocatablesResponseDev(val locatables: List<LocatableV3>) : LocatablesResponse { + companion object { + private fun unmarshal(stream: InputStream): LocatablesResponseDev { + return LocatablesResponseDev(listOf()) + } + } +} + + +data class QueryablesResponseDev(val queryables: List<QueryableV4>) : QueryablesResponse { + companion object { + private fun unmarshal(stream: InputStream): QueryablesResponseDev { + return QueryablesResponseDev(listOf()) + } + } +} diff --git a/build.gradle b/build.gradle index ec17768e78cd5c3819e1d1a71950aed04334e679..279f2c1cfcf841d408996f85e4d1efb1ffa12ef0 100644 --- a/build.gradle +++ b/build.gradle @@ -4,14 +4,10 @@ // SPDX-License-Identifier: GPL-3.0-or-later // Top-level build file where you can add configuration options common to all sub-projects/modules. plugins { - id 'com.android.application' version '8.3.0' apply false - id 'com.android.library' version '8.3.0' apply false + id 'com.android.application' version '8.3.2' apply false + id 'com.android.library' version '8.3.2' apply false id 'org.jetbrains.kotlin.android' version '1.7.10' apply false id 'org.jetbrains.kotlin.jvm' version '1.7.20' apply false id "org.jetbrains.kotlin.plugin.parcelize" version "1.8.20" apply false id 'org.jetbrains.kotlin.plugin.serialization' version '1.9.22' apply false } - -task clean(type: Delete) { - delete rootProject.buildDir -} \ No newline at end of file diff --git a/fruchtfleisch/build.gradle b/fruchtfleisch/build.gradle index a510340b4c57eef3e6a539c90d057d9dd62b44f1..85c32f76546645efcd429747057f155ce9b51955 100644 --- a/fruchtfleisch/build.gradle +++ b/fruchtfleisch/build.gradle @@ -8,9 +8,15 @@ id 'org.jetbrains.kotlin.jvm' } dependencies { + testImplementation 'org.junit.jupiter:junit-jupiter:5.8.1' + testImplementation 'org.junit.jupiter:junit-jupiter:5.8.1' + //implementation 'org.jetbrains.kotlin:kotlin-reflect:1.8.10' } java { sourceCompatibility = JavaVersion.VERSION_17 targetCompatibility = JavaVersion.VERSION_17 +} +test { + useJUnitPlatform() } \ No newline at end of file diff --git a/fruchtfleisch/src/main/java/xyz/apiote/fruchtfleisch/Reader.kt b/fruchtfleisch/src/main/java/xyz/apiote/fruchtfleisch/Reader.kt index a336c9f136e40ff0896895110071eb81a23f8ba3..3b29727d880991e2d5a8cd58f7e91bd2e34401f4 100644 --- a/fruchtfleisch/src/main/java/xyz/apiote/fruchtfleisch/Reader.kt +++ b/fruchtfleisch/src/main/java/xyz/apiote/fruchtfleisch/Reader.kt @@ -9,17 +9,10 @@ import java.io.InputStream import java.lang.Double.longBitsToDouble import java.lang.Float.intBitsToFloat -data class IntVar(private val v: Long) { - fun toLong() = v -} -data class UIntVar(private val v: ULong) { - fun toULong() = v -} - @Suppress("MemberVisibilityCanBePrivate", "unused", "BooleanMethodIsAlwaysInverted") class Reader(private val stream: InputStream) { fun readUInt(): UIntVar { - var result: ULong = 0UL + var result = 0UL var i = 0 var s = 0 while (true) { diff --git a/fruchtfleisch/src/main/java/xyz/apiote/fruchtfleisch/UInt.kt b/fruchtfleisch/src/main/java/xyz/apiote/fruchtfleisch/UInt.kt new file mode 100644 index 0000000000000000000000000000000000000000..d058ee473c7bc0bf6bc200dfc5aca6ac8da2ed84 --- /dev/null +++ b/fruchtfleisch/src/main/java/xyz/apiote/fruchtfleisch/UInt.kt @@ -0,0 +1,12 @@ +// SPDX-FileCopyrightText: Adam Evyčędo +// +// SPDX-License-Identifier: GPL-3.0-or-later + +package xyz.apiote.fruchtfleisch + +data class IntVar(private val v: Long) { + fun toLong() = v +} +data class UIntVar(private val v: ULong) { + fun toULong() = v +} \ No newline at end of file diff --git a/fruchtfleisch/src/main/java/xyz/apiote/fruchtfleisch/Writer.kt b/fruchtfleisch/src/main/java/xyz/apiote/fruchtfleisch/Writer.kt new file mode 100644 index 0000000000000000000000000000000000000000..7fe8ebe1fe76d0dbda1f0af4bb65cf8f69e673fe --- /dev/null +++ b/fruchtfleisch/src/main/java/xyz/apiote/fruchtfleisch/Writer.kt @@ -0,0 +1,35 @@ +// SPDX-FileCopyrightText: Adam Evyčędo +// +// SPDX-License-Identifier: GPL-3.0-or-later + +package xyz.apiote.fruchtfleisch + +import java.io.OutputStream + +@Suppress("MemberVisibilityCanBePrivate") +class Writer(private val stream: OutputStream) { + @OptIn(ExperimentalUnsignedTypes::class) + fun writeUInt(v: ULong) { + var value = v + val bytes = mutableListOf<UByte>() + while (value >= 0x80u) { + bytes.add(value.toUByte() or 0x80u) + value = value.shr(7) + } + bytes.add(value.toUByte()) + stream.write(bytes.toUByteArray().toByteArray()) + } + + fun writeFixedData(v: ByteArray) { + stream.write(v) + } + + fun writeData(v: ByteArray) { + writeUInt(v.size.toULong()) + writeFixedData(v) + } + + fun writeString(v: String) { + writeData(v.encodeToByteArray()) + } +} \ No newline at end of file diff --git a/fruchtfleisch/src/test/java/xyz/apiote/fruchtfleisch/ReaderTest.kt b/fruchtfleisch/src/test/java/xyz/apiote/fruchtfleisch/ReaderTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..210beec41cea3377f8cd050f2091bb4f4c1bb652 --- /dev/null +++ b/fruchtfleisch/src/test/java/xyz/apiote/fruchtfleisch/ReaderTest.kt @@ -0,0 +1,96 @@ +// SPDX-FileCopyrightText: Adam Evyčędo +// +// SPDX-License-Identifier: GPL-3.0-or-later + +package xyz.apiote.fruchtfleisch + +import org.junit.jupiter.api.Test + + +@OptIn(ExperimentalUnsignedTypes::class) +class ReaderTest { + @Test + fun readUInt17() { + val stream = byteArrayOf(0x11).inputStream() + val reader = Reader(stream) + assert(reader.readUInt().toULong().toInt() == 17) + } + @Test + fun readUInt23() { + val stream = byteArrayOf(0x17).inputStream() + val reader = Reader(stream) + assert(reader.readUInt().toULong().toInt() == 23) + } + @Test + fun readUInt999() { + val stream = ubyteArrayOf(0xe7u, 0x7u).toByteArray().inputStream() + val reader = Reader(stream) + assert(reader.readUInt().toULong().toInt() == 999) + } + + @Test + fun readInt() { + } + + @Test + fun readU8() { + } + + @Test + fun readU16() { + } + + @Test + fun readU32() { + } + + @Test + fun readU64() { + } + + @Test + fun readI8() { + } + + @Test + fun readI16() { + } + + @Test + fun readI32() { + } + + @Test + fun readI64() { + } + + @Test + fun readFloat32() { + } + + @Test + fun readFloat64() { + } + + @Test + fun readData() { + } + + @Test + fun readStringAscii() { + val stream = byteArrayOf(0x24, 0x4d, 0x72, 0x2e, 0x20, 0x4a, 0x6f, 0x63, 0x6b, 0x2c, 0x20, 0x54, 0x56, 0x20, 0x71, 0x75, 0x69, 0x7a, 0x20, 0x50, 0x68, 0x44, 0x2c, 0x20, 0x62, 0x61, 0x67, 0x73, 0x20, 0x66, 0x65, 0x77, 0x20, 0x6c, 0x79, 0x6e, 0x78).inputStream() + val reader = Reader(stream) + assert(reader.readString() == "Mr. Jock, TV quiz PhD, bags few lynx") + } + + @Test + fun readStringUnicode() { + val stream = ubyteArrayOf(0x34u, 0x53u, 0x74u, 0x72u, 0xc3u, 0xb3u, 0xc5u, 0xbcu, 0x20u, 0x70u, 0x63u, 0x68u, 0x6eu, 0xc4u, 0x85u, 0xc5u, 0x82u, 0x20u, 0x6bu, 0x6fu, 0xc5u, 0x9bu, 0xc4u, 0x87u, 0x20u, 0x77u, 0x20u, 0x71u, 0x75u, 0x69u, 0x7au, 0x20u, 0x67u, 0xc4u, 0x99u, 0x64u, 0xc5u, 0xbau, 0x62u, 0x20u, 0x76u, 0x65u, 0x6cu, 0x20u, 0x66u, 0x61u, 0x78u, 0x20u, 0x6du, 0x79u, 0x6au, 0xc5u, 0x84u).toByteArray().inputStream() + val reader = Reader(stream) + assert(reader.readString() == "Stróż pchnął kość w quiz gędźb vel fax myjń") + } + + @Test + fun readBoolean() { + } +} \ No newline at end of file diff --git a/fruchtfleisch/src/test/java/xyz/apiote/fruchtfleisch/WriterTest.kt b/fruchtfleisch/src/test/java/xyz/apiote/fruchtfleisch/WriterTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..cad8ee16bbcd6c8082e51e99b0302636c75913c1 --- /dev/null +++ b/fruchtfleisch/src/test/java/xyz/apiote/fruchtfleisch/WriterTest.kt @@ -0,0 +1,48 @@ +// SPDX-FileCopyrightText: Adam Evyčędo +// +// SPDX-License-Identifier: GPL-3.0-or-later + +package xyz.apiote.fruchtfleisch + +import org.junit.jupiter.api.Test +import java.io.ByteArrayOutputStream + +@OptIn(ExperimentalUnsignedTypes::class) +class WriterTest { + + @Test + fun writeUInt17() { + val stream = ByteArrayOutputStream() + val writer = Writer(stream) + writer.writeUInt(17u) + val bytes = stream.toByteArray() + assert(bytes.contentEquals(byteArrayOf(0x11))) + } + + @Test + fun writeUInt23() { + val stream = ByteArrayOutputStream() + val writer = Writer(stream) + writer.writeUInt(23u) + val bytes = stream.toByteArray() + assert(bytes.contentEquals(byteArrayOf(0x17))) + } + + @Test + fun writeUInt999() { + val stream = ByteArrayOutputStream() + val writer = Writer(stream) + writer.writeUInt(999u) + val bytes = stream.toByteArray().toUByteArray() + assert(bytes.contentEquals(ubyteArrayOf(0xe7u, 0x7u))) + } + + @Test + fun writeStringAscii() { + val stream = ByteArrayOutputStream() + val writer = Writer(stream) + writer.writeString("Mr. Jock, TV quiz PhD, bags few lynx") + val bytes = stream.toByteArray() + assert(bytes.contentEquals(byteArrayOf(0x24, 0x4d, 0x72, 0x2e, 0x20, 0x4a, 0x6f, 0x63, 0x6b, 0x2c, 0x20, 0x54, 0x56, 0x20, 0x71, 0x75, 0x69, 0x7a, 0x20, 0x50, 0x68, 0x44, 0x2c, 0x20, 0x62, 0x61, 0x67, 0x73, 0x20, 0x66, 0x65, 0x77, 0x20, 0x6c, 0x79, 0x6e, 0x78))) + } +} \ No newline at end of file diff --git a/metadata/en-GB/changelogs/25.txt b/metadata/en-GB/changelogs/25.txt new file mode 100644 index 0000000000000000000000000000000000000000..c8c2609f0a17d73980acfcf8d11fa13d9411c4ee --- /dev/null +++ b/metadata/en-GB/changelogs/25.txt @@ -0,0 +1,11 @@ +Changes in version 3.3 +* Added selecting date and filtering by time +* Added filtering departures by line +* Added alerts shown in stop and departures +* Fixed landscape version of ‘about’ screen +* Fixed capabilities for vehicles on map +* Changed cached localities to lower contrast +* Fixed showing line directions for other than 2 +* Fixed saving localities cache +* Updated dependencies +* Updated deprecated methods in code diff --git a/metadata/en-US/changelogs/25.txt b/metadata/en-US/changelogs/25.txt new file mode 100644 index 0000000000000000000000000000000000000000..0629d1dd250bc5eacdf49792617e42e57bc0ee02 --- /dev/null +++ b/metadata/en-US/changelogs/25.txt @@ -0,0 +1,11 @@ +Changes in version 3.3 +* Added selecting date and filtering by time +* Added filtering departures by line +* Added alerts shown in stop and departures +* Fixed landscape version of “about” screen +* Fixed capabilities for vehicles on map +* Changed cached localities to lower contrast +* Fixed showing line directions for other than 2 +* Fixed saving localities cache +* Updated dependencies +* Updated deprecated methods in code diff --git a/metadata/pl-PL/changelogs/25.txt b/metadata/pl-PL/changelogs/25.txt new file mode 100644 index 0000000000000000000000000000000000000000..098bde1fbac4f2486da1f3a5b4e78fa9ec92cc9c --- /dev/null +++ b/metadata/pl-PL/changelogs/25.txt @@ -0,0 +1,11 @@ +Zmiany w wersji 3.3 +* Dodano wybieranie daty i filtrowanie po czasie +* Dodano filtrowanie po linii +* Dodany alerty przy przystankach i odjazdach +* Poprawiono horyzontalną wersję ekranu o Bimbie +* Poprawiono cechy pojazdów na mapie +* Zmieniono zapisane lokalizacje na mniejszy kontrast +* Poprawiono kierunki linii dla innej liczby niż 2 +* Poprawiono zapisuwanie lokalizacji +* Zaktualizowano zależności +* Zaktualizowano przestarzałe metody w kodzie diff --git a/release.sh b/release.sh index 3911f00b6ba62512d5e54885dcbc99f2a73de5be..3d8e82bbf0884ed1f50f229fd1afe6bc67c81556 100755 --- a/release.sh +++ b/release.sh @@ -4,13 +4,21 @@ # SPDX-FileCopyrightText: Adam Evyčędo # # SPDX-License-Identifier: GPL-3.0-or-later +set -e + releaseType="" phase=0 case "$1" in major|minor|patch) releaseType=$1 ;; -c) phase=1 ;; + -t) phase=2 ;; + -h) + echo "release.sh (major|minor|patch)" + echo "release.sh (-c|-t)" + exit 0 + ;; *) - echo "no release type given or -c given" + echo "no release type, -c, or -t given" exit 1 ;; esac @@ -95,9 +103,24 @@ fi git add app/build.gradle git add metadata/ git commit -S -m "release version $newVersionName ($newVersionCode)" + echo 'pushing …' git push git switch master git merge -S --no-ff -m "merge develop into master for version $newVersionName" develop + echo 'pushing …' + git push + echo 'tag and push tag?' + read -r yn + if [ "$yn" != 'y' ] && [ "$yn" != 'Y' ]; then + exit 1 + fi + git tag -s -m "v${newVersionName}" "v${newVersionName}" + git push origin --tags + git switch develop + git merge master +elif [ $phase -eq 2 ] +then + newVersionName=$(grep 'versionName' app/build.gradle | tr -s ' ' | cut -d ' ' -f3 | tr -d '"') git tag -s -m "v${newVersionName}" "v${newVersionName}" git push origin --tags git switch develop