From 3e0605d00b0d2e67352179dd49d6c8a872624150 Mon Sep 17 00:00:00 2001 From: Zyrouge Date: Sat, 26 Oct 2024 21:16:05 +0530 Subject: [PATCH] refactor: better persistent & cache database --- .gitignore | 1 + app/build.gradle.kts | 10 ++ .../zyrouge/symphony/services/Settings.kt | 58 ++++---- .../services/database/CacheDatabase.kt | 26 ++++ .../symphony/services/database/Database.kt | 13 +- .../services/database/PersistentDatabase.kt | 26 ++++ .../symphony/services/database/PlaylistBox.kt | 25 ---- .../symphony/services/database/SongCache.kt | 25 ---- .../ArtworkCacheStore.kt} | 4 +- .../LyricsCacheStore.kt} | 4 +- .../services/database/store/PlaylistStore.kt | 23 +++ .../services/database/store/SongCacheStore.kt | 26 ++++ .../services/groove/AlbumArtistRepository.kt | 28 ++-- .../services/groove/AlbumRepository.kt | 28 ++-- .../services/groove/ArtistRepository.kt | 28 ++-- .../symphony/services/groove/Explorer.kt | 105 -------------- .../services/groove/GenreRepository.kt | 24 ++-- .../symphony/services/groove/GrooveKinds.kt | 10 ++ .../symphony/services/groove/GrooveManager.kt | 9 -- .../symphony/services/groove/MediaExposer.kt | 134 +++++++++++------- .../symphony/services/groove/Playlist.kt | 58 +++----- .../services/groove/PlaylistRepository.kt | 79 +++++------ .../zyrouge/symphony/services/groove/Song.kt | 80 ++--------- .../services/groove/SongRepository.kt | 66 +++++---- .../symphony/services/i18n/Translation.kt | 6 +- .../ui/components/AddToPlaylistDialog.kt | 2 +- .../symphony/ui/components/AlbumArtistGrid.kt | 17 ++- .../symphony/ui/components/AlbumGrid.kt | 17 +-- .../symphony/ui/components/ArtistGrid.kt | 17 ++- .../symphony/ui/components/GenreGrid.kt | 15 +- .../ui/components/LongPressCopyableText.kt | 4 +- .../symphony/ui/components/PlaylistGrid.kt | 15 +- .../components/PlaylistInformationDialog.kt | 2 +- .../symphony/ui/components/PlaylistTile.kt | 8 +- .../ui/components/SongExplorerList.kt | 43 +++--- .../ui/components/SongInformationDialog.kt | 8 +- .../symphony/ui/components/SongList.kt | 42 +++--- .../symphony/ui/components/SongTreeList.kt | 45 +++--- .../zyrouge/symphony/ui/helpers/Context.kt | 8 +- .../zyrouge/symphony/ui/helpers/Explorer.kt | 14 -- .../symphony/ui/helpers/SimpleFileSystem.kt | 14 ++ .../zyrouge/symphony/ui/view/Playlist.kt | 2 +- .../zyrouge/symphony/ui/view/Settings.kt | 6 +- .../zyrouge/symphony/ui/view/home/Folders.kt | 57 ++++---- .../zyrouge/symphony/ui/view/home/ForYou.kt | 4 +- .../symphony/ui/view/home/Playlists.kt | 16 ++- .../ui/view/nowPlaying/BodyContent.kt | 4 +- .../ui/view/nowPlaying/SleepTimerDialog.kt | 6 +- .../symphony/ui/view/settings/LinkTile.kt | 4 +- .../ui/view/settings/MultiGrooveFolderTile.kt | 19 ++- .../ui/view/settings/MultiSystemFolderTile.kt | 23 +-- .../github/zyrouge/symphony/utils/Activity.kt | 10 -- .../zyrouge/symphony/utils/ActivityUtils.kt | 29 ++++ .../github/zyrouge/symphony/utils/Compose.kt | 18 --- .../zyrouge/symphony/utils/DocumentFileX.kt | 10 +- .../utils/{Duration.kt => DurationUtils.kt} | 2 +- .../github/zyrouge/symphony/utils/Eventer.kt | 6 - .../utils/{FuzzySearch.kt => Fuzzy.kt} | 0 .../utils/{Range.kt => RangeUtils.kt} | 0 .../zyrouge/symphony/utils/RoomConvertors.kt | 27 ++++ .../symphony/utils/{RunX.kt => Run.kt} | 0 .../zyrouge/symphony/utils/Serializable.kt | 19 --- .../symphony/utils/SimpleFileSystem.kt | 55 +++++++ .../zyrouge/symphony/utils/SimplePath.kt | 35 +++++ .../zyrouge/symphony/utils/StringListUtils.kt | 16 +++ build.gradle.kts | 2 + gradle/libs.versions.toml | 7 + 67 files changed, 776 insertions(+), 768 deletions(-) create mode 100644 app/src/main/java/io/github/zyrouge/symphony/services/database/CacheDatabase.kt create mode 100644 app/src/main/java/io/github/zyrouge/symphony/services/database/PersistentDatabase.kt delete mode 100644 app/src/main/java/io/github/zyrouge/symphony/services/database/PlaylistBox.kt delete mode 100644 app/src/main/java/io/github/zyrouge/symphony/services/database/SongCache.kt rename app/src/main/java/io/github/zyrouge/symphony/services/database/{ArtworkCache.kt => store/ArtworkCacheStore.kt} (80%) rename app/src/main/java/io/github/zyrouge/symphony/services/database/{LyricsCache.kt => store/LyricsCacheStore.kt} (86%) create mode 100644 app/src/main/java/io/github/zyrouge/symphony/services/database/store/PlaylistStore.kt create mode 100644 app/src/main/java/io/github/zyrouge/symphony/services/database/store/SongCacheStore.kt delete mode 100644 app/src/main/java/io/github/zyrouge/symphony/services/groove/Explorer.kt create mode 100644 app/src/main/java/io/github/zyrouge/symphony/services/groove/GrooveKinds.kt delete mode 100644 app/src/main/java/io/github/zyrouge/symphony/ui/helpers/Explorer.kt create mode 100644 app/src/main/java/io/github/zyrouge/symphony/ui/helpers/SimpleFileSystem.kt delete mode 100644 app/src/main/java/io/github/zyrouge/symphony/utils/Activity.kt create mode 100644 app/src/main/java/io/github/zyrouge/symphony/utils/ActivityUtils.kt delete mode 100644 app/src/main/java/io/github/zyrouge/symphony/utils/Compose.kt rename app/src/main/java/io/github/zyrouge/symphony/utils/{Duration.kt => DurationUtils.kt} (96%) rename app/src/main/java/io/github/zyrouge/symphony/utils/{FuzzySearch.kt => Fuzzy.kt} (100%) rename app/src/main/java/io/github/zyrouge/symphony/utils/{Range.kt => RangeUtils.kt} (100%) create mode 100644 app/src/main/java/io/github/zyrouge/symphony/utils/RoomConvertors.kt rename app/src/main/java/io/github/zyrouge/symphony/utils/{RunX.kt => Run.kt} (100%) delete mode 100644 app/src/main/java/io/github/zyrouge/symphony/utils/Serializable.kt create mode 100644 app/src/main/java/io/github/zyrouge/symphony/utils/SimpleFileSystem.kt create mode 100644 app/src/main/java/io/github/zyrouge/symphony/utils/SimplePath.kt create mode 100644 app/src/main/java/io/github/zyrouge/symphony/utils/StringListUtils.kt diff --git a/.gitignore b/.gitignore index e7d4a7ca..81b4c3a4 100644 --- a/.gitignore +++ b/.gitignore @@ -22,4 +22,5 @@ app/src/main/assets/i18n dist secrets .kotlin +app/room-schemas/ diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 82981beb..eae5637f 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -3,6 +3,8 @@ plugins { alias(libs.plugins.android.kotlin) alias(libs.plugins.compose.compiler) alias(libs.plugins.kotlin.serialization) + alias(libs.plugins.ksp) + alias(libs.plugins.room) } android { @@ -32,6 +34,7 @@ android { keyPassword = System.getenv("SIGNING_KEY_PASSWORD") } } + buildTypes { getByName("release") { isMinifyEnabled = true @@ -76,6 +79,10 @@ android { it.useJUnitPlatform() } } + + room { + schemaDirectory("$projectDir/room-schemas") + } } dependencies { @@ -93,6 +100,9 @@ dependencies { implementation(libs.lifecycle.runtime) implementation(libs.media) implementation(libs.okhttp3) + ksp(libs.room.compiler) + implementation(libs.room.ktx) + implementation(libs.room.runtime) debugImplementation(libs.compose.ui.tooling) debugImplementation(libs.compose.ui.test.manifest) 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 25154b3d..71286546 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 @@ -6,13 +6,12 @@ import android.net.Uri import android.os.Environment import androidx.core.content.edit import io.github.zyrouge.symphony.Symphony -import io.github.zyrouge.symphony.services.groove.AlbumArtistSortBy -import io.github.zyrouge.symphony.services.groove.AlbumSortBy -import io.github.zyrouge.symphony.services.groove.ArtistSortBy -import io.github.zyrouge.symphony.services.groove.GenreSortBy -import io.github.zyrouge.symphony.services.groove.PathSortBy -import io.github.zyrouge.symphony.services.groove.PlaylistSortBy -import io.github.zyrouge.symphony.services.groove.SongSortBy +import io.github.zyrouge.symphony.services.groove.AlbumArtistRepository +import io.github.zyrouge.symphony.services.groove.AlbumRepository +import io.github.zyrouge.symphony.services.groove.ArtistRepository +import io.github.zyrouge.symphony.services.groove.GenreRepository +import io.github.zyrouge.symphony.services.groove.PlaylistRepository +import io.github.zyrouge.symphony.services.groove.SongRepository import io.github.zyrouge.symphony.services.radio.RadioQueue import io.github.zyrouge.symphony.ui.theme.ThemeMode import io.github.zyrouge.symphony.ui.view.HomePageBottomBarLabelVisibility @@ -20,6 +19,7 @@ import io.github.zyrouge.symphony.ui.view.HomePages import io.github.zyrouge.symphony.ui.view.NowPlayingControlsLayout import io.github.zyrouge.symphony.ui.view.NowPlayingLyricsLayout import io.github.zyrouge.symphony.ui.view.home.ForYou +import io.github.zyrouge.symphony.utils.StringListUtils import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update @@ -91,17 +91,17 @@ object SettingsKeys { object SettingsDefaults { val themeMode = ThemeMode.SYSTEM const val useMaterialYou = true - val lastUsedSongSortBy = SongSortBy.TITLE - val lastUsedArtistsSortBy = ArtistSortBy.ARTIST_NAME - val lastUsedAlbumArtistsSortBy = AlbumArtistSortBy.ARTIST_NAME - val lastUsedAlbumsSortBy = AlbumSortBy.ALBUM_NAME - val lastUsedGenresSortBy = GenreSortBy.GENRE - val lastUsedBrowserSortBy = SongSortBy.FILENAME - val lastUsedPlaylistsSortBy = PlaylistSortBy.TITLE - val lastUsedPlaylistSongsSortBy = SongSortBy.CUSTOM - val lastUsedAlbumSongsSortBy = SongSortBy.TRACK_NUMBER - val lastUsedTreePathSortBy = PathSortBy.NAME - val lastUsedFoldersSortBy = PathSortBy.NAME + val lastUsedSongSortBy = SongRepository.SortBy.TITLE + val lastUsedArtistsSortBy = ArtistRepository.SortBy.ARTIST_NAME + val lastUsedAlbumArtistsSortBy = AlbumArtistRepository.SortBy.ARTIST_NAME + val lastUsedAlbumsSortBy = AlbumRepository.SortBy.ALBUM_NAME + val lastUsedGenresSortBy = GenreRepository.SortBy.GENRE + val lastUsedBrowserSortBy = SongRepository.SortBy.FILENAME + val lastUsedPlaylistsSortBy = PlaylistRepository.SortBy.TITLE + val lastUsedPlaylistSongsSortBy = SongRepository.SortBy.CUSTOM + val lastUsedAlbumSongsSortBy = SongRepository.SortBy.TRACK_NUMBER + val lastUsedTreePathSortBy = StringListUtils.SortBy.NAME + val lastUsedFoldersSortBy = StringListUtils.SortBy.NAME const val checkForUpdates = false const val fadePlayback = false const val requireAudioFocus = true @@ -363,7 +363,7 @@ class SettingsManager(private val symphony: Symphony) { .getEnum(SettingsKeys.lastUsedSongsSortBy, null) ?: SettingsDefaults.lastUsedSongSortBy - fun setLastUsedSongsSortBy(sortBy: SongSortBy) { + fun setLastUsedSongsSortBy(sortBy: SongRepository.SortBy) { getSharedPreferences().edit { putEnum(SettingsKeys.lastUsedSongsSortBy, sortBy) } @@ -384,7 +384,7 @@ class SettingsManager(private val symphony: Symphony) { .getEnum(SettingsKeys.lastUsedArtistsSortBy, null) ?: SettingsDefaults.lastUsedArtistsSortBy - fun setLastUsedArtistsSortBy(sortBy: ArtistSortBy) { + fun setLastUsedArtistsSortBy(sortBy: ArtistRepository.SortBy) { getSharedPreferences().edit { putEnum(SettingsKeys.lastUsedArtistsSortBy, sortBy) } @@ -405,7 +405,7 @@ class SettingsManager(private val symphony: Symphony) { .getEnum(SettingsKeys.lastUsedAlbumArtistsSortBy, null) ?: SettingsDefaults.lastUsedAlbumArtistsSortBy - fun setLastUsedAlbumArtistsSortBy(sortBy: AlbumArtistSortBy) { + fun setLastUsedAlbumArtistsSortBy(sortBy: AlbumArtistRepository.SortBy) { getSharedPreferences().edit { putEnum(SettingsKeys.lastUsedAlbumArtistsSortBy, sortBy) } @@ -426,7 +426,7 @@ class SettingsManager(private val symphony: Symphony) { .getEnum(SettingsKeys.lastUsedAlbumsSortBy, null) ?: SettingsDefaults.lastUsedAlbumsSortBy - fun setLastUsedAlbumsSortBy(sortBy: AlbumSortBy) { + fun setLastUsedAlbumsSortBy(sortBy: AlbumRepository.SortBy) { getSharedPreferences().edit { putEnum(SettingsKeys.lastUsedAlbumsSortBy, sortBy) } @@ -447,7 +447,7 @@ class SettingsManager(private val symphony: Symphony) { .getEnum(SettingsKeys.lastUsedGenresSortBy, null) ?: SettingsDefaults.lastUsedGenresSortBy - fun setLastUsedGenresSortBy(sortBy: GenreSortBy) { + fun setLastUsedGenresSortBy(sortBy: GenreRepository.SortBy) { getSharedPreferences().edit { putEnum(SettingsKeys.lastUsedGenresSortBy, sortBy) } @@ -468,7 +468,7 @@ class SettingsManager(private val symphony: Symphony) { .getEnum(SettingsKeys.lastUsedBrowserSortBy, null) ?: SettingsDefaults.lastUsedBrowserSortBy - fun setLastUsedBrowserSortBy(sortBy: SongSortBy) { + fun setLastUsedBrowserSortBy(sortBy: SongRepository.SortBy) { getSharedPreferences().edit { putEnum(SettingsKeys.lastUsedBrowserSortBy, sortBy) } @@ -500,7 +500,7 @@ class SettingsManager(private val symphony: Symphony) { .getEnum(SettingsKeys.lastUsedPlaylistsSortBy, null) ?: SettingsDefaults.lastUsedPlaylistsSortBy - fun setLastUsedPlaylistsSortBy(sortBy: PlaylistSortBy) { + fun setLastUsedPlaylistsSortBy(sortBy: PlaylistRepository.SortBy) { getSharedPreferences().edit { putEnum(SettingsKeys.lastUsedPlaylistsSortBy, sortBy) } @@ -521,7 +521,7 @@ class SettingsManager(private val symphony: Symphony) { .getEnum(SettingsKeys.lastUsedPlaylistSongsSortBy, null) ?: SettingsDefaults.lastUsedPlaylistSongsSortBy - fun setLastUsedPlaylistSongsSortBy(sortBy: SongSortBy) { + fun setLastUsedPlaylistSongsSortBy(sortBy: SongRepository.SortBy) { getSharedPreferences().edit { putEnum(SettingsKeys.lastUsedPlaylistSongsSortBy, sortBy) } @@ -542,7 +542,7 @@ class SettingsManager(private val symphony: Symphony) { .getEnum(SettingsKeys.lastUsedAlbumSongsSortBy, null) ?: SettingsDefaults.lastUsedAlbumSongsSortBy - fun setLastUsedAlbumSongsSortBy(sortBy: SongSortBy) { + fun setLastUsedAlbumSongsSortBy(sortBy: SongRepository.SortBy) { getSharedPreferences().edit { putEnum(SettingsKeys.lastUsedAlbumSongsSortBy, sortBy) } @@ -563,7 +563,7 @@ class SettingsManager(private val symphony: Symphony) { .getEnum(SettingsKeys.lastUsedTreePathSortBy, null) ?: SettingsDefaults.lastUsedTreePathSortBy - fun setLastUsedTreePathSortBy(sortBy: PathSortBy) { + fun setLastUsedTreePathSortBy(sortBy: StringListUtils.SortBy) { getSharedPreferences().edit { putEnum(SettingsKeys.lastUsedTreePathSortBy, sortBy) } @@ -584,7 +584,7 @@ class SettingsManager(private val symphony: Symphony) { .getEnum(SettingsKeys.lastUsedFoldersSortBy, null) ?: SettingsDefaults.lastUsedFoldersSortBy - fun setLastUsedFoldersSortBy(sortBy: PathSortBy) { + fun setLastUsedFoldersSortBy(sortBy: StringListUtils.SortBy) { getSharedPreferences().edit { putEnum(SettingsKeys.lastUsedFoldersSortBy, sortBy) } diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/database/CacheDatabase.kt b/app/src/main/java/io/github/zyrouge/symphony/services/database/CacheDatabase.kt new file mode 100644 index 00000000..fb682a65 --- /dev/null +++ b/app/src/main/java/io/github/zyrouge/symphony/services/database/CacheDatabase.kt @@ -0,0 +1,26 @@ +package io.github.zyrouge.symphony.services.database + +import androidx.room.Database +import androidx.room.Room +import androidx.room.RoomDatabase +import androidx.room.TypeConverters +import io.github.zyrouge.symphony.Symphony +import io.github.zyrouge.symphony.services.database.store.SongCacheStore +import io.github.zyrouge.symphony.services.groove.Song +import io.github.zyrouge.symphony.utils.RoomConvertors + +@Database(entities = [Song::class], version = 1) +@TypeConverters(RoomConvertors::class) +abstract class CacheDatabase : RoomDatabase() { + abstract fun songs(): SongCacheStore + + companion object { + fun create(symphony: Symphony) = Room + .databaseBuilder( + symphony.applicationContext, + CacheDatabase::class.java, + "cache" + ) + .build() + } +} diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/database/Database.kt b/app/src/main/java/io/github/zyrouge/symphony/services/database/Database.kt index 8750775b..71c9a588 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/database/Database.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/database/Database.kt @@ -1,10 +1,15 @@ package io.github.zyrouge.symphony.services.database import io.github.zyrouge.symphony.Symphony +import io.github.zyrouge.symphony.services.database.store.ArtworkCacheStore +import io.github.zyrouge.symphony.services.database.store.LyricsCacheStore class Database(symphony: Symphony) { - val songCache = SongCache(symphony) - val artworkCache = ArtworkCache(symphony) - val lyricsCache = LyricsCache(symphony) - val playlists = PlaylistBox(symphony) + private val cache = CacheDatabase.create(symphony) + private val persistent = PersistentDatabase.create(symphony) + + val artworkCache = ArtworkCacheStore(symphony) + val lyricsCache = LyricsCacheStore(symphony) + val songCache get() = cache.songs() + val playlists get() = persistent.playlists() } diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/database/PersistentDatabase.kt b/app/src/main/java/io/github/zyrouge/symphony/services/database/PersistentDatabase.kt new file mode 100644 index 00000000..ff90262b --- /dev/null +++ b/app/src/main/java/io/github/zyrouge/symphony/services/database/PersistentDatabase.kt @@ -0,0 +1,26 @@ +package io.github.zyrouge.symphony.services.database + +import androidx.room.Database +import androidx.room.Room +import androidx.room.RoomDatabase +import androidx.room.TypeConverters +import io.github.zyrouge.symphony.Symphony +import io.github.zyrouge.symphony.services.database.store.PlaylistStore +import io.github.zyrouge.symphony.services.groove.Playlist +import io.github.zyrouge.symphony.utils.RoomConvertors + +@Database(entities = [Playlist::class], version = 1) +@TypeConverters(RoomConvertors::class) +abstract class PersistentDatabase : RoomDatabase() { + abstract fun playlists(): PlaylistStore + + companion object { + fun create(symphony: Symphony) = Room + .databaseBuilder( + symphony.applicationContext, + PersistentDatabase::class.java, + "persistent" + ) + .build() + } +} diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/database/PlaylistBox.kt b/app/src/main/java/io/github/zyrouge/symphony/services/database/PlaylistBox.kt deleted file mode 100644 index df36c087..00000000 --- a/app/src/main/java/io/github/zyrouge/symphony/services/database/PlaylistBox.kt +++ /dev/null @@ -1,25 +0,0 @@ -package io.github.zyrouge.symphony.services.database - -import io.github.zyrouge.symphony.Symphony -import io.github.zyrouge.symphony.services.database.adapters.SQLiteKeyValueDatabaseAdapter -import io.github.zyrouge.symphony.services.groove.Playlist - -class PlaylistBox(private val symphony: Symphony) { - private val adapter = SQLiteKeyValueDatabaseAdapter( - PlaylistTransformer(), - SQLiteKeyValueDatabaseAdapter.CacheOpenHelper(symphony.applicationContext, "playlists", 1) - ) - - fun get(key: String) = adapter.get(key) - fun put(key: String, value: Playlist) = adapter.put(key, value) - fun delete(key: String) = adapter.delete(key) - fun delete(keys: Collection) = adapter.delete(keys) - fun keys() = adapter.keys() - fun all() = adapter.all() - fun clear() = adapter.clear() - - private class PlaylistTransformer : SQLiteKeyValueDatabaseAdapter.Transformer { - override fun serialize(data: Playlist) = data.toJson() - override fun deserialize(data: String) = Playlist.fromJson(data) - } -} diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/database/SongCache.kt b/app/src/main/java/io/github/zyrouge/symphony/services/database/SongCache.kt deleted file mode 100644 index 5f5d309d..00000000 --- a/app/src/main/java/io/github/zyrouge/symphony/services/database/SongCache.kt +++ /dev/null @@ -1,25 +0,0 @@ -package io.github.zyrouge.symphony.services.database - -import io.github.zyrouge.symphony.Symphony -import io.github.zyrouge.symphony.services.database.adapters.SQLiteKeyValueDatabaseAdapter -import io.github.zyrouge.symphony.services.groove.Song - -class SongCache(val symphony: Symphony) { - private val adapter = SQLiteKeyValueDatabaseAdapter( - SongTransformer(), - SQLiteKeyValueDatabaseAdapter.CacheOpenHelper(symphony.applicationContext, "songs", 1) - ) - - fun get(key: String) = adapter.get(key) - fun put(key: String, value: Song) = adapter.put(key, value) - fun delete(key: String) = adapter.delete(key) - fun delete(keys: Collection) = adapter.delete(keys) - fun keys() = adapter.keys() - fun all() = adapter.all() - fun clear() = adapter.clear() - - private class SongTransformer : SQLiteKeyValueDatabaseAdapter.Transformer { - override fun serialize(data: Song) = data.toJson() - override fun deserialize(data: String) = Song.fromJson(data) - } -} diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/database/ArtworkCache.kt b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/ArtworkCacheStore.kt similarity index 80% rename from app/src/main/java/io/github/zyrouge/symphony/services/database/ArtworkCache.kt rename to app/src/main/java/io/github/zyrouge/symphony/services/database/store/ArtworkCacheStore.kt index 1b24e278..8959ce18 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/database/ArtworkCache.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/ArtworkCacheStore.kt @@ -1,10 +1,10 @@ -package io.github.zyrouge.symphony.services.database +package io.github.zyrouge.symphony.services.database.store import io.github.zyrouge.symphony.Symphony import io.github.zyrouge.symphony.services.database.adapters.FileTreeDatabaseAdapter import java.nio.file.Paths -class ArtworkCache(val symphony: Symphony) { +class ArtworkCacheStore(val symphony: Symphony) { private val adapter = FileTreeDatabaseAdapter( Paths .get(symphony.applicationContext.dataDir.absolutePath, "covers") diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/database/LyricsCache.kt b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/LyricsCacheStore.kt similarity index 86% rename from app/src/main/java/io/github/zyrouge/symphony/services/database/LyricsCache.kt rename to app/src/main/java/io/github/zyrouge/symphony/services/database/store/LyricsCacheStore.kt index eb48d47d..1bf59dd0 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/database/LyricsCache.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/LyricsCacheStore.kt @@ -1,9 +1,9 @@ -package io.github.zyrouge.symphony.services.database +package io.github.zyrouge.symphony.services.database.store import io.github.zyrouge.symphony.Symphony import io.github.zyrouge.symphony.services.database.adapters.SQLiteKeyValueDatabaseAdapter -class LyricsCache(val symphony: Symphony) { +class LyricsCacheStore(val symphony: Symphony) { private val adapter = SQLiteKeyValueDatabaseAdapter( SQLiteKeyValueDatabaseAdapter.Transformer.AsString(), SQLiteKeyValueDatabaseAdapter.CacheOpenHelper(symphony.applicationContext, "lyrics", 1) 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 new file mode 100644 index 00000000..0f5df04c --- /dev/null +++ b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/PlaylistStore.kt @@ -0,0 +1,23 @@ +package io.github.zyrouge.symphony.services.database.store + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.MapColumn +import androidx.room.Query +import androidx.room.Update +import io.github.zyrouge.symphony.services.groove.Playlist + +@Dao +interface PlaylistStore { + @Insert + suspend fun insert(vararg playlist: Playlist) + + @Update + suspend fun update(vararg playlist: Playlist) + + @Query("DELETE FROM playlists WHERE id IN (:playlistIds)") + suspend fun delete(playlistIds: Collection) + + @Query("SELECT * FROM playlists") + suspend fun entries(): Map<@MapColumn("id") String, Playlist> +} diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/SongCacheStore.kt b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/SongCacheStore.kt new file mode 100644 index 00000000..0eca6519 --- /dev/null +++ b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/SongCacheStore.kt @@ -0,0 +1,26 @@ +package io.github.zyrouge.symphony.services.database.store + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.MapColumn +import androidx.room.Query +import androidx.room.Update +import io.github.zyrouge.symphony.services.groove.Song + +@Dao +interface SongCacheStore { + @Insert + suspend fun insert(vararg song: Song) + + @Update + suspend fun update(vararg song: Song) + + @Query("DELETE FROM songs WHERE id IN (:songIds)") + suspend fun delete(songIds: Collection) + + @Query("DELETE FROM songs") + suspend fun clear() + + @Query("SELECT * FROM songs") + suspend fun entriesPathMapped(): Map<@MapColumn("path") String, Song> +} diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/groove/AlbumArtistRepository.kt b/app/src/main/java/io/github/zyrouge/symphony/services/groove/AlbumArtistRepository.kt index c8f29081..4df0291f 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/groove/AlbumArtistRepository.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/groove/AlbumArtistRepository.kt @@ -12,14 +12,14 @@ import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import java.util.concurrent.ConcurrentHashMap -enum class AlbumArtistSortBy { - CUSTOM, - ARTIST_NAME, - TRACKS_COUNT, - ALBUMS_COUNT, -} - class AlbumArtistRepository(private val symphony: Symphony) { + enum class SortBy { + CUSTOM, + ARTIST_NAME, + TRACKS_COUNT, + ALBUMS_COUNT, + } + private val cache = ConcurrentHashMap() private val songIdsCache = ConcurrentHashMap>() private val albumIdsCache = ConcurrentHashMap>() @@ -91,16 +91,12 @@ class AlbumArtistRepository(private val symphony: Symphony) { fun search(albumArtistNames: List, terms: String, limit: Int = 7) = searcher .search(terms, albumArtistNames, maxLength = limit) - fun sort( - albumArtistNames: List, - by: AlbumArtistSortBy, - reverse: Boolean, - ): List { + fun sort(albumArtistNames: List, by: SortBy, reverse: Boolean): List { val sorted = when (by) { - AlbumArtistSortBy.CUSTOM -> albumArtistNames - AlbumArtistSortBy.ARTIST_NAME -> albumArtistNames.sortedBy { get(it)?.name } - AlbumArtistSortBy.TRACKS_COUNT -> albumArtistNames.sortedBy { get(it)?.numberOfTracks } - AlbumArtistSortBy.ALBUMS_COUNT -> albumArtistNames.sortedBy { get(it)?.numberOfTracks } + SortBy.CUSTOM -> albumArtistNames + SortBy.ARTIST_NAME -> albumArtistNames.sortedBy { get(it)?.name } + SortBy.TRACKS_COUNT -> albumArtistNames.sortedBy { get(it)?.numberOfTracks } + SortBy.ALBUMS_COUNT -> albumArtistNames.sortedBy { get(it)?.numberOfTracks } } return if (reverse) sorted.reversed() else sorted } diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/groove/AlbumRepository.kt b/app/src/main/java/io/github/zyrouge/symphony/services/groove/AlbumRepository.kt index 04f41728..9f5c29c1 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/groove/AlbumRepository.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/groove/AlbumRepository.kt @@ -13,14 +13,14 @@ import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import java.util.concurrent.ConcurrentHashMap -enum class AlbumSortBy { - CUSTOM, - ALBUM_NAME, - ARTIST_NAME, - TRACKS_COUNT, -} - class AlbumRepository(private val symphony: Symphony) { + enum class SortBy { + CUSTOM, + ALBUM_NAME, + ARTIST_NAME, + TRACKS_COUNT, + } + private val cache = ConcurrentHashMap() private val songIdsCache = ConcurrentHashMap>() private val searcher = FuzzySearcher( @@ -94,16 +94,12 @@ class AlbumRepository(private val symphony: Symphony) { fun search(albumIds: List, terms: String, limit: Int = 7) = searcher .search(terms, albumIds, maxLength = limit) - fun sort( - albumIds: List, - by: AlbumSortBy, - reverse: Boolean, - ): List { + fun sort(albumIds: List, by: SortBy, reverse: Boolean): List { val sorted = when (by) { - AlbumSortBy.CUSTOM -> albumIds - AlbumSortBy.ALBUM_NAME -> albumIds.sortedBy { get(it)?.name } - AlbumSortBy.ARTIST_NAME -> albumIds.sortedBy { get(it)?.artists?.joinToStringIfNotEmpty() } - AlbumSortBy.TRACKS_COUNT -> albumIds.sortedBy { get(it)?.numberOfTracks } + SortBy.CUSTOM -> albumIds + SortBy.ALBUM_NAME -> albumIds.sortedBy { get(it)?.name } + SortBy.ARTIST_NAME -> albumIds.sortedBy { get(it)?.artists?.joinToStringIfNotEmpty() } + SortBy.TRACKS_COUNT -> albumIds.sortedBy { get(it)?.numberOfTracks } } return if (reverse) sorted.reversed() else sorted } diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/groove/ArtistRepository.kt b/app/src/main/java/io/github/zyrouge/symphony/services/groove/ArtistRepository.kt index 92479853..6a8e47b3 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/groove/ArtistRepository.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/groove/ArtistRepository.kt @@ -12,14 +12,14 @@ import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import java.util.concurrent.ConcurrentHashMap -enum class ArtistSortBy { - CUSTOM, - ARTIST_NAME, - TRACKS_COUNT, - ALBUMS_COUNT, -} - class ArtistRepository(private val symphony: Symphony) { + enum class SortBy { + CUSTOM, + ARTIST_NAME, + TRACKS_COUNT, + ALBUMS_COUNT, + } + private val cache = ConcurrentHashMap() private val songIdsCache = ConcurrentHashMap>() private val albumIdsCache = ConcurrentHashMap>() @@ -91,16 +91,12 @@ class ArtistRepository(private val symphony: Symphony) { fun search(artistNames: List, terms: String, limit: Int = 7) = searcher .search(terms, artistNames, maxLength = limit) - fun sort( - artistNames: List, - by: ArtistSortBy, - reverse: Boolean, - ): List { + fun sort(artistNames: List, by: SortBy, reverse: Boolean): List { val sorted = when (by) { - ArtistSortBy.CUSTOM -> artistNames - ArtistSortBy.ARTIST_NAME -> artistNames.sortedBy { get(it)?.name } - ArtistSortBy.TRACKS_COUNT -> artistNames.sortedBy { get(it)?.numberOfTracks } - ArtistSortBy.ALBUMS_COUNT -> artistNames.sortedBy { get(it)?.numberOfTracks } + SortBy.CUSTOM -> artistNames + SortBy.ARTIST_NAME -> artistNames.sortedBy { get(it)?.name } + SortBy.TRACKS_COUNT -> artistNames.sortedBy { get(it)?.numberOfTracks } + SortBy.ALBUMS_COUNT -> artistNames.sortedBy { get(it)?.numberOfTracks } } return if (reverse) sorted.reversed() else sorted } diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/groove/Explorer.kt b/app/src/main/java/io/github/zyrouge/symphony/services/groove/Explorer.kt deleted file mode 100644 index 92345c61..00000000 --- a/app/src/main/java/io/github/zyrouge/symphony/services/groove/Explorer.kt +++ /dev/null @@ -1,105 +0,0 @@ -package io.github.zyrouge.symphony.services.groove - -import java.util.concurrent.ConcurrentHashMap - -enum class PathSortBy { - CUSTOM, - NAME, -} - -object GrooveExplorer { - abstract class Entity(val basename: String, var parent: Folder? = null) { - val pathParts: List - get() = parent?.let { it.pathParts + basename } ?: listOf(basename) - - val fullPath: String - get() = pathParts.joinToString("/") - - abstract fun addRelativePath(path: Path): Entity - } - - class File(basename: String, parent: Folder? = null, var data: Any? = null) : - Entity(basename, parent) { - override fun addRelativePath(path: Path): Nothing { - throw Exception("Cannot add paths to file") - } - } - - class Folder( - basename: String = "root", - parent: Folder? = null, - var children: ConcurrentHashMap = ConcurrentHashMap(), - ) : Entity(basename, parent) { - val isEmpty: Boolean get() = children.isEmpty() - - fun addChild(child: Entity): Entity { - child.parent = this - children[child.basename] = child - return child - } - - override fun addRelativePath(path: Path): Entity { - if (!path.hasChildParts) { - val child = when { - path.isFile -> File(path.firstPart) - else -> Folder(path.firstPart) - } - return addChild(child) - } - val child = children[path.firstPart] ?: addChild(Folder(path.firstPart)) - return child.addRelativePath(path.shift()) - } - } - - class Path(val parts: List) { - constructor(path: String) : this(intoParts(path)) - - val isAbsolute: Boolean get() = parts.firstOrNull() == "" - val isFile: Boolean get() = isFileRegex.containsMatchIn(basename) - - val hasChildParts: Boolean get() = parts.size > 1 - val firstPart: String get() = parts.first() - val basename: String get() = parts.last() - val dirname: Path get() = Path(parts.subList(0, parts.size - 1)) - - fun shift() = Path(parts.subList(1, parts.size)) - - fun resolve(to: Path): Path { - if (to.isAbsolute) return to - val a = parts.toMutableList() - val b = to.parts.toMutableList() - while (true) { - when (b.firstOrNull()) { - "." -> b.removeAt(0) - ".." -> { - b.removeAt(0) - a.removeAt(a.lastIndex) - } - - else -> break - } - } - a.addAll(b) - return Path(a) - } - - override fun toString() = parts.joinToString("/") - - companion object { - private val isFileRegex = Regex(""".+\..+""") - - fun isAbsolute(path: String) = path.startsWith("/") - - private fun intoParts(path: String) = - path.split("/", "\\").filter { it.isNotBlank() } - } - } - - fun sort(paths: List, by: PathSortBy, reverse: Boolean): List { - val sorted = when (by) { - PathSortBy.CUSTOM -> paths - PathSortBy.NAME -> paths.sorted() - } - return if (reverse) sorted.reversed() else sorted - } -} diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/groove/GenreRepository.kt b/app/src/main/java/io/github/zyrouge/symphony/services/groove/GenreRepository.kt index 0a07c819..fa595131 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/groove/GenreRepository.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/groove/GenreRepository.kt @@ -10,13 +10,13 @@ import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import java.util.concurrent.ConcurrentHashMap -enum class GenreSortBy { - CUSTOM, - GENRE, - TRACKS_COUNT, -} - class GenreRepository(private val symphony: Symphony) { + enum class SortBy { + CUSTOM, + GENRE, + TRACKS_COUNT, + } + private val cache = ConcurrentHashMap() private val songIdsCache = ConcurrentHashMap>() private val searcher = FuzzySearcher( @@ -69,15 +69,11 @@ class GenreRepository(private val symphony: Symphony) { fun search(genreNames: List, terms: String, limit: Int = 7) = searcher .search(terms, genreNames, maxLength = limit) - fun sort( - genreNames: List, - by: GenreSortBy, - reverse: Boolean, - ): List { + fun sort(genreNames: List, by: SortBy, reverse: Boolean): List { val sorted = when (by) { - GenreSortBy.CUSTOM -> genreNames - GenreSortBy.GENRE -> genreNames.sortedBy { get(it)?.name } - GenreSortBy.TRACKS_COUNT -> genreNames.sortedBy { get(it)?.numberOfTracks } + SortBy.CUSTOM -> genreNames + SortBy.GENRE -> genreNames.sortedBy { get(it)?.name } + SortBy.TRACKS_COUNT -> genreNames.sortedBy { get(it)?.numberOfTracks } } return if (reverse) sorted.reversed() else sorted } diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/groove/GrooveKinds.kt b/app/src/main/java/io/github/zyrouge/symphony/services/groove/GrooveKinds.kt new file mode 100644 index 00000000..52ae8cf0 --- /dev/null +++ b/app/src/main/java/io/github/zyrouge/symphony/services/groove/GrooveKinds.kt @@ -0,0 +1,10 @@ +package io.github.zyrouge.symphony.services.groove + +enum class GrooveKinds { + SONG, + ALBUM, + ARTIST, + ALBUM_ARTIST, + GENRE, + PLAYLIST, +} diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/groove/GrooveManager.kt b/app/src/main/java/io/github/zyrouge/symphony/services/groove/GrooveManager.kt index ee6a1f93..218f3b27 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/groove/GrooveManager.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/groove/GrooveManager.kt @@ -10,15 +10,6 @@ import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.coroutines.launch -enum class GrooveKinds { - SONG, - ALBUM, - ARTIST, - ALBUM_ARTIST, - GENRE, - PLAYLIST, -} - class GrooveManager(private val symphony: Symphony) : SymphonyHooks { val coroutineScope = CoroutineScope(Dispatchers.Default) var readyDeferred = CompletableDeferred() diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/groove/MediaExposer.kt b/app/src/main/java/io/github/zyrouge/symphony/services/groove/MediaExposer.kt index 1e457c66..e21e47de 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/groove/MediaExposer.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/groove/MediaExposer.kt @@ -1,26 +1,28 @@ package io.github.zyrouge.symphony.services.groove -import android.content.Intent import android.net.Uri import io.github.zyrouge.symphony.Symphony +import io.github.zyrouge.symphony.utils.ActivityUtils +import io.github.zyrouge.symphony.utils.ConcurrentSet import io.github.zyrouge.symphony.utils.DocumentFileX import io.github.zyrouge.symphony.utils.Logger +import io.github.zyrouge.symphony.utils.SimpleFileSystem +import io.github.zyrouge.symphony.utils.SimplePath import io.github.zyrouge.symphony.utils.concurrentSetOf import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update -import kotlinx.coroutines.launch -import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withContext import java.util.concurrent.ConcurrentHashMap class MediaExposer(private val symphony: Symphony) { internal val uris = ConcurrentHashMap() - var explorer = GrooveExplorer.Folder() + var explorer = SimpleFileSystem.Folder() private val _isUpdating = MutableStateFlow(false) val isUpdating = _isUpdating.asStateFlow() @@ -28,34 +30,51 @@ class MediaExposer(private val symphony: Symphony) { value } - private inner class ScanCycle { - val songCache = ConcurrentHashMap(symphony.database.songCache.all()) - val songCacheUnused = concurrentSetOf(songCache.keys) - val artworkCacheUnused = concurrentSetOf(symphony.database.artworkCache.all()) - val lyricsCacheUnused = concurrentSetOf(symphony.database.lyricsCache.keys()) - val filter = MediaFilter( - symphony.settings.songsFilterPattern.value, - symphony.settings.blacklistFolders.value.toSortedSet(), - symphony.settings.whitelistFolders.value.toSortedSet() - ) + private data class ScanCycle( + val songCache: ConcurrentHashMap, + val songCacheUnused: ConcurrentSet, + val artworkCacheUnused: ConcurrentSet, + val lyricsCacheUnused: ConcurrentSet, + val filter: MediaFilter, + ) { + companion object { + suspend fun create(symphony: Symphony): ScanCycle { + val songCache = ConcurrentHashMap(symphony.database.songCache.entriesPathMapped()) + val songCacheUnused = concurrentSetOf(songCache.keys) + val artworkCacheUnused = concurrentSetOf(symphony.database.artworkCache.all()) + val lyricsCacheUnused = concurrentSetOf(symphony.database.lyricsCache.keys()) + val filter = MediaFilter( + symphony.settings.songsFilterPattern.value, + symphony.settings.blacklistFolders.value.toSortedSet(), + symphony.settings.whitelistFolders.value.toSortedSet() + ) + return ScanCycle( + songCache = songCache, + songCacheUnused = songCacheUnused, + artworkCacheUnused = artworkCacheUnused, + lyricsCacheUnused = lyricsCacheUnused, + filter = filter, + ) + } + } } - fun fetch() { + @OptIn(ExperimentalCoroutinesApi::class) + suspend fun fetch() { emitUpdate(true) try { val context = symphony.applicationContext val folderUris = symphony.settings.mediaFolders.value - val permissions = Intent.FLAG_GRANT_READ_URI_PERMISSION - val cycle = ScanCycle() - runBlocking { - folderUris.map { x -> - async(Dispatchers.IO) { - context.contentResolver.takePersistableUriPermission(x, permissions) - DocumentFileX.fromTreeUri(context, x)?.let { - scanMediaTree(cycle, it) - } + val cycle = ScanCycle.create(symphony) + folderUris.map { x -> + ActivityUtils.makePersistableReadableUri(context, x) + DocumentFileX.fromTreeUri(context, x)?.let { + val parent = DocumentFileX.getParentPathOfTreeUri(x) + val path = parent?.join(it.name) ?: SimplePath(it.name) + with(Dispatchers.IO) { + scanMediaTree(cycle, path, it) } - }.awaitAll() + } } trimCache(cycle) } catch (err: Exception) { @@ -65,72 +84,79 @@ class MediaExposer(private val symphony: Symphony) { emitFinish() } - private suspend fun scanMediaTree(cycle: ScanCycle, file: DocumentFileX) { + private suspend fun scanMediaTree(cycle: ScanCycle, path: SimplePath, file: DocumentFileX) { try { if (!cycle.filter.isWhitelisted(file.name)) { return } coroutineScope { - file.list { - launch(Dispatchers.IO) { + file.list().map { + val childPath = path.join(it.name) + async { when { - it.isDirectory -> scanMediaTree(cycle, it) - else -> scanMediaFile(cycle, it) + it.isDirectory -> scanMediaTree(cycle, childPath, it) + else -> scanMediaFile(cycle, childPath, it) } } - } + }.awaitAll() } } catch (err: Exception) { Logger.error("MediaExposer", "scan media tree failed", err) } } - private suspend fun scanMediaFile(cycle: ScanCycle, file: DocumentFileX) { + private suspend fun scanMediaFile(cycle: ScanCycle, path: SimplePath, file: DocumentFileX) { try { - val path = file.name - explorer.addRelativePath(GrooveExplorer.Path(path)) - val mimeType = file.mimeType when { - mimeType == MIMETYPE_M3U -> scanM3UFile(cycle, file) - path.endsWith(".lrc") -> scanLrcFile(cycle, file) - mimeType.startsWith("audio/") -> scanAudioFile(cycle, file) + path.extension == "lrc" -> scanLrcFile(cycle, path, file) + file.mimeType == MIMETYPE_M3U -> scanM3UFile(cycle, path, file) + file.mimeType.startsWith("audio/") -> scanAudioFile(cycle, path, file) } } catch (err: Exception) { Logger.error("MediaExposer", "scan media file failed", err) } } - private suspend fun scanAudioFile(cycle: ScanCycle, file: DocumentFileX) { - val path = file.name - uris[path] = file.uri + private suspend fun scanAudioFile(cycle: ScanCycle, path: SimplePath, file: DocumentFileX) { + val pathString = path.pathString + uris[pathString] = file.uri val lastModified = file.lastModified - val cached = cycle.songCache[path]?.takeIf { + val cached = cycle.songCache[pathString]?.takeIf { it.dateModified == lastModified - && it.coverFile?.let { x -> cycle.artworkCacheUnused.contains(x) } ?: true + && it.coverFile?.let { x -> cycle.artworkCacheUnused.contains(x) } != false } - val song = cached ?: Song.parse(symphony, file) ?: return + val song = cached ?: Song.parse(symphony, path, file) ?: return if (cached == null) { - symphony.database.songCache.put(path, song) + symphony.database.songCache.insert(song) } - cycle.songCacheUnused.remove(path) + cycle.songCacheUnused.remove(pathString) song.coverFile?.let { cycle.artworkCacheUnused.remove(it) } cycle.lyricsCacheUnused.remove(song.id) + explorer.addChildFile(path) withContext(Dispatchers.Main) { emitSong(song) } } - private fun scanLrcFile(cycle: ScanCycle, file: DocumentFileX) { - val path = file.name - uris[path] = file.uri + private fun scanLrcFile( + @Suppress("Unused") cycle: ScanCycle, + path: SimplePath, + file: DocumentFileX, + ) { + uris[path.pathString] = file.uri + explorer.addChildFile(path) } - private fun scanM3UFile(cycle: ScanCycle, file: DocumentFileX) { - val path = file.name - uris[path] = file.uri + private fun scanM3UFile( + @Suppress("Unused") cycle: ScanCycle, + path: SimplePath, + file: DocumentFileX, + ) { + uris[path.pathString] = file.uri + explorer.addChildFile(path) } - private fun trimCache(cycle: ScanCycle) { + private suspend fun trimCache(cycle: ScanCycle) { try { symphony.database.songCache.delete(cycle.songCacheUnused) } catch (err: Exception) { @@ -153,7 +179,7 @@ class MediaExposer(private val symphony: Symphony) { fun reset() { emitUpdate(true) uris.clear() - explorer = GrooveExplorer.Folder() + explorer = SimpleFileSystem.Folder() emitUpdate(false) } diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/groove/Playlist.kt b/app/src/main/java/io/github/zyrouge/symphony/services/groove/Playlist.kt index aae203d5..71bc98dd 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/groove/Playlist.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/groove/Playlist.kt @@ -1,49 +1,44 @@ package io.github.zyrouge.symphony.services.groove import android.net.Uri +import android.provider.DocumentsContract import androidx.compose.runtime.Immutable +import androidx.room.Entity +import androidx.room.PrimaryKey import io.github.zyrouge.symphony.Symphony import io.github.zyrouge.symphony.ui.helpers.Assets import io.github.zyrouge.symphony.utils.DocumentFileX -import io.github.zyrouge.symphony.utils.RelaxedJsonDecoder -import io.github.zyrouge.symphony.utils.UriSerializer -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable -import kotlinx.serialization.encodeToString -import kotlinx.serialization.json.Json +import io.github.zyrouge.symphony.utils.SimplePath import kotlin.io.path.Path -import kotlin.io.path.name import kotlin.io.path.nameWithoutExtension -import kotlin.io.path.pathString @Immutable -@Serializable +@Entity("playlists") data class Playlist( - @SerialName(KEY_ID) + @PrimaryKey val id: String, - @SerialName(KEY_TITLE) val title: String, - @SerialName(KEY_SONG_PATHS) val songPaths: List, - @SerialName(KEY_URI) - @Serializable(UriSerializer::class) val uri: Uri?, - @SerialName(KEY_PATH) val path: String?, ) { val numberOfTracks: Int get() = songPaths.size - val basename: String get() = path?.let { Path(it).name } ?: "$title.m3u" - private val dirname: String? get() = path?.let { Path(it).parent.pathString } + val isLocal get() = uri != null + val isNotLocal get() = uri == null fun createArtworkImageRequest(symphony: Symphony) = getSongIds(symphony).firstOrNull() ?.let { symphony.groove.song.get(it)?.createArtworkImageRequest(symphony) } ?: Assets.createPlaceholderImageRequest(symphony) - fun getSongIds(symphony: Symphony) = songPaths.mapNotNull { x -> - symphony.groove.song.pathCache[x] - ?: dirname?.let { symphony.groove.song.pathCache[Path(it, x).pathString] } - ?: symphony.groove.song.pathCache[Path(ROOT_STORAGE, x).pathString] + fun getSongIds(symphony: Symphony): List { + val parentPath = path?.let { SimplePath(it) } + val primaryPath = SimplePath(PRIMARY_STORAGE) + return songPaths.mapNotNull { x -> + symphony.groove.song.pathCache[x] + ?: parentPath?.let { symphony.groove.song.pathCache[it.join(x).pathString] } + ?: symphony.groove.song.pathCache[primaryPath.join(x).pathString] + } } fun getSortedSongIds(symphony: Symphony) = symphony.groove.song.sort( @@ -52,9 +47,6 @@ data class Playlist( symphony.settings.lastUsedPlaylistSongsSortReverse.value, ) - fun isLocal() = uri != null - fun isNotLocal() = uri == null - fun withTitle(title: String) = Playlist( id = id, title = title, @@ -63,31 +55,19 @@ data class Playlist( path = path, ) - fun toJson() = Json.encodeToString(this) - companion object { - private const val ROOT_STORAGE = "/storage/emulated/0" - - const val KEY_ID = "0" - const val KEY_TITLE = "1" - const val KEY_SONG_PATHS = "2" - const val KEY_URI = "3" - const val KEY_PATH = "4" - - fun fromJson(json: String) = RelaxedJsonDecoder.decodeFromString(json) + private const val PRIMARY_STORAGE = "primary:" fun parse(symphony: Symphony, uri: Uri): Playlist { val file = DocumentFileX.fromSingleUri(symphony.applicationContext, uri)!! - val path = file.name!! val content = symphony.applicationContext.contentResolver.openInputStream(uri) ?.use { String(it.readBytes()) } ?: "" val songPaths = content.lineSequence() .map { it.trim() } - .filter { - it.isNotEmpty() && it[0] != '#' - } + .filter { it.isNotEmpty() && it[0] != '#' } .toList() val id = symphony.groove.playlist.idGenerator.next() + val path = runCatching { DocumentsContract.getDocumentId(uri) }.getOrNull() ?: file.name return Playlist( id = id, title = Path(path).nameWithoutExtension, diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/groove/PlaylistRepository.kt b/app/src/main/java/io/github/zyrouge/symphony/services/groove/PlaylistRepository.kt index 375823b0..1ced0f29 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/groove/PlaylistRepository.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/groove/PlaylistRepository.kt @@ -1,30 +1,27 @@ package io.github.zyrouge.symphony.services.groove -import android.content.Intent import android.net.Uri import io.github.zyrouge.symphony.Symphony +import io.github.zyrouge.symphony.utils.ActivityUtils import io.github.zyrouge.symphony.utils.FuzzySearchOption import io.github.zyrouge.symphony.utils.FuzzySearcher import io.github.zyrouge.symphony.utils.KeyGenerator import io.github.zyrouge.symphony.utils.Logger import io.github.zyrouge.symphony.utils.mutate -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.async import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -import kotlinx.coroutines.runBlocking import java.io.FileNotFoundException import java.util.concurrent.ConcurrentHashMap -enum class PlaylistSortBy { - CUSTOM, - TITLE, - TRACKS_COUNT, -} - class PlaylistRepository(private val symphony: Symphony) { + enum class SortBy { + CUSTOM, + TITLE, + TRACKS_COUNT, + } + private val cache = ConcurrentHashMap() internal val idGenerator = KeyGenerator.TimeIncremental() private val searcher = FuzzySearcher( @@ -54,34 +51,26 @@ class PlaylistRepository(private val symphony: Symphony) { cache.size } - fun fetch() { + suspend fun fetch() { emitUpdate(true) try { val context = symphony.applicationContext - val permissions = Intent.FLAG_GRANT_READ_URI_PERMISSION - val playlists = symphony.database.playlists.all() - runBlocking { - playlists.values.map { x -> - val playlist = when { - x.isLocal() -> { - context.contentResolver.takePersistableUriPermission( - x.uri!!, - permissions - ) - async(Dispatchers.IO) { - Playlist.parse(symphony, x.uri) - }.await() - } - - else -> x + val playlists = symphony.database.playlists.entries() + playlists.values.map { x -> + val playlist = when { + x.isLocal -> { + ActivityUtils.makePersistableReadableUri(context, x.uri!!) + Playlist.parse(symphony, x.uri) } - cache[playlist.id] = playlist - _all.update { - it + playlist.id - } - emitUpdateId() - emitCount() + + else -> x + } + cache[playlist.id] = playlist + _all.update { + it + playlist.id } + emitUpdateId() + emitCount() } } catch (_: FileNotFoundException) { } catch (err: Exception) { @@ -111,17 +100,17 @@ class PlaylistRepository(private val symphony: Symphony) { fun search(playlistIds: List, terms: String, limit: Int = 7) = searcher .search(terms, playlistIds, maxLength = limit) - fun sort(playlistIds: List, by: PlaylistSortBy, reverse: Boolean): List { + fun sort(playlistIds: List, by: SortBy, reverse: Boolean): List { val sorted = when (by) { - PlaylistSortBy.CUSTOM -> { + SortBy.CUSTOM -> { val prefix = listOfNotNull(FAVORITE_PLAYLIST) val others = playlistIds.toMutableList() prefix.forEach { others.remove(it) } prefix + others } - PlaylistSortBy.TITLE -> playlistIds.sortedBy { get(it)?.title } - PlaylistSortBy.TRACKS_COUNT -> playlistIds.sortedBy { get(it)?.numberOfTracks } + SortBy.TITLE -> playlistIds.sortedBy { get(it)?.title } + SortBy.TRACKS_COUNT -> playlistIds.sortedBy { get(it)?.numberOfTracks } } return if (reverse) sorted.reversed() else sorted } @@ -142,27 +131,27 @@ class PlaylistRepository(private val symphony: Symphony) { path = null, ) - fun add(playlist: Playlist) { + suspend fun add(playlist: Playlist) { cache[playlist.id] = playlist _all.update { it + playlist.id } emitUpdateId() emitCount() - symphony.database.playlists.put(playlist.id, playlist) + symphony.database.playlists.insert(playlist) } - fun delete(id: String) { + suspend fun delete(id: String) { cache.remove(id) _all.update { it - id } emitUpdateId() emitCount() - symphony.database.playlists.delete(id) + symphony.database.playlists.delete(listOf(id)) } - fun update(id: String, songIds: List) { + suspend fun update(id: String, songIds: List) { val old = get(id) ?: return val new = Playlist( id = id, @@ -179,7 +168,7 @@ class PlaylistRepository(private val symphony: Symphony) { songIds } } - symphony.database.playlists.put(id, new) + symphony.database.playlists.update(new) } // NOTE: maybe we shouldn't use groove's coroutine scope? @@ -216,11 +205,11 @@ class PlaylistRepository(private val symphony: Symphony) { } } - fun renamePlaylist(playlist: Playlist, title: String) { + suspend fun renamePlaylist(playlist: Playlist, title: String) { val renamed = playlist.withTitle(title) cache[playlist.id] = renamed emitUpdateId() - symphony.database.playlists.put(playlist.id, renamed) + symphony.database.playlists.update(renamed) } private fun createFavorites(): Playlist { diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/groove/Song.kt b/app/src/main/java/io/github/zyrouge/symphony/services/groove/Song.kt index fec54518..5957e96a 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/groove/Song.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/groove/Song.kt @@ -2,66 +2,40 @@ package io.github.zyrouge.symphony.services.groove import android.net.Uri import androidx.compose.runtime.Immutable +import androidx.room.Entity +import androidx.room.PrimaryKey import io.github.zyrouge.metaphony.AudioArtwork import io.github.zyrouge.metaphony.AudioParser import io.github.zyrouge.symphony.Symphony import io.github.zyrouge.symphony.utils.DocumentFileX -import io.github.zyrouge.symphony.utils.RelaxedJsonDecoder -import io.github.zyrouge.symphony.utils.UriSerializer -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable -import kotlinx.serialization.encodeToString -import kotlinx.serialization.json.Json +import io.github.zyrouge.symphony.utils.SimplePath import java.math.RoundingMode -import kotlin.io.path.Path @Immutable -@Serializable +@Entity("songs") data class Song( - @SerialName(KEY_ID) + @PrimaryKey val id: String, - @SerialName(KEY_TITLE) val title: String, - @SerialName(KEY_ALBUM) val album: String?, - @SerialName(KEY_ARTISTS) val artists: Set, - @SerialName(KEY_COMPOSERS) val composers: Set, - @SerialName(KEY_ALBUM_ARTISTS) val albumArtists: Set, - @SerialName(KEY_GENRES) val genres: Set, - @SerialName(KEY_TRACK_NUMBER) val trackNumber: Int?, - @SerialName(KEY_TRACK_TOTAL) val trackTotal: Int?, - @SerialName(KEY_DISC_NUMBER) val discNumber: Int?, - @SerialName(KEY_DISC_TOTAL) val discTotal: Int?, - @SerialName(KEY_YEAR) val year: Int?, - @SerialName(KEY_DURATION) val duration: Long, - @SerialName(KEY_BITRATE) val bitrate: Long?, - @SerialName(KEY_BITS_PER_SAMPLE) val bitsPerSample: Int?, - @SerialName(KEY_SAMPLING_RATE) val samplingRate: Long?, - @SerialName(KEY_CODEC) val codec: String?, - @SerialName(KEY_DATE_MODIFIED) val dateModified: Long, - @SerialName(KEY_SIZE) val size: Long, - @SerialName(KEY_COVER_FILE) val coverFile: String?, - @SerialName(KEY_URI) - @Serializable(UriSerializer::class) val uri: Uri, - @SerialName(KEY_PATH) val path: String, ) { val bitrateK: Long? get() = bitrate?.let { it / 1000 } @@ -73,7 +47,7 @@ data class Song( .toFloat() } - val filename = Path(path).fileName.toString() + val filename get() = SimplePath(path).name fun createArtworkImageRequest(symphony: Symphony) = symphony.groove.song.createArtworkImageRequest(id) @@ -96,40 +70,10 @@ data class Song( } } - fun toJson() = Json.encodeToString(this) - companion object { - const val KEY_TITLE = "0" - const val KEY_ALBUM = "1" - const val KEY_ARTISTS = "2" - const val KEY_COMPOSERS = "3" - const val KEY_ALBUM_ARTISTS = "4" - const val KEY_GENRES = "5" - const val KEY_TRACK_NUMBER = "6" - const val KEY_TRACK_TOTAL = "7" - const val KEY_DISC_NUMBER = "8" - const val KEY_DISC_TOTAL = "9" - const val KEY_YEAR = "10" - const val KEY_DURATION = "11" - const val KEY_BITRATE = "12" - const val KEY_BITS_PER_SAMPLE = "13" - const val KEY_SAMPLING_RATE = "14" - const val KEY_CODEC = "15" - const val KEY_DATE_MODIFIED = "17" - const val KEY_SIZE = "18" - const val KEY_URI = "19" - const val KEY_PATH = "20" - const val KEY_ID = "21" - const val KEY_COVER_FILE = "22" - - fun fromJson(json: String) = RelaxedJsonDecoder.decodeFromString(json) - - fun parse(symphony: Symphony, file: DocumentFileX): Song? { - val path = file.name - val mimeType = file.mimeType - val uri = file.uri - val audio = symphony.applicationContext.contentResolver.openInputStream(uri) - ?.use { AudioParser.read(it, mimeType) } + fun parse(symphony: Symphony, path: SimplePath, file: DocumentFileX): Song? { + val audio = symphony.applicationContext.contentResolver.openInputStream(file.uri) + ?.use { AudioParser.read(it, file.mimeType) } ?: return null val metadata = audio.getMetadata() val stream = audio.getStreamInfo() @@ -149,7 +93,7 @@ data class Song( } return Song( id = id, - title = metadata.title ?: path, // TODO + title = metadata.title ?: path.nameWithoutExtension, // TODO album = metadata.album, artists = metadata.artists, composers = metadata.composer, @@ -168,8 +112,8 @@ data class Song( dateModified = file.lastModified, size = file.size, coverFile = coverFile, - uri = uri, - path = path, + uri = file.uri, + path = path.pathString, ) } diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/groove/SongRepository.kt b/app/src/main/java/io/github/zyrouge/symphony/services/groove/SongRepository.kt index db52708a..a300600f 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/groove/SongRepository.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/groove/SongRepository.kt @@ -9,29 +9,29 @@ import io.github.zyrouge.symphony.utils.FuzzySearchOption import io.github.zyrouge.symphony.utils.FuzzySearcher import io.github.zyrouge.symphony.utils.KeyGenerator import io.github.zyrouge.symphony.utils.Logger +import io.github.zyrouge.symphony.utils.SimpleFileSystem +import io.github.zyrouge.symphony.utils.SimplePath import io.github.zyrouge.symphony.utils.joinToStringIfNotEmpty import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import java.util.concurrent.ConcurrentHashMap -import kotlin.io.path.Path -import kotlin.io.path.nameWithoutExtension - -enum class SongSortBy { - CUSTOM, - TITLE, - ARTIST, - ALBUM, - DURATION, - DATE_MODIFIED, - COMPOSER, - ALBUM_ARTIST, - YEAR, - FILENAME, - TRACK_NUMBER, -} class SongRepository(private val symphony: Symphony) { + enum class SortBy { + CUSTOM, + TITLE, + ARTIST, + ALBUM, + DURATION, + DATE_MODIFIED, + COMPOSER, + ALBUM_ARTIST, + YEAR, + FILENAME, + TRACK_NUMBER, + } + private val cache = ConcurrentHashMap() internal val pathCache = ConcurrentHashMap() internal val idGenerator = KeyGenerator.TimeIncremental() @@ -51,7 +51,7 @@ class SongRepository(private val symphony: Symphony) { val count = _count.asStateFlow() private val _id = MutableStateFlow(System.currentTimeMillis()) val id = _id.asStateFlow() - var explorer = GrooveExplorer.Folder() + var explorer = SimpleFileSystem.Folder() private fun emitCount() = _count.update { cache.size } @@ -62,9 +62,7 @@ class SongRepository(private val symphony: Symphony) { internal fun onSong(song: Song) { cache[song.id] = song pathCache[song.path] = song.id - val entity = explorer - .addRelativePath(GrooveExplorer.Path(song.path)) as GrooveExplorer.File - entity.data = song.id + explorer.addChildFile(SimplePath(song.path)).data = song.id emitIds() _all.update { it + song.id @@ -77,7 +75,7 @@ class SongRepository(private val symphony: Symphony) { fun reset() { cache.clear() pathCache.clear() - explorer = GrooveExplorer.Folder() + explorer = SimpleFileSystem.Folder() emitIds() _all.update { emptyList() @@ -88,19 +86,19 @@ class SongRepository(private val symphony: Symphony) { fun search(songIds: List, terms: String, limit: Int = 7) = searcher .search(terms, songIds, maxLength = limit) - fun sort(songIds: List, by: SongSortBy, reverse: Boolean): List { + fun sort(songIds: List, by: SortBy, reverse: Boolean): List { val sorted = when (by) { - SongSortBy.CUSTOM -> songIds - SongSortBy.TITLE -> songIds.sortedBy { get(it)?.title } - SongSortBy.ARTIST -> songIds.sortedBy { get(it)?.artists?.joinToStringIfNotEmpty() } - SongSortBy.ALBUM -> songIds.sortedBy { get(it)?.album } - SongSortBy.DURATION -> songIds.sortedBy { get(it)?.duration } - SongSortBy.DATE_MODIFIED -> songIds.sortedBy { get(it)?.dateModified } - SongSortBy.COMPOSER -> songIds.sortedBy { get(it)?.composers?.joinToStringIfNotEmpty() } - SongSortBy.ALBUM_ARTIST -> songIds.sortedBy { get(it)?.albumArtists?.joinToStringIfNotEmpty() } - SongSortBy.YEAR -> songIds.sortedBy { get(it)?.year } - SongSortBy.FILENAME -> songIds.sortedBy { get(it)?.filename } - SongSortBy.TRACK_NUMBER -> songIds.sortedBy { get(it)?.trackNumber } + SortBy.CUSTOM -> songIds + SortBy.TITLE -> songIds.sortedBy { get(it)?.title } + SortBy.ARTIST -> songIds.sortedBy { get(it)?.artists?.joinToStringIfNotEmpty() } + SortBy.ALBUM -> songIds.sortedBy { get(it)?.album } + SortBy.DURATION -> songIds.sortedBy { get(it)?.duration } + SortBy.DATE_MODIFIED -> songIds.sortedBy { get(it)?.dateModified } + SortBy.COMPOSER -> songIds.sortedBy { get(it)?.composers?.joinToStringIfNotEmpty() } + SortBy.ALBUM_ARTIST -> songIds.sortedBy { get(it)?.albumArtists?.joinToStringIfNotEmpty() } + SortBy.YEAR -> songIds.sortedBy { get(it)?.year } + SortBy.FILENAME -> songIds.sortedBy { get(it)?.filename } + SortBy.TRACK_NUMBER -> songIds.sortedBy { get(it)?.trackNumber } } return if (reverse) sorted.reversed() else sorted } @@ -126,7 +124,7 @@ class SongRepository(private val symphony: Symphony) { suspend fun getLyrics(song: Song): String? { try { - val lrcFilePath = Path(song.path).nameWithoutExtension + ".lrc" + val lrcFilePath = SimplePath(song.path).nameWithoutExtension + ".lrc" symphony.groove.exposer.uris[lrcFilePath]?.let { uri -> symphony.applicationContext.contentResolver .openInputStream(uri) diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/i18n/Translation.kt b/app/src/main/java/io/github/zyrouge/symphony/services/i18n/Translation.kt index c714209d..158d32cb 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/i18n/Translation.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/i18n/Translation.kt @@ -1,15 +1,17 @@ package io.github.zyrouge.symphony.services.i18n -import io.github.zyrouge.symphony.utils.RelaxedJsonDecoder import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.json.Json import kotlinx.serialization.json.decodeFromStream import java.io.InputStream class Translation(container: _Container) : _Translation(container) { companion object { + private val json = Json { ignoreUnknownKeys = true } + @OptIn(ExperimentalSerializationApi::class) fun fromInputStream(input: InputStream): Translation { - val container = RelaxedJsonDecoder.decodeFromStream<_Container>(input) + val container = json.decodeFromStream<_Container>(input) return Translation(container) } } 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 bc8373fa..2b410fcb 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 @@ -39,7 +39,7 @@ fun AddToPlaylistDialog( derivedStateOf { allPlaylistsIds .mapNotNull { context.symphony.groove.playlist.get(it) } - .filter { it.isNotLocal() } + .filter { it.isNotLocal } .toMutableStateList() } } 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 3ce77ac8..484aaf12 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 @@ -10,10 +10,9 @@ 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.AlbumArtistSortBy +import io.github.zyrouge.symphony.services.groove.AlbumArtistRepository import io.github.zyrouge.symphony.services.groove.GrooveKinds import io.github.zyrouge.symphony.ui.helpers.ViewContext -import io.github.zyrouge.symphony.utils.wrapInViewContext @Composable fun AlbumArtistGrid( @@ -38,8 +37,8 @@ fun AlbumArtistGrid( context.symphony.settings.setLastUsedAlbumArtistsSortReverse(it) }, sort = sortBy, - sorts = AlbumArtistSortBy.entries - .associateWith { x -> wrapInViewContext { x.label(context) } }, + sorts = AlbumArtistRepository.SortBy.entries + .associateWith { x -> ViewContext.parameterizedFn { x.label(context) } }, onSortChange = { context.symphony.settings.setLastUsedAlbumArtistsSortBy(it) }, @@ -82,9 +81,9 @@ fun AlbumArtistGrid( ) } -private fun AlbumArtistSortBy.label(context: ViewContext) = when (this) { - AlbumArtistSortBy.CUSTOM -> context.symphony.t.Custom - AlbumArtistSortBy.ARTIST_NAME -> context.symphony.t.Artist - AlbumArtistSortBy.ALBUMS_COUNT -> context.symphony.t.AlbumCount - AlbumArtistSortBy.TRACKS_COUNT -> context.symphony.t.TrackCount +private fun AlbumArtistRepository.SortBy.label(context: ViewContext) = when (this) { + AlbumArtistRepository.SortBy.CUSTOM -> context.symphony.t.Custom + AlbumArtistRepository.SortBy.ARTIST_NAME -> context.symphony.t.Artist + AlbumArtistRepository.SortBy.ALBUMS_COUNT -> context.symphony.t.AlbumCount + AlbumArtistRepository.SortBy.TRACKS_COUNT -> context.symphony.t.TrackCount } 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 65b6d862..7fb1859c 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 @@ -10,10 +10,9 @@ 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.AlbumSortBy +import io.github.zyrouge.symphony.services.groove.AlbumRepository import io.github.zyrouge.symphony.services.groove.GrooveKinds import io.github.zyrouge.symphony.ui.helpers.ViewContext -import io.github.zyrouge.symphony.utils.wrapInViewContext @Composable fun AlbumGrid( @@ -38,7 +37,9 @@ fun AlbumGrid( context.symphony.settings.setLastUsedAlbumsSortReverse(it) }, sort = sortBy, - sorts = AlbumSortBy.entries.associateWith { x -> wrapInViewContext { x.label(it) } }, + sorts = AlbumRepository.SortBy.entries.associateWith { x -> + ViewContext.parameterizedFn { x.label(it) } + }, onSortChange = { context.symphony.settings.setLastUsedAlbumsSortBy(it) }, @@ -76,9 +77,9 @@ fun AlbumGrid( ) } -private fun AlbumSortBy.label(context: ViewContext) = when (this) { - AlbumSortBy.CUSTOM -> context.symphony.t.Custom - AlbumSortBy.ALBUM_NAME -> context.symphony.t.Album - AlbumSortBy.ARTIST_NAME -> context.symphony.t.Artist - AlbumSortBy.TRACKS_COUNT -> context.symphony.t.TrackCount +private fun AlbumRepository.SortBy.label(context: ViewContext) = when (this) { + AlbumRepository.SortBy.CUSTOM -> context.symphony.t.Custom + AlbumRepository.SortBy.ALBUM_NAME -> context.symphony.t.Album + AlbumRepository.SortBy.ARTIST_NAME -> context.symphony.t.Artist + AlbumRepository.SortBy.TRACKS_COUNT -> context.symphony.t.TrackCount } 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 a88f907d..c57989ff 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 @@ -10,10 +10,9 @@ 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.ArtistSortBy +import io.github.zyrouge.symphony.services.groove.ArtistRepository import io.github.zyrouge.symphony.services.groove.GrooveKinds import io.github.zyrouge.symphony.ui.helpers.ViewContext -import io.github.zyrouge.symphony.utils.wrapInViewContext @Composable fun ArtistGrid( @@ -38,8 +37,8 @@ fun ArtistGrid( context.symphony.settings.setLastUsedArtistsSortReverse(it) }, sort = sortBy, - sorts = ArtistSortBy.entries - .associateWith { x -> wrapInViewContext { x.label(it) } }, + sorts = ArtistRepository.SortBy.entries + .associateWith { x -> ViewContext.parameterizedFn { x.label(it) } }, onSortChange = { context.symphony.settings.setLastUsedArtistsSortBy(it) }, @@ -77,9 +76,9 @@ fun ArtistGrid( ) } -private fun ArtistSortBy.label(context: ViewContext) = when (this) { - ArtistSortBy.CUSTOM -> context.symphony.t.Custom - ArtistSortBy.ARTIST_NAME -> context.symphony.t.Artist - ArtistSortBy.ALBUMS_COUNT -> context.symphony.t.AlbumCount - ArtistSortBy.TRACKS_COUNT -> context.symphony.t.TrackCount +private fun ArtistRepository.SortBy.label(context: ViewContext) = when (this) { + ArtistRepository.SortBy.CUSTOM -> context.symphony.t.Custom + ArtistRepository.SortBy.ARTIST_NAME -> context.symphony.t.Artist + ArtistRepository.SortBy.ALBUMS_COUNT -> context.symphony.t.AlbumCount + ArtistRepository.SortBy.TRACKS_COUNT -> context.symphony.t.TrackCount } 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 d42c696a..9831784d 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 @@ -32,12 +32,11 @@ import androidx.compose.ui.text.font.FontWeight 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.GenreSortBy +import io.github.zyrouge.symphony.services.groove.GenreRepository import io.github.zyrouge.symphony.services.groove.GrooveKinds import io.github.zyrouge.symphony.ui.helpers.Routes import io.github.zyrouge.symphony.ui.helpers.ViewContext import io.github.zyrouge.symphony.ui.helpers.navigateTo -import io.github.zyrouge.symphony.utils.wrapInViewContext private object GenreTile { val colors = listOf( @@ -87,8 +86,8 @@ fun GenreGrid( context.symphony.settings.setLastUsedGenresSortReverse(it) }, sort = sortBy, - sorts = GenreSortBy.entries - .associateWith { x -> wrapInViewContext { x.label(it) } }, + sorts = GenreRepository.SortBy.entries + .associateWith { x -> ViewContext.parameterizedFn { x.label(it) } }, onSortChange = { context.symphony.settings.setLastUsedGenresSortBy(it) }, @@ -185,8 +184,8 @@ fun GenreGrid( ) } -private fun GenreSortBy.label(context: ViewContext) = when (this) { - GenreSortBy.CUSTOM -> context.symphony.t.Custom - GenreSortBy.GENRE -> context.symphony.t.Genre - GenreSortBy.TRACKS_COUNT -> context.symphony.t.TrackCount +private fun GenreRepository.SortBy.label(context: ViewContext) = when (this) { + GenreRepository.SortBy.CUSTOM -> context.symphony.t.Custom + GenreRepository.SortBy.GENRE -> context.symphony.t.Genre + GenreRepository.SortBy.TRACKS_COUNT -> context.symphony.t.TrackCount } diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/components/LongPressCopyableText.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/components/LongPressCopyableText.kt index 6d76f32c..28797bae 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/components/LongPressCopyableText.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/components/LongPressCopyableText.kt @@ -6,7 +6,7 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.input.pointer.pointerInput import io.github.zyrouge.symphony.ui.helpers.ViewContext -import io.github.zyrouge.symphony.utils.copyToClipboardWithToast +import io.github.zyrouge.symphony.utils.ActivityUtils @Composable fun LongPressCopyableText(context: ViewContext, text: String) { @@ -14,7 +14,7 @@ fun LongPressCopyableText(context: ViewContext, text: String) { text, modifier = Modifier.pointerInput(Unit) { detectTapGestures(onLongPress = { - copyToClipboardWithToast(context, text) + ActivityUtils.copyToClipboardAndNotify(context.symphony, text) }) } ) 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 07933907..6f7b0772 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 @@ -12,9 +12,8 @@ import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import io.github.zyrouge.symphony.services.groove.GrooveKinds -import io.github.zyrouge.symphony.services.groove.PlaylistSortBy +import io.github.zyrouge.symphony.services.groove.PlaylistRepository import io.github.zyrouge.symphony.ui.helpers.ViewContext -import io.github.zyrouge.symphony.utils.wrapInViewContext @Composable fun PlaylistGrid( @@ -42,8 +41,8 @@ fun PlaylistGrid( context.symphony.settings.setLastUsedPlaylistsSortReverse(it) }, sort = sortBy, - sorts = PlaylistSortBy.entries - .associateWith { x -> wrapInViewContext { x.label(it) } }, + sorts = PlaylistRepository.SortBy.entries + .associateWith { x -> ViewContext.parameterizedFn { x.label(it) } }, onSortChange = { context.symphony.settings.setLastUsedPlaylistsSortBy(it) }, @@ -88,8 +87,8 @@ fun PlaylistGrid( ) } -private fun PlaylistSortBy.label(context: ViewContext) = when (this) { - PlaylistSortBy.CUSTOM -> context.symphony.t.Custom - PlaylistSortBy.TITLE -> context.symphony.t.Title - PlaylistSortBy.TRACKS_COUNT -> context.symphony.t.TrackCount +private fun PlaylistRepository.SortBy.label(context: ViewContext) = when (this) { + PlaylistRepository.SortBy.CUSTOM -> context.symphony.t.Custom + PlaylistRepository.SortBy.TITLE -> context.symphony.t.Title + PlaylistRepository.SortBy.TRACKS_COUNT -> context.symphony.t.TrackCount } diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/components/PlaylistInformationDialog.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/components/PlaylistInformationDialog.kt index dab71821..8de13300 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/components/PlaylistInformationDialog.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/components/PlaylistInformationDialog.kt @@ -23,7 +23,7 @@ fun PlaylistInformationDialog( LongPressCopyableText( context, when { - playlist.isLocal() -> context.symphony.t.LocalStorage + playlist.isLocal -> context.symphony.t.LocalStorage else -> context.symphony.t.AppBuiltIn } ) 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 3ed0345c..4482b26e 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 @@ -149,7 +149,7 @@ fun PlaylistDropdownMenu( context.symphony.groove.playlist.savePlaylistToUri(playlist, uri) Toast.makeText( context.activity, - context.symphony.t.ExportedX(playlist.basename), + context.symphony.t.ExportedX(playlist.title), Toast.LENGTH_SHORT, ).show() } catch (err: Exception) { @@ -217,7 +217,7 @@ fun PlaylistDropdownMenu( showAddToPlaylistDialog = true } ) - if (playlist.isNotLocal()) { + if (playlist.isNotLocal) { DropdownMenuItem( leadingIcon = { Icon(Icons.AutoMirrored.Filled.PlaylistAdd, null) @@ -243,7 +243,7 @@ fun PlaylistDropdownMenu( showInfoDialog = true } ) - if (playlist.isNotLocal()) { + if (playlist.isNotLocal) { DropdownMenuItem( leadingIcon = { Icon(Icons.Filled.Save, null) @@ -254,7 +254,7 @@ fun PlaylistDropdownMenu( onClick = { onDismissRequest() try { - savePlaylistLauncher.launch(playlist.basename) + savePlaylistLauncher.launch("${playlist.title}.m3u") } catch (err: Exception) { Logger.error("PlaylistTile", "export failed", err) Toast.makeText( 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 0c8d0171..b50b93db 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 @@ -46,17 +46,16 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp -import io.github.zyrouge.symphony.services.groove.GrooveExplorer import io.github.zyrouge.symphony.services.groove.GrooveKinds -import io.github.zyrouge.symphony.services.groove.SongSortBy +import io.github.zyrouge.symphony.services.groove.SongRepository import io.github.zyrouge.symphony.services.radio.Radio import io.github.zyrouge.symphony.ui.helpers.ViewContext import io.github.zyrouge.symphony.ui.helpers.navigateToFolder -import io.github.zyrouge.symphony.utils.wrapInViewContext +import io.github.zyrouge.symphony.utils.SimpleFileSystem private data class SongExplorerResult( - val folders: List, - val files: Map, + val folders: List, + val files: Map, ) private const val SongFolderContentType = "folder" @@ -66,7 +65,7 @@ fun SongExplorerList( context: ViewContext, key: Any?, initialPath: List?, - explorer: GrooveExplorer.Folder, + explorer: SimpleFileSystem.Folder, onPathChange: (List) -> Unit, ) { var currentFolder by remember(key) { @@ -85,9 +84,9 @@ fun SongExplorerList( SongExplorerResult( folders = run { val sorted = when (sortBy) { - SongSortBy.TITLE, - SongSortBy.FILENAME, - -> categorized.folders.sortedBy { it.basename } + SongRepository.SortBy.TITLE, + SongRepository.SortBy.FILENAME, + -> categorized.folders.sortedBy { it.name } else -> categorized.folders } @@ -100,7 +99,7 @@ fun SongExplorerList( } } val currentPath by remember(currentFolder) { - derivedStateOf { currentFolder.pathParts } + derivedStateOf { currentFolder.fullPath.parts } } val currentPathScrollState = rememberScrollState() @@ -156,8 +155,8 @@ fun SongExplorerList( context.symphony.settings.setLastUsedBrowserSortReverse(it) }, sort = sortBy, - sorts = SongSortBy.entries - .associateWith { x -> wrapInViewContext { x.label(it) } }, + sorts = SongRepository.SortBy.entries + .associateWith { x -> ViewContext.parameterizedFn { x.label(it) } }, onSortChange = { context.symphony.settings.setLastUsedBrowserSortBy(it) }, @@ -227,7 +226,7 @@ fun SongExplorerList( Spacer(modifier = Modifier.width(20.dp)) Column(modifier = Modifier.weight(1f)) { Text( - folder.basename, + folder.name, maxLines = 1, overflow = TextOverflow.Ellipsis, ) @@ -277,17 +276,17 @@ fun SongExplorerList( } private data class GrooveExplorerCategorizedData( - val folders: List, - val files: Map, + val folders: List, + val files: Map, ) -private fun GrooveExplorer.Folder.categorizedChildren(): GrooveExplorerCategorizedData { - val folders = mutableListOf() - val files = mutableMapOf() +private fun SimpleFileSystem.Folder.categorizedChildren(): GrooveExplorerCategorizedData { + val folders = mutableListOf() + val files = mutableMapOf() children.values.forEach { entity -> when (entity) { - is GrooveExplorer.Folder -> folders.add(entity) - is GrooveExplorer.File -> { + is SimpleFileSystem.Folder -> folders.add(entity) + is SimpleFileSystem.File -> { files[entity.data as String] = entity } } @@ -295,9 +294,9 @@ private fun GrooveExplorer.Folder.categorizedChildren(): GrooveExplorerCategoriz return GrooveExplorerCategorizedData(folders = folders, files = files) } -private fun GrooveExplorer.Folder.childrenAsSongIds() = children.values.mapNotNull { entity -> +private fun SimpleFileSystem.Folder.childrenAsSongIds() = children.values.mapNotNull { entity -> when (entity) { - is GrooveExplorer.File -> entity.data as String + is SimpleFileSystem.File -> entity.data as String else -> null } } diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/components/SongInformationDialog.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/components/SongInformationDialog.kt index f13535f2..49a97630 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/components/SongInformationDialog.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/components/SongInformationDialog.kt @@ -13,8 +13,8 @@ import io.github.zyrouge.symphony.services.groove.Song import io.github.zyrouge.symphony.ui.helpers.Routes import io.github.zyrouge.symphony.ui.helpers.ViewContext import io.github.zyrouge.symphony.ui.helpers.navigateTo -import io.github.zyrouge.symphony.utils.DurationFormatter -import io.github.zyrouge.symphony.utils.copyToClipboardWithToast +import io.github.zyrouge.symphony.utils.ActivityUtils +import io.github.zyrouge.symphony.utils.DurationUtils import java.text.SimpleDateFormat import java.util.Date import kotlin.math.round @@ -88,7 +88,7 @@ fun SongInformationDialog(context: ViewContext, song: Song, onDismissRequest: () } } InformationKeyValue(context.symphony.t.Duration) { - LongPressCopyableText(context, DurationFormatter.formatMs(song.duration)) + LongPressCopyableText(context, DurationUtils.formatMs(song.duration)) } song.codec?.let { InformationKeyValue(context.symphony.t.Codec) { @@ -149,7 +149,7 @@ private fun LongPressCopyableAndTappableText( modifier = Modifier.pointerInput(Unit) { detectTapGestures( onLongPress = { _ -> - copyToClipboardWithToast(context, it) + ActivityUtils.copyToClipboardAndNotify(context.symphony, it) }, onTap = { _ -> onTap(it) diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/components/SongList.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/components/SongList.kt index df5dde20..6e23a720 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/components/SongList.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/components/SongList.kt @@ -17,10 +17,9 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import io.github.zyrouge.symphony.services.groove.GrooveKinds import io.github.zyrouge.symphony.services.groove.Song -import io.github.zyrouge.symphony.services.groove.SongSortBy +import io.github.zyrouge.symphony.services.groove.SongRepository import io.github.zyrouge.symphony.services.radio.Radio import io.github.zyrouge.symphony.ui.helpers.ViewContext -import io.github.zyrouge.symphony.utils.wrapInViewContext enum class SongListType { Default, @@ -58,8 +57,8 @@ fun SongList( type.setLastUsedSortReverse(context, it) }, sort = sortBy, - sorts = SongSortBy.entries - .associateWith { x -> wrapInViewContext { x.label(it) } }, + sorts = SongRepository.SortBy.entries + .associateWith { x -> ViewContext.parameterizedFn { x.label(it) } }, onSortChange = { type.setLastUsedSortBy(context, it) }, @@ -125,18 +124,18 @@ fun SongList( ) } -fun SongSortBy.label(context: ViewContext) = when (this) { - SongSortBy.CUSTOM -> context.symphony.t.Custom - SongSortBy.TITLE -> context.symphony.t.Title - SongSortBy.ARTIST -> context.symphony.t.Artist - SongSortBy.ALBUM -> context.symphony.t.Album - SongSortBy.DURATION -> context.symphony.t.Duration - SongSortBy.DATE_MODIFIED -> context.symphony.t.LastModified - SongSortBy.COMPOSER -> context.symphony.t.Composer - SongSortBy.ALBUM_ARTIST -> context.symphony.t.AlbumArtist - SongSortBy.YEAR -> context.symphony.t.Year - SongSortBy.FILENAME -> context.symphony.t.Filename - SongSortBy.TRACK_NUMBER -> context.symphony.t.TrackNumber +fun SongRepository.SortBy.label(context: ViewContext) = when (this) { + SongRepository.SortBy.CUSTOM -> context.symphony.t.Custom + SongRepository.SortBy.TITLE -> context.symphony.t.Title + SongRepository.SortBy.ARTIST -> context.symphony.t.Artist + SongRepository.SortBy.ALBUM -> context.symphony.t.Album + SongRepository.SortBy.DURATION -> context.symphony.t.Duration + SongRepository.SortBy.DATE_MODIFIED -> context.symphony.t.LastModified + SongRepository.SortBy.COMPOSER -> context.symphony.t.Composer + SongRepository.SortBy.ALBUM_ARTIST -> context.symphony.t.AlbumArtist + SongRepository.SortBy.YEAR -> context.symphony.t.Year + SongRepository.SortBy.FILENAME -> context.symphony.t.Filename + SongRepository.SortBy.TRACK_NUMBER -> context.symphony.t.TrackNumber } fun SongListType.getLastUsedSortBy(context: ViewContext) = when (this) { @@ -145,11 +144,12 @@ fun SongListType.getLastUsedSortBy(context: ViewContext) = when (this) { SongListType.Playlist -> context.symphony.settings.lastUsedPlaylistSongsSortBy } -fun SongListType.setLastUsedSortBy(context: ViewContext, sort: SongSortBy) = when (this) { - SongListType.Default -> context.symphony.settings.setLastUsedSongsSortBy(sort) - SongListType.Playlist -> context.symphony.settings.setLastUsedPlaylistSongsSortBy(sort) - SongListType.Album -> context.symphony.settings.setLastUsedAlbumSongsSortBy(sort) -} +fun SongListType.setLastUsedSortBy(context: ViewContext, sort: SongRepository.SortBy) = + when (this) { + SongListType.Default -> context.symphony.settings.setLastUsedSongsSortBy(sort) + SongListType.Playlist -> context.symphony.settings.setLastUsedPlaylistSongsSortBy(sort) + SongListType.Album -> context.symphony.settings.setLastUsedAlbumSongsSortBy(sort) + } fun SongListType.getLastUsedSortReverse(context: ViewContext) = when (this) { SongListType.Default -> context.symphony.settings.lastUsedSongsSortReverse 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 f6451db8..8acbfabe 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 @@ -55,11 +55,11 @@ import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp import coil.compose.AsyncImage -import io.github.zyrouge.symphony.services.groove.GrooveExplorer -import io.github.zyrouge.symphony.services.groove.PathSortBy -import io.github.zyrouge.symphony.services.groove.SongSortBy +import io.github.zyrouge.symphony.services.groove.SongRepository import io.github.zyrouge.symphony.services.radio.Radio import io.github.zyrouge.symphony.ui.helpers.ViewContext +import io.github.zyrouge.symphony.utils.SimplePath +import io.github.zyrouge.symphony.utils.StringListUtils @Composable fun SongTreeList( @@ -83,15 +83,14 @@ fun SongTreeList( val songsSortReverse by context.symphony.settings.lastUsedSongsSortReverse.collectAsState() val sortedTree by remember(tree, pathsSortBy, pathsSortReverse, songsSortBy, songsSortReverse) { derivedStateOf { - val pairs = - GrooveExplorer.sort(tree.keys.toList(), pathsSortBy, pathsSortReverse) - .map { - it to context.symphony.groove.song.sort( - tree[it]!!, - songsSortBy, - songsSortReverse - ) - } + val pairs = StringListUtils.sort(tree.keys.toList(), pathsSortBy, pathsSortReverse) + .map { + it to context.symphony.groove.song.sort( + tree[it]!!, + songsSortBy, + songsSortReverse + ) + } mapOf(*pairs.toTypedArray()) } } @@ -356,13 +355,13 @@ fun SongTreeListSongCardIconButton( private fun SongTreeListMediaSortBar( context: ViewContext, songsCount: Int, - pathsSortBy: PathSortBy, + pathsSortBy: StringListUtils.SortBy, pathsSortReverse: Boolean, - songsSortBy: SongSortBy, + songsSortBy: SongRepository.SortBy, songsSortReverse: Boolean, - setPathsSortBy: (PathSortBy) -> Unit, + setPathsSortBy: (StringListUtils.SortBy) -> Unit, setPathsSortReverse: (Boolean) -> Unit, - setSongsSortBy: (SongSortBy) -> Unit, + setSongsSortBy: (SongRepository.SortBy) -> Unit, setSongsSortReverse: (Boolean) -> Unit, ) { val currentTextStyle = MaterialTheme.typography.bodySmall.run { @@ -428,7 +427,7 @@ private fun SongTreeListMediaSortBar( style = currentTextStyle, modifier = Modifier.padding(16.dp, 8.dp), ) - PathSortBy.entries.forEach { sortBy -> + StringListUtils.SortBy.entries.forEach { sortBy -> SongTreeListMediaSortBarDropdownMenuItem( selected = pathsSortBy == sortBy, reversed = pathsSortReverse, @@ -448,7 +447,7 @@ private fun SongTreeListMediaSortBar( style = currentTextStyle, modifier = Modifier.padding(16.dp, 8.dp), ) - SongSortBy.entries.forEach { sortBy -> + SongRepository.SortBy.entries.forEach { sortBy -> SongTreeListMediaSortBarDropdownMenuItem( selected = songsSortBy == sortBy, reversed = songsSortReverse, @@ -515,9 +514,9 @@ private fun SongTreeListMediaSortBarDropdownMenuItem( ) } -fun PathSortBy.label(context: ViewContext) = when (this) { - PathSortBy.CUSTOM -> context.symphony.t.Custom - PathSortBy.NAME -> context.symphony.t.Name +fun StringListUtils.SortBy.label(context: ViewContext) = when (this) { + StringListUtils.SortBy.CUSTOM -> context.symphony.t.Custom + StringListUtils.SortBy.NAME -> context.symphony.t.Name } private fun createLinearTree( @@ -527,8 +526,8 @@ private fun createLinearTree( val result = mutableMapOf>() songIds.forEach { songId -> val song = context.symphony.groove.song.get(songId) ?: return@forEach - val parsedPath = GrooveExplorer.Path(song.path) - val dirname = parsedPath.dirname.toString() + val parsedPath = SimplePath(song.path) + val dirname = parsedPath.parent!!.pathString if (!result.containsKey(dirname)) { result[dirname] = mutableListOf() } diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/helpers/Context.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/helpers/Context.kt index 9ca7e447..4993de4b 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/helpers/Context.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/helpers/Context.kt @@ -7,5 +7,9 @@ import io.github.zyrouge.symphony.Symphony data class ViewContext( val symphony: Symphony, val activity: MainActivity, - val navController: NavHostController -) + val navController: NavHostController, +) { + companion object { + fun parameterizedFn(fn: (ViewContext) -> T) = fn + } +} diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/helpers/Explorer.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/helpers/Explorer.kt deleted file mode 100644 index 0e290efd..00000000 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/helpers/Explorer.kt +++ /dev/null @@ -1,14 +0,0 @@ -package io.github.zyrouge.symphony.ui.helpers - -import io.github.zyrouge.symphony.services.groove.GrooveExplorer - -fun GrooveExplorer.Folder.navigateToFolder(parts: List): GrooveExplorer.Folder? { - var folder: GrooveExplorer.Folder? = this - parts.forEach { part -> - folder = folder?.let { - val child = it.children[part] - if (child is GrooveExplorer.Folder) child else null - } - } - return folder -} diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/helpers/SimpleFileSystem.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/helpers/SimpleFileSystem.kt new file mode 100644 index 00000000..adcbb374 --- /dev/null +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/helpers/SimpleFileSystem.kt @@ -0,0 +1,14 @@ +package io.github.zyrouge.symphony.ui.helpers + +import io.github.zyrouge.symphony.utils.SimpleFileSystem + +fun SimpleFileSystem.Folder.navigateToFolder(parts: List): SimpleFileSystem.Folder? { + var folder: SimpleFileSystem.Folder? = this + parts.forEach { part -> + folder = folder?.let { + val child = it.children[part] + child as? SimpleFileSystem.Folder + } + } + return folder +} 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 bc816b74..bef87225 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 @@ -132,7 +132,7 @@ fun PlaylistView(context: ViewContext, playlistId: String) { type = SongListType.Playlist, disableHeartIcon = isFavoritesPlaylist, trailingOptionsContent = { _, song, onDismissRequest -> - playlist?.takeIf { it.isNotLocal() }?.let { + playlist?.takeIf { it.isNotLocal }?.let { DropdownMenuItem( leadingIcon = { Icon( diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/view/Settings.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/view/Settings.kt index c2a2bfe0..f566a1be 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/view/Settings.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/view/Settings.kt @@ -95,7 +95,7 @@ import io.github.zyrouge.symphony.ui.view.settings.SettingsSimpleTile import io.github.zyrouge.symphony.ui.view.settings.SettingsSliderTile import io.github.zyrouge.symphony.ui.view.settings.SettingsSwitchTile import io.github.zyrouge.symphony.ui.view.settings.SettingsTextInputTile -import io.github.zyrouge.symphony.utils.startBrowserActivity +import io.github.zyrouge.symphony.utils.ActivityUtils import kotlinx.coroutines.launch import kotlin.math.roundToInt @@ -202,7 +202,7 @@ fun SettingsView(context: ViewContext) { .fillMaxWidth() .background(MaterialTheme.colorScheme.primary) .clickable { - startBrowserActivity( + ActivityUtils.startBrowserActivity( context.activity, Uri.parse(AppMeta.contributingUrl) ) @@ -834,7 +834,7 @@ fun SettingsView(context: ViewContext) { else -> null }, onClick = { - startBrowserActivity( + ActivityUtils.startBrowserActivity( context.activity, Uri.parse(if (isLatestVersion) AppMeta.githubRepositoryUrl else AppMeta.githubLatestReleaseUrl) ) 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 74ef08b0..23ba8d4b 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 @@ -28,9 +28,7 @@ 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.GrooveExplorer import io.github.zyrouge.symphony.services.groove.GrooveKinds -import io.github.zyrouge.symphony.services.groove.PathSortBy import io.github.zyrouge.symphony.ui.components.AddToPlaylistDialog import io.github.zyrouge.symphony.ui.components.IconTextBody import io.github.zyrouge.symphony.ui.components.LoaderScaffold @@ -45,7 +43,8 @@ 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.wrapInViewContext +import io.github.zyrouge.symphony.utils.SimpleFileSystem +import io.github.zyrouge.symphony.utils.StringListUtils import java.util.Stack @Composable @@ -55,8 +54,8 @@ fun FoldersView(context: ViewContext) { val explorer = context.symphony.groove.song.explorer val folders = remember(id) { - val entities = mutableMapOf() - val stack = Stack() + val entities = mutableMapOf() + val stack = Stack() stack.add(explorer) while (stack.isNotEmpty()) { val current = stack.pop() @@ -64,20 +63,20 @@ fun FoldersView(context: ViewContext) { var hasSongs = false current.children.values.forEach { when (it) { - is GrooveExplorer.Folder -> stack.push(it) - is GrooveExplorer.File -> { + is SimpleFileSystem.Folder -> stack.push(it) + is SimpleFileSystem.File -> { hasSongs = true } } } if (hasSongs) { - entities[current.fullPath] = current + entities[current.fullPath.pathString] = current } } entities.toMap() } var currentFolder by remember(id) { - mutableStateOf(null) + mutableStateOf(null) } BackHandler(currentFolder != null) { @@ -101,7 +100,7 @@ fun FoldersView(context: ViewContext) { derivedStateOf { folder.children.values.mapNotNull { when (it) { - is GrooveExplorer.File -> it.data as String + is SimpleFileSystem.File -> it.data as String else -> null } } @@ -125,10 +124,7 @@ fun FoldersView(context: ViewContext) { ), ) } - Text( - folder.basename, - style = MaterialTheme.typography.bodyLarge, - ) + Text(folder.name, style = MaterialTheme.typography.bodyLarge) } HorizontalDivider() SongList(context, songIds = songIds, songsCount = songIds.size) @@ -149,14 +145,14 @@ fun FoldersView(context: ViewContext) { @Composable private fun FoldersGrid( context: ViewContext, - folders: Map, - onClick: (GrooveExplorer.Folder) -> Unit, + folders: Map, + onClick: (SimpleFileSystem.Folder) -> Unit, ) { val sortBy by context.symphony.settings.lastUsedFoldersSortBy.collectAsState() val sortReverse by context.symphony.settings.lastUsedFoldersSortReverse.collectAsState() val sortedFolderNames by remember(folders, sortBy, sortReverse) { derivedStateOf { - GrooveExplorer.sort(folders.keys.toList(), sortBy, sortReverse) + StringListUtils.sort(folders.keys.toList(), sortBy, sortReverse) } } @@ -169,8 +165,8 @@ private fun FoldersGrid( context.symphony.settings.setLastUsedFoldersSortReverse(it) }, sort = sortBy, - sorts = PathSortBy.entries - .associateWith { x -> wrapInViewContext { x.label(context) } }, + sorts = StringListUtils.SortBy.entries + .associateWith { x -> ViewContext.parameterizedFn { x.label(context) } }, onSortChange = { context.symphony.settings.setLastUsedFoldersSortBy(it) }, @@ -214,7 +210,7 @@ private fun FoldersGrid( @Composable private fun FolderTile( context: ViewContext, - folder: GrooveExplorer.Folder, + folder: SimpleFileSystem.Folder, onClick: () -> Unit, ) { SquareGrooveTile( @@ -282,7 +278,7 @@ private fun FolderTile( }, content = { Text( - folder.basename, + folder.name, style = MaterialTheme.typography.bodyMedium, textAlign = TextAlign.Center, maxLines = 2, @@ -297,18 +293,19 @@ private fun FolderTile( ) } -private fun GrooveExplorer.Folder.createArtworkImageRequest(context: ViewContext) = children.values - .find { it is GrooveExplorer.File } - ?.let { - val songId = (it as GrooveExplorer.File).data as String - context.symphony.groove.song.createArtworkImageRequest(songId) - } - ?: Assets.createPlaceholderImageRequest(context.symphony) +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 GrooveExplorer.Folder.getSortedSongIds(context: ViewContext): List { +private fun SimpleFileSystem.Folder.getSortedSongIds(context: ViewContext): List { val songIds = children.values.mapNotNull { when (it) { - is GrooveExplorer.File -> it.data as String + is SimpleFileSystem.File -> it.data as String else -> null } } 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 664f11d3..e268d6ab 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 @@ -48,7 +48,7 @@ import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import coil.compose.AsyncImage -import io.github.zyrouge.symphony.services.groove.SongSortBy +import io.github.zyrouge.symphony.services.groove.SongRepository import io.github.zyrouge.symphony.services.radio.Radio import io.github.zyrouge.symphony.ui.components.IconTextBody import io.github.zyrouge.symphony.ui.helpers.Routes @@ -92,7 +92,7 @@ fun ForYouView(context: ViewContext) { runIfOrDefault(!songsIsUpdating, listOf()) { context.symphony.groove.song.sort( songIds.toList(), - SongSortBy.DATE_MODIFIED, + SongRepository.SortBy.DATE_MODIFIED, true ) } 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 c9817770..cfed81cf 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 @@ -32,6 +32,7 @@ import io.github.zyrouge.symphony.ui.components.LoaderScaffold import io.github.zyrouge.symphony.ui.components.NewPlaylistDialog import io.github.zyrouge.symphony.ui.components.PlaylistGrid import io.github.zyrouge.symphony.ui.helpers.ViewContext +import io.github.zyrouge.symphony.utils.ActivityUtils import io.github.zyrouge.symphony.utils.Logger import kotlinx.coroutines.launch @@ -44,14 +45,17 @@ fun PlaylistsView(context: ViewContext) { var showPlaylistCreator by remember { mutableStateOf(false) } val openPlaylistLauncher = rememberLauncherForActivityResult( - ActivityResultContracts.OpenDocument() - ) { uri -> - uri?.let { _ -> + ActivityResultContracts.OpenMultipleDocuments() + ) { uris -> + uris.forEach { x -> try { - val playlist = Playlist.parse(context.symphony, uri) - context.symphony.groove.playlist.add(playlist) + ActivityUtils.makePersistableReadableUri(context.symphony.applicationContext, x) + val playlist = Playlist.parse(context.symphony, x) + coroutineScope.launch { + context.symphony.groove.playlist.add(playlist) + } } catch (err: Exception) { - Logger.error("PlaylistTile", "import failed (activity result)", err) + Logger.error("PlaylistView", "import failed (activity result)", err) Toast.makeText( context.symphony.applicationContext, context.symphony.t.InvalidM3UFile, 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 8032069c..e583282f 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 @@ -59,7 +59,7 @@ import io.github.zyrouge.symphony.ui.helpers.ViewContext import io.github.zyrouge.symphony.ui.helpers.navigateTo import io.github.zyrouge.symphony.ui.view.NowPlayingControlsLayout import io.github.zyrouge.symphony.ui.view.NowPlayingData -import io.github.zyrouge.symphony.utils.DurationFormatter +import io.github.zyrouge.symphony.utils.DurationUtils @OptIn(ExperimentalLayoutApi::class) @Composable @@ -421,7 +421,7 @@ private fun NowPlayingPlaybackPositionText( alignment: Alignment, ) { val textStyle = MaterialTheme.typography.labelMedium - val durationFormatted = DurationFormatter.formatMs(duration) + val durationFormatted = DurationUtils.formatMs(duration) Box(contentAlignment = alignment) { Text( diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/view/nowPlaying/SleepTimerDialog.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/view/nowPlaying/SleepTimerDialog.kt index 483c87cc..7cd92ff1 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/view/nowPlaying/SleepTimerDialog.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/view/nowPlaying/SleepTimerDialog.kt @@ -39,7 +39,7 @@ import androidx.compose.ui.unit.dp import io.github.zyrouge.symphony.services.radio.RadioSleepTimer import io.github.zyrouge.symphony.ui.components.ScaffoldDialog import io.github.zyrouge.symphony.ui.helpers.ViewContext -import io.github.zyrouge.symphony.utils.DurationFormatter +import io.github.zyrouge.symphony.utils.DurationUtils import java.time.Duration import java.util.Timer import kotlin.concurrent.timer @@ -75,7 +75,7 @@ fun NowPlayingSleepTimerDialog( }, content = { Text( - DurationFormatter.formatMs(endsIn), + DurationUtils.formatMs(endsIn), textAlign = TextAlign.Center, style = MaterialTheme.typography.headlineMedium, modifier = Modifier @@ -167,7 +167,7 @@ fun NowPlayingSleepTimerSetDialog( val shape = RoundedCornerShape(4.dp) Text( - DurationFormatter.formatMinSec(0L, 0L, hours, minutes), + DurationUtils.formatMinSec(0L, 0L, hours, minutes), style = MaterialTheme.typography.labelMedium, modifier = Modifier .background( diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/view/settings/LinkTile.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/view/settings/LinkTile.kt index 1d5fdd17..e9593eb8 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/view/settings/LinkTile.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/view/settings/LinkTile.kt @@ -7,7 +7,7 @@ import androidx.compose.material3.ListItem import androidx.compose.material3.Text import androidx.compose.runtime.Composable import io.github.zyrouge.symphony.ui.helpers.ViewContext -import io.github.zyrouge.symphony.utils.startBrowserActivity +import io.github.zyrouge.symphony.utils.ActivityUtils @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -20,7 +20,7 @@ fun SettingsLinkTile( Card( colors = SettingsTileDefaults.cardColors(), onClick = { - startBrowserActivity(context.activity, Uri.parse(url)) + ActivityUtils.startBrowserActivity(context.activity, Uri.parse(url)) } ) { ListItem( diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/view/settings/MultiGrooveFolderTile.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/view/settings/MultiGrooveFolderTile.kt index 52adff56..f7979555 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/view/settings/MultiGrooveFolderTile.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/view/settings/MultiGrooveFolderTile.kt @@ -37,13 +37,13 @@ import androidx.compose.ui.draw.clip 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.GrooveExplorer import io.github.zyrouge.symphony.ui.components.ScaffoldDialog import io.github.zyrouge.symphony.ui.components.ScaffoldDialogDefaults import io.github.zyrouge.symphony.ui.components.SubtleCaptionText import io.github.zyrouge.symphony.ui.components.drawScrollBar import io.github.zyrouge.symphony.ui.helpers.ViewContext import io.github.zyrouge.symphony.ui.helpers.navigateToFolder +import io.github.zyrouge.symphony.utils.SimpleFileSystem private const val SettingsFolderContentType = "folder" @@ -52,7 +52,7 @@ fun SettingsMultiGrooveFolderTile( context: ViewContext, icon: @Composable () -> Unit, title: @Composable () -> Unit, - explorer: GrooveExplorer.Folder, + explorer: SimpleFileSystem.Folder, initialValues: Set, onChange: (Set) -> Unit, ) { @@ -170,7 +170,7 @@ fun SettingsMultiGrooveFolderTile( @Composable private fun SettingsFolderTilePickerDialog( context: ViewContext, - explorer: GrooveExplorer.Folder, + explorer: SimpleFileSystem.Folder, onSelect: (List?) -> Unit, ) { var currentFolder by remember { mutableStateOf(explorer) } @@ -178,14 +178,14 @@ private fun SettingsFolderTilePickerDialog( derivedStateOf { currentFolder.children.values.mapNotNull { entity -> when (entity) { - is GrooveExplorer.Folder -> entity + is SimpleFileSystem.Folder -> entity else -> null } } } } val currentPath by remember(currentFolder) { - derivedStateOf { currentFolder.pathParts } + derivedStateOf { currentFolder.fullPath.parts } } val currentPathScrollState = rememberScrollState() @@ -255,7 +255,7 @@ private fun SettingsFolderTilePickerDialog( ) { items( sortedEntities, - key = { it.basename }, + key = { it.name }, contentType = { SettingsFolderContentType } ) { folder -> Card( @@ -276,10 +276,10 @@ private fun SettingsFolderTilePickerDialog( ) Spacer(modifier = Modifier.width(20.dp)) Column { - Text(folder.basename) + Text(folder.name) Text( context.symphony.t.XFolders( - folder.countChildrenFolders().toString() + folder.childFoldersCount.toString() ), style = MaterialTheme.typography.labelSmall, ) @@ -301,6 +301,3 @@ private fun SettingsFolderTilePickerDialog( }, ) } - -private fun GrooveExplorer.Folder.countChildrenFolders() = children.values - .count { it is GrooveExplorer.Folder } diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/view/settings/MultiSystemFolderTile.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/view/settings/MultiSystemFolderTile.kt index 34936c7e..e4a81d1b 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/view/settings/MultiSystemFolderTile.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/view/settings/MultiSystemFolderTile.kt @@ -40,13 +40,14 @@ import androidx.compose.ui.draw.clip 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.GrooveExplorer import io.github.zyrouge.symphony.ui.components.ScaffoldDialog import io.github.zyrouge.symphony.ui.components.ScaffoldDialogDefaults import io.github.zyrouge.symphony.ui.components.SubtleCaptionText import io.github.zyrouge.symphony.ui.components.drawScrollBar import io.github.zyrouge.symphony.ui.helpers.ViewContext import io.github.zyrouge.symphony.ui.helpers.navigateToFolder +import io.github.zyrouge.symphony.utils.ActivityUtils +import io.github.zyrouge.symphony.utils.SimpleFileSystem private const val SettingsFolderContentType = "folder" @@ -81,7 +82,10 @@ fun SettingsMultiSystemFolderTile( val pickFolderLauncher = rememberLauncherForActivityResult( ActivityResultContracts.OpenDocumentTree() ) { uri -> - uri?.let { _ -> values.add(uri) } + uri?.let { _ -> + ActivityUtils.makePersistableReadableUri(context.symphony.applicationContext, uri) + values.add(uri) + } } // TODO: workaround for dialog resize bug @@ -160,7 +164,7 @@ fun SettingsMultiSystemFolderTile( @Composable private fun SettingsFolderTilePickerDialog( context: ViewContext, - explorer: GrooveExplorer.Folder, + explorer: SimpleFileSystem.Folder, onSelect: (List?) -> Unit, ) { var currentFolder by remember { mutableStateOf(explorer) } @@ -168,14 +172,14 @@ private fun SettingsFolderTilePickerDialog( derivedStateOf { currentFolder.children.values.mapNotNull { entity -> when (entity) { - is GrooveExplorer.Folder -> entity + is SimpleFileSystem.Folder -> entity else -> null } } } } val currentPath by remember(currentFolder) { - derivedStateOf { currentFolder.pathParts } + derivedStateOf { currentFolder.fullPath.parts } } val currentPathScrollState = rememberScrollState() @@ -245,7 +249,7 @@ private fun SettingsFolderTilePickerDialog( ) { items( sortedEntities, - key = { it.basename }, + key = { it.name }, contentType = { SettingsFolderContentType } ) { folder -> Card( @@ -266,10 +270,10 @@ private fun SettingsFolderTilePickerDialog( ) Spacer(modifier = Modifier.width(20.dp)) Column { - Text(folder.basename) + Text(folder.name) Text( context.symphony.t.XFolders( - folder.countChildrenFolders().toString() + folder.childFoldersCount.toString() ), style = MaterialTheme.typography.labelSmall, ) @@ -291,6 +295,3 @@ private fun SettingsFolderTilePickerDialog( }, ) } - -private fun GrooveExplorer.Folder.countChildrenFolders() = children.values - .count { it is GrooveExplorer.Folder } diff --git a/app/src/main/java/io/github/zyrouge/symphony/utils/Activity.kt b/app/src/main/java/io/github/zyrouge/symphony/utils/Activity.kt deleted file mode 100644 index 1bb59c93..00000000 --- a/app/src/main/java/io/github/zyrouge/symphony/utils/Activity.kt +++ /dev/null @@ -1,10 +0,0 @@ -package io.github.zyrouge.symphony.utils - -import android.content.Context -import android.content.Intent -import android.net.Uri - -fun startBrowserActivity(activity: Context, uri: Uri) { - activity.startActivity(Intent(Intent.ACTION_VIEW).setData(uri)) -} - diff --git a/app/src/main/java/io/github/zyrouge/symphony/utils/ActivityUtils.kt b/app/src/main/java/io/github/zyrouge/symphony/utils/ActivityUtils.kt new file mode 100644 index 00000000..da53940e --- /dev/null +++ b/app/src/main/java/io/github/zyrouge/symphony/utils/ActivityUtils.kt @@ -0,0 +1,29 @@ +package io.github.zyrouge.symphony.utils + +import android.content.ClipData +import android.content.ClipboardManager +import android.content.Context +import android.content.Intent +import android.net.Uri +import android.widget.Toast +import io.github.zyrouge.symphony.Symphony + +object ActivityUtils { + fun startBrowserActivity(activity: Context, uri: Uri) { + activity.startActivity(Intent(Intent.ACTION_VIEW).setData(uri)) + } + + fun copyToClipboardAndNotify(symphony: Symphony, text: String) { + val context = symphony.applicationContext + val clipboardManager = context.getSystemService(ClipboardManager::class.java) + clipboardManager.setPrimaryClip(ClipData.newPlainText(null, text)) + Toast.makeText(context, symphony.t.CopiedXToClipboard(text), Toast.LENGTH_SHORT).show() + } + + fun makePersistableReadableUri(context: Context, uri: Uri) { + context.contentResolver.takePersistableUriPermission( + uri, + Intent.FLAG_GRANT_READ_URI_PERMISSION + ) + } +} diff --git a/app/src/main/java/io/github/zyrouge/symphony/utils/Compose.kt b/app/src/main/java/io/github/zyrouge/symphony/utils/Compose.kt deleted file mode 100644 index 4c91a2d4..00000000 --- a/app/src/main/java/io/github/zyrouge/symphony/utils/Compose.kt +++ /dev/null @@ -1,18 +0,0 @@ -package io.github.zyrouge.symphony.utils - -import android.content.ClipData -import android.content.ClipboardManager -import android.widget.Toast -import io.github.zyrouge.symphony.ui.helpers.ViewContext - -fun wrapInViewContext(fn: (ViewContext) -> T) = fn - -fun copyToClipboardWithToast(context: ViewContext, text: String) { - val clipboardManager = context.activity.getSystemService(ClipboardManager::class.java) - clipboardManager.setPrimaryClip(ClipData.newPlainText(null, text)) - Toast.makeText( - context.activity, - context.symphony.t.CopiedXToClipboard(text), - Toast.LENGTH_SHORT, - ).show() -} diff --git a/app/src/main/java/io/github/zyrouge/symphony/utils/DocumentFileX.kt b/app/src/main/java/io/github/zyrouge/symphony/utils/DocumentFileX.kt index b0c4e331..7d84a31c 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/utils/DocumentFileX.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/utils/DocumentFileX.kt @@ -18,19 +18,21 @@ data class DocumentFileX( val isDirectory: Boolean get() = mimeType.contentEquals(DocumentsContract.Document.MIME_TYPE_DIR) - fun list(onChild: (file: DocumentFileX) -> Unit) { + fun list(): List { val childrenUri = DocumentsContract.buildChildDocumentsUriUsingTree( uri, DocumentsContract.getDocumentId(uri) ) + val items = mutableListOf() context.contentResolver.query(childrenUri, selectionColumns, null, null, null)?.use { while (it.moveToNext()) { val file = fromCursor(context, it) { id -> DocumentsContract.buildDocumentUriUsingTree(uri, id) } - onChild(file) + items.add(file) } } + return items } companion object { @@ -75,5 +77,9 @@ data class DocumentFileX( DocumentsContract.getTreeDocumentId(treeUri) ) ) + + fun getParentPathOfTreeUri(treeUri: Uri) = runCatching { + DocumentsContract.getTreeDocumentId(treeUri) + }.getOrNull()?.let { SimplePath(it) } } } \ No newline at end of file diff --git a/app/src/main/java/io/github/zyrouge/symphony/utils/Duration.kt b/app/src/main/java/io/github/zyrouge/symphony/utils/DurationUtils.kt similarity index 96% rename from app/src/main/java/io/github/zyrouge/symphony/utils/Duration.kt rename to app/src/main/java/io/github/zyrouge/symphony/utils/DurationUtils.kt index 68146380..c237051d 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/utils/Duration.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/utils/DurationUtils.kt @@ -2,7 +2,7 @@ package io.github.zyrouge.symphony.utils import java.util.concurrent.TimeUnit -object DurationFormatter { +object DurationUtils { fun formatMs(ms: Long) = formatMinSec( TimeUnit.MILLISECONDS.toDays(ms).floorDiv(TimeUnit.DAYS.toDays(1)), TimeUnit.MILLISECONDS.toHours(ms) % TimeUnit.DAYS.toHours(1), diff --git a/app/src/main/java/io/github/zyrouge/symphony/utils/Eventer.kt b/app/src/main/java/io/github/zyrouge/symphony/utils/Eventer.kt index 2f2f93a3..ba5ae583 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/utils/Eventer.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/utils/Eventer.kt @@ -18,10 +18,4 @@ class Eventer { fun dispatch(event: T) { subscribers.forEach { it(event) } } - - companion object { - fun nothing() = Eventer() - } } - -fun Eventer.dispatch() = dispatch(null) diff --git a/app/src/main/java/io/github/zyrouge/symphony/utils/FuzzySearch.kt b/app/src/main/java/io/github/zyrouge/symphony/utils/Fuzzy.kt similarity index 100% rename from app/src/main/java/io/github/zyrouge/symphony/utils/FuzzySearch.kt rename to app/src/main/java/io/github/zyrouge/symphony/utils/Fuzzy.kt diff --git a/app/src/main/java/io/github/zyrouge/symphony/utils/Range.kt b/app/src/main/java/io/github/zyrouge/symphony/utils/RangeUtils.kt similarity index 100% rename from app/src/main/java/io/github/zyrouge/symphony/utils/Range.kt rename to app/src/main/java/io/github/zyrouge/symphony/utils/RangeUtils.kt diff --git a/app/src/main/java/io/github/zyrouge/symphony/utils/RoomConvertors.kt b/app/src/main/java/io/github/zyrouge/symphony/utils/RoomConvertors.kt new file mode 100644 index 00000000..8a915f76 --- /dev/null +++ b/app/src/main/java/io/github/zyrouge/symphony/utils/RoomConvertors.kt @@ -0,0 +1,27 @@ +package io.github.zyrouge.symphony.utils + +import android.net.Uri +import androidx.room.TypeConverter +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json + +@Suppress("unused") +class RoomConvertors { + @TypeConverter + fun serializeUri(value: String) = Uri.parse(value) + + @TypeConverter + fun deserializeUri(value: Uri) = value.toString() + + @TypeConverter + fun serializeStringSet(value: String) = Json.decodeFromString>(value) + + @TypeConverter + fun deserializeStringSet(value: Set) = Json.encodeToString(value) + + @TypeConverter + fun serializeStringList(value: String) = Json.decodeFromString>(value) + + @TypeConverter + fun deserializeStringList(value: List) = Json.encodeToString(value) +} diff --git a/app/src/main/java/io/github/zyrouge/symphony/utils/RunX.kt b/app/src/main/java/io/github/zyrouge/symphony/utils/Run.kt similarity index 100% rename from app/src/main/java/io/github/zyrouge/symphony/utils/RunX.kt rename to app/src/main/java/io/github/zyrouge/symphony/utils/Run.kt diff --git a/app/src/main/java/io/github/zyrouge/symphony/utils/Serializable.kt b/app/src/main/java/io/github/zyrouge/symphony/utils/Serializable.kt deleted file mode 100644 index ad1ac820..00000000 --- a/app/src/main/java/io/github/zyrouge/symphony/utils/Serializable.kt +++ /dev/null @@ -1,19 +0,0 @@ -package io.github.zyrouge.symphony.utils - -import android.net.Uri -import kotlinx.serialization.KSerializer -import kotlinx.serialization.descriptors.PrimitiveKind -import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor -import kotlinx.serialization.encoding.Decoder -import kotlinx.serialization.encoding.Encoder -import kotlinx.serialization.json.Json - -val RelaxedJsonDecoder = Json { - ignoreUnknownKeys = true -} - -object UriSerializer : KSerializer { - override val descriptor = PrimitiveSerialDescriptor("Uri", PrimitiveKind.STRING) - override fun serialize(encoder: Encoder, value: Uri) = encoder.encodeString(value.toString()) - override fun deserialize(decoder: Decoder): Uri = Uri.parse(decoder.decodeString()) -} diff --git a/app/src/main/java/io/github/zyrouge/symphony/utils/SimpleFileSystem.kt b/app/src/main/java/io/github/zyrouge/symphony/utils/SimpleFileSystem.kt new file mode 100644 index 00000000..581665fe --- /dev/null +++ b/app/src/main/java/io/github/zyrouge/symphony/utils/SimpleFileSystem.kt @@ -0,0 +1,55 @@ +package io.github.zyrouge.symphony.utils + +import java.util.concurrent.ConcurrentHashMap + +sealed class SimpleFileSystem(val parent: Folder?, val name: String) { + val fullPath + get(): SimplePath { + val parts = mutableListOf(name) + var currentParent = parent + while (currentParent != null) { + parts.add(0, currentParent.name) + currentParent = currentParent.parent + } + return SimplePath(parts) + } + + class File( + parent: Folder? = null, + name: String, + var data: Any? = null, + ) : SimpleFileSystem(parent, name) + + class Folder( + parent: Folder? = null, + name: String = "root", + var children: ConcurrentHashMap = ConcurrentHashMap(), + ) : SimpleFileSystem(parent, name) { + val isEmpty get() = children.isEmpty() + val childFoldersCount get() = children.values.count { it is Folder } + + fun addChildFile(name: String): File { + if (children.containsKey(name)) { + throw Exception("Child '$name' already exists") + } + val child = File(this, name) + children[name] = child + return child + } + + fun addChildFile(path: SimplePath): File { + val parts = path.parts.toMutableList() + var parent = this + while (parts.size > 1) { + val x = parts.removeAt(0) + val found = parent.children[x] + parent = when (found) { + is Folder -> found + null -> Folder(parent, x) + else -> throw Exception("Child '$x' is not a folder") + } + } + return parent.addChildFile(parts[0]) + } + } +} diff --git a/app/src/main/java/io/github/zyrouge/symphony/utils/SimplePath.kt b/app/src/main/java/io/github/zyrouge/symphony/utils/SimplePath.kt new file mode 100644 index 00000000..4ecda6c5 --- /dev/null +++ b/app/src/main/java/io/github/zyrouge/symphony/utils/SimplePath.kt @@ -0,0 +1,35 @@ +package io.github.zyrouge.symphony.utils + +class SimplePath(val parts: List) { + constructor(path: String) : this(n(p(path))) + constructor(path: String, vararg subParts: String) : this(n(p(path) + subParts)) + constructor(path: SimplePath, vararg subParts: String) : this(n(path.parts + subParts)) + + val name get() = parts.last() + val nameWithoutExtension get() = name.substringBeforeLast(".") + val extension get() = name.substringAfterLast(".", "") + val parent get() = if (parts.size > 1) SimplePath(parts.subList(0, parts.size)) else null + val size get() = parts.size + val pathString get() = parts.joinToString("/") + + fun join(vararg nParts: String) = SimplePath(this, *nParts) + + override fun toString() = pathString + + companion object { + private fun p(path: String) = path.split("/", "\\") + + private fun n(parts: List): List { + val normalized = mutableListOf() + for (x in parts) { + when { + x.isEmpty() -> {} + x == "." -> {} + x == ".." -> normalized.removeAt(parts.lastIndex) + else -> normalized.add(x) + } + } + return parts + } + } +} diff --git a/app/src/main/java/io/github/zyrouge/symphony/utils/StringListUtils.kt b/app/src/main/java/io/github/zyrouge/symphony/utils/StringListUtils.kt new file mode 100644 index 00000000..121f1789 --- /dev/null +++ b/app/src/main/java/io/github/zyrouge/symphony/utils/StringListUtils.kt @@ -0,0 +1,16 @@ +package io.github.zyrouge.symphony.utils + +object StringListUtils { + enum class SortBy { + CUSTOM, + NAME, + } + + fun sort(values: List, by: SortBy, reverse: Boolean): List { + val sorted = when (by) { + SortBy.CUSTOM -> values + SortBy.NAME -> values.sorted() + } + return if (reverse) sorted.reversed() else sorted + } +} diff --git a/build.gradle.kts b/build.gradle.kts index 0b2c9a5f..162b10e0 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -3,4 +3,6 @@ plugins { alias(libs.plugins.android.kotlin) apply false alias(libs.plugins.compose.compiler) apply false alias(libs.plugins.kotlin.serialization) apply false + alias(libs.plugins.ksp) apply false + alias(libs.plugins.room) apply false } \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 1215c398..0884f2d8 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -13,10 +13,12 @@ kotlinx-serialization-json = "1.7.3" lifecycle-runtime = "2.8.6" media = "1.7.0" okhttp3 = "4.12.0" +room = "2.6.1" android-gradle-plugin = "8.7.1" kotlin-gradle-plugin = "2.0.21" kotlin-serialization-plugin = "2.0.21" +ksp-plugin = "2.0.21-1.0.26" compile-sdk = "34" min-sdk = "28" @@ -40,9 +42,14 @@ kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx- lifecycle-runtime = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycle-runtime" } media = { group = "androidx.media", name = "media", version.ref = "media" } okhttp3 = { group = "com.squareup.okhttp3", name = "okhttp", version.ref = "okhttp3" } +room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "room" } +room-ktx = { group = "androidx.room", name = "room-ktx", version.ref = "room" } +room-runtime = { group = "androidx.room", name = "room-runtime", version.ref = "room" } [plugins] android-app = { id = "com.android.application", version.ref = "android-gradle-plugin" } android-kotlin = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin-gradle-plugin" } compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin-gradle-plugin" } kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin-serialization-plugin" } +ksp = { id = "com.google.devtools.ksp", version.ref = "ksp-plugin" } +room = { id = "androidx.room", version.ref = "room" }