Bimba.git

commit 7b2d0a631fe48221089f609e7e0afef6feca06a2

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

merge develop into master for version 3.9.0-rc.2

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


diff --git a/CHANGELOG.adoc b/CHANGELOG.adoc
index 4c4f412fafbcd7a61baecb8ed4e14734b604c450..4e291197dd184e9aae4739b7b9102e4c2759de9e 100644
--- a/CHANGELOG.adoc
+++ b/CHANGELOG.adoc
@@ -14,13 +14,20 @@
 * Travel planning
 * Offline timetable
 
-== [3.9.0] – 2025-02-08
+== [3.9.0] – 2025-xx-xx
 
 === Added
 
 * Travel planning based on Transitous
 
-== [3.6.1] – 2024-09-04
+== [3.8.0] – 2025-01-21
+
+=== Changed
+
+* separated arrivals and departures
+* switched to Transitous MOTIS 2 API
+
+== [3.7.0] – 2024-10-15
 
 === Fixed
 




diff --git a/README.adoc b/README.adoc
index 911bd206a8b9b12526d94e40597541b17bee5bb3..dd2f04766ac9ef8678baf589cd7a8b819e7d02da 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.9.0-rc.1 2025-02-08
+v3.9.0-rc.3 2025-03-12
 :toc:
 
 Bimba is a FLOSS public transport passenger companion; a timetable in your pocket.




diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index cef626c86995282d54924611ad1a8f66dce60045..b8bc7888e2be37668b6401d107b9212e643f9613 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -21,14 +21,14 @@ android {
 	namespace = "xyz.apiote.bimba.czwek"
 	// NOTE apksigner with `--alignment-preserved` https://gitlab.com/fdroid/fdroiddata/-/issues/3299#note_1989808414
 	compileSdk = 35
-	buildToolsVersion = "35.0.0"
+	buildToolsVersion = "35.0.1"
 
 	defaultConfig {
 		applicationId = "xyz.apiote.bimba.czwek"
 		minSdk = 21
 		targetSdk = 35
-		versionCode = 35
-		versionName = "3.9.0-rc.1"
+		versionCode = 36
+		versionName = "3.9.0-rc.2"
 
 		testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
 		resourceConfigurations += listOf("en", "de", "en-rGB", "en-rUS", "et", "fr", "it", "pl", "ru", "ta")
@@ -98,17 +98,17 @@ dependencies {
 	implementation("androidx.core:core-ktx:1.15.0")
 	implementation("androidx.appcompat:appcompat:1.7.0")
 	implementation("com.google.android.material:material:1.12.0")
-	implementation("androidx.constraintlayout:constraintlayout:2.2.0")
+	implementation("androidx.constraintlayout:constraintlayout:2.2.1")
 	implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.8.7")
 	implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.7")
-	implementation("androidx.navigation:navigation-fragment-ktx:2.8.6")
-	implementation("androidx.navigation:navigation-ui-ktx:2.8.6")
+	implementation("androidx.navigation:navigation-fragment-ktx:2.8.8")
+	implementation("androidx.navigation:navigation-ui-ktx:2.8.8")
 	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.20")
-	implementation("org.yaml:snakeyaml:2.3")
-	implementation("androidx.activity:activity-ktx:1.10.0")
+	implementation("org.yaml:snakeyaml:2.4")
+	implementation("androidx.activity:activity-ktx:1.10.1")
 	implementation("com.otaliastudios:zoomlayout:1.9.0")
 	implementation("dev.bandb.graphview:graphview:0.8.1")
 	implementation("org.jetbrains.kotlinx:kotlinx-serialization-core:1.8.0")
@@ -123,9 +123,9 @@ 	implementation("ch.acra:acra-http:5.12.0")
 	implementation("ch.acra:acra-notification:5.12.0")
 	implementation("com.squareup.okhttp3:okhttp:4.12.0")
 	implementation("com.squareup.moshi:moshi-kotlin:1.15.2")
-	implementation("androidx.activity:activity:1.10.0")
+	implementation("androidx.activity:activity:1.10.1")
 
-	coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.1.4")
+	coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.1.5")
 
 	androidTestImplementation("androidx.test.ext:junit:1.2.1")
 	androidTestImplementation("androidx.test.espresso:espresso-core:3.6.1")




diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/RoundedBackgroundSpan.kt b/app/src/main/java/xyz/apiote/bimba/czwek/RoundedBackgroundSpan.kt
index e38daad84e9aba836e3195be5c442c0abf6aa678..b410764e4abb270111a89221bf2c8fe6b5198b4d 100644
--- a/app/src/main/java/xyz/apiote/bimba/czwek/RoundedBackgroundSpan.kt
+++ b/app/src/main/java/xyz/apiote/bimba/czwek/RoundedBackgroundSpan.kt
@@ -9,7 +9,13 @@ import android.graphics.Paint
 import android.graphics.RectF
 import android.text.style.ReplacementSpan
 
-class RoundedBackgroundSpan(private val bgColour: Int, private val fgColour: Int) : ReplacementSpan() {
+class RoundedBackgroundSpan(private val bgColour: Int, private val fgColour: Int) :
+	ReplacementSpan() {
+
+		companion object {
+			const val PADDING = 20  // TODO based on small text, make dependent on eg. em-dash length
+		}
+
 	override fun getSize(
 		paint: Paint,
 		text: CharSequence,
@@ -17,7 +23,15 @@ 		start: Int,
 		end: Int,
 		fm: Paint.FontMetricsInt?
 	): Int {
-		return (paint.measureText(text, start, end)+20).toInt()
+		val l = paint.measureText(text, start, end) + PADDING
+		if (fm != null) {
+			fm.ascent = paint.fontMetrics.ascent.toInt()
+			fm.bottom = paint.fontMetrics.bottom.toInt()
+			fm.descent = paint.fontMetrics.descent.toInt()
+			fm.leading = paint.fontMetrics.leading.toInt()
+			fm.top = paint.fontMetrics.top.toInt()
+		}
+		return l.toInt()
 	}
 
 	override fun draw(
@@ -31,12 +45,19 @@ 		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)
+		val length = paint.measureText(text, start, end) + PADDING
+		/*
+		Log.i("BackgroundSpan", "x: $x, y: $y, start: $start, end: $end, top: $top, bottom: $bottom")
+		Log.i("BackgroundSpan", "text: $text, length: $length")
+		Log.i("BackgroundSpan", "fontMetrics:: bottom: ${paint.fontMetricsInt.bottom}, top: ${paint.fontMetricsInt.top}, ascent: ${paint.fontMetricsInt.ascent}, descent: ${paint.fontMetricsInt.descent}, leading: ${paint.fontMetricsInt.leading}")
+		Log.i("BackgroundSpan", "fontSpacing: ${paint.fontSpacing}; 1dp=${dpToPixelI(1f)}")
+		*/
+		val height = y - top
+		val rect = RectF(x, top.toFloat()-(height/6), x + length, y.toFloat()+(height/6))
 		paint.color = bgColour
-		canvas.drawRoundRect(rect, 10f, 10f, paint)
+		canvas.drawRoundRect(rect, height/4f, height/4f, paint)
 		paint.color = fgColour
 		paint.textAlign = Paint.Align.CENTER
-		canvas.drawText(text, start, end, x+(length/2), y.toFloat(), paint)
+		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/journeys/Journeys.kt b/app/src/main/java/xyz/apiote/bimba/czwek/journeys/Journeys.kt
index ff1d71676d397e60dcf7203f80d6cfa180069dc9..2f4d18ccb8bbebd0cd076fefb7d802efd9acc97e 100644
--- a/app/src/main/java/xyz/apiote/bimba/czwek/journeys/Journeys.kt
+++ b/app/src/main/java/xyz/apiote/bimba/czwek/journeys/Journeys.kt
@@ -6,6 +6,7 @@ package xyz.apiote.bimba.czwek.journeys
 
 import android.annotation.SuppressLint
 import android.content.Context
+import android.text.SpannableStringBuilder
 import android.view.LayoutInflater
 import android.view.View
 import android.view.ViewGroup
@@ -17,6 +18,7 @@ import androidx.recyclerview.widget.RecyclerView.ViewHolder
 import com.google.android.material.card.MaterialCardView
 import xyz.apiote.bimba.czwek.R
 import xyz.apiote.bimba.czwek.repo.Journey
+import xyz.apiote.bimba.czwek.repo.Stop.LineDecoration
 import xyz.apiote.bimba.czwek.units.UnitSystem
 
 class JourneysViewHolder(itemView: View) : ViewHolder(itemView) {
@@ -43,8 +45,19 @@ 			holder.startTime.text =
 				context.getString(R.string.time, journey.startTime.hour, journey.startTime.minute)
 			holder.endTime.text =
 				context.getString(R.string.time, journey.endTime.hour, journey.endTime.minute)
-			holder.lines.text =
-				journey.legs.map { it.start.vehicle.Line.name }.filter { it.isNotBlank() }.joinToString()
+			holder.lines.text = journey.legs.filter { it.start.vehicle.Line.name.isNotBlank() }
+				.fold(SpannableStringBuilder("")) { acc, leg ->
+					if (acc.isNotBlank()) {
+						acc.append(" ")
+					}
+					val s = leg.start.vehicle.Line.decorate(
+						SpannableStringBuilder(leg.start.vehicle.Line.name),
+						LineDecoration.COLOUR,
+						null
+					)
+					acc.append(s)
+					acc
+				}
 
 			holder.legs.removeAllViews()
 			journey.legs.forEach {
@@ -98,7 +111,11 @@ 					context.getString(R.string.journey_stops_headsign, stops, headsign)
 				}
 
 				val legDestination = legView.findViewById<TextView>(R.id.leg_destination)
-				if (it.destination.stop.name.isBlank() || it.destination.stop.name in arrayOf("START", "END")) {
+				if (it.destination.stop.name.isBlank() || it.destination.stop.name in arrayOf(
+						"START",
+						"END"
+					)
+				) {
 					legDestination.visibility = View.GONE
 				} else {
 					legDestination.apply {




diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/journeys/JourneysViewModel.kt b/app/src/main/java/xyz/apiote/bimba/czwek/journeys/JourneysViewModel.kt
index 961dd33b1e600b758ad71fb0098aeb8925628eb8..8f54abe1ac6a5e29c1d615a22b523201afad8fd2 100644
--- a/app/src/main/java/xyz/apiote/bimba/czwek/journeys/JourneysViewModel.kt
+++ b/app/src/main/java/xyz/apiote/bimba/czwek/journeys/JourneysViewModel.kt
@@ -10,6 +10,7 @@ import androidx.lifecycle.MutableLiveData
 import androidx.lifecycle.ViewModel
 import androidx.lifecycle.viewModelScope
 import kotlinx.coroutines.launch
+import org.openapitools.client.infrastructure.ServerException
 import xyz.apiote.bimba.czwek.repo.Journey
 import xyz.apiote.bimba.czwek.repo.JourneyParams
 import xyz.apiote.bimba.czwek.repo.Place
@@ -20,7 +21,6 @@
 	private val _journeys = MutableLiveData<List<Journey>>()
 	val journeys: LiveData<List<Journey>> = _journeys
 
-	// FIXME when not in foreground, throws java.net.SocketException: Software caused connection abort
 	fun getJourneys(context: Context, origin: Place, destination: Place, params: JourneyParams) {
 		viewModelScope.launch {
 			try {
@@ -29,6 +29,12 @@ 					xyz.apiote.bimba.czwek.api.getJourney(origin, destination, params, context)
 			} catch (e: SocketTimeoutException) {
 				_journeys.value = emptyList<Journey>()
 				Log.e("Journeys", "timeout: $e")
+			} catch (e: ServerException) {
+				_journeys.value = emptyList<Journey>()
+				Log.w("Suggestion", "Transitous returned: ${e.statusCode}, ${e.message}")
+			} catch (e: Exception) {
+				_journeys.value = emptyList<Journey>()
+				Log.w("Suggestion", "Exception: $e")
 			}
 		}
 	}




diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/repo/LineStub.kt b/app/src/main/java/xyz/apiote/bimba/czwek/repo/LineStub.kt
index 86565ae24cf1322fba82acae25063e71bb026d28..26b4b729f25698d95be373a282a99c6066da3fb9 100644
--- a/app/src/main/java/xyz/apiote/bimba/czwek/repo/LineStub.kt
+++ b/app/src/main/java/xyz/apiote/bimba/czwek/repo/LineStub.kt
@@ -5,13 +5,21 @@
 package xyz.apiote.bimba.czwek.repo
 
 import android.content.Context
-import android.graphics.drawable.BitmapDrawable
+import android.graphics.Typeface
 import android.graphics.drawable.Drawable
 import android.os.Parcelable
+import android.text.Annotation
+import android.text.SpannableStringBuilder
+import android.text.Spanned
+import android.text.style.StyleSpan
+import android.util.Log
+import androidx.core.graphics.drawable.toDrawable
 import kotlinx.parcelize.Parcelize
+import xyz.apiote.bimba.czwek.RoundedBackgroundSpan
 import xyz.apiote.bimba.czwek.api.LineStubV1
 import xyz.apiote.bimba.czwek.api.LineStubV2
 import xyz.apiote.bimba.czwek.api.LineStubV3
+import xyz.apiote.bimba.czwek.repo.Stop.LineDecoration
 
 @Parcelize
 data class LineStub(val name: String, val kind: LineType, val colour: Colour) : LineAbstract, Parcelable {
@@ -20,6 +28,45 @@ 	constructor(l: LineStubV2) : this(l.name, LineType.of(l.kind), Colour(l.colour))
 	constructor(l: LineStubV3) : this(l.name, LineType.of(l.kind), Colour(l.colour))
 
 	fun icon(context: Context, scale: Float = 1f): Drawable {
-		return BitmapDrawable(context.resources, super.icon(context, kind, colour, scale))
+		return super.icon(context, kind, colour, scale).toDrawable(context.resources)
+	}
+
+	fun decorate(
+		str: SpannableStringBuilder,
+		decoration: LineDecoration,
+		annotation: Annotation?
+	): SpannableStringBuilder {
+		val spanStart = if (annotation != null) {
+			str.getSpanStart(annotation)
+		} else {
+			0
+		}
+		val spanEnd = if (annotation != null) {
+			str.getSpanEnd(annotation)
+		} else {
+			str.length
+		}
+
+		val background = RoundedBackgroundSpan(colour.toInt(), textColour(colour))
+		val ital = StyleSpan(Typeface.ITALIC)
+		when (decoration) {
+			LineDecoration.ITALICS -> str.setSpan(
+				ital,
+				spanStart, spanEnd,
+				Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
+			)
+
+			LineDecoration.COLOUR -> {
+				str.setSpan(
+					background,
+					spanStart, spanEnd,
+					Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
+				)
+			}
+
+			LineDecoration.NONE -> {}
+		}
+
+		return str
 	}
 }
\ No newline at end of file




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 1f0f19b8282e80f48a826ec163380a09b270afc6..6359c8153f193f226c7a4665a8760bd266f4c79c 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,19 +5,15 @@
 package xyz.apiote.bimba.czwek.repo
 
 import android.content.Context
-import android.graphics.Typeface
 import android.graphics.drawable.Drawable
 import android.os.Parcelable
 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 kotlinx.parcelize.Parcelize
 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
@@ -167,29 +163,7 @@ 				"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 -> {}
-					}
+					line.decorate(str, decoration, it)
 				}
 			}
 		}
@@ -215,10 +189,10 @@ 		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
+					"italics" -> ITALICS
+					"colour" -> COLOUR
+					"none" -> NONE
+					else -> ITALICS
 				}
 		}
 	}




diff --git a/app/src/main/res/layout/journey.xml b/app/src/main/res/layout/journey.xml
index be5120d120ac8514a2a112c47feef904733d0a6b..3ee0cb0a56ec6137a7dc092b3796f6ac35ed865d 100644
--- a/app/src/main/res/layout/journey.xml
+++ b/app/src/main/res/layout/journey.xml
@@ -25,17 +25,21 @@ 			app:layout_constraintStart_toStartOf="parent"
 			app:layout_constraintTop_toTopOf="parent"
 			tools:text="11:25" />
 
+		<!-- TODO line height multiplier -->
 		<com.google.android.material.textview.MaterialTextView
 			android:id="@+id/lines"
 			android:layout_width="0dp"
 			android:layout_height="wrap_content"
-			android:layout_marginStart="16dp"
-			android:layout_marginEnd="16dp"
+			android:layout_marginStart="8dp"
+			android:layout_marginTop="4dp"
+			android:layout_marginEnd="8dp"
+			android:lineSpacingExtra="18sp"
+			android:padding="4dp"
 			android:textAlignment="center"
 			android:textAppearance="@style/TextAppearance.Material3.HeadlineSmall"
-			app:layout_constraintEnd_toStartOf="@+id/end_time"
-			app:layout_constraintStart_toEndOf="@+id/start_time"
-			app:layout_constraintTop_toTopOf="@+id/start_time"
+			app:layout_constraintEnd_toEndOf="parent"
+			app:layout_constraintStart_toStartOf="parent"
+			app:layout_constraintTop_toBottomOf="@+id/start_time"
 			tools:text="Metropolitan, Circle, Hammersmith&amp;City" />
 
 		<com.google.android.material.textview.MaterialTextView
@@ -44,7 +48,7 @@ 			android:layout_width="wrap_content"
 			android:layout_height="wrap_content"
 			android:textAppearance="@style/TextAppearance.Material3.HeadlineSmall"
 			app:layout_constraintEnd_toEndOf="parent"
-			app:layout_constraintTop_toTopOf="@+id/lines"
+			app:layout_constraintTop_toTopOf="@+id/start_time"
 			tools:text="12:30" />
 
 		<LinearLayout




diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml
index a4545ae29ca307e330a99ba00ef35bb4082f99ee..f7eef407a55811571f2891156a693fa0affa2925 100644
--- a/app/src/main/res/values-fr/strings.xml
+++ b/app/src/main/res/values-fr/strings.xml
@@ -25,7 +25,7 @@ 	Parti
 	<string name="at_time">à %1$02d:%2$02d</string>
 	<string name="departure_headsign_content_description">vers %1$s</string>
 	<string name="on_demand">à la demande</string>
-	<string name="no_boarding">pas d\'embarquement</string>
+	<string name="no_boarding">pas d’embarquement</string>
 	<string name="on_boarding">Embarquement</string>
 	<string name="boarding">Embarquement possible</string>
 	<string name="line_headsign">» %1$s</string>
@@ -33,7 +33,7 @@ 	Arrêts à proximité
 	<string name="line_headsigns_content_description">entre %1$s et %2$s</string>
 	<string name="line_headsign_content_description">vers %1$s</string>
 	<string name="line_headsigns">%1$s «» %2$s</string>
-	<string name="bimba_server_token_hint">Jeton d\'identification</string>
+	<string name="bimba_server_token_hint">Jeton d’identification</string>
 	<string name="tickets_sold_content_description">Tickets vendus à bord</string>
 	<string name="usb_charging_content_description">Charge USB</string>
 	<string name="voice_announcements_content_description">Annonces vocales</string>
@@ -42,13 +42,13 @@ 	Avancé
 	<string name="title_feeds">Horaires</string>
 	<string name="error">Erreur</string>
 	<string name="onboarding_advanced_action">Choisir un serveur</string>
-	<string name="server_private_question">Ce serveur est privé et aucun jeton n\'a été fourni</string>
+	<string name="server_private_question">Ce serveur est privé et aucun jeton n’a été fourni</string>
 	<string name="stops_near_code">Arrêts à proximité de %1$s</string>
 	<string name="vehicle_headsign_content_description">%1$s vers %2$s</string>
 	<string name="title_about">A propos</string>
 	<string name="website_button_description">Lien vers le site</string>
 	<string name="use_online_feed">Utiliser le flux en ligne</string>
-	<string name="error_406">La version de l\'application est incompatible avec le serveur</string>
+	<string name="error_406">La version de l’application est incompatible avec le serveur</string>
 	<string name="title_filter">Filtrer</string>
 	<string name="cancel">Annuler</string>
 	<string name="more">Plus</string>
@@ -69,7 +69,7 @@ 	Le véhicule est accessible pour les fauteuils roulants
 	<string name="departure_momentarily">dans un instant</string>
 	<string name="error_offline">Vous êtes hors ligne. Connectez-vous à Internet</string>
 	<string name="off_boarding">Débarquement</string>
-	<string name="error_gps">Impossible d\'obtenir la position actuelle</string>
+	<string name="error_gps">Impossible d’obtenir la position actuelle</string>
 	<string name="speed_in_km_per_h">%1$s km/h</string>
 	<string name="occupancy_unknown">Inconnu</string>
 	<string name="occupancy_few_seats">Peu de sièges</string>
@@ -85,25 +85,25 @@ 	Choisir une ligne
 	<string name="last_update">Dernière mise à jour : %1$s</string>
 	<string name="title_servers">Serveurs</string>
 	<string name="title_cities">Localités</string>
-	<string name="information_may_be_outdated">L\'information peut être obsolète</string>
+	<string name="information_may_be_outdated">L’information peut être obsolète</string>
 	<string name="ok">OK</string>
-	<string name="no_location_message">L\'accès à la position est nécéssaire pour trouver les arrêts à proximité et afficher la position actuelle sur la carte. Les autres fonctions peuvent fonctionner sans cela. L\'accès peut être activé ou désactiver dans les paramètres du système à tout moment.</string>
+	<string name="no_location_message">L’accès à la position est nécéssaire pour trouver les arrêts à proximité et afficher la position actuelle sur la carte. Les autres fonctions peuvent fonctionner sans cela. L’accès peut être activé ou désactiver dans les paramètres du système à tout moment.</string>
 	<string name="stop_stub_on_demand_in_zone">Arrêt à la demande dans la zone %1$s</string>
 	<string name="stop_stub_in_zone">Arrêt dans la zone %1$s</string>
 	<string name="stop_stub_on_demand">Arrêt à la demande</string>
 	<string name="departure_headsign">» %1$s</string>
 	<string name="translation_button_description">Lien vers le service de traduction</string>
 	<string name="app_description">Assistant de transport public libre ; une fiche horaire dans votre poche.</string>
-	<string name="error_41">Cette localité n\'est pas couverte par le serveur</string>
+	<string name="error_41">Cette localité n’est pas couverte par le serveur</string>
 	<string name="current_timetable_validity">Horaire valide : du %1$s au %2$s</string>
 	<string name="filter_localities">Filtrer les localités</string>
-	<string name="stop_from_qr_code">Code QR d\'arrêt</string>
+	<string name="stop_from_qr_code">Code QR d’arrêt</string>
 	<string name="title_filter_byline">Filtrer par ligne</string>
 	<string name="title_filter_bytime">Filtrer par horaire</string>
-	<string name="title_select_time_end">Sélectionner l\'heure d\'arrivée</string>
-	<string name="title_select_time_start">Sélectionner l\'heure de départ</string>
+	<string name="title_select_time_end">Sélectionner l’heure d’arrivée</string>
+	<string name="title_select_time_start">Sélectionner l’heure de départ</string>
 	<string name="credits">Police yellowcircle8 (https://git.apiote.xyz/yellowcircle8.git) basée sur Railway Sans © Greg Fleming, OFL-1.1 https://github.com/davelab6/Railway-Sans\n\n Icône Mastodon (https://github.com/mastodon/joinmastodon) © contributeurs de Mastodon, AGPL-3.0-or-later\n\n Logo Bimba créé par https://github.com/tebriz159\n\n Icônes Material © Google, Apache-2.0\n\n Données cartographiques © contributeurs d’OpenStreetMap, ODbL-1.0\n\n Liste des villes utilisées pour le géocodage des codes short plus © Geonames (https://geonames.org), CC BY\n\n Logo Matrix™/® Matrix.org</string>
-	<string name="error_400">L\'application a effectué une requête mal formulée</string>
+	<string name="error_400">L’application a effectué une requête mal formulée</string>
 	<string name="error_404">Pas trouvé</string>
 	<string name="error_429">Limite dépassée. Réessayez plus tard</string>
 	<string name="onboarding_simple_action">Choisir la localité</string>
@@ -201,7 +201,7 @@ 	%1$s yd
 	<string name="distance_in_mi">%1$s mi</string>
 	<string name="distance_in_two_units_cd">%1$s et %2$s</string>
 	<string name="time_in_s">%1$s s</string>
-	<string name="map_attribution">© contributeurs d’&lt;a href=https://www.openstreetmap.org/copyright&gt;OpenStreetMap&lt;/a&gt;</string>
+	<string name="map_attribution"><![CDATA[© contributeurs d’<a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>]]></string>
 	<string name="no_email_app">Aucune application d’e-mail installée</string>
 	<string name="favourite_content_description">Enregistrer comme favori</string>
 	<string name="unfiltered">Non filtré</string>
@@ -225,9 +225,9 @@ 	un
 	<string name="filtered_stop_question">Voulez-vous enregistrer un favori filtré avec des lignes sélectionnées ?</string>
 	<string name="line_decorations">Décorations du nom de la ligne</string>
 	<string name="acra_notification_channel">Fil des rapports de problèmes</string>
-	<string name="acra_notification_channel_description">Notifications indiquant les problèmes et permettant d\'envoyer des rapports de problèmes</string>
+	<string name="acra_notification_channel_description">Notifications indiquant les problèmes et permettant d’envoyer des rapports de problèmes</string>
 	<string name="acra_notification_title">Bimba s’est arrêté</string>
-	<string name="acra_notification_text">Un blocage inattendu s\'est dressé sur le chemin de Bimba. Voulez-vous envoyer un rapport ?</string>
+	<string name="acra_notification_text">Un blocage inattendu s’est dressé sur le chemin de Bimba. Voulez-vous envoyer un rapport ?</string>
 	<string name="send">Envoyer</string>
 	<string name="discard">Supprimer</string>
 	<string name="send_with_comment">Envoyer avec commentaire</string>
@@ -236,9 +236,9 @@ 	Alertes
 	<string name="terminus_arrival_showing">Arrivées au terminus</string>
 	<string name="matrix_button_description">lien vers le salon Matrix</string>
 	<string name="email_button_description">lien vers le courriel</string>
-	<string name="transitous_description">Un service de transport public international, neutre sur le plan des fournisseurs et géré par la communauté. La couverture est disponible à l\'adresse suivante : https://transitous.org/sources/</string>
+	<string name="transitous_description">Un service de transport public international, neutre sur le plan des fournisseurs et géré par la communauté. La couverture est disponible à l’adresse suivante : https://transitous.org/sources/</string>
 	<string name="transitous_attribution">API Transitous (https://transitous.org) fournie par Spline (https://routing.spline.de). Localités (https://github.com/public-transport/transitous/tree/main/feeds) maintenues par la communauté.</string>
-	<string name="no_geocoding_data_description">La requête contient un code short plus mais il n\'y a pas de données de géocodage présentes. Téléchargez les données de géocodage ou activez leur mise à jour automatique dans les paramètres.</string>
+	<string name="no_geocoding_data_description">La requête contient un code short plus mais il n’y a pas de données de géocodage présentes. Téléchargez les données de géocodage ou activez leur mise à jour automatique dans les paramètres.</string>
 	<string name="congestion_congestion">ralentissements</string>
 	<string name="about_time">environ %1$02d:%2$02d</string>
 	<string name="italics">italique</string>
@@ -248,4 +248,4 @@ 	Départ
 	<string name="departure_arrived">Arrivé</string>
 	<string name="departure_approximate">Départ approximatif</string>
 	<string name="approximately">Approximativement</string>
-</resources>
\ No newline at end of file
+</resources>




diff --git a/fruchtfleisch/build.gradle.kts b/fruchtfleisch/build.gradle.kts
index 43fb515fc848c87a101c325f986d0cc4e2de095d..46f31384bc4e95115c60fb43676853c364c0227e 100644
--- a/fruchtfleisch/build.gradle.kts
+++ b/fruchtfleisch/build.gradle.kts
@@ -8,8 +8,8 @@     kotlin("jvm")
 }
 
 dependencies {
-    testImplementation("org.junit.jupiter:junit-jupiter:5.11.4")
-    testImplementation("org.junit.jupiter:junit-jupiter:5.11.4")
+    testImplementation("org.junit.jupiter:junit-jupiter:5.12.0")
+    testImplementation("org.junit.jupiter:junit-jupiter:5.12.0")
 
     //implementation("org.jetbrains.kotlin:kotlin-reflect:1.8.10")
 }




diff --git a/metadata/en-US/changelogs/36.txt b/metadata/en-US/changelogs/36.txt
new file mode 100644
index 0000000000000000000000000000000000000000..5d9b90e518374aa4790e7770deb1656621d7c0a9
--- /dev/null
+++ b/metadata/en-US/changelogs/36.txt
@@ -0,0 +1,2 @@
+* added travel planning based on Transitous
+* added translations by Arkadiusz, ArnaudDvs, Jonah Brüchert, Kemal Oktay Aktoğan, Priit Jõerüüt, Xapitonov, தமிழ்நேரம்