Bimba.git

commit c551691bfe838b2e93672c4be848ead2a61ad5d9

Author: Adam Evyčędo <git@apiote.xyz>

merge develop into master for version 3.6.0

%!v(PANIC=String method: strings: negative Repeat count)


diff --git a/CHANGELOG.adoc b/CHANGELOG.adoc
index 29474db0c178e8e7fe407d7c4101c311eed2dece..367bd8dfd668f769b62e19efc9e94772c4f324e3 100644
--- a/CHANGELOG.adoc
+++ b/CHANGELOG.adoc
@@ -14,7 +14,21 @@
 * Travel planning
 * Offline timetable
 
-== [3.4] – 2024-07-03
+== [3.6] – 2024-08-30
+
+=== Added
+
+* reporting crashes with ACRA
+* use kotlin build scripts and cache
+* use elizabeth dev responses to show inexact times
+* use elizabeth dev responses to show coloured/italics lines in change options
+* use elizabeth dev responses to grey-out or hide terminus arrivals
+
+=== Fixed
+
+* fixed updating conflicting rows for favourites and geonames on older Android versions
+
+== [3.5] – 2024-07-24
 
 === Added
 




diff --git a/README.adoc b/README.adoc
index b98ef0e65cef40da413c6bf0b9a81ea1a0c955c3..d17ae3df924cad086569306e48bdb61c72af03d6 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.5.0 2024-07-24
+v3.6.0 2024-08-30
 :toc:
 
 Bimba is a FLOSS public transport passenger companion; a timetable in your pocket.
@@ -29,15 +29,21 @@ This project uses The Code of Merit, which is available as CODE_OF_CONDUCT file.
 
 Bimba is translated using https://hosted.weblate.org/projects/bimba/[Weblate]
 
-The roadmap is available in `CHANGELOG.adoc` file and—although it’s not set in stone—feature requests are highly discouraged. Contributions, however, are welcome as patches; please send them to `bimba@git.apiote.xyz` using `git send-email`. Patches must include a sign-off to certify agreement to https://developercertificate.org/[Developer Certificate of Origin].
+The roadmap is available in `CHANGELOG.adoc` file and—although it’s not set in stone—feature requests are highly discouraged. Contributions, however, are welcome as patches; please send them to mailto:patches@bimba.app using `git send-email`. Patches must include a sign-off to certify agreement to https://developercertificate.org/[Developer Certificate of Origin].
+
+== Contact
 
-All communication—questions, bugs, etc.—should go through the mailing list available at `bimba@git.apiote.xyz`. Note that all communication will be made public at https://asgard.apiote.xyz/.
+Communication—questions, bugs, etc.—should go through either:
+
+* the mailing list at mailto:questions@bimba.app,
+* the public Matrix channel https://matrix.to/#/#marblearch:apiote.xyz[#marblearch:apiote.xyz],
+* the Mastodon account at https://floss.social/@bimba
 
-This project can be translated using Weblate at https://hosted.weblate.org/projects/bimba/
+Note that all communication may be or will be made public.
 
 == Mirrors
 
-The canonical repository for this project is https://git.apiote.xyz/Bimba.git it’s mirrored at https://notabug.org/apiote/Bimba
+The canonical repository for this project is https://git.apiote.xyz/Bimba.git it’s mirrored at https://notabug.org/apiote/Bimba and https://codeberg.org/apiote/Bimba
 
 Mirrors exist solely for the sake of the code and any additional functions provided by third-party services (including but not limited to issues and pull requests) will not be used and will be ignored.
 
@@ -63,5 +69,4 @@
 === Thanks to…
 
 * https://github.com/tebriz159 for new logo
-
-* https://fonts.google.com/icons[Material Icons], © Google Apache 2.0
+* https://material.io/icons[Material Icons], © Google Apache 2.0




diff --git a/app/build.gradle b/app/build.gradle
deleted file mode 100644
index 3a3a2355b4f3fd3ae0f7a7f826c94e101701ae5f..0000000000000000000000000000000000000000
--- a/app/build.gradle
+++ /dev/null
@@ -1,114 +0,0 @@
-// SPDX-FileCopyrightText: Adam Evyčędo
-//
-// SPDX-License-Identifier: GPL-3.0-or-later
-
-import com.android.tools.profgen.ArtProfileKt
-import com.android.tools.profgen.ArtProfileSerializer
-import com.android.tools.profgen.DexFile
-
-plugins {
-    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'
-    id "com.google.protobuf" version "0.9.4"
-}
-
-android {
-    compileSdk 34  // https://gitlab.com/fdroid/fdroiddata/-/issues/3299#note_1989808414
-
-    defaultConfig {
-        applicationId "xyz.apiote.bimba.czwek"
-        minSdk 21
-        targetSdk 35
-        versionCode 29
-        versionName "3.5.0"
-
-        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
-        resourceConfigurations += ["en", "pl", "it", "de", "fr", "en-rUS"]
-    }
-
-    applicationVariants.configureEach { variant ->
-        variant.resValue "string", "versionName", variant.versionName
-        variant.resValue "string", "applicationId", variant.applicationId
-    }
-
-    buildTypes {
-        release {
-            minifyEnabled false
-            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
-        }
-    }
-    compileOptions {
-        sourceCompatibility = 17
-        targetCompatibility = 17
-        coreLibraryDesugaringEnabled true
-    }
-    buildFeatures {
-        viewBinding true
-    }
-    namespace 'xyz.apiote.bimba.czwek'
-    buildToolsVersion = '34.0.0'  // https://gitlab.com/fdroid/fdroiddata/-/issues/3299#note_1989808414
-}
-
-dependencies {
-    implementation 'androidx.core:core-ktx:1.13.1'
-    implementation 'androidx.appcompat:appcompat:1.7.0'
-    implementation 'com.google.android.material:material:1.12.0'
-    implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
-    implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.8.3'
-    implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.3'
-    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'
-    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.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'
-    implementation 'org.jetbrains.kotlinx:kotlinx-serialization-core:1.6.3'
-    implementation 'com.github.jershell:kbson:0.5.0'
-    implementation 'androidx.preference:preference-ktx:1.2.1'
-    implementation 'androidx.work:work-runtime-ktx:2.9.0'
-    implementation 'com.github.doyaaaaaken:kotlin-csv-jvm:1.9.3'
-    implementation 'commons-io:commons-io:2.16.1'
-
-
-    implementation project(path: ':fruchtfleisch')
-
-    coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.4'
-
-    testImplementation 'junit:junit:4.13.2'
-    androidTestImplementation 'androidx.test.ext:junit:1.2.1'
-    androidTestImplementation 'androidx.test.espresso:espresso-core:3.6.1'
-}
-
-// NOTE fixes reproducible builds
-project.afterEvaluate {
-    tasks.each { task ->
-        if (task.name.startsWith("compile") && task.name.endsWith("ReleaseArtProfile")) {
-            task.doLast {
-                outputs.files.each { file ->
-                    if (file.name.endsWith(".profm")) {
-                        println("Sorting ${file} ...")
-                        def version = ArtProfileSerializer.valueOf("METADATA_0_0_2")
-                        def profile = ArtProfileKt.ArtProfile(file)
-                        def keys = new ArrayList(profile.profileData.keySet())
-                        def sortedData = new LinkedHashMap()
-                        Collections.sort keys, new DexFile.Companion()
-                        keys.each { key -> sortedData[key] = profile.profileData[key] }
-                        new FileOutputStream(file).with {
-                            write(version.magicBytes$profgen)
-                            write(version.versionBytes$profgen)
-                            version.write$profgen(it, sortedData, "")
-                        }
-                    }
-                }
-            }
-        }
-    }
-}
\ No newline at end of file




diff --git a/app/build.gradle.kts b/app/build.gradle.kts
new file mode 100644
index 0000000000000000000000000000000000000000..caa5baee991e29af63d161080acef2f03cca8501
--- /dev/null
+++ b/app/build.gradle.kts
@@ -0,0 +1,85 @@
+// SPDX-FileCopyrightText: Adam Evyčędo
+//
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+plugins {
+	id("com.android.application")
+	kotlin("android")
+	kotlin("plugin.parcelize")
+	kotlin("plugin.serialization")
+}
+
+android {
+	namespace = "xyz.apiote.bimba.czwek"
+	compileSdk = 34  // https://gitlab.com/fdroid/fdroiddata/-/issues/3299#note_1989808414
+	buildToolsVersion =
+		"34.0.0"  // https://gitlab.com/fdroid/fdroiddata/-/issues/3299#note_1989808414
+
+	defaultConfig {
+		applicationId = "xyz.apiote.bimba.czwek"
+		minSdk = 21
+		targetSdk = 35
+		versionCode = 30
+		versionName = "3.6.0"
+
+		testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
+		resourceConfigurations += listOf("en", "pl", "it", "de", "fr", "en-rUS")
+	}
+
+	buildTypes {
+		getByName("release") {
+			isMinifyEnabled = false
+			proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
+		}
+	}
+
+	applicationVariants.configureEach {
+		resValue("string", "versionName", versionName)
+		resValue("string", "applicationId", applicationId)
+	}
+
+	compileOptions {
+		sourceCompatibility = JavaVersion.VERSION_17
+		targetCompatibility = JavaVersion.VERSION_17
+		isCoreLibraryDesugaringEnabled = true
+	}
+
+	buildFeatures {
+		viewBinding = true
+	}
+}
+
+dependencies {
+	implementation("androidx.core:core-ktx:1.13.1")
+	implementation("androidx.appcompat:appcompat:1.7.0")
+	implementation("com.google.android.material:material:1.12.0")
+	implementation("androidx.constraintlayout:constraintlayout:2.1.4")
+	implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.8.4")
+	implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.4")
+	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")
+	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.9.1")
+	implementation("com.otaliastudios:zoomlayout:1.9.0")
+	implementation("dev.bandb.graphview:graphview:0.8.1")
+	implementation("org.jetbrains.kotlinx:kotlinx-serialization-core:1.7.1")
+	implementation("com.github.jershell:kbson:0.5.0")
+	implementation("androidx.preference:preference-ktx:1.2.1")
+	implementation("androidx.work:work-runtime-ktx:2.9.1")
+	implementation("com.github.doyaaaaaken:kotlin-csv-jvm:1.10.0")
+	implementation("commons-io:commons-io:2.16.1")
+	implementation("com.google.guava:guava:31.0.1-android")
+	implementation(project(":fruchtfleisch"))
+	implementation("ch.acra:acra-http:5.11.3")
+	implementation("ch.acra:acra-notification:5.11.3")
+
+	coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.0.4")
+
+	testImplementation("junit:junit:4.13.2")
+	androidTestImplementation("androidx.test.ext:junit:1.2.1")
+	androidTestImplementation("androidx.test.espresso:espresso-core:3.6.1")
+}
\ No newline at end of file




diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro
index 37c7d0b38a0ab9297ade9fd023db967079196459..801f6aa0c4a4c11225ecb6988dbef3b67ce57f2c 100644
--- a/app/proguard-rules.pro
+++ b/app/proguard-rules.pro
@@ -4,7 +4,7 @@ # SPDX-License-Identifier: GPL-3.0-or-later
 
 # Add project specific ProGuard rules here.
 # You can control the set of applied configuration files using the
-# proguardFiles setting in build.gradle.
+# proguardFiles setting in build.gradle.kts.
 #
 # For more details, see
 #   http://developer.android.com/guide/developing/tools/proguard.html




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
index e7e36dc7168480786df2c912ee01962d0bfb3826..bb77730d3df8a610a21d21b72dbb39116f45d245 100644
--- 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
@@ -5,11 +5,11 @@
 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.DepartureV5
 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.LocatableV4
+import xyz.apiote.bimba.czwek.api.QueryableV5
+import xyz.apiote.bimba.czwek.api.StopV3
 import xyz.apiote.bimba.czwek.api.UnknownResourceVersionException
 import xyz.apiote.bimba.czwek.api.VehicleV3
 import xyz.apiote.bimba.czwek.api.structs.FeedInfoV2
@@ -18,13 +18,13 @@ import java.io.InputStream
 
 data class DeparturesResponseDev(
 	val alerts: List<AlertV1>,
-	val departures: List<DepartureV4>,
-	val stop: StopV2
+	val departures: List<DepartureV5>,
+	val stop: StopV3
 ) : DeparturesResponse {
 	companion object {
 		fun unmarshal(stream: InputStream): DeparturesResponseDev {
 			val alerts = mutableListOf<AlertV1>()
-			val departures = mutableListOf<DepartureV4>()
+			val departures = mutableListOf<DepartureV5>()
 
 			val reader = Reader(stream)
 			val alertsNum = reader.readUInt().toULong()
@@ -34,11 +34,11 @@ 				alerts.add(alert)
 			}
 			val departuresNum = reader.readUInt().toULong()
 			for (i in 0UL until departuresNum) {
-				val departure = DepartureV4.unmarshal(stream)
+				val departure = DepartureV5.unmarshal(stream)
 				departures.add(departure)
 			}
 
-			return DeparturesResponseDev(alerts, departures, StopV2.unmarshal(stream))
+			return DeparturesResponseDev(alerts, departures, StopV3.unmarshal(stream))
 		}
 	}
 }
@@ -69,16 +69,16 @@ 		}
 	}
 }
 
-data class LocatablesResponseDev(val locatables: List<LocatableV3>) : LocatablesResponse {
+data class LocatablesResponseDev(val locatables: List<LocatableV4>) : LocatablesResponse {
 	companion object {
 		fun unmarshal(stream: InputStream): LocatablesResponseDev {
-			val locatables = mutableListOf<LocatableV3>()
+			val locatables = mutableListOf<LocatableV4>()
 			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))
+						locatables.add(StopV3.unmarshal(stream))
 					}
 
 					1UL -> {
@@ -96,16 +96,16 @@ 	}
 }
 
 
-data class QueryablesResponseDev(val queryables: List<QueryableV4>) : QueryablesResponse {
+data class QueryablesResponseDev(val queryables: List<QueryableV5>) : QueryablesResponse {
 	companion object {
 		fun unmarshal(stream: InputStream): QueryablesResponseDev {
-			val queryables = mutableListOf<QueryableV4>()
+			val queryables = mutableListOf<QueryableV5>()
 			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))
+						queryables.add(StopV3.unmarshal(stream))
 					}
 
 					1UL -> {




diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index df18dfb966f5fd8c37c0893fa264b05f1a37ef6b..66a021e365d8b0f0506f6a62a283ce3d9d891df6 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -16,7 +16,7 @@ 		android:enableOnBackInvokedCallback="true"
 		android:fullBackupContent="@xml/backup_rules"
 		android:icon="@mipmap/ic_launcher"
 		android:label="@string/app_name"
-		android:localeConfig="@xml/locale_config"
+		android:localeConfig="@xml/locales_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/AboutActivity.kt b/app/src/main/java/xyz/apiote/bimba/czwek/AboutActivity.kt
index cee5e4e154d8391de4925ee64a6afa39e1afce80..5bb8e4a5c9f44221b359c8a56d11ccee050a6b22 100644
--- a/app/src/main/java/xyz/apiote/bimba/czwek/AboutActivity.kt
+++ b/app/src/main/java/xyz/apiote/bimba/czwek/AboutActivity.kt
@@ -4,9 +4,11 @@ // SPDX-License-Identifier: GPL-3.0-or-later
 
 package xyz.apiote.bimba.czwek
 
+import android.content.ActivityNotFoundException
 import android.content.Intent
 import android.net.Uri
 import android.os.Bundle
+import android.widget.Toast
 import androidx.activity.enableEdgeToEdge
 import androidx.appcompat.app.AppCompatActivity
 import androidx.core.view.ViewCompat
@@ -38,11 +40,21 @@ 		binding.mastodon.setOnClickListener {
 			startActivity(Intent(Intent.ACTION_VIEW, Uri.parse("https://floss.social/@bimba")))
 		}
 		binding.website.setOnClickListener {
-			startActivity(Intent(Intent.ACTION_VIEW, Uri.parse("https://bimba.apiote.xyz")))
+			startActivity(Intent(Intent.ACTION_VIEW, Uri.parse("https://bimba.app")))
 		}
 		binding.code.setOnClickListener {
 			startActivity(Intent(Intent.ACTION_VIEW, Uri.parse("https://git.apiote.xyz/Bimba.git")))
 		}
+		binding.email.setOnClickListener {
+			val intent = Intent(Intent.ACTION_SENDTO).apply {
+				setData(Uri.parse("mailto:questions@bimba.app"))
+			}
+			try {
+				startActivity(intent)
+			} catch (_: ActivityNotFoundException) {
+				Toast.makeText(this, getString(R.string.no_email_app), Toast.LENGTH_SHORT).show()
+			}
+		}
 		binding.translate.setOnClickListener {
 			startActivity(
 				Intent(
@@ -50,6 +62,9 @@ 					Intent.ACTION_VIEW,
 					Uri.parse("https://hosted.weblate.org/projects/bimba/")
 				)
 			)
+		}
+		binding.matrix.setOnClickListener {
+			startActivity(Intent(Intent.ACTION_VIEW, Uri.parse("https://matrix.to/#/#marblearch:apiote.xyz")))
 		}
 	}
 }




diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/Bimba.kt b/app/src/main/java/xyz/apiote/bimba/czwek/Bimba.kt
index ac8fd82f922094e41a9dc17ba643694a1833df24..d695be178ef80a558d1d7d8c6f56ad77db757a28 100644
--- a/app/src/main/java/xyz/apiote/bimba/czwek/Bimba.kt
+++ b/app/src/main/java/xyz/apiote/bimba/czwek/Bimba.kt
@@ -4,6 +4,15 @@ // SPDX-License-Identifier: GPL-3.0-or-later
 
 package xyz.apiote.bimba.czwek
 
+import android.content.Context
+import androidx.core.app.NotificationManagerCompat
+import org.acra.BuildConfig
+import org.acra.config.httpSender
+import org.acra.config.notification
+import org.acra.data.StringFormat
+import org.acra.ktx.initAcra
+import org.acra.security.TLS
+import org.acra.sender.HttpSender
 import org.osmdroid.config.Configuration
 import java.io.File
 
@@ -20,5 +29,35 @@ 				config.osmdroidBasePath = File(applicationContext.cacheDir.absolutePath, "osmdroid")
 
 				config.osmdroidTileCache = File(config.osmdroidBasePath.absolutePath, "tile")
 			}
+	}
+
+	override fun attachBaseContext(base: Context) {
+		super.attachBaseContext(base)
+
+		initAcra {
+			buildConfigClass = BuildConfig::class.java
+			reportFormat = StringFormat.JSON
+
+			httpSender {
+				uri = "https://bimba.apiote.xyz/acra/send"
+				httpMethod = HttpSender.Method.POST
+				tlsProtocols = listOf(TLS.V1_3, TLS.V1_2)
+			}
+
+			notification {
+				title = getString(R.string.acra_notification_title)
+				text = getString(R.string.acra_notification_text)
+				channelName = getString(R.string.acra_notification_channel)
+				channelDescription = getString(R.string.acra_notification_channel_description)
+				channelImportance = NotificationManagerCompat.IMPORTANCE_DEFAULT
+				sendButtonText = getString(R.string.send)
+				resSendButtonIcon = R.drawable.send
+				discardButtonText = getString(R.string.discard)
+				resDiscardButtonIcon = R.drawable.discard
+				sendWithCommentButtonText = getString(R.string.send_with_comment)
+				resSendWithCommentButtonIcon = R.drawable.comment
+				commentPrompt = getString(R.string.acra_notification_comment)
+			}
+		}
 	}
 }
\ No newline at end of file




diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/RoundedBackgroundSpan.kt b/app/src/main/java/xyz/apiote/bimba/czwek/RoundedBackgroundSpan.kt
new file mode 100644
index 0000000000000000000000000000000000000000..13a93d42c47db30aa1a50a5bc2fbdfdd2ac8e3ff
--- /dev/null
+++ b/app/src/main/java/xyz/apiote/bimba/czwek/RoundedBackgroundSpan.kt
@@ -0,0 +1,38 @@
+package xyz.apiote.bimba.czwek
+
+import android.graphics.Canvas
+import android.graphics.Paint
+import android.graphics.RectF
+import android.text.style.ReplacementSpan
+
+class RoundedBackgroundSpan(private val bgColour: Int, private val fgColour: Int) : ReplacementSpan() {
+	override fun getSize(
+		paint: Paint,
+		text: CharSequence,
+		start: Int,
+		end: Int,
+		fm: Paint.FontMetricsInt?
+	): Int {
+		return (paint.measureText(text, start, end)+20).toInt()
+	}
+
+	override fun draw(
+		canvas: Canvas,
+		text: CharSequence,
+		start: Int,
+		end: Int,
+		x: Float,
+		top: Int,
+		y: Int,
+		bottom: Int,
+		paint: Paint
+	) {
+		val length = paint.measureText(text, start, end) + 20
+		val rect = RectF(x, top.toFloat() - 5f, x + length, y.toFloat() + 5f)
+		paint.color = bgColour
+		canvas.drawRoundRect(rect, 10f, 10f, paint)
+		paint.color = fgColour
+		paint.textAlign = Paint.Align.CENTER
+		canvas.drawText(text, start, end, x+(length/2), y.toFloat(), paint)
+	}
+}
\ No newline at end of file




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 76953700a4a76e06a41716bcdcc43d07e50e1008..ce68b7ac246599433119ef2f5aef9f8daf9c0cde 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
@@ -32,13 +32,18 @@ 	val feeds: FeedsSettings,
 	val apiPath: String
 ) {
 	companion object {
+		const val DEFAULT = "bimba.apiote.xyz"
+		const val HOST_KEY = "host"
+		const val TOKEN_KEY = "token"
+		const val API_PATH_KEY = "apiPath"
+
 		fun get(context: Context): Server {
 			val preferences = context.getSharedPreferences("shp", MODE_PRIVATE)
-			val apiPath = preferences.getString("apiPath", "")!!
+			val apiPath = preferences.getString(API_PATH_KEY, "")!!
 			val feeds = FeedsSettings.load(context, apiPath)
-			val host = preferences.getString("host", "bimba.apiote.xyz")!!
+			val host = preferences.getString(HOST_KEY, DEFAULT)!!
 			return Server(
-				host, preferences.getString("token", "")!!,
+				host, preferences.getString(TOKEN_KEY, "")!!,
 				feeds, apiPath
 			)
 		}




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 4bfc2f5c505cd365ec5a973bc01a4558856f35c9..ae3a82af89e9bd12e3796f093a0ad2d14362c6a0 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
@@ -8,6 +8,8 @@ interface QueryableV1
 interface QueryableV2
 interface QueryableV3
 interface QueryableV4
+interface QueryableV5
 interface LocatableV1
 interface LocatableV2
-interface LocatableV3
\ No newline at end of file
+interface LocatableV3
+interface LocatableV4
\ 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 9e604c62e01918732d5d03565aa5492cb5989dd5..62d9c8a13101ed601d9234c19ba9b2b8551b6f9b 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
@@ -198,7 +198,8 @@ 		}
 	}
 }
 
-data class ColourV1(val R: UByte, val G: UByte, val B: UByte) {
+@Parcelize
+data class ColourV1(val R: UByte, val G: UByte, val B: UByte): Parcelable {
 	companion object {
 		fun unmarshal(stream: InputStream): ColourV1 {
 			val reader = Reader(stream)
@@ -309,7 +310,7 @@ 	val Line: LineStubV3,
 	val Headsign: String,
 	val CongestionLevel: CongestionLevelV1,
 	val OccupancyStatus: OccupancyStatusV1
-) : LocatableV3 {
+) : LocatableV3, LocatableV4 {
 	companion object {
 		fun unmarshal(stream: InputStream): VehicleV3 {
 			val reader = Reader(stream)
@@ -327,9 +328,10 @@ 		}
 	}
 }
 
+@Parcelize
 data class LineStubV1(
 	val name: String, val kind: LineTypeV1, val colour: ColourV1
-) {
+):Parcelable {
 	companion object {
 		fun unmarshal(stream: InputStream): LineStubV1 {
 			val reader = Reader(stream)
@@ -470,6 +472,76 @@ 		}
 	}
 }
 
+data class DepartureV5(
+	val ID: String,
+	val time: Time,
+	val status: VehicleStatusV1,
+	val isRealtime: Boolean,
+	val vehicle: VehicleV3,
+	val boarding: UByte,
+	val alerts: List<AlertV1>,
+	val exact: Boolean,
+	val terminusArrival: Boolean
+) {
+
+	companion object {
+		fun unmarshal(stream: InputStream): DepartureV5 {
+			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))
+			}
+			val exact = reader.readBoolean()
+			val terminusArrival = reader.readBoolean()
+			return DepartureV5(id, time, status, isRealtime, vehicle, boarding, alerts, exact, terminusArrival)
+		}
+	}
+}
+
+@Parcelize
+data class StopV3(
+	val code: String,
+	val name: String,
+	val nodeName: String,
+	val zone: String,
+	val feedID: String,
+	val position: PositionV1,
+	val changeOptions: List<ChangeOptionV2>
+) : Parcelable, LocatableV4, QueryableV5 {
+	companion object {
+		fun unmarshal(stream: InputStream): StopV3 {
+			val reader = Reader(stream)
+			val code = reader.readString()
+			val name = reader.readString()
+			val nodeName = reader.readString()
+			val zone = reader.readString()
+			val feedID = reader.readString()
+			val position = PositionV1.unmarshal(stream)
+			val chOptionsNum = reader.readUInt().toULong()
+			val changeOptions = mutableListOf<ChangeOptionV2>()
+			for (i in 0UL until chOptionsNum) {
+				changeOptions.add(ChangeOptionV2.unmarshal(stream))
+			}
+			return StopV3(
+				name = name,
+				nodeName = nodeName,
+				code = code,
+				zone = zone,
+				position = position,
+				feedID = feedID,
+				changeOptions = changeOptions
+			)
+		}
+	}
+}
+
 @Parcelize
 data class StopV2(
 	val code: String,
@@ -632,7 +704,7 @@ 	val type: LineTypeV3,
 	val feedID: String,
 	val headsigns: List<List<String>>,
 	val graphs: List<LineGraph>,
-) : QueryableV4 {
+) : QueryableV4, QueryableV5 {
 	override fun toString(): String {
 		return "$name ($type) [$colour]\n${headsigns.map { "-> ${it.joinToString()}" }}"
 	}
@@ -722,6 +794,21 @@ 				9u -> valueOf("FUNICULAR")
 				10u -> valueOf("MONORAIL")
 				else -> throw UnknownResourceVersionException("LineType/$type", 3u)
 			}
+		}
+	}
+}
+
+@Parcelize
+data class ChangeOptionV2(val line: LineStubV1, val headsigns: List<String>) : Parcelable {
+	companion object {
+		fun unmarshal(stream: InputStream): ChangeOptionV2 {
+			val reader = Reader(stream)
+			val line = LineStubV1.unmarshal(stream)
+			val headsignsNum = reader.readUInt().toULong().toInt()
+			val headsigns = (0 until headsignsNum).map {
+				reader.readString()
+			}
+			return ChangeOptionV2(line = line, headsigns = headsigns)
 		}
 	}
 }




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 a176bd2dff780abb44aa27b89af2679233679fe7..3a42262c3955a75ef6245f6b17e8ceb88f03d429 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
@@ -34,7 +34,7 @@
 
 data class LocatablesResponseV3(val locatables: List<LocatableV3>) : LocatablesResponse {
 	companion object {
-		fun unmarshal(stream: InputStream): LocatablesResponseDev {
+		fun unmarshal(stream: InputStream): LocatablesResponseV3 {
 			val locatables = mutableListOf<LocatableV3>()
 			val reader = Reader(stream)
 			val n = reader.readUInt().toULong()
@@ -53,7 +53,7 @@ 						throw UnknownResourceVersionException("Locatable/$r", 0u)
 					}
 				}
 			}
-			return LocatablesResponseDev(locatables)
+			return LocatablesResponseV3(locatables)
 		}
 	}
 }




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 5b7701049711808d7606ecf398e3d60a65f685aa..ac05a4944b6d577160c3793997583c9c7126ccf1 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
@@ -39,7 +39,9 @@ import xyz.apiote.bimba.czwek.dashboard.ui.home.HomeFragment
 import xyz.apiote.bimba.czwek.dashboard.ui.map.MapFragment
 import xyz.apiote.bimba.czwek.dashboard.ui.voyage.VoyageFragment
 import xyz.apiote.bimba.czwek.databinding.ActivityMainBinding
+import xyz.apiote.bimba.czwek.onboarding.FirstRunActivity
 import xyz.apiote.bimba.czwek.search.ResultsActivity
+import xyz.apiote.bimba.czwek.settings.DownloadCitiesWorker
 import xyz.apiote.bimba.czwek.settings.ServerChooserActivity
 import xyz.apiote.bimba.czwek.settings.SettingsActivity
 import xyz.apiote.bimba.czwek.settings.feeds.FeedChooserActivity
@@ -58,9 +60,7 @@ 		super.onCreate(savedInstanceState)
 		binding = ActivityMainBinding.inflate(layoutInflater)
 		setContentView(binding.root)
 
-		getSharedPreferences("shp", MODE_PRIVATE).edit(true) {
-			putBoolean("firstRun", false)
-		}
+		FirstRunActivity.setFirstRunDone(this)
 
 		ViewCompat.setOnApplyWindowInsetsListener(binding.root) { _, windowInsets ->
 			val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
@@ -166,12 +166,19 @@ 			}
 		}
 
 		if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+			val notificationPermissionAsked =
+				PreferenceManager.getDefaultSharedPreferences(this).getBoolean(
+					NOTIFICATION_PERMISSION_ASKED, false
+				)
 			if (ActivityCompat.checkSelfPermission(
 					this,
 					Manifest.permission.POST_NOTIFICATIONS
-				) != PackageManager.PERMISSION_GRANTED && shouldShowRequestPermissionRationale(Manifest.permission.POST_NOTIFICATIONS)
+				) != PackageManager.PERMISSION_GRANTED && !notificationPermissionAsked
 			) {
 				requestPermissions(arrayOf(Manifest.permission.POST_NOTIFICATIONS), 1)
+				PreferenceManager.getDefaultSharedPreferences(this).edit {
+					putBoolean(NOTIFICATION_PERMISSION_ASKED, true)
+				}
 			}
 		}
 	}
@@ -227,10 +234,10 @@ 				text.toString().trim().split(" ").first().trim(',').trim()
 			)
 		) {
 			if (PreferenceManager.getDefaultSharedPreferences(applicationContext)
-					.getLong("cities_last_update", -1) < 0
+					.getLong(DownloadCitiesWorker.LAST_UPDATE_KEY, -1) < 0
 			) {
 				if (!PreferenceManager.getDefaultSharedPreferences(applicationContext)
-						.getBoolean("no_geocoding_data_shown", false)
+						.getBoolean(NO_GEOCODING_DATA_SHOWN, false)
 				) {
 					MaterialAlertDialogBuilder(this)
 						.setIcon(R.drawable.geocoding)
@@ -243,6 +250,9 @@ 								text.toString()
 							)
 						}
 						.show()
+					PreferenceManager.getDefaultSharedPreferences(applicationContext).edit{
+						putBoolean(NO_GEOCODING_DATA_SHOWN, true)
+					}
 				}
 			} else {
 				showResults(ResultsActivity.Mode.MODE_SHORT_CODE, text.toString())
@@ -257,13 +267,7 @@ 		/* todo [3.2] (ux,low) animation
 			https://developer.android.com/guide/fragments/animate
 			https://github.com/raheemadamboev/fab-explosion-animation-app
 		*/
-		val intent = Intent(this, ResultsActivity::class.java).apply {
-			putExtra("mode", ResultsActivity.Mode.MODE_POSITION)
-			putExtra("query", query)
-			putExtra("lat", centerLatitude)
-			putExtra("lon", centerLongitude)
-		}
-		startActivity(intent)
+		startActivity(ResultsActivity.getIntent(this, ResultsActivity.Mode.MODE_POSITION, query, centerLatitude, centerLongitude))
 	}
 
 	private fun showResults(mode: ResultsActivity.Mode, query: String = "") {
@@ -271,11 +275,7 @@ 		/* todo [3.2] (ux,low) animation
 			https://developer.android.com/guide/fragments/animate
 			https://github.com/raheemadamboev/fab-explosion-animation-app
 		*/
-		val intent = Intent(this, ResultsActivity::class.java).apply {
-			putExtra("mode", mode)
-			putExtra("query", query)
-		}
-		startActivity(intent)
+		startActivity(ResultsActivity.getIntent(this, mode, query))
 	}
 
 	private fun setNavbarIcons(f: Fragment) {
@@ -300,5 +300,10 @@ 			else -> {
 				binding.bottomNavigation.menu[1].setIcon(R.drawable.home_black)
 			}
 		}
+	}
+
+	companion object {
+		const val NOTIFICATION_PERMISSION_ASKED = "notificationPermissionAsked"
+		const val NO_GEOCODING_DATA_SHOWN = "no_geocoding_data_shown"
 	}
 }
\ No newline at end of file




diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/dashboard/ui/home/Favourites.kt b/app/src/main/java/xyz/apiote/bimba/czwek/dashboard/ui/home/Favourites.kt
index 711016241320500c160eac7dd14196cd1b974411..2b1d50a476c8181b6758489d60b61fe772080d09 100644
--- a/app/src/main/java/xyz/apiote/bimba/czwek/dashboard/ui/home/Favourites.kt
+++ b/app/src/main/java/xyz/apiote/bimba/czwek/dashboard/ui/home/Favourites.kt
@@ -5,7 +5,6 @@
 package xyz.apiote.bimba.czwek.dashboard.ui.home
 
 import android.content.Context
-import android.content.Intent
 import android.view.LayoutInflater
 import android.view.View
 import android.view.ViewGroup
@@ -200,13 +199,7 @@ 					)
 			}
 
 			holder.root.setOnClickListener {
-				val intent = Intent(context, DeparturesActivity::class.java).apply {
-					putExtra("code", favourite.stopCode)
-					putExtra("name", favourite.stopName)
-					putExtra("feedID", favourite.feedID)
-					putExtra("linesFilter", favourite.lines.toTypedArray())
-				}
-				context.startActivity(intent)
+				context.startActivity(DeparturesActivity.getIntent(context, favourite.stopCode, favourite.stopName, favourite.feedID, favourite.lines.toTypedArray()))
 			}
 		}
 	}




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 42dcc1a330de20dc80aa99bab9e5755931c00351..65339fd2b8ef9eed82359fd09bd00e3bea0bce44 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
@@ -57,6 +57,13 @@
 
 class MapFragment : Fragment() {
 
+	companion object {
+		const val PREFERENCES_NAME = "shp"
+		const val ZOOM_KEY = "mapZoom"
+		const val CENTRE_LATITUDE_KEY = "mapCentreLat"
+		const val CENTRE_LONGITUDE_KEY = "mapCentreLon"
+	}
+
 	private var maybeBinding: FragmentMapBinding? = null
 	private val binding get() = maybeBinding!!
 
@@ -96,7 +103,7 @@ 		binding.map.overlays.add(RotationGestureOverlay(binding.map).apply { isEnabled = true })
 
 		locationOverlay = MyLocationNewOverlay(GpsMyLocationProvider(context), binding.map)
 		context?.let {
-			centreMap(it.getSharedPreferences("shp", MODE_PRIVATE))
+			centreMap(it.getSharedPreferences(PREFERENCES_NAME, MODE_PRIVATE))
 
 			locationOverlay.setDirectionIcon(
 				AppCompatResources.getDrawable(it, R.drawable.navigation_arrow)?.mutate()
@@ -250,10 +257,10 @@ 	}
 
 	private fun centreMap(preferences: SharedPreferences) {
 		maybeBinding?.map?.controller?.apply {
-			setZoom(preferences.getFloat("mapZoom", 17.0f).toDouble())
+			setZoom(preferences.getFloat(ZOOM_KEY, 17.0f).toDouble())
 			val startPoint = GeoPoint(
-				preferences.getFloat("mapCentreLat", 52.39511f).toDouble(),
-				preferences.getFloat("mapCentreLon", 16.89506f).toDouble()
+				preferences.getFloat(CENTRE_LATITUDE_KEY, 52.39511f).toDouble(),
+				preferences.getFloat(CENTRE_LONGITUDE_KEY, 16.89506f).toDouble()
 			)
 			setCenter(startPoint)
 		}
@@ -264,7 +271,7 @@ 		super.onResume()
 		binding.map.onResume()
 		locationOverlay.enableMyLocation()
 		context?.let { ctx ->
-			ctx.getSharedPreferences("shp", MODE_PRIVATE).let {
+			ctx.getSharedPreferences(PREFERENCES_NAME, MODE_PRIVATE).let {
 				Configuration.getInstance()
 					.load(ctx, it)
 				centreMap(it)
@@ -278,10 +285,10 @@ 		binding.map.onPause()
 		locationOverlay.disableMyLocation()
 		val centre = binding.map.mapCenter
 		context?.let { ctx ->
-			ctx.getSharedPreferences("shp", MODE_PRIVATE).edit(true) {
-				putFloat("mapCentreLat", centre.latitude.toFloat())
-				putFloat("mapCentreLon", centre.longitude.toFloat())
-				putFloat("mapZoom", binding.map.zoomLevelDouble.toFloat())
+			ctx.getSharedPreferences(PREFERENCES_NAME, MODE_PRIVATE).edit(true) {
+				putFloat(CENTRE_LATITUDE_KEY, centre.latitude.toFloat())
+				putFloat(CENTRE_LONGITUDE_KEY, centre.longitude.toFloat())
+				putFloat(ZOOM_KEY, binding.map.zoomLevelDouble.toFloat())
 			}
 		}
 		handler.removeCallbacks(workRunnable)




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 324169dea1b496d9daa3162b5c04dc6281fd1909..29ab95545f1d7ad0c79246c6a85e095b5f023af5 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
@@ -81,7 +81,6 @@ 			content.findViewById(R.id.rt_icon).visibility = View.GONE
 			// TODO vehicle accessible
 			content.findViewById<ImageView>(R.id.wheelchair_icon).visibility = View.GONE
 
-			Log.i("unit", "${vehicle.Speed.mps}")
 			UnitSystem.getSelected(requireContext()).let { us ->
 				content.findViewById<TextView>(R.id.speed_text).apply {
 					text =
@@ -168,12 +167,7 @@ 	private fun showStop(content: View, stop: Stop) {
 		context?.let { ctx ->
 			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)
-					putExtra("name", stop.name)
-					putExtra("feedID", stop.feedID)
-				}
-				startActivity(intent)
+				startActivity(DeparturesActivity.getIntent(requireContext(), stop.code, stop.name, stop.feedID!!))
 			}
 			content.findViewById<Button>(R.id.navigation_button).setOnClickListener {
 				try {
@@ -188,7 +182,7 @@ 					Toast.makeText(context, ctx.getString(R.string.no_map_app), Toast.LENGTH_SHORT).show()
 				}
 			}
 
-			stop.changeOptions(ctx).let { changeOptions ->
+			stop.changeOptions(ctx, Stop.LineDecoration.NONE).let { changeOptions ->
 				content.findViewById<TextView>(R.id.change_options).apply {
 					text = changeOptions.first
 					contentDescription = changeOptions.second




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 bd20eaf7e474954a96ea993491dddebf413e1fb9..5f50f5add8aba3a517babe6d39eb0166eef25257 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
@@ -19,6 +19,7 @@ import android.widget.ImageView
 import android.widget.LinearLayout
 import android.widget.TextView
 import androidx.appcompat.widget.TooltipCompat
+import androidx.preference.PreferenceManager
 import androidx.recyclerview.widget.DiffUtil
 import androidx.recyclerview.widget.RecyclerView
 import androidx.recyclerview.widget.RecyclerView.ViewHolder
@@ -51,6 +52,7 @@ 	val lineIcon: ImageView = itemView.findViewById(R.id.line_icon)
 	val departureTime: TextView = itemView.findViewById(R.id.departure_time)
 	val lineName: TextView = itemView.findViewById(R.id.departure_line)
 	val headsign: TextView = itemView.findViewById(R.id.departure_headsign)
+	val timeStatus: ImageView = itemView.findViewById(R.id.time_status)
 
 	companion object {
 		fun bind(
@@ -58,7 +60,8 @@ 			departure: Departure,
 			holder: BimbaDepartureViewHolder?,
 			context: Context?,
 			showAsTime: Boolean,
-			onClickListener: (Departure) -> Unit
+			onClickListener: (Departure) -> Unit,
+			showingTerminusArrivals: String
 		) {
 			holder?.root?.setOnClickListener {
 				onClickListener(departure)
@@ -74,7 +77,51 @@ 					R.string.departure_headsign_content_description,
 					departure.vehicle.Headsign
 				)
 
+			when {
+				departure.isRealtime -> {
+					holder?.timeStatus?.setImageResource(R.drawable.radar)
+					holder?.timeStatus?.contentDescription =
+						context?.getString(R.string.realtime_content_description)
+					holder?.timeStatus?.let {
+						TooltipCompat.setTooltipText(
+							it,
+							context?.getString(R.string.realtime_content_description)
+						)
+					}
+				}
+
+				departure.exact -> {
+					holder?.timeStatus?.setImageResource(R.drawable.calendar)
+					holder?.timeStatus?.contentDescription =
+						context?.getString(R.string.exact_content_description)
+					holder?.timeStatus?.let {
+						TooltipCompat.setTooltipText(
+							it,
+							context?.getString(R.string.exact_content_description)
+						)
+					}
+				}
+
+				else -> {
+					holder?.timeStatus?.setImageResource(R.drawable.inexact)
+					holder?.timeStatus?.contentDescription =
+						context?.getString(R.string.inexact_content_description)
+					holder?.timeStatus?.let {
+						TooltipCompat.setTooltipText(
+							it,
+							context?.getString(R.string.inexact_content_description)
+						)
+					}
+				}
+			}
+
 			holder?.departureTime?.text = departure.statusText(context, showAsTime)
+			holder?.root?.alpha =
+				if (departure.terminusArrival && showingTerminusArrivals == BimbaDeparturesAdapter.TERMINUS_ARRIVAL_GREY_OUT) {
+					.5f
+				} else {
+					1f
+				}
 		}
 	}
 }
@@ -94,7 +141,7 @@ 			val alertDescriptions = alerts.map { it.description }.filter { it != "" }
 				.joinToString(separator = "\n")
 			holder?.moreButton?.setOnClickListener {
 				MaterialAlertDialogBuilder(context!!)
-					.setTitle("Alerts")
+					.setTitle(R.string.alerts)
 					.setPositiveButton(R.string.ok) { _, _ -> }
 					.setMessage(alertDescriptions)
 					.show()
@@ -113,9 +160,27 @@ class BimbaDeparturesAdapter(
 	private val inflater: LayoutInflater,
 	private val context: Context?,
 	private var items: List<DepartureItem>,
-	private val onClickListener: ((Departure) -> Unit)
+	private val onClickListener: ((Departure) -> Unit),
 ) :
 	RecyclerView.Adapter<ViewHolder>() {
+
+	companion object {
+		const val ALERT_ITEM_ID = "alert"
+
+		// TODO to enum
+		const val TERMINUS_ARRIVAL_SHOWING_KEY = "terminus_arrival_showing"
+		const val TERMINUS_ARRIVAL_GREY_OUT = "grey_out"
+		const val TERMINUS_ARRIVAL_HIDE = "hide"
+		const val TERMINUS_ARRIVAL_SHOW = "show"
+	}
+
+	var showingTerminusArrivals: String = context?.let {
+		PreferenceManager.getDefaultSharedPreferences(
+			it
+		).getString(TERMINUS_ARRIVAL_SHOWING_KEY, TERMINUS_ARRIVAL_GREY_OUT)
+	}
+	?: TERMINUS_ARRIVAL_GREY_OUT
+
 	var lastUpdate: ZonedDateTime =
 		ZonedDateTime.of(0, 1, 1, 0, 0, 0, 0, ZoneId.systemDefault())
 		private set
@@ -132,13 +197,16 @@ 		override fun getNewListSize() = newDepartures.size
 
 		override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int) =
 			(oldDepartures[oldItemPosition].departure?.ID
-				?: "alert") == (newDepartures[newItemPosition].departure?.ID ?: "alert")
+				?: ALERT_ITEM_ID) == (newDepartures[newItemPosition].departure?.ID ?: ALERT_ITEM_ID)
 
 		override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
 			val oldDeparture = oldDepartures[oldItemPosition]
 			val newDeparture = newDepartures[newItemPosition]
 			return if (oldDeparture.departure != null && newDeparture.departure != null) {
-				oldDeparture.departure.vehicle.Line == newDeparture.departure.vehicle.Line &&
+				!oldDeparture.departure.terminusArrival &&
+				oldDeparture.departure.terminusArrival == newDeparture.departure.terminusArrival &&
+					oldDeparture.departure.exact == newDeparture.departure.exact &&
+					oldDeparture.departure.vehicle.Line == newDeparture.departure.vehicle.Line &&
 					oldDeparture.departure.vehicle.Headsign == newDeparture.departure.vehicle.Headsign &&
 					oldDeparture.departure.statusText(
 						context,
@@ -157,7 +225,7 @@ 	private var departuresPositions: MutableMap = HashMap()
 
 	init {
 		items.forEachIndexed { i, departure ->
-			departuresPositions[departure.departure?.ID ?: "alert"] = i
+			departuresPositions[departure.departure?.ID ?: ALERT_ITEM_ID] = i
 		}
 	}
 
@@ -186,7 +254,8 @@ 				items[position].departure!!,
 				holder,
 				context,
 				showAsTime,
-				onClickListener
+				onClickListener,
+				showingTerminusArrivals
 			)
 		} else {
 			BimbaAlertViewHolder.bind(items[position].alert, holder as BimbaAlertViewHolder, context)
@@ -217,7 +286,7 @@ 			departures
 		}
 		val newPositions: MutableMap<String, Int> = HashMap()
 		newDepartures.forEachIndexed { i, departure ->
-			newPositions[departure.departure?.ID ?: "alert"] = i
+			newPositions[departure.departure?.ID ?: ALERT_ITEM_ID] = i
 		}
 		val diff = DiffUtil.calculateDiff(
 			DiffUtilCallback(
@@ -391,7 +460,7 @@ 				findViewById(R.id.alerts).apply {
 					visibility = View.VISIBLE
 					setOnClickListener {
 						MaterialAlertDialogBuilder(context)
-							.setTitle("Alerts")
+							.setTitle(R.string.alerts)
 							.setPositiveButton(R.string.ok) { _, _ -> }
 							.setMessage(departure.alerts.map { it.description }.filter { it != "" }
 								.joinToString(separator = "\n"))




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 1164836fb36f512ff578e26a75e9c1ac04013cac..0b725721dcfca87b8b52eaa06e4407b36784c410 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
@@ -4,6 +4,7 @@ // SPDX-License-Identifier: GPL-3.0-or-later
 
 package xyz.apiote.bimba.czwek.departures
 
+import android.content.Context
 import android.content.Intent
 import android.net.ConnectivityManager
 import android.net.ConnectivityManager.NetworkCallback
@@ -26,6 +27,7 @@ import androidx.core.view.WindowCompat
 import androidx.core.view.WindowInsetsCompat
 import androidx.core.view.updatePadding
 import androidx.lifecycle.ViewModelProvider
+import androidx.preference.PreferenceManager
 import androidx.recyclerview.widget.LinearLayoutManager
 import androidx.recyclerview.widget.RecyclerView
 import com.google.android.material.datepicker.MaterialDatePicker
@@ -40,6 +42,9 @@ import xyz.apiote.bimba.czwek.R
 import xyz.apiote.bimba.czwek.api.Error
 import xyz.apiote.bimba.czwek.api.Server
 import xyz.apiote.bimba.czwek.databinding.ActivityDeparturesBinding
+import xyz.apiote.bimba.czwek.departures.BimbaDeparturesAdapter.Companion.TERMINUS_ARRIVAL_GREY_OUT
+import xyz.apiote.bimba.czwek.departures.BimbaDeparturesAdapter.Companion.TERMINUS_ARRIVAL_HIDE
+import xyz.apiote.bimba.czwek.departures.BimbaDeparturesAdapter.Companion.TERMINUS_ARRIVAL_SHOWING_KEY
 import xyz.apiote.bimba.czwek.repo.DepartureItem
 import xyz.apiote.bimba.czwek.repo.Favourite
 import xyz.apiote.bimba.czwek.repo.OfflineRepository
@@ -52,6 +57,38 @@ import java.time.ZoneId
 import java.time.ZonedDateTime
 
 class DeparturesActivity : AppCompatActivity() {
+	companion object {
+		const val CODE_PARAM = "code"
+		const val NAME_PARAM = "name"
+		const val FEED_PARAM = "feedID"
+		const val LINES_FILTER_PARAM = "linesFilter"
+		const val LINE_PARAM = "line"
+
+		fun getIntent(
+			context: Context,
+			code: String,
+			name: String,
+			feedID: String,
+		) = Intent(context, DeparturesActivity::class.java).apply {
+			putExtra(CODE_PARAM, code)
+			putExtra(NAME_PARAM, name)
+			putExtra(FEED_PARAM, feedID)
+		}
+
+		fun getIntent(
+			context: Context,
+			code: String,
+			name: String,
+			feedID: String,
+			lines: Array<String>
+		) = Intent(context, DeparturesActivity::class.java).apply {
+			putExtra(CODE_PARAM, code)
+			putExtra(NAME_PARAM, name)
+			putExtra(FEED_PARAM, feedID)
+			putExtra(LINES_FILTER_PARAM, lines)
+		}
+	}
+
 	private var _binding: ActivityDeparturesBinding? = null
 	private val binding get() = _binding!!
 
@@ -74,7 +111,7 @@ 	// TODO [elizabeth] millisInFuture from header Cache-Control max-age
 	private val countdown =
 		object : CountDownTimer(Second(30).milliseconds(), Tim(1).milliseconds()) {
 			override fun onTick(millisUntilFinished: Long) {
-				val timsUntillFinished = Tim(Second(millisUntilFinished.toDouble()/1000))
+				val timsUntillFinished = Tim(Second(millisUntilFinished.toDouble() / 1000))
 				binding.departuresUpdatesProgress.progress = timsUntillFinished.tims.toInt()
 			}
 
@@ -103,6 +140,9 @@ 			windowInsets
 		}
 
 		viewModel = ViewModelProvider(this)[DeparturesViewModel::class.java]
+		viewModel.showingTerminusArrivals = PreferenceManager.getDefaultSharedPreferences(this)
+			.getString(TERMINUS_ARRIVAL_SHOWING_KEY, TERMINUS_ARRIVAL_GREY_OUT)
+			?: TERMINUS_ARRIVAL_GREY_OUT
 
 		getLine()?.let {
 			viewModel.mutableLinesFilter.value = mapOf(Pair(it, true))
@@ -134,6 +174,9 @@ 				.filter { d ->
 					it.values.all { !it } or (it[d.vehicle.Line.name] ?: false)
 				}
 				.filter { d ->
+					viewModel.showingTerminusArrivals != TERMINUS_ARRIVAL_HIDE || !d.terminusArrival
+				}
+				.filter { d ->
 					val t = LocalTime.of(d.time.Hour.toInt(), d.time.Minute.toInt())
 					t >= viewModel.startTime && t <= viewModel.endTime
 				}.map { DepartureItem(it) },
@@ -152,6 +195,9 @@ 				.filter { d ->
 					viewModel.linesFilter.value?.let { filter ->
 						filter.values.all { !it } or (filter[d.vehicle.Line.name] ?: false)
 					} ?: true
+				}
+				.filter { d ->
+					viewModel.showingTerminusArrivals != TERMINUS_ARRIVAL_HIDE || !d.terminusArrival
 				}
 				.filter { d ->
 					val t = LocalTime.of(d.time.Hour.toInt(), d.time.Minute.toInt())
@@ -231,7 +277,7 @@ 								this,
 								R.drawable.filter
 							)
 						)
-							.setTitle("Filtered departures")
+							.setTitle(R.string.filtered_departures)
 							.setMessage(R.string.filtered_stop_question)
 							.setPositiveButton(R.string.filtered) { _, _ ->
 								saveFavourite(viewModel.linesFilter.value!!.keys)
@@ -295,11 +341,40 @@ 					timePickerStart!!.show(supportFragmentManager, "timePickerStart")
 					true
 				}
 
+				/* TODO elizabeth
+				R.id.terminus_arrival_showing -> {
+					// TODO get array from R.arrays
+					val options = arrayOf(
+						TERMINUS_ARRIVAL_GREY_OUT,
+						TERMINUS_ARRIVAL_HIDE,
+						BimbaDeparturesAdapter.TERMINUS_ARRIVAL_SHOW
+					)
+					var selected = viewModel.showingTerminusArrivals!!
+					MaterialAlertDialogBuilder(this)
+						.setTitle(R.string.terminus_arrival_showing)
+						.setIcon(R.drawable.terminus)
+						.setSingleChoiceItems(
+							options,
+							options.indexOf(viewModel.showingTerminusArrivals)
+						) { _, i ->
+							selected = options[i]
+						}
+						.setPositiveButton(R.string.ok) { _, _ ->
+							viewModel.showingTerminusArrivals = selected
+							adapter.showingTerminusArrivals = selected
+							getDepartures()
+						}
+						.setNegativeButton(R.string.cancel) { _, _ -> }
+						.show()
+					true
+				}*/
+
 				else -> super.onOptionsItemSelected(it)
 			}
 		}
 
 		binding.departuresRecycler.layoutManager = LinearLayoutManager(this)
+		binding.departuresRecycler.itemAnimator = null
 		binding.departuresRecycler.addOnScrollListener(
 			object : RecyclerView.OnScrollListener() {
 				override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
@@ -322,6 +397,7 @@ 				viewModel.openBottomSheet = this
 				setOnCancel { viewModel.openBottomSheet = null }
 			}
 		}
+		adapter.showingTerminusArrivals = viewModel.showingTerminusArrivals!!
 		binding.departuresRecycler.adapter = adapter
 		WindowCompat.setDecorFitsSystemWindows(window, false)
 
@@ -367,26 +443,26 @@
 	private fun getName(): String {
 		return when (intent?.action) {
 			Intent.ACTION_VIEW -> getString(R.string.stop_from_qr_code)
-			null -> intent?.extras?.getString("name") ?: ""
+			null -> intent?.extras?.getString(NAME_PARAM) ?: ""
 			else -> ""
 		}
 	}
 
 	private fun getLine(): String? {
 		return when (intent?.action) {
-			null -> intent?.extras?.getString("line")
+			null -> intent?.extras?.getString(LINE_PARAM)
 			else -> null
 		}
 	}
 
 	private fun getLines(): List<String>? {
 		return when (intent?.action) {
-			null -> intent?.extras?.getStringArray("linesFilter")?.toList()
+			null -> intent?.extras?.getStringArray(LINES_FILTER_PARAM)?.toList()
 			else -> null
 		}
 	}
 
-	private fun getCode() = intent?.extras?.getString("code")
+	private fun getCode() = intent?.extras?.getString(CODE_PARAM)
 
 	fun getDepartures(force: Boolean = false) {
 		binding.departuresUpdatesProgress.isIndeterminate = true
@@ -447,7 +523,9 @@ 		stop: Stop?,
 		leaveAlert: Boolean = false
 	) {
 		setupSnackbar()
-		binding.departuresRecycler.scrollToPosition(0)
+		if (adapter.itemCount == 0) {
+			binding.departuresRecycler.scrollToPosition(0)
+		}
 		binding.departuresProgress.visibility = View.GONE
 		// TODO [elizabeth] max, progress from header Cache-Control max-age
 		binding.departuresUpdatesProgress.apply {
@@ -487,8 +565,8 @@ 	}
 
 	private fun saveFavourite(linesFilter: Set<String>) {
 		val context = this
-		val feedID = intent.extras?.getString("feedID")
-		val code = intent?.extras?.getString("code")
+		val feedID = intent.extras?.getString(FEED_PARAM)
+		val code = intent?.extras?.getString(CODE_PARAM)
 		if (feedID == null || code == null) {
 			Toast.makeText(this, R.string.cannot_save_favourite, Toast.LENGTH_LONG).show()
 			return




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 9d3b81505bb8907710dcf14bb6d543a1c5c1e3bf..bd61194560e6a232a5754bbf33fa60b3ed03dc6f 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
@@ -37,6 +37,7 @@ 	var openBottomSheet: DepartureBottomSheet? = null
 	private lateinit var code: String
 	val mutableLinesFilter = MutableLiveData<Map<String, Boolean>>()
 	val linesFilter: LiveData<Map<String, Boolean>> = mutableLinesFilter
+	var showingTerminusArrivals: String? = null
 
 	// TODO observe in activity, maybe refreshing and not getting departures is enough
 	var startTime: LocalTime = LocalTime.MIN
@@ -104,7 +105,7 @@ 				}
 			}
 
 			null -> {
-				val feedID = intent.extras?.getString("feedID")
+				val feedID = intent.extras?.getString(DeparturesActivity.FEED_PARAM)
 				feeds?.get(feedID) ?: throw TrafficResponseException(41)
 			}
 
@@ -145,7 +146,7 @@ 					}
 				} ?: throw TrafficResponseException(41)
 			}
 
-			null -> intent?.extras?.getString("code") ?: throw TrafficResponseException(41)
+			null -> intent?.extras?.getString(DeparturesActivity.CODE_PARAM) ?: throw TrafficResponseException(41)
 			else -> throw TrafficResponseException(41)
 		}
 	}




diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/onboarding/FirstRunActivity.kt b/app/src/main/java/xyz/apiote/bimba/czwek/onboarding/FirstRunActivity.kt
index 117278a8722bd5277e13416aeb8abdeea2ea8c24..e7828395a0b090e4ce33797eb61fa5fc2c789f1d 100644
--- a/app/src/main/java/xyz/apiote/bimba/czwek/onboarding/FirstRunActivity.kt
+++ b/app/src/main/java/xyz/apiote/bimba/czwek/onboarding/FirstRunActivity.kt
@@ -6,12 +6,13 @@ package xyz.apiote.bimba.czwek.onboarding
 
 import android.app.NotificationChannel
 import android.app.NotificationManager
+import android.content.Context
 import android.content.Intent
 import android.os.Build
 import android.os.Bundle
 import androidx.appcompat.app.AppCompatActivity
+import androidx.core.content.edit
 import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
-import androidx.preference.PreferenceManager
 import androidx.work.OneTimeWorkRequest
 import androidx.work.WorkManager
 import xyz.apiote.bimba.czwek.R
@@ -19,34 +20,22 @@ import xyz.apiote.bimba.czwek.dashboard.MainActivity
 import xyz.apiote.bimba.czwek.repo.migrateDB
 import xyz.apiote.bimba.czwek.settings.DownloadCitiesWorker
 import xyz.apiote.bimba.czwek.settings.feeds.migrateFeedsSettings
-import java.time.Instant
-import java.time.temporal.ChronoUnit
 
 class FirstRunActivity : AppCompatActivity() {
 	override fun onCreate(savedInstanceState: Bundle?) {
 		installSplashScreen()
 		super.onCreate(savedInstanceState)
 
-		val preferences = getSharedPreferences("shp", MODE_PRIVATE)
-
 		migrateFeedsSettings(this)
 		migrateDB(this)
 		createNotificationChannels()
 
-		val (updatesEnabled, weekPassed) = PreferenceManager.getDefaultSharedPreferences(this).let {
-			arrayOf(
-				it.getBoolean("autoupdate_cities_list", false),
-				Instant.ofEpochSecond(it.getLong("cities_last_update", 0)).plus(7, ChronoUnit.DAYS)
-					.isBefore(Instant.now())
-			)
-		}
-
-		if (updatesEnabled && weekPassed) {
+		if (DownloadCitiesWorker.shouldUpdate(this)) {
 			WorkManager.getInstance(this)
 				.enqueue(OneTimeWorkRequest.from(DownloadCitiesWorker::class.java))
 		}
 
-		val intent = if (preferences.getBoolean("firstRun", true)) {
+		val intent = if (getFirstRun(this)) {
 			Intent(this, OnboardingActivity::class.java)
 		} else {
 			Intent(this, MainActivity::class.java)
@@ -60,12 +49,27 @@ 		if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
 			val name = getString(R.string.cities_channel_name)
 			val descriptionText = getString(R.string.cities_channel_description)
 			val importance = NotificationManager.IMPORTANCE_LOW
-			val channel = NotificationChannel("cities_channel", name, importance).apply {
+			val channel = NotificationChannel(DownloadCitiesWorker.NOTIFICATION_CHANNEL, name, importance).apply {
 				description = descriptionText
 			}
 			val notificationManager: NotificationManager =
 				getSystemService(NOTIFICATION_SERVICE) as NotificationManager
 			notificationManager.createNotificationChannel(channel)
+		}
+	}
+
+	companion object {
+		private const val PREFERENCES_NAME = "shp"
+		private const val FIRST_RUN_KEY = "firstRun"
+
+		fun setFirstRunDone(context: Context) {
+			context.getSharedPreferences(PREFERENCES_NAME, MODE_PRIVATE).edit {
+				putBoolean(FIRST_RUN_KEY, false)
+			}
+		}
+
+		fun getFirstRun(context: Context): Boolean {
+			return context.getSharedPreferences(PREFERENCES_NAME, MODE_PRIVATE).getBoolean(FIRST_RUN_KEY, true)
 		}
 	}
 }
\ No newline at end of file




diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/onboarding/OnboardingActivity.kt b/app/src/main/java/xyz/apiote/bimba/czwek/onboarding/OnboardingActivity.kt
index 7833875ef94ce6fe166c7f4536871d0827d49e19..f5df578827b6c4cc1cdb8844307a9ca4fa94b4dc 100644
--- a/app/src/main/java/xyz/apiote/bimba/czwek/onboarding/OnboardingActivity.kt
+++ b/app/src/main/java/xyz/apiote/bimba/czwek/onboarding/OnboardingActivity.kt
@@ -4,7 +4,6 @@ // SPDX-License-Identifier: GPL-3.0-or-later
 
 package xyz.apiote.bimba.czwek.onboarding
 
-import android.content.Intent
 import android.graphics.Typeface
 import android.os.Bundle
 import android.text.Spannable
@@ -28,7 +27,7 @@ 	private val binding get() = _binding!!
 
 	private val activityLauncher =
 		registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
-			if (!getSharedPreferences("shp", MODE_PRIVATE).getBoolean("firstRun", true)) {
+			if (!FirstRunActivity.getFirstRun(this)) {
 				finish()
 			}
 		}
@@ -79,9 +78,6 @@ 		}
 	}
 
 	private fun moveOn(simple: Boolean) {
-		val intent = Intent(this, ServerChooserActivity::class.java).apply {
-			putExtra("simple", simple)
-		}
-		activityLauncher.launch(intent)
+		activityLauncher.launch(ServerChooserActivity.getIntent(this, simple))
 	}
 }
\ No newline at end of file




diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/repo/ChangeOption.kt b/app/src/main/java/xyz/apiote/bimba/czwek/repo/ChangeOption.kt
index b41f089279301f16959982baf3d256b52c5512ad..110e85d8f3bea00613dcb3c89ed024dac1a4f4f4 100644
--- a/app/src/main/java/xyz/apiote/bimba/czwek/repo/ChangeOption.kt
+++ b/app/src/main/java/xyz/apiote/bimba/czwek/repo/ChangeOption.kt
@@ -5,7 +5,9 @@
 package xyz.apiote.bimba.czwek.repo
 
 import xyz.apiote.bimba.czwek.api.ChangeOptionV1
+import xyz.apiote.bimba.czwek.api.ChangeOptionV2
 
-data class ChangeOption(val line: String, val headsign: String) {
-	constructor(c: ChangeOptionV1) : this(c.line, c.headsign)
+data class ChangeOption(val line: LineStub, val headsigns: List<String>) {
+	constructor(c: ChangeOptionV1) : this(LineStub(c.line, LineType.UNKNOWN, Colour(0u,0u,0u)), listOf(c.headsign))
+	constructor(c: ChangeOptionV2) : this(LineStub(c.line), c.headsigns)
 }
\ No newline at end of file




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 b3459fda1568096322caab7e16e7dace0d9b87a6..66ac62094ec48793ce330634aa369da2d1b18c68 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
@@ -14,6 +14,7 @@ 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.DepartureV5
 import xyz.apiote.bimba.czwek.api.Time
 import xyz.apiote.bimba.czwek.api.UnknownResourceVersionException
 import xyz.apiote.bimba.czwek.units.Second
@@ -115,7 +116,9 @@ 	val status: ULong,
 	val isRealtime: Boolean,
 	val vehicle: Vehicle,
 	val boarding: UByte,
-	val alerts: List<Alert>
+	val alerts: List<Alert>,
+	val exact: Boolean,
+	val terminusArrival: Boolean
 ) {
 
 	constructor(d: DepartureV1) : this(
@@ -125,7 +128,9 @@ 		d.status,
 		d.isRealtime,
 		Vehicle(d.vehicle),
 		d.boarding,
-		emptyList()
+		emptyList(),
+		true,
+		false
 	)
 
 	constructor(d: DepartureV2) : this(
@@ -135,7 +140,9 @@ 		d.status,
 		d.isRealtime,
 		Vehicle(d.vehicle),
 		d.boarding,
-		emptyList()
+		emptyList(),
+		true,
+		false
 	)
 
 	constructor(d: DepartureV3) : this(
@@ -145,7 +152,9 @@ 		d.status.ordinal.toULong(), // TODO VehicleStatus
 		d.isRealtime,
 		Vehicle(d.vehicle),
 		d.boarding,
-		emptyList()
+		emptyList(),
+		true,
+		false
 	)
 
 	constructor(d: DepartureV4) : this(
@@ -155,7 +164,21 @@ 		d.status.ordinal.toULong(), // TODO VehicleStatus
 		d.isRealtime,
 		Vehicle(d.vehicle),
 		d.boarding,
-		d.alerts.map { Alert(it) }
+		d.alerts.map { Alert(it) },
+		true,
+		false
+	)
+
+	constructor(d: DepartureV5) : this(
+		d.ID,
+		d.time,
+		d.status.ordinal.toULong(), // TODO VehicleStatus
+		d.isRealtime,
+		Vehicle(d.vehicle),
+		d.boarding,
+		d.alerts.map { Alert(it) },
+		d.exact,
+		d.terminusArrival
 	)
 
 	fun statusText(context: Context?, showAsTime: Boolean, at: ZonedDateTime? = null): String {
@@ -195,12 +218,13 @@ 		}
 	}
 
 	fun timeString(context: Context): String {
-		return if (isRealtime) {
-			context.getString(
+		return when {
+			isRealtime -> context.getString(
 				R.string.at_time_realtime, time.Hour.toInt(), time.Minute.toInt(), time.Second.toInt()
 			)
-		} else {
-			context.getString(R.string.at_time, time.Hour.toInt(), time.Minute.toInt())
+
+			exact -> context.getString(R.string.at_time, time.Hour.toInt(), time.Minute.toInt())
+			else -> context.getString(R.string.about_time, time.Hour.toInt(), time.Minute.toInt())
 		}
 	}
 




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 3d645e655861a1f67f3694aa12da1ab90eb1d675..d7d1924cfa5e28c9a41b7433bde9aa7608279cdf 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
@@ -18,7 +18,7 @@ import java.util.Locale
 
 class FeedInfoPrev {
 	companion object {
-		fun unmarshal(stream: InputStream): FeedInfo {
+		fun unmarshal(@Suppress("UNUSED_PARAMETER") stream: InputStream): FeedInfo {
 			return FeedInfo(FeedInfoPrev())
 		}
 	}
@@ -100,7 +100,7 @@ 		null,
 		cached
 	)
 
-	constructor(f: FeedInfoPrev) : this(
+	constructor(@Suppress("UNUSED_PARAMETER") f: FeedInfoPrev) : this(
 		"",
 		"",
 		"",




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 d5ecc9bbbba7111c55e31d0015ce15f696e17274..db2525fa84c91a88fb53b7d6c1b40de93540f8d7 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
@@ -67,9 +67,9 @@ 			LineType.TROLLEYBUS -> R.drawable.trolleybus_black
 			LineType.METRO -> R.drawable.metro_black
 			LineType.RAIL -> R.drawable.train_black
 			LineType.FERRY -> R.drawable.ferry_black
-			LineType.CABLE_TRAM -> TODO()
-			LineType.CABLE_CAR -> TODO()
-			LineType.FUNICULAR -> TODO()
+			LineType.CABLE_TRAM -> R.drawable.cablecar_black
+			LineType.CABLE_CAR -> R.drawable.cabletram_black
+			LineType.FUNICULAR -> R.drawable.funicular_black
 			LineType.MONORAIL -> R.drawable.monorail_black
 			LineType.UNKNOWN -> R.drawable.vehicle_black
 		}




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 165e9c932064611d7cf5be4f334a473356886eb9..f8126a10ef5aafae21a5afbbc30bbcca87a1f49e 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
@@ -101,20 +101,25 @@ 			}
 			cursor.close()
 			s
 		}
-		db.execSQL(
-			"insert into favourites(sequence, feed_id, feed_name, stop_code, stop_name, lines) values (?, ?,?,?,?,?) on conflict(feed_id, stop_code) do update set stop_name = ?, lines = ?, sequence = ?",
-			arrayOf(
-				sequence,
-				favourite.feedID,
-				favourite.feedName,
-				favourite.stopCode,
-				favourite.stopName,
-				favourite.lines.joinToString(separator = "||"),
-				favourite.stopName,
-				favourite.lines.joinToString(separator = "||"),
-				favourite.sequence
+
+		// XXX `on conflict` is not supported on older versions of Android
+		val cursor = db.rawQuery("select * from favourites where feed_id = ? and stop_code = ?", arrayOf(favourite.feedID, favourite.stopCode))
+		if (cursor.count > 0) {
+			db.execSQL("update favourites set stop_name = ?, lines = ?, sequence = ? where feed_id = ? and stop_code = ?", arrayOf(favourite.stopName, favourite.lines.joinToString(separator = "||"), favourite.sequence, favourite.feedID, favourite.stopCode))
+		} else {
+			db.execSQL(
+				"insert into favourites(sequence, feed_id, feed_name, stop_code, stop_name, lines) values (?, ?,?,?,?,?)",
+				arrayOf(
+					sequence,
+					favourite.feedID,
+					favourite.feedName,
+					favourite.stopCode,
+					favourite.stopName,
+					favourite.lines.joinToString(separator = "||"),
+				)
 			)
-		)
+		}
+		cursor.close()
 	}
 
 	override suspend fun saveFavourites(favourites: Set<Favourite>) {




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 1c3f36595fe1c6bc646a137d9475294d59f0f9d1..1ba24e3cbe16562fe30e5fb186d1087c948659c4 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
@@ -14,6 +14,7 @@ import xyz.apiote.bimba.czwek.api.PositionV1
 import xyz.apiote.bimba.czwek.api.Server
 import xyz.apiote.bimba.czwek.api.StopV1
 import xyz.apiote.bimba.czwek.api.StopV2
+import xyz.apiote.bimba.czwek.api.StopV3
 import xyz.apiote.bimba.czwek.api.UnknownResourceException
 import xyz.apiote.bimba.czwek.api.VehicleV1
 import xyz.apiote.bimba.czwek.api.VehicleV2
@@ -171,7 +172,7 @@ 			return when (val response =
 				withContext(Dispatchers.IO) { LocatablesResponse.unmarshal(result.stream!!) }) {
 				is LocatablesResponseDev -> response.locatables.map {
 					when (it) {
-						is StopV2 -> Stop(it)
+						is StopV3 -> Stop(it)
 						is VehicleV3 -> Vehicle(it)
 						else -> throw UnknownResourceException("locatables", it::class)
 					}
@@ -273,7 +274,7 @@ 			return when (val response =
 				withContext(Dispatchers.IO) { QueryablesResponse.unmarshal(result.stream!!) }) {
 				is QueryablesResponseDev -> response.queryables.map {
 					when (it) {
-						is StopV2 -> Stop(it)
+						is StopV3 -> Stop(it)
 						is LineV3 -> Line(it)
 						else -> throw UnknownResourceException("queryablesV4", it::class)
 					}




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 0c6909006f92fe6a5bc7add619c14d4b7cc7a0a4..af81fbf17847b928c5c2ee0c674cb4a451bf858a 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,10 +5,21 @@
 package xyz.apiote.bimba.czwek.repo
 
 import android.content.Context
+import android.graphics.Typeface
 import android.graphics.drawable.Drawable
+import android.text.Annotation
+import android.text.Spannable
+import android.text.SpannableStringBuilder
+import android.text.Spanned
+import android.text.SpannedString
+import android.text.style.StyleSpan
+import androidx.preference.PreferenceManager
 import xyz.apiote.bimba.czwek.R
+import xyz.apiote.bimba.czwek.RoundedBackgroundSpan
 import xyz.apiote.bimba.czwek.api.StopV1
 import xyz.apiote.bimba.czwek.api.StopV2
+import xyz.apiote.bimba.czwek.api.StopV3
+
 
 data class Stop(
 	val code: String,
@@ -46,22 +57,119 @@ 		s.feedID,
 		Position(s.position),
 		s.changeOptions.map { ChangeOption(it) })
 
-	fun changeOptions(context: Context): Pair<String, String> = Pair(changeOptions.groupBy { it.line }
-		.map { Pair(it.key, it.value.joinToString { co -> co.headsign }) }.joinToString {
-			context.getString(
-				R.string.vehicle_headsign, it.first, it.second
-			)
-		},
-		changeOptions.groupBy { it.line }
-			.map { Pair(it.key, it.value.joinToString { co -> co.headsign }) }.joinToString {
-				context.getString(
-					R.string.vehicle_headsign_content_description, it.first, it.second
+	constructor(s: StopV3) : this(
+		s.code,
+		s.name,
+		s.nodeName,
+		s.zone,
+		s.feedID,
+		Position(s.position),
+		s.changeOptions.map { ChangeOption(it) })
+
+	fun changeOptions(context: Context, decoration: LineDecoration): Pair<Spannable, String> {
+		return Pair(changeOptions.groupBy { it.line }
+			.map {
+				Pair(
+					it.key,
+					it.value.flatMap { co -> co.headsigns }.sortedBy { headsign -> headsign }.joinToString()
+				)
+			}.fold(SpannableStringBuilder("")) { acc, p ->
+				if (acc.toString() != "") {
+					acc.append("; ")
+				}
+				var str = SpannableStringBuilder(
+					context.getText(
+						R.string.vehicle_headsign
+					) as SpannedString
 				)
-			})
+				str = applyAnnotations(str, decoration, p.first, p.first.name, p.second)
+				str = applyAnnotations(str, decoration, p.first)
+				acc.append(str)
+				acc
+			},
+			changeOptions.groupBy { it.line }
+				.map {
+					Pair(
+						it.key,
+						it.value.flatMap { co -> co.headsigns }.sortedBy { headsign -> headsign }.joinToString()
+					)
+				}.joinToString {
+					context.getString(
+						R.string.vehicle_headsign_content_description, it.first, it.second
+					)
+				})
+	}
+
+	private fun applyAnnotations(
+		s: SpannableStringBuilder,
+		decoration: LineDecoration,
+		line: LineStub,
+		vararg args: Any
+	): SpannableStringBuilder {
+		val str = SpannableStringBuilder(s)
+		val annotations = str.getSpans(0, str.length, Annotation::class.java)
+		annotations.forEach {
+			when (it.key) {
+				"arg" -> {
+					if (args.isEmpty()) {
+						return@forEach
+					}
+					val argIndex = Integer.parseInt(it.value)
+					str.replace(str.getSpanStart(it), str.getSpanEnd(it), args[argIndex] as String)
+				}
+
+				"decoration" -> {
+					if (args.isNotEmpty()) {
+						return@forEach
+					}
+					// TODO rounded corners/padding
+					val background = RoundedBackgroundSpan(line.colour.toInt(), line.textColour(line.colour))
+					val ital = StyleSpan(Typeface.ITALIC)
+					when (decoration) {
+						LineDecoration.ITALICS -> str.setSpan(
+							ital,
+							str.getSpanStart(it),
+							str.getSpanEnd(it),
+							Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
+						)
+
+						LineDecoration.COLOUR -> {
+							str.setSpan(background, str.getSpanStart(it), str.getSpanEnd(it), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
+							// str.setSpan(foreground, str.getSpanStart(it), str.getSpanEnd(it), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
+						}
+
+						LineDecoration.NONE -> {}
+					}
+				}
+			}
+		}
+		return str
+	}
+
+	fun changeOptionsString(): String = changeOptions.groupBy { it.line }
+		.map {
+			Pair(
+				it.key,
+				it.value.flatMap { co -> co.headsigns }.sortedBy { headsign -> headsign }.joinToString()
+			)
+		}.joinToString("; ")
 
 	override fun toString(): String {
-		var result = "$name ($code) [$zone] $position\n"
-		for (chOpt in changeOptions) result += "${chOpt.line} → ${chOpt.headsign}\n"
-		return result
+		return "$name ($code) [$zone] $position\n${changeOptionsString()}"
 	}
-}
\ No newline at end of file
+
+	enum class LineDecoration {
+		NONE, ITALICS, COLOUR;
+		companion object {
+			fun fromPreferences(context: Context) =
+				when (PreferenceManager.getDefaultSharedPreferences(context)
+					.getString("line_decoration", "italics")) {
+					"italics" -> Stop.LineDecoration.ITALICS
+					"colour" -> Stop.LineDecoration.COLOUR
+					"none" -> Stop.LineDecoration.NONE
+					else -> Stop.LineDecoration.ITALICS
+				}
+		}
+	}
+}
+




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 7ce6dc062432c68e176a752e609cc2b966dbef52..a13611c7ee852e3421c01a0bdc651128d0ef2d6f 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
@@ -144,7 +144,7 @@ 				holder?.feedName?.visibility = View.VISIBLE
 				holder?.feedName?.text = feeds?.get(stop.feedID)?.name ?: ""
 			}
 			context?.let {
-				stop.changeOptions(it).let { changeOptions ->
+				stop.changeOptions(it, Stop.LineDecoration.fromPreferences(context)).let { changeOptions ->
 					holder?.description?.apply {
 						text = changeOptions.first
 						contentDescription = changeOptions.second
@@ -257,10 +257,8 @@ 				}
 
 				is Stop -> {
 					assert(newQueryable is Stop)
-					val oldChangeOptions =
-						oldQueryable.changeOptions.joinToString { "${it.line}->${it.headsign}" }
-					val newChangeOptions =
-						(newQueryable as Stop).changeOptions.joinToString { "${it.line}->${it.headsign}" }
+					val oldChangeOptions = oldQueryable.changeOptionsString()
+					val newChangeOptions = (newQueryable as Stop).changeOptionsString()
 					oldQueryable.name == newQueryable.name && oldChangeOptions == newChangeOptions &&
 						oldPosition?.latitude == newPosition?.latitude &&
 						oldPosition?.longitude == newPosition?.longitude &&




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 9a778552b653033536f19fbbb2deb113457e765b..b29210b31e39602eb0a83b32b219f1d55c41ee7b 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
@@ -5,6 +5,7 @@
 package xyz.apiote.bimba.czwek.search
 
 import android.content.Context
+import android.content.Intent
 import android.hardware.Sensor
 import android.hardware.SensorEvent
 import android.hardware.SensorEventListener
@@ -46,6 +47,33 @@ 	enum class Mode {
 		MODE_LOCATION, MODE_SEARCH, MODE_POSITION, MODE_SHORT_CODE_LOCATION, MODE_SHORT_CODE
 	}
 
+	companion object {
+		const val MODE_KEY = "mode"
+		const val QUERY_KEY = "query"
+		const val LATITUDE_KEY = "lat"
+		const val LONGITUDE_KEY = "lon"
+		fun getIntent(
+			context: Context,
+			mode: Mode,
+			query: String,
+			latitude: Double,
+			longitude: Double
+		) =
+			Intent(context, ResultsActivity::class.java).apply {
+				putExtra(MODE_KEY, mode)
+				putExtra(QUERY_KEY, query)
+				putExtra(LATITUDE_KEY, latitude)
+				putExtra(LONGITUDE_KEY, longitude)
+			}
+
+		fun getIntent(context: Context, mode: Mode, query: String) =
+			Intent(context, ResultsActivity::class.java).apply {
+				putExtra(MODE_KEY, mode)
+				putExtra(QUERY_KEY, query)
+			}
+
+	}
+
 	private var _binding: ActivityResultsBinding? = null
 	private val binding get() = _binding!!
 
@@ -91,14 +119,15 @@ 				locate()
 			}
 
 			Mode.MODE_SHORT_CODE_LOCATION -> {
-				val query = intent.extras?.getString("query")
-				getString(R.string.stops_near_code, query)
+				val query = intent.extras?.getString(QUERY_KEY)
+				supportActionBar?.title = getString(R.string.stops_near_code, query)
 				shortOLC = OpenLocationCode(query)
 				locate()
 			}
 
 			Mode.MODE_SHORT_CODE -> {
-				val query = intent.extras?.getString("query")
+				val query = intent.extras?.getString(QUERY_KEY)
+				supportActionBar?.title = getString(R.string.stops_near_code, query)
 				val split = query!!.trim().split(" ")
 				val code = split.first().trim(',').trim()
 				val freePart = split.drop(1).joinToString(" ")
@@ -115,9 +144,9 @@ 				}
 			}
 
 			Mode.MODE_POSITION -> {
-				val query = intent.extras?.getString("query")
-				val lat = intent.extras?.getDouble("lat")
-				val lon = intent.extras?.getDouble("lon")
+				val query = intent.extras?.getString(QUERY_KEY)
+				val lat = intent.extras?.getDouble(LATITUDE_KEY)
+				val lon = intent.extras?.getDouble(LONGITUDE_KEY)
 				supportActionBar?.title = getString(R.string.stops_near_code, query)
 				getQueryablesByLocation(Location(null).apply {
 					latitude = lat!!
@@ -126,7 +155,7 @@ 				}, this)
 			}
 
 			Mode.MODE_SEARCH -> {
-				val query = intent.extras?.getString("query")!!
+				val query = intent.extras?.getString(QUERY_KEY)!!
 				supportActionBar?.title = getString(R.string.results_for, query)
 				getQueryablesByQuery(query, this)
 			}
@@ -135,10 +164,10 @@ 	}
 
 	private fun getMode(): Mode {
 		return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
-			intent.extras!!.getSerializable("mode", Mode::class.java)!!
+			intent.extras!!.getSerializable(MODE_KEY, Mode::class.java)!!
 		} else {
 			@Suppress("DEPRECATION")
-			intent.extras!!.get("mode") as Mode
+			intent.extras!!.get(MODE_KEY) as Mode
 		}
 	}
 
@@ -162,7 +191,10 @@ 			handler.postDelayed(runnable, 60 * 1000)
 			locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER)
 				?.let { onLocationChanged(it) }
 		} catch (_: SecurityException) {
-			// this won’t happen because we don’t start this activity without location permission
+			Log.wtf(
+				"locate",
+				"this shouldn’t happen because we don’t start this activity without location permission"
+			)
 		}
 	}
 




diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/settings/DownloadCitiesWorker.kt b/app/src/main/java/xyz/apiote/bimba/czwek/settings/DownloadCitiesWorker.kt
index 02de1d344e0666614ee02207fe90531cd3cb5626..feb3d2b4e863968a3bd9715c5990bba505205e6f 100644
--- a/app/src/main/java/xyz/apiote/bimba/czwek/settings/DownloadCitiesWorker.kt
+++ b/app/src/main/java/xyz/apiote/bimba/czwek/settings/DownloadCitiesWorker.kt
@@ -1,9 +1,9 @@
 package xyz.apiote.bimba.czwek.settings
 
 import android.Manifest
-import android.app.NotificationManager
 import android.content.Context
 import android.content.pm.PackageManager
+import android.database.sqlite.SQLiteConstraintException
 import android.database.sqlite.SQLiteDatabase
 import android.util.Log
 import androidx.core.app.ActivityCompat
@@ -21,16 +21,42 @@ import java.io.BufferedInputStream
 import java.io.BufferedOutputStream
 import java.io.File
 import java.net.URL
+import java.time.Instant
 import java.time.ZonedDateTime
+import java.time.temporal.ChronoUnit
 import java.util.UUID
 import java.util.zip.ZipEntry
 import java.util.zip.ZipInputStream
 
+// FIXME doesn't work on older versions of Android
 class DownloadCitiesWorker(appContext: Context, workerParams: WorkerParameters) :
 	Worker(appContext, workerParams) {
 
+	companion object {
+		const val AUTOUPDATE_KEY = "autoupdate_cities_list"
+		const val LAST_UPDATE_KEY = "cities_last_update"
+		const val NOTIFICATION_CHANNEL = "cities_channel"
+		const val DATABASE_NAME = "geocoding"
+		const val ETAG_HEADER_NAME = "ETag"
+		const val ETAG_KEY = "cities_etag"
+		const val RESULT_ZIP_FILE = "cities.zip"
+		const val CITIES_URL = "https://download.geonames.org/export/dump/cities15000.zip"
+		const val CITIES_FILE = "cities15000.txt"
+		fun shouldUpdate(context: Context): Boolean {
+			val (updatesEnabled, weekPassed) = PreferenceManager.getDefaultSharedPreferences(context)
+				.let {
+					arrayOf(
+						it.getBoolean(AUTOUPDATE_KEY, false),
+						Instant.ofEpochSecond(it.getLong(LAST_UPDATE_KEY, 0)).plus(7, ChronoUnit.DAYS)
+							.isBefore(Instant.now())
+					)
+				}
+			return updatesEnabled && weekPassed
+		}
+	}
+
 	override fun doWork(): Result {
-		val notificationBuilder = NotificationCompat.Builder(applicationContext, "cities_channel")
+		val notificationBuilder = NotificationCompat.Builder(applicationContext, NOTIFICATION_CHANNEL)
 			.setSmallIcon(R.drawable.geocoding)
 			.setContentTitle(applicationContext.getString(R.string.updating_geocoding_data))
 			.setContentText(applicationContext.getString(R.string.downloading_cities_list))
@@ -46,15 +72,15 @@ 				NotificationManagerCompat.from(applicationContext).notify(0, notificationBuilder.build())
 			}
 
 			val db = SQLiteDatabase.openOrCreateDatabase(
-				applicationContext.getDatabasePath("geocoding").path,
+				applicationContext.getDatabasePath(DATABASE_NAME).path,
 				null
 			)
-			val url = URL("https://download.geonames.org/export/dump/cities15000.zip")
+			val url = URL(CITIES_URL)
 			val connection = url.openConnection()
 			var length = connection.contentLength.toLong()
-			val connectionEtag = connection.getHeaderField("ETag")
+			val connectionEtag = connection.getHeaderField(ETAG_HEADER_NAME)
 			val savedEtag = PreferenceManager.getDefaultSharedPreferences(applicationContext)
-				.getString("cities_etag", null)
+				.getString(ETAG_KEY, null)
 			if (savedEtag != null && savedEtag == connectionEtag) {
 				if (ActivityCompat.checkSelfPermission(
 						applicationContext,
@@ -77,7 +103,7 @@ 					.setInputStream(BufferedInputStream(connection.getInputStream())).get()
 			val zipFileStream = BufferedOutputStream(
 				File(
 					applicationContext.noBackupFilesDir.path,
-					"cities.zip"
+					RESULT_ZIP_FILE
 				).outputStream()
 			)
 
@@ -113,7 +139,7 @@ 				) == PackageManager.PERMISSION_GRANTED
 			) {
 				NotificationManagerCompat.from(applicationContext).notify(0, notificationBuilder.build())
 			}
-			val zipFile = File(applicationContext.noBackupFilesDir.path, "cities.zip")
+			val zipFile = File(applicationContext.noBackupFilesDir.path, RESULT_ZIP_FILE)
 			length = zipFile.length()
 			countingStream =
 				BoundedInputStream.Builder().setInputStream(BufferedInputStream(zipFile.inputStream()))
@@ -121,7 +147,7 @@ 					.get()
 			val stream = ZipInputStream(countingStream)
 			var entry: ZipEntry? = stream.nextEntry
 			while (entry != null) {
-				if (entry.name != "cities15000.txt") {
+				if (entry.name != CITIES_FILE) {
 					entry = stream.nextEntry
 					continue
 				}
@@ -159,14 +185,28 @@
 							val id = UUID.randomUUID()
 							db.execSQL("insert into places2 values(?, ?, ?)", arrayOf(id, row[4], row[5]))
 							names.split(",").toSet().forEach { name ->
-								db.execSQL(
-									"insert into place_names2 values(?, ?) on conflict(name) do nothing",
-									arrayOf(id, name)
-								)
-								db.execSQL(
-									"insert into place_names2 values(?, ?) on conflict(name) do nothing",
-									arrayOf(id, "$name, ${row[8]}")
-								)
+								try {
+									db.execSQL(
+										"insert into place_names2 values(?, ?)",
+										arrayOf(id, name)
+									)
+								} catch (e: SQLiteConstraintException) {
+									// XXX `on conflict` doesn't work on older versions of Android
+									if (e.message?.contains("UNIQUE constraint failed: place_names2.name") != true) {
+										throw e
+									}
+								}
+								try {
+									db.execSQL(
+										"insert into place_names2 values(?, ?)",
+										arrayOf(id, "$name, ${row[8]}")
+									)
+								} catch (e: SQLiteConstraintException) {
+									// XXX `on conflict` doesn't work on older versions of Android
+									if (e.message?.contains("UNIQUE constraint failed: place_names2.name") != true) {
+										throw e
+									}
+								}
 							}
 						}
 					}
@@ -185,8 +225,8 @@ 			db.execSQL("alter table place_names2 rename to place_names")
 			db.execSQL("create unique index place_names__name on place_names(name)")
 
 			PreferenceManager.getDefaultSharedPreferences(applicationContext).edit {
-				putLong("cities_last_update", ZonedDateTime.now().toEpochSecond())
-				putString("cities_etag", connectionEtag)
+				putLong(LAST_UPDATE_KEY, ZonedDateTime.now().toEpochSecond())
+				putString(ETAG_KEY, connectionEtag)
 			}
 
 			db.close()




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 88165f880c011dfc8b55b0e6d32ac1363d8b0f55..8eb69c2ac80b7122daaae47c52eb377f6755e1d4 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
@@ -4,6 +4,7 @@ // SPDX-License-Identifier: GPL-3.0-or-later
 
 package xyz.apiote.bimba.czwek.settings
 
+import android.content.Context
 import android.content.Intent
 import android.content.SharedPreferences
 import android.graphics.Color
@@ -30,15 +31,25 @@ import xyz.apiote.bimba.czwek.api.Server
 import xyz.apiote.bimba.czwek.api.TrafficFormatException
 import xyz.apiote.bimba.czwek.api.getBimba
 import xyz.apiote.bimba.czwek.databinding.ActivityServerChooserBinding
+import xyz.apiote.bimba.czwek.onboarding.FirstRunActivity
 import xyz.apiote.bimba.czwek.settings.feeds.FeedChooserActivity
 
 class ServerChooserActivity : AppCompatActivity() {
+	companion object {
+		const val PARAM_SIMPLE = "simple"
+		const val IN_FEEDS_TRANSACTION = "inFeedsTransaction"
+		const val PREFERENCES_NAME = "shp"
+		fun getIntent(context: Context, simple: Boolean) = Intent(context, ServerChooserActivity::class.java).apply {
+				putExtra(PARAM_SIMPLE, simple)
+		}
+	}
+
 	private var _binding: ActivityServerChooserBinding? = null
 	private val binding get() = _binding!!
 
 	private val activityLauncher =
 		registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
-			if (!preferences.getBoolean("inFeedsTransaction", true)) {
+			if (!preferences.getBoolean(IN_FEEDS_TRANSACTION, true)) {
 				finish()
 			}
 		}
@@ -49,10 +60,10 @@ 	override fun onCreate(savedInstanceState: Bundle?) {
 		enableEdgeToEdge()
 		super.onCreate(savedInstanceState)
 
-		preferences = getSharedPreferences("shp", MODE_PRIVATE)
+		preferences = getSharedPreferences(PREFERENCES_NAME, MODE_PRIVATE)
 
-		if (intent.getBooleanExtra("simple", false)) {
-			setServer("bimba.apiote.xyz", "")
+		if (intent.getBooleanExtra(PARAM_SIMPLE, false)) {
+			setServer(Server.DEFAULT, "")
 			checkServer(true)
 		} else {
 			_binding = ActivityServerChooserBinding.inflate(layoutInflater)
@@ -65,7 +76,7 @@ 				windowInsets
 			}
 
 			preferences.edit(true) {
-				putBoolean("inFeedsTransaction", true)
+				putBoolean(IN_FEEDS_TRANSACTION, true)
 			}
 
 			if (preferences.getBoolean("shibboleet", false)) {
@@ -78,7 +89,7 @@ 			binding.serverField.editText!!.addTextChangedListener { editable ->
 				binding.button.isEnabled = !editable.isNullOrBlank()
 			}
 
-			if (!preferences.getBoolean("firstRun", true)) {
+			if (!FirstRunActivity.getFirstRun(this)) {
 				Server.get(this).let { server ->
 					binding.serverField.editText!!.setText(server.host)
 					binding.tokenField.editText!!.setText(server.token)
@@ -93,7 +104,7 @@ 						binding.button.setTextColor(Color.WHITE)
 						preferences.edit(true) {
 							putBoolean("shibboleet", true)
 						}
-						if (!preferences.getBoolean("firstRun", true)) {
+						if (!FirstRunActivity.getFirstRun(this)) {
 							Server.get(this).let { server ->
 								binding.serverField.editText!!.setText(server.host)
 								binding.tokenField.editText!!.setText(server.token)
@@ -107,7 +118,7 @@ 						setContentView(binding.root)
 						preferences.edit(true) {
 							putBoolean("shibboleet", false)
 						}
-						if (!preferences.getBoolean("firstRun", true)) {
+						if (!FirstRunActivity.getFirstRun(this)) {
 							Server.get(this).let { server ->
 								binding.serverField.editText!!.setText(server.host)
 								binding.tokenField.editText!!.setText(server.token)
@@ -185,7 +196,7 @@ 		}
 	}
 
 	private fun moveOn(bimba: Bimba, isSimple: Boolean) {
-		val token = preferences.getString("token", "")
+		val token = preferences.getString(Server.TOKEN_KEY, "")
 
 		if (bimba.isPrivate() && token == "") {
 			showDialog(R.string.error, R.string.server_private_question, R.drawable.error_sec, null)
@@ -204,20 +215,20 @@ 	}
 
 	private fun setServer(hostname: String, token: String) {
 		preferences.edit(true) {
-			putString("host", hostname)
-			putString("token", token)
+			putString(Server.HOST_KEY, hostname)
+			putString(Server.TOKEN_KEY, token)
 		}
 	}
 
 	private fun updateServer(apiPath: String) {
 		preferences.edit(true) {
-			putString("apiPath", apiPath)
+			putString(Server.API_PATH_KEY, apiPath)
 		}
 	}
 
 	private fun runFeedsActivity() {
 		activityLauncher.launch(Intent(this, FeedChooserActivity::class.java))
-		if (intent.getBooleanExtra("simple", false)) {
+		if (intent.getBooleanExtra(PARAM_SIMPLE, false)) {
 			finish()
 		}
 	}




diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/settings/SettingsActivity.kt b/app/src/main/java/xyz/apiote/bimba/czwek/settings/SettingsActivity.kt
index 4a76cd5ef120f9419ff479301845c81d8240ceae..7f75a263c06670675535a7945c4367d2e62a30a1 100644
--- a/app/src/main/java/xyz/apiote/bimba/czwek/settings/SettingsActivity.kt
+++ b/app/src/main/java/xyz/apiote/bimba/czwek/settings/SettingsActivity.kt
@@ -70,7 +70,7 @@ 				findPreference("download_cities_list")?.isEnabled = false
 			}
 
 			val citiesLastUpdate = PreferenceManager.getDefaultSharedPreferences(requireContext())
-				.getLong("cities_last_update", -1)
+				.getLong(DownloadCitiesWorker.LAST_UPDATE_KEY, -1)
 			if (citiesLastUpdate > 0) {
 				val lastUpdateTime = DateUtils.getRelativeDateTimeString(
 					context,




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 09f4dd40f4c2eca59f1f092a66042214f88f14a6..3d49614ac2d04edf2269267b404c0f4c5ecc7b7e 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
@@ -23,12 +23,17 @@ import xyz.apiote.bimba.czwek.R
 import xyz.apiote.bimba.czwek.api.Server
 import xyz.apiote.bimba.czwek.dashboard.MainActivity
 import xyz.apiote.bimba.czwek.databinding.ActivityFeedChooserBinding
+import xyz.apiote.bimba.czwek.onboarding.FirstRunActivity
 import xyz.apiote.bimba.czwek.repo.FeedInfo
+import xyz.apiote.bimba.czwek.settings.ServerChooserActivity
 
 // TODO on internet connection -> getServer
 // TODO swipe to refresh?
 
 class FeedChooserActivity : AppCompatActivity() {
+	companion object {
+		const val PREFERENCES_NAME = "shp"
+	}
 	private lateinit var viewModel: FeedsViewModel
 	private var _binding: ActivityFeedChooserBinding? = null
 	private val binding get() = _binding!!
@@ -124,11 +129,11 @@ 	}
 
 	private fun moveOn() {
 		viewModel.settings.value?.save(this, Server.get(this))
-		val preferences = getSharedPreferences("shp", MODE_PRIVATE)
+		val preferences = getSharedPreferences(PREFERENCES_NAME, MODE_PRIVATE)
 		preferences.edit(true) {
-			putBoolean("inFeedsTransaction", false)
+			putBoolean(ServerChooserActivity.IN_FEEDS_TRANSACTION, false)
 		}
-		if (preferences.getBoolean("firstRun", true)) {
+		if (FirstRunActivity.getFirstRun(this)) {
 			val intent = Intent(this, MainActivity::class.java)
 			startActivity(intent)
 		}




diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/settings/feeds/FeedSettings.kt b/app/src/main/java/xyz/apiote/bimba/czwek/settings/feeds/FeedSettings.kt
index 632af91e8e3286638d6614460593ed54b59dcc18..31c0bcd141c540e1b70d418b402b6859ee6a9199 100644
--- a/app/src/main/java/xyz/apiote/bimba/czwek/settings/feeds/FeedSettings.kt
+++ b/app/src/main/java/xyz/apiote/bimba/czwek/settings/feeds/FeedSettings.kt
@@ -22,7 +22,7 @@
 	fun save(context: Context, server: Server) {
 		val doc = KBson().dump(serializer(), this).toHexString()
 		val feedsPreferences =
-			context.getSharedPreferences("feeds_settings", AppCompatActivity.MODE_PRIVATE)
+			context.getSharedPreferences(PREFERENCES_NAME, AppCompatActivity.MODE_PRIVATE)
 		feedsPreferences.edit {
 			val key = URLEncoder.encode(server.apiPath, "utf-8")
 			putString(key, doc)
@@ -30,9 +30,10 @@ 		}
 	}
 
 	companion object {
+		const val PREFERENCES_NAME = "feeds_settings"
 		fun load(context: Context, apiPath: String = Server.get(context).apiPath): FeedsSettings {
 			val doc = context.getSharedPreferences(
-				"feeds_settings",
+				PREFERENCES_NAME,
 				Context.MODE_PRIVATE
 			).getString(URLEncoder.encode(apiPath, "utf-8"), null)
 			return doc?.let { KBson().load(serializer(), doc.hexToByteArray()) } ?: FeedsSettings(




diff --git a/app/src/main/res/drawable/cablecar_black.xml b/app/src/main/res/drawable/cablecar_black.xml
new file mode 100644
index 0000000000000000000000000000000000000000..4f2979fcb7103f73915f871cf103dcba9e06c3b9
--- /dev/null
+++ b/app/src/main/res/drawable/cablecar_black.xml
@@ -0,0 +1,17 @@
+<!--
+SPDX-FileCopyrightText: Austin Andrews
+
+SPDX-License-Identifier: Apache-2.0
+
+source: https://pictogrammers.com/library/mdi/icon/gondola/
+-->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+	android:width="24dp"
+	android:height="24dp"
+	android:viewportWidth="24"
+	android:viewportHeight="24">
+	<path
+		android:fillColor="#000000"
+		android:pathData="M18,10H13V7.59L22.12,6.07L21.88,4.59L16.41,5.5C16.46,5.35 16.5,5.18 16.5,5A1.5,1.5 0 0,0 15,3.5A1.5,1.5 0 0,0 13.5,5C13.5,5.35 13.63,5.68 13.84,5.93L13,6.07V5H11V6.41L10.41,6.5C10.46,6.35 10.5,6.18 10.5,6A1.5,1.5 0 0,0 9,4.5A1.5,1.5 0 0,0 7.5,6C7.5,6.36 7.63,6.68 7.83,6.93L1.88,7.93L2.12,9.41L11,7.93V10H6C4.89,10 4,10.9 4,12V18A2,2 0 0,0 6,20H18A2,2 0 0,0 20,18V12A2,2 0 0,0 18,10M6,12H8.25V16H6V12M9.75,16V12H14.25V16H9.75M18,16H15.75V12H18V16Z" />
+</vector>
\ No newline at end of file




diff --git a/app/src/main/res/drawable/cabletram_black.xml b/app/src/main/res/drawable/cabletram_black.xml
new file mode 100644
index 0000000000000000000000000000000000000000..3947ee16f0d93ca3e4c67b9e861a0309e6ec255b
--- /dev/null
+++ b/app/src/main/res/drawable/cabletram_black.xml
@@ -0,0 +1,44 @@
+<!--
+SPDX-FileCopyrightText: Jamison Wieser
+
+SPDX-License-Identifier: CC0-1.0
+
+source: https://thenounproject.com/icon/cable-car-4173/
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+	android:width="24dp"
+	android:height="24dp"
+	android:viewportWidth="1600"
+	android:viewportHeight="1600">
+	<group
+		android:translateX="200"
+		android:translateY="200">
+		<path
+			android:fillColor="#000000"
+			android:pathData="m166.5,228v48.2h48.2v413.8h770.7v-413.9h48.1v-48.2c-289,-96.3 -578,-96.3 -867,0zM455.5,641.8h-192.6v-316.5c64.2,-38.5 128.4,-38.5 192.6,0 -0,38.5 -0,316.5 -0,316.5zM696.3,641.8h-192.6v-316.5c64.2,-38.5 128.4,-38.5 192.6,0 -0,38.5 -0,316.5 -0,316.5zM937.1,641.8h-192.6v-316.5c64.2,-38.5 128.4,-38.5 192.6,0v316.5z" />
+
+		<path
+			android:fillColor="#000000"
+			android:pathData="m214.7,714.6v262.6h770.7l-0,-262.6zM600,894.8c-26.6,0 -48.2,-21.6 -48.2,-48.2 0,-26.6 21.6,-48.1 48.2,-48.1 26.6,0 48.2,21.5 48.2,48.1 0,26.6 -21.6,48.2 -48.2,48.2z" />
+
+		<path
+			android:fillColor="#000000"
+			android:pathData="m985.3,1055.5v-48.2h-770.7v48.2h-96.3v48.1h125.2c10.6,0 25.4,-6.1 32.9,-13.6l21,-21c6.2,-6.2 17.3,-11.4 27,-13 16.7,2.7 19.2,15 4.6,29.6l-84.2,84.2c-16.6,16.6 -11,30.2 12.4,30.2h685.4c23.4,0 29.1,-13.6 12.5,-30.2l-84.2,-84.2c-14.5,-14.5 -12,-26.8 4.7,-29.6 9.6,1.7 20.8,6.8 27,13l20.9,21c7.5,7.5 22.3,13.6 32.9,13.6h125.2v-48.1h-96.3z" />
+
+		<path
+			android:fillColor="#000000"
+			android:pathData="m359.2,146.4c192.6,-24.1 289,-24.1 481.6,0v-24.1h48.2v-24.1c-192.7,-48.2 -385.3,-48.2 -578,0v24.1h48.2v24.1z" />
+
+		<path
+			android:fillColor="#000000"
+			android:pathData="m648.2,46c-1.2,-25.6 -22.3,-46 -48.2,-46 -25.9,0 -47,20.4 -48.2,46 32.1,-2.8 64.3,-2.8 96.4,0z" />
+
+		<path
+			android:fillColor="#000000"
+			android:pathData="m189.4,446c0,-10.8 -8.8,-19.6 -19.7,-19.6s-19.6,8.8 -19.6,19.6v458.3c0,10.8 8.7,19.6 19.6,19.6 10.9,0 19.7,-8.8 19.7,-19.6z" />
+
+		<path
+			android:fillColor="#000000"
+			android:pathData="m1049.9,446c0,-10.8 -8.8,-19.6 -19.6,-19.6 -10.9,0 -19.7,8.8 -19.7,19.6v458.3c0,10.8 8.8,19.6 19.7,19.6 10.8,0 19.6,-8.8 19.6,-19.6z" />
+	</group>
+</vector>




diff --git a/app/src/main/res/drawable/comment.xml b/app/src/main/res/drawable/comment.xml
new file mode 100644
index 0000000000000000000000000000000000000000..bd983286222a7a64987fe28d3cd5bd7eff9816c9
--- /dev/null
+++ b/app/src/main/res/drawable/comment.xml
@@ -0,0 +1,17 @@
+<!--
+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:tint="#000000"
+	android:viewportWidth="24"
+	android:viewportHeight="24">
+
+	<path
+		android:fillColor="@android:color/white"
+		android:pathData="M21.99,4c0,-1.1 -0.89,-2 -1.99,-2H4c-1.1,0 -2,0.9 -2,2v12c0,1.1 0.9,2 2,2h14l4,4 -0.01,-18zM17,11h-4v4h-2v-4H7V9h4V5h2v4h4v2z" />
+
+</vector>




diff --git a/app/src/main/res/drawable/discard.xml b/app/src/main/res/drawable/discard.xml
new file mode 100644
index 0000000000000000000000000000000000000000..dd97b1f0bcfa64600d99e7794bbc07c29c6b8456
--- /dev/null
+++ b/app/src/main/res/drawable/discard.xml
@@ -0,0 +1,17 @@
+<!--
+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:tint="#000000"
+	android:viewportWidth="24"
+	android:viewportHeight="24">
+
+	<path
+		android:fillColor="@android:color/white"
+		android:pathData="M19,6.41L17.59,5 12,10.59 6.41,5 5,6.41 10.59,12 5,17.59 6.41,19 12,13.41 17.59,19 19,17.59 13.41,12z" />
+
+</vector>




diff --git a/app/src/main/res/drawable/email.xml b/app/src/main/res/drawable/email.xml
new file mode 100644
index 0000000000000000000000000000000000000000..249c960053e0f0a4d2c5eaa1663cdd04ff65c6db
--- /dev/null
+++ b/app/src/main/res/drawable/email.xml
@@ -0,0 +1,18 @@
+<!--
+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:tint="?attr/colorOnSurface"
+	android:viewportWidth="24"
+	android:viewportHeight="24">
+
+	<path
+		android:fillColor="@android:color/white"
+		android:pathData="M20,4L4,4c-1.1,0 -1.99,0.9 -1.99,2L2,18c0,1.1 0.9,2 2,2h16c1.1,0 2,-0.9 2,-2L22,6c0,-1.1 -0.9,-2 -2,-2zM20,8l-8,5 -8,-5L4,6l8,5 8,-5v2z" />
+
+</vector>




diff --git a/app/src/main/res/drawable/funicular_black.xml b/app/src/main/res/drawable/funicular_black.xml
new file mode 100644
index 0000000000000000000000000000000000000000..3472477cc7d19902ba293b28b182f8ab6a52d069
--- /dev/null
+++ b/app/src/main/res/drawable/funicular_black.xml
@@ -0,0 +1,25 @@
+<!--
+SPDX-FileCopyrightText: Daniel Calliess
+
+SPDX-License-Identifier: CC0-1.0
+
+source: https://thenounproject.com/icon/funicular-635171/
+-->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+	android:width="24dp"
+	android:height="24dp"
+	android:viewportWidth="1600"
+	android:viewportHeight="1600">
+	<group
+		android:translateX="200"
+		android:translateY="200">
+		<path
+			android:fillColor="#000000"
+			android:pathData="m60,1086 l1080,-624.6v54l-1080,624.6z" />
+
+		<path
+			android:fillColor="#000000"
+			android:pathData="m81.6,663c-12,6.9 -21.6,22.1 -21.6,34.1v313.3c0,12 9.6,16 21.6,9.1l1036.8,-599.6c12,-6.9 21.6,-22.1 21.6,-34.1v-313.3c0,-12 -9.6,-16 -21.6,-9.1zM135.6,696.6 L387.7,550.8c6,-3.5 10.8,-1.4 10.8,4.6v172.9c0,6 -4.8,13.6 -10.8,17.1l-252.1,145.8c-6,3.5 -10.8,1.4 -10.8,-4.6v-172.9c0,-6 4.8,-13.6 10.8,-17.1zM474.1,500.8 L725.9,355.2c6,-3.5 10.8,-1.4 10.8,4.6v172.9c0,6 -4.8,13.6 -10.8,17.1l-251.8,145.6c-6,3.5 -10.8,1.4 -10.8,-4.6v-172.9c0,-6 4.8,-13.6 10.8,-17.1zM812.3,305.2 L1064.4,159.4c6,-3.5 10.8,-1.4 10.8,4.6v172.9c0,6 -4.8,13.6 -10.8,17.1l-252.1,145.8c-6,3.5 -10.8,1.4 -10.8,-4.6v-172.9c0,-6 4.8,-13.6 10.8,-17.1z" />
+	</group>
+</vector>




diff --git a/app/src/main/res/drawable/inexact.xml b/app/src/main/res/drawable/inexact.xml
new file mode 100644
index 0000000000000000000000000000000000000000..d1ba0403097cbc7a980bc9d5ddfd921596069d04
--- /dev/null
+++ b/app/src/main/res/drawable/inexact.xml
@@ -0,0 +1,17 @@
+<!--
+SPDX-FileCopyrightText: Michael Irigoyen
+
+SPDX-License-Identifier: Apache-2.0
+
+source: https://pictogrammers.com/library/mdi/icon/tilde/
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+	android:width="24dp"
+	android:height="24dp"
+	android:tint="?attr/colorOnSurface"
+	android:viewportWidth="24"
+	android:viewportHeight="24">
+	<path
+		android:fillColor="#000000"
+		android:pathData="M2,15C2,15 2,9 8,9C12,9 12.5,12.5 15.5,12.5C19.5,12.5 19.5,9 19.5,9H22C22,9 22,15 16,15C12,15 10.5,11.5 8.5,11.5C4.5,11.5 4.5,15 4.5,15H2" />
+</vector>
\ No newline at end of file




diff --git a/app/src/main/res/drawable/info.xml b/app/src/main/res/drawable/info.xml
new file mode 100644
index 0000000000000000000000000000000000000000..2131db7742b29fd42abe5a1bf96f90e2233eadae
--- /dev/null
+++ b/app/src/main/res/drawable/info.xml
@@ -0,0 +1,18 @@
+<!--
+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:tint="?attr/colorOnSurface"
+	android:viewportWidth="24"
+	android:viewportHeight="24">
+
+	<path
+		android:fillColor="@android:color/white"
+		android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM13,17h-2v-6h2v6zM13,9h-2L11,7h2v2z" />
+
+</vector>




diff --git a/app/src/main/res/drawable/matrix.xml b/app/src/main/res/drawable/matrix.xml
new file mode 100644
index 0000000000000000000000000000000000000000..54644fae698519a4c776de8dd92c0fd1e3e09053
--- /dev/null
+++ b/app/src/main/res/drawable/matrix.xml
@@ -0,0 +1,15 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="48dp"
+    android:height="48dp"
+    android:viewportWidth="48"
+    android:viewportHeight="48">
+  <path
+      android:pathData="M37.082,39.234L37.082,8.734h-2.19v-0.732h3.04v32h-3.04v-0.732z"
+      android:fillColor="#040404"/>
+  <path
+      android:pathData="m18.212,18.434v1.54h0.044c0.385,-0.564 0.893,-1.03 1.49,-1.37 0.58,-0.323 1.25,-0.485 1.99,-0.485 0.72,0 1.38,0.14 1.97,0.42 0.595,0.279 1.05,0.771 1.36,1.48 0.338,-0.5 0.796,-0.941 1.38,-1.32 0.58,-0.383 1.27,-0.574 2.06,-0.574 0.602,0 1.16,0.074 1.67,0.22 0.514,0.148 0.954,0.383 1.32,0.707 0.366,0.323 0.653,0.746 0.859,1.27 0.205,0.522 0.308,1.15 0.308,1.89v7.63h-3.13v-6.46c0,-0.383 -0.015,-0.743 -0.044,-1.08 -0.021,-0.307 -0.103,-0.607 -0.242,-0.882 -0.133,-0.251 -0.336,-0.458 -0.584,-0.596 -0.257,-0.146 -0.606,-0.22 -1.05,-0.22 -0.44,0 -0.796,0.085 -1.07,0.253 -0.272,0.17 -0.485,0.39 -0.639,0.662 -0.159,0.287 -0.264,0.602 -0.308,0.927 -0.052,0.347 -0.078,0.697 -0.078,1.05v6.35h-3.13v-6.4c0,-0.338 -0.007,-0.673 -0.021,-1 -0.011,-0.314 -0.075,-0.623 -0.188,-0.916 -0.108,-0.277 -0.3,-0.512 -0.55,-0.673 -0.258,-0.168 -0.636,-0.253 -1.14,-0.253 -0.198,0.008 -0.394,0.042 -0.584,0.1 -0.258,0.075 -0.498,0.202 -0.705,0.374 -0.228,0.184 -0.422,0.449 -0.584,0.794 -0.161,0.346 -0.242,0.798 -0.242,1.36v6.62h-3.13v-11.4z"
+      android:fillColor="#040404"/>
+  <path
+      android:pathData="m10.918,8.766v30.5h2.19v0.732h-3.04v-32h3.03v0.732z"
+      android:fillColor="#040404"/>
+</vector>




diff --git a/app/src/main/res/drawable/send.xml b/app/src/main/res/drawable/send.xml
new file mode 100644
index 0000000000000000000000000000000000000000..42caa11f3d69d9e70a3b73d5f03d617c349af2cc
--- /dev/null
+++ b/app/src/main/res/drawable/send.xml
@@ -0,0 +1,19 @@
+<!--
+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:autoMirrored="true"
+	android:tint="#000000"
+	android:viewportWidth="24"
+	android:viewportHeight="24">
+
+	<path
+		android:fillColor="@android:color/white"
+		android:pathData="M2.01,21L23,12 2.01,3 2,10l15,2 -15,2z" />
+
+</vector>




diff --git a/app/src/main/res/drawable/settings.xml b/app/src/main/res/drawable/settings.xml
new file mode 100644
index 0000000000000000000000000000000000000000..e363b3e6e6567aa5db2c040f4432ddd803247b10
--- /dev/null
+++ b/app/src/main/res/drawable/settings.xml
@@ -0,0 +1,18 @@
+<!--
+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:tint="?attr/colorOnSurface"
+	android:viewportWidth="24"
+	android:viewportHeight="24">
+
+	<path
+		android:fillColor="@android:color/white"
+		android:pathData="M19.14,12.94c0.04,-0.3 0.06,-0.61 0.06,-0.94c0,-0.32 -0.02,-0.64 -0.07,-0.94l2.03,-1.58c0.18,-0.14 0.23,-0.41 0.12,-0.61l-1.92,-3.32c-0.12,-0.22 -0.37,-0.29 -0.59,-0.22l-2.39,0.96c-0.5,-0.38 -1.03,-0.7 -1.62,-0.94L14.4,2.81c-0.04,-0.24 -0.24,-0.41 -0.48,-0.41h-3.84c-0.24,0 -0.43,0.17 -0.47,0.41L9.25,5.35C8.66,5.59 8.12,5.92 7.63,6.29L5.24,5.33c-0.22,-0.08 -0.47,0 -0.59,0.22L2.74,8.87C2.62,9.08 2.66,9.34 2.86,9.48l2.03,1.58C4.84,11.36 4.8,11.69 4.8,12s0.02,0.64 0.07,0.94l-2.03,1.58c-0.18,0.14 -0.23,0.41 -0.12,0.61l1.92,3.32c0.12,0.22 0.37,0.29 0.59,0.22l2.39,-0.96c0.5,0.38 1.03,0.7 1.62,0.94l0.36,2.54c0.05,0.24 0.24,0.41 0.48,0.41h3.84c0.24,0 0.44,-0.17 0.47,-0.41l0.36,-2.54c0.59,-0.24 1.13,-0.56 1.62,-0.94l2.39,0.96c0.22,0.08 0.47,0 0.59,-0.22l1.92,-3.32c0.12,-0.22 0.07,-0.47 -0.12,-0.61L19.14,12.94zM12,15.6c-1.98,0 -3.6,-1.62 -3.6,-3.6s1.62,-3.6 3.6,-3.6s3.6,1.62 3.6,3.6S13.98,15.6 12,15.6z" />
+
+</vector>




diff --git a/app/src/main/res/drawable/speed.xml b/app/src/main/res/drawable/speed.xml
index 67e8c0ca8f57a67bc7480bc1d8ba887928814491..139f3480d252286fa5ef0522b2d89b0f5300426c 100644
--- a/app/src/main/res/drawable/speed.xml
+++ b/app/src/main/res/drawable/speed.xml
@@ -4,8 +4,13 @@
 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="M20.38,8.57l-1.23,1.85a8,8 0,0 1,-0.22 7.58L5.07,18A8,8 0,0 1,15.58 6.85l1.85,-1.23A10,10 0,0 0,3.35 19a2,2 0,0 0,1.72 1h13.85a2,2 0,0 0,1.74 -1,10 10,0 0,0 -0.27,-10.44zM10.59,15.41a2,2 0,0 0,2.83 0l5.66,-8.49 -8.49,5.66a2,2 0,0 0,0 2.83z"/>
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+	android:width="24dp"
+	android:height="24dp"
+	android:tint="?attr/colorOnSurface"
+	android:viewportWidth="24"
+	android:viewportHeight="24">
+	<path
+		android:fillColor="@android:color/white"
+		android:pathData="M20.38,8.57l-1.23,1.85a8,8 0,0 1,-0.22 7.58L5.07,18A8,8 0,0 1,15.58 6.85l1.85,-1.23A10,10 0,0 0,3.35 19a2,2 0,0 0,1.72 1h13.85a2,2 0,0 0,1.74 -1,10 10,0 0,0 -0.27,-10.44zM10.59,15.41a2,2 0,0 0,2.83 0l5.66,-8.49 -8.49,5.66a2,2 0,0 0,0 2.83z" />
 </vector>




diff --git a/app/src/main/res/drawable/terminus.xml b/app/src/main/res/drawable/terminus.xml
new file mode 100644
index 0000000000000000000000000000000000000000..277c549dbaf8031cff5931858c75b49b0c1efe3e
--- /dev/null
+++ b/app/src/main/res/drawable/terminus.xml
@@ -0,0 +1,17 @@
+<!--
+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:tint="?attr/colorOnSurface"
+	android:viewportWidth="24"
+	android:viewportHeight="24">
+
+	<path
+		android:fillColor="@android:color/white"
+		android:pathData="M21,12v-2h-2V7l-3,-3l-2,2l-2,-2l-2,2L8,4L5,7v3H3v2h2v2H3v2h2v4h14v-4h2v-2h-2v-2H21zM16,6.83l1,1V10h-2V7.83l0.41,-0.41L16,6.83zM12,6.83l0.59,0.59L13,7.83V10h-2V7.83l0.41,-0.41L12,6.83zM11,14v-2h2v2H11zM13,16v2h-2v-2H13zM7,7.83l1,-1l0.59,0.59L9,7.83V10H7V7.83zM7,12h2v2H7V12zM7,16h2v2H7V16zM17,18h-2v-2h2V18zM17,14h-2v-2h2V14z" />
+
+</vector>




diff --git a/app/src/main/res/drawable/vehicle.xml b/app/src/main/res/drawable/vehicle.xml
new file mode 100644
index 0000000000000000000000000000000000000000..6f99d217d5e9cf41d2ea910980fd338244a1b1f0
--- /dev/null
+++ b/app/src/main/res/drawable/vehicle.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="M12,4L5,4C3.34,4 2,5.34 2,7v8c0,1.66 1.34,3 3,3l-1,1v1h1l2,-2.03L9,18v-5L4,13L4,5.98L13,6v2h2L15,7c0,-1.66 -1.34,-3 -3,-3zM5,14c0.55,0 1,0.45 1,1s-0.45,1 -1,1 -1,-0.45 -1,-1 0.45,-1 1,-1zM20.57,9.66c-0.14,-0.4 -0.52,-0.66 -0.97,-0.66h-7.19c-0.46,0 -0.83,0.26 -0.98,0.66L10,13.77l0.01,5.51c0,0.38 0.31,0.72 0.69,0.72h0.62c0.38,0 0.68,-0.38 0.68,-0.76L12,18h8v1.24c0,0.38 0.31,0.76 0.69,0.76h0.61c0.38,0 0.69,-0.34 0.69,-0.72l0.01,-1.37v-4.14l-1.43,-4.11zM12.41,10h7.19l1.03,3h-9.25l1.03,-3zM12,16c-0.55,0 -1,-0.45 -1,-1s0.45,-1 1,-1 1,0.45 1,1 -0.45,1 -1,1zM20,16c-0.55,0 -1,-0.45 -1,-1s0.45,-1 1,-1 1,0.45 1,1 -0.45,1 -1,1z"/>
+</vector>




diff --git a/app/src/main/res/layout/activity_about.xml b/app/src/main/res/layout/activity_about.xml
index cd8babd727c8641214ec665d3005151cf3f12d2b..fdd8cf829328cce044bb0346f7e66d6d2fa0d5d9 100644
--- a/app/src/main/res/layout/activity_about.xml
+++ b/app/src/main/res/layout/activity_about.xml
@@ -66,7 +66,7 @@         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:constraint_referenced_ids="website,code,translate,mastodon,matrix,email"
         app:flow_horizontalGap="16dp"
         app:flow_horizontalStyle="packed"
         app:flow_verticalGap="4dp"
@@ -110,6 +110,24 @@         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
         tools:ignore="MissingConstraints"
         app:icon="@drawable/translate" />
+
+    <Button
+      android:contentDescription="@string/matrix_button_description"
+      android:id="@+id/matrix"
+      style="@style/Widget.Material3.Button.IconButton"
+      android:layout_width="wrap_content"
+      android:layout_height="wrap_content"
+      tools:ignore="MissingConstraints"
+      app:icon="@drawable/matrix" />
+
+    <Button
+      android:contentDescription="@string/email_button_description"
+      android:id="@+id/email"
+      style="@style/Widget.Material3.Button.IconButton"
+      android:layout_width="wrap_content"
+      android:layout_height="wrap_content"
+      tools:ignore="MissingConstraints"
+      app:icon="@drawable/email" />
 
     <TextView
         android:id="@+id/credits"




diff --git a/app/src/main/res/layout/alert.xml b/app/src/main/res/layout/alert.xml
index 643fd32e34735a592742d869500910aa13314b87..56819d0fd9f418fd6c0e896db130906251530f78 100644
--- a/app/src/main/res/layout/alert.xml
+++ b/app/src/main/res/layout/alert.xml
@@ -34,7 +34,7 @@ 			android:layout_marginStart="8dp"
 			android:layout_marginTop="8dp"
 			android:layout_marginEnd="8dp"
 			android:ellipsize="end"
-			android:maxLines="2"
+			android:maxLines="3"
 			android:textColor="@color/black"
 			app:layout_constraintBottom_toBottomOf="parent"
 			app:layout_constraintEnd_toStartOf="@+id/more_button"




diff --git a/app/src/main/res/layout/departure.xml b/app/src/main/res/layout/departure.xml
index 834cbe0773dbb70fe4aa6a9c8f26ac4d5b114224..b2514262349aef0d448fed775a6116b4042ece6d 100644
--- a/app/src/main/res/layout/departure.xml
+++ b/app/src/main/res/layout/departure.xml
@@ -37,14 +37,16 @@ 		tool:text="1hr" />
 
 	<com.google.android.material.textview.MaterialTextView
 		android:id="@+id/departure_line"
-		android:layout_width="wrap_content"
+		android:layout_width="0dp"
 		android:layout_height="wrap_content"
 		android:layout_marginStart="8dp"
 		android:layout_marginTop="8dp"
+		android:layout_marginEnd="8dp"
 		android:textAppearance="@style/TextAppearance.Material3.HeadlineSmall"
 		app:layout_constraintStart_toEndOf="@+id/line_icon"
 		app:layout_constraintTop_toTopOf="parent"
-		tool:text="Metropolitan" />
+		app:layout_constraintEnd_toStartOf="@id/departure_time"
+		tool:text="Circle" />
 
 	<com.google.android.material.textview.MaterialTextView
 		android:id="@+id/departure_headsign"
@@ -55,5 +57,13 @@ 		app:layout_constraintStart_toStartOf="@+id/departure_line"
 		app:layout_constraintTop_toBottomOf="@+id/departure_line"
 		tool:text="» Tower Hill" />
 
-
+	<ImageView
+		android:id="@+id/time_status"
+		android:layout_width="12dp"
+		android:layout_height="12dp"
+		android:layout_marginEnd="8dp"
+		app:layout_constraintEnd_toEndOf="parent"
+		app:layout_constraintTop_toBottomOf="@+id/departure_time"
+		tool:ignore="ContentDescription"
+		tool:srcCompat="@drawable/inexact" />
 </androidx.constraintlayout.widget.ConstraintLayout>
\ No newline at end of file




diff --git a/app/src/main/res/layout/result.xml b/app/src/main/res/layout/result.xml
index e7dd45553043a5ffad0cfceb1ed5fc71ace8b0b6..76614ac6dfa60c71c84bfc0c97448af7c18426af 100644
--- a/app/src/main/res/layout/result.xml
+++ b/app/src/main/res/layout/result.xml
@@ -78,13 +78,19 @@ 		android:id="@+id/suggestion_description"
 		style="@style/Theme.Bimba.SearchResult.Description"
 		android:layout_width="0dp"
 		android:layout_height="wrap_content"
-		android:maxWidth="360dp"
 		android:layout_marginTop="4dp"
 		android:layout_marginEnd="4dp"
+		android:includeFontPadding="false"
+		android:lineSpacingMultiplier="1.6"
+		android:maxWidth="360dp"
+		android:paddingTop="4dp"
+		android:paddingBottom="4dp"
 		app:layout_constraintBottom_toBottomOf="parent"
 		app:layout_constraintEnd_toStartOf="@+id/distance"
+		app:layout_constraintHorizontal_bias="0.0"
 		app:layout_constraintStart_toStartOf="@+id/suggestion_title"
 		app:layout_constraintTop_toBottomOf="@+id/feed_name"
+		app:layout_constraintVertical_bias="1.0"
 		tool:text="Metropolitan » Baker Street, Tower Hill The Monument, Westminster, Piccadilly Circus, Oxford Street" />
 
 </androidx.constraintlayout.widget.ConstraintLayout>
\ No newline at end of file




diff --git a/app/src/main/res/layout-land/activity_about.xml b/app/src/main/res/layout-land/activity_about.xml
index 6e1131b7366a7fa3430448c54fc8f7ad38b78368..8872139c2754c5f6ec8afe32c2c692a107e3ab80 100644
--- a/app/src/main/res/layout-land/activity_about.xml
+++ b/app/src/main/res/layout-land/activity_about.xml
@@ -66,7 +66,7 @@ 		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:constraint_referenced_ids="website,code,translate,mastodon,matrix,email"
 		app:flow_horizontalGap="16dp"
 		app:flow_horizontalStyle="packed"
 		app:flow_verticalGap="4dp"
@@ -110,6 +110,24 @@ 		android:layout_width="wrap_content"
 		android:layout_height="wrap_content"
 		tools:ignore="MissingConstraints"
 		app:icon="@drawable/translate" />
+
+	<Button
+		android:contentDescription="@string/matrix_button_description"
+		android:id="@+id/matrix"
+		style="@style/Widget.Material3.Button.IconButton"
+		android:layout_width="wrap_content"
+		android:layout_height="wrap_content"
+		tools:ignore="MissingConstraints"
+		app:icon="@drawable/matrix" />
+
+	<Button
+		android:contentDescription="@string/email_button_description"
+		android:id="@+id/email"
+		style="@style/Widget.Material3.Button.IconButton"
+		android:layout_width="wrap_content"
+		android:layout_height="wrap_content"
+		tools:ignore="MissingConstraints"
+		app:icon="@drawable/email" />
 
 	<androidx.constraintlayout.widget.Guideline
 		android:id="@+id/middle"




diff --git a/app/src/main/res/menu/departures_menu.xml b/app/src/main/res/menu/departures_menu.xml
index 0c3f27a935ef2b0bca568758f44f8d641d5d3c6c..9aef4cf6ed99cb06a663eeb16dedd94e4145a664 100644
--- a/app/src/main/res/menu/departures_menu.xml
+++ b/app/src/main/res/menu/departures_menu.xml
@@ -39,4 +39,12 @@ 		android:icon="@drawable/calendar"
 		app:showAsAction="ifRoom"
 		android:contentDescription="@string/title_select_date"
 		android:title="@string/title_select_date"/>
+	<!-- TODO elizabeth
+	<item
+		android:id="@+id/terminus_arrival_showing"
+		android:icon="@drawable/terminus"
+		app:showAsAction="ifRoom"
+		android:contentDescription="@string/terminus_arrival_showing"
+		android:title="@string/terminus_arrival_showing"
+		/>-->
 </menu>




diff --git a/app/src/main/res/menu/drawer.xml b/app/src/main/res/menu/drawer.xml
index 1d876c0cf374716191449dcfaf664152687c4623..5a5194c75d78419d3249ea66b2dc7a1e1354d202 100644
--- a/app/src/main/res/menu/drawer.xml
+++ b/app/src/main/res/menu/drawer.xml
@@ -25,9 +25,11 @@ 		
 	</item>
 	<!-- other settings -->
 	<item
+		android:icon="@drawable/settings"
 	android:id="@+id/drawer_settings"
 	android:title="@string/title_settings"/>
 	<item
+		android:icon="@drawable/info"
 		android:id="@+id/drawer_about"
 		android:title="@string/title_about"/>
 </menu>
\ No newline at end of file




diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml
index 3b5e70a97a94517eebe0bf7f8d1b7273fd45f931..cf156aef73eedc2bde7a2d851b0b81fcf8842ac1 100644
--- a/app/src/main/res/values/arrays.xml
+++ b/app/src/main/res/values/arrays.xml
@@ -46,4 +46,28 @@ 		@string/neen
 		<item>@string/dex</item>
 		<item>@string/lef</item>
 	</string-array>
+
+	<string-array name="line_decorations">
+		<item>@string/italics</item>
+		<!--<item>@string/colour</item>-->
+		<item>@string/none</item>
+	</string-array>
+
+	<string-array name="line_decorations_values">
+		<item>italics</item>
+		<!--<item>colour</item>-->
+		<item>none</item>
+	</string-array>
+
+	<string-array name="terminus_arrival_showing">
+		<item>@string/grey_out</item>
+		<item>@string/hide</item>
+		<item>@string/show</item>
+	</string-array>
+
+	<string-array name="terminus_arrival_showing_values">
+		<item>grey_out</item>
+		<item>hide</item>
+		<item>show</item>
+	</string-array>
 </resources>
\ No newline at end of file




diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 8e5ca43fe47402d84cefdf881ac378ccd664757d..0f28969a1ce9661561811d143e015aedfe141d66 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -23,13 +23,13 @@ 	Not found
 	<string name="error_429">Rate limit exceeded. Try again later</string>
 	<string name="error_50x">There was an error on the sever. Try again later</string>
 	<string name="error_unknown">Unknown error happened</string>
-	<!-- send a bug report to bimba@git.apiote.xyz, details are: url=$URL, response=$response -->
+	<!-- send a bug report to questions@bimba.app, details are: url=$URL, response=$response -->
 	<string name="error_connecting">Error connecting to the server. Try again later</string>
 	<string name="error_offline">You are offline. Connect to the Internet</string>
 	<string name="error_gps">Cannot obtain current location</string>
 	<string name="no_departures">No departures</string>
 	<string name="waiting_position">waiting for position</string>
-	<string name="vehicle_headsign">%1$s » %2$s</string>
+	<string name="vehicle_headsign"><annotation decoration="apply"><annotation arg="0">%1$s</annotation></annotation> » <annotation arg="1">%2$s</annotation></string>
 	<string name="distance_in_m">%1$s m</string>
 	<plurals name="distance_in_m_cd">
 		<item quantity="one">%1$d metre</item>
@@ -126,11 +126,13 @@ 	crowded
 	<string name="occupancy_full">full</string>
 	<string name="occupancy_wont_let">won’t let in</string>
 	<string name="no_map_app">No maps app installed</string>
+	<string name="no_email_app">No email app installed</string>
 	<string name="departure_headsign_content_description">towards %1$s</string>
 	<string name="departure_momentarily">momentarily</string>
 	<string name="departure_departed">departed</string>
 	<string name="departure_now">now</string>
 	<string name="at_time">at %1$02d:%2$02d</string>
+	<string name="about_time">about %1$02d:%2$02d</string>
 	<string name="at_time_realtime">at %1$02d:%2$02d:%3$02d</string>
 	<string name="on_demand">on demand</string>
 	<string name="no_boarding">no boarding</string>
@@ -146,6 +148,10 @@ 	Results for ‘%1$s’
 	<string name="bimba_server_address_hint">Server</string>
 	<string name="bimba_server_token_hint">Token</string>
 	<string name="realtime_content_description">departure is realtime</string>
+	<!-- cf timepoint field in https://gtfs.org/schedule/reference/#stop_timestxt -->
+	<string name="exact_content_description">departure time is exact from schedule</string>
+	<!-- cf timepoint field in https://gtfs.org/schedule/reference/#stop_timestxt -->
+	<string name="inexact_content_description">departure time is approximate from schedule</string>
 	<string name="wheelchair_content_description">vehicle is wheelchair accessible</string>
 	<string name="air_condition_content_description">air conditioning</string>
 	<string name="bicycles_allowed_content_description">bicycles allowed</string>
@@ -183,7 +189,7 @@ 	Stop on demand
 	<string name="stop_stub_in_zone">Stop in zone %1$s</string>
 	<string name="vehicle_headsign_content_description">%1$s towards %2$s</string>
 	<string name="departure_headsign">» %1$s</string>
-	<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 (https://www.openstreetmap.org/copyright), ODbL-1.0\n\n Cities list used for geocoding short plus codes © Geonames (https://geonames.org), CC BY</string>
+	<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 (https://www.openstreetmap.org/copyright), ODbL-1.0\n\n Cities list used for geocoding short plus codes © Geonames (https://geonames.org), CC BY\n\n Matrix logo ™/® Matrix.org</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>
@@ -259,4 +265,24 @@ 	neen
 	<string name="dex">dex</string>
 	<string name="lef">lef</string>
 	<string name="filtered_stop_question">Do you want to save a favourite filtered with selected lines?</string>
+	<string name="none">none</string>
+	<string name="italics">italics</string>
+	<string name="colour">colour</string>
+	<string name="line_decorations">Line name decorations</string>
+	<string name="acra_notification_channel">Crash reports channel</string>
+	<string name="acra_notification_channel_description">Notifications showing crashes and allowing sending crash reports</string>
+	<string name="acra_notification_title">Bimba has crashed</string>
+	<string name="acra_notification_text">An unexpected obstruction has shown up on Bimba’s way. Do you want to send a report?</string>
+	<string name="send">Send</string>
+	<string name="discard">Discard</string>
+	<string name="send_with_comment">Send with commend</string>
+	<string name="acra_notification_comment">Comment added to crash report</string>
+	<string name="filtered_departures">Filtered departures</string>
+	<string name="alerts">Alerts</string>
+	<string name="grey_out">grey out</string>
+	<string name="hide">hide</string>
+	<string name="show">show</string>
+	<string name="terminus_arrival_showing">Terminus arrivals</string>
+	<string name="matrix_button_description">link to Matrix channel</string>
+	<string name="email_button_description">link to email</string>
 </resources>




diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml
index 0cd1a0dacf54562ef83c5f01f304f305c18a6ec6..23ea65341109cbdcd89d71536547c76ec2212e18 100644
--- a/app/src/main/res/values-de/strings.xml
+++ b/app/src/main/res/values-de/strings.xml
@@ -9,7 +9,7 @@     Karte
     <string name="home_fab_description">GPS-Symbol</string>
     <string name="title_activity_results">Ergebnisse</string>
     <string name="continue_">Fortsetzen</string>
-    <string name="vehicle_headsign">%1$s » %2$s</string>
+    <string name="vehicle_headsign"><annotation decoration="apply"><annotation arg="0">%1$s</annotation></annotation> » <annotation arg="1">%2$s</annotation></string>
     <string name="speed_in_km_per_h">%1$s km/h</string>
     <string name="congestion_unknown">unbekannt</string>
     <string name="congestion_congestion">Stau</string>




diff --git a/app/src/main/res/values-en-rGB/strings.xml b/app/src/main/res/values-en-rGB/strings.xml
new file mode 100644
index 0000000000000000000000000000000000000000..ed15c05948b2e42e983638a24f3c885597f30c96
--- /dev/null
+++ b/app/src/main/res/values-en-rGB/strings.xml
@@ -0,0 +1,286 @@
+<!--
+SPDX-FileCopyrightText: Adam Evyčędo and contributors using Weblate
+
+SPDX-License-Identifier: GPL-3.0-or-later
+-->
+
+<resources>
+	<string name="app_name">Bimba</string>
+	<string name="title_home">Home</string>
+	<string name="title_map">Map</string>
+	<string name="title_journey">Journey</string>
+	<string name="home_fab_description">GPS icon</string>
+	<string name="search_placeholder">stops, lines, or plus codes</string>
+	<string name="title_activity_results">Results</string>
+	<string name="continue_">Continue</string>
+	<string name="save">Save</string>
+	<string name="error_400">The application made a malformed request</string>
+	<string name="error_401">A token is needed to use this server</string>
+	<string name="error_403">The token you provided is incorrect</string>
+	<string name="error_404">Not found</string>
+	<string name="error_429">Rate limit exceeded. Try again later</string>
+	<string name="error_50x">There was an error on the sever. Try again later</string>
+	<string name="error_unknown">Unknown error happened</string>
+	<!-- send a bug report to questions@bimba.app, details are: url=$URL, response=$response -->
+	<string name="error_connecting">Error connecting to the server. Try again later</string>
+	<string name="error_offline">You are offline. Connect to the Internet</string>
+	<string name="error_gps">Cannot obtain current location</string>
+	<string name="no_departures">No departures</string>
+	<string name="waiting_position">waiting for position</string>
+	<string name="vehicle_headsign"><annotation decoration="apply"><annotation arg="0">%1$s</annotation></annotation> » <annotation arg="1">%2$s</annotation></string>
+	<string name="distance_in_m">%1$s m</string>
+	<plurals name="distance_in_m_cd">
+		<item quantity="one">%1$d metre</item>
+		<item quantity="other">%1$d metres</item>
+	</plurals>
+	<string name="distance_in_km">%1$s km</string>
+	<plurals name="distance_in_km_cd">
+		<item quantity="one">%1$d kilometre</item>
+		<item quantity="other">%1$d kilometres</item>
+	</plurals>
+	<string name="distance_in_two_units_cd">%1$s and %2$s</string>
+	<string name="distance_in_yd">%1$s yd</string>
+	<plurals name="distance_in_yd_cd">
+		<item quantity="one">%1$d yard</item>
+		<item quantity="other">%1$d yards</item>
+	</plurals>
+	<string name="distance_in_ft">%1$s ft</string>
+	<plurals name="distance_in_ft_cd">
+		<item quantity="one">%1$d foot</item>
+		<item quantity="other">%1$d feet</item>
+	</plurals>
+	<string name="distance_in_mi">%1$s mi</string>
+	<plurals name="distance_in_mi_cd">
+		<item quantity="one">%1$d mile</item>
+		<item quantity="other">%1$d miles</item>
+	</plurals>
+	<string name="distance_in_gf">%1$s %2$sGf</string>
+	<plurals name="distance_in_gf_cd">
+		<item quantity="one">%1$d grafut</item>
+		<item quantity="other">%1$d grafuts</item>
+	</plurals>
+	<plurals name="distance_in_3gf_12_cd">
+		<item quantity="one">%1$s trinagrafut</item>
+		<item quantity="other">%1$s trinagrafuts</item>
+	</plurals>
+	<plurals name="distance_in_gf_12_cd">
+		<item quantity="one">%1$s grafut</item>
+		<item quantity="other">%1$s grafuts</item>
+	</plurals>
+	<string name="time_in_s">%1$s s</string>
+	<plurals name="time_in_s_cd">
+		<item quantity="one">%1$d second</item>
+		<item quantity="other">%1$d seconds</item>
+	</plurals>
+	<string name="time_in_tm">%1$s %2$sTm</string>
+	<string name="time_in_tm_past">%1$s %2$sTm ago</string>
+	<plurals name="time_in_tm_cd">
+		<item quantity="one">%1$d tim</item>
+		<item quantity="other">%1$d tims</item>
+	</plurals>
+	<plurals name="time_in_4tm_12_cd">
+		<item quantity="one">%1$s quedratim</item>
+		<item quantity="other">%1$s quedratims</item>
+	</plurals>
+	<plurals name="time_in_2tm_12_cd">
+		<item quantity="one">%1$s dunatim</item>
+		<item quantity="other">%1$s dunatims</item>
+	</plurals>
+	<string name="speed_in_km_per_h">%1$s km/h</string>
+	<string name="speed_in_m_per_s">%1$s m/s</string>
+	<string name="speed_in_mi_per_h">%1$s mph</string>
+	<string name="speed_in_vl">%1$s Vl</string>
+	<plurals name="speed_in_m_per_s_cd">
+		<item quantity="one">%1$d meter per second</item>
+		<item quantity="other">%1$d meters per second</item>
+	</plurals>
+	<plurals name="speed_in_km_per_h_cd">
+		<item quantity="one">%1$d kilometer per hour</item>
+		<item quantity="other">%1$d kilometers per hour</item>
+	</plurals>
+	<plurals name="speed_in_mi_per_h_cd">
+		<item quantity="one">%1$d mile per hour</item>
+		<item quantity="other">%1$d mile per hour</item>
+	</plurals>
+	<plurals name="speed_in_vl_cd">
+		<item quantity="one">%1$d vlos</item>
+		<item quantity="other">%1$d vlos</item>
+	</plurals>
+	<plurals name="speed_in_vl_12_cd">
+		<item quantity="one">%1$s vlos</item>
+		<item quantity="other">%1$s vlos</item>
+	</plurals>
+	<string name="congestion_unknown">unknown</string>
+	<string name="congestion_smooth">smooth</string>
+	<string name="congestion_stop_and_go">stop and go</string>
+	<string name="congestion_congestion">congestion</string>
+	<string name="congestion_jams">jams</string>
+	<string name="occupancy_unknown">unknown</string>
+	<string name="occupancy_empty">empty</string>
+	<string name="occupancy_many_seats">many seats</string>
+	<string name="occupancy_few_seats">few seats</string>
+	<string name="occupancy_standing_only">standing only</string>
+	<string name="occupancy_crowded">crowded</string>
+	<string name="occupancy_full">full</string>
+	<string name="occupancy_wont_let">won’t let in</string>
+	<string name="no_map_app">No maps app installed</string>
+	<string name="no_email_app">No email app installed</string>
+	<string name="departure_headsign_content_description">towards %1$s</string>
+	<string name="departure_momentarily">momentarily</string>
+	<string name="departure_departed">departed</string>
+	<string name="departure_now">now</string>
+	<string name="at_time">at %1$02d:%2$02d</string>
+	<string name="about_time">about %1$02d:%2$02d</string>
+	<string name="at_time_realtime">at %1$02d:%2$02d:%3$02d</string>
+	<string name="on_demand">on demand</string>
+	<string name="no_boarding">no boarding</string>
+	<string name="on_boarding">on-boarding</string>
+	<string name="off_boarding">off-boarding</string>
+	<string name="boarding">can board</string>
+	<string name="line_headsign">» %1$s</string>
+	<string name="line_headsign_content_description">towards %1$s</string>
+	<string name="line_headsigns">%1$s «» %2$s</string>
+	<string name="line_headsigns_content_description">between %1$s and %2$s</string>
+	<string name="stops_nearby">Stops nearby</string>
+	<string name="results_for">Results for ‘%1$s’</string>
+	<string name="bimba_server_address_hint">Server</string>
+	<string name="bimba_server_token_hint">Token</string>
+	<string name="realtime_content_description">departure is realtime</string>
+	<!-- cf timepoint field in https://gtfs.org/schedule/reference/#stop_timestxt -->
+	<string name="exact_content_description">departure time is exact from schedule</string>
+	<!-- cf timepoint field in https://gtfs.org/schedule/reference/#stop_timestxt -->
+	<string name="inexact_content_description">departure time is approximate from schedule</string>
+	<string name="wheelchair_content_description">vehicle is wheelchair accessible</string>
+	<string name="air_condition_content_description">air conditioning</string>
+	<string name="bicycles_allowed_content_description">bicycles allowed</string>
+	<string name="voice_announcements_content_description">voice announcements</string>
+	<string name="tickets_sold_content_description">tickets sold on board</string>
+	<string name="usb_charging_content_description">USB charging</string>
+	<string name="show_departures">Show departures</string>
+	<string name="open_in_maps_app">Open in maps app</string>
+	<string name="stop_content_description">stop</string>
+	<!-- taken from ‘Magic School Bus’. Should be translated like in the series. It’s the first words of the intro song -->
+	<string name="seatbelts_everyone">Seatbelts, everyone!</string>
+	<string name="onboarding_question">How would you like to start?</string>
+	<string name="onboarding_simple">Simple</string>
+	<string name="onboarding_simple_action">choose localities</string>
+	<string name="onboarding_advanced">Advanced</string>
+	<string name="onboarding_advanced_action">choose server</string>
+	<string name="cancel">Cancel</string>
+	<string name="error">Error</string>
+	<string name="rate_limit">Rate limit</string>
+	<string name="server_rate_limited_question">This server is rate-limited and no token was given. Do you want to continue?</string>
+	<string name="server_private_question">This server is private and no token was given</string>
+	<string name="last_update">Last update: %1$s</string>
+	<string name="title_feeds">Timetables</string>
+	<string name="title_servers">Servers</string>
+	<string name="title_cities">Localities</string>
+	<string name="error_url">Malformed URL provided</string>
+	<string name="error_traffic_spec">Cannot verify server</string>
+	<string name="stops_near_code">Stops near %1$s</string>
+	<string name="choose_server">Choose server flavour</string>
+	<string name="ok">OK</string>
+	<string name="no_location_access">Location access not given</string>
+	<string name="no_location_message">Permission to use location is needed to find nearby stops and show current position on map. Other features will work without it. It can be enabled and disabled in system settings any time.</string>
+	<string name="stop_stub_on_demand_in_zone">Stop on demand in zone %1$s</string>
+	<string name="stop_stub_on_demand">Stop on demand</string>
+	<string name="stop_stub_in_zone">Stop in zone %1$s</string>
+	<string name="vehicle_headsign_content_description">%1$s towards %2$s</string>
+	<string name="departure_headsign">» %1$s</string>
+	<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 (https://www.openstreetmap.org/copyright), ODbL-1.0\n\n Cities list used for geocoding short plus codes © Geonames (https://geonames.org), CC BY\n\n Matrix logo ™/® Matrix.org</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="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>
+	<string name="use_online_feed">Use online feed</string>
+	<string name="information_may_be_outdated">Information may be outdated</string>
+	<string name="current_timetable_validity">Current timetable valid: %1$s to %2$s</string>
+	<string name="error_406">App version is not compatible with the server</string>
+	<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="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>
+	<string name="map_attribution"><![CDATA[© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors]]></string>
+	<string name="favourite_content_description">Save as favourite</string>
+	<string name="favourite">Favourite</string>
+	<string name="filtered">Filtered</string>
+	<string name="unfiltered">Unfiltered</string>
+	<string name="cannot_save_favourite">Couldn’t save the favourite</string>
+	<string name="error_44">No more departures</string>
+	<string name="loading">loading…</string>
+	<string name="favourite_deleted">Favourite deleted</string>
+	<string name="undo">Undo</string>
+	<string name="units_title">Unit system</string>
+	<string name="units_locale_based">Locale based</string>
+	<string name="units_metric">Metric (SI)</string>
+	<string name="units_imperial">Imperial (UK)</string>
+	<string name="units_customary">US Customary</string>
+	<string name="units_tgm10">TGM (base 10)</string>
+	<string name="units_tgm12">TGM (base 12)</string>
+	<string name="title_settings">Settings</string>
+	<string name="no_geocoding_data">No geocoding data</string>
+	<string name="no_geocoding_data_description">The query contains a short plus code but there is no geocoding data present. Download geocoding data or enable auto updating in settings.</string>
+	<string name="error_geocoding">City not found</string>
+	<string name="cities_channel_name">Cities update channel</string>
+	<string name="cities_channel_description">Notifications showing progress of updating geocoding local data</string>
+	<string name="saving_cities_list">saving cities list</string>
+	<string name="updating_geocoding_data">Updating geocoding data</string>
+	<string name="downloading_cities_list">downloading cities list</string>
+	<string name="finished_updating_geocoding_data">Finished updating geocoding data</string>
+	<string name="updating_geocoding_data_failed">Updating geocoding data failed</string>
+	<string name="zero">zero</string>
+	<string name="one">one</string>
+	<string name="two">two</string>
+	<string name="three">three</string>
+	<string name="four">four</string>
+	<string name="five">five</string>
+	<string name="six">six</string>
+	<string name="seven">seven</string>
+	<string name="eight">eight</string>
+	<string name="nine">nine</string>
+	<string name="ten">ten</string>
+	<string name="elv">elv</string>
+	<string name="zen">zen</string>
+	<string name="duna">duna</string>
+	<string name="quedra">quedra</string>
+	<string name="trin">trin</string>
+	<string name="quen">quen</string>
+	<string name="hes">hes</string>
+	<string name="sev">sev</string>
+	<string name="ak">ak</string>
+	<string name="neen">neen</string>
+	<string name="dex">dex</string>
+	<string name="lef">lef</string>
+	<string name="filtered_stop_question">Do you want to save a favourite filtered with selected lines?</string>
+	<string name="none">none</string>
+	<string name="italics">italics</string>
+	<string name="colour">colour</string>
+	<string name="line_decorations">Line name decorations</string>
+	<string name="acra_notification_channel">Crash reports channel</string>
+	<string name="acra_notification_channel_description">Notifications showing crashes and allowing sending crash reports</string>
+	<string name="acra_notification_title">Bimba has crashed</string>
+	<string name="acra_notification_text">An unexpected obstruction has shown up on Bimba’s way. Do you want to send a report?</string>
+	<string name="send">Send</string>
+	<string name="discard">Discard</string>
+	<string name="send_with_comment">Send with commend</string>
+	<string name="acra_notification_comment">Comment added to crash report</string>
+	<string name="filtered_departures">Filtered departures</string>
+	<string name="alerts">Alerts</string>
+	<string name="grey_out">grey out</string>
+	<string name="hide">hide</string>
+	<string name="show">show</string>
+	<string name="terminus_arrival_showing">Terminus arrivals</string>
+	<string name="matrix_button_description">link to Matrix channel</string>
+	<string name="email_button_description">link to email</string>
+</resources>




diff --git a/app/src/main/res/values-en-rUS/strings.xml b/app/src/main/res/values-en-rUS/strings.xml
index 8afbdd1697a0c2c92063444971b7bb220891b8e1..229ed1c026cdbd5babade118fd129c6f22f72f1a 100644
--- a/app/src/main/res/values-en-rUS/strings.xml
+++ b/app/src/main/res/values-en-rUS/strings.xml
@@ -1,269 +1,283 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
+<?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>
-    <string name="app_name">Bimba</string>
-    <string name="title_home">Home</string>
-    <string name="title_map">Map</string>
-    <string name="title_journey">Journey</string>
-    <string name="home_fab_description">GPS icon</string>
-    <string name="search_placeholder">stops, lines, or plus codes</string>
-    <string name="title_activity_results">Results</string>
-    <string name="continue_">Continue</string>
-    <string name="save">Save</string>
-    <string name="error_400">The application made a malformed request</string>
-    <string name="error_401">A token is needed to use this server</string>
-    <string name="error_403">The token you provided is incorrect</string>
-    <string name="error_404">Not found</string>
-    <string name="error_429">Rate limit exceeded. Try again later</string>
-    <string name="error_50x">There was an error on the sever. Try again later</string>
-    <string name="error_unknown">Unknown error happened</string>
-    <string name="error_connecting">Error connecting to the server. Try again later</string>
-    <!-- send a bug report to bimba@git.apiote.xyz, details are: url=$URL, response=$response -->
+-->
+<resources>
+	<string name="app_name">Bimba</string>
+	<string name="title_home">Home</string>
+	<string name="title_map">Map</string>
+	<string name="title_journey">Journey</string>
+	<string name="home_fab_description">GPS icon</string>
+	<string name="search_placeholder">stops, lines, or plus codes</string>
+	<string name="title_activity_results">Results</string>
+	<string name="continue_">Continue</string>
+	<string name="save">Save</string>
+	<string name="error_400">The application made a malformed request</string>
+	<string name="error_401">A token is needed to use this server</string>
+	<string name="error_403">The token you provided is incorrect</string>
+	<string name="error_404">Not found</string>
+	<string name="error_429">Rate limit exceeded. Try again later</string>
+	<string name="error_50x">There was an error on the sever. Try again later</string>
+	<string name="error_unknown">Unknown error happened</string>
+	<string name="error_connecting">Error connecting to the server. Try again later</string>
+	<!-- send a bug report to questions@bimba.app, details are: url=$URL, response=$response -->
 	<string name="error_offline">You are offline. Connect to the Internet</string>
-    <string name="error_gps">Cannot obtain current location</string>
-    <string name="no_departures">No departures</string>
-    <string name="waiting_position">waiting for position</string>
-    <string name="vehicle_headsign">%1$s » %2$s</string>
-    <string name="distance_in_m">%1$s m</string>
-    <plurals name="distance_in_m_cd">
-        <item quantity="one">%1$d meter</item>
-        <item quantity="other">%1$d meters</item>
-    </plurals>
-    <string name="distance_in_km">%1$s km</string>
-    <plurals name="distance_in_km_cd">
-        <item quantity="one">%1$d kilometer</item>
-        <item quantity="other">%1$d kilometers</item>
-    </plurals>
-    <string name="time_in_s">%1$s s</string>
-    <plurals name="time_in_s_cd">
-        <item quantity="one">%1$d second</item>
-        <item quantity="other">%1$d seconds</item>
-    </plurals>
-    <string name="time_in_tm">%1$s %2$sTm</string>
-    <string name="time_in_tm_past">%1$s %2$sTm ago</string>
-    <plurals name="time_in_tm_cd">
-        <item quantity="one">%1$d tim</item>
-        <item quantity="other">%1$d tims</item>
-    </plurals>
-    <string name="speed_in_km_per_h">%1$s km/h</string>
-    <string name="speed_in_m_per_s">%1$s m/s</string>
-    <string name="speed_in_mi_per_h">%1$s mph</string>
-    <string name="speed_in_vl">%1$s Vl</string>
-    <plurals name="speed_in_m_per_s_cd">
-        <item quantity="one">%1$d meter per second</item>
-        <item quantity="other">%1$d meters per second</item>
-    </plurals>
-    <plurals name="speed_in_km_per_h_cd">
-        <item quantity="one">%1$d kilometer per hour</item>
-        <item quantity="other">%1$d kilometers per hour</item>
-    </plurals>
-    <plurals name="speed_in_mi_per_h_cd">
-        <item quantity="one">%1$d mile per hour</item>
-        <item quantity="other">%1$d mile per hour</item>
-    </plurals>
-    <plurals name="speed_in_vl_cd">
-        <item quantity="one">%1$d vlos</item>
-        <item quantity="other">%1$d vlos</item>
-    </plurals>
-    <string name="congestion_unknown">unknown</string>
-    <string name="congestion_smooth">smooth</string>
-    <string name="congestion_stop_and_go">stop and go</string>
-    <string name="congestion_congestion">congestion</string>
-    <string name="congestion_jams">jams</string>
-    <string name="occupancy_unknown">unknown</string>
-    <string name="occupancy_empty">empty</string>
-    <string name="occupancy_many_seats">many seats</string>
-    <string name="occupancy_few_seats">few seats</string>
-    <string name="occupancy_standing_only">standing only</string>
-    <string name="occupancy_crowded">crowded</string>
-    <string name="occupancy_full">full</string>
-    <string name="occupancy_wont_let">won’t let in</string>
-    <string name="no_map_app">No maps app installed</string>
-    <string name="departure_headsign_content_description">towards %1$s</string>
-    <string name="departure_momentarily">momentarily</string>
-    <string name="departure_departed">departed</string>
-    <string name="departure_now">now</string>
-    <string name="at_time">at %1$02d:%2$02d</string>
-    <string name="at_time_realtime">at %1$02d:%2$02d:%3$02d</string>
-    <string name="on_demand">on demand</string>
-    <string name="no_boarding">no boarding</string>
-    <string name="on_boarding">on-boarding</string>
-    <string name="off_boarding">off-boarding</string>
-    <string name="boarding">can board</string>
-    <string name="line_headsign">» %1$s</string>
-    <string name="line_headsign_content_description">towards %1$s</string>
-    <string name="line_headsigns">%1$s «» %2$s</string>
-    <string name="line_headsigns_content_description">between %1$s and %2$s</string>
-    <string name="stops_nearby">Stops nearby</string>
-    <string name="results_for">Results for ‘%1$s’</string>
-    <string name="bimba_server_address_hint">Server</string>
-    <string name="bimba_server_token_hint">Token</string>
-    <string name="realtime_content_description">departure is realtime</string>
-    <string name="wheelchair_content_description">vehicle is wheelchair accessible</string>
-    <string name="air_condition_content_description">air conditioning</string>
-    <string name="bicycles_allowed_content_description">bicycles allowed</string>
-    <string name="voice_announcements_content_description">voice announcements</string>
-    <string name="tickets_sold_content_description">tickets sold on board</string>
-    <string name="usb_charging_content_description">USB charging</string>
-    <string name="show_departures">Show departures</string>
-    <string name="open_in_maps_app">Open in maps app</string>
-    <string name="stop_content_description">stop</string>
-    <string name="seatbelts_everyone">Seatbelts, everyone!</string>
-    <!-- taken from ‘Magic School Bus’. Should be translated like in the series. It’s the first words of the intro song -->
+	<string name="error_gps">Cannot obtain current location</string>
+	<string name="no_departures">No departures</string>
+	<string name="waiting_position">waiting for position</string>
+	<string name="vehicle_headsign"><annotation decoration="apply"><annotation arg="0">%1$s</annotation></annotation> » <annotation arg="1">%2$s</annotation></string>
+	<string name="distance_in_m">%1$s m</string>
+	<plurals name="distance_in_m_cd">
+		<item quantity="one">%1$d meter</item>
+		<item quantity="other">%1$d meters</item>
+	</plurals>
+	<string name="distance_in_km">%1$s km</string>
+	<plurals name="distance_in_km_cd">
+		<item quantity="one">%1$d kilometer</item>
+		<item quantity="other">%1$d kilometers</item>
+	</plurals>
+	<string name="time_in_s">%1$s s</string>
+	<plurals name="time_in_s_cd">
+		<item quantity="one">%1$d second</item>
+		<item quantity="other">%1$d seconds</item>
+	</plurals>
+	<string name="time_in_tm">%1$s %2$sTm</string>
+	<string name="time_in_tm_past">%1$s %2$sTm ago</string>
+	<plurals name="time_in_tm_cd">
+		<item quantity="one">%1$d tim</item>
+		<item quantity="other">%1$d tims</item>
+	</plurals>
+	<string name="speed_in_km_per_h">%1$s km/h</string>
+	<string name="speed_in_m_per_s">%1$s m/s</string>
+	<string name="speed_in_mi_per_h">%1$s mph</string>
+	<string name="speed_in_vl">%1$s Vl</string>
+	<plurals name="speed_in_m_per_s_cd">
+		<item quantity="one">%1$d meter per second</item>
+		<item quantity="other">%1$d meters per second</item>
+	</plurals>
+	<plurals name="speed_in_km_per_h_cd">
+		<item quantity="one">%1$d kilometer per hour</item>
+		<item quantity="other">%1$d kilometers per hour</item>
+	</plurals>
+	<plurals name="speed_in_mi_per_h_cd">
+		<item quantity="one">%1$d mile per hour</item>
+		<item quantity="other">%1$d mile per hour</item>
+	</plurals>
+	<plurals name="speed_in_vl_cd">
+		<item quantity="one">%1$d vlos</item>
+		<item quantity="other">%1$d vlos</item>
+	</plurals>
+	<string name="congestion_unknown">unknown</string>
+	<string name="congestion_smooth">smooth</string>
+	<string name="congestion_stop_and_go">stop and go</string>
+	<string name="congestion_congestion">congestion</string>
+	<string name="congestion_jams">jams</string>
+	<string name="occupancy_unknown">unknown</string>
+	<string name="occupancy_empty">empty</string>
+	<string name="occupancy_many_seats">many seats</string>
+	<string name="occupancy_few_seats">few seats</string>
+	<string name="occupancy_standing_only">standing only</string>
+	<string name="occupancy_crowded">crowded</string>
+	<string name="occupancy_full">full</string>
+	<string name="occupancy_wont_let">won’t let in</string>
+	<string name="no_map_app">No maps app installed</string>
+	<string name="departure_headsign_content_description">towards %1$s</string>
+	<string name="departure_momentarily">momentarily</string>
+	<string name="departure_departed">departed</string>
+	<string name="departure_now">now</string>
+	<string name="at_time">at %1$02d:%2$02d</string>
+	<string name="about_time">about %1$02d:%2$02d</string>
+	<string name="at_time_realtime">at %1$02d:%2$02d:%3$02d</string>
+	<string name="on_demand">on demand</string>
+	<string name="no_boarding">no boarding</string>
+	<string name="on_boarding">on-boarding</string>
+	<string name="off_boarding">off-boarding</string>
+	<string name="boarding">can board</string>
+	<string name="line_headsign">» %1$s</string>
+	<string name="line_headsign_content_description">towards %1$s</string>
+	<string name="line_headsigns">%1$s «» %2$s</string>
+	<string name="line_headsigns_content_description">between %1$s and %2$s</string>
+	<string name="stops_nearby">Stops nearby</string>
+	<string name="results_for">Results for ‘%1$s’</string>
+	<string name="bimba_server_address_hint">Server</string>
+	<string name="bimba_server_token_hint">Token</string>
+	<string name="realtime_content_description">departure is realtime</string>
+	<string name="exact_content_description">departure time is exact from schedule</string>
+	<string name="inexact_content_description">departure time is approximate</string>
+	<string name="wheelchair_content_description">vehicle is wheelchair accessible</string>
+	<string name="air_condition_content_description">air conditioning</string>
+	<string name="bicycles_allowed_content_description">bicycles allowed</string>
+	<string name="voice_announcements_content_description">voice announcements</string>
+	<string name="tickets_sold_content_description">tickets sold on board</string>
+	<string name="usb_charging_content_description">USB charging</string>
+	<string name="show_departures">Show departures</string>
+	<string name="open_in_maps_app">Open in maps app</string>
+	<string name="stop_content_description">stop</string>
+	<string name="seatbelts_everyone">Seatbelts, everyone!</string>
+	<!-- taken from ‘Magic School Bus’. Should be translated like in the series. It’s the first words of the intro song -->
 	<string name="onboarding_question">How would you like to start?</string>
-    <string name="onboarding_simple">Simple</string>
-    <string name="onboarding_simple_action">choose localities</string>
-    <string name="onboarding_advanced">Advanced</string>
-    <string name="onboarding_advanced_action">choose server</string>
-    <string name="cancel">Cancel</string>
-    <string name="error">Error</string>
-    <string name="rate_limit">Rate limit</string>
-    <string name="server_rate_limited_question">This server is rate-limited and no token was given. Do you want to continue?</string>
-    <string name="server_private_question">This server is private and no token was given</string>
-    <string name="last_update">Last update: %1$s</string>
-    <string name="title_feeds">Timetables</string>
-    <string name="title_servers">Servers</string>
-    <string name="title_cities">Localities</string>
-    <string name="error_url">Malformed URL provided</string>
-    <string name="error_traffic_spec">Cannot verify server</string>
-    <string name="stops_near_code">Stops near %1$s</string>
-    <string name="choose_server">Choose server flavour</string>
-    <string name="ok">OK</string>
-    <string name="no_location_access">Location access not given</string>
-    <string name="no_location_message">Permission to use location is needed to find nearby stops and show current position on map. Other features will work without it. It can be enabled and disabled in system settings any time.</string>
-    <string name="stop_stub_on_demand_in_zone">Stop on demand in zone %1$s</string>
-    <string name="stop_stub_on_demand">Stop on demand</string>
-    <string name="stop_stub_in_zone">Stop in zone %1$s</string>
-    <string name="vehicle_headsign_content_description">%1$s towards %2$s</string>
-    <string name="departure_headsign">» %1$s</string>
-    <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 (https://www.openstreetmap.org/copyright), ODbL-1.0
-\n
-\n Cities list used for geocoding short plus codes © Geonames (https://geonames.org), CC BY</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="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>
-    <string name="use_online_feed">Use online feed</string>
-    <string name="information_may_be_outdated">Information may be outdated</string>
-    <string name="current_timetable_validity">Current timetable valid: %1$s to %2$s</string>
-    <string name="error_406">App version is not compatible with the server</string>
-    <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="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>
-    <string name="map_attribution"><![CDATA[© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors]]></string>
-    <string name="favourite_content_description">Save as favorite</string>
-    <string name="favourite">Favorite</string>
-    <string name="filtered">Filtered</string>
-    <string name="unfiltered">Unfiltered</string>
-    <string name="cannot_save_favourite">Couldn’t save the favorite</string>
-    <string name="error_44">No more departures</string>
-    <string name="loading">loading…</string>
-    <string name="favourite_deleted">Favorite deleted</string>
-    <string name="undo">Undo</string>
-    <string name="units_title">Unit system</string>
-    <string name="units_locale_based">Locale based</string>
-    <string name="units_metric">Metric (SI)</string>
-    <string name="units_imperial">Imperial (UK)</string>
-    <string name="units_customary">US Customary</string>
-    <string name="units_tgm10">TGM (base 10)</string>
-    <string name="units_tgm12">TGM (base 12)</string>
-    <string name="title_settings">Settings</string>
-    <string name="no_geocoding_data">No geocoding data</string>
-    <string name="no_geocoding_data_description">The query contains a short plus code but there is no geocoding data present. Download geocoding data or enable auto updating in settings.</string>
-    <string name="error_geocoding">City not found</string>
-    <string name="cities_channel_name">Cities update channel</string>
-    <string name="cities_channel_description">Notifications showing progress of updating geocoding local data</string>
-    <string name="saving_cities_list">saving cities list</string>
-    <string name="updating_geocoding_data">Updating geocoding data</string>
-    <string name="downloading_cities_list">downloading cities list</string>
-    <string name="finished_updating_geocoding_data">Finished updating geocoding data</string>
-    <string name="updating_geocoding_data_failed">Updating geocoding data failed</string>
-    <string name="zero">zero</string>
-    <string name="one">one</string>
-    <string name="two">two</string>
-    <string name="three">three</string>
-    <string name="four">four</string>
-    <string name="five">five</string>
-    <string name="six">six</string>
-    <string name="seven">seven</string>
-    <string name="eight">eight</string>
-    <string name="nine">nine</string>
-    <string name="ten">ten</string>
-    <string name="elv">elv</string>
-    <string name="zen">zen</string>
-    <string name="duna">duna</string>
-    <string name="quedra">quedra</string>
-    <string name="trin">trin</string>
-    <string name="quen">quen</string>
-    <string name="hes">hes</string>
-    <string name="sev">sev</string>
-    <string name="ak">ak</string>
-    <string name="neen">neen</string>
-    <string name="dex">dex</string>
-    <string name="lef">lef</string>
-    <string name="distance_in_two_units_cd">%1$s and %2$s</string>
-    <string name="distance_in_yd">%1$s yd</string>
-    <string name="distance_in_ft">%1$s ft</string>
-    <string name="distance_in_mi">%1$s mi</string>
-    <string name="distance_in_gf">%1$s %2$sGf</string>
-    <string name="filtered_stop_question">Do you want to save a favorite filtered with selected lines?</string>
-    <plurals name="distance_in_yd_cd">
-        <item quantity="one">%1$d yard</item>
-        <item quantity="other">%1$d yards</item>
-    </plurals>
-    <plurals name="distance_in_ft_cd">
-        <item quantity="one">%1$d foot</item>
-        <item quantity="other">%1$d feet</item>
-    </plurals>
-    <plurals name="distance_in_mi_cd">
-        <item quantity="one">%1$d mile</item>
-        <item quantity="other">%1$d miles</item>
-    </plurals>
-    <plurals name="distance_in_gf_cd">
-        <item quantity="one">%1$d grafut</item>
-        <item quantity="other">%1$d grafuts</item>
-    </plurals>
-    <plurals name="distance_in_3gf_12_cd">
-        <item quantity="one">%1$s trinagrafut</item>
-        <item quantity="other">%1$s trinagrafuts</item>
-    </plurals>
-    <plurals name="distance_in_gf_12_cd">
-        <item quantity="one">%1$s grafut</item>
-        <item quantity="other">%1$s grafuts</item>
-    </plurals>
-    <plurals name="time_in_4tm_12_cd">
-        <item quantity="one">%1$s quedratim</item>
-        <item quantity="other">%1$s quedratims</item>
-    </plurals>
-    <plurals name="time_in_2tm_12_cd">
-        <item quantity="one">%1$s dunatim</item>
-        <item quantity="other">%1$s dunatims</item>
-    </plurals>
-    <plurals name="speed_in_vl_12_cd">
-        <item quantity="one">%1$s vlos</item>
-        <item quantity="other">%1$s vlos</item>
-    </plurals>
+	<string name="onboarding_simple">Simple</string>
+	<string name="onboarding_simple_action">choose localities</string>
+	<string name="onboarding_advanced">Advanced</string>
+	<string name="onboarding_advanced_action">choose server</string>
+	<string name="cancel">Cancel</string>
+	<string name="error">Error</string>
+	<string name="rate_limit">Rate limit</string>
+	<string name="server_rate_limited_question">This server is rate-limited and no token was given. Do you want to continue?</string>
+	<string name="server_private_question">This server is private and no token was given</string>
+	<string name="last_update">Last update: %1$s</string>
+	<string name="title_feeds">Timetables</string>
+	<string name="title_servers">Servers</string>
+	<string name="title_cities">Localities</string>
+	<string name="error_url">Malformed URL provided</string>
+	<string name="error_traffic_spec">Cannot verify server</string>
+	<string name="stops_near_code">Stops near %1$s</string>
+	<string name="choose_server">Choose server flavour</string>
+	<string name="ok">OK</string>
+	<string name="no_location_access">Location access not given</string>
+	<string name="no_location_message">Permission to use location is needed to find nearby stops and show current position on map. Other features will work without it. It can be enabled and disabled in system settings any time.</string>
+	<string name="stop_stub_on_demand_in_zone">Stop on demand in zone %1$s</string>
+	<string name="stop_stub_on_demand">Stop on demand</string>
+	<string name="stop_stub_in_zone">Stop in zone %1$s</string>
+	<string name="vehicle_headsign_content_description">%1$s towards %2$s</string>
+	<string name="departure_headsign">» %1$s</string>
+	<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 (https://www.openstreetmap.org/copyright), ODbL-1.0\n\n Cities list used for geocoding short plus codes © Geonames (https://geonames.org), CC BY\n\n Matrix logo ™/® Matrix.org</string>
+	<string name="title_about">About</string>
+	<string name="translation_button_description">" Mastodon icon (https://github.com/mastodon/joinmastodon) © Mastodon contributors, AGPL-3.0-or-later"</string>
+	<string name="app_description">FLOSS public transport passenger companion; a timetable in your pocket.</string>
+	<string name="website_button_description">" Bimba logo created by https://github.com/tebriz159"</string>
+	<string name="code_button_description">link to source code</string>
+	<string name="mastodon_button_description">" Material icons © Google, Apache-2.0"</string>
+	<string name="use_online_feed">Use online feed</string>
+	<string name="information_may_be_outdated">" Map data © OpenStreetMap contributors (https://www.openstreetmap.org/copyright), ODbL-1.0"</string>
+	<string name="current_timetable_validity">Current timetable valid: %1$s to %2$s</string>
+	<string name="error_406">" Cities list used for geocoding short plus codes © Geonames (https://geonames.org), CC BY"</string>
+	<string name="filter_localities">filter localities</string>
+	<string name="error_41">" Matrix logo ™/® Matrix.org"</string>
+	<string name="stop_from_qr_code">QR code stop</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>
+	<string name="map_attribution"><![CDATA[© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors]]></string>
+	<string name="favourite_content_description">Save as favorite</string>
+	<string name="favourite">Favorite</string>
+	<string name="filtered">Filtered</string>
+	<string name="unfiltered">Unfiltered</string>
+	<string name="cannot_save_favourite">Couldn’t save the favorite</string>
+	<string name="error_44">No more departures</string>
+	<string name="loading">loading…</string>
+	<string name="favourite_deleted">Favorite deleted</string>
+	<string name="undo">Undo</string>
+	<string name="units_title">Unit system</string>
+	<string name="units_locale_based">Locale based</string>
+	<string name="units_metric">Metric (SI)</string>
+	<string name="units_imperial">Imperial (UK)</string>
+	<string name="units_customary">US Customary</string>
+	<string name="units_tgm10">TGM (base 10)</string>
+	<string name="units_tgm12">TGM (base 12)</string>
+	<string name="title_settings">Settings</string>
+	<string name="no_geocoding_data">No geocoding data</string>
+	<string name="no_geocoding_data_description">The query contains a short plus code but there is no geocoding data present. Download geocoding data or enable auto updating in settings.</string>
+	<string name="error_geocoding">City not found</string>
+	<string name="cities_channel_name">Cities update channel</string>
+	<string name="cities_channel_description">Notifications showing progress of updating geocoding local data</string>
+	<string name="saving_cities_list">saving cities list</string>
+	<string name="updating_geocoding_data">Updating geocoding data</string>
+	<string name="downloading_cities_list">downloading cities list</string>
+	<string name="finished_updating_geocoding_data">Finished updating geocoding data</string>
+	<string name="updating_geocoding_data_failed">Updating geocoding data failed</string>
+	<string name="zero">zero</string>
+	<string name="one">one</string>
+	<string name="two">two</string>
+	<string name="three">three</string>
+	<string name="four">four</string>
+	<string name="five">five</string>
+	<string name="six">six</string>
+	<string name="seven">seven</string>
+	<string name="eight">eight</string>
+	<string name="nine">nine</string>
+	<string name="ten">ten</string>
+	<string name="elv">elv</string>
+	<string name="zen">zen</string>
+	<string name="duna">duna</string>
+	<string name="quedra">quedra</string>
+	<string name="trin">trin</string>
+	<string name="quen">quen</string>
+	<string name="hes">hes</string>
+	<string name="sev">sev</string>
+	<string name="ak">ak</string>
+	<string name="neen">neen</string>
+	<string name="dex">dex</string>
+	<string name="lef">lef</string>
+	<string name="distance_in_two_units_cd">%1$s and %2$s</string>
+	<string name="distance_in_yd">%1$s yd</string>
+	<string name="distance_in_ft">%1$s ft</string>
+	<string name="distance_in_mi">%1$s mi</string>
+	<string name="distance_in_gf">%1$s %2$sGf</string>
+	<string name="filtered_stop_question">Do you want to save a favorite filtered with selected lines?</string>
+	<plurals name="distance_in_yd_cd">
+		<item quantity="one">%1$d yard</item>
+		<item quantity="other">%1$d yards</item>
+	</plurals>
+	<plurals name="distance_in_ft_cd">
+		<item quantity="one">%1$d foot</item>
+		<item quantity="other">%1$d feet</item>
+	</plurals>
+	<plurals name="distance_in_mi_cd">
+		<item quantity="one">%1$d mile</item>
+		<item quantity="other">%1$d miles</item>
+	</plurals>
+	<plurals name="distance_in_gf_cd">
+		<item quantity="one">%1$d grafut</item>
+		<item quantity="other">%1$d grafuts</item>
+	</plurals>
+	<plurals name="distance_in_3gf_12_cd">
+		<item quantity="one">%1$s trinagrafut</item>
+		<item quantity="other">%1$s trinagrafuts</item>
+	</plurals>
+	<plurals name="distance_in_gf_12_cd">
+		<item quantity="one">%1$s grafut</item>
+		<item quantity="other">%1$s grafuts</item>
+	</plurals>
+	<plurals name="time_in_4tm_12_cd">
+		<item quantity="one">%1$s quedratim</item>
+		<item quantity="other">%1$s quedratims</item>
+	</plurals>
+	<plurals name="time_in_2tm_12_cd">
+		<item quantity="one">%1$s dunatim</item>
+		<item quantity="other">%1$s dunatims</item>
+	</plurals>
+	<plurals name="speed_in_vl_12_cd">
+		<item quantity="one">%1$s vlos</item>
+		<item quantity="other">%1$s vlos</item>
+	</plurals>
+	<string name="none">none</string>
+	<string name="italics">italics</string>
+	<string name="colour">color</string>
+	<string name="line_decorations">Line name decorations</string>
+	<string name="acra_notification_channel">Crash reports channel</string>
+	<string name="acra_notification_channel_description">Notifications showing crashes and allowing sending crash reports</string>
+	<string name="acra_notification_title">Bimba crashed</string>
+	<string name="acra_notification_text">An unexpected obstruction showed up on Bimba’s way. Do you want to send a report?</string>
+	<string name="send">Send</string>
+	<string name="discard">Discard</string>
+	<string name="send_with_comment">Send with commend</string>
+	<string name="acra_notification_comment">Comment added to crash report</string>
+	<string name="filtered_departures">Filtered departures</string>
+	<string name="alerts">Alerts</string>
+	<string name="grey_out">gray out</string>
+	<string name="hide">hide</string>
+	<string name="show">show</string>
+	<string name="terminus_arrival_showing">Terminus arrivals</string>
+	<string name="matrix_button_description">link to Matrix channel</string>
+	<string name="email_button_description">link to email</string>
+	<string name="no_email_app">No email app installed</string>
 </resources>
\ No newline at end of file




diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml
index 681fc303bc0dd98da77ad08144d901815fb93f97..1a44d63c4dfbb2e513512abbbe81adabb33a2ef4 100644
--- a/app/src/main/res/values-fr/strings.xml
+++ b/app/src/main/res/values-fr/strings.xml
@@ -16,7 +16,7 @@     Il y a eu une erreur sur le serveur. Réessayez plus tard
     <string name="error_connecting">Erreur lors de la connection au serveur. Réessayez plus tard</string>
     <string name="error_unknown">Erreur inconnue</string>
     <string name="waiting_position">En attente de la position</string>
-    <string name="vehicle_headsign">%1$s » %2$s</string>
+    <string name="vehicle_headsign"><annotation decoration="apply"><annotation arg="0">%1$s</annotation></annotation> » <annotation arg="1">%2$s</annotation></string>
     <string name="congestion_unknown">Inconnu</string>
     <string name="congestion_smooth">Fluide</string>
     <string name="occupancy_standing_only">Uniquement debout</string>




diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml
index 69ecbb6d8a8a72b7e4f4af3003ecdcb28aebaf0a..9b4c97506c9d76ed958272f6fa3adac7d6357eb4 100644
--- a/app/src/main/res/values-it/strings.xml
+++ b/app/src/main/res/values-it/strings.xml
@@ -25,7 +25,7 @@     Sei offline. Collega al’Internet
     <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"><annotation decoration="apply"><annotation arg="0">%1$s</annotation></annotation> » <annotation arg="1">%2$s</annotation></string>
     <string name="vehicle_headsign_content_description">%1$s verso %2$s</string>
     <string name="speed_in_km_per_h">%1$s km/h</string>
     <string name="congestion_unknown">sconosciuta</string>




diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml
index c91cb8c9b197de575f215cf9b77488998cebbdc7..6879010dd16f035617f2930f61b1a6a86163abed 100644
--- a/app/src/main/res/values-pl/strings.xml
+++ b/app/src/main/res/values-pl/strings.xml
@@ -25,7 +25,7 @@     Brak połączenia z Internetem
     <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"><annotation decoration="apply"><annotation arg="0">%1$s</annotation></annotation> » <annotation arg="1">%2$s</annotation></string>
     <string name="vehicle_headsign_content_description">%1$s w kierunku przystanku %2$s</string>
     <string name="speed_in_km_per_h">%1$s km/h</string>
     <string name="congestion_unknown">nieznane</string>
@@ -48,6 +48,7 @@     za moment
     <string name="departure_departed">odjechał</string>
     <string name="departure_now">teraz</string>
     <string name="at_time">o %1$02d:%2$02d</string>
+    <string name="about_time">about %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>
@@ -63,6 +64,8 @@     Wyniki dla „%1$s”
     <string name="bimba_server_address_hint">Serwer</string>
     <string name="bimba_server_token_hint">Żeton</string>
     <string name="realtime_content_description">odjazd w czasie rzeczywistym</string>
+    <string name="exact_content_description">czas odjazdu jest dokładny z rozkładu</string>
+    <string name="inexact_content_description">czas odjazdu jest przybliżony</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>
@@ -287,4 +290,25 @@         %1$s wlosy
         <item quantity="many">%1$s wlosów</item>
         <item quantity="other">%1$s wlosów</item>
     </plurals>
+	<string name="none">brak</string>
+    <string name="italics">kursywa</string>
+    <string name="colour">kolor</string>
+    <string name="line_decorations">Dekoracje nazw linii</string>
+    <string name="acra_notification_channel">Kanał raportowania błędów</string>
+    <string name="acra_notification_channel_description">Powiadomienia pokazujące błędy i pozwalające na wysłanie raportów</string>
+    <string name="acra_notification_title">Bimba się wykoleiła</string>
+    <string name="acra_notification_text">Nieoczekiwana przeszkoda pojawiła się na drodze Bimby. Czy wysłać raport?</string>
+    <string name="send">Wyślij</string>
+    <string name="discard">Pomiń</string>
+    <string name="send_with_comment">Wyślij z komentarzem</string>
+    <string name="acra_notification_comment">Komentarz dodany do raportu</string>
+    <string name="filtered_departures">Filtrowane odjazdy</string>
+    <string name="alerts">Alerty</string>
+    <string name="grey_out">wyszarzone</string>
+    <string name="hide">ukryte</string>
+    <string name="show">widoczne</string>
+    <string name="terminus_arrival_showing">Przyjazdy na pętle</string>
+    <string name="matrix_button_description">link do kanału na Matrixie</string>
+    <string name="email_button_description">link do e-maila</string>
+    <string name="no_email_app">Brak aplikacji e-mail</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
new file mode 100644
index 0000000000000000000000000000000000000000..27e57b55cc383ece2b9d21fa54bca4717eee18c2
--- /dev/null
+++ b/app/src/main/res/xml/locales_config.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<locale-config xmlns:android="http://schemas.android.com/apk/res/android">
+    <locale android:name="en"/>
+    <locale android:name="pl"/>
+    <locale android:name="it"/>
+    <locale android:name="de"/>
+    <locale android:name="fr"/>
+    <locale android:name="en-rUS"/>
+</locale-config>
\ No newline at end of file




diff --git a/app/src/main/res/xml/root_preferences.xml b/app/src/main/res/xml/root_preferences.xml
index ab4959ff16b28aba10d9e4f2e244473c76f04fe2..2127ae46dc0a24e21c53e4cc6fcd662fe492916e 100644
--- a/app/src/main/res/xml/root_preferences.xml
+++ b/app/src/main/res/xml/root_preferences.xml
@@ -21,4 +21,26 @@ 			app:icon="@drawable/download"
 			app:key="download_cities_list"
 			app:title="Update cities list now" />
 	</PreferenceCategory>
+
+	<PreferenceCategory app:title="Appearance">
+		<ListPreference
+			app:defaultValue="italics"
+			app:entries="@array/line_decorations"
+			app:entryValues="@array/line_decorations_values"
+			app:icon="@drawable/vehicle"
+			app:key="line_decoration"
+			app:title="@string/line_decorations"
+			app:useSimpleSummaryProvider="true"
+			/>
+		<!-- TODO elizabeth
+		<ListPreference
+			app:defaultValue="grey_out"
+			app:entries="@array/terminus_arrival_showing"
+			app:entryValues="@array/terminus_arrival_showing_values"
+			app:icon="@drawable/terminus"
+			app:key="terminus_arrival_showing"
+			app:title="@string/terminus_arrival_showing"
+			app:useSimpleSummaryProvider="true"
+			/>-->
+	</PreferenceCategory>
 </PreferenceScreen>
\ 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
index 8290582612b62aedbdcfdc18846921dd774b2d60..ff0bb2cb49a07b13f1b90518a772d06e5933e071 100644
--- 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
@@ -9,14 +9,11 @@ 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.LocatableV4
 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.QueryableV5
 import xyz.apiote.bimba.czwek.api.structs.FeedInfoV2
-import xyz.apiote.fruchtfleisch.Reader
 import java.io.InputStream
 
 data class DeparturesResponseDev(
@@ -51,7 +48,7 @@ 		}
 	}
 }
 
-data class LocatablesResponseDev(val locatables: List<LocatableV3>) : LocatablesResponse {
+data class LocatablesResponseDev(val locatables: List<LocatableV4>) : LocatablesResponse {
 	companion object {
 		private fun unmarshal(stream: InputStream): LocatablesResponseDev {
 			return LocatablesResponseDev(listOf())
@@ -60,7 +57,7 @@ 	}
 }
 
 
-data class QueryablesResponseDev(val queryables: List<QueryableV4>) : QueryablesResponse {
+data class QueryablesResponseDev(val queryables: List<QueryableV5>) : QueryablesResponse {
 	companion object {
 		private fun unmarshal(stream: InputStream): QueryablesResponseDev {
 			return QueryablesResponseDev(listOf())




diff --git a/build.gradle b/build.gradle
deleted file mode 100644
index 279f2c1cfcf841d408996f85e4d1efb1ffa12ef0..0000000000000000000000000000000000000000
--- a/build.gradle
+++ /dev/null
@@ -1,13 +0,0 @@
-// SPDX-FileCopyrightText: Adam Evyčędo
-//
-// 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.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
-}




diff --git a/build.gradle.kts b/build.gradle.kts
new file mode 100644
index 0000000000000000000000000000000000000000..46c4cc081eaca93cfd488ae20a55802bb1a9de63
--- /dev/null
+++ b/build.gradle.kts
@@ -0,0 +1,13 @@
+// SPDX-FileCopyrightText: Adam Evyčędo
+//
+// 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.5.1" apply false
+    id("com.android.library") version "8.5.1" apply false
+    kotlin("android") version "2.0.10" apply false
+    kotlin("jvm") version "1.7.20" apply false
+    kotlin("plugin.parcelize") version "1.8.20" apply false
+    kotlin("plugin.serialization") version "1.9.22" apply false
+}




diff --git a/fruchtfleisch/build.gradle b/fruchtfleisch/build.gradle
deleted file mode 100644
index 85c32f76546645efcd429747057f155ce9b51955..0000000000000000000000000000000000000000
--- a/fruchtfleisch/build.gradle
+++ /dev/null
@@ -1,22 +0,0 @@
-// SPDX-FileCopyrightText: Adam Evyčędo
-//
-// SPDX-License-Identifier: GPL-3.0-or-later
-
-plugins {
-    id 'java-library'
-    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/build.gradle.kts b/fruchtfleisch/build.gradle.kts
new file mode 100644
index 0000000000000000000000000000000000000000..d8428614c918943ac054616ce6052d6d5a72fc33
--- /dev/null
+++ b/fruchtfleisch/build.gradle.kts
@@ -0,0 +1,24 @@
+// SPDX-FileCopyrightText: Adam Evyčędo
+//
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+plugins {
+    id("java-library")
+    kotlin("jvm")
+}
+
+dependencies {
+    testImplementation("org.junit.jupiter:junit-jupiter:5.11.0")
+    testImplementation("org.junit.jupiter:junit-jupiter:5.11.0")
+
+    //implementation("org.jetbrains.kotlin:kotlin-reflect:1.8.10")
+}
+
+java {
+    sourceCompatibility = JavaVersion.VERSION_17
+    targetCompatibility = JavaVersion.VERSION_17
+}
+
+tasks.withType<Test> {
+    useJUnitPlatform()
+}
\ No newline at end of file




diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index 92dd17cf105d5e62e6c0ad89238ee13f72fcea5a..e6b146979591f464343c7a948c2493b91598885d 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -4,7 +4,7 @@ # SPDX-License-Identifier: GPL-3.0-or-later
 
 #Tue Aug 09 15:48:25 CEST 2022
 distributionBase=GRADLE_USER_HOME
-distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip
 distributionPath=wrapper/dists
 zipStorePath=wrapper/dists
 zipStoreBase=GRADLE_USER_HOME




diff --git a/gradle.properties b/gradle.properties
index 414ce984ef231e8254ae7fe6d233b446195ee574..2540962b6fd2fcdfbeea18da64677febf2d800ec 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -27,3 +27,4 @@ # thereby reducing the size of the R class for that library
 android.nonTransitiveRClass=true
 android.nonFinalResIds=true
 org.gradle.unsafe.configuration-cache=true
+org.gradle.caching=true




diff --git a/metadata/en/changelogs/30.txt b/metadata/en/changelogs/30.txt
new file mode 100644
index 0000000000000000000000000000000000000000..bb3377a509d9ef0382effa92ee13f819092b6148
--- /dev/null
+++ b/metadata/en/changelogs/30.txt
@@ -0,0 +1,6 @@
+Changes in version 3.6:
+* Change options can now show lines in italics
+* Crashes can be reported
+* About page shows more contact points
+* Fixed updating favourites and geonames on older versions of Android
+




diff --git a/metadata/en-US/changelogs/30.txt b/metadata/en-US/changelogs/30.txt
new file mode 100644
index 0000000000000000000000000000000000000000..bb3377a509d9ef0382effa92ee13f819092b6148
--- /dev/null
+++ b/metadata/en-US/changelogs/30.txt
@@ -0,0 +1,6 @@
+Changes in version 3.6:
+* Change options can now show lines in italics
+* Crashes can be reported
+* About page shows more contact points
+* Fixed updating favourites and geonames on older versions of Android
+




diff --git a/metadata/pl-PL/changelogs/30.txt b/metadata/pl-PL/changelogs/30.txt
new file mode 100644
index 0000000000000000000000000000000000000000..c9dffb4f175dcb5023b047f619ee93cb41238726
--- /dev/null
+++ b/metadata/pl-PL/changelogs/30.txt
@@ -0,0 +1,5 @@
+Zmiany w wersji 3.6:
+* Opcje przesiadki mogą pokazywać linie kursywą
+* Błędy mogą być raportowane
+* Strona o Bimbie zawiera więcej możliwości kontaktu
+* Naprawiono aktualizowanie ulubionych i danych geokodowania na starszych wersjach Androida




diff --git a/release.sh b/release.sh
index aa6d0ce73903de6cd4504626e025f7fdbcfd2fc2..62122ff378c7bd472d9e463b2b145ef1637b4669 100755
--- a/release.sh
+++ b/release.sh
@@ -71,11 +71,11 @@ 			exit 1
 		fi
 	done
 
-	currentVersionName=$(grep -E 'versionName "[0-9\.]+"' app/build.gradle | tr -s ' ' | cut -d ' ' -f3 | tr -d '"')
+	currentVersionName=$(grep -Eo 'versionName = "[0-9\.]+"' app/build.gradle.kts | cut -d '=' -f2 | tr -d ' "')
 	major=$(echo "$currentVersionName" | cut -d '.' -f1)
 	minor=$(echo "$currentVersionName" | cut -d '.' -f2)
 	patch=$(echo "$currentVersionName" | cut -d '.' -f3)
-	currentVersionCode=$(grep 'versionCode' app/build.gradle | tr -s ' ' | cut -d ' ' -f3)
+	currentVersionCode=$(grep 'versionCode' app/build.gradle.kts | cut -d '=' -f2 | tr -d ' ')
 
 	case $releaseType in
 		major) newVersionName="$((major + 1)).0.0" ;;
@@ -85,16 +85,16 @@ 		*) echo "wrong release type given"; exit 1 ;;
 	esac
 	newVersionCode=$((currentVersionCode + 1))
 
-	sed -i "s/versionName \"$currentVersionName\"/versionName \"$newVersionName\"/" app/build.gradle
-	sed -i "s/versionCode $currentVersionCode/versionCode $newVersionCode/" app/build.gradle
+	sed -i "s/versionName = \"$currentVersionName\"/versionName = \"$newVersionName\"/" app/build.gradle.kts
+	sed -i "s/versionCode = $currentVersionCode/versionCode = $newVersionCode/" app/build.gradle.kts
 
 	git shortlog "v${currentVersionName}..HEAD" >> "metadata/en-US/changelogs/$newVersionCode.txt"
 
 	echo "time to update changelogs"
 elif [ $phase -eq 1 ]
 then
-	newVersionName=$(grep -E 'versionName "[0-9\.]+"' app/build.gradle | tr -s ' ' | cut -d ' ' -f3 | tr -d '"')
-	newVersionCode=$(grep 'versionCode' app/build.gradle | tr -s ' ' | cut -d ' ' -f3)
+	newVersionName=$(grep -Eo 'versionName = "[0-9\.]+"' app/build.gradle.kts | cut -d '=' -f2 | tr -d ' "')
+	newVersionCode=$(grep 'versionCode' app/build.gradle.kts | cut -d '=' -f2 | tr -d ' ')
 	if ! find metadata -type d -name changelogs -print0 | xargs -0 -I{} [ -f "{}/$newVersionCode.txt" ]
 	then
 		echo "not all languages have changelog"
@@ -104,7 +104,7 @@ 		if [ "$yn" != 'y' ] && [ "$yn" != 'Y' ]; then
 			exit 1
 		fi
 	fi
-	git add app/build.gradle
+	git add app/build.gradle.kts
 	git add metadata/
 	git commit -S -m "release version $newVersionName ($newVersionCode)" || true
 	echo 'pushing …'
@@ -129,7 +129,7 @@ 	git switch develop
 	git merge master
 elif [ $phase -eq 2 ]
 then
-	newVersionName=$(grep -E 'versionName "[0-9\.]+"' app/build.gradle | tr -s ' ' | cut -d ' ' -f3 | tr -d '"')
+	newVersionName=$(grep -Eo 'versionName = "[0-9\.]+"' app/build.gradle.kts | cut -d '=' -f2 | tr -d ' "')
 	git tag -s -m "v${newVersionName}" "v${newVersionName}"
 	git push origin --tags
 	git switch develop




diff --git a/settings.gradle b/settings.gradle
deleted file mode 100644
index 4f933d18b9fa89ddb028f9dbfdaabc5f5fcc3c47..0000000000000000000000000000000000000000
--- a/settings.gradle
+++ /dev/null
@@ -1,22 +0,0 @@
-// SPDX-FileCopyrightText: Adam Evyčędo
-//
-// SPDX-License-Identifier: GPL-3.0-or-later
-
-pluginManagement {
-    repositories {
-        gradlePluginPortal()
-        google()
-        mavenCentral()
-    }
-}
-dependencyResolutionManagement {
-    repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
-    repositories {
-        google()
-        mavenCentral()
-        maven { url "https://jitpack.io" }
-    }
-}
-rootProject.name = "Bimba"
-include ':app'
-include ':fruchtfleisch'




diff --git a/settings.gradle.kts b/settings.gradle.kts
new file mode 100644
index 0000000000000000000000000000000000000000..1ce7ce840b4bacaa13bd373882ed08820ac536d9
--- /dev/null
+++ b/settings.gradle.kts
@@ -0,0 +1,34 @@
+// SPDX-FileCopyrightText: Adam Evyčędo
+//
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+pluginManagement {
+    repositories {
+        gradlePluginPortal()
+        google()
+        mavenCentral()
+    }
+}
+val cacheUser: String by settings
+val cachePass: String by settings
+buildCache {
+    remote<HttpBuildCache> {
+        url = uri("https://cranberry.apiote.xyz")
+        isPush = true
+        credentials {
+            username = cacheUser
+            password = cachePass
+        }
+        // on command line add -PcacheUser=user -PcachePassword=password or in ~/.gradle/gradle.properies
+    }
+}
+dependencyResolutionManagement {
+    repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
+    repositories {
+        google()
+        mavenCentral()
+    }
+}
+rootProject.name = "Bimba"
+include(":app")
+include(":fruchtfleisch")