diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 9a63af9a4..98d6bce68 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -32,7 +32,7 @@ jobs: cache: 'gradle' - name: Build debug APK and run jvm tests - run: ./gradlew assembleDebug lintDebug testDebugUnitTest --stacktrace -DskipFormatKtlint + run: ./gradlew assembleDebug lintFullDebug testFullDebugUnitTest --stacktrace -DskipFormatKtlint env: MUSIC_DEBUG_KEYSTORE_FILE: 'music-debug.jks' MUSIC_DEBUG_SIGNING_STORE_PASSWORD: ${{ secrets.SIGNING_KEY_PASSWORD }} @@ -42,4 +42,4 @@ jobs: uses: actions/upload-artifact@v3 with: name: app - path: app/build/outputs/apk/debug/*.apk + path: app/build/outputs/apk/full/debug/*.apk \ No newline at end of file diff --git a/.github/workflows/build_pr.yml b/.github/workflows/build_pr.yml index 6fafe23f7..86e12b112 100644 --- a/.github/workflows/build_pr.yml +++ b/.github/workflows/build_pr.yml @@ -18,7 +18,7 @@ jobs: cache: 'gradle' - name: Build debug APK and run jvm tests - run: ./gradlew assembleDebug lintDebug testDebugUnitTest --stacktrace -DskipFormatKtlint + run: ./gradlew assembleDebug lintFullDebug testFullDebugUnitTest --stacktrace -DskipFormatKtlint env: PULL_REQUEST: 'true' @@ -26,4 +26,4 @@ jobs: uses: actions/upload-artifact@v3 with: name: app - path: app/build/outputs/apk/debug/*.apk + path: app/build/outputs/apk/full/debug/*.apk diff --git a/README.md b/README.md index 442d9899c..bce7be45c 100644 --- a/README.md +++ b/README.md @@ -4,13 +4,16 @@ A Material 3 YouTube Music client for Android -[<img src="https://fdroid.gitlab.io/artwork/badge/get-it-on.png" alt="Get it on F-Droid" height="80">](https://f-droid.org/packages/com.zionhuang.music) -[<img src="https://gitlab.com/IzzyOnDroid/repo/-/raw/master/assets/IzzyOnDroid.png" height="80">](https://apt.izzysoft.de/fdroid/index/apk/com.zionhuang.music) - [](https://github.com/z-huang/music/releases) [](https://www.gnu.org/licenses/gpl-3.0) [](https://github.com/z-huang/InnerTune/releases) +[<img src="https://github.com/machiav3lli/oandbackupx/blob/034b226cea5c1b30eb4f6a6f313e4dadcbb0ece4/badge_github.png" alt="Get it on GitHub" height="80">](https://github.com/z-huang/InnerTune/releases/latest) +[<img src="https://fdroid.gitlab.io/artwork/badge/get-it-on.png" alt="Get it on F-Droid" height="80">](https://f-droid.org/packages/com.zionhuang.music) +[<img src="https://gitlab.com/IzzyOnDroid/repo/-/raw/master/assets/IzzyOnDroid.png" height="80">](https://apt.izzysoft.de/fdroid/index/apk/com.zionhuang.music) + +[Compare versions](https://github.com/z-huang/InnerTune/wiki/App-Versions) + ## Features - Play songs from YT/YT Music without ads @@ -20,6 +23,7 @@ A Material 3 YouTube Music client for Android - Library management - Cache and download songs for offline playback - Synchronized lyrics +- Lyrics translator (experimental) - Skip silence - Audio normalization - Adjust tempo/pitch diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 5b368d687..02eb490e1 100755 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -1,16 +1,19 @@ @file:Suppress("UnstableApiUsage") +val isFullBuild: Boolean by rootProject.extra + plugins { id("com.android.application") kotlin("android") kotlin("kapt") id("com.google.dagger.hilt.android") id("com.google.devtools.ksp") - if (System.getenv("PULL_REQUEST") == null) { - id("com.google.gms.google-services") - id("com.google.firebase.crashlytics") - id("com.google.firebase.firebase-perf") - } +} + +if (isFullBuild && System.getenv("PULL_REQUEST") == null) { + apply(plugin = "com.google.gms.google-services") + apply(plugin = "com.google.firebase.crashlytics") + apply(plugin = "com.google.firebase.firebase-perf") } android { @@ -21,8 +24,8 @@ android { applicationId = "com.zionhuang.music" minSdk = 24 targetSdk = 33 - versionCode = 17 - versionName = "0.5.1" + versionCode = 18 + versionName = "0.5.2" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } buildTypes { @@ -37,6 +40,15 @@ android { resValue("string", "app_name", "InnerTune Debug") } } + flavorDimensions += "version" + productFlavors { + create("full") { + dimension = "version" + } + create("foss") { + dimension = "version" + } + } signingConfigs { getByName("debug") { if (System.getenv("MUSIC_DEBUG_SIGNING_STORE_PASSWORD") != null) { @@ -65,11 +77,13 @@ android { freeCompilerArgs = freeCompilerArgs + "-Xcontext-receivers" jvmTarget = "11" } - testOptions { unitTests.isIncludeAndroidResources = true unitTests.isReturnDefaultValues = true } + lint { + disable += "MissingTranslation" + } } ksp { @@ -126,11 +140,14 @@ dependencies { coreLibraryDesugaring(libs.desugaring) - implementation(platform(libs.firebase.bom)) - implementation(libs.firebase.analytics) - implementation(libs.firebase.crashlytics) - implementation(libs.firebase.config) - implementation(libs.firebase.perf) + "fullImplementation"(platform(libs.firebase.bom)) + "fullImplementation"(libs.firebase.analytics) + "fullImplementation"(libs.firebase.crashlytics) + "fullImplementation"(libs.firebase.config) + "fullImplementation"(libs.firebase.perf) + "fullImplementation"(libs.mlkit.language.id) + "fullImplementation"(libs.mlkit.translate) + "fullImplementation"(libs.opencc4j) implementation(libs.timber) } diff --git a/app/schemas/com.zionhuang.music.db.InternalDatabase/12.json b/app/schemas/com.zionhuang.music.db.InternalDatabase/12.json new file mode 100644 index 000000000..2570d1e76 --- /dev/null +++ b/app/schemas/com.zionhuang.music.db.InternalDatabase/12.json @@ -0,0 +1,812 @@ +{ + "formatVersion": 1, + "database": { + "version": 12, + "identityHash": "8db3d5731dbcc716a90427d4dde63c66", + "entities": [ + { + "tableName": "song", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `title` TEXT NOT NULL, `duration` INTEGER NOT NULL, `thumbnailUrl` TEXT, `albumId` TEXT, `albumName` TEXT, `liked` INTEGER NOT NULL, `totalPlayTime` INTEGER NOT NULL, `inLibrary` INTEGER, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "duration", + "columnName": "duration", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "thumbnailUrl", + "columnName": "thumbnailUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "albumId", + "columnName": "albumId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "albumName", + "columnName": "albumName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "liked", + "columnName": "liked", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "totalPlayTime", + "columnName": "totalPlayTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "inLibrary", + "columnName": "inLibrary", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_song_albumId", + "unique": false, + "columnNames": [ + "albumId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_song_albumId` ON `${TABLE_NAME}` (`albumId`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "artist", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `thumbnailUrl` TEXT, `lastUpdateTime` INTEGER NOT NULL, `bookmarkedAt` INTEGER, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "thumbnailUrl", + "columnName": "thumbnailUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastUpdateTime", + "columnName": "lastUpdateTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookmarkedAt", + "columnName": "bookmarkedAt", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "album", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `title` TEXT NOT NULL, `year` INTEGER, `thumbnailUrl` TEXT, `themeColor` INTEGER, `songCount` INTEGER NOT NULL, `duration` INTEGER NOT NULL, `lastUpdateTime` INTEGER NOT NULL, `bookmarkedAt` INTEGER, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "year", + "columnName": "year", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "thumbnailUrl", + "columnName": "thumbnailUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "themeColor", + "columnName": "themeColor", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "songCount", + "columnName": "songCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "duration", + "columnName": "duration", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastUpdateTime", + "columnName": "lastUpdateTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookmarkedAt", + "columnName": "bookmarkedAt", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "playlist", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `browseId` TEXT, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "browseId", + "columnName": "browseId", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "song_artist_map", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`songId` TEXT NOT NULL, `artistId` TEXT NOT NULL, `position` INTEGER NOT NULL, PRIMARY KEY(`songId`, `artistId`), FOREIGN KEY(`songId`) REFERENCES `song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`artistId`) REFERENCES `artist`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "songId", + "columnName": "songId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "artistId", + "columnName": "artistId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "songId", + "artistId" + ] + }, + "indices": [ + { + "name": "index_song_artist_map_songId", + "unique": false, + "columnNames": [ + "songId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_song_artist_map_songId` ON `${TABLE_NAME}` (`songId`)" + }, + { + "name": "index_song_artist_map_artistId", + "unique": false, + "columnNames": [ + "artistId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_song_artist_map_artistId` ON `${TABLE_NAME}` (`artistId`)" + } + ], + "foreignKeys": [ + { + "table": "song", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "songId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "artist", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "artistId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "song_album_map", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`songId` TEXT NOT NULL, `albumId` TEXT NOT NULL, `index` INTEGER NOT NULL, PRIMARY KEY(`songId`, `albumId`), FOREIGN KEY(`songId`) REFERENCES `song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`albumId`) REFERENCES `album`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "songId", + "columnName": "songId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "albumId", + "columnName": "albumId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "index", + "columnName": "index", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "songId", + "albumId" + ] + }, + "indices": [ + { + "name": "index_song_album_map_songId", + "unique": false, + "columnNames": [ + "songId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_song_album_map_songId` ON `${TABLE_NAME}` (`songId`)" + }, + { + "name": "index_song_album_map_albumId", + "unique": false, + "columnNames": [ + "albumId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_song_album_map_albumId` ON `${TABLE_NAME}` (`albumId`)" + } + ], + "foreignKeys": [ + { + "table": "song", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "songId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "album", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "albumId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "album_artist_map", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`albumId` TEXT NOT NULL, `artistId` TEXT NOT NULL, `order` INTEGER NOT NULL, PRIMARY KEY(`albumId`, `artistId`), FOREIGN KEY(`albumId`) REFERENCES `album`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`artistId`) REFERENCES `artist`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "albumId", + "columnName": "albumId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "artistId", + "columnName": "artistId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "albumId", + "artistId" + ] + }, + "indices": [ + { + "name": "index_album_artist_map_albumId", + "unique": false, + "columnNames": [ + "albumId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_album_artist_map_albumId` ON `${TABLE_NAME}` (`albumId`)" + }, + { + "name": "index_album_artist_map_artistId", + "unique": false, + "columnNames": [ + "artistId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_album_artist_map_artistId` ON `${TABLE_NAME}` (`artistId`)" + } + ], + "foreignKeys": [ + { + "table": "album", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "albumId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "artist", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "artistId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "playlist_song_map", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `playlistId` TEXT NOT NULL, `songId` TEXT NOT NULL, `position` INTEGER NOT NULL, FOREIGN KEY(`playlistId`) REFERENCES `playlist`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`songId`) REFERENCES `song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "playlistId", + "columnName": "playlistId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "songId", + "columnName": "songId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_playlist_song_map_playlistId", + "unique": false, + "columnNames": [ + "playlistId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_playlist_song_map_playlistId` ON `${TABLE_NAME}` (`playlistId`)" + }, + { + "name": "index_playlist_song_map_songId", + "unique": false, + "columnNames": [ + "songId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_playlist_song_map_songId` ON `${TABLE_NAME}` (`songId`)" + } + ], + "foreignKeys": [ + { + "table": "playlist", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "playlistId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "song", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "songId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "search_history", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `query` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "query", + "columnName": "query", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_search_history_query", + "unique": true, + "columnNames": [ + "query" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_search_history_query` ON `${TABLE_NAME}` (`query`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "format", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `itag` INTEGER NOT NULL, `mimeType` TEXT NOT NULL, `codecs` TEXT NOT NULL, `bitrate` INTEGER NOT NULL, `sampleRate` INTEGER, `contentLength` INTEGER NOT NULL, `loudnessDb` REAL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "itag", + "columnName": "itag", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "mimeType", + "columnName": "mimeType", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "codecs", + "columnName": "codecs", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bitrate", + "columnName": "bitrate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sampleRate", + "columnName": "sampleRate", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "contentLength", + "columnName": "contentLength", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "loudnessDb", + "columnName": "loudnessDb", + "affinity": "REAL", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "lyrics", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `lyrics` TEXT NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lyrics", + "columnName": "lyrics", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "event", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `songId` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, `playTime` INTEGER NOT NULL, FOREIGN KEY(`songId`) REFERENCES `song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "songId", + "columnName": "songId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "playTime", + "columnName": "playTime", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_event_songId", + "unique": false, + "columnNames": [ + "songId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_event_songId` ON `${TABLE_NAME}` (`songId`)" + } + ], + "foreignKeys": [ + { + "table": "song", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "songId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "related_song_map", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `songId` TEXT NOT NULL, `relatedSongId` TEXT NOT NULL, FOREIGN KEY(`songId`) REFERENCES `song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`relatedSongId`) REFERENCES `song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "songId", + "columnName": "songId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "relatedSongId", + "columnName": "relatedSongId", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_related_song_map_songId", + "unique": false, + "columnNames": [ + "songId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_related_song_map_songId` ON `${TABLE_NAME}` (`songId`)" + }, + { + "name": "index_related_song_map_relatedSongId", + "unique": false, + "columnNames": [ + "relatedSongId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_related_song_map_relatedSongId` ON `${TABLE_NAME}` (`relatedSongId`)" + } + ], + "foreignKeys": [ + { + "table": "song", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "songId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "song", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "relatedSongId" + ], + "referencedColumns": [ + "id" + ] + } + ] + } + ], + "views": [ + { + "viewName": "sorted_song_artist_map", + "createSql": "CREATE VIEW `${VIEW_NAME}` AS SELECT * FROM song_artist_map ORDER BY position" + }, + { + "viewName": "sorted_song_album_map", + "createSql": "CREATE VIEW `${VIEW_NAME}` AS SELECT * FROM song_album_map ORDER BY `index`" + }, + { + "viewName": "playlist_song_map_preview", + "createSql": "CREATE VIEW `${VIEW_NAME}` AS SELECT * FROM playlist_song_map WHERE position <= 3 ORDER BY position" + } + ], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '8db3d5731dbcc716a90427d4dde63c66')" + ] + } +} \ No newline at end of file diff --git a/app/src/foss/java/com/zionhuang/music/utils/TranslationHelper.kt b/app/src/foss/java/com/zionhuang/music/utils/TranslationHelper.kt new file mode 100644 index 000000000..f48e10821 --- /dev/null +++ b/app/src/foss/java/com/zionhuang/music/utils/TranslationHelper.kt @@ -0,0 +1,8 @@ +package com.zionhuang.music.utils + +import com.zionhuang.music.db.entities.LyricsEntity + +object TranslationHelper { + suspend fun translate(lyrics: LyricsEntity): LyricsEntity = lyrics + suspend fun clearModels() {} +} \ No newline at end of file diff --git a/app/src/foss/java/com/zionhuang/music/utils/Utils.kt b/app/src/foss/java/com/zionhuang/music/utils/Utils.kt new file mode 100644 index 000000000..2a471f2b6 --- /dev/null +++ b/app/src/foss/java/com/zionhuang/music/utils/Utils.kt @@ -0,0 +1,10 @@ +package com.zionhuang.music.utils + +import com.zionhuang.music.MainActivity +import java.lang.Exception + +fun MainActivity.setupRemoteConfig() {} + +fun reportException(throwable: Throwable) { + throwable.printStackTrace() +} diff --git a/app/src/full/java/com/zionhuang/music/utils/TranslationHelper.kt b/app/src/full/java/com/zionhuang/music/utils/TranslationHelper.kt new file mode 100644 index 000000000..900107ec1 --- /dev/null +++ b/app/src/full/java/com/zionhuang/music/utils/TranslationHelper.kt @@ -0,0 +1,78 @@ +package com.zionhuang.music.utils + +import android.util.LruCache +import com.github.houbb.opencc4j.util.ZhConverterUtil +import com.google.mlkit.common.model.DownloadConditions +import com.google.mlkit.common.model.RemoteModelManager +import com.google.mlkit.nl.languageid.LanguageIdentification +import com.google.mlkit.nl.translate.TranslateLanguage +import com.google.mlkit.nl.translate.TranslateRemoteModel +import com.google.mlkit.nl.translate.Translation +import com.google.mlkit.nl.translate.TranslatorOptions +import com.zionhuang.music.db.entities.LyricsEntity +import com.zionhuang.music.lyrics.LyricsUtils +import kotlinx.coroutines.tasks.await +import java.util.Locale + +object TranslationHelper { + private const val MAX_CACHE_SIZE = 20 + private val cache = LruCache<String, LyricsEntity>(MAX_CACHE_SIZE) + + suspend fun translate(lyrics: LyricsEntity): LyricsEntity { + cache[lyrics.id]?.let { return it } + val isSynced = lyrics.lyrics.startsWith("[") + val sourceLanguage = TranslateLanguage.fromLanguageTag( + LanguageIdentification.getClient().identifyLanguage( + lyrics.lyrics.lines().joinToString(separator = "\n") { it.replace("\\[\\d{2}:\\d{2}.\\d{2,3}\\] *".toRegex(), "") } + ).await() + ) + val targetLanguage = TranslateLanguage.fromLanguageTag( + Locale.getDefault().toLanguageTag().substring(0..1) + ) + return if (sourceLanguage == null || targetLanguage == null || sourceLanguage == targetLanguage) { + lyrics + } else { + val translator = Translation.getClient( + TranslatorOptions.Builder() + .setSourceLanguage(sourceLanguage) + .setTargetLanguage(targetLanguage) + .build() + ) + translator.downloadModelIfNeeded( + DownloadConditions.Builder() + .requireWifi() + .build() + ).await() + val traditionalChinese = Locale.getDefault().toLanguageTag().replace("-Hant", "") == "zh-TW" + lyrics.copy( + lyrics = if (isSynced) { + LyricsUtils.parseLyrics(lyrics.lyrics).map { + val translated = translator.translate(it.text).await() + it.copy( + text = if (traditionalChinese) ZhConverterUtil.toTraditional(translated) else translated + ) + }.joinToString(separator = "\n") { + "[%02d:%02d.%03d]${it.text}".format(it.time / 60000, (it.time / 1000) % 60, it.time % 1000) + } + } else { + lyrics.lyrics.lines() + .map { + val translated = translator.translate(it).await() + if (traditionalChinese) ZhConverterUtil.toTraditional(translated) else translated + } + .joinToString(separator = "\n") + } + ) + }.also { + cache.put(it.id, it) + } + } + + suspend fun clearModels() { + val modelManager = RemoteModelManager.getInstance() + val downloadedModels = modelManager.getDownloadedModels(TranslateRemoteModel::class.java).await() + downloadedModels.forEach { + modelManager.deleteDownloadedModel(it).await() + } + } +} \ No newline at end of file diff --git a/app/src/full/java/com/zionhuang/music/utils/Utils.kt b/app/src/full/java/com/zionhuang/music/utils/Utils.kt new file mode 100644 index 000000000..cb1e2148d --- /dev/null +++ b/app/src/full/java/com/zionhuang/music/utils/Utils.kt @@ -0,0 +1,37 @@ +package com.zionhuang.music.utils + +import com.google.firebase.crashlytics.ktx.crashlytics +import com.google.firebase.ktx.Firebase +import com.google.firebase.remoteconfig.ConfigUpdate +import com.google.firebase.remoteconfig.ConfigUpdateListener +import com.google.firebase.remoteconfig.FirebaseRemoteConfigException +import com.google.firebase.remoteconfig.ktx.remoteConfig +import com.google.firebase.remoteconfig.ktx.remoteConfigSettings +import com.zionhuang.music.MainActivity +import kotlin.time.Duration.Companion.hours + +fun MainActivity.setupRemoteConfig() { + val remoteConfig = Firebase.remoteConfig + remoteConfig.setConfigSettingsAsync(remoteConfigSettings { + minimumFetchIntervalInSeconds = 6.hours.inWholeSeconds + }) + remoteConfig.fetchAndActivate() + .addOnCompleteListener(this) { task -> + if (task.isSuccessful) { + latestVersion = remoteConfig.getLong("latest_version") + } + } + remoteConfig.addOnConfigUpdateListener(object : ConfigUpdateListener { + override fun onError(error: FirebaseRemoteConfigException) {} + override fun onUpdate(configUpdate: ConfigUpdate) { + remoteConfig.activate().addOnCompleteListener { + latestVersion = remoteConfig.getLong("latest_version") + } + } + }) +} + +fun reportException(throwable: Throwable) { + Firebase.crashlytics.recordException(throwable) + throwable.printStackTrace() +} diff --git a/app/src/main/java/com/zionhuang/music/App.kt b/app/src/main/java/com/zionhuang/music/App.kt index e8113327e..cc31c882d 100644 --- a/app/src/main/java/com/zionhuang/music/App.kt +++ b/app/src/main/java/com/zionhuang/music/App.kt @@ -15,6 +15,7 @@ import com.zionhuang.music.constants.* import com.zionhuang.music.extensions.* import com.zionhuang.music.utils.dataStore import com.zionhuang.music.utils.get +import com.zionhuang.music.utils.reportException import dagger.hilt.android.HiltAndroidApp import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.GlobalScope @@ -55,7 +56,7 @@ class App : Application(), ImageLoaderFactory { ) } catch (e: Exception) { Toast.makeText(this, "Failed to parse proxy url.", LENGTH_SHORT).show() - e.printStackTrace() + reportException(e) } } diff --git a/app/src/main/java/com/zionhuang/music/MainActivity.kt b/app/src/main/java/com/zionhuang/music/MainActivity.kt index 494a7bb83..dfd5b330c 100644 --- a/app/src/main/java/com/zionhuang/music/MainActivity.kt +++ b/app/src/main/java/com/zionhuang/music/MainActivity.kt @@ -64,19 +64,11 @@ import androidx.navigation.navArgument import coil.imageLoader import coil.request.ImageRequest import com.google.common.util.concurrent.MoreExecutors -import com.google.firebase.ktx.Firebase -import com.google.firebase.remoteconfig.ConfigUpdate -import com.google.firebase.remoteconfig.ConfigUpdateListener -import com.google.firebase.remoteconfig.FirebaseRemoteConfigException -import com.google.firebase.remoteconfig.ktx.remoteConfig -import com.google.firebase.remoteconfig.ktx.remoteConfigSettings import com.valentinilk.shimmer.LocalShimmerTheme import com.zionhuang.innertube.YouTube import com.zionhuang.innertube.models.SongItem import com.zionhuang.music.constants.* import com.zionhuang.music.db.MusicDatabase -import com.zionhuang.music.db.entities.PlaylistEntity.Companion.DOWNLOADED_PLAYLIST_ID -import com.zionhuang.music.db.entities.PlaylistEntity.Companion.LIKED_PLAYLIST_ID import com.zionhuang.music.db.entities.SearchHistory import com.zionhuang.music.extensions.* import com.zionhuang.music.playback.DownloadUtil @@ -95,7 +87,6 @@ import com.zionhuang.music.ui.screens.library.LibraryAlbumsScreen import com.zionhuang.music.ui.screens.library.LibraryArtistsScreen import com.zionhuang.music.ui.screens.library.LibraryPlaylistsScreen import com.zionhuang.music.ui.screens.library.LibrarySongsScreen -import com.zionhuang.music.ui.screens.playlist.BuiltInPlaylistScreen import com.zionhuang.music.ui.screens.playlist.LocalPlaylistScreen import com.zionhuang.music.ui.screens.playlist.OnlinePlaylistScreen import com.zionhuang.music.ui.screens.search.LocalSearchScreen @@ -110,6 +101,8 @@ import com.zionhuang.music.utils.dataStore import com.zionhuang.music.utils.get import com.zionhuang.music.utils.rememberEnumPreference import com.zionhuang.music.utils.rememberPreference +import com.zionhuang.music.utils.reportException +import com.zionhuang.music.utils.setupRemoteConfig import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.collectLatest @@ -118,7 +111,6 @@ import kotlinx.coroutines.withContext import java.net.URLDecoder import java.net.URLEncoder import javax.inject.Inject -import kotlin.time.Duration.Companion.hours @AndroidEntryPoint class MainActivity : ComponentActivity() { @@ -133,7 +125,7 @@ class MainActivity : ComponentActivity() { private val serviceConnection = object : ServiceConnection { override fun onServiceConnected(name: ComponentName?, service: IBinder?) { if (service is MusicBinder) { - playerConnection = PlayerConnection(service, database, lifecycleScope) + playerConnection = PlayerConnection(this@MainActivity, service, database, lifecycleScope) } } @@ -142,7 +134,7 @@ class MainActivity : ComponentActivity() { playerConnection = null } } - private var latestVersion by mutableStateOf(BuildConfig.VERSION_CODE.toLong()) + var latestVersion by mutableStateOf(BuildConfig.VERSION_CODE.toLong()) override fun onStart() { super.onStart() @@ -376,7 +368,7 @@ class MainActivity : ComponentActivity() { navController.navigate("album/$browseId") } }.onFailure { - it.printStackTrace() + reportException(it) } } } else { @@ -399,7 +391,7 @@ class MainActivity : ComponentActivity() { }.onSuccess { sharedSong = it.firstOrNull() }.onFailure { - it.printStackTrace() + reportException(it) } } } @@ -539,13 +531,8 @@ class MainActivity : ComponentActivity() { type = NavType.StringType } ) - ) { backStackEntry -> - val playlistId = backStackEntry.arguments?.getString("playlistId")!! - if (playlistId == LIKED_PLAYLIST_ID || playlistId == DOWNLOADED_PLAYLIST_ID) { - BuiltInPlaylistScreen(navController, scrollBehavior) - } else { - LocalPlaylistScreen(navController, scrollBehavior) - } + ) { + LocalPlaylistScreen(navController, scrollBehavior) } composable( route = "youtube_browse/{browseId}?params={params}", @@ -843,27 +830,6 @@ class MainActivity : ComponentActivity() { } } - private fun setupRemoteConfig() { - val remoteConfig = Firebase.remoteConfig - remoteConfig.setConfigSettingsAsync(remoteConfigSettings { - minimumFetchIntervalInSeconds = 12.hours.inWholeSeconds - }) - remoteConfig.fetchAndActivate() - .addOnCompleteListener(this) { task -> - if (task.isSuccessful) { - latestVersion = remoteConfig.getLong("latest_version") - } - } - remoteConfig.addOnConfigUpdateListener(object : ConfigUpdateListener { - override fun onError(error: FirebaseRemoteConfigException) {} - override fun onUpdate(configUpdate: ConfigUpdate) { - remoteConfig.activate().addOnCompleteListener { - latestVersion = remoteConfig.getLong("latest_version") - } - } - }) - } - companion object { const val ACTION_SEARCH = "com.zionhuang.music.action.SEARCH" const val ACTION_SONGS = "com.zionhuang.music.action.SONGS" diff --git a/app/src/main/java/com/zionhuang/music/constants/PreferenceKeys.kt b/app/src/main/java/com/zionhuang/music/constants/PreferenceKeys.kt index 4c1053692..4157a7e90 100644 --- a/app/src/main/java/com/zionhuang/music/constants/PreferenceKeys.kt +++ b/app/src/main/java/com/zionhuang/music/constants/PreferenceKeys.kt @@ -36,8 +36,6 @@ val EnableKugouKey = booleanPreferencesKey("enableKugou") val SongSortTypeKey = stringPreferencesKey("songSortType") val SongSortDescendingKey = booleanPreferencesKey("songSortDescending") -val DownloadedSongSortTypeKey = stringPreferencesKey("songSortType") -val DownloadedSongSortDescendingKey = booleanPreferencesKey("songSortDescending") val PlaylistSongSortTypeKey = stringPreferencesKey("playlistSongSortType") val PlaylistSongSortDescendingKey = booleanPreferencesKey("playlistSongSortDescending") val ArtistSortTypeKey = stringPreferencesKey("artistSortType") @@ -49,7 +47,9 @@ val PlaylistSortDescendingKey = booleanPreferencesKey("playlistSortDescending") val ArtistSongSortTypeKey = stringPreferencesKey("artistSongSortType") val ArtistSongSortDescendingKey = booleanPreferencesKey("artistSongSortDescending") -val ArtistViewTypeKey = stringPreferencesKey("artistViewType") +val SongFilterKey = stringPreferencesKey("songFilter") +val ArtistFilterKey = stringPreferencesKey("artistFilter") +val AlbumFilterKey = stringPreferencesKey("albumFilter") val PlaylistEditLockKey = booleanPreferencesKey("playlistEditLock") @@ -57,10 +57,6 @@ enum class SongSortType { CREATE_DATE, NAME, ARTIST, PLAY_TIME } -enum class DownloadedSongSortType { - CREATE_DATE, NAME, ARTIST, PLAY_TIME -} - enum class PlaylistSongSortType { CUSTOM, CREATE_DATE, NAME, ARTIST, PLAY_TIME } @@ -81,12 +77,21 @@ enum class PlaylistSortType { CREATE_DATE, NAME, SONG_COUNT } -enum class ArtistViewType { - ALL, BOOKMARKED +enum class SongFilter { + LIBRARY, LIKED, DOWNLOADED +} + +enum class ArtistFilter { + LIBRARY, LIKED +} + +enum class AlbumFilter { + LIBRARY, LIKED } val ShowLyricsKey = booleanPreferencesKey("showLyrics") val LyricsTextPositionKey = stringPreferencesKey("lyricsTextPosition") +val TranslateLyricsKey = booleanPreferencesKey("translateLyrics") val PlayerVolumeKey = floatPreferencesKey("playerVolume") val RepeatModeKey = intPreferencesKey("repeatMode") diff --git a/app/src/main/java/com/zionhuang/music/db/DatabaseDao.kt b/app/src/main/java/com/zionhuang/music/db/DatabaseDao.kt index 7c86c4c31..642dcd5fa 100644 --- a/app/src/main/java/com/zionhuang/music/db/DatabaseDao.kt +++ b/app/src/main/java/com/zionhuang/music/db/DatabaseDao.kt @@ -186,28 +186,31 @@ interface DatabaseDao { @Transaction @Query( """ - SELECT albumId - FROM song - JOIN (SELECT songId, SUM(playTime) AS songTotalPlayTime - FROM event - WHERE timestamp > :fromTimeStamp - GROUP BY songId) AS e - ON song.id = e.songId - WHERE albumId IS NOT NULL - GROUP BY albumId - ORDER BY SUM(songTotalPlayTime) DESC - LIMIT :limit + SELECT album.* + FROM album + JOIN(SELECT albumId + FROM song + JOIN (SELECT songId, SUM(playTime) AS songTotalPlayTime + FROM event + WHERE timestamp > :fromTimeStamp + GROUP BY songId) AS e + ON song.id = e.songId + WHERE albumId IS NOT NULL + GROUP BY albumId + ORDER BY SUM(songTotalPlayTime) DESC + LIMIT :limit) + ON album.id = albumId """ ) - fun mostPlayedAlbums(fromTimeStamp: Long, limit: Int = 6): Flow<List<String>> + fun mostPlayedAlbums(fromTimeStamp: Long, limit: Int = 6): Flow<List<Album>> @Transaction @Query("SELECT * FROM song WHERE id = :songId") fun song(songId: String?): Flow<Song?> @Transaction - @Query("SELECT * FROM song WHERE id IN (:songIds)") - fun songs(songIds: List<String>): Flow<List<Song>> + @Query("SELECT * FROM song") + fun allSongs(): Flow<List<Song>> @Query("SELECT * FROM format WHERE id = :id") fun format(id: String?): Flow<FormatEntity?> @@ -216,7 +219,7 @@ interface DatabaseDao { fun lyrics(id: String?): Flow<LyricsEntity?> @Transaction - @Query("SELECT *, (SELECT COUNT(1) FROM song_artist_map JOIN song ON song_artist_map.songId = song.id WHERE artistId = artist.id AND song.inLibrary IS NOT NULL) AS songCount FROM artist WHERE songCount > 0 ORDER BY bookmarkedAt") + @Query("SELECT *, (SELECT COUNT(1) FROM song_artist_map JOIN song ON song_artist_map.songId = song.id WHERE artistId = artist.id AND song.inLibrary IS NOT NULL) AS songCount FROM artist WHERE songCount > 0 ORDER BY rowId") fun artistsByCreateDateAsc(): Flow<List<Artist>> @Transaction @@ -289,7 +292,11 @@ interface DatabaseDao { ArtistSortType.NAME -> artistsByNameAsc() ArtistSortType.SONG_COUNT -> artistsBySongCountAsc() ArtistSortType.PLAY_TIME -> artistsByPlayTimeAsc() - }.map { it.reversed(descending) } + }.map { artists -> + artists + .filter { it.artist.isYouTubeArtist } + .reversed(descending) + } fun artistsBookmarked(sortType: ArtistSortType, descending: Boolean) = when (sortType) { @@ -297,33 +304,33 @@ interface DatabaseDao { ArtistSortType.NAME -> artistsBookmarkedByNameAsc() ArtistSortType.SONG_COUNT -> artistsBookmarkedBySongCountAsc() ArtistSortType.PLAY_TIME -> artistsBookmarkedByPlayTimeAsc() - }.map { it.reversed(descending) } + }.map { artists -> + artists + .filter { it.artist.isYouTubeArtist } + .reversed(descending) + } @Query("SELECT * FROM artist WHERE id = :id") fun artist(id: String): Flow<ArtistEntity?> @Transaction - @Query("SELECT * FROM album ORDER BY rowId") - fun albumsByRowIdAsc(): Flow<List<Album>> - - @Transaction - @Query("SELECT * FROM album ORDER BY createDate") + @Query("SELECT * FROM album WHERE EXISTS(SELECT * FROM song WHERE song.albumId = album.id AND song.inLibrary IS NOT NULL) ORDER BY rowId") fun albumsByCreateDateAsc(): Flow<List<Album>> @Transaction - @Query("SELECT * FROM album ORDER BY title") + @Query("SELECT * FROM album WHERE EXISTS(SELECT * FROM song WHERE song.albumId = album.id AND song.inLibrary IS NOT NULL) ORDER BY title") fun albumsByNameAsc(): Flow<List<Album>> @Transaction - @Query("SELECT * FROM album ORDER BY year") + @Query("SELECT * FROM album WHERE EXISTS(SELECT * FROM song WHERE song.albumId = album.id AND song.inLibrary IS NOT NULL) ORDER BY year") fun albumsByYearAsc(): Flow<List<Album>> @Transaction - @Query("SELECT * FROM album ORDER BY songCount") + @Query("SELECT * FROM album WHERE EXISTS(SELECT * FROM song WHERE song.albumId = album.id AND song.inLibrary IS NOT NULL) ORDER BY songCount") fun albumsBySongCountAsc(): Flow<List<Album>> @Transaction - @Query("SELECT * FROM album ORDER BY duration") + @Query("SELECT * FROM album WHERE EXISTS(SELECT * FROM song WHERE song.albumId = album.id AND song.inLibrary IS NOT NULL) ORDER BY duration") fun albumsByLengthAsc(): Flow<List<Album>> @Transaction @@ -333,17 +340,52 @@ interface DatabaseDao { FROM album JOIN song ON song.albumId = album.id + WHERE EXISTS(SELECT * FROM song WHERE song.albumId = album.id AND song.inLibrary IS NOT NULL) GROUP BY album.id ORDER BY SUM(song.totalPlayTime) """ ) fun albumsByPlayTimeAsc(): Flow<List<Album>> + @Transaction + @Query("SELECT * FROM album WHERE bookmarkedAt IS NOT NULL ORDER BY rowId") + fun albumsLikedByCreateDateAsc(): Flow<List<Album>> + + @Transaction + @Query("SELECT * FROM album WHERE bookmarkedAt IS NOT NULL ORDER BY title") + fun albumsLikedByNameAsc(): Flow<List<Album>> + + @Transaction + @Query("SELECT * FROM album WHERE bookmarkedAt IS NOT NULL ORDER BY year") + fun albumsLikedByYearAsc(): Flow<List<Album>> + + @Transaction + @Query("SELECT * FROM album WHERE bookmarkedAt IS NOT NULL ORDER BY songCount") + fun albumsLikedBySongCountAsc(): Flow<List<Album>> + + @Transaction + @Query("SELECT * FROM album WHERE bookmarkedAt IS NOT NULL ORDER BY duration") + fun albumsLikedByLengthAsc(): Flow<List<Album>> + + @Transaction + @Query( + """ + SELECT album.* + FROM album + JOIN song + ON song.albumId = album.id + WHERE bookmarkedAt IS NOT NULL + GROUP BY album.id + ORDER BY SUM(song.totalPlayTime) + """ + ) + fun albumsLikedByPlayTimeAsc(): Flow<List<Album>> + fun albums(sortType: AlbumSortType, descending: Boolean) = when (sortType) { AlbumSortType.CREATE_DATE -> albumsByCreateDateAsc() AlbumSortType.NAME -> albumsByNameAsc() - AlbumSortType.ARTIST -> albumsByRowIdAsc().map { albums -> + AlbumSortType.ARTIST -> albumsByCreateDateAsc().map { albums -> albums.sortedBy { album -> album.artists.joinToString(separator = "") { it.name } } @@ -355,6 +397,22 @@ interface DatabaseDao { AlbumSortType.PLAY_TIME -> albumsByPlayTimeAsc() }.map { it.reversed(descending) } + fun albumsLiked(sortType: AlbumSortType, descending: Boolean) = + when (sortType) { + AlbumSortType.CREATE_DATE -> albumsLikedByCreateDateAsc() + AlbumSortType.NAME -> albumsLikedByNameAsc() + AlbumSortType.ARTIST -> albumsLikedByCreateDateAsc().map { albums -> + albums.sortedBy { album -> + album.artists.joinToString(separator = "") { it.name } + } + } + + AlbumSortType.YEAR -> albumsLikedByYearAsc() + AlbumSortType.SONG_COUNT -> albumsLikedBySongCountAsc() + AlbumSortType.LENGTH -> albumsLikedByLengthAsc() + AlbumSortType.PLAY_TIME -> albumsLikedByPlayTimeAsc() + }.map { it.reversed(descending) } + @Transaction @Query("SELECT * FROM album WHERE id = :id") fun album(id: String): Flow<Album?> @@ -395,7 +453,7 @@ interface DatabaseDao { fun searchArtists(query: String, previewSize: Int = Int.MAX_VALUE): Flow<List<Artist>> @Transaction - @Query("SELECT * FROM album WHERE title LIKE '%' || :query || '%' LIMIT :previewSize") + @Query("SELECT * FROM album WHERE title LIKE '%' || :query || '%' AND EXISTS(SELECT * FROM song WHERE song.albumId = album.id AND song.inLibrary IS NOT NULL) LIMIT :previewSize") fun searchAlbums(query: String, previewSize: Int = Int.MAX_VALUE): Flow<List<Album>> @Transaction @@ -536,33 +594,15 @@ interface DatabaseDao { ?.forEach(::insert) } - @Transaction - fun insert(albumWithSongs: AlbumWithSongs) { - if (insert(albumWithSongs.album) == -1L) return - albumWithSongs.songs.map(Song::toMediaMetadata).forEach(::insert) - albumWithSongs.songs.mapIndexed { index, song -> - SongAlbumMap( - songId = song.id, - albumId = albumWithSongs.album.id, - index = index - ) - }.forEach(::upsert) - albumWithSongs.artists.forEach(::insert) - albumWithSongs.artists.mapIndexed { index, artist -> - AlbumArtistMap( - albumId = albumWithSongs.album.id, - artistId = artist.id, - order = index - ) - }.forEach(::insert) - } - @Update fun update(song: SongEntity) @Update fun update(artist: ArtistEntity) + @Update + fun update(album: AlbumEntity) + @Update fun update(playlist: PlaylistEntity) @@ -579,6 +619,46 @@ interface DatabaseDao { ) } + @Transaction + fun update(album: AlbumEntity, albumPage: AlbumPage) { + update( + album.copy( + id = albumPage.album.browseId, + title = albumPage.album.title, + year = albumPage.album.year, + thumbnailUrl = albumPage.album.thumbnail, + songCount = albumPage.songs.size, + duration = albumPage.songs.sumOf { it.duration ?: 0 } + ) + ) + albumPage.songs.map(SongItem::toMediaMetadata) + .onEach(::insert) + .mapIndexed { index, song -> + SongAlbumMap( + songId = song.id, + albumId = albumPage.album.browseId, + index = index + ) + } + .forEach(::upsert) + albumPage.album.artists + ?.map { artist -> + ArtistEntity( + id = artist.id ?: artistByName(artist.name)?.id ?: ArtistEntity.generateArtistId(), + name = artist.name + ) + } + ?.onEach(::insert) + ?.mapIndexed { index, artist -> + AlbumArtistMap( + albumId = albumPage.album.browseId, + artistId = artist.id, + order = index + ) + } + ?.forEach(::insert) + } + @Upsert fun upsert(map: SongAlbumMap) diff --git a/app/src/main/java/com/zionhuang/music/db/MusicDatabase.kt b/app/src/main/java/com/zionhuang/music/db/MusicDatabase.kt index a867fa5d8..a6c12f021 100644 --- a/app/src/main/java/com/zionhuang/music/db/MusicDatabase.kt +++ b/app/src/main/java/com/zionhuang/music/db/MusicDatabase.kt @@ -59,7 +59,7 @@ class MusicDatabase( SortedSongAlbumMap::class, PlaylistSongMapPreview::class ], - version = 11, + version = 12, exportSchema = true, autoMigrations = [ AutoMigration(from = 2, to = 3), @@ -70,7 +70,8 @@ class MusicDatabase( AutoMigration(from = 7, to = 8, spec = Migration7To8::class), AutoMigration(from = 8, to = 9), AutoMigration(from = 9, to = 10, spec = Migration9To10::class), - AutoMigration(from = 10, to = 11, spec = Migration10To11::class) + AutoMigration(from = 10, to = 11, spec = Migration10To11::class), + AutoMigration(from = 11, to = 12, spec = Migration11To12::class) ] ) @TypeConverters(Converters::class) @@ -314,4 +315,31 @@ class Migration9To10 : AutoMigrationSpec DeleteColumn(tableName = "artist", columnName = "description"), DeleteColumn(tableName = "artist", columnName = "createDate") ) -class Migration10To11 : AutoMigrationSpec \ No newline at end of file +class Migration10To11 : AutoMigrationSpec + +@DeleteColumn.Entries( + DeleteColumn(tableName = "album", columnName = "createDate") +) +class Migration11To12 : AutoMigrationSpec { + override fun onPostMigrate(db: SupportSQLiteDatabase) { + db.execSQL("UPDATE album SET bookmarkedAt = lastUpdateTime") + db.query("SELECT DISTINCT albumId, albumName FROM song").use { cursor -> + while (cursor.moveToNext()) { + val albumId = cursor.getString(0) + val albumName = cursor.getString(1) + db.insert( + table = "album", + conflictAlgorithm = SQLiteDatabase.CONFLICT_IGNORE, + values = contentValuesOf( + "id" to albumId, + "title" to albumName, + "songCount" to 0, + "duration" to 0, + "lastUpdateTime" to 0 + ) + ) + } + } + db.query("CREATE INDEX IF NOT EXISTS `index_song_albumId` ON `song` (`albumId`)") + } +} diff --git a/app/src/main/java/com/zionhuang/music/db/entities/AlbumEntity.kt b/app/src/main/java/com/zionhuang/music/db/entities/AlbumEntity.kt index 0f1678107..a56283291 100644 --- a/app/src/main/java/com/zionhuang/music/db/entities/AlbumEntity.kt +++ b/app/src/main/java/com/zionhuang/music/db/entities/AlbumEntity.kt @@ -12,8 +12,13 @@ data class AlbumEntity( val title: String, val year: Int? = null, val thumbnailUrl: String? = null, + val themeColor: Int? = null, val songCount: Int, val duration: Int, - val createDate: LocalDateTime = LocalDateTime.now(), val lastUpdateTime: LocalDateTime = LocalDateTime.now(), -) \ No newline at end of file + val bookmarkedAt: LocalDateTime? = null, +) { + fun toggleLike() = copy( + bookmarkedAt = if (bookmarkedAt != null) null else LocalDateTime.now() + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/zionhuang/music/db/entities/ArtistEntity.kt b/app/src/main/java/com/zionhuang/music/db/entities/ArtistEntity.kt index e38a3a937..6fa79bd9e 100644 --- a/app/src/main/java/com/zionhuang/music/db/entities/ArtistEntity.kt +++ b/app/src/main/java/com/zionhuang/music/db/entities/ArtistEntity.kt @@ -21,6 +21,10 @@ data class ArtistEntity( val isLocalArtist: Boolean get() = id.startsWith("LA") + fun toggleLike() = copy( + bookmarkedAt = if (bookmarkedAt != null) null else LocalDateTime.now() + ) + companion object { fun generateArtistId() = "LA" + RandomStringUtils.random(8, true, false) } diff --git a/app/src/main/java/com/zionhuang/music/db/entities/SongEntity.kt b/app/src/main/java/com/zionhuang/music/db/entities/SongEntity.kt index c5a1d3681..bbc53b145 100644 --- a/app/src/main/java/com/zionhuang/music/db/entities/SongEntity.kt +++ b/app/src/main/java/com/zionhuang/music/db/entities/SongEntity.kt @@ -2,11 +2,19 @@ package com.zionhuang.music.db.entities import androidx.compose.runtime.Immutable import androidx.room.Entity +import androidx.room.Index import androidx.room.PrimaryKey import java.time.LocalDateTime @Immutable -@Entity(tableName = "song") +@Entity( + tableName = "song", + indices = [ + Index( + value = ["albumId"] + ) + ] +) data class SongEntity( @PrimaryKey val id: String, val title: String, diff --git a/app/src/main/java/com/zionhuang/music/extensions/UtilExt.kt b/app/src/main/java/com/zionhuang/music/extensions/UtilExt.kt new file mode 100644 index 000000000..f4baf24ed --- /dev/null +++ b/app/src/main/java/com/zionhuang/music/extensions/UtilExt.kt @@ -0,0 +1,8 @@ +package com.zionhuang.music.extensions + +fun <T> tryOrNull(block: () -> T): T? = + try { + block() + } catch (e: Exception) { + null + } \ No newline at end of file diff --git a/app/src/main/java/com/zionhuang/music/lyrics/LyricsHelper.kt b/app/src/main/java/com/zionhuang/music/lyrics/LyricsHelper.kt index 9c1121aa2..86b791398 100644 --- a/app/src/main/java/com/zionhuang/music/lyrics/LyricsHelper.kt +++ b/app/src/main/java/com/zionhuang/music/lyrics/LyricsHelper.kt @@ -4,6 +4,7 @@ import android.content.Context import android.util.LruCache import com.zionhuang.music.db.entities.LyricsEntity.Companion.LYRICS_NOT_FOUND import com.zionhuang.music.models.MediaMetadata +import com.zionhuang.music.utils.reportException import dagger.hilt.android.qualifiers.ApplicationContext import javax.inject.Inject @@ -28,7 +29,7 @@ class LyricsHelper @Inject constructor( ).onSuccess { lyrics -> return lyrics }.onFailure { - it.printStackTrace() + reportException(it) } } } diff --git a/app/src/main/java/com/zionhuang/music/lyrics/LyricsUtils.kt b/app/src/main/java/com/zionhuang/music/lyrics/LyricsUtils.kt index 821df447c..bd127a36f 100644 --- a/app/src/main/java/com/zionhuang/music/lyrics/LyricsUtils.kt +++ b/app/src/main/java/com/zionhuang/music/lyrics/LyricsUtils.kt @@ -5,8 +5,8 @@ import com.zionhuang.music.ui.component.animateScrollDuration @Suppress("RegExpRedundantEscape") object LyricsUtils { - private val LINE_REGEX = "((\\[\\d\\d:\\d\\d\\.\\d{2,3}\\])+)(.+)".toRegex() - private val TIME_REGEX = "\\[(\\d\\d):(\\d\\d)\\.(\\d{2,3})\\]".toRegex() + val LINE_REGEX = "((\\[\\d\\d:\\d\\d\\.\\d{2,3}\\])+)(.+)".toRegex() + val TIME_REGEX = "\\[(\\d\\d):(\\d\\d)\\.(\\d{2,3})\\]".toRegex() fun parseLyrics(lyrics: String): List<LyricsEntry> = lyrics.lines() diff --git a/app/src/main/java/com/zionhuang/music/playback/MusicService.kt b/app/src/main/java/com/zionhuang/music/playback/MusicService.kt index 219738c80..6a63749aa 100644 --- a/app/src/main/java/com/zionhuang/music/playback/MusicService.kt +++ b/app/src/main/java/com/zionhuang/music/playback/MusicService.kt @@ -120,6 +120,7 @@ import com.zionhuang.music.utils.CoilBitmapLoader import com.zionhuang.music.utils.dataStore import com.zionhuang.music.utils.enumPreference import com.zionhuang.music.utils.get +import com.zionhuang.music.utils.reportException import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -134,6 +135,7 @@ import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChangedBy import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map import kotlinx.coroutines.guava.future import kotlinx.coroutines.launch @@ -711,7 +713,7 @@ class MusicService : MediaLibraryService(), } } }.onFailure { - it.printStackTrace() + reportException(it) } } @@ -831,15 +833,18 @@ class MusicService : MediaLibraryService(), LIKED_PLAYLIST_ID -> database.likedSongs(SongSortType.CREATE_DATE, true) DOWNLOADED_PLAYLIST_ID -> { val downloads = downloadUtil.downloads.value - database.songs( - downloads.filter { (_, download) -> - download.state == Download.STATE_COMPLETED - }.keys.toList() - ).map { songs -> - songs.map { it to downloads[it.id] } - .sortedBy { it.second?.updateTimeMs ?: 0L } - .map { it.first } - } + database.allSongs() + .flowOn(Dispatchers.IO) + .map { songs -> + songs.filter { + downloads[it.id]?.state == Download.STATE_COMPLETED + } + } + .map { songs -> + songs.map { it to downloads[it.id] } + .sortedBy { it.second?.updateTimeMs ?: 0L } + .map { it.first } + } } else -> database.playlistSongs(playlistId).map { list -> @@ -917,15 +922,18 @@ class MusicService : MediaLibraryService(), LIKED_PLAYLIST_ID -> database.likedSongs(SongSortType.CREATE_DATE, descending = true) DOWNLOADED_PLAYLIST_ID -> { val downloads = downloadUtil.downloads.value - database.songs( - downloads.filter { (_, download) -> - download.state == Download.STATE_COMPLETED - }.keys.toList() - ).map { songs -> - songs.map { it to downloads[it.id] } - .sortedBy { it.second?.updateTimeMs ?: 0L } - .map { it.first } - } + database.allSongs() + .flowOn(Dispatchers.IO) + .map { songs -> + songs.filter { + downloads[it.id]?.state == Download.STATE_COMPLETED + } + } + .map { songs -> + songs.map { it to downloads[it.id] } + .sortedBy { it.second?.updateTimeMs ?: 0L } + .map { it.first } + } } else -> database.playlistSongs(playlistId).map { list -> diff --git a/app/src/main/java/com/zionhuang/music/playback/PlayerConnection.kt b/app/src/main/java/com/zionhuang/music/playback/PlayerConnection.kt index 74c63cb2c..e66ef1253 100644 --- a/app/src/main/java/com/zionhuang/music/playback/PlayerConnection.kt +++ b/app/src/main/java/com/zionhuang/music/playback/PlayerConnection.kt @@ -1,5 +1,6 @@ package com.zionhuang.music.playback +import android.content.Context import androidx.media3.common.MediaItem import androidx.media3.common.PlaybackException import androidx.media3.common.Player @@ -11,23 +12,31 @@ import androidx.media3.common.Player.REPEAT_MODE_OFF import androidx.media3.common.Player.REPEAT_MODE_ONE import androidx.media3.common.Player.STATE_ENDED import androidx.media3.common.Timeline +import com.zionhuang.music.constants.TranslateLyricsKey import com.zionhuang.music.db.MusicDatabase +import com.zionhuang.music.db.entities.LyricsEntity.Companion.LYRICS_NOT_FOUND import com.zionhuang.music.extensions.currentMetadata import com.zionhuang.music.extensions.getCurrentQueueIndex import com.zionhuang.music.extensions.getQueueWindows import com.zionhuang.music.extensions.metadata import com.zionhuang.music.playback.MusicService.MusicBinder import com.zionhuang.music.playback.queues.Queue +import com.zionhuang.music.utils.TranslationHelper +import com.zionhuang.music.utils.dataStore +import com.zionhuang.music.utils.reportException import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn @OptIn(ExperimentalCoroutinesApi::class) class PlayerConnection( + context: Context, binder: MusicBinder, val database: MusicDatabase, scope: CoroutineScope, @@ -44,9 +53,26 @@ class PlayerConnection( val currentSong = mediaMetadata.flatMapLatest { database.song(it?.id) } - val currentLyrics = mediaMetadata.flatMapLatest { mediaMetadata -> - database.lyrics(mediaMetadata?.id) - } + val translating = MutableStateFlow(false) + val currentLyrics = combine( + context.dataStore.data.map { + it[TranslateLyricsKey] ?: true + }.distinctUntilChanged(), + mediaMetadata.flatMapLatest { mediaMetadata -> + database.lyrics(mediaMetadata?.id) + } + ) { translateEnabled, lyrics -> + if (!translateEnabled || lyrics == null || lyrics.lyrics == LYRICS_NOT_FOUND) return@combine lyrics + translating.value = true + try { + TranslationHelper.translate(lyrics) + } catch (e: Exception) { + reportException(e) + lyrics + }.also { + translating.value = false + } + }.stateIn(scope, SharingStarted.Lazily, null) val currentFormat = mediaMetadata.flatMapLatest { mediaMetadata -> database.format(mediaMetadata?.id) } @@ -148,6 +174,9 @@ class PlayerConnection( } override fun onPlayerErrorChanged(playbackError: PlaybackException?) { + if (playbackError != null) { + reportException(playbackError) + } error.value = playbackError } diff --git a/app/src/main/java/com/zionhuang/music/ui/component/ChipsRow.kt b/app/src/main/java/com/zionhuang/music/ui/component/ChipsRow.kt new file mode 100644 index 000000000..fafaa2952 --- /dev/null +++ b/app/src/main/java/com/zionhuang/music/ui/component/ChipsRow.kt @@ -0,0 +1,44 @@ +package com.zionhuang.music.ui.component + +import androidx.compose.foundation.horizontalScroll +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.rememberScrollState +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.FilterChip +import androidx.compose.material3.FilterChipDefaults +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun <E> ChipsRow( + chips: List<Pair<E, String>>, + currentValue: E, + onValueUpdate: (E) -> Unit, + modifier: Modifier = Modifier, +) { + Row( + modifier = modifier + .fillMaxWidth() + .horizontalScroll(rememberScrollState()) + ) { + Spacer(Modifier.width(12.dp)) + + chips.forEach { (value, label) -> + FilterChip( + label = { Text(label) }, + selected = currentValue == value, + colors = FilterChipDefaults.filterChipColors(containerColor = MaterialTheme.colorScheme.background), + onClick = { onValueUpdate(value) } + ) + + Spacer(Modifier.width(8.dp)) + } + } +} diff --git a/app/src/main/java/com/zionhuang/music/ui/component/Items.kt b/app/src/main/java/com/zionhuang/music/ui/component/Items.kt index 1d35bf1aa..88045a713 100644 --- a/app/src/main/java/com/zionhuang/music/ui/component/Items.kt +++ b/app/src/main/java/com/zionhuang/music/ui/component/Items.kt @@ -31,12 +31,15 @@ import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.pluralStringResource import androidx.compose.ui.text.font.FontWeight @@ -45,11 +48,14 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.compose.ui.util.fastForEachIndexed +import androidx.core.graphics.drawable.toBitmapOrNull import androidx.media3.exoplayer.offline.Download import androidx.media3.exoplayer.offline.Download.STATE_COMPLETED import androidx.media3.exoplayer.offline.Download.STATE_DOWNLOADING import androidx.media3.exoplayer.offline.Download.STATE_QUEUED import coil.compose.AsyncImage +import coil.compose.AsyncImagePainter +import coil.request.ImageRequest import com.zionhuang.innertube.models.AlbumItem import com.zionhuang.innertube.models.ArtistItem import com.zionhuang.innertube.models.PlaylistItem @@ -67,8 +73,11 @@ import com.zionhuang.music.db.entities.Artist import com.zionhuang.music.db.entities.Playlist import com.zionhuang.music.db.entities.Song import com.zionhuang.music.models.MediaMetadata +import com.zionhuang.music.ui.theme.extractThemeColor import com.zionhuang.music.utils.joinByBullet import com.zionhuang.music.utils.makeTimeString +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch @Composable inline fun ListItem( @@ -302,17 +311,8 @@ fun ArtistListItem( fun AlbumListItem( album: Album, modifier: Modifier = Modifier, - isActive: Boolean = false, - isPlaying: Boolean = false, - trailingContent: @Composable RowScope.() -> Unit = {}, -) = ListItem( - title = album.album.title, - subtitle = joinByBullet( - album.artists.joinToString { it.name }, - pluralStringResource(R.plurals.n_song, album.album.songCount, album.album.songCount), - album.album.year?.toString() - ), - badges = { + showLikedIcon: Boolean = true, + badges: @Composable RowScope.() -> Unit = { val database = LocalDatabase.current val downloadUtil = LocalDownloadUtil.current var songs by remember { @@ -346,6 +346,17 @@ fun AlbumListItem( } } + if (showLikedIcon && album.album.bookmarkedAt != null) { + Icon( + painter = painterResource(R.drawable.favorite), + contentDescription = null, + tint = MaterialTheme.colorScheme.error, + modifier = Modifier + .size(18.dp) + .padding(end = 2.dp) + ) + } + when (downloadState) { STATE_COMPLETED -> Icon( painter = painterResource(R.drawable.offline), @@ -365,10 +376,38 @@ fun AlbumListItem( else -> {} } }, + isActive: Boolean = false, + isPlaying: Boolean = false, + trailingContent: @Composable RowScope.() -> Unit = {}, +) = ListItem( + title = album.album.title, + subtitle = joinByBullet( + album.artists.joinToString { it.name }, + pluralStringResource(R.plurals.n_song, album.album.songCount, album.album.songCount), + album.album.year?.toString() + ), + badges = badges, thumbnailContent = { + val database = LocalDatabase.current + val coroutineScope = rememberCoroutineScope() + AsyncImage( - model = album.album.thumbnailUrl, + model = ImageRequest.Builder(LocalContext.current) + .data(album.album.thumbnailUrl) + .allowHardware(false) + .build(), contentDescription = null, + onState = { state -> + if (album.album.themeColor == null && state is AsyncImagePainter.State.Success) { + coroutineScope.launch(Dispatchers.IO) { + state.result.drawable.toBitmapOrNull()?.extractThemeColor()?.toArgb()?.let { color -> + database.query { + update(album.album.copy(themeColor = color)) + } + } + } + } + }, modifier = Modifier .size(ListThumbnailSize) .clip(RoundedCornerShape(ThumbnailCornerRadius)) @@ -487,9 +526,10 @@ fun YouTubeListItem( val database = LocalDatabase.current val song by database.song(item.id).collectAsState(initial = null) val album by database.album(item.id).collectAsState(initial = null) - val playlist by database.playlist(item.id).collectAsState(initial = null) - if (item is SongItem && song?.song?.liked == true) { + if (item is SongItem && song?.song?.liked == true || + item is AlbumItem && album?.album?.bookmarkedAt != null + ) { Icon( painter = painterResource(R.drawable.favorite), contentDescription = null, @@ -508,10 +548,7 @@ fun YouTubeListItem( .padding(end = 2.dp) ) } - if (item is SongItem && song?.song?.inLibrary != null || - item is AlbumItem && album != null || - item is PlaylistItem && playlist != null - ) { + if (item is SongItem && song?.song?.inLibrary != null) { Icon( painter = painterResource(R.drawable.library_add_check), contentDescription = null, @@ -606,9 +643,10 @@ fun YouTubeGridItem( val database = LocalDatabase.current val song by database.song(item.id).collectAsState(initial = null) val album by database.album(item.id).collectAsState(initial = null) - val playlist by database.playlist(item.id).collectAsState(initial = null) - if (item is SongItem && song?.song?.liked == true) { + if (item is SongItem && song?.song?.liked == true || + item is AlbumItem && album?.album?.bookmarkedAt != null + ) { Icon( painter = painterResource(R.drawable.favorite), contentDescription = null, @@ -627,10 +665,7 @@ fun YouTubeGridItem( .padding(end = 2.dp) ) } - if (item is SongItem && song?.song?.inLibrary != null || - item is AlbumItem && album != null || - item is PlaylistItem && playlist != null - ) { + if (item is SongItem && song?.song?.inLibrary != null) { Icon( painter = painterResource(R.drawable.library_add_check), contentDescription = null, diff --git a/app/src/main/java/com/zionhuang/music/ui/component/Lyrics.kt b/app/src/main/java/com/zionhuang/music/ui/component/Lyrics.kt index d315665ef..ae8e3acad 100644 --- a/app/src/main/java/com/zionhuang/music/ui/component/Lyrics.kt +++ b/app/src/main/java/com/zionhuang/music/ui/component/Lyrics.kt @@ -1,18 +1,34 @@ package com.zionhuang.music.ui.component -import android.app.SearchManager -import android.content.Intent -import androidx.compose.animation.animateContentSize import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxWithConstraints +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.WindowInsetsSides +import androidx.compose.foundation.layout.add +import androidx.compose.foundation.layout.asPaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.only +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.systemBars import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll -import androidx.compose.material3.* -import androidx.compose.runtime.* +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.LocalContentColor +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha @@ -20,35 +36,31 @@ import androidx.compose.ui.geometry.Offset import androidx.compose.ui.input.nestedscroll.NestedScrollConnection import androidx.compose.ui.input.nestedscroll.NestedScrollSource import androidx.compose.ui.input.nestedscroll.nestedScroll -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.Velocity import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import androidx.hilt.navigation.compose.hiltViewModel -import com.zionhuang.music.LocalDatabase +import com.zionhuang.music.BuildConfig import com.zionhuang.music.LocalPlayerConnection import com.zionhuang.music.R import com.zionhuang.music.constants.LyricsTextPositionKey -import com.zionhuang.music.db.entities.LyricsEntity +import com.zionhuang.music.constants.TranslateLyricsKey import com.zionhuang.music.db.entities.LyricsEntity.Companion.LYRICS_NOT_FOUND import com.zionhuang.music.lyrics.LyricsEntry import com.zionhuang.music.lyrics.LyricsEntry.Companion.HEAD_LYRICS_ENTRY import com.zionhuang.music.lyrics.LyricsUtils.findCurrentLineIndex import com.zionhuang.music.lyrics.LyricsUtils.parseLyrics -import com.zionhuang.music.models.MediaMetadata import com.zionhuang.music.ui.component.shimmer.ShimmerHost import com.zionhuang.music.ui.component.shimmer.TextPlaceholder +import com.zionhuang.music.ui.menu.LyricsMenu import com.zionhuang.music.ui.screens.settings.LyricsPosition import com.zionhuang.music.ui.utils.fadingEdge import com.zionhuang.music.utils.rememberEnumPreference -import com.zionhuang.music.viewmodels.LyricsMenuViewModel +import com.zionhuang.music.utils.rememberPreference import kotlinx.coroutines.delay import kotlinx.coroutines.isActive import kotlin.time.Duration.Companion.seconds @@ -63,11 +75,14 @@ fun Lyrics( val density = LocalDensity.current val lyricsTextPosition by rememberEnumPreference(LyricsTextPositionKey, LyricsPosition.CENTER) + var translationEnabled by rememberPreference(TranslateLyricsKey, false) val mediaMetadata by playerConnection.mediaMetadata.collectAsState() + val translating by playerConnection.translating.collectAsState() val lyricsEntity by playerConnection.currentLyrics.collectAsState(initial = null) - val lyrics = remember(lyricsEntity) { - lyricsEntity?.lyrics + val lyrics = remember(lyricsEntity, translating) { + if (translating) null + else lyricsEntity?.lyrics } val lines = remember(lyrics) { @@ -162,31 +177,8 @@ fun Lyrics( }) ) { val displayedCurrentLineIndex = if (isSeeking) deferredCurrentLineIndex else currentLineIndex - itemsIndexed( - items = lines - ) { index, item -> - Text( - text = item.text, - fontSize = 20.sp, - color = if (index == displayedCurrentLineIndex) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.secondary, - textAlign = when (lyricsTextPosition) { - LyricsPosition.LEFT -> TextAlign.Left - LyricsPosition.CENTER -> TextAlign.Center - LyricsPosition.RIGHT -> TextAlign.Right - }, - fontWeight = FontWeight.Bold, - modifier = Modifier - .fillMaxWidth() - .clickable(enabled = isSynced) { - playerConnection.player.seekTo(item.time) - lastPreviewTime = 0L - } - .padding(horizontal = 24.dp, vertical = 8.dp) - .alpha(if (!isSynced || index == displayedCurrentLineIndex) 1f else 0.5f) - ) - } - if (lyrics == null) { + if (lyrics == null || translating) { item { ShimmerHost { repeat(10) { @@ -205,6 +197,30 @@ fun Lyrics( } } } + } else { + itemsIndexed( + items = lines + ) { index, item -> + Text( + text = item.text, + fontSize = 20.sp, + color = if (index == displayedCurrentLineIndex) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.secondary, + textAlign = when (lyricsTextPosition) { + LyricsPosition.LEFT -> TextAlign.Left + LyricsPosition.CENTER -> TextAlign.Center + LyricsPosition.RIGHT -> TextAlign.Right + }, + fontWeight = FontWeight.Bold, + modifier = Modifier + .fillMaxWidth() + .clickable(enabled = isSynced) { + playerConnection.player.seekTo(item.time) + lastPreviewTime = 0L + } + .padding(horizontal = 24.dp, vertical = 8.dp) + .alpha(if (!isSynced || index == displayedCurrentLineIndex) 1f else 0.5f) + ) + } } } @@ -227,279 +243,44 @@ fun Lyrics( } mediaMetadata?.let { mediaMetadata -> - IconButton( + Row( modifier = Modifier .align(Alignment.BottomEnd) - .padding(12.dp), - onClick = { - menuState.show { - LyricsMenu( - lyricsProvider = { lyricsEntity }, - mediaMetadataProvider = { mediaMetadata }, - onDismiss = menuState::dismiss - ) - } - } + .padding(end = 12.dp) ) { - Icon( - painter = painterResource(id = R.drawable.more_horiz), - contentDescription = null - ) - } - } - } -} - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun LyricsMenu( - lyricsProvider: () -> LyricsEntity?, - mediaMetadataProvider: () -> MediaMetadata, - onDismiss: () -> Unit, - viewModel: LyricsMenuViewModel = hiltViewModel(), -) { - val context = LocalContext.current - val database = LocalDatabase.current - - var showEditDialog by rememberSaveable { - mutableStateOf(false) - } - - if (showEditDialog) { - TextFieldDialog( - onDismiss = { showEditDialog = false }, - icon = { Icon(painter = painterResource(R.drawable.edit), contentDescription = null) }, - title = { Text(text = mediaMetadataProvider().title) }, - initialTextFieldValue = TextFieldValue(lyricsProvider()?.lyrics.orEmpty()), - singleLine = false, - onDone = { - database.query { - upsert( - LyricsEntity( - id = mediaMetadataProvider().id, - lyrics = it - ) - ) - } - } - ) - } - - var showSearchDialog by rememberSaveable { - mutableStateOf(false) - } - var showSearchResultDialog by rememberSaveable { - mutableStateOf(false) - } - - val searchMediaMetadata = remember(showSearchDialog) { - mediaMetadataProvider() - } - val (titleField, onTitleFieldChange) = rememberSaveable(showSearchDialog, stateSaver = TextFieldValue.Saver) { - mutableStateOf( - TextFieldValue( - text = mediaMetadataProvider().title - ) - ) - } - val (artistField, onArtistFieldChange) = rememberSaveable(showSearchDialog, stateSaver = TextFieldValue.Saver) { - mutableStateOf( - TextFieldValue( - text = mediaMetadataProvider().artists.joinToString { it.name } - ) - ) - } - - if (showSearchDialog) { - DefaultDialog( - modifier = Modifier.verticalScroll(rememberScrollState()), - onDismiss = { showSearchDialog = false }, - icon = { Icon(painter = painterResource(R.drawable.search), contentDescription = null) }, - title = { Text(stringResource(R.string.search_lyrics)) }, - buttons = { - TextButton( - onClick = { showSearchDialog = false } - ) { - Text(stringResource(android.R.string.cancel)) - } - - Spacer(Modifier.width(8.dp)) - - TextButton( - onClick = { - showSearchDialog = false - onDismiss() - try { - context.startActivity( - Intent(Intent.ACTION_WEB_SEARCH).apply { - putExtra(SearchManager.QUERY, "${artistField.text} ${titleField.text} lyrics") - } - ) - } catch (_: Exception) { - } - } - ) { - Text(stringResource(R.string.search_online)) - } - - Spacer(Modifier.width(8.dp)) - - TextButton( - onClick = { - viewModel.search(searchMediaMetadata.id, titleField.text, artistField.text, searchMediaMetadata.duration) - showSearchResultDialog = true - } - ) { - Text(stringResource(android.R.string.ok)) - } - } - ) { - OutlinedTextField( - value = titleField, - onValueChange = onTitleFieldChange, - singleLine = true, - label = { Text(stringResource(R.string.song_title)) } - ) - - Spacer(Modifier.height(12.dp)) - - OutlinedTextField( - value = artistField, - onValueChange = onArtistFieldChange, - singleLine = true, - label = { Text(stringResource(R.string.song_artists)) } - ) - } - } - - if (showSearchResultDialog) { - val results by viewModel.results.collectAsState() - val isLoading by viewModel.isLoading.collectAsState() - - var expandedItemIndex by rememberSaveable { - mutableStateOf(-1) - } - - ListDialog( - onDismiss = { showSearchResultDialog = false } - ) { - itemsIndexed(results) { index, result -> - Row( - modifier = Modifier - .fillMaxWidth() - .clickable { - onDismiss() - viewModel.cancelSearch() - database.query { - upsert( - LyricsEntity( - id = searchMediaMetadata.id, - lyrics = result.lyrics - ) - ) - } - } - .padding(12.dp) - .animateContentSize() - ) { - Column( - modifier = Modifier.weight(1f) - ) { - Text( - text = result.lyrics, - style = MaterialTheme.typography.bodyMedium, - maxLines = if (index == expandedItemIndex) Int.MAX_VALUE else 2, - overflow = TextOverflow.Ellipsis, - modifier = Modifier.padding(bottom = 4.dp) - ) - - Row( - verticalAlignment = Alignment.CenterVertically - ) { - Text( - text = result.providerName, - style = MaterialTheme.typography.labelMedium, - color = MaterialTheme.colorScheme.secondary, - maxLines = 1 - ) - if (result.lyrics.startsWith("[")) { - Icon( - painter = painterResource(R.drawable.sync), - contentDescription = null, - tint = MaterialTheme.colorScheme.secondary, - modifier = Modifier - .padding(start = 4.dp) - .size(18.dp) - ) - } - } - } - + if (BuildConfig.FLAVOR == "full") { IconButton( onClick = { - expandedItemIndex = if (expandedItemIndex == index) -1 else index + translationEnabled = !translationEnabled } ) { Icon( - painter = painterResource(if (index == expandedItemIndex) R.drawable.expand_less else R.drawable.expand_more), - contentDescription = null + painter = painterResource(id = R.drawable.translate), + contentDescription = null, + tint = LocalContentColor.current.copy(alpha = if (translationEnabled) 1f else 0.3f) ) } } - } - if (isLoading) { - item { - Box( - contentAlignment = Alignment.Center, - modifier = Modifier.fillMaxWidth() - ) { - CircularProgressIndicator() + IconButton( + onClick = { + menuState.show { + LyricsMenu( + lyricsProvider = { lyricsEntity }, + mediaMetadataProvider = { mediaMetadata }, + onDismiss = menuState::dismiss + ) + } } - } - } - - if (!isLoading && results.isEmpty()) { - item { - Text( - text = context.getString(R.string.lyrics_not_found), - textAlign = TextAlign.Center, - modifier = Modifier - .fillMaxWidth() + ) { + Icon( + painter = painterResource(id = R.drawable.more_horiz), + contentDescription = null ) } } } } - - GridMenu( - contentPadding = PaddingValues( - start = 8.dp, - top = 8.dp, - end = 8.dp, - bottom = 8.dp + WindowInsets.systemBars.asPaddingValues().calculateBottomPadding() - ) - ) { - GridMenuItem( - icon = R.drawable.edit, - title = R.string.edit - ) { - showEditDialog = true - } - GridMenuItem( - icon = R.drawable.cached, - title = R.string.refetch - ) { - onDismiss() - viewModel.refetchLyrics(mediaMetadataProvider(), lyricsProvider()) - } - GridMenuItem( - icon = R.drawable.search, - title = R.string.search, - ) { - showSearchDialog = true - } - } } const val animateScrollDuration = 300L diff --git a/app/src/main/java/com/zionhuang/music/ui/menu/AlbumMenu.kt b/app/src/main/java/com/zionhuang/music/ui/menu/AlbumMenu.kt index 0dfa5b39b..3ab27542b 100644 --- a/app/src/main/java/com/zionhuang/music/ui/menu/AlbumMenu.kt +++ b/app/src/main/java/com/zionhuang/music/ui/menu/AlbumMenu.kt @@ -13,9 +13,15 @@ import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.systemBars import androidx.compose.foundation.lazy.items import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.Divider +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.LocalContentColor +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -25,6 +31,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp @@ -49,6 +56,7 @@ import com.zionhuang.music.db.entities.Song import com.zionhuang.music.extensions.toMediaItem import com.zionhuang.music.playback.ExoDownloadService import com.zionhuang.music.playback.PlayerConnection +import com.zionhuang.music.ui.component.AlbumListItem import com.zionhuang.music.ui.component.DownloadGridMenu import com.zionhuang.music.ui.component.GridMenu import com.zionhuang.music.ui.component.GridMenuItem @@ -56,15 +64,16 @@ import com.zionhuang.music.ui.component.ListDialog @Composable fun AlbumMenu( - album: Album, + originalAlbum: Album, navController: NavController, playerConnection: PlayerConnection, - showDeleteButton: Boolean = true, onDismiss: () -> Unit, ) { val context = LocalContext.current val database = LocalDatabase.current val downloadUtil = LocalDownloadUtil.current + val libraryAlbum by database.album(originalAlbum.id).collectAsState(initial = originalAlbum) + val album = libraryAlbum ?: originalAlbum var songs by remember { mutableStateOf(emptyList<Song>()) } @@ -171,6 +180,29 @@ fun AlbumMenu( } } + AlbumListItem( + album = album, + showLikedIcon = false, + badges = {}, + trailingContent = { + IconButton( + onClick = { + database.query { + update(album.album.toggleLike()) + } + } + ) { + Icon( + painter = painterResource(if (album.album.bookmarkedAt != null) R.drawable.favorite else R.drawable.favorite_border), + tint = if (album.album.bookmarkedAt != null) MaterialTheme.colorScheme.error else LocalContentColor.current, + contentDescription = null + ) + } + } + ) + + Divider() + GridMenu( contentPadding = PaddingValues( start = 8.dp, @@ -249,16 +281,5 @@ fun AlbumMenu( } context.startActivity(Intent.createChooser(intent, null)) } - if (showDeleteButton) { - GridMenuItem( - icon = R.drawable.delete, - title = R.string.delete - ) { - onDismiss() - database.query { - delete(album.album) - } - } - } } } diff --git a/app/src/main/java/com/zionhuang/music/ui/menu/LyricsMenu.kt b/app/src/main/java/com/zionhuang/music/ui/menu/LyricsMenu.kt new file mode 100644 index 000000000..33d48d2e6 --- /dev/null +++ b/app/src/main/java/com/zionhuang/music/ui/menu/LyricsMenu.kt @@ -0,0 +1,310 @@ +package com.zionhuang.music.ui.menu + +import android.app.SearchManager +import android.content.Intent +import androidx.compose.animation.animateContentSize +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.asPaddingValues +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.systemBars +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.TextFieldValue +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import com.zionhuang.music.LocalDatabase +import com.zionhuang.music.R +import com.zionhuang.music.db.entities.LyricsEntity +import com.zionhuang.music.models.MediaMetadata +import com.zionhuang.music.ui.component.DefaultDialog +import com.zionhuang.music.ui.component.GridMenu +import com.zionhuang.music.ui.component.GridMenuItem +import com.zionhuang.music.ui.component.ListDialog +import com.zionhuang.music.ui.component.TextFieldDialog +import com.zionhuang.music.viewmodels.LyricsMenuViewModel + + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun LyricsMenu( + lyricsProvider: () -> LyricsEntity?, + mediaMetadataProvider: () -> MediaMetadata, + onDismiss: () -> Unit, + viewModel: LyricsMenuViewModel = hiltViewModel(), +) { + val context = LocalContext.current + val database = LocalDatabase.current + + var showEditDialog by rememberSaveable { + mutableStateOf(false) + } + + if (showEditDialog) { + TextFieldDialog( + onDismiss = { showEditDialog = false }, + icon = { Icon(painter = painterResource(R.drawable.edit), contentDescription = null) }, + title = { Text(text = mediaMetadataProvider().title) }, + initialTextFieldValue = TextFieldValue(lyricsProvider()?.lyrics.orEmpty()), + singleLine = false, + onDone = { + database.query { + upsert( + LyricsEntity( + id = mediaMetadataProvider().id, + lyrics = it + ) + ) + } + } + ) + } + + var showSearchDialog by rememberSaveable { + mutableStateOf(false) + } + var showSearchResultDialog by rememberSaveable { + mutableStateOf(false) + } + + val searchMediaMetadata = remember(showSearchDialog) { + mediaMetadataProvider() + } + val (titleField, onTitleFieldChange) = rememberSaveable(showSearchDialog, stateSaver = TextFieldValue.Saver) { + mutableStateOf( + TextFieldValue( + text = mediaMetadataProvider().title + ) + ) + } + val (artistField, onArtistFieldChange) = rememberSaveable(showSearchDialog, stateSaver = TextFieldValue.Saver) { + mutableStateOf( + TextFieldValue( + text = mediaMetadataProvider().artists.joinToString { it.name } + ) + ) + } + + if (showSearchDialog) { + DefaultDialog( + modifier = Modifier.verticalScroll(rememberScrollState()), + onDismiss = { showSearchDialog = false }, + icon = { Icon(painter = painterResource(R.drawable.search), contentDescription = null) }, + title = { Text(stringResource(R.string.search_lyrics)) }, + buttons = { + TextButton( + onClick = { showSearchDialog = false } + ) { + Text(stringResource(android.R.string.cancel)) + } + + Spacer(Modifier.width(8.dp)) + + TextButton( + onClick = { + showSearchDialog = false + onDismiss() + try { + context.startActivity( + Intent(Intent.ACTION_WEB_SEARCH).apply { + putExtra(SearchManager.QUERY, "${artistField.text} ${titleField.text} lyrics") + } + ) + } catch (_: Exception) { + } + } + ) { + Text(stringResource(R.string.search_online)) + } + + Spacer(Modifier.width(8.dp)) + + TextButton( + onClick = { + viewModel.search(searchMediaMetadata.id, titleField.text, artistField.text, searchMediaMetadata.duration) + showSearchResultDialog = true + } + ) { + Text(stringResource(android.R.string.ok)) + } + } + ) { + OutlinedTextField( + value = titleField, + onValueChange = onTitleFieldChange, + singleLine = true, + label = { Text(stringResource(R.string.song_title)) } + ) + + Spacer(Modifier.height(12.dp)) + + OutlinedTextField( + value = artistField, + onValueChange = onArtistFieldChange, + singleLine = true, + label = { Text(stringResource(R.string.song_artists)) } + ) + } + } + + if (showSearchResultDialog) { + val results by viewModel.results.collectAsState() + val isLoading by viewModel.isLoading.collectAsState() + + var expandedItemIndex by rememberSaveable { + mutableStateOf(-1) + } + + ListDialog( + onDismiss = { showSearchResultDialog = false } + ) { + itemsIndexed(results) { index, result -> + Row( + modifier = Modifier + .fillMaxWidth() + .clickable { + onDismiss() + viewModel.cancelSearch() + database.query { + upsert( + LyricsEntity( + id = searchMediaMetadata.id, + lyrics = result.lyrics + ) + ) + } + } + .padding(12.dp) + .animateContentSize() + ) { + Column( + modifier = Modifier.weight(1f) + ) { + Text( + text = result.lyrics, + style = MaterialTheme.typography.bodyMedium, + maxLines = if (index == expandedItemIndex) Int.MAX_VALUE else 2, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.padding(bottom = 4.dp) + ) + + Row( + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = result.providerName, + style = MaterialTheme.typography.labelMedium, + color = MaterialTheme.colorScheme.secondary, + maxLines = 1 + ) + if (result.lyrics.startsWith("[")) { + Icon( + painter = painterResource(R.drawable.sync), + contentDescription = null, + tint = MaterialTheme.colorScheme.secondary, + modifier = Modifier + .padding(start = 4.dp) + .size(18.dp) + ) + } + } + } + + IconButton( + onClick = { + expandedItemIndex = if (expandedItemIndex == index) -1 else index + } + ) { + Icon( + painter = painterResource(if (index == expandedItemIndex) R.drawable.expand_less else R.drawable.expand_more), + contentDescription = null + ) + } + } + } + + if (isLoading) { + item { + Box( + contentAlignment = Alignment.Center, + modifier = Modifier.fillMaxWidth() + ) { + CircularProgressIndicator() + } + } + } + + if (!isLoading && results.isEmpty()) { + item { + Text( + text = context.getString(R.string.lyrics_not_found), + textAlign = TextAlign.Center, + modifier = Modifier + .fillMaxWidth() + ) + } + } + } + } + + GridMenu( + contentPadding = PaddingValues( + start = 8.dp, + top = 8.dp, + end = 8.dp, + bottom = 8.dp + WindowInsets.systemBars.asPaddingValues().calculateBottomPadding() + ) + ) { + GridMenuItem( + icon = R.drawable.edit, + title = R.string.edit + ) { + showEditDialog = true + } + GridMenuItem( + icon = R.drawable.cached, + title = R.string.refetch + ) { + onDismiss() + viewModel.refetchLyrics(mediaMetadataProvider(), lyricsProvider()) + } + GridMenuItem( + icon = R.drawable.search, + title = R.string.search, + ) { + showSearchDialog = true + } + } +} diff --git a/app/src/main/java/com/zionhuang/music/ui/menu/PlayerMenu.kt b/app/src/main/java/com/zionhuang/music/ui/menu/PlayerMenu.kt index 057e75b66..963d2acd8 100644 --- a/app/src/main/java/com/zionhuang/music/ui/menu/PlayerMenu.kt +++ b/app/src/main/java/com/zionhuang/music/ui/menu/PlayerMenu.kt @@ -1,7 +1,6 @@ package com.zionhuang.music.ui.menu import android.content.Intent -import android.content.res.Configuration.ORIENTATION_LANDSCAPE import android.media.audiofx.AudioEffect import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts @@ -9,6 +8,7 @@ import androidx.annotation.DrawableRes import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.WindowInsets @@ -18,11 +18,14 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.systemBars +import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.items +import androidx.compose.material3.AlertDialog import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text +import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue @@ -32,14 +35,15 @@ import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource 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 androidx.compose.ui.unit.sp +import androidx.compose.ui.window.DialogProperties import androidx.core.net.toUri import androidx.media3.common.PlaybackParameters import androidx.media3.exoplayer.offline.DownloadRequest @@ -47,6 +51,7 @@ import androidx.media3.exoplayer.offline.DownloadService import androidx.navigation.NavController import com.zionhuang.music.LocalDatabase import com.zionhuang.music.LocalDownloadUtil +import com.zionhuang.music.LocalPlayerConnection import com.zionhuang.music.R import com.zionhuang.music.constants.ListItemHeight import com.zionhuang.music.db.entities.PlaylistSongMap @@ -75,7 +80,6 @@ fun PlayerMenu( mediaMetadata ?: return val context = LocalContext.current val database = LocalDatabase.current - val localConfiguration = LocalConfiguration.current val playerVolume = playerConnection.service.playerVolume.collectAsState() val activityResultLauncher = rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { } @@ -138,14 +142,14 @@ fun PlayerMenu( } } - var tempo by remember { - mutableStateOf(playerConnection.player.playbackParameters.speed) - } - var transposeValue by remember { - mutableStateOf(round(12 * log2(playerConnection.player.playbackParameters.pitch)).toInt()) + var showPitchTempoDialog by rememberSaveable { + mutableStateOf(false) } - val updatePlaybackParameters = { - playerConnection.player.playbackParameters = PlaybackParameters(tempo, 2f.pow(transposeValue.toFloat() / 12)) + + if (showPitchTempoDialog) { + PitchTempoDialog( + onDismiss = { showPitchTempoDialog = false } + ) } Row( @@ -169,65 +173,6 @@ fun PlayerMenu( ) } - if (localConfiguration.orientation == ORIENTATION_LANDSCAPE) { - Row( - horizontalArrangement = Arrangement.spacedBy(24.dp), - modifier = Modifier.padding(horizontal = 24.dp, vertical = 6.dp) - ) { - ValueAdjuster( - icon = R.drawable.slow_motion_video, - currentValue = tempo, - values = listOf(0.25f, 0.5f, 0.75f, 1f, 1.25f, 1.5f, 1.75f, 2f), - onValueUpdate = { - tempo = it - updatePlaybackParameters() - }, - valueText = { "x$it" }, - modifier = Modifier.weight(1f) - ) - - ValueAdjuster( - icon = R.drawable.tune, - currentValue = transposeValue, - values = (-12..12).toList(), - onValueUpdate = { - transposeValue = it - updatePlaybackParameters() - }, - valueText = { "${if (it > 0) "+" else ""}$it" }, - modifier = Modifier.weight(1f) - ) - } - } else { - ValueAdjuster( - icon = R.drawable.slow_motion_video, - currentValue = tempo, - values = listOf(0.25f, 0.5f, 0.75f, 1f, 1.25f, 1.5f, 1.75f, 2f), - onValueUpdate = { - tempo = it - updatePlaybackParameters() - }, - valueText = { "x$it" }, - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 24.dp, vertical = 6.dp) - ) - - ValueAdjuster( - icon = R.drawable.tune, - currentValue = transposeValue, - values = (-12..12).toList(), - onValueUpdate = { - transposeValue = it - updatePlaybackParameters() - }, - valueText = { "${if (it > 0) "+" else ""}$it" }, - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 24.dp, vertical = 6.dp) - ) - } - GridMenu( contentPadding = PaddingValues( start = 8.dp, @@ -330,9 +275,79 @@ fun PlayerMenu( } onDismiss() } + GridMenuItem( + icon = R.drawable.tune, + title = R.string.advanced + ) { + showPitchTempoDialog = true + } } } +@Composable +fun PitchTempoDialog( + onDismiss: () -> Unit, +) { + val playerConnection = LocalPlayerConnection.current ?: return + var tempo by remember { + mutableStateOf(playerConnection.player.playbackParameters.speed) + } + var transposeValue by remember { + mutableStateOf(round(12 * log2(playerConnection.player.playbackParameters.pitch)).toInt()) + } + val updatePlaybackParameters = { + playerConnection.player.playbackParameters = PlaybackParameters(tempo, 2f.pow(transposeValue.toFloat() / 12)) + } + + AlertDialog( + properties = DialogProperties(usePlatformDefaultWidth = false), + onDismissRequest = onDismiss, + dismissButton = { + TextButton( + onClick = { + tempo = 1f + transposeValue = 0 + updatePlaybackParameters() + } + ) { + Text(stringResource(R.string.reset)) + } + }, + confirmButton = { + TextButton( + onClick = onDismiss + ) { + Text(stringResource(android.R.string.ok)) + } + }, + text = { + Column { + ValueAdjuster( + icon = R.drawable.slow_motion_video, + currentValue = tempo, + values = listOf(0.25f, 0.5f, 0.75f, 1f, 1.25f, 1.5f, 1.75f, 2f), + onValueUpdate = { + tempo = it + updatePlaybackParameters() + }, + valueText = { "x$it" }, + modifier = Modifier.padding(bottom = 12.dp) + ) + ValueAdjuster( + icon = R.drawable.discover_tune, + currentValue = transposeValue, + values = (-12..12).toList(), + onValueUpdate = { + transposeValue = it + updatePlaybackParameters() + }, + valueText = { "${if (it > 0) "+" else ""}$it" } + ) + } + } + ) +} + @Composable fun <T> ValueAdjuster( @DrawableRes icon: Int, @@ -340,7 +355,7 @@ fun <T> ValueAdjuster( values: List<T>, onValueUpdate: (T) -> Unit, valueText: (T) -> String, - modifier: Modifier, + modifier: Modifier = Modifier, ) { Row( horizontalArrangement = Arrangement.spacedBy(24.dp), @@ -369,7 +384,7 @@ fun <T> ValueAdjuster( text = valueText(currentValue), style = MaterialTheme.typography.titleMedium, textAlign = TextAlign.Center, - modifier = Modifier.weight(1f) + modifier = Modifier.width(80.dp) ) IconButton( diff --git a/app/src/main/java/com/zionhuang/music/ui/menu/SongMenu.kt b/app/src/main/java/com/zionhuang/music/ui/menu/SongMenu.kt index af363a5a4..e4d94cc90 100644 --- a/app/src/main/java/com/zionhuang/music/ui/menu/SongMenu.kt +++ b/app/src/main/java/com/zionhuang/music/ui/menu/SongMenu.kt @@ -16,6 +16,7 @@ import androidx.compose.foundation.shape.CircleShape import androidx.compose.material3.Divider import androidx.compose.material3.Icon import androidx.compose.material3.IconButton +import androidx.compose.material3.LocalContentColor import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -178,7 +179,7 @@ fun SongMenu( ) { Icon( painter = painterResource(if (song.song.liked) R.drawable.favorite else R.drawable.favorite_border), - tint = MaterialTheme.colorScheme.error, + tint = if (song.song.liked) MaterialTheme.colorScheme.error else LocalContentColor.current, contentDescription = null ) } diff --git a/app/src/main/java/com/zionhuang/music/ui/menu/YouTubeAlbumMenu.kt b/app/src/main/java/com/zionhuang/music/ui/menu/YouTubeAlbumMenu.kt index 3fd63d30f..711b718df 100644 --- a/app/src/main/java/com/zionhuang/music/ui/menu/YouTubeAlbumMenu.kt +++ b/app/src/main/java/com/zionhuang/music/ui/menu/YouTubeAlbumMenu.kt @@ -11,6 +11,11 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.systemBars import androidx.compose.foundation.lazy.items +import androidx.compose.material3.Divider +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.LocalContentColor +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -23,6 +28,7 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp @@ -34,14 +40,12 @@ import androidx.media3.exoplayer.offline.DownloadService import androidx.navigation.NavController import com.zionhuang.innertube.YouTube import com.zionhuang.innertube.models.AlbumItem -import com.zionhuang.innertube.pages.AlbumPage import com.zionhuang.music.LocalDatabase import com.zionhuang.music.LocalDownloadUtil import com.zionhuang.music.R import com.zionhuang.music.constants.ListItemHeight import com.zionhuang.music.db.entities.PlaylistSongMap import com.zionhuang.music.extensions.toMediaItem -import com.zionhuang.music.models.toMediaMetadata import com.zionhuang.music.playback.ExoDownloadService import com.zionhuang.music.playback.PlayerConnection import com.zionhuang.music.playback.queues.YouTubeAlbumRadio @@ -49,10 +53,12 @@ import com.zionhuang.music.ui.component.DownloadGridMenu import com.zionhuang.music.ui.component.GridMenu import com.zionhuang.music.ui.component.GridMenuItem import com.zionhuang.music.ui.component.ListDialog +import com.zionhuang.music.ui.component.YouTubeListItem +import com.zionhuang.music.utils.reportException @Composable fun YouTubeAlbumMenu( - album: AlbumItem, + albumItem: AlbumItem, navController: NavController, playerConnection: PlayerConnection, onDismiss: () -> Unit, @@ -60,14 +66,19 @@ fun YouTubeAlbumMenu( val context = LocalContext.current val database = LocalDatabase.current val downloadUtil = LocalDownloadUtil.current - val libraryAlbum by database.album(album.id).collectAsState(initial = null) - var albumPage: AlbumPage? by remember { - mutableStateOf(null) - } + val album by database.albumWithSongs(albumItem.id).collectAsState(initial = null) LaunchedEffect(Unit) { - YouTube.album(album.browseId).onSuccess { - albumPage = it + database.album(albumItem.id).collect { album -> + if (album == null) { + YouTube.album(albumItem.id).onSuccess { albumPage -> + database.transaction { + insert(albumPage) + } + }.onFailure { + reportException(it) + } + } } } @@ -75,8 +86,8 @@ fun YouTubeAlbumMenu( mutableStateOf(Download.STATE_STOPPED) } - LaunchedEffect(albumPage) { - val songs = albumPage?.songs?.map { it.id } ?: return@LaunchedEffect + LaunchedEffect(album) { + val songs = album?.songs?.map { it.id } ?: return@LaunchedEffect downloadUtil.downloads.collect { downloads -> downloadState = if (songs.all { downloads[it]?.state == Download.STATE_COMPLETED }) @@ -101,19 +112,14 @@ fun YouTubeAlbumMenu( onAdd = { playlist -> var position = playlist.songCount database.transaction { - albumPage?.let { albumPage -> - albumPage.songs - .map { it.toMediaMetadata() } - .onEach(::insert) - .forEach { song -> - insert( - PlaylistSongMap( - songId = song.id, - playlistId = playlist.id, - position = position++ - ) - ) - } + album?.songs?.forEach { song -> + insert( + PlaylistSongMap( + songId = song.id, + playlistId = playlist.id, + position = position++ + ) + ) } } }, @@ -129,8 +135,8 @@ fun YouTubeAlbumMenu( onDismiss = { showSelectArtistDialog = false } ) { items( - items = album.artists.orEmpty(), - key = { it.id!! } + items = album?.artists.orEmpty(), + key = { it.id } ) { artist -> Row( verticalAlignment = Alignment.CenterVertically, @@ -168,6 +174,28 @@ fun YouTubeAlbumMenu( } } + YouTubeListItem( + item = albumItem, + badges = {}, + trailingContent = { + IconButton( + onClick = { + database.query { + album?.album?.toggleLike()?.let(::update) + } + } + ) { + Icon( + painter = painterResource(if (album?.album?.bookmarkedAt != null) R.drawable.favorite else R.drawable.favorite_border), + tint = if (album?.album?.bookmarkedAt != null) MaterialTheme.colorScheme.error else LocalContentColor.current, + contentDescription = null + ) + } + } + ) + + Divider() + GridMenu( contentPadding = PaddingValues( start = 8.dp, @@ -180,14 +208,14 @@ fun YouTubeAlbumMenu( icon = R.drawable.radio, title = R.string.start_radio ) { - playerConnection.playQueue(YouTubeAlbumRadio(album.playlistId)) + playerConnection.playQueue(YouTubeAlbumRadio(albumItem.playlistId)) onDismiss() } GridMenuItem( icon = R.drawable.playlist_play, title = R.string.play_next ) { - albumPage?.songs + album?.songs ?.map { it.toMediaItem() } ?.let(playerConnection::playNext) onDismiss() @@ -196,31 +224,11 @@ fun YouTubeAlbumMenu( icon = R.drawable.queue_music, title = R.string.add_to_queue ) { - albumPage?.songs + album?.songs ?.map { it.toMediaItem() } ?.let(playerConnection::addToQueue) onDismiss() } - if (libraryAlbum != null) { - GridMenuItem( - icon = R.drawable.library_add_check, - title = R.string.remove_from_library - ) { - database.query { - libraryAlbum?.album?.let(::delete) - } - } - } else { - GridMenuItem( - icon = R.drawable.library_add, - title = R.string.add_to_library - ) { - database.transaction { - albumPage?.let(::insert) - } - } - } - GridMenuItem( icon = R.drawable.playlist_add, title = R.string.add_to_playlist @@ -230,10 +238,10 @@ fun YouTubeAlbumMenu( DownloadGridMenu( state = downloadState, onDownload = { - albumPage?.songs?.forEach { song -> + album?.songs?.forEach { song -> val downloadRequest = DownloadRequest.Builder(song.id, song.id.toUri()) .setCustomCacheKey(song.id) - .setData(song.title.toByteArray()) + .setData(song.song.title.toByteArray()) .build() DownloadService.sendAddDownload( context, @@ -244,7 +252,7 @@ fun YouTubeAlbumMenu( } }, onRemoveDownload = { - albumPage?.songs?.forEach { song -> + album?.songs?.forEach { song -> DownloadService.sendRemoveDownload( context, ExoDownloadService::class.java, @@ -254,7 +262,7 @@ fun YouTubeAlbumMenu( } } ) - album.artists?.let { artists -> + albumItem.artists?.let { artists -> GridMenuItem( icon = R.drawable.artist, title = R.string.view_artist @@ -274,7 +282,7 @@ fun YouTubeAlbumMenu( val intent = Intent().apply { action = Intent.ACTION_SEND type = "text/plain" - putExtra(Intent.EXTRA_TEXT, album.shareLink) + putExtra(Intent.EXTRA_TEXT, albumItem.shareLink) } context.startActivity(Intent.createChooser(intent, null)) onDismiss() diff --git a/app/src/main/java/com/zionhuang/music/ui/menu/YouTubePlaylistMenu.kt b/app/src/main/java/com/zionhuang/music/ui/menu/YouTubePlaylistMenu.kt index 705a86b10..cee8308e3 100644 --- a/app/src/main/java/com/zionhuang/music/ui/menu/YouTubePlaylistMenu.kt +++ b/app/src/main/java/com/zionhuang/music/ui/menu/YouTubePlaylistMenu.kt @@ -125,7 +125,7 @@ fun YouTubePlaylistMenu( icon = R.drawable.queue_music, title = R.string.add_to_queue ) { - coroutineScope.launch(Dispatchers.IO) { + coroutineScope.launch { songs.ifEmpty { withContext(Dispatchers.IO) { YouTube.playlist(playlist.id).completed().getOrNull()?.songs.orEmpty() diff --git a/app/src/main/java/com/zionhuang/music/ui/menu/YouTubeSongMenu.kt b/app/src/main/java/com/zionhuang/music/ui/menu/YouTubeSongMenu.kt index 655b932a0..fb9c4a0a4 100644 --- a/app/src/main/java/com/zionhuang/music/ui/menu/YouTubeSongMenu.kt +++ b/app/src/main/java/com/zionhuang/music/ui/menu/YouTubeSongMenu.kt @@ -16,6 +16,7 @@ import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Divider import androidx.compose.material3.Icon import androidx.compose.material3.IconButton +import androidx.compose.material3.LocalContentColor import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -180,7 +181,7 @@ fun YouTubeSongMenu( ) { Icon( painter = painterResource(if (librarySong?.song?.liked == true) R.drawable.favorite else R.drawable.favorite_border), - tint = MaterialTheme.colorScheme.error, + tint = if (librarySong?.song?.liked == true) MaterialTheme.colorScheme.error else LocalContentColor.current, contentDescription = null ) } diff --git a/app/src/main/java/com/zionhuang/music/ui/player/MiniPlayer.kt b/app/src/main/java/com/zionhuang/music/ui/player/MiniPlayer.kt index badff73af..6e6d9fe3f 100644 --- a/app/src/main/java/com/zionhuang/music/ui/player/MiniPlayer.kt +++ b/app/src/main/java/com/zionhuang/music/ui/player/MiniPlayer.kt @@ -2,9 +2,7 @@ package com.zionhuang.music.ui.player import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut -import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background -import androidx.compose.foundation.gestures.snapping.rememberSnapFlingBehavior import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -18,7 +16,6 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.systemBars import androidx.compose.foundation.layout.windowInsetsPadding -import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Icon import androidx.compose.material3.IconButton @@ -26,11 +23,8 @@ import androidx.compose.material3.LinearProgressIndicator import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember -import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -47,15 +41,9 @@ import com.zionhuang.music.LocalPlayerConnection import com.zionhuang.music.R import com.zionhuang.music.constants.MiniPlayerHeight import com.zionhuang.music.constants.ThumbnailCornerRadius -import com.zionhuang.music.extensions.metadata import com.zionhuang.music.extensions.togglePlayPause import com.zionhuang.music.models.MediaMetadata -import com.zionhuang.music.ui.utils.HorizontalPager -import com.zionhuang.music.ui.utils.SnapLayoutInfoProvider -import kotlinx.coroutines.flow.drop -import kotlin.math.abs -@OptIn(ExperimentalFoundationApi::class) @Composable fun MiniPlayer( position: Long, @@ -66,40 +54,8 @@ fun MiniPlayer( val isPlaying by playerConnection.isPlaying.collectAsState() val playbackState by playerConnection.playbackState.collectAsState() val error by playerConnection.error.collectAsState() - val windows by playerConnection.queueWindows.collectAsState() - val currentWindowIndex by playerConnection.currentWindowIndex.collectAsState() - - val pagerState = rememberPagerState( - initialPage = currentWindowIndex.takeIf { it != -1 } ?: 0 - ) - - val snapLayoutInfoProvider = remember(pagerState) { - SnapLayoutInfoProvider( - pagerState = pagerState, - positionInLayout = { _, _ -> 0f } - ) - } - - LaunchedEffect(pagerState, currentWindowIndex) { - if (windows.isNotEmpty()) { - try { - if (abs(pagerState.currentPage - currentWindowIndex) <= 1) { - pagerState.animateScrollToPage(currentWindowIndex) - } else { - pagerState.scrollToPage(currentWindowIndex) - } - } catch (_: Exception) { - } - } - } - - LaunchedEffect(pagerState) { - snapshotFlow { pagerState.settledPage }.drop(1).collect { index -> - if (!pagerState.isScrollInProgress && index != currentWindowIndex && windows.isNotEmpty()) { - playerConnection.player.seekToDefaultPosition(windows[index].firstPeriodIndex) - } - } - } + val mediaMetadata by playerConnection.mediaMetadata.collectAsState() + val canSkipNext by playerConnection.canSkipNext.collectAsState() Box( modifier = modifier @@ -118,17 +74,10 @@ fun MiniPlayer( verticalAlignment = Alignment.CenterVertically, modifier = modifier .fillMaxSize() - .padding(end = 12.dp), + .padding(end = 6.dp), ) { - HorizontalPager( - state = pagerState, - flingBehavior = rememberSnapFlingBehavior(snapLayoutInfoProvider), - items = windows, - key = { it.uid.hashCode() }, - beyondBoundsPageCount = 2, - modifier = Modifier.weight(1f) - ) { window -> - window.mediaItem.metadata?.let { + Box(Modifier.weight(1f)) { + mediaMetadata?.let { MiniMediaInfo( mediaMetadata = it, error = error, @@ -152,6 +101,16 @@ fun MiniPlayer( contentDescription = null ) } + + IconButton( + enabled = canSkipNext, + onClick = playerConnection.player::seekToNext + ) { + Icon( + painter = painterResource(R.drawable.skip_next), + contentDescription = null + ) + } } } } diff --git a/app/src/main/java/com/zionhuang/music/ui/player/Player.kt b/app/src/main/java/com/zionhuang/music/ui/player/Player.kt index f5766cbfb..934411645 100644 --- a/app/src/main/java/com/zionhuang/music/ui/player/Player.kt +++ b/app/src/main/java/com/zionhuang/music/ui/player/Player.kt @@ -22,6 +22,7 @@ import androidx.compose.foundation.layout.systemBars import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.LocalContentColor import androidx.compose.material3.MaterialTheme import androidx.compose.material3.NavigationBarDefaults import androidx.compose.material3.Slider @@ -215,7 +216,7 @@ fun BottomSheetPlayer( Box(modifier = Modifier.weight(1f)) { ResizableIconButton( icon = if (currentSong?.song?.liked == true) R.drawable.favorite else R.drawable.favorite_border, - color = MaterialTheme.colorScheme.error, + color = if (currentSong?.song?.liked == true) MaterialTheme.colorScheme.error else LocalContentColor.current, modifier = Modifier .size(32.dp) .padding(4.dp) diff --git a/app/src/main/java/com/zionhuang/music/ui/player/Thumbnail.kt b/app/src/main/java/com/zionhuang/music/ui/player/Thumbnail.kt index 8950e644a..ad549194b 100644 --- a/app/src/main/java/com/zionhuang/music/ui/player/Thumbnail.kt +++ b/app/src/main/java/com/zionhuang/music/ui/player/Thumbnail.kt @@ -3,13 +3,17 @@ package com.zionhuang.music.ui.player import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut -import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.gestures.detectTapGestures -import androidx.compose.foundation.gestures.snapping.rememberSnapFlingBehavior -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.pager.rememberPagerState +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.statusBarsPadding import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.runtime.* +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -20,15 +24,9 @@ import coil.compose.AsyncImage import com.zionhuang.music.LocalPlayerConnection import com.zionhuang.music.constants.ShowLyricsKey import com.zionhuang.music.constants.ThumbnailCornerRadius -import com.zionhuang.music.extensions.metadata import com.zionhuang.music.ui.component.Lyrics -import com.zionhuang.music.ui.utils.HorizontalPager -import com.zionhuang.music.ui.utils.SnapLayoutInfoProvider import com.zionhuang.music.utils.rememberPreference -import kotlinx.coroutines.flow.drop -import kotlin.math.abs -@OptIn(ExperimentalFoundationApi::class) @Composable fun Thumbnail( sliderPositionProvider: () -> Long?, @@ -37,50 +35,11 @@ fun Thumbnail( val playerConnection = LocalPlayerConnection.current ?: return val currentView = LocalView.current - val windows by playerConnection.queueWindows.collectAsState() - val currentWindowIndex by playerConnection.currentWindowIndex.collectAsState() + val mediaMetadata by playerConnection.mediaMetadata.collectAsState() val error by playerConnection.error.collectAsState() val showLyrics by rememberPreference(ShowLyricsKey, false) - val pagerState = rememberPagerState( - initialPage = currentWindowIndex.takeIf { it != -1 } ?: 0 - ) - - val snapLayoutInfoProvider = remember(pagerState) { - SnapLayoutInfoProvider( - pagerState = pagerState, - positionInLayout = { _, _ -> 0f } - ) - } - - LaunchedEffect(pagerState, currentWindowIndex) { - if (windows.isNotEmpty()) { - try { - if (abs(pagerState.currentPage - currentWindowIndex) <= 1) { - pagerState.animateScrollToPage(currentWindowIndex) - } else { - pagerState.scrollToPage(currentWindowIndex) - } - } catch (_: Exception) { - } - } - } - - LaunchedEffect(pagerState) { - snapshotFlow { pagerState.settledPage }.drop(1).collect { index -> - if (!pagerState.isScrollInProgress && index != currentWindowIndex && windows.isNotEmpty()) { - playerConnection.player.seekToDefaultPosition(windows[index].firstPeriodIndex) - } - } - } - - LaunchedEffect(showLyrics) { - if (!showLyrics) { - pagerState.scrollToPage(currentWindowIndex) - } - } - DisposableEffect(showLyrics) { currentView.keepScreenOn = showLyrics onDispose { @@ -97,35 +56,30 @@ fun Thumbnail( .fillMaxSize() .statusBarsPadding() ) { - HorizontalPager( - state = pagerState, - flingBehavior = rememberSnapFlingBehavior(snapLayoutInfoProvider), - items = windows, - key = { it.uid.hashCode() }, - beyondBoundsPageCount = 2 - ) { window -> - Box(Modifier.fillMaxSize()) { - AsyncImage( - model = window.mediaItem.metadata?.thumbnailUrl, - contentDescription = null, - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp) - .clip(RoundedCornerShape(ThumbnailCornerRadius)) - .align(Alignment.Center) - .pointerInput(Unit) { - detectTapGestures( - onDoubleTap = { offset -> - if (offset.x < size.width / 2) { - playerConnection.player.seekBack() - } else { - playerConnection.player.seekForward() - } + Box( + contentAlignment = Alignment.Center, + modifier = Modifier + .fillMaxSize() + .padding(horizontal = 16.dp) + ) { + AsyncImage( + model = mediaMetadata?.thumbnailUrl, + contentDescription = null, + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(ThumbnailCornerRadius)) + .pointerInput(Unit) { + detectTapGestures( + onDoubleTap = { offset -> + if (offset.x < size.width / 2) { + playerConnection.player.seekBack() + } else { + playerConnection.player.seekForward() } - ) - } - ) - } + } + ) + } + ) } } diff --git a/app/src/main/java/com/zionhuang/music/ui/screens/AlbumScreen.kt b/app/src/main/java/com/zionhuang/music/ui/screens/AlbumScreen.kt index 287f05e9e..060769738 100644 --- a/app/src/main/java/com/zionhuang/music/ui/screens/AlbumScreen.kt +++ b/app/src/main/java/com/zionhuang/music/ui/screens/AlbumScreen.kt @@ -23,6 +23,7 @@ import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton +import androidx.compose.material3.LocalContentColor import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedButton import androidx.compose.material3.Text @@ -55,18 +56,14 @@ import androidx.media3.exoplayer.offline.DownloadRequest import androidx.media3.exoplayer.offline.DownloadService import androidx.navigation.NavController import coil.compose.AsyncImage -import com.zionhuang.innertube.models.SongItem -import com.zionhuang.innertube.pages.AlbumPage import com.zionhuang.music.LocalDatabase import com.zionhuang.music.LocalDownloadUtil import com.zionhuang.music.LocalPlayerAwareWindowInsets import com.zionhuang.music.LocalPlayerConnection import com.zionhuang.music.R import com.zionhuang.music.constants.AlbumThumbnailSize -import com.zionhuang.music.constants.CONTENT_TYPE_SONG import com.zionhuang.music.constants.ThumbnailCornerRadius import com.zionhuang.music.db.entities.Album -import com.zionhuang.music.db.entities.AlbumWithSongs import com.zionhuang.music.db.entities.Song import com.zionhuang.music.extensions.toMediaItem import com.zionhuang.music.extensions.togglePlayPause @@ -76,20 +73,13 @@ import com.zionhuang.music.ui.component.AutoResizeText import com.zionhuang.music.ui.component.FontSizeRange import com.zionhuang.music.ui.component.LocalMenuState import com.zionhuang.music.ui.component.SongListItem -import com.zionhuang.music.ui.component.YouTubeListItem import com.zionhuang.music.ui.component.shimmer.ButtonPlaceholder import com.zionhuang.music.ui.component.shimmer.ListItemPlaceHolder import com.zionhuang.music.ui.component.shimmer.ShimmerHost import com.zionhuang.music.ui.component.shimmer.TextPlaceholder import com.zionhuang.music.ui.menu.AlbumMenu import com.zionhuang.music.ui.menu.SongMenu -import com.zionhuang.music.ui.menu.YouTubeAlbumMenu -import com.zionhuang.music.ui.menu.YouTubeSongMenu import com.zionhuang.music.viewmodels.AlbumViewModel -import com.zionhuang.music.viewmodels.AlbumViewState -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.runBlocking @OptIn(ExperimentalFoundationApi::class, ExperimentalMaterial3Api::class) @Composable @@ -100,24 +90,20 @@ fun AlbumScreen( ) { val context = LocalContext.current val menuState = LocalMenuState.current + val database = LocalDatabase.current val playerConnection = LocalPlayerConnection.current ?: return val isPlaying by playerConnection.isPlaying.collectAsState() val mediaMetadata by playerConnection.mediaMetadata.collectAsState() - val viewState by viewModel.viewState.collectAsState() - val inLibrary by viewModel.inLibrary.collectAsState() + val albumWithSongs by viewModel.albumWithSongs.collectAsState() val downloadUtil = LocalDownloadUtil.current var downloadState by remember { mutableStateOf(Download.STATE_STOPPED) } - LaunchedEffect(viewState) { - val songs = when (val state = viewState) { - is AlbumViewState.Local -> state.albumWithSongs.songs.map { it.id } - is AlbumViewState.Remote -> state.albumPage.songs.map { it.id } - else -> return@LaunchedEffect - } + LaunchedEffect(albumWithSongs) { + val songs = albumWithSongs?.songs?.map { it.id } ?: return@LaunchedEffect downloadUtil.downloads.collect { downloads -> downloadState = if (songs.all { downloads[it]?.state == Download.STATE_COMPLETED }) @@ -136,140 +122,153 @@ fun AlbumScreen( LazyColumn( contentPadding = LocalPlayerAwareWindowInsets.current.asPaddingValues() ) { - viewState.let { viewState -> - when (viewState) { - is AlbumViewState.Local -> { - item { - LocalAlbumHeader( - albumWithSongs = viewState.albumWithSongs, - inLibrary = inLibrary, - downloadState = downloadState, - onDownload = { - viewState.albumWithSongs.songs.forEach { song -> - val downloadRequest = DownloadRequest.Builder(song.id, song.id.toUri()) - .setCustomCacheKey(song.id) - .setData(song.song.title.toByteArray()) - .build() - DownloadService.sendAddDownload( - context, - ExoDownloadService::class.java, - downloadRequest, - false - ) + val albumWithSongs = albumWithSongs + if (albumWithSongs != null) { + item { + Column( + modifier = Modifier.padding(12.dp) + ) { + Row( + verticalAlignment = Alignment.CenterVertically + ) { + AsyncImage( + model = albumWithSongs.album.thumbnailUrl, + contentDescription = null, + modifier = Modifier + .size(AlbumThumbnailSize) + .clip(RoundedCornerShape(ThumbnailCornerRadius)) + ) + + Spacer(Modifier.width(16.dp)) + + Column( + verticalArrangement = Arrangement.Center, + ) { + AutoResizeText( + text = albumWithSongs.album.title, + fontWeight = FontWeight.Bold, + maxLines = 2, + overflow = TextOverflow.Ellipsis, + fontSizeRange = FontSizeRange(16.sp, 22.sp) + ) + + val annotatedString = buildAnnotatedString { + withStyle( + style = MaterialTheme.typography.titleMedium.copy( + fontWeight = FontWeight.Normal, + color = MaterialTheme.colorScheme.onBackground + ).toSpanStyle() + ) { + albumWithSongs.artists.fastForEachIndexed { index, artist -> + pushStringAnnotation(artist.id, artist.name) + append(artist.name) + pop() + if (index != albumWithSongs.artists.lastIndex) { + append(", ") + } + } } - }, - onRemoveDownload = { - viewState.albumWithSongs.songs.forEach { song -> - DownloadService.sendRemoveDownload( - context, - ExoDownloadService::class.java, - song.id, - false - ) + } + ClickableText(annotatedString) { offset -> + annotatedString.getStringAnnotations(offset, offset).firstOrNull()?.let { range -> + navController.navigate("artist/${range.tag}") } - }, - navController = navController - ) - } + } - itemsIndexed( - items = viewState.albumWithSongs.songs, - key = { _, song -> song.id } - ) { index, song -> - SongListItem( - song = song, - albumIndex = index + 1, - isActive = song.id == mediaMetadata?.id, - isPlaying = isPlaying, - showInLibraryIcon = true, - trailingContent = { + if (albumWithSongs.album.year != null) { + Text( + text = albumWithSongs.album.year.toString(), + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.Normal + ) + } + + Row { IconButton( onClick = { - menuState.show { - SongMenu( - originalSong = song, - navController = navController, - playerConnection = playerConnection, - onDismiss = menuState::dismiss - ) + database.query { + update(albumWithSongs.album.toggleLike()) } } ) { Icon( - painter = painterResource(R.drawable.more_vert), - contentDescription = null + painter = painterResource(if (albumWithSongs.album.bookmarkedAt != null) R.drawable.favorite else R.drawable.favorite_border), + contentDescription = null, + tint = if (albumWithSongs.album.bookmarkedAt != null) MaterialTheme.colorScheme.error else LocalContentColor.current ) } - }, - modifier = Modifier - .fillMaxWidth() - .combinedClickable { - if (song.id == mediaMetadata?.id) { - playerConnection.player.togglePlayPause() - } else { - playerConnection.playQueue( - ListQueue( - title = viewState.albumWithSongs.album.title, - items = viewState.albumWithSongs.songs.map { it.toMediaItem() }, - startIndex = index + + when (downloadState) { + Download.STATE_COMPLETED -> { + IconButton( + onClick = { + albumWithSongs.songs.forEach { song -> + DownloadService.sendRemoveDownload( + context, + ExoDownloadService::class.java, + song.id, + false + ) + } + } + ) { + Icon( + painter = painterResource(R.drawable.offline), + contentDescription = null ) - ) + } } - } - ) - } - } - is AlbumViewState.Remote -> { - item { - RemoteAlbumHeader( - albumPage = viewState.albumPage, - inLibrary = inLibrary, - downloadState = downloadState, - onDownload = { - viewState.albumPage.songs.forEach { song -> - val downloadRequest = DownloadRequest.Builder(song.id, song.id.toUri()) - .setCustomCacheKey(song.id) - .setData(song.title.toByteArray()) - .build() - DownloadService.sendAddDownload( - context, - ExoDownloadService::class.java, - downloadRequest, - false - ) - } - }, - onRemoveDownload = { - viewState.albumPage.songs.forEach { song -> - DownloadService.sendRemoveDownload( - context, - ExoDownloadService::class.java, - song.id, - false - ) + Download.STATE_DOWNLOADING -> { + IconButton( + onClick = { + albumWithSongs.songs.forEach { song -> + DownloadService.sendRemoveDownload( + context, + ExoDownloadService::class.java, + song.id, + false + ) + } + } + ) { + CircularProgressIndicator( + strokeWidth = 2.dp, + modifier = Modifier.size(24.dp) + ) + } + } + + else -> { + IconButton( + onClick = { + albumWithSongs.songs.forEach { song -> + val downloadRequest = DownloadRequest.Builder(song.id, song.id.toUri()) + .setCustomCacheKey(song.id) + .setData(song.song.title.toByteArray()) + .build() + DownloadService.sendAddDownload( + context, + ExoDownloadService::class.java, + downloadRequest, + false + ) + } + } + ) { + Icon( + painter = painterResource(R.drawable.download), + contentDescription = null + ) + } + } } - }, - navController = navController - ) - } - itemsIndexed( - items = viewState.albumPage.songs, - key = { _, song -> song.id }, - contentType = { _, _ -> CONTENT_TYPE_SONG } - ) { index, song -> - YouTubeListItem( - item = song, - albumIndex = index + 1, - isActive = song.id == mediaMetadata?.id, - isPlaying = isPlaying, - trailingContent = { IconButton( onClick = { menuState.show { - YouTubeSongMenu( - song = song, + AlbumMenu( + originalAlbum = Album(albumWithSongs.album, albumWithSongs.artists), navController = navController, playerConnection = playerConnection, onDismiss = menuState::dismiss @@ -282,467 +281,158 @@ fun AlbumScreen( contentDescription = null ) } - }, - modifier = Modifier - .fillMaxWidth() - .combinedClickable { - if (song.id == mediaMetadata?.id) { - playerConnection.player.togglePlayPause() - } else { - playerConnection.playQueue( - ListQueue( - title = viewState.albumPage.album.title, - items = viewState.albumPage.songs.map { it.toMediaItem() }, - startIndex = index - ) - ) - } - } - ) - } - } - - null -> { - item { - ShimmerHost { - Column(Modifier.padding(12.dp)) { - Row(verticalAlignment = Alignment.CenterVertically) { - Spacer( - modifier = Modifier - .size(AlbumThumbnailSize) - .clip(RoundedCornerShape(ThumbnailCornerRadius)) - .background(MaterialTheme.colorScheme.onSurface) - ) - - Spacer(Modifier.width(16.dp)) - - Column( - verticalArrangement = Arrangement.Center, - ) { - TextPlaceholder() - TextPlaceholder() - TextPlaceholder() - } - } - - Spacer(Modifier.padding(8.dp)) - - Row { - ButtonPlaceholder(Modifier.weight(1f)) - - Spacer(Modifier.width(12.dp)) - - ButtonPlaceholder(Modifier.weight(1f)) - } - } - - repeat(6) { - ListItemPlaceHolder() } } } - } - } - } - } - TopAppBar( - title = { }, - navigationIcon = { - IconButton(onClick = navController::navigateUp) { - Icon( - painterResource(R.drawable.arrow_back), - contentDescription = null - ) - } - }, - scrollBehavior = scrollBehavior - ) -} - -@Composable -fun LocalAlbumHeader( - albumWithSongs: AlbumWithSongs, - inLibrary: Boolean, - downloadState: Int, - onDownload: () -> Unit, - onRemoveDownload: () -> Unit, - navController: NavController, -) { - val playerConnection = LocalPlayerConnection.current ?: return - val database = LocalDatabase.current - val menuState = LocalMenuState.current - - Column( - modifier = Modifier.padding(12.dp) - ) { - Row( - verticalAlignment = Alignment.CenterVertically - ) { - AsyncImage( - model = albumWithSongs.album.thumbnailUrl, - contentDescription = null, - modifier = Modifier - .size(AlbumThumbnailSize) - .clip(RoundedCornerShape(ThumbnailCornerRadius)) - ) - - Spacer(Modifier.width(16.dp)) - - Column( - verticalArrangement = Arrangement.Center, - ) { - AutoResizeText( - text = albumWithSongs.album.title, - fontWeight = FontWeight.Bold, - maxLines = 2, - overflow = TextOverflow.Ellipsis, - fontSizeRange = FontSizeRange(16.sp, 22.sp) - ) - - val annotatedString = buildAnnotatedString { - withStyle( - style = MaterialTheme.typography.titleMedium.copy( - fontWeight = FontWeight.Normal, - color = MaterialTheme.colorScheme.onBackground - ).toSpanStyle() - ) { - albumWithSongs.artists.fastForEachIndexed { index, artist -> - pushStringAnnotation(artist.id, artist.name) - append(artist.name) - pop() - if (index != albumWithSongs.artists.lastIndex) { - append(", ") - } - } - } - } - ClickableText(annotatedString) { offset -> - annotatedString.getStringAnnotations(offset, offset).firstOrNull()?.let { range -> - navController.navigate("artist/${range.tag}") - } - } + Spacer(Modifier.height(12.dp)) - if (albumWithSongs.album.year != null) { - Text( - text = albumWithSongs.album.year.toString(), - style = MaterialTheme.typography.titleMedium, - fontWeight = FontWeight.Normal - ) - } - - Row { - IconButton( - onClick = { - database.query { - if (inLibrary) { - delete(albumWithSongs.album) - } else { - insert(albumWithSongs) - } - } - } - ) { - Icon( - painter = painterResource(if (inLibrary) R.drawable.library_add_check else R.drawable.library_add), - contentDescription = null - ) - } - - when (downloadState) { - Download.STATE_COMPLETED -> { - IconButton(onClick = onRemoveDownload) { - Icon( - painter = painterResource(R.drawable.offline), - contentDescription = null + Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) { + Button( + onClick = { + playerConnection.playQueue( + ListQueue( + title = albumWithSongs.album.title, + items = albumWithSongs.songs.map(Song::toMediaItem) + ) ) - } + }, + contentPadding = ButtonDefaults.ButtonWithIconContentPadding, + modifier = Modifier.weight(1f) + ) { + Icon( + painter = painterResource(R.drawable.play), + contentDescription = null, + modifier = Modifier.size(ButtonDefaults.IconSize) + ) + Spacer(Modifier.size(ButtonDefaults.IconSpacing)) + Text( + text = stringResource(R.string.play) + ) } - Download.STATE_DOWNLOADING -> { - IconButton(onClick = onRemoveDownload) { - CircularProgressIndicator( - strokeWidth = 2.dp, - modifier = Modifier.size(24.dp) + OutlinedButton( + onClick = { + playerConnection.playQueue( + ListQueue( + title = albumWithSongs.album.title, + items = albumWithSongs.songs.shuffled().map(Song::toMediaItem) + ) ) - } + }, + contentPadding = ButtonDefaults.ButtonWithIconContentPadding, + modifier = Modifier.weight(1f) + ) { + Icon( + painter = painterResource(R.drawable.shuffle), + contentDescription = null, + modifier = Modifier.size(ButtonDefaults.IconSize) + ) + Spacer(Modifier.size(ButtonDefaults.IconSpacing)) + Text(stringResource(R.string.shuffle)) } + } + } + } - else -> { - IconButton(onClick = onDownload) { - Icon( - painter = painterResource(R.drawable.download), - contentDescription = null - ) + itemsIndexed( + items = albumWithSongs.songs, + key = { _, song -> song.id } + ) { index, song -> + SongListItem( + song = song, + albumIndex = index + 1, + isActive = song.id == mediaMetadata?.id, + isPlaying = isPlaying, + showInLibraryIcon = true, + trailingContent = { + IconButton( + onClick = { + menuState.show { + SongMenu( + originalSong = song, + navController = navController, + playerConnection = playerConnection, + onDismiss = menuState::dismiss + ) + } } + ) { + Icon( + painter = painterResource(R.drawable.more_vert), + contentDescription = null + ) } - } - - IconButton( - onClick = { - menuState.show { - AlbumMenu( - album = Album(albumWithSongs.album, albumWithSongs.artists), - navController = navController, - playerConnection = playerConnection, - showDeleteButton = false, - onDismiss = menuState::dismiss + }, + modifier = Modifier + .fillMaxWidth() + .combinedClickable { + if (song.id == mediaMetadata?.id) { + playerConnection.player.togglePlayPause() + } else { + playerConnection.playQueue( + ListQueue( + title = albumWithSongs.album.title, + items = albumWithSongs.songs.map { it.toMediaItem() }, + startIndex = index + ) ) } } - ) { - Icon( - painter = painterResource(R.drawable.more_vert), - contentDescription = null - ) - } - } - } - } - - Spacer(Modifier.height(12.dp)) - - Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) { - Button( - onClick = { - playerConnection.playQueue( - ListQueue( - title = albumWithSongs.album.title, - items = albumWithSongs.songs.map(Song::toMediaItem) - ) - ) - }, - contentPadding = ButtonDefaults.ButtonWithIconContentPadding, - modifier = Modifier.weight(1f) - ) { - Icon( - painter = painterResource(R.drawable.play), - contentDescription = null, - modifier = Modifier.size(ButtonDefaults.IconSize) - ) - Spacer(Modifier.size(ButtonDefaults.IconSpacing)) - Text( - text = stringResource(R.string.play) - ) - } - - OutlinedButton( - onClick = { - playerConnection.playQueue( - ListQueue( - title = albumWithSongs.album.title, - items = albumWithSongs.songs.shuffled().map(Song::toMediaItem) - ) - ) - }, - contentPadding = ButtonDefaults.ButtonWithIconContentPadding, - modifier = Modifier.weight(1f) - ) { - Icon( - painter = painterResource(R.drawable.shuffle), - contentDescription = null, - modifier = Modifier.size(ButtonDefaults.IconSize) ) - Spacer(Modifier.size(ButtonDefaults.IconSpacing)) - Text(stringResource(R.string.shuffle)) } - } - } -} - -@Composable -fun RemoteAlbumHeader( - albumPage: AlbumPage, - inLibrary: Boolean, - downloadState: Int, - onDownload: () -> Unit, - onRemoveDownload: () -> Unit, - navController: NavController, -) { - val playerConnection = LocalPlayerConnection.current ?: return - val menuState = LocalMenuState.current - val database = LocalDatabase.current - - Column( - modifier = Modifier.padding(12.dp) - ) { - Row( - verticalAlignment = Alignment.CenterVertically - ) { - AsyncImage( - model = albumPage.album.thumbnail, - contentDescription = null, - modifier = Modifier - .size(AlbumThumbnailSize) - .clip(RoundedCornerShape(ThumbnailCornerRadius)) - ) - - Spacer(Modifier.width(16.dp)) - - Column( - verticalArrangement = Arrangement.Center, - ) { - AutoResizeText( - text = albumPage.album.title, - fontWeight = FontWeight.Bold, - maxLines = 2, - overflow = TextOverflow.Ellipsis, - fontSizeRange = FontSizeRange(16.sp, 22.sp) - ) - - val annotatedString = buildAnnotatedString { - withStyle( - style = MaterialTheme.typography.titleMedium.copy( - fontWeight = FontWeight.Normal, - color = MaterialTheme.colorScheme.onBackground - ).toSpanStyle() - ) { - albumPage.album.artists?.fastForEachIndexed { index, artist -> - if (artist.id != null) { - pushStringAnnotation(artist.id!!, artist.name) - append(artist.name) - pop() - } else { - append(artist.name) - } - if (index != albumPage.album.artists?.lastIndex) { - append(", ") + } else { + item { + ShimmerHost { + Column(Modifier.padding(12.dp)) { + Row(verticalAlignment = Alignment.CenterVertically) { + Spacer( + modifier = Modifier + .size(AlbumThumbnailSize) + .clip(RoundedCornerShape(ThumbnailCornerRadius)) + .background(MaterialTheme.colorScheme.onSurface) + ) + + Spacer(Modifier.width(16.dp)) + + Column( + verticalArrangement = Arrangement.Center, + ) { + TextPlaceholder() + TextPlaceholder() + TextPlaceholder() } } - } - } - ClickableText(annotatedString) { offset -> - annotatedString.getStringAnnotations(offset, offset).firstOrNull()?.let { range -> - navController.navigate("artist/${range.tag}") - } - } - if (albumPage.album.year != null) { - Text( - text = albumPage.album.year.toString(), - style = MaterialTheme.typography.titleMedium, - fontWeight = FontWeight.Normal - ) - } + Spacer(Modifier.padding(8.dp)) - Row { - IconButton( - onClick = { - database.query { - if (inLibrary) { - runBlocking(Dispatchers.IO) { - albumWithSongs(albumPage.album.browseId).first() - }?.let { - delete(it.album) - } - } else { - insert(albumPage) - } - } - } - ) { - Icon( - painter = painterResource(if (inLibrary) R.drawable.library_add_check else R.drawable.library_add), - contentDescription = null - ) - } + Row { + ButtonPlaceholder(Modifier.weight(1f)) - when (downloadState) { - Download.STATE_COMPLETED -> { - IconButton(onClick = onRemoveDownload) { - Icon( - painter = painterResource(R.drawable.offline), - contentDescription = null - ) - } - } + Spacer(Modifier.width(12.dp)) - Download.STATE_DOWNLOADING -> { - IconButton(onClick = onRemoveDownload) { - CircularProgressIndicator( - strokeWidth = 2.dp, - modifier = Modifier.size(24.dp) - ) - } - } - - else -> { - IconButton(onClick = onDownload) { - Icon( - painter = painterResource(R.drawable.download), - contentDescription = null - ) - } + ButtonPlaceholder(Modifier.weight(1f)) } } - IconButton( - onClick = { - menuState.show { - YouTubeAlbumMenu( - album = albumPage.album, - navController = navController, - playerConnection = playerConnection, - onDismiss = menuState::dismiss - ) - } - } - ) { - Icon( - painter = painterResource(R.drawable.more_vert), - contentDescription = null - ) + repeat(6) { + ListItemPlaceHolder() } } } } + } - Spacer(Modifier.height(12.dp)) - - Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) { - Button( - onClick = { - playerConnection.playQueue( - ListQueue( - title = albumPage.album.title, - items = albumPage.songs.map(SongItem::toMediaItem) - ) - ) - }, - contentPadding = ButtonDefaults.ButtonWithIconContentPadding, - modifier = Modifier.weight(1f) - ) { - Icon( - painter = painterResource(R.drawable.play), - contentDescription = null, - modifier = Modifier.size(ButtonDefaults.IconSize) - ) - Spacer(Modifier.size(ButtonDefaults.IconSpacing)) - Text( - text = stringResource(R.string.play) - ) - } - - OutlinedButton( - onClick = { - playerConnection.playQueue( - ListQueue( - title = albumPage.album.title, - items = albumPage.songs.shuffled().map(SongItem::toMediaItem) - ) - ) - }, - contentPadding = ButtonDefaults.ButtonWithIconContentPadding, - modifier = Modifier.weight(1f) - ) { + TopAppBar( + title = { }, + navigationIcon = { + IconButton(onClick = navController::navigateUp) { Icon( - painter = painterResource(R.drawable.shuffle), - contentDescription = null, - modifier = Modifier.size(ButtonDefaults.IconSize) + painterResource(R.drawable.arrow_back), + contentDescription = null ) - Spacer(Modifier.size(ButtonDefaults.IconSpacing)) - Text(stringResource(R.string.shuffle)) } - } - } + }, + scrollBehavior = scrollBehavior + ) } diff --git a/app/src/main/java/com/zionhuang/music/ui/screens/HomeScreen.kt b/app/src/main/java/com/zionhuang/music/ui/screens/HomeScreen.kt index 191d0282d..83497de52 100644 --- a/app/src/main/java/com/zionhuang/music/ui/screens/HomeScreen.kt +++ b/app/src/main/java/com/zionhuang/music/ui/screens/HomeScreen.kt @@ -230,7 +230,7 @@ fun HomeScreen( onLongClick = { menuState.show { YouTubeAlbumMenu( - album = album, + albumItem = album, navController = navController, playerConnection = playerConnection, onDismiss = menuState::dismiss diff --git a/app/src/main/java/com/zionhuang/music/ui/screens/LoginScreen.kt b/app/src/main/java/com/zionhuang/music/ui/screens/LoginScreen.kt index 63e4e0c12..75f315fb5 100644 --- a/app/src/main/java/com/zionhuang/music/ui/screens/LoginScreen.kt +++ b/app/src/main/java/com/zionhuang/music/ui/screens/LoginScreen.kt @@ -29,6 +29,7 @@ import com.zionhuang.music.constants.AccountNameKey import com.zionhuang.music.constants.InnerTubeCookieKey import com.zionhuang.music.constants.VisitorDataKey import com.zionhuang.music.utils.rememberPreference +import com.zionhuang.music.utils.reportException import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch @@ -58,10 +59,10 @@ fun LoginScreen( innerTubeCookie = CookieManager.getInstance().getCookie(url) GlobalScope.launch { YouTube.accountInfo().onSuccess { - accountName = it?.name.orEmpty() - accountEmail = it?.email.orEmpty() + accountName = it.name + accountEmail = it.email }.onFailure { - it.printStackTrace() + reportException(it) } } } diff --git a/app/src/main/java/com/zionhuang/music/ui/screens/NewReleaseScreen.kt b/app/src/main/java/com/zionhuang/music/ui/screens/NewReleaseScreen.kt index 14eb1408e..b2e053024 100644 --- a/app/src/main/java/com/zionhuang/music/ui/screens/NewReleaseScreen.kt +++ b/app/src/main/java/com/zionhuang/music/ui/screens/NewReleaseScreen.kt @@ -67,7 +67,7 @@ fun NewReleaseScreen( onLongClick = { menuState.show { YouTubeAlbumMenu( - album = album, + albumItem = album, navController = navController, playerConnection = playerConnection, onDismiss = menuState::dismiss diff --git a/app/src/main/java/com/zionhuang/music/ui/screens/StatsScreen.kt b/app/src/main/java/com/zionhuang/music/ui/screens/StatsScreen.kt index a72f2b024..82bc94319 100644 --- a/app/src/main/java/com/zionhuang/music/ui/screens/StatsScreen.kt +++ b/app/src/main/java/com/zionhuang/music/ui/screens/StatsScreen.kt @@ -3,24 +3,16 @@ package com.zionhuang.music.ui.screens import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.clickable import androidx.compose.foundation.combinedClickable -import androidx.compose.foundation.horizontalScroll -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.WindowInsetsSides import androidx.compose.foundation.layout.asPaddingValues import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.only -import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items -import androidx.compose.foundation.rememberScrollState import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.FilterChip -import androidx.compose.material3.FilterChipDefaults import androidx.compose.material3.Icon import androidx.compose.material3.IconButton -import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable @@ -30,7 +22,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.pluralStringResource import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.navigation.NavController import com.zionhuang.innertube.models.WatchEndpoint @@ -41,13 +32,14 @@ import com.zionhuang.music.constants.StatPeriod import com.zionhuang.music.extensions.togglePlayPause import com.zionhuang.music.models.toMediaMetadata import com.zionhuang.music.playback.queues.YouTubeQueue +import com.zionhuang.music.ui.component.AlbumListItem import com.zionhuang.music.ui.component.ArtistListItem +import com.zionhuang.music.ui.component.ChipsRow import com.zionhuang.music.ui.component.LocalMenuState import com.zionhuang.music.ui.component.NavigationTitle import com.zionhuang.music.ui.component.SongListItem -import com.zionhuang.music.ui.component.YouTubeListItem +import com.zionhuang.music.ui.menu.AlbumMenu import com.zionhuang.music.ui.menu.SongMenu -import com.zionhuang.music.ui.menu.YouTubeAlbumMenu import com.zionhuang.music.viewmodels.StatsViewModel @OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class) @@ -71,36 +63,18 @@ fun StatsScreen( modifier = Modifier.windowInsetsPadding(LocalPlayerAwareWindowInsets.current.only(WindowInsetsSides.Top)) ) { item { - Row( - modifier = Modifier - .fillMaxWidth() - .horizontalScroll(rememberScrollState()) - ) { - Spacer(Modifier.width(8.dp)) - - StatPeriod.values().forEach { period -> - FilterChip( - label = { - Text( - when (period) { - StatPeriod.`1_WEEK` -> pluralStringResource(R.plurals.n_week, 1, 1) - StatPeriod.`1_MONTH` -> pluralStringResource(R.plurals.n_month, 1, 1) - StatPeriod.`3_MONTH` -> pluralStringResource(R.plurals.n_month, 3, 3) - StatPeriod.`6_MONTH` -> pluralStringResource(R.plurals.n_month, 6, 6) - StatPeriod.`1_YEAR` -> pluralStringResource(R.plurals.n_year, 1, 1) - StatPeriod.ALL -> stringResource(R.string.filter_all) - } - ) - }, - selected = statPeriod == period, - colors = FilterChipDefaults.filterChipColors(containerColor = MaterialTheme.colorScheme.background), - onClick = { - viewModel.statPeriod.value = period - } - ) - Spacer(Modifier.width(8.dp)) - } - } + ChipsRow( + chips = listOf( + StatPeriod.`1_WEEK` to pluralStringResource(R.plurals.n_week, 1, 1), + StatPeriod.`1_MONTH` to pluralStringResource(R.plurals.n_month, 1, 1), + StatPeriod.`3_MONTH` to pluralStringResource(R.plurals.n_month, 3, 3), + StatPeriod.`6_MONTH` to pluralStringResource(R.plurals.n_month, 6, 6), + StatPeriod.`1_YEAR` to pluralStringResource(R.plurals.n_year, 1, 1), + StatPeriod.ALL to stringResource(R.string.filter_all) + ), + currentValue = statPeriod, + onValueUpdate = { viewModel.statPeriod.value = it } + ) } item { @@ -176,17 +150,17 @@ fun StatsScreen( items( items = mostPlayedAlbums, key = { it.id } - ) { item -> - YouTubeListItem( - item = item, - isActive = mediaMetadata?.album?.id == item.id, + ) { album -> + AlbumListItem( + album = album, + isActive = album.id == mediaMetadata?.album?.id, isPlaying = isPlaying, trailingContent = { IconButton( onClick = { menuState.show { - YouTubeAlbumMenu( - album = item, + AlbumMenu( + originalAlbum = album, navController = navController, playerConnection = playerConnection, onDismiss = menuState::dismiss @@ -201,8 +175,9 @@ fun StatsScreen( } }, modifier = Modifier - .clickable { - navController.navigate("album/${item.id}") + .fillMaxWidth() + .combinedClickable { + navController.navigate("album/${album.id}") } .animateItemPlacement() ) diff --git a/app/src/main/java/com/zionhuang/music/ui/screens/YouTubeBrowseScreen.kt b/app/src/main/java/com/zionhuang/music/ui/screens/YouTubeBrowseScreen.kt index 02a948503..1d037fb7f 100644 --- a/app/src/main/java/com/zionhuang/music/ui/screens/YouTubeBrowseScreen.kt +++ b/app/src/main/java/com/zionhuang/music/ui/screens/YouTubeBrowseScreen.kt @@ -108,7 +108,7 @@ fun YouTubeBrowseScreen( ) is AlbumItem -> YouTubeAlbumMenu( - album = item, + albumItem = item, navController = navController, playerConnection = playerConnection, onDismiss = menuState::dismiss diff --git a/app/src/main/java/com/zionhuang/music/ui/screens/artist/ArtistItemsScreen.kt b/app/src/main/java/com/zionhuang/music/ui/screens/artist/ArtistItemsScreen.kt index 78d8b637d..b16d32cb5 100644 --- a/app/src/main/java/com/zionhuang/music/ui/screens/artist/ArtistItemsScreen.kt +++ b/app/src/main/java/com/zionhuang/music/ui/screens/artist/ArtistItemsScreen.kt @@ -118,7 +118,7 @@ fun ArtistItemsScreen( ) is AlbumItem -> YouTubeAlbumMenu( - album = item, + albumItem = item, navController = navController, playerConnection = playerConnection, onDismiss = menuState::dismiss @@ -214,7 +214,7 @@ fun ArtistItemsScreen( ) is AlbumItem -> YouTubeAlbumMenu( - album = item, + albumItem = item, navController = navController, playerConnection = playerConnection, onDismiss = menuState::dismiss diff --git a/app/src/main/java/com/zionhuang/music/ui/screens/artist/ArtistScreen.kt b/app/src/main/java/com/zionhuang/music/ui/screens/artist/ArtistScreen.kt index 14b0c453a..f8e168dee 100644 --- a/app/src/main/java/com/zionhuang/music/ui/screens/artist/ArtistScreen.kt +++ b/app/src/main/java/com/zionhuang/music/ui/screens/artist/ArtistScreen.kt @@ -343,7 +343,7 @@ fun ArtistScreen( ) is AlbumItem -> YouTubeAlbumMenu( - album = item, + albumItem = item, navController = navController, playerConnection = playerConnection, onDismiss = menuState::dismiss @@ -434,11 +434,7 @@ fun ArtistScreen( database.transaction { val artist = libraryArtist if (artist != null) { - update( - artist.copy( - bookmarkedAt = if (artist.bookmarkedAt != null) null else LocalDateTime.now() - ) - ) + update(artist.toggleLike()) } else { artistPage?.artist?.let { insert( @@ -455,8 +451,8 @@ fun ArtistScreen( } ) { Icon( - painter = painterResource(if (libraryArtist?.bookmarkedAt != null) R.drawable.bookmark_filled else R.drawable.bookmark), - tint = if (libraryArtist?.bookmarkedAt != null) MaterialTheme.colorScheme.primary else LocalContentColor.current, + painter = painterResource(if (libraryArtist?.bookmarkedAt != null) R.drawable.favorite else R.drawable.favorite_border), + tint = if (libraryArtist?.bookmarkedAt != null) MaterialTheme.colorScheme.error else LocalContentColor.current, contentDescription = null ) } diff --git a/app/src/main/java/com/zionhuang/music/ui/screens/library/LibraryAlbumsScreen.kt b/app/src/main/java/com/zionhuang/music/ui/screens/library/LibraryAlbumsScreen.kt index 7c222e653..22f541fef 100644 --- a/app/src/main/java/com/zionhuang/music/ui/screens/library/LibraryAlbumsScreen.kt +++ b/app/src/main/java/com/zionhuang/music/ui/screens/library/LibraryAlbumsScreen.kt @@ -13,9 +13,11 @@ import androidx.compose.material3.IconButton import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.pluralStringResource +import androidx.compose.ui.res.stringResource import androidx.hilt.navigation.compose.hiltViewModel import androidx.navigation.NavController import com.zionhuang.music.LocalPlayerAwareWindowInsets @@ -23,6 +25,7 @@ import com.zionhuang.music.LocalPlayerConnection import com.zionhuang.music.R import com.zionhuang.music.constants.* import com.zionhuang.music.ui.component.AlbumListItem +import com.zionhuang.music.ui.component.ChipsRow import com.zionhuang.music.ui.component.LocalMenuState import com.zionhuang.music.ui.component.SortHeader import com.zionhuang.music.ui.menu.AlbumMenu @@ -41,6 +44,7 @@ fun LibraryAlbumsScreen( val isPlaying by playerConnection.isPlaying.collectAsState() val mediaMetadata by playerConnection.mediaMetadata.collectAsState() + var filter by rememberEnumPreference(AlbumFilterKey, AlbumFilter.LIBRARY) val (sortType, onSortTypeChange) = rememberEnumPreference(AlbumSortTypeKey, AlbumSortType.CREATE_DATE) val (sortDescending, onSortDescendingChange) = rememberPreference(AlbumSortDescendingKey, true) @@ -52,6 +56,17 @@ fun LibraryAlbumsScreen( LazyColumn( contentPadding = LocalPlayerAwareWindowInsets.current.asPaddingValues() ) { + item(key = "filter") { + ChipsRow( + chips = listOf( + AlbumFilter.LIBRARY to stringResource(R.string.filter_library), + AlbumFilter.LIKED to stringResource(R.string.filter_liked) + ), + currentValue = filter, + onValueUpdate = { filter = it } + ) + } + item( key = "header", contentType = CONTENT_TYPE_HEADER @@ -90,7 +105,7 @@ fun LibraryAlbumsScreen( onClick = { menuState.show { AlbumMenu( - album = album, + originalAlbum = album, navController = navController, playerConnection = playerConnection, onDismiss = menuState::dismiss diff --git a/app/src/main/java/com/zionhuang/music/ui/screens/library/LibraryArtistsScreen.kt b/app/src/main/java/com/zionhuang/music/ui/screens/library/LibraryArtistsScreen.kt index cfdb49da7..92b5c784c 100644 --- a/app/src/main/java/com/zionhuang/music/ui/screens/library/LibraryArtistsScreen.kt +++ b/app/src/main/java/com/zionhuang/music/ui/screens/library/LibraryArtistsScreen.kt @@ -2,25 +2,16 @@ package com.zionhuang.music.ui.screens.library import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.clickable -import androidx.compose.foundation.horizontalScroll import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.asPaddingValues import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items -import androidx.compose.foundation.rememberScrollState -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.FilterChip -import androidx.compose.material3.FilterChipDefaults import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.LocalContentColor import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue @@ -29,33 +20,32 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.pluralStringResource import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.navigation.NavController import com.zionhuang.music.LocalDatabase import com.zionhuang.music.LocalPlayerAwareWindowInsets import com.zionhuang.music.R +import com.zionhuang.music.constants.ArtistFilter +import com.zionhuang.music.constants.ArtistFilterKey import com.zionhuang.music.constants.ArtistSortDescendingKey import com.zionhuang.music.constants.ArtistSortType import com.zionhuang.music.constants.ArtistSortTypeKey -import com.zionhuang.music.constants.ArtistViewType -import com.zionhuang.music.constants.ArtistViewTypeKey import com.zionhuang.music.constants.CONTENT_TYPE_ARTIST import com.zionhuang.music.ui.component.ArtistListItem +import com.zionhuang.music.ui.component.ChipsRow import com.zionhuang.music.ui.component.SortHeader import com.zionhuang.music.utils.rememberEnumPreference import com.zionhuang.music.utils.rememberPreference import com.zionhuang.music.viewmodels.LibraryArtistsViewModel -import java.time.LocalDateTime -@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterial3Api::class) +@OptIn(ExperimentalFoundationApi::class) @Composable fun LibraryArtistsScreen( navController: NavController, viewModel: LibraryArtistsViewModel = hiltViewModel(), ) { val database = LocalDatabase.current - var viewType by rememberEnumPreference(ArtistViewTypeKey, ArtistViewType.ALL) + var filter by rememberEnumPreference(ArtistFilterKey, ArtistFilter.LIBRARY) val (sortType, onSortTypeChange) = rememberEnumPreference(ArtistSortTypeKey, ArtistSortType.CREATE_DATE) val (sortDescending, onSortDescendingChange) = rememberPreference(ArtistSortDescendingKey, true) @@ -67,27 +57,15 @@ fun LibraryArtistsScreen( LazyColumn( contentPadding = LocalPlayerAwareWindowInsets.current.asPaddingValues() ) { - item(key = "viewType") { - Row( - modifier = Modifier - .fillMaxWidth() - .horizontalScroll(rememberScrollState()) - ) { - Spacer(Modifier.width(16.dp)) - - listOf( - ArtistViewType.ALL to stringResource(R.string.filter_all), - ArtistViewType.BOOKMARKED to stringResource(R.string.filter_bookmarked) - ).forEach { - FilterChip( - label = { Text(it.second) }, - selected = viewType == it.first, - colors = FilterChipDefaults.filterChipColors(containerColor = MaterialTheme.colorScheme.background), - onClick = { viewType = it.first } - ) - Spacer(Modifier.width(8.dp)) - } - } + item(key = "filter") { + ChipsRow( + chips = listOf( + ArtistFilter.LIBRARY to stringResource(R.string.filter_library), + ArtistFilter.LIKED to stringResource(R.string.filter_liked) + ), + currentValue = filter, + onValueUpdate = { filter = it } + ) } item(key = "header") { @@ -119,17 +97,13 @@ fun LibraryArtistsScreen( IconButton( onClick = { database.transaction { - update( - artist.artist.copy( - bookmarkedAt = if (artist.artist.bookmarkedAt != null) null else LocalDateTime.now() - ) - ) + update(artist.artist.toggleLike()) } } ) { Icon( - painter = painterResource(if (artist.artist.bookmarkedAt != null) R.drawable.bookmark_filled else R.drawable.bookmark), - tint = if (artist.artist.bookmarkedAt != null) MaterialTheme.colorScheme.primary else LocalContentColor.current, + painter = painterResource(if (artist.artist.bookmarkedAt != null) R.drawable.favorite else R.drawable.favorite_border), + tint = if (artist.artist.bookmarkedAt != null) MaterialTheme.colorScheme.error else LocalContentColor.current, contentDescription = null ) } diff --git a/app/src/main/java/com/zionhuang/music/ui/screens/library/LibraryPlaylistsScreen.kt b/app/src/main/java/com/zionhuang/music/ui/screens/library/LibraryPlaylistsScreen.kt index 68580b118..27e4b5168 100644 --- a/app/src/main/java/com/zionhuang/music/ui/screens/library/LibraryPlaylistsScreen.kt +++ b/app/src/main/java/com/zionhuang/music/ui/screens/library/LibraryPlaylistsScreen.kt @@ -6,13 +6,11 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.asPaddingValues import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material3.Icon import androidx.compose.material3.IconButton -import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState @@ -32,15 +30,11 @@ import com.zionhuang.music.LocalPlayerAwareWindowInsets import com.zionhuang.music.R import com.zionhuang.music.constants.CONTENT_TYPE_HEADER import com.zionhuang.music.constants.CONTENT_TYPE_PLAYLIST -import com.zionhuang.music.constants.ListThumbnailSize import com.zionhuang.music.constants.PlaylistSortDescendingKey import com.zionhuang.music.constants.PlaylistSortType import com.zionhuang.music.constants.PlaylistSortTypeKey import com.zionhuang.music.db.entities.PlaylistEntity -import com.zionhuang.music.db.entities.PlaylistEntity.Companion.DOWNLOADED_PLAYLIST_ID -import com.zionhuang.music.db.entities.PlaylistEntity.Companion.LIKED_PLAYLIST_ID import com.zionhuang.music.ui.component.HideOnScrollFAB -import com.zionhuang.music.ui.component.ListItem import com.zionhuang.music.ui.component.LocalMenuState import com.zionhuang.music.ui.component.PlaylistListItem import com.zionhuang.music.ui.component.SortHeader @@ -64,8 +58,6 @@ fun LibraryPlaylistsScreen( val (sortType, onSortTypeChange) = rememberEnumPreference(PlaylistSortTypeKey, PlaylistSortType.CREATE_DATE) val (sortDescending, onSortDescendingChange) = rememberPreference(PlaylistSortDescendingKey, true) - val likedSongCount by viewModel.likedSongCount.collectAsState() - val downloadedSongCount by viewModel.downloadedSongCount.collectAsState(0) val playlists by viewModel.allPlaylists.collectAsState() val lazyListState = rememberLazyListState() @@ -118,51 +110,6 @@ fun LibraryPlaylistsScreen( ) } - item( - key = LIKED_PLAYLIST_ID, - contentType = CONTENT_TYPE_PLAYLIST - ) { - ListItem( - title = stringResource(R.string.liked_songs), - subtitle = pluralStringResource(R.plurals.n_song, likedSongCount, likedSongCount), - thumbnailContent = { - Icon( - painter = painterResource(R.drawable.favorite), - contentDescription = null, - tint = MaterialTheme.colorScheme.error, - modifier = Modifier.size(ListThumbnailSize) - ) - }, - modifier = Modifier - .clickable { - navController.navigate("local_playlist/$LIKED_PLAYLIST_ID") - } - .animateItemPlacement() - ) - } - - item( - key = DOWNLOADED_PLAYLIST_ID, - contentType = CONTENT_TYPE_PLAYLIST - ) { - ListItem( - title = stringResource(R.string.downloaded_songs), - subtitle = pluralStringResource(R.plurals.n_song, downloadedSongCount, downloadedSongCount), - thumbnailContent = { - Icon( - painter = painterResource(R.drawable.offline), - contentDescription = null, - modifier = Modifier.size(ListThumbnailSize) - ) - }, - modifier = Modifier - .clickable { - navController.navigate("local_playlist/$DOWNLOADED_PLAYLIST_ID") - } - .animateItemPlacement() - ) - } - items( items = playlists, key = { it.id }, diff --git a/app/src/main/java/com/zionhuang/music/ui/screens/library/LibrarySongsScreen.kt b/app/src/main/java/com/zionhuang/music/ui/screens/library/LibrarySongsScreen.kt index 4503ffc35..dda00765a 100644 --- a/app/src/main/java/com/zionhuang/music/ui/screens/library/LibrarySongsScreen.kt +++ b/app/src/main/java/com/zionhuang/music/ui/screens/library/LibrarySongsScreen.kt @@ -11,10 +11,12 @@ import androidx.compose.material3.IconButton import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.pluralStringResource +import androidx.compose.ui.res.stringResource import androidx.hilt.navigation.compose.hiltViewModel import androidx.navigation.NavController import com.zionhuang.music.LocalPlayerAwareWindowInsets @@ -24,6 +26,7 @@ import com.zionhuang.music.constants.* import com.zionhuang.music.extensions.toMediaItem import com.zionhuang.music.extensions.togglePlayPause import com.zionhuang.music.playback.queues.ListQueue +import com.zionhuang.music.ui.component.ChipsRow import com.zionhuang.music.ui.component.HideOnScrollFAB import com.zionhuang.music.ui.component.LocalMenuState import com.zionhuang.music.ui.component.SongListItem @@ -45,6 +48,7 @@ fun LibrarySongsScreen( val isPlaying by playerConnection.isPlaying.collectAsState() val mediaMetadata by playerConnection.mediaMetadata.collectAsState() + var filter by rememberEnumPreference(SongFilterKey, SongFilter.LIBRARY) val (sortType, onSortTypeChange) = rememberEnumPreference(SongSortTypeKey, SongSortType.CREATE_DATE) val (sortDescending, onSortDescendingChange) = rememberPreference(SongSortDescendingKey, true) @@ -59,10 +63,19 @@ fun LibrarySongsScreen( state = lazyListState, contentPadding = LocalPlayerAwareWindowInsets.current.asPaddingValues() ) { - item( - key = "header", - contentType = CONTENT_TYPE_HEADER - ) { + item(key = "filter") { + ChipsRow( + chips = listOf( + SongFilter.LIBRARY to stringResource(R.string.filter_library), + SongFilter.LIKED to stringResource(R.string.filter_liked), + SongFilter.DOWNLOADED to stringResource(R.string.filter_downloaded) + ), + currentValue = filter, + onValueUpdate = { filter = it } + ) + } + + item(key = "header") { SortHeader( sortType = sortType, sortDescending = sortDescending, diff --git a/app/src/main/java/com/zionhuang/music/ui/screens/playlist/BuiltInPlaylistScreen.kt b/app/src/main/java/com/zionhuang/music/ui/screens/playlist/BuiltInPlaylistScreen.kt deleted file mode 100644 index 41e68f994..000000000 --- a/app/src/main/java/com/zionhuang/music/ui/screens/playlist/BuiltInPlaylistScreen.kt +++ /dev/null @@ -1,214 +0,0 @@ -package com.zionhuang.music.ui.screens.playlist - -import androidx.compose.foundation.ExperimentalFoundationApi -import androidx.compose.foundation.combinedClickable -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.asPaddingValues -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.itemsIndexed -import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.Text -import androidx.compose.material3.TopAppBar -import androidx.compose.material3.TopAppBarScrollBehavior -import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.pluralStringResource -import androidx.compose.ui.util.fastSumBy -import androidx.hilt.navigation.compose.hiltViewModel -import androidx.navigation.NavController -import com.zionhuang.music.LocalPlayerAwareWindowInsets -import com.zionhuang.music.LocalPlayerConnection -import com.zionhuang.music.R -import com.zionhuang.music.constants.DownloadedSongSortDescendingKey -import com.zionhuang.music.constants.DownloadedSongSortType -import com.zionhuang.music.constants.DownloadedSongSortTypeKey -import com.zionhuang.music.constants.SongSortDescendingKey -import com.zionhuang.music.constants.SongSortType -import com.zionhuang.music.constants.SongSortTypeKey -import com.zionhuang.music.db.entities.PlaylistEntity.Companion.DOWNLOADED_PLAYLIST_ID -import com.zionhuang.music.db.entities.PlaylistEntity.Companion.LIKED_PLAYLIST_ID -import com.zionhuang.music.extensions.toMediaItem -import com.zionhuang.music.extensions.togglePlayPause -import com.zionhuang.music.playback.queues.ListQueue -import com.zionhuang.music.ui.component.HideOnScrollFAB -import com.zionhuang.music.ui.component.LocalMenuState -import com.zionhuang.music.ui.component.SongListItem -import com.zionhuang.music.ui.component.SortHeader -import com.zionhuang.music.ui.menu.SongMenu -import com.zionhuang.music.utils.joinByBullet -import com.zionhuang.music.utils.makeTimeString -import com.zionhuang.music.utils.rememberEnumPreference -import com.zionhuang.music.utils.rememberPreference -import com.zionhuang.music.viewmodels.BuiltInPlaylistViewModel - -@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterial3Api::class) -@Composable -fun BuiltInPlaylistScreen( - navController: NavController, - scrollBehavior: TopAppBarScrollBehavior, - viewModel: BuiltInPlaylistViewModel = hiltViewModel(), -) { - val context = LocalContext.current - val menuState = LocalMenuState.current - val playerConnection = LocalPlayerConnection.current ?: return - val isPlaying by playerConnection.isPlaying.collectAsState() - val mediaMetadata by playerConnection.mediaMetadata.collectAsState() - - val (sortType, onSortTypeChange) = rememberEnumPreference(SongSortTypeKey, SongSortType.CREATE_DATE) - val (sortDescending, onSortDescendingChange) = rememberPreference(SongSortDescendingKey, true) - val (dlSortType, onDlSortTypeChange) = rememberEnumPreference(DownloadedSongSortTypeKey, DownloadedSongSortType.CREATE_DATE) - val (dlSortDescending, onDlSortDescendingChange) = rememberPreference(DownloadedSongSortDescendingKey, true) - - val songs by viewModel.songs.collectAsState() - val playlistLength = remember(songs) { - songs.fastSumBy { it.song.duration } - } - val playlistName = remember { - context.getString( - when (viewModel.playlistId) { - LIKED_PLAYLIST_ID -> R.string.liked_songs - DOWNLOADED_PLAYLIST_ID -> R.string.downloaded_songs - else -> error("Unknown playlist id") - } - ) - } - - val lazyListState = rememberLazyListState() - - Box( - modifier = Modifier.fillMaxSize() - ) { - LazyColumn( - state = lazyListState, - contentPadding = LocalPlayerAwareWindowInsets.current.asPaddingValues() - ) { - item { - if (viewModel.playlistId == LIKED_PLAYLIST_ID) { - SortHeader( - sortType = sortType, - sortDescending = sortDescending, - onSortTypeChange = onSortTypeChange, - onSortDescendingChange = onSortDescendingChange, - sortTypeText = { sortType -> - when (sortType) { - SongSortType.CREATE_DATE -> R.string.sort_by_create_date - SongSortType.NAME -> R.string.sort_by_name - SongSortType.ARTIST -> R.string.sort_by_artist - SongSortType.PLAY_TIME -> R.string.sort_by_play_time - } - }, - trailingText = joinByBullet( - makeTimeString(playlistLength * 1000L), - pluralStringResource(R.plurals.n_song, songs.size, songs.size) - ) - ) - } else { - SortHeader( - sortType = dlSortType, - sortDescending = dlSortDescending, - onSortTypeChange = onDlSortTypeChange, - onSortDescendingChange = onDlSortDescendingChange, - sortTypeText = { sortType -> - when (sortType) { - DownloadedSongSortType.CREATE_DATE -> R.string.sort_by_create_date - DownloadedSongSortType.NAME -> R.string.sort_by_name - DownloadedSongSortType.ARTIST -> R.string.sort_by_artist - DownloadedSongSortType.PLAY_TIME -> R.string.sort_by_play_time - } - }, - trailingText = joinByBullet( - makeTimeString(playlistLength * 1000L), - pluralStringResource(R.plurals.n_song, songs.size, songs.size) - ) - ) - } - } - - itemsIndexed( - items = songs, - key = { _, song -> song.id } - ) { index, song -> - SongListItem( - song = song, - showLikedIcon = viewModel.playlistId != LIKED_PLAYLIST_ID, - showInLibraryIcon = true, - showDownloadIcon = viewModel.playlistId != DOWNLOADED_PLAYLIST_ID, - isActive = song.id == mediaMetadata?.id, - isPlaying = isPlaying, - trailingContent = { - IconButton( - onClick = { - menuState.show { - SongMenu( - originalSong = song, - navController = navController, - playerConnection = playerConnection, - onDismiss = menuState::dismiss - ) - } - } - ) { - Icon( - painter = painterResource(R.drawable.more_vert), - contentDescription = null - ) - } - }, - modifier = Modifier - .fillMaxWidth() - .combinedClickable { - if (song.id == mediaMetadata?.id) { - playerConnection.player.togglePlayPause() - } else { - playerConnection.playQueue( - ListQueue( - title = playlistName, - items = songs.map { it.toMediaItem() }, - startIndex = index - ) - ) - } - } - .animateItemPlacement() - ) - } - } - - TopAppBar( - title = { Text(playlistName) }, - navigationIcon = { - IconButton(onClick = navController::navigateUp) { - Icon( - painterResource(R.drawable.arrow_back), - contentDescription = null - ) - } - }, - scrollBehavior = scrollBehavior - ) - - HideOnScrollFAB( - visible = songs.isNotEmpty(), - lazyListState = lazyListState, - icon = R.drawable.shuffle, - onClick = { - playerConnection.playQueue( - ListQueue( - title = playlistName, - items = songs.shuffled().map { it.toMediaItem() }, - ) - ) - } - ) - } -} diff --git a/app/src/main/java/com/zionhuang/music/ui/screens/search/LocalSearchScreen.kt b/app/src/main/java/com/zionhuang/music/ui/screens/search/LocalSearchScreen.kt index f6ad2af55..74026d030 100644 --- a/app/src/main/java/com/zionhuang/music/ui/screens/search/LocalSearchScreen.kt +++ b/app/src/main/java/com/zionhuang/music/ui/screens/search/LocalSearchScreen.kt @@ -2,7 +2,6 @@ package com.zionhuang.music.ui.screens.search import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.clickable -import androidx.compose.foundation.horizontalScroll import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -12,10 +11,7 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.foundation.rememberScrollState import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.FilterChip -import androidx.compose.material3.FilterChipDefaults import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme @@ -48,6 +44,7 @@ import com.zionhuang.music.extensions.togglePlayPause import com.zionhuang.music.playback.queues.ListQueue import com.zionhuang.music.ui.component.AlbumListItem import com.zionhuang.music.ui.component.ArtistListItem +import com.zionhuang.music.ui.component.ChipsRow import com.zionhuang.music.ui.component.EmptyPlaceholder import com.zionhuang.music.ui.component.LocalMenuState import com.zionhuang.music.ui.component.PlaylistListItem @@ -90,28 +87,17 @@ fun LocalSearchScreen( } Column { - Row( - horizontalArrangement = Arrangement.spacedBy(8.dp), - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 8.dp) - .horizontalScroll(rememberScrollState()) - ) { - listOf( - LocalFilter.ALL to R.string.filter_all, - LocalFilter.SONG to R.string.filter_songs, - LocalFilter.ALBUM to R.string.filter_albums, - LocalFilter.ARTIST to R.string.filter_artists, - LocalFilter.PLAYLIST to R.string.filter_playlists - ).forEach { (filter, label) -> - FilterChip( - label = { Text(stringResource(label)) }, - selected = searchFilter == filter, - colors = FilterChipDefaults.filterChipColors(containerColor = MaterialTheme.colorScheme.background), - onClick = { viewModel.filter.value = filter } - ) - } - } + ChipsRow( + chips = listOf( + LocalFilter.ALL to stringResource(R.string.filter_all), + LocalFilter.SONG to stringResource(R.string.filter_songs), + LocalFilter.ALBUM to stringResource(R.string.filter_albums), + LocalFilter.ARTIST to stringResource(R.string.filter_artists), + LocalFilter.PLAYLIST to stringResource(R.string.filter_playlists) + ), + currentValue = searchFilter, + onValueUpdate = { viewModel.filter.value = it } + ) LazyColumn( state = lazyListState, diff --git a/app/src/main/java/com/zionhuang/music/ui/screens/search/OnlineSearchResult.kt b/app/src/main/java/com/zionhuang/music/ui/screens/search/OnlineSearchResult.kt index ffec47295..b6e92f45e 100644 --- a/app/src/main/java/com/zionhuang/music/ui/screens/search/OnlineSearchResult.kt +++ b/app/src/main/java/com/zionhuang/music/ui/screens/search/OnlineSearchResult.kt @@ -2,31 +2,21 @@ package com.zionhuang.music.ui.screens.search import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.clickable -import androidx.compose.foundation.horizontalScroll -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsetsSides import androidx.compose.foundation.layout.add import androidx.compose.foundation.layout.asPaddingValues -import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.only import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.systemBars -import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyItemScope import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.foundation.rememberScrollState import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.FilterChip -import androidx.compose.material3.FilterChipDefaults import androidx.compose.material3.Icon import androidx.compose.material3.IconButton -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState @@ -38,7 +28,6 @@ import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.navigation.NavController import com.zionhuang.innertube.YouTube.SearchFilter.Companion.FILTER_ALBUM @@ -61,6 +50,7 @@ import com.zionhuang.music.constants.SearchFilterHeight import com.zionhuang.music.extensions.togglePlayPause import com.zionhuang.music.models.toMediaMetadata import com.zionhuang.music.playback.queues.YouTubeQueue +import com.zionhuang.music.ui.component.ChipsRow import com.zionhuang.music.ui.component.EmptyPlaceholder import com.zionhuang.music.ui.component.LocalMenuState import com.zionhuang.music.ui.component.NavigationTitle @@ -129,7 +119,7 @@ fun OnlineSearchResult( ) is AlbumItem -> YouTubeAlbumMenu( - album = item, + albumItem = item, navController = navController, playerConnection = playerConnection, onDismiss = menuState::dismiss @@ -242,38 +232,27 @@ fun OnlineSearchResult( } } - Row( + ChipsRow( + chips = listOf( + null to stringResource(R.string.filter_all), + FILTER_SONG to stringResource(R.string.filter_songs), + FILTER_VIDEO to stringResource(R.string.filter_videos), + FILTER_ALBUM to stringResource(R.string.filter_albums), + FILTER_ARTIST to stringResource(R.string.filter_artists), + FILTER_COMMUNITY_PLAYLIST to stringResource(R.string.filter_community_playlists), + FILTER_FEATURED_PLAYLIST to stringResource(R.string.filter_featured_playlists) + ), + currentValue = searchFilter, + onValueUpdate = { + if (viewModel.filter.value != it) { + viewModel.filter.value = it + } + coroutineScope.launch { + lazyListState.animateScrollToItem(0) + } + }, modifier = Modifier .windowInsetsPadding(WindowInsets.systemBars.only(WindowInsetsSides.Top)) .padding(top = AppBarHeight) - .fillMaxWidth() - .horizontalScroll(rememberScrollState()) - ) { - Spacer(Modifier.width(8.dp)) - - listOf( - null to R.string.filter_all, - FILTER_SONG to R.string.filter_songs, - FILTER_VIDEO to R.string.filter_videos, - FILTER_ALBUM to R.string.filter_albums, - FILTER_ARTIST to R.string.filter_artists, - FILTER_COMMUNITY_PLAYLIST to R.string.filter_community_playlists, - FILTER_FEATURED_PLAYLIST to R.string.filter_featured_playlists - ).forEach { (filter, label) -> - FilterChip( - label = { Text(text = stringResource(label)) }, - selected = searchFilter == filter, - colors = FilterChipDefaults.filterChipColors(containerColor = MaterialTheme.colorScheme.background), - onClick = { - if (viewModel.filter.value != filter) { - viewModel.filter.value = filter - } - coroutineScope.launch { - lazyListState.animateScrollToItem(0) - } - } - ) - Spacer(Modifier.width(8.dp)) - } - } + ) } diff --git a/app/src/main/java/com/zionhuang/music/ui/screens/settings/AboutScreen.kt b/app/src/main/java/com/zionhuang/music/ui/screens/settings/AboutScreen.kt index 97168db71..7444f441c 100644 --- a/app/src/main/java/com/zionhuang/music/ui/screens/settings/AboutScreen.kt +++ b/app/src/main/java/com/zionhuang/music/ui/screens/settings/AboutScreen.kt @@ -67,6 +67,25 @@ fun AboutScreen( style = MaterialTheme.typography.titleMedium, color = MaterialTheme.colorScheme.secondary ) + + Spacer(Modifier.width(4.dp)) + + Text( + text = BuildConfig.FLAVOR.uppercase(), + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.secondary, + modifier = Modifier + .border( + width = 1.dp, + color = MaterialTheme.colorScheme.secondary, + shape = CircleShape + ) + .padding( + horizontal = 6.dp, + vertical = 2.dp + ) + ) + if (BuildConfig.DEBUG) { Spacer(Modifier.width(4.dp)) @@ -81,7 +100,7 @@ fun AboutScreen( shape = CircleShape ) .padding( - horizontal = 4.dp, + horizontal = 6.dp, vertical = 2.dp ) ) diff --git a/app/src/main/java/com/zionhuang/music/ui/screens/settings/StorageSettings.kt b/app/src/main/java/com/zionhuang/music/ui/screens/settings/StorageSettings.kt index 2ec8fa899..89ae0cef0 100644 --- a/app/src/main/java/com/zionhuang/music/ui/screens/settings/StorageSettings.kt +++ b/app/src/main/java/com/zionhuang/music/ui/screens/settings/StorageSettings.kt @@ -29,15 +29,18 @@ import androidx.compose.ui.unit.dp import androidx.navigation.NavController import coil.annotation.ExperimentalCoilApi import coil.imageLoader +import com.zionhuang.music.BuildConfig import com.zionhuang.music.LocalPlayerAwareWindowInsets import com.zionhuang.music.LocalPlayerConnection import com.zionhuang.music.R import com.zionhuang.music.constants.MaxImageCacheSizeKey import com.zionhuang.music.constants.MaxSongCacheSizeKey +import com.zionhuang.music.extensions.tryOrNull import com.zionhuang.music.ui.component.ListPreference import com.zionhuang.music.ui.component.PreferenceEntry import com.zionhuang.music.ui.component.PreferenceGroupTitle import com.zionhuang.music.ui.utils.formatFileSize +import com.zionhuang.music.utils.TranslationHelper import com.zionhuang.music.utils.rememberPreference import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay @@ -61,10 +64,10 @@ fun StorageSettings( mutableStateOf(imageDiskCache.size) } var playerCacheSize by remember { - mutableStateOf(playerCache.cacheSpace) + mutableStateOf(tryOrNull { playerCache.cacheSpace } ?: 0) } var downloadCacheSize by remember { - mutableStateOf(downloadCache.cacheSpace) + mutableStateOf(tryOrNull { downloadCache.cacheSpace } ?: 0) } LaunchedEffect(imageDiskCache) { @@ -76,13 +79,13 @@ fun StorageSettings( LaunchedEffect(playerCache) { while (isActive) { delay(500) - playerCacheSize = playerCache.cacheSpace + playerCacheSize = tryOrNull { playerCache.cacheSpace } ?: 0 } } LaunchedEffect(downloadCache) { while (isActive) { delay(500) - downloadCacheSize = downloadCache.cacheSpace + downloadCacheSize = tryOrNull { downloadCache.cacheSpace } ?: 0 } } @@ -194,6 +197,21 @@ fun StorageSettings( } }, ) + + if (BuildConfig.FLAVOR == "full") { + PreferenceGroupTitle( + title = stringResource(R.string.translation_models) + ) + + PreferenceEntry( + title = { Text(stringResource(R.string.clear_translation_models)) }, + onClick = { + coroutineScope.launch(Dispatchers.IO) { + TranslationHelper.clearModels() + } + }, + ) + } } TopAppBar( diff --git a/app/src/main/java/com/zionhuang/music/utils/CoilBitmapLoader.kt b/app/src/main/java/com/zionhuang/music/utils/CoilBitmapLoader.kt index 1b4beff72..855ca4933 100644 --- a/app/src/main/java/com/zionhuang/music/utils/CoilBitmapLoader.kt +++ b/app/src/main/java/com/zionhuang/music/utils/CoilBitmapLoader.kt @@ -27,6 +27,7 @@ class CoilBitmapLoader( val result = context.imageLoader.execute( ImageRequest.Builder(context) .data(uri) + .allowHardware(false) .build() ) (result.drawable as BitmapDrawable).bitmap diff --git a/app/src/main/java/com/zionhuang/music/viewmodels/AccountViewModel.kt b/app/src/main/java/com/zionhuang/music/viewmodels/AccountViewModel.kt index dd73c3124..2c4974bc6 100644 --- a/app/src/main/java/com/zionhuang/music/viewmodels/AccountViewModel.kt +++ b/app/src/main/java/com/zionhuang/music/viewmodels/AccountViewModel.kt @@ -4,6 +4,7 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.zionhuang.innertube.YouTube import com.zionhuang.innertube.models.PlaylistItem +import com.zionhuang.music.utils.reportException import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.launch @@ -18,7 +19,7 @@ class AccountViewModel @Inject constructor() : ViewModel() { YouTube.likedPlaylists().onSuccess { playlists.value = it }.onFailure { - it.printStackTrace() + reportException(it) } } } diff --git a/app/src/main/java/com/zionhuang/music/viewmodels/AlbumViewModel.kt b/app/src/main/java/com/zionhuang/music/viewmodels/AlbumViewModel.kt index 401d27927..6bc539b93 100644 --- a/app/src/main/java/com/zionhuang/music/viewmodels/AlbumViewModel.kt +++ b/app/src/main/java/com/zionhuang/music/viewmodels/AlbumViewModel.kt @@ -4,16 +4,11 @@ import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.zionhuang.innertube.YouTube -import com.zionhuang.innertube.pages.AlbumPage import com.zionhuang.music.db.MusicDatabase -import com.zionhuang.music.db.entities.AlbumWithSongs +import com.zionhuang.music.utils.reportException import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import javax.inject.Inject @@ -24,35 +19,27 @@ class AlbumViewModel @Inject constructor( savedStateHandle: SavedStateHandle, ) : ViewModel() { val albumId = savedStateHandle.get<String>("albumId")!! - private val _viewState = MutableStateFlow<AlbumViewState?>(null) - val viewState = _viewState.asStateFlow() - val inLibrary: StateFlow<Boolean> = database.album(albumId) - .map { it != null } - .stateIn(viewModelScope, SharingStarted.Eagerly, false) + val albumWithSongs = database.albumWithSongs(albumId) + .stateIn(viewModelScope, SharingStarted.Eagerly, null) init { viewModelScope.launch { - if (database.albumWithSongs(albumId).first() == null) { - YouTube.album(albumId).getOrNull()?.let { - _viewState.value = AlbumViewState.Remote(it) - } - } else { - database.albumWithSongs(albumId).collect { albumWithSongs -> - if (albumWithSongs != null) { - _viewState.value = AlbumViewState.Local(albumWithSongs) + val album = database.album(albumId).first() + if (album == null || album.album.songCount == 0) { + YouTube.album(albumId).onSuccess { + database.transaction { + if (album == null) insert(it) + else update(album.album, it) + } + }.onFailure { + reportException(it) + if (it.message?.contains("NOT_FOUND") == true) { + database.query { + album?.album?.let(::delete) + } } } } } } } - -sealed class AlbumViewState { - data class Local( - val albumWithSongs: AlbumWithSongs, - ) : AlbumViewState() - - data class Remote( - val albumPage: AlbumPage, - ) : AlbumViewState() -} diff --git a/app/src/main/java/com/zionhuang/music/viewmodels/ArtistItemsViewModel.kt b/app/src/main/java/com/zionhuang/music/viewmodels/ArtistItemsViewModel.kt index e759ecc45..c3e26137a 100644 --- a/app/src/main/java/com/zionhuang/music/viewmodels/ArtistItemsViewModel.kt +++ b/app/src/main/java/com/zionhuang/music/viewmodels/ArtistItemsViewModel.kt @@ -6,6 +6,7 @@ import androidx.lifecycle.viewModelScope import com.zionhuang.innertube.YouTube import com.zionhuang.innertube.models.BrowseEndpoint import com.zionhuang.music.models.ItemsPage +import com.zionhuang.music.utils.reportException import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.update @@ -35,8 +36,8 @@ class ArtistItemsViewModel @Inject constructor( items = artistItemsPage.items, continuation = artistItemsPage.continuation ) - }.onFailure { e -> - e.printStackTrace() + }.onFailure { + reportException(it) } } } @@ -53,8 +54,8 @@ class ArtistItemsViewModel @Inject constructor( continuation = artistItemsContinuationPage.continuation ) } - }.onFailure { e -> - e.printStackTrace() + }.onFailure { + reportException(it) } } } diff --git a/app/src/main/java/com/zionhuang/music/viewmodels/ArtistViewModel.kt b/app/src/main/java/com/zionhuang/music/viewmodels/ArtistViewModel.kt index 7f61d6e80..aa7ad1fee 100644 --- a/app/src/main/java/com/zionhuang/music/viewmodels/ArtistViewModel.kt +++ b/app/src/main/java/com/zionhuang/music/viewmodels/ArtistViewModel.kt @@ -9,6 +9,7 @@ import androidx.lifecycle.viewModelScope import com.zionhuang.innertube.YouTube import com.zionhuang.innertube.pages.ArtistPage import com.zionhuang.music.db.MusicDatabase +import com.zionhuang.music.utils.reportException import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.stateIn @@ -32,8 +33,8 @@ class ArtistViewModel @Inject constructor( YouTube.artist(artistId) .onSuccess { artistPage = it - }.onFailure { e -> - e.printStackTrace() + }.onFailure { + reportException(it) } } } diff --git a/app/src/main/java/com/zionhuang/music/viewmodels/BackupRestoreViewModel.kt b/app/src/main/java/com/zionhuang/music/viewmodels/BackupRestoreViewModel.kt index 60b92321f..5a8bf5f14 100644 --- a/app/src/main/java/com/zionhuang/music/viewmodels/BackupRestoreViewModel.kt +++ b/app/src/main/java/com/zionhuang/music/viewmodels/BackupRestoreViewModel.kt @@ -14,6 +14,7 @@ import com.zionhuang.music.extensions.zipInputStream import com.zionhuang.music.extensions.zipOutputStream import com.zionhuang.music.playback.MusicService import com.zionhuang.music.playback.MusicService.Companion.PERSISTENT_QUEUE_FILE +import com.zionhuang.music.utils.reportException import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.runBlocking @@ -47,7 +48,7 @@ class BackupRestoreViewModel @Inject constructor( }.onSuccess { Toast.makeText(context, R.string.backup_create_success, Toast.LENGTH_SHORT).show() }.onFailure { - it.printStackTrace() + reportException(it) Toast.makeText(context, R.string.backup_create_failed, Toast.LENGTH_SHORT).show() } } @@ -84,7 +85,7 @@ class BackupRestoreViewModel @Inject constructor( context.startActivity(Intent(context, MainActivity::class.java)) exitProcess(0) }.onFailure { - it.printStackTrace() + reportException(it) Toast.makeText(context, R.string.restore_failed, Toast.LENGTH_SHORT).show() } } diff --git a/app/src/main/java/com/zionhuang/music/viewmodels/BuiltInPlaylistViewModel.kt b/app/src/main/java/com/zionhuang/music/viewmodels/BuiltInPlaylistViewModel.kt deleted file mode 100644 index 6f813253d..000000000 --- a/app/src/main/java/com/zionhuang/music/viewmodels/BuiltInPlaylistViewModel.kt +++ /dev/null @@ -1,82 +0,0 @@ -package com.zionhuang.music.viewmodels - -import android.content.Context -import androidx.lifecycle.SavedStateHandle -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import androidx.media3.exoplayer.offline.Download.STATE_COMPLETED -import com.zionhuang.music.constants.DownloadedSongSortDescendingKey -import com.zionhuang.music.constants.DownloadedSongSortType -import com.zionhuang.music.constants.DownloadedSongSortTypeKey -import com.zionhuang.music.constants.SongSortDescendingKey -import com.zionhuang.music.constants.SongSortType -import com.zionhuang.music.constants.SongSortTypeKey -import com.zionhuang.music.db.MusicDatabase -import com.zionhuang.music.db.entities.PlaylistEntity.Companion.DOWNLOADED_PLAYLIST_ID -import com.zionhuang.music.db.entities.PlaylistEntity.Companion.LIKED_PLAYLIST_ID -import com.zionhuang.music.extensions.reversed -import com.zionhuang.music.extensions.toEnum -import com.zionhuang.music.playback.DownloadUtil -import com.zionhuang.music.utils.dataStore -import dagger.hilt.android.lifecycle.HiltViewModel -import dagger.hilt.android.qualifiers.ApplicationContext -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.flatMapLatest -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.stateIn -import javax.inject.Inject - -@HiltViewModel -class BuiltInPlaylistViewModel @Inject constructor( - @ApplicationContext context: Context, - database: MusicDatabase, - downloadUtil: DownloadUtil, - savedStateHandle: SavedStateHandle, -) : ViewModel() { - val playlistId = savedStateHandle.get<String>("playlistId")!! - - @OptIn(ExperimentalCoroutinesApi::class) - val songs = when (playlistId) { - LIKED_PLAYLIST_ID -> context.dataStore.data - .map { - it[SongSortTypeKey].toEnum(SongSortType.CREATE_DATE) to (it[SongSortDescendingKey] ?: true) - } - .distinctUntilChanged() - .flatMapLatest { (sortType, descending) -> - database.likedSongs(sortType, descending) - } - - DOWNLOADED_PLAYLIST_ID -> combine( - downloadUtil.downloads.flatMapLatest { downloads -> - database.songs( - downloads.filter { (_, download) -> - download.state == STATE_COMPLETED - }.keys.toList() - ).map { songs -> - songs.map { it to downloads[it.id] } - } - }, - context.dataStore.data - .map { - it[DownloadedSongSortTypeKey].toEnum(DownloadedSongSortType.CREATE_DATE) to (it[DownloadedSongSortDescendingKey] ?: true) - } - .distinctUntilChanged() - ) { songs, (sortType, descending) -> - when (sortType) { - DownloadedSongSortType.CREATE_DATE -> songs.sortedBy { it.second?.updateTimeMs ?: 0L } - DownloadedSongSortType.NAME -> songs.sortedBy { it.first.song.title } - DownloadedSongSortType.ARTIST -> songs.sortedBy { song -> - song.first.artists.joinToString(separator = "") { it.name } - } - DownloadedSongSortType.PLAY_TIME -> songs.sortedBy { it.first.song.totalPlayTime } - } - .map { it.first } - .reversed(descending) - } - - else -> error("Unknown playlist id") - }.stateIn(viewModelScope, SharingStarted.Lazily, emptyList()) -} diff --git a/app/src/main/java/com/zionhuang/music/viewmodels/HomeViewModel.kt b/app/src/main/java/com/zionhuang/music/viewmodels/HomeViewModel.kt index cabe66ff9..8f2fb6627 100644 --- a/app/src/main/java/com/zionhuang/music/viewmodels/HomeViewModel.kt +++ b/app/src/main/java/com/zionhuang/music/viewmodels/HomeViewModel.kt @@ -6,6 +6,7 @@ import com.zionhuang.innertube.YouTube import com.zionhuang.innertube.pages.ExplorePage import com.zionhuang.music.db.MusicDatabase import com.zionhuang.music.db.entities.Song +import com.zionhuang.music.utils.reportException import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow @@ -27,7 +28,7 @@ class HomeViewModel @Inject constructor( YouTube.explore().onSuccess { explorePage.value = it }.onFailure { - it.printStackTrace() + reportException(it) } } diff --git a/app/src/main/java/com/zionhuang/music/viewmodels/LibraryViewModels.kt b/app/src/main/java/com/zionhuang/music/viewmodels/LibraryViewModels.kt index b5ff00181..15fee1187 100644 --- a/app/src/main/java/com/zionhuang/music/viewmodels/LibraryViewModels.kt +++ b/app/src/main/java/com/zionhuang/music/viewmodels/LibraryViewModels.kt @@ -6,15 +6,18 @@ import android.content.Context import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import androidx.media3.exoplayer.offline.Download.STATE_COMPLETED +import androidx.media3.exoplayer.offline.Download import com.zionhuang.innertube.YouTube import com.zionhuang.music.constants.* import com.zionhuang.music.db.MusicDatabase +import com.zionhuang.music.extensions.reversed import com.zionhuang.music.extensions.toEnum import com.zionhuang.music.playback.DownloadUtil import com.zionhuang.music.utils.dataStore +import com.zionhuang.music.utils.reportException import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch @@ -26,14 +29,42 @@ import javax.inject.Inject class LibrarySongsViewModel @Inject constructor( @ApplicationContext context: Context, database: MusicDatabase, + downloadUtil: DownloadUtil, ) : ViewModel() { val allSongs = context.dataStore.data .map { - it[SongSortTypeKey].toEnum(SongSortType.CREATE_DATE) to (it[SongSortDescendingKey] ?: true) + Triple( + it[SongFilterKey].toEnum(SongFilter.LIBRARY), + it[SongSortTypeKey].toEnum(SongSortType.CREATE_DATE), + (it[SongSortDescendingKey] ?: true) + ) } .distinctUntilChanged() - .flatMapLatest { (sortType, descending) -> - database.songs(sortType, descending) + .flatMapLatest { (filter, sortType, descending) -> + when (filter) { + SongFilter.LIBRARY -> database.songs(sortType, descending) + SongFilter.LIKED -> database.likedSongs(sortType, descending) + SongFilter.DOWNLOADED -> downloadUtil.downloads.flatMapLatest { downloads -> + database.allSongs() + .flowOn(Dispatchers.IO) + .map { songs -> + songs.filter { + downloads[it.id]?.state == Download.STATE_COMPLETED + } + } + .map { songs -> + when (sortType) { + SongSortType.CREATE_DATE -> songs.sortedBy { downloads[it.id]?.updateTimeMs ?: 0L } + SongSortType.NAME -> songs.sortedBy { it.song.title } + SongSortType.ARTIST -> songs.sortedBy { song -> + song.artists.joinToString(separator = "") { it.name } + } + + SongSortType.PLAY_TIME -> songs.sortedBy { it.song.totalPlayTime } + }.reversed(descending) + } + } + } }.stateIn(viewModelScope, SharingStarted.Lazily, emptyList()) } @@ -45,22 +76,22 @@ class LibraryArtistsViewModel @Inject constructor( val allArtists = context.dataStore.data .map { Triple( - it[ArtistViewTypeKey].toEnum(ArtistViewType.ALL), + it[ArtistFilterKey].toEnum(ArtistFilter.LIBRARY), it[ArtistSortTypeKey].toEnum(ArtistSortType.CREATE_DATE), it[ArtistSortDescendingKey] ?: true ) } .distinctUntilChanged() - .flatMapLatest { (viewType, sortType, descending) -> - when (viewType) { - ArtistViewType.ALL -> database.artists(sortType, descending) - ArtistViewType.BOOKMARKED -> database.artistsBookmarked(sortType, descending) + .flatMapLatest { (filter, sortType, descending) -> + when (filter) { + ArtistFilter.LIBRARY -> database.artists(sortType, descending) + ArtistFilter.LIKED -> database.artistsBookmarked(sortType, descending) } } .stateIn(viewModelScope, SharingStarted.Lazily, emptyList()) init { - viewModelScope.launch { + viewModelScope.launch(Dispatchers.IO) { allArtists.collect { artists -> artists .map { it.artist } @@ -86,30 +117,50 @@ class LibraryAlbumsViewModel @Inject constructor( ) : ViewModel() { val allAlbums = context.dataStore.data .map { - it[AlbumSortTypeKey].toEnum(AlbumSortType.CREATE_DATE) to (it[AlbumSortDescendingKey] ?: true) + Triple( + it[AlbumFilterKey].toEnum(AlbumFilter.LIBRARY), + it[AlbumSortTypeKey].toEnum(AlbumSortType.CREATE_DATE), + it[AlbumSortDescendingKey] ?: true + ) } .distinctUntilChanged() - .flatMapLatest { (sortType, descending) -> - database.albums(sortType, descending) + .flatMapLatest { (filter, sortType, descending) -> + when (filter) { + AlbumFilter.LIBRARY -> database.albums(sortType, descending) + AlbumFilter.LIKED -> database.albumsLiked(sortType, descending) + } } .stateIn(viewModelScope, SharingStarted.Lazily, emptyList()) + + init { + viewModelScope.launch(Dispatchers.IO) { + allAlbums.collect { albums -> + albums.filter { + it.album.songCount == 0 + }.forEach { album -> + YouTube.album(album.id).onSuccess { albumPage -> + database.query { + update(album.album, albumPage) + } + }.onFailure { + reportException(it) + if (it.message?.contains("NOT_FOUND") == true) { + database.query { + delete(album.album) + } + } + } + } + } + } + } } @HiltViewModel class LibraryPlaylistsViewModel @Inject constructor( @ApplicationContext context: Context, database: MusicDatabase, - downloadUtil: DownloadUtil, ) : ViewModel() { - val likedSongCount = database.likedSongsCount() - .stateIn(viewModelScope, SharingStarted.Lazily, 0) - - val downloadedSongCount = downloadUtil.downloads.map { - it.count { (_, download) -> - download.state == STATE_COMPLETED - } - } - val allPlaylists = context.dataStore.data .map { it[PlaylistSortTypeKey].toEnum(PlaylistSortType.CREATE_DATE) to (it[PlaylistSortDescendingKey] ?: true) diff --git a/app/src/main/java/com/zionhuang/music/viewmodels/MoodAndGenresViewModel.kt b/app/src/main/java/com/zionhuang/music/viewmodels/MoodAndGenresViewModel.kt index ea2a29d51..c764cc28d 100644 --- a/app/src/main/java/com/zionhuang/music/viewmodels/MoodAndGenresViewModel.kt +++ b/app/src/main/java/com/zionhuang/music/viewmodels/MoodAndGenresViewModel.kt @@ -4,6 +4,7 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.zionhuang.innertube.YouTube import com.zionhuang.innertube.pages.MoodAndGenres +import com.zionhuang.music.utils.reportException import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.launch @@ -18,7 +19,7 @@ class MoodAndGenresViewModel @Inject constructor() : ViewModel() { YouTube.moodAndGenres().onSuccess { moodAndGenres.value = it }.onFailure { - it.printStackTrace() + reportException(it) } } } diff --git a/app/src/main/java/com/zionhuang/music/viewmodels/NewReleaseViewModel.kt b/app/src/main/java/com/zionhuang/music/viewmodels/NewReleaseViewModel.kt index 8770dfe64..fa30406bd 100644 --- a/app/src/main/java/com/zionhuang/music/viewmodels/NewReleaseViewModel.kt +++ b/app/src/main/java/com/zionhuang/music/viewmodels/NewReleaseViewModel.kt @@ -4,6 +4,7 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.zionhuang.innertube.YouTube import com.zionhuang.innertube.models.AlbumItem +import com.zionhuang.music.utils.reportException import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow @@ -20,7 +21,7 @@ class NewReleaseViewModel @Inject constructor() : ViewModel() { YouTube.newReleaseAlbums().onSuccess { _newReleaseAlbums.value = it }.onFailure { - it.printStackTrace() + reportException(it) } } } diff --git a/app/src/main/java/com/zionhuang/music/viewmodels/OnlinePlaylistViewModel.kt b/app/src/main/java/com/zionhuang/music/viewmodels/OnlinePlaylistViewModel.kt index cd42a4676..f3b6e5589 100644 --- a/app/src/main/java/com/zionhuang/music/viewmodels/OnlinePlaylistViewModel.kt +++ b/app/src/main/java/com/zionhuang/music/viewmodels/OnlinePlaylistViewModel.kt @@ -7,6 +7,7 @@ import com.zionhuang.innertube.YouTube import com.zionhuang.innertube.models.PlaylistItem import com.zionhuang.innertube.models.SongItem import com.zionhuang.innertube.utils.completed +import com.zionhuang.music.utils.reportException import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow @@ -28,8 +29,8 @@ class OnlinePlaylistViewModel @Inject constructor( .onSuccess { playlistPage -> playlist.value = playlistPage.playlist playlistSongs.value = playlistPage.songs - }.onFailure { e -> - e.printStackTrace() + }.onFailure { + reportException(it) } } } diff --git a/app/src/main/java/com/zionhuang/music/viewmodels/StatsViewModel.kt b/app/src/main/java/com/zionhuang/music/viewmodels/StatsViewModel.kt index d8dda558d..b68686009 100644 --- a/app/src/main/java/com/zionhuang/music/viewmodels/StatsViewModel.kt +++ b/app/src/main/java/com/zionhuang/music/viewmodels/StatsViewModel.kt @@ -5,12 +5,12 @@ import androidx.lifecycle.viewModelScope import com.zionhuang.innertube.YouTube import com.zionhuang.music.constants.StatPeriod import com.zionhuang.music.db.MusicDatabase +import com.zionhuang.music.utils.reportException import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.flatMapLatest -import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import java.time.Duration @@ -35,10 +35,6 @@ class StatsViewModel @Inject constructor( val mostPlayedAlbums = statPeriod.flatMapLatest { period -> database.mostPlayedAlbums(period.toTimeMillis()) - }.map { albums -> - albums.mapNotNull { id -> - YouTube.album(id, withSongs = false).getOrNull()?.album - } }.stateIn(viewModelScope, SharingStarted.Lazily, emptyList()) init { @@ -58,5 +54,25 @@ class StatsViewModel @Inject constructor( } } } + viewModelScope.launch { + mostPlayedAlbums.collect { albums -> + albums.filter { + it.album.songCount == 0 + }.forEach { album -> + YouTube.album(album.id).onSuccess { albumPage -> + database.query { + update(album.album, albumPage) + } + }.onFailure { + reportException(it) + if (it.message?.contains("NOT_FOUND") == true) { + database.query { + delete(album.album) + } + } + } + } + } + } } } diff --git a/app/src/main/java/com/zionhuang/music/viewmodels/YouTubeBrowseViewModel.kt b/app/src/main/java/com/zionhuang/music/viewmodels/YouTubeBrowseViewModel.kt index 2e53332d3..1627041b2 100644 --- a/app/src/main/java/com/zionhuang/music/viewmodels/YouTubeBrowseViewModel.kt +++ b/app/src/main/java/com/zionhuang/music/viewmodels/YouTubeBrowseViewModel.kt @@ -5,6 +5,7 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.zionhuang.innertube.YouTube import com.zionhuang.innertube.pages.BrowseResult +import com.zionhuang.music.utils.reportException import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.launch @@ -24,7 +25,7 @@ class YouTubeBrowseViewModel @Inject constructor( YouTube.browse(browseId, params).onSuccess { result.value = it }.onFailure { - it.printStackTrace() + reportException(it) } } } diff --git a/app/src/main/res/drawable/discover_tune.xml b/app/src/main/res/drawable/discover_tune.xml new file mode 100644 index 000000000..fa2974502 --- /dev/null +++ b/app/src/main/res/drawable/discover_tune.xml @@ -0,0 +1,9 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="960" + android:viewportHeight="960"> + <path + android:fillColor="@android:color/white" + android:pathData="M517.5,357.5L517.5,282.5L637.5,282.5L637.5,127.5L712.5,127.5L712.5,282.5L832.5,282.5L832.5,357.5L517.5,357.5ZM637.5,832.5L637.5,442.5L712.5,442.5L712.5,832.5L637.5,832.5ZM247.5,832.5L247.5,677.5L127.5,677.5L127.5,602.5L442.5,602.5L442.5,677.5L322.5,677.5L322.5,832.5L247.5,832.5ZM247.5,517.5L247.5,127.5L322.5,127.5L322.5,517.5L247.5,517.5Z"/> +</vector> diff --git a/app/src/main/res/drawable/translate.xml b/app/src/main/res/drawable/translate.xml new file mode 100644 index 000000000..c1d325b9f --- /dev/null +++ b/app/src/main/res/drawable/translate.xml @@ -0,0 +1,9 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="960" + android:viewportHeight="960"> + <path + android:fillColor="@android:color/white" + android:pathData="M476,880L658,400L742,400L924,880L840,880L798,758L604,758L560,880L476,880ZM628,688L772,688L702,490L698,490L628,688ZM160,760L104,704L306,502Q268,460 239.5,415Q211,370 190,320L274,320Q292,356 312.5,385Q333,414 362,446Q406,398 435,347.5Q464,297 484,240L40,240L40,160L320,160L320,80L400,80L400,160L680,160L680,240L564,240Q543,311 507,378Q471,445 418,504L514,602L484,684L360,560L160,760Z" /> +</vector> diff --git a/app/src/main/res/values-DE/strings.xml b/app/src/main/res/values-DE/strings.xml index adc998ec7..0c520dfb7 100644 --- a/app/src/main/res/values-DE/strings.xml +++ b/app/src/main/res/values-DE/strings.xml @@ -36,6 +36,9 @@ <string name="search">Suche</string> <string name="search_yt_music">YouTube Musik durchsuchen…</string> <string name="search_library">Bibliothek durchsuchen…</string> + <string name="filter_library">Library</string> + <string name="filter_liked">Liked</string> + <string name="filter_downloaded">Downloaded</string> <string name="filter_all">Alles</string> <string name="filter_songs">Titel</string> <string name="filter_videos">Videos</string> @@ -59,6 +62,7 @@ <string name="retry">Wiederholen</string> <string name="radio">Radio</string> <string name="shuffle">Shuffle</string> + <string name="reset">Reset</string> <!-- Menu --> <string name="details">Details</string> @@ -82,6 +86,7 @@ <string name="remove_from_history">aus dem Wiedergabeverlauf entfernen</string> <string name="search_online">Online-Suche</string> <string name="sync">Sync</string> + <string name="advanced">Advanced</string> <!-- Sort menu --> <string name="sort_by_create_date">Datum hinzugefügt</string> @@ -255,4 +260,6 @@ <string name="app_version">App-Version</string> <string name="new_version_available">New version available</string> + <string name="translation_models">Translation Models</string> + <string name="clear_translation_models">Clear translation models</string> </resources> diff --git a/app/src/main/res/values-be/strings.xml b/app/src/main/res/values-be/strings.xml index 5e4f5bfec..e2b2ff4cf 100644 --- a/app/src/main/res/values-be/strings.xml +++ b/app/src/main/res/values-be/strings.xml @@ -38,6 +38,9 @@ <string name="search">Пошук</string> <string name="search_yt_music">Пошук у YouTube Music…</string> <string name="search_library">Пошук у бібліятэцы…</string> + <string name="filter_library">Library</string> + <string name="filter_liked">Liked</string> + <string name="filter_downloaded">Downloaded</string> <string name="filter_all">Усе</string> <string name="filter_songs">Песні</string> <string name="filter_videos">Відэа</string> @@ -61,6 +64,7 @@ <string name="retry">Паўтарыць</string> <string name="radio">Радыё</string> <string name="shuffle">Перамяшаць</string> + <string name="reset">Reset</string> <!-- Menu --> <string name="details">Падрабязнасці</string> @@ -84,6 +88,7 @@ <string name="remove_from_history">Выдаліць з гісторыі</string> <string name="search_online">Шукаць у сетцы</string> <string name="sync">Cінхранізацыя</string> + <string name="advanced">Advanced</string> <!-- Sort menu --> <string name="sort_by_create_date">Нядаўна дададзена</string> @@ -273,4 +278,6 @@ <string name="app_version">Версія праграмы</string> <string name="new_version_available">New version available</string> + <string name="translation_models">Translation Models</string> + <string name="clear_translation_models">Clear translation models</string> </resources> diff --git a/app/src/main/res/values-bn-rIN/strings.xml b/app/src/main/res/values-bn-rIN/strings.xml index 9088e7ba1..e7c8642b0 100644 --- a/app/src/main/res/values-bn-rIN/strings.xml +++ b/app/src/main/res/values-bn-rIN/strings.xml @@ -36,6 +36,9 @@ <string name="search">খুঁজুন</string> <string name="search_yt_music">খুঁজুন YouTube Music এ</string> <string name="search_library">খুঁজুন লাইব্রেরি তে</string> + <string name="filter_library">Library</string> + <string name="filter_liked">Liked</string> + <string name="filter_downloaded">Downloaded</string> <string name="filter_all">সব</string> <string name="filter_songs">সংগীত</string> <string name="filter_videos">ভিডিও</string> @@ -59,6 +62,7 @@ <string name="retry">পুনরায় চেষ্টা করুন</string> <string name="radio">বেতার</string> <string name="shuffle">এলোমেলো</string> + <string name="reset">Reset</string> <!-- Menu --> <string name="details">বিস্তারিত</string> @@ -82,6 +86,7 @@ <string name="remove_from_history">ইতিহাস থেকে অপসারণ</string> <string name="search_online">অনলাইন এ খুঁজুন</string> <string name="sync">সুসংগত</string> + <string name="advanced">Advanced</string> <!-- Sort menu --> <string name="sort_by_create_date">সময় সম্পাদনা করা হয়েছে</string> @@ -255,4 +260,6 @@ <string name="app_version">অ্যাপ সংস্করণ</string> <string name="new_version_available">New version available</string> + <string name="translation_models">Translation Models</string> + <string name="clear_translation_models">Clear translation models</string> </resources> diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index a7642851e..029bd1a7f 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -38,6 +38,9 @@ <string name="search">Vyhledávání</string> <string name="search_yt_music">Hledat v YouTube Music…</string> <string name="search_library">Hledat v knihovně…</string> + <string name="filter_library">Library</string> + <string name="filter_liked">Liked</string> + <string name="filter_downloaded">Downloaded</string> <string name="filter_all">Vše</string> <string name="filter_songs">Skladby</string> <string name="filter_videos">Videa</string> @@ -61,6 +64,7 @@ <string name="retry">Zkusit znovu</string> <string name="radio">Rádio</string> <string name="shuffle">Náhodně</string> + <string name="reset">Reset</string> <!-- Menu --> <string name="details">Podrobnosti</string> @@ -84,6 +88,7 @@ <string name="remove_from_history">Remove from history</string> <string name="search_online">Hledat online</string> <string name="sync">Sync</string> + <string name="advanced">Advanced</string> <!-- Sort menu --> <string name="sort_by_create_date">Datum přidání</string> @@ -267,4 +272,6 @@ <string name="app_version">Verze aplikace</string> <string name="new_version_available">New version available</string> + <string name="translation_models">Translation Models</string> + <string name="clear_translation_models">Clear translation models</string> </resources> diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 90924cfd5..323f1b0a4 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -37,6 +37,9 @@ <string name="search">Buscar</string> <string name="search_yt_music">Buscar en Youtube Music…</string> <string name="search_library">Buscar en la biblioteca…</string> + <string name="filter_library">Library</string> + <string name="filter_liked">Liked</string> + <string name="filter_downloaded">Downloaded</string> <string name="filter_all">Todo</string> <string name="filter_songs">Canciones</string> <string name="filter_videos">Vídeos</string> @@ -60,6 +63,7 @@ <string name="retry">Reintentar</string> <string name="radio">Radio</string> <string name="shuffle">Mezclar</string> + <string name="reset">Reset</string> <!-- Menu --> <string name="details">Detalles</string> @@ -83,6 +87,7 @@ <string name="remove_from_history">Eliminar del historial</string> <string name="search_online">Buscar online</string> <string name="sync">Sincronizar</string> + <string name="advanced">Advanced</string> <!-- Sort menu --> <string name="sort_by_create_date">Fecha añadida</string> @@ -264,4 +269,6 @@ <string name="app_version">Versión de la app</string> <string name="new_version_available">New version available</string> + <string name="translation_models">Translation Models</string> + <string name="clear_translation_models">Clear translation models</string> </resources> diff --git a/app/src/main/res/values-fa-rIR/strings.xml b/app/src/main/res/values-fa-rIR/strings.xml index 152449763..1cddb06c6 100644 --- a/app/src/main/res/values-fa-rIR/strings.xml +++ b/app/src/main/res/values-fa-rIR/strings.xml @@ -36,6 +36,9 @@ <string name="search">جستجو</string> <string name="search_yt_music">جستجو در یوتیوب موزیک…</string> <string name="search_library">جستجوی کتابخانه…</string> + <string name="filter_library">Library</string> + <string name="filter_liked">Liked</string> + <string name="filter_downloaded">Downloaded</string> <string name="filter_all">همه</string> <string name="filter_songs">آهنگها</string> <string name="filter_videos">فیلمها</string> @@ -59,6 +62,7 @@ <string name="retry">تلاشمجدد</string> <string name="radio">رادیو</string> <string name="shuffle">بُرزدن</string> + <string name="reset">Reset</string> <!-- Menu --> <string name="details">Details</string> @@ -82,6 +86,7 @@ <string name="remove_from_history">Remove from history</string> <string name="search_online">Search online</string> <string name="sync">Sync</string> + <string name="advanced">Advanced</string> <!-- Sort menu --> <string name="sort_by_create_date">تاریخ اضافهشده</string> @@ -255,4 +260,6 @@ <string name="app_version">نسخهی برنامه</string> <string name="new_version_available">New version available</string> + <string name="translation_models">Translation Models</string> + <string name="clear_translation_models">Clear translation models</string> </resources> diff --git a/app/src/main/res/values-fi-rFI/strings.xml b/app/src/main/res/values-fi-rFI/strings.xml index 2e830364e..25c20df95 100644 --- a/app/src/main/res/values-fi-rFI/strings.xml +++ b/app/src/main/res/values-fi-rFI/strings.xml @@ -36,6 +36,9 @@ <string name="search">Etsi</string> <string name="search_yt_music">Search YouTube Music…</string> <string name="search_library">Search library…</string> + <string name="filter_library">Library</string> + <string name="filter_liked">Liked</string> + <string name="filter_downloaded">Downloaded</string> <string name="filter_all">Kaikki</string> <string name="filter_songs">Kappaleet</string> <string name="filter_videos">Videot</string> @@ -59,6 +62,7 @@ <string name="retry">Toisto</string> <string name="radio">Radio</string> <string name="shuffle">Sekoita</string> + <string name="reset">Reset</string> <!-- Menu --> <string name="details">Details</string> @@ -82,6 +86,7 @@ <string name="remove_from_history">Remove from history</string> <string name="search_online">Search online</string> <string name="sync">Sync</string> + <string name="advanced">Advanced</string> <!-- Sort menu --> <string name="sort_by_create_date">Lisäyspäivä</string> @@ -255,4 +260,6 @@ <string name="app_version">Sovelluksen versio</string> <string name="new_version_available">New version available</string> + <string name="translation_models">Translation Models</string> + <string name="clear_translation_models">Clear translation models</string> </resources> diff --git a/app/src/main/res/values-fr-rFR/strings.xml b/app/src/main/res/values-fr-rFR/strings.xml index 342970be2..4afb606e5 100644 --- a/app/src/main/res/values-fr-rFR/strings.xml +++ b/app/src/main/res/values-fr-rFR/strings.xml @@ -1,10 +1,10 @@ -<resources xmlns:tools="http://schemas.android.com/tools"> +<resources> <!-- Bottom navigation --> - <string name="home">Home</string> + <string name="home">Accueil</string> <string name="songs">Chansons</string> <string name="artists">Artistes</string> <string name="albums">Albums</string> - <string name="playlists">Listes de lecture</string> + <string name="playlists">Listes de cours</string> <!-- Top bar --> <plurals name="n_selected"> @@ -14,102 +14,107 @@ </plurals> <!-- Home --> - <string name="history">History</string> - <string name="stats">Stats</string> - <string name="mood_and_genres">Mood and Genres</string> - <string name="account">Account</string> - <string name="quick_picks">Quick picks</string> - <string name="quick_picks_empty">Listen to songs to generate your quick picks</string> - <string name="new_release_albums">New release albums</string> + <string name="history">Historique</string> + <string name="stats">Statistiques</string> + <string name="mood_and_genres">Humeurs et genres</string> + <string name="account">Compte</string> + <string name="quick_picks">Sélections rapides</string> + <string name="quick_picks_empty">Écoutez quelques musiques pour générer vos sélections rapides</string> + <string name="new_release_albums">Nouveautés</string> <!-- History --> - <string name="today">Today</string> - <string name="yesterday">Yesterday</string> - <string name="this_week">This week</string> - <string name="last_week">Last week</string> + <string name="today">Aujourd\'hui</string> + <string name="yesterday">Hier</string> + <string name="this_week">Cette semaine</string> + <string name="last_week">Semaine dernière</string> <!-- Stats --> - <string name="most_played_songs">Most played songs</string> - <string name="most_played_artists">Most played artists</string> - <string name="most_played_albums">Most played albums</string> + <string name="most_played_songs">Musiques les plus jouées</string> + <string name="most_played_artists">Artistes les plus joués</string> + <string name="most_played_albums">Albums les plus joués</string> <!-- Search --> - <string name="search">Recherche</string> - <string name="search_yt_music">Search YouTube Music…</string> - <string name="search_library">Search library…</string> + <string name="search">Recherché</string> + <string name="search_yt_music">Rechercher sur Youtube Musique…</string> + <string name="search_library">Rechercher dans votre librairie…</string> + <string name="filter_library">Librairie</string> + <string name="filter_liked">Aimé</string> + <string name="filter_downloaded">Télécharger</string> <string name="filter_all">Tout</string> <string name="filter_songs">Chansons</string> <string name="filter_videos">Vidéos</string> <string name="filter_albums">Albums</string> <string name="filter_artists">Artistes</string> - <string name="filter_playlists">Listes de lecture</string> - <string name="filter_community_playlists">Community playlists</string> - <string name="filter_featured_playlists">Featured playlists</string> - <string name="filter_bookmarked">Bookmarked</string> - <string name="no_results_found">No results found</string> + <string name="filter_playlists">Playlist</string> + <string name="filter_community_playlists">Playlist de la communauté</string> + <string name="filter_featured_playlists">Playlist mise en avant</string> + <string name="filter_bookmarked">Favoris</string> + <string name="no_results_found">Aucun résultat trouvé</string> <!-- Artist screen --> - <string name="from_your_library">From your library</string> + <string name="from_your_library">De votre bibliothèque</string> <!-- Playlist --> - <string name="liked_songs">Liked songs</string> - <string name="downloaded_songs">Downloaded songs</string> - <string name="playlist_is_empty">The playlist is empty</string> + <string name="liked_songs">Chansons aimées</string> + <string name="downloaded_songs">Musiques téléchargées</string> + <string name="playlist_is_empty">La playlist est vide</string> <!-- Button --> <string name="retry">Réessayer</string> <string name="radio">Radio</string> <string name="shuffle">Lecture aléatoire</string> + <string name="reset">Reset</string> <!-- Menu --> - <string name="details">Details</string> + <string name="details">Détails</string> <string name="edit">Modifier</string> - <string name="start_radio">Start radio</string> - <string name="play">Play</string> + <string name="start_radio">Démarrer la radio</string> + <string name="play">Jouer</string> <string name="play_next">Jouer à la suite</string> <string name="add_to_queue">Ajouter à la file d\'attente</string> <string name="add_to_library">Ajouter à la bibliothèque</string> - <string name="remove_from_library">Remove from library</string> + <string name="remove_from_library">Supprimer de la bibliothèque</string> <string name="download">Télécharger</string> - <string name="downloading">Downloading</string> + <string name="downloading">Téléchargement</string> <string name="remove_download">Supprimer le téléchargement</string> - <string name="import_playlist">Import playlist</string> + <string name="import_playlist">Importer une liste de lecture</string> <string name="add_to_playlist">Ajouter à la liste de lecture</string> - <string name="view_artist">View artist</string> - <string name="view_album">View album</string> - <string name="refetch">Refetch</string> - <string name="share">Share</string> - <string name="delete">Effacer</string> - <string name="remove_from_history">Remove from history</string> - <string name="search_online">Search online</string> - <string name="sync">Sync</string> + <string name="view_artist">Voir l\'artiste</string> + <string name="view_album">Voir l\'album</string> + <string name="refetch">Récupérer</string> + <string name="share">Partager</string> + <string name="delete">Effaceur</string> + <string name="remove_from_history">Supprimer de l\'historique</string> + <string name="search_online">Rechercher en ligne</string> + <string name="sync">Synchroniser</string> + <string name="advanced">Advanced</string> <!-- Sort menu --> <string name="sort_by_create_date">Date d\'ajout</string> <string name="sort_by_name">Nom</string> <string name="sort_by_artist">Artiste</string> - <string name="sort_by_year">Year</string> - <string name="sort_by_song_count">Song count</string> - <string name="sort_by_length">Length</string> - <string name="sort_by_play_time">Play time</string> - <string name="sort_by_custom">Custom order</string> + <string name="sort_by_year">Année</string> + <string name="sort_by_song_count">Nombre de chansons</string> + <string name="sort_by_length">Longueur</string> + <string name="sort_by_play_time">Récréation</string> + <string name="sort_by_custom">Commande personnalisée</string> <!-- Dialog --> - <string name="media_id">Media id</string> - <string name="mime_type">MIME type</string> + <string name="media_id">Identifiant du média</string> + <string name="mime_type">Type MIME</string> <string name="codecs">Codecs</string> - <string name="bitrate">Bitrate</string> - <string name="sample_rate">Sample rate</string> - <string name="loudness">Loudness</string> + <string name="bitrate">Débit</string> + <string name="sample_rate">Taux d\'échantillonnage</string> + <string name="loudness">Intensité</string> <string name="volume">Volume</string> - <string name="file_size">File size</string> - <string name="unknown">Unknown</string> - <string name="copied">Copied to clipboard</string> + <string name="file_size">Taille du fichier</string> + <string name="unknown">Inconnu</string> + <string name="copied">Copié dans le presse-papier</string> - <string name="edit_lyrics">Edit lyrics</string> - <string name="search_lyrics">Search lyrics</string> + <string name="edit_lyrics">Modifier les paroles</string> + <string name="search_lyrics">Rechercher des paroles</string> - <string name="edit_song">Editer la chanson</string> + <string name="edit_song">Éditer la chanson</string> <string name="song_title">Titre de la chanson</string> <string name="song_artists">Artiste de la chanson</string> <string name="error_song_title_empty">Le titre de la chanson ne peut pas être vide.</string> @@ -117,12 +122,12 @@ <string name="save">Sauvegarder</string> <string name="choose_playlist">Choisir la liste de lecture</string> - <string name="edit_playlist">Editer la liste de lecture</string> + <string name="edit_playlist">Editer la playlist</string> <string name="create_playlist">Créer une liste de lecture</string> - <string name="playlist_name">Nom de la liste de lecture</string> - <string name="error_playlist_name_empty">Le nom de la liste de lecture ne peut pas être vide.</string> + <string name="playlist_name">Nom de la playlist</string> + <string name="error_playlist_name_empty">Le nom de la playlist ne peut pas être vide.</string> - <string name="edit_artist">Editer l\'artiste</string> + <string name="edit_artist">Éditer l\'artiste</string> <string name="artist_name">Nom de l\'artiste</string> <string name="error_artist_name_empty">Le nom de l\'artiste ne peut être vide.</string> @@ -145,120 +150,125 @@ <plurals name="n_playlist"> <item quantity="one">%d playlist</item> <item quantity="other">%d playlists</item> - <item quantity="many">%d albums</item> + <item quantity="many">%d playlists</item> </plurals> - <plurals name="n_week" tools:ignore="MissingQuantity"> - <item quantity="one">%d week</item> - <item quantity="other">%d weeks</item> + <plurals name="n_week"> + <item quantity="one">%d semaine</item> + <item quantity="many">%d semaines</item> + <item quantity="other">%d semaines</item> </plurals> - <plurals name="n_month" tools:ignore="MissingQuantity"> - <item quantity="one">%d month</item> - <item quantity="other">%d months</item> + <plurals name="n_month"> + <item quantity="one">%d mois</item> + <item quantity="many">%d mois</item> + <item quantity="other">%d mois</item> </plurals> - <plurals name="n_year" tools:ignore="MissingQuantity"> - <item quantity="one">%d year</item> - <item quantity="other">%d years</item> + <plurals name="n_year"> + <item quantity="one">%d ans</item> + <item quantity="many">%d ans</item> + <item quantity="other">%d ans</item> </plurals> <!-- Snackbar --> - <string name="playlist_imported">Playlist imported</string> - <string name="removed_song_from_playlist">Removed \"%s\" from playlist</string> - <string name="playlist_synced">Playlist synced</string> - <string name="undo">Undo</string> + <string name="playlist_imported">Liste de lecture importée</string> + <string name="removed_song_from_playlist">\"%s\" supprimé de la liste de lecture</string> + <string name="playlist_synced">Liste de lecture synchronisée</string> + <string name="undo">annuler</string> <!-- Player --> - <string name="lyrics_not_found">Lyrics not found</string> - <string name="sleep_timer">Sleep timer</string> - <string name="end_of_song">End of song</string> + <string name="lyrics_not_found">Paroles introuvables</string> + <string name="sleep_timer">Minuterie de sommeil</string> + <string name="end_of_song">Fin de chanson</string> <plurals name="minute"> - <item quantity="one" tools:ignore="ImpliedQuantity">1 minute</item> + <item quantity="one">%d minute</item> <item quantity="many">%d minutes</item> <item quantity="other">%d minutes</item> </plurals> - <string name="error_no_stream">No stream available</string> - <string name="error_no_internet">No network connection</string> - <string name="error_timeout">Timeout</string> - <string name="error_unknown">Unknown error</string> + <string name="error_no_stream">Aucun flux disponible</string> + <string name="error_no_internet">Pas de connexion réseau</string> + <string name="error_timeout">Temps libre</string> + <string name="error_unknown">Erreur inconnue</string> <!-- Player action --> <string name="action_like">Like</string> - <string name="action_remove_like">Remove like</string> + <string name="action_remove_like">Enlever le like</string> <!-- Queue Title --> - <string name="queue_all_songs">All songs</string> - <string name="queue_searched_songs">Searched songs</string> + <string name="queue_all_songs">Toutes les chansons</string> + <string name="queue_searched_songs">Chansons recherchées</string> <!-- Notification name --> <string name="music_player">Lecteur de musique</string> <!-- Settings --> <string name="settings">Paramètres</string> - <string name="appearance">Appearance</string> - <string name="enable_dynamic_theme">Enable dynamic theme</string> - <string name="dark_theme">Dark theme</string> - <string name="dark_theme_on">On</string> - <string name="dark_theme_off">Off</string> - <string name="dark_theme_follow_system">Follow system</string> - <string name="pure_black">Pure black</string> - <string name="default_open_tab">Default open tab</string> - <string name="customize_navigation_tabs">Customize navigation tabs</string> - <string name="lyrics_text_position">Lyrics text position</string> - <string name="left">Left</string> - <string name="center">Center</string> - <string name="right">Right</string> + <string name="appearance">Apparence</string> + <string name="enable_dynamic_theme">Activer le thème dynamique</string> + <string name="dark_theme">Thème sombre</string> + <string name="dark_theme_on">Activé</string> + <string name="dark_theme_off">Désactivé</string> + <string name="dark_theme_follow_system">Suivre le système</string> + <string name="pure_black">Noir pur</string> + <string name="default_open_tab">Menu ouvert par défaut</string> + <string name="customize_navigation_tabs">Personnaliser les menus de navigation</string> + <string name="lyrics_text_position">Position du texte des paroles</string> + <string name="left">Gauche</string> + <string name="center">Centre</string> + <string name="right">Droite</string> - <string name="content">Content</string> - <string name="login">Login</string> + <string name="content">Contenu</string> + <string name="login">Connexion</string> <string name="content_language">Langue du contenu par défaut</string> <string name="content_country">Pays du contenu par défaut</string> <string name="system_default">Système par défaut</string> - <string name="enable_proxy">Enable proxy</string> - <string name="proxy_type">Proxy type</string> - <string name="proxy_url">Proxy URL</string> - <string name="restart_to_take_effect">Restart to take effect</string> + <string name="enable_proxy">Activer le proxy</string> + <string name="proxy_type">Type de proxy</string> + <string name="proxy_url">URL proxy</string> + <string name="restart_to_take_effect">Redémarrer pour prendre effet</string> - <string name="player_and_audio">Player and audio</string> - <string name="audio_quality">Audio quality</string> + <string name="player_and_audio">Lecteur et audio</string> + <string name="audio_quality">Qualité audio</string> <string name="audio_quality_auto">Auto</string> - <string name="audio_quality_high">High</string> - <string name="audio_quality_low">Low</string> - <string name="persistent_queue">Persistent queue</string> - <string name="skip_silence">Skip silence</string> - <string name="audio_normalization">Audio normalization</string> - <string name="equalizer">Equalizer</string> + <string name="audio_quality_high">Haut</string> + <string name="audio_quality_low">Faible</string> + <string name="persistent_queue">File d\'attente persistante</string> + <string name="skip_silence">Ignorer le silence</string> + <string name="audio_normalization">Normalisation audio</string> + <string name="equalizer">Égaliseur</string> - <string name="storage">Storage</string> + <string name="storage">Stockage</string> <string name="cache">Cache</string> - <string name="image_cache">Image Cache</string> - <string name="song_cache">Song Cache</string> - <string name="max_cache_size">Max cache size</string> - <string name="unlimited">Unlimited</string> - <string name="clear_all_downloads">Clear all downloads</string> - <string name="max_image_cache_size">Max image cache size</string> - <string name="clear_image_cache">Clear image cache</string> - <string name="max_song_cache_size">Max song cache size</string> - <string name="clear_song_cache">Clear song cache</string> - <string name="size_used">%s used</string> + <string name="image_cache">Cache d\'images</string> + <string name="song_cache">Cache de chanson</string> + <string name="max_cache_size">Taille maximale du cache</string> + <string name="unlimited">Illimité</string> + <string name="clear_all_downloads">Effacer tous les téléchargements</string> + <string name="max_image_cache_size">Taille maximale du cache d\'images</string> + <string name="clear_image_cache">Effacer le cache des images</string> + <string name="max_song_cache_size">Taille maximale du cache de chansons</string> + <string name="clear_song_cache">Effacer le cache de la chanson</string> + <string name="size_used">%s utilisé</string> - <string name="privacy">Privacy</string> - <string name="pause_listen_history">Pause listen history</string> - <string name="clear_listen_history">Clear listen history</string> - <string name="clear_listen_history_confirm">Are you sure to clear all listen history?</string> - <string name="pause_search_history">Pause search history</string> - <string name="clear_search_history">Clear search history</string> - <string name="clear_search_history_confirm">Are you sure to clear all search history?</string> - <string name="enable_kugou">Enable KuGou lyrics provider</string> + <string name="privacy">Confidentialité</string> + <string name="pause_listen_history">Suspendre l\'historique d\'écoute</string> + <string name="clear_listen_history">Effacer l\'historique d\'écoute</string> + <string name="clear_listen_history_confirm">Voulez-vous vraiment effacer tout l\'historique d\'écoute?</string> + <string name="pause_search_history">Suspendre l\'historique des recherches</string> + <string name="clear_search_history">Effacer l\'historique</string> + <string name="clear_search_history_confirm">Voulez-vous vraiment effacer tout l\'historique de recherche?</string> + <string name="enable_kugou">Activer le fournisseur de paroles KuGou</string> - <string name="backup_restore">Backup and restore</string> - <string name="backup">Backup</string> - <string name="restore">Restore</string> - <string name="imported_playlist">Imported playlist</string> - <string name="backup_create_success">Backup created successfully</string> - <string name="backup_create_failed">Couldn\'t create backup</string> - <string name="restore_failed">Failed to restore backup</string> + <string name="backup_restore">Sauvegarde et restauration</string> + <string name="backup">Sauvegarde</string> + <string name="restore">Restaurer</string> + <string name="imported_playlist">Liste de lecture importée</string> + <string name="backup_create_success">Sauvegarde créée avec succès</string> + <string name="backup_create_failed">Impossible de créer la sauvegarde</string> + <string name="restore_failed">Échec de la restauration de la sauvegarde</string> - <string name="about">À propos</string> + <string name="about">À propos de</string> <string name="app_version">Version de l\'application</string> - <string name="new_version_available">New version available</string> + <string name="new_version_available">Nouvelle version disponible</string> + <string name="translation_models">Translation Models</string> + <string name="clear_translation_models">Clear translation models</string> </resources> diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml index cc35c1078..d6eeefef4 100644 --- a/app/src/main/res/values-hu/strings.xml +++ b/app/src/main/res/values-hu/strings.xml @@ -15,8 +15,8 @@ <!-- Home --> <string name="history">Előzmény</string> <string name="stats">Statisztika</string> - <string name="mood_and_genres">Mood and Genres</string> - <string name="account">Account</string> + <string name="mood_and_genres">Hangulat és műfajok</string> + <string name="account">Fiók</string> <string name="quick_picks">Gyors választás</string> <string name="quick_picks_empty">Hallgasson meg néhány dalt a gyors választások elkészítéséhez</string> <string name="new_release_albums">Új kiadású albumok</string> @@ -36,6 +36,9 @@ <string name="search">Keresés</string> <string name="search_yt_music">YouTube Music keresés…</string> <string name="search_library">Keresés könyvtárban…</string> + <string name="filter_library">Könyvtár</string> + <string name="filter_liked">Lájkolt</string> + <string name="filter_downloaded">Letöltött</string> <string name="filter_all">Mind</string> <string name="filter_songs">Dalok</string> <string name="filter_videos">Videók</string> @@ -44,7 +47,7 @@ <string name="filter_playlists">Listák</string> <string name="filter_community_playlists">Közösségi listák</string> <string name="filter_featured_playlists">Kiemelt lejátszási listák</string> - <string name="filter_bookmarked">Bookmarked</string> + <string name="filter_bookmarked">Könyvjelzőzött</string> <string name="no_results_found">Nincs találat</string> <!-- Artist screen --> @@ -59,6 +62,7 @@ <string name="retry">Újra</string> <string name="radio">Rádió</string> <string name="shuffle">Keverés</string> + <string name="reset">Reset</string> <!-- Menu --> <string name="details">Részletek</string> @@ -82,6 +86,7 @@ <string name="remove_from_history">Előzményből eltávolít</string> <string name="search_online">Keresés online</string> <string name="sync">Szinkron.</string> + <string name="advanced">Advanced</string> <!-- Sort menu --> <string name="sort_by_create_date">Létrehozás dátuma</string> @@ -143,16 +148,16 @@ <item quantity="other">%d lejátszólista</item> </plurals> <plurals name="n_week" tools:ignore="MissingQuantity"> - <item quantity="one">%d week</item> - <item quantity="other">%d weeks</item> + <item quantity="one">%d hét</item> + <item quantity="other">%d hét</item> </plurals> <plurals name="n_month" tools:ignore="MissingQuantity"> - <item quantity="one">%d month</item> - <item quantity="other">%d months</item> + <item quantity="one">%d hónap</item> + <item quantity="other">%d hónap</item> </plurals> <plurals name="n_year" tools:ignore="MissingQuantity"> - <item quantity="one">%d year</item> - <item quantity="other">%d years</item> + <item quantity="one">%d év</item> + <item quantity="other">%d év</item> </plurals> <!-- Snackbar --> @@ -255,4 +260,6 @@ <string name="app_version">App verzió</string> <string name="new_version_available">New version available</string> + <string name="translation_models">Translation Models</string> + <string name="clear_translation_models">Clear translation models</string> </resources> diff --git a/app/src/main/res/values-id/strings.xml b/app/src/main/res/values-id/strings.xml index 95bede859..cd8b84f28 100644 --- a/app/src/main/res/values-id/strings.xml +++ b/app/src/main/res/values-id/strings.xml @@ -35,6 +35,9 @@ <string name="search">Caru</string> <string name="search_yt_music">Cari di YouTube Music…</string> <string name="search_library">Cari di perpustakaan…</string> + <string name="filter_library">Library</string> + <string name="filter_liked">Liked</string> + <string name="filter_downloaded">Downloaded</string> <string name="filter_all">Semua</string> <string name="filter_songs">Lagu</string> <string name="filter_videos">Video</string> @@ -58,6 +61,7 @@ <string name="retry">Mencoba kembali</string> <string name="radio">Radio</string> <string name="shuffle">Acak</string> + <string name="reset">Reset</string> <!-- Menu --> <string name="details">Rincian</string> @@ -81,6 +85,7 @@ <string name="remove_from_history">Remove from history</string> <string name="search_online">Cari secara online</string> <string name="sync">Sync</string> + <string name="advanced">Advanced</string> <!-- Sort menu --> <string name="sort_by_create_date">Tanggal ditambahkan</string> @@ -246,4 +251,6 @@ <string name="app_version">Versi aplikasi</string> <string name="new_version_available">New version available</string> + <string name="translation_models">Translation Models</string> + <string name="clear_translation_models">Clear translation models</string> </resources> diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index b82d543f7..0ee41e82d 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -37,6 +37,9 @@ <string name="search">Cerca</string> <string name="search_yt_music">Cerca su YouTube Music…</string> <string name="search_library">Cerca nella libreria…</string> + <string name="filter_library">Library</string> + <string name="filter_liked">Liked</string> + <string name="filter_downloaded">Downloaded</string> <string name="filter_all">Tutto</string> <string name="filter_songs">Brani</string> <string name="filter_videos">Video</string> @@ -60,6 +63,7 @@ <string name="retry">Riprova</string> <string name="radio">Radio</string> <string name="shuffle">Mischia</string> + <string name="reset">Reset</string> <!-- Menu --> <string name="details">Dettagli</string> @@ -83,6 +87,7 @@ <string name="remove_from_history">Rimuovi da cronologia</string> <string name="search_online">Cerca online</string> <string name="sync">Sincronizza</string> + <string name="advanced">Advanced</string> <!-- Sort menu --> <string name="sort_by_create_date">Data di aggiunta</string> @@ -264,4 +269,6 @@ <string name="app_version">Versione dell\'app</string> <string name="new_version_available">New version available</string> + <string name="translation_models">Translation Models</string> + <string name="clear_translation_models">Clear translation models</string> </resources> diff --git a/app/src/main/res/values-ja-rJP/strings.xml b/app/src/main/res/values-ja-rJP/strings.xml index f95b6b252..70f770914 100644 --- a/app/src/main/res/values-ja-rJP/strings.xml +++ b/app/src/main/res/values-ja-rJP/strings.xml @@ -35,6 +35,9 @@ <string name="search">検索</string> <string name="search_yt_music">YouTube Musicを検索…</string> <string name="search_library">ライブラリを検索…</string> + <string name="filter_library">Library</string> + <string name="filter_liked">Liked</string> + <string name="filter_downloaded">Downloaded</string> <string name="filter_all">すべて</string> <string name="filter_songs">曲</string> <string name="filter_videos">動画</string> @@ -58,6 +61,7 @@ <string name="retry">再試行</string> <string name="radio">ラジオ</string> <string name="shuffle">シャッフル</string> + <string name="reset">Reset</string> <!-- Menu --> <string name="details">詳細</string> @@ -81,6 +85,7 @@ <string name="remove_from_history">履歴から削除</string> <string name="search_online">オンラインで検索</string> <string name="sync">同期</string> + <string name="advanced">Advanced</string> <!-- Sort menu --> <string name="sort_by_create_date">追加日時</string> @@ -246,4 +251,6 @@ <string name="app_version">アプリのバージョン</string> <string name="new_version_available">New version available</string> + <string name="translation_models">Translation Models</string> + <string name="clear_translation_models">Clear translation models</string> </resources> diff --git a/app/src/main/res/values-ko-rKR/strings.xml b/app/src/main/res/values-ko-rKR/strings.xml index 62486122a..8f53fcd23 100644 --- a/app/src/main/res/values-ko-rKR/strings.xml +++ b/app/src/main/res/values-ko-rKR/strings.xml @@ -35,6 +35,9 @@ <string name="search">검색</string> <string name="search_yt_music">Search YouTube Music…</string> <string name="search_library">Search library…</string> + <string name="filter_library">Library</string> + <string name="filter_liked">Liked</string> + <string name="filter_downloaded">Downloaded</string> <string name="filter_all">모두</string> <string name="filter_songs">노래</string> <string name="filter_videos">비디오</string> @@ -58,6 +61,7 @@ <string name="retry">다시</string> <string name="radio">Radio</string> <string name="shuffle">셔플</string> + <string name="reset">Reset</string> <!-- Menu --> <string name="details">Details</string> @@ -81,6 +85,7 @@ <string name="remove_from_history">Remove from history</string> <string name="search_online">Search online</string> <string name="sync">Sync</string> + <string name="advanced">Advanced</string> <!-- Sort menu --> <string name="sort_by_create_date">추가된 날짜</string> @@ -246,4 +251,6 @@ <string name="app_version">앱 버전</string> <string name="new_version_available">New version available</string> + <string name="translation_models">Translation Models</string> + <string name="clear_translation_models">Clear translation models</string> </resources> diff --git a/app/src/main/res/values-ml-rIN/strings.xml b/app/src/main/res/values-ml-rIN/strings.xml index 02283bfca..8d70e692c 100644 --- a/app/src/main/res/values-ml-rIN/strings.xml +++ b/app/src/main/res/values-ml-rIN/strings.xml @@ -36,6 +36,9 @@ <string name="search">തിരയുക</string> <string name="search_yt_music">യൂട്യൂബ് സംഗീതം തിരയുക…</string> <string name="search_library">തിരയൽ ലൈബ്രറി…</string> + <string name="filter_library">Library</string> + <string name="filter_liked">Liked</string> + <string name="filter_downloaded">Downloaded</string> <string name="filter_all">എല്ലാം</string> <string name="filter_songs">പാട്ടുകൾ</string> <string name="filter_videos">വീഡിയോകൾ</string> @@ -59,6 +62,7 @@ <string name="retry">വീണ്ടും ശ്രമിക്കുക</string> <string name="radio">റേഡിയോ</string> <string name="shuffle">ഷഫിൾ</string> + <string name="reset">Reset</string> <!-- Menu --> <string name="details">Details</string> @@ -82,6 +86,7 @@ <string name="remove_from_history">Remove from history</string> <string name="search_online">Search online</string> <string name="sync">Sync</string> + <string name="advanced">Advanced</string> <!-- Sort menu --> <string name="sort_by_create_date">ചേർത്ത തീയതി</string> @@ -255,4 +260,6 @@ <string name="app_version">അപ്ലിക്കേഷൻ പതിപ്പ്</string> <string name="new_version_available">New version available</string> + <string name="translation_models">Translation Models</string> + <string name="clear_translation_models">Clear translation models</string> </resources> diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index cee9296e8..c9cf5e63c 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -36,6 +36,9 @@ <string name="search">Zoeken</string> <string name="search_yt_music">Zoeken via YouTube Music…</string> <string name="search_library">Zoeken in bibliotheek</string> + <string name="filter_library">Library</string> + <string name="filter_liked">Liked</string> + <string name="filter_downloaded">Downloaded</string> <string name="filter_all">Alles</string> <string name="filter_songs">Nummers</string> <string name="filter_videos">Videos</string> @@ -59,6 +62,7 @@ <string name="retry">Opnieuw</string> <string name="radio">Radio</string> <string name="shuffle">Shuffle</string> + <string name="reset">Reset</string> <!-- Menu --> <string name="details">Details</string> @@ -82,6 +86,7 @@ <string name="remove_from_history">Verwijder uit geschiedenis</string> <string name="search_online">Zoek online</string> <string name="sync">Synchroniseer</string> + <string name="advanced">Advanced</string> <!-- Sort menu --> <string name="sort_by_create_date">Datum toegevoegd</string> @@ -255,4 +260,6 @@ <string name="app_version">App versie</string> <string name="new_version_available">New version available</string> + <string name="translation_models">Translation Models</string> + <string name="clear_translation_models">Clear translation models</string> </resources> diff --git a/app/src/main/res/values-or-rIN/strings.xml b/app/src/main/res/values-or-rIN/strings.xml index 940e2e4c2..eaacaf138 100644 --- a/app/src/main/res/values-or-rIN/strings.xml +++ b/app/src/main/res/values-or-rIN/strings.xml @@ -36,6 +36,9 @@ <string name="search">ସନ୍ଧାନ କରନ୍ତୁ</string> <string name="search_yt_music">ୟୁଟ୍ୟୁବ୍ ମ୍ୟୁଜିକ୍ ଖୋଜ…</string> <string name="search_library">ଲାଇବ୍ରେରୀ ଖୋଜ…</string> + <string name="filter_library">Library</string> + <string name="filter_liked">Liked</string> + <string name="filter_downloaded">Downloaded</string> <string name="filter_all">ସମସ୍ତ</string> <string name="filter_songs">ସଙ୍ଗୀତ ଗୁଡ଼ିକ</string> <string name="filter_videos">ଭିଡିଓ ଗୁଡ଼ିକ</string> @@ -59,6 +62,7 @@ <string name="retry">ପୁନଃ ଚେଷ୍ଟା କରନ୍ତୁ</string> <string name="radio">ରେଡିଓ</string> <string name="shuffle">ଶଫଲ୍ କରନ୍ତୁ</string> + <string name="reset">Reset</string> <!-- Menu --> <string name="details">ବିବରଣୀ</string> @@ -82,6 +86,7 @@ <string name="remove_from_history">Remove from history</string> <string name="search_online">ଅନଲାଇନ୍ ସନ୍ଧାନ କରନ୍ତୁ</string> <string name="sync">Sync</string> + <string name="advanced">Advanced</string> <!-- Sort menu --> <string name="sort_by_create_date">ତାରିଖ ରେ ଯୋଡା ଯାଇଛି</string> @@ -255,4 +260,6 @@ <string name="app_version">ଆପ୍ ସଂସ୍କରଣ</string> <string name="new_version_available">New version available</string> + <string name="translation_models">Translation Models</string> + <string name="clear_translation_models">Clear translation models</string> </resources> diff --git a/app/src/main/res/values-pa/strings.xml b/app/src/main/res/values-pa/strings.xml index 716626bb7..7e70755e9 100644 --- a/app/src/main/res/values-pa/strings.xml +++ b/app/src/main/res/values-pa/strings.xml @@ -36,6 +36,9 @@ <string name="search">ਖੋਜੋ</string> <string name="search_yt_music">ਯੂਟਿਊਬ ਮਿਊਜ਼ਿਕ ਖੋਜੋ…</string> <string name="search_library">ਲਾਇਬ੍ਰੇਰੀ ਖੋਜੋ…</string> + <string name="filter_library">Library</string> + <string name="filter_liked">Liked</string> + <string name="filter_downloaded">Downloaded</string> <string name="filter_all">ਸਾਰੇ</string> <string name="filter_songs">ਗੀਤ</string> <string name="filter_videos">ਵੀਡੀਓ</string> @@ -59,6 +62,7 @@ <string name="retry">ਦੁਬਾਰਾ ਕੋਸ਼ਿਸ਼ ਕਰੋ</string> <string name="radio">ਰੇਡੀਓ</string> <string name="shuffle">ਸ਼ਫਲ ਕਰੋ</string> + <string name="reset">Reset</string> <!-- Menu --> <string name="details">ਵੇਰਵੇ</string> @@ -82,6 +86,7 @@ <string name="remove_from_history">ਅਤੀਤ ਵਿੱਚੋਂ ਹਟਾਓ</string> <string name="search_online">ਆਨਲਾਈਨ ਖੋਜ ਕਰੋ</string> <string name="sync">ਸਿੰਕਰੋਨਾਈਜ਼ ਕਰੋ</string> + <string name="advanced">Advanced</string> <!-- Sort menu --> <string name="sort_by_create_date">ਮਿਤੀ</string> @@ -255,4 +260,6 @@ <string name="app_version">ਐਪ ਸੰਸਕਰਣ</string> <string name="new_version_available">New version available</string> + <string name="translation_models">Translation Models</string> + <string name="clear_translation_models">Clear translation models</string> </resources> diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index 234a85b47..5b8ff7887 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -38,6 +38,9 @@ <string name="search">Szukaj</string> <string name="search_yt_music">Szukaj w YouTube Music…</string> <string name="search_library">Szukaj w bibliotece…</string> + <string name="filter_library">Biblioteka</string> + <string name="filter_liked">Polubione</string> + <string name="filter_downloaded">Pobrane</string> <string name="filter_all">Wszystko</string> <string name="filter_songs">Utwory</string> <string name="filter_videos">Filmy</string> @@ -61,6 +64,7 @@ <string name="retry">Spróbuj ponownie</string> <string name="radio">Radio</string> <string name="shuffle">Losowo</string> + <string name="reset">Reset</string> <!-- Menu --> <string name="details">Szczegóły</string> @@ -84,6 +88,7 @@ <string name="remove_from_history">Usuń z historii</string> <string name="search_online">Szukaj online</string> <string name="sync">Synchronizuj</string> + <string name="advanced">Advanced</string> <!-- Sort menu --> <string name="sort_by_create_date">Data dodania</string> @@ -273,4 +278,6 @@ <string name="app_version">Wersja aplikacji</string> <string name="new_version_available">Dostępna nowa wersja</string> + <string name="translation_models">Translation Models</string> + <string name="clear_translation_models">Clear translation models</string> </resources> diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index e671ed8ba..80e022273 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -37,6 +37,9 @@ <string name="search">Pesquisar</string> <string name="search_yt_music">Pesquisar no YouTube Music…</string> <string name="search_library">Pesquisar na biblioteca…</string> + <string name="filter_library">Library</string> + <string name="filter_liked">Liked</string> + <string name="filter_downloaded">Downloaded</string> <string name="filter_all">Tudo</string> <string name="filter_songs">Músicas</string> <string name="filter_videos">Vídeos</string> @@ -60,6 +63,7 @@ <string name="retry">Tentar novamente</string> <string name="radio">Rádio</string> <string name="shuffle">Aleatório</string> + <string name="reset">Reset</string> <!-- Menu --> <string name="details">Detalhes</string> @@ -83,6 +87,7 @@ <string name="remove_from_history">Remove from history</string> <string name="search_online">Search online</string> <string name="sync">Sync</string> + <string name="advanced">Advanced</string> <!-- Sort menu --> <string name="sort_by_create_date">Quando adicionada</string> @@ -264,4 +269,6 @@ <string name="app_version">Versão do aplicativo</string> <string name="new_version_available">New version available</string> + <string name="translation_models">Translation Models</string> + <string name="clear_translation_models">Clear translation models</string> </resources> diff --git a/app/src/main/res/values-ru-rRU/strings.xml b/app/src/main/res/values-ru-rRU/strings.xml index bae17e29e..7d2b665f3 100644 --- a/app/src/main/res/values-ru-rRU/strings.xml +++ b/app/src/main/res/values-ru-rRU/strings.xml @@ -38,6 +38,9 @@ <string name="search">Поиск</string> <string name="search_yt_music">Поиск в YouTube Music…</string> <string name="search_library">Поиск в библиотеке…</string> + <string name="filter_library">Библиотека</string> + <string name="filter_liked">Понравившиеся</string> + <string name="filter_downloaded">Загруженные</string> <string name="filter_all">Все</string> <string name="filter_songs">Композиции</string> <string name="filter_videos">Видео</string> @@ -61,6 +64,7 @@ <string name="retry">Повторить</string> <string name="radio">Радио</string> <string name="shuffle">Перемешать</string> + <string name="reset">Сбросить</string> <!-- Menu --> <string name="details">Подробнее</string> @@ -84,6 +88,7 @@ <string name="remove_from_history">Удалить из истории</string> <string name="search_online">Поиск в Интернете</string> <string name="sync">Синхронизация</string> + <string name="advanced">Контроль аудио</string> <!-- Sort menu --> <string name="sort_by_create_date">Недавно добавленные</string> @@ -273,4 +278,6 @@ <string name="app_version">Версия приложения</string> <string name="new_version_available">Доступна новая версия</string> + <string name="translation_models">Translation Models</string> + <string name="clear_translation_models">Clear translation models</string> </resources> diff --git a/app/src/main/res/values-sv-rSE/strings.xml b/app/src/main/res/values-sv-rSE/strings.xml index eac93c378..2a1aad6ca 100644 --- a/app/src/main/res/values-sv-rSE/strings.xml +++ b/app/src/main/res/values-sv-rSE/strings.xml @@ -36,6 +36,9 @@ <string name="search">Sök</string> <string name="search_yt_music">Search YouTube Music…</string> <string name="search_library">Search library…</string> + <string name="filter_library">Library</string> + <string name="filter_liked">Liked</string> + <string name="filter_downloaded">Downloaded</string> <string name="filter_all">Alla</string> <string name="filter_songs">Låtar</string> <string name="filter_videos">Videor</string> @@ -59,6 +62,7 @@ <string name="retry">Försök igen</string> <string name="radio">Radio</string> <string name="shuffle">Blanda</string> + <string name="reset">Reset</string> <!-- Menu --> <string name="details">Details</string> @@ -82,6 +86,7 @@ <string name="remove_from_history">Remove from history</string> <string name="search_online">Search online</string> <string name="sync">Sync</string> + <string name="advanced">Advanced</string> <!-- Sort menu --> <string name="sort_by_create_date">Datum tillagd</string> @@ -255,4 +260,6 @@ <string name="app_version">App version</string> <string name="new_version_available">New version available</string> + <string name="translation_models">Translation Models</string> + <string name="clear_translation_models">Clear translation models</string> </resources> diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index 42441d045..08cddf1ee 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -36,6 +36,9 @@ <string name="search">Arama</string> <string name="search_yt_music">YouTube Müzik\'te Ara…</string> <string name="search_library">Kütüphanede ara…</string> + <string name="filter_library">Kütüphane</string> + <string name="filter_liked">Beğenilenler</string> + <string name="filter_downloaded">İndirilenler</string> <string name="filter_all">Hepsi</string> <string name="filter_songs">Şarkılar</string> <string name="filter_videos">Videolar</string> @@ -59,6 +62,7 @@ <string name="retry">Yeniden dene</string> <string name="radio">Radyo</string> <string name="shuffle">Karıştır</string> + <string name="reset">Reset</string> <!-- Menu --> <string name="details">Ayrıntılar</string> @@ -82,6 +86,7 @@ <string name="remove_from_history">Geçmişten kaldır</string> <string name="search_online">Çevrimiçi arama</string> <string name="sync">Eşitle</string> + <string name="advanced">Advanced</string> <!-- Sort menu --> <string name="sort_by_create_date">Eklendiği tarih</string> @@ -255,4 +260,6 @@ <string name="app_version">Uygulama sürümü</string> <string name="new_version_available">Yeni sürüm mevcut</string> + <string name="translation_models">Translation Models</string> + <string name="clear_translation_models">Clear translation models</string> </resources> diff --git a/app/src/main/res/values-uk-rUA/strings.xml b/app/src/main/res/values-uk-rUA/strings.xml index 686cf8591..83475b8e8 100644 --- a/app/src/main/res/values-uk-rUA/strings.xml +++ b/app/src/main/res/values-uk-rUA/strings.xml @@ -38,6 +38,9 @@ <string name="search">Пошук</string> <string name="search_yt_music">Пошук в YouTube Music…</string> <string name="search_library">Пошук в бібліотеці…</string> + <string name="filter_library">Бібліотека</string> + <string name="filter_liked">Вподобані</string> + <string name="filter_downloaded">Завантажені</string> <string name="filter_all">Всі</string> <string name="filter_songs">Композиції</string> <string name="filter_videos">Відео</string> @@ -61,6 +64,7 @@ <string name="retry">Повторювати</string> <string name="radio">Радіо</string> <string name="shuffle">Перемішати</string> + <string name="reset">Скинути</string> <!-- Menu --> <string name="details">Детальніше</string> @@ -84,6 +88,7 @@ <string name="remove_from_history">Видалити з історії</string> <string name="search_online">Пошук в Інтернеті</string> <string name="sync">Синхронізація</string> + <string name="advanced">Контроль аудіо</string> <!-- Sort menu --> <string name="sort_by_create_date">Нещодавно додані</string> @@ -273,4 +278,6 @@ <string name="app_version">Версія застосунку</string> <string name="new_version_available">Доступна нова версія</string> + <string name="translation_models">Translation Models</string> + <string name="clear_translation_models">Clear translation models</string> </resources> diff --git a/app/src/main/res/values-vn/strings.xml b/app/src/main/res/values-vi/strings.xml similarity index 96% rename from app/src/main/res/values-vn/strings.xml rename to app/src/main/res/values-vi/strings.xml index 659aff00b..79f87da9e 100644 --- a/app/src/main/res/values-vn/strings.xml +++ b/app/src/main/res/values-vi/strings.xml @@ -35,6 +35,9 @@ <string name="search">Tìm kiếm</string> <string name="search_yt_music">Tìm kiếm YouTube Music…</string> <string name="search_library">Tìm kiếm thư viện…</string> + <string name="filter_library">Library</string> + <string name="filter_liked">Liked</string> + <string name="filter_downloaded">Downloaded</string> <string name="filter_all">Tất cả</string> <string name="filter_songs">Bài hát</string> <string name="filter_videos">Videos</string> @@ -58,6 +61,7 @@ <string name="retry">Thử lại</string> <string name="radio">Đài</string> <string name="shuffle">Trộn</string> + <string name="reset">Reset</string> <!-- Menu --> <string name="details">Chi tiết</string> @@ -81,6 +85,7 @@ <string name="remove_from_history">Xoá khỏi lịch sử</string> <string name="search_online">Tìm kiếm trực tuyến</string> <string name="sync">Đồng bộ</string> + <string name="advanced">Advanced</string> <!-- Sort menu --> <string name="sort_by_create_date">Ngày thêm vào</string> @@ -126,31 +131,24 @@ <!-- Noun --> <plurals name="n_song"> - <item quantity="one">Bài hát %d </item> <item quantity="other">Các bài hát %d </item> </plurals> <plurals name="n_artist"> - <item quantity="one">Tác giả %d </item> <item quantity="other">Các tác giả %d</item> </plurals> <plurals name="n_album"> - <item quantity="one">%d album</item> <item quantity="other">%d albums</item> </plurals> <plurals name="n_playlist"> - <item quantity="one">Danh sách phát %d </item> <item quantity="other">Các danh sách phát %d</item> </plurals> <plurals name="n_week"> - <item quantity="one">%d tuần</item> <item quantity="other">%d tuần</item> </plurals> <plurals name="n_month"> - <item quantity="one">%d tháng</item> <item quantity="other">%d tháng</item> </plurals> <plurals name="n_year"> - <item quantity="one">%d năm</item> <item quantity="other">%d năm</item> </plurals> @@ -165,7 +163,6 @@ <string name="sleep_timer">Hẹn giờ</string> <string name="end_of_song">Kết thúc bài hát</string> <plurals name="minute"> - <item quantity="one">1 phút</item> <item quantity="other">%d phút</item> </plurals> <string name="error_no_stream">Không có stream</string> @@ -254,4 +251,6 @@ <string name="app_version">Phiên bản ứng dụng</string> <string name="new_version_available">New version available</string> + <string name="translation_models">Translation Models</string> + <string name="clear_translation_models">Clear translation models</string> </resources> diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 423d76a33..1ce3da49f 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -35,6 +35,9 @@ <string name="search">搜索</string> <string name="search_yt_music">搜索 YouTube Music…</string> <string name="search_library">搜索媒体库…</string> + <string name="filter_library">Library</string> + <string name="filter_liked">Liked</string> + <string name="filter_downloaded">Downloaded</string> <string name="filter_all">全部</string> <string name="filter_songs">歌曲</string> <string name="filter_videos">视频</string> @@ -58,6 +61,7 @@ <string name="retry">重试</string> <string name="radio">电台</string> <string name="shuffle">随机播放</string> + <string name="reset">Reset</string> <!-- Menu --> <string name="details">详情</string> @@ -81,6 +85,7 @@ <string name="remove_from_history">从记录中移除</string> <string name="search_online">在线搜索</string> <string name="sync">同步</string> + <string name="advanced">Advanced</string> <!-- Sort menu --> <string name="sort_by_create_date">新建时间</string> @@ -246,4 +251,6 @@ <string name="app_version">应用版本</string> <string name="new_version_available">New version available</string> + <string name="translation_models">Translation Models</string> + <string name="clear_translation_models">Clear translation models</string> </resources> diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index 2cd8eadef..49357eebc 100755 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -35,6 +35,9 @@ <string name="search">搜尋</string> <string name="search_yt_music">搜尋 YouTube Music…</string> <string name="search_library">搜尋媒體庫…</string> + <string name="filter_library">媒體庫</string> + <string name="filter_liked">已按讚</string> + <string name="filter_downloaded">已下載</string> <string name="filter_all">全部</string> <string name="filter_songs">歌曲</string> <string name="filter_videos">影片</string> @@ -58,6 +61,7 @@ <string name="retry">重試</string> <string name="radio">電台</string> <string name="shuffle">隨機播放</string> + <string name="reset">重設</string> <!-- Menu --> <string name="details">詳細資訊</string> @@ -81,6 +85,7 @@ <string name="remove_from_history">從記錄中移除</string> <string name="search_online">線上搜尋</string> <string name="sync">同步</string> + <string name="advanced">進階</string> <!-- Sort menu --> <string name="sort_by_create_date">新增時間</string> @@ -246,4 +251,6 @@ <string name="app_version">應用程式版本</string> <string name="new_version_available">發現新版本</string> + <string name="translation_models">翻譯模型</string> + <string name="clear_translation_models">清除翻譯模型</string> </resources> diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index eda7659dc..cc29fc3cf 100755 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -35,6 +35,9 @@ <string name="search">Search</string> <string name="search_yt_music">Search YouTube Music…</string> <string name="search_library">Search library…</string> + <string name="filter_library">Library</string> + <string name="filter_liked">Liked</string> + <string name="filter_downloaded">Downloaded</string> <string name="filter_all">All</string> <string name="filter_songs">Songs</string> <string name="filter_videos">Videos</string> @@ -58,6 +61,7 @@ <string name="retry">Retry</string> <string name="radio">Radio</string> <string name="shuffle">Shuffle</string> + <string name="reset">Reset</string> <!-- Menu --> <string name="details">Details</string> @@ -81,6 +85,7 @@ <string name="remove_from_history">Remove from history</string> <string name="search_online">Search online</string> <string name="sync">Sync</string> + <string name="advanced">Advanced</string> <!-- Sort menu --> <string name="sort_by_create_date">Date added</string> @@ -254,4 +259,6 @@ <string name="app_version">App version</string> <string name="new_version_available">New version available</string> + <string name="translation_models">Translation Models</string> + <string name="clear_translation_models">Clear translation models</string> </resources> diff --git a/build.gradle.kts b/build.gradle.kts index c5b81cacb..5dcafdea6 100755 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -4,6 +4,10 @@ plugins { } buildscript { + val isFullBuild by extra { + gradle.startParameter.taskNames.none { task -> task.contains("foss", ignoreCase = true) } + } + repositories { google() mavenCentral() @@ -12,9 +16,11 @@ buildscript { dependencies { classpath(libs.gradle) classpath(kotlin("gradle-plugin", libs.versions.kotlin.get())) - classpath(libs.google.services) - classpath(libs.firebase.crashlytics.plugin) - classpath(libs.firebase.perf.plugin) + if (isFullBuild) { + classpath(libs.google.services) + classpath(libs.firebase.crashlytics.plugin) + classpath(libs.firebase.perf.plugin) + } } } diff --git a/crowdin.yml b/crowdin.yml new file mode 100644 index 000000000..7a6371886 --- /dev/null +++ b/crowdin.yml @@ -0,0 +1,3 @@ +files: + - source: /app/src/main/res/values/strings.xml + translation: /app/src/main/res/values-%android_code%/strings.xml diff --git a/fastlane/metadata/android/en-US/changelogs/18.txt b/fastlane/metadata/android/en-US/changelogs/18.txt new file mode 100644 index 000000000..4334ebd2b --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/18.txt @@ -0,0 +1,3 @@ +- Improve library design +- Lyrics translator (full version only) +- Minor enhancement and bug fixes \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 31f77b1d0..291b47f43 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -83,5 +83,8 @@ firebase-config = { group = "com.google.firebase", name = "firebase-config-ktx" firebase-perf = { group = "com.google.firebase", name = "firebase-perf-ktx" } firebase-perf-plugin = { module = "com.google.firebase:perf-plugin", version = "1.4.2" } +mlkit-language-id = { group = "com.google.mlkit", name = "language-id", version = "17.0.4" } +mlkit-translate = { group = "com.google.mlkit", name = "translate", version = "17.0.1" } + [plugins] kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } \ No newline at end of file diff --git a/innertube/src/main/java/com/zionhuang/innertube/InnerTube.kt b/innertube/src/main/java/com/zionhuang/innertube/InnerTube.kt index f97b3dc88..b41ffba18 100644 --- a/innertube/src/main/java/com/zionhuang/innertube/InnerTube.kt +++ b/innertube/src/main/java/com/zionhuang/innertube/InnerTube.kt @@ -239,7 +239,7 @@ class InnerTube { suspend fun getSwJsData() = httpClient.get("https://music.youtube.com/sw.js_data") suspend fun accountMenu(client: YouTubeClient) = httpClient.post("account/account_menu") { - ytClient(client) + ytClient(client, setLogin = true) setBody(AccountMenuBody(client.toContext(locale, visitorData))) } } diff --git a/innertube/src/main/java/com/zionhuang/innertube/YouTube.kt b/innertube/src/main/java/com/zionhuang/innertube/YouTube.kt index 0376212e2..18bf4fed6 100644 --- a/innertube/src/main/java/com/zionhuang/innertube/YouTube.kt +++ b/innertube/src/main/java/com/zionhuang/innertube/YouTube.kt @@ -110,14 +110,19 @@ object YouTube { ?.mapNotNull { it.musicResponsiveListItemRenderer } ?.mapNotNull(SearchSummaryPage.Companion::fromMusicResponsiveListItemRenderer) .orEmpty() - ).takeIf { it.isNotEmpty() } ?: return@mapNotNull null + ) + .distinctBy { it.id } + .ifEmpty { null } ?: return@mapNotNull null ) else SearchSummary( title = it.musicShelfRenderer?.title?.runs?.firstOrNull()?.text ?: return@mapNotNull null, - items = it.musicShelfRenderer.contents?.mapNotNull { - SearchSummaryPage.fromMusicResponsiveListItemRenderer(it.musicResponsiveListItemRenderer) - }?.ifEmpty { null } ?: return@mapNotNull null + items = it.musicShelfRenderer.contents + ?.mapNotNull { + SearchSummaryPage.fromMusicResponsiveListItemRenderer(it.musicResponsiveListItemRenderer) + } + ?.distinctBy { it.id } + ?.ifEmpty { null } ?: return@mapNotNull null ) }!! ) @@ -485,8 +490,8 @@ object YouTube { .jsonPrimitive.content } - suspend fun accountInfo(): Result<AccountInfo?> = runCatching { - innerTube.accountMenu(WEB_REMIX).body<AccountMenuResponse>().actions[0].openPopupAction.popup.multiPageMenuRenderer.header?.activeAccountHeaderRenderer?.toAccountInfo() + suspend fun accountInfo(): Result<AccountInfo> = runCatching { + innerTube.accountMenu(WEB_REMIX).body<AccountMenuResponse>().actions[0].openPopupAction.popup.multiPageMenuRenderer.header?.activeAccountHeaderRenderer?.toAccountInfo()!! } @JvmInline