diff --git a/app/src/main/java/com/sosauce/chocola/data/datastore/DataStore.kt b/app/src/main/java/com/sosauce/chocola/data/datastore/DataStore.kt index b2c0326..9511de3 100644 --- a/app/src/main/java/com/sosauce/chocola/data/datastore/DataStore.kt +++ b/app/src/main/java/com/sosauce/chocola/data/datastore/DataStore.kt @@ -4,7 +4,6 @@ package com.sosauce.chocola.data.datastore import android.content.Context import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.datastore.core.DataStore import androidx.datastore.preferences.core.Preferences @@ -29,11 +28,13 @@ import com.sosauce.chocola.data.datastore.PreferencesKeys.HIDDEN_TRACKS import com.sosauce.chocola.data.datastore.PreferencesKeys.INITIAL_SCREEN import com.sosauce.chocola.data.datastore.PreferencesKeys.LYRICS_ALIGNMENT import com.sosauce.chocola.data.datastore.PreferencesKeys.LYRICS_FONT_SIZE +import com.sosauce.chocola.data.datastore.PreferencesKeys.MATCH_CASE_FILTER import com.sosauce.chocola.data.datastore.PreferencesKeys.MIN_TRACK_DURATION import com.sosauce.chocola.data.datastore.PreferencesKeys.NUMBER_OF_ALBUM_GRIDS import com.sosauce.chocola.data.datastore.PreferencesKeys.PALETTE_STYLE import com.sosauce.chocola.data.datastore.PreferencesKeys.PAUSE_ON_MUTE import com.sosauce.chocola.data.datastore.PreferencesKeys.PLAYLIST_SORT +import com.sosauce.chocola.data.datastore.PreferencesKeys.REGEX_FILTER import com.sosauce.chocola.data.datastore.PreferencesKeys.SAF_TRACKS import com.sosauce.chocola.data.datastore.PreferencesKeys.SEEK_BUTTONS_DURATION import com.sosauce.chocola.data.datastore.PreferencesKeys.SHOW_ALBUM_NAME @@ -86,6 +87,10 @@ data object PreferencesKeys { val ALBUM_SORT = intPreferencesKey("ALBUM_SORT") val TRACK_SORT = intPreferencesKey("TRACK_SORT") val ARTIST_SORT = intPreferencesKey("ARTIST_SORT") + + val REGEX_FILTER = booleanPreferencesKey("REGEX_FILTER") + val MATCH_CASE_FILTER = booleanPreferencesKey("MATCH_CASE_FILTER") + val PAUSE_ON_MUTE = booleanPreferencesKey("PAUSE_ON_MUTE") val MIN_TRACK_DURATION = intPreferencesKey("MIN_TRACK_DURATION") val PLAYLIST_SORT = intPreferencesKey("PLAYLIST_SORT") @@ -203,6 +208,14 @@ fun rememberHasSeenTip() = fun rememberSortTracksAscending() = rememberPreference(key = SORT_TRACKS_ASCENDING, defaultValue = true) +@Composable +fun rememberRegexFilter() = + rememberPreference(key = REGEX_FILTER, defaultValue = false) + +@Composable +fun rememberMatchCaseFilter() = + rememberPreference(key = MATCH_CASE_FILTER, defaultValue = false) + @Composable fun rememberHiddenTracks() = rememberPreference(key = HIDDEN_TRACKS, defaultValue = emptySet()) diff --git a/app/src/main/java/com/sosauce/chocola/data/datastore/UserPreferences.kt b/app/src/main/java/com/sosauce/chocola/data/datastore/UserPreferences.kt index 0ad5512..f04b871 100644 --- a/app/src/main/java/com/sosauce/chocola/data/datastore/UserPreferences.kt +++ b/app/src/main/java/com/sosauce/chocola/data/datastore/UserPreferences.kt @@ -9,9 +9,11 @@ import com.sosauce.chocola.data.datastore.PreferencesKeys.EQUALIZER_ENABLED import com.sosauce.chocola.data.datastore.PreferencesKeys.EQUALIZER_PRESETS import com.sosauce.chocola.data.datastore.PreferencesKeys.HIDDEN_TRACKS import com.sosauce.chocola.data.datastore.PreferencesKeys.LAST_MUSIC_STATE +import com.sosauce.chocola.data.datastore.PreferencesKeys.MATCH_CASE_FILTER import com.sosauce.chocola.data.datastore.PreferencesKeys.MIN_TRACK_DURATION import com.sosauce.chocola.data.datastore.PreferencesKeys.PAUSE_ON_MUTE import com.sosauce.chocola.data.datastore.PreferencesKeys.PLAYLIST_SORT +import com.sosauce.chocola.data.datastore.PreferencesKeys.REGEX_FILTER import com.sosauce.chocola.data.datastore.PreferencesKeys.SAF_TRACKS import com.sosauce.chocola.data.datastore.PreferencesKeys.SORT_ALBUMS_ASCENDING import com.sosauce.chocola.data.datastore.PreferencesKeys.SORT_ARTISTS_ASCENDING @@ -30,7 +32,6 @@ import com.sosauce.chocola.utils.copyMutate import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map -import kotlinx.serialization.decodeFromString import kotlinx.serialization.json.Json class UserPreferences( @@ -57,6 +58,14 @@ class UserPreferences( PlaylistSort.entries[sort] } + val getRegexFilter = context.dataStore.data.map { + it[REGEX_FILTER] ?: false + } + + val getMatchCaseFilter = context.dataStore.data.map { + it[MATCH_CASE_FILTER] ?: false + } + val sortTracksAscending = context.dataStore.data.map { it[SORT_TRACKS_ASCENDING] ?: true } diff --git a/app/src/main/java/com/sosauce/chocola/presentation/screens/album/AlbumDetailsScreen.kt b/app/src/main/java/com/sosauce/chocola/presentation/screens/album/AlbumDetailsScreen.kt index d207d00..20b3a5b 100644 --- a/app/src/main/java/com/sosauce/chocola/presentation/screens/album/AlbumDetailsScreen.kt +++ b/app/src/main/java/com/sosauce/chocola/presentation/screens/album/AlbumDetailsScreen.kt @@ -33,6 +33,8 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import com.sosauce.chocola.R import com.sosauce.chocola.data.datastore.rememberIsLandscape +import com.sosauce.chocola.data.datastore.rememberMatchCaseFilter +import com.sosauce.chocola.data.datastore.rememberRegexFilter import com.sosauce.chocola.data.datastore.rememberSortTracksAscending import com.sosauce.chocola.data.datastore.rememberTrackSort import com.sosauce.chocola.data.models.CuteTrack @@ -63,6 +65,8 @@ fun SharedTransitionScope.AlbumDetailsScreen( val lazyState = rememberLazyListState() val isLandscape = rememberIsLandscape() var sortTracksAsc by rememberSortTracksAscending() + var regexFilter by rememberRegexFilter() + var matchCaseFilter by rememberMatchCaseFilter() var trackSort by rememberTrackSort() val multiSelectState = rememberSweetSelectState() @@ -133,7 +137,11 @@ fun SharedTransitionScope.AlbumDetailsScreen( sortMenu = { SortingDropdownMenu( isSortedAscending = sortTracksAsc, - onChangeSorting = { sortTracksAsc = it } + onChangeSorting = { sortTracksAsc = it }, + isRegexFilter = regexFilter, + onChangeRegexFilter = { regexFilter = it }, + isMatchCaseFilter = matchCaseFilter, + onChangeMatchCaseFilter = { matchCaseFilter = it } ) { repeat(6) { i -> val text = when (i) { diff --git a/app/src/main/java/com/sosauce/chocola/presentation/screens/album/AlbumDetailsViewModel.kt b/app/src/main/java/com/sosauce/chocola/presentation/screens/album/AlbumDetailsViewModel.kt index a593553..894c5c9 100644 --- a/app/src/main/java/com/sosauce/chocola/presentation/screens/album/AlbumDetailsViewModel.kt +++ b/app/src/main/java/com/sosauce/chocola/presentation/screens/album/AlbumDetailsViewModel.kt @@ -53,8 +53,6 @@ class AlbumDetailsViewModel( } } } - - } data class AlbumDetailsState( diff --git a/app/src/main/java/com/sosauce/chocola/presentation/screens/album/AlbumsScreen.kt b/app/src/main/java/com/sosauce/chocola/presentation/screens/album/AlbumsScreen.kt index de6bb74..bb4891b 100644 --- a/app/src/main/java/com/sosauce/chocola/presentation/screens/album/AlbumsScreen.kt +++ b/app/src/main/java/com/sosauce/chocola/presentation/screens/album/AlbumsScreen.kt @@ -4,7 +4,6 @@ package com.sosauce.chocola.presentation.screens.album import androidx.compose.animation.ExperimentalSharedTransitionApi import androidx.compose.animation.SharedTransitionScope -import androidx.compose.animation.animateContentSize import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.WindowInsets @@ -14,9 +13,7 @@ import androidx.compose.foundation.layout.safeDrawing import androidx.compose.foundation.lazy.grid.GridCells import androidx.compose.foundation.lazy.grid.LazyVerticalGrid import androidx.compose.foundation.lazy.grid.items -import androidx.compose.foundation.lazy.grid.itemsIndexed import androidx.compose.foundation.lazy.grid.rememberLazyGridState -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.ContainedLoadingIndicator import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @@ -29,13 +26,14 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Shape import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import com.sosauce.chocola.R import com.sosauce.chocola.data.datastore.rememberAlbumGrids import com.sosauce.chocola.data.datastore.rememberAlbumSort +import com.sosauce.chocola.data.datastore.rememberMatchCaseFilter +import com.sosauce.chocola.data.datastore.rememberRegexFilter import com.sosauce.chocola.data.datastore.rememberSortAlbumsAscending import com.sosauce.chocola.data.states.MusicState import com.sosauce.chocola.domain.actions.PlayerActions @@ -56,6 +54,8 @@ fun SharedTransitionScope.AlbumsScreen( ) { var isSortedByASC by rememberSortAlbumsAscending() var albumSort by rememberAlbumSort() + var regexFilter by rememberRegexFilter() + var matchCaseFilter by rememberMatchCaseFilter() val lazyState = rememberLazyGridState() var numberOfAlbumGrids by rememberAlbumGrids() @@ -79,6 +79,10 @@ fun SharedTransitionScope.AlbumsScreen( SortingDropdownMenu( isSortedAscending = isSortedByASC, onChangeSorting = { isSortedByASC = it }, + isRegexFilter = regexFilter, + onChangeRegexFilter = { regexFilter = it }, + isMatchCaseFilter = matchCaseFilter, + onChangeMatchCaseFilter = { matchCaseFilter = it }, topContent = { DropdownMenuItem( onClick = { diff --git a/app/src/main/java/com/sosauce/chocola/presentation/screens/album/AlbumsViewModel.kt b/app/src/main/java/com/sosauce/chocola/presentation/screens/album/AlbumsViewModel.kt index fe4c10e..f64dadf 100644 --- a/app/src/main/java/com/sosauce/chocola/presentation/screens/album/AlbumsViewModel.kt +++ b/app/src/main/java/com/sosauce/chocola/presentation/screens/album/AlbumsViewModel.kt @@ -19,7 +19,6 @@ import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -import kotlin.system.measureTimeMillis import kotlin.time.Duration.Companion.milliseconds class AlbumsViewModel( @@ -39,10 +38,12 @@ class AlbumsViewModel( combine( userPreferences.getAlbumsSort, + userPreferences.getRegexFilter, + userPreferences.getMatchCaseFilter, userPreferences.sortAlbumsAscending, userQuery - ) { sort, ascending, query -> - val sortedAlbums = albums.ordered(sort, ascending, query.toString()) + ) { sort, regex, matchCase, ascending, query -> + val sortedAlbums = albums.ordered(sort, regex, matchCase, ascending, query.toString()) AlbumsState( isLoading = false, diff --git a/app/src/main/java/com/sosauce/chocola/presentation/screens/artist/ArtistDetailsScreen.kt b/app/src/main/java/com/sosauce/chocola/presentation/screens/artist/ArtistDetailsScreen.kt index c10d2c5..8eb87ba 100644 --- a/app/src/main/java/com/sosauce/chocola/presentation/screens/artist/ArtistDetailsScreen.kt +++ b/app/src/main/java/com/sosauce/chocola/presentation/screens/artist/ArtistDetailsScreen.kt @@ -10,8 +10,6 @@ import androidx.compose.animation.ExperimentalSharedTransitionApi import androidx.compose.animation.SharedTransitionScope import androidx.compose.foundation.background import androidx.compose.foundation.basicMarquee -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues @@ -19,7 +17,6 @@ import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.safeDrawing import androidx.compose.foundation.layout.size @@ -44,26 +41,21 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.blur -import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.navigation3.ui.LocalNavAnimatedContentScope import coil3.compose.AsyncImage import com.sosauce.chocola.R -import com.sosauce.chocola.data.datastore.rememberIsLandscape import com.sosauce.chocola.data.datastore.rememberSortTracksAscending import com.sosauce.chocola.data.datastore.rememberTrackSort import com.sosauce.chocola.data.models.CuteTrack import com.sosauce.chocola.data.states.MusicState import com.sosauce.chocola.domain.actions.PlayerActions import com.sosauce.chocola.presentation.navigation.Screen -import com.sosauce.chocola.presentation.screens.album.components.AlbumCard import com.sosauce.chocola.presentation.screens.album.components.NumberOfTracks import com.sosauce.chocola.presentation.screens.artist.components.ArtistHeader import com.sosauce.chocola.presentation.screens.artist.components.NumberOfAlbums @@ -78,8 +70,6 @@ import com.sosauce.chocola.utils.ImageUtils import com.sosauce.chocola.utils.barsContentTransform import com.sosauce.chocola.utils.selfAlignHorizontally import com.sosauce.sweetselect.rememberSweetSelectState -import sv.lib.squircleshape.CornerSmoothing -import sv.lib.squircleshape.SquircleShape @Composable fun SharedTransitionScope.ArtistDetailsScreen( @@ -251,6 +241,10 @@ fun SharedTransitionScope.ArtistDetailsScreen( SortingDropdownMenu( isSortedAscending = sortTracksAscending, onChangeSorting = { sortTracksAscending = it }, + isMatchCaseFilter = null, + isRegexFilter = null, + onChangeMatchCaseFilter = null, + onChangeRegexFilter = null ) { repeat(5) { i -> val text = when (i) { diff --git a/app/src/main/java/com/sosauce/chocola/presentation/screens/artist/ArtistsScreen.kt b/app/src/main/java/com/sosauce/chocola/presentation/screens/artist/ArtistsScreen.kt index 59fb491..9fd5557 100644 --- a/app/src/main/java/com/sosauce/chocola/presentation/screens/artist/ArtistsScreen.kt +++ b/app/src/main/java/com/sosauce/chocola/presentation/screens/artist/ArtistsScreen.kt @@ -30,7 +30,6 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.pluralStringResource import androidx.compose.ui.res.stringResource @@ -39,6 +38,8 @@ import androidx.navigation3.ui.LocalNavAnimatedContentScope import coil3.compose.AsyncImage import com.sosauce.chocola.R import com.sosauce.chocola.data.datastore.rememberArtistSort +import com.sosauce.chocola.data.datastore.rememberMatchCaseFilter +import com.sosauce.chocola.data.datastore.rememberRegexFilter import com.sosauce.chocola.data.datastore.rememberSortArtistsAscending import com.sosauce.chocola.data.models.Artist import com.sosauce.chocola.data.states.MusicState @@ -64,6 +65,8 @@ fun SharedTransitionScope.ArtistsScreen( val lazyState = rememberLazyListState() var artistSort by rememberArtistSort() + var regexFilter by rememberRegexFilter() + var matchCaseFilter by rememberMatchCaseFilter() var isSortedByASC by rememberSortArtistsAscending() if (state.isLoading) { @@ -86,6 +89,10 @@ fun SharedTransitionScope.ArtistsScreen( SortingDropdownMenu( isSortedAscending = isSortedByASC, onChangeSorting = { isSortedByASC = it }, + isMatchCaseFilter = matchCaseFilter, + onChangeMatchCaseFilter = { matchCaseFilter = it }, + isRegexFilter = regexFilter, + onChangeRegexFilter = { regexFilter = it }, ) { repeat(3) { i -> val text = when (i) { diff --git a/app/src/main/java/com/sosauce/chocola/presentation/screens/artist/ArtistsViewModel.kt b/app/src/main/java/com/sosauce/chocola/presentation/screens/artist/ArtistsViewModel.kt index 323dd01..efe909a 100644 --- a/app/src/main/java/com/sosauce/chocola/presentation/screens/artist/ArtistsViewModel.kt +++ b/app/src/main/java/com/sosauce/chocola/presentation/screens/artist/ArtistsViewModel.kt @@ -44,11 +44,13 @@ class ArtistsViewModel( combine( userPreferences.getArtistsSort, + userPreferences.getRegexFilter, + userPreferences.getMatchCaseFilter, userPreferences.sortArtistsAscending, userQuery - ) { sort, ascending, query -> + ) { sort, regex, matchCase, ascending, query -> - val sortedArtists = artists.ordered(sort, ascending, query.toString()) + val sortedArtists = artists.ordered(sort, regex, matchCase, ascending, query.toString()) ArtistsState( isLoading = false, diff --git a/app/src/main/java/com/sosauce/chocola/presentation/screens/main/MainScreen.kt b/app/src/main/java/com/sosauce/chocola/presentation/screens/main/MainScreen.kt index 6d319d5..3dbf2e7 100644 --- a/app/src/main/java/com/sosauce/chocola/presentation/screens/main/MainScreen.kt +++ b/app/src/main/java/com/sosauce/chocola/presentation/screens/main/MainScreen.kt @@ -42,6 +42,8 @@ import androidx.compose.ui.util.fastForEach import com.sosauce.chocola.R import com.sosauce.chocola.data.datastore.rememberGroupByFolders import com.sosauce.chocola.data.datastore.rememberHiddenFolders +import com.sosauce.chocola.data.datastore.rememberMatchCaseFilter +import com.sosauce.chocola.data.datastore.rememberRegexFilter import com.sosauce.chocola.data.datastore.rememberShowShuffleButton import com.sosauce.chocola.data.datastore.rememberSortTracksAscending import com.sosauce.chocola.data.datastore.rememberTrackSort @@ -79,6 +81,8 @@ fun SharedTransitionScope.MainScreen( var groupByFolders by rememberGroupByFolders() val showShuffleButton by rememberShowShuffleButton() var trackSort by rememberTrackSort() + var regexFilter by rememberRegexFilter() + var matchCaseFilter by rememberMatchCaseFilter() var sortTracksAscending by rememberSortTracksAscending() val multiSelectState = rememberSweetSelectState() @@ -96,8 +100,8 @@ fun SharedTransitionScope.MainScreen( AnimatedContent( targetState = multiSelectState.isInSelectionMode, transitionSpec = { barsContentTransform }, - ) { - if (it) { + ) { isInSelectionMode -> + if (isInSelectionMode) { TracksSelectedBar( modifier = Modifier.selfAlignHorizontally(), tracks = state.tracks, @@ -114,6 +118,10 @@ fun SharedTransitionScope.MainScreen( SortingDropdownMenu( isSortedAscending = sortTracksAscending, onChangeSorting = { sortTracksAscending = it }, + isMatchCaseFilter = matchCaseFilter, + onChangeMatchCaseFilter = { matchCaseFilter = it }, + isRegexFilter = regexFilter, + onChangeRegexFilter = { regexFilter = it }, topContent = { DropdownMenuItem( selected = groupByFolders, diff --git a/app/src/main/java/com/sosauce/chocola/presentation/screens/main/MainViewModel.kt b/app/src/main/java/com/sosauce/chocola/presentation/screens/main/MainViewModel.kt index 7e1f7df..c7b12e2 100644 --- a/app/src/main/java/com/sosauce/chocola/presentation/screens/main/MainViewModel.kt +++ b/app/src/main/java/com/sosauce/chocola/presentation/screens/main/MainViewModel.kt @@ -3,32 +3,25 @@ package com.sosauce.chocola.presentation.screens.main import android.app.Application -import android.media.MediaScannerConnection -import android.net.Uri -import android.os.Environment import androidx.compose.foundation.text.input.TextFieldState import androidx.compose.runtime.snapshotFlow -import androidx.compose.ui.util.fastFilter import androidx.lifecycle.AndroidViewModel -import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.sosauce.chocola.data.AbstractTracksScanner import com.sosauce.chocola.data.datastore.UserPreferences import com.sosauce.chocola.data.models.CuteTrack -import com.sosauce.chocola.utils.TrackSort -import com.sosauce.chocola.utils.copyMutate +import com.sosauce.chocola.utils.combine import com.sosauce.chocola.utils.ordered import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.collectLatest -import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -import kotlin.system.measureTimeMillis +import kotlin.time.Duration.Companion.milliseconds class MainViewModel( private val application: Application, @@ -37,7 +30,7 @@ class MainViewModel( ) : AndroidViewModel(application) { val textFieldState = TextFieldState() - private val userQuery = snapshotFlow { textFieldState.text }.debounce(250) + private val userQuery = snapshotFlow { textFieldState.text }.debounce(250.milliseconds) private val _state = MutableStateFlow(MainState(isLoading = true, textFieldState = textFieldState)) val state = _state.asStateFlow() @@ -49,9 +42,11 @@ class MainViewModel( abstractTracksScanner.fetchLatestTracks(null, null), userQuery, userPreferences.getTrackSort, + userPreferences.getRegexFilter, + userPreferences.getMatchCaseFilter, userPreferences.sortTracksAscending - ) { tracks, query, trackSort, ascending -> - tracks.ordered(trackSort, ascending, query) to query.isNotEmpty() + ) { tracks, query, trackSort, regex, matchCase, ascending -> + tracks.ordered(trackSort,regex, matchCase, ascending, query.toString()) to query.isNotEmpty() } .flowOn(Dispatchers.Default) .collectLatest { (tracks, isSearching) -> @@ -65,8 +60,6 @@ class MainViewModel( } } } - - } diff --git a/app/src/main/java/com/sosauce/chocola/presentation/screens/playing/QueueScreen.kt b/app/src/main/java/com/sosauce/chocola/presentation/screens/playing/QueueScreen.kt index a86fb26..4e28839 100644 --- a/app/src/main/java/com/sosauce/chocola/presentation/screens/playing/QueueScreen.kt +++ b/app/src/main/java/com/sosauce/chocola/presentation/screens/playing/QueueScreen.kt @@ -17,14 +17,11 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.scale import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.res.painterResource import androidx.compose.ui.unit.dp -import androidx.compose.ui.util.fastMap import com.sosauce.chocola.R import com.sosauce.chocola.data.states.MusicState import com.sosauce.chocola.domain.actions.PlayerActions @@ -34,7 +31,6 @@ import com.sosauce.chocola.utils.selfAlignHorizontally import sh.calvin.reorderable.ReorderableItem import sh.calvin.reorderable.rememberReorderableLazyListState import kotlin.uuid.ExperimentalUuidApi -import kotlin.uuid.Uuid @Composable fun QueueScreen( @@ -71,7 +67,7 @@ fun QueueScreen( ) { items( items = musicState.loadedMedias, - key = { it.mediaItem} + key = { it.mediaItem.mediaId } ) { track -> ReorderableItem( state = reorderableLazyListState, @@ -119,10 +115,7 @@ fun QueueScreen( } ) } - } } - } - } \ No newline at end of file diff --git a/app/src/main/java/com/sosauce/chocola/presentation/screens/playlists/PlaylistDetailsScreen.kt b/app/src/main/java/com/sosauce/chocola/presentation/screens/playlists/PlaylistDetailsScreen.kt index 89616ea..4312649 100644 --- a/app/src/main/java/com/sosauce/chocola/presentation/screens/playlists/PlaylistDetailsScreen.kt +++ b/app/src/main/java/com/sosauce/chocola/presentation/screens/playlists/PlaylistDetailsScreen.kt @@ -25,14 +25,11 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import com.materialkolor.DynamicMaterialExpressiveTheme @@ -40,7 +37,6 @@ import com.materialkolor.dynamiccolor.ColorSpec import com.materialkolor.rememberDynamicMaterialThemeState import com.sosauce.chocola.R import com.sosauce.chocola.data.datastore.rememberAppTheme -import com.sosauce.chocola.data.datastore.rememberIsLandscape import com.sosauce.chocola.data.datastore.rememberPaletteStyle import com.sosauce.chocola.data.datastore.rememberSortTracksAscending import com.sosauce.chocola.data.datastore.rememberTrackSort @@ -167,7 +163,11 @@ fun SharedTransitionScope.PlaylistDetailsScreen( sortMenu = { SortingDropdownMenu( isSortedAscending = sortTracksAsc, - onChangeSorting = { sortTracksAsc = it } + onChangeSorting = { sortTracksAsc = it }, + isMatchCaseFilter = null, + isRegexFilter = null, + onChangeMatchCaseFilter = null, + onChangeRegexFilter = null, ) { repeat(6) { i -> val text = when (i) { diff --git a/app/src/main/java/com/sosauce/chocola/presentation/screens/playlists/PlaylistViewModel.kt b/app/src/main/java/com/sosauce/chocola/presentation/screens/playlists/PlaylistViewModel.kt index cc026cf..06b5367 100644 --- a/app/src/main/java/com/sosauce/chocola/presentation/screens/playlists/PlaylistViewModel.kt +++ b/app/src/main/java/com/sosauce/chocola/presentation/screens/playlists/PlaylistViewModel.kt @@ -15,6 +15,7 @@ import com.sosauce.chocola.data.datastore.UserPreferences import com.sosauce.chocola.data.models.Playlist import com.sosauce.chocola.data.playlist.PlaylistDao import com.sosauce.chocola.domain.actions.PlaylistActions +import com.sosauce.chocola.utils.combine import com.sosauce.chocola.utils.ordered import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.FlowPreview @@ -22,11 +23,11 @@ import kotlinx.coroutines.ensureActive import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.collectLatest -import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import kotlin.time.Duration.Companion.milliseconds class PlaylistViewModel( private val application: Application, @@ -35,7 +36,7 @@ class PlaylistViewModel( ) : AndroidViewModel(application) { val textFieldState = TextFieldState() - private val userQuery = snapshotFlow { textFieldState.text }.debounce(250) + private val userQuery = snapshotFlow { textFieldState.text }.debounce(250.milliseconds) private val _state = MutableStateFlow(PlaylistsState(isLoading = true)) val state = _state.asStateFlow() @@ -46,11 +47,13 @@ class PlaylistViewModel( combine( dao.getPlaylists(), userPreferences.getPlaylistsSort, + userPreferences.getRegexFilter, + userPreferences.getMatchCaseFilter, userPreferences.sortPlaylistsAscending, userQuery - ) { playlists, sort, ascending, query -> + ) { playlists, sort, regex, matchCase, ascending, query -> - val sortedPlaylists = playlists.ordered(sort, ascending, query.toString()) + val sortedPlaylists = playlists.ordered(sort, regex, matchCase, ascending, query.toString()) PlaylistsState( playlists = sortedPlaylists, diff --git a/app/src/main/java/com/sosauce/chocola/presentation/screens/playlists/PlaylistsScreen.kt b/app/src/main/java/com/sosauce/chocola/presentation/screens/playlists/PlaylistsScreen.kt index 919e3d3..6837864 100644 --- a/app/src/main/java/com/sosauce/chocola/presentation/screens/playlists/PlaylistsScreen.kt +++ b/app/src/main/java/com/sosauce/chocola/presentation/screens/playlists/PlaylistsScreen.kt @@ -45,7 +45,9 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import com.sosauce.chocola.R +import com.sosauce.chocola.data.datastore.rememberMatchCaseFilter import com.sosauce.chocola.data.datastore.rememberPlaylistSort +import com.sosauce.chocola.data.datastore.rememberRegexFilter import com.sosauce.chocola.data.datastore.rememberSortPlaylistsAscending import com.sosauce.chocola.data.models.Playlist import com.sosauce.chocola.data.states.MusicState @@ -79,6 +81,8 @@ fun SharedTransitionScope.PlaylistsScreen( var showPlaylistCreatorDialog by remember { mutableStateOf(false) } val lazyState = rememberLazyListState() var isSortedByASC by rememberSortPlaylistsAscending() + var regexFilter by rememberRegexFilter() + var matchCaseFilter by rememberMatchCaseFilter() var fabMenuExpanded by remember { mutableStateOf(false) } var showDeletionDialog by remember { mutableStateOf(false) } var playlistSort by rememberPlaylistSort() @@ -127,8 +131,8 @@ fun SharedTransitionScope.PlaylistsScreen( AnimatedContent( targetState = multiSelectState.isInSelectionMode, transitionSpec = { barsContentTransform } - ) { - if (it) { + ) { isInSelectionMode -> + if (isInSelectionMode) { SelectedBarSurface( modifier = Modifier.selfAlignHorizontally(), items = state.playlists, @@ -158,7 +162,11 @@ fun SharedTransitionScope.PlaylistsScreen( sortingMenu = { SortingDropdownMenu( isSortedAscending = isSortedByASC, - onChangeSorting = { isSortedByASC = it } + onChangeSorting = { isSortedByASC = it }, + isMatchCaseFilter = matchCaseFilter, + onChangeMatchCaseFilter = { matchCaseFilter = it }, + isRegexFilter = regexFilter, + onChangeRegexFilter = { regexFilter = it }, ) { repeat(4) { i -> val text = when (i) { diff --git a/app/src/main/java/com/sosauce/chocola/presentation/shared_components/SortingDropdownMenu.kt b/app/src/main/java/com/sosauce/chocola/presentation/shared_components/SortingDropdownMenu.kt index 9a8b952..f1061d8 100644 --- a/app/src/main/java/com/sosauce/chocola/presentation/shared_components/SortingDropdownMenu.kt +++ b/app/src/main/java/com/sosauce/chocola/presentation/shared_components/SortingDropdownMenu.kt @@ -12,10 +12,7 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.Button -import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.ButtonGroup -import androidx.compose.material3.ButtonGroupDefaults import androidx.compose.material3.DropdownMenuGroup import androidx.compose.material3.DropdownMenuPopup import androidx.compose.material3.ExperimentalMaterial3Api @@ -24,9 +21,7 @@ import androidx.compose.material3.FilledIconButton import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.IconButtonDefaults -import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MenuDefaults -import androidx.compose.material3.contentColorFor import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -42,13 +37,17 @@ import com.sosauce.chocola.utils.rememberInteractionSource @Composable fun SortingDropdownMenu( isSortedAscending: Boolean, + isMatchCaseFilter: Boolean?, + isRegexFilter: Boolean?, onChangeSorting: (Boolean) -> Unit, + onChangeMatchCaseFilter: ((Boolean) -> Unit)?, + onChangeRegexFilter: ((Boolean) -> Unit)?, topContent: @Composable (() -> Unit)? = null, content: @Composable (ColumnScope.() -> Unit) ) { var expanded by remember { mutableStateOf(false) } - val interactionSources = List(2) { rememberInteractionSource() } + val interactionSources = List(4) { rememberInteractionSource() } Box { IconButton( @@ -140,6 +139,77 @@ fun SortingDropdownMenu( menuContent = {} ) } + + if (isRegexFilter != null || isMatchCaseFilter != null) { + Spacer(Modifier.height(MenuDefaults.GroupSpacing)) + + ButtonGroup( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(2.dp, Alignment.CenterHorizontally), + overflowIndicator = {} + ) { + + if (isRegexFilter != null) { + customItem( + buttonGroupContent = { + val shape3 by animateDpAsState( + targetValue = if (isRegexFilter) 50.dp else 12.dp + ) + + FilledIconButton( + onClick = { onChangeRegexFilter!!(!isRegexFilter) }, + interactionSource = interactionSources[2], + modifier = Modifier + .animateWidth(interactionSources[2]) + .weight(1f) + .size(IconButtonDefaults.mediumContainerSize(IconButtonDefaults.IconButtonWidthOption.Wide)), + colors = IconButtonDefaults.filledIconButtonColors( + containerColor = if (isRegexFilter) MenuDefaults.groupVibrantContainerColor else MenuDefaults.groupStandardContainerColor + ), + shape = RoundedCornerShape(shape3) + ) { + Icon( + painter = painterResource(R.drawable.regular_expression), + contentDescription = null, + modifier = Modifier.size(IconButtonDefaults.mediumIconSize) + ) + } + }, + menuContent = {} + ) + } + + if (isMatchCaseFilter != null) { + customItem( + buttonGroupContent = { + val shape4 by animateDpAsState( + targetValue = if (isMatchCaseFilter) 50.dp else 12.dp + ) + + FilledIconButton( + onClick = { onChangeMatchCaseFilter!!(!isMatchCaseFilter) }, + interactionSource = interactionSources[3], + modifier = Modifier + .animateWidth(interactionSources[3]) + .weight(1f) + .size(IconButtonDefaults.mediumContainerSize(IconButtonDefaults.IconButtonWidthOption.Wide)), + colors = IconButtonDefaults.filledIconButtonColors( + containerColor = if (isMatchCaseFilter) MenuDefaults.groupVibrantContainerColor else MenuDefaults.groupStandardContainerColor + ), + shape = RoundedCornerShape(shape4) + ) { + Icon( + painter = painterResource(R.drawable.match_case), + contentDescription = null, + modifier = Modifier.size(IconButtonDefaults.mediumIconSize) + ) + } + }, + menuContent = {} + ) + } + } + } } } diff --git a/app/src/main/java/com/sosauce/chocola/utils/Extensions.kt b/app/src/main/java/com/sosauce/chocola/utils/Extensions.kt index 097b6a7..a410622 100644 --- a/app/src/main/java/com/sosauce/chocola/utils/Extensions.kt +++ b/app/src/main/java/com/sosauce/chocola/utils/Extensions.kt @@ -13,7 +13,7 @@ import android.net.Uri import android.os.Build import android.os.Handler import android.os.Looper -import android.text.format.DateFormat +import android.util.Log import androidx.compose.animation.ContentTransform import androidx.compose.animation.SizeTransform import androidx.compose.animation.core.Spring @@ -27,30 +27,22 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.wrapContentWidth import androidx.compose.material3.ColorScheme import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi -import androidx.compose.material3.MaterialShapes -import androidx.compose.material3.MenuDefaults import androidx.compose.material3.darkColorScheme import androidx.compose.material3.dynamicDarkColorScheme import androidx.compose.material3.dynamicLightColorScheme import androidx.compose.material3.lightColorScheme -import androidx.compose.material3.toShape import androidx.compose.runtime.Composable import androidx.compose.runtime.Stable import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester -import androidx.compose.ui.graphics.Shape -import androidx.compose.ui.layout.layout import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.offset import androidx.compose.ui.util.fastFilter -import androidx.core.net.toUri -import androidx.media3.common.MediaItem import androidx.media3.common.Player import androidx.navigation3.runtime.NavBackStack import androidx.navigation3.runtime.NavKey @@ -64,19 +56,15 @@ import com.sosauce.chocola.data.models.Playlist import com.sosauce.chocola.presentation.navigation.Screen import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.callbackFlow -import sv.lib.squircleshape.CornerSmoothing -import sv.lib.squircleshape.SquircleShape import java.io.File import java.io.FileOutputStream -import java.time.Instant -import java.time.ZoneId -import java.time.format.DateTimeFormatter import java.util.Locale import kotlin.math.round import kotlin.random.Random import kotlin.time.Duration.Companion.milliseconds import kotlin.uuid.ExperimentalUuidApi import kotlin.uuid.Uuid + fun NavKey.showBackButton(): Boolean { return this is Screen.AlbumsDetails || this is Screen.ArtistsDetails || this is Screen.PlaylistDetails } @@ -231,6 +219,7 @@ fun PropertyMap.toModifiableMap(separator: String = ", "): MutableMap { return this?.split(separator)?.map { it.trim() }?.toTypedArray() ?: arrayOf(this ?: "") } + @Stable data class AudioFileMetadata( val title: String?, @@ -283,13 +272,37 @@ inline fun List.thenIf( crossinline block: List.() -> List ): List = if (condition) block() else this + +fun String.regexOrNull(matchCase: Boolean): Regex? = try { + if (matchCase) { + this.toRegex() + } else { + this.toRegex(RegexOption.IGNORE_CASE) + } +} catch (e: Exception) { + Log.e("RegexPattern", "Failed to parse regex pattern", e) + null +} + fun List.ordered( sort: TrackSort, + regex: Boolean, + matchCase: Boolean, ascending: Boolean, - query: CharSequence + query: String ): List { - return this.fastFilter { it.title.contains(query, true) } + val regexPattern = query.regexOrNull(matchCase) + + val filtered = this.fastFilter { track -> + if (regex) { + regexPattern?.containsMatchIn(track.title) ?: false + } else { + track.title.contains(query, !matchCase) + } + } + + return filtered .sortedWith( compareBy(String.CASE_INSENSITIVE_ORDER) { when (sort) { @@ -302,8 +315,9 @@ fun List.ordered( } } ).thenIf(!ascending) { asReversed() } - } + + // Having a version with no search (album/artist details for example) is actually faster than passing an empty query fun List.ordered( sort: TrackSort, @@ -328,10 +342,22 @@ fun List.ordered( fun List.ordered( sort: AlbumSort, + regex: Boolean, + matchCase: Boolean, ascending: Boolean, query: String ): List { - return this.fastFilter { it.name.contains(query, true) } + val regexPattern = query.regexOrNull(matchCase) + + val filtered = this.fastFilter { track -> + if (regex) { + regexPattern?.containsMatchIn(track.name) ?: false + } else { + track.name.contains(query, !matchCase) + } + } + + return filtered .sortedWith( compareBy(String.CASE_INSENSITIVE_ORDER) { when (sort) { @@ -344,10 +370,22 @@ fun List.ordered( fun List.ordered( sort: ArtistSort, + regex: Boolean, + matchCase: Boolean, ascending: Boolean, query: String ): List { - return this.fastFilter { it.name.contains(query, true) } + val regexPattern = query.regexOrNull(matchCase) + + val filtered = this.fastFilter { track -> + if (regex) { + regexPattern?.containsMatchIn(track.name) ?: false + } else { + track.name.contains(query, !matchCase) + } + } + + return filtered .sortedWith( compareBy(String.CASE_INSENSITIVE_ORDER) { when (sort) { @@ -361,10 +399,22 @@ fun List.ordered( fun List.ordered( sort: PlaylistSort, + regex: Boolean, + matchCase: Boolean, ascending: Boolean, query: String ): List { - return this.fastFilter { it.name.contains(query, true) } + val regexPattern = query.regexOrNull(matchCase) + + val filtered = this.fastFilter { track -> + if (regex) { + regexPattern?.containsMatchIn(track.name) ?: false + } else { + track.name.contains(query, !matchCase) + } + } + + return filtered .sortedWith( compareBy(String.CASE_INSENSITIVE_ORDER) { when (sort) { @@ -399,8 +449,6 @@ fun ContentResolver.observe(uri: Uri) = callbackFlow { } - - val Context.appVersion get() = packageManager.getPackageInfo(packageName, 0).versionName @@ -490,7 +538,6 @@ fun bouncySpec() = spring( ) - val navigationBouncySpec = spring(Spring.DampingRatioLowBouncy, Spring.StiffnessLow) @@ -512,7 +559,7 @@ val barsContentTransform = ContentTransform( fun String.toLyricsAlignment(): TextAlign { - return when(this) { + return when (this) { LyricsAlignment.START -> TextAlign.Start LyricsAlignment.CENTERED -> TextAlign.Center LyricsAlignment.END -> TextAlign.End diff --git a/app/src/main/java/com/sosauce/chocola/utils/combine.kt b/app/src/main/java/com/sosauce/chocola/utils/combine.kt new file mode 100644 index 0000000..f8748fc --- /dev/null +++ b/app/src/main/java/com/sosauce/chocola/utils/combine.kt @@ -0,0 +1,26 @@ +package com.sosauce.chocola.utils + +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine + +fun combine( + flow1: Flow, + flow2: Flow, + flow3: Flow, + flow4: Flow, + flow5: Flow, + flow6: Flow, + transform: (T1, T2, T3, T4, T5, T6) -> R +): Flow = combine( + flow1, flow2, flow3, flow4, flow5, flow6 +) { args: Array -> + @Suppress("UNCHECKED_CAST") + transform( + args[0] as T1, + args[1] as T2, + args[2] as T3, + args[3] as T4, + args[4] as T5, + args[5] as T6 + ) +} \ No newline at end of file diff --git a/app/src/main/res/drawable/match_case.xml b/app/src/main/res/drawable/match_case.xml new file mode 100644 index 0000000..653ef56 --- /dev/null +++ b/app/src/main/res/drawable/match_case.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/regular_expression.xml b/app/src/main/res/drawable/regular_expression.xml new file mode 100644 index 0000000..2ac8253 --- /dev/null +++ b/app/src/main/res/drawable/regular_expression.xml @@ -0,0 +1,9 @@ + + +