Author: Adam Evyčędo <git@apiote.xyz>
merge develop into master for version 3.4.0
%!v(PANIC=String method: strings: negative Repeat count)
diff --git a/README.adoc b/README.adoc index ca64b9acbe6363bc49bc599f03346fe86e70d549..f89c19fe5a0c108193f3166443f30c00901f5f57 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.3.2 2024-05-22 +v3.4.0 2024-07-03 :toc: Bimba is a FLOSS public transport passenger companion; a timetable in your pocket. diff --git a/app/build.gradle b/app/build.gradle index 95ad937cd17c833225b63ce882c5893c84872f58..54d50b5a32b00c98bde44d662ce7e94bfbe0df97 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -12,6 +12,7 @@ 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 { @@ -20,12 +21,12 @@ defaultConfig { applicationId "xyz.apiote.bimba.czwek" minSdk 21 - targetSdk 34 - versionCode 27 - versionName "3.3.2" + targetSdk 35 + versionCode 28 + versionName "3.4.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" - resourceConfigurations += ["en", "pl", "it", "de"] + resourceConfigurations += ["en", "pl", "it", "de", "fr"] } applicationVariants.configureEach { variant -> @@ -49,15 +50,19 @@ viewBinding true } namespace 'xyz.apiote.bimba.czwek' buildToolsVersion = '34.0.0' + + sourceSets { + main.java.srcDirs += 'src/main/proto' + } } dependencies { implementation 'androidx.core:core-ktx:1.13.1' - implementation 'androidx.appcompat:appcompat:1.6.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.0' - implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.0' + 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' @@ -77,8 +82,8 @@ coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.4' testImplementation 'junit:junit:4.13.2' - androidTestImplementation 'androidx.test.ext:junit:1.1.5' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' + androidTestImplementation 'androidx.test.ext:junit:1.2.1' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.6.1' } // NOTE fixes reproducible builds 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 836b721962f4c3a6f6056cffb95c1897a5d31ca6..cee5e4e154d8391de4925ee64a6afa39e1afce80 100644 --- a/app/src/main/java/xyz/apiote/bimba/czwek/AboutActivity.kt +++ b/app/src/main/java/xyz/apiote/bimba/czwek/AboutActivity.kt @@ -7,29 +7,49 @@ import android.content.Intent import android.net.Uri import android.os.Bundle +import androidx.activity.enableEdgeToEdge import androidx.appcompat.app.AppCompatActivity +import androidx.core.view.ViewCompat +import androidx.core.view.WindowInsetsCompat +import androidx.core.view.updatePadding import xyz.apiote.bimba.czwek.databinding.ActivityAboutBinding +import kotlin.math.max class AboutActivity : AppCompatActivity() { - private var _binding: ActivityAboutBinding? = null - private val binding get() = _binding!! - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - _binding = ActivityAboutBinding.inflate(layoutInflater) - setContentView(binding.root) + private var _binding: ActivityAboutBinding? = null + private val binding get() = _binding!! + override fun onCreate(savedInstanceState: Bundle?) { + enableEdgeToEdge() + super.onCreate(savedInstanceState) + _binding = ActivityAboutBinding.inflate(layoutInflater) + setContentView(binding.root) - 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"))) - } - binding.code.setOnClickListener { - startActivity(Intent(Intent.ACTION_VIEW, Uri.parse("https://git.apiote.xyz/Bimba.git"))) - } - binding.translate.setOnClickListener { - startActivity(Intent(Intent.ACTION_VIEW, Uri.parse("https://hosted.weblate.org/projects/bimba/"))) - } - } + ViewCompat.setOnApplyWindowInsetsListener(binding.root) { v, windowInsets -> + val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()) + val l = max(windowInsets.displayCutout?.safeInsetLeft ?: 0, insets.left) + val r = max(windowInsets.displayCutout?.safeInsetRight ?: 0, insets.right) + val t = max(windowInsets.displayCutout?.safeInsetTop ?: 0, insets.top) + v.updatePadding(left = l, right = r, top = t) + windowInsets + } + + 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"))) + } + binding.code.setOnClickListener { + startActivity(Intent(Intent.ACTION_VIEW, Uri.parse("https://git.apiote.xyz/Bimba.git"))) + } + binding.translate.setOnClickListener { + startActivity( + Intent( + Intent.ACTION_VIEW, + Uri.parse("https://hosted.weblate.org/projects/bimba/") + ) + ) + } + } } 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 3ebf1676b286d06d1505a26a162af9843274a88a..76953700a4a76e06a41716bcdcc43d07e50e1008 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 @@ -159,7 +159,8 @@ } fun mapHttpError(code: Int): Pair<Int, Int> { return when (code) { - 41 -> Pair(R.string.error_41, R.drawable.error_other) // TODO error_locality + 41 -> Pair(R.string.error_41, R.drawable.error_locality) + 44 -> Pair(R.string.error_44, R.drawable.error_departures) 400 -> Pair(R.string.error_400, R.drawable.error_app) 401 -> Pair(R.string.error_401, R.drawable.error_sec) 403 -> Pair(R.string.error_403, R.drawable.error_sec) @@ -205,15 +206,15 @@ private fun isNetworkAvailable(context: Context): Boolean { val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager - + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - connectivityManager.activeNetwork?.let {network -> - connectivityManager.getNetworkCapabilities(network)?.let {capabilities -> + connectivityManager.activeNetwork?.let { network -> + connectivityManager.getNetworkCapabilities(network)?.let { capabilities -> capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) || capabilities.hasTransport( NetworkCapabilities.TRANSPORT_CELLULAR ) || capabilities.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET) } - }?: false + } ?: false } else { @Suppress("DEPRECATION") connectivityManager.activeNetworkInfo?.isConnected ?: false 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 090e870cac6a76658ce555f4a8e7a0eac3f98355..b2a20c6ca37251d8b153b0789dbfa920fa015eb3 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 @@ -11,14 +11,17 @@ import android.os.Bundle import android.view.View import android.widget.Toast import androidx.activity.OnBackPressedCallback +import androidx.activity.enableEdgeToEdge import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.contract.ActivityResultContracts import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.content.res.AppCompatResources import androidx.core.content.ContextCompat import androidx.core.content.edit -import androidx.core.view.WindowCompat +import androidx.core.view.ViewCompat +import androidx.core.view.WindowInsetsCompat import androidx.core.view.get +import androidx.core.view.updatePadding import androidx.drawerlayout.widget.DrawerLayout.DrawerListener import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentManager @@ -47,12 +50,20 @@ private lateinit var permissionAsker: Fragment private var locationPermissionDialogShown = false override fun onCreate(savedInstanceState: Bundle?) { + enableEdgeToEdge() super.onCreate(savedInstanceState) binding = ActivityMainBinding.inflate(layoutInflater) setContentView(binding.root) getSharedPreferences("shp", MODE_PRIVATE).edit(true) { putBoolean("firstRun", false) + } + + ViewCompat.setOnApplyWindowInsetsListener(binding.root) { _, windowInsets -> + val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()) + val l = windowInsets.displayCutout?.safeInsetLeft?.takeIf { it > 0 } ?: insets.left + binding.navigationDrawer.updatePadding(left = l) + windowInsets } supportFragmentManager.registerFragmentLifecycleCallbacks( @@ -105,7 +116,6 @@ } false } - WindowCompat.setDecorFitsSystemWindows(window, false) val navView: BottomNavigationView = binding.bottomNavigation val navHostFragment = supportFragmentManager.findFragmentById(R.id.nav_host_fragment_activity_main) as NavHostFragment @@ -117,7 +127,7 @@ ActivityResultContracts.RequestMultiplePermissions() ) { permissions -> when { permissions[Manifest.permission.ACCESS_FINE_LOCATION] ?: false || - permissions[Manifest.permission.ACCESS_COARSE_LOCATION] ?: false -> { + permissions[Manifest.permission.ACCESS_COARSE_LOCATION] ?: false -> { when (permissionAsker) { is HomeFragment -> { showResults(ResultsActivity.Mode.MODE_LOCATION) @@ -133,10 +143,15 @@ else -> { if (locationPermissionDialogShown) { return@registerForActivityResult } - MaterialAlertDialogBuilder(this).setIcon(AppCompatResources.getDrawable(this, R.drawable.error_gps)) + MaterialAlertDialogBuilder(this).setIcon( + AppCompatResources.getDrawable( + this, + R.drawable.error_gps + ) + ) .setTitle(getString(R.string.no_location_access)) .setMessage(getString(R.string.no_location_message)) - .setPositiveButton(resources.getString(R.string.ok)) { _, _ ->} + .setPositiveButton(R.string.ok) { _, _ ->} .show() locationPermissionDialogShown = true } 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 new file mode 100644 index 0000000000000000000000000000000000000000..b3cb8722547c815be4076bc24615a101aba0a8f6 --- /dev/null +++ b/app/src/main/java/xyz/apiote/bimba/czwek/dashboard/ui/home/Favourites.kt @@ -0,0 +1,213 @@ +// SPDX-FileCopyrightText: Adam Evyčędo +// +// SPDX-License-Identifier: GPL-3.0-or-later + +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 +import android.widget.ImageView +import android.widget.TextView +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.RecyclerView +import xyz.apiote.bimba.czwek.R +import xyz.apiote.bimba.czwek.departures.DeparturesActivity +import xyz.apiote.bimba.czwek.repo.Departure +import xyz.apiote.bimba.czwek.repo.Favourite +import java.time.ZoneId +import java.time.ZonedDateTime +import java.util.Collections +import java.util.Optional + +class BimbaFavouritesAdapter( + private var favourites: List<Favourite>, + private var departures: Map<String, Optional<Departure>>, + private val inflater: LayoutInflater, + private val context: Context +) : + RecyclerView.Adapter<FavouriteViewHolder>() { + var lastUpdate: ZonedDateTime = + ZonedDateTime.of(0, 1, 1, 0, 0, 0, 0, ZoneId.systemDefault()) + private set + + inner class DiffUtilCallback( + private val oldFavourites: List<Favourite>, + private val oldDepartures: Map<String, Optional<Departure>?>, + private val newFavourites: List<Favourite>, + private val newDepartures: Map<String, Optional<Departure>?> + ) : DiffUtil.Callback() { + override fun getOldListSize() = oldFavourites.size + + override fun getNewListSize() = newFavourites.size + + override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int) = + oldFavourites[oldItemPosition].feedID + oldFavourites[oldItemPosition].stopCode == newFavourites[newItemPosition].feedID + newFavourites[newItemPosition].stopCode + + @Suppress("KotlinConstantConditions", "UNNECESSARY_NOT_NULL_ASSERTION") + override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { + val oldFav = oldFavourites[oldItemPosition] + val newFav = newFavourites[newItemPosition] + val oldDeparture = oldDepartures[oldFav.feedID + oldFav.stopCode] + val newDeparture = newDepartures[oldFav.feedID + oldFav.stopCode] + + if ((oldDeparture == null && newDeparture != null) || (oldDeparture != null && newDeparture == null)) { + return false + } + + val favouritesSame = oldFav.feedName == newFav.feedName && + oldFav.stopName == newFav.stopName && + oldFav.sequence == newFav.sequence && + oldFav.lines == newFav.lines + + if (!favouritesSame) { + return false + } + + if ((oldDeparture == null && newDeparture == null) || (oldDeparture!!.isEmpty && newDeparture!!.isEmpty)) { + return true + } + + if ((oldDeparture!!.isEmpty && !newDeparture!!.isEmpty) || (!oldDeparture!!.isEmpty && newDeparture!!.isEmpty)) { + return false + } + + return oldDeparture!!.get().ID == newDeparture!!.get().ID && + oldDeparture!!.get().vehicle.Line == newDeparture!!.get().vehicle.Line && + oldDeparture!!.get().vehicle.Headsign == newDeparture!!.get().vehicle.Headsign && + oldDeparture!!.get().statusText( + context, + false, + lastUpdate + ) == newDeparture!!.get().statusText(context, false) + } + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FavouriteViewHolder { + val rowView = inflater.inflate(R.layout.favourite, parent, false) + return FavouriteViewHolder(rowView) + } + + override fun getItemCount() = favourites.size + + override fun onBindViewHolder(holder: FavouriteViewHolder, position: Int) { + FavouriteViewHolder.bind( + favourites[position], holder, context, + departures[favourites[position].feedID + favourites[position].stopCode] + ) + } + + fun updateFavourites(favourites: List<Favourite>) { + val diff = DiffUtil.calculateDiff( + DiffUtilCallback( + this.favourites, + this.departures, + favourites, + this.departures + ) + ) + this.favourites = favourites + diff.dispatchUpdatesTo(this) + } + + fun updateDepartures(departures: Map<String, Optional<Departure>>) { + this.departures = departures + notifyDataSetChanged() + lastUpdate = ZonedDateTime.now() + } + + fun swap(from: Int, to: Int): List<Favourite> { + Collections.swap(favourites, from, to) + favourites = favourites.mapIndexed { i, it -> + it.copy(sequence = i) + } + notifyItemMoved(from, to) + return favourites + } + + fun delete(position: Int): Pair<List<Favourite>, Favourite> { + val removedFavourite = favourites[position] + favourites = favourites.filterIndexed { i, _ -> + i != position + }.mapIndexed { i, it -> + it.copy(sequence = i) + } + notifyItemRemoved(position) + return Pair(favourites, removedFavourite) + } + + fun insert(removedFavourite: Favourite): List<Favourite> { + favourites = favourites.toMutableList().apply { + add(removedFavourite.sequence!!, removedFavourite) + }.mapIndexed { i, it -> + it.copy(sequence = i) + } + return favourites + } +} + +class FavouriteViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { + val root: View = itemView.findViewById(R.id.favourite) + val feedName: TextView = itemView.findViewById(R.id.feed_name) + val lineIcon: ImageView = itemView.findViewById(R.id.line_icon) + val departureTime: TextView = itemView.findViewById(R.id.departure_time) + val departureTimeFull: TextView = itemView.findViewById(R.id.departure_full_time) + val lineName: TextView = itemView.findViewById(R.id.departure_line) + val headsign: TextView = itemView.findViewById(R.id.departure_headsign) + val stopHeadline: TextView = itemView.findViewById(R.id.stop_name) + + companion object { + fun bind( + favourite: Favourite, + holder: FavouriteViewHolder, + context: Context, + departure: Optional<Departure>? + ) { + if (departure == null) { + holder.feedName.text = favourite.feedName + holder.stopHeadline.text = favourite.stopName + holder.lineIcon.setImageDrawable(null) + holder.lineName.text = context.getString(R.string.loading) + holder.departureTime.text = "" + holder.departureTimeFull.text = "" + holder.headsign.text = "" + } else if (departure.isEmpty) { + holder.feedName.text = favourite.feedName + holder.stopHeadline.text = favourite.stopName + holder.lineIcon.setImageDrawable(null) + holder.lineName.text = context.getString(R.string.no_next_departures) + holder.departureTime.text = "" + holder.departureTimeFull.text = "" + holder.headsign.text = "" + } else { + val vehicle = departure.get().vehicle + holder.feedName.text = favourite.feedName + holder.stopHeadline.text = favourite.stopName + holder.lineIcon.setImageDrawable(vehicle.Line.icon(context)) + holder.lineIcon.contentDescription = vehicle.Line.kind.name + holder.lineName.text = vehicle.Line.name + holder.departureTime.text = departure.get().statusText(context, false) + holder.departureTimeFull.text = departure.get().timeString(context) + holder.headsign.text = + context.getString(R.string.departure_headsign, vehicle.Headsign) + holder.headsign.contentDescription = + context.getString( + R.string.departure_headsign_content_description, + vehicle.Headsign + ) + } + + 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) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/dashboard/ui/home/HomeFragment.kt b/app/src/main/java/xyz/apiote/bimba/czwek/dashboard/ui/home/HomeFragment.kt index 937651e34f7d64ab76dc0caeecf9ae19f23374ce..d3d3c1afc89244763f309441eb5c24275862d30d 100644 --- a/app/src/main/java/xyz/apiote/bimba/czwek/dashboard/ui/home/HomeFragment.kt +++ b/app/src/main/java/xyz/apiote/bimba/czwek/dashboard/ui/home/HomeFragment.kt @@ -5,28 +5,50 @@ package xyz.apiote.bimba.czwek.dashboard.ui.home import android.os.Bundle +import android.os.CountDownTimer import android.view.KeyEvent import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import android.widget.FrameLayout import android.widget.TextView import androidx.activity.OnBackPressedCallback import androidx.core.view.ViewCompat import androidx.core.view.WindowInsetsCompat +import androidx.core.view.updateLayoutParams +import androidx.core.view.updatePadding import androidx.fragment.app.Fragment import androidx.lifecycle.ViewModelProvider +import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.LinearLayoutManager -import com.google.android.material.search.SearchView +import androidx.recyclerview.widget.RecyclerView +import com.google.android.material.search.SearchView.TransitionState +import com.google.android.material.snackbar.Snackbar +import xyz.apiote.bimba.czwek.R import xyz.apiote.bimba.czwek.dashboard.MainActivity import xyz.apiote.bimba.czwek.databinding.FragmentHomeBinding +import xyz.apiote.bimba.czwek.dpToPixelI +import xyz.apiote.bimba.czwek.repo.Favourite import xyz.apiote.bimba.czwek.search.BimbaResultsAdapter +import xyz.apiote.bimba.czwek.units.Millisecond +import xyz.apiote.bimba.czwek.units.Second class HomeFragment : Fragment() { private var _binding: FragmentHomeBinding? = null private val binding get() = _binding!! private lateinit var adapter: BimbaResultsAdapter + private lateinit var favouritesAdapter: BimbaFavouritesAdapter + private lateinit var viewModel: HomeViewModel + + private val countdown = + object : CountDownTimer(Millisecond(Second(30)).millis, Millisecond(Second(10)).millis) { + override fun onTick(millisUntilFinished: Long) { + } + + override fun onFinish() { + refreshDepartures() + } + } override fun onCreateView( inflater: LayoutInflater, @@ -35,35 +57,40 @@ savedInstanceState: Bundle? ): View { _binding = FragmentHomeBinding.inflate(inflater, container, false) - val homeViewModel = + viewModel = ViewModelProvider(this)[HomeViewModel::class.java] - homeViewModel.queryables.observe(viewLifecycleOwner) { - adapter.feedsSettings = homeViewModel.feedsSettings - adapter.feeds = homeViewModel.feeds + viewModel.queryables.observe(viewLifecycleOwner) { + adapter.feedsSettings = viewModel.feedsSettings + adapter.feeds = viewModel.feeds adapter.update(it) } - - val root = binding.root - ViewCompat.setOnApplyWindowInsetsListener(root) { view, windowInsets -> - val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()) - view.layoutParams = (view.layoutParams as FrameLayout.LayoutParams).apply { - topMargin = insets.top - } - WindowInsetsCompat.CONSUMED + viewModel.favourites.observe(viewLifecycleOwner) { + favouritesAdapter.updateFavourites(it) + refreshDepartures() + } + viewModel.departures.observe(viewLifecycleOwner) { + favouritesAdapter.updateDepartures(it) } + countdown.start() + + binding.searchView.setupWithSearchBar(binding.searchBar) + val onBackPressedCallback = object : - OnBackPressedCallback(binding.searchView.currentTransitionState == SearchView.TransitionState.SHOWN) { + OnBackPressedCallback(binding.searchView.currentTransitionState == TransitionState.SHOWN) { override fun handleOnBackPressed() { - binding.searchView.hide() + binding.searchBar.collapse(binding.searchView) } } activity?.onBackPressedDispatcher?.addCallback(onBackPressedCallback) binding.searchView.addTransitionListener { _, _, newState -> onBackPressedCallback.isEnabled = when (newState) { - SearchView.TransitionState.SHOWN -> true - SearchView.TransitionState.HIDDEN -> false + TransitionState.SHOWN -> true + TransitionState.HIDDEN -> false else -> false + } + if (newState === TransitionState.HIDING) { + adapter.update(listOf()) } } @@ -75,7 +102,7 @@ adapter = BimbaResultsAdapter(layoutInflater, activity, listOf()) binding.suggestionsRecycler.adapter = adapter binding.searchView.editText.addTextChangedListener( - homeViewModel.SearchBarWatcher(requireContext()) + viewModel.SearchBarWatcher(requireContext()) ) binding.searchView.editText.setOnKeyListener { v, keyCode, event -> when (keyCode) { @@ -96,11 +123,101 @@ binding.floatingActionButton.setOnClickListener { (context as MainActivity).onGpsClicked(this) } + ViewCompat.setOnApplyWindowInsetsListener(binding.root) { _, windowInsets -> + val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()) + binding.floatingActionButton.updateLayoutParams<ViewGroup.MarginLayoutParams> { + rightMargin = insets.right + dpToPixelI(16f) + } + binding.searchBar.updateLayoutParams<ViewGroup.MarginLayoutParams> { + rightMargin = insets.right + dpToPixelI(8f) + leftMargin = insets.left + dpToPixelI(8f) + topMargin = insets.top + dpToPixelI(8f) + } + binding.suggestionsRecycler.updatePadding(left = insets.left, right = insets.right) + windowInsets.displayCutout?.safeInsetLeft?.let { + binding.suggestionsRecycler.updateLayoutParams<ViewGroup.MarginLayoutParams> { + leftMargin = it + } + } + binding.favourites.updatePadding(left = insets.left, right = insets.right) + binding.favourites.updateLayoutParams<ViewGroup.MarginLayoutParams> { + windowInsets.displayCutout?.safeInsetLeft?.let { + leftMargin = it + dpToPixelI(16f) + } + windowInsets.displayCutout?.safeInsetRight?.let { + rightMargin = it + dpToPixelI(16f) + } + } + windowInsets + } + + binding.favourites.layoutManager = LinearLayoutManager(context) + favouritesAdapter = BimbaFavouritesAdapter(listOf(), mapOf(), layoutInflater, requireContext()) + binding.favourites.adapter = favouritesAdapter + + viewModel.getFavourites(requireContext()) + + val ith = ItemTouchHelper(object : ItemTouchHelper.Callback() { + var newFavourites = emptyList<Favourite>() + override fun onMove( + recyclerView: RecyclerView, + viewHolder: RecyclerView.ViewHolder, + target: RecyclerView.ViewHolder + ): Boolean { + newFavourites = + favouritesAdapter.swap(viewHolder.absoluteAdapterPosition, target.absoluteAdapterPosition) + return true + } + + override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) { + val (newFavourites, removedFavourite) = favouritesAdapter.delete(viewHolder.absoluteAdapterPosition) + this.newFavourites = newFavourites + Snackbar.make(binding.fragmentRoot, R.string.favourite_deleted, Snackbar.LENGTH_LONG) + .setAction(R.string.undo) { + this.newFavourites = favouritesAdapter.insert(removedFavourite) + viewModel.saveFavourites(this.newFavourites, requireContext()) + } + .show() + } + + override fun clearView(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder) { + super.clearView(recyclerView, viewHolder) + + viewModel.saveFavourites(newFavourites, requireContext()) + } + + override fun getMovementFlags( + recyclerView: RecyclerView, + viewHolder: RecyclerView.ViewHolder + ): Int { + return makeFlag( + ItemTouchHelper.ACTION_STATE_DRAG, + ItemTouchHelper.DOWN or ItemTouchHelper.UP + ) or makeFlag( + ItemTouchHelper.ACTION_STATE_SWIPE, + ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT + ) + } + }) + ith.attachToRecyclerView(binding.favourites) + + return binding.root } + fun refreshDepartures() { + viewModel.getDepartures(requireContext()) + countdown.start() + } + + override fun onResume() { + super.onResume() + viewModel.getFavourites(requireContext()) + } + override fun onDestroyView() { super.onDestroyView() _binding = null + countdown.cancel() } } diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/dashboard/ui/home/HomeViewModel.kt b/app/src/main/java/xyz/apiote/bimba/czwek/dashboard/ui/home/HomeViewModel.kt index d42be675e2f6c4b569a608c3c0e66e7598f1b4ab..ba6da66f9308e7dc91bd81aca46cff37c3dffee1 100644 --- a/app/src/main/java/xyz/apiote/bimba/czwek/dashboard/ui/home/HomeViewModel.kt +++ b/app/src/main/java/xyz/apiote/bimba/czwek/dashboard/ui/home/HomeViewModel.kt @@ -14,19 +14,30 @@ import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.launch +import xyz.apiote.bimba.czwek.repo.Departure +import xyz.apiote.bimba.czwek.repo.Favourite import xyz.apiote.bimba.czwek.repo.FeedInfo import xyz.apiote.bimba.czwek.repo.OfflineRepository import xyz.apiote.bimba.czwek.repo.OnlineRepository import xyz.apiote.bimba.czwek.repo.Queryable import xyz.apiote.bimba.czwek.repo.TrafficResponseException import xyz.apiote.bimba.czwek.settings.feeds.FeedsSettings +import java.sql.SQLException +import java.util.Optional class HomeViewModel : ViewModel() { private val mutableQueryables = MutableLiveData<List<Queryable>>() val queryables: LiveData<List<Queryable>> = mutableQueryables var feeds: Map<String, FeedInfo>? = null var feedsSettings: FeedsSettings? = null + private val mutableFavourites = MutableLiveData<List<Favourite>>() + val favourites: LiveData<List<Favourite>> = mutableFavourites + private val mutableDepartures = MutableLiveData<Map<String, Optional<Departure>>>() + val departures: LiveData<Map<String, Optional<Departure>>> = mutableDepartures fun getQueryables(query: String, context: Context) { viewModelScope.launch { @@ -34,15 +45,87 @@ try { getFeeds(context) mutableQueryables.value = OnlineRepository().queryQueryables(query, context) ?: emptyList() } catch (e: TrafficResponseException) { - // xxx intentionally no error showing in suggestions + // XXX intentionally no error showing in suggestions Log.e("Suggestion", "$e") } } } + fun getFavourites(context: Context) { + viewModelScope.launch { + try { + getFeeds(context) + val repository = OfflineRepository(context) + mutableFavourites.value = + repository.getFavourites(feedsSettings?.activeFeeds() ?: emptySet()) + repository.close() + } catch (e: SQLException) { + Log.w("FavouritesForFavourite", "$e") + } + getDeparturesOnly(context) + } + } + + fun getDepartures(context: Context) { + viewModelScope.launch { + getDeparturesOnly(context) + } + } + + private suspend fun getDeparturesOnly(context: Context) { + coroutineScope { + if (favourites.value == null) + return@coroutineScope + mutableDepartures.value = favourites.value!!.map { favourite -> + async { + try { + val repository = OnlineRepository() + val stopDepartures = + repository.getDepartures( + favourite.feedID, + favourite.stopCode, + null, + context, + 12 // XXX heuristics + ) + stopDepartures?.let { sDs -> + if (sDs.departures.isEmpty()) { + Pair(favourite.feedID+favourite.stopCode, Optional.empty()) + } else { + Pair(favourite.feedID+favourite.stopCode, Optional.ofNullable(sDs.departures.find { departure -> + favourite.lines.isEmpty() or favourite.lines.contains( + departure.vehicle.Line.name + ) + })) + } + } ?: Pair(favourite.feedID+favourite.stopCode, Optional.empty()) + } catch (e: TrafficResponseException) { + Log.w("DeparturesForFavourite", "$e") + Pair(favourite.feedID+favourite.stopCode, Optional.empty()) + } + } + }.awaitAll().associate { it } + } + } + private suspend fun getFeeds(context: Context) { - feeds = OfflineRepository().getFeeds(context) + val repository = OfflineRepository(context) + feeds = repository.getFeeds(context) + repository.close() feedsSettings = FeedsSettings.load(context) + } + + fun saveFavourites(newFavourites: List<Favourite>, context: Context) { + viewModelScope.launch { + try { + val repository = OfflineRepository(context) + repository.saveFavourites(newFavourites.toSet()) + mutableFavourites.value = newFavourites + repository.close() + } catch (e: SQLException) { + Log.w("FavouritesForFavourite", "$e") + } + } } 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 5ac3de0f96d50708abd7e8c71f2e31aaaa772966..42dcc1a330de20dc80aa99bab9e5755931c00351 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 @@ -11,12 +11,12 @@ import android.content.res.Configuration.UI_MODE_NIGHT_MASK import android.content.res.Configuration.UI_MODE_NIGHT_UNDEFINED import android.content.res.Configuration.UI_MODE_NIGHT_YES import android.graphics.Bitmap +import android.os.Build import android.os.Bundle import android.os.Handler import android.os.Looper import android.text.Html import android.text.method.LinkMovementMethod -import android.util.Log import android.view.Gravity import android.view.LayoutInflater import android.view.View @@ -27,6 +27,9 @@ import androidx.appcompat.content.res.AppCompatResources import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.core.content.edit import androidx.core.graphics.drawable.toBitmap +import androidx.core.view.ViewCompat +import androidx.core.view.WindowInsetsCompat +import androidx.core.view.updateLayoutParams import androidx.fragment.app.Fragment import androidx.lifecycle.ViewModelProvider import com.google.android.material.snackbar.Snackbar @@ -81,7 +84,7 @@ val root: View = binding.root binding.map.setTileSource(TileSourceFactory.MAPNIK) if (((context?.resources?.configuration?.uiMode ?: UI_MODE_NIGHT_UNDEFINED) - and UI_MODE_NIGHT_MASK) == UI_MODE_NIGHT_YES + and UI_MODE_NIGHT_MASK) == UI_MODE_NIGHT_YES ) { binding.map.overlayManager.tilesOverlay.setColorFilter(TilesOverlay.INVERT_COLORS) } @@ -129,12 +132,15 @@ false } val attributionOverlay = TextView(context).apply { - // TODO deprecated in API 24 - text = + text = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + Html.fromHtml(getString(R.string.map_attribution), 0) + } else { + @Suppress("DEPRECATION") Html.fromHtml(getString(R.string.map_attribution)) + } linksClickable = true movementMethod = LinkMovementMethod.getInstance() - setPadding(10,10,10,10) + setPadding(10, 10, 10, 10) } val layoutParams = CoordinatorLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT).apply { gravity = Gravity.END or Gravity.BOTTOM @@ -145,12 +151,22 @@ layoutParams ) attributionOverlay.bringToFront() + ViewCompat.setOnApplyWindowInsetsListener(binding.root) { _, windowInsets -> + val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()) + binding.floatingActionButton.updateLayoutParams<ViewGroup.MarginLayoutParams> { + rightMargin += insets.right + } + attributionOverlay.updateLayoutParams<ViewGroup.MarginLayoutParams> { + rightMargin += insets.right + } + windowInsets + } + return root } private fun onMapMove() { snack?.dismiss() - Log.i("ZOOM", "${binding.map.zoomLevelDouble}") delayGetLocatables() } @@ -263,9 +279,9 @@ locationOverlay.disableMyLocation() val centre = binding.map.mapCenter context?.let { ctx -> ctx.getSharedPreferences("shp", MODE_PRIVATE).edit(true) { - this.putFloat("mapCentreLat", centre.latitude.toFloat()) - this.putFloat("mapCentreLon", centre.longitude.toFloat()) - this.putFloat("mapZoom", binding.map.zoomLevelDouble.toFloat()) + putFloat("mapCentreLat", centre.latitude.toFloat()) + putFloat("mapCentreLon", centre.longitude.toFloat()) + putFloat("mapZoom", binding.map.zoomLevelDouble.toFloat()) } } handler.removeCallbacks(workRunnable) 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 7b334c3d4efecdfa7b18e8d98c1de5bb9d8e1e6c..b9b2871dd953aab86b88c501729cc02801e8f705 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 @@ -14,12 +14,14 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import android.widget.Button import android.widget.ImageView import android.widget.LinearLayout import android.widget.TextView import androidx.appcompat.widget.TooltipCompat import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView +import androidx.recyclerview.widget.RecyclerView.ViewHolder import com.google.android.material.bottomsheet.BottomSheetDialog import com.google.android.material.bottomsheet.BottomSheetDialogFragment import com.google.android.material.dialog.MaterialAlertDialogBuilder @@ -33,15 +35,16 @@ import org.osmdroid.views.overlay.TilesOverlay import org.osmdroid.views.overlay.gestures.RotationGestureOverlay import xyz.apiote.bimba.czwek.R import xyz.apiote.bimba.czwek.dpToPixelI +import xyz.apiote.bimba.czwek.repo.Alert import xyz.apiote.bimba.czwek.repo.CongestionLevel import xyz.apiote.bimba.czwek.repo.Departure +import xyz.apiote.bimba.czwek.repo.DepartureItem import xyz.apiote.bimba.czwek.repo.OccupancyStatus import xyz.apiote.bimba.czwek.repo.Vehicle import java.time.ZoneId import java.time.ZonedDateTime - -class BimbaDepartureViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { +class BimbaDepartureViewHolder(itemView: View) : ViewHolder(itemView) { val root: View = itemView.findViewById(R.id.departure) val lineIcon: ImageView = itemView.findViewById(R.id.line_icon) val departureTime: TextView = itemView.findViewById(R.id.departure_time) @@ -71,6 +74,36 @@ departure.vehicle.Headsign ) holder?.departureTime?.text = departure.statusText(context, showAsTime) + } + } +} + +class BimbaAlertViewHolder(itemView: View) : ViewHolder(itemView) { + val root: View = itemView.findViewById(R.id.alerts) + val text: TextView = itemView.findViewById(R.id.alerts_text) + val moreButton: Button = itemView.findViewById(R.id.more_button) + + companion object { + fun bind( + alerts: List<Alert>, + holder: BimbaAlertViewHolder?, + context: Context? + ) { + val alertDescriptions = alerts.map { it.description }.filter { it != "" } + .joinToString(separator = "\n") + holder?.moreButton?.setOnClickListener{ + MaterialAlertDialogBuilder(context!!) + .setTitle("Alerts") + .setPositiveButton(R.string.ok) { _, _ -> } + .setMessage(alertDescriptions) + .show() + } + holder?.moreButton?.visibility = if (alertDescriptions == "") View.GONE else View.VISIBLE + holder?.text?.text = alerts.map { + it.header.ifEmpty { + context!!.getString(R.string.alert_header) + } + }.toSet().joinToString(separator = "\n") } } } @@ -78,82 +111,124 @@ class BimbaDeparturesAdapter( private val inflater: LayoutInflater, private val context: Context?, - private var departures: List<Departure>, + private var items: List<DepartureItem>, private val onClickListener: ((Departure) -> Unit) ) : - RecyclerView.Adapter<BimbaDepartureViewHolder>() { + RecyclerView.Adapter<ViewHolder>() { var lastUpdate: ZonedDateTime = ZonedDateTime.of(0, 1, 1, 0, 0, 0, 0, ZoneId.systemDefault()) private set private var showAsTime: Boolean = false inner class DiffUtilCallback( - private val oldDepartures: List<Departure>, - private val newDepartures: List<Departure>, - private val showAsTimeChanged: Boolean + private val oldDepartures: List<DepartureItem>, + private val newDepartures: List<DepartureItem>, + private val showAsTimeChanged: Boolean, ) : DiffUtil.Callback() { override fun getOldListSize() = oldDepartures.size override fun getNewListSize() = newDepartures.size override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int) = - oldDepartures[oldItemPosition].ID == newDepartures[newItemPosition].ID + (oldDepartures[oldItemPosition].departure?.ID + ?: "alert") == (newDepartures[newItemPosition].departure?.ID ?: "alert") override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { val oldDeparture = oldDepartures[oldItemPosition] val newDeparture = newDepartures[newItemPosition] - return oldDeparture.vehicle.Line == newDeparture.vehicle.Line && oldDeparture.vehicle.Headsign == newDeparture.vehicle.Headsign && - oldDeparture.statusText(context, false, lastUpdate) == newDeparture.statusText( - context, + return if (oldDeparture.departure != null && newDeparture.departure != null) { + oldDeparture.departure.vehicle.Line == newDeparture.departure.vehicle.Line && + oldDeparture.departure.vehicle.Headsign == newDeparture.departure.vehicle.Headsign && + oldDeparture.departure.statusText( + context, + false, + lastUpdate + ) == newDeparture.departure.statusText(context, false) && !showAsTimeChanged + } else if (oldDeparture.alert.isNotEmpty() && newDeparture.alert.isEmpty()) { + oldDeparture.alert == newDeparture.alert + } else { false - ) && !showAsTimeChanged + } } } private var departuresPositions: MutableMap<String, Int> = HashMap() init { - departures.forEachIndexed { i, departure -> - departuresPositions[departure.ID] = i + items.forEachIndexed { i, departure -> + departuresPositions[departure.departure?.ID ?: "alert"] = i + } + } + + override fun getItemViewType(position: Int): Int { + return if (items[position].departure != null) { + 0 + } else { + 1 } } - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BimbaDepartureViewHolder { - val rowView = inflater.inflate(R.layout.departure, parent, false) - return BimbaDepartureViewHolder(rowView) + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + if (viewType == 0) { + val rowView = inflater.inflate(R.layout.departure, parent, false) + return BimbaDepartureViewHolder(rowView) + } else { + val rowView = inflater.inflate(R.layout.alert, parent, false) + return BimbaAlertViewHolder(rowView) + } } - override fun onBindViewHolder(holder: BimbaDepartureViewHolder, position: Int) { - BimbaDepartureViewHolder.bind( - departures[position], - holder, - context, - showAsTime, - onClickListener - ) + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + if (holder is BimbaDepartureViewHolder) { + BimbaDepartureViewHolder.bind( + items[position].departure!!, + holder, + context, + showAsTime, + onClickListener + ) + } else { + BimbaAlertViewHolder.bind(items[position].alert, holder as BimbaAlertViewHolder, context) + } } - override fun getItemCount(): Int = departures.size + override fun getItemCount(): Int = items.size - fun get(id: String): Departure? { + fun get(id: String): DepartureItem? { val position = departuresPositions[id] return if (position == null) { null } else { - departures[position] + items[position] } } - fun update(departures: List<Departure>, showAsTime: Boolean, areNewObserved: Boolean = false) { + fun update( + departures: List<DepartureItem>, + showAsTime: Boolean, + areNewObserved: Boolean = false, + leaveAlert: Boolean = false + ) { + val newDepartures = if (leaveAlert && items.getOrNull(0)?.alert?.isNotEmpty() == true) { + listOf(items[0]) + departures + } else { + departures + } val newPositions: MutableMap<String, Int> = HashMap() - departures.forEachIndexed { i, departure -> - newPositions[departure.ID] = i + newDepartures.forEachIndexed { i, departure -> + newPositions[departure.departure?.ID ?: "alert"] = i } - val diff = DiffUtil.calculateDiff(DiffUtilCallback(this.departures, departures, this.showAsTime != showAsTime)) + val diff = DiffUtil.calculateDiff( + DiffUtilCallback( + this.items, + newDepartures, + this.showAsTime != showAsTime + ) + ) this.showAsTime = showAsTime - this.departures = departures + this.items = newDepartures departuresPositions = newPositions if (areNewObserved) { lastUpdate = ZonedDateTime.now() @@ -162,7 +237,7 @@ diff.dispatchUpdatesTo(this) } fun refreshItems() { - update(this.departures, showAsTime) + update(this.items, showAsTime) } } @@ -188,7 +263,7 @@ } fun update(departure: Departure) { this.departure = departure - this.view?.let { context?.let { ctx -> setContent(it, ctx, true) } } + view?.let { context?.let { ctx -> setContent(it, ctx, true) } } } private fun setContent(view: View, ctx: Context, updating: Boolean = false) { 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 3ace64fd176fe6079d89e6ecd848ed72e9a42ec6..783606925186535e8a5ba30761e3b1ae932b6b2d 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 @@ -12,15 +12,19 @@ import android.net.NetworkCapabilities import android.net.NetworkRequest import android.os.Build import android.os.Bundle -import android.os.Handler -import android.os.Looper +import android.os.CountDownTimer import android.text.format.DateUtils import android.text.format.DateUtils.MINUTE_IN_MILLIS import android.view.View +import android.widget.Toast +import androidx.activity.enableEdgeToEdge import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.content.res.AppCompatResources import androidx.core.content.res.ResourcesCompat +import androidx.core.view.ViewCompat import androidx.core.view.WindowCompat +import androidx.core.view.WindowInsetsCompat +import androidx.core.view.updatePadding import androidx.lifecycle.ViewModelProvider import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView @@ -29,14 +33,21 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.snackbar.Snackbar import com.google.android.material.timepicker.MaterialTimePicker import com.google.android.material.timepicker.TimeFormat -import kotlinx.coroutines.Runnable +import kotlinx.coroutines.MainScope +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking 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.repo.Departure +import xyz.apiote.bimba.czwek.repo.DepartureItem +import xyz.apiote.bimba.czwek.repo.Favourite +import xyz.apiote.bimba.czwek.repo.OfflineRepository import xyz.apiote.bimba.czwek.repo.Stop +import xyz.apiote.bimba.czwek.units.Millisecond +import xyz.apiote.bimba.czwek.units.Second +import xyz.apiote.bimba.czwek.units.Tim import java.time.Instant -import java.time.LocalDate import java.time.LocalTime import java.time.ZoneId import java.time.ZonedDateTime @@ -47,10 +58,6 @@ private val binding get() = _binding!! private lateinit var adapter: BimbaDeparturesAdapter - private val handler = Handler(Looper.getMainLooper()) - private var runnable = Runnable {} - - private var openBottomSheet: DepartureBottomSheet? = null private lateinit var snackbar: Snackbar private lateinit var viewModel: DeparturesViewModel @@ -62,66 +69,99 @@ .build() private var timePickerStart: MaterialTimePicker? = null private var timePickerEnd: MaterialTimePicker? = null private var linePicker: MaterialAlertDialogBuilder? = null - private var date: LocalDate? = null - private val linesFilter = mutableMapOf<String, Boolean>() private val linesFilterTemporary = mutableMapOf<String, Boolean>() - private var startTime: LocalTime = LocalTime.MIN - private var endTime: LocalTime = LocalTime.MAX - private var alertDescriptions: String = "" + + // TODO [elizabeth] millisInFuture from header Cache-Control max-age + private val countdown = + object : CountDownTimer(Millisecond(Second(30)).millis, Millisecond(Tim(1)).millis) { + override fun onTick(millisUntilFinished: Long) { + val timsUntillFinished = Tim(Millisecond(millisUntilFinished)) + binding.departuresUpdatesProgress.progress = timsUntillFinished.tims + } + + override fun onFinish() { + getDepartures() + } + } override fun onCreate(savedInstanceState: Bundle?) { + enableEdgeToEdge() super.onCreate(savedInstanceState) _binding = ActivityDeparturesBinding.inflate(layoutInflater) setContentView(binding.root) + ViewCompat.setOnApplyWindowInsetsListener(binding.departuresRecycler) { v, windowInsets -> + windowInsets.displayCutout?.safeInsetRight?.let { v.updatePadding(right = it) } + windowInsets.displayCutout?.safeInsetLeft?.let { v.updatePadding(left = it) } + val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()) + v.updatePadding(bottom = insets.bottom) + WindowInsetsCompat.CONSUMED + } + ViewCompat.setOnApplyWindowInsetsListener(binding.root) { v, windowInsets -> + val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()) + v.updatePadding(right = insets.right, left = insets.left) + windowInsets + } + viewModel = ViewModelProvider(this)[DeparturesViewModel::class.java] + getLine()?.let { + viewModel.mutableLinesFilter.value = mapOf(Pair(it, true)) + } + + getLines()?.associate { Pair(it, true) }?.let { + if (it.isNotEmpty()) { + viewModel.mutableLinesFilter.value = it + } + } + linePicker = MaterialAlertDialogBuilder(this) .setTitle(resources.getString(R.string.title_select_line)) .setNegativeButton(R.string.clear_date_selection) { _, _ -> - linesFilter.clear() - getDepartures() + viewModel.mutableLinesFilter.value = emptyMap() + //getDepartures() } .setPositiveButton(R.string.ok) { _, _ -> - linesFilterTemporary.forEach { linesFilter[it.key] = it.value } - getDepartures() + viewModel.mutableLinesFilter.value = emptyMap() + viewModel.mutableLinesFilter.value = linesFilterTemporary + //getDepartures() } - binding.moreButton.setOnClickListener { - MaterialAlertDialogBuilder(this) - .setTitle("Alerts") - .setPositiveButton(R.string.ok) { _, _ -> } - .setMessage(alertDescriptions) - .show() + viewModel.linesFilter.observe(this) { + // TODO if is before we got departures, do nothing + val departures = viewModel.departures.value?.departures ?: emptyList() + updateItems(departures + .filter { d -> + it.values.all { !it } or (it[d.vehicle.Line.name] ?: false) + } + .filter { d -> + val t = LocalTime.of(d.time.Hour.toInt(), d.time.Minute.toInt()) + t >= viewModel.startTime && t <= viewModel.endTime + }.map { DepartureItem(it) }, + null, + true + ) } viewModel.departures.observe(this) { stopDepartures -> + val items = mutableListOf<DepartureItem>() if (stopDepartures.alerts.isNotEmpty()) { - binding.alerts.visibility = View.VISIBLE - binding.alertsText.text = stopDepartures.alerts.map { - it.header.ifEmpty { - getString(R.string.alert_header) - } - }.toSet().joinToString(separator = "\n") - alertDescriptions = stopDepartures.alerts.map { it.description }.filter { it != "" } - .joinToString(separator = "\n") - binding.moreButton.visibility = if (alertDescriptions == "") View.GONE else View.VISIBLE - - } else { - binding.alerts.visibility = View.GONE + items.add(DepartureItem(stopDepartures.alerts)) } - updateItems( - stopDepartures.departures - .filter { d -> - linesFilter.values.all { !it } or (linesFilter[d.vehicle.Line.name] ?: false) - } - .filter { d -> - val t = LocalTime.of(d.time.Hour.toInt(), d.time.Minute.toInt()) - t >= startTime && t <= endTime - }, - stopDepartures.stop - ) - openBottomSheet?.departureID()?.let { adapter.get(it) }?.let { openBottomSheet?.update(it) } + items.addAll(stopDepartures.departures + .filter { d -> + viewModel.linesFilter.value?.let { filter -> + filter.values.all { !it } or (filter[d.vehicle.Line.name] ?: false) + } ?: true + } + .filter { d -> + val t = LocalTime.of(d.time.Hour.toInt(), d.time.Minute.toInt()) + t >= viewModel.startTime && t <= viewModel.endTime + }.map { DepartureItem(it) }) + updateItems(items, stopDepartures.stop) + viewModel.openBottomSheet?.departureID()?.let { adapter.get(it) } + ?.let { it.departure?.let { departure -> viewModel.openBottomSheet?.update(departure) } } + val lines = stopDepartures.departures.map { it.vehicle.Line.name }.sortedWith { s1, s2 -> val s1n = s1.toIntOrNull() @@ -132,7 +172,8 @@ } else { s1.compareTo(s2) } }.toSet().toTypedArray() - val selections = lines.map { linesFilter.getOrDefault(it, false) }.toBooleanArray() + val selections = + lines.map { viewModel.linesFilter.value?.getOrDefault(it, false) ?: false }.toBooleanArray() linePicker?.setMultiChoiceItems(lines, selections) { _, which, checked -> linesFilterTemporary[lines[which]] = checked @@ -144,20 +185,31 @@ } binding.departuresAppBar.menu.findItem(R.id.departures_filter_bytime).setEnabled(false) + // TODO async esp. with Online + if (runBlocking { + val repository = OfflineRepository(this@DeparturesActivity) + val f = repository.getFavourite( + getCode() ?: "" + ) + repository.close() + f + } != null) { + binding.departuresAppBar.menu.findItem(R.id.favourite).setIcon(R.drawable.favourite_full) + } datePicker.addOnNegativeButtonClickListener { - date = null - startTime = LocalTime.MIN - endTime = LocalTime.MAX + viewModel.date = null + viewModel.startTime = LocalTime.MIN + viewModel.endTime = LocalTime.MAX binding.departuresAppBar.menu.findItem(R.id.departures_filter_bytime).setEnabled(false) getDepartures(true) } datePicker.addOnPositiveButtonClickListener { - if (date == null) { - startTime = LocalTime.MIN - endTime = LocalTime.MAX + if (viewModel.date == null) { + viewModel.startTime = LocalTime.MIN + viewModel.endTime = LocalTime.MAX } - date = Instant.ofEpochMilli(it).atZone(ZoneId.systemDefault()) + viewModel.date = Instant.ofEpochMilli(it).atZone(ZoneId.systemDefault()) .toLocalDate() binding.departuresAppBar.menu.findItem(R.id.departures_filter_bytime).setEnabled(true) getDepartures(true) @@ -169,8 +221,33 @@ val tf = ResourcesCompat.getFont(this@DeparturesActivity, R.font.yellowcircle8) setCollapsedTitleTypeface(tf) setExpandedTitleTypeface(tf) } + binding.departuresAppBar.setOnMenuItemClickListener { when (it.itemId) { + R.id.favourite -> { + if (!viewModel.linesFilter.value.isNullOrEmpty() && viewModel.linesFilter.value!!.any { filter -> filter.value }) { + MaterialAlertDialogBuilder(this).setIcon( + AppCompatResources.getDrawable( + this, + R.drawable.filter + ) + ) + .setTitle("Filtered departures") + .setMessage("Do you want to save a favourite filtered with selected lines?") + .setPositiveButton(R.string.filtered) { _, _ -> + saveFavourite(viewModel.linesFilter.value!!.keys) + } + .setNegativeButton(R.string.unfiltered) { _, _ -> + saveFavourite(setOf()) + } + .setNeutralButton(R.string.cancel) { _, _ -> } + .show() + } else { + saveFavourite(setOf()) + } + true + } + R.id.departures_calendar -> { datePicker.show(supportFragmentManager, "datePicker") true @@ -178,7 +255,9 @@ } R.id.departures_filter_byline -> { linesFilterTemporary.clear() - linesFilter.forEach { filter -> linesFilterTemporary[filter.key] = filter.value } + viewModel.linesFilter.value?.forEach { filter -> + linesFilterTemporary[filter.key] = filter.value + } linePicker?.show() true } @@ -187,30 +266,30 @@ R.id.departures_filter_bytime -> { timePickerStart = MaterialTimePicker.Builder().setTitleText(R.string.title_select_time_start) .setTimeFormat(TimeFormat.CLOCK_24H) - .setHour(startTime.hour) - .setMinute(startTime.minute) + .setHour(viewModel.startTime.hour) + .setMinute(viewModel.startTime.minute) .setNegativeButtonText(R.string.clear_date_selection) .build() timePickerEnd = MaterialTimePicker.Builder().setTitleText(R.string.title_select_time_end) .setTimeFormat(TimeFormat.CLOCK_24H) - .setHour(endTime.hour) - .setMinute(endTime.minute) + .setHour(viewModel.endTime.hour) + .setMinute(viewModel.endTime.minute) .setNegativeButtonText(R.string.clear_date_selection) .build() timePickerEnd!!.addOnPositiveButtonClickListener { - endTime = LocalTime.of(timePickerEnd!!.hour, timePickerEnd!!.minute) + viewModel.endTime = LocalTime.of(timePickerEnd!!.hour, timePickerEnd!!.minute) getDepartures(true) } timePickerEnd!!.addOnNegativeButtonClickListener { - endTime = LocalTime.MAX + viewModel.endTime = LocalTime.MAX getDepartures(true) } timePickerStart!!.addOnPositiveButtonClickListener { - startTime = LocalTime.of(timePickerStart!!.hour, timePickerStart!!.minute) + viewModel.startTime = LocalTime.of(timePickerStart!!.hour, timePickerStart!!.minute) timePickerEnd!!.show(supportFragmentManager, "timePickerEnd") } timePickerStart!!.addOnNegativeButtonClickListener { - startTime = LocalTime.MIN + viewModel.startTime = LocalTime.MIN timePickerEnd!!.show(supportFragmentManager, "timePickerEnd") } timePickerStart!!.show(supportFragmentManager, "timePickerStart") @@ -240,8 +319,8 @@ ) adapter = BimbaDeparturesAdapter(layoutInflater, this, listOf()) { DepartureBottomSheet(it).apply { show(supportFragmentManager, DepartureBottomSheet.TAG) - openBottomSheet = this - setOnCancel { openBottomSheet = null } + viewModel.openBottomSheet = this + setOnCancel { viewModel.openBottomSheet = null } } } binding.departuresRecycler.adapter = adapter @@ -269,28 +348,21 @@ connectivityManager.registerNetworkCallback(request, networkCallback) } } - override fun onStart() { - super.onStart() - linesFilter.clear() - getLine()?.let { - linesFilter[it] = true - } - } - override fun onResume() { super.onResume() getDepartures() + viewModel.openBottomSheet?.show(supportFragmentManager, DepartureBottomSheet.TAG) } override fun onPause() { + viewModel.openBottomSheet?.dismiss() super.onPause() - handler.removeCallbacks(runnable) + countdown.cancel() } override fun onStop() { super.onStop() - handler.removeCallbacks(runnable) - handler.removeCallbacksAndMessages(null) + countdown.cancel() } private fun getName(): String { @@ -308,17 +380,24 @@ else -> null } } + private fun getLines(): List<String>? { + return when (intent?.action) { + null -> intent?.extras?.getStringArray("linesFilter")?.toList() + else -> null + } + } + + private fun getCode() = intent?.extras?.getString("code") + fun getDepartures(force: Boolean = false) { + binding.departuresUpdatesProgress.isIndeterminate = true if (force) { showLoading() } else { adapter.refreshItems() setupSnackbar() } - viewModel.getDepartures(this, date, force) - handler.removeCallbacks(runnable) - runnable = Runnable { getDepartures() } - handler.postDelayed(runnable, 30 * 1000) + viewModel.getDepartures(this, viewModel.date, force) } private fun setupSnackbar() { @@ -343,6 +422,7 @@ private fun showError(error: Error) { binding.departuresProgress.visibility = View.GONE binding.departuresRecycler.visibility = View.GONE + binding.departuresUpdatesProgress.visibility = View.GONE binding.errorImage.visibility = View.VISIBLE binding.errorText.visibility = View.VISIBLE @@ -358,17 +438,30 @@ private fun showLoading() { binding.departuresOverlay.visibility = View.VISIBLE binding.departuresProgress.visibility = View.VISIBLE - binding.departuresRecycler.visibility = View.GONE binding.errorImage.visibility = View.GONE binding.errorText.visibility = View.GONE } - private fun updateItems(departures: List<Departure>, stop: Stop) { + private fun updateItems( + departures: List<DepartureItem>, + stop: Stop?, + leaveAlert: Boolean = false + ) { setupSnackbar() + binding.departuresRecycler.scrollToPosition(0) binding.departuresProgress.visibility = View.GONE - adapter.update(departures, date != null, true) + // TODO [elizabeth] max, progress from header Cache-Control max-age + binding.departuresUpdatesProgress.apply { + visibility = View.VISIBLE + isIndeterminate = false + max = Tim(Second(30)).tims + progress = Tim(Second(30)).tims + } + countdown.cancel() + countdown.start() + adapter.update(departures, viewModel.date != null, true, leaveAlert) binding.collapsingLayout.apply { - title = stop.name + stop?.let { title = it.name } } if (departures.isEmpty()) { binding.errorImage.visibility = View.VISIBLE @@ -392,4 +485,33 @@ binding.departuresRecycler.visibility = View.VISIBLE } // todo [3.2; traffic] stop info } -} \ No newline at end of file + + private fun saveFavourite(linesFilter: Set<String>) { + val context = this + val feedID = intent.extras?.getString("feedID") + val code = intent?.extras?.getString("code") + if (feedID == null || code == null) { + Toast.makeText(this, R.string.cannot_save_favourite, Toast.LENGTH_LONG).show() + return + } + binding.departuresAppBar.menu.findItem(R.id.favourite).setIcon(R.drawable.favourite_full) + MainScope().launch { + val repo = OfflineRepository(context) + val feedName = repo.getFeeds(context, Server.get(context))?.get(feedID)?.name + if (feedName == null) { + Toast.makeText(context, R.string.cannot_save_favourite, Toast.LENGTH_LONG).show() + return@launch + } + val favourite = (repo.getFavourite(code) ?: Favourite( + null, + feedID, + feedName, + code, + getName(), + linesFilter.toList() + )).copy(lines = linesFilter.toList()) + repo.saveFavourite(favourite) + repo.close() + } + } +} 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 b973174eeb0b9325f3c5282aaa682545ac5187e4..9d3b81505bb8907710dcf14bb6d543a1c5c1e3bf 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 @@ -23,6 +23,7 @@ import xyz.apiote.bimba.czwek.repo.QrLocation import xyz.apiote.bimba.czwek.repo.StopDepartures import xyz.apiote.bimba.czwek.repo.TrafficResponseException import java.time.LocalDate +import java.time.LocalTime class DeparturesViewModel : ViewModel() { private val _departures = MutableLiveData<StopDepartures>() @@ -32,7 +33,17 @@ val error: LiveData= _error var requestedItemsNumber = 12 var allItemsRequested = false private var feed: FeedInfo? = null + var openBottomSheet: DepartureBottomSheet? = null private lateinit var code: String + val mutableLinesFilter = MutableLiveData<Map<String, Boolean>>() + val linesFilter: LiveData<Map<String, Boolean>> = mutableLinesFilter + + // TODO observe in activity, maybe refreshing and not getting departures is enough + var startTime: LocalTime = LocalTime.MIN + var endTime: LocalTime = LocalTime.MAX + + // TODO observe in activity, refreshing is not enough + var date: LocalDate? = null fun getDepartures(context: Context, date: LocalDate?, force: Boolean) { MainScope().launch { @@ -52,9 +63,9 @@ context, requestedItemsNumber ) stopDepartures?.let { - if (stopDepartures.departures.isEmpty()) { // TODO other error for empty than not-found - val (string, image) = mapHttpError(404) - throw TrafficResponseException(404, "", Error(404, string, image)) + if (stopDepartures.departures.isEmpty()) { + val (string, image) = mapHttpError(44) + throw TrafficResponseException(44, "", Error(44, string, image)) } _departures.value = it } @@ -69,13 +80,15 @@ } private suspend fun getFeed(context: Context): FeedInfo { val intent = (context as Activity).intent - var feeds = OfflineRepository().getFeeds(context) + val repository = OfflineRepository(context) + var feeds = repository.getFeeds(context) if (feeds.isNullOrEmpty()) { feeds = OnlineRepository().getFeeds(context) if (feeds != null) { - OfflineRepository().saveFeedCache(context, feeds) + repository.saveFeedCache(context, feeds) } } + repository.close() return when (intent.action) { Intent.ACTION_VIEW -> { val feed = feeds?.values?.find { it.qrHost == intent.data?.host } 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 f8be699529927e36494d50aa63eeeb58f94595f1..8f29cb95067119e2ee24b1688138d347ad55b74b 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 @@ -9,6 +9,7 @@ import android.os.Bundle import androidx.appcompat.app.AppCompatActivity import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import xyz.apiote.bimba.czwek.dashboard.MainActivity +import xyz.apiote.bimba.czwek.repo.migrateDB import xyz.apiote.bimba.czwek.settings.feeds.migrateFeedsSettings class FirstRunActivity : AppCompatActivity() { @@ -19,6 +20,7 @@ val preferences = getSharedPreferences("shp", MODE_PRIVATE) migrateFeedsSettings(this) + migrateDB(this) val intent = if (preferences.getBoolean("firstRun", true)) { Intent(this, OnboardingActivity::class.java) 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 4656071ec4e4d07508c2520557cbbdd9d46e7604..7833875ef94ce6fe166c7f4536871d0827d49e19 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 @@ -12,8 +12,12 @@ import android.text.SpannableStringBuilder import android.text.style.RelativeSizeSpan import android.text.style.StyleSpan import android.widget.Button +import androidx.activity.enableEdgeToEdge import androidx.activity.result.contract.ActivityResultContracts import androidx.appcompat.app.AppCompatActivity +import androidx.core.view.ViewCompat +import androidx.core.view.WindowInsetsCompat +import androidx.core.view.updatePadding import xyz.apiote.bimba.czwek.R import xyz.apiote.bimba.czwek.databinding.ActivityOnboardingBinding import xyz.apiote.bimba.czwek.settings.ServerChooserActivity @@ -30,10 +34,16 @@ } } override fun onCreate(savedInstanceState: Bundle?) { + enableEdgeToEdge() super.onCreate(savedInstanceState) _binding = ActivityOnboardingBinding.inflate(layoutInflater) setContentView(binding.root) + ViewCompat.setOnApplyWindowInsetsListener(binding.root) { v, windowInsets -> + val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()) + v.updatePadding(right = insets.right, left = insets.left, bottom = insets.bottom) + windowInsets + } prepareButton( binding.buttonSimple, 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 358a7bbd7ed88d19af4e2e9fdb6a26ff7774cb6f..e90ce512e3b7eba9e635c9514268ee246ddc9db8 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 @@ -22,6 +22,20 @@ import java.time.ZonedDateTime import java.time.format.DateTimeFormatter import java.time.temporal.ChronoUnit + +class DepartureItem { + private constructor(d: Departure?, a: List<Alert>) { + departure = d + alert = a + } + + constructor(d: Departure) : this(d, emptyList()) + constructor(a: List<Alert>) : this(null, a) + + val departure: Departure? + val alert: List<Alert> +} + enum class AlertCause { UNKNOWN, OTHER, TECHNICAL_PROBLEM, STRIKE, DEMONSTRATION, ACCIDENT, HOLIDAY, WEATHER, MAINTENANCE, CONSTRUCTION, POLICE_ACTIVITY, MEDICAL_EMERGENCY; @@ -61,7 +75,7 @@ AlertEffectV1.SIGNIFICANT_DELAYS -> valueOf("SIGNIFICANT_DELAYS") AlertEffectV1.DETOUR -> valueOf("DETOUR") AlertEffectV1.ADDITIONAL_SERVICE -> valueOf("ADDITIONAL_SERVICE") AlertEffectV1.MODIFIED_SERVICE -> valueOf("MODIFIED_SERVICE") - AlertEffectV1.STOP_MOVED -> valueOf("STOP_MOVED") + AlertEffectV1.STOP_MOVED -> valueOf("STOP_MOVED") AlertEffectV1.NONE -> valueOf("NONE") AlertEffectV1.ACCESSIBILITY_ISSUE -> valueOf("ACCESSIBILITY_ISSUE") } @@ -76,7 +90,13 @@ val url: String, val cause: AlertCause, val effect: AlertEffect ) { - constructor(a: AlertV1) : this(a.header, a.Description, a.Url, AlertCause.of(a.Cause), AlertEffect.of(a.Effect)) + constructor(a: AlertV1) : this( + a.header, + a.Description, + a.Url, + AlertCause.of(a.Cause), + AlertEffect.of(a.Effect) + ) } data class StopDepartures( diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/repo/Favourite.kt b/app/src/main/java/xyz/apiote/bimba/czwek/repo/Favourite.kt new file mode 100644 index 0000000000000000000000000000000000000000..a24d42ebccfb157547772a58006dcfcb0f561d05 --- /dev/null +++ b/app/src/main/java/xyz/apiote/bimba/czwek/repo/Favourite.kt @@ -0,0 +1,3 @@ +package xyz.apiote.bimba.czwek.repo + +data class Favourite(val sequence: Int?, val feedID: String, val feedName: String, val stopCode: String, val stopName: String, val lines: List<String>) \ No newline at end of file diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/repo/Interfaces.kt b/app/src/main/java/xyz/apiote/bimba/czwek/repo/Interfaces.kt index 7d70dbc8aa3e2cedc903e6e19ea399841623f2a1..95fa5fa0c75f0c3e765eefd64c22091021260eb8 100644 --- a/app/src/main/java/xyz/apiote/bimba/czwek/repo/Interfaces.kt +++ b/app/src/main/java/xyz/apiote/bimba/czwek/repo/Interfaces.kt @@ -17,6 +17,12 @@ fun id(): String } interface Repository { + suspend fun getFavourite(stopCode: String): Favourite? + suspend fun getFavourites(feedIDs: Set<String> = emptySet()): List<Favourite> + + suspend fun saveFavourite(favourite: Favourite) + suspend fun saveFavourites(favourites: Set<Favourite>) + suspend fun getFeeds( context: Context, server: Server = Server.get(context) 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 6f9849cfc8b086ec4809afba0ad14e6218b4f58f..165e9c932064611d7cf5be4f334a473356886eb9 100644 --- a/app/src/main/java/xyz/apiote/bimba/czwek/repo/OfflineRepository.kt +++ b/app/src/main/java/xyz/apiote/bimba/czwek/repo/OfflineRepository.kt @@ -5,7 +5,9 @@ package xyz.apiote.bimba.czwek.repo import android.content.Context +import android.database.sqlite.SQLiteDatabase import androidx.core.content.edit +import androidx.core.database.sqlite.transaction import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import xyz.apiote.bimba.czwek.api.Server @@ -15,7 +17,10 @@ import java.io.File import java.net.URLEncoder import java.time.LocalDate -class OfflineRepository : Repository { +class OfflineRepository(context: Context) : Repository { + private val db = + SQLiteDatabase.openOrCreateDatabase(context.getDatabasePath("favourites").path, null) + fun saveFeedCache(context: Context, feedInfos: Map<String, FeedInfo>) { val file = File( context.filesDir, URLEncoder.encode(Server.get(context).apiPath, "utf-8") @@ -33,6 +38,94 @@ stream.flush() stream.close() } + override suspend fun getFavourite(stopCode: String): Favourite? { + val cursor = + db.rawQuery( + "select sequence, stop_name, feed_id, feed_name, lines from favourites where stop_code = ?", + listOf(stopCode).toTypedArray() + ) + if (cursor.count == 0) { + return null + } + cursor.moveToNext() + val f = Favourite( + cursor.getInt(0), + cursor.getString(2), + cursor.getString(3), + stopCode, + cursor.getString(1), + cursor.getString(4).split("||").filter { it != "" } + ) + cursor.close() + return f + } + + override suspend fun getFavourites(feedIDs: Set<String>): List<Favourite> { + val whereClause = if (feedIDs.isNotEmpty()) { + feedIDs.indices.joinToString(prefix = "where feed_id in (", postfix = ")") { "?" } + } else { + "" + } + val cursor = + db.rawQuery( + "select sequence, stop_name, feed_id, feed_name, stop_code, lines from favourites $whereClause order by sequence", + feedIDs.toTypedArray() + ) + val l = mutableListOf<Favourite>() + while (cursor.moveToNext()) { + l.add( + Favourite( + cursor.getInt(0), + cursor.getString(2), + cursor.getString(3), + cursor.getString(4), + cursor.getString(1), + cursor.getString(5).split("||").filter { it != "" } + ) + ) + } + cursor.close() + return l + } + + override suspend fun saveFavourite(favourite: Favourite) { + val sequence = favourite.sequence ?: run { + val cursor = + db.rawQuery("select max(ROWID) from favourites", emptyArray<String?>()) + val s = if (cursor.count == 0) { + 0 + } else { + cursor.moveToNext() + cursor.getInt(0) + } + 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 + ) + ) + } + + override suspend fun saveFavourites(favourites: Set<Favourite>) { + db.execSQL("delete from favourites") + db.transaction { + favourites.forEach { + saveFavourite(it) + } + } + } + @Suppress("RedundantNullableReturnType") override suspend fun getFeeds( context: Context, @@ -108,4 +201,13 @@ ): List ? { TODO("Not yet implemented") } + fun close() { + db.close() + } +} + + +fun migrateDB(context: Context) { + val db = SQLiteDatabase.openOrCreateDatabase(context.getDatabasePath("favourites").path, null) + db.execSQL("create table if not exists favourites(sequence integer, feed_id text, feed_name text, stop_code text, stop_name text, lines text, primary key(feed_id, stop_code))") } 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 8432f937e713904e92c77440c6d634607f8167fe..1c3f36595fe1c6bc646a137d9475294d59f0f9d1 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 @@ -50,6 +50,22 @@ // todo [3.2] in Repository check if responses are BARE or HTML class OnlineRepository : Repository { + override suspend fun getFavourite(stopCode: String): Favourite? { + TODO("Not yet implemented; waits for ampelmänchen") + } + + override suspend fun getFavourites(feedIDs: Set<String>): List<Favourite> { + TODO("Not yet implemented; waits for ampelmänchen") + } + + override suspend fun saveFavourite(favourite: Favourite) { + TODO("Not yet implemented; waits for ampelmänchen") + } + + override suspend fun saveFavourites(favourites: Set<Favourite>) { + TODO("Not yet implemented; waits for ampelmänchen") + } + override suspend fun getFeeds( context: Context, server: Server diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/search/LineGraphActivity.kt b/app/src/main/java/xyz/apiote/bimba/czwek/search/LineGraphActivity.kt index 128a2199a685110c6bfd442bf81ed6509cbda12f..280697cf61759672e9809e22babc5fda4b001137 100644 --- a/app/src/main/java/xyz/apiote/bimba/czwek/search/LineGraphActivity.kt +++ b/app/src/main/java/xyz/apiote/bimba/czwek/search/LineGraphActivity.kt @@ -7,8 +7,12 @@ import android.os.Bundle import android.util.Log import android.view.View +import androidx.activity.enableEdgeToEdge import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.content.res.AppCompatResources +import androidx.core.view.ViewCompat +import androidx.core.view.WindowInsetsCompat +import androidx.core.view.updatePadding import androidx.viewpager.widget.ViewPager import com.google.android.material.tabs.TabLayout import kotlinx.coroutines.MainScope @@ -24,10 +28,17 @@ private lateinit var binding: ActivityLineGraphBinding private lateinit var sectionsPagerAdapter: SectionsPagerAdapter override fun onCreate(savedInstanceState: Bundle?) { + enableEdgeToEdge() super.onCreate(savedInstanceState) binding = ActivityLineGraphBinding.inflate(layoutInflater) setContentView(binding.root) + + ViewCompat.setOnApplyWindowInsetsListener(binding.root) { v, windowInsets -> + val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()) + v.updatePadding(top = insets.top, left = insets.left, right = insets.right) + windowInsets + } val lineName = intent.getStringExtra("lineName")!! val lineID = intent.getStringExtra("lineID")!! 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 d18cb129731d39ea38fdf1878605754b52088caa..5575f8547ed6f8abfeacabc82767927c814a74ac 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 @@ -89,7 +89,7 @@ setImageDrawable(stop.icon(context!!)) contentDescription = context.getString(R.string.stop_content_description) } holder?.title?.text = stop.name - if ((feedsSettings?.activeFeeds() ?: 0) > 1) { + if ((feedsSettings?.activeFeedsCount() ?: 0) > 1) { holder?.feedName?.visibility = View.VISIBLE holder?.feedName?.text = feeds?.get(stop.feedID)?.name ?: "" } @@ -115,7 +115,7 @@ setImageDrawable(line.icon(context!!)) contentDescription = line.type.name colorFilter = null } - if ((feedsSettings?.activeFeeds() ?: 0) > 1) { + if ((feedsSettings?.activeFeedsCount() ?: 0) > 1) { holder?.feedName?.visibility = View.VISIBLE holder?.feedName?.text = feeds?.get(line.feedID)?.name ?: "" } 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 95054348f2de4faf524e27245737e784f1222b45..f8729039653a32172c5b25a51668ff9ddfb15a6e 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 @@ -14,9 +14,14 @@ import android.os.Handler import android.os.Looper import android.util.Log import android.view.View +import android.view.ViewGroup.MarginLayoutParams +import androidx.activity.enableEdgeToEdge import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.content.res.AppCompatResources -import androidx.core.view.WindowCompat +import androidx.core.view.ViewCompat +import androidx.core.view.WindowInsetsCompat +import androidx.core.view.updateLayoutParams +import androidx.core.view.updatePadding import androidx.recyclerview.widget.LinearLayoutManager import kotlinx.coroutines.MainScope import kotlinx.coroutines.Runnable @@ -45,18 +50,31 @@ private val handler = Handler(Looper.getMainLooper()) private var runnable = Runnable {} override fun onCreate(savedInstanceState: Bundle?) { + enableEdgeToEdge() super.onCreate(savedInstanceState) _binding = ActivityResultsBinding.inflate(layoutInflater) setContentView(binding.root) + ViewCompat.setOnApplyWindowInsetsListener(binding.resultsRecycler) { v, windowInsets -> + + windowInsets.displayCutout?.safeInsetLeft?.let { + v.updateLayoutParams<MarginLayoutParams> { + leftMargin = it + } + } + val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()) + v.updatePadding(right = insets.right, left = insets.left) + windowInsets + } + ViewCompat.setOnApplyWindowInsetsListener(binding.topAppBar) { v, windowInsets -> + val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()) + v.updatePadding(right = insets.right, left = insets.left) + windowInsets + } + binding.resultsRecycler.layoutManager = LinearLayoutManager(this) adapter = BimbaResultsAdapter(layoutInflater, this, listOf()) binding.resultsRecycler.adapter = adapter - setSupportActionBar(binding.topAppBar) - - WindowCompat.setDecorFitsSystemWindows(window, false) - - when (getMode()) { Mode.MODE_LOCATION -> { @@ -135,7 +153,9 @@ } private suspend fun getFeeds() { if (adapter.feeds.isNullOrEmpty()) { - adapter.feeds = OfflineRepository().getFeeds(this) + val repository = OfflineRepository(this) + adapter.feeds = repository.getFeeds(this) + repository.close() } if (adapter.feedsSettings == null) { adapter.feedsSettings = FeedsSettings.load(this) diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/search/ui/LineGraphFragment.kt b/app/src/main/java/xyz/apiote/bimba/czwek/search/ui/LineGraphFragment.kt index 84c6285e11884e3a4ff364dbe0b27f128f519785..ffb32d9486fbf7056c3629add6fdf2d58550898e 100644 --- a/app/src/main/java/xyz/apiote/bimba/czwek/search/ui/LineGraphFragment.kt +++ b/app/src/main/java/xyz/apiote/bimba/czwek/search/ui/LineGraphFragment.kt @@ -14,6 +14,9 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.core.view.ViewCompat +import androidx.core.view.WindowInsetsCompat +import androidx.core.view.updatePadding import androidx.fragment.app.Fragment import androidx.lifecycle.ViewModelProvider import dev.bandb.graphview.AbstractGraphAdapter @@ -51,6 +54,12 @@ inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View { _binding = FragmentLineGraphBinding.inflate(inflater, container, false) + + ViewCompat.setOnApplyWindowInsetsListener(binding.recycler) { v, windowInsets -> + val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()) + v.updatePadding(right = insets.right, left = insets.left, top = insets.top, bottom = insets.bottom) + windowInsets + } val configuration = SugiyamaConfiguration.Builder() .setLevelSeparation(100) 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 2780cb83d4dea6903fe6a720c6ad8c95622ddc0b..223dfeee4be34ef5e501c4760e382eb3342bdf24 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,17 +4,19 @@ // 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 -import android.net.ConnectivityManager import android.os.Bundle import android.util.Log +import androidx.activity.enableEdgeToEdge import androidx.activity.result.contract.ActivityResultContracts import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.content.res.AppCompatResources import androidx.core.content.edit +import androidx.core.view.ViewCompat +import androidx.core.view.WindowInsetsCompat +import androidx.core.view.updatePadding import androidx.core.widget.addTextChangedListener import com.google.android.material.dialog.MaterialAlertDialogBuilder import kotlinx.coroutines.Dispatchers @@ -44,7 +46,9 @@ private lateinit var preferences: SharedPreferences override fun onCreate(savedInstanceState: Bundle?) { + enableEdgeToEdge() super.onCreate(savedInstanceState) + preferences = getSharedPreferences("shp", MODE_PRIVATE) if (intent.getBooleanExtra("simple", false)) { @@ -53,6 +57,12 @@ checkServer(true) } else { _binding = ActivityServerChooserBinding.inflate(layoutInflater) setContentView(binding.root) + + ViewCompat.setOnApplyWindowInsetsListener(binding.root) { v, windowInsets -> + val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()) + v.updatePadding(right = insets.right, left = insets.left, top = insets.top) + windowInsets + } preferences.edit(true) { putBoolean("inFeedsTransaction", true) @@ -122,9 +132,9 @@ title: Int, description: Int, icon: Int, onPositive: (() -> Unit)? ) { MaterialAlertDialogBuilder(this).setIcon(AppCompatResources.getDrawable(this, icon)) .setTitle(getString(title)).setMessage(getString(description)) - .setNegativeButton(resources.getString(R.string.cancel)) { _, _ -> }.apply { + .setNegativeButton(R.string.cancel) { _, _ -> }.apply { if (onPositive != null) { - setPositiveButton(resources.getString(R.string.cont)) { _, _ -> + setPositiveButton(R.string.cont) { _, _ -> onPositive() } } @@ -132,7 +142,6 @@ }.show() } private fun checkServer(isSimple: Boolean) { - val cm = getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager MainScope().launch { val result = getBimba(this@ServerChooserActivity, Server.get(this@ServerChooserActivity)) if (result.error != null) { 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 c01d13f54ea0addd2b246d3d070d96aac3aed5f0..09f4dd40f4c2eca59f1f092a66042214f88f14a6 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 @@ -7,9 +7,13 @@ import android.content.Intent import android.os.Bundle import android.view.View +import androidx.activity.enableEdgeToEdge import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.content.res.AppCompatResources import androidx.core.content.edit +import androidx.core.view.ViewCompat +import androidx.core.view.WindowInsetsCompat +import androidx.core.view.updatePadding import androidx.core.widget.doAfterTextChanged import androidx.lifecycle.ViewModelProvider import androidx.recyclerview.widget.LinearLayoutManager @@ -32,9 +36,23 @@ private lateinit var adapter: BimbaFeedInfoAdapter override fun onCreate(savedInstanceState: Bundle?) { + enableEdgeToEdge() super.onCreate(savedInstanceState) _binding = ActivityFeedChooserBinding.inflate(layoutInflater) setContentView(binding.root) + + ViewCompat.setOnApplyWindowInsetsListener(binding.root) { v, windowInsets -> + windowInsets.displayCutout?.safeInsetRight?.let { binding.resultsRecycler.updatePadding(right = it) } + windowInsets.displayCutout?.safeInsetLeft?.let { binding.resultsRecycler.updatePadding(left = it) } + val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()) + v.updatePadding( + right = insets.right, + left = insets.left, + top = insets.top, + bottom = insets.bottom + ) + windowInsets + } viewModel = ViewModelProvider(this)[FeedsViewModel::class.java] 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 91921e1025e74c19ad73d9d75cd4e39d5668bfec..632af91e8e3286638d6614460593ed54b59dcc18 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 @@ -15,8 +15,9 @@ @Serializable @OptIn(ExperimentalStdlibApi::class) data class FeedsSettings(val settings: MutableMap<String, FeedSettings>) { - fun activeFeeds() = settings.count { it.value.enabled && it.value.useOnline } - fun getIDs() = settings.filter { it.value.enabled && it.value.useOnline }.keys.joinToString(",") + fun activeFeedsCount() = settings.count { it.value.enabled && it.value.useOnline } + fun activeFeeds() = settings.filter { it.value.enabled && it.value.useOnline }.keys + fun getIDs() = activeFeeds().joinToString(",") fun save(context: Context, server: Server) { val doc = KBson().dump(serializer(), this).toHexString() diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/settings/feeds/FeedsViewModel.kt b/app/src/main/java/xyz/apiote/bimba/czwek/settings/feeds/FeedsViewModel.kt index 6d5fb983e6baf603ce0237922088462d9450f328..c178b2c8ba05f338651ef3d93bb0580ac2f6a3a1 100644 --- a/app/src/main/java/xyz/apiote/bimba/czwek/settings/feeds/FeedsViewModel.kt +++ b/app/src/main/java/xyz/apiote/bimba/czwek/settings/feeds/FeedsViewModel.kt @@ -51,8 +51,10 @@ var error: Error? = null MainScope().launch { withContext(coroutineContext) { launch { + val repository = OfflineRepository(context) offlineFeeds = - OfflineRepository().getFeeds(context) + repository.getFeeds(context) + repository.close() if (!offlineFeeds.isNullOrEmpty()) { _feeds.value = offlineFeeds!! } @@ -70,10 +72,12 @@ } } if (offlineFeeds.isNullOrEmpty() && error != null) { _error.value = error!! - } else{ + } else { joinFeeds(offlineFeeds, onlineFeeds).let { joinedFeeds -> _feeds.value = joinedFeeds - OfflineRepository().saveFeedCache(context, joinedFeeds) + val repository = OfflineRepository(context) + repository.saveFeedCache(context, joinedFeeds) + repository.close() } } } diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/units/Time.kt b/app/src/main/java/xyz/apiote/bimba/czwek/units/Time.kt new file mode 100644 index 0000000000000000000000000000000000000000..9456eae31f27301d561789f4e07ca44dd9519bef --- /dev/null +++ b/app/src/main/java/xyz/apiote/bimba/czwek/units/Time.kt @@ -0,0 +1,17 @@ +package xyz.apiote.bimba.czwek.units + +interface TimeUnit + +data class Second(val secs: Long) { + constructor(t: Tim) : this(t.tims.toLong() * 25 / 144) +} + +data class Millisecond(val millis: Long) { + constructor(t: Tim) : this(t.tims.toLong() * 25 * 1000 / 144) + constructor(s: Second) : this(s.secs * 1000) +} + +data class Tim(val tims: Int) { + constructor(s: Second) : this((s.secs * 144 / 25).toInt()) + constructor(m: Millisecond) : this((m.millis * 144 / 25 / 1000).toInt()) +} \ No newline at end of file diff --git a/app/src/main/res/drawable/error_departures.xml b/app/src/main/res/drawable/error_departures.xml new file mode 100644 index 0000000000000000000000000000000000000000..dfec75446a44e14d388b991b40bf57d6c7f6bbed --- /dev/null +++ b/app/src/main/res/drawable/error_departures.xml @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- +SPDX-FileCopyrightText: Google + +SPDX-License-Identifier: Apache-2.0 +--> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24" + android:viewportHeight="24" + android:tint="?attr/colorOnSurface"> + <path + android:fillColor="@android:color/white" + android:pathData="M21.19,21.19L2.81,2.81L1.39,4.22L4,6.83V16c0,0.88 0.39,1.67 1,2.22V20c0,0.55 0.45,1 1,1h1c0.55,0 1,-0.45 1,-1v-1h8v1c0,0.55 0.45,1 1,1h1c0.05,0 0.09,-0.02 0.14,-0.03l1.64,1.64L21.19,21.19zM7.5,17C6.67,17 6,16.33 6,15.5C6,14.67 6.67,14 7.5,14S9,14.67 9,15.5C9,16.33 8.33,17 7.5,17zM6,11V8.83L8.17,11H6zM8.83,6L5.78,2.95C7.24,2.16 9.48,2 12,2c4.42,0 8,0.5 8,4v10c0,0.35 -0.08,0.67 -0.19,0.98L13.83,11H18V6H8.83z"/> +</vector> \ No newline at end of file diff --git a/app/src/main/res/drawable/error_locality.xml b/app/src/main/res/drawable/error_locality.xml new file mode 100644 index 0000000000000000000000000000000000000000..78add21082685570637e641f93757bae7c99c26e --- /dev/null +++ b/app/src/main/res/drawable/error_locality.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- +SPDX-FileCopyrightText: Google + +SPDX-License-Identifier: Apache-2.0 +--> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24" + android:viewportHeight="24" + android:tint="?attr/colorOnSurface"> + <path + android:fillColor="@android:color/white" + android:pathData="M14,10V3.26C13.35,3.09 12.68,3 12,3c-4.2,0 -8,3.22 -8,8.2c0,3.32 2.67,7.25 8,11.8c5.33,-4.55 8,-8.48 8,-11.8c0,-0.41 -0.04,-0.81 -0.09,-1.2H14zM12,13c-1.1,0 -2,-0.9 -2,-2c0,-1.1 0.9,-2 2,-2s2,0.9 2,2C14,12.1 13.1,13 12,13z"/> + <path + android:fillColor="@android:color/white" + android:pathData="M22.54,2.88l-1.42,-1.42l-2.12,2.13l-2.12,-2.13l-1.42,1.42l2.13,2.12l-2.13,2.12l1.42,1.42l2.12,-2.13l2.12,2.13l1.42,-1.42l-2.13,-2.12z"/> +</vector> \ No newline at end of file diff --git a/app/src/main/res/drawable/favourite_empty.xml b/app/src/main/res/drawable/favourite_empty.xml new file mode 100644 index 0000000000000000000000000000000000000000..94a481425f0379f97bd860b0886a16f648c2abcb --- /dev/null +++ b/app/src/main/res/drawable/favourite_empty.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="utf-8"?><!-- +SPDX-FileCopyrightText: Google + +SPDX-License-Identifier: Apache-2.0 +--> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24" + android:viewportHeight="24" + android:tint="?attr/colorOnSurface"> + <path + android:fillColor="@android:color/white" + android:pathData="M22,9.24l-7.19,-0.62L12,2 9.19,8.63 2,9.24l5.46,4.73L5.82,21 12,17.27 18.18,21l-1.63,-7.03L22,9.24zM12,15.4l-3.76,2.27 1,-4.28 -3.32,-2.88 4.38,-0.38L12,6.1l1.71,4.04 4.38,0.38 -3.32,2.88 1,4.28L12,15.4z"/> +</vector> \ No newline at end of file diff --git a/app/src/main/res/drawable/favourite_full.xml b/app/src/main/res/drawable/favourite_full.xml new file mode 100644 index 0000000000000000000000000000000000000000..fc3f1e83de9cde90c2a6181e7c16bdb4c2310a33 --- /dev/null +++ b/app/src/main/res/drawable/favourite_full.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="utf-8"?><!-- +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,17.27L18.18,21l-1.64,-7.03L22,9.24l-7.19,-0.61L12,2 9.19,8.63 2,9.24l5.46,4.73L5.82,21z" /> +</vector> \ No newline at end of file diff --git a/app/src/main/res/layout/activity_departures.xml b/app/src/main/res/layout/activity_departures.xml index 4744dc352f27edaf2b35495e8d89e7775388ef95..d4361d70d4f149c2a311b179544e52b0c7994183 100644 --- a/app/src/main/res/layout/activity_departures.xml +++ b/app/src/main/res/layout/activity_departures.xml @@ -13,66 +13,51 @@ android:layout_width="match_parent" android:layout_height="match_parent" android:paddingBottom="16dp"> - <androidx.recyclerview.widget.RecyclerView - android:id="@+id/departures_recycler" + <com.google.android.material.appbar.AppBarLayout + android:id="@+id/app_bar_layout" android:layout_width="match_parent" - android:layout_height="match_parent" - android:clipToPadding="false" - android:fitsSystemWindows="true" - android:visibility="gone" - app:layout_behavior="@string/appbar_scrolling_view_behavior" /> + android:layout_height="wrap_content" + android:fitsSystemWindows="true"> - <com.google.android.material.card.MaterialCardView - android:id="@+id/alerts" - android:layout_width="match_parent" - android:layout_height="100dp" - android:backgroundTint="@color/safety" - android:visibility="gone" - app:layout_anchor="@id/app_bar_layout" - app:layout_anchorGravity="bottom"> + <com.google.android.material.appbar.CollapsingToolbarLayout + android:id="@+id/collapsing_layout" + style="?attr/collapsingToolbarLayoutMediumStyle" + android:layout_width="match_parent" + android:layout_height="?attr/collapsingToolbarLayoutMediumSize" + app:layout_scrollFlags="scroll|exitUntilCollapsed|snap" + app:maxLines="2"> - <androidx.constraintlayout.widget.ConstraintLayout + <com.google.android.material.appbar.MaterialToolbar + android:id="@+id/departures_app_bar" + android:layout_width="match_parent" + android:layout_height="?attr/actionBarSize" + android:elevation="0dp" + app:layout_collapseMode="pin" + app:menu="@menu/departures_menu" /> + + </com.google.android.material.appbar.CollapsingToolbarLayout> + + <com.google.android.material.progressindicator.LinearProgressIndicator + android:id="@+id/departures_updates_progress" android:layout_width="match_parent" - android:layout_height="match_parent"> + android:layout_height="wrap_content" + android:layout_marginStart="4dp" + android:layout_marginEnd="4dp" + android:indeterminate="true" + android:visibility="gone" /> - <ImageView - android:id="@+id/imageView" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_marginStart="8dp" - android:importantForAccessibility="no" - android:src="@drawable/warning" - app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toTopOf="@+id/alerts_text" - app:tint="@color/black" /> + </com.google.android.material.appbar.AppBarLayout> - <com.google.android.material.textview.MaterialTextView - android:id="@+id/alerts_text" - android:layout_width="0dp" - android:layout_height="0dp" - android:layout_marginStart="8dp" - android:layout_marginTop="58dp" - android:layout_marginEnd="8dp" - android:ellipsize="end" - android:maxLines="2" - android:textColor="@color/black" - app:layout_constraintBottom_toBottomOf="parent" - app:layout_constraintEnd_toStartOf="@+id/more_button" - app:layout_constraintStart_toEndOf="@+id/imageView" - app:layout_constraintTop_toTopOf="parent" - tool:text="Warning: Serious blockade on Piastowska towards Wojska Polskiego. Lines 5, 14, 163 diverted. Change for other means of transport, e.g. lines \n\naaaaa" /> - - <com.google.android.material.button.MaterialButton - android:id="@+id/more_button" - style="@style/Widget.Material3.Button.TextButton" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:text="@string/more" - android:textColor="@color/link" - app:layout_constraintBottom_toBottomOf="parent" - app:layout_constraintEnd_toEndOf="parent" /> - </androidx.constraintlayout.widget.ConstraintLayout> - </com.google.android.material.card.MaterialCardView> + <androidx.recyclerview.widget.RecyclerView + android:id="@+id/departures_recycler" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:layout_marginTop="8dp" + android:clipToPadding="false" + app:layout_behavior="@string/appbar_scrolling_view_behavior" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/alerts" /> <androidx.constraintlayout.widget.ConstraintLayout android:id="@+id/departures_overlay" @@ -116,31 +101,5 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/error_image" tool:text="No connection" /> </androidx.constraintlayout.widget.ConstraintLayout> - - <com.google.android.material.appbar.AppBarLayout - android:id="@+id/app_bar_layout" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:fitsSystemWindows="true"> - - <com.google.android.material.appbar.CollapsingToolbarLayout - android:id="@+id/collapsing_layout" - style="?attr/collapsingToolbarLayoutMediumStyle" - android:layout_width="match_parent" - android:layout_height="?attr/collapsingToolbarLayoutMediumSize" - app:layout_scrollFlags="scroll|exitUntilCollapsed|snap" - app:maxLines="2"> - - <com.google.android.material.appbar.MaterialToolbar - android:id="@+id/departures_app_bar" - android:layout_width="match_parent" - android:layout_height="?attr/actionBarSize" - android:elevation="0dp" - app:layout_collapseMode="pin" - app:menu="@menu/departures_menu" /> - - </com.google.android.material.appbar.CollapsingToolbarLayout> - - </com.google.android.material.appbar.AppBarLayout> </androidx.coordinatorlayout.widget.CoordinatorLayout> \ No newline at end of file diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index c4a4b92fc7e9ffa3068acb27897972c4c71ef6aa..8869f8b60d73a3890d711e643e79f2bec0ec2d2c 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -12,21 +12,6 @@ android:id="@+id/container" android:layout_width="match_parent" android:layout_height="match_parent"> - <androidx.coordinatorlayout.widget.CoordinatorLayout - android:layout_width="match_parent" - android:layout_height="match_parent"> - - <com.google.android.material.appbar.AppBarLayout - android:layout_width="match_parent" - android:layout_height="wrap_content"> - - <com.google.android.material.appbar.MaterialToolbar - android:id="@+id/top_app_bar" - android:layout_width="match_parent" - android:layout_height="?attr/actionBarSize" - app:title="" /> - </com.google.android.material.appbar.AppBarLayout> - <androidx.constraintlayout.widget.ConstraintLayout android:layout_width="match_parent" android:layout_height="match_parent"> @@ -53,7 +38,6 @@ app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:menu="@menu/bottom_nav_menu" /> </androidx.constraintlayout.widget.ConstraintLayout> - </androidx.coordinatorlayout.widget.CoordinatorLayout> <com.google.android.material.navigation.NavigationView android:id="@+id/navigation_drawer" diff --git a/app/src/main/res/layout/activity_results.xml b/app/src/main/res/layout/activity_results.xml index c4813dace5058c3fffcf5bb325f8ddd75ba24e33..92b758af8e0a7d62e6b0a9ac97749e09f7b443a5 100644 --- a/app/src/main/res/layout/activity_results.xml +++ b/app/src/main/res/layout/activity_results.xml @@ -13,6 +13,20 @@ android:layout_width="match_parent" android:layout_height="match_parent" android:paddingBottom="16dp"> + <com.google.android.material.appbar.AppBarLayout + android:id="@+id/app_bar_layout" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:fitsSystemWindows="true"> + + <com.google.android.material.appbar.MaterialToolbar + android:id="@+id/top_app_bar" + android:layout_width="match_parent" + android:layout_height="?attr/actionBarSize" + app:title="@string/title_activity_results" /> + + </com.google.android.material.appbar.AppBarLayout> + <androidx.constraintlayout.widget.ConstraintLayout android:id="@+id/results_overlay" android:layout_width="match_parent" @@ -56,20 +70,6 @@ app:layout_constraintTop_toBottomOf="@+id/error_image" tool:text="No connection" /> </androidx.constraintlayout.widget.ConstraintLayout> - - <com.google.android.material.appbar.AppBarLayout - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:fitsSystemWindows="true" - app:liftOnScroll="true"> - - <com.google.android.material.appbar.MaterialToolbar - android:id="@+id/top_app_bar" - android:layout_width="match_parent" - android:layout_height="?attr/actionBarSize" - app:title="@string/title_activity_results" /> - - </com.google.android.material.appbar.AppBarLayout> <androidx.recyclerview.widget.RecyclerView android:id="@+id/results_recycler" diff --git a/app/src/main/res/layout/alert.xml b/app/src/main/res/layout/alert.xml new file mode 100644 index 0000000000000000000000000000000000000000..643fd32e34735a592742d869500910aa13314b87 --- /dev/null +++ b/app/src/main/res/layout/alert.xml @@ -0,0 +1,54 @@ +<?xml version="1.0" encoding="utf-8"?> +<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tool="http://schemas.android.com/tools" + android:id="@+id/alerts" + android:layout_width="match_parent" + android:layout_height="64dp" + android:layout_marginStart="8dp" + android:layout_marginTop="8dp" + android:layout_marginEnd="8dp" + android:backgroundTint="@color/safety"> + + <androidx.constraintlayout.widget.ConstraintLayout + android:layout_width="match_parent" + android:layout_height="match_parent"> + + <ImageView + android:id="@+id/imageView" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginStart="8dp" + android:importantForAccessibility="no" + android:src="@drawable/warning" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="@+id/alerts_text" + app:tint="@color/black" /> + + <com.google.android.material.textview.MaterialTextView + android:id="@+id/alerts_text" + tool:text="Warning: Serious blockade on Piastowska towards Wojska Polskiego. Lines 5, 14, 163 diverted. Change for other means of transport, e.g. lines \n\naaaaa" + android:layout_width="0dp" + android:layout_height="0dp" + android:layout_marginStart="8dp" + android:layout_marginTop="8dp" + android:layout_marginEnd="8dp" + android:ellipsize="end" + android:maxLines="2" + android:textColor="@color/black" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toStartOf="@+id/more_button" + app:layout_constraintStart_toEndOf="@+id/imageView" + app:layout_constraintTop_toTopOf="parent" /> + + <com.google.android.material.button.MaterialButton + android:id="@+id/more_button" + style="@style/Widget.Material3.Button.TextButton" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@string/more" + android:textColor="@color/link" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" /> + </androidx.constraintlayout.widget.ConstraintLayout> +</com.google.android.material.card.MaterialCardView> \ No newline at end of file diff --git a/app/src/main/res/layout/favourite.xml b/app/src/main/res/layout/favourite.xml new file mode 100644 index 0000000000000000000000000000000000000000..7d47ac03dc672895ee185d4c471ffb19cf4847b7 --- /dev/null +++ b/app/src/main/res/layout/favourite.xml @@ -0,0 +1,94 @@ +<?xml version="1.0" encoding="utf-8"?><!-- +SPDX-FileCopyrightText: Adam Evyčędo + +SPDX-License-Identifier: GPL-3.0-or-later +--> +<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tool="http://schemas.android.com/tools" + android:id="@+id/favourite" + android:layout_marginTop="8dp" + android:layout_marginBottom="8dp" + android:layout_width="match_parent" + android:layout_height="wrap_content"> + + <androidx.constraintlayout.widget.ConstraintLayout + android:layout_width="match_parent" + android:layout_height="match_parent" + android:paddingBottom="16dp"> + + <com.google.android.material.textview.MaterialTextView + android:id="@+id/feed_name" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginStart="16dp" + android:layout_marginTop="16dp" + android:textAppearance="@style/TextAppearance.Material3.LabelSmall" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" + tool:text="GB" /> + + <com.google.android.material.textview.MaterialTextView + android:id="@+id/stop_name" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginStart="16dp" + android:textAppearance="@style/TextAppearance.Material3.HeadlineSmall" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/feed_name" + tool:text="Westminster" /> + + <ImageView + android:id="@+id/line_icon" + android:layout_width="24dp" + android:layout_height="24dp" + android:layout_marginStart="8dp" + app:layout_constraintBottom_toTopOf="@+id/departure_headsign" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="@+id/departure_time" + tool:ignore="ContentDescription" + tool:srcCompat="@drawable/bus_black" /> + + <com.google.android.material.textview.MaterialTextView + android:id="@+id/departure_time" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginEnd="8dp" + android:textAppearance="@style/TextAppearance.Material3.HeadlineSmall" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintTop_toTopOf="@+id/departure_line" + tool:text="1hr" /> + + <com.google.android.material.textview.MaterialTextView + android:id="@+id/departure_full_time" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginTop="4dp" + android:textAppearance="@style/TextAppearance.Material3.LabelSmall" + app:layout_constraintEnd_toEndOf="@+id/departure_time" + app:layout_constraintTop_toBottomOf="@+id/departure_time" + tool:text="18:55" /> + + <com.google.android.material.textview.MaterialTextView + android:id="@+id/departure_line" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginStart="8dp" + android:layout_marginTop="8dp" + android:text="@string/loading" + android:textAppearance="@style/TextAppearance.Material3.HeadlineMedium" + app:layout_constraintStart_toEndOf="@+id/line_icon" + app:layout_constraintTop_toBottomOf="@+id/stop_name" + tool:text="Metropolitan" /> + + <com.google.android.material.textview.MaterialTextView + android:id="@+id/departure_headsign" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:textAppearance="@style/TextAppearance.Material3.BodySmall" + app:layout_constraintStart_toStartOf="@+id/departure_line" + app:layout_constraintTop_toBottomOf="@+id/departure_line" + tool:text="» Tower Hill" /> + + </androidx.constraintlayout.widget.ConstraintLayout> +</com.google.android.material.card.MaterialCardView> \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_home.xml b/app/src/main/res/layout/fragment_home.xml index eb621f767365a76f5fa26f83738325eaefa828dc..925487e2b450251a216845ce8573e76fb1c4acb8 100644 --- a/app/src/main/res/layout/fragment_home.xml +++ b/app/src/main/res/layout/fragment_home.xml @@ -9,14 +9,15 @@ <androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tool="http://schemas.android.com/tools" + android:id="@+id/fragment_root" android:layout_width="match_parent" android:layout_height="match_parent" android:tag="@string/title_home" tool:context="xyz.apiote.bimba.czwek.dashboard.ui.home.HomeFragment"> - <com.google.android.material.appbar.AppBarLayout + <androidx.constraintlayout.widget.ConstraintLayout android:layout_width="match_parent" - android:layout_height="wrap_content"> + android:layout_height="match_parent"> <com.google.android.material.search.SearchBar android:id="@+id/search_bar" @@ -31,32 +32,30 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" app:navigationIcon="@drawable/menu" app:useDrawerArrowDrawable="true" /> - </com.google.android.material.appbar.AppBarLayout> - <com.google.android.material.search.SearchView - android:id="@+id/search_view" - android:layout_width="match_parent" - android:layout_height="match_parent" - android:hint="@string/search_placeholder" - app:layout_anchor="@id/search_bar"> - - <androidx.recyclerview.widget.RecyclerView - android:id="@+id/suggestions_recycler" + <com.google.android.material.search.SearchView + android:id="@+id/search_view" android:layout_width="match_parent" android:layout_height="match_parent" - app:layout_behavior="@string/appbar_scrolling_view_behavior" /> - </com.google.android.material.search.SearchView> + android:hint="@string/search_placeholder" + app:layout_anchor="@id/search_bar" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent"> - <androidx.constraintlayout.widget.ConstraintLayout - android:layout_width="match_parent" - android:layout_height="match_parent"> + <androidx.recyclerview.widget.RecyclerView + android:id="@+id/suggestions_recycler" + android:layout_width="match_parent" + android:layout_height="match_parent" /> + </com.google.android.material.search.SearchView> <ImageView android:id="@+id/inari" android:layout_width="0dp" android:layout_height="0dp" android:layout_marginStart="16dp" - android:layout_marginTop="64dp" + android:layout_marginTop="16dp" android:layout_marginEnd="16dp" android:layout_marginBottom="16dp" android:alpha="0.25" @@ -67,6 +66,17 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" tool:ignore="ContentDescription,ImageContrastCheck" /> + <androidx.recyclerview.widget.RecyclerView + android:id="@+id/favourites" + android:layout_width="0dp" + android:layout_height="0dp" + android:layout_marginStart="16dp" + android:layout_marginTop="16dp" + android:layout_marginEnd="16dp" + app:layout_constraintBottom_toBottomOf="@id/inari" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/search_bar" /> </androidx.constraintlayout.widget.ConstraintLayout> <com.google.android.material.floatingactionbutton.FloatingActionButton @@ -77,7 +87,6 @@ android:layout_gravity="bottom|end" android:layout_margin="16dp" android:contentDescription="@string/home_fab_description" android:src="@drawable/gps_black" - app:layout_constraintBottom_toBottomOf="parent" - app:layout_constraintEnd_toEndOf="parent" + app:layout_behavior="com.google.android.material.floatingactionbutton.FloatingActionButton$Behavior" tool:ignore="ImageContrastCheck" /> </androidx.coordinatorlayout.widget.CoordinatorLayout> \ No newline at end of file diff --git a/app/src/main/res/menu/departures_menu.xml b/app/src/main/res/menu/departures_menu.xml index 2a4d18f7c839b628af7e21dcae639ed287fb05e5..0c3f27a935ef2b0bca568758f44f8d641d5d3c6c 100644 --- a/app/src/main/res/menu/departures_menu.xml +++ b/app/src/main/res/menu/departures_menu.xml @@ -9,6 +9,12 @@ <menu xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto"> <item + android:id="@+id/favourite" + android:icon="@drawable/favourite_empty" + app:showAsAction="always" + android:contentDescription="@string/favourite_content_description" + android:title="@string/favourite" /> + <item android:id="@+id/departures_filter" android:icon="@drawable/filter" app:showAsAction="ifRoom" diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index fd0d12a8a9e5c7a3b6851551306b5d4ef7820137..19afd0e9e16147ad1463025d7342a239b206452a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -126,5 +126,15 @@ Select end time <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="no_next_departures">no more departures</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> </resources> diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml new file mode 100644 index 0000000000000000000000000000000000000000..df879f01ea8436856a0eed72136b1a4b85951084 --- /dev/null +++ b/app/src/main/res/values-fr/strings.xml @@ -0,0 +1,119 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="app_name">Bimba</string> + <string name="title_home">Accueil</string> + <string name="home_fab_description">Icône GPS</string> + <string name="save">Enregistrer</string> + <string name="cont">Continuer</string> + <string name="search_placeholder">Arrêts, lignes, ou Plus Codes</string> + <string name="error_401">Un jeton est nécessaire pour utiliser ce serveur</string> + <string name="error_403">Le jeton fourni est incorrect</string> + <string name="error_50x">Il y a eu une erreur sur le serveur. Réessayez plus tard</string> + <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="congestion_unknown">Inconnu</string> + <string name="congestion_smooth">Fluide</string> + <string name="occupancy_standing_only">Uniquement debout</string> + <string name="congestion_jams">Embouteillages</string> + <string name="departure_departed">Parti</string> + <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="on_boarding">Embarquement</string> + <string name="boarding">Embarquement possible</string> + <string name="line_headsign">» %1$s</string> + <string name="stops_nearby">Arrêts à proximité</string> + <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</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> + <string name="stop_content_description">Arrêt</string> + <string name="onboarding_advanced">Avancé</string> + <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="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="departures_snackbar">Dernière mise à jour : %1$s</string> + <string name="title_filter">Filtrer</string> + <string name="cancel">Annuler</string> + <string name="more">Plus</string> + <string name="title_voyage">Voyage</string> + <string name="title_activity_results">Résultats</string> + <string name="title_map">Carte</string> + <string name="no_departures">Aucun départ</string> + <string name="bimba_server_address_hint">Serveur</string> + <string name="air_condition_content_description">Air conditionné</string> + <string name="bicycles_allowed_content_description">Vélos autorisés</string> + <string name="show_departures">Montrer les départs</string> + <string name="bimba_server_continue_button">Continuer</string> + <string name="realtime_content_description">Départ en temps réel</string> + <string name="open_in_maps_app">Ouvrir dans une application de cartes</string> + <string name="departure_now">Maintenant</string> + <string name="results_for">Résultats pour ‘%1$s’</string> + <string name="at_time_realtime">à %1$02d:%2$02d:%3$02d</string> + <string name="wheelchair_content_description">Le véhicule est accessible pour les fauteuils roulants</string> + <string name="departure_momentarily">Prochainement</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="speed_in_km_per_h">%1$.3f km/h</string> + <string name="occupancy_unknown">Inconnu</string> + <string name="occupancy_few_seats">Peu de sièges</string> + <string name="occupancy_crowded">Encombré</string> + <string name="occupancy_empty">Vide</string> + <string name="occupancy_many_seats">Nombreux sièges</string> + <string name="occupancy_full">Rempli</string> + <string name="no_map_app">Aucune application de carte installée</string> + <string name="code_button_description">Lien vers le code source</string> + <string name="mastodon_button_description">Lien vers Mastodon</string> + <string name="title_select_date">Choisir le jour du départ</string> + <string name="title_select_line">Choisir une ligne</string> + <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="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="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 ; un horaire dans votre poche.</string> + <string name="error_41">Cette localité n\'est pas couverte par le serveur</string> + <string name="current_timetable_validity">Horraire 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="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="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) © Mastodon contributors, 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 © OpenStreetMap contributors, ODbL-1.0</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> + <string name="no_location_access">Accès à la position non autorisé</string> + <string name="seatbelts_everyone">En voiture tout le monde!</string> + <string name="onboarding_simple">Simple</string> +</resources> \ No newline at end of file diff --git a/bimba.svg b/bimba.svg new file mode 100644 index 0000000000000000000000000000000000000000..8bfa5182d6af0ad1a4cfa32f9383bcb8c1844c9a --- /dev/null +++ b/bimba.svg @@ -0,0 +1,158 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!-- +SPDX-FileCopyrightText: https://github.com/tebriz159 + +SPDX-License-Identifier: GPL-3.0-or-later +--> +<svg + viewBox="0 0 91.999998 92" + id="vector" + version="1.1" + sodipodi:docname="bimba.svg" + width="92" + height="92" + inkscape:version="1.3.2 (091e20ef0f, 2023-11-25)" + inkscape:export-filename="bimba.png" + inkscape:export-xdpi="534.26086" + inkscape:export-ydpi="534.26086" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns="http://www.w3.org/2000/svg" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:dc="http://purl.org/dc/elements/1.1/"> + <metadata + id="metadata176"> + <rdf:RDF> + <cc:Work + rdf:about=""> + <dc:format>image/svg+xml</dc:format> + <dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> + </cc:Work> + </rdf:RDF> + </metadata> + <defs + id="defs174" /> + <sodipodi:namedview + pagecolor="#ffffff" + bordercolor="#666666" + borderopacity="1" + objecttolerance="10" + gridtolerance="10" + guidetolerance="10" + inkscape:pageopacity="0" + inkscape:pageshadow="2" + inkscape:window-width="1504" + inkscape:window-height="1002" + id="namedview172" + showgrid="false" + fit-margin-top="8" + fit-margin-left="8" + fit-margin-bottom="8" + fit-margin-right="8" + inkscape:zoom="8.5108696" + inkscape:cx="46" + inkscape:cy="-26.965517" + inkscape:window-x="0" + inkscape:window-y="0" + inkscape:window-maximized="0" + inkscape:current-layer="vector" + inkscape:showpageshadow="0" + inkscape:pagecheckerboard="0" + inkscape:deskcolor="#505050" /> + <rect + id="rect178" + width="92" + height="92" + x="0" + y="0" + style="display:inline;fill:#3a3a3b;fill-opacity:1;stroke-width:0.436372" + ry="0" /> + <g + id="group_1" + transform="matrix(0.16904335,0,0,0.16904335,-7.9999769,-8)" + style="display:inline"> + <g + id="group" + transform="translate(173.03255,173.03255)"> + <path + id="path" + d="m 62.962,292.5 v 0 c -3.7,-0.9 -5.7,-3.8 -4.3,-6.5 l 32.3,-62.9 c 1.3,-2.6 5.5,-4 9.2,-3 v 0 c 3.7,0.9 5.7,3.8 4.3,6.5 l -32.3,62.9 c -1.3,2.6 -5.4,4 -9.2,3 z" + inkscape:connector-curvature="0" + style="fill:#54af39" /> + <path + id="path_1" + d="m 229.662,292.5 v 0 c 3.7,-0.9 5.7,-3.8 4.3,-6.5 l -32.3,-62.9 c -1.3,-2.6 -5.5,-4 -9.2,-3 v 0 c -3.7,0.9 -5.7,3.8 -4.3,6.5 l 32.3,62.9 c 1.3,2.6 5.5,4 9.2,3 z" + inkscape:connector-curvature="0" + style="fill:#54af39" /> + <path + id="path_2" + d="m 151.362,36.9 -0.6,0.2 c -1.8,0.7 -3.8,-0.3 -4.5,-2.1 l -9.6,-26.7 c -0.7,-1.8 0.3,-3.8 2.1,-4.5 l 0.6,-0.2 c 1.8,-0.7 3.8,0.3 4.5,2.1 l 9.6,26.7 c 0.7,1.9 -0.3,3.9 -2.1,4.5 z" + inkscape:connector-curvature="0" + style="fill:#54af39" /> + <path + id="path_3" + d="m 180.662,3.8 v 0 c 0,2.1 -1.7,3.8 -3.8,3.8 h -61 c -2.1,0 -3.8,-1.7 -3.8,-3.8 v 0 c 0,-2.1 1.7,-3.8 3.8,-3.8 h 61 c 2.1,0 3.8,1.7 3.8,3.8 z" + inkscape:connector-curvature="0" + style="fill:#54af39" /> + <path + id="path_4" + d="m 218.762,236.7 h -144.9 c -13.3,0 -24,-10.8 -24,-24 v -108 c 0,-41.3 33.5,-74.9 74.9,-74.9 h 43.3 c 41.3,0 74.9,33.5 74.9,74.9 v 108 c -0.1,13.2 -10.9,24 -24.2,24 z" + inkscape:connector-curvature="0" + style="fill:#54af39" /> + <path + id="path_5" + d="m 212.562,146.2 h -132.5 c -5.6,0 -10.2,-4.5 -10.2,-10.2 v -34.9 c 0,-16.9 13.7,-30.6 30.6,-30.6 h 91.7 c 16.9,0 30.6,13.7 30.6,30.6 V 136 c -0.1,5.7 -4.6,10.2 -10.2,10.2 z" + inkscape:connector-curvature="0" + style="fill:#ffffff" /> + <path + id="path_6" + d="m 161.462,55.5 h -30.3 c -3.2,0 -5.7,-2.6 -5.7,-5.7 v 0 c 0,-3.2 2.6,-5.7 5.7,-5.7 h 30.3 c 3.2,0 5.7,2.6 5.7,5.7 v 0 c 0,3.1 -2.6,5.7 -5.7,5.7 z" + inkscape:connector-curvature="0" + style="fill:#ffffff" /> + <path + id="path_7" + d="m 87.062,191.9 m -14.8,0 c 0,-3.924 1.56,-7.691 4.335,-10.465 2.774,-2.775 6.541,-4.335 10.465,-4.335 3.924,0 7.691,1.56 10.465,4.335 2.775,2.774 4.335,6.541 4.335,10.465 0,3.924 -1.56,7.691 -4.335,10.465 -2.774,2.775 -6.541,4.335 -10.465,4.335 -3.924,0 -7.691,-1.56 -10.465,-4.335 -2.775,-2.774 -4.335,-6.541 -4.335,-10.465" + inkscape:connector-curvature="0" + style="fill:#ffffff" /> + <path + id="path_8" + d="m 205.662,191.9 m -14.8,0 c 0,-3.924 1.56,-7.691 4.335,-10.465 2.774,-2.775 6.541,-4.335 10.465,-4.335 3.924,0 7.691,1.56 10.465,4.335 2.775,2.774 4.335,6.541 4.335,10.465 0,3.924 -1.56,7.691 -4.335,10.465 -2.774,2.775 -6.541,4.335 -10.465,4.335 -3.924,0 -7.691,-1.56 -10.465,-4.335 -2.775,-2.774 -4.335,-6.541 -4.335,-10.465" + inkscape:connector-curvature="0" + style="fill:#ffffff" /> + </g> + </g> + <g + aria-label="Bimba" + style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:56px;line-height:1.25;font-family:Railway;-inkscape-font-specification:Railway;letter-spacing:0px;word-spacing:0px;display:none;fill:#ffffff;fill-opacity:1;stroke:none" + id="text1004" + transform="translate(-8.114,-14.86441)"> + <path + d="m 116.40217,69.760412 c 0,0.896 -0.256,1.792 -0.704,2.624 -0.448,0.832 -1.024,1.536 -1.792,2.176 -0.768,0.64 -1.6,1.088 -2.624,1.472 -1.024,0.384 -2.048,0.512 -3.2,0.512 h -7.936 v -14.848 h 3.392 4.416 c 1.152,0 2.24,0.256 3.264,0.64 1.024,0.448 1.92,1.024 2.688,1.728 0.768,0.768 1.344,1.6 1.792,2.56 0.448,1.024 0.704,2.048 0.704,3.136 z m -4.672,-19.968 c 0,0.768 -0.192,1.536 -0.512,2.176 -0.384,0.64 -0.832,1.216 -1.408,1.728 -0.576,0.512 -1.216,0.896 -1.984,1.152 -0.768,0.32 -1.536,0.448 -2.368,0.448 h -5.312 v -10.752 h 5.12 c 0.832,0 1.664,0.192 2.432,0.448 0.768,0.256 1.472,0.64 2.112,1.088 0.576,0.512 1.024,1.024 1.408,1.664 0.32,0.64 0.512,1.344 0.512,2.048 z m 11.584,19.904 c 0,-2.688 -0.768,-5.184 -2.24,-7.424 -1.536,-2.24 -3.456,-3.968 -5.824,-5.248 0.96,-0.96 1.728,-2.048 2.304,-3.328 0.512,-1.216 0.832,-2.496 0.832,-3.904 0,-1.6 -0.384,-3.136 -1.024,-4.544 -0.704,-1.408 -1.664,-2.624 -2.816,-3.712 -1.216,-1.024 -2.624,-1.856 -4.224,-2.496 -1.6,-0.576 -3.264,-0.896 -5.056,-0.896 H 93.746168 v 44.8 h 14.272002 c 2.048,0 4.032,-0.32 5.888,-1.024 1.856,-0.704 3.456,-1.664 4.864,-2.88 1.408,-1.152 2.496,-2.56 3.328,-4.224 0.768,-1.6 1.216,-3.328 1.216,-5.12 z" + style="font-size:64px" + id="path4709" + inkscape:connector-curvature="0" /> + <path + d="m 134.45016,82.944412 v -30.336 h -6.336 v 30.336 z m 1.344,-38.272 -4.48,-4.544 -4.48,4.544 4.48,4.48 z" + style="font-size:64px" + id="path4711" + inkscape:connector-curvature="0" /> + <path + d="m 188.59409,82.944412 v -18.048 c 0,-1.664 -0.384,-3.328 -1.024,-4.864 -0.704,-1.536 -1.6,-2.88 -2.752,-4.032 -1.152,-1.152 -2.496,-2.048 -4.032,-2.752 -1.536,-0.64 -3.072,-1.024 -4.736,-1.024 -2.048,0 -4.032,0.448 -5.888,1.216 -1.92,0.832 -3.712,1.984 -5.376,3.392 -1.216,-1.344 -2.624,-2.432 -4.224,-3.328 -1.664,-0.832 -3.392,-1.28 -5.184,-1.28 -1.472,0 -2.88,0.192 -4.16,0.576 -1.344,0.384 -2.56,0.96 -3.712,1.6 v -1.856 h -6.592 v 30.4 h 6.528 v -21.504 c 0.768,-0.768 1.792,-1.408 3.008,-1.92 1.216,-0.448 2.56,-0.704 4.032,-0.704 0.96,0 1.856,0.192 2.752,0.512 0.832,0.32 1.6,0.768 2.24,1.28 0.64,0.576 1.088,1.216 1.472,1.984 0.384,0.768 0.576,1.536 0.576,2.304 v 18.048 h 6.4 v -20.096 c 0.96,-1.216 2.048,-2.176 3.328,-2.944 1.216,-0.704 2.496,-1.088 3.904,-1.088 0.96,0 1.856,0.192 2.752,0.512 0.832,0.32 1.6,0.768 2.24,1.28 0.64,0.576 1.088,1.216 1.472,1.984 0.384,0.768 0.576,1.536 0.576,2.304 v 18.048 z" + style="font-size:64px" + id="path4713" + inkscape:connector-curvature="0" /> + <path + d="m 217.90603,67.776412 c 0,1.28 -0.256,2.496 -0.768,3.648 -0.512,1.152 -1.152,2.176 -1.984,3.008 -0.896,0.896 -1.856,1.536 -2.944,2.048 -1.152,0.512 -2.304,0.704 -3.584,0.704 -1.536,0 -3.008,-0.256 -4.352,-0.768 -1.408,-0.512 -2.56,-1.216 -3.52,-2.112 v -12.736 c 0.96,-0.896 2.112,-1.6 3.456,-2.112 1.344,-0.512 2.816,-0.832 4.416,-0.832 1.28,0 2.432,0.256 3.584,0.768 1.088,0.512 2.048,1.152 2.944,1.984 0.832,0.832 1.472,1.792 1.984,2.88 0.512,1.152 0.768,2.304 0.768,3.52 z m 6.4,0 c 0,-2.112 -0.448,-4.096 -1.28,-6.016 -0.832,-1.856 -1.92,-3.52 -3.328,-4.928 -1.408,-1.408 -3.072,-2.496 -4.992,-3.328 -1.92,-0.832 -3.904,-1.28 -6.016,-1.28 -1.472,0 -2.88,0.192 -4.224,0.576 -1.344,0.384 -2.56,0.96 -3.712,1.6 v -16.256 h -6.4 v 44.8 h 6.4 v -1.472 c 1.152,0.704 2.368,1.216 3.712,1.6 1.28,0.384 2.688,0.512 4.16,0.512 2.112,0 4.096,-0.384 6.016,-1.216 1.92,-0.832 3.584,-1.984 4.992,-3.392 1.408,-1.408 2.56,-3.136 3.392,-5.056 0.832,-1.92 1.28,-3.968 1.28,-6.144 z" + style="font-size:64px" + id="path4715" + inkscape:connector-curvature="0" /> + <path + d="m 246.57798,76.160412 c -0.896,0.512 -1.856,0.96 -2.88,1.28 -1.024,0.32 -2.048,0.448 -2.944,0.448 -1.664,0 -3.136,-0.192 -4.288,-0.576 -1.152,-0.384 -1.728,-1.216 -1.728,-2.624 0,-0.896 0.256,-1.664 0.896,-2.304 0.64,-0.576 1.472,-1.024 2.56,-1.344 1.024,-0.32 2.304,-0.576 3.776,-0.704 1.408,-0.128 2.944,-0.192 4.608,-0.192 z m 6.4,6.784 v -18.88 c 0,-2.816 -0.448,-5.12 -1.344,-6.848 -0.896,-1.728 -1.92,-3.072 -3.2,-3.968 -1.28,-0.896 -2.624,-1.472 -4.032,-1.728 -1.408,-0.256 -2.688,-0.448 -3.776,-0.448 -1.536,0 -3.072,0.256 -4.672,0.768 -1.664,0.512 -3.072,1.088 -4.224,1.856 l -2.432,8.064 h 0.64 c 1.792,-1.6 3.52,-2.688 5.184,-3.328 1.664,-0.64 3.392,-0.96 5.248,-0.96 1.216,0 2.24,0.192 3.072,0.448 0.768,0.32 1.408,0.768 1.92,1.344 0.448,0.576 0.768,1.28 0.96,2.176 0.128,0.896 0.256,1.856 0.256,2.944 -3.328,0 -6.144,0.32 -8.448,0.832 -2.304,0.576 -4.16,1.344 -5.568,2.24 -1.408,0.96 -2.432,2.048 -3.072,3.328 -0.64,1.28 -0.896,2.624 -0.896,4.032 0,1.28 0.256,2.432 0.832,3.456 0.512,1.024 1.28,1.984 2.24,2.752 0.896,0.832 1.984,1.472 3.264,1.92 1.28,0.448 2.624,0.64 4.096,0.64 1.152,0 2.368,-0.192 3.776,-0.576 1.408,-0.384 2.624,-0.896 3.776,-1.664 v 1.6 z" + style="font-size:64px" + id="path4717" + inkscape:connector-curvature="0" /> + </g> +</svg> diff --git a/metadata/en-GB/changelogs/28.txt b/metadata/en-GB/changelogs/28.txt new file mode 100644 index 0000000000000000000000000000000000000000..d809d57cf88dd3aea767adf91b0362ca0174683a --- /dev/null +++ b/metadata/en-GB/changelogs/28.txt @@ -0,0 +1,9 @@ +Changes in version 3.4 +* Added countdown to show next update of departures +* Fixed persisting filters and bottom sheets across screen changes +* Changed layout of alerts above departures +* Fixed phantom search results +* Fixed edge-to-edge and landscape layouts +* Added simple favourites +* Added French translation (by ArnaudDvs) + diff --git a/metadata/en-GB/images/phoneScreenshots/dash_w_fav.png b/metadata/en-GB/images/phoneScreenshots/dash_w_fav.png new file mode 100644 index 0000000000000000000000000000000000000000..cb5c6df2d6613b95031e864a2e691de790eb9e84 Binary files /dev/null and b/metadata/en-GB/images/phoneScreenshots/dash_w_fav.png differ diff --git a/metadata/en-US/changelogs/28.txt b/metadata/en-US/changelogs/28.txt new file mode 100644 index 0000000000000000000000000000000000000000..cb8a27715209ea9d00bbeee41297eea86c78bd6a --- /dev/null +++ b/metadata/en-US/changelogs/28.txt @@ -0,0 +1,8 @@ +Changes in version 3.4 +* Added countdown to show next update of departures +* Fixed persisting filters and bottom sheets across screen changes +* Changed layout of alerts above departures +* Fixed phantom search results +* Fixed edge-to-edge and landscape layouts +* Added simple favorites +* Added French translation (by ArnaudDvs) diff --git a/metadata/en-US/images/phoneScreenshots/dash_w_fav.png b/metadata/en-US/images/phoneScreenshots/dash_w_fav.png new file mode 100644 index 0000000000000000000000000000000000000000..cb5c6df2d6613b95031e864a2e691de790eb9e84 Binary files /dev/null and b/metadata/en-US/images/phoneScreenshots/dash_w_fav.png differ diff --git a/metadata/fr-FR/changelogs/20.txt b/metadata/fr-FR/changelogs/20.txt new file mode 100644 index 0000000000000000000000000000000000000000..aa3a5c91697382ea5b00c3252d51aad6a3319d0b --- /dev/null +++ b/metadata/fr-FR/changelogs/20.txt @@ -0,0 +1,5 @@ +La version 3.0 a été complètement réécrite, et présente une toute nouvelle architecture. + +* Les favoris sont supprimés +* Les arrêts et véhicules sont indiqués sur une carte +* L'interface est désormais Material3 diff --git a/metadata/fr-FR/changelogs/21.txt b/metadata/fr-FR/changelogs/21.txt new file mode 100644 index 0000000000000000000000000000000000000000..aa3a5c91697382ea5b00c3252d51aad6a3319d0b --- /dev/null +++ b/metadata/fr-FR/changelogs/21.txt @@ -0,0 +1,5 @@ +La version 3.0 a été complètement réécrite, et présente une toute nouvelle architecture. + +* Les favoris sont supprimés +* Les arrêts et véhicules sont indiqués sur une carte +* L'interface est désormais Material3 diff --git a/metadata/fr-FR/changelogs/22.txt b/metadata/fr-FR/changelogs/22.txt new file mode 100644 index 0000000000000000000000000000000000000000..5a6ae1abe9e941b410ef81fa1dc70ab2fa3fa5c3 --- /dev/null +++ b/metadata/fr-FR/changelogs/22.txt @@ -0,0 +1,11 @@ +Caractéristiques de la version 3.1 : +* icône de lancement monochrome pour Android 12, +* thème Material You avec couleurs du système, +* nouvelle barre de recherche, +* recherche par code Open Location, +* glissement dans la minicarte, +* traduction en polonais et en italien, +* gestion de multiples flux, +* codes QR dans la Métropole GZM, +* Recherche par ligne (et sélection d'arrêt sur graphes), +* mise à jour des dépendances, corrections de bugs, et refactorisation du code. diff --git a/metadata/fr-FR/changelogs/23.txt b/metadata/fr-FR/changelogs/23.txt new file mode 100644 index 0000000000000000000000000000000000000000..47cca9d523ae18d4118e7fa1f3c2f4102807ecca --- /dev/null +++ b/metadata/fr-FR/changelogs/23.txt @@ -0,0 +1 @@ +La version 3.1.1 corrige le bouton "Continuer" lors du choix du serveur diff --git a/metadata/fr-FR/changelogs/24.txt b/metadata/fr-FR/changelogs/24.txt new file mode 100644 index 0000000000000000000000000000000000000000..0855dbc3062b5827cd01bc882cbce227ba727f9b --- /dev/null +++ b/metadata/fr-FR/changelogs/24.txt @@ -0,0 +1,10 @@ +Changements dans la version 3.2 : +* Ajout d'un écran "A propos" +* le défilement vers le bas charge plus de départs +* les informations de localités et les paramètres sont mis en cache hors ligne +* les listes mettent seulement à jour les éléments modifiés +* les résultats de recherche incluent le nom de la localité +* La liste des localités peut être filtrée (plus de 12 éléments) +* changement des icônes d'arrêts +* les icônes sur la carte sont mises à l'échelle +* Bimba est conforme REUSE diff --git a/metadata/fr-FR/changelogs/28.txt b/metadata/fr-FR/changelogs/28.txt new file mode 100644 index 0000000000000000000000000000000000000000..cb8a27715209ea9d00bbeee41297eea86c78bd6a --- /dev/null +++ b/metadata/fr-FR/changelogs/28.txt @@ -0,0 +1,8 @@ +Changes in version 3.4 +* Added countdown to show next update of departures +* Fixed persisting filters and bottom sheets across screen changes +* Changed layout of alerts above departures +* Fixed phantom search results +* Fixed edge-to-edge and landscape layouts +* Added simple favorites +* Added French translation (by ArnaudDvs) diff --git a/metadata/fr-FR/full_description.txt b/metadata/fr-FR/full_description.txt new file mode 100644 index 0000000000000000000000000000000000000000..eff6644da8451bb0b3e02f91d14b41d468b746c6 --- /dev/null +++ b/metadata/fr-FR/full_description.txt @@ -0,0 +1,9 @@ +Bimba vous permet de consulter les horaires des transports publics, avec départs en temps réels dans plusieurs villes. + +Caractéristiques : +* consulter les départs par arrêts, +* chercher les arrêts à proximité, +* chercher des arrêts avec un code QR (dans les horaires sélectionnés), +* chercher les arrêts sur une ligne, +* caractéristiques des véhicules (en fonction des informations disponibles), +* véhicules et arrêts sur la carte en temps réel, diff --git a/metadata/fr-FR/short_description.txt b/metadata/fr-FR/short_description.txt new file mode 100644 index 0000000000000000000000000000000000000000..9d264b9a83281e1cd943525e6aa9e0e0096768ea --- /dev/null +++ b/metadata/fr-FR/short_description.txt @@ -0,0 +1 @@ +Assistant de transports publics libre ; un horaire dans votre poche. diff --git a/metadata/fr-FR/title.txt b/metadata/fr-FR/title.txt new file mode 100644 index 0000000000000000000000000000000000000000..038d5e6047bc863c8fd2210be761c396b834e447 --- /dev/null +++ b/metadata/fr-FR/title.txt @@ -0,0 +1 @@ +Bimba diff --git a/metadata/pl-PL/changelogs/28.txt b/metadata/pl-PL/changelogs/28.txt new file mode 100644 index 0000000000000000000000000000000000000000..13d42a664af2a78825953c59dfc204b945f0a680 --- /dev/null +++ b/metadata/pl-PL/changelogs/28.txt @@ -0,0 +1,8 @@ +Zmiany w wersji 3.4 +* Dodano odliczanie do następnej aktualizacji odjazdów +* Naprawiono zachowywanie filtrów bottom sheets pomiędzy zmianami orientacji +* Zmieniono układ alertów nad odjazdami +* Naprawiono fantomowe wyniki wyszukiwania +* Naprawiono układy horyzontalne i krawędź-do-krawędzi +* Dodano proste ulubione przystanki +* Dodano francuskie tłumaczenie (ArnaudDvs) diff --git a/metadata/pl-PL/images/phoneScreenshots/dash_w_fav.png b/metadata/pl-PL/images/phoneScreenshots/dash_w_fav.png new file mode 100644 index 0000000000000000000000000000000000000000..f774583c27d590632b3d8b77bde9c88907d163b6 Binary files /dev/null and b/metadata/pl-PL/images/phoneScreenshots/dash_w_fav.png differ diff --git a/release.sh b/release.sh index 76d0c0530391fe1a3d0cb63b0d404cd4e6135e95..2929171a1ce309602ad7733a1b8914e677fa95fe 100755 --- a/release.sh +++ b/release.sh @@ -102,7 +102,7 @@ exit 1 fi git add app/build.gradle git add metadata/ - git commit -S -m "release version $newVersionName ($newVersionCode)" + git commit -S -m "release version $newVersionName ($newVersionCode)" || true echo 'pushing …' git push git switch master