Added year filter
This commit is contained in:
parent
889166c88a
commit
40ca9b7090
2 changed files with 86 additions and 7 deletions
|
|
@ -82,16 +82,21 @@ fun RecordingListScreen(
|
||||||
val sortMode by vm.sortMode.collectAsStateWithLifecycle()
|
val sortMode by vm.sortMode.collectAsStateWithLifecycle()
|
||||||
val hiddenArtists by vm.hiddenArtists.collectAsStateWithLifecycle()
|
val hiddenArtists by vm.hiddenArtists.collectAsStateWithLifecycle()
|
||||||
val availableArtists by vm.availableArtists.collectAsStateWithLifecycle()
|
val availableArtists by vm.availableArtists.collectAsStateWithLifecycle()
|
||||||
|
val hiddenYears by vm.hiddenYears.collectAsStateWithLifecycle()
|
||||||
|
val availableYears by vm.availableYears.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
var showControls by rememberSaveable { mutableStateOf(false) }
|
var showControls by rememberSaveable { mutableStateOf(false) }
|
||||||
var showSortMenu by remember { mutableStateOf(false) }
|
var showSortMenu by remember { mutableStateOf(false) }
|
||||||
var showArtistFilter by rememberSaveable { mutableStateOf(false) }
|
var showArtistFilter by rememberSaveable { mutableStateOf(false) }
|
||||||
|
var showYearFilter by rememberSaveable { mutableStateOf(false) }
|
||||||
|
|
||||||
val listState = rememberLazyListState()
|
val listState = rememberLazyListState()
|
||||||
|
|
||||||
// Bug fix: when the sort mode changes, jump back to the TOP of the list
|
// Jump back to the TOP of the list whenever the ordering or the active
|
||||||
// instead of the LazyColumn trying (and failing) to preserve scroll position.
|
// filters change — otherwise the LazyColumn keeps its old scroll offset and
|
||||||
LaunchedEffect(sortMode) {
|
// can end up stranded (e.g. mid-list or at the bottom) after the set of
|
||||||
|
// visible items shrinks then grows again.
|
||||||
|
LaunchedEffect(sortMode, hiddenArtists, hiddenYears) {
|
||||||
listState.scrollToItem(0)
|
listState.scrollToItem(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -145,7 +150,7 @@ fun RecordingListScreen(
|
||||||
}
|
}
|
||||||
|
|
||||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
|
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
|
||||||
val hasActiveFilter = searchQuery.isNotEmpty() || hiddenArtists.isNotEmpty()
|
val hasActiveFilter = searchQuery.isNotEmpty() || hiddenArtists.isNotEmpty() || hiddenYears.isNotEmpty()
|
||||||
|
|
||||||
// The recording matching the active track, if any — used by the mini-player.
|
// The recording matching the active track, if any — used by the mini-player.
|
||||||
// We look in the full unfiltered list so the bar stays even if a filter would
|
// We look in the full unfiltered list so the bar stays even if a filter would
|
||||||
|
|
@ -302,6 +307,14 @@ fun RecordingListScreen(
|
||||||
style = MaterialTheme.typography.labelMedium
|
style = MaterialTheme.typography.labelMedium
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TextButton(onClick = { showYearFilter = !showYearFilter }) {
|
||||||
|
Text(
|
||||||
|
if (hiddenYears.isEmpty()) "Years"
|
||||||
|
else "Years (${hiddenYears.size} hidden)",
|
||||||
|
style = MaterialTheme.typography.labelMedium
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
AnimatedVisibility(
|
AnimatedVisibility(
|
||||||
|
|
@ -333,6 +346,35 @@ fun RecordingListScreen(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
AnimatedVisibility(
|
||||||
|
visible = showYearFilter,
|
||||||
|
enter = expandVertically(),
|
||||||
|
exit = shrinkVertically()
|
||||||
|
) {
|
||||||
|
Column(modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp)) {
|
||||||
|
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||||
|
TextButton(onClick = { vm.showAllYears() }) {
|
||||||
|
Text("All", style = MaterialTheme.typography.labelSmall)
|
||||||
|
}
|
||||||
|
TextButton(onClick = { vm.hideAllYears() }) {
|
||||||
|
Text("None", style = MaterialTheme.typography.labelSmall)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
FlowRow(
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(6.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(2.dp)
|
||||||
|
) {
|
||||||
|
availableYears.forEach { year ->
|
||||||
|
FilterChip(
|
||||||
|
selected = year !in hiddenYears,
|
||||||
|
onClick = { vm.toggleYear(year) },
|
||||||
|
label = { Text(year.toString(), style = MaterialTheme.typography.labelSmall) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
HorizontalDivider(modifier = Modifier.padding(top = 8.dp))
|
HorizontalDivider(modifier = Modifier.padding(top = 8.dp))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,10 +3,10 @@ package com.keylightpiano.pianotimeline.ui
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import androidx.lifecycle.AndroidViewModel
|
import androidx.lifecycle.AndroidViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import com.keylightpiano.pianotimeline.data.DeepLinkParser
|
||||||
import com.keylightpiano.pianotimeline.data.MusicRepository
|
import com.keylightpiano.pianotimeline.data.MusicRepository
|
||||||
import com.keylightpiano.pianotimeline.data.SettingsStore
|
import com.keylightpiano.pianotimeline.data.SettingsStore
|
||||||
import com.keylightpiano.pianotimeline.data.SyncManager
|
import com.keylightpiano.pianotimeline.data.SyncManager
|
||||||
import com.keylightpiano.pianotimeline.data.DeepLinkParser
|
|
||||||
import com.keylightpiano.pianotimeline.domain.Recording
|
import com.keylightpiano.pianotimeline.domain.Recording
|
||||||
import com.keylightpiano.pianotimeline.player.PlaybackState
|
import com.keylightpiano.pianotimeline.player.PlaybackState
|
||||||
import com.keylightpiano.pianotimeline.player.PlayerManager
|
import com.keylightpiano.pianotimeline.player.PlayerManager
|
||||||
|
|
@ -106,6 +106,12 @@ class RecordingListViewModel(app: Application) : AndroidViewModel(app) {
|
||||||
private val _hiddenArtists = MutableStateFlow<Set<String>>(emptySet())
|
private val _hiddenArtists = MutableStateFlow<Set<String>>(emptySet())
|
||||||
val hiddenArtists: StateFlow<Set<String>> = _hiddenArtists.asStateFlow()
|
val hiddenArtists: StateFlow<Set<String>> = _hiddenArtists.asStateFlow()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set of HIDDEN years. Empty = every year visible. Same pattern as artists.
|
||||||
|
*/
|
||||||
|
private val _hiddenYears = MutableStateFlow<Set<Int>>(emptySet())
|
||||||
|
val hiddenYears: StateFlow<Set<Int>> = _hiddenYears.asStateFlow()
|
||||||
|
|
||||||
// ── Derived: all unique grouping artists, for the filter UI ────────
|
// ── Derived: all unique grouping artists, for the filter UI ────────
|
||||||
|
|
||||||
/** All recordings straight from Room (unfiltered). */
|
/** All recordings straight from Room (unfiltered). */
|
||||||
|
|
@ -123,15 +129,27 @@ class RecordingListViewModel(app: Application) : AndroidViewModel(app) {
|
||||||
.sorted()
|
.sorted()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Distinct years present in the library, newest first (matches the default sort).
|
||||||
|
*/
|
||||||
|
val availableYears: StateFlow<List<Int>> = allRecordings
|
||||||
|
.mapToState(viewModelScope) { recordings ->
|
||||||
|
recordings.map { it.date.year }
|
||||||
|
.distinct()
|
||||||
|
.sortedDescending()
|
||||||
|
}
|
||||||
|
|
||||||
// ── The final filtered + sorted list the UI renders ────────────────
|
// ── The final filtered + sorted list the UI renders ────────────────
|
||||||
|
|
||||||
val recordings: StateFlow<List<Recording>> = combine(
|
val recordings: StateFlow<List<Recording>> = combine(
|
||||||
allRecordings,
|
allRecordings,
|
||||||
_searchQuery,
|
_searchQuery,
|
||||||
_sortMode,
|
_sortMode,
|
||||||
_hiddenArtists
|
_hiddenArtists,
|
||||||
) { all, query, sort, hidden ->
|
_hiddenYears
|
||||||
|
) { all, query, sort, hidden, hiddenYears ->
|
||||||
all.filterByArtist(hidden)
|
all.filterByArtist(hidden)
|
||||||
|
.filterByYear(hiddenYears)
|
||||||
.filterBySearch(query)
|
.filterBySearch(query)
|
||||||
.sortedWith(sort.comparator())
|
.sortedWith(sort.comparator())
|
||||||
// Favorites float to the top, preserving the sort order within each group.
|
// Favorites float to the top, preserving the sort order within each group.
|
||||||
|
|
@ -198,6 +216,20 @@ class RecordingListViewModel(app: Application) : AndroidViewModel(app) {
|
||||||
_hiddenArtists.value = availableArtists.value.toSet()
|
_hiddenArtists.value = availableArtists.value.toSet()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun toggleYear(year: Int) {
|
||||||
|
_hiddenYears.value = _hiddenYears.value.let { current ->
|
||||||
|
if (year in current) current - year else current + year
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun showAllYears() {
|
||||||
|
_hiddenYears.value = emptySet()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun hideAllYears() {
|
||||||
|
_hiddenYears.value = availableYears.value.toSet()
|
||||||
|
}
|
||||||
|
|
||||||
fun toggleFavorite(recording: Recording) {
|
fun toggleFavorite(recording: Recording) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
repo.setFavorite(recording.id, !recording.isFavorite)
|
repo.setFavorite(recording.id, !recording.isFavorite)
|
||||||
|
|
@ -512,6 +544,11 @@ private fun List<Recording>.filterByArtist(hidden: Set<String>): List<Recording>
|
||||||
return filter { it.groupingArtist !in hidden }
|
return filter { it.groupingArtist !in hidden }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun List<Recording>.filterByYear(hidden: Set<Int>): List<Recording> {
|
||||||
|
if (hidden.isEmpty()) return this
|
||||||
|
return filter { it.date.year !in hidden }
|
||||||
|
}
|
||||||
|
|
||||||
private fun SortMode.comparator(): Comparator<Recording> = when (this) {
|
private fun SortMode.comparator(): Comparator<Recording> = when (this) {
|
||||||
SortMode.DATE_DESC -> compareByDescending<Recording> { it.date }
|
SortMode.DATE_DESC -> compareByDescending<Recording> { it.date }
|
||||||
.thenByDescending { it.performanceNumber }
|
.thenByDescending { it.performanceNumber }
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue