diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 901114823..9a63af9a4 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -1,8 +1,16 @@
name: Build debug APK
on:
+ workflow_dispatch:
push:
branches:
- '**'
+ paths-ignore:
+ - 'README.md'
+ - 'fastlane/**'
+ - 'assets/**'
+ - '.github/**/*.md'
+ - '.github/FUNDING.yml'
+ - '.github/ISSUE_TEMPLATE/**'
jobs:
build:
@@ -10,12 +18,11 @@ jobs:
steps:
- uses: actions/checkout@v3
+ - name: Decode google-services.json
+ run: echo ${{ secrets.GOOGLE_SERVICES }} | base64 -d >> app/google-services.json
+
- name: Decode Keystore
- id: decode_keystore
- uses: timheuer/base64-to-file@v1
- with:
- fileName: 'Key/music-debug.jks'
- encodedString: ${{ secrets.KEYSTORE }}
+ run: echo ${{ secrets.KEYSTORE }} | base64 -d >> app/music-debug.jks
- name: set up JDK 11
uses: actions/setup-java@v3
@@ -27,13 +34,12 @@ jobs:
- name: Build debug APK and run jvm tests
run: ./gradlew assembleDebug lintDebug testDebugUnitTest --stacktrace -DskipFormatKtlint
env:
- MUSIC_DEBUG_KEYSTORE_FILE: 'Key/music-debug.jks'
+ MUSIC_DEBUG_KEYSTORE_FILE: 'music-debug.jks'
MUSIC_DEBUG_SIGNING_STORE_PASSWORD: ${{ secrets.SIGNING_KEY_PASSWORD }}
- MUSIC_DEBUG_SIGNING_KEY_ALIAS: ${{ secrets.SIGNING_KEY_ALIAS }}
MUSIC_DEBUG_SIGNING_KEY_PASSWORD: ${{ secrets.SIGNING_STORE_PASSWORD }}
- name: Upload APK
- uses: actions/upload-artifact@v2
+ uses: actions/upload-artifact@v3
with:
name: app
- path: app/build/outputs/apk/debug/*.apk
\ No newline at end of file
+ path: app/build/outputs/apk/debug/*.apk
diff --git a/.github/workflows/build_pr.yml b/.github/workflows/build_pr.yml
index 2857a57a0..6fafe23f7 100644
--- a/.github/workflows/build_pr.yml
+++ b/.github/workflows/build_pr.yml
@@ -19,9 +19,11 @@ jobs:
- name: Build debug APK and run jvm tests
run: ./gradlew assembleDebug lintDebug testDebugUnitTest --stacktrace -DskipFormatKtlint
+ env:
+ PULL_REQUEST: 'true'
- name: Upload APK
- uses: actions/upload-artifact@v2
+ uses: actions/upload-artifact@v3
with:
name: app
- path: app/build/outputs/apk/debug/*.apk
\ No newline at end of file
+ path: app/build/outputs/apk/debug/*.apk
diff --git a/.gitignore b/.gitignore
index 7a638a3dd..ecfc60f0e 100755
--- a/.gitignore
+++ b/.gitignore
@@ -61,7 +61,7 @@ captures/
.cxx/
# Google Services (e.g. APIs or Firebase)
-# google-services.json
+google-services.json
# Freeline
freeline.py
diff --git a/README.md b/README.md
index f40518fe7..442d9899c 100644
--- a/README.md
+++ b/README.md
@@ -16,11 +16,13 @@ A Material 3 YouTube Music client for Android
- Play songs from YT/YT Music without ads
- Background playback
- Search songs, videos, albums, and playlists from YouTube Music
+- Login support
- Library management
- Cache and download songs for offline playback
- Synchronized lyrics
- Skip silence
- Audio normalization
+- Adjust tempo/pitch
- Dynamic theme
- Localization
- Android Auto support
@@ -66,7 +68,8 @@ before you create a pull request.
## Donate
-If you like InnerTune, you're welcome to send a donation.
+If you like InnerTune, you're welcome to send a donation. Donations will support the development,
+including bug fixes and new features.
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index eea305491..5b368d687 100755
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -6,6 +6,11 @@ plugins {
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")
+ }
}
android {
@@ -16,8 +21,8 @@ android {
applicationId = "com.zionhuang.music"
minSdk = 24
targetSdk = 33
- versionCode = 16
- versionName = "0.5.0"
+ versionCode = 17
+ versionName = "0.5.1"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
@@ -35,12 +40,9 @@ android {
signingConfigs {
getByName("debug") {
if (System.getenv("MUSIC_DEBUG_SIGNING_STORE_PASSWORD") != null) {
- val tmpFilePath = System.getProperty("user.home") + "/work/_temp/Key/"
- val allFilesFromDir = File(tmpFilePath).listFiles()
- val keystoreFile = allFilesFromDir?.first()
- storeFile = keystoreFile ?: file(System.getenv("MUSIC_DEBUG_KEYSTORE_FILE"))
+ storeFile = file(System.getenv("MUSIC_DEBUG_KEYSTORE_FILE"))
storePassword = System.getenv("MUSIC_DEBUG_SIGNING_STORE_PASSWORD")
- keyAlias = System.getenv("MUSIC_DEBUG_SIGNING_KEY_ALIAS")
+ keyAlias = "debug"
keyPassword = System.getenv("MUSIC_DEBUG_SIGNING_KEY_PASSWORD")
}
}
@@ -124,5 +126,11 @@ 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)
+
implementation(libs.timber)
}
diff --git a/app/schemas/com.zionhuang.music.db.InternalDatabase/11.json b/app/schemas/com.zionhuang.music.db.InternalDatabase/11.json
new file mode 100644
index 000000000..c3981e170
--- /dev/null
+++ b/app/schemas/com.zionhuang.music.db.InternalDatabase/11.json
@@ -0,0 +1,796 @@
+{
+ "formatVersion": 1,
+ "database": {
+ "version": 11,
+ "identityHash": "de2e37d1206f721ad51de3a08f66f99c",
+ "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": [],
+ "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, `songCount` INTEGER NOT NULL, `duration` INTEGER NOT NULL, `createDate` INTEGER NOT NULL, `lastUpdateTime` INTEGER NOT NULL, 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": "songCount",
+ "columnName": "songCount",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "duration",
+ "columnName": "duration",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "createDate",
+ "columnName": "createDate",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "lastUpdateTime",
+ "columnName": "lastUpdateTime",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "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, 'de2e37d1206f721ad51de3a08f66f99c')"
+ ]
+ }
+}
\ No newline at end of file
diff --git a/app/src/debug/res/xml-v25/shortcuts.xml b/app/src/debug/res/xml-v25/shortcuts.xml
new file mode 100644
index 000000000..a3ea2b929
--- /dev/null
+++ b/app/src/debug/res/xml-v25/shortcuts.xml
@@ -0,0 +1,43 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 302df9b66..d981d51ae 100755
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -104,6 +104,9 @@
+
+
NavigationTab.SONG
+ ACTION_ALBUMS -> NavigationTab.ALBUM
+ ACTION_PLAYLISTS -> NavigationTab.PLAYLIST
+ else -> null
+ }
+ }
val (query, onQueryChange) = rememberSaveable(stateSaver = TextFieldValue.Saver) {
mutableStateOf(TextFieldValue())
@@ -238,10 +261,12 @@ class MainActivity : ComponentActivity() {
}
var searchSource by rememberEnumPreference(SearchSourceKey, SearchSource.ONLINE)
+ val searchBarFocusRequester = remember { FocusRequester() }
+
val onSearch: (String) -> Unit = {
if (it.isNotEmpty()) {
onActiveChange(false)
- navController.navigate("search/$it")
+ navController.navigate("search/${URLEncoder.encode(it, "UTF-8")}")
if (dataStore[PauseSearchHistoryKey] != true) {
database.query {
insert(SearchHistory(query = it))
@@ -250,6 +275,10 @@ class MainActivity : ComponentActivity() {
}
}
+ var openSearchImmediately: Boolean by remember {
+ mutableStateOf(intent?.action == ACTION_SEARCH)
+ }
+
val shouldShowSearchBar = remember(active, navBackStackEntry) {
active || navigationItems.fastAny { it.route == navBackStackEntry?.destination?.route } ||
navBackStackEntry?.destination?.route?.startsWith("search/") == true
@@ -288,7 +317,9 @@ class MainActivity : ComponentActivity() {
LaunchedEffect(navBackStackEntry) {
if (navBackStackEntry?.destination?.route?.startsWith("search/") == true) {
- val searchQuery = navBackStackEntry?.arguments?.getString("query")!!
+ val searchQuery = withContext(Dispatchers.IO) {
+ URLDecoder.decode(navBackStackEntry?.arguments?.getString("query")!!, "UTF-8")
+ }
onQueryChange(TextFieldValue(searchQuery, TextRange(searchQuery.length)))
} else if (navigationItems.fastAny { it.route == navBackStackEntry?.destination?.route }) {
onQueryChange(TextFieldValue())
@@ -344,6 +375,8 @@ class MainActivity : ComponentActivity() {
songs.firstOrNull()?.album?.id?.let { browseId ->
navController.navigate("album/$browseId")
}
+ }.onFailure {
+ it.printStackTrace()
}
}
} else {
@@ -365,6 +398,8 @@ class MainActivity : ComponentActivity() {
YouTube.queue(listOf(videoId))
}.onSuccess {
sharedSong = it.firstOrNull()
+ }.onFailure {
+ it.printStackTrace()
}
}
}
@@ -385,7 +420,7 @@ class MainActivity : ComponentActivity() {
) {
NavHost(
navController = navController,
- startDestination = when (defaultOpenTab) {
+ startDestination = when (tabOpenedFromShortcut ?: defaultOpenTab) {
NavigationTab.HOME -> Screens.Home
NavigationTab.SONG -> Screens.Songs
NavigationTab.ARTIST -> Screens.Artists
@@ -415,6 +450,12 @@ class MainActivity : ComponentActivity() {
composable("stats") {
StatsScreen(navController)
}
+ composable("mood_and_genres") {
+ MoodAndGenresScreen(navController, scrollBehavior)
+ }
+ composable("account") {
+ AccountScreen(navController, scrollBehavior)
+ }
composable("new_release") {
NewReleaseScreen(navController, scrollBehavior)
}
@@ -506,8 +547,23 @@ class MainActivity : ComponentActivity() {
LocalPlaylistScreen(navController, scrollBehavior)
}
}
+ composable(
+ route = "youtube_browse/{browseId}?params={params}",
+ arguments = listOf(
+ navArgument("browseId") {
+ type = NavType.StringType
+ nullable = true
+ },
+ navArgument("params") {
+ type = NavType.StringType
+ nullable = true
+ }
+ )
+ ) {
+ YouTubeBrowseScreen(navController, scrollBehavior)
+ }
composable("settings") {
- SettingsScreen(navController, scrollBehavior)
+ SettingsScreen(latestVersion, navController, scrollBehavior)
}
composable("settings/appearance") {
AppearanceSettings(navController, scrollBehavior)
@@ -530,6 +586,9 @@ class MainActivity : ComponentActivity() {
composable("settings/about") {
AboutScreen(navController, scrollBehavior)
}
+ composable("login") {
+ LoginScreen(navController)
+ }
}
AnimatedVisibility(
@@ -605,9 +664,41 @@ class MainActivity : ComponentActivity() {
contentDescription = null
)
}
+ } else if (navBackStackEntry?.destination?.route in listOf(
+ Screens.Home.route,
+ Screens.Songs.route,
+ Screens.Artists.route,
+ Screens.Albums.route,
+ Screens.Playlists.route
+ )
+ ) {
+ Box(
+ contentAlignment = Alignment.Center,
+ modifier = Modifier
+ .size(48.dp)
+ .clip(CircleShape)
+ .clickable {
+ navController.navigate("settings")
+ }
+ ) {
+ BadgedBox(
+ badge = {
+ if (latestVersion > BuildConfig.VERSION_CODE) {
+ Badge()
+ }
+ }
+ ) {
+
+ Icon(
+ painter = painterResource(R.drawable.settings),
+ contentDescription = null
+ )
+ }
+ }
}
},
- modifier = Modifier.align(Alignment.TopCenter)
+ focusRequester = searchBarFocusRequester,
+ modifier = Modifier.align(Alignment.TopCenter),
) {
Crossfade(
targetState = searchSource,
@@ -629,7 +720,7 @@ class MainActivity : ComponentActivity() {
onQueryChange = onQueryChange,
navController = navController,
onSearch = {
- navController.navigate("search/$it")
+ navController.navigate("search/${URLEncoder.encode(it, "UTF-8")}")
if (dataStore[PauseSearchHistoryKey] != true) {
database.query {
insert(SearchHistory(query = it))
@@ -725,6 +816,14 @@ class MainActivity : ComponentActivity() {
}
}
}
+
+ LaunchedEffect(shouldShowSearchBar, openSearchImmediately) {
+ if (shouldShowSearchBar && openSearchImmediately) {
+ onActiveChange(true)
+ searchBarFocusRequester.requestFocus()
+ openSearchImmediately = false
+ }
+ }
}
}
}
@@ -743,6 +842,34 @@ class MainActivity : ComponentActivity() {
window.navigationBarColor = (if (isDark) Color.Transparent else Color.Black.copy(alpha = 0.2f)).toArgb()
}
}
+
+ 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"
+ const val ACTION_ALBUMS = "com.zionhuang.music.action.ALBUMS"
+ const val ACTION_PLAYLISTS = "com.zionhuang.music.action.PLAYLISTS"
+ }
}
val LocalDatabase = staticCompositionLocalOf { error("No database provided") }
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 ac29554e1..4c1053692 100644
--- a/app/src/main/java/com/zionhuang/music/constants/PreferenceKeys.kt
+++ b/app/src/main/java/com/zionhuang/music/constants/PreferenceKeys.kt
@@ -38,8 +38,8 @@ val SongSortTypeKey = stringPreferencesKey("songSortType")
val SongSortDescendingKey = booleanPreferencesKey("songSortDescending")
val DownloadedSongSortTypeKey = stringPreferencesKey("songSortType")
val DownloadedSongSortDescendingKey = booleanPreferencesKey("songSortDescending")
-val PlaylistSongSortTypeKey = stringPreferencesKey("songSortType")
-val PlaylistSongSortDescendingKey = booleanPreferencesKey("songSortDescending")
+val PlaylistSongSortTypeKey = stringPreferencesKey("playlistSongSortType")
+val PlaylistSongSortDescendingKey = booleanPreferencesKey("playlistSongSortDescending")
val ArtistSortTypeKey = stringPreferencesKey("artistSortType")
val ArtistSortDescendingKey = booleanPreferencesKey("artistSortDescending")
val AlbumSortTypeKey = stringPreferencesKey("albumSortType")
@@ -49,41 +49,45 @@ val PlaylistSortDescendingKey = booleanPreferencesKey("playlistSortDescending")
val ArtistSongSortTypeKey = stringPreferencesKey("artistSongSortType")
val ArtistSongSortDescendingKey = booleanPreferencesKey("artistSongSortDescending")
+val ArtistViewTypeKey = stringPreferencesKey("artistViewType")
+
val PlaylistEditLockKey = booleanPreferencesKey("playlistEditLock")
enum class SongSortType {
- CREATE_DATE, NAME, ARTIST
+ CREATE_DATE, NAME, ARTIST, PLAY_TIME
}
enum class DownloadedSongSortType {
- CREATE_DATE, NAME, ARTIST
+ CREATE_DATE, NAME, ARTIST, PLAY_TIME
}
enum class PlaylistSongSortType {
- CUSTOM, CREATE_DATE, NAME, ARTIST
+ CUSTOM, CREATE_DATE, NAME, ARTIST, PLAY_TIME
}
enum class ArtistSortType {
- CREATE_DATE, NAME, SONG_COUNT
+ CREATE_DATE, NAME, SONG_COUNT, PLAY_TIME
}
enum class ArtistSongSortType {
- CREATE_DATE, NAME
+ CREATE_DATE, NAME, PLAY_TIME
}
enum class AlbumSortType {
- CREATE_DATE, NAME, ARTIST, YEAR, SONG_COUNT, LENGTH
+ CREATE_DATE, NAME, ARTIST, YEAR, SONG_COUNT, LENGTH, PLAY_TIME
}
enum class PlaylistSortType {
CREATE_DATE, NAME, SONG_COUNT
}
+enum class ArtistViewType {
+ ALL, BOOKMARKED
+}
+
val ShowLyricsKey = booleanPreferencesKey("showLyrics")
val LyricsTextPositionKey = stringPreferencesKey("lyricsTextPosition")
-val NavTabConfigKey = stringPreferencesKey("navTabConfig")
-
val PlayerVolumeKey = floatPreferencesKey("playerVolume")
val RepeatModeKey = intPreferencesKey("repeatMode")
diff --git a/app/src/main/java/com/zionhuang/music/constants/StatPeriod.kt b/app/src/main/java/com/zionhuang/music/constants/StatPeriod.kt
new file mode 100644
index 000000000..6127aae46
--- /dev/null
+++ b/app/src/main/java/com/zionhuang/music/constants/StatPeriod.kt
@@ -0,0 +1,18 @@
+package com.zionhuang.music.constants
+
+import java.time.LocalDateTime
+import java.time.ZoneOffset
+
+enum class StatPeriod {
+ `1_WEEK`, `1_MONTH`, `3_MONTH`, `6_MONTH`, `1_YEAR`, ALL;
+
+ fun toTimeMillis(): Long =
+ when (this) {
+ `1_WEEK` -> LocalDateTime.now().minusWeeks(1).toInstant(ZoneOffset.UTC).toEpochMilli()
+ `1_MONTH` -> LocalDateTime.now().minusMonths(1).toInstant(ZoneOffset.UTC).toEpochMilli()
+ `3_MONTH` -> LocalDateTime.now().minusMonths(3).toInstant(ZoneOffset.UTC).toEpochMilli()
+ `6_MONTH` -> LocalDateTime.now().minusMonths(6).toInstant(ZoneOffset.UTC).toEpochMilli()
+ `1_YEAR` -> LocalDateTime.now().minusMonths(12).toInstant(ZoneOffset.UTC).toEpochMilli()
+ ALL -> 0
+ }
+}
\ No newline at end of file
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 e387ae520..7c86c4c31 100644
--- a/app/src/main/java/com/zionhuang/music/db/DatabaseDao.kt
+++ b/app/src/main/java/com/zionhuang/music/db/DatabaseDao.kt
@@ -30,6 +30,10 @@ interface DatabaseDao {
@Query("SELECT * FROM song WHERE inLibrary IS NOT NULL ORDER BY title")
fun songsByNameAsc(): Flow>
+ @Transaction
+ @Query("SELECT * FROM song WHERE inLibrary IS NOT NULL ORDER BY totalPlayTime")
+ fun songsByPlayTimeAsc(): Flow>
+
fun songs(sortType: SongSortType, descending: Boolean) =
when (sortType) {
SongSortType.CREATE_DATE -> songsByCreateDateAsc()
@@ -39,6 +43,8 @@ interface DatabaseDao {
song.artists.joinToString(separator = "") { it.name }
}
}
+
+ SongSortType.PLAY_TIME -> songsByPlayTimeAsc()
}.map { it.reversed(descending) }
@Transaction
@@ -53,6 +59,10 @@ interface DatabaseDao {
@Query("SELECT * FROM song WHERE liked ORDER BY title")
fun likedSongsByNameAsc(): Flow>
+ @Transaction
+ @Query("SELECT * FROM song WHERE liked ORDER BY totalPlayTime")
+ fun likedSongsByPlayTimeAsc(): Flow>
+
fun likedSongs(sortType: SongSortType, descending: Boolean) =
when (sortType) {
SongSortType.CREATE_DATE -> likedSongsByCreateDateAsc()
@@ -62,6 +72,8 @@ interface DatabaseDao {
song.artists.joinToString(separator = "") { it.name }
}
}
+
+ SongSortType.PLAY_TIME -> likedSongsByPlayTimeAsc()
}.map { it.reversed(descending) }
@Query("SELECT COUNT(1) FROM song WHERE liked")
@@ -83,10 +95,15 @@ interface DatabaseDao {
@Query("SELECT song.* FROM song_artist_map JOIN song ON song_artist_map.songId = song.id WHERE artistId = :artistId AND inLibrary IS NOT NULL ORDER BY title")
fun artistSongsByNameAsc(artistId: String): Flow>
+ @Transaction
+ @Query("SELECT song.* FROM song_artist_map JOIN song ON song_artist_map.songId = song.id WHERE artistId = :artistId AND inLibrary IS NOT NULL ORDER BY totalPlayTime")
+ fun artistSongsByPlayTimeAsc(artistId: String): Flow>
+
fun artistSongs(artistId: String, sortType: ArtistSongSortType, descending: Boolean) =
when (sortType) {
ArtistSongSortType.CREATE_DATE -> artistSongsByCreateDateAsc(artistId)
ArtistSongSortType.NAME -> artistSongsByNameAsc(artistId)
+ ArtistSongSortType.PLAY_TIME -> artistSongsByPlayTimeAsc(artistId)
}.map { it.reversed(descending) }
@Transaction
@@ -127,8 +144,19 @@ interface DatabaseDao {
fun quickPicks(now: Long = System.currentTimeMillis()): Flow>
@Transaction
- @Query("SELECT * FROM song ORDER BY totalPlayTime DESC LIMIT :limit")
- fun mostPlayedSongs(limit: Int = 6): Flow>
+ @Query(
+ """
+ SELECT *
+ FROM song
+ WHERE id IN (SELECT songId
+ FROM event
+ WHERE timestamp > :fromTimeStamp
+ GROUP BY songId
+ ORDER BY SUM(playTime) DESC
+ LIMIT :limit)
+ """
+ )
+ fun mostPlayedSongs(fromTimeStamp: Long, limit: Int = 6): Flow>
@Transaction
@Query(
@@ -139,17 +167,39 @@ interface DatabaseDao {
JOIN song ON song_artist_map.songId = song.id
WHERE artistId = artist.id
AND song.inLibrary IS NOT NULL) AS songCount
- FROM (SELECT artistId, SUM(playtime) AS totalPlaytime
- FROM (SELECT *, (SELECT totalPlayTime FROM song WHERE id = songId) AS playtime
- FROM song_artist_map)
- GROUP BY artistId)
- JOIN artist
- ON artist.id = artistId
- ORDER BY totalPlaytime DESC
+ FROM artist
+ JOIN(SELECT artistId, SUM(songTotalPlayTime) AS totalPlayTime
+ FROM song_artist_map
+ JOIN (SELECT songId, SUM(playTime) AS songTotalPlayTime
+ FROM event
+ WHERE timestamp > :fromTimeStamp
+ GROUP BY songId) AS e
+ ON song_artist_map.songId = e.songId
+ GROUP BY artistId
+ ORDER BY totalPlayTime DESC
+ LIMIT :limit)
+ ON artist.id = artistId
+ """
+ )
+ fun mostPlayedArtists(fromTimeStamp: Long, limit: Int = 6): Flow>
+
+ @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
"""
)
- fun mostPlayedArtists(limit: Int = 6): Flow>
+ fun mostPlayedAlbums(fromTimeStamp: Long, limit: Int = 6): Flow>
@Transaction
@Query("SELECT * FROM song WHERE id = :songId")
@@ -166,7 +216,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 rowId")
+ @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")
fun artistsByCreateDateAsc(): Flow>
@Transaction
@@ -177,11 +227,76 @@ interface DatabaseDao {
@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 songCount")
fun artistsBySongCountAsc(): Flow>
+ @Transaction
+ @Query(
+ """
+ SELECT artist.*,
+ (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
+ JOIN(SELECT artistId, SUM(totalPlayTime) AS totalPlayTime
+ FROM song_artist_map
+ JOIN song
+ ON song_artist_map.songId = song.id
+ GROUP BY artistId
+ ORDER BY totalPlayTime)
+ ON artist.id = artistId
+ WHERE songCount > 0
+ """
+ )
+ fun artistsByPlayTimeAsc(): 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 bookmarkedAt IS NOT NULL ORDER BY bookmarkedAt")
+ fun artistsBookmarkedByCreateDateAsc(): 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 bookmarkedAt IS NOT NULL ORDER BY name")
+ fun artistsBookmarkedByNameAsc(): 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 bookmarkedAt IS NOT NULL ORDER BY songCount")
+ fun artistsBookmarkedBySongCountAsc(): Flow>
+
+ @Transaction
+ @Query(
+ """
+ SELECT artist.*,
+ (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
+ JOIN(SELECT artistId, SUM(totalPlayTime) AS totalPlayTime
+ FROM song_artist_map
+ JOIN song
+ ON song_artist_map.songId = song.id
+ GROUP BY artistId
+ ORDER BY totalPlayTime)
+ ON artist.id = artistId
+ WHERE bookmarkedAt IS NOT NULL
+ """
+ )
+ fun artistsBookmarkedByPlayTimeAsc(): Flow>
+
fun artists(sortType: ArtistSortType, descending: Boolean) =
when (sortType) {
ArtistSortType.CREATE_DATE -> artistsByCreateDateAsc()
ArtistSortType.NAME -> artistsByNameAsc()
ArtistSortType.SONG_COUNT -> artistsBySongCountAsc()
+ ArtistSortType.PLAY_TIME -> artistsByPlayTimeAsc()
+ }.map { it.reversed(descending) }
+
+ fun artistsBookmarked(sortType: ArtistSortType, descending: Boolean) =
+ when (sortType) {
+ ArtistSortType.CREATE_DATE -> artistsBookmarkedByCreateDateAsc()
+ ArtistSortType.NAME -> artistsBookmarkedByNameAsc()
+ ArtistSortType.SONG_COUNT -> artistsBookmarkedBySongCountAsc()
+ ArtistSortType.PLAY_TIME -> artistsBookmarkedByPlayTimeAsc()
}.map { it.reversed(descending) }
@Query("SELECT * FROM artist WHERE id = :id")
@@ -211,6 +326,19 @@ interface DatabaseDao {
@Query("SELECT * FROM album ORDER BY duration")
fun albumsByLengthAsc(): Flow>
+ @Transaction
+ @Query(
+ """
+ SELECT album.*
+ FROM album
+ JOIN song
+ ON song.albumId = album.id
+ GROUP BY album.id
+ ORDER BY SUM(song.totalPlayTime)
+ """
+ )
+ fun albumsByPlayTimeAsc(): Flow>
+
fun albums(sortType: AlbumSortType, descending: Boolean) =
when (sortType) {
AlbumSortType.CREATE_DATE -> albumsByCreateDateAsc()
@@ -224,6 +352,7 @@ interface DatabaseDao {
AlbumSortType.YEAR -> albumsByYearAsc()
AlbumSortType.SONG_COUNT -> albumsBySongCountAsc()
AlbumSortType.LENGTH -> albumsByLengthAsc()
+ AlbumSortType.PLAY_TIME -> albumsByPlayTimeAsc()
}.map { it.reversed(descending) }
@Transaction
@@ -262,7 +391,7 @@ interface DatabaseDao {
fun searchSongs(query: String, previewSize: Int = Int.MAX_VALUE): 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 name LIKE '%' || :query || '%' AND songCount > 0 LIMIT :previewSize")
+ @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 name LIKE '%' || :query || '%' AND bookmarkedAt IS NOT NULL LIMIT :previewSize")
fun searchArtists(query: String, previewSize: Int = Int.MAX_VALUE): Flow>
@Transaction
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 42597f518..a867fa5d8 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 = 10,
+ version = 11,
exportSchema = true,
autoMigrations = [
AutoMigration(from = 2, to = 3),
@@ -69,7 +69,8 @@ class MusicDatabase(
AutoMigration(from = 6, to = 7, spec = Migration6To7::class),
AutoMigration(from = 7, to = 8, spec = Migration7To8::class),
AutoMigration(from = 8, to = 9),
- AutoMigration(from = 9, to = 10, spec = Migration9To10::class)
+ AutoMigration(from = 9, to = 10, spec = Migration9To10::class),
+ AutoMigration(from = 10, to = 11, spec = Migration10To11::class)
]
)
@TypeConverters(Converters::class)
@@ -99,7 +100,7 @@ val MIGRATION_1_2 = object : Migration(1, 2) {
val albumName: String? = null,
val liked: Boolean = false,
val totalPlayTime: Long = 0, // in milliseconds
- val downloadState: Int = SongEntity.STATE_NOT_DOWNLOADED,
+ val downloadState: Int = 0,
val createDate: LocalDateTime = LocalDateTime.now(),
val modifyDate: LocalDateTime = LocalDateTime.now(),
)
@@ -211,7 +212,7 @@ val MIGRATION_1_2 = object : Migration(1, 2) {
"artist", SQLiteDatabase.CONFLICT_ABORT, contentValuesOf(
"id" to artist.id,
"name" to artist.name,
- "createDate" to converters.dateToTimestamp(artist.createDate),
+ "createDate" to converters.dateToTimestamp(artist.lastUpdateTime),
"lastUpdateTime" to converters.dateToTimestamp(artist.lastUpdateTime)
)
)
@@ -305,4 +306,12 @@ class Migration7To8 : AutoMigrationSpec
@DeleteTable.Entries(
DeleteTable(tableName = "download")
)
-class Migration9To10 : AutoMigrationSpec
\ No newline at end of file
+class Migration9To10 : AutoMigrationSpec
+
+@DeleteColumn.Entries(
+ DeleteColumn(tableName = "song", columnName = "downloadState"),
+ DeleteColumn(tableName = "artist", columnName = "bannerUrl"),
+ DeleteColumn(tableName = "artist", columnName = "description"),
+ DeleteColumn(tableName = "artist", columnName = "createDate")
+)
+class Migration10To11 : AutoMigrationSpec
\ 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 e054e2e22..e38a3a937 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
@@ -12,13 +12,9 @@ data class ArtistEntity(
@PrimaryKey val id: String,
val name: String,
val thumbnailUrl: String? = null,
- val bannerUrl: String? = null,
- val description: String? = null,
- val createDate: LocalDateTime = LocalDateTime.now(),
val lastUpdateTime: LocalDateTime = LocalDateTime.now(),
+ val bookmarkedAt: LocalDateTime? = null,
) {
- override fun toString(): String = name
-
val isYouTubeArtist: Boolean
get() = id.startsWith("UC")
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 dfed7f0b7..c5a1d3681 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
@@ -16,7 +16,6 @@ data class SongEntity(
val albumName: String? = null,
val liked: Boolean = false,
val totalPlayTime: Long = 0, // in milliseconds
- val downloadState: Int = STATE_NOT_DOWNLOADED,
val inLibrary: LocalDateTime? = null,
) {
fun toggleLike() = copy(
@@ -25,11 +24,4 @@ data class SongEntity(
)
fun toggleLibrary() = copy(inLibrary = if (inLibrary == null) LocalDateTime.now() else null)
-
- companion object {
- const val STATE_NOT_DOWNLOADED = 0
- const val STATE_PREPARING = 1
- const val STATE_DOWNLOADING = 2
- const val STATE_DOWNLOADED = 3
- }
}
diff --git a/app/src/main/java/com/zionhuang/music/extensions/MediaItemExt.kt b/app/src/main/java/com/zionhuang/music/extensions/MediaItemExt.kt
index 50ae92c12..f0717300f 100644
--- a/app/src/main/java/com/zionhuang/music/extensions/MediaItemExt.kt
+++ b/app/src/main/java/com/zionhuang/music/extensions/MediaItemExt.kt
@@ -22,6 +22,7 @@ fun Song.toMediaItem() = MediaItem.Builder()
.setSubtitle(artists.joinToString { it.name })
.setArtist(artists.joinToString { it.name })
.setArtworkUri(song.thumbnailUrl?.toUri())
+ .setAlbumTitle(song.albumName)
.setMediaType(MEDIA_TYPE_MUSIC)
.build()
)
@@ -38,6 +39,7 @@ fun SongItem.toMediaItem() = MediaItem.Builder()
.setSubtitle(artists.joinToString { it.name })
.setArtist(artists.joinToString { it.name })
.setArtworkUri(thumbnail.toUri())
+ .setAlbumTitle(album?.name)
.setMediaType(MEDIA_TYPE_MUSIC)
.build()
)
@@ -54,6 +56,7 @@ fun MediaMetadata.toMediaItem() = MediaItem.Builder()
.setSubtitle(artists.joinToString { it.name })
.setArtist(artists.joinToString { it.name })
.setArtworkUri(thumbnailUrl?.toUri())
+ .setAlbumTitle(album?.title)
.setMediaType(MEDIA_TYPE_MUSIC)
.build()
)
diff --git a/app/src/main/java/com/zionhuang/music/playback/BitmapProvider.kt b/app/src/main/java/com/zionhuang/music/playback/BitmapProvider.kt
deleted file mode 100644
index 6043651de..000000000
--- a/app/src/main/java/com/zionhuang/music/playback/BitmapProvider.kt
+++ /dev/null
@@ -1,60 +0,0 @@
-package com.zionhuang.music.playback
-
-import android.content.Context
-import android.graphics.Bitmap
-import android.graphics.drawable.BitmapDrawable
-import android.util.LruCache
-import coil.imageLoader
-import coil.request.Disposable
-import coil.request.ImageRequest
-
-class BitmapProvider(private val context: Context) {
- var currentUrl: String? = null
- var currentBitmap: Bitmap? = null
- private val map = LruCache(MAX_CACHE_SIZE)
- private var disposable: Disposable? = null
- var onBitmapChanged: (Bitmap?) -> Unit = {}
- set(value) {
- field = value
- value(currentBitmap)
- }
-
- fun load(url: String, callback: (Bitmap) -> Unit): Bitmap? {
- if (url == currentUrl) return map.get(url)
- currentUrl = url
- disposable?.dispose()
- val cache = map.get(url)
- if (cache == null) {
- disposable = context.imageLoader.enqueue(
- ImageRequest.Builder(context)
- .data(url)
- .allowHardware(false)
- .target(
- onSuccess = { drawable ->
- val bitmap = (drawable as BitmapDrawable).bitmap
- map.put(url, bitmap)
- callback(bitmap)
- currentBitmap = bitmap
- onBitmapChanged(bitmap)
- }
- )
- .build()
- )
- } else {
- currentBitmap = cache
- onBitmapChanged(cache)
- }
- return cache
- }
-
- fun clear() {
- disposable?.dispose()
- currentUrl = null
- currentBitmap = null
- onBitmapChanged(null)
- }
-
- companion object {
- const val MAX_CACHE_SIZE = 15
- }
-}
\ No newline at end of file
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 b85b9bdb9..219738c80 100644
--- a/app/src/main/java/com/zionhuang/music/playback/MusicService.kt
+++ b/app/src/main/java/com/zionhuang/music/playback/MusicService.kt
@@ -171,7 +171,6 @@ class MusicService : MediaLibraryService(),
private val binder = MusicBinder()
private lateinit var connectivityManager: ConnectivityManager
- val bitmapProvider = BitmapProvider(this)
private val audioQuality by enumPreference(this, AudioQualityKey, AudioQuality.AUTO)
@@ -503,9 +502,6 @@ class MusicService : MediaLibraryService(),
}
}
}
- if (mediaItem == null) {
- bitmapProvider.clear()
- }
if (pauseWhenSongEnd) {
pauseWhenSongEnd = false
player.pause()
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 1fa3de085..74c63cb2c 100644
--- a/app/src/main/java/com/zionhuang/music/playback/PlayerConnection.kt
+++ b/app/src/main/java/com/zionhuang/music/playback/PlayerConnection.kt
@@ -1,6 +1,5 @@
package com.zionhuang.music.playback
-import android.graphics.Bitmap
import androidx.media3.common.MediaItem
import androidx.media3.common.PlaybackException
import androidx.media3.common.Player
@@ -37,7 +36,7 @@ class PlayerConnection(
val player = service.player
val playbackState = MutableStateFlow(player.playbackState)
- val playWhenReady = MutableStateFlow(player.playWhenReady)
+ private val playWhenReady = MutableStateFlow(player.playWhenReady)
val isPlaying = combine(playbackState, playWhenReady) { playbackState, playWhenReady ->
playWhenReady && playbackState != STATE_ENDED
}.stateIn(scope, SharingStarted.Lazily, player.playWhenReady && player.playbackState != STATE_ENDED)
@@ -63,17 +62,10 @@ class PlayerConnection(
val canSkipPrevious = MutableStateFlow(true)
val canSkipNext = MutableStateFlow(true)
- var onBitmapChanged: (Bitmap?) -> Unit = {}
- set(value) {
- field = value
- service.bitmapProvider.onBitmapChanged = value
- }
-
val error = MutableStateFlow(null)
init {
player.addListener(this)
- service.bitmapProvider.onBitmapChanged = onBitmapChanged
playbackState.value = player.playbackState
playWhenReady.value = player.playWhenReady
@@ -174,7 +166,6 @@ class PlayerConnection(
}
fun dispose() {
- service.bitmapProvider.onBitmapChanged = {}
player.removeListener(this)
}
}
diff --git a/app/src/main/java/com/zionhuang/music/ui/component/HideOnScrollFAB.kt b/app/src/main/java/com/zionhuang/music/ui/component/HideOnScrollFAB.kt
new file mode 100644
index 000000000..64581e51b
--- /dev/null
+++ b/app/src/main/java/com/zionhuang/music/ui/component/HideOnScrollFAB.kt
@@ -0,0 +1,82 @@
+package com.zionhuang.music.ui.component
+
+import androidx.annotation.DrawableRes
+import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.animation.slideInVertically
+import androidx.compose.animation.slideOutVertically
+import androidx.compose.foundation.ScrollState
+import androidx.compose.foundation.layout.BoxScope
+import androidx.compose.foundation.layout.WindowInsetsSides
+import androidx.compose.foundation.layout.only
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.windowInsetsPadding
+import androidx.compose.foundation.lazy.LazyListState
+import androidx.compose.material3.FloatingActionButton
+import androidx.compose.material3.Icon
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.unit.dp
+import com.zionhuang.music.LocalPlayerAwareWindowInsets
+import com.zionhuang.music.ui.utils.isScrollingUp
+
+@Composable
+fun BoxScope.HideOnScrollFAB(
+ visible: Boolean = true,
+ lazyListState: LazyListState,
+ @DrawableRes icon: Int,
+ onClick: () -> Unit,
+) {
+ AnimatedVisibility(
+ visible = visible && lazyListState.isScrollingUp(),
+ enter = slideInVertically { it },
+ exit = slideOutVertically { it },
+ modifier = Modifier
+ .align(Alignment.BottomEnd)
+ .windowInsetsPadding(
+ LocalPlayerAwareWindowInsets.current
+ .only(WindowInsetsSides.Bottom + WindowInsetsSides.Horizontal)
+ )
+ ) {
+ FloatingActionButton(
+ modifier = Modifier.padding(16.dp),
+ onClick = onClick
+ ) {
+ Icon(
+ painter = painterResource(icon),
+ contentDescription = null
+ )
+ }
+ }
+}
+
+@Composable
+fun BoxScope.HideOnScrollFAB(
+ visible: Boolean = true,
+ scrollState: ScrollState,
+ @DrawableRes icon: Int,
+ onClick: () -> Unit,
+) {
+ AnimatedVisibility(
+ visible = visible && scrollState.isScrollingUp(),
+ enter = slideInVertically { it },
+ exit = slideOutVertically { it },
+ modifier = Modifier
+ .align(Alignment.BottomEnd)
+ .windowInsetsPadding(
+ LocalPlayerAwareWindowInsets.current
+ .only(WindowInsetsSides.Bottom + WindowInsetsSides.Horizontal)
+ )
+ ) {
+ FloatingActionButton(
+ modifier = Modifier.padding(16.dp),
+ onClick = onClick
+ ) {
+ Icon(
+ painter = painterResource(icon),
+ contentDescription = null
+ )
+ }
+ }
+}
\ No newline at end of file
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 c2a46449c..1d35bf1aa 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
@@ -230,7 +230,7 @@ fun SongListItem(
) = ListItem(
title = song.song.title,
subtitle = joinByBullet(
- song.artists.joinToString(),
+ song.artists.joinToString { it.name },
makeTimeString(song.song.duration * 1000L)
),
badges = badges,
@@ -308,7 +308,7 @@ fun AlbumListItem(
) = ListItem(
title = album.album.title,
subtitle = joinByBullet(
- album.artists.joinToString(),
+ album.artists.joinToString { it.name },
pluralStringResource(R.plurals.n_song, album.album.songCount, album.album.songCount),
album.album.year?.toString()
),
@@ -551,7 +551,7 @@ fun YouTubeListItem(
is SongItem -> joinByBullet(item.artists.joinToString { it.name }, makeTimeString(item.duration?.times(1000L)))
is AlbumItem -> joinByBullet(item.artists?.joinToString { it.name }, item.year?.toString())
is ArtistItem -> null
- is PlaylistItem -> joinByBullet(item.author.name, item.songCountText)
+ is PlaylistItem -> joinByBullet(item.author?.name, item.songCountText)
},
badges = badges,
thumbnailContent = {
@@ -744,7 +744,7 @@ fun YouTubeGridItem(
is SongItem -> joinByBullet(item.artists.joinToString { it.name }, makeTimeString(item.duration?.times(1000L)))
is AlbumItem -> joinByBullet(item.artists?.joinToString { it.name }, item.year?.toString())
is ArtistItem -> null
- is PlaylistItem -> joinByBullet(item.author.name, item.songCountText)
+ is PlaylistItem -> joinByBullet(item.author?.name, item.songCountText)
}
if (subtitle != null) {
diff --git a/app/src/main/java/com/zionhuang/music/ui/component/NavigationTile.kt b/app/src/main/java/com/zionhuang/music/ui/component/NavigationTile.kt
new file mode 100644
index 000000000..ecd3771f9
--- /dev/null
+++ b/app/src/main/java/com/zionhuang/music/ui/component/NavigationTile.kt
@@ -0,0 +1,47 @@
+package com.zionhuang.music.ui.component
+
+import androidx.annotation.DrawableRes
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.unit.dp
+
+@Composable
+fun NavigationTile(
+ title: String,
+ @DrawableRes icon: Int,
+ onClick: () -> Unit,
+ modifier: Modifier = Modifier,
+) {
+ Column(
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.spacedBy(4.dp),
+ modifier = modifier
+ .clip(RoundedCornerShape(4.dp))
+ .clickable(onClick = onClick)
+ .padding(4.dp),
+ ) {
+ Icon(
+ painter = painterResource(icon),
+ contentDescription = null
+ )
+
+ Text(
+ text = title,
+ style = MaterialTheme.typography.labelMedium,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis
+ )
+ }
+}
diff --git a/app/src/main/java/com/zionhuang/music/ui/component/NavigationTitle.kt b/app/src/main/java/com/zionhuang/music/ui/component/NavigationTitle.kt
new file mode 100644
index 000000000..37551bdc8
--- /dev/null
+++ b/app/src/main/java/com/zionhuang/music/ui/component/NavigationTitle.kt
@@ -0,0 +1,55 @@
+package com.zionhuang.music.ui.component
+
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.WindowInsets
+import androidx.compose.foundation.layout.WindowInsetsSides
+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.windowInsetsPadding
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.unit.dp
+import com.zionhuang.music.R
+
+@Composable
+fun NavigationTitle(
+ title: String,
+ modifier: Modifier = Modifier,
+ onClick: (() -> Unit)? = null,
+) {
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ modifier = modifier
+ .fillMaxWidth()
+ .windowInsetsPadding(WindowInsets.systemBars.only(WindowInsetsSides.Horizontal))
+ .clickable(enabled = onClick != null) {
+ onClick?.invoke()
+ }
+ .padding(12.dp)
+ ) {
+ Column(
+ modifier = Modifier.weight(1f)
+ ) {
+ Text(
+ text = title,
+ style = MaterialTheme.typography.headlineSmall
+ )
+ }
+
+ if (onClick != null) {
+ Icon(
+ painter = painterResource(R.drawable.navigate_next),
+ contentDescription = null
+ )
+ }
+ }
+}
diff --git a/app/src/main/java/com/zionhuang/music/ui/component/Preference.kt b/app/src/main/java/com/zionhuang/music/ui/component/Preference.kt
index e0f0f8d4a..65f591d1e 100644
--- a/app/src/main/java/com/zionhuang/music/ui/component/Preference.kt
+++ b/app/src/main/java/com/zionhuang/music/ui/component/Preference.kt
@@ -1,15 +1,28 @@
package com.zionhuang.music.ui.component
-import androidx.annotation.DrawableRes
import androidx.compose.foundation.clickable
-import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.items
-import androidx.compose.material3.*
-import androidx.compose.runtime.*
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.ProvideTextStyle
+import androidx.compose.material3.RadioButton
+import androidx.compose.material3.Switch
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
-import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.TextRange
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.unit.dp
@@ -17,10 +30,10 @@ import androidx.compose.ui.unit.dp
@Composable
fun PreferenceEntry(
modifier: Modifier = Modifier,
- title: String,
+ title: @Composable () -> Unit,
description: String? = null,
content: (@Composable () -> Unit)? = null,
- @DrawableRes icon: Int? = null,
+ icon: (@Composable () -> Unit)? = null,
trailingContent: (@Composable () -> Unit)? = null,
onClick: () -> Unit,
isEnabled: Boolean = true,
@@ -40,10 +53,7 @@ fun PreferenceEntry(
Box(
modifier = Modifier.padding(horizontal = 4.dp)
) {
- Icon(
- painter = painterResource(icon),
- contentDescription = null
- )
+ icon()
}
Spacer(Modifier.width(12.dp))
@@ -53,10 +63,9 @@ fun PreferenceEntry(
verticalArrangement = Arrangement.Center,
modifier = Modifier.weight(1f)
) {
- Text(
- text = title,
- style = MaterialTheme.typography.titleMedium
- )
+ ProvideTextStyle(MaterialTheme.typography.titleMedium) {
+ title()
+ }
if (description != null) {
Text(
@@ -80,8 +89,8 @@ fun PreferenceEntry(
@Composable
fun ListPreference(
modifier: Modifier = Modifier,
- title: String,
- @DrawableRes icon: Int? = null,
+ title: @Composable () -> Unit,
+ icon: (@Composable () -> Unit)? = null,
selectedValue: T,
values: List,
valueText: @Composable (T) -> String,
@@ -134,8 +143,8 @@ fun ListPreference(
@Composable
inline fun > EnumListPreference(
modifier: Modifier = Modifier,
- title: String,
- @DrawableRes icon: Int,
+ noinline title: @Composable () -> Unit,
+ noinline icon: (@Composable () -> Unit)?,
selectedValue: T,
noinline valueText: @Composable (T) -> String,
noinline onValueSelected: (T) -> Unit,
@@ -156,9 +165,9 @@ inline fun > EnumListPreference(
@Composable
fun SwitchPreference(
modifier: Modifier = Modifier,
- title: String,
+ title: @Composable () -> Unit,
description: String? = null,
- @DrawableRes icon: Int? = null,
+ icon: (@Composable () -> Unit)? = null,
checked: Boolean,
onCheckedChange: (Boolean) -> Unit,
isEnabled: Boolean = true,
@@ -182,8 +191,8 @@ fun SwitchPreference(
@Composable
fun EditTextPreference(
modifier: Modifier = Modifier,
- title: String,
- @DrawableRes icon: Int? = null,
+ title: @Composable () -> Unit,
+ icon: (@Composable () -> Unit)? = null,
value: String,
onValueChange: (String) -> Unit,
singleLine: Boolean = true,
diff --git a/app/src/main/java/com/zionhuang/music/ui/component/SearchBar.kt b/app/src/main/java/com/zionhuang/music/ui/component/SearchBar.kt
index e6db353d1..42db9b6c6 100644
--- a/app/src/main/java/com/zionhuang/music/ui/component/SearchBar.kt
+++ b/app/src/main/java/com/zionhuang/music/ui/component/SearchBar.kt
@@ -68,6 +68,7 @@ fun SearchBar(
tonalElevation: Dp = SearchBarDefaults.Elevation,
windowInsets: WindowInsets = WindowInsets.systemBars,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
+ focusRequester: FocusRequester = remember { FocusRequester() },
content: @Composable ColumnScope.() -> Unit,
) {
val heightOffsetLimit = with(LocalDensity.current) {
@@ -172,6 +173,7 @@ fun SearchBar(
trailingIcon = trailingIcon,
colors = colors.inputFieldColors,
interactionSource = interactionSource,
+ focusRequester = focusRequester,
)
if (animationProgress > 0) {
@@ -204,8 +206,8 @@ private fun SearchBarInputField(
trailingIcon: @Composable (() -> Unit)? = null,
colors: TextFieldColors = SearchBarDefaults.inputFieldColors(),
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
+ focusRequester: FocusRequester = remember { FocusRequester() }
) {
- val focusRequester = remember { FocusRequester() }
val searchSemantics = getString(Strings.SearchBarSearch)
val suggestionsAvailableSemantics = getString(Strings.SuggestionsAvailable)
val textColor = LocalTextStyle.current.color.takeOrElse {
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
new file mode 100644
index 000000000..057e75b66
--- /dev/null
+++ b/app/src/main/java/com/zionhuang/music/ui/menu/PlayerMenu.kt
@@ -0,0 +1,387 @@
+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
+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.PaddingValues
+import androidx.compose.foundation.layout.Row
+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.lazy.items
+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
+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.LocalConfiguration
+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.TextAlign
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import androidx.core.net.toUri
+import androidx.media3.common.PlaybackParameters
+import androidx.media3.exoplayer.offline.DownloadRequest
+import androidx.media3.exoplayer.offline.DownloadService
+import androidx.navigation.NavController
+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.models.MediaMetadata
+import com.zionhuang.music.playback.ExoDownloadService
+import com.zionhuang.music.playback.PlayerConnection
+import com.zionhuang.music.ui.component.BigSeekBar
+import com.zionhuang.music.ui.component.BottomSheetState
+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 kotlin.math.log2
+import kotlin.math.pow
+import kotlin.math.round
+
+@Composable
+fun PlayerMenu(
+ mediaMetadata: MediaMetadata?,
+ navController: NavController,
+ playerBottomSheetState: BottomSheetState,
+ playerConnection: PlayerConnection,
+ onShowDetailsDialog: () -> Unit,
+ onDismiss: () -> Unit,
+) {
+ 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()) { }
+
+ val download by LocalDownloadUtil.current.getDownload(mediaMetadata.id).collectAsState(initial = null)
+
+ var showChoosePlaylistDialog by rememberSaveable {
+ mutableStateOf(false)
+ }
+
+ AddToPlaylistDialog(
+ isVisible = showChoosePlaylistDialog,
+ onAdd = { playlist ->
+ database.transaction {
+ insert(mediaMetadata)
+ insert(
+ PlaylistSongMap(
+ songId = mediaMetadata.id,
+ playlistId = playlist.id,
+ position = playlist.songCount
+ )
+ )
+ }
+ },
+ onDismiss = {
+ showChoosePlaylistDialog = false
+ }
+ )
+
+ var showSelectArtistDialog by rememberSaveable {
+ mutableStateOf(false)
+ }
+
+ if (showSelectArtistDialog) {
+ ListDialog(
+ onDismiss = { showSelectArtistDialog = false }
+ ) {
+ items(mediaMetadata.artists) { artist ->
+ Box(
+ contentAlignment = Alignment.CenterStart,
+ modifier = Modifier
+ .fillParentMaxWidth()
+ .height(ListItemHeight)
+ .clickable {
+ navController.navigate("artist/${artist.id}")
+ showSelectArtistDialog = false
+ playerBottomSheetState.collapseSoft()
+ onDismiss()
+ }
+ .padding(horizontal = 24.dp),
+ ) {
+ Text(
+ text = artist.name,
+ fontSize = 18.sp,
+ fontWeight = FontWeight.Bold,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis
+ )
+ }
+ }
+ }
+ }
+
+ 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))
+ }
+
+ Row(
+ horizontalArrangement = Arrangement.spacedBy(24.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 24.dp)
+ .padding(top = 24.dp, bottom = 6.dp)
+ ) {
+ Icon(
+ painter = painterResource(R.drawable.volume_up),
+ contentDescription = null,
+ modifier = Modifier.size(28.dp)
+ )
+
+ BigSeekBar(
+ progressProvider = playerVolume::value,
+ onProgressChange = { playerConnection.service.playerVolume.value = it },
+ modifier = Modifier.weight(1f)
+ )
+ }
+
+ 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,
+ top = 8.dp,
+ end = 8.dp,
+ bottom = 8.dp + WindowInsets.systemBars.asPaddingValues().calculateBottomPadding()
+ )
+ ) {
+ GridMenuItem(
+ icon = R.drawable.radio,
+ title = R.string.start_radio
+ ) {
+ playerConnection.service.startRadioSeamlessly()
+ onDismiss()
+ }
+ GridMenuItem(
+ icon = R.drawable.playlist_add,
+ title = R.string.add_to_playlist
+ ) {
+ showChoosePlaylistDialog = true
+ }
+ DownloadGridMenu(
+ state = download?.state,
+ onDownload = {
+ database.transaction {
+ insert(mediaMetadata)
+ }
+ val downloadRequest = DownloadRequest.Builder(mediaMetadata.id, mediaMetadata.id.toUri())
+ .setCustomCacheKey(mediaMetadata.id)
+ .setData(mediaMetadata.title.toByteArray())
+ .build()
+ DownloadService.sendAddDownload(
+ context,
+ ExoDownloadService::class.java,
+ downloadRequest,
+ false
+ )
+ },
+ onRemoveDownload = {
+ DownloadService.sendRemoveDownload(
+ context,
+ ExoDownloadService::class.java,
+ mediaMetadata.id,
+ false
+ )
+ }
+ )
+ GridMenuItem(
+ icon = R.drawable.artist,
+ title = R.string.view_artist
+ ) {
+ if (mediaMetadata.artists.size == 1) {
+ navController.navigate("artist/${mediaMetadata.artists[0].id}")
+ playerBottomSheetState.collapseSoft()
+ onDismiss()
+ } else {
+ showSelectArtistDialog = true
+ }
+ }
+ if (mediaMetadata.album != null) {
+ GridMenuItem(
+ icon = R.drawable.album,
+ title = R.string.view_album
+ ) {
+ navController.navigate("album/${mediaMetadata.album.id}")
+ playerBottomSheetState.collapseSoft()
+ onDismiss()
+ }
+ }
+ GridMenuItem(
+ icon = R.drawable.share,
+ title = R.string.share
+ ) {
+ val intent = Intent().apply {
+ action = Intent.ACTION_SEND
+ type = "text/plain"
+ putExtra(Intent.EXTRA_TEXT, "https://music.youtube.com/watch?v=${mediaMetadata.id}")
+ }
+ context.startActivity(Intent.createChooser(intent, null))
+ onDismiss()
+ }
+ GridMenuItem(
+ icon = R.drawable.info,
+ title = R.string.details
+ ) {
+ onShowDetailsDialog()
+ onDismiss()
+ }
+ GridMenuItem(
+ icon = R.drawable.equalizer,
+ title = R.string.equalizer
+ ) {
+ val intent = Intent(AudioEffect.ACTION_DISPLAY_AUDIO_EFFECT_CONTROL_PANEL).apply {
+ putExtra(AudioEffect.EXTRA_AUDIO_SESSION, playerConnection.player.audioSessionId)
+ putExtra(AudioEffect.EXTRA_PACKAGE_NAME, context.packageName)
+ putExtra(AudioEffect.EXTRA_CONTENT_TYPE, AudioEffect.CONTENT_TYPE_MUSIC)
+ }
+ if (intent.resolveActivity(context.packageManager) != null) {
+ activityResultLauncher.launch(intent)
+ }
+ onDismiss()
+ }
+ }
+}
+
+@Composable
+fun ValueAdjuster(
+ @DrawableRes icon: Int,
+ currentValue: T,
+ values: List,
+ onValueUpdate: (T) -> Unit,
+ valueText: (T) -> String,
+ modifier: Modifier,
+) {
+ Row(
+ horizontalArrangement = Arrangement.spacedBy(24.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ modifier = modifier
+ ) {
+ Icon(
+ painter = painterResource(icon),
+ contentDescription = null,
+ modifier = Modifier.size(28.dp)
+ )
+
+ IconButton(
+ enabled = currentValue != values.first(),
+ onClick = {
+ onValueUpdate(values[values.indexOf(currentValue) - 1])
+ }
+ ) {
+ Icon(
+ painter = painterResource(R.drawable.remove),
+ contentDescription = null
+ )
+ }
+
+ Text(
+ text = valueText(currentValue),
+ style = MaterialTheme.typography.titleMedium,
+ textAlign = TextAlign.Center,
+ modifier = Modifier.weight(1f)
+ )
+
+ IconButton(
+ enabled = currentValue != values.last(),
+ onClick = {
+ onValueUpdate(values[values.indexOf(currentValue) + 1])
+ }
+ ) {
+ Icon(
+ painter = painterResource(R.drawable.add),
+ 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 830e3a928..badff73af 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,7 +2,9 @@ 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
@@ -16,6 +18,7 @@ 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
@@ -23,8 +26,11 @@ 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
@@ -34,28 +40,66 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
+import androidx.media3.common.PlaybackException
import androidx.media3.common.Player
import coil.compose.AsyncImage
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(
- mediaMetadata: MediaMetadata?,
position: Long,
duration: Long,
modifier: Modifier = Modifier,
) {
- mediaMetadata ?: return
val playerConnection = LocalPlayerConnection.current ?: return
val isPlaying by playerConnection.isPlaying.collectAsState()
val playbackState by playerConnection.playbackState.collectAsState()
- val canSkipNext by playerConnection.canSkipNext.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)
+ }
+ }
+ }
Box(
modifier = modifier
@@ -74,62 +118,25 @@ fun MiniPlayer(
verticalAlignment = Alignment.CenterVertically,
modifier = modifier
.fillMaxSize()
- .padding(horizontal = 6.dp),
+ .padding(end = 12.dp),
) {
- Box(modifier = Modifier.padding(6.dp)) {
- AsyncImage(
- model = mediaMetadata.thumbnailUrl,
- contentDescription = null,
- modifier = Modifier
- .size(48.dp)
- .clip(RoundedCornerShape(ThumbnailCornerRadius))
- )
- androidx.compose.animation.AnimatedVisibility(
- visible = error != null,
- enter = fadeIn(),
- exit = fadeOut()
- ) {
- Box(
- Modifier
- .size(48.dp)
- .background(
- color = Color.Black.copy(alpha = 0.6f),
- shape = RoundedCornerShape(ThumbnailCornerRadius)
- )
- ) {
- Icon(
- painter = painterResource(R.drawable.info),
- contentDescription = null,
- tint = MaterialTheme.colorScheme.error,
- modifier = Modifier
- .align(Alignment.Center)
- )
- }
+ HorizontalPager(
+ state = pagerState,
+ flingBehavior = rememberSnapFlingBehavior(snapLayoutInfoProvider),
+ items = windows,
+ key = { it.uid.hashCode() },
+ beyondBoundsPageCount = 2,
+ modifier = Modifier.weight(1f)
+ ) { window ->
+ window.mediaItem.metadata?.let {
+ MiniMediaInfo(
+ mediaMetadata = it,
+ error = error,
+ modifier = Modifier.padding(horizontal = 6.dp)
+ )
}
}
- Column(
- modifier = Modifier
- .weight(1f)
- .padding(horizontal = 6.dp)
- ) {
- Text(
- text = mediaMetadata.title,
- color = MaterialTheme.colorScheme.onSurface,
- fontSize = 16.sp,
- fontWeight = FontWeight.Bold,
- maxLines = 1,
- overflow = TextOverflow.Ellipsis,
- )
- Text(
- text = mediaMetadata.artists.joinToString { it.name },
- color = MaterialTheme.colorScheme.secondary,
- fontSize = 12.sp,
- maxLines = 1,
- overflow = TextOverflow.Ellipsis,
- )
- }
-
IconButton(
onClick = {
if (playbackState == Player.STATE_ENDED) {
@@ -145,15 +152,72 @@ fun MiniPlayer(
contentDescription = null
)
}
- IconButton(
- enabled = canSkipNext,
- onClick = playerConnection.player::seekToNext
+ }
+ }
+}
+
+@Composable
+fun MiniMediaInfo(
+ mediaMetadata: MediaMetadata,
+ error: PlaybackException?,
+ modifier: Modifier = Modifier,
+) {
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ modifier = modifier
+ ) {
+ Box(modifier = Modifier.padding(6.dp)) {
+ AsyncImage(
+ model = mediaMetadata.thumbnailUrl,
+ contentDescription = null,
+ modifier = Modifier
+ .size(48.dp)
+ .clip(RoundedCornerShape(ThumbnailCornerRadius))
+ )
+ androidx.compose.animation.AnimatedVisibility(
+ visible = error != null,
+ enter = fadeIn(),
+ exit = fadeOut()
) {
- Icon(
- painter = painterResource(R.drawable.skip_next),
- contentDescription = null
- )
+ Box(
+ Modifier
+ .size(48.dp)
+ .background(
+ color = Color.Black.copy(alpha = 0.6f),
+ shape = RoundedCornerShape(ThumbnailCornerRadius)
+ )
+ ) {
+ Icon(
+ painter = painterResource(R.drawable.info),
+ contentDescription = null,
+ tint = MaterialTheme.colorScheme.error,
+ modifier = Modifier
+ .align(Alignment.Center)
+ )
+ }
}
}
+
+ Column(
+ modifier = Modifier
+ .weight(1f)
+ .padding(horizontal = 6.dp)
+ ) {
+ Text(
+ text = mediaMetadata.title,
+ color = MaterialTheme.colorScheme.onSurface,
+ fontSize = 16.sp,
+ fontWeight = FontWeight.Bold,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis,
+ )
+ Text(
+ text = mediaMetadata.artists.joinToString { it.name },
+ color = MaterialTheme.colorScheme.secondary,
+ fontSize = 12.sp,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis,
+ )
+ }
}
-}
\ No newline at end of file
+}
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 71a388061..f5766cbfb 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
@@ -119,7 +119,6 @@ fun BottomSheetPlayer(
},
collapsedContent = {
MiniPlayer(
- mediaMetadata = mediaMetadata,
position = position,
duration = duration
)
diff --git a/app/src/main/java/com/zionhuang/music/ui/player/Queue.kt b/app/src/main/java/com/zionhuang/music/ui/player/Queue.kt
index 444c5c393..0719dc1b8 100644
--- a/app/src/main/java/com/zionhuang/music/ui/player/Queue.kt
+++ b/app/src/main/java/com/zionhuang/music/ui/player/Queue.kt
@@ -1,11 +1,7 @@
package com.zionhuang.music.ui.player
-import android.content.Intent
-import android.media.audiofx.AudioEffect
import android.text.format.Formatter
import android.widget.Toast
-import androidx.activity.compose.rememberLauncherForActivityResult
-import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.foundation.background
@@ -14,7 +10,6 @@ import androidx.compose.foundation.interaction.MutableInteractionSource
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.Spacer
import androidx.compose.foundation.layout.WindowInsets
@@ -26,11 +21,9 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.only
import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.sizeIn
import androidx.compose.foundation.layout.systemBars
import androidx.compose.foundation.layout.windowInsetsPadding
-import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.rememberScrollState
@@ -71,39 +64,23 @@ import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.pluralStringResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString
-import androidx.compose.ui.text.font.FontWeight
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.exoplayer.offline.DownloadRequest
-import androidx.media3.exoplayer.offline.DownloadService
import androidx.media3.exoplayer.source.ShuffleOrder.DefaultShuffleOrder
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.constants.ShowLyricsKey
-import com.zionhuang.music.db.entities.PlaylistSongMap
import com.zionhuang.music.extensions.metadata
import com.zionhuang.music.extensions.move
import com.zionhuang.music.extensions.togglePlayPause
-import com.zionhuang.music.models.MediaMetadata
-import com.zionhuang.music.playback.ExoDownloadService
-import com.zionhuang.music.playback.PlayerConnection
-import com.zionhuang.music.ui.component.BigSeekBar
import com.zionhuang.music.ui.component.BottomSheet
import com.zionhuang.music.ui.component.BottomSheetState
-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.LocalMenuState
import com.zionhuang.music.ui.component.MediaMetadataListItem
-import com.zionhuang.music.ui.menu.AddToPlaylistDialog
+import com.zionhuang.music.ui.menu.PlayerMenu
import com.zionhuang.music.ui.utils.reordering.ReorderingLazyColumn
import com.zionhuang.music.ui.utils.reordering.draggedItem
import com.zionhuang.music.ui.utils.reordering.rememberReorderingState
@@ -549,203 +526,3 @@ fun Queue(
}
}
}
-
-@Composable
-fun PlayerMenu(
- mediaMetadata: MediaMetadata?,
- navController: NavController,
- playerBottomSheetState: BottomSheetState,
- playerConnection: PlayerConnection,
- onShowDetailsDialog: () -> Unit,
- onDismiss: () -> Unit,
-) {
- mediaMetadata ?: return
- val context = LocalContext.current
- val database = LocalDatabase.current
- val playerVolume = playerConnection.service.playerVolume.collectAsState()
- val activityResultLauncher = rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { }
-
- val download by LocalDownloadUtil.current.getDownload(mediaMetadata.id).collectAsState(initial = null)
-
- var showChoosePlaylistDialog by rememberSaveable {
- mutableStateOf(false)
- }
-
- AddToPlaylistDialog(
- isVisible = showChoosePlaylistDialog,
- onAdd = { playlist ->
- database.transaction {
- insert(mediaMetadata)
- insert(
- PlaylistSongMap(
- songId = mediaMetadata.id,
- playlistId = playlist.id,
- position = playlist.songCount
- )
- )
- }
- },
- onDismiss = {
- showChoosePlaylistDialog = false
- }
- )
-
- var showSelectArtistDialog by rememberSaveable {
- mutableStateOf(false)
- }
-
- if (showSelectArtistDialog) {
- ListDialog(
- onDismiss = { showSelectArtistDialog = false }
- ) {
- items(mediaMetadata.artists) { artist ->
- Box(
- contentAlignment = Alignment.CenterStart,
- modifier = Modifier
- .fillParentMaxWidth()
- .height(ListItemHeight)
- .clickable {
- navController.navigate("artist/${artist.id}")
- showSelectArtistDialog = false
- playerBottomSheetState.collapseSoft()
- onDismiss()
- }
- .padding(horizontal = 24.dp),
- ) {
- Text(
- text = artist.name,
- fontSize = 18.sp,
- fontWeight = FontWeight.Bold,
- maxLines = 1,
- overflow = TextOverflow.Ellipsis
- )
- }
- }
- }
- }
-
- Row(
- horizontalArrangement = Arrangement.spacedBy(24.dp),
- verticalAlignment = Alignment.CenterVertically,
- modifier = Modifier
- .fillMaxWidth()
- .padding(horizontal = 24.dp)
- .padding(top = 24.dp, bottom = 12.dp)
- ) {
- Icon(
- painter = painterResource(R.drawable.volume_up),
- contentDescription = null,
- modifier = Modifier.size(28.dp)
- )
-
- BigSeekBar(
- progressProvider = playerVolume::value,
- onProgressChange = { playerConnection.service.playerVolume.value = it },
- modifier = Modifier.weight(1f)
- )
- }
-
- GridMenu(
- contentPadding = PaddingValues(
- start = 8.dp,
- top = 8.dp,
- end = 8.dp,
- bottom = 8.dp + WindowInsets.systemBars.asPaddingValues().calculateBottomPadding()
- )
- ) {
- GridMenuItem(
- icon = R.drawable.radio,
- title = R.string.start_radio
- ) {
- playerConnection.service.startRadioSeamlessly()
- onDismiss()
- }
- GridMenuItem(
- icon = R.drawable.playlist_add,
- title = R.string.add_to_playlist
- ) {
- showChoosePlaylistDialog = true
- }
- DownloadGridMenu(
- state = download?.state,
- onDownload = {
- database.transaction {
- insert(mediaMetadata)
- }
- val downloadRequest = DownloadRequest.Builder(mediaMetadata.id, mediaMetadata.id.toUri())
- .setCustomCacheKey(mediaMetadata.id)
- .setData(mediaMetadata.title.toByteArray())
- .build()
- DownloadService.sendAddDownload(
- context,
- ExoDownloadService::class.java,
- downloadRequest,
- false
- )
- },
- onRemoveDownload = {
- DownloadService.sendRemoveDownload(
- context,
- ExoDownloadService::class.java,
- mediaMetadata.id,
- false
- )
- }
- )
- GridMenuItem(
- icon = R.drawable.artist,
- title = R.string.view_artist
- ) {
- if (mediaMetadata.artists.size == 1) {
- navController.navigate("artist/${mediaMetadata.artists[0].id}")
- playerBottomSheetState.collapseSoft()
- onDismiss()
- } else {
- showSelectArtistDialog = true
- }
- }
- if (mediaMetadata.album != null) {
- GridMenuItem(
- icon = R.drawable.album,
- title = R.string.view_album
- ) {
- navController.navigate("album/${mediaMetadata.album.id}")
- playerBottomSheetState.collapseSoft()
- onDismiss()
- }
- }
- GridMenuItem(
- icon = R.drawable.share,
- title = R.string.share
- ) {
- val intent = Intent().apply {
- action = Intent.ACTION_SEND
- type = "text/plain"
- putExtra(Intent.EXTRA_TEXT, "https://music.youtube.com/watch?v=${mediaMetadata.id}")
- }
- context.startActivity(Intent.createChooser(intent, null))
- onDismiss()
- }
- GridMenuItem(
- icon = R.drawable.info,
- title = R.string.details
- ) {
- onShowDetailsDialog()
- onDismiss()
- }
- GridMenuItem(
- icon = R.drawable.equalizer,
- title = R.string.equalizer
- ) {
- val intent = Intent(AudioEffect.ACTION_DISPLAY_AUDIO_EFFECT_CONTROL_PANEL).apply {
- putExtra(AudioEffect.EXTRA_AUDIO_SESSION, playerConnection.player.audioSessionId)
- putExtra(AudioEffect.EXTRA_PACKAGE_NAME, context.packageName)
- putExtra(AudioEffect.EXTRA_CONTENT_TYPE, AudioEffect.CONTENT_TYPE_MUSIC)
- }
- if (intent.resolveActivity(context.packageManager) != null) {
- activityResultLauncher.launch(intent)
- }
- onDismiss()
- }
- }
-}
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 07d761da2..8950e644a 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
@@ -26,6 +26,7 @@ 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
@@ -56,7 +57,11 @@ fun Thumbnail(
LaunchedEffect(pagerState, currentWindowIndex) {
if (windows.isNotEmpty()) {
try {
- pagerState.animateScrollToPage(currentWindowIndex)
+ if (abs(pagerState.currentPage - currentWindowIndex) <= 1) {
+ pagerState.animateScrollToPage(currentWindowIndex)
+ } else {
+ pagerState.scrollToPage(currentWindowIndex)
+ }
} catch (_: Exception) {
}
}
@@ -70,6 +75,12 @@ fun Thumbnail(
}
}
+ LaunchedEffect(showLyrics) {
+ if (!showLyrics) {
+ pagerState.scrollToPage(currentWindowIndex)
+ }
+ }
+
DisposableEffect(showLyrics) {
currentView.keepScreenOn = showLyrics
onDispose {
diff --git a/app/src/main/java/com/zionhuang/music/ui/screens/AccountScreen.kt b/app/src/main/java/com/zionhuang/music/ui/screens/AccountScreen.kt
new file mode 100644
index 000000000..ad104f7f6
--- /dev/null
+++ b/app/src/main/java/com/zionhuang/music/ui/screens/AccountScreen.kt
@@ -0,0 +1,101 @@
+package com.zionhuang.music.ui.screens
+
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.combinedClickable
+import androidx.compose.foundation.layout.asPaddingValues
+import androidx.compose.foundation.lazy.grid.GridCells
+import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
+import androidx.compose.foundation.lazy.grid.items
+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.rememberCoroutineScope
+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.music.LocalPlayerAwareWindowInsets
+import com.zionhuang.music.LocalPlayerConnection
+import com.zionhuang.music.R
+import com.zionhuang.music.constants.GridThumbnailHeight
+import com.zionhuang.music.ui.component.LocalMenuState
+import com.zionhuang.music.ui.component.YouTubeGridItem
+import com.zionhuang.music.ui.component.shimmer.GridItemPlaceHolder
+import com.zionhuang.music.ui.component.shimmer.ShimmerHost
+import com.zionhuang.music.ui.menu.YouTubePlaylistMenu
+import com.zionhuang.music.viewmodels.AccountViewModel
+
+@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class)
+@Composable
+fun AccountScreen(
+ navController: NavController,
+ scrollBehavior: TopAppBarScrollBehavior,
+ viewModel: AccountViewModel = hiltViewModel(),
+) {
+ val menuState = LocalMenuState.current
+ val playerConnection = LocalPlayerConnection.current ?: return
+
+ val coroutineScope = rememberCoroutineScope()
+
+ val playlists by viewModel.playlists.collectAsState()
+
+ LazyVerticalGrid(
+ columns = GridCells.Adaptive(minSize = GridThumbnailHeight + 24.dp),
+ contentPadding = LocalPlayerAwareWindowInsets.current.asPaddingValues()
+ ) {
+ items(
+ items = playlists.orEmpty(),
+ key = { it.id }
+ ) { item ->
+ YouTubeGridItem(
+ item = item,
+ fillMaxWidth = true,
+ modifier = Modifier
+ .combinedClickable(
+ onClick = {
+ navController.navigate("online_playlist/${item.id}")
+ },
+ onLongClick = {
+ menuState.show {
+ YouTubePlaylistMenu(
+ playlist = item,
+ playerConnection = playerConnection,
+ coroutineScope = coroutineScope,
+ onDismiss = menuState::dismiss
+ )
+ }
+ }
+ )
+ )
+ }
+
+ if (playlists == null) {
+ items(8) {
+ ShimmerHost {
+ GridItemPlaceHolder(fillMaxWidth = true)
+ }
+ }
+ }
+ }
+
+ TopAppBar(
+ title = { Text(stringResource(R.string.account)) },
+ navigationIcon = {
+ IconButton(onClick = navController::navigateUp) {
+ Icon(
+ painterResource(R.drawable.arrow_back),
+ contentDescription = null
+ )
+ }
+ },
+ scrollBehavior = scrollBehavior
+ )
+}
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 57244fae3..287f05e9e 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
@@ -181,6 +181,7 @@ fun AlbumScreen(
albumIndex = index + 1,
isActive = song.id == mediaMetadata?.id,
isPlaying = isPlaying,
+ showInLibraryIcon = true,
trailingContent = {
IconButton(
onClick = {
diff --git a/app/src/main/java/com/zionhuang/music/ui/screens/HistoryScreen.kt b/app/src/main/java/com/zionhuang/music/ui/screens/HistoryScreen.kt
index 65437edf3..56ccaf206 100644
--- a/app/src/main/java/com/zionhuang/music/ui/screens/HistoryScreen.kt
+++ b/app/src/main/java/com/zionhuang/music/ui/screens/HistoryScreen.kt
@@ -7,7 +7,6 @@ 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.padding
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
@@ -23,8 +22,6 @@ import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.text.style.TextOverflow
-import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation.NavController
import com.zionhuang.innertube.models.WatchEndpoint
@@ -35,6 +32,7 @@ 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.LocalMenuState
+import com.zionhuang.music.ui.component.NavigationTitle
import com.zionhuang.music.ui.component.SongListItem
import com.zionhuang.music.ui.menu.SongMenu
import com.zionhuang.music.viewmodels.DateAgo
@@ -60,20 +58,17 @@ fun HistoryScreen(
) {
events.forEach { (dateAgo, events) ->
stickyHeader {
- Text(
- text = when (dateAgo) {
+ NavigationTitle(
+ title = when (dateAgo) {
DateAgo.Today -> stringResource(R.string.today)
DateAgo.Yesterday -> stringResource(R.string.yesterday)
DateAgo.ThisWeek -> stringResource(R.string.this_week)
DateAgo.LastWeek -> stringResource(R.string.last_week)
is DateAgo.Other -> dateAgo.date.format(DateTimeFormatter.ofPattern("yyyy/MM"))
},
- style = MaterialTheme.typography.headlineMedium,
- overflow = TextOverflow.Ellipsis,
modifier = Modifier
.fillMaxWidth()
.background(MaterialTheme.colorScheme.background)
- .padding(horizontal = 12.dp, vertical = 8.dp)
)
}
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 2f2065f9e..191d0282d 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
@@ -1,6 +1,5 @@
package com.zionhuang.music.ui.screens
-import androidx.annotation.DrawableRes
import androidx.compose.foundation.*
import androidx.compose.foundation.gestures.snapping.rememberSnapFlingBehavior
import androidx.compose.foundation.layout.*
@@ -10,37 +9,39 @@ import androidx.compose.foundation.lazy.grid.LazyHorizontalGrid
import androidx.compose.foundation.lazy.grid.items
import androidx.compose.foundation.lazy.grid.rememberLazyGridState
import androidx.compose.foundation.lazy.items
-import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
-import androidx.compose.ui.draw.alpha
-import androidx.compose.ui.draw.clip
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation.NavController
import com.google.accompanist.swiperefresh.SwipeRefresh
import com.google.accompanist.swiperefresh.rememberSwipeRefreshState
import com.zionhuang.innertube.models.WatchEndpoint
+import com.zionhuang.innertube.utils.parseCookieString
import com.zionhuang.music.LocalDatabase
import com.zionhuang.music.LocalPlayerAwareWindowInsets
import com.zionhuang.music.LocalPlayerConnection
import com.zionhuang.music.R
+import com.zionhuang.music.constants.InnerTubeCookieKey
import com.zionhuang.music.constants.ListItemHeight
import com.zionhuang.music.extensions.togglePlayPause
import com.zionhuang.music.models.toMediaMetadata
import com.zionhuang.music.playback.queues.YouTubeAlbumRadio
import com.zionhuang.music.playback.queues.YouTubeQueue
+import com.zionhuang.music.ui.component.HideOnScrollFAB
import com.zionhuang.music.ui.component.LocalMenuState
+import com.zionhuang.music.ui.component.NavigationTile
+import com.zionhuang.music.ui.component.NavigationTitle
import com.zionhuang.music.ui.component.SongListItem
import com.zionhuang.music.ui.component.YouTubeGridItem
import com.zionhuang.music.ui.menu.SongMenu
import com.zionhuang.music.ui.menu.YouTubeAlbumMenu
import com.zionhuang.music.ui.utils.SnapLayoutInfoProvider
+import com.zionhuang.music.utils.rememberPreference
import com.zionhuang.music.viewmodels.HomeViewModel
import kotlin.random.Random
@@ -58,11 +59,18 @@ fun HomeScreen(
val mediaMetadata by playerConnection.mediaMetadata.collectAsState()
val quickPicks by viewModel.quickPicks.collectAsState()
- val newReleaseAlbums by viewModel.newReleaseAlbums.collectAsState()
+ val explorePage by viewModel.explorePage.collectAsState()
val isRefreshing by viewModel.isRefreshing.collectAsState()
val mostPlayedLazyGridState = rememberLazyGridState()
+ val innerTubeCookie by rememberPreference(InnerTubeCookieKey, "")
+ val isLoggedIn = remember(innerTubeCookie) {
+ "SAPISID" in parseCookieString(innerTubeCookie)
+ }
+
+ val scrollState = rememberScrollState()
+
SwipeRefresh(
state = rememberSwipeRefreshState(isRefreshing),
onRefresh = viewModel::refresh,
@@ -83,12 +91,12 @@ fun HomeScreen(
}
Column(
- modifier = Modifier.verticalScroll(rememberScrollState())
+ modifier = Modifier.verticalScroll(scrollState)
) {
Spacer(Modifier.height(LocalPlayerAwareWindowInsets.current.asPaddingValues().calculateTopPadding()))
Row(
- horizontalArrangement = Arrangement.spacedBy(12.dp),
+ horizontalArrangement = Arrangement.spacedBy(4.dp),
modifier = Modifier
.windowInsetsPadding(WindowInsets.systemBars.only(WindowInsetsSides.Horizontal))
.padding(horizontal = 12.dp, vertical = 6.dp)
@@ -99,26 +107,28 @@ fun HomeScreen(
onClick = { navController.navigate("history") },
modifier = Modifier.weight(1f)
)
+
NavigationTile(
title = stringResource(R.string.stats),
icon = R.drawable.trending_up,
onClick = { navController.navigate("stats") },
modifier = Modifier.weight(1f)
)
- NavigationTile(
- title = stringResource(R.string.settings),
- icon = R.drawable.settings,
- onClick = { navController.navigate("settings") },
- modifier = Modifier.weight(1f)
- )
+
+ if (isLoggedIn) {
+ NavigationTile(
+ title = stringResource(R.string.account),
+ icon = R.drawable.person,
+ onClick = {
+ navController.navigate("account")
+ },
+ modifier = Modifier.weight(1f)
+ )
+ }
}
- Text(
- text = stringResource(R.string.quick_picks),
- style = MaterialTheme.typography.headlineSmall,
- modifier = Modifier
- .windowInsetsPadding(WindowInsets.systemBars.only(WindowInsetsSides.Horizontal))
- .padding(12.dp)
+ NavigationTitle(
+ title = stringResource(R.string.quick_picks)
)
quickPicks?.let { quickPicks ->
@@ -191,31 +201,13 @@ fun HomeScreen(
}
}
- if (newReleaseAlbums.isNotEmpty()) {
- Row(
- verticalAlignment = Alignment.CenterVertically,
- modifier = Modifier
- .fillMaxWidth()
- .windowInsetsPadding(WindowInsets.systemBars.only(WindowInsetsSides.Horizontal))
- .clickable {
- navController.navigate("new_release")
- }
- .padding(12.dp)
- ) {
- Column(
- modifier = Modifier.weight(1f)
- ) {
- Text(
- text = stringResource(R.string.new_release_albums),
- style = MaterialTheme.typography.headlineSmall
- )
+ explorePage?.newReleaseAlbums?.let { newReleaseAlbums ->
+ NavigationTitle(
+ title = stringResource(R.string.new_release_albums),
+ onClick = {
+ navController.navigate("new_release")
}
-
- Icon(
- painter = painterResource(R.drawable.navigate_next),
- contentDescription = null
- )
- }
+ )
LazyRow(
contentPadding = WindowInsets.systemBars
@@ -252,67 +244,50 @@ fun HomeScreen(
}
}
- Spacer(Modifier.height(LocalPlayerAwareWindowInsets.current.asPaddingValues().calculateBottomPadding()))
- }
-
- if (!quickPicks.isNullOrEmpty() || newReleaseAlbums.isNotEmpty()) {
- FloatingActionButton(
- modifier = Modifier
- .align(Alignment.BottomEnd)
- .windowInsetsPadding(
- LocalPlayerAwareWindowInsets.current
- .only(WindowInsetsSides.Bottom + WindowInsetsSides.Horizontal)
- )
- .padding(16.dp),
- onClick = {
- if (Random.nextBoolean() && !quickPicks.isNullOrEmpty()) {
- val song = quickPicks!!.random()
- playerConnection.playQueue(YouTubeQueue(WatchEndpoint(videoId = song.id), song.toMediaMetadata()))
- } else if (newReleaseAlbums.isNotEmpty()) {
- val album = newReleaseAlbums.random()
- playerConnection.playQueue(YouTubeAlbumRadio(album.playlistId))
+ explorePage?.moodAndGenres?.let { moodAndGenres ->
+ NavigationTitle(
+ title = stringResource(R.string.mood_and_genres),
+ onClick = {
+ navController.navigate("mood_and_genres")
}
- }) {
- Icon(
- painter = painterResource(R.drawable.casino),
- contentDescription = null
)
- }
- }
- }
- }
-}
-@Composable
-fun NavigationTile(
- title: String,
- @DrawableRes icon: Int,
- onClick: () -> Unit,
- modifier: Modifier = Modifier,
- enabled: Boolean = true,
-) {
- Row(
- horizontalArrangement = Arrangement.spacedBy(4.dp),
- verticalAlignment = Alignment.CenterVertically,
- modifier = modifier
- .clip(RoundedCornerShape(6.dp))
- .background(MaterialTheme.colorScheme.surfaceColorAtElevation(6.dp))
- .clickable(enabled = enabled, onClick = onClick)
- .padding(12.dp)
- .alpha(if (enabled) 1f else 0.5f)
- ) {
- Icon(
- painter = painterResource(icon),
- contentDescription = null
- )
+ LazyHorizontalGrid(
+ rows = GridCells.Fixed(4),
+ contentPadding = PaddingValues(6.dp),
+ modifier = Modifier.height((MoodAndGenresButtonHeight + 12.dp) * 4 + 12.dp)
+ ) {
+ items(moodAndGenres) {
+ MoodAndGenresButton(
+ title = it.title,
+ onClick = {
+ navController.navigate("youtube_browse/${it.endpoint.browseId}?params=${it.endpoint.params}")
+ },
+ modifier = Modifier
+ .padding(6.dp)
+ .width(180.dp)
+ )
+ }
+ }
+ }
- Spacer(Modifier.height(6.dp))
+ Spacer(Modifier.height(LocalPlayerAwareWindowInsets.current.asPaddingValues().calculateBottomPadding()))
+ }
- Text(
- text = title,
- style = MaterialTheme.typography.labelLarge,
- maxLines = 1,
- overflow = TextOverflow.Ellipsis
- )
+ HideOnScrollFAB(
+ visible = !quickPicks.isNullOrEmpty() || explorePage?.newReleaseAlbums?.isNotEmpty() == true,
+ scrollState = scrollState,
+ icon = R.drawable.casino,
+ onClick = {
+ if (Random.nextBoolean() && !quickPicks.isNullOrEmpty()) {
+ val song = quickPicks!!.random()
+ playerConnection.playQueue(YouTubeQueue(WatchEndpoint(videoId = song.id), song.toMediaMetadata()))
+ } else if (explorePage?.newReleaseAlbums?.isNotEmpty() == true) {
+ val album = explorePage?.newReleaseAlbums!!.random()
+ playerConnection.playQueue(YouTubeAlbumRadio(album.playlistId))
+ }
+ }
+ )
+ }
}
}
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
new file mode 100644
index 000000000..63e4e0c12
--- /dev/null
+++ b/app/src/main/java/com/zionhuang/music/ui/screens/LoginScreen.kt
@@ -0,0 +1,108 @@
+package com.zionhuang.music.ui.screens
+
+import android.annotation.SuppressLint
+import android.webkit.CookieManager
+import android.webkit.JavascriptInterface
+import android.webkit.WebView
+import android.webkit.WebViewClient
+import androidx.activity.compose.BackHandler
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.windowInsetsPadding
+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.runtime.Composable
+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.stringResource
+import androidx.compose.ui.viewinterop.AndroidView
+import androidx.navigation.NavController
+import com.zionhuang.innertube.YouTube
+import com.zionhuang.music.LocalPlayerAwareWindowInsets
+import com.zionhuang.music.R
+import com.zionhuang.music.constants.AccountEmailKey
+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 kotlinx.coroutines.DelicateCoroutinesApi
+import kotlinx.coroutines.GlobalScope
+import kotlinx.coroutines.launch
+
+@SuppressLint("SetJavaScriptEnabled")
+@OptIn(ExperimentalMaterial3Api::class, DelicateCoroutinesApi::class)
+@Composable
+fun LoginScreen(
+ navController: NavController,
+) {
+ var visitorData by rememberPreference(VisitorDataKey, "")
+ var innerTubeCookie by rememberPreference(InnerTubeCookieKey, "")
+ var accountName by rememberPreference(AccountNameKey, "")
+ var accountEmail by rememberPreference(AccountEmailKey, "")
+
+ var webView: WebView? = null
+
+ AndroidView(
+ modifier = Modifier
+ .windowInsetsPadding(LocalPlayerAwareWindowInsets.current)
+ .fillMaxSize(),
+ factory = { context ->
+ WebView(context).apply {
+ webViewClient = object : WebViewClient() {
+ override fun doUpdateVisitedHistory(view: WebView, url: String, isReload: Boolean) {
+ if (url.startsWith("https://music.youtube.com")) {
+ innerTubeCookie = CookieManager.getInstance().getCookie(url)
+ GlobalScope.launch {
+ YouTube.accountInfo().onSuccess {
+ accountName = it?.name.orEmpty()
+ accountEmail = it?.email.orEmpty()
+ }.onFailure {
+ it.printStackTrace()
+ }
+ }
+ }
+ }
+
+ override fun onPageFinished(view: WebView, url: String?) {
+ loadUrl("javascript:Android.onRetrieveVisitorData(window.yt.config_.VISITOR_DATA)")
+ }
+ }
+ settings.apply {
+ javaScriptEnabled = true
+ setSupportZoom(true)
+ builtInZoomControls = true
+ }
+ addJavascriptInterface(object {
+ @JavascriptInterface
+ fun onRetrieveVisitorData(newVisitorData: String?) {
+ if (newVisitorData != null) {
+ visitorData = newVisitorData
+ }
+ }
+ }, "Android")
+ webView = this
+ loadUrl("https://accounts.google.com/ServiceLogin?ltmpl=music&service=youtube&passive=true&continue=https%3A%2F%2Fwww.youtube.com%2Fsignin%3Faction_handle_signin%3Dtrue%26next%3Dhttps%253A%252F%252Fmusic.youtube.com%252F")
+ }
+ }
+ )
+
+ TopAppBar(
+ title = { Text(stringResource(R.string.login)) },
+ navigationIcon = {
+ IconButton(onClick = navController::navigateUp) {
+ Icon(
+ painterResource(R.drawable.arrow_back),
+ contentDescription = null
+ )
+ }
+ }
+ )
+
+ BackHandler(enabled = webView?.canGoBack() == true) {
+ webView?.goBack()
+ }
+}
diff --git a/app/src/main/java/com/zionhuang/music/ui/screens/MoodAndGenresScreen.kt b/app/src/main/java/com/zionhuang/music/ui/screens/MoodAndGenresScreen.kt
new file mode 100644
index 000000000..03adecc6f
--- /dev/null
+++ b/app/src/main/java/com/zionhuang/music/ui/screens/MoodAndGenresScreen.kt
@@ -0,0 +1,143 @@
+package com.zionhuang.music.ui.screens
+
+import android.content.res.Configuration.ORIENTATION_LANDSCAPE
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+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.lazy.LazyColumn
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.ExperimentalMaterial3Api
+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.material3.TopAppBarScrollBehavior
+import androidx.compose.material3.surfaceColorAtElevation
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.platform.LocalConfiguration
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.unit.dp
+import androidx.hilt.navigation.compose.hiltViewModel
+import androidx.navigation.NavController
+import com.zionhuang.music.LocalPlayerAwareWindowInsets
+import com.zionhuang.music.R
+import com.zionhuang.music.ui.component.shimmer.ListItemPlaceHolder
+import com.zionhuang.music.ui.component.shimmer.ShimmerHost
+import com.zionhuang.music.viewmodels.MoodAndGenresViewModel
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun MoodAndGenresScreen(
+ navController: NavController,
+ scrollBehavior: TopAppBarScrollBehavior,
+ viewModel: MoodAndGenresViewModel = hiltViewModel(),
+) {
+ val localConfiguration = LocalConfiguration.current
+ val itemsPerRow = if (localConfiguration.orientation == ORIENTATION_LANDSCAPE) 3 else 2
+
+ val moodAndGenresList by viewModel.moodAndGenres.collectAsState()
+
+ LazyColumn(
+ contentPadding = LocalPlayerAwareWindowInsets.current.asPaddingValues()
+ ) {
+ if (moodAndGenresList == null) {
+ item {
+ ShimmerHost {
+ repeat(8) {
+ ListItemPlaceHolder()
+ }
+ }
+ }
+ }
+
+ moodAndGenresList?.forEach { moodAndGenres ->
+ item {
+ Text(
+ text = moodAndGenres.title,
+ style = MaterialTheme.typography.headlineMedium,
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 12.dp, vertical = 8.dp)
+ )
+
+ Column(
+ modifier = Modifier.padding(horizontal = 6.dp)
+ ) {
+ moodAndGenres.items.chunked(itemsPerRow).forEach { row ->
+ Row {
+ row.forEach {
+ MoodAndGenresButton(
+ title = it.title,
+ onClick = {
+ navController.navigate("youtube_browse/${it.endpoint.browseId}?params=${it.endpoint.params}")
+ },
+ modifier = Modifier
+ .weight(1f)
+ .padding(6.dp)
+ )
+ }
+
+ repeat(itemsPerRow - row.size) {
+ Spacer(Modifier.weight(1f))
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ TopAppBar(
+ title = { Text(stringResource(R.string.mood_and_genres)) },
+ navigationIcon = {
+ IconButton(onClick = navController::navigateUp) {
+ Icon(
+ painterResource(R.drawable.arrow_back),
+ contentDescription = null
+ )
+ }
+ },
+ scrollBehavior = scrollBehavior
+ )
+}
+
+@Composable
+fun MoodAndGenresButton(
+ title: String,
+ onClick: () -> Unit,
+ modifier: Modifier = Modifier,
+) {
+ Box(
+ contentAlignment = Alignment.CenterStart,
+ modifier = modifier
+ .height(MoodAndGenresButtonHeight)
+ .clip(RoundedCornerShape(6.dp))
+ .background(MaterialTheme.colorScheme.surfaceColorAtElevation(6.dp))
+ .clickable(onClick = onClick)
+ .padding(horizontal = 12.dp)
+ ) {
+ Text(
+ text = title,
+ style = MaterialTheme.typography.labelLarge,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis
+ )
+ }
+}
+
+val MoodAndGenresButtonHeight = 48.dp
\ No newline at end of file
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 46c6143ac..a72f2b024 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
@@ -1,18 +1,23 @@
package com.zionhuang.music.ui.screens
import androidx.compose.foundation.ExperimentalFoundationApi
-import androidx.compose.foundation.background
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.padding
+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
@@ -23,8 +28,8 @@ import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
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.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation.NavController
@@ -32,13 +37,17 @@ import com.zionhuang.innertube.models.WatchEndpoint
import com.zionhuang.music.LocalPlayerAwareWindowInsets
import com.zionhuang.music.LocalPlayerConnection
import com.zionhuang.music.R
+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.ArtistListItem
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.SongMenu
+import com.zionhuang.music.ui.menu.YouTubeAlbumMenu
import com.zionhuang.music.viewmodels.StatsViewModel
@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class)
@@ -52,23 +61,50 @@ fun StatsScreen(
val isPlaying by playerConnection.isPlaying.collectAsState()
val mediaMetadata by playerConnection.mediaMetadata.collectAsState()
+ val statPeriod by viewModel.statPeriod.collectAsState()
val mostPlayedSongs by viewModel.mostPlayedSongs.collectAsState()
val mostPlayedArtists by viewModel.mostPlayedArtists.collectAsState()
+ val mostPlayedAlbums by viewModel.mostPlayedAlbums.collectAsState()
LazyColumn(
contentPadding = LocalPlayerAwareWindowInsets.current.only(WindowInsetsSides.Horizontal + WindowInsetsSides.Bottom).asPaddingValues(),
modifier = Modifier.windowInsetsPadding(LocalPlayerAwareWindowInsets.current.only(WindowInsetsSides.Top))
) {
item {
- Text(
- text = stringResource(R.string.most_played_songs),
- style = MaterialTheme.typography.headlineMedium,
- overflow = TextOverflow.Ellipsis,
+ Row(
modifier = Modifier
.fillMaxWidth()
- .background(MaterialTheme.colorScheme.background)
- .padding(horizontal = 12.dp, vertical = 8.dp)
- )
+ .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))
+ }
+ }
+ }
+
+ item {
+ NavigationTitle(stringResource(R.string.most_played_songs))
}
items(
items = mostPlayedSongs,
@@ -114,16 +150,9 @@ fun StatsScreen(
.animateItemPlacement()
)
}
+
item {
- Text(
- text = stringResource(R.string.most_played_artists),
- style = MaterialTheme.typography.headlineMedium,
- overflow = TextOverflow.Ellipsis,
- modifier = Modifier
- .fillMaxWidth()
- .background(MaterialTheme.colorScheme.background)
- .padding(horizontal = 12.dp, vertical = 8.dp)
- )
+ NavigationTitle(stringResource(R.string.most_played_artists))
}
items(
items = mostPlayedArtists,
@@ -139,6 +168,46 @@ fun StatsScreen(
.animateItemPlacement()
)
}
+
+ if (mostPlayedAlbums.isNotEmpty()) {
+ item {
+ NavigationTitle(stringResource(R.string.most_played_albums))
+ }
+ items(
+ items = mostPlayedAlbums,
+ key = { it.id }
+ ) { item ->
+ YouTubeListItem(
+ item = item,
+ isActive = mediaMetadata?.album?.id == item.id,
+ isPlaying = isPlaying,
+ trailingContent = {
+ IconButton(
+ onClick = {
+ menuState.show {
+ YouTubeAlbumMenu(
+ album = item,
+ navController = navController,
+ playerConnection = playerConnection,
+ onDismiss = menuState::dismiss
+ )
+ }
+ }
+ ) {
+ Icon(
+ painter = painterResource(R.drawable.more_vert),
+ contentDescription = null
+ )
+ }
+ },
+ modifier = Modifier
+ .clickable {
+ navController.navigate("album/${item.id}")
+ }
+ .animateItemPlacement()
+ )
+ }
+ }
}
TopAppBar(
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
new file mode 100644
index 000000000..02a948503
--- /dev/null
+++ b/app/src/main/java/com/zionhuang/music/ui/screens/YouTubeBrowseScreen.kt
@@ -0,0 +1,173 @@
+package com.zionhuang.music.ui.screens
+
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.asPaddingValues
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.items
+import androidx.compose.material3.ExperimentalMaterial3Api
+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.material3.TopAppBarScrollBehavior
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.unit.dp
+import androidx.hilt.navigation.compose.hiltViewModel
+import androidx.navigation.NavController
+import com.zionhuang.innertube.models.AlbumItem
+import com.zionhuang.innertube.models.ArtistItem
+import com.zionhuang.innertube.models.PlaylistItem
+import com.zionhuang.innertube.models.SongItem
+import com.zionhuang.innertube.models.WatchEndpoint
+import com.zionhuang.music.LocalPlayerAwareWindowInsets
+import com.zionhuang.music.LocalPlayerConnection
+import com.zionhuang.music.R
+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.LocalMenuState
+import com.zionhuang.music.ui.component.YouTubeListItem
+import com.zionhuang.music.ui.component.shimmer.ListItemPlaceHolder
+import com.zionhuang.music.ui.component.shimmer.ShimmerHost
+import com.zionhuang.music.ui.menu.YouTubeAlbumMenu
+import com.zionhuang.music.ui.menu.YouTubeArtistMenu
+import com.zionhuang.music.ui.menu.YouTubePlaylistMenu
+import com.zionhuang.music.ui.menu.YouTubeSongMenu
+import com.zionhuang.music.viewmodels.YouTubeBrowseViewModel
+
+@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class)
+@Composable
+fun YouTubeBrowseScreen(
+ navController: NavController,
+ scrollBehavior: TopAppBarScrollBehavior,
+ viewModel: YouTubeBrowseViewModel = hiltViewModel(),
+) {
+ val menuState = LocalMenuState.current
+ val playerConnection = LocalPlayerConnection.current ?: return
+ val isPlaying by playerConnection.isPlaying.collectAsState()
+ val mediaMetadata by playerConnection.mediaMetadata.collectAsState()
+
+ val browseResult by viewModel.result.collectAsState()
+
+ val coroutineScope = rememberCoroutineScope()
+
+ LazyColumn(
+ contentPadding = LocalPlayerAwareWindowInsets.current.asPaddingValues()
+ ) {
+ if (browseResult == null) {
+ item {
+ ShimmerHost {
+ repeat(8) {
+ ListItemPlaceHolder()
+ }
+ }
+ }
+ }
+
+ browseResult?.items?.forEach {
+ it.title?.let { title ->
+ item {
+ Text(
+ text = title,
+ style = MaterialTheme.typography.headlineMedium,
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 12.dp, vertical = 8.dp)
+ )
+ }
+ }
+
+ items(it.items) { item ->
+ YouTubeListItem(
+ item = item,
+ isActive = when (item) {
+ is SongItem -> mediaMetadata?.id == item.id
+ is AlbumItem -> mediaMetadata?.album?.id == item.id
+ else -> false
+ },
+ isPlaying = isPlaying,
+ trailingContent = {
+ IconButton(
+ onClick = {
+ menuState.show {
+ when (item) {
+ is SongItem -> YouTubeSongMenu(
+ song = item,
+ navController = navController,
+ playerConnection = playerConnection,
+ onDismiss = menuState::dismiss
+ )
+
+ is AlbumItem -> YouTubeAlbumMenu(
+ album = item,
+ navController = navController,
+ playerConnection = playerConnection,
+ onDismiss = menuState::dismiss
+ )
+
+ is ArtistItem -> YouTubeArtistMenu(
+ artist = item,
+ playerConnection = playerConnection,
+ onDismiss = menuState::dismiss
+ )
+
+ is PlaylistItem -> YouTubePlaylistMenu(
+ playlist = item,
+ playerConnection = playerConnection,
+ coroutineScope = coroutineScope,
+ onDismiss = menuState::dismiss
+ )
+ }
+ }
+ }
+ ) {
+ Icon(
+ painter = painterResource(R.drawable.more_vert),
+ contentDescription = null
+ )
+ }
+ },
+ modifier = Modifier
+ .clickable {
+ when (item) {
+ is SongItem -> {
+ if (item.id == mediaMetadata?.id) {
+ playerConnection.player.togglePlayPause()
+ } else {
+ playerConnection.playQueue(YouTubeQueue(WatchEndpoint(videoId = item.id), item.toMediaMetadata()))
+ }
+ }
+
+ is AlbumItem -> navController.navigate("album/${item.id}")
+ is ArtistItem -> navController.navigate("artist/${item.id}")
+ is PlaylistItem -> navController.navigate("online_playlist/${item.id}")
+ }
+ }
+ .animateItemPlacement()
+ )
+ }
+ }
+ }
+
+ TopAppBar(
+ title = { Text(browseResult?.title.orEmpty()) },
+ navigationIcon = {
+ IconButton(onClick = navController::navigateUp) {
+ Icon(
+ painterResource(R.drawable.arrow_back),
+ contentDescription = null
+ )
+ }
+ },
+ scrollBehavior = scrollBehavior
+ )
+}
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 7efc0238e..14b0c453a 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
@@ -1,5 +1,6 @@
package com.zionhuang.music.ui.screens.artist
+import android.content.Intent
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
@@ -27,6 +28,7 @@ import androidx.compose.material3.ButtonDefaults
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
@@ -42,6 +44,7 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
@@ -59,16 +62,19 @@ import com.zionhuang.innertube.models.ArtistItem
import com.zionhuang.innertube.models.PlaylistItem
import com.zionhuang.innertube.models.SongItem
import com.zionhuang.innertube.models.WatchEndpoint
+import com.zionhuang.music.LocalDatabase
import com.zionhuang.music.LocalPlayerAwareWindowInsets
import com.zionhuang.music.LocalPlayerConnection
import com.zionhuang.music.R
import com.zionhuang.music.constants.AppBarHeight
+import com.zionhuang.music.db.entities.ArtistEntity
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.AutoResizeText
import com.zionhuang.music.ui.component.FontSizeRange
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.YouTubeGridItem
import com.zionhuang.music.ui.component.YouTubeListItem
@@ -84,6 +90,7 @@ import com.zionhuang.music.ui.menu.YouTubeSongMenu
import com.zionhuang.music.ui.utils.fadingEdge
import com.zionhuang.music.ui.utils.resize
import com.zionhuang.music.viewmodels.ArtistViewModel
+import java.time.LocalDateTime
@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterial3Api::class)
@Composable
@@ -92,6 +99,8 @@ fun ArtistScreen(
scrollBehavior: TopAppBarScrollBehavior,
viewModel: ArtistViewModel = hiltViewModel(),
) {
+ val context = LocalContext.current
+ val database = LocalDatabase.current
val menuState = LocalMenuState.current
val coroutineScope = rememberCoroutineScope()
val playerConnection = LocalPlayerConnection.current ?: return
@@ -99,6 +108,7 @@ fun ArtistScreen(
val mediaMetadata by playerConnection.mediaMetadata.collectAsState()
val artistPage = viewModel.artistPage
+ val libraryArtist by viewModel.libraryArtist.collectAsState()
val librarySongs by viewModel.librarySongs.collectAsState()
val lazyListState = rememberLazyListState()
@@ -197,28 +207,12 @@ fun ArtistScreen(
if (librarySongs.isNotEmpty()) {
item {
- Row(
- verticalAlignment = Alignment.CenterVertically,
- modifier = Modifier
- .fillMaxWidth()
- .clickable {
- navController.navigate("artist/${viewModel.artistId}/songs")
- }
- .padding(12.dp)
- ) {
- Column(
- modifier = Modifier.weight(1f)
- ) {
- Text(
- text = stringResource(R.string.from_your_library),
- style = MaterialTheme.typography.headlineMedium
- )
+ NavigationTitle(
+ title = stringResource(R.string.from_your_library),
+ onClick = {
+ navController.navigate("artist/${viewModel.artistId}/songs")
}
- Icon(
- painter = painterResource(R.drawable.navigate_next),
- contentDescription = null
- )
- }
+ )
}
items(
@@ -264,30 +258,14 @@ fun ArtistScreen(
artistPage.sections.fastForEach { section ->
item {
- Row(
- verticalAlignment = Alignment.CenterVertically,
- modifier = Modifier
- .fillMaxWidth()
- .clickable(enabled = section.moreEndpoint != null) {
- navController.navigate("artist/${viewModel.artistId}/items?browseId=${section.moreEndpoint?.browseId}?params=${section.moreEndpoint?.params}")
+ NavigationTitle(
+ title = section.title,
+ onClick = section.moreEndpoint?.let {
+ {
+ navController.navigate("artist/${viewModel.artistId}/items?browseId=${it.browseId}?params=${it.params}")
}
- .padding(12.dp)
- ) {
- Column(
- modifier = Modifier.weight(1f)
- ) {
- Text(
- text = section.title,
- style = MaterialTheme.typography.headlineMedium
- )
- }
- if (section.moreEndpoint != null) {
- Icon(
- painter = painterResource(R.drawable.navigate_next),
- contentDescription = null
- )
}
- }
+ )
}
if ((section.items.firstOrNull() as? SongItem)?.album != null) {
@@ -450,6 +428,57 @@ fun ArtistScreen(
)
}
},
+ actions = {
+ IconButton(
+ onClick = {
+ database.transaction {
+ val artist = libraryArtist
+ if (artist != null) {
+ update(
+ artist.copy(
+ bookmarkedAt = if (artist.bookmarkedAt != null) null else LocalDateTime.now()
+ )
+ )
+ } else {
+ artistPage?.artist?.let {
+ insert(
+ ArtistEntity(
+ id = it.id,
+ name = it.title,
+ thumbnailUrl = it.thumbnail,
+ bookmarkedAt = LocalDateTime.now()
+ )
+ )
+ }
+ }
+ }
+ }
+ ) {
+ 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,
+ contentDescription = null
+ )
+ }
+
+ IconButton(
+ onClick = {
+ viewModel.artistPage?.artist?.shareLink?.let { link ->
+ val intent = Intent().apply {
+ action = Intent.ACTION_SEND
+ type = "text/plain"
+ putExtra(Intent.EXTRA_TEXT, link)
+ }
+ context.startActivity(Intent.createChooser(intent, null))
+ }
+ }
+ ) {
+ Icon(
+ painterResource(R.drawable.share),
+ contentDescription = null
+ )
+ }
+ },
scrollBehavior = scrollBehavior,
colors = if (transparentAppBar) {
TopAppBarDefaults.topAppBarColors(containerColor = Color.Transparent)
diff --git a/app/src/main/java/com/zionhuang/music/ui/screens/artist/ArtistSongsScreen.kt b/app/src/main/java/com/zionhuang/music/ui/screens/artist/ArtistSongsScreen.kt
index 583f2aea8..0e3ba8458 100644
--- a/app/src/main/java/com/zionhuang/music/ui/screens/artist/ArtistSongsScreen.kt
+++ b/app/src/main/java/com/zionhuang/music/ui/screens/artist/ArtistSongsScreen.kt
@@ -1,23 +1,15 @@
package com.zionhuang.music.ui.screens.artist
-import androidx.compose.animation.AnimatedVisibility
-import androidx.compose.animation.slideInVertically
-import androidx.compose.animation.slideOutVertically
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.WindowInsetsSides
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.windowInsetsPadding
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.FloatingActionButton
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Text
@@ -26,12 +18,10 @@ import androidx.compose.material3.TopAppBarScrollBehavior
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
-import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.pluralStringResource
-import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation.NavController
import com.zionhuang.music.LocalPlayerAwareWindowInsets
@@ -45,11 +35,11 @@ import com.zionhuang.music.constants.CONTENT_TYPE_SONG
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.ui.utils.isScrollingUp
import com.zionhuang.music.utils.rememberEnumPreference
import com.zionhuang.music.utils.rememberPreference
import com.zionhuang.music.viewmodels.ArtistSongsViewModel
@@ -95,6 +85,7 @@ fun ArtistSongsScreen(
when (sortType) {
ArtistSongSortType.CREATE_DATE -> R.string.sort_by_create_date
ArtistSongSortType.NAME -> R.string.sort_by_name
+ ArtistSongSortType.PLAY_TIME -> R.string.sort_by_play_time
}
},
trailingText = pluralStringResource(R.plurals.n_song, songs.size, songs.size)
@@ -162,33 +153,17 @@ fun ArtistSongsScreen(
scrollBehavior = scrollBehavior
)
- AnimatedVisibility(
- visible = lazyListState.isScrollingUp(),
- enter = slideInVertically { it },
- exit = slideOutVertically { it },
- modifier = Modifier
- .align(Alignment.BottomEnd)
- .windowInsetsPadding(
- LocalPlayerAwareWindowInsets.current
- .only(WindowInsetsSides.Bottom + WindowInsetsSides.Horizontal)
- )
- ) {
- FloatingActionButton(
- modifier = Modifier.padding(16.dp),
- onClick = {
- playerConnection.playQueue(
- ListQueue(
- title = artist?.name,
- items = songs.shuffled().map { it.toMediaItem() },
- )
+ HideOnScrollFAB(
+ lazyListState = lazyListState,
+ icon = R.drawable.shuffle,
+ onClick = {
+ playerConnection.playQueue(
+ ListQueue(
+ title = artist?.name,
+ items = songs.shuffled().map { it.toMediaItem() },
)
- }
- ) {
- Icon(
- painter = painterResource(R.drawable.shuffle),
- contentDescription = null
)
}
- }
+ )
}
}
\ No newline at end of file
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 66a385115..7c222e653 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
@@ -69,6 +69,7 @@ fun LibraryAlbumsScreen(
AlbumSortType.YEAR -> R.string.sort_by_year
AlbumSortType.SONG_COUNT -> R.string.sort_by_song_count
AlbumSortType.LENGTH -> R.string.sort_by_length
+ AlbumSortType.PLAY_TIME -> R.string.sort_by_play_time
}
},
trailingText = pluralStringResource(R.plurals.n_album, albums.size, albums.size)
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 5b3eed2e1..cfdb49da7 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,34 +2,60 @@ 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
+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.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.*
+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.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)
+@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterial3Api::class)
@Composable
fun LibraryArtistsScreen(
navController: NavController,
viewModel: LibraryArtistsViewModel = hiltViewModel(),
) {
+ val database = LocalDatabase.current
+ var viewType by rememberEnumPreference(ArtistViewTypeKey, ArtistViewType.ALL)
val (sortType, onSortTypeChange) = rememberEnumPreference(ArtistSortTypeKey, ArtistSortType.CREATE_DATE)
val (sortDescending, onSortDescendingChange) = rememberPreference(ArtistSortDescendingKey, true)
@@ -41,10 +67,30 @@ fun LibraryArtistsScreen(
LazyColumn(
contentPadding = LocalPlayerAwareWindowInsets.current.asPaddingValues()
) {
- item(
- key = "header",
- contentType = CONTENT_TYPE_HEADER
- ) {
+ 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 = "header") {
SortHeader(
sortType = sortType,
sortDescending = sortDescending,
@@ -55,6 +101,7 @@ fun LibraryArtistsScreen(
ArtistSortType.CREATE_DATE -> R.string.sort_by_create_date
ArtistSortType.NAME -> R.string.sort_by_name
ArtistSortType.SONG_COUNT -> R.string.sort_by_song_count
+ ArtistSortType.PLAY_TIME -> R.string.sort_by_play_time
}
},
trailingText = pluralStringResource(R.plurals.n_artist, artists.size, artists.size)
@@ -68,6 +115,25 @@ fun LibraryArtistsScreen(
) { artist ->
ArtistListItem(
artist = artist,
+ trailingContent = {
+ IconButton(
+ onClick = {
+ database.transaction {
+ update(
+ artist.artist.copy(
+ bookmarkedAt = if (artist.artist.bookmarkedAt != null) null else LocalDateTime.now()
+ )
+ )
+ }
+ }
+ ) {
+ 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,
+ contentDescription = null
+ )
+ }
+ },
modifier = Modifier
.fillMaxWidth()
.clickable {
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 65942a76b..68580b118 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
@@ -1,23 +1,15 @@
package com.zionhuang.music.ui.screens.library
-import androidx.compose.animation.AnimatedVisibility
-import androidx.compose.animation.slideInVertically
-import androidx.compose.animation.slideOutVertically
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.WindowInsetsSides
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.size
-import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
-import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
@@ -29,12 +21,10 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.rememberCoroutineScope
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.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
@@ -49,13 +39,13 @@ 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
import com.zionhuang.music.ui.component.TextFieldDialog
import com.zionhuang.music.ui.menu.PlaylistMenu
-import com.zionhuang.music.ui.utils.isScrollingUp
import com.zionhuang.music.utils.rememberEnumPreference
import com.zionhuang.music.utils.rememberPreference
import com.zionhuang.music.viewmodels.LibraryPlaylistsViewModel
@@ -208,26 +198,12 @@ fun LibraryPlaylistsScreen(
}
}
- AnimatedVisibility(
- visible = lazyListState.isScrollingUp(),
- enter = slideInVertically { it },
- exit = slideOutVertically { it },
- modifier = Modifier
- .align(Alignment.BottomEnd)
- .windowInsetsPadding(
- LocalPlayerAwareWindowInsets.current
- .only(WindowInsetsSides.Bottom + WindowInsetsSides.Horizontal)
- )
- ) {
- FloatingActionButton(
- modifier = Modifier.padding(16.dp),
- onClick = { showAddPlaylistDialog = true }
- ) {
- Icon(
- painter = painterResource(R.drawable.add),
- contentDescription = null
- )
+ HideOnScrollFAB(
+ lazyListState = lazyListState,
+ icon = R.drawable.add,
+ onClick = {
+ showAddPlaylistDialog = true
}
- }
+ )
}
}
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 51487e3d7..4503ffc35 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
@@ -1,26 +1,20 @@
package com.zionhuang.music.ui.screens.library
-import androidx.compose.animation.AnimatedVisibility
-import androidx.compose.animation.slideInVertically
-import androidx.compose.animation.slideOutVertically
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState
-import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
-import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.pluralStringResource
-import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation.NavController
import com.zionhuang.music.LocalPlayerAwareWindowInsets
@@ -30,11 +24,11 @@ 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.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.ui.utils.isScrollingUp
import com.zionhuang.music.utils.rememberEnumPreference
import com.zionhuang.music.utils.rememberPreference
import com.zionhuang.music.viewmodels.LibrarySongsViewModel
@@ -79,6 +73,7 @@ fun LibrarySongsScreen(
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 = pluralStringResource(R.plurals.n_song, songs.size, songs.size)
@@ -133,33 +128,18 @@ fun LibrarySongsScreen(
}
}
- AnimatedVisibility(
- visible = songs.isNotEmpty() && lazyListState.isScrollingUp(),
- enter = slideInVertically { it },
- exit = slideOutVertically { it },
- modifier = Modifier
- .align(Alignment.BottomEnd)
- .windowInsetsPadding(
- LocalPlayerAwareWindowInsets.current
- .only(WindowInsetsSides.Bottom + WindowInsetsSides.Horizontal)
- )
- ) {
- FloatingActionButton(
- modifier = Modifier.padding(16.dp),
- onClick = {
- playerConnection.playQueue(
- ListQueue(
- title = context.getString(R.string.queue_all_songs),
- items = songs.shuffled().map { it.toMediaItem() },
- )
+ HideOnScrollFAB(
+ visible = songs.isNotEmpty(),
+ lazyListState = lazyListState,
+ icon = R.drawable.shuffle,
+ onClick = {
+ playerConnection.playQueue(
+ ListQueue(
+ title = context.getString(R.string.queue_all_songs),
+ items = songs.shuffled().map { it.toMediaItem() },
)
- }
- ) {
- Icon(
- painter = painterResource(R.drawable.shuffle),
- contentDescription = null
)
}
- }
+ )
}
}
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
index e9f9be42c..41e68f994 100644
--- 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
@@ -1,23 +1,15 @@
package com.zionhuang.music.ui.screens.playlist
-import androidx.compose.animation.AnimatedVisibility
-import androidx.compose.animation.slideInVertically
-import androidx.compose.animation.slideOutVertically
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.WindowInsetsSides
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.windowInsetsPadding
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.FloatingActionButton
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Text
@@ -27,12 +19,10 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
-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.pluralStringResource
-import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.fastSumBy
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation.NavController
@@ -50,11 +40,11 @@ import com.zionhuang.music.db.entities.PlaylistEntity.Companion.LIKED_PLAYLIST_I
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.ui.utils.isScrollingUp
import com.zionhuang.music.utils.joinByBullet
import com.zionhuang.music.utils.makeTimeString
import com.zionhuang.music.utils.rememberEnumPreference
@@ -114,6 +104,7 @@ fun BuiltInPlaylistScreen(
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(
@@ -132,6 +123,7 @@ fun BuiltInPlaylistScreen(
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(
@@ -205,33 +197,18 @@ fun BuiltInPlaylistScreen(
scrollBehavior = scrollBehavior
)
- AnimatedVisibility(
- visible = songs.isNotEmpty() && lazyListState.isScrollingUp(),
- enter = slideInVertically { it },
- exit = slideOutVertically { it },
- modifier = Modifier
- .align(Alignment.BottomEnd)
- .windowInsetsPadding(
- LocalPlayerAwareWindowInsets.current
- .only(WindowInsetsSides.Bottom + WindowInsetsSides.Horizontal)
- )
- ) {
- FloatingActionButton(
- modifier = Modifier.padding(16.dp),
- onClick = {
- playerConnection.playQueue(
- ListQueue(
- title = playlistName,
- items = songs.shuffled().map { it.toMediaItem() },
- )
+ HideOnScrollFAB(
+ visible = songs.isNotEmpty(),
+ lazyListState = lazyListState,
+ icon = R.drawable.shuffle,
+ onClick = {
+ playerConnection.playQueue(
+ ListQueue(
+ title = playlistName,
+ items = songs.shuffled().map { it.toMediaItem() },
)
- }
- ) {
- Icon(
- painter = painterResource(R.drawable.shuffle),
- contentDescription = null
)
}
- }
+ )
}
}
diff --git a/app/src/main/java/com/zionhuang/music/ui/screens/playlist/LocalPlaylistScreen.kt b/app/src/main/java/com/zionhuang/music/ui/screens/playlist/LocalPlaylistScreen.kt
index 051bd7aee..d6cddc2b3 100644
--- a/app/src/main/java/com/zionhuang/music/ui/screens/playlist/LocalPlaylistScreen.kt
+++ b/app/src/main/java/com/zionhuang/music/ui/screens/playlist/LocalPlaylistScreen.kt
@@ -25,6 +25,7 @@ import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton
+import androidx.compose.material3.SnackbarDuration
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.SnackbarResult
@@ -443,6 +444,7 @@ fun LocalPlaylistScreen(
PlaylistSongSortType.CREATE_DATE -> R.string.sort_by_create_date
PlaylistSongSortType.NAME -> R.string.sort_by_name
PlaylistSongSortType.ARTIST -> R.string.sort_by_artist
+ PlaylistSongSortType.PLAY_TIME -> R.string.sort_by_play_time
}
},
trailingText = "",
@@ -481,7 +483,8 @@ fun LocalPlaylistScreen(
coroutineScope.launch {
val snackbarResult = snackbarHostState.showSnackbar(
message = context.getString(R.string.removed_song_from_playlist, currentItem.song.song.title),
- actionLabel = context.getString(R.string.undo)
+ actionLabel = context.getString(R.string.undo),
+ duration = SnackbarDuration.Short
)
if (snackbarResult == SnackbarResult.ActionPerformed) {
database.transaction {
diff --git a/app/src/main/java/com/zionhuang/music/ui/screens/playlist/OnlinePlaylistScreen.kt b/app/src/main/java/com/zionhuang/music/ui/screens/playlist/OnlinePlaylistScreen.kt
index 93202821d..3902578e2 100644
--- a/app/src/main/java/com/zionhuang/music/ui/screens/playlist/OnlinePlaylistScreen.kt
+++ b/app/src/main/java/com/zionhuang/music/ui/screens/playlist/OnlinePlaylistScreen.kt
@@ -143,25 +143,27 @@ fun OnlinePlaylistScreen(
fontSizeRange = FontSizeRange(16.sp, 22.sp)
)
- val annotatedString = buildAnnotatedString {
- withStyle(
- style = MaterialTheme.typography.titleMedium.copy(
- fontWeight = FontWeight.Normal,
- color = MaterialTheme.colorScheme.onBackground
- ).toSpanStyle()
- ) {
- if (playlist.author.id != null) {
- pushStringAnnotation(playlist.author.id!!, playlist.author.name)
- append(playlist.author.name)
- pop()
- } else {
- append(playlist.author.name)
+ playlist.author?.let { artist ->
+ val annotatedString = buildAnnotatedString {
+ withStyle(
+ style = MaterialTheme.typography.titleMedium.copy(
+ fontWeight = FontWeight.Normal,
+ color = MaterialTheme.colorScheme.onBackground
+ ).toSpanStyle()
+ ) {
+ if (artist.id != null) {
+ pushStringAnnotation(artist.id!!, artist.name)
+ append(artist.name)
+ pop()
+ } else {
+ append(artist.name)
+ }
}
}
- }
- ClickableText(annotatedString) { offset ->
- annotatedString.getStringAnnotations(offset, offset).firstOrNull()?.let { range ->
- navController.navigate("artist/${range.tag}")
+ ClickableText(annotatedString) { offset ->
+ annotatedString.getStringAnnotations(offset, offset).firstOrNull()?.let { range ->
+ navController.navigate("artist/${range.tag}")
+ }
}
}
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 f02282d0a..ffec47295 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
@@ -3,7 +3,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.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
@@ -11,7 +10,6 @@ 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.height
import androidx.compose.foundation.layout.only
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.systemBars
@@ -37,7 +35,6 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.snapshotFlow
-import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
@@ -60,13 +57,13 @@ import com.zionhuang.music.LocalPlayerAwareWindowInsets
import com.zionhuang.music.LocalPlayerConnection
import com.zionhuang.music.R
import com.zionhuang.music.constants.AppBarHeight
-import com.zionhuang.music.constants.ListItemHeight
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.EmptyPlaceholder
import com.zionhuang.music.ui.component.LocalMenuState
+import com.zionhuang.music.ui.component.NavigationTitle
import com.zionhuang.music.ui.component.YouTubeListItem
import com.zionhuang.music.ui.component.shimmer.ListItemPlaceHolder
import com.zionhuang.music.ui.component.shimmer.ShimmerHost
@@ -189,20 +186,7 @@ fun OnlineSearchResult(
if (searchFilter == null) {
searchSummary?.summaries?.forEach { summary ->
item {
- Box(
- contentAlignment = Alignment.CenterStart,
- modifier = Modifier
- .fillMaxWidth()
- .height(ListItemHeight)
- .padding(12.dp)
- .animateItemPlacement()
- ) {
- Text(
- text = summary.title,
- style = MaterialTheme.typography.headlineMedium,
- maxLines = 1
- )
- }
+ NavigationTitle(summary.title)
}
items(
diff --git a/app/src/main/java/com/zionhuang/music/ui/screens/settings/AppearanceSettings.kt b/app/src/main/java/com/zionhuang/music/ui/screens/settings/AppearanceSettings.kt
index 9a010bb04..460e2a05b 100644
--- a/app/src/main/java/com/zionhuang/music/ui/screens/settings/AppearanceSettings.kt
+++ b/app/src/main/java/com/zionhuang/music/ui/screens/settings/AppearanceSettings.kt
@@ -40,14 +40,14 @@ fun AppearanceSettings(
.verticalScroll(rememberScrollState())
) {
SwitchPreference(
- title = stringResource(R.string.enable_dynamic_theme),
- icon = R.drawable.palette,
+ title = { Text(stringResource(R.string.enable_dynamic_theme)) },
+ icon = { Icon(painterResource(R.drawable.palette), null) },
checked = dynamicTheme,
onCheckedChange = onDynamicThemeChange
)
EnumListPreference(
- title = stringResource(R.string.dark_theme),
- icon = R.drawable.dark_mode,
+ title = { Text(stringResource(R.string.dark_theme)) },
+ icon = { Icon(painterResource(R.drawable.dark_mode), null) },
selectedValue = darkMode,
onValueSelected = onDarkModeChange,
valueText = {
@@ -59,14 +59,14 @@ fun AppearanceSettings(
}
)
SwitchPreference(
- title = stringResource(R.string.pure_black),
- icon = R.drawable.contrast,
+ title = { Text(stringResource(R.string.pure_black)) },
+ icon = { Icon(painterResource(R.drawable.contrast), null) },
checked = pureBlack,
onCheckedChange = onPureBlackChange
)
EnumListPreference(
- title = stringResource(R.string.default_open_tab),
- icon = R.drawable.tab,
+ title = { Text(stringResource(R.string.default_open_tab)) },
+ icon = { Icon(painterResource(R.drawable.tab), null) },
selectedValue = defaultOpenTab,
onValueSelected = onDefaultOpenTabChange,
valueText = {
@@ -80,8 +80,8 @@ fun AppearanceSettings(
}
)
EnumListPreference(
- title = stringResource(R.string.lyrics_text_position),
- icon = R.drawable.lyrics,
+ title = { Text(stringResource(R.string.lyrics_text_position)) },
+ icon = { Icon(painterResource(R.drawable.lyrics), null) },
selectedValue = lyricsPosition,
onValueSelected = onLyricsPositionChange,
valueText = {
diff --git a/app/src/main/java/com/zionhuang/music/ui/screens/settings/BackupAndRestore.kt b/app/src/main/java/com/zionhuang/music/ui/screens/settings/BackupAndRestore.kt
index c3adc8ac7..8480cfe85 100644
--- a/app/src/main/java/com/zionhuang/music/ui/screens/settings/BackupAndRestore.kt
+++ b/app/src/main/java/com/zionhuang/music/ui/screens/settings/BackupAndRestore.kt
@@ -39,11 +39,6 @@ fun BackupAndRestore(
viewModel.restore(context, uri)
}
}
- val importLauncher = rememberLauncherForActivityResult(ActivityResultContracts.OpenDocument()) { uri ->
- if (uri != null) {
- viewModel.import(context, uri)
- }
- }
Column(
Modifier
@@ -51,28 +46,20 @@ fun BackupAndRestore(
.verticalScroll(rememberScrollState())
) {
PreferenceEntry(
- title = stringResource(R.string.backup),
- icon = R.drawable.backup,
+ title = { Text(stringResource(R.string.backup)) },
+ icon = { Icon(painterResource(R.drawable.backup), null) },
onClick = {
val formatter = DateTimeFormatter.ofPattern("yyyyMMddHHmmss")
backupLauncher.launch("${context.getString(R.string.app_name)}_${LocalDateTime.now().format(formatter)}.backup")
}
)
PreferenceEntry(
- title = stringResource(R.string.restore),
- icon = R.drawable.restore,
+ title = { Text(stringResource(R.string.restore)) },
+ icon = { Icon(painterResource(R.drawable.restore), null) },
onClick = {
restoreLauncher.launch(arrayOf("application/octet-stream"))
}
)
- PreferenceEntry(
- title = stringResource(R.string.import_playlist),
- description = stringResource(R.string.choose_csv_file_from_google_takeout),
- icon = R.drawable.input,
- onClick = {
- importLauncher.launch(arrayOf("*/*"))
- }
- )
}
TopAppBar(
diff --git a/app/src/main/java/com/zionhuang/music/ui/screens/settings/ContentSettings.kt b/app/src/main/java/com/zionhuang/music/ui/screens/settings/ContentSettings.kt
index ed49b9e1a..42e3e7952 100644
--- a/app/src/main/java/com/zionhuang/music/ui/screens/settings/ContentSettings.kt
+++ b/app/src/main/java/com/zionhuang/music/ui/screens/settings/ContentSettings.kt
@@ -6,15 +6,19 @@ import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.navigation.NavController
+import com.zionhuang.innertube.utils.parseCookieString
import com.zionhuang.music.LocalPlayerAwareWindowInsets
import com.zionhuang.music.R
import com.zionhuang.music.constants.*
import com.zionhuang.music.ui.component.EditTextPreference
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.component.SwitchPreference
import com.zionhuang.music.utils.rememberEnumPreference
@@ -27,6 +31,12 @@ fun ContentSettings(
navController: NavController,
scrollBehavior: TopAppBarScrollBehavior,
) {
+ val accountName by rememberPreference(AccountNameKey, "")
+ val accountEmail by rememberPreference(AccountEmailKey, "")
+ val innerTubeCookie by rememberPreference(InnerTubeCookieKey, "")
+ val isLoggedIn = remember(innerTubeCookie) {
+ "SAPISID" in parseCookieString(innerTubeCookie)
+ }
val (contentLanguage, onContentLanguageChange) = rememberPreference(key = ContentLanguageKey, defaultValue = "system")
val (contentCountry, onContentCountryChange) = rememberPreference(key = ContentCountryKey, defaultValue = "system")
val (proxyEnabled, onProxyEnabledChange) = rememberPreference(key = ProxyEnabledKey, defaultValue = false)
@@ -39,9 +49,15 @@ fun ContentSettings(
.windowInsetsPadding(LocalPlayerAwareWindowInsets.current)
.verticalScroll(rememberScrollState())
) {
+ PreferenceEntry(
+ title = { Text(if (isLoggedIn) accountName else stringResource(R.string.login)) },
+ description = if (isLoggedIn) accountEmail else null,
+ icon = { Icon(painterResource(R.drawable.person), null) },
+ onClick = { navController.navigate("login") }
+ )
ListPreference(
- title = stringResource(R.string.content_language),
- icon = R.drawable.language,
+ title = { Text(stringResource(R.string.content_language)) },
+ icon = { Icon(painterResource(R.drawable.language), null) },
selectedValue = contentLanguage,
values = listOf(SYSTEM_DEFAULT) + LanguageCodeToName.keys.toList(),
valueText = {
@@ -52,8 +68,8 @@ fun ContentSettings(
onValueSelected = onContentLanguageChange
)
ListPreference(
- title = stringResource(R.string.content_country),
- icon = R.drawable.location_on,
+ title = { Text(stringResource(R.string.content_country)) },
+ icon = { Icon(painterResource(R.drawable.location_on), null) },
selectedValue = contentCountry,
values = listOf(SYSTEM_DEFAULT) + CountryCodeToName.keys.toList(),
valueText = {
@@ -69,21 +85,21 @@ fun ContentSettings(
)
SwitchPreference(
- title = stringResource(R.string.enable_proxy),
+ title = { Text(stringResource(R.string.enable_proxy)) },
checked = proxyEnabled,
onCheckedChange = onProxyEnabledChange
)
if (proxyEnabled) {
ListPreference(
- title = stringResource(R.string.proxy_type),
+ title = { Text(stringResource(R.string.proxy_type)) },
selectedValue = proxyType,
values = listOf(Proxy.Type.HTTP, Proxy.Type.SOCKS),
valueText = { it.name },
onValueSelected = onProxyTypeChange
)
EditTextPreference(
- title = stringResource(R.string.proxy_url),
+ title = { Text(stringResource(R.string.proxy_url)) },
value = proxyUrl,
onValueChange = onProxyUrlChange
)
diff --git a/app/src/main/java/com/zionhuang/music/ui/screens/settings/PlayerSettings.kt b/app/src/main/java/com/zionhuang/music/ui/screens/settings/PlayerSettings.kt
index 5679864a8..49a988efb 100644
--- a/app/src/main/java/com/zionhuang/music/ui/screens/settings/PlayerSettings.kt
+++ b/app/src/main/java/com/zionhuang/music/ui/screens/settings/PlayerSettings.kt
@@ -39,8 +39,8 @@ fun PlayerSettings(
.verticalScroll(rememberScrollState())
) {
EnumListPreference(
- title = stringResource(R.string.audio_quality),
- icon = R.drawable.graphic_eq,
+ title = { Text(stringResource(R.string.audio_quality)) },
+ icon = { Icon(painterResource(R.drawable.graphic_eq), null) },
selectedValue = audioQuality,
onValueSelected = onAudioQualityChange,
valueText = {
@@ -52,20 +52,20 @@ fun PlayerSettings(
}
)
SwitchPreference(
- title = stringResource(R.string.persistent_queue),
- icon = R.drawable.queue_music,
+ title = { Text(stringResource(R.string.persistent_queue)) },
+ icon = { Icon(painterResource(R.drawable.queue_music), null) },
checked = persistentQueue,
onCheckedChange = onPersistentQueueChange
)
SwitchPreference(
- title = stringResource(R.string.skip_silence),
- icon = R.drawable.skip_next,
+ title = { Text(stringResource(R.string.skip_silence)) },
+ icon = { Icon(painterResource(R.drawable.skip_next), null) },
checked = skipSilence,
onCheckedChange = onSkipSilenceChange
)
SwitchPreference(
- title = stringResource(R.string.audio_normalization),
- icon = R.drawable.volume_up,
+ title = { Text(stringResource(R.string.audio_normalization)) },
+ icon = { Icon(painterResource(R.drawable.volume_up), null) },
checked = audioNormalization,
onCheckedChange = onAudioNormalizationChange
)
diff --git a/app/src/main/java/com/zionhuang/music/ui/screens/settings/PrivacySettings.kt b/app/src/main/java/com/zionhuang/music/ui/screens/settings/PrivacySettings.kt
index d8e1fd7db..e43a25b1d 100644
--- a/app/src/main/java/com/zionhuang/music/ui/screens/settings/PrivacySettings.kt
+++ b/app/src/main/java/com/zionhuang/music/ui/screens/settings/PrivacySettings.kt
@@ -110,30 +110,30 @@ fun PrivacySettings(
.verticalScroll(rememberScrollState())
) {
SwitchPreference(
- title = stringResource(R.string.pause_listen_history),
- icon = R.drawable.history,
+ title = { Text(stringResource(R.string.pause_listen_history)) },
+ icon = { Icon(painterResource(R.drawable.history), null) },
checked = pauseListenHistory,
onCheckedChange = onPauseListenHistoryChange
)
PreferenceEntry(
- title = stringResource(R.string.clear_listen_history),
- icon = R.drawable.clear_all,
+ title = { Text(stringResource(R.string.clear_listen_history)) },
+ icon = { Icon(painterResource(R.drawable.clear_all), null) },
onClick = { showClearListenHistoryDialog = true }
)
SwitchPreference(
- title = stringResource(R.string.pause_search_history),
- icon = R.drawable.manage_search,
+ title = { Text(stringResource(R.string.pause_search_history)) },
+ icon = { Icon(painterResource(R.drawable.manage_search), null) },
checked = pauseSearchHistory,
onCheckedChange = onPauseSearchHistoryChange
)
PreferenceEntry(
- title = stringResource(R.string.clear_search_history),
- icon = R.drawable.clear_all,
+ title = { Text(stringResource(R.string.clear_search_history)) },
+ icon = { Icon(painterResource(R.drawable.clear_all), null) },
onClick = { showClearSearchHistoryDialog = true }
)
SwitchPreference(
- title = stringResource(R.string.enable_kugou),
- icon = R.drawable.lyrics,
+ title = { Text(stringResource(R.string.enable_kugou)) },
+ icon = { Icon(painterResource(R.drawable.lyrics), null) },
checked = enableKugou,
onCheckedChange = onEnableKugouChange
)
diff --git a/app/src/main/java/com/zionhuang/music/ui/screens/settings/SettingsScreen.kt b/app/src/main/java/com/zionhuang/music/ui/screens/settings/SettingsScreen.kt
index 4bb4c24bb..698323091 100644
--- a/app/src/main/java/com/zionhuang/music/ui/screens/settings/SettingsScreen.kt
+++ b/app/src/main/java/com/zionhuang/music/ui/screens/settings/SettingsScreen.kt
@@ -7,9 +7,11 @@ import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.navigation.NavController
+import com.zionhuang.music.BuildConfig
import com.zionhuang.music.LocalPlayerAwareWindowInsets
import com.zionhuang.music.R
import com.zionhuang.music.ui.component.PreferenceEntry
@@ -17,49 +19,71 @@ import com.zionhuang.music.ui.component.PreferenceEntry
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SettingsScreen(
+ latestVersion: Long,
navController: NavController,
scrollBehavior: TopAppBarScrollBehavior,
) {
+ val uriHandler = LocalUriHandler.current
+
Column(
modifier = Modifier
.windowInsetsPadding(LocalPlayerAwareWindowInsets.current)
.verticalScroll(rememberScrollState())
) {
PreferenceEntry(
- title = stringResource(R.string.appearance),
- icon = R.drawable.palette,
+ title = { Text(stringResource(R.string.appearance)) },
+ icon = { Icon(painterResource(R.drawable.palette), null) },
onClick = { navController.navigate("settings/appearance") }
)
PreferenceEntry(
- title = stringResource(R.string.content),
- icon = R.drawable.language,
+ title = { Text(stringResource(R.string.content)) },
+ icon = { Icon(painterResource(R.drawable.language), null) },
onClick = { navController.navigate("settings/content") }
)
PreferenceEntry(
- title = stringResource(R.string.player_and_audio),
- icon = R.drawable.play,
+ title = { Text(stringResource(R.string.player_and_audio)) },
+ icon = { Icon(painterResource(R.drawable.play), null) },
onClick = { navController.navigate("settings/player") }
)
PreferenceEntry(
- title = stringResource(R.string.storage),
- icon = R.drawable.storage,
+ title = { Text(stringResource(R.string.storage)) },
+ icon = { Icon(painterResource(R.drawable.storage), null) },
onClick = { navController.navigate("settings/storage") }
)
PreferenceEntry(
- title = stringResource(R.string.privacy),
- icon = R.drawable.security,
+ title = { Text(stringResource(R.string.privacy)) },
+ icon = { Icon(painterResource(R.drawable.security), null) },
onClick = { navController.navigate("settings/privacy") }
)
PreferenceEntry(
- title = stringResource(R.string.backup_restore),
- icon = R.drawable.restore,
+ title = { Text(stringResource(R.string.backup_restore)) },
+ icon = { Icon(painterResource(R.drawable.restore), null) },
onClick = { navController.navigate("settings/backup_restore") }
)
PreferenceEntry(
- title = stringResource(R.string.about),
- icon = R.drawable.info,
+ title = { Text(stringResource(R.string.about)) },
+ icon = { Icon(painterResource(R.drawable.info), null) },
onClick = { navController.navigate("settings/about") }
)
+ if (latestVersion > BuildConfig.VERSION_CODE) {
+ PreferenceEntry(
+ title = {
+ Text(
+ text = stringResource(R.string.new_version_available),
+ )
+ },
+ icon = {
+ BadgedBox(
+ badge = { Badge() }
+ ) {
+ Icon(painterResource(R.drawable.update), null)
+ }
+ },
+ onClick = {
+ uriHandler.openUri("https://github.com/z-huang/InnerTune/releases/latest")
+ }
+ )
+ }
}
TopAppBar(
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 5ea11de3a..2ec8fa899 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
@@ -105,7 +105,7 @@ fun StorageSettings(
)
PreferenceEntry(
- title = stringResource(R.string.clear_all_downloads),
+ title = { Text(stringResource(R.string.clear_all_downloads)) },
onClick = {
coroutineScope.launch(Dispatchers.IO) {
downloadCache.keys.forEach { key ->
@@ -141,7 +141,7 @@ fun StorageSettings(
}
ListPreference(
- title = stringResource(R.string.max_cache_size),
+ title = { Text(stringResource(R.string.max_cache_size)) },
selectedValue = maxSongCacheSize,
values = listOf(128, 256, 512, 1024, 2048, 4096, 8192, -1),
valueText = {
@@ -151,7 +151,7 @@ fun StorageSettings(
)
PreferenceEntry(
- title = stringResource(R.string.clear_song_cache),
+ title = { Text(stringResource(R.string.clear_song_cache)) },
onClick = {
coroutineScope.launch(Dispatchers.IO) {
playerCache.keys.forEach { key ->
@@ -179,7 +179,7 @@ fun StorageSettings(
)
ListPreference(
- title = stringResource(R.string.max_cache_size),
+ title = { Text(stringResource(R.string.max_cache_size)) },
selectedValue = maxImageCacheSize,
values = listOf(128, 256, 512, 1024, 2048, 4096, 8192),
valueText = { formatFileSize(it * 1024 * 1024L) },
@@ -187,7 +187,7 @@ fun StorageSettings(
)
PreferenceEntry(
- title = stringResource(R.string.clear_image_cache),
+ title = { Text(stringResource(R.string.clear_image_cache)) },
onClick = {
coroutineScope.launch(Dispatchers.IO) {
imageDiskCache.clear()
diff --git a/app/src/main/java/com/zionhuang/music/ui/utils/LazyListStateUtils.kt b/app/src/main/java/com/zionhuang/music/ui/utils/ScrollUtils.kt
similarity index 73%
rename from app/src/main/java/com/zionhuang/music/ui/utils/LazyListStateUtils.kt
rename to app/src/main/java/com/zionhuang/music/ui/utils/ScrollUtils.kt
index cc7061c2e..975e195f4 100644
--- a/app/src/main/java/com/zionhuang/music/ui/utils/LazyListStateUtils.kt
+++ b/app/src/main/java/com/zionhuang/music/ui/utils/ScrollUtils.kt
@@ -1,5 +1,6 @@
package com.zionhuang.music.ui.utils
+import androidx.compose.foundation.ScrollState
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.derivedStateOf
@@ -24,4 +25,16 @@ fun LazyListState.isScrollingUp(): Boolean {
}
}
}.value
-}
\ No newline at end of file
+}
+
+@Composable
+fun ScrollState.isScrollingUp(): Boolean {
+ var previousScrollOffset by remember(this) { mutableStateOf(value) }
+ return remember(this) {
+ derivedStateOf {
+ (previousScrollOffset >= value).also {
+ previousScrollOffset = value
+ }
+ }
+ }.value
+}
diff --git a/app/src/main/java/com/zionhuang/music/viewmodels/AccountViewModel.kt b/app/src/main/java/com/zionhuang/music/viewmodels/AccountViewModel.kt
new file mode 100644
index 000000000..dd73c3124
--- /dev/null
+++ b/app/src/main/java/com/zionhuang/music/viewmodels/AccountViewModel.kt
@@ -0,0 +1,25 @@
+package com.zionhuang.music.viewmodels
+
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import com.zionhuang.innertube.YouTube
+import com.zionhuang.innertube.models.PlaylistItem
+import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.launch
+import javax.inject.Inject
+
+@HiltViewModel
+class AccountViewModel @Inject constructor() : ViewModel() {
+ val playlists = MutableStateFlow?>(null)
+
+ init {
+ viewModelScope.launch {
+ YouTube.likedPlaylists().onSuccess {
+ playlists.value = it
+ }.onFailure {
+ it.printStackTrace()
+ }
+ }
+ }
+}
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 d90d047ed..7f61d6e80 100644
--- a/app/src/main/java/com/zionhuang/music/viewmodels/ArtistViewModel.kt
+++ b/app/src/main/java/com/zionhuang/music/viewmodels/ArtistViewModel.kt
@@ -22,6 +22,8 @@ class ArtistViewModel @Inject constructor(
) : ViewModel() {
val artistId = savedStateHandle.get("artistId")!!
var artistPage by mutableStateOf(null)
+ val libraryArtist = database.artist(artistId)
+ .stateIn(viewModelScope, SharingStarted.Lazily, null)
val librarySongs = database.artistSongsPreview(artistId)
.stateIn(viewModelScope, SharingStarted.Lazily, emptyList())
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 a1483f963..60b92321f 100644
--- a/app/src/main/java/com/zionhuang/music/viewmodels/BackupRestoreViewModel.kt
+++ b/app/src/main/java/com/zionhuang/music/viewmodels/BackupRestoreViewModel.kt
@@ -3,31 +3,20 @@ package com.zionhuang.music.viewmodels
import android.content.Context
import android.content.Intent
import android.net.Uri
-import android.provider.OpenableColumns
import android.widget.Toast
import androidx.lifecycle.ViewModel
-import androidx.lifecycle.viewModelScope
-import com.zionhuang.innertube.YouTube
-import com.zionhuang.innertube.YouTube.MAX_GET_QUEUE_SIZE
-import com.zionhuang.innertube.models.SongItem
import com.zionhuang.music.MainActivity
import com.zionhuang.music.R
import com.zionhuang.music.db.InternalDatabase
import com.zionhuang.music.db.MusicDatabase
-import com.zionhuang.music.db.entities.PlaylistEntity
-import com.zionhuang.music.db.entities.PlaylistEntity.Companion.generatePlaylistId
-import com.zionhuang.music.db.entities.PlaylistSongMap
import com.zionhuang.music.extensions.div
import com.zionhuang.music.extensions.zipInputStream
import com.zionhuang.music.extensions.zipOutputStream
-import com.zionhuang.music.models.toMediaMetadata
import com.zionhuang.music.playback.MusicService
import com.zionhuang.music.playback.MusicService.Companion.PERSISTENT_QUEUE_FILE
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
-import kotlinx.coroutines.withContext
import java.io.FileInputStream
import java.io.FileOutputStream
import java.util.zip.ZipEntry
@@ -100,64 +89,6 @@ class BackupRestoreViewModel @Inject constructor(
}
}
- fun import(context: Context, uri: Uri) {
- runCatching {
- val videoIds = mutableListOf()
- context.applicationContext.contentResolver.openInputStream(uri)?.use { inputStream ->
- val br = inputStream.bufferedReader()
- repeat(8) {
- br.readLine()
- }
- var line = br.readLine()
- while (line != null) {
- line.split(",").firstOrNull()
- ?.takeIf { it.isNotEmpty() }
- ?.let {
- videoIds.add(it.trim())
- }
- line = br.readLine()
- }
- }
- val playlistName = context.contentResolver.query(uri, null, null, null, null)?.use { cursor ->
- val nameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)
- cursor.moveToFirst()
- cursor.getString(nameIndex)
- }?.removeSuffix(".csv") ?: context.getString(R.string.imported_playlist)
- viewModelScope.launch {
- val songs = videoIds.chunked(MAX_GET_QUEUE_SIZE).flatMap {
- withContext(Dispatchers.IO) {
- YouTube.queue(videoIds = it)
- }.getOrNull().orEmpty()
- }
- database.transaction {
- val playlistId = generatePlaylistId()
- var position = 0
- insert(
- PlaylistEntity(
- id = playlistId,
- name = playlistName
- )
- )
- songs.map(SongItem::toMediaMetadata)
- .onEach(::insert)
- .forEach {
- insert(
- PlaylistSongMap(
- playlistId = playlistId,
- songId = it.id,
- position = position++
- )
- )
- }
- }
- Toast.makeText(context, context.resources.getQuantityString(R.plurals.import_success, songs.size, playlistName, songs.size), Toast.LENGTH_SHORT).show()
- }
- }.onFailure {
- it.printStackTrace()
- Toast.makeText(context, R.string.restore_failed, Toast.LENGTH_SHORT).show()
- }
- }
-
companion object {
const val SETTINGS_FILENAME = "settings.preferences_pb"
}
diff --git a/app/src/main/java/com/zionhuang/music/viewmodels/BuiltInPlaylistViewModel.kt b/app/src/main/java/com/zionhuang/music/viewmodels/BuiltInPlaylistViewModel.kt
index a53f6afa8..6f813253d 100644
--- a/app/src/main/java/com/zionhuang/music/viewmodels/BuiltInPlaylistViewModel.kt
+++ b/app/src/main/java/com/zionhuang/music/viewmodels/BuiltInPlaylistViewModel.kt
@@ -71,6 +71,7 @@ class BuiltInPlaylistViewModel @Inject constructor(
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)
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 9aa7de8c4..cabe66ff9 100644
--- a/app/src/main/java/com/zionhuang/music/viewmodels/HomeViewModel.kt
+++ b/app/src/main/java/com/zionhuang/music/viewmodels/HomeViewModel.kt
@@ -3,7 +3,7 @@ package com.zionhuang.music.viewmodels
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.zionhuang.innertube.YouTube
-import com.zionhuang.innertube.models.AlbumItem
+import com.zionhuang.innertube.pages.ExplorePage
import com.zionhuang.music.db.MusicDatabase
import com.zionhuang.music.db.entities.Song
import dagger.hilt.android.lifecycle.HiltViewModel
@@ -20,12 +20,14 @@ class HomeViewModel @Inject constructor(
val isRefreshing = MutableStateFlow(false)
val quickPicks = MutableStateFlow?>(null)
- val newReleaseAlbums = MutableStateFlow>(emptyList())
+ val explorePage = MutableStateFlow(null)
private suspend fun load() {
quickPicks.value = database.quickPicks().first().shuffled().take(20)
- YouTube.newReleaseAlbumsPreview().onSuccess {
- newReleaseAlbums.value = it
+ YouTube.explore().onSuccess {
+ explorePage.value = it
+ }.onFailure {
+ it.printStackTrace()
}
}
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 c6784e8e1..b5ff00181 100644
--- a/app/src/main/java/com/zionhuang/music/viewmodels/LibraryViewModels.kt
+++ b/app/src/main/java/com/zionhuang/music/viewmodels/LibraryViewModels.kt
@@ -44,11 +44,18 @@ class LibraryArtistsViewModel @Inject constructor(
) : ViewModel() {
val allArtists = context.dataStore.data
.map {
- it[ArtistSortTypeKey].toEnum(ArtistSortType.CREATE_DATE) to (it[ArtistSortDescendingKey] ?: true)
+ Triple(
+ it[ArtistViewTypeKey].toEnum(ArtistViewType.ALL),
+ it[ArtistSortTypeKey].toEnum(ArtistSortType.CREATE_DATE),
+ it[ArtistSortDescendingKey] ?: true
+ )
}
.distinctUntilChanged()
- .flatMapLatest { (sortType, descending) ->
- database.artists(sortType, descending)
+ .flatMapLatest { (viewType, sortType, descending) ->
+ when (viewType) {
+ ArtistViewType.ALL -> database.artists(sortType, descending)
+ ArtistViewType.BOOKMARKED -> database.artistsBookmarked(sortType, descending)
+ }
}
.stateIn(viewModelScope, SharingStarted.Lazily, emptyList())
diff --git a/app/src/main/java/com/zionhuang/music/viewmodels/LocalPlaylistViewModel.kt b/app/src/main/java/com/zionhuang/music/viewmodels/LocalPlaylistViewModel.kt
index 959dfe7d6..380a41cbf 100644
--- a/app/src/main/java/com/zionhuang/music/viewmodels/LocalPlaylistViewModel.kt
+++ b/app/src/main/java/com/zionhuang/music/viewmodels/LocalPlaylistViewModel.kt
@@ -44,6 +44,7 @@ class LocalPlaylistViewModel @Inject constructor(
PlaylistSongSortType.ARTIST -> songs.sortedBy { song ->
song.song.artists.joinToString { it.name }
}
+ PlaylistSongSortType.PLAY_TIME -> songs.sortedBy { it.song.song.totalPlayTime }
}.reversed(sortDescending && sortType != PlaylistSongSortType.CUSTOM)
}.stateIn(viewModelScope, SharingStarted.Lazily, emptyList())
}
diff --git a/app/src/main/java/com/zionhuang/music/viewmodels/MoodAndGenresViewModel.kt b/app/src/main/java/com/zionhuang/music/viewmodels/MoodAndGenresViewModel.kt
new file mode 100644
index 000000000..ea2a29d51
--- /dev/null
+++ b/app/src/main/java/com/zionhuang/music/viewmodels/MoodAndGenresViewModel.kt
@@ -0,0 +1,25 @@
+package com.zionhuang.music.viewmodels
+
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import com.zionhuang.innertube.YouTube
+import com.zionhuang.innertube.pages.MoodAndGenres
+import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.launch
+import javax.inject.Inject
+
+@HiltViewModel
+class MoodAndGenresViewModel @Inject constructor() : ViewModel() {
+ val moodAndGenres = MutableStateFlow?>(null)
+
+ init {
+ viewModelScope.launch {
+ YouTube.moodAndGenres().onSuccess {
+ moodAndGenres.value = it
+ }.onFailure {
+ it.printStackTrace()
+ }
+ }
+ }
+}
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 8153e8cb6..8770dfe64 100644
--- a/app/src/main/java/com/zionhuang/music/viewmodels/NewReleaseViewModel.kt
+++ b/app/src/main/java/com/zionhuang/music/viewmodels/NewReleaseViewModel.kt
@@ -19,6 +19,8 @@ class NewReleaseViewModel @Inject constructor() : ViewModel() {
viewModelScope.launch {
YouTube.newReleaseAlbums().onSuccess {
_newReleaseAlbums.value = it
+ }.onFailure {
+ it.printStackTrace()
}
}
}
diff --git a/app/src/main/java/com/zionhuang/music/viewmodels/OnlineSearchViewModel.kt b/app/src/main/java/com/zionhuang/music/viewmodels/OnlineSearchViewModel.kt
index 3baf5146a..0de0e189c 100644
--- a/app/src/main/java/com/zionhuang/music/viewmodels/OnlineSearchViewModel.kt
+++ b/app/src/main/java/com/zionhuang/music/viewmodels/OnlineSearchViewModel.kt
@@ -13,13 +13,14 @@ import com.zionhuang.music.models.ItemsPage
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch
+import java.net.URLDecoder
import javax.inject.Inject
@HiltViewModel
class OnlineSearchViewModel @Inject constructor(
savedStateHandle: SavedStateHandle,
) : ViewModel() {
- val query = savedStateHandle.get("query")!!
+ val query = URLDecoder.decode(savedStateHandle.get("query")!!, "UTF-8")!!
val filter = MutableStateFlow(null)
var summaryPage by mutableStateOf(null)
val viewStateMap = mutableStateMapOf()
@@ -33,8 +34,8 @@ class OnlineSearchViewModel @Inject constructor(
}
} else {
if (viewStateMap[filter.value] == null) {
- viewStateMap[filter.value] = YouTube.search(query, filter).getOrNull()?.let {
- ItemsPage(it.items, it.continuation)
+ viewStateMap[filter.value] = YouTube.search(query, filter).getOrNull()?.let { result ->
+ ItemsPage(result.items.distinctBy { it.id }, result.continuation)
}
}
}
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 499a08417..d8dda558d 100644
--- a/app/src/main/java/com/zionhuang/music/viewmodels/StatsViewModel.kt
+++ b/app/src/main/java/com/zionhuang/music/viewmodels/StatsViewModel.kt
@@ -3,23 +3,43 @@ package com.zionhuang.music.viewmodels
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.zionhuang.innertube.YouTube
+import com.zionhuang.music.constants.StatPeriod
import com.zionhuang.music.db.MusicDatabase
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
import java.time.LocalDateTime
import javax.inject.Inject
+@OptIn(ExperimentalCoroutinesApi::class)
@HiltViewModel
class StatsViewModel @Inject constructor(
val database: MusicDatabase,
) : ViewModel() {
- val mostPlayedSongs = database.mostPlayedSongs()
- .stateIn(viewModelScope, SharingStarted.Lazily, emptyList())
- val mostPlayedArtists = database.mostPlayedArtists()
- .stateIn(viewModelScope, SharingStarted.Lazily, emptyList())
+ val statPeriod = MutableStateFlow(StatPeriod.`1_WEEK`)
+
+ val mostPlayedSongs = statPeriod.flatMapLatest { period ->
+ database.mostPlayedSongs(period.toTimeMillis())
+ }.stateIn(viewModelScope, SharingStarted.Lazily, emptyList())
+
+ val mostPlayedArtists = statPeriod.flatMapLatest { period ->
+ database.mostPlayedArtists(period.toTimeMillis())
+ }.stateIn(viewModelScope, SharingStarted.Lazily, emptyList())
+
+
+ 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 {
viewModelScope.launch {
@@ -39,4 +59,4 @@ class StatsViewModel @Inject constructor(
}
}
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/com/zionhuang/music/viewmodels/YouTubeBrowseViewModel.kt b/app/src/main/java/com/zionhuang/music/viewmodels/YouTubeBrowseViewModel.kt
new file mode 100644
index 000000000..2e53332d3
--- /dev/null
+++ b/app/src/main/java/com/zionhuang/music/viewmodels/YouTubeBrowseViewModel.kt
@@ -0,0 +1,31 @@
+package com.zionhuang.music.viewmodels
+
+import androidx.lifecycle.SavedStateHandle
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import com.zionhuang.innertube.YouTube
+import com.zionhuang.innertube.pages.BrowseResult
+import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.launch
+import javax.inject.Inject
+
+@HiltViewModel
+class YouTubeBrowseViewModel @Inject constructor(
+ savedStateHandle: SavedStateHandle,
+) : ViewModel() {
+ private val browseId = savedStateHandle.get("browseId")!!
+ private val params = savedStateHandle.get("params")
+
+ val result = MutableStateFlow(null)
+
+ init {
+ viewModelScope.launch {
+ YouTube.browse(browseId, params).onSuccess {
+ result.value = it
+ }.onFailure {
+ it.printStackTrace()
+ }
+ }
+ }
+}
diff --git a/app/src/main/res/drawable/bookmark.xml b/app/src/main/res/drawable/bookmark.xml
new file mode 100644
index 000000000..e69165b59
--- /dev/null
+++ b/app/src/main/res/drawable/bookmark.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/bookmark_filled.xml b/app/src/main/res/drawable/bookmark_filled.xml
new file mode 100644
index 000000000..c3cb3d600
--- /dev/null
+++ b/app/src/main/res/drawable/bookmark_filled.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/mood.xml b/app/src/main/res/drawable/mood.xml
new file mode 100644
index 000000000..adb1afa06
--- /dev/null
+++ b/app/src/main/res/drawable/mood.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/person.xml b/app/src/main/res/drawable/person.xml
new file mode 100644
index 000000000..cac31b256
--- /dev/null
+++ b/app/src/main/res/drawable/person.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/remove.xml b/app/src/main/res/drawable/remove.xml
new file mode 100644
index 000000000..b13fa153f
--- /dev/null
+++ b/app/src/main/res/drawable/remove.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/settings.xml b/app/src/main/res/drawable/settings.xml
index 48db39640..57c7adb7c 100644
--- a/app/src/main/res/drawable/settings.xml
+++ b/app/src/main/res/drawable/settings.xml
@@ -5,5 +5,5 @@
android:viewportHeight="960">
+ android:pathData="M370,880L354,752Q341,747 329.5,740Q318,733 307,725L188,775L78,585L181,507Q180,500 180,493.5Q180,487 180,480Q180,473 180,466.5Q180,460 181,453L78,375L188,185L307,235Q318,227 330,220Q342,213 354,208L370,80L590,80L606,208Q619,213 630.5,220Q642,227 653,235L772,185L882,375L779,453Q780,460 780,466.5Q780,473 780,480Q780,487 780,493.5Q780,500 778,507L881,585L771,775L653,725Q642,733 630,740Q618,747 606,752L590,880L370,880ZM482,620Q540,620 581,579Q622,538 622,480Q622,422 581,381Q540,340 482,340Q423,340 382.5,381Q342,422 342,480Q342,538 382.5,579Q423,620 482,620ZM482,540Q457,540 439.5,522.5Q422,505 422,480Q422,455 439.5,437.5Q457,420 482,420Q507,420 524.5,437.5Q542,455 542,480Q542,505 524.5,522.5Q507,540 482,540ZM480,480L480,480L480,480Q480,480 480,480Q480,480 480,480L480,480L480,480L480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480L480,480L480,480L480,480Q480,480 480,480Q480,480 480,480L480,480L480,480L480,480Q480,480 480,480Q480,480 480,480L480,480L480,480L480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480L480,480L480,480L480,480Q480,480 480,480Q480,480 480,480L480,480ZM440,800L519,800L533,694Q564,686 590.5,670.5Q617,655 639,633L738,674L777,606L691,541Q696,527 698,511.5Q700,496 700,480Q700,464 698,448.5Q696,433 691,419L777,354L738,286L639,328Q617,305 590.5,289.5Q564,274 533,266L520,160L441,160L427,266Q396,274 369.5,289.5Q343,305 321,327L222,286L183,354L269,418Q264,433 262,448Q260,463 260,480Q260,496 262,511Q264,526 269,541L183,606L222,674L321,632Q343,655 369.5,670.5Q396,686 427,694L440,800Z" />
diff --git a/app/src/main/res/drawable/shortcut_albums.xml b/app/src/main/res/drawable/shortcut_albums.xml
new file mode 100644
index 000000000..7aed64f3a
--- /dev/null
+++ b/app/src/main/res/drawable/shortcut_albums.xml
@@ -0,0 +1,9 @@
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/shortcut_playlists.xml b/app/src/main/res/drawable/shortcut_playlists.xml
new file mode 100644
index 000000000..eba0fe7ea
--- /dev/null
+++ b/app/src/main/res/drawable/shortcut_playlists.xml
@@ -0,0 +1,10 @@
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/shortcut_search.xml b/app/src/main/res/drawable/shortcut_search.xml
new file mode 100644
index 000000000..c56846b00
--- /dev/null
+++ b/app/src/main/res/drawable/shortcut_search.xml
@@ -0,0 +1,9 @@
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/shortcut_songs.xml b/app/src/main/res/drawable/shortcut_songs.xml
new file mode 100644
index 000000000..710ec0019
--- /dev/null
+++ b/app/src/main/res/drawable/shortcut_songs.xml
@@ -0,0 +1,9 @@
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/slow_motion_video.xml b/app/src/main/res/drawable/slow_motion_video.xml
new file mode 100644
index 000000000..69ed770ae
--- /dev/null
+++ b/app/src/main/res/drawable/slow_motion_video.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/tune.xml b/app/src/main/res/drawable/tune.xml
new file mode 100644
index 000000000..9ab9dfc71
--- /dev/null
+++ b/app/src/main/res/drawable/tune.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/update.xml b/app/src/main/res/drawable/update.xml
new file mode 100644
index 000000000..65fd69ac7
--- /dev/null
+++ b/app/src/main/res/drawable/update.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 75d7584a0..adc998ec7 100644
--- a/app/src/main/res/values-DE/strings.xml
+++ b/app/src/main/res/values-DE/strings.xml
@@ -13,21 +13,24 @@
- History
- Stats
- Quick picks
- Listen some songs to generate your quick picks
- New release albums
+ Hörverlauf
+ Statistiken
+ Stimmungen und Genres
+ Account
+ Schnellauswahl
+ Hören Sie sich einige Songs an, um Ihre Schnellauswahl zu treffen
+ Neu veröffentlichte Alben
- Today
- Yesterday
- This week
- Last week
+ Heute
+ Gestern
+ Diese Woche
+ Letzte Woche
- Most played songs
- Most played artists
+ Meist gespielte Songs
+ Meist gespielte Künstler
+ Meist gespielte Alben
Suche
@@ -41,7 +44,8 @@
Wiedergabelisten
Community-Wiedergabelisten
Ausgewählte Wiedergabelisten
- No results found
+ Bookmarked
+ Keine Ergebnisse gefunden
Aus der Bibliothek
@@ -49,7 +53,7 @@
Beliebte Titel
Heruntergeladene Titel
- The playlist is empty
+ Die Wiedergabeliste ist leer
Wiederholen
@@ -75,7 +79,7 @@
Neu laden
Teilen
Löschen
- Remove from history
+ aus dem Wiedergabeverlauf entfernen
Online-Suche
Sync
@@ -87,7 +91,7 @@
Anzahl der Lieder
Länge
Spielzeit
- Custom order
+ Individuelle Reinfolge
Medien-ID
@@ -138,20 +142,32 @@
- %d Wiedergabeliste
- %d Wiedergabelisten
+
+ - %d Woche
+ - %d Wochen
+
+
+ - %d Monat
+ - %d Monate
+
+
+ - %d Jahr
+ - %d Jahre
+
Wiedergabeliste importiert
- Removed \"%s\" from playlist
- Playlist synced
- Undo
+ Entfernt \"%s\" aus der Wiedergabeliste
+ Wiedergabeliste synchronisiert
+ Rückgängig machen
Liedtext nicht gefunden
- Sleep timer
- End of song
+ Schlaf-Timer
+ Ende des Liedes
- 1 minute
- - %d minutes
+ - %d Minuten
Kein Stream verfügbar
Keine Netzverbindung
@@ -172,12 +188,12 @@
Einstellungen
Erscheinungsbild
- Enable dynamic theme
+ Dynamisches Design aktivieren
Dunkles Thema
An
Aus
System folgen
- Pure black
+ Reines Schwarz
Standardmäßig geöffnete Registerkarte
Anpassen der Navigationsregisterkarten
Position des Liedtextes
@@ -207,39 +223,36 @@
Speicher
Zwischenspeicher
- Image Cache
- Song Cache
- Max cache size
- Unlimited
- Clear all downloads
+ Bild-Cache
+ Song-Cache
+ Maximale Cache-Größe
+ Unbegrenzt
+ Alle Downloads entfernen
Maximale Größe des Bild-Caches
Bild-Cache löschen
Maximale Größe des Song-Cache
- Clear song cache
+ Song-Cache löschen
%s verwendet
Privatsphäre
- Pause listen history
- Clear listen history
- Are you sure to clear all listen history?
+ Pausieren des Hörverlaufs
+ Hörverlauf löschen
+ Sind Sie sicher, dass Sie den gesamten Hörverlauf löschen wollen?
Suchverlauf anhalten
Suchverlauf löschen
- Sind Sie sicher, dass Sie den gesamten Suchverlauf löschen?
+ Sind Sie sicher, dass Sie den gesamten Suchverlauf löschen wollen?
KuGou-Liedtextanbieter aktivieren
Sichern und Wiederherstellen
Datensicherung
Wiederherstellen
- Choose a csv file from Google Takeout
- Imported playlist
-
- - Imported \"%s\" with %d song
- - Imported \"%s\" with %d songs
-
+ Importierte Wiedergabeliste
Sicherung erfolgreich erstellt
Konnte keine Sicherung erstellen
Wiederherstellung der Sicherung fehlgeschlagen
Über
App-Version
+
+ New version available
diff --git a/app/src/main/res/values-be/strings.xml b/app/src/main/res/values-be/strings.xml
new file mode 100644
index 000000000..5e4f5bfec
--- /dev/null
+++ b/app/src/main/res/values-be/strings.xml
@@ -0,0 +1,276 @@
+
+
+ Галоўнае
+ Песні
+ Выканаўцы
+ Альбомы
+ Плэй-лісты
+
+
+
+ - %d абрана
+ - %d абраны
+ - %d абрана
+ - %d абрана
+
+
+
+ Гісторыя
+ Статыстыка
+ Настроі і жанры
+ Уліковы запіс
+ Хуткі выбар
+ Праслухайце якія-небудзь кампазіцыі, каб стварыць ваш хуткі выбар.
+ Новыя рэлізы альбомаў
+
+
+ Сёння
+ Учора
+ На гэтым тыднія
+ На тым тыдні
+
+
+ Лепшыя кампазіцыі
+ Лепшыя выканаўцы
+ Most played albums
+
+
+ Пошук
+ Пошук у YouTube Music…
+ Пошук у бібліятэцы…
+ Усе
+ Песні
+ Відэа
+ Альбомы
+ Выканаўцы
+ Плэй-лісты
+ Плэй-лісты aд cупольнасці
+ Вартыя ўвагі плэй-лісты
+ Bookmarked
+ Вынікі не знойдзены
+
+
+ З вашай бібліятэкі
+
+
+ Упадабаныя кампазіцыi
+ Спампаваныя кампазіцыі
+ Плай-ліст пусты
+
+
+ Паўтарыць
+ Радыё
+ Перамяшаць
+
+
+ Падрабязнасці
+ Змяніць
+ Уключыць радыё
+ Прайграць
+ Прайграць наступным
+ Дадаць у чаргу
+ Дадаць у бібліятэку
+ Выдаліць з бібліятэкі
+ Спампаваць
+ Спампоўка
+ Выдаліць са спамповак
+ Імпартаваць плей-ліст
+ Дадаць у плэй-ліст
+ Перайсці да выканаўца
+ Перайсці да альбома
+ Абнавіць
+ Абагуліць
+ Выдаліць
+ Выдаліць з гісторыі
+ Шукаць у сетцы
+ Cінхранізацыя
+
+
+ Нядаўна дададзена
+ Назва
+ Выканаўца
+ Год
+ Колькасць песен
+ Працягласць
+ Колькасць прайграванняў
+ Уласны парадак
+
+
+ Iдэнтыфікатар медыя
+ Тып MIME
+ Кодэкі
+ Хуткасць
+ Частата дыскрэтызацыі
+ Гучнасць
+ Узровень гучнасці
+ Памер файла
+ Невядомы
+ Скапіявана
+
+ Змяниць тэкст песні
+ Пошук тэкста песні
+
+ Змяніць песню
+ Назва песні
+ Выканаўца песні
+ Песня павінна мець назву.
+ Песня павінна мець выканаўца.
+ Захаваць
+
+ Выбраць плэй-ліст
+ Змяніць плей-ліст
+ Стварыць плей-ліст
+ Назва плей-ліста
+ Плей-ліст павінен мець назву.
+
+ Змяніць выканайца
+ Імя ваканаўца
+ Выканаўца павінен мець імя.
+
+
+
+ - %d песня
+ - %d песні
+ - %d песен
+ - %d песні
+
+
+ - %d выканаўца
+ - %d выканаўцы
+ - %d выканаўцаў
+ - %d выканаўцы
+
+
+ - %d альбом
+ - %d альбомы
+ - %d альбомаў
+ - %d альбомы
+
+
+ - %d плей-ліст
+ - %d плей-ліста
+ - %d плей-лістоў
+ - %d плей-ліста
+
+
+ - %d тыдзень
+ - %d тыдні
+ - %d тыдняў
+ - %d тыдні
+
+
+ - %d месяц
+ - %d месяцы
+ - %d месяцаў
+ - %d месяцы
+
+
+ - %d год
+ - %d гады
+ - %d raдоў
+ - %d гады
+
+
+
+ Плей-ліст імпартаваны
+ \"%s\" выдалена з плей-ліста
+ Плей-ліст сінхранізаваны
+ Адрабіць
+
+
+ Тэкст песні не знойдзены
+ Таймер рэжыму сну
+ Канец песні
+
+ - 1 хвіліна
+ - %d хвіліны
+ - %d хвілін
+ - %d хвілін
+
+ Няма даступных плыняў
+ Няма падлучэння да сеткі
+ Час чакання
+ Невядомая памылка
+
+
+ Упадабаць
+ Выдаліць з упадабаных
+
+
+ Усе песні
+ Шуканыя кампазіцыі
+
+
+ Музычны прайгравальнік
+
+
+ Налады
+ Выгляд
+ Уключыць дынамічную каляровую тэму
+ Цёмная каляровая тэма
+ Укл.
+ Выкл.
+ Прытрымлівацца сістэмнай каляровай тэмы
+ Рэжым чыстага чорнага колеру
+ Прадвызначаная укладка
+ Дапасаваць укладак
+ Пазіцыя тэкста песні
+ Па левым краі
+ Па цэнтры
+ Па правым краі
+
+ Змесціва
+ Лагін
+ Мова змесціва
+ Краіна змесціва
+ Прытрымлівацца сістэмнай
+ Уключыць проксі
+ Тып проксі
+ URL проксі
+ Перазапусціць каб ужыць змяненні
+
+ Прайгравальнік ды аўдыя
+ Якасць аўдыя
+ Аўта
+ Высокая
+ Нізкая
+ Сталая чарга
+ Прапусціць цішыню
+ Нармалізацыя аўдыя
+ Эквалайзер
+
+ Сховішча
+ Кеш
+ Кеш выяў
+ Кеш песен
+ Максімальны памер кэшу
+ Безліміт
+ Ачысціць усе спампоўкі
+ Максімальны памер кэшу выяў
+ Ачысціць кэш выяў
+ Максімальны памер кэшу песен
+ Ачысціць кэш песен
+ %s выкарыстоўваецца
+
+ Прыватнасць
+ Прыпыніць гісторыю праслухоўвання
+ Ачысціць гісторыю праслухоўвання
+ Сапраўды ачысціць гісторыю праслухоўвання?
+ Прыпыніць гісторыю пошуку
+ Ачысціць гісторыю пошуку
+ Сапраўды ачысціць гісторыю пошуку?
+ Шукаць тэксты песен у KuGou
+
+ Рэзервовае капіраванне ды аднеўленне
+ Рэзервовае капіраванне
+ Аднаўленне з рэзервовай копіі
+ Імпартаваны плей-ліст
+ Рэзервовная копія паспяхова створана
+ Немагчыма стапрыць рэзервовую копію
+ Немагчыма ужывіць рэзервовую копію
+
+ Аб праграме
+ Версія праграмы
+
+ New version available
+
diff --git a/app/src/main/res/values-bn-rIN/strings.xml b/app/src/main/res/values-bn-rIN/strings.xml
new file mode 100644
index 000000000..9088e7ba1
--- /dev/null
+++ b/app/src/main/res/values-bn-rIN/strings.xml
@@ -0,0 +1,258 @@
+
+
+ হোম
+ গান
+ শিল্পী
+ অ্যালবাম
+ প্লেলিস্ট
+
+
+
+ - %d নির্বাচিত
+ - %d নির্বাচিত
+
+
+
+ ইতিহাস
+ পরিসংখ্যান
+ মেজাজ এবং শৈলী
+ অ্যাকাউন্ট
+ দ্রুত পছন্দ
+ আপনার নিজের শর্টকাট তৈরি করতে কিছু টিউন শুনুন
+ নতুন অ্যালবাম ও সিঙ্গেল
+
+
+ আজ
+ গতকাল
+ এই সপ্তাহ
+ গত সপ্তাহ
+
+
+ সর্বাধিক শোনা গানগুলি
+ সর্বাধিক শোনা শিল্পী
+ সর্বাধিক শোনা অ্যালবাম
+
+
+ খুঁজুন
+ খুঁজুন YouTube Music এ
+ খুঁজুন লাইব্রেরি তে
+ সব
+ সংগীত
+ ভিডিও
+ অ্যালবাম
+ শিল্পী
+ প্লেলিস্ট
+ প্লেলিস্ট ক্রম
+ বিশিষ্ট প্লেলিস্ট
+ Bookmarked
+ কিছু পাওয়া যায়নি
+
+
+ আপনার লাইব্রেরি থেকে
+
+
+ আপনার পছন্দের গান
+ ডাউনলোড করা গান
+ প্লেলিস্ট খালি
+
+
+ পুনরায় চেষ্টা করুন
+ বেতার
+ এলোমেলো
+
+
+ বিস্তারিত
+ সম্পাদনা
+ বেতার খুলুন
+ গান বাজান
+ পরবর্তী গান বাজান
+ পরবর্তী গান যোগ করুন
+ লাইব্রেরি তে যোগ করুন
+ লাইব্রেরি থেকে সরান
+ ডাউনলোড
+ ডাউনলোড হচ্ছে
+ ডাউনলোড মুছে ফেলুন
+ প্লেলিস্ট আমদানি করুন
+ প্লেলিস্টে যোগ করুন
+ শিল্পী দেখুন
+ অ্যালবাম দেখুন
+ আবার আনুন
+ শেয়ার
+ মুছুন
+ ইতিহাস থেকে অপসারণ
+ অনলাইন এ খুঁজুন
+ সুসংগত
+
+
+ সময় সম্পাদনা করা হয়েছে
+ নাম
+ শিল্পী
+ বছর
+ প্লে সংখ্যান
+ দৈর্ঘ্য
+ বাজানো হয়েছে
+ অনুকুলিত
+
+
+ Media ID
+ MIME Type
+ Codec
+ Bitrate
+ Sample Rate
+ শব্দের মাত্রা
+ ধ্বনির মাত্রা
+ ফাইলের আকার
+ অজানা
+ ক্লিপবোর্ডে কপি করা হয়েছে
+
+ গানের কথা সম্পাদনা
+ গানের কথা অনুসন্ধান
+
+ গান সম্পাদনা করুন
+ শিরোনাম
+ শিল্পী
+ গানের শিরোনাম খালি হতে পারে না।
+ গানের শিল্পী খালি থাকতে পারে না।
+ সংরক্ষণ
+
+ একটি প্লেলিস্ট চয়ন করুন
+ প্লেলিস্ট সম্পাদনা করুন
+ প্লেলিস্ট তৈরি করুন
+ প্লেলিস্টের নাম
+ প্লেলিস্টের নাম খালি রাখা যাবে না।
+
+ শিল্পী সম্পাদনা করুন
+ শিল্পীর নাম
+ শিল্পীর নাম খালি রাখা যাবে না।
+
+
+
+ - %d গান
+ - %d গান
+
+
+ - %d শিল্পী
+ - %d শিল্পী
+
+
+ - %d অ্যালবাম
+ - %d অ্যালবাম
+
+
+ - %d প্লেলিস্ট
+ - %d প্লেলিস্ট
+
+
+ - %d সপ্তাহ
+ - %d সপ্তাহ
+
+
+ - %d মাস
+ - %d মাস
+
+
+ - %d বছর
+ - %d বছর
+
+
+
+ আমদানি করা প্লেলিস্ট
+ প্লেলিস্ট থেকে \"%s\" সরানো হয়েছে
+ সিঙ্ক্রোনাইজ করা প্লেলিস্ট
+ বাতিল করুন
+
+
+ গানের কথা পাওয়া যায়নি
+ ঘুমের টাইমার
+ গানের শেষ
+
+ - %d মিনিট
+ - %d মিনিট
+
+ স্ট্রিম উপলব্ধ নয়
+ নেটওয়ার্ক সংযোগ নেই
+ ত্রুটি সময়সীমা শেষ
+ অজানা ত্রুটি
+
+
+ পছন্দ
+ অপছন্দ
+
+
+ সব গান
+ অনুসন্ধান করা গান
+
+
+ প্লেয়ার
+
+
+ সেটিংস
+ দৃষ্টিগোচরতা
+ গতিশীল থিম সক্ষম করুন
+ অন্ধকার থিম
+ সক্রিয়
+ নিষ্ক্রিয়
+ সিস্টেম অনুসরণ করুন
+ কালো
+ ডিফল্ট প্রধান ট্যাব
+ নেভিগেশন ট্যাব কাস্টমাইজ করুন
+ গানের কথার অবস্থান
+ বাম
+ কেন্দ্র
+ ডান
+
+ কনটেন্ট
+ সাইন ইন
+ ডিফল্ট কন্টেন্টের ভাষা
+ ডিফল্ট কনটেন্ট দেশ
+ সিস্টেমের ডিফল্ট
+ প্রক্সি সক্রিয় করুন
+ প্রক্সি ধরণ
+ প্রক্সি URL
+ পরিবর্তনগুলি প্রয়োগ করতে অ্যাপটি পুনরায় চালু করুন
+
+ প্লেয়ার এবং অডিও
+ অডিওর মান
+ স্বয়ংক্রিয়
+ উচ্চ
+ নিম্ন
+ অবিরাম সারি
+ নীরবতা এড়িয়ে যান
+ অডিও স্বাভাবিকীকরণ
+ অডিও টিউনার
+
+ স্টোরেজ
+ ক্যাশে
+ ইমেজ ক্যাশে
+ অডিও ক্যাশে
+ সর্বাধিক ক্যাশে আকার
+ সীমাহীন
+ সমস্ত ডাউনলোড মুছুন
+ ইমেজ ক্যাশে সর্বাধিক আকার
+ ইমেজ ক্যাশে সাফ করুন
+ অডিও ক্যাশে সর্বোচ্চ আকার
+ অডিও ক্যাশে সাফ করুন
+ %s ব্যবহৃত
+
+ গোপনীয়তা
+ আপনার শোনার ইতিহাস থামান
+ আপনার শোনার ইতিহাস সাফ করুন
+ আপনি কি আপনার শোনার ইতিহাস মুছে ফেলার বিষয়ে নিশ্চিত?
+ আপনার অনুসন্ধান ইতিহাস স্থগিত
+ আপনার অনুসন্ধান ইতিহাস সাফ করুন
+ আপনি কি আপনার অনুসন্ধান ইতিহাস মুছে ফেলার বিষয়ে নিশ্চিত?
+ KuGou দ্বারা প্রদত্ত গানের কথা সক্রিয় করুন
+
+ ব্যাকআপ এবং পুনঃস্থাপন
+ ব্যাকআপ
+ পুনঃস্থাপন
+ আমদানি করা প্লেলিস্ট
+ ব্যাকআপ সফলভাবে তৈরি করা হয়েছে
+ ব্যাকআপ করতে অক্ষম
+ ব্যাকআপ থেকে পুনরুদ্ধার করতে অক্ষম
+
+ সম্পর্কিত
+ অ্যাপ সংস্করণ
+
+ New version available
+
diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml
index 9cc60ac2d..a7642851e 100644
--- a/app/src/main/res/values-cs/strings.xml
+++ b/app/src/main/res/values-cs/strings.xml
@@ -17,8 +17,10 @@
History
Stats
+ Mood and Genres
+ Account
Quick picks
- Listen some songs to generate your quick picks
+ Listen to songs to generate your quick picks
New release albums
@@ -30,6 +32,7 @@
Most played songs
Most played artists
+ Most played albums
Vyhledávání
@@ -43,6 +46,7 @@
Playlisty
Komunitní playlisty
Doporučené playlisty
+ Bookmarked
No results found
@@ -148,6 +152,18 @@
- %d playlistů
- %d playlistů
+
+ - %d week
+ - %d weeks
+
+
+ - %d month
+ - %d months
+
+
+ - %d year
+ - %d years
+
Playlist importován
@@ -163,6 +179,7 @@
- 1 minute
- %d minutes
- %d minutes
+ - %d minutes
Není dostupný žádný stream
Není dostupné připojení k internetu
@@ -241,16 +258,13 @@
Záloha a obnovení
Zálohovat
Obnovit
- Choose a csv file from Google Takeout
Imported playlist
-
- - Imported \"%s\" with %d song
- - Imported \"%s\" with %d songs
-
Záloha úspěšně vytvořena
Nepodařilo se vytvořit zálohu
Nepodařilo se obnovit zálohu
O aplikaci
Verze aplikace
+
+ New version available
diff --git a/app/src/main/res/values-es-rUS/strings.xml b/app/src/main/res/values-es-rUS/strings.xml
deleted file mode 100644
index e9991abd7..000000000
--- a/app/src/main/res/values-es-rUS/strings.xml
+++ /dev/null
@@ -1,245 +0,0 @@
-
-
- Home
- Canciones
- Artistas
- Albums
- Playlists
-
-
-
- - %d seleccionada
- - %d seleccionadas
-
-
-
- History
- Stats
- Quick picks
- Listen some songs to generate your quick picks
- New release albums
-
-
- Today
- Yesterday
- This week
- Last week
-
-
- Most played songs
- Most played artists
-
-
- Buscar
- Search YouTube Music…
- Search library…
- Todo
- Canciones
- Vídeos
- Álbumes
- Artistas
- Playlists
- Community playlists
- Featured playlists
- No results found
-
-
- From your library
-
-
- Liked songs
- Downloaded songs
- The playlist is empty
-
-
- Reintentar
- Radio
- Aleatorio
-
-
- Details
- Editar
- Start radio
- Play
- Reproducir luego
- Añadir a la cola
- Añadir a la biblioteca
- Remove from library
- Descargar
- Downloading
- Remover archivo descargado
- Import playlist
- Añadir a una playlist
- View artist
- View album
- Refetch
- Share
- Eliminar
- Remove from history
- Search online
- Sync
-
-
- Fecha de incorporación
- Nombre
- Artista
- Year
- Song count
- Length
- Play time
- Custom order
-
-
- Media id
- MIME type
- Codecs
- Bitrate
- Sample rate
- Loudness
- Volume
- File size
- Unknown
- Copied to clipboard
-
- Edit lyrics
- Search lyrics
-
- Editar canción
- Título
- Artista
- El título no puede estar vacío.
- El artista no puede estar vacío.
- Guardar
-
- Elige una playlist
- Editar playlist
- Crear playlist
- Nombre
- El nombre no puede estar vacío.
-
- Editar artista
- Nombre
- El nombre no puede estar vacío.
-
-
-
- - %d canción
- - %d canciones
-
-
- - %d artist
- - %d artists
-
-
- - %d album
- - %d albums
-
-
- - %d playlist
- - %d playlists
-
-
-
- Playlist imported
- Removed \"%s\" from playlist
- Playlist synced
- Undo
-
-
- Lyrics not found
- Sleep timer
- End of song
-
- - 1 minute
- - %d minutes
-
- No stream available
- No network connection
- Timeout
- Unknown error
-
-
- Like
- Remove like
-
-
- All songs
- Searched songs
-
-
- Reproductor
-
-
- Configuración
- Apariencia
- Enable dynamic theme
- Modo oscuro
- Encendido
- Apagado
- Predeterminado del sistema
- Pure black
- Default open tab
- Customize navigation tabs
- Lyrics text position
- Left
- Center
- Right
-
- Contenido
- Login
- Idioma por defecto del contenido
- País por defecto del contenido
- Predeterminado del sistema
- Enable proxy
- Proxy type
- Proxy URL
- Restart to take effect
-
- Player and audio
- Audio quality
- Auto
- High
- Low
- Persistent queue
- Skip silence
- Audio normalization
- Equalizer
-
- Storage
- 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
-
- 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
-
- Backup and restore
- Backup
- Restore
- Choose a csv file from Google Takeout
- Imported playlist
-
- - Imported \"%s\" with %d song
- - Imported \"%s\" with %d songs
-
- Backup created successfully
- Couldn\'t create backup
- Failed to restore backup
-
- Acerca de
- Versión de la aplicación
-
diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml
index b493d8f27..90924cfd5 100644
--- a/app/src/main/res/values-es/strings.xml
+++ b/app/src/main/res/values-es/strings.xml
@@ -1,4 +1,4 @@
-
+
Inicio
Canciones
@@ -9,14 +9,17 @@
- %d seleccionada
+ - %d seleccionadas
- %d seleccionadas
Historial
Estadísticas
+ Ánimo y géneros
+ Cuenta
Selecciones rápidas
- Listen some songs to generate your quick picks
+ Escucha algunas canciones para generar tus selecciones rápidas
Nuevos lanzamientos
@@ -27,12 +30,13 @@
Canciones más reproducidas
- Most played artists
+ Artistas más reproducidos
+ Álbumes más reproducidos
Buscar
Buscar en Youtube Music…
- Buscar en biblioteca…
+ Buscar en la biblioteca…
Todo
Canciones
Vídeos
@@ -41,6 +45,7 @@
Listas de reproducción
Listas de la comunidad
Listas destacadas
+ En marcadores
No se han encontrado resultados
@@ -66,7 +71,7 @@
Agregar a la biblioteca
Quitar de la biblioteca
Descargar
- Downloading
+ Descargando
Borrar descarga
Importar lista de reproducción
Agregar a la lista de reproducción
@@ -87,7 +92,7 @@
Número de canciones
Duración
Tiempo de reproducción
- Custom order
+ Orden personalizado
ID multimedia
@@ -124,26 +129,45 @@
- %d canción
+ - %d canciones
- %d canciones
- %d artista
+ - %d artistas
- %d artistas
- %d álbum
+ - %d álbumes
- %d álbumes
- %d lista de reproducción
+ - %d listas de reproducción
- %d listas de reproducción
+
+ - %d semana
+ - %d semanas
+ - %d semanas
+
+
+ - %d mes
+ - %d meses
+ - %d meses
+
+
+ - %d año
+ - %d años
+ - %d años
+
Lista de reproducción importada
- Removed \"%s\" from playlist
- Playlist synced
- Undo
+ Eliminado \"%s\" de la lista de reproducción
+ Lista de reproducción sincronizada
+ Deshacer
Letras no encontradas
@@ -151,6 +175,7 @@
Al finalizar la canción
- 1 minuto
+ - %d minutos
- %d minutos
No hay un stream disponible
@@ -211,7 +236,7 @@
Caché de canciones
Tamaño máximo de la caché
Ilimitado
- Clear all downloads
+ Borrar todas las descargas
Tamaño máximo de la caché de imágenes
Borrar la caché de imágenes
Tamaño máximo de la caché de canciones
@@ -219,9 +244,9 @@
%s usado
Privacidad
- Pausar historial de escucha
- Clear listen history
- Are you sure to clear all listen history?
+ Pausar historial de escuchas
+ Borrar historial de escuchas
+ ¿Estás seguro de que quieres borrar todo el historial de escuchas?
Pausar historial de búsquedas
Borrar historial de búsquedas
¿Estás seguro de que quieres borrar todo el historial de búsquedas?
@@ -230,16 +255,13 @@
Copias de seguridad y restauración
Hacer copia de seguridad
Restaurar
- Choose a csv file from Google Takeout
- Imported playlist
-
- - Imported \"%s\" with %d song
- - Imported \"%s\" with %d songs
-
+ Lista de reproducción importada
Copia de seguridad creada con éxito
No se ha podido crear la copia de seguridad
Error al restaurar la copia de seguridad
Acerca de
Versión de la app
+
+ New version available
diff --git a/app/src/main/res/values-fa-rIR/strings.xml b/app/src/main/res/values-fa-rIR/strings.xml
index 0dca69e70..152449763 100644
--- a/app/src/main/res/values-fa-rIR/strings.xml
+++ b/app/src/main/res/values-fa-rIR/strings.xml
@@ -15,8 +15,10 @@
History
Stats
+ Mood and Genres
+ Account
Quick picks
- Listen some songs to generate your quick picks
+ Listen to songs to generate your quick picks
New release albums
@@ -28,6 +30,7 @@
Most played songs
Most played artists
+ Most played albums
جستجو
@@ -41,6 +44,7 @@
لیستپخشها
لیستپخشهای انجمن
لیستپخشهای ویژه
+ Bookmarked
No results found
@@ -138,6 +142,18 @@
- %d لیستپخش
- %d لیستپخشها
+
+ - %d week
+ - %d weeks
+
+
+ - %d month
+ - %d months
+
+
+ - %d year
+ - %d years
+
لیستپخش واردشد
@@ -230,16 +246,13 @@
پشتیبانگیری و بازگردانی
پشتیبانگیری
بازگردانی
- Choose a csv file from Google Takeout
Imported playlist
-
- - Imported \"%s\" with %d song
- - Imported \"%s\" with %d songs
-
پشتیبان باموفقیت ایجادشد
پشتیبان ایجاد نشد
بازیابی پشتیبان انجامنشد
درباره
نسخهی برنامه
+
+ New version available
diff --git a/app/src/main/res/values-fi-rFI/strings.xml b/app/src/main/res/values-fi-rFI/strings.xml
index 62e6deafe..2e830364e 100644
--- a/app/src/main/res/values-fi-rFI/strings.xml
+++ b/app/src/main/res/values-fi-rFI/strings.xml
@@ -15,8 +15,10 @@
History
Stats
+ Mood and Genres
+ Account
Quick picks
- Listen some songs to generate your quick picks
+ Listen to songs to generate your quick picks
New release albums
@@ -28,6 +30,7 @@
Most played songs
Most played artists
+ Most played albums
Etsi
@@ -41,6 +44,7 @@
Soittolistat
Community playlists
Featured playlists
+ Bookmarked
No results found
@@ -138,6 +142,18 @@
- %d playlist
- %d playlists
+
+ - %d week
+ - %d weeks
+
+
+ - %d month
+ - %d months
+
+
+ - %d year
+ - %d years
+
Playlist imported
@@ -230,16 +246,13 @@
Backup and restore
Backup
Restore
- Choose a csv file from Google Takeout
Imported playlist
-
- - Imported \"%s\" with %d song
- - Imported \"%s\" with %d songs
-
Backup created successfully
Couldn\'t create backup
Failed to restore backup
Tietoa
Sovelluksen versio
+
+ New version available
diff --git a/app/src/main/res/values-fr-rFR/strings.xml b/app/src/main/res/values-fr-rFR/strings.xml
index 7df9f1acf..342970be2 100644
--- a/app/src/main/res/values-fr-rFR/strings.xml
+++ b/app/src/main/res/values-fr-rFR/strings.xml
@@ -16,8 +16,10 @@
History
Stats
+ Mood and Genres
+ Account
Quick picks
- Listen some songs to generate your quick picks
+ Listen to songs to generate your quick picks
New release albums
@@ -29,6 +31,7 @@
Most played songs
Most played artists
+ Most played albums
Recherche
@@ -42,6 +45,7 @@
Listes de lecture
Community playlists
Featured playlists
+ Bookmarked
No results found
@@ -143,6 +147,18 @@
- %d playlists
- %d albums
+
+ - %d week
+ - %d weeks
+
+
+ - %d month
+ - %d months
+
+
+ - %d year
+ - %d years
+
Playlist imported
@@ -236,16 +252,13 @@
Backup and restore
Backup
Restore
- Choose a csv file from Google Takeout
Imported playlist
-
- - Imported \"%s\" with %d song
- - Imported \"%s\" with %d songs
-
Backup created successfully
Couldn\'t create backup
Failed to restore backup
À propos
Version de l\'application
+
+ New version available
diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml
index fd519f52c..cc35c1078 100644
--- a/app/src/main/res/values-hu/strings.xml
+++ b/app/src/main/res/values-hu/strings.xml
@@ -13,21 +13,24 @@
- History
- Stats
- Quick picks
- Listen some songs to generate your quick picks
- New release albums
+ Előzmény
+ Statisztika
+ Mood and Genres
+ Account
+ Gyors választás
+ Hallgasson meg néhány dalt a gyors választások elkészítéséhez
+ Új kiadású albumok
- Today
- Yesterday
- This week
- Last week
+ Ma
+ Tegnap
+ A héten
+ Múlt héten
- Most played songs
- Most played artists
+ A legtöbbet játszott dalok
+ A legtöbbet játszott előadók
+ Most played albums
Keresés
@@ -41,7 +44,8 @@
Listák
Közösségi listák
Kiemelt lejátszási listák
- No results found
+ Bookmarked
+ Nincs találat
Saját könyvtárból
@@ -49,7 +53,7 @@
Lájkolt dalok
Letöltött dalok
- The playlist is empty
+ A lejátszólista üres
Újra
@@ -66,7 +70,7 @@
Könyvtárhoz ad
Eltávolít a könyvtárból
Letöltés
- Downloading
+ Letöltés
Letöltés eltávolítása
Lista importálása
Lejátszólistához ad
@@ -75,9 +79,9 @@
Újrahív
Megosztás
Törlés
- Remove from history
+ Előzményből eltávolít
Keresés online
- Sync
+ Szinkron.
Létrehozás dátuma
@@ -87,7 +91,7 @@
Dal szám
Hossz
Lejátszási idő
- Custom order
+ Egyéni sorrend
Média id
@@ -138,20 +142,32 @@
- %d lejátszólista
- %d lejátszólista
+
+ - %d week
+ - %d weeks
+
+
+ - %d month
+ - %d months
+
+
+ - %d year
+ - %d years
+
Lista importálva
- Removed \"%s\" from playlist
- Playlist synced
- Undo
+ \"%s\" eltávolítva a lejátszólistából
+ Lejátszólista szinkronban
+ Visszavon
A dalszöveg nem található
- Sleep timer
- End of song
+ Alvás időzítő
+ A dal vége
- - 1 minute
- - %d minutes
+ - 1 perc
+ - %d perc
Nincs elérhető adatfolyam
Nincs hálózati kapcsolat
@@ -172,12 +188,12 @@
Beállítások
Megjelenés
- Enable dynamic theme
+ Dinamikus téma bekapcsolás
Sötét téma
Be
Ki
Rendszer szerint
- Pure black
+ Tiszta fekete
Alapért. nyitott lap
A navigációs lapok testreszabása
Dalszöveg szöveg pozíció
@@ -207,21 +223,21 @@
Tárhely
Gyorsítótár
- Image Cache
- Song Cache
- Max cache size
- Unlimited
- Clear all downloads
+ Kép gyorsítótár
+ Dal gyorsítótár
+ Max. gyors.tár méret
+ Korlátlan
+ Minden letöltés törlése
Max. kép gyorsítótár
Kép gyors.tár törlés
Max. dal gyorsítótár
- Clear song cache
+ Dal gyors.tár törlése
%s használva
Adatvédelem
- Pause listen history
- Clear listen history
- Are you sure to clear all listen history?
+ A hallgatási előzmények szüneteltetése
+ Hallgatási előzmény törlése
+ Biztosan törli az összes hallgatási előzményt?
Keresési előzmények szüneteltetése
Keresési előzmények törlése
Biztosan töröl minden keresési előzményt?
@@ -230,16 +246,13 @@
Bizt.mentés és visszaállítás
Bizt.mentés
Visszaállítás
- Choose a csv file from Google Takeout
- Imported playlist
-
- - Imported \"%s\" with %d song
- - Imported \"%s\" with %d songs
-
+ Importált lejátszási lista
A bizt.mentés sikeresen létrehozva
Nem sikerült biztonsági mentést készíteni
Nem sikerült visszaállítani a biztonsági másolatot
Rólunk
App verzió
+
+ New version available
diff --git a/app/src/main/res/values-id/strings.xml b/app/src/main/res/values-id/strings.xml
index dba0e8fcb..95bede859 100644
--- a/app/src/main/res/values-id/strings.xml
+++ b/app/src/main/res/values-id/strings.xml
@@ -1,4 +1,4 @@
-
+
Beranda
Lagu
@@ -14,8 +14,10 @@
History
Stats
+ Mood and Genres
+ Account
Quick picks
- Listen some songs to generate your quick picks
+ Listen to songs to generate your quick picks
New release albums
@@ -27,6 +29,7 @@
Most played songs
Most played artists
+ Most played albums
Caru
@@ -40,6 +43,7 @@
Daftar putar
Daftar putar komunitas
Daftar putar unggulan
+ Bookmarked
No results found
@@ -133,6 +137,15 @@
- %d daftar putar
+
+ - %d weeks
+
+
+ - %d months
+
+
+ - %d years
+
Daftar putar dipindahkan
@@ -224,16 +237,13 @@
Cadangkan dan pulihkan
Cadangkan
Pulihkan
- Choose a csv file from Google Takeout
Imported playlist
-
- - Imported \"%s\" with %d song
- - Imported \"%s\" with %d songs
-
Cadangan berhasil dibuat
Tidak dapat membuat cadangan
Gagal untuk memulihkan cadangan
Tentang
Versi aplikasi
+
+ New version available
diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml
index cc1935e48..b82d543f7 100644
--- a/app/src/main/res/values-it/strings.xml
+++ b/app/src/main/res/values-it/strings.xml
@@ -1,4 +1,4 @@
-
+
Home
Brani
@@ -10,24 +10,28 @@
- %d selezionato
- %d selezionati
+ - %d selezionati
- History
- Stats
- Quick picks
- Listen some songs to generate your quick picks
- New release albums
+ Cronologia
+ Statistiche
+ Mood e generi
+ Account
+ Scelte rapide
+ Ascolta alcuni brani per generare le tue scelte rapide
+ Nuovi album in uscita
- Today
- Yesterday
- This week
- Last week
+ Oggi
+ Ieri
+ Questa settimana
+ Settimana scorsa
- Most played songs
- Most played artists
+ Brani più ascoltati
+ Artisti più ascoltati
+ Album più ascoltati
Cerca
@@ -39,9 +43,10 @@
Album
Artisti
Playlist
- Playlist della comunità
+ Playlist della community
Playlist in rilievo
- No results found
+ Bookmarked
+ Nessun risultato trovato
Dalla tua libreria
@@ -49,7 +54,7 @@
Brani piaciuti
Brani scaricati
- The playlist is empty
+ La playlist è vuota
Riprova
@@ -59,25 +64,25 @@
Dettagli
Modifica
- Apri la radio
+ Apri radio
Riproduci
Riproduci come successiva
- Aggiungi alla coda
- Aggiungi alla libreria
- Rimuovi dalla libreria
+ Metti in coda
+ Aggiungi a libreria
+ Rimuovi da libreria
Scarica
- Downloading
+ In scaricamento
Rimuovi scaricamento
Importa playlist
- Aggiungi alla playlist
+ Aggiungi a playlist
Mostra artista
Mostra album
- Riottieni
+ Aggiorna
Condividi
Elimina
- Remove from history
+ Rimuovi da cronologia
Cerca online
- Sync
+ Sincronizza
Data di aggiunta
@@ -87,7 +92,7 @@
Numero brani
Durata
Numero di riproduzioni
- Custom order
+ Ordine personalizzato
ID media
@@ -105,8 +110,8 @@
Cerca testo
Modifica brano
- Titolo
- Artista
+ Titolo del brano
+ Artisti del brano
Il titolo del brano non può essere vuoto.
L\'artista del brano non può essere vuoto.
Salva
@@ -125,42 +130,62 @@
- %d brano
- %d brani
+ - %d brani
- %d artista
- %d artisti
+ - %d artisti
- %d album
- %d album
+ - %d album
- %d playlist
- %d playlist
+ - %d playlist
+
+
+ - %d settimana
+ - %d settimane
+ - %d settimane
+
+
+ - %d mese
+ - %d mesi
+ - %d mesi
+
+
+ - %d anno
+ - %d anni
+ - %d anni
Playlist importata
- Removed \"%s\" from playlist
- Playlist synced
- Undo
+ Rimosso \"%s\" da playlist
+ Playlist sincronizzata
+ Annulla
Testo non trovato
- Sleep timer
- End of song
+ Timer per il sonno
+ Fine del brano
- - 1 minute
- - %d minutes
+ - 1 minuto
+ - %d minuti
+ - %d minuti
Stream non disponibile
- Nessuna connessione a internet presente
+ Nessuna connessione di rete
Tempo scaduto
Errore sconosciuto
Mi piace
- Non mi piace più
+ Rimuovi mi piace
Tutti i brani
@@ -172,14 +197,14 @@
Impostazioni
Aspetto
- Enable dynamic theme
+ Abilita il tema dinamico
Tema scuro
Attivato
Disattivato
Segui sistema
- Pure black
+ Nero
Scheda principale predefinita
- Personalizza schede di navigazione
+ Personalizza le schede di navigazione
Posizione del testo dei brani
Sinistra
Centro
@@ -191,7 +216,7 @@
Paese predefinito dei contenuti
Predefinito di sistema
Attiva proxy
- Tipo di proxy
+ Tipologia del proxy
URL proxy
Riavvia l\'app per applicare le modifiche
@@ -201,45 +226,42 @@
Alta
Bassa
Coda persistente
- Salta silenzio
+ Salta il silenzio
Normalizzazione dell\'audio
Equalizzatore
Archiviazione
Cache
- Image Cache
- Song Cache
- Max cache size
- Unlimited
- Clear all downloads
+ Cache delle immagini
+ Cache dei brani
+ Dimensione massima della cache
+ Illimitata
+ Cancella tutti i download
Grandezza massima della cache delle immagini
- Pulisci cache delle immagini
+ Pulisci la cache delle immagini
Grandezza massima della cache dei brani
- Clear song cache
- %s usato
+ Pulisci la cache dei brani
+ %s usati
Privacy
- Pause listen history
- Clear listen history
- Are you sure to clear all listen history?
+ Sospendi la cronologia degli ascolti
+ Cancella la cronologia degli ascolti
+ Sei sicuro di voler cancellare la cronologia degli ascolti?
Sospendi la cronologia delle ricerche
Pulisci la cronologia delle ricerche
Sei sicuro di voler cancellare la cronologia delle ricerche?
Attiva i testi forniti da KuGou
- Backup e ripristina
+ Backup e ripristino
Backup
Ripristina
- Choose a csv file from Google Takeout
- Imported playlist
-
- - Imported \"%s\" with %d song
- - Imported \"%s\" with %d songs
-
+ Playlist importata
Backup creato con successo
- Impossibile fare il backup
+ Impossibile eseguire il backup
Impossibile eseguire il ripristino dal backup
Informazioni
Versione dell\'app
+
+ New version available
diff --git a/app/src/main/res/values-ja-rJP/strings.xml b/app/src/main/res/values-ja-rJP/strings.xml
index 9b0815e4e..f95b6b252 100644
--- a/app/src/main/res/values-ja-rJP/strings.xml
+++ b/app/src/main/res/values-ja-rJP/strings.xml
@@ -1,4 +1,4 @@
-
+
ホーム
曲
@@ -12,21 +12,24 @@
- History
- Stats
- Quick picks
- Listen some songs to generate your quick picks
- New release albums
+ 履歴
+ 統計
+ ムードとジャンル
+ アカウント
+ おすすめ
+ 何曲か再生するとおすすめを生成します
+ 新作アルバム
- Today
- Yesterday
- This week
- Last week
+ 今日
+ 昨日
+ 今週
+ 先週
- Most played songs
- Most played artists
+ 最も再生された曲
+ 最も再生されたアーティスト
+ 最も再生されたアルバム
検索
@@ -39,8 +42,9 @@
アーティスト
プレイリスト
コミュニティのプレイリスト
- 注目のプレイリスト
- No results found
+ おすすめのプレイリスト
+ Bookmarked
+ 見つかりませんでした
ライブラリから
@@ -48,7 +52,7 @@
いいねした曲
ダウンロードした曲
- The playlist is empty
+ プレイリストが空です
再試行
@@ -56,7 +60,7 @@
シャッフル
- Details
+ 詳細
編集
ラジオを再生
再生
@@ -65,18 +69,18 @@
ライブラリに追加
ライブラリから削除
ダウンロード
- Downloading
- ダウンロードを削除
+ ダウンロード中
+ ダウンロード削除
プレイリストをインポート
プレイリストに追加
- アーティストを見る
- アルバムを見る
+ アーティスト表示
+ アルバムを表示
再取得
共有
削除
- Remove from history
+ 履歴から削除
オンラインで検索
- Sync
+ 同期
追加日時
@@ -86,7 +90,7 @@
曲数
長さ
再生時間
- Custom order
+ カスタム
メディアID
@@ -95,7 +99,7 @@
ビットレート
サンプリングレート
ラウドネス
- ボリューム
+ 音量
ファイルサイズ
不明
クリップボードにコピー
@@ -106,8 +110,8 @@
曲を編集
曲名
曲のアーティスト
- 曲名は空白にできません。
- 曲のアーティストは空白にできません。
+ 曲名は空にできません。
+ 曲のアーティストは空にできません。
保存
プレイリストを選択
@@ -133,24 +137,33 @@
- %d プレイリスト
+
+ - %d週間
+
+
+ - %dか月
+
+
+ - %d年
+
プレイリストをインポートしました
- Removed \"%s\" from playlist
- Playlist synced
- Undo
+ プレイリストから「%s」を削除しました
+ プレイリストが同期されました
+ 元に戻す
歌詞が見つかりません
- Sleep timer
- End of song
+ スリープタイマー
+ 曲の終わり
- - %d minutes
+ - %d 分
ストリームが利用できません
ネットワーク接続がありません
タイムアウトしました
- 不明なエラーです
+ 不明なエラー
いいね
@@ -166,7 +179,7 @@
設定
外観
- Enable dynamic theme
+ 動的なテーマを有効化
ダークテーマ
オン
オフ
@@ -181,59 +194,56 @@
コンテンツ
ログイン
- デフォルトのコンテンツの言語
- デフォルトのコンテンツの国
+ コンテンツの既定の言語
+ コンテンツの既定の国
システムに従う
プロキシを有効化
プロキシのタイプ
プロキシのURL
再起動して反映
- プレイヤーとオーディオ
- オーディオ品質
+ プレイヤーと音声
+ 音声品質
自動
高
低
再生キューを保持
無音部分をスキップ
- オーディオノーマライゼーション
+ 音声の正規化
イコライザー
ストレージ
キャッシュ
- Image Cache
- Song Cache
- Max cache size
- Unlimited
- Clear all downloads
+ 画像のキャッシュ
+ 曲のキャッシュ
+ 最大キャッシュサイズ
+ 無制限
+ すべてのダウンロードを削除
画像の最大キャッシュサイズ
画像のキャッシュを削除
曲の最大キャッシュサイズ
- Clear song cache
+ 曲のキャッシュを削除
%s 使用中
プライバシー
- Pause listen history
- Clear listen history
- Are you sure to clear all listen history?
- 履歴の記録を一時停止
- 履歴を削除
+ 再生履歴を一時停止
+ 再生履歴を削除
+ すべての再生履歴を削除しますか?
+ 検索履歴の記録を一時停止
+ 検索履歴を削除
すべての検索履歴を削除しますか?
- 酷狗からの歌詞取得を有効化
+ 酷狗 (Kugou) からの歌詞取得を有効化
- バックアップとリストア
+ バックアップと復元
バックアップ
- リストア
- Choose a csv file from Google Takeout
- Imported playlist
-
- - Imported \"%s\" with %d song
- - Imported \"%s\" with %d songs
-
+ 復元
+ インポートしたプレイリスト
バックアップの作成に成功しました
バックアップを作成できませんでした
- バックアップのリストアに失敗しました
+ バックアップの復元に失敗しました
アプリについて
アプリのバージョン
+
+ New version available
diff --git a/app/src/main/res/values-ko-rKR/strings.xml b/app/src/main/res/values-ko-rKR/strings.xml
index 1c51a9aa2..62486122a 100644
--- a/app/src/main/res/values-ko-rKR/strings.xml
+++ b/app/src/main/res/values-ko-rKR/strings.xml
@@ -1,4 +1,4 @@
-
+
Home
노래
@@ -14,8 +14,10 @@
History
Stats
+ Mood and Genres
+ Account
Quick picks
- Listen some songs to generate your quick picks
+ Listen to songs to generate your quick picks
New release albums
@@ -27,6 +29,7 @@
Most played songs
Most played artists
+ Most played albums
검색
@@ -40,6 +43,7 @@
플레이리스트
Community playlists
Featured playlists
+ Bookmarked
No results found
@@ -133,6 +137,15 @@
- %d playlists
+
+ - %d weeks
+
+
+ - %d months
+
+
+ - %d years
+
Playlist imported
@@ -224,16 +237,13 @@
Backup and restore
Backup
Restore
- Choose a csv file from Google Takeout
Imported playlist
-
- - Imported \"%s\" with %d song
- - Imported \"%s\" with %d songs
-
Backup created successfully
Couldn\'t create backup
Failed to restore backup
정보
앱 버전
+
+ New version available
diff --git a/app/src/main/res/values-ml-rIN/strings.xml b/app/src/main/res/values-ml-rIN/strings.xml
index 9205fb986..02283bfca 100644
--- a/app/src/main/res/values-ml-rIN/strings.xml
+++ b/app/src/main/res/values-ml-rIN/strings.xml
@@ -15,8 +15,10 @@
History
Stats
+ Mood and Genres
+ Account
Quick picks
- Listen some songs to generate your quick picks
+ Listen to songs to generate your quick picks
New release albums
@@ -28,6 +30,7 @@
Most played songs
Most played artists
+ Most played albums
തിരയുക
@@ -41,6 +44,7 @@
പ്ലേലിസ്റ്റുകൾ
കമ്മ്യൂണിറ്റി പ്ലേലിസ്റ്റുകൾ
തിരഞ്ഞെടുത്ത പ്ലേലിസ്റ്റുകൾ
+ Bookmarked
No results found
@@ -138,6 +142,18 @@
- %d പ്ലേലിസ്റ്റ്
- %d പ്ലേലിസ്റ്റുകൾ
+
+ - %d week
+ - %d weeks
+
+
+ - %d month
+ - %d months
+
+
+ - %d year
+ - %d years
+
പ്ലേലിസ്റ്റ് ഇറക്കുമതി ചെയ്തു
@@ -230,16 +246,13 @@
ബാക്കപ്പും വീണ്ടെടുക്കലും
ബാക്കപ്പ്
വീണ്ടെടുക്കൽ
- Choose a csv file from Google Takeout
Imported playlist
-
- - Imported \"%s\" with %d song
- - Imported \"%s\" with %d songs
-
ബാക്കപ്പ് സൃഷ്ടിച്ചു
ബാക്കപ്പ് സൃഷ്ടിക്കാൻ കഴിഞ്ഞില്ല
ബാക്കപ്പ് പുനഃസ്ഥാപിക്കാൻ കഴിഞ്ഞില്ല
കുറിച്ച്
അപ്ലിക്കേഷൻ പതിപ്പ്
+
+ New version available
diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml
index 30fe5e150..cee9296e8 100644
--- a/app/src/main/res/values-nl/strings.xml
+++ b/app/src/main/res/values-nl/strings.xml
@@ -15,6 +15,8 @@
Geschiedenis
Statistieken
+ Stemmingen en genres
+ Account
Snelle keuzes
Beluister een aantal nummers om je snelle keuzes te genereren
Nieuwe albums
@@ -28,11 +30,12 @@
Meest afgespeelde nummers
Meest afgespeelde artiesten
+ Meest afgespeelde artiesten
Zoeken
Zoeken via YouTube Music…
- Zoeken in bibliotheek…
+ Zoeken in bibliotheek
Alles
Nummers
Videos
@@ -41,6 +44,7 @@
Afspeellijsten
Afspeellijsten van de community
Voorgestelde afspeellijsten
+ Bookmarked
Geen resultaten gevonden
@@ -138,6 +142,18 @@
- %d afspeellijst
- %d afspeellijsten
+
+ - %d week
+ - %d weeks
+
+
+ - %d month
+ - %d months
+
+
+ - %d year
+ - %d years
+
Afspeellijst geimporteerd
@@ -230,16 +246,13 @@
Backup en herstel
Backup
Herstellen
- Kies een csv-bestand van Google Takeout
Afspeellijst geimporteerd
-
- - Imported \"%s\" with %d song
- - Imported \"%s\" with %d songs
-
Back-up succesvol gemaakt.
Back-up maken mislukt.
Fout bij terugzetten back-up.
Over
App versie
+
+ New version available
diff --git a/app/src/main/res/values-or-rIN/strings.xml b/app/src/main/res/values-or-rIN/strings.xml
index e1c3fc854..940e2e4c2 100644
--- a/app/src/main/res/values-or-rIN/strings.xml
+++ b/app/src/main/res/values-or-rIN/strings.xml
@@ -15,8 +15,10 @@
History
Stats
+ Mood and Genres
+ Account
Quick picks
- Listen some songs to generate your quick picks
+ Listen to songs to generate your quick picks
New release albums
@@ -28,6 +30,7 @@
Most played songs
Most played artists
+ Most played albums
ସନ୍ଧାନ କରନ୍ତୁ
@@ -41,6 +44,7 @@
ପ୍ଲେଲିଷ୍ଟ ଗୁଡ଼ିକ
ସମ୍ପ୍ରଦାୟ ପ୍ଲେଲିଷ୍ଟଗୁଡିକ
ବୈଶିଷ୍ଟ୍ୟଯୁକ୍ତ ତାଲିକାଗୁଡ଼ିକ
+ Bookmarked
No results found
@@ -138,6 +142,18 @@
- %d playlist
- %d playlists
+
+ - %d week
+ - %d weeks
+
+
+ - %d month
+ - %d months
+
+
+ - %d year
+ - %d years
+
ପ୍ଲେଲିଷ୍ଟ ଆମଦାନୀ ହୋଇଛି
@@ -230,16 +246,13 @@
ନକଲ ସଂରକ୍ଷଣ ଏବଂ ପୁନରୁଦ୍ଧାର କରନ୍ତୁ
ନକଲ ସଂରକ୍ଷଣ
ପୁନରୁଦ୍ଧାର କରନ୍ତୁ
- Choose a csv file from Google Takeout
Imported playlist
-
- - Imported \"%s\" with %d song
- - Imported \"%s\" with %d songs
-
ବ୍ୟାକଅପ୍ ସଫଳତାର ସହିତ ସୃଷ୍ଟି ହେଲା
ବ୍ୟାକଅପ୍ ସୃଷ୍ଟି କରିପାରିବ ନାହିଁ
ବ୍ୟାକଅପ୍ ପୁନରୁଦ୍ଧାର କରିବାରେ ବିଫଳ
ବିଷୟରେ
ଆପ୍ ସଂସ୍କରଣ
+
+ New version available
diff --git a/app/src/main/res/values-pa/strings.xml b/app/src/main/res/values-pa/strings.xml
index 6d657d45d..716626bb7 100644
--- a/app/src/main/res/values-pa/strings.xml
+++ b/app/src/main/res/values-pa/strings.xml
@@ -15,8 +15,10 @@
ਅਤੀਤ
ਅੰਕੜੇ
+ Mood and Genres
+ Account
ਉਂਗਲਾਂ ਤੇ ਚੁਣੇ ਗੀਤ
- Listen some songs to generate your quick picks
+ Listen to songs to generate your quick picks
ਨਵੀਆਂ ਜਾਰੀ ਐਲਬਮਾਂ
@@ -28,6 +30,7 @@
ਵੱਧ ਸੁਣੇ ਗਏ ਗੀਤ
Most played artists
+ Most played albums
ਖੋਜੋ
@@ -41,6 +44,7 @@
ਪਲੇਲਿਸਟਾਂ
ਕਮਿਊਨਿਟੀ ਪਲੇਲਿਸਟਾਂ
ਪ੍ਰਦਰਸ਼ਿਤ ਪਲੇਲਿਸਟਾਂ
+ Bookmarked
ਕੋਈ ਨਤੀਜੇ ਨਹੀਂ ਲੱਭੇ
@@ -138,6 +142,18 @@
- %d ਪਲੇਲਿਸਟ
- %d ਪਲੇਲਿਸਟਾਂ
+
+ - %d week
+ - %d weeks
+
+
+ - %d month
+ - %d months
+
+
+ - %d year
+ - %d years
+
ਪਲੇਲਿਸਟ ਅਯਾਤ ਕੀਤੀ ਗਈ
@@ -230,16 +246,13 @@
ਬੈਕਅੱਪ ਅਤੇ ਰੀਸਟੋਰ
ਬੈਕਅੱਪ
ਰੀਸਟੋਰ
- Choose a csv file from Google Takeout
Imported playlist
-
- - Imported \"%s\" with %d song
- - Imported \"%s\" with %d songs
-
ਬੈਕਅੱਪ ਸਫਲਤਾਪੂਰਵਕ ਬਣਾਇਆ ਗਿਆ
ਬੈਕਅੱਪ ਨਹੀਂ ਬਣਾਇਆ ਜਾ ਸਕਿਆ
ਬੈਕਅੱਪ ਰੀਸਟੋਰ ਕਰਨਾ ਅਸਫਲ ਰਿਹਾ
ਦੇ ਬਾਰੇ
ਐਪ ਸੰਸਕਰਣ
+
+ New version available
diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml
new file mode 100644
index 000000000..234a85b47
--- /dev/null
+++ b/app/src/main/res/values-pl/strings.xml
@@ -0,0 +1,276 @@
+
+
+ Główna
+ Utwory
+ Wykonawcy
+ Albumy
+ Playlisty
+
+
+
+ - %d zaznaczony
+ - %d zaznaczone
+ - %d zaznaczonych
+ - %d zaznaczonych
+
+
+
+ Historia
+ Statystyki
+ Nastroje i gatunki
+ Konto
+ Szybki wybór
+ Słuchaj utworów, aby wygenerować szybkie wybory
+ Nowo wydane albumy
+
+
+ Dzisiaj
+ Wczoraj
+ Ten tydzień
+ Ostatni tydzień
+
+
+ Najczęściej słuchane utwory
+ Najczęściej słuchani wykonawcy
+ Najczęściej słuchane albumy
+
+
+ Szukaj
+ Szukaj w YouTube Music…
+ Szukaj w bibliotece…
+ Wszystko
+ Utwory
+ Filmy
+ Albumy
+ Wykonawcy
+ Playlisty
+ Playlisty tworzone przez społeczność
+ Polecane playlisty
+ Ulubione
+ Brak wyników
+
+
+ Z twojej biblioteki
+
+
+ Polubione utwory
+ Pobrane utwory
+ Playlista jest pusta
+
+
+ Spróbuj ponownie
+ Radio
+ Losowo
+
+
+ Szczegóły
+ Edytuj
+ Włącz radio
+ Odtwórz
+ Odtwórz jako następny
+ Dodaj do kolejki
+ Dodaj do biblioteki
+ Usuń z biblioteki
+ Pobierz
+ Pobieranie
+ Usuń z pobranych
+ Importuj playlistę
+ Dodaj do playlisty
+ Pokaż wykonawcę
+ Pokaż album
+ Odśwież
+ Udostępnij
+ Usuń
+ Usuń z historii
+ Szukaj online
+ Synchronizuj
+
+
+ Data dodania
+ Nazwa
+ Wykonawca
+ Rok
+ Liczba utworów
+ Długość
+ Długość utworu
+ Niestandardowa kolejność
+
+
+ ID media
+ Typ MIME
+ Kodeki
+ Przepływność
+ Częstotliwość próbkowania
+ Natężenie
+ Głośność
+ Rozmiar pliku
+ Nieznane
+ Skopiowano do schowka
+
+ Edytuj tekst
+ Szukaj tekstu
+
+ Edytuj utwór
+ Tytuł utworu
+ Wykonawcy utworu
+ Tytuł utworu nie może być pusty.
+ Wykonawca utworu nie może być pusty.
+ Zapisz
+
+ Wybierz playlistę
+ Edytuj playlistę
+ Utwórz playlistę
+ Nazwa playlisty
+ Nazwa playlisty nie może być pusta.
+
+ Edytuj wykonawcę
+ Nazwa wykonawcy
+ Nazwa wykonawcy nie może być pusta.
+
+
+
+ - %d utwór
+ - %d utwory
+ - %d utworów
+ - %d utworów
+
+
+ - %d wykonawca
+ - %d wykonawców
+ - %d wykonawców
+ - %d wykonawców
+
+
+ - %d album
+ - %d albumy
+ - %d albumów
+ - %d albumów
+
+
+ - %d playlista
+ - %d playlisty
+ - %d playlist
+ - %d playlist
+
+
+ - %d tydzień
+ - %d tygodnie
+ - %d tygodni
+ - %d tygodni
+
+
+ - %d miesiąc
+ - %d miesiące
+ - %d miesięcy
+ - %d miesięcy
+
+
+ - %d rok
+ - %d lata
+ - %d lat
+ - %d lat
+
+
+
+ Playlista zaimportowana
+ Usunięto \"%s\" z playlisty
+ Playlista zsynchronizowana
+ Cofnij
+
+
+ Nie znaleziono tekstu
+ Wyłącznik czasowy
+ Koniec utworu
+
+ - 1 minuta
+ - %d minuty
+ - %d minut
+ - %d minut
+
+ Brak dostępnego źródła
+ Brak połączenia z internetem
+ Upłynął limit czasu
+ Nieznany błąd
+
+
+ Polub
+ Usuń polubienie
+
+
+ Wszystkie utwory
+ Wyszukane utwory
+
+
+ Odtwarzacz
+
+
+ Ustawienia
+ Wygląd
+ Włącz motyw dynamiczny
+ Ciemny motyw
+ Włącz
+ Wyłącz
+ Zgodnie z systemem
+ Czysta czerń
+ Domyślnie otwarta zakładka
+ Modyfikuj zakładki nawigacji
+ Położenie tekstu utworu
+ Z lewej
+ Na środku
+ Z prawej
+
+ Zawartość
+ Zaloguj się
+ Domyślny język zawartości
+ Domyślny kraj zawartości
+ Domyślny systemowy
+ Włącz proxy
+ Typ proxy
+ URL proxy
+ Uruchom ponownie, aby zastosować zmiany
+
+ Odtwarzacz i audio
+ Jakość audio
+ Automatyczna
+ Wysoka
+ Niska
+ Trwała kolejka
+ Pomiń ciszę
+ Normalizacja audio
+ Korektor graficzny
+
+ Pamięć
+ Cache
+ Cache obrazów
+ Cache utworów
+ Maksymalny rozmiar cache
+ Nieskończony
+ Wyczyść pobrane
+ Maksymalny rozmiar cache obrazów
+ Wyczyść cache obrazów
+ Maksymalny rozmiar cache utworów
+ Wyczyść cache utworów
+ %s w użyciu
+
+ Prywatność
+ Wstrzymaj historię odtwarzania
+ Wyczyść historię odtwarzania
+ Czy na pewno chcesz wyczyścić całą historię odtwarzania?
+ Wstrzymaj historię wyszukiwania
+ Wyczyść historię wyszukiwania
+ Czy na pewno chcesz wyczyścić całą historię wyszukiwania?
+ Pobieraj teksty utworów z KuGou
+
+ Kopia zapasowa i przywracanie
+ Utwórz kopię zapasową
+ Przywróć
+ Zaimportowane playlisty
+ Kopia zapasowa utworzona pomyślnie
+ Nie udało się stworzyć kopii zapasowej
+ Nie udało się przywrócić kopii zapasowej
+
+ O aplikacji
+ Wersja aplikacji
+
+ Dostępna nowa wersja
+
diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml
index 494be8452..e671ed8ba 100644
--- a/app/src/main/res/values-pt-rBR/strings.xml
+++ b/app/src/main/res/values-pt-rBR/strings.xml
@@ -1,4 +1,4 @@
-
+
Início
Músicas
@@ -10,13 +10,16 @@
- %d selecionada
- %d selecionadas
+ - %d selecionadas
History
Stats
+ Mood and Genres
+ Account
Quick picks
- Listen some songs to generate your quick picks
+ Listen to songs to generate your quick picks
New release álbuns
@@ -28,6 +31,7 @@
Most played songs
Most played artists
+ Most played álbuns
Pesquisar
@@ -41,6 +45,7 @@
Playlists
Playlists da comunidade
Playlists em destaque
+ Bookmarked
No results found
@@ -125,18 +130,37 @@
- %d música
- %d músicas
+ - %d músicas
- %d artista
- %d artistas
+ - %d artistas
- %d álbum
- %d álbuns
+ - %d álbuns
- %d playlist
- %d playlists
+ - %d playlists
+
+
+ - %d week
+ - %d weeks
+ - %d weeks
+
+
+ - %d month
+ - %d months
+ - %d months
+
+
+ - %d year
+ - %d years
+ - %d years
@@ -150,8 +174,9 @@
Sleep timer
End of song
- - 1 minute
+ - %d minute
- %d minutes
+ - %d minutes
Nenhum canal de reprodução disponível
Sem conexão com à internet
@@ -230,16 +255,13 @@
Backup e restauração
Backup
Restaurar
- Choose a csv file from Google Takeout
Imported playlist
-
- - Imported \"%s\" with %d song
- - Imported \"%s\" with %d songs
-
Backup criado com sucesso
Não foi possível criar o backup
Falha ao restaurar o backup
Sobre
Versão do aplicativo
+
+ New version available
diff --git a/app/src/main/res/values-ru-rRU/strings.xml b/app/src/main/res/values-ru-rRU/strings.xml
index 95624559a..bae17e29e 100644
--- a/app/src/main/res/values-ru-rRU/strings.xml
+++ b/app/src/main/res/values-ru-rRU/strings.xml
@@ -17,8 +17,10 @@
История
Статистика
+ Настроение и жанры
+ Аккаунт
Быстрый выбор
- Listen some songs to generate your quick picks
+ Послушайте несколько песен, чтобы создать ваш быстрый выбор
Новые релизы альбомов
@@ -30,6 +32,7 @@
Самые популярные песни
Самые популярные исполнители
+ Самые популярные альбомы
Поиск
@@ -43,6 +46,7 @@
Плейлисты
Плейлисты сообщества
Избранные плейлисты
+ Добавлено в закладки
Результаты не найдены
@@ -148,11 +152,29 @@
- %d плейлистов
- %d плейлистов
+
+ - %d неделя
+ - %d недели
+ - %d недель
+ - %d недель
+
+
+ - %d месяц
+ - %d месяца
+ - %d месяцев
+ - %d месяцев
+
+
+ - %d год
+ - %d года
+ - %d лет
+ - %d лет
+
Плейлист импортирован
«%s» удалено из списка воспроизведения
- Playlist synced
+ Плейлист синхронизирован
Отменить
@@ -242,16 +264,13 @@
Резервное копирование
Создать резервную копию
Восстановить из резервной копии
- Выбрать csv-файл из Google Takeout
Импортированный плейлист
-
- - Imported \"%s\" with %d song
- - Imported \"%s\" with %d songs
-
Резервная копия создана успешно
Не удалось создать резервную копию
Не удалось восстановить резервную копию
О приложении
Версия приложения
+
+ Доступна новая версия
diff --git a/app/src/main/res/values-sv-rSE/strings.xml b/app/src/main/res/values-sv-rSE/strings.xml
index 24522ccdf..eac93c378 100644
--- a/app/src/main/res/values-sv-rSE/strings.xml
+++ b/app/src/main/res/values-sv-rSE/strings.xml
@@ -15,8 +15,10 @@
History
Stats
+ Mood and Genres
+ Account
Quick picks
- Listen some songs to generate your quick picks
+ Listen to songs to generate your quick picks
New release albums
@@ -28,6 +30,7 @@
Most played songs
Most played artists
+ Most played albums
Sök
@@ -41,6 +44,7 @@
Spellistor
Community playlists
Featured playlists
+ Bookmarked
No results found
@@ -138,6 +142,18 @@
- %d playlist
- %d playlists
+
+ - %d week
+ - %d weeks
+
+
+ - %d month
+ - %d months
+
+
+ - %d year
+ - %d years
+
Playlist imported
@@ -230,16 +246,13 @@
Backup and restore
Backup
Restore
- Choose a csv file from Google Takeout
Imported playlist
-
- - Imported \"%s\" with %d song
- - Imported \"%s\" with %d songs
-
Backup created successfully
Couldn\'t create backup
Failed to restore backup
Om
App version
+
+ New version available
diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml
index 2a0169930..42441d045 100644
--- a/app/src/main/res/values-tr/strings.xml
+++ b/app/src/main/res/values-tr/strings.xml
@@ -15,6 +15,8 @@
Geçmiş
İstatistikler
+ Ruh Hali ve Türler
+ Hesap
Hızlı seçimler
Hızlı seçimlerinizi oluşturmak için birkaç şarkı dinleyin
Yeni çıkan albümler
@@ -28,6 +30,7 @@
En çok dinlenen şarkılar
En çok dinlenen sanatçılar
+ En çok dinlenen albümler
Arama
@@ -41,6 +44,7 @@
Çalma Listeleri
Topluluk çalma listeleri
Öne çıkan listeler
+ Favoriler
Sonuç bulunamadı
@@ -61,7 +65,7 @@
Düzenle
Radyo başlat
Çal
- Sonraki olarak çal
+ Bir sonra çal
Sıraya ekle
Kütüphaneye ekle
Kütüphaneden kaldır
@@ -138,11 +142,23 @@
- %d çalma listesi
- %d çalma listesi
+
+ - %d hafta
+ - %d hafta
+
+
+ - %d ay
+ - %d ay
+
+
+ - %d yıl
+ - %d yıl
+
Çalma listesi içe aktarıldı
- Çalma listesinden \"%s\" kaldırıldı
- Playlist synced
+ \"%s\" çalma listesinden kaldırıldı
+ Çalma listesi eşitlendi
Geri al
@@ -230,16 +246,13 @@
Yedekleme ve geri yükleme
Yedekle
Geri yükle
- Google Takeout\'tan bir csv dosyası seçin
Çalma listesi içe aktarıldı
-
- - Imported \"%s\" with %d song
- - Imported \"%s\" with %d songs
-
Yedekleme başarıyla oluşturuldu
Yedekleme oluşturulamadı
Yedekleme geri yüklenemedi
Hakkında
Uygulama sürümü
+
+ Yeni sürüm mevcut
diff --git a/app/src/main/res/values-uk-rUA/strings.xml b/app/src/main/res/values-uk-rUA/strings.xml
index ae3f24e7c..686cf8591 100644
--- a/app/src/main/res/values-uk-rUA/strings.xml
+++ b/app/src/main/res/values-uk-rUA/strings.xml
@@ -17,8 +17,10 @@
Історія
Статистика
+ Настрій та жанри
+ Акаунт
Швидкий вибір
- Listen some songs to generate your quick picks
+ Послухайте кілька пісень, щоб створити ваш швидкий вибір
Нові релізи альбомів
@@ -30,6 +32,7 @@
Найпопулярніші пісні
Найпопулярніші виконавці
+ Найпопулярніші альбоми
Пошук
@@ -43,6 +46,7 @@
Плейлисти
Плейлисти спільноти
Обрані плейлисти
+ Додано в закладки
Результатів не знайдено
@@ -148,11 +152,29 @@
- %d плейлистів
- %d плейлистів
+
+ - %d тиждень
+ - %d тижні
+ - %d тижнів
+ - %d тижнів
+
+
+ - %d місяць
+ - %d місяці
+ - %d місяців
+ - %d місяців
+
+
+ - %d рік
+ - %d роки
+ - %d років
+ - %d років
+
Плейлист імпортовано
«%s» видалено зі списку відтворення
- Playlist synced
+ Плейлист синхронізовано
Скасувати
@@ -242,16 +264,13 @@
Резервне копіювання
Створити резервну копію
Відновити з резервної копії
- Вибрати csv-файл із Google Takeout
Імпортований плейлист
-
- - Imported \"%s\" with %d song
- - Imported \"%s\" with %d songs
-
Резервну копію створено успішно
Не вдалося створити резервну копію
Не вдалося відновити з резервної копії
Про програму
Версія застосунку
+
+ Доступна нова версія
diff --git a/app/src/main/res/values-vn/strings.xml b/app/src/main/res/values-vn/strings.xml
new file mode 100644
index 000000000..659aff00b
--- /dev/null
+++ b/app/src/main/res/values-vn/strings.xml
@@ -0,0 +1,257 @@
+
+
+ Trang chủ
+ Bài hát
+ Tác giả
+ Albums
+ Danh sách phát
+
+
+
+ - Đã chọn %d
+
+
+
+ Lịch sử
+ Thống kê
+ Tâm trạng và thể loại
+ Tài Khoản
+ Chọn nhanh
+ Nghe các bài hát để tạo danh sách chọn nhanh của bạn
+ Các albums mới phát hành
+
+
+ Hôm nay
+ Hôm qua
+ Tuần này
+ Tuần trước
+
+
+ Bài hát được nghe nhiều nhất
+ Tác giả được nghe nhiều nhất
+ Album được nghe nhiều nhất
+
+
+ Tìm kiếm
+ Tìm kiếm YouTube Music…
+ Tìm kiếm thư viện…
+ Tất cả
+ Bài hát
+ Videos
+ Albums
+ Tác giả
+ Danh sách phát
+ Danh sách phát cộng đồng
+ Danh sách phát tiêu biểu
+ Bookmarked
+ Không tìm thấy kết quả
+
+
+ Từ thư viện của bạn
+
+
+ Bài hát được yêu thích
+ Bài hát được tải về
+ Danh sách phát trống
+
+
+ Thử lại
+ Đài
+ Trộn
+
+
+ Chi tiết
+ Tuỳ chỉnh
+ Bắt đầu nghe đài
+ Chạy
+ Chạy bài tiếp theo
+ Thêm vào hàng chờ
+ Thêm vào thư viện
+ Xoá khỏi thư viện
+ Tải xuống
+ Đang tải xuống
+ Xoá tải
+ Thêm danh sách phát
+ Thêm vào danh sách phát
+ Xem tác giả
+ Xem album
+ Làm mới
+ Chia sẻ
+ Xoá
+ Xoá khỏi lịch sử
+ Tìm kiếm trực tuyến
+ Đồng bộ
+
+
+ Ngày thêm vào
+ Tên
+ Tác giả
+ Năm
+ Số lượng bài hát
+ Độ dài
+ Thời gian phát
+ Tuỳ chỉnh bộ lọc
+
+
+ Media id
+ MIME type
+ Codecs
+ Bitrate
+ Sample rate
+ Loudness
+ Volume
+ File size
+ Unknown
+ Copied to clipboard
+
+ Sửa lời bài hát
+ Tìm kiếm lời bài hát
+
+ Sửa bài hát
+ Tiêu đề bài hát
+ Tác giả bài hát
+ Tiêu đề không được để trống.
+ Tác giả không được để trống.
+ Lưu lại
+
+ Lựa chọn danh sách phát
+ Tuỳ chọn danh sách phát
+ Tạo danh sách phát
+ Tên danh sách phát
+ Tên danh sách phát không được để trống.
+
+ Tuỳ chỉnh tác giả
+ Tên tác giả
+ Tên tác giả không được để trống.
+
+
+
+ - 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
+
+
+
+ Danh sách phát đã được thêm vào
+ Đã xoá \"%s\" từ danh sách phát
+ Danh sách phát được đồng bộ
+ Làm lại
+
+
+ Không tìm thấy lời bài hát
+ Hẹn giờ
+ Kết thúc bài hát
+
+ - 1 phút
+ - %d phút
+
+ Không có stream
+ Không có kết nối mạng
+ Hết giờ
+ Lỗi không được xác định
+
+
+ Thích
+ Bỏ thích
+
+
+ Tất cả bài hát
+ Những bài hát được tìm kiếm
+
+
+ Music Player
+
+
+ Cài đặt
+ Hiện thị
+ Bật chủ đề theo màu sắc
+ Chủ đề tối
+ Bật
+ Tắt
+ Theo hệ thống
+ Tối hoàn toàn
+ Tab mặc định
+ Tuỳ chỉnh tab tuỳ biền
+ Vị trí lời bài hát
+ Trái
+ Canh giữa
+ Phải
+
+ Nội dung
+ Đăng nhập
+ Ngôn ngữ nội dung mặc định
+ Quốc gia nội dung mặc định
+ Hệ thống mặc định
+ Bật proxy
+ Loại Proxy
+ Liên kết Proxy
+ Khởi động lại để áp dụng
+
+ Player and audio
+ Chất lượng âm thanh
+ Tự động
+ Cao
+ Thấp
+ Hàng đợi liên tục
+ bỏ qua đoạn im lặng
+ Chuẩn hoá âm thanh
+ Bộ chỉnh âm
+
+ Lưu trữ
+ Cache
+ Cache hình ảnh
+ Cache bài hát
+ Tối đa dung lượng cache
+ Không giới hạn
+ Xoá tất cả đã tải xuống
+ Dung lượng cache hình ảnh tối đa
+ Xoá hết cache hình ảnh
+ Dung lượng cache bài hát tối đa
+ Xoá hết cache nhạc
+ Đã sử dụng %s
+
+ Riêng tư
+ Dừng lịch sử nghe
+ Xoá hết lịch sử nghe
+ Bạn có chắc muốn xoá hết lịch sử nghe?
+ Tạm dừng lịch sử tìm kiếm
+ Xoá hết lịch sử tìm kiếm
+ Bạn có chắc muốn xoá hết lịch sử tìm kiếm?
+ Kích hoạt nhà cung cấp lời bài hát KuGou
+
+ Sao lưu và khôi phục
+ Sao lưu
+ Khôi phục
+ Đã thêm danh sách phát
+ Sao lưu thành công
+ Không thể sao lưu
+ Khôi phục thất bại
+
+ Tác giả
+ Phiên bản ứng dụng
+
+ New version available
+
diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml
index a8d24ed0e..423d76a33 100644
--- a/app/src/main/res/values-zh-rCN/strings.xml
+++ b/app/src/main/res/values-zh-rCN/strings.xml
@@ -14,8 +14,10 @@
历史记录
统计
+ Mood and Genres
+ Account
歌曲快选
- Listen some songs to generate your quick picks
+ Listen to songs to generate your quick picks
新专辑
@@ -27,6 +29,7 @@
最常播放
Most played artists
+ Most played albums
搜索
@@ -40,6 +43,7 @@
播放列表
社区播放列表
精选播放列表
+ Bookmarked
找不到结果
@@ -133,6 +137,15 @@
- %d 个播放列表
+
+ - %d weeks
+
+
+ - %d months
+
+
+ - %d years
+
已导入此播放列表
@@ -224,16 +237,13 @@
备份与还原
备份
还原
- Choose a csv file from Google Takeout
Imported playlist
-
- - Imported \"%s\" with %d song
- - Imported \"%s\" with %d songs
-
成功新建备份
无法新建备份
无法还原备份
关于
应用版本
+
+ New version available
diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml
index a4fd3e908..2cd8eadef 100755
--- a/app/src/main/res/values-zh-rTW/strings.xml
+++ b/app/src/main/res/values-zh-rTW/strings.xml
@@ -14,6 +14,8 @@
歷史記錄
統計
+ 情境與類型
+ 帳號
歌曲快選
聽一些音樂讓我們知道您的喜好
新專輯
@@ -27,6 +29,7 @@
最常播放的歌曲
最常播放的藝人
+ 最常播放的專輯
搜尋
@@ -40,6 +43,7 @@
播放清單
社群播放清單
精選播放清單
+ 收藏
找不到結果
@@ -133,6 +137,15 @@
- %d 個播放清單
+
+ - %d 週
+
+
+ - %d 個月
+
+
+ - %d 年
+
已匯入此播放清單
@@ -224,15 +237,13 @@
備份與還原
備份
還原
- 選擇從 Google Takeout 匯出的 csv 檔
已匯入的播放清單
-
- - 已匯入「%s」,有%d首歌曲
-
成功建立備份
無法建立備份
無法還原備份
關於
應用程式版本
+
+ 發現新版本
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 3ec15c363..eda7659dc 100755
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -14,8 +14,10 @@
History
Stats
+ Mood and Genres
+ Account
Quick picks
- Listen some songs to generate your quick picks
+ Listen to songs to generate your quick picks
New release albums
@@ -27,6 +29,7 @@
Most played songs
Most played artists
+ Most played albums
Search
@@ -40,6 +43,7 @@
Playlists
Community playlists
Featured playlists
+ Bookmarked
No results found
@@ -137,6 +141,18 @@
- %d playlist
- %d playlists
+
+ - %d week
+ - %d weeks
+
+
+ - %d month
+ - %d months
+
+
+ - %d year
+ - %d years
+
Playlist imported
@@ -229,16 +245,13 @@
Backup and restore
Backup
Restore
- Choose a csv file from Google Takeout
Imported playlist
-
- - Imported \"%s\" with %d song
- - Imported \"%s\" with %d songs
-
Backup created successfully
Couldn\'t create backup
Failed to restore backup
About
App version
+
+ New version available
diff --git a/app/src/main/res/xml-v25/shortcuts.xml b/app/src/main/res/xml-v25/shortcuts.xml
new file mode 100644
index 000000000..4d027a670
--- /dev/null
+++ b/app/src/main/res/xml-v25/shortcuts.xml
@@ -0,0 +1,43 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/build.gradle.kts b/build.gradle.kts
index 42d159bfa..c5b81cacb 100755
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -12,6 +12,9 @@ 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)
}
}
diff --git a/fastlane/metadata/android/en-US/changelogs/17.txt b/fastlane/metadata/android/en-US/changelogs/17.txt
new file mode 100644
index 000000000..70719318c
--- /dev/null
+++ b/fastlane/metadata/android/en-US/changelogs/17.txt
@@ -0,0 +1,5 @@
+- Login support
+- Mood & Genres
+- Better stats screen
+- Bookmark artists
+- Minor enhancement and bug fixes
\ No newline at end of file
diff --git a/fastlane/metadata/android/es/full_description.txt b/fastlane/metadata/android/es/full_description.txt
new file mode 100644
index 000000000..9b26f0a96
--- /dev/null
+++ b/fastlane/metadata/android/es/full_description.txt
@@ -0,0 +1,17 @@
+Un cliente de YouTube Music con Material 3 para Android
+
+
Características:
+
+- Reproduce canciones de YT/YT Music sin anuncios
+- Reproducción en segundo plano
+- Busca canciones, vídeos, álbumes y listas de reproducción de YouTube Music
+- Gestión de biblioteca
+- Guarda en caché y descarga canciones para reproducirlas sin conexión
+- Letras sincronizadas
+- Saltar silencio
+- Normalización de audio
+- Tema dinámico
+- Localización
+- Compatibilidad con Android Auto
+- Selecciones rápidas personalizadas
+- Material 3
\ No newline at end of file
diff --git a/fastlane/metadata/android/es/short_description.txt b/fastlane/metadata/android/es/short_description.txt
new file mode 100644
index 000000000..a8a999bb8
--- /dev/null
+++ b/fastlane/metadata/android/es/short_description.txt
@@ -0,0 +1 @@
+Un cliente de YouTube Music con Material 3 para Android
\ No newline at end of file
diff --git a/fastlane/metadata/android/it/full_description.txt b/fastlane/metadata/android/it/full_description.txt
index 0b21305f8..63534faf2 100644
--- a/fastlane/metadata/android/it/full_description.txt
+++ b/fastlane/metadata/android/it/full_description.txt
@@ -1,27 +1,17 @@
-Con quest'app, avrai un servizio di streaming musicale gratuito. Puoi ascoltare musica da YouTube Music e crearti la tua libreria musicale personale. Inoltre, i brani possono essere scaricati per un ascolto offline. Puoi anche creare delle playlist per organizzare la tua musica. L'obiettivo di InnerTune è quello di dare la possibilità a chiunque di ascoltare musica senza costi e facilmente, in maniera pratica e priva di pubblicità.
-
-
Nota:
-
-Il progetto è attualmente in una fase instabile. Se trovi degli errori, per favore segnalali facendolo su GitHub.
+Un client di YouTube Music in Material 3 per Android
Caratteristiche:
-* Ascolta brani senza nessuna pubblicità
-* Naviga attraverso quasi tutte le pagine di YouTube Music
-* Cerca brani, video, playlist e canali da YouTube Music
-* Apri link di YouTube Music
-* Salva brani, album e playlist in un database locale
-* Scarica musica per un ascolto offline
-* Modifica il titolo dei brani
-* Aggiungi collegamenti alle tue playlist preferite di YouTube Music
-* Riproduttore in Material Design
-* Riproduzione anche con lo schermo bloccato
-* Controlli in notifica
-* Passa al prossimo brano o a quello precedente
-* Modalità a ripetizione o casuale
-* Modifica la coda dei brani in riproduzione
-* Temi personalizzati
-* Tema scuro
-* Localizzazione
-* Proxy
-* Backup e ripristina
+- Ascolto dei brani da YT/YT Music senza pubblicità
+- Riproduzione in background
+- Ricerca dei brani, video, album e playlist da YouTube Music
+- Gestione della libreria
+- Cache e download dei brani per la riproduzione offline
+- Testi sincronizzati
+- Salto del silenzio
+- Normalizzazione dell'audio
+- Tema dinamico
+- Varie lingue disponibili
+- Supporto per Android Auto
+- Scelte rapide personalizzate
+- Material 3
diff --git a/fastlane/metadata/android/it/short_description.txt b/fastlane/metadata/android/it/short_description.txt
index fb186a850..ac29f8275 100644
--- a/fastlane/metadata/android/it/short_description.txt
+++ b/fastlane/metadata/android/it/short_description.txt
@@ -1 +1 @@
-un riproduttore musicale fatto in Material Design per YouTube Music
+Un client di YouTube Music in Material 3 per Android
diff --git a/fastlane/metadata/android/ru-RU/full_description.txt b/fastlane/metadata/android/ru-RU/full_description.txt
new file mode 100644
index 000000000..2ec4ab3b1
--- /dev/null
+++ b/fastlane/metadata/android/ru-RU/full_description.txt
@@ -0,0 +1,17 @@
+Клиент YouTube Music для Android в стиле Material 3
+
+
Особенности:
+
+- Воспроизведение песен с YT/YT Music без рекламы
+- Фоновое воспроизведение
+- Поиск песен, видео, альбомов и плейлистов в YouTube Music
+- Управление библиотекой
+- Кэширование и загрузка песен для офлайн-воспроизведения
+- Синхронизированный текст песен
+- Пропуск тишины
+- Нормализация аудио
+- Динамическая тема
+- Локализация
+- Поддержка Android Auto
+- Персонализированные быстрые выборки
+- Material 3
diff --git a/fastlane/metadata/android/ru-RU/short_description.txt b/fastlane/metadata/android/ru-RU/short_description.txt
new file mode 100644
index 000000000..1e6c1efe2
--- /dev/null
+++ b/fastlane/metadata/android/ru-RU/short_description.txt
@@ -0,0 +1 @@
+Клиент YouTube Music для Android в стиле Material 3
diff --git a/fastlane/metadata/android/ru-RU/title.txt b/fastlane/metadata/android/ru-RU/title.txt
new file mode 100644
index 000000000..0facb35fe
--- /dev/null
+++ b/fastlane/metadata/android/ru-RU/title.txt
@@ -0,0 +1 @@
+InnerTune
diff --git a/fastlane/metadata/android/uk-UA/full_description.txt b/fastlane/metadata/android/uk-UA/full_description.txt
new file mode 100644
index 000000000..8cc1dc47d
--- /dev/null
+++ b/fastlane/metadata/android/uk-UA/full_description.txt
@@ -0,0 +1,17 @@
+Клієнт YouTube Music для Android у стилі Material 3
+
+Особливості:
+
+- Відтворення пісень з YT/YT Music без реклами
+- Фонове відтворення
+- Пошук пісень, відео, альбомів та плейлистів в YouTube Music
+- Керування бібліотекою
+- Кешування та завантаження пісень для офлайн-відтворення
+- Синхронізований текст пісень
+- Пропуск тиші
+- Нормалізація аудіо
+- Динамічна тема
+- Локалізація
+- Підтримка Android Auto
+- Персоналізовані швидкі вибірки
+- Material 3
diff --git a/fastlane/metadata/android/uk-UA/short_description.txt b/fastlane/metadata/android/uk-UA/short_description.txt
new file mode 100644
index 000000000..56c1e10fc
--- /dev/null
+++ b/fastlane/metadata/android/uk-UA/short_description.txt
@@ -0,0 +1 @@
+Клієнт YouTube Music для Android у стилі Material 3
diff --git a/fastlane/metadata/android/uk-UA/title.txt b/fastlane/metadata/android/uk-UA/title.txt
new file mode 100644
index 000000000..0facb35fe
--- /dev/null
+++ b/fastlane/metadata/android/uk-UA/title.txt
@@ -0,0 +1 @@
+InnerTune
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 002915e6f..31f77b1d0 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -74,5 +74,14 @@ junit = { group = "junit", name = "junit", version = "4.13.2" }
timber = { group = "com.jakewharton.timber", name = "timber", version = "4.7.1" }
+google-services = { module = "com.google.gms:google-services", version = "4.3.15" }
+firebase-bom = { group = "com.google.firebase", name = "firebase-bom", version = "32.2.0" }
+firebase-analytics = { group = "com.google.firebase", name = "firebase-analytics-ktx" }
+firebase-crashlytics = { group = "com.google.firebase", name = "firebase-crashlytics-ktx" }
+firebase-crashlytics-plugin = { module = "com.google.firebase:firebase-crashlytics-gradle", version = "2.9.7" }
+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" }
+
[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 e37a90e38..f97b3dc88 100644
--- a/innertube/src/main/java/com/zionhuang/innertube/InnerTube.kt
+++ b/innertube/src/main/java/com/zionhuang/innertube/InnerTube.kt
@@ -76,7 +76,7 @@ class InnerTube {
}
}
- private fun HttpRequestBuilder.configYTClient(client: YouTubeClient) {
+ private fun HttpRequestBuilder.ytClient(client: YouTubeClient, setLogin: Boolean = false) {
contentType(ContentType.Application.Json)
headers {
append("X-Goog-Api-Format-Version", "1")
@@ -86,12 +86,14 @@ class InnerTube {
if (client.referer != null) {
append("Referer", client.referer)
}
- cookie?.let { cookie ->
- append("cookie", cookie)
- if ("SAPISID" !in cookieMap) return@let
- val currentTime = System.currentTimeMillis() / 1000
- val sapisidHash = sha1("$currentTime ${cookieMap["SAPISID"]} https://music.youtube.com")
- append("Authorization", "SAPISIDHASH ${currentTime}_${sapisidHash}")
+ if (setLogin) {
+ cookie?.let { cookie ->
+ append("cookie", cookie)
+ if ("SAPISID" !in cookieMap) return@let
+ val currentTime = System.currentTimeMillis() / 1000
+ val sapisidHash = sha1("$currentTime ${cookieMap["SAPISID"]} https://music.youtube.com")
+ append("Authorization", "SAPISIDHASH ${currentTime}_${sapisidHash}")
+ }
}
}
userAgent(client.userAgent)
@@ -105,7 +107,7 @@ class InnerTube {
params: String? = null,
continuation: String? = null,
) = httpClient.post("search") {
- configYTClient(client)
+ ytClient(client)
setBody(
SearchBody(
context = client.toContext(locale, visitorData),
@@ -122,7 +124,7 @@ class InnerTube {
videoId: String,
playlistId: String?,
) = httpClient.post("player") {
- configYTClient(client)
+ ytClient(client, setLogin = true)
setBody(
PlayerBody(
context = client.toContext(locale, visitorData).let {
@@ -141,7 +143,7 @@ class InnerTube {
}
suspend fun pipedStreams(videoId: String) =
- httpClient.get("https://watchapi.whatever.social/streams/${videoId}") {
+ httpClient.get("https://pipedapi.kavin.rocks/streams/${videoId}") {
contentType(ContentType.Application.Json)
}
@@ -150,8 +152,9 @@ class InnerTube {
browseId: String? = null,
params: String? = null,
continuation: String? = null,
+ setLogin: Boolean = false,
) = httpClient.post("browse") {
- configYTClient(client)
+ ytClient(client, setLogin)
setBody(
BrowseBody(
context = client.toContext(locale, visitorData),
@@ -175,7 +178,7 @@ class InnerTube {
params: String?,
continuation: String? = null,
) = httpClient.post("next") {
- configYTClient(client)
+ ytClient(client, setLogin = true)
setBody(
NextBody(
context = client.toContext(locale, visitorData),
@@ -193,7 +196,7 @@ class InnerTube {
client: YouTubeClient,
input: String,
) = httpClient.post("music/get_search_suggestions") {
- configYTClient(client)
+ ytClient(client)
setBody(
GetSearchSuggestionsBody(
context = client.toContext(locale, visitorData),
@@ -207,7 +210,7 @@ class InnerTube {
videoIds: List?,
playlistId: String?,
) = httpClient.post("music/get_queue") {
- configYTClient(client)
+ ytClient(client)
setBody(
GetQueueBody(
context = client.toContext(locale, visitorData),
@@ -236,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") {
- configYTClient(client)
+ ytClient(client)
setBody(AccountMenuBody(client.toContext(locale, visitorData)))
}
-}
\ No newline at end of file
+}
diff --git a/innertube/src/main/java/com/zionhuang/innertube/YouTube.kt b/innertube/src/main/java/com/zionhuang/innertube/YouTube.kt
index 627d8eec5..0376212e2 100644
--- a/innertube/src/main/java/com/zionhuang/innertube/YouTube.kt
+++ b/innertube/src/main/java/com/zionhuang/innertube/YouTube.kt
@@ -5,6 +5,8 @@ import com.zionhuang.innertube.models.AlbumItem
import com.zionhuang.innertube.models.Artist
import com.zionhuang.innertube.models.ArtistItem
import com.zionhuang.innertube.models.BrowseEndpoint
+import com.zionhuang.innertube.models.GridRenderer
+import com.zionhuang.innertube.models.MusicCarouselShelfRenderer
import com.zionhuang.innertube.models.PlaylistItem
import com.zionhuang.innertube.models.SearchSuggestions
import com.zionhuang.innertube.models.SongItem
@@ -31,6 +33,9 @@ import com.zionhuang.innertube.pages.AlbumPage
import com.zionhuang.innertube.pages.ArtistItemsContinuationPage
import com.zionhuang.innertube.pages.ArtistItemsPage
import com.zionhuang.innertube.pages.ArtistPage
+import com.zionhuang.innertube.pages.BrowseResult
+import com.zionhuang.innertube.pages.ExplorePage
+import com.zionhuang.innertube.pages.MoodAndGenres
import com.zionhuang.innertube.pages.NewReleaseAlbumPage
import com.zionhuang.innertube.pages.NextPage
import com.zionhuang.innertube.pages.NextResult
@@ -143,7 +148,7 @@ object YouTube {
)
}
- suspend fun album(browseId: String): Result = runCatching {
+ suspend fun album(browseId: String, withSongs: Boolean = true): Result = runCatching {
val response = innerTube.browse(WEB_REMIX, browseId).body()
val playlistId = response.microformat?.microformatDataRenderer?.urlCanonical?.substringAfterLast('=')!!
AlbumPage(
@@ -160,7 +165,7 @@ object YouTube {
year = response.header.musicDetailHeaderRenderer.subtitle.runs.lastOrNull()?.text?.toIntOrNull(),
thumbnail = response.header.musicDetailHeaderRenderer.thumbnail.croppedSquareThumbnailRenderer?.getThumbnailUrl()!!
),
- songs = albumSongs(playlistId).getOrThrow()
+ songs = if (withSongs) albumSongs(playlistId).getOrThrow() else emptyList()
)
}
@@ -234,22 +239,27 @@ object YouTube {
}
suspend fun playlist(playlistId: String): Result = runCatching {
- val response = innerTube.browse(WEB_REMIX, "VL$playlistId").body()
+ val response = innerTube.browse(
+ client = WEB_REMIX,
+ browseId = "VL$playlistId",
+ setLogin = true
+ ).body()
+ val header = response.header?.musicDetailHeaderRenderer ?: response.header?.musicEditablePlaylistDetailHeaderRenderer?.header?.musicDetailHeaderRenderer!!
PlaylistPage(
playlist = PlaylistItem(
id = playlistId,
- title = response.header?.musicDetailHeaderRenderer?.title?.runs?.firstOrNull()?.text!!,
- author = response.header.musicDetailHeaderRenderer.subtitle.runs?.getOrNull(2)?.let {
+ title = header.title.runs?.firstOrNull()?.text!!,
+ author = header.subtitle.runs?.getOrNull(2)?.let {
Artist(
name = it.text,
id = it.navigationEndpoint?.browseEndpoint?.browseId
)
- }!!,
- songCountText = response.header.musicDetailHeaderRenderer.secondSubtitle.runs?.firstOrNull()?.text,
- thumbnail = response.header.musicDetailHeaderRenderer.thumbnail.croppedSquareThumbnailRenderer?.getThumbnailUrl()!!,
+ },
+ songCountText = header.secondSubtitle.runs?.firstOrNull()?.text,
+ thumbnail = header.thumbnail.croppedSquareThumbnailRenderer?.getThumbnailUrl()!!,
playEndpoint = null,
- shuffleEndpoint = response.header.musicDetailHeaderRenderer.menu.menuRenderer.topLevelButtons?.firstOrNull()?.buttonRenderer?.navigationEndpoint?.watchPlaylistEndpoint!!,
- radioEndpoint = response.header.musicDetailHeaderRenderer.menu.menuRenderer.items.find {
+ shuffleEndpoint = header.menu.menuRenderer.topLevelButtons?.firstOrNull()?.buttonRenderer?.navigationEndpoint?.watchPlaylistEndpoint!!,
+ radioEndpoint = header.menu.menuRenderer.items.find {
it.menuNavigationItemRenderer?.icon?.iconType == "MIX"
}?.menuNavigationItemRenderer?.navigationEndpoint?.watchPlaylistEndpoint!!
),
@@ -267,7 +277,11 @@ object YouTube {
}
suspend fun playlistContinuation(continuation: String) = runCatching {
- val response = innerTube.browse(WEB_REMIX, continuation = continuation).body()
+ val response = innerTube.browse(
+ client = WEB_REMIX,
+ continuation = continuation,
+ setLogin = true
+ ).body()
PlaylistContinuationPage(
songs = response.continuationContents?.musicPlaylistShelfContinuation?.contents?.mapNotNull {
PlaylistPage.fromMusicResponsiveListItemRenderer(it.musicResponsiveListItemRenderer)
@@ -276,23 +290,79 @@ object YouTube {
)
}
-
- suspend fun newReleaseAlbumsPreview(): Result> = runCatching {
+ suspend fun explore(): Result = runCatching {
val response = innerTube.browse(WEB_REMIX, browseId = "FEmusic_explore").body()
- response.contents?.singleColumnBrowseResultsRenderer?.tabs?.firstOrNull()?.tabRenderer?.content?.sectionListRenderer?.contents?.getOrNull(1)?.musicCarouselShelfRenderer?.contents?.mapNotNull {
- it.musicTwoRowItemRenderer?.let { renderer ->
- NewReleaseAlbumPage.fromMusicTwoRowItemRenderer(renderer)
- }
- }.orEmpty()
+ ExplorePage(
+ newReleaseAlbums = response.contents?.singleColumnBrowseResultsRenderer?.tabs?.firstOrNull()?.tabRenderer?.content?.sectionListRenderer?.contents?.find {
+ it.musicCarouselShelfRenderer?.header?.musicCarouselShelfBasicHeaderRenderer?.moreContentButton?.buttonRenderer?.navigationEndpoint?.browseEndpoint?.browseId == "FEmusic_new_releases_albums"
+ }?.musicCarouselShelfRenderer?.contents
+ ?.mapNotNull { it.musicTwoRowItemRenderer }
+ ?.mapNotNull(NewReleaseAlbumPage::fromMusicTwoRowItemRenderer).orEmpty(),
+ moodAndGenres = response.contents?.singleColumnBrowseResultsRenderer?.tabs?.firstOrNull()?.tabRenderer?.content?.sectionListRenderer?.contents?.find {
+ it.musicCarouselShelfRenderer?.header?.musicCarouselShelfBasicHeaderRenderer?.moreContentButton?.buttonRenderer?.navigationEndpoint?.browseEndpoint?.browseId == "FEmusic_moods_and_genres"
+ }?.musicCarouselShelfRenderer?.contents
+ ?.mapNotNull { it.musicNavigationButtonRenderer }
+ ?.mapNotNull(MoodAndGenres.Companion::fromMusicNavigationButtonRenderer)
+ .orEmpty()
+ )
}
suspend fun newReleaseAlbums(): Result> = runCatching {
val response = innerTube.browse(WEB_REMIX, browseId = "FEmusic_new_releases_albums").body()
- response.contents?.singleColumnBrowseResultsRenderer?.tabs?.firstOrNull()?.tabRenderer?.content?.sectionListRenderer?.contents?.firstOrNull()?.gridRenderer?.items?.mapNotNull {
- it.musicTwoRowItemRenderer?.let { renderer ->
- NewReleaseAlbumPage.fromMusicTwoRowItemRenderer(renderer)
+ response.contents?.singleColumnBrowseResultsRenderer?.tabs?.firstOrNull()?.tabRenderer?.content?.sectionListRenderer?.contents?.firstOrNull()?.gridRenderer?.items
+ ?.mapNotNull { it.musicTwoRowItemRenderer }
+ ?.mapNotNull(NewReleaseAlbumPage::fromMusicTwoRowItemRenderer)
+ .orEmpty()
+ }
+
+ suspend fun moodAndGenres(): Result> = runCatching {
+ val response = innerTube.browse(WEB_REMIX, browseId = "FEmusic_moods_and_genres").body()
+ response.contents?.singleColumnBrowseResultsRenderer?.tabs?.firstOrNull()?.tabRenderer?.content?.sectionListRenderer?.contents!!
+ .mapNotNull(MoodAndGenres.Companion::fromSectionListRendererContent)
+ }
+
+ suspend fun browse(browseId: String, params: String?): Result = runCatching {
+ val response = innerTube.browse(WEB_REMIX, browseId = browseId, params = params).body()
+ BrowseResult(
+ title = response.header?.musicHeaderRenderer?.title?.runs?.firstOrNull()?.text,
+ items = response.contents?.singleColumnBrowseResultsRenderer?.tabs?.firstOrNull()?.tabRenderer?.content?.sectionListRenderer?.contents?.mapNotNull { content ->
+ when {
+ content.gridRenderer != null -> {
+ BrowseResult.Item(
+ title = content.gridRenderer.header?.gridHeaderRenderer?.title?.runs?.firstOrNull()?.text,
+ items = content.gridRenderer.items
+ .mapNotNull(GridRenderer.Item::musicTwoRowItemRenderer)
+ .mapNotNull(RelatedPage.Companion::fromMusicTwoRowItemRenderer)
+ )
+ }
+
+ content.musicCarouselShelfRenderer != null -> {
+ BrowseResult.Item(
+ title = content.musicCarouselShelfRenderer.header?.musicCarouselShelfBasicHeaderRenderer?.title?.runs?.firstOrNull()?.text,
+ items = content.musicCarouselShelfRenderer.contents
+ .mapNotNull(MusicCarouselShelfRenderer.Content::musicTwoRowItemRenderer)
+ .mapNotNull(RelatedPage.Companion::fromMusicTwoRowItemRenderer)
+ )
+ }
+
+ else -> null
+ }
+ }.orEmpty()
+ )
+ }
+
+ suspend fun likedPlaylists(): Result> = runCatching {
+ val response = innerTube.browse(
+ client = WEB_REMIX,
+ browseId = "FEmusic_liked_playlists",
+ setLogin = true
+ ).body()
+ response.contents?.singleColumnBrowseResultsRenderer?.tabs?.firstOrNull()?.tabRenderer?.content?.sectionListRenderer?.contents?.firstOrNull()?.gridRenderer?.items!!
+ .drop(1) // the first item is "create new playlist"
+ .mapNotNull(GridRenderer.Item::musicTwoRowItemRenderer)
+ .mapNotNull {
+ ArtistItemsPage.fromMusicTwoRowItemRenderer(it) as? PlaylistItem
}
- }.orEmpty()
}
suspend fun player(videoId: String, playlistId: String? = null): Result = runCatching {
diff --git a/innertube/src/main/java/com/zionhuang/innertube/models/MusicTwoRowItemRenderer.kt b/innertube/src/main/java/com/zionhuang/innertube/models/MusicTwoRowItemRenderer.kt
index c07e06d7e..22b86f2a6 100644
--- a/innertube/src/main/java/com/zionhuang/innertube/models/MusicTwoRowItemRenderer.kt
+++ b/innertube/src/main/java/com/zionhuang/innertube/models/MusicTwoRowItemRenderer.kt
@@ -14,9 +14,9 @@ import kotlinx.serialization.Serializable
@Serializable
data class MusicTwoRowItemRenderer(
val title: Runs,
- val subtitle: Runs,
+ val subtitle: Runs?,
val subtitleBadges: List?,
- val menu: Menu,
+ val menu: Menu?,
val thumbnailRenderer: ThumbnailRenderer,
val navigationEndpoint: NavigationEndpoint,
val thumbnailOverlay: MusicResponsiveListItemRenderer.Overlay?,
diff --git a/innertube/src/main/java/com/zionhuang/innertube/models/SectionListRenderer.kt b/innertube/src/main/java/com/zionhuang/innertube/models/SectionListRenderer.kt
index 63fdd7a62..b85f79f20 100644
--- a/innertube/src/main/java/com/zionhuang/innertube/models/SectionListRenderer.kt
+++ b/innertube/src/main/java/com/zionhuang/innertube/models/SectionListRenderer.kt
@@ -12,7 +12,7 @@ data class SectionListRenderer(
) {
@Serializable
data class Header(
- val chipCloudRenderer: ChipCloudRenderer,
+ val chipCloudRenderer: ChipCloudRenderer?,
) {
@Serializable
data class ChipCloudRenderer(
diff --git a/innertube/src/main/java/com/zionhuang/innertube/models/YTItem.kt b/innertube/src/main/java/com/zionhuang/innertube/models/YTItem.kt
index e71e088e2..d35765650 100644
--- a/innertube/src/main/java/com/zionhuang/innertube/models/YTItem.kt
+++ b/innertube/src/main/java/com/zionhuang/innertube/models/YTItem.kt
@@ -49,7 +49,7 @@ data class AlbumItem(
data class PlaylistItem(
override val id: String,
override val title: String,
- val author: Artist,
+ val author: Artist?,
val songCountText: String?,
override val thumbnail: String,
val playEndpoint: WatchEndpoint?,
diff --git a/innertube/src/main/java/com/zionhuang/innertube/models/response/AccountMenuResponse.kt b/innertube/src/main/java/com/zionhuang/innertube/models/response/AccountMenuResponse.kt
index 7c1118771..b343157ce 100644
--- a/innertube/src/main/java/com/zionhuang/innertube/models/response/AccountMenuResponse.kt
+++ b/innertube/src/main/java/com/zionhuang/innertube/models/response/AccountMenuResponse.kt
@@ -34,8 +34,8 @@ data class AccountMenuResponse(
val email: Runs,
) {
fun toAccountInfo() = AccountInfo(
- accountName.toString(),
- email.toString()
+ accountName.runs!!.first().text,
+ email.runs!!.first().text
)
}
}
diff --git a/innertube/src/main/java/com/zionhuang/innertube/models/response/BrowseResponse.kt b/innertube/src/main/java/com/zionhuang/innertube/models/response/BrowseResponse.kt
index bf9380546..897c8bcc5 100644
--- a/innertube/src/main/java/com/zionhuang/innertube/models/response/BrowseResponse.kt
+++ b/innertube/src/main/java/com/zionhuang/innertube/models/response/BrowseResponse.kt
@@ -1,6 +1,14 @@
package com.zionhuang.innertube.models.response
-import com.zionhuang.innertube.models.*
+import com.zionhuang.innertube.models.Button
+import com.zionhuang.innertube.models.Continuation
+import com.zionhuang.innertube.models.Menu
+import com.zionhuang.innertube.models.MusicShelfRenderer
+import com.zionhuang.innertube.models.ResponseContext
+import com.zionhuang.innertube.models.Runs
+import com.zionhuang.innertube.models.SectionListRenderer
+import com.zionhuang.innertube.models.Tabs
+import com.zionhuang.innertube.models.ThumbnailRenderer
import kotlinx.serialization.Serializable
@Serializable
@@ -39,6 +47,7 @@ data class BrowseResponse(
data class Header(
val musicImmersiveHeaderRenderer: MusicImmersiveHeaderRenderer?,
val musicDetailHeaderRenderer: MusicDetailHeaderRenderer?,
+ val musicEditablePlaylistDetailHeaderRenderer: MusicEditablePlaylistDetailHeaderRenderer?,
val musicVisualHeaderRenderer: MusicVisualHeaderRenderer?,
val musicHeaderRenderer: MusicHeaderRenderer?,
) {
@@ -62,6 +71,16 @@ data class BrowseResponse(
val menu: Menu,
)
+ @Serializable
+ data class MusicEditablePlaylistDetailHeaderRenderer(
+ val header: Header,
+ ) {
+ @Serializable
+ data class Header(
+ val musicDetailHeaderRenderer: MusicDetailHeaderRenderer,
+ )
+ }
+
@Serializable
data class MusicVisualHeaderRenderer(
val title: Runs,
diff --git a/innertube/src/main/java/com/zionhuang/innertube/pages/ArtistItemsPage.kt b/innertube/src/main/java/com/zionhuang/innertube/pages/ArtistItemsPage.kt
index bb4866d66..04a7d6dca 100644
--- a/innertube/src/main/java/com/zionhuang/innertube/pages/ArtistItemsPage.kt
+++ b/innertube/src/main/java/com/zionhuang/innertube/pages/ArtistItemsPage.kt
@@ -1,6 +1,15 @@
package com.zionhuang.innertube.pages
-import com.zionhuang.innertube.models.*
+import com.zionhuang.innertube.models.Album
+import com.zionhuang.innertube.models.AlbumItem
+import com.zionhuang.innertube.models.Artist
+import com.zionhuang.innertube.models.MusicResponsiveListItemRenderer
+import com.zionhuang.innertube.models.MusicTwoRowItemRenderer
+import com.zionhuang.innertube.models.PlaylistItem
+import com.zionhuang.innertube.models.SongItem
+import com.zionhuang.innertube.models.YTItem
+import com.zionhuang.innertube.models.oddElements
+import com.zionhuang.innertube.models.splitBySeparator
import com.zionhuang.innertube.utils.parseTime
data class ArtistItemsPage(
@@ -48,7 +57,7 @@ data class ArtistItemsPage(
?.watchPlaylistEndpoint?.playlistId ?: return null,
title = renderer.title.runs?.firstOrNull()?.text ?: return null,
artists = null,
- year = renderer.subtitle.runs?.lastOrNull()?.text?.toIntOrNull(),
+ year = renderer.subtitle?.runs?.lastOrNull()?.text?.toIntOrNull(),
thumbnail = renderer.thumbnailRenderer.musicThumbnailRenderer?.getThumbnailUrl() ?: return null,
explicit = renderer.subtitleBadges?.find {
it.musicInlineBadgeRenderer.icon.iconType == "MUSIC_EXPLICIT_BADGE"
@@ -58,7 +67,7 @@ data class ArtistItemsPage(
renderer.isSong -> SongItem(
id = renderer.navigationEndpoint.watchEndpoint?.videoId ?: return null,
title = renderer.title.runs?.firstOrNull()?.text ?: return null,
- artists = renderer.subtitle.runs?.splitBySeparator()?.firstOrNull()?.oddElements()?.map {
+ artists = renderer.subtitle?.runs?.splitBySeparator()?.firstOrNull()?.oddElements()?.map {
Artist(
name = it.text,
id = it.navigationEndpoint?.browseEndpoint?.browseId
@@ -72,19 +81,19 @@ data class ArtistItemsPage(
renderer.isPlaylist -> PlaylistItem(
id = renderer.navigationEndpoint.browseEndpoint?.browseId?.removePrefix("VL") ?: return null,
title = renderer.title.runs?.firstOrNull()?.text ?: return null,
- author = renderer.subtitle.runs?.getOrNull(2)?.let {
+ author = renderer.subtitle?.runs?.getOrNull(2)?.let {
Artist(
name = it.text,
id = it.navigationEndpoint?.browseEndpoint?.browseId
)
- } ?: return null,
- songCountText = renderer.subtitle.runs.getOrNull(4)?.text,
+ },
+ songCountText = renderer.subtitle?.runs?.getOrNull(4)?.text,
thumbnail = renderer.thumbnailRenderer.musicThumbnailRenderer?.getThumbnailUrl() ?: return null,
playEndpoint = renderer.thumbnailOverlay
?.musicItemThumbnailOverlayRenderer?.content
?.musicPlayButtonRenderer?.playNavigationEndpoint
?.watchPlaylistEndpoint ?: return null,
- shuffleEndpoint = renderer.menu.menuRenderer.items.find {
+ shuffleEndpoint = renderer.menu?.menuRenderer?.items?.find {
it.menuNavigationItemRenderer?.icon?.iconType == "MUSIC_SHUFFLE"
}?.menuNavigationItemRenderer?.navigationEndpoint?.watchPlaylistEndpoint ?: return null,
radioEndpoint = renderer.menu.menuRenderer.items.find {
diff --git a/innertube/src/main/java/com/zionhuang/innertube/pages/ArtistPage.kt b/innertube/src/main/java/com/zionhuang/innertube/pages/ArtistPage.kt
index 0e1ba7d7e..fec6bdb54 100644
--- a/innertube/src/main/java/com/zionhuang/innertube/pages/ArtistPage.kt
+++ b/innertube/src/main/java/com/zionhuang/innertube/pages/ArtistPage.kt
@@ -1,6 +1,19 @@
package com.zionhuang.innertube.pages
-import com.zionhuang.innertube.models.*
+import com.zionhuang.innertube.models.Album
+import com.zionhuang.innertube.models.AlbumItem
+import com.zionhuang.innertube.models.Artist
+import com.zionhuang.innertube.models.ArtistItem
+import com.zionhuang.innertube.models.BrowseEndpoint
+import com.zionhuang.innertube.models.MusicCarouselShelfRenderer
+import com.zionhuang.innertube.models.MusicResponsiveListItemRenderer
+import com.zionhuang.innertube.models.MusicShelfRenderer
+import com.zionhuang.innertube.models.MusicTwoRowItemRenderer
+import com.zionhuang.innertube.models.PlaylistItem
+import com.zionhuang.innertube.models.SectionListRenderer
+import com.zionhuang.innertube.models.SongItem
+import com.zionhuang.innertube.models.YTItem
+import com.zionhuang.innertube.models.oddElements
data class ArtistSection(
val title: String,
@@ -76,7 +89,7 @@ data class ArtistPage(
SongItem(
id = renderer.navigationEndpoint.watchEndpoint?.videoId ?: return null,
title = renderer.title.runs?.firstOrNull()?.text ?: return null,
- artists = listOfNotNull(renderer.subtitle.runs?.firstOrNull()?.let {
+ artists = listOfNotNull(renderer.subtitle?.runs?.firstOrNull()?.let {
Artist(
name = it.text,
id = it.navigationEndpoint?.browseEndpoint?.browseId
@@ -90,29 +103,30 @@ data class ArtistPage(
} != null
)
}
+
renderer.isAlbum -> {
AlbumItem(
browseId = renderer.navigationEndpoint.browseEndpoint?.browseId ?: return null,
- playlistId = renderer.thumbnailOverlay
- ?.musicItemThumbnailOverlayRenderer?.content
+ playlistId = renderer.thumbnailOverlay?.musicItemThumbnailOverlayRenderer?.content
?.musicPlayButtonRenderer?.playNavigationEndpoint
?.watchPlaylistEndpoint?.playlistId ?: return null,
title = renderer.title.runs?.firstOrNull()?.text ?: return null,
artists = null,
- year = renderer.subtitle.runs?.lastOrNull()?.text?.toIntOrNull(),
+ year = renderer.subtitle?.runs?.lastOrNull()?.text?.toIntOrNull(),
thumbnail = renderer.thumbnailRenderer.musicThumbnailRenderer?.getThumbnailUrl() ?: return null,
explicit = renderer.subtitleBadges?.find {
it.musicInlineBadgeRenderer.icon.iconType == "MUSIC_EXPLICIT_BADGE"
} != null
)
}
+
renderer.isPlaylist -> {
// Playlist from YouTube Music
PlaylistItem(
id = renderer.navigationEndpoint.browseEndpoint?.browseId?.removePrefix("VL") ?: return null,
title = renderer.title.runs?.firstOrNull()?.text ?: return null,
author = Artist(
- name = renderer.subtitle.runs?.lastOrNull()?.text ?: return null,
+ name = renderer.subtitle?.runs?.lastOrNull()?.text ?: return null,
id = null
),
songCountText = null,
@@ -121,7 +135,7 @@ data class ArtistPage(
?.musicItemThumbnailOverlayRenderer?.content
?.musicPlayButtonRenderer?.playNavigationEndpoint
?.watchPlaylistEndpoint ?: return null,
- shuffleEndpoint = renderer.menu.menuRenderer.items.find {
+ shuffleEndpoint = renderer.menu?.menuRenderer?.items?.find {
it.menuNavigationItemRenderer?.icon?.iconType == "MUSIC_SHUFFLE"
}?.menuNavigationItemRenderer?.navigationEndpoint?.watchPlaylistEndpoint ?: return null,
radioEndpoint = renderer.menu.menuRenderer.items.find {
@@ -129,12 +143,13 @@ data class ArtistPage(
}?.menuNavigationItemRenderer?.navigationEndpoint?.watchPlaylistEndpoint ?: return null
)
}
+
renderer.isArtist -> {
ArtistItem(
id = renderer.navigationEndpoint.browseEndpoint?.browseId ?: return null,
title = renderer.title.runs?.lastOrNull()?.text ?: return null,
thumbnail = renderer.thumbnailRenderer.musicThumbnailRenderer?.getThumbnailUrl() ?: return null,
- shuffleEndpoint = renderer.menu.menuRenderer.items.find {
+ shuffleEndpoint = renderer.menu?.menuRenderer?.items?.find {
it.menuNavigationItemRenderer?.icon?.iconType == "MUSIC_SHUFFLE"
}?.menuNavigationItemRenderer?.navigationEndpoint?.watchPlaylistEndpoint ?: return null,
radioEndpoint = renderer.menu.menuRenderer.items.find {
@@ -142,6 +157,7 @@ data class ArtistPage(
}?.menuNavigationItemRenderer?.navigationEndpoint?.watchPlaylistEndpoint ?: return null,
)
}
+
else -> null
}
}
diff --git a/innertube/src/main/java/com/zionhuang/innertube/pages/BrowseResult.kt b/innertube/src/main/java/com/zionhuang/innertube/pages/BrowseResult.kt
new file mode 100644
index 000000000..2099183ec
--- /dev/null
+++ b/innertube/src/main/java/com/zionhuang/innertube/pages/BrowseResult.kt
@@ -0,0 +1,13 @@
+package com.zionhuang.innertube.pages
+
+import com.zionhuang.innertube.models.YTItem
+
+data class BrowseResult(
+ val title: String?,
+ val items: List- ,
+) {
+ data class Item(
+ val title: String?,
+ val items: List,
+ )
+}
diff --git a/innertube/src/main/java/com/zionhuang/innertube/pages/ExplorePage.kt b/innertube/src/main/java/com/zionhuang/innertube/pages/ExplorePage.kt
new file mode 100644
index 000000000..faee829de
--- /dev/null
+++ b/innertube/src/main/java/com/zionhuang/innertube/pages/ExplorePage.kt
@@ -0,0 +1,8 @@
+package com.zionhuang.innertube.pages
+
+import com.zionhuang.innertube.models.AlbumItem
+
+data class ExplorePage(
+ val newReleaseAlbums: List,
+ val moodAndGenres: List,
+)
diff --git a/innertube/src/main/java/com/zionhuang/innertube/pages/MoodAndGenres.kt b/innertube/src/main/java/com/zionhuang/innertube/pages/MoodAndGenres.kt
new file mode 100644
index 000000000..ecaed16e4
--- /dev/null
+++ b/innertube/src/main/java/com/zionhuang/innertube/pages/MoodAndGenres.kt
@@ -0,0 +1,36 @@
+package com.zionhuang.innertube.pages
+
+import com.zionhuang.innertube.models.BrowseEndpoint
+import com.zionhuang.innertube.models.GridRenderer
+import com.zionhuang.innertube.models.MusicNavigationButtonRenderer
+import com.zionhuang.innertube.models.SectionListRenderer
+
+data class MoodAndGenres(
+ val title: String,
+ val items: List
- ,
+) {
+ data class Item(
+ val title: String,
+ val stripeColor: Long,
+ val endpoint: BrowseEndpoint,
+ )
+
+ companion object {
+ fun fromSectionListRendererContent(content: SectionListRenderer.Content): MoodAndGenres? {
+ return MoodAndGenres(
+ title = content.gridRenderer?.header?.gridHeaderRenderer?.title?.runs?.firstOrNull()?.text ?: return null,
+ items = content.gridRenderer.items
+ .mapNotNull(GridRenderer.Item::musicNavigationButtonRenderer)
+ .mapNotNull(::fromMusicNavigationButtonRenderer)
+ )
+ }
+
+ fun fromMusicNavigationButtonRenderer(renderer: MusicNavigationButtonRenderer): Item? {
+ return Item(
+ title = renderer.buttonText.runs?.firstOrNull()?.text ?: return null,
+ stripeColor = renderer.solid?.leftStripeColor ?: return null,
+ endpoint = renderer.clickCommand.browseEndpoint ?: return null,
+ )
+ }
+ }
+}
diff --git a/innertube/src/main/java/com/zionhuang/innertube/pages/NewReleaseAlbumPage.kt b/innertube/src/main/java/com/zionhuang/innertube/pages/NewReleaseAlbumPage.kt
index fbf840d61..04d8d5c58 100644
--- a/innertube/src/main/java/com/zionhuang/innertube/pages/NewReleaseAlbumPage.kt
+++ b/innertube/src/main/java/com/zionhuang/innertube/pages/NewReleaseAlbumPage.kt
@@ -1,6 +1,10 @@
package com.zionhuang.innertube.pages
-import com.zionhuang.innertube.models.*
+import com.zionhuang.innertube.models.AlbumItem
+import com.zionhuang.innertube.models.Artist
+import com.zionhuang.innertube.models.MusicTwoRowItemRenderer
+import com.zionhuang.innertube.models.oddElements
+import com.zionhuang.innertube.models.splitBySeparator
object NewReleaseAlbumPage {
fun fromMusicTwoRowItemRenderer(renderer: MusicTwoRowItemRenderer): AlbumItem? {
@@ -11,7 +15,7 @@ object NewReleaseAlbumPage {
?.musicPlayButtonRenderer?.playNavigationEndpoint
?.watchPlaylistEndpoint?.playlistId ?: return null,
title = renderer.title.runs?.firstOrNull()?.text ?: return null,
- artists = renderer.subtitle.runs?.splitBySeparator()?.getOrNull(1)?.oddElements()?.map {
+ artists = renderer.subtitle?.runs?.splitBySeparator()?.getOrNull(1)?.oddElements()?.map {
Artist(
name = it.text,
id = it.navigationEndpoint?.browseEndpoint?.browseId
diff --git a/innertube/src/main/java/com/zionhuang/innertube/pages/RelatedPage.kt b/innertube/src/main/java/com/zionhuang/innertube/pages/RelatedPage.kt
index 9460c9350..3272f54f5 100644
--- a/innertube/src/main/java/com/zionhuang/innertube/pages/RelatedPage.kt
+++ b/innertube/src/main/java/com/zionhuang/innertube/pages/RelatedPage.kt
@@ -1,6 +1,15 @@
package com.zionhuang.innertube.pages
-import com.zionhuang.innertube.models.*
+import com.zionhuang.innertube.models.Album
+import com.zionhuang.innertube.models.AlbumItem
+import com.zionhuang.innertube.models.Artist
+import com.zionhuang.innertube.models.ArtistItem
+import com.zionhuang.innertube.models.MusicResponsiveListItemRenderer
+import com.zionhuang.innertube.models.MusicTwoRowItemRenderer
+import com.zionhuang.innertube.models.PlaylistItem
+import com.zionhuang.innertube.models.SongItem
+import com.zionhuang.innertube.models.YTItem
+import com.zionhuang.innertube.models.oddElements
data class RelatedPage(
val songs: List,
@@ -44,7 +53,7 @@ data class RelatedPage(
?.watchPlaylistEndpoint?.playlistId ?: return null,
title = renderer.title.runs?.firstOrNull()?.text ?: return null,
artists = null,
- year = renderer.subtitle.runs?.lastOrNull()?.text?.toIntOrNull(),
+ year = renderer.subtitle?.runs?.lastOrNull()?.text?.toIntOrNull(),
thumbnail = renderer.thumbnailRenderer.musicThumbnailRenderer?.getThumbnailUrl() ?: return null,
explicit = renderer.subtitleBadges?.find {
it.musicInlineBadgeRenderer.icon.iconType == "MUSIC_EXPLICIT_BADGE"
@@ -53,19 +62,19 @@ data class RelatedPage(
renderer.isPlaylist -> PlaylistItem(
id = renderer.navigationEndpoint.browseEndpoint?.browseId?.removePrefix("VL") ?: return null,
title = renderer.title.runs?.firstOrNull()?.text ?: return null,
- author = renderer.subtitle.runs?.getOrNull(2)?.let {
+ author = renderer.subtitle?.runs?.getOrNull(2)?.let {
Artist(
name = it.text,
id = it.navigationEndpoint?.browseEndpoint?.browseId
)
- } ?: return null,
- songCountText = renderer.subtitle.runs.getOrNull(4)?.text,
+ },
+ songCountText = renderer.subtitle?.runs?.getOrNull(4)?.text,
thumbnail = renderer.thumbnailRenderer.musicThumbnailRenderer?.getThumbnailUrl() ?: return null,
playEndpoint = renderer.thumbnailOverlay
?.musicItemThumbnailOverlayRenderer?.content
?.musicPlayButtonRenderer?.playNavigationEndpoint
?.watchPlaylistEndpoint ?: return null,
- shuffleEndpoint = renderer.menu.menuRenderer.items.find {
+ shuffleEndpoint = renderer.menu?.menuRenderer?.items?.find {
it.menuNavigationItemRenderer?.icon?.iconType == "MUSIC_SHUFFLE"
}?.menuNavigationItemRenderer?.navigationEndpoint?.watchPlaylistEndpoint ?: return null,
radioEndpoint = renderer.menu.menuRenderer.items.find {
@@ -77,7 +86,7 @@ data class RelatedPage(
id = renderer.navigationEndpoint.browseEndpoint?.browseId ?: return null,
title = renderer.title.runs?.firstOrNull()?.text ?: return null,
thumbnail = renderer.thumbnailRenderer.musicThumbnailRenderer?.getThumbnailUrl() ?: return null,
- shuffleEndpoint = renderer.menu.menuRenderer.items.find {
+ shuffleEndpoint = renderer.menu?.menuRenderer?.items?.find {
it.menuNavigationItemRenderer?.icon?.iconType == "MUSIC_SHUFFLE"
}?.menuNavigationItemRenderer?.navigationEndpoint?.watchPlaylistEndpoint ?: return null,
radioEndpoint = renderer.menu.menuRenderer.items.find {
diff --git a/innertube/src/main/java/com/zionhuang/innertube/utils/Utils.kt b/innertube/src/main/java/com/zionhuang/innertube/utils/Utils.kt
index 8f63cc50b..2e727f0e6 100644
--- a/innertube/src/main/java/com/zionhuang/innertube/utils/Utils.kt
+++ b/innertube/src/main/java/com/zionhuang/innertube/utils/Utils.kt
@@ -26,10 +26,12 @@ fun ByteArray.toHex(): String = joinToString(separator = "") { eachByte -> "%02x
fun sha1(str: String): String = MessageDigest.getInstance("SHA-1").digest(str.toByteArray()).toHex()
fun parseCookieString(cookie: String): Map =
- cookie.split("; ").associate {
- val (key, value) = it.split("=")
- key to value
- }
+ cookie.split("; ")
+ .filter { it.isNotEmpty() }
+ .associate {
+ val (key, value) = it.split("=")
+ key to value
+ }
fun String.parseTime(): Int? {
try {