Author: Adam Evyčędo <git@apiote.xyz>
support offline feed settings
%!v(PANIC=String method: strings: negative Repeat count)
diff --git a/app/build.gradle b/app/build.gradle index f383e96e7700b05f0146a3eb1a5d314d37c9a1a5..87a703a70ac8b7d7748fc9a3266cba2be6a994fa 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -61,7 +61,7 @@ implementation 'androidx.navigation:navigation-ui-ktx:2.7.7' implementation 'androidx.legacy:legacy-support-v4:1.0.0' implementation 'androidx.core:core-splashscreen:1.0.1' implementation 'com.google.openlocationcode:openlocationcode:1.0.4' - implementation 'org.osmdroid:osmdroid-android:6.1.16' + implementation 'org.osmdroid:osmdroid-android:6.1.18' implementation 'org.yaml:snakeyaml:2.2' implementation 'androidx.activity:activity-ktx:1.8.2' implementation 'com.google.openlocationcode:openlocationcode:1.0.4' diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/repo/FeedInfo.kt b/app/src/main/java/xyz/apiote/bimba/czwek/repo/FeedInfo.kt index 8f9260fce714586c54352e6d9d1b61603ad996e1..abf0ba7c8cb21b07c003e68828ba78ab11ef6e3b 100644 --- a/app/src/main/java/xyz/apiote/bimba/czwek/repo/FeedInfo.kt +++ b/app/src/main/java/xyz/apiote/bimba/czwek/repo/FeedInfo.kt @@ -20,8 +20,8 @@ val lastUpdate: LocalDate, val qrHost: String, val qrIn: QrLocation, val qrSelector: String, - val validSince: LocalDate, - val validTill: LocalDate, + val validSince: LocalDate?, + val validTill: LocalDate?, val cached: Boolean ) { constructor(f: FeedInfoV2, cached: Boolean = false) : this( @@ -47,8 +47,8 @@ f.lastUpdate.toLocalDate(), "", QrLocation.UNKNOWN, "", - LocalDate.MIN, - LocalDate.MIN, + null, + null, cached ) @@ -74,8 +74,8 @@ if (lastUpdate.isAfter(other.lastUpdate)) lastUpdate else other.lastUpdate, other.qrHost, other.qrIn, other.qrSelector, - if (validSince.isAfter(other.validSince)) validSince else other.validSince, - if (validTill.isAfter(other.validTill)) validTill else other.validTill, + if (validSince == null || validSince.isAfter(other.validSince)) validSince else other.validSince, + if (validTill == null || validTill.isAfter(other.validTill)) validTill else other.validTill, this.cached && other.cached ) } 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 f5c784bd6ea8733475970f88cd3cd38e803c3ad7..750cf0d4bc662994915fad370e5db855265362c8 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 @@ -30,9 +30,26 @@ return emptyMap() } return when (val response = withContext(Dispatchers.IO) { FeedsResponse.unmarshal(FileInputStream(file)) }) { - is FeedsResponseDev -> response.feeds.associate { Pair(it.id, FeedInfo(it)) } - is FeedsResponseV2 -> response.feeds.associate { Pair(it.id, FeedInfo(it)) } - is FeedsResponseV1 -> response.feeds.associate { Pair(it.id, FeedInfo(it)) } + is FeedsResponseDev -> response.feeds.associate { + Pair( + it.id, + FeedInfo(it).copy(cached = true) + ) + } + + is FeedsResponseV2 -> response.feeds.associate { + Pair( + it.id, + FeedInfo(it).copy(cached = true) + ) + } + + is FeedsResponseV1 -> response.feeds.associate { + Pair( + it.id, + FeedInfo(it).copy(cached = true) + ) + } else -> 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 82ea79773bd1938aa7eec2658d0f5c613e79b7c7..2642ac78ca1681506a386eba7c3b4d7a86bea3ab 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 @@ -24,6 +24,9 @@ import xyz.apiote.bimba.czwek.repo.OnlineRepository import xyz.apiote.bimba.czwek.repo.TrafficResponseException import xyz.apiote.bimba.czwek.repo.join +// TODO on internet connection -> getServer +// TODO swipe to refresh? + class FeedChooserActivity : AppCompatActivity() { private var _binding: ActivityFeedChooserBinding? = null private val binding get() = _binding!! @@ -31,7 +34,7 @@ private lateinit var adapter: BimbaFeedInfoAdapter private val feeds: MutableMap<String, FeedInfo> = mutableMapOf() - private val feedsSettings = FeedsSettings.load(this, Server.get(this).apiPath) + private lateinit var feedsSettings: FeedsSettings override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -39,6 +42,7 @@ _binding = ActivityFeedChooserBinding.inflate(layoutInflater) setContentView(binding.root) migrateFeedsSettings(this) + feedsSettings = FeedsSettings.load(this, Server.get(this).apiPath) setUpRecycler() getServer() @@ -55,6 +59,7 @@ BimbaFeedInfoAdapter( layoutInflater, feeds.map { it.value }.sortedBy { it.name }, feedsSettings, + this, { feedID -> FeedBottomSheet(feeds[feedID]!!, feedsSettings.settings[feedID]) { if (it != null) { @@ -71,7 +76,6 @@ enabled = false, useOnline = true ) updateItems(null, feedsSettings, false) - adapter.notifyItemChanged(adapter.getItemPosition(feedID)) }) binding.resultsRecycler.adapter = adapter } @@ -79,21 +83,24 @@ private fun getServer() { binding.progress.visibility = View.VISIBLE binding.resultsRecycler.visibility = View.GONE - - migrateFeedsSettings(this) MainScope().launch { val offlineRepository = OfflineRepository() val offlineFeeds = offlineRepository.getFeeds(Server.get(this@FeedChooserActivity), this@FeedChooserActivity) if (!offlineFeeds.isNullOrEmpty()) { + feeds.putAll(offlineFeeds) updateItems(offlineFeeds.map { it.value }, null) } try { val repository = OnlineRepository() val onlineFeeds = repository.getFeeds(Server.get(this@FeedChooserActivity), this@FeedChooserActivity) - updateItems(joinFeeds(offlineFeeds, onlineFeeds).map { it.value }, null) + feeds.clear() + joinFeeds(offlineFeeds, onlineFeeds).let { joinedFeeds -> + feeds.putAll(joinedFeeds) + updateItems(joinedFeeds.map { it.value }, null) + } } catch (e: TrafficResponseException) { if (offlineFeeds.isNullOrEmpty()) { showError(e.error.imageResource, e.error.stringResource) @@ -157,7 +164,7 @@ binding.feedsOverlay.visibility = View.GONE binding.resultsRecycler.visibility = View.VISIBLE binding.button.visibility = View.VISIBLE adapter.update(feeds, feedsSettings, notify) - if (feeds.isNullOrEmpty()) { + if (feeds?.isEmpty() == true) { showError(R.drawable.error_search, R.string.error_404) } } diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/settings/feeds/FeedInfos.kt b/app/src/main/java/xyz/apiote/bimba/czwek/settings/feeds/FeedInfos.kt index 4c5f8b8b5d9a5ed2ace5fc9888517b2435867db2..0ba9ada8523ff57245310e2197ac5d2476485d1f 100644 --- a/app/src/main/java/xyz/apiote/bimba/czwek/settings/feeds/FeedInfos.kt +++ b/app/src/main/java/xyz/apiote/bimba/czwek/settings/feeds/FeedInfos.kt @@ -5,6 +5,7 @@ package xyz.apiote.bimba.czwek.settings.feeds import android.annotation.SuppressLint +import android.content.Context import android.content.DialogInterface import android.os.Bundle import android.view.LayoutInflater @@ -15,8 +16,10 @@ import androidx.recyclerview.widget.RecyclerView import com.google.android.material.bottomsheet.BottomSheetDialogFragment import com.google.android.material.divider.MaterialDivider import com.google.android.material.materialswitch.MaterialSwitch +import com.google.android.material.textview.MaterialTextView import xyz.apiote.bimba.czwek.R import xyz.apiote.bimba.czwek.repo.FeedInfo +import java.time.format.DateTimeFormatter class BimbaFeedInfoViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { @@ -30,10 +33,20 @@ fun bind( feed: FeedInfo, feedSettings: FeedSettings?, holder: BimbaFeedInfoViewHolder?, + context: Context, onClickListener: (String) -> Unit, onCheckedChangeListener: (String, Boolean) -> Unit ) { - // TODO update bottom sheet + val colorAttr = if (feed.cached) { + com.google.android.material.R.attr.colorOnSurfaceVariant + } else { + com.google.android.material.R.attr.colorOnSurface + } + holder?.name?.setTextColor( + context.theme.obtainStyledAttributes( + R.style.Theme_Bimba, intArrayOf(colorAttr) + ).getColor(0, 0) + ) holder?.root?.setOnClickListener { onClickListener(feed.id) } @@ -56,6 +69,7 @@ class BimbaFeedInfoAdapter( private val inflater: LayoutInflater, private var feeds: List<FeedInfo>, private var feedsSettings: FeedsSettings, + private val context: Context, private val onClickListener: ((String) -> Unit), private val onEnabledChangedListener: ((String, Boolean) -> Unit) ) : @@ -71,6 +85,7 @@ BimbaFeedInfoViewHolder.bind( feed, feedsSettings.settings[feed.id], holder, + context, onClickListener, onEnabledChangedListener ) @@ -98,7 +113,7 @@ } class FeedBottomSheet( private var feed: FeedInfo, - private val settings: FeedSettings?, + private var settings: FeedSettings?, private val onDismiss: (FeedSettings?) -> Unit ) : BottomSheetDialogFragment() { @@ -112,11 +127,36 @@ container: ViewGroup?, savedInstanceState: Bundle? ): View { val content = inflater.inflate(R.layout.feed_bottom_sheet, container, false) - content.findViewById<TextView>(R.id.title).text = feed.name - content.findViewById<TextView>(R.id.description).text = feed.description - content.findViewById<TextView>(R.id.attribution).text = feed.attribution - content.findViewById<TextView>(R.id.update_time).text = + content.findViewById<MaterialTextView>(R.id.title).text = feed.name + content.findViewById<MaterialTextView>(R.id.description).text = feed.description + content.findViewById<MaterialTextView>(R.id.outdated_info_warning).visibility = + if (feed.cached) { + View.VISIBLE + } else { + View.GONE + } + if (feed.validSince != null && feed.validTill != null) { + content.findViewById<MaterialTextView>(R.id.timetable_validity).apply { + visibility = View.VISIBLE + text = getString( + R.string.current_timetable_validity, + feed.validSince!!.format(DateTimeFormatter.ISO_LOCAL_DATE), + feed.validTill!!.format(DateTimeFormatter.ISO_LOCAL_DATE) + ) + } + } + content.findViewById<MaterialTextView>(R.id.attribution).text = feed.attribution + content.findViewById<MaterialTextView>(R.id.update_time).text = getString(R.string.last_update, feed.formatDate()) + content.findViewById<MaterialSwitch>(R.id.onlineSwitch).apply { + isChecked = settings?.useOnline ?: false + setOnCheckedChangeListener { _, isChecked -> + settings = settings?.copy(useOnline = isChecked) ?: FeedSettings( + enabled = true, + useOnline = isChecked + ) + } + } return content } diff --git a/app/src/main/res/layout/feed_bottom_sheet.xml b/app/src/main/res/layout/feed_bottom_sheet.xml index c1822e07f849202c3f3bb1674337293a4c8e75d8..bcb47b534c804342efbcdece3b2dee15fa746216 100644 --- a/app/src/main/res/layout/feed_bottom_sheet.xml +++ b/app/src/main/res/layout/feed_bottom_sheet.xml @@ -33,32 +33,91 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" tool:text="Poznań ZTM" /> + <com.google.android.material.materialswitch.MaterialSwitch + android:id="@+id/onlineSwitch" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginEnd="16dp" + app:layout_constraintBottom_toBottomOf="@+id/onlineSwitchLabel" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintTop_toTopOf="@+id/onlineSwitchLabel" /> + + <com.google.android.material.textview.MaterialTextView + android:id="@+id/onlineSwitchLabel" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginStart="16dp" + android:layout_marginTop="16dp" + android:layout_marginEnd="8dp" + android:labelFor="@id/onlineSwitch" + android:text="@string/use_online_feed" + android:textAppearance="@style/TextAppearance.Material3.HeadlineMedium" + app:layout_constraintEnd_toStartOf="@+id/onlineSwitch" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/title" /> + + <com.google.android.material.divider.MaterialDivider + android:id="@+id/onlineOfflineDivider" + android:layout_width="match_parent" + android:layout_height="wrap_content" + app:dividerInsetEnd="16dp" + app:dividerInsetStart="16dp" + android:layout_marginTop="16dp" + app:layout_constraintTop_toBottomOf="@+id/onlineSwitch" /> + + <com.google.android.material.textview.MaterialTextView + android:id="@+id/outdated_info_warning" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginTop="8dp" + android:text="@string/information_may_be_outdated" + android:textStyle="italic" + android:textAppearance="@style/TextAppearance.Material3.BodySmall" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/onlineOfflineDivider" /> + <com.google.android.material.textview.MaterialTextView android:id="@+id/description" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_marginStart="8dp" - android:layout_marginTop="16dp" android:layout_marginEnd="8dp" + android:layout_marginTop="16dp" android:textAlignment="center" android:textAppearance="@style/TextAppearance.Material3.BodyLarge" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@+id/title" + app:layout_constraintTop_toBottomOf="@+id/outdated_info_warning" tool:text="Feed for Poznań" /> <com.google.android.material.textview.MaterialTextView + android:id="@+id/timetable_validity" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginStart="8dp" + android:textAlignment="center" + android:layout_marginEnd="8dp" + android:visibility="gone" + android:layout_marginTop="16dp" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + android:textAppearance="@style/TextAppearance.Material3.BodyMedium" + app:layout_constraintTop_toBottomOf="@+id/description" + tool:text="Current timetable valid: 2024-01-01 to 2024-02-01" /> + + <com.google.android.material.textview.MaterialTextView android:id="@+id/attribution" + android:layout_marginTop="16dp" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_marginStart="8dp" - android:layout_marginTop="16dp" android:layout_marginEnd="8dp" android:textAlignment="center" android:textAppearance="@style/TextAppearance.Material3.BodyMedium" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@+id/description" + app:layout_constraintTop_toBottomOf="@+id/timetable_validity" tool:text="(c) Poznań" /> <com.google.android.material.textview.MaterialTextView diff --git a/app/src/main/res/layout/feedinfo.xml b/app/src/main/res/layout/feedinfo.xml index 55a9fc293bf994e148ad829cf70cdc43789e9b72..57d1b22fb8aa55e6da9dd91c87f6d22761b9517a 100644 --- a/app/src/main/res/layout/feedinfo.xml +++ b/app/src/main/res/layout/feedinfo.xml @@ -32,7 +32,7 @@ android:id="@+id/feed_switch" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="16dp" - android:visibility="gone" + android:visibility="invisible" android:layout_marginEnd="16dp" android:layout_marginBottom="16dp" android:text="" @@ -43,7 +43,7 @@ <com.google.android.material.divider.MaterialDivider android:id="@+id/divider" android:layout_width="1dp" - android:visibility="gone" + android:visibility="invisible" android:layout_height="0dp" android:layout_marginTop="16dp" android:layout_marginEnd="8dp" diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 4ef13134bf545a274dab57a52410b86a6729f898..bb318f8b3b8560f527858737a74b1f5de2929e9d 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -107,4 +107,7 @@FLOSS public transport passenger companion; a timetable in your pocket. <string name="website_button_description">link to website</string> <string name="code_button_description">link to source code</string> <string name="mastodon_button_description">link to Mastodon</string> + <string name="use_online_feed">Use online feed</string> + <string name="information_may_be_outdated">Information may be outdated</string> + <string name="current_timetable_validity">Current timetable valid: %1$s to %2$s</string> </resources> \ No newline at end of file