diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/Settings.kt b/app/src/main/java/io/github/zyrouge/symphony/services/Settings.kt index 96109d1b..05f78120 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/Settings.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/Settings.kt @@ -4,10 +4,10 @@ import android.content.Context import android.net.Uri import androidx.core.content.edit import io.github.zyrouge.symphony.Symphony -import io.github.zyrouge.symphony.services.groove.repositories.AlbumArtistRepository import io.github.zyrouge.symphony.services.groove.repositories.AlbumRepository import io.github.zyrouge.symphony.services.groove.repositories.ArtistRepository import io.github.zyrouge.symphony.services.groove.repositories.GenreRepository +import io.github.zyrouge.symphony.services.groove.repositories.MediaTreeRepository import io.github.zyrouge.symphony.services.groove.repositories.PlaylistRepository import io.github.zyrouge.symphony.services.groove.repositories.SongRepository import io.github.zyrouge.symphony.services.radio.RadioQueue @@ -145,8 +145,8 @@ class Settings(private val symphony: Symphony) { ) val lastUsedAlbumArtistsSortBy = EnumEntry( "last_used_album_artists_sort_by", - enumEntries(), - AlbumArtistRepository.SortBy.ARTIST_NAME, + enumEntries(), + ArtistRepository.SortBy.ARTIST_NAME, ) val lastUsedAlbumArtistsSortReverse = BooleanEntry("last_used_album_artists_sort_reverse", false) @@ -199,6 +199,12 @@ class Settings(private val symphony: Symphony) { PlaylistRepository.SortBy.CUSTOM, ) val lastUsedPlaylistsSortReverse = BooleanEntry("last_used_playlists_sort_reverse", false) + val lastUsedFoldersSortBy = EnumEntry( + "last_used_folders_sort_by", + enumEntries(), + MediaTreeRepository.SortBy.TITLE, + ) + val lastUsedFoldersSortReverse = BooleanEntry("last_used_folders_sort_reverse", false) val lastUsedPlaylistsHorizontalGridColumns = IntEntry( "last_used_playlists_horizontal_grid_columns", ResponsiveGridColumns.DEFAULT_HORIZONTAL_COLUMNS, @@ -220,18 +226,18 @@ class Settings(private val symphony: Symphony) { SongRepository.SortBy.TRACK_NUMBER, ) val lastUsedAlbumSongsSortReverse = BooleanEntry("last_used_album_songs_sort_reverse", false) + val lastUsedArtistSongsSortBy = EnumEntry( + "last_used_artist_songs_sort_by", + enumEntries(), + SongRepository.SortBy.YEAR, + ) + val lastUsedArtistSongsSortReverse = BooleanEntry("last_used_artist_songs_sort_reverse", false) val lastUsedTreePathSortBy = EnumEntry( "last_used_tree_path_sort_by", enumEntries(), StringListUtils.SortBy.NAME, ) val lastUsedTreePathSortReverse = BooleanEntry("last_used_tree_path_sort_reverse", false) - val lastUsedFoldersSortBy = EnumEntry( - "last_used_folders_sort_by", - enumEntries(), - StringListUtils.SortBy.NAME, - ) - val lastUsedFoldersSortReverse = BooleanEntry("last_used_folders_sort_reverse", false) val lastUsedFoldersHorizontalGridColumns = IntEntry( "last_used_folders_horizontal_grid_columns", ResponsiveGridColumns.DEFAULT_HORIZONTAL_COLUMNS, diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/AlbumArtistMappingStore.kt b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/AlbumArtistMappingStore.kt index c37e5a6d..d1d340ef 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/AlbumArtistMappingStore.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/AlbumArtistMappingStore.kt @@ -3,42 +3,10 @@ package io.github.zyrouge.symphony.services.database.store import androidx.room.Dao import androidx.room.Insert import androidx.room.OnConflictStrategy -import androidx.room.RawQuery -import androidx.sqlite.db.SimpleSQLiteQuery -import androidx.sqlite.db.SupportSQLiteQuery import io.github.zyrouge.symphony.services.groove.entities.AlbumArtistMapping -import io.github.zyrouge.symphony.services.groove.entities.Artist -import io.github.zyrouge.symphony.services.groove.entities.ArtistSongMapping -import io.github.zyrouge.symphony.services.groove.repositories.AlbumArtistRepository -import kotlinx.coroutines.flow.Flow @Dao interface AlbumArtistMappingStore { @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun upsert(vararg entities: AlbumArtistMapping) - - @RawQuery(observedEntities = [AlbumArtistMapping::class, Artist::class, ArtistSongMapping::class]) - fun valuesAsFlowRaw(query: SupportSQLiteQuery): Flow> -} - -fun AlbumArtistMappingStore.valuesAsFlow( - sortBy: AlbumArtistRepository.SortBy, - sortReverse: Boolean, -): Flow> { - val orderBy = when (sortBy) { - AlbumArtistRepository.SortBy.CUSTOM -> "${Artist.TABLE}.${Artist.COLUMN_ID}" - AlbumArtistRepository.SortBy.ARTIST_NAME -> "${Artist.TABLE}.${Artist.COLUMN_NAME}" - AlbumArtistRepository.SortBy.TRACKS_COUNT -> Artist.AlongAttributes.EMBEDDED_TRACKS_COUNT - AlbumArtistRepository.SortBy.ALBUMS_COUNT -> Artist.AlongAttributes.EMBEDDED_ALBUMS_COUNT - } - val orderDirection = if (sortReverse) "DESC" else "ASC" - val query = "SELECT ${Artist.TABLE}.*, " + - "COUNT(${ArtistSongMapping.TABLE}.${ArtistSongMapping.COLUMN_SONG_ID}) as ${Artist.AlongAttributes.EMBEDDED_TRACKS_COUNT}, " + - "COUNT(${AlbumArtistMapping.TABLE}.${AlbumArtistMapping.COLUMN_ALBUM_ID}) as ${Artist.AlongAttributes.EMBEDDED_ALBUMS_COUNT} " + - "FROM ${Artist.TABLE} " + - "LEFT JOIN ${ArtistSongMapping.TABLE} ON ${ArtistSongMapping.TABLE}.${ArtistSongMapping.COLUMN_ARTIST_ID} = ${Artist.TABLE}.${Artist.COLUMN_ID} " + - "LEFT JOIN ${AlbumArtistMapping.TABLE} ON ${AlbumArtistMapping.TABLE}.${AlbumArtistMapping.COLUMN_ARTIST_ID} = ${Artist.TABLE}.${Artist.COLUMN_ID} " + - "WHERE ${AlbumArtistMapping.COLUMN_IS_ALBUM_ARTIST} = 1 " + - "ORDER BY $orderBy $orderDirection" - return valuesAsFlowRaw(SimpleSQLiteQuery(query)) } diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/AlbumStore.kt b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/AlbumStore.kt index 8290b525..a2b25336 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/AlbumStore.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/AlbumStore.kt @@ -25,16 +25,30 @@ interface AlbumStore { @Query("SELECT * FROM ${Album.TABLE} WHERE ${Album.COLUMN_NAME} = :name LIMIT 1") fun findByName(name: String): Album? - @Query("SELECT * FROM ${Album.TABLE} WHERE ${Album.COLUMN_ID} = :id LIMIT 1") - fun findByIdAsFlow(id: String): Flow + @RawQuery(observedEntities = [Album::class, AlbumArtistMapping::class, AlbumSongMapping::class]) + fun findByIdAsFlowRaw(query: SupportSQLiteQuery): Flow @RawQuery(observedEntities = [Album::class, AlbumArtistMapping::class, AlbumSongMapping::class]) fun valuesAsFlowRaw(query: SupportSQLiteQuery): Flow> } +fun AlbumStore.findByIdAsFlow(id: String): Flow { + val query = "SELECT ${Album.TABLE}.*, " + + "COUNT(${AlbumSongMapping.TABLE}.${AlbumSongMapping.COLUMN_SONG_ID}) as ${Album.AlongAttributes.EMBEDDED_TRACKS_COUNT}, " + + "COUNT(${AlbumArtistMapping.TABLE}.${AlbumArtistMapping.COLUMN_ARTIST_ID}) as ${Album.AlongAttributes.EMBEDDED_ARTISTS_COUNT} " + + "FROM ${Album.TABLE} " + + "LEFT JOIN ${AlbumSongMapping.TABLE} ON ${AlbumSongMapping.TABLE}.${AlbumSongMapping.COLUMN_ALBUM_ID} = ${Album.TABLE}.${Album.COLUMN_ID} " + + "LEFT JOIN ${AlbumArtistMapping.TABLE} ON ${AlbumArtistMapping.TABLE}.${AlbumArtistMapping.COLUMN_ALBUM_ID} = ${Album.TABLE}.${Album.COLUMN_ID} " + + "WHERE ${Album.COLUMN_ID} = ? " + + "LIMIT 1" + val args = arrayOf(id) + return findByIdAsFlowRaw(SimpleSQLiteQuery(query, args)) +} + fun AlbumStore.valuesAsFlow( sortBy: AlbumRepository.SortBy, sortReverse: Boolean, + artistId: String? = null, ): Flow> { val aliasFirstArtist = "firstArtist" val embeddedArtistName = "firstArtistName" @@ -52,14 +66,17 @@ fun AlbumStore.valuesAsFlow( "FROM ${AlbumArtistMapping.TABLE} " + "WHERE ${AlbumArtistMapping.TABLE}.${AlbumArtistMapping.COLUMN_ALBUM_ID} = ${Album.COLUMN_ID} " + "ORDER BY ${AlbumArtistMapping.COLUMN_IS_ALBUM_ARTIST} DESC" + val albumArtistMappingJoin = "" + + (if (artistId != null) "${AlbumArtistMapping.TABLE}.${AlbumArtistMapping.TABLE}.${AlbumArtistMapping.COLUMN_ARTIST_ID} = ? " else "") + + "${AlbumArtistMapping.TABLE}.${AlbumArtistMapping.COLUMN_ALBUM_ID} = ${Album.TABLE}.${Album.COLUMN_ID}" val query = "SELECT ${Album.TABLE}.*, " + "COUNT(${AlbumSongMapping.TABLE}.${AlbumSongMapping.COLUMN_SONG_ID}) as ${Album.AlongAttributes.EMBEDDED_TRACKS_COUNT}, " + "COUNT(${AlbumArtistMapping.TABLE}.${AlbumArtistMapping.COLUMN_ARTIST_ID}) as ${Album.AlongAttributes.EMBEDDED_ARTISTS_COUNT}, " + "$aliasFirstArtist.${Artist.COLUMN_NAME} as $embeddedArtistName" + "FROM ${Album.TABLE} " + "LEFT JOIN ${AlbumSongMapping.TABLE} ON ${AlbumSongMapping.TABLE}.${AlbumSongMapping.COLUMN_ALBUM_ID} = ${Album.TABLE}.${Album.COLUMN_ID} " + - "LEFT JOIN ${AlbumArtistMapping.TABLE} ON ${AlbumArtistMapping.TABLE}.${AlbumArtistMapping.COLUMN_ALBUM_ID} = ${Album.TABLE}.${Album.COLUMN_ID} " + - "LEFT JOIN ${Artist.TABLE} $aliasFirstArtist ON ${Artist.TABLE}.${Artist.COLUMN_ID} = ($artistQuery)" + + "LEFT JOIN ${AlbumArtistMapping.TABLE} ON $albumArtistMappingJoin " + + "LEFT JOIN ${Artist.TABLE} $aliasFirstArtist ON ${Artist.TABLE}.${Artist.COLUMN_ID} = ($artistQuery) " + "ORDER BY $orderBy $orderDirection" return valuesAsFlowRaw(SimpleSQLiteQuery(query)) } diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/ArtistStore.kt b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/ArtistStore.kt index e9623573..ae67ee7b 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/ArtistStore.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/ArtistStore.kt @@ -9,6 +9,7 @@ import androidx.room.Update import androidx.sqlite.db.SimpleSQLiteQuery import androidx.sqlite.db.SupportSQLiteQuery import io.github.zyrouge.symphony.services.groove.entities.AlbumArtistMapping +import io.github.zyrouge.symphony.services.groove.entities.AlbumSongMapping import io.github.zyrouge.symphony.services.groove.entities.Artist import io.github.zyrouge.symphony.services.groove.entities.ArtistSongMapping import io.github.zyrouge.symphony.services.groove.repositories.ArtistRepository @@ -22,6 +23,9 @@ interface ArtistStore { @Update suspend fun update(vararg entities: Artist): Int + @RawQuery(observedEntities = [Artist::class, ArtistSongMapping::class, AlbumArtistMapping::class]) + fun findByIdAsFlowRaw(query: SupportSQLiteQuery): Flow + @RawQuery(observedEntities = [Artist::class, ArtistSongMapping::class, AlbumArtistMapping::class]) fun valuesAsFlowRaw(query: SupportSQLiteQuery): Flow> @@ -31,9 +35,24 @@ interface ArtistStore { @MapColumn(Artist.COLUMN_ID) String> } +fun ArtistStore.findByIdAsFlow(id: String): Flow { + val query = "SELECT ${Artist.TABLE}.*, " + + "COUNT(${ArtistSongMapping.TABLE}.${ArtistSongMapping.COLUMN_SONG_ID}) as ${Artist.AlongAttributes.EMBEDDED_TRACKS_COUNT}, " + + "COUNT(${AlbumArtistMapping.TABLE}.${AlbumArtistMapping.COLUMN_ALBUM_ID}) as ${Artist.AlongAttributes.EMBEDDED_ALBUMS_COUNT} " + + "FROM ${Artist.TABLE} " + + "LEFT JOIN ${AlbumSongMapping.TABLE} ON ${AlbumSongMapping.TABLE}.${AlbumSongMapping.COLUMN_ALBUM_ID} = ${Artist.TABLE}.${Artist.COLUMN_ID} " + + "LEFT JOIN ${AlbumArtistMapping.TABLE} ON ${AlbumArtistMapping.TABLE}.${AlbumArtistMapping.COLUMN_ALBUM_ID} = ${Artist.TABLE}.${Artist.COLUMN_ID} " + + "WHERE ${Artist.COLUMN_ID} = ? " + + "LIMIT 1" + val args = arrayOf(id) + return findByIdAsFlowRaw(SimpleSQLiteQuery(query, args)) +} + fun ArtistStore.valuesAsFlow( sortBy: ArtistRepository.SortBy, sortReverse: Boolean, + albumId: String? = null, + onlyAlbumArtists: Boolean = false, ): Flow> { val orderBy = when (sortBy) { ArtistRepository.SortBy.CUSTOM -> "${Artist.TABLE}.${Artist.COLUMN_ID}" @@ -42,12 +61,20 @@ fun ArtistStore.valuesAsFlow( ArtistRepository.SortBy.ALBUMS_COUNT -> Artist.AlongAttributes.EMBEDDED_ALBUMS_COUNT } val orderDirection = if (sortReverse) "DESC" else "ASC" + val albumArtistMappingJoin = "" + + (if (albumId != null) "${AlbumArtistMapping.TABLE}.${AlbumArtistMapping.TABLE}.${AlbumArtistMapping.COLUMN_ALBUM_ID} = ? " else "") + + (if (onlyAlbumArtists) "${AlbumArtistMapping.TABLE}.${AlbumArtistMapping.TABLE}.${AlbumArtistMapping.COLUMN_IS_ALBUM_ARTIST} = 1 " else "") + + "${AlbumArtistMapping.TABLE}.${AlbumArtistMapping.COLUMN_ARTIST_ID} = ${Artist.TABLE}.${Artist.COLUMN_ID}" val query = "SELECT ${Artist.TABLE}.*, " + "COUNT(${ArtistSongMapping.TABLE}.${ArtistSongMapping.COLUMN_SONG_ID}) as ${Artist.AlongAttributes.EMBEDDED_TRACKS_COUNT}, " + "COUNT(${AlbumArtistMapping.TABLE}.${AlbumArtistMapping.COLUMN_ALBUM_ID}) as ${Artist.AlongAttributes.EMBEDDED_ALBUMS_COUNT} " + "FROM ${Artist.TABLE} " + - "LEFT JOIN ${ArtistSongMapping.TABLE} ON ${ArtistSongMapping.TABLE}.${ArtistSongMapping.COLUMN_ARTIST_ID} = ${Artist.TABLE}.${Artist.COLUMN_ID} " + - "LEFT JOIN ${AlbumArtistMapping.TABLE} ON ${AlbumArtistMapping.TABLE}.${AlbumArtistMapping.COLUMN_ARTIST_ID} = ${Artist.TABLE}.${Artist.COLUMN_ID}" + + "LEFT JOIN ${ArtistSongMapping.TABLE} ON ${ArtistSongMapping.TABLE}.${ArtistSongMapping.TABLE}.${ArtistSongMapping.COLUMN_ARTIST_ID} = ${Artist.TABLE}.${Artist.COLUMN_ID} " + + "LEFT JOIN ${AlbumArtistMapping.TABLE} ON $albumArtistMappingJoin " + "ORDER BY $orderBy $orderDirection" - return valuesAsFlowRaw(SimpleSQLiteQuery(query)) + val args = mutableListOf() + if (albumId != null) { + args.add(albumId) + } + return valuesAsFlowRaw(SimpleSQLiteQuery(query, args.toTypedArray())) } diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/GenreStore.kt b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/GenreStore.kt index 3ac131ec..261ee0f3 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/GenreStore.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/GenreStore.kt @@ -17,6 +17,9 @@ interface GenreStore { @Insert suspend fun insert(vararg entities: Genre): List + @RawQuery(observedEntities = [Genre::class, GenreSongMapping::class]) + fun findByIdAsFlowRaw(query: SupportSQLiteQuery): Flow + @RawQuery(observedEntities = [Genre::class, GenreSongMapping::class]) fun valuesAsFlowRaw(query: SupportSQLiteQuery): Flow> @@ -26,6 +29,18 @@ interface GenreStore { @MapColumn(Genre.COLUMN_ID) String> } +fun GenreStore.findByIdAsFlow(id: String): Flow { + val query = "SELECT ${Genre.TABLE}.*, " + + "COUNT(${GenreSongMapping.TABLE}.${GenreSongMapping.COLUMN_SONG_ID}) as ${Genre.AlongAttributes.EMBEDDED_TRACKS_COUNT} " + + "FROM ${Genre.TABLE} " + + "LEFT JOIN ${GenreSongMapping.TABLE} ON ${GenreSongMapping.TABLE}.${GenreSongMapping.COLUMN_GENRE_ID} = ${Genre.TABLE}.${Genre.COLUMN_ID} " + + "LEFT JOIN ${GenreSongMapping.TABLE} ON ${GenreSongMapping.TABLE}.${GenreSongMapping.COLUMN_GENRE_ID} = ${Genre.TABLE}.${Genre.COLUMN_ID} " + + "WHERE ${Genre.COLUMN_ID} = ? " + + "LIMIT 1" + val args = arrayOf(id) + return findByIdAsFlowRaw(SimpleSQLiteQuery(query, args)) +} + fun GenreStore.valuesAsFlow( sortBy: GenreRepository.SortBy, sortReverse: Boolean, diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/MediaTreeFolderStore.kt b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/MediaTreeFolderStore.kt index de4949e3..75941376 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/MediaTreeFolderStore.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/MediaTreeFolderStore.kt @@ -4,15 +4,21 @@ import androidx.room.Dao import androidx.room.Insert import androidx.room.MapColumn import androidx.room.Query +import androidx.room.RawQuery import androidx.room.Update +import androidx.sqlite.db.SimpleSQLiteQuery +import androidx.sqlite.db.SupportSQLiteQuery import io.github.zyrouge.symphony.services.groove.entities.MediaTreeFolder +import io.github.zyrouge.symphony.services.groove.entities.MediaTreeSongFile +import io.github.zyrouge.symphony.services.groove.repositories.MediaTreeRepository +import kotlinx.coroutines.flow.Flow @Dao interface MediaTreeFolderStore { - @Insert() + @Insert suspend fun insert(vararg entities: MediaTreeFolder): List - @Update() + @Update suspend fun update(vararg entities: MediaTreeFolder): Int @Query("SELECT id FROM ${MediaTreeFolder.TABLE} WHERE ${MediaTreeFolder.COLUMN_PARENT_ID} = :parentId") @@ -26,4 +32,26 @@ interface MediaTreeFolderStore { @Query("SELECT * FROM ${MediaTreeFolder.TABLE} WHERE ${MediaTreeFolder.COLUMN_PARENT_ID} = :parentId") fun entriesNameMapped(parentId: String): Map<@MapColumn(MediaTreeFolder.COLUMN_NAME) String, MediaTreeFolder> + + @RawQuery(observedEntities = [MediaTreeFolder::class, MediaTreeSongFile::class]) + fun valuesAsFlowRaw(query: SupportSQLiteQuery): Flow> +} + +fun MediaTreeFolderStore.valuesAsFlow( + sortBy: MediaTreeRepository.SortBy, + sortReverse: Boolean, +): Flow> { + val orderBy = when (sortBy) { + MediaTreeRepository.SortBy.CUSTOM -> "${MediaTreeFolder.TABLE}.${MediaTreeFolder.COLUMN_ID}" + MediaTreeRepository.SortBy.TITLE -> "${MediaTreeFolder.TABLE}.${MediaTreeFolder.COLUMN_NAME}" + MediaTreeRepository.SortBy.TRACKS_COUNT -> MediaTreeFolder.AlongAttributes.EMBEDDED_TRACKS_COUNT + } + val orderDirection = if (sortReverse) "DESC" else "ASC" + val query = "SELECT ${MediaTreeFolder.TABLE}.*, " + + "COUNT(${MediaTreeSongFile.TABLE}.${MediaTreeSongFile.COLUMN_ID}) as ${MediaTreeFolder.AlongAttributes.EMBEDDED_TRACKS_COUNT} " + + "FROM ${MediaTreeFolder.TABLE} " + + "LEFT JOIN ${MediaTreeSongFile.TABLE} ON ${MediaTreeSongFile.TABLE}.${MediaTreeSongFile.COLUMN_PARENT_ID} = ${MediaTreeFolder.TABLE}.${MediaTreeFolder.COLUMN_ID} " + + "WHERE ${MediaTreeFolder.AlongAttributes.EMBEDDED_TRACKS_COUNT} > 0 " + + "ORDER BY $orderBy $orderDirection" + return valuesAsFlowRaw(SimpleSQLiteQuery(query)) } diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/MediaTreeLyricFileStore.kt b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/MediaTreeLyricFileStore.kt index 78f0886a..7fda2db4 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/MediaTreeLyricFileStore.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/MediaTreeLyricFileStore.kt @@ -9,10 +9,10 @@ import io.github.zyrouge.symphony.services.groove.entities.MediaTreeLyricFile @Dao interface MediaTreeLyricFileStore { - @Insert() + @Insert suspend fun insert(vararg entities: MediaTreeLyricFile): List - @Update() + @Update suspend fun update(vararg entities: MediaTreeLyricFile): Int @Query("SELECT id FROM ${MediaTreeLyricFile.TABLE} WHERE ${MediaTreeLyricFile.COLUMN_PARENT_ID} = :parentId") diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/PlaylistSongMappingStore.kt b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/PlaylistSongMappingStore.kt index cb62f999..9e6aa543 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/PlaylistSongMappingStore.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/PlaylistSongMappingStore.kt @@ -5,6 +5,7 @@ import androidx.room.Insert import androidx.room.Query import androidx.room.RawQuery import androidx.sqlite.db.SimpleSQLiteQuery +import io.github.zyrouge.symphony.services.groove.entities.Playlist import io.github.zyrouge.symphony.services.groove.entities.PlaylistSongMapping import io.github.zyrouge.symphony.services.groove.entities.Song import io.github.zyrouge.symphony.services.groove.entities.SongArtworkIndex @@ -23,6 +24,9 @@ interface PlaylistSongMappingStore { @RawQuery(observedEntities = [SongArtworkIndex::class, PlaylistSongMapping::class]) fun findTop4SongArtworksAsFlowRaw(query: SimpleSQLiteQuery): Flow> + + @RawQuery + fun findSongIdsByPlaylistInternalIdAsFlowRaw(query: SimpleSQLiteQuery): Flow> } @OptIn(ExperimentalCoroutinesApi::class) @@ -32,27 +36,29 @@ fun PlaylistSongMappingStore.valuesMappedAsFlow( sortBy: SongRepository.SortBy, sortReverse: Boolean, ): Flow> { - val query = songStore.valuesAsFlowQuery( + val query = songStore.valuesQuery( sortBy, sortReverse, additionalClauseBeforeJoins = "JOIN ${PlaylistSongMapping.TABLE} ON ${PlaylistSongMapping.TABLE}.${PlaylistSongMapping.COLUMN_PLAYLIST_ID} = ? AND ${PlaylistSongMapping.TABLE}.${PlaylistSongMapping.COLUMN_SONG_ID} = ${Song.COLUMN_ID} ", additionalArgsBeforeJoins = arrayOf(id), ) - val entries = songStore.entriesAsPlaylistSongMappedFlowRaw(query) - return entries.mapLatest { - val list = mutableListOf() - var head = it.firstNotNullOfOrNull { - when { - it.value.mapping.isHead -> it.value - else -> null - } - } - while (head != null) { - list.add(head.song) - head = it[head.mapping.nextId] + val entries = songStore.entriesAsPlaylistSongMappedAsFlowRaw(query) + return entries.mapLatest { transformEntriesAsValues(it) } +} + +fun PlaylistSongMappingStore.transformEntriesAsValues(entries: Map): List { + val list = mutableListOf() + var head = entries.firstNotNullOfOrNull { + when { + it.value.mapping.isHead -> it.value + else -> null } - list.toList() } + while (head != null) { + list.add(head.song) + head = entries[head.mapping.nextId] + } + return list.toList() } fun PlaylistSongMappingStore.findTop4SongArtworksAsFlow(playlistId: String): Flow> { @@ -65,3 +71,12 @@ fun PlaylistSongMappingStore.findTop4SongArtworksAsFlow(playlistId: String): Flo val args = arrayOf(playlistId) return findTop4SongArtworksAsFlowRaw(SimpleSQLiteQuery(query, args)) } + +@OptIn(ExperimentalCoroutinesApi::class) +fun PlaylistSongMappingStore.findSongIdsByPlaylistInternalIdAsFlow(playlistInternalId: Int): Flow> { + val query = "SELECT ${PlaylistSongMapping.TABLE}.${PlaylistSongMapping.COLUMN_SONG_ID} " + + "FROM ${PlaylistSongMapping.TABLE} " + + "LEFT JOIN ${Playlist.TABLE} ON ${Playlist.TABLE}.${Playlist.COLUMN_INTERNAL_ID} = ? AND ${PlaylistSongMapping.TABLE}.${PlaylistSongMapping.COLUMN_PLAYLIST_ID} = ${Playlist.TABLE}.${Playlist.COLUMN_ID} " + val args = arrayOf(playlistInternalId) + return findSongIdsByPlaylistInternalIdAsFlowRaw(SimpleSQLiteQuery(query, args)) +} diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/PlaylistStore.kt b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/PlaylistStore.kt index d02cc1bc..52e17561 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/PlaylistStore.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/PlaylistStore.kt @@ -23,6 +23,9 @@ interface PlaylistStore { @Query("DELETE FROM ${Playlist.TABLE} WHERE ${Playlist.COLUMN_ID} = :id") suspend fun delete(id: String): Int + @RawQuery(observedEntities = [Playlist::class, PlaylistSongMapping::class]) + fun findByIdAsFlowRaw(query: SupportSQLiteQuery): Flow + @Query("SELECT * FROM ${Playlist.TABLE} WHERE ${Playlist.COLUMN_URI} != NULL") fun valuesLocalOnly(): List @@ -30,6 +33,17 @@ interface PlaylistStore { fun valuesAsFlowRaw(query: SupportSQLiteQuery): Flow> } +fun PlaylistStore.findByIdAsFlow(id: String): Flow { + val query = "SELECT ${Playlist.TABLE}.*, " + + "COUNT(${PlaylistSongMapping.TABLE}.${PlaylistSongMapping.COLUMN_SONG_ID}) as ${Playlist.AlongAttributes.EMBEDDED_TRACKS_COUNT} " + + "FROM ${Playlist.TABLE} " + + "LEFT JOIN ${PlaylistSongMapping.TABLE} ON ${PlaylistSongMapping.TABLE}.${PlaylistSongMapping.COLUMN_PLAYLIST_ID} = ${Playlist.TABLE}.${Playlist.COLUMN_ID} " + + "WHERE ${Playlist.TABLE}.${Playlist.COLUMN_ID} = ? " + + "LIMIT 1" + val args = arrayOf(id) + return findByIdAsFlowRaw(SimpleSQLiteQuery(query, args)) +} + fun PlaylistStore.valuesAsFlow( sortBy: PlaylistRepository.SortBy, sortReverse: Boolean, diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/SongQueueSongMappingStore.kt b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/SongQueueSongMappingStore.kt index 61396573..d3570912 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/SongQueueSongMappingStore.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/SongQueueSongMappingStore.kt @@ -47,7 +47,7 @@ fun SongQueueSongMappingStore.transformEntriesAsValuesFlow(entries: Flow> + fun entriesAsPlaylistSongMappedAsFlowRaw(query: SupportSQLiteQuery): Flow> + + @RawQuery + fun valuesRaw(query: SupportSQLiteQuery): List @RawQuery( observedEntities = [ @@ -78,7 +81,7 @@ interface SongStore { fun valuesAsFlowRaw(query: SupportSQLiteQuery): Flow> } -internal fun SongStore.valuesAsFlowQuery( +internal fun SongStore.valuesQuery( sortBy: SongRepository.SortBy, sortReverse: Boolean, additionalClauseAfterSongSelect: String = "", @@ -138,6 +141,25 @@ internal fun SongStore.valuesAsFlowQuery( return SimpleSQLiteQuery(query, args) } +fun SongStore.values( + sortBy: SongRepository.SortBy, + sortReverse: Boolean, + additionalClauseAfterSongSelect: String = "", + additionalClauseBeforeJoins: String = "", + additionalArgsBeforeJoins: Array = emptyArray(), + overrideOrderBy: String? = null, +): List { + val query = valuesQuery( + sortBy = sortBy, + sortReverse = sortReverse, + additionalClauseAfterSongSelect = additionalClauseAfterSongSelect, + additionalClauseBeforeJoins = additionalClauseBeforeJoins, + additionalArgsBeforeJoins = additionalArgsBeforeJoins, + overrideOrderBy = overrideOrderBy, + ) + return valuesRaw(query) +} + fun SongStore.valuesAsFlow( sortBy: SongRepository.SortBy, sortReverse: Boolean, @@ -146,7 +168,7 @@ fun SongStore.valuesAsFlow( additionalArgsBeforeJoins: Array = emptyArray(), overrideOrderBy: String? = null, ): Flow> { - val query = valuesAsFlowQuery( + val query = valuesQuery( sortBy = sortBy, sortReverse = sortReverse, additionalClauseAfterSongSelect = additionalClauseAfterSongSelect, diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/groove/Groove.kt b/app/src/main/java/io/github/zyrouge/symphony/services/groove/Groove.kt index 5bf5f313..5861401f 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/groove/Groove.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/groove/Groove.kt @@ -1,7 +1,6 @@ package io.github.zyrouge.symphony.services.groove import io.github.zyrouge.symphony.Symphony -import io.github.zyrouge.symphony.services.groove.repositories.AlbumArtistRepository import io.github.zyrouge.symphony.services.groove.repositories.AlbumRepository import io.github.zyrouge.symphony.services.groove.repositories.ArtistRepository import io.github.zyrouge.symphony.services.groove.repositories.GenreRepository @@ -29,7 +28,6 @@ class Groove(private val symphony: Symphony) : Symphony.Hooks { val song = SongRepository(symphony) val album = AlbumRepository(symphony) val artist = ArtistRepository(symphony) - val albumArtist = AlbumArtistRepository(symphony) val genre = GenreRepository(symphony) val playlist = PlaylistRepository(symphony) diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/Album.kt b/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/Album.kt index 181adaa5..f1255f28 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/Album.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/Album.kt @@ -25,7 +25,7 @@ data class Album( ) { data class AlongAttributes( @Embedded - val album: Album, + val entity: Album, @Embedded val tracksCount: Int, @Embedded diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/Artist.kt b/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/Artist.kt index f50f2f2d..2d568a0f 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/Artist.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/Artist.kt @@ -22,7 +22,7 @@ data class Artist( ) { data class AlongAttributes( @Embedded - val artist: Artist, + val entity: Artist, @Embedded val tracksCount: Int, @Embedded diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/Composer.kt b/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/Composer.kt index 9ebbb56f..81ffa539 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/Composer.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/Composer.kt @@ -21,7 +21,7 @@ data class Composer( ) { data class AlongAttributes( @Embedded - val composer: Composer, + val entity: Composer, @Embedded val tracksCount: Int, @Embedded diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/Genre.kt b/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/Genre.kt index 076bfedd..ca5072d6 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/Genre.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/Genre.kt @@ -21,7 +21,7 @@ data class Genre( ) { data class AlongAttributes( @Embedded - val genre: Genre, + val entity: Genre, @Embedded val tracksCount: Int, ) { diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/MediaTreeFolder.kt b/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/MediaTreeFolder.kt index dd701745..d002796b 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/MediaTreeFolder.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/MediaTreeFolder.kt @@ -3,6 +3,7 @@ package io.github.zyrouge.symphony.services.groove.entities import android.net.Uri import androidx.compose.runtime.Immutable import androidx.room.ColumnInfo +import androidx.room.Embedded import androidx.room.Entity import androidx.room.ForeignKey import androidx.room.Index @@ -40,6 +41,17 @@ data class MediaTreeFolder( @ColumnInfo(COLUMN_URI) val uri: Uri?, ) { + data class AlongAttributes( + @Embedded + val folder: MediaTreeFolder, + @Embedded + val tracksCount: Int, + ) { + companion object { + const val EMBEDDED_TRACKS_COUNT = "tracksCount" + } + } + companion object { const val TABLE = "media_tree_folders" const val COLUMN_ID = "id" diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/Playlist.kt b/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/Playlist.kt index 8221fd1f..e5070c37 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/Playlist.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/Playlist.kt @@ -15,12 +15,17 @@ import kotlin.io.path.nameWithoutExtension @Immutable @Entity( Playlist.TABLE, - indices = [Index(Playlist.COLUMN_TITLE)], + indices = [ + Index(Playlist.COLUMN_INTERNAL_ID, unique = true), + Index(Playlist.COLUMN_TITLE), + ], ) data class Playlist( @PrimaryKey @ColumnInfo(COLUMN_ID) val id: String, + @ColumnInfo(COLUMN_INTERNAL_ID) + val internalId: Int? = null, @ColumnInfo(COLUMN_TITLE) val title: String, @ColumnInfo(COLUMN_URI) @@ -28,9 +33,13 @@ data class Playlist( @ColumnInfo(COLUMN_PATH) val path: String?, ) { + val isLocal get() = uri != null + val isModifiable get() = !isLocal + val isInternal get() = internalId != null + data class AlongAttributes( @Embedded - val playlist: Playlist, + val entity: Playlist, @Embedded val tracksCount: Int, ) { @@ -44,13 +53,11 @@ data class Playlist( companion object { const val TABLE = "playlists" const val COLUMN_ID = "id" + const val COLUMN_INTERNAL_ID = "internal_id" const val COLUMN_TITLE = "title" const val COLUMN_URI = "title" const val COLUMN_PATH = "path" - private const val PRIMARY_STORAGE = "primary:" - const val MIMETYPE_M3U = "" - fun parse(symphony: Symphony, id: String, uri: Uri): Parsed { val file = DocumentFileX.fromSingleUri(symphony.applicationContext, uri)!! val content = symphony.applicationContext.contentResolver.openInputStream(uri) diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/Song.kt b/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/Song.kt index ab9b9b2f..23b7facb 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/Song.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/Song.kt @@ -69,7 +69,7 @@ data class Song( ) { data class AlongSongQueueMapping( @Embedded - val song: Song, + val entity: Song, @Embedded val mapping: SongQueueSongMapping, ) diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/SongQueue.kt b/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/SongQueue.kt index 96ae3ce7..728985ef 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/SongQueue.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/SongQueue.kt @@ -44,7 +44,7 @@ data class SongQueue( data class AlongAttributes( @Embedded - val queue: SongQueue, + val entity: SongQueue, @Embedded val tracksCount: Int, ) { diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/groove/repositories/AlbumArtistRepository.kt b/app/src/main/java/io/github/zyrouge/symphony/services/groove/repositories/AlbumArtistRepository.kt deleted file mode 100644 index dc4b7ca2..00000000 --- a/app/src/main/java/io/github/zyrouge/symphony/services/groove/repositories/AlbumArtistRepository.kt +++ /dev/null @@ -1,16 +0,0 @@ -package io.github.zyrouge.symphony.services.groove.repositories - -import io.github.zyrouge.symphony.Symphony -import io.github.zyrouge.symphony.services.database.store.valuesAsFlow - -class AlbumArtistRepository(private val symphony: Symphony) { - enum class SortBy { - CUSTOM, - ARTIST_NAME, - TRACKS_COUNT, - ALBUMS_COUNT, - } - - fun valuesAsFlow(sortBy: SortBy, sortReverse: Boolean) = - symphony.database.albumArtistMapping.valuesAsFlow(sortBy, sortReverse) -} diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/groove/repositories/AlbumRepository.kt b/app/src/main/java/io/github/zyrouge/symphony/services/groove/repositories/AlbumRepository.kt index 6e440713..ed225956 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/groove/repositories/AlbumRepository.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/groove/repositories/AlbumRepository.kt @@ -1,6 +1,7 @@ package io.github.zyrouge.symphony.services.groove.repositories import io.github.zyrouge.symphony.Symphony +import io.github.zyrouge.symphony.services.database.store.findByIdAsFlow import io.github.zyrouge.symphony.services.database.store.findTop4SongArtworksAsFlow import io.github.zyrouge.symphony.services.database.store.valuesAsFlow import io.github.zyrouge.symphony.services.database.store.valuesMappedAsFlow @@ -19,6 +20,12 @@ class AlbumRepository(private val symphony: Symphony) { fun findByIdAsFlow(id: String) = symphony.database.albums.findByIdAsFlow(id) + fun findArtistsOfIdAsFlow(id: String) = symphony.database.artists.valuesAsFlow( + ArtistRepository.SortBy.ARTIST_NAME, + false, + albumId = id, + ) + fun findSongsByIdAsFlow(id: String, sortBy: SongRepository.SortBy, sortReverse: Boolean) = symphony.database.albumSongMapping.valuesMappedAsFlow( symphony.database.songs, diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/groove/repositories/ArtistRepository.kt b/app/src/main/java/io/github/zyrouge/symphony/services/groove/repositories/ArtistRepository.kt index 527a2f96..f62de8d0 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/groove/repositories/ArtistRepository.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/groove/repositories/ArtistRepository.kt @@ -1,8 +1,10 @@ package io.github.zyrouge.symphony.services.groove.repositories import io.github.zyrouge.symphony.Symphony +import io.github.zyrouge.symphony.services.database.store.findByIdAsFlow import io.github.zyrouge.symphony.services.database.store.findTop4SongArtworksAsFlow import io.github.zyrouge.symphony.services.database.store.valuesAsFlow +import io.github.zyrouge.symphony.services.database.store.valuesMappedAsFlow import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.mapLatest @@ -14,6 +16,22 @@ class ArtistRepository(private val symphony: Symphony) { ALBUMS_COUNT, } + fun findByIdAsFlow(id: String) = symphony.database.artists.findByIdAsFlow(id) + + fun findAlbumsOfIdAsFlow(id: String) = symphony.database.albums.valuesAsFlow( + AlbumRepository.SortBy.ARTIST_NAME, + false, + artistId = id, + ) + + fun findSongsByIdAsFlow(id: String, sortBy: SongRepository.SortBy, sortReverse: Boolean) = + symphony.database.artistSongMapping.valuesMappedAsFlow( + symphony.database.songs, + id, + sortBy, + sortReverse + ) + @OptIn(ExperimentalCoroutinesApi::class) fun getTop4ArtworkUriAsFlow(id: String) = symphony.database.artistSongMapping.findTop4SongArtworksAsFlow(id) @@ -21,6 +39,10 @@ class ArtistRepository(private val symphony: Symphony) { indices.map { symphony.groove.song.getArtworkUriFromIndex(it) } } - fun valuesAsFlow(sortBy: SortBy, sortReverse: Boolean) = - symphony.database.artists.valuesAsFlow(sortBy, sortReverse) + fun valuesAsFlow(sortBy: SortBy, sortReverse: Boolean, onlyAlbumArtists: Boolean = false) = + symphony.database.artists.valuesAsFlow( + sortBy, + sortReverse, + onlyAlbumArtists = onlyAlbumArtists + ) } diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/groove/repositories/GenreRepository.kt b/app/src/main/java/io/github/zyrouge/symphony/services/groove/repositories/GenreRepository.kt index ed4dc71f..5dbb3778 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/groove/repositories/GenreRepository.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/groove/repositories/GenreRepository.kt @@ -1,7 +1,9 @@ package io.github.zyrouge.symphony.services.groove.repositories import io.github.zyrouge.symphony.Symphony +import io.github.zyrouge.symphony.services.database.store.findByIdAsFlow import io.github.zyrouge.symphony.services.database.store.valuesAsFlow +import io.github.zyrouge.symphony.services.database.store.valuesMappedAsFlow class GenreRepository(private val symphony: Symphony) { enum class SortBy { @@ -10,6 +12,16 @@ class GenreRepository(private val symphony: Symphony) { TRACKS_COUNT, } + fun findByIdAsFlow(id: String) = symphony.database.genres.findByIdAsFlow(id) + + fun findSongsByIdAsFlow(id: String, sortBy: SongRepository.SortBy, sortReverse: Boolean) = + symphony.database.genreSongMapping.valuesMappedAsFlow( + symphony.database.songs, + id, + sortBy, + sortReverse + ) + fun valuesAsFlow(sortBy: SortBy, sortReverse: Boolean) = symphony.database.genres.valuesAsFlow(sortBy, sortReverse) } diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/groove/repositories/MediaTreeRepository.kt b/app/src/main/java/io/github/zyrouge/symphony/services/groove/repositories/MediaTreeRepository.kt new file mode 100644 index 00000000..470bf7c0 --- /dev/null +++ b/app/src/main/java/io/github/zyrouge/symphony/services/groove/repositories/MediaTreeRepository.kt @@ -0,0 +1,29 @@ +package io.github.zyrouge.symphony.services.groove.repositories + +import io.github.zyrouge.symphony.Symphony +import io.github.zyrouge.symphony.services.database.store.findTop4SongArtworksAsFlow +import io.github.zyrouge.symphony.services.database.store.valuesAsFlow +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.mapLatest + +class MediaTreeRepository(private val symphony: Symphony) { + enum class SortBy { + CUSTOM, + TITLE, + TRACKS_COUNT, + } + + @OptIn(ExperimentalCoroutinesApi::class) + fun getTop4ArtworkUriAsFlow(id: String) = + symphony.database.playlistSongMapping.findTop4SongArtworksAsFlow(id) + .mapLatest { indices -> + indices.map { symphony.groove.song.getArtworkUriFromIndex(it) } + } + + fun valuesAsFlow(sortBy: SortBy, sortReverse: Boolean) = + symphony.database.playlists.valuesAsFlow(sortBy, sortReverse) + + companion object { + private const val FAVORITE_PLAYLIST = "favorites" + } +} diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/groove/repositories/PlaylistRepository.kt b/app/src/main/java/io/github/zyrouge/symphony/services/groove/repositories/PlaylistRepository.kt index 76bc8ada..1cbd0726 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/groove/repositories/PlaylistRepository.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/groove/repositories/PlaylistRepository.kt @@ -1,10 +1,17 @@ package io.github.zyrouge.symphony.services.groove.repositories import io.github.zyrouge.symphony.Symphony +import io.github.zyrouge.symphony.services.database.store.findByIdAsFlow +import io.github.zyrouge.symphony.services.database.store.findSongIdsByPlaylistInternalIdAsFlow import io.github.zyrouge.symphony.services.database.store.findTop4SongArtworksAsFlow import io.github.zyrouge.symphony.services.database.store.valuesAsFlow +import io.github.zyrouge.symphony.services.database.store.valuesMappedAsFlow +import io.github.zyrouge.symphony.services.groove.entities.Playlist +import io.github.zyrouge.symphony.services.groove.entities.PlaylistSongMapping import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.mapLatest +import kotlinx.coroutines.launch class PlaylistRepository(private val symphony: Symphony) { enum class SortBy { @@ -13,6 +20,66 @@ class PlaylistRepository(private val symphony: Symphony) { TRACKS_COUNT, } + private val favoriteSongIdsFlow = symphony.database.playlistSongMapping + .findSongIdsByPlaylistInternalIdAsFlow(PLAYLIST_INTERNAL_ID_FAVORITES) + private var favoriteSongIds = emptyList() + + init { + symphony.groove.coroutineScope.launch { + favoriteSongIdsFlow.collectLatest { + favoriteSongIds = it + } + } + } + + data class AddOptions( + val playlist: Playlist, + val songIds: List = emptyList(), + val songPaths: List = emptyList(), + ) + + fun add(options: AddOptions) { + val mappings = mutableListOf() + var nextId: String? = null + for (i in (options.songPaths.size - 1) downTo 0) { + val mapping = PlaylistSongMapping( + id = symphony.database.playlistSongMappingIdGenerator.next(), + playlistId = options.playlist.id, + songId = null, + songPath = options.songPaths[i], + isHead = i == 0, + nextId = nextId, + ) + mappings.add(mapping) + nextId = mapping.id + } + symphony.groove.coroutineScope.launch { + symphony.database.playlists.insert(options.playlist) + symphony.database.playlistSongMapping.insert(*mappings.toTypedArray()) + } + } + + fun removeSongs(playlistId: String, songIds: List) { + // TODO: implement this + } + + fun isFavoriteSong(songId: String) = favoriteSongIds.contains(songId) + + @OptIn(ExperimentalCoroutinesApi::class) + fun isFavoriteSongAsFlow(songId: String) = favoriteSongIdsFlow.mapLatest { + it.contains(songId) + } + + fun findByIdAsFlow(id: String) = symphony.database.playlists.findByIdAsFlow(id) + + fun findSongsByIdAsFlow(id: String, sortBy: SongRepository.SortBy, sortReverse: Boolean) = + symphony.database.playlistSongMapping.valuesMappedAsFlow( + symphony.database.songs, + id, + sortBy, + sortReverse + ) + @OptIn(ExperimentalCoroutinesApi::class) fun getTop4ArtworkUriAsFlow(id: String) = symphony.database.playlistSongMapping.findTop4SongArtworksAsFlow(id) @@ -24,6 +91,6 @@ class PlaylistRepository(private val symphony: Symphony) { symphony.database.playlists.valuesAsFlow(sortBy, sortReverse) companion object { - private const val FAVORITE_PLAYLIST = "favorites" + const val PLAYLIST_INTERNAL_ID_FAVORITES = 1 } } diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/radio/RadioQueue.kt b/app/src/main/java/io/github/zyrouge/symphony/services/radio/RadioQueue.kt index d04d42df..c99fc10a 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/radio/RadioQueue.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/radio/RadioQueue.kt @@ -1,6 +1,7 @@ package io.github.zyrouge.symphony.services.radio import io.github.zyrouge.symphony.Symphony +import io.github.zyrouge.symphony.services.groove.entities.Song import io.github.zyrouge.symphony.utils.concurrentListOf class RadioQueue(private val symphony: Symphony) { @@ -72,6 +73,18 @@ class RadioQueue(private val symphony: Symphony) { options: Radio.PlayOptions = Radio.PlayOptions(), ) = add(listOf(songId), index, options) + fun add( + songs: List, + index: Int? = null, + options: Radio.PlayOptions = Radio.PlayOptions(), + ) = add(songs.map { it.id }, index, options) + + fun add( + song: Song, + index: Int? = null, + options: Radio.PlayOptions = Radio.PlayOptions(), + ) = add(listOf(song.id), index, options) + private fun afterAdd(options: Radio.PlayOptions) { if (!symphony.radio.hasPlayer) { symphony.radio.play(options) diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/radio/RadioShorty.kt b/app/src/main/java/io/github/zyrouge/symphony/services/radio/RadioShorty.kt index d73c7280..9cae963d 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/radio/RadioShorty.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/radio/RadioShorty.kt @@ -1,6 +1,7 @@ package io.github.zyrouge.symphony.services.radio import io.github.zyrouge.symphony.Symphony +import io.github.zyrouge.symphony.services.groove.entities.Song import kotlin.random.Random class RadioShorty(private val symphony: Symphony) { @@ -24,33 +25,29 @@ class RadioShorty(private val symphony: Symphony) { } } - fun previous(): Boolean { - return when { - !symphony.radio.hasPlayer -> false - symphony.radio.currentPlaybackPosition!!.played <= 3000 && symphony.radio.canJumpToPrevious() -> { - symphony.radio.jumpToPrevious() - true - } + fun previous() = when { + !symphony.radio.hasPlayer -> false + symphony.radio.currentPlaybackPosition!!.played <= 3000 && symphony.radio.canJumpToPrevious() -> { + symphony.radio.jumpToPrevious() + true + } - else -> { - symphony.radio.seek(0) - false - } + else -> { + symphony.radio.seek(0) + false } } - fun skip(): Boolean { - return when { - !symphony.radio.hasPlayer -> false - symphony.radio.canJumpToNext() -> { - symphony.radio.jumpToNext() - true - } + fun skip() = when { + !symphony.radio.hasPlayer -> false + symphony.radio.canJumpToNext() -> { + symphony.radio.jumpToNext() + true + } - else -> { - symphony.radio.play(Radio.PlayOptions(index = 0, autostart = false)) - false - } + else -> { + symphony.radio.play(Radio.PlayOptions(index = 0, autostart = false)) + false } } @@ -77,4 +74,16 @@ class RadioShorty(private val symphony: Symphony) { options: Radio.PlayOptions = Radio.PlayOptions(), shuffle: Boolean = false, ) = playQueue(listOf(songId), options = options, shuffle = shuffle) + + fun playQueue( + songs: List, + options: Radio.PlayOptions = Radio.PlayOptions(), + shuffle: Boolean = false, + ) = playQueue(songs.map { it.id }, options = options, shuffle = shuffle) + + fun playQueue( + song: Song, + options: Radio.PlayOptions = Radio.PlayOptions(), + shuffle: Boolean = false, + ) = playQueue(listOf(song.id), options = options, shuffle = shuffle) } diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/components/AddToPlaylistDialog.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/components/AddToPlaylistDialog.kt index afc83a04..a24e8409 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/components/AddToPlaylistDialog.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/components/AddToPlaylistDialog.kt @@ -5,14 +5,12 @@ import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Check import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -31,7 +29,8 @@ fun AddToPlaylistDialog( onDismissRequest: () -> Unit, ) { var showNewPlaylistDialog by remember { mutableStateOf(false) } - val allPlaylistsIds by context.symphony.groove.playlist.all.collectAsState() + val allPlaylistsIds by context.symphony.groove.playlist.valuesAsFlow() + .collectAsStateWithLifecycle() val playlists by remember(allPlaylistsIds) { derivedStateOf { allPlaylistsIds diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/components/AlbumArtistGrid.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/components/AlbumArtistGrid.kt index 4597620a..17442193 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/components/AlbumArtistGrid.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/components/AlbumArtistGrid.kt @@ -7,7 +7,6 @@ import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -15,7 +14,6 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import io.github.zyrouge.symphony.services.groove.Groove import io.github.zyrouge.symphony.services.groove.entities.Artist -import io.github.zyrouge.symphony.services.groove.repositories.AlbumArtistRepository import io.github.zyrouge.symphony.ui.helpers.ViewContext @OptIn(ExperimentalMaterial3Api::class) @@ -26,8 +24,8 @@ fun AlbumArtistGrid( sortBy: AlbumArtistRepository.SortBy, sortReverse: Boolean, ) { - val horizontalGridColumns by context.symphony.settings.lastUsedAlbumArtistsHorizontalGridColumns.flow.collectAsState() - val verticalGridColumns by context.symphony.settings.lastUsedAlbumArtistsVerticalGridColumns.flow.collectAsState() + val horizontalGridColumns by context.symphony.settings.lastUsedAlbumArtistsHorizontalGridColumns.flow.collectAsStateWithLifecycle() + val verticalGridColumns by context.symphony.settings.lastUsedAlbumArtistsVerticalGridColumns.flow.collectAsStateWithLifecycle() val gridColumns by remember(horizontalGridColumns, verticalGridColumns) { derivedStateOf { ResponsiveGridColumns(horizontalGridColumns, verticalGridColumns) diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/components/AlbumGrid.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/components/AlbumGrid.kt index 03a85388..9cb8d769 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/components/AlbumGrid.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/components/AlbumGrid.kt @@ -7,7 +7,6 @@ import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -26,8 +25,8 @@ fun AlbumGrid( sortBy: AlbumRepository.SortBy, sortReverse: Boolean, ) { - val horizontalGridColumns by context.symphony.settings.lastUsedAlbumsHorizontalGridColumns.flow.collectAsState() - val verticalGridColumns by context.symphony.settings.lastUsedAlbumsVerticalGridColumns.flow.collectAsState() + val horizontalGridColumns by context.symphony.settings.lastUsedAlbumsHorizontalGridColumns.flow.collectAsStateWithLifecycle() + val verticalGridColumns by context.symphony.settings.lastUsedAlbumsVerticalGridColumns.flow.collectAsStateWithLifecycle() val gridColumns by remember(horizontalGridColumns, verticalGridColumns) { derivedStateOf { ResponsiveGridColumns(horizontalGridColumns, verticalGridColumns) diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/components/AlbumRow.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/components/AlbumRow.kt index c9084722..55e98202 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/components/AlbumRow.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/components/AlbumRow.kt @@ -10,10 +10,11 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.min import io.github.zyrouge.symphony.services.groove.Groove +import io.github.zyrouge.symphony.services.groove.entities.Album import io.github.zyrouge.symphony.ui.helpers.ViewContext @Composable -fun AlbumRow(context: ViewContext, albumIds: List) { +fun AlbumRow(context: ViewContext, albums: List) { BoxWithConstraints { val maxSize = min( this@BoxWithConstraints.maxHeight, @@ -23,14 +24,12 @@ fun AlbumRow(context: ViewContext, albumIds: List) { LazyRow { itemsIndexed( - albumIds, + albums, key = { i, x -> "$i-$x" }, contentType = { _, _ -> Groove.Kind.ALBUM } - ) { _, albumId -> - context.symphony.groove.album.get(albumId)?.let { album -> - Box(modifier = Modifier.width(width)) { - AlbumTile(context, album) - } + ) { _, album -> + Box(modifier = Modifier.width(width)) { + AlbumTile(context, album) } } } diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/components/ArtistGrid.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/components/ArtistGrid.kt index 40dc1f26..85425787 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/components/ArtistGrid.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/components/ArtistGrid.kt @@ -7,7 +7,6 @@ import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -26,8 +25,8 @@ fun ArtistGrid( sortBy: ArtistRepository.SortBy, sortReverse: Boolean, ) { - val horizontalGridColumns by context.symphony.settings.lastUsedArtistsHorizontalGridColumns.flow.collectAsState() - val verticalGridColumns by context.symphony.settings.lastUsedArtistsVerticalGridColumns.flow.collectAsState() + val horizontalGridColumns by context.symphony.settings.lastUsedArtistsHorizontalGridColumns.flow.collectAsStateWithLifecycle() + val verticalGridColumns by context.symphony.settings.lastUsedArtistsVerticalGridColumns.flow.collectAsStateWithLifecycle() val gridColumns by remember(horizontalGridColumns, verticalGridColumns) { derivedStateOf { ResponsiveGridColumns(horizontalGridColumns, verticalGridColumns) diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/components/GenericGrooveBanner.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/components/GenericGrooveBanner.kt index 61dd3a34..dd704558 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/components/GenericGrooveBanner.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/components/GenericGrooveBanner.kt @@ -1,8 +1,10 @@ package io.github.zyrouge.symphony.ui.components +import android.net.Uri import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.BoxWithConstraints +import androidx.compose.foundation.layout.BoxWithConstraintsScope import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height @@ -27,30 +29,20 @@ import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import coil.compose.AsyncImage -import coil.request.ImageRequest +import io.github.zyrouge.symphony.ui.helpers.Assets import io.github.zyrouge.symphony.ui.helpers.ScreenOrientation +import io.github.zyrouge.symphony.ui.helpers.ViewContext +import io.github.zyrouge.symphony.ui.helpers.createHandyImageRequest @Composable fun GenericGrooveBanner( - image: ImageRequest, + image: @Composable (BoxWithConstraintsScope) -> Unit, options: @Composable (Boolean, () -> Unit) -> Unit, content: @Composable () -> Unit, ) { val defaultHorizontalPadding = 20.dp BoxWithConstraints { - AsyncImage( - image, - null, - contentScale = ContentScale.Crop, - modifier = Modifier - .fillMaxWidth() - .height( - when (ScreenOrientation.fromConfiguration(LocalConfiguration.current)) { - ScreenOrientation.PORTRAIT -> this@BoxWithConstraints.maxWidth.times(0.7f) - ScreenOrientation.LANDSCAPE -> this@BoxWithConstraints.maxWidth.times(0.25f) - } - ) - ) + image(this@BoxWithConstraints) Row( modifier = Modifier .background( @@ -94,3 +86,54 @@ fun GenericGrooveBanner( } } } + +@Composable +fun GenericGrooveBannerSingleImage( + context: ViewContext, + uri: Uri?, + constraints: BoxWithConstraintsScope, +) { + AsyncImage( + createGrooveImageRequest(context, uri), + null, + contentScale = ContentScale.Crop, + modifier = Modifier + .fillMaxWidth() + .height( + when (ScreenOrientation.fromConfiguration(LocalConfiguration.current)) { + ScreenOrientation.PORTRAIT -> constraints.maxWidth.times(0.7f) + ScreenOrientation.LANDSCAPE -> constraints.maxWidth.times(0.25f) + } + ) + ) +} + +@Composable +fun GenericGrooveBannerQuadImage( + context: ViewContext, + images: List, + constraints: BoxWithConstraintsScope, +) { + // TODO: implement collage + AsyncImage( + createGrooveImageRequest(context, images.firstOrNull()), + null, + contentScale = ContentScale.Crop, + modifier = Modifier + .fillMaxWidth() + .height( + when (ScreenOrientation.fromConfiguration(LocalConfiguration.current)) { + ScreenOrientation.PORTRAIT -> constraints.maxWidth.times(0.7f) + ScreenOrientation.LANDSCAPE -> constraints.maxWidth.times(0.25f) + } + ) + ) +} + +private fun createGrooveImageRequest(context: ViewContext, uri: Uri?) { + createHandyImageRequest( + context.symphony.applicationContext, + uri ?: Assets.getPlaceholderUri(context.symphony), + Assets.getPlaceholderId(context.symphony), + ) +} diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/components/GenericSongListDropdown.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/components/GenericSongListDropdown.kt index f269f1b8..5547a132 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/components/GenericSongListDropdown.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/components/GenericSongListDropdown.kt @@ -36,7 +36,7 @@ fun GenericSongListDropdown( }, onClick = { onDismissRequest() - context.symphony.radio.shorty.playQueue(songIds, shuffle = true) + context.symphony.radio.shorty.playQueue(songs, shuffle = true) } ) DropdownMenuItem( @@ -49,7 +49,7 @@ fun GenericSongListDropdown( onClick = { onDismissRequest() context.symphony.radio.queue.add( - songIds, + songs, context.symphony.radio.queue.currentSongIndex + 1 ) } diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/components/GenreGrid.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/components/GenreGrid.kt index d7e99a51..5911c6cc 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/components/GenreGrid.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/components/GenreGrid.kt @@ -20,7 +20,6 @@ import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -71,8 +70,8 @@ fun GenreGrid( sortBy: GenreRepository.SortBy, sortReverse: Boolean, ) { - val horizontalGridColumns by context.symphony.settings.lastUsedGenresHorizontalGridColumns.flow.collectAsState() - val verticalGridColumns by context.symphony.settings.lastUsedGenresVerticalGridColumns.flow.collectAsState() + val horizontalGridColumns by context.symphony.settings.lastUsedGenresHorizontalGridColumns.flow.collectAsStateWithLifecycle() + val verticalGridColumns by context.symphony.settings.lastUsedGenresVerticalGridColumns.flow.collectAsStateWithLifecycle() val gridColumns by remember(horizontalGridColumns, verticalGridColumns) { derivedStateOf { ResponsiveGridColumns(horizontalGridColumns, verticalGridColumns) @@ -171,7 +170,7 @@ private fun GenreTile( ), colors = GenreTile.cardColors(index), onClick = { - context.navController.navigate(GenreViewRoute(attributedGenre.genre.name)) + context.navController.navigate(GenreViewRoute(attributedGenre.entity.name)) } ) { Box( @@ -189,7 +188,7 @@ private fun GenreTile( .absoluteOffset(8.dp, 12.dp) ) { Text( - attributedGenre.genre.name, + attributedGenre.entity.name, textAlign = TextAlign.Start, style = MaterialTheme.typography.displaySmall .copy(fontWeight = FontWeight.Bold), @@ -203,7 +202,7 @@ private fun GenreTile( verticalArrangement = Arrangement.Center, ) { Text( - attributedGenre.genre.name, + attributedGenre.entity.name, textAlign = TextAlign.Center, style = MaterialTheme.typography.bodyLarge .copy(fontWeight = FontWeight.Bold), diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/components/IntroductoryDialog.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/components/IntroductoryDialog.kt index 2745bd17..e1343216 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/components/IntroductoryDialog.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/components/IntroductoryDialog.kt @@ -22,7 +22,6 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.ProvideTextStyle import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -35,8 +34,8 @@ fun IntroductoryDialog( context: ViewContext, onDismissRequest: () -> Unit, ) { - val checkForUpdates by context.symphony.settings.checkForUpdates.flow.collectAsState() - val showUpdateToast by context.symphony.settings.showUpdateToast.flow.collectAsState() + val checkForUpdates by context.symphony.settings.checkForUpdates.flow.collectAsStateWithLifecycle() + val showUpdateToast by context.symphony.settings.showUpdateToast.flow.collectAsStateWithLifecycle() ScaffoldDialog( onDismissRequest = onDismissRequest, diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/components/LyricsText.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/components/LyricsText.kt index 64be88ca..fd9096a1 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/components/LyricsText.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/components/LyricsText.kt @@ -9,7 +9,6 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf @@ -45,8 +44,8 @@ fun LyricsText( ) } var playbackPositionTimer: Timer? = remember { null } - val queue by context.symphony.radio.observatory.queue.collectAsState() - val queueIndex by context.symphony.radio.observatory.queueIndex.collectAsState() + val queue by context.symphony.radio.observatory.queue.collectAsStateWithLifecycle() + val queueIndex by context.symphony.radio.observatory.queueIndex.collectAsStateWithLifecycle() val song by remember(queue, queueIndex) { derivedStateOf { queue.getOrNull(queueIndex)?.let { context.symphony.groove.song.get(it) } diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/components/NewPlaylistDialog.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/components/NewPlaylistDialog.kt index 4a4d6f4c..d7abebde 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/components/NewPlaylistDialog.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/components/NewPlaylistDialog.kt @@ -22,14 +22,15 @@ import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp -import io.github.zyrouge.symphony.services.groove.Playlist +import io.github.zyrouge.symphony.services.groove.entities.Playlist +import io.github.zyrouge.symphony.services.groove.repositories.PlaylistRepository import io.github.zyrouge.symphony.ui.helpers.ViewContext @Composable fun NewPlaylistDialog( context: ViewContext, initialSongIds: List = listOf(), - onDone: (Playlist) -> Unit, + onDone: (PlaylistRepository.AddOptions) -> Unit, onDismissRequest: () -> Unit, ) { var input by remember { mutableStateOf("") } @@ -81,11 +82,17 @@ fun NewPlaylistDialog( TextButton( enabled = input.isNotBlank(), onClick = { - val playlist = context.symphony.groove.playlist.create( + val playlist = Playlist( + id = context.symphony.database.playlistsIdGenerator.next(), title = input, + uri = null, + path = null, + ) + val addOptions = PlaylistRepository.AddOptions( + playlist = playlist, songIds = songIds.toList(), ) - onDone(playlist) + onDone(addOptions) } ) { Text(context.symphony.t.Done) diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/components/NowPlayingBottomBar.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/components/NowPlayingBottomBar.kt index fd89aee3..ae363a91 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/components/NowPlayingBottomBar.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/components/NowPlayingBottomBar.kt @@ -40,7 +40,6 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.material3.surfaceColorAtElevation import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableFloatStateOf @@ -97,19 +96,19 @@ private fun nowPlayingBottomBarEnterAnimationSpec() = TransitionDurations.No @Composable fun NowPlayingBottomBar(context: ViewContext, insetPadding: Boolean = true) { - val queue by context.symphony.radio.observatory.queue.collectAsState() - val queueIndex by context.symphony.radio.observatory.queueIndex.collectAsState() + val queue by context.symphony.radio.observatory.queue.collectAsStateWithLifecycle() + val queueIndex by context.symphony.radio.observatory.queueIndex.collectAsStateWithLifecycle() val currentPlayingSong by remember(queue, queueIndex) { derivedStateOf { queue.getOrNull(queueIndex)?.let { context.symphony.groove.song.get(it) } } } - val isPlaying by context.symphony.radio.observatory.isPlaying.collectAsState() - val playbackPosition by context.symphony.radio.observatory.playbackPosition.collectAsState() - val showTrackControls by context.symphony.settings.miniPlayerTrackControls.flow.collectAsState() - val showSeekControls by context.symphony.settings.miniPlayerSeekControls.flow.collectAsState() - val seekBackDuration by context.symphony.settings.seekBackDuration.flow.collectAsState() - val seekForwardDuration by context.symphony.settings.seekForwardDuration.flow.collectAsState() + val isPlaying by context.symphony.radio.observatory.isPlaying.collectAsStateWithLifecycle() + val playbackPosition by context.symphony.radio.observatory.playbackPosition.collectAsStateWithLifecycle() + val showTrackControls by context.symphony.settings.miniPlayerTrackControls.flow.collectAsStateWithLifecycle() + val showSeekControls by context.symphony.settings.miniPlayerSeekControls.flow.collectAsStateWithLifecycle() + val seekBackDuration by context.symphony.settings.seekBackDuration.flow.collectAsStateWithLifecycle() + val seekForwardDuration by context.symphony.settings.seekForwardDuration.flow.collectAsStateWithLifecycle() AnimatedContent( modifier = Modifier.fillMaxWidth(), @@ -320,7 +319,7 @@ private fun NowPlayingBottomBarContentText( text: String, style: TextStyle, ) { - val textMarquee by context.symphony.settings.miniPlayerTextMarquee.flow.collectAsState() + val textMarquee by context.symphony.settings.miniPlayerTextMarquee.flow.collectAsStateWithLifecycle() var showOverlay by remember { mutableStateOf(false) } Box { diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/components/PlaylistGrid.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/components/PlaylistGrid.kt index 5721d58c..a94b994e 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/components/PlaylistGrid.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/components/PlaylistGrid.kt @@ -8,7 +8,6 @@ import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -28,8 +27,8 @@ fun PlaylistGrid( sortReverse: Boolean, leadingContent: @Composable () -> Unit = {}, ) { - val horizontalGridColumns by context.symphony.settings.lastUsedPlaylistsHorizontalGridColumns.flow.collectAsState() - val verticalGridColumns by context.symphony.settings.lastUsedPlaylistsVerticalGridColumns.flow.collectAsState() + val horizontalGridColumns by context.symphony.settings.lastUsedPlaylistsHorizontalGridColumns.flow.collectAsStateWithLifecycle() + val verticalGridColumns by context.symphony.settings.lastUsedPlaylistsVerticalGridColumns.flow.collectAsStateWithLifecycle() val gridColumns by remember(horizontalGridColumns, verticalGridColumns) { derivedStateOf { ResponsiveGridColumns(horizontalGridColumns, verticalGridColumns) diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/components/PlaylistManageSongsDialog.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/components/PlaylistManageSongsDialog.kt index 98ee24b6..c7e7c978 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/components/PlaylistManageSongsDialog.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/components/PlaylistManageSongsDialog.kt @@ -9,7 +9,6 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Check @@ -22,7 +21,6 @@ import androidx.compose.material3.TextField import androidx.compose.material3.TextFieldDefaults import androidx.compose.material3.surfaceColorAtElevation import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -33,18 +31,22 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.RectangleShape import androidx.compose.ui.unit.dp +import io.github.zyrouge.symphony.services.groove.entities.Song import io.github.zyrouge.symphony.ui.helpers.ViewContext @Composable fun PlaylistManageSongsDialog( context: ViewContext, - selectedSongIds: List, - onDone: (List) -> Unit, + selectedSongs: List, + onDone: (List) -> Unit, ) { - val allSongIds by context.symphony.groove.song.all.collectAsState() - val nSelectedSongIds = remember { selectedSongIds.toMutableStateList() } + val songsSortBy by context.symphony.settings.lastUsedSongsSortBy.flow.collectAsStateWithLifecycle() + val songsSortReverse by context.symphony.settings.lastUsedSongsSortReverse.flow.collectAsStateWithLifecycle() + val allSongs by context.symphony.groove.song.valuesAsFlow(songsSortBy, songsSortReverse) + .collectAsStateWithLifecycle(emptyList()) + val nSelectedSongs = remember { selectedSongs.toMutableStateList() } var terms by remember { mutableStateOf("") } - val songIds by remember(allSongIds, terms, selectedSongIds) { + val songIds by remember(allSongs, terms, selectedSongs) { derivedStateOf { context.symphony.groove.song.search(allSongIds, terms, limit = -1) .map { it.entity } @@ -54,7 +56,7 @@ fun PlaylistManageSongsDialog( ScaffoldDialog( onDismissRequest = { - onDone(nSelectedSongIds.toList()) + onDone(nSelectedSongs.toList()) }, title = { Text(context.symphony.t.ManageSongs) @@ -65,7 +67,7 @@ fun PlaylistManageSongsDialog( .padding(start = 8.dp) .clip(CircleShape) .clickable { - onDone(selectedSongIds) + onDone(nSelectedSongs.toList()) }, ) { Icon( @@ -81,7 +83,7 @@ fun PlaylistManageSongsDialog( .padding(end = 8.dp) .clip(CircleShape) .clickable { - onDone(nSelectedSongIds.toList()) + onDone(nSelectedSongs.toList()) }, ) { Icon( @@ -112,7 +114,7 @@ fun PlaylistManageSongsDialog( }, ) when { - songIds.isEmpty() -> Box(modifier = Modifier.padding(0.dp, 12.dp)) { + allSongs.isEmpty() -> Box(modifier = Modifier.padding(0.dp, 12.dp)) { SubtleCaptionText(context.symphony.t.DamnThisIsSoEmpty) } diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/components/PlaylistTile.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/components/PlaylistTile.kt index 1d6ff981..d3c29799 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/components/PlaylistTile.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/components/PlaylistTile.kt @@ -32,7 +32,6 @@ import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -46,7 +45,8 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import coil.compose.AsyncImage import io.github.zyrouge.symphony.services.groove.MediaExposer -import io.github.zyrouge.symphony.services.groove.Playlist +import io.github.zyrouge.symphony.services.groove.entities.Playlist +import io.github.zyrouge.symphony.services.groove.entities.Song import io.github.zyrouge.symphony.ui.helpers.ViewContext import io.github.zyrouge.symphony.ui.theme.ThemeColors import io.github.zyrouge.symphony.ui.view.PlaylistViewRoute @@ -54,7 +54,8 @@ import io.github.zyrouge.symphony.utils.Logger @Composable fun PlaylistTile(context: ViewContext, playlist: Playlist) { - val updateId by context.symphony.groove.playlist.updateId.collectAsState() + val artworks by context.symphony.groove.playlist.getTop4ArtworkUriAsFlow(playlist.id) + .collectAsStateWithLifecycle(emptyList()) Card( modifier = Modifier @@ -70,9 +71,7 @@ fun PlaylistTile(context: ViewContext, playlist: Playlist) { Box { AsyncImage( // TODO: remove this hack after moving to reactive objects - remember(updateId, playlist) { - playlist.createArtworkImageRequest(context.symphony).build() - }, + artworks.first(), null, contentScale = ContentScale.Crop, modifier = Modifier @@ -86,6 +85,7 @@ fun PlaylistTile(context: ViewContext, playlist: Playlist) { .padding(top = 4.dp) ) { var showOptionsMenu by remember { mutableStateOf(false) } + IconButton( onClick = { showOptionsMenu = !showOptionsMenu } ) { @@ -136,11 +136,10 @@ fun PlaylistTile(context: ViewContext, playlist: Playlist) { @Composable fun PlaylistDropdownMenu( context: ViewContext, - playlist: Playlist, + playlist: Playlist.AlongAttributes, + songs: List, expanded: Boolean, - onSongsChanged: (() -> Unit) = {}, - onRename: (() -> Unit) = {}, - onDelete: (() -> Unit) = {}, + onDelete: () -> Unit, onDismissRequest: () -> Unit, ) { val savePlaylistLauncher = rememberLauncherForActivityResult( @@ -186,10 +185,7 @@ fun PlaylistDropdownMenu( }, onClick = { onDismissRequest() - context.symphony.radio.shorty.playQueue( - playlist.getSortedSongIds(context.symphony), - shuffle = true, - ) + context.symphony.radio.shorty.playQueue(songs, shuffle = true) } ) DropdownMenuItem( @@ -202,7 +198,7 @@ fun PlaylistDropdownMenu( onClick = { onDismissRequest() context.symphony.radio.queue.add( - playlist.getSortedSongIds(context.symphony), + songs, context.symphony.radio.queue.currentSongIndex + 1 ) } @@ -219,7 +215,7 @@ fun PlaylistDropdownMenu( showAddToPlaylistDialog = true } ) - if (playlist.isNotLocal) { + if (!playlist.entity.isModifiable) { DropdownMenuItem( leadingIcon = { Icon(Icons.AutoMirrored.Filled.PlaylistAdd, null) @@ -245,7 +241,7 @@ fun PlaylistDropdownMenu( showInfoDialog = true } ) - if (playlist.isNotLocal) { + if (playlist.entity.isModifiable) { DropdownMenuItem( leadingIcon = { Icon(Icons.Filled.Save, null) @@ -256,7 +252,7 @@ fun PlaylistDropdownMenu( onClick = { onDismissRequest() try { - savePlaylistLauncher.launch("${playlist.title}.m3u") + savePlaylistLauncher.launch("${playlist.entity.title}.m3u") } catch (err: Exception) { Logger.error("PlaylistTile", "export failed", err) Toast.makeText( @@ -282,7 +278,7 @@ fun PlaylistDropdownMenu( } ) } - if (!context.symphony.groove.playlist.isBuiltInPlaylist(playlist)) { + if (!playlist.entity.isInternal) { DropdownMenuItem( leadingIcon = { Icon( @@ -318,7 +314,6 @@ fun PlaylistDropdownMenu( selectedSongIds = playlist.getSongIds(context.symphony), onDone = { context.symphony.groove.playlist.update(playlist.id, it) - onSongsChanged() showSongsPicker = false } ) diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/components/SongCard.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/components/SongCard.kt index cbea674a..84b9b7aa 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/components/SongCard.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/components/SongCard.kt @@ -35,7 +35,6 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.ProvideTextStyle import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -47,8 +46,9 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle import coil.compose.AsyncImage -import io.github.zyrouge.symphony.services.groove.Song +import io.github.zyrouge.symphony.services.groove.entities.Song import io.github.zyrouge.symphony.ui.helpers.ViewContext import io.github.zyrouge.symphony.ui.view.AlbumArtistViewRoute import io.github.zyrouge.symphony.ui.view.AlbumViewRoute @@ -68,15 +68,15 @@ fun SongCard( trailingOptionsContent: (@Composable ColumnScope.(() -> Unit) -> Unit)? = null, onClick: () -> Unit, ) { - val queue by context.symphony.radio.observatory.queue.collectAsState() - val queueIndex by context.symphony.radio.observatory.queueIndex.collectAsState() + val queue by context.symphony.radio.observatory.queue.collectAsStateWithLifecycle() + val queueIndex by context.symphony.radio.observatory.queueIndex.collectAsStateWithLifecycle() val isCurrentPlaying by remember(autoHighlight, song, queue) { derivedStateOf { autoHighlight && song.id == queue.getOrNull(queueIndex) } } - val favoriteSongIds by context.symphony.groove.playlist.favorites.collectAsState() - val isFavorite by remember(favoriteSongIds, song) { - derivedStateOf { favoriteSongIds.contains(song.id) } - } + val isFavorite by context.symphony.groove.playlist.isFavoriteSongAsFlow(song.id) + .collectAsStateWithLifecycle(false) + val artwork by context.symphony.groove.song.getArtworkUriAsFlow(song.id) + .collectAsStateWithLifecycle(null) Card( modifier = Modifier.fillMaxWidth(), diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/components/SongExplorerList.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/components/SongExplorerList.kt index dd99b4df..cc6408b5 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/components/SongExplorerList.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/components/SongExplorerList.kt @@ -31,7 +31,6 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -74,8 +73,8 @@ fun SongExplorerList( initialPath?.let { explorer.navigateToFolder(it) } ?: explorer ) } - val sortBy by context.symphony.settings.lastUsedBrowserSortBy.flow.collectAsState() - val sortReverse by context.symphony.settings.lastUsedBrowserSortReverse.flow.collectAsState() + val sortBy by context.symphony.settings.lastUsedBrowserSortBy.flow.collectAsStateWithLifecycle() + val sortReverse by context.symphony.settings.lastUsedBrowserSortReverse.flow.collectAsStateWithLifecycle() val sortedEntities by remember(key, currentFolder) { derivedStateOf { val categorized = currentFolder.categorizedChildren() diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/components/SongTreeList.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/components/SongTreeList.kt index 7e8a24af..fe3ef2dc 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/components/SongTreeList.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/components/SongTreeList.kt @@ -41,7 +41,6 @@ import androidx.compose.material3.RadioButton import androidx.compose.material3.Text import androidx.compose.material3.surfaceColorAtElevation import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateListOf @@ -77,10 +76,10 @@ fun SongTreeList( addAll(initialDisabled) } } - val pathsSortBy by context.symphony.settings.lastUsedTreePathSortBy.flow.collectAsState() - val pathsSortReverse by context.symphony.settings.lastUsedTreePathSortReverse.flow.collectAsState() - val songsSortBy by context.symphony.settings.lastUsedSongsSortBy.flow.collectAsState() - val songsSortReverse by context.symphony.settings.lastUsedSongsSortReverse.flow.collectAsState() + val pathsSortBy by context.symphony.settings.lastUsedTreePathSortBy.flow.collectAsStateWithLifecycle() + val pathsSortReverse by context.symphony.settings.lastUsedTreePathSortReverse.flow.collectAsStateWithLifecycle() + val songsSortBy by context.symphony.settings.lastUsedSongsSortBy.flow.collectAsStateWithLifecycle() + val songsSortReverse by context.symphony.settings.lastUsedSongsSortReverse.flow.collectAsStateWithLifecycle() val sortedTree by remember(tree, pathsSortBy, pathsSortReverse, songsSortBy, songsSortReverse) { derivedStateOf { val pairs = StringListUtils.sort(tree.keys.toList(), pathsSortBy, pathsSortReverse) @@ -162,12 +161,12 @@ fun SongTreeListContent( togglePath: (String) -> Unit, ) { val lazyListState = rememberLazyListState() - val queue by context.symphony.radio.observatory.queue.collectAsState() - val queueIndex by context.symphony.radio.observatory.queueIndex.collectAsState() + val queue by context.symphony.radio.observatory.queue.collectAsStateWithLifecycle() + val queueIndex by context.symphony.radio.observatory.queueIndex.collectAsStateWithLifecycle() val currentPlayingSongId by remember(queue, queueIndex) { derivedStateOf { queue.getOrNull(queueIndex) } } - val favoriteIds by context.symphony.groove.playlist.favorites.collectAsState() + val favoriteIds by context.symphony.groove.playlist.favorites.collectAsStateWithLifecycle() LazyColumn( state = lazyListState, diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/theme/Theme.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/theme/Theme.kt index 45684468..d42de0e5 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/theme/Theme.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/theme/Theme.kt @@ -10,7 +10,6 @@ import androidx.compose.material3.dynamicLightColorScheme import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.SideEffect -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalDensity @@ -40,12 +39,12 @@ fun SymphonyTheme( context: ViewContext, content: @Composable () -> Unit, ) { - val themeMode by context.symphony.settings.themeMode.flow.collectAsState() - val useMaterialYou by context.symphony.settings.useMaterialYou.flow.collectAsState() - val primaryColorName by context.symphony.settings.primaryColor.flow.collectAsState() - val fontName by context.symphony.settings.fontFamily.flow.collectAsState() - val fontScale by context.symphony.settings.fontScale.flow.collectAsState() - val contentScale by context.symphony.settings.contentScale.flow.collectAsState() + val themeMode by context.symphony.settings.themeMode.flow.collectAsStateWithLifecycle() + val useMaterialYou by context.symphony.settings.useMaterialYou.flow.collectAsStateWithLifecycle() + val primaryColorName by context.symphony.settings.primaryColor.flow.collectAsStateWithLifecycle() + val fontName by context.symphony.settings.fontFamily.flow.collectAsStateWithLifecycle() + val fontScale by context.symphony.settings.fontScale.flow.collectAsStateWithLifecycle() + val contentScale by context.symphony.settings.contentScale.flow.collectAsStateWithLifecycle() val colorSchemeMode = themeMode.toColorSchemeMode(isSystemInDarkTheme()) val colorScheme = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && useMaterialYou) { diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/view/Album.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/view/Album.kt index 85090226..d744a323 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/view/Album.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/view/Album.kt @@ -26,7 +26,6 @@ import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -36,18 +35,22 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import io.github.zyrouge.symphony.services.groove.entities.Album +import io.github.zyrouge.symphony.services.groove.entities.Artist import io.github.zyrouge.symphony.ui.components.AlbumDropdownMenu import io.github.zyrouge.symphony.ui.components.AnimatedNowPlayingBottomBar import io.github.zyrouge.symphony.ui.components.GenericGrooveBanner +import io.github.zyrouge.symphony.ui.components.GenericGrooveBannerQuadImage import io.github.zyrouge.symphony.ui.components.IconButtonPlaceholder import io.github.zyrouge.symphony.ui.components.IconTextBody import io.github.zyrouge.symphony.ui.components.SongCardThumbnailLabelStyle import io.github.zyrouge.symphony.ui.components.SongList import io.github.zyrouge.symphony.ui.components.TopAppBarMinimalTitle import io.github.zyrouge.symphony.ui.helpers.ViewContext +import io.github.zyrouge.symphony.utils.DurationUtils import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.emitAll import kotlinx.coroutines.flow.emptyFlow +import kotlinx.coroutines.flow.mapLatest import kotlinx.coroutines.flow.transformLatest import kotlinx.serialization.Serializable @@ -58,22 +61,28 @@ data class AlbumViewRoute(val albumId: String) @Composable fun AlbumView(context: ViewContext, route: AlbumViewRoute) { val albumFlow = context.symphony.groove.album.findByIdAsFlow(route.albumId) - val album by albumFlow.collectAsState(null) - val songsSortBy by context.symphony.settings.lastUsedAlbumSongsSortBy.flow.collectAsState() - val songsSortReverse by context.symphony.settings.lastUsedAlbumSongsSortReverse.flow.collectAsState() - val songs by albumFlow - .transformLatest { album -> - val value = when { - album == null -> emptyFlow() - else -> context.symphony.groove.album.findSongsByIdAsFlow( - album.id, - songsSortBy, - songsSortReverse, - ) - } - emitAll(value) + val album by albumFlow.collectAsStateWithLifecycle(null) + val artists by context.symphony.groove.album.findArtistsOfIdAsFlow(route.albumId) + .collectAsStateWithLifecycle(emptyList()) + val songsSortBy by context.symphony.settings.lastUsedAlbumSongsSortBy.flow.collectAsStateWithLifecycle() + val songsSortReverse by context.symphony.settings.lastUsedAlbumSongsSortReverse.flow.collectAsStateWithLifecycle() + val songsFlow = albumFlow.transformLatest { album -> + val value = when { + album == null -> emptyFlow() + else -> context.symphony.groove.album.findSongsByIdAsFlow( + album.entity.id, + songsSortBy, + songsSortReverse, + ) + } + emitAll(value) + } + val songs by songsFlow.collectAsStateWithLifecycle(emptyList()) + val duration by songsFlow + .mapLatest { + it.fold(0L) { target, x -> target + x.duration } } - .collectAsState(emptyList()) + .collectAsStateWithLifecycle(0L) Scaffold( modifier = Modifier.fillMaxSize(), @@ -87,7 +96,8 @@ fun AlbumView(context: ViewContext, route: AlbumViewRoute) { title = { TopAppBarMinimalTitle { Text( - context.symphony.t.Album + (album?.let { " - ${it.name}" } ?: ""), + context.symphony.t.Album + (album?.let { " - ${it.entity.name}" } + ?: ""), maxLines = 2, overflow = TextOverflow.Ellipsis, ) @@ -115,7 +125,12 @@ fun AlbumView(context: ViewContext, route: AlbumViewRoute) { sortReverse = songsSortReverse, leadingContent = { item { - AlbumHero(context, album!!) + AlbumHero( + context, + album = album!!, + artists = artists, + duration = duration, + ) } }, cardThumbnailLabel = { _, song -> @@ -136,9 +151,19 @@ fun AlbumView(context: ViewContext, route: AlbumViewRoute) { @OptIn(ExperimentalLayoutApi::class) @Composable -private fun AlbumHero(context: ViewContext, album: Album.AlongAttributes) { +private fun AlbumHero( + context: ViewContext, + album: Album.AlongAttributes, + artists: List, + duration: Long, +) { + val artworks by context.symphony.groove.album.getTop4ArtworkUriAsFlow(album.entity.id) + .collectAsStateWithLifecycle(emptyList()) + GenericGrooveBanner( - image = album.createArtworkImageRequest(context.symphony).build(), + image = { constraints -> + GenericGrooveBannerQuadImage(context, artworks, constraints) + }, options = { expanded, onDismissRequest -> AlbumDropdownMenu( context, @@ -149,22 +174,22 @@ private fun AlbumHero(context: ViewContext, album: Album.AlongAttributes) { }, content = { Column { - Text(album.name) - if (album.artists.isNotEmpty()) { + Text(album.entity.name) + if (artists.isNotEmpty()) { ProvideTextStyle(MaterialTheme.typography.bodyMedium.copy(fontWeight = FontWeight.Bold)) { FlowRow { - album.artists.forEachIndexed { i, it -> + artists.forEachIndexed { i, it -> Text( - it, + it.entity.name, maxLines = 2, overflow = TextOverflow.Ellipsis, modifier = Modifier.pointerInput(Unit) { detectTapGestures { _ -> - context.navController.navigate(ArtistViewRoute(it)) + context.navController.navigate(ArtistViewRoute(it.entity.id)) } }, ) - if (i != album.artists.size - 1) { + if (i != artists.size - 1) { Text(", ") } } @@ -176,8 +201,8 @@ private fun AlbumHero(context: ViewContext, album: Album.AlongAttributes) { horizontalArrangement = Arrangement.spacedBy(6.dp), verticalAlignment = Alignment.CenterVertically, ) { - album.startYear?.let { startYear -> - val endYear = album.endYear + album.entity.startYear?.let { startYear -> + val endYear = album.entity.endYear Text( when { @@ -189,7 +214,7 @@ private fun AlbumHero(context: ViewContext, album: Album.AlongAttributes) { CircleSeparator() } Text( - album.duration.toString(), + DurationUtils.formatMs(duration), style = MaterialTheme.typography.bodyMedium.copy(fontWeight = FontWeight.Bold) ) } diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/view/AlbumArtist.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/view/AlbumArtist.kt deleted file mode 100644 index 4ed1a55d..00000000 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/view/AlbumArtist.kt +++ /dev/null @@ -1,156 +0,0 @@ -package io.github.zyrouge.symphony.ui.view - -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.filled.ArrowBack -import androidx.compose.material.icons.filled.PriorityHigh -import androidx.compose.material3.CenterAlignedTopAppBar -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.HorizontalDivider -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.Scaffold -import androidx.compose.material3.Text -import androidx.compose.material3.TopAppBarDefaults -import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.derivedStateOf -import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.unit.dp -import io.github.zyrouge.symphony.services.groove.AlbumArtist -import io.github.zyrouge.symphony.ui.components.AlbumArtistDropdownMenu -import io.github.zyrouge.symphony.ui.components.AlbumRow -import io.github.zyrouge.symphony.ui.components.AnimatedNowPlayingBottomBar -import io.github.zyrouge.symphony.ui.components.GenericGrooveBanner -import io.github.zyrouge.symphony.ui.components.IconButtonPlaceholder -import io.github.zyrouge.symphony.ui.components.IconTextBody -import io.github.zyrouge.symphony.ui.components.SongList -import io.github.zyrouge.symphony.ui.components.TopAppBarMinimalTitle -import io.github.zyrouge.symphony.ui.helpers.ViewContext -import kotlinx.serialization.Serializable - -@Serializable -data class AlbumArtistViewRoute(val albumArtistName: String) - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun AlbumArtistView(context: ViewContext, route: AlbumArtistViewRoute) { - val allAlbumArtistNames by context.symphony.groove.albumArtist.all.collectAsState() - val allSongIds by context.symphony.groove.song.all.collectAsState() - val allAlbumIds = context.symphony.groove.album.all - val albumArtist by remember(allAlbumArtistNames) { - derivedStateOf { context.symphony.groove.albumArtist.get(route.albumArtistName) } - } - val songIds by remember(albumArtist, allSongIds) { - derivedStateOf { albumArtist?.getSongIds(context.symphony) ?: listOf() } - } - val albumIds by remember(albumArtist, allAlbumIds) { - derivedStateOf { albumArtist?.getAlbumIds(context.symphony) ?: listOf() } - } - val isViable by remember(albumArtist) { - derivedStateOf { albumArtist != null } - } - - Scaffold( - modifier = Modifier.fillMaxSize(), - topBar = { - CenterAlignedTopAppBar( - navigationIcon = { - IconButton( - onClick = { context.navController.popBackStack() } - ) { - Icon(Icons.AutoMirrored.Filled.ArrowBack, null) - } - }, - title = { - TopAppBarMinimalTitle { - Text( - context.symphony.t.AlbumArtist + - (albumArtist?.let { " - ${it.name}" } ?: ""), - maxLines = 2, - overflow = TextOverflow.Ellipsis, - ) - } - }, - colors = TopAppBarDefaults.centerAlignedTopAppBarColors( - containerColor = Color.Transparent - ), - actions = { - IconButtonPlaceholder() - }, - ) - }, - content = { contentPadding -> - Box( - modifier = Modifier - .padding(contentPadding) - .fillMaxSize() - ) { - if (isViable) { - SongList( - context, - songIds = songIds, - leadingContent = { - item { - AlbumArtistHero(context, albumArtist!!) - } - if (albumIds.isNotEmpty()) { - item { - Spacer(modifier = Modifier.height(4.dp)) - AlbumRow(context, albumIds) - Spacer(modifier = Modifier.height(4.dp)) - HorizontalDivider() - } - } - } - ) - } else UnknownAlbumArtist(context, route.albumArtistName) - } - }, - bottomBar = { - AnimatedNowPlayingBottomBar(context) - } - ) -} - -@Composable -private fun AlbumArtistHero(context: ViewContext, albumArtist: AlbumArtist) { - GenericGrooveBanner( - image = albumArtist.createArtworkImageRequest(context.symphony).build(), - options = { expanded, onDismissRequest -> - AlbumArtistDropdownMenu( - context, - albumArtist, - expanded = expanded, - onDismissRequest = onDismissRequest - ) - }, - content = { - Text(albumArtist.name) - } - ) -} - -@Composable -private fun UnknownAlbumArtist(context: ViewContext, artistName: String) { - IconTextBody( - icon = { modifier -> - Icon( - Icons.Filled.PriorityHigh, - null, - modifier = modifier - ) - }, - content = { - Text(context.symphony.t.UnknownArtistX(artistName)) - } - ) -} diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/view/Artist.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/view/Artist.kt index 423057ef..a098689c 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/view/Artist.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/view/Artist.kt @@ -17,47 +17,52 @@ import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp -import io.github.zyrouge.symphony.services.groove.Artist +import io.github.zyrouge.symphony.services.groove.entities.Artist import io.github.zyrouge.symphony.ui.components.AlbumRow import io.github.zyrouge.symphony.ui.components.AnimatedNowPlayingBottomBar import io.github.zyrouge.symphony.ui.components.ArtistDropdownMenu import io.github.zyrouge.symphony.ui.components.GenericGrooveBanner +import io.github.zyrouge.symphony.ui.components.GenericGrooveBannerQuadImage import io.github.zyrouge.symphony.ui.components.IconButtonPlaceholder import io.github.zyrouge.symphony.ui.components.IconTextBody import io.github.zyrouge.symphony.ui.components.SongList import io.github.zyrouge.symphony.ui.components.TopAppBarMinimalTitle import io.github.zyrouge.symphony.ui.helpers.ViewContext +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.emitAll +import kotlinx.coroutines.flow.emptyFlow +import kotlinx.coroutines.flow.transformLatest import kotlinx.serialization.Serializable @Serializable -data class ArtistViewRoute(val artistName: String) +data class ArtistViewRoute(val artistId: String) -@OptIn(ExperimentalMaterial3Api::class) +@OptIn(ExperimentalMaterial3Api::class, ExperimentalCoroutinesApi::class) @Composable fun ArtistView(context: ViewContext, route: ArtistViewRoute) { - val allArtistNames by context.symphony.groove.artist.all.collectAsState() - val allSongIds by context.symphony.groove.song.all.collectAsState() - val allAlbumIds by context.symphony.groove.album.all.collectAsState() - val artist by remember(allArtistNames) { - derivedStateOf { context.symphony.groove.artist.get(route.artistName) } - } - val songIds by remember(artist, allSongIds) { - derivedStateOf { artist?.getSongIds(context.symphony) ?: listOf() } - } - val albumIds by remember(artist, allAlbumIds) { - derivedStateOf { artist?.getAlbumIds(context.symphony) ?: listOf() } - } - val isViable by remember(allArtistNames) { - derivedStateOf { allArtistNames.contains(route.artistName) } + val artistFlow = context.symphony.groove.artist.findByIdAsFlow(route.artistId) + val artist by artistFlow.collectAsStateWithLifecycle(null) + val albums by context.symphony.groove.artist.findAlbumsOfIdAsFlow(route.artistId) + .collectAsStateWithLifecycle(emptyList()) + val songsSortBy by context.symphony.settings.lastUsedArtistSongsSortBy.flow.collectAsStateWithLifecycle() + val songsSortReverse by context.symphony.settings.lastUsedArtistSongsSortReverse.flow.collectAsStateWithLifecycle() + val songsFlow = artistFlow.transformLatest { artist -> + val value = when { + artist == null -> emptyFlow() + else -> context.symphony.groove.artist.findSongsByIdAsFlow( + artist.entity.id, + songsSortBy, + songsSortReverse, + ) + } + emitAll(value) } + val songs by songsFlow.collectAsStateWithLifecycle(emptyList()) Scaffold( modifier = Modifier.fillMaxSize(), @@ -73,7 +78,7 @@ fun ArtistView(context: ViewContext, route: ArtistViewRoute) { title = { TopAppBarMinimalTitle { Text( - context.symphony.t.Artist + (artist?.let { " - ${it.name}" } ?: ""), + "${context.symphony.t.Artist} - ${artist?.entity?.name ?: context.symphony.t.UnknownSymbol}", maxLines = 2, overflow = TextOverflow.Ellipsis, ) @@ -93,25 +98,29 @@ fun ArtistView(context: ViewContext, route: ArtistViewRoute) { .padding(contentPadding) .fillMaxSize() ) { - if (isViable) { - SongList( + when { + artist != null -> SongList( context, - songIds = songIds, + songs = songs, + sortBy = songsSortBy, + sortReverse = songsSortReverse, leadingContent = { item { ArtistHero(context, artist!!) } - if (albumIds.isNotEmpty()) { + if (albums.isNotEmpty()) { item { Spacer(modifier = Modifier.height(4.dp)) - AlbumRow(context, albumIds) + AlbumRow(context, albums) Spacer(modifier = Modifier.height(4.dp)) HorizontalDivider() } } } ) - } else UnknownArtist(context, route.artistName) + + else -> UnknownArtist(context, route.artistId) + } } }, bottomBar = { @@ -121,9 +130,14 @@ fun ArtistView(context: ViewContext, route: ArtistViewRoute) { } @Composable -private fun ArtistHero(context: ViewContext, artist: Artist) { +private fun ArtistHero(context: ViewContext, artist: Artist.AlongAttributes) { + val artworks by context.symphony.groove.artist.getTop4ArtworkUriAsFlow(artist.entity.id) + .collectAsStateWithLifecycle(emptyList()) + GenericGrooveBanner( - image = artist.createArtworkImageRequest(context.symphony).build(), + image = { constraints -> + GenericGrooveBannerQuadImage(context, artworks, constraints) + }, options = { expanded, onDismissRequest -> ArtistDropdownMenu( context, @@ -133,13 +147,13 @@ private fun ArtistHero(context: ViewContext, artist: Artist) { ) }, content = { - Text(artist.name) + Text(artist.entity.name) } ) } @Composable -private fun UnknownArtist(context: ViewContext, artistName: String) { +private fun UnknownArtist(context: ViewContext, artistId: String) { IconTextBody( icon = { modifier -> Icon( @@ -149,7 +163,7 @@ private fun UnknownArtist(context: ViewContext, artistName: String) { ) }, content = { - Text(context.symphony.t.UnknownArtistX(artistName)) + Text(context.symphony.t.UnknownArtistX(artistId)) } ) } diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/view/Base.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/view/Base.kt index b005886d..84c679b1 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/view/Base.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/view/Base.kt @@ -71,9 +71,6 @@ fun BaseView(symphony: Symphony, activity: MainActivity) { baseComposable { SearchView(context, it.toRoute()) } - baseComposable { - AlbumArtistView(context, it.toRoute()) - } baseComposable { GenreView(context, it.toRoute()) } diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/view/Genre.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/view/Genre.kt index e3c485ef..4fcab52e 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/view/Genre.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/view/Genre.kt @@ -15,8 +15,6 @@ import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -29,25 +27,34 @@ import io.github.zyrouge.symphony.ui.components.IconTextBody import io.github.zyrouge.symphony.ui.components.SongList import io.github.zyrouge.symphony.ui.components.TopAppBarMinimalTitle import io.github.zyrouge.symphony.ui.helpers.ViewContext +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.emitAll +import kotlinx.coroutines.flow.emptyFlow +import kotlinx.coroutines.flow.transformLatest import kotlinx.serialization.Serializable @Serializable -data class GenreViewRoute(val genreName: String) +data class GenreViewRoute(val genreId: String) -@OptIn(ExperimentalMaterial3Api::class) +@OptIn(ExperimentalMaterial3Api::class, ExperimentalCoroutinesApi::class) @Composable fun GenreView(context: ViewContext, route: GenreViewRoute) { - val allGenreNames by context.symphony.groove.genre.all.collectAsState() - val allSongIds by context.symphony.groove.song.all.collectAsState() - val genre by remember(allGenreNames) { - derivedStateOf { context.symphony.groove.genre.get(route.genreName) } - } - val songIds by remember(genre, allSongIds) { - derivedStateOf { genre?.getSongIds(context.symphony) ?: listOf() } - } - val isViable by remember(allGenreNames) { - derivedStateOf { allGenreNames.contains(route.genreName) } + val genreFlow = context.symphony.groove.genre.findByIdAsFlow(route.genreId) + val genre by genreFlow.collectAsStateWithLifecycle(null) + val songsSortBy by context.symphony.settings.lastUsedSongsSortBy.flow.collectAsStateWithLifecycle() + val songsSortReverse by context.symphony.settings.lastUsedSongsSortReverse.flow.collectAsStateWithLifecycle() + val songsFlow = genreFlow.transformLatest { genre -> + val value = when { + genre == null -> emptyFlow() + else -> context.symphony.groove.genre.findSongsByIdAsFlow( + genre.entity.id, + songsSortBy, + songsSortReverse, + ) + } + emitAll(value) } + val songs by songsFlow.collectAsStateWithLifecycle(emptyList()) Scaffold( modifier = Modifier.fillMaxSize(), @@ -62,8 +69,7 @@ fun GenreView(context: ViewContext, route: GenreViewRoute) { }, title = { TopAppBarMinimalTitle { - Text(context.symphony.t.Genre - + (genre?.let { " - ${it.name}" } ?: "")) + Text("${context.symphony.t.Genre} - ${genre?.entity?.name ?: context.symphony.t.UnknownSymbol}") } }, actions = { @@ -77,7 +83,7 @@ fun GenreView(context: ViewContext, route: GenreViewRoute) { Icon(Icons.Filled.MoreVert, null) GenericSongListDropdown( context, - songIds = songIds, + songIds = songs.map { it.id }, expanded = showOptionsMenu, onDismissRequest = { showOptionsMenu = false @@ -97,8 +103,14 @@ fun GenreView(context: ViewContext, route: GenreViewRoute) { .fillMaxSize() ) { when { - isViable -> SongList(context, songIds = songIds) - else -> UnknownGenre(context, route.genreName) + genre != null -> SongList( + context, + songs = songs, + sortBy = songsSortBy, + sortReverse = songsSortReverse, + ) + + else -> UnknownGenre(context, route.genreId) } } }, @@ -109,7 +121,7 @@ fun GenreView(context: ViewContext, route: GenreViewRoute) { } @Composable -private fun UnknownGenre(context: ViewContext, genre: String) { +private fun UnknownGenre(context: ViewContext, genreId: String) { IconTextBody( icon = { modifier -> Icon( @@ -119,7 +131,7 @@ private fun UnknownGenre(context: ViewContext, genre: String) { ) }, content = { - Text(context.symphony.t.UnknownGenreX(genre)) + Text(context.symphony.t.UnknownGenreX(genreId)) } ) } diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/view/Home.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/view/Home.kt index 7f07de37..0420a540 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/view/Home.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/view/Home.kt @@ -58,7 +58,6 @@ import androidx.compose.material3.Text import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -168,10 +167,10 @@ object HomeViewRoute @OptIn(ExperimentalMaterial3Api::class) @Composable fun HomeView(context: ViewContext) { - val readIntroductoryMessage by context.symphony.settings.readIntroductoryMessage.flow.collectAsState() - val tabs by context.symphony.settings.homeTabs.flow.collectAsState() - val labelVisibility by context.symphony.settings.homePageBottomBarLabelVisibility.flow.collectAsState() - val currentTab by context.symphony.settings.lastHomeTab.flow.collectAsState() + val readIntroductoryMessage by context.symphony.settings.readIntroductoryMessage.flow.collectAsStateWithLifecycle() + val tabs by context.symphony.settings.homeTabs.flow.collectAsStateWithLifecycle() + val labelVisibility by context.symphony.settings.homePageBottomBarLabelVisibility.flow.collectAsStateWithLifecycle() + val currentTab by context.symphony.settings.lastHomeTab.flow.collectAsStateWithLifecycle() var showOptionsDropdown by remember { mutableStateOf(false) } var showTabsSheet by remember { mutableStateOf(false) } diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/view/Lyrics.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/view/Lyrics.kt index d2ed1434..a7c4917f 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/view/Lyrics.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/view/Lyrics.kt @@ -20,7 +20,6 @@ import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color @@ -44,7 +43,7 @@ object LyricsViewRoute @OptIn(ExperimentalMaterial3Api::class) @Composable fun LyricsView(context: ViewContext) { - val keepScreenAwake by context.symphony.settings.lyricsKeepScreenAwake.flow.collectAsState() + val keepScreenAwake by context.symphony.settings.lyricsKeepScreenAwake.flow.collectAsStateWithLifecycle() if (keepScreenAwake) { KeepScreenAwake() @@ -70,10 +69,7 @@ fun LyricsView(context: ViewContext) { }, title = { TopAppBarMinimalTitle { - Text( - context.symphony.t.Lyrics + - (data?.song?.title?.let { " - $it" } ?: "") - ) + Text("${context.symphony.t.Lyrics} - ${data?.song?.title ?: context.symphony.t.UnknownSymbol}") } }, actions = { diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/view/NowPlaying.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/view/NowPlaying.kt index 8646a476..3e92591c 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/view/NowPlaying.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/view/NowPlaying.kt @@ -2,11 +2,10 @@ package io.github.zyrouge.symphony.ui.view import androidx.compose.runtime.Composable import androidx.compose.runtime.Immutable -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.remember -import io.github.zyrouge.symphony.services.groove.Song +import io.github.zyrouge.symphony.services.groove.entities.Song import io.github.zyrouge.symphony.services.radio.RadioQueue import io.github.zyrouge.symphony.ui.helpers.ViewContext import io.github.zyrouge.symphony.ui.view.nowPlaying.NothingPlaying @@ -73,8 +72,8 @@ fun NowPlayingObserver( context: ViewContext, content: @Composable (NowPlayingData?) -> Unit, ) { - val queue by context.symphony.radio.observatory.queue.collectAsState() - val queueIndex by context.symphony.radio.observatory.queueIndex.collectAsState() + val queue by context.symphony.radio.observatory.queue.collectAsStateWithLifecycle() + val queueIndex by context.symphony.radio.observatory.queueIndex.collectAsStateWithLifecycle() val song by remember(queue, queueIndex) { derivedStateOf { queue.getOrNull(queueIndex)?.let { context.symphony.groove.song.get(it) } @@ -84,21 +83,21 @@ fun NowPlayingObserver( derivedStateOf { song != null } } - val isPlaying by context.symphony.radio.observatory.isPlaying.collectAsState() - val currentLoopMode by context.symphony.radio.observatory.loopMode.collectAsState() - val currentShuffleMode by context.symphony.radio.observatory.shuffleMode.collectAsState() - val currentSpeed by context.symphony.radio.observatory.speed.collectAsState() - val currentPitch by context.symphony.radio.observatory.pitch.collectAsState() - val persistedSpeed by context.symphony.radio.observatory.persistedSpeed.collectAsState() - val persistedPitch by context.symphony.radio.observatory.persistedPitch.collectAsState() - val sleepTimer by context.symphony.radio.observatory.sleepTimer.collectAsState() - val pauseOnCurrentSongEnd by context.symphony.radio.observatory.pauseOnCurrentSongEnd.collectAsState() - val showSongAdditionalInfo by context.symphony.settings.nowPlayingAdditionalInfo.flow.collectAsState() - val enableSeekControls by context.symphony.settings.nowPlayingSeekControls.flow.collectAsState() - val seekBackDuration by context.symphony.settings.seekBackDuration.flow.collectAsState() - val seekForwardDuration by context.symphony.settings.seekForwardDuration.flow.collectAsState() - val controlsLayout by context.symphony.settings.nowPlayingControlsLayout.flow.collectAsState() - val lyricsLayout by context.symphony.settings.nowPlayingLyricsLayout.flow.collectAsState() + val isPlaying by context.symphony.radio.observatory.isPlaying.collectAsStateWithLifecycle() + val currentLoopMode by context.symphony.radio.observatory.loopMode.collectAsStateWithLifecycle() + val currentShuffleMode by context.symphony.radio.observatory.shuffleMode.collectAsStateWithLifecycle() + val currentSpeed by context.symphony.radio.observatory.speed.collectAsStateWithLifecycle() + val currentPitch by context.symphony.radio.observatory.pitch.collectAsStateWithLifecycle() + val persistedSpeed by context.symphony.radio.observatory.persistedSpeed.collectAsStateWithLifecycle() + val persistedPitch by context.symphony.radio.observatory.persistedPitch.collectAsStateWithLifecycle() + val sleepTimer by context.symphony.radio.observatory.sleepTimer.collectAsStateWithLifecycle() + val pauseOnCurrentSongEnd by context.symphony.radio.observatory.pauseOnCurrentSongEnd.collectAsStateWithLifecycle() + val showSongAdditionalInfo by context.symphony.settings.nowPlayingAdditionalInfo.flow.collectAsStateWithLifecycle() + val enableSeekControls by context.symphony.settings.nowPlayingSeekControls.flow.collectAsStateWithLifecycle() + val seekBackDuration by context.symphony.settings.seekBackDuration.flow.collectAsStateWithLifecycle() + val seekForwardDuration by context.symphony.settings.seekForwardDuration.flow.collectAsStateWithLifecycle() + val controlsLayout by context.symphony.settings.nowPlayingControlsLayout.flow.collectAsStateWithLifecycle() + val lyricsLayout by context.symphony.settings.nowPlayingLyricsLayout.flow.collectAsStateWithLifecycle() val data = when { isViable -> NowPlayingData( diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/view/Playlist.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/view/Playlist.kt index cef2d1a5..d40588a9 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/view/Playlist.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/view/Playlist.kt @@ -17,54 +17,49 @@ import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import io.github.zyrouge.symphony.services.groove.repositories.PlaylistRepository import io.github.zyrouge.symphony.ui.components.AnimatedNowPlayingBottomBar import io.github.zyrouge.symphony.ui.components.IconTextBody import io.github.zyrouge.symphony.ui.components.PlaylistDropdownMenu import io.github.zyrouge.symphony.ui.components.SongList -import io.github.zyrouge.symphony.ui.components.SongListType import io.github.zyrouge.symphony.ui.components.TopAppBarMinimalTitle import io.github.zyrouge.symphony.ui.helpers.ViewContext import io.github.zyrouge.symphony.ui.theme.ThemeColors -import io.github.zyrouge.symphony.utils.mutate +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.emitAll +import kotlinx.coroutines.flow.emptyFlow +import kotlinx.coroutines.flow.transformLatest import kotlinx.serialization.Serializable @Serializable data class PlaylistViewRoute(val playlistId: String) -@OptIn(ExperimentalMaterial3Api::class) +@OptIn(ExperimentalMaterial3Api::class, ExperimentalCoroutinesApi::class) @Composable fun PlaylistView(context: ViewContext, route: PlaylistViewRoute) { - val allPlaylistIds by context.symphony.groove.playlist.all.collectAsState() - val updateId by context.symphony.groove.playlist.updateId.collectAsState() - var updateCounter by remember { mutableIntStateOf(0) } - val playlist by remember(route.playlistId, updateId) { - derivedStateOf { context.symphony.groove.playlist.get(route.playlistId) } - } - val songIds by remember(playlist) { - derivedStateOf { playlist?.getSongIds(context.symphony) ?: emptyList() } - } - val isViable by remember(allPlaylistIds, route.playlistId) { - derivedStateOf { allPlaylistIds.contains(route.playlistId) } - } - var showOptionsMenu by remember { mutableStateOf(false) } - val isFavoritesPlaylist by remember(playlist) { - derivedStateOf { - playlist?.let { context.symphony.groove.playlist.isFavoritesPlaylist(it) } == true + val playlistFlow = context.symphony.groove.playlist.findByIdAsFlow(route.playlistId) + val playlist by playlistFlow.collectAsStateWithLifecycle(null) + val songsSortBy by context.symphony.settings.lastUsedPlaylistSongsSortBy.flow.collectAsStateWithLifecycle() + val songsSortReverse by context.symphony.settings.lastUsedPlaylistSongsSortReverse.flow.collectAsStateWithLifecycle() + val songsFlow = playlistFlow.transformLatest { playlist -> + val value = when { + playlist == null -> emptyFlow() + else -> context.symphony.groove.playlist.findSongsByIdAsFlow( + playlist.entity.id, + songsSortBy, + songsSortReverse, + ) } + emitAll(value) } - - val incrementUpdateCounter = { - updateCounter = if (updateCounter > 25) 0 else updateCounter + 1 - } + val songs by songsFlow.collectAsStateWithLifecycle(emptyList()) + var showOptionsMenu by remember { mutableStateOf(false) } Scaffold( modifier = Modifier.fillMaxSize(), @@ -79,14 +74,11 @@ fun PlaylistView(context: ViewContext, route: PlaylistViewRoute) { }, title = { TopAppBarMinimalTitle { - Text( - context.symphony.t.Playlist - + (playlist?.let { " - ${it.title}" } ?: "") - ) + Text("${context.symphony.t.Playlist} - ${playlist?.entity?.title ?: context.symphony.t.UnknownSymbol}") } }, actions = { - if (isViable) { + if (playlist != null) { IconButton( onClick = { showOptionsMenu = true @@ -95,14 +87,9 @@ fun PlaylistView(context: ViewContext, route: PlaylistViewRoute) { Icon(Icons.Filled.MoreVert, null) PlaylistDropdownMenu( context, - playlist!!, + playlist = playlist!!, + songs = songs, expanded = showOptionsMenu, - onSongsChanged = { - incrementUpdateCounter() - }, - onRename = { - incrementUpdateCounter() - }, onDelete = { context.navController.popBackStack() }, @@ -125,33 +112,36 @@ fun PlaylistView(context: ViewContext, route: PlaylistViewRoute) { .fillMaxSize() ) { when { - isViable -> SongList( + playlist != null -> SongList( context, - songIds = songIds, - type = SongListType.Playlist, - disableHeartIcon = isFavoritesPlaylist, + songs = songs, + sortBy = songsSortBy, + sortReverse = songsSortReverse, + disableHeartIcon = playlist?.entity?.internalId == PlaylistRepository.PLAYLIST_INTERNAL_ID_FAVORITES, trailingOptionsContent = { _, song, onDismissRequest -> - playlist?.takeIf { it.isNotLocal }?.let { - DropdownMenuItem( - leadingIcon = { - Icon( - Icons.Filled.DeleteForever, - null, - tint = ThemeColors.Red, - ) - }, - text = { - Text(context.symphony.t.RemoveFromPlaylist) - }, - onClick = { - onDismissRequest() - context.symphony.groove.playlist.update( - it.id, - songIds.mutate { remove(song.id) }, - ) - } - ) - } + playlist + ?.takeIf { !it.entity.isModifiable } + ?.let { + DropdownMenuItem( + leadingIcon = { + Icon( + Icons.Filled.DeleteForever, + null, + tint = ThemeColors.Red, + ) + }, + text = { + Text(context.symphony.t.RemoveFromPlaylist) + }, + onClick = { + onDismissRequest() + context.symphony.groove.playlist.removeSongs( + it.entity.id, + listOf(song.id), + ) + } + ) + } }, ) diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/view/Queue.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/view/Queue.kt index 9c0a07d6..4f948eab 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/view/Queue.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/view/Queue.kt @@ -26,7 +26,6 @@ import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateOf @@ -53,8 +52,8 @@ object QueueViewRoute @Composable fun QueueView(context: ViewContext) { val coroutineScope = rememberCoroutineScope() - val queue by context.symphony.radio.observatory.queue.collectAsState() - val queueIndex by context.symphony.radio.observatory.queueIndex.collectAsState() + val queue by context.symphony.radio.observatory.queue.collectAsStateWithLifecycle() + val queueIndex by context.symphony.radio.observatory.queueIndex.collectAsStateWithLifecycle() val selectedSongIndices = remember { mutableStateListOf() } val listState = rememberLazyListState( initialFirstVisibleItemIndex = queueIndex, diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/view/home/AlbumArtists.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/view/home/AlbumArtists.kt index 7e53b503..6ec05645 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/view/home/AlbumArtists.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/view/home/AlbumArtists.kt @@ -1,7 +1,6 @@ package io.github.zyrouge.symphony.ui.view.home import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import io.github.zyrouge.symphony.ui.components.AlbumArtistGrid import io.github.zyrouge.symphony.ui.components.LoaderScaffold @@ -12,12 +11,13 @@ import kotlinx.coroutines.flow.mapLatest @OptIn(ExperimentalCoroutinesApi::class) @Composable fun AlbumArtistsView(context: ViewContext) { - val isUpdating by context.symphony.groove.exposer.isUpdating.collectAsState() - val sortBy by context.symphony.settings.lastUsedAlbumArtistsSortBy.flow.collectAsState() - val sortReverse by context.symphony.settings.lastUsedAlbumArtistsSortReverse.flow.collectAsState() - val albumArtists by context.symphony.groove.albumArtist.valuesAsFlow(sortBy, sortReverse) - .mapLatest { it.map { x -> x.artist } } - .collectAsState(emptyList()) + val isUpdating by context.symphony.groove.exposer.isUpdating.collectAsStateWithLifecycle() + val sortBy by context.symphony.settings.lastUsedAlbumArtistsSortBy.flow.collectAsStateWithLifecycle() + val sortReverse by context.symphony.settings.lastUsedAlbumArtistsSortReverse.flow.collectAsStateWithLifecycle() + val albumArtists by context.symphony.groove.artist + .valuesAsFlow(sortBy, sortReverse, onlyAlbumArtists = true) + .mapLatest { it.map { x -> x.entity } } + .collectAsStateWithLifecycle(emptyList()) LoaderScaffold(context, isLoading = isUpdating) { AlbumArtistGrid( diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/view/home/Albums.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/view/home/Albums.kt index 7dbb31fa..38a2f1db 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/view/home/Albums.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/view/home/Albums.kt @@ -1,7 +1,6 @@ package io.github.zyrouge.symphony.ui.view.home import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import io.github.zyrouge.symphony.ui.components.AlbumGrid import io.github.zyrouge.symphony.ui.components.LoaderScaffold @@ -12,12 +11,12 @@ import kotlinx.coroutines.flow.mapLatest @OptIn(ExperimentalCoroutinesApi::class) @Composable fun AlbumsView(context: ViewContext) { - val isUpdating by context.symphony.groove.exposer.isUpdating.collectAsState() - val sortBy by context.symphony.settings.lastUsedAlbumsSortBy.flow.collectAsState() - val sortReverse by context.symphony.settings.lastUsedAlbumsSortReverse.flow.collectAsState() + val isUpdating by context.symphony.groove.exposer.isUpdating.collectAsStateWithLifecycle() + val sortBy by context.symphony.settings.lastUsedAlbumsSortBy.flow.collectAsStateWithLifecycle() + val sortReverse by context.symphony.settings.lastUsedAlbumsSortReverse.flow.collectAsStateWithLifecycle() val albums by context.symphony.groove.album.valuesAsFlow(sortBy, sortReverse) - .mapLatest { it.map { x -> x.album } } - .collectAsState(emptyList()) + .mapLatest { it.map { x -> x.entity } } + .collectAsStateWithLifecycle(emptyList()) LoaderScaffold(context, isLoading = isUpdating) { AlbumGrid( diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/view/home/Artists.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/view/home/Artists.kt index 4b80e229..d710a57c 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/view/home/Artists.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/view/home/Artists.kt @@ -1,7 +1,6 @@ package io.github.zyrouge.symphony.ui.view.home import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import io.github.zyrouge.symphony.ui.components.ArtistGrid import io.github.zyrouge.symphony.ui.components.LoaderScaffold @@ -12,12 +11,12 @@ import kotlinx.coroutines.flow.mapLatest @OptIn(ExperimentalCoroutinesApi::class) @Composable fun ArtistsView(context: ViewContext) { - val isUpdating by context.symphony.groove.exposer.isUpdating.collectAsState() - val sortBy by context.symphony.settings.lastUsedArtistsSortBy.flow.collectAsState() - val sortReverse by context.symphony.settings.lastUsedArtistsSortReverse.flow.collectAsState() + val isUpdating by context.symphony.groove.exposer.isUpdating.collectAsStateWithLifecycle() + val sortBy by context.symphony.settings.lastUsedArtistsSortBy.flow.collectAsStateWithLifecycle() + val sortReverse by context.symphony.settings.lastUsedArtistsSortReverse.flow.collectAsStateWithLifecycle() val artists by context.symphony.groove.artist.valuesAsFlow(sortBy, sortReverse) - .mapLatest { it.map { x -> x.artist } } - .collectAsState(emptyList()) + .mapLatest { it.map { x -> x.entity } } + .collectAsStateWithLifecycle(emptyList()) LoaderScaffold(context, isLoading = isUpdating) { ArtistGrid( diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/view/home/Browser.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/view/home/Browser.kt index f1ffe09f..217987a0 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/view/home/Browser.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/view/home/Browser.kt @@ -1,28 +1,24 @@ package io.github.zyrouge.symphony.ui.view.home import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import io.github.zyrouge.symphony.ui.components.LoaderScaffold -import io.github.zyrouge.symphony.ui.components.SongExplorerList import io.github.zyrouge.symphony.ui.helpers.ViewContext -import io.github.zyrouge.symphony.utils.SimplePath @Composable fun BrowserView(context: ViewContext) { - val isUpdating by context.symphony.groove.exposer.isUpdating.collectAsState() - val explorer = context.symphony.groove.song.explorer - val lastUsedFolderPath by context.symphony.settings.lastUsedBrowserPath.flow.collectAsState() + val isUpdating by context.symphony.groove.exposer.isUpdating.collectAsStateWithLifecycle() + val lastUsedFolderPath by context.symphony.settings.lastUsedBrowserPath.flow.collectAsStateWithLifecycle() LoaderScaffold(context, isLoading = isUpdating) { - SongExplorerList( - context, - initialPath = lastUsedFolderPath?.let { SimplePath(it) }, - key = id, - explorer = explorer, - onPathChange = { path -> - context.symphony.settings.lastUsedBrowserPath.setValue(path.pathString) - } - ) +// SongExplorerList( +// context, +// initialPath = lastUsedFolderPath?.let { SimplePath(it) }, +// key = id, +// explorer = explorer, +// onPathChange = { path -> +// context.symphony.settings.lastUsedBrowserPath.setValue(path.pathString) +// } +// ) } } diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/view/home/Folders.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/view/home/Folders.kt index 7d69f37a..dfcc92b7 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/view/home/Folders.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/view/home/Folders.kt @@ -1,350 +1,75 @@ package io.github.zyrouge.symphony.ui.view.home -import androidx.activity.compose.BackHandler -import androidx.compose.animation.AnimatedContent -import androidx.compose.animation.togetherWith -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.lazy.grid.itemsIndexed -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.filled.PlaylistAdd -import androidx.compose.material.icons.automirrored.filled.PlaylistPlay -import androidx.compose.material.icons.filled.FolderCopy -import androidx.compose.material3.DropdownMenu -import androidx.compose.material3.DropdownMenuItem -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.HorizontalDivider -import androidx.compose.material3.Icon -import androidx.compose.material3.LocalContentColor -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.unit.dp -import io.github.zyrouge.symphony.services.groove.Groove -import io.github.zyrouge.symphony.ui.components.AddToPlaylistDialog -import io.github.zyrouge.symphony.ui.components.IconTextBody +import io.github.zyrouge.symphony.services.database.store.valuesAsFlow import io.github.zyrouge.symphony.ui.components.LoaderScaffold -import io.github.zyrouge.symphony.ui.components.MediaSortBar -import io.github.zyrouge.symphony.ui.components.MediaSortBarScaffold -import io.github.zyrouge.symphony.ui.components.ResponsiveGrid -import io.github.zyrouge.symphony.ui.components.ResponsiveGridColumns -import io.github.zyrouge.symphony.ui.components.ResponsiveGridSizeAdjustBottomSheet -import io.github.zyrouge.symphony.ui.components.SongList -import io.github.zyrouge.symphony.ui.components.SquareGrooveTile -import io.github.zyrouge.symphony.ui.components.label -import io.github.zyrouge.symphony.ui.helpers.Assets -import io.github.zyrouge.symphony.ui.helpers.FadeTransition -import io.github.zyrouge.symphony.ui.helpers.SlideTransition import io.github.zyrouge.symphony.ui.helpers.ViewContext -import io.github.zyrouge.symphony.ui.view.nowPlaying.defaultHorizontalPadding -import io.github.zyrouge.symphony.utils.SimpleFileSystem -import io.github.zyrouge.symphony.utils.StringListUtils -import java.util.Stack @Composable fun FoldersView(context: ViewContext) { - val isUpdating by context.symphony.groove.song.isUpdating.collectAsState() - val id by context.symphony.groove.song.id.collectAsState() - val explorer = context.symphony.groove.song.explorer - - val folders = remember(id) { - val entities = mutableMapOf() - val stack = Stack() - stack.add(explorer) - while (stack.isNotEmpty()) { - val current = stack.pop() - if (current.isEmpty) continue - var hasSongs = false - current.children.values.forEach { - when (it) { - is SimpleFileSystem.Folder -> stack.push(it) - is SimpleFileSystem.File -> { - hasSongs = true - } - } - } - if (hasSongs) { - entities[current.fullPath.pathString] = current - } - } - entities.toMap() - } - var currentFolder by remember(id) { - mutableStateOf(null) - } - - BackHandler(currentFolder != null) { - currentFolder = null - } + val isUpdating by context.symphony.groove.exposer.isUpdating.collectAsStateWithLifecycle() + val sortBy by context.symphony.settings.lastUsedFoldersSortBy.flow.collectAsStateWithLifecycle() + val sortReverse by context.symphony.settings.lastUsedFoldersSortReverse.flow.collectAsStateWithLifecycle() + val folders by context.symphony.database.mediaTreeFolders.valuesAsFlow(sortBy, sortReverse) + .collectAsStateWithLifecycle(emptyList()) LoaderScaffold(context, isLoading = isUpdating) { - AnimatedContent( - label = "folders-view-content", - targetState = currentFolder, - transitionSpec = { - val enter = when { - targetState != null -> SlideTransition.slideUp.enterTransition() - else -> FadeTransition.enterTransition() - } - enter.togetherWith(FadeTransition.exitTransition()) - }, - ) { folder -> - if (folder != null) { - val songIds by remember(folder) { - derivedStateOf { - folder.children.values.mapNotNull { - when (it) { - is SimpleFileSystem.File -> it.data as String - else -> null - } - } - } - } - - Column { - Column( - modifier = Modifier.padding( - start = defaultHorizontalPadding, - end = defaultHorizontalPadding, - top = 4.dp, - bottom = 12.dp, - ), - ) { - folder.parent?.let { parent -> - Text( - "${parent.fullPath}/", - style = MaterialTheme.typography.bodyMedium.copy( - color = LocalContentColor.current.copy(alpha = 0.7f), - ), - ) - } - Text(folder.name, style = MaterialTheme.typography.bodyLarge) - } - HorizontalDivider() - SongList(context, songIds = songIds, songsCount = songIds.size) - } - } else { - FoldersGrid( - context, - folders = folders, - onClick = { - currentFolder = it - } - ) - } - } - } -} - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -private fun FoldersGrid( - context: ViewContext, - folders: Map, - onClick: (SimpleFileSystem.Folder) -> Unit, -) { - val sortBy by context.symphony.settings.lastUsedFoldersSortBy.flow.collectAsState() - val sortReverse by context.symphony.settings.lastUsedFoldersSortReverse.flow.collectAsState() - val sortedFolderNames by remember(folders, sortBy, sortReverse) { - derivedStateOf { - StringListUtils.sort(folders.keys.toList(), sortBy, sortReverse) - } - } - val horizontalGridColumns by context.symphony.settings.lastUsedFoldersHorizontalGridColumns.flow.collectAsState() - val verticalGridColumns by context.symphony.settings.lastUsedFoldersVerticalGridColumns.flow.collectAsState() - val gridColumns by remember(horizontalGridColumns, verticalGridColumns) { - derivedStateOf { - ResponsiveGridColumns(horizontalGridColumns, verticalGridColumns) - } - } - var showModifyLayoutSheet by remember { mutableStateOf(false) } - - MediaSortBarScaffold( - mediaSortBar = { - MediaSortBar( - context, - reverse = sortReverse, - onReverseChange = { - context.symphony.settings.lastUsedFoldersSortReverse.setValue(it) - }, - sort = sortBy, - sorts = StringListUtils.SortBy.entries - .associateWith { x -> ViewContext.parameterizedFn { x.label(context) } }, - onSortChange = { - context.symphony.settings.lastUsedFoldersSortBy.setValue(it) - }, - label = { - Text(context.symphony.t.XFolders(folders.size.toString())) - }, - onShowModifyLayout = { - showModifyLayoutSheet = true - } - ) - }, - content = { - when { - sortedFolderNames.isEmpty() -> IconTextBody( - icon = { modifier -> - Icon( - Icons.Filled.FolderCopy, - null, - modifier = modifier, - ) - }, - content = { Text(context.symphony.t.DamnThisIsSoEmpty) } - ) - - else -> ResponsiveGrid(gridColumns) { - itemsIndexed( - sortedFolderNames, - key = { i, x -> "$i-$x" }, - contentType = { _, _ -> Groove.Kind.ARTIST } - ) { _, folderName -> - folders[folderName]?.let { folder -> - FolderTile( - context, folder = folder, - onClick = { onClick(folder) }, - ) - } - } - } - } - - if (showModifyLayoutSheet) { - ResponsiveGridSizeAdjustBottomSheet( - context, - columns = gridColumns, - onColumnsChange = { - context.symphony.settings.lastUsedFoldersHorizontalGridColumns.setValue( - it.horizontal - ) - context.symphony.settings.lastUsedFoldersVerticalGridColumns.setValue( - it.vertical - ) - }, - onDismissRequest = { - showModifyLayoutSheet = false - } - ) - } - } - ) -} - -@Composable -private fun FolderTile( - context: ViewContext, - folder: SimpleFileSystem.Folder, - onClick: () -> Unit, -) { - SquareGrooveTile( - image = folder.createArtworkImageRequest(context).build(), - options = { expanded, onDismissRequest -> - var showAddToPlaylistDialog by remember { mutableStateOf(false) } - - DropdownMenu( - expanded = expanded, - onDismissRequest = onDismissRequest - ) { - DropdownMenuItem( - leadingIcon = { - Icon(Icons.AutoMirrored.Filled.PlaylistPlay, null) - }, - text = { - Text(context.symphony.t.ShufflePlay) - }, - onClick = { - onDismissRequest() - context.symphony.radio.shorty.playQueue( - folder.getSortedSongIds(context), - shuffle = true, - ) - } - ) - DropdownMenuItem( - leadingIcon = { - Icon(Icons.AutoMirrored.Filled.PlaylistPlay, null) - }, - text = { - Text(context.symphony.t.PlayNext) - }, - onClick = { - onDismissRequest() - context.symphony.radio.queue.add( - folder.getSortedSongIds(context), - context.symphony.radio.queue.currentSongIndex + 1 - ) - } - ) - DropdownMenuItem( - leadingIcon = { - Icon(Icons.AutoMirrored.Filled.PlaylistAdd, null) - }, - text = { - Text(context.symphony.t.AddToPlaylist) - }, - onClick = { - onDismissRequest() - showAddToPlaylistDialog = true - } - ) - } - - if (showAddToPlaylistDialog) { - AddToPlaylistDialog( - context, - songIds = folder.getSortedSongIds(context), - onDismissRequest = { - showAddToPlaylistDialog = false - } - ) - } - }, - content = { - Text( - folder.name, - style = MaterialTheme.typography.bodyMedium, - textAlign = TextAlign.Center, - maxLines = 2, - overflow = TextOverflow.Ellipsis, - ) - }, - onPlay = { - val sortedSongIds = folder.getSortedSongIds(context) - context.symphony.radio.shorty.playQueue(sortedSongIds) - }, - onClick = onClick, - ) -} - -private fun SimpleFileSystem.Folder.createArtworkImageRequest(context: ViewContext) = - children.values - .find { it is SimpleFileSystem.File } - ?.let { - val songId = (it as SimpleFileSystem.File).data as String - context.symphony.groove.song.createArtworkImageRequest(songId) - } - ?: Assets.createPlaceholderImageRequest(context.symphony) - -private fun SimpleFileSystem.Folder.getSortedSongIds(context: ViewContext): List { - val songIds = children.values.mapNotNull { - when (it) { - is SimpleFileSystem.File -> it.data as String - else -> null - } +// AnimatedContent( +// label = "folders-view-content", +// targetState = currentFolder, +// transitionSpec = { +// val enter = when { +// targetState != null -> SlideTransition.slideUp.enterTransition() +// else -> FadeTransition.enterTransition() +// } +// enter.togetherWith(FadeTransition.exitTransition()) +// }, +// ) { folder -> +// if (folder != null) { +// val songIds by remember(folder) { +// derivedStateOf { +// folder.children.values.mapNotNull { +// when (it) { +// is SimpleFileSystem.File -> it.data as String +// else -> null +// } +// } +// } +// } +// +// Column { +// Column( +// modifier = Modifier.padding( +// start = defaultHorizontalPadding, +// end = defaultHorizontalPadding, +// top = 4.dp, +// bottom = 12.dp, +// ), +// ) { +// folder.parent?.let { parent -> +// Text( +// "${parent.fullPath}/", +// style = MaterialTheme.typography.bodyMedium.copy( +// color = LocalContentColor.current.copy(alpha = 0.7f), +// ), +// ) +// } +// Text(folder.name, style = MaterialTheme.typography.bodyLarge) +// } +// HorizontalDivider() +// SongList(context, songIds = songIds, songsCount = songIds.size) +// } +// } else { +// FoldersGrid( +// context, +// folders = folders, +// onClick = { +// currentFolder = it +// } +// ) +// } +// } +// } } - return context.symphony.groove.song.sort( - songIds, - context.symphony.settings.lastUsedSongsSortBy.value, - context.symphony.settings.lastUsedSongsSortReverse.value, - ) } diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/view/home/ForYou.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/view/home/ForYou.kt index a146a0b3..7e27fe84 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/view/home/ForYou.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/view/home/ForYou.kt @@ -34,7 +34,6 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.ProvideTextStyle import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.remember @@ -68,16 +67,16 @@ enum class ForYou(val label: (context: ViewContext) -> String) { @OptIn(ExperimentalMaterial3Api::class) @Composable fun ForYouView(context: ViewContext) { - val albumArtistsIsUpdating by context.symphony.groove.albumArtist.isUpdating.collectAsState() - val albumsIsUpdating by context.symphony.groove.album.isUpdating.collectAsState() - val artistsIsUpdating by context.symphony.groove.artist.isUpdating.collectAsState() - val songsIsUpdating by context.symphony.groove.song.isUpdating.collectAsState() - val albumArtistNames by context.symphony.groove.albumArtist.all.collectAsState() - val albumIds by context.symphony.groove.album.all.collectAsState() - val artistNames by context.symphony.groove.artist.all.collectAsState() - val songIds by context.symphony.groove.song.all.collectAsState() - val sortBy by context.symphony.settings.lastUsedSongsSortBy.flow.collectAsState() - val sortReverse by context.symphony.settings.lastUsedSongsSortReverse.flow.collectAsState() + val albumArtistsIsUpdating by context.symphony.groove.albumArtist.isUpdating.collectAsStateWithLifecycle() + val albumsIsUpdating by context.symphony.groove.album.isUpdating.collectAsStateWithLifecycle() + val artistsIsUpdating by context.symphony.groove.artist.isUpdating.collectAsStateWithLifecycle() + val songsIsUpdating by context.symphony.groove.song.isUpdating.collectAsStateWithLifecycle() + val albumArtistNames by context.symphony.groove.albumArtist.all.collectAsStateWithLifecycle() + val albumIds by context.symphony.groove.album.all.collectAsStateWithLifecycle() + val artistNames by context.symphony.groove.artist.all.collectAsStateWithLifecycle() + val songIds by context.symphony.groove.song.all.collectAsStateWithLifecycle() + val sortBy by context.symphony.settings.lastUsedSongsSortBy.flow.collectAsStateWithLifecycle() + val sortReverse by context.symphony.settings.lastUsedSongsSortReverse.flow.collectAsStateWithLifecycle() when { songIds.isNotEmpty() -> { @@ -265,7 +264,7 @@ fun ForYouView(context: ViewContext) { } } } - val contents by context.symphony.settings.forYouContents.flow.collectAsState() + val contents by context.symphony.settings.forYouContents.flow.collectAsStateWithLifecycle() contents.forEach { when (it) { ForYou.Albums -> SuggestedAlbums( diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/view/home/Genres.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/view/home/Genres.kt index 4ae49cb8..a7dbde7f 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/view/home/Genres.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/view/home/Genres.kt @@ -1,19 +1,19 @@ package io.github.zyrouge.symphony.ui.view.home import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.lifecycle.compose.collectAsStateWithLifecycle import io.github.zyrouge.symphony.ui.components.GenreGrid import io.github.zyrouge.symphony.ui.components.LoaderScaffold import io.github.zyrouge.symphony.ui.helpers.ViewContext @Composable fun GenresView(context: ViewContext) { - val isUpdating by context.symphony.groove.exposer.isUpdating.collectAsState() - val sortBy by context.symphony.settings.lastUsedGenresSortBy.flow.collectAsState() - val sortReverse by context.symphony.settings.lastUsedGenresSortReverse.flow.collectAsState() + val isUpdating by context.symphony.groove.exposer.isUpdating.collectAsStateWithLifecycle() + val sortBy by context.symphony.settings.lastUsedGenresSortBy.flow.collectAsStateWithLifecycle() + val sortReverse by context.symphony.settings.lastUsedGenresSortReverse.flow.collectAsStateWithLifecycle() val attributedGenres by context.symphony.groove.genre.valuesAsFlow(sortBy, sortReverse) - .collectAsState(emptyList()) + .collectAsStateWithLifecycle(emptyList()) LoaderScaffold(context, isLoading = isUpdating) { GenreGrid( diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/view/home/Playlists.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/view/home/Playlists.kt index 5090471e..160eaac6 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/view/home/Playlists.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/view/home/Playlists.kt @@ -17,7 +17,6 @@ import androidx.compose.material3.ElevatedButton import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -25,8 +24,8 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp -import io.github.zyrouge.symphony.services.groove.Playlist import io.github.zyrouge.symphony.services.groove.entities.Playlist +import io.github.zyrouge.symphony.services.groove.repositories.PlaylistRepository import io.github.zyrouge.symphony.ui.components.LoaderScaffold import io.github.zyrouge.symphony.ui.components.NewPlaylistDialog import io.github.zyrouge.symphony.ui.components.PlaylistGrid @@ -39,12 +38,12 @@ import kotlinx.coroutines.flow.mapLatest @OptIn(ExperimentalCoroutinesApi::class) @Composable fun PlaylistsView(context: ViewContext) { - val isUpdating by context.symphony.groove.exposer.isUpdating.collectAsState() - val sortBy by context.symphony.settings.lastUsedPlaylistsSortBy.flow.collectAsState() - val sortReverse by context.symphony.settings.lastUsedPlaylistsSortReverse.flow.collectAsState() + val isUpdating by context.symphony.groove.exposer.isUpdating.collectAsStateWithLifecycle() + val sortBy by context.symphony.settings.lastUsedPlaylistsSortBy.flow.collectAsStateWithLifecycle() + val sortReverse by context.symphony.settings.lastUsedPlaylistsSortReverse.flow.collectAsStateWithLifecycle() val playlists by context.symphony.groove.playlist.valuesAsFlow(sortBy, sortReverse) - .mapLatest { it.map { x -> x.playlist } } - .collectAsState(emptyList()) + .mapLatest { it.map { x -> x.entity } } + .collectAsStateWithLifecycle(emptyList()) var showPlaylistCreator by remember { mutableStateOf(false) } val openPlaylistLauncher = rememberLauncherForActivityResult( @@ -53,8 +52,13 @@ fun PlaylistsView(context: ViewContext) { uris.forEach { x -> try { ActivityUtils.makePersistableReadableUri(context.symphony.applicationContext, x) - val playlist = Playlist.parse(context.symphony, null, x) - context.symphony.groove.playlist.add(playlist) + val id = context.symphony.database.playlistsIdGenerator.next() + val parsed = Playlist.parse(context.symphony, id, x) + val addOptions = PlaylistRepository.AddOptions( + playlist = parsed.playlist, + songPaths = parsed.songPaths, + ) + context.symphony.groove.playlist.add(addOptions) } catch (err: Exception) { Logger.error("PlaylistView", "import failed (activity result)", err) Toast.makeText( @@ -90,9 +94,9 @@ fun PlaylistsView(context: ViewContext) { if (showPlaylistCreator) { NewPlaylistDialog( context, - onDone = { playlist -> + onDone = { addOptions -> showPlaylistCreator = false - context.symphony.groove.playlist.add(playlist) + context.symphony.groove.playlist.add(addOptions) }, onDismissRequest = { showPlaylistCreator = false diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/view/home/Songs.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/view/home/Songs.kt index 282b3fcd..4d1b09ce 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/view/home/Songs.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/view/home/Songs.kt @@ -1,23 +1,20 @@ package io.github.zyrouge.symphony.ui.view.home import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import io.github.zyrouge.symphony.ui.components.LoaderScaffold import io.github.zyrouge.symphony.ui.components.SongList import io.github.zyrouge.symphony.ui.helpers.ViewContext import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.transformLatest @OptIn(ExperimentalCoroutinesApi::class) @Composable fun SongsView(context: ViewContext) { - val isUpdating by context.symphony.groove.exposer.isUpdating.collectAsState() - val sortBy by context.symphony.settings.lastUsedSongsSortBy.flow.collectAsState() - val sortReverse by context.symphony.settings.lastUsedSongsSortReverse.flow.collectAsState() + val isUpdating by context.symphony.groove.exposer.isUpdating.collectAsStateWithLifecycle() + val sortBy by context.symphony.settings.lastUsedSongsSortBy.flow.collectAsStateWithLifecycle() + val sortReverse by context.symphony.settings.lastUsedSongsSortReverse.flow.collectAsStateWithLifecycle() val songs by context.symphony.groove.song.valuesAsFlow(sortBy, sortReverse) - .transformLatest { emit(it.map { x -> x.song }) } - .collectAsState(emptyList()) + .collectAsStateWithLifecycle(emptyList()) LoaderScaffold(context, isLoading = isUpdating) { SongList( diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/view/home/Tree.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/view/home/Tree.kt index 88f2a0e3..1f36254b 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/view/home/Tree.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/view/home/Tree.kt @@ -1,28 +1,24 @@ package io.github.zyrouge.symphony.ui.view.home import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue -import io.github.zyrouge.symphony.ui.components.LoaderScaffold -import io.github.zyrouge.symphony.ui.components.SongTreeList +import androidx.lifecycle.compose.collectAsStateWithLifecycle import io.github.zyrouge.symphony.ui.helpers.ViewContext @Composable fun TreeView(context: ViewContext) { - val isUpdating by context.symphony.groove.song.isUpdating.collectAsState() - val songIds by context.symphony.groove.song.all.collectAsState() - val songsCount by context.symphony.groove.song.count.collectAsState() - val disabledTreePaths by context.symphony.settings.lastDisabledTreePaths.flow.collectAsState() + val isUpdating by context.symphony.groove.exposer.isUpdating.collectAsStateWithLifecycle() +// val disabledTreePaths by context.symphony.settings.lastDisabledTreePaths.flow.collectAsStateWithLifecycle() - LoaderScaffold(context, isLoading = isUpdating) { - SongTreeList( - context, - songIds = songIds, - songsCount = songsCount, - initialDisabled = disabledTreePaths.toList(), - onDisable = { paths -> - context.symphony.settings.lastDisabledTreePaths.setValue(paths.toSet()) - }, - ) - } +// LoaderScaffold(context, isLoading = isUpdating) { +// SongTreeList( +// context, +// songIds = songIds, +// songsCount = songsCount, +// initialDisabled = disabledTreePaths.toList(), +// onDisable = { paths -> +// context.symphony.settings.lastDisabledTreePaths.setValue(paths.toSet()) +// }, +// ) +// } } diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/view/nowPlaying/BodyContent.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/view/nowPlaying/BodyContent.kt index 68e36687..f7661ad2 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/view/nowPlaying/BodyContent.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/view/nowPlaying/BodyContent.kt @@ -36,7 +36,6 @@ import androidx.compose.material3.LocalContentColor import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableFloatStateOf @@ -62,7 +61,7 @@ import io.github.zyrouge.symphony.utils.DurationUtils @OptIn(ExperimentalLayoutApi::class) @Composable fun NowPlayingBodyContent(context: ViewContext, data: NowPlayingData) { - val favoriteSongIds by context.symphony.groove.playlist.favorites.collectAsState() + val favoriteSongIds by context.symphony.groove.playlist.favorites.collectAsStateWithLifecycle() val isFavorite by remember(data) { derivedStateOf { favoriteSongIds.contains(data.song.id) } } @@ -289,7 +288,7 @@ fun NowPlayingTraditionalControls(context: ViewContext, data: NowPlayingData) { @Composable fun NowPlayingSeekBar(context: ViewContext) { - val playbackPosition by context.symphony.radio.observatory.playbackPosition.collectAsState() + val playbackPosition by context.symphony.radio.observatory.playbackPosition.collectAsStateWithLifecycle() Row( modifier = Modifier.padding(defaultHorizontalPadding, 0.dp), diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/view/nowPlaying/BodyCover.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/view/nowPlaying/BodyCover.kt index d6ae3eaf..1deb0a66 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/view/nowPlaying/BodyCover.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/view/nowPlaying/BodyCover.kt @@ -15,7 +15,6 @@ import androidx.compose.material3.LocalContentColor import androidx.compose.material3.LocalTextStyle import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -45,7 +44,7 @@ fun NowPlayingBodyCover( states: NowPlayingStates, orientation: ScreenOrientation, ) { - val showLyrics by states.showLyrics.collectAsState() + val showLyrics by states.showLyrics.collectAsStateWithLifecycle() Box(modifier = Modifier.padding(defaultHorizontalPadding, 0.dp)) { AnimatedContent( @@ -69,12 +68,12 @@ fun NowPlayingBodyCover( @Composable private fun NowPlayingBodyCoverLyrics(context: ViewContext, orientation: ScreenOrientation) { - val keepScreenAwake by context.symphony.settings.lyricsKeepScreenAwake.flow.collectAsState() + val keepScreenAwake by context.symphony.settings.lyricsKeepScreenAwake.flow.collectAsStateWithLifecycle() if (keepScreenAwake) { KeepScreenAwake() } - + Box( modifier = Modifier .fillMaxSize() diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/view/nowPlaying/BottomBar.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/view/nowPlaying/BottomBar.kt index ad354ec1..047e008d 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/view/nowPlaying/BottomBar.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/view/nowPlaying/BottomBar.kt @@ -36,7 +36,6 @@ import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -69,7 +68,7 @@ fun NowPlayingBodyBottomBar( context.symphony.radio.session.createEqualizerActivityContract() ) {} - val sleepTimer by context.symphony.radio.observatory.sleepTimer.collectAsState() + val sleepTimer by context.symphony.radio.observatory.sleepTimer.collectAsStateWithLifecycle() var showSleepTimerDialog by remember { mutableStateOf(false) } var showSpeedDialog by remember { mutableStateOf(false) } var showPitchDialog by remember { mutableStateOf(false) } @@ -107,7 +106,7 @@ fun NowPlayingBodyBottomBar( } Spacer(modifier = Modifier.weight(1f)) states.showLyrics.let { showLyricsState -> - val showLyrics by showLyricsState.collectAsState() + val showLyrics by showLyricsState.collectAsStateWithLifecycle() IconButton( onClick = { diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/view/settings/AppearanceSettingsView.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/view/settings/AppearanceSettingsView.kt index 682bc076..19bb31f5 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/view/settings/AppearanceSettingsView.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/view/settings/AppearanceSettingsView.kt @@ -24,7 +24,6 @@ import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color @@ -56,13 +55,13 @@ object AppearanceSettingsViewRoute @Composable fun AppearanceSettingsView(context: ViewContext) { val scrollState = rememberScrollState() - val language by context.symphony.settings.language.flow.collectAsState() - val fontFamily by context.symphony.settings.fontFamily.flow.collectAsState() - val themeMode by context.symphony.settings.themeMode.flow.collectAsState() - val useMaterialYou by context.symphony.settings.useMaterialYou.flow.collectAsState() - val primaryColor by context.symphony.settings.primaryColor.flow.collectAsState() - val fontScale by context.symphony.settings.fontScale.flow.collectAsState() - val contentScale by context.symphony.settings.contentScale.flow.collectAsState() + val language by context.symphony.settings.language.flow.collectAsStateWithLifecycle() + val fontFamily by context.symphony.settings.fontFamily.flow.collectAsStateWithLifecycle() + val themeMode by context.symphony.settings.themeMode.flow.collectAsStateWithLifecycle() + val useMaterialYou by context.symphony.settings.useMaterialYou.flow.collectAsStateWithLifecycle() + val primaryColor by context.symphony.settings.primaryColor.flow.collectAsStateWithLifecycle() + val fontScale by context.symphony.settings.fontScale.flow.collectAsStateWithLifecycle() + val contentScale by context.symphony.settings.contentScale.flow.collectAsStateWithLifecycle() Scaffold( modifier = Modifier.fillMaxSize(), diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/view/settings/GrooveSettingsView.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/view/settings/GrooveSettingsView.kt index 6e134311..6015b4d0 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/view/settings/GrooveSettingsView.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/view/settings/GrooveSettingsView.kt @@ -35,7 +35,6 @@ import androidx.compose.material3.Text import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope @@ -75,16 +74,16 @@ fun GrooveSettingsView(context: ViewContext, route: GrooveSettingsViewRoute) { val coroutineScope = rememberCoroutineScope() val snackbarHostState = remember { SnackbarHostState() } val scrollState = rememberScrollState() - val songsFilterPattern by context.symphony.settings.songsFilterPattern.flow.collectAsState() - val minSongDuration by context.symphony.settings.minSongDuration.flow.collectAsState() - val blacklistFolders by context.symphony.settings.blacklistFolders.flow.collectAsState() - val whitelistFolders by context.symphony.settings.whitelistFolders.flow.collectAsState() - val artistTagSeparators by context.symphony.settings.artistTagSeparators.flow.collectAsState() - val genreTagSeparators by context.symphony.settings.genreTagSeparators.flow.collectAsState() - val mediaFolders by context.symphony.settings.mediaFolders.flow.collectAsState() - val artworkQuality by context.symphony.settings.artworkQuality.flow.collectAsState() - val caseSensitiveSorting by context.symphony.settings.caseSensitiveSorting.flow.collectAsState() - val useMetaphony by context.symphony.settings.useMetaphony.flow.collectAsState() + val songsFilterPattern by context.symphony.settings.songsFilterPattern.flow.collectAsStateWithLifecycle() + val minSongDuration by context.symphony.settings.minSongDuration.flow.collectAsStateWithLifecycle() + val blacklistFolders by context.symphony.settings.blacklistFolders.flow.collectAsStateWithLifecycle() + val whitelistFolders by context.symphony.settings.whitelistFolders.flow.collectAsStateWithLifecycle() + val artistTagSeparators by context.symphony.settings.artistTagSeparators.flow.collectAsStateWithLifecycle() + val genreTagSeparators by context.symphony.settings.genreTagSeparators.flow.collectAsStateWithLifecycle() + val mediaFolders by context.symphony.settings.mediaFolders.flow.collectAsStateWithLifecycle() + val artworkQuality by context.symphony.settings.artworkQuality.flow.collectAsStateWithLifecycle() + val caseSensitiveSorting by context.symphony.settings.caseSensitiveSorting.flow.collectAsStateWithLifecycle() + val useMetaphony by context.symphony.settings.useMetaphony.flow.collectAsStateWithLifecycle() Scaffold( modifier = Modifier.fillMaxSize(), diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/view/settings/HomePageSettingsView.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/view/settings/HomePageSettingsView.kt index a98e76c0..0690f7cb 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/view/settings/HomePageSettingsView.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/view/settings/HomePageSettingsView.kt @@ -20,7 +20,6 @@ import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color @@ -43,9 +42,9 @@ object HomePageSettingsViewRoute @Composable fun HomePageSettingsView(context: ViewContext) { val scrollState = rememberScrollState() - val homeTabs by context.symphony.settings.homeTabs.flow.collectAsState() - val forYouContents by context.symphony.settings.forYouContents.flow.collectAsState() - val homePageBottomBarLabelVisibility by context.symphony.settings.homePageBottomBarLabelVisibility.flow.collectAsState() + val homeTabs by context.symphony.settings.homeTabs.flow.collectAsStateWithLifecycle() + val forYouContents by context.symphony.settings.forYouContents.flow.collectAsStateWithLifecycle() + val homePageBottomBarLabelVisibility by context.symphony.settings.homePageBottomBarLabelVisibility.flow.collectAsStateWithLifecycle() Scaffold( modifier = Modifier.fillMaxSize(), diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/view/settings/MiniPlayerSettingsView.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/view/settings/MiniPlayerSettingsView.kt index fbcffd8f..3c4c6abd 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/view/settings/MiniPlayerSettingsView.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/view/settings/MiniPlayerSettingsView.kt @@ -20,7 +20,6 @@ import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color @@ -39,9 +38,9 @@ object MiniPlayerSettingsViewRoute @Composable fun MiniPlayerSettingsView(context: ViewContext) { val scrollState = rememberScrollState() - val miniPlayerTrackControls by context.symphony.settings.miniPlayerTrackControls.flow.collectAsState() - val miniPlayerSeekControls by context.symphony.settings.miniPlayerSeekControls.flow.collectAsState() - val miniPlayerTextMarquee by context.symphony.settings.miniPlayerTextMarquee.flow.collectAsState() + val miniPlayerTrackControls by context.symphony.settings.miniPlayerTrackControls.flow.collectAsStateWithLifecycle() + val miniPlayerSeekControls by context.symphony.settings.miniPlayerSeekControls.flow.collectAsStateWithLifecycle() + val miniPlayerTextMarquee by context.symphony.settings.miniPlayerTextMarquee.flow.collectAsStateWithLifecycle() Scaffold( modifier = Modifier.fillMaxSize(), diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/view/settings/NowPlayingSettingsView.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/view/settings/NowPlayingSettingsView.kt index b62f6c27..5f37d613 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/view/settings/NowPlayingSettingsView.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/view/settings/NowPlayingSettingsView.kt @@ -22,7 +22,6 @@ import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color @@ -44,11 +43,11 @@ object NowPlayingSettingsViewRoute @Composable fun NowPlayingSettingsView(context: ViewContext) { val scrollState = rememberScrollState() - val nowPlayingControlsLayout by context.symphony.settings.nowPlayingControlsLayout.flow.collectAsState() - val nowPlayingAdditionalInfo by context.symphony.settings.nowPlayingAdditionalInfo.flow.collectAsState() - val nowPlayingSeekControls by context.symphony.settings.nowPlayingSeekControls.flow.collectAsState() - val nowPlayingLyricsLayout by context.symphony.settings.nowPlayingLyricsLayout.flow.collectAsState() - val lyricsKeepScreenAwake by context.symphony.settings.lyricsKeepScreenAwake.flow.collectAsState() + val nowPlayingControlsLayout by context.symphony.settings.nowPlayingControlsLayout.flow.collectAsStateWithLifecycle() + val nowPlayingAdditionalInfo by context.symphony.settings.nowPlayingAdditionalInfo.flow.collectAsStateWithLifecycle() + val nowPlayingSeekControls by context.symphony.settings.nowPlayingSeekControls.flow.collectAsStateWithLifecycle() + val nowPlayingLyricsLayout by context.symphony.settings.nowPlayingLyricsLayout.flow.collectAsStateWithLifecycle() + val lyricsKeepScreenAwake by context.symphony.settings.lyricsKeepScreenAwake.flow.collectAsStateWithLifecycle() Scaffold( modifier = Modifier.fillMaxSize(), diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/view/settings/PlayerSettingsView.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/view/settings/PlayerSettingsView.kt index 8438d7fb..dda33249 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/view/settings/PlayerSettingsView.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/view/settings/PlayerSettingsView.kt @@ -24,7 +24,6 @@ import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color @@ -45,15 +44,15 @@ object PlayerSettingsViewRoute @Composable fun PlayerSettingsView(context: ViewContext) { val scrollState = rememberScrollState() - val fadePlayback by context.symphony.settings.fadePlayback.flow.collectAsState() - val fadePlaybackDuration by context.symphony.settings.fadePlaybackDuration.flow.collectAsState() - val requireAudioFocus by context.symphony.settings.requireAudioFocus.flow.collectAsState() - val ignoreAudioFocusLoss by context.symphony.settings.ignoreAudioFocusLoss.flow.collectAsState() - val playOnHeadphonesConnect by context.symphony.settings.playOnHeadphonesConnect.flow.collectAsState() - val pauseOnHeadphonesDisconnect by context.symphony.settings.pauseOnHeadphonesDisconnect.flow.collectAsState() - val seekBackDuration by context.symphony.settings.seekBackDuration.flow.collectAsState() - val seekForwardDuration by context.symphony.settings.seekForwardDuration.flow.collectAsState() - val gaplessPlayback by context.symphony.settings.gaplessPlayback.flow.collectAsState() + val fadePlayback by context.symphony.settings.fadePlayback.flow.collectAsStateWithLifecycle() + val fadePlaybackDuration by context.symphony.settings.fadePlaybackDuration.flow.collectAsStateWithLifecycle() + val requireAudioFocus by context.symphony.settings.requireAudioFocus.flow.collectAsStateWithLifecycle() + val ignoreAudioFocusLoss by context.symphony.settings.ignoreAudioFocusLoss.flow.collectAsStateWithLifecycle() + val playOnHeadphonesConnect by context.symphony.settings.playOnHeadphonesConnect.flow.collectAsStateWithLifecycle() + val pauseOnHeadphonesDisconnect by context.symphony.settings.pauseOnHeadphonesDisconnect.flow.collectAsStateWithLifecycle() + val seekBackDuration by context.symphony.settings.seekBackDuration.flow.collectAsStateWithLifecycle() + val seekForwardDuration by context.symphony.settings.seekForwardDuration.flow.collectAsStateWithLifecycle() + val gaplessPlayback by context.symphony.settings.gaplessPlayback.flow.collectAsStateWithLifecycle() Scaffold( modifier = Modifier.fillMaxSize(), diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/view/settings/UpdateSettingsView.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/view/settings/UpdateSettingsView.kt index b15ee1af..6e175ce6 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/view/settings/UpdateSettingsView.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/view/settings/UpdateSettingsView.kt @@ -18,7 +18,6 @@ import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color @@ -37,8 +36,8 @@ object UpdateSettingsViewRoute @Composable fun UpdateSettingsView(context: ViewContext) { val scrollState = rememberScrollState() - val checkForUpdates by context.symphony.settings.checkForUpdates.flow.collectAsState() - val showUpdateToast by context.symphony.settings.showUpdateToast.flow.collectAsState() + val checkForUpdates by context.symphony.settings.checkForUpdates.flow.collectAsStateWithLifecycle() + val showUpdateToast by context.symphony.settings.showUpdateToast.flow.collectAsStateWithLifecycle() Scaffold( modifier = Modifier.fillMaxSize(),