address review: keep per-tab search visible and clearable, fix direction arrow, guard scroll reset, harden date parsing

This commit is contained in:
austinried
2026-10-04 00:29:56 +00:00
parent fc3beaee11
commit d55406c85e
5 changed files with 30 additions and 20 deletions
@@ -134,15 +134,20 @@ private fun Element.dateAttr(name: String): Long? = getAttribute(name).takeIf {
internal object IsoDate { internal object IsoDate {
private val timezone = Regex("(Z|[+-]\\d{2}:?\\d{2})$", RegexOption.IGNORE_CASE) private val timezone = Regex("(Z|[+-]\\d{2}:?\\d{2})$", RegexOption.IGNORE_CASE)
private val longFraction = Regex("\\.(\\d{9})\\d+")
fun parse(value: String): Long? { fun parse(value: String): Long? {
val text = value.trim().let { if (it.endsWith("z")) it.dropLast(1) + "Z" else it } val text =
value
.trim()
.let { if (it.endsWith("z")) it.dropLast(1) + "Z" else it }
.replace(longFraction) { ".${it.groupValues[1]}" }
return try { return try {
val zoned = if (timezone.containsMatchIn(text)) text else text + "Z" val zoned = if (timezone.containsMatchIn(text)) text else text + "Z"
OffsetDateTime.parse(zoned).toEpochSecond() OffsetDateTime.parse(zoned).toEpochSecond()
} catch (_: DateTimeParseException) { } catch (_: DateTimeParseException) {
try { try {
LocalDate.parse(text).atStartOfDay(ZoneOffset.UTC).toEpochSecond() LocalDate.parse(text.removeSuffix("Z")).atStartOfDay(ZoneOffset.UTC).toEpochSecond()
} catch (_: DateTimeParseException) { } catch (_: DateTimeParseException) {
null null
} }
@@ -22,7 +22,6 @@ import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.consumeWindowInsets import androidx.compose.foundation.layout.consumeWindowInsets
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.ime
import androidx.compose.foundation.layout.imeAnimationTarget import androidx.compose.foundation.layout.imeAnimationTarget
import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.navigationBars import androidx.compose.foundation.layout.navigationBars
@@ -30,7 +30,10 @@ fun ResetScrollOnChange(
onReset: suspend () -> Unit, onReset: suspend () -> Unit,
) { ) {
var pending by remember { mutableStateOf(false) } var pending by remember { mutableStateOf(false) }
LaunchedEffect(resetKey) { pending = true } var primed by remember { mutableStateOf(false) }
LaunchedEffect(resetKey) {
if (primed) pending = true else primed = true
}
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
var refreshes = 0 var refreshes = 0
var handled = 0 var handled = 0
@@ -14,7 +14,6 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
@@ -115,6 +114,7 @@ enum class LibraryTab(
data class SortOption( data class SortOption(
val value: String, val value: String,
val label: String, val label: String,
val descendingByDefault: Boolean = false,
) )
fun LibraryTab.listTab(): LibraryListTab = LibraryListTab.valueOf(name) fun LibraryTab.listTab(): LibraryListTab = LibraryListTab.valueOf(name)
@@ -125,25 +125,25 @@ fun sortOptionsFor(tab: LibraryTab): List<SortOption> =
listOf( listOf(
SortOption(AlbumSort.Name.name, "Name"), SortOption(AlbumSort.Name.name, "Name"),
SortOption(AlbumSort.Artist.name, "Artist"), SortOption(AlbumSort.Artist.name, "Artist"),
SortOption(AlbumSort.Year.name, "Year"), SortOption(AlbumSort.Year.name, "Year", descendingByDefault = true),
SortOption(AlbumSort.Added.name, "Added"), SortOption(AlbumSort.Added.name, "Added", descendingByDefault = true),
SortOption(AlbumSort.Starred.name, "Starred"), SortOption(AlbumSort.Starred.name, "Starred", descendingByDefault = true),
) )
} }
LibraryTab.Artists -> { LibraryTab.Artists -> {
listOf( listOf(
SortOption(ArtistSort.Name.name, "Name"), SortOption(ArtistSort.Name.name, "Name"),
SortOption(ArtistSort.AlbumCount.name, "Albums"), SortOption(ArtistSort.AlbumCount.name, "Albums", descendingByDefault = true),
SortOption(ArtistSort.Starred.name, "Starred"), SortOption(ArtistSort.Starred.name, "Starred", descendingByDefault = true),
) )
} }
LibraryTab.Playlists -> { LibraryTab.Playlists -> {
listOf( listOf(
SortOption(PlaylistSort.Name.name, "Name"), SortOption(PlaylistSort.Name.name, "Name"),
SortOption(PlaylistSort.Added.name, "Added"), SortOption(PlaylistSort.Added.name, "Added", descendingByDefault = true),
SortOption(PlaylistSort.Updated.name, "Updated"), SortOption(PlaylistSort.Updated.name, "Updated", descendingByDefault = true),
) )
} }
@@ -152,8 +152,8 @@ fun sortOptionsFor(tab: LibraryTab): List<SortOption> =
SortOption(SongSort.Album.name, "Album"), SortOption(SongSort.Album.name, "Album"),
SortOption(SongSort.Title.name, "Title"), SortOption(SongSort.Title.name, "Title"),
SortOption(SongSort.Artist.name, "Artist"), SortOption(SongSort.Artist.name, "Artist"),
SortOption(SongSort.Starred.name, "Starred"), SortOption(SongSort.Starred.name, "Starred", descendingByDefault = true),
SortOption(SongSort.Added.name, "Added"), SortOption(SongSort.Added.name, "Added", descendingByDefault = true),
) )
} }
} }
@@ -271,7 +271,7 @@ fun LibraryScreen(
if (pagerState.currentPage != selectedTab.ordinal) { if (pagerState.currentPage != selectedTab.ordinal) {
pagerState.animateScrollToPage(selectedTab.ordinal) pagerState.animateScrollToPage(selectedTab.ordinal)
} }
searchActive = false searchActive = search.isNotEmpty()
} }
val density = LocalDensity.current val density = LocalDensity.current
@@ -418,7 +418,7 @@ fun LibraryScreen(
) { ) {
Box { Box {
Icon(Icons.Filled.Sort, contentDescription = "List options") Icon(Icons.Filled.Sort, contentDescription = "List options")
if (listQuery.starred != StarredFilter.Any) { if (filtersActive) {
Box( Box(
modifier = modifier =
Modifier Modifier
@@ -440,6 +440,7 @@ fun LibraryScreen(
listQuery = listQuery, listQuery = listQuery,
sortOptions = sortOptions, sortOptions = sortOptions,
starredSupported = starredSupported, starredSupported = starredSupported,
canClearFilters = filtersActive,
onSortChange = onSortChange, onSortChange = onSortChange,
onToggleSortDirection = onToggleSortDirection, onToggleSortDirection = onToggleSortDirection,
onCycleStarred = onCycleStarred, onCycleStarred = onCycleStarred,
@@ -459,6 +460,7 @@ private fun ListOptionsSheet(
listQuery: ListQuery, listQuery: ListQuery,
sortOptions: List<SortOption>, sortOptions: List<SortOption>,
starredSupported: Boolean, starredSupported: Boolean,
canClearFilters: Boolean,
onSortChange: (String) -> Unit, onSortChange: (String) -> Unit,
onToggleSortDirection: () -> Unit, onToggleSortDirection: () -> Unit,
onCycleStarred: () -> Unit, onCycleStarred: () -> Unit,
@@ -466,6 +468,8 @@ private fun ListOptionsSheet(
onSearch: () -> Unit, onSearch: () -> Unit,
) { ) {
Column(Modifier.fillMaxWidth().padding(bottom = 24.dp)) { Column(Modifier.fillMaxWidth().padding(bottom = 24.dp)) {
val naturalDescending = sortOptions.firstOrNull { it.value == listQuery.sort }?.descendingByDefault == true
val descending = if (naturalDescending) !listQuery.descending else listQuery.descending
Surface( Surface(
onClick = onSearch, onClick = onSearch,
shape = RoundedCornerShape(4.dp), shape = RoundedCornerShape(4.dp),
@@ -495,8 +499,8 @@ private fun ListOptionsSheet(
) )
IconButton(onClick = onToggleSortDirection) { IconButton(onClick = onToggleSortDirection) {
Icon( Icon(
imageVector = if (listQuery.descending) Icons.Rounded.ArrowDownward else Icons.Rounded.ArrowUpward, imageVector = if (descending) Icons.Rounded.ArrowDownward else Icons.Rounded.ArrowUpward,
contentDescription = if (listQuery.descending) "Sort descending" else "Sort ascending", contentDescription = if (descending) "Sort descending" else "Sort ascending",
) )
} }
} }
@@ -528,7 +532,7 @@ private fun ListOptionsSheet(
) )
IconButton( IconButton(
onClick = onClearFilters, onClick = onClearFilters,
enabled = listQuery.starred != StarredFilter.Any, enabled = canClearFilters,
) { ) {
Icon(Icons.Rounded.FilterAltOff, contentDescription = "Clear filters") Icon(Icons.Rounded.FilterAltOff, contentDescription = "Clear filters")
} }
@@ -2,7 +2,6 @@ package com.subtracks.ui
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.test.junit4.v2.createAndroidComposeRule import androidx.compose.ui.test.junit4.v2.createAndroidComposeRule
import androidx.compose.ui.test.onAllNodesWithTag import androidx.compose.ui.test.onAllNodesWithTag
import androidx.compose.ui.test.onNodeWithContentDescription import androidx.compose.ui.test.onNodeWithContentDescription