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
-[](https://f-droid.org/packages/com.zionhuang.music)
-[](https://apt.izzysoft.de/fdroid/index/apk/com.zionhuang.music)
-
[![Latest release](https://img.shields.io/github/v/release/z-huang/InnerTune?include_prereleases)](https://github.com/z-huang/music/releases)
[![License](https://img.shields.io/github/license/z-huang/InnerTune)](https://www.gnu.org/licenses/gpl-3.0)
[![Downloads](https://img.shields.io/github/downloads/z-huang/InnerTune/total)](https://github.com/z-huang/InnerTune/releases)
+[](https://github.com/z-huang/InnerTune/releases/latest)
+[](https://f-droid.org/packages/com.zionhuang.music)
+[](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(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>
+ fun mostPlayedAlbums(fromTimeStamp: Long, limit: Int = 6): Flow>
@Transaction
@Query("SELECT * FROM song WHERE id = :songId")
fun song(songId: String?): Flow
@Transaction
- @Query("SELECT * FROM song WHERE id IN (:songIds)")
- fun songs(songIds: List): Flow>
+ @Query("SELECT * FROM song")
+ fun allSongs(): Flow>
@Query("SELECT * FROM format WHERE id = :id")
fun format(id: String?): Flow
@@ -216,7 +219,7 @@ interface DatabaseDao {
fun lyrics(id: String?): Flow
@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>
@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
@Transaction
- @Query("SELECT * FROM album ORDER BY rowId")
- fun albumsByRowIdAsc(): Flow>
-
- @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>
@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>
@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>
@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>
@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>
@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>
+ @Transaction
+ @Query("SELECT * FROM album WHERE bookmarkedAt IS NOT NULL ORDER BY rowId")
+ fun albumsLikedByCreateDateAsc(): Flow>
+
+ @Transaction
+ @Query("SELECT * FROM album WHERE bookmarkedAt IS NOT NULL ORDER BY title")
+ fun albumsLikedByNameAsc(): Flow>
+
+ @Transaction
+ @Query("SELECT * FROM album WHERE bookmarkedAt IS NOT NULL ORDER BY year")
+ fun albumsLikedByYearAsc(): Flow>
+
+ @Transaction
+ @Query("SELECT * FROM album WHERE bookmarkedAt IS NOT NULL ORDER BY songCount")
+ fun albumsLikedBySongCountAsc(): Flow>
+
+ @Transaction
+ @Query("SELECT * FROM album WHERE bookmarkedAt IS NOT NULL ORDER BY duration")
+ fun albumsLikedByLengthAsc(): Flow>
+
+ @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>
+
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
@@ -395,7 +453,7 @@ interface DatabaseDao {
fun searchArtists(query: String, previewSize: Int = Int.MAX_VALUE): Flow>
@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>
@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 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 =
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 ChipsRow(
+ chips: List>,
+ 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())
}
@@ -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 ValueAdjuster(
@DrawableRes icon: Int,
@@ -340,7 +355,7 @@ fun ValueAdjuster(
values: List,
onValueUpdate: (T) -> Unit,
valueText: (T) -> String,
- modifier: Modifier,
+ modifier: Modifier = Modifier,
) {
Row(
horizontalArrangement = Arrangement.spacedBy(24.dp),
@@ -369,7 +384,7 @@ fun 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("albumId")!!
- private val _viewState = MutableStateFlow(null)
- val viewState = _viewState.asStateFlow()
- val inLibrary: StateFlow = 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("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 @@
+
+
+
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 @@
+
+
+
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 @@
Suche
YouTube Musik durchsuchen…
Bibliothek durchsuchen…
+ Library
+ Liked
+ Downloaded
Alles
Titel
Videos
@@ -59,6 +62,7 @@
Wiederholen
Radio
Shuffle
+ Reset
Details
@@ -82,6 +86,7 @@
aus dem Wiedergabeverlauf entfernen
Online-Suche
Sync
+ Advanced
Datum hinzugefügt
@@ -255,4 +260,6 @@
App-Version
New version available
+ Translation Models
+ Clear translation models
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 @@
Пошук
Пошук у YouTube Music…
Пошук у бібліятэцы…
+ Library
+ Liked
+ Downloaded
Усе
Песні
Відэа
@@ -61,6 +64,7 @@
Паўтарыць
Радыё
Перамяшаць
+ Reset
Падрабязнасці
@@ -84,6 +88,7 @@
Выдаліць з гісторыі
Шукаць у сетцы
Cінхранізацыя
+ Advanced
Нядаўна дададзена
@@ -273,4 +278,6 @@
Версія праграмы
New version available
+ Translation Models
+ Clear translation models
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 @@
খুঁজুন
খুঁজুন YouTube Music এ
খুঁজুন লাইব্রেরি তে
+ Library
+ Liked
+ Downloaded
সব
সংগীত
ভিডিও
@@ -59,6 +62,7 @@
পুনরায় চেষ্টা করুন
বেতার
এলোমেলো
+ Reset
বিস্তারিত
@@ -82,6 +86,7 @@
ইতিহাস থেকে অপসারণ
অনলাইন এ খুঁজুন
সুসংগত
+ Advanced
সময় সম্পাদনা করা হয়েছে
@@ -255,4 +260,6 @@
অ্যাপ সংস্করণ
New version available
+ Translation Models
+ Clear translation models
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 @@
Vyhledávání
Hledat v YouTube Music…
Hledat v knihovně…
+ Library
+ Liked
+ Downloaded
Vše
Skladby
Videa
@@ -61,6 +64,7 @@
Zkusit znovu
Rádio
Náhodně
+ Reset
Podrobnosti
@@ -84,6 +88,7 @@
Remove from history
Hledat online
Sync
+ Advanced
Datum přidání
@@ -267,4 +272,6 @@
Verze aplikace
New version available
+ Translation Models
+ Clear translation models
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 @@
Buscar
Buscar en Youtube Music…
Buscar en la biblioteca…
+ Library
+ Liked
+ Downloaded
Todo
Canciones
Vídeos
@@ -60,6 +63,7 @@
Reintentar
Radio
Mezclar
+ Reset
Detalles
@@ -83,6 +87,7 @@
Eliminar del historial
Buscar online
Sincronizar
+ Advanced
Fecha añadida
@@ -264,4 +269,6 @@
Versión de la app
New version available
+ Translation Models
+ Clear translation models
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 @@
جستجو
جستجو در یوتیوب موزیک…
جستجوی کتابخانه…
+ Library
+ Liked
+ Downloaded
همه
آهنگها
فیلمها
@@ -59,6 +62,7 @@
تلاشمجدد
رادیو
بُرزدن
+ Reset
Details
@@ -82,6 +86,7 @@
Remove from history
Search online
Sync
+ Advanced
تاریخ اضافهشده
@@ -255,4 +260,6 @@
نسخهی برنامه
New version available
+ Translation Models
+ Clear translation models
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 @@
Etsi
Search YouTube Music…
Search library…
+ Library
+ Liked
+ Downloaded
Kaikki
Kappaleet
Videot
@@ -59,6 +62,7 @@
Toisto
Radio
Sekoita
+ Reset
Details
@@ -82,6 +86,7 @@
Remove from history
Search online
Sync
+ Advanced
Lisäyspäivä
@@ -255,4 +260,6 @@
Sovelluksen versio
New version available
+ Translation Models
+ Clear translation models
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 @@
-
+
- Home
+ Accueil
Chansons
Artistes
Albums
- Listes de lecture
+ Listes de cours
@@ -14,102 +14,107 @@
- History
- Stats
- Mood and Genres
- Account
- Quick picks
- Listen to songs to generate your quick picks
- New release albums
+ Historique
+ Statistiques
+ Humeurs et genres
+ Compte
+ Sélections rapides
+ Écoutez quelques musiques pour générer vos sélections rapides
+ Nouveautés
- Today
- Yesterday
- This week
- Last week
+ Aujourd\'hui
+ Hier
+ Cette semaine
+ Semaine dernière
- Most played songs
- Most played artists
- Most played albums
+ Musiques les plus jouées
+ Artistes les plus joués
+ Albums les plus joués
- Recherche
- Search YouTube Music…
- Search library…
+ Recherché
+ Rechercher sur Youtube Musique…
+ Rechercher dans votre librairie…
+ Librairie
+ Aimé
+ Télécharger
Tout
Chansons
Vidéos
Albums
Artistes
- Listes de lecture
- Community playlists
- Featured playlists
- Bookmarked
- No results found
+ Playlist
+ Playlist de la communauté
+ Playlist mise en avant
+ Favoris
+ Aucun résultat trouvé
- From your library
+ De votre bibliothèque
- Liked songs
- Downloaded songs
- The playlist is empty
+ Chansons aimées
+ Musiques téléchargées
+ La playlist est vide
Réessayer
Radio
Lecture aléatoire
+ Reset
- Details
+ Détails
Modifier
- Start radio
- Play
+ Démarrer la radio
+ Jouer
Jouer à la suite
Ajouter à la file d\'attente
Ajouter à la bibliothèque
- Remove from library
+ Supprimer de la bibliothèque
Télécharger
- Downloading
+ Téléchargement
Supprimer le téléchargement
- Import playlist
+ Importer une liste de lecture
Ajouter à la liste de lecture
- View artist
- View album
- Refetch
- Share
- Effacer
- Remove from history
- Search online
- Sync
+ Voir l\'artiste
+ Voir l\'album
+ Récupérer
+ Partager
+ Effaceur
+ Supprimer de l\'historique
+ Rechercher en ligne
+ Synchroniser
+ Advanced
Date d\'ajout
Nom
Artiste
- Year
- Song count
- Length
- Play time
- Custom order
+ Année
+ Nombre de chansons
+ Longueur
+ Récréation
+ Commande personnalisée
- Media id
- MIME type
+ Identifiant du média
+ Type MIME
Codecs
- Bitrate
- Sample rate
- Loudness
+ Débit
+ Taux d\'échantillonnage
+ Intensité
Volume
- File size
- Unknown
- Copied to clipboard
+ Taille du fichier
+ Inconnu
+ Copié dans le presse-papier
- Edit lyrics
- Search lyrics
+ Modifier les paroles
+ Rechercher des paroles
- Editer la chanson
+ Éditer la chanson
Titre de la chanson
Artiste de la chanson
Le titre de la chanson ne peut pas être vide.
@@ -117,12 +122,12 @@
Sauvegarder
Choisir la liste de lecture
- Editer la liste de lecture
+ Editer la playlist
Créer une liste de lecture
- Nom de la liste de lecture
- Le nom de la liste de lecture ne peut pas être vide.
+ Nom de la playlist
+ Le nom de la playlist ne peut pas être vide.
- Editer l\'artiste
+ Éditer l\'artiste
Nom de l\'artiste
Le nom de l\'artiste ne peut être vide.
@@ -145,120 +150,125 @@
- %d playlist
- %d playlists
- - %d albums
+ - %d playlists
-
- - %d week
- - %d weeks
+
+ - %d semaine
+ - %d semaines
+ - %d semaines
-
- - %d month
- - %d months
+
+ - %d mois
+ - %d mois
+ - %d mois
-
- - %d year
- - %d years
+
+ - %d ans
+ - %d ans
+ - %d ans
- Playlist imported
- Removed \"%s\" from playlist
- Playlist synced
- Undo
+ Liste de lecture importée
+ \"%s\" supprimé de la liste de lecture
+ Liste de lecture synchronisée
+ annuler
- Lyrics not found
- Sleep timer
- End of song
+ Paroles introuvables
+ Minuterie de sommeil
+ Fin de chanson
- - 1 minute
+ - %d minute
- %d minutes
- %d minutes
- No stream available
- No network connection
- Timeout
- Unknown error
+ Aucun flux disponible
+ Pas de connexion réseau
+ Temps libre
+ Erreur inconnue
Like
- Remove like
+ Enlever le like
- All songs
- Searched songs
+ Toutes les chansons
+ Chansons recherchées
Lecteur de musique
Paramètres
- Appearance
- Enable dynamic theme
- Dark theme
- On
- Off
- Follow system
- Pure black
- Default open tab
- Customize navigation tabs
- Lyrics text position
- Left
- Center
- Right
+ Apparence
+ Activer le thème dynamique
+ Thème sombre
+ Activé
+ Désactivé
+ Suivre le système
+ Noir pur
+ Menu ouvert par défaut
+ Personnaliser les menus de navigation
+ Position du texte des paroles
+ Gauche
+ Centre
+ Droite
- Content
- Login
+ Contenu
+ Connexion
Langue du contenu par défaut
Pays du contenu par défaut
Système par défaut
- Enable proxy
- Proxy type
- Proxy URL
- Restart to take effect
+ Activer le proxy
+ Type de proxy
+ URL proxy
+ Redémarrer pour prendre effet
- Player and audio
- Audio quality
+ Lecteur et audio
+ Qualité audio
Auto
- High
- Low
- Persistent queue
- Skip silence
- Audio normalization
- Equalizer
+ Haut
+ Faible
+ File d\'attente persistante
+ Ignorer le silence
+ Normalisation audio
+ Égaliseur
- Storage
+ Stockage
Cache
- Image Cache
- Song Cache
- Max cache size
- Unlimited
- Clear all downloads
- Max image cache size
- Clear image cache
- Max song cache size
- Clear song cache
- %s used
+ Cache d\'images
+ Cache de chanson
+ Taille maximale du cache
+ Illimité
+ Effacer tous les téléchargements
+ Taille maximale du cache d\'images
+ Effacer le cache des images
+ Taille maximale du cache de chansons
+ Effacer le cache de la chanson
+ %s utilisé
- Privacy
- Pause listen history
- Clear listen history
- Are you sure to clear all listen history?
- Pause search history
- Clear search history
- Are you sure to clear all search history?
- Enable KuGou lyrics provider
+ Confidentialité
+ Suspendre l\'historique d\'écoute
+ Effacer l\'historique d\'écoute
+ Voulez-vous vraiment effacer tout l\'historique d\'écoute?
+ Suspendre l\'historique des recherches
+ Effacer l\'historique
+ Voulez-vous vraiment effacer tout l\'historique de recherche?
+ Activer le fournisseur de paroles KuGou
- Backup and restore
- Backup
- Restore
- Imported playlist
- Backup created successfully
- Couldn\'t create backup
- Failed to restore backup
+ Sauvegarde et restauration
+ Sauvegarde
+ Restaurer
+ Liste de lecture importée
+ Sauvegarde créée avec succès
+ Impossible de créer la sauvegarde
+ Échec de la restauration de la sauvegarde
- À propos
+ À propos de
Version de l\'application
- New version available
+ Nouvelle version disponible
+ Translation Models
+ Clear translation models
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 @@
Előzmény
Statisztika
- Mood and Genres
- Account
+ Hangulat és műfajok
+ Fiók
Gyors választás
Hallgasson meg néhány dalt a gyors választások elkészítéséhez
Új kiadású albumok
@@ -36,6 +36,9 @@
Keresés
YouTube Music keresés…
Keresés könyvtárban…
+ Könyvtár
+ Lájkolt
+ Letöltött
Mind
Dalok
Videók
@@ -44,7 +47,7 @@
Listák
Közösségi listák
Kiemelt lejátszási listák
- Bookmarked
+ Könyvjelzőzött
Nincs találat
@@ -59,6 +62,7 @@
Újra
Rádió
Keverés
+ Reset
Részletek
@@ -82,6 +86,7 @@
Előzményből eltávolít
Keresés online
Szinkron.
+ Advanced
Létrehozás dátuma
@@ -143,16 +148,16 @@
- %d lejátszólista
- - %d week
- - %d weeks
+ - %d hét
+ - %d hét
- - %d month
- - %d months
+ - %d hónap
+ - %d hónap
- - %d year
- - %d years
+ - %d év
+ - %d év
@@ -255,4 +260,6 @@
App verzió
New version available
+ Translation Models
+ Clear translation models
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 @@
Caru
Cari di YouTube Music…
Cari di perpustakaan…
+ Library
+ Liked
+ Downloaded
Semua
Lagu
Video
@@ -58,6 +61,7 @@
Mencoba kembali
Radio
Acak
+ Reset
Rincian
@@ -81,6 +85,7 @@
Remove from history
Cari secara online
Sync
+ Advanced
Tanggal ditambahkan
@@ -246,4 +251,6 @@
Versi aplikasi
New version available
+ Translation Models
+ Clear translation models
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 @@
Cerca
Cerca su YouTube Music…
Cerca nella libreria…
+ Library
+ Liked
+ Downloaded
Tutto
Brani
Video
@@ -60,6 +63,7 @@
Riprova
Radio
Mischia
+ Reset
Dettagli
@@ -83,6 +87,7 @@
Rimuovi da cronologia
Cerca online
Sincronizza
+ Advanced
Data di aggiunta
@@ -264,4 +269,6 @@
Versione dell\'app
New version available
+ Translation Models
+ Clear translation models
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 @@
検索
YouTube Musicを検索…
ライブラリを検索…
+ Library
+ Liked
+ Downloaded
すべて
曲
動画
@@ -58,6 +61,7 @@
再試行
ラジオ
シャッフル
+ Reset
詳細
@@ -81,6 +85,7 @@
履歴から削除
オンラインで検索
同期
+ Advanced
追加日時
@@ -246,4 +251,6 @@
アプリのバージョン
New version available
+ Translation Models
+ Clear translation models
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 @@
검색
Search YouTube Music…
Search library…
+ Library
+ Liked
+ Downloaded
모두
노래
비디오
@@ -58,6 +61,7 @@
다시
Radio
셔플
+ Reset
Details
@@ -81,6 +85,7 @@
Remove from history
Search online
Sync
+ Advanced
추가된 날짜
@@ -246,4 +251,6 @@
앱 버전
New version available
+ Translation Models
+ Clear translation models
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 @@
തിരയുക
യൂട്യൂബ് സംഗീതം തിരയുക…
തിരയൽ ലൈബ്രറി…
+ Library
+ Liked
+ Downloaded
എല്ലാം
പാട്ടുകൾ
വീഡിയോകൾ
@@ -59,6 +62,7 @@
വീണ്ടും ശ്രമിക്കുക
റേഡിയോ
ഷഫിൾ
+ Reset
Details
@@ -82,6 +86,7 @@
Remove from history
Search online
Sync
+ Advanced
ചേർത്ത തീയതി
@@ -255,4 +260,6 @@
അപ്ലിക്കേഷൻ പതിപ്പ്
New version available
+ Translation Models
+ Clear translation models
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 @@
Zoeken
Zoeken via YouTube Music…
Zoeken in bibliotheek
+ Library
+ Liked
+ Downloaded
Alles
Nummers
Videos
@@ -59,6 +62,7 @@
Opnieuw
Radio
Shuffle
+ Reset
Details
@@ -82,6 +86,7 @@
Verwijder uit geschiedenis
Zoek online
Synchroniseer
+ Advanced
Datum toegevoegd
@@ -255,4 +260,6 @@
App versie
New version available
+ Translation Models
+ Clear translation models
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 @@
ସନ୍ଧାନ କରନ୍ତୁ
ୟୁଟ୍ୟୁବ୍ ମ୍ୟୁଜିକ୍ ଖୋଜ…
ଲାଇବ୍ରେରୀ ଖୋଜ…
+ Library
+ Liked
+ Downloaded
ସମସ୍ତ
ସଙ୍ଗୀତ ଗୁଡ଼ିକ
ଭିଡିଓ ଗୁଡ଼ିକ
@@ -59,6 +62,7 @@
ପୁନଃ ଚେଷ୍ଟା କରନ୍ତୁ
ରେଡିଓ
ଶଫଲ୍ କରନ୍ତୁ
+ Reset
ବିବରଣୀ
@@ -82,6 +86,7 @@
Remove from history
ଅନଲାଇନ୍ ସନ୍ଧାନ କରନ୍ତୁ
Sync
+ Advanced
ତାରିଖ ରେ ଯୋଡା ଯାଇଛି
@@ -255,4 +260,6 @@
ଆପ୍ ସଂସ୍କରଣ
New version available
+ Translation Models
+ Clear translation models
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 @@
ਖੋਜੋ
ਯੂਟਿਊਬ ਮਿਊਜ਼ਿਕ ਖੋਜੋ…
ਲਾਇਬ੍ਰੇਰੀ ਖੋਜੋ…
+ Library
+ Liked
+ Downloaded
ਸਾਰੇ
ਗੀਤ
ਵੀਡੀਓ
@@ -59,6 +62,7 @@
ਦੁਬਾਰਾ ਕੋਸ਼ਿਸ਼ ਕਰੋ
ਰੇਡੀਓ
ਸ਼ਫਲ ਕਰੋ
+ Reset
ਵੇਰਵੇ
@@ -82,6 +86,7 @@
ਅਤੀਤ ਵਿੱਚੋਂ ਹਟਾਓ
ਆਨਲਾਈਨ ਖੋਜ ਕਰੋ
ਸਿੰਕਰੋਨਾਈਜ਼ ਕਰੋ
+ Advanced
ਮਿਤੀ
@@ -255,4 +260,6 @@
ਐਪ ਸੰਸਕਰਣ
New version available
+ Translation Models
+ Clear translation models
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 @@
Szukaj
Szukaj w YouTube Music…
Szukaj w bibliotece…
+ Biblioteka
+ Polubione
+ Pobrane
Wszystko
Utwory
Filmy
@@ -61,6 +64,7 @@
Spróbuj ponownie
Radio
Losowo
+ Reset
Szczegóły
@@ -84,6 +88,7 @@
Usuń z historii
Szukaj online
Synchronizuj
+ Advanced
Data dodania
@@ -273,4 +278,6 @@
Wersja aplikacji
Dostępna nowa wersja
+ Translation Models
+ Clear translation models
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 @@
Pesquisar
Pesquisar no YouTube Music…
Pesquisar na biblioteca…
+ Library
+ Liked
+ Downloaded
Tudo
Músicas
Vídeos
@@ -60,6 +63,7 @@
Tentar novamente
Rádio
Aleatório
+ Reset
Detalhes
@@ -83,6 +87,7 @@
Remove from history
Search online
Sync
+ Advanced
Quando adicionada
@@ -264,4 +269,6 @@
Versão do aplicativo
New version available
+ Translation Models
+ Clear translation models
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 @@
Поиск
Поиск в YouTube Music…
Поиск в библиотеке…
+ Библиотека
+ Понравившиеся
+ Загруженные
Все
Композиции
Видео
@@ -61,6 +64,7 @@
Повторить
Радио
Перемешать
+ Сбросить
Подробнее
@@ -84,6 +88,7 @@
Удалить из истории
Поиск в Интернете
Синхронизация
+ Контроль аудио
Недавно добавленные
@@ -273,4 +278,6 @@
Версия приложения
Доступна новая версия
+ Translation Models
+ Clear translation models
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 @@
Sök
Search YouTube Music…
Search library…
+ Library
+ Liked
+ Downloaded
Alla
Låtar
Videor
@@ -59,6 +62,7 @@
Försök igen
Radio
Blanda
+ Reset
Details
@@ -82,6 +86,7 @@
Remove from history
Search online
Sync
+ Advanced
Datum tillagd
@@ -255,4 +260,6 @@
App version
New version available
+ Translation Models
+ Clear translation models
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 @@
Arama
YouTube Müzik\'te Ara…
Kütüphanede ara…
+ Kütüphane
+ Beğenilenler
+ İndirilenler
Hepsi
Şarkılar
Videolar
@@ -59,6 +62,7 @@
Yeniden dene
Radyo
Karıştır
+ Reset
Ayrıntılar
@@ -82,6 +86,7 @@
Geçmişten kaldır
Çevrimiçi arama
Eşitle
+ Advanced
Eklendiği tarih
@@ -255,4 +260,6 @@
Uygulama sürümü
Yeni sürüm mevcut
+ Translation Models
+ Clear translation models
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 @@
Пошук
Пошук в YouTube Music…
Пошук в бібліотеці…
+ Бібліотека
+ Вподобані
+ Завантажені
Всі
Композиції
Відео
@@ -61,6 +64,7 @@
Повторювати
Радіо
Перемішати
+ Скинути
Детальніше
@@ -84,6 +88,7 @@
Видалити з історії
Пошук в Інтернеті
Синхронізація
+ Контроль аудіо
Нещодавно додані
@@ -273,4 +278,6 @@
Версія застосунку
Доступна нова версія
+ Translation Models
+ Clear translation models
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 @@
Tìm kiếm
Tìm kiếm YouTube Music…
Tìm kiếm thư viện…
+ Library
+ Liked
+ Downloaded
Tất cả
Bài hát
Videos
@@ -58,6 +61,7 @@
Thử lại
Đài
Trộn
+ Reset
Chi tiết
@@ -81,6 +85,7 @@
Xoá khỏi lịch sử
Tìm kiếm trực tuyến
Đồng bộ
+ Advanced
Ngày thêm vào
@@ -126,31 +131,24 @@
- - Bài hát %d
- Các bài hát %d
- - Tác giả %d
- Các tác giả %d
- - %d album
- %d albums
- - Danh sách phát %d
- Các danh sách phát %d
- - %d tuần
- %d tuần
- - %d tháng
- %d tháng
- - %d năm
- %d năm
@@ -165,7 +163,6 @@
Hẹn giờ
Kết thúc bài hát
- - 1 phút
- %d phút
Không có stream
@@ -254,4 +251,6 @@
Phiên bản ứng dụng
New version available
+ Translation Models
+ Clear translation models
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 @@
搜索
搜索 YouTube Music…
搜索媒体库…
+ Library
+ Liked
+ Downloaded
全部
歌曲
视频
@@ -58,6 +61,7 @@
重试
电台
随机播放
+ Reset
详情
@@ -81,6 +85,7 @@
从记录中移除
在线搜索
同步
+ Advanced
新建时间
@@ -246,4 +251,6 @@
应用版本
New version available
+ Translation Models
+ Clear translation models
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 @@
搜尋
搜尋 YouTube Music…
搜尋媒體庫…
+ 媒體庫
+ 已按讚
+ 已下載
全部
歌曲
影片
@@ -58,6 +61,7 @@
重試
電台
隨機播放
+ 重設
詳細資訊
@@ -81,6 +85,7 @@
從記錄中移除
線上搜尋
同步
+ 進階
新增時間
@@ -246,4 +251,6 @@
應用程式版本
發現新版本
+ 翻譯模型
+ 清除翻譯模型
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 @@
Search
Search YouTube Music…
Search library…
+ Library
+ Liked
+ Downloaded
All
Songs
Videos
@@ -58,6 +61,7 @@
Retry
Radio
Shuffle
+ Reset
Details
@@ -81,6 +85,7 @@
Remove from history
Search online
Sync
+ Advanced
Date added
@@ -254,4 +259,6 @@
App version
New version available
+ Translation Models
+ Clear translation models
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 = runCatching {
- innerTube.accountMenu(WEB_REMIX).body().actions[0].openPopupAction.popup.multiPageMenuRenderer.header?.activeAccountHeaderRenderer?.toAccountInfo()
+ suspend fun accountInfo(): Result = runCatching {
+ innerTube.accountMenu(WEB_REMIX).body().actions[0].openPopupAction.popup.multiPageMenuRenderer.header?.activeAccountHeaderRenderer?.toAccountInfo()!!
}
@JvmInline