Added year filter

This commit is contained in:
OmniaX-Dev 2026-06-24 00:38:04 +02:00
parent 889166c88a
commit 40ca9b7090
2 changed files with 86 additions and 7 deletions

View file

@ -82,16 +82,21 @@ fun RecordingListScreen(
val sortMode by vm.sortMode.collectAsStateWithLifecycle()
val hiddenArtists by vm.hiddenArtists.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 showSortMenu by remember { mutableStateOf(false) }
var showArtistFilter by rememberSaveable { mutableStateOf(false) }
var showYearFilter by rememberSaveable { mutableStateOf(false) }
val listState = rememberLazyListState()
// Bug fix: when the sort mode changes, jump back to the TOP of the list
// instead of the LazyColumn trying (and failing) to preserve scroll position.
LaunchedEffect(sortMode) {
// Jump back to the TOP of the list whenever the ordering or the active
// filters change — otherwise the LazyColumn keeps its old scroll offset and
// 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)
}
@ -145,7 +150,7 @@ fun RecordingListScreen(
}
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.
// 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
)
}
TextButton(onClick = { showYearFilter = !showYearFilter }) {
Text(
if (hiddenYears.isEmpty()) "Years"
else "Years (${hiddenYears.size} hidden)",
style = MaterialTheme.typography.labelMedium
)
}
}
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))
}
}

View file

@ -3,10 +3,10 @@ package com.keylightpiano.pianotimeline.ui
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope
import com.keylightpiano.pianotimeline.data.DeepLinkParser
import com.keylightpiano.pianotimeline.data.MusicRepository
import com.keylightpiano.pianotimeline.data.SettingsStore
import com.keylightpiano.pianotimeline.data.SyncManager
import com.keylightpiano.pianotimeline.data.DeepLinkParser
import com.keylightpiano.pianotimeline.domain.Recording
import com.keylightpiano.pianotimeline.player.PlaybackState
import com.keylightpiano.pianotimeline.player.PlayerManager
@ -106,6 +106,12 @@ class RecordingListViewModel(app: Application) : AndroidViewModel(app) {
private val _hiddenArtists = MutableStateFlow<Set<String>>(emptySet())
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 ────────
/** All recordings straight from Room (unfiltered). */
@ -123,15 +129,27 @@ class RecordingListViewModel(app: Application) : AndroidViewModel(app) {
.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 ────────────────
val recordings: StateFlow<List<Recording>> = combine(
allRecordings,
_searchQuery,
_sortMode,
_hiddenArtists
) { all, query, sort, hidden ->
_hiddenArtists,
_hiddenYears
) { all, query, sort, hidden, hiddenYears ->
all.filterByArtist(hidden)
.filterByYear(hiddenYears)
.filterBySearch(query)
.sortedWith(sort.comparator())
// 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()
}
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) {
viewModelScope.launch {
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 }
}
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) {
SortMode.DATE_DESC -> compareByDescending<Recording> { it.date }
.thenByDescending { it.performanceNumber }