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