Author: Adam <git@apiote.xyz>
select date in departures
%!v(PANIC=String method: strings: negative Repeat count)
diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/api/Api.kt b/app/src/main/java/xyz/apiote/bimba/czwek/api/Api.kt index 8a942fd52c4c7aed749d85e8dc70c1953f7bdc22..453ebf9ef8476585c8c8f3d16175c2cf33ffba36 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 @@ -17,6 +17,8 @@ import java.net.HttpURLConnection import java.net.MalformedURLException import java.net.URL import java.net.URLEncoder +import java.time.LocalDate +import java.time.format.DateTimeFormatter // todo [3.2] constants // todo [3.2] split api files to classes files @@ -126,10 +128,14 @@ cm: ConnectivityManager, server: Server, feedID: String, stop: String, + date: LocalDate?, line: String? = null, limit: Int? = null ): Result { val params = mutableMapOf("code" to stop) + if (date != null) { + params["date"] = date.format(DateTimeFormatter.ISO_LOCAL_DATE) + } if (line != null) { params["line"] = line } diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/departures/Departures.kt b/app/src/main/java/xyz/apiote/bimba/czwek/departures/Departures.kt index 04c4d0a1eab4505ce9237ecf6e031cf6c5beeb22..b1075c10180fb11455713a1a54b273f982d8872e 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 @@ -11,6 +11,7 @@ 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.os.Bundle +import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup @@ -47,6 +48,7 @@ fun bind( departure: Departure, holder: BimbaDepartureViewHolder?, context: Context?, + showAsTime: Boolean, onClickListener: (Departure) -> Unit ) { holder?.root?.setOnClickListener { @@ -63,7 +65,7 @@ R.string.departure_headsign_content_description, departure.vehicle.Headsign ) - holder?.departureTime?.text = departure.statusText(context) + holder?.departureTime?.text = departure.statusText(context, showAsTime) } } } @@ -78,6 +80,7 @@ RecyclerView.Adapter() { var lastUpdate: ZonedDateTime = ZonedDateTime.of(0, 1, 1, 0, 0, 0, 0, ZoneId.systemDefault()) private set + private var showAsTime: Boolean = false inner class DiffUtilCallback( private val oldDepartures: List<Departure>, @@ -94,7 +97,7 @@ override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { val oldDeparture = oldDepartures[oldItemPosition] val newDeparture = newDepartures[newItemPosition] return oldDeparture.vehicle.Line == newDeparture.vehicle.Line && oldDeparture.vehicle.Headsign == newDeparture.vehicle.Headsign && - oldDeparture.statusText(context, lastUpdate) == newDeparture.statusText(context) + oldDeparture.statusText(context, false, lastUpdate) == newDeparture.statusText(context, false) } } @@ -112,7 +115,8 @@ return BimbaDepartureViewHolder(rowView) } override fun onBindViewHolder(holder: BimbaDepartureViewHolder, position: Int) { - BimbaDepartureViewHolder.bind(departures[position], holder, context, onClickListener) + Log.i("show as time?", "$showAsTime") + BimbaDepartureViewHolder.bind(departures[position], holder, context, showAsTime, onClickListener) } override fun getItemCount(): Int = departures.size @@ -126,7 +130,8 @@ departures[position] } } - fun update(departures: List<Departure>, areNewObserved: Boolean = false) { + fun update(departures: List<Departure>, showAsTime: Boolean, areNewObserved: Boolean = false) { + this.showAsTime = showAsTime val newPositions: MutableMap<String, Int> = HashMap() departures.forEachIndexed { i, departure -> newPositions[departure.ID] = i @@ -142,7 +147,7 @@ diff.dispatchUpdatesTo(this) } fun refreshItems() { - update(this.departures) + update(this.departures, showAsTime) } } diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/departures/DeparturesActivity.kt b/app/src/main/java/xyz/apiote/bimba/czwek/departures/DeparturesActivity.kt index f47185c9f0a0948217cb80a2aa3dbbb6db37e310..cc2c30cb53c2df594b2e983e3b4444b136d9d8d5 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 @@ -10,7 +10,6 @@ import android.os.Handler import android.os.Looper import android.text.format.DateUtils import android.text.format.DateUtils.MINUTE_IN_MILLIS -import android.util.Log import android.view.View import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.content.res.AppCompatResources @@ -19,6 +18,7 @@ import androidx.core.view.WindowCompat import androidx.lifecycle.ViewModelProvider import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView +import com.google.android.material.datepicker.MaterialDatePicker import com.google.android.material.snackbar.Snackbar import kotlinx.coroutines.Runnable import xyz.apiote.bimba.czwek.R @@ -26,6 +26,9 @@ import xyz.apiote.bimba.czwek.api.Error import xyz.apiote.bimba.czwek.databinding.ActivityDeparturesBinding import xyz.apiote.bimba.czwek.repo.Departure import xyz.apiote.bimba.czwek.repo.Stop +import java.time.Instant +import java.time.LocalDate +import java.time.ZoneId import java.time.ZonedDateTime class DeparturesActivity : AppCompatActivity() { @@ -42,6 +45,12 @@ private lateinit var snackbar: Snackbar private lateinit var viewModel: DeparturesViewModel + private val datePicker = + MaterialDatePicker.Builder.datePicker().setTitleText("Select day of departures") + .setNegativeButtonText(R.string.clear_date_selection) + .build() + private var date: LocalDate? = null + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) _binding = ActivityDeparturesBinding.inflate(layoutInflater) @@ -57,12 +66,32 @@ viewModel.error.observe(this) { showError(it) } + datePicker.addOnNegativeButtonClickListener { + date = null + getDepartures() // TODO force clear and error + } + datePicker.addOnPositiveButtonClickListener { + date = Instant.ofEpochMilli(it).atZone(ZoneId.systemDefault()) + .toLocalDate() + getDepartures() // TODO force clear and error + } + binding.collapsingLayout.apply { title = getName() val tf = ResourcesCompat.getFont(this@DeparturesActivity, R.font.yellowcircle8) setCollapsedTitleTypeface(tf) setExpandedTitleTypeface(tf) } + binding.departuresAppBar.setOnMenuItemClickListener { + when (it.itemId) { + R.id.departures_calendar -> { + datePicker.show(supportFragmentManager, "datePicker") + true + } + + else -> super.onOptionsItemSelected(it) + } + } binding.departuresRecycler.layoutManager = LinearLayoutManager(this) binding.departuresRecycler.addOnScrollListener( @@ -121,7 +150,7 @@ fun getDepartures() { adapter.refreshItems() setupSnackbar() - viewModel.getDepartures(this, getLine()) + viewModel.getDepartures(this, getLine(), date) handler.removeCallbacks(runnable) runnable = Runnable { getDepartures() } handler.postDelayed(runnable, 30 * 1000) @@ -130,12 +159,16 @@ private fun setupSnackbar() { val lastUpdateAgo = ZonedDateTime.now().toEpochSecond() - adapter.lastUpdate.toEpochSecond() if (lastUpdateAgo > 59 && adapter.lastUpdate.year != 0) { - snackbar.setText(getString(R.string.departures_snackbar, - DateUtils.getRelativeTimeSpanString( - adapter.lastUpdate.toEpochSecond() * 1000, - ZonedDateTime.now().toEpochSecond() * 1000, - MINUTE_IN_MILLIS, - DateUtils.FORMAT_ABBREV_RELATIVE)) + snackbar.setText( + getString( + R.string.departures_snackbar, + DateUtils.getRelativeTimeSpanString( + adapter.lastUpdate.toEpochSecond() * 1000, + ZonedDateTime.now().toEpochSecond() * 1000, + MINUTE_IN_MILLIS, + DateUtils.FORMAT_ABBREV_RELATIVE + ) + ) ).show() } else { snackbar.dismiss() @@ -149,13 +182,18 @@ binding.errorImage.visibility = View.VISIBLE binding.errorText.visibility = View.VISIBLE binding.errorText.text = getString(error.stringResource) - binding.errorImage.setImageDrawable(AppCompatResources.getDrawable(this, error.imageResource)) + binding.errorImage.setImageDrawable( + AppCompatResources.getDrawable( + this, + error.imageResource + ) + ) } private fun updateItems(departures: List<Departure>, stop: Stop) { setupSnackbar() binding.departuresProgress.visibility = View.GONE - adapter.update(departures, true) + adapter.update(departures, date != null, true) binding.collapsingLayout.apply { title = stop.name } diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/departures/DeparturesViewModel.kt b/app/src/main/java/xyz/apiote/bimba/czwek/departures/DeparturesViewModel.kt index 57f6fb5bfc9505ba2262d3cfe16cf7e48345f627..e5a198517744559aa0d6cf9e64bb0231b01f4230 100644 --- a/app/src/main/java/xyz/apiote/bimba/czwek/departures/DeparturesViewModel.kt +++ b/app/src/main/java/xyz/apiote/bimba/czwek/departures/DeparturesViewModel.kt @@ -22,6 +22,7 @@ import xyz.apiote.bimba.czwek.repo.OnlineRepository import xyz.apiote.bimba.czwek.repo.QrLocation import xyz.apiote.bimba.czwek.repo.StopDepartures import xyz.apiote.bimba.czwek.repo.TrafficResponseException +import java.time.LocalDate class DeparturesViewModel : ViewModel() { private val _departures = MutableLiveData<StopDepartures>() @@ -33,7 +34,7 @@ var allItemsRequested = false private var feed: FeedInfo? = null private lateinit var code: String - fun getDepartures(context: Context, line: String?) { + fun getDepartures(context: Context, line: String?, date: LocalDate?) { MainScope().launch { try { if (feed == null) { @@ -47,11 +48,12 @@ repository.getDepartures( feed!!.id, code, line, + date, context, requestedItemsNumber ) stopDepartures?.let { - if (stopDepartures.departures.isEmpty()) { + if (stopDepartures.departures.isEmpty()) { // TODO other error for empty than not-found val (string, image) = mapHttpError(404) throw TrafficResponseException(404, "", Error(404, string, image)) } diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/repo/Departure.kt b/app/src/main/java/xyz/apiote/bimba/czwek/repo/Departure.kt index 617515c72577de45af91523cd643eb14e07a9760..4294211626b90a8acd68bf77db242f52020bd789 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 @@ -18,6 +18,7 @@ import xyz.apiote.bimba.czwek.api.UnknownResourceVersionException import java.time.Instant import java.time.ZoneId import java.time.ZonedDateTime +import java.time.format.DateTimeFormatter import java.time.temporal.ChronoUnit enum class AlertCause { @@ -119,12 +120,15 @@ Vehicle(d.vehicle), d.boarding ) - fun statusText(context: Context?, at: ZonedDateTime? = null): String { + fun statusText(context: Context?, showAsTime: Boolean, at: ZonedDateTime? = null): String { val now = at ?: Instant.now().atZone(ZoneId.systemDefault()) val departureTime = ZonedDateTime.of( now.year, now.monthValue, now.dayOfMonth, time.Hour.toInt(), time.Minute.toInt(), time.Second.toInt(), 0, ZoneId.of(time.Zone) ).plus(time.DayOffset.toLong(), ChronoUnit.DAYS) + if (showAsTime) { + return departureTime.format(DateTimeFormatter.ofPattern("HH:mm")) + } var r = status.toUInt() if (departureTime.isBefore(now) && r < 3u) { r = 0u diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/repo/Interfaces.kt b/app/src/main/java/xyz/apiote/bimba/czwek/repo/Interfaces.kt index dc8bdba932bf7327316bd331ae92791002adce35..873a343c9e4316f433134cfda163a476c1e84896 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 @@ -8,6 +8,7 @@ import android.content.Context import android.graphics.drawable.Drawable import android.net.ConnectivityManager import xyz.apiote.bimba.czwek.api.Server +import java.time.LocalDate interface Queryable interface Locatable { @@ -26,6 +27,7 @@ suspend fun getDepartures( feedID: String, stop: String, line: String?, + date: LocalDate?, context: Context, limit: Int? ): StopDepartures? diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/repo/OfflineRepository.kt b/app/src/main/java/xyz/apiote/bimba/czwek/repo/OfflineRepository.kt index 0a92c37cd563b5560cd44f559b7ff8ce787d82bb..df3306a663bfbe18efa4c1ef22d42981f00fe7bf 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 @@ -16,6 +16,7 @@ import xyz.apiote.bimba.czwek.api.responses.FeedsResponseV2 import java.io.File import java.io.FileInputStream import java.net.URLEncoder +import java.time.LocalDate class OfflineRepository : Repository { override suspend fun getFeeds( @@ -59,6 +60,7 @@ override suspend fun getDepartures( feedID: String, stop: String, line: String?, + date: LocalDate?, context: Context, limit: Int? ): StopDepartures? { diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/repo/OnlineRepository.kt b/app/src/main/java/xyz/apiote/bimba/czwek/repo/OnlineRepository.kt index e25943d37a5bb1d941a759ad6cd56a29f23b20ba..bdac1f0a6db1e288e14b923b1c6257369abcf06c 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 @@ -44,6 +44,7 @@ import xyz.apiote.bimba.czwek.api.responses.QueryablesResponseV2 import xyz.apiote.bimba.czwek.api.responses.QueryablesResponseV3 import java.io.File import java.net.URLEncoder +import java.time.LocalDate // todo [3.2] in Repository check if responses are BARE or HTML @@ -87,12 +88,13 @@ override suspend fun getDepartures( feedID: String, stop: String, line: String?, + date: LocalDate?, context: Context, limit: Int? ): StopDepartures? { val cm = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager val result = - xyz.apiote.bimba.czwek.api.getDepartures(cm, Server.get(context), feedID, stop, line, limit) + xyz.apiote.bimba.czwek.api.getDepartures(cm, Server.get(context), feedID, stop, date, line, limit) if (result.error != null) { if (result.stream != null) { val response = withContext(Dispatchers.IO) { ErrorResponse.unmarshal(result.stream) } diff --git a/app/src/main/res/drawable/calendar.xml b/app/src/main/res/drawable/calendar.xml new file mode 100644 index 0000000000000000000000000000000000000000..46e37e41f0d907e88910b18e012b37d092bf89d6 --- /dev/null +++ b/app/src/main/res/drawable/calendar.xml @@ -0,0 +1,5 @@ +<vector android:height="24dp" android:tint="?attr/colorOnSurface" + android:viewportHeight="24" android:viewportWidth="24" + android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android"> + <path android:fillColor="@android:color/white" android:pathData="M19,4h-1V2h-2v2H8V2H6v2H5C3.89,4 3.01,4.9 3.01,6L3,20c0,1.1 0.89,2 2,2h14c1.1,0 2,-0.9 2,-2V6C21,4.9 20.1,4 19,4zM19,20H5V10h14V20zM9,14H7v-2h2V14zM13,14h-2v-2h2V14zM17,14h-2v-2h2V14zM9,18H7v-2h2V18zM13,18h-2v-2h2V18zM17,18h-2v-2h2V18z"/> +</vector> diff --git a/app/src/main/res/layout/activity_departures.xml b/app/src/main/res/layout/activity_departures.xml index d71bb1c201f10214475335f515a81489fd690a3b..cb7760a48a12afb83d48beef6462b72db547422d 100644 --- a/app/src/main/res/layout/activity_departures.xml +++ b/app/src/main/res/layout/activity_departures.xml @@ -74,6 +74,7 @@ android:id="@+id/departures_app_bar" android:layout_width="match_parent" android:layout_height="?attr/actionBarSize" android:elevation="0dp" + app:menu="@menu/departures_menu" app:layout_collapseMode="pin" /> </com.google.android.material.appbar.CollapsingToolbarLayout> diff --git a/app/src/main/res/menu/departures_menu.xml b/app/src/main/res/menu/departures_menu.xml new file mode 100644 index 0000000000000000000000000000000000000000..cea228aa205e42352b56fc464f32fdf84982fd96 --- /dev/null +++ b/app/src/main/res/menu/departures_menu.xml @@ -0,0 +1,10 @@ +<?xml version="1.0" encoding="utf-8"?> +<menu xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto"> + <item + android:id="@+id/departures_calendar" + android:icon="@drawable/calendar" + app:showAsAction="always" + android:contentDescription="@string/title_select_date" + android:title="@string/title_select_date" /> +</menu> \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 8b0524c6126083cb284dacaa638ca95ba4efe2b3..dab2829c9c31bdbe31b3844513eb1d334283f95b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -115,4 +115,6 @@ filter localities <string name="error_41">This locality is not supported by the server</string> <string name="stop_from_qr_code">QR code stop</string> <string name="departures_snackbar">Last update: %1$s</string> + <string name="title_select_date">select date</string> + <string name="clear_date_selection">Clear</string> </resources> \ No newline at end of file