From db454bce27543245adf482fbfd615b9e79a32a10 Mon Sep 17 00:00:00 2001 From: Dae Euhwa Date: Wed, 24 Jun 2026 01:44:02 -0500 Subject: [PATCH 01/73] Add Japanese translations New values-ja/ resource directory with all string categories translated. --- app/src/main/res/values-ja/strings.xml | 128 ++++ .../main/res/values-ja/strings_changelogs.xml | 9 + .../res/values-ja/strings_cloud_services.xml | 226 ++++++ .../main/res/values-ja/strings_equalizer.xml | 57 ++ .../res/values-ja/strings_home_screen.xml | 276 ++++++++ .../main/res/values-ja/strings_library.xml | 558 +++++++++++++++ app/src/main/res/values-ja/strings_player.xml | 195 ++++++ .../main/res/values-ja/strings_screens.xml | 244 +++++++ .../main/res/values-ja/strings_settings.xml | 641 ++++++++++++++++++ app/src/main/res/values-ja/strings_widget.xml | 17 + 10 files changed, 2351 insertions(+) create mode 100644 app/src/main/res/values-ja/strings.xml create mode 100644 app/src/main/res/values-ja/strings_changelogs.xml create mode 100644 app/src/main/res/values-ja/strings_cloud_services.xml create mode 100644 app/src/main/res/values-ja/strings_equalizer.xml create mode 100644 app/src/main/res/values-ja/strings_home_screen.xml create mode 100644 app/src/main/res/values-ja/strings_library.xml create mode 100644 app/src/main/res/values-ja/strings_player.xml create mode 100644 app/src/main/res/values-ja/strings_screens.xml create mode 100644 app/src/main/res/values-ja/strings_settings.xml create mode 100644 app/src/main/res/values-ja/strings_widget.xml diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml new file mode 100644 index 0000000000..5dedb3c73a --- /dev/null +++ b/app/src/main/res/values-ja/strings.xml @@ -0,0 +1,128 @@ + + + PixelPlayer + 音楽プレイヤー + アプリ名の変更について + 商標上の理由により、アプリ名を PixelPlay から PixelPlayer に変更しました。引き続きお楽しみください! + 今後表示しない + + + ホーム + 検索 + ライブラリ + + + 特別な権限が必要です + 曲のメタデータ(.mp3 ファイル)を編集するには、PixelPlayer にすべてのファイルへの特別なアクセス権限が必要です。これにより、トラックのタグを直接変更できます。メタデータ編集を有効にするには、次の画面でこの権限を許可してください。 + 権限を許可 + + + すぐに再生 + このオーディオファイルを開けませんでした。 + フルプレイヤーを開く + + + シャッフル + すべての曲をシャッフル + すべてシャッフル + 最後のプレイリスト + 開けるプレイリストがありません + + + Play ストアを開く + ベータを続ける + Play ストアのリンクは GitHub の設定から有効化されます。 + PixelPlayer が Google Play で公開されました + リリース更新は Google Play の安定版チャンネルをご利用ください。ベータビルドも引き続き提供されます。 + PixelPlayer + リリースのお知らせ + 近日公開 + + + PixelPlayer をご利用いただきありがとうございます! + ハイスコア %1$d + 閉じる + スコア + レベル %1$d + ライフ + レベルクリア! + ゲームオーバー + スコア: %1$d + もう一度? + 次のレベル + ゲームを再起動 + タップして再起動 + ランダムに音楽を再生 + ブロック崩し + ハイスコア %1$d + プレイ + ドラッグしてパドルを動かす + + + プレイヤーを閉じる + 再生操作を処理中… + 再生エラー: %1$s + + + 戻る + OK + キャンセル + 閉じる + エラー + 検索 + 検索をクリア + すべて + 確認 + 保存しました! + 選択済み + %1$d%% + アーティスト + すべて選択 + クリア + 不明なエラー + + + 保存 + 完了 + リセット + 適用 + シャッフル + コピー + 共有 + 元に戻す + インポート + 削除 + エクスポート + 結合 + 名前を変更 + 作成 + 歌詞 + 設定 + アルバムアート + プレイリスト + 不明なトラック + 不明なアーティスト + 不明なアルバム + 閉じる + 追加 + 削除 + 再生 + 前のトラック + 次のトラック + お気に入り + 一時停止 + リピート + オプション + シャッフル再生 + %1$s のその他のオプション + メニューを展開 + 次へ + 完了 + デフォルトに戻す + すべてエクスポート + すべて結合 + すべて共有 + アルバムを再生 + アルバムをシャッフル再生 + %1$s のアルバムアート + diff --git a/app/src/main/res/values-ja/strings_changelogs.xml b/app/src/main/res/values-ja/strings_changelogs.xml new file mode 100644 index 0000000000..85544d5ba6 --- /dev/null +++ b/app/src/main/res/values-ja/strings_changelogs.xml @@ -0,0 +1,9 @@ + + + 変更履歴 + GitHub で見る + 改善 + 修正 + 新機能 + 追加 + diff --git a/app/src/main/res/values-ja/strings_cloud_services.xml b/app/src/main/res/values-ja/strings_cloud_services.xml new file mode 100644 index 0000000000..a2e5bac754 --- /dev/null +++ b/app/src/main/res/values-ja/strings_cloud_services.xml @@ -0,0 +1,226 @@ + + + + Telegram ログイン + 番号を編集中です。再送すると前のコードが無効になります。 + 処理中… + Telegram を初期化中… + ログアウト中… + セッションを閉じています… + セッションが閉じました。続けるにはログインを再度開いてください。 + 安全な Telegram セッションを準備中… + Telegram からの応答を待機中… + Telegram に接続 + Telegram に接続してチャンネルやチャットから音楽をストリーミングします。 + 電話番号 + Telegram の番号を入力してください。後で戻って編集することもできます。 + 電話番号 + 81 + 09012345678 + コードを送信 + 確認コード + Telegram からのコードを入力してください。番号が間違っている場合は戻って修正してください。 + コード + 12345 + 電話番号を編集 + コードを再送 + コードを確認 + 二段階認証パスワード + Telegram のパスワードを入力してください。番号を修正するために戻ることもできます。 + パスワード + パスワードを確認 + しばらくお待ちください… + + + Telegram チャンネル + チャンネルを追加 + Telegram パブリックチャンネル + 同期中 + 今すぐ同期 + トピックを折りたたむ + トピックを表示 + チャンネルオプション + トピック + チャンネルを同期中 + Telegram から曲を更新中 + このチャンネルから最新の曲を取得 + チャンネルを削除 + 同期を停止してキャッシュされた曲を削除 + チャンネルを削除しますか? + %1$s の同期が停止し、このチャンネルのキャッシュされた曲がすべて削除されます。 + 削除 + 同期済みチャンネルがありません + Telegram のパブリックチャンネルを追加して\n音楽ライブラリを同期しましょう + チャンネルを追加 + 未同期 + %1$s に同期 + + + チャンネルを追加 + 音楽を同期する Telegram パブリックチャンネルを検索 + \@チャンネル名またはリンク + 検索中… + チャンネルを検索 + パブリックチャンネルのユーザー名またはリンクを入力して\nオーディオファイルを同期してください + + + %d 曲 + + + %d トピック + + + + Subsonic + Navidrome、Airsonic などの Subsonic 互換サーバーを管理します。 + + + 同期をタップして Jellyfin のプレイリストを取得してください + Jellyfin サーバーの接続を管理します。 + + + 音楽フォルダ + + をタップして Drive フォルダを追加 + フォルダがまだ追加されていません + %1$d フォルダが同期済み + フォルダを追加 + + + プレイリストの種類を選択 + 同期するプレイリストを選択: + すべてのプレイリスト + 作成 & お気に入り + 作成したプレイリスト + お気に入りのプレイリスト + + + %1$d プレイリストが同期済み + プレイリスト + 同期 + まだプレイリストが同期されていません + 同期をタップしてプレイリストを取得してください + クイックアクション + ライブラリを同期 + 切断 + %1$d 曲 + + + 同期中 + ライブラリを同期中… + プレイリストを取得中… + プレイリストを同期中: %1$s + ローカルライブラリを更新中… + 同期完了 + アルバムリストを取得中… + %1$s から曲を取得中… + %1$d 曲をデータベースに保存中… + ライブラリに曲が見つかりません + ライブラリ同期完了 + 同期中… + エラー: %1$s + + + 同期 + すべて同期 + ログアウト + すべてのプレイリストを同期 + ユーザーアバター + + + インターネット接続がありません + このコンテンツにはインターネット接続が必要です。ネットワーク設定を確認して再試行してください。 + オフラインです + このコンテンツにアクセスするにはインターネット接続を確認して再試行してください。 + + + 接続 + 接続中… + サーバー URL とアカウントの認証情報を入力してください。 + 接続詳細 + パスワードを非表示 + パスワード + パスワードを入力 + http:// を入力 + サーバー URL + パスワードを表示 + Telegram + ユーザー名 + admin + ようこそ、%1$s! + + + Navidrome、Gonic、Airsonic などの Subsonic 互換サーバーに対応 + Navidrome、Airsonic、Gonic、Ampache などの Subsonic API 互換サーバーをサポートします。 + サーバーが対応している場合はアプリパスワードも使用できます。 + https:// を入力 + セルフホスト型音楽サーバーに接続 + Navidrome + サーバーの完全な https:// ベースアドレスを使用してください。 + https://music.example.com + Subsonic または Navidrome のアカウント名です。 + Subsonic / Navidrome + Subsonic + + + Jellyfin サーバー URL とアカウントの認証情報を入力してください。 + 音楽ライブラリをストリーミングするために Jellyfin サーバーに接続します + Jellyfin サーバーに接続します。ローカルネットワークアクセスには HTTP と HTTPS の両方がサポートされています。 + Jellyfin + Jellyfin アカウントのパスワード。 + Jellyfin メディアサーバーに接続 + Jellyfin + ポートを含む Jellyfin サーバーの完全な URL。 + http://192.168.1.100:8096 + Jellyfin アカウントのユーザー名。 + + + Google Drive から直接音楽ファイルをストリーミング + Google Drive に接続 + Google Drive に接続しました! + 「PixelPlayer Music」を作成 + ここに音楽用の新しいフォルダを作成 + フォルダがありません + フォルダを開く + 音楽ソースとして使用するフォルダを選択または作成 + 音楽フォルダを選択 + Google Drive をセットアップ中… + Google でサインイン + Google Drive + 使用 + + + セッション Cookie を読み取れませんでした。 + 完了 + 終了 + Cookie が見つかりません。先にログインしてください。 + ページの読み込みに時間がかかっています。更新するか別のネットワークをお試しください。 + + + 保存中… + 残る + ページの読み込みがタイムアウトしました。進捗を失わずに再試行できます。 + Web で戻る + 後で戻れます。閉じると現在のページの状態は破棄されます。 + Web で進む + 更新 + 再試行 + ホームを開く + WebView の読み込みに失敗しました。 + + + NetEase の Cookie を読み取れませんでした: %1$s + NetEase のログインを終了しますか? + NetEase の読み込み中に HTTP %1$d エラーが発生しました。 + まだログインが検出されていません。完了を押す前に NetEase のログインを完了してください。 + NetEase Music にログイン + セキュリティについて: パスワードは NetEase のウェブページにのみ入力されます。PixelPlayer はライブラリを同期するためにセッション Cookie(MUSIC_U)を保存します。 + NetEase Music + + + QQ Music の Cookie を読み取れませんでした: %1$s + QQ Music のログインを終了しますか? + QQ Music の読み込み中に HTTP %1$d エラーが発生しました。 + まだログインが検出されていません。完了を押す前に QQ Music のログインを完了してください。 + QQ Music にログイン + セキュリティについて: パスワードは QQ Music のウェブページにのみ入力されます。PixelPlayer はライブラリを同期するためにセッション Cookie を保存します。 + QQ Music + diff --git a/app/src/main/res/values-ja/strings_equalizer.xml b/app/src/main/res/values-ja/strings_equalizer.xml new file mode 100644 index 0000000000..3c36811ada --- /dev/null +++ b/app/src/main/res/values-ja/strings_equalizer.xml @@ -0,0 +1,57 @@ + + + + 名前を入力してください + 名前を変更 + + + 表示モードを変更 + イコライザーを無効化 + イコライザーを有効化 + 編集 + プリセットを編集 + カスタムプリセット + プリセット + 更新 + バスブースト + バーチャライザー + ラウドネス + 非対応 + この端末では非対応 + 音量 + 周波数特性 + Hz + バス + ローミッド + ハイミッド + トレブル + バス / ロー + ミッド / ハイ + ページ %1$d + 時間をリセット + 新規保存 + + + 保存済みプリセット + カスタムプリセットがまだ保存されていません。 + ピンを外す + ピン留め + 名前を変更 + 削除 + + + カスタムプリセットを保存 + カスタムイコライザープリセットの名前を入力してください。 + プリセット名 + プリセット名を変更 + + + プリセットを管理 + ドラッグして並び替え • 目のアイコンで表示/非表示を切り替え + 並び替え + プリセットをリセット + デフォルトのプリセット順と表示状態に戻します。続けますか? + デフォルトに戻す + 表示 + 非表示 + diff --git a/app/src/main/res/values-ja/strings_home_screen.xml b/app/src/main/res/values-ja/strings_home_screen.xml new file mode 100644 index 0000000000..ac2021f3e9 --- /dev/null +++ b/app/src/main/res/values-ja/strings_home_screen.xml @@ -0,0 +1,276 @@ + + + + β + ベータ + クラウドストリーミング + 変更履歴 + クラウドストリーミング + クラウドアカウントから音楽をストリーミング + + + Beta 0.7.0 + β + PixelPlayer 0.7.0-beta へようこそ + バグ、クラッシュ、または試験的な機能が含まれている可能性があるベータビルドを使用しています。問題を報告して改善にご協力ください。 + 期待されること + バグ、クラッシュ、または未完成の機能が予期せず発生することがあります。 + 一部の機能は予告なく変更または削除される場合があります。 + ベータビルドはリリース版より不安定な場合があります。 + 既知の問題を報告する前に必ず最新版を確認してください。 + テスト中にベータビルドが変更、破損、または改善される可能性があること。 + GitHub Issue のショートカット + まず検索してから、バグ、クラッシュ、要望、質問に対する集中したレポートを作成してください。 + 既存の Issue を開く + Issue またはクラッシュを報告 + 再現手順、期待される結果、実際の結果、デバイス/OS の詳細を共有してください。 + 報告方法 + 新しい Issue を開く前の簡単なチェックリスト。 + Issue を開く前に + 重複を避けるために既存のオープンおよびクローズ済みの Issue を検索してください。 + 最新の PixelPlayer バージョンに更新して問題が引き続き発生することを確認してください。 + アプリを再起動して問題が続くことを確認してください。 + 再現を試みて正確な手順を書き留めてください。 + Issue の種類は? + バグ報告: 何かが正しく動作しない。 + 機能リクエスト: 新機能や改善の追加。 + 質問: Discussions が有効な場合はそちらを使用するか、question ラベルで Issue を開いてください。 + バグ報告 + 何かが正しく動作しないまたはクラッシュする場合にこれらのフィールドをコピーしてください。 + バグ報告 + 概要: + 期待される動作: + 現在の動作: + 再現手順: 1. 2. 3. + 頻度は? 常時 / 時々 / まれに。 + スクリーンショット / 動画: あれば。 + ログ / スタックトレース: あれば。 + 環境 + PixelPlayer バージョン: + インストール元: GitHub リリース、デバッグビルド、ナイトリービルドなど。 + Android バージョン: + 端末モデル: + 補足情報: SD カードの使用、特別な設定、権限など。 + 機能リクエスト + 新機能や改善を要望する場合にこれらのフィールドをコピーしてください。 + 問題の説明: 解決しようとしている問題は何ですか? + 提案する解決策: どのように機能すればよいですか? + 検討した代替案: 他のアプローチはありますか? + 範囲: どの画面やフローが影響を受けますか? + 利用可能であればモックアップや参考画像。 + タイトル、プライバシー、範囲 + 報告をトリアージしやすく安全に共有できるようにします。 + 良い Issue タイトルの例 + イコライザー: プリセットタブを切り替えるとインジケーターがずれる + 検索: 空のクエリで履歴リストが表示されない + 機能: 「最近追加された」プレイリストの並び替えオプションを追加 + 避けるべきこと + 「動かない」のような一般的な報告。 + 1 つの Issue に複数の無関係な問題を含める。 + プライベートデータが含まれた未編集のログやスクリーンショット。 + プライバシーについて + ログ、スクリーンショット、動画を投稿する前に個人情報やプライベートな情報を削除してください。 + ナイトリービルド + ナイトリーとリリースの違い、および破損した場合に含めるべき情報。 + ナイトリービルドは最新のコミットから生成され、未完成の変更、一時的なバグ、またはリグレッションが含まれる場合があります。公式リリースよりも試験的です。 + 利用可能な場合はリポジトリの GitHub Actions ワークフローアーティファクトからアクセスできます。 + ナイトリーの問題を報告する + ナイトリービルドで問題を報告する場合は、公式リリースではなくナイトリービルドで発生したことを必ず記載してください。可能であればビルド日、ワークフロー実行名または番号、コミット SHA を含めてください。また同じ問題が最新の公式リリースでも発生するか確認してください。 + Beta 0.5.0 アップグレード + クリーンインストール推奨 + beta 0.5.0 からのアップデートの場合、このアップデートでは古いキャッシュ状態ではなく新しいライブラリデータが必要な場合があります。 + メタデータやライブラリエントリがおかしい場合 + 曲のメタデータが間違っている、アーティストやアルバムが一致しない、または重複しているように見えるエントリは通常クリーンインストールで解決します。 + 今後表示しない + 了解 + + + 問題が発生しました + 前回のセッション中にアプリがクラッシュしました。クラッシュレポートを共有して修正にご協力ください。 + 日時: %1$s + エラー: + スタックトレース(プレビュー): + クラッシュログ + クラッシュログをクリップボードにコピーしました + PixelPlayer クラッシュレポート + クラッシュレポートを共有 + + + DJ ミキサー + + + あなたの\nミックス + まだ表示するデータがありません + PixelPlayer が曲を見つけるかソースを同期するとミックスがここに表示されます。 + 更新 + + + デイリーミックス + 履歴に基づく + デイリーミックスをすべて確認 + デイリーミックス + + + デイリーミックス + + %1$d 曲 • %2$s + + 再生する + AI プレイリストジェネレーター + + + デイリーミックスの作られ方 + デイリーミックスはお気に入りのよく再生される曲から作られます。好みのアーティストやジャンルのトラックも追加されるので新しい音楽を発見できます。 + 今日何を聴きたいか AI に伝えましょう + コストを抑えるため少量のサンプルを使用します + 更新中… + デイリーミックスを更新 + + + 完璧にキュレーション + デイリーミックス + あなたのソニックジャーニーの準備ができました + AI プレイリストジェネレーター + 雰囲気、ムード、アクティビティを説明して、ライブラリから AI に完璧なプレイリストをキュレーションさせましょう。 + プレイリストのサイズ + 最小曲数 + 最大曲数 + 例: チルな夜の雰囲気、アップビートなワークアウトエネルギー… + タップして再試行 + ソニックジャーニーが完成しました! + 再生準備完了 + 生成中… + プレイリストを生成 + + + 最近再生した曲 + + + 最近再生した曲 + 最新を再生 + %1$s に最近の再生はありません + 範囲を変更するか、タイムラインを埋めるためにもっと曲を再生してください。 + 最近再生した曲 + 今日 + 昨日 + + + リスニング統計 + 総再生回数 + 1 日平均 + トップトラック + %1$s • %2$d 回 + + + リスニング統計 + リスニング統計を更新 + 今日 + 今週 + 今月 + 今年 + 全期間 + リスニング + 再生 + リスニングタイムライン + リスニング時間 + 選択した範囲でのリスニングの合計。 + 再生回数 + セグメントごとに完了したセッション数。 + 平均セッション + 各セグメントの平均リスニング時間。 + 4 時間ごとに分割して日々のリズムを確認できます。 + 日別バーで週ごとの習慣を比較しやすくします。 + 週別バーで月のトレンドを確認できます。 + 月別バーで年間の季節性を確認できます。 + 年別バーで全履歴を要約します。 + まだリスニングデータがありません + 再生を始めてリスニングタイムラインを構築しましょう + 日々のリズム + 週のリズム + 月のリズム + 年間一覧 + 全期間の推移 + 4 時間ごとのセグメントでグループ化 + 曜日でグループ化 + 月の週でグループ化 + 月でグループ化 + 年でグループ化 + ピークセグメント + %1$d 回 + + トップカテゴリ + ジャンル、アーティスト、アルバム、曲ごとのリスニングを比較します。 + ジャンル + アーティスト + アルバム + + ジャンル別リスニング + アーティスト別リスニング + アルバム別リスニング + 曲別リスニング + %1$d 回 • %2$d アーティスト + %1$d 回 • %2$d トラック + まだカテゴリデータがありません + 再生を始めてリスニングのハイライトを確認しましょう + リスニング習慣 + まだ習慣データがありません + あなたのことをより知ったらリスニング習慣を表示します。 + 総セッション数 + 平均セッション + 最長セッション + セッション/日 + 最もアクティブな日 + まだ再生履歴がありません + ピークタイムラインスロット + トップアーティスト + トップアーティストがいません + 聴き続けるとお気に入りのアーティストがここに表示されます。 + \? + %1$d. %2$s + トップアルバム + トップアルバムがありません + よく聴くアルバムがここに表示されます。 + %1$d. %2$s + トラック集中度 + トップトラック全体でリスニング時間がどのように分散しているか。 + まだ集中度データがありません + より多くのトラックを再生してリスニングの集中度を確認しましょう。 + トップ 1 + トップ 2-3 + その他 + %1$d%% + リスニング集中度 + トップ 3 トラックがリスニング時間の %1$d%% を占めています。 + 平均再生回数/トラック + ユニークトラック + トップ 3 シェア + この期間のトラック + 選択した期間で最も再生されたトラック。 + トップトラックがありません + お気に入りを聴き続けるとここでハイライトされます。 + トラックを折りたたむ + すべてのトラックを表示 + + + %1$d 時間 %2$02d 分 + %1$d 分 + %1$d 時間 %2$02d 分 + %1$d 時間 + %1$d 分 + %1$d 秒 + %1$d 時間 %2$02d 分 + %1$d 時間 + %1$d 分 + %1$d 秒 + なし + たった今 + 1 日前 + %1$d 日前 + 1 時間前 + %1$d 時間前 + 1 分前 + %1$d 分前 + %1$d 曲 + %1$d 曲 + 第 %1$d 週 + diff --git a/app/src/main/res/values-ja/strings_library.xml b/app/src/main/res/values-ja/strings_library.xml new file mode 100644 index 0000000000..4c7c2d6ca2 --- /dev/null +++ b/app/src/main/res/values-ja/strings_library.xml @@ -0,0 +1,558 @@ + + + + ライブラリ + ライブラリタブ + 任意のタブへ直接ジャンプするか、順序を変更できます。 + タブを並び替え + + + + アルバム + アーティスト + プレイリスト + フォルダ + お気に入り + + + プレイリストを作成しました + 先に AI プロバイダーの API キーを設定してください + 先に Gemini API キーを設定してください + キューに追加しました + 次に再生 + + + Watch への転送 + 設定 + 編集 + タブを並び替え + メニューを展開 + + + 選択できるアルバムは最大 %1$d 枚です + フォルダ + フォルダ + + + 並び替え + 表示 + プレイリスト表示 + グリッド + リスト + 内部ストレージ + SD カード + SD カードは現在利用できません。 + クラウド + Telegram クラウドチャンネル + トピック表示 + チャンネル + トピック + 両方 + クラウド + クラウドのみ + + + AI でメタデータを生成中… + + + 曲の読み込みエラー + アルバムの読み込みエラー + アーティストの読み込みエラー + 再試行 + + + ライブラリに曲が見つかりませんでした。 + 端末に音楽がある場合は、設定からライブラリを再スキャンしてみてください。 + 曲が見つかりません + + + 新規 + 新しいプレイリストを作成 + M3U プレイリストをインポート + 現在の曲を探す + すべての曲 + クラウド + ローカル + 並び替えオプション + + + すべて + 選択解除 + その他のオプション + + + 音楽ファイルをスキャン中… + ファイルを処理中… + %2$d 件中 %1$d 件 + ライブラリを同期中… + 同期完了 + 待機中… + ライブラリを同期中… + アルバムアートキャッシュをクリア中… + クラウドソースを同期中… + 歌詞をスキャン中… + + + 曲がまだありません + 音楽を端末に追加するか、クラウドソースを同期して再生を始めましょう。 + ローカルの曲が見つかりません + 別のソースフィルターを試すか、端末のライブラリを再スキャンしてください。 + クラウドの曲が見つかりません + Telegram や NetEase の曲を同期するか、ローカルソースに切り替えてください。 + アルバムがありません + ライブラリにトラックがグループ化されるとアルバムが表示されます。 + ローカルアルバムが見つかりません + ローカルアルバムを作成するにはローカルの曲が必要です。 + クラウドアルバムが見つかりません + アルバムデータを持つクラウドの曲は同期後にここに表示されます。 + アーティストがいません + いずれかのソースから曲がインデックスされるとアーティストが表示されます。 + ローカルアーティストが見つかりません + ローカルの曲にアーティストのメタデータがありません。 + クラウドアーティストが見つかりません + リモートの曲が同期されるとクラウドアーティストが表示されます。 + お気に入りの曲がまだありません + 再生中にハートアイコンをタップして曲を保存しましょう。 + お気に入りのローカル曲がありません + ソースフィルターを切り替えるか、端末の曲をお気に入りに追加してください。 + お気に入りのクラウド曲がありません + Telegram や NetEase のトラックをお気に入りに追加するとここに表示されます。 + フォルダが見つかりません + 音楽が入った内部ストレージのフォルダがここに表示されます。 + プレイリストがまだありません + 最初のプレイリストを作成してライブラリを整理しましょう。 + + + 曲のメタデータを編集 + 再生 + 曲を再生 + すべて再生 + すべて再生 + お気に入りに追加 + すべてお気に入りに追加 + お気に入りから削除 + すべてお気に入りから削除 + 曲ファイルを共有するアプリを選択 + 曲ファイルを共有 + すべてを ZIP で共有 + 曲を共有できませんでした: %1$s + キューに追加 + キューに追加 + 次に再生 + キューで次に再生 + プレイリストに追加 + 削除 + すべて削除 + Watch を確認中 + 転送中 %1$d%% + Watch に転送中 + 転送中 + Watch に送る + Watch が利用できません + 曲を Watch に送る + Watch が利用できません + サウンドとして設定 + サウンドとして設定 + この曲をシステムサウンドとして使う方法を選択 + この曲を使う場所 + PixelPlayer がこのサウンドをインストールする場所を選択してください。 + 着信音 + 電話の着信 + 通知音 + メッセージとアプリの通知 + アラーム音 + 時計のアラーム + サウンドの変更を確認 + 「%1$s」を %2$s に設定しますか? + サウンドを設定 + 「%1$s」を %2$s に設定しました + 着信音 + 通知音 + アラーム音 + 「システム設定の変更」を有効にしてから PixelPlayer に戻ると自動で完了します。 + 「システム設定の変更」が有効になっていません。 + 「%1$s」を着信音に設定しました + 着信音にはローカルの曲のみ使用できます。 + この音声ファイルを着信音用に準備できませんでした。 + 着信音を設定できませんでした: %1$s + オプション + オプション + 情報 + 情報 + 再生時間 + ジャンル + アルバム + アーティスト + 曲の情報 + プロバイダー + ファイル + %1$d 曲 + 選択中 + %1$d プレイリスト + %1$d アルバム + 選択中 + 上限: %1$d アルバム + キューへの追加と再生は選択順序に従います。 + %1$d ジャンル + 選択中 + 選択したジャンル内のすべての曲に対して一括操作を実行します。 + + + デフォルト順 + タイトル(A〜Z) + タイトル(Z〜A) + アーティスト + アーティスト(Z〜A) + アルバム + アルバム(Z〜A) + 追加日 + 追加日(古い順) + 再生時間 + 再生時間(短い順) + リリース年 + リリース年(古い順) + 曲数が少ない順 + 曲数が多い順 + 名前(A〜Z) + 名前(Z〜A) + 曲数(多い順) + 曲数(少ない順) + 作成日 + 作成日(古い順) + お気に入り追加日 + お気に入り追加日(古い順) + サブフォルダが少ない順 + サブフォルダが多い順 + + + タイトル + アーティスト + アルバム + 追加日 + 再生時間 + リリース年 + 曲数 + 名前 + 曲数 + 作成日 + お気に入り追加日 + サブフォルダ数 + + + ソース + 順序 + 降順 + 昇順 + 元の順序 + タップして昇順に切り替え + タップして降順に切り替え + この並び替えは元の順序を維持します + スイッチがオン + + + ライブラリタブを並び替え + 順序をリセット + タブの順序をデフォルトに戻しますか? + タブを並び替え中… + ドラッグハンドル + + + アーティストを選択 + 1 アーティスト + %1$d アーティスト + メインアーティスト + アーティストページ + + + 転送をキャンセル + %1$s / %2$s + スマートフォンから Watch への音楽転送の進捗をリアルタイムで表示します + Watch への転送 + Watch に送信中 + キャンセル済み + 転送をキャンセルしました + 転送が完了しました + 完了 + 失敗 + 転送に失敗しました + 複数の転送が進行中 + %1$s • %2$s + 準備中 + Watch への転送を準備中 + 転送を準備中… + Watch に %1$d 曲を送信中 + Watch に送信中 + 転送を開始中… + 開始中 + 転送中 + %1$d 件の転送 + + + 曲を編集 + 情報を表示 + 曲のメタデータを編集中 + 曲のメタデータを編集すると、ライブラリでの表示や整理に影響することがあります。変更は永続的で、元に戻せない場合があります。 + 了解 + 情報 + カバーアート + 正方形の画像を選択して調整し、アプリ全体でカバーアートが美しく表示されるようにしましょう。 + カバーアートを変更 + カバーアートを削除 + タイトル + アーティスト + アルバム + アルバムアーティスト + ジャンル + 作曲者 + トラック番号 + ディスク番号 + ReplayGain トラック(dB) + ReplayGain アルバム(dB) + -6.50 + -8.20 + 新しいカバーアートのプレビュー + 現在の曲のカバーアート + カバーアートを調整 + ピンチとドラッグで最適なフレーミングを見つけてください。 + カバーアートを適用 + 選択した画像を読み込めませんでした + lrclib.net で歌詞を検索 + + + %d 曲を編集 + 変更したフィールドのみ更新されます。空白のフィールドは既存の値が保持されます。 + (複数の値) + (任意 — スキップする場合は空白のまま) + %d 曲を更新しました + %2$d 曲中 %1$d 曲を更新しました。一部のファイルは編集できませんでした。 + 曲の更新に失敗しました + カバーアートの一括変更 + 選択した %d 曲すべてのカバーアートが置き換えられます + すべてにカバーアートを設定 + すべてのカバーアートを削除 + (複数の異なるカバー) + + + プレイリストを閉じました + + + プレイリストを作成 + 作成方法を選択してください。 + 手動 + アートワーク・アイコン・形状をデザインし、曲を自分で選びます。 + AI で作成 + 高度なコントロールでキュレーションされたプレイリストを生成します。 + 設定で Gemini API キーを設定する必要があります。 + API キーを設定 + + + AI プレイリストラボ + リセット + 生成中… + 生成 + 意図 + プレイリスト名(任意) + このプレイリストの雰囲気は? + 例:夕暮れのドライブにウォームなシンセ + 方向性 + ムード + アクティビティ + 年代 + キュレーション + エネルギー + 曲の強度とテンポを調整します。1 = 穏やか/スロー、5 = ハイエネルギー/ファスト。 + ディスカバリー + 選曲の馴染み度を調整します。1 = 最もよく聴くお気に入り、5 = あまり聴いていないレアな曲。 + 最小曲数 + 最大曲数 + フィルター + 優先するジャンル(任意) + 例:シンセウェーブ、インディーポップ + 避けるジャンル(任意) + 例:メタル、ハードトラップ + 優先言語(任意) + 例:日本語、英語、インストゥルメンタル + お気に入りを優先 + 不適切な歌詞を除外 + プロンプトのプレビュー + 好みを追加すると最終プロンプトがここに表示されます。 + 精密なキュレーション + ムード・アクティビティ・制約・深さを定義します。 + AI はローカルライブラリの曲のみを使用します。 + AI への指示を少なくとも 1 つ追加してください。 + 有効な曲数の範囲を設定してください。 + %1$d/5 + カスタム… + カスタム値を入力 + カスタム値を入力してください + + + すべての年代 + コアリクエスト: %1$s。 + ムード目標: %1$s。 + アクティビティ: %1$s。 + 年代: %1$s。 + 優先ジャンル: %1$s。 + 避けるジャンル: %1$s。 + 優先言語: %1$s。 + エネルギーレベル目標: %1$d/5。 + ディスカバリー目標: %1$d/5(1 = 馴染みあり、5 = レアな掘り出し物)。 + 可能な限りお気に入りに近い曲を優先する。 + 代替曲がある場合は不適切な歌詞を避ける。 + スムーズなトランジションを維持し、同じアーティストが連続しないようにする。 + + チル + エネルギッシュ + ハッピー + ダーク + ロマンティック + メランコリック + + + ワークアウト + 集中 + ロードトリップ + パーティー + 勉強 + 深夜 + + + @string/playlist_creation_ai_era_any + 70年代 + 80年代 + 90年代 + 2000年代 + 2010年代 + 2020年代 + + + + プレイリストがまだ作成されていません。 + 「新しいプレイリスト」ボタンをタップして始めましょう。 + 新しいプレイリスト + プレイリスト名 + マイプレイリスト + + + %1$d 曲を追加先… + プレイリストを選択 + プレイリストを検索… + プレイリストに曲を追加しました + プレイリストを作成して曲を追加しました + 内部ストレージ + + + 曲を追加 + 選択した曲を追加 + 追加 + 曲を検索またはフィルター… + お気に入り + 曲の読み込みに失敗しました + さらに読み込む + + + プレイリストを結合 + 結合後のプレイリスト名を入力してください: + 結合プレイリスト + 選択した %1$d 件のプレイリストを 1 つに結合します。 + + + 再生できる有効な曲が見つかりませんでした + 現在のリストに曲が見つかりません + 曲を見つけられませんでした + ライブラリに曲が見つかりません + %1$s の再生が終了しました(トラック終了)。 + トラック + シャッフルする曲がありません。 + 選択したアルバム + 選択したアルバムに再生可能な曲が見つかりませんでした + 選択したジャンルに再生可能な曲が見つかりませんでした + 最初の %1$d アルバムのみキューに追加しました + %1$d アルバムをキューに追加しました(%2$d 曲) + 選択したアルバムをキューに追加できませんでした + すべての曲がすでにお気に入りにあります + お気に入りに曲がありませんでした + ZIP ファイルを作成中… + 共有に失敗しました: %1$s + + %d 曲をキューに追加しました + + + %d 曲が次に再生されます + + + %d 曲をお気に入りに追加しました + + + %d 曲をお気に入りから削除しました + + + + 共有するプレイリストがありません + プレイリストを共有 + 共有に失敗しました: %1$s + エクスポートするプレイリストがありません + エクスポートに失敗しました: %1$s + Music/PixelPlayer Exports + 設定で Gemini API キーを設定してください。 + プレイリストを復元しました + + %d 件のプレイリストを共有中 + + + %2$s に %1$d 件のプレイリストをエクスポートしました + + + + 無効なアルバム ID + アルバム ID が見つかりません + アルバムデータの読み込みエラー: %s + アルバムが見つかりません + + + 無効なアーティスト ID + アーティスト ID が見つかりません + アーティストデータの読み込みエラー: %s + アーティストが見つかりませんでした + + + 再生中の曲は削除できません + %1$d 件のファイルを削除しました(%2$d 件スキップ — 再生中) + %2$d 件中 %1$d 件のファイルを削除しました + ファイルの削除に失敗しました + ファイルを削除しました + ファイルを削除できないか、見つかりません + 削除をキャンセルしました + 曲を削除しますか? + %2$s の「%1$s」\n\nこの曲は端末から完全に削除され、元に戻せません。 + これらの曲は端末から完全に削除され、元に戻せません。 + + %d 件のファイルを削除しました + + + %d 曲を削除しますか? + + + + メタデータを更新しました + %1$d 曲を更新中… + %1$d 曲を正常に更新しました! + %1$d 曲を更新しました。失敗: %2$d 曲 + 歌詞を保存しました + 歌詞の保存に失敗しました + 保存できる歌詞がありません + 権限が拒否されました — ファイルを編集できません + 権限が拒否されました — 歌詞を保存できません + 権限が拒否されました — このファイルを編集できません + + + 設定で選択した AI プロバイダーの有効な API キーを設定してください。 + AI エラー: %s + 選択した AI プロバイダーはアカウントのクレジットまたはクォータが不足しているためリクエストを拒否しました。 + 選択した AI モデルは利用できなくなりました。PixelPlayer がサポート対象のモデルへ自動的に切り替えを試みました。 + AI がプロンプトに合う曲を見つけられませんでした。 + デイリーミックスのアイデアを書いてください + AI でデイリーミックスを更新しました + 更新できませんでした: %s + AI がこのミックスに合う曲を見つけられませんでした + diff --git a/app/src/main/res/values-ja/strings_player.xml b/app/src/main/res/values-ja/strings_player.xml new file mode 100644 index 0000000000..eae601ef39 --- /dev/null +++ b/app/src/main/res/values-ja/strings_player.xml @@ -0,0 +1,195 @@ + + + + プレイヤーを閉じる + 再生中 + クラウドストリーム + キャスト + Bluetooth + 本体再生 + 接続中… + キューを開く + + + 接続の準備 + キャスト・Bluetooth オーディオ・スピーカーを同期するために、PixelPlayer が近くのデバイスと現在の Wi‑Fi を確認できるよう許可してください。 + 近くのデバイス + 接続済み Bluetooth オーディオ機器の読み取りと制御に必要です。 + Wi‑Fi 用の位置情報 + Android では、互換性のあるキャストデバイスを検出するために Wi‑Fi ネットワーク(SSID)の共有に位置情報が必要です。 + アクセスを許可 + これらの権限はデバイスの相互接続(キャスト・近くのスピーカーの制御・オーディオ同期)にのみ使用します。 + デバイスを接続 + 近くをスキャン中 + キャストセッション + 接続中 + 接続済み + このスマートフォン + Bluetooth オーディオ + 本体再生 + 再生中 + 一時停止中 + デバイスの音量 + スマートフォンの音量 + %1$d/%2$d + バッテリー残量 + 音量レベル + 切断 + 接続性 + Wi-Fi または Bluetooth をオンにしてください + 接続を更新 + Wi-Fi + オフ + オン + 接続済み + Bluetooth + オフ + オン + 接続済み + 近くのデバイス + デバイスを更新 + 接続済み + 接続中 + 接続可能 + 利用可能 + 接続中... + デバイスを検索中… + テレビやスピーカーの電源が入っており、同じ Wi‑Fi ネットワークに接続されていることを確認してください。 + コントロール + デバイス + + + キャストメディアサーバー + デバイスにキャスト中 + キャストデバイスにメディアを配信中 + %1$s: %2$s + このオーディオフォーマットはキャスト中にシークするとセッションがクラッシュする可能性があるため、一時的に利用できません。 + + + スリープタイマー + タイマー + %1$d 分 + %1$d 分後にタイマーをセットしました。 + 1 回 + + %d 回 + + 再生回数: %1$s + 現在のトラックの終わり + トラックの終わりで再生を停止します。 + スイッチをオン + カスタム時間 + タイマーをキャンセル + トラックの終わり + タイマーをキャンセルしました。 + 再生中の曲がないため、トラック終了タイマーを有効にできません。 + 曲が %1$s から %2$s に変わったため、トラック終了タイマーを無効にしました。 + 前のトラック + 現在のトラック + カスタム時間を設定 + + + 次の曲 + キューはまだ空です。 + + %d 曲待機中 + + キュー + キューは空です。 + 曲を並び替え + シャッフルを切り替え + リピートを切り替え + スリープタイマー + その他の操作 + 現在の曲を探す + キューをクリア + キューをクリア + 現在再生中の曲以外をすべてキューから削除しますか? + プレイリストとして保存 + %1$s のキュー + 現在のキュー + 曲を削除 + 削除しました + プレイリストとして保存 + すべて選択解除 + プレイリスト名 + 含める曲を検索… + 「%1$s」に一致する曲はありません + + %d 曲を選択中 + + %1$s として保存 + プレイリスト名を入力 + プレイリストから削除 + %1$s のその他のオプション + + + 歌詞 + 歌詞を読み込み中… + 同期あり + テキストのみ + 歌詞オプション + −.5 + −.1 + +.1 + +.5 + 0s + %1$+.1f 秒 + + + 歌詞の検索に失敗しました + リモートからの歌詞取得に失敗しました + 接続がタイムアウトしました。インターネット接続を確認してください。 + ネットワークエラー。インターネット接続を確認してください。 + サーバーエラー(コード %d)。しばらくしてから再試行してください。 + + + 歌詞はすでに利用可能です。オンライン取得をスキップしました。 + 埋め込み歌詞が見つかりました。オンライン取得をスキップしました。 + ローカル(.lrc)歌詞が見つかりました。オンライン取得をスキップしました。 + + + 歌詞を保存 + AI で翻訳 + この歌詞にはすでに翻訳があります + この歌詞はすでにこの言語です + API が設定されていません + 歌詞の翻訳が完了しました! + 歌詞を翻訳中... + インポートした歌詞をリセット + 歌詞をリセットしますか? + この曲の歌詞をリセットしてもよろしいですか? + 表示 + 配置 + 左揃え + 中央揃え + 右揃え + コントロール + 同期を調整 + 同期コントロールを非表示 + ローマ字表記を表示 + 翻訳を表示 + 没入モードを一時解除 + 画面をオンに保つ + + + 歌詞を保存 + 保存するバージョンを選択してください: + 同期あり(タイムスタンプ付き) + テキストのみ + + + 歌詞をオンラインで検索しますか? + 歌詞の候補を表示 + 最初の候補を自動適用せず、常に選択画面を開く + 歌詞を検索中… + 歌詞が見つかりませんでした + 歌詞を自動で見つけられませんでした。タイトルやアーティスト名を編集して手動で検索できます。 + 曲名 + アーティスト(任意) + %d 件見つかりました + 同期あり + %1$s • %2$s + 歌詞提供元: + https://lrclib.net/ + diff --git a/app/src/main/res/values-ja/strings_screens.xml b/app/src/main/res/values-ja/strings_screens.xml new file mode 100644 index 0000000000..be88a28c03 --- /dev/null +++ b/app/src/main/res/values-ja/strings_screens.xml @@ -0,0 +1,244 @@ + + + + エラー: ジャンル ID がありません + + + 始めましょう! + ステップ %1$d / %2$d + 先に必要な権限を許可してください。 + 必要な権限をすべて許可してください。 + ようこそ + β + ベータ + セットアップを完了しましょう。 + メディアの権限 + 音楽ライブラリを構築するために、PixelPlayer がオーディオファイルへアクセスする必要があります。 + 権限が許可されました + メディア権限を許可 + 通知 + ロック画面や通知シェードから音楽を操作するために通知を有効にします。 + 通知を有効化 + バックアップはありますか? + PixelPlayer のバックアップがある場合は今すぐ復元することでこのデバイスのセットアップの大部分をスキップできます。 + バックアップをインポート + バックアップを確認中 + バックアップパッケージを確認中… + バックアップを復元中 + スキップ / あとで + バックアップを復元 + セットアップを完了する前にインポートする内容を確認してください。 + %2$d モジュール中 %1$d を選択中 + %1$s に作成 + %1$s からのバックアップ + バージョン不明 + 選択を復元 + 復元中 + 除外フォルダ + デフォルトではすべてのフォルダがスキャンされます。ライブラリ構築時に無視する場所を選択してください。 + 無視するフォルダを選択 + 先にストレージの権限を許可してください + アプリのテーマ + ライブラリの探索を始める前に好みの外観を選んでください。 + ダーク + PixelPlayer のデフォルトの Material 3 ダーク外観。 + ライト + アプリ全体のより明るい Material 3 外観。 + システムに合わせる + スマートフォンの現在の外観設定に合わせます。 + おすすめ + 後から 設定 > 外観 > アプリのテーマ で変更できます。 + ライブラリレイアウト + ライブラリのナビゲーション方法を選択してください。 + + コンパクトモード + 最小化されたピルナビゲーションを使用 + 標準のタブ行を使用 + + アルバム + アーティスト + 後から 設定 > 外観 > ライブラリナビゲーション で変更できます。 + アプリナビゲーション + ボトムナビゲーションバーのスタイルを選択してください。 + デフォルトスタイル + 角が丸いフローティングピル + 標準のフル幅バー + コーナー半径をカスタマイズ + 後から 設定 > 外観 > ナビバースタイル で変更できます。 + アラームとリマインダー + 任意ですが、スリープタイマーを使用して PixelPlayer を正確な時刻に停止させたい場合はおすすめです。 + 権限を許可 + バッテリー最適化 + 一部の Android 端末はバックグラウンドアプリを積極的に終了させます。予期しない再生の中断を防ぐために PixelPlayer のバッテリー最適化を無効にしてください。 + 最適化を無効化 + 準備完了! + 音楽を楽しむ準備ができました。 + + + 検索… + 検索 + 検索をクリア + 最近の検索 + すべてクリア + 履歴 + 検索履歴アイテムを削除 + 結果なし + 「%1$s」の検索結果はありません + 見つかりませんでした + 別の検索語またはフィルターを試してください。 + 結果が見つかりませんでした。 + ジャンルで探す + 利用可能なジャンルがありません。 + + + %1$s を再生 + %1$s を折りたたむ + %1$s を展開 + アーティスト画像を編集 + 写真を変更 + デフォルトに戻す + アーティストをシャッフル再生 + + + ディスク %d + %1$s のカバー + %1$s · %2$s + + + プレイリストが見つかりません。 + このプレイリストは空です。 + 「曲を追加」をタップして始めましょう。 + このフォルダに曲はありません。 + 曲を並び替え + その他のオプション + プレイリストのオプション + プレイリストを編集 + プレイリストを削除 + プレイリストを削除しますか? + このプレイリストを本当に削除しますか? + デフォルトトランジションを設定 + プレイリストをエクスポート + %1$s • %2$s + 再生する + 追加 + 曲を追加 + 削除 + 曲を削除 + 並び替え + 曲を並び替え + + + グローバルトランジション + プレイリストルール + 上書きされない限り、すべての再生ソースにこの設定が適用されます。 + この特定のプレイリストのデフォルト動作を設定します。 + アクティブ状態 + グローバルデフォルト + プレイリストデフォルト + グローバルに従う + カスタム上書き + カスタム上書き + 有効にするとこのプレイリストに特定のルールを設定できます。 + グローバルデフォルトを使用 + 変更を保存しました + トランジションスタイル + トラックのブレンド方法 + なし + クロスフェード + トランジションの長さ + %1$d 秒のオーバーラップ + トランジションをリセット + 現在の曲 + 次の曲 + トラックは %1$d 秒間オーバーラップします + 音量カーブ + オーディオのスロープを微調整 + フェードアウト + フェードイン + + + 新しいスマートプレイリスト + 新しいプレイリスト + 曲を追加 + 戻るまたはキャンセル + 次へ + 作成 + プレイリストを編集 + 自動生成コラージュ + 写真を追加 + 画像を選択 + 変更 + 削除 + プレイリスト名 + マイ素敵なミックス + カバーを編集 + カバーアートを調整 + ピンチとドラッグで最適なフレーミングを見つけてください + 手動 + スマート + AI で生成 + スマートルール + デフォルト + 画像 + アイコン + 背景色 + アイコンシンボル + 形状スタイル + 形状パラメーター + コーナー半径 + 滑らかさ + 辺の数 + カーブ + 回転 + スケール + よく再生する曲 + 最も再生されたトラック。 + 最近再生した曲 + 最近聴いた曲。 + 忘れられたお気に入り + しばらく再生していないお気に入りのトラック。 + 新着の宝石 + 再生回数が少ない最近追加されたトラック。 + + + ジャンルに曲を素早く追加 + 並び替えと再生 + シャッフル + 並び替え基準 + アーティスト + アルバム + タイトル + 一般アーティスト + %1$s シャッフル + + + 曲を選択 + ジャンルを選択 + 曲を検索 + 新しいジャンル + カスタムを追加 + カスタムジャンルを追加 + ジャンル名 + アイコンを選択 + ジャンル: %1$s + ジャンルを選択 + 素早く追加 + + + DJ スペース + 読み込み中… + デッキ %1$d + 曲を読み込む + 曲が読み込まれていません + + ステム分離はまだ利用できません。 + 音量 + 速度 + クロスフェーダー + デッキ 1 + デッキ 2 + 曲を選択 + 再生/一時停止 + 曲のカバー + x%1$.2f + diff --git a/app/src/main/res/values-ja/strings_settings.xml b/app/src/main/res/values-ja/strings_settings.xml new file mode 100644 index 0000000000..ee41a6b0dd --- /dev/null +++ b/app/src/main/res/values-ja/strings_settings.xml @@ -0,0 +1,641 @@ + + + + 音楽管理 + フォルダ管理、ライブラリ更新、解析オプション + 外観 + テーマ、レイアウト、ビジュアルスタイル + 再生 + オーディオ動作、クロスフェード、バックグラウンド再生 + 動作 + ジェスチャー、触覚フィードバック、ナビゲーション動作 + AI 連携(β) + AI プロバイダー、API キー、モデル設定 + バックアップ & 復元 + 個人データのエクスポートと復元 + 開発者オプション + 試験的機能とデバッグ + イコライザー + 音域とプリセットの調整 + デバイス情報 + オーディオ仕様、コーデック、デコーダー情報 + アカウント + Telegram、Google Drive、NetEase などのサービスを管理 + このアプリについて + アプリ情報、バージョン、クレジット + + + オン + オフ + 有効 + 無効 + 開く + すべて選択 + 選択を解除 + 通知を閉じる + + + ライブラリ構造 + 除外ディレクトリ + ここに追加したフォルダはライブラリスキャン時にスキップされます。 + アーティスト + 複数アーティストの解析と整理オプション。 + フィルタリング + 最低曲の長さ + アルバムの最低トラック数 + アルバムアートキャッシュ上限 + 同期とスキャン + ライブラリを更新 + 新しいファイルや変更されたファイルをライブラリ全体からスキャンします。 + フルリスキャン + フルリスキャン実行中 + フルリスキャンを開始しました… + ライブラリ同期が完了しました + データベースを再構築 + データベースを再構築しますか? + 音楽ライブラリを最初から完全に再構築します。インポートした歌詞、お気に入り、カスタムメタデータはすべて失われます。この操作は元に戻せません。 + 再構築 + データベースを再構築中 + データベースを再構築中… + .lrc ファイルを自動スキャン + ライブラリ同期中に、同じフォルダ内の .lrc ファイルを自動でスキャンして割り当てます。 + 歌詞管理 + 歌詞ソースの優先順位 + 歌詞を取得する際に最初に試みるソースを選択します。 + 埋め込みを優先 + オンラインを優先 + ローカル(.lrc)を優先 + インポートした歌詞をリセット + データベースからインポートした歌詞をすべて削除します。 + インポートした歌詞をリセットしますか? + この操作は元に戻せません。 + + + 更新 + デフォルトではすべて許可されています。フォルダをタップするとスキャンから除外されます。 + サブフォルダがありません + 上へ移動 + ルートへ移動 + + + リスキャンが必要です + アーティスト設定が変更されました。ライブラリをリスキャンして適用してください。 + リスキャン + スキャン中… + 複数アーティストの解析 + 文字区切り + 現在: %1$s + 単語区切り + なし + 現在: %1$s + 設定 + タイトルからアーティストを抽出 + 曲タイトルの feat., ft., with を検出 + ライブラリ整理 + アルバムアーティストでグループ化 + コラボアルバムをメインアーティストの下に表示 + 複数アーティスト解析について + + PixelPlayer は文字区切り(/、;、&)と単語区切り(feat.、ft.、vs.、x)を使ってアーティストタグを分割します。単語区切りは大文字小文字を区別しません。 + 「タイトルからアーティストを抽出」は曲タイトルの (feat. アーティスト名) のようなパターンを検出します。 + バックスラッシュ(\)で文字区切りをエスケープできます。 + + + + + \"Artist1/Artist2\" + Artist1, Artist2 + \"Drake feat. Rihanna\" + Drake, Rihanna + \"Marshmello x Bastille\" + Marshmello, Bastille + \"Song (ft. B)\" by A + A, B + \"AC\\DC\" + AC/DC(エスケープ済み) + + + 区切り文字 + 現在の区切り文字 + 区切り文字をタップして削除します。少なくとも 1 つ必要です。 + 新しい区切り文字を追加 + 例: / または ; + 区切り文字を追加 + デフォルトの区切り文字 + 区切り文字をリセットしますか? + カスタム区切り文字をすべてクリアしてデフォルトに戻します。この操作は元に戻せません。 + 区切り文字をデフォルトにリセットしました + 少なくとも 1 つの区切り文字が必要です + 区切り文字を追加しました + すでに存在するか無効な区切り文字です + スペース + + + 単語区切り + 現在の単語区切り + スペースで囲まれているときにアーティスト名を分割するキーワードです。大文字小文字を区別しません。タップして削除。 + 単語区切りが設定されていません + 新しい単語区切りを追加 + 例: feat. または ft. + 単語区切りを追加 + 単語区切りの仕組み + 単語区切りはスペースで囲まれている場合に大文字小文字を区別せずマッチします。\n\n1文字の区切り(例: \"x\")は誤マッチを防ぐために両側にスペースが必要です。\n\n例:\n \"Drake feat. Rihanna\" -> Drake, Rihanna\n \"Marshmello x Bastille\" -> Marshmello, Bastille\n \"A vs. B\" -> A, B + 単語区切りをリセットしますか? + カスタム単語区切りをすべてクリアしてデフォルトキーワードに戻します。この操作は元に戻せません。 + 単語区切りを追加しました + すでに存在するか無効です + 単語区切りをデフォルトにリセットしました + + + 同期を準備中 + MediaStore を読み込み中 + トラックを処理中 + データベースに保存中 + 歌詞ファイルをスキャン中 + アルバムアートキャッシュをクリア中 + クラウドソースを同期中 + 同期を完了中 + %1$s • %2$d%% (%3$d/%4$d) + %1$s… + + + グローバルテーマ + アプリの言語 + アプリ全体で使用する言語を選択します。 + システムのデフォルト + English + Español + Deutsch + Français + Русский + 简体中文 + Bahasa Indonesia + Italiano + 한국어 + Norsk (Bokmål) + Türkçe + 日本語 + アプリのテーマ + ライト、ダーク、またはシステムに合わせるを選択します。 + ライトテーマ + ダークテーマ + システムに合わせる + スムーズコーナーを使用 + 複雑な形状のコーナーを使用して見た目を向上させますが、ローエンド端末ではパフォーマンスに影響する場合があります。 + ブラー効果を無効化 + アプリ全体のブラー効果をオフにしてバッテリーとリソースを節約します。 + スクロールバーを表示 + 音楽リストにスクロールバーを表示してすばやくスクロールできます。 + 再生中 + プレイヤーテーマ + フローティングプレイヤーの外観を選択します。 + アルバムアート + システムダイナミック + プレイヤーのファイル情報を表示 + プレイヤーの進行バーにコーデック、ビットレート、サンプルレートを表示します。 + アルバムアートパレットスタイル + 現在: %1$s。ライブプレビューを開いてスタイルを選択してください。 + カルーセルスタイル + アルバムカルーセルの外観を選択します。 + のぞき込みなし + のぞき込み 1 枚 + ホームコラージュ + コラージュパターン + 「あなたのミックス」コラージュの形状を選択します。 + パターンを自動ローテーション + ホームを訪れるたびにコラージュパターンを切り替えます。 + ナビゲーションバー + ナビバースタイル + ナビゲーションバーの外観を選択します。 + デフォルト + フル幅 + コンパクトモード + アイコンのみ表示してナビバーの高さを縮小します。 + ナビバーのコーナー半径 + ナビゲーションバーのコーナー半径を調整します。 + 歌詞画面 + 没入型歌詞 + コントロールを自動非表示にしてテキストを拡大します。 + 自動非表示の遅延 + コントロールが非表示になるまでの時間。 + 3 秒 + 4 秒 + 5 秒 + 6 秒 + アプリナビゲーション + デフォルトタブ + 起動時のデフォルトタブを選択します。 + ホーム + 検索 + ライブラリ + ライブラリナビゲーション + ライブラリタブ間の移動方法を選択します。 + タブ行(デフォルト) + コンパクトピル & グリッド + + + カラー + パレットスタイル + プレイヤー UI のアルバムカラーを選択します。 + トーナルスポット + バランスが取れた落ち着いた雰囲気。 + ビビッド + 高彩度のアクセント。 + エクスプレッシブ + 大胆な色相シフトとコントラスト。 + フルーツサラダ + 楽しい回転アクセント。 + カラーの精度 + 0 は現在の調整を維持します。高い値ほどアルバムアートの主要色に近くなります。 + 現在 + より正確 + 0 • 現在 + %1$d • 穏やか + %1$d • バランス + %1$d • 正確 + + + コーナー半径を調整 + ナビバーの形状のコーナーをデバイスの物理コーナーに合わせてシームレスな外観にします。 + コーナー半径 + %1$d dp + + + バックグラウンド再生 + 閉じても再生を続ける + オフにすると、アプリを履歴から削除したときに再生が停止します。 + バッテリー最適化 + バッテリー最適化を無効にして再生の中断を防ぎます。 + バッテリー最適化はすでに無効になっています + バッテリー設定を開けませんでした + 音量ノーマライゼーション(ReplayGain) + ReplayGain を有効化 + オーディオファイルの ReplayGain メタデータを使って音量レベルを正規化します。 + ゲインモード + トラック: 曲ごとに正規化。アルバム: アルバム単位で正規化。 + トラック + アルバム + キャスト + キャスト接続/切断時に自動再生 + キャスト接続を切り替えた直後に自動で再生を開始します。 + ヘッドフォン + ヘッドフォン再接続時に再開 + ヘッドフォンを外したために一時停止した場合、再接続すると自動で再開します。 + キューとトランジション + クロスフェード + 曲間のスムーズなトランジションを有効にします。 + クロスフェードの長さ + Hi-Fi モード + 32 ビット float オーディオ出力。端末で再生がカクつく場合は無効にしてください。 + この端末ではサポートされていません(PCM_FLOAT AudioTrack 非対応)。 + シャッフルを保持 + アプリを閉じた後もシャッフル設定を記憶します。 + キュー履歴を表示 + キューに以前再生した曲を表示します。 + + + フォルダ + 戻るジェスチャーでフォルダを操作 + フォルダタブで、システムの戻る操作がライブラリを離れる前にフォルダ階層をさかのぼります。 + プレイヤーのジェスチャー + 背景タップでプレイヤーを閉じる + ぼかした背景をタップするとプレイヤーシートが閉じます。 + 触覚フィードバック + 触覚フィードバック + アプリ全体でバイブレーションフィードバックを有効にします。 + + + AI プロバイダー + プロバイダー + AI プロバイダーを選択してください + セーフトークンモード + ON — 高速 & 低コスト。AI に最小限のデータ(約 1K トークン)を送信します。 + OFF — 深いコンテキスト。より豊かな結果のためにリスニングプロフィール全体(約 8K トークン)を送信します。 + 認証情報 + %1$s API キー + %1$s から取得 + Google AI Studio (aistudio.google.com) + DeepSeek Platform (api.deepseek.com) + Groq Console (console.groq.com) + Mistral AI Platform (console.mistral.ai) + NVIDIA Build (build.nvidia.com) + Moonshot AI Platform (platform.moonshot.cn) + Zhipu AI Open Platform (bigmodel.cn) + OpenAI Platform (platform.openai.com) + モデル選択 + 利用可能なモデルを読み込み中… + モデルの読み込みに失敗しました + AI モデル + モデルを選択してください。 + API キーを入力 + プロンプト動作 + システムプロンプト + AI の動作をカスタマイズします。 + プリセットプロンプト + システムプロンプトを入力… + プロフェッショナルキュレーター + あなたは「Vibe-Engine」という世界トップクラスの音楽キュレーターで、ソニックフローの達人です。シームレスで高品質なリスニング体験を提供することが目標です。和声の相性、論理的な BPM トランジション、馴染みのお気に入りと洗練された発見のバランスを優先してください。 + クリエイティブマーベリック + あなたは「予期しない統一感」を専門とする前衛的な音楽探求者です。非自明なソニックの共通点を見つけることで従来のジャンルの壁を打ち破ることが使命です。レアなディープカット、実験的なテクスチャー、芸術的な新しさを優先しながら、驚きつつも否定できないトランジションロジックを維持してください。 + 厳格な司書 + あなたは精密な音楽データベースアーキテクトです。絶対的なメタデータの精度と厳格なカテゴリ遵守によってロジックを動かします。アルゴリズムによる発見を最小化し、厳格なジャンルの一貫性、エネルギーレベルのマッチング、ユーザーが明確に定義した好みの高精度な取得を最大化してください。 + アトモスフェリックガイド + あなたはアンビエントテクスチャーと低エネルギーフローの達人です。「深い集中」や「静けさ」の状態を促すトラックだけに集中してください。アコースティックな温かさ、ミニマリストのアレンジ、穏やかなトランジションを優先し、高い過渡音や急激なダイナミックの変化を厳しく避けてください。 + ソニックエンスージアスト + あなたはプロダクションの複雑さと演奏に焦点を当てたオーディオファイルアナリストです。高いダイナミックレンジ、複雑なポリリズム、優れたサウンドステージ品質を持つトラックを優先してください。技術的な忠実度とアレンジの細部に注意を払うリスナーを喜ばせるアクティブリスニング作品を選んでください。 + エナジーカタリスト + あなたは高モメンタムのリズムジェネレーターです。強烈なベースライン、パーカッシブな強度、感染力のあるグルーヴを中心哲学とします。高 BPM のクラブ互換性、シンコペーションエネルギー、継続的なリズムの張りを優先して、リスナーの心拍数とモチベーションをピーク状態に保ってください。 + AI 使用レポート + 総消費量 + %1$s トークンを追跡中\nプロンプト: %2$s | 出力: %3$s | 思考: %4$s + ログをクリア + AI アクティビティログ(%1$d 件) + %1$s · %2$s + 表示 + 非表示 + + + バックアップの仕組み + セクションを選んで .pxpl ファイルをエクスポートし、後でインポートして復元します。復元は選択したセクションのみを置き換えます。 + バックアップを作成 + バックアップをエクスポート + セクションが選択されていません。 + すべてのセクションが選択されています。 + %2$d セクション中 %1$d を選択中。 + %1$s .pxpl バックアップファイルを作成します。 + 選択してエクスポート + バックアップを復元 + バックアップをインポート + 選択して復元 + 最近のバックアップを参照または選択します。選択したデータが現在のデータを置き換えます。 + + + バックアップパッケージに含める内容を正確に選択してください。 + .pxpl バックアップファイルを選択して確認します。次のステップで復元するセクションを選択します。 + %2$d セクション中 %1$d を選択中 + %2$d モジュール中 %1$d を選択中 + 最近のバックアップ + 最近のバックアップはありません + 以前にインポートしたバックアップがここに表示されます。 + %1$d エントリー · 現在のデータを置き換えます + .pxpl をエクスポート + 選択を復元 + 転送中… + PixelPlayer_Backup_%1$d.pxpl + バックアップを作成中 + バックアップを復元中 + %1$d%% + %1$s • %2$s + エクスポート中 + インポート中 + 復元中 + 履歴から削除 + 確認中… + ファイルを参照 + ステップ %1$d / %2$d + モジュールを復元 + バックアップの詳細 + 作成日 + アプリバージョン + スキーマ + デバイス + 不明 + · %1$s + %1$d モジュール · v%2$s · スキーマ v%3$d + \? + すべて選択 + 選択をクリア + + + 無効なバックアップ: %1$s + 復元を準備中 + 復元タスクを開始しています。 + バックアップを準備中 + バックアップタスクを開始しています。 + バックアップを正常に復元しました + 一部の未解決の問題がありましたが復元は完了しました。 + 復元を完了できませんでした: %1$s + 復元に失敗しました: %1$s + データを正常にエクスポートしました + エクスポートに失敗しました: %1$s + データを正常に復元しました + 未解決の問題で復元が完了しました。失敗: %1$s + v%1$d + %1$s %2$s + + + 実験的機能 + 試験的 + プレイヤー UI 読み込みの実験とトグル。 + セットアップフローをテスト + テスト用にオンボーディングのセットアップ画面を起動します。 + メンテナンス + デイリーミックスの強制再生成 + デイリーミックスプレイリストをすぐに再作成します。 + デイリーミックスを再生成 + デイリーミックスを再生成しますか? + 現在のミックスを破棄して、最近のリスニング習慣に基づいて新しいミックスを生成します。 + デイリーミックスの再生成を開始しました + 統計の強制再生成 + キャッシュをクリアして再生統計を再計算します。 + 再生成 + 処理中… + 統計を再生成 + 統計を再生成しますか? + 統計キャッシュをクリアして、データベース履歴から強制的に再計算します。 + 統計の再生成を開始しました + アルバムパレットの強制再生成 + すべてのアルバムアートのキャッシュされたパレットバリアントを再構築するか、特定の 1 枚を更新します。 + すべて再生成 + すべてのアルバムパレットを再生成しますか? + キャッシュされたテーマデータをクリアして、%1$d 枚のユニークなアルバムアートのすべてのパレットスタイルを再構築します。 + 再生成中… + アルバムパレットを再生成中… + %1$d 枚のユニークなアルバムアートのキャッシュされたパレットバリアントを再構築中です。大きなライブラリでは時間がかかることがあります。 + %1$d / %2$d 完了 + %1$d 枚のアルバムアートパレットを再生成しました + %2$d 枚中 %1$d 枚のアルバムアートパレットを再生成しました + 曲を選択 + 曲を選択するとキャッシュされたテーマデータをクリアして、アルバムアートからすべてのパレットスタイルを再生成します。 + タイトル、アーティスト、アルバムで検索 + 検索に一致する曲がありません。 + アルバムアートのある曲が見つかりませんでした。 + パレットを再生成中… + %1$s のパレットを再生成しました + %1$s のパレットを再生成できませんでした + 診断 + テストクラッシュを発生させる + クラッシュレポートシステムをテストするためにクラッシュをシミュレートします。 + 開発者オプションからテストクラッシュを発生させました — これはクラッシュレポートシステムをテストするための意図的な操作です + + + 試験的 + プレイヤー UI 読み込みの調整 + アニメーション歌詞(ハイエンド端末向け) + 歌詞にスプリングアニメーションとビジュアル効果を使用します。ローエンド端末ではフレームドロップが発生する場合があります。 + 歌詞のブラー効果 + 非アクティブな歌詞に被写界深度ブラーを適用します。 + ブラー強度 + ブラー効果の強さを調整します。 + %1$.1f倍 + ステップ 1 · 遅延する対象を選択 + すべてを遅延 + シートの背景が完全に展開されるまでプレイヤーのコンテンツ全体を保持します。 + アルバムカルーセル + シートが展開されるまでアルバムアートとカルーセルを遅延します。 + 曲のメタデータ + タイトル、アーティスト、歌詞/キューのアクションを遅延します。 + 進行バー + 展開完了までタイムラインと時刻ラベルを遅延します。 + 再生コントロール + 再生/一時停止、シーク、お気に入りコントロールを遅延します。 + 遅延するコンポーネントがすべてアクティブです。「すべてを遅延」を無効にして各パーツをカスタマイズします。 + ステップ 2 · プレースホルダーの動作を設定 + 遅延項目にプレースホルダーを使用 + コンポーネントが展開を待つ間、軽量なプレースホルダーを描画してレイアウトを安定させます。 + ステップ 3 · プレースホルダーから実コンテンツに切り替えるタイミングを選択 + モードを 1 つ選択してください。閾値モードはスライダーを使用します。ドラッグリリースモードはシートジェスチャーを離すまで待機します。 + トリガーモードを解除するには遅延コンポーネントを少なくとも 1 つ有効にしてください。 + 閾値 + 展開率を使用します。 + ドラッグリリース + ジェスチャーを離した後のみ切り替えます。 + 展開閾値 + 遅延コンポーネントが表示されるまでにシートがどれだけ展開している必要があるか。 + コンテンツは %1$d%% 展開時に表示されます + プレイヤーを閉じるときにも適用 + 折りたたむ際に閉じる閾値を使ってプレースホルダーに戻します。 + 閉じる閾値 + プレースホルダーが再び表示されるまでにどれだけ折りたたまれている必要があるか。 + %1$d%% 折りたたみ後にプレースホルダーが表示されます + ドラッグリリースモードは閾値と閉じる動作をバイパスします。切り替えはシートのドラッグジェスチャーが終了したときのみ発生します。 + プレースホルダーを透明にする + プレースホルダーはレイアウトスペースを保持したまま見えなくなります。 + 画質 + アルバムアートの解像度 + 低(256px)- パフォーマンス重視 + 中(512px)- バランス型 + 高(800px)- 最高品質 + オリジナル - 最大品質 + + + 再生には確認が必要です + 再生の準備ができています + -- + フォーマット + HW デコーダー + ローカル曲 + ローカル音楽ストレージ + 音楽サイズ + %1$d 曲(ローカル) + 利用可能 + %1$s 合計 + 音楽の使用量 + デバイス使用中 + %1$d%% + <1% + %1$d 曲(クラウド) + %1$d ファイルは読み取り不可 + 再生パス + はい + いいえ + サンプルレート + %1$d Hz + %1$d フレーム/バッファ + Hi-Fi PCM Float + 32 ビット float 出力パス + 低レイテンシーサポート + プロオーディオサポート + メモリ + %1$s 中利用可能 + オフロード対応フォーマット + ハードウェアオフロードをサポートする圧縮フォーマットは報告されませんでした。 + 他 %1$d 件 + 検出された出力 + 内蔵出力 + Bluetooth オーディオ + USB オーディオ + 有線オーディオ + デジタル出力 + その他の出力 + Android から出力ルートは報告されませんでした。 + ExoPlayer エンジン + %1$s レンダラー + フォーマット互換性 + %1$d 対応トラック + %1$d 不明なフォーマット + デコーダーが報告されません + ハードウェアデコーダー + ソフトウェアデコーダー + オフロード + ライブラリ内 %1$d 件 + 互換性の確認結果 + 大きな非互換性はありません + インデックスされたトラックはこのデバイスで Android が報告するデコーダーと一致しています。 + %1$d 件のトラックはネイティブデコードできない可能性があります + 確認が必要なフォーマット: %1$s。 + %1$d 件のローカルトラックはリサンプリングされる可能性があります + ライブラリは現在の出力サンプルレートを超える %1$d Hz に達しています。 + %1$d 件のトラックはメタデータが不明です + ライブラリを完全にリスキャンすると MIME、ビットレート、サンプルレートの欠損データを補完できます。 + デバイス情報 + メーカー + モデル + ブランド + デバイス + Android バージョン + SDK バージョン + ハードウェア + パフォーマンスレポート + 再生やスキャンのラグを分類するのに役立つ共有可能な診断レポートを生成します。デバイス、ライブラリ、タイミングデータのみを含み、ファイルパス、タイトル、アーティストは含まれません。 + レポートを生成 + 再生成 + コピー + 共有 + レポートをクリップボードにコピーしました + PixelPlayer パフォーマンスレポート + 高度なパフォーマンス診断 + デフォルトではオフです。ベータのトラブルシューティング用に短いラグタイムラインを記録します。 + %1$s まで有効 + 今ラグをマーク + ラグの瞬間をマークしました + + + 接続済みアカウント + リンクされたプロバイダーを管理して各連携をコントロールします。 + リンク済みサービス + アクティブ + 利用可能 + 近日公開 + 接続済み + 近日公開 + サービスを開く + ログアウト中… + リンク済みアカウントがまだありません + プロバイダーを接続するとこの画面で管理できます。 + %1$s に接続 + %1$s(近日公開) + Google Drive は近日公開予定です。 + 現在この画面を開けません。 + + + このアプリについて + PixelPlayer + コミュニティと共に作られたオープンソースの音楽プレイヤー。 + バージョン v%1$s + オープンソース + コミュニティファースト + Material 3 エクスプレッシブ + 現在コントリビューターが見つかりません。後でもう一度お試しください。 + メンテナー + PixelPlayer の開発者。 + コミュニティスポットライト + 大きな貢献をしたコラボレーターへの感謝。 + オープンソースコントリビューター + GitHub からのライブコントリビューターリスト。 + %1$d 回のコントリビューション + GitHub プロフィールを開く + Telegram を開く + %1$s のアバター + %1$s のアイコン + diff --git a/app/src/main/res/values-ja/strings_widget.xml b/app/src/main/res/values-ja/strings_widget.xml new file mode 100644 index 0000000000..c0d6f47874 --- /dev/null +++ b/app/src/main/res/values-ja/strings_widget.xml @@ -0,0 +1,17 @@ + + + サイズに合わせて自動調整するウィジェット + コンパクトなプレイヤーバー + シャッフルとリピートを含むフルコントロール + ミニマリストな正方形プレイヤー + + タップして開く + アルバムアート + アルバムアートのプレースホルダー + + タップして再生 + 曲のタイトル + アーティスト + + 進行バー、%1$d%% + From 7e927c01db6e28b4d4a2337bbe2de9f12e5e7218 Mon Sep 17 00:00:00 2001 From: Dae Euhwa Date: Wed, 24 Jun 2026 01:44:03 -0500 Subject: [PATCH 02/73] Add MultiLangRomanizer functions for Thai, Arabic, Greek, Hebrew, and Vietnamese MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - isThai(): detect Thai script via U+0E00..U+0E7F - isArabic(): detect Arabic script via U+0600..U+06FF - isGreek(): detect Greek script via U+0370..U+03FF - isHebrew(): detect Hebrew script via U+0590..U+05FF - isVietnamese(): detect Vietnamese via Latin Extended Additional block (U+1EA0..U+1EF9) and specific chars (ă, đ, ơ, ư) - romanizeVietnamese(): NFKD-decompose and strip diacritics to plain ASCII --- .../theveloper/pixelplay/utils/LyricsUtils.kt | 29 +++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/theveloper/pixelplay/utils/LyricsUtils.kt b/app/src/main/java/com/theveloper/pixelplay/utils/LyricsUtils.kt index 3c44e7d595..f1045eb700 100644 --- a/app/src/main/java/com/theveloper/pixelplay/utils/LyricsUtils.kt +++ b/app/src/main/java/com/theveloper/pixelplay/utils/LyricsUtils.kt @@ -48,6 +48,7 @@ import com.theveloper.pixelplay.data.model.SyncedLine import com.theveloper.pixelplay.data.model.SyncedWord import kotlinx.coroutines.flow.Flow +import java.text.Normalizer import java.util.Locale import java.util.regex.Pattern import kotlin.math.PI @@ -119,6 +120,14 @@ object MultiLangRomanizer { fun isPunjabi(text: String) = text.any { it in '\u0A00'..'\u0A7F' } fun isCyrillic(text: String) = text.any { it in '\u0400'..'\u04FF' } fun isChinese(text: String) = text.any { it in '\u4E00'..'\u9FFF' } + fun isThai(text: String) = text.any { it in '\u0E00'..'\u0E7F' } + fun isArabic(text: String) = text.any { it in '\u0600'..'\u06FF' } + fun isGreek(text: String) = text.any { it in '\u0370'..'\u03FF' } + fun isHebrew(text: String) = text.any { it in '\u0590'..'\u05FF' } + fun isVietnamese(text: String): Boolean { + return text.any { it in '\u1EA0'..'\u1EF9' } || + text.any { it in "\u0102\u0103\u0110\u0111\u01A0\u01A1\u01AF\u01B0" } + } fun isScriptThatNeedsRomanization(text: String): Boolean { return text.any { char -> @@ -133,8 +142,8 @@ object MultiLangRomanizer { } } - private fun isRussian(text: String) = text.any { RUSSIAN_CYRILLIC_LETTERS.contains(it.toString()) } && text.all { RUSSIAN_CYRILLIC_LETTERS.contains(it.toString()) || !it.toString().matches("[\\u0400-\\u04FF]".toRegex()) } - private fun isUkrainian(text: String) = text.any { UKRAINIAN_CYRILLIC_LETTERS.contains(it.toString()) || UKRAINIAN_SPECIFIC_CYRILLIC_LETTERS.contains(it.toString()) } && text.all { UKRAINIAN_CYRILLIC_LETTERS.contains(it.toString()) || UKRAINIAN_SPECIFIC_CYRILLIC_LETTERS.contains(it.toString()) || !it.toString().matches("[\\u0400-\\u04FF]".toRegex()) } + internal fun isRussian(text: String) = text.any { RUSSIAN_CYRILLIC_LETTERS.contains(it.toString()) } && text.all { RUSSIAN_CYRILLIC_LETTERS.contains(it.toString()) || !it.toString().matches("[\\u0400-\\u04FF]".toRegex()) } + internal fun isUkrainian(text: String) = text.any { UKRAINIAN_CYRILLIC_LETTERS.contains(it.toString()) || UKRAINIAN_SPECIFIC_CYRILLIC_LETTERS.contains(it.toString()) } && text.all { UKRAINIAN_CYRILLIC_LETTERS.contains(it.toString()) || UKRAINIAN_SPECIFIC_CYRILLIC_LETTERS.contains(it.toString()) || !it.toString().matches("[\\u0400-\\u04FF]".toRegex()) } private fun isSerbian(text: String) = text.any { SERBIAN_CYRILLIC_LETTERS.contains(it.toString()) || SERBIAN_SPECIFIC_CYRILLIC_LETTERS.contains(it.toString()) } && text.all { SERBIAN_CYRILLIC_LETTERS.contains(it.toString()) || SERBIAN_SPECIFIC_CYRILLIC_LETTERS.contains(it.toString()) || !it.toString().matches("[\\u0400-\\u04FF]".toRegex()) } private fun isBulgarian(text: String) = text.any { BULGARIAN_CYRILLIC_LETTERS.contains(it.toString()) } && text.all { BULGARIAN_CYRILLIC_LETTERS.contains(it.toString()) || !it.toString().matches("[\\u0400-\\u04FF]".toRegex()) } private fun isBelarusian(text: String) = text.any { BELARUSIAN_CYRILLIC_LETTERS.contains(it.toString()) || BELARUSIAN_SPECIFIC_CYRILLIC_LETTERS.contains(it.toString()) } && text.all { BELARUSIAN_CYRILLIC_LETTERS.contains(it.toString()) || BELARUSIAN_SPECIFIC_CYRILLIC_LETTERS.contains(it.toString()) || !it.toString().matches("[\\u0400-\\u04FF]".toRegex()) } @@ -547,6 +556,22 @@ object MultiLangRomanizer { } } + fun romanizeVietnamese(text: String): String { + if (text.isEmpty()) return text + val normalized = Normalizer.normalize(text, Normalizer.Form.NFKD) + val sb = StringBuilder(text.length) + for (char in normalized) { + when { + char == '\u0111' -> sb.append('d') + char == '\u0110' -> sb.append('D') + char in '\u0300'..'\u036F' || char in '\uFE20'..'\uFE2F' -> {} + char.code > 0x7E && char in '\u00C0'..'\u024F' -> {} + else -> sb.append(char) + } + } + return sb.toString() + } + private fun processCyrillicWordByWord(text: String, specificMap: Map, isRussian: Boolean = false): String { val romajiBuilder = StringBuilder(text.length) val words = text.split("((?<=\\s|[.,!?;])|(?=\\s|[.,!?;]))".toRegex()).filter { it.isNotEmpty() } From f53d85737bf7d2ad5eee79ae11183af68d98a8e7 Mon Sep 17 00:00:00 2001 From: Dae Euhwa Date: Wed, 24 Jun 2026 01:44:04 -0500 Subject: [PATCH 03/73] Add romanization and language detection for Cyrillic and Vietnamese LyricsRepositoryImpl.kt: - Route Cyrillic text to romanizeCyrillic() before falling through - Route Vietnamese text to romanizeVietnamese() - Include synced lyrics translations in LRC output AiStateHolder.kt: - Detect source language before AI translation using MultiLangRomanizer - Support all newly detected scripts (Thai, Arabic, Greek, Hebrew, Vietnamese) - Prefix translation prompt with detected language for better AI results --- .../data/repository/LyricsRepositoryImpl.kt | 18 ++++++++- .../presentation/viewmodel/AiStateHolder.kt | 40 ++++++++++++++++++- 2 files changed, 54 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/theveloper/pixelplay/data/repository/LyricsRepositoryImpl.kt b/app/src/main/java/com/theveloper/pixelplay/data/repository/LyricsRepositoryImpl.kt index 05e357d05c..61e1b6982c 100644 --- a/app/src/main/java/com/theveloper/pixelplay/data/repository/LyricsRepositoryImpl.kt +++ b/app/src/main/java/com/theveloper/pixelplay/data/repository/LyricsRepositoryImpl.kt @@ -1056,7 +1056,14 @@ class LyricsRepositoryImpl @Inject constructor( val lyricsData = LyricsData( plainLyrics = lyrics.plain?.joinToString("\n"), - syncedLyrics = lyrics.synced?.joinToString("\n") { "[${formatTimestamp(it.time)}]${it.line}" }, + syncedLyrics = lyrics.synced?.joinToString("\n") { line -> + buildString { + append("[${formatTimestamp(line.time)}]${line.line}") + if (!line.translation.isNullOrBlank()) { + append("\n[${formatTimestamp(line.time)}]${line.translation}") + } + } + }, wordByWordLyrics = wordByWordLyrics ) @@ -1247,7 +1254,12 @@ class LyricsRepositoryImpl @Inject constructor( toWordByWordLrc(syncedLyrics) } else { syncedLyrics.joinToString("\n") { line -> - "[${formatTimestamp(line.time)}]${line.line}" + buildString { + append("[${formatTimestamp(line.time)}]${line.line}") + if (!line.translation.isNullOrBlank()) { + append("\n[${formatTimestamp(line.time)}]${line.translation}") + } + } } } } @@ -1710,6 +1722,8 @@ class LyricsRepositoryImpl @Inject constructor( MultiLangRomanizer.isJapanese(text) -> MultiLangRomanizer.romanizeJapanese(text) ?: text MultiLangRomanizer.isChinese(text) -> MultiLangRomanizer.romanizeChinese(text) ?: text MultiLangRomanizer.isKorean(text) -> MultiLangRomanizer.romanizeKorean(text) + MultiLangRomanizer.isCyrillic(text) -> MultiLangRomanizer.romanizeCyrillic(text) ?: text + MultiLangRomanizer.isVietnamese(text) -> MultiLangRomanizer.romanizeVietnamese(text) else -> text } } diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/AiStateHolder.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/AiStateHolder.kt index ad68cea838..8e125a235b 100644 --- a/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/AiStateHolder.kt +++ b/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/AiStateHolder.kt @@ -10,6 +10,7 @@ import com.theveloper.pixelplay.data.ai.AiSystemPromptType import com.theveloper.pixelplay.data.ai.provider.AiProviderException import com.theveloper.pixelplay.data.preferences.PlaylistPreferencesRepository import com.theveloper.pixelplay.data.model.Song +import com.theveloper.pixelplay.utils.MultiLangRomanizer import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.MutableStateFlow @@ -17,6 +18,7 @@ import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch import timber.log.Timber +import java.util.Locale import javax.inject.Inject import javax.inject.Singleton @@ -287,8 +289,14 @@ class AiStateHolder @Inject constructor( suspend fun translateLyrics(lyricsText: String): Result { return try { val targetLanguage = context.resources.configuration.locales[0].displayLanguage + val sourceLanguage = detectLyricsLanguage(lyricsText) + val taskPrefix = if (sourceLanguage != null) { + "Translate these $sourceLanguage song lyrics into $targetLanguage." + } else { + "Translate these song lyrics into $targetLanguage." + } val prompt = """ -Translate song lyrics into $targetLanguage. +$taskPrefix - Preserve ALL timestamps [mm:ss.xx] exactly — never modify, merge, or drop them. @@ -307,7 +315,7 @@ class AiStateHolder @Inject constructor( $lyricsText """.trimIndent() - + val response = aiHandler.generateContent( prompt = prompt, type = AiSystemPromptType.GENERAL, @@ -319,6 +327,34 @@ $lyricsText } } + private val timestampRegex = Regex("\\[\\d{1,2}:\\d{2}(?:[.:]\\d{1,3})?]") + + private fun detectLyricsLanguage(lyricsText: String): String? { + val sample = timestampRegex.replace(lyricsText, "").trim().take(200) + if (sample.isBlank()) return null + + return when { + MultiLangRomanizer.isJapanese(sample) -> "Japanese" + MultiLangRomanizer.isKorean(sample) -> "Korean" + MultiLangRomanizer.isChinese(sample) -> "Chinese" + MultiLangRomanizer.isThai(sample) -> "Thai" + MultiLangRomanizer.isArabic(sample) -> "Arabic" + MultiLangRomanizer.isGreek(sample) -> "Greek" + MultiLangRomanizer.isHebrew(sample) -> "Hebrew" + MultiLangRomanizer.isHindi(sample) -> "Hindi" + MultiLangRomanizer.isPunjabi(sample) -> "Punjabi" + MultiLangRomanizer.isCyrillic(sample) -> { + when { + MultiLangRomanizer.isRussian(sample) -> "Russian" + MultiLangRomanizer.isUkrainian(sample) -> "Ukrainian" + else -> null + } + } + MultiLangRomanizer.isVietnamese(sample) -> "Vietnamese" + else -> null + } + } + fun onCleared() { scope = null allSongsProvider = null From d74f593cb86aa3116f83c8d37bb4d64d997c04cc Mon Sep 17 00:00:00 2001 From: Dae Euhwa Date: Wed, 24 Jun 2026 01:44:04 -0500 Subject: [PATCH 04/73] Fix 21 pre-existing lint errors MissingDefaultResource (14): Create base values/plurals.xml with English defaults for all 14 plural names that only existed in values-ar/ NewApi (3): Annotate BlurEffectCache with @RequiresApi(31); guard ForegroundServiceStartNotAllowedException with SDK_INT >= S check WrongConstant (1): Suppress lint on takePersistableUriPermission flags NonObservableLocale (2): Suppress lint on Locale.getDefault() in composables MissingIntentFilterForMediaSearch (1): Add MEDIA_PLAY_FROM_SEARCH intent-filter to MainActivity for Android Auto voice search support --- app/src/main/AndroidManifest.xml | 4 ++ .../pixelplay/ExternalPlayerActivity.kt | 1 + .../com/theveloper/pixelplay/MainActivity.kt | 4 ++ .../presentation/screens/AiUsageComponents.kt | 1 + .../PlayerControlActionCallback.kt | 6 +- app/src/main/res/values/plurals.xml | 59 +++++++++++++++++++ 6 files changed, 73 insertions(+), 2 deletions(-) create mode 100644 app/src/main/res/values/plurals.xml diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 1afb6f4044..46a821a28e 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -73,6 +73,10 @@ + + + + diff --git a/app/src/main/java/com/theveloper/pixelplay/ExternalPlayerActivity.kt b/app/src/main/java/com/theveloper/pixelplay/ExternalPlayerActivity.kt index 2d993383df..3ffd21abfe 100644 --- a/app/src/main/java/com/theveloper/pixelplay/ExternalPlayerActivity.kt +++ b/app/src/main/java/com/theveloper/pixelplay/ExternalPlayerActivity.kt @@ -117,6 +117,7 @@ class ExternalPlayerActivity : ComponentActivity() { return intent.data } + @Suppress("WrongConstant") private fun persistUriPermissionIfNeeded(intent: Intent, uri: Uri) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { val hasPersistablePermission = intent.flags and Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION != 0 diff --git a/app/src/main/java/com/theveloper/pixelplay/MainActivity.kt b/app/src/main/java/com/theveloper/pixelplay/MainActivity.kt index 503ac7a21a..02fa1a2488 100644 --- a/app/src/main/java/com/theveloper/pixelplay/MainActivity.kt +++ b/app/src/main/java/com/theveloper/pixelplay/MainActivity.kt @@ -10,6 +10,7 @@ import android.content.Context import android.content.ComponentName import android.content.Intent import android.os.Build +import androidx.annotation.RequiresApi import android.graphics.RenderEffect as AndroidRenderEffect import android.graphics.Shader as AndroidShader import androidx.compose.ui.graphics.asComposeRenderEffect @@ -943,6 +944,7 @@ class MainActivity : ComponentActivity() { val expansionFractionProvider = remember(playerViewModel.playerContentExpansionFraction) { { playerViewModel.playerContentExpansionFraction.value } } + @Suppress("NewApi") val blurEffectCache = remember { BlurEffectCache() } Box( @@ -1152,10 +1154,12 @@ class MainActivity : ComponentActivity() { * blur every animation frame. The radius is quantized at the call site, so this * only rebuilds ~25 times across the whole expand animation instead of 60+/sec. */ +@RequiresApi(Build.VERSION_CODES.S) private class BlurEffectCache { private var lastRadiusPx: Float = Float.NaN private var cached: androidx.compose.ui.graphics.RenderEffect? = null + @RequiresApi(Build.VERSION_CODES.S) fun get(radiusPx: Float): androidx.compose.ui.graphics.RenderEffect? { if (radiusPx <= 0f) { lastRadiusPx = 0f diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/screens/AiUsageComponents.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/screens/AiUsageComponents.kt index 8c9a9b17fa..750f992a79 100644 --- a/app/src/main/java/com/theveloper/pixelplay/presentation/screens/AiUsageComponents.kt +++ b/app/src/main/java/com/theveloper/pixelplay/presentation/screens/AiUsageComponents.kt @@ -49,6 +49,7 @@ fun AiUsageDateHeader(date: String) { } @Composable +@Suppress("NonObservableLocale") fun AiUsageLogItem( usage: AiUsageEntity ) { diff --git a/app/src/main/java/com/theveloper/pixelplay/ui/glancewidget/PlayerControlActionCallback.kt b/app/src/main/java/com/theveloper/pixelplay/ui/glancewidget/PlayerControlActionCallback.kt index fcd2a1e471..22fd1aced5 100644 --- a/app/src/main/java/com/theveloper/pixelplay/ui/glancewidget/PlayerControlActionCallback.kt +++ b/app/src/main/java/com/theveloper/pixelplay/ui/glancewidget/PlayerControlActionCallback.kt @@ -58,9 +58,11 @@ class PlayerControlActionCallback : ActionCallback { context.startService(serviceIntent) } Timber.tag(TAG).d("Service intent sent for action: $action") - } catch (e: android.app.ForegroundServiceStartNotAllowedException) { - Timber.tag(TAG).w(e, "Cannot start foreground service from background for action $action") } catch (e: Exception) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && e is android.app.ForegroundServiceStartNotAllowedException) { + Timber.tag(TAG).w(e, "Cannot start foreground service from background for action $action") + return + } Timber.tag(TAG).e(e, "Error starting service for action $action: ${e.message}") } } diff --git a/app/src/main/res/values/plurals.xml b/app/src/main/res/values/plurals.xml new file mode 100644 index 0000000000..6961e51cef --- /dev/null +++ b/app/src/main/res/values/plurals.xml @@ -0,0 +1,59 @@ + + + + Sharing %d playlist + Sharing %d playlists + + + Exported %1$d playlist to %2$s + Exported %1$d playlists to %2$s + + + %d song added to queue + %d songs added to queue + + + %d song will play next + %d songs will play next + + + %d song added to favorites + %d songs added to favorites + + + %d song removed from favorites + %d songs removed from favorites + + + %d file deleted + %d files deleted + + + Delete %d file? + Delete %d files? + + + %d track selected + %d tracks selected + + + %1$d song • %2$s + %1$d songs • %2$s + + + %d track lined up. + %d tracks lined up. + + + %d song selected + %d songs selected + + + %d song + %d songs + + + %d topic + %d topics + + From 2580e86a7ca93022e34fdc8233b8c640ab05ef99 Mon Sep 17 00:00:00 2001 From: Dae Euhwa Date: Wed, 24 Jun 2026 01:44:05 -0500 Subject: [PATCH 05/73] Add new app languages and update locale configuration - Add JAPANESE, TRADITIONAL_CHINESE, VIETNAMESE to AppLanguage enum - Update locales_config.xml with all supported locales (de, en, es, fr, in, it, ja, ko, nb, ru, tr, vi, zh-CN, zh-TW) - Add German translations for changelogs and settings - Add Korean, Norwegian Bokmal, Russian translations for changelogs and settings - Add Japanese strings_settings.xml update --- .../pixelplay/data/preferences/AppLanguage.kt | 5 ++++- .../main/res/values-de/strings_changelogs.xml | 18 ++++++++++++++++++ .../main/res/values-de/strings_settings.xml | 5 +++++ .../main/res/values-ja/strings_settings.xml | 13 +++++++++++++ .../main/res/values-ko/strings_changelogs.xml | 18 ++++++++++++++++++ .../main/res/values-ko/strings_settings.xml | 5 +++++ .../main/res/values-nb/strings_changelogs.xml | 18 ++++++++++++++++++ .../main/res/values-nb/strings_settings.xml | 5 +++++ .../main/res/values-ru/strings_changelogs.xml | 18 ++++++++++++++++++ .../main/res/values-ru/strings_settings.xml | 5 +++++ app/src/main/res/values/strings_settings.xml | 5 +++++ app/src/main/res/xml/locales_config.xml | 11 +++++++++++ 12 files changed, 125 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/theveloper/pixelplay/data/preferences/AppLanguage.kt b/app/src/main/java/com/theveloper/pixelplay/data/preferences/AppLanguage.kt index 5630700f90..e926bda366 100644 --- a/app/src/main/java/com/theveloper/pixelplay/data/preferences/AppLanguage.kt +++ b/app/src/main/java/com/theveloper/pixelplay/data/preferences/AppLanguage.kt @@ -12,11 +12,14 @@ enum class AppLanguage(val tag: String, @StringRes val labelRes: Int) { FRENCH("fr", R.string.settings_language_french), INDONESIAN("in", R.string.settings_language_indonesian), ITALIAN("it", R.string.settings_language_italian), + JAPANESE("ja", R.string.settings_language_japanese), KOREAN("ko", R.string.settings_language_korean), NORWEGIAN_BOKMAL("nb", R.string.settings_language_norwegian_bokmal), RUSSIAN("ru", R.string.settings_language_russian), SIMPLIFIED_CHINESE("zh-CN", R.string.settings_language_chinese), - TURKISH("tr", R.string.settings_language_turkish); + TRADITIONAL_CHINESE("zh-TW", R.string.settings_language_traditional_chinese), + TURKISH("tr", R.string.settings_language_turkish), + VIETNAMESE("vi", R.string.settings_language_vietnamese); companion object { val supportedLanguageTags: Set = values().map { it.tag }.toSet() diff --git a/app/src/main/res/values-de/strings_changelogs.xml b/app/src/main/res/values-de/strings_changelogs.xml index b5e8947760..a0863d12d2 100644 --- a/app/src/main/res/values-de/strings_changelogs.xml +++ b/app/src/main/res/values-de/strings_changelogs.xml @@ -129,4 +129,22 @@ Lokalisierung: Spanisch, Französisch, Russisch, Vereinfachtes Chinesisch, Indonesisch, Italienisch + + Google Drive-Integration mit Player-Lebenszyklus-Management. + Batch-Bearbeitung von Song-Metadaten (Tags und Cover-Art). + KI-Übersetzung von Songtexten mit anpassbaren Wear OS-Einstellungen. + Latenzdiagnose-Tool und Mehrfachauswahl auf dem Suchbildschirm. + Arabische & Türkische Unterstützung, mit lokalisierten http-URL-Lokalisierungsoptionen. + + + Drastische Akku-Ersparnis (Audio-Offloading und UI-Polling-Gates). + Optimiertes Queue-Management (schnellere Einfügungen und explizite Indizierung). + Material 3 Expressive Motion-Animationen für Übergangsbildschirme. + Überarbeitete Bibliothekssynchronisierung mittels gedrosseltem Scannen. + + + Behobene Ruckel-/Aussetzer bei der Wiedergabe und Pufferungsprobleme. + Behobene Synchronisation externer Songlöschungen und Metadatenkonsistenz. + Behobene Speicherprobleme, Abstürze und Layout-Fehler auf Wear OS und Telefon. + \ No newline at end of file diff --git a/app/src/main/res/values-de/strings_settings.xml b/app/src/main/res/values-de/strings_settings.xml index e6fcda8436..147d337ada 100644 --- a/app/src/main/res/values-de/strings_settings.xml +++ b/app/src/main/res/values-de/strings_settings.xml @@ -7,6 +7,8 @@ Design, Layout und Farben Wiedergabe Audio, Crossfade und Hintergrundwiedergabe + Songtexte + Quellpriorität, immersiver Modus und Importverwaltung Verhalten Gesten, Haptik und Navigation KI-Integration (β) @@ -174,6 +176,9 @@ Koreanisch Norwegisch Bokmål Türkisch + 日本語 + 繁體中文 + Tiếng Việt App-Design Hell, Dunkel oder System-Design – ganz nach Geschmack. Hell diff --git a/app/src/main/res/values-ja/strings_settings.xml b/app/src/main/res/values-ja/strings_settings.xml index ee41a6b0dd..6678113cc5 100644 --- a/app/src/main/res/values-ja/strings_settings.xml +++ b/app/src/main/res/values-ja/strings_settings.xml @@ -7,6 +7,8 @@ テーマ、レイアウト、ビジュアルスタイル 再生 オーディオ動作、クロスフェード、バックグラウンド再生 + 歌詞 + 優先ソース、没入型歌詞、インポート管理 動作 ジェスチャー、触覚フィードバック、ナビゲーション動作 AI 連携(β) @@ -175,6 +177,8 @@ Norsk (Bokmål) Türkçe 日本語 + 繁體中文 + Tiếng Việt アプリのテーマ ライト、ダーク、またはシステムに合わせるを選択します。 ライトテーマ @@ -268,6 +272,9 @@ バッテリー最適化を無効にして再生の中断を防ぎます。 バッテリー最適化はすでに無効になっています バッテリー設定を開けませんでした + 音量 + 音量ゼロで一時停止 + 音量がゼロのときに再生を一時停止します 音量ノーマライゼーション(ReplayGain) ReplayGain を有効化 オーディオファイルの ReplayGain メタデータを使って音量レベルを正規化します。 @@ -622,6 +629,12 @@ このアプリについて PixelPlayer コミュニティと共に作られたオープンソースの音楽プレイヤー。 + GitHub + ソースコード、リリース、イシュー + Telegram + コミュニティに参加、開発をフォロー + Telegram に参加 + GitHub リポジトリを開く バージョン v%1$s オープンソース コミュニティファースト diff --git a/app/src/main/res/values-ko/strings_changelogs.xml b/app/src/main/res/values-ko/strings_changelogs.xml index acb78fe7b1..81b43ec1ca 100644 --- a/app/src/main/res/values-ko/strings_changelogs.xml +++ b/app/src/main/res/values-ko/strings_changelogs.xml @@ -129,4 +129,22 @@ 현지화: 스페인어, 프랑스어, 러시아어, 중국어(간체), 인도네시아어, 이탈리아어 + + 플레이어 수명 주기 관리와 함께 Google Drive 통합. + 노래 메타데이터 일괄 편집(태그 및 앨범 아트). + 맞춤형 Wear OS 환경설정과 함께 AI 가사 번역. + 지연 진단 도구 및 검색 화면에서 다중 선택. + 아랍어 및 터키어 지원, 로컬 네트워크 옵션을 위한 지역화된 http URL. + + + 획기적인 배터리 절약(오디오 오프로드 및 UI 폴링 게이트). + 최적화된 대기열 관리(더 빠른 삽입 및 명시적 인덱싱). + 전환 화면을 위한 Material 3 Expressive 모션 애니메이션. + 조절된 스캐닝을 통한 리팩토링된 라이브러리 동기화. + + + 끊김/건너뜀 재생 지연 및 버퍼링 문제 해결. + 외부 노래 삭제 동기화 및 메타데이터 일관성 수정. + Wear OS 및 휴대폰에서 메모리 문제, 충돌 및 레이아웃 결함 수정. + \ No newline at end of file diff --git a/app/src/main/res/values-ko/strings_settings.xml b/app/src/main/res/values-ko/strings_settings.xml index 4b4fe3a87f..da2e36e8de 100644 --- a/app/src/main/res/values-ko/strings_settings.xml +++ b/app/src/main/res/values-ko/strings_settings.xml @@ -7,6 +7,8 @@ 테마, 레이아웃 및 시각적 스타일 재생 오디오 동작, 크로스페이드 및 백그라운드 재생 + 가사 + 소스 우선순위, 몰입형 가사 및 가져오기 관리 동작 제스처, 햅틱 및 내비게이션 동작 AI 연동 (β) @@ -174,6 +176,9 @@ 한국어 노르웨이어 (Bokmål) 터키어 + 일본어 + 중국어(번체) + 베트남어 앱 테마 밝은 테마, 어두운 테마 또는 시스템 설정 따르기 중에서 선택하세요. 밝은 테마 diff --git a/app/src/main/res/values-nb/strings_changelogs.xml b/app/src/main/res/values-nb/strings_changelogs.xml index c12abe6370..0c8ebb6711 100644 --- a/app/src/main/res/values-nb/strings_changelogs.xml +++ b/app/src/main/res/values-nb/strings_changelogs.xml @@ -129,4 +129,22 @@ Lokalisering: Spansk, Fransk, Russisk, Forenklet kinesisk, Indonesisk, Italiensk + + Google Drive-integrasjon med spillerens livssyklusstyring. + Batch-redigering av sangmetadata (tagger og coverkunst). + AI-tekstoversettelse med tilpassbare Wear OS-preferanser. + Forsinkelsesdiagnoseverktøy og flervalg på søkeskjermen. + Arabisk og tyrkisk støtte, med lokaliserte http-URL-alternativer for lokalnettverk. + + + Drastisk batteribesparelse (lydavlasting og UI-avstemningsporter). + Optimalisert køstyring (raskere innsettinger og eksplisitt indeksering). + Material 3 Expressive-bevegelsesanimasjoner for overgangsskjermer. + Omstrukturert biblioteksynkronisering via strupte skanninger. + + + Løst problemer med hakkete/hoppende avspillingsforsinkelser og bufringsproblemer. + Fikset synkronisering av eksterne slettede sanger og metadatakonsistens. + Fikset minneproblemer, krasjer og layoutfeil på Wear OS og telefon. + \ No newline at end of file diff --git a/app/src/main/res/values-nb/strings_settings.xml b/app/src/main/res/values-nb/strings_settings.xml index d8e2abe1fc..0295cb20aa 100644 --- a/app/src/main/res/values-nb/strings_settings.xml +++ b/app/src/main/res/values-nb/strings_settings.xml @@ -7,6 +7,8 @@ Temaer, layout og visuelle stiler Avspilling Lydatferd, crossfade og bakgrunnsavspilling + Lyrics + Source priority, immersive lyrics, and import management Atferd Gester, haptikk og navigasjonsatferd AI-integrasjon (β) @@ -174,6 +176,9 @@ Koreansk Norsk bokmål Tyrkisk + Japansk + Tradisjonell kinesisk + Vietnamesisk App-tema Bytt mellom lyst, mørkt eller følg systemets utseende. Lyst tema diff --git a/app/src/main/res/values-ru/strings_changelogs.xml b/app/src/main/res/values-ru/strings_changelogs.xml index 1e30b701ce..048bcadced 100644 --- a/app/src/main/res/values-ru/strings_changelogs.xml +++ b/app/src/main/res/values-ru/strings_changelogs.xml @@ -129,4 +129,22 @@ Локализация: испанский, французский, русский, упрощённый китайский, индонезийский, итальянский + + Интеграция с Google Drive с управлением жизненным циклом плеера. + Пакетное редактирование метаданных песен (теги и обложки). + Перевод текстов песен с помощью ИИ с настраиваемыми параметрами Wear OS. + Инструмент диагностики задержек и множественный выбор на экране поиска. + Поддержка арабского и турецкого языков, локализованные http URL для локальной сети. + + + Кардинальная экономия заряда батареи (выгрузка аудио и шлюзы опроса UI). + Оптимизированное управление очередью (более быстрая вставка и явная индексация). + Анимация Material 3 Expressive для экранов переходов. + Переработанная синхронизация библиотеки с регулируемым сканированием. + + + Устранены задержки воспроизведения (заикания, пропуски) и проблемы с буферизацией. + Исправлена синхронизация удаления внешних песен и согласованность метаданных. + Исправлены проблемы с памятью, сбои и ошибки макета на Wear OS и телефоне. + \ No newline at end of file diff --git a/app/src/main/res/values-ru/strings_settings.xml b/app/src/main/res/values-ru/strings_settings.xml index c49784964b..faceb539a2 100644 --- a/app/src/main/res/values-ru/strings_settings.xml +++ b/app/src/main/res/values-ru/strings_settings.xml @@ -7,6 +7,8 @@ Темы, макеты и визуальные стили Воспроизведение Поведение аудио, кроссфейд и фоновое воспроизведение + Тексты песен + Приоритет источника, иммерсивный режим и управление импортом Поведение Жесты, тактильный отклик и навигация Интеграция ИИ (β) @@ -174,6 +176,9 @@ Корейский Норвежский (Bokmål) Турецкий + Японский + Китайский (традиционный) + Вьетнамский Тема приложения Светлая, тёмная тема или настройки системы. Светлая тема diff --git a/app/src/main/res/values/strings_settings.xml b/app/src/main/res/values/strings_settings.xml index ea36028155..b7f5cc4f53 100644 --- a/app/src/main/res/values/strings_settings.xml +++ b/app/src/main/res/values/strings_settings.xml @@ -5,6 +5,8 @@ Manage folders, refresh library, parsing options Appearance Themes, layout, and visual styles + Lyrics + Source priority, immersive lyrics, and import management Playback Audio behavior, crossfade, and background play Behavior @@ -171,9 +173,12 @@ 简体中文 Bahasa Indonesia Italiano + 日本語 Korean Norwegian (Bokmål) + 繁體中文 Türkçe + Tiếng Việt App Theme Switch between light, dark, or follow system appearance. Light Theme diff --git a/app/src/main/res/xml/locales_config.xml b/app/src/main/res/xml/locales_config.xml index 12f03ebe00..6d332ab8d5 100644 --- a/app/src/main/res/xml/locales_config.xml +++ b/app/src/main/res/xml/locales_config.xml @@ -1,6 +1,17 @@ + + + + + + + + + + + \ No newline at end of file From 5a900052662e79492c9209b40fc96713ffb1d075 Mon Sep 17 00:00:00 2001 From: Dae Euhwa Date: Wed, 24 Jun 2026 01:44:05 -0500 Subject: [PATCH 06/73] Add LYRICS settings category and restructure settings screens SettingsCategory.kt: - Add LYRICS category with QueueMusic icon between APPEARANCE and PLAYBACK - Move EQUALIZER after PLAYBACK for better grouping - Add DEVICE_CAPABILITIES after DEVELOPER SettingsCategoryScreen.kt: - Extract lyrics management (source priority, reset) into dedicated LYRICS section - Extract lyrics display (immersive mode, auto-hide delay) into LYRICS section - Keep PLAYBACK focused on playback-specific settings only - Suppress NonObservableLocale in AI usage section SettingsScreen.kt: - Add color scheme for LYRICS category FullPlayerContent.kt: - Remove redundant resetLyricsSearchState() call when opening lyrics sheet --- .../components/player/FullPlayerContent.kt | 1 - .../presentation/model/SettingsCategory.kt | 25 ++-- .../screens/SettingsCategoryScreen.kt | 109 +++++++++--------- .../presentation/screens/SettingsScreen.kt | 2 + 4 files changed, 75 insertions(+), 62 deletions(-) diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/components/player/FullPlayerContent.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/components/player/FullPlayerContent.kt index 8a30f6f41d..336588bda0 100644 --- a/app/src/main/java/com/theveloper/pixelplay/presentation/components/player/FullPlayerContent.kt +++ b/app/src/main/java/com/theveloper/pixelplay/presentation/components/player/FullPlayerContent.kt @@ -378,7 +378,6 @@ fun FullPlayerContent( if (showFetchLyricsDialog) { showFetchLyricsDialog = false showLyricsSheet = true - playerViewModel.resetLyricsSearchState() } } is LyricsSearchUiState.Error -> { diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/model/SettingsCategory.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/model/SettingsCategory.kt index e59c3d7a8c..7d827896f5 100644 --- a/app/src/main/java/com/theveloper/pixelplay/presentation/model/SettingsCategory.kt +++ b/app/src/main/java/com/theveloper/pixelplay/presentation/model/SettingsCategory.kt @@ -9,6 +9,7 @@ import androidx.compose.material.icons.rounded.Info import androidx.compose.material.icons.rounded.LibraryMusic import androidx.compose.material.icons.rounded.MusicNote import androidx.compose.material.icons.rounded.Palette +import androidx.compose.material.icons.rounded.QueueMusic import androidx.compose.ui.graphics.vector.ImageVector import com.theveloper.pixelplay.R @@ -31,11 +32,23 @@ enum class SettingsCategory( subtitleRes = R.string.settings_category_appearance_subtitle, icon = Icons.Rounded.Palette ), + LYRICS( + id = "lyrics", + titleRes = R.string.settings_category_lyrics_title, + subtitleRes = R.string.settings_category_lyrics_subtitle, + icon = Icons.Rounded.QueueMusic + ), PLAYBACK( id = "playback", titleRes = R.string.settings_category_playback_title, subtitleRes = R.string.settings_category_playback_subtitle, - icon = Icons.Rounded.MusicNote // Using MusicNote again or maybe PlayCircle if available + icon = Icons.Rounded.MusicNote + ), + EQUALIZER( + id = "equalizer", + titleRes = R.string.settings_category_equalizer_title, + subtitleRes = R.string.settings_category_equalizer_subtitle, + icon = Icons.Rounded.GraphicEq ), BEHAVIOR( id = "behavior", @@ -61,17 +74,11 @@ enum class SettingsCategory( subtitleRes = R.string.settings_category_developer_subtitle, icon = Icons.Rounded.DeveloperMode ), - EQUALIZER( - id = "equalizer", - titleRes = R.string.settings_category_equalizer_title, - subtitleRes = R.string.settings_category_equalizer_subtitle, - icon = Icons.Rounded.GraphicEq - ), DEVICE_CAPABILITIES( id = "device_capabilities", titleRes = R.string.settings_category_device_capabilities_title, subtitleRes = R.string.settings_category_device_capabilities_subtitle, - icon = Icons.Rounded.DeveloperBoard // Placeholder, maybe Memory or SettingsInputComponent + icon = Icons.Rounded.DeveloperBoard ), ABOUT( id = "about", @@ -83,4 +90,6 @@ enum class SettingsCategory( companion object { fun fromId(id: String): SettingsCategory? = entries.find { it.id == id } } + + } diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/screens/SettingsCategoryScreen.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/screens/SettingsCategoryScreen.kt index dd1672dbc6..bb97e988b4 100644 --- a/app/src/main/java/com/theveloper/pixelplay/presentation/screens/SettingsCategoryScreen.kt +++ b/app/src/main/java/com/theveloper/pixelplay/presentation/screens/SettingsCategoryScreen.kt @@ -508,33 +508,6 @@ fun SettingsCategoryScreen( ) } - SettingsSubsection( - title = stringResource(R.string.settings_lyrics_management_section), - addBottomSpace = false - ) { - ThemeSelectorItem( - label = stringResource(R.string.settings_lyrics_source_priority_title), - description = stringResource(R.string.settings_lyrics_source_priority_subtitle), - options = mapOf( - LyricsSourcePreference.EMBEDDED_FIRST.name to stringResource(R.string.settings_lyrics_embedded_first), - LyricsSourcePreference.API_FIRST.name to stringResource(R.string.settings_lyrics_online_first), - LyricsSourcePreference.LOCAL_FIRST.name to stringResource(R.string.settings_lyrics_local_first) - ), - selectedKey = uiState.lyricsSourcePreference.name, - onSelectionChanged = { key -> - settingsViewModel.setLyricsSourcePreference( - LyricsSourcePreference.fromName(key) - ) - }, - leadingIcon = { Icon(painterResource(R.drawable.rounded_lyrics_24), null, tint = MaterialTheme.colorScheme.secondary) } - ) - SettingsItem( - title = stringResource(R.string.settings_reset_imported_lyrics_title), - subtitle = stringResource(R.string.settings_reset_imported_lyrics_subtitle), - leadingIcon = { Icon(Icons.Outlined.ClearAll, null, tint = MaterialTheme.colorScheme.secondary) }, - onClick = { showClearLyricsDialog = true } - ) - } } SettingsCategory.APPEARANCE -> { val useSmoothCorners by settingsViewModel.useSmoothCorners.collectAsStateWithLifecycle() @@ -679,32 +652,6 @@ fun SettingsCategoryScreen( ) } - SettingsSubsection(title = stringResource(R.string.settings_lyrics_screen_section)) { - SwitchSettingItem( - title = stringResource(R.string.settings_immersive_lyrics_title), - subtitle = stringResource(R.string.settings_immersive_lyrics_subtitle), - checked = uiState.immersiveLyricsEnabled, - onCheckedChange = { settingsViewModel.setImmersiveLyricsEnabled(it) }, - leadingIcon = { Icon(painterResource(R.drawable.rounded_lyrics_24), null, tint = MaterialTheme.colorScheme.secondary) } - ) - - if (uiState.immersiveLyricsEnabled) { - ThemeSelectorItem( - label = stringResource(R.string.settings_auto_hide_delay_title), - description = stringResource(R.string.settings_auto_hide_delay_subtitle), - options = mapOf( - "3000" to stringResource(R.string.settings_auto_hide_delay_3s), - "4000" to stringResource(R.string.settings_auto_hide_delay_4s), - "5000" to stringResource(R.string.settings_auto_hide_delay_5s), - "6000" to stringResource(R.string.settings_auto_hide_delay_6s) - ), - selectedKey = uiState.immersiveLyricsTimeout.toString(), - onSelectionChanged = { settingsViewModel.setImmersiveLyricsTimeout(it.toLong()) }, - leadingIcon = { Icon(Icons.Rounded.Timer, null, tint = MaterialTheme.colorScheme.secondary) } - ) - } - } - SettingsSubsection( title = stringResource(R.string.settings_app_navigation_section), addBottomSpace = false @@ -734,6 +681,61 @@ fun SettingsCategoryScreen( ) } } + SettingsCategory.LYRICS -> { + SettingsSubsection( + title = stringResource(R.string.settings_lyrics_management_section), + addBottomSpace = false + ) { + ThemeSelectorItem( + label = stringResource(R.string.settings_lyrics_source_priority_title), + description = stringResource(R.string.settings_lyrics_source_priority_subtitle), + options = mapOf( + LyricsSourcePreference.EMBEDDED_FIRST.name to stringResource(R.string.settings_lyrics_embedded_first), + LyricsSourcePreference.API_FIRST.name to stringResource(R.string.settings_lyrics_online_first), + LyricsSourcePreference.LOCAL_FIRST.name to stringResource(R.string.settings_lyrics_local_first) + ), + selectedKey = uiState.lyricsSourcePreference.name, + onSelectionChanged = { key -> + settingsViewModel.setLyricsSourcePreference( + LyricsSourcePreference.fromName(key) + ) + }, + leadingIcon = { Icon(painterResource(R.drawable.rounded_lyrics_24), null, tint = MaterialTheme.colorScheme.secondary) } + ) + SettingsItem( + title = stringResource(R.string.settings_reset_imported_lyrics_title), + subtitle = stringResource(R.string.settings_reset_imported_lyrics_subtitle), + leadingIcon = { Icon(Icons.Outlined.ClearAll, null, tint = MaterialTheme.colorScheme.secondary) }, + onClick = { showClearLyricsDialog = true } + ) + } + + SettingsSubsection(title = stringResource(R.string.settings_lyrics_screen_section)) { + SwitchSettingItem( + title = stringResource(R.string.settings_immersive_lyrics_title), + subtitle = stringResource(R.string.settings_immersive_lyrics_subtitle), + checked = uiState.immersiveLyricsEnabled, + onCheckedChange = { settingsViewModel.setImmersiveLyricsEnabled(it) }, + leadingIcon = { Icon(painterResource(R.drawable.rounded_lyrics_24), null, tint = MaterialTheme.colorScheme.secondary) } + ) + + if (uiState.immersiveLyricsEnabled) { + ThemeSelectorItem( + label = stringResource(R.string.settings_auto_hide_delay_title), + description = stringResource(R.string.settings_auto_hide_delay_subtitle), + options = mapOf( + "3000" to stringResource(R.string.settings_auto_hide_delay_3s), + "4000" to stringResource(R.string.settings_auto_hide_delay_4s), + "5000" to stringResource(R.string.settings_auto_hide_delay_5s), + "6000" to stringResource(R.string.settings_auto_hide_delay_6s) + ), + selectedKey = uiState.immersiveLyricsTimeout.toString(), + onSelectionChanged = { settingsViewModel.setImmersiveLyricsTimeout(it.toLong()) }, + leadingIcon = { Icon(Icons.Rounded.Timer, null, tint = MaterialTheme.colorScheme.secondary) } + ) + } + } + } SettingsCategory.PLAYBACK -> { SettingsSubsection(title = stringResource(R.string.settings_background_playback_section)) { ThemeSelectorItem( @@ -1269,6 +1271,7 @@ fun SettingsCategoryScreen( .fillMaxWidth() .padding(top = 8.dp, bottom = 8.dp) ) { + @Suppress("NonObservableLocale") val dateFormat = SimpleDateFormat("MMMM d, yyyy", Locale.getDefault()) val groupedUsage = recentAiUsage.groupBy { dateFormat.format(Date(it.timestamp)) diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/screens/SettingsScreen.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/screens/SettingsScreen.kt index fa40ecb265..292d3448ef 100644 --- a/app/src/main/java/com/theveloper/pixelplay/presentation/screens/SettingsScreen.kt +++ b/app/src/main/java/com/theveloper/pixelplay/presentation/screens/SettingsScreen.kt @@ -480,6 +480,7 @@ private fun getCategoryColors(category: SettingsCategory, isDark: Boolean): Pair when (category) { SettingsCategory.LIBRARY -> Color(0xFF004A77) to Color(0xFFC2E7FF) SettingsCategory.APPEARANCE -> Color(0xFF7D5260) to Color(0xFFFFD8E4) + SettingsCategory.LYRICS -> Color(0xFF4A1A6E) to Color(0xFFE8D0FF) SettingsCategory.PLAYBACK -> Color(0xFF633B48) to Color(0xFFFFD8EC) SettingsCategory.BEHAVIOR -> Color(0xFF3E4C63) to Color(0xFFD7E3FF) SettingsCategory.AI_INTEGRATION -> Color(0xFF004F58) to Color(0xFF88FAFF) @@ -493,6 +494,7 @@ private fun getCategoryColors(category: SettingsCategory, isDark: Boolean): Pair when (category) { SettingsCategory.LIBRARY -> Color(0xFFD7E3FF) to Color(0xFF005AC1) SettingsCategory.APPEARANCE -> Color(0xFFFFD8E4) to Color(0xFF631835) + SettingsCategory.LYRICS -> Color(0xFFF0E0FF) to Color(0xFF4A1A6E) SettingsCategory.PLAYBACK -> Color(0xFFFFD8EC) to Color(0xFF631B4B) SettingsCategory.BEHAVIOR -> Color(0xFFD7E3FF) to Color(0xFF253347) SettingsCategory.AI_INTEGRATION -> Color(0xFFCCE8EA) to Color(0xFF004F58) From a88c9a11e5b7575c7966867a03359aaaf89736b6 Mon Sep 17 00:00:00 2001 From: Dae Euhwa Date: Wed, 24 Jun 2026 01:44:06 -0500 Subject: [PATCH 07/73] Update CI workflows and build config - Add --no-build-cache to all Gradle build steps for CI reproducibility - Reorder CodeQL workflow steps: pin Kotlin version before Gradle setup - Remove explicit maxHeapSize from app build.gradle.kts - Remove kotlin and ksp version overrides from libs.versions.toml --- .github/workflows/codeql.yml | 17 +++++++---------- .github/workflows/nightly-apk.yml | 2 +- .github/workflows/phone-debug.yml | 2 +- .github/workflows/phone-release.yml | 2 +- .github/workflows/wearos-apk.yml | 2 +- app/build.gradle.kts | 1 - gradle/libs.versions.toml | 6 ------ 7 files changed, 11 insertions(+), 21 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index b2793cfd88..d4bb1bac00 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -29,19 +29,16 @@ jobs: distribution: 'temurin' java-version: '21' - - name: Setup Gradle - uses: gradle/actions/setup-gradle@v6 - with: - cache-provider: 'enhanced' - - name: Pin Kotlin version for CodeQL compatibility run: | - # More robust sed pattern that handles various formats sed -i 's/kotlin = "[^"]*"/kotlin = "2.3.20"/g' gradle/libs.versions.toml - - # Verify the change was made grep 'kotlin = "2.3.20"' gradle/libs.versions.toml - + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v6 + with: + cache-provider: 'enhanced' + - name: Initialize CodeQL uses: github/codeql-action/init@v4 with: @@ -49,7 +46,7 @@ jobs: build-mode: manual - name: Build with Gradle - run: ${{ github.workspace }}/gradlew :app:assembleDebug :wear:assembleDebug --no-daemon + run: ${{ github.workspace }}/gradlew :app:assembleDebug :wear:assembleDebug --no-daemon --no-build-cache - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v4 diff --git a/.github/workflows/nightly-apk.yml b/.github/workflows/nightly-apk.yml index 2c6f7728f5..12a85bebe0 100644 --- a/.github/workflows/nightly-apk.yml +++ b/.github/workflows/nightly-apk.yml @@ -117,7 +117,7 @@ jobs: echo "keyPassword=994273" >> keystore.properties - name: Build Phone nightly release APKs - run: gradle :app:assembleRelease -Ppixelplay.enableAbiSplits=true + run: gradle :app:assembleRelease -Ppixelplay.enableAbiSplits=true --no-build-cache - name: Verify Phone nightly split APKs run: | diff --git a/.github/workflows/phone-debug.yml b/.github/workflows/phone-debug.yml index 3f7d70fb2e..91d799be56 100644 --- a/.github/workflows/phone-debug.yml +++ b/.github/workflows/phone-debug.yml @@ -30,7 +30,7 @@ jobs: uses: gradle/actions/setup-gradle@v6 - name: Build Phone debug APK - run: gradle :app:assembleDebug -Ppixelplay.enableAbiSplits=true + run: gradle :app:assembleDebug -Ppixelplay.enableAbiSplits=true --no-build-cache - name: Verify Phone split APKs run: | diff --git a/.github/workflows/phone-release.yml b/.github/workflows/phone-release.yml index 1c33965a3e..14cd887493 100644 --- a/.github/workflows/phone-release.yml +++ b/.github/workflows/phone-release.yml @@ -52,7 +52,7 @@ jobs: echo "keyPassword=994273" >> keystore.properties - name: Build Phone release APK - run: gradle :app:assembleRelease -Ppixelplay.enableAbiSplits=true + run: gradle :app:assembleRelease -Ppixelplay.enableAbiSplits=true --no-build-cache - name: Verify Phone split APKs run: | diff --git a/.github/workflows/wearos-apk.yml b/.github/workflows/wearos-apk.yml index 6e7b53b6da..862829a51d 100644 --- a/.github/workflows/wearos-apk.yml +++ b/.github/workflows/wearos-apk.yml @@ -30,7 +30,7 @@ jobs: uses: gradle/actions/setup-gradle@v6 - name: Build Wear OS debug APK - run: gradle :wear:assembleDebug + run: gradle :wear:assembleDebug --no-build-cache - name: Upload Wear OS APK artifact uses: actions/upload-artifact@v7.0.1 diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 04d49b171a..dbe91c5898 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -280,7 +280,6 @@ dependencies { // UI Utilities & Extra implementation(libs.timber) - implementation(libs.generativeai) implementation(libs.smooth.corner.rect.android.compose) implementation(libs.reorderables) implementation(libs.codeview) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index e653436ec5..4a104ce637 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -9,7 +9,6 @@ appcompat = "1.7.1" capturable = "3.0.1" codeview = "1.3.9" coilCompose = "2.7.0" -composeDnd = "0.4.0" composeMaterialIcons = "1.7.8" composeUi = "1.11.3" constraintlayoutCompose = "1.1.1" @@ -46,7 +45,6 @@ paletteKtx = "1.0.0" protobufJavalite = "4.35.1" pytorch_android = "2.1.0" pytorch_android_torchvision = "2.1.0" -reorderable = "0.9.6" reorderables = "3.1.0" paging = "3.5.0" roomCompiler = "2.8.4" @@ -88,7 +86,6 @@ wavySlider = "2.2.0" workRuntimeKtx = "2.11.2" composeTesting = "1.0.0-alpha03" timber = "5.0.1" -generativeai = "0.9.0" mockk = "1.14.11" turbine = "1.2.1" truth = "1.4.5" @@ -162,7 +159,6 @@ androidx-work-runtime-ktx = { module = "androidx.work:work-runtime-ktx", version capturable = { module = "dev.shreyaspatil:capturable", version.ref = "capturable" } codeview = { module = "io.github.amrdeveloper:codeview", version.ref = "codeview" } coil-compose = { module = "io.coil-kt:coil-compose", version.ref = "coilCompose" } -compose-dnd = { module = "com.mohamedrejeb.dnd:compose-dnd", version.ref = "composeDnd" } duktape-android = { module = "com.squareup.duktape:duktape-android", version.ref = "duktapeAndroid" } gson = { module = "com.google.code.gson:gson", version.ref = "gson" } hilt-android = { module = "com.google.dagger:hilt-android", version.ref = "hiltAndroid" } @@ -196,7 +192,6 @@ material3 = { module = "androidx.compose.material3:material3", version.ref = "ma protobuf-javalite = { module = "com.google.protobuf:protobuf-javalite", version.ref = "protobufJavalite" } pytorch_android = { module = "org.pytorch:pytorch_android", version.ref = "pytorch_android" } pytorch_android_torchvision = { module = "org.pytorch:pytorch_android_torchvision", version.ref = "pytorch_android_torchvision" } -reorderable = { module = "org.burnoutcrew.composereorderable:reorderable", version.ref = "reorderable" } accompanist-permissions = { module = "com.google.accompanist:accompanist-permissions", version.ref = "accompanist" } smooth-corner-rect-android-compose = { module = "com.github.racra:smooth-corner-rect-android-compose", version.ref = "smoothCornerRectAndroidCompose" } spleeter-android-ios = { module = "com.github.FaceOnLive:Spleeter-Android-iOS", version.ref = "spleeterAndroidIos" } @@ -211,7 +206,6 @@ taglib = { group = "io.github.kyant0", name = "taglib", version.ref = "taglib" } jaudiotagger = { group = "net.jthink", name = "jaudiotagger", version.ref = "jaudiotagger" } vorbisjava-core = { group = "org.gagravarr", name = "vorbis-java-core", version.ref = "vorbisjava" } reorderables = { group = "sh.calvin.reorderable", name = "reorderable", version.ref = "reorderables" } -generativeai = { group = "com.google.ai.client.generativeai", name = "generativeai", version.ref = "generativeai" } retrofit = { group = "com.squareup.retrofit2", name = "retrofit", version.ref = "retrofit" } converter-gson = { group = "com.squareup.retrofit2", name = "converter-gson", version.ref = "retrofit" } okhttp = { group = "com.squareup.okhttp3", name = "okhttp", version.ref = "okhttp" } From 4940904a76cd7d6eed53f63c49e74f3bff263fa5 Mon Sep 17 00:00:00 2001 From: Dae Euhwa Date: Wed, 24 Jun 2026 01:44:06 -0500 Subject: [PATCH 08/73] Misc fixes: AI max tokens and lyrics loading priority AiPreferencesRepository.kt: Increase default AI max tokens from 4096 to 8192 LyricsStateHolder.kt: Fix lyrics loading priority - read local file and embedded lyrics before falling back to stored database lyrics --- .../pixelplay/data/preferences/AiPreferencesRepository.kt | 2 +- .../pixelplay/presentation/viewmodel/LyricsStateHolder.kt | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/theveloper/pixelplay/data/preferences/AiPreferencesRepository.kt b/app/src/main/java/com/theveloper/pixelplay/data/preferences/AiPreferencesRepository.kt index 964875d040..32339a44cd 100644 --- a/app/src/main/java/com/theveloper/pixelplay/data/preferences/AiPreferencesRepository.kt +++ b/app/src/main/java/com/theveloper/pixelplay/data/preferences/AiPreferencesRepository.kt @@ -152,7 +152,7 @@ class AiPreferencesRepository @Inject constructor( dataStore.data.map { preferences -> preferences[Keys.AI_TOP_K] ?: 64 } val aiMaxTokens: Flow = - dataStore.data.map { preferences -> preferences[Keys.AI_MAX_TOKENS] ?: 4096 } + dataStore.data.map { preferences -> preferences[Keys.AI_MAX_TOKENS] ?: 8192 } val aiPresencePenalty: Flow = dataStore.data.map { preferences -> preferences[Keys.AI_PRESENCE_PENALTY] ?: 0.0f } diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/LyricsStateHolder.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/LyricsStateHolder.kt index 87985fd6eb..de7a1a4a28 100644 --- a/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/LyricsStateHolder.kt +++ b/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/LyricsStateHolder.kt @@ -356,9 +356,9 @@ class LyricsStateHolder @Inject constructor( _messageEvents.emit(cb.getString(R.string.lyrics_translate_progress)) val rawLyrics = withContext(Dispatchers.IO) { - currentSong.lyrics?.takeIf { it.isNotBlank() } - ?: readLocalLyricsFile(currentSong) + readLocalLyricsFile(currentSong) ?: readEmbeddedLyricsFromFile(currentSong) + ?: currentSong.lyrics?.takeIf { it.isNotBlank() } ?: musicRepository.getStoredLyrics(currentSong)?.second } From 0f4045bd9c2513dd82e2aae45732f566bd459cc1 Mon Sep 17 00:00:00 2001 From: Dae Euhwa Date: Wed, 24 Jun 2026 01:44:07 -0500 Subject: [PATCH 09/73] Add ServerUrlUtils, Vietnamese/Traditional Chinese translations, and lint baseline - ServerUrlUtils.kt: URL utility for server configuration - values-vi/ and values-zh-rTW/: Vietnamese and Traditional Chinese translation resources - app/lint.xml: Lint baseline configuration file --- app/lint.xml | 20 + .../pixelplay/utils/ServerUrlUtils.kt | 46 ++ app/src/main/res/values-vi/strings.xml | 128 ++++ .../main/res/values-vi/strings_changelogs.xml | 150 ++++ .../res/values-vi/strings_cloud_services.xml | 228 ++++++ .../main/res/values-vi/strings_equalizer.xml | 57 ++ .../res/values-vi/strings_home_screen.xml | 277 ++++++++ .../main/res/values-vi/strings_library.xml | 566 +++++++++++++++ app/src/main/res/values-vi/strings_player.xml | 198 ++++++ .../main/res/values-vi/strings_screens.xml | 244 +++++++ .../main/res/values-vi/strings_settings.xml | 654 ++++++++++++++++++ app/src/main/res/values-vi/strings_widget.xml | 17 + app/src/main/res/values-zh-rTW/strings.xml | 128 ++++ .../res/values-zh-rTW/strings_changelogs.xml | 132 ++++ .../values-zh-rTW/strings_cloud_services.xml | 228 ++++++ .../res/values-zh-rTW/strings_equalizer.xml | 57 ++ .../res/values-zh-rTW/strings_home_screen.xml | 277 ++++++++ .../res/values-zh-rTW/strings_library.xml | 566 +++++++++++++++ .../main/res/values-zh-rTW/strings_player.xml | 198 ++++++ .../res/values-zh-rTW/strings_screens.xml | 244 +++++++ .../res/values-zh-rTW/strings_settings.xml | 652 +++++++++++++++++ .../main/res/values-zh-rTW/strings_widget.xml | 17 + 22 files changed, 5084 insertions(+) create mode 100644 app/lint.xml create mode 100644 app/src/main/java/com/theveloper/pixelplay/utils/ServerUrlUtils.kt create mode 100644 app/src/main/res/values-vi/strings.xml create mode 100644 app/src/main/res/values-vi/strings_changelogs.xml create mode 100644 app/src/main/res/values-vi/strings_cloud_services.xml create mode 100644 app/src/main/res/values-vi/strings_equalizer.xml create mode 100644 app/src/main/res/values-vi/strings_home_screen.xml create mode 100644 app/src/main/res/values-vi/strings_library.xml create mode 100644 app/src/main/res/values-vi/strings_player.xml create mode 100644 app/src/main/res/values-vi/strings_screens.xml create mode 100644 app/src/main/res/values-vi/strings_settings.xml create mode 100644 app/src/main/res/values-vi/strings_widget.xml create mode 100644 app/src/main/res/values-zh-rTW/strings.xml create mode 100644 app/src/main/res/values-zh-rTW/strings_changelogs.xml create mode 100644 app/src/main/res/values-zh-rTW/strings_cloud_services.xml create mode 100644 app/src/main/res/values-zh-rTW/strings_equalizer.xml create mode 100644 app/src/main/res/values-zh-rTW/strings_home_screen.xml create mode 100644 app/src/main/res/values-zh-rTW/strings_library.xml create mode 100644 app/src/main/res/values-zh-rTW/strings_player.xml create mode 100644 app/src/main/res/values-zh-rTW/strings_screens.xml create mode 100644 app/src/main/res/values-zh-rTW/strings_settings.xml create mode 100644 app/src/main/res/values-zh-rTW/strings_widget.xml diff --git a/app/lint.xml b/app/lint.xml new file mode 100644 index 0000000000..6fb7e7f3ea --- /dev/null +++ b/app/lint.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/java/com/theveloper/pixelplay/utils/ServerUrlUtils.kt b/app/src/main/java/com/theveloper/pixelplay/utils/ServerUrlUtils.kt new file mode 100644 index 0000000000..5ac552e63c --- /dev/null +++ b/app/src/main/java/com/theveloper/pixelplay/utils/ServerUrlUtils.kt @@ -0,0 +1,46 @@ +package com.theveloper.pixelplay.utils + +import com.theveloper.pixelplay.data.stream.CloudStreamSecurity +import okhttp3.HttpUrl +import okhttp3.HttpUrl.Companion.toHttpUrlOrNull + +object ServerUrlUtils { + + data class NormalizedUrl( + val url: HttpUrl, + val original: String, + val validationError: String? = null + ) + + fun normalizeUrlOrNull(raw: String): HttpUrl? { + val trimmed = raw.trim().trimEnd('/') + val withScheme = if (!trimmed.startsWith("http://", ignoreCase = true) && + !trimmed.startsWith("https://", ignoreCase = true) + ) { + "https://$trimmed" + } else { + trimmed + } + return withScheme.toHttpUrlOrNull() + } + + fun normalizeUrlString(raw: String): String = + normalizeUrlOrNull(raw)?.toString()?.trimEnd('/') ?: raw.trim().trimEnd('/') + + fun validateConnectionUrl(parsed: HttpUrl): String? { + if (parsed.username.isNotEmpty() || parsed.password.isNotEmpty()) { + return "Server URL must not contain embedded credentials" + } + return null + } + + fun requireSecureForRemote(parsed: HttpUrl, service: String = "server"): String? { + if (parsed.username.isNotEmpty() || parsed.password.isNotEmpty()) { + return "Server URL must not contain embedded credentials" + } + if (!parsed.isHttps && !CloudStreamSecurity.isLocalOrPrivateHost(parsed.host)) { + return "Use https:// for remote $service servers. HTTP is only allowed for local network addresses." + } + return null + } +} diff --git a/app/src/main/res/values-vi/strings.xml b/app/src/main/res/values-vi/strings.xml new file mode 100644 index 0000000000..2cb4e6d908 --- /dev/null +++ b/app/src/main/res/values-vi/strings.xml @@ -0,0 +1,128 @@ + + + PixelPlayer + Music Player + App Name Change + We have changed our app\'s name from PixelPlay to PixelPlayer due to a trademark-related issue. Keep playing! + Do not show again + + + Home + Search + Library + + + Special Permission Required + To edit song metadata (.mp3 files), PixelPlayer needs special access to all files. This allows us to modify track tags directly. Please grant this permission on the next screen to enable metadata editing. + Grant Permission + + + Quick play + Unable to open that audio file. + Open full player + + + Shuffle + Shuffle all songs + Shuffle All + Last Playlist + No playlist available to open + + + Open Play Store + Continue beta + The Play Store link will be enabled from GitHub config. + PixelPlayer is now available on Google Play + Use the stable channel on Google Play for release updates while we keep beta builds active. + PixelPlayer + Release announcement + Coming soon + + + Thank you for using PixelPlayer! + HIGH %1$d + Close + SCORE + LVL %1$d + LIVES + LEVEL COMPLETE! + GAME OVER + Score: %1$d + Try Again? + Next Level + Restart Game + TAP TO RELAUNCH + Play Random Music + Brick Breaker + HIGH SCORE %1$d + Play + Drag to move the paddle + + + Close player + Processing playback action… + Playback error: %1$s + + + Back + OK + Cancel + Dismiss + Error + Search + Clear search + All + Confirm + Saved! + Selected + %1$d%% + Artist + Select All + Clear + Unknown error + + + Save + Done + Reset + Apply + Shuffle + Copy + Share + Undo + Import + Delete + Export + Merge + Rename + Create + Lyrics + Settings + Album Art + Playlist + Unknown track + Unknown artist + Unknown album + Close + Add + Remove + Play + Previous track + Next track + Favorite + Pause + Repeat + Options + Shuffle Play + More options for %1$s + Expand menu + Next + Finish + Reset defaults + Export all + Merge all + Share all + Play album + Shuffle play album + Album art for %1$s + diff --git a/app/src/main/res/values-vi/strings_changelogs.xml b/app/src/main/res/values-vi/strings_changelogs.xml new file mode 100644 index 0000000000..21e8758403 --- /dev/null +++ b/app/src/main/res/values-vi/strings_changelogs.xml @@ -0,0 +1,150 @@ + + + Changelog + View on GitHub + Improvements + Fixes + What\'s New + Added + + Chromecast support for casting audio from your device. + In-app changelog to keep you updated on the latest features. + Support for .LRC files, both embedded and external. + Offline lyrics support. + Synchronized lyrics (synced with the song). + New screen to view the full queue. + Reorder and remove songs from the queue. + Mini-player gestures (swipe down to close). + Added more material animations. + New settings to customize the look and feel. + New settings to clear the cache. + + + Complete redesign of the user interface. + Complete redesign of the player. + Performance improvements in the library. + Improved application startup speed. + The AI now provides better results. + + + Fixed various bugs in the tag editor. + Fixed a bug where the playback notification was not clearing. + Fixed several bugs that caused the app to crash. + + + Introduced a richer listening stats hub with deeper insights into your sessions. + Launched a floating quick player to instantly open and preview local files. + Added a folders tab with a tree-style navigator and playlist-ready view. + + + Refined the overall Material 3 UI for a cleaner and more cohesive experience. + Metadata editing now supports cover art change. + Smoothed out animations and transitions across the app for more fluid navigation. + Enhanced the artist screen layout with richer details and polish. + Upgraded DailyMix and YourMix generation with smarter, more diverse selections. + Strengthened the AI playlist generation. + Improved search relevance and presentation for faster discovery. + Expanded support for a broader range of audio file formats. + + + Resolved metadata quirks so song details stay accurate everywhere. + Restored notification shortcuts so they reliably jump back into playback. + + + Major navigation redesign + New file explorer for choosing source directories + New Connectivity and casting functionalities + Seamless continuity between remote devices + Gapless transition between songs + Crossfade control + New Custom Transitions feature (only for playlists) + Keep playing after closed the app + UI Optimizations + Improved stats feature + Redesigned Queue control with more features + Improved different filetypes support for playing and metadata editing + Improved permission controller + Minor bug fixes + + + Material 3 Expressive UI Update + 10-band Equalizer & Effects + New Library Sync Flow + AI Integration (Gemini Models) + M3U Playlist Import/Export + Deezer Artist Artwork Integration + Custom Playlist Covers + Settings Architecture Refactor + Queue & Player Animations + Baseline Profiles & Performance + Better Lyrics System with Sync Offset + + + Casting Stability Improvements + Player Sheet Stability + General Bug Fixes & Cleanup + + + Android Auto support is now available for in-car playback. + Wear OS support is live, including better watch-to-phone playback controls. + Cloud integrations expanded with Telegram, NetEase, QQ Music, and Google Drive improvements. + Recently Played and persistent queue restoration keep your listening session ready. + Backup & Restore v3 and account management tools are now included. + Lyrics got smarter with manual search fallback and storage improvements. + + + Big performance pass across startup, library, queue, and player interactions. + Player, Cast, Lyrics, Artist, and Genre surfaces were redesigned for smoother use. + Navigation and search flows are more reliable, with safer route handling. + Audio playback compatibility improved for more devices and formats. + Multi-selection workflows were expanded across songs, albums, and playlists. + + + Queue and shuffle behavior is now more stable and predictable. + Several background playback and casting edge cases were fixed. + Sleep Timer, Files tab navigation, and album artist crash issues were fixed. + Widget loading and service stability were improved to reduce overheating/memory issues. + General bug fixes and UI polish across the app. + + + Wear OS: Music transfer, local playback, queue sync, and remote control from watch. + AI: Groq AI and OpenRouter (experimental) integration with token optimization. + Cloud: Jellyfin support added. + Lyrics: Synced translation with dedicated toggle, Kugou LRC format support, text alignment customization, and improved remote loading. + UI/UX: Compact navigation bar mode, dynamic themes from album art palette, marquee for long titles, and new sorting options. + Telegram: Native topics support and enhanced display modes. + + + Audio Engine: Complete overhaul with support for more formats (MIDI, ALAC, M4A) and decoder optimization. + Efficiency: Drastic energy consumption reduction, overheating fixes, and background task optimization (SyncWorker). + Database: Massive query optimizations and redesigned cover art cache to prevent data loss. + Startup: Improved loading time via Baseline Profile optimization. + + + Playback: Fixed stuttering in Opus/MP3, ReplayGain errors during crossfades, and startup issues on Samsung decoders. + Stability: Eliminated crashes on startup, artist navigation, and Android 12+ devices. + UI: Fixed cover art flickering, text overflow in non-Latin scripts, and navigation bar/miniplayer behavior. + Security: Hardened credential handling, storage permissions, and media server communication. + + + Localization: Spanish, French, Russian, Simplified Chinese, Indonesian, Italian + + + Google Drive integration with player lifecycle management. + Batch song metadata editing (tags and cover art). + AI lyrics translation with customizable Wear OS preferences. + Lag diagnostic tool and multi-selection on Search screen. + Arabic & Turkish support, with localized http URL local-network options. + + + Drastic battery saving (audio offload and UI polling gates). + Optimized queue management (faster insertions and explicit indexing). + Material 3 Expressive motion animations for transition screens. + Refactored library synchronization via throttled scanning. + + + Resolved stuttering/skipping playback lags and buffering issues. + Fixed external song deletion sync and metadata consistency. + Fixed memory issues, crashes, and layout glitches on Wear OS and phone. + + \ No newline at end of file diff --git a/app/src/main/res/values-vi/strings_cloud_services.xml b/app/src/main/res/values-vi/strings_cloud_services.xml new file mode 100644 index 0000000000..df633dab6a --- /dev/null +++ b/app/src/main/res/values-vi/strings_cloud_services.xml @@ -0,0 +1,228 @@ + + + + Telegram Login + You are editing your number. Sending code again will replace the previous one. + Working… + Initializing Telegram… + Logging out… + Closing session… + Session closed. Re-open login to continue. + Preparing secure Telegram session… + Waiting for Telegram response… + Connect Telegram + Connect Telegram to stream music from your channels and chats. + Phone Number + Enter your Telegram number. You can come back and edit it later. + Phone number + 1 + 5551234567 + Send Code + Verification Code + Enter the code from Telegram. If the number is wrong, go back and edit it. + Code + 12345 + Edit phone + Resend code + Verify Code + Two-Step Password + Enter your Telegram password. You can still go back to fix your number. + Password + Verify Password + Please wait… + + + Telegram Channels + Add Channel + Public Telegram channel + Syncing + Sync now + Collapse topics + Show topics + Channel options + Topics + Syncing channel + Updating songs from Telegram + Fetch latest songs from this channel + Remove channel + Stop syncing and remove cached songs + Remove channel? + %1$s will stop syncing and all cached songs from this channel will be removed. + Remove + No Channels Synced + Add public Telegram channels to sync\nyour music library + Add channel + Never synced + Synced %1$s + + + Add Channel + Search for a public Telegram channel to sync its music + \@channelname or link + Searching… + Search for a channel + Enter a public channel username or link\nto sync its audio files + + + %d song + %d songs + + + %d topic + %d topics + + + + Subsonic + Manage Navidrome, Airsonic and other Subsonic-compatible servers. + + + Tap sync to fetch your Jellyfin playlists + Manage your Jellyfin server connection. + + + Music folders + Tap + to add a Drive folder + No folders added yet + %1$d folders synced + Add folder + + + Select playlist type + Choose which playlists to sync: + All playlists + Created & collected + Created playlists + Collected playlists + + + %1$d playlists synced + Playlists + Sync + No playlists synced yet + Tap sync to fetch your playlists + Quick actions + Sync library + Disconnect + %1$d songs + + + Syncing + Syncing library… + Fetching playlists… + Syncing playlist: %1$s + Updating local library… + Sync complete + Fetching album list… + Fetching songs from %1$s… + Saving %1$d songs to database… + No library songs found + Library sync complete + Syncing… + Error: %1$s + + + Sync + Sync all + Log out + Sync all playlists + User avatar + + + No internet connection + This content requires an internet connection. Please check your network settings and try again. + You\'re offline + Please check your internet connection and try again to access this content. + + + Connect + Connecting… + Enter your server URL and account credentials. + Connection details + Hide password + Password + Enter password + Prefill http:// + Server URL + Show password + Telegram + Username + admin + Welcome, %1$s! + + + Compatible with Navidrome, Gonic, Airsonic, and other Subsonic-compatible servers + Supports Navidrome, Airsonic, Gonic, Ampache and other servers compatible with the Subsonic API. + App password also works if your server supports it. + Prefill https:// + Connect to your self-hosted music server + Navidrome + Use the full https:// base address of your server. + https://music.example.com + This is your Subsonic or Navidrome account name. + Subsonic / Navidrome + Subsonic + + + Enter your Jellyfin server URL and account credentials. + Connects to Jellyfin servers for streaming your music library + Connects to Jellyfin servers. Both HTTP and HTTPS are supported for local network access. + Jellyfin + Your Jellyfin account password. + Connect to your Jellyfin media server + Jellyfin + Full URL of your Jellyfin server, including port. + http://192.168.1.100:8096 + Your Jellyfin account username. + + + Stream music files directly from your Google Drive + Connect Google Drive + Google Drive connected! + Create \"PixelPlayer Music\" + Create a new folder here for your music + No folders here + Open folder + Choose or create a folder to use as your music source + Select a music folder + Setting up Google Drive… + Sign in with Google + Google Drive + Use + + + Could not read session cookies. + Done + Exit + No cookies found. Log in first. + Page is taking too long to load. Use refresh or try another network. + + + Saving… + Stay + Page load timed out. You can retry without losing your progress. + Web back + You can come back later. Current page state will be discarded when closing. + Web forward + Refresh + Retry + Open home + WebView load failed. + + + Failed to read NetEase cookies: %1$s + Exit NetEase login? + HTTP %1$d while loading NetEase. + Login not detected yet. Complete NetEase login before pressing Done. + Login to NetEase Music + Security note: your password is entered only in NetEase web pages. PixelPlayer stores session cookies (MUSIC_U) to sync your library. + NetEase Music + + + Failed to read QQ Music cookies: %1$s + Exit QQ Music login? + HTTP %1$d while loading QQ Music. + Login not detected yet. Complete QQ Music login before pressing Done. + Login to QQ Music + Security note: your password is entered only in QQ Music web pages. PixelPlayer stores session cookies to sync your library. + QQ Music + \ No newline at end of file diff --git a/app/src/main/res/values-vi/strings_equalizer.xml b/app/src/main/res/values-vi/strings_equalizer.xml new file mode 100644 index 0000000000..c633b7bf65 --- /dev/null +++ b/app/src/main/res/values-vi/strings_equalizer.xml @@ -0,0 +1,57 @@ + + + + Name cannot be empty + Rename + + + Change view mode + Disable equalizer + Enable equalizer + Edit + Edit presets + Custom preset + Presets + Update + Bass Boost + Virtualizer + Loudness + Not supported + Not supported on this device + Volume + Frequency Response + Hz + Bass + Low Mids + High Mids + Treble + Bass / Low + Mid / High + Page %1$d + Reset duration + Save New + + + Saved Presets + No custom presets saved yet. + Unpin + Pin + Rename + Delete + + + Save custom preset + Enter a name for your custom equalizer preset. + Preset name + Rename preset + + + Manage presets + Drag to reorder • Tap the eye to show or hide + Reorder + Reset presets + This will restore the default preset order and visibility. Continue? + Reset to default + Visible + Hidden + diff --git a/app/src/main/res/values-vi/strings_home_screen.xml b/app/src/main/res/values-vi/strings_home_screen.xml new file mode 100644 index 0000000000..2a7e3ad80f --- /dev/null +++ b/app/src/main/res/values-vi/strings_home_screen.xml @@ -0,0 +1,277 @@ + + + + β + Beta + Cloud Streaming + Changelog + Cloud Streaming + Stream music from your cloud accounts + + + Beta 0.7.5 + β + Welcome to PixelPlayer 0.7.5-beta + You\'re using a beta build that may contain bugs, crashes, or experimental features. Help us improve by reporting issues. + What to expect + Bugs, crashes, or incomplete features may occur unexpectedly. + Some features may change or be removed without notice. + Beta builds may be more unstable than release versions. + Always check for updates before reporting a known issue. + What beta builds can change, break, or improve while testing. + GitHub issue shortcut + Search first, then open a focused report for bugs, crashes, requests, or questions. + Open existing issues + Report issue or crash + Share steps to reproduce, expected results, actual results, and your device/OS details. + How to report + A quick checklist before opening a new issue. + Before opening an issue + Search existing open and closed issues to avoid duplicates. + Update to the latest PixelPlayer version and confirm the problem still happens. + Restart the app and confirm the problem persists. + Try to reproduce it and write down the exact steps. + Which issue type? + Bug report: Something behaves incorrectly. + Feature request: Add a new feature or improvement. + Question: Use Discussions if enabled, or open an issue with a question label. + Bug report + Copy these fields when something behaves incorrectly or crashes. + Bug Report + Short summary: + Expected behavior: + Current behavior: + Steps to play/reproduce: 1. 2. 3. + How often does it happen? Always / Sometimes / Rarely. + Screenshot / video: if available. + Logs / stack trace: if available. + Environment + PixelPlayer version: + Install source: GitHub Release, debug build, nightly build, etc. + Android version: + Device model: + Extra context: SD card usage, special settings, permissions, etc. + Feature request + Copy these fields when you want a new feature or improvement. + Problem statement: What problem are you trying to solve? + Proposed solution: How should it work? + Alternatives considered: Any other approaches? + Scope: Which screens or flows are affected? + Mockup or reference image if available. + Titles, privacy, and scope + Make the report easy to triage and safe to share. + Good issue titles + Equalizer: Indicator shifts when switching preset tabs + Search: History list does not appear for empty query + Feature: Add \"Recently Added\" playlist sort option + Please avoid + Generic reports like \"It doesn\'t work\". + Multiple unrelated problems in one issue. + Unredacted logs or screenshots with private data. + Privacy note + Before posting logs, screenshots, or videos, remove personal or private information. + Nightly builds + How nightlies differ from releases, and what to include when they break. + Nightly builds are generated from the latest commit and may contain unfinished changes, temporary bugs, or regressions. They are more experimental than official releases. + Access them from the repository\'s GitHub Actions workflow artifacts if available. + Reporting nightly issues + When reporting an issue from a nightly build, always mention that it happened on a nightly build, not on the official release. Include the build date, workflow run name or number, or commit SHA if possible. Also check if the same problem happens on the latest official release. + Beta 0.5.0 upgrade + Clean install recommended + If you\'re coming from beta 0.5.0, this update may need fresh library data instead of older cached state. + If metadata or library entries look wrong + Wrong song metadata, mismatched artists or albums, or duplicate-looking entries usually mean a clean install is the fix. + Don\'t show again + Got it + + + Oops! Something went wrong + The app crashed during your last session. Help us fix this by sharing the crash report. + Date: %1$s + Error: + Stack trace (preview): + Crash log + Crash log copied to clipboard + PixelPlayer crash report + Share crash report + + + DJ Mashup + + + Your\nMix + No data to show yet + Your mix will appear here when PixelPlayer finds songs or syncs a source. + Refresh + + + DAILY MIX + Based on History + Check all of Daily Mix + Daily Mix + + + Daily Mix + + %1$d song • %2$s + %1$d songs • %2$s + + Play it + AI Playlist Generator + + + How your Daily Mix is built + Your Daily Mix is built from your favorite and most-played songs. We also add tracks from artists and genres you like so you can discover new music. + Tell the AI what you want to listen to today + We use a small sample to keep costs low + Updating… + Update Daily Mix + + + Perfectly Curated + Daily Mix + Your sonic journey is ready + AI Playlist Generator + Describe the vibe, mood, or activity and let AI curate the perfect playlist from your library. + Playlist size + Min songs + Max songs + e.g. Chill evening vibes, upbeat workout energy… + Tap to Retry + Sonic journey synthesized! + Ready to Play + Generating… + Generate Playlist + + + Recently Played + + + Recently Played + Play latest + No recent plays in %1$s + Change the range or play more songs to fill this timeline. + Recently Played + Today + Yesterday + + + Listening stats + Total plays + Avg per day + Top track + %1$s • %2$d plays + + + Listening Stats + Refresh listening stats + Today + Week to Date + Month to Date + Year to Date + All Time + Listening + Plays + Listening timeline + Listening time + Total listening captured in the selected range. + Play count + How many sessions you completed per segment. + Avg. session + Average listening length for each segment. + Split into 4-hour windows to reveal your daily rhythm. + Daily bars make week-to-week habits easy to compare. + Weekly bars show how the month is trending. + Monthly bars show seasonality across the year. + Yearly bars summarize your full history. + No listening data yet + Press play to start building your listening timeline + Daily rhythm + Weekly rhythm + Monthly rhythm + Year at a glance + All-time progression + Grouped into 4-hour segments + Grouped by day of week + Grouped by week of month + Grouped by month + Grouped by year + Peak segment + %1$d plays + + Top categories + Compare how you listen across genres, artists, albums, and songs. + Genre + Artist + Album + Song + Listening by genre + Listening by artist + Listening by album + Listening by song + %1$d plays • %2$d artists + %1$d plays • %2$d tracks + No category data yet + Press play to surface your listening highlights + Listening habits + No habits yet + We will surface your listening habits once we know you better. + Total sessions + Avg session + Longest session + Sessions/day + Most active day + No playback yet + Peak timeline slot + Top artists + No top artists + Keep listening and your favorite artists will show up here. + \? + %1$d. %2$s + Top albums + No top albums + Albums you revisit often will appear here. + %1$d. %2$s + Track concentration + How your listening time is distributed across your top tracks. + No concentration data yet + Play more tracks to see how focused your listening is. + Top 1 + Top 2-3 + Others + %1$d%% + Listening concentration + Top 3 tracks account for %1$d%% of your listening time. + Avg plays/track + Unique tracks + Top 3 share + Tracks in this range + Most played tracks for the selected time range. + No top tracks + Listen to your favorites to see them highlighted here. + Collapse tracks + Show all tracks + + + %1$d h %2$02d min + %1$d min + %1$d h %2$02d m + %1$d h + %1$d m + %1$d s + %1$dh %2$02dm + %1$dh + %1$dm + %1$ds + Never + Just now + 1 day ago + %1$d days ago + 1 hour ago + %1$d hours ago + 1 minute ago + %1$d minutes ago + %1$d Song + %1$d Songs + Week %1$d + diff --git a/app/src/main/res/values-vi/strings_library.xml b/app/src/main/res/values-vi/strings_library.xml new file mode 100644 index 0000000000..8b7c6c2c60 --- /dev/null +++ b/app/src/main/res/values-vi/strings_library.xml @@ -0,0 +1,566 @@ + + + + Library + Library tabs + Jump directly to any tab or reorder them. + Reorder tabs + + + Songs + Albums + Artists + Playlists + Folders + Liked + + + Playlist created successfully + Set your AI provider API key first + Set your Gemini API key first + Added to queue + Playing next + + + Watch transfer + Settings + Edit + Reorder tabs + Expand menu + + + You can select up to %1$d albums + Folder + Folder + + + Sort by + View + Playlist View + Grid + List + Internal + SD Card + SD card is not available right now. + Cloud + Telegram Cloud Channels + Topics Display + Channels + Topics + Both + Cloud + Cloud Only + + + Generating metadata with AI… + + + Error loading songs + Error loading albums + Error loading artists + Retry + + + No songs found in your library. + Try rescanning your library in settings if you have music on your device. + No songs found + + + New + Create new playlist + Import M3U playlist + Locate current song + All songs + CLOUD + LOCAL + Sort options + + + All + Deselect + More options + + + Scanning music files… + Processing files… + %1$d of %2$d files + Syncing library… + Sync complete + Waiting… + Syncing library… + Cleaning album art cache… + Syncing cloud sources… + Scanning lyrics… + + + No songs yet + Add music to your device or sync a cloud source to start listening. + No local songs found + Try another source filter or rescan your device library. + No cloud songs found + Sync Telegram or NetEase songs, or switch to local source. + No albums available + Albums will appear here as soon as your library has grouped tracks. + No local albums found + Local songs are required to build local album groups. + No cloud albums found + Cloud songs with album data will appear here after sync. + No artists available + Artists are shown after songs are indexed from any source. + No local artists found + No artist metadata is available for local songs right now. + No cloud artists found + Cloud artist entries appear when remote songs are synced. + No liked songs yet + Tap the heart icon while playing a song to save it here. + No liked local songs + Switch source filter or like songs from your device. + No liked cloud songs + Like Telegram or NetEase tracks to see them in this view. + No folders found + Internal storage folders with music will appear here. + No playlists yet + Create your first playlist to organize your library. + + + Edit song metadata + Play + Play song + Play all + Play all + Add to favorites + Add all to favorites + Remove from favorites + Remove all from favorites + Share song file via + Share song file + Share all as ZIP + Could not share song: %1$s + Add to queue + Add to queue + Next + Play next in queue + Add to playlist + Delete + Delete all + Checking watch + Transferring %1$d%% + Transferring to watch + Transfer in progress + Send to watch + Watch unavailable + Send song to watch + Watch unavailable + Set as + Set as sound + Choose how to use this song as a system sound + Use this song as + Choose where PixelPlayer should install this sound. + Phone ringtone + Incoming calls + Notification sound + Messages and app alerts + Alarm sound + Clock alarms + Confirm sound change + Set \"%1$s\" as your %2$s? + Set sound + Set \"%1$s\" as your %2$s + ringtone + notification sound + alarm sound + Enable Modify system settings, then return to PixelPlayer to finish automatically. + Modify system settings was not enabled. + Set \"%1$s\" as your ringtone + Only local songs can be used as ringtones. + Couldn\'t prepare this audio file for ringtone. + Couldn\'t set ringtone: %1$s + Options + OPTIONS + Info + INFO + Duration + Genre + Album + Artist + Song info + Provider + File + %1$d SONGS + selected + %1$d PLAYLISTS + %1$d ALBUMS + selected + Limit: %1$d albums per selection. + Queue + play respects your selection order. + %1$d GENRES + selected + Perform batch operations on all songs within these genres. + + + Default Order + Title (A-Z) + Title (Z-A) + Artist + Artist (Z-A) + Album + Album (Z-A) + Date Added + Date Added (Oldest First) + Duration + Duration (Shortest First) + Release Year + Release Year (Oldest First) + Fewest Songs + Most Songs + Name (A-Z) + Name (Z-A) + Number of Songs (Most) + Number of Songs (Fewest) + Date Created + Date Created (Oldest First) + Date Liked + Date Liked (Oldest First) + Fewest Subfolders + Most Subfolders + + + Title + Artist + Album + Date Added + Duration + Release Year + Song Count + Name + Number of Songs + Date Created + Date Liked + Subfolder Count + + + Source + Order + Descending + Ascending + Original Order + Tap to switch to ascending + Tap to switch to descending + This sort keeps its original order + Switch is on + + + Reorder library tabs + Reset order + Reset tab order to the default? + Reordering tabs… + Drag handle + + + Pick an Artist + 1 artist + %1$d artists + Primary artist + Artist page + + + Cancel transfer + %1$s / %2$s + Shows live progress for phone-to-watch music transfers + Watch transfers + Sending to Watch + Cancelled + Transfer cancelled + Transfer complete + Completed + Failed + Transfer failed + Multiple active transfers + %1$s • %2$s + Preparing + Preparing watch transfer + Preparing transfer… + Sending %1$d songs to watch + Sending to watch + Starting transfer… + Starting + Transferring + %1$d transfers + + + Edit song + Show information + Editing song metadata + Editing a song\'s metadata can affect how it\'s displayed and organized in your library. Changes are permanent and may not be reversible. + Got it + Information + Cover art + Select a square image and fine-tune it so your cover art looks great across the app. + Change cover art + Delete cover art + Title + Artist + Album + Album artist + Genre + Composer + Track number + Disc number + ReplayGain track (dB) + ReplayGain album (dB) + -6.50 + -8.20 + Preview of the new cover art + Current song cover art + Adjust your cover art + Use pinch and drag gestures to find the perfect framing. + Apply cover art + Unable to load the selected image + Search lyrics on lrclib.net + + + Edit %d Songs + Only modified fields will be updated. Leave fields empty to keep existing values. + (Mixed values) + (Optional - leave empty to skip) + Successfully updated %d songs + Updated %1$d of %2$d songs. Some files could not be edited. + Failed to update songs + Batch Cover Art + This will replace the cover art for all %d selected songs + Set Cover Art for All + Remove All Cover Art + (Multiple different covers) + + + Playlist dismissed + + + Create playlist + Choose the creation flow. + Manual + Design artwork, icon, shape and pick songs yourself. + With AI + Generate a curated playlist with advanced controls. + Requires a Gemini API key configured in settings. + Set up API key + + + AI Playlist Lab + Reset + Generating… + Generate + Intent + Playlist name (optional) + What should this playlist feel like? + Example: sunset drive with warm synths + Direction + Mood + Activity + Era + Curation engine + Energy + Controls the intensity and tempo of songs. 1 = calm/slow, 5 = high-energy/fast. + Discovery + Controls how familiar the selections are. 1 = your most played favorites, 5 = rarely played deep cuts. + Min songs + Max songs + Filters + Prioritize genres (optional) + e.g. synthwave, indie pop + Avoid genres (optional) + e.g. metal, hard trap + Preferred language (optional) + e.g. English, Spanish, instrumental + Prioritize favorites + Avoid explicit lyrics + Prompt preview + Your final prompt will appear here once you add preferences. + Curate with precision + Define mood, activity, constraints and depth. + The AI will only use songs from your local library. + Add at least one instruction for AI. + Set a valid song range. + %1$d/5 + Custom… + Enter custom value + Enter your custom value + + + Any era + Core request: %1$s. + Mood target: %1$s. + Activity context: %1$s. + Era focus: %1$s. + Prioritize genres: %1$s. + Avoid genres: %1$s. + Preferred language: %1$s. + Energy level target: %1$d/5. + Discovery target: %1$d/5 where 1 is familiar and 5 is deep cuts. + Prioritize songs closer to listener favorites when possible. + Avoid explicit lyrics whenever alternatives exist. + Keep transitions smooth and avoid repetitive artist clustering. + + Chill + Energetic + Happy + Dark + Romantic + Melancholic + + + Workout + Focus + Road trip + Party + Study + Late night + + + @string/playlist_creation_ai_era_any + 70s + 80s + 90s + 2000s + 2010s + 2020s + + + + No playlist has been created. + Touch the \'New Playlist\' button to start. + New playlist + Playlist name + My playlist + + + Add %1$d songs to… + Select playlists + Search for playlists… + Songs added to playlists + Playlist created and songs added + Internal Storage + + + Add songs + Add selected songs + Add + Search or filter songs… + Liked + Failed to load songs + Load more + + + Merge Playlists + Enter a name for the merged playlist: + Merged Playlist + This will merge %1$d selected playlists into one. + + + No valid songs found to play + Song not found in current list + Could not locate song + No songs found in library + Playback stopped: %1$s finished (End of Track). + Track + No songs to shuffle. + Selected Albums + No playable songs found in selected albums + No playable songs found in selected genres + Only the first %1$d albums were queued + %1$d albums queued (%2$d songs) + Could not queue selected albums + All songs already in favorites + No songs were in favorites + Creating ZIP file… + Failed to share: %1$s + + %d song added to queue + %d songs added to queue + + + %d song will play next + %d songs will play next + + + %d song added to favorites + %d songs added to favorites + + + %d song removed from favorites + %d songs removed from favorites + + + + No playlists to share + Share playlists + Share failed: %1$s + No playlists to export + Export failed: %1$s + Music/PixelPlayer Exports + Please configure your Gemini API key in Settings. + Playlist restored + + Sharing %d playlist + Sharing %d playlists + + + Exported %1$d playlist to %2$s + Exported %1$d playlists to %2$s + + + + Invalid album ID + Album ID not found + Error loading album data: %s + Album not found + + + Invalid artist ID + Artist ID not found + Error loading artist data: %s + Could not find the artist + + + Cannot delete currently playing song + %1$d files deleted (%2$d skipped - playing) + %1$d of %2$d files deleted + Failed to delete files + File deleted + Can\'t delete the file or file not found + Deletion cancelled + Delete song? + \"%1$s\" by %2$s\n\nThis song will be permanently deleted from your device and cannot be recovered. + These songs will be permanently deleted from your device and cannot be recovered. + + %d file deleted + %d files deleted + + + Delete %d song? + Delete %d songs? + + + + Metadata updated successfully + Updating %1$d songs… + Successfully updated %1$d songs! + Updated %1$d songs. Failed: %2$d + Lyrics saved successfully + Failed to save lyrics + No lyrics available to save + Permission denied – cannot edit files + Permission denied – cannot save lyrics + Permission denied – cannot edit this file + + + Please configure a valid API key for the selected AI provider in Settings. + AI Error: %s + The selected AI provider rejected the request because the account has no credits or available quota. + The selected AI model is no longer available. PixelPlayer tried to switch to a supported model automatically. + AI couldn\'t find any songs for your prompt. + Write an idea for your Daily Mix + Daily Mix updated with AI + Could not update: %s + AI couldn\'t find songs for this mix + \ No newline at end of file diff --git a/app/src/main/res/values-vi/strings_player.xml b/app/src/main/res/values-vi/strings_player.xml new file mode 100644 index 0000000000..e5a3b76777 --- /dev/null +++ b/app/src/main/res/values-vi/strings_player.xml @@ -0,0 +1,198 @@ + + + + Collapse player + Now Playing + Cloud stream + Cast + Bluetooth + Local playback + Connecting… + Open Queue + + + Get ready to connect + Allow PixelPlayer to see your nearby devices and current Wi‑Fi so we can keep your cast, Bluetooth audio, and speakers in sync. + Nearby devices + Needed to read and control your connected Bluetooth audio gear. + Location for Wi‑Fi + Android requires Location to share the Wi‑Fi network (SSID) you\'re on so we can find compatible cast devices. + Allow access + We only use these permissions for device interconnectivity — casting, controlling nearby speakers, and keeping audio in sync. + Connect device + Scanning nearby + Casting session + Connecting + Connected + This phone + Bluetooth audio + Local playback + Playing + Paused + Device volume + Phone volume + %1$d/%2$d + Battery level + Volume level + Disconnect + Connectivity + Turn on Wi-Fi or Bluetooth + Refresh connections + Wi-Fi + Off + On + Connected + Bluetooth + Off + On + Connected + Nearby devices + Refresh devices + Connected + Connecting + Available to connect + Available + Connecting... + Searching for devices… + Make sure your TV or speaker is on and sharing the same Wi‑Fi network. + Controls + Devices + + + Cast media server + Casting to device + Serving media to Cast device + %1$s: %2$s + Seeking is temporarily unavailable for this audio format on Cast because it can crash the Cast session. + + + Sleep timer + Timer + %1$d minutes + Timer set for %1$d minutes. + 1 time + + %d time + %d times + + Play count: %1$s + End of current track + Playback will stop at end of track. + Switch on + Custom time + Cancel timer + End of track + Timer cancelled. + Cannot enable end of track: no active song. + End of track timer deactivated: song changed from %1$s to %2$s. + Previous track + Current track + Set custom duration + + + Next up + Queue is empty for now. + + 1 track lined up. + %d tracks lined up. + + Queue + Queue is empty. + Reorder song + Toggle shuffle + Toggle repeat + Sleep timer + More actions + Locate current song + Clear queue + Clear queue + Are you sure you want to clear all songs from the queue except the current one? + Save as playlist + %1$s queue + Current queue + Dismiss song + removed + Save as playlist + Deselect all + Playlist name + Search songs to include… + No songs match \"%1$s\" + + %d song selected + %d songs selected + + Save as: %1$s + Enter a playlist name + Remove from playlist + More options for %1$s + + + Lyrics + Loading lyrics… + Synced + Static + Lyrics options + −.5 + −.1 + +.1 + +.5 + 0s + %1$+.1fs + + + Failed to search for lyrics + Failed to fetch lyrics from remote + Connection timed out. Please check your internet connection. + Network error. Please check your internet connection. + Server error (code %d). Please try again later. + + + Lyrics already available. Online fetch skipped. + Embedded lyrics already found. Online fetch skipped. + Local (.lrc) lyrics already found. Online fetch skipped. + + + Save Lyrics + Translate via AI + These lyrics already have a translation + These lyrics are already in this language + API is not configured + Lyrics translated successfully! + Translating lyrics... + Reset imported lyrics + Reset lyrics? + Are you sure you want to reset the lyrics for this song? + Appearance + Alignment + Align lyrics left + Align lyrics center + Align lyrics right + Controls + Adjust sync + Hide sync controls + Show romanization + Show translations + Disable immersive (once) + Keep screen on + + + Save Lyrics + Choose which version to save: + Synced (with timestamps) + Plain (text only) + + + Would you like to search for lyrics online? + Show lyric options + Always open the picker instead of auto-applying the first match + Searching for lyrics… + Lyrics not found + We couldn\'t find lyrics automatically. You can edit the title or artist and try searching manually. + Song Title + Artist (optional) + Found %d match(es) + SYNCED + %1$s • %2$s + Lyrics provided by + https://lrclib.net/ + diff --git a/app/src/main/res/values-vi/strings_screens.xml b/app/src/main/res/values-vi/strings_screens.xml new file mode 100644 index 0000000000..90ec1b118e --- /dev/null +++ b/app/src/main/res/values-vi/strings_screens.xml @@ -0,0 +1,244 @@ + + + + Error: Genre ID missing + + + Let\'s Go! + Step %1$d of %2$d + Please grant the required permission first. + Please grant all required permissions. + Welcome to + β + Beta + Let\'s get everything set up for you. + Media Permission + PixelPlayer needs access to your audio files to build your music library. + Permission Granted + Grant Media Permission + Notifications + Enable notifications to control your music from the lock screen and notification shade. + Enable Notifications + Do you have a backup? + If you already have a PixelPlayer backup, restore it now and skip most of the remaining setup on this device. + Import backup + Inspecting backup + Checking backup package… + Restoring backup + Skip / Not now + Restore Backup + Review what you want to import before finishing setup. + %1$d of %2$d modules selected + Created %1$s + Backup from %1$s + Unknown version + Restore selected + Restoring + Excluded folders + All folders are scanned by default. Pick any locations you want to ignore when building your library. + Choose folders to ignore + Grant storage permissions first + App Theme + Pick the look you want before you start exploring your library. + Dark + The default Material 3 dark look for PixelPlayer. + Light + A brighter Material 3 look across the app. + Follow system + Match your phone\'s current appearance setting. + Recommended + You can change this later in Settings > Appearance > App Theme. + Library Layout + Choose your preferred way to navigate your library. + Songs + Compact Mode + Using minimal pill navigation + Using standard tab row + SONGS + ALBUMS + ARTISTS + You can change this later in Settings > Appearance > Library Navigation. + App Navigation + Choose the style of the bottom navigation bar. + Default Style + Floating pill with rounded corners + Standard full-width bar + Customize Corner Radius + You can change this later in Settings > Appearance > Navbar Style. + Alarms & Reminders + Optional, but recommended if you use Sleep Timer and want PixelPlayer to stop playback exactly on time. + Grant Permission + Battery Optimization + Some Android devices aggressively kill background apps. Disable battery optimization for PixelPlayer to prevent unexpected playback interruptions. + Disable Optimization + All Set! + You\'re ready to enjoy your music. + + + Search… + Search + Clear search + Recent searches + Clear all + History + Delete search history item + No results + No results for \"%1$s\" + Nothing found + Try a different search term or check your filters. + No results found. + Browse by genre + No genres available. + + + Play %1$s + Collapse %1$s + Expand %1$s + Edit artist image + Change photo + Reset to default + Shuffle play artist + + + Disc %d + Cover of %1$s + %1$s · %2$s + + + Playlist not found. + This playlist is empty. + Tap on \'Add Songs\' to begin. + This folder doesn\'t contain songs. + Sort Songs + More Options + Playlist options + Edit playlist + Delete playlist + Delete playlist? + Are you sure you want to delete this playlist? + Set default transition + Export Playlist + %1$s • %2$s + Play it + Add + Add songs + Remove + Remove songs + Reorder + Reorder songs + + + Global transitions + Playlist rules + This configuration applies to all playback sources unless overridden. + Configure default behavior for this specific playlist. + Active Status + Global Default + Playlist Default + Following Global + Custom Override + Custom Override + Enable to set specific rules for this playlist. + Using global defaults + Changes saved successfully + Transition Style + How tracks blend together + None + Crossfade + Transition Duration + %1$ds total overlap + Reset Transition + Current Song + Next Song + Tracks will overlap for %1$ds + Volume Curves + Fine tune the audio slope + Fade Out + Fade In + + + New smart playlist + New playlist + Add Songs + Back or Cancel + Next + Create + Edit Playlist + Auto-generated collage + Add Photo + Pick Image + Change + Remove + Playlist Name + My awesome mix + Edit Cover + Adjust your cover art + Use pinch and drag gestures to find the perfect framing + Manual + Smart + Generate with AI + Smart Rule + Default + Image + Icon + Background Color + Icon Symbol + Shape Style + Shape parameters + Corner Radius + Smoothness + Sides + Curve + Rotation + Scale + Top Played + Your most played tracks. + Recently Played + Songs you listened to most recently. + Forgotten Favorites + Favorite tracks you haven\'t played in a while. + New Gems + Recently added tracks with low play counts. + + + Quick Fill Genre + Sort & Play + Shuffle + Sort By + Artist + Album + Title + Generic artist + %1$s Shuffle + + + Select Songs + Choose Genre + Search songs + New Genre + Add Custom + Add Custom Genre + Genre Name + Select Icon + Genre: %1$s + Select a genre + Quick Fill + + + DJ Space + Loading… + Deck %1$d + Load song + No song loaded + + Stem separation is not available yet. + Volume + Speed + Crossfader + Deck 1 + Deck 2 + Select a song + Play/Pause + Song cover + x%1$.2f + diff --git a/app/src/main/res/values-vi/strings_settings.xml b/app/src/main/res/values-vi/strings_settings.xml new file mode 100644 index 0000000000..b7f5cc4f53 --- /dev/null +++ b/app/src/main/res/values-vi/strings_settings.xml @@ -0,0 +1,654 @@ + + + + Music Management + Manage folders, refresh library, parsing options + Appearance + Themes, layout, and visual styles + Lyrics + Source priority, immersive lyrics, and import management + Playback + Audio behavior, crossfade, and background play + Behavior + Gestures, haptics, and navigation behavior + AI Integration (β) + AI providers, API keys, and model settings + Backup & Restore + Export and recover your personal app data + Developer Options + Experimental features and debugging + Equalizer + Adjust audio frequencies and presets + Device Capabilities + Audio specs, codecs, and decoder info + Accounts + Manage Telegram, Google Drive, NetEase, and more services + About + App info, version, and credits + + + On + Off + Enabled + Disabled + Open + Select all + Clear selection + Close notice + + + Library Structure + Excluded Directories + Folders here will be skipped when scanning your library. + Artists + Multi-artist parsing and organization options. + Filtering + Minimum Song Duration + Minimum Tracks per Album + Album Art Cache Limit + Sync and Scanning + Refresh Library + Scan entire library for new and modified files. + Full Rescan + Running full rescan + Full rescan started… + Library sync finished + Rebuild Database + Rebuild database? + This will completely rebuild your music library from scratch. All imported lyrics, favorites, and custom metadata will be lost. This action cannot be undone. + Rebuild + Rebuilding database + Rebuilding database… + Auto-scan .lrc files + Automatically scan and assign .lrc files in the same folder during library sync. + Lyrics Management + Lyrics Source Priority + Choose which source to try first when fetching lyrics. + Embedded First + Online First + Local (.lrc) First + Reset Imported Lyrics + Remove all imported lyrics from the database. + Reset imported lyrics? + This action cannot be undone. + + + Refresh + Everything is allowed by default. Tap a folder to mark it as excluded from scans. + No subfolders here + Navigate up + Go to root + + + Rescan Required + Artist settings have changed. Rescan your library to apply. + Rescan + Scanning… + Multi-Artist Parsing + Character Delimiters + Current: %1$s + Word Delimiters + None + Current: %1$s + Configure + Extract Artists from Title + Detect feat., ft., with in song titles + Library Organization + Group by Album Artist + Show collaboration albums under main artist + About Multi-Artist Parsing + + PixelPlayer splits artist tags using character delimiters (/, ;, &) and word delimiters (feat., ft., vs., x). Word delimiters are matched case-insensitively. + \"Extract Artists from Title\" detects patterns like (feat. Artist) in song titles. + Backslash (\) can be used to escape character delimiters. + + Examples + \u2192 + \u266A + \"Artist1/Artist2\" + Artist1, Artist2 + \"Drake feat. Rihanna\" + Drake, Rihanna + \"Marshmello x Bastille\" + Marshmello, Bastille + \"Song (ft. B)\" by A + A, B + \"AC\\DC\" + AC/DC (escaped) + + + Delimiters + Current Delimiters + Tap a delimiter to remove it. At least one delimiter is required. + Add New Delimiter + e.g., / or ; + Add delimiter + Default Delimiters + Reset Delimiters? + This will clear all your custom delimiters and restore the defaults. This action cannot be undone. + Delimiters reset to defaults + At least one delimiter is required + Delimiter added + Delimiter already exists or is invalid + Space + + + Word Delimiters + Current Word Delimiters + These keywords split artist names when surrounded by spaces. Matched case-insensitively. Tap to remove. + No word delimiters configured + Add New Word Delimiter + e.g., feat. or ft. + Add word delimiter + How Word Delimiters Work + Word delimiters are matched case-insensitively with spaces around them.\n\nSingle-character delimiters (like \"x\") require spaces on both sides to avoid false matches.\n\nExamples:\n \"Drake feat. Rihanna\" -> Drake, Rihanna\n \"Marshmello x Bastille\" -> Marshmello, Bastille\n \"A vs. B\" -> A, B + Reset Word Delimiters? + This will clear all your custom word delimiters and restore the default keywords. This action cannot be undone. + Word delimiter added + Already exists or is invalid + Word delimiters reset to defaults + + + Preparing sync + Reading MediaStore + Processing tracks + Saving to database + Scanning lyrics files + Cleaning album art cache + Syncing cloud sources + Completing sync + %1$s • %2$d%% (%3$d/%4$d) + %1$s… + + + Global Theme + App Language + Choose the language used across the app interface. + System default + English + Español + Deutsch + Français + Русский + 简体中文 + Bahasa Indonesia + Italiano + 日本語 + Korean + Norwegian (Bokmål) + 繁體中文 + Türkçe + Tiếng Việt + App Theme + Switch between light, dark, or follow system appearance. + Light Theme + Dark Theme + Follow System + Use Smooth Corners + Use complex shaped corners effectively improving aesthetics but may affect performance on low-end devices + Disable Blur Effects + Turn off blur effects across the app to save battery and resources. + Show Scrollbar + Display lateral scrollbar on music lists for quick scrolling + Now Playing + Player Theme + Choose the appearance for the floating player. + Album Art + System Dynamic + Show player file info + Show codec, bitrate, and sample rate in the player progress section. + Album Art Palette Style + Current: %1$s. Open live preview and choose style. + Carousel Style + Choose the appearance for the album carousel. + No Peek + One Peek + Home Collage + Collage Pattern + Choose the shape arrangement for the Your Mix collage. + Auto-Rotate Patterns + Cycle through collage patterns each time you visit Home. + Navigation Bar + NavBar Style + Choose the appearance for the navigation bar. + Default + Full Width + Compact mode + Show only icons and reduce the navbar height. + NavBar Corner Radius + Adjust the corner radius of the navigation bar. + Lyrics Screen + Immersive Lyrics + Auto-hide controls and enlarge text. + Auto-hide Delay + Time before controls hide. + 3s + 4s + 5s + 6s + App Navigation + Default Tab + Choose the Default launch tab. + Home + Search + Library + Library Navigation + Choose how to move between Library tabs. + Tab row (default) + Compact pill & grid + + + Colors + Palette style + Choose the album colors for the player UI. + Tonal Spot + Balanced and calm. + Vibrant + High saturation accents. + Expressive + Bold hue shifts and contrast. + Fruit Salad + Playful rotated accents. + Color accuracy + 0 keeps the current tuning. Higher values stay closer to the album art\'s dominant hue. + Current + More accurate + 0 • Current + %1$d • Subtle + %1$d • Balanced + %1$d • Precise + + + Adjust Corner Radius + Match the navbar shape\'s corners with your device\'s physical corners for a seamless look. + Corner Radius + %1$d dp + + + Background Playback + Keep playing after closing + If off, removing the app from recents will stop playback. + Battery Optimization + Disable battery optimization to prevent playback interruptions. + Battery optimization is already disabled + Could not open battery settings + Volume Normalization (ReplayGain) + Enable ReplayGain + Normalize volume levels using ReplayGain metadata from audio files. + Gain Mode + Track: normalize each song. Album: normalize per album. + Track + Album + Cast + Auto-play on cast connect/disconnect + Start playing immediately after switching cast connections. + Headphones + Resume when headphones reconnect + If playback paused because headphones were removed, resume automatically when they connect again. + Queue and Transitions + Crossfade + Enable smooth transition between songs. + Crossfade Duration + Hi-Fi Mode + Float 32-bit audio output. Disable if playback stutters on your device. + Not supported on this device (PCM_FLOAT AudioTrack unavailable). + Persistent Shuffle + Remember shuffle setting even after closing the app. + Show queue history + Show previously played songs in the queue. + + + Folders + Back gesture controls folders + In Folders tab, system back navigates folder stack before leaving Library. + Player Gestures + Tap background closes player + Tap the blurred background to close the player sheet. + Haptics + Haptic feedback + Enable vibration feedback across the app. + + + AI Provider + Provider + Choose your AI provider + Safe Token Mode + ON — Fast & cheap. Sends minimal data (~1K tokens) to AI. + OFF — Deep context. Sends full listening profile (~8K tokens) for richer results. + Credentials + %1$s API Key + Get from %1$s + Google AI Studio (aistudio.google.com) + DeepSeek Platform (api.deepseek.com) + Groq Console (console.groq.com) + Mistral AI Platform (console.mistral.ai) + NVIDIA Build (build.nvidia.com) + Moonshot AI Platform (platform.moonshot.cn) + Zhipu AI Open Platform (bigmodel.cn) + OpenAI Platform (platform.openai.com) + Model Selection + Loading available models… + Failed to load models + AI Model + Select a model. + Enter API Key + Prompt Behavior + System Prompt + Customize how the AI behaves. + Preset Prompts + Enter system prompt… + Professional Curator + You are \'Vibe-Engine\', a world-class music curator and master of sonic flow. Your goal is to construct seamless, high-fidelity listening experiences. Prioritize harmonic compatibility, logical BPM transitions, and a sophisticated balance between familiar favorites and tasteful, logic-driven discovery. + Creative Maverick + You are an avant-garde music explorer specializing in \'unexpected cohesion\'. Your mission is to break conventional genre boundaries by identifying non-obvious sonic parallels. Prioritize rare deep cuts, experimental textures, and artistic novelty while maintaining a surprising but undeniable transition logic. + Strict Librarian + You are a surgical music database architect. Your logic is driven by absolute metadata precision and rigid categorical adherence. Minimize algorithmic discovery in favor of strict genre consistency, energy-level matching, and maximizing the retrieval of highly specific user-defined preferences. + Atmospheric Guide + You are a master of ambient textures and low-energy flow. Focus exclusively on tracks that facilitate a \'deep focus\' or \'tranquility\' state. Prioritize acoustic warmth, minimalist arrangements, and gentle transitions, while strictly avoiding high-transient sounds or sudden shifts in dynamic range. + Sonic Enthusiast + You are an audiophile analyst focused on production complexity and instrumentation. Prioritize tracks characterized by high dynamic range, intricate polyrhythms, and superior soundstage quality. Favor active-listening pieces that reward the listener for paying attention to technical fidelity and arrangement detail. + Energy Catalyst + You are a high-momentum rhythm generator. Your philosophy centers on driving basslines, percussive intensity, and infectious grooves. Prioritize high-BPM club compatibility, syncopated energy, and continuous rhythmic tension to keep the listener\'s heart rate and motivation peak-level. + AI Usage Report + Total Consumption + %1$s tokens tracking\nPrompt: %2$s | Output: %3$s | Thought: %4$s + Clear Logs + AI Activity Log (%1$d) + %1$s · %2$s + Show + Hide + + + How backup works + Choose sections, export a .pxpl file, and import it later. Restore only replaces the sections you select. + Create Backup + Export Backup + No sections selected. + All sections selected. + Selected %1$d of %2$d sections. + %1$s Creates a .pxpl backup file. + Select & Export + Restore Backup + Import Backup + Select & Restore + Browse or pick from recent backups. Selected data will replace current data. + + + Choose exactly what you want to include in the backup package. + Select a .pxpl backup file to inspect. You\'ll choose which sections to restore in the next step. + %1$d of %2$d sections selected + %1$d of %2$d modules selected + Recent Backups + No recent backups + Previously imported backups will appear here. + %1$d entries · Will replace current data + Export .pxpl + Restore Selected + Transfer in progress… + PixelPlayer_Backup_%1$d.pxpl + Creating Backup + Restoring Backup + %1$d%% + %1$s • %2$s + Exporting + Importing + Restoring + Remove from history + Inspecting… + Browse for file + Step %1$d of %2$d + Restore Modules + Backup Details + Created + App Version + Schema + Device + Unknown + · %1$s + %1$d modules · v%2$s · schema v%3$d + \? + Select all + Clear selection + + + Invalid backup: %1$s + Preparing restore + Starting restore task. + Preparing backup + Starting backup task. + Backup restored successfully + Restore completed with some unresolved issues. + Restore could not be completed: %1$s + Restore failed: %1$s + Data exported successfully + Export failed: %1$s + Data restored successfully + Restore completed with unresolved issues. Failed: %1$s + v%1$d + %1$s %2$s + + + Experiments + Experimental + Player UI loading experiments and toggles. + Test Setup Flow + Launch the onboarding setup screen for testing. + Maintenance + Force Daily Mix Regeneration + Re-creates the daily mix playlist immediately. + Regenerate Daily Mix + Regenerate Daily Mix? + This will discard the current mix and generate a new one based on recent listening habits. + Daily Mix regeneration started + Force Stats Regeneration + Clears cache and recalculates playback statistics. + Regenerate + Working… + Regenerate Stats + Regenerate Stats? + This will clear the statistics cache and force a recalculation from the database history. + Stats regeneration started + Force Album Palette Regeneration + Rebuild all cached palette variants for every album art, or pick a single one to refresh. + Regenerate All + Regenerate all album palettes? + This will clear cached theme data and rebuild all palette styles for %1$d unique album arts. + Regenerating… + Regenerating album palettes… + Rebuilding cached palette variants for %1$d unique album arts. This can take a while on large libraries. + %1$d of %2$d completed + Regenerated %1$d album art palettes + Regenerated %1$d of %2$d album art palettes + Choose Song + Select a song to clear cached theme data and regenerate all palette styles from the album art. + Search by title, artist, or album + No songs match your search. + No songs with album art were found. + Regenerating palette… + Palette regenerated for %1$s + Could not regenerate palette for %1$s + Diagnostics + Trigger Test Crash + Simulate a crash to test the crash reporting system. + Test crash triggered from Developer Options - This is intentional for testing the crash reporting system + + + Experimental + PlayerUI loading tweaks + Animated Lyrics (High-end devices) + Uses spring animations and visual effects for lyrics. May cause frame drops on low-end devices. + Lyric Blur Effect + Applies a depth-of-field blur to inactive lyrics. + Blur Strength + Adjust the intensity of the blur effect. + %1$.1fx + Step 1 · Choose what to delay + Delay everything + Hold the full player content until the sheet background is fully expanded. + Album carousel + Delay album art and carousel until the sheet is expanded. + Song metadata + Delay title, artist, and lyrics/queue actions. + Progress bar + Delay the timeline and time labels until expansion completes. + Playback controls + Delay play/pause, seek, and favorite controls. + All delayed components are active. Disable \"Delay everything\" to customize each part. + Step 2 · Configure placeholder behavior + Use placeholders for delayed items + Keep layout stable by rendering lightweight placeholders while components wait for expansion. + Step 3 · Choose when placeholders switch to real content + Select one mode. Threshold mode uses sliders; Drag release mode waits until you release the sheet gesture. + Enable at least one delayed component to unlock trigger mode. + Threshold + Uses expansion percentage. + Drag release + Switches only after gesture release. + Expand threshold + How expanded the sheet must be before delayed components become visible. + Content appears at %1$d%% expansion + Also apply on player close + Use close threshold to switch back to placeholders while collapsing. + Close threshold + How much collapse is required before placeholders take over again. + Placeholders appear after %1$d%% collapse + Drag release mode bypasses thresholds and close behavior. The swap happens only when the sheet drag gesture ends. + Make placeholders transparent + Placeholders keep their layout space but become invisible. + Visual Quality + Album Art Resolution + Low (256px) - Better performance + Medium (512px) - Balanced + High (800px) - Best quality + Original - Maximum quality + + + Playback needs review + Ready for playback + -- + Formats + HW decoders + Local songs + Local Music Storage + Music size + %1$d local songs + Available + %1$s total + Music footprint + Device used + %1$d%% + <1% + %1$d cloud songs + %1$d files not readable + Playback Path + Yes + No + Sample Rate + %1$d Hz + %1$d frames per buffer + Hi-Fi PCM Float + 32-bit float output path + Low Latency Support + Pro Audio Support + Memory + available of %1$s + Offload-ready formats + No compressed format reported hardware offload support. + +%1$d more + Detected outputs + Built-in output + Bluetooth audio + USB audio + Wired audio + Digital output + Other output + No output routes were reported by Android. + ExoPlayer Engine + %1$s renderers + Format Compatibility + %1$d supported tracks + %1$d unknown format + No decoder reported + Hardware decoder + Software decoder + Offload + %1$d in library + Compatibility Findings + No major incompatibilities + Your indexed tracks match the decoders Android reports on this device. + %1$d tracks may not decode natively + Formats to review: %1$s. + %1$d local tracks may be resampled + The library reaches %1$d Hz, above the current output sample rate. + %1$d tracks have unknown metadata + A full library rescan can fill missing MIME, bitrate, and sample-rate data. + Device Info + Manufacturer + Model + Brand + Device + Android Version + SDK Version + Hardware + Performance report + Generate a shareable diagnostic report to help us classify playback or scan lag. It contains only device, library, and timing data — no file paths, titles, or artists. + Generate report + Regenerate + Copy + Share + Report copied to clipboard + PixelPlayer performance report + Advanced performance diagnostics + Off by default. Records a short lag timeline for beta troubleshooting. + Active until %1$s + Mark lag now + Lag moment marked + + + Connected Accounts + Manage linked providers and keep each integration under your control. + Linked Services + Active + Available + Soon + Connected + Coming soon + Open Service + Logging out… + No linked accounts yet + Connect a provider to manage it from this screen. + Connect %1$s + %1$s (Coming soon) + Google Drive is coming soon. + Unable to open this screen right now. + + + About + PixelPlayer + Open source music player built with its community. + Version v%1$s + Open source + Community-first + Material 3 expressive + No contributors found right now. Please try again later. + Maintainer + The person behind PixelPlayer. + Community spotlight + Recognition for collaborators with major impact. + Open source contributors + Live contributor list from GitHub. + %1$d contrib. + GitHub + Repository + Telegram + Support + Open GitHub repository + Join Telegram community + Open GitHub profile + Open Telegram + Avatar of %1$s + Icon of %1$s + Volume + Pause when volume reaches zero + Automatically pause playback when the volume is set to 0 + diff --git a/app/src/main/res/values-vi/strings_widget.xml b/app/src/main/res/values-vi/strings_widget.xml new file mode 100644 index 0000000000..5dd2eabafc --- /dev/null +++ b/app/src/main/res/values-vi/strings_widget.xml @@ -0,0 +1,17 @@ + + + Responsive widget that adapts to its size + Compact player bar + Full controls with shuffle and repeat + Minimalist square player + + Tap to open + Album art + Album art placeholder + + Tap to play + Song title + Artist + + Progress bar, %1$d percent + \ No newline at end of file diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml new file mode 100644 index 0000000000..7f38b9115f --- /dev/null +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -0,0 +1,128 @@ + + + PixelPlayer + 音乐播放器 + 應用名稱更改 + 由於商標相關問題,我們將應用名稱從 PixelPlay 更改為 PixelPlayer。請繼續享受音樂! + 不再顯示 + + + 首页 + 搜索 + 音乐库 + + + 需要特殊权限 + 为了编辑歌曲信息(.mp3 文件),PixelPlayer 需要访问所有文件的特殊权限。这将允许我们直接修改曲目标签。请在下一界面授予此权限以开启信息编辑功能。 + 授予权限 + + + 快速播放 + 无法打开该音频文件。 + 打开完整播放器 + + + 随机播放 + 随机播放所有歌曲 + 全部随机播放 + 最近播放列表 + 没有可打开的播放列表 + + + 打开 Play 商店 + 继续使用 Beta 版 + Play 商店链接将通过 GitHub 配置启用。 + PixelPlayer 现已上线 Google Play + 请在 Google Play 上使用稳定版以获取正式更新,同时我们仍会保持 Beta 测试版本的更新。 + PixelPlayer + 发布公告 + 敬请期待 + + + 感谢您使用 PixelPlayer! + 最高分 %1$d + 关闭 + 分数 + 第 %1$d 关 + 生命 + 关卡完成! + 游戏结束 + 分数:%1$d + 再试一次? + 下一关 + 重新开始游戏 + 点击重新启动 + 随机播放音乐 + 打砖块 + 最高分 %1$d + 开始 + 拖动以移动挡板 + + + 关闭播放器 + 正在处理播放操作… + 播放错误:%1$s + + + 返回 + 确定 + 取消 + 关闭 + 错误 + 搜索 + 清除搜索 + 全部 + 确认 + 已保存! + 已选择 + %1$d%% + 艺术家 + 全选 + 清除 + 未知错误 + + + 保存 + 完成 + 重置 + 应用 + 随机播放 + 复制 + 分享 + 撤销 + 导入 + 删除 + 导出 + 合并 + 重命名 + 创建 + 歌词 + 设置 + 专辑封面 + 播放列表 + 未知曲目 + 未知艺术家 + 未知专辑 + 关闭 + 添加 + 移除 + 播放 + 上一首 + 下一首 + 收藏 + 暂停 + 循环播放 + 选项 + 随机播放 + %1$s 的更多选项 + 展开菜单 + 下一步 + 结束 + 恢复默认设置 + 导出全部 + 合并全部 + 分享全部 + 播放专辑 + 随机播放专辑 + %1$s 的专辑封面 + \ No newline at end of file diff --git a/app/src/main/res/values-zh-rTW/strings_changelogs.xml b/app/src/main/res/values-zh-rTW/strings_changelogs.xml new file mode 100644 index 0000000000..b0b82e4a30 --- /dev/null +++ b/app/src/main/res/values-zh-rTW/strings_changelogs.xml @@ -0,0 +1,132 @@ + + + 更新日志 + 在 GitHub 上查看 + 改进 + 修复 + 新功能 + 新增 + + 支持 Chromecast 从您的设备投射音频。 + 应用内更新日志,让您随时了解最新功能。 + 支持嵌入式和外部的 .LRC 歌词文件。 + 支持离线歌词。 + 同步歌词(与歌曲同步)。 + 查看完整队列的新界面。 + 支持在队列中重新排序和移除歌曲。 + 迷你播放器手势(向下滑动关闭)。 + 添加了更多 Material 动画。 + 新增设置项以自定义外观。 + 新增设置项以清除缓存。 + + + 用户界面的全面重新设计。 + 播放器的全面重新设计。 + 媒体库性能改进。 + 提升了应用的启动速度。 + AI 现在能提供更好的结果。 + + + 修复了标签编辑器中的各种错误。 + 修复了播放通知未被清除的错误。 + 修复了导致应用崩溃的几个错误。 + + + 引入了更丰富的听歌统计中心,可深入了解您的播放会话。 + 推出了悬浮快速播放器,可立即打开并预览本地文件。 + 添加了包含树状导航器 and “播放列表就绪”视图的文件夹标签页。 + + + 改进了整体 Material 3 UI,带来更简洁、更连贯的体验。 + 元数据编辑现已支持更改封面。 + 优化了应用内的动画和过渡效果,使导航更加流畅。 + 增强了艺术家界面的布局,细节更丰富且更精美。 + 升级了 DailyMix 和 YourMix 的生成,选择更加智能且多样化。 + 强化了 AI 播放列表的生成能力。 + 改进了搜索相关性和展示方式,以便更快地发现内容。 + 扩大了对更多音频文件格式的支持。 + + + 解决了元数据异常问题,确保各处的歌曲详情保持准确。 + 恢复了通知快捷操作,以便可靠地跳转回播放界面。 + + + 导航栏重大重新设计 + 用于选择源目录的全新文件管理器 + 全新的连接与投屏功能 + 远程设备之间的无缝流转 + 歌曲间无缝过渡 + 淡入淡出控制 + 全新的自定义过渡功能(仅限播放列表) + 关闭应用后继续播放 + UI 优化 + 统计功能改进 + 重新设计并增加了更多功能的队列控制 + 改进了对不同文件格式的播放和元数据编辑支持 + 改进的权限控制器 + 修复了一些小错误 + + + Material 3 Expressive UI 全面升级 + 10 段均衡器与音效 + 全新的媒体库同步流程 + AI 集成(Gemini 模型) + M3U 播放列表导入/导出 + 集成 Deezer 艺术家封面 + 自定义播放列表封面 + 设置架构重构 + 队列与播放器动画 + 基线配置文件 (Baseline Profiles) 与性能优化 + 具有同步偏移调整功能的更佳歌词系统 + + + 投屏稳定性改进 + 播放面板稳定性 + 常规错误修复与代码清理 + + + 现已支持 Android Auto 车载播放。 + 已支持 Wear OS,包括更完善的手表对手机播放控制。 + 扩展了云端集成,改进了对 Telegram、网易云音乐、QQ 音乐和 Google Drive 的支持。 + “最近播放”和持久化队列恢复功能,让您的听歌会话随时就绪。 + 现已包含备份与恢复 v3 以及账号管理工具。 + 歌词功能更加智能,增加了手动搜索备用方案和存储优化。 + + + 大幅优化了启动、媒体库、队列和播放器交互的性能。 + 重新设计了播放器、投屏、歌词、艺术家和流派界面,带来更流畅的体验。 + 导航和搜索流程更加可靠,路由处理更加安全。 + 提升了音频播放在更多设备 and 格式上的兼容性。 + 扩展了歌曲、专辑和播放列表的多选操作流程。 + + + 队列和随机播放行为现已更加稳定且符合预期。 + 修复了几个后台播放和投屏的边缘情况。 + 修复了睡眠定时器、文件标签页导航以及导致专辑艺术家崩溃的问题。 + 改善了小组件加载和服务稳定性,减少了设备发热/内存问题。 + 全局的常规错误修复和界面细节打磨。 + + + Wear OS:支持音乐传输、本地播放、队列同步以及从手表进行远程控制。 + AI:集成了 Groq AI 和 OpenRouter(实验性),并进行了 Token 优化。 + 云端:新增了 Jellyfin 支持。 + 歌词:带有专属开关的同步翻译,支持酷狗 LRC 格式,自定义文本对齐方式,以及改进了远程加载。 + UI/UX:紧凑的导航栏模式,从专辑封面提取调色板的动态主题,长标题跑马灯效果,以及全新的排序选项。 + Telegram:原生话题 (Topics) 支持和增强的显示模式。 + + + 音频引擎:全面重构,支持更多格式(MIDI、ALAC、M4A)并进行了解码器优化。 + 效率:大幅降低能耗,修复设备发热问题,以及后台任务优化 (SyncWorker)。 + 数据库:大规模的查询优化,并重新设计了封面缓存以防止数据丢失。 + 启动:通过基线配置文件 (Baseline Profile) 优化了加载时间。 + + + 播放:修复了 Opus/MP3 的卡顿、淡入淡出期间的 ReplayGain 错误,以及三星解码器上的启动问题。 + 稳定性:消除了启动、艺术家导航和 Android 12+ 设备上的崩溃问题。 + UI:修复了封面闪烁、非拉丁语系文本溢出,以及导航栏/迷你播放器的行为问题。 + 安全性:强化了凭证处理、存储权限以及与媒体服务器的通信安全性。 + + + 本地化:西班牙语、法语、俄语、简体中文、印度尼西亚语、意大利语 + + \ No newline at end of file diff --git a/app/src/main/res/values-zh-rTW/strings_cloud_services.xml b/app/src/main/res/values-zh-rTW/strings_cloud_services.xml new file mode 100644 index 0000000000..13dd260f70 --- /dev/null +++ b/app/src/main/res/values-zh-rTW/strings_cloud_services.xml @@ -0,0 +1,228 @@ + + + + 登录 Telegram + 您正在修改号码。再次发送验证码将替换上一个。 + 处理中… + 正在初始化 Telegram… + 正在登出… + 正在关闭会话… + 会话已关闭。请重新打开登录界面以继续。 + 正在准备安全的 Telegram 会话… + 正在等待 Telegram 响应… + 连接 Telegram + 连接 Telegram 即可流式播放您频道和聊天中的音乐。 + 手机号码 + 请输入您的 Telegram 手机号码。您稍后可以返回并进行修改。 + 手机号码 + 86 + 13800138000 + 发送验证码 + 验证码 + 请输入来自 Telegram 的验证码。如果号码有误,请返回并修改。 + 验证码 + 12345 + 修改手机号 + 重新发送验证码 + 验证验证码 + 两步验证密码 + 请输入您的 Telegram 密码。您仍然可以返回以修改手机号码。 + 密码 + 验证密码 + 请稍候… + + + Telegram 频道 + 添加频道 + 公开的 Telegram 频道 + 正在同步 + 立即同步 + 收起话题 + 显示话题 + 频道选项 + 话题 + 正在同步频道 + 正在从 Telegram 更新歌曲 + 从此频道获取最新歌曲 + 移除频道 + 停止同步并移除已缓存的歌曲 + 是否移除频道? + %1$s 将停止同步,并且从此频道缓存的所有歌曲都将被移除。 + 移除 + 未同步任何频道 + 添加公开的 Telegram 频道\n以同步您的音乐库 + 添加频道 + 从未同步 + 上次同步于 %1$s + + + 添加频道 + 搜索一个公开的 Telegram 频道以同步其音乐 + \@channelname 或链接 + 正在搜索… + 搜索频道 + 输入公开频道的用户名或链接\n以同步其音频文件 + + + %d 首歌曲 + %d 首歌曲 + + + %d 个话题 + %d 个话题 + + + + Subsonic + 管理 Navidrome、Airsonic 以及其他兼容 Subsonic 的服务器。 + + + 点击同步以获取您的 Jellyfin 播放列表 + 管理您的 Jellyfin 服务器连接。 + + + 音乐文件夹 + 点击 + 添加 Drive 文件夹 + 暂未添加文件夹 + 已同步 %1$d 个文件夹 + 添加文件夹 + + + 选择播放列表类型 + 选择要同步的播放列表: + 所有播放列表 + 自建与收藏 + 自建播放列表 + 收藏播放列表 + + + 已同步 %1$d 个播放列表 + 播放列表 + 同步 + 暂未同步任何播放列表 + 点击同步以获取您的播放列表 + 快捷操作 + 同步媒体库 + 断开连接 + %1$d 首歌曲 + + + 正在同步 + 正在同步媒体库… + 正在获取播放列表… + 正在同步播放列表:%1$s + 正在更新本地媒体库… + 同步完成 + 正在获取专辑列表… + 正在从 %1$s 获取歌曲… + 正在将 %1$d 首歌曲保存到数据库… + 未找到媒体库歌曲 + 媒体库同步完成 + 正在同步… + 错误:%1$s + + + 同步 + 全部同步 + 登出 + 同步所有播放列表 + 用户头像 + + + 无网络连接 + 此内容需要网络连接。请检查您的网络设置并重试。 + 您已离线 + 请检查您的网络连接并重试以访问此内容。 + + + 连接 + 正在连接… + 输入您的服务器地址和账户凭据。 + 连接详情 + 隐藏密码 + 密码 + 输入密码 + 预填 http:// + 服务器 URL + 显示密码 + Telegram + 用户名 + admin + 欢迎您,%1$s! + + + 兼容 Navidrome、Gonic、Airsonic 以及其他兼容 Subsonic 的服务器 + 支持 Navidrome、Airsonic、Gonic、Ampache 以及其他兼容 Subsonic API 的服务器。 + 如果您的服务器支持,应用密码同样有效。 + 预填 https:// + 连接到您的自托管音乐服务器 + Navidrome + 请使用您服务器的完整 https:// 基础地址。 + https://music.example.com + 这是您的 Subsonic 或 Navidrome 账户名称。 + Subsonic / Navidrome + Subsonic + + + 输入您的 Jellyfin 服务器 URL 和账户凭据。 + 连接到 Jellyfin 服务器以串流播放您的媒体库 + 连接到 Jellyfin 服务器。支持使用 HTTP 和 HTTPS 进行本地网络访问。 + Jellyfin + 您的 Jellyfin 账户密码。 + 连接到您的 Jellyfin 媒体服务器 + Jellyfin + 您的 Jellyfin 服务器完整 URL,包含端口。 + http://192.168.1.100:8096 + 您的 Jellyfin 账户用户名。 + + + 直接从您的 Google Drive 串流音乐文件 + 连接 Google Drive + Google Drive 已连接! + 创建 \"PixelPlayer Music\" + 在这里为您的音乐创建一个新文件夹 + 这里没有文件夹 + 打开文件夹 + 选择或创建一个文件夹作为您的音乐源 + 选择一个音乐文件夹 + 正在设置 Google Drive… + 使用 Google 账号登录 + Google Drive + 使用 + + + 无法读取会话 Cookie。 + 完成 + 退出 + 未找到 Cookie。请先登录。 + 页面加载时间过长。请刷新或尝试其他网络。 + + + 正在保存… + 停留 + 页面加载超时。您可以重试,进度不会丢失。 + 网页后退 + 您可以稍后返回。关闭时将丢弃当前页面状态。 + 网页前进 + 刷新 + 重试 + 打开主页 + WebView 加载失败。 + + + 读取网易云音乐 Cookie 失败:%1$s + 退出网易云音乐登录? + 加载网易云音乐时出现 HTTP %1$d。 + 尚未检测到登录。请在点击完成前完成网易云音乐的登录。 + 登录网易云音乐 + 安全提示:您的密码仅在网易云音乐网页中输入。PixelPlayer 仅存储会话 Cookie (MUSIC_U) 以同步您的媒体库。 + 网易云音乐 + + + 读取 QQ 音乐 Cookie 失败:%1$s + 退出 QQ 音乐登录? + 加载 QQ 音乐时出现 HTTP %1$d。 + 尚未检测到登录。请在点击完成前完成 QQ 音乐的登录。 + 登录 QQ 音乐 + 安全提示:您的密码仅在 QQ 音乐网页中输入。PixelPlayer 仅存储会话 Cookie 以同步您的媒体库。 + QQ 音乐 + \ No newline at end of file diff --git a/app/src/main/res/values-zh-rTW/strings_equalizer.xml b/app/src/main/res/values-zh-rTW/strings_equalizer.xml new file mode 100644 index 0000000000..f1e662c938 --- /dev/null +++ b/app/src/main/res/values-zh-rTW/strings_equalizer.xml @@ -0,0 +1,57 @@ + + + + 名称不能为空 + 重命名 + + + 更改视图模式 + 禁用均衡器 + 启用均衡器 + 编辑 + 编辑预设 + 自定义预设 + 预设 + 更新 + 低音增强 + 虚拟音效 + 响度 + 不支持 + 此设备不支持 + 音量 + 频率响应 + Hz + 低音 + 中低音 + 中高音 + 高音 + 低音 / 低频 + 中音 / 高频 + 第 %1$d 页 + 重置持续时间 + 保存为新预设 + + + 已保存的预设 + 暂未保存自定义预设。 + 取消固定 + 固定 + 重命名 + 删除 + + + 保存自定义预设 + 为您的自定义均衡器预设输入一个名称。 + 预设名称 + 重命名预设 + + + 管理预设 + 拖动以重新排序 • 点击眼睛图标显示或隐藏 + 重新排序 + 重置预设 + 这将恢复默认的预设顺序和可见性。是否继续? + 恢复默认 + 可见 + 隐藏 + diff --git a/app/src/main/res/values-zh-rTW/strings_home_screen.xml b/app/src/main/res/values-zh-rTW/strings_home_screen.xml new file mode 100644 index 0000000000..46a70f8063 --- /dev/null +++ b/app/src/main/res/values-zh-rTW/strings_home_screen.xml @@ -0,0 +1,277 @@ + + + + β + Beta + 云端串流 + 更新日志 + 云端串流 + 从您的云端账户串流音乐 + + + Beta 0.7.5 + β + 欢迎使用 PixelPlayer 0.7.5-beta + 您正在使用的是可能包含错误、崩溃或实验性功能的测试版本。请报告问题以帮助我们改进。 + 预期情况 + 可能会意外出现错误、崩溃或未完成的功能。 + 某些功能可能会在不另行通知的情况下更改或移除。 + 测试版可能比正式发布版本更不稳定。 + 在报告已知问题之前,请务必先检查更新。 + 测试版在测试期间可能会更改、损坏或改进的内容。 + GitHub Issue 快捷入口 + 请先搜索,然后针对错误、崩溃、请求或问题开启一个明确的报告。 + 浏览现有的 Issue + 报告问题或崩溃 + 分享复现步骤、预期结果、实际结果以及您的设备/操作系统详细信息。 + 如何报告 + 开启新 Issue 前的快速检查清单。 + 开启 Issue 前 + 搜索现有已开启和已关闭的 Issue,以避免重复提交。 + 更新至 PixelPlayer 最新版本并确认问题是否仍然存在。 + 重启应用并确认问题是否仍然存在。 + 尝试复现它并写下确切的步骤。 + 哪种问题类型? + 错误报告:某些功能表现异常。 + 功能请求:添加新功能或改进。 + 提问:如果已启用,请使用 Discussions(讨论),或开启一个带有问题标签的 Issue。 + 错误报告 + 当某些功能表现异常或崩溃时,请复制这些字段。 + 错误报告 + 简要概述: + 预期行为: + 当前行为: + 播放/复现步骤:1. 2. 3. + 发生的频率如何?总是 / 有时 / 很少。 + 截图/视频:如果有。 + 日志/堆栈跟踪:如果有。 + 环境 + PixelPlayer 版本: + 安装来源:GitHub Release、debug 构建、Nightly 构建等。 + Android 版本: + 设备型号: + 额外信息:SD 卡使用情况、特殊设置、权限等。 + 功能请求 + 当您希望添加新功能或改进时,请复制这些字段。 + 问题描述:您想解决什么问题? + 建议方案:它应该如何工作? + 考虑过的替代方案:是否有其他方法? + 影响范围:哪些界面或流程会受影响? + 样图或参考图片(如果有)。 + 标题、隐私和范围 + 使报告易于分类且安全共享。 + 好的 Issue 标题示例 + 均衡器:切换预设选项卡时指示器偏移 + 搜索:查询为空时历史记录列表不显示 + 功能:添加“最近添加”播放列表排序选项 + 请避免 + 诸如“它不起作用”之类的通用报告。 + 在一个 Issue 中包含多个不相关的问题。 + 包含私人数据的未经打码处理的日志或截图。 + 隐私声明 + 在发布日志、截图或视频之前,请删除个人或私人信息。 + Nightly 构建版本 + Nightly 版本与正式版有何不同,以及出错时应提供哪些信息。 + Nightly 版本是从最新提交 (commit) 生成的,可能包含未完成的更改、临时错误或回归。它们比正式发布版本更具实验性。 + 如果可用,可从仓库的 GitHub Actions 工作流产物 (artifacts) 中获取。 + 报告 Nightly 问题 + 在报告 Nightly 版本的问题时,请务必说明该问题发生于 Nightly 版本,而非正式发布版本。请尽可能提供构建日期、工作流运行名称或编号,或者提交 SHA。并请检查最新的正式版本中是否会出现相同问题。 + Beta 0.5.0 升级 + 建议全新安装 + 如果您从 Beta 0.5.0 升级,此更新可能需要全新的媒体库数据,而不是旧的缓存状态。 + 如果元数据或媒体库条目看起来有误 + 歌曲元数据错误、艺术家或专辑不匹配,或出现类似重复的条目,通常意味着只需全新安装即可解决。 + 不再显示 + 知道了 + + + 糟糕!出错了 + 应用在您上次使用时崩溃了。请分享崩溃报告以帮助我们修复此问题。 + 日期:%1$s + 错误: + 堆栈跟踪(预览): + 崩溃日志 + 崩溃日志已复制到剪贴板 + PixelPlayer 崩溃报告 + 分享崩溃报告 + + + DJ 混音 + + + 你的\n推荐 + 暂无数据 + 当 PixelPlayer 发现歌曲或同步数据源时,您的推荐将显示在此处。 + 刷新 + + + DAILY MIX + 基于听歌历史 + 查看全部 Daily Mix + Daily Mix + + + Daily Mix + + %1$d 首歌曲 • %2$s + %1$d 首歌曲 • %2$s + + 播放 + AI 播放列表生成器 + + + 您的 Daily Mix 是如何生成的 + 您的 Daily Mix 由您最喜爱和播放次数最多的歌曲组成。我们还会添加您喜欢的艺术家和流派的曲目,让您发现新音乐。 + 告诉 AI 您今天想听什么 + 我们使用小样本以保持较低的成本 + 正在更新… + 更新 Daily Mix + + + 完美精选 + Daily Mix + 您的声音之旅已就绪 + AI 播放列表生成器 + 描述您的氛围、心情或活动,让 AI 从您的媒体库中精心挑选完美的播放列表。 + 播放列表大小 + 最少歌曲数 + 最多歌曲数 + 例如:轻松的夜晚氛围,欢快的健身音乐… + 点击重试 + 声音之旅合成完毕! + 准备播放 + 正在生成… + 生成播放列表 + + + 最近播放 + + + 最近播放 + 播放最新 + 在 %1$s 内无最近播放记录 + 更改时间范围或播放更多歌曲以填充此时间轴。 + 最近播放 + 今天 + 昨天 + + + 听歌统计 + 总播放量 + 日均播放 + 最爱曲目 + %1$s • %2$d 次播放 + + + 听歌统计 + 刷新听歌统计 + 今天 + 本周至今 + 本月至今 + 本年至今 + 全部时间 + 听歌时长 + 播放次数 + 听歌时间轴 + 听歌时长 + 所选时间范围内的总听歌时长。 + 播放次数 + 您在每个时间段内完成的会话次数。 + 平均会话 + 每个时间段的平均听歌时长。 + 划分为 4 小时的时间段,揭示您的日常节奏。 + 每日柱状图,方便比较每周习惯。 + 每周柱状图,显示本月趋势。 + 每月柱状图,显示全年的季节性变化。 + 每年柱状图,总结您的完整听歌历史。 + 暂无听歌数据 + 点击播放开始建立您的听歌时间轴 + 每日节奏 + 每周节奏 + 每月节奏 + 全年概览 + 历年进展 + 按 4 小时分段分组 + 按星期几分组 + 按月内周次分组 + 按月分组 + 按年份分组 + 高峰时段 + %1$d 次播放 + + 热门类别 + 比较您在各个流派、艺术家、专辑和歌曲中的听歌偏好。 + 流派 + 艺术家 + 专辑 + 歌曲 + 按流派统计 + 按艺术家统计 + 按专辑统计 + 按歌曲统计 + %1$d 次播放 • %2$d 位艺术家 + %1$d 次播放 • %2$d 首曲目 + 暂无类别数据 + 点击播放,您的听歌亮点将在这里浮现 + 听歌习惯 + 暂无习惯数据 + 当我们更了解您时,就会在这里展示您的听歌习惯。 + 总会话数 + 平均会话时长 + 最长会话 + 会话数/天 + 最活跃的一天 + 暂无播放 + 时间轴最高峰 + 热门艺术家 + 暂无热门艺术家 + 继续听歌,您最喜爱的艺术家将出现在这里。 + \? + %1$d. %2$s + 热门专辑 + 暂无热门专辑 + 您经常回顾的专辑将出现在这里。 + %1$d. %2$s + 曲目集中度 + 您的听歌时间在最爱曲目中的分布情况。 + 暂无集中度数据 + 播放更多曲目即可了解您听歌的专一程度。 + Top 1 + Top 2-3 + 其他 + %1$d%% + 听歌集中度 + 排名前 3 的曲目占据了您 %1$d%% 的听歌时间。 + 平均播放/曲目 + 去重曲目数 + Top 3 占比 + 此范围内的曲目 + 选定时间范围内播放次数最多的曲目。 + 暂无热门曲目 + 听您喜欢的歌,它们将会在这里高亮显示。 + 收起曲目 + 显示所有曲目 + + + %1$d 小时 %2$02d 分钟 + %1$d 分钟 + %1$d 小时 %2$02d 分 + %1$d 小时 + %1$d 分 + %1$d 秒 + %1$d小时%2$02d分 + %1$d小时 + %1$d分 + %1$d秒 + 从未 + 刚刚 + 1 天前 + %1$d 天前 + 1 小时前 + %1$d 小时前 + 1 分钟前 + %1$d 分钟前 + %1$d 首歌曲 + %1$d 首歌曲 + 第 %1$d 周 + diff --git a/app/src/main/res/values-zh-rTW/strings_library.xml b/app/src/main/res/values-zh-rTW/strings_library.xml new file mode 100644 index 0000000000..fbe7fe995e --- /dev/null +++ b/app/src/main/res/values-zh-rTW/strings_library.xml @@ -0,0 +1,566 @@ + + + + 音乐库 + 媒体库选项卡 + 直接跳转到任意选项卡或对它们进行重新排序。 + 重新排序选项卡 + + + 歌曲 + 专辑 + 艺术家 + 播放列表 + 文件夹 + 已收藏 + + + 播放列表创建成功 + 请先设置您的 AI 提供商 API 密钥 + 请先设置您的 Gemini API 密钥 + 已添加到队列 + 下一首播放 + + + 手表传输 + 设置 + 编辑 + 重新排序选项卡 + 展开菜单 + + + 您最多可以选择 %1$d 张专辑 + 文件夹 + 文件夹 + + + 排序方式 + 视图 + 播放列表视图 + 网格 + 列表 + 内部存储 + SD 卡 + SD 卡目前不可用。 + 云端 + Telegram 云端频道 + 话题显示 (Topics) + 频道 + 话题 + 全部 + 云端 + 仅云端 + + + 正在使用 AI 生成元数据… + + + 加载歌曲时出错 + 加载专辑时出错 + 加载艺术家时出错 + 重试 + + + 在您的媒体库中未找到歌曲。 + 如果您设备上有音乐,请尝试在设置中重新扫描媒体库。 + 未找到歌曲 + + + 新建 + 创建新播放列表 + 导入 M3U 播放列表 + 定位当前歌曲 + 所有歌曲 + 云端 + 本地 + 排序选项 + + + 全选 + 取消选择 + 更多选项 + + + 正在扫描音乐文件… + 正在处理文件… + 第 %1$d 个,共 %2$d 个文件 + 正在同步媒体库… + 同步完成 + 等待中… + 正在同步媒体库… + 正在清理专辑封面缓存… + 正在同步云端数据源… + 正在扫描歌词… + + + 暂无歌曲 + 将音乐添加到您的设备或同步云端数据源以开始收听。 + 未找到本地歌曲 + 尝试其他数据源过滤或重新扫描您的设备媒体库。 + 未找到云端歌曲 + 同步 Telegram 或网易云音乐歌曲,或切换到本地源。 + 暂无专辑 + 一旦您的媒体库对曲目进行分组,专辑就会显示在这里。 + 未找到本地专辑 + 需要本地歌曲才能构建本地专辑组。 + 未找到云端专辑 + 同步后,带有专辑数据的云端歌曲将显示在这里。 + 暂无艺术家 + 从任何来源为歌曲建立索引后,将显示艺术家。 + 未找到本地艺术家 + 目前没有本地歌曲的艺术家元数据可用。 + 未找到云端艺术家 + 同步远程歌曲时会显示云端艺术家条目。 + 暂无收藏歌曲 + 在播放歌曲时点击心形图标将其保存在这里。 + 无本地收藏歌曲 + 切换数据源过滤或收藏您设备中的歌曲。 + 无云端收藏歌曲 + 收藏 Telegram 或网易云音乐的曲目,即可在此视图中看到它们。 + 未找到文件夹 + 包含音乐的内部存储文件夹将显示在这里。 + 暂无播放列表 + 创建您的第一个播放列表来整理您的媒体库。 + + + 编辑歌曲元数据 + 播放 + 播放歌曲 + 全部播放 + 全部播放 + 添加到收藏 + 全部添加到收藏 + 取消收藏 + 全部取消收藏 + 分享歌曲文件 + 分享歌曲文件 + 作为 ZIP 全部打包分享 + 无法分享歌曲:%1$s + 添加到队列 + 添加到队列 + 下一首播放 + 在队列中下一首播放 + 添加到播放列表 + 删除 + 全部删除 + 正在检查手表 + 正在传输 %1$d%% + 正在传至手表 + 传输进行中 + 发送至手表 + 手表不可用 + 将歌曲发送至手表 + 手表不可用 + 设为 + 设为声音 + 选择如何将此歌曲用作系统声音 + 将此歌曲用作 + 选择 PixelPlayer 应在哪里安装此声音。 + 手机铃声 + 来电 + 通知音 + 消息和应用提醒 + 闹钟音 + 时钟闹钟 + 确认更改声音 + 将“%1$s”设为您的 %2$s? + 设置声音 + 已将“%1$s”设为您的 %2$s + 铃声 + 通知音 + 闹钟音 + 启用“修改系统设置”,然后返回 PixelPlayer 自动完成。 + 未启用“修改系统设置”。 + 已将“%1$s”设为您的铃声 + 只有本地歌曲才能设为铃声。 + 无法将此音频文件准备为铃声。 + 无法设置铃声:%1$s + 选项 + 选项 + 信息 + 信息 + 时长 + 流派 + 专辑 + 艺术家 + 歌曲信息 + 提供商 + 文件 + %1$d 首歌曲 + 已选择 + %1$d 个播放列表 + %1$d 张专辑 + 已选择 + 限制:每次选择最多 %1$d 张专辑。 + 加入队列及播放将遵循您的选择顺序。 + %1$d 个流派 + 已选择 + 对这些流派中的所有歌曲进行批量操作。 + + + 默认顺序 + 标题 (A-Z) + 标题 (Z-A) + 艺术家 + 艺术家 (Z-A) + 专辑 + 专辑 (Z-A) + 添加时间 + 添加时间 (最早优先) + 时长 + 时长 (最短优先) + 发行年份 + 发行年份 (最早优先) + 歌曲最少 + 歌曲最多 + 名称 (A-Z) + 名称 (Z-A) + 歌曲数量 (最多) + 歌曲数量 (最少) + 创建时间 + 创建时间 (最早优先) + 收藏时间 + 收藏时间 (最早优先) + 子文件夹最少 + 子文件夹最多 + + + 标题 + 艺术家 + 专辑 + 添加时间 + 时长 + 发行年份 + 歌曲数量 + 名称 + 歌曲数量 + 创建时间 + 收藏时间 + 子文件夹数量 + + + 来源 + 顺序 + 降序 + 升序 + 原始顺序 + 点击切换为升序 + 点击切换为降序 + 此排序保持其原始顺序 + 开关已开启 + + + 重新排序媒体库选项卡 + 重置顺序 + 将选项卡顺序重置为默认? + 正在重新排序选项卡… + 拖动手柄 + + + 选择一位艺术家 + 1 位艺术家 + %1$d 位艺术家 + 主要艺术家 + 艺术家主页 + + + 取消传输 + %1$s / %2$s + 显示手机到手表音乐传输的实时进度 + 手表传输 + 正在发送至手表 + 已取消 + 传输已取消 + 传输完成 + 已完成 + 失败 + 传输失败 + 有多个活动中的传输 + %1$s • %2$s + 正在准备 + 正在准备手表传输 + 正在准备传输… + 正在将 %1$d 首歌曲发送至手表 + 正在发送至手表 + 正在开始传输… + 正在开始 + 正在传输 + %1$d 个传输任务 + + + 编辑歌曲 + 显示信息 + 正在编辑歌曲元数据 + 编辑歌曲元数据可能会影响其在媒体库中的显示和整理方式。更改是永久性的,且可能无法撤销。 + 知道了 + 信息 + 封面 + 选择一张正方形图片并进行微调,让您的封面在整个应用中看起来更棒。 + 更改封面 + 删除封面 + 标题 + 艺术家 + 专辑 + 专辑艺术家 + 流派 + 作曲 + 曲目编号 + 唱片编号 + 音量标准化-曲目增益 (dB) + 音量标准化-专辑增益 (dB) + -6.50 + -8.20 + 新封面预览 + 当前歌曲封面 + 调整您的封面 + 使用双指缩放和拖动手势找到完美的构图。 + 应用封面 + 无法加载选定的图片 + 在 lrclib.net 搜索歌词 + + + 批量编辑 %d 首歌曲 + 仅会更新已修改的字段。将字段留空则保持现有值。 + (混合值) + (可选 - 留空则跳过) + 成功更新 %d 首歌曲 + 已更新 %1$d 首(共 %2$d 首)歌曲。某些文件无法编辑。 + 更新歌曲失败 + 批量更换封面 + 这将替换所有 %d 首选定歌曲的封面 + 为所有选定歌曲设置封面 + 移除所有封面 + (多个不同的封面) + + + 已忽略播放列表 + + + 创建播放列表 + 选择创建方式。 + 手动 + 自己设计封面、图标、形状并挑选歌曲。 + 使用 AI + 使用高级控制生成精选的播放列表。 + 需要在设置中配置 Gemini API 密钥。 + 设置 API 密钥 + + + AI 播放列表实验室 + 重置 + 正在生成… + 生成 + 意图 + 播放列表名称(可选) + 这个播放列表应该带来什么感觉? + 示例:伴随温暖合成器音乐的日落驾驶 + 方向 + 情绪 + 活动 + 时代 + 精选引擎 + 能量 + 控制歌曲的强度和节奏。1 = 平静/缓慢,5 = 高能量/快节奏。 + 探索度 + 控制所选歌曲的熟悉程度。1 = 你最常听的最爱,5 = 很少播放的冷门曲目。 + 最少歌曲数 + 最多歌曲数 + 过滤 + 优先考虑的流派(可选) + 例如:合成器波 (synthwave)、独立流行 (indie pop) + 避免的流派(可选) + 例如:金属 (metal)、硬核陷阱 (hard trap) + 首选语言(可选) + 例如:英语,西班牙语,纯音乐 (instrumental) + 优先考虑已收藏的最爱 + 避免露骨歌词 (Explicit) + 提示词预览 + 当您添加偏好设置后,最终的提示词将显示在这里。 + 精准策展 + 定义情绪、活动、约束和深度。 + AI 将仅使用您本地媒体库中的歌曲。 + 至少为 AI 添加一项指令。 + 请设置有效的歌曲数量范围。 + %1$d/5 + 自定义… + 输入自定义值 + 输入您的自定义值 + + + 任何时代 + 核心请求:%1$s。 + 情绪目标:%1$s。 + 活动背景:%1$s。 + 时代焦点:%1$s。 + 优先考虑的流派:%1$s。 + 避免的流派:%1$s。 + 首选语言:%1$s。 + 能量等级目标:%1$d/5。 + 探索度目标:%1$d/5(1 代表熟悉,5 代表冷门曲目)。 + 可能的话,优先选择更接近听众最爱的歌曲。 + 只要有替代选择,尽量避免露骨歌词。 + 保持平滑的过渡,避免重复聚集同一艺术家。 + + 放松 + 活力 + 快乐 + 暗黑 + 浪漫 + 忧郁 + + + 健身 + 专注 + 公路旅行 + 派对 + 学习 + 深夜 + + + @string/playlist_creation_ai_era_any + 70年代 + 80年代 + 90年代 + 2000年代 + 2010年代 + 2020年代 + + + + 尚未创建播放列表。 + 点击“新建播放列表”按钮开始。 + 新建播放列表 + 播放列表名称 + 我的播放列表 + + + 添加 %1$d 首歌曲到… + 选择播放列表 + 搜索播放列表… + 已将歌曲添加到播放列表 + 播放列表已创建并已添加歌曲 + 内部存储 + + + 添加歌曲 + 添加所选歌曲 + 添加 + 搜索或过滤歌曲… + 已收藏 + 加载歌曲失败 + 加载更多 + + + 合并播放列表 + 为合并后的播放列表输入名称: + 合并的播放列表 + 这将把 %1$d 个选定的播放列表合并为一个。 + + + 未找到可播放的有效歌曲 + 在当前列表中未找到歌曲 + 无法定位歌曲 + 在媒体库中未找到歌曲 + 播放停止:%1$s 已结束(曲目结束)。 + 曲目 + 没有可供随机播放的歌曲。 + 选定的专辑 + 在选定的专辑中未找到可播放的歌曲 + 在选定的流派中未找到可播放的歌曲 + 仅前 %1$d 张专辑已加入队列 + 已将 %1$d 张专辑加入队列(共 %2$d 首歌曲) + 无法将选定的专辑加入队列 + 所有歌曲均已在收藏中 + 没有被收藏的歌曲 + 正在创建 ZIP 文件… + 分享失败:%1$s + + 添加了 %d 首歌曲到队列 + 添加了 %d 首歌曲到队列 + + + %d 首歌曲将作为下一首播放 + %d 首歌曲将作为下一首播放 + + + 添加了 %d 首歌曲到收藏 + 添加了 %d 首歌曲到收藏 + + + 从收藏中移除了 %d 首歌曲 + 从收藏中移除了 %d 首歌曲 + + + + 没有可分享的播放列表 + 分享播放列表 + 分享失败:%1$s + 没有可导出的播放列表 + 导出失败:%1$s + Music/PixelPlayer Exports + 请在设置中配置您的 Gemini API 密钥。 + 播放列表已恢复 + + 正在分享 %d 个播放列表 + 正在分享 %d 个播放列表 + + + 已导出 %1$d 个播放列表到 %2$s + 已导出 %1$d 个播放列表到 %2$s + + + + 无效的专辑 ID + 未找到专辑 ID + 加载专辑数据出错:%s + 未找到专辑 + + + 无效的艺术家 ID + 未找到艺术家 ID + 加载艺术家数据出错:%s + 找不到该艺术家 + + + 无法删除当前正在播放的歌曲 + 删除了 %1$d 个文件(跳过了 %2$d 个 - 正在播放) + 删除了 %1$d 个(共 %2$d 个)文件 + 删除文件失败 + 文件已删除 + 无法删除文件或找不到文件 + 删除已取消 + 删除歌曲? + “%1$s” - %2$s\n\n此歌曲将从您的设备中永久删除,且无法恢复。 + 这些歌曲将从您的设备中永久删除,且无法恢复。 + + 删除了 %d 个文件 + 删除了 %d 个文件 + + + 删除 %d 首歌曲? + 删除 %d 首歌曲? + + + + 元数据更新成功 + 正在更新 %1$d 首歌曲… + 已成功更新 %1$d 首歌曲! + 更新了 %1$d 首歌曲。失败:%2$d 首 + 歌词保存成功 + 保存歌词失败 + 没有可保存的歌词 + 权限被拒绝 – 无法编辑文件 + 权限被拒绝 – 无法保存歌词 + 权限被拒绝 – 无法编辑此文件 + + + 请在设置中为选定的 AI 提供商配置有效的 API 密钥。 + AI 错误:%s + 所选 AI 提供商拒绝了请求,原因是账户没有积分或可用配额不足。 + 所选 AI 模型不再可用。PixelPlayer 尝试自动切换到支持的模型。 + AI 无法根据您的提示找到任何歌曲。 + 写下一个 Daily Mix 的灵感 + 已使用 AI 更新 Daily Mix + 无法更新:%s + AI 无法为此合集找到歌曲 + \ No newline at end of file diff --git a/app/src/main/res/values-zh-rTW/strings_player.xml b/app/src/main/res/values-zh-rTW/strings_player.xml new file mode 100644 index 0000000000..73811e2fb6 --- /dev/null +++ b/app/src/main/res/values-zh-rTW/strings_player.xml @@ -0,0 +1,198 @@ + + + + 收起播放器 + 正在播放 + 云端串流 + 投屏 + 蓝牙 + 本地播放 + 正在连接… + 打开队列 + + + 准备连接 + 允许 PixelPlayer 查看您附近的设备和当前 Wi-Fi,以便我们保持您的投屏、蓝牙音频和扬声器同步。 + 附近设备 + 需要此权限来读取和控制您连接的蓝牙音频设备。 + 用于 Wi-Fi 的位置信息 + Android 需要位置信息才能共享您当前所连接的 Wi-Fi 网络 (SSID),以便我们找到兼容的投屏设备。 + 允许访问 + 我们仅将这些权限用于设备互连——投屏、控制附近扬声器以及保持音频同步。 + 连接设备 + 正在扫描附近 + 投屏会话 + 正在连接 + 已连接 + 本机 + 蓝牙音频 + 本地播放 + 正在播放 + 已暂停 + 设备音量 + 手机音量 + %1$d/%2$d + 电量 + 音量 + 断开连接 + 连接性 + 开启 Wi-Fi 或蓝牙 + 刷新连接 + Wi-Fi + + + 已连接 + 蓝牙 + + + 已连接 + 附近设备 + 刷新设备 + 已连接 + 正在连接 + 可连接 + 可用 + 正在连接... + 正在搜索设备… + 请确保您的电视或扬声器已开启,并连接到同一 Wi-Fi 网络。 + 控制 + 设备 + + + 投屏媒体服务器 + 正在投屏至设备 + 正在向投屏设备提供媒体 + %1$s: %2$s + 投屏时暂不支持对此音频格式进行进度调节,这可能会导致投屏会话崩溃。 + + + 睡眠定时器 + 定时器 + %1$d 分钟 + 定时器已设为 %1$d 分钟。 + 1 次 + + %d 次 + %d 次 + + 播放次数:%1$s + 当前曲目结束时 + 播放将在曲目结束时停止。 + 开启 + 自定义时间 + 取消定时器 + 曲目结束 + 定时器已取消。 + 无法启用“曲目结束时停止”:没有正在播放的歌曲。 + “曲目结束”定时器已停用:歌曲已从 %1$s 切换到 %2$s。 + 上一首曲目 + 当前曲目 + 设置自定义时长 + + + 接下来播放 + 队列当前为空。 + + 1 首曲目在列。 + %d 首曲目在列。 + + 队列 + 队列为空。 + 重新排序歌曲 + 切换随机播放 + 切换循环播放 + 睡眠定时器 + 更多操作 + 定位当前歌曲 + 清空队列 + 清空队列 + 您确定要清空队列中除当前正在播放之外的所有歌曲吗? + 保存为播放列表 + %1$s 队列 + 当前队列 + 移除歌曲 + 已移除 + 保存为播放列表 + 取消全选 + 播放列表名称 + 搜索要包含的歌曲… + 没有与 \"%1$s\" 匹配的歌曲 + + 已选择 %d 首歌曲 + 已选择 %d 首歌曲 + + 保存为:%1$s + 输入播放列表名称 + 从播放列表中移除 + %1$s 的更多选项 + + + 歌词 + 正在加载歌词… + 逐字同步 + 静态 + 歌词选项 + −.5 + −.1 + +.1 + +.5 + 0s + %1$+.1fs + + + 搜索歌词失败 + 无法从远程获取歌词 + 连接超时。请检查您的网络连接。 + 网络错误。请检查您的网络连接。 + 服务器错误(代码 %d)。请稍后再试。 + + + 歌词已可用,跳过在线获取。 + 已找到内嵌歌词,跳过在线获取。 + 已找到本地 (.lrc) 歌词,跳过在线获取。 + + + 保存歌词 + 使用 AI 翻译 + 此歌词已有翻译 + 此歌词已经是该语言 + 未配置 API + 歌词翻译成功! + 正在翻译歌词... + 重置已导入的歌词 + 重置歌词? + 您确定要重置此歌曲的歌词吗? + 外观 + 对齐方式 + 歌词左对齐 + 歌词居中 + 歌词右对齐 + 控制 + 调整同步 + 隐藏同步控制 + 显示罗马音 + 显示翻译 + 禁用沉浸模式(一次) + 保持屏幕常亮 + + + 保存歌词 + 选择要保存的版本: + 同步版(带时间戳) + 纯文本(仅文本) + + + 您想在网上搜索歌词吗? + 显示歌词选项 + 始终打开选择器,而不是自动应用第一个匹配项 + 正在搜索歌词… + 未找到歌词 + 我们无法自动找到歌词。您可以编辑标题或艺术家名称并尝试手动搜索。 + 歌曲标题 + 艺术家(可选) + 找到 %d 个匹配项 + 已同步 + %1$s • %2$s + 歌词提供者 + https://lrclib.net/ + diff --git a/app/src/main/res/values-zh-rTW/strings_screens.xml b/app/src/main/res/values-zh-rTW/strings_screens.xml new file mode 100644 index 0000000000..a0ae15a250 --- /dev/null +++ b/app/src/main/res/values-zh-rTW/strings_screens.xml @@ -0,0 +1,244 @@ + + + + 错误:流派 ID 缺失 + + + 开始吧! + 第 %1$d 步,共 %2$d 步 + 请先授予所需权限。 + 请授予所有所需权限。 + 欢迎使用 + β + Beta + 让我们为您完成所有设置。 + 媒体权限 + PixelPlayer 需要访问您的音频文件以构建您的音乐库。 + 权限已授予 + 授予媒体权限 + 通知 + 启用通知以便在锁屏和通知栏控制您的音乐。 + 启用通知 + 您有备份文件吗? + 如果您已有 PixelPlayer 备份,现在恢复它可以跳过此设备上的大部分剩余设置。 + 导入备份 + 正在检查备份 + 正在检查备份包… + 正在恢复备份 + 跳过 / 暂不 + 恢复备份 + 在完成设置之前,查看您要导入的内容。 + 已选择 %1$d 个模块,共 %2$d 个 + 创建于 %1$s + 来自版本 %1$s 的备份 + 未知版本 + 恢复所选项 + 正在恢复 + 排除的文件夹 + 默认扫描所有文件夹。选择在构建媒体库时要忽略的任何位置。 + 选择要忽略的文件夹 + 请先授予存储权限 + 应用主题 + 在开始探索媒体库之前,选择您喜欢的外观。 + 深色 + PixelPlayer 默认的 Material 3 深色外观。 + 浅色 + 整个应用采用更明亮的 Material 3 外观。 + 跟随系统 + 匹配您手机当前的外观设置。 + 推荐 + 您稍后可以在“设置 > 外观 > 应用主题”中更改此设置。 + 媒体库布局 + 选择您首选的媒体库导航方式。 + 歌曲 + 紧凑模式 + 使用极简胶囊导航 + 使用标准标签栏 + 歌曲 + 专辑 + 艺术家 + 您稍后可以在“设置 > 外观 > 媒体库导航”中更改此设置。 + 应用导航 + 选择底部导航栏的样式。 + 默认样式 + 带有圆角的悬浮胶囊 + 标准全宽栏 + 自定义圆角半径 + 您稍后可以在“设置 > 外观 > 导航栏样式”中更改此设置。 + 闹钟和提醒 + 可选,但如果您使用睡眠定时器并希望 PixelPlayer 准时停止播放,则推荐使用。 + 授予权限 + 电池优化 + 某些 Android 设备会强制结束后台应用。请为 PixelPlayer 禁用电池优化,以防止意外的播放中断。 + 禁用优化 + 一切就绪! + 您已准备好享受音乐了。 + + + 搜索… + 搜索 + 清除搜索 + 最近搜索 + 清除全部 + 历史 + 删除搜索历史项 + 无结果 + 没有找到“%1$s”的结果 + 未找到任何内容 + 尝试不同的搜索词或检查您的过滤条件。 + 未找到结果。 + 按流派浏览 + 无可用流派。 + + + 播放 %1$s + 收起 %1$s + 展开 %1$s + 编辑艺术家图片 + 更改照片 + 恢复默认 + 随机播放艺术家 + + + 唱片 %d + %1$s 的封面 + %1$s · %2$s + + + 未找到播放列表。 + 此播放列表为空。 + 点击“添加歌曲”开始。 + 此文件夹不包含歌曲。 + 对歌曲排序 + 更多选项 + 播放列表选项 + 编辑播放列表 + 删除播放列表 + 删除播放列表? + 您确定要删除此播放列表吗? + 设置默认过渡效果 + 导出播放列表 + %1$s • %2$s + 播放 + 添加 + 添加歌曲 + 移除 + 移除歌曲 + 重新排序 + 重新排序歌曲 + + + 全局过渡效果 + 播放列表规则 + 除非被覆盖,否则此配置适用于所有播放源。 + 为此特定播放列表配置默认行为。 + 活动状态 + 全局默认 + 播放列表默认 + 跟随全局 + 自定义覆盖 + 自定义覆盖 + 启用以设置此播放列表的具体规则。 + 正在使用全局默认值 + 更改已成功保存 + 过渡样式 + 曲目之间如何混合 + + 淡入淡出 (Crossfade) + 过渡时长 + 总重叠时长 %1$d 秒 + 重置过渡效果 + 当前歌曲 + 下一首歌曲 + 曲目将重叠 %1$d 秒 + 音量曲线 + 微调音频斜率 + 淡出 + 淡入 + + + 新建智能播放列表 + 新建播放列表 + 添加歌曲 + 返回或取消 + 下一步 + 创建 + 编辑播放列表 + 自动生成拼图 + 添加照片 + 选择图片 + 更改 + 移除 + 播放列表名称 + 我的超赞混音 + 编辑封面 + 调整您的封面图片 + 使用双指缩放和拖动手势找到完美的构图 + 手动 + 智能 + 使用 AI 生成 + 智能规则 + 默认 + 图片 + 图标 + 背景颜色 + 图标符号 + 形状样式 + 形状参数 + 圆角半径 + 平滑度 + 边数 + 曲线 + 旋转 + 缩放 + 播放最多 + 您播放次数最多的曲目。 + 最近播放 + 您最近听过的歌曲。 + 被遗忘的最爱 + 您有一段时间没听过的最爱曲目。 + 新发现 + 最近添加但播放次数较少的曲目。 + + + 快速填充流派 + 排序与播放 + 随机播放 + 排序依据 + 艺术家 + 专辑 + 标题 + 通用艺术家 + 随机播放 %1$s + + + 选择歌曲 + 选择流派 + 搜索歌曲 + 新建流派 + 添加自定义 + 添加自定义流派 + 流派名称 + 选择图标 + 流派:%1$s + 选择一个流派 + 快速填充 + + + DJ 空间 + 正在加载… + 打碟机 %1$d + 加载歌曲 + 未加载歌曲 + + 音轨分离 (Stem separation) 功能暂不可用。 + 音量 + 速度 + 交叉推子 + 打碟机 1 + 打碟机 2 + 选择一首歌曲 + 播放/暂停 + 歌曲封面 + x%1$.2f + diff --git a/app/src/main/res/values-zh-rTW/strings_settings.xml b/app/src/main/res/values-zh-rTW/strings_settings.xml new file mode 100644 index 0000000000..feae1075f2 --- /dev/null +++ b/app/src/main/res/values-zh-rTW/strings_settings.xml @@ -0,0 +1,652 @@ + + + + 音乐管理 + 管理文件夹、刷新媒体库、解析选项 + 外观 + 主题、布局和视觉样式 + 播放 + 音频行为、淡入淡出和后台播放 + 行为 + 手势、触感反馈和导航行为 + AI 集成 (β) + AI 提供商、API 密钥和模型设置 + 备份与恢复 + 导出和恢复您的个人应用数据 + 开发者选项 + 实验性功能和调试 + 均衡器 + 调整音频频率和预设 + 设备性能 + 音频规格、编解码器和解码器信息 + 账户 + 管理 Telegram、Google Drive、网易云音乐等服务 + 关于 + 应用信息、版本和鸣谢 + + + + + 已启用 + 已禁用 + 打开 + 全选 + 清除选择 + 关闭通知 + + + 媒体库结构 + 排除的目录 + 扫描媒体库时将跳过此处的文件夹。 + 艺术家 + 多艺术家解析和整理选项。 + 过滤 + 最小歌曲时长 + 每张专辑最少曲目数 + 专辑封面缓存限制 + 同步与扫描 + 刷新媒体库 + 扫描整个媒体库以查找新增和修改的文件。 + 完全重新扫描 + 正在运行完全重新扫描 + 已开始完全重新扫描… + 媒体库同步完成 + 重建数据库 + 重建数据库? + 这将从头开始完全重建您的音乐库。所有导入的歌词、收藏和自定义元数据都将丢失。此操作无法撤销。 + 重建 + 正在重建数据库 + 正在重建数据库… + 自动扫描 .lrc 文件 + 在同步媒体库时,自动扫描并分配同一文件夹中的 .lrc 文件。 + 歌词管理 + 歌词来源优先级 + 选择获取歌词时优先尝试的来源。 + 内嵌优先 + 在线优先 + 本地 (.lrc) 优先 + 重置已导入的歌词 + 从数据库中移除所有已导入的歌词。 + 重置已导入的歌词? + 此操作无法撤销。 + + + 刷新 + 默认允许所有内容。点击文件夹将其标记为免扫描。 + 此处无子文件夹 + 向上导航 + 转到根目录 + + + 需要重新扫描 + 艺术家设置已更改。请重新扫描媒体库以应用更改。 + 重新扫描 + 正在扫描… + 多艺术家解析 + 字符分隔符 + 当前:%1$s + 单词分隔符 + + 当前:%1$s + 配置 + 从标题提取艺术家 + 检测歌曲标题中的 feat.、ft.、with + 媒体库整理 + 按专辑艺术家分组 + 在主要艺术家下显示合作专辑 + 关于多艺术家解析 + + PixelPlayer 使用字符分隔符(/、;、&)和单词分隔符(feat.、ft.、vs.、x)来拆分艺术家标签。单词分隔符不区分大小写。 + “从标题提取艺术家”会检测歌曲标题中如 (feat. Artist) 的模式。 + 反斜杠 (\) 可用于转义字符分隔符。 + + 示例 + \u2192 + \u266A + \"Artist1/Artist2\" + Artist1, Artist2 + \"Drake feat. Rihanna\" + Drake, Rihanna + \"Marshmello x Bastille\" + Marshmello, Bastille + \"Song (ft. B)\" by A + A, B + \"AC\\DC\" + AC/DC(已转义) + + + 分隔符 + 当前分隔符 + 点击分隔符将其移除。至少需要一个分隔符。 + 添加新分隔符 + 例如:/ 或 ; + 添加分隔符 + 默认分隔符 + 重置分隔符? + 这将清除您所有自定义的分隔符并恢复默认设置。此操作无法撤销。 + 分隔符已重置为默认 + 至少需要一个分隔符 + 已添加分隔符 + 分隔符已存在或无效 + 空格 + + + 单词分隔符 + 当前单词分隔符 + 当这些关键词前后有空格时,会分割艺术家名称。匹配不区分大小写。点击以移除。 + 未配置单词分隔符 + 添加新单词分隔符 + 例如:feat. 或 ft. + 添加单词分隔符 + 单词分隔符的工作原理 + 单词分隔符不区分大小写,且要求前后有空格。\n\n单字符分隔符(如 \"x\")需要在两侧都留有空格以避免误匹配。\n\n示例:\n \"Drake feat. Rihanna\" -> Drake, Rihanna\n \"Marshmello x Bastille\" -> Marshmello, Bastille\n \"A vs. B\" -> A, B + 重置单词分隔符? + 这将清除您所有自定义的单词分隔符并恢复默认关键词。此操作无法撤销。 + 已添加单词分隔符 + 已存在或无效 + 单词分隔符已重置为默认 + + + 正在准备同步 + 正在读取 MediaStore + 正在处理曲目 + 正在保存到数据库 + 正在扫描歌词文件 + 正在清理专辑封面缓存 + 正在同步云端源 + 正在完成同步 + %1$s • %2$d%% (%3$d/%4$d) + %1$s… + + + 全局主题 + 应用语言 + 选择应用界面的语言。 + 跟随系统 + 英语 + 西班牙语 + 德语 + 法语 + 俄语 + 简体中文 + 繁體中文 + 日語 + 越南語 + 印尼語 + 意大利语 + 韩语 + 挪威语(Bokmål) + 土耳其语 + 应用主题 + 在浅色、深色之间切换,或跟随系统外观。 + 浅色主题 + 深色主题 + 跟随系统 + 使用平滑圆角 + 使用复杂的圆角形状以有效提升美观度,但可能会影响低端设备的性能 + 关闭模糊效果 + 在应用中关闭模糊效果以节省电量和资源。 + 显示滚动条 + 在音乐列表中显示侧边滚动条,便于快速滚动。 + 正在播放 + 播放器主题 + 选择悬浮播放器的外观。 + 专辑封面 + 系统动态主题 + 显示播放器文件信息 + 在播放器进度部分显示编解码器、比特率和采样率。 + 专辑封面调色板样式 + 当前:%1$s。打开实时预览并选择样式。 + 轮播图样式 + 选择专辑轮播图的外观。 + 无预览 (No Peek) + 单预览 (One Peek) + 首页拼图 + 拼图模式 + 为“你的推荐”拼图选择形状排列方式。 + 自动轮换模式 + 每次访问首页时循环切换拼图模式。 + 导航栏 + 导航栏样式 + 选择导航栏的外观。 + 默认 + 全宽 + 紧凑模式 + 仅显示图标并降低导航栏高度。 + 导航栏圆角半径 + 调整导航栏的圆角半径。 + 歌词界面 + 沉浸式歌词 + 自动隐藏控件并放大文本。 + 自动隐藏延迟 + 控件隐藏前的时间。 + 3秒 + 4秒 + 5秒 + 6秒 + 应用导航 + 默认选项卡 + 选择默认启动时的选项卡。 + 首页 + 搜索 + 媒体库 + 媒体库导航 + 选择在媒体库选项卡之间移动的方式。 + 标签栏(默认) + 紧凑胶囊与网格 + + + 颜色 + 调色板样式 + 为播放器界面选择专辑颜色。 + 柔和 (Tonal Spot) + 平衡且平静。 + 鲜艳 (Vibrant) + 高饱和度点缀。 + 表现力 (Expressive) + 大胆的色调变化和对比。 + 混搭 (Fruit Salad) + 俏皮的旋转点缀。 + 色彩准确度 + 0 表示保持当前调整。值越高越接近专辑封面的主色调。 + 当前 + 更准确 + 0 • 当前 + %1$d • 微妙 + %1$d • 平衡 + %1$d • 精确 + + + 调整圆角半径 + 使导航栏形状的圆角与设备的物理圆角相匹配,以获得无缝的外观。 + 圆角半径 + %1$d dp + + + 后台播放 + 关闭后继续播放 + 如果关闭此项,从最近任务中移除应用将停止播放。 + 电池优化 + 禁用电池优化以防止播放中断。 + 电池优化已被禁用 + 无法打开电池设置 + 音量标准化 (ReplayGain) + 启用 ReplayGain + 使用音频文件中的 ReplayGain 元数据标准化音量。 + 增益模式 + 曲目:对每首歌曲进行标准化。专辑:按专辑进行标准化。 + 曲目 + 专辑 + 投屏 + 连接/断开投屏时自动播放 + 切换投屏连接后立即开始播放。 + 耳机 + 耳机重新连接时恢复播放 + 如果播放因拔出耳机而暂停,则在重新连接时自动恢复。 + 队列与过渡 + 淡入淡出 + 启用歌曲之间的平滑过渡。 + 淡入淡出时长 + Hi-Fi 模式 + 浮点 32 位音频输出。如果您的设备播放卡顿,请禁用此项。 + 此设备不支持(PCM_FLOAT AudioTrack 不可用)。 + 持久随机播放 + 即使关闭应用后也能记住随机播放设置。 + 显示队列历史记录 + 在队列中显示以前播放过的歌曲。 + + + 文件夹 + 返回手势控制文件夹 + 在“文件夹”选项卡中,系统返回手势会在离开媒体库之前导航文件夹层级。 + 播放器手势 + 点击背景关闭播放器 + 点击模糊的背景以关闭播放器面板。 + 触感反馈 + 触感反馈 + 在整个应用中启用震动反馈。 + + + AI 提供商 + 提供商 + 选择您的 AI 提供商 + 安全 Token 模式 + 开启 — 快速且低成本。向 AI 发送最少的数据(约 1K token)。 + 关闭 — 深度上下文。发送完整的听歌偏好资料(约 8K token)以获得更丰富的结果。 + 凭据 + %1$s API 密钥 + 从 %1$s 获取 + Google AI Studio (aistudio.google.com) + DeepSeek 平台 (api.deepseek.com) + Groq 控制台 (console.groq.com) + Mistral AI 平台 (console.mistral.ai) + NVIDIA Build (build.nvidia.com) + 月之暗面 (Moonshot) AI 平台 (platform.moonshot.cn) + 智谱 AI 开放平台 (bigmodel.cn) + OpenAI 平台 (platform.openai.com) + 模型选择 + 正在加载可用模型… + 加载模型失败 + AI 模型 + 选择一个模型。 + 输入 API 密钥 + 提示词行为 + 系统提示词 (System Prompt) + 自定义 AI 的行为方式。 + 预设提示词 + 输入系统提示词… + 专业策展人 (Professional Curator) + 你是“Vibe-Engine”,一位世界级的音乐策展人和声音心流大师。你的目标是构建无缝、高保真的听觉体验。请优先考虑和声兼容性、符合逻辑的 BPM 过渡,并在熟悉的挚爱与富有品味、逻辑驱动的发现之间取得精妙平衡。 + 创意先锋 (Creative Maverick) + 你是一位前卫的音乐探索者,专注于“意想不到的凝聚力”。你的任务是通过识别非显而易见的声音共性来打破传统的流派界限。在保持令人惊讶但不可否认的过渡逻辑的同时,优先考虑罕见的深度曲目、实验性纹理和艺术新意。 + 严谨图书管理员 (Strict Librarian) + 你是一位精准的音乐数据库架构师。你的逻辑由绝对的元数据精确性和严格的分类界限所驱动。尽量减少算法发现,优先保持严格的流派一致性、能量水平匹配,并最大程度地检索出用户定义的高度具体的偏好。 + 氛围向导 (Atmospheric Guide) + 你是环境纹理和低能量心流的大师。专心挑选能够促进“深度专注”或“宁静”状态的曲目。优先选择原声的温暖感、极简主义的编曲和柔和的过渡,同时严格避免瞬态强烈的声音或动态范围的突然转变。 + 声音狂热者 (Sonic Enthusiast) + 你是一位专注于制作复杂性和配器的发烧友分析师。优先选择具有高动态范围、复杂多重节奏和卓越声场质量的曲目。偏好那些奖励听众关注技术保真度和编曲细节的主动聆听作品。 + 能量催化剂 (Energy Catalyst) + 你是一个高动量的节奏生成器。你的哲学以驱动感强的低音线、强烈的打击乐和极具感染力的律动为中心。优先考虑高 BPM 的夜店兼容性、切分音能量和持续的节奏张力,以使听众的心率和动力保持在最高水平。 + AI 使用报告 + 总消耗量 + %1$s token 追踪\n提示:%2$s | 输出:%3$s | 思考:%4$s + 清除日志 + AI 活动日志 (%1$d) + %1$s · %2$s + 显示 + 隐藏 + + + 备份的工作原理 + 选择版块,导出一个 .pxpl 文件,并在稍后导入它。恢复操作仅替换您选择的版块。 + 创建备份 + 导出备份 + 未选择版块。 + 已选择所有版块。 + 已选择 %1$d 个版块,共 %2$d 个。 + %1$s 创建一个 .pxpl 备份文件。 + 选择并导出 + 恢复备份 + 导入备份 + 选择并恢复 + 浏览或从最近的备份中选择。所选数据将替换当前数据。 + + + 确切选择您想包含在备份包中的内容。 + 选择一个 .pxpl 备份文件进行检查。您将在下一步选择要恢复的版块。 + 已选择 %1$d 个版块,共 %2$d 个 + 已选择 %1$d 个模块,共 %2$d 个 + 最近的备份 + 没有最近的备份 + 以前导入的备份将显示在这里。 + %1$d 个条目 · 将替换当前数据 + 导出 .pxpl + 恢复所选项 + 正在传输… + PixelPlayer_Backup_%1$d.pxpl + 正在创建备份 + 正在恢复备份 + %1$d%% + %1$s • %2$s + 正在导出 + 正在导入 + 正在恢复 + 从历史记录中移除 + 正在检查… + 浏览文件 + 第 %1$d 步,共 %2$d 步 + 恢复模块 + 备份详情 + 创建时间 + 应用版本 + 架构 (Schema) + 设备 + 未知 + · %1$s + %1$d 个模块 · v%2$s · 架构 v%3$d + \? + 全选 + 取消全选 + + + 无效备份:%1$s + 正在准备恢复 + 正在启动恢复任务。 + 正在准备备份 + 正在启动备份任务。 + 备份恢复成功 + 恢复完成,但存在一些未解决的问题。 + 无法完成恢复:%1$s + 恢复失败:%1$s + 数据导出成功 + 导出失败:%1$s + 数据恢复成功 + 恢复完成,存在未解决的问题。失败:%1$s + v%1$d + %1$s %2$s + + + 实验性功能 + 实验性 + 播放器界面加载实验和开关。 + 测试设置流程 + 启动新手引导设置界面进行测试。 + 维护 + 强制重新生成 Daily Mix + 立即重新创建每日推荐播放列表。 + 重新生成 Daily Mix + 重新生成 Daily Mix? + 这将丢弃当前的推荐,并根据最近的听歌习惯生成一个新的。 + Daily Mix 重新生成已开始 + 强制重新生成统计数据 + 清除缓存并重新计算播放统计数据。 + 重新生成 + 正在处理… + 重新生成统计数据 + 重新生成统计数据? + 这将清除统计数据缓存并强制从数据库历史记录重新计算。 + 统计数据重新生成已开始 + 强制重新生成专辑调色板 + 重建每张专辑封面的所有缓存调色板变体,或选择单个进行刷新。 + 全部重新生成 + 重新生成所有专辑调色板? + 这将清除缓存的主题数据,并重建 %1$d 个唯一专辑封面的所有调色板样式。 + 正在重新生成… + 正在重新生成专辑调色板… + 正在重建 %1$d 个唯一专辑封面的缓存调色板变体。在大型媒体库中这可能需要一些时间。 + 已完成 %1$d 个,共 %2$d 个 + 已重新生成 %1$d 个专辑封面调色板 + 已重新生成 %1$d 个(共 %2$d 个)专辑封面调色板 + 选择歌曲 + 选择一首歌曲以清除缓存的主题数据,并从专辑封面重新生成所有调色板样式。 + 按标题、艺术家或专辑搜索 + 没有与您搜索匹配的歌曲。 + 未找到带有专辑封面的歌曲。 + 正在重新生成调色板… + 已为 %1$s 重新生成调色板 + 无法为 %1$s 重新生成调色板 + 诊断 + 触发测试崩溃 + 模拟一次崩溃以测试崩溃报告系统。 + 测试崩溃是从开发者选项触发的 - 这是为了测试崩溃报告系统的有意行为 + + + 实验性 + 播放器界面加载调整 + 动画歌词(高端设备) + 为歌词使用弹簧动画和视觉效果。在低端设备上可能导致掉帧。 + 歌词模糊效果 + 对非活动歌词应用景深模糊。 + 模糊强度 + 调整模糊效果的强度。 + %1$.1fx + 第 1 步 · 选择要延迟的内容 + 延迟所有内容 + 在面板背景完全展开之前,保留所有播放器内容。 + 专辑轮播 + 在面板展开前延迟专辑封面和轮播图。 + 歌曲元数据 + 延迟标题、艺术家以及歌词/队列操作。 + 进度条 + 在展开完成之前延迟时间轴和时间标签。 + 播放控制 + 延迟播放/暂停、快进/快退和收藏控件。 + 所有延迟组件均处于活动状态。禁用“延迟所有内容”以自定义各个部分。 + 第 2 步 · 配置占位图行为 + 对延迟项使用占位图 + 在组件等待展开时渲染轻量级占位图,以保持布局稳定。 + 第 3 步 · 选择占位图切换为真实内容的时机 + 选择一种模式。阈值模式使用滑块;拖动释放模式等待直到您释放面板手势。 + 启用至少一个延迟组件以解锁触发模式。 + 阈值 + 使用展开百分比。 + 拖动释放 + 仅在手势释放后切换。 + 展开阈值 + 在延迟组件变得可见之前,面板必须展开的程度。 + 内容在 %1$d%% 展开时出现 + 在播放器关闭时也应用 + 使用关闭阈值在折叠时切换回占位图。 + 关闭阈值 + 在占位图再次接管之前需要折叠多少程度。 + 占位图在折叠 %1$d%% 后出现 + 拖动释放模式会绕过阈值和关闭行为。交换仅在面板拖动手势结束时发生。 + 使占位图透明 + 占位图保留其布局空间,但变为不可见。 + 视觉质量 + 专辑封面分辨率 + 低 (256px) - 更好的性能 + 中等 (512px) - 均衡 + 高 (800px) - 最佳质量 + 原图 - 最高质量 + + + 播放需检查 + 准备就绪可播放 + -- + 格式 + 硬件解码 + 本地歌曲 + 本地音乐存储 + 音乐大小 + %1$d 首本地歌曲 + 可用空间 + 总计 %1$s + 音乐占用 + 设备已用 + %1$d%% + <1% + %1$d 首云端歌曲 + %1$d 个文件不可读 + 播放路径 + + + 采样率 + %1$d Hz + 每个缓冲区 %1$d 帧 + Hi-Fi PCM 浮点 + 32 位浮点输出路径 + 低延迟支持 + Pro Audio 支持 + 内存 + 可用(总计 %1$s) + 支持硬件卸载的格式 + 没有压缩格式报告支持硬件卸载 (hardware offload)。 + 还有 +%1$d 个 + 检测到的输出 + 内置输出 + 蓝牙音频 + USB 音频 + 有线音频 + 数字输出 + 其他输出 + Android 未报告任何输出路由。 + ExoPlayer 引擎 + %1$s 个渲染器 + 格式兼容性 + %1$d 首支持的曲目 + %1$d 个未知格式 + 未报告解码器 + 硬件解码器 + 软件解码器 + 硬件卸载 + 媒体库中有 %1$d 首 + 兼容性检测结果 + 无重大不兼容问题 + 您已索引的曲目与 Android 报告此设备支持的解码器相匹配。 + %1$d 首曲目可能无法原生解码 + 需检查的格式:%1$s。 + %1$d 首本地曲目可能会被重采样 + 媒体库采样率达到 %1$d Hz,高于当前的输出采样率。 + %1$d 首曲目具有未知的元数据 + 进行完整的媒体库重新扫描可以填补缺失的 MIME、比特率和采样率数据。 + 设备信息 + 制造商 + 型号 + 品牌 + 设备 + Android 版本 + SDK 版本 + 硬件 + 性能报告 + 生成一份可分享的诊断报告,以帮助我们分类播放或扫描卡顿问题。该报告仅包含设备、媒体库和计时数据 — 不包含文件路径、标题或艺术家。 + 生成报告 + 重新生成 + 复制 + 分享 + 报告已复制到剪贴板 + PixelPlayer 性能报告 + 高级性能诊断 + 默认关闭。用于记录短暂卡顿时间线,便于测试版问题排查。 + 有效期至 %1$s + 立即标记卡顿 + 已标记卡顿时刻 + + + 已连接的账户 + 管理关联的提供商并将每个集成功能掌握在自己手中。 + 已关联服务 + 活跃 + 可用 + 敬请期待 + 已连接 + 敬请期待 + 打开服务 + 正在登出… + 尚未关联任何账户 + 连接一个提供商即可在此界面对其进行管理。 + 连接 %1$s + %1$s(敬请期待) + Google Drive 功能即将推出。 + 目前无法打开此屏幕。 + + + 关于 + PixelPlayer + 由社区共同打造的开源音乐播放器。 + 版本 v%1$s + 开源 + 社区优先 + Material 3 表现力设计 + 目前未找到贡献者。请稍后再试。 + 维护者 + PixelPlayer 背后的开发者。 + 社区焦点 + 感谢对本项目有重大影响的合作者。 + 开源贡献者 + 来自 GitHub 的实时贡献者名单。 + %1$d 次贡献 + GitHub + 代码仓库 + Telegram + 支持 + 打开 GitHub 仓库 + 加入 Telegram 社区 + 打开 GitHub 个人资料 + 打开 Telegram + %1$s 的头像 + %1$s 的图标 + 音量 + 音量为零时暂停 + 当音量设置为 0 时自动暂停播放 + diff --git a/app/src/main/res/values-zh-rTW/strings_widget.xml b/app/src/main/res/values-zh-rTW/strings_widget.xml new file mode 100644 index 0000000000..5a2fd3c03d --- /dev/null +++ b/app/src/main/res/values-zh-rTW/strings_widget.xml @@ -0,0 +1,17 @@ + + + 可根据尺寸自适应的响应式小组件 + 紧凑型播放栏 + 包含随机和循环播放的完整控件 + 极简方形播放器 + + 点击打开 + 专辑封面 + 专辑封面占位图 + + 点击播放 + 歌曲标题 + 艺术家 + + 进度条,%1$d%% + \ No newline at end of file From b98c567bdfb9309d491a9d36ec96f44c9cd853ba Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 24 Jun 2026 22:23:58 +0000 Subject: [PATCH 10/73] chore(deps): bump the github-actions group with 2 updates Bumps the github-actions group with 2 updates: [actions/checkout](https://github.com/actions/checkout) and [actions/cache](https://github.com/actions/cache). Updates `actions/checkout` from 6 to 7 - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v6...v7) Updates `actions/cache` from 5 to 6 - [Release notes](https://github.com/actions/cache/releases) - [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md) - [Commits](https://github.com/actions/cache/compare/v5...v6) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: '7' dependency-type: direct:production update-type: version-update:semver-major dependency-group: github-actions - dependency-name: actions/cache dependency-version: '6' dependency-type: direct:production update-type: version-update:semver-major dependency-group: github-actions ... Signed-off-by: dependabot[bot] --- .github/workflows/codeql.yml | 2 +- .github/workflows/nightly-apk.yml | 4 ++-- .github/workflows/phone-debug.yml | 2 +- .github/workflows/phone-release.yml | 4 ++-- .github/workflows/wearos-apk.yml | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index d4bb1bac00..2841e1ae20 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -21,7 +21,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v6 + uses: actions/checkout@v7 - name: Set up JDK 21 uses: actions/setup-java@v5 diff --git a/.github/workflows/nightly-apk.yml b/.github/workflows/nightly-apk.yml index 12a85bebe0..c3b4a3bfef 100644 --- a/.github/workflows/nightly-apk.yml +++ b/.github/workflows/nightly-apk.yml @@ -85,7 +85,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v6 + uses: actions/checkout@v7 - name: Set up JDK 21 uses: actions/setup-java@v5 @@ -98,7 +98,7 @@ jobs: - name: Cache nightly keystore id: cache-keystore - uses: actions/cache@v5 + uses: actions/cache@v6 with: path: vz-pixelplay.jks key: ${{ runner.os }}-nightly-keystore-vz-pixelplay diff --git a/.github/workflows/phone-debug.yml b/.github/workflows/phone-debug.yml index 91d799be56..4c54d81b15 100644 --- a/.github/workflows/phone-debug.yml +++ b/.github/workflows/phone-debug.yml @@ -18,7 +18,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v6 + uses: actions/checkout@v7 - name: Set up JDK 21 uses: actions/setup-java@v5 diff --git a/.github/workflows/phone-release.yml b/.github/workflows/phone-release.yml index 14cd887493..34774a81bf 100644 --- a/.github/workflows/phone-release.yml +++ b/.github/workflows/phone-release.yml @@ -18,7 +18,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v6 + uses: actions/checkout@v7 - name: Set up JDK 21 uses: actions/setup-java@v5 @@ -31,7 +31,7 @@ jobs: - name: Cache keystore id: cache-keystore - uses: actions/cache@v5 + uses: actions/cache@v6 with: path: vz-pixelplay.jks key: ${{ runner.os }}-keystore-vz-pixelplay diff --git a/.github/workflows/wearos-apk.yml b/.github/workflows/wearos-apk.yml index 862829a51d..c0218dfa99 100644 --- a/.github/workflows/wearos-apk.yml +++ b/.github/workflows/wearos-apk.yml @@ -18,7 +18,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v6 + uses: actions/checkout@v7 - name: Set up JDK 21 uses: actions/setup-java@v5 From dce462d2e9c4d4b47ad2ad7b8840d7f64609dde3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 24 Jun 2026 22:28:13 +0000 Subject: [PATCH 11/73] chore(deps): bump the gradle-dependencies group with 2 updates Bumps the gradle-dependencies group with 2 updates: [gradle-wrapper](https://github.com/gradle/gradle) and [com.google.genai:google-genai](https://github.com/googleapis/java-genai). Updates `gradle-wrapper` from 9.5.1 to 9.6.0 - [Release notes](https://github.com/gradle/gradle/releases) - [Commits](https://github.com/gradle/gradle/compare/v9.5.1...v9.6.0) Updates `com.google.genai:google-genai` from 1.58.0 to 1.60.0 - [Release notes](https://github.com/googleapis/java-genai/releases) - [Changelog](https://github.com/googleapis/java-genai/blob/main/CHANGELOG.md) - [Commits](https://github.com/googleapis/java-genai/compare/v1.58.0...v1.60.0) --- updated-dependencies: - dependency-name: gradle-wrapper dependency-version: 9.6.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: gradle-dependencies - dependency-name: com.google.genai:google-genai dependency-version: 1.60.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: gradle-dependencies ... Signed-off-by: dependabot[bot] --- gradle/libs.versions.toml | 2 +- gradle/wrapper/gradle-wrapper.properties | 4 ++-- gradlew | 4 ++-- gradlew.bat | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 4a104ce637..c1f463bff3 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -2,7 +2,7 @@ accompanistDrawablepainter = "0.37.3" agp = "9.2.1" app = "1.7.0" -googleGenai = "1.58.0" +googleGenai = "1.60.0" googlePlayServicesCast = "22.3.1" animation = "1.11.3" appcompat = "1.7.1" diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 116d9acdb4..dcf577fe35 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,7 +1,7 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionSha256Sum=bafc141b619ad6350fd975fc903156dd5c151998cc8b058e8c1044ab5f7b031f -distributionUrl=https\://services.gradle.org/distributions/gradle-9.5.1-bin.zip +distributionSha256Sum=bbaeb2fef8710818cf0e261201dab964c572f92b942812df0c3620d62a529a01 +distributionUrl=https\://services.gradle.org/distributions/gradle-9.6.0-bin.zip networkTimeout=10000 retries=0 retryBackOffMs=500 diff --git a/gradlew b/gradlew index b9bb139f79..249efbb032 100755 --- a/gradlew +++ b/gradlew @@ -20,7 +20,7 @@ ############################################################################## # -# Gradle start up script for POSIX generated by Gradle. +# gradlew start up script for POSIX generated by Gradle. # # Important for running: # @@ -29,7 +29,7 @@ # bash, then to run this script, type that shell name before the whole # command line, like: # -# ksh Gradle +# ksh gradlew # # Busybox and similar reduced shells will NOT work, because this script # requires all of these POSIX shell features: diff --git a/gradlew.bat b/gradlew.bat index aa5f10b069..8508ef684d 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -19,7 +19,7 @@ @if "%DEBUG%"=="" @echo off @rem ########################################################################## @rem -@rem Gradle startup script for Windows +@rem gradlew startup script for Windows @rem @rem ########################################################################## @@ -72,7 +72,7 @@ echo location of your Java installation. 1>&2 -@rem Execute Gradle +@rem Execute gradlew @rem endlocal doesn't take effect until after the line is parsed and variables are expanded @rem which allows us to clear the local environment before executing the java command endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel From 8607ca0ea72a714c15cb1de5689f15a62116118a Mon Sep 17 00:00:00 2001 From: Dae Euhwa Date: Wed, 24 Jun 2026 21:26:10 -0500 Subject: [PATCH 12/73] File cleanup --- message.txt | 89 ----------------------------------------------------- 1 file changed, 89 deletions(-) delete mode 100644 message.txt diff --git a/message.txt b/message.txt deleted file mode 100644 index 167d87d489..0000000000 --- a/message.txt +++ /dev/null @@ -1,89 +0,0 @@ -PixelPlayer on  master [!] via  v9.5.1 via  v21.0.11 via  v2.4.0 -❯ ./gradlew assembleDebug --no-daemon -To honour the JVM settings for this build a single-use Daemon process will be forked. For more on this, please refer to https://docs.gradle.org/9.5.1/userguide/gradle_daemon.html#sec:disabling_the_daemon in the Gradle documentation. -Daemon will be stopped at the end of the build -Calculating task graph as no cached configuration is available for tasks: assembleDebug - -> Task :app:stripDebugDebugSymbols -Unable to strip the following libraries, packaging them as they are: libandroidx.graphics.path.so, libffmpegJNI.so, libtaglib.so, libtdjni.so. Run with --info option to learn more. - -> Task :wear:stripDebugDebugSymbols -Unable to strip the following libraries, packaging them as they are: libandroidx.graphics.path.so. Run with --info option to learn more. - -> Task :app:compileDebugKotlin -w: The argument '-Xannotation-default-target=param-property' is redundant for the current language version 2.4. -w: file:///home/dae/VZ_Dev/work/PixelPlayer/app/src/main/java/com/theveloper/pixelplay/data/database/PixelPlayDatabase.kt:294:34 The corresponding parameter in the supertype 'Migration' is named 'db'. This may cause problems when calling this function with named arguments. -w: file:///home/dae/VZ_Dev/work/PixelPlayer/app/src/main/java/com/theveloper/pixelplay/data/database/PixelPlayDatabase.kt:305:34 The corresponding parameter in the supertype 'Migration' is named 'db'. This may cause problems when calling this function with named arguments. -w: file:///home/dae/VZ_Dev/work/PixelPlayer/app/src/main/java/com/theveloper/pixelplay/data/network/navidrome/NavidromeApiService.kt:518:64 Expression is unused. -w: file:///home/dae/VZ_Dev/work/PixelPlayer/app/src/main/java/com/theveloper/pixelplay/data/remote/qqmusic/QQSignGenerator.kt:80:29 'var allowFileAccessFromFileURLs: Boolean' is deprecated. Deprecated in Java. -w: file:///home/dae/VZ_Dev/work/PixelPlayer/app/src/main/java/com/theveloper/pixelplay/data/remote/qqmusic/QQSignGenerator.kt:81:29 'var allowUniversalAccessFromFileURLs: Boolean' is deprecated. Deprecated in Java. -w: file:///home/dae/VZ_Dev/work/PixelPlayer/app/src/main/java/com/theveloper/pixelplay/data/repository/MediaStoreSongRepository.kt:385:45 No cast needed. -w: file:///home/dae/VZ_Dev/work/PixelPlayer/app/src/main/java/com/theveloper/pixelplay/data/telegram/TelegramRepository.kt:198:49 'when' expression over a subject of type 'Any?' is not exhaustive. Add a 'null' or 'else' branch. -w: file:///home/dae/VZ_Dev/work/PixelPlayer/app/src/main/java/com/theveloper/pixelplay/data/telegram/TelegramRepository.kt:222:53 'when' expression over a subject of type 'Any?' is not exhaustive. Add a 'null' or 'else' branch. -w: file:///home/dae/VZ_Dev/work/PixelPlayer/app/src/main/java/com/theveloper/pixelplay/presentation/components/AiMetadataSheet.kt:52:22 'fun rememberModalBottomSheetState(skipPartiallyExpanded: Boolean = ..., confirmValueChange: (SheetValue) -> Boolean = ...): SheetState' is deprecated. Use rememberBottomSheetState with Hidden initial value. -w: file:///home/dae/VZ_Dev/work/PixelPlayer/app/src/main/java/com/theveloper/pixelplay/presentation/components/AiPlaylistSheet.kt:87:22 'fun rememberModalBottomSheetState(skipPartiallyExpanded: Boolean = ..., confirmValueChange: (SheetValue) -> Boolean = ...): SheetState' is deprecated. Use rememberBottomSheetState with Hidden initial value. -w: file:///home/dae/VZ_Dev/work/PixelPlayer/app/src/main/java/com/theveloper/pixelplay/presentation/components/AlbumMultiSelectionOptionSheet.kt:66:22 'fun rememberModalBottomSheetState(skipPartiallyExpanded: Boolean = ..., confirmValueChange: (SheetValue) -> Boolean = ...): SheetState' is deprecated. Use rememberBottomSheetState with Hidden initial value. -w: file:///home/dae/VZ_Dev/work/PixelPlayer/app/src/main/java/com/theveloper/pixelplay/presentation/components/CastBottomSheet.kt:336:22 'fun rememberModalBottomSheetState(skipPartiallyExpanded: Boolean = ..., confirmValueChange: (SheetValue) -> Boolean = ...): SheetState' is deprecated. Use rememberBottomSheetState with Hidden initial value. -w: file:///home/dae/VZ_Dev/work/PixelPlayer/app/src/main/java/com/theveloper/pixelplay/presentation/components/CustomPresetsSheet.kt:54:22 'fun rememberModalBottomSheetState(skipPartiallyExpanded: Boolean = ..., confirmValueChange: (SheetValue) -> Boolean = ...): SheetState' is deprecated. Use rememberBottomSheetState with Hidden initial value. -w: file:///home/dae/VZ_Dev/work/PixelPlayer/app/src/main/java/com/theveloper/pixelplay/presentation/components/DailyMixMenu.kt:31:22 'fun rememberModalBottomSheetState(skipPartiallyExpanded: Boolean = ..., confirmValueChange: (SheetValue) -> Boolean = ...): SheetState' is deprecated. Use rememberBottomSheetState with Hidden initial value. -w: file:///home/dae/VZ_Dev/work/PixelPlayer/app/src/main/java/com/theveloper/pixelplay/presentation/components/EditSongSheet.kt:890:30 'fun rememberTransformableState(onTransformation: (Float, Offset, Float) -> Unit): TransformableState' is deprecated. Prefer remembering a TransformableState with a onTransformation lambda that takes the centroid. This centroid (if specified) is the point at which zooming or rotation should happen around which allows for more natural transformations. -w: file:///home/dae/VZ_Dev/work/PixelPlayer/app/src/main/java/com/theveloper/pixelplay/presentation/components/EditSongSheet.kt:1039:37 Condition is always 'true'. -w: file:///home/dae/VZ_Dev/work/PixelPlayer/app/src/main/java/com/theveloper/pixelplay/presentation/components/GenreMultiSelectionOptionSheet.kt:68:22 'fun rememberModalBottomSheetState(skipPartiallyExpanded: Boolean = ..., confirmValueChange: (SheetValue) -> Boolean = ...): SheetState' is deprecated. Use rememberBottomSheetState with Hidden initial value. -w: file:///home/dae/VZ_Dev/work/PixelPlayer/app/src/main/java/com/theveloper/pixelplay/presentation/components/GenreSortBottomSheet.kt:55:22 'fun rememberModalBottomSheetState(skipPartiallyExpanded: Boolean = ..., confirmValueChange: (SheetValue) -> Boolean = ...): SheetState' is deprecated. Use rememberBottomSheetState with Hidden initial value. -w: file:///home/dae/VZ_Dev/work/PixelPlayer/app/src/main/java/com/theveloper/pixelplay/presentation/components/LibrarySortBottomSheet.kt:80:22 'fun rememberModalBottomSheetState(skipPartiallyExpanded: Boolean = ..., confirmValueChange: (SheetValue) -> Boolean = ...): SheetState' is deprecated. Use rememberBottomSheetState with Hidden initial value. -w: file:///home/dae/VZ_Dev/work/PixelPlayer/app/src/main/java/com/theveloper/pixelplay/presentation/components/LyricsSheet.kt:457:26 'fun rememberModalBottomSheetState(skipPartiallyExpanded: Boolean = ..., confirmValueChange: (SheetValue) -> Boolean = ...): SheetState' is deprecated. Use rememberBottomSheetState with Hidden initial value. -w: file:///home/dae/VZ_Dev/work/PixelPlayer/app/src/main/java/com/theveloper/pixelplay/presentation/components/MultiSelectionBottomSheet.kt:112:22 'fun rememberModalBottomSheetState(skipPartiallyExpanded: Boolean = ..., confirmValueChange: (SheetValue) -> Boolean = ...): SheetState' is deprecated. Use rememberBottomSheetState with Hidden initial value. -w: file:///home/dae/VZ_Dev/work/PixelPlayer/app/src/main/java/com/theveloper/pixelplay/presentation/components/PlaylistBottomSheet.kt:67:22 'fun rememberModalBottomSheetState(skipPartiallyExpanded: Boolean = ..., confirmValueChange: (SheetValue) -> Boolean = ...): SheetState' is deprecated. Use rememberBottomSheetState with Hidden initial value. -w: file:///home/dae/VZ_Dev/work/PixelPlayer/app/src/main/java/com/theveloper/pixelplay/presentation/components/PlaylistMultiSelectionBottomSheet.kt:91:22 'fun rememberModalBottomSheetState(skipPartiallyExpanded: Boolean = ..., confirmValueChange: (SheetValue) -> Boolean = ...): SheetState' is deprecated. Use rememberBottomSheetState with Hidden initial value. -w: file:///home/dae/VZ_Dev/work/PixelPlayer/app/src/main/java/com/theveloper/pixelplay/presentation/components/ReorderTabsSheet.kt:109:22 'fun rememberModalBottomSheetState(skipPartiallyExpanded: Boolean = ..., confirmValueChange: (SheetValue) -> Boolean = ...): SheetState' is deprecated. Use rememberBottomSheetState with Hidden initial value. -w: file:///home/dae/VZ_Dev/work/PixelPlayer/app/src/main/java/com/theveloper/pixelplay/presentation/components/ReorderTabsSheet.kt:124:41 'static field FLAG_IGNORE_GLOBAL_SETTING: Int' is deprecated. Deprecated in Java. -w: file:///home/dae/VZ_Dev/work/PixelPlayer/app/src/main/java/com/theveloper/pixelplay/presentation/components/RoundedParallaxCarousell.kt:578:59 Redundant call of conversion method. -w: file:///home/dae/VZ_Dev/work/PixelPlayer/app/src/main/java/com/theveloper/pixelplay/presentation/components/RoundedParallaxCarousell.kt:578:82 Redundant call of conversion method. -w: file:///home/dae/VZ_Dev/work/PixelPlayer/app/src/main/java/com/theveloper/pixelplay/presentation/components/SongInfoBottomSheet.kt:332:22 'fun rememberModalBottomSheetState(skipPartiallyExpanded: Boolean = ..., confirmValueChange: (SheetValue) -> Boolean = ...): SheetState' is deprecated. Use rememberBottomSheetState with Hidden initial value. -w: file:///home/dae/VZ_Dev/work/PixelPlayer/app/src/main/java/com/theveloper/pixelplay/presentation/components/SongInfoBottomSheet.kt:777:61 'fun rememberModalBottomSheetState(skipPartiallyExpanded: Boolean = ..., confirmValueChange: (SheetValue) -> Boolean = ...): SheetState' is deprecated. Use rememberBottomSheetState with Hidden initial value. -w: file:///home/dae/VZ_Dev/work/PixelPlayer/app/src/main/java/com/theveloper/pixelplay/presentation/components/SongPickerBottomSheet.kt:107:22 'fun rememberModalBottomSheetState(skipPartiallyExpanded: Boolean = ..., confirmValueChange: (SheetValue) -> Boolean = ...): SheetState' is deprecated. Use rememberBottomSheetState with Hidden initial value. -w: file:///home/dae/VZ_Dev/work/PixelPlayer/app/src/main/java/com/theveloper/pixelplay/presentation/components/StreamingProviderSheet.kt:52:30 'fun rememberModalBottomSheetState(skipPartiallyExpanded: Boolean = ..., confirmValueChange: (SheetValue) -> Boolean = ...): SheetState' is deprecated. Use rememberBottomSheetState with Hidden initial value. -w: file:///home/dae/VZ_Dev/work/PixelPlayer/app/src/main/java/com/theveloper/pixelplay/presentation/components/TimerOptionsBottomSheet.kt:78:22 'fun rememberModalBottomSheetState(skipPartiallyExpanded: Boolean = ..., confirmValueChange: (SheetValue) -> Boolean = ...): SheetState' is deprecated. Use rememberBottomSheetState with Hidden initial value. -w: file:///home/dae/VZ_Dev/work/PixelPlayer/app/src/main/java/com/theveloper/pixelplay/presentation/components/player/FullPlayerContent.kt:983:34 'fun rememberModalBottomSheetState(skipPartiallyExpanded: Boolean = ..., confirmValueChange: (SheetValue) -> Boolean = ...): SheetState' is deprecated. Use rememberBottomSheetState with Hidden initial value. -w: file:///home/dae/VZ_Dev/work/PixelPlayer/app/src/main/java/com/theveloper/pixelplay/presentation/screens/DeviceCapabilitiesScreen.kt:307:28 'val LocalClipboardManager: ProvidableCompositionLocal' is deprecated. Use LocalClipboard instead which supports suspend functions. -w: file:///home/dae/VZ_Dev/work/PixelPlayer/app/src/main/java/com/theveloper/pixelplay/presentation/screens/HomeScreen.kt:255:22 'fun rememberModalBottomSheetState(skipPartiallyExpanded: Boolean = ..., confirmValueChange: (SheetValue) -> Boolean = ...): SheetState' is deprecated. Use rememberBottomSheetState with Hidden initial value. -w: file:///home/dae/VZ_Dev/work/PixelPlayer/app/src/main/java/com/theveloper/pixelplay/presentation/screens/HomeScreen.kt:256:26 'fun rememberModalBottomSheetState(skipPartiallyExpanded: Boolean = ..., confirmValueChange: (SheetValue) -> Boolean = ...): SheetState' is deprecated. Use rememberBottomSheetState with Hidden initial value. -w: file:///home/dae/VZ_Dev/work/PixelPlayer/app/src/main/java/com/theveloper/pixelplay/presentation/screens/LibraryMediaTabs.kt:223:39 No cast needed. -w: file:///home/dae/VZ_Dev/work/PixelPlayer/app/src/main/java/com/theveloper/pixelplay/presentation/screens/LibraryMediaTabs.kt:534:39 No cast needed. -w: file:///home/dae/VZ_Dev/work/PixelPlayer/app/src/main/java/com/theveloper/pixelplay/presentation/screens/LibraryScreen.kt:2657:22 'fun rememberModalBottomSheetState(skipPartiallyExpanded: Boolean = ..., confirmValueChange: (SheetValue) -> Boolean = ...): SheetState' is deprecated. Use rememberBottomSheetState with Hidden initial value. -w: file:///home/dae/VZ_Dev/work/PixelPlayer/app/src/main/java/com/theveloper/pixelplay/presentation/screens/LibrarySongsTab.kt:225:39 No cast needed. -w: file:///home/dae/VZ_Dev/work/PixelPlayer/app/src/main/java/com/theveloper/pixelplay/presentation/screens/MashupScreen.kt:69:22 'fun rememberModalBottomSheetState(skipPartiallyExpanded: Boolean = ..., confirmValueChange: (SheetValue) -> Boolean = ...): SheetState' is deprecated. Use rememberBottomSheetState with Hidden initial value. -w: file:///home/dae/VZ_Dev/work/PixelPlayer/app/src/main/java/com/theveloper/pixelplay/presentation/screens/PlaylistDetailScreen.kt:818:26 'fun rememberModalBottomSheetState(skipPartiallyExpanded: Boolean = ..., confirmValueChange: (SheetValue) -> Boolean = ...): SheetState' is deprecated. Use rememberBottomSheetState with Hidden initial value. -w: file:///home/dae/VZ_Dev/work/PixelPlayer/app/src/main/java/com/theveloper/pixelplay/presentation/screens/SettingsCategoryScreen.kt:297:39 'fun rememberModalBottomSheetState(skipPartiallyExpanded: Boolean = ..., confirmValueChange: (SheetValue) -> Boolean = ...): SheetState' is deprecated. Use rememberBottomSheetState with Hidden initial value. -w: file:///home/dae/VZ_Dev/work/PixelPlayer/app/src/main/java/com/theveloper/pixelplay/presentation/screens/StatsScreen.kt:220:13 Expression is unused. -w: file:///home/dae/VZ_Dev/work/PixelPlayer/app/src/main/java/com/theveloper/pixelplay/presentation/telegram/channel/TelegramChannelSearchSheet.kt:78:22 'fun rememberModalBottomSheetState(skipPartiallyExpanded: Boolean = ..., confirmValueChange: (SheetValue) -> Boolean = ...): SheetState' is deprecated. Use rememberBottomSheetState with Hidden initial value. -w: file:///home/dae/VZ_Dev/work/PixelPlayer/app/src/main/java/com/theveloper/pixelplay/presentation/telegram/dashboard/TelegramDashboardScreen.kt:706:22 'fun rememberModalBottomSheetState(skipPartiallyExpanded: Boolean = ..., confirmValueChange: (SheetValue) -> Boolean = ...): SheetState' is deprecated. Use rememberBottomSheetState with Hidden initial value. -w: file:///home/dae/VZ_Dev/work/PixelPlayer/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/AccountsViewModel.kt:117:67 No cast needed. -w: file:///home/dae/VZ_Dev/work/PixelPlayer/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/AccountsViewModel.kt:118:62 No cast needed. -w: file:///home/dae/VZ_Dev/work/PixelPlayer/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/AccountsViewModel.kt:119:66 No cast needed. -w: file:///home/dae/VZ_Dev/work/PixelPlayer/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/AccountsViewModel.kt:120:56 No cast needed. -w: file:///home/dae/VZ_Dev/work/PixelPlayer/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/AccountsViewModel.kt:121:70 No cast needed. -w: file:///home/dae/VZ_Dev/work/PixelPlayer/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/AccountsViewModel.kt:122:68 No cast needed. -w: file:///home/dae/VZ_Dev/work/PixelPlayer/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/CastStateHolder.kt:199:22 This declaration overrides a deprecated member but is not marked as deprecated itself. Add the '@Deprecated' annotation or suppress the diagnostic. -w: file:///home/dae/VZ_Dev/work/PixelPlayer/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/CastTransferStateHolder.kt:1036:34 Unnecessary safe call on a non-null receiver of type 'HttpURLConnection'. -w: file:///home/dae/VZ_Dev/work/PixelPlayer/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/ConnectivityStateHolder.kt:334:49 Unnecessary safe call on a non-null receiver of type 'String'. -w: file:///home/dae/VZ_Dev/work/PixelPlayer/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/ConnectivityStateHolder.kt:509:33 'val connectionInfo: WifiInfo!' is deprecated. Deprecated in Java. -w: file:///home/dae/VZ_Dev/work/PixelPlayer/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/GenreDetailViewModel.kt:161:61 Elvis operator (?:) always returns the left operand of non-nullable type 'String'. -w: file:///home/dae/VZ_Dev/work/PixelPlayer/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/GenreDetailViewModel.kt:198:82 Elvis operator (?:) always returns the left operand of non-nullable type 'String'. -w: file:///home/dae/VZ_Dev/work/PixelPlayer/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/GenreDetailViewModel.kt:199:80 Elvis operator (?:) always returns the left operand of non-nullable type 'String'. -w: file:///home/dae/VZ_Dev/work/PixelPlayer/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/GenreDetailViewModel.kt:281:57 Elvis operator (?:) always returns the left operand of non-nullable type 'String'. -w: file:///home/dae/VZ_Dev/work/PixelPlayer/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/GenreDetailViewModel.kt:282:58 Elvis operator (?:) always returns the left operand of non-nullable type 'String'. -w: file:///home/dae/VZ_Dev/work/PixelPlayer/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/GenreDetailViewModel.kt:284:65 Elvis operator (?:) always returns the left operand of non-nullable type 'String'. -w: file:///home/dae/VZ_Dev/work/PixelPlayer/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/GenreDetailViewModel.kt:296:56 Elvis operator (?:) always returns the left operand of non-nullable type 'String'. -w: file:///home/dae/VZ_Dev/work/PixelPlayer/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/GenreDetailViewModel.kt:297:57 Elvis operator (?:) always returns the left operand of non-nullable type 'String'. -w: file:///home/dae/VZ_Dev/work/PixelPlayer/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/PlaybackStateHolder.kt:701:45 Unnecessary safe call on a non-null receiver of type 'Song'. -w: file:///home/dae/VZ_Dev/work/PixelPlayer/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/PlayerViewModel.kt:153:40 'fun set(index: Int, element: Song): PersistentList' is deprecated. Use replacingAt() instead. For more details, read the documentation for this function. -w: file:///home/dae/VZ_Dev/work/PixelPlayer/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/PlayerViewModel.kt:159:40 'fun removeAt(index: Int): PersistentList' is deprecated. Use removingAt() instead. For more details, read the documentation for this function. -w: file:///home/dae/VZ_Dev/work/PixelPlayer/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/PlayerViewModel.kt:166:10 'fun removeAt(index: Int): PersistentList' is deprecated. Use removingAt() instead. For more details, read the documentation for this function. -w: file:///home/dae/VZ_Dev/work/PixelPlayer/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/PlayerViewModel.kt:167:10 'fun add(index: Int, element: Song): PersistentList' is deprecated. Use addingAt() instead. For more details, read the documentation for this function. - -[Incubating] Problems report is available at: file:///home/dae/VZ_Dev/work/PixelPlayer/build/reports/problems/problems-report.html - -BUILD SUCCESSFUL in 10m 43s -115 actionable tasks: 114 executed, 1 up-to-date -Configuration cache entry stored. From ea72d884dfac3bb140aa47484243f9b84ffb520e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 26 Jun 2026 22:28:43 +0000 Subject: [PATCH 13/73] chore(deps): bump the gradle-dependencies group across 1 directory with 7 updates Bumps the gradle-dependencies group with 7 updates in the / directory: | Package | From | To | | --- | --- | --- | | [gradle-wrapper](https://github.com/gradle/gradle) | `9.6.0` | `9.6.1` | | [com.google.dagger:hilt-android](https://github.com/google/dagger) | `2.59.2` | `2.60` | | [com.google.dagger:hilt-android-compiler](https://github.com/google/dagger) | `2.59.2` | `2.60` | | [io.ktor:ktor-server-core-jvm](https://github.com/ktorio/ktor) | `3.5.0` | `3.5.1` | | [io.ktor:ktor-server-netty-jvm](https://github.com/ktorio/ktor) | `3.5.0` | `3.5.1` | | [io.ktor:ktor-server-cio-jvm](https://github.com/ktorio/ktor) | `3.5.0` | `3.5.1` | | [com.google.dagger.hilt.android](https://github.com/google/dagger) | `2.59.2` | `2.60` | Updates `gradle-wrapper` from 9.6.0 to 9.6.1 - [Release notes](https://github.com/gradle/gradle/releases) - [Commits](https://github.com/gradle/gradle/compare/v9.6.0...v9.6.1) Updates `com.google.dagger:hilt-android` from 2.59.2 to 2.60 - [Release notes](https://github.com/google/dagger/releases) - [Changelog](https://github.com/google/dagger/blob/master/CHANGELOG.md) - [Commits](https://github.com/google/dagger/compare/dagger-2.59.2...dagger-2.60) Updates `com.google.dagger:hilt-android-compiler` from 2.59.2 to 2.60 - [Release notes](https://github.com/google/dagger/releases) - [Changelog](https://github.com/google/dagger/blob/master/CHANGELOG.md) - [Commits](https://github.com/google/dagger/compare/dagger-2.59.2...dagger-2.60) Updates `com.google.dagger:hilt-android-compiler` from 2.59.2 to 2.60 - [Release notes](https://github.com/google/dagger/releases) - [Changelog](https://github.com/google/dagger/blob/master/CHANGELOG.md) - [Commits](https://github.com/google/dagger/compare/dagger-2.59.2...dagger-2.60) Updates `io.ktor:ktor-server-core-jvm` from 3.5.0 to 3.5.1 - [Release notes](https://github.com/ktorio/ktor/releases) - [Changelog](https://github.com/ktorio/ktor/blob/main/CHANGELOG.md) - [Commits](https://github.com/ktorio/ktor/compare/3.5.0...3.5.1) Updates `io.ktor:ktor-server-netty-jvm` from 3.5.0 to 3.5.1 - [Release notes](https://github.com/ktorio/ktor/releases) - [Changelog](https://github.com/ktorio/ktor/blob/main/CHANGELOG.md) - [Commits](https://github.com/ktorio/ktor/compare/3.5.0...3.5.1) Updates `io.ktor:ktor-server-cio-jvm` from 3.5.0 to 3.5.1 - [Release notes](https://github.com/ktorio/ktor/releases) - [Changelog](https://github.com/ktorio/ktor/blob/main/CHANGELOG.md) - [Commits](https://github.com/ktorio/ktor/compare/3.5.0...3.5.1) Updates `io.ktor:ktor-server-netty-jvm` from 3.5.0 to 3.5.1 - [Release notes](https://github.com/ktorio/ktor/releases) - [Changelog](https://github.com/ktorio/ktor/blob/main/CHANGELOG.md) - [Commits](https://github.com/ktorio/ktor/compare/3.5.0...3.5.1) Updates `io.ktor:ktor-server-cio-jvm` from 3.5.0 to 3.5.1 - [Release notes](https://github.com/ktorio/ktor/releases) - [Changelog](https://github.com/ktorio/ktor/blob/main/CHANGELOG.md) - [Commits](https://github.com/ktorio/ktor/compare/3.5.0...3.5.1) Updates `com.google.dagger.hilt.android` from 2.59.2 to 2.60 - [Release notes](https://github.com/google/dagger/releases) - [Changelog](https://github.com/google/dagger/blob/master/CHANGELOG.md) - [Commits](https://github.com/google/dagger/compare/dagger-2.59.2...dagger-2.60) --- updated-dependencies: - dependency-name: gradle-wrapper dependency-version: 9.6.1 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: gradle-dependencies - dependency-name: com.google.dagger:hilt-android dependency-version: '2.60' dependency-type: direct:production update-type: version-update:semver-minor dependency-group: gradle-dependencies - dependency-name: com.google.dagger:hilt-android-compiler dependency-version: '2.60' dependency-type: direct:production update-type: version-update:semver-minor dependency-group: gradle-dependencies - dependency-name: com.google.dagger:hilt-android-compiler dependency-version: '2.60' dependency-type: direct:production update-type: version-update:semver-minor dependency-group: gradle-dependencies - dependency-name: io.ktor:ktor-server-core-jvm dependency-version: 3.5.1 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: gradle-dependencies - dependency-name: io.ktor:ktor-server-netty-jvm dependency-version: 3.5.1 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: gradle-dependencies - dependency-name: io.ktor:ktor-server-cio-jvm dependency-version: 3.5.1 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: gradle-dependencies - dependency-name: io.ktor:ktor-server-netty-jvm dependency-version: 3.5.1 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: gradle-dependencies - dependency-name: io.ktor:ktor-server-cio-jvm dependency-version: 3.5.1 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: gradle-dependencies - dependency-name: com.google.dagger.hilt.android dependency-version: '2.60' dependency-type: direct:production update-type: version-update:semver-minor dependency-group: gradle-dependencies ... Signed-off-by: dependabot[bot] --- gradle/libs.versions.toml | 6 +++--- gradle/wrapper/gradle-wrapper.properties | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index c1f463bff3..ec90601c7b 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -19,9 +19,9 @@ foundation = "1.11.3" glance = "1.3.0-alpha01" graphicsShapes = "1.1.0" gson = "2.14.0" -hiltAndroid = "2.59.2" +hiltAndroid = "2.60" hiltNavigationCompose = "1.3.0" -ktor = "3.5.0" +ktor = "3.5.1" kotlin = "2.4.0" coreKtx = "1.19.0" junit = "4.13.2" @@ -71,7 +71,7 @@ jose4j = "0.9.6" httpclient = "4.5.14" # DI -dagger = "2.59.2" +dagger = "2.60" javax-inject = "1" # Annotations procesing diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index dcf577fe35..59f71f7b1d 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,7 +1,7 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionSha256Sum=bbaeb2fef8710818cf0e261201dab964c572f92b942812df0c3620d62a529a01 -distributionUrl=https\://services.gradle.org/distributions/gradle-9.6.0-bin.zip +distributionSha256Sum=9c0f7faeeb306cb14e4279a3e084ca6b596894089a0638e68a07c945a32c9e14 +distributionUrl=https\://services.gradle.org/distributions/gradle-9.6.1-bin.zip networkTimeout=10000 retries=0 retryBackOffMs=500 From 66be53cc21f3d18aa3e49faa91b37e0daf359663 Mon Sep 17 00:00:00 2001 From: VoidX3D Date: Sun, 28 Jun 2026 21:39:46 +0545 Subject: [PATCH 14/73] perf: defer legacy migrations and non-critical loads out of init hot path - Merge migrateTabOrder, ensureLibrarySortDefaults, and legacy favorite migration into a single deferred coroutine (2s delay) - Defer loadPersistedDailyMix and loadSearchHistory to after first frame (1.5s delay) - This prevents DataStore first() calls and DB queries from blocking the critical init path that feeds first-frame rendering --- .../presentation/viewmodel/PlayerViewModel.kt | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/PlayerViewModel.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/PlayerViewModel.kt index 4ddadbedea..2936fa01f1 100644 --- a/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/PlayerViewModel.kt +++ b/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/PlayerViewModel.kt @@ -1527,15 +1527,11 @@ class PlayerViewModel @Inject constructor( + // Deferred: legacy migrations run after init to avoid blocking first render viewModelScope.launch { + kotlinx.coroutines.delay(2000) userPreferencesRepository.migrateTabOrder() - } - - viewModelScope.launch { userPreferencesRepository.ensureLibrarySortDefaults() - } - - viewModelScope.launch { val legacyFavoriteIds = userPreferencesRepository.favoriteSongIdsFlow.first() if (legacyFavoriteIds.isNotEmpty()) { val roomFavoriteIds = musicRepository.getFavoriteSongIdsOnce() @@ -1677,8 +1673,12 @@ class PlayerViewModel @Inject constructor( // launchColorSchemeProcessor() - Handled by ThemeStateHolder and on-demand calls - loadPersistedDailyMix() - loadSearchHistory() + // Deferred: daily mix and search history load after first frame + viewModelScope.launch { + kotlinx.coroutines.delay(1500) + loadPersistedDailyMix() + loadSearchHistory() + } viewModelScope.launch { isSyncingStateFlow.collect { isSyncing -> From b4689a63ba2ef5493276c58add5dd5cd78f14fcc Mon Sep 17 00:00:00 2001 From: VoidX3D Date: Sun, 28 Jun 2026 21:41:20 +0545 Subject: [PATCH 15/73] perf: serialize library data loading to avoid I/O contention on slow storage - Load songs first (most critical tab, needed for Songs tab rendering) - Chain albums after songs, artists after albums, folders after artists using Job.join() to prevent 4 simultaneous Room queries from competing for limited I/O bandwidth on eMMC storage (Redmi) - Reduces GC pressure from concurrent heap allocations --- .../presentation/viewmodel/LibraryStateHolder.kt | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/LibraryStateHolder.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/LibraryStateHolder.kt index 22fb7d311f..dbafa307b2 100644 --- a/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/LibraryStateHolder.kt +++ b/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/LibraryStateHolder.kt @@ -257,22 +257,21 @@ class LibraryStateHolder @Inject constructor( songsJob = scope?.launch { _isLoadingLibrary.value = true musicRepository.getAudioFiles().conflate().collect { songs -> - // Process heavy list conversions on Default dispatcher to avoid blocking UI val immutableSongs = withContext(Dispatchers.Default) { songs.toImmutableList() } val songsMap = withContext(Dispatchers.Default) { songs.associateBy { it.id } } _allSongs.value = immutableSongs _allSongsById.value = songsMap - // When the repository emits a new list (triggered by directory changes), - // we update our state and re-apply current sorting. - // Apply sort to the new data sortSongs(_currentSongSortOption.value, persist = false) _isLoadingLibrary.value = false } } + // Albums, artists, and folders load sequentially after songs to avoid + // I/O contention on slower storage (eMMC on Redmi vs UFS on Samsung). albumsJob = scope?.launch { + songsJob?.join() _isLoadingCategories.value = true @OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class) kotlinx.coroutines.flow.combine( @@ -292,6 +291,7 @@ class LibraryStateHolder @Inject constructor( } artistsJob = scope?.launch { + albumsJob?.join() _isLoadingCategories.value = true @OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class) effectiveStorageFilter.flatMapLatest { filter -> @@ -306,6 +306,7 @@ class LibraryStateHolder @Inject constructor( } foldersJob = scope?.launch { + artistsJob?.join() @OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class) effectiveStorageFilter.flatMapLatest { filter -> musicRepository.getMusicFolders(effectiveFoldersStorageFilter(filter)) From 37ef4d2a0d106649497dafeab742eddc1ed7de7c Mon Sep 17 00:00:00 2001 From: VoidX3D Date: Sun, 28 Jun 2026 21:42:30 +0545 Subject: [PATCH 16/73] perf: derive isLibraryContentEmpty from in-memory state instead of Room queries - Replace songCountFlow (Room query) with allSongsFlow (in-memory) - Now all 3 flows in the combine are in-memory StateFlows, not Room queries - Eliminates redundant SELECT COUNT(*) query that re-emitted on every songs table write during sync --- .../pixelplay/presentation/screens/LibraryScreen.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/screens/LibraryScreen.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/screens/LibraryScreen.kt index 5d37d029e9..3c9a6590a3 100644 --- a/app/src/main/java/com/theveloper/pixelplay/presentation/screens/LibraryScreen.kt +++ b/app/src/main/java/com/theveloper/pixelplay/presentation/screens/LibraryScreen.kt @@ -1002,11 +1002,11 @@ fun LibraryScreen( }.collectAsStateWithLifecycle(initialValue = LibraryScreenPlayerProjection()) val isLibraryContentEmpty by remember(playerViewModel) { combine( - playerViewModel.songCountFlow, + playerViewModel.allSongsFlow, playerViewModel.albumsFlow, playerViewModel.artistsFlow - ) { songCount, albums, artists -> - songCount == 0 && albums.isEmpty() && artists.isEmpty() + ) { songs, albums, artists -> + songs.isEmpty() && albums.isEmpty() && artists.isEmpty() }.distinctUntilChanged() }.collectAsStateWithLifecycle(initialValue = true) From 7b9407de5da42e0b8fa6102b9ca878be2a973828 Mon Sep 17 00:00:00 2001 From: VoidX3D Date: Sun, 28 Jun 2026 21:43:07 +0545 Subject: [PATCH 17/73] perf: eliminate loading state flapping during sequential library load - Remove individual _isLoadingCategories toggles from albums and artists jobs (they no longer manage their own loading state) - Add a single watcher coroutine that sets _isLoadingCategories = true when songs complete, then false when all sequential jobs finish - Prevents 3 redundant recompositions of LibraryScreen through playerUiState during initial data load --- .../presentation/viewmodel/LibraryStateHolder.kt | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/LibraryStateHolder.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/LibraryStateHolder.kt index dbafa307b2..3515c7de8a 100644 --- a/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/LibraryStateHolder.kt +++ b/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/LibraryStateHolder.kt @@ -272,7 +272,6 @@ class LibraryStateHolder @Inject constructor( // I/O contention on slower storage (eMMC on Redmi vs UFS on Samsung). albumsJob = scope?.launch { songsJob?.join() - _isLoadingCategories.value = true @OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class) kotlinx.coroutines.flow.combine( effectiveStorageFilter, @@ -286,13 +285,11 @@ class LibraryStateHolder @Inject constructor( sortAlbumsList(albums, _currentAlbumSortOption.value).toImmutableList() } _albums.value = sortedAlbums - _isLoadingCategories.value = false } } artistsJob = scope?.launch { albumsJob?.join() - _isLoadingCategories.value = true @OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class) effectiveStorageFilter.flatMapLatest { filter -> musicRepository.getArtists(filter) @@ -301,7 +298,6 @@ class LibraryStateHolder @Inject constructor( sortArtistsList(artists, _currentArtistSortOption.value).toImmutableList() } _artists.value = sortedArtists - _isLoadingCategories.value = false } } @@ -317,6 +313,16 @@ class LibraryStateHolder @Inject constructor( _musicFolders.value = sortedFolders } } + + // Single loading state: true when songs start, false when all sequential + // jobs (albums + artists) finish. This avoids flapping true→false→true + // as each sequential coroutine completes. + scope?.launch { + songsJob?.join() + _isLoadingCategories.value = true + foldersJob?.join() + _isLoadingCategories.value = false + } } // Deprecated imperative loaders - redirected to observer start From 898f7564b8f2a3e43b0dbbf1eb359b654ad91c22 Mon Sep 17 00:00:00 2001 From: VoidX3D Date: Sun, 28 Jun 2026 21:43:46 +0545 Subject: [PATCH 18/73] perf: add distinctUntilChanged to playerUiState combine collectors - Library folders/loading combine: skip update when Triple unchanged - Sort options combine: skip update when SortOptionsSnapshot unchanged - AiUiSnapshot combine: skip update when snapshot unchanged - Prevents cascading recompositions of LibraryScreen from redundant playerUiState emissions during multiple rapid state changes --- .../pixelplay/presentation/viewmodel/PlayerViewModel.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/PlayerViewModel.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/PlayerViewModel.kt index 2936fa01f1..a08f4727ac 100644 --- a/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/PlayerViewModel.kt +++ b/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/PlayerViewModel.kt @@ -1797,7 +1797,7 @@ class PlayerViewModel @Inject constructor( aiStatus = status, aiError = error ) - }.collect { snapshot -> + }.distinctUntilChanged().collect { snapshot -> _playerUiState.update { it.copy( showAiPlaylistSheet = snapshot.showAiPlaylistSheet, @@ -1820,7 +1820,7 @@ class PlayerViewModel @Inject constructor( libraryStateHolder.isLoadingCategories, ) { folders, loadingLibrary, loadingCategories -> Triple(folders, loadingLibrary, loadingCategories) - }.collect { (folders, loadingLibrary, loadingCategories) -> + }.distinctUntilChanged().collect { (folders, loadingLibrary, loadingCategories) -> _playerUiState.update { it.copy( musicFolders = folders, @@ -1841,7 +1841,7 @@ class PlayerViewModel @Inject constructor( libraryStateHolder.currentFavoriteSortOption, ) { songSort, albumSort, artistSort, folderSort, favoriteSort -> SortOptionsSnapshot(songSort, albumSort, artistSort, folderSort, favoriteSort) - }.collect { snapshot -> + }.distinctUntilChanged().collect { snapshot -> _playerUiState.update { it.copy( currentSongSortOption = snapshot.songSort, From 80fb3bf67e6c77fba11e51a7b1c1ce1449d3a484 Mon Sep 17 00:00:00 2001 From: VoidX3D Date: Sun, 28 Jun 2026 21:48:09 +0545 Subject: [PATCH 19/73] feat(ai): add LYRICS prompt type for proper logging of translation requests - Add LYRICS to AiSystemPromptType enum - Add LYRICS case to AiSystemPromptEngine.buildPrompt with role/strategy - Update translateLyrics in AiStateHolder to use AiSystemPromptType.LYRICS - Add 'Lyrics' display label to formatPromptType in AiUsageComponents - Lyrics translation requests now appear distinctly in AI activity logs --- .../pixelplay/data/ai/AiSystemPromptEngine.kt | 11 +++++++++++ .../presentation/screens/AiUsageComponents.kt | 1 + .../pixelplay/presentation/viewmodel/AiStateHolder.kt | 8 ++++---- 3 files changed, 16 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/theveloper/pixelplay/data/ai/AiSystemPromptEngine.kt b/app/src/main/java/com/theveloper/pixelplay/data/ai/AiSystemPromptEngine.kt index 9edf9e4042..a103af1f07 100644 --- a/app/src/main/java/com/theveloper/pixelplay/data/ai/AiSystemPromptEngine.kt +++ b/app/src/main/java/com/theveloper/pixelplay/data/ai/AiSystemPromptEngine.kt @@ -11,6 +11,7 @@ enum class AiSystemPromptType { MOOD_ANALYSIS, PERSONA, DAILY_MIX, + LYRICS, GENERAL } @@ -182,6 +183,16 @@ class AiSystemPromptEngine @Inject constructor() { $dailyMixPersonaPrompt """.trimIndent() + AiSystemPromptType.LYRICS -> """ + Song lyrics translator — you translate lyrics between languages while preserving structure. + + - Preserve ALL timestamps and line structure exactly. + - Output each original line followed by its translation on the next line. + - Never add explanations, labels, or formatting beyond the requested format. + - If the source is already in the target language, respond with: ALREADY_IN_TARGET_LANGUAGE + + """.trimIndent() + AiSystemPromptType.GENERAL -> """ PixelPlayer Assistant — a knowledgeable music companion. diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/screens/AiUsageComponents.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/screens/AiUsageComponents.kt index 8c9a9b17fa..7f7b9b4fa9 100644 --- a/app/src/main/java/com/theveloper/pixelplay/presentation/screens/AiUsageComponents.kt +++ b/app/src/main/java/com/theveloper/pixelplay/presentation/screens/AiUsageComponents.kt @@ -178,6 +178,7 @@ private fun formatPromptType(type: String): String { "MOOD_ANALYSIS" -> "Analysis" "PERSONA" -> "Persona" "DAILY_MIX" -> "Daily Mix" + "LYRICS" -> "Lyrics" "GENERAL" -> "General" else -> type.lowercase().replaceFirstChar { it.uppercase() } } diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/AiStateHolder.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/AiStateHolder.kt index ad68cea838..2f15469893 100644 --- a/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/AiStateHolder.kt +++ b/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/AiStateHolder.kt @@ -287,7 +287,7 @@ class AiStateHolder @Inject constructor( suspend fun translateLyrics(lyricsText: String): Result { return try { val targetLanguage = context.resources.configuration.locales[0].displayLanguage - val prompt = """ + val xmlPrompt = """ Translate song lyrics into $targetLanguage. @@ -307,10 +307,10 @@ class AiStateHolder @Inject constructor( $lyricsText """.trimIndent() - + val response = aiHandler.generateContent( - prompt = prompt, - type = AiSystemPromptType.GENERAL, + prompt = xmlPrompt, + type = AiSystemPromptType.LYRICS, temperature = 0.1f ) Result.success(response) From d799cd9e8e64c1f4cd99083f18d4cc19dd7263d2 Mon Sep 17 00:00:00 2001 From: VoidX3D Date: Sun, 28 Jun 2026 21:50:38 +0545 Subject: [PATCH 20/73] feat(settings): split AI settings into AI Provider tab and Generation Parameters tab - Add GENERATION_PARAMETERS category to SettingsCategory enum with Tune icon - AI Integration tab now keeps only: provider selection, API key entry, model selection, base URL, and activity logs - New Generation Parameters tab gets: system prompt editor, temperature, top P, top K, max tokens, presence/frequency penalty, sample size, digest mode, and extended song fields - Add English string resources for the new category --- .../presentation/model/SettingsCategory.kt | 7 + .../screens/SettingsCategoryScreen.kt | 203 +++++++++--------- app/src/main/res/values/strings_settings.xml | 4 +- 3 files changed, 113 insertions(+), 101 deletions(-) diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/model/SettingsCategory.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/model/SettingsCategory.kt index e59c3d7a8c..423863c735 100644 --- a/app/src/main/java/com/theveloper/pixelplay/presentation/model/SettingsCategory.kt +++ b/app/src/main/java/com/theveloper/pixelplay/presentation/model/SettingsCategory.kt @@ -9,6 +9,7 @@ import androidx.compose.material.icons.rounded.Info import androidx.compose.material.icons.rounded.LibraryMusic import androidx.compose.material.icons.rounded.MusicNote import androidx.compose.material.icons.rounded.Palette +import androidx.compose.material.icons.rounded.Tune import androidx.compose.ui.graphics.vector.ImageVector import com.theveloper.pixelplay.R @@ -49,6 +50,12 @@ enum class SettingsCategory( subtitleRes = R.string.settings_category_ai_subtitle, iconRes = R.drawable.gemini_ai ), + GENERATION_PARAMETERS( + id = "generation_parameters", + titleRes = R.string.settings_category_generation_parameters_title, + subtitleRes = R.string.settings_category_generation_parameters_subtitle, + icon = Icons.Rounded.Tune + ), BACKUP_RESTORE( id = "backup_restore", titleRes = R.string.settings_category_backup_title, diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/screens/SettingsCategoryScreen.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/screens/SettingsCategoryScreen.kt index dd1672dbc6..1e6c262b0e 100644 --- a/app/src/main/java/com/theveloper/pixelplay/presentation/screens/SettingsCategoryScreen.kt +++ b/app/src/main/java/com/theveloper/pixelplay/presentation/screens/SettingsCategoryScreen.kt @@ -1037,6 +1037,107 @@ fun SettingsCategoryScreen( } } + Spacer(modifier = Modifier.height(16.dp)) + + SettingsSubsection(title = stringResource(R.string.settings_ai_usage_report_section)) { + val recentAiUsage by settingsViewModel.recentAiUsage.collectAsStateWithLifecycle() + val totalPromptTokens by settingsViewModel.totalPromptTokens.collectAsStateWithLifecycle() + val totalOutputTokens by settingsViewModel.totalOutputTokens.collectAsStateWithLifecycle() + val totalThoughtTokens by settingsViewModel.totalThoughtTokens.collectAsStateWithLifecycle() + + val totalTokens = totalPromptTokens + totalOutputTokens + totalThoughtTokens + val totalTokStr = String.format(Locale.US, "%,d", totalTokens) + val promptTokStr = String.format(Locale.US, "%,d", totalPromptTokens) + val outputTokStr = String.format(Locale.US, "%,d", totalOutputTokens) + val thoughtTokStr = String.format(Locale.US, "%,d", totalThoughtTokens) + + ActionSettingsItem( + title = stringResource(R.string.settings_total_consumption_title), + subtitle = stringResource( + R.string.settings_ai_usage_tokens_subtitle, + totalTokStr, + promptTokStr, + outputTokStr, + thoughtTokStr + ), + icon = { + Icon( + painter = painterResource(R.drawable.rounded_monitoring_24), + contentDescription = null, + tint = MaterialTheme.colorScheme.tertiary + ) + }, + primaryActionLabel = stringResource(R.string.settings_ai_clear_logs), + onPrimaryAction = { settingsViewModel.clearAiUsageData() } + ) + + if (recentAiUsage.isNotEmpty()) { + Spacer(modifier = Modifier.height(12.dp)) + var expanded by remember { mutableStateOf(false) } + val rotation by animateFloatAsState(targetValue = if (expanded) 180f else 0f) + + Surface( + modifier = Modifier + .fillMaxWidth() + .clickable { expanded = !expanded }, + color = Color.Transparent + ) { + Row( + modifier = Modifier.padding(vertical = 8.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + Row(verticalAlignment = Alignment.CenterVertically) { + Icon( + painter = painterResource(R.drawable.rounded_monitoring_24), + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.size(20.dp) + ) + Spacer(modifier = Modifier.width(12.dp)) + Text( + text = stringResource(R.string.settings_ai_activity_log_title, recentAiUsage.size), + style = MaterialTheme.typography.titleMedium.copy(fontFamily = GoogleSansRounded), + color = MaterialTheme.colorScheme.onSurface + ) + } + Icon( + imageVector = Icons.Rounded.ExpandMore, + contentDescription = if (expanded) stringResource(R.string.settings_ai_hide_logs) else stringResource(R.string.settings_ai_show_logs), + tint = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.rotate(rotation) + ) + } + } + + AnimatedVisibility( + visible = expanded, + enter = expandVertically() + fadeIn(), + exit = shrinkVertically() + fadeOut() + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(top = 8.dp, bottom = 8.dp) + ) { + val dateFormat = SimpleDateFormat("MMMM d, yyyy", Locale.getDefault()) + val groupedUsage = recentAiUsage.groupBy { + dateFormat.format(Date(it.timestamp)) + } + + groupedUsage.forEach { (date, items) -> + AiUsageDateHeader(date = date) + items.forEach { usage -> + AiUsageLogItem(usage = usage) + } + Spacer(modifier = Modifier.height(8.dp)) + } + } + } + } + } + } + SettingsCategory.GENERATION_PARAMETERS -> { // Prompt Behavior Section SettingsSubsection( title = stringResource(R.string.settings_prompt_behavior_section), @@ -1140,6 +1241,8 @@ fun SettingsCategoryScreen( ) } + Spacer(modifier = Modifier.height(16.dp)) + // Song Data Configuration Section SettingsSubsection(title = "Song Data Configuration") { val aiSampleSize by settingsViewModel.aiSampleSize.collectAsStateWithLifecycle() @@ -1185,106 +1288,6 @@ fun SettingsCategoryScreen( } ) } - - Spacer(modifier = Modifier.height(16.dp)) - - SettingsSubsection(title = stringResource(R.string.settings_ai_usage_report_section)) { - val recentAiUsage by settingsViewModel.recentAiUsage.collectAsStateWithLifecycle() - val totalPromptTokens by settingsViewModel.totalPromptTokens.collectAsStateWithLifecycle() - val totalOutputTokens by settingsViewModel.totalOutputTokens.collectAsStateWithLifecycle() - val totalThoughtTokens by settingsViewModel.totalThoughtTokens.collectAsStateWithLifecycle() - - val totalTokens = totalPromptTokens + totalOutputTokens + totalThoughtTokens - val totalTokStr = String.format(Locale.US, "%,d", totalTokens) - val promptTokStr = String.format(Locale.US, "%,d", totalPromptTokens) - val outputTokStr = String.format(Locale.US, "%,d", totalOutputTokens) - val thoughtTokStr = String.format(Locale.US, "%,d", totalThoughtTokens) - - ActionSettingsItem( - title = stringResource(R.string.settings_total_consumption_title), - subtitle = stringResource( - R.string.settings_ai_usage_tokens_subtitle, - totalTokStr, - promptTokStr, - outputTokStr, - thoughtTokStr - ), - icon = { - Icon( - painter = painterResource(R.drawable.rounded_monitoring_24), - contentDescription = null, - tint = MaterialTheme.colorScheme.tertiary - ) - }, - primaryActionLabel = stringResource(R.string.settings_ai_clear_logs), - onPrimaryAction = { settingsViewModel.clearAiUsageData() } - ) - - if (recentAiUsage.isNotEmpty()) { - Spacer(modifier = Modifier.height(12.dp)) - var expanded by remember { mutableStateOf(false) } - val rotation by animateFloatAsState(targetValue = if (expanded) 180f else 0f) - - Surface( - modifier = Modifier - .fillMaxWidth() - .clickable { expanded = !expanded }, - color = Color.Transparent - ) { - Row( - modifier = Modifier.padding(vertical = 8.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceBetween - ) { - Row(verticalAlignment = Alignment.CenterVertically) { - Icon( - painter = painterResource(R.drawable.rounded_monitoring_24), - contentDescription = null, - tint = MaterialTheme.colorScheme.onSurfaceVariant, - modifier = Modifier.size(20.dp) - ) - Spacer(modifier = Modifier.width(12.dp)) - Text( - text = stringResource(R.string.settings_ai_activity_log_title, recentAiUsage.size), - style = MaterialTheme.typography.titleMedium.copy(fontFamily = GoogleSansRounded), - color = MaterialTheme.colorScheme.onSurface - ) - } - Icon( - imageVector = Icons.Rounded.ExpandMore, - contentDescription = if (expanded) stringResource(R.string.settings_ai_hide_logs) else stringResource(R.string.settings_ai_show_logs), - tint = MaterialTheme.colorScheme.onSurfaceVariant, - modifier = Modifier.rotate(rotation) - ) - } - } - - AnimatedVisibility( - visible = expanded, - enter = expandVertically() + fadeIn(), - exit = shrinkVertically() + fadeOut() - ) { - Column( - modifier = Modifier - .fillMaxWidth() - .padding(top = 8.dp, bottom = 8.dp) - ) { - val dateFormat = SimpleDateFormat("MMMM d, yyyy", Locale.getDefault()) - val groupedUsage = recentAiUsage.groupBy { - dateFormat.format(Date(it.timestamp)) - } - - groupedUsage.forEach { (date, items) -> - AiUsageDateHeader(date = date) - items.forEach { usage -> - AiUsageLogItem(usage = usage) - } - Spacer(modifier = Modifier.height(8.dp)) - } - } - } - } - } } SettingsCategory.BACKUP_RESTORE -> { if (!uiState.backupInfoDismissed) { diff --git a/app/src/main/res/values/strings_settings.xml b/app/src/main/res/values/strings_settings.xml index ea36028155..d1820c6ab6 100644 --- a/app/src/main/res/values/strings_settings.xml +++ b/app/src/main/res/values/strings_settings.xml @@ -10,7 +10,9 @@ Behavior Gestures, haptics, and navigation behavior AI Integration (β) - AI providers, API keys, and model settings + AI providers, API keys, model selection, and activity logs + Generation Parameters + Temperature, sampling, prompts, and song data configuration Backup & Restore Export and recover your personal app data Developer Options From 08e941e3254382d6b6ff283230382d8bc2a58807 Mon Sep 17 00:00:00 2001 From: VoidX3D Date: Sun, 28 Jun 2026 21:52:07 +0545 Subject: [PATCH 21/73] feat(ai): add SearchableProviderSelector with descriptions and live search - Add description field to AiProvider enum entries - Create SearchableProviderSelector composable similar to SearchableModelSelector with search, filtering, and count label - Replace ThemeSelectorItem in AI_INTEGRATION settings with SearchableProviderSelector for provider selection - Search covers name, displayName, and description fields --- .../pixelplay/data/ai/provider/AiProvider.kt | 29 +-- .../screens/SettingsCategoryScreen.kt | 8 +- .../screens/SettingsComponents.kt | 177 ++++++++++++++++++ 3 files changed, 198 insertions(+), 16 deletions(-) diff --git a/app/src/main/java/com/theveloper/pixelplay/data/ai/provider/AiProvider.kt b/app/src/main/java/com/theveloper/pixelplay/data/ai/provider/AiProvider.kt index 229f1d3140..31c2a0d39f 100644 --- a/app/src/main/java/com/theveloper/pixelplay/data/ai/provider/AiProvider.kt +++ b/app/src/main/java/com/theveloper/pixelplay/data/ai/provider/AiProvider.kt @@ -3,18 +3,23 @@ package com.theveloper.pixelplay.data.ai.provider /** * Enum representing available AI providers */ -enum class AiProvider(val displayName: String, val requiresApiKey: Boolean, val hasConfigurableUrl: Boolean = false) { - GEMINI("Google Gemini", requiresApiKey = true), - DEEPSEEK("DeepSeek", requiresApiKey = true), - GROQ("Groq", requiresApiKey = true), - MISTRAL("Mistral", requiresApiKey = true), - NVIDIA("NVIDIA NIM", requiresApiKey = true), - KIMI("Kimi (Moonshot)", requiresApiKey = true), - GLM("Zhipu GLM", requiresApiKey = true), - OPENAI("OpenAI", requiresApiKey = true), - OPENROUTER("OpenRouter", requiresApiKey = true), - OLLAMA("Ollama", requiresApiKey = true), - CUSTOM("Custom Provider", requiresApiKey = true, hasConfigurableUrl = true); +enum class AiProvider( + val displayName: String, + val requiresApiKey: Boolean, + val hasConfigurableUrl: Boolean = false, + val description: String = "" +) { + GEMINI("Google Gemini", requiresApiKey = true, description = "Google's flagship AI models (Gemini 1.5/2.0)"), + DEEPSEEK("DeepSeek", requiresApiKey = true, description = "Open-source reasoning models via DeepSeek API"), + GROQ("Groq", requiresApiKey = true, description = "Ultra-fast inference with Groq LPU hardware"), + MISTRAL("Mistral", requiresApiKey = true, description = "Mistral AI's efficient and powerful models"), + NVIDIA("NVIDIA NIM", requiresApiKey = true, description = "NVIDIA's optimized inference microservices"), + KIMI("Kimi (Moonshot)", requiresApiKey = true, description = "Moonshot AI's long-context Kimi models"), + GLM("Zhipu GLM", requiresApiKey = true, description = "Zhipu AI's bilingual GLM series"), + OPENAI("OpenAI", requiresApiKey = true, description = "GPT-4o, GPT-4, and other OpenAI models"), + OPENROUTER("OpenRouter", requiresApiKey = true, description = "Unified access to 200+ models via OpenRouter"), + OLLAMA("Ollama", requiresApiKey = true, description = "Local models via Ollama (requires running server)"), + CUSTOM("Custom Provider", requiresApiKey = true, hasConfigurableUrl = true, description = "Any OpenAI-compatible API endpoint"); companion object { fun fromString(value: String): AiProvider { diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/screens/SettingsCategoryScreen.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/screens/SettingsCategoryScreen.kt index 1e6c262b0e..ad4810654e 100644 --- a/app/src/main/java/com/theveloper/pixelplay/presentation/screens/SettingsCategoryScreen.kt +++ b/app/src/main/java/com/theveloper/pixelplay/presentation/screens/SettingsCategoryScreen.kt @@ -921,12 +921,12 @@ fun SettingsCategoryScreen( // AI Provider Selection SettingsSubsection(title = stringResource(R.string.settings_ai_provider_section)) { - ThemeSelectorItem( + SearchableProviderSelector( label = stringResource(R.string.settings_ai_provider_title), description = stringResource(R.string.settings_ai_provider_subtitle), - options = com.theveloper.pixelplay.data.ai.provider.AiProvider.entries.associate { it.name to it.displayName }, - selectedKey = aiProvider, - onSelectionChanged = { settingsViewModel.onAiProviderChange(it) }, + providers = com.theveloper.pixelplay.data.ai.provider.AiProvider.entries, + selectedProvider = aiProvider, + onProviderSelected = { settingsViewModel.onAiProviderChange(it) }, leadingIcon = { Icon(Icons.Rounded.Science, null, tint = MaterialTheme.colorScheme.secondary) } ) SwitchSettingItem( diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/screens/SettingsComponents.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/screens/SettingsComponents.kt index bc5dbb3293..a4a656df42 100644 --- a/app/src/main/java/com/theveloper/pixelplay/presentation/screens/SettingsComponents.kt +++ b/app/src/main/java/com/theveloper/pixelplay/presentation/screens/SettingsComponents.kt @@ -551,6 +551,183 @@ fun SearchableModelSelector( } } +@Composable +fun SearchableProviderSelector( + label: String, + description: String, + providers: List, + selectedProvider: String, + onProviderSelected: (String) -> Unit, + leadingIcon: @Composable () -> Unit +) { + var showSheet by remember { mutableStateOf(false) } + var searchQuery by remember { mutableStateOf("") } + val selectedDisplayName = providers.find { it.name == selectedProvider }?.displayName + ?: selectedProvider + + Surface( + color = MaterialTheme.colorScheme.surfaceContainer, + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(10.dp)) + .clickable { showSheet = true } + ) { + Column(modifier = Modifier.padding(16.dp)) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.fillMaxWidth() + ) { + Box( + modifier = Modifier + .padding(end = 16.dp) + .size(24.dp), + contentAlignment = Alignment.Center + ) { leadingIcon() } + + Column(modifier = Modifier.weight(1f)) { + Text( + text = label, + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.onSurface + ) + Spacer(modifier = Modifier.height(6.dp)) + Text( + text = description, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + Spacer(modifier = Modifier.height(10.dp)) + Surface( + color = MaterialTheme.colorScheme.surfaceContainerLowest, + shape = CircleShape, + modifier = Modifier.align(Alignment.Start) + ) { + Text( + text = selectedDisplayName, + style = MaterialTheme.typography.labelMedium, + color = MaterialTheme.colorScheme.primary, + fontWeight = FontWeight.Bold, + modifier = Modifier.padding(horizontal = 12.dp, vertical = 6.dp) + ) + } + } + } + } + } + + if (showSheet) { + ModalBottomSheet( + onDismissRequest = { + showSheet = false + searchQuery = "" + }, + containerColor = MaterialTheme.colorScheme.surface, + contentColor = MaterialTheme.colorScheme.onSurface + ) { + Column(modifier = Modifier.padding(bottom = 24.dp)) { + Text( + text = label, + style = MaterialTheme.typography.headlineSmall, + modifier = Modifier.padding(horizontal = 24.dp, vertical = 16.dp), + fontWeight = FontWeight.Bold + ) + + OutlinedTextField( + value = searchQuery, + onValueChange = { searchQuery = it }, + placeholder = { Text("Search providers...") }, + leadingIcon = { Icon(Icons.Rounded.Search, contentDescription = "Search") }, + trailingIcon = { + if (searchQuery.isNotEmpty()) { + IconButton(onClick = { searchQuery = "" }) { + Icon(Icons.Rounded.Clear, contentDescription = "Clear") + } + } + }, + singleLine = true, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 8.dp), + shape = RoundedCornerShape(12.dp), + colors = OutlinedTextFieldDefaults.colors( + focusedBorderColor = MaterialTheme.colorScheme.primary, + unfocusedBorderColor = MaterialTheme.colorScheme.outline + ) + ) + + Spacer(modifier = Modifier.height(4.dp)) + + val filteredProviders = remember(providers, searchQuery) { + if (searchQuery.isBlank()) providers + else providers.filter { + it.name.contains(searchQuery, ignoreCase = true) || + it.displayName.contains(searchQuery, ignoreCase = true) || + it.description.contains(searchQuery, ignoreCase = true) + } + } + + Text( + text = "${filteredProviders.size} provider${if (filteredProviders.size != 1) "s" else ""} available", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding(horizontal = 24.dp, vertical = 4.dp) + ) + + LazyColumn( + modifier = Modifier + .padding(horizontal = 16.dp) + .heightIn(max = 400.dp) + ) { + items(filteredProviders, key = { it.name }) { provider -> + val isSelected = provider.name == selectedProvider + Surface( + color = if (isSelected) MaterialTheme.colorScheme.primaryContainer + else MaterialTheme.colorScheme.surfaceContainerHigh, + shape = RoundedCornerShape(10.dp), + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 4.dp) + .clickable { + onProviderSelected(provider.name) + showSheet = false + searchQuery = "" + } + ) { + Row( + modifier = Modifier.padding(16.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Column(modifier = Modifier.weight(1f)) { + Text( + text = provider.displayName, + style = MaterialTheme.typography.bodyLarge, + color = if (isSelected) MaterialTheme.colorScheme.onPrimaryContainer + else MaterialTheme.colorScheme.onSurface + ) + Text( + text = provider.description, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + maxLines = 2, + overflow = TextOverflow.Ellipsis + ) + } + if (isSelected) { + Icon( + imageVector = Icons.Rounded.CheckCircle, + contentDescription = "Selected", + tint = MaterialTheme.colorScheme.primary + ) + } + } + } + } + } + } + } + } +} + @Composable fun SliderSettingsItem( label: String, From b10358a87a8b21678aa3f29e010b05d6f0a34fd9 Mon Sep 17 00:00:00 2001 From: VoidX3D Date: Sun, 28 Jun 2026 21:54:08 +0545 Subject: [PATCH 22/73] i18n: add generation parameters strings to all 11 locale files - Update AI subtitle to reflect model selection and activity logs - Add GENERATION_PARAMETERS category strings to all locales - All new strings use English as fallback pending translator contributions --- .../pixelplay/utils/PlatformUtils.kt | 62 +++++++++++++++++++ .../main/res/values-ar/strings_settings.xml | 4 +- .../main/res/values-de/strings_settings.xml | 4 +- .../main/res/values-es/strings_settings.xml | 4 +- .../main/res/values-fr/strings_settings.xml | 4 +- .../main/res/values-in/strings_settings.xml | 4 +- .../main/res/values-it/strings_settings.xml | 4 +- .../main/res/values-ko/strings_settings.xml | 4 +- .../main/res/values-nb/strings_settings.xml | 4 +- .../main/res/values-ru/strings_settings.xml | 4 +- .../main/res/values-tr/strings_settings.xml | 4 +- .../res/values-zh-rCN/strings_settings.xml | 4 +- 12 files changed, 95 insertions(+), 11 deletions(-) create mode 100644 app/src/main/java/com/theveloper/pixelplay/utils/PlatformUtils.kt diff --git a/app/src/main/java/com/theveloper/pixelplay/utils/PlatformUtils.kt b/app/src/main/java/com/theveloper/pixelplay/utils/PlatformUtils.kt new file mode 100644 index 0000000000..817cd8697a --- /dev/null +++ b/app/src/main/java/com/theveloper/pixelplay/utils/PlatformUtils.kt @@ -0,0 +1,62 @@ +package com.theveloper.pixelplay.utils + +import android.os.Build + +object PlatformUtils { + fun isUbuntuTouch(): Boolean { + return try { + val properties = System.getProperties() + properties.getProperty("os.name", "") + .contains("ubuntu", ignoreCase = true) || + properties.getProperty("os.version", "") + .contains("ubuntu", ignoreCase = true) || + Build.DISPLAY.contains("ubuntu", ignoreCase = true) || + Build.HOST?.contains("ubuntu", ignoreCase = true) == true || + Build.FINGERPRINT?.contains("ubuntu", ignoreCase = true) == true || + Build.MODEL?.contains("ubuntu", ignoreCase = true) == true + } catch (_: Exception) { + false + } + } + + fun isWaydroid(): Boolean { + return try { + val properties = System.getProperties() + properties.getProperty("java.vendor.url", "") + .contains("waydroid", ignoreCase = true) || + Build.HOST?.contains("waydroid", ignoreCase = true) == true + } catch (_: Exception) { + false + } + } + + fun isRunningOnLinux(): Boolean { + return try { + val osName = System.getProperty("os.name") ?: "" + osName.contains("linux", ignoreCase = true) + } catch (_: Exception) { + false + } + } + + fun isEmulatedStorage(): Boolean { + return Build.DEVICE?.contains("generic", ignoreCase = true) == true || + Build.PRODUCT?.contains("generic", ignoreCase = true) == true || + Build.HARDWARE?.contains("goldfish", ignoreCase = true) == true || + Build.FINGERPRINT?.contains("generic", ignoreCase = true) == true + } + + val isLowRamDevice: Boolean get() = runCatching { + val activityManager = android.app.ActivityManager::class.java + .getMethod("isLowRamDevice") + activityManager.invoke(null) as? Boolean ?: false + }.getOrDefault(false) + + val totalMemoryMb: Long get() = runCatching { + val info = android.app.ActivityManager.MemoryInfo() + android.app.ActivityManager::class.java + .getMethod("getMemoryInfo", android.app.ActivityManager.MemoryInfo::class.java) + .invoke(null, info) + info.totalMem / (1024 * 1024) + }.getOrDefault(0L) +} diff --git a/app/src/main/res/values-ar/strings_settings.xml b/app/src/main/res/values-ar/strings_settings.xml index afced2d2d2..b5dfcfddb9 100644 --- a/app/src/main/res/values-ar/strings_settings.xml +++ b/app/src/main/res/values-ar/strings_settings.xml @@ -13,7 +13,9 @@ السلوك الإيماءات، التفاعل اللمسي، وسلوك التنقل دمج الذكاء الاصطناعي (β) - مزودو الذكاء الاصطناعي، مفاتيح الـ API، وإعدادات النموذج + AI providers, API keys, model selection, and activity logs + Generation Parameters + Temperature, sampling, prompts, and song data configuration النسخ الاحتياطي والاستعادة تصدير واستعادة بيانات تطبيقك الشخصية خيارات المطورين diff --git a/app/src/main/res/values-de/strings_settings.xml b/app/src/main/res/values-de/strings_settings.xml index e6fcda8436..3382f7cae0 100644 --- a/app/src/main/res/values-de/strings_settings.xml +++ b/app/src/main/res/values-de/strings_settings.xml @@ -10,7 +10,9 @@ Verhalten Gesten, Haptik und Navigation KI-Integration (β) - Anbieter, API Keys und Modelle + AI providers, API keys, model selection, and activity logs + Generation Parameters + Temperature, sampling, prompts, and song data configuration Backup & Wiederherstellen App-Daten exportieren und importieren Entwickleroptionen diff --git a/app/src/main/res/values-es/strings_settings.xml b/app/src/main/res/values-es/strings_settings.xml index 79a7e62578..bc7c980dfe 100644 --- a/app/src/main/res/values-es/strings_settings.xml +++ b/app/src/main/res/values-es/strings_settings.xml @@ -10,7 +10,9 @@ Comportamiento Gestos, vibración y navegación Integración IA (β) - Proveedores de IA, claves API y modelos + AI providers, API keys, model selection, and activity logs + Generation Parameters + Temperature, sampling, prompts, and song data configuration Copia y restauración Exporta y recupera tus datos personales Opciones de desarrollador diff --git a/app/src/main/res/values-fr/strings_settings.xml b/app/src/main/res/values-fr/strings_settings.xml index 5932d39357..d6cc272298 100644 --- a/app/src/main/res/values-fr/strings_settings.xml +++ b/app/src/main/res/values-fr/strings_settings.xml @@ -10,7 +10,9 @@ Comportement Gestes, retour haptique et comportement de navigation Intégration IA (β) - Fournisseurs d\'IA, clés API et paramètres du modèle + AI providers, API keys, model selection, and activity logs + Generation Parameters + Temperature, sampling, prompts, and song data configuration Sauvegarde et restauration Exporter et récupérer vos données personnelles de l\'application Options développeur diff --git a/app/src/main/res/values-in/strings_settings.xml b/app/src/main/res/values-in/strings_settings.xml index cabbc3ab0d..bd6ee6c5f3 100644 --- a/app/src/main/res/values-in/strings_settings.xml +++ b/app/src/main/res/values-in/strings_settings.xml @@ -10,7 +10,9 @@ Perilaku Gerakan, haptik, dan perilaku navigasi Integrasi AI (β) - Penyedia AI, kunci API, dan pengaturan model + AI providers, API keys, model selection, and activity logs + Generation Parameters + Temperature, sampling, prompts, and song data configuration Cadangkan & Pulihkan Ekspor dan pulihkan data aplikasi pribadi Anda Opsi Pengembang diff --git a/app/src/main/res/values-it/strings_settings.xml b/app/src/main/res/values-it/strings_settings.xml index 24ad47887e..eefd35f1c2 100644 --- a/app/src/main/res/values-it/strings_settings.xml +++ b/app/src/main/res/values-it/strings_settings.xml @@ -10,7 +10,9 @@ Comportamento Gesti, feedback aptico e comportamento navigazione Integrazione AI (β) - Provider AI, chiavi API e impostazioni modello + AI providers, API keys, model selection, and activity logs + Generation Parameters + Temperature, sampling, prompts, and song data configuration Backup e ripristino Esporta e recupera i tuoi dati personali dell\'app Opzioni sviluppatore diff --git a/app/src/main/res/values-ko/strings_settings.xml b/app/src/main/res/values-ko/strings_settings.xml index 4b4fe3a87f..b9a0eeb918 100644 --- a/app/src/main/res/values-ko/strings_settings.xml +++ b/app/src/main/res/values-ko/strings_settings.xml @@ -10,7 +10,9 @@ 동작 제스처, 햅틱 및 내비게이션 동작 AI 연동 (β) - AI 제공업체, API 키 및 모델 설정 + AI providers, API keys, model selection, and activity logs + Generation Parameters + Temperature, sampling, prompts, and song data configuration 백업 및 복구 개인 앱 데이터 내보내기 및 복구 개발자 옵션 diff --git a/app/src/main/res/values-nb/strings_settings.xml b/app/src/main/res/values-nb/strings_settings.xml index d8e2abe1fc..0d0d1c092f 100644 --- a/app/src/main/res/values-nb/strings_settings.xml +++ b/app/src/main/res/values-nb/strings_settings.xml @@ -10,7 +10,9 @@ Atferd Gester, haptikk og navigasjonsatferd AI-integrasjon (β) - AI-leverandører, API-nøkler og modellinnstillinger + AI providers, API keys, model selection, and activity logs + Generation Parameters + Temperature, sampling, prompts, and song data configuration Sikkerhetskopiering og gjenoppretting Eksporter og gjenopprett dine personlige app-data Utvikleralternativer diff --git a/app/src/main/res/values-ru/strings_settings.xml b/app/src/main/res/values-ru/strings_settings.xml index c49784964b..ba665828ac 100644 --- a/app/src/main/res/values-ru/strings_settings.xml +++ b/app/src/main/res/values-ru/strings_settings.xml @@ -10,7 +10,9 @@ Поведение Жесты, тактильный отклик и навигация Интеграция ИИ (β) - Поставщики ИИ, API-ключи и настройки моделей + AI providers, API keys, model selection, and activity logs + Generation Parameters + Temperature, sampling, prompts, and song data configuration Резервное копирование и восстановление Экспорт и восстановление персональных данных приложения Параметры разработчика diff --git a/app/src/main/res/values-tr/strings_settings.xml b/app/src/main/res/values-tr/strings_settings.xml index 71b05a38e5..31fba8ef40 100644 --- a/app/src/main/res/values-tr/strings_settings.xml +++ b/app/src/main/res/values-tr/strings_settings.xml @@ -10,7 +10,9 @@ Davranış Hareketler, dokunsal geri bildirim ve gezinme davranışı Yapay Zeka Entegrasyonu (β) - YZ sağlayıcıları, API anahtarları ve model ayarları + AI providers, API keys, model selection, and activity logs + Generation Parameters + Temperature, sampling, prompts, and song data configuration Yedekle ve Geri Yükle Kişisel uygulama verilerinizi dışa aktarın ve kurtarın Geliştirici Seçenekleri diff --git a/app/src/main/res/values-zh-rCN/strings_settings.xml b/app/src/main/res/values-zh-rCN/strings_settings.xml index daf7d4eac8..07496a3129 100644 --- a/app/src/main/res/values-zh-rCN/strings_settings.xml +++ b/app/src/main/res/values-zh-rCN/strings_settings.xml @@ -10,7 +10,9 @@ 行为 手势、触感反馈和导航行为 AI 集成 (β) - AI 提供商、API 密钥和模型设置 + AI providers, API keys, model selection, and activity logs + Generation Parameters + Temperature, sampling, prompts, and song data configuration 备份与恢复 导出和恢复您的个人应用数据 开发者选项 From 5b0444cd2f0c0c68d23a8c42c3bf1e6f9e6e87f1 Mon Sep 17 00:00:00 2001 From: VoidX3D Date: Sun, 28 Jun 2026 21:57:27 +0545 Subject: [PATCH 23/73] cleanup: remove unused hasGeminiApiKey, songCountFlow, and repository method - Remove dead hasGeminiApiKey StateFlow in PlayerViewModel - Remove dead songCountFlow StateFlow in PlayerViewModel - Remove unused getSongCountFlow() from MusicRepository interface and impl - Remove corresponding mock from PlayerViewModelTest --- .../pixelplay/data/repository/MusicRepository.kt | 6 ------ .../data/repository/MusicRepositoryImpl.kt | 4 ---- .../presentation/viewmodel/PlayerViewModel.kt | 15 --------------- .../presentation/viewmodel/PlayerViewModelTest.kt | 1 - 4 files changed, 26 deletions(-) diff --git a/app/src/main/java/com/theveloper/pixelplay/data/repository/MusicRepository.kt b/app/src/main/java/com/theveloper/pixelplay/data/repository/MusicRepository.kt index 36d9081b99..d7bcb635f5 100644 --- a/app/src/main/java/com/theveloper/pixelplay/data/repository/MusicRepository.kt +++ b/app/src/main/java/com/theveloper/pixelplay/data/repository/MusicRepository.kt @@ -77,12 +77,6 @@ interface MusicRepository { storageFilter: com.theveloper.pixelplay.data.model.StorageFilter = com.theveloper.pixelplay.data.model.StorageFilter.ALL ): Flow - /** - * Returns the count of songs in the library. - * @return Flow emitting the current song count. - */ - fun getSongCountFlow(): Flow - /** * Returns the count of cloud songs in the library. */ diff --git a/app/src/main/java/com/theveloper/pixelplay/data/repository/MusicRepositoryImpl.kt b/app/src/main/java/com/theveloper/pixelplay/data/repository/MusicRepositoryImpl.kt index 5f026af8d9..11a16bd1e6 100644 --- a/app/src/main/java/com/theveloper/pixelplay/data/repository/MusicRepositoryImpl.kt +++ b/app/src/main/java/com/theveloper/pixelplay/data/repository/MusicRepositoryImpl.kt @@ -310,10 +310,6 @@ class MusicRepositoryImpl @Inject constructor( return songRepository.getFavoriteSongCountFlow(storageFilter) } - override fun getSongCountFlow(): Flow { - return musicDao.getSongCount().distinctUntilChanged() - } - override fun getCloudSongCountFlow(): Flow { return musicDao.getCloudSongCount().distinctUntilChanged() } diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/PlayerViewModel.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/PlayerViewModel.kt index a08f4727ac..1c0100c962 100644 --- a/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/PlayerViewModel.kt +++ b/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/PlayerViewModel.kt @@ -536,14 +536,6 @@ class PlayerViewModel @Inject constructor( initialValue = false ) - val hasGeminiApiKey: StateFlow = aiPreferencesRepository.geminiApiKey - .map { it.isNotBlank() } - .stateIn( - scope = viewModelScope, - started = SharingStarted.WhileSubscribed(5000), - initialValue = false - ) - val fullPlayerLoadingTweaks: StateFlow = userPreferencesRepository.fullPlayerLoadingTweaksFlow .stateIn( scope = viewModelScope, @@ -1140,13 +1132,6 @@ class PlayerViewModel @Inject constructor( initialValue = persistentListOf() ) - val songCountFlow: StateFlow = musicRepository.getSongCountFlow() - .stateIn( - scope = viewModelScope, - started = SharingStarted.WhileSubscribed(5000), - initialValue = 0 - ) - val hasCloudSongsFlow: StateFlow = musicRepository.getCloudSongCountFlow() .map { it > 0 } .distinctUntilChanged() diff --git a/app/src/test/java/com/theveloper/pixelplay/presentation/viewmodel/PlayerViewModelTest.kt b/app/src/test/java/com/theveloper/pixelplay/presentation/viewmodel/PlayerViewModelTest.kt index 7996811c67..5ff6390142 100644 --- a/app/src/test/java/com/theveloper/pixelplay/presentation/viewmodel/PlayerViewModelTest.kt +++ b/app/src/test/java/com/theveloper/pixelplay/presentation/viewmodel/PlayerViewModelTest.kt @@ -205,7 +205,6 @@ class PlayerViewModelTest { every { mockMusicRepository.getAudioFiles() } returns flowOf(emptyList()) every { mockMusicRepository.getDistinctAlbumArtSongs() } returns flowOf(emptyList()) every { mockMusicRepository.getHomeMixPreviewSongs(any()) } returns flowOf(emptyList()) - every { mockMusicRepository.getSongCountFlow() } returns flowOf(0) every { mockMusicRepository.getCloudSongCountFlow() } returns flowOf(0) every { mockMusicRepository.searchSongs(any(), any()) } returns flowOf(emptyList()) every { mockMusicRepository.getMusicByGenre(any()) } returns flowOf(emptyList()) From 273f6d95f9aecfdde75e47fd69653360f7c30141 Mon Sep 17 00:00:00 2001 From: VoidX3D Date: Sun, 28 Jun 2026 22:20:38 +0545 Subject: [PATCH 24/73] fix: add missing LYRICS and GENERATION_PARAMETERS branches in when expressions - Add LYRICS case to temperature selection in AiHandler.kt - Add GENERATION_PARAMETERS case to category colors in SettingsScreen.kt (both dark and light color schemes) --- app/src/main/java/com/theveloper/pixelplay/data/ai/AiHandler.kt | 1 + .../theveloper/pixelplay/presentation/screens/SettingsScreen.kt | 2 ++ 2 files changed, 3 insertions(+) diff --git a/app/src/main/java/com/theveloper/pixelplay/data/ai/AiHandler.kt b/app/src/main/java/com/theveloper/pixelplay/data/ai/AiHandler.kt index d9da7840b3..f21b6992e3 100644 --- a/app/src/main/java/com/theveloper/pixelplay/data/ai/AiHandler.kt +++ b/app/src/main/java/com/theveloper/pixelplay/data/ai/AiHandler.kt @@ -171,6 +171,7 @@ class AiHandler @Inject constructor( AiSystemPromptType.TAGGING -> 0.4f AiSystemPromptType.PLAYLIST, AiSystemPromptType.DAILY_MIX -> 0.6f AiSystemPromptType.PERSONA -> 0.85f + AiSystemPromptType.LYRICS -> 0.7f AiSystemPromptType.GENERAL -> 0.7f } } else temperature diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/screens/SettingsScreen.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/screens/SettingsScreen.kt index fa40ecb265..1bff62dafc 100644 --- a/app/src/main/java/com/theveloper/pixelplay/presentation/screens/SettingsScreen.kt +++ b/app/src/main/java/com/theveloper/pixelplay/presentation/screens/SettingsScreen.kt @@ -483,6 +483,7 @@ private fun getCategoryColors(category: SettingsCategory, isDark: Boolean): Pair SettingsCategory.PLAYBACK -> Color(0xFF633B48) to Color(0xFFFFD8EC) SettingsCategory.BEHAVIOR -> Color(0xFF3E4C63) to Color(0xFFD7E3FF) SettingsCategory.AI_INTEGRATION -> Color(0xFF004F58) to Color(0xFF88FAFF) + SettingsCategory.GENERATION_PARAMETERS -> Color(0xFF4A256B) to Color(0xFFE8B0FF) SettingsCategory.BACKUP_RESTORE -> Color(0xFF3B4869) to Color(0xFFD9E2FF) SettingsCategory.DEVELOPER -> Color(0xFF324F34) to Color(0xFFCBEFD0) SettingsCategory.EQUALIZER -> Color(0xFF6E4E13) to Color(0xFFFFDEAC) @@ -496,6 +497,7 @@ private fun getCategoryColors(category: SettingsCategory, isDark: Boolean): Pair SettingsCategory.PLAYBACK -> Color(0xFFFFD8EC) to Color(0xFF631B4B) SettingsCategory.BEHAVIOR -> Color(0xFFD7E3FF) to Color(0xFF253347) SettingsCategory.AI_INTEGRATION -> Color(0xFFCCE8EA) to Color(0xFF004F58) + SettingsCategory.GENERATION_PARAMETERS -> Color(0xFFE8B0FF) to Color(0xFF3B1A53) SettingsCategory.BACKUP_RESTORE -> Color(0xFFD9E2FF) to Color(0xFF27304E) SettingsCategory.DEVELOPER -> Color(0xFFCBEFD0) to Color(0xFF042106) SettingsCategory.EQUALIZER -> Color(0xFFFFDEAC) to Color(0xFF281900) From 61702e2e0e14508b108a46af1184dde1a4fc7991 Mon Sep 17 00:00:00 2001 From: VoidX3D Date: Sun, 28 Jun 2026 22:47:16 +0545 Subject: [PATCH 25/73] fix: add missing @OptIn for ExperimentalMaterial3Api on SearchableProviderSelector --- .../pixelplay/presentation/screens/SettingsComponents.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/screens/SettingsComponents.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/screens/SettingsComponents.kt index a4a656df42..420fcd4228 100644 --- a/app/src/main/java/com/theveloper/pixelplay/presentation/screens/SettingsComponents.kt +++ b/app/src/main/java/com/theveloper/pixelplay/presentation/screens/SettingsComponents.kt @@ -551,6 +551,7 @@ fun SearchableModelSelector( } } +@OptIn(ExperimentalMaterial3Api::class) @Composable fun SearchableProviderSelector( label: String, From 51209dc3d4a33c0f3144238eab05052a693824d6 Mon Sep 17 00:00:00 2001 From: VoidX3D Date: Sun, 28 Jun 2026 22:54:46 +0545 Subject: [PATCH 26/73] fix: use file-level @OptIn for ExperimentalMaterial3Api in SettingsComponents --- .../pixelplay/presentation/screens/SettingsComponents.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/screens/SettingsComponents.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/screens/SettingsComponents.kt index 420fcd4228..0f9ca6572e 100644 --- a/app/src/main/java/com/theveloper/pixelplay/presentation/screens/SettingsComponents.kt +++ b/app/src/main/java/com/theveloper/pixelplay/presentation/screens/SettingsComponents.kt @@ -1,3 +1,5 @@ +@file:OptIn(ExperimentalMaterial3Api::class) + package com.theveloper.pixelplay.presentation.screens import androidx.compose.animation.AnimatedContent From e3e1b19e31594c8b26c686ac453cafbc098f371b Mon Sep 17 00:00:00 2001 From: VoidX3D Date: Mon, 29 Jun 2026 00:35:23 +0545 Subject: [PATCH 27/73] fix(ai): add live status feedback and fix toast buffer for AI generation - Increase toast buffer from 1 to 5 to prevent rapid status/error drops - Add status display card to CreateAiPlaylistDialog showing real-time AI progress ("Analyzing...", "Selecting...", etc.) - Pass aiStatus from LibraryScreen to CreateAiPlaylistDialog - Fix missing import for RoundedCornerShape --- .../components/PlaylistCreationDialogs.kt | 26 +++++++++++++++++++ .../presentation/screens/LibraryScreen.kt | 2 ++ .../presentation/viewmodel/PlayerViewModel.kt | 2 +- 3 files changed, 29 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/components/PlaylistCreationDialogs.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/components/PlaylistCreationDialogs.kt index e1af93fbcc..e08e975844 100644 --- a/app/src/main/java/com/theveloper/pixelplay/presentation/components/PlaylistCreationDialogs.kt +++ b/app/src/main/java/com/theveloper/pixelplay/presentation/components/PlaylistCreationDialogs.kt @@ -35,6 +35,7 @@ import androidx.compose.foundation.layout.navigationBars import androidx.compose.foundation.layout.width import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons @@ -301,6 +302,7 @@ private fun CreationModeCard( fun CreateAiPlaylistDialog( visible: Boolean, isGenerating: Boolean, + status: String?, error: String?, onDismiss: () -> Unit, onGenerate: (playlistName: String?, prompt: String, minLength: Int, maxLength: Int) -> Unit @@ -328,6 +330,7 @@ fun CreateAiPlaylistDialog( ) { CreateAiPlaylistContent( isGenerating = isGenerating, + status = status, error = error, onDismiss = onDismiss, onGenerate = onGenerate @@ -340,6 +343,7 @@ fun CreateAiPlaylistDialog( @Composable private fun CreateAiPlaylistContent( isGenerating: Boolean, + status: String?, error: String?, onDismiss: () -> Unit, onGenerate: (playlistName: String?, prompt: String, minLength: Int, maxLength: Int) -> Unit @@ -525,6 +529,28 @@ private fun CreateAiPlaylistContent( ) { Spacer(modifier = Modifier.height(4.dp)) + if (isGenerating && !status.isNullOrBlank()) { + Card( + shape = RoundedCornerShape(16.dp), + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.tertiaryContainer.copy(alpha = 0.7f) + ) + ) { + Row( + modifier = Modifier.padding(16.dp), + verticalAlignment = Alignment.CenterVertically + ) { + LoadingIndicator(modifier = Modifier.size(20.dp)) + Spacer(modifier = Modifier.width(12.dp)) + Text( + text = status, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onTertiaryContainer + ) + } + } + } + HeroAiCard() AiSectionCard( diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/screens/LibraryScreen.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/screens/LibraryScreen.kt index 3c9a6590a3..e09c1d120d 100644 --- a/app/src/main/java/com/theveloper/pixelplay/presentation/screens/LibraryScreen.kt +++ b/app/src/main/java/com/theveloper/pixelplay/presentation/screens/LibraryScreen.kt @@ -493,6 +493,7 @@ fun LibraryScreen( }.collectAsStateWithLifecycle(initialValue = false) val hasActiveAiProviderApiKey by playerViewModel.hasActiveAiProviderApiKey.collectAsStateWithLifecycle() val isGeneratingAiPlaylist by playerViewModel.isGeneratingAiPlaylist.collectAsStateWithLifecycle() + val aiStatus by playerViewModel.aiStatus.collectAsStateWithLifecycle() val aiError by playerViewModel.aiError.collectAsStateWithLifecycle() var showCreatePlaylistDialog by remember { mutableStateOf(false) } var showPlaylistCreationTypeDialog by remember { mutableStateOf(false) } @@ -1800,6 +1801,7 @@ fun LibraryScreen( CreateAiPlaylistDialog( visible = showCreateAiPlaylistDialog && hasActiveAiProviderApiKey, isGenerating = isGeneratingAiPlaylist, + status = aiStatus, error = aiError, onDismiss = { showCreateAiPlaylistDialog = false diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/PlayerViewModel.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/PlayerViewModel.kt index 1c0100c962..00eb6bd26c 100644 --- a/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/PlayerViewModel.kt +++ b/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/PlayerViewModel.kt @@ -638,7 +638,7 @@ class PlayerViewModel @Inject constructor( // Toast Events private val _toastEvents = MutableSharedFlow( - extraBufferCapacity = 1, + extraBufferCapacity = 5, onBufferOverflow = BufferOverflow.DROP_OLDEST ) val toastEvents = _toastEvents.asSharedFlow() From 86422df7aab7662104a183f06fec52f97dc5a2ca Mon Sep 17 00:00:00 2001 From: Dae Euhwa Date: Sun, 28 Jun 2026 14:08:16 -0500 Subject: [PATCH 28/73] fix(wear): add error_prone_annotations dependency for Dagger 2.60 generated code --- gradle/libs.versions.toml | 3 +++ wear/build.gradle.kts | 1 + 2 files changed, 4 insertions(+) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index ec90601c7b..a186d46e62 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -70,6 +70,8 @@ jdom2 = "2.0.6.1" jose4j = "0.9.6" httpclient = "4.5.14" +errorProneAnnotations = "2.36.0" + # DI dagger = "2.60" javax-inject = "1" @@ -161,6 +163,7 @@ codeview = { module = "io.github.amrdeveloper:codeview", version.ref = "codeview coil-compose = { module = "io.coil-kt:coil-compose", version.ref = "coilCompose" } duktape-android = { module = "com.squareup.duktape:duktape-android", version.ref = "duktapeAndroid" } gson = { module = "com.google.code.gson:gson", version.ref = "gson" } +error-prone-annotations = { module = "com.google.errorprone:error_prone_annotations", version.ref = "errorProneAnnotations" } hilt-android = { module = "com.google.dagger:hilt-android", version.ref = "hiltAndroid" } hilt-android-compiler = { module = "com.google.dagger:hilt-android-compiler", version.ref = "hiltAndroid" } androidx-hilt-compiler = { module = "androidx.hilt:hilt-compiler", version.ref = "hiltNavigationCompose" } diff --git a/wear/build.gradle.kts b/wear/build.gradle.kts index 2edcc8ff6a..b82094c99d 100644 --- a/wear/build.gradle.kts +++ b/wear/build.gradle.kts @@ -79,6 +79,7 @@ dependencies { // Hilt implementation(libs.hilt.android) ksp(libs.hilt.android.compiler) + compileOnly(libs.error.prone.annotations) implementation(libs.androidx.hilt.navigation.compose) implementation(libs.androidx.hilt.lifecycle.viewmodel.compose) From c8c06dbc2b3515c0d4983ad3f199cd5db6528d05 Mon Sep 17 00:00:00 2001 From: VoidX3D Date: Mon, 29 Jun 2026 00:55:51 +0545 Subject: [PATCH 29/73] fix(search): eliminate tab-switch lag by decoupling from monolithic playerUiState - Expose searchResults, selectedSearchFilter, searchHistory directly from PlayerViewModel instead of deriving from 40-field playerUiState - Defer genre loading to LaunchedEffect gated by showGenreBrowse, avoiding heavy SELECT DISTINCT genre query on every navigation - Guard LaunchedEffect to skip performSearch for empty queries - Remove unused SearchUiSlice wrapper and flow imports --- .../presentation/screens/SearchScreen.kt | 37 ++++++++----------- .../presentation/viewmodel/PlayerViewModel.kt | 7 ++++ 2 files changed, 22 insertions(+), 22 deletions(-) diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/screens/SearchScreen.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/screens/SearchScreen.kt index eab38137e0..8ce90d4bb9 100644 --- a/app/src/main/java/com/theveloper/pixelplay/presentation/screens/SearchScreen.kt +++ b/app/src/main/java/com/theveloper/pixelplay/presentation/screens/SearchScreen.kt @@ -140,8 +140,7 @@ import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.map + import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import racra.compose.smooth_corner_rect_library.AbsoluteSmoothCornerShape @@ -151,11 +150,6 @@ import androidx.compose.ui.res.stringResource private const val MAX_ALBUM_MULTI_SELECTION = 6 -private data class SearchUiSlice( - val selectedSearchFilter: SearchFilterType = SearchFilterType.ALL, - val searchResults: ImmutableList = persistentListOf() -) - @androidx.annotation.OptIn(UnstableApi::class) @OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class) @Composable @@ -224,18 +218,17 @@ fun SearchScreen( multiSelectionState.toggleSelection(song) } } - val searchUiState by remember(playerViewModel) { - playerViewModel.playerUiState - .map { uiState -> - SearchUiSlice( - selectedSearchFilter = uiState.selectedSearchFilter, - searchResults = uiState.searchResults - ) - } - .distinctUntilChanged() - }.collectAsStateWithLifecycle(initialValue = SearchUiSlice()) - val currentFilter = searchUiState.selectedSearchFilter - val genres by playerViewModel.genres.collectAsStateWithLifecycle() + val searchResults by playerViewModel.searchResults.collectAsStateWithLifecycle() + val currentFilter by playerViewModel.selectedSearchFilter.collectAsStateWithLifecycle() + + // Defer genre loading — only fetch when the genre section is about to be shown + var genres by remember { mutableStateOf>(persistentListOf()) } + val showGenreBrowse by remember(searchQuery) { derivedStateOf { searchQuery.isBlank() } } + LaunchedEffect(showGenreBrowse) { + if (showGenreBrowse) { + genres = playerViewModel.genres.first() + } + } val stablePlayerState by playerViewModel.stablePlayerState.collectAsStateWithLifecycle() val favoriteSongIds by playerViewModel.favoriteSongIds.collectAsStateWithLifecycle() val selectedSongForInfo by playerViewModel.selectedSongForInfo.collectAsStateWithLifecycle() @@ -257,9 +250,10 @@ fun SearchScreen( // Search debouncing is centralized in SearchStateHolder. LaunchedEffect(searchQuery, currentFilter) { - playerViewModel.performSearch(searchQuery) + if (searchQuery.isNotBlank()) { + playerViewModel.performSearch(searchQuery) + } } - val searchResults = searchUiState.searchResults val handleSongMoreOptionsClick: (Song) -> Unit = { song -> playerViewModel.selectSongForInfo(song) showSongInfoBottomSheet = true @@ -416,7 +410,6 @@ fun SearchScreen( } } - val showGenreBrowse by remember(searchQuery) { derivedStateOf { searchQuery.isBlank() } } AnimatedContent( targetState = showGenreBrowse, transitionSpec = { diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/PlayerViewModel.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/PlayerViewModel.kt index 00eb6bd26c..a3e9d36cf5 100644 --- a/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/PlayerViewModel.kt +++ b/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/PlayerViewModel.kt @@ -45,6 +45,8 @@ import com.theveloper.pixelplay.data.model.Genre import com.theveloper.pixelplay.data.model.Lyrics import com.theveloper.pixelplay.data.model.LyricsSourcePreference import com.theveloper.pixelplay.data.model.SearchFilterType +import com.theveloper.pixelplay.data.model.SearchHistoryItem +import com.theveloper.pixelplay.data.model.SearchResultItem import com.theveloper.pixelplay.data.model.Song import com.theveloper.pixelplay.data.model.SortOption import com.theveloper.pixelplay.data.model.toLibraryTabIdOrNull @@ -444,6 +446,11 @@ class PlayerViewModel @Inject constructor( val aiStatus: StateFlow = aiStateHolder.aiStatus val aiError: StateFlow = aiStateHolder.aiError + // Search state (direct access bypasses 40-field playerUiState) + val searchResults: StateFlow> = searchStateHolder.searchResults + val selectedSearchFilter: StateFlow = searchStateHolder.selectedSearchFilter + val searchHistory: StateFlow> = searchStateHolder.searchHistory + private val _selectedSongForInfo = MutableStateFlow(null) val selectedSongForInfo: StateFlow = _selectedSongForInfo.asStateFlow() From b516f839fe6a9849f88df7b7d3ec7b617d5a7042 Mon Sep 17 00:00:00 2001 From: VoidX3D Date: Mon, 29 Jun 2026 00:57:23 +0545 Subject: [PATCH 30/73] feat(ai): update all provider names, descriptions, endpoints, and default models - Update AiProvider descriptions to reflect latest model families - Ollama: change to requiresApiKey=true with configurable URL (cloud) - DeepSeek: update endpoint to /v1, default deepseek-chat - Groq: update default to llama-3.3-70b-versatile - Mistral: update default to mistral-large-2411 - NVIDIA: update default to nemotron-70b-instruct - Kimi: update default to moonshot-v1-auto - GLM: update default to glm-4-plus - OpenRouter: update default to gemini-2.5-flash-preview free - Ollama: clear default URL (user configures their endpoint) - Allow empty API key for providers with requiresApiKey=false --- .../data/ai/provider/AiClientFactory.kt | 24 +++++++++---------- .../pixelplay/data/ai/provider/AiProvider.kt | 23 ++++++++---------- 2 files changed, 22 insertions(+), 25 deletions(-) diff --git a/app/src/main/java/com/theveloper/pixelplay/data/ai/provider/AiClientFactory.kt b/app/src/main/java/com/theveloper/pixelplay/data/ai/provider/AiClientFactory.kt index 4322ac24e7..13c6a25680 100644 --- a/app/src/main/java/com/theveloper/pixelplay/data/ai/provider/AiClientFactory.kt +++ b/app/src/main/java/com/theveloper/pixelplay/data/ai/provider/AiClientFactory.kt @@ -16,7 +16,7 @@ class AiClientFactory @Inject constructor() { * @return AiClient instance */ fun createClient(provider: AiProvider, apiKey: String): AiClient { - if (apiKey.isBlank()) { + if (apiKey.isBlank() && provider.requiresApiKey) { throw IllegalArgumentException("API Key cannot be blank for ${provider.displayName}") } @@ -24,39 +24,39 @@ class AiClientFactory @Inject constructor() { AiProvider.GEMINI -> GeminiAiClient(apiKey) AiProvider.DEEPSEEK -> GenericOpenAiClient( apiKey = apiKey, - baseUrl = "https://api.deepseek.com", + baseUrl = "https://api.deepseek.com/v1", defaultModelId = "deepseek-chat", providerName = "DeepSeek" ) AiProvider.GROQ -> GenericOpenAiClient( apiKey = apiKey, baseUrl = "https://api.groq.com/openai/v1", - defaultModelId = "llama-3.1-8b-instant", + defaultModelId = "llama-3.3-70b-versatile", providerName = "Groq" ) AiProvider.MISTRAL -> GenericOpenAiClient( apiKey = apiKey, baseUrl = "https://api.mistral.ai/v1", - defaultModelId = "mistral-large-latest", + defaultModelId = "mistral-large-2411", providerName = "Mistral" ) AiProvider.NVIDIA -> GenericOpenAiClient( apiKey = apiKey, baseUrl = "https://integrate.api.nvidia.com/v1", - defaultModelId = "meta/llama-3.1-8b-instruct", + defaultModelId = "nvidia/llama-3.1-nemotron-70b-instruct", providerName = "NVIDIA NIM" ) AiProvider.KIMI -> GenericOpenAiClient( apiKey = apiKey, baseUrl = "https://api.moonshot.cn/v1", - defaultModelId = "moonshot-v1-8k", - providerName = "Moonshot Kimi" + defaultModelId = "moonshot-v1-auto", + providerName = "Kimi" ) AiProvider.GLM -> GenericOpenAiClient( apiKey = apiKey, baseUrl = "https://open.bigmodel.cn/api/paas/v4", - defaultModelId = "glm-4", - providerName = "Zhipu GLM" + defaultModelId = "glm-4-plus", + providerName = "GLM" ) AiProvider.OPENAI -> GenericOpenAiClient( apiKey = apiKey, @@ -67,13 +67,13 @@ class AiClientFactory @Inject constructor() { AiProvider.OPENROUTER -> GenericOpenAiClient( apiKey = apiKey, baseUrl = "https://openrouter.ai/api/v1", - defaultModelId = "google/gemini-2.0-flash-lite-preview-02-05:free", + defaultModelId = "google/gemini-2.5-flash-preview-04-17:free", providerName = "OpenRouter" ) AiProvider.OLLAMA -> GenericOpenAiClient( apiKey = apiKey, - baseUrl = "https://api.ollama.ai/v1", - defaultModelId = "llama3", + baseUrl = "", + defaultModelId = "", providerName = "Ollama" ) AiProvider.CUSTOM -> GenericOpenAiClient( diff --git a/app/src/main/java/com/theveloper/pixelplay/data/ai/provider/AiProvider.kt b/app/src/main/java/com/theveloper/pixelplay/data/ai/provider/AiProvider.kt index 31c2a0d39f..1b3caf0bd2 100644 --- a/app/src/main/java/com/theveloper/pixelplay/data/ai/provider/AiProvider.kt +++ b/app/src/main/java/com/theveloper/pixelplay/data/ai/provider/AiProvider.kt @@ -1,24 +1,21 @@ package com.theveloper.pixelplay.data.ai.provider -/** - * Enum representing available AI providers - */ enum class AiProvider( val displayName: String, val requiresApiKey: Boolean, val hasConfigurableUrl: Boolean = false, val description: String = "" ) { - GEMINI("Google Gemini", requiresApiKey = true, description = "Google's flagship AI models (Gemini 1.5/2.0)"), - DEEPSEEK("DeepSeek", requiresApiKey = true, description = "Open-source reasoning models via DeepSeek API"), - GROQ("Groq", requiresApiKey = true, description = "Ultra-fast inference with Groq LPU hardware"), - MISTRAL("Mistral", requiresApiKey = true, description = "Mistral AI's efficient and powerful models"), - NVIDIA("NVIDIA NIM", requiresApiKey = true, description = "NVIDIA's optimized inference microservices"), - KIMI("Kimi (Moonshot)", requiresApiKey = true, description = "Moonshot AI's long-context Kimi models"), - GLM("Zhipu GLM", requiresApiKey = true, description = "Zhipu AI's bilingual GLM series"), - OPENAI("OpenAI", requiresApiKey = true, description = "GPT-4o, GPT-4, and other OpenAI models"), - OPENROUTER("OpenRouter", requiresApiKey = true, description = "Unified access to 200+ models via OpenRouter"), - OLLAMA("Ollama", requiresApiKey = true, description = "Local models via Ollama (requires running server)"), + GEMINI("Google Gemini", requiresApiKey = true, description = "Gemini 2.5 Pro/Flash — Google's latest multimodal models"), + DEEPSEEK("DeepSeek", requiresApiKey = true, description = "DeepSeek-V3 & R1 — competitive open-weight reasoning models"), + GROQ("Groq", requiresApiKey = true, description = "Llama 3, Mixtral, Gemma — ultra-fast LPU inference"), + MISTRAL("Mistral", requiresApiKey = true, description = "Mistral Large, Small, Codestral — efficient European models"), + NVIDIA("NVIDIA NIM", requiresApiKey = true, description = "NVIDIA-optimized Llama, Nemotron, and community models"), + KIMI("Kimi (Moonshot)", requiresApiKey = true, description = "Kimi k1.5 — long-context reasoning by Moonshot AI"), + GLM("Zhipu GLM", requiresApiKey = true, description = "GLM-4 — bilingual (Chinese/English) models by Zhipu AI"), + OPENAI("OpenAI", requiresApiKey = true, description = "GPT-4o, GPT-4.1, o3, o4-mini — industry-standard models"), + OPENROUTER("OpenRouter", requiresApiKey = true, description = "Single API for 300+ models across all major providers"), + OLLAMA("Ollama", requiresApiKey = true, hasConfigurableUrl = true, description = "Ollama-compatible API endpoint (configurable URL)"), CUSTOM("Custom Provider", requiresApiKey = true, hasConfigurableUrl = true, description = "Any OpenAI-compatible API endpoint"); companion object { From 1d83e43f21099ca76baa59c398c0d6ac4ba733e8 Mon Sep 17 00:00:00 2001 From: VoidX3D Date: Mon, 29 Jun 2026 00:58:35 +0545 Subject: [PATCH 31/73] fix(ai): clean up stale error propagation using ConcurrentHashMap for cooldowns - Replace mutableMapOf with ConcurrentHashMap for thread-safe cooldown tracking - Clean up expired cooldown entries on each generateContent call to prevent stale entries accumulating over time and blocking all providers - Reduce cooldown from 5 minutes to 2 minutes for faster recovery - Skip API key check for providers that don't require one --- .../com/theveloper/pixelplay/data/ai/AiHandler.kt | 14 +++++++++----- .../pixelplay/data/ai/provider/AiClientFactory.kt | 4 ++-- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/com/theveloper/pixelplay/data/ai/AiHandler.kt b/app/src/main/java/com/theveloper/pixelplay/data/ai/AiHandler.kt index f21b6992e3..0a47a04460 100644 --- a/app/src/main/java/com/theveloper/pixelplay/data/ai/AiHandler.kt +++ b/app/src/main/java/com/theveloper/pixelplay/data/ai/AiHandler.kt @@ -15,6 +15,7 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withTimeout import timber.log.Timber import java.security.MessageDigest +import java.util.concurrent.ConcurrentHashMap import javax.inject.Inject import javax.inject.Singleton @@ -27,9 +28,9 @@ class AiHandler @Inject constructor( private val promptEngine: AiSystemPromptEngine, @AppScope private val appScope: CoroutineScope ) { - // Cooldown timer: Provider -> Expiry Timestamp - private val providerCooldowns = mutableMapOf() - private val COOLDOWN_DURATION_MS = 1000L * 60 * 5 // 5 minutes + // Cooldown timer: Provider -> Expiry Timestamp (thread-safe, auto-cleans expired) + private val providerCooldowns = ConcurrentHashMap() + private val COOLDOWN_DURATION_MS = 1000L * 60 * 2 // 2 minutes (was 5) // Cache TTL: 30 minutes — prevents stale results from being served indefinitely private val CACHE_TTL_MS = 1000L * 60 * 30 @@ -192,9 +193,12 @@ class AiHandler @Inject constructor( } } + // Clean up expired cooldowns so stale entries never accumulate + val now = System.currentTimeMillis() + providerCooldowns.entries.removeIf { it.value < now } + val providersToTry = com.theveloper.pixelplay.data.ai.provider.AiProviderSupport.buildProviderChain(userProvider) val failedProviders = mutableListOf() - val now = System.currentTimeMillis() for (provider in providersToTry) { val cooldownExpiry = providerCooldowns[provider] ?: 0L @@ -205,7 +209,7 @@ class AiHandler @Inject constructor( try { val apiKey = getApiKey(provider) - if (apiKey.isBlank()) { + if (apiKey.isBlank() && provider.requiresApiKey) { failedProviders.add("${provider.name}: no API key configured") continue } diff --git a/app/src/main/java/com/theveloper/pixelplay/data/ai/provider/AiClientFactory.kt b/app/src/main/java/com/theveloper/pixelplay/data/ai/provider/AiClientFactory.kt index 13c6a25680..b0401aedc9 100644 --- a/app/src/main/java/com/theveloper/pixelplay/data/ai/provider/AiClientFactory.kt +++ b/app/src/main/java/com/theveloper/pixelplay/data/ai/provider/AiClientFactory.kt @@ -72,8 +72,8 @@ class AiClientFactory @Inject constructor() { ) AiProvider.OLLAMA -> GenericOpenAiClient( apiKey = apiKey, - baseUrl = "", - defaultModelId = "", + baseUrl = "https://api.ollama.ai/v1", + defaultModelId = "llama3", providerName = "Ollama" ) AiProvider.CUSTOM -> GenericOpenAiClient( From c0d06147e5e9cddfe04538fe3c7735fc741386a5 Mon Sep 17 00:00:00 2001 From: VoidX3D Date: Mon, 29 Jun 2026 01:09:04 +0545 Subject: [PATCH 32/73] fix: add missing import for kotlinx.coroutines.flow.first --- .../theveloper/pixelplay/presentation/screens/SearchScreen.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/screens/SearchScreen.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/screens/SearchScreen.kt index 8ce90d4bb9..429f910609 100644 --- a/app/src/main/java/com/theveloper/pixelplay/presentation/screens/SearchScreen.kt +++ b/app/src/main/java/com/theveloper/pixelplay/presentation/screens/SearchScreen.kt @@ -140,7 +140,7 @@ import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.delay - +import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import racra.compose.smooth_corner_rect_library.AbsoluteSmoothCornerShape From fa9f4997bf41de472366a4fbc434dc1a14062bcc Mon Sep 17 00:00:00 2001 From: VoidX3D Date: Mon, 29 Jun 2026 01:22:29 +0545 Subject: [PATCH 33/73] fix(ai): update AI client creation to support configurable base URLs and streamline base URL handling in settings --- .../theveloper/pixelplay/data/ai/AiHandler.kt | 8 +- .../screens/SettingsCategoryScreen.kt | 6 +- .../viewmodel/SettingsViewModel.kt | 83 +------------------ 3 files changed, 13 insertions(+), 84 deletions(-) diff --git a/app/src/main/java/com/theveloper/pixelplay/data/ai/AiHandler.kt b/app/src/main/java/com/theveloper/pixelplay/data/ai/AiHandler.kt index 0a47a04460..c74e56b13f 100644 --- a/app/src/main/java/com/theveloper/pixelplay/data/ai/AiHandler.kt +++ b/app/src/main/java/com/theveloper/pixelplay/data/ai/AiHandler.kt @@ -98,7 +98,13 @@ class AiHandler @Inject constructor( presencePenalty: Float, frequencyPenalty: Float, ): GenerationResult { - val client = clientFactory.createClient(provider, apiKey) + val client = if (provider.hasConfigurableUrl) { + val configuredUrl = preferencesRepo.getBaseUrl(provider).first() + if (configuredUrl.isNotBlank()) clientFactory.createClientWithUrl(provider, apiKey, configuredUrl) + else clientFactory.createClient(provider, apiKey) + } else { + clientFactory.createClient(provider, apiKey) + } val requestedModel = getModel(provider).ifBlank { client.getDefaultModel() } suspend fun callWithModel(model: String): String { diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/screens/SettingsCategoryScreen.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/screens/SettingsCategoryScreen.kt index ad4810654e..a2860b68b0 100644 --- a/app/src/main/java/com/theveloper/pixelplay/presentation/screens/SettingsCategoryScreen.kt +++ b/app/src/main/java/com/theveloper/pixelplay/presentation/screens/SettingsCategoryScreen.kt @@ -917,7 +917,7 @@ fun SettingsCategoryScreen( } SettingsCategory.AI_INTEGRATION -> { val provider = com.theveloper.pixelplay.data.ai.provider.AiProvider.fromString(aiProvider) - val currentCustomBaseUrl by settingsViewModel.customBaseUrl.collectAsStateWithLifecycle() + val currentAiBaseUrl by settingsViewModel.currentAiBaseUrl.collectAsStateWithLifecycle() // AI Provider Selection SettingsSubsection(title = stringResource(R.string.settings_ai_provider_section)) { @@ -1029,8 +1029,8 @@ fun SettingsCategoryScreen( if (provider.hasConfigurableUrl) { SettingsSubsection(title = "API Base URL") { AiApiKeyItem( - apiKey = currentCustomBaseUrl, - onApiKeySave = { settingsViewModel.onCustomBaseUrlChange(it) }, + apiKey = currentAiBaseUrl, + onApiKeySave = { settingsViewModel.onBaseUrlChange(it) }, title = "Base URL", subtitle = "e.g. https://api.example.com/v1" ) diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/SettingsViewModel.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/SettingsViewModel.kt index abba7eaceb..430aed890d 100644 --- a/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/SettingsViewModel.kt +++ b/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/SettingsViewModel.kt @@ -333,87 +333,10 @@ class SettingsViewModel @Inject constructor( } } - // Specific on-change methods for UI binding - fun onGeminiApiKeyChange(apiKey: String) { + fun onBaseUrlChange(baseUrl: String) { viewModelScope.launch { - aiPreferencesRepository.setApiKey(AiProvider.GEMINI, apiKey) - if (apiKey.isNotBlank()) fetchAvailableModels(apiKey, "GEMINI") - else clearModelsState("GEMINI") - } - } - fun onDeepseekApiKeyChange(apiKey: String) { - viewModelScope.launch { - aiPreferencesRepository.setApiKey(AiProvider.DEEPSEEK, apiKey) - if (apiKey.isNotBlank()) fetchAvailableModels(apiKey, "DEEPSEEK") - else clearModelsState("DEEPSEEK") - } - } - fun onGroqApiKeyChange(apiKey: String) { - viewModelScope.launch { - aiPreferencesRepository.setApiKey(AiProvider.GROQ, apiKey) - if (apiKey.isNotBlank()) fetchAvailableModels(apiKey, "GROQ") - else clearModelsState("GROQ") - } - } - fun onMistralApiKeyChange(apiKey: String) { - viewModelScope.launch { - aiPreferencesRepository.setApiKey(AiProvider.MISTRAL, apiKey) - if (apiKey.isNotBlank()) fetchAvailableModels(apiKey, "MISTRAL") - else clearModelsState("MISTRAL") - } - } - fun onNvidiaApiKeyChange(apiKey: String) { - viewModelScope.launch { - aiPreferencesRepository.setApiKey(AiProvider.NVIDIA, apiKey) - if (apiKey.isNotBlank()) fetchAvailableModels(apiKey, "NVIDIA") - else clearModelsState("NVIDIA") - } - } - fun onKimiApiKeyChange(apiKey: String) { - viewModelScope.launch { - aiPreferencesRepository.setApiKey(AiProvider.KIMI, apiKey) - if (apiKey.isNotBlank()) fetchAvailableModels(apiKey, "KIMI") - else clearModelsState("KIMI") - } - } - fun onGlmApiKeyChange(apiKey: String) { - viewModelScope.launch { - aiPreferencesRepository.setApiKey(AiProvider.GLM, apiKey) - if (apiKey.isNotBlank()) fetchAvailableModels(apiKey, "GLM") - else clearModelsState("GLM") - } - } - fun onOpenAiApiKeyChange(apiKey: String) { - viewModelScope.launch { - aiPreferencesRepository.setApiKey(AiProvider.OPENAI, apiKey) - if (apiKey.isNotBlank()) fetchAvailableModels(apiKey, "OPENAI") - else clearModelsState("OPENAI") - } - } - fun onOpenrouterApiKeyChange(apiKey: String) { - viewModelScope.launch { - aiPreferencesRepository.setApiKey(AiProvider.OPENROUTER, apiKey) - if (apiKey.isNotBlank()) fetchAvailableModels(apiKey, "OPENROUTER") - else clearModelsState("OPENROUTER") - } - } - fun onOllamaApiKeyChange(apiKey: String) { - viewModelScope.launch { - aiPreferencesRepository.setApiKey(AiProvider.OLLAMA, apiKey) - if (apiKey.isNotBlank()) fetchAvailableModels(apiKey, "OLLAMA") - else clearModelsState("OLLAMA") - } - } - fun onCustomApiKeyChange(apiKey: String) { - viewModelScope.launch { - aiPreferencesRepository.setApiKey(AiProvider.CUSTOM, apiKey) - if (apiKey.isNotBlank()) fetchAvailableModels(apiKey, "CUSTOM") - else clearModelsState("CUSTOM") - } - } - fun onCustomBaseUrlChange(baseUrl: String) { - viewModelScope.launch { - aiPreferencesRepository.setBaseUrl(AiProvider.CUSTOM, baseUrl) + val provider = AiProvider.fromString(aiProvider.value) + aiPreferencesRepository.setBaseUrl(provider, baseUrl) } } From 2b509c2f6b73efb0959f61de3c4b20849b2bd256 Mon Sep 17 00:00:00 2001 From: VoidX3D Date: Mon, 29 Jun 2026 01:28:26 +0545 Subject: [PATCH 34/73] fix(ai): strip markdown formatting from lyrics responses and dynamically scale maxTokens - Enhanced AiResponseCleaner.cleanTextResponse to strip paired **text**/__text__ markers - Strip conversational framing lines prepended by AI (Here is, Sure!, etc.) - Apply cleaning in AiStateHolder.translateLyrics before returning response - Dynamically scale maxTokens for LYRICS type based on input size (2x+ output, 4096 min, 16384 max) - Prevents aggressive cut-off and formatting markers in AI-translated lyrics --- .../theveloper/pixelplay/data/ai/AiHandler.kt | 14 +++++++++++++- .../pixelplay/data/ai/AiResponseCleaner.kt | 19 ++++++++++++++++++- .../presentation/viewmodel/AiStateHolder.kt | 4 +++- 3 files changed, 34 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/theveloper/pixelplay/data/ai/AiHandler.kt b/app/src/main/java/com/theveloper/pixelplay/data/ai/AiHandler.kt index c74e56b13f..08089561fd 100644 --- a/app/src/main/java/com/theveloper/pixelplay/data/ai/AiHandler.kt +++ b/app/src/main/java/com/theveloper/pixelplay/data/ai/AiHandler.kt @@ -170,6 +170,18 @@ class AiHandler @Inject constructor( context: String = "" ): String { val params = getGenerationParams() + val effectiveMaxTokens = if (type == AiSystemPromptType.LYRICS) { + // Lyrics translation needs more output tokens because each original + // line is followed by its translation (2x output). Also factor in + // the full lyrics text in the prompt. Use at least 4096, at most + // 16384, scaling linearly with input length. + val estimatedInputChars = prompt.length + val estimatedOutputChars = estimatedInputChars * 2 + val estimatedOutputTokens = (estimatedOutputChars / 4).coerceAtLeast(4096) + estimatedOutputTokens.coerceAtMost(16384) + } else { + params.maxTokens + } val effectiveTemperature = if (params.temperature == 0.7f) { if (temperature == 0.7f) { when (type) { @@ -231,7 +243,7 @@ class AiHandler @Inject constructor( temperature = effectiveTemperature, topP = params.topP, topK = params.topK, - maxTokens = params.maxTokens, + maxTokens = effectiveMaxTokens, presencePenalty = params.presencePenalty, frequencyPenalty = params.frequencyPenalty, ) diff --git a/app/src/main/java/com/theveloper/pixelplay/data/ai/AiResponseCleaner.kt b/app/src/main/java/com/theveloper/pixelplay/data/ai/AiResponseCleaner.kt index 92d6e27de8..2e315e4f72 100644 --- a/app/src/main/java/com/theveloper/pixelplay/data/ai/AiResponseCleaner.kt +++ b/app/src/main/java/com/theveloper/pixelplay/data/ai/AiResponseCleaner.kt @@ -21,10 +21,27 @@ object AiResponseCleaner { } fun cleanTextResponse(raw: String): String { - return raw + var cleaned = raw .replace("```text", "") .replace("```", "") .trim() + + // Remove conversational framing lines that AIs sometimes prepend + val framingPrefixes = listOf( + "Here is", "Here's", "Here are", "Sure", "Certainly", + "Of course", "I've", "I have", "The translated", "Translation:", + "Translated lyrics:", "Output:" + ) + val framingPattern = framingPrefixes.joinToString("|") { Regex.escape(it) } + cleaned = cleaned.replace(Regex("^(?:$framingPattern).*\\n?", RegexOption.MULTILINE), "") + + // Strip paired markdown formatting markers (**text** or __text__) + cleaned = cleaned + .replace(Regex("\\*\\*(.*?)\\*\\*"), "$1") + .replace(Regex("__(.*?)__"), "$1") + .trim() + + return cleaned } fun extractJsonArray(text: String): String? { diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/AiStateHolder.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/AiStateHolder.kt index 2f15469893..3f0d3f125f 100644 --- a/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/AiStateHolder.kt +++ b/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/AiStateHolder.kt @@ -6,6 +6,7 @@ import com.theveloper.pixelplay.R import com.theveloper.pixelplay.data.DailyMixManager import com.theveloper.pixelplay.data.ai.AiNotificationManager import com.theveloper.pixelplay.data.ai.AiPlaylistGenerator +import com.theveloper.pixelplay.data.ai.AiResponseCleaner import com.theveloper.pixelplay.data.ai.AiSystemPromptType import com.theveloper.pixelplay.data.ai.provider.AiProviderException import com.theveloper.pixelplay.data.preferences.PlaylistPreferencesRepository @@ -313,7 +314,8 @@ $lyricsText type = AiSystemPromptType.LYRICS, temperature = 0.1f ) - Result.success(response) + val cleaned = AiResponseCleaner.cleanTextResponse(response) + Result.success(cleaned) } catch (e: Exception) { Result.failure(e) } From a10cfe5724cbff6f4e0596fa1e1eb2694db046b9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 29 Jun 2026 22:28:03 +0000 Subject: [PATCH 35/73] chore(deps): bump the gradle-dependencies group with 6 updates Bumps the gradle-dependencies group with 6 updates: | Package | From | To | | --- | --- | --- | | [org.junit.platform:junit-platform-launcher](https://github.com/junit-team/junit-framework) | `6.1.0` | `6.1.1` | | [org.junit.jupiter:junit-jupiter-api](https://github.com/junit-team/junit-framework) | `6.1.0` | `6.1.1` | | [org.junit.jupiter:junit-jupiter-engine](https://github.com/junit-team/junit-framework) | `6.1.0` | `6.1.1` | | [org.junit.vintage:junit-vintage-engine](https://github.com/junit-team/junit-framework) | `6.1.0` | `6.1.1` | | [com.google.errorprone:error_prone_annotations](https://github.com/google/error-prone) | `2.36.0` | `2.50.0` | | [org.junit.jupiter:junit-jupiter-params](https://github.com/junit-team/junit-framework) | `6.1.0` | `6.1.1` | Updates `org.junit.platform:junit-platform-launcher` from 6.1.0 to 6.1.1 - [Release notes](https://github.com/junit-team/junit-framework/releases) - [Commits](https://github.com/junit-team/junit-framework/compare/r6.1.0...r6.1.1) Updates `org.junit.jupiter:junit-jupiter-api` from 6.1.0 to 6.1.1 - [Release notes](https://github.com/junit-team/junit-framework/releases) - [Commits](https://github.com/junit-team/junit-framework/compare/r6.1.0...r6.1.1) Updates `org.junit.jupiter:junit-jupiter-engine` from 6.1.0 to 6.1.1 - [Release notes](https://github.com/junit-team/junit-framework/releases) - [Commits](https://github.com/junit-team/junit-framework/compare/r6.1.0...r6.1.1) Updates `org.junit.vintage:junit-vintage-engine` from 6.1.0 to 6.1.1 - [Release notes](https://github.com/junit-team/junit-framework/releases) - [Commits](https://github.com/junit-team/junit-framework/compare/r6.1.0...r6.1.1) Updates `com.google.errorprone:error_prone_annotations` from 2.36.0 to 2.50.0 - [Release notes](https://github.com/google/error-prone/releases) - [Commits](https://github.com/google/error-prone/compare/v2.36.0...v2.50.0) Updates `org.junit.jupiter:junit-jupiter-params` from 6.1.0 to 6.1.1 - [Release notes](https://github.com/junit-team/junit-framework/releases) - [Commits](https://github.com/junit-team/junit-framework/compare/r6.1.0...r6.1.1) Updates `org.junit.jupiter:junit-jupiter-api` from 6.1.0 to 6.1.1 - [Release notes](https://github.com/junit-team/junit-framework/releases) - [Commits](https://github.com/junit-team/junit-framework/compare/r6.1.0...r6.1.1) Updates `org.junit.jupiter:junit-jupiter-engine` from 6.1.0 to 6.1.1 - [Release notes](https://github.com/junit-team/junit-framework/releases) - [Commits](https://github.com/junit-team/junit-framework/compare/r6.1.0...r6.1.1) Updates `org.junit.vintage:junit-vintage-engine` from 6.1.0 to 6.1.1 - [Release notes](https://github.com/junit-team/junit-framework/releases) - [Commits](https://github.com/junit-team/junit-framework/compare/r6.1.0...r6.1.1) --- updated-dependencies: - dependency-name: org.junit.platform:junit-platform-launcher dependency-version: 6.1.1 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: gradle-dependencies - dependency-name: org.junit.jupiter:junit-jupiter-api dependency-version: 6.1.1 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: gradle-dependencies - dependency-name: org.junit.jupiter:junit-jupiter-engine dependency-version: 6.1.1 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: gradle-dependencies - dependency-name: org.junit.vintage:junit-vintage-engine dependency-version: 6.1.1 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: gradle-dependencies - dependency-name: com.google.errorprone:error_prone_annotations dependency-version: 2.50.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: gradle-dependencies - dependency-name: org.junit.jupiter:junit-jupiter-params dependency-version: 6.1.1 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: gradle-dependencies - dependency-name: org.junit.jupiter:junit-jupiter-api dependency-version: 6.1.1 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: gradle-dependencies - dependency-name: org.junit.jupiter:junit-jupiter-engine dependency-version: 6.1.1 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: gradle-dependencies - dependency-name: org.junit.vintage:junit-vintage-engine dependency-version: 6.1.1 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: gradle-dependencies ... Signed-off-by: dependabot[bot] --- gradle/libs.versions.toml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index a186d46e62..68204db386 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -26,7 +26,7 @@ kotlin = "2.4.0" coreKtx = "1.19.0" junit = "4.13.2" junitVersion = "1.3.0" -junitJupiter = "6.1.0" +junitJupiter = "6.1.1" espressoCore = "3.7.0" kotlinx-coroutines = "1.11.0" kotlinxCollectionsImmutable = "0.5.0" @@ -59,7 +59,7 @@ datastore = "1.2.1" credentials = "1.6.0" googleid = "1.2.0" androidxTestCore = "1.7.0" -junit5 = "6.1.0" +junit5 = "6.1.1" kuromoji = "0.9.0" pinyin4j = "2.5.1" securityCrypto = "1.1.0" @@ -70,7 +70,7 @@ jdom2 = "2.0.6.1" jose4j = "0.9.6" httpclient = "4.5.14" -errorProneAnnotations = "2.36.0" +errorProneAnnotations = "2.50.0" # DI dagger = "2.60" From b6517b9ed4a44ec743e2bdf6a0e432e53c013176 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 2 Jul 2026 22:27:29 +0000 Subject: [PATCH 36/73] chore(deps): bump the gradle-dependencies group across 1 directory with 22 updates Bumps the gradle-dependencies group with 22 updates in the / directory: | Package | From | To | | --- | --- | --- | | androidx.compose.ui:ui-text-google-fonts | `1.11.3` | `1.11.4` | | androidx.compose.ui:ui | `1.11.3` | `1.11.4` | | androidx.compose.ui:ui-graphics | `1.11.3` | `1.11.4` | | androidx.compose.ui:ui-tooling | `1.11.3` | `1.11.4` | | androidx.compose.ui:ui-tooling-preview | `1.11.3` | `1.11.4` | | androidx.compose.ui:ui-test-manifest | `1.11.3` | `1.11.4` | | androidx.compose.ui:ui-test-junit4 | `1.11.3` | `1.11.4` | | androidx.compose.animation:animation | `1.11.3` | `1.11.4` | | androidx.compose.foundation:foundation | `1.11.3` | `1.11.4` | | androidx.glance:glance | `1.3.0-alpha01` | `1.3.0-alpha02` | | androidx.glance:glance-appwidget | `1.3.0-alpha01` | `1.3.0-alpha02` | | androidx.glance:glance-material3 | `1.3.0-alpha01` | `1.3.0-alpha02` | | androidx.hilt:hilt-lifecycle-viewmodel-compose | `1.3.0` | `1.4.0` | | androidx.hilt:hilt-navigation-compose | `1.3.0` | `1.4.0` | | androidx.hilt:hilt-work | `1.3.0` | `1.4.0` | | androidx.hilt:hilt-compiler | `1.3.0` | `1.4.0` | | androidx.compose:compose-bom | `2026.06.00` | `2026.06.01` | | [org.jetbrains.kotlinx:kotlinx-collections-immutable](https://github.com/Kotlin/kotlinx.collections.immutable) | `0.5.0` | `0.5.1` | | androidx.compose.material3:material3 | `1.5.0-alpha22` | `1.5.0-alpha23` | | [org.checkerframework:checker-qual](https://github.com/typetools/checker-framework) | `4.2.0` | `4.2.1` | | androidx.test.uiautomator:uiautomator | `2.3.0` | `2.4.0` | | androidx.baselineprofile | `1.5.0-alpha06` | `1.5.0-alpha07` | Updates `androidx.compose.ui:ui-text-google-fonts` from 1.11.3 to 1.11.4 Updates `androidx.compose.ui:ui` from 1.11.3 to 1.11.4 Updates `androidx.compose.ui:ui-graphics` from 1.11.3 to 1.11.4 Updates `androidx.compose.ui:ui-tooling` from 1.11.3 to 1.11.4 Updates `androidx.compose.ui:ui-tooling-preview` from 1.11.3 to 1.11.4 Updates `androidx.compose.ui:ui-test-manifest` from 1.11.3 to 1.11.4 Updates `androidx.compose.ui:ui-test-junit4` from 1.11.3 to 1.11.4 Updates `androidx.compose.animation:animation` from 1.11.3 to 1.11.4 Updates `androidx.compose.foundation:foundation` from 1.11.3 to 1.11.4 Updates `androidx.glance:glance` from 1.3.0-alpha01 to 1.3.0-alpha02 Updates `androidx.glance:glance-appwidget` from 1.3.0-alpha01 to 1.3.0-alpha02 Updates `androidx.glance:glance-material3` from 1.3.0-alpha01 to 1.3.0-alpha02 Updates `androidx.glance:glance-appwidget` from 1.3.0-alpha01 to 1.3.0-alpha02 Updates `androidx.glance:glance-material3` from 1.3.0-alpha01 to 1.3.0-alpha02 Updates `androidx.hilt:hilt-lifecycle-viewmodel-compose` from 1.3.0 to 1.4.0 Updates `androidx.hilt:hilt-navigation-compose` from 1.3.0 to 1.4.0 Updates `androidx.hilt:hilt-work` from 1.3.0 to 1.4.0 Updates `androidx.hilt:hilt-compiler` from 1.3.0 to 1.4.0 Updates `androidx.hilt:hilt-navigation-compose` from 1.3.0 to 1.4.0 Updates `androidx.hilt:hilt-work` from 1.3.0 to 1.4.0 Updates `androidx.hilt:hilt-compiler` from 1.3.0 to 1.4.0 Updates `androidx.compose:compose-bom` from 2026.06.00 to 2026.06.01 Updates `androidx.compose.ui:ui` from 1.11.3 to 1.11.4 Updates `androidx.compose.ui:ui-graphics` from 1.11.3 to 1.11.4 Updates `androidx.compose.ui:ui-tooling` from 1.11.3 to 1.11.4 Updates `androidx.compose.ui:ui-tooling-preview` from 1.11.3 to 1.11.4 Updates `androidx.compose.ui:ui-test-manifest` from 1.11.3 to 1.11.4 Updates `androidx.compose.ui:ui-test-junit4` from 1.11.3 to 1.11.4 Updates `org.jetbrains.kotlinx:kotlinx-collections-immutable` from 0.5.0 to 0.5.1 - [Release notes](https://github.com/Kotlin/kotlinx.collections.immutable/releases) - [Changelog](https://github.com/Kotlin/kotlinx.collections.immutable/blob/master/CHANGELOG.md) - [Commits](https://github.com/Kotlin/kotlinx.collections.immutable/compare/v0.5.0...v0.5.1) Updates `androidx.compose.material3:material3` from 1.5.0-alpha22 to 1.5.0-alpha23 Updates `org.checkerframework:checker-qual` from 4.2.0 to 4.2.1 - [Release notes](https://github.com/typetools/checker-framework/releases) - [Changelog](https://github.com/typetools/checker-framework/blob/master/docs/CHANGELOG.md) - [Commits](https://github.com/typetools/checker-framework/compare/checker-framework-4.2.0...checker-framework-4.2.1) Updates `androidx.test.uiautomator:uiautomator` from 2.3.0 to 2.4.0 Updates `androidx.baselineprofile` from 1.5.0-alpha06 to 1.5.0-alpha07 --- updated-dependencies: - dependency-name: androidx.compose.ui:ui-text-google-fonts dependency-version: 1.11.4 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: gradle-dependencies - dependency-name: androidx.compose.ui:ui dependency-version: 1.11.4 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: gradle-dependencies - dependency-name: androidx.compose.ui:ui-graphics dependency-version: 1.11.4 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: gradle-dependencies - dependency-name: androidx.compose.ui:ui-tooling dependency-version: 1.11.4 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: gradle-dependencies - dependency-name: androidx.compose.ui:ui-tooling-preview dependency-version: 1.11.4 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: gradle-dependencies - dependency-name: androidx.compose.ui:ui-test-manifest dependency-version: 1.11.4 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: gradle-dependencies - dependency-name: androidx.compose.ui:ui-test-junit4 dependency-version: 1.11.4 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: gradle-dependencies - dependency-name: androidx.compose.animation:animation dependency-version: 1.11.4 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: gradle-dependencies - dependency-name: androidx.compose.foundation:foundation dependency-version: 1.11.4 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: gradle-dependencies - dependency-name: androidx.glance:glance dependency-version: 1.3.0-alpha02 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: gradle-dependencies - dependency-name: androidx.glance:glance-appwidget dependency-version: 1.3.0-alpha02 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: gradle-dependencies - dependency-name: androidx.glance:glance-material3 dependency-version: 1.3.0-alpha02 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: gradle-dependencies - dependency-name: androidx.glance:glance-appwidget dependency-version: 1.3.0-alpha02 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: gradle-dependencies - dependency-name: androidx.glance:glance-material3 dependency-version: 1.3.0-alpha02 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: gradle-dependencies - dependency-name: androidx.hilt:hilt-lifecycle-viewmodel-compose dependency-version: 1.4.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: gradle-dependencies - dependency-name: androidx.hilt:hilt-navigation-compose dependency-version: 1.4.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: gradle-dependencies - dependency-name: androidx.hilt:hilt-work dependency-version: 1.4.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: gradle-dependencies - dependency-name: androidx.hilt:hilt-compiler dependency-version: 1.4.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: gradle-dependencies - dependency-name: androidx.hilt:hilt-navigation-compose dependency-version: 1.4.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: gradle-dependencies - dependency-name: androidx.hilt:hilt-work dependency-version: 1.4.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: gradle-dependencies - dependency-name: androidx.hilt:hilt-compiler dependency-version: 1.4.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: gradle-dependencies - dependency-name: androidx.compose:compose-bom dependency-version: 2026.06.01 dependency-type: direct:production dependency-group: gradle-dependencies - dependency-name: androidx.compose.ui:ui dependency-version: 1.11.4 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: gradle-dependencies - dependency-name: androidx.compose.ui:ui-graphics dependency-version: 1.11.4 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: gradle-dependencies - dependency-name: androidx.compose.ui:ui-tooling dependency-version: 1.11.4 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: gradle-dependencies - dependency-name: androidx.compose.ui:ui-tooling-preview dependency-version: 1.11.4 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: gradle-dependencies - dependency-name: androidx.compose.ui:ui-test-manifest dependency-version: 1.11.4 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: gradle-dependencies - dependency-name: androidx.compose.ui:ui-test-junit4 dependency-version: 1.11.4 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: gradle-dependencies - dependency-name: org.jetbrains.kotlinx:kotlinx-collections-immutable dependency-version: 0.5.1 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: gradle-dependencies - dependency-name: androidx.compose.material3:material3 dependency-version: 1.5.0-alpha23 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: gradle-dependencies - dependency-name: org.checkerframework:checker-qual dependency-version: 4.2.1 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: gradle-dependencies - dependency-name: androidx.test.uiautomator:uiautomator dependency-version: 2.4.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: gradle-dependencies - dependency-name: androidx.baselineprofile dependency-version: 1.5.0-alpha07 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: gradle-dependencies ... Signed-off-by: dependabot[bot] --- gradle/libs.versions.toml | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 68204db386..062e40f890 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -4,23 +4,23 @@ agp = "9.2.1" app = "1.7.0" googleGenai = "1.60.0" googlePlayServicesCast = "22.3.1" -animation = "1.11.3" +animation = "1.11.4" appcompat = "1.7.1" capturable = "3.0.1" codeview = "1.3.9" coilCompose = "2.7.0" composeMaterialIcons = "1.7.8" -composeUi = "1.11.3" +composeUi = "1.11.4" constraintlayoutCompose = "1.1.1" coreSplashscreen = "1.2.0" desugarJdkLibs = "2.1.5" duktapeAndroid = "1.4.0" -foundation = "1.11.3" -glance = "1.3.0-alpha01" +foundation = "1.11.4" +glance = "1.3.0-alpha02" graphicsShapes = "1.1.0" gson = "2.14.0" hiltAndroid = "2.60" -hiltNavigationCompose = "1.3.0" +hiltNavigationCompose = "1.4.0" ktor = "3.5.1" kotlin = "2.4.0" coreKtx = "1.19.0" @@ -29,13 +29,13 @@ junitVersion = "1.3.0" junitJupiter = "6.1.1" espressoCore = "3.7.0" kotlinx-coroutines = "1.11.0" -kotlinxCollectionsImmutable = "0.5.0" +kotlinxCollectionsImmutable = "0.5.1" kotlinxSerializationJson = "1.11.0" lifecycleRuntimeKtx = "2.11.0" activityCompose = "1.13.0" -composeBom = "2026.06.00" +composeBom = "2026.06.01" material = "1.14.0" -material3 = "1.5.0-alpha22" +material3 = "1.5.0-alpha23" media = "1.8.0" media3Session = "1.10.1" media3Transformer = "1.10.1" @@ -51,7 +51,7 @@ roomCompiler = "2.8.4" roomKtx = "2.8.4" roomRuntime = "2.8.4" accompanist = "0.37.3" -checkerframework = "4.2.0" # O la versión más reciente que encuentres +checkerframework = "4.2.1" # O la versión más reciente que encuentres taglib = "1.0.6" jaudiotagger = "3.0.1" vorbisjava = "0.8" @@ -97,9 +97,9 @@ okhttp = "5.4.0" mediarouterVersion = "1.8.1" playServicesCastFramework = "22.3.1" navigationRuntimeKtx = "2.9.8" -uiautomator = "2.3.0" +uiautomator = "2.4.0" benchmarkMacroJunit4 = "1.4.1" -baselineprofile = "1.5.0-alpha06" +baselineprofile = "1.5.0-alpha07" profileinstaller = "1.4.1" pagingCommon = "3.3.6" From 523c8f9b826cadbe4bff9451518c0e33b54af74c Mon Sep 17 00:00:00 2001 From: Dae Euhwa Date: Sun, 5 Jul 2026 15:18:55 -0500 Subject: [PATCH 37/73] Fix: CJK token scaling, embedded lyrics reading, local .lrc Unicode matching, romanization detection --- .../com/theveloper/pixelplay/data/ai/AiHandler.kt | 8 +++----- .../data/repository/LyricsRepositoryImpl.kt | 4 ++-- .../presentation/viewmodel/LyricsStateHolder.kt | 13 +++++++------ .../com/theveloper/pixelplay/utils/LyricsUtils.kt | 1 + 4 files changed, 13 insertions(+), 13 deletions(-) diff --git a/app/src/main/java/com/theveloper/pixelplay/data/ai/AiHandler.kt b/app/src/main/java/com/theveloper/pixelplay/data/ai/AiHandler.kt index 08089561fd..47069b938c 100644 --- a/app/src/main/java/com/theveloper/pixelplay/data/ai/AiHandler.kt +++ b/app/src/main/java/com/theveloper/pixelplay/data/ai/AiHandler.kt @@ -171,13 +171,11 @@ class AiHandler @Inject constructor( ): String { val params = getGenerationParams() val effectiveMaxTokens = if (type == AiSystemPromptType.LYRICS) { - // Lyrics translation needs more output tokens because each original - // line is followed by its translation (2x output). Also factor in - // the full lyrics text in the prompt. Use at least 4096, at most - // 16384, scaling linearly with input length. val estimatedInputChars = prompt.length val estimatedOutputChars = estimatedInputChars * 2 - val estimatedOutputTokens = (estimatedOutputChars / 4).coerceAtLeast(4096) + val cjkRatio = prompt.count { it.code in 0x4E00..0x9FFF || it.code in 0x3040..0x30FF || it.code in 0xAC00..0xD7AF }.toFloat() / estimatedInputChars.coerceAtLeast(1) + val charsPerToken = (4f - cjkRatio * 3f).coerceIn(1f, 4f) + val estimatedOutputTokens = (estimatedOutputChars / charsPerToken).toInt().coerceAtLeast(4096) estimatedOutputTokens.coerceAtMost(16384) } else { params.maxTokens diff --git a/app/src/main/java/com/theveloper/pixelplay/data/repository/LyricsRepositoryImpl.kt b/app/src/main/java/com/theveloper/pixelplay/data/repository/LyricsRepositoryImpl.kt index 61e1b6982c..8a9ca81daa 100644 --- a/app/src/main/java/com/theveloper/pixelplay/data/repository/LyricsRepositoryImpl.kt +++ b/app/src/main/java/com/theveloper/pixelplay/data/repository/LyricsRepositoryImpl.kt @@ -1014,8 +1014,8 @@ class LyricsRepositoryImpl @Inject constructor( } } - val cleanArtist = song.displayArtist.replace(Regex("[^a-zA-Z0-9]"), "_") - val cleanTitle = song.title.replace(Regex("[^a-zA-Z0-9]"), "_") + val cleanArtist = song.displayArtist.replace(Regex("[^\\p{L}\\p{N}]"), "_") + val cleanTitle = song.title.replace(Regex("[^\\p{L}\\p{N}]"), "_") for (extension in LyricsImportSecurity.supportedFileExtensions()) { val alternativeLyricsFile = File(directory, "${cleanArtist}_${cleanTitle}.$extension") diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/LyricsStateHolder.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/LyricsStateHolder.kt index de7a1a4a28..7ac9fe9d7f 100644 --- a/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/LyricsStateHolder.kt +++ b/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/LyricsStateHolder.kt @@ -428,17 +428,18 @@ class LyricsStateHolder @Inject constructor( } private fun readEmbeddedLyricsFromFile(song: Song): String? { - song.lyrics - ?.trim() - ?.takeIf { it.isNotBlank() } - ?.let { return it } - - return runCatching { + val fileLyrics = runCatching { AudioMetadataReader.read(File(song.path)) ?.lyrics ?.trim() ?.takeIf { it.isNotBlank() } }.getOrNull() + + if (fileLyrics != null) return fileLyrics + + return song.lyrics + ?.trim() + ?.takeIf { it.isNotBlank() } } private fun readLocalLyricsFile(song: Song): String? { diff --git a/app/src/main/java/com/theveloper/pixelplay/utils/LyricsUtils.kt b/app/src/main/java/com/theveloper/pixelplay/utils/LyricsUtils.kt index f1045eb700..d225c7e178 100644 --- a/app/src/main/java/com/theveloper/pixelplay/utils/LyricsUtils.kt +++ b/app/src/main/java/com/theveloper/pixelplay/utils/LyricsUtils.kt @@ -130,6 +130,7 @@ object MultiLangRomanizer { } fun isScriptThatNeedsRomanization(text: String): Boolean { + if (isVietnamese(text) || isThai(text) || isArabic(text) || isGreek(text) || isHebrew(text)) return true return text.any { char -> val code = char.code code in 0x3040..0x309F || // Hiragana From 1a0361dd11198ba98db3ba1d2aed85ad9a486c99 Mon Sep 17 00:00:00 2001 From: Dae Euhwa Date: Sun, 5 Jul 2026 15:50:34 -0500 Subject: [PATCH 38/73] fix: suppress NonObservableLocale lint in AiUsage date format --- .../pixelplay/presentation/screens/SettingsCategoryScreen.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/screens/SettingsCategoryScreen.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/screens/SettingsCategoryScreen.kt index 8de454b763..46f1c540a1 100644 --- a/app/src/main/java/com/theveloper/pixelplay/presentation/screens/SettingsCategoryScreen.kt +++ b/app/src/main/java/com/theveloper/pixelplay/presentation/screens/SettingsCategoryScreen.kt @@ -1122,6 +1122,7 @@ fun SettingsCategoryScreen( .fillMaxWidth() .padding(top = 8.dp, bottom = 8.dp) ) { + @Suppress("NonObservableLocale") val dateFormat = SimpleDateFormat("MMMM d, yyyy", Locale.getDefault()) val groupedUsage = recentAiUsage.groupBy { dateFormat.format(Date(it.timestamp)) From f774525508f5c8948bb94fe80541587cef368814 Mon Sep 17 00:00:00 2001 From: Dae Euhwa Date: Mon, 6 Jul 2026 04:17:52 -0500 Subject: [PATCH 39/73] fix: lyrics translation cutoff, false already-translated, search dialog stuck, non-Latin search - Increase lyrics translation token cap from 16384 to 32768 - Use 3x output estimation (original+translation per line) - Expand CJK detection to include Thai, Arabic, Greek, Hebrew, Hindi, Punjabi - Remove false 'already translated' guard that checked stale UI state - Reorder lyrics source priority: local file > embedded > DB > song.lyrics - Close lyrics search dialog on Success (sync searchUiState with dialog visibility) - Add romanized artist search strategies for non-Latin scripts - Improve translation prompt to handle both synced and plain (unsynced) lyrics - Fix fetchFromRemote missing return@withContext for exact-match - Fix bulk LRC scanner to use Unicode-aware regex - Fix thinking model detection false positives - Add AI response cache cleanup (7-day TTL) - Consolidate duplicate model filtering (UnifiedModelFilter) - Move Gemini API key from URL query param to HTTP header - Remove dead code (countTokens, duplicate clearAll, FQN imports) --- .../theveloper/pixelplay/data/ai/AiHandler.kt | 36 +++++++++++------- .../pixelplay/data/ai/GeminiModelService.kt | 18 +++------ .../pixelplay/data/ai/provider/AiClient.kt | 5 --- .../data/ai/provider/GeminiAiClient.kt | 38 ------------------- .../data/ai/provider/GenericOpenAiClient.kt | 5 --- .../backup/module/AiUsageBackupHandler.kt | 2 +- .../pixelplay/data/database/AiUsageDao.kt | 3 -- .../data/repository/LyricsRepositoryImpl.kt | 37 ++++++++++++++++-- .../presentation/components/LyricsSheet.kt | 7 +++- .../presentation/viewmodel/AiStateHolder.kt | 31 ++++++++++++--- .../viewmodel/LyricsStateHolder.kt | 12 ++---- 11 files changed, 95 insertions(+), 99 deletions(-) diff --git a/app/src/main/java/com/theveloper/pixelplay/data/ai/AiHandler.kt b/app/src/main/java/com/theveloper/pixelplay/data/ai/AiHandler.kt index 47069b938c..6bdd82c551 100644 --- a/app/src/main/java/com/theveloper/pixelplay/data/ai/AiHandler.kt +++ b/app/src/main/java/com/theveloper/pixelplay/data/ai/AiHandler.kt @@ -1,8 +1,11 @@ package com.theveloper.pixelplay.data.ai +import com.theveloper.pixelplay.data.ai.provider.AiClient import com.theveloper.pixelplay.data.ai.provider.AiClientFactory import com.theveloper.pixelplay.data.ai.provider.AiProvider +import com.theveloper.pixelplay.data.ai.provider.AiProviderException +import com.theveloper.pixelplay.data.ai.provider.AiProviderSupport import com.theveloper.pixelplay.data.database.AiCacheDao import com.theveloper.pixelplay.data.database.AiCacheEntity import com.theveloper.pixelplay.data.preferences.AiPreferencesRepository @@ -12,6 +15,7 @@ import com.theveloper.pixelplay.di.AppScope import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch +import kotlinx.coroutines.TimeoutCancellationException import kotlinx.coroutines.withTimeout import timber.log.Timber import java.security.MessageDigest @@ -115,8 +119,8 @@ class AiHandler @Inject constructor( topP, topK, maxTokens, presencePenalty, frequencyPenalty, ) } - } catch (e: kotlinx.coroutines.TimeoutCancellationException) { - throw com.theveloper.pixelplay.data.ai.provider.AiProviderSupport.createException( + } catch (e: TimeoutCancellationException) { + throw AiProviderSupport.createException( providerName = provider.displayName, statusCode = null, transportMessage = "Request timed out after ${REQUEST_TIMEOUT_MS / 1000}s. The model may be overloaded.", @@ -130,7 +134,7 @@ class AiHandler @Inject constructor( val response = callWithModel(requestedModel) GenerationResult(response, requestedModel) } catch (e: Exception) { - val failure = com.theveloper.pixelplay.data.ai.provider.AiProviderSupport.wrapThrowable( + val failure = AiProviderSupport.wrapThrowable( provider.displayName, e, requestedModel ) @@ -147,13 +151,13 @@ class AiHandler @Inject constructor( provider: AiProvider, apiKey: String, requestedModel: String, - client: com.theveloper.pixelplay.data.ai.provider.AiClient, - failure: com.theveloper.pixelplay.data.ai.provider.AiProviderException + client: AiClient, + failure: AiProviderException ): String? { if (!failure.isModelUnavailable()) return null val availableModels = runCatching { client.getAvailableModels(apiKey) }.getOrDefault(emptyList()) - val recoveredModel = com.theveloper.pixelplay.data.ai.provider.AiProviderSupport.selectRecoveryModel( + val recoveredModel = AiProviderSupport.selectRecoveryModel( currentModel = requestedModel, defaultModel = client.getDefaultModel(), availableModels = availableModels @@ -172,11 +176,12 @@ class AiHandler @Inject constructor( val params = getGenerationParams() val effectiveMaxTokens = if (type == AiSystemPromptType.LYRICS) { val estimatedInputChars = prompt.length - val estimatedOutputChars = estimatedInputChars * 2 - val cjkRatio = prompt.count { it.code in 0x4E00..0x9FFF || it.code in 0x3040..0x30FF || it.code in 0xAC00..0xD7AF }.toFloat() / estimatedInputChars.coerceAtLeast(1) + // Translation doubles output (original + translated per line), plus formatting overhead + val estimatedOutputChars = estimatedInputChars * 3 + val cjkRatio = prompt.count { it.code in 0x4E00..0x9FFF || it.code in 0x3040..0x30FF || it.code in 0xAC00..0xD7AF || it.code in 0x0E00..0x0E7F || it.code in 0x0600..0x06FF || it.code in 0x0370..0x03FF || it.code in 0x0590..0x05FF || it.code in 0x0900..0x097F || it.code in 0x0A00..0x0A7F }.toFloat() / estimatedInputChars.coerceAtLeast(1) val charsPerToken = (4f - cjkRatio * 3f).coerceIn(1f, 4f) - val estimatedOutputTokens = (estimatedOutputChars / charsPerToken).toInt().coerceAtLeast(4096) - estimatedOutputTokens.coerceAtMost(16384) + val estimatedOutputTokens = (estimatedOutputChars / charsPerToken).toInt().coerceAtLeast(8192) + estimatedOutputTokens.coerceAtMost(32768) } else { params.maxTokens } @@ -213,7 +218,7 @@ class AiHandler @Inject constructor( val now = System.currentTimeMillis() providerCooldowns.entries.removeIf { it.value < now } - val providersToTry = com.theveloper.pixelplay.data.ai.provider.AiProviderSupport.buildProviderChain(userProvider) + val providersToTry = AiProviderSupport.buildProviderChain(userProvider) val failedProviders = mutableListOf() for (provider in providersToTry) { @@ -251,7 +256,7 @@ class AiHandler @Inject constructor( continue } - val isThinkingModel = finalSystemPrompt.contains("think", true) || provider.name.contains("reasoning", true) + val isThinkingModel = result.modelUsed.contains("think", true) || result.modelUsed.contains("reason", true) || provider.name.contains("reasoning", true) val estimatedPromptTokens = (finalSystemPrompt.length + prompt.length) / 4 val estimatedOutputTokens = result.response.length / 4 val estimatedThoughtTokens = if (isThinkingModel) (estimatedOutputTokens * 1.5).toInt() else 0 @@ -275,10 +280,15 @@ class AiHandler @Inject constructor( } cacheDao.insert(AiCacheEntity(promptHash = hash, responseJson = result.response, timestamp = System.currentTimeMillis())) + + // Evict cache entries older than 7 days + val sevenDaysAgo = System.currentTimeMillis() - (7 * 24 * 60 * 60 * 1000L) + try { cacheDao.clearOldCache(sevenDaysAgo) } catch (_: Exception) {} + return result.response } catch (e: Exception) { // AI Optimization: Robust failover logic—if one provider fails, we log and try the next in the chain - val failure = com.theveloper.pixelplay.data.ai.provider.AiProviderSupport.wrapThrowable(provider.displayName, e) + val failure = AiProviderSupport.wrapThrowable(provider.displayName, e) Timber.tag("AiHandler").w(e, "Provider ${provider.name} failed: ${failure.message}") failedProviders.add("${provider.name}: ${failure.message ?: "Unknown error"}") // Trigger cooldown only on provider-level outages and account problems. diff --git a/app/src/main/java/com/theveloper/pixelplay/data/ai/GeminiModelService.kt b/app/src/main/java/com/theveloper/pixelplay/data/ai/GeminiModelService.kt index c97959f5c3..3fbbaf1c2e 100644 --- a/app/src/main/java/com/theveloper/pixelplay/data/ai/GeminiModelService.kt +++ b/app/src/main/java/com/theveloper/pixelplay/data/ai/GeminiModelService.kt @@ -1,5 +1,6 @@ package com.theveloper.pixelplay.data.ai +import com.theveloper.pixelplay.data.ai.provider.UnifiedModelFilter import com.theveloper.pixelplay.data.repository.MusicRepository import com.theveloper.pixelplay.data.worker.AiWorkerManager import kotlinx.coroutines.Dispatchers @@ -22,12 +23,7 @@ class GeminiModelService @Inject constructor( ) { companion object { - // Markers for models that cannot perform text chat generation. These are the - // only things we filter out — every other model the API returns is selectable. - private val NON_CHAT_MARKERS = listOf( - "embedding", "aqa", "imagen", "image-generation", - "tts", "audio", "veo", "vision-only", "learnlm-embedding" - ) + // Use UnifiedModelFilter for consistent model filtering across all providers. } suspend fun fetchAvailableModels(apiKey: String): Result> { @@ -48,10 +44,11 @@ class GeminiModelService @Inject constructor( private suspend fun makeModelsListRequest(apiKey: String): List { return withContext(Dispatchers.IO) { try { - val url = "https://generativelanguage.googleapis.com/v1beta/models?key=$apiKey" + val url = "https://generativelanguage.googleapis.com/v1beta/models" val connection = java.net.URL(url).openConnection() as java.net.HttpURLConnection connection.requestMethod = "GET" + connection.setRequestProperty("x-goog-api-key", apiKey) connection.connectTimeout = 10000 connection.readTimeout = 10000 @@ -88,7 +85,7 @@ class GeminiModelService @Inject constructor( // version — let the user pick any chat-capable model their key supports. if ((modelName.startsWith("gemini", ignoreCase = true) || modelName.startsWith("gemma", ignoreCase = true)) && - !isNonChatModel(modelName)) { + UnifiedModelFilter.isModelUsableForChat(modelName)) { models.add(GeminiModel( name = modelName, displayName = formatDisplayName(modelName) @@ -131,11 +128,6 @@ class GeminiModelService @Inject constructor( } } - private fun isNonChatModel(modelName: String): Boolean { - val lower = modelName.lowercase() - return NON_CHAT_MARKERS.any { lower.contains(it) } - } - private fun formatDisplayName(modelName: String): String { return modelName .split("-") diff --git a/app/src/main/java/com/theveloper/pixelplay/data/ai/provider/AiClient.kt b/app/src/main/java/com/theveloper/pixelplay/data/ai/provider/AiClient.kt index 348547ee68..430c425f4c 100644 --- a/app/src/main/java/com/theveloper/pixelplay/data/ai/provider/AiClient.kt +++ b/app/src/main/java/com/theveloper/pixelplay/data/ai/provider/AiClient.kt @@ -18,11 +18,6 @@ interface AiClient { frequencyPenalty: Float = 0.0f ): String - /** - * Estimate or count tokens for a given prompt - */ - suspend fun countTokens(model: String, systemPrompt: String, prompt: String): Int - /** * Get list of available models for this provider */ diff --git a/app/src/main/java/com/theveloper/pixelplay/data/ai/provider/GeminiAiClient.kt b/app/src/main/java/com/theveloper/pixelplay/data/ai/provider/GeminiAiClient.kt index cee7e23a45..be42fb9f85 100644 --- a/app/src/main/java/com/theveloper/pixelplay/data/ai/provider/GeminiAiClient.kt +++ b/app/src/main/java/com/theveloper/pixelplay/data/ai/provider/GeminiAiClient.kt @@ -165,44 +165,6 @@ class GeminiAiClient(private val apiKey: String) : AiClient { } } - override suspend fun countTokens(model: String, systemPrompt: String, prompt: String): Int { - return withContext(Dispatchers.IO) { - val resolvedModel = model.ifBlank { DEFAULT_GEMINI_MODEL } - try { - val requestBody = GenerateRequest( - contents = listOf(Content(role = "user", parts = listOf(Part(prompt)))), - systemInstruction = systemPrompt - .takeIf { it.isNotBlank() } - ?.let { Content(parts = listOf(Part(it))) }, - generationConfig = GenerationConfig(temperature = 0.0) - ) - val jsonBody = json.encodeToString(GenerateRequest.serializer(), requestBody) - val body = jsonBody.toRequestBody("application/json".toMediaType()) - - val request = Request.Builder() - .url("$BASE_URL/models/$resolvedModel:countTokens") - .addHeader("x-goog-api-key", apiKey) - .addHeader("Content-Type", "application/json") - .post(body) - .build() - - httpClient.newCall(request).execute().use { response -> - if (!response.isSuccessful) { - return@withContext (prompt.length / 4) + (systemPrompt.length / 4) - } - val totalTokens = Regex(""""totalTokens"\s*:\s*(\d+)""") - .find(response.body.string()) - ?.groupValues - ?.getOrNull(1) - ?.toIntOrNull() - totalTokens ?: ((prompt.length / 4) + (systemPrompt.length / 4)) - } - } catch (e: Exception) { - (prompt.length / 4) + (systemPrompt.length / 4) - } - } - } - override suspend fun getAvailableModels(apiKey: String): List { return withContext(Dispatchers.IO) { try { diff --git a/app/src/main/java/com/theveloper/pixelplay/data/ai/provider/GenericOpenAiClient.kt b/app/src/main/java/com/theveloper/pixelplay/data/ai/provider/GenericOpenAiClient.kt index fd0fb1c1d8..c31fb83e8e 100644 --- a/app/src/main/java/com/theveloper/pixelplay/data/ai/provider/GenericOpenAiClient.kt +++ b/app/src/main/java/com/theveloper/pixelplay/data/ai/provider/GenericOpenAiClient.kt @@ -132,11 +132,6 @@ class GenericOpenAiClient( } } - override suspend fun countTokens(model: String, systemPrompt: String, prompt: String): Int { - // Estimation for generic providers - return (systemPrompt.length + prompt.length) / 4 - } - override suspend fun getAvailableModels(apiKey: String): List { return withContext(Dispatchers.IO) { try { diff --git a/app/src/main/java/com/theveloper/pixelplay/data/backup/module/AiUsageBackupHandler.kt b/app/src/main/java/com/theveloper/pixelplay/data/backup/module/AiUsageBackupHandler.kt index 6805e692fd..7fbf6792c1 100644 --- a/app/src/main/java/com/theveloper/pixelplay/data/backup/module/AiUsageBackupHandler.kt +++ b/app/src/main/java/com/theveloper/pixelplay/data/backup/module/AiUsageBackupHandler.kt @@ -34,7 +34,7 @@ class AiUsageBackupHandler @Inject constructor( // We additive restore AI logs or clear? Usually for logs we additive restore // but BackupModuleHandler documentation says "Clear existing data and restore". - aiUsageDao.clearAll() + aiUsageDao.clearUsage() if (logs.isNotEmpty()) { aiUsageDao.insertAll(logs) } diff --git a/app/src/main/java/com/theveloper/pixelplay/data/database/AiUsageDao.kt b/app/src/main/java/com/theveloper/pixelplay/data/database/AiUsageDao.kt index 28a7503f34..705c3f2eec 100644 --- a/app/src/main/java/com/theveloper/pixelplay/data/database/AiUsageDao.kt +++ b/app/src/main/java/com/theveloper/pixelplay/data/database/AiUsageDao.kt @@ -33,7 +33,4 @@ interface AiUsageDao { @Query("DELETE FROM ai_usage") suspend fun clearUsage() - - @Query("DELETE FROM ai_usage") - suspend fun clearAll() } diff --git a/app/src/main/java/com/theveloper/pixelplay/data/repository/LyricsRepositoryImpl.kt b/app/src/main/java/com/theveloper/pixelplay/data/repository/LyricsRepositoryImpl.kt index 8a9ca81daa..33c266a0e6 100644 --- a/app/src/main/java/com/theveloper/pixelplay/data/repository/LyricsRepositoryImpl.kt +++ b/app/src/main/java/com/theveloper/pixelplay/data/repository/LyricsRepositoryImpl.kt @@ -527,6 +527,15 @@ class LyricsRepositoryImpl @Inject constructor( }) } } + // Romanized search strategy for non-Latin artists + if (MultiLangRomanizer.isScriptThatNeedsRomanization(cleanArtist)) { + val romanArtist = romanizeForMatch(cleanArtist) + if (romanArtist != cleanArtist) { + add(RemoteSearchStrategy("romanized_artist") { + lrcLibApiService.searchLyrics(trackName = cleanTitle, artistName = romanArtist) + }) + } + } // Smart title cleanup strategy (removes leading digits/spaces and truncates at -, (, )) val smartTitle = cleanTitleSmart(cleanTitle) @@ -1364,7 +1373,7 @@ class LyricsRepositoryImpl @Inject constructor( saveLocalLyricsJson(song, parsedLyrics) LogUtils.d(this@LyricsRepositoryImpl, "Fetched and cached remote lyrics (exact match) for: ${song.title}") - Result.success(Pair(parsedLyrics, rawLyricsToSave)) + return@withContext Result.success(Pair(parsedLyrics, rawLyricsToSave)) } else { LogUtils.d(this@LyricsRepositoryImpl, "No lyrics found remotely for: ${song.title}") Result.failure(NoLyricsFoundException()) @@ -1400,6 +1409,24 @@ class LyricsRepositoryImpl @Inject constructor( lrcLibApiService.searchLyrics(trackName = cleanTitle, artistName = cleanArtist) }) + // Romanized search strategy for non-Latin titles + if (MultiLangRomanizer.isScriptThatNeedsRomanization(cleanTitle)) { + val romanTitle = romanizeForMatch(cleanTitle) + if (romanTitle != cleanTitle) { + add(RemoteSearchStrategy("romanized_track") { + lrcLibApiService.searchLyrics(trackName = romanTitle, artistName = cleanArtist) + }) + } + } + if (MultiLangRomanizer.isScriptThatNeedsRomanization(cleanArtist)) { + val romanArtist = romanizeForMatch(cleanArtist) + if (romanArtist != cleanArtist) { + add(RemoteSearchStrategy("romanized_artist") { + lrcLibApiService.searchLyrics(trackName = cleanTitle, artistName = romanArtist) + }) + } + } + // Smart title cleanup strategy val smartTitle = cleanTitleSmart(cleanTitle) if (smartTitle != cleanTitle && smartTitle.isNotBlank()) { @@ -1547,7 +1574,9 @@ class LyricsRepositoryImpl @Inject constructor( // Also remove JSON cache try { val file = File(context.filesDir, "lyrics/${songId}.json") - if (file.exists()) file.delete() + if (file.exists() && !file.delete()) { + Log.w(TAG, "Failed to delete JSON cache for songId: $songId") + } } catch (e: Exception) { Log.w(TAG, "Error deleting JSON cache: ${e.message}") } @@ -1625,8 +1654,8 @@ class LyricsRepositoryImpl @Inject constructor( // Strategy 2: Artist - Title if (foundFile == null) { - val cleanArtist = song.displayArtist.replace(Regex("[^a-zA-Z0-9]"), "_") - val cleanTitle = song.title.replace(Regex("[^a-zA-Z0-9]"), "_") + val cleanArtist = song.displayArtist.replace(Regex("[^\\p{L}\\p{N}]"), "_") + val cleanTitle = song.title.replace(Regex("[^\\p{L}\\p{N}]"), "_") for (extension in LyricsImportSecurity.supportedFileExtensions()) { val altMatch = File(directory, "${cleanArtist}_${cleanTitle}.$extension") if (altMatch.exists() && altMatch.canRead()) { diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/components/LyricsSheet.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/components/LyricsSheet.kt index 9787826e8f..7b6ddc37ad 100644 --- a/app/src/main/java/com/theveloper/pixelplay/presentation/components/LyricsSheet.kt +++ b/app/src/main/java/com/theveloper/pixelplay/presentation/components/LyricsSheet.kt @@ -513,8 +513,11 @@ fun LyricsSheet( immersiveMode = false } - LaunchedEffect(currentSong, lyrics, isLoadingLyrics) { - if (currentSong != null && lyrics == null && !isLoadingLyrics) { + LaunchedEffect(currentSong, lyrics, isLoadingLyrics, lyricsSearchUiState) { + if (lyricsSearchUiState is LyricsSearchUiState.Success) { + // Search completed successfully — close the dialog + showFetchLyricsDialog = false + } else if (currentSong != null && lyrics == null && !isLoadingLyrics) { // Only show dialog if reset was not just triggered if (!wasResetTriggered) { showFetchLyricsDialog = true diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/AiStateHolder.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/AiStateHolder.kt index 369533fd5c..3dee09de7c 100644 --- a/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/AiStateHolder.kt +++ b/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/AiStateHolder.kt @@ -292,26 +292,45 @@ class AiStateHolder @Inject constructor( val targetLanguage = context.resources.configuration.locales[0] .getDisplayLanguage(Locale.US) val sourceLanguage = detectLyricsLanguage(lyricsText) + val hasTimestamps = lyricsText.contains(Regex("\\[\\d{1,2}:\\d{2}")) val taskPrefix = if (sourceLanguage != null) { "Translate these $sourceLanguage song lyrics into $targetLanguage." } else { "Translate these song lyrics into $targetLanguage. Detect the source language from the lyrics content." } + val formatSection = if (hasTimestamps) { + """ + +For SYNCED lyrics — output TWO lines per original line: +[original timestamp] original text +[same timestamp] translated text + +Preserve ALL timestamps [mm:ss.xx] exactly — never modify, merge, or drop them. + + """.trimIndent() + } else { + """ + +For PLAIN lyrics (no timestamps) — output TWO lines per original line: +original text +translated text + +Keep the same line order as the original. + + """.trimIndent() + } val xmlPrompt = """ $taskPrefix -- Preserve ALL timestamps [mm:ss.xx] exactly — never modify, merge, or drop them. -- Output TWO lines per original line: the original, then the translation with the same timestamp. +- Output TWO lines per original line: the original, then the translation. - NEVER add explanations, labels, numbering, section headers, or formatting. - NEVER remove, merge, split, or reorder lines. +- Translate ALL lines completely — do not skip or truncate any part. - If lyrics are ALREADY mostly in $targetLanguage, output ONLY: ALREADY_IN_TARGET_LANGUAGE - -[original timestamp] original text -[same timestamp] translated text - +$formatSection $lyricsText diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/LyricsStateHolder.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/LyricsStateHolder.kt index 7ac9fe9d7f..19ae900724 100644 --- a/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/LyricsStateHolder.kt +++ b/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/LyricsStateHolder.kt @@ -344,22 +344,16 @@ class LyricsStateHolder @Inject constructor( fun translateLyricsViaAi(currentSong: Song, lyricsObj: Lyrics?, cb: LyricsTranslationCallbacks) { val songId = currentSong.id.toLongOrNull() ?: return - if (lyricsObj?.synced != null) { - val hasValidTranslation = lyricsObj.synced.any { !it.translation.isNullOrBlank() } - if (hasValidTranslation) { - _messageEvents.tryEmit(cb.getString(R.string.lyrics_translate_already_translated)) - return - } - } - scope?.launch { _messageEvents.emit(cb.getString(R.string.lyrics_translate_progress)) + // Always read fresh lyrics from source — never rely on the potentially + // stale Lyrics object from UI state which may contain old translations. val rawLyrics = withContext(Dispatchers.IO) { readLocalLyricsFile(currentSong) ?: readEmbeddedLyricsFromFile(currentSong) - ?: currentSong.lyrics?.takeIf { it.isNotBlank() } ?: musicRepository.getStoredLyrics(currentSong)?.second + ?: currentSong.lyrics?.takeIf { it.isNotBlank() } } if (rawLyrics.isNullOrBlank()) { From acd1ce7607f48525040b7391982c5e6b5568add5 Mon Sep 17 00:00:00 2001 From: Dae Euhwa Date: Mon, 6 Jul 2026 04:29:15 -0500 Subject: [PATCH 40/73] Fix #58, #64, #67: Shared AI OkHttpClient, FLAC fallback, provider deduplication #58: Shared OkHttpClient singleton for AI clients - Added @AiOkHttpClient qualifier in Qualifiers.kt - Provided shared OkHttpClient in AppModule.kt with 30s/60s/30s timeouts - Injected into AiClientFactory and passed to GeminiAiClient/GenericOpenAiClient - Removed per-client OkHttpClient creation from GeminiAiClient and GenericOpenAiClient - Connection pool is now reused across all AI client instances #64: Fix high-res FLAC metadata write skipping - Changed updateFileMetadataWithTagLib to return false for problematic FLAC instead of true (was silently succeeding without writing) - Added FLAC verification after JAudioTagger commit to detect failed writes - Falls back to TagLib if JAudioTagger fails on FLAC files #67: Extract hardcoded provider labels, deduplicate sections - AiClientFactory now uses provider.displayName instead of hardcoded strings - Removed 80+ lines of dead per-provider StateFlows from SettingsViewModel - Removed dead per-provider system prompt change/reset functions - Removed dead per-provider model change functions --- .../data/ai/provider/AiClientFactory.kt | 39 ++++-- .../data/ai/provider/GeminiAiClient.kt | 11 +- .../data/ai/provider/GenericOpenAiClient.kt | 14 +-- .../data/media/SongMetadataEditor.kt | 17 ++- .../com/theveloper/pixelplay/di/AppModule.kt | 15 +++ .../com/theveloper/pixelplay/di/Qualifiers.kt | 7 ++ .../viewmodel/SettingsViewModel.kt | 116 ------------------ 7 files changed, 68 insertions(+), 151 deletions(-) diff --git a/app/src/main/java/com/theveloper/pixelplay/data/ai/provider/AiClientFactory.kt b/app/src/main/java/com/theveloper/pixelplay/data/ai/provider/AiClientFactory.kt index b0401aedc9..eb0c79f3b2 100644 --- a/app/src/main/java/com/theveloper/pixelplay/data/ai/provider/AiClientFactory.kt +++ b/app/src/main/java/com/theveloper/pixelplay/data/ai/provider/AiClientFactory.kt @@ -1,5 +1,6 @@ package com.theveloper.pixelplay.data.ai.provider +import okhttp3.OkHttpClient import javax.inject.Inject import javax.inject.Singleton @@ -7,7 +8,9 @@ import javax.inject.Singleton * Factory for creating AI client instances based on provider type */ @Singleton -class AiClientFactory @Inject constructor() { +class AiClientFactory @Inject constructor( + @com.theveloper.pixelplay.di.AiOkHttpClient private val sharedHttpClient: OkHttpClient +) { /** * Create an AI client for the specified provider @@ -21,72 +24,82 @@ class AiClientFactory @Inject constructor() { } return when (provider) { - AiProvider.GEMINI -> GeminiAiClient(apiKey) + AiProvider.GEMINI -> GeminiAiClient(apiKey, sharedHttpClient) AiProvider.DEEPSEEK -> GenericOpenAiClient( apiKey = apiKey, baseUrl = "https://api.deepseek.com/v1", defaultModelId = "deepseek-chat", - providerName = "DeepSeek" + providerName = provider.displayName, + httpClient = sharedHttpClient ) AiProvider.GROQ -> GenericOpenAiClient( apiKey = apiKey, baseUrl = "https://api.groq.com/openai/v1", defaultModelId = "llama-3.3-70b-versatile", - providerName = "Groq" + providerName = provider.displayName, + httpClient = sharedHttpClient ) AiProvider.MISTRAL -> GenericOpenAiClient( apiKey = apiKey, baseUrl = "https://api.mistral.ai/v1", defaultModelId = "mistral-large-2411", - providerName = "Mistral" + providerName = provider.displayName, + httpClient = sharedHttpClient ) AiProvider.NVIDIA -> GenericOpenAiClient( apiKey = apiKey, baseUrl = "https://integrate.api.nvidia.com/v1", defaultModelId = "nvidia/llama-3.1-nemotron-70b-instruct", - providerName = "NVIDIA NIM" + providerName = provider.displayName, + httpClient = sharedHttpClient ) AiProvider.KIMI -> GenericOpenAiClient( apiKey = apiKey, baseUrl = "https://api.moonshot.cn/v1", defaultModelId = "moonshot-v1-auto", - providerName = "Kimi" + providerName = provider.displayName, + httpClient = sharedHttpClient ) AiProvider.GLM -> GenericOpenAiClient( apiKey = apiKey, baseUrl = "https://open.bigmodel.cn/api/paas/v4", defaultModelId = "glm-4-plus", - providerName = "GLM" + providerName = provider.displayName, + httpClient = sharedHttpClient ) AiProvider.OPENAI -> GenericOpenAiClient( apiKey = apiKey, baseUrl = "https://api.openai.com/v1", defaultModelId = "gpt-4o-mini", - providerName = "OpenAI" + providerName = provider.displayName, + httpClient = sharedHttpClient ) AiProvider.OPENROUTER -> GenericOpenAiClient( apiKey = apiKey, baseUrl = "https://openrouter.ai/api/v1", defaultModelId = "google/gemini-2.5-flash-preview-04-17:free", - providerName = "OpenRouter" + providerName = provider.displayName, + httpClient = sharedHttpClient ) AiProvider.OLLAMA -> GenericOpenAiClient( apiKey = apiKey, baseUrl = "https://api.ollama.ai/v1", defaultModelId = "llama3", - providerName = "Ollama" + providerName = provider.displayName, + httpClient = sharedHttpClient ) AiProvider.CUSTOM -> GenericOpenAiClient( apiKey = apiKey, baseUrl = "", defaultModelId = "", - providerName = "Custom Provider" + providerName = provider.displayName, + httpClient = sharedHttpClient ) } } fun createClientWithUrl(provider: AiProvider, apiKey: String, baseUrl: String): AiClient { val displayName = provider.displayName - return GenericOpenAiClient(apiKey, baseUrl.trimEnd('/'), "", displayName) + return GenericOpenAiClient(apiKey, baseUrl.trimEnd('/'), "", displayName, sharedHttpClient) } } diff --git a/app/src/main/java/com/theveloper/pixelplay/data/ai/provider/GeminiAiClient.kt b/app/src/main/java/com/theveloper/pixelplay/data/ai/provider/GeminiAiClient.kt index be42fb9f85..27a4cce85d 100644 --- a/app/src/main/java/com/theveloper/pixelplay/data/ai/provider/GeminiAiClient.kt +++ b/app/src/main/java/com/theveloper/pixelplay/data/ai/provider/GeminiAiClient.kt @@ -9,7 +9,6 @@ import okhttp3.MediaType.Companion.toMediaType import okhttp3.OkHttpClient import okhttp3.Request import okhttp3.RequestBody.Companion.toRequestBody -import java.util.concurrent.TimeUnit /** * Gemini provider implementation. @@ -19,21 +18,13 @@ import java.util.concurrent.TimeUnit * SDK. This keeps generation in lockstep with whatever models the API actually serves, * so any model the user selects works as long as their key supports it. */ -class GeminiAiClient(private val apiKey: String) : AiClient { +class GeminiAiClient(private val apiKey: String, private val httpClient: OkHttpClient) : AiClient { companion object { private const val DEFAULT_GEMINI_MODEL = "gemini-3.1-flash-lite" private const val BASE_URL = "https://generativelanguage.googleapis.com/v1beta" - - } - private val httpClient = OkHttpClient.Builder() - .connectTimeout(30, TimeUnit.SECONDS) - .readTimeout(60, TimeUnit.SECONDS) - .writeTimeout(30, TimeUnit.SECONDS) - .build() - private val json = Json { ignoreUnknownKeys = true isLenient = true diff --git a/app/src/main/java/com/theveloper/pixelplay/data/ai/provider/GenericOpenAiClient.kt b/app/src/main/java/com/theveloper/pixelplay/data/ai/provider/GenericOpenAiClient.kt index c31fb83e8e..2346b18579 100644 --- a/app/src/main/java/com/theveloper/pixelplay/data/ai/provider/GenericOpenAiClient.kt +++ b/app/src/main/java/com/theveloper/pixelplay/data/ai/provider/GenericOpenAiClient.kt @@ -9,7 +9,6 @@ import okhttp3.MediaType.Companion.toMediaType import okhttp3.OkHttpClient import okhttp3.Request import okhttp3.RequestBody.Companion.toRequestBody -import java.util.concurrent.TimeUnit /** * A generic AI client for OpenAI-compatible APIs (NVIDIA, Kimi, GLM, etc.) @@ -18,7 +17,12 @@ class GenericOpenAiClient( private val apiKey: String, private val baseUrl: String, private val defaultModelId: String, - private val providerName: String = "OpenAI" + private val providerName: String = "OpenAI", + private val client: OkHttpClient = OkHttpClient.Builder() + .connectTimeout(30, TimeUnit.SECONDS) + .readTimeout(60, TimeUnit.SECONDS) + .writeTimeout(30, TimeUnit.SECONDS) + .build() ) : AiClient { @Serializable @@ -47,12 +51,6 @@ class GenericOpenAiClient( @Serializable private data class ModelsResponse(val data: List) - private val client = OkHttpClient.Builder() - .connectTimeout(30, TimeUnit.SECONDS) - .readTimeout(60, TimeUnit.SECONDS) - .writeTimeout(30, TimeUnit.SECONDS) - .build() - private val json = Json { ignoreUnknownKeys = true isLenient = true diff --git a/app/src/main/java/com/theveloper/pixelplay/data/media/SongMetadataEditor.kt b/app/src/main/java/com/theveloper/pixelplay/data/media/SongMetadataEditor.kt index e1f7c41eb6..41d1f5435d 100644 --- a/app/src/main/java/com/theveloper/pixelplay/data/media/SongMetadataEditor.kt +++ b/app/src/main/java/com/theveloper/pixelplay/data/media/SongMetadataEditor.kt @@ -769,10 +769,8 @@ class SongMetadataEditor( Timber.tag(TAG) .w("TAGLIB: Skipping file modification for high-resolution FLAC (${flacResult.sampleRate}Hz, ${flacResult.bitsPerSample}bit)") Timber.tag(TAG) - .w("TAGLIB: High-res FLAC files may not work correctly with TagLib. Will update MediaStore only.") - // Return true to indicate we should proceed with MediaStore-only update - // The calling code will still update MediaStore and local DB - return true + .w("TAGLIB: High-res FLAC files may not work correctly with TagLib. Falling back to JAudioTagger.") + return false } else -> { /* Continue with normal processing */ } } @@ -963,6 +961,17 @@ class SongMetadataEditor( } audioFile.commit() + + // Verify the write succeeded for FLAC files which can be problematic + if (targetFile.extension.equals("flac", ignoreCase = true)) { + val verifyFile = AudioFileIO.read(targetFile) + val verifyTag = verifyFile?.tag + if (verifyTag == null || verifyTag.getFirst(FieldKey.TITLE) != newTitle) { + Timber.tag(TAG).w("JAUDIOTAGGER: FLAC verification failed - tag not persisted, falling back to TagLib") + return false + } + } + Timber.tag(TAG).d("JAUDIOTAGGER: SUCCESS - Updated file metadata: $filePath") true } catch (e: Exception) { diff --git a/app/src/main/java/com/theveloper/pixelplay/di/AppModule.kt b/app/src/main/java/com/theveloper/pixelplay/di/AppModule.kt index 7f1d935994..4953534951 100644 --- a/app/src/main/java/com/theveloper/pixelplay/di/AppModule.kt +++ b/app/src/main/java/com/theveloper/pixelplay/di/AppModule.kt @@ -530,6 +530,21 @@ object AppModule { .build() } + /** + * Shared OkHttpClient for AI provider HTTP calls. + * Connection pooling is reused across all AI client instances. + */ + @Provides + @Singleton + @AiOkHttpClient + fun provideAiOkHttpClient(): OkHttpClient { + return OkHttpClient.Builder() + .connectTimeout(30, java.util.concurrent.TimeUnit.SECONDS) + .readTimeout(60, java.util.concurrent.TimeUnit.SECONDS) + .writeTimeout(30, java.util.concurrent.TimeUnit.SECONDS) + .build() + } + /** * Provee una instancia singleton de Retrofit para la API de LRCLIB. */ diff --git a/app/src/main/java/com/theveloper/pixelplay/di/Qualifiers.kt b/app/src/main/java/com/theveloper/pixelplay/di/Qualifiers.kt index d0207b1997..e978cd7ce8 100644 --- a/app/src/main/java/com/theveloper/pixelplay/di/Qualifiers.kt +++ b/app/src/main/java/com/theveloper/pixelplay/di/Qualifiers.kt @@ -29,3 +29,10 @@ annotation class BackupGson @Qualifier @Retention(AnnotationRetention.BINARY) annotation class AppScope + +/** + * Qualifier for shared AI OkHttpClient instance. + */ +@Qualifier +@Retention(AnnotationRetention.BINARY) +annotation class AiOkHttpClient diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/SettingsViewModel.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/SettingsViewModel.kt index 430aed890d..d8831fd9fc 100644 --- a/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/SettingsViewModel.kt +++ b/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/SettingsViewModel.kt @@ -213,86 +213,6 @@ class SettingsViewModel @Inject constructor( .flatMapLatest { provider -> aiPreferencesRepository.getSystemPrompt(AiProvider.fromString(provider)) } .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), AiPreferencesRepository.DEFAULT_SYSTEM_PROMPT) - // Specific Provider StateFlows for UI Compatibility - val geminiApiKey: StateFlow = aiPreferencesRepository.geminiApiKey - .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), "") - val geminiModel: StateFlow = aiPreferencesRepository.geminiModel - .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), "") - val geminiSystemPrompt: StateFlow = aiPreferencesRepository.geminiSystemPrompt - .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), AiPreferencesRepository.DEFAULT_SYSTEM_PROMPT) - - val deepseekApiKey: StateFlow = aiPreferencesRepository.deepseekApiKey - .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), "") - val deepseekModel: StateFlow = aiPreferencesRepository.deepseekModel - .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), "") - val deepseekSystemPrompt: StateFlow = aiPreferencesRepository.deepseekSystemPrompt - .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), AiPreferencesRepository.DEFAULT_DEEPSEEK_SYSTEM_PROMPT) - - val groqApiKey: StateFlow = aiPreferencesRepository.groqApiKey - .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), "") - val groqModel: StateFlow = aiPreferencesRepository.groqModel - .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), "") - val groqSystemPrompt: StateFlow = aiPreferencesRepository.groqSystemPrompt - .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), AiPreferencesRepository.DEFAULT_GROQ_SYSTEM_PROMPT) - - val mistralApiKey: StateFlow = aiPreferencesRepository.mistralApiKey - .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), "") - val mistralModel: StateFlow = aiPreferencesRepository.mistralModel - .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), "") - val mistralSystemPrompt: StateFlow = aiPreferencesRepository.mistralSystemPrompt - .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), AiPreferencesRepository.DEFAULT_MISTRAL_SYSTEM_PROMPT) - - val nvidiaApiKey: StateFlow = aiPreferencesRepository.nvidiaApiKey - .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), "") - val nvidiaModel: StateFlow = aiPreferencesRepository.nvidiaModel - .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), "") - val nvidiaSystemPrompt: StateFlow = aiPreferencesRepository.nvidiaSystemPrompt - .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), AiPreferencesRepository.DEFAULT_NVIDIA_SYSTEM_PROMPT) - - val kimiApiKey: StateFlow = aiPreferencesRepository.kimiApiKey - .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), "") - val kimiModel: StateFlow = aiPreferencesRepository.kimiModel - .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), "") - val kimiSystemPrompt: StateFlow = aiPreferencesRepository.kimiSystemPrompt - .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), AiPreferencesRepository.DEFAULT_KIMI_SYSTEM_PROMPT) - - val glmApiKey: StateFlow = aiPreferencesRepository.glmApiKey - .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), "") - val glmModel: StateFlow = aiPreferencesRepository.glmModel - .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), "") - val glmSystemPrompt: StateFlow = aiPreferencesRepository.glmSystemPrompt - .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), AiPreferencesRepository.DEFAULT_GLM_SYSTEM_PROMPT) - - val openaiApiKey: StateFlow = aiPreferencesRepository.openaiApiKey - .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), "") - val openaiModel: StateFlow = aiPreferencesRepository.openaiModel - .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), "") - val openaiSystemPrompt: StateFlow = aiPreferencesRepository.openaiSystemPrompt - .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), AiPreferencesRepository.DEFAULT_OPENAI_SYSTEM_PROMPT) - - val openrouterApiKey: StateFlow = aiPreferencesRepository.openrouterApiKey - .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), "") - val openrouterModel: StateFlow = aiPreferencesRepository.openrouterModel - .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), "") - val openrouterSystemPrompt: StateFlow = aiPreferencesRepository.openrouterSystemPrompt - .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), AiPreferencesRepository.DEFAULT_OPENROUTER_SYSTEM_PROMPT) - - val ollamaApiKey: StateFlow = aiPreferencesRepository.ollamaApiKey - .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), "") - val ollamaModel: StateFlow = aiPreferencesRepository.ollamaModel - .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), "") - val ollamaSystemPrompt: StateFlow = aiPreferencesRepository.ollamaSystemPrompt - .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), AiPreferencesRepository.DEFAULT_SYSTEM_PROMPT) - - val customApiKey: StateFlow = aiPreferencesRepository.customApiKey - .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), "") - val customModel: StateFlow = aiPreferencesRepository.customModel - .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), "") - val customSystemPrompt: StateFlow = aiPreferencesRepository.customSystemPrompt - .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), AiPreferencesRepository.DEFAULT_SYSTEM_PROMPT) - val customBaseUrl: StateFlow = aiPreferencesRepository.customBaseUrl - .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), "") - val currentAiBaseUrl: StateFlow = aiProvider .flatMapLatest { provider -> val p = AiProvider.fromString(provider) @@ -375,18 +295,6 @@ class SettingsViewModel @Inject constructor( } } - fun onGeminiModelChange(model: String) = viewModelScope.launch { aiPreferencesRepository.setModel(AiProvider.GEMINI, model) } - fun onDeepseekModelChange(model: String) = viewModelScope.launch { aiPreferencesRepository.setModel(AiProvider.DEEPSEEK, model) } - fun onGroqModelChange(model: String) = viewModelScope.launch { aiPreferencesRepository.setModel(AiProvider.GROQ, model) } - fun onMistralModelChange(model: String) = viewModelScope.launch { aiPreferencesRepository.setModel(AiProvider.MISTRAL, model) } - fun onNvidiaModelChange(model: String) = viewModelScope.launch { aiPreferencesRepository.setModel(AiProvider.NVIDIA, model) } - fun onKimiModelChange(model: String) = viewModelScope.launch { aiPreferencesRepository.setModel(AiProvider.KIMI, model) } - fun onGlmModelChange(model: String) = viewModelScope.launch { aiPreferencesRepository.setModel(AiProvider.GLM, model) } - fun onOpenAiModelChange(model: String) = viewModelScope.launch { aiPreferencesRepository.setModel(AiProvider.OPENAI, model) } - fun onOpenrouterModelChange(model: String) = viewModelScope.launch { aiPreferencesRepository.setModel(AiProvider.OPENROUTER, model) } - fun onOllamaModelChange(model: String) = viewModelScope.launch { aiPreferencesRepository.setModel(AiProvider.OLLAMA, model) } - fun onCustomModelChange(model: String) = viewModelScope.launch { aiPreferencesRepository.setModel(AiProvider.CUSTOM, model) } - fun onAiSystemPromptChange(prompt: String) { viewModelScope.launch { val provider = AiProvider.fromString(aiProvider.value) @@ -394,18 +302,6 @@ class SettingsViewModel @Inject constructor( } } - fun onGeminiSystemPromptChange(prompt: String) = viewModelScope.launch { aiPreferencesRepository.setSystemPrompt(AiProvider.GEMINI, prompt) } - fun onDeepseekSystemPromptChange(prompt: String) = viewModelScope.launch { aiPreferencesRepository.setSystemPrompt(AiProvider.DEEPSEEK, prompt) } - fun onGroqSystemPromptChange(prompt: String) = viewModelScope.launch { aiPreferencesRepository.setSystemPrompt(AiProvider.GROQ, prompt) } - fun onMistralSystemPromptChange(prompt: String) = viewModelScope.launch { aiPreferencesRepository.setSystemPrompt(AiProvider.MISTRAL, prompt) } - fun onNvidiaSystemPromptChange(prompt: String) = viewModelScope.launch { aiPreferencesRepository.setSystemPrompt(AiProvider.NVIDIA, prompt) } - fun onKimiSystemPromptChange(prompt: String) = viewModelScope.launch { aiPreferencesRepository.setSystemPrompt(AiProvider.KIMI, prompt) } - fun onGlmSystemPromptChange(prompt: String) = viewModelScope.launch { aiPreferencesRepository.setSystemPrompt(AiProvider.GLM, prompt) } - fun onOpenAiSystemPromptChange(prompt: String) = viewModelScope.launch { aiPreferencesRepository.setSystemPrompt(AiProvider.OPENAI, prompt) } - fun onOpenrouterSystemPromptChange(prompt: String) = viewModelScope.launch { aiPreferencesRepository.setSystemPrompt(AiProvider.OPENROUTER, prompt) } - fun onOllamaSystemPromptChange(prompt: String) = viewModelScope.launch { aiPreferencesRepository.setSystemPrompt(AiProvider.OLLAMA, prompt) } - fun onCustomSystemPromptChange(prompt: String) = viewModelScope.launch { aiPreferencesRepository.setSystemPrompt(AiProvider.CUSTOM, prompt) } - fun resetAiSystemPrompt() { viewModelScope.launch { val provider = AiProvider.fromString(aiProvider.value) @@ -413,18 +309,6 @@ class SettingsViewModel @Inject constructor( } } - fun resetGeminiSystemPrompt() = viewModelScope.launch { aiPreferencesRepository.resetSystemPrompt(AiProvider.GEMINI) } - fun resetDeepseekSystemPrompt() = viewModelScope.launch { aiPreferencesRepository.resetSystemPrompt(AiProvider.DEEPSEEK) } - fun resetGroqSystemPrompt() = viewModelScope.launch { aiPreferencesRepository.resetSystemPrompt(AiProvider.GROQ) } - fun resetMistralSystemPrompt() = viewModelScope.launch { aiPreferencesRepository.resetSystemPrompt(AiProvider.MISTRAL) } - fun resetNvidiaSystemPrompt() = viewModelScope.launch { aiPreferencesRepository.resetSystemPrompt(AiProvider.NVIDIA) } - fun resetKimiSystemPrompt() = viewModelScope.launch { aiPreferencesRepository.resetSystemPrompt(AiProvider.KIMI) } - fun resetGlmSystemPrompt() = viewModelScope.launch { aiPreferencesRepository.resetSystemPrompt(AiProvider.GLM) } - fun resetOpenAiSystemPrompt() = viewModelScope.launch { aiPreferencesRepository.resetSystemPrompt(AiProvider.OPENAI) } - fun resetOpenrouterSystemPrompt() = viewModelScope.launch { aiPreferencesRepository.resetSystemPrompt(AiProvider.OPENROUTER) } - fun resetOllamaSystemPrompt() = viewModelScope.launch { aiPreferencesRepository.resetSystemPrompt(AiProvider.OLLAMA) } - fun resetCustomSystemPrompt() = viewModelScope.launch { aiPreferencesRepository.resetSystemPrompt(AiProvider.CUSTOM) } - fun clearAiUsageData() { viewModelScope.launch { aiUsageDao.clearUsage() From 2f1df7639f1cb29190d0fb39ba134452f6de0f55 Mon Sep 17 00:00:00 2001 From: Dae Euhwa Date: Mon, 6 Jul 2026 04:34:44 -0500 Subject: [PATCH 41/73] Fix compilation: add TimeUnit import, rename httpClient param to client --- .../data/ai/provider/AiClientFactory.kt | 20 +++++++++---------- .../data/ai/provider/GenericOpenAiClient.kt | 1 + 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/app/src/main/java/com/theveloper/pixelplay/data/ai/provider/AiClientFactory.kt b/app/src/main/java/com/theveloper/pixelplay/data/ai/provider/AiClientFactory.kt index eb0c79f3b2..6eec6d11e6 100644 --- a/app/src/main/java/com/theveloper/pixelplay/data/ai/provider/AiClientFactory.kt +++ b/app/src/main/java/com/theveloper/pixelplay/data/ai/provider/AiClientFactory.kt @@ -30,70 +30,70 @@ class AiClientFactory @Inject constructor( baseUrl = "https://api.deepseek.com/v1", defaultModelId = "deepseek-chat", providerName = provider.displayName, - httpClient = sharedHttpClient + client = sharedHttpClient ) AiProvider.GROQ -> GenericOpenAiClient( apiKey = apiKey, baseUrl = "https://api.groq.com/openai/v1", defaultModelId = "llama-3.3-70b-versatile", providerName = provider.displayName, - httpClient = sharedHttpClient + client = sharedHttpClient ) AiProvider.MISTRAL -> GenericOpenAiClient( apiKey = apiKey, baseUrl = "https://api.mistral.ai/v1", defaultModelId = "mistral-large-2411", providerName = provider.displayName, - httpClient = sharedHttpClient + client = sharedHttpClient ) AiProvider.NVIDIA -> GenericOpenAiClient( apiKey = apiKey, baseUrl = "https://integrate.api.nvidia.com/v1", defaultModelId = "nvidia/llama-3.1-nemotron-70b-instruct", providerName = provider.displayName, - httpClient = sharedHttpClient + client = sharedHttpClient ) AiProvider.KIMI -> GenericOpenAiClient( apiKey = apiKey, baseUrl = "https://api.moonshot.cn/v1", defaultModelId = "moonshot-v1-auto", providerName = provider.displayName, - httpClient = sharedHttpClient + client = sharedHttpClient ) AiProvider.GLM -> GenericOpenAiClient( apiKey = apiKey, baseUrl = "https://open.bigmodel.cn/api/paas/v4", defaultModelId = "glm-4-plus", providerName = provider.displayName, - httpClient = sharedHttpClient + client = sharedHttpClient ) AiProvider.OPENAI -> GenericOpenAiClient( apiKey = apiKey, baseUrl = "https://api.openai.com/v1", defaultModelId = "gpt-4o-mini", providerName = provider.displayName, - httpClient = sharedHttpClient + client = sharedHttpClient ) AiProvider.OPENROUTER -> GenericOpenAiClient( apiKey = apiKey, baseUrl = "https://openrouter.ai/api/v1", defaultModelId = "google/gemini-2.5-flash-preview-04-17:free", providerName = provider.displayName, - httpClient = sharedHttpClient + client = sharedHttpClient ) AiProvider.OLLAMA -> GenericOpenAiClient( apiKey = apiKey, baseUrl = "https://api.ollama.ai/v1", defaultModelId = "llama3", providerName = provider.displayName, - httpClient = sharedHttpClient + client = sharedHttpClient ) AiProvider.CUSTOM -> GenericOpenAiClient( apiKey = apiKey, baseUrl = "", defaultModelId = "", providerName = provider.displayName, - httpClient = sharedHttpClient + client = sharedHttpClient ) } } diff --git a/app/src/main/java/com/theveloper/pixelplay/data/ai/provider/GenericOpenAiClient.kt b/app/src/main/java/com/theveloper/pixelplay/data/ai/provider/GenericOpenAiClient.kt index 2346b18579..609c9b5685 100644 --- a/app/src/main/java/com/theveloper/pixelplay/data/ai/provider/GenericOpenAiClient.kt +++ b/app/src/main/java/com/theveloper/pixelplay/data/ai/provider/GenericOpenAiClient.kt @@ -9,6 +9,7 @@ import okhttp3.MediaType.Companion.toMediaType import okhttp3.OkHttpClient import okhttp3.Request import okhttp3.RequestBody.Companion.toRequestBody +import java.util.concurrent.TimeUnit /** * A generic AI client for OpenAI-compatible APIs (NVIDIA, Kimi, GLM, etc.) From 4658ca3e820e06190b13c2ab957a4633c56a03c7 Mon Sep 17 00:00:00 2001 From: Dae Euhwa Date: Mon, 6 Jul 2026 05:34:24 -0500 Subject: [PATCH 42/73] Fix compilation errors, Song.id type safety, dead code removal, and code quality improvements MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix LyricsRepositoryImpl: Song.id.toLong() → toLongOrNull() with guards for non-numeric IDs (Telegram, Netease, GDrive sources) - Fix return type mismatch in scanAndAssignLocalLrcFiles - Remove dead favorites operations from MusicDao and UserPreferencesRepository (favorites now managed via FavoritesDao with SQL triggers) - Extract duplicated AiUsageReportSection composable in SettingsCategoryScreen - Hoist 9 inline collectAsStateWithLifecycle calls in Generation Parameters - SearchScreen: derive selectedSongIds/selectedPlaylistIds from remembered sets instead of separate Flow collections; pass isShuffleEnabled as param - SongEntity: toLongOrNull() with explicit exception for invalid IDs - SongRemovalStateHolder: guard non-numeric Song.id on delete --- .../pixelplay/data/database/MusicDao.kt | 16 +- .../pixelplay/data/database/SongEntity.kt | 4 +- .../data/paging/MediaStorePagingSource.kt | 2 +- .../preferences/UserPreferencesRepository.kt | 23 +- .../data/repository/LyricsRepositoryImpl.kt | 68 ++-- .../presentation/screens/SearchScreen.kt | 9 +- .../screens/SettingsCategoryScreen.kt | 327 +++++++----------- .../viewmodel/SongRemovalStateHolder.kt | 2 +- 8 files changed, 178 insertions(+), 273 deletions(-) diff --git a/app/src/main/java/com/theveloper/pixelplay/data/database/MusicDao.kt b/app/src/main/java/com/theveloper/pixelplay/data/database/MusicDao.kt index 8a141e350e..05a05deb08 100644 --- a/app/src/main/java/com/theveloper/pixelplay/data/database/MusicDao.kt +++ b/app/src/main/java/com/theveloper/pixelplay/data/database/MusicDao.kt @@ -1631,20 +1631,8 @@ interface MusicDao { suspend fun deleteOrphanedArtists() // --- Favorite Operations --- - @Query("UPDATE songs SET is_favorite = :isFavorite WHERE id = :songId") - suspend fun setFavoriteStatus(songId: Long, isFavorite: Boolean) - - @Query("SELECT is_favorite FROM songs WHERE id = :songId") - suspend fun getFavoriteStatus(songId: Long): Boolean? - - // Transaction to toggle favorite status - @Transaction - suspend fun toggleFavoriteStatus(songId: Long): Boolean { - val currentStatus = getFavoriteStatus(songId) ?: false // Default to false if not found (should not happen for existing song) - val newStatus = !currentStatus - setFavoriteStatus(songId, newStatus) - return newStatus - } + // Note: Favorite status is managed via FavoritesDao. The songs.is_favorite column + // is kept in sync by SQL triggers on the favorites table. @Query(""" UPDATE songs diff --git a/app/src/main/java/com/theveloper/pixelplay/data/database/SongEntity.kt b/app/src/main/java/com/theveloper/pixelplay/data/database/SongEntity.kt index 19f948b792..744f61cf0b 100644 --- a/app/src/main/java/com/theveloper/pixelplay/data/database/SongEntity.kt +++ b/app/src/main/java/com/theveloper/pixelplay/data/database/SongEntity.kt @@ -217,7 +217,7 @@ fun List.toSongs(): List { // filePath y parentDirectoryPath se poblarán desde MediaStore en el SyncWorker. fun Song.toEntity(filePathFromMediaStore: String, parentDirFromMediaStore: String): SongEntity { return SongEntity( - id = this.id.toLong(), + id = this.id.toLongOrNull() ?: throw IllegalArgumentException("Cannot convert Song.id '${this.id}' to Long"), title = this.title, artistName = this.artist, artistId = this.artistId, @@ -256,7 +256,7 @@ data class SongSummary( // (menos probable que se use si la entidad siempre requiere los paths) fun Song.toEntityWithoutPaths(): SongEntity { return SongEntity( - id = this.id.toLong(), + id = this.id.toLongOrNull() ?: throw IllegalArgumentException("Cannot convert Song.id '${this.id}' to Long"), title = this.title, artistName = this.artist, artistId = this.artistId, diff --git a/app/src/main/java/com/theveloper/pixelplay/data/paging/MediaStorePagingSource.kt b/app/src/main/java/com/theveloper/pixelplay/data/paging/MediaStorePagingSource.kt index 2fda86c0b0..4e5f478ac7 100644 --- a/app/src/main/java/com/theveloper/pixelplay/data/paging/MediaStorePagingSource.kt +++ b/app/src/main/java/com/theveloper/pixelplay/data/paging/MediaStorePagingSource.kt @@ -61,7 +61,7 @@ class MediaStorePagingSource( val songs = fetchSongDetails(idsToLoad) // Sort songs to match the order of idsToLoad (because "IN" query doesn't guarantee order) - val songsMap = songs.associateBy { it.id.toLong() } + val songsMap = songs.associateBy { it.id.toLongOrNull() ?: -1L } val orderedSongs = idsToLoad.mapNotNull { songsMap[it] } val nextKey = if (end < filteredIds.size) pageIndex + 1 else null diff --git a/app/src/main/java/com/theveloper/pixelplay/data/preferences/UserPreferencesRepository.kt b/app/src/main/java/com/theveloper/pixelplay/data/preferences/UserPreferencesRepository.kt index 4598f1a86f..d353c6c118 100644 --- a/app/src/main/java/com/theveloper/pixelplay/data/preferences/UserPreferencesRepository.kt +++ b/app/src/main/java/com/theveloper/pixelplay/data/preferences/UserPreferencesRepository.kt @@ -507,32 +507,11 @@ class UserPreferencesRepository @Inject constructor( dataStore.edit { it[PreferencesKeys.GLOBAL_TRANSITION_SETTINGS] = json.encodeToString(settings) } } - // ─── Favorites ──────────────────────────────────────────────────────────── + // ─── Favorites (Legacy — only used for one-time migration) ───────────────── val favoriteSongIdsFlow: Flow> = pref { it[PreferencesKeys.FAVORITE_SONG_IDS] ?: emptySet() } - /** - * Adds or removes [songId] from favorites depending on [isFavorite]. - * Prefer this over [toggleFavoriteSong] when the desired state is known. - */ - suspend fun setFavoriteSong(songId: String, isFavorite: Boolean) { - dataStore.edit { preferences -> - val current = preferences[PreferencesKeys.FAVORITE_SONG_IDS] ?: emptySet() - preferences[PreferencesKeys.FAVORITE_SONG_IDS] = - if (isFavorite) current + songId else current - songId - } - } - - /** Toggles [songId] in the favorites set. */ - suspend fun toggleFavoriteSong(songId: String) { - dataStore.edit { preferences -> - val current = preferences[PreferencesKeys.FAVORITE_SONG_IDS] ?: emptySet() - preferences[PreferencesKeys.FAVORITE_SONG_IDS] = - if (songId in current) current - songId else current + songId - } - } - suspend fun clearFavoriteSongIds() { dataStore.edit { it[PreferencesKeys.FAVORITE_SONG_IDS] = emptySet() } } diff --git a/app/src/main/java/com/theveloper/pixelplay/data/repository/LyricsRepositoryImpl.kt b/app/src/main/java/com/theveloper/pixelplay/data/repository/LyricsRepositoryImpl.kt index 33c266a0e6..948fdfc62a 100644 --- a/app/src/main/java/com/theveloper/pixelplay/data/repository/LyricsRepositoryImpl.kt +++ b/app/src/main/java/com/theveloper/pixelplay/data/repository/LyricsRepositoryImpl.kt @@ -590,14 +590,19 @@ class LyricsRepositoryImpl @Inject constructor( // Save to database try { - lyricsDao.insert( - com.theveloper.pixelplay.data.database.LyricsEntity( - songId = song.id.toLong(), - content = rawLyrics, - isSynced = !bestMatch.syncedLyrics.isNullOrBlank(), - source = "remote" + val songIdLong = song.id.toLongOrNull() + if (songIdLong == null) { + Log.w(TAG, "Cannot persist lyrics to DB: non-numeric Song.id '${song.id}'") + } else { + lyricsDao.insert( + com.theveloper.pixelplay.data.database.LyricsEntity( + songId = songIdLong, + content = rawLyrics, + isSynced = !bestMatch.syncedLyrics.isNullOrBlank(), + source = "remote" + ) ) - ) + } } catch (e: NumberFormatException) { Log.w(TAG, "Skipping database save for non-numeric song ID: ${song.id} (possible streaming or external source). Lyrics will be cached in JSON.") } @@ -1314,14 +1319,19 @@ class LyricsRepositoryImpl @Inject constructor( val rawLyricsToSave = best.rawLyrics try { - lyricsDao.insert( - com.theveloper.pixelplay.data.database.LyricsEntity( - songId = song.id.toLong(), - content = rawLyricsToSave, - isSynced = !best.lyrics.synced.isNullOrEmpty(), - source = "remote" - ) - ) + val songIdLong = song.id.toLongOrNull() + if (songIdLong == null) { + Log.w(TAG, "Cannot persist lyrics to DB: non-numeric Song.id '${song.id}'") + } else { + lyricsDao.insert( + com.theveloper.pixelplay.data.database.LyricsEntity( + songId = songIdLong, + content = rawLyricsToSave, + isSynced = !best.lyrics.synced.isNullOrEmpty(), + source = "remote" + ) + ) + } } catch (e: NumberFormatException) { Log.w(TAG, "Skipping DB update for non-numeric ID: ${song.id}") } @@ -1357,9 +1367,14 @@ class LyricsRepositoryImpl @Inject constructor( } try { + val songIdLong = song.id.toLongOrNull() + if (songIdLong == null) { + Log.w(TAG, "Cannot persist lyrics to DB: non-numeric Song.id '${song.id}'") + return@withContext Result.failure(LyricsException("Non-numeric Song.id")) + } lyricsDao.insert( com.theveloper.pixelplay.data.database.LyricsEntity( - songId = song.id.toLong(), + songId = songIdLong, content = rawLyricsToSave, isSynced = !parsedLyrics.synced.isNullOrEmpty(), source = "remote" @@ -1669,14 +1684,19 @@ class LyricsRepositoryImpl @Inject constructor( val validated = readValidatedLocalLyrics(foundFile) if (validated != null) { try { - lyricsDao.insert( - com.theveloper.pixelplay.data.database.LyricsEntity( - songId = song.id.toLong(), - content = validated.sanitizedContent, - isSynced = validated.parsedLyrics.synced?.isNotEmpty() == true, - source = "local_file" - ) - ) + val songIdLong = song.id.toLongOrNull() + if (songIdLong == null) { + Log.w(TAG, "Cannot persist lyrics to DB: non-numeric Song.id '${song.id}'") + } else { + lyricsDao.insert( + com.theveloper.pixelplay.data.database.LyricsEntity( + songId = songIdLong, + content = validated.sanitizedContent, + isSynced = validated.parsedLyrics.synced?.isNotEmpty() == true, + source = "local_file" + ) + ) + } updatedCount.incrementAndGet() LogUtils.d(this@LyricsRepositoryImpl, "Auto-assigned lyrics from ${foundFile.name}") } catch (e: Exception) { diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/screens/SearchScreen.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/screens/SearchScreen.kt index 429f910609..2831b36c1a 100644 --- a/app/src/main/java/com/theveloper/pixelplay/presentation/screens/SearchScreen.kt +++ b/app/src/main/java/com/theveloper/pixelplay/presentation/screens/SearchScreen.kt @@ -175,7 +175,7 @@ fun SearchScreen( val multiSelectionState = playerViewModel.multiSelectionStateHolder val selectedSongs by multiSelectionState.selectedSongs.collectAsStateWithLifecycle() val isSongSelectionMode by multiSelectionState.isSelectionMode.collectAsStateWithLifecycle() - val selectedSongIds by multiSelectionState.selectedSongIds.collectAsStateWithLifecycle() + val selectedSongIds = remember(selectedSongs) { selectedSongs.map { it.id }.toSet() } var showMultiSelectionSheet by remember { mutableStateOf(false) } // Multi-selection state for albums @@ -188,7 +188,7 @@ fun SearchScreen( val playlistSelectionState = playerViewModel.playlistSelectionStateHolder val selectedPlaylists by playlistSelectionState.selectedPlaylists.collectAsStateWithLifecycle() val isPlaylistSelectionMode by playlistSelectionState.isSelectionMode.collectAsStateWithLifecycle() - val selectedPlaylistIds by playlistSelectionState.selectedPlaylistIds.collectAsStateWithLifecycle() + val selectedPlaylistIds = remember(selectedPlaylists) { selectedPlaylists.map { it.id }.toSet() } var showPlaylistMultiSelectionSheet by remember { mutableStateOf(false) } // Multi-selection state for genres @@ -580,6 +580,7 @@ fun SearchScreen( }, currentPlayingSongId = stablePlayerState.currentSong?.id, isPlaying = stablePlayerState.isPlaying, + isShuffleEnabled = stablePlayerState.isShuffleEnabled, onSongMoreOptionsClick = handleSongMoreOptionsClick, navController = navController, isSelectionMode = isSongSelectionMode, @@ -1069,6 +1070,7 @@ fun SearchResultsList( onItemSelected: () -> Unit, currentPlayingSongId: String?, isPlaying: Boolean, + isShuffleEnabled: Boolean = false, onSongMoreOptionsClick: (Song) -> Unit, navController: NavHostController, isSelectionMode: Boolean = false, @@ -1087,7 +1089,6 @@ fun SearchResultsList( getPlaylistSelectionIndex: (String) -> Int? = { null } ) { val localDensity = LocalDensity.current - val playerStableState by playerViewModel.stablePlayerState.collectAsStateWithLifecycle() if (results.isEmpty()) { Box( @@ -1294,7 +1295,7 @@ fun SearchResultsList( songs.first(), item.playlist.name ) - if (playerStableState.isShuffleEnabled) playerViewModel.toggleShuffle() + if (isShuffleEnabled) playerViewModel.toggleShuffle() } else { playerViewModel.sendToast("Empty playlist") } diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/screens/SettingsCategoryScreen.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/screens/SettingsCategoryScreen.kt index 46f1c540a1..d23d652ca1 100644 --- a/app/src/main/java/com/theveloper/pixelplay/presentation/screens/SettingsCategoryScreen.kt +++ b/app/src/main/java/com/theveloper/pixelplay/presentation/screens/SettingsCategoryScreen.kt @@ -1041,106 +1041,19 @@ fun SettingsCategoryScreen( Spacer(modifier = Modifier.height(16.dp)) - SettingsSubsection(title = stringResource(R.string.settings_ai_usage_report_section)) { - val recentAiUsage by settingsViewModel.recentAiUsage.collectAsStateWithLifecycle() - val totalPromptTokens by settingsViewModel.totalPromptTokens.collectAsStateWithLifecycle() - val totalOutputTokens by settingsViewModel.totalOutputTokens.collectAsStateWithLifecycle() - val totalThoughtTokens by settingsViewModel.totalThoughtTokens.collectAsStateWithLifecycle() - - val totalTokens = totalPromptTokens + totalOutputTokens + totalThoughtTokens - val totalTokStr = String.format(Locale.US, "%,d", totalTokens) - val promptTokStr = String.format(Locale.US, "%,d", totalPromptTokens) - val outputTokStr = String.format(Locale.US, "%,d", totalOutputTokens) - val thoughtTokStr = String.format(Locale.US, "%,d", totalThoughtTokens) - - ActionSettingsItem( - title = stringResource(R.string.settings_total_consumption_title), - subtitle = stringResource( - R.string.settings_ai_usage_tokens_subtitle, - totalTokStr, - promptTokStr, - outputTokStr, - thoughtTokStr - ), - icon = { - Icon( - painter = painterResource(R.drawable.rounded_monitoring_24), - contentDescription = null, - tint = MaterialTheme.colorScheme.tertiary - ) - }, - primaryActionLabel = stringResource(R.string.settings_ai_clear_logs), - onPrimaryAction = { settingsViewModel.clearAiUsageData() } - ) - - if (recentAiUsage.isNotEmpty()) { - Spacer(modifier = Modifier.height(12.dp)) - var expanded by remember { mutableStateOf(false) } - val rotation by animateFloatAsState(targetValue = if (expanded) 180f else 0f) - - Surface( - modifier = Modifier - .fillMaxWidth() - .clickable { expanded = !expanded }, - color = Color.Transparent - ) { - Row( - modifier = Modifier.padding(vertical = 8.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceBetween - ) { - Row(verticalAlignment = Alignment.CenterVertically) { - Icon( - painter = painterResource(R.drawable.rounded_monitoring_24), - contentDescription = null, - tint = MaterialTheme.colorScheme.onSurfaceVariant, - modifier = Modifier.size(20.dp) - ) - Spacer(modifier = Modifier.width(12.dp)) - Text( - text = stringResource(R.string.settings_ai_activity_log_title, recentAiUsage.size), - style = MaterialTheme.typography.titleMedium.copy(fontFamily = GoogleSansRounded), - color = MaterialTheme.colorScheme.onSurface - ) - } - Icon( - imageVector = Icons.Rounded.ExpandMore, - contentDescription = if (expanded) stringResource(R.string.settings_ai_hide_logs) else stringResource(R.string.settings_ai_show_logs), - tint = MaterialTheme.colorScheme.onSurfaceVariant, - modifier = Modifier.rotate(rotation) - ) - } - } - - AnimatedVisibility( - visible = expanded, - enter = expandVertically() + fadeIn(), - exit = shrinkVertically() + fadeOut() - ) { - Column( - modifier = Modifier - .fillMaxWidth() - .padding(top = 8.dp, bottom = 8.dp) - ) { - @Suppress("NonObservableLocale") - val dateFormat = SimpleDateFormat("MMMM d, yyyy", Locale.getDefault()) - val groupedUsage = recentAiUsage.groupBy { - dateFormat.format(Date(it.timestamp)) - } - - groupedUsage.forEach { (date, items) -> - AiUsageDateHeader(date = date) - items.forEach { usage -> - AiUsageLogItem(usage = usage) - } - Spacer(modifier = Modifier.height(8.dp)) - } - } - } - } - } + AiUsageReportSection(settingsViewModel = settingsViewModel) } SettingsCategory.GENERATION_PARAMETERS -> { + val aiTemperature by settingsViewModel.aiTemperature.collectAsStateWithLifecycle() + val aiTopP by settingsViewModel.aiTopP.collectAsStateWithLifecycle() + val aiTopK by settingsViewModel.aiTopK.collectAsStateWithLifecycle() + val aiMaxTokens by settingsViewModel.aiMaxTokens.collectAsStateWithLifecycle() + val aiPresencePenalty by settingsViewModel.aiPresencePenalty.collectAsStateWithLifecycle() + val aiFrequencyPenalty by settingsViewModel.aiFrequencyPenalty.collectAsStateWithLifecycle() + val aiSampleSize by settingsViewModel.aiSampleSize.collectAsStateWithLifecycle() + val aiDigestMode by settingsViewModel.aiDigestMode.collectAsStateWithLifecycle() + val aiIncludeExtendedFields by settingsViewModel.aiIncludeExtendedFields.collectAsStateWithLifecycle() + // Prompt Behavior Section SettingsSubsection( title = stringResource(R.string.settings_prompt_behavior_section), @@ -1160,7 +1073,7 @@ fun SettingsCategoryScreen( SettingsSubsection(title = "Generation Parameters") { SliderSettingsItem( label = "Temperature", - value = settingsViewModel.aiTemperature.collectAsStateWithLifecycle().value, + value = aiTemperature, valueRange = 0.0f..2.0f, steps = 20, onValueChange = { settingsViewModel.onAiTemperatureChange(it) }, @@ -1174,7 +1087,7 @@ fun SettingsCategoryScreen( ) SliderSettingsItem( label = "Top P", - value = settingsViewModel.aiTopP.collectAsStateWithLifecycle().value, + value = aiTopP, valueRange = 0.0f..1.0f, steps = 20, onValueChange = { settingsViewModel.onAiTopPChange(it) }, @@ -1188,7 +1101,7 @@ fun SettingsCategoryScreen( ) SliderSettingsItem( label = "Top K", - value = settingsViewModel.aiTopK.collectAsStateWithLifecycle().value.toFloat(), + value = aiTopK.toFloat(), valueRange = 1f..100f, steps = 99, onValueChange = { settingsViewModel.onAiTopKChange(it.toInt()) }, @@ -1202,7 +1115,7 @@ fun SettingsCategoryScreen( ) SliderSettingsItem( label = "Max Output Tokens", - value = settingsViewModel.aiMaxTokens.collectAsStateWithLifecycle().value.toFloat(), + value = aiMaxTokens.toFloat(), valueRange = 128f..8192f, steps = 63, onValueChange = { settingsViewModel.onAiMaxTokensChange(it.toInt()) }, @@ -1216,7 +1129,7 @@ fun SettingsCategoryScreen( ) SliderSettingsItem( label = "Presence Penalty", - value = settingsViewModel.aiPresencePenalty.collectAsStateWithLifecycle().value, + value = aiPresencePenalty, valueRange = -2.0f..2.0f, steps = 40, onValueChange = { settingsViewModel.onAiPresencePenaltyChange(it) }, @@ -1230,7 +1143,7 @@ fun SettingsCategoryScreen( ) SliderSettingsItem( label = "Frequency Penalty", - value = settingsViewModel.aiFrequencyPenalty.collectAsStateWithLifecycle().value, + value = aiFrequencyPenalty, valueRange = -2.0f..2.0f, steps = 40, onValueChange = { settingsViewModel.onAiFrequencyPenaltyChange(it) }, @@ -1248,7 +1161,6 @@ fun SettingsCategoryScreen( // Song Data Configuration Section SettingsSubsection(title = "Song Data Configuration") { - val aiSampleSize by settingsViewModel.aiSampleSize.collectAsStateWithLifecycle() SliderSettingsItem( label = "Sample Size", value = aiSampleSize.toFloat(), @@ -1267,7 +1179,7 @@ fun SettingsCategoryScreen( label = "Digest Detail", description = "Controls how much listening history data is included", options = mapOf("safe" to "Concise (faster)", "full" to "Full (better quality)"), - selectedKey = settingsViewModel.aiDigestMode.collectAsStateWithLifecycle().value, + selectedKey = aiDigestMode, onSelectionChanged = { settingsViewModel.onAiDigestModeChange(it) }, leadingIcon = { Icon( @@ -1280,7 +1192,7 @@ fun SettingsCategoryScreen( SwitchSettingItem( title = "Extended Song Fields", subtitle = "Include album, year, and genre info in song data sent to AI", - checked = settingsViewModel.aiIncludeExtendedFields.collectAsStateWithLifecycle().value, + checked = aiIncludeExtendedFields, onCheckedChange = { settingsViewModel.onAiIncludeExtendedFieldsChange(it) }, leadingIcon = { Icon( @@ -1294,104 +1206,7 @@ fun SettingsCategoryScreen( Spacer(modifier = Modifier.height(16.dp)) - SettingsSubsection(title = stringResource(R.string.settings_ai_usage_report_section)) { - val recentAiUsage by settingsViewModel.recentAiUsage.collectAsStateWithLifecycle() - val totalPromptTokens by settingsViewModel.totalPromptTokens.collectAsStateWithLifecycle() - val totalOutputTokens by settingsViewModel.totalOutputTokens.collectAsStateWithLifecycle() - val totalThoughtTokens by settingsViewModel.totalThoughtTokens.collectAsStateWithLifecycle() - - val totalTokens = totalPromptTokens + totalOutputTokens + totalThoughtTokens - val totalTokStr = String.format(Locale.US, "%,d", totalTokens) - val promptTokStr = String.format(Locale.US, "%,d", totalPromptTokens) - val outputTokStr = String.format(Locale.US, "%,d", totalOutputTokens) - val thoughtTokStr = String.format(Locale.US, "%,d", totalThoughtTokens) - - ActionSettingsItem( - title = stringResource(R.string.settings_total_consumption_title), - subtitle = stringResource( - R.string.settings_ai_usage_tokens_subtitle, - totalTokStr, - promptTokStr, - outputTokStr, - thoughtTokStr - ), - icon = { - Icon( - painter = painterResource(R.drawable.rounded_monitoring_24), - contentDescription = null, - tint = MaterialTheme.colorScheme.tertiary - ) - }, - primaryActionLabel = stringResource(R.string.settings_ai_clear_logs), - onPrimaryAction = { settingsViewModel.clearAiUsageData() } - ) - - if (recentAiUsage.isNotEmpty()) { - Spacer(modifier = Modifier.height(12.dp)) - var expanded by remember { mutableStateOf(false) } - val rotation by animateFloatAsState(targetValue = if (expanded) 180f else 0f) - - Surface( - modifier = Modifier - .fillMaxWidth() - .clickable { expanded = !expanded }, - color = Color.Transparent - ) { - Row( - modifier = Modifier.padding(vertical = 8.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceBetween - ) { - Row(verticalAlignment = Alignment.CenterVertically) { - Icon( - painter = painterResource(R.drawable.rounded_monitoring_24), - contentDescription = null, - tint = MaterialTheme.colorScheme.onSurfaceVariant, - modifier = Modifier.size(20.dp) - ) - Spacer(modifier = Modifier.width(12.dp)) - Text( - text = stringResource(R.string.settings_ai_activity_log_title, recentAiUsage.size), - style = MaterialTheme.typography.titleMedium.copy(fontFamily = GoogleSansRounded), - color = MaterialTheme.colorScheme.onSurface - ) - } - Icon( - imageVector = Icons.Rounded.ExpandMore, - contentDescription = if (expanded) stringResource(R.string.settings_ai_hide_logs) else stringResource(R.string.settings_ai_show_logs), - tint = MaterialTheme.colorScheme.onSurfaceVariant, - modifier = Modifier.rotate(rotation) - ) - } - } - - AnimatedVisibility( - visible = expanded, - enter = expandVertically() + fadeIn(), - exit = shrinkVertically() + fadeOut() - ) { - Column( - modifier = Modifier - .fillMaxWidth() - .padding(top = 8.dp, bottom = 8.dp) - ) { - @Suppress("NonObservableLocale") - val dateFormat = SimpleDateFormat("MMMM d, yyyy", Locale.getDefault()) - val groupedUsage = recentAiUsage.groupBy { - dateFormat.format(Date(it.timestamp)) - } - - groupedUsage.forEach { (date, items) -> - AiUsageDateHeader(date = date) - items.forEach { usage -> - AiUsageLogItem(usage = usage) - } - Spacer(modifier = Modifier.height(8.dp)) - } - } - } - } - } + AiUsageReportSection(settingsViewModel = settingsViewModel) } SettingsCategory.BACKUP_RESTORE -> { @@ -3035,3 +2850,105 @@ private fun SettingsSubsection( Spacer(modifier = Modifier.height(10.dp)) } } + +@Composable +private fun AiUsageReportSection(settingsViewModel: SettingsViewModel) { + SettingsSubsection(title = stringResource(R.string.settings_ai_usage_report_section)) { + val recentAiUsage by settingsViewModel.recentAiUsage.collectAsStateWithLifecycle() + val totalPromptTokens by settingsViewModel.totalPromptTokens.collectAsStateWithLifecycle() + val totalOutputTokens by settingsViewModel.totalOutputTokens.collectAsStateWithLifecycle() + val totalThoughtTokens by settingsViewModel.totalThoughtTokens.collectAsStateWithLifecycle() + + val totalTokens = totalPromptTokens + totalOutputTokens + totalThoughtTokens + val totalTokStr = String.format(Locale.US, "%,d", totalTokens) + val promptTokStr = String.format(Locale.US, "%,d", totalPromptTokens) + val outputTokStr = String.format(Locale.US, "%,d", totalOutputTokens) + val thoughtTokStr = String.format(Locale.US, "%,d", totalThoughtTokens) + + ActionSettingsItem( + title = stringResource(R.string.settings_total_consumption_title), + subtitle = stringResource( + R.string.settings_ai_usage_tokens_subtitle, + totalTokStr, + promptTokStr, + outputTokStr, + thoughtTokStr + ), + icon = { + Icon( + painter = painterResource(R.drawable.rounded_monitoring_24), + contentDescription = null, + tint = MaterialTheme.colorScheme.tertiary + ) + }, + primaryActionLabel = stringResource(R.string.settings_ai_clear_logs), + onPrimaryAction = { settingsViewModel.clearAiUsageData() } + ) + + if (recentAiUsage.isNotEmpty()) { + Spacer(modifier = Modifier.height(12.dp)) + var expanded by remember { mutableStateOf(false) } + val rotation by animateFloatAsState(targetValue = if (expanded) 180f else 0f) + + Surface( + modifier = Modifier + .fillMaxWidth() + .clickable { expanded = !expanded }, + color = Color.Transparent + ) { + Row( + modifier = Modifier.padding(vertical = 8.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + Row(verticalAlignment = Alignment.CenterVertically) { + Icon( + painter = painterResource(R.drawable.rounded_monitoring_24), + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.size(20.dp) + ) + Spacer(modifier = Modifier.width(12.dp)) + Text( + text = stringResource(R.string.settings_ai_activity_log_title, recentAiUsage.size), + style = MaterialTheme.typography.titleMedium.copy(fontFamily = GoogleSansRounded), + color = MaterialTheme.colorScheme.onSurface + ) + } + Icon( + imageVector = Icons.Rounded.ExpandMore, + contentDescription = if (expanded) stringResource(R.string.settings_ai_hide_logs) else stringResource(R.string.settings_ai_show_logs), + tint = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.rotate(rotation) + ) + } + } + + AnimatedVisibility( + visible = expanded, + enter = expandVertically() + fadeIn(), + exit = shrinkVertically() + fadeOut() + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(top = 8.dp, bottom = 8.dp) + ) { + @Suppress("NonObservableLocale") + val dateFormat = SimpleDateFormat("MMMM d, yyyy", Locale.getDefault()) + val groupedUsage = recentAiUsage.groupBy { + dateFormat.format(Date(it.timestamp)) + } + + groupedUsage.forEach { (date, items) -> + AiUsageDateHeader(date = date) + items.forEach { usage -> + AiUsageLogItem(usage = usage) + } + Spacer(modifier = Modifier.height(8.dp)) + } + } + } + } + } +} diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/SongRemovalStateHolder.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/SongRemovalStateHolder.kt index 91e2d858fe..751fb52b06 100644 --- a/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/SongRemovalStateHolder.kt +++ b/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/SongRemovalStateHolder.kt @@ -104,7 +104,7 @@ class SongRemovalStateHolder @Inject constructor( suspend fun removeSongFromLibrary(song: Song) { libraryStateHolder.removeSong(song.id) - musicRepository.deleteById(song.id.toLong()) + song.id.toLongOrNull()?.let { musicRepository.deleteById(it) } playlistPreferencesRepository.removeSongFromAllPlaylists(song.id) } From c29725610add04d5e0e73a603de5717571dd0817 Mon Sep 17 00:00:00 2001 From: Dae Euhwa Date: Tue, 7 Jul 2026 16:21:32 -0500 Subject: [PATCH 43/73] Add opencode workflow for testing and extra analysis --- .github/workflows/opencode.yml | 35 ++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 .github/workflows/opencode.yml diff --git a/.github/workflows/opencode.yml b/.github/workflows/opencode.yml new file mode 100644 index 0000000000..b81f55a859 --- /dev/null +++ b/.github/workflows/opencode.yml @@ -0,0 +1,35 @@ +name: opencode + +on: + issue_comment: + types: [created] + pull_request_review_comment: + types: [created] + +jobs: + opencode: + if: | + contains(github.event.comment.body, ' /oc') || + startsWith(github.event.comment.body, '/oc') || + contains(github.event.comment.body, ' /opencode') || + startsWith(github.event.comment.body, '/opencode') + runs-on: ubuntu-latest + permissions: + id-token: write + contents: read + pull-requests: read + issues: read + steps: + - name: Checkout repository + uses: actions/checkout@v6 + with: + persist-credentials: false + + - name: Run opencode + uses: anomalyco/opencode/github@latest + env: + GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY }} + GOOGLE_GENERATIVE_AI_API_KEY: ${{ secrets.GOOGLE_GENERATIVE_AI_API_KEY }} + GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }} + with: + model: google/gemini-3.1-flash-lite-preview \ No newline at end of file From ea424987c575631d94e1e4dcd293d595901dd20d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 7 Jul 2026 22:23:54 +0000 Subject: [PATCH 44/73] chore(deps): bump actions/checkout in the github-actions group Bumps the github-actions group with 1 update: [actions/checkout](https://github.com/actions/checkout). Updates `actions/checkout` from 6 to 7 - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v6...v7) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: '7' dependency-type: direct:production update-type: version-update:semver-major dependency-group: github-actions ... Signed-off-by: dependabot[bot] --- .github/workflows/opencode.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/opencode.yml b/.github/workflows/opencode.yml index b81f55a859..2fdffec3dc 100644 --- a/.github/workflows/opencode.yml +++ b/.github/workflows/opencode.yml @@ -21,7 +21,7 @@ jobs: issues: read steps: - name: Checkout repository - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: persist-credentials: false From 8a8b18ef883f7f0ba3be3e90e099943849fd7420 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 7 Jul 2026 22:27:25 +0000 Subject: [PATCH 45/73] chore(deps): bump the gradle-dependencies group across 1 directory with 3 updates Bumps the gradle-dependencies group with 3 updates in the / directory: [com.google.dagger:hilt-android](https://github.com/google/dagger), [com.google.dagger:hilt-android-compiler](https://github.com/google/dagger) and [com.google.dagger.hilt.android](https://github.com/google/dagger). Updates `com.google.dagger:hilt-android` from 2.60 to 2.60.1 - [Release notes](https://github.com/google/dagger/releases) - [Changelog](https://github.com/google/dagger/blob/master/CHANGELOG.md) - [Commits](https://github.com/google/dagger/compare/dagger-2.60...dagger-2.60.1) Updates `com.google.dagger:hilt-android-compiler` from 2.60 to 2.60.1 - [Release notes](https://github.com/google/dagger/releases) - [Changelog](https://github.com/google/dagger/blob/master/CHANGELOG.md) - [Commits](https://github.com/google/dagger/compare/dagger-2.60...dagger-2.60.1) Updates `com.google.dagger:hilt-android-compiler` from 2.60 to 2.60.1 - [Release notes](https://github.com/google/dagger/releases) - [Changelog](https://github.com/google/dagger/blob/master/CHANGELOG.md) - [Commits](https://github.com/google/dagger/compare/dagger-2.60...dagger-2.60.1) Updates `com.google.dagger.hilt.android` from 2.60 to 2.60.1 - [Release notes](https://github.com/google/dagger/releases) - [Changelog](https://github.com/google/dagger/blob/master/CHANGELOG.md) - [Commits](https://github.com/google/dagger/compare/dagger-2.60...dagger-2.60.1) --- updated-dependencies: - dependency-name: com.google.dagger.hilt.android dependency-version: 2.60.1 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: gradle-dependencies - dependency-name: com.google.dagger:hilt-android dependency-version: 2.60.1 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: gradle-dependencies - dependency-name: com.google.dagger:hilt-android-compiler dependency-version: 2.60.1 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: gradle-dependencies - dependency-name: com.google.dagger:hilt-android-compiler dependency-version: 2.60.1 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: gradle-dependencies ... Signed-off-by: dependabot[bot] --- gradle/libs.versions.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 062e40f890..588b6b9431 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -19,7 +19,7 @@ foundation = "1.11.4" glance = "1.3.0-alpha02" graphicsShapes = "1.1.0" gson = "2.14.0" -hiltAndroid = "2.60" +hiltAndroid = "2.60.1" hiltNavigationCompose = "1.4.0" ktor = "3.5.1" kotlin = "2.4.0" @@ -73,7 +73,7 @@ httpclient = "4.5.14" errorProneAnnotations = "2.50.0" # DI -dagger = "2.60" +dagger = "2.60.1" javax-inject = "1" # Annotations procesing From 82ab444520e513f31a96a814a49d9709aa9b8c09 Mon Sep 17 00:00:00 2001 From: Dae Euhwa Date: Tue, 7 Jul 2026 23:47:29 -0500 Subject: [PATCH 46/73] update opencode workflow --- .github/workflows/opencode.yml | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/.github/workflows/opencode.yml b/.github/workflows/opencode.yml index b81f55a859..daf27f7ae8 100644 --- a/.github/workflows/opencode.yml +++ b/.github/workflows/opencode.yml @@ -1,6 +1,10 @@ name: opencode on: + issues: + types: [opened] + pull_request: + types: [opened] issue_comment: types: [created] pull_request_review_comment: @@ -9,6 +13,8 @@ on: jobs: opencode: if: | + github.event_name == 'issues' || + github.event_name == 'pull_request' || contains(github.event.comment.body, ' /oc') || startsWith(github.event.comment.body, '/oc') || contains(github.event.comment.body, ' /opencode') || @@ -17,8 +23,8 @@ jobs: permissions: id-token: write contents: read - pull-requests: read - issues: read + pull-requests: write + issues: write steps: - name: Checkout repository uses: actions/checkout@v6 From bc1fbf9701e1a1f489ad8a9e4431f3db13296d3d Mon Sep 17 00:00:00 2001 From: Dae Euhwa Date: Thu, 9 Jul 2026 20:03:39 -0500 Subject: [PATCH 47/73] Remove opencode yaml --- .github/workflows/opencode.yml | 41 ---------------------------------- 1 file changed, 41 deletions(-) delete mode 100644 .github/workflows/opencode.yml diff --git a/.github/workflows/opencode.yml b/.github/workflows/opencode.yml deleted file mode 100644 index 53f64520f7..0000000000 --- a/.github/workflows/opencode.yml +++ /dev/null @@ -1,41 +0,0 @@ -name: opencode - -on: - issues: - types: [opened] - pull_request: - types: [opened] - issue_comment: - types: [created] - pull_request_review_comment: - types: [created] - -jobs: - opencode: - if: | - github.event_name == 'issues' || - github.event_name == 'pull_request' || - contains(github.event.comment.body, ' /oc') || - startsWith(github.event.comment.body, '/oc') || - contains(github.event.comment.body, ' /opencode') || - startsWith(github.event.comment.body, '/opencode') - runs-on: ubuntu-latest - permissions: - id-token: write - contents: read - pull-requests: write - issues: write - steps: - - name: Checkout repository - uses: actions/checkout@v7 - with: - persist-credentials: false - - - name: Run opencode - uses: anomalyco/opencode/github@latest - env: - GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY }} - GOOGLE_GENERATIVE_AI_API_KEY: ${{ secrets.GOOGLE_GENERATIVE_AI_API_KEY }} - GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }} - with: - model: google/gemini-3.1-flash-lite-preview \ No newline at end of file From b7863783edb8a294a429ff308adf07ebef6c1aed Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 14 Jul 2026 22:43:35 +0000 Subject: [PATCH 48/73] chore(deps): bump the gradle-dependencies group across 1 directory with 17 updates Bumps the gradle-dependencies group with 17 updates in the / directory: | Package | From | To | | --- | --- | --- | | [org.jetbrains.kotlin:kotlin-metadata-jvm](https://github.com/JetBrains/kotlin) | `2.4.0` | `2.4.10` | | [org.junit.platform:junit-platform-launcher](https://github.com/junit-team/junit-framework) | `6.1.1` | `6.1.2` | | [org.junit.jupiter:junit-jupiter-api](https://github.com/junit-team/junit-framework) | `6.1.1` | `6.1.2` | | [org.junit.jupiter:junit-jupiter-engine](https://github.com/junit-team/junit-framework) | `6.1.1` | `6.1.2` | | [org.junit.vintage:junit-vintage-engine](https://github.com/junit-team/junit-framework) | `6.1.1` | `6.1.2` | | [com.google.genai:google-genai](https://github.com/googleapis/java-genai) | `1.60.0` | `1.61.0` | | [org.junit.jupiter:junit-jupiter-params](https://github.com/junit-team/junit-framework) | `6.1.1` | `6.1.2` | | [org.jetbrains.kotlin:kotlin-test-junit](https://github.com/JetBrains/kotlin) | `2.4.0` | `2.4.10` | | [org.jetbrains.kotlin.android](https://github.com/JetBrains/kotlin) | `2.4.0` | `2.4.10` | | [org.jetbrains.kotlin.plugin.compose](https://github.com/JetBrains/kotlin) | `2.4.0` | `2.4.10` | | [org.jetbrains.kotlin.plugin.serialization](https://github.com/JetBrains/kotlin) | `2.4.0` | `2.4.10` | | [org.bouncycastle:bcprov-jdk18on](https://github.com/bcgit/bc-java) | `1.84` | `1.85` | | [org.bouncycastle:bcpkix-jdk18on](https://github.com/bcgit/bc-java) | `1.84` | `1.85` | | com.android.application | `9.2.1` | `9.3.0` | | com.android.test | `9.2.1` | `9.3.0` | | com.android.library | `9.2.1` | `9.3.0` | | [com.google.devtools.ksp](https://github.com/google/ksp) | `2.3.9` | `2.3.10` | Updates `org.jetbrains.kotlin:kotlin-metadata-jvm` from 2.4.0 to 2.4.10 - [Release notes](https://github.com/JetBrains/kotlin/releases) - [Changelog](https://github.com/JetBrains/kotlin/blob/master/ChangeLog.md) - [Commits](https://github.com/JetBrains/kotlin/compare/v2.4.0...v2.4.10) Updates `org.junit.platform:junit-platform-launcher` from 6.1.1 to 6.1.2 - [Release notes](https://github.com/junit-team/junit-framework/releases) - [Commits](https://github.com/junit-team/junit-framework/compare/r6.1.1...r6.1.2) Updates `org.junit.jupiter:junit-jupiter-api` from 6.1.1 to 6.1.2 - [Release notes](https://github.com/junit-team/junit-framework/releases) - [Commits](https://github.com/junit-team/junit-framework/compare/r6.1.1...r6.1.2) Updates `org.junit.jupiter:junit-jupiter-engine` from 6.1.1 to 6.1.2 - [Release notes](https://github.com/junit-team/junit-framework/releases) - [Commits](https://github.com/junit-team/junit-framework/compare/r6.1.1...r6.1.2) Updates `org.junit.vintage:junit-vintage-engine` from 6.1.1 to 6.1.2 - [Release notes](https://github.com/junit-team/junit-framework/releases) - [Commits](https://github.com/junit-team/junit-framework/compare/r6.1.1...r6.1.2) Updates `com.google.genai:google-genai` from 1.60.0 to 1.61.0 - [Release notes](https://github.com/googleapis/java-genai/releases) - [Changelog](https://github.com/googleapis/java-genai/blob/main/CHANGELOG.md) - [Commits](https://github.com/googleapis/java-genai/compare/v1.60.0...v1.61.0) Updates `org.junit.jupiter:junit-jupiter-params` from 6.1.1 to 6.1.2 - [Release notes](https://github.com/junit-team/junit-framework/releases) - [Commits](https://github.com/junit-team/junit-framework/compare/r6.1.1...r6.1.2) Updates `org.jetbrains.kotlin:kotlin-test-junit` from 2.4.0 to 2.4.10 - [Release notes](https://github.com/JetBrains/kotlin/releases) - [Changelog](https://github.com/JetBrains/kotlin/blob/master/ChangeLog.md) - [Commits](https://github.com/JetBrains/kotlin/compare/v2.4.0...v2.4.10) Updates `org.jetbrains.kotlin.android` from 2.4.0 to 2.4.10 - [Release notes](https://github.com/JetBrains/kotlin/releases) - [Changelog](https://github.com/JetBrains/kotlin/blob/master/ChangeLog.md) - [Commits](https://github.com/JetBrains/kotlin/compare/v2.4.0...v2.4.10) Updates `org.jetbrains.kotlin.plugin.compose` from 2.4.0 to 2.4.10 - [Release notes](https://github.com/JetBrains/kotlin/releases) - [Changelog](https://github.com/JetBrains/kotlin/blob/master/ChangeLog.md) - [Commits](https://github.com/JetBrains/kotlin/compare/v2.4.0...v2.4.10) Updates `org.jetbrains.kotlin.plugin.serialization` from 2.4.0 to 2.4.10 - [Release notes](https://github.com/JetBrains/kotlin/releases) - [Changelog](https://github.com/JetBrains/kotlin/blob/master/ChangeLog.md) - [Commits](https://github.com/JetBrains/kotlin/compare/v2.4.0...v2.4.10) Updates `org.junit.jupiter:junit-jupiter-api` from 6.1.1 to 6.1.2 - [Release notes](https://github.com/junit-team/junit-framework/releases) - [Commits](https://github.com/junit-team/junit-framework/compare/r6.1.1...r6.1.2) Updates `org.junit.jupiter:junit-jupiter-engine` from 6.1.1 to 6.1.2 - [Release notes](https://github.com/junit-team/junit-framework/releases) - [Commits](https://github.com/junit-team/junit-framework/compare/r6.1.1...r6.1.2) Updates `org.junit.vintage:junit-vintage-engine` from 6.1.1 to 6.1.2 - [Release notes](https://github.com/junit-team/junit-framework/releases) - [Commits](https://github.com/junit-team/junit-framework/compare/r6.1.1...r6.1.2) Updates `org.bouncycastle:bcprov-jdk18on` from 1.84 to 1.85 - [Changelog](https://github.com/bcgit/bc-java/blob/main/docs/releasenotes.html) - [Commits](https://github.com/bcgit/bc-java/commits) Updates `org.bouncycastle:bcpkix-jdk18on` from 1.84 to 1.85 - [Changelog](https://github.com/bcgit/bc-java/blob/main/docs/releasenotes.html) - [Commits](https://github.com/bcgit/bc-java/commits) Updates `org.bouncycastle:bcpkix-jdk18on` from 1.84 to 1.85 - [Changelog](https://github.com/bcgit/bc-java/blob/main/docs/releasenotes.html) - [Commits](https://github.com/bcgit/bc-java/commits) Updates `com.android.application` from 9.2.1 to 9.3.0 Updates `com.android.test` from 9.2.1 to 9.3.0 Updates `com.android.library` from 9.2.1 to 9.3.0 Updates `org.jetbrains.kotlin.android` from 2.4.0 to 2.4.10 - [Release notes](https://github.com/JetBrains/kotlin/releases) - [Changelog](https://github.com/JetBrains/kotlin/blob/master/ChangeLog.md) - [Commits](https://github.com/JetBrains/kotlin/compare/v2.4.0...v2.4.10) Updates `org.jetbrains.kotlin.plugin.compose` from 2.4.0 to 2.4.10 - [Release notes](https://github.com/JetBrains/kotlin/releases) - [Changelog](https://github.com/JetBrains/kotlin/blob/master/ChangeLog.md) - [Commits](https://github.com/JetBrains/kotlin/compare/v2.4.0...v2.4.10) Updates `com.google.devtools.ksp` from 2.3.9 to 2.3.10 - [Release notes](https://github.com/google/ksp/releases) - [Commits](https://github.com/google/ksp/compare/2.3.9...2.3.10) Updates `com.android.test` from 9.2.1 to 9.3.0 Updates `com.android.library` from 9.2.1 to 9.3.0 Updates `org.jetbrains.kotlin.plugin.serialization` from 2.4.0 to 2.4.10 - [Release notes](https://github.com/JetBrains/kotlin/releases) - [Changelog](https://github.com/JetBrains/kotlin/blob/master/ChangeLog.md) - [Commits](https://github.com/JetBrains/kotlin/compare/v2.4.0...v2.4.10) --- updated-dependencies: - dependency-name: org.jetbrains.kotlin:kotlin-metadata-jvm dependency-version: 2.4.10 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: gradle-dependencies - dependency-name: org.junit.platform:junit-platform-launcher dependency-version: 6.1.2 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: gradle-dependencies - dependency-name: org.junit.jupiter:junit-jupiter-api dependency-version: 6.1.2 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: gradle-dependencies - dependency-name: org.junit.jupiter:junit-jupiter-engine dependency-version: 6.1.2 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: gradle-dependencies - dependency-name: org.junit.vintage:junit-vintage-engine dependency-version: 6.1.2 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: gradle-dependencies - dependency-name: com.google.genai:google-genai dependency-version: 1.61.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: gradle-dependencies - dependency-name: org.junit.jupiter:junit-jupiter-params dependency-version: 6.1.2 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: gradle-dependencies - dependency-name: org.jetbrains.kotlin:kotlin-test-junit dependency-version: 2.4.10 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: gradle-dependencies - dependency-name: org.jetbrains.kotlin.android dependency-version: 2.4.10 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: gradle-dependencies - dependency-name: org.jetbrains.kotlin.plugin.compose dependency-version: 2.4.10 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: gradle-dependencies - dependency-name: org.jetbrains.kotlin.plugin.serialization dependency-version: 2.4.10 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: gradle-dependencies - dependency-name: org.junit.jupiter:junit-jupiter-api dependency-version: 6.1.2 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: gradle-dependencies - dependency-name: org.junit.jupiter:junit-jupiter-engine dependency-version: 6.1.2 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: gradle-dependencies - dependency-name: org.junit.vintage:junit-vintage-engine dependency-version: 6.1.2 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: gradle-dependencies - dependency-name: org.bouncycastle:bcprov-jdk18on dependency-version: '1.85' dependency-type: direct:production update-type: version-update:semver-minor dependency-group: gradle-dependencies - dependency-name: org.bouncycastle:bcpkix-jdk18on dependency-version: '1.85' dependency-type: direct:production update-type: version-update:semver-minor dependency-group: gradle-dependencies - dependency-name: org.bouncycastle:bcpkix-jdk18on dependency-version: '1.85' dependency-type: direct:production update-type: version-update:semver-minor dependency-group: gradle-dependencies - dependency-name: com.android.application dependency-version: 9.3.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: gradle-dependencies - dependency-name: com.android.test dependency-version: 9.3.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: gradle-dependencies - dependency-name: com.android.library dependency-version: 9.3.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: gradle-dependencies - dependency-name: org.jetbrains.kotlin.android dependency-version: 2.4.10 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: gradle-dependencies - dependency-name: org.jetbrains.kotlin.plugin.compose dependency-version: 2.4.10 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: gradle-dependencies - dependency-name: com.google.devtools.ksp dependency-version: 2.3.10 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: gradle-dependencies - dependency-name: com.android.test dependency-version: 9.3.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: gradle-dependencies - dependency-name: com.android.library dependency-version: 9.3.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: gradle-dependencies - dependency-name: org.jetbrains.kotlin.plugin.serialization dependency-version: 2.4.10 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: gradle-dependencies ... Signed-off-by: dependabot[bot] --- build.gradle.kts | 2 +- gradle/libs.versions.toml | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index 58e6b1614f..08a447d767 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -13,7 +13,7 @@ plugins { allprojects { configurations.all { resolutionStrategy { - force("org.jetbrains.kotlin:kotlin-metadata-jvm:2.4.0") + force("org.jetbrains.kotlin:kotlin-metadata-jvm:2.4.10") force("org.jetbrains.kotlinx:kotlinx-metadata-jvm:0.9.0") } } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 588b6b9431..911d1663d5 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,8 +1,8 @@ [versions] accompanistDrawablepainter = "0.37.3" -agp = "9.2.1" +agp = "9.3.0" app = "1.7.0" -googleGenai = "1.60.0" +googleGenai = "1.61.0" googlePlayServicesCast = "22.3.1" animation = "1.11.4" appcompat = "1.7.1" @@ -22,11 +22,11 @@ gson = "2.14.0" hiltAndroid = "2.60.1" hiltNavigationCompose = "1.4.0" ktor = "3.5.1" -kotlin = "2.4.0" +kotlin = "2.4.10" coreKtx = "1.19.0" junit = "4.13.2" junitVersion = "1.3.0" -junitJupiter = "6.1.1" +junitJupiter = "6.1.2" espressoCore = "3.7.0" kotlinx-coroutines = "1.11.0" kotlinxCollectionsImmutable = "0.5.1" @@ -59,12 +59,12 @@ datastore = "1.2.1" credentials = "1.6.0" googleid = "1.2.0" androidxTestCore = "1.7.0" -junit5 = "6.1.1" +junit5 = "6.1.2" kuromoji = "0.9.0" pinyin4j = "2.5.1" securityCrypto = "1.1.0" netty = "4.2.28.Final" -bouncycastle = "1.84" +bouncycastle = "1.85" commons-lang3 = "3.20.0" jdom2 = "2.0.6.1" jose4j = "0.9.6" @@ -77,7 +77,7 @@ dagger = "2.60.1" javax-inject = "1" # Annotations procesing -ksp = "2.3.9" +ksp = "2.3.10" smoothCornerRectAndroidCompose = "v1.0.0" spleeterAndroidIos = "1.0.2" tensorflowLite = "2.17.0" From c7f5afa3b88f784dda6b384413dbf97bd274ee3b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 15 Jul 2026 19:13:34 +0000 Subject: [PATCH 49/73] chore(deps): bump androidx.compose.material3:material3 Bumps the gradle-dependencies group with 1 update in the / directory: androidx.compose.material3:material3. Updates `androidx.compose.material3:material3` from 1.5.0-alpha23 to 1.5.0-alpha24 --- updated-dependencies: - dependency-name: androidx.compose.material3:material3 dependency-version: 1.5.0-alpha24 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: gradle-dependencies ... Signed-off-by: dependabot[bot] --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index ebecf99ee2..05fd3669ec 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -35,7 +35,7 @@ lifecycleRuntimeKtx = "2.11.0" activityCompose = "1.13.0" composeBom = "2026.06.01" material = "1.14.0" -material3 = "1.5.0-alpha23" +material3 = "1.5.0-alpha24" media = "1.8.0" media3Session = "1.10.1" media3Transformer = "1.10.1" From d56e11ce4e1b2ab73189893522fa200e3c4d538a Mon Sep 17 00:00:00 2001 From: Dae Euhwa Date: Wed, 15 Jul 2026 14:14:00 -0500 Subject: [PATCH 50/73] chore: re-add missing aboutlibraries dependency --- gradle/libs.versions.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index ebecf99ee2..4100685423 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,5 +1,6 @@ [versions] accompanistDrawablepainter = "0.37.3" +aboutLibraries = "15.0.3" agp = "9.3.0" app = "1.7.0" googleGenai = "1.61.0" From 6a52fc7e0662d9af0d134db9a637893a21a57180 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 16 Jul 2026 22:28:19 +0000 Subject: [PATCH 51/73] bump the gradle-dependencies group with 4 updates Bumps the gradle-dependencies group with 4 updates: [com.mikepenz:aboutlibraries-core](https://github.com/mikepenz/AboutLibraries), [com.mikepenz:aboutlibraries-compose-m3](https://github.com/mikepenz/AboutLibraries), com.mikepenz.aboutlibraries.plugin.android and [com.google.genai:google-genai](https://github.com/googleapis/java-genai). Updates `com.mikepenz:aboutlibraries-core` from 15.0.3 to 15.0.4 - [Release notes](https://github.com/mikepenz/AboutLibraries/releases) - [Commits](https://github.com/mikepenz/AboutLibraries/compare/15.0.3...15.0.4) Updates `com.mikepenz:aboutlibraries-compose-m3` from 15.0.3 to 15.0.4 - [Release notes](https://github.com/mikepenz/AboutLibraries/releases) - [Commits](https://github.com/mikepenz/AboutLibraries/compare/15.0.3...15.0.4) Updates `com.mikepenz.aboutlibraries.plugin.android` from 15.0.3 to 15.0.4 Updates `com.mikepenz:aboutlibraries-compose-m3` from 15.0.3 to 15.0.4 - [Release notes](https://github.com/mikepenz/AboutLibraries/releases) - [Commits](https://github.com/mikepenz/AboutLibraries/compare/15.0.3...15.0.4) Updates `com.google.genai:google-genai` from 1.61.0 to 1.62.0 - [Release notes](https://github.com/googleapis/java-genai/releases) - [Changelog](https://github.com/googleapis/java-genai/blob/main/CHANGELOG.md) - [Commits](https://github.com/googleapis/java-genai/compare/v1.61.0...v1.62.0) Updates `com.mikepenz.aboutlibraries.plugin.android` from 15.0.3 to 15.0.4 --- updated-dependencies: - dependency-name: com.mikepenz:aboutlibraries-core dependency-version: 15.0.4 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: gradle-dependencies - dependency-name: com.mikepenz:aboutlibraries-compose-m3 dependency-version: 15.0.4 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: gradle-dependencies - dependency-name: com.mikepenz.aboutlibraries.plugin.android dependency-version: 15.0.4 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: gradle-dependencies - dependency-name: com.mikepenz:aboutlibraries-compose-m3 dependency-version: 15.0.4 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: gradle-dependencies - dependency-name: com.google.genai:google-genai dependency-version: 1.62.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: gradle-dependencies - dependency-name: com.mikepenz.aboutlibraries.plugin.android dependency-version: 15.0.4 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: gradle-dependencies ... Signed-off-by: dependabot[bot] --- gradle/libs.versions.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index f4f2b8b8ca..2631e6b176 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,9 +1,9 @@ [versions] accompanistDrawablepainter = "0.37.3" -aboutLibraries = "15.0.3" +aboutLibraries = "15.0.4" agp = "9.3.0" app = "1.7.0" -googleGenai = "1.61.0" +googleGenai = "1.62.0" googlePlayServicesCast = "22.3.1" animation = "1.11.4" appcompat = "1.7.1" From 4e828c30478a84f8a16e490ba3d1f000ad2dab89 Mon Sep 17 00:00:00 2001 From: Dae Euhwa Date: Fri, 17 Jul 2026 03:51:23 -0500 Subject: [PATCH 52/73] fix: move @OptIn(ExperimentalMaterial3Api) to function level to fix release build --- .../presentation/components/subcomps/LibraryActionRow.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/components/subcomps/LibraryActionRow.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/components/subcomps/LibraryActionRow.kt index fb04d8a3d8..9bb1eae421 100644 --- a/app/src/main/java/com/theveloper/pixelplay/presentation/components/subcomps/LibraryActionRow.kt +++ b/app/src/main/java/com/theveloper/pixelplay/presentation/components/subcomps/LibraryActionRow.kt @@ -82,6 +82,7 @@ import androidx.compose.ui.res.stringResource val defaultShape = RoundedCornerShape(26.dp) // Fallback shape val genHeight = 42.dp +@OptIn(ExperimentalMaterial3Api::class) @Composable fun LibraryActionRow( onMainActionClick: () -> Unit, @@ -352,7 +353,6 @@ fun LibraryActionRow( } val tooltipState = rememberTooltipState() - @OptIn(ExperimentalMaterial3Api::class) TooltipBox( positionProvider = TooltipDefaults.rememberTooltipPositionProvider(TooltipAnchorPosition.Above), tooltip = { From c10f74344f1a58cc5e220faed2866426a42075b4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 23 Jul 2026 22:28:03 +0000 Subject: [PATCH 53/73] chore(deps): bump the gradle-dependencies group across 1 directory with 4 updates Bumps the gradle-dependencies group with 4 updates in the / directory: [com.google.genai:google-genai](https://github.com/googleapis/java-genai), com.android.application, com.android.test and com.android.library. Updates `com.google.genai:google-genai` from 1.62.0 to 1.63.0 - [Release notes](https://github.com/googleapis/java-genai/releases) - [Changelog](https://github.com/googleapis/java-genai/blob/main/CHANGELOG.md) - [Commits](https://github.com/googleapis/java-genai/compare/v1.62.0...v1.63.0) Updates `com.android.application` from 9.3.0 to 9.3.1 Updates `com.android.test` from 9.3.0 to 9.3.1 Updates `com.android.library` from 9.3.0 to 9.3.1 Updates `com.android.test` from 9.3.0 to 9.3.1 Updates `com.android.library` from 9.3.0 to 9.3.1 --- updated-dependencies: - dependency-name: com.google.genai:google-genai dependency-version: 1.63.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: gradle-dependencies - dependency-name: com.android.application dependency-version: 9.3.1 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: gradle-dependencies - dependency-name: com.android.test dependency-version: 9.3.1 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: gradle-dependencies - dependency-name: com.android.library dependency-version: 9.3.1 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: gradle-dependencies - dependency-name: com.android.test dependency-version: 9.3.1 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: gradle-dependencies - dependency-name: com.android.library dependency-version: 9.3.1 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: gradle-dependencies ... Signed-off-by: dependabot[bot] --- gradle/libs.versions.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 2631e6b176..c03d8b480d 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,9 +1,9 @@ [versions] accompanistDrawablepainter = "0.37.3" aboutLibraries = "15.0.4" -agp = "9.3.0" +agp = "9.3.1" app = "1.7.0" -googleGenai = "1.62.0" +googleGenai = "1.63.0" googlePlayServicesCast = "22.3.1" animation = "1.11.4" appcompat = "1.7.1" From 9ac48f64068410725ffaaf65af0877057d640290 Mon Sep 17 00:00:00 2001 From: Dae Euhwa Date: Mon, 27 Jul 2026 20:20:08 -0500 Subject: [PATCH 54/73] feat(i18n): add Japanese, Vietnamese, Traditional Chinese translations and romanization - Add full translations for JA, VI, ZH-TW locale files - Add MultiLangRomanizer for Thai, Arabic, Greek, Hebrew, Vietnamese - Add romanization and Cyrillic language detection - Korean, German, Spanish, French, Arabic, Indonesian, Italian, Norwegian, Russian, Turkish translations - Add locale configuration and plural support --- .../main/res/values-ar/strings_settings.xml | 4 +- .../main/res/values-de/strings_changelogs.xml | 18 + .../main/res/values-de/strings_settings.xml | 9 +- .../main/res/values-es/strings_settings.xml | 4 +- .../main/res/values-fr/strings_settings.xml | 4 +- .../main/res/values-in/strings_settings.xml | 4 +- .../main/res/values-it/strings_settings.xml | 4 +- app/src/main/res/values-ja/strings.xml | 128 ++++ .../main/res/values-ja/strings_changelogs.xml | 9 + .../res/values-ja/strings_cloud_services.xml | 226 ++++++ .../main/res/values-ja/strings_equalizer.xml | 57 ++ .../res/values-ja/strings_home_screen.xml | 276 ++++++++ .../main/res/values-ja/strings_library.xml | 558 +++++++++++++++ app/src/main/res/values-ja/strings_player.xml | 195 ++++++ .../main/res/values-ja/strings_screens.xml | 244 +++++++ .../main/res/values-ja/strings_settings.xml | 654 ++++++++++++++++++ app/src/main/res/values-ja/strings_widget.xml | 17 + .../main/res/values-ko/strings_changelogs.xml | 18 + .../main/res/values-ko/strings_settings.xml | 9 +- .../main/res/values-nb/strings_changelogs.xml | 18 + .../main/res/values-nb/strings_settings.xml | 9 +- .../main/res/values-ru/strings_changelogs.xml | 18 + .../main/res/values-ru/strings_settings.xml | 9 +- .../main/res/values-tr/strings_settings.xml | 4 +- app/src/main/res/values-vi/strings.xml | 128 ++++ .../main/res/values-vi/strings_changelogs.xml | 150 ++++ .../res/values-vi/strings_cloud_services.xml | 228 ++++++ .../main/res/values-vi/strings_equalizer.xml | 57 ++ .../res/values-vi/strings_home_screen.xml | 277 ++++++++ .../main/res/values-vi/strings_library.xml | 566 +++++++++++++++ app/src/main/res/values-vi/strings_player.xml | 198 ++++++ .../main/res/values-vi/strings_screens.xml | 244 +++++++ .../main/res/values-vi/strings_settings.xml | 654 ++++++++++++++++++ app/src/main/res/values-vi/strings_widget.xml | 17 + .../res/values-zh-rCN/strings_settings.xml | 4 +- app/src/main/res/values-zh-rTW/strings.xml | 128 ++++ .../res/values-zh-rTW/strings_changelogs.xml | 132 ++++ .../values-zh-rTW/strings_cloud_services.xml | 228 ++++++ .../res/values-zh-rTW/strings_equalizer.xml | 57 ++ .../res/values-zh-rTW/strings_home_screen.xml | 277 ++++++++ .../res/values-zh-rTW/strings_library.xml | 566 +++++++++++++++ .../main/res/values-zh-rTW/strings_player.xml | 198 ++++++ .../res/values-zh-rTW/strings_screens.xml | 244 +++++++ .../res/values-zh-rTW/strings_settings.xml | 652 +++++++++++++++++ .../main/res/values-zh-rTW/strings_widget.xml | 17 + app/src/main/res/values/plurals.xml | 59 ++ app/src/main/res/values/strings_settings.xml | 9 +- app/src/main/res/xml/locales_config.xml | 11 + 48 files changed, 7585 insertions(+), 12 deletions(-) create mode 100644 app/src/main/res/values-ja/strings.xml create mode 100644 app/src/main/res/values-ja/strings_changelogs.xml create mode 100644 app/src/main/res/values-ja/strings_cloud_services.xml create mode 100644 app/src/main/res/values-ja/strings_equalizer.xml create mode 100644 app/src/main/res/values-ja/strings_home_screen.xml create mode 100644 app/src/main/res/values-ja/strings_library.xml create mode 100644 app/src/main/res/values-ja/strings_player.xml create mode 100644 app/src/main/res/values-ja/strings_screens.xml create mode 100644 app/src/main/res/values-ja/strings_settings.xml create mode 100644 app/src/main/res/values-ja/strings_widget.xml create mode 100644 app/src/main/res/values-vi/strings.xml create mode 100644 app/src/main/res/values-vi/strings_changelogs.xml create mode 100644 app/src/main/res/values-vi/strings_cloud_services.xml create mode 100644 app/src/main/res/values-vi/strings_equalizer.xml create mode 100644 app/src/main/res/values-vi/strings_home_screen.xml create mode 100644 app/src/main/res/values-vi/strings_library.xml create mode 100644 app/src/main/res/values-vi/strings_player.xml create mode 100644 app/src/main/res/values-vi/strings_screens.xml create mode 100644 app/src/main/res/values-vi/strings_settings.xml create mode 100644 app/src/main/res/values-vi/strings_widget.xml create mode 100644 app/src/main/res/values-zh-rTW/strings.xml create mode 100644 app/src/main/res/values-zh-rTW/strings_changelogs.xml create mode 100644 app/src/main/res/values-zh-rTW/strings_cloud_services.xml create mode 100644 app/src/main/res/values-zh-rTW/strings_equalizer.xml create mode 100644 app/src/main/res/values-zh-rTW/strings_home_screen.xml create mode 100644 app/src/main/res/values-zh-rTW/strings_library.xml create mode 100644 app/src/main/res/values-zh-rTW/strings_player.xml create mode 100644 app/src/main/res/values-zh-rTW/strings_screens.xml create mode 100644 app/src/main/res/values-zh-rTW/strings_settings.xml create mode 100644 app/src/main/res/values-zh-rTW/strings_widget.xml create mode 100644 app/src/main/res/values/plurals.xml diff --git a/app/src/main/res/values-ar/strings_settings.xml b/app/src/main/res/values-ar/strings_settings.xml index afced2d2d2..b5dfcfddb9 100644 --- a/app/src/main/res/values-ar/strings_settings.xml +++ b/app/src/main/res/values-ar/strings_settings.xml @@ -13,7 +13,9 @@ السلوك الإيماءات، التفاعل اللمسي، وسلوك التنقل دمج الذكاء الاصطناعي (β) - مزودو الذكاء الاصطناعي، مفاتيح الـ API، وإعدادات النموذج + AI providers, API keys, model selection, and activity logs + Generation Parameters + Temperature, sampling, prompts, and song data configuration النسخ الاحتياطي والاستعادة تصدير واستعادة بيانات تطبيقك الشخصية خيارات المطورين diff --git a/app/src/main/res/values-de/strings_changelogs.xml b/app/src/main/res/values-de/strings_changelogs.xml index b5e8947760..a0863d12d2 100644 --- a/app/src/main/res/values-de/strings_changelogs.xml +++ b/app/src/main/res/values-de/strings_changelogs.xml @@ -129,4 +129,22 @@ Lokalisierung: Spanisch, Französisch, Russisch, Vereinfachtes Chinesisch, Indonesisch, Italienisch + + Google Drive-Integration mit Player-Lebenszyklus-Management. + Batch-Bearbeitung von Song-Metadaten (Tags und Cover-Art). + KI-Übersetzung von Songtexten mit anpassbaren Wear OS-Einstellungen. + Latenzdiagnose-Tool und Mehrfachauswahl auf dem Suchbildschirm. + Arabische & Türkische Unterstützung, mit lokalisierten http-URL-Lokalisierungsoptionen. + + + Drastische Akku-Ersparnis (Audio-Offloading und UI-Polling-Gates). + Optimiertes Queue-Management (schnellere Einfügungen und explizite Indizierung). + Material 3 Expressive Motion-Animationen für Übergangsbildschirme. + Überarbeitete Bibliothekssynchronisierung mittels gedrosseltem Scannen. + + + Behobene Ruckel-/Aussetzer bei der Wiedergabe und Pufferungsprobleme. + Behobene Synchronisation externer Songlöschungen und Metadatenkonsistenz. + Behobene Speicherprobleme, Abstürze und Layout-Fehler auf Wear OS und Telefon. + \ No newline at end of file diff --git a/app/src/main/res/values-de/strings_settings.xml b/app/src/main/res/values-de/strings_settings.xml index e6fcda8436..db1a0dd223 100644 --- a/app/src/main/res/values-de/strings_settings.xml +++ b/app/src/main/res/values-de/strings_settings.xml @@ -7,10 +7,14 @@ Design, Layout und Farben Wiedergabe Audio, Crossfade und Hintergrundwiedergabe + Songtexte + Quellpriorität, immersiver Modus und Importverwaltung Verhalten Gesten, Haptik und Navigation KI-Integration (β) - Anbieter, API Keys und Modelle + AI providers, API keys, model selection, and activity logs + Generation Parameters + Temperature, sampling, prompts, and song data configuration Backup & Wiederherstellen App-Daten exportieren und importieren Entwickleroptionen @@ -174,6 +178,9 @@ Koreanisch Norwegisch Bokmål Türkisch + 日本語 + 繁體中文 + Tiếng Việt App-Design Hell, Dunkel oder System-Design – ganz nach Geschmack. Hell diff --git a/app/src/main/res/values-es/strings_settings.xml b/app/src/main/res/values-es/strings_settings.xml index 79a7e62578..bc7c980dfe 100644 --- a/app/src/main/res/values-es/strings_settings.xml +++ b/app/src/main/res/values-es/strings_settings.xml @@ -10,7 +10,9 @@ Comportamiento Gestos, vibración y navegación Integración IA (β) - Proveedores de IA, claves API y modelos + AI providers, API keys, model selection, and activity logs + Generation Parameters + Temperature, sampling, prompts, and song data configuration Copia y restauración Exporta y recupera tus datos personales Opciones de desarrollador diff --git a/app/src/main/res/values-fr/strings_settings.xml b/app/src/main/res/values-fr/strings_settings.xml index 5932d39357..d6cc272298 100644 --- a/app/src/main/res/values-fr/strings_settings.xml +++ b/app/src/main/res/values-fr/strings_settings.xml @@ -10,7 +10,9 @@ Comportement Gestes, retour haptique et comportement de navigation Intégration IA (β) - Fournisseurs d\'IA, clés API et paramètres du modèle + AI providers, API keys, model selection, and activity logs + Generation Parameters + Temperature, sampling, prompts, and song data configuration Sauvegarde et restauration Exporter et récupérer vos données personnelles de l\'application Options développeur diff --git a/app/src/main/res/values-in/strings_settings.xml b/app/src/main/res/values-in/strings_settings.xml index cabbc3ab0d..bd6ee6c5f3 100644 --- a/app/src/main/res/values-in/strings_settings.xml +++ b/app/src/main/res/values-in/strings_settings.xml @@ -10,7 +10,9 @@ Perilaku Gerakan, haptik, dan perilaku navigasi Integrasi AI (β) - Penyedia AI, kunci API, dan pengaturan model + AI providers, API keys, model selection, and activity logs + Generation Parameters + Temperature, sampling, prompts, and song data configuration Cadangkan & Pulihkan Ekspor dan pulihkan data aplikasi pribadi Anda Opsi Pengembang diff --git a/app/src/main/res/values-it/strings_settings.xml b/app/src/main/res/values-it/strings_settings.xml index 24ad47887e..eefd35f1c2 100644 --- a/app/src/main/res/values-it/strings_settings.xml +++ b/app/src/main/res/values-it/strings_settings.xml @@ -10,7 +10,9 @@ Comportamento Gesti, feedback aptico e comportamento navigazione Integrazione AI (β) - Provider AI, chiavi API e impostazioni modello + AI providers, API keys, model selection, and activity logs + Generation Parameters + Temperature, sampling, prompts, and song data configuration Backup e ripristino Esporta e recupera i tuoi dati personali dell\'app Opzioni sviluppatore diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml new file mode 100644 index 0000000000..5dedb3c73a --- /dev/null +++ b/app/src/main/res/values-ja/strings.xml @@ -0,0 +1,128 @@ + + + PixelPlayer + 音楽プレイヤー + アプリ名の変更について + 商標上の理由により、アプリ名を PixelPlay から PixelPlayer に変更しました。引き続きお楽しみください! + 今後表示しない + + + ホーム + 検索 + ライブラリ + + + 特別な権限が必要です + 曲のメタデータ(.mp3 ファイル)を編集するには、PixelPlayer にすべてのファイルへの特別なアクセス権限が必要です。これにより、トラックのタグを直接変更できます。メタデータ編集を有効にするには、次の画面でこの権限を許可してください。 + 権限を許可 + + + すぐに再生 + このオーディオファイルを開けませんでした。 + フルプレイヤーを開く + + + シャッフル + すべての曲をシャッフル + すべてシャッフル + 最後のプレイリスト + 開けるプレイリストがありません + + + Play ストアを開く + ベータを続ける + Play ストアのリンクは GitHub の設定から有効化されます。 + PixelPlayer が Google Play で公開されました + リリース更新は Google Play の安定版チャンネルをご利用ください。ベータビルドも引き続き提供されます。 + PixelPlayer + リリースのお知らせ + 近日公開 + + + PixelPlayer をご利用いただきありがとうございます! + ハイスコア %1$d + 閉じる + スコア + レベル %1$d + ライフ + レベルクリア! + ゲームオーバー + スコア: %1$d + もう一度? + 次のレベル + ゲームを再起動 + タップして再起動 + ランダムに音楽を再生 + ブロック崩し + ハイスコア %1$d + プレイ + ドラッグしてパドルを動かす + + + プレイヤーを閉じる + 再生操作を処理中… + 再生エラー: %1$s + + + 戻る + OK + キャンセル + 閉じる + エラー + 検索 + 検索をクリア + すべて + 確認 + 保存しました! + 選択済み + %1$d%% + アーティスト + すべて選択 + クリア + 不明なエラー + + + 保存 + 完了 + リセット + 適用 + シャッフル + コピー + 共有 + 元に戻す + インポート + 削除 + エクスポート + 結合 + 名前を変更 + 作成 + 歌詞 + 設定 + アルバムアート + プレイリスト + 不明なトラック + 不明なアーティスト + 不明なアルバム + 閉じる + 追加 + 削除 + 再生 + 前のトラック + 次のトラック + お気に入り + 一時停止 + リピート + オプション + シャッフル再生 + %1$s のその他のオプション + メニューを展開 + 次へ + 完了 + デフォルトに戻す + すべてエクスポート + すべて結合 + すべて共有 + アルバムを再生 + アルバムをシャッフル再生 + %1$s のアルバムアート + diff --git a/app/src/main/res/values-ja/strings_changelogs.xml b/app/src/main/res/values-ja/strings_changelogs.xml new file mode 100644 index 0000000000..85544d5ba6 --- /dev/null +++ b/app/src/main/res/values-ja/strings_changelogs.xml @@ -0,0 +1,9 @@ + + + 変更履歴 + GitHub で見る + 改善 + 修正 + 新機能 + 追加 + diff --git a/app/src/main/res/values-ja/strings_cloud_services.xml b/app/src/main/res/values-ja/strings_cloud_services.xml new file mode 100644 index 0000000000..a2e5bac754 --- /dev/null +++ b/app/src/main/res/values-ja/strings_cloud_services.xml @@ -0,0 +1,226 @@ + + + + Telegram ログイン + 番号を編集中です。再送すると前のコードが無効になります。 + 処理中… + Telegram を初期化中… + ログアウト中… + セッションを閉じています… + セッションが閉じました。続けるにはログインを再度開いてください。 + 安全な Telegram セッションを準備中… + Telegram からの応答を待機中… + Telegram に接続 + Telegram に接続してチャンネルやチャットから音楽をストリーミングします。 + 電話番号 + Telegram の番号を入力してください。後で戻って編集することもできます。 + 電話番号 + 81 + 09012345678 + コードを送信 + 確認コード + Telegram からのコードを入力してください。番号が間違っている場合は戻って修正してください。 + コード + 12345 + 電話番号を編集 + コードを再送 + コードを確認 + 二段階認証パスワード + Telegram のパスワードを入力してください。番号を修正するために戻ることもできます。 + パスワード + パスワードを確認 + しばらくお待ちください… + + + Telegram チャンネル + チャンネルを追加 + Telegram パブリックチャンネル + 同期中 + 今すぐ同期 + トピックを折りたたむ + トピックを表示 + チャンネルオプション + トピック + チャンネルを同期中 + Telegram から曲を更新中 + このチャンネルから最新の曲を取得 + チャンネルを削除 + 同期を停止してキャッシュされた曲を削除 + チャンネルを削除しますか? + %1$s の同期が停止し、このチャンネルのキャッシュされた曲がすべて削除されます。 + 削除 + 同期済みチャンネルがありません + Telegram のパブリックチャンネルを追加して\n音楽ライブラリを同期しましょう + チャンネルを追加 + 未同期 + %1$s に同期 + + + チャンネルを追加 + 音楽を同期する Telegram パブリックチャンネルを検索 + \@チャンネル名またはリンク + 検索中… + チャンネルを検索 + パブリックチャンネルのユーザー名またはリンクを入力して\nオーディオファイルを同期してください + + + %d 曲 + + + %d トピック + + + + Subsonic + Navidrome、Airsonic などの Subsonic 互換サーバーを管理します。 + + + 同期をタップして Jellyfin のプレイリストを取得してください + Jellyfin サーバーの接続を管理します。 + + + 音楽フォルダ + + をタップして Drive フォルダを追加 + フォルダがまだ追加されていません + %1$d フォルダが同期済み + フォルダを追加 + + + プレイリストの種類を選択 + 同期するプレイリストを選択: + すべてのプレイリスト + 作成 & お気に入り + 作成したプレイリスト + お気に入りのプレイリスト + + + %1$d プレイリストが同期済み + プレイリスト + 同期 + まだプレイリストが同期されていません + 同期をタップしてプレイリストを取得してください + クイックアクション + ライブラリを同期 + 切断 + %1$d 曲 + + + 同期中 + ライブラリを同期中… + プレイリストを取得中… + プレイリストを同期中: %1$s + ローカルライブラリを更新中… + 同期完了 + アルバムリストを取得中… + %1$s から曲を取得中… + %1$d 曲をデータベースに保存中… + ライブラリに曲が見つかりません + ライブラリ同期完了 + 同期中… + エラー: %1$s + + + 同期 + すべて同期 + ログアウト + すべてのプレイリストを同期 + ユーザーアバター + + + インターネット接続がありません + このコンテンツにはインターネット接続が必要です。ネットワーク設定を確認して再試行してください。 + オフラインです + このコンテンツにアクセスするにはインターネット接続を確認して再試行してください。 + + + 接続 + 接続中… + サーバー URL とアカウントの認証情報を入力してください。 + 接続詳細 + パスワードを非表示 + パスワード + パスワードを入力 + http:// を入力 + サーバー URL + パスワードを表示 + Telegram + ユーザー名 + admin + ようこそ、%1$s! + + + Navidrome、Gonic、Airsonic などの Subsonic 互換サーバーに対応 + Navidrome、Airsonic、Gonic、Ampache などの Subsonic API 互換サーバーをサポートします。 + サーバーが対応している場合はアプリパスワードも使用できます。 + https:// を入力 + セルフホスト型音楽サーバーに接続 + Navidrome + サーバーの完全な https:// ベースアドレスを使用してください。 + https://music.example.com + Subsonic または Navidrome のアカウント名です。 + Subsonic / Navidrome + Subsonic + + + Jellyfin サーバー URL とアカウントの認証情報を入力してください。 + 音楽ライブラリをストリーミングするために Jellyfin サーバーに接続します + Jellyfin サーバーに接続します。ローカルネットワークアクセスには HTTP と HTTPS の両方がサポートされています。 + Jellyfin + Jellyfin アカウントのパスワード。 + Jellyfin メディアサーバーに接続 + Jellyfin + ポートを含む Jellyfin サーバーの完全な URL。 + http://192.168.1.100:8096 + Jellyfin アカウントのユーザー名。 + + + Google Drive から直接音楽ファイルをストリーミング + Google Drive に接続 + Google Drive に接続しました! + 「PixelPlayer Music」を作成 + ここに音楽用の新しいフォルダを作成 + フォルダがありません + フォルダを開く + 音楽ソースとして使用するフォルダを選択または作成 + 音楽フォルダを選択 + Google Drive をセットアップ中… + Google でサインイン + Google Drive + 使用 + + + セッション Cookie を読み取れませんでした。 + 完了 + 終了 + Cookie が見つかりません。先にログインしてください。 + ページの読み込みに時間がかかっています。更新するか別のネットワークをお試しください。 + + + 保存中… + 残る + ページの読み込みがタイムアウトしました。進捗を失わずに再試行できます。 + Web で戻る + 後で戻れます。閉じると現在のページの状態は破棄されます。 + Web で進む + 更新 + 再試行 + ホームを開く + WebView の読み込みに失敗しました。 + + + NetEase の Cookie を読み取れませんでした: %1$s + NetEase のログインを終了しますか? + NetEase の読み込み中に HTTP %1$d エラーが発生しました。 + まだログインが検出されていません。完了を押す前に NetEase のログインを完了してください。 + NetEase Music にログイン + セキュリティについて: パスワードは NetEase のウェブページにのみ入力されます。PixelPlayer はライブラリを同期するためにセッション Cookie(MUSIC_U)を保存します。 + NetEase Music + + + QQ Music の Cookie を読み取れませんでした: %1$s + QQ Music のログインを終了しますか? + QQ Music の読み込み中に HTTP %1$d エラーが発生しました。 + まだログインが検出されていません。完了を押す前に QQ Music のログインを完了してください。 + QQ Music にログイン + セキュリティについて: パスワードは QQ Music のウェブページにのみ入力されます。PixelPlayer はライブラリを同期するためにセッション Cookie を保存します。 + QQ Music + diff --git a/app/src/main/res/values-ja/strings_equalizer.xml b/app/src/main/res/values-ja/strings_equalizer.xml new file mode 100644 index 0000000000..3c36811ada --- /dev/null +++ b/app/src/main/res/values-ja/strings_equalizer.xml @@ -0,0 +1,57 @@ + + + + 名前を入力してください + 名前を変更 + + + 表示モードを変更 + イコライザーを無効化 + イコライザーを有効化 + 編集 + プリセットを編集 + カスタムプリセット + プリセット + 更新 + バスブースト + バーチャライザー + ラウドネス + 非対応 + この端末では非対応 + 音量 + 周波数特性 + Hz + バス + ローミッド + ハイミッド + トレブル + バス / ロー + ミッド / ハイ + ページ %1$d + 時間をリセット + 新規保存 + + + 保存済みプリセット + カスタムプリセットがまだ保存されていません。 + ピンを外す + ピン留め + 名前を変更 + 削除 + + + カスタムプリセットを保存 + カスタムイコライザープリセットの名前を入力してください。 + プリセット名 + プリセット名を変更 + + + プリセットを管理 + ドラッグして並び替え • 目のアイコンで表示/非表示を切り替え + 並び替え + プリセットをリセット + デフォルトのプリセット順と表示状態に戻します。続けますか? + デフォルトに戻す + 表示 + 非表示 + diff --git a/app/src/main/res/values-ja/strings_home_screen.xml b/app/src/main/res/values-ja/strings_home_screen.xml new file mode 100644 index 0000000000..ac2021f3e9 --- /dev/null +++ b/app/src/main/res/values-ja/strings_home_screen.xml @@ -0,0 +1,276 @@ + + + + β + ベータ + クラウドストリーミング + 変更履歴 + クラウドストリーミング + クラウドアカウントから音楽をストリーミング + + + Beta 0.7.0 + β + PixelPlayer 0.7.0-beta へようこそ + バグ、クラッシュ、または試験的な機能が含まれている可能性があるベータビルドを使用しています。問題を報告して改善にご協力ください。 + 期待されること + バグ、クラッシュ、または未完成の機能が予期せず発生することがあります。 + 一部の機能は予告なく変更または削除される場合があります。 + ベータビルドはリリース版より不安定な場合があります。 + 既知の問題を報告する前に必ず最新版を確認してください。 + テスト中にベータビルドが変更、破損、または改善される可能性があること。 + GitHub Issue のショートカット + まず検索してから、バグ、クラッシュ、要望、質問に対する集中したレポートを作成してください。 + 既存の Issue を開く + Issue またはクラッシュを報告 + 再現手順、期待される結果、実際の結果、デバイス/OS の詳細を共有してください。 + 報告方法 + 新しい Issue を開く前の簡単なチェックリスト。 + Issue を開く前に + 重複を避けるために既存のオープンおよびクローズ済みの Issue を検索してください。 + 最新の PixelPlayer バージョンに更新して問題が引き続き発生することを確認してください。 + アプリを再起動して問題が続くことを確認してください。 + 再現を試みて正確な手順を書き留めてください。 + Issue の種類は? + バグ報告: 何かが正しく動作しない。 + 機能リクエスト: 新機能や改善の追加。 + 質問: Discussions が有効な場合はそちらを使用するか、question ラベルで Issue を開いてください。 + バグ報告 + 何かが正しく動作しないまたはクラッシュする場合にこれらのフィールドをコピーしてください。 + バグ報告 + 概要: + 期待される動作: + 現在の動作: + 再現手順: 1. 2. 3. + 頻度は? 常時 / 時々 / まれに。 + スクリーンショット / 動画: あれば。 + ログ / スタックトレース: あれば。 + 環境 + PixelPlayer バージョン: + インストール元: GitHub リリース、デバッグビルド、ナイトリービルドなど。 + Android バージョン: + 端末モデル: + 補足情報: SD カードの使用、特別な設定、権限など。 + 機能リクエスト + 新機能や改善を要望する場合にこれらのフィールドをコピーしてください。 + 問題の説明: 解決しようとしている問題は何ですか? + 提案する解決策: どのように機能すればよいですか? + 検討した代替案: 他のアプローチはありますか? + 範囲: どの画面やフローが影響を受けますか? + 利用可能であればモックアップや参考画像。 + タイトル、プライバシー、範囲 + 報告をトリアージしやすく安全に共有できるようにします。 + 良い Issue タイトルの例 + イコライザー: プリセットタブを切り替えるとインジケーターがずれる + 検索: 空のクエリで履歴リストが表示されない + 機能: 「最近追加された」プレイリストの並び替えオプションを追加 + 避けるべきこと + 「動かない」のような一般的な報告。 + 1 つの Issue に複数の無関係な問題を含める。 + プライベートデータが含まれた未編集のログやスクリーンショット。 + プライバシーについて + ログ、スクリーンショット、動画を投稿する前に個人情報やプライベートな情報を削除してください。 + ナイトリービルド + ナイトリーとリリースの違い、および破損した場合に含めるべき情報。 + ナイトリービルドは最新のコミットから生成され、未完成の変更、一時的なバグ、またはリグレッションが含まれる場合があります。公式リリースよりも試験的です。 + 利用可能な場合はリポジトリの GitHub Actions ワークフローアーティファクトからアクセスできます。 + ナイトリーの問題を報告する + ナイトリービルドで問題を報告する場合は、公式リリースではなくナイトリービルドで発生したことを必ず記載してください。可能であればビルド日、ワークフロー実行名または番号、コミット SHA を含めてください。また同じ問題が最新の公式リリースでも発生するか確認してください。 + Beta 0.5.0 アップグレード + クリーンインストール推奨 + beta 0.5.0 からのアップデートの場合、このアップデートでは古いキャッシュ状態ではなく新しいライブラリデータが必要な場合があります。 + メタデータやライブラリエントリがおかしい場合 + 曲のメタデータが間違っている、アーティストやアルバムが一致しない、または重複しているように見えるエントリは通常クリーンインストールで解決します。 + 今後表示しない + 了解 + + + 問題が発生しました + 前回のセッション中にアプリがクラッシュしました。クラッシュレポートを共有して修正にご協力ください。 + 日時: %1$s + エラー: + スタックトレース(プレビュー): + クラッシュログ + クラッシュログをクリップボードにコピーしました + PixelPlayer クラッシュレポート + クラッシュレポートを共有 + + + DJ ミキサー + + + あなたの\nミックス + まだ表示するデータがありません + PixelPlayer が曲を見つけるかソースを同期するとミックスがここに表示されます。 + 更新 + + + デイリーミックス + 履歴に基づく + デイリーミックスをすべて確認 + デイリーミックス + + + デイリーミックス + + %1$d 曲 • %2$s + + 再生する + AI プレイリストジェネレーター + + + デイリーミックスの作られ方 + デイリーミックスはお気に入りのよく再生される曲から作られます。好みのアーティストやジャンルのトラックも追加されるので新しい音楽を発見できます。 + 今日何を聴きたいか AI に伝えましょう + コストを抑えるため少量のサンプルを使用します + 更新中… + デイリーミックスを更新 + + + 完璧にキュレーション + デイリーミックス + あなたのソニックジャーニーの準備ができました + AI プレイリストジェネレーター + 雰囲気、ムード、アクティビティを説明して、ライブラリから AI に完璧なプレイリストをキュレーションさせましょう。 + プレイリストのサイズ + 最小曲数 + 最大曲数 + 例: チルな夜の雰囲気、アップビートなワークアウトエネルギー… + タップして再試行 + ソニックジャーニーが完成しました! + 再生準備完了 + 生成中… + プレイリストを生成 + + + 最近再生した曲 + + + 最近再生した曲 + 最新を再生 + %1$s に最近の再生はありません + 範囲を変更するか、タイムラインを埋めるためにもっと曲を再生してください。 + 最近再生した曲 + 今日 + 昨日 + + + リスニング統計 + 総再生回数 + 1 日平均 + トップトラック + %1$s • %2$d 回 + + + リスニング統計 + リスニング統計を更新 + 今日 + 今週 + 今月 + 今年 + 全期間 + リスニング + 再生 + リスニングタイムライン + リスニング時間 + 選択した範囲でのリスニングの合計。 + 再生回数 + セグメントごとに完了したセッション数。 + 平均セッション + 各セグメントの平均リスニング時間。 + 4 時間ごとに分割して日々のリズムを確認できます。 + 日別バーで週ごとの習慣を比較しやすくします。 + 週別バーで月のトレンドを確認できます。 + 月別バーで年間の季節性を確認できます。 + 年別バーで全履歴を要約します。 + まだリスニングデータがありません + 再生を始めてリスニングタイムラインを構築しましょう + 日々のリズム + 週のリズム + 月のリズム + 年間一覧 + 全期間の推移 + 4 時間ごとのセグメントでグループ化 + 曜日でグループ化 + 月の週でグループ化 + 月でグループ化 + 年でグループ化 + ピークセグメント + %1$d 回 + + トップカテゴリ + ジャンル、アーティスト、アルバム、曲ごとのリスニングを比較します。 + ジャンル + アーティスト + アルバム + + ジャンル別リスニング + アーティスト別リスニング + アルバム別リスニング + 曲別リスニング + %1$d 回 • %2$d アーティスト + %1$d 回 • %2$d トラック + まだカテゴリデータがありません + 再生を始めてリスニングのハイライトを確認しましょう + リスニング習慣 + まだ習慣データがありません + あなたのことをより知ったらリスニング習慣を表示します。 + 総セッション数 + 平均セッション + 最長セッション + セッション/日 + 最もアクティブな日 + まだ再生履歴がありません + ピークタイムラインスロット + トップアーティスト + トップアーティストがいません + 聴き続けるとお気に入りのアーティストがここに表示されます。 + \? + %1$d. %2$s + トップアルバム + トップアルバムがありません + よく聴くアルバムがここに表示されます。 + %1$d. %2$s + トラック集中度 + トップトラック全体でリスニング時間がどのように分散しているか。 + まだ集中度データがありません + より多くのトラックを再生してリスニングの集中度を確認しましょう。 + トップ 1 + トップ 2-3 + その他 + %1$d%% + リスニング集中度 + トップ 3 トラックがリスニング時間の %1$d%% を占めています。 + 平均再生回数/トラック + ユニークトラック + トップ 3 シェア + この期間のトラック + 選択した期間で最も再生されたトラック。 + トップトラックがありません + お気に入りを聴き続けるとここでハイライトされます。 + トラックを折りたたむ + すべてのトラックを表示 + + + %1$d 時間 %2$02d 分 + %1$d 分 + %1$d 時間 %2$02d 分 + %1$d 時間 + %1$d 分 + %1$d 秒 + %1$d 時間 %2$02d 分 + %1$d 時間 + %1$d 分 + %1$d 秒 + なし + たった今 + 1 日前 + %1$d 日前 + 1 時間前 + %1$d 時間前 + 1 分前 + %1$d 分前 + %1$d 曲 + %1$d 曲 + 第 %1$d 週 + diff --git a/app/src/main/res/values-ja/strings_library.xml b/app/src/main/res/values-ja/strings_library.xml new file mode 100644 index 0000000000..4c7c2d6ca2 --- /dev/null +++ b/app/src/main/res/values-ja/strings_library.xml @@ -0,0 +1,558 @@ + + + + ライブラリ + ライブラリタブ + 任意のタブへ直接ジャンプするか、順序を変更できます。 + タブを並び替え + + + + アルバム + アーティスト + プレイリスト + フォルダ + お気に入り + + + プレイリストを作成しました + 先に AI プロバイダーの API キーを設定してください + 先に Gemini API キーを設定してください + キューに追加しました + 次に再生 + + + Watch への転送 + 設定 + 編集 + タブを並び替え + メニューを展開 + + + 選択できるアルバムは最大 %1$d 枚です + フォルダ + フォルダ + + + 並び替え + 表示 + プレイリスト表示 + グリッド + リスト + 内部ストレージ + SD カード + SD カードは現在利用できません。 + クラウド + Telegram クラウドチャンネル + トピック表示 + チャンネル + トピック + 両方 + クラウド + クラウドのみ + + + AI でメタデータを生成中… + + + 曲の読み込みエラー + アルバムの読み込みエラー + アーティストの読み込みエラー + 再試行 + + + ライブラリに曲が見つかりませんでした。 + 端末に音楽がある場合は、設定からライブラリを再スキャンしてみてください。 + 曲が見つかりません + + + 新規 + 新しいプレイリストを作成 + M3U プレイリストをインポート + 現在の曲を探す + すべての曲 + クラウド + ローカル + 並び替えオプション + + + すべて + 選択解除 + その他のオプション + + + 音楽ファイルをスキャン中… + ファイルを処理中… + %2$d 件中 %1$d 件 + ライブラリを同期中… + 同期完了 + 待機中… + ライブラリを同期中… + アルバムアートキャッシュをクリア中… + クラウドソースを同期中… + 歌詞をスキャン中… + + + 曲がまだありません + 音楽を端末に追加するか、クラウドソースを同期して再生を始めましょう。 + ローカルの曲が見つかりません + 別のソースフィルターを試すか、端末のライブラリを再スキャンしてください。 + クラウドの曲が見つかりません + Telegram や NetEase の曲を同期するか、ローカルソースに切り替えてください。 + アルバムがありません + ライブラリにトラックがグループ化されるとアルバムが表示されます。 + ローカルアルバムが見つかりません + ローカルアルバムを作成するにはローカルの曲が必要です。 + クラウドアルバムが見つかりません + アルバムデータを持つクラウドの曲は同期後にここに表示されます。 + アーティストがいません + いずれかのソースから曲がインデックスされるとアーティストが表示されます。 + ローカルアーティストが見つかりません + ローカルの曲にアーティストのメタデータがありません。 + クラウドアーティストが見つかりません + リモートの曲が同期されるとクラウドアーティストが表示されます。 + お気に入りの曲がまだありません + 再生中にハートアイコンをタップして曲を保存しましょう。 + お気に入りのローカル曲がありません + ソースフィルターを切り替えるか、端末の曲をお気に入りに追加してください。 + お気に入りのクラウド曲がありません + Telegram や NetEase のトラックをお気に入りに追加するとここに表示されます。 + フォルダが見つかりません + 音楽が入った内部ストレージのフォルダがここに表示されます。 + プレイリストがまだありません + 最初のプレイリストを作成してライブラリを整理しましょう。 + + + 曲のメタデータを編集 + 再生 + 曲を再生 + すべて再生 + すべて再生 + お気に入りに追加 + すべてお気に入りに追加 + お気に入りから削除 + すべてお気に入りから削除 + 曲ファイルを共有するアプリを選択 + 曲ファイルを共有 + すべてを ZIP で共有 + 曲を共有できませんでした: %1$s + キューに追加 + キューに追加 + 次に再生 + キューで次に再生 + プレイリストに追加 + 削除 + すべて削除 + Watch を確認中 + 転送中 %1$d%% + Watch に転送中 + 転送中 + Watch に送る + Watch が利用できません + 曲を Watch に送る + Watch が利用できません + サウンドとして設定 + サウンドとして設定 + この曲をシステムサウンドとして使う方法を選択 + この曲を使う場所 + PixelPlayer がこのサウンドをインストールする場所を選択してください。 + 着信音 + 電話の着信 + 通知音 + メッセージとアプリの通知 + アラーム音 + 時計のアラーム + サウンドの変更を確認 + 「%1$s」を %2$s に設定しますか? + サウンドを設定 + 「%1$s」を %2$s に設定しました + 着信音 + 通知音 + アラーム音 + 「システム設定の変更」を有効にしてから PixelPlayer に戻ると自動で完了します。 + 「システム設定の変更」が有効になっていません。 + 「%1$s」を着信音に設定しました + 着信音にはローカルの曲のみ使用できます。 + この音声ファイルを着信音用に準備できませんでした。 + 着信音を設定できませんでした: %1$s + オプション + オプション + 情報 + 情報 + 再生時間 + ジャンル + アルバム + アーティスト + 曲の情報 + プロバイダー + ファイル + %1$d 曲 + 選択中 + %1$d プレイリスト + %1$d アルバム + 選択中 + 上限: %1$d アルバム + キューへの追加と再生は選択順序に従います。 + %1$d ジャンル + 選択中 + 選択したジャンル内のすべての曲に対して一括操作を実行します。 + + + デフォルト順 + タイトル(A〜Z) + タイトル(Z〜A) + アーティスト + アーティスト(Z〜A) + アルバム + アルバム(Z〜A) + 追加日 + 追加日(古い順) + 再生時間 + 再生時間(短い順) + リリース年 + リリース年(古い順) + 曲数が少ない順 + 曲数が多い順 + 名前(A〜Z) + 名前(Z〜A) + 曲数(多い順) + 曲数(少ない順) + 作成日 + 作成日(古い順) + お気に入り追加日 + お気に入り追加日(古い順) + サブフォルダが少ない順 + サブフォルダが多い順 + + + タイトル + アーティスト + アルバム + 追加日 + 再生時間 + リリース年 + 曲数 + 名前 + 曲数 + 作成日 + お気に入り追加日 + サブフォルダ数 + + + ソース + 順序 + 降順 + 昇順 + 元の順序 + タップして昇順に切り替え + タップして降順に切り替え + この並び替えは元の順序を維持します + スイッチがオン + + + ライブラリタブを並び替え + 順序をリセット + タブの順序をデフォルトに戻しますか? + タブを並び替え中… + ドラッグハンドル + + + アーティストを選択 + 1 アーティスト + %1$d アーティスト + メインアーティスト + アーティストページ + + + 転送をキャンセル + %1$s / %2$s + スマートフォンから Watch への音楽転送の進捗をリアルタイムで表示します + Watch への転送 + Watch に送信中 + キャンセル済み + 転送をキャンセルしました + 転送が完了しました + 完了 + 失敗 + 転送に失敗しました + 複数の転送が進行中 + %1$s • %2$s + 準備中 + Watch への転送を準備中 + 転送を準備中… + Watch に %1$d 曲を送信中 + Watch に送信中 + 転送を開始中… + 開始中 + 転送中 + %1$d 件の転送 + + + 曲を編集 + 情報を表示 + 曲のメタデータを編集中 + 曲のメタデータを編集すると、ライブラリでの表示や整理に影響することがあります。変更は永続的で、元に戻せない場合があります。 + 了解 + 情報 + カバーアート + 正方形の画像を選択して調整し、アプリ全体でカバーアートが美しく表示されるようにしましょう。 + カバーアートを変更 + カバーアートを削除 + タイトル + アーティスト + アルバム + アルバムアーティスト + ジャンル + 作曲者 + トラック番号 + ディスク番号 + ReplayGain トラック(dB) + ReplayGain アルバム(dB) + -6.50 + -8.20 + 新しいカバーアートのプレビュー + 現在の曲のカバーアート + カバーアートを調整 + ピンチとドラッグで最適なフレーミングを見つけてください。 + カバーアートを適用 + 選択した画像を読み込めませんでした + lrclib.net で歌詞を検索 + + + %d 曲を編集 + 変更したフィールドのみ更新されます。空白のフィールドは既存の値が保持されます。 + (複数の値) + (任意 — スキップする場合は空白のまま) + %d 曲を更新しました + %2$d 曲中 %1$d 曲を更新しました。一部のファイルは編集できませんでした。 + 曲の更新に失敗しました + カバーアートの一括変更 + 選択した %d 曲すべてのカバーアートが置き換えられます + すべてにカバーアートを設定 + すべてのカバーアートを削除 + (複数の異なるカバー) + + + プレイリストを閉じました + + + プレイリストを作成 + 作成方法を選択してください。 + 手動 + アートワーク・アイコン・形状をデザインし、曲を自分で選びます。 + AI で作成 + 高度なコントロールでキュレーションされたプレイリストを生成します。 + 設定で Gemini API キーを設定する必要があります。 + API キーを設定 + + + AI プレイリストラボ + リセット + 生成中… + 生成 + 意図 + プレイリスト名(任意) + このプレイリストの雰囲気は? + 例:夕暮れのドライブにウォームなシンセ + 方向性 + ムード + アクティビティ + 年代 + キュレーション + エネルギー + 曲の強度とテンポを調整します。1 = 穏やか/スロー、5 = ハイエネルギー/ファスト。 + ディスカバリー + 選曲の馴染み度を調整します。1 = 最もよく聴くお気に入り、5 = あまり聴いていないレアな曲。 + 最小曲数 + 最大曲数 + フィルター + 優先するジャンル(任意) + 例:シンセウェーブ、インディーポップ + 避けるジャンル(任意) + 例:メタル、ハードトラップ + 優先言語(任意) + 例:日本語、英語、インストゥルメンタル + お気に入りを優先 + 不適切な歌詞を除外 + プロンプトのプレビュー + 好みを追加すると最終プロンプトがここに表示されます。 + 精密なキュレーション + ムード・アクティビティ・制約・深さを定義します。 + AI はローカルライブラリの曲のみを使用します。 + AI への指示を少なくとも 1 つ追加してください。 + 有効な曲数の範囲を設定してください。 + %1$d/5 + カスタム… + カスタム値を入力 + カスタム値を入力してください + + + すべての年代 + コアリクエスト: %1$s。 + ムード目標: %1$s。 + アクティビティ: %1$s。 + 年代: %1$s。 + 優先ジャンル: %1$s。 + 避けるジャンル: %1$s。 + 優先言語: %1$s。 + エネルギーレベル目標: %1$d/5。 + ディスカバリー目標: %1$d/5(1 = 馴染みあり、5 = レアな掘り出し物)。 + 可能な限りお気に入りに近い曲を優先する。 + 代替曲がある場合は不適切な歌詞を避ける。 + スムーズなトランジションを維持し、同じアーティストが連続しないようにする。 + + チル + エネルギッシュ + ハッピー + ダーク + ロマンティック + メランコリック + + + ワークアウト + 集中 + ロードトリップ + パーティー + 勉強 + 深夜 + + + @string/playlist_creation_ai_era_any + 70年代 + 80年代 + 90年代 + 2000年代 + 2010年代 + 2020年代 + + + + プレイリストがまだ作成されていません。 + 「新しいプレイリスト」ボタンをタップして始めましょう。 + 新しいプレイリスト + プレイリスト名 + マイプレイリスト + + + %1$d 曲を追加先… + プレイリストを選択 + プレイリストを検索… + プレイリストに曲を追加しました + プレイリストを作成して曲を追加しました + 内部ストレージ + + + 曲を追加 + 選択した曲を追加 + 追加 + 曲を検索またはフィルター… + お気に入り + 曲の読み込みに失敗しました + さらに読み込む + + + プレイリストを結合 + 結合後のプレイリスト名を入力してください: + 結合プレイリスト + 選択した %1$d 件のプレイリストを 1 つに結合します。 + + + 再生できる有効な曲が見つかりませんでした + 現在のリストに曲が見つかりません + 曲を見つけられませんでした + ライブラリに曲が見つかりません + %1$s の再生が終了しました(トラック終了)。 + トラック + シャッフルする曲がありません。 + 選択したアルバム + 選択したアルバムに再生可能な曲が見つかりませんでした + 選択したジャンルに再生可能な曲が見つかりませんでした + 最初の %1$d アルバムのみキューに追加しました + %1$d アルバムをキューに追加しました(%2$d 曲) + 選択したアルバムをキューに追加できませんでした + すべての曲がすでにお気に入りにあります + お気に入りに曲がありませんでした + ZIP ファイルを作成中… + 共有に失敗しました: %1$s + + %d 曲をキューに追加しました + + + %d 曲が次に再生されます + + + %d 曲をお気に入りに追加しました + + + %d 曲をお気に入りから削除しました + + + + 共有するプレイリストがありません + プレイリストを共有 + 共有に失敗しました: %1$s + エクスポートするプレイリストがありません + エクスポートに失敗しました: %1$s + Music/PixelPlayer Exports + 設定で Gemini API キーを設定してください。 + プレイリストを復元しました + + %d 件のプレイリストを共有中 + + + %2$s に %1$d 件のプレイリストをエクスポートしました + + + + 無効なアルバム ID + アルバム ID が見つかりません + アルバムデータの読み込みエラー: %s + アルバムが見つかりません + + + 無効なアーティスト ID + アーティスト ID が見つかりません + アーティストデータの読み込みエラー: %s + アーティストが見つかりませんでした + + + 再生中の曲は削除できません + %1$d 件のファイルを削除しました(%2$d 件スキップ — 再生中) + %2$d 件中 %1$d 件のファイルを削除しました + ファイルの削除に失敗しました + ファイルを削除しました + ファイルを削除できないか、見つかりません + 削除をキャンセルしました + 曲を削除しますか? + %2$s の「%1$s」\n\nこの曲は端末から完全に削除され、元に戻せません。 + これらの曲は端末から完全に削除され、元に戻せません。 + + %d 件のファイルを削除しました + + + %d 曲を削除しますか? + + + + メタデータを更新しました + %1$d 曲を更新中… + %1$d 曲を正常に更新しました! + %1$d 曲を更新しました。失敗: %2$d 曲 + 歌詞を保存しました + 歌詞の保存に失敗しました + 保存できる歌詞がありません + 権限が拒否されました — ファイルを編集できません + 権限が拒否されました — 歌詞を保存できません + 権限が拒否されました — このファイルを編集できません + + + 設定で選択した AI プロバイダーの有効な API キーを設定してください。 + AI エラー: %s + 選択した AI プロバイダーはアカウントのクレジットまたはクォータが不足しているためリクエストを拒否しました。 + 選択した AI モデルは利用できなくなりました。PixelPlayer がサポート対象のモデルへ自動的に切り替えを試みました。 + AI がプロンプトに合う曲を見つけられませんでした。 + デイリーミックスのアイデアを書いてください + AI でデイリーミックスを更新しました + 更新できませんでした: %s + AI がこのミックスに合う曲を見つけられませんでした + diff --git a/app/src/main/res/values-ja/strings_player.xml b/app/src/main/res/values-ja/strings_player.xml new file mode 100644 index 0000000000..eae601ef39 --- /dev/null +++ b/app/src/main/res/values-ja/strings_player.xml @@ -0,0 +1,195 @@ + + + + プレイヤーを閉じる + 再生中 + クラウドストリーム + キャスト + Bluetooth + 本体再生 + 接続中… + キューを開く + + + 接続の準備 + キャスト・Bluetooth オーディオ・スピーカーを同期するために、PixelPlayer が近くのデバイスと現在の Wi‑Fi を確認できるよう許可してください。 + 近くのデバイス + 接続済み Bluetooth オーディオ機器の読み取りと制御に必要です。 + Wi‑Fi 用の位置情報 + Android では、互換性のあるキャストデバイスを検出するために Wi‑Fi ネットワーク(SSID)の共有に位置情報が必要です。 + アクセスを許可 + これらの権限はデバイスの相互接続(キャスト・近くのスピーカーの制御・オーディオ同期)にのみ使用します。 + デバイスを接続 + 近くをスキャン中 + キャストセッション + 接続中 + 接続済み + このスマートフォン + Bluetooth オーディオ + 本体再生 + 再生中 + 一時停止中 + デバイスの音量 + スマートフォンの音量 + %1$d/%2$d + バッテリー残量 + 音量レベル + 切断 + 接続性 + Wi-Fi または Bluetooth をオンにしてください + 接続を更新 + Wi-Fi + オフ + オン + 接続済み + Bluetooth + オフ + オン + 接続済み + 近くのデバイス + デバイスを更新 + 接続済み + 接続中 + 接続可能 + 利用可能 + 接続中... + デバイスを検索中… + テレビやスピーカーの電源が入っており、同じ Wi‑Fi ネットワークに接続されていることを確認してください。 + コントロール + デバイス + + + キャストメディアサーバー + デバイスにキャスト中 + キャストデバイスにメディアを配信中 + %1$s: %2$s + このオーディオフォーマットはキャスト中にシークするとセッションがクラッシュする可能性があるため、一時的に利用できません。 + + + スリープタイマー + タイマー + %1$d 分 + %1$d 分後にタイマーをセットしました。 + 1 回 + + %d 回 + + 再生回数: %1$s + 現在のトラックの終わり + トラックの終わりで再生を停止します。 + スイッチをオン + カスタム時間 + タイマーをキャンセル + トラックの終わり + タイマーをキャンセルしました。 + 再生中の曲がないため、トラック終了タイマーを有効にできません。 + 曲が %1$s から %2$s に変わったため、トラック終了タイマーを無効にしました。 + 前のトラック + 現在のトラック + カスタム時間を設定 + + + 次の曲 + キューはまだ空です。 + + %d 曲待機中 + + キュー + キューは空です。 + 曲を並び替え + シャッフルを切り替え + リピートを切り替え + スリープタイマー + その他の操作 + 現在の曲を探す + キューをクリア + キューをクリア + 現在再生中の曲以外をすべてキューから削除しますか? + プレイリストとして保存 + %1$s のキュー + 現在のキュー + 曲を削除 + 削除しました + プレイリストとして保存 + すべて選択解除 + プレイリスト名 + 含める曲を検索… + 「%1$s」に一致する曲はありません + + %d 曲を選択中 + + %1$s として保存 + プレイリスト名を入力 + プレイリストから削除 + %1$s のその他のオプション + + + 歌詞 + 歌詞を読み込み中… + 同期あり + テキストのみ + 歌詞オプション + −.5 + −.1 + +.1 + +.5 + 0s + %1$+.1f 秒 + + + 歌詞の検索に失敗しました + リモートからの歌詞取得に失敗しました + 接続がタイムアウトしました。インターネット接続を確認してください。 + ネットワークエラー。インターネット接続を確認してください。 + サーバーエラー(コード %d)。しばらくしてから再試行してください。 + + + 歌詞はすでに利用可能です。オンライン取得をスキップしました。 + 埋め込み歌詞が見つかりました。オンライン取得をスキップしました。 + ローカル(.lrc)歌詞が見つかりました。オンライン取得をスキップしました。 + + + 歌詞を保存 + AI で翻訳 + この歌詞にはすでに翻訳があります + この歌詞はすでにこの言語です + API が設定されていません + 歌詞の翻訳が完了しました! + 歌詞を翻訳中... + インポートした歌詞をリセット + 歌詞をリセットしますか? + この曲の歌詞をリセットしてもよろしいですか? + 表示 + 配置 + 左揃え + 中央揃え + 右揃え + コントロール + 同期を調整 + 同期コントロールを非表示 + ローマ字表記を表示 + 翻訳を表示 + 没入モードを一時解除 + 画面をオンに保つ + + + 歌詞を保存 + 保存するバージョンを選択してください: + 同期あり(タイムスタンプ付き) + テキストのみ + + + 歌詞をオンラインで検索しますか? + 歌詞の候補を表示 + 最初の候補を自動適用せず、常に選択画面を開く + 歌詞を検索中… + 歌詞が見つかりませんでした + 歌詞を自動で見つけられませんでした。タイトルやアーティスト名を編集して手動で検索できます。 + 曲名 + アーティスト(任意) + %d 件見つかりました + 同期あり + %1$s • %2$s + 歌詞提供元: + https://lrclib.net/ + diff --git a/app/src/main/res/values-ja/strings_screens.xml b/app/src/main/res/values-ja/strings_screens.xml new file mode 100644 index 0000000000..be88a28c03 --- /dev/null +++ b/app/src/main/res/values-ja/strings_screens.xml @@ -0,0 +1,244 @@ + + + + エラー: ジャンル ID がありません + + + 始めましょう! + ステップ %1$d / %2$d + 先に必要な権限を許可してください。 + 必要な権限をすべて許可してください。 + ようこそ + β + ベータ + セットアップを完了しましょう。 + メディアの権限 + 音楽ライブラリを構築するために、PixelPlayer がオーディオファイルへアクセスする必要があります。 + 権限が許可されました + メディア権限を許可 + 通知 + ロック画面や通知シェードから音楽を操作するために通知を有効にします。 + 通知を有効化 + バックアップはありますか? + PixelPlayer のバックアップがある場合は今すぐ復元することでこのデバイスのセットアップの大部分をスキップできます。 + バックアップをインポート + バックアップを確認中 + バックアップパッケージを確認中… + バックアップを復元中 + スキップ / あとで + バックアップを復元 + セットアップを完了する前にインポートする内容を確認してください。 + %2$d モジュール中 %1$d を選択中 + %1$s に作成 + %1$s からのバックアップ + バージョン不明 + 選択を復元 + 復元中 + 除外フォルダ + デフォルトではすべてのフォルダがスキャンされます。ライブラリ構築時に無視する場所を選択してください。 + 無視するフォルダを選択 + 先にストレージの権限を許可してください + アプリのテーマ + ライブラリの探索を始める前に好みの外観を選んでください。 + ダーク + PixelPlayer のデフォルトの Material 3 ダーク外観。 + ライト + アプリ全体のより明るい Material 3 外観。 + システムに合わせる + スマートフォンの現在の外観設定に合わせます。 + おすすめ + 後から 設定 > 外観 > アプリのテーマ で変更できます。 + ライブラリレイアウト + ライブラリのナビゲーション方法を選択してください。 + + コンパクトモード + 最小化されたピルナビゲーションを使用 + 標準のタブ行を使用 + + アルバム + アーティスト + 後から 設定 > 外観 > ライブラリナビゲーション で変更できます。 + アプリナビゲーション + ボトムナビゲーションバーのスタイルを選択してください。 + デフォルトスタイル + 角が丸いフローティングピル + 標準のフル幅バー + コーナー半径をカスタマイズ + 後から 設定 > 外観 > ナビバースタイル で変更できます。 + アラームとリマインダー + 任意ですが、スリープタイマーを使用して PixelPlayer を正確な時刻に停止させたい場合はおすすめです。 + 権限を許可 + バッテリー最適化 + 一部の Android 端末はバックグラウンドアプリを積極的に終了させます。予期しない再生の中断を防ぐために PixelPlayer のバッテリー最適化を無効にしてください。 + 最適化を無効化 + 準備完了! + 音楽を楽しむ準備ができました。 + + + 検索… + 検索 + 検索をクリア + 最近の検索 + すべてクリア + 履歴 + 検索履歴アイテムを削除 + 結果なし + 「%1$s」の検索結果はありません + 見つかりませんでした + 別の検索語またはフィルターを試してください。 + 結果が見つかりませんでした。 + ジャンルで探す + 利用可能なジャンルがありません。 + + + %1$s を再生 + %1$s を折りたたむ + %1$s を展開 + アーティスト画像を編集 + 写真を変更 + デフォルトに戻す + アーティストをシャッフル再生 + + + ディスク %d + %1$s のカバー + %1$s · %2$s + + + プレイリストが見つかりません。 + このプレイリストは空です。 + 「曲を追加」をタップして始めましょう。 + このフォルダに曲はありません。 + 曲を並び替え + その他のオプション + プレイリストのオプション + プレイリストを編集 + プレイリストを削除 + プレイリストを削除しますか? + このプレイリストを本当に削除しますか? + デフォルトトランジションを設定 + プレイリストをエクスポート + %1$s • %2$s + 再生する + 追加 + 曲を追加 + 削除 + 曲を削除 + 並び替え + 曲を並び替え + + + グローバルトランジション + プレイリストルール + 上書きされない限り、すべての再生ソースにこの設定が適用されます。 + この特定のプレイリストのデフォルト動作を設定します。 + アクティブ状態 + グローバルデフォルト + プレイリストデフォルト + グローバルに従う + カスタム上書き + カスタム上書き + 有効にするとこのプレイリストに特定のルールを設定できます。 + グローバルデフォルトを使用 + 変更を保存しました + トランジションスタイル + トラックのブレンド方法 + なし + クロスフェード + トランジションの長さ + %1$d 秒のオーバーラップ + トランジションをリセット + 現在の曲 + 次の曲 + トラックは %1$d 秒間オーバーラップします + 音量カーブ + オーディオのスロープを微調整 + フェードアウト + フェードイン + + + 新しいスマートプレイリスト + 新しいプレイリスト + 曲を追加 + 戻るまたはキャンセル + 次へ + 作成 + プレイリストを編集 + 自動生成コラージュ + 写真を追加 + 画像を選択 + 変更 + 削除 + プレイリスト名 + マイ素敵なミックス + カバーを編集 + カバーアートを調整 + ピンチとドラッグで最適なフレーミングを見つけてください + 手動 + スマート + AI で生成 + スマートルール + デフォルト + 画像 + アイコン + 背景色 + アイコンシンボル + 形状スタイル + 形状パラメーター + コーナー半径 + 滑らかさ + 辺の数 + カーブ + 回転 + スケール + よく再生する曲 + 最も再生されたトラック。 + 最近再生した曲 + 最近聴いた曲。 + 忘れられたお気に入り + しばらく再生していないお気に入りのトラック。 + 新着の宝石 + 再生回数が少ない最近追加されたトラック。 + + + ジャンルに曲を素早く追加 + 並び替えと再生 + シャッフル + 並び替え基準 + アーティスト + アルバム + タイトル + 一般アーティスト + %1$s シャッフル + + + 曲を選択 + ジャンルを選択 + 曲を検索 + 新しいジャンル + カスタムを追加 + カスタムジャンルを追加 + ジャンル名 + アイコンを選択 + ジャンル: %1$s + ジャンルを選択 + 素早く追加 + + + DJ スペース + 読み込み中… + デッキ %1$d + 曲を読み込む + 曲が読み込まれていません + + ステム分離はまだ利用できません。 + 音量 + 速度 + クロスフェーダー + デッキ 1 + デッキ 2 + 曲を選択 + 再生/一時停止 + 曲のカバー + x%1$.2f + diff --git a/app/src/main/res/values-ja/strings_settings.xml b/app/src/main/res/values-ja/strings_settings.xml new file mode 100644 index 0000000000..6678113cc5 --- /dev/null +++ b/app/src/main/res/values-ja/strings_settings.xml @@ -0,0 +1,654 @@ + + + + 音楽管理 + フォルダ管理、ライブラリ更新、解析オプション + 外観 + テーマ、レイアウト、ビジュアルスタイル + 再生 + オーディオ動作、クロスフェード、バックグラウンド再生 + 歌詞 + 優先ソース、没入型歌詞、インポート管理 + 動作 + ジェスチャー、触覚フィードバック、ナビゲーション動作 + AI 連携(β) + AI プロバイダー、API キー、モデル設定 + バックアップ & 復元 + 個人データのエクスポートと復元 + 開発者オプション + 試験的機能とデバッグ + イコライザー + 音域とプリセットの調整 + デバイス情報 + オーディオ仕様、コーデック、デコーダー情報 + アカウント + Telegram、Google Drive、NetEase などのサービスを管理 + このアプリについて + アプリ情報、バージョン、クレジット + + + オン + オフ + 有効 + 無効 + 開く + すべて選択 + 選択を解除 + 通知を閉じる + + + ライブラリ構造 + 除外ディレクトリ + ここに追加したフォルダはライブラリスキャン時にスキップされます。 + アーティスト + 複数アーティストの解析と整理オプション。 + フィルタリング + 最低曲の長さ + アルバムの最低トラック数 + アルバムアートキャッシュ上限 + 同期とスキャン + ライブラリを更新 + 新しいファイルや変更されたファイルをライブラリ全体からスキャンします。 + フルリスキャン + フルリスキャン実行中 + フルリスキャンを開始しました… + ライブラリ同期が完了しました + データベースを再構築 + データベースを再構築しますか? + 音楽ライブラリを最初から完全に再構築します。インポートした歌詞、お気に入り、カスタムメタデータはすべて失われます。この操作は元に戻せません。 + 再構築 + データベースを再構築中 + データベースを再構築中… + .lrc ファイルを自動スキャン + ライブラリ同期中に、同じフォルダ内の .lrc ファイルを自動でスキャンして割り当てます。 + 歌詞管理 + 歌詞ソースの優先順位 + 歌詞を取得する際に最初に試みるソースを選択します。 + 埋め込みを優先 + オンラインを優先 + ローカル(.lrc)を優先 + インポートした歌詞をリセット + データベースからインポートした歌詞をすべて削除します。 + インポートした歌詞をリセットしますか? + この操作は元に戻せません。 + + + 更新 + デフォルトではすべて許可されています。フォルダをタップするとスキャンから除外されます。 + サブフォルダがありません + 上へ移動 + ルートへ移動 + + + リスキャンが必要です + アーティスト設定が変更されました。ライブラリをリスキャンして適用してください。 + リスキャン + スキャン中… + 複数アーティストの解析 + 文字区切り + 現在: %1$s + 単語区切り + なし + 現在: %1$s + 設定 + タイトルからアーティストを抽出 + 曲タイトルの feat., ft., with を検出 + ライブラリ整理 + アルバムアーティストでグループ化 + コラボアルバムをメインアーティストの下に表示 + 複数アーティスト解析について + + PixelPlayer は文字区切り(/、;、&)と単語区切り(feat.、ft.、vs.、x)を使ってアーティストタグを分割します。単語区切りは大文字小文字を区別しません。 + 「タイトルからアーティストを抽出」は曲タイトルの (feat. アーティスト名) のようなパターンを検出します。 + バックスラッシュ(\)で文字区切りをエスケープできます。 + + + + + \"Artist1/Artist2\" + Artist1, Artist2 + \"Drake feat. Rihanna\" + Drake, Rihanna + \"Marshmello x Bastille\" + Marshmello, Bastille + \"Song (ft. B)\" by A + A, B + \"AC\\DC\" + AC/DC(エスケープ済み) + + + 区切り文字 + 現在の区切り文字 + 区切り文字をタップして削除します。少なくとも 1 つ必要です。 + 新しい区切り文字を追加 + 例: / または ; + 区切り文字を追加 + デフォルトの区切り文字 + 区切り文字をリセットしますか? + カスタム区切り文字をすべてクリアしてデフォルトに戻します。この操作は元に戻せません。 + 区切り文字をデフォルトにリセットしました + 少なくとも 1 つの区切り文字が必要です + 区切り文字を追加しました + すでに存在するか無効な区切り文字です + スペース + + + 単語区切り + 現在の単語区切り + スペースで囲まれているときにアーティスト名を分割するキーワードです。大文字小文字を区別しません。タップして削除。 + 単語区切りが設定されていません + 新しい単語区切りを追加 + 例: feat. または ft. + 単語区切りを追加 + 単語区切りの仕組み + 単語区切りはスペースで囲まれている場合に大文字小文字を区別せずマッチします。\n\n1文字の区切り(例: \"x\")は誤マッチを防ぐために両側にスペースが必要です。\n\n例:\n \"Drake feat. Rihanna\" -> Drake, Rihanna\n \"Marshmello x Bastille\" -> Marshmello, Bastille\n \"A vs. B\" -> A, B + 単語区切りをリセットしますか? + カスタム単語区切りをすべてクリアしてデフォルトキーワードに戻します。この操作は元に戻せません。 + 単語区切りを追加しました + すでに存在するか無効です + 単語区切りをデフォルトにリセットしました + + + 同期を準備中 + MediaStore を読み込み中 + トラックを処理中 + データベースに保存中 + 歌詞ファイルをスキャン中 + アルバムアートキャッシュをクリア中 + クラウドソースを同期中 + 同期を完了中 + %1$s • %2$d%% (%3$d/%4$d) + %1$s… + + + グローバルテーマ + アプリの言語 + アプリ全体で使用する言語を選択します。 + システムのデフォルト + English + Español + Deutsch + Français + Русский + 简体中文 + Bahasa Indonesia + Italiano + 한국어 + Norsk (Bokmål) + Türkçe + 日本語 + 繁體中文 + Tiếng Việt + アプリのテーマ + ライト、ダーク、またはシステムに合わせるを選択します。 + ライトテーマ + ダークテーマ + システムに合わせる + スムーズコーナーを使用 + 複雑な形状のコーナーを使用して見た目を向上させますが、ローエンド端末ではパフォーマンスに影響する場合があります。 + ブラー効果を無効化 + アプリ全体のブラー効果をオフにしてバッテリーとリソースを節約します。 + スクロールバーを表示 + 音楽リストにスクロールバーを表示してすばやくスクロールできます。 + 再生中 + プレイヤーテーマ + フローティングプレイヤーの外観を選択します。 + アルバムアート + システムダイナミック + プレイヤーのファイル情報を表示 + プレイヤーの進行バーにコーデック、ビットレート、サンプルレートを表示します。 + アルバムアートパレットスタイル + 現在: %1$s。ライブプレビューを開いてスタイルを選択してください。 + カルーセルスタイル + アルバムカルーセルの外観を選択します。 + のぞき込みなし + のぞき込み 1 枚 + ホームコラージュ + コラージュパターン + 「あなたのミックス」コラージュの形状を選択します。 + パターンを自動ローテーション + ホームを訪れるたびにコラージュパターンを切り替えます。 + ナビゲーションバー + ナビバースタイル + ナビゲーションバーの外観を選択します。 + デフォルト + フル幅 + コンパクトモード + アイコンのみ表示してナビバーの高さを縮小します。 + ナビバーのコーナー半径 + ナビゲーションバーのコーナー半径を調整します。 + 歌詞画面 + 没入型歌詞 + コントロールを自動非表示にしてテキストを拡大します。 + 自動非表示の遅延 + コントロールが非表示になるまでの時間。 + 3 秒 + 4 秒 + 5 秒 + 6 秒 + アプリナビゲーション + デフォルトタブ + 起動時のデフォルトタブを選択します。 + ホーム + 検索 + ライブラリ + ライブラリナビゲーション + ライブラリタブ間の移動方法を選択します。 + タブ行(デフォルト) + コンパクトピル & グリッド + + + カラー + パレットスタイル + プレイヤー UI のアルバムカラーを選択します。 + トーナルスポット + バランスが取れた落ち着いた雰囲気。 + ビビッド + 高彩度のアクセント。 + エクスプレッシブ + 大胆な色相シフトとコントラスト。 + フルーツサラダ + 楽しい回転アクセント。 + カラーの精度 + 0 は現在の調整を維持します。高い値ほどアルバムアートの主要色に近くなります。 + 現在 + より正確 + 0 • 現在 + %1$d • 穏やか + %1$d • バランス + %1$d • 正確 + + + コーナー半径を調整 + ナビバーの形状のコーナーをデバイスの物理コーナーに合わせてシームレスな外観にします。 + コーナー半径 + %1$d dp + + + バックグラウンド再生 + 閉じても再生を続ける + オフにすると、アプリを履歴から削除したときに再生が停止します。 + バッテリー最適化 + バッテリー最適化を無効にして再生の中断を防ぎます。 + バッテリー最適化はすでに無効になっています + バッテリー設定を開けませんでした + 音量 + 音量ゼロで一時停止 + 音量がゼロのときに再生を一時停止します + 音量ノーマライゼーション(ReplayGain) + ReplayGain を有効化 + オーディオファイルの ReplayGain メタデータを使って音量レベルを正規化します。 + ゲインモード + トラック: 曲ごとに正規化。アルバム: アルバム単位で正規化。 + トラック + アルバム + キャスト + キャスト接続/切断時に自動再生 + キャスト接続を切り替えた直後に自動で再生を開始します。 + ヘッドフォン + ヘッドフォン再接続時に再開 + ヘッドフォンを外したために一時停止した場合、再接続すると自動で再開します。 + キューとトランジション + クロスフェード + 曲間のスムーズなトランジションを有効にします。 + クロスフェードの長さ + Hi-Fi モード + 32 ビット float オーディオ出力。端末で再生がカクつく場合は無効にしてください。 + この端末ではサポートされていません(PCM_FLOAT AudioTrack 非対応)。 + シャッフルを保持 + アプリを閉じた後もシャッフル設定を記憶します。 + キュー履歴を表示 + キューに以前再生した曲を表示します。 + + + フォルダ + 戻るジェスチャーでフォルダを操作 + フォルダタブで、システムの戻る操作がライブラリを離れる前にフォルダ階層をさかのぼります。 + プレイヤーのジェスチャー + 背景タップでプレイヤーを閉じる + ぼかした背景をタップするとプレイヤーシートが閉じます。 + 触覚フィードバック + 触覚フィードバック + アプリ全体でバイブレーションフィードバックを有効にします。 + + + AI プロバイダー + プロバイダー + AI プロバイダーを選択してください + セーフトークンモード + ON — 高速 & 低コスト。AI に最小限のデータ(約 1K トークン)を送信します。 + OFF — 深いコンテキスト。より豊かな結果のためにリスニングプロフィール全体(約 8K トークン)を送信します。 + 認証情報 + %1$s API キー + %1$s から取得 + Google AI Studio (aistudio.google.com) + DeepSeek Platform (api.deepseek.com) + Groq Console (console.groq.com) + Mistral AI Platform (console.mistral.ai) + NVIDIA Build (build.nvidia.com) + Moonshot AI Platform (platform.moonshot.cn) + Zhipu AI Open Platform (bigmodel.cn) + OpenAI Platform (platform.openai.com) + モデル選択 + 利用可能なモデルを読み込み中… + モデルの読み込みに失敗しました + AI モデル + モデルを選択してください。 + API キーを入力 + プロンプト動作 + システムプロンプト + AI の動作をカスタマイズします。 + プリセットプロンプト + システムプロンプトを入力… + プロフェッショナルキュレーター + あなたは「Vibe-Engine」という世界トップクラスの音楽キュレーターで、ソニックフローの達人です。シームレスで高品質なリスニング体験を提供することが目標です。和声の相性、論理的な BPM トランジション、馴染みのお気に入りと洗練された発見のバランスを優先してください。 + クリエイティブマーベリック + あなたは「予期しない統一感」を専門とする前衛的な音楽探求者です。非自明なソニックの共通点を見つけることで従来のジャンルの壁を打ち破ることが使命です。レアなディープカット、実験的なテクスチャー、芸術的な新しさを優先しながら、驚きつつも否定できないトランジションロジックを維持してください。 + 厳格な司書 + あなたは精密な音楽データベースアーキテクトです。絶対的なメタデータの精度と厳格なカテゴリ遵守によってロジックを動かします。アルゴリズムによる発見を最小化し、厳格なジャンルの一貫性、エネルギーレベルのマッチング、ユーザーが明確に定義した好みの高精度な取得を最大化してください。 + アトモスフェリックガイド + あなたはアンビエントテクスチャーと低エネルギーフローの達人です。「深い集中」や「静けさ」の状態を促すトラックだけに集中してください。アコースティックな温かさ、ミニマリストのアレンジ、穏やかなトランジションを優先し、高い過渡音や急激なダイナミックの変化を厳しく避けてください。 + ソニックエンスージアスト + あなたはプロダクションの複雑さと演奏に焦点を当てたオーディオファイルアナリストです。高いダイナミックレンジ、複雑なポリリズム、優れたサウンドステージ品質を持つトラックを優先してください。技術的な忠実度とアレンジの細部に注意を払うリスナーを喜ばせるアクティブリスニング作品を選んでください。 + エナジーカタリスト + あなたは高モメンタムのリズムジェネレーターです。強烈なベースライン、パーカッシブな強度、感染力のあるグルーヴを中心哲学とします。高 BPM のクラブ互換性、シンコペーションエネルギー、継続的なリズムの張りを優先して、リスナーの心拍数とモチベーションをピーク状態に保ってください。 + AI 使用レポート + 総消費量 + %1$s トークンを追跡中\nプロンプト: %2$s | 出力: %3$s | 思考: %4$s + ログをクリア + AI アクティビティログ(%1$d 件) + %1$s · %2$s + 表示 + 非表示 + + + バックアップの仕組み + セクションを選んで .pxpl ファイルをエクスポートし、後でインポートして復元します。復元は選択したセクションのみを置き換えます。 + バックアップを作成 + バックアップをエクスポート + セクションが選択されていません。 + すべてのセクションが選択されています。 + %2$d セクション中 %1$d を選択中。 + %1$s .pxpl バックアップファイルを作成します。 + 選択してエクスポート + バックアップを復元 + バックアップをインポート + 選択して復元 + 最近のバックアップを参照または選択します。選択したデータが現在のデータを置き換えます。 + + + バックアップパッケージに含める内容を正確に選択してください。 + .pxpl バックアップファイルを選択して確認します。次のステップで復元するセクションを選択します。 + %2$d セクション中 %1$d を選択中 + %2$d モジュール中 %1$d を選択中 + 最近のバックアップ + 最近のバックアップはありません + 以前にインポートしたバックアップがここに表示されます。 + %1$d エントリー · 現在のデータを置き換えます + .pxpl をエクスポート + 選択を復元 + 転送中… + PixelPlayer_Backup_%1$d.pxpl + バックアップを作成中 + バックアップを復元中 + %1$d%% + %1$s • %2$s + エクスポート中 + インポート中 + 復元中 + 履歴から削除 + 確認中… + ファイルを参照 + ステップ %1$d / %2$d + モジュールを復元 + バックアップの詳細 + 作成日 + アプリバージョン + スキーマ + デバイス + 不明 + · %1$s + %1$d モジュール · v%2$s · スキーマ v%3$d + \? + すべて選択 + 選択をクリア + + + 無効なバックアップ: %1$s + 復元を準備中 + 復元タスクを開始しています。 + バックアップを準備中 + バックアップタスクを開始しています。 + バックアップを正常に復元しました + 一部の未解決の問題がありましたが復元は完了しました。 + 復元を完了できませんでした: %1$s + 復元に失敗しました: %1$s + データを正常にエクスポートしました + エクスポートに失敗しました: %1$s + データを正常に復元しました + 未解決の問題で復元が完了しました。失敗: %1$s + v%1$d + %1$s %2$s + + + 実験的機能 + 試験的 + プレイヤー UI 読み込みの実験とトグル。 + セットアップフローをテスト + テスト用にオンボーディングのセットアップ画面を起動します。 + メンテナンス + デイリーミックスの強制再生成 + デイリーミックスプレイリストをすぐに再作成します。 + デイリーミックスを再生成 + デイリーミックスを再生成しますか? + 現在のミックスを破棄して、最近のリスニング習慣に基づいて新しいミックスを生成します。 + デイリーミックスの再生成を開始しました + 統計の強制再生成 + キャッシュをクリアして再生統計を再計算します。 + 再生成 + 処理中… + 統計を再生成 + 統計を再生成しますか? + 統計キャッシュをクリアして、データベース履歴から強制的に再計算します。 + 統計の再生成を開始しました + アルバムパレットの強制再生成 + すべてのアルバムアートのキャッシュされたパレットバリアントを再構築するか、特定の 1 枚を更新します。 + すべて再生成 + すべてのアルバムパレットを再生成しますか? + キャッシュされたテーマデータをクリアして、%1$d 枚のユニークなアルバムアートのすべてのパレットスタイルを再構築します。 + 再生成中… + アルバムパレットを再生成中… + %1$d 枚のユニークなアルバムアートのキャッシュされたパレットバリアントを再構築中です。大きなライブラリでは時間がかかることがあります。 + %1$d / %2$d 完了 + %1$d 枚のアルバムアートパレットを再生成しました + %2$d 枚中 %1$d 枚のアルバムアートパレットを再生成しました + 曲を選択 + 曲を選択するとキャッシュされたテーマデータをクリアして、アルバムアートからすべてのパレットスタイルを再生成します。 + タイトル、アーティスト、アルバムで検索 + 検索に一致する曲がありません。 + アルバムアートのある曲が見つかりませんでした。 + パレットを再生成中… + %1$s のパレットを再生成しました + %1$s のパレットを再生成できませんでした + 診断 + テストクラッシュを発生させる + クラッシュレポートシステムをテストするためにクラッシュをシミュレートします。 + 開発者オプションからテストクラッシュを発生させました — これはクラッシュレポートシステムをテストするための意図的な操作です + + + 試験的 + プレイヤー UI 読み込みの調整 + アニメーション歌詞(ハイエンド端末向け) + 歌詞にスプリングアニメーションとビジュアル効果を使用します。ローエンド端末ではフレームドロップが発生する場合があります。 + 歌詞のブラー効果 + 非アクティブな歌詞に被写界深度ブラーを適用します。 + ブラー強度 + ブラー効果の強さを調整します。 + %1$.1f倍 + ステップ 1 · 遅延する対象を選択 + すべてを遅延 + シートの背景が完全に展開されるまでプレイヤーのコンテンツ全体を保持します。 + アルバムカルーセル + シートが展開されるまでアルバムアートとカルーセルを遅延します。 + 曲のメタデータ + タイトル、アーティスト、歌詞/キューのアクションを遅延します。 + 進行バー + 展開完了までタイムラインと時刻ラベルを遅延します。 + 再生コントロール + 再生/一時停止、シーク、お気に入りコントロールを遅延します。 + 遅延するコンポーネントがすべてアクティブです。「すべてを遅延」を無効にして各パーツをカスタマイズします。 + ステップ 2 · プレースホルダーの動作を設定 + 遅延項目にプレースホルダーを使用 + コンポーネントが展開を待つ間、軽量なプレースホルダーを描画してレイアウトを安定させます。 + ステップ 3 · プレースホルダーから実コンテンツに切り替えるタイミングを選択 + モードを 1 つ選択してください。閾値モードはスライダーを使用します。ドラッグリリースモードはシートジェスチャーを離すまで待機します。 + トリガーモードを解除するには遅延コンポーネントを少なくとも 1 つ有効にしてください。 + 閾値 + 展開率を使用します。 + ドラッグリリース + ジェスチャーを離した後のみ切り替えます。 + 展開閾値 + 遅延コンポーネントが表示されるまでにシートがどれだけ展開している必要があるか。 + コンテンツは %1$d%% 展開時に表示されます + プレイヤーを閉じるときにも適用 + 折りたたむ際に閉じる閾値を使ってプレースホルダーに戻します。 + 閉じる閾値 + プレースホルダーが再び表示されるまでにどれだけ折りたたまれている必要があるか。 + %1$d%% 折りたたみ後にプレースホルダーが表示されます + ドラッグリリースモードは閾値と閉じる動作をバイパスします。切り替えはシートのドラッグジェスチャーが終了したときのみ発生します。 + プレースホルダーを透明にする + プレースホルダーはレイアウトスペースを保持したまま見えなくなります。 + 画質 + アルバムアートの解像度 + 低(256px)- パフォーマンス重視 + 中(512px)- バランス型 + 高(800px)- 最高品質 + オリジナル - 最大品質 + + + 再生には確認が必要です + 再生の準備ができています + -- + フォーマット + HW デコーダー + ローカル曲 + ローカル音楽ストレージ + 音楽サイズ + %1$d 曲(ローカル) + 利用可能 + %1$s 合計 + 音楽の使用量 + デバイス使用中 + %1$d%% + <1% + %1$d 曲(クラウド) + %1$d ファイルは読み取り不可 + 再生パス + はい + いいえ + サンプルレート + %1$d Hz + %1$d フレーム/バッファ + Hi-Fi PCM Float + 32 ビット float 出力パス + 低レイテンシーサポート + プロオーディオサポート + メモリ + %1$s 中利用可能 + オフロード対応フォーマット + ハードウェアオフロードをサポートする圧縮フォーマットは報告されませんでした。 + 他 %1$d 件 + 検出された出力 + 内蔵出力 + Bluetooth オーディオ + USB オーディオ + 有線オーディオ + デジタル出力 + その他の出力 + Android から出力ルートは報告されませんでした。 + ExoPlayer エンジン + %1$s レンダラー + フォーマット互換性 + %1$d 対応トラック + %1$d 不明なフォーマット + デコーダーが報告されません + ハードウェアデコーダー + ソフトウェアデコーダー + オフロード + ライブラリ内 %1$d 件 + 互換性の確認結果 + 大きな非互換性はありません + インデックスされたトラックはこのデバイスで Android が報告するデコーダーと一致しています。 + %1$d 件のトラックはネイティブデコードできない可能性があります + 確認が必要なフォーマット: %1$s。 + %1$d 件のローカルトラックはリサンプリングされる可能性があります + ライブラリは現在の出力サンプルレートを超える %1$d Hz に達しています。 + %1$d 件のトラックはメタデータが不明です + ライブラリを完全にリスキャンすると MIME、ビットレート、サンプルレートの欠損データを補完できます。 + デバイス情報 + メーカー + モデル + ブランド + デバイス + Android バージョン + SDK バージョン + ハードウェア + パフォーマンスレポート + 再生やスキャンのラグを分類するのに役立つ共有可能な診断レポートを生成します。デバイス、ライブラリ、タイミングデータのみを含み、ファイルパス、タイトル、アーティストは含まれません。 + レポートを生成 + 再生成 + コピー + 共有 + レポートをクリップボードにコピーしました + PixelPlayer パフォーマンスレポート + 高度なパフォーマンス診断 + デフォルトではオフです。ベータのトラブルシューティング用に短いラグタイムラインを記録します。 + %1$s まで有効 + 今ラグをマーク + ラグの瞬間をマークしました + + + 接続済みアカウント + リンクされたプロバイダーを管理して各連携をコントロールします。 + リンク済みサービス + アクティブ + 利用可能 + 近日公開 + 接続済み + 近日公開 + サービスを開く + ログアウト中… + リンク済みアカウントがまだありません + プロバイダーを接続するとこの画面で管理できます。 + %1$s に接続 + %1$s(近日公開) + Google Drive は近日公開予定です。 + 現在この画面を開けません。 + + + このアプリについて + PixelPlayer + コミュニティと共に作られたオープンソースの音楽プレイヤー。 + GitHub + ソースコード、リリース、イシュー + Telegram + コミュニティに参加、開発をフォロー + Telegram に参加 + GitHub リポジトリを開く + バージョン v%1$s + オープンソース + コミュニティファースト + Material 3 エクスプレッシブ + 現在コントリビューターが見つかりません。後でもう一度お試しください。 + メンテナー + PixelPlayer の開発者。 + コミュニティスポットライト + 大きな貢献をしたコラボレーターへの感謝。 + オープンソースコントリビューター + GitHub からのライブコントリビューターリスト。 + %1$d 回のコントリビューション + GitHub プロフィールを開く + Telegram を開く + %1$s のアバター + %1$s のアイコン + diff --git a/app/src/main/res/values-ja/strings_widget.xml b/app/src/main/res/values-ja/strings_widget.xml new file mode 100644 index 0000000000..c0d6f47874 --- /dev/null +++ b/app/src/main/res/values-ja/strings_widget.xml @@ -0,0 +1,17 @@ + + + サイズに合わせて自動調整するウィジェット + コンパクトなプレイヤーバー + シャッフルとリピートを含むフルコントロール + ミニマリストな正方形プレイヤー + + タップして開く + アルバムアート + アルバムアートのプレースホルダー + + タップして再生 + 曲のタイトル + アーティスト + + 進行バー、%1$d%% + diff --git a/app/src/main/res/values-ko/strings_changelogs.xml b/app/src/main/res/values-ko/strings_changelogs.xml index acb78fe7b1..81b43ec1ca 100644 --- a/app/src/main/res/values-ko/strings_changelogs.xml +++ b/app/src/main/res/values-ko/strings_changelogs.xml @@ -129,4 +129,22 @@ 현지화: 스페인어, 프랑스어, 러시아어, 중국어(간체), 인도네시아어, 이탈리아어 + + 플레이어 수명 주기 관리와 함께 Google Drive 통합. + 노래 메타데이터 일괄 편집(태그 및 앨범 아트). + 맞춤형 Wear OS 환경설정과 함께 AI 가사 번역. + 지연 진단 도구 및 검색 화면에서 다중 선택. + 아랍어 및 터키어 지원, 로컬 네트워크 옵션을 위한 지역화된 http URL. + + + 획기적인 배터리 절약(오디오 오프로드 및 UI 폴링 게이트). + 최적화된 대기열 관리(더 빠른 삽입 및 명시적 인덱싱). + 전환 화면을 위한 Material 3 Expressive 모션 애니메이션. + 조절된 스캐닝을 통한 리팩토링된 라이브러리 동기화. + + + 끊김/건너뜀 재생 지연 및 버퍼링 문제 해결. + 외부 노래 삭제 동기화 및 메타데이터 일관성 수정. + Wear OS 및 휴대폰에서 메모리 문제, 충돌 및 레이아웃 결함 수정. + \ No newline at end of file diff --git a/app/src/main/res/values-ko/strings_settings.xml b/app/src/main/res/values-ko/strings_settings.xml index 4b4fe3a87f..1d1957f05f 100644 --- a/app/src/main/res/values-ko/strings_settings.xml +++ b/app/src/main/res/values-ko/strings_settings.xml @@ -7,10 +7,14 @@ 테마, 레이아웃 및 시각적 스타일 재생 오디오 동작, 크로스페이드 및 백그라운드 재생 + 가사 + 소스 우선순위, 몰입형 가사 및 가져오기 관리 동작 제스처, 햅틱 및 내비게이션 동작 AI 연동 (β) - AI 제공업체, API 키 및 모델 설정 + AI providers, API keys, model selection, and activity logs + Generation Parameters + Temperature, sampling, prompts, and song data configuration 백업 및 복구 개인 앱 데이터 내보내기 및 복구 개발자 옵션 @@ -174,6 +178,9 @@ 한국어 노르웨이어 (Bokmål) 터키어 + 일본어 + 중국어(번체) + 베트남어 앱 테마 밝은 테마, 어두운 테마 또는 시스템 설정 따르기 중에서 선택하세요. 밝은 테마 diff --git a/app/src/main/res/values-nb/strings_changelogs.xml b/app/src/main/res/values-nb/strings_changelogs.xml index c12abe6370..0c8ebb6711 100644 --- a/app/src/main/res/values-nb/strings_changelogs.xml +++ b/app/src/main/res/values-nb/strings_changelogs.xml @@ -129,4 +129,22 @@ Lokalisering: Spansk, Fransk, Russisk, Forenklet kinesisk, Indonesisk, Italiensk + + Google Drive-integrasjon med spillerens livssyklusstyring. + Batch-redigering av sangmetadata (tagger og coverkunst). + AI-tekstoversettelse med tilpassbare Wear OS-preferanser. + Forsinkelsesdiagnoseverktøy og flervalg på søkeskjermen. + Arabisk og tyrkisk støtte, med lokaliserte http-URL-alternativer for lokalnettverk. + + + Drastisk batteribesparelse (lydavlasting og UI-avstemningsporter). + Optimalisert køstyring (raskere innsettinger og eksplisitt indeksering). + Material 3 Expressive-bevegelsesanimasjoner for overgangsskjermer. + Omstrukturert biblioteksynkronisering via strupte skanninger. + + + Løst problemer med hakkete/hoppende avspillingsforsinkelser og bufringsproblemer. + Fikset synkronisering av eksterne slettede sanger og metadatakonsistens. + Fikset minneproblemer, krasjer og layoutfeil på Wear OS og telefon. + \ No newline at end of file diff --git a/app/src/main/res/values-nb/strings_settings.xml b/app/src/main/res/values-nb/strings_settings.xml index d8e2abe1fc..39af2eb945 100644 --- a/app/src/main/res/values-nb/strings_settings.xml +++ b/app/src/main/res/values-nb/strings_settings.xml @@ -7,10 +7,14 @@ Temaer, layout og visuelle stiler Avspilling Lydatferd, crossfade og bakgrunnsavspilling + Lyrics + Source priority, immersive lyrics, and import management Atferd Gester, haptikk og navigasjonsatferd AI-integrasjon (β) - AI-leverandører, API-nøkler og modellinnstillinger + AI providers, API keys, model selection, and activity logs + Generation Parameters + Temperature, sampling, prompts, and song data configuration Sikkerhetskopiering og gjenoppretting Eksporter og gjenopprett dine personlige app-data Utvikleralternativer @@ -174,6 +178,9 @@ Koreansk Norsk bokmål Tyrkisk + Japansk + Tradisjonell kinesisk + Vietnamesisk App-tema Bytt mellom lyst, mørkt eller følg systemets utseende. Lyst tema diff --git a/app/src/main/res/values-ru/strings_changelogs.xml b/app/src/main/res/values-ru/strings_changelogs.xml index 1e30b701ce..048bcadced 100644 --- a/app/src/main/res/values-ru/strings_changelogs.xml +++ b/app/src/main/res/values-ru/strings_changelogs.xml @@ -129,4 +129,22 @@ Локализация: испанский, французский, русский, упрощённый китайский, индонезийский, итальянский + + Интеграция с Google Drive с управлением жизненным циклом плеера. + Пакетное редактирование метаданных песен (теги и обложки). + Перевод текстов песен с помощью ИИ с настраиваемыми параметрами Wear OS. + Инструмент диагностики задержек и множественный выбор на экране поиска. + Поддержка арабского и турецкого языков, локализованные http URL для локальной сети. + + + Кардинальная экономия заряда батареи (выгрузка аудио и шлюзы опроса UI). + Оптимизированное управление очередью (более быстрая вставка и явная индексация). + Анимация Material 3 Expressive для экранов переходов. + Переработанная синхронизация библиотеки с регулируемым сканированием. + + + Устранены задержки воспроизведения (заикания, пропуски) и проблемы с буферизацией. + Исправлена синхронизация удаления внешних песен и согласованность метаданных. + Исправлены проблемы с памятью, сбои и ошибки макета на Wear OS и телефоне. + \ No newline at end of file diff --git a/app/src/main/res/values-ru/strings_settings.xml b/app/src/main/res/values-ru/strings_settings.xml index c49784964b..c99d6d76c1 100644 --- a/app/src/main/res/values-ru/strings_settings.xml +++ b/app/src/main/res/values-ru/strings_settings.xml @@ -7,10 +7,14 @@ Темы, макеты и визуальные стили Воспроизведение Поведение аудио, кроссфейд и фоновое воспроизведение + Тексты песен + Приоритет источника, иммерсивный режим и управление импортом Поведение Жесты, тактильный отклик и навигация Интеграция ИИ (β) - Поставщики ИИ, API-ключи и настройки моделей + AI providers, API keys, model selection, and activity logs + Generation Parameters + Temperature, sampling, prompts, and song data configuration Резервное копирование и восстановление Экспорт и восстановление персональных данных приложения Параметры разработчика @@ -174,6 +178,9 @@ Корейский Норвежский (Bokmål) Турецкий + Японский + Китайский (традиционный) + Вьетнамский Тема приложения Светлая, тёмная тема или настройки системы. Светлая тема diff --git a/app/src/main/res/values-tr/strings_settings.xml b/app/src/main/res/values-tr/strings_settings.xml index 71b05a38e5..31fba8ef40 100644 --- a/app/src/main/res/values-tr/strings_settings.xml +++ b/app/src/main/res/values-tr/strings_settings.xml @@ -10,7 +10,9 @@ Davranış Hareketler, dokunsal geri bildirim ve gezinme davranışı Yapay Zeka Entegrasyonu (β) - YZ sağlayıcıları, API anahtarları ve model ayarları + AI providers, API keys, model selection, and activity logs + Generation Parameters + Temperature, sampling, prompts, and song data configuration Yedekle ve Geri Yükle Kişisel uygulama verilerinizi dışa aktarın ve kurtarın Geliştirici Seçenekleri diff --git a/app/src/main/res/values-vi/strings.xml b/app/src/main/res/values-vi/strings.xml new file mode 100644 index 0000000000..2cb4e6d908 --- /dev/null +++ b/app/src/main/res/values-vi/strings.xml @@ -0,0 +1,128 @@ + + + PixelPlayer + Music Player + App Name Change + We have changed our app\'s name from PixelPlay to PixelPlayer due to a trademark-related issue. Keep playing! + Do not show again + + + Home + Search + Library + + + Special Permission Required + To edit song metadata (.mp3 files), PixelPlayer needs special access to all files. This allows us to modify track tags directly. Please grant this permission on the next screen to enable metadata editing. + Grant Permission + + + Quick play + Unable to open that audio file. + Open full player + + + Shuffle + Shuffle all songs + Shuffle All + Last Playlist + No playlist available to open + + + Open Play Store + Continue beta + The Play Store link will be enabled from GitHub config. + PixelPlayer is now available on Google Play + Use the stable channel on Google Play for release updates while we keep beta builds active. + PixelPlayer + Release announcement + Coming soon + + + Thank you for using PixelPlayer! + HIGH %1$d + Close + SCORE + LVL %1$d + LIVES + LEVEL COMPLETE! + GAME OVER + Score: %1$d + Try Again? + Next Level + Restart Game + TAP TO RELAUNCH + Play Random Music + Brick Breaker + HIGH SCORE %1$d + Play + Drag to move the paddle + + + Close player + Processing playback action… + Playback error: %1$s + + + Back + OK + Cancel + Dismiss + Error + Search + Clear search + All + Confirm + Saved! + Selected + %1$d%% + Artist + Select All + Clear + Unknown error + + + Save + Done + Reset + Apply + Shuffle + Copy + Share + Undo + Import + Delete + Export + Merge + Rename + Create + Lyrics + Settings + Album Art + Playlist + Unknown track + Unknown artist + Unknown album + Close + Add + Remove + Play + Previous track + Next track + Favorite + Pause + Repeat + Options + Shuffle Play + More options for %1$s + Expand menu + Next + Finish + Reset defaults + Export all + Merge all + Share all + Play album + Shuffle play album + Album art for %1$s + diff --git a/app/src/main/res/values-vi/strings_changelogs.xml b/app/src/main/res/values-vi/strings_changelogs.xml new file mode 100644 index 0000000000..21e8758403 --- /dev/null +++ b/app/src/main/res/values-vi/strings_changelogs.xml @@ -0,0 +1,150 @@ + + + Changelog + View on GitHub + Improvements + Fixes + What\'s New + Added + + Chromecast support for casting audio from your device. + In-app changelog to keep you updated on the latest features. + Support for .LRC files, both embedded and external. + Offline lyrics support. + Synchronized lyrics (synced with the song). + New screen to view the full queue. + Reorder and remove songs from the queue. + Mini-player gestures (swipe down to close). + Added more material animations. + New settings to customize the look and feel. + New settings to clear the cache. + + + Complete redesign of the user interface. + Complete redesign of the player. + Performance improvements in the library. + Improved application startup speed. + The AI now provides better results. + + + Fixed various bugs in the tag editor. + Fixed a bug where the playback notification was not clearing. + Fixed several bugs that caused the app to crash. + + + Introduced a richer listening stats hub with deeper insights into your sessions. + Launched a floating quick player to instantly open and preview local files. + Added a folders tab with a tree-style navigator and playlist-ready view. + + + Refined the overall Material 3 UI for a cleaner and more cohesive experience. + Metadata editing now supports cover art change. + Smoothed out animations and transitions across the app for more fluid navigation. + Enhanced the artist screen layout with richer details and polish. + Upgraded DailyMix and YourMix generation with smarter, more diverse selections. + Strengthened the AI playlist generation. + Improved search relevance and presentation for faster discovery. + Expanded support for a broader range of audio file formats. + + + Resolved metadata quirks so song details stay accurate everywhere. + Restored notification shortcuts so they reliably jump back into playback. + + + Major navigation redesign + New file explorer for choosing source directories + New Connectivity and casting functionalities + Seamless continuity between remote devices + Gapless transition between songs + Crossfade control + New Custom Transitions feature (only for playlists) + Keep playing after closed the app + UI Optimizations + Improved stats feature + Redesigned Queue control with more features + Improved different filetypes support for playing and metadata editing + Improved permission controller + Minor bug fixes + + + Material 3 Expressive UI Update + 10-band Equalizer & Effects + New Library Sync Flow + AI Integration (Gemini Models) + M3U Playlist Import/Export + Deezer Artist Artwork Integration + Custom Playlist Covers + Settings Architecture Refactor + Queue & Player Animations + Baseline Profiles & Performance + Better Lyrics System with Sync Offset + + + Casting Stability Improvements + Player Sheet Stability + General Bug Fixes & Cleanup + + + Android Auto support is now available for in-car playback. + Wear OS support is live, including better watch-to-phone playback controls. + Cloud integrations expanded with Telegram, NetEase, QQ Music, and Google Drive improvements. + Recently Played and persistent queue restoration keep your listening session ready. + Backup & Restore v3 and account management tools are now included. + Lyrics got smarter with manual search fallback and storage improvements. + + + Big performance pass across startup, library, queue, and player interactions. + Player, Cast, Lyrics, Artist, and Genre surfaces were redesigned for smoother use. + Navigation and search flows are more reliable, with safer route handling. + Audio playback compatibility improved for more devices and formats. + Multi-selection workflows were expanded across songs, albums, and playlists. + + + Queue and shuffle behavior is now more stable and predictable. + Several background playback and casting edge cases were fixed. + Sleep Timer, Files tab navigation, and album artist crash issues were fixed. + Widget loading and service stability were improved to reduce overheating/memory issues. + General bug fixes and UI polish across the app. + + + Wear OS: Music transfer, local playback, queue sync, and remote control from watch. + AI: Groq AI and OpenRouter (experimental) integration with token optimization. + Cloud: Jellyfin support added. + Lyrics: Synced translation with dedicated toggle, Kugou LRC format support, text alignment customization, and improved remote loading. + UI/UX: Compact navigation bar mode, dynamic themes from album art palette, marquee for long titles, and new sorting options. + Telegram: Native topics support and enhanced display modes. + + + Audio Engine: Complete overhaul with support for more formats (MIDI, ALAC, M4A) and decoder optimization. + Efficiency: Drastic energy consumption reduction, overheating fixes, and background task optimization (SyncWorker). + Database: Massive query optimizations and redesigned cover art cache to prevent data loss. + Startup: Improved loading time via Baseline Profile optimization. + + + Playback: Fixed stuttering in Opus/MP3, ReplayGain errors during crossfades, and startup issues on Samsung decoders. + Stability: Eliminated crashes on startup, artist navigation, and Android 12+ devices. + UI: Fixed cover art flickering, text overflow in non-Latin scripts, and navigation bar/miniplayer behavior. + Security: Hardened credential handling, storage permissions, and media server communication. + + + Localization: Spanish, French, Russian, Simplified Chinese, Indonesian, Italian + + + Google Drive integration with player lifecycle management. + Batch song metadata editing (tags and cover art). + AI lyrics translation with customizable Wear OS preferences. + Lag diagnostic tool and multi-selection on Search screen. + Arabic & Turkish support, with localized http URL local-network options. + + + Drastic battery saving (audio offload and UI polling gates). + Optimized queue management (faster insertions and explicit indexing). + Material 3 Expressive motion animations for transition screens. + Refactored library synchronization via throttled scanning. + + + Resolved stuttering/skipping playback lags and buffering issues. + Fixed external song deletion sync and metadata consistency. + Fixed memory issues, crashes, and layout glitches on Wear OS and phone. + + \ No newline at end of file diff --git a/app/src/main/res/values-vi/strings_cloud_services.xml b/app/src/main/res/values-vi/strings_cloud_services.xml new file mode 100644 index 0000000000..df633dab6a --- /dev/null +++ b/app/src/main/res/values-vi/strings_cloud_services.xml @@ -0,0 +1,228 @@ + + + + Telegram Login + You are editing your number. Sending code again will replace the previous one. + Working… + Initializing Telegram… + Logging out… + Closing session… + Session closed. Re-open login to continue. + Preparing secure Telegram session… + Waiting for Telegram response… + Connect Telegram + Connect Telegram to stream music from your channels and chats. + Phone Number + Enter your Telegram number. You can come back and edit it later. + Phone number + 1 + 5551234567 + Send Code + Verification Code + Enter the code from Telegram. If the number is wrong, go back and edit it. + Code + 12345 + Edit phone + Resend code + Verify Code + Two-Step Password + Enter your Telegram password. You can still go back to fix your number. + Password + Verify Password + Please wait… + + + Telegram Channels + Add Channel + Public Telegram channel + Syncing + Sync now + Collapse topics + Show topics + Channel options + Topics + Syncing channel + Updating songs from Telegram + Fetch latest songs from this channel + Remove channel + Stop syncing and remove cached songs + Remove channel? + %1$s will stop syncing and all cached songs from this channel will be removed. + Remove + No Channels Synced + Add public Telegram channels to sync\nyour music library + Add channel + Never synced + Synced %1$s + + + Add Channel + Search for a public Telegram channel to sync its music + \@channelname or link + Searching… + Search for a channel + Enter a public channel username or link\nto sync its audio files + + + %d song + %d songs + + + %d topic + %d topics + + + + Subsonic + Manage Navidrome, Airsonic and other Subsonic-compatible servers. + + + Tap sync to fetch your Jellyfin playlists + Manage your Jellyfin server connection. + + + Music folders + Tap + to add a Drive folder + No folders added yet + %1$d folders synced + Add folder + + + Select playlist type + Choose which playlists to sync: + All playlists + Created & collected + Created playlists + Collected playlists + + + %1$d playlists synced + Playlists + Sync + No playlists synced yet + Tap sync to fetch your playlists + Quick actions + Sync library + Disconnect + %1$d songs + + + Syncing + Syncing library… + Fetching playlists… + Syncing playlist: %1$s + Updating local library… + Sync complete + Fetching album list… + Fetching songs from %1$s… + Saving %1$d songs to database… + No library songs found + Library sync complete + Syncing… + Error: %1$s + + + Sync + Sync all + Log out + Sync all playlists + User avatar + + + No internet connection + This content requires an internet connection. Please check your network settings and try again. + You\'re offline + Please check your internet connection and try again to access this content. + + + Connect + Connecting… + Enter your server URL and account credentials. + Connection details + Hide password + Password + Enter password + Prefill http:// + Server URL + Show password + Telegram + Username + admin + Welcome, %1$s! + + + Compatible with Navidrome, Gonic, Airsonic, and other Subsonic-compatible servers + Supports Navidrome, Airsonic, Gonic, Ampache and other servers compatible with the Subsonic API. + App password also works if your server supports it. + Prefill https:// + Connect to your self-hosted music server + Navidrome + Use the full https:// base address of your server. + https://music.example.com + This is your Subsonic or Navidrome account name. + Subsonic / Navidrome + Subsonic + + + Enter your Jellyfin server URL and account credentials. + Connects to Jellyfin servers for streaming your music library + Connects to Jellyfin servers. Both HTTP and HTTPS are supported for local network access. + Jellyfin + Your Jellyfin account password. + Connect to your Jellyfin media server + Jellyfin + Full URL of your Jellyfin server, including port. + http://192.168.1.100:8096 + Your Jellyfin account username. + + + Stream music files directly from your Google Drive + Connect Google Drive + Google Drive connected! + Create \"PixelPlayer Music\" + Create a new folder here for your music + No folders here + Open folder + Choose or create a folder to use as your music source + Select a music folder + Setting up Google Drive… + Sign in with Google + Google Drive + Use + + + Could not read session cookies. + Done + Exit + No cookies found. Log in first. + Page is taking too long to load. Use refresh or try another network. + + + Saving… + Stay + Page load timed out. You can retry without losing your progress. + Web back + You can come back later. Current page state will be discarded when closing. + Web forward + Refresh + Retry + Open home + WebView load failed. + + + Failed to read NetEase cookies: %1$s + Exit NetEase login? + HTTP %1$d while loading NetEase. + Login not detected yet. Complete NetEase login before pressing Done. + Login to NetEase Music + Security note: your password is entered only in NetEase web pages. PixelPlayer stores session cookies (MUSIC_U) to sync your library. + NetEase Music + + + Failed to read QQ Music cookies: %1$s + Exit QQ Music login? + HTTP %1$d while loading QQ Music. + Login not detected yet. Complete QQ Music login before pressing Done. + Login to QQ Music + Security note: your password is entered only in QQ Music web pages. PixelPlayer stores session cookies to sync your library. + QQ Music + \ No newline at end of file diff --git a/app/src/main/res/values-vi/strings_equalizer.xml b/app/src/main/res/values-vi/strings_equalizer.xml new file mode 100644 index 0000000000..c633b7bf65 --- /dev/null +++ b/app/src/main/res/values-vi/strings_equalizer.xml @@ -0,0 +1,57 @@ + + + + Name cannot be empty + Rename + + + Change view mode + Disable equalizer + Enable equalizer + Edit + Edit presets + Custom preset + Presets + Update + Bass Boost + Virtualizer + Loudness + Not supported + Not supported on this device + Volume + Frequency Response + Hz + Bass + Low Mids + High Mids + Treble + Bass / Low + Mid / High + Page %1$d + Reset duration + Save New + + + Saved Presets + No custom presets saved yet. + Unpin + Pin + Rename + Delete + + + Save custom preset + Enter a name for your custom equalizer preset. + Preset name + Rename preset + + + Manage presets + Drag to reorder • Tap the eye to show or hide + Reorder + Reset presets + This will restore the default preset order and visibility. Continue? + Reset to default + Visible + Hidden + diff --git a/app/src/main/res/values-vi/strings_home_screen.xml b/app/src/main/res/values-vi/strings_home_screen.xml new file mode 100644 index 0000000000..2a7e3ad80f --- /dev/null +++ b/app/src/main/res/values-vi/strings_home_screen.xml @@ -0,0 +1,277 @@ + + + + β + Beta + Cloud Streaming + Changelog + Cloud Streaming + Stream music from your cloud accounts + + + Beta 0.7.5 + β + Welcome to PixelPlayer 0.7.5-beta + You\'re using a beta build that may contain bugs, crashes, or experimental features. Help us improve by reporting issues. + What to expect + Bugs, crashes, or incomplete features may occur unexpectedly. + Some features may change or be removed without notice. + Beta builds may be more unstable than release versions. + Always check for updates before reporting a known issue. + What beta builds can change, break, or improve while testing. + GitHub issue shortcut + Search first, then open a focused report for bugs, crashes, requests, or questions. + Open existing issues + Report issue or crash + Share steps to reproduce, expected results, actual results, and your device/OS details. + How to report + A quick checklist before opening a new issue. + Before opening an issue + Search existing open and closed issues to avoid duplicates. + Update to the latest PixelPlayer version and confirm the problem still happens. + Restart the app and confirm the problem persists. + Try to reproduce it and write down the exact steps. + Which issue type? + Bug report: Something behaves incorrectly. + Feature request: Add a new feature or improvement. + Question: Use Discussions if enabled, or open an issue with a question label. + Bug report + Copy these fields when something behaves incorrectly or crashes. + Bug Report + Short summary: + Expected behavior: + Current behavior: + Steps to play/reproduce: 1. 2. 3. + How often does it happen? Always / Sometimes / Rarely. + Screenshot / video: if available. + Logs / stack trace: if available. + Environment + PixelPlayer version: + Install source: GitHub Release, debug build, nightly build, etc. + Android version: + Device model: + Extra context: SD card usage, special settings, permissions, etc. + Feature request + Copy these fields when you want a new feature or improvement. + Problem statement: What problem are you trying to solve? + Proposed solution: How should it work? + Alternatives considered: Any other approaches? + Scope: Which screens or flows are affected? + Mockup or reference image if available. + Titles, privacy, and scope + Make the report easy to triage and safe to share. + Good issue titles + Equalizer: Indicator shifts when switching preset tabs + Search: History list does not appear for empty query + Feature: Add \"Recently Added\" playlist sort option + Please avoid + Generic reports like \"It doesn\'t work\". + Multiple unrelated problems in one issue. + Unredacted logs or screenshots with private data. + Privacy note + Before posting logs, screenshots, or videos, remove personal or private information. + Nightly builds + How nightlies differ from releases, and what to include when they break. + Nightly builds are generated from the latest commit and may contain unfinished changes, temporary bugs, or regressions. They are more experimental than official releases. + Access them from the repository\'s GitHub Actions workflow artifacts if available. + Reporting nightly issues + When reporting an issue from a nightly build, always mention that it happened on a nightly build, not on the official release. Include the build date, workflow run name or number, or commit SHA if possible. Also check if the same problem happens on the latest official release. + Beta 0.5.0 upgrade + Clean install recommended + If you\'re coming from beta 0.5.0, this update may need fresh library data instead of older cached state. + If metadata or library entries look wrong + Wrong song metadata, mismatched artists or albums, or duplicate-looking entries usually mean a clean install is the fix. + Don\'t show again + Got it + + + Oops! Something went wrong + The app crashed during your last session. Help us fix this by sharing the crash report. + Date: %1$s + Error: + Stack trace (preview): + Crash log + Crash log copied to clipboard + PixelPlayer crash report + Share crash report + + + DJ Mashup + + + Your\nMix + No data to show yet + Your mix will appear here when PixelPlayer finds songs or syncs a source. + Refresh + + + DAILY MIX + Based on History + Check all of Daily Mix + Daily Mix + + + Daily Mix + + %1$d song • %2$s + %1$d songs • %2$s + + Play it + AI Playlist Generator + + + How your Daily Mix is built + Your Daily Mix is built from your favorite and most-played songs. We also add tracks from artists and genres you like so you can discover new music. + Tell the AI what you want to listen to today + We use a small sample to keep costs low + Updating… + Update Daily Mix + + + Perfectly Curated + Daily Mix + Your sonic journey is ready + AI Playlist Generator + Describe the vibe, mood, or activity and let AI curate the perfect playlist from your library. + Playlist size + Min songs + Max songs + e.g. Chill evening vibes, upbeat workout energy… + Tap to Retry + Sonic journey synthesized! + Ready to Play + Generating… + Generate Playlist + + + Recently Played + + + Recently Played + Play latest + No recent plays in %1$s + Change the range or play more songs to fill this timeline. + Recently Played + Today + Yesterday + + + Listening stats + Total plays + Avg per day + Top track + %1$s • %2$d plays + + + Listening Stats + Refresh listening stats + Today + Week to Date + Month to Date + Year to Date + All Time + Listening + Plays + Listening timeline + Listening time + Total listening captured in the selected range. + Play count + How many sessions you completed per segment. + Avg. session + Average listening length for each segment. + Split into 4-hour windows to reveal your daily rhythm. + Daily bars make week-to-week habits easy to compare. + Weekly bars show how the month is trending. + Monthly bars show seasonality across the year. + Yearly bars summarize your full history. + No listening data yet + Press play to start building your listening timeline + Daily rhythm + Weekly rhythm + Monthly rhythm + Year at a glance + All-time progression + Grouped into 4-hour segments + Grouped by day of week + Grouped by week of month + Grouped by month + Grouped by year + Peak segment + %1$d plays + + Top categories + Compare how you listen across genres, artists, albums, and songs. + Genre + Artist + Album + Song + Listening by genre + Listening by artist + Listening by album + Listening by song + %1$d plays • %2$d artists + %1$d plays • %2$d tracks + No category data yet + Press play to surface your listening highlights + Listening habits + No habits yet + We will surface your listening habits once we know you better. + Total sessions + Avg session + Longest session + Sessions/day + Most active day + No playback yet + Peak timeline slot + Top artists + No top artists + Keep listening and your favorite artists will show up here. + \? + %1$d. %2$s + Top albums + No top albums + Albums you revisit often will appear here. + %1$d. %2$s + Track concentration + How your listening time is distributed across your top tracks. + No concentration data yet + Play more tracks to see how focused your listening is. + Top 1 + Top 2-3 + Others + %1$d%% + Listening concentration + Top 3 tracks account for %1$d%% of your listening time. + Avg plays/track + Unique tracks + Top 3 share + Tracks in this range + Most played tracks for the selected time range. + No top tracks + Listen to your favorites to see them highlighted here. + Collapse tracks + Show all tracks + + + %1$d h %2$02d min + %1$d min + %1$d h %2$02d m + %1$d h + %1$d m + %1$d s + %1$dh %2$02dm + %1$dh + %1$dm + %1$ds + Never + Just now + 1 day ago + %1$d days ago + 1 hour ago + %1$d hours ago + 1 minute ago + %1$d minutes ago + %1$d Song + %1$d Songs + Week %1$d + diff --git a/app/src/main/res/values-vi/strings_library.xml b/app/src/main/res/values-vi/strings_library.xml new file mode 100644 index 0000000000..8b7c6c2c60 --- /dev/null +++ b/app/src/main/res/values-vi/strings_library.xml @@ -0,0 +1,566 @@ + + + + Library + Library tabs + Jump directly to any tab or reorder them. + Reorder tabs + + + Songs + Albums + Artists + Playlists + Folders + Liked + + + Playlist created successfully + Set your AI provider API key first + Set your Gemini API key first + Added to queue + Playing next + + + Watch transfer + Settings + Edit + Reorder tabs + Expand menu + + + You can select up to %1$d albums + Folder + Folder + + + Sort by + View + Playlist View + Grid + List + Internal + SD Card + SD card is not available right now. + Cloud + Telegram Cloud Channels + Topics Display + Channels + Topics + Both + Cloud + Cloud Only + + + Generating metadata with AI… + + + Error loading songs + Error loading albums + Error loading artists + Retry + + + No songs found in your library. + Try rescanning your library in settings if you have music on your device. + No songs found + + + New + Create new playlist + Import M3U playlist + Locate current song + All songs + CLOUD + LOCAL + Sort options + + + All + Deselect + More options + + + Scanning music files… + Processing files… + %1$d of %2$d files + Syncing library… + Sync complete + Waiting… + Syncing library… + Cleaning album art cache… + Syncing cloud sources… + Scanning lyrics… + + + No songs yet + Add music to your device or sync a cloud source to start listening. + No local songs found + Try another source filter or rescan your device library. + No cloud songs found + Sync Telegram or NetEase songs, or switch to local source. + No albums available + Albums will appear here as soon as your library has grouped tracks. + No local albums found + Local songs are required to build local album groups. + No cloud albums found + Cloud songs with album data will appear here after sync. + No artists available + Artists are shown after songs are indexed from any source. + No local artists found + No artist metadata is available for local songs right now. + No cloud artists found + Cloud artist entries appear when remote songs are synced. + No liked songs yet + Tap the heart icon while playing a song to save it here. + No liked local songs + Switch source filter or like songs from your device. + No liked cloud songs + Like Telegram or NetEase tracks to see them in this view. + No folders found + Internal storage folders with music will appear here. + No playlists yet + Create your first playlist to organize your library. + + + Edit song metadata + Play + Play song + Play all + Play all + Add to favorites + Add all to favorites + Remove from favorites + Remove all from favorites + Share song file via + Share song file + Share all as ZIP + Could not share song: %1$s + Add to queue + Add to queue + Next + Play next in queue + Add to playlist + Delete + Delete all + Checking watch + Transferring %1$d%% + Transferring to watch + Transfer in progress + Send to watch + Watch unavailable + Send song to watch + Watch unavailable + Set as + Set as sound + Choose how to use this song as a system sound + Use this song as + Choose where PixelPlayer should install this sound. + Phone ringtone + Incoming calls + Notification sound + Messages and app alerts + Alarm sound + Clock alarms + Confirm sound change + Set \"%1$s\" as your %2$s? + Set sound + Set \"%1$s\" as your %2$s + ringtone + notification sound + alarm sound + Enable Modify system settings, then return to PixelPlayer to finish automatically. + Modify system settings was not enabled. + Set \"%1$s\" as your ringtone + Only local songs can be used as ringtones. + Couldn\'t prepare this audio file for ringtone. + Couldn\'t set ringtone: %1$s + Options + OPTIONS + Info + INFO + Duration + Genre + Album + Artist + Song info + Provider + File + %1$d SONGS + selected + %1$d PLAYLISTS + %1$d ALBUMS + selected + Limit: %1$d albums per selection. + Queue + play respects your selection order. + %1$d GENRES + selected + Perform batch operations on all songs within these genres. + + + Default Order + Title (A-Z) + Title (Z-A) + Artist + Artist (Z-A) + Album + Album (Z-A) + Date Added + Date Added (Oldest First) + Duration + Duration (Shortest First) + Release Year + Release Year (Oldest First) + Fewest Songs + Most Songs + Name (A-Z) + Name (Z-A) + Number of Songs (Most) + Number of Songs (Fewest) + Date Created + Date Created (Oldest First) + Date Liked + Date Liked (Oldest First) + Fewest Subfolders + Most Subfolders + + + Title + Artist + Album + Date Added + Duration + Release Year + Song Count + Name + Number of Songs + Date Created + Date Liked + Subfolder Count + + + Source + Order + Descending + Ascending + Original Order + Tap to switch to ascending + Tap to switch to descending + This sort keeps its original order + Switch is on + + + Reorder library tabs + Reset order + Reset tab order to the default? + Reordering tabs… + Drag handle + + + Pick an Artist + 1 artist + %1$d artists + Primary artist + Artist page + + + Cancel transfer + %1$s / %2$s + Shows live progress for phone-to-watch music transfers + Watch transfers + Sending to Watch + Cancelled + Transfer cancelled + Transfer complete + Completed + Failed + Transfer failed + Multiple active transfers + %1$s • %2$s + Preparing + Preparing watch transfer + Preparing transfer… + Sending %1$d songs to watch + Sending to watch + Starting transfer… + Starting + Transferring + %1$d transfers + + + Edit song + Show information + Editing song metadata + Editing a song\'s metadata can affect how it\'s displayed and organized in your library. Changes are permanent and may not be reversible. + Got it + Information + Cover art + Select a square image and fine-tune it so your cover art looks great across the app. + Change cover art + Delete cover art + Title + Artist + Album + Album artist + Genre + Composer + Track number + Disc number + ReplayGain track (dB) + ReplayGain album (dB) + -6.50 + -8.20 + Preview of the new cover art + Current song cover art + Adjust your cover art + Use pinch and drag gestures to find the perfect framing. + Apply cover art + Unable to load the selected image + Search lyrics on lrclib.net + + + Edit %d Songs + Only modified fields will be updated. Leave fields empty to keep existing values. + (Mixed values) + (Optional - leave empty to skip) + Successfully updated %d songs + Updated %1$d of %2$d songs. Some files could not be edited. + Failed to update songs + Batch Cover Art + This will replace the cover art for all %d selected songs + Set Cover Art for All + Remove All Cover Art + (Multiple different covers) + + + Playlist dismissed + + + Create playlist + Choose the creation flow. + Manual + Design artwork, icon, shape and pick songs yourself. + With AI + Generate a curated playlist with advanced controls. + Requires a Gemini API key configured in settings. + Set up API key + + + AI Playlist Lab + Reset + Generating… + Generate + Intent + Playlist name (optional) + What should this playlist feel like? + Example: sunset drive with warm synths + Direction + Mood + Activity + Era + Curation engine + Energy + Controls the intensity and tempo of songs. 1 = calm/slow, 5 = high-energy/fast. + Discovery + Controls how familiar the selections are. 1 = your most played favorites, 5 = rarely played deep cuts. + Min songs + Max songs + Filters + Prioritize genres (optional) + e.g. synthwave, indie pop + Avoid genres (optional) + e.g. metal, hard trap + Preferred language (optional) + e.g. English, Spanish, instrumental + Prioritize favorites + Avoid explicit lyrics + Prompt preview + Your final prompt will appear here once you add preferences. + Curate with precision + Define mood, activity, constraints and depth. + The AI will only use songs from your local library. + Add at least one instruction for AI. + Set a valid song range. + %1$d/5 + Custom… + Enter custom value + Enter your custom value + + + Any era + Core request: %1$s. + Mood target: %1$s. + Activity context: %1$s. + Era focus: %1$s. + Prioritize genres: %1$s. + Avoid genres: %1$s. + Preferred language: %1$s. + Energy level target: %1$d/5. + Discovery target: %1$d/5 where 1 is familiar and 5 is deep cuts. + Prioritize songs closer to listener favorites when possible. + Avoid explicit lyrics whenever alternatives exist. + Keep transitions smooth and avoid repetitive artist clustering. + + Chill + Energetic + Happy + Dark + Romantic + Melancholic + + + Workout + Focus + Road trip + Party + Study + Late night + + + @string/playlist_creation_ai_era_any + 70s + 80s + 90s + 2000s + 2010s + 2020s + + + + No playlist has been created. + Touch the \'New Playlist\' button to start. + New playlist + Playlist name + My playlist + + + Add %1$d songs to… + Select playlists + Search for playlists… + Songs added to playlists + Playlist created and songs added + Internal Storage + + + Add songs + Add selected songs + Add + Search or filter songs… + Liked + Failed to load songs + Load more + + + Merge Playlists + Enter a name for the merged playlist: + Merged Playlist + This will merge %1$d selected playlists into one. + + + No valid songs found to play + Song not found in current list + Could not locate song + No songs found in library + Playback stopped: %1$s finished (End of Track). + Track + No songs to shuffle. + Selected Albums + No playable songs found in selected albums + No playable songs found in selected genres + Only the first %1$d albums were queued + %1$d albums queued (%2$d songs) + Could not queue selected albums + All songs already in favorites + No songs were in favorites + Creating ZIP file… + Failed to share: %1$s + + %d song added to queue + %d songs added to queue + + + %d song will play next + %d songs will play next + + + %d song added to favorites + %d songs added to favorites + + + %d song removed from favorites + %d songs removed from favorites + + + + No playlists to share + Share playlists + Share failed: %1$s + No playlists to export + Export failed: %1$s + Music/PixelPlayer Exports + Please configure your Gemini API key in Settings. + Playlist restored + + Sharing %d playlist + Sharing %d playlists + + + Exported %1$d playlist to %2$s + Exported %1$d playlists to %2$s + + + + Invalid album ID + Album ID not found + Error loading album data: %s + Album not found + + + Invalid artist ID + Artist ID not found + Error loading artist data: %s + Could not find the artist + + + Cannot delete currently playing song + %1$d files deleted (%2$d skipped - playing) + %1$d of %2$d files deleted + Failed to delete files + File deleted + Can\'t delete the file or file not found + Deletion cancelled + Delete song? + \"%1$s\" by %2$s\n\nThis song will be permanently deleted from your device and cannot be recovered. + These songs will be permanently deleted from your device and cannot be recovered. + + %d file deleted + %d files deleted + + + Delete %d song? + Delete %d songs? + + + + Metadata updated successfully + Updating %1$d songs… + Successfully updated %1$d songs! + Updated %1$d songs. Failed: %2$d + Lyrics saved successfully + Failed to save lyrics + No lyrics available to save + Permission denied – cannot edit files + Permission denied – cannot save lyrics + Permission denied – cannot edit this file + + + Please configure a valid API key for the selected AI provider in Settings. + AI Error: %s + The selected AI provider rejected the request because the account has no credits or available quota. + The selected AI model is no longer available. PixelPlayer tried to switch to a supported model automatically. + AI couldn\'t find any songs for your prompt. + Write an idea for your Daily Mix + Daily Mix updated with AI + Could not update: %s + AI couldn\'t find songs for this mix + \ No newline at end of file diff --git a/app/src/main/res/values-vi/strings_player.xml b/app/src/main/res/values-vi/strings_player.xml new file mode 100644 index 0000000000..e5a3b76777 --- /dev/null +++ b/app/src/main/res/values-vi/strings_player.xml @@ -0,0 +1,198 @@ + + + + Collapse player + Now Playing + Cloud stream + Cast + Bluetooth + Local playback + Connecting… + Open Queue + + + Get ready to connect + Allow PixelPlayer to see your nearby devices and current Wi‑Fi so we can keep your cast, Bluetooth audio, and speakers in sync. + Nearby devices + Needed to read and control your connected Bluetooth audio gear. + Location for Wi‑Fi + Android requires Location to share the Wi‑Fi network (SSID) you\'re on so we can find compatible cast devices. + Allow access + We only use these permissions for device interconnectivity — casting, controlling nearby speakers, and keeping audio in sync. + Connect device + Scanning nearby + Casting session + Connecting + Connected + This phone + Bluetooth audio + Local playback + Playing + Paused + Device volume + Phone volume + %1$d/%2$d + Battery level + Volume level + Disconnect + Connectivity + Turn on Wi-Fi or Bluetooth + Refresh connections + Wi-Fi + Off + On + Connected + Bluetooth + Off + On + Connected + Nearby devices + Refresh devices + Connected + Connecting + Available to connect + Available + Connecting... + Searching for devices… + Make sure your TV or speaker is on and sharing the same Wi‑Fi network. + Controls + Devices + + + Cast media server + Casting to device + Serving media to Cast device + %1$s: %2$s + Seeking is temporarily unavailable for this audio format on Cast because it can crash the Cast session. + + + Sleep timer + Timer + %1$d minutes + Timer set for %1$d minutes. + 1 time + + %d time + %d times + + Play count: %1$s + End of current track + Playback will stop at end of track. + Switch on + Custom time + Cancel timer + End of track + Timer cancelled. + Cannot enable end of track: no active song. + End of track timer deactivated: song changed from %1$s to %2$s. + Previous track + Current track + Set custom duration + + + Next up + Queue is empty for now. + + 1 track lined up. + %d tracks lined up. + + Queue + Queue is empty. + Reorder song + Toggle shuffle + Toggle repeat + Sleep timer + More actions + Locate current song + Clear queue + Clear queue + Are you sure you want to clear all songs from the queue except the current one? + Save as playlist + %1$s queue + Current queue + Dismiss song + removed + Save as playlist + Deselect all + Playlist name + Search songs to include… + No songs match \"%1$s\" + + %d song selected + %d songs selected + + Save as: %1$s + Enter a playlist name + Remove from playlist + More options for %1$s + + + Lyrics + Loading lyrics… + Synced + Static + Lyrics options + −.5 + −.1 + +.1 + +.5 + 0s + %1$+.1fs + + + Failed to search for lyrics + Failed to fetch lyrics from remote + Connection timed out. Please check your internet connection. + Network error. Please check your internet connection. + Server error (code %d). Please try again later. + + + Lyrics already available. Online fetch skipped. + Embedded lyrics already found. Online fetch skipped. + Local (.lrc) lyrics already found. Online fetch skipped. + + + Save Lyrics + Translate via AI + These lyrics already have a translation + These lyrics are already in this language + API is not configured + Lyrics translated successfully! + Translating lyrics... + Reset imported lyrics + Reset lyrics? + Are you sure you want to reset the lyrics for this song? + Appearance + Alignment + Align lyrics left + Align lyrics center + Align lyrics right + Controls + Adjust sync + Hide sync controls + Show romanization + Show translations + Disable immersive (once) + Keep screen on + + + Save Lyrics + Choose which version to save: + Synced (with timestamps) + Plain (text only) + + + Would you like to search for lyrics online? + Show lyric options + Always open the picker instead of auto-applying the first match + Searching for lyrics… + Lyrics not found + We couldn\'t find lyrics automatically. You can edit the title or artist and try searching manually. + Song Title + Artist (optional) + Found %d match(es) + SYNCED + %1$s • %2$s + Lyrics provided by + https://lrclib.net/ + diff --git a/app/src/main/res/values-vi/strings_screens.xml b/app/src/main/res/values-vi/strings_screens.xml new file mode 100644 index 0000000000..90ec1b118e --- /dev/null +++ b/app/src/main/res/values-vi/strings_screens.xml @@ -0,0 +1,244 @@ + + + + Error: Genre ID missing + + + Let\'s Go! + Step %1$d of %2$d + Please grant the required permission first. + Please grant all required permissions. + Welcome to + β + Beta + Let\'s get everything set up for you. + Media Permission + PixelPlayer needs access to your audio files to build your music library. + Permission Granted + Grant Media Permission + Notifications + Enable notifications to control your music from the lock screen and notification shade. + Enable Notifications + Do you have a backup? + If you already have a PixelPlayer backup, restore it now and skip most of the remaining setup on this device. + Import backup + Inspecting backup + Checking backup package… + Restoring backup + Skip / Not now + Restore Backup + Review what you want to import before finishing setup. + %1$d of %2$d modules selected + Created %1$s + Backup from %1$s + Unknown version + Restore selected + Restoring + Excluded folders + All folders are scanned by default. Pick any locations you want to ignore when building your library. + Choose folders to ignore + Grant storage permissions first + App Theme + Pick the look you want before you start exploring your library. + Dark + The default Material 3 dark look for PixelPlayer. + Light + A brighter Material 3 look across the app. + Follow system + Match your phone\'s current appearance setting. + Recommended + You can change this later in Settings > Appearance > App Theme. + Library Layout + Choose your preferred way to navigate your library. + Songs + Compact Mode + Using minimal pill navigation + Using standard tab row + SONGS + ALBUMS + ARTISTS + You can change this later in Settings > Appearance > Library Navigation. + App Navigation + Choose the style of the bottom navigation bar. + Default Style + Floating pill with rounded corners + Standard full-width bar + Customize Corner Radius + You can change this later in Settings > Appearance > Navbar Style. + Alarms & Reminders + Optional, but recommended if you use Sleep Timer and want PixelPlayer to stop playback exactly on time. + Grant Permission + Battery Optimization + Some Android devices aggressively kill background apps. Disable battery optimization for PixelPlayer to prevent unexpected playback interruptions. + Disable Optimization + All Set! + You\'re ready to enjoy your music. + + + Search… + Search + Clear search + Recent searches + Clear all + History + Delete search history item + No results + No results for \"%1$s\" + Nothing found + Try a different search term or check your filters. + No results found. + Browse by genre + No genres available. + + + Play %1$s + Collapse %1$s + Expand %1$s + Edit artist image + Change photo + Reset to default + Shuffle play artist + + + Disc %d + Cover of %1$s + %1$s · %2$s + + + Playlist not found. + This playlist is empty. + Tap on \'Add Songs\' to begin. + This folder doesn\'t contain songs. + Sort Songs + More Options + Playlist options + Edit playlist + Delete playlist + Delete playlist? + Are you sure you want to delete this playlist? + Set default transition + Export Playlist + %1$s • %2$s + Play it + Add + Add songs + Remove + Remove songs + Reorder + Reorder songs + + + Global transitions + Playlist rules + This configuration applies to all playback sources unless overridden. + Configure default behavior for this specific playlist. + Active Status + Global Default + Playlist Default + Following Global + Custom Override + Custom Override + Enable to set specific rules for this playlist. + Using global defaults + Changes saved successfully + Transition Style + How tracks blend together + None + Crossfade + Transition Duration + %1$ds total overlap + Reset Transition + Current Song + Next Song + Tracks will overlap for %1$ds + Volume Curves + Fine tune the audio slope + Fade Out + Fade In + + + New smart playlist + New playlist + Add Songs + Back or Cancel + Next + Create + Edit Playlist + Auto-generated collage + Add Photo + Pick Image + Change + Remove + Playlist Name + My awesome mix + Edit Cover + Adjust your cover art + Use pinch and drag gestures to find the perfect framing + Manual + Smart + Generate with AI + Smart Rule + Default + Image + Icon + Background Color + Icon Symbol + Shape Style + Shape parameters + Corner Radius + Smoothness + Sides + Curve + Rotation + Scale + Top Played + Your most played tracks. + Recently Played + Songs you listened to most recently. + Forgotten Favorites + Favorite tracks you haven\'t played in a while. + New Gems + Recently added tracks with low play counts. + + + Quick Fill Genre + Sort & Play + Shuffle + Sort By + Artist + Album + Title + Generic artist + %1$s Shuffle + + + Select Songs + Choose Genre + Search songs + New Genre + Add Custom + Add Custom Genre + Genre Name + Select Icon + Genre: %1$s + Select a genre + Quick Fill + + + DJ Space + Loading… + Deck %1$d + Load song + No song loaded + + Stem separation is not available yet. + Volume + Speed + Crossfader + Deck 1 + Deck 2 + Select a song + Play/Pause + Song cover + x%1$.2f + diff --git a/app/src/main/res/values-vi/strings_settings.xml b/app/src/main/res/values-vi/strings_settings.xml new file mode 100644 index 0000000000..b7f5cc4f53 --- /dev/null +++ b/app/src/main/res/values-vi/strings_settings.xml @@ -0,0 +1,654 @@ + + + + Music Management + Manage folders, refresh library, parsing options + Appearance + Themes, layout, and visual styles + Lyrics + Source priority, immersive lyrics, and import management + Playback + Audio behavior, crossfade, and background play + Behavior + Gestures, haptics, and navigation behavior + AI Integration (β) + AI providers, API keys, and model settings + Backup & Restore + Export and recover your personal app data + Developer Options + Experimental features and debugging + Equalizer + Adjust audio frequencies and presets + Device Capabilities + Audio specs, codecs, and decoder info + Accounts + Manage Telegram, Google Drive, NetEase, and more services + About + App info, version, and credits + + + On + Off + Enabled + Disabled + Open + Select all + Clear selection + Close notice + + + Library Structure + Excluded Directories + Folders here will be skipped when scanning your library. + Artists + Multi-artist parsing and organization options. + Filtering + Minimum Song Duration + Minimum Tracks per Album + Album Art Cache Limit + Sync and Scanning + Refresh Library + Scan entire library for new and modified files. + Full Rescan + Running full rescan + Full rescan started… + Library sync finished + Rebuild Database + Rebuild database? + This will completely rebuild your music library from scratch. All imported lyrics, favorites, and custom metadata will be lost. This action cannot be undone. + Rebuild + Rebuilding database + Rebuilding database… + Auto-scan .lrc files + Automatically scan and assign .lrc files in the same folder during library sync. + Lyrics Management + Lyrics Source Priority + Choose which source to try first when fetching lyrics. + Embedded First + Online First + Local (.lrc) First + Reset Imported Lyrics + Remove all imported lyrics from the database. + Reset imported lyrics? + This action cannot be undone. + + + Refresh + Everything is allowed by default. Tap a folder to mark it as excluded from scans. + No subfolders here + Navigate up + Go to root + + + Rescan Required + Artist settings have changed. Rescan your library to apply. + Rescan + Scanning… + Multi-Artist Parsing + Character Delimiters + Current: %1$s + Word Delimiters + None + Current: %1$s + Configure + Extract Artists from Title + Detect feat., ft., with in song titles + Library Organization + Group by Album Artist + Show collaboration albums under main artist + About Multi-Artist Parsing + + PixelPlayer splits artist tags using character delimiters (/, ;, &) and word delimiters (feat., ft., vs., x). Word delimiters are matched case-insensitively. + \"Extract Artists from Title\" detects patterns like (feat. Artist) in song titles. + Backslash (\) can be used to escape character delimiters. + + Examples + \u2192 + \u266A + \"Artist1/Artist2\" + Artist1, Artist2 + \"Drake feat. Rihanna\" + Drake, Rihanna + \"Marshmello x Bastille\" + Marshmello, Bastille + \"Song (ft. B)\" by A + A, B + \"AC\\DC\" + AC/DC (escaped) + + + Delimiters + Current Delimiters + Tap a delimiter to remove it. At least one delimiter is required. + Add New Delimiter + e.g., / or ; + Add delimiter + Default Delimiters + Reset Delimiters? + This will clear all your custom delimiters and restore the defaults. This action cannot be undone. + Delimiters reset to defaults + At least one delimiter is required + Delimiter added + Delimiter already exists or is invalid + Space + + + Word Delimiters + Current Word Delimiters + These keywords split artist names when surrounded by spaces. Matched case-insensitively. Tap to remove. + No word delimiters configured + Add New Word Delimiter + e.g., feat. or ft. + Add word delimiter + How Word Delimiters Work + Word delimiters are matched case-insensitively with spaces around them.\n\nSingle-character delimiters (like \"x\") require spaces on both sides to avoid false matches.\n\nExamples:\n \"Drake feat. Rihanna\" -> Drake, Rihanna\n \"Marshmello x Bastille\" -> Marshmello, Bastille\n \"A vs. B\" -> A, B + Reset Word Delimiters? + This will clear all your custom word delimiters and restore the default keywords. This action cannot be undone. + Word delimiter added + Already exists or is invalid + Word delimiters reset to defaults + + + Preparing sync + Reading MediaStore + Processing tracks + Saving to database + Scanning lyrics files + Cleaning album art cache + Syncing cloud sources + Completing sync + %1$s • %2$d%% (%3$d/%4$d) + %1$s… + + + Global Theme + App Language + Choose the language used across the app interface. + System default + English + Español + Deutsch + Français + Русский + 简体中文 + Bahasa Indonesia + Italiano + 日本語 + Korean + Norwegian (Bokmål) + 繁體中文 + Türkçe + Tiếng Việt + App Theme + Switch between light, dark, or follow system appearance. + Light Theme + Dark Theme + Follow System + Use Smooth Corners + Use complex shaped corners effectively improving aesthetics but may affect performance on low-end devices + Disable Blur Effects + Turn off blur effects across the app to save battery and resources. + Show Scrollbar + Display lateral scrollbar on music lists for quick scrolling + Now Playing + Player Theme + Choose the appearance for the floating player. + Album Art + System Dynamic + Show player file info + Show codec, bitrate, and sample rate in the player progress section. + Album Art Palette Style + Current: %1$s. Open live preview and choose style. + Carousel Style + Choose the appearance for the album carousel. + No Peek + One Peek + Home Collage + Collage Pattern + Choose the shape arrangement for the Your Mix collage. + Auto-Rotate Patterns + Cycle through collage patterns each time you visit Home. + Navigation Bar + NavBar Style + Choose the appearance for the navigation bar. + Default + Full Width + Compact mode + Show only icons and reduce the navbar height. + NavBar Corner Radius + Adjust the corner radius of the navigation bar. + Lyrics Screen + Immersive Lyrics + Auto-hide controls and enlarge text. + Auto-hide Delay + Time before controls hide. + 3s + 4s + 5s + 6s + App Navigation + Default Tab + Choose the Default launch tab. + Home + Search + Library + Library Navigation + Choose how to move between Library tabs. + Tab row (default) + Compact pill & grid + + + Colors + Palette style + Choose the album colors for the player UI. + Tonal Spot + Balanced and calm. + Vibrant + High saturation accents. + Expressive + Bold hue shifts and contrast. + Fruit Salad + Playful rotated accents. + Color accuracy + 0 keeps the current tuning. Higher values stay closer to the album art\'s dominant hue. + Current + More accurate + 0 • Current + %1$d • Subtle + %1$d • Balanced + %1$d • Precise + + + Adjust Corner Radius + Match the navbar shape\'s corners with your device\'s physical corners for a seamless look. + Corner Radius + %1$d dp + + + Background Playback + Keep playing after closing + If off, removing the app from recents will stop playback. + Battery Optimization + Disable battery optimization to prevent playback interruptions. + Battery optimization is already disabled + Could not open battery settings + Volume Normalization (ReplayGain) + Enable ReplayGain + Normalize volume levels using ReplayGain metadata from audio files. + Gain Mode + Track: normalize each song. Album: normalize per album. + Track + Album + Cast + Auto-play on cast connect/disconnect + Start playing immediately after switching cast connections. + Headphones + Resume when headphones reconnect + If playback paused because headphones were removed, resume automatically when they connect again. + Queue and Transitions + Crossfade + Enable smooth transition between songs. + Crossfade Duration + Hi-Fi Mode + Float 32-bit audio output. Disable if playback stutters on your device. + Not supported on this device (PCM_FLOAT AudioTrack unavailable). + Persistent Shuffle + Remember shuffle setting even after closing the app. + Show queue history + Show previously played songs in the queue. + + + Folders + Back gesture controls folders + In Folders tab, system back navigates folder stack before leaving Library. + Player Gestures + Tap background closes player + Tap the blurred background to close the player sheet. + Haptics + Haptic feedback + Enable vibration feedback across the app. + + + AI Provider + Provider + Choose your AI provider + Safe Token Mode + ON — Fast & cheap. Sends minimal data (~1K tokens) to AI. + OFF — Deep context. Sends full listening profile (~8K tokens) for richer results. + Credentials + %1$s API Key + Get from %1$s + Google AI Studio (aistudio.google.com) + DeepSeek Platform (api.deepseek.com) + Groq Console (console.groq.com) + Mistral AI Platform (console.mistral.ai) + NVIDIA Build (build.nvidia.com) + Moonshot AI Platform (platform.moonshot.cn) + Zhipu AI Open Platform (bigmodel.cn) + OpenAI Platform (platform.openai.com) + Model Selection + Loading available models… + Failed to load models + AI Model + Select a model. + Enter API Key + Prompt Behavior + System Prompt + Customize how the AI behaves. + Preset Prompts + Enter system prompt… + Professional Curator + You are \'Vibe-Engine\', a world-class music curator and master of sonic flow. Your goal is to construct seamless, high-fidelity listening experiences. Prioritize harmonic compatibility, logical BPM transitions, and a sophisticated balance between familiar favorites and tasteful, logic-driven discovery. + Creative Maverick + You are an avant-garde music explorer specializing in \'unexpected cohesion\'. Your mission is to break conventional genre boundaries by identifying non-obvious sonic parallels. Prioritize rare deep cuts, experimental textures, and artistic novelty while maintaining a surprising but undeniable transition logic. + Strict Librarian + You are a surgical music database architect. Your logic is driven by absolute metadata precision and rigid categorical adherence. Minimize algorithmic discovery in favor of strict genre consistency, energy-level matching, and maximizing the retrieval of highly specific user-defined preferences. + Atmospheric Guide + You are a master of ambient textures and low-energy flow. Focus exclusively on tracks that facilitate a \'deep focus\' or \'tranquility\' state. Prioritize acoustic warmth, minimalist arrangements, and gentle transitions, while strictly avoiding high-transient sounds or sudden shifts in dynamic range. + Sonic Enthusiast + You are an audiophile analyst focused on production complexity and instrumentation. Prioritize tracks characterized by high dynamic range, intricate polyrhythms, and superior soundstage quality. Favor active-listening pieces that reward the listener for paying attention to technical fidelity and arrangement detail. + Energy Catalyst + You are a high-momentum rhythm generator. Your philosophy centers on driving basslines, percussive intensity, and infectious grooves. Prioritize high-BPM club compatibility, syncopated energy, and continuous rhythmic tension to keep the listener\'s heart rate and motivation peak-level. + AI Usage Report + Total Consumption + %1$s tokens tracking\nPrompt: %2$s | Output: %3$s | Thought: %4$s + Clear Logs + AI Activity Log (%1$d) + %1$s · %2$s + Show + Hide + + + How backup works + Choose sections, export a .pxpl file, and import it later. Restore only replaces the sections you select. + Create Backup + Export Backup + No sections selected. + All sections selected. + Selected %1$d of %2$d sections. + %1$s Creates a .pxpl backup file. + Select & Export + Restore Backup + Import Backup + Select & Restore + Browse or pick from recent backups. Selected data will replace current data. + + + Choose exactly what you want to include in the backup package. + Select a .pxpl backup file to inspect. You\'ll choose which sections to restore in the next step. + %1$d of %2$d sections selected + %1$d of %2$d modules selected + Recent Backups + No recent backups + Previously imported backups will appear here. + %1$d entries · Will replace current data + Export .pxpl + Restore Selected + Transfer in progress… + PixelPlayer_Backup_%1$d.pxpl + Creating Backup + Restoring Backup + %1$d%% + %1$s • %2$s + Exporting + Importing + Restoring + Remove from history + Inspecting… + Browse for file + Step %1$d of %2$d + Restore Modules + Backup Details + Created + App Version + Schema + Device + Unknown + · %1$s + %1$d modules · v%2$s · schema v%3$d + \? + Select all + Clear selection + + + Invalid backup: %1$s + Preparing restore + Starting restore task. + Preparing backup + Starting backup task. + Backup restored successfully + Restore completed with some unresolved issues. + Restore could not be completed: %1$s + Restore failed: %1$s + Data exported successfully + Export failed: %1$s + Data restored successfully + Restore completed with unresolved issues. Failed: %1$s + v%1$d + %1$s %2$s + + + Experiments + Experimental + Player UI loading experiments and toggles. + Test Setup Flow + Launch the onboarding setup screen for testing. + Maintenance + Force Daily Mix Regeneration + Re-creates the daily mix playlist immediately. + Regenerate Daily Mix + Regenerate Daily Mix? + This will discard the current mix and generate a new one based on recent listening habits. + Daily Mix regeneration started + Force Stats Regeneration + Clears cache and recalculates playback statistics. + Regenerate + Working… + Regenerate Stats + Regenerate Stats? + This will clear the statistics cache and force a recalculation from the database history. + Stats regeneration started + Force Album Palette Regeneration + Rebuild all cached palette variants for every album art, or pick a single one to refresh. + Regenerate All + Regenerate all album palettes? + This will clear cached theme data and rebuild all palette styles for %1$d unique album arts. + Regenerating… + Regenerating album palettes… + Rebuilding cached palette variants for %1$d unique album arts. This can take a while on large libraries. + %1$d of %2$d completed + Regenerated %1$d album art palettes + Regenerated %1$d of %2$d album art palettes + Choose Song + Select a song to clear cached theme data and regenerate all palette styles from the album art. + Search by title, artist, or album + No songs match your search. + No songs with album art were found. + Regenerating palette… + Palette regenerated for %1$s + Could not regenerate palette for %1$s + Diagnostics + Trigger Test Crash + Simulate a crash to test the crash reporting system. + Test crash triggered from Developer Options - This is intentional for testing the crash reporting system + + + Experimental + PlayerUI loading tweaks + Animated Lyrics (High-end devices) + Uses spring animations and visual effects for lyrics. May cause frame drops on low-end devices. + Lyric Blur Effect + Applies a depth-of-field blur to inactive lyrics. + Blur Strength + Adjust the intensity of the blur effect. + %1$.1fx + Step 1 · Choose what to delay + Delay everything + Hold the full player content until the sheet background is fully expanded. + Album carousel + Delay album art and carousel until the sheet is expanded. + Song metadata + Delay title, artist, and lyrics/queue actions. + Progress bar + Delay the timeline and time labels until expansion completes. + Playback controls + Delay play/pause, seek, and favorite controls. + All delayed components are active. Disable \"Delay everything\" to customize each part. + Step 2 · Configure placeholder behavior + Use placeholders for delayed items + Keep layout stable by rendering lightweight placeholders while components wait for expansion. + Step 3 · Choose when placeholders switch to real content + Select one mode. Threshold mode uses sliders; Drag release mode waits until you release the sheet gesture. + Enable at least one delayed component to unlock trigger mode. + Threshold + Uses expansion percentage. + Drag release + Switches only after gesture release. + Expand threshold + How expanded the sheet must be before delayed components become visible. + Content appears at %1$d%% expansion + Also apply on player close + Use close threshold to switch back to placeholders while collapsing. + Close threshold + How much collapse is required before placeholders take over again. + Placeholders appear after %1$d%% collapse + Drag release mode bypasses thresholds and close behavior. The swap happens only when the sheet drag gesture ends. + Make placeholders transparent + Placeholders keep their layout space but become invisible. + Visual Quality + Album Art Resolution + Low (256px) - Better performance + Medium (512px) - Balanced + High (800px) - Best quality + Original - Maximum quality + + + Playback needs review + Ready for playback + -- + Formats + HW decoders + Local songs + Local Music Storage + Music size + %1$d local songs + Available + %1$s total + Music footprint + Device used + %1$d%% + <1% + %1$d cloud songs + %1$d files not readable + Playback Path + Yes + No + Sample Rate + %1$d Hz + %1$d frames per buffer + Hi-Fi PCM Float + 32-bit float output path + Low Latency Support + Pro Audio Support + Memory + available of %1$s + Offload-ready formats + No compressed format reported hardware offload support. + +%1$d more + Detected outputs + Built-in output + Bluetooth audio + USB audio + Wired audio + Digital output + Other output + No output routes were reported by Android. + ExoPlayer Engine + %1$s renderers + Format Compatibility + %1$d supported tracks + %1$d unknown format + No decoder reported + Hardware decoder + Software decoder + Offload + %1$d in library + Compatibility Findings + No major incompatibilities + Your indexed tracks match the decoders Android reports on this device. + %1$d tracks may not decode natively + Formats to review: %1$s. + %1$d local tracks may be resampled + The library reaches %1$d Hz, above the current output sample rate. + %1$d tracks have unknown metadata + A full library rescan can fill missing MIME, bitrate, and sample-rate data. + Device Info + Manufacturer + Model + Brand + Device + Android Version + SDK Version + Hardware + Performance report + Generate a shareable diagnostic report to help us classify playback or scan lag. It contains only device, library, and timing data — no file paths, titles, or artists. + Generate report + Regenerate + Copy + Share + Report copied to clipboard + PixelPlayer performance report + Advanced performance diagnostics + Off by default. Records a short lag timeline for beta troubleshooting. + Active until %1$s + Mark lag now + Lag moment marked + + + Connected Accounts + Manage linked providers and keep each integration under your control. + Linked Services + Active + Available + Soon + Connected + Coming soon + Open Service + Logging out… + No linked accounts yet + Connect a provider to manage it from this screen. + Connect %1$s + %1$s (Coming soon) + Google Drive is coming soon. + Unable to open this screen right now. + + + About + PixelPlayer + Open source music player built with its community. + Version v%1$s + Open source + Community-first + Material 3 expressive + No contributors found right now. Please try again later. + Maintainer + The person behind PixelPlayer. + Community spotlight + Recognition for collaborators with major impact. + Open source contributors + Live contributor list from GitHub. + %1$d contrib. + GitHub + Repository + Telegram + Support + Open GitHub repository + Join Telegram community + Open GitHub profile + Open Telegram + Avatar of %1$s + Icon of %1$s + Volume + Pause when volume reaches zero + Automatically pause playback when the volume is set to 0 + diff --git a/app/src/main/res/values-vi/strings_widget.xml b/app/src/main/res/values-vi/strings_widget.xml new file mode 100644 index 0000000000..5dd2eabafc --- /dev/null +++ b/app/src/main/res/values-vi/strings_widget.xml @@ -0,0 +1,17 @@ + + + Responsive widget that adapts to its size + Compact player bar + Full controls with shuffle and repeat + Minimalist square player + + Tap to open + Album art + Album art placeholder + + Tap to play + Song title + Artist + + Progress bar, %1$d percent + \ No newline at end of file diff --git a/app/src/main/res/values-zh-rCN/strings_settings.xml b/app/src/main/res/values-zh-rCN/strings_settings.xml index daf7d4eac8..07496a3129 100644 --- a/app/src/main/res/values-zh-rCN/strings_settings.xml +++ b/app/src/main/res/values-zh-rCN/strings_settings.xml @@ -10,7 +10,9 @@ 行为 手势、触感反馈和导航行为 AI 集成 (β) - AI 提供商、API 密钥和模型设置 + AI providers, API keys, model selection, and activity logs + Generation Parameters + Temperature, sampling, prompts, and song data configuration 备份与恢复 导出和恢复您的个人应用数据 开发者选项 diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml new file mode 100644 index 0000000000..7f38b9115f --- /dev/null +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -0,0 +1,128 @@ + + + PixelPlayer + 音乐播放器 + 應用名稱更改 + 由於商標相關問題,我們將應用名稱從 PixelPlay 更改為 PixelPlayer。請繼續享受音樂! + 不再顯示 + + + 首页 + 搜索 + 音乐库 + + + 需要特殊权限 + 为了编辑歌曲信息(.mp3 文件),PixelPlayer 需要访问所有文件的特殊权限。这将允许我们直接修改曲目标签。请在下一界面授予此权限以开启信息编辑功能。 + 授予权限 + + + 快速播放 + 无法打开该音频文件。 + 打开完整播放器 + + + 随机播放 + 随机播放所有歌曲 + 全部随机播放 + 最近播放列表 + 没有可打开的播放列表 + + + 打开 Play 商店 + 继续使用 Beta 版 + Play 商店链接将通过 GitHub 配置启用。 + PixelPlayer 现已上线 Google Play + 请在 Google Play 上使用稳定版以获取正式更新,同时我们仍会保持 Beta 测试版本的更新。 + PixelPlayer + 发布公告 + 敬请期待 + + + 感谢您使用 PixelPlayer! + 最高分 %1$d + 关闭 + 分数 + 第 %1$d 关 + 生命 + 关卡完成! + 游戏结束 + 分数:%1$d + 再试一次? + 下一关 + 重新开始游戏 + 点击重新启动 + 随机播放音乐 + 打砖块 + 最高分 %1$d + 开始 + 拖动以移动挡板 + + + 关闭播放器 + 正在处理播放操作… + 播放错误:%1$s + + + 返回 + 确定 + 取消 + 关闭 + 错误 + 搜索 + 清除搜索 + 全部 + 确认 + 已保存! + 已选择 + %1$d%% + 艺术家 + 全选 + 清除 + 未知错误 + + + 保存 + 完成 + 重置 + 应用 + 随机播放 + 复制 + 分享 + 撤销 + 导入 + 删除 + 导出 + 合并 + 重命名 + 创建 + 歌词 + 设置 + 专辑封面 + 播放列表 + 未知曲目 + 未知艺术家 + 未知专辑 + 关闭 + 添加 + 移除 + 播放 + 上一首 + 下一首 + 收藏 + 暂停 + 循环播放 + 选项 + 随机播放 + %1$s 的更多选项 + 展开菜单 + 下一步 + 结束 + 恢复默认设置 + 导出全部 + 合并全部 + 分享全部 + 播放专辑 + 随机播放专辑 + %1$s 的专辑封面 + \ No newline at end of file diff --git a/app/src/main/res/values-zh-rTW/strings_changelogs.xml b/app/src/main/res/values-zh-rTW/strings_changelogs.xml new file mode 100644 index 0000000000..b0b82e4a30 --- /dev/null +++ b/app/src/main/res/values-zh-rTW/strings_changelogs.xml @@ -0,0 +1,132 @@ + + + 更新日志 + 在 GitHub 上查看 + 改进 + 修复 + 新功能 + 新增 + + 支持 Chromecast 从您的设备投射音频。 + 应用内更新日志,让您随时了解最新功能。 + 支持嵌入式和外部的 .LRC 歌词文件。 + 支持离线歌词。 + 同步歌词(与歌曲同步)。 + 查看完整队列的新界面。 + 支持在队列中重新排序和移除歌曲。 + 迷你播放器手势(向下滑动关闭)。 + 添加了更多 Material 动画。 + 新增设置项以自定义外观。 + 新增设置项以清除缓存。 + + + 用户界面的全面重新设计。 + 播放器的全面重新设计。 + 媒体库性能改进。 + 提升了应用的启动速度。 + AI 现在能提供更好的结果。 + + + 修复了标签编辑器中的各种错误。 + 修复了播放通知未被清除的错误。 + 修复了导致应用崩溃的几个错误。 + + + 引入了更丰富的听歌统计中心,可深入了解您的播放会话。 + 推出了悬浮快速播放器,可立即打开并预览本地文件。 + 添加了包含树状导航器 and “播放列表就绪”视图的文件夹标签页。 + + + 改进了整体 Material 3 UI,带来更简洁、更连贯的体验。 + 元数据编辑现已支持更改封面。 + 优化了应用内的动画和过渡效果,使导航更加流畅。 + 增强了艺术家界面的布局,细节更丰富且更精美。 + 升级了 DailyMix 和 YourMix 的生成,选择更加智能且多样化。 + 强化了 AI 播放列表的生成能力。 + 改进了搜索相关性和展示方式,以便更快地发现内容。 + 扩大了对更多音频文件格式的支持。 + + + 解决了元数据异常问题,确保各处的歌曲详情保持准确。 + 恢复了通知快捷操作,以便可靠地跳转回播放界面。 + + + 导航栏重大重新设计 + 用于选择源目录的全新文件管理器 + 全新的连接与投屏功能 + 远程设备之间的无缝流转 + 歌曲间无缝过渡 + 淡入淡出控制 + 全新的自定义过渡功能(仅限播放列表) + 关闭应用后继续播放 + UI 优化 + 统计功能改进 + 重新设计并增加了更多功能的队列控制 + 改进了对不同文件格式的播放和元数据编辑支持 + 改进的权限控制器 + 修复了一些小错误 + + + Material 3 Expressive UI 全面升级 + 10 段均衡器与音效 + 全新的媒体库同步流程 + AI 集成(Gemini 模型) + M3U 播放列表导入/导出 + 集成 Deezer 艺术家封面 + 自定义播放列表封面 + 设置架构重构 + 队列与播放器动画 + 基线配置文件 (Baseline Profiles) 与性能优化 + 具有同步偏移调整功能的更佳歌词系统 + + + 投屏稳定性改进 + 播放面板稳定性 + 常规错误修复与代码清理 + + + 现已支持 Android Auto 车载播放。 + 已支持 Wear OS,包括更完善的手表对手机播放控制。 + 扩展了云端集成,改进了对 Telegram、网易云音乐、QQ 音乐和 Google Drive 的支持。 + “最近播放”和持久化队列恢复功能,让您的听歌会话随时就绪。 + 现已包含备份与恢复 v3 以及账号管理工具。 + 歌词功能更加智能,增加了手动搜索备用方案和存储优化。 + + + 大幅优化了启动、媒体库、队列和播放器交互的性能。 + 重新设计了播放器、投屏、歌词、艺术家和流派界面,带来更流畅的体验。 + 导航和搜索流程更加可靠,路由处理更加安全。 + 提升了音频播放在更多设备 and 格式上的兼容性。 + 扩展了歌曲、专辑和播放列表的多选操作流程。 + + + 队列和随机播放行为现已更加稳定且符合预期。 + 修复了几个后台播放和投屏的边缘情况。 + 修复了睡眠定时器、文件标签页导航以及导致专辑艺术家崩溃的问题。 + 改善了小组件加载和服务稳定性,减少了设备发热/内存问题。 + 全局的常规错误修复和界面细节打磨。 + + + Wear OS:支持音乐传输、本地播放、队列同步以及从手表进行远程控制。 + AI:集成了 Groq AI 和 OpenRouter(实验性),并进行了 Token 优化。 + 云端:新增了 Jellyfin 支持。 + 歌词:带有专属开关的同步翻译,支持酷狗 LRC 格式,自定义文本对齐方式,以及改进了远程加载。 + UI/UX:紧凑的导航栏模式,从专辑封面提取调色板的动态主题,长标题跑马灯效果,以及全新的排序选项。 + Telegram:原生话题 (Topics) 支持和增强的显示模式。 + + + 音频引擎:全面重构,支持更多格式(MIDI、ALAC、M4A)并进行了解码器优化。 + 效率:大幅降低能耗,修复设备发热问题,以及后台任务优化 (SyncWorker)。 + 数据库:大规模的查询优化,并重新设计了封面缓存以防止数据丢失。 + 启动:通过基线配置文件 (Baseline Profile) 优化了加载时间。 + + + 播放:修复了 Opus/MP3 的卡顿、淡入淡出期间的 ReplayGain 错误,以及三星解码器上的启动问题。 + 稳定性:消除了启动、艺术家导航和 Android 12+ 设备上的崩溃问题。 + UI:修复了封面闪烁、非拉丁语系文本溢出,以及导航栏/迷你播放器的行为问题。 + 安全性:强化了凭证处理、存储权限以及与媒体服务器的通信安全性。 + + + 本地化:西班牙语、法语、俄语、简体中文、印度尼西亚语、意大利语 + + \ No newline at end of file diff --git a/app/src/main/res/values-zh-rTW/strings_cloud_services.xml b/app/src/main/res/values-zh-rTW/strings_cloud_services.xml new file mode 100644 index 0000000000..13dd260f70 --- /dev/null +++ b/app/src/main/res/values-zh-rTW/strings_cloud_services.xml @@ -0,0 +1,228 @@ + + + + 登录 Telegram + 您正在修改号码。再次发送验证码将替换上一个。 + 处理中… + 正在初始化 Telegram… + 正在登出… + 正在关闭会话… + 会话已关闭。请重新打开登录界面以继续。 + 正在准备安全的 Telegram 会话… + 正在等待 Telegram 响应… + 连接 Telegram + 连接 Telegram 即可流式播放您频道和聊天中的音乐。 + 手机号码 + 请输入您的 Telegram 手机号码。您稍后可以返回并进行修改。 + 手机号码 + 86 + 13800138000 + 发送验证码 + 验证码 + 请输入来自 Telegram 的验证码。如果号码有误,请返回并修改。 + 验证码 + 12345 + 修改手机号 + 重新发送验证码 + 验证验证码 + 两步验证密码 + 请输入您的 Telegram 密码。您仍然可以返回以修改手机号码。 + 密码 + 验证密码 + 请稍候… + + + Telegram 频道 + 添加频道 + 公开的 Telegram 频道 + 正在同步 + 立即同步 + 收起话题 + 显示话题 + 频道选项 + 话题 + 正在同步频道 + 正在从 Telegram 更新歌曲 + 从此频道获取最新歌曲 + 移除频道 + 停止同步并移除已缓存的歌曲 + 是否移除频道? + %1$s 将停止同步,并且从此频道缓存的所有歌曲都将被移除。 + 移除 + 未同步任何频道 + 添加公开的 Telegram 频道\n以同步您的音乐库 + 添加频道 + 从未同步 + 上次同步于 %1$s + + + 添加频道 + 搜索一个公开的 Telegram 频道以同步其音乐 + \@channelname 或链接 + 正在搜索… + 搜索频道 + 输入公开频道的用户名或链接\n以同步其音频文件 + + + %d 首歌曲 + %d 首歌曲 + + + %d 个话题 + %d 个话题 + + + + Subsonic + 管理 Navidrome、Airsonic 以及其他兼容 Subsonic 的服务器。 + + + 点击同步以获取您的 Jellyfin 播放列表 + 管理您的 Jellyfin 服务器连接。 + + + 音乐文件夹 + 点击 + 添加 Drive 文件夹 + 暂未添加文件夹 + 已同步 %1$d 个文件夹 + 添加文件夹 + + + 选择播放列表类型 + 选择要同步的播放列表: + 所有播放列表 + 自建与收藏 + 自建播放列表 + 收藏播放列表 + + + 已同步 %1$d 个播放列表 + 播放列表 + 同步 + 暂未同步任何播放列表 + 点击同步以获取您的播放列表 + 快捷操作 + 同步媒体库 + 断开连接 + %1$d 首歌曲 + + + 正在同步 + 正在同步媒体库… + 正在获取播放列表… + 正在同步播放列表:%1$s + 正在更新本地媒体库… + 同步完成 + 正在获取专辑列表… + 正在从 %1$s 获取歌曲… + 正在将 %1$d 首歌曲保存到数据库… + 未找到媒体库歌曲 + 媒体库同步完成 + 正在同步… + 错误:%1$s + + + 同步 + 全部同步 + 登出 + 同步所有播放列表 + 用户头像 + + + 无网络连接 + 此内容需要网络连接。请检查您的网络设置并重试。 + 您已离线 + 请检查您的网络连接并重试以访问此内容。 + + + 连接 + 正在连接… + 输入您的服务器地址和账户凭据。 + 连接详情 + 隐藏密码 + 密码 + 输入密码 + 预填 http:// + 服务器 URL + 显示密码 + Telegram + 用户名 + admin + 欢迎您,%1$s! + + + 兼容 Navidrome、Gonic、Airsonic 以及其他兼容 Subsonic 的服务器 + 支持 Navidrome、Airsonic、Gonic、Ampache 以及其他兼容 Subsonic API 的服务器。 + 如果您的服务器支持,应用密码同样有效。 + 预填 https:// + 连接到您的自托管音乐服务器 + Navidrome + 请使用您服务器的完整 https:// 基础地址。 + https://music.example.com + 这是您的 Subsonic 或 Navidrome 账户名称。 + Subsonic / Navidrome + Subsonic + + + 输入您的 Jellyfin 服务器 URL 和账户凭据。 + 连接到 Jellyfin 服务器以串流播放您的媒体库 + 连接到 Jellyfin 服务器。支持使用 HTTP 和 HTTPS 进行本地网络访问。 + Jellyfin + 您的 Jellyfin 账户密码。 + 连接到您的 Jellyfin 媒体服务器 + Jellyfin + 您的 Jellyfin 服务器完整 URL,包含端口。 + http://192.168.1.100:8096 + 您的 Jellyfin 账户用户名。 + + + 直接从您的 Google Drive 串流音乐文件 + 连接 Google Drive + Google Drive 已连接! + 创建 \"PixelPlayer Music\" + 在这里为您的音乐创建一个新文件夹 + 这里没有文件夹 + 打开文件夹 + 选择或创建一个文件夹作为您的音乐源 + 选择一个音乐文件夹 + 正在设置 Google Drive… + 使用 Google 账号登录 + Google Drive + 使用 + + + 无法读取会话 Cookie。 + 完成 + 退出 + 未找到 Cookie。请先登录。 + 页面加载时间过长。请刷新或尝试其他网络。 + + + 正在保存… + 停留 + 页面加载超时。您可以重试,进度不会丢失。 + 网页后退 + 您可以稍后返回。关闭时将丢弃当前页面状态。 + 网页前进 + 刷新 + 重试 + 打开主页 + WebView 加载失败。 + + + 读取网易云音乐 Cookie 失败:%1$s + 退出网易云音乐登录? + 加载网易云音乐时出现 HTTP %1$d。 + 尚未检测到登录。请在点击完成前完成网易云音乐的登录。 + 登录网易云音乐 + 安全提示:您的密码仅在网易云音乐网页中输入。PixelPlayer 仅存储会话 Cookie (MUSIC_U) 以同步您的媒体库。 + 网易云音乐 + + + 读取 QQ 音乐 Cookie 失败:%1$s + 退出 QQ 音乐登录? + 加载 QQ 音乐时出现 HTTP %1$d。 + 尚未检测到登录。请在点击完成前完成 QQ 音乐的登录。 + 登录 QQ 音乐 + 安全提示:您的密码仅在 QQ 音乐网页中输入。PixelPlayer 仅存储会话 Cookie 以同步您的媒体库。 + QQ 音乐 + \ No newline at end of file diff --git a/app/src/main/res/values-zh-rTW/strings_equalizer.xml b/app/src/main/res/values-zh-rTW/strings_equalizer.xml new file mode 100644 index 0000000000..f1e662c938 --- /dev/null +++ b/app/src/main/res/values-zh-rTW/strings_equalizer.xml @@ -0,0 +1,57 @@ + + + + 名称不能为空 + 重命名 + + + 更改视图模式 + 禁用均衡器 + 启用均衡器 + 编辑 + 编辑预设 + 自定义预设 + 预设 + 更新 + 低音增强 + 虚拟音效 + 响度 + 不支持 + 此设备不支持 + 音量 + 频率响应 + Hz + 低音 + 中低音 + 中高音 + 高音 + 低音 / 低频 + 中音 / 高频 + 第 %1$d 页 + 重置持续时间 + 保存为新预设 + + + 已保存的预设 + 暂未保存自定义预设。 + 取消固定 + 固定 + 重命名 + 删除 + + + 保存自定义预设 + 为您的自定义均衡器预设输入一个名称。 + 预设名称 + 重命名预设 + + + 管理预设 + 拖动以重新排序 • 点击眼睛图标显示或隐藏 + 重新排序 + 重置预设 + 这将恢复默认的预设顺序和可见性。是否继续? + 恢复默认 + 可见 + 隐藏 + diff --git a/app/src/main/res/values-zh-rTW/strings_home_screen.xml b/app/src/main/res/values-zh-rTW/strings_home_screen.xml new file mode 100644 index 0000000000..46a70f8063 --- /dev/null +++ b/app/src/main/res/values-zh-rTW/strings_home_screen.xml @@ -0,0 +1,277 @@ + + + + β + Beta + 云端串流 + 更新日志 + 云端串流 + 从您的云端账户串流音乐 + + + Beta 0.7.5 + β + 欢迎使用 PixelPlayer 0.7.5-beta + 您正在使用的是可能包含错误、崩溃或实验性功能的测试版本。请报告问题以帮助我们改进。 + 预期情况 + 可能会意外出现错误、崩溃或未完成的功能。 + 某些功能可能会在不另行通知的情况下更改或移除。 + 测试版可能比正式发布版本更不稳定。 + 在报告已知问题之前,请务必先检查更新。 + 测试版在测试期间可能会更改、损坏或改进的内容。 + GitHub Issue 快捷入口 + 请先搜索,然后针对错误、崩溃、请求或问题开启一个明确的报告。 + 浏览现有的 Issue + 报告问题或崩溃 + 分享复现步骤、预期结果、实际结果以及您的设备/操作系统详细信息。 + 如何报告 + 开启新 Issue 前的快速检查清单。 + 开启 Issue 前 + 搜索现有已开启和已关闭的 Issue,以避免重复提交。 + 更新至 PixelPlayer 最新版本并确认问题是否仍然存在。 + 重启应用并确认问题是否仍然存在。 + 尝试复现它并写下确切的步骤。 + 哪种问题类型? + 错误报告:某些功能表现异常。 + 功能请求:添加新功能或改进。 + 提问:如果已启用,请使用 Discussions(讨论),或开启一个带有问题标签的 Issue。 + 错误报告 + 当某些功能表现异常或崩溃时,请复制这些字段。 + 错误报告 + 简要概述: + 预期行为: + 当前行为: + 播放/复现步骤:1. 2. 3. + 发生的频率如何?总是 / 有时 / 很少。 + 截图/视频:如果有。 + 日志/堆栈跟踪:如果有。 + 环境 + PixelPlayer 版本: + 安装来源:GitHub Release、debug 构建、Nightly 构建等。 + Android 版本: + 设备型号: + 额外信息:SD 卡使用情况、特殊设置、权限等。 + 功能请求 + 当您希望添加新功能或改进时,请复制这些字段。 + 问题描述:您想解决什么问题? + 建议方案:它应该如何工作? + 考虑过的替代方案:是否有其他方法? + 影响范围:哪些界面或流程会受影响? + 样图或参考图片(如果有)。 + 标题、隐私和范围 + 使报告易于分类且安全共享。 + 好的 Issue 标题示例 + 均衡器:切换预设选项卡时指示器偏移 + 搜索:查询为空时历史记录列表不显示 + 功能:添加“最近添加”播放列表排序选项 + 请避免 + 诸如“它不起作用”之类的通用报告。 + 在一个 Issue 中包含多个不相关的问题。 + 包含私人数据的未经打码处理的日志或截图。 + 隐私声明 + 在发布日志、截图或视频之前,请删除个人或私人信息。 + Nightly 构建版本 + Nightly 版本与正式版有何不同,以及出错时应提供哪些信息。 + Nightly 版本是从最新提交 (commit) 生成的,可能包含未完成的更改、临时错误或回归。它们比正式发布版本更具实验性。 + 如果可用,可从仓库的 GitHub Actions 工作流产物 (artifacts) 中获取。 + 报告 Nightly 问题 + 在报告 Nightly 版本的问题时,请务必说明该问题发生于 Nightly 版本,而非正式发布版本。请尽可能提供构建日期、工作流运行名称或编号,或者提交 SHA。并请检查最新的正式版本中是否会出现相同问题。 + Beta 0.5.0 升级 + 建议全新安装 + 如果您从 Beta 0.5.0 升级,此更新可能需要全新的媒体库数据,而不是旧的缓存状态。 + 如果元数据或媒体库条目看起来有误 + 歌曲元数据错误、艺术家或专辑不匹配,或出现类似重复的条目,通常意味着只需全新安装即可解决。 + 不再显示 + 知道了 + + + 糟糕!出错了 + 应用在您上次使用时崩溃了。请分享崩溃报告以帮助我们修复此问题。 + 日期:%1$s + 错误: + 堆栈跟踪(预览): + 崩溃日志 + 崩溃日志已复制到剪贴板 + PixelPlayer 崩溃报告 + 分享崩溃报告 + + + DJ 混音 + + + 你的\n推荐 + 暂无数据 + 当 PixelPlayer 发现歌曲或同步数据源时,您的推荐将显示在此处。 + 刷新 + + + DAILY MIX + 基于听歌历史 + 查看全部 Daily Mix + Daily Mix + + + Daily Mix + + %1$d 首歌曲 • %2$s + %1$d 首歌曲 • %2$s + + 播放 + AI 播放列表生成器 + + + 您的 Daily Mix 是如何生成的 + 您的 Daily Mix 由您最喜爱和播放次数最多的歌曲组成。我们还会添加您喜欢的艺术家和流派的曲目,让您发现新音乐。 + 告诉 AI 您今天想听什么 + 我们使用小样本以保持较低的成本 + 正在更新… + 更新 Daily Mix + + + 完美精选 + Daily Mix + 您的声音之旅已就绪 + AI 播放列表生成器 + 描述您的氛围、心情或活动,让 AI 从您的媒体库中精心挑选完美的播放列表。 + 播放列表大小 + 最少歌曲数 + 最多歌曲数 + 例如:轻松的夜晚氛围,欢快的健身音乐… + 点击重试 + 声音之旅合成完毕! + 准备播放 + 正在生成… + 生成播放列表 + + + 最近播放 + + + 最近播放 + 播放最新 + 在 %1$s 内无最近播放记录 + 更改时间范围或播放更多歌曲以填充此时间轴。 + 最近播放 + 今天 + 昨天 + + + 听歌统计 + 总播放量 + 日均播放 + 最爱曲目 + %1$s • %2$d 次播放 + + + 听歌统计 + 刷新听歌统计 + 今天 + 本周至今 + 本月至今 + 本年至今 + 全部时间 + 听歌时长 + 播放次数 + 听歌时间轴 + 听歌时长 + 所选时间范围内的总听歌时长。 + 播放次数 + 您在每个时间段内完成的会话次数。 + 平均会话 + 每个时间段的平均听歌时长。 + 划分为 4 小时的时间段,揭示您的日常节奏。 + 每日柱状图,方便比较每周习惯。 + 每周柱状图,显示本月趋势。 + 每月柱状图,显示全年的季节性变化。 + 每年柱状图,总结您的完整听歌历史。 + 暂无听歌数据 + 点击播放开始建立您的听歌时间轴 + 每日节奏 + 每周节奏 + 每月节奏 + 全年概览 + 历年进展 + 按 4 小时分段分组 + 按星期几分组 + 按月内周次分组 + 按月分组 + 按年份分组 + 高峰时段 + %1$d 次播放 + + 热门类别 + 比较您在各个流派、艺术家、专辑和歌曲中的听歌偏好。 + 流派 + 艺术家 + 专辑 + 歌曲 + 按流派统计 + 按艺术家统计 + 按专辑统计 + 按歌曲统计 + %1$d 次播放 • %2$d 位艺术家 + %1$d 次播放 • %2$d 首曲目 + 暂无类别数据 + 点击播放,您的听歌亮点将在这里浮现 + 听歌习惯 + 暂无习惯数据 + 当我们更了解您时,就会在这里展示您的听歌习惯。 + 总会话数 + 平均会话时长 + 最长会话 + 会话数/天 + 最活跃的一天 + 暂无播放 + 时间轴最高峰 + 热门艺术家 + 暂无热门艺术家 + 继续听歌,您最喜爱的艺术家将出现在这里。 + \? + %1$d. %2$s + 热门专辑 + 暂无热门专辑 + 您经常回顾的专辑将出现在这里。 + %1$d. %2$s + 曲目集中度 + 您的听歌时间在最爱曲目中的分布情况。 + 暂无集中度数据 + 播放更多曲目即可了解您听歌的专一程度。 + Top 1 + Top 2-3 + 其他 + %1$d%% + 听歌集中度 + 排名前 3 的曲目占据了您 %1$d%% 的听歌时间。 + 平均播放/曲目 + 去重曲目数 + Top 3 占比 + 此范围内的曲目 + 选定时间范围内播放次数最多的曲目。 + 暂无热门曲目 + 听您喜欢的歌,它们将会在这里高亮显示。 + 收起曲目 + 显示所有曲目 + + + %1$d 小时 %2$02d 分钟 + %1$d 分钟 + %1$d 小时 %2$02d 分 + %1$d 小时 + %1$d 分 + %1$d 秒 + %1$d小时%2$02d分 + %1$d小时 + %1$d分 + %1$d秒 + 从未 + 刚刚 + 1 天前 + %1$d 天前 + 1 小时前 + %1$d 小时前 + 1 分钟前 + %1$d 分钟前 + %1$d 首歌曲 + %1$d 首歌曲 + 第 %1$d 周 + diff --git a/app/src/main/res/values-zh-rTW/strings_library.xml b/app/src/main/res/values-zh-rTW/strings_library.xml new file mode 100644 index 0000000000..fbe7fe995e --- /dev/null +++ b/app/src/main/res/values-zh-rTW/strings_library.xml @@ -0,0 +1,566 @@ + + + + 音乐库 + 媒体库选项卡 + 直接跳转到任意选项卡或对它们进行重新排序。 + 重新排序选项卡 + + + 歌曲 + 专辑 + 艺术家 + 播放列表 + 文件夹 + 已收藏 + + + 播放列表创建成功 + 请先设置您的 AI 提供商 API 密钥 + 请先设置您的 Gemini API 密钥 + 已添加到队列 + 下一首播放 + + + 手表传输 + 设置 + 编辑 + 重新排序选项卡 + 展开菜单 + + + 您最多可以选择 %1$d 张专辑 + 文件夹 + 文件夹 + + + 排序方式 + 视图 + 播放列表视图 + 网格 + 列表 + 内部存储 + SD 卡 + SD 卡目前不可用。 + 云端 + Telegram 云端频道 + 话题显示 (Topics) + 频道 + 话题 + 全部 + 云端 + 仅云端 + + + 正在使用 AI 生成元数据… + + + 加载歌曲时出错 + 加载专辑时出错 + 加载艺术家时出错 + 重试 + + + 在您的媒体库中未找到歌曲。 + 如果您设备上有音乐,请尝试在设置中重新扫描媒体库。 + 未找到歌曲 + + + 新建 + 创建新播放列表 + 导入 M3U 播放列表 + 定位当前歌曲 + 所有歌曲 + 云端 + 本地 + 排序选项 + + + 全选 + 取消选择 + 更多选项 + + + 正在扫描音乐文件… + 正在处理文件… + 第 %1$d 个,共 %2$d 个文件 + 正在同步媒体库… + 同步完成 + 等待中… + 正在同步媒体库… + 正在清理专辑封面缓存… + 正在同步云端数据源… + 正在扫描歌词… + + + 暂无歌曲 + 将音乐添加到您的设备或同步云端数据源以开始收听。 + 未找到本地歌曲 + 尝试其他数据源过滤或重新扫描您的设备媒体库。 + 未找到云端歌曲 + 同步 Telegram 或网易云音乐歌曲,或切换到本地源。 + 暂无专辑 + 一旦您的媒体库对曲目进行分组,专辑就会显示在这里。 + 未找到本地专辑 + 需要本地歌曲才能构建本地专辑组。 + 未找到云端专辑 + 同步后,带有专辑数据的云端歌曲将显示在这里。 + 暂无艺术家 + 从任何来源为歌曲建立索引后,将显示艺术家。 + 未找到本地艺术家 + 目前没有本地歌曲的艺术家元数据可用。 + 未找到云端艺术家 + 同步远程歌曲时会显示云端艺术家条目。 + 暂无收藏歌曲 + 在播放歌曲时点击心形图标将其保存在这里。 + 无本地收藏歌曲 + 切换数据源过滤或收藏您设备中的歌曲。 + 无云端收藏歌曲 + 收藏 Telegram 或网易云音乐的曲目,即可在此视图中看到它们。 + 未找到文件夹 + 包含音乐的内部存储文件夹将显示在这里。 + 暂无播放列表 + 创建您的第一个播放列表来整理您的媒体库。 + + + 编辑歌曲元数据 + 播放 + 播放歌曲 + 全部播放 + 全部播放 + 添加到收藏 + 全部添加到收藏 + 取消收藏 + 全部取消收藏 + 分享歌曲文件 + 分享歌曲文件 + 作为 ZIP 全部打包分享 + 无法分享歌曲:%1$s + 添加到队列 + 添加到队列 + 下一首播放 + 在队列中下一首播放 + 添加到播放列表 + 删除 + 全部删除 + 正在检查手表 + 正在传输 %1$d%% + 正在传至手表 + 传输进行中 + 发送至手表 + 手表不可用 + 将歌曲发送至手表 + 手表不可用 + 设为 + 设为声音 + 选择如何将此歌曲用作系统声音 + 将此歌曲用作 + 选择 PixelPlayer 应在哪里安装此声音。 + 手机铃声 + 来电 + 通知音 + 消息和应用提醒 + 闹钟音 + 时钟闹钟 + 确认更改声音 + 将“%1$s”设为您的 %2$s? + 设置声音 + 已将“%1$s”设为您的 %2$s + 铃声 + 通知音 + 闹钟音 + 启用“修改系统设置”,然后返回 PixelPlayer 自动完成。 + 未启用“修改系统设置”。 + 已将“%1$s”设为您的铃声 + 只有本地歌曲才能设为铃声。 + 无法将此音频文件准备为铃声。 + 无法设置铃声:%1$s + 选项 + 选项 + 信息 + 信息 + 时长 + 流派 + 专辑 + 艺术家 + 歌曲信息 + 提供商 + 文件 + %1$d 首歌曲 + 已选择 + %1$d 个播放列表 + %1$d 张专辑 + 已选择 + 限制:每次选择最多 %1$d 张专辑。 + 加入队列及播放将遵循您的选择顺序。 + %1$d 个流派 + 已选择 + 对这些流派中的所有歌曲进行批量操作。 + + + 默认顺序 + 标题 (A-Z) + 标题 (Z-A) + 艺术家 + 艺术家 (Z-A) + 专辑 + 专辑 (Z-A) + 添加时间 + 添加时间 (最早优先) + 时长 + 时长 (最短优先) + 发行年份 + 发行年份 (最早优先) + 歌曲最少 + 歌曲最多 + 名称 (A-Z) + 名称 (Z-A) + 歌曲数量 (最多) + 歌曲数量 (最少) + 创建时间 + 创建时间 (最早优先) + 收藏时间 + 收藏时间 (最早优先) + 子文件夹最少 + 子文件夹最多 + + + 标题 + 艺术家 + 专辑 + 添加时间 + 时长 + 发行年份 + 歌曲数量 + 名称 + 歌曲数量 + 创建时间 + 收藏时间 + 子文件夹数量 + + + 来源 + 顺序 + 降序 + 升序 + 原始顺序 + 点击切换为升序 + 点击切换为降序 + 此排序保持其原始顺序 + 开关已开启 + + + 重新排序媒体库选项卡 + 重置顺序 + 将选项卡顺序重置为默认? + 正在重新排序选项卡… + 拖动手柄 + + + 选择一位艺术家 + 1 位艺术家 + %1$d 位艺术家 + 主要艺术家 + 艺术家主页 + + + 取消传输 + %1$s / %2$s + 显示手机到手表音乐传输的实时进度 + 手表传输 + 正在发送至手表 + 已取消 + 传输已取消 + 传输完成 + 已完成 + 失败 + 传输失败 + 有多个活动中的传输 + %1$s • %2$s + 正在准备 + 正在准备手表传输 + 正在准备传输… + 正在将 %1$d 首歌曲发送至手表 + 正在发送至手表 + 正在开始传输… + 正在开始 + 正在传输 + %1$d 个传输任务 + + + 编辑歌曲 + 显示信息 + 正在编辑歌曲元数据 + 编辑歌曲元数据可能会影响其在媒体库中的显示和整理方式。更改是永久性的,且可能无法撤销。 + 知道了 + 信息 + 封面 + 选择一张正方形图片并进行微调,让您的封面在整个应用中看起来更棒。 + 更改封面 + 删除封面 + 标题 + 艺术家 + 专辑 + 专辑艺术家 + 流派 + 作曲 + 曲目编号 + 唱片编号 + 音量标准化-曲目增益 (dB) + 音量标准化-专辑增益 (dB) + -6.50 + -8.20 + 新封面预览 + 当前歌曲封面 + 调整您的封面 + 使用双指缩放和拖动手势找到完美的构图。 + 应用封面 + 无法加载选定的图片 + 在 lrclib.net 搜索歌词 + + + 批量编辑 %d 首歌曲 + 仅会更新已修改的字段。将字段留空则保持现有值。 + (混合值) + (可选 - 留空则跳过) + 成功更新 %d 首歌曲 + 已更新 %1$d 首(共 %2$d 首)歌曲。某些文件无法编辑。 + 更新歌曲失败 + 批量更换封面 + 这将替换所有 %d 首选定歌曲的封面 + 为所有选定歌曲设置封面 + 移除所有封面 + (多个不同的封面) + + + 已忽略播放列表 + + + 创建播放列表 + 选择创建方式。 + 手动 + 自己设计封面、图标、形状并挑选歌曲。 + 使用 AI + 使用高级控制生成精选的播放列表。 + 需要在设置中配置 Gemini API 密钥。 + 设置 API 密钥 + + + AI 播放列表实验室 + 重置 + 正在生成… + 生成 + 意图 + 播放列表名称(可选) + 这个播放列表应该带来什么感觉? + 示例:伴随温暖合成器音乐的日落驾驶 + 方向 + 情绪 + 活动 + 时代 + 精选引擎 + 能量 + 控制歌曲的强度和节奏。1 = 平静/缓慢,5 = 高能量/快节奏。 + 探索度 + 控制所选歌曲的熟悉程度。1 = 你最常听的最爱,5 = 很少播放的冷门曲目。 + 最少歌曲数 + 最多歌曲数 + 过滤 + 优先考虑的流派(可选) + 例如:合成器波 (synthwave)、独立流行 (indie pop) + 避免的流派(可选) + 例如:金属 (metal)、硬核陷阱 (hard trap) + 首选语言(可选) + 例如:英语,西班牙语,纯音乐 (instrumental) + 优先考虑已收藏的最爱 + 避免露骨歌词 (Explicit) + 提示词预览 + 当您添加偏好设置后,最终的提示词将显示在这里。 + 精准策展 + 定义情绪、活动、约束和深度。 + AI 将仅使用您本地媒体库中的歌曲。 + 至少为 AI 添加一项指令。 + 请设置有效的歌曲数量范围。 + %1$d/5 + 自定义… + 输入自定义值 + 输入您的自定义值 + + + 任何时代 + 核心请求:%1$s。 + 情绪目标:%1$s。 + 活动背景:%1$s。 + 时代焦点:%1$s。 + 优先考虑的流派:%1$s。 + 避免的流派:%1$s。 + 首选语言:%1$s。 + 能量等级目标:%1$d/5。 + 探索度目标:%1$d/5(1 代表熟悉,5 代表冷门曲目)。 + 可能的话,优先选择更接近听众最爱的歌曲。 + 只要有替代选择,尽量避免露骨歌词。 + 保持平滑的过渡,避免重复聚集同一艺术家。 + + 放松 + 活力 + 快乐 + 暗黑 + 浪漫 + 忧郁 + + + 健身 + 专注 + 公路旅行 + 派对 + 学习 + 深夜 + + + @string/playlist_creation_ai_era_any + 70年代 + 80年代 + 90年代 + 2000年代 + 2010年代 + 2020年代 + + + + 尚未创建播放列表。 + 点击“新建播放列表”按钮开始。 + 新建播放列表 + 播放列表名称 + 我的播放列表 + + + 添加 %1$d 首歌曲到… + 选择播放列表 + 搜索播放列表… + 已将歌曲添加到播放列表 + 播放列表已创建并已添加歌曲 + 内部存储 + + + 添加歌曲 + 添加所选歌曲 + 添加 + 搜索或过滤歌曲… + 已收藏 + 加载歌曲失败 + 加载更多 + + + 合并播放列表 + 为合并后的播放列表输入名称: + 合并的播放列表 + 这将把 %1$d 个选定的播放列表合并为一个。 + + + 未找到可播放的有效歌曲 + 在当前列表中未找到歌曲 + 无法定位歌曲 + 在媒体库中未找到歌曲 + 播放停止:%1$s 已结束(曲目结束)。 + 曲目 + 没有可供随机播放的歌曲。 + 选定的专辑 + 在选定的专辑中未找到可播放的歌曲 + 在选定的流派中未找到可播放的歌曲 + 仅前 %1$d 张专辑已加入队列 + 已将 %1$d 张专辑加入队列(共 %2$d 首歌曲) + 无法将选定的专辑加入队列 + 所有歌曲均已在收藏中 + 没有被收藏的歌曲 + 正在创建 ZIP 文件… + 分享失败:%1$s + + 添加了 %d 首歌曲到队列 + 添加了 %d 首歌曲到队列 + + + %d 首歌曲将作为下一首播放 + %d 首歌曲将作为下一首播放 + + + 添加了 %d 首歌曲到收藏 + 添加了 %d 首歌曲到收藏 + + + 从收藏中移除了 %d 首歌曲 + 从收藏中移除了 %d 首歌曲 + + + + 没有可分享的播放列表 + 分享播放列表 + 分享失败:%1$s + 没有可导出的播放列表 + 导出失败:%1$s + Music/PixelPlayer Exports + 请在设置中配置您的 Gemini API 密钥。 + 播放列表已恢复 + + 正在分享 %d 个播放列表 + 正在分享 %d 个播放列表 + + + 已导出 %1$d 个播放列表到 %2$s + 已导出 %1$d 个播放列表到 %2$s + + + + 无效的专辑 ID + 未找到专辑 ID + 加载专辑数据出错:%s + 未找到专辑 + + + 无效的艺术家 ID + 未找到艺术家 ID + 加载艺术家数据出错:%s + 找不到该艺术家 + + + 无法删除当前正在播放的歌曲 + 删除了 %1$d 个文件(跳过了 %2$d 个 - 正在播放) + 删除了 %1$d 个(共 %2$d 个)文件 + 删除文件失败 + 文件已删除 + 无法删除文件或找不到文件 + 删除已取消 + 删除歌曲? + “%1$s” - %2$s\n\n此歌曲将从您的设备中永久删除,且无法恢复。 + 这些歌曲将从您的设备中永久删除,且无法恢复。 + + 删除了 %d 个文件 + 删除了 %d 个文件 + + + 删除 %d 首歌曲? + 删除 %d 首歌曲? + + + + 元数据更新成功 + 正在更新 %1$d 首歌曲… + 已成功更新 %1$d 首歌曲! + 更新了 %1$d 首歌曲。失败:%2$d 首 + 歌词保存成功 + 保存歌词失败 + 没有可保存的歌词 + 权限被拒绝 – 无法编辑文件 + 权限被拒绝 – 无法保存歌词 + 权限被拒绝 – 无法编辑此文件 + + + 请在设置中为选定的 AI 提供商配置有效的 API 密钥。 + AI 错误:%s + 所选 AI 提供商拒绝了请求,原因是账户没有积分或可用配额不足。 + 所选 AI 模型不再可用。PixelPlayer 尝试自动切换到支持的模型。 + AI 无法根据您的提示找到任何歌曲。 + 写下一个 Daily Mix 的灵感 + 已使用 AI 更新 Daily Mix + 无法更新:%s + AI 无法为此合集找到歌曲 + \ No newline at end of file diff --git a/app/src/main/res/values-zh-rTW/strings_player.xml b/app/src/main/res/values-zh-rTW/strings_player.xml new file mode 100644 index 0000000000..73811e2fb6 --- /dev/null +++ b/app/src/main/res/values-zh-rTW/strings_player.xml @@ -0,0 +1,198 @@ + + + + 收起播放器 + 正在播放 + 云端串流 + 投屏 + 蓝牙 + 本地播放 + 正在连接… + 打开队列 + + + 准备连接 + 允许 PixelPlayer 查看您附近的设备和当前 Wi-Fi,以便我们保持您的投屏、蓝牙音频和扬声器同步。 + 附近设备 + 需要此权限来读取和控制您连接的蓝牙音频设备。 + 用于 Wi-Fi 的位置信息 + Android 需要位置信息才能共享您当前所连接的 Wi-Fi 网络 (SSID),以便我们找到兼容的投屏设备。 + 允许访问 + 我们仅将这些权限用于设备互连——投屏、控制附近扬声器以及保持音频同步。 + 连接设备 + 正在扫描附近 + 投屏会话 + 正在连接 + 已连接 + 本机 + 蓝牙音频 + 本地播放 + 正在播放 + 已暂停 + 设备音量 + 手机音量 + %1$d/%2$d + 电量 + 音量 + 断开连接 + 连接性 + 开启 Wi-Fi 或蓝牙 + 刷新连接 + Wi-Fi + + + 已连接 + 蓝牙 + + + 已连接 + 附近设备 + 刷新设备 + 已连接 + 正在连接 + 可连接 + 可用 + 正在连接... + 正在搜索设备… + 请确保您的电视或扬声器已开启,并连接到同一 Wi-Fi 网络。 + 控制 + 设备 + + + 投屏媒体服务器 + 正在投屏至设备 + 正在向投屏设备提供媒体 + %1$s: %2$s + 投屏时暂不支持对此音频格式进行进度调节,这可能会导致投屏会话崩溃。 + + + 睡眠定时器 + 定时器 + %1$d 分钟 + 定时器已设为 %1$d 分钟。 + 1 次 + + %d 次 + %d 次 + + 播放次数:%1$s + 当前曲目结束时 + 播放将在曲目结束时停止。 + 开启 + 自定义时间 + 取消定时器 + 曲目结束 + 定时器已取消。 + 无法启用“曲目结束时停止”:没有正在播放的歌曲。 + “曲目结束”定时器已停用:歌曲已从 %1$s 切换到 %2$s。 + 上一首曲目 + 当前曲目 + 设置自定义时长 + + + 接下来播放 + 队列当前为空。 + + 1 首曲目在列。 + %d 首曲目在列。 + + 队列 + 队列为空。 + 重新排序歌曲 + 切换随机播放 + 切换循环播放 + 睡眠定时器 + 更多操作 + 定位当前歌曲 + 清空队列 + 清空队列 + 您确定要清空队列中除当前正在播放之外的所有歌曲吗? + 保存为播放列表 + %1$s 队列 + 当前队列 + 移除歌曲 + 已移除 + 保存为播放列表 + 取消全选 + 播放列表名称 + 搜索要包含的歌曲… + 没有与 \"%1$s\" 匹配的歌曲 + + 已选择 %d 首歌曲 + 已选择 %d 首歌曲 + + 保存为:%1$s + 输入播放列表名称 + 从播放列表中移除 + %1$s 的更多选项 + + + 歌词 + 正在加载歌词… + 逐字同步 + 静态 + 歌词选项 + −.5 + −.1 + +.1 + +.5 + 0s + %1$+.1fs + + + 搜索歌词失败 + 无法从远程获取歌词 + 连接超时。请检查您的网络连接。 + 网络错误。请检查您的网络连接。 + 服务器错误(代码 %d)。请稍后再试。 + + + 歌词已可用,跳过在线获取。 + 已找到内嵌歌词,跳过在线获取。 + 已找到本地 (.lrc) 歌词,跳过在线获取。 + + + 保存歌词 + 使用 AI 翻译 + 此歌词已有翻译 + 此歌词已经是该语言 + 未配置 API + 歌词翻译成功! + 正在翻译歌词... + 重置已导入的歌词 + 重置歌词? + 您确定要重置此歌曲的歌词吗? + 外观 + 对齐方式 + 歌词左对齐 + 歌词居中 + 歌词右对齐 + 控制 + 调整同步 + 隐藏同步控制 + 显示罗马音 + 显示翻译 + 禁用沉浸模式(一次) + 保持屏幕常亮 + + + 保存歌词 + 选择要保存的版本: + 同步版(带时间戳) + 纯文本(仅文本) + + + 您想在网上搜索歌词吗? + 显示歌词选项 + 始终打开选择器,而不是自动应用第一个匹配项 + 正在搜索歌词… + 未找到歌词 + 我们无法自动找到歌词。您可以编辑标题或艺术家名称并尝试手动搜索。 + 歌曲标题 + 艺术家(可选) + 找到 %d 个匹配项 + 已同步 + %1$s • %2$s + 歌词提供者 + https://lrclib.net/ + diff --git a/app/src/main/res/values-zh-rTW/strings_screens.xml b/app/src/main/res/values-zh-rTW/strings_screens.xml new file mode 100644 index 0000000000..a0ae15a250 --- /dev/null +++ b/app/src/main/res/values-zh-rTW/strings_screens.xml @@ -0,0 +1,244 @@ + + + + 错误:流派 ID 缺失 + + + 开始吧! + 第 %1$d 步,共 %2$d 步 + 请先授予所需权限。 + 请授予所有所需权限。 + 欢迎使用 + β + Beta + 让我们为您完成所有设置。 + 媒体权限 + PixelPlayer 需要访问您的音频文件以构建您的音乐库。 + 权限已授予 + 授予媒体权限 + 通知 + 启用通知以便在锁屏和通知栏控制您的音乐。 + 启用通知 + 您有备份文件吗? + 如果您已有 PixelPlayer 备份,现在恢复它可以跳过此设备上的大部分剩余设置。 + 导入备份 + 正在检查备份 + 正在检查备份包… + 正在恢复备份 + 跳过 / 暂不 + 恢复备份 + 在完成设置之前,查看您要导入的内容。 + 已选择 %1$d 个模块,共 %2$d 个 + 创建于 %1$s + 来自版本 %1$s 的备份 + 未知版本 + 恢复所选项 + 正在恢复 + 排除的文件夹 + 默认扫描所有文件夹。选择在构建媒体库时要忽略的任何位置。 + 选择要忽略的文件夹 + 请先授予存储权限 + 应用主题 + 在开始探索媒体库之前,选择您喜欢的外观。 + 深色 + PixelPlayer 默认的 Material 3 深色外观。 + 浅色 + 整个应用采用更明亮的 Material 3 外观。 + 跟随系统 + 匹配您手机当前的外观设置。 + 推荐 + 您稍后可以在“设置 > 外观 > 应用主题”中更改此设置。 + 媒体库布局 + 选择您首选的媒体库导航方式。 + 歌曲 + 紧凑模式 + 使用极简胶囊导航 + 使用标准标签栏 + 歌曲 + 专辑 + 艺术家 + 您稍后可以在“设置 > 外观 > 媒体库导航”中更改此设置。 + 应用导航 + 选择底部导航栏的样式。 + 默认样式 + 带有圆角的悬浮胶囊 + 标准全宽栏 + 自定义圆角半径 + 您稍后可以在“设置 > 外观 > 导航栏样式”中更改此设置。 + 闹钟和提醒 + 可选,但如果您使用睡眠定时器并希望 PixelPlayer 准时停止播放,则推荐使用。 + 授予权限 + 电池优化 + 某些 Android 设备会强制结束后台应用。请为 PixelPlayer 禁用电池优化,以防止意外的播放中断。 + 禁用优化 + 一切就绪! + 您已准备好享受音乐了。 + + + 搜索… + 搜索 + 清除搜索 + 最近搜索 + 清除全部 + 历史 + 删除搜索历史项 + 无结果 + 没有找到“%1$s”的结果 + 未找到任何内容 + 尝试不同的搜索词或检查您的过滤条件。 + 未找到结果。 + 按流派浏览 + 无可用流派。 + + + 播放 %1$s + 收起 %1$s + 展开 %1$s + 编辑艺术家图片 + 更改照片 + 恢复默认 + 随机播放艺术家 + + + 唱片 %d + %1$s 的封面 + %1$s · %2$s + + + 未找到播放列表。 + 此播放列表为空。 + 点击“添加歌曲”开始。 + 此文件夹不包含歌曲。 + 对歌曲排序 + 更多选项 + 播放列表选项 + 编辑播放列表 + 删除播放列表 + 删除播放列表? + 您确定要删除此播放列表吗? + 设置默认过渡效果 + 导出播放列表 + %1$s • %2$s + 播放 + 添加 + 添加歌曲 + 移除 + 移除歌曲 + 重新排序 + 重新排序歌曲 + + + 全局过渡效果 + 播放列表规则 + 除非被覆盖,否则此配置适用于所有播放源。 + 为此特定播放列表配置默认行为。 + 活动状态 + 全局默认 + 播放列表默认 + 跟随全局 + 自定义覆盖 + 自定义覆盖 + 启用以设置此播放列表的具体规则。 + 正在使用全局默认值 + 更改已成功保存 + 过渡样式 + 曲目之间如何混合 + + 淡入淡出 (Crossfade) + 过渡时长 + 总重叠时长 %1$d 秒 + 重置过渡效果 + 当前歌曲 + 下一首歌曲 + 曲目将重叠 %1$d 秒 + 音量曲线 + 微调音频斜率 + 淡出 + 淡入 + + + 新建智能播放列表 + 新建播放列表 + 添加歌曲 + 返回或取消 + 下一步 + 创建 + 编辑播放列表 + 自动生成拼图 + 添加照片 + 选择图片 + 更改 + 移除 + 播放列表名称 + 我的超赞混音 + 编辑封面 + 调整您的封面图片 + 使用双指缩放和拖动手势找到完美的构图 + 手动 + 智能 + 使用 AI 生成 + 智能规则 + 默认 + 图片 + 图标 + 背景颜色 + 图标符号 + 形状样式 + 形状参数 + 圆角半径 + 平滑度 + 边数 + 曲线 + 旋转 + 缩放 + 播放最多 + 您播放次数最多的曲目。 + 最近播放 + 您最近听过的歌曲。 + 被遗忘的最爱 + 您有一段时间没听过的最爱曲目。 + 新发现 + 最近添加但播放次数较少的曲目。 + + + 快速填充流派 + 排序与播放 + 随机播放 + 排序依据 + 艺术家 + 专辑 + 标题 + 通用艺术家 + 随机播放 %1$s + + + 选择歌曲 + 选择流派 + 搜索歌曲 + 新建流派 + 添加自定义 + 添加自定义流派 + 流派名称 + 选择图标 + 流派:%1$s + 选择一个流派 + 快速填充 + + + DJ 空间 + 正在加载… + 打碟机 %1$d + 加载歌曲 + 未加载歌曲 + + 音轨分离 (Stem separation) 功能暂不可用。 + 音量 + 速度 + 交叉推子 + 打碟机 1 + 打碟机 2 + 选择一首歌曲 + 播放/暂停 + 歌曲封面 + x%1$.2f + diff --git a/app/src/main/res/values-zh-rTW/strings_settings.xml b/app/src/main/res/values-zh-rTW/strings_settings.xml new file mode 100644 index 0000000000..feae1075f2 --- /dev/null +++ b/app/src/main/res/values-zh-rTW/strings_settings.xml @@ -0,0 +1,652 @@ + + + + 音乐管理 + 管理文件夹、刷新媒体库、解析选项 + 外观 + 主题、布局和视觉样式 + 播放 + 音频行为、淡入淡出和后台播放 + 行为 + 手势、触感反馈和导航行为 + AI 集成 (β) + AI 提供商、API 密钥和模型设置 + 备份与恢复 + 导出和恢复您的个人应用数据 + 开发者选项 + 实验性功能和调试 + 均衡器 + 调整音频频率和预设 + 设备性能 + 音频规格、编解码器和解码器信息 + 账户 + 管理 Telegram、Google Drive、网易云音乐等服务 + 关于 + 应用信息、版本和鸣谢 + + + + + 已启用 + 已禁用 + 打开 + 全选 + 清除选择 + 关闭通知 + + + 媒体库结构 + 排除的目录 + 扫描媒体库时将跳过此处的文件夹。 + 艺术家 + 多艺术家解析和整理选项。 + 过滤 + 最小歌曲时长 + 每张专辑最少曲目数 + 专辑封面缓存限制 + 同步与扫描 + 刷新媒体库 + 扫描整个媒体库以查找新增和修改的文件。 + 完全重新扫描 + 正在运行完全重新扫描 + 已开始完全重新扫描… + 媒体库同步完成 + 重建数据库 + 重建数据库? + 这将从头开始完全重建您的音乐库。所有导入的歌词、收藏和自定义元数据都将丢失。此操作无法撤销。 + 重建 + 正在重建数据库 + 正在重建数据库… + 自动扫描 .lrc 文件 + 在同步媒体库时,自动扫描并分配同一文件夹中的 .lrc 文件。 + 歌词管理 + 歌词来源优先级 + 选择获取歌词时优先尝试的来源。 + 内嵌优先 + 在线优先 + 本地 (.lrc) 优先 + 重置已导入的歌词 + 从数据库中移除所有已导入的歌词。 + 重置已导入的歌词? + 此操作无法撤销。 + + + 刷新 + 默认允许所有内容。点击文件夹将其标记为免扫描。 + 此处无子文件夹 + 向上导航 + 转到根目录 + + + 需要重新扫描 + 艺术家设置已更改。请重新扫描媒体库以应用更改。 + 重新扫描 + 正在扫描… + 多艺术家解析 + 字符分隔符 + 当前:%1$s + 单词分隔符 + + 当前:%1$s + 配置 + 从标题提取艺术家 + 检测歌曲标题中的 feat.、ft.、with + 媒体库整理 + 按专辑艺术家分组 + 在主要艺术家下显示合作专辑 + 关于多艺术家解析 + + PixelPlayer 使用字符分隔符(/、;、&)和单词分隔符(feat.、ft.、vs.、x)来拆分艺术家标签。单词分隔符不区分大小写。 + “从标题提取艺术家”会检测歌曲标题中如 (feat. Artist) 的模式。 + 反斜杠 (\) 可用于转义字符分隔符。 + + 示例 + \u2192 + \u266A + \"Artist1/Artist2\" + Artist1, Artist2 + \"Drake feat. Rihanna\" + Drake, Rihanna + \"Marshmello x Bastille\" + Marshmello, Bastille + \"Song (ft. B)\" by A + A, B + \"AC\\DC\" + AC/DC(已转义) + + + 分隔符 + 当前分隔符 + 点击分隔符将其移除。至少需要一个分隔符。 + 添加新分隔符 + 例如:/ 或 ; + 添加分隔符 + 默认分隔符 + 重置分隔符? + 这将清除您所有自定义的分隔符并恢复默认设置。此操作无法撤销。 + 分隔符已重置为默认 + 至少需要一个分隔符 + 已添加分隔符 + 分隔符已存在或无效 + 空格 + + + 单词分隔符 + 当前单词分隔符 + 当这些关键词前后有空格时,会分割艺术家名称。匹配不区分大小写。点击以移除。 + 未配置单词分隔符 + 添加新单词分隔符 + 例如:feat. 或 ft. + 添加单词分隔符 + 单词分隔符的工作原理 + 单词分隔符不区分大小写,且要求前后有空格。\n\n单字符分隔符(如 \"x\")需要在两侧都留有空格以避免误匹配。\n\n示例:\n \"Drake feat. Rihanna\" -> Drake, Rihanna\n \"Marshmello x Bastille\" -> Marshmello, Bastille\n \"A vs. B\" -> A, B + 重置单词分隔符? + 这将清除您所有自定义的单词分隔符并恢复默认关键词。此操作无法撤销。 + 已添加单词分隔符 + 已存在或无效 + 单词分隔符已重置为默认 + + + 正在准备同步 + 正在读取 MediaStore + 正在处理曲目 + 正在保存到数据库 + 正在扫描歌词文件 + 正在清理专辑封面缓存 + 正在同步云端源 + 正在完成同步 + %1$s • %2$d%% (%3$d/%4$d) + %1$s… + + + 全局主题 + 应用语言 + 选择应用界面的语言。 + 跟随系统 + 英语 + 西班牙语 + 德语 + 法语 + 俄语 + 简体中文 + 繁體中文 + 日語 + 越南語 + 印尼語 + 意大利语 + 韩语 + 挪威语(Bokmål) + 土耳其语 + 应用主题 + 在浅色、深色之间切换,或跟随系统外观。 + 浅色主题 + 深色主题 + 跟随系统 + 使用平滑圆角 + 使用复杂的圆角形状以有效提升美观度,但可能会影响低端设备的性能 + 关闭模糊效果 + 在应用中关闭模糊效果以节省电量和资源。 + 显示滚动条 + 在音乐列表中显示侧边滚动条,便于快速滚动。 + 正在播放 + 播放器主题 + 选择悬浮播放器的外观。 + 专辑封面 + 系统动态主题 + 显示播放器文件信息 + 在播放器进度部分显示编解码器、比特率和采样率。 + 专辑封面调色板样式 + 当前:%1$s。打开实时预览并选择样式。 + 轮播图样式 + 选择专辑轮播图的外观。 + 无预览 (No Peek) + 单预览 (One Peek) + 首页拼图 + 拼图模式 + 为“你的推荐”拼图选择形状排列方式。 + 自动轮换模式 + 每次访问首页时循环切换拼图模式。 + 导航栏 + 导航栏样式 + 选择导航栏的外观。 + 默认 + 全宽 + 紧凑模式 + 仅显示图标并降低导航栏高度。 + 导航栏圆角半径 + 调整导航栏的圆角半径。 + 歌词界面 + 沉浸式歌词 + 自动隐藏控件并放大文本。 + 自动隐藏延迟 + 控件隐藏前的时间。 + 3秒 + 4秒 + 5秒 + 6秒 + 应用导航 + 默认选项卡 + 选择默认启动时的选项卡。 + 首页 + 搜索 + 媒体库 + 媒体库导航 + 选择在媒体库选项卡之间移动的方式。 + 标签栏(默认) + 紧凑胶囊与网格 + + + 颜色 + 调色板样式 + 为播放器界面选择专辑颜色。 + 柔和 (Tonal Spot) + 平衡且平静。 + 鲜艳 (Vibrant) + 高饱和度点缀。 + 表现力 (Expressive) + 大胆的色调变化和对比。 + 混搭 (Fruit Salad) + 俏皮的旋转点缀。 + 色彩准确度 + 0 表示保持当前调整。值越高越接近专辑封面的主色调。 + 当前 + 更准确 + 0 • 当前 + %1$d • 微妙 + %1$d • 平衡 + %1$d • 精确 + + + 调整圆角半径 + 使导航栏形状的圆角与设备的物理圆角相匹配,以获得无缝的外观。 + 圆角半径 + %1$d dp + + + 后台播放 + 关闭后继续播放 + 如果关闭此项,从最近任务中移除应用将停止播放。 + 电池优化 + 禁用电池优化以防止播放中断。 + 电池优化已被禁用 + 无法打开电池设置 + 音量标准化 (ReplayGain) + 启用 ReplayGain + 使用音频文件中的 ReplayGain 元数据标准化音量。 + 增益模式 + 曲目:对每首歌曲进行标准化。专辑:按专辑进行标准化。 + 曲目 + 专辑 + 投屏 + 连接/断开投屏时自动播放 + 切换投屏连接后立即开始播放。 + 耳机 + 耳机重新连接时恢复播放 + 如果播放因拔出耳机而暂停,则在重新连接时自动恢复。 + 队列与过渡 + 淡入淡出 + 启用歌曲之间的平滑过渡。 + 淡入淡出时长 + Hi-Fi 模式 + 浮点 32 位音频输出。如果您的设备播放卡顿,请禁用此项。 + 此设备不支持(PCM_FLOAT AudioTrack 不可用)。 + 持久随机播放 + 即使关闭应用后也能记住随机播放设置。 + 显示队列历史记录 + 在队列中显示以前播放过的歌曲。 + + + 文件夹 + 返回手势控制文件夹 + 在“文件夹”选项卡中,系统返回手势会在离开媒体库之前导航文件夹层级。 + 播放器手势 + 点击背景关闭播放器 + 点击模糊的背景以关闭播放器面板。 + 触感反馈 + 触感反馈 + 在整个应用中启用震动反馈。 + + + AI 提供商 + 提供商 + 选择您的 AI 提供商 + 安全 Token 模式 + 开启 — 快速且低成本。向 AI 发送最少的数据(约 1K token)。 + 关闭 — 深度上下文。发送完整的听歌偏好资料(约 8K token)以获得更丰富的结果。 + 凭据 + %1$s API 密钥 + 从 %1$s 获取 + Google AI Studio (aistudio.google.com) + DeepSeek 平台 (api.deepseek.com) + Groq 控制台 (console.groq.com) + Mistral AI 平台 (console.mistral.ai) + NVIDIA Build (build.nvidia.com) + 月之暗面 (Moonshot) AI 平台 (platform.moonshot.cn) + 智谱 AI 开放平台 (bigmodel.cn) + OpenAI 平台 (platform.openai.com) + 模型选择 + 正在加载可用模型… + 加载模型失败 + AI 模型 + 选择一个模型。 + 输入 API 密钥 + 提示词行为 + 系统提示词 (System Prompt) + 自定义 AI 的行为方式。 + 预设提示词 + 输入系统提示词… + 专业策展人 (Professional Curator) + 你是“Vibe-Engine”,一位世界级的音乐策展人和声音心流大师。你的目标是构建无缝、高保真的听觉体验。请优先考虑和声兼容性、符合逻辑的 BPM 过渡,并在熟悉的挚爱与富有品味、逻辑驱动的发现之间取得精妙平衡。 + 创意先锋 (Creative Maverick) + 你是一位前卫的音乐探索者,专注于“意想不到的凝聚力”。你的任务是通过识别非显而易见的声音共性来打破传统的流派界限。在保持令人惊讶但不可否认的过渡逻辑的同时,优先考虑罕见的深度曲目、实验性纹理和艺术新意。 + 严谨图书管理员 (Strict Librarian) + 你是一位精准的音乐数据库架构师。你的逻辑由绝对的元数据精确性和严格的分类界限所驱动。尽量减少算法发现,优先保持严格的流派一致性、能量水平匹配,并最大程度地检索出用户定义的高度具体的偏好。 + 氛围向导 (Atmospheric Guide) + 你是环境纹理和低能量心流的大师。专心挑选能够促进“深度专注”或“宁静”状态的曲目。优先选择原声的温暖感、极简主义的编曲和柔和的过渡,同时严格避免瞬态强烈的声音或动态范围的突然转变。 + 声音狂热者 (Sonic Enthusiast) + 你是一位专注于制作复杂性和配器的发烧友分析师。优先选择具有高动态范围、复杂多重节奏和卓越声场质量的曲目。偏好那些奖励听众关注技术保真度和编曲细节的主动聆听作品。 + 能量催化剂 (Energy Catalyst) + 你是一个高动量的节奏生成器。你的哲学以驱动感强的低音线、强烈的打击乐和极具感染力的律动为中心。优先考虑高 BPM 的夜店兼容性、切分音能量和持续的节奏张力,以使听众的心率和动力保持在最高水平。 + AI 使用报告 + 总消耗量 + %1$s token 追踪\n提示:%2$s | 输出:%3$s | 思考:%4$s + 清除日志 + AI 活动日志 (%1$d) + %1$s · %2$s + 显示 + 隐藏 + + + 备份的工作原理 + 选择版块,导出一个 .pxpl 文件,并在稍后导入它。恢复操作仅替换您选择的版块。 + 创建备份 + 导出备份 + 未选择版块。 + 已选择所有版块。 + 已选择 %1$d 个版块,共 %2$d 个。 + %1$s 创建一个 .pxpl 备份文件。 + 选择并导出 + 恢复备份 + 导入备份 + 选择并恢复 + 浏览或从最近的备份中选择。所选数据将替换当前数据。 + + + 确切选择您想包含在备份包中的内容。 + 选择一个 .pxpl 备份文件进行检查。您将在下一步选择要恢复的版块。 + 已选择 %1$d 个版块,共 %2$d 个 + 已选择 %1$d 个模块,共 %2$d 个 + 最近的备份 + 没有最近的备份 + 以前导入的备份将显示在这里。 + %1$d 个条目 · 将替换当前数据 + 导出 .pxpl + 恢复所选项 + 正在传输… + PixelPlayer_Backup_%1$d.pxpl + 正在创建备份 + 正在恢复备份 + %1$d%% + %1$s • %2$s + 正在导出 + 正在导入 + 正在恢复 + 从历史记录中移除 + 正在检查… + 浏览文件 + 第 %1$d 步,共 %2$d 步 + 恢复模块 + 备份详情 + 创建时间 + 应用版本 + 架构 (Schema) + 设备 + 未知 + · %1$s + %1$d 个模块 · v%2$s · 架构 v%3$d + \? + 全选 + 取消全选 + + + 无效备份:%1$s + 正在准备恢复 + 正在启动恢复任务。 + 正在准备备份 + 正在启动备份任务。 + 备份恢复成功 + 恢复完成,但存在一些未解决的问题。 + 无法完成恢复:%1$s + 恢复失败:%1$s + 数据导出成功 + 导出失败:%1$s + 数据恢复成功 + 恢复完成,存在未解决的问题。失败:%1$s + v%1$d + %1$s %2$s + + + 实验性功能 + 实验性 + 播放器界面加载实验和开关。 + 测试设置流程 + 启动新手引导设置界面进行测试。 + 维护 + 强制重新生成 Daily Mix + 立即重新创建每日推荐播放列表。 + 重新生成 Daily Mix + 重新生成 Daily Mix? + 这将丢弃当前的推荐,并根据最近的听歌习惯生成一个新的。 + Daily Mix 重新生成已开始 + 强制重新生成统计数据 + 清除缓存并重新计算播放统计数据。 + 重新生成 + 正在处理… + 重新生成统计数据 + 重新生成统计数据? + 这将清除统计数据缓存并强制从数据库历史记录重新计算。 + 统计数据重新生成已开始 + 强制重新生成专辑调色板 + 重建每张专辑封面的所有缓存调色板变体,或选择单个进行刷新。 + 全部重新生成 + 重新生成所有专辑调色板? + 这将清除缓存的主题数据,并重建 %1$d 个唯一专辑封面的所有调色板样式。 + 正在重新生成… + 正在重新生成专辑调色板… + 正在重建 %1$d 个唯一专辑封面的缓存调色板变体。在大型媒体库中这可能需要一些时间。 + 已完成 %1$d 个,共 %2$d 个 + 已重新生成 %1$d 个专辑封面调色板 + 已重新生成 %1$d 个(共 %2$d 个)专辑封面调色板 + 选择歌曲 + 选择一首歌曲以清除缓存的主题数据,并从专辑封面重新生成所有调色板样式。 + 按标题、艺术家或专辑搜索 + 没有与您搜索匹配的歌曲。 + 未找到带有专辑封面的歌曲。 + 正在重新生成调色板… + 已为 %1$s 重新生成调色板 + 无法为 %1$s 重新生成调色板 + 诊断 + 触发测试崩溃 + 模拟一次崩溃以测试崩溃报告系统。 + 测试崩溃是从开发者选项触发的 - 这是为了测试崩溃报告系统的有意行为 + + + 实验性 + 播放器界面加载调整 + 动画歌词(高端设备) + 为歌词使用弹簧动画和视觉效果。在低端设备上可能导致掉帧。 + 歌词模糊效果 + 对非活动歌词应用景深模糊。 + 模糊强度 + 调整模糊效果的强度。 + %1$.1fx + 第 1 步 · 选择要延迟的内容 + 延迟所有内容 + 在面板背景完全展开之前,保留所有播放器内容。 + 专辑轮播 + 在面板展开前延迟专辑封面和轮播图。 + 歌曲元数据 + 延迟标题、艺术家以及歌词/队列操作。 + 进度条 + 在展开完成之前延迟时间轴和时间标签。 + 播放控制 + 延迟播放/暂停、快进/快退和收藏控件。 + 所有延迟组件均处于活动状态。禁用“延迟所有内容”以自定义各个部分。 + 第 2 步 · 配置占位图行为 + 对延迟项使用占位图 + 在组件等待展开时渲染轻量级占位图,以保持布局稳定。 + 第 3 步 · 选择占位图切换为真实内容的时机 + 选择一种模式。阈值模式使用滑块;拖动释放模式等待直到您释放面板手势。 + 启用至少一个延迟组件以解锁触发模式。 + 阈值 + 使用展开百分比。 + 拖动释放 + 仅在手势释放后切换。 + 展开阈值 + 在延迟组件变得可见之前,面板必须展开的程度。 + 内容在 %1$d%% 展开时出现 + 在播放器关闭时也应用 + 使用关闭阈值在折叠时切换回占位图。 + 关闭阈值 + 在占位图再次接管之前需要折叠多少程度。 + 占位图在折叠 %1$d%% 后出现 + 拖动释放模式会绕过阈值和关闭行为。交换仅在面板拖动手势结束时发生。 + 使占位图透明 + 占位图保留其布局空间,但变为不可见。 + 视觉质量 + 专辑封面分辨率 + 低 (256px) - 更好的性能 + 中等 (512px) - 均衡 + 高 (800px) - 最佳质量 + 原图 - 最高质量 + + + 播放需检查 + 准备就绪可播放 + -- + 格式 + 硬件解码 + 本地歌曲 + 本地音乐存储 + 音乐大小 + %1$d 首本地歌曲 + 可用空间 + 总计 %1$s + 音乐占用 + 设备已用 + %1$d%% + <1% + %1$d 首云端歌曲 + %1$d 个文件不可读 + 播放路径 + + + 采样率 + %1$d Hz + 每个缓冲区 %1$d 帧 + Hi-Fi PCM 浮点 + 32 位浮点输出路径 + 低延迟支持 + Pro Audio 支持 + 内存 + 可用(总计 %1$s) + 支持硬件卸载的格式 + 没有压缩格式报告支持硬件卸载 (hardware offload)。 + 还有 +%1$d 个 + 检测到的输出 + 内置输出 + 蓝牙音频 + USB 音频 + 有线音频 + 数字输出 + 其他输出 + Android 未报告任何输出路由。 + ExoPlayer 引擎 + %1$s 个渲染器 + 格式兼容性 + %1$d 首支持的曲目 + %1$d 个未知格式 + 未报告解码器 + 硬件解码器 + 软件解码器 + 硬件卸载 + 媒体库中有 %1$d 首 + 兼容性检测结果 + 无重大不兼容问题 + 您已索引的曲目与 Android 报告此设备支持的解码器相匹配。 + %1$d 首曲目可能无法原生解码 + 需检查的格式:%1$s。 + %1$d 首本地曲目可能会被重采样 + 媒体库采样率达到 %1$d Hz,高于当前的输出采样率。 + %1$d 首曲目具有未知的元数据 + 进行完整的媒体库重新扫描可以填补缺失的 MIME、比特率和采样率数据。 + 设备信息 + 制造商 + 型号 + 品牌 + 设备 + Android 版本 + SDK 版本 + 硬件 + 性能报告 + 生成一份可分享的诊断报告,以帮助我们分类播放或扫描卡顿问题。该报告仅包含设备、媒体库和计时数据 — 不包含文件路径、标题或艺术家。 + 生成报告 + 重新生成 + 复制 + 分享 + 报告已复制到剪贴板 + PixelPlayer 性能报告 + 高级性能诊断 + 默认关闭。用于记录短暂卡顿时间线,便于测试版问题排查。 + 有效期至 %1$s + 立即标记卡顿 + 已标记卡顿时刻 + + + 已连接的账户 + 管理关联的提供商并将每个集成功能掌握在自己手中。 + 已关联服务 + 活跃 + 可用 + 敬请期待 + 已连接 + 敬请期待 + 打开服务 + 正在登出… + 尚未关联任何账户 + 连接一个提供商即可在此界面对其进行管理。 + 连接 %1$s + %1$s(敬请期待) + Google Drive 功能即将推出。 + 目前无法打开此屏幕。 + + + 关于 + PixelPlayer + 由社区共同打造的开源音乐播放器。 + 版本 v%1$s + 开源 + 社区优先 + Material 3 表现力设计 + 目前未找到贡献者。请稍后再试。 + 维护者 + PixelPlayer 背后的开发者。 + 社区焦点 + 感谢对本项目有重大影响的合作者。 + 开源贡献者 + 来自 GitHub 的实时贡献者名单。 + %1$d 次贡献 + GitHub + 代码仓库 + Telegram + 支持 + 打开 GitHub 仓库 + 加入 Telegram 社区 + 打开 GitHub 个人资料 + 打开 Telegram + %1$s 的头像 + %1$s 的图标 + 音量 + 音量为零时暂停 + 当音量设置为 0 时自动暂停播放 + diff --git a/app/src/main/res/values-zh-rTW/strings_widget.xml b/app/src/main/res/values-zh-rTW/strings_widget.xml new file mode 100644 index 0000000000..5a2fd3c03d --- /dev/null +++ b/app/src/main/res/values-zh-rTW/strings_widget.xml @@ -0,0 +1,17 @@ + + + 可根据尺寸自适应的响应式小组件 + 紧凑型播放栏 + 包含随机和循环播放的完整控件 + 极简方形播放器 + + 点击打开 + 专辑封面 + 专辑封面占位图 + + 点击播放 + 歌曲标题 + 艺术家 + + 进度条,%1$d%% + \ No newline at end of file diff --git a/app/src/main/res/values/plurals.xml b/app/src/main/res/values/plurals.xml new file mode 100644 index 0000000000..6961e51cef --- /dev/null +++ b/app/src/main/res/values/plurals.xml @@ -0,0 +1,59 @@ + + + + Sharing %d playlist + Sharing %d playlists + + + Exported %1$d playlist to %2$s + Exported %1$d playlists to %2$s + + + %d song added to queue + %d songs added to queue + + + %d song will play next + %d songs will play next + + + %d song added to favorites + %d songs added to favorites + + + %d song removed from favorites + %d songs removed from favorites + + + %d file deleted + %d files deleted + + + Delete %d file? + Delete %d files? + + + %d track selected + %d tracks selected + + + %1$d song • %2$s + %1$d songs • %2$s + + + %d track lined up. + %d tracks lined up. + + + %d song selected + %d songs selected + + + %d song + %d songs + + + %d topic + %d topics + + diff --git a/app/src/main/res/values/strings_settings.xml b/app/src/main/res/values/strings_settings.xml index 1c6ae4e284..a6f8d9f1dc 100644 --- a/app/src/main/res/values/strings_settings.xml +++ b/app/src/main/res/values/strings_settings.xml @@ -5,12 +5,16 @@ Manage folders, refresh library, parsing options Appearance Themes, layout, and visual styles + Lyrics + Source priority, immersive lyrics, and import management Playback Audio behavior, crossfade, and background play Behavior Gestures, haptics, and navigation behavior AI Integration (β) - AI providers, API keys, and model settings + AI providers, API keys, model selection, and activity logs + Generation Parameters + Temperature, sampling, prompts, and song data configuration Backup & Restore Export and recover your personal app data Developer Options @@ -171,9 +175,12 @@ 简体中文 Bahasa Indonesia Italiano + 日本語 Korean Norwegian (Bokmål) + 繁體中文 Türkçe + Tiếng Việt App Theme Switch between light, dark, or follow system appearance. Light Theme diff --git a/app/src/main/res/xml/locales_config.xml b/app/src/main/res/xml/locales_config.xml index 12f03ebe00..6d332ab8d5 100644 --- a/app/src/main/res/xml/locales_config.xml +++ b/app/src/main/res/xml/locales_config.xml @@ -1,6 +1,17 @@ + + + + + + + + + + + \ No newline at end of file From 24679c4b4d9cc04b2de29beba6fdb534df6910c3 Mon Sep 17 00:00:00 2001 From: Dae Euhwa Date: Mon, 27 Jul 2026 20:20:14 -0500 Subject: [PATCH 55/73] perf: optimize library loading, player, and data layer - Defer legacy migrations and non-critical loads from init hot path - Serialize library data loading to avoid I/O contention - Derive isLibraryContentEmpty from in-memory state instead of Room queries - Add distinctUntilChanged to playerUiState combine collectors - Eliminate loading state flapping during sequential library load - Context-based ActivityManager APIs replacing reflection --- .../data/paging/MediaStorePagingSource.kt | 2 +- .../preferences/AiPreferencesRepository.kt | 2 +- .../preferences/UserPreferencesRepository.kt | 23 +------ .../data/repository/MusicRepository.kt | 6 -- .../data/repository/MusicRepositoryImpl.kt | 4 -- .../com/theveloper/pixelplay/di/AppModule.kt | 15 +++++ .../com/theveloper/pixelplay/di/Qualifiers.kt | 7 +++ .../viewmodel/LibraryStateHolder.kt | 23 ++++--- .../viewmodel/LyricsStateHolder.kt | 27 ++++---- .../presentation/viewmodel/PlayerViewModel.kt | 46 ++++++-------- .../viewmodel/SongRemovalStateHolder.kt | 2 +- .../pixelplay/utils/PlatformUtils.kt | 62 +++++++++++++++++++ 12 files changed, 133 insertions(+), 86 deletions(-) create mode 100644 app/src/main/java/com/theveloper/pixelplay/utils/PlatformUtils.kt diff --git a/app/src/main/java/com/theveloper/pixelplay/data/paging/MediaStorePagingSource.kt b/app/src/main/java/com/theveloper/pixelplay/data/paging/MediaStorePagingSource.kt index 2fda86c0b0..4e5f478ac7 100644 --- a/app/src/main/java/com/theveloper/pixelplay/data/paging/MediaStorePagingSource.kt +++ b/app/src/main/java/com/theveloper/pixelplay/data/paging/MediaStorePagingSource.kt @@ -61,7 +61,7 @@ class MediaStorePagingSource( val songs = fetchSongDetails(idsToLoad) // Sort songs to match the order of idsToLoad (because "IN" query doesn't guarantee order) - val songsMap = songs.associateBy { it.id.toLong() } + val songsMap = songs.associateBy { it.id.toLongOrNull() ?: -1L } val orderedSongs = idsToLoad.mapNotNull { songsMap[it] } val nextKey = if (end < filteredIds.size) pageIndex + 1 else null diff --git a/app/src/main/java/com/theveloper/pixelplay/data/preferences/AiPreferencesRepository.kt b/app/src/main/java/com/theveloper/pixelplay/data/preferences/AiPreferencesRepository.kt index 964875d040..32339a44cd 100644 --- a/app/src/main/java/com/theveloper/pixelplay/data/preferences/AiPreferencesRepository.kt +++ b/app/src/main/java/com/theveloper/pixelplay/data/preferences/AiPreferencesRepository.kt @@ -152,7 +152,7 @@ class AiPreferencesRepository @Inject constructor( dataStore.data.map { preferences -> preferences[Keys.AI_TOP_K] ?: 64 } val aiMaxTokens: Flow = - dataStore.data.map { preferences -> preferences[Keys.AI_MAX_TOKENS] ?: 4096 } + dataStore.data.map { preferences -> preferences[Keys.AI_MAX_TOKENS] ?: 8192 } val aiPresencePenalty: Flow = dataStore.data.map { preferences -> preferences[Keys.AI_PRESENCE_PENALTY] ?: 0.0f } diff --git a/app/src/main/java/com/theveloper/pixelplay/data/preferences/UserPreferencesRepository.kt b/app/src/main/java/com/theveloper/pixelplay/data/preferences/UserPreferencesRepository.kt index 4598f1a86f..d353c6c118 100644 --- a/app/src/main/java/com/theveloper/pixelplay/data/preferences/UserPreferencesRepository.kt +++ b/app/src/main/java/com/theveloper/pixelplay/data/preferences/UserPreferencesRepository.kt @@ -507,32 +507,11 @@ class UserPreferencesRepository @Inject constructor( dataStore.edit { it[PreferencesKeys.GLOBAL_TRANSITION_SETTINGS] = json.encodeToString(settings) } } - // ─── Favorites ──────────────────────────────────────────────────────────── + // ─── Favorites (Legacy — only used for one-time migration) ───────────────── val favoriteSongIdsFlow: Flow> = pref { it[PreferencesKeys.FAVORITE_SONG_IDS] ?: emptySet() } - /** - * Adds or removes [songId] from favorites depending on [isFavorite]. - * Prefer this over [toggleFavoriteSong] when the desired state is known. - */ - suspend fun setFavoriteSong(songId: String, isFavorite: Boolean) { - dataStore.edit { preferences -> - val current = preferences[PreferencesKeys.FAVORITE_SONG_IDS] ?: emptySet() - preferences[PreferencesKeys.FAVORITE_SONG_IDS] = - if (isFavorite) current + songId else current - songId - } - } - - /** Toggles [songId] in the favorites set. */ - suspend fun toggleFavoriteSong(songId: String) { - dataStore.edit { preferences -> - val current = preferences[PreferencesKeys.FAVORITE_SONG_IDS] ?: emptySet() - preferences[PreferencesKeys.FAVORITE_SONG_IDS] = - if (songId in current) current - songId else current + songId - } - } - suspend fun clearFavoriteSongIds() { dataStore.edit { it[PreferencesKeys.FAVORITE_SONG_IDS] = emptySet() } } diff --git a/app/src/main/java/com/theveloper/pixelplay/data/repository/MusicRepository.kt b/app/src/main/java/com/theveloper/pixelplay/data/repository/MusicRepository.kt index 36d9081b99..d7bcb635f5 100644 --- a/app/src/main/java/com/theveloper/pixelplay/data/repository/MusicRepository.kt +++ b/app/src/main/java/com/theveloper/pixelplay/data/repository/MusicRepository.kt @@ -77,12 +77,6 @@ interface MusicRepository { storageFilter: com.theveloper.pixelplay.data.model.StorageFilter = com.theveloper.pixelplay.data.model.StorageFilter.ALL ): Flow - /** - * Returns the count of songs in the library. - * @return Flow emitting the current song count. - */ - fun getSongCountFlow(): Flow - /** * Returns the count of cloud songs in the library. */ diff --git a/app/src/main/java/com/theveloper/pixelplay/data/repository/MusicRepositoryImpl.kt b/app/src/main/java/com/theveloper/pixelplay/data/repository/MusicRepositoryImpl.kt index 5f026af8d9..11a16bd1e6 100644 --- a/app/src/main/java/com/theveloper/pixelplay/data/repository/MusicRepositoryImpl.kt +++ b/app/src/main/java/com/theveloper/pixelplay/data/repository/MusicRepositoryImpl.kt @@ -310,10 +310,6 @@ class MusicRepositoryImpl @Inject constructor( return songRepository.getFavoriteSongCountFlow(storageFilter) } - override fun getSongCountFlow(): Flow { - return musicDao.getSongCount().distinctUntilChanged() - } - override fun getCloudSongCountFlow(): Flow { return musicDao.getCloudSongCount().distinctUntilChanged() } diff --git a/app/src/main/java/com/theveloper/pixelplay/di/AppModule.kt b/app/src/main/java/com/theveloper/pixelplay/di/AppModule.kt index 7f1d935994..4953534951 100644 --- a/app/src/main/java/com/theveloper/pixelplay/di/AppModule.kt +++ b/app/src/main/java/com/theveloper/pixelplay/di/AppModule.kt @@ -530,6 +530,21 @@ object AppModule { .build() } + /** + * Shared OkHttpClient for AI provider HTTP calls. + * Connection pooling is reused across all AI client instances. + */ + @Provides + @Singleton + @AiOkHttpClient + fun provideAiOkHttpClient(): OkHttpClient { + return OkHttpClient.Builder() + .connectTimeout(30, java.util.concurrent.TimeUnit.SECONDS) + .readTimeout(60, java.util.concurrent.TimeUnit.SECONDS) + .writeTimeout(30, java.util.concurrent.TimeUnit.SECONDS) + .build() + } + /** * Provee una instancia singleton de Retrofit para la API de LRCLIB. */ diff --git a/app/src/main/java/com/theveloper/pixelplay/di/Qualifiers.kt b/app/src/main/java/com/theveloper/pixelplay/di/Qualifiers.kt index d0207b1997..e978cd7ce8 100644 --- a/app/src/main/java/com/theveloper/pixelplay/di/Qualifiers.kt +++ b/app/src/main/java/com/theveloper/pixelplay/di/Qualifiers.kt @@ -29,3 +29,10 @@ annotation class BackupGson @Qualifier @Retention(AnnotationRetention.BINARY) annotation class AppScope + +/** + * Qualifier for shared AI OkHttpClient instance. + */ +@Qualifier +@Retention(AnnotationRetention.BINARY) +annotation class AiOkHttpClient diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/LibraryStateHolder.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/LibraryStateHolder.kt index 22fb7d311f..3515c7de8a 100644 --- a/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/LibraryStateHolder.kt +++ b/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/LibraryStateHolder.kt @@ -257,23 +257,21 @@ class LibraryStateHolder @Inject constructor( songsJob = scope?.launch { _isLoadingLibrary.value = true musicRepository.getAudioFiles().conflate().collect { songs -> - // Process heavy list conversions on Default dispatcher to avoid blocking UI val immutableSongs = withContext(Dispatchers.Default) { songs.toImmutableList() } val songsMap = withContext(Dispatchers.Default) { songs.associateBy { it.id } } _allSongs.value = immutableSongs _allSongsById.value = songsMap - // When the repository emits a new list (triggered by directory changes), - // we update our state and re-apply current sorting. - // Apply sort to the new data sortSongs(_currentSongSortOption.value, persist = false) _isLoadingLibrary.value = false } } + // Albums, artists, and folders load sequentially after songs to avoid + // I/O contention on slower storage (eMMC on Redmi vs UFS on Samsung). albumsJob = scope?.launch { - _isLoadingCategories.value = true + songsJob?.join() @OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class) kotlinx.coroutines.flow.combine( effectiveStorageFilter, @@ -287,12 +285,11 @@ class LibraryStateHolder @Inject constructor( sortAlbumsList(albums, _currentAlbumSortOption.value).toImmutableList() } _albums.value = sortedAlbums - _isLoadingCategories.value = false } } artistsJob = scope?.launch { - _isLoadingCategories.value = true + albumsJob?.join() @OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class) effectiveStorageFilter.flatMapLatest { filter -> musicRepository.getArtists(filter) @@ -301,11 +298,11 @@ class LibraryStateHolder @Inject constructor( sortArtistsList(artists, _currentArtistSortOption.value).toImmutableList() } _artists.value = sortedArtists - _isLoadingCategories.value = false } } foldersJob = scope?.launch { + artistsJob?.join() @OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class) effectiveStorageFilter.flatMapLatest { filter -> musicRepository.getMusicFolders(effectiveFoldersStorageFilter(filter)) @@ -316,6 +313,16 @@ class LibraryStateHolder @Inject constructor( _musicFolders.value = sortedFolders } } + + // Single loading state: true when songs start, false when all sequential + // jobs (albums + artists) finish. This avoids flapping true→false→true + // as each sequential coroutine completes. + scope?.launch { + songsJob?.join() + _isLoadingCategories.value = true + foldersJob?.join() + _isLoadingCategories.value = false + } } // Deprecated imperative loaders - redirected to observer start diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/LyricsStateHolder.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/LyricsStateHolder.kt index 87985fd6eb..19ae900724 100644 --- a/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/LyricsStateHolder.kt +++ b/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/LyricsStateHolder.kt @@ -344,22 +344,16 @@ class LyricsStateHolder @Inject constructor( fun translateLyricsViaAi(currentSong: Song, lyricsObj: Lyrics?, cb: LyricsTranslationCallbacks) { val songId = currentSong.id.toLongOrNull() ?: return - if (lyricsObj?.synced != null) { - val hasValidTranslation = lyricsObj.synced.any { !it.translation.isNullOrBlank() } - if (hasValidTranslation) { - _messageEvents.tryEmit(cb.getString(R.string.lyrics_translate_already_translated)) - return - } - } - scope?.launch { _messageEvents.emit(cb.getString(R.string.lyrics_translate_progress)) + // Always read fresh lyrics from source — never rely on the potentially + // stale Lyrics object from UI state which may contain old translations. val rawLyrics = withContext(Dispatchers.IO) { - currentSong.lyrics?.takeIf { it.isNotBlank() } - ?: readLocalLyricsFile(currentSong) + readLocalLyricsFile(currentSong) ?: readEmbeddedLyricsFromFile(currentSong) ?: musicRepository.getStoredLyrics(currentSong)?.second + ?: currentSong.lyrics?.takeIf { it.isNotBlank() } } if (rawLyrics.isNullOrBlank()) { @@ -428,17 +422,18 @@ class LyricsStateHolder @Inject constructor( } private fun readEmbeddedLyricsFromFile(song: Song): String? { - song.lyrics - ?.trim() - ?.takeIf { it.isNotBlank() } - ?.let { return it } - - return runCatching { + val fileLyrics = runCatching { AudioMetadataReader.read(File(song.path)) ?.lyrics ?.trim() ?.takeIf { it.isNotBlank() } }.getOrNull() + + if (fileLyrics != null) return fileLyrics + + return song.lyrics + ?.trim() + ?.takeIf { it.isNotBlank() } } private fun readLocalLyricsFile(song: Song): String? { diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/PlayerViewModel.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/PlayerViewModel.kt index 4ddadbedea..a3e9d36cf5 100644 --- a/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/PlayerViewModel.kt +++ b/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/PlayerViewModel.kt @@ -45,6 +45,8 @@ import com.theveloper.pixelplay.data.model.Genre import com.theveloper.pixelplay.data.model.Lyrics import com.theveloper.pixelplay.data.model.LyricsSourcePreference import com.theveloper.pixelplay.data.model.SearchFilterType +import com.theveloper.pixelplay.data.model.SearchHistoryItem +import com.theveloper.pixelplay.data.model.SearchResultItem import com.theveloper.pixelplay.data.model.Song import com.theveloper.pixelplay.data.model.SortOption import com.theveloper.pixelplay.data.model.toLibraryTabIdOrNull @@ -444,6 +446,11 @@ class PlayerViewModel @Inject constructor( val aiStatus: StateFlow = aiStateHolder.aiStatus val aiError: StateFlow = aiStateHolder.aiError + // Search state (direct access bypasses 40-field playerUiState) + val searchResults: StateFlow> = searchStateHolder.searchResults + val selectedSearchFilter: StateFlow = searchStateHolder.selectedSearchFilter + val searchHistory: StateFlow> = searchStateHolder.searchHistory + private val _selectedSongForInfo = MutableStateFlow(null) val selectedSongForInfo: StateFlow = _selectedSongForInfo.asStateFlow() @@ -536,14 +543,6 @@ class PlayerViewModel @Inject constructor( initialValue = false ) - val hasGeminiApiKey: StateFlow = aiPreferencesRepository.geminiApiKey - .map { it.isNotBlank() } - .stateIn( - scope = viewModelScope, - started = SharingStarted.WhileSubscribed(5000), - initialValue = false - ) - val fullPlayerLoadingTweaks: StateFlow = userPreferencesRepository.fullPlayerLoadingTweaksFlow .stateIn( scope = viewModelScope, @@ -646,7 +645,7 @@ class PlayerViewModel @Inject constructor( // Toast Events private val _toastEvents = MutableSharedFlow( - extraBufferCapacity = 1, + extraBufferCapacity = 5, onBufferOverflow = BufferOverflow.DROP_OLDEST ) val toastEvents = _toastEvents.asSharedFlow() @@ -1140,13 +1139,6 @@ class PlayerViewModel @Inject constructor( initialValue = persistentListOf() ) - val songCountFlow: StateFlow = musicRepository.getSongCountFlow() - .stateIn( - scope = viewModelScope, - started = SharingStarted.WhileSubscribed(5000), - initialValue = 0 - ) - val hasCloudSongsFlow: StateFlow = musicRepository.getCloudSongCountFlow() .map { it > 0 } .distinctUntilChanged() @@ -1527,15 +1519,11 @@ class PlayerViewModel @Inject constructor( + // Deferred: legacy migrations run after init to avoid blocking first render viewModelScope.launch { + kotlinx.coroutines.delay(2000) userPreferencesRepository.migrateTabOrder() - } - - viewModelScope.launch { userPreferencesRepository.ensureLibrarySortDefaults() - } - - viewModelScope.launch { val legacyFavoriteIds = userPreferencesRepository.favoriteSongIdsFlow.first() if (legacyFavoriteIds.isNotEmpty()) { val roomFavoriteIds = musicRepository.getFavoriteSongIdsOnce() @@ -1677,8 +1665,12 @@ class PlayerViewModel @Inject constructor( // launchColorSchemeProcessor() - Handled by ThemeStateHolder and on-demand calls - loadPersistedDailyMix() - loadSearchHistory() + // Deferred: daily mix and search history load after first frame + viewModelScope.launch { + kotlinx.coroutines.delay(1500) + loadPersistedDailyMix() + loadSearchHistory() + } viewModelScope.launch { isSyncingStateFlow.collect { isSyncing -> @@ -1797,7 +1789,7 @@ class PlayerViewModel @Inject constructor( aiStatus = status, aiError = error ) - }.collect { snapshot -> + }.distinctUntilChanged().collect { snapshot -> _playerUiState.update { it.copy( showAiPlaylistSheet = snapshot.showAiPlaylistSheet, @@ -1820,7 +1812,7 @@ class PlayerViewModel @Inject constructor( libraryStateHolder.isLoadingCategories, ) { folders, loadingLibrary, loadingCategories -> Triple(folders, loadingLibrary, loadingCategories) - }.collect { (folders, loadingLibrary, loadingCategories) -> + }.distinctUntilChanged().collect { (folders, loadingLibrary, loadingCategories) -> _playerUiState.update { it.copy( musicFolders = folders, @@ -1841,7 +1833,7 @@ class PlayerViewModel @Inject constructor( libraryStateHolder.currentFavoriteSortOption, ) { songSort, albumSort, artistSort, folderSort, favoriteSort -> SortOptionsSnapshot(songSort, albumSort, artistSort, folderSort, favoriteSort) - }.collect { snapshot -> + }.distinctUntilChanged().collect { snapshot -> _playerUiState.update { it.copy( currentSongSortOption = snapshot.songSort, diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/SongRemovalStateHolder.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/SongRemovalStateHolder.kt index 91e2d858fe..751fb52b06 100644 --- a/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/SongRemovalStateHolder.kt +++ b/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/SongRemovalStateHolder.kt @@ -104,7 +104,7 @@ class SongRemovalStateHolder @Inject constructor( suspend fun removeSongFromLibrary(song: Song) { libraryStateHolder.removeSong(song.id) - musicRepository.deleteById(song.id.toLong()) + song.id.toLongOrNull()?.let { musicRepository.deleteById(it) } playlistPreferencesRepository.removeSongFromAllPlaylists(song.id) } diff --git a/app/src/main/java/com/theveloper/pixelplay/utils/PlatformUtils.kt b/app/src/main/java/com/theveloper/pixelplay/utils/PlatformUtils.kt new file mode 100644 index 0000000000..817cd8697a --- /dev/null +++ b/app/src/main/java/com/theveloper/pixelplay/utils/PlatformUtils.kt @@ -0,0 +1,62 @@ +package com.theveloper.pixelplay.utils + +import android.os.Build + +object PlatformUtils { + fun isUbuntuTouch(): Boolean { + return try { + val properties = System.getProperties() + properties.getProperty("os.name", "") + .contains("ubuntu", ignoreCase = true) || + properties.getProperty("os.version", "") + .contains("ubuntu", ignoreCase = true) || + Build.DISPLAY.contains("ubuntu", ignoreCase = true) || + Build.HOST?.contains("ubuntu", ignoreCase = true) == true || + Build.FINGERPRINT?.contains("ubuntu", ignoreCase = true) == true || + Build.MODEL?.contains("ubuntu", ignoreCase = true) == true + } catch (_: Exception) { + false + } + } + + fun isWaydroid(): Boolean { + return try { + val properties = System.getProperties() + properties.getProperty("java.vendor.url", "") + .contains("waydroid", ignoreCase = true) || + Build.HOST?.contains("waydroid", ignoreCase = true) == true + } catch (_: Exception) { + false + } + } + + fun isRunningOnLinux(): Boolean { + return try { + val osName = System.getProperty("os.name") ?: "" + osName.contains("linux", ignoreCase = true) + } catch (_: Exception) { + false + } + } + + fun isEmulatedStorage(): Boolean { + return Build.DEVICE?.contains("generic", ignoreCase = true) == true || + Build.PRODUCT?.contains("generic", ignoreCase = true) == true || + Build.HARDWARE?.contains("goldfish", ignoreCase = true) == true || + Build.FINGERPRINT?.contains("generic", ignoreCase = true) == true + } + + val isLowRamDevice: Boolean get() = runCatching { + val activityManager = android.app.ActivityManager::class.java + .getMethod("isLowRamDevice") + activityManager.invoke(null) as? Boolean ?: false + }.getOrDefault(false) + + val totalMemoryMb: Long get() = runCatching { + val info = android.app.ActivityManager.MemoryInfo() + android.app.ActivityManager::class.java + .getMethod("getMemoryInfo", android.app.ActivityManager.MemoryInfo::class.java) + .invoke(null, info) + info.totalMem / (1024 * 1024) + }.getOrDefault(0L) +} From 96e4b67a82f64067f83acf56425af1caa5b4633d Mon Sep 17 00:00:00 2001 From: Dae Euhwa Date: Mon, 27 Jul 2026 20:20:20 -0500 Subject: [PATCH 56/73] fix(lyrics): fix translation cutoff, CJK matching, and UI - Fix lyrics translation cutoff and false already-translated detection - Fix search dialog stuck and non-Latin search - Fix CJK token scaling, embedded lyrics reading, local .lrc Unicode matching - Romanization detection improvements - LyricsSheet UI improvements (keep-screen-on, drag handling) - LyricsStateHolder improvements --- .../data/repository/LyricsRepositoryImpl.kt | 127 +++++++++++++----- .../presentation/components/LyricsSheet.kt | 7 +- .../viewmodel/LyricsStateHolder.kt | 27 ++-- .../theveloper/pixelplay/utils/LyricsUtils.kt | 30 ++++- 4 files changed, 139 insertions(+), 52 deletions(-) diff --git a/app/src/main/java/com/theveloper/pixelplay/data/repository/LyricsRepositoryImpl.kt b/app/src/main/java/com/theveloper/pixelplay/data/repository/LyricsRepositoryImpl.kt index 05e357d05c..948fdfc62a 100644 --- a/app/src/main/java/com/theveloper/pixelplay/data/repository/LyricsRepositoryImpl.kt +++ b/app/src/main/java/com/theveloper/pixelplay/data/repository/LyricsRepositoryImpl.kt @@ -527,6 +527,15 @@ class LyricsRepositoryImpl @Inject constructor( }) } } + // Romanized search strategy for non-Latin artists + if (MultiLangRomanizer.isScriptThatNeedsRomanization(cleanArtist)) { + val romanArtist = romanizeForMatch(cleanArtist) + if (romanArtist != cleanArtist) { + add(RemoteSearchStrategy("romanized_artist") { + lrcLibApiService.searchLyrics(trackName = cleanTitle, artistName = romanArtist) + }) + } + } // Smart title cleanup strategy (removes leading digits/spaces and truncates at -, (, )) val smartTitle = cleanTitleSmart(cleanTitle) @@ -581,14 +590,19 @@ class LyricsRepositoryImpl @Inject constructor( // Save to database try { - lyricsDao.insert( - com.theveloper.pixelplay.data.database.LyricsEntity( - songId = song.id.toLong(), - content = rawLyrics, - isSynced = !bestMatch.syncedLyrics.isNullOrBlank(), - source = "remote" + val songIdLong = song.id.toLongOrNull() + if (songIdLong == null) { + Log.w(TAG, "Cannot persist lyrics to DB: non-numeric Song.id '${song.id}'") + } else { + lyricsDao.insert( + com.theveloper.pixelplay.data.database.LyricsEntity( + songId = songIdLong, + content = rawLyrics, + isSynced = !bestMatch.syncedLyrics.isNullOrBlank(), + source = "remote" + ) ) - ) + } } catch (e: NumberFormatException) { Log.w(TAG, "Skipping database save for non-numeric song ID: ${song.id} (possible streaming or external source). Lyrics will be cached in JSON.") } @@ -1014,8 +1028,8 @@ class LyricsRepositoryImpl @Inject constructor( } } - val cleanArtist = song.displayArtist.replace(Regex("[^a-zA-Z0-9]"), "_") - val cleanTitle = song.title.replace(Regex("[^a-zA-Z0-9]"), "_") + val cleanArtist = song.displayArtist.replace(Regex("[^\\p{L}\\p{N}]"), "_") + val cleanTitle = song.title.replace(Regex("[^\\p{L}\\p{N}]"), "_") for (extension in LyricsImportSecurity.supportedFileExtensions()) { val alternativeLyricsFile = File(directory, "${cleanArtist}_${cleanTitle}.$extension") @@ -1056,7 +1070,14 @@ class LyricsRepositoryImpl @Inject constructor( val lyricsData = LyricsData( plainLyrics = lyrics.plain?.joinToString("\n"), - syncedLyrics = lyrics.synced?.joinToString("\n") { "[${formatTimestamp(it.time)}]${it.line}" }, + syncedLyrics = lyrics.synced?.joinToString("\n") { line -> + buildString { + append("[${formatTimestamp(line.time)}]${line.line}") + if (!line.translation.isNullOrBlank()) { + append("\n[${formatTimestamp(line.time)}]${line.translation}") + } + } + }, wordByWordLyrics = wordByWordLyrics ) @@ -1247,7 +1268,12 @@ class LyricsRepositoryImpl @Inject constructor( toWordByWordLrc(syncedLyrics) } else { syncedLyrics.joinToString("\n") { line -> - "[${formatTimestamp(line.time)}]${line.line}" + buildString { + append("[${formatTimestamp(line.time)}]${line.line}") + if (!line.translation.isNullOrBlank()) { + append("\n[${formatTimestamp(line.time)}]${line.translation}") + } + } } } } @@ -1293,14 +1319,19 @@ class LyricsRepositoryImpl @Inject constructor( val rawLyricsToSave = best.rawLyrics try { - lyricsDao.insert( - com.theveloper.pixelplay.data.database.LyricsEntity( - songId = song.id.toLong(), - content = rawLyricsToSave, - isSynced = !best.lyrics.synced.isNullOrEmpty(), - source = "remote" - ) - ) + val songIdLong = song.id.toLongOrNull() + if (songIdLong == null) { + Log.w(TAG, "Cannot persist lyrics to DB: non-numeric Song.id '${song.id}'") + } else { + lyricsDao.insert( + com.theveloper.pixelplay.data.database.LyricsEntity( + songId = songIdLong, + content = rawLyricsToSave, + isSynced = !best.lyrics.synced.isNullOrEmpty(), + source = "remote" + ) + ) + } } catch (e: NumberFormatException) { Log.w(TAG, "Skipping DB update for non-numeric ID: ${song.id}") } @@ -1336,9 +1367,14 @@ class LyricsRepositoryImpl @Inject constructor( } try { + val songIdLong = song.id.toLongOrNull() + if (songIdLong == null) { + Log.w(TAG, "Cannot persist lyrics to DB: non-numeric Song.id '${song.id}'") + return@withContext Result.failure(LyricsException("Non-numeric Song.id")) + } lyricsDao.insert( com.theveloper.pixelplay.data.database.LyricsEntity( - songId = song.id.toLong(), + songId = songIdLong, content = rawLyricsToSave, isSynced = !parsedLyrics.synced.isNullOrEmpty(), source = "remote" @@ -1352,7 +1388,7 @@ class LyricsRepositoryImpl @Inject constructor( saveLocalLyricsJson(song, parsedLyrics) LogUtils.d(this@LyricsRepositoryImpl, "Fetched and cached remote lyrics (exact match) for: ${song.title}") - Result.success(Pair(parsedLyrics, rawLyricsToSave)) + return@withContext Result.success(Pair(parsedLyrics, rawLyricsToSave)) } else { LogUtils.d(this@LyricsRepositoryImpl, "No lyrics found remotely for: ${song.title}") Result.failure(NoLyricsFoundException()) @@ -1388,6 +1424,24 @@ class LyricsRepositoryImpl @Inject constructor( lrcLibApiService.searchLyrics(trackName = cleanTitle, artistName = cleanArtist) }) + // Romanized search strategy for non-Latin titles + if (MultiLangRomanizer.isScriptThatNeedsRomanization(cleanTitle)) { + val romanTitle = romanizeForMatch(cleanTitle) + if (romanTitle != cleanTitle) { + add(RemoteSearchStrategy("romanized_track") { + lrcLibApiService.searchLyrics(trackName = romanTitle, artistName = cleanArtist) + }) + } + } + if (MultiLangRomanizer.isScriptThatNeedsRomanization(cleanArtist)) { + val romanArtist = romanizeForMatch(cleanArtist) + if (romanArtist != cleanArtist) { + add(RemoteSearchStrategy("romanized_artist") { + lrcLibApiService.searchLyrics(trackName = cleanTitle, artistName = romanArtist) + }) + } + } + // Smart title cleanup strategy val smartTitle = cleanTitleSmart(cleanTitle) if (smartTitle != cleanTitle && smartTitle.isNotBlank()) { @@ -1535,7 +1589,9 @@ class LyricsRepositoryImpl @Inject constructor( // Also remove JSON cache try { val file = File(context.filesDir, "lyrics/${songId}.json") - if (file.exists()) file.delete() + if (file.exists() && !file.delete()) { + Log.w(TAG, "Failed to delete JSON cache for songId: $songId") + } } catch (e: Exception) { Log.w(TAG, "Error deleting JSON cache: ${e.message}") } @@ -1613,8 +1669,8 @@ class LyricsRepositoryImpl @Inject constructor( // Strategy 2: Artist - Title if (foundFile == null) { - val cleanArtist = song.displayArtist.replace(Regex("[^a-zA-Z0-9]"), "_") - val cleanTitle = song.title.replace(Regex("[^a-zA-Z0-9]"), "_") + val cleanArtist = song.displayArtist.replace(Regex("[^\\p{L}\\p{N}]"), "_") + val cleanTitle = song.title.replace(Regex("[^\\p{L}\\p{N}]"), "_") for (extension in LyricsImportSecurity.supportedFileExtensions()) { val altMatch = File(directory, "${cleanArtist}_${cleanTitle}.$extension") if (altMatch.exists() && altMatch.canRead()) { @@ -1628,14 +1684,19 @@ class LyricsRepositoryImpl @Inject constructor( val validated = readValidatedLocalLyrics(foundFile) if (validated != null) { try { - lyricsDao.insert( - com.theveloper.pixelplay.data.database.LyricsEntity( - songId = song.id.toLong(), - content = validated.sanitizedContent, - isSynced = validated.parsedLyrics.synced?.isNotEmpty() == true, - source = "local_file" - ) - ) + val songIdLong = song.id.toLongOrNull() + if (songIdLong == null) { + Log.w(TAG, "Cannot persist lyrics to DB: non-numeric Song.id '${song.id}'") + } else { + lyricsDao.insert( + com.theveloper.pixelplay.data.database.LyricsEntity( + songId = songIdLong, + content = validated.sanitizedContent, + isSynced = validated.parsedLyrics.synced?.isNotEmpty() == true, + source = "local_file" + ) + ) + } updatedCount.incrementAndGet() LogUtils.d(this@LyricsRepositoryImpl, "Auto-assigned lyrics from ${foundFile.name}") } catch (e: Exception) { @@ -1710,6 +1771,8 @@ class LyricsRepositoryImpl @Inject constructor( MultiLangRomanizer.isJapanese(text) -> MultiLangRomanizer.romanizeJapanese(text) ?: text MultiLangRomanizer.isChinese(text) -> MultiLangRomanizer.romanizeChinese(text) ?: text MultiLangRomanizer.isKorean(text) -> MultiLangRomanizer.romanizeKorean(text) + MultiLangRomanizer.isCyrillic(text) -> MultiLangRomanizer.romanizeCyrillic(text) ?: text + MultiLangRomanizer.isVietnamese(text) -> MultiLangRomanizer.romanizeVietnamese(text) else -> text } } diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/components/LyricsSheet.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/components/LyricsSheet.kt index 9787826e8f..7b6ddc37ad 100644 --- a/app/src/main/java/com/theveloper/pixelplay/presentation/components/LyricsSheet.kt +++ b/app/src/main/java/com/theveloper/pixelplay/presentation/components/LyricsSheet.kt @@ -513,8 +513,11 @@ fun LyricsSheet( immersiveMode = false } - LaunchedEffect(currentSong, lyrics, isLoadingLyrics) { - if (currentSong != null && lyrics == null && !isLoadingLyrics) { + LaunchedEffect(currentSong, lyrics, isLoadingLyrics, lyricsSearchUiState) { + if (lyricsSearchUiState is LyricsSearchUiState.Success) { + // Search completed successfully — close the dialog + showFetchLyricsDialog = false + } else if (currentSong != null && lyrics == null && !isLoadingLyrics) { // Only show dialog if reset was not just triggered if (!wasResetTriggered) { showFetchLyricsDialog = true diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/LyricsStateHolder.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/LyricsStateHolder.kt index 87985fd6eb..19ae900724 100644 --- a/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/LyricsStateHolder.kt +++ b/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/LyricsStateHolder.kt @@ -344,22 +344,16 @@ class LyricsStateHolder @Inject constructor( fun translateLyricsViaAi(currentSong: Song, lyricsObj: Lyrics?, cb: LyricsTranslationCallbacks) { val songId = currentSong.id.toLongOrNull() ?: return - if (lyricsObj?.synced != null) { - val hasValidTranslation = lyricsObj.synced.any { !it.translation.isNullOrBlank() } - if (hasValidTranslation) { - _messageEvents.tryEmit(cb.getString(R.string.lyrics_translate_already_translated)) - return - } - } - scope?.launch { _messageEvents.emit(cb.getString(R.string.lyrics_translate_progress)) + // Always read fresh lyrics from source — never rely on the potentially + // stale Lyrics object from UI state which may contain old translations. val rawLyrics = withContext(Dispatchers.IO) { - currentSong.lyrics?.takeIf { it.isNotBlank() } - ?: readLocalLyricsFile(currentSong) + readLocalLyricsFile(currentSong) ?: readEmbeddedLyricsFromFile(currentSong) ?: musicRepository.getStoredLyrics(currentSong)?.second + ?: currentSong.lyrics?.takeIf { it.isNotBlank() } } if (rawLyrics.isNullOrBlank()) { @@ -428,17 +422,18 @@ class LyricsStateHolder @Inject constructor( } private fun readEmbeddedLyricsFromFile(song: Song): String? { - song.lyrics - ?.trim() - ?.takeIf { it.isNotBlank() } - ?.let { return it } - - return runCatching { + val fileLyrics = runCatching { AudioMetadataReader.read(File(song.path)) ?.lyrics ?.trim() ?.takeIf { it.isNotBlank() } }.getOrNull() + + if (fileLyrics != null) return fileLyrics + + return song.lyrics + ?.trim() + ?.takeIf { it.isNotBlank() } } private fun readLocalLyricsFile(song: Song): String? { diff --git a/app/src/main/java/com/theveloper/pixelplay/utils/LyricsUtils.kt b/app/src/main/java/com/theveloper/pixelplay/utils/LyricsUtils.kt index 3c44e7d595..d225c7e178 100644 --- a/app/src/main/java/com/theveloper/pixelplay/utils/LyricsUtils.kt +++ b/app/src/main/java/com/theveloper/pixelplay/utils/LyricsUtils.kt @@ -48,6 +48,7 @@ import com.theveloper.pixelplay.data.model.SyncedLine import com.theveloper.pixelplay.data.model.SyncedWord import kotlinx.coroutines.flow.Flow +import java.text.Normalizer import java.util.Locale import java.util.regex.Pattern import kotlin.math.PI @@ -119,8 +120,17 @@ object MultiLangRomanizer { fun isPunjabi(text: String) = text.any { it in '\u0A00'..'\u0A7F' } fun isCyrillic(text: String) = text.any { it in '\u0400'..'\u04FF' } fun isChinese(text: String) = text.any { it in '\u4E00'..'\u9FFF' } + fun isThai(text: String) = text.any { it in '\u0E00'..'\u0E7F' } + fun isArabic(text: String) = text.any { it in '\u0600'..'\u06FF' } + fun isGreek(text: String) = text.any { it in '\u0370'..'\u03FF' } + fun isHebrew(text: String) = text.any { it in '\u0590'..'\u05FF' } + fun isVietnamese(text: String): Boolean { + return text.any { it in '\u1EA0'..'\u1EF9' } || + text.any { it in "\u0102\u0103\u0110\u0111\u01A0\u01A1\u01AF\u01B0" } + } fun isScriptThatNeedsRomanization(text: String): Boolean { + if (isVietnamese(text) || isThai(text) || isArabic(text) || isGreek(text) || isHebrew(text)) return true return text.any { char -> val code = char.code code in 0x3040..0x309F || // Hiragana @@ -133,8 +143,8 @@ object MultiLangRomanizer { } } - private fun isRussian(text: String) = text.any { RUSSIAN_CYRILLIC_LETTERS.contains(it.toString()) } && text.all { RUSSIAN_CYRILLIC_LETTERS.contains(it.toString()) || !it.toString().matches("[\\u0400-\\u04FF]".toRegex()) } - private fun isUkrainian(text: String) = text.any { UKRAINIAN_CYRILLIC_LETTERS.contains(it.toString()) || UKRAINIAN_SPECIFIC_CYRILLIC_LETTERS.contains(it.toString()) } && text.all { UKRAINIAN_CYRILLIC_LETTERS.contains(it.toString()) || UKRAINIAN_SPECIFIC_CYRILLIC_LETTERS.contains(it.toString()) || !it.toString().matches("[\\u0400-\\u04FF]".toRegex()) } + internal fun isRussian(text: String) = text.any { RUSSIAN_CYRILLIC_LETTERS.contains(it.toString()) } && text.all { RUSSIAN_CYRILLIC_LETTERS.contains(it.toString()) || !it.toString().matches("[\\u0400-\\u04FF]".toRegex()) } + internal fun isUkrainian(text: String) = text.any { UKRAINIAN_CYRILLIC_LETTERS.contains(it.toString()) || UKRAINIAN_SPECIFIC_CYRILLIC_LETTERS.contains(it.toString()) } && text.all { UKRAINIAN_CYRILLIC_LETTERS.contains(it.toString()) || UKRAINIAN_SPECIFIC_CYRILLIC_LETTERS.contains(it.toString()) || !it.toString().matches("[\\u0400-\\u04FF]".toRegex()) } private fun isSerbian(text: String) = text.any { SERBIAN_CYRILLIC_LETTERS.contains(it.toString()) || SERBIAN_SPECIFIC_CYRILLIC_LETTERS.contains(it.toString()) } && text.all { SERBIAN_CYRILLIC_LETTERS.contains(it.toString()) || SERBIAN_SPECIFIC_CYRILLIC_LETTERS.contains(it.toString()) || !it.toString().matches("[\\u0400-\\u04FF]".toRegex()) } private fun isBulgarian(text: String) = text.any { BULGARIAN_CYRILLIC_LETTERS.contains(it.toString()) } && text.all { BULGARIAN_CYRILLIC_LETTERS.contains(it.toString()) || !it.toString().matches("[\\u0400-\\u04FF]".toRegex()) } private fun isBelarusian(text: String) = text.any { BELARUSIAN_CYRILLIC_LETTERS.contains(it.toString()) || BELARUSIAN_SPECIFIC_CYRILLIC_LETTERS.contains(it.toString()) } && text.all { BELARUSIAN_CYRILLIC_LETTERS.contains(it.toString()) || BELARUSIAN_SPECIFIC_CYRILLIC_LETTERS.contains(it.toString()) || !it.toString().matches("[\\u0400-\\u04FF]".toRegex()) } @@ -547,6 +557,22 @@ object MultiLangRomanizer { } } + fun romanizeVietnamese(text: String): String { + if (text.isEmpty()) return text + val normalized = Normalizer.normalize(text, Normalizer.Form.NFKD) + val sb = StringBuilder(text.length) + for (char in normalized) { + when { + char == '\u0111' -> sb.append('d') + char == '\u0110' -> sb.append('D') + char in '\u0300'..'\u036F' || char in '\uFE20'..'\uFE2F' -> {} + char.code > 0x7E && char in '\u00C0'..'\u024F' -> {} + else -> sb.append(char) + } + } + return sb.toString() + } + private fun processCyrillicWordByWord(text: String, specificMap: Map, isRussian: Boolean = false): String { val romajiBuilder = StringBuilder(text.length) val words = text.split("((?<=\\s|[.,!?;])|(?=\\s|[.,!?;]))".toRegex()).filter { it.isNotEmpty() } From ef7c4daa8576bdba38209a47ec3eac74a65ecc8e Mon Sep 17 00:00:00 2001 From: Dae Euhwa Date: Mon, 27 Jul 2026 20:20:02 -0500 Subject: [PATCH 57/73] feat(ai): AI overhaul - providers, prompts, settings, romanization - Shared AI OkHttpClient, FLAC fallback, provider deduplication - Configurable base URLs and streamlined base URL handling - SearchableProviderSelector with descriptions and live search - Split AI settings into AI Provider tab and Generation Parameters tab - LYRICS prompt type for translation logging - MultiLangRomanizer support for multiple script systems - Server URL utilities for multi-provider support - Cleanup of stale AI state and error propagation --- app/build.gradle.kts | 1 - .../theveloper/pixelplay/data/ai/AiHandler.kt | 63 ++++++++++---- .../pixelplay/data/ai/AiResponseCleaner.kt | 19 ++++- .../pixelplay/data/ai/AiSystemPromptEngine.kt | 11 +++ .../pixelplay/data/ai/GeminiModelService.kt | 18 ++-- .../pixelplay/data/ai/provider/AiClient.kt | 5 -- .../data/ai/provider/AiClientFactory.kt | 55 +++++++----- .../pixelplay/data/ai/provider/AiProvider.kt | 32 +++---- .../data/ai/provider/GeminiAiClient.kt | 49 +---------- .../data/ai/provider/GenericOpenAiClient.kt | 18 ++-- .../backup/module/AiUsageBackupHandler.kt | 2 +- .../pixelplay/data/database/AiUsageDao.kt | 3 - .../preferences/AiPreferencesRepository.kt | 2 +- .../pixelplay/data/preferences/AppLanguage.kt | 5 +- .../preferences/UserPreferencesRepository.kt | 23 +---- .../com/theveloper/pixelplay/di/Qualifiers.kt | 7 ++ .../presentation/screens/AiUsageComponents.kt | 2 + .../presentation/viewmodel/AiStateHolder.kt | 84 ++++++++++++++++--- .../pixelplay/utils/ServerUrlUtils.kt | 46 ++++++++++ 19 files changed, 272 insertions(+), 173 deletions(-) create mode 100644 app/src/main/java/com/theveloper/pixelplay/utils/ServerUrlUtils.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 9f2583065f..806448f975 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -325,7 +325,6 @@ dependencies { // UI Utilities & Extra implementation(libs.timber) - implementation(libs.generativeai) implementation(libs.smooth.corner.rect.android.compose) implementation(libs.reorderables) implementation(libs.codeview) diff --git a/app/src/main/java/com/theveloper/pixelplay/data/ai/AiHandler.kt b/app/src/main/java/com/theveloper/pixelplay/data/ai/AiHandler.kt index d9da7840b3..6bdd82c551 100644 --- a/app/src/main/java/com/theveloper/pixelplay/data/ai/AiHandler.kt +++ b/app/src/main/java/com/theveloper/pixelplay/data/ai/AiHandler.kt @@ -1,8 +1,11 @@ package com.theveloper.pixelplay.data.ai +import com.theveloper.pixelplay.data.ai.provider.AiClient import com.theveloper.pixelplay.data.ai.provider.AiClientFactory import com.theveloper.pixelplay.data.ai.provider.AiProvider +import com.theveloper.pixelplay.data.ai.provider.AiProviderException +import com.theveloper.pixelplay.data.ai.provider.AiProviderSupport import com.theveloper.pixelplay.data.database.AiCacheDao import com.theveloper.pixelplay.data.database.AiCacheEntity import com.theveloper.pixelplay.data.preferences.AiPreferencesRepository @@ -12,9 +15,11 @@ import com.theveloper.pixelplay.di.AppScope import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch +import kotlinx.coroutines.TimeoutCancellationException import kotlinx.coroutines.withTimeout import timber.log.Timber import java.security.MessageDigest +import java.util.concurrent.ConcurrentHashMap import javax.inject.Inject import javax.inject.Singleton @@ -27,9 +32,9 @@ class AiHandler @Inject constructor( private val promptEngine: AiSystemPromptEngine, @AppScope private val appScope: CoroutineScope ) { - // Cooldown timer: Provider -> Expiry Timestamp - private val providerCooldowns = mutableMapOf() - private val COOLDOWN_DURATION_MS = 1000L * 60 * 5 // 5 minutes + // Cooldown timer: Provider -> Expiry Timestamp (thread-safe, auto-cleans expired) + private val providerCooldowns = ConcurrentHashMap() + private val COOLDOWN_DURATION_MS = 1000L * 60 * 2 // 2 minutes (was 5) // Cache TTL: 30 minutes — prevents stale results from being served indefinitely private val CACHE_TTL_MS = 1000L * 60 * 30 @@ -97,7 +102,13 @@ class AiHandler @Inject constructor( presencePenalty: Float, frequencyPenalty: Float, ): GenerationResult { - val client = clientFactory.createClient(provider, apiKey) + val client = if (provider.hasConfigurableUrl) { + val configuredUrl = preferencesRepo.getBaseUrl(provider).first() + if (configuredUrl.isNotBlank()) clientFactory.createClientWithUrl(provider, apiKey, configuredUrl) + else clientFactory.createClient(provider, apiKey) + } else { + clientFactory.createClient(provider, apiKey) + } val requestedModel = getModel(provider).ifBlank { client.getDefaultModel() } suspend fun callWithModel(model: String): String { @@ -108,8 +119,8 @@ class AiHandler @Inject constructor( topP, topK, maxTokens, presencePenalty, frequencyPenalty, ) } - } catch (e: kotlinx.coroutines.TimeoutCancellationException) { - throw com.theveloper.pixelplay.data.ai.provider.AiProviderSupport.createException( + } catch (e: TimeoutCancellationException) { + throw AiProviderSupport.createException( providerName = provider.displayName, statusCode = null, transportMessage = "Request timed out after ${REQUEST_TIMEOUT_MS / 1000}s. The model may be overloaded.", @@ -123,7 +134,7 @@ class AiHandler @Inject constructor( val response = callWithModel(requestedModel) GenerationResult(response, requestedModel) } catch (e: Exception) { - val failure = com.theveloper.pixelplay.data.ai.provider.AiProviderSupport.wrapThrowable( + val failure = AiProviderSupport.wrapThrowable( provider.displayName, e, requestedModel ) @@ -140,13 +151,13 @@ class AiHandler @Inject constructor( provider: AiProvider, apiKey: String, requestedModel: String, - client: com.theveloper.pixelplay.data.ai.provider.AiClient, - failure: com.theveloper.pixelplay.data.ai.provider.AiProviderException + client: AiClient, + failure: AiProviderException ): String? { if (!failure.isModelUnavailable()) return null val availableModels = runCatching { client.getAvailableModels(apiKey) }.getOrDefault(emptyList()) - val recoveredModel = com.theveloper.pixelplay.data.ai.provider.AiProviderSupport.selectRecoveryModel( + val recoveredModel = AiProviderSupport.selectRecoveryModel( currentModel = requestedModel, defaultModel = client.getDefaultModel(), availableModels = availableModels @@ -163,6 +174,17 @@ class AiHandler @Inject constructor( context: String = "" ): String { val params = getGenerationParams() + val effectiveMaxTokens = if (type == AiSystemPromptType.LYRICS) { + val estimatedInputChars = prompt.length + // Translation doubles output (original + translated per line), plus formatting overhead + val estimatedOutputChars = estimatedInputChars * 3 + val cjkRatio = prompt.count { it.code in 0x4E00..0x9FFF || it.code in 0x3040..0x30FF || it.code in 0xAC00..0xD7AF || it.code in 0x0E00..0x0E7F || it.code in 0x0600..0x06FF || it.code in 0x0370..0x03FF || it.code in 0x0590..0x05FF || it.code in 0x0900..0x097F || it.code in 0x0A00..0x0A7F }.toFloat() / estimatedInputChars.coerceAtLeast(1) + val charsPerToken = (4f - cjkRatio * 3f).coerceIn(1f, 4f) + val estimatedOutputTokens = (estimatedOutputChars / charsPerToken).toInt().coerceAtLeast(8192) + estimatedOutputTokens.coerceAtMost(32768) + } else { + params.maxTokens + } val effectiveTemperature = if (params.temperature == 0.7f) { if (temperature == 0.7f) { when (type) { @@ -171,6 +193,7 @@ class AiHandler @Inject constructor( AiSystemPromptType.TAGGING -> 0.4f AiSystemPromptType.PLAYLIST, AiSystemPromptType.DAILY_MIX -> 0.6f AiSystemPromptType.PERSONA -> 0.85f + AiSystemPromptType.LYRICS -> 0.7f AiSystemPromptType.GENERAL -> 0.7f } } else temperature @@ -191,9 +214,12 @@ class AiHandler @Inject constructor( } } - val providersToTry = com.theveloper.pixelplay.data.ai.provider.AiProviderSupport.buildProviderChain(userProvider) - val failedProviders = mutableListOf() + // Clean up expired cooldowns so stale entries never accumulate val now = System.currentTimeMillis() + providerCooldowns.entries.removeIf { it.value < now } + + val providersToTry = AiProviderSupport.buildProviderChain(userProvider) + val failedProviders = mutableListOf() for (provider in providersToTry) { val cooldownExpiry = providerCooldowns[provider] ?: 0L @@ -204,7 +230,7 @@ class AiHandler @Inject constructor( try { val apiKey = getApiKey(provider) - if (apiKey.isBlank()) { + if (apiKey.isBlank() && provider.requiresApiKey) { failedProviders.add("${provider.name}: no API key configured") continue } @@ -220,7 +246,7 @@ class AiHandler @Inject constructor( temperature = effectiveTemperature, topP = params.topP, topK = params.topK, - maxTokens = params.maxTokens, + maxTokens = effectiveMaxTokens, presencePenalty = params.presencePenalty, frequencyPenalty = params.frequencyPenalty, ) @@ -230,7 +256,7 @@ class AiHandler @Inject constructor( continue } - val isThinkingModel = finalSystemPrompt.contains("think", true) || provider.name.contains("reasoning", true) + val isThinkingModel = result.modelUsed.contains("think", true) || result.modelUsed.contains("reason", true) || provider.name.contains("reasoning", true) val estimatedPromptTokens = (finalSystemPrompt.length + prompt.length) / 4 val estimatedOutputTokens = result.response.length / 4 val estimatedThoughtTokens = if (isThinkingModel) (estimatedOutputTokens * 1.5).toInt() else 0 @@ -254,10 +280,15 @@ class AiHandler @Inject constructor( } cacheDao.insert(AiCacheEntity(promptHash = hash, responseJson = result.response, timestamp = System.currentTimeMillis())) + + // Evict cache entries older than 7 days + val sevenDaysAgo = System.currentTimeMillis() - (7 * 24 * 60 * 60 * 1000L) + try { cacheDao.clearOldCache(sevenDaysAgo) } catch (_: Exception) {} + return result.response } catch (e: Exception) { // AI Optimization: Robust failover logic—if one provider fails, we log and try the next in the chain - val failure = com.theveloper.pixelplay.data.ai.provider.AiProviderSupport.wrapThrowable(provider.displayName, e) + val failure = AiProviderSupport.wrapThrowable(provider.displayName, e) Timber.tag("AiHandler").w(e, "Provider ${provider.name} failed: ${failure.message}") failedProviders.add("${provider.name}: ${failure.message ?: "Unknown error"}") // Trigger cooldown only on provider-level outages and account problems. diff --git a/app/src/main/java/com/theveloper/pixelplay/data/ai/AiResponseCleaner.kt b/app/src/main/java/com/theveloper/pixelplay/data/ai/AiResponseCleaner.kt index 92d6e27de8..2e315e4f72 100644 --- a/app/src/main/java/com/theveloper/pixelplay/data/ai/AiResponseCleaner.kt +++ b/app/src/main/java/com/theveloper/pixelplay/data/ai/AiResponseCleaner.kt @@ -21,10 +21,27 @@ object AiResponseCleaner { } fun cleanTextResponse(raw: String): String { - return raw + var cleaned = raw .replace("```text", "") .replace("```", "") .trim() + + // Remove conversational framing lines that AIs sometimes prepend + val framingPrefixes = listOf( + "Here is", "Here's", "Here are", "Sure", "Certainly", + "Of course", "I've", "I have", "The translated", "Translation:", + "Translated lyrics:", "Output:" + ) + val framingPattern = framingPrefixes.joinToString("|") { Regex.escape(it) } + cleaned = cleaned.replace(Regex("^(?:$framingPattern).*\\n?", RegexOption.MULTILINE), "") + + // Strip paired markdown formatting markers (**text** or __text__) + cleaned = cleaned + .replace(Regex("\\*\\*(.*?)\\*\\*"), "$1") + .replace(Regex("__(.*?)__"), "$1") + .trim() + + return cleaned } fun extractJsonArray(text: String): String? { diff --git a/app/src/main/java/com/theveloper/pixelplay/data/ai/AiSystemPromptEngine.kt b/app/src/main/java/com/theveloper/pixelplay/data/ai/AiSystemPromptEngine.kt index 9edf9e4042..a103af1f07 100644 --- a/app/src/main/java/com/theveloper/pixelplay/data/ai/AiSystemPromptEngine.kt +++ b/app/src/main/java/com/theveloper/pixelplay/data/ai/AiSystemPromptEngine.kt @@ -11,6 +11,7 @@ enum class AiSystemPromptType { MOOD_ANALYSIS, PERSONA, DAILY_MIX, + LYRICS, GENERAL } @@ -182,6 +183,16 @@ class AiSystemPromptEngine @Inject constructor() { $dailyMixPersonaPrompt """.trimIndent() + AiSystemPromptType.LYRICS -> """ + Song lyrics translator — you translate lyrics between languages while preserving structure. + + - Preserve ALL timestamps and line structure exactly. + - Output each original line followed by its translation on the next line. + - Never add explanations, labels, or formatting beyond the requested format. + - If the source is already in the target language, respond with: ALREADY_IN_TARGET_LANGUAGE + + """.trimIndent() + AiSystemPromptType.GENERAL -> """ PixelPlayer Assistant — a knowledgeable music companion. diff --git a/app/src/main/java/com/theveloper/pixelplay/data/ai/GeminiModelService.kt b/app/src/main/java/com/theveloper/pixelplay/data/ai/GeminiModelService.kt index c97959f5c3..3fbbaf1c2e 100644 --- a/app/src/main/java/com/theveloper/pixelplay/data/ai/GeminiModelService.kt +++ b/app/src/main/java/com/theveloper/pixelplay/data/ai/GeminiModelService.kt @@ -1,5 +1,6 @@ package com.theveloper.pixelplay.data.ai +import com.theveloper.pixelplay.data.ai.provider.UnifiedModelFilter import com.theveloper.pixelplay.data.repository.MusicRepository import com.theveloper.pixelplay.data.worker.AiWorkerManager import kotlinx.coroutines.Dispatchers @@ -22,12 +23,7 @@ class GeminiModelService @Inject constructor( ) { companion object { - // Markers for models that cannot perform text chat generation. These are the - // only things we filter out — every other model the API returns is selectable. - private val NON_CHAT_MARKERS = listOf( - "embedding", "aqa", "imagen", "image-generation", - "tts", "audio", "veo", "vision-only", "learnlm-embedding" - ) + // Use UnifiedModelFilter for consistent model filtering across all providers. } suspend fun fetchAvailableModels(apiKey: String): Result> { @@ -48,10 +44,11 @@ class GeminiModelService @Inject constructor( private suspend fun makeModelsListRequest(apiKey: String): List { return withContext(Dispatchers.IO) { try { - val url = "https://generativelanguage.googleapis.com/v1beta/models?key=$apiKey" + val url = "https://generativelanguage.googleapis.com/v1beta/models" val connection = java.net.URL(url).openConnection() as java.net.HttpURLConnection connection.requestMethod = "GET" + connection.setRequestProperty("x-goog-api-key", apiKey) connection.connectTimeout = 10000 connection.readTimeout = 10000 @@ -88,7 +85,7 @@ class GeminiModelService @Inject constructor( // version — let the user pick any chat-capable model their key supports. if ((modelName.startsWith("gemini", ignoreCase = true) || modelName.startsWith("gemma", ignoreCase = true)) && - !isNonChatModel(modelName)) { + UnifiedModelFilter.isModelUsableForChat(modelName)) { models.add(GeminiModel( name = modelName, displayName = formatDisplayName(modelName) @@ -131,11 +128,6 @@ class GeminiModelService @Inject constructor( } } - private fun isNonChatModel(modelName: String): Boolean { - val lower = modelName.lowercase() - return NON_CHAT_MARKERS.any { lower.contains(it) } - } - private fun formatDisplayName(modelName: String): String { return modelName .split("-") diff --git a/app/src/main/java/com/theveloper/pixelplay/data/ai/provider/AiClient.kt b/app/src/main/java/com/theveloper/pixelplay/data/ai/provider/AiClient.kt index 348547ee68..430c425f4c 100644 --- a/app/src/main/java/com/theveloper/pixelplay/data/ai/provider/AiClient.kt +++ b/app/src/main/java/com/theveloper/pixelplay/data/ai/provider/AiClient.kt @@ -18,11 +18,6 @@ interface AiClient { frequencyPenalty: Float = 0.0f ): String - /** - * Estimate or count tokens for a given prompt - */ - suspend fun countTokens(model: String, systemPrompt: String, prompt: String): Int - /** * Get list of available models for this provider */ diff --git a/app/src/main/java/com/theveloper/pixelplay/data/ai/provider/AiClientFactory.kt b/app/src/main/java/com/theveloper/pixelplay/data/ai/provider/AiClientFactory.kt index 4322ac24e7..6eec6d11e6 100644 --- a/app/src/main/java/com/theveloper/pixelplay/data/ai/provider/AiClientFactory.kt +++ b/app/src/main/java/com/theveloper/pixelplay/data/ai/provider/AiClientFactory.kt @@ -1,5 +1,6 @@ package com.theveloper.pixelplay.data.ai.provider +import okhttp3.OkHttpClient import javax.inject.Inject import javax.inject.Singleton @@ -7,7 +8,9 @@ import javax.inject.Singleton * Factory for creating AI client instances based on provider type */ @Singleton -class AiClientFactory @Inject constructor() { +class AiClientFactory @Inject constructor( + @com.theveloper.pixelplay.di.AiOkHttpClient private val sharedHttpClient: OkHttpClient +) { /** * Create an AI client for the specified provider @@ -16,77 +19,87 @@ class AiClientFactory @Inject constructor() { * @return AiClient instance */ fun createClient(provider: AiProvider, apiKey: String): AiClient { - if (apiKey.isBlank()) { + if (apiKey.isBlank() && provider.requiresApiKey) { throw IllegalArgumentException("API Key cannot be blank for ${provider.displayName}") } return when (provider) { - AiProvider.GEMINI -> GeminiAiClient(apiKey) + AiProvider.GEMINI -> GeminiAiClient(apiKey, sharedHttpClient) AiProvider.DEEPSEEK -> GenericOpenAiClient( apiKey = apiKey, - baseUrl = "https://api.deepseek.com", + baseUrl = "https://api.deepseek.com/v1", defaultModelId = "deepseek-chat", - providerName = "DeepSeek" + providerName = provider.displayName, + client = sharedHttpClient ) AiProvider.GROQ -> GenericOpenAiClient( apiKey = apiKey, baseUrl = "https://api.groq.com/openai/v1", - defaultModelId = "llama-3.1-8b-instant", - providerName = "Groq" + defaultModelId = "llama-3.3-70b-versatile", + providerName = provider.displayName, + client = sharedHttpClient ) AiProvider.MISTRAL -> GenericOpenAiClient( apiKey = apiKey, baseUrl = "https://api.mistral.ai/v1", - defaultModelId = "mistral-large-latest", - providerName = "Mistral" + defaultModelId = "mistral-large-2411", + providerName = provider.displayName, + client = sharedHttpClient ) AiProvider.NVIDIA -> GenericOpenAiClient( apiKey = apiKey, baseUrl = "https://integrate.api.nvidia.com/v1", - defaultModelId = "meta/llama-3.1-8b-instruct", - providerName = "NVIDIA NIM" + defaultModelId = "nvidia/llama-3.1-nemotron-70b-instruct", + providerName = provider.displayName, + client = sharedHttpClient ) AiProvider.KIMI -> GenericOpenAiClient( apiKey = apiKey, baseUrl = "https://api.moonshot.cn/v1", - defaultModelId = "moonshot-v1-8k", - providerName = "Moonshot Kimi" + defaultModelId = "moonshot-v1-auto", + providerName = provider.displayName, + client = sharedHttpClient ) AiProvider.GLM -> GenericOpenAiClient( apiKey = apiKey, baseUrl = "https://open.bigmodel.cn/api/paas/v4", - defaultModelId = "glm-4", - providerName = "Zhipu GLM" + defaultModelId = "glm-4-plus", + providerName = provider.displayName, + client = sharedHttpClient ) AiProvider.OPENAI -> GenericOpenAiClient( apiKey = apiKey, baseUrl = "https://api.openai.com/v1", defaultModelId = "gpt-4o-mini", - providerName = "OpenAI" + providerName = provider.displayName, + client = sharedHttpClient ) AiProvider.OPENROUTER -> GenericOpenAiClient( apiKey = apiKey, baseUrl = "https://openrouter.ai/api/v1", - defaultModelId = "google/gemini-2.0-flash-lite-preview-02-05:free", - providerName = "OpenRouter" + defaultModelId = "google/gemini-2.5-flash-preview-04-17:free", + providerName = provider.displayName, + client = sharedHttpClient ) AiProvider.OLLAMA -> GenericOpenAiClient( apiKey = apiKey, baseUrl = "https://api.ollama.ai/v1", defaultModelId = "llama3", - providerName = "Ollama" + providerName = provider.displayName, + client = sharedHttpClient ) AiProvider.CUSTOM -> GenericOpenAiClient( apiKey = apiKey, baseUrl = "", defaultModelId = "", - providerName = "Custom Provider" + providerName = provider.displayName, + client = sharedHttpClient ) } } fun createClientWithUrl(provider: AiProvider, apiKey: String, baseUrl: String): AiClient { val displayName = provider.displayName - return GenericOpenAiClient(apiKey, baseUrl.trimEnd('/'), "", displayName) + return GenericOpenAiClient(apiKey, baseUrl.trimEnd('/'), "", displayName, sharedHttpClient) } } diff --git a/app/src/main/java/com/theveloper/pixelplay/data/ai/provider/AiProvider.kt b/app/src/main/java/com/theveloper/pixelplay/data/ai/provider/AiProvider.kt index 229f1d3140..1b3caf0bd2 100644 --- a/app/src/main/java/com/theveloper/pixelplay/data/ai/provider/AiProvider.kt +++ b/app/src/main/java/com/theveloper/pixelplay/data/ai/provider/AiProvider.kt @@ -1,20 +1,22 @@ package com.theveloper.pixelplay.data.ai.provider -/** - * Enum representing available AI providers - */ -enum class AiProvider(val displayName: String, val requiresApiKey: Boolean, val hasConfigurableUrl: Boolean = false) { - GEMINI("Google Gemini", requiresApiKey = true), - DEEPSEEK("DeepSeek", requiresApiKey = true), - GROQ("Groq", requiresApiKey = true), - MISTRAL("Mistral", requiresApiKey = true), - NVIDIA("NVIDIA NIM", requiresApiKey = true), - KIMI("Kimi (Moonshot)", requiresApiKey = true), - GLM("Zhipu GLM", requiresApiKey = true), - OPENAI("OpenAI", requiresApiKey = true), - OPENROUTER("OpenRouter", requiresApiKey = true), - OLLAMA("Ollama", requiresApiKey = true), - CUSTOM("Custom Provider", requiresApiKey = true, hasConfigurableUrl = true); +enum class AiProvider( + val displayName: String, + val requiresApiKey: Boolean, + val hasConfigurableUrl: Boolean = false, + val description: String = "" +) { + GEMINI("Google Gemini", requiresApiKey = true, description = "Gemini 2.5 Pro/Flash — Google's latest multimodal models"), + DEEPSEEK("DeepSeek", requiresApiKey = true, description = "DeepSeek-V3 & R1 — competitive open-weight reasoning models"), + GROQ("Groq", requiresApiKey = true, description = "Llama 3, Mixtral, Gemma — ultra-fast LPU inference"), + MISTRAL("Mistral", requiresApiKey = true, description = "Mistral Large, Small, Codestral — efficient European models"), + NVIDIA("NVIDIA NIM", requiresApiKey = true, description = "NVIDIA-optimized Llama, Nemotron, and community models"), + KIMI("Kimi (Moonshot)", requiresApiKey = true, description = "Kimi k1.5 — long-context reasoning by Moonshot AI"), + GLM("Zhipu GLM", requiresApiKey = true, description = "GLM-4 — bilingual (Chinese/English) models by Zhipu AI"), + OPENAI("OpenAI", requiresApiKey = true, description = "GPT-4o, GPT-4.1, o3, o4-mini — industry-standard models"), + OPENROUTER("OpenRouter", requiresApiKey = true, description = "Single API for 300+ models across all major providers"), + OLLAMA("Ollama", requiresApiKey = true, hasConfigurableUrl = true, description = "Ollama-compatible API endpoint (configurable URL)"), + CUSTOM("Custom Provider", requiresApiKey = true, hasConfigurableUrl = true, description = "Any OpenAI-compatible API endpoint"); companion object { fun fromString(value: String): AiProvider { diff --git a/app/src/main/java/com/theveloper/pixelplay/data/ai/provider/GeminiAiClient.kt b/app/src/main/java/com/theveloper/pixelplay/data/ai/provider/GeminiAiClient.kt index cee7e23a45..27a4cce85d 100644 --- a/app/src/main/java/com/theveloper/pixelplay/data/ai/provider/GeminiAiClient.kt +++ b/app/src/main/java/com/theveloper/pixelplay/data/ai/provider/GeminiAiClient.kt @@ -9,7 +9,6 @@ import okhttp3.MediaType.Companion.toMediaType import okhttp3.OkHttpClient import okhttp3.Request import okhttp3.RequestBody.Companion.toRequestBody -import java.util.concurrent.TimeUnit /** * Gemini provider implementation. @@ -19,21 +18,13 @@ import java.util.concurrent.TimeUnit * SDK. This keeps generation in lockstep with whatever models the API actually serves, * so any model the user selects works as long as their key supports it. */ -class GeminiAiClient(private val apiKey: String) : AiClient { +class GeminiAiClient(private val apiKey: String, private val httpClient: OkHttpClient) : AiClient { companion object { private const val DEFAULT_GEMINI_MODEL = "gemini-3.1-flash-lite" private const val BASE_URL = "https://generativelanguage.googleapis.com/v1beta" - - } - private val httpClient = OkHttpClient.Builder() - .connectTimeout(30, TimeUnit.SECONDS) - .readTimeout(60, TimeUnit.SECONDS) - .writeTimeout(30, TimeUnit.SECONDS) - .build() - private val json = Json { ignoreUnknownKeys = true isLenient = true @@ -165,44 +156,6 @@ class GeminiAiClient(private val apiKey: String) : AiClient { } } - override suspend fun countTokens(model: String, systemPrompt: String, prompt: String): Int { - return withContext(Dispatchers.IO) { - val resolvedModel = model.ifBlank { DEFAULT_GEMINI_MODEL } - try { - val requestBody = GenerateRequest( - contents = listOf(Content(role = "user", parts = listOf(Part(prompt)))), - systemInstruction = systemPrompt - .takeIf { it.isNotBlank() } - ?.let { Content(parts = listOf(Part(it))) }, - generationConfig = GenerationConfig(temperature = 0.0) - ) - val jsonBody = json.encodeToString(GenerateRequest.serializer(), requestBody) - val body = jsonBody.toRequestBody("application/json".toMediaType()) - - val request = Request.Builder() - .url("$BASE_URL/models/$resolvedModel:countTokens") - .addHeader("x-goog-api-key", apiKey) - .addHeader("Content-Type", "application/json") - .post(body) - .build() - - httpClient.newCall(request).execute().use { response -> - if (!response.isSuccessful) { - return@withContext (prompt.length / 4) + (systemPrompt.length / 4) - } - val totalTokens = Regex(""""totalTokens"\s*:\s*(\d+)""") - .find(response.body.string()) - ?.groupValues - ?.getOrNull(1) - ?.toIntOrNull() - totalTokens ?: ((prompt.length / 4) + (systemPrompt.length / 4)) - } - } catch (e: Exception) { - (prompt.length / 4) + (systemPrompt.length / 4) - } - } - } - override suspend fun getAvailableModels(apiKey: String): List { return withContext(Dispatchers.IO) { try { diff --git a/app/src/main/java/com/theveloper/pixelplay/data/ai/provider/GenericOpenAiClient.kt b/app/src/main/java/com/theveloper/pixelplay/data/ai/provider/GenericOpenAiClient.kt index fd0fb1c1d8..609c9b5685 100644 --- a/app/src/main/java/com/theveloper/pixelplay/data/ai/provider/GenericOpenAiClient.kt +++ b/app/src/main/java/com/theveloper/pixelplay/data/ai/provider/GenericOpenAiClient.kt @@ -18,7 +18,12 @@ class GenericOpenAiClient( private val apiKey: String, private val baseUrl: String, private val defaultModelId: String, - private val providerName: String = "OpenAI" + private val providerName: String = "OpenAI", + private val client: OkHttpClient = OkHttpClient.Builder() + .connectTimeout(30, TimeUnit.SECONDS) + .readTimeout(60, TimeUnit.SECONDS) + .writeTimeout(30, TimeUnit.SECONDS) + .build() ) : AiClient { @Serializable @@ -47,12 +52,6 @@ class GenericOpenAiClient( @Serializable private data class ModelsResponse(val data: List) - private val client = OkHttpClient.Builder() - .connectTimeout(30, TimeUnit.SECONDS) - .readTimeout(60, TimeUnit.SECONDS) - .writeTimeout(30, TimeUnit.SECONDS) - .build() - private val json = Json { ignoreUnknownKeys = true isLenient = true @@ -132,11 +131,6 @@ class GenericOpenAiClient( } } - override suspend fun countTokens(model: String, systemPrompt: String, prompt: String): Int { - // Estimation for generic providers - return (systemPrompt.length + prompt.length) / 4 - } - override suspend fun getAvailableModels(apiKey: String): List { return withContext(Dispatchers.IO) { try { diff --git a/app/src/main/java/com/theveloper/pixelplay/data/backup/module/AiUsageBackupHandler.kt b/app/src/main/java/com/theveloper/pixelplay/data/backup/module/AiUsageBackupHandler.kt index 6805e692fd..7fbf6792c1 100644 --- a/app/src/main/java/com/theveloper/pixelplay/data/backup/module/AiUsageBackupHandler.kt +++ b/app/src/main/java/com/theveloper/pixelplay/data/backup/module/AiUsageBackupHandler.kt @@ -34,7 +34,7 @@ class AiUsageBackupHandler @Inject constructor( // We additive restore AI logs or clear? Usually for logs we additive restore // but BackupModuleHandler documentation says "Clear existing data and restore". - aiUsageDao.clearAll() + aiUsageDao.clearUsage() if (logs.isNotEmpty()) { aiUsageDao.insertAll(logs) } diff --git a/app/src/main/java/com/theveloper/pixelplay/data/database/AiUsageDao.kt b/app/src/main/java/com/theveloper/pixelplay/data/database/AiUsageDao.kt index 28a7503f34..705c3f2eec 100644 --- a/app/src/main/java/com/theveloper/pixelplay/data/database/AiUsageDao.kt +++ b/app/src/main/java/com/theveloper/pixelplay/data/database/AiUsageDao.kt @@ -33,7 +33,4 @@ interface AiUsageDao { @Query("DELETE FROM ai_usage") suspend fun clearUsage() - - @Query("DELETE FROM ai_usage") - suspend fun clearAll() } diff --git a/app/src/main/java/com/theveloper/pixelplay/data/preferences/AiPreferencesRepository.kt b/app/src/main/java/com/theveloper/pixelplay/data/preferences/AiPreferencesRepository.kt index 964875d040..32339a44cd 100644 --- a/app/src/main/java/com/theveloper/pixelplay/data/preferences/AiPreferencesRepository.kt +++ b/app/src/main/java/com/theveloper/pixelplay/data/preferences/AiPreferencesRepository.kt @@ -152,7 +152,7 @@ class AiPreferencesRepository @Inject constructor( dataStore.data.map { preferences -> preferences[Keys.AI_TOP_K] ?: 64 } val aiMaxTokens: Flow = - dataStore.data.map { preferences -> preferences[Keys.AI_MAX_TOKENS] ?: 4096 } + dataStore.data.map { preferences -> preferences[Keys.AI_MAX_TOKENS] ?: 8192 } val aiPresencePenalty: Flow = dataStore.data.map { preferences -> preferences[Keys.AI_PRESENCE_PENALTY] ?: 0.0f } diff --git a/app/src/main/java/com/theveloper/pixelplay/data/preferences/AppLanguage.kt b/app/src/main/java/com/theveloper/pixelplay/data/preferences/AppLanguage.kt index 5630700f90..e926bda366 100644 --- a/app/src/main/java/com/theveloper/pixelplay/data/preferences/AppLanguage.kt +++ b/app/src/main/java/com/theveloper/pixelplay/data/preferences/AppLanguage.kt @@ -12,11 +12,14 @@ enum class AppLanguage(val tag: String, @StringRes val labelRes: Int) { FRENCH("fr", R.string.settings_language_french), INDONESIAN("in", R.string.settings_language_indonesian), ITALIAN("it", R.string.settings_language_italian), + JAPANESE("ja", R.string.settings_language_japanese), KOREAN("ko", R.string.settings_language_korean), NORWEGIAN_BOKMAL("nb", R.string.settings_language_norwegian_bokmal), RUSSIAN("ru", R.string.settings_language_russian), SIMPLIFIED_CHINESE("zh-CN", R.string.settings_language_chinese), - TURKISH("tr", R.string.settings_language_turkish); + TRADITIONAL_CHINESE("zh-TW", R.string.settings_language_traditional_chinese), + TURKISH("tr", R.string.settings_language_turkish), + VIETNAMESE("vi", R.string.settings_language_vietnamese); companion object { val supportedLanguageTags: Set = values().map { it.tag }.toSet() diff --git a/app/src/main/java/com/theveloper/pixelplay/data/preferences/UserPreferencesRepository.kt b/app/src/main/java/com/theveloper/pixelplay/data/preferences/UserPreferencesRepository.kt index 4598f1a86f..d353c6c118 100644 --- a/app/src/main/java/com/theveloper/pixelplay/data/preferences/UserPreferencesRepository.kt +++ b/app/src/main/java/com/theveloper/pixelplay/data/preferences/UserPreferencesRepository.kt @@ -507,32 +507,11 @@ class UserPreferencesRepository @Inject constructor( dataStore.edit { it[PreferencesKeys.GLOBAL_TRANSITION_SETTINGS] = json.encodeToString(settings) } } - // ─── Favorites ──────────────────────────────────────────────────────────── + // ─── Favorites (Legacy — only used for one-time migration) ───────────────── val favoriteSongIdsFlow: Flow> = pref { it[PreferencesKeys.FAVORITE_SONG_IDS] ?: emptySet() } - /** - * Adds or removes [songId] from favorites depending on [isFavorite]. - * Prefer this over [toggleFavoriteSong] when the desired state is known. - */ - suspend fun setFavoriteSong(songId: String, isFavorite: Boolean) { - dataStore.edit { preferences -> - val current = preferences[PreferencesKeys.FAVORITE_SONG_IDS] ?: emptySet() - preferences[PreferencesKeys.FAVORITE_SONG_IDS] = - if (isFavorite) current + songId else current - songId - } - } - - /** Toggles [songId] in the favorites set. */ - suspend fun toggleFavoriteSong(songId: String) { - dataStore.edit { preferences -> - val current = preferences[PreferencesKeys.FAVORITE_SONG_IDS] ?: emptySet() - preferences[PreferencesKeys.FAVORITE_SONG_IDS] = - if (songId in current) current - songId else current + songId - } - } - suspend fun clearFavoriteSongIds() { dataStore.edit { it[PreferencesKeys.FAVORITE_SONG_IDS] = emptySet() } } diff --git a/app/src/main/java/com/theveloper/pixelplay/di/Qualifiers.kt b/app/src/main/java/com/theveloper/pixelplay/di/Qualifiers.kt index d0207b1997..e978cd7ce8 100644 --- a/app/src/main/java/com/theveloper/pixelplay/di/Qualifiers.kt +++ b/app/src/main/java/com/theveloper/pixelplay/di/Qualifiers.kt @@ -29,3 +29,10 @@ annotation class BackupGson @Qualifier @Retention(AnnotationRetention.BINARY) annotation class AppScope + +/** + * Qualifier for shared AI OkHttpClient instance. + */ +@Qualifier +@Retention(AnnotationRetention.BINARY) +annotation class AiOkHttpClient diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/screens/AiUsageComponents.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/screens/AiUsageComponents.kt index 8c9a9b17fa..97933c5dd1 100644 --- a/app/src/main/java/com/theveloper/pixelplay/presentation/screens/AiUsageComponents.kt +++ b/app/src/main/java/com/theveloper/pixelplay/presentation/screens/AiUsageComponents.kt @@ -49,6 +49,7 @@ fun AiUsageDateHeader(date: String) { } @Composable +@Suppress("NonObservableLocale") fun AiUsageLogItem( usage: AiUsageEntity ) { @@ -178,6 +179,7 @@ private fun formatPromptType(type: String): String { "MOOD_ANALYSIS" -> "Analysis" "PERSONA" -> "Persona" "DAILY_MIX" -> "Daily Mix" + "LYRICS" -> "Lyrics" "GENERAL" -> "General" else -> type.lowercase().replaceFirstChar { it.uppercase() } } diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/AiStateHolder.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/AiStateHolder.kt index ad68cea838..3dee09de7c 100644 --- a/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/AiStateHolder.kt +++ b/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/AiStateHolder.kt @@ -6,10 +6,12 @@ import com.theveloper.pixelplay.R import com.theveloper.pixelplay.data.DailyMixManager import com.theveloper.pixelplay.data.ai.AiNotificationManager import com.theveloper.pixelplay.data.ai.AiPlaylistGenerator +import com.theveloper.pixelplay.data.ai.AiResponseCleaner import com.theveloper.pixelplay.data.ai.AiSystemPromptType import com.theveloper.pixelplay.data.ai.provider.AiProviderException import com.theveloper.pixelplay.data.preferences.PlaylistPreferencesRepository import com.theveloper.pixelplay.data.model.Song +import com.theveloper.pixelplay.utils.MultiLangRomanizer import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.MutableStateFlow @@ -17,6 +19,7 @@ import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch import timber.log.Timber +import java.util.Locale import javax.inject.Inject import javax.inject.Singleton @@ -286,39 +289,94 @@ class AiStateHolder @Inject constructor( suspend fun translateLyrics(lyricsText: String): Result { return try { - val targetLanguage = context.resources.configuration.locales[0].displayLanguage - val prompt = """ -Translate song lyrics into $targetLanguage. + val targetLanguage = context.resources.configuration.locales[0] + .getDisplayLanguage(Locale.US) + val sourceLanguage = detectLyricsLanguage(lyricsText) + val hasTimestamps = lyricsText.contains(Regex("\\[\\d{1,2}:\\d{2}")) + val taskPrefix = if (sourceLanguage != null) { + "Translate these $sourceLanguage song lyrics into $targetLanguage." + } else { + "Translate these song lyrics into $targetLanguage. Detect the source language from the lyrics content." + } + val formatSection = if (hasTimestamps) { + """ + +For SYNCED lyrics — output TWO lines per original line: +[original timestamp] original text +[same timestamp] translated text + +Preserve ALL timestamps [mm:ss.xx] exactly — never modify, merge, or drop them. + + """.trimIndent() + } else { + """ + +For PLAIN lyrics (no timestamps) — output TWO lines per original line: +original text +translated text + +Keep the same line order as the original. + + """.trimIndent() + } + val xmlPrompt = """ +$taskPrefix -- Preserve ALL timestamps [mm:ss.xx] exactly — never modify, merge, or drop them. -- Output TWO lines per original line: the original, then the translation with the same timestamp. +- Output TWO lines per original line: the original, then the translation. - NEVER add explanations, labels, numbering, section headers, or formatting. - NEVER remove, merge, split, or reorder lines. +- Translate ALL lines completely — do not skip or truncate any part. - If lyrics are ALREADY mostly in $targetLanguage, output ONLY: ALREADY_IN_TARGET_LANGUAGE - -[original timestamp] original text -[same timestamp] translated text - +$formatSection $lyricsText """.trimIndent() - + val response = aiHandler.generateContent( - prompt = prompt, - type = AiSystemPromptType.GENERAL, + prompt = xmlPrompt, + type = AiSystemPromptType.LYRICS, temperature = 0.1f ) - Result.success(response) + val cleaned = AiResponseCleaner.cleanTextResponse(response) + Result.success(cleaned) } catch (e: Exception) { Result.failure(e) } } + private val timestampRegex = Regex("\\[\\d{1,2}:\\d{2}(?:[.:]\\d{1,3})?]") + + private fun detectLyricsLanguage(lyricsText: String): String? { + val sample = timestampRegex.replace(lyricsText, "").trim().take(200) + if (sample.isBlank()) return null + + return when { + MultiLangRomanizer.isJapanese(sample) -> "Japanese" + MultiLangRomanizer.isKorean(sample) -> "Korean" + MultiLangRomanizer.isChinese(sample) -> "Chinese" + MultiLangRomanizer.isThai(sample) -> "Thai" + MultiLangRomanizer.isArabic(sample) -> "Arabic" + MultiLangRomanizer.isGreek(sample) -> "Greek" + MultiLangRomanizer.isHebrew(sample) -> "Hebrew" + MultiLangRomanizer.isHindi(sample) -> "Hindi" + MultiLangRomanizer.isPunjabi(sample) -> "Punjabi" + MultiLangRomanizer.isCyrillic(sample) -> { + when { + MultiLangRomanizer.isRussian(sample) -> "Russian" + MultiLangRomanizer.isUkrainian(sample) -> "Ukrainian" + else -> null + } + } + MultiLangRomanizer.isVietnamese(sample) -> "Vietnamese" + else -> null + } + } + fun onCleared() { scope = null allSongsProvider = null diff --git a/app/src/main/java/com/theveloper/pixelplay/utils/ServerUrlUtils.kt b/app/src/main/java/com/theveloper/pixelplay/utils/ServerUrlUtils.kt new file mode 100644 index 0000000000..5ac552e63c --- /dev/null +++ b/app/src/main/java/com/theveloper/pixelplay/utils/ServerUrlUtils.kt @@ -0,0 +1,46 @@ +package com.theveloper.pixelplay.utils + +import com.theveloper.pixelplay.data.stream.CloudStreamSecurity +import okhttp3.HttpUrl +import okhttp3.HttpUrl.Companion.toHttpUrlOrNull + +object ServerUrlUtils { + + data class NormalizedUrl( + val url: HttpUrl, + val original: String, + val validationError: String? = null + ) + + fun normalizeUrlOrNull(raw: String): HttpUrl? { + val trimmed = raw.trim().trimEnd('/') + val withScheme = if (!trimmed.startsWith("http://", ignoreCase = true) && + !trimmed.startsWith("https://", ignoreCase = true) + ) { + "https://$trimmed" + } else { + trimmed + } + return withScheme.toHttpUrlOrNull() + } + + fun normalizeUrlString(raw: String): String = + normalizeUrlOrNull(raw)?.toString()?.trimEnd('/') ?: raw.trim().trimEnd('/') + + fun validateConnectionUrl(parsed: HttpUrl): String? { + if (parsed.username.isNotEmpty() || parsed.password.isNotEmpty()) { + return "Server URL must not contain embedded credentials" + } + return null + } + + fun requireSecureForRemote(parsed: HttpUrl, service: String = "server"): String? { + if (parsed.username.isNotEmpty() || parsed.password.isNotEmpty()) { + return "Server URL must not contain embedded credentials" + } + if (!parsed.isHttps && !CloudStreamSecurity.isLocalOrPrivateHost(parsed.host)) { + return "Use https:// for remote $service servers. HTTP is only allowed for local network addresses." + } + return null + } +} From ffbd8105c2e3f0fb4023218cc48a52a24d024c5f Mon Sep 17 00:00:00 2001 From: Dae Euhwa Date: Mon, 27 Jul 2026 20:20:32 -0500 Subject: [PATCH 58/73] fix(lint): address pre-existing lint errors and manifest/compilation fix - Fix 21 pre-existing lint errors - Add lint baseline configuration - AndroidManifest updates for external player - MainActivity fixes --- app/lint.xml | 20 +++++++++++++++++++ app/src/main/AndroidManifest.xml | 4 ++++ .../pixelplay/ExternalPlayerActivity.kt | 1 + .../com/theveloper/pixelplay/MainActivity.kt | 4 ++++ .../pixelplay/data/database/SongEntity.kt | 4 ++-- .../data/media/SongMetadataEditor.kt | 17 ++++++++++++---- .../viewmodel/PlayerViewModelTest.kt | 1 - 7 files changed, 44 insertions(+), 7 deletions(-) create mode 100644 app/lint.xml diff --git a/app/lint.xml b/app/lint.xml new file mode 100644 index 0000000000..6fb7e7f3ea --- /dev/null +++ b/app/lint.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 1afb6f4044..46a821a28e 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -73,6 +73,10 @@ + + + + diff --git a/app/src/main/java/com/theveloper/pixelplay/ExternalPlayerActivity.kt b/app/src/main/java/com/theveloper/pixelplay/ExternalPlayerActivity.kt index 2d993383df..3ffd21abfe 100644 --- a/app/src/main/java/com/theveloper/pixelplay/ExternalPlayerActivity.kt +++ b/app/src/main/java/com/theveloper/pixelplay/ExternalPlayerActivity.kt @@ -117,6 +117,7 @@ class ExternalPlayerActivity : ComponentActivity() { return intent.data } + @Suppress("WrongConstant") private fun persistUriPermissionIfNeeded(intent: Intent, uri: Uri) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { val hasPersistablePermission = intent.flags and Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION != 0 diff --git a/app/src/main/java/com/theveloper/pixelplay/MainActivity.kt b/app/src/main/java/com/theveloper/pixelplay/MainActivity.kt index 6931cb6ca8..5733e8360e 100644 --- a/app/src/main/java/com/theveloper/pixelplay/MainActivity.kt +++ b/app/src/main/java/com/theveloper/pixelplay/MainActivity.kt @@ -10,6 +10,7 @@ import android.content.Context import android.content.ComponentName import android.content.Intent import android.os.Build +import androidx.annotation.RequiresApi import android.graphics.RenderEffect as AndroidRenderEffect import android.graphics.Shader as AndroidShader import androidx.compose.ui.graphics.asComposeRenderEffect @@ -944,6 +945,7 @@ class MainActivity : ComponentActivity() { val expansionFractionProvider = remember(playerViewModel.playerContentExpansionFraction) { { playerViewModel.playerContentExpansionFraction.value } } + @Suppress("NewApi") val blurEffectCache = remember { BlurEffectCache() } Box( @@ -1153,10 +1155,12 @@ class MainActivity : ComponentActivity() { * blur every animation frame. The radius is quantized at the call site, so this * only rebuilds ~25 times across the whole expand animation instead of 60+/sec. */ +@RequiresApi(Build.VERSION_CODES.S) private class BlurEffectCache { private var lastRadiusPx: Float = Float.NaN private var cached: androidx.compose.ui.graphics.RenderEffect? = null + @RequiresApi(Build.VERSION_CODES.S) fun get(radiusPx: Float): androidx.compose.ui.graphics.RenderEffect? { if (radiusPx <= 0f) { lastRadiusPx = 0f diff --git a/app/src/main/java/com/theveloper/pixelplay/data/database/SongEntity.kt b/app/src/main/java/com/theveloper/pixelplay/data/database/SongEntity.kt index 19f948b792..744f61cf0b 100644 --- a/app/src/main/java/com/theveloper/pixelplay/data/database/SongEntity.kt +++ b/app/src/main/java/com/theveloper/pixelplay/data/database/SongEntity.kt @@ -217,7 +217,7 @@ fun List.toSongs(): List { // filePath y parentDirectoryPath se poblarán desde MediaStore en el SyncWorker. fun Song.toEntity(filePathFromMediaStore: String, parentDirFromMediaStore: String): SongEntity { return SongEntity( - id = this.id.toLong(), + id = this.id.toLongOrNull() ?: throw IllegalArgumentException("Cannot convert Song.id '${this.id}' to Long"), title = this.title, artistName = this.artist, artistId = this.artistId, @@ -256,7 +256,7 @@ data class SongSummary( // (menos probable que se use si la entidad siempre requiere los paths) fun Song.toEntityWithoutPaths(): SongEntity { return SongEntity( - id = this.id.toLong(), + id = this.id.toLongOrNull() ?: throw IllegalArgumentException("Cannot convert Song.id '${this.id}' to Long"), title = this.title, artistName = this.artist, artistId = this.artistId, diff --git a/app/src/main/java/com/theveloper/pixelplay/data/media/SongMetadataEditor.kt b/app/src/main/java/com/theveloper/pixelplay/data/media/SongMetadataEditor.kt index e1f7c41eb6..41d1f5435d 100644 --- a/app/src/main/java/com/theveloper/pixelplay/data/media/SongMetadataEditor.kt +++ b/app/src/main/java/com/theveloper/pixelplay/data/media/SongMetadataEditor.kt @@ -769,10 +769,8 @@ class SongMetadataEditor( Timber.tag(TAG) .w("TAGLIB: Skipping file modification for high-resolution FLAC (${flacResult.sampleRate}Hz, ${flacResult.bitsPerSample}bit)") Timber.tag(TAG) - .w("TAGLIB: High-res FLAC files may not work correctly with TagLib. Will update MediaStore only.") - // Return true to indicate we should proceed with MediaStore-only update - // The calling code will still update MediaStore and local DB - return true + .w("TAGLIB: High-res FLAC files may not work correctly with TagLib. Falling back to JAudioTagger.") + return false } else -> { /* Continue with normal processing */ } } @@ -963,6 +961,17 @@ class SongMetadataEditor( } audioFile.commit() + + // Verify the write succeeded for FLAC files which can be problematic + if (targetFile.extension.equals("flac", ignoreCase = true)) { + val verifyFile = AudioFileIO.read(targetFile) + val verifyTag = verifyFile?.tag + if (verifyTag == null || verifyTag.getFirst(FieldKey.TITLE) != newTitle) { + Timber.tag(TAG).w("JAUDIOTAGGER: FLAC verification failed - tag not persisted, falling back to TagLib") + return false + } + } + Timber.tag(TAG).d("JAUDIOTAGGER: SUCCESS - Updated file metadata: $filePath") true } catch (e: Exception) { diff --git a/app/src/test/java/com/theveloper/pixelplay/presentation/viewmodel/PlayerViewModelTest.kt b/app/src/test/java/com/theveloper/pixelplay/presentation/viewmodel/PlayerViewModelTest.kt index 7996811c67..5ff6390142 100644 --- a/app/src/test/java/com/theveloper/pixelplay/presentation/viewmodel/PlayerViewModelTest.kt +++ b/app/src/test/java/com/theveloper/pixelplay/presentation/viewmodel/PlayerViewModelTest.kt @@ -205,7 +205,6 @@ class PlayerViewModelTest { every { mockMusicRepository.getAudioFiles() } returns flowOf(emptyList()) every { mockMusicRepository.getDistinctAlbumArtSongs() } returns flowOf(emptyList()) every { mockMusicRepository.getHomeMixPreviewSongs(any()) } returns flowOf(emptyList()) - every { mockMusicRepository.getSongCountFlow() } returns flowOf(0) every { mockMusicRepository.getCloudSongCountFlow() } returns flowOf(0) every { mockMusicRepository.searchSongs(any(), any()) } returns flowOf(emptyList()) every { mockMusicRepository.getMusicByGenre(any()) } returns flowOf(emptyList()) From 869696593dda5bd70aed2dd934943383eba840db Mon Sep 17 00:00:00 2001 From: Dae Euhwa Date: Mon, 27 Jul 2026 20:20:26 -0500 Subject: [PATCH 59/73] feat(settings): restructure settings, UI improvements - Split AI settings into AI Provider tab and Generation Parameters tab - Add LYRICS settings category - Settings screen restructuring and components - Library screen loading indicators and optimizations - Search screen improvements - Player view model fixes --- .../pixelplay/data/database/MusicDao.kt | 16 +- .../data/repository/MusicRepository.kt | 6 - .../data/repository/MusicRepositoryImpl.kt | 4 - .../components/PlaylistCreationDialogs.kt | 26 ++ .../components/player/FullPlayerContent.kt | 1 - .../components/subcomps/LibraryActionRow.kt | 2 +- .../presentation/model/SettingsCategory.kt | 32 +- .../presentation/screens/AiUsageComponents.kt | 2 + .../presentation/screens/LibraryScreen.kt | 8 +- .../presentation/screens/SearchScreen.kt | 46 +-- .../screens/SettingsCategoryScreen.kt | 355 ++++++++++-------- .../screens/SettingsComponents.kt | 180 +++++++++ .../presentation/screens/SettingsScreen.kt | 4 + .../presentation/viewmodel/AiStateHolder.kt | 84 ++++- .../viewmodel/LibraryStateHolder.kt | 23 +- .../presentation/viewmodel/PlayerViewModel.kt | 46 +-- .../viewmodel/SettingsViewModel.kt | 199 +--------- .../PlayerControlActionCallback.kt | 6 +- 18 files changed, 566 insertions(+), 474 deletions(-) diff --git a/app/src/main/java/com/theveloper/pixelplay/data/database/MusicDao.kt b/app/src/main/java/com/theveloper/pixelplay/data/database/MusicDao.kt index 8a141e350e..05a05deb08 100644 --- a/app/src/main/java/com/theveloper/pixelplay/data/database/MusicDao.kt +++ b/app/src/main/java/com/theveloper/pixelplay/data/database/MusicDao.kt @@ -1631,20 +1631,8 @@ interface MusicDao { suspend fun deleteOrphanedArtists() // --- Favorite Operations --- - @Query("UPDATE songs SET is_favorite = :isFavorite WHERE id = :songId") - suspend fun setFavoriteStatus(songId: Long, isFavorite: Boolean) - - @Query("SELECT is_favorite FROM songs WHERE id = :songId") - suspend fun getFavoriteStatus(songId: Long): Boolean? - - // Transaction to toggle favorite status - @Transaction - suspend fun toggleFavoriteStatus(songId: Long): Boolean { - val currentStatus = getFavoriteStatus(songId) ?: false // Default to false if not found (should not happen for existing song) - val newStatus = !currentStatus - setFavoriteStatus(songId, newStatus) - return newStatus - } + // Note: Favorite status is managed via FavoritesDao. The songs.is_favorite column + // is kept in sync by SQL triggers on the favorites table. @Query(""" UPDATE songs diff --git a/app/src/main/java/com/theveloper/pixelplay/data/repository/MusicRepository.kt b/app/src/main/java/com/theveloper/pixelplay/data/repository/MusicRepository.kt index 36d9081b99..d7bcb635f5 100644 --- a/app/src/main/java/com/theveloper/pixelplay/data/repository/MusicRepository.kt +++ b/app/src/main/java/com/theveloper/pixelplay/data/repository/MusicRepository.kt @@ -77,12 +77,6 @@ interface MusicRepository { storageFilter: com.theveloper.pixelplay.data.model.StorageFilter = com.theveloper.pixelplay.data.model.StorageFilter.ALL ): Flow - /** - * Returns the count of songs in the library. - * @return Flow emitting the current song count. - */ - fun getSongCountFlow(): Flow - /** * Returns the count of cloud songs in the library. */ diff --git a/app/src/main/java/com/theveloper/pixelplay/data/repository/MusicRepositoryImpl.kt b/app/src/main/java/com/theveloper/pixelplay/data/repository/MusicRepositoryImpl.kt index 5f026af8d9..11a16bd1e6 100644 --- a/app/src/main/java/com/theveloper/pixelplay/data/repository/MusicRepositoryImpl.kt +++ b/app/src/main/java/com/theveloper/pixelplay/data/repository/MusicRepositoryImpl.kt @@ -310,10 +310,6 @@ class MusicRepositoryImpl @Inject constructor( return songRepository.getFavoriteSongCountFlow(storageFilter) } - override fun getSongCountFlow(): Flow { - return musicDao.getSongCount().distinctUntilChanged() - } - override fun getCloudSongCountFlow(): Flow { return musicDao.getCloudSongCount().distinctUntilChanged() } diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/components/PlaylistCreationDialogs.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/components/PlaylistCreationDialogs.kt index e1af93fbcc..e08e975844 100644 --- a/app/src/main/java/com/theveloper/pixelplay/presentation/components/PlaylistCreationDialogs.kt +++ b/app/src/main/java/com/theveloper/pixelplay/presentation/components/PlaylistCreationDialogs.kt @@ -35,6 +35,7 @@ import androidx.compose.foundation.layout.navigationBars import androidx.compose.foundation.layout.width import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons @@ -301,6 +302,7 @@ private fun CreationModeCard( fun CreateAiPlaylistDialog( visible: Boolean, isGenerating: Boolean, + status: String?, error: String?, onDismiss: () -> Unit, onGenerate: (playlistName: String?, prompt: String, minLength: Int, maxLength: Int) -> Unit @@ -328,6 +330,7 @@ fun CreateAiPlaylistDialog( ) { CreateAiPlaylistContent( isGenerating = isGenerating, + status = status, error = error, onDismiss = onDismiss, onGenerate = onGenerate @@ -340,6 +343,7 @@ fun CreateAiPlaylistDialog( @Composable private fun CreateAiPlaylistContent( isGenerating: Boolean, + status: String?, error: String?, onDismiss: () -> Unit, onGenerate: (playlistName: String?, prompt: String, minLength: Int, maxLength: Int) -> Unit @@ -525,6 +529,28 @@ private fun CreateAiPlaylistContent( ) { Spacer(modifier = Modifier.height(4.dp)) + if (isGenerating && !status.isNullOrBlank()) { + Card( + shape = RoundedCornerShape(16.dp), + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.tertiaryContainer.copy(alpha = 0.7f) + ) + ) { + Row( + modifier = Modifier.padding(16.dp), + verticalAlignment = Alignment.CenterVertically + ) { + LoadingIndicator(modifier = Modifier.size(20.dp)) + Spacer(modifier = Modifier.width(12.dp)) + Text( + text = status, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onTertiaryContainer + ) + } + } + } + HeroAiCard() AiSectionCard( diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/components/player/FullPlayerContent.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/components/player/FullPlayerContent.kt index 8a30f6f41d..336588bda0 100644 --- a/app/src/main/java/com/theveloper/pixelplay/presentation/components/player/FullPlayerContent.kt +++ b/app/src/main/java/com/theveloper/pixelplay/presentation/components/player/FullPlayerContent.kt @@ -378,7 +378,6 @@ fun FullPlayerContent( if (showFetchLyricsDialog) { showFetchLyricsDialog = false showLyricsSheet = true - playerViewModel.resetLyricsSearchState() } } is LyricsSearchUiState.Error -> { diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/components/subcomps/LibraryActionRow.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/components/subcomps/LibraryActionRow.kt index fb04d8a3d8..9bb1eae421 100644 --- a/app/src/main/java/com/theveloper/pixelplay/presentation/components/subcomps/LibraryActionRow.kt +++ b/app/src/main/java/com/theveloper/pixelplay/presentation/components/subcomps/LibraryActionRow.kt @@ -82,6 +82,7 @@ import androidx.compose.ui.res.stringResource val defaultShape = RoundedCornerShape(26.dp) // Fallback shape val genHeight = 42.dp +@OptIn(ExperimentalMaterial3Api::class) @Composable fun LibraryActionRow( onMainActionClick: () -> Unit, @@ -352,7 +353,6 @@ fun LibraryActionRow( } val tooltipState = rememberTooltipState() - @OptIn(ExperimentalMaterial3Api::class) TooltipBox( positionProvider = TooltipDefaults.rememberTooltipPositionProvider(TooltipAnchorPosition.Above), tooltip = { diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/model/SettingsCategory.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/model/SettingsCategory.kt index e59c3d7a8c..5a3d1c7892 100644 --- a/app/src/main/java/com/theveloper/pixelplay/presentation/model/SettingsCategory.kt +++ b/app/src/main/java/com/theveloper/pixelplay/presentation/model/SettingsCategory.kt @@ -9,6 +9,8 @@ import androidx.compose.material.icons.rounded.Info import androidx.compose.material.icons.rounded.LibraryMusic import androidx.compose.material.icons.rounded.MusicNote import androidx.compose.material.icons.rounded.Palette +import androidx.compose.material.icons.rounded.QueueMusic +import androidx.compose.material.icons.rounded.Tune import androidx.compose.ui.graphics.vector.ImageVector import com.theveloper.pixelplay.R @@ -31,11 +33,23 @@ enum class SettingsCategory( subtitleRes = R.string.settings_category_appearance_subtitle, icon = Icons.Rounded.Palette ), + LYRICS( + id = "lyrics", + titleRes = R.string.settings_category_lyrics_title, + subtitleRes = R.string.settings_category_lyrics_subtitle, + icon = Icons.Rounded.QueueMusic + ), PLAYBACK( id = "playback", titleRes = R.string.settings_category_playback_title, subtitleRes = R.string.settings_category_playback_subtitle, - icon = Icons.Rounded.MusicNote // Using MusicNote again or maybe PlayCircle if available + icon = Icons.Rounded.MusicNote + ), + EQUALIZER( + id = "equalizer", + titleRes = R.string.settings_category_equalizer_title, + subtitleRes = R.string.settings_category_equalizer_subtitle, + icon = Icons.Rounded.GraphicEq ), BEHAVIOR( id = "behavior", @@ -49,6 +63,12 @@ enum class SettingsCategory( subtitleRes = R.string.settings_category_ai_subtitle, iconRes = R.drawable.gemini_ai ), + GENERATION_PARAMETERS( + id = "generation_parameters", + titleRes = R.string.settings_category_generation_parameters_title, + subtitleRes = R.string.settings_category_generation_parameters_subtitle, + icon = Icons.Rounded.Tune + ), BACKUP_RESTORE( id = "backup_restore", titleRes = R.string.settings_category_backup_title, @@ -61,17 +81,11 @@ enum class SettingsCategory( subtitleRes = R.string.settings_category_developer_subtitle, icon = Icons.Rounded.DeveloperMode ), - EQUALIZER( - id = "equalizer", - titleRes = R.string.settings_category_equalizer_title, - subtitleRes = R.string.settings_category_equalizer_subtitle, - icon = Icons.Rounded.GraphicEq - ), DEVICE_CAPABILITIES( id = "device_capabilities", titleRes = R.string.settings_category_device_capabilities_title, subtitleRes = R.string.settings_category_device_capabilities_subtitle, - icon = Icons.Rounded.DeveloperBoard // Placeholder, maybe Memory or SettingsInputComponent + icon = Icons.Rounded.DeveloperBoard ), ABOUT( id = "about", @@ -83,4 +97,6 @@ enum class SettingsCategory( companion object { fun fromId(id: String): SettingsCategory? = entries.find { it.id == id } } + + } diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/screens/AiUsageComponents.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/screens/AiUsageComponents.kt index 8c9a9b17fa..97933c5dd1 100644 --- a/app/src/main/java/com/theveloper/pixelplay/presentation/screens/AiUsageComponents.kt +++ b/app/src/main/java/com/theveloper/pixelplay/presentation/screens/AiUsageComponents.kt @@ -49,6 +49,7 @@ fun AiUsageDateHeader(date: String) { } @Composable +@Suppress("NonObservableLocale") fun AiUsageLogItem( usage: AiUsageEntity ) { @@ -178,6 +179,7 @@ private fun formatPromptType(type: String): String { "MOOD_ANALYSIS" -> "Analysis" "PERSONA" -> "Persona" "DAILY_MIX" -> "Daily Mix" + "LYRICS" -> "Lyrics" "GENERAL" -> "General" else -> type.lowercase().replaceFirstChar { it.uppercase() } } diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/screens/LibraryScreen.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/screens/LibraryScreen.kt index 5d37d029e9..e09c1d120d 100644 --- a/app/src/main/java/com/theveloper/pixelplay/presentation/screens/LibraryScreen.kt +++ b/app/src/main/java/com/theveloper/pixelplay/presentation/screens/LibraryScreen.kt @@ -493,6 +493,7 @@ fun LibraryScreen( }.collectAsStateWithLifecycle(initialValue = false) val hasActiveAiProviderApiKey by playerViewModel.hasActiveAiProviderApiKey.collectAsStateWithLifecycle() val isGeneratingAiPlaylist by playerViewModel.isGeneratingAiPlaylist.collectAsStateWithLifecycle() + val aiStatus by playerViewModel.aiStatus.collectAsStateWithLifecycle() val aiError by playerViewModel.aiError.collectAsStateWithLifecycle() var showCreatePlaylistDialog by remember { mutableStateOf(false) } var showPlaylistCreationTypeDialog by remember { mutableStateOf(false) } @@ -1002,11 +1003,11 @@ fun LibraryScreen( }.collectAsStateWithLifecycle(initialValue = LibraryScreenPlayerProjection()) val isLibraryContentEmpty by remember(playerViewModel) { combine( - playerViewModel.songCountFlow, + playerViewModel.allSongsFlow, playerViewModel.albumsFlow, playerViewModel.artistsFlow - ) { songCount, albums, artists -> - songCount == 0 && albums.isEmpty() && artists.isEmpty() + ) { songs, albums, artists -> + songs.isEmpty() && albums.isEmpty() && artists.isEmpty() }.distinctUntilChanged() }.collectAsStateWithLifecycle(initialValue = true) @@ -1800,6 +1801,7 @@ fun LibraryScreen( CreateAiPlaylistDialog( visible = showCreateAiPlaylistDialog && hasActiveAiProviderApiKey, isGenerating = isGeneratingAiPlaylist, + status = aiStatus, error = aiError, onDismiss = { showCreateAiPlaylistDialog = false diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/screens/SearchScreen.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/screens/SearchScreen.kt index eab38137e0..2831b36c1a 100644 --- a/app/src/main/java/com/theveloper/pixelplay/presentation/screens/SearchScreen.kt +++ b/app/src/main/java/com/theveloper/pixelplay/presentation/screens/SearchScreen.kt @@ -140,8 +140,7 @@ import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import racra.compose.smooth_corner_rect_library.AbsoluteSmoothCornerShape @@ -151,11 +150,6 @@ import androidx.compose.ui.res.stringResource private const val MAX_ALBUM_MULTI_SELECTION = 6 -private data class SearchUiSlice( - val selectedSearchFilter: SearchFilterType = SearchFilterType.ALL, - val searchResults: ImmutableList = persistentListOf() -) - @androidx.annotation.OptIn(UnstableApi::class) @OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class) @Composable @@ -181,7 +175,7 @@ fun SearchScreen( val multiSelectionState = playerViewModel.multiSelectionStateHolder val selectedSongs by multiSelectionState.selectedSongs.collectAsStateWithLifecycle() val isSongSelectionMode by multiSelectionState.isSelectionMode.collectAsStateWithLifecycle() - val selectedSongIds by multiSelectionState.selectedSongIds.collectAsStateWithLifecycle() + val selectedSongIds = remember(selectedSongs) { selectedSongs.map { it.id }.toSet() } var showMultiSelectionSheet by remember { mutableStateOf(false) } // Multi-selection state for albums @@ -194,7 +188,7 @@ fun SearchScreen( val playlistSelectionState = playerViewModel.playlistSelectionStateHolder val selectedPlaylists by playlistSelectionState.selectedPlaylists.collectAsStateWithLifecycle() val isPlaylistSelectionMode by playlistSelectionState.isSelectionMode.collectAsStateWithLifecycle() - val selectedPlaylistIds by playlistSelectionState.selectedPlaylistIds.collectAsStateWithLifecycle() + val selectedPlaylistIds = remember(selectedPlaylists) { selectedPlaylists.map { it.id }.toSet() } var showPlaylistMultiSelectionSheet by remember { mutableStateOf(false) } // Multi-selection state for genres @@ -224,18 +218,17 @@ fun SearchScreen( multiSelectionState.toggleSelection(song) } } - val searchUiState by remember(playerViewModel) { - playerViewModel.playerUiState - .map { uiState -> - SearchUiSlice( - selectedSearchFilter = uiState.selectedSearchFilter, - searchResults = uiState.searchResults - ) - } - .distinctUntilChanged() - }.collectAsStateWithLifecycle(initialValue = SearchUiSlice()) - val currentFilter = searchUiState.selectedSearchFilter - val genres by playerViewModel.genres.collectAsStateWithLifecycle() + val searchResults by playerViewModel.searchResults.collectAsStateWithLifecycle() + val currentFilter by playerViewModel.selectedSearchFilter.collectAsStateWithLifecycle() + + // Defer genre loading — only fetch when the genre section is about to be shown + var genres by remember { mutableStateOf>(persistentListOf()) } + val showGenreBrowse by remember(searchQuery) { derivedStateOf { searchQuery.isBlank() } } + LaunchedEffect(showGenreBrowse) { + if (showGenreBrowse) { + genres = playerViewModel.genres.first() + } + } val stablePlayerState by playerViewModel.stablePlayerState.collectAsStateWithLifecycle() val favoriteSongIds by playerViewModel.favoriteSongIds.collectAsStateWithLifecycle() val selectedSongForInfo by playerViewModel.selectedSongForInfo.collectAsStateWithLifecycle() @@ -257,9 +250,10 @@ fun SearchScreen( // Search debouncing is centralized in SearchStateHolder. LaunchedEffect(searchQuery, currentFilter) { - playerViewModel.performSearch(searchQuery) + if (searchQuery.isNotBlank()) { + playerViewModel.performSearch(searchQuery) + } } - val searchResults = searchUiState.searchResults val handleSongMoreOptionsClick: (Song) -> Unit = { song -> playerViewModel.selectSongForInfo(song) showSongInfoBottomSheet = true @@ -416,7 +410,6 @@ fun SearchScreen( } } - val showGenreBrowse by remember(searchQuery) { derivedStateOf { searchQuery.isBlank() } } AnimatedContent( targetState = showGenreBrowse, transitionSpec = { @@ -587,6 +580,7 @@ fun SearchScreen( }, currentPlayingSongId = stablePlayerState.currentSong?.id, isPlaying = stablePlayerState.isPlaying, + isShuffleEnabled = stablePlayerState.isShuffleEnabled, onSongMoreOptionsClick = handleSongMoreOptionsClick, navController = navController, isSelectionMode = isSongSelectionMode, @@ -1076,6 +1070,7 @@ fun SearchResultsList( onItemSelected: () -> Unit, currentPlayingSongId: String?, isPlaying: Boolean, + isShuffleEnabled: Boolean = false, onSongMoreOptionsClick: (Song) -> Unit, navController: NavHostController, isSelectionMode: Boolean = false, @@ -1094,7 +1089,6 @@ fun SearchResultsList( getPlaylistSelectionIndex: (String) -> Int? = { null } ) { val localDensity = LocalDensity.current - val playerStableState by playerViewModel.stablePlayerState.collectAsStateWithLifecycle() if (results.isEmpty()) { Box( @@ -1301,7 +1295,7 @@ fun SearchResultsList( songs.first(), item.playlist.name ) - if (playerStableState.isShuffleEnabled) playerViewModel.toggleShuffle() + if (isShuffleEnabled) playerViewModel.toggleShuffle() } else { playerViewModel.sendToast("Empty playlist") } diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/screens/SettingsCategoryScreen.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/screens/SettingsCategoryScreen.kt index dd1672dbc6..d23d652ca1 100644 --- a/app/src/main/java/com/theveloper/pixelplay/presentation/screens/SettingsCategoryScreen.kt +++ b/app/src/main/java/com/theveloper/pixelplay/presentation/screens/SettingsCategoryScreen.kt @@ -508,33 +508,6 @@ fun SettingsCategoryScreen( ) } - SettingsSubsection( - title = stringResource(R.string.settings_lyrics_management_section), - addBottomSpace = false - ) { - ThemeSelectorItem( - label = stringResource(R.string.settings_lyrics_source_priority_title), - description = stringResource(R.string.settings_lyrics_source_priority_subtitle), - options = mapOf( - LyricsSourcePreference.EMBEDDED_FIRST.name to stringResource(R.string.settings_lyrics_embedded_first), - LyricsSourcePreference.API_FIRST.name to stringResource(R.string.settings_lyrics_online_first), - LyricsSourcePreference.LOCAL_FIRST.name to stringResource(R.string.settings_lyrics_local_first) - ), - selectedKey = uiState.lyricsSourcePreference.name, - onSelectionChanged = { key -> - settingsViewModel.setLyricsSourcePreference( - LyricsSourcePreference.fromName(key) - ) - }, - leadingIcon = { Icon(painterResource(R.drawable.rounded_lyrics_24), null, tint = MaterialTheme.colorScheme.secondary) } - ) - SettingsItem( - title = stringResource(R.string.settings_reset_imported_lyrics_title), - subtitle = stringResource(R.string.settings_reset_imported_lyrics_subtitle), - leadingIcon = { Icon(Icons.Outlined.ClearAll, null, tint = MaterialTheme.colorScheme.secondary) }, - onClick = { showClearLyricsDialog = true } - ) - } } SettingsCategory.APPEARANCE -> { val useSmoothCorners by settingsViewModel.useSmoothCorners.collectAsStateWithLifecycle() @@ -679,32 +652,6 @@ fun SettingsCategoryScreen( ) } - SettingsSubsection(title = stringResource(R.string.settings_lyrics_screen_section)) { - SwitchSettingItem( - title = stringResource(R.string.settings_immersive_lyrics_title), - subtitle = stringResource(R.string.settings_immersive_lyrics_subtitle), - checked = uiState.immersiveLyricsEnabled, - onCheckedChange = { settingsViewModel.setImmersiveLyricsEnabled(it) }, - leadingIcon = { Icon(painterResource(R.drawable.rounded_lyrics_24), null, tint = MaterialTheme.colorScheme.secondary) } - ) - - if (uiState.immersiveLyricsEnabled) { - ThemeSelectorItem( - label = stringResource(R.string.settings_auto_hide_delay_title), - description = stringResource(R.string.settings_auto_hide_delay_subtitle), - options = mapOf( - "3000" to stringResource(R.string.settings_auto_hide_delay_3s), - "4000" to stringResource(R.string.settings_auto_hide_delay_4s), - "5000" to stringResource(R.string.settings_auto_hide_delay_5s), - "6000" to stringResource(R.string.settings_auto_hide_delay_6s) - ), - selectedKey = uiState.immersiveLyricsTimeout.toString(), - onSelectionChanged = { settingsViewModel.setImmersiveLyricsTimeout(it.toLong()) }, - leadingIcon = { Icon(Icons.Rounded.Timer, null, tint = MaterialTheme.colorScheme.secondary) } - ) - } - } - SettingsSubsection( title = stringResource(R.string.settings_app_navigation_section), addBottomSpace = false @@ -734,6 +681,61 @@ fun SettingsCategoryScreen( ) } } + SettingsCategory.LYRICS -> { + SettingsSubsection( + title = stringResource(R.string.settings_lyrics_management_section), + addBottomSpace = false + ) { + ThemeSelectorItem( + label = stringResource(R.string.settings_lyrics_source_priority_title), + description = stringResource(R.string.settings_lyrics_source_priority_subtitle), + options = mapOf( + LyricsSourcePreference.EMBEDDED_FIRST.name to stringResource(R.string.settings_lyrics_embedded_first), + LyricsSourcePreference.API_FIRST.name to stringResource(R.string.settings_lyrics_online_first), + LyricsSourcePreference.LOCAL_FIRST.name to stringResource(R.string.settings_lyrics_local_first) + ), + selectedKey = uiState.lyricsSourcePreference.name, + onSelectionChanged = { key -> + settingsViewModel.setLyricsSourcePreference( + LyricsSourcePreference.fromName(key) + ) + }, + leadingIcon = { Icon(painterResource(R.drawable.rounded_lyrics_24), null, tint = MaterialTheme.colorScheme.secondary) } + ) + SettingsItem( + title = stringResource(R.string.settings_reset_imported_lyrics_title), + subtitle = stringResource(R.string.settings_reset_imported_lyrics_subtitle), + leadingIcon = { Icon(Icons.Outlined.ClearAll, null, tint = MaterialTheme.colorScheme.secondary) }, + onClick = { showClearLyricsDialog = true } + ) + } + + SettingsSubsection(title = stringResource(R.string.settings_lyrics_screen_section)) { + SwitchSettingItem( + title = stringResource(R.string.settings_immersive_lyrics_title), + subtitle = stringResource(R.string.settings_immersive_lyrics_subtitle), + checked = uiState.immersiveLyricsEnabled, + onCheckedChange = { settingsViewModel.setImmersiveLyricsEnabled(it) }, + leadingIcon = { Icon(painterResource(R.drawable.rounded_lyrics_24), null, tint = MaterialTheme.colorScheme.secondary) } + ) + + if (uiState.immersiveLyricsEnabled) { + ThemeSelectorItem( + label = stringResource(R.string.settings_auto_hide_delay_title), + description = stringResource(R.string.settings_auto_hide_delay_subtitle), + options = mapOf( + "3000" to stringResource(R.string.settings_auto_hide_delay_3s), + "4000" to stringResource(R.string.settings_auto_hide_delay_4s), + "5000" to stringResource(R.string.settings_auto_hide_delay_5s), + "6000" to stringResource(R.string.settings_auto_hide_delay_6s) + ), + selectedKey = uiState.immersiveLyricsTimeout.toString(), + onSelectionChanged = { settingsViewModel.setImmersiveLyricsTimeout(it.toLong()) }, + leadingIcon = { Icon(Icons.Rounded.Timer, null, tint = MaterialTheme.colorScheme.secondary) } + ) + } + } + } SettingsCategory.PLAYBACK -> { SettingsSubsection(title = stringResource(R.string.settings_background_playback_section)) { ThemeSelectorItem( @@ -917,16 +919,16 @@ fun SettingsCategoryScreen( } SettingsCategory.AI_INTEGRATION -> { val provider = com.theveloper.pixelplay.data.ai.provider.AiProvider.fromString(aiProvider) - val currentCustomBaseUrl by settingsViewModel.customBaseUrl.collectAsStateWithLifecycle() + val currentAiBaseUrl by settingsViewModel.currentAiBaseUrl.collectAsStateWithLifecycle() // AI Provider Selection SettingsSubsection(title = stringResource(R.string.settings_ai_provider_section)) { - ThemeSelectorItem( + SearchableProviderSelector( label = stringResource(R.string.settings_ai_provider_title), description = stringResource(R.string.settings_ai_provider_subtitle), - options = com.theveloper.pixelplay.data.ai.provider.AiProvider.entries.associate { it.name to it.displayName }, - selectedKey = aiProvider, - onSelectionChanged = { settingsViewModel.onAiProviderChange(it) }, + providers = com.theveloper.pixelplay.data.ai.provider.AiProvider.entries, + selectedProvider = aiProvider, + onProviderSelected = { settingsViewModel.onAiProviderChange(it) }, leadingIcon = { Icon(Icons.Rounded.Science, null, tint = MaterialTheme.colorScheme.secondary) } ) SwitchSettingItem( @@ -1029,14 +1031,29 @@ fun SettingsCategoryScreen( if (provider.hasConfigurableUrl) { SettingsSubsection(title = "API Base URL") { AiApiKeyItem( - apiKey = currentCustomBaseUrl, - onApiKeySave = { settingsViewModel.onCustomBaseUrlChange(it) }, + apiKey = currentAiBaseUrl, + onApiKeySave = { settingsViewModel.onBaseUrlChange(it) }, title = "Base URL", subtitle = "e.g. https://api.example.com/v1" ) } } + Spacer(modifier = Modifier.height(16.dp)) + + AiUsageReportSection(settingsViewModel = settingsViewModel) + } + SettingsCategory.GENERATION_PARAMETERS -> { + val aiTemperature by settingsViewModel.aiTemperature.collectAsStateWithLifecycle() + val aiTopP by settingsViewModel.aiTopP.collectAsStateWithLifecycle() + val aiTopK by settingsViewModel.aiTopK.collectAsStateWithLifecycle() + val aiMaxTokens by settingsViewModel.aiMaxTokens.collectAsStateWithLifecycle() + val aiPresencePenalty by settingsViewModel.aiPresencePenalty.collectAsStateWithLifecycle() + val aiFrequencyPenalty by settingsViewModel.aiFrequencyPenalty.collectAsStateWithLifecycle() + val aiSampleSize by settingsViewModel.aiSampleSize.collectAsStateWithLifecycle() + val aiDigestMode by settingsViewModel.aiDigestMode.collectAsStateWithLifecycle() + val aiIncludeExtendedFields by settingsViewModel.aiIncludeExtendedFields.collectAsStateWithLifecycle() + // Prompt Behavior Section SettingsSubsection( title = stringResource(R.string.settings_prompt_behavior_section), @@ -1056,7 +1073,7 @@ fun SettingsCategoryScreen( SettingsSubsection(title = "Generation Parameters") { SliderSettingsItem( label = "Temperature", - value = settingsViewModel.aiTemperature.collectAsStateWithLifecycle().value, + value = aiTemperature, valueRange = 0.0f..2.0f, steps = 20, onValueChange = { settingsViewModel.onAiTemperatureChange(it) }, @@ -1070,7 +1087,7 @@ fun SettingsCategoryScreen( ) SliderSettingsItem( label = "Top P", - value = settingsViewModel.aiTopP.collectAsStateWithLifecycle().value, + value = aiTopP, valueRange = 0.0f..1.0f, steps = 20, onValueChange = { settingsViewModel.onAiTopPChange(it) }, @@ -1084,7 +1101,7 @@ fun SettingsCategoryScreen( ) SliderSettingsItem( label = "Top K", - value = settingsViewModel.aiTopK.collectAsStateWithLifecycle().value.toFloat(), + value = aiTopK.toFloat(), valueRange = 1f..100f, steps = 99, onValueChange = { settingsViewModel.onAiTopKChange(it.toInt()) }, @@ -1098,7 +1115,7 @@ fun SettingsCategoryScreen( ) SliderSettingsItem( label = "Max Output Tokens", - value = settingsViewModel.aiMaxTokens.collectAsStateWithLifecycle().value.toFloat(), + value = aiMaxTokens.toFloat(), valueRange = 128f..8192f, steps = 63, onValueChange = { settingsViewModel.onAiMaxTokensChange(it.toInt()) }, @@ -1112,7 +1129,7 @@ fun SettingsCategoryScreen( ) SliderSettingsItem( label = "Presence Penalty", - value = settingsViewModel.aiPresencePenalty.collectAsStateWithLifecycle().value, + value = aiPresencePenalty, valueRange = -2.0f..2.0f, steps = 40, onValueChange = { settingsViewModel.onAiPresencePenaltyChange(it) }, @@ -1126,7 +1143,7 @@ fun SettingsCategoryScreen( ) SliderSettingsItem( label = "Frequency Penalty", - value = settingsViewModel.aiFrequencyPenalty.collectAsStateWithLifecycle().value, + value = aiFrequencyPenalty, valueRange = -2.0f..2.0f, steps = 40, onValueChange = { settingsViewModel.onAiFrequencyPenaltyChange(it) }, @@ -1140,9 +1157,10 @@ fun SettingsCategoryScreen( ) } + Spacer(modifier = Modifier.height(16.dp)) + // Song Data Configuration Section SettingsSubsection(title = "Song Data Configuration") { - val aiSampleSize by settingsViewModel.aiSampleSize.collectAsStateWithLifecycle() SliderSettingsItem( label = "Sample Size", value = aiSampleSize.toFloat(), @@ -1161,7 +1179,7 @@ fun SettingsCategoryScreen( label = "Digest Detail", description = "Controls how much listening history data is included", options = mapOf("safe" to "Concise (faster)", "full" to "Full (better quality)"), - selectedKey = settingsViewModel.aiDigestMode.collectAsStateWithLifecycle().value, + selectedKey = aiDigestMode, onSelectionChanged = { settingsViewModel.onAiDigestModeChange(it) }, leadingIcon = { Icon( @@ -1174,7 +1192,7 @@ fun SettingsCategoryScreen( SwitchSettingItem( title = "Extended Song Fields", subtitle = "Include album, year, and genre info in song data sent to AI", - checked = settingsViewModel.aiIncludeExtendedFields.collectAsStateWithLifecycle().value, + checked = aiIncludeExtendedFields, onCheckedChange = { settingsViewModel.onAiIncludeExtendedFieldsChange(it) }, leadingIcon = { Icon( @@ -1188,103 +1206,8 @@ fun SettingsCategoryScreen( Spacer(modifier = Modifier.height(16.dp)) - SettingsSubsection(title = stringResource(R.string.settings_ai_usage_report_section)) { - val recentAiUsage by settingsViewModel.recentAiUsage.collectAsStateWithLifecycle() - val totalPromptTokens by settingsViewModel.totalPromptTokens.collectAsStateWithLifecycle() - val totalOutputTokens by settingsViewModel.totalOutputTokens.collectAsStateWithLifecycle() - val totalThoughtTokens by settingsViewModel.totalThoughtTokens.collectAsStateWithLifecycle() - - val totalTokens = totalPromptTokens + totalOutputTokens + totalThoughtTokens - val totalTokStr = String.format(Locale.US, "%,d", totalTokens) - val promptTokStr = String.format(Locale.US, "%,d", totalPromptTokens) - val outputTokStr = String.format(Locale.US, "%,d", totalOutputTokens) - val thoughtTokStr = String.format(Locale.US, "%,d", totalThoughtTokens) - - ActionSettingsItem( - title = stringResource(R.string.settings_total_consumption_title), - subtitle = stringResource( - R.string.settings_ai_usage_tokens_subtitle, - totalTokStr, - promptTokStr, - outputTokStr, - thoughtTokStr - ), - icon = { - Icon( - painter = painterResource(R.drawable.rounded_monitoring_24), - contentDescription = null, - tint = MaterialTheme.colorScheme.tertiary - ) - }, - primaryActionLabel = stringResource(R.string.settings_ai_clear_logs), - onPrimaryAction = { settingsViewModel.clearAiUsageData() } - ) - - if (recentAiUsage.isNotEmpty()) { - Spacer(modifier = Modifier.height(12.dp)) - var expanded by remember { mutableStateOf(false) } - val rotation by animateFloatAsState(targetValue = if (expanded) 180f else 0f) - - Surface( - modifier = Modifier - .fillMaxWidth() - .clickable { expanded = !expanded }, - color = Color.Transparent - ) { - Row( - modifier = Modifier.padding(vertical = 8.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceBetween - ) { - Row(verticalAlignment = Alignment.CenterVertically) { - Icon( - painter = painterResource(R.drawable.rounded_monitoring_24), - contentDescription = null, - tint = MaterialTheme.colorScheme.onSurfaceVariant, - modifier = Modifier.size(20.dp) - ) - Spacer(modifier = Modifier.width(12.dp)) - Text( - text = stringResource(R.string.settings_ai_activity_log_title, recentAiUsage.size), - style = MaterialTheme.typography.titleMedium.copy(fontFamily = GoogleSansRounded), - color = MaterialTheme.colorScheme.onSurface - ) - } - Icon( - imageVector = Icons.Rounded.ExpandMore, - contentDescription = if (expanded) stringResource(R.string.settings_ai_hide_logs) else stringResource(R.string.settings_ai_show_logs), - tint = MaterialTheme.colorScheme.onSurfaceVariant, - modifier = Modifier.rotate(rotation) - ) - } - } - - AnimatedVisibility( - visible = expanded, - enter = expandVertically() + fadeIn(), - exit = shrinkVertically() + fadeOut() - ) { - Column( - modifier = Modifier - .fillMaxWidth() - .padding(top = 8.dp, bottom = 8.dp) - ) { - val dateFormat = SimpleDateFormat("MMMM d, yyyy", Locale.getDefault()) - val groupedUsage = recentAiUsage.groupBy { - dateFormat.format(Date(it.timestamp)) - } + AiUsageReportSection(settingsViewModel = settingsViewModel) - groupedUsage.forEach { (date, items) -> - AiUsageDateHeader(date = date) - items.forEach { usage -> - AiUsageLogItem(usage = usage) - } - Spacer(modifier = Modifier.height(8.dp)) - } - } - } - } - } } SettingsCategory.BACKUP_RESTORE -> { if (!uiState.backupInfoDismissed) { @@ -2927,3 +2850,105 @@ private fun SettingsSubsection( Spacer(modifier = Modifier.height(10.dp)) } } + +@Composable +private fun AiUsageReportSection(settingsViewModel: SettingsViewModel) { + SettingsSubsection(title = stringResource(R.string.settings_ai_usage_report_section)) { + val recentAiUsage by settingsViewModel.recentAiUsage.collectAsStateWithLifecycle() + val totalPromptTokens by settingsViewModel.totalPromptTokens.collectAsStateWithLifecycle() + val totalOutputTokens by settingsViewModel.totalOutputTokens.collectAsStateWithLifecycle() + val totalThoughtTokens by settingsViewModel.totalThoughtTokens.collectAsStateWithLifecycle() + + val totalTokens = totalPromptTokens + totalOutputTokens + totalThoughtTokens + val totalTokStr = String.format(Locale.US, "%,d", totalTokens) + val promptTokStr = String.format(Locale.US, "%,d", totalPromptTokens) + val outputTokStr = String.format(Locale.US, "%,d", totalOutputTokens) + val thoughtTokStr = String.format(Locale.US, "%,d", totalThoughtTokens) + + ActionSettingsItem( + title = stringResource(R.string.settings_total_consumption_title), + subtitle = stringResource( + R.string.settings_ai_usage_tokens_subtitle, + totalTokStr, + promptTokStr, + outputTokStr, + thoughtTokStr + ), + icon = { + Icon( + painter = painterResource(R.drawable.rounded_monitoring_24), + contentDescription = null, + tint = MaterialTheme.colorScheme.tertiary + ) + }, + primaryActionLabel = stringResource(R.string.settings_ai_clear_logs), + onPrimaryAction = { settingsViewModel.clearAiUsageData() } + ) + + if (recentAiUsage.isNotEmpty()) { + Spacer(modifier = Modifier.height(12.dp)) + var expanded by remember { mutableStateOf(false) } + val rotation by animateFloatAsState(targetValue = if (expanded) 180f else 0f) + + Surface( + modifier = Modifier + .fillMaxWidth() + .clickable { expanded = !expanded }, + color = Color.Transparent + ) { + Row( + modifier = Modifier.padding(vertical = 8.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + Row(verticalAlignment = Alignment.CenterVertically) { + Icon( + painter = painterResource(R.drawable.rounded_monitoring_24), + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.size(20.dp) + ) + Spacer(modifier = Modifier.width(12.dp)) + Text( + text = stringResource(R.string.settings_ai_activity_log_title, recentAiUsage.size), + style = MaterialTheme.typography.titleMedium.copy(fontFamily = GoogleSansRounded), + color = MaterialTheme.colorScheme.onSurface + ) + } + Icon( + imageVector = Icons.Rounded.ExpandMore, + contentDescription = if (expanded) stringResource(R.string.settings_ai_hide_logs) else stringResource(R.string.settings_ai_show_logs), + tint = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.rotate(rotation) + ) + } + } + + AnimatedVisibility( + visible = expanded, + enter = expandVertically() + fadeIn(), + exit = shrinkVertically() + fadeOut() + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(top = 8.dp, bottom = 8.dp) + ) { + @Suppress("NonObservableLocale") + val dateFormat = SimpleDateFormat("MMMM d, yyyy", Locale.getDefault()) + val groupedUsage = recentAiUsage.groupBy { + dateFormat.format(Date(it.timestamp)) + } + + groupedUsage.forEach { (date, items) -> + AiUsageDateHeader(date = date) + items.forEach { usage -> + AiUsageLogItem(usage = usage) + } + Spacer(modifier = Modifier.height(8.dp)) + } + } + } + } + } +} diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/screens/SettingsComponents.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/screens/SettingsComponents.kt index bc5dbb3293..0f9ca6572e 100644 --- a/app/src/main/java/com/theveloper/pixelplay/presentation/screens/SettingsComponents.kt +++ b/app/src/main/java/com/theveloper/pixelplay/presentation/screens/SettingsComponents.kt @@ -1,3 +1,5 @@ +@file:OptIn(ExperimentalMaterial3Api::class) + package com.theveloper.pixelplay.presentation.screens import androidx.compose.animation.AnimatedContent @@ -551,6 +553,184 @@ fun SearchableModelSelector( } } +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun SearchableProviderSelector( + label: String, + description: String, + providers: List, + selectedProvider: String, + onProviderSelected: (String) -> Unit, + leadingIcon: @Composable () -> Unit +) { + var showSheet by remember { mutableStateOf(false) } + var searchQuery by remember { mutableStateOf("") } + val selectedDisplayName = providers.find { it.name == selectedProvider }?.displayName + ?: selectedProvider + + Surface( + color = MaterialTheme.colorScheme.surfaceContainer, + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(10.dp)) + .clickable { showSheet = true } + ) { + Column(modifier = Modifier.padding(16.dp)) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.fillMaxWidth() + ) { + Box( + modifier = Modifier + .padding(end = 16.dp) + .size(24.dp), + contentAlignment = Alignment.Center + ) { leadingIcon() } + + Column(modifier = Modifier.weight(1f)) { + Text( + text = label, + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.onSurface + ) + Spacer(modifier = Modifier.height(6.dp)) + Text( + text = description, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + Spacer(modifier = Modifier.height(10.dp)) + Surface( + color = MaterialTheme.colorScheme.surfaceContainerLowest, + shape = CircleShape, + modifier = Modifier.align(Alignment.Start) + ) { + Text( + text = selectedDisplayName, + style = MaterialTheme.typography.labelMedium, + color = MaterialTheme.colorScheme.primary, + fontWeight = FontWeight.Bold, + modifier = Modifier.padding(horizontal = 12.dp, vertical = 6.dp) + ) + } + } + } + } + } + + if (showSheet) { + ModalBottomSheet( + onDismissRequest = { + showSheet = false + searchQuery = "" + }, + containerColor = MaterialTheme.colorScheme.surface, + contentColor = MaterialTheme.colorScheme.onSurface + ) { + Column(modifier = Modifier.padding(bottom = 24.dp)) { + Text( + text = label, + style = MaterialTheme.typography.headlineSmall, + modifier = Modifier.padding(horizontal = 24.dp, vertical = 16.dp), + fontWeight = FontWeight.Bold + ) + + OutlinedTextField( + value = searchQuery, + onValueChange = { searchQuery = it }, + placeholder = { Text("Search providers...") }, + leadingIcon = { Icon(Icons.Rounded.Search, contentDescription = "Search") }, + trailingIcon = { + if (searchQuery.isNotEmpty()) { + IconButton(onClick = { searchQuery = "" }) { + Icon(Icons.Rounded.Clear, contentDescription = "Clear") + } + } + }, + singleLine = true, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 8.dp), + shape = RoundedCornerShape(12.dp), + colors = OutlinedTextFieldDefaults.colors( + focusedBorderColor = MaterialTheme.colorScheme.primary, + unfocusedBorderColor = MaterialTheme.colorScheme.outline + ) + ) + + Spacer(modifier = Modifier.height(4.dp)) + + val filteredProviders = remember(providers, searchQuery) { + if (searchQuery.isBlank()) providers + else providers.filter { + it.name.contains(searchQuery, ignoreCase = true) || + it.displayName.contains(searchQuery, ignoreCase = true) || + it.description.contains(searchQuery, ignoreCase = true) + } + } + + Text( + text = "${filteredProviders.size} provider${if (filteredProviders.size != 1) "s" else ""} available", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding(horizontal = 24.dp, vertical = 4.dp) + ) + + LazyColumn( + modifier = Modifier + .padding(horizontal = 16.dp) + .heightIn(max = 400.dp) + ) { + items(filteredProviders, key = { it.name }) { provider -> + val isSelected = provider.name == selectedProvider + Surface( + color = if (isSelected) MaterialTheme.colorScheme.primaryContainer + else MaterialTheme.colorScheme.surfaceContainerHigh, + shape = RoundedCornerShape(10.dp), + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 4.dp) + .clickable { + onProviderSelected(provider.name) + showSheet = false + searchQuery = "" + } + ) { + Row( + modifier = Modifier.padding(16.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Column(modifier = Modifier.weight(1f)) { + Text( + text = provider.displayName, + style = MaterialTheme.typography.bodyLarge, + color = if (isSelected) MaterialTheme.colorScheme.onPrimaryContainer + else MaterialTheme.colorScheme.onSurface + ) + Text( + text = provider.description, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + maxLines = 2, + overflow = TextOverflow.Ellipsis + ) + } + if (isSelected) { + Icon( + imageVector = Icons.Rounded.CheckCircle, + contentDescription = "Selected", + tint = MaterialTheme.colorScheme.primary + ) + } + } + } + } + } + } + } + } +} + @Composable fun SliderSettingsItem( label: String, diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/screens/SettingsScreen.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/screens/SettingsScreen.kt index fa40ecb265..2acbf769a9 100644 --- a/app/src/main/java/com/theveloper/pixelplay/presentation/screens/SettingsScreen.kt +++ b/app/src/main/java/com/theveloper/pixelplay/presentation/screens/SettingsScreen.kt @@ -480,9 +480,11 @@ private fun getCategoryColors(category: SettingsCategory, isDark: Boolean): Pair when (category) { SettingsCategory.LIBRARY -> Color(0xFF004A77) to Color(0xFFC2E7FF) SettingsCategory.APPEARANCE -> Color(0xFF7D5260) to Color(0xFFFFD8E4) + SettingsCategory.LYRICS -> Color(0xFF4A1A6E) to Color(0xFFE8D0FF) SettingsCategory.PLAYBACK -> Color(0xFF633B48) to Color(0xFFFFD8EC) SettingsCategory.BEHAVIOR -> Color(0xFF3E4C63) to Color(0xFFD7E3FF) SettingsCategory.AI_INTEGRATION -> Color(0xFF004F58) to Color(0xFF88FAFF) + SettingsCategory.GENERATION_PARAMETERS -> Color(0xFF4A256B) to Color(0xFFE8B0FF) SettingsCategory.BACKUP_RESTORE -> Color(0xFF3B4869) to Color(0xFFD9E2FF) SettingsCategory.DEVELOPER -> Color(0xFF324F34) to Color(0xFFCBEFD0) SettingsCategory.EQUALIZER -> Color(0xFF6E4E13) to Color(0xFFFFDEAC) @@ -493,9 +495,11 @@ private fun getCategoryColors(category: SettingsCategory, isDark: Boolean): Pair when (category) { SettingsCategory.LIBRARY -> Color(0xFFD7E3FF) to Color(0xFF005AC1) SettingsCategory.APPEARANCE -> Color(0xFFFFD8E4) to Color(0xFF631835) + SettingsCategory.LYRICS -> Color(0xFFF0E0FF) to Color(0xFF4A1A6E) SettingsCategory.PLAYBACK -> Color(0xFFFFD8EC) to Color(0xFF631B4B) SettingsCategory.BEHAVIOR -> Color(0xFFD7E3FF) to Color(0xFF253347) SettingsCategory.AI_INTEGRATION -> Color(0xFFCCE8EA) to Color(0xFF004F58) + SettingsCategory.GENERATION_PARAMETERS -> Color(0xFFE8B0FF) to Color(0xFF3B1A53) SettingsCategory.BACKUP_RESTORE -> Color(0xFFD9E2FF) to Color(0xFF27304E) SettingsCategory.DEVELOPER -> Color(0xFFCBEFD0) to Color(0xFF042106) SettingsCategory.EQUALIZER -> Color(0xFFFFDEAC) to Color(0xFF281900) diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/AiStateHolder.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/AiStateHolder.kt index ad68cea838..3dee09de7c 100644 --- a/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/AiStateHolder.kt +++ b/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/AiStateHolder.kt @@ -6,10 +6,12 @@ import com.theveloper.pixelplay.R import com.theveloper.pixelplay.data.DailyMixManager import com.theveloper.pixelplay.data.ai.AiNotificationManager import com.theveloper.pixelplay.data.ai.AiPlaylistGenerator +import com.theveloper.pixelplay.data.ai.AiResponseCleaner import com.theveloper.pixelplay.data.ai.AiSystemPromptType import com.theveloper.pixelplay.data.ai.provider.AiProviderException import com.theveloper.pixelplay.data.preferences.PlaylistPreferencesRepository import com.theveloper.pixelplay.data.model.Song +import com.theveloper.pixelplay.utils.MultiLangRomanizer import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.MutableStateFlow @@ -17,6 +19,7 @@ import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch import timber.log.Timber +import java.util.Locale import javax.inject.Inject import javax.inject.Singleton @@ -286,39 +289,94 @@ class AiStateHolder @Inject constructor( suspend fun translateLyrics(lyricsText: String): Result { return try { - val targetLanguage = context.resources.configuration.locales[0].displayLanguage - val prompt = """ -Translate song lyrics into $targetLanguage. + val targetLanguage = context.resources.configuration.locales[0] + .getDisplayLanguage(Locale.US) + val sourceLanguage = detectLyricsLanguage(lyricsText) + val hasTimestamps = lyricsText.contains(Regex("\\[\\d{1,2}:\\d{2}")) + val taskPrefix = if (sourceLanguage != null) { + "Translate these $sourceLanguage song lyrics into $targetLanguage." + } else { + "Translate these song lyrics into $targetLanguage. Detect the source language from the lyrics content." + } + val formatSection = if (hasTimestamps) { + """ + +For SYNCED lyrics — output TWO lines per original line: +[original timestamp] original text +[same timestamp] translated text + +Preserve ALL timestamps [mm:ss.xx] exactly — never modify, merge, or drop them. + + """.trimIndent() + } else { + """ + +For PLAIN lyrics (no timestamps) — output TWO lines per original line: +original text +translated text + +Keep the same line order as the original. + + """.trimIndent() + } + val xmlPrompt = """ +$taskPrefix -- Preserve ALL timestamps [mm:ss.xx] exactly — never modify, merge, or drop them. -- Output TWO lines per original line: the original, then the translation with the same timestamp. +- Output TWO lines per original line: the original, then the translation. - NEVER add explanations, labels, numbering, section headers, or formatting. - NEVER remove, merge, split, or reorder lines. +- Translate ALL lines completely — do not skip or truncate any part. - If lyrics are ALREADY mostly in $targetLanguage, output ONLY: ALREADY_IN_TARGET_LANGUAGE - -[original timestamp] original text -[same timestamp] translated text - +$formatSection $lyricsText """.trimIndent() - + val response = aiHandler.generateContent( - prompt = prompt, - type = AiSystemPromptType.GENERAL, + prompt = xmlPrompt, + type = AiSystemPromptType.LYRICS, temperature = 0.1f ) - Result.success(response) + val cleaned = AiResponseCleaner.cleanTextResponse(response) + Result.success(cleaned) } catch (e: Exception) { Result.failure(e) } } + private val timestampRegex = Regex("\\[\\d{1,2}:\\d{2}(?:[.:]\\d{1,3})?]") + + private fun detectLyricsLanguage(lyricsText: String): String? { + val sample = timestampRegex.replace(lyricsText, "").trim().take(200) + if (sample.isBlank()) return null + + return when { + MultiLangRomanizer.isJapanese(sample) -> "Japanese" + MultiLangRomanizer.isKorean(sample) -> "Korean" + MultiLangRomanizer.isChinese(sample) -> "Chinese" + MultiLangRomanizer.isThai(sample) -> "Thai" + MultiLangRomanizer.isArabic(sample) -> "Arabic" + MultiLangRomanizer.isGreek(sample) -> "Greek" + MultiLangRomanizer.isHebrew(sample) -> "Hebrew" + MultiLangRomanizer.isHindi(sample) -> "Hindi" + MultiLangRomanizer.isPunjabi(sample) -> "Punjabi" + MultiLangRomanizer.isCyrillic(sample) -> { + when { + MultiLangRomanizer.isRussian(sample) -> "Russian" + MultiLangRomanizer.isUkrainian(sample) -> "Ukrainian" + else -> null + } + } + MultiLangRomanizer.isVietnamese(sample) -> "Vietnamese" + else -> null + } + } + fun onCleared() { scope = null allSongsProvider = null diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/LibraryStateHolder.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/LibraryStateHolder.kt index 22fb7d311f..3515c7de8a 100644 --- a/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/LibraryStateHolder.kt +++ b/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/LibraryStateHolder.kt @@ -257,23 +257,21 @@ class LibraryStateHolder @Inject constructor( songsJob = scope?.launch { _isLoadingLibrary.value = true musicRepository.getAudioFiles().conflate().collect { songs -> - // Process heavy list conversions on Default dispatcher to avoid blocking UI val immutableSongs = withContext(Dispatchers.Default) { songs.toImmutableList() } val songsMap = withContext(Dispatchers.Default) { songs.associateBy { it.id } } _allSongs.value = immutableSongs _allSongsById.value = songsMap - // When the repository emits a new list (triggered by directory changes), - // we update our state and re-apply current sorting. - // Apply sort to the new data sortSongs(_currentSongSortOption.value, persist = false) _isLoadingLibrary.value = false } } + // Albums, artists, and folders load sequentially after songs to avoid + // I/O contention on slower storage (eMMC on Redmi vs UFS on Samsung). albumsJob = scope?.launch { - _isLoadingCategories.value = true + songsJob?.join() @OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class) kotlinx.coroutines.flow.combine( effectiveStorageFilter, @@ -287,12 +285,11 @@ class LibraryStateHolder @Inject constructor( sortAlbumsList(albums, _currentAlbumSortOption.value).toImmutableList() } _albums.value = sortedAlbums - _isLoadingCategories.value = false } } artistsJob = scope?.launch { - _isLoadingCategories.value = true + albumsJob?.join() @OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class) effectiveStorageFilter.flatMapLatest { filter -> musicRepository.getArtists(filter) @@ -301,11 +298,11 @@ class LibraryStateHolder @Inject constructor( sortArtistsList(artists, _currentArtistSortOption.value).toImmutableList() } _artists.value = sortedArtists - _isLoadingCategories.value = false } } foldersJob = scope?.launch { + artistsJob?.join() @OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class) effectiveStorageFilter.flatMapLatest { filter -> musicRepository.getMusicFolders(effectiveFoldersStorageFilter(filter)) @@ -316,6 +313,16 @@ class LibraryStateHolder @Inject constructor( _musicFolders.value = sortedFolders } } + + // Single loading state: true when songs start, false when all sequential + // jobs (albums + artists) finish. This avoids flapping true→false→true + // as each sequential coroutine completes. + scope?.launch { + songsJob?.join() + _isLoadingCategories.value = true + foldersJob?.join() + _isLoadingCategories.value = false + } } // Deprecated imperative loaders - redirected to observer start diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/PlayerViewModel.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/PlayerViewModel.kt index 4ddadbedea..a3e9d36cf5 100644 --- a/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/PlayerViewModel.kt +++ b/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/PlayerViewModel.kt @@ -45,6 +45,8 @@ import com.theveloper.pixelplay.data.model.Genre import com.theveloper.pixelplay.data.model.Lyrics import com.theveloper.pixelplay.data.model.LyricsSourcePreference import com.theveloper.pixelplay.data.model.SearchFilterType +import com.theveloper.pixelplay.data.model.SearchHistoryItem +import com.theveloper.pixelplay.data.model.SearchResultItem import com.theveloper.pixelplay.data.model.Song import com.theveloper.pixelplay.data.model.SortOption import com.theveloper.pixelplay.data.model.toLibraryTabIdOrNull @@ -444,6 +446,11 @@ class PlayerViewModel @Inject constructor( val aiStatus: StateFlow = aiStateHolder.aiStatus val aiError: StateFlow = aiStateHolder.aiError + // Search state (direct access bypasses 40-field playerUiState) + val searchResults: StateFlow> = searchStateHolder.searchResults + val selectedSearchFilter: StateFlow = searchStateHolder.selectedSearchFilter + val searchHistory: StateFlow> = searchStateHolder.searchHistory + private val _selectedSongForInfo = MutableStateFlow(null) val selectedSongForInfo: StateFlow = _selectedSongForInfo.asStateFlow() @@ -536,14 +543,6 @@ class PlayerViewModel @Inject constructor( initialValue = false ) - val hasGeminiApiKey: StateFlow = aiPreferencesRepository.geminiApiKey - .map { it.isNotBlank() } - .stateIn( - scope = viewModelScope, - started = SharingStarted.WhileSubscribed(5000), - initialValue = false - ) - val fullPlayerLoadingTweaks: StateFlow = userPreferencesRepository.fullPlayerLoadingTweaksFlow .stateIn( scope = viewModelScope, @@ -646,7 +645,7 @@ class PlayerViewModel @Inject constructor( // Toast Events private val _toastEvents = MutableSharedFlow( - extraBufferCapacity = 1, + extraBufferCapacity = 5, onBufferOverflow = BufferOverflow.DROP_OLDEST ) val toastEvents = _toastEvents.asSharedFlow() @@ -1140,13 +1139,6 @@ class PlayerViewModel @Inject constructor( initialValue = persistentListOf() ) - val songCountFlow: StateFlow = musicRepository.getSongCountFlow() - .stateIn( - scope = viewModelScope, - started = SharingStarted.WhileSubscribed(5000), - initialValue = 0 - ) - val hasCloudSongsFlow: StateFlow = musicRepository.getCloudSongCountFlow() .map { it > 0 } .distinctUntilChanged() @@ -1527,15 +1519,11 @@ class PlayerViewModel @Inject constructor( + // Deferred: legacy migrations run after init to avoid blocking first render viewModelScope.launch { + kotlinx.coroutines.delay(2000) userPreferencesRepository.migrateTabOrder() - } - - viewModelScope.launch { userPreferencesRepository.ensureLibrarySortDefaults() - } - - viewModelScope.launch { val legacyFavoriteIds = userPreferencesRepository.favoriteSongIdsFlow.first() if (legacyFavoriteIds.isNotEmpty()) { val roomFavoriteIds = musicRepository.getFavoriteSongIdsOnce() @@ -1677,8 +1665,12 @@ class PlayerViewModel @Inject constructor( // launchColorSchemeProcessor() - Handled by ThemeStateHolder and on-demand calls - loadPersistedDailyMix() - loadSearchHistory() + // Deferred: daily mix and search history load after first frame + viewModelScope.launch { + kotlinx.coroutines.delay(1500) + loadPersistedDailyMix() + loadSearchHistory() + } viewModelScope.launch { isSyncingStateFlow.collect { isSyncing -> @@ -1797,7 +1789,7 @@ class PlayerViewModel @Inject constructor( aiStatus = status, aiError = error ) - }.collect { snapshot -> + }.distinctUntilChanged().collect { snapshot -> _playerUiState.update { it.copy( showAiPlaylistSheet = snapshot.showAiPlaylistSheet, @@ -1820,7 +1812,7 @@ class PlayerViewModel @Inject constructor( libraryStateHolder.isLoadingCategories, ) { folders, loadingLibrary, loadingCategories -> Triple(folders, loadingLibrary, loadingCategories) - }.collect { (folders, loadingLibrary, loadingCategories) -> + }.distinctUntilChanged().collect { (folders, loadingLibrary, loadingCategories) -> _playerUiState.update { it.copy( musicFolders = folders, @@ -1841,7 +1833,7 @@ class PlayerViewModel @Inject constructor( libraryStateHolder.currentFavoriteSortOption, ) { songSort, albumSort, artistSort, folderSort, favoriteSort -> SortOptionsSnapshot(songSort, albumSort, artistSort, folderSort, favoriteSort) - }.collect { snapshot -> + }.distinctUntilChanged().collect { snapshot -> _playerUiState.update { it.copy( currentSongSortOption = snapshot.songSort, diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/SettingsViewModel.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/SettingsViewModel.kt index abba7eaceb..d8831fd9fc 100644 --- a/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/SettingsViewModel.kt +++ b/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/SettingsViewModel.kt @@ -213,86 +213,6 @@ class SettingsViewModel @Inject constructor( .flatMapLatest { provider -> aiPreferencesRepository.getSystemPrompt(AiProvider.fromString(provider)) } .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), AiPreferencesRepository.DEFAULT_SYSTEM_PROMPT) - // Specific Provider StateFlows for UI Compatibility - val geminiApiKey: StateFlow = aiPreferencesRepository.geminiApiKey - .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), "") - val geminiModel: StateFlow = aiPreferencesRepository.geminiModel - .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), "") - val geminiSystemPrompt: StateFlow = aiPreferencesRepository.geminiSystemPrompt - .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), AiPreferencesRepository.DEFAULT_SYSTEM_PROMPT) - - val deepseekApiKey: StateFlow = aiPreferencesRepository.deepseekApiKey - .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), "") - val deepseekModel: StateFlow = aiPreferencesRepository.deepseekModel - .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), "") - val deepseekSystemPrompt: StateFlow = aiPreferencesRepository.deepseekSystemPrompt - .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), AiPreferencesRepository.DEFAULT_DEEPSEEK_SYSTEM_PROMPT) - - val groqApiKey: StateFlow = aiPreferencesRepository.groqApiKey - .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), "") - val groqModel: StateFlow = aiPreferencesRepository.groqModel - .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), "") - val groqSystemPrompt: StateFlow = aiPreferencesRepository.groqSystemPrompt - .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), AiPreferencesRepository.DEFAULT_GROQ_SYSTEM_PROMPT) - - val mistralApiKey: StateFlow = aiPreferencesRepository.mistralApiKey - .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), "") - val mistralModel: StateFlow = aiPreferencesRepository.mistralModel - .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), "") - val mistralSystemPrompt: StateFlow = aiPreferencesRepository.mistralSystemPrompt - .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), AiPreferencesRepository.DEFAULT_MISTRAL_SYSTEM_PROMPT) - - val nvidiaApiKey: StateFlow = aiPreferencesRepository.nvidiaApiKey - .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), "") - val nvidiaModel: StateFlow = aiPreferencesRepository.nvidiaModel - .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), "") - val nvidiaSystemPrompt: StateFlow = aiPreferencesRepository.nvidiaSystemPrompt - .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), AiPreferencesRepository.DEFAULT_NVIDIA_SYSTEM_PROMPT) - - val kimiApiKey: StateFlow = aiPreferencesRepository.kimiApiKey - .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), "") - val kimiModel: StateFlow = aiPreferencesRepository.kimiModel - .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), "") - val kimiSystemPrompt: StateFlow = aiPreferencesRepository.kimiSystemPrompt - .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), AiPreferencesRepository.DEFAULT_KIMI_SYSTEM_PROMPT) - - val glmApiKey: StateFlow = aiPreferencesRepository.glmApiKey - .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), "") - val glmModel: StateFlow = aiPreferencesRepository.glmModel - .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), "") - val glmSystemPrompt: StateFlow = aiPreferencesRepository.glmSystemPrompt - .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), AiPreferencesRepository.DEFAULT_GLM_SYSTEM_PROMPT) - - val openaiApiKey: StateFlow = aiPreferencesRepository.openaiApiKey - .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), "") - val openaiModel: StateFlow = aiPreferencesRepository.openaiModel - .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), "") - val openaiSystemPrompt: StateFlow = aiPreferencesRepository.openaiSystemPrompt - .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), AiPreferencesRepository.DEFAULT_OPENAI_SYSTEM_PROMPT) - - val openrouterApiKey: StateFlow = aiPreferencesRepository.openrouterApiKey - .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), "") - val openrouterModel: StateFlow = aiPreferencesRepository.openrouterModel - .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), "") - val openrouterSystemPrompt: StateFlow = aiPreferencesRepository.openrouterSystemPrompt - .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), AiPreferencesRepository.DEFAULT_OPENROUTER_SYSTEM_PROMPT) - - val ollamaApiKey: StateFlow = aiPreferencesRepository.ollamaApiKey - .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), "") - val ollamaModel: StateFlow = aiPreferencesRepository.ollamaModel - .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), "") - val ollamaSystemPrompt: StateFlow = aiPreferencesRepository.ollamaSystemPrompt - .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), AiPreferencesRepository.DEFAULT_SYSTEM_PROMPT) - - val customApiKey: StateFlow = aiPreferencesRepository.customApiKey - .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), "") - val customModel: StateFlow = aiPreferencesRepository.customModel - .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), "") - val customSystemPrompt: StateFlow = aiPreferencesRepository.customSystemPrompt - .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), AiPreferencesRepository.DEFAULT_SYSTEM_PROMPT) - val customBaseUrl: StateFlow = aiPreferencesRepository.customBaseUrl - .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), "") - val currentAiBaseUrl: StateFlow = aiProvider .flatMapLatest { provider -> val p = AiProvider.fromString(provider) @@ -333,87 +253,10 @@ class SettingsViewModel @Inject constructor( } } - // Specific on-change methods for UI binding - fun onGeminiApiKeyChange(apiKey: String) { + fun onBaseUrlChange(baseUrl: String) { viewModelScope.launch { - aiPreferencesRepository.setApiKey(AiProvider.GEMINI, apiKey) - if (apiKey.isNotBlank()) fetchAvailableModels(apiKey, "GEMINI") - else clearModelsState("GEMINI") - } - } - fun onDeepseekApiKeyChange(apiKey: String) { - viewModelScope.launch { - aiPreferencesRepository.setApiKey(AiProvider.DEEPSEEK, apiKey) - if (apiKey.isNotBlank()) fetchAvailableModels(apiKey, "DEEPSEEK") - else clearModelsState("DEEPSEEK") - } - } - fun onGroqApiKeyChange(apiKey: String) { - viewModelScope.launch { - aiPreferencesRepository.setApiKey(AiProvider.GROQ, apiKey) - if (apiKey.isNotBlank()) fetchAvailableModels(apiKey, "GROQ") - else clearModelsState("GROQ") - } - } - fun onMistralApiKeyChange(apiKey: String) { - viewModelScope.launch { - aiPreferencesRepository.setApiKey(AiProvider.MISTRAL, apiKey) - if (apiKey.isNotBlank()) fetchAvailableModels(apiKey, "MISTRAL") - else clearModelsState("MISTRAL") - } - } - fun onNvidiaApiKeyChange(apiKey: String) { - viewModelScope.launch { - aiPreferencesRepository.setApiKey(AiProvider.NVIDIA, apiKey) - if (apiKey.isNotBlank()) fetchAvailableModels(apiKey, "NVIDIA") - else clearModelsState("NVIDIA") - } - } - fun onKimiApiKeyChange(apiKey: String) { - viewModelScope.launch { - aiPreferencesRepository.setApiKey(AiProvider.KIMI, apiKey) - if (apiKey.isNotBlank()) fetchAvailableModels(apiKey, "KIMI") - else clearModelsState("KIMI") - } - } - fun onGlmApiKeyChange(apiKey: String) { - viewModelScope.launch { - aiPreferencesRepository.setApiKey(AiProvider.GLM, apiKey) - if (apiKey.isNotBlank()) fetchAvailableModels(apiKey, "GLM") - else clearModelsState("GLM") - } - } - fun onOpenAiApiKeyChange(apiKey: String) { - viewModelScope.launch { - aiPreferencesRepository.setApiKey(AiProvider.OPENAI, apiKey) - if (apiKey.isNotBlank()) fetchAvailableModels(apiKey, "OPENAI") - else clearModelsState("OPENAI") - } - } - fun onOpenrouterApiKeyChange(apiKey: String) { - viewModelScope.launch { - aiPreferencesRepository.setApiKey(AiProvider.OPENROUTER, apiKey) - if (apiKey.isNotBlank()) fetchAvailableModels(apiKey, "OPENROUTER") - else clearModelsState("OPENROUTER") - } - } - fun onOllamaApiKeyChange(apiKey: String) { - viewModelScope.launch { - aiPreferencesRepository.setApiKey(AiProvider.OLLAMA, apiKey) - if (apiKey.isNotBlank()) fetchAvailableModels(apiKey, "OLLAMA") - else clearModelsState("OLLAMA") - } - } - fun onCustomApiKeyChange(apiKey: String) { - viewModelScope.launch { - aiPreferencesRepository.setApiKey(AiProvider.CUSTOM, apiKey) - if (apiKey.isNotBlank()) fetchAvailableModels(apiKey, "CUSTOM") - else clearModelsState("CUSTOM") - } - } - fun onCustomBaseUrlChange(baseUrl: String) { - viewModelScope.launch { - aiPreferencesRepository.setBaseUrl(AiProvider.CUSTOM, baseUrl) + val provider = AiProvider.fromString(aiProvider.value) + aiPreferencesRepository.setBaseUrl(provider, baseUrl) } } @@ -452,18 +295,6 @@ class SettingsViewModel @Inject constructor( } } - fun onGeminiModelChange(model: String) = viewModelScope.launch { aiPreferencesRepository.setModel(AiProvider.GEMINI, model) } - fun onDeepseekModelChange(model: String) = viewModelScope.launch { aiPreferencesRepository.setModel(AiProvider.DEEPSEEK, model) } - fun onGroqModelChange(model: String) = viewModelScope.launch { aiPreferencesRepository.setModel(AiProvider.GROQ, model) } - fun onMistralModelChange(model: String) = viewModelScope.launch { aiPreferencesRepository.setModel(AiProvider.MISTRAL, model) } - fun onNvidiaModelChange(model: String) = viewModelScope.launch { aiPreferencesRepository.setModel(AiProvider.NVIDIA, model) } - fun onKimiModelChange(model: String) = viewModelScope.launch { aiPreferencesRepository.setModel(AiProvider.KIMI, model) } - fun onGlmModelChange(model: String) = viewModelScope.launch { aiPreferencesRepository.setModel(AiProvider.GLM, model) } - fun onOpenAiModelChange(model: String) = viewModelScope.launch { aiPreferencesRepository.setModel(AiProvider.OPENAI, model) } - fun onOpenrouterModelChange(model: String) = viewModelScope.launch { aiPreferencesRepository.setModel(AiProvider.OPENROUTER, model) } - fun onOllamaModelChange(model: String) = viewModelScope.launch { aiPreferencesRepository.setModel(AiProvider.OLLAMA, model) } - fun onCustomModelChange(model: String) = viewModelScope.launch { aiPreferencesRepository.setModel(AiProvider.CUSTOM, model) } - fun onAiSystemPromptChange(prompt: String) { viewModelScope.launch { val provider = AiProvider.fromString(aiProvider.value) @@ -471,18 +302,6 @@ class SettingsViewModel @Inject constructor( } } - fun onGeminiSystemPromptChange(prompt: String) = viewModelScope.launch { aiPreferencesRepository.setSystemPrompt(AiProvider.GEMINI, prompt) } - fun onDeepseekSystemPromptChange(prompt: String) = viewModelScope.launch { aiPreferencesRepository.setSystemPrompt(AiProvider.DEEPSEEK, prompt) } - fun onGroqSystemPromptChange(prompt: String) = viewModelScope.launch { aiPreferencesRepository.setSystemPrompt(AiProvider.GROQ, prompt) } - fun onMistralSystemPromptChange(prompt: String) = viewModelScope.launch { aiPreferencesRepository.setSystemPrompt(AiProvider.MISTRAL, prompt) } - fun onNvidiaSystemPromptChange(prompt: String) = viewModelScope.launch { aiPreferencesRepository.setSystemPrompt(AiProvider.NVIDIA, prompt) } - fun onKimiSystemPromptChange(prompt: String) = viewModelScope.launch { aiPreferencesRepository.setSystemPrompt(AiProvider.KIMI, prompt) } - fun onGlmSystemPromptChange(prompt: String) = viewModelScope.launch { aiPreferencesRepository.setSystemPrompt(AiProvider.GLM, prompt) } - fun onOpenAiSystemPromptChange(prompt: String) = viewModelScope.launch { aiPreferencesRepository.setSystemPrompt(AiProvider.OPENAI, prompt) } - fun onOpenrouterSystemPromptChange(prompt: String) = viewModelScope.launch { aiPreferencesRepository.setSystemPrompt(AiProvider.OPENROUTER, prompt) } - fun onOllamaSystemPromptChange(prompt: String) = viewModelScope.launch { aiPreferencesRepository.setSystemPrompt(AiProvider.OLLAMA, prompt) } - fun onCustomSystemPromptChange(prompt: String) = viewModelScope.launch { aiPreferencesRepository.setSystemPrompt(AiProvider.CUSTOM, prompt) } - fun resetAiSystemPrompt() { viewModelScope.launch { val provider = AiProvider.fromString(aiProvider.value) @@ -490,18 +309,6 @@ class SettingsViewModel @Inject constructor( } } - fun resetGeminiSystemPrompt() = viewModelScope.launch { aiPreferencesRepository.resetSystemPrompt(AiProvider.GEMINI) } - fun resetDeepseekSystemPrompt() = viewModelScope.launch { aiPreferencesRepository.resetSystemPrompt(AiProvider.DEEPSEEK) } - fun resetGroqSystemPrompt() = viewModelScope.launch { aiPreferencesRepository.resetSystemPrompt(AiProvider.GROQ) } - fun resetMistralSystemPrompt() = viewModelScope.launch { aiPreferencesRepository.resetSystemPrompt(AiProvider.MISTRAL) } - fun resetNvidiaSystemPrompt() = viewModelScope.launch { aiPreferencesRepository.resetSystemPrompt(AiProvider.NVIDIA) } - fun resetKimiSystemPrompt() = viewModelScope.launch { aiPreferencesRepository.resetSystemPrompt(AiProvider.KIMI) } - fun resetGlmSystemPrompt() = viewModelScope.launch { aiPreferencesRepository.resetSystemPrompt(AiProvider.GLM) } - fun resetOpenAiSystemPrompt() = viewModelScope.launch { aiPreferencesRepository.resetSystemPrompt(AiProvider.OPENAI) } - fun resetOpenrouterSystemPrompt() = viewModelScope.launch { aiPreferencesRepository.resetSystemPrompt(AiProvider.OPENROUTER) } - fun resetOllamaSystemPrompt() = viewModelScope.launch { aiPreferencesRepository.resetSystemPrompt(AiProvider.OLLAMA) } - fun resetCustomSystemPrompt() = viewModelScope.launch { aiPreferencesRepository.resetSystemPrompt(AiProvider.CUSTOM) } - fun clearAiUsageData() { viewModelScope.launch { aiUsageDao.clearUsage() diff --git a/app/src/main/java/com/theveloper/pixelplay/ui/glancewidget/PlayerControlActionCallback.kt b/app/src/main/java/com/theveloper/pixelplay/ui/glancewidget/PlayerControlActionCallback.kt index fcd2a1e471..22fd1aced5 100644 --- a/app/src/main/java/com/theveloper/pixelplay/ui/glancewidget/PlayerControlActionCallback.kt +++ b/app/src/main/java/com/theveloper/pixelplay/ui/glancewidget/PlayerControlActionCallback.kt @@ -58,9 +58,11 @@ class PlayerControlActionCallback : ActionCallback { context.startService(serviceIntent) } Timber.tag(TAG).d("Service intent sent for action: $action") - } catch (e: android.app.ForegroundServiceStartNotAllowedException) { - Timber.tag(TAG).w(e, "Cannot start foreground service from background for action $action") } catch (e: Exception) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && e is android.app.ForegroundServiceStartNotAllowedException) { + Timber.tag(TAG).w(e, "Cannot start foreground service from background for action $action") + return + } Timber.tag(TAG).e(e, "Error starting service for action $action: ${e.message}") } } From 557030a5b5880d2e06f16abd41c317efbbcce748 Mon Sep 17 00:00:00 2001 From: Dae Euhwa Date: Mon, 27 Jul 2026 20:24:13 -0500 Subject: [PATCH 60/73] chore: ignore message.txt build log output --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 986f70d62c..1510df9893 100644 --- a/.gitignore +++ b/.gitignore @@ -49,3 +49,4 @@ markdown.xml **/.env **/.env.* !**/.env.example +message.txt From 0b31a4e98a70b09b31dd83f71a44e6ed81528d79 Mon Sep 17 00:00:00 2001 From: Dae Euhwa Date: Mon, 27 Jul 2026 20:34:08 -0500 Subject: [PATCH 61/73] chore(deps): update netty to latest stable 4.2.16.Final --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index c03d8b480d..221b502293 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -64,7 +64,7 @@ junit5 = "6.1.2" kuromoji = "0.9.0" pinyin4j = "2.5.1" securityCrypto = "1.1.0" -netty = "4.2.28.Final" +netty = "4.2.16.Final" bouncycastle = "1.85" commons-lang3 = "3.20.0" jdom2 = "2.0.6.1" From a0856f2948da8ea6cffa40bcc26146726789804d Mon Sep 17 00:00:00 2001 From: Dae Euhwa Date: Wed, 29 Jul 2026 16:59:50 -0500 Subject: [PATCH 62/73] feat(i18n): complete translations - fix vi, zh-rTW, add missing keys, plurals, language labels - Translate Vietnamese (vi) locale files properly (was English clone) - Convert zh-rTW from Simplified to Traditional Chinese characters - Add missing license/notice keys to ja, vi, zh-rTW - Add missing lyrics keys to zh-rTW - Add missing generation parameters keys to ja, vi, zh-rTW - Create plurals.xml for ja, vi, zh-rTW, de, ko, nb, ru - Add 3 missing language labels (ja, zh-rTW, vi) to es, fr, in, it, tr, zh-rCN - Add 9 missing license keys to de, ko, nb, ru --- app/src/main/res/values-de/plurals.xml | 59 + .../main/res/values-de/strings_settings.xml | 9 + .../main/res/values-es/strings_settings.xml | 3 + .../main/res/values-fr/strings_settings.xml | 3 + .../main/res/values-in/strings_settings.xml | 3 + .../main/res/values-it/strings_settings.xml | 3 + app/src/main/res/values-ja/plurals.xml | 45 + .../main/res/values-ja/strings_settings.xml | 11 + app/src/main/res/values-ko/plurals.xml | 45 + .../main/res/values-ko/strings_settings.xml | 9 + app/src/main/res/values-nb/plurals.xml | 59 + .../main/res/values-nb/strings_settings.xml | 9 + app/src/main/res/values-ru/plurals.xml | 87 ++ .../main/res/values-ru/strings_settings.xml | 9 + .../main/res/values-tr/strings_settings.xml | 3 + app/src/main/res/values-vi/plurals.xml | 45 + app/src/main/res/values-vi/strings.xml | 206 +-- .../main/res/values-vi/strings_changelogs.xml | 220 ++-- .../res/values-vi/strings_cloud_services.xml | 338 ++--- .../main/res/values-vi/strings_equalizer.xml | 82 +- .../res/values-vi/strings_home_screen.xml | 454 +++---- .../main/res/values-vi/strings_library.xml | 900 +++++++------- app/src/main/res/values-vi/strings_player.xml | 308 ++--- .../main/res/values-vi/strings_screens.xml | 416 +++---- .../main/res/values-vi/strings_settings.xml | 1101 +++++++++-------- app/src/main/res/values-vi/strings_widget.xml | 24 +- .../res/values-zh-rCN/strings_settings.xml | 3 + app/src/main/res/values-zh-rTW/plurals.xml | 45 + app/src/main/res/values-zh-rTW/strings.xml | 146 +-- .../res/values-zh-rTW/strings_changelogs.xml | 184 +-- .../values-zh-rTW/strings_cloud_services.xml | 282 ++--- .../res/values-zh-rTW/strings_equalizer.xml | 62 +- .../res/values-zh-rTW/strings_home_screen.xml | 366 +++--- .../res/values-zh-rTW/strings_library.xml | 744 +++++------ .../main/res/values-zh-rTW/strings_player.xml | 262 ++-- .../res/values-zh-rTW/strings_screens.xml | 326 ++--- .../res/values-zh-rTW/strings_settings.xml | 1013 +++++++-------- .../main/res/values-zh-rTW/strings_widget.xml | 22 +- 38 files changed, 4190 insertions(+), 3716 deletions(-) create mode 100644 app/src/main/res/values-de/plurals.xml create mode 100644 app/src/main/res/values-ja/plurals.xml create mode 100644 app/src/main/res/values-ko/plurals.xml create mode 100644 app/src/main/res/values-nb/plurals.xml create mode 100644 app/src/main/res/values-ru/plurals.xml create mode 100644 app/src/main/res/values-vi/plurals.xml create mode 100644 app/src/main/res/values-zh-rTW/plurals.xml diff --git a/app/src/main/res/values-de/plurals.xml b/app/src/main/res/values-de/plurals.xml new file mode 100644 index 0000000000..a364fd09a0 --- /dev/null +++ b/app/src/main/res/values-de/plurals.xml @@ -0,0 +1,59 @@ + + + + %d Playlist wird geteilt + %d Playlists werden geteilt + + + %1$d Playlist nach %2$s exportiert + %1$d Playlists nach %2$s exportiert + + + %d Song zur Warteschlange hinzugefügt + %d Songs zur Warteschlange hinzugefügt + + + %d Song wird als nächstes abgespielt + %d Songs werden als nächstes abgespielt + + + %d Song zu Favoriten hinzugefügt + %d Songs zu Favoriten hinzugefügt + + + %d Song aus Favoriten entfernt + %d Songs aus Favoriten entfernt + + + %d Datei gelöscht + %d Dateien gelöscht + + + %d Datei löschen? + %d Dateien löschen? + + + %d Titel ausgewählt + %d Titel ausgewählt + + + %1$d Song • %2$s + %1$d Songs • %2$s + + + %d Titel eingereiht. + %d Titel eingereiht. + + + %d Song ausgewählt + %d Songs ausgewählt + + + %d Song + %d Songs + + + %d Thema + %d Themen + + diff --git a/app/src/main/res/values-de/strings_settings.xml b/app/src/main/res/values-de/strings_settings.xml index db1a0dd223..3ae3244e7c 100644 --- a/app/src/main/res/values-de/strings_settings.xml +++ b/app/src/main/res/values-de/strings_settings.xml @@ -637,6 +637,15 @@ Die Person hinter PixelPlayer. Community-Spotlight Anerkennung für Mitwirkende mit besonders großem Einfluss. + Lizenzen und Hinweise + Zuschreibung für Abhängigkeiten und frühere Beiträge. + Open-Source-Lizenzen + Abhängigkeitslizenzen und Drittanbieter-Hinweise + Open-Source-Lizenzen + MIT-ähnlicher Hinweis + Drittanbieter-Hinweise + Schließen + Der gebündelte Drittanbieter-Hinweis konnte nicht geladen werden. Open-Source-Mitwirkende Aktuelle Mitwirkenden-Liste von GitHub. %1$d Beiträge diff --git a/app/src/main/res/values-es/strings_settings.xml b/app/src/main/res/values-es/strings_settings.xml index bc7c980dfe..ef9f488064 100644 --- a/app/src/main/res/values-es/strings_settings.xml +++ b/app/src/main/res/values-es/strings_settings.xml @@ -176,6 +176,9 @@ Coreano Noruego (Bokmål) Turco + Japonés + Chino tradicional + Vietnamita Tema de la app Cambia entre claro, oscuro o seguir el sistema. Tema claro diff --git a/app/src/main/res/values-fr/strings_settings.xml b/app/src/main/res/values-fr/strings_settings.xml index d6cc272298..426ae3a21f 100644 --- a/app/src/main/res/values-fr/strings_settings.xml +++ b/app/src/main/res/values-fr/strings_settings.xml @@ -172,6 +172,9 @@ Coréen Norvégien (Bokmål) Turc + Japonais + Chinois traditionnel + Vietnamien Thème de l\'application Passer du mode clair au mode sombre, ou suivre l\'apparence du système. Thème clair diff --git a/app/src/main/res/values-in/strings_settings.xml b/app/src/main/res/values-in/strings_settings.xml index bd6ee6c5f3..d2eff886fe 100644 --- a/app/src/main/res/values-in/strings_settings.xml +++ b/app/src/main/res/values-in/strings_settings.xml @@ -172,6 +172,9 @@ Korea Norwegia (Bokmål) Turki + Jepang + Tionghoa Tradisional + Vietnam Tema Aplikasi Beralih antara terang, gelap, atau ikuti tampilan sistem. Tema Terang diff --git a/app/src/main/res/values-it/strings_settings.xml b/app/src/main/res/values-it/strings_settings.xml index eefd35f1c2..cff5cadc1e 100644 --- a/app/src/main/res/values-it/strings_settings.xml +++ b/app/src/main/res/values-it/strings_settings.xml @@ -176,6 +176,9 @@ Coreano Norvegese (Bokmål) Turco + Giapponese + Cinese tradizionale + Vietnamita Tema app Passa tra chiaro, scuro o segui l\'aspetto di sistema. Tema chiaro diff --git a/app/src/main/res/values-ja/plurals.xml b/app/src/main/res/values-ja/plurals.xml new file mode 100644 index 0000000000..c18d5b2cba --- /dev/null +++ b/app/src/main/res/values-ja/plurals.xml @@ -0,0 +1,45 @@ + + + + %d 件のプレイリストを共有中 + + + %1$d 件のプレイリストを %2$s にエクスポートしました + + + %d 曲をキューに追加しました + + + %d 曲が次に再生されます + + + %d 曲をお気に入りに追加しました + + + %d 曲をお気に入りから削除しました + + + %d ファイルを削除しました + + + %d ファイルを削除しますか? + + + %d 曲を選択中 + + + %1$d 曲 • %2$s + + + %d 曲が予約されました。 + + + %d 曲を選択中 + + + %d 曲 + + + %d 件のトピック + + diff --git a/app/src/main/res/values-ja/strings_settings.xml b/app/src/main/res/values-ja/strings_settings.xml index 6678113cc5..0492aa489a 100644 --- a/app/src/main/res/values-ja/strings_settings.xml +++ b/app/src/main/res/values-ja/strings_settings.xml @@ -13,6 +13,8 @@ ジェスチャー、触覚フィードバック、ナビゲーション動作 AI 連携(β) AI プロバイダー、API キー、モデル設定 + 生成パラメータ + 温度、サンプリング、プロンプト、楽曲データ設定 バックアップ & 復元 個人データのエクスポートと復元 開発者オプション @@ -644,6 +646,15 @@ PixelPlayer の開発者。 コミュニティスポットライト 大きな貢献をしたコラボレーターへの感謝。 + ライセンスと通知 + 依存関係および以前の貢献への帰属表示。 + オープンソースライセンス + 依存ライセンスとサードパーティ通知 + オープンソースライセンス + MIT風通知 + サードパーティ通知 + 閉じる + バンドルされたサードパーティ通知を読み込めませんでした。 オープンソースコントリビューター GitHub からのライブコントリビューターリスト。 %1$d 回のコントリビューション diff --git a/app/src/main/res/values-ko/plurals.xml b/app/src/main/res/values-ko/plurals.xml new file mode 100644 index 0000000000..da2e397128 --- /dev/null +++ b/app/src/main/res/values-ko/plurals.xml @@ -0,0 +1,45 @@ + + + + %d개의 재생목록을 공유하고 있습니다 + + + %1$d개의 재생목록을 %2$s로 내보냈습니다 + + + %d곡이 재생목록에 추가되었습니다 + + + %d곡이 다음에 재생됩니다 + + + %d곡이 즐겨찾기에 추가되었습니다 + + + %d곡이 즐겨찾기에서 제거되었습니다 + + + %d개의 파일이 삭제되었습니다 + + + %d개의 파일을 삭제하시겠습니까? + + + %d곡이 선택되었습니다 + + + %1$d곡 • %2$s + + + %d곡이 대기열에 추가되었습니다. + + + %d곡이 선택되었습니다 + + + %d곡 + + + %d개의 주제 + + diff --git a/app/src/main/res/values-ko/strings_settings.xml b/app/src/main/res/values-ko/strings_settings.xml index 1d1957f05f..83554e05be 100644 --- a/app/src/main/res/values-ko/strings_settings.xml +++ b/app/src/main/res/values-ko/strings_settings.xml @@ -637,6 +637,15 @@ PixelPlayer를 만드는 사람입니다. 커뮤니티 스포트라이트 큰 영향력을 미친 기여자들에 대한 감사의 표시입니다. + 라이선스 및 고지사항 + 의존성 및 이전 기여에 대한 저작권 표시. + 오픈 소스 라이선스 + 의존성 라이선스 및 서드파티 고지사항 + 오픈 소스 라이선스 + MIT 방식 고지사항 + 서드파티 고지사항 + 닫기 + 번들된 서드파티 고지사항을 불러올 수 없습니다. 오픈 소스 기여자 GitHub의 실시간 기여자 목록입니다. 기여 %1$d회 diff --git a/app/src/main/res/values-nb/plurals.xml b/app/src/main/res/values-nb/plurals.xml new file mode 100644 index 0000000000..f9b8355a09 --- /dev/null +++ b/app/src/main/res/values-nb/plurals.xml @@ -0,0 +1,59 @@ + + + + Delte %d spilleliste + Delte %d spillelister + + + Eksporterte %1$d spilleliste til %2$s + Eksporterte %1$d spillelister til %2$s + + + %d sang lagt til i køen + %d sanger lagt til i køen + + + %d sang skal spilles neste + %d sanger skal spilles neste + + + %d sang lagt til i favoritter + %d sanger lagt til i favoritter + + + %d sang fjernet fra favoritter + %d sanger fjernet fra favoritter + + + %d fil slettet + %d filer slettet + + + Slette %d fil? + Slette %d filer? + + + %d spor valgt + %d spor valgt + + + %1$d sang • %2$s + %1$d sanger • %2$s + + + %d spor satt opp. + %d spor satt opp. + + + %d sang valgt + %d sanger valgt + + + %d sang + %d sanger + + + %d emne + %d emner + + diff --git a/app/src/main/res/values-nb/strings_settings.xml b/app/src/main/res/values-nb/strings_settings.xml index 39af2eb945..2b0ff05225 100644 --- a/app/src/main/res/values-nb/strings_settings.xml +++ b/app/src/main/res/values-nb/strings_settings.xml @@ -637,6 +637,15 @@ Personen bak PixelPlayer. Samfunns-spotlight Anerkjennelse for samarbeidspartnere med stor innvirkning. + Lisenser og varsler + Tilskrivning for avhengigheter og tidligere bidrag. + Åpen kildekode-lisenser + Avhengighetslisenser og tredjepartsvarsler + Åpen kildekode-lisenser + MIT-lignende varsel + Tredjepartsvarsler + Lukk + Den bun­dlede tredjeparts­vars­let kunne ikke lastes. Bidragsytere til åpen kildekode Live bidragsyterliste fra GitHub. %1$d bidrag. diff --git a/app/src/main/res/values-ru/plurals.xml b/app/src/main/res/values-ru/plurals.xml new file mode 100644 index 0000000000..09a9a4feda --- /dev/null +++ b/app/src/main/res/values-ru/plurals.xml @@ -0,0 +1,87 @@ + + + + Общий доступ к %d плейлисту + Общий доступ к %d плейлистам + Общий доступ к %d плейлистам + Общий доступ к %d плейлистам + + + Экспортирован %1$d плейлист в %2$s + Экспортировано %1$d плейлиста в %2$s + Экспортировано %1$d плейлистов в %2$s + Экспортировано %1$d плейлистов в %2$s + + + %d песня добавлена в очередь + %d песни добавлены в очередь + %d песен добавлено в очередь + %d песен добавлено в очередь + + + %d песня будет воспроизведена следующей + %d песни будут воспроизведены следующими + %d песен будут воспроизведены следующими + %d песен будут воспроизведены следующими + + + %d песня добавлена в избранное + %d песни добавлены в избранное + %d песен добавлено в избранное + %d песен добавлено в избранное + + + %d песня удалена из избранного + %d песни удалены из избранного + %d песен удалено из избранного + %d песен удалено из избранного + + + %d файл удалён + %d файла удалено + %d файлов удалено + %d файлов удалено + + + Удалить %d файл? + Удалить %d файла? + Удалить %d файлов? + Удалить %d файлов? + + + %d трек выбран + %d трека выбрано + %d треков выбрано + %d треков выбрано + + + %1$d песня • %2$s + %1$d песни • %2$s + %1$d песен • %2$s + %1$d песен • %2$s + + + %d трек поставлен в очередь. + %d трека поставлено в очередь. + %d треков поставлено в очередь. + %d треков поставлено в очередь. + + + %d песня выбрана + %d песни выбрано + %d песен выбрано + %d песен выбрано + + + %d песня + %d песни + %d песен + %d песен + + + %d тема + %d темы + %d тем + %d тем + + diff --git a/app/src/main/res/values-ru/strings_settings.xml b/app/src/main/res/values-ru/strings_settings.xml index c99d6d76c1..3128db38db 100644 --- a/app/src/main/res/values-ru/strings_settings.xml +++ b/app/src/main/res/values-ru/strings_settings.xml @@ -637,6 +637,15 @@ Человек, стоящий за PixelPlayer. Гордость сообщества Признание участников, внёсших значительный вклад. + Лицензии и уведомления + Атрибуция для зависимостей и предыдущих вкладов. + Лицензии открытого ПО + Лицензии зависимостей и уведомления третьих лиц + Лицензии открытого ПО + Уведомление в стиле MIT + Уведомления третьих лиц + Закрыть + Не удалось загрузить встроенное уведомление третьей стороны. Участники open source Актуальный список участников с GitHub. %1$d вклад. diff --git a/app/src/main/res/values-tr/strings_settings.xml b/app/src/main/res/values-tr/strings_settings.xml index 31fba8ef40..ed299517fd 100644 --- a/app/src/main/res/values-tr/strings_settings.xml +++ b/app/src/main/res/values-tr/strings_settings.xml @@ -176,6 +176,9 @@ Korece Norveççe (Bokmål) Türkçe + Japonca + Geleneksel Çince + Vietnamca Uygulama Teması Açık, koyu tema arasında geçiş yapın veya sistem görünümünü takip edin. Açık Tema diff --git a/app/src/main/res/values-vi/plurals.xml b/app/src/main/res/values-vi/plurals.xml new file mode 100644 index 0000000000..6a26e37fbd --- /dev/null +++ b/app/src/main/res/values-vi/plurals.xml @@ -0,0 +1,45 @@ + + + + Đang chia sẻ %d danh sách phát + + + Đã xuất %1$d danh sách phát tới %2$s + + + Đã thêm %d bài hát vào hàng đợi + + + %d bài hát sẽ phát tiếp theo + + + Đã thêm %d bài hát vào yêu thích + + + Đã xóa %d bài hát khỏi yêu thích + + + Đã xóa %d tệp + + + Xóa %d tệp? + + + Đã chọn %d bài hát + + + %1$d bài hát • %2$s + + + %d bài hát đã được xếp hàng. + + + Đã chọn %d bài hát + + + %d bài hát + + + %d chủ đề + + diff --git a/app/src/main/res/values-vi/strings.xml b/app/src/main/res/values-vi/strings.xml index 2cb4e6d908..82cb0250ef 100644 --- a/app/src/main/res/values-vi/strings.xml +++ b/app/src/main/res/values-vi/strings.xml @@ -1,128 +1,128 @@ PixelPlayer - Music Player - App Name Change - We have changed our app\'s name from PixelPlay to PixelPlayer due to a trademark-related issue. Keep playing! - Do not show again + Trình phát nhạc + Đổi tên ứng dụng + Chúng tôi đã đổi tên ứng dụng từ PixelPlay thành PixelPlayer do vấn đề liên quan đến nhãn hiệu. Tiếp tục thưởng thức nhạc! + Không hiển thị lại - Home - Search - Library + Trang chủ + Tìm kiếm + Thư viện - Special Permission Required - To edit song metadata (.mp3 files), PixelPlayer needs special access to all files. This allows us to modify track tags directly. Please grant this permission on the next screen to enable metadata editing. - Grant Permission + Cần cấp quyền đặc biệt + Để chỉnh sửa siêu dữ liệu bài hát (tệp .mp3), PixelPlayer cần quyền truy cập đặc biệt vào tất cả tệp. Điều này cho phép chúng tôi chỉnh sửa trực tiếp thẻ bài hát. Vui lòng cấp quyền này ở màn hình tiếp theo để bật tính năng chỉnh sửa siêu dữ liệu. + Cấp quyền - Quick play - Unable to open that audio file. - Open full player + Phát nhanh + Không thể mở tệp âm thanh này. + Mở trình phát đầy đủ - Shuffle - Shuffle all songs - Shuffle All - Last Playlist - No playlist available to open + Phát ngẫu nhiên + Phát ngẫu nhiên tất cả bài hát + Phát ngẫu nhiên tất cả + Danh sách phát gần nhất + Không có danh sách phát nào để mở - Open Play Store - Continue beta - The Play Store link will be enabled from GitHub config. - PixelPlayer is now available on Google Play - Use the stable channel on Google Play for release updates while we keep beta builds active. + Mở Google Play + Tiếp tục beta + Liên kết Google Play sẽ được kích hoạt từ cấu hình GitHub. + PixelPlayer hiện đã có trên Google Play + Sử dụng kênh ổn định trên Google Play để cập nhật bản phát hành trong khi chúng tôi giữ các bản beta hoạt động. PixelPlayer - Release announcement - Coming soon + Thông báo phát hành + Sắp ra mắt - Thank you for using PixelPlayer! - HIGH %1$d - Close - SCORE - LVL %1$d - LIVES - LEVEL COMPLETE! - GAME OVER - Score: %1$d - Try Again? - Next Level - Restart Game - TAP TO RELAUNCH - Play Random Music - Brick Breaker - HIGH SCORE %1$d - Play - Drag to move the paddle + Cảm ơn bạn đã sử dụng PixelPlayer! + CAO %1$d + Đóng + ĐIỂM + CẤP %1$d + MẠNG + HOÀN THÀNH CẤP! + KẾT THÚC TRÒ CHƠI + Điểm: %1$d + Thử lại? + Cấp tiếp theo + Chơi lại + NHẤN ĐỂ KHỞI ĐỘNG LẠI + Phát nhạc ngẫu nhiên + Phá Gạch + ĐIỂM CAO %1$d + Chơi + Kéo để di chuyển thanh chắn - Close player - Processing playback action… - Playback error: %1$s + Đóng trình phát + Đang xử lý hành động phát lại… + Lỗi phát lại: %1$s - Back + Quay lại OK - Cancel - Dismiss - Error - Search - Clear search - All - Confirm - Saved! - Selected + Hủy + Bỏ qua + Lỗi + Tìm kiếm + Xóa tìm kiếm + Tất cả + Xác nhận + Đã lưu! + Đã chọn %1$d%% - Artist - Select All - Clear - Unknown error + Nghệ sĩ + Chọn tất cả + Xóa + Lỗi không xác định - Save - Done - Reset - Apply - Shuffle - Copy - Share - Undo - Import - Delete - Export - Merge - Rename - Create - Lyrics - Settings - Album Art - Playlist - Unknown track - Unknown artist - Unknown album - Close - Add - Remove - Play - Previous track - Next track - Favorite - Pause - Repeat - Options - Shuffle Play - More options for %1$s - Expand menu - Next - Finish - Reset defaults - Export all - Merge all - Share all - Play album - Shuffle play album - Album art for %1$s + Lưu + Xong + Đặt lại + Áp dụng + Phát ngẫu nhiên + Sao chép + Chia sẻ + Hoàn tác + Nhập + Xóa + Xuất + Gộp + Đổi tên + Tạo + Lời bài hát + Cài đặt + Ảnh bìa album + Danh sách phát + Bài hát không xác định + Nghệ sĩ không xác định + Album không xác định + Đóng + Thêm + Xóa + Phát + Bài trước + Bài tiếp + Yêu thích + Tạm dừng + Lặp lại + Tùy chọn + Phát ngẫu nhiên + Thêm tùy chọn cho %1$s + Mở rộng menu + Tiếp theo + Hoàn thành + Đặt lại mặc định + Xuất tất cả + Gộp tất cả + Chia sẻ tất cả + Phát album + Phát ngẫu nhiên album + Ảnh bìa cho %1$s diff --git a/app/src/main/res/values-vi/strings_changelogs.xml b/app/src/main/res/values-vi/strings_changelogs.xml index 21e8758403..0c956f7d29 100644 --- a/app/src/main/res/values-vi/strings_changelogs.xml +++ b/app/src/main/res/values-vi/strings_changelogs.xml @@ -1,150 +1,150 @@ - Changelog - View on GitHub - Improvements - Fixes - What\'s New - Added + Nhật ký thay đổi + Xem trên GitHub + Cải tiến + Sửa lỗi + Có gì mới + Đã thêm - Chromecast support for casting audio from your device. - In-app changelog to keep you updated on the latest features. - Support for .LRC files, both embedded and external. - Offline lyrics support. - Synchronized lyrics (synced with the song). - New screen to view the full queue. - Reorder and remove songs from the queue. - Mini-player gestures (swipe down to close). - Added more material animations. - New settings to customize the look and feel. - New settings to clear the cache. + Hỗ trợ Chromecast để truyền âm thanh từ thiết bị. + Nhật ký thay đổi trong ứng dụng để cập nhật tính năng mới nhất. + Hỗ trợ tệp .LRC, cả nhúng và bên ngoài. + Hỗ trợ lời bài hát ngoại tuyến. + Lời bài hát đồng bộ (đồng bộ với bài hát). + Màn hình mới để xem hàng đợi đầy đủ. + Sắp xếp lại và xóa bài hát khỏi hàng đợi. + Cử chỉ mini-player (vuốt xuống để đóng). + Thêm nhiều hoạt ảnh material hơn. + Cài đặt mới để tùy chỉnh giao diện. + Cài đặt mới để xóa cache. - Complete redesign of the user interface. - Complete redesign of the player. - Performance improvements in the library. - Improved application startup speed. - The AI now provides better results. + Thiết kế lại hoàn toàn giao diện người dùng. + Thiết kế lại hoàn toàn trình phát. + Cải tiến hiệu suất trong thư viện. + Cải thiện tốc độ khởi động ứng dụng. + AI giờ cung cấp kết quả tốt hơn. - Fixed various bugs in the tag editor. - Fixed a bug where the playback notification was not clearing. - Fixed several bugs that caused the app to crash. + Sửa các lỗi trong trình chỉnh sửa tag. + Sửa lỗi thông báo phát không bị xóa. + Sửa nhiều lỗi gây crash ứng dụng. - Introduced a richer listening stats hub with deeper insights into your sessions. - Launched a floating quick player to instantly open and preview local files. - Added a folders tab with a tree-style navigator and playlist-ready view. + Giới thiệu trung tâm thống kê nghe phong phú hơn với phân tích sâu hơn về các phiên. + Phát hành trình phát nhanh nổi để mở và xem trước tệp cục bộ ngay lập tức. + Thêm tab thư mục với trình điều hướng kiểu cây và chế độ xem sẵn sàng cho playlist. - Refined the overall Material 3 UI for a cleaner and more cohesive experience. - Metadata editing now supports cover art change. - Smoothed out animations and transitions across the app for more fluid navigation. - Enhanced the artist screen layout with richer details and polish. - Upgraded DailyMix and YourMix generation with smarter, more diverse selections. - Strengthened the AI playlist generation. - Improved search relevance and presentation for faster discovery. - Expanded support for a broader range of audio file formats. + Hoàn thiện giao diện Material 3 tổng thể để có trải nghiệm sạch sẽ và nhất quán hơn. + Chỉnh sửa metadata giờ hỗ trợ thay đổi ảnh bìa. + Làm mượt hoạt ảnh và chuyển tiếp trên toàn ứng dụng để điều hướng mượt mà hơn. + Cải thiện bố cục màn hình nghệ sĩ với chi tiết phong phú hơn. + Nâng cấp tạo DailyMix và YourMix với lựa chọn thông minh và đa dạng hơn. + Tăng cường tạo playlist AI. + Cải thiện relevance và trình bày tìm kiếm để khám phá nhanh hơn. + Mở rộng hỗ trợ cho nhiều định dạng tệp âm thanh hơn. - Resolved metadata quirks so song details stay accurate everywhere. - Restored notification shortcuts so they reliably jump back into playback. + Giải quyết các vấn đề metadata để chi tiết bài hát luôn chính xác. + Khôi phục shortcut thông báo để chúng đáng tin cậy quay lại phát. - Major navigation redesign - New file explorer for choosing source directories - New Connectivity and casting functionalities - Seamless continuity between remote devices - Gapless transition between songs - Crossfade control - New Custom Transitions feature (only for playlists) - Keep playing after closed the app - UI Optimizations - Improved stats feature - Redesigned Queue control with more features - Improved different filetypes support for playing and metadata editing - Improved permission controller - Minor bug fixes + Thiết kế lại điều hướng chính + Trình khám phá tệp mới để chọn thư mục nguồn + Chức năng kết nối và truyền mới + Tiếp nối liền mạch giữa các thiết bị từ xa + Chuyển tiếp không khoảng cách giữa các bài hát + Điều khiển crossfade + Tính năng Chuyển tiếp tùy chỉnh mới (chỉ cho playlist) + Tiếp tục phát sau khi đóng ứng dụng + Tối ưu hóa giao diện + Cải thiện tính năng thống kê + Thiết kế lại điều khiển hàng đợi với nhiều tính năng hơn + Cải thiện hỗ trợ các loại tệp khác nhau để phát và chỉnh sửa metadata + Cải thiện bộ điều khiển quyền + Sửa lỗi nhỏ - Material 3 Expressive UI Update - 10-band Equalizer & Effects - New Library Sync Flow - AI Integration (Gemini Models) - M3U Playlist Import/Export - Deezer Artist Artwork Integration - Custom Playlist Covers - Settings Architecture Refactor - Queue & Player Animations - Baseline Profiles & Performance - Better Lyrics System with Sync Offset + Cập nhật giao diện Material 3 Expressive + Equalizer 10 dải & Hiệu ứng + Luồng đồng bộ thư viện mới + Tích hợp AI (Gemini Models) + Nhập/Xuất playlist M3U + Tích hợp ảnh nghệ sĩ Deezer + Ảnh bìa playlist tùy chỉnh + Thiết kế lại kiến trúc cài đặt + Hoạt ảnh Hàng đợi & Trình phát + Baseline Profiles & Hiệu suất + Hệ thống lời bài hát tốt hơn với bù đồng bộ - Casting Stability Improvements - Player Sheet Stability - General Bug Fixes & Cleanup + Cải thiện ổn định truyền + Ổn định_sheet trình phát + Sửa lỗi chung & Dọn dẹp - Android Auto support is now available for in-car playback. - Wear OS support is live, including better watch-to-phone playback controls. - Cloud integrations expanded with Telegram, NetEase, QQ Music, and Google Drive improvements. - Recently Played and persistent queue restoration keep your listening session ready. - Backup & Restore v3 and account management tools are now included. - Lyrics got smarter with manual search fallback and storage improvements. + Hỗ trợ Android Auto hiện có sẵn để phát trong xe. + Hỗ trợ Wear OS đã hoạt động, bao gồm điều khiển phát tốt hơn từ đồng hồ. + Tích hợp đám mây mở rộng với cải tiến Telegram, NetEase, QQ Music và Google Drive. + Phát gần đây và khôi phục hàng đợi liên tục giúp phiên nghe của bạn sẵn sàng. + Sao lưu & Khôi phục v3 và công cụ quản lý tài khoản đã được bao gồm. + Lời bài hát thông minh hơn với tìm kiếm thủ công và cải thiện lưu trữ. - Big performance pass across startup, library, queue, and player interactions. - Player, Cast, Lyrics, Artist, and Genre surfaces were redesigned for smoother use. - Navigation and search flows are more reliable, with safer route handling. - Audio playback compatibility improved for more devices and formats. - Multi-selection workflows were expanded across songs, albums, and playlists. + Đợt cải thiện hiệu suất lớn trên khởi động, thư viện, hàng đợi và tương tác trình phát. + Trình phát, Truyền, Lời bài hát, Nghệ sĩ và Thể loại được thiết kế lại để sử dụng mượt mà hơn. + Điều hướng và luồng tìm kiếm đáng tin cậy hơn, với xử lý tuyến đường an toàn hơn. + Cải thiện khả năng tương thích phát âm thanh cho nhiều thiết bị và định dạng hơn. + Mở rộng quy trình đa chọn cho bài hát, album và playlist. - Queue and shuffle behavior is now more stable and predictable. - Several background playback and casting edge cases were fixed. - Sleep Timer, Files tab navigation, and album artist crash issues were fixed. - Widget loading and service stability were improved to reduce overheating/memory issues. - General bug fixes and UI polish across the app. + Hành vi hàng đợi và phát ngẫu nhiên giờ ổn định và dự đoán được hơn. + Giải quyết nhiều trường hợp phát nền và truyền ngoại lệ. + Sửa lỗi Hẹn giờ ngủ, điều hướng tab Tệp và lỗi crash artist album. + Cải thiện tải widget và ổn định dịch vụ để giảm quá nhiệt/vấn đề bộ nhớ. + Sửa lỗi chung và hoàn thiện giao diện trên toàn ứng dụng. - Wear OS: Music transfer, local playback, queue sync, and remote control from watch. - AI: Groq AI and OpenRouter (experimental) integration with token optimization. - Cloud: Jellyfin support added. - Lyrics: Synced translation with dedicated toggle, Kugou LRC format support, text alignment customization, and improved remote loading. - UI/UX: Compact navigation bar mode, dynamic themes from album art palette, marquee for long titles, and new sorting options. - Telegram: Native topics support and enhanced display modes. + Wear OS: Chuyển nhạc, phát cục bộ, đồng bộ hàng đợi và điều khiển từ xa từ đồng hồ. + AI: Tích hợp Groq AI và OpenRouter (thử nghiệm) với tối ưu hóa token. + Đám mây: Thêm hỗ trợ Jellyfin. + Lời bài hát: Dịch đồng bộ với nút chuyển đổi riêng, hỗ trợ định dạng Kugou LRC, tùy chỉnh căn chỉnh văn bản và cải thiện tải từ xa. + Giao diện/Trải nghiệm: Chế độ thanh điều_navigation thu gọn, theme động từ bảng màu ảnh bìa, marquee cho tiêu đề dài và tùy chọn sắp xếp mới. + Telegram: Hỗ trợ chủ đề gốc và chế độ hiển thị nâng cao. - Audio Engine: Complete overhaul with support for more formats (MIDI, ALAC, M4A) and decoder optimization. - Efficiency: Drastic energy consumption reduction, overheating fixes, and background task optimization (SyncWorker). - Database: Massive query optimizations and redesigned cover art cache to prevent data loss. - Startup: Improved loading time via Baseline Profile optimization. + Động cơ âm thanh: Đại tu hoàn toàn với hỗ trợ nhiều định dạng hơn (MIDI, ALAC, M4A) và tối ưu hóa bộ giải mã. + Hiệu quả: Giảm tiêu hao năng lượng mạnh, sửa lỗi quá nhiệt và tối ưu hóa tác vụ nền (SyncWorker). + Cơ sở dữ liệu: Tối ưu hóa query lớn và thiết kế lại cache ảnh bìa để ngăn mất dữ liệu. + Khởi động: Cải thiện thời gian tải qua tối ưu hóa Baseline Profile. - Playback: Fixed stuttering in Opus/MP3, ReplayGain errors during crossfades, and startup issues on Samsung decoders. - Stability: Eliminated crashes on startup, artist navigation, and Android 12+ devices. - UI: Fixed cover art flickering, text overflow in non-Latin scripts, and navigation bar/miniplayer behavior. - Security: Hardened credential handling, storage permissions, and media server communication. + Phát: Sửa lỗi giật cục bộ trong Opus/MP3, lỗi ReplayGain khi crossfade và lỗi khởi động trên bộ giải mã Samsung. + Ổn định: Loại bỏ crash khi khởi động, điều hướng nghệ sĩ và trên thiết bị Android 12+. + Giao diện: Sửa lỗi nhấp nháy ảnh bìa, tràn văn bản trong chữ viết phi Latin và hành vi thanh điều hướng/miniplayer. + Bảo mật: Tăng cường xử lý thông tin đăng nhập, quyền lưu trữ và giao tiếp media server. - Localization: Spanish, French, Russian, Simplified Chinese, Indonesian, Italian + Địa phương hóa: Tiếng Tây Ban Nha, Tiếng Pháp, Tiếng Nga, Tiếng Trung giản thể, Tiếng Indonesia, Tiếng Ý - Google Drive integration with player lifecycle management. - Batch song metadata editing (tags and cover art). - AI lyrics translation with customizable Wear OS preferences. - Lag diagnostic tool and multi-selection on Search screen. - Arabic & Turkish support, with localized http URL local-network options. + Tích hợp Google Drive với quản lý vòng đời trình phát. + Chỉnh sửa hàng loạt metadata bài hát (tag và ảnh bìa). + Dịch lời bài hát AI với tùy chỉnh Wear OS. + Công cụ chẩn đoán lag và đa chọn trên màn hình Tìm kiếm. + Hỗ trợ tiếng Ả Rập & Thổ Nhĩ Kỳ, với tùy chọn mạng cục bộ http URL được địa phương hóa. - Drastic battery saving (audio offload and UI polling gates). - Optimized queue management (faster insertions and explicit indexing). - Material 3 Expressive motion animations for transition screens. - Refactored library synchronization via throttled scanning. + Tiết kiệm pin mạnh (tải âm thanh ngoại vi và kiểm soát polling giao diện). + Tối ưu hóa quản lý hàng đợi (chèn nhanh hơn và đánh chỉ mục rõ ràng). + Hoạt ảnh chuyển động Material 3 Expressive cho màn hình chuyển tiếp. + Thiết kế lại đồng bộ thư viện qua quét được kiểm soát. - Resolved stuttering/skipping playback lags and buffering issues. - Fixed external song deletion sync and metadata consistency. - Fixed memory issues, crashes, and layout glitches on Wear OS and phone. + Giải quyết các vấn đề lag giật/bỏ qua phát và buffering. + Sửa đồng bộ xóa bài hát bên ngoài và nhất quán metadata. + Sửa vấn đề bộ nhớ, crash và lỗi bố cục trên Wear OS và điện thoại. - \ No newline at end of file + diff --git a/app/src/main/res/values-vi/strings_cloud_services.xml b/app/src/main/res/values-vi/strings_cloud_services.xml index df633dab6a..33db5f6580 100644 --- a/app/src/main/res/values-vi/strings_cloud_services.xml +++ b/app/src/main/res/values-vi/strings_cloud_services.xml @@ -1,228 +1,228 @@ - Telegram Login - You are editing your number. Sending code again will replace the previous one. - Working… - Initializing Telegram… - Logging out… - Closing session… - Session closed. Re-open login to continue. - Preparing secure Telegram session… - Waiting for Telegram response… - Connect Telegram - Connect Telegram to stream music from your channels and chats. - Phone Number - Enter your Telegram number. You can come back and edit it later. - Phone number + Đăng nhập Telegram + Bạn đang chỉnh sửa số điện thoại. Gửi lại mã sẽ thay thế mã trước đó. + Đang xử lý… + Đang khởi tạo Telegram… + Đang đăng xuất… + Đang đóng phiên… + Đã đóng phiên. Mở lại đăng nhập để tiếp tục. + Đang chuẩn bị phiên Telegram bảo mật… + Đang chờ phản hồi từ Telegram… + Kết nối Telegram + Kết nối Telegram để phát nhạc từ các kênh và cuộc trò chuyện của bạn. + Số điện thoại + Nhập số Telegram của bạn. Bạn có thể quay lại chỉnh sửa sau. + Số điện thoại 1 5551234567 - Send Code - Verification Code - Enter the code from Telegram. If the number is wrong, go back and edit it. - Code + Gửi mã + Mã xác minh + Nhập mã từ Telegram. Nếu số sai, hãy quay lại chỉnh sửa. + 12345 - Edit phone - Resend code - Verify Code - Two-Step Password - Enter your Telegram password. You can still go back to fix your number. - Password - Verify Password - Please wait… + Chỉnh sửa số điện thoại + Gửi lại mã + Xác minh mã + Mật khẩu hai bước + Nhập mật khẩu Telegram của bạn. Bạn vẫn có thể quay lại để sửa số điện thoại. + Mật khẩu + Xác minh mật khẩu + Vui lòng chờ… - Telegram Channels - Add Channel - Public Telegram channel - Syncing - Sync now - Collapse topics - Show topics - Channel options - Topics - Syncing channel - Updating songs from Telegram - Fetch latest songs from this channel - Remove channel - Stop syncing and remove cached songs - Remove channel? - %1$s will stop syncing and all cached songs from this channel will be removed. - Remove - No Channels Synced - Add public Telegram channels to sync\nyour music library - Add channel - Never synced - Synced %1$s + Kênh Telegram + Thêm kênh + Kênh Telegram công khai + Đang đồng bộ + Đồng bộ ngay + Thu gọn chủ đề + Hiện chủ đề + Tùy chọn kênh + Chủ đề + Đang đồng bộ kênh + Đang cập nhật bài hát từ Telegram + Lấy bài hát mới nhất từ kênh này + Xóa kênh + Dừng đồng bộ và xóa bài hát đã lưu + Xóa kênh? + %1$s sẽ dừng đồng bộ và tất cả bài hát đã lưu từ kênh này sẽ bị xóa. + Xóa + Chưa đồng bộ kênh nào + Thêm kênh Telegram công khai để đồng bộ\nthư viện nhạc của bạn + Thêm kênh + Chưa đồng bộ + Đã đồng bộ %1$s - Add Channel - Search for a public Telegram channel to sync its music - \@channelname or link - Searching… - Search for a channel - Enter a public channel username or link\nto sync its audio files + Thêm kênh + Tìm kiếm kênh Telegram công khai để đồng bộ nhạc + \@channelname hoặc link + Đang tìm kiếm… + Tìm kiếm kênh + Nhập tên người dùng hoặc link kênh công khai\nde đồng bộ các tệp âm thanh - %d song - %d songs + %d bài hát + %d bài hát - %d topic - %d topics + %d chủ đề + %d chủ đề Subsonic - Manage Navidrome, Airsonic and other Subsonic-compatible servers. + Quản lý Navidrome, Airsonic và các server tương thích Subsonic khác. - Tap sync to fetch your Jellyfin playlists - Manage your Jellyfin server connection. + Nhấn đồng bộ để lấy playlist Jellyfin của bạn + Quản lý kết nối server Jellyfin của bạn. - Music folders - Tap + to add a Drive folder - No folders added yet - %1$d folders synced - Add folder + Thư mục nhạc + Nhấn + để thêm thư mục Drive + Chưa thêm thư mục nào + %1$d thư mục đã đồng bộ + Thêm thư mục - Select playlist type - Choose which playlists to sync: - All playlists - Created & collected - Created playlists - Collected playlists + Chọn loại playlist + Chọn playlist muốn đồng bộ: + Tất cả playlist + Tạo và thu thập + Playlist đã tạo + Playlist đã thu thập - %1$d playlists synced - Playlists - Sync - No playlists synced yet - Tap sync to fetch your playlists - Quick actions - Sync library - Disconnect - %1$d songs + %1$d playlist đã đồng bộ + Playlist + Đồng bộ + Chưa đồng bộ playlist nào + Nhấn đồng bộ để lấy playlist của bạn + Thao tác nhanh + Đồng bộ thư viện + Ngắt kết nối + %1$d bài hát - Syncing - Syncing library… - Fetching playlists… - Syncing playlist: %1$s - Updating local library… - Sync complete - Fetching album list… - Fetching songs from %1$s… - Saving %1$d songs to database… - No library songs found - Library sync complete - Syncing… - Error: %1$s + Đang đồng bộ + Đang đồng bộ thư viện… + Đang lấy playlist… + Đang đồng bộ playlist: %1$s + Đang cập nhật thư viện cục bộ… + Đồng bộ hoàn tất + Đang lấy danh sách album… + Đang lấy bài hát từ %1$s… + Đang lưu %1$d bài hát vào cơ sở dữ liệu… + Không tìm thấy bài hát trong thư viện + Đồng bộ thư viện hoàn tất + Đang đồng bộ… + Lỗi: %1$s - Sync - Sync all - Log out - Sync all playlists - User avatar + Đồng bộ + Đồng bộ tất cả + Đăng xuất + Đồng bộ tất cả playlist + Ảnh đại diện người dùng - No internet connection - This content requires an internet connection. Please check your network settings and try again. - You\'re offline - Please check your internet connection and try again to access this content. + Không có kết nối internet + Nội dung này yêu cầu kết nối internet. Vui lòng kiểm tra cài đặt mạng và thử lại. + Bạn đang ngoại tuyến + Vui lòng kiểm tra kết nối internet và thử lại để truy cập nội dung này. - Connect - Connecting… - Enter your server URL and account credentials. - Connection details - Hide password - Password - Enter password - Prefill http:// - Server URL - Show password + Kết nối + Đang kết nối… + Nhập URL server và thông tin đăng nhập tài khoản. + Chi tiết kết nối + Ẩn mật khẩu + Mật khẩu + Nhập mật khẩu + Điền sẵn http:// + URL server + Hiện mật khẩu Telegram - Username + Tên người dùng admin - Welcome, %1$s! + Chào mừng, %1$s! - Compatible with Navidrome, Gonic, Airsonic, and other Subsonic-compatible servers - Supports Navidrome, Airsonic, Gonic, Ampache and other servers compatible with the Subsonic API. - App password also works if your server supports it. - Prefill https:// - Connect to your self-hosted music server + Tương thích với Navidrome, Gonic, Airsonic và các server tương thích Subsonic khác + Hỗ trợ Navidrome, Airsonic, Gonic, Ampache và các server tương thích với API Subsonic. + Mật khẩu ứng dụng cũng hoạt động nếu server của bạn hỗ trợ. + Điền sẵn https:// + Kết nối với server nhạc tự host của bạn Navidrome - Use the full https:// base address of your server. + Sử dụng địa chỉ https:// đầy đủ của server. https://music.example.com - This is your Subsonic or Navidrome account name. + Đây là tên tài khoản Subsonic hoặc Navidrome của bạn. Subsonic / Navidrome Subsonic - Enter your Jellyfin server URL and account credentials. - Connects to Jellyfin servers for streaming your music library - Connects to Jellyfin servers. Both HTTP and HTTPS are supported for local network access. + Nhập URL server Jellyfin và thông tin đăng nhập tài khoản. + Kết nối với server Jellyfin để phát nhạc từ thư viện của bạn + Kết nối với server Jellyfin. Cả HTTP và HTTPS đều được hỗ trợ cho truy cập mạng cục bộ. Jellyfin - Your Jellyfin account password. - Connect to your Jellyfin media server + Mật khẩu tài khoản Jellyfin của bạn. + Kết nối với server đa phương tiện Jellyfin của bạn Jellyfin - Full URL of your Jellyfin server, including port. + URL đầy đủ của server Jellyfin, bao gồm cổng. http://192.168.1.100:8096 - Your Jellyfin account username. + Tên người dùng tài khoản Jellyfin của bạn. - Stream music files directly from your Google Drive - Connect Google Drive - Google Drive connected! - Create \"PixelPlayer Music\" - Create a new folder here for your music - No folders here - Open folder - Choose or create a folder to use as your music source - Select a music folder - Setting up Google Drive… - Sign in with Google + Phát tệp nhạc trực tiếp từ Google Drive của bạn + Kết nối Google Drive + Đã kết nối Google Drive! + Tạo \"PixelPlayer Music\" + Tạo thư mục mới tại đây cho nhạc của bạn + Không có thư mục nào ở đây + Mở thư mục + Chọn hoặc tạo thư mục để sử dụng làm nguồn nhạc + Chọn thư mục nhạc + Đang thiết lập Google Drive… + Đăng nhập bằng Google Google Drive - Use + Sử dụng - Could not read session cookies. - Done - Exit - No cookies found. Log in first. - Page is taking too long to load. Use refresh or try another network. + Không thể đọc cookie phiên. + Xong + Thoát + Không tìm thấy cookie. Hãy đăng nhập trước. + Trang tải quá chậm. Hãy thử làm mới hoặc đổi mạng. + - Saving… - Stay - Page load timed out. You can retry without losing your progress. - Web back - You can come back later. Current page state will be discarded when closing. - Web forward - Refresh - Retry - Open home - WebView load failed. + Đang lưu… + Ở lại + Tải trang bị timeout. Bạn có thể thử lại mà không mất tiến trình. + Quay lại web + Bạn có thể quay lại sau. Trạng thái trang hiện tại sẽ bị xóa khi đóng. + Tiến lên web + Làm mới + Thử lại + Mở trang chủ + Tải WebView thất bại. - Failed to read NetEase cookies: %1$s - Exit NetEase login? - HTTP %1$d while loading NetEase. - Login not detected yet. Complete NetEase login before pressing Done. - Login to NetEase Music - Security note: your password is entered only in NetEase web pages. PixelPlayer stores session cookies (MUSIC_U) to sync your library. + Không thể đọc cookie NetEase: %1$s + Thoát đăng nhập NetEase? + HTTP %1$d khi tải NetEase. + Chưa phát hiện đăng nhập. Hoàn tất đăng nhập NetEase trước khi nhấn Xong. + Đăng nhập NetEase Music + Lưu ý bảo mật: Mật khẩu của bạn chỉ được nhập trên các trang web NetEase. PixelPlayer lưu cookie phiên (MUSIC_U) để đồng bộ thư viện. NetEase Music - Failed to read QQ Music cookies: %1$s - Exit QQ Music login? - HTTP %1$d while loading QQ Music. - Login not detected yet. Complete QQ Music login before pressing Done. - Login to QQ Music - Security note: your password is entered only in QQ Music web pages. PixelPlayer stores session cookies to sync your library. + Không thể đọc cookie QQ Music: %1$s + Thoát đăng nhập QQ Music? + HTTP %1$d khi tải QQ Music. + Chưa phát hiện đăng nhập. Hoàn tất đăng nhập QQ Music trước khi nhấn Xong. + Đăng nhập QQ Music + Lưu ý bảo mật: Mật khẩu của bạn chỉ được nhập trên các trang web QQ Music. PixelPlayer lưu cookie phiên để đồng bộ thư viện. QQ Music - \ No newline at end of file + diff --git a/app/src/main/res/values-vi/strings_equalizer.xml b/app/src/main/res/values-vi/strings_equalizer.xml index c633b7bf65..e1ebf496a4 100644 --- a/app/src/main/res/values-vi/strings_equalizer.xml +++ b/app/src/main/res/values-vi/strings_equalizer.xml @@ -1,57 +1,57 @@ - Name cannot be empty - Rename + Tên không được để trống + Đổi tên - Change view mode - Disable equalizer - Enable equalizer - Edit - Edit presets - Custom preset - Presets - Update - Bass Boost + Đổi chế độ xem + Tắt equalizer + Bật equalizer + Chỉnh sửa + Chỉnh sửa preset + Preset tùy chỉnh + Preset + Cập nhật + Tăng cường bass Virtualizer - Loudness - Not supported - Not supported on this device - Volume - Frequency Response + Độ to + Không hỗ trợ + Không hỗ trợ trên thiết bị này + Âm lượng + Phản hồi tần số Hz Bass - Low Mids - High Mids + Trung thấp + Trung cao Treble - Bass / Low - Mid / High - Page %1$d - Reset duration - Save New + Bass / Thấp + Trung / Cao + Trang %1$d + Đặt lại thời lượng + Lưu mới - Saved Presets - No custom presets saved yet. - Unpin - Pin - Rename - Delete + Preset đã lưu + Chưa lưu preset tùy chỉnh nào. + Bỏ ghim + Ghim + Đổi tên + Xóa - Save custom preset - Enter a name for your custom equalizer preset. - Preset name - Rename preset + Lưu preset tùy chỉnh + Nhập tên cho preset equalizer tùy chỉnh của bạn. + Tên preset + Đổi tên preset - Manage presets - Drag to reorder • Tap the eye to show or hide - Reorder - Reset presets - This will restore the default preset order and visibility. Continue? - Reset to default - Visible - Hidden + Quản lý preset + Kéo để sắp xếp lại • Nhấn mắt để hiện hoặc ẩn + Sắp xếp lại + Đặt lại preset + Thao tác này sẽ khôi phục thứ tự và hiển thị preset mặc định. Tiếp tục? + Đặt lại mặc định + Hiển thị + Ẩn diff --git a/app/src/main/res/values-vi/strings_home_screen.xml b/app/src/main/res/values-vi/strings_home_screen.xml index 2a7e3ad80f..5a995b8990 100644 --- a/app/src/main/res/values-vi/strings_home_screen.xml +++ b/app/src/main/res/values-vi/strings_home_screen.xml @@ -3,275 +3,275 @@ β Beta - Cloud Streaming - Changelog - Cloud Streaming - Stream music from your cloud accounts + Phát trực tuyến đám mây + Nhật ký thay đổi + Phát trực tuyến đám mây + Phát nhạc từ tài khoản đám mây của bạn Beta 0.7.5 β - Welcome to PixelPlayer 0.7.5-beta - You\'re using a beta build that may contain bugs, crashes, or experimental features. Help us improve by reporting issues. - What to expect - Bugs, crashes, or incomplete features may occur unexpectedly. - Some features may change or be removed without notice. - Beta builds may be more unstable than release versions. - Always check for updates before reporting a known issue. - What beta builds can change, break, or improve while testing. - GitHub issue shortcut - Search first, then open a focused report for bugs, crashes, requests, or questions. - Open existing issues - Report issue or crash - Share steps to reproduce, expected results, actual results, and your device/OS details. - How to report - A quick checklist before opening a new issue. - Before opening an issue - Search existing open and closed issues to avoid duplicates. - Update to the latest PixelPlayer version and confirm the problem still happens. - Restart the app and confirm the problem persists. - Try to reproduce it and write down the exact steps. - Which issue type? - Bug report: Something behaves incorrectly. - Feature request: Add a new feature or improvement. - Question: Use Discussions if enabled, or open an issue with a question label. - Bug report - Copy these fields when something behaves incorrectly or crashes. - Bug Report - Short summary: - Expected behavior: - Current behavior: - Steps to play/reproduce: 1. 2. 3. - How often does it happen? Always / Sometimes / Rarely. - Screenshot / video: if available. - Logs / stack trace: if available. - Environment - PixelPlayer version: - Install source: GitHub Release, debug build, nightly build, etc. - Android version: - Device model: - Extra context: SD card usage, special settings, permissions, etc. - Feature request - Copy these fields when you want a new feature or improvement. - Problem statement: What problem are you trying to solve? - Proposed solution: How should it work? - Alternatives considered: Any other approaches? - Scope: Which screens or flows are affected? - Mockup or reference image if available. - Titles, privacy, and scope - Make the report easy to triage and safe to share. - Good issue titles - Equalizer: Indicator shifts when switching preset tabs - Search: History list does not appear for empty query - Feature: Add \"Recently Added\" playlist sort option - Please avoid - Generic reports like \"It doesn\'t work\". - Multiple unrelated problems in one issue. - Unredacted logs or screenshots with private data. - Privacy note - Before posting logs, screenshots, or videos, remove personal or private information. - Nightly builds - How nightlies differ from releases, and what to include when they break. - Nightly builds are generated from the latest commit and may contain unfinished changes, temporary bugs, or regressions. They are more experimental than official releases. - Access them from the repository\'s GitHub Actions workflow artifacts if available. - Reporting nightly issues - When reporting an issue from a nightly build, always mention that it happened on a nightly build, not on the official release. Include the build date, workflow run name or number, or commit SHA if possible. Also check if the same problem happens on the latest official release. - Beta 0.5.0 upgrade - Clean install recommended - If you\'re coming from beta 0.5.0, this update may need fresh library data instead of older cached state. - If metadata or library entries look wrong - Wrong song metadata, mismatched artists or albums, or duplicate-looking entries usually mean a clean install is the fix. - Don\'t show again - Got it + Chào mừng đến với PixelPlayer 0.7.5-beta + Bạn đang sử dụng bản beta có thể chứa lỗi, sự cố hoặc các tính năng thử nghiệm. Hãy giúp chúng tôi cải thiện bằng cách báo cáo vấn đề. + Những gì cần mong đợi + Lỗi, sự cố hoặc các tính năng chưa hoàn thiện có thể xảy ra bất ngờ. + Một số tính năng có thể thay đổi hoặc bị xóa mà không thông báo. + Các bản beta có thể không ổn định hơn bản phát hành chính thức. + Luôn kiểm tra bản cập nhật trước khi báo cáo vấn đề đã biết. + Những gì bản beta có thể thay đổi, gây lỗi hoặc cải thiện trong quá trình thử nghiệm. + Shortcut vấn đề GitHub + Tìm kiếm trước, sau đó mở báo cáo tập trung cho lỗi, sự cố, yêu cầu hoặc câu hỏi. + Mở các vấn đề hiện có + Báo cáo vấn đề hoặc sự cố + Chia sẻ các bước tái tạo, kết quả mong đợi, kết quả thực tế và thông tin thiết bị/hệ điều hành của bạn. + Cách báo cáo + Danh sách kiểm tra nhanh trước khi mở vấn đề mới. + Trước khi mở vấn đề + Tìm kiếm các vấn đề đã mở và đã đóng để tránh trùng lặp. + Cập nhật lên phiên bản PixelPlayer mới nhất và xác nhận vấn đề vẫn xảy ra. + Khởi động lại ứng dụng và xác nhận vấn đề vẫn tồn tại. + Thử tái tạo vấn đề và ghi lại các bước chính xác. + Loại vấn đề nào? + Báo cáo lỗi: Một thứ gì đó hoạt động không chính xác. + Yêu cầu tính năng: Thêm tính năng mới hoặc cải tiến. + Câu hỏi: Sử dụng Thảo luận nếu được bật, hoặc mở vấn đề với nhãn câu hỏi. + Báo cáo lỗi + Sao chép các trường này khi có thứ gì đó hoạt động sai hoặc bị crash. + Báo cáo lỗi + Tóm tắt ngắn: + Hành vi mong đợi: + Hành vi hiện tại: + Các bước tái tạo: 1. 2. 3. + Tần suất xảy ra? Luôn luôn / Đôi khi / Hiếm khi. + Ảnh chụp màn hình / video: nếu có. + Nhật ký / stack trace: nếu có. + Môi trường + Phiên bản PixelPlayer: + Nguồn cài đặt: GitHub Release, bản debug, bản nightly, v.v. + Phiên bản Android: + Mẫu thiết bị: + Thông tin bổ sung: Sử dụng thẻ SD, cài đặt đặc biệt, quyền hạn, v.v. + Yêu cầu tính năng + Sao chép các trường này khi bạn muốn có tính năng mới hoặc cải tiến. + Mô tả vấn đề: Bạn đang cố gắng giải quyết vấn đề gì? + Giải pháp đề xuất: Nó nên hoạt động như thế nào? + Các giải pháp thay thế đã xem xét: Có cách tiếp cận nào khác không? + Phạm vi: Màn hình hoặc luồng nào bị ảnh hưởng? + Bản thiết kế hoặc hình ảnh tham khảo nếu có. + Tiêu đề, quyền riêng tư và phạm vi + Làm cho báo cáo dễ phân loại và an toàn để chia sẻ. + Tiêu đề vấn đề tốt + Equalizer: Chỉ báo dịch chuyển khi chuyển tab preset + Tìm kiếm: Danh sách lịch sử không hiển thị cho truy vấn trống + Tính năng: Thêm tùy chọn sắp xếp playlist \"Mới thêm\" + Vui lòng tránh + Các báo cáo chung chung như \"Nó không hoạt động\". + Nhiều vấn đề không liên quan trong một vấn đề. + Nhật ký hoặc ảnh chụp màn hình chưa chỉnh sửa với dữ liệu riêng tư. + Lưu ý quyền riêng tư + Trước khi đăng nhật ký, ảnh chụp màn hình hoặc video, hãy xóa thông tin cá nhân hoặc riêng tư. + Các bản nightly + Các bản nightly khác với bản phát hành như thế nào, và cần bao gồm gì khi chúng gặp sự cố. + Các bản nightly được tạo từ commit mới nhất và có thể chứa các thay đổi chưa hoàn thành, lỗi tạm thời hoặc sự suy giảm. Chúng mang tính thử nghiệm hơn các bản phát hành chính thức. + Truy cập chúng từ các artifact của quy trình GitHub Actions trong kho lưu trữ nếu có. + Báo cáo vấn đề nightly + Khi báo cáo vấn đề từ bản nightly, luôn đề cập rằng nó xảy ra trên bản nightly, không phải trên bản phát hành chính thức. Bao gồm ngày build, tên hoặc số quy trình chạy, hoặc commit SHA nếu có thể. Cũng kiểm tra xem vấn đề tương tự có xảy ra trên bản phát hành chính thức mới nhất không. + Nâng cấp Beta 0.5.0 + Khuyến nghị cài đặt sạch + Nếu bạn đến từ beta 0.5.0, bản cập nhật này có thể cần dữ liệu thư viện mới thay vì trạng thái cache cũ. + Nếu metadata hoặc mục thư viện hiển thị sai + Metadata bài hát sai, nghệ sĩ hoặc album không khớp, hoặc các mục bị trùng lặp thường có nghĩa là cài đặt sạch là giải pháp. + Không hiển thị lại + Đã hiểu - Oops! Something went wrong - The app crashed during your last session. Help us fix this by sharing the crash report. - Date: %1$s - Error: - Stack trace (preview): - Crash log - Crash log copied to clipboard - PixelPlayer crash report - Share crash report + Ối! Có gì đó không ổn + Ứng dụng đã bị crash trong phiên làm việc cuối. Hãy giúp chúng tôi sửa lỗi này bằng cách chia sẻ báo cáo sự cố. + Ngày: %1$s + Lỗi: + Stack trace (xem trước): + Nhật ký sự cố + Nhật ký sự cố đã được sao chép vào clipboard + Báo cáo sự cố PixelPlayer + Chia sẻ báo cáo sự cố DJ Mashup - Your\nMix - No data to show yet - Your mix will appear here when PixelPlayer finds songs or syncs a source. - Refresh + Phối\ncủa bạn + Chưa có dữ liệu hiển thị + Phối của bạn sẽ xuất hiện ở đây khi PixelPlayer tìm thấy bài hát hoặc đồng bộ nguồn. + Làm mới - DAILY MIX - Based on History - Check all of Daily Mix - Daily Mix + PHỐI HÀNG NGÀY + Dựa trên lịch sử + Xem tất cả Phối hàng ngày + Phối hàng ngày - Daily Mix + Phối hàng ngày - %1$d song • %2$s - %1$d songs • %2$s + %1$d bài hát • %2$s + %1$d bài hát • %2$s - Play it - AI Playlist Generator + Phát ngay + Trình tạo playlist AI - How your Daily Mix is built - Your Daily Mix is built from your favorite and most-played songs. We also add tracks from artists and genres you like so you can discover new music. - Tell the AI what you want to listen to today - We use a small sample to keep costs low - Updating… - Update Daily Mix + Phối hàng ngày được tạo như thế nào + Phối hàng ngày được tạo từ các bài hát yêu thích và được phát nhiều nhất của bạn. Chúng tôi cũng thêm các bản nhạc từ nghệ sĩ và thể loại bạn thích để bạn có thể khám phá nhạc mới. + Hãy cho AI biết bạn muốn nghe gì hôm nay + Chúng tôi sử dụng mẫu nhỏ để giữ chi phí thấp + Đang cập nhật… + Cập nhật Phối hàng ngày - Perfectly Curated - Daily Mix - Your sonic journey is ready - AI Playlist Generator - Describe the vibe, mood, or activity and let AI curate the perfect playlist from your library. - Playlist size - Min songs - Max songs - e.g. Chill evening vibes, upbeat workout energy… - Tap to Retry - Sonic journey synthesized! - Ready to Play - Generating… - Generate Playlist + Được tuyển chọn hoàn hảo + Phối hàng ngày + Hành trình âm nhạc của bạn đã sẵn sàng + Trình tạo playlist AI + Mô tả tâm trạng, cảm xúc hoặc hoạt động và để AI tuyển chọn playlist hoàn hảo từ thư viện của bạn. + Kích thước playlist + Tối thiểu bài hát + Tối đa bài hát + ví dụ: Tâm trạng buổi tối thư giãn, năng lượng tập luyện sôi động… + Nhấn để thử lại + Hành trình âm nhạc đã được tổng hợp! + Sẵn sàng phát + Đang tạo… + Tạo playlist - Recently Played + Phát gần đây - Recently Played - Play latest - No recent plays in %1$s - Change the range or play more songs to fill this timeline. - Recently Played - Today - Yesterday + Phát gần đây + Phát mới nhất + Không có phát nào gần đây trong %1$s + Thay đổi phạm vi hoặc phát thêm bài hát để điền vào dòng thời gian này. + Phát gần đây + Hôm nay + Hôm qua - Listening stats - Total plays - Avg per day - Top track - %1$s • %2$d plays + Thống kê nghe + Tổng số lần phát + Trung bình/ngày + Bài hát hàng đầu + %1$s • %2$d lần phát - Listening Stats - Refresh listening stats - Today - Week to Date - Month to Date - Year to Date - All Time - Listening - Plays - Listening timeline - Listening time - Total listening captured in the selected range. - Play count - How many sessions you completed per segment. - Avg. session - Average listening length for each segment. - Split into 4-hour windows to reveal your daily rhythm. - Daily bars make week-to-week habits easy to compare. - Weekly bars show how the month is trending. - Monthly bars show seasonality across the year. - Yearly bars summarize your full history. - No listening data yet - Press play to start building your listening timeline - Daily rhythm - Weekly rhythm - Monthly rhythm - Year at a glance - All-time progression - Grouped into 4-hour segments - Grouped by day of week - Grouped by week of month - Grouped by month - Grouped by year - Peak segment - %1$d plays + Thống kê nghe + Làm mới thống kê nghe + Hôm nay + Tuần đến nay + Tháng đến nay + Năm đến nay + Tất cả thời gian + Nghe + Lần phát + Dòng thời gian nghe + Thời gian nghe + Tổng thời gian nghe được ghi lại trong phạm vi đã chọn. + Số lần phát + Bạn hoàn thành bao nhiêu phiên mỗi phân khúc. + Phiên trung bình + Thời lượng nghe trung bình cho mỗi phân khúc. + Chia thành các khoảng 4 giờ để làm nổi bật nhịp điệu hàng ngày của bạn. + Các thanh hàng ngày giúp so sánh thói quen từ tuần này sang tuần khác dễ dàng hơn. + Các thanh hàng tuần cho thấy xu hướng của tháng. + Các thanh hàng tháng cho thấy tính thời vụ trong suốt năm. + Các thanh hàng năm tóm tắt toàn bộ lịch sử của bạn. + Chưa có dữ liệu nghe + Nhấn phát để bắt đầu xây dựng dòng thời gian nghe của bạn + Nhịp điệu hàng ngày + Nhịp điệu hàng tuần + Nhịp điệu hàng tháng + Năm trong cái nhìn + Tiến triển qua thời gian + Nhóm theo phân khúc 4 giờ + Nhóm theo ngày trong tuần + Nhóm theo tuần trong tháng + Nhóm theo tháng + Nhóm theo năm + Phân khúc đỉnh + %1$d lần phát - Top categories - Compare how you listen across genres, artists, albums, and songs. - Genre - Artist + Danh mục hàng đầu + So sánh cách bạn nghe qua thể loại, nghệ sĩ, album và bài hát. + Thể loại + Nghệ sĩ Album - Song - Listening by genre - Listening by artist - Listening by album - Listening by song - %1$d plays • %2$d artists - %1$d plays • %2$d tracks - No category data yet - Press play to surface your listening highlights - Listening habits - No habits yet - We will surface your listening habits once we know you better. - Total sessions - Avg session - Longest session - Sessions/day - Most active day - No playback yet - Peak timeline slot - Top artists - No top artists - Keep listening and your favorite artists will show up here. + Bài hát + Nghe theo thể loại + Nghe theo nghệ sĩ + Nghe theo album + Nghe theo bài hát + %1$d lần phát • %2$d nghệ sĩ + %1$d lần phát • %2$d bản nhạc + Chưa có dữ liệu danh mục + Nhấn phát để hiển thị điểm nổi bật về việc nghe của bạn + Thói quen nghe + Chưa có thói quen + Chúng tôi sẽ hiển thị thói quen nghe của bạn khi hiểu bạn rõ hơn. + Tổng số phiên + Phiên trung bình + Phiên dài nhất + Phiên/ngày + Ngày hoạt động nhiều nhất + Chưa phát + Khoảng thời gian đỉnh + Nghệ sĩ hàng đầu + Không có nghệ sĩ hàng đầu + Tiếp tục nghe và nghệ sĩ yêu thích của bạn sẽ xuất hiện ở đây. \? %1$d. %2$s - Top albums - No top albums - Albums you revisit often will appear here. + Album hàng đầu + Không có album hàng đầu + Các album bạn thường xuyên nghe lại sẽ xuất hiện ở đây. %1$d. %2$s - Track concentration - How your listening time is distributed across your top tracks. - No concentration data yet - Play more tracks to see how focused your listening is. + Mật độ bản nhạc + Thời gian nghe của bạn được phân bổ như thế nào qua các bản nhạc hàng đầu. + Chưa có dữ liệu mật độ + Phát thêm bản nhạc để xem mức độ tập trung khi nghe của bạn. Top 1 Top 2-3 - Others + Khác %1$d%% - Listening concentration - Top 3 tracks account for %1$d%% of your listening time. - Avg plays/track - Unique tracks - Top 3 share - Tracks in this range - Most played tracks for the selected time range. - No top tracks - Listen to your favorites to see them highlighted here. - Collapse tracks - Show all tracks + Mật độ nghe + Top 3 bản nhạc chiếm %1$d%% thời gian nghe của bạn. + Lần phát/bản nhạc trung bình + Bản nhạc duy nhất + Tỷ lệ Top 3 + Bản nhạc trong phạm vi này + Bản nhạc được phát nhiều nhất cho phạm vi thời gian đã chọn. + Không có bản nhạc hàng đầu + Nghe các bản yêu thích của bạn để xem chúng được hiển thị ở đây. + Thu gọn bản nhạc + Hiện tất cả bản nhạc - %1$d h %2$02d min - %1$d min - %1$d h %2$02d m + %1$d h %2$02d phút + %1$d phút + %1$d h %2$02d p %1$d h - %1$d m - %1$d s + %1$d p + %1$d giây %1$dh %2$02dm %1$dh %1$dm %1$ds - Never - Just now - 1 day ago - %1$d days ago - 1 hour ago - %1$d hours ago - 1 minute ago - %1$d minutes ago - %1$d Song - %1$d Songs - Week %1$d + Không bao giờ + Vừa xong + 1 ngày trước + %1$d ngày trước + 1 giờ trước + %1$d giờ trước + 1 phút trước + %1$d phút trước + %1$d bài hát + %1$d bài hát + Tuần %1$d diff --git a/app/src/main/res/values-vi/strings_library.xml b/app/src/main/res/values-vi/strings_library.xml index 8b7c6c2c60..989484fb5c 100644 --- a/app/src/main/res/values-vi/strings_library.xml +++ b/app/src/main/res/values-vi/strings_library.xml @@ -1,420 +1,420 @@ - Library - Library tabs - Jump directly to any tab or reorder them. - Reorder tabs + Thư viện + Các tab thư viện + Chuyển trực tiếp đến bất kỳ tab nào hoặc sắp xếp lại chúng. + Sắp xếp lại tab - Songs - Albums - Artists - Playlists - Folders - Liked + Bài hát + Album + Nghệ sĩ + Danh sách phát + Thư mục + Đã thích - Playlist created successfully - Set your AI provider API key first - Set your Gemini API key first - Added to queue - Playing next + Đã tạo danh sách phát thành công + Vui lòng đặt API key của nhà cung cấp AI trước + Vui lòng đặt Gemini API key trước + Đã thêm vào hàng chờ + Sẽ phát tiếp theo - Watch transfer - Settings - Edit - Reorder tabs - Expand menu + Chuyển sang đồng hồ + Cài đặt + Chỉnh sửa + Sắp xếp lại tab + Mở rộng menu - You can select up to %1$d albums - Folder - Folder + Bạn có thể chọn tối đa %1$d album + Thư mục + Thư mục - Sort by - View - Playlist View - Grid - List - Internal - SD Card - SD card is not available right now. - Cloud - Telegram Cloud Channels - Topics Display - Channels - Topics - Both - Cloud - Cloud Only + Sắp xếp theo + Hiển thị + Chế độ xem danh sách phát + Lưới + Danh sách + Bộ nhớ trong + Thẻ SD + Thẻ SD hiện không khả dụng. + Đám mây + Kênh Telegram Cloud + Hiển thị chủ đề + Kênh + Chủ đề + Cả hai + Đám mây + Chỉ đám mây - Generating metadata with AI… + Đang tạo metadata bằng AI… - Error loading songs - Error loading albums - Error loading artists - Retry + Lỗi khi tải bài hát + Lỗi khi tải album + Lỗi khi tải nghệ sĩ + Thử lại - No songs found in your library. - Try rescanning your library in settings if you have music on your device. - No songs found + Không tìm thấy bài hát nào trong thư viện. + Hãy thử quét lại thư viện trong cài đặt nếu bạn có nhạc trên thiết bị. + Không tìm thấy bài hát - New - Create new playlist - Import M3U playlist - Locate current song - All songs - CLOUD - LOCAL - Sort options + Mới + Tạo danh sách phát mới + Nhập danh sách phát M3U + Tìm bài hát hiện tại + Tất cả bài hát + ĐÁM MÂY + CỤC BỘ + Tùy chọn sắp xếp - All - Deselect - More options + Tất cả + Bỏ chọn + Thêm tùy chọn - Scanning music files… - Processing files… - %1$d of %2$d files - Syncing library… - Sync complete - Waiting… - Syncing library… - Cleaning album art cache… - Syncing cloud sources… - Scanning lyrics… + Đang quét tệp nhạc… + Đang xử lý tệp… + %1$d trên %2$d tệp + Đang đồng bộ thư viện… + Đồng bộ hoàn tất + Đang chờ… + Đang đồng bộ thư viện… + Đang xóa bộ nhớ đệm ảnh bìa… + Đang đồng bộ nguồn đám mây… + Đang quét lời bài hát… - No songs yet - Add music to your device or sync a cloud source to start listening. - No local songs found - Try another source filter or rescan your device library. - No cloud songs found - Sync Telegram or NetEase songs, or switch to local source. - No albums available - Albums will appear here as soon as your library has grouped tracks. - No local albums found - Local songs are required to build local album groups. - No cloud albums found - Cloud songs with album data will appear here after sync. - No artists available - Artists are shown after songs are indexed from any source. - No local artists found - No artist metadata is available for local songs right now. - No cloud artists found - Cloud artist entries appear when remote songs are synced. - No liked songs yet - Tap the heart icon while playing a song to save it here. - No liked local songs - Switch source filter or like songs from your device. - No liked cloud songs - Like Telegram or NetEase tracks to see them in this view. - No folders found - Internal storage folders with music will appear here. - No playlists yet - Create your first playlist to organize your library. + Chưa có bài hát nào + Thêm nhạc vào thiết bị hoặc đồng bộ nguồn đám mây để bắt đầu nghe. + Không tìm thấy bài hát cục bộ + Hãy thử bộ lọc nguồn khác hoặc quét lại thư viện trên thiết bị. + Không tìm thấy bài hát trên đám mây + Đồng bộ bài hát từ Telegram hoặc NetEase, hoặc chuyển sang nguồn cục bộ. + Chưa có album nào + Album sẽ xuất hiện ở đây khi thư viện đã nhóm các bài hát. + Không tìm thấy album cục bộ + Cần có bài hát cục bộ để tạo nhóm album cục bộ. + Không tìm thấy album đám mây + Bài hát trên đám mây có dữ liệu album sẽ xuất hiện ở đây sau khi đồng bộ. + Chưa có nghệ sĩ nào + Nghệ sĩ sẽ được hiển thị sau khi bài hát được lập chỉ mục từ bất kỳ nguồn nào. + Không tìm thấy nghệ sĩ cục bộ + Hiện chưa có metadata nghệ sĩ cho các bài hát cục bộ. + Không tìm thấy nghệ sĩ đám mây + Mục nghệ sĩ đám mây xuất hiện khi các bài hát từ xa được đồng bộ. + Chưa có bài hát yêu thích nào + Nhấn vào biểu tượng trái tim khi đang phát bài hát để lưu vào đây. + Không có bài hát cục bộ yêu thích + Chuyển bộ lọc nguồn hoặc thích bài hát từ thiết bị. + Không có bài hát đám mây yêu thích + Thích bài hát từ Telegram hoặc NetEase để xem chúng trong chế độ này. + Không tìm thấy thư mục nào + Các thư mục bộ nhớ trong có nhạc sẽ xuất hiện ở đây. + Chưa có danh sách phát nào + Tạo danh sách phát đầu tiên để tổ chức thư viện. - Edit song metadata - Play - Play song - Play all - Play all - Add to favorites - Add all to favorites - Remove from favorites - Remove all from favorites - Share song file via - Share song file - Share all as ZIP - Could not share song: %1$s - Add to queue - Add to queue - Next - Play next in queue - Add to playlist - Delete - Delete all - Checking watch - Transferring %1$d%% - Transferring to watch - Transfer in progress - Send to watch - Watch unavailable - Send song to watch - Watch unavailable - Set as - Set as sound - Choose how to use this song as a system sound - Use this song as - Choose where PixelPlayer should install this sound. - Phone ringtone - Incoming calls - Notification sound - Messages and app alerts - Alarm sound - Clock alarms - Confirm sound change - Set \"%1$s\" as your %2$s? - Set sound - Set \"%1$s\" as your %2$s - ringtone - notification sound - alarm sound - Enable Modify system settings, then return to PixelPlayer to finish automatically. - Modify system settings was not enabled. - Set \"%1$s\" as your ringtone - Only local songs can be used as ringtones. - Couldn\'t prepare this audio file for ringtone. - Couldn\'t set ringtone: %1$s - Options - OPTIONS - Info - INFO - Duration - Genre + Chỉnh sửa metadata bài hát + Phát + Phát bài hát + Phát tất cả + Phát tất cả + Thêm vào yêu thích + Thêm tất cả vào yêu thích + Xóa khỏi yêu thích + Xóa tất cả khỏi yêu thích + Chia sẻ tệp bài hát qua + Chia sẻ tệp bài hát + Chia sẻ tất cả dưới dạng ZIP + Không thể chia sẻ bài hát: %1$s + Thêm vào hàng chờ + Thêm vào hàng chờ + Tiếp theo + Phát tiếp theo trong hàng chờ + Thêm vào danh sách phát + Xóa + Xóa tất cả + Đang kiểm tra đồng hồ + Đang chuyển %1$d%% + Đang chuyển sang đồng hồ + Đang trong quá trình chuyển + Gửi đến đồng hồ + Đồng hồ không khả dụng + Gửi bài hát đến đồng hồ + Đồng hồ không khả dụng + Đặt làm + Đặt làm âm thanh + Chọn cách sử dụng bài hát này làm âm thanh hệ thống + Sử dụng bài hát này làm + Chọn nơi PixelPlayer sẽ cài đặt âm thanh này. + Nhạc chuông điện thoại + Cuộc gọi đến + Âm thanh thông báo + Tin nhắn và cảnh báo ứng dụng + Âm thanh báo thức + Báo thức đồng hồ + Xác nhận thay đổi âm thanh + Đặt \"%1$s\" làm %2$s? + Đặt âm thanh + Đã đặt \"%1$s\" làm %2$s + nhạc chuông + âm thanh thông báo + âm thanh báo thức + Bật \"Sửa đổi cài đặt hệ thống\", sau đó quay lại PixelPlayer để hoàn tất tự động. + Chưa bật \"Sửa đổi cài đặt hệ thống\". + Đã đặt \"%1$s\" làm nhạc chuông + Chỉ bài hát cục bộ mới có thể dùng làm nhạc chuông. + Không thể chuẩn bị tệp âm thanh này làm nhạc chuông. + Không thể đặt nhạc chuông: %1$s + Tùy chọn + TÙY CHỌN + Thông tin + THÔNG TIN + Thời lượng + Thể loại Album - Artist - Song info - Provider - File - %1$d SONGS - selected - %1$d PLAYLISTS - %1$d ALBUMS - selected - Limit: %1$d albums per selection. - Queue + play respects your selection order. - %1$d GENRES - selected - Perform batch operations on all songs within these genres. + Nghệ sĩ + Thông tin bài hát + Nhà cung cấp + Tệp + %1$d BÀI HÁT + đã chọn + %1$d DANH SÁCH PHÁT + %1$d ALBUM + đã chọn + Giới hạn: %1$d album mỗi lần chọn. + Hàng chờ + phát sẽ theo thứ tự bạn chọn. + %1$d THỂ LOẠI + đã chọn + Thực hiện thao tác hàng loạt trên tất cả bài hát thuộc các thể loại này. - Default Order - Title (A-Z) - Title (Z-A) - Artist - Artist (Z-A) + Thứ tự mặc định + Tiêu đề (A-Z) + Tiêu đề (Z-A) + Nghệ sĩ + Nghệ sĩ (Z-A) Album Album (Z-A) - Date Added - Date Added (Oldest First) - Duration - Duration (Shortest First) - Release Year - Release Year (Oldest First) - Fewest Songs - Most Songs - Name (A-Z) - Name (Z-A) - Number of Songs (Most) - Number of Songs (Fewest) - Date Created - Date Created (Oldest First) - Date Liked - Date Liked (Oldest First) - Fewest Subfolders - Most Subfolders + Ngày thêm + Ngày thêm (Cũ nhất trước) + Thời lượng + Thời lượng (Ngắn nhất trước) + Năm phát hành + Năm phát hành (Cũ nhất trước) + Ít bài hát nhất + Nhiều bài hát nhất + Tên (A-Z) + Tên (Z-A) + Số lượng bài hát (Nhiều nhất) + Số lượng bài hát (Ít nhất) + Ngày tạo + Ngày tạo (Cũ nhất trước) + Ngày thích + Ngày thích (Cũ nhất trước) + Ít thư mục con nhất + Nhiều thư mục con nhất - Title - Artist + Tiêu đề + Nghệ sĩ Album - Date Added - Duration - Release Year - Song Count - Name - Number of Songs - Date Created - Date Liked - Subfolder Count + Ngày thêm + Thời lượng + Năm phát hành + Số lượng bài hát + Tên + Số lượng bài hát + Ngày tạo + Ngày thích + Số lượng thư mục con - Source - Order - Descending - Ascending - Original Order - Tap to switch to ascending - Tap to switch to descending - This sort keeps its original order - Switch is on + Nguồn + Thứ tự + Giảm dần + Tăng dần + Thứ tự gốc + Nhấn để chuyển sang tăng dần + Nhấn để chuyển sang giảm dần + Sắp xếp này giữ nguyên thứ tự gốc + Đang bật - Reorder library tabs - Reset order - Reset tab order to the default? - Reordering tabs… - Drag handle + Sắp xếp lại tab thư viện + Đặt lại thứ tự + Đặt lại thứ tự tab về mặc định? + Đang sắp xếp lại tab… + Thanh kéo - Pick an Artist - 1 artist - %1$d artists - Primary artist - Artist page + Chọn nghệ sĩ + 1 nghệ sĩ + %1$d nghệ sĩ + Nghệ sĩ chính + Trang nghệ sĩ - Cancel transfer + Hủy chuyển %1$s / %2$s - Shows live progress for phone-to-watch music transfers - Watch transfers - Sending to Watch - Cancelled - Transfer cancelled - Transfer complete - Completed - Failed - Transfer failed - Multiple active transfers + Hiển thị tiến trình chuyển nhạc từ điện thoại sang đồng hồ + Chuyển sang đồng hồ + Đang gửi đến đồng hồ + Đã hủy + Đã hủy chuyển + Chuyển hoàn tất + Hoàn tất + Thất bại + Chuyển thất bại + Nhiều lượt chuyển đang hoạt động %1$s • %2$s - Preparing - Preparing watch transfer - Preparing transfer… - Sending %1$d songs to watch - Sending to watch - Starting transfer… - Starting - Transferring - %1$d transfers + Đang chuẩn bị + Đang chuẩn bị chuyển sang đồng hồ + Đang chuẩn bị chuyển… + Đang gửi %1$d bài hát đến đồng hồ + Đang gửi đến đồng hồ + Đang bắt đầu chuyển… + Đang bắt đầu + Đang chuyển + %1$d lượt chuyển - Edit song - Show information - Editing song metadata - Editing a song\'s metadata can affect how it\'s displayed and organized in your library. Changes are permanent and may not be reversible. - Got it - Information - Cover art - Select a square image and fine-tune it so your cover art looks great across the app. - Change cover art - Delete cover art - Title - Artist + Chỉnh sửa bài hát + Hiển thị thông tin + Đang chỉnh sửa metadata bài hát + Việc chỉnh sửa metadata bài hát có thể ảnh hưởng đến cách hiển thị và tổ chức trong thư viện. Các thay đổi là vĩnh viễn và có thể không thể hoàn tác. + Đã hiểu + Thông tin + Ảnh bìa + Chọn hình ảnh vuông và điều chỉnh để ảnh bìa hiển thị tốt trên toàn ứng dụng. + Đổi ảnh bìa + Xóa ảnh bìa + Tiêu đề + Nghệ sĩ Album - Album artist - Genre - Composer - Track number - Disc number - ReplayGain track (dB) + Nghệ sĩ album + Thể loại + Nhà soạn nhạc + Số thứ tự bài hát + Số thứ tự đĩa + ReplayGain bài hát (dB) ReplayGain album (dB) -6.50 -8.20 - Preview of the new cover art - Current song cover art - Adjust your cover art - Use pinch and drag gestures to find the perfect framing. - Apply cover art - Unable to load the selected image - Search lyrics on lrclib.net + Xem trước ảnh bìa mới + Ảnh bìa bài hát hiện tại + Điều chỉnh ảnh bìa + Sử dụng thao tác cuộn và kéo để tìm khung hình hoàn hảo. + Áp dụng ảnh bìa + Không thể tải hình ảnh đã chọn + Tìm lời bài hát trên lrclib.net - Edit %d Songs - Only modified fields will be updated. Leave fields empty to keep existing values. - (Mixed values) - (Optional - leave empty to skip) - Successfully updated %d songs - Updated %1$d of %2$d songs. Some files could not be edited. - Failed to update songs - Batch Cover Art - This will replace the cover art for all %d selected songs - Set Cover Art for All - Remove All Cover Art - (Multiple different covers) + Chỉnh sửa %d bài hát + Chỉ các trường đã sửa sẽ được cập nhật. Để trống trường để giữ nguyên giá trị hiện tại. + (Giá trị hỗn hợp) + (Tùy chọn - để trống để bỏ qua) + Đã cập nhật %d bài hát thành công + Đã cập nhật %1$d trên %2$d bài hát. Một số tệp không thể chỉnh sửa. + Cập nhật bài hát thất bại + Ảnh bìa hàng loạt + Thao tác này sẽ thay thế ảnh bìa cho tất cả %d bài hát đã chọn + Đặt ảnh bìa cho tất cả + Xóa tất cả ảnh bìa + (Nhiều ảnh bìa khác nhau) - Playlist dismissed + Đã đóng danh sách phát - Create playlist - Choose the creation flow. - Manual - Design artwork, icon, shape and pick songs yourself. - With AI - Generate a curated playlist with advanced controls. - Requires a Gemini API key configured in settings. - Set up API key + Tạo danh sách phát + Chọn luồng tạo. + Thủ công + Tự thiết kế hình ảnh, biểu tượng, hình dạng và chọn bài hát. + Với AI + Tạo danh sách phát được tuyển chọn với các điều khiển nâng cao. + Cần có Gemini API key đã cấu hình trong cài đặt. + Thiết lập API key - AI Playlist Lab - Reset - Generating… - Generate - Intent - Playlist name (optional) - What should this playlist feel like? - Example: sunset drive with warm synths - Direction - Mood - Activity - Era - Curation engine - Energy - Controls the intensity and tempo of songs. 1 = calm/slow, 5 = high-energy/fast. - Discovery - Controls how familiar the selections are. 1 = your most played favorites, 5 = rarely played deep cuts. - Min songs - Max songs - Filters - Prioritize genres (optional) - e.g. synthwave, indie pop - Avoid genres (optional) - e.g. metal, hard trap - Preferred language (optional) - e.g. English, Spanish, instrumental - Prioritize favorites - Avoid explicit lyrics - Prompt preview - Your final prompt will appear here once you add preferences. - Curate with precision - Define mood, activity, constraints and depth. - The AI will only use songs from your local library. - Add at least one instruction for AI. - Set a valid song range. + Phòng thí nghiệm danh sách phát AI + Đặt lại + Đang tạo… + Tạo + Mục đích + Tên danh sách phát (tùy chọn) + Danh sách phát này nên có cảm giác như thế nào? + Ví dụ: lái xe lúc hoàng hôn với âm thanh synth ấm áp + Hướng đi + Tâm trạng + Hoạt động + Thời đại + Công cụ tuyển chọn + Năng lượng + Điều chỉnh cường độ và tiết tấu của bài hát. 1 = tĩnh lặng/chậm, 5 = năng lượng cao/nhanh. + Khám phá + Điều chỉnh mức độ quen thuộc của các bài hát được chọn. 1 = bài yêu thích phát nhiều nhất, 5 = bài hiếm khi phát. + Tối thiểu bài hát + Tối đa bài hát + Bộ lọc + Ưu tiên thể loại (tùy chọn) + ví dụ: synthwave, indie pop + Tránh thể loại (tùy chọn) + ví dụ: metal, hard trap + Ngôn ngữ ưu tiên (tùy chọn) + ví dụ: English, Spanish, instrumental + Ưu tiên bài yêu thích + Tránh lời bài hát thô tục + Xem trước prompt + Prompt cuối cùng sẽ xuất hiện ở đây khi bạn thêm tùy chọn. + Tuyển chọn chính xác + Xác định tâm trạng, hoạt động, ràng buộc và độ sâu. + AI chỉ sử dụng bài hát từ thư viện cục bộ của bạn. + Thêm ít nhất một hướng dẫn cho AI. + Đặt khoảng bài hát hợp lệ. %1$d/5 - Custom… - Enter custom value - Enter your custom value + Tùy chỉnh… + Nhập giá trị tùy chỉnh + Nhập giá trị tùy chỉnh - Any era - Core request: %1$s. - Mood target: %1$s. - Activity context: %1$s. - Era focus: %1$s. - Prioritize genres: %1$s. - Avoid genres: %1$s. - Preferred language: %1$s. - Energy level target: %1$d/5. - Discovery target: %1$d/5 where 1 is familiar and 5 is deep cuts. - Prioritize songs closer to listener favorites when possible. - Avoid explicit lyrics whenever alternatives exist. - Keep transitions smooth and avoid repetitive artist clustering. + Bất kỳ thời đại nào + Yêu cầu cốt lõi: %1$s. + Tâm trạng mục tiêu: %1$s. + Bối cảnh hoạt động: %1$s. + Tập trung thời đại: %1$s. + Ưu tiên thể loại: %1$s. + Tránh thể loại: %1$s. + Ngôn ngữ ưu tiên: %1$s. + Mục tiêu mức năng lượng: %1$d/5. + Mục tiêu khám phá: %1$d/5 trong đó 1 là quen thuộc và 5 là bài hiếm. + Ưu tiên bài hát gần với sở thích của người nghe khi có thể. + Tránh lời bài hát thô tục mỗi khi có phương án thay thế. + Giữ chuyển tiếp mượt mà và tránh lặp lại nhóm nghệ sĩ. - Chill - Energetic - Happy - Dark - Romantic - Melancholic + Điềm tĩnh + Tràn đầy năng lượng + Vui vẻ + Tối tăm + Lãng mạn + U sầu - Workout - Focus - Road trip + Tập thể dục + Tập trung + Đi đường dài Party - Study - Late night + Học tập + Đêm khuya @string/playlist_creation_ai_era_any @@ -427,140 +427,140 @@ - No playlist has been created. - Touch the \'New Playlist\' button to start. - New playlist - Playlist name - My playlist + Chưa tạo danh sách phát nào. + Nhấn nút \'Danh sách phát mới\' để bắt đầu. + Danh sách phát mới + Tên danh sách phát + Danh sách phát của tôi - Add %1$d songs to… - Select playlists - Search for playlists… - Songs added to playlists - Playlist created and songs added - Internal Storage + Thêm %1$d bài hát vào… + Chọn danh sách phát + Tìm danh sách phát… + Đã thêm bài hát vào danh sách phát + Đã tạo danh sách phát và thêm bài hát + Bộ nhớ trong - Add songs - Add selected songs - Add - Search or filter songs… - Liked - Failed to load songs - Load more + Thêm bài hát + Thêm các bài hát đã chọn + Thêm + Tìm kiếm hoặc lọc bài hát… + Đã thích + Tải bài hát thất bại + Tải thêm - Merge Playlists - Enter a name for the merged playlist: - Merged Playlist - This will merge %1$d selected playlists into one. + Gộp danh sách phát + Nhập tên cho danh sách phát đã gộp: + Danh sách phát đã gộp + Thao tác này sẽ gộp %1$d danh sách phát đã chọn thành một. - No valid songs found to play - Song not found in current list - Could not locate song - No songs found in library - Playback stopped: %1$s finished (End of Track). - Track - No songs to shuffle. - Selected Albums - No playable songs found in selected albums - No playable songs found in selected genres - Only the first %1$d albums were queued - %1$d albums queued (%2$d songs) - Could not queue selected albums - All songs already in favorites - No songs were in favorites - Creating ZIP file… - Failed to share: %1$s + Không tìm thấy bài hát hợp lệ để phát + Không tìm thấy bài hát trong danh sách hiện tại + Không thể tìm bài hát + Không tìm thấy bài hát trong thư viện + Đã dừng phát: %1$s kết thúc (Hết bài). + Bài hát + Không có bài hát để trộn. + Album đã chọn + Không tìm thấy bài hát có thể phát trong album đã chọn + Không tìm thấy bài hát có thể phát trong thể loại đã chọn + Chỉ %1$d album đầu tiên được thêm vào hàng chờ + %1$d album đã thêm vào hàng chờ (%2$d bài hát) + Không thể thêm album đã chọn vào hàng chờ + Tất cả bài hát đã có trong yêu thích + Không có bài hát nào trong yêu thích + Đang tạo tệp ZIP… + Chia sẻ thất bại: %1$s - %d song added to queue - %d songs added to queue + Đã thêm %d bài hát vào hàng chờ + Đã thêm %d bài hát vào hàng chờ - %d song will play next - %d songs will play next + %d bài hát sẽ phát tiếp theo + %d bài hát sẽ phát tiếp theo - %d song added to favorites - %d songs added to favorites + Đã thêm %d bài hát vào yêu thích + Đã thêm %d bài hát vào yêu thích - %d song removed from favorites - %d songs removed from favorites + Đã xóa %d bài hát khỏi yêu thích + Đã xóa %d bài hát khỏi yêu thích - No playlists to share - Share playlists - Share failed: %1$s - No playlists to export - Export failed: %1$s - Music/PixelPlayer Exports - Please configure your Gemini API key in Settings. - Playlist restored + Không có danh sách phát để chia sẻ + Chia sẻ danh sách phát + Chia sẻ thất bại: %1$s + Không có danh sách phát để xuất + Xuất thất bại: %1$s + Nhạc/Xuất PixelPlayer + Vui lòng cấu hình Gemini API key trong Cài đặt. + Đã khôi phục danh sách phát - Sharing %d playlist - Sharing %d playlists + Đang chia sẻ %d danh sách phát + Đang chia sẻ %d danh sách phát - Exported %1$d playlist to %2$s - Exported %1$d playlists to %2$s + Đã xuất %1$d danh sách phát tới %2$s + Đã xuất %1$d danh sách phát tới %2$s - Invalid album ID - Album ID not found - Error loading album data: %s - Album not found + ID album không hợp lệ + Không tìm thấy ID album + Lỗi khi tải dữ liệu album: %s + Không tìm thấy album - Invalid artist ID - Artist ID not found - Error loading artist data: %s - Could not find the artist + ID nghệ sĩ không hợp lệ + Không tìm thấy ID nghệ sĩ + Lỗi khi tải dữ liệu nghệ sĩ: %s + Không tìm thấy nghệ sĩ - Cannot delete currently playing song - %1$d files deleted (%2$d skipped - playing) - %1$d of %2$d files deleted - Failed to delete files - File deleted - Can\'t delete the file or file not found - Deletion cancelled - Delete song? - \"%1$s\" by %2$s\n\nThis song will be permanently deleted from your device and cannot be recovered. - These songs will be permanently deleted from your device and cannot be recovered. + Không thể xóa bài hát đang phát + Đã xóa %1$d tệp (%2$d bị bỏ qua - đang phát) + Đã xóa %1$d trong %2$d tệp + Xóa tệp thất bại + Đã xóa tệp + Không thể xóa tệp hoặc tệp không tìm thấy + Đã hủy xóa + Xóa bài hát? + \"%1$s\" của %2$s\n\nBài hát này sẽ bị xóa vĩnh viễn khỏi thiết bị và không thể khôi phục. + Các bài hát này sẽ bị xóa vĩnh viễn khỏi thiết bị và không thể khôi phục. - %d file deleted - %d files deleted + Đã xóa %d tệp + Đã xóa %d tệp - Delete %d song? - Delete %d songs? + Xóa %d bài hát? + Xóa %d bài hát? - Metadata updated successfully - Updating %1$d songs… - Successfully updated %1$d songs! - Updated %1$d songs. Failed: %2$d - Lyrics saved successfully - Failed to save lyrics - No lyrics available to save - Permission denied – cannot edit files - Permission denied – cannot save lyrics - Permission denied – cannot edit this file + Đã cập nhật metadata thành công + Đang cập nhật %1$d bài hát… + Đã cập nhật %1$d bài hát thành công! + Đã cập nhật %1$d bài hát. Thất bại: %2$d + Đã lưu lời bài hát thành công + Lưu lời bài hát thất bại + Không có lời bài hát để lưu + Bị từ chối quyền - không thể chỉnh sửa tệp + Bị từ chối quyền - không thể lưu lời bài hát + Bị từ chối quyền - không thể chỉnh sửa tệp này - Please configure a valid API key for the selected AI provider in Settings. - AI Error: %s - The selected AI provider rejected the request because the account has no credits or available quota. - The selected AI model is no longer available. PixelPlayer tried to switch to a supported model automatically. - AI couldn\'t find any songs for your prompt. - Write an idea for your Daily Mix - Daily Mix updated with AI - Could not update: %s - AI couldn\'t find songs for this mix - \ No newline at end of file + Vui lòng cấu hình API key hợp lệ cho nhà cung cấp AI đã chọn trong Cài đặt. + Lỗi AI: %s + Nhà cung cấp AI đã chọn từ chối yêu cầu vì tài khoản không có credit hoặc quota khả dụng. + Mô hình AI đã chọn không còn khả dụng. PixelPlayer đã thử chuyển sang mô hình được hỗ trợ tự động. + AI không tìm thấy bài hát nào cho prompt của bạn. + Viết ý tưởng cho Daily Mix của bạn + Đã cập nhật Daily Mix bằng AI + Không thể cập nhật: %s + AI không tìm thấy bài hát cho mix này + diff --git a/app/src/main/res/values-vi/strings_player.xml b/app/src/main/res/values-vi/strings_player.xml index e5a3b76777..769a6c862c 100644 --- a/app/src/main/res/values-vi/strings_player.xml +++ b/app/src/main/res/values-vi/strings_player.xml @@ -1,137 +1,137 @@ - Collapse player - Now Playing - Cloud stream + Thu nhỏ trình phát + Đang phát + Phát trực tuyến đám mây Cast Bluetooth - Local playback - Connecting… - Open Queue + Phát cục bộ + Đang kết nối… + Mở hàng đợi - Get ready to connect - Allow PixelPlayer to see your nearby devices and current Wi‑Fi so we can keep your cast, Bluetooth audio, and speakers in sync. - Nearby devices - Needed to read and control your connected Bluetooth audio gear. - Location for Wi‑Fi - Android requires Location to share the Wi‑Fi network (SSID) you\'re on so we can find compatible cast devices. - Allow access - We only use these permissions for device interconnectivity — casting, controlling nearby speakers, and keeping audio in sync. - Connect device - Scanning nearby - Casting session - Connecting - Connected - This phone - Bluetooth audio - Local playback - Playing - Paused - Device volume - Phone volume + Sẵn sàng kết nối + Cho phép PixelPlayer xem các thiết bị ở gần và Wi‑Fi hiện tại của bạn để chúng tôi có thể giữ cho cast, âm thanh Bluetooth và loa luôn đồng bộ. + Thiết bị ở gần + Cần thiết để đọc và điều khiển thiết bị âm thanh Bluetooth đã kết nối của bạn. + Vị trí cho Wi‑Fi + Android yêu cầu Vị trí để chia sẻ mạng Wi‑Fi (SSID) mà bạn đang sử dụng để chúng tôi có thể tìm các thiết bị cast tương thích. + Cho phép truy cập + Chúng tôi chỉ sử dụng các quyền này cho việc kết nối thiết bị — cast, điều khiển loa ở gần và giữ cho âm thanh đồng bộ. + Kết nối thiết bị + Đang quét thiết bị ở gần + Phiên cast + Đang kết nối + Đã kết nối + Điện thoại này + Âm thanh Bluetooth + Phát cục bộ + Đang phát + Tạm dừng + Âm lượng thiết bị + Âm lượng điện thoại %1$d/%2$d - Battery level - Volume level - Disconnect - Connectivity - Turn on Wi-Fi or Bluetooth - Refresh connections + Mức pin + Mức âm lượng + Ngắt kết nối + Kết nối + Bật Wi-Fi hoặc Bluetooth + Làm mới kết nối Wi-Fi - Off - On - Connected + Tắt + Bật + Đã kết nối Bluetooth - Off - On - Connected - Nearby devices - Refresh devices - Connected - Connecting - Available to connect - Available - Connecting... - Searching for devices… - Make sure your TV or speaker is on and sharing the same Wi‑Fi network. - Controls - Devices + Tắt + Bật + Đã kết nối + Thiết bị ở gần + Làm mới thiết bị + Đã kết nối + Đang kết nối + Sẵn sàng kết nối + Sẵn có + Đang kết nối... + Đang tìm thiết bị… + Hãy đảm bảo TV hoặc loa của bạn đang bật và chia sẻ cùng mạng Wi‑Fi. + Điều khiển + Thiết bị - Cast media server - Casting to device - Serving media to Cast device + Máy chủ media Cast + Đang cast đến thiết bị + Đang cung cấp media cho thiết bị Cast %1$s: %2$s - Seeking is temporarily unavailable for this audio format on Cast because it can crash the Cast session. + Tua tạm thời không khả dụng cho định dạng âm thanh này trên Cast vì nó có thể làm phiên cast bị crash. - Sleep timer - Timer - %1$d minutes - Timer set for %1$d minutes. - 1 time + Hẹn giờ ngủ + Hẹn giờ + %1$d phút + Đã đặt hẹn giờ %1$d phút. + 1 lần - %d time - %d times + %d lần + %d lần - Play count: %1$s - End of current track - Playback will stop at end of track. - Switch on - Custom time - Cancel timer - End of track - Timer cancelled. - Cannot enable end of track: no active song. - End of track timer deactivated: song changed from %1$s to %2$s. - Previous track - Current track - Set custom duration + Số lần phát: %1$s + Kết thúc bài hát hiện tại + Phát sẽ dừng ở cuối bài hát. + Bật + Thời gian tùy chỉnh + Hủy hẹn giờ + Kết thúc bài hát + Đã hủy hẹn giờ. + Không thể bật kết thúc bài hát: không có bài hát đang phát. + Hẹn giờ kết thúc bài hát đã tắt: bài hát đã đổi từ %1$s sang %2$s. + Bài trước + Bài hiện tại + Đặt thời lượng tùy chỉnh - Next up - Queue is empty for now. + Tiếp theo + Hàng đợi hiện đang trống. - 1 track lined up. - %d tracks lined up. + 1 bài đã xếp hàng. + %d bài đã xếp hàng. - Queue - Queue is empty. - Reorder song - Toggle shuffle - Toggle repeat - Sleep timer - More actions - Locate current song - Clear queue - Clear queue - Are you sure you want to clear all songs from the queue except the current one? - Save as playlist - %1$s queue - Current queue - Dismiss song - removed - Save as playlist - Deselect all - Playlist name - Search songs to include… - No songs match \"%1$s\" + Hàng đợi + Hàng đợi trống. + Sắp xếp lại bài hát + Bật/tắt xáo trộn + Bật/tắt lặp lại + Hẹn giờ ngủ + Thêm thao tác + Định vị bài hát hiện tại + Xóa hàng đợi + Xóa hàng đợi + Bạn có chắc muốn xóa tất cả bài hát khỏi hàng đợi ngoại trừ bài hiện tại? + Lưu thành danh sách phát + Hàng đợi %1$s + Hàng đợi hiện tại + Bỏ bài hát + đã xóa + Lưu thành danh sách phát + Bỏ chọn tất cả + Tên danh sách phát + Tìm bài hát để thêm… + Không có bài hát nào khớp \"%1$s\" - %d song selected - %d songs selected + %d bài đã chọn + %d bài đã chọn - Save as: %1$s - Enter a playlist name - Remove from playlist - More options for %1$s + Lưu dưới dạng: %1$s + Nhập tên danh sách phát + Xóa khỏi danh sách phát + Thêm tùy chọn cho %1$s - Lyrics - Loading lyrics… - Synced - Static - Lyrics options + Lời bài hát + Đang tải lời bài hát… + Đồng bộ + Cố định + Tùy chọn lời bài hát −.5 −.1 +.1 @@ -140,59 +140,59 @@ %1$+.1fs - Failed to search for lyrics - Failed to fetch lyrics from remote - Connection timed out. Please check your internet connection. - Network error. Please check your internet connection. - Server error (code %d). Please try again later. + Không thể tìm lời bài hát + Không thể tải lời bài hát từ máy chủ + Kết nối đã hết thời gian. Vui lòng kiểm tra kết nối internet của bạn. + Lỗi mạng. Vui lòng kiểm tra kết nối internet của bạn. + Lỗi máy chủ (mã %d). Vui lòng thử lại sau. - Lyrics already available. Online fetch skipped. - Embedded lyrics already found. Online fetch skipped. - Local (.lrc) lyrics already found. Online fetch skipped. + Lời bài hát đã có sẵn. Bỏ qua tải trực tuyến. + Lời bài hát nhúng đã có sẵn. Bỏ qua tải trực tuyến. + Lời bài hát cục bộ (.lrc) đã có sẵn. Bỏ qua tải trực tuyến. - Save Lyrics - Translate via AI - These lyrics already have a translation - These lyrics are already in this language - API is not configured - Lyrics translated successfully! - Translating lyrics... - Reset imported lyrics - Reset lyrics? - Are you sure you want to reset the lyrics for this song? - Appearance - Alignment - Align lyrics left - Align lyrics center - Align lyrics right - Controls - Adjust sync - Hide sync controls - Show romanization - Show translations - Disable immersive (once) - Keep screen on + Lưu lời bài hát + Dịch qua AI + Lời bài hát này đã có bản dịch + Lời bài hát này đã bằng ngôn ngữ này + API chưa được cấu hình + Dịch lời bài hát thành công! + Đang dịch lời bài hát... + Đặt lại lời bài hát đã nhập + Đặt lại lời bài hát? + Bạn có chắc muốn đặt lại lời bài hát cho bài hát này? + Giao diện + Căn lề + Căn lời bài hát bên trái + Căn lời bài hát giữa + Căn lời bài hát bên phải + Điều khiển + Điều chỉnh đồng bộ + Ẩn điều khiển đồng bộ + Hiện phiên âm + Hiện bản dịch + Tắt chế độ toàn màn hình (một lần) + Giữ màn hình sáng - Save Lyrics - Choose which version to save: - Synced (with timestamps) - Plain (text only) + Lưu lời bài hát + Chọn phiên bản để lưu: + Đồng bộ (có đánh dấu thời gian) + Thô (chỉ văn bản) - Would you like to search for lyrics online? - Show lyric options - Always open the picker instead of auto-applying the first match - Searching for lyrics… - Lyrics not found - We couldn\'t find lyrics automatically. You can edit the title or artist and try searching manually. - Song Title - Artist (optional) - Found %d match(es) - SYNCED + Bạn có muốn tìm lời bài hát trực tuyến không? + Hiện tùy chọn lời bài hát + Luôn mở bộ chọn thay vì tự động áp dụng kết quả phù hợp đầu tiên + Đang tìm lời bài hát… + Không tìm thấy lời bài hát + Chúng tôi không thể tự động tìm lời bài hát. Bạn có thể chỉnh sửa tên bài hát hoặc nghệ sĩ và thử tìm kiếm thủ công. + Tên bài hát + Nghệ sĩ (tùy chọn) + Tìm thấy %d kết quả phù hợp + ĐỒNG BỘ %1$s • %2$s - Lyrics provided by + Lời bài hát được cung cấp bởi https://lrclib.net/ diff --git a/app/src/main/res/values-vi/strings_screens.xml b/app/src/main/res/values-vi/strings_screens.xml index 90ec1b118e..357e18951a 100644 --- a/app/src/main/res/values-vi/strings_screens.xml +++ b/app/src/main/res/values-vi/strings_screens.xml @@ -1,244 +1,244 @@ - Error: Genre ID missing + Lỗi: Thiếu ID thể loại - Let\'s Go! - Step %1$d of %2$d - Please grant the required permission first. - Please grant all required permissions. - Welcome to + Bắt đầu! + Bước %1$d/%2$d + Vui lòng cấp quyền cần thiết trước. + Vui lòng cấp tất cả quyền cần thiết. + Chào mừng đến β Beta - Let\'s get everything set up for you. - Media Permission - PixelPlayer needs access to your audio files to build your music library. - Permission Granted - Grant Media Permission - Notifications - Enable notifications to control your music from the lock screen and notification shade. - Enable Notifications - Do you have a backup? - If you already have a PixelPlayer backup, restore it now and skip most of the remaining setup on this device. - Import backup - Inspecting backup - Checking backup package… - Restoring backup - Skip / Not now - Restore Backup - Review what you want to import before finishing setup. - %1$d of %2$d modules selected - Created %1$s - Backup from %1$s - Unknown version - Restore selected - Restoring - Excluded folders - All folders are scanned by default. Pick any locations you want to ignore when building your library. - Choose folders to ignore - Grant storage permissions first - App Theme - Pick the look you want before you start exploring your library. - Dark - The default Material 3 dark look for PixelPlayer. - Light - A brighter Material 3 look across the app. - Follow system - Match your phone\'s current appearance setting. - Recommended - You can change this later in Settings > Appearance > App Theme. - Library Layout - Choose your preferred way to navigate your library. - Songs - Compact Mode - Using minimal pill navigation - Using standard tab row - SONGS - ALBUMS - ARTISTS - You can change this later in Settings > Appearance > Library Navigation. - App Navigation - Choose the style of the bottom navigation bar. - Default Style - Floating pill with rounded corners - Standard full-width bar - Customize Corner Radius - You can change this later in Settings > Appearance > Navbar Style. - Alarms & Reminders - Optional, but recommended if you use Sleep Timer and want PixelPlayer to stop playback exactly on time. - Grant Permission - Battery Optimization - Some Android devices aggressively kill background apps. Disable battery optimization for PixelPlayer to prevent unexpected playback interruptions. - Disable Optimization - All Set! - You\'re ready to enjoy your music. + Hãy thiết lập mọi thứ cho bạn. + Quyền đa phương tiện + PixelPlayer cần truy cập tệp âm thanh để xây dựng thư viện nhạc của bạn. + Đã cấp quyền + Cấp quyền đa phương tiện + Thông báo + Bật thông báo để điều khiển nhạc từ màn hình khóa và bảng thông báo. + Bật thông báo + Bạn có bản sao lưu không? + Nếu bạn đã có bản sao lưu PixelPlayer, hãy khôi phục ngay và bỏ qua hầu hết thiết lập còn lại trên thiết bị này. + Nhập bản sao lưu + Kiểm tra bản sao lưu + Kiểm tra gói sao lưu… + Khôi phục bản sao lưu + Bỏ qua / Chưa + Khôi phục bản sao lưu + Xem xét những gì bạn muốn nhập trước khi hoàn tất thiết lập. + Đã chọn %1$d/%2$d mô-đun + Đã tạo %1$s + Bản sao lưu từ %1$s + Phiên bản không xác định + Khôi phục đã chọn + Đang khôi phục + Thư mục bị loại trừ + Mặc định tất cả thư mục đều được quét. Chọn các vị trí bạn muốn bỏ qua khi xây dựng thư viện. + Chọn thư mục bỏ qua + Hãy cấp quyền lưu trữ trước + Giao diện ứng dụng + Chọn giao diện bạn muốn trước khi bắt đầu khám phá thư viện. + Tối + Giao diện Material 3 tối mặc định cho PixelPlayer. + Sáng + Giao diện Material 3 sáng hơn trên toàn ứng dụng. + Theo hệ thống + Khớp với cài đặt giao diện hiện tại của điện thoại. + Đề xuất + Bạn có thể thay đổi sau trong Cài đặt > Giao diện > Giao diện ứng dụng. + Bố cục thư viện + Chọn cách điều hướng thư viện ưa thích. + Bài hát + Chế độ thu gọn + Sử dụng thanh điều hướng pill tối giản + Sử dụng hàng tab tiêu chuẩn + BÀI HÁT + ALBUM + NGHỆ SĨ + Bạn có thể thay đổi sau trong Cài đặt > Giao diện > Điều hướng thư viện. + Điều hướng ứng dụng + Chọn kiểu thanh điều hướng dưới cùng. + Kiểu mặc định + Pill nổi với góc tròn + Thanh tiêu chuẩn đầy đủ chiều rộng + Tùy chỉnh bán kính góc + Bạn có thể thay đổi sau trong Cài đặt > Giao diện > Kiểu thanh điều hướng. + Báo thức & Nhắc nhở + Tùy chọn, nhưng được khuyến nghị nếu bạn sử dụng Hẹn giờ ngủ và muốn PixelPlayer dừng phát đúng giờ. + Cấp quyền + Tối ưu hóa pin + Một số thiết bị Android killing ứng dụng nền mạnh. Tắt tối ưu hóa pin cho PixelPlayer để ngăn chặn sự cố phát ngắt quãng bất ngờ. + Tắt tối ưu hóa + Sẵn sàng! + Bạn đã sẵn sàng thưởng thức nhạc. - Search… - Search - Clear search - Recent searches - Clear all - History - Delete search history item - No results - No results for \"%1$s\" - Nothing found - Try a different search term or check your filters. - No results found. - Browse by genre - No genres available. + Tìm kiếm… + Tìm kiếm + Xóa tìm kiếm + Tìm kiếm gần đây + Xóa tất cả + Lịch sử + Xóa mục lịch sử tìm kiếm + Không có kết quả + Không có kết quả cho \"%1$s\" + Không tìm thấy gì + Thử từ khóa khác hoặc kiểm tra bộ lọc. + Không tìm thấy kết quả. + Duyệt theo thể loại + Không có thể loại nào. - Play %1$s - Collapse %1$s - Expand %1$s - Edit artist image - Change photo - Reset to default - Shuffle play artist + Phát %1$s + Thu gọn %1$s + Mở rộng %1$s + Chỉnh sửa ảnh nghệ sĩ + Đổi ảnh + Đặt lại mặc định + Phát ngẫu nhiên nghệ sĩ - Disc %d - Cover of %1$s + Đĩa %d + Ảnh bìa của %1$s %1$s · %2$s - Playlist not found. - This playlist is empty. - Tap on \'Add Songs\' to begin. - This folder doesn\'t contain songs. - Sort Songs - More Options - Playlist options - Edit playlist - Delete playlist - Delete playlist? - Are you sure you want to delete this playlist? - Set default transition - Export Playlist + Không tìm thấy playlist. + Playlist này trống. + Nhấn \'Thêm bài hát\' để bắt đầu. + Thư mục này không chứa bài hát. + Sắp xếp bài hát + Tùy chọn khác + Tùy chọn playlist + Chỉnh sửa playlist + Xóa playlist + Xóa playlist? + Bạn có chắc muốn xóa playlist này? + Đặt chuyển tiếp mặc định + Xuất playlist %1$s • %2$s - Play it - Add - Add songs - Remove - Remove songs - Reorder - Reorder songs + Phát ngay + Thêm + Thêm bài hát + Xóa + Xóa bài hát + Sắp xếp lại + Sắp xếp lại bài hát - Global transitions - Playlist rules - This configuration applies to all playback sources unless overridden. - Configure default behavior for this specific playlist. - Active Status - Global Default - Playlist Default - Following Global - Custom Override - Custom Override - Enable to set specific rules for this playlist. - Using global defaults - Changes saved successfully - Transition Style - How tracks blend together - None + Chuyển tiếp toàn cục + Quy tắc playlist + Cấu hình này áp dụng cho tất cả nguồn phát trừ khi bị ghi đè. + Cấu hình hành vi mặc định cho playlist cụ thể này. + Trạng thái hoạt động + Mặc định toàn cục + Mặc định playlist + Theo toàn cục + Ghi đè tùy chỉnh + Ghi đè tùy chỉnh + Bật để đặt quy tắc cụ thể cho playlist này. + Sử dụng mặc định toàn cục + Đã lưu thay đổi thành công + Kiểu chuyển tiếp + Cách bản nhạc hòa trộn + Không Crossfade - Transition Duration - %1$ds total overlap - Reset Transition - Current Song - Next Song - Tracks will overlap for %1$ds - Volume Curves - Fine tune the audio slope + Thời lượng chuyển tiếp + Tổng chồng chéo %1$ds + Đặt lại chuyển tiếp + Bài hát hiện tại + Bài hát tiếp theo + Các bản nhạc sẽ chồng chéo trong %1$ds + Đường cong âm lượng + Điều chỉnh tông âm thanh Fade Out Fade In - New smart playlist - New playlist - Add Songs - Back or Cancel - Next - Create - Edit Playlist - Auto-generated collage - Add Photo - Pick Image - Change - Remove - Playlist Name - My awesome mix - Edit Cover - Adjust your cover art - Use pinch and drag gestures to find the perfect framing - Manual - Smart - Generate with AI - Smart Rule - Default - Image - Icon - Background Color - Icon Symbol - Shape Style - Shape parameters - Corner Radius - Smoothness - Sides - Curve - Rotation - Scale - Top Played - Your most played tracks. - Recently Played - Songs you listened to most recently. - Forgotten Favorites - Favorite tracks you haven\'t played in a while. - New Gems - Recently added tracks with low play counts. + Playlist thông minh mới + Playlist mới + Thêm bài hát + Quay lại hoặc Hủy + Tiếp theo + Tạo + Chỉnh sửa playlist + Collage tự động + Thêm ảnh + Chọn ảnh + Đổi + Xóa + Tên playlist + Playlist tuyệt vời của tôi + Chỉnh sửa bìa + Điều chỉnh ảnh bìa + Sử dụng thao tác vuốt và kéo để tìm khung hình hoàn hảo + Thủ công + Thông minh + Tạo với AI + Quy tắc thông minh + Mặc định + Ảnh + Biểu tượng + Màu nền + Ký hiệu biểu tượng + Kiểu hình dạng + Tham số hình dạng + Bán kính góc + Độ mượt + Cạnh + Đường cong + Xoay + Tỷ lệ + Phát nhiều nhất + Các bản nhạc bạn phát nhiều nhất. + Phát gần đây + Bài hát bạn nghe gần đây nhất. + Yêu thích bị lãng quên + Bản nhạc yêu thích bạn đã lâu không nghe. + Kho báu mới + Bản nhạc mới thêm với số lần phát thấp. - Quick Fill Genre - Sort & Play - Shuffle - Sort By - Artist + Điền nhanh thể loại + Sắp xếp & Phát + Phát ngẫu nhiên + Sắp xếp theo + Nghệ sĩ Album - Title - Generic artist - %1$s Shuffle + Tiêu đề + Nghệ sĩ chung + %1$s Ngẫu nhiên - Select Songs - Choose Genre - Search songs - New Genre - Add Custom - Add Custom Genre - Genre Name - Select Icon - Genre: %1$s - Select a genre - Quick Fill + Chọn bài hát + Chọn thể loại + Tìm bài hát + Thể loại mới + Thêm tùy chỉnh + Thêm thể loại tùy chỉnh + Tên thể loại + Chọn biểu tượng + Thể loại: %1$s + Chọn thể loại + Điền nhanh DJ Space - Loading… - Deck %1$d - Load song - No song loaded + Đang tải… + Bộ %1$d + Tải bài hát + Chưa tải bài hát - Stem separation is not available yet. - Volume - Speed + Phân tách stem chưa khả dụng. + Âm lượng + Tốc độ Crossfader - Deck 1 - Deck 2 - Select a song - Play/Pause - Song cover + Bộ 1 + Bộ 2 + Chọn bài hát + Phát/Tạm dừng + Ảnh bìa bài hát x%1$.2f diff --git a/app/src/main/res/values-vi/strings_settings.xml b/app/src/main/res/values-vi/strings_settings.xml index b7f5cc4f53..7082109828 100644 --- a/app/src/main/res/values-vi/strings_settings.xml +++ b/app/src/main/res/values-vi/strings_settings.xml @@ -1,108 +1,110 @@ - Music Management - Manage folders, refresh library, parsing options - Appearance - Themes, layout, and visual styles - Lyrics - Source priority, immersive lyrics, and import management - Playback - Audio behavior, crossfade, and background play - Behavior - Gestures, haptics, and navigation behavior - AI Integration (β) - AI providers, API keys, and model settings - Backup & Restore - Export and recover your personal app data - Developer Options - Experimental features and debugging - Equalizer - Adjust audio frequencies and presets - Device Capabilities - Audio specs, codecs, and decoder info - Accounts - Manage Telegram, Google Drive, NetEase, and more services - About - App info, version, and credits + Quản lý nhạc + Quản lý thư mục, làm mới thư viện, tùy chọn phân tích + Giao diện + Chủ đề, bố cục và kiểu hiển thị + Lời bài hát + Ưu tiên nguồn, lời bài hát đắm chìm và quản lý nhập + Phát lại + Hành vi âm thanh, crossfade và phát nền + Hành vi + Cử chỉ, rung cảm ứng và hành vi điều hướng + Tích hợp AI (β) + Nhà cung cấp AI, API key, chọn mô hình và nhật ký hoạt động + Tham số tạo + Nhiệt độ, lấy mẫu, prompt và cấu hình dữ liệu bài hát + Sao lưu & Khôi phục + Xuất và khôi phục dữ liệu cá nhân của bạn + Tùy chọn nhà phát triển + Tính năng thử nghiệm và gỡ lỗi + Bộ equalizer + Điều chỉnh tần số âm thanh và preset + Khả năng thiết bị + Thông số âm thanh, codec và thông tin bộ giải mã + Tài khoản + Quản lý Telegram, Google Drive, NetEase và nhiều dịch vụ khác + Giới thiệu + Thông tin ứng dụng, phiên bản và credits - On - Off - Enabled - Disabled - Open - Select all - Clear selection - Close notice + Bật + Tắt + Đã bật + Đã tắt + Mở + Chọn tất cả + Xóa lựa chọn + Đóng thông báo - Library Structure - Excluded Directories - Folders here will be skipped when scanning your library. - Artists - Multi-artist parsing and organization options. - Filtering - Minimum Song Duration - Minimum Tracks per Album - Album Art Cache Limit - Sync and Scanning - Refresh Library - Scan entire library for new and modified files. - Full Rescan - Running full rescan - Full rescan started… - Library sync finished - Rebuild Database - Rebuild database? - This will completely rebuild your music library from scratch. All imported lyrics, favorites, and custom metadata will be lost. This action cannot be undone. - Rebuild - Rebuilding database - Rebuilding database… - Auto-scan .lrc files - Automatically scan and assign .lrc files in the same folder during library sync. - Lyrics Management - Lyrics Source Priority - Choose which source to try first when fetching lyrics. - Embedded First - Online First - Local (.lrc) First - Reset Imported Lyrics - Remove all imported lyrics from the database. - Reset imported lyrics? - This action cannot be undone. + Cấu trúc thư viện + Thư mục loại trừ + Các thư mục ở đây sẽ bị bỏ qua khi quét thư viện của bạn. + Nghệ sĩ + Tùy chọn phân tích và tổ chức đa nghệ sĩ. + Lọc + Thời lượng bài hát tối thiểu + Số bài hát tối thiểu mỗi album + Giới hạn bộ nhớ đệm ảnh album + Đồng bộ và quét + Làm mới thư viện + Quét toàn bộ thư viện để tìm file mới và đã sửa đổi. + Quét lại toàn bộ + Đang quét lại toàn bộ + Đã bắt đầu quét lại toàn bộ… + Đồng bộ thư viện hoàn tất + Xây dựng lại cơ sở dữ liệu + Xây dựng lại cơ sở dữ liệu? + Điều này sẽ xây dựng lại toàn bộ thư viện nhạc từ đầu. Tất cả lời bài hát đã nhập, yêu thích và metadata tùy chỉnh sẽ bị mất. Hành động này không thể hoàn tác. + Xây dựng lại + Đang xây dựng lại cơ sở dữ liệu + Đang xây dựng lại cơ sở dữ liệu… + Tự động quét file .lrc + Tự động quét và gán file .lrc trong cùng thư mục khi đồng bộ thư viện. + Quản lý lời bài hát + Ưu tiên nguồn lời bài hát + Chọn nguồn nào thử trước khi lấy lời bài hát. + Embedded trước + Trực tuyến trước + Local (.lrc) trước + Đặt lại lời bài hát đã nhập + Xóa tất cả lời bài hát đã nhập khỏi cơ sở dữ liệu. + Đặt lại lời bài hát đã nhập? + Hành động này không thể hoàn tác. - Refresh - Everything is allowed by default. Tap a folder to mark it as excluded from scans. - No subfolders here - Navigate up - Go to root + Làm mới + Mọi thứ được phép theo mặc định. Nhấn vào thư mục để đánh dấu loại trừ khỏi quét. + Không có thư mục con ở đây + Đi lên + Về thư mục gốc - Rescan Required - Artist settings have changed. Rescan your library to apply. - Rescan - Scanning… - Multi-Artist Parsing - Character Delimiters - Current: %1$s - Word Delimiters - None - Current: %1$s - Configure - Extract Artists from Title - Detect feat., ft., with in song titles - Library Organization - Group by Album Artist - Show collaboration albums under main artist - About Multi-Artist Parsing + Cần quét lại + Cài đặt nghệ sĩ đã thay đổi. Quét lại thư viện để áp dụng. + Quét lại + Đang quét… + Phân tích đa nghệ sĩ + Dấu phân cách ký tự + Hiện tại: %1$s + Dấu phân cách từ + Không có + Hiện tại: %1$s + Cấu hình + Trích xuất nghệ sĩ từ tiêu đề + Phát hiện feat., ft., with trong tiêu đề bài hát + Tổ chức thư viện + Nhóm theo nghệ sĩ album + Hiển thị album hợp tác dưới nghệ sĩ chính + Về phân tích đa nghệ sĩ - PixelPlayer splits artist tags using character delimiters (/, ;, &) and word delimiters (feat., ft., vs., x). Word delimiters are matched case-insensitively. - \"Extract Artists from Title\" detects patterns like (feat. Artist) in song titles. - Backslash (\) can be used to escape character delimiters. + PixelPlayer tách tag nghệ sĩ sử dụng dấu phân cách ký tự (/, ;, &) và dấu phân cách từ (feat., ft., vs., x). Dấu phân cách từ được khớp không phân biệt hoa thường. + \"Trích xuất nghệ sĩ từ tiêu đề\" phát hiện các mẫu như (feat. Artist) trong tiêu đề bài hát. + Dấu gạch chéo ngược (\\) có thể được sử dụng để escape dấu phân cách ký tự. - Examples + Ví dụ \u2192 \u266A \"Artist1/Artist2\" @@ -117,54 +119,54 @@ AC/DC (escaped) - Delimiters - Current Delimiters - Tap a delimiter to remove it. At least one delimiter is required. - Add New Delimiter - e.g., / or ; - Add delimiter - Default Delimiters - Reset Delimiters? - This will clear all your custom delimiters and restore the defaults. This action cannot be undone. - Delimiters reset to defaults - At least one delimiter is required - Delimiter added - Delimiter already exists or is invalid - Space + Dấu phân cách + Dấu phân cách hiện tại + Nhấn vào dấu phân cách để xóa. Cần ít nhất một dấu phân cách. + Thêm dấu phân cách mới + ví dụ: / hoặc ; + Thêm dấu phân cách + Dấu phân cách mặc định + Đặt lại dấu phân cách? + Điều này sẽ xóa tất cả dấu phân cách tùy chỉnh của bạn và khôi phục mặc định. Hành động này không thể hoàn tác. + Dấu phân cách đã đặt lại mặc định + Cần ít nhất một dấu phân cách + Đã thêm dấu phân cách + Dấu phân cách đã tồn tại hoặc không hợp lệ + Dấu cách - Word Delimiters - Current Word Delimiters - These keywords split artist names when surrounded by spaces. Matched case-insensitively. Tap to remove. - No word delimiters configured - Add New Word Delimiter - e.g., feat. or ft. - Add word delimiter - How Word Delimiters Work - Word delimiters are matched case-insensitively with spaces around them.\n\nSingle-character delimiters (like \"x\") require spaces on both sides to avoid false matches.\n\nExamples:\n \"Drake feat. Rihanna\" -> Drake, Rihanna\n \"Marshmello x Bastille\" -> Marshmello, Bastille\n \"A vs. B\" -> A, B - Reset Word Delimiters? - This will clear all your custom word delimiters and restore the default keywords. This action cannot be undone. - Word delimiter added - Already exists or is invalid - Word delimiters reset to defaults + Dấu phân cách từ + Dấu phân cách từ hiện tại + Các từ khóa này tách tên nghệ sĩ khi được bao quanh bởi dấu cách. Khớp không phân biệt hoa thường. Nhấn để xóa. + Chưa cấu hình dấu phân cách từ + Thêm dấu phân cách từ mới + ví dụ: feat. hoặc ft. + Thêm dấu phân cách từ + Cách dấu phân cách từ hoạt động + Dấu phân cách từ được khớp không phân biệt hoa thường với dấu cách xung quanh.\n\nDấu phân cách một ký tự (như \"x\") cần dấu cách ở cả hai bên để tránh khớp sai.\n\nVí dụ:\n \"Drake feat. Rihanna\" -> Drake, Rihanna\n \"Marshmello x Bastille\" -> Marshmello, Bastille\n \"A vs. B\" -> A, B + Đặt lại dấu phân cách từ? + Điều này sẽ xóa tất cả dấu phân cách từ tùy chỉnh của bạn và khôi phục từ khóa mặc định. Hành động này không thể hoàn tác. + Đã thêm dấu phân cách từ + Đã tồn tại hoặc không hợp lệ + Dấu phân cách từ đã đặt lại mặc định - Preparing sync - Reading MediaStore - Processing tracks - Saving to database - Scanning lyrics files - Cleaning album art cache - Syncing cloud sources - Completing sync + Chuẩn bị đồng bộ + Đọc MediaStore + Xử lý bài hát + Lưu vào cơ sở dữ liệu + Quét file lời bài hát + Dọn dẹp bộ nhớ đệm ảnh album + Đồng bộ nguồn cloud + Hoàn tất đồng bộ %1$s • %2$d%% (%3$d/%4$d) %1$s… - Global Theme - App Language - Choose the language used across the app interface. - System default + Chủ đề toàn cục + Ngôn ngữ ứng dụng + Chọn ngôn ngữ sử dụng trong giao diện ứng dụng. + Mặc định hệ thống English Español Deutsch @@ -174,150 +176,150 @@ Bahasa Indonesia Italiano 日本語 - Korean + 한국어 Norwegian (Bokmål) 繁體中文 Türkçe Tiếng Việt - App Theme - Switch between light, dark, or follow system appearance. - Light Theme - Dark Theme - Follow System - Use Smooth Corners - Use complex shaped corners effectively improving aesthetics but may affect performance on low-end devices - Disable Blur Effects - Turn off blur effects across the app to save battery and resources. - Show Scrollbar - Display lateral scrollbar on music lists for quick scrolling - Now Playing - Player Theme - Choose the appearance for the floating player. - Album Art - System Dynamic - Show player file info - Show codec, bitrate, and sample rate in the player progress section. - Album Art Palette Style - Current: %1$s. Open live preview and choose style. - Carousel Style - Choose the appearance for the album carousel. - No Peek - One Peek - Home Collage - Collage Pattern - Choose the shape arrangement for the Your Mix collage. - Auto-Rotate Patterns - Cycle through collage patterns each time you visit Home. - Navigation Bar - NavBar Style - Choose the appearance for the navigation bar. - Default - Full Width - Compact mode - Show only icons and reduce the navbar height. - NavBar Corner Radius - Adjust the corner radius of the navigation bar. - Lyrics Screen - Immersive Lyrics - Auto-hide controls and enlarge text. - Auto-hide Delay - Time before controls hide. + Chủ đề ứng dụng + Chuyển đổi giữa sáng, tối hoặc theo giao diện hệ thống. + Chủ đề sáng + Chủ đề tối + Theo hệ thống + Sử dụng góc mềm + Sử dụng góc có hình dạng phức tạp giúp cải thiện thẩm mỹ nhưng có thể ảnh hưởng hiệu suất trên thiết bị thấp + Tắt hiệu ứng mờ + Tắt hiệu ứng mờ trong toàn bộ ứng dụng để tiết kiệm pin và tài nguyên. + Hiển thị thanh cuộn + Hiển thị thanh cuộn bên trên danh sách nhạc để cuộn nhanh + Đang phát + Chủ đề trình phát + Chọn giao diện cho trình phát nổi. + Ảnh album + Động hệ thống + Hiển thị thông tin file trình phát + Hiển thị codec, bitrate và sample rate trong phần tiến trình trình phát. + Kiểu bảng màu ảnh album + Hiện tại: %1$s. Mở xem trước trực tiếp và chọn kiểu. + Kiểu carousel + Chọn giao diện cho carousel album. + Không lén nhìn + Một lần lén nhìn + Collage trang chủ + Mẫu collage + Chọn cách sắp xếp hình dạng cho collage Mix của bạn. + Tự động xoay mẫu + Đổi qua các mẫu collage mỗi lần bạn truy cập Trang chủ. + Thanh điều hướng + Kiểu NavBar + Chọn giao diện cho thanh điều hướng. + Mặc định + Đầy đủ chiều rộng + Chế độ thu gọn + Chỉ hiển thị biểu tượng và giảm chiều cao navbar. + Bán kính góc NavBar + Điều chỉnh bán kính góc của thanh điều hướng. + Màn hình lời bài hát + Lời bài hát đắm chìm + Tự động ẩn điều khiển và phóng to chữ. + Trì hoãn tự ẩn + Thời gian trước khi điều khiển bị ẩn. 3s 4s 5s 6s - App Navigation - Default Tab - Choose the Default launch tab. - Home - Search - Library - Library Navigation - Choose how to move between Library tabs. - Tab row (default) - Compact pill & grid + Điều hướng ứng dụng + Tab mặc định + Chọn tab khởi chạy mặc định. + Trang chủ + Tìm kiếm + Thư viện + Điều hướng thư viện + Chọn cách di chuyển giữa các tab Thư viện. + Hàng tab (mặc định) + Viên thu gọn & lưới - Colors - Palette style - Choose the album colors for the player UI. - Tonal Spot - Balanced and calm. - Vibrant - High saturation accents. - Expressive - Bold hue shifts and contrast. - Fruit Salad - Playful rotated accents. - Color accuracy - 0 keeps the current tuning. Higher values stay closer to the album art\'s dominant hue. - Current - More accurate - 0 • Current - %1$d • Subtle - %1$d • Balanced - %1$d • Precise + Màu sắc + Kiểu bảng màu + Chọn màu album cho giao diện trình phát. + Điểm tông + Cân bằng và bình tĩnh. + Sống động + Sắc thái bão hòa cao. + Phóng khoáng + Đổi màu táo bạo và tương phản. + Salad trái cây + Sắc thái vui nhộn xoay vòng. + Độ chính xác màu + 0 giữ nguyên tinh chỉnh hiện tại. Giá trị cao hơn giữ gần với màu chủ đạo của ảnh album. + Hiện tại + Chính xác hơn + 0 • Hiện tại + %1$d • Nhẹ nhàng + %1$d • Cân bằng + %1$d • Chính xác - Adjust Corner Radius - Match the navbar shape\'s corners with your device\'s physical corners for a seamless look. - Corner Radius + Điều chỉnh bán kính góc + Khớp các góc của hình dạng navbar với góc vật lý thiết bị của bạn để có vẻ liền mạch. + Bán kính góc %1$d dp - Background Playback - Keep playing after closing - If off, removing the app from recents will stop playback. - Battery Optimization - Disable battery optimization to prevent playback interruptions. - Battery optimization is already disabled - Could not open battery settings - Volume Normalization (ReplayGain) - Enable ReplayGain - Normalize volume levels using ReplayGain metadata from audio files. - Gain Mode - Track: normalize each song. Album: normalize per album. - Track + Phát nền + Tiếp tục phát sau khi đóng + Nếu tắt, xóa ứng dụng khỏi danh sách gần đây sẽ dừng phát. + Tối ưu hóa pin + Tắt tối ưu hóa pin để ngăn chặn gián đoạn phát. + Tối ưu hóa pin đã bị tắt + Không thể mở cài đặt pin + Chuẩn hóa âm lượng (ReplayGain) + Bật ReplayGain + Chuẩn hóa mức âm lượng bằng metadata ReplayGain từ file âm thanh. + Chế độ Gain + Bài hát: chuẩn hóa mỗi bài. Album: chuẩn hóa mỗi album. + Bài hát Album - Cast - Auto-play on cast connect/disconnect - Start playing immediately after switching cast connections. - Headphones - Resume when headphones reconnect - If playback paused because headphones were removed, resume automatically when they connect again. - Queue and Transitions + Phản chiếu + Tự động phát khi kết nối/ngắt kết nối cast + Bắt đầu phát ngay sau khi chuyển đổi kết nối cast. + Tai nghe + Tiếp tục khi tai nghe kết nối lại + Nếu phát bị tạm dừng vì tháo tai nghe, tự động tiếp tục khi chúng kết nối lại. + Hàng đợi và chuyển đổi Crossfade - Enable smooth transition between songs. - Crossfade Duration - Hi-Fi Mode - Float 32-bit audio output. Disable if playback stutters on your device. - Not supported on this device (PCM_FLOAT AudioTrack unavailable). - Persistent Shuffle - Remember shuffle setting even after closing the app. - Show queue history - Show previously played songs in the queue. + Bật chuyển đổi mượt mà giữa các bài hát. + Thời lượng Crossfade + Chế độ Hi-Fi + Đầu ra âm thanh float 32-bit. Tắt nếu phát bị giật trên thiết bị của bạn. + Không được hỗ trợ trên thiết bị này (PCM_FLOAT AudioTrack không khả dụng). + Trộn bài bền vững + Nhớ cài đặt trộn bài ngay cả sau khi đóng ứng dụng. + Hiển thị lịch sử hàng đợi + Hiển thị các bài hát đã phát trước đó trong hàng đợi. - Folders - Back gesture controls folders - In Folders tab, system back navigates folder stack before leaving Library. - Player Gestures - Tap background closes player - Tap the blurred background to close the player sheet. - Haptics - Haptic feedback - Enable vibration feedback across the app. + Thư mục + Cử chỉ quay lại kiểm soát thư mục + Trong tab Thư mục, quay lại hệ thống điều hướng ngăn xếp thư mục trước khi rời Thư viện. + Cử chỉ trình phát + Nhấn nền đóng trình phát + Nhấn vào nền mờ để đóng sheet trình phát. + Cảm ứng + Phản hồi cảm ứng + Bật phản hồi rung trong toàn bộ ứng dụng. - AI Provider - Provider - Choose your AI provider - Safe Token Mode - ON — Fast & cheap. Sends minimal data (~1K tokens) to AI. - OFF — Deep context. Sends full listening profile (~8K tokens) for richer results. - Credentials - %1$s API Key - Get from %1$s + Nhà cung cấp AI + Nhà cung cấp + Chọn nhà cung cấp AI của bạn + Chế độ Token An toàn + BẬT — Nhanh & rẻ. Gửi dữ liệu tối thiểu (~1K token) đến AI. + TẮT — Ngữ cảnh sâu. Gửi toàn bộ hồ sơ nghe (~8K token) để có kết quả phong phú hơn. + Thông tin đăng nhập + API Key %1$s + Lấy từ %1$s Google AI Studio (aistudio.google.com) DeepSeek Platform (api.deepseek.com) Groq Console (console.groq.com) @@ -326,329 +328,338 @@ Moonshot AI Platform (platform.moonshot.cn) Zhipu AI Open Platform (bigmodel.cn) OpenAI Platform (platform.openai.com) - Model Selection - Loading available models… - Failed to load models - AI Model - Select a model. - Enter API Key - Prompt Behavior - System Prompt - Customize how the AI behaves. - Preset Prompts - Enter system prompt… - Professional Curator - You are \'Vibe-Engine\', a world-class music curator and master of sonic flow. Your goal is to construct seamless, high-fidelity listening experiences. Prioritize harmonic compatibility, logical BPM transitions, and a sophisticated balance between familiar favorites and tasteful, logic-driven discovery. - Creative Maverick - You are an avant-garde music explorer specializing in \'unexpected cohesion\'. Your mission is to break conventional genre boundaries by identifying non-obvious sonic parallels. Prioritize rare deep cuts, experimental textures, and artistic novelty while maintaining a surprising but undeniable transition logic. - Strict Librarian - You are a surgical music database architect. Your logic is driven by absolute metadata precision and rigid categorical adherence. Minimize algorithmic discovery in favor of strict genre consistency, energy-level matching, and maximizing the retrieval of highly specific user-defined preferences. - Atmospheric Guide - You are a master of ambient textures and low-energy flow. Focus exclusively on tracks that facilitate a \'deep focus\' or \'tranquility\' state. Prioritize acoustic warmth, minimalist arrangements, and gentle transitions, while strictly avoiding high-transient sounds or sudden shifts in dynamic range. - Sonic Enthusiast - You are an audiophile analyst focused on production complexity and instrumentation. Prioritize tracks characterized by high dynamic range, intricate polyrhythms, and superior soundstage quality. Favor active-listening pieces that reward the listener for paying attention to technical fidelity and arrangement detail. - Energy Catalyst - You are a high-momentum rhythm generator. Your philosophy centers on driving basslines, percussive intensity, and infectious grooves. Prioritize high-BPM club compatibility, syncopated energy, and continuous rhythmic tension to keep the listener\'s heart rate and motivation peak-level. - AI Usage Report - Total Consumption - %1$s tokens tracking\nPrompt: %2$s | Output: %3$s | Thought: %4$s - Clear Logs - AI Activity Log (%1$d) + Chọn mô hình + Đang tải mô hình khả dụng… + Không thể tải mô hình + Mô hình AI + Chọn một mô hình. + Nhập API Key + Hành vi Prompt + Prompt hệ thống + Tùy chỉnh cách AI hoạt động. + Prompt có sẵn + Nhập prompt hệ thống… + Chuyên gia tuyển chọn chuyên nghiệp + Bạn là \'Vibe-Engine\', một chuyên gia tuyển chọn nhạc đẳng cấp thế giới và bậc thầy về dòng chảy âm thanh. Mục tiêu của bạn là xây dựng trải nghiệm nghe liền mạch, chất lượng cao. Ưu tiên sự hài hòa, chuyển đổi BPM logic và sự cân bằng tinh tế giữa các bài yêu thích quen thuộc và khám phá có gu, dựa trên logic. + Nhà khám phá sáng tạo + Bạn là nhà thám hiểm âm nhạc tiên phong chuyên về \'sự gắn kết bất ngờ\'. Sứ mệnh của bạn là phá vỡ ranh giới thể loại truyền thống bằng cách xác định các điểm tương đồng âm thanh không obvious. Ưu tiên các bản cắt sâu hiếm có, kết cấu thử nghiệm và sự mới lạ nghệ thuật trong khi duy trì logic chuyển đổi bất ngờ nhưng không thể phủ nhận. + Thủ thư nghiêm ngặt + Bạn là kiến trúc sư cơ sở dữ liệu âm nhạc chính xác. Logic của bạn được thúc đẩy bởi độ chính xác metadata tuyệt đối và sự tuân thủ danh mục nghiêm ngặt. Giảm thiểu khám phá алгоритм để ủng hộ sự nhất quán thể loại nghiêm ngặt, sự khớp_LEVEL năng lượng và tối đa hóa việc truy xuất các sở thích được người dùng xác định rất cụ thể. + Hướng dẫn viên không khí + Bạn là bậc thầy về kết cấu môi trường và dòng chảy năng lượng thấp. Tập trung độc quyền vào các bản nhạc tạo điều kiện cho trạng thái \'tập trung sâu\' hoặc \'bình yên\'. Ưu tiên sự ấm áp âm thanh, sắp xếp tối giản và chuyển đổi nhẹ nhàng, đồng thời tránh nghiêm ngặt các âm thanh transient cao hoặc thay đổi đột ngột trong phạm vi động. + Người đam mê âm thanh + Bạn là nhà phân tích âm thanh tập trung vào độ phức tạp sản xuất và nhạc cụ. Ưu tiên các bản nhạc được đặc trưng bởi phạm vi động cao, polyrhythm phức tạp và chất lượng soundstage vượt trội. Ưa thích các bản nhạc nghe tích cực thưởng cho người nghe khi chú ý đến độ trung thực kỹ thuật và chi tiết sắp xếp. + Chất xúc tác năng lượng + Bạn là máy tạo nhịp động lượng cao. Triết lý của bạn tập trung vào đường bass mạnh mẽ, cường độ gõ và groove lây lan. Ưu tiên khả năng tương thích câu lạc bộ BPM cao, năng lượng syncopated và căng thẳng nhịp liên tục để giữ nhịp tim và động lực ở mức cao nhất của người nghe. + Báo cáo sử dụng AI + Tổng tiêu thụ + Theo dõi %1$s token\nPrompt: %2$s | Đầu ra: %3$s | Suy nghĩ: %4$s + Xóa nhật ký + Nhật ký hoạt động AI (%1$d) %1$s · %2$s - Show - Hide + Hiển thị + Ẩn - How backup works - Choose sections, export a .pxpl file, and import it later. Restore only replaces the sections you select. - Create Backup - Export Backup - No sections selected. - All sections selected. - Selected %1$d of %2$d sections. - %1$s Creates a .pxpl backup file. - Select & Export - Restore Backup - Import Backup - Select & Restore - Browse or pick from recent backups. Selected data will replace current data. + Sao lưu hoạt động như thế nào + Chọn các phần, xuất file .pxpl và nhập sau. Khôi phục chỉ thay thế các phần bạn chọn. + Tạo sao lưu + Xuất sao lưu + Chưa chọn phần nào. + Đã chọn tất cả phần. + Đã chọn %1$d trong số %2$d phần. + %1$s Tạo file sao lưu .pxpl. + Chọn & Xuất + Khôi phục sao lưu + Nhập sao lưu + Chọn & Khôi phục + Duyệt hoặc chọn từ các bản sao lưu gần đây. Dữ liệu được chọn sẽ thay thế dữ liệu hiện tại. - Choose exactly what you want to include in the backup package. - Select a .pxpl backup file to inspect. You\'ll choose which sections to restore in the next step. - %1$d of %2$d sections selected - %1$d of %2$d modules selected - Recent Backups - No recent backups - Previously imported backups will appear here. - %1$d entries · Will replace current data - Export .pxpl - Restore Selected - Transfer in progress… + Chọn chính xác những gì bạn muốn đưa vào gói sao lưu. + Chọn file sao lưu .pxpl để kiểm tra. Bạn sẽ chọn các phần nào để khôi phục ở bước tiếp theo. + Đã chọn %1$d trong số %2$d phần + Đã chọn %1$d trong số %2$d mô-đun + Sao lưu gần đây + Không có bản sao lưu gần đây + Các bản sao lưu đã nhập trước đó sẽ xuất hiện ở đây. + %1$d mục · Sẽ thay thế dữ liệu hiện tại + Xuất .pxpl + Khôi phục đã chọn + Đang truyền… PixelPlayer_Backup_%1$d.pxpl - Creating Backup - Restoring Backup + Đang tạo sao lưu + Đang khôi phục sao lưu %1$d%% %1$s • %2$s - Exporting - Importing - Restoring - Remove from history - Inspecting… - Browse for file - Step %1$d of %2$d - Restore Modules - Backup Details - Created - App Version + Đang xuất + Đang nhập + Đang khôi phục + Xóa khỏi lịch sử + Đang kiểm tra… + Duyệt file + Bước %1$d trong số %2$d + Khôi phục mô-đun + Chi tiết sao lưu + Ngày tạo + Phiên bản ứng dụng Schema - Device - Unknown + Thiết bị + Không xác định · %1$s - %1$d modules · v%2$s · schema v%3$d + %1$d mô-đun · v%2$s · schema v%3$d \? - Select all - Clear selection + Chọn tất cả + Xóa lựa chọn - Invalid backup: %1$s - Preparing restore - Starting restore task. - Preparing backup - Starting backup task. - Backup restored successfully - Restore completed with some unresolved issues. - Restore could not be completed: %1$s - Restore failed: %1$s - Data exported successfully - Export failed: %1$s - Data restored successfully - Restore completed with unresolved issues. Failed: %1$s + Sao lưu không hợp lệ: %1$s + Chuẩn bị khôi phục + Bắt đầu tác vụ khôi phục. + Chuẩn bị sao lưu + Bắt đầu tác vụ sao lưu. + Khôi phục sao lưu thành công + Khôi phục hoàn tất với một số vấn đề chưa giải quyết. + Không thể hoàn tất khôi phục: %1$s + Khôi phục thất bại: %1$s + Xuất dữ liệu thành công + Xuất thất bại: %1$s + Khôi phục dữ liệu thành công + Khôi phục hoàn tất với các vấn đề chưa giải quyết. Thất bại: %1$s v%1$d %1$s %2$s - Experiments - Experimental - Player UI loading experiments and toggles. - Test Setup Flow - Launch the onboarding setup screen for testing. - Maintenance - Force Daily Mix Regeneration - Re-creates the daily mix playlist immediately. - Regenerate Daily Mix - Regenerate Daily Mix? - This will discard the current mix and generate a new one based on recent listening habits. - Daily Mix regeneration started - Force Stats Regeneration - Clears cache and recalculates playback statistics. - Regenerate - Working… - Regenerate Stats - Regenerate Stats? - This will clear the statistics cache and force a recalculation from the database history. - Stats regeneration started - Force Album Palette Regeneration - Rebuild all cached palette variants for every album art, or pick a single one to refresh. - Regenerate All - Regenerate all album palettes? - This will clear cached theme data and rebuild all palette styles for %1$d unique album arts. - Regenerating… - Regenerating album palettes… - Rebuilding cached palette variants for %1$d unique album arts. This can take a while on large libraries. - %1$d of %2$d completed - Regenerated %1$d album art palettes - Regenerated %1$d of %2$d album art palettes - Choose Song - Select a song to clear cached theme data and regenerate all palette styles from the album art. - Search by title, artist, or album - No songs match your search. - No songs with album art were found. - Regenerating palette… - Palette regenerated for %1$s - Could not regenerate palette for %1$s - Diagnostics - Trigger Test Crash - Simulate a crash to test the crash reporting system. - Test crash triggered from Developer Options - This is intentional for testing the crash reporting system + Thử nghiệm + Thử nghiệm + Các experiment tải UI trình phát và công tắc. + Luồng cài đặt kiểm tra + Khởi chạy màn hình cài đặt onboarding để kiểm tra. + Bảo trì + Bắt buộc tạo lại Mix hàng ngày + Tạo lại danh sách phát mix hàng ngày ngay lập tức. + Tạo lại Mix hàng ngày + Tạo lại Mix hàng ngày? + Điều này sẽ loại bỏ mix hiện tại và tạo bản mới dựa trên thói quen nghe gần đây. + Đã bắt đầu tạo lại Mix hàng ngày + Bắt buộc tạo lại thống kê + Xóa bộ nhớ đệm và tính lại thống kê phát. + Tạo lại + Đang xử lý… + Tạo lại thống kê + Tạo lại thống kê? + Điều này sẽ xóa bộ nhớ đệm thống kê và bắt buộc tính lại từ lịch sử cơ sở dữ liệu. + Đã bắt đầu tạo lại thống kê + Bắt buộc tạo lại bảng màu album + Xây dựng lại tất cả biến thể bảng màu đã đệm cho mọi ảnh album, hoặc chọn một ảnh để làm mới. + Tạo lại tất cả + Tạo lại tất cả bảng màu album? + Điều này sẽ xóa dữ liệu chủ đề đã đệm và xây dựng lại tất cả kiểu bảng màu cho %1$d ảnh album duy nhất. + Đang tạo lại… + Đang tạo lại bảng màu album… + Xây dựng lại biến thể bảng màu đã đệm cho %1$d ảnh album duy nhất. Điều này có thể mất thời gian trên thư viện lớn. + Đã hoàn tất %1$d trong số %2$d + Đã tạo lại %1$d bảng màu ảnh album + Đã tạo lại %1$d trong số %2$d bảng màu ảnh album + Chọn bài hát + Chọn một bài hát để xóa dữ liệu chủ đề đã đệm và tạo lại tất cả kiểu bảng màu từ ảnh album. + Tìm theo tiêu đề, nghệ sĩ hoặc album + Không có bài hát nào khớp với tìm kiếm của bạn. + Không tìm thấy bài hát nào có ảnh album. + Đang tạo lại bảng màu… + Đã tạo lại bảng màu cho %1$s + Không thể tạo lại bảng màu cho %1$s + Chẩn đoán + Kích hoạt lỗi thử nghiệm + Mô phỏng lỗi để kiểm tra hệ thống báo cáo lỗi. + Lỗi thử nghiệm được kích hoạt từ Tùy chọn nhà phát triển - Đây là hành động có chủ đích để kiểm tra hệ thống báo cáo lỗi - Experimental - PlayerUI loading tweaks - Animated Lyrics (High-end devices) - Uses spring animations and visual effects for lyrics. May cause frame drops on low-end devices. - Lyric Blur Effect - Applies a depth-of-field blur to inactive lyrics. - Blur Strength - Adjust the intensity of the blur effect. + Thử nghiệm + Các experiment tải PlayerUI + Lời bài hát động (Thiết bị cao cấp) + Sử dụng spring animation và hiệu ứng thị giác cho lời bài hát. Có thể gây tụt khung hình trên thiết bị thấp. + Hiệu ứng mờ lời bài hát + Áp dụng độ mờ trường ảnh cho lời bài hát không hoạt động. + Cường độ mờ + Điều chỉnh cường độ hiệu ứng mờ. %1$.1fx - Step 1 · Choose what to delay - Delay everything - Hold the full player content until the sheet background is fully expanded. - Album carousel - Delay album art and carousel until the sheet is expanded. - Song metadata - Delay title, artist, and lyrics/queue actions. - Progress bar - Delay the timeline and time labels until expansion completes. - Playback controls - Delay play/pause, seek, and favorite controls. - All delayed components are active. Disable \"Delay everything\" to customize each part. - Step 2 · Configure placeholder behavior - Use placeholders for delayed items - Keep layout stable by rendering lightweight placeholders while components wait for expansion. - Step 3 · Choose when placeholders switch to real content - Select one mode. Threshold mode uses sliders; Drag release mode waits until you release the sheet gesture. - Enable at least one delayed component to unlock trigger mode. - Threshold - Uses expansion percentage. - Drag release - Switches only after gesture release. - Expand threshold - How expanded the sheet must be before delayed components become visible. - Content appears at %1$d%% expansion - Also apply on player close - Use close threshold to switch back to placeholders while collapsing. - Close threshold - How much collapse is required before placeholders take over again. - Placeholders appear after %1$d%% collapse - Drag release mode bypasses thresholds and close behavior. The swap happens only when the sheet drag gesture ends. - Make placeholders transparent - Placeholders keep their layout space but become invisible. - Visual Quality - Album Art Resolution - Low (256px) - Better performance - Medium (512px) - Balanced - High (800px) - Best quality - Original - Maximum quality + Bước 1 · Chọn những gì để trì hoãn + Trì hoãn mọi thứ + Giữ toàn bộ nội dung trình phát cho đến khi nền sheet được mở rộng hoàn toàn. + Carousel album + Trì hoãn ảnh album và carousel cho đến khi sheet được mở rộng. + Metadata bài hát + Trì hoãn tiêu đề, nghệ sĩ và hành động lời bài hát/hàng đợi. + Thanh tiến trình + Trì hoãn đường thời gian và nhãn thời gian cho đến khi mở rộng hoàn tất. + Điều khiển phát lại + Trì hoãn phát/tạm dừng, tìm nhanh và điều khiển yêu thích. + Tất cả thành phần bị trì hoãn đều đang hoạt động. Tắt \"Trì hoãn mọi thứ\" để tùy chỉnh từng phần. + Bước 2 · Cấu hình hành vi placeholder + Sử dụng placeholder cho các mục bị trì hoãn + Giữ bố cục ổn định bằng cách kết xuất placeholder nhẹ trong khi các thành phần chờ mở rộng. + Bước 3 · Chọn khi nào placeholder chuyển sang nội dung thực + Chọn một chế độ. Chế độ ngưỡng sử dụng thanh trượt; chế độ Thả kéo chờ cho đến khi bạn nhả cử chỉ sheet. + Bật ít nhất một thành phần bị trì hoãn để mở khóa chế độ kích hoạt. + Ngưỡng + Sử dụng phần trăm mở rộng. + Thả kéo + Chỉ chuyển đổi sau khi nhả cử chỉ. + Ngưỡng mở rộng + Sheet phải mở rộng bao nhiêu trước khi các thành phần bị trì hoãn hiển thị. + Nội dung xuất hiện ở %1$d%% mở rộng + Cũng áp dụng khi đóng trình phát + Sử dụng ngưỡng đóng để chuyển sang placeholder trong khi thu gọn. + Ngưỡng đóng + Cần thu gọn bao nhiêu trước khi placeholder tiếp quản lại. + Placeholder xuất hiện sau %1$d%% thu gọn + Chế độ thả kéo bỏ qua ngưỡng và hành vi đóng. Sự hoán đổi chỉ xảy ra khi cử chỉ kéo sheet kết thúc. + Làm placeholder trong suốt + Placeholder giữ không gian bố cục nhưng trở nên vô hình. + Chất lượng hình ảnh + Độ phân giải ảnh album + Thấp (256px) - Hiệu suất tốt hơn + Trung bình (512px) - Cân bằng + Cao (800px) - Chất lượng tốt nhất + Gốc - Chất lượng tối đa - Playback needs review - Ready for playback + Phát lại cần xem xét + Sẵn sàng phát lại -- - Formats - HW decoders - Local songs - Local Music Storage - Music size - %1$d local songs - Available - %1$s total - Music footprint - Device used + Định dạng + Bộ giải mã HW + Bài hát cục bộ + Lưu trữ nhạc cục bộ + Kích thước nhạc + %1$d bài hát cục bộ + Khả dụng + %1$s tổng cộng + Dung lượng nhạc + Thiết bị đã dùng %1$d%% <1% - %1$d cloud songs - %1$d files not readable - Playback Path - Yes - No - Sample Rate + %1$d bài hát cloud + %1$d file không thể đọc + Đường dẫn phát lại + + Không + Tần số mẫu %1$d Hz - %1$d frames per buffer + %1$d khung mỗi bộ đệm Hi-Fi PCM Float - 32-bit float output path - Low Latency Support - Pro Audio Support - Memory - available of %1$s - Offload-ready formats - No compressed format reported hardware offload support. - +%1$d more - Detected outputs - Built-in output - Bluetooth audio - USB audio - Wired audio - Digital output - Other output - No output routes were reported by Android. - ExoPlayer Engine - %1$s renderers - Format Compatibility - %1$d supported tracks - %1$d unknown format - No decoder reported - Hardware decoder - Software decoder + Đường đầu ra float 32-bit + Hỗ trợ độ trễ thấp + Hỗ trợ Pro Audio + Bộ nhớ + khả dụng trong %1$s + Định dạng sẵn sàng offload + Không có định dạng nén nào báo cáo hỗ trợ offload phần cứng. + +%1$d thêm + Đầu ra được phát hiện + Đầu ra tích hợp + Âm thanh Bluetooth + Âm thanh USB + Âm thanh có dây + Đầu ra kỹ thuật số + Đầu ra khác + Không có tuyến đầu ra nào được Android báo cáo. + Trình phát ExoPlayer + %1$s bộ kết xuất + Tương thích định dạng + %1$d bài hát được hỗ trợ + %1$d định dạng không xác định + Không có bộ giải mã nào được báo cáo + Bộ giải mã phần cứng + Bộ giải mã phần mềm Offload - %1$d in library - Compatibility Findings - No major incompatibilities - Your indexed tracks match the decoders Android reports on this device. - %1$d tracks may not decode natively - Formats to review: %1$s. - %1$d local tracks may be resampled - The library reaches %1$d Hz, above the current output sample rate. - %1$d tracks have unknown metadata - A full library rescan can fill missing MIME, bitrate, and sample-rate data. - Device Info - Manufacturer - Model - Brand - Device - Android Version - SDK Version - Hardware - Performance report - Generate a shareable diagnostic report to help us classify playback or scan lag. It contains only device, library, and timing data — no file paths, titles, or artists. - Generate report - Regenerate - Copy - Share - Report copied to clipboard - PixelPlayer performance report - Advanced performance diagnostics - Off by default. Records a short lag timeline for beta troubleshooting. - Active until %1$s - Mark lag now - Lag moment marked + %1$d trong thư viện + Kết quả tương thích + Không có bất tương thích lớn + Các bài hát đã lập chỉ mục của bạn khớp với các bộ giải mã Android báo cáo trên thiết bị này. + %1$d bài hát có thể không giải mã gốc + Định dạng cần xem xét: %1$s. + %1$d bài hát cục bộ có thể được lấy mẫu lại + Thư viện đạt %1$d Hz, cao hơn tần số mẫu đầu ra hiện tại. + %1$d bài hát có metadata không xác định + Một lần quét lại thư viện đầy đủ có thể điền dữ liệu MIME, bitrate và tần số mẫu bị thiếu. + Thông tin thiết bị + Nhà sản xuất + Mẫu + Thương hiệu + Thiết bị + Phiên bản Android + Phiên bản SDK + Phần cứng + Báo cáo hiệu suất + Tạo báo cáo chẩn đoán có thể chia sẻ để giúp chúng tôi phân loại lỗi phát lại hoặc quét chậm. Nó chỉ chứa dữ liệu thiết bị, thư viện và thời gian — không có đường dẫn file, tiêu đề hay nghệ sĩ. + Tạo báo cáo + Tạo lại + Sao chép + Chia sẻ + Đã sao chép báo cáo vào clipboard + Báo cáo hiệu suất PixelPlayer + Chẩn đoán hiệu suất nâng cao + Tắt theo mặc định. Ghi lại dòng thời gian lỗi ngắn để sửa lỗi beta. + Hoạt động cho đến %1$s + Đánh dấu lỗi ngay + Đã đánh dấu thời điểm lỗi - Connected Accounts - Manage linked providers and keep each integration under your control. - Linked Services - Active - Available - Soon - Connected - Coming soon - Open Service - Logging out… - No linked accounts yet - Connect a provider to manage it from this screen. - Connect %1$s - %1$s (Coming soon) - Google Drive is coming soon. - Unable to open this screen right now. + Tài khoản đã kết nối + Quản lý các nhà cung cấp đã liên kết và giữ mỗi tích hợp dưới sự kiểm soát của bạn. + Dịch vụ đã liên kết + Đang hoạt động + Khả dụng + Sắp ra mắt + Đã kết nối + Sắp ra mắt + Mở dịch vụ + Đang đăng xuất… + Chưa có tài khoản liên kết + Kết nối một nhà cung cấp để quản lý từ màn hình này. + Kết nối %1$s + %1$s (Sắp ra mắt) + Google Drive sắp ra mắt. + Không thể mở màn hình này lúc này. - About + Giới thiệu PixelPlayer - Open source music player built with its community. - Version v%1$s - Open source - Community-first - Material 3 expressive - No contributors found right now. Please try again later. - Maintainer - The person behind PixelPlayer. - Community spotlight - Recognition for collaborators with major impact. - Open source contributors - Live contributor list from GitHub. - %1$d contrib. + Trình phát nhạc mã nguồn mở được xây dựng cùng cộng đồng. + Phiên bản v%1$s + Mã nguồn mở + Cộng đồng ưu tiên + Material 3 phóng khoáng + Không tìm thấy đóng góp nào lúc này. Vui lòng thử lại sau. + Người duy trì + Người đứng sau PixelPlayer. + Điểm nhấn cộng đồng + Ghi nhận những cộng tác viên có tác động lớn. + Giấy phép và thông báo + Ghi nhận cho các phụ thuộc và đóng góp trước đó. + Giấy phép mã nguồn mở + Giấy phép phụ thuộc và thông báo bên thứ ba + Giấy phép mã nguồn mở + Thông báo kiểu MIT + Thông báo bên thứ ba + Đóng + Không thể tải thông báo bên thứ ba được đóng gói. + Đóng góp mã nguồn mở + Danh sách đóng góp trực tuyến từ GitHub. + %1$d đóng góp. GitHub - Repository + Kho lưu trữ Telegram - Support - Open GitHub repository - Join Telegram community - Open GitHub profile - Open Telegram - Avatar of %1$s - Icon of %1$s - Volume - Pause when volume reaches zero - Automatically pause playback when the volume is set to 0 + Hỗ trợ + Mở kho lưu trữ GitHub + Tham gia cộng đồng Telegram + Mở hồ sơ GitHub + Mở Telegram + Ảnh đại diện của %1$s + Biểu tượng của %1$s + Âm lượng + Tạm dừng khi âm lượng về không + Tự động tạm dừng phát lại khi âm lượng được đặt ở 0 diff --git a/app/src/main/res/values-vi/strings_widget.xml b/app/src/main/res/values-vi/strings_widget.xml index 5dd2eabafc..b8059dc969 100644 --- a/app/src/main/res/values-vi/strings_widget.xml +++ b/app/src/main/res/values-vi/strings_widget.xml @@ -1,17 +1,17 @@ - Responsive widget that adapts to its size - Compact player bar - Full controls with shuffle and repeat - Minimalist square player + Widget thích ứng với kích thước + Thanh trình phát thu gọn + Điều khiển đầy đủ với phát ngẫu nhiên và lặp lại + Trình phát hình vuông tối giản - Tap to open - Album art - Album art placeholder + Nhấn để mở + Ảnh bìa album + Placeholder ảnh bìa album - Tap to play - Song title - Artist + Nhấn để phát + Tên bài hát + Nghệ sĩ - Progress bar, %1$d percent - \ No newline at end of file + Thanh tiến trình, %1$d phần trăm + diff --git a/app/src/main/res/values-zh-rCN/strings_settings.xml b/app/src/main/res/values-zh-rCN/strings_settings.xml index 07496a3129..6849662a7e 100644 --- a/app/src/main/res/values-zh-rCN/strings_settings.xml +++ b/app/src/main/res/values-zh-rCN/strings_settings.xml @@ -176,6 +176,9 @@ 韩语 挪威语(Bokmål) 土耳其语 + 日语 + 繁体中文 + 越南语 应用主题 在浅色、深色之间切换,或跟随系统外观。 浅色主题 diff --git a/app/src/main/res/values-zh-rTW/plurals.xml b/app/src/main/res/values-zh-rTW/plurals.xml new file mode 100644 index 0000000000..a1bf52e6e2 --- /dev/null +++ b/app/src/main/res/values-zh-rTW/plurals.xml @@ -0,0 +1,45 @@ + + + + 正在分享 %d 個播放清單 + + + 已將 %1$d 個播放清單匯出至 %2$s + + + 已將 %d 首歌曲加入佇列 + + + %d 首歌曲將接著播放 + + + 已將 %d 首歌曲加入最愛 + + + 已將 %d 首歌曲從最愛移除 + + + 已刪除 %d 個檔案 + + + 刪除 %d 個檔案? + + + 已選擇 %d 首歌曲 + + + %1$d 首歌曲 • %2$s + + + %d 首歌曲已排入佇列。 + + + 已選擇 %d 首歌曲 + + + %d 首歌曲 + + + %d 個主題 + + diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index 7f38b9115f..0fca03d251 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -1,128 +1,128 @@ PixelPlayer - 音乐播放器 + 音樂播放器 應用名稱更改 由於商標相關問題,我們將應用名稱從 PixelPlay 更改為 PixelPlayer。請繼續享受音樂! 不再顯示 - 首页 + 首頁 搜索 - 音乐库 + 音樂庫 - 需要特殊权限 - 为了编辑歌曲信息(.mp3 文件),PixelPlayer 需要访问所有文件的特殊权限。这将允许我们直接修改曲目标签。请在下一界面授予此权限以开启信息编辑功能。 - 授予权限 + 需要特殊權限 + 爲了編輯歌曲信息(.mp3 文件),PixelPlayer 需要訪問所有文件的特殊權限。這將允許我們直接修改曲目標籤。請在下一界面授予此權限以開啓信息編輯功能。 + 授予權限 快速播放 - 无法打开该音频文件。 - 打开完整播放器 + 無法打開該音頻文件。 + 打開完整播放器 - 随机播放 - 随机播放所有歌曲 - 全部随机播放 + 隨機播放 + 隨機播放所有歌曲 + 全部隨機播放 最近播放列表 - 没有可打开的播放列表 + 沒有可打開的播放列表 - 打开 Play 商店 - 继续使用 Beta 版 - Play 商店链接将通过 GitHub 配置启用。 - PixelPlayer 现已上线 Google Play - 请在 Google Play 上使用稳定版以获取正式更新,同时我们仍会保持 Beta 测试版本的更新。 + 打開 Play 商店 + 繼續使用 Beta 版 + Play 商店鏈接將通過 GitHub 配置啓用。 + PixelPlayer 現已上線 Google Play + 請在 Google Play 上使用穩定版以獲取正式更新,同時我們仍會保持 Beta 測試版本的更新。 PixelPlayer - 发布公告 - 敬请期待 + 發佈公告 + 敬請期待 - 感谢您使用 PixelPlayer! + 感謝您使用 PixelPlayer! 最高分 %1$d - 关闭 - 分数 - 第 %1$d 关 + 關閉 + 分數 + 第 %1$d 關 生命 - 关卡完成! - 游戏结束 - 分数:%1$d - 再试一次? - 下一关 - 重新开始游戏 - 点击重新启动 - 随机播放音乐 - 打砖块 + 關卡完成! + 遊戲結束 + 分數:%1$d + 再試一次? + 下一關 + 重新開始遊戲 + 點擊重新啓動 + 隨機播放音樂 + 打磚塊 最高分 %1$d - 开始 - 拖动以移动挡板 + 開始 + 拖動以移動擋板 - 关闭播放器 - 正在处理播放操作… - 播放错误:%1$s + 關閉播放器 + 正在處理播放操作… + 播放錯誤:%1$s 返回 - 确定 + 確定 取消 - 关闭 - 错误 + 關閉 + 錯誤 搜索 清除搜索 全部 - 确认 + 確認 已保存! - 已选择 + 已選擇 %1$d%% - 艺术家 - 全选 + 藝術家 + 全選 清除 - 未知错误 + 未知錯誤 保存 完成 重置 - 应用 - 随机播放 - 复制 + 應用 + 隨機播放 + 複製 分享 - 撤销 - 导入 - 删除 - 导出 - 合并 + 撤銷 + 導入 + 刪除 + 導出 + 合併 重命名 - 创建 - 歌词 - 设置 - 专辑封面 + 創建 + 歌詞 + 設置 + 專輯封面 播放列表 未知曲目 - 未知艺术家 - 未知专辑 - 关闭 + 未知藝術家 + 未知專輯 + 關閉 添加 移除 播放 上一首 下一首 收藏 - 暂停 - 循环播放 - 选项 - 随机播放 - %1$s 的更多选项 - 展开菜单 + 暫停 + 循環播放 + 選項 + 隨機播放 + %1$s 的更多選項 + 展開菜單 下一步 - 结束 - 恢复默认设置 - 导出全部 - 合并全部 + 結束 + 恢復默認設置 + 導出全部 + 合併全部 分享全部 - 播放专辑 - 随机播放专辑 - %1$s 的专辑封面 + 播放專輯 + 隨機播放專輯 + %1$s 的專輯封面 \ No newline at end of file diff --git a/app/src/main/res/values-zh-rTW/strings_changelogs.xml b/app/src/main/res/values-zh-rTW/strings_changelogs.xml index b0b82e4a30..6055745830 100644 --- a/app/src/main/res/values-zh-rTW/strings_changelogs.xml +++ b/app/src/main/res/values-zh-rTW/strings_changelogs.xml @@ -1,132 +1,132 @@ - 更新日志 + 更新日誌 在 GitHub 上查看 - 改进 - 修复 + 改進 + 修復 新功能 新增 - 支持 Chromecast 从您的设备投射音频。 - 应用内更新日志,让您随时了解最新功能。 - 支持嵌入式和外部的 .LRC 歌词文件。 - 支持离线歌词。 - 同步歌词(与歌曲同步)。 - 查看完整队列的新界面。 - 支持在队列中重新排序和移除歌曲。 - 迷你播放器手势(向下滑动关闭)。 - 添加了更多 Material 动画。 - 新增设置项以自定义外观。 - 新增设置项以清除缓存。 + 支持 Chromecast 從您的設備投射音頻。 + 應用內更新日誌,讓您隨時瞭解最新功能。 + 支持嵌入式和外部的 .LRC 歌詞文件。 + 支持離線歌詞。 + 同步歌詞(與歌曲同步)。 + 查看完整隊列的新界面。 + 支持在隊列中重新排序和移除歌曲。 + 迷你播放器手勢(向下滑動關閉)。 + 添加了更多 Material 動畫。 + 新增設置項以自定義外觀。 + 新增設置項以清除緩存。 - 用户界面的全面重新设计。 - 播放器的全面重新设计。 - 媒体库性能改进。 - 提升了应用的启动速度。 - AI 现在能提供更好的结果。 + 用戶界面的全面重新設計。 + 播放器的全面重新設計。 + 媒體庫性能改進。 + 提升了應用的啓動速度。 + AI 現在能提供更好的結果。 - 修复了标签编辑器中的各种错误。 - 修复了播放通知未被清除的错误。 - 修复了导致应用崩溃的几个错误。 + 修復了標籤編輯器中的各種錯誤。 + 修復了播放通知未被清除的錯誤。 + 修復了導致應用崩潰的幾個錯誤。 - 引入了更丰富的听歌统计中心,可深入了解您的播放会话。 - 推出了悬浮快速播放器,可立即打开并预览本地文件。 - 添加了包含树状导航器 and “播放列表就绪”视图的文件夹标签页。 + 引入了更豐富的聽歌統計中心,可深入瞭解您的播放會話。 + 推出了懸浮快速播放器,可立即打開並預覽本地文件。 + 添加了包含樹狀導航器 and “播放列表就緒”視圖的文件夾標籤頁。 - 改进了整体 Material 3 UI,带来更简洁、更连贯的体验。 - 元数据编辑现已支持更改封面。 - 优化了应用内的动画和过渡效果,使导航更加流畅。 - 增强了艺术家界面的布局,细节更丰富且更精美。 - 升级了 DailyMix 和 YourMix 的生成,选择更加智能且多样化。 - 强化了 AI 播放列表的生成能力。 - 改进了搜索相关性和展示方式,以便更快地发现内容。 - 扩大了对更多音频文件格式的支持。 + 改進了整體 Material 3 UI,帶來更簡潔、更連貫的體驗。 + 元數據編輯現已支持更改封面。 + 優化了應用內的動畫和過渡效果,使導航更加流暢。 + 增強了藝術家界面的佈局,細節更豐富且更精美。 + 升級了 DailyMix 和 YourMix 的生成,選擇更加智能且多樣化。 + 強化了 AI 播放列表的生成能力。 + 改進了搜索相關性和展示方式,以便更快地發現內容。 + 擴大了對更多音頻文件格式的支持。 - 解决了元数据异常问题,确保各处的歌曲详情保持准确。 - 恢复了通知快捷操作,以便可靠地跳转回播放界面。 + 解決了元數據異常問題,確保各處的歌曲詳情保持準確。 + 恢復了通知快捷操作,以便可靠地跳轉回播放界面。 - 导航栏重大重新设计 - 用于选择源目录的全新文件管理器 - 全新的连接与投屏功能 - 远程设备之间的无缝流转 - 歌曲间无缝过渡 + 導航欄重大重新設計 + 用於選擇源目錄的全新文件管理器 + 全新的連接與投屏功能 + 遠程設備之間的無縫流轉 + 歌曲間無縫過渡 淡入淡出控制 - 全新的自定义过渡功能(仅限播放列表) - 关闭应用后继续播放 - UI 优化 - 统计功能改进 - 重新设计并增加了更多功能的队列控制 - 改进了对不同文件格式的播放和元数据编辑支持 - 改进的权限控制器 - 修复了一些小错误 + 全新的自定義過渡功能(僅限播放列表) + 關閉應用後繼續播放 + UI 優化 + 統計功能改進 + 重新設計並增加了更多功能的隊列控制 + 改進了對不同文件格式的播放和元數據編輯支持 + 改進的權限控制器 + 修復了一些小錯誤 - Material 3 Expressive UI 全面升级 - 10 段均衡器与音效 - 全新的媒体库同步流程 + Material 3 Expressive UI 全面升級 + 10 段均衡器與音效 + 全新的媒體庫同步流程 AI 集成(Gemini 模型) - M3U 播放列表导入/导出 - 集成 Deezer 艺术家封面 - 自定义播放列表封面 - 设置架构重构 - 队列与播放器动画 - 基线配置文件 (Baseline Profiles) 与性能优化 - 具有同步偏移调整功能的更佳歌词系统 + M3U 播放列表導入/導出 + 集成 Deezer 藝術家封面 + 自定義播放列表封面 + 設置架構重構 + 隊列與播放器動畫 + 基線配置文件 (Baseline Profiles) 與性能優化 + 具有同步偏移調整功能的更佳歌詞系統 - 投屏稳定性改进 - 播放面板稳定性 - 常规错误修复与代码清理 + 投屏穩定性改進 + 播放面板穩定性 + 常規錯誤修復與代碼清理 - 现已支持 Android Auto 车载播放。 - 已支持 Wear OS,包括更完善的手表对手机播放控制。 - 扩展了云端集成,改进了对 Telegram、网易云音乐、QQ 音乐和 Google Drive 的支持。 - “最近播放”和持久化队列恢复功能,让您的听歌会话随时就绪。 - 现已包含备份与恢复 v3 以及账号管理工具。 - 歌词功能更加智能,增加了手动搜索备用方案和存储优化。 + 現已支持 Android Auto 車載播放。 + 已支持 Wear OS,包括更完善的手錶對手機播放控制。 + 擴展了雲端集成,改進了對 Telegram、網易雲音樂、QQ 音樂和 Google Drive 的支持。 + “最近播放”和持久化隊列恢復功能,讓您的聽歌會話隨時就緒。 + 現已包含備份與恢復 v3 以及賬號管理工具。 + 歌詞功能更加智能,增加了手動搜索備用方案和存儲優化。 - 大幅优化了启动、媒体库、队列和播放器交互的性能。 - 重新设计了播放器、投屏、歌词、艺术家和流派界面,带来更流畅的体验。 - 导航和搜索流程更加可靠,路由处理更加安全。 - 提升了音频播放在更多设备 and 格式上的兼容性。 - 扩展了歌曲、专辑和播放列表的多选操作流程。 + 大幅優化了啓動、媒體庫、隊列和播放器交互的性能。 + 重新設計了播放器、投屏、歌詞、藝術家和流派界面,帶來更流暢的體驗。 + 導航和搜索流程更加可靠,路由處理更加安全。 + 提升了音頻播放在更多設備 and 格式上的兼容性。 + 擴展了歌曲、專輯和播放列表的多選操作流程。 - 队列和随机播放行为现已更加稳定且符合预期。 - 修复了几个后台播放和投屏的边缘情况。 - 修复了睡眠定时器、文件标签页导航以及导致专辑艺术家崩溃的问题。 - 改善了小组件加载和服务稳定性,减少了设备发热/内存问题。 - 全局的常规错误修复和界面细节打磨。 + 隊列和隨機播放行爲現已更加穩定且符合預期。 + 修復了幾個後臺播放和投屏的邊緣情況。 + 修復了睡眠定時器、文件標籤頁導航以及導致專輯藝術家崩潰的問題。 + 改善了小組件加載和服務穩定性,減少了設備發熱/內存問題。 + 全局的常規錯誤修復和界面細節打磨。 - Wear OS:支持音乐传输、本地播放、队列同步以及从手表进行远程控制。 - AI:集成了 Groq AI 和 OpenRouter(实验性),并进行了 Token 优化。 - 云端:新增了 Jellyfin 支持。 - 歌词:带有专属开关的同步翻译,支持酷狗 LRC 格式,自定义文本对齐方式,以及改进了远程加载。 - UI/UX:紧凑的导航栏模式,从专辑封面提取调色板的动态主题,长标题跑马灯效果,以及全新的排序选项。 - Telegram:原生话题 (Topics) 支持和增强的显示模式。 + Wear OS:支持音樂傳輸、本地播放、隊列同步以及從手錶進行遠程控制。 + AI:集成了 Groq AI 和 OpenRouter(實驗性),並進行了 Token 優化。 + 雲端:新增了 Jellyfin 支持。 + 歌詞:帶有專屬開關的同步翻譯,支持酷狗 LRC 格式,自定義文本對齊方式,以及改進了遠程加載。 + UI/UX:緊湊的導航欄模式,從專輯封面提取調色板的動態主題,長標題跑馬燈效果,以及全新的排序選項。 + Telegram:原生話題 (Topics) 支持和增強的顯示模式。 - 音频引擎:全面重构,支持更多格式(MIDI、ALAC、M4A)并进行了解码器优化。 - 效率:大幅降低能耗,修复设备发热问题,以及后台任务优化 (SyncWorker)。 - 数据库:大规模的查询优化,并重新设计了封面缓存以防止数据丢失。 - 启动:通过基线配置文件 (Baseline Profile) 优化了加载时间。 + 音頻引擎:全面重構,支持更多格式(MIDI、ALAC、M4A)並進行了解碼器優化。 + 效率:大幅降低能耗,修復設備發熱問題,以及後臺任務優化 (SyncWorker)。 + 數據庫:大規模的查詢優化,並重新設計了封面緩存以防止數據丟失。 + 啓動:通過基線配置文件 (Baseline Profile) 優化了加載時間。 - 播放:修复了 Opus/MP3 的卡顿、淡入淡出期间的 ReplayGain 错误,以及三星解码器上的启动问题。 - 稳定性:消除了启动、艺术家导航和 Android 12+ 设备上的崩溃问题。 - UI:修复了封面闪烁、非拉丁语系文本溢出,以及导航栏/迷你播放器的行为问题。 - 安全性:强化了凭证处理、存储权限以及与媒体服务器的通信安全性。 + 播放:修復了 Opus/MP3 的卡頓、淡入淡出期間的 ReplayGain 錯誤,以及三星解碼器上的啓動問題。 + 穩定性:消除了啓動、藝術家導航和 Android 12+ 設備上的崩潰問題。 + UI:修復了封面閃爍、非拉丁語系文本溢出,以及導航欄/迷你播放器的行爲問題。 + 安全性:強化了憑證處理、存儲權限以及與媒體服務器的通信安全性。 - 本地化:西班牙语、法语、俄语、简体中文、印度尼西亚语、意大利语 + 本地化:西班牙語、法語、俄語、簡體中文、印度尼西亞語、意大利語 \ No newline at end of file diff --git a/app/src/main/res/values-zh-rTW/strings_cloud_services.xml b/app/src/main/res/values-zh-rTW/strings_cloud_services.xml index 13dd260f70..189b9eeebf 100644 --- a/app/src/main/res/values-zh-rTW/strings_cloud_services.xml +++ b/app/src/main/res/values-zh-rTW/strings_cloud_services.xml @@ -1,228 +1,228 @@ - 登录 Telegram - 您正在修改号码。再次发送验证码将替换上一个。 - 处理中… + 登錄 Telegram + 您正在修改號碼。再次發送驗證碼將替換上一個。 + 處理中… 正在初始化 Telegram… 正在登出… - 正在关闭会话… - 会话已关闭。请重新打开登录界面以继续。 - 正在准备安全的 Telegram 会话… - 正在等待 Telegram 响应… - 连接 Telegram - 连接 Telegram 即可流式播放您频道和聊天中的音乐。 - 手机号码 - 请输入您的 Telegram 手机号码。您稍后可以返回并进行修改。 - 手机号码 + 正在關閉會話… + 會話已關閉。請重新打開登錄界面以繼續。 + 正在準備安全的 Telegram 會話… + 正在等待 Telegram 響應… + 連接 Telegram + 連接 Telegram 即可流式播放您頻道和聊天中的音樂。 + 手機號碼 + 請輸入您的 Telegram 手機號碼。您稍後可以返回並進行修改。 + 手機號碼 86 13800138000 - 发送验证码 - 验证码 - 请输入来自 Telegram 的验证码。如果号码有误,请返回并修改。 - 验证码 + 發送驗證碼 + 驗證碼 + 請輸入來自 Telegram 的驗證碼。如果號碼有誤,請返回並修改。 + 驗證碼 12345 - 修改手机号 - 重新发送验证码 - 验证验证码 - 两步验证密码 - 请输入您的 Telegram 密码。您仍然可以返回以修改手机号码。 - 密码 - 验证密码 - 请稍候… + 修改手機號 + 重新發送驗證碼 + 驗證驗證碼 + 兩步驗證密碼 + 請輸入您的 Telegram 密碼。您仍然可以返回以修改手機號碼。 + 密碼 + 驗證密碼 + 請稍候… - Telegram 频道 - 添加频道 - 公开的 Telegram 频道 + Telegram 頻道 + 添加頻道 + 公開的 Telegram 頻道 正在同步 立即同步 - 收起话题 - 显示话题 - 频道选项 - 话题 - 正在同步频道 - 正在从 Telegram 更新歌曲 - 从此频道获取最新歌曲 - 移除频道 - 停止同步并移除已缓存的歌曲 - 是否移除频道? - %1$s 将停止同步,并且从此频道缓存的所有歌曲都将被移除。 + 收起話題 + 顯示話題 + 頻道選項 + 話題 + 正在同步頻道 + 正在從 Telegram 更新歌曲 + 從此頻道獲取最新歌曲 + 移除頻道 + 停止同步並移除已緩存的歌曲 + 是否移除頻道? + %1$s 將停止同步,並且從此頻道緩存的所有歌曲都將被移除。 移除 - 未同步任何频道 - 添加公开的 Telegram 频道\n以同步您的音乐库 - 添加频道 - 从未同步 - 上次同步于 %1$s + 未同步任何頻道 + 添加公開的 Telegram 頻道\n以同步您的音樂庫 + 添加頻道 + 從未同步 + 上次同步於 %1$s - 添加频道 - 搜索一个公开的 Telegram 频道以同步其音乐 - \@channelname 或链接 + 添加頻道 + 搜索一個公開的 Telegram 頻道以同步其音樂 + \@channelname 或鏈接 正在搜索… - 搜索频道 - 输入公开频道的用户名或链接\n以同步其音频文件 + 搜索頻道 + 輸入公開頻道的用戶名或鏈接\n以同步其音頻文件 %d 首歌曲 %d 首歌曲 - %d 个话题 - %d 个话题 + %d 個話題 + %d 個話題 Subsonic - 管理 Navidrome、Airsonic 以及其他兼容 Subsonic 的服务器。 + 管理 Navidrome、Airsonic 以及其他兼容 Subsonic 的服務器。 - 点击同步以获取您的 Jellyfin 播放列表 - 管理您的 Jellyfin 服务器连接。 + 點擊同步以獲取您的 Jellyfin 播放列表 + 管理您的 Jellyfin 服務器連接。 - 音乐文件夹 - 点击 + 添加 Drive 文件夹 - 暂未添加文件夹 - 已同步 %1$d 个文件夹 - 添加文件夹 + 音樂文件夾 + 點擊 + 添加 Drive 文件夾 + 暫未添加文件夾 + 已同步 %1$d 個文件夾 + 添加文件夾 - 选择播放列表类型 - 选择要同步的播放列表: + 選擇播放列表類型 + 選擇要同步的播放列表: 所有播放列表 - 自建与收藏 + 自建與收藏 自建播放列表 收藏播放列表 - 已同步 %1$d 个播放列表 + 已同步 %1$d 個播放列表 播放列表 同步 - 暂未同步任何播放列表 - 点击同步以获取您的播放列表 + 暫未同步任何播放列表 + 點擊同步以獲取您的播放列表 快捷操作 - 同步媒体库 - 断开连接 + 同步媒體庫 + 斷開連接 %1$d 首歌曲 正在同步 - 正在同步媒体库… - 正在获取播放列表… + 正在同步媒體庫… + 正在獲取播放列表… 正在同步播放列表:%1$s - 正在更新本地媒体库… + 正在更新本地媒體庫… 同步完成 - 正在获取专辑列表… - 正在从 %1$s 获取歌曲… - 正在将 %1$d 首歌曲保存到数据库… - 未找到媒体库歌曲 - 媒体库同步完成 + 正在獲取專輯列表… + 正在從 %1$s 獲取歌曲… + 正在將 %1$d 首歌曲保存到數據庫… + 未找到媒體庫歌曲 + 媒體庫同步完成 正在同步… - 错误:%1$s + 錯誤:%1$s 同步 全部同步 登出 同步所有播放列表 - 用户头像 + 用戶頭像 - 无网络连接 - 此内容需要网络连接。请检查您的网络设置并重试。 - 您已离线 - 请检查您的网络连接并重试以访问此内容。 + 無網絡連接 + 此內容需要網絡連接。請檢查您的網絡設置並重試。 + 您已離線 + 請檢查您的網絡連接並重試以訪問此內容。 - 连接 - 正在连接… - 输入您的服务器地址和账户凭据。 - 连接详情 - 隐藏密码 - 密码 - 输入密码 - 预填 http:// - 服务器 URL - 显示密码 + 連接 + 正在連接… + 輸入您的服務器地址和賬戶憑據。 + 連接詳情 + 隱藏密碼 + 密碼 + 輸入密碼 + 預填 http:// + 服務器 URL + 顯示密碼 Telegram - 用户名 + 用戶名 admin - 欢迎您,%1$s! + 歡迎您,%1$s! - 兼容 Navidrome、Gonic、Airsonic 以及其他兼容 Subsonic 的服务器 - 支持 Navidrome、Airsonic、Gonic、Ampache 以及其他兼容 Subsonic API 的服务器。 - 如果您的服务器支持,应用密码同样有效。 - 预填 https:// - 连接到您的自托管音乐服务器 + 兼容 Navidrome、Gonic、Airsonic 以及其他兼容 Subsonic 的服務器 + 支持 Navidrome、Airsonic、Gonic、Ampache 以及其他兼容 Subsonic API 的服務器。 + 如果您的服務器支持,應用密碼同樣有效。 + 預填 https:// + 連接到您的自託管音樂服務器 Navidrome - 请使用您服务器的完整 https:// 基础地址。 + 請使用您服務器的完整 https:// 基礎地址。 https://music.example.com - 这是您的 Subsonic 或 Navidrome 账户名称。 + 這是您的 Subsonic 或 Navidrome 賬戶名稱。 Subsonic / Navidrome Subsonic - 输入您的 Jellyfin 服务器 URL 和账户凭据。 - 连接到 Jellyfin 服务器以串流播放您的媒体库 - 连接到 Jellyfin 服务器。支持使用 HTTP 和 HTTPS 进行本地网络访问。 + 輸入您的 Jellyfin 服務器 URL 和賬戶憑據。 + 連接到 Jellyfin 服務器以串流播放您的媒體庫 + 連接到 Jellyfin 服務器。支持使用 HTTP 和 HTTPS 進行本地網絡訪問。 Jellyfin - 您的 Jellyfin 账户密码。 - 连接到您的 Jellyfin 媒体服务器 + 您的 Jellyfin 賬戶密碼。 + 連接到您的 Jellyfin 媒體服務器 Jellyfin - 您的 Jellyfin 服务器完整 URL,包含端口。 + 您的 Jellyfin 服務器完整 URL,包含端口。 http://192.168.1.100:8096 - 您的 Jellyfin 账户用户名。 + 您的 Jellyfin 賬戶用戶名。 - 直接从您的 Google Drive 串流音乐文件 - 连接 Google Drive - Google Drive 已连接! - 创建 \"PixelPlayer Music\" - 在这里为您的音乐创建一个新文件夹 - 这里没有文件夹 - 打开文件夹 - 选择或创建一个文件夹作为您的音乐源 - 选择一个音乐文件夹 - 正在设置 Google Drive… - 使用 Google 账号登录 + 直接從您的 Google Drive 串流音樂文件 + 連接 Google Drive + Google Drive 已連接! + 創建 \"PixelPlayer Music\" + 在這裏爲您的音樂創建一個新文件夾 + 這裏沒有文件夾 + 打開文件夾 + 選擇或創建一個文件夾作爲您的音樂源 + 選擇一個音樂文件夾 + 正在設置 Google Drive… + 使用 Google 賬號登錄 Google Drive 使用 - 无法读取会话 Cookie。 + 無法讀取會話 Cookie。 完成 退出 - 未找到 Cookie。请先登录。 - 页面加载时间过长。请刷新或尝试其他网络。 + 未找到 Cookie。請先登錄。 + 頁面加載時間過長。請刷新或嘗試其他網絡。 + 正在保存… 停留 - 页面加载超时。您可以重试,进度不会丢失。 - 网页后退 - 您可以稍后返回。关闭时将丢弃当前页面状态。 - 网页前进 + 頁面加載超時。您可以重試,進度不會丟失。 + 網頁後退 + 您可以稍後返回。關閉時將丟棄當前頁面狀態。 + 網頁前進 刷新 - 重试 - 打开主页 - WebView 加载失败。 + 重試 + 打開主頁 + WebView 加載失敗。 - 读取网易云音乐 Cookie 失败:%1$s - 退出网易云音乐登录? - 加载网易云音乐时出现 HTTP %1$d。 - 尚未检测到登录。请在点击完成前完成网易云音乐的登录。 - 登录网易云音乐 - 安全提示:您的密码仅在网易云音乐网页中输入。PixelPlayer 仅存储会话 Cookie (MUSIC_U) 以同步您的媒体库。 - 网易云音乐 + 讀取網易雲音樂 Cookie 失敗:%1$s + 退出網易雲音樂登錄? + 加載網易雲音樂時出現 HTTP %1$d。 + 尚未檢測到登錄。請在點擊完成前完成網易雲音樂的登錄。 + 登錄網易雲音樂 + 安全提示:您的密碼僅在網易雲音樂網頁中輸入。PixelPlayer 僅存儲會話 Cookie (MUSIC_U) 以同步您的媒體庫。 + 網易雲音樂 - 读取 QQ 音乐 Cookie 失败:%1$s - 退出 QQ 音乐登录? - 加载 QQ 音乐时出现 HTTP %1$d。 - 尚未检测到登录。请在点击完成前完成 QQ 音乐的登录。 - 登录 QQ 音乐 - 安全提示:您的密码仅在 QQ 音乐网页中输入。PixelPlayer 仅存储会话 Cookie 以同步您的媒体库。 - QQ 音乐 + 讀取 QQ 音樂 Cookie 失敗:%1$s + 退出 QQ 音樂登錄? + 加載 QQ 音樂時出現 HTTP %1$d。 + 尚未檢測到登錄。請在點擊完成前完成 QQ 音樂的登錄。 + 登錄 QQ 音樂 + 安全提示:您的密碼僅在 QQ 音樂網頁中輸入。PixelPlayer 僅存儲會話 Cookie 以同步您的媒體庫。 + QQ 音樂 \ No newline at end of file diff --git a/app/src/main/res/values-zh-rTW/strings_equalizer.xml b/app/src/main/res/values-zh-rTW/strings_equalizer.xml index f1e662c938..7ca427ded4 100644 --- a/app/src/main/res/values-zh-rTW/strings_equalizer.xml +++ b/app/src/main/res/values-zh-rTW/strings_equalizer.xml @@ -1,57 +1,57 @@ - 名称不能为空 + 名稱不能爲空 重命名 - 更改视图模式 + 更改視圖模式 禁用均衡器 - 启用均衡器 - 编辑 - 编辑预设 - 自定义预设 - 预设 + 啓用均衡器 + 編輯 + 編輯預設 + 自定義預設 + 預設 更新 - 低音增强 - 虚拟音效 - 响度 + 低音增強 + 虛擬音效 + 響度 不支持 - 此设备不支持 + 此設備不支持 音量 - 频率响应 + 頻率響應 Hz 低音 中低音 中高音 高音 - 低音 / 低频 - 中音 / 高频 - 第 %1$d 页 - 重置持续时间 - 保存为新预设 + 低音 / 低頻 + 中音 / 高頻 + 第 %1$d 頁 + 重置持續時間 + 保存爲新預設 - 已保存的预设 - 暂未保存自定义预设。 + 已保存的預設 + 暫未保存自定義預設。 取消固定 固定 重命名 - 删除 + 刪除 - 保存自定义预设 - 为您的自定义均衡器预设输入一个名称。 - 预设名称 - 重命名预设 + 保存自定義預設 + 爲您的自定義均衡器預設輸入一個名稱。 + 預設名稱 + 重命名預設 - 管理预设 - 拖动以重新排序 • 点击眼睛图标显示或隐藏 + 管理預設 + 拖動以重新排序 • 點擊眼睛圖標顯示或隱藏 重新排序 - 重置预设 - 这将恢复默认的预设顺序和可见性。是否继续? - 恢复默认 - 可见 - 隐藏 + 重置預設 + 這將恢復默認的預設順序和可見性。是否繼續? + 恢復默認 + 可見 + 隱藏 diff --git a/app/src/main/res/values-zh-rTW/strings_home_screen.xml b/app/src/main/res/values-zh-rTW/strings_home_screen.xml index 46a70f8063..78446dd4bc 100644 --- a/app/src/main/res/values-zh-rTW/strings_home_screen.xml +++ b/app/src/main/res/values-zh-rTW/strings_home_screen.xml @@ -3,110 +3,110 @@ β Beta - 云端串流 - 更新日志 - 云端串流 - 从您的云端账户串流音乐 + 雲端串流 + 更新日誌 + 雲端串流 + 從您的雲端賬戶串流音樂 Beta 0.7.5 β - 欢迎使用 PixelPlayer 0.7.5-beta - 您正在使用的是可能包含错误、崩溃或实验性功能的测试版本。请报告问题以帮助我们改进。 - 预期情况 - 可能会意外出现错误、崩溃或未完成的功能。 - 某些功能可能会在不另行通知的情况下更改或移除。 - 测试版可能比正式发布版本更不稳定。 - 在报告已知问题之前,请务必先检查更新。 - 测试版在测试期间可能会更改、损坏或改进的内容。 + 歡迎使用 PixelPlayer 0.7.5-beta + 您正在使用的是可能包含錯誤、崩潰或實驗性功能的測試版本。請報告問題以幫助我們改進。 + 預期情況 + 可能會意外出現錯誤、崩潰或未完成的功能。 + 某些功能可能會在不另行通知的情況下更改或移除。 + 測試版可能比正式發佈版本更不穩定。 + 在報告已知問題之前,請務必先檢查更新。 + 測試版在測試期間可能會更改、損壞或改進的內容。 GitHub Issue 快捷入口 - 请先搜索,然后针对错误、崩溃、请求或问题开启一个明确的报告。 - 浏览现有的 Issue - 报告问题或崩溃 - 分享复现步骤、预期结果、实际结果以及您的设备/操作系统详细信息。 - 如何报告 - 开启新 Issue 前的快速检查清单。 - 开启 Issue 前 - 搜索现有已开启和已关闭的 Issue,以避免重复提交。 - 更新至 PixelPlayer 最新版本并确认问题是否仍然存在。 - 重启应用并确认问题是否仍然存在。 - 尝试复现它并写下确切的步骤。 - 哪种问题类型? - 错误报告:某些功能表现异常。 - 功能请求:添加新功能或改进。 - 提问:如果已启用,请使用 Discussions(讨论),或开启一个带有问题标签的 Issue。 - 错误报告 - 当某些功能表现异常或崩溃时,请复制这些字段。 - 错误报告 - 简要概述: - 预期行为: - 当前行为: - 播放/复现步骤:1. 2. 3. - 发生的频率如何?总是 / 有时 / 很少。 - 截图/视频:如果有。 - 日志/堆栈跟踪:如果有。 - 环境 + 請先搜索,然後針對錯誤、崩潰、請求或問題開啓一個明確的報告。 + 瀏覽現有的 Issue + 報告問題或崩潰 + 分享復現步驟、預期結果、實際結果以及您的設備/操作系統詳細信息。 + 如何報告 + 開啓新 Issue 前的快速檢查清單。 + 開啓 Issue 前 + 搜索現有已開啓和已關閉的 Issue,以避免重複提交。 + 更新至 PixelPlayer 最新版本並確認問題是否仍然存在。 + 重啓應用並確認問題是否仍然存在。 + 嘗試復現它並寫下確切的步驟。 + 哪種問題類型? + 錯誤報告:某些功能表現異常。 + 功能請求:添加新功能或改進。 + 提問:如果已啓用,請使用 Discussions(討論),或開啓一個帶有問題標籤的 Issue。 + 錯誤報告 + 當某些功能表現異常或崩潰時,請複製這些字段。 + 錯誤報告 + 簡要概述: + 預期行爲: + 當前行爲: + 播放/復現步驟:1. 2. 3. + 發生的頻率如何?總是 / 有時 / 很少。 + 截圖/視頻:如果有。 + 日誌/堆棧跟蹤:如果有。 + 環境 PixelPlayer 版本: - 安装来源:GitHub Release、debug 构建、Nightly 构建等。 + 安裝來源:GitHub Release、debug 構建、Nightly 構建等。 Android 版本: - 设备型号: - 额外信息:SD 卡使用情况、特殊设置、权限等。 - 功能请求 - 当您希望添加新功能或改进时,请复制这些字段。 - 问题描述:您想解决什么问题? - 建议方案:它应该如何工作? - 考虑过的替代方案:是否有其他方法? - 影响范围:哪些界面或流程会受影响? - 样图或参考图片(如果有)。 - 标题、隐私和范围 - 使报告易于分类且安全共享。 - 好的 Issue 标题示例 - 均衡器:切换预设选项卡时指示器偏移 - 搜索:查询为空时历史记录列表不显示 - 功能:添加“最近添加”播放列表排序选项 - 请避免 - 诸如“它不起作用”之类的通用报告。 - 在一个 Issue 中包含多个不相关的问题。 - 包含私人数据的未经打码处理的日志或截图。 - 隐私声明 - 在发布日志、截图或视频之前,请删除个人或私人信息。 - Nightly 构建版本 - Nightly 版本与正式版有何不同,以及出错时应提供哪些信息。 - Nightly 版本是从最新提交 (commit) 生成的,可能包含未完成的更改、临时错误或回归。它们比正式发布版本更具实验性。 - 如果可用,可从仓库的 GitHub Actions 工作流产物 (artifacts) 中获取。 - 报告 Nightly 问题 - 在报告 Nightly 版本的问题时,请务必说明该问题发生于 Nightly 版本,而非正式发布版本。请尽可能提供构建日期、工作流运行名称或编号,或者提交 SHA。并请检查最新的正式版本中是否会出现相同问题。 - Beta 0.5.0 升级 - 建议全新安装 - 如果您从 Beta 0.5.0 升级,此更新可能需要全新的媒体库数据,而不是旧的缓存状态。 - 如果元数据或媒体库条目看起来有误 - 歌曲元数据错误、艺术家或专辑不匹配,或出现类似重复的条目,通常意味着只需全新安装即可解决。 - 不再显示 + 設備型號: + 額外信息:SD 卡使用情況、特殊設置、權限等。 + 功能請求 + 當您希望添加新功能或改進時,請複製這些字段。 + 問題描述:您想解決什麼問題? + 建議方案:它應該如何工作? + 考慮過的替代方案:是否有其他方法? + 影響範圍:哪些界面或流程會受影響? + 樣圖或參考圖片(如果有)。 + 標題、隱私和範圍 + 使報告易於分類且安全共享。 + 好的 Issue 標題示例 + 均衡器:切換預設選項卡時指示器偏移 + 搜索:查詢爲空時歷史記錄列表不顯示 + 功能:添加“最近添加”播放列表排序選項 + 請避免 + 諸如“它不起作用”之類的通用報告。 + 在一個 Issue 中包含多個不相關的問題。 + 包含私人數據的未經打碼處理的日誌或截圖。 + 隱私聲明 + 在發佈日誌、截圖或視頻之前,請刪除個人或私人信息。 + Nightly 構建版本 + Nightly 版本與正式版有何不同,以及出錯時應提供哪些信息。 + Nightly 版本是從最新提交 (commit) 生成的,可能包含未完成的更改、臨時錯誤或迴歸。它們比正式發佈版本更具實驗性。 + 如果可用,可從倉庫的 GitHub Actions 工作流產物 (artifacts) 中獲取。 + 報告 Nightly 問題 + 在報告 Nightly 版本的問題時,請務必說明該問題發生於 Nightly 版本,而非正式發佈版本。請儘可能提供構建日期、工作流運行名稱或編號,或者提交 SHA。並請檢查最新的正式版本中是否會出現相同問題。 + Beta 0.5.0 升級 + 建議全新安裝 + 如果您從 Beta 0.5.0 升級,此更新可能需要全新的媒體庫數據,而不是舊的緩存狀態。 + 如果元數據或媒體庫條目看起來有誤 + 歌曲元數據錯誤、藝術家或專輯不匹配,或出現類似重複的條目,通常意味着只需全新安裝即可解決。 + 不再顯示 知道了 - 糟糕!出错了 - 应用在您上次使用时崩溃了。请分享崩溃报告以帮助我们修复此问题。 + 糟糕!出錯了 + 應用在您上次使用時崩潰了。請分享崩潰報告以幫助我們修復此問題。 日期:%1$s - 错误: - 堆栈跟踪(预览): - 崩溃日志 - 崩溃日志已复制到剪贴板 - PixelPlayer 崩溃报告 - 分享崩溃报告 + 錯誤: + 堆棧跟蹤(預覽): + 崩潰日誌 + 崩潰日誌已複製到剪貼板 + PixelPlayer 崩潰報告 + 分享崩潰報告 DJ 混音 - 你的\n推荐 - 暂无数据 - 当 PixelPlayer 发现歌曲或同步数据源时,您的推荐将显示在此处。 + 你的\n推薦 + 暫無數據 + 當 PixelPlayer 發現歌曲或同步數據源時,您的推薦將顯示在此處。 刷新 DAILY MIX - 基于听歌历史 + 基於聽歌歷史 查看全部 Daily Mix Daily Mix @@ -121,25 +121,25 @@ 您的 Daily Mix 是如何生成的 - 您的 Daily Mix 由您最喜爱和播放次数最多的歌曲组成。我们还会添加您喜欢的艺术家和流派的曲目,让您发现新音乐。 - 告诉 AI 您今天想听什么 - 我们使用小样本以保持较低的成本 + 您的 Daily Mix 由您最喜愛和播放次數最多的歌曲組成。我們還會添加您喜歡的藝術家和流派的曲目,讓您發現新音樂。 + 告訴 AI 您今天想聽什麼 + 我們使用小樣本以保持較低的成本 正在更新… 更新 Daily Mix - 完美精选 + 完美精選 Daily Mix - 您的声音之旅已就绪 + 您的聲音之旅已就緒 AI 播放列表生成器 - 描述您的氛围、心情或活动,让 AI 从您的媒体库中精心挑选完美的播放列表。 + 描述您的氛圍、心情或活動,讓 AI 從您的媒體庫中精心挑選完美的播放列表。 播放列表大小 - 最少歌曲数 - 最多歌曲数 - 例如:轻松的夜晚氛围,欢快的健身音乐… - 点击重试 - 声音之旅合成完毕! - 准备播放 + 最少歌曲數 + 最多歌曲數 + 例如:輕鬆的夜晚氛圍,歡快的健身音樂… + 點擊重試 + 聲音之旅合成完畢! + 準備播放 正在生成… 生成播放列表 @@ -149,128 +149,128 @@ 最近播放 播放最新 - 在 %1$s 内无最近播放记录 - 更改时间范围或播放更多歌曲以填充此时间轴。 + 在 %1$s 內無最近播放記錄 + 更改時間範圍或播放更多歌曲以填充此時間軸。 最近播放 今天 昨天 - 听歌统计 - 总播放量 + 聽歌統計 + 總播放量 日均播放 - 最爱曲目 + 最愛曲目 %1$s • %2$d 次播放 - 听歌统计 - 刷新听歌统计 + 聽歌統計 + 刷新聽歌統計 今天 - 本周至今 + 本週至今 本月至今 本年至今 - 全部时间 - 听歌时长 - 播放次数 - 听歌时间轴 - 听歌时长 - 所选时间范围内的总听歌时长。 - 播放次数 - 您在每个时间段内完成的会话次数。 - 平均会话 - 每个时间段的平均听歌时长。 - 划分为 4 小时的时间段,揭示您的日常节奏。 - 每日柱状图,方便比较每周习惯。 - 每周柱状图,显示本月趋势。 - 每月柱状图,显示全年的季节性变化。 - 每年柱状图,总结您的完整听歌历史。 - 暂无听歌数据 - 点击播放开始建立您的听歌时间轴 - 每日节奏 - 每周节奏 - 每月节奏 - 全年概览 - 历年进展 - 按 4 小时分段分组 - 按星期几分组 - 按月内周次分组 - 按月分组 - 按年份分组 - 高峰时段 + 全部時間 + 聽歌時長 + 播放次數 + 聽歌時間軸 + 聽歌時長 + 所選時間範圍內的總聽歌時長。 + 播放次數 + 您在每個時間段內完成的會話次數。 + 平均會話 + 每個時間段的平均聽歌時長。 + 劃分爲 4 小時的時間段,揭示您的日常節奏。 + 每日柱狀圖,方便比較每週習慣。 + 每週柱狀圖,顯示本月趨勢。 + 每月柱狀圖,顯示全年的季節性變化。 + 每年柱狀圖,總結您的完整聽歌歷史。 + 暫無聽歌數據 + 點擊播放開始建立您的聽歌時間軸 + 每日節奏 + 每週節奏 + 每月節奏 + 全年概覽 + 歷年進展 + 按 4 小時分段分組 + 按星期幾分組 + 按月內周次分組 + 按月分組 + 按年份分組 + 高峯時段 %1$d 次播放 - 热门类别 - 比较您在各个流派、艺术家、专辑和歌曲中的听歌偏好。 + 熱門類別 + 比較您在各個流派、藝術家、專輯和歌曲中的聽歌偏好。 流派 - 艺术家 - 专辑 + 藝術家 + 專輯 歌曲 - 按流派统计 - 按艺术家统计 - 按专辑统计 - 按歌曲统计 - %1$d 次播放 • %2$d 位艺术家 + 按流派統計 + 按藝術家統計 + 按專輯統計 + 按歌曲統計 + %1$d 次播放 • %2$d 位藝術家 %1$d 次播放 • %2$d 首曲目 - 暂无类别数据 - 点击播放,您的听歌亮点将在这里浮现 - 听歌习惯 - 暂无习惯数据 - 当我们更了解您时,就会在这里展示您的听歌习惯。 - 总会话数 - 平均会话时长 - 最长会话 - 会话数/天 - 最活跃的一天 - 暂无播放 - 时间轴最高峰 - 热门艺术家 - 暂无热门艺术家 - 继续听歌,您最喜爱的艺术家将出现在这里。 + 暫無類別數據 + 點擊播放,您的聽歌亮點將在這裏浮現 + 聽歌習慣 + 暫無習慣數據 + 當我們更瞭解您時,就會在這裏展示您的聽歌習慣。 + 總會話數 + 平均會話時長 + 最長會話 + 會話數/天 + 最活躍的一天 + 暫無播放 + 時間軸最高峯 + 熱門藝術家 + 暫無熱門藝術家 + 繼續聽歌,您最喜愛的藝術家將出現在這裏。 \? %1$d. %2$s - 热门专辑 - 暂无热门专辑 - 您经常回顾的专辑将出现在这里。 + 熱門專輯 + 暫無熱門專輯 + 您經常回顧的專輯將出現在這裏。 %1$d. %2$s 曲目集中度 - 您的听歌时间在最爱曲目中的分布情况。 - 暂无集中度数据 - 播放更多曲目即可了解您听歌的专一程度。 + 您的聽歌時間在最愛曲目中的分佈情況。 + 暫無集中度數據 + 播放更多曲目即可瞭解您聽歌的專一程度。 Top 1 Top 2-3 其他 %1$d%% - 听歌集中度 - 排名前 3 的曲目占据了您 %1$d%% 的听歌时间。 + 聽歌集中度 + 排名前 3 的曲目佔據了您 %1$d%% 的聽歌時間。 平均播放/曲目 - 去重曲目数 - Top 3 占比 - 此范围内的曲目 - 选定时间范围内播放次数最多的曲目。 - 暂无热门曲目 - 听您喜欢的歌,它们将会在这里高亮显示。 + 去重曲目數 + Top 3 佔比 + 此範圍內的曲目 + 選定時間範圍內播放次數最多的曲目。 + 暫無熱門曲目 + 聽您喜歡的歌,它們將會在這裏高亮顯示。 收起曲目 - 显示所有曲目 + 顯示所有曲目 - %1$d 小时 %2$02d 分钟 - %1$d 分钟 - %1$d 小时 %2$02d 分 - %1$d 小时 + %1$d 小時 %2$02d 分鐘 + %1$d 分鐘 + %1$d 小時 %2$02d 分 + %1$d 小時 %1$d 分 %1$d 秒 - %1$d小时%2$02d分 - %1$d小时 + %1$d小時%2$02d分 + %1$d小時 %1$d分 %1$d秒 - 从未 - 刚刚 + 從未 + 剛剛 1 天前 %1$d 天前 - 1 小时前 - %1$d 小时前 - 1 分钟前 - %1$d 分钟前 + 1 小時前 + %1$d 小時前 + 1 分鐘前 + %1$d 分鐘前 %1$d 首歌曲 %1$d 首歌曲 第 %1$d 周 diff --git a/app/src/main/res/values-zh-rTW/strings_library.xml b/app/src/main/res/values-zh-rTW/strings_library.xml index fbe7fe995e..42dac71316 100644 --- a/app/src/main/res/values-zh-rTW/strings_library.xml +++ b/app/src/main/res/values-zh-rTW/strings_library.xml @@ -1,129 +1,129 @@ - 音乐库 - 媒体库选项卡 - 直接跳转到任意选项卡或对它们进行重新排序。 - 重新排序选项卡 + 音樂庫 + 媒體庫選項卡 + 直接跳轉到任意選項卡或對它們進行重新排序。 + 重新排序選項卡 歌曲 - 专辑 - 艺术家 + 專輯 + 藝術家 播放列表 - 文件夹 + 文件夾 已收藏 - 播放列表创建成功 - 请先设置您的 AI 提供商 API 密钥 - 请先设置您的 Gemini API 密钥 - 已添加到队列 + 播放列表創建成功 + 請先設置您的 AI 提供商 API 密鑰 + 請先設置您的 Gemini API 密鑰 + 已添加到隊列 下一首播放 - 手表传输 - 设置 - 编辑 - 重新排序选项卡 - 展开菜单 + 手錶傳輸 + 設置 + 編輯 + 重新排序選項卡 + 展開菜單 - 您最多可以选择 %1$d 张专辑 - 文件夹 - 文件夹 + 您最多可以選擇 %1$d 張專輯 + 文件夾 + 文件夾 排序方式 - 视图 - 播放列表视图 - 网格 + 視圖 + 播放列表視圖 + 網格 列表 - 内部存储 + 內部存儲 SD 卡 SD 卡目前不可用。 - 云端 - Telegram 云端频道 - 话题显示 (Topics) - 频道 - 话题 + 雲端 + Telegram 雲端頻道 + 話題顯示 (Topics) + 頻道 + 話題 全部 - 云端 - 仅云端 + 雲端 + 僅雲端 - 正在使用 AI 生成元数据… + 正在使用 AI 生成元數據… - 加载歌曲时出错 - 加载专辑时出错 - 加载艺术家时出错 - 重试 + 加載歌曲時出錯 + 加載專輯時出錯 + 加載藝術家時出錯 + 重試 - 在您的媒体库中未找到歌曲。 - 如果您设备上有音乐,请尝试在设置中重新扫描媒体库。 + 在您的媒體庫中未找到歌曲。 + 如果您設備上有音樂,請嘗試在設置中重新掃描媒體庫。 未找到歌曲 新建 - 创建新播放列表 - 导入 M3U 播放列表 - 定位当前歌曲 + 創建新播放列表 + 導入 M3U 播放列表 + 定位當前歌曲 所有歌曲 - 云端 + 雲端 本地 - 排序选项 + 排序選項 - 全选 - 取消选择 - 更多选项 + 全選 + 取消選擇 + 更多選項 - 正在扫描音乐文件… - 正在处理文件… - 第 %1$d 个,共 %2$d 个文件 - 正在同步媒体库… + 正在掃描音樂文件… + 正在處理文件… + 第 %1$d 個,共 %2$d 個文件 + 正在同步媒體庫… 同步完成 等待中… - 正在同步媒体库… - 正在清理专辑封面缓存… - 正在同步云端数据源… - 正在扫描歌词… + 正在同步媒體庫… + 正在清理專輯封面緩存… + 正在同步雲端數據源… + 正在掃描歌詞… - 暂无歌曲 - 将音乐添加到您的设备或同步云端数据源以开始收听。 + 暫無歌曲 + 將音樂添加到您的設備或同步雲端數據源以開始收聽。 未找到本地歌曲 - 尝试其他数据源过滤或重新扫描您的设备媒体库。 - 未找到云端歌曲 - 同步 Telegram 或网易云音乐歌曲,或切换到本地源。 - 暂无专辑 - 一旦您的媒体库对曲目进行分组,专辑就会显示在这里。 - 未找到本地专辑 - 需要本地歌曲才能构建本地专辑组。 - 未找到云端专辑 - 同步后,带有专辑数据的云端歌曲将显示在这里。 - 暂无艺术家 - 从任何来源为歌曲建立索引后,将显示艺术家。 - 未找到本地艺术家 - 目前没有本地歌曲的艺术家元数据可用。 - 未找到云端艺术家 - 同步远程歌曲时会显示云端艺术家条目。 - 暂无收藏歌曲 - 在播放歌曲时点击心形图标将其保存在这里。 - 无本地收藏歌曲 - 切换数据源过滤或收藏您设备中的歌曲。 - 无云端收藏歌曲 - 收藏 Telegram 或网易云音乐的曲目,即可在此视图中看到它们。 - 未找到文件夹 - 包含音乐的内部存储文件夹将显示在这里。 - 暂无播放列表 - 创建您的第一个播放列表来整理您的媒体库。 + 嘗試其他數據源過濾或重新掃描您的設備媒體庫。 + 未找到雲端歌曲 + 同步 Telegram 或網易雲音樂歌曲,或切換到本地源。 + 暫無專輯 + 一旦您的媒體庫對曲目進行分組,專輯就會顯示在這裏。 + 未找到本地專輯 + 需要本地歌曲才能構建本地專輯組。 + 未找到雲端專輯 + 同步後,帶有專輯數據的雲端歌曲將顯示在這裏。 + 暫無藝術家 + 從任何來源爲歌曲建立索引後,將顯示藝術家。 + 未找到本地藝術家 + 目前沒有本地歌曲的藝術家元數據可用。 + 未找到雲端藝術家 + 同步遠程歌曲時會顯示雲端藝術家條目。 + 暫無收藏歌曲 + 在播放歌曲時點擊心形圖標將其保存在這裏。 + 無本地收藏歌曲 + 切換數據源過濾或收藏您設備中的歌曲。 + 無雲端收藏歌曲 + 收藏 Telegram 或網易雲音樂的曲目,即可在此視圖中看到它們。 + 未找到文件夾 + 包含音樂的內部存儲文件夾將顯示在這裏。 + 暫無播放列表 + 創建您的第一個播放列表來整理您的媒體庫。 - 编辑歌曲元数据 + 編輯歌曲元數據 播放 播放歌曲 全部播放 @@ -134,286 +134,286 @@ 全部取消收藏 分享歌曲文件 分享歌曲文件 - 作为 ZIP 全部打包分享 - 无法分享歌曲:%1$s - 添加到队列 - 添加到队列 + 作爲 ZIP 全部打包分享 + 無法分享歌曲:%1$s + 添加到隊列 + 添加到隊列 下一首播放 - 在队列中下一首播放 + 在隊列中下一首播放 添加到播放列表 - 删除 - 全部删除 - 正在检查手表 - 正在传输 %1$d%% - 正在传至手表 - 传输进行中 - 发送至手表 - 手表不可用 - 将歌曲发送至手表 - 手表不可用 - 设为 - 设为声音 - 选择如何将此歌曲用作系统声音 - 将此歌曲用作 - 选择 PixelPlayer 应在哪里安装此声音。 - 手机铃声 - 来电 + 刪除 + 全部刪除 + 正在檢查手錶 + 正在傳輸 %1$d%% + 正在傳至手錶 + 傳輸進行中 + 發送至手錶 + 手錶不可用 + 將歌曲發送至手錶 + 手錶不可用 + 設爲 + 設爲聲音 + 選擇如何將此歌曲用作系統聲音 + 將此歌曲用作 + 選擇 PixelPlayer 應在哪裏安裝此聲音。 + 手機鈴聲 + 來電 通知音 - 消息和应用提醒 - 闹钟音 - 时钟闹钟 - 确认更改声音 - 将“%1$s”设为您的 %2$s? - 设置声音 - 已将“%1$s”设为您的 %2$s - 铃声 + 消息和應用提醒 + 鬧鐘音 + 時鐘鬧鐘 + 確認更改聲音 + 將“%1$s”設爲您的 %2$s? + 設置聲音 + 已將“%1$s”設爲您的 %2$s + 鈴聲 通知音 - 闹钟音 - 启用“修改系统设置”,然后返回 PixelPlayer 自动完成。 - 未启用“修改系统设置”。 - 已将“%1$s”设为您的铃声 - 只有本地歌曲才能设为铃声。 - 无法将此音频文件准备为铃声。 - 无法设置铃声:%1$s - 选项 - 选项 + 鬧鐘音 + 啓用“修改系統設置”,然後返回 PixelPlayer 自動完成。 + 未啓用“修改系統設置”。 + 已將“%1$s”設爲您的鈴聲 + 只有本地歌曲才能設爲鈴聲。 + 無法將此音頻文件準備爲鈴聲。 + 無法設置鈴聲:%1$s + 選項 + 選項 信息 信息 - 时长 + 時長 流派 - 专辑 - 艺术家 + 專輯 + 藝術家 歌曲信息 提供商 文件 %1$d 首歌曲 - 已选择 - %1$d 个播放列表 - %1$d 张专辑 - 已选择 - 限制:每次选择最多 %1$d 张专辑。 - 加入队列及播放将遵循您的选择顺序。 - %1$d 个流派 - 已选择 - 对这些流派中的所有歌曲进行批量操作。 + 已選擇 + %1$d 個播放列表 + %1$d 張專輯 + 已選擇 + 限制:每次選擇最多 %1$d 張專輯。 + 加入隊列及播放將遵循您的選擇順序。 + %1$d 個流派 + 已選擇 + 對這些流派中的所有歌曲進行批量操作。 - 默认顺序 - 标题 (A-Z) - 标题 (Z-A) - 艺术家 - 艺术家 (Z-A) - 专辑 - 专辑 (Z-A) - 添加时间 - 添加时间 (最早优先) - 时长 - 时长 (最短优先) - 发行年份 - 发行年份 (最早优先) + 默認順序 + 標題 (A-Z) + 標題 (Z-A) + 藝術家 + 藝術家 (Z-A) + 專輯 + 專輯 (Z-A) + 添加時間 + 添加時間 (最早優先) + 時長 + 時長 (最短優先) + 發行年份 + 發行年份 (最早優先) 歌曲最少 歌曲最多 - 名称 (A-Z) - 名称 (Z-A) - 歌曲数量 (最多) - 歌曲数量 (最少) - 创建时间 - 创建时间 (最早优先) - 收藏时间 - 收藏时间 (最早优先) - 子文件夹最少 - 子文件夹最多 + 名稱 (A-Z) + 名稱 (Z-A) + 歌曲數量 (最多) + 歌曲數量 (最少) + 創建時間 + 創建時間 (最早優先) + 收藏時間 + 收藏時間 (最早優先) + 子文件夾最少 + 子文件夾最多 - 标题 - 艺术家 - 专辑 - 添加时间 - 时长 - 发行年份 - 歌曲数量 - 名称 - 歌曲数量 - 创建时间 - 收藏时间 - 子文件夹数量 + 標題 + 藝術家 + 專輯 + 添加時間 + 時長 + 發行年份 + 歌曲數量 + 名稱 + 歌曲數量 + 創建時間 + 收藏時間 + 子文件夾數量 - 来源 - 顺序 + 來源 + 順序 降序 升序 - 原始顺序 - 点击切换为升序 - 点击切换为降序 - 此排序保持其原始顺序 - 开关已开启 + 原始順序 + 點擊切換爲升序 + 點擊切換爲降序 + 此排序保持其原始順序 + 開關已開啓 - 重新排序媒体库选项卡 - 重置顺序 - 将选项卡顺序重置为默认? - 正在重新排序选项卡… - 拖动手柄 + 重新排序媒體庫選項卡 + 重置順序 + 將選項卡順序重置爲默認? + 正在重新排序選項卡… + 拖動手柄 - 选择一位艺术家 - 1 位艺术家 - %1$d 位艺术家 - 主要艺术家 - 艺术家主页 + 選擇一位藝術家 + 1 位藝術家 + %1$d 位藝術家 + 主要藝術家 + 藝術家主頁 - 取消传输 + 取消傳輸 %1$s / %2$s - 显示手机到手表音乐传输的实时进度 - 手表传输 - 正在发送至手表 + 顯示手機到手錶音樂傳輸的實時進度 + 手錶傳輸 + 正在發送至手錶 已取消 - 传输已取消 - 传输完成 + 傳輸已取消 + 傳輸完成 已完成 - 失败 - 传输失败 - 有多个活动中的传输 + 失敗 + 傳輸失敗 + 有多個活動中的傳輸 %1$s • %2$s - 正在准备 - 正在准备手表传输 - 正在准备传输… - 正在将 %1$d 首歌曲发送至手表 - 正在发送至手表 - 正在开始传输… - 正在开始 - 正在传输 - %1$d 个传输任务 + 正在準備 + 正在準備手錶傳輸 + 正在準備傳輸… + 正在將 %1$d 首歌曲發送至手錶 + 正在發送至手錶 + 正在開始傳輸… + 正在開始 + 正在傳輸 + %1$d 個傳輸任務 - 编辑歌曲 - 显示信息 - 正在编辑歌曲元数据 - 编辑歌曲元数据可能会影响其在媒体库中的显示和整理方式。更改是永久性的,且可能无法撤销。 + 編輯歌曲 + 顯示信息 + 正在編輯歌曲元數據 + 編輯歌曲元數據可能會影響其在媒體庫中的顯示和整理方式。更改是永久性的,且可能無法撤銷。 知道了 信息 封面 - 选择一张正方形图片并进行微调,让您的封面在整个应用中看起来更棒。 + 選擇一張正方形圖片並進行微調,讓您的封面在整個應用中看起來更棒。 更改封面 - 删除封面 - 标题 - 艺术家 - 专辑 - 专辑艺术家 + 刪除封面 + 標題 + 藝術家 + 專輯 + 專輯藝術家 流派 作曲 - 曲目编号 - 唱片编号 - 音量标准化-曲目增益 (dB) - 音量标准化-专辑增益 (dB) + 曲目編號 + 唱片編號 + 音量標準化-曲目增益 (dB) + 音量標準化-專輯增益 (dB) -6.50 -8.20 - 新封面预览 - 当前歌曲封面 - 调整您的封面 - 使用双指缩放和拖动手势找到完美的构图。 - 应用封面 - 无法加载选定的图片 - 在 lrclib.net 搜索歌词 + 新封面預覽 + 當前歌曲封面 + 調整您的封面 + 使用雙指縮放和拖動手勢找到完美的構圖。 + 應用封面 + 無法加載選定的圖片 + 在 lrclib.net 搜索歌詞 - 批量编辑 %d 首歌曲 - 仅会更新已修改的字段。将字段留空则保持现有值。 + 批量編輯 %d 首歌曲 + 僅會更新已修改的字段。將字段留空則保持現有值。 (混合值) - (可选 - 留空则跳过) + (可選 - 留空則跳過) 成功更新 %d 首歌曲 - 已更新 %1$d 首(共 %2$d 首)歌曲。某些文件无法编辑。 - 更新歌曲失败 - 批量更换封面 - 这将替换所有 %d 首选定歌曲的封面 - 为所有选定歌曲设置封面 + 已更新 %1$d 首(共 %2$d 首)歌曲。某些文件無法編輯。 + 更新歌曲失敗 + 批量更換封面 + 這將替換所有 %d 首選定歌曲的封面 + 爲所有選定歌曲設置封面 移除所有封面 - (多个不同的封面) + (多個不同的封面) 已忽略播放列表 - 创建播放列表 - 选择创建方式。 - 手动 - 自己设计封面、图标、形状并挑选歌曲。 + 創建播放列表 + 選擇創建方式。 + 手動 + 自己設計封面、圖標、形狀並挑選歌曲。 使用 AI - 使用高级控制生成精选的播放列表。 - 需要在设置中配置 Gemini API 密钥。 - 设置 API 密钥 + 使用高級控制生成精選的播放列表。 + 需要在設置中配置 Gemini API 密鑰。 + 設置 API 密鑰 - AI 播放列表实验室 + AI 播放列表實驗室 重置 正在生成… 生成 - 意图 - 播放列表名称(可选) - 这个播放列表应该带来什么感觉? - 示例:伴随温暖合成器音乐的日落驾驶 + 意圖 + 播放列表名稱(可選) + 這個播放列表應該帶來什麼感覺? + 示例:伴隨溫暖合成器音樂的日落駕駛 方向 - 情绪 - 活动 - 时代 - 精选引擎 + 情緒 + 活動 + 時代 + 精選引擎 能量 - 控制歌曲的强度和节奏。1 = 平静/缓慢,5 = 高能量/快节奏。 + 控制歌曲的強度和節奏。1 = 平靜/緩慢,5 = 高能量/快節奏。 探索度 - 控制所选歌曲的熟悉程度。1 = 你最常听的最爱,5 = 很少播放的冷门曲目。 - 最少歌曲数 - 最多歌曲数 - 过滤 - 优先考虑的流派(可选) - 例如:合成器波 (synthwave)、独立流行 (indie pop) - 避免的流派(可选) - 例如:金属 (metal)、硬核陷阱 (hard trap) - 首选语言(可选) - 例如:英语,西班牙语,纯音乐 (instrumental) - 优先考虑已收藏的最爱 - 避免露骨歌词 (Explicit) - 提示词预览 - 当您添加偏好设置后,最终的提示词将显示在这里。 - 精准策展 - 定义情绪、活动、约束和深度。 - AI 将仅使用您本地媒体库中的歌曲。 - 至少为 AI 添加一项指令。 - 请设置有效的歌曲数量范围。 + 控制所選歌曲的熟悉程度。1 = 你最常聽的最愛,5 = 很少播放的冷門曲目。 + 最少歌曲數 + 最多歌曲數 + 過濾 + 優先考慮的流派(可選) + 例如:合成器波 (synthwave)、獨立流行 (indie pop) + 避免的流派(可選) + 例如:金屬 (metal)、硬核陷阱 (hard trap) + 首選語言(可選) + 例如:英語,西班牙語,純音樂 (instrumental) + 優先考慮已收藏的最愛 + 避免露骨歌詞 (Explicit) + 提示詞預覽 + 當您添加偏好設置後,最終的提示詞將顯示在這裏。 + 精準策展 + 定義情緒、活動、約束和深度。 + AI 將僅使用您本地媒體庫中的歌曲。 + 至少爲 AI 添加一項指令。 + 請設置有效的歌曲數量範圍。 %1$d/5 - 自定义… - 输入自定义值 - 输入您的自定义值 + 自定義… + 輸入自定義值 + 輸入您的自定義值 - 任何时代 - 核心请求:%1$s。 - 情绪目标:%1$s。 - 活动背景:%1$s。 - 时代焦点:%1$s。 - 优先考虑的流派:%1$s。 + 任何時代 + 核心請求:%1$s。 + 情緒目標:%1$s。 + 活動背景:%1$s。 + 時代焦點:%1$s。 + 優先考慮的流派:%1$s。 避免的流派:%1$s。 - 首选语言:%1$s。 - 能量等级目标:%1$d/5。 - 探索度目标:%1$d/5(1 代表熟悉,5 代表冷门曲目)。 - 可能的话,优先选择更接近听众最爱的歌曲。 - 只要有替代选择,尽量避免露骨歌词。 - 保持平滑的过渡,避免重复聚集同一艺术家。 + 首選語言:%1$s。 + 能量等級目標:%1$d/5。 + 探索度目標:%1$d/5(1 代表熟悉,5 代表冷門曲目)。 + 可能的話,優先選擇更接近聽衆最愛的歌曲。 + 只要有替代選擇,儘量避免露骨歌詞。 + 保持平滑的過渡,避免重複聚集同一藝術家。 - 放松 + 放鬆 活力 - 快乐 + 快樂 暗黑 浪漫 - 忧郁 + 憂鬱 健身 - 专注 + 專注 公路旅行 - 派对 - 学习 + 派對 + 學習 深夜 @@ -427,140 +427,140 @@ - 尚未创建播放列表。 - 点击“新建播放列表”按钮开始。 + 尚未創建播放列表。 + 點擊“新建播放列表”按鈕開始。 新建播放列表 - 播放列表名称 + 播放列表名稱 我的播放列表 添加 %1$d 首歌曲到… - 选择播放列表 + 選擇播放列表 搜索播放列表… - 已将歌曲添加到播放列表 - 播放列表已创建并已添加歌曲 - 内部存储 + 已將歌曲添加到播放列表 + 播放列表已創建並已添加歌曲 + 內部存儲 添加歌曲 - 添加所选歌曲 + 添加所選歌曲 添加 - 搜索或过滤歌曲… + 搜索或過濾歌曲… 已收藏 - 加载歌曲失败 - 加载更多 + 加載歌曲失敗 + 加載更多 - 合并播放列表 - 为合并后的播放列表输入名称: - 合并的播放列表 - 这将把 %1$d 个选定的播放列表合并为一个。 + 合併播放列表 + 爲合併後的播放列表輸入名稱: + 合併的播放列表 + 這將把 %1$d 個選定的播放列表合併爲一個。 未找到可播放的有效歌曲 - 在当前列表中未找到歌曲 - 无法定位歌曲 - 在媒体库中未找到歌曲 - 播放停止:%1$s 已结束(曲目结束)。 + 在當前列表中未找到歌曲 + 無法定位歌曲 + 在媒體庫中未找到歌曲 + 播放停止:%1$s 已結束(曲目結束)。 曲目 - 没有可供随机播放的歌曲。 - 选定的专辑 - 在选定的专辑中未找到可播放的歌曲 - 在选定的流派中未找到可播放的歌曲 - 仅前 %1$d 张专辑已加入队列 - 已将 %1$d 张专辑加入队列(共 %2$d 首歌曲) - 无法将选定的专辑加入队列 + 沒有可供隨機播放的歌曲。 + 選定的專輯 + 在選定的專輯中未找到可播放的歌曲 + 在選定的流派中未找到可播放的歌曲 + 僅前 %1$d 張專輯已加入隊列 + 已將 %1$d 張專輯加入隊列(共 %2$d 首歌曲) + 無法將選定的專輯加入隊列 所有歌曲均已在收藏中 - 没有被收藏的歌曲 - 正在创建 ZIP 文件… - 分享失败:%1$s + 沒有被收藏的歌曲 + 正在創建 ZIP 文件… + 分享失敗:%1$s - 添加了 %d 首歌曲到队列 - 添加了 %d 首歌曲到队列 + 添加了 %d 首歌曲到隊列 + 添加了 %d 首歌曲到隊列 - %d 首歌曲将作为下一首播放 - %d 首歌曲将作为下一首播放 + %d 首歌曲將作爲下一首播放 + %d 首歌曲將作爲下一首播放 添加了 %d 首歌曲到收藏 添加了 %d 首歌曲到收藏 - 从收藏中移除了 %d 首歌曲 - 从收藏中移除了 %d 首歌曲 + 從收藏中移除了 %d 首歌曲 + 從收藏中移除了 %d 首歌曲 - 没有可分享的播放列表 + 沒有可分享的播放列表 分享播放列表 - 分享失败:%1$s - 没有可导出的播放列表 - 导出失败:%1$s + 分享失敗:%1$s + 沒有可導出的播放列表 + 導出失敗:%1$s Music/PixelPlayer Exports - 请在设置中配置您的 Gemini API 密钥。 - 播放列表已恢复 + 請在設置中配置您的 Gemini API 密鑰。 + 播放列表已恢復 - 正在分享 %d 个播放列表 - 正在分享 %d 个播放列表 + 正在分享 %d 個播放列表 + 正在分享 %d 個播放列表 - 已导出 %1$d 个播放列表到 %2$s - 已导出 %1$d 个播放列表到 %2$s + 已導出 %1$d 個播放列表到 %2$s + 已導出 %1$d 個播放列表到 %2$s - 无效的专辑 ID - 未找到专辑 ID - 加载专辑数据出错:%s - 未找到专辑 + 無效的專輯 ID + 未找到專輯 ID + 加載專輯數據出錯:%s + 未找到專輯 - 无效的艺术家 ID - 未找到艺术家 ID - 加载艺术家数据出错:%s - 找不到该艺术家 + 無效的藝術家 ID + 未找到藝術家 ID + 加載藝術家數據出錯:%s + 找不到該藝術家 - 无法删除当前正在播放的歌曲 - 删除了 %1$d 个文件(跳过了 %2$d 个 - 正在播放) - 删除了 %1$d 个(共 %2$d 个)文件 - 删除文件失败 - 文件已删除 - 无法删除文件或找不到文件 - 删除已取消 - 删除歌曲? - “%1$s” - %2$s\n\n此歌曲将从您的设备中永久删除,且无法恢复。 - 这些歌曲将从您的设备中永久删除,且无法恢复。 + 無法刪除當前正在播放的歌曲 + 刪除了 %1$d 個文件(跳過了 %2$d 個 - 正在播放) + 刪除了 %1$d 個(共 %2$d 個)文件 + 刪除文件失敗 + 文件已刪除 + 無法刪除文件或找不到文件 + 刪除已取消 + 刪除歌曲? + “%1$s” - %2$s\n\n此歌曲將從您的設備中永久刪除,且無法恢復。 + 這些歌曲將從您的設備中永久刪除,且無法恢復。 - 删除了 %d 个文件 - 删除了 %d 个文件 + 刪除了 %d 個文件 + 刪除了 %d 個文件 - 删除 %d 首歌曲? - 删除 %d 首歌曲? + 刪除 %d 首歌曲? + 刪除 %d 首歌曲? - 元数据更新成功 + 元數據更新成功 正在更新 %1$d 首歌曲… 已成功更新 %1$d 首歌曲! - 更新了 %1$d 首歌曲。失败:%2$d 首 - 歌词保存成功 - 保存歌词失败 - 没有可保存的歌词 - 权限被拒绝 – 无法编辑文件 - 权限被拒绝 – 无法保存歌词 - 权限被拒绝 – 无法编辑此文件 + 更新了 %1$d 首歌曲。失敗:%2$d 首 + 歌詞保存成功 + 保存歌詞失敗 + 沒有可保存的歌詞 + 權限被拒絕 – 無法編輯文件 + 權限被拒絕 – 無法保存歌詞 + 權限被拒絕 – 無法編輯此文件 - 请在设置中为选定的 AI 提供商配置有效的 API 密钥。 - AI 错误:%s - 所选 AI 提供商拒绝了请求,原因是账户没有积分或可用配额不足。 - 所选 AI 模型不再可用。PixelPlayer 尝试自动切换到支持的模型。 - AI 无法根据您的提示找到任何歌曲。 - 写下一个 Daily Mix 的灵感 + 請在設置中爲選定的 AI 提供商配置有效的 API 密鑰。 + AI 錯誤:%s + 所選 AI 提供商拒絕了請求,原因是賬戶沒有積分或可用配額不足。 + 所選 AI 模型不再可用。PixelPlayer 嘗試自動切換到支持的模型。 + AI 無法根據您的提示找到任何歌曲。 + 寫下一個 Daily Mix 的靈感 已使用 AI 更新 Daily Mix - 无法更新:%s - AI 无法为此合集找到歌曲 + 無法更新:%s + AI 無法爲此合集找到歌曲 \ No newline at end of file diff --git a/app/src/main/res/values-zh-rTW/strings_player.xml b/app/src/main/res/values-zh-rTW/strings_player.xml index 73811e2fb6..c4746cd765 100644 --- a/app/src/main/res/values-zh-rTW/strings_player.xml +++ b/app/src/main/res/values-zh-rTW/strings_player.xml @@ -3,135 +3,135 @@ 收起播放器 正在播放 - 云端串流 + 雲端串流 投屏 - 蓝牙 + 藍牙 本地播放 - 正在连接… - 打开队列 + 正在連接… + 打開隊列 - 准备连接 - 允许 PixelPlayer 查看您附近的设备和当前 Wi-Fi,以便我们保持您的投屏、蓝牙音频和扬声器同步。 - 附近设备 - 需要此权限来读取和控制您连接的蓝牙音频设备。 - 用于 Wi-Fi 的位置信息 - Android 需要位置信息才能共享您当前所连接的 Wi-Fi 网络 (SSID),以便我们找到兼容的投屏设备。 - 允许访问 - 我们仅将这些权限用于设备互连——投屏、控制附近扬声器以及保持音频同步。 - 连接设备 - 正在扫描附近 - 投屏会话 - 正在连接 - 已连接 - 本机 - 蓝牙音频 + 準備連接 + 允許 PixelPlayer 查看您附近的設備和當前 Wi-Fi,以便我們保持您的投屏、藍牙音頻和揚聲器同步。 + 附近設備 + 需要此權限來讀取和控制您連接的藍牙音頻設備。 + 用於 Wi-Fi 的位置信息 + Android 需要位置信息才能共享您當前所連接的 Wi-Fi 網絡 (SSID),以便我們找到兼容的投屏設備。 + 允許訪問 + 我們僅將這些權限用於設備互連——投屏、控制附近揚聲器以及保持音頻同步。 + 連接設備 + 正在掃描附近 + 投屏會話 + 正在連接 + 已連接 + 本機 + 藍牙音頻 本地播放 正在播放 - 已暂停 - 设备音量 - 手机音量 + 已暫停 + 設備音量 + 手機音量 %1$d/%2$d - 电量 + 電量 音量 - 断开连接 - 连接性 - 开启 Wi-Fi 或蓝牙 - 刷新连接 + 斷開連接 + 連接性 + 開啓 Wi-Fi 或藍牙 + 刷新連接 Wi-Fi - - - 已连接 - 蓝牙 - - - 已连接 - 附近设备 - 刷新设备 - 已连接 - 正在连接 - 可连接 + + + 已連接 + 藍牙 + + + 已連接 + 附近設備 + 刷新設備 + 已連接 + 正在連接 + 可連接 可用 - 正在连接... - 正在搜索设备… - 请确保您的电视或扬声器已开启,并连接到同一 Wi-Fi 网络。 + 正在連接... + 正在搜索設備… + 請確保您的電視或揚聲器已開啓,並連接到同一 Wi-Fi 網絡。 控制 - 设备 + 設備 - 投屏媒体服务器 - 正在投屏至设备 - 正在向投屏设备提供媒体 + 投屏媒體服務器 + 正在投屏至設備 + 正在向投屏設備提供媒體 %1$s: %2$s - 投屏时暂不支持对此音频格式进行进度调节,这可能会导致投屏会话崩溃。 + 投屏時暫不支持對此音頻格式進行進度調節,這可能會導致投屏會話崩潰。 - 睡眠定时器 - 定时器 - %1$d 分钟 - 定时器已设为 %1$d 分钟。 + 睡眠定時器 + 定時器 + %1$d 分鐘 + 定時器已設爲 %1$d 分鐘。 1 次 %d 次 %d 次 - 播放次数:%1$s - 当前曲目结束时 - 播放将在曲目结束时停止。 - 开启 - 自定义时间 - 取消定时器 - 曲目结束 - 定时器已取消。 - 无法启用“曲目结束时停止”:没有正在播放的歌曲。 - “曲目结束”定时器已停用:歌曲已从 %1$s 切换到 %2$s。 + 播放次數:%1$s + 當前曲目結束時 + 播放將在曲目結束時停止。 + 開啓 + 自定義時間 + 取消定時器 + 曲目結束 + 定時器已取消。 + 無法啓用“曲目結束時停止”:沒有正在播放的歌曲。 + “曲目結束”定時器已停用:歌曲已從 %1$s 切換到 %2$s。 上一首曲目 - 当前曲目 - 设置自定义时长 + 當前曲目 + 設置自定義時長 - 接下来播放 - 队列当前为空。 + 接下來播放 + 隊列當前爲空。 1 首曲目在列。 %d 首曲目在列。 - 队列 - 队列为空。 + 隊列 + 隊列爲空。 重新排序歌曲 - 切换随机播放 - 切换循环播放 - 睡眠定时器 + 切換隨機播放 + 切換循環播放 + 睡眠定時器 更多操作 - 定位当前歌曲 - 清空队列 - 清空队列 - 您确定要清空队列中除当前正在播放之外的所有歌曲吗? - 保存为播放列表 - %1$s 队列 - 当前队列 + 定位當前歌曲 + 清空隊列 + 清空隊列 + 您確定要清空隊列中除當前正在播放之外的所有歌曲嗎? + 保存爲播放列表 + %1$s 隊列 + 當前隊列 移除歌曲 已移除 - 保存为播放列表 - 取消全选 - 播放列表名称 + 保存爲播放列表 + 取消全選 + 播放列表名稱 搜索要包含的歌曲… - 没有与 \"%1$s\" 匹配的歌曲 + 沒有與 \"%1$s\" 匹配的歌曲 - 已选择 %d 首歌曲 - 已选择 %d 首歌曲 + 已選擇 %d 首歌曲 + 已選擇 %d 首歌曲 - 保存为:%1$s - 输入播放列表名称 - 从播放列表中移除 - %1$s 的更多选项 + 保存爲:%1$s + 輸入播放列表名稱 + 從播放列表中移除 + %1$s 的更多選項 - 歌词 - 正在加载歌词… + 歌詞 + 正在加載歌詞… 逐字同步 - 静态 - 歌词选项 + 靜態 + 歌詞選項 −.5 −.1 +.1 @@ -140,59 +140,59 @@ %1$+.1fs - 搜索歌词失败 - 无法从远程获取歌词 - 连接超时。请检查您的网络连接。 - 网络错误。请检查您的网络连接。 - 服务器错误(代码 %d)。请稍后再试。 + 搜索歌詞失敗 + 無法從遠程獲取歌詞 + 連接超時。請檢查您的網絡連接。 + 網絡錯誤。請檢查您的網絡連接。 + 服務器錯誤(代碼 %d)。請稍後再試。 - 歌词已可用,跳过在线获取。 - 已找到内嵌歌词,跳过在线获取。 - 已找到本地 (.lrc) 歌词,跳过在线获取。 + 歌詞已可用,跳過在線獲取。 + 已找到內嵌歌詞,跳過在線獲取。 + 已找到本地 (.lrc) 歌詞,跳過在線獲取。 - 保存歌词 - 使用 AI 翻译 - 此歌词已有翻译 - 此歌词已经是该语言 + 保存歌詞 + 使用 AI 翻譯 + 此歌詞已有翻譯 + 此歌詞已經是該語言 未配置 API - 歌词翻译成功! - 正在翻译歌词... - 重置已导入的歌词 - 重置歌词? - 您确定要重置此歌曲的歌词吗? - 外观 - 对齐方式 - 歌词左对齐 - 歌词居中 - 歌词右对齐 + 歌詞翻譯成功! + 正在翻譯歌詞... + 重置已導入的歌詞 + 重置歌詞? + 您確定要重置此歌曲的歌詞嗎? + 外觀 + 對齊方式 + 歌詞左對齊 + 歌詞居中 + 歌詞右對齊 控制 - 调整同步 - 隐藏同步控制 - 显示罗马音 - 显示翻译 + 調整同步 + 隱藏同步控制 + 顯示羅馬音 + 顯示翻譯 禁用沉浸模式(一次) 保持屏幕常亮 - 保存歌词 - 选择要保存的版本: - 同步版(带时间戳) - 纯文本(仅文本) + 保存歌詞 + 選擇要保存的版本: + 同步版(帶時間戳) + 純文本(僅文本) - 您想在网上搜索歌词吗? - 显示歌词选项 - 始终打开选择器,而不是自动应用第一个匹配项 - 正在搜索歌词… - 未找到歌词 - 我们无法自动找到歌词。您可以编辑标题或艺术家名称并尝试手动搜索。 - 歌曲标题 - 艺术家(可选) - 找到 %d 个匹配项 + 您想在網上搜索歌詞嗎? + 顯示歌詞選項 + 始終打開選擇器,而不是自動應用第一個匹配項 + 正在搜索歌詞… + 未找到歌詞 + 我們無法自動找到歌詞。您可以編輯標題或藝術家名稱並嘗試手動搜索。 + 歌曲標題 + 藝術家(可選) + 找到 %d 個匹配項 已同步 %1$s • %2$s - 歌词提供者 + 歌詞提供者 https://lrclib.net/ diff --git a/app/src/main/res/values-zh-rTW/strings_screens.xml b/app/src/main/res/values-zh-rTW/strings_screens.xml index a0ae15a250..66b19b5630 100644 --- a/app/src/main/res/values-zh-rTW/strings_screens.xml +++ b/app/src/main/res/values-zh-rTW/strings_screens.xml @@ -1,78 +1,78 @@ - 错误:流派 ID 缺失 + 錯誤:流派 ID 缺失 - 开始吧! + 開始吧! 第 %1$d 步,共 %2$d 步 - 请先授予所需权限。 - 请授予所有所需权限。 - 欢迎使用 + 請先授予所需權限。 + 請授予所有所需權限。 + 歡迎使用 β Beta - 让我们为您完成所有设置。 - 媒体权限 - PixelPlayer 需要访问您的音频文件以构建您的音乐库。 - 权限已授予 - 授予媒体权限 + 讓我們爲您完成所有設置。 + 媒體權限 + PixelPlayer 需要訪問您的音頻文件以構建您的音樂庫。 + 權限已授予 + 授予媒體權限 通知 - 启用通知以便在锁屏和通知栏控制您的音乐。 - 启用通知 - 您有备份文件吗? - 如果您已有 PixelPlayer 备份,现在恢复它可以跳过此设备上的大部分剩余设置。 - 导入备份 - 正在检查备份 - 正在检查备份包… - 正在恢复备份 - 跳过 / 暂不 - 恢复备份 - 在完成设置之前,查看您要导入的内容。 - 已选择 %1$d 个模块,共 %2$d 个 - 创建于 %1$s - 来自版本 %1$s 的备份 + 啓用通知以便在鎖屏和通知欄控制您的音樂。 + 啓用通知 + 您有備份文件嗎? + 如果您已有 PixelPlayer 備份,現在恢復它可以跳過此設備上的大部分剩餘設置。 + 導入備份 + 正在檢查備份 + 正在檢查備份包… + 正在恢復備份 + 跳過 / 暫不 + 恢復備份 + 在完成設置之前,查看您要導入的內容。 + 已選擇 %1$d 個模塊,共 %2$d 個 + 創建於 %1$s + 來自版本 %1$s 的備份 未知版本 - 恢复所选项 - 正在恢复 - 排除的文件夹 - 默认扫描所有文件夹。选择在构建媒体库时要忽略的任何位置。 - 选择要忽略的文件夹 - 请先授予存储权限 - 应用主题 - 在开始探索媒体库之前,选择您喜欢的外观。 + 恢復所選項 + 正在恢復 + 排除的文件夾 + 默認掃描所有文件夾。選擇在構建媒體庫時要忽略的任何位置。 + 選擇要忽略的文件夾 + 請先授予存儲權限 + 應用主題 + 在開始探索媒體庫之前,選擇您喜歡的外觀。 深色 - PixelPlayer 默认的 Material 3 深色外观。 - 浅色 - 整个应用采用更明亮的 Material 3 外观。 - 跟随系统 - 匹配您手机当前的外观设置。 - 推荐 - 您稍后可以在“设置 > 外观 > 应用主题”中更改此设置。 - 媒体库布局 - 选择您首选的媒体库导航方式。 + PixelPlayer 默認的 Material 3 深色外觀。 + 淺色 + 整個應用採用更明亮的 Material 3 外觀。 + 跟隨系統 + 匹配您手機當前的外觀設置。 + 推薦 + 您稍後可以在“設置 > 外觀 > 應用主題”中更改此設置。 + 媒體庫佈局 + 選擇您首選的媒體庫導航方式。 歌曲 - 紧凑模式 - 使用极简胶囊导航 - 使用标准标签栏 + 緊湊模式 + 使用極簡膠囊導航 + 使用標準標籤欄 歌曲 - 专辑 - 艺术家 - 您稍后可以在“设置 > 外观 > 媒体库导航”中更改此设置。 - 应用导航 - 选择底部导航栏的样式。 - 默认样式 - 带有圆角的悬浮胶囊 - 标准全宽栏 - 自定义圆角半径 - 您稍后可以在“设置 > 外观 > 导航栏样式”中更改此设置。 - 闹钟和提醒 - 可选,但如果您使用睡眠定时器并希望 PixelPlayer 准时停止播放,则推荐使用。 - 授予权限 - 电池优化 - 某些 Android 设备会强制结束后台应用。请为 PixelPlayer 禁用电池优化,以防止意外的播放中断。 - 禁用优化 - 一切就绪! - 您已准备好享受音乐了。 + 專輯 + 藝術家 + 您稍後可以在“設置 > 外觀 > 媒體庫導航”中更改此設置。 + 應用導航 + 選擇底部導航欄的樣式。 + 默認樣式 + 帶有圓角的懸浮膠囊 + 標準全寬欄 + 自定義圓角半徑 + 您稍後可以在“設置 > 外觀 > 導航欄樣式”中更改此設置。 + 鬧鐘和提醒 + 可選,但如果您使用睡眠定時器並希望 PixelPlayer 準時停止播放,則推薦使用。 + 授予權限 + 電池優化 + 某些 Android 設備會強制結束後臺應用。請爲 PixelPlayer 禁用電池優化,以防止意外的播放中斷。 + 禁用優化 + 一切就緒! + 您已準備好享受音樂了。 搜索… @@ -80,24 +80,24 @@ 清除搜索 最近搜索 清除全部 - 历史 - 删除搜索历史项 - 无结果 - 没有找到“%1$s”的结果 - 未找到任何内容 - 尝试不同的搜索词或检查您的过滤条件。 - 未找到结果。 - 按流派浏览 - 无可用流派。 + 歷史 + 刪除搜索歷史項 + 無結果 + 沒有找到“%1$s”的結果 + 未找到任何內容 + 嘗試不同的搜索詞或檢查您的過濾條件。 + 未找到結果。 + 按流派瀏覽 + 無可用流派。 播放 %1$s 收起 %1$s - 展开 %1$s - 编辑艺术家图片 + 展開 %1$s + 編輯藝術家圖片 更改照片 - 恢复默认 - 随机播放艺术家 + 恢復默認 + 隨機播放藝術家 唱片 %d @@ -106,18 +106,18 @@ 未找到播放列表。 - 此播放列表为空。 - 点击“添加歌曲”开始。 - 此文件夹不包含歌曲。 - 对歌曲排序 - 更多选项 - 播放列表选项 - 编辑播放列表 - 删除播放列表 - 删除播放列表? - 您确定要删除此播放列表吗? - 设置默认过渡效果 - 导出播放列表 + 此播放列表爲空。 + 點擊“添加歌曲”開始。 + 此文件夾不包含歌曲。 + 對歌曲排序 + 更多選項 + 播放列表選項 + 編輯播放列表 + 刪除播放列表 + 刪除播放列表? + 您確定要刪除此播放列表嗎? + 設置默認過渡效果 + 導出播放列表 %1$s • %2$s 播放 添加 @@ -128,31 +128,31 @@ 重新排序歌曲 - 全局过渡效果 - 播放列表规则 - 除非被覆盖,否则此配置适用于所有播放源。 - 为此特定播放列表配置默认行为。 - 活动状态 - 全局默认 - 播放列表默认 - 跟随全局 - 自定义覆盖 - 自定义覆盖 - 启用以设置此播放列表的具体规则。 - 正在使用全局默认值 + 全局過渡效果 + 播放列表規則 + 除非被覆蓋,否則此配置適用於所有播放源。 + 爲此特定播放列表配置默認行爲。 + 活動狀態 + 全局默認 + 播放列表默認 + 跟隨全局 + 自定義覆蓋 + 自定義覆蓋 + 啓用以設置此播放列表的具體規則。 + 正在使用全局默認值 更改已成功保存 - 过渡样式 - 曲目之间如何混合 - + 過渡樣式 + 曲目之間如何混合 + 淡入淡出 (Crossfade) - 过渡时长 - 总重叠时长 %1$d 秒 - 重置过渡效果 - 当前歌曲 + 過渡時長 + 總重疊時長 %1$d 秒 + 重置過渡效果 + 當前歌曲 下一首歌曲 - 曲目将重叠 %1$d 秒 - 音量曲线 - 微调音频斜率 + 曲目將重疊 %1$d 秒 + 音量曲線 + 微調音頻斜率 淡出 淡入 @@ -162,83 +162,83 @@ 添加歌曲 返回或取消 下一步 - 创建 - 编辑播放列表 - 自动生成拼图 + 創建 + 編輯播放列表 + 自動生成拼圖 添加照片 - 选择图片 + 選擇圖片 更改 移除 - 播放列表名称 - 我的超赞混音 - 编辑封面 - 调整您的封面图片 - 使用双指缩放和拖动手势找到完美的构图 - 手动 + 播放列表名稱 + 我的超讚混音 + 編輯封面 + 調整您的封面圖片 + 使用雙指縮放和拖動手勢找到完美的構圖 + 手動 智能 使用 AI 生成 - 智能规则 - 默认 - 图片 - 图标 - 背景颜色 - 图标符号 - 形状样式 - 形状参数 - 圆角半径 + 智能規則 + 默認 + 圖片 + 圖標 + 背景顏色 + 圖標符號 + 形狀樣式 + 形狀參數 + 圓角半徑 平滑度 - 边数 - 曲线 - 旋转 - 缩放 + 邊數 + 曲線 + 旋轉 + 縮放 播放最多 - 您播放次数最多的曲目。 + 您播放次數最多的曲目。 最近播放 - 您最近听过的歌曲。 - 被遗忘的最爱 - 您有一段时间没听过的最爱曲目。 - 新发现 - 最近添加但播放次数较少的曲目。 + 您最近聽過的歌曲。 + 被遺忘的最愛 + 您有一段時間沒聽過的最愛曲目。 + 新發現 + 最近添加但播放次數較少的曲目。 快速填充流派 - 排序与播放 - 随机播放 - 排序依据 - 艺术家 - 专辑 - 标题 - 通用艺术家 - 随机播放 %1$s + 排序與播放 + 隨機播放 + 排序依據 + 藝術家 + 專輯 + 標題 + 通用藝術家 + 隨機播放 %1$s - 选择歌曲 - 选择流派 + 選擇歌曲 + 選擇流派 搜索歌曲 新建流派 - 添加自定义 - 添加自定义流派 - 流派名称 - 选择图标 + 添加自定義 + 添加自定義流派 + 流派名稱 + 選擇圖標 流派:%1$s - 选择一个流派 + 選擇一個流派 快速填充 - DJ 空间 - 正在加载… - 打碟机 %1$d - 加载歌曲 - 未加载歌曲 + DJ 空間 + 正在加載… + 打碟機 %1$d + 加載歌曲 + 未加載歌曲 - 音轨分离 (Stem separation) 功能暂不可用。 + 音軌分離 (Stem separation) 功能暫不可用。 音量 速度 交叉推子 - 打碟机 1 - 打碟机 2 - 选择一首歌曲 - 播放/暂停 + 打碟機 1 + 打碟機 2 + 選擇一首歌曲 + 播放/暫停 歌曲封面 x%1$.2f diff --git a/app/src/main/res/values-zh-rTW/strings_settings.xml b/app/src/main/res/values-zh-rTW/strings_settings.xml index feae1075f2..468a46c6d0 100644 --- a/app/src/main/res/values-zh-rTW/strings_settings.xml +++ b/app/src/main/res/values-zh-rTW/strings_settings.xml @@ -1,104 +1,108 @@ - 音乐管理 - 管理文件夹、刷新媒体库、解析选项 - 外观 - 主题、布局和视觉样式 + 音樂管理 + 管理文件夾、刷新媒體庫、解析選項 + 外觀 + 主題、佈局和視覺樣式 + 歌詞 + 來源優先級、沉浸式歌詞和導入管理 播放 - 音频行为、淡入淡出和后台播放 - 行为 - 手势、触感反馈和导航行为 + 音頻行爲、淡入淡出和後臺播放 + 行爲 + 手勢、觸感反饋和導航行爲 AI 集成 (β) - AI 提供商、API 密钥和模型设置 - 备份与恢复 - 导出和恢复您的个人应用数据 - 开发者选项 - 实验性功能和调试 + AI 提供商、API 密鑰和模型設置 + 生成參數 + 溫度、採樣、提示詞和歌曲數據配置 + 備份與恢復 + 導出和恢復您的個人應用數據 + 開發者選項 + 實驗性功能和調試 均衡器 - 调整音频频率和预设 - 设备性能 - 音频规格、编解码器和解码器信息 - 账户 - 管理 Telegram、Google Drive、网易云音乐等服务 - 关于 - 应用信息、版本和鸣谢 + 調整音頻頻率和預設 + 設備性能 + 音頻規格、編解碼器和解碼器信息 + 賬戶 + 管理 Telegram、Google Drive、網易雲音樂等服務 + 關於 + 應用信息、版本和鳴謝 - - - 已启用 + + + 已啓用 已禁用 - 打开 - 全选 - 清除选择 - 关闭通知 + 打開 + 全選 + 清除選擇 + 關閉通知 - 媒体库结构 - 排除的目录 - 扫描媒体库时将跳过此处的文件夹。 - 艺术家 - 多艺术家解析和整理选项。 - 过滤 - 最小歌曲时长 - 每张专辑最少曲目数 - 专辑封面缓存限制 - 同步与扫描 - 刷新媒体库 - 扫描整个媒体库以查找新增和修改的文件。 - 完全重新扫描 - 正在运行完全重新扫描 - 已开始完全重新扫描… - 媒体库同步完成 - 重建数据库 - 重建数据库? - 这将从头开始完全重建您的音乐库。所有导入的歌词、收藏和自定义元数据都将丢失。此操作无法撤销。 + 媒體庫結構 + 排除的目錄 + 掃描媒體庫時將跳過此處的文件夾。 + 藝術家 + 多藝術家解析和整理選項。 + 過濾 + 最小歌曲時長 + 每張專輯最少曲目數 + 專輯封面緩存限制 + 同步與掃描 + 刷新媒體庫 + 掃描整個媒體庫以查找新增和修改的文件。 + 完全重新掃描 + 正在運行完全重新掃描 + 已開始完全重新掃描… + 媒體庫同步完成 + 重建數據庫 + 重建數據庫? + 這將從頭開始完全重建您的音樂庫。所有導入的歌詞、收藏和自定義元數據都將丟失。此操作無法撤銷。 重建 - 正在重建数据库 - 正在重建数据库… - 自动扫描 .lrc 文件 - 在同步媒体库时,自动扫描并分配同一文件夹中的 .lrc 文件。 - 歌词管理 - 歌词来源优先级 - 选择获取歌词时优先尝试的来源。 - 内嵌优先 - 在线优先 - 本地 (.lrc) 优先 - 重置已导入的歌词 - 从数据库中移除所有已导入的歌词。 - 重置已导入的歌词? - 此操作无法撤销。 + 正在重建數據庫 + 正在重建數據庫… + 自動掃描 .lrc 文件 + 在同步媒體庫時,自動掃描並分配同一文件夾中的 .lrc 文件。 + 歌詞管理 + 歌詞來源優先級 + 選擇獲取歌詞時優先嚐試的來源。 + 內嵌優先 + 在線優先 + 本地 (.lrc) 優先 + 重置已導入的歌詞 + 從數據庫中移除所有已導入的歌詞。 + 重置已導入的歌詞? + 此操作無法撤銷。 刷新 - 默认允许所有内容。点击文件夹将其标记为免扫描。 - 此处无子文件夹 - 向上导航 - 转到根目录 + 默認允許所有內容。點擊文件夾將其標記爲免掃描。 + 此處無子文件夾 + 向上導航 + 轉到根目錄 - 需要重新扫描 - 艺术家设置已更改。请重新扫描媒体库以应用更改。 - 重新扫描 - 正在扫描… - 多艺术家解析 + 需要重新掃描 + 藝術家設置已更改。請重新掃描媒體庫以應用更改。 + 重新掃描 + 正在掃描… + 多藝術家解析 字符分隔符 - 当前:%1$s - 单词分隔符 - - 当前:%1$s + 當前:%1$s + 單詞分隔符 + + 當前:%1$s 配置 - 从标题提取艺术家 - 检测歌曲标题中的 feat.、ft.、with - 媒体库整理 - 按专辑艺术家分组 - 在主要艺术家下显示合作专辑 - 关于多艺术家解析 + 從標題提取藝術家 + 檢測歌曲標題中的 feat.、ft.、with + 媒體庫整理 + 按專輯藝術家分組 + 在主要藝術家下顯示合作專輯 + 關於多藝術家解析 - PixelPlayer 使用字符分隔符(/、;、&)和单词分隔符(feat.、ft.、vs.、x)来拆分艺术家标签。单词分隔符不区分大小写。 - “从标题提取艺术家”会检测歌曲标题中如 (feat. Artist) 的模式。 - 反斜杠 (\) 可用于转义字符分隔符。 + PixelPlayer 使用字符分隔符(/、;、&)和單詞分隔符(feat.、ft.、vs.、x)來拆分藝術家標籤。單詞分隔符不區分大小寫。 + “從標題提取藝術家”會檢測歌曲標題中如 (feat. Artist) 的模式。 + 反斜槓 (\) 可用於轉義字符分隔符。 示例 \u2192 @@ -112,541 +116,550 @@ \"Song (ft. B)\" by A A, B \"AC\\DC\" - AC/DC(已转义) + AC/DC(已轉義) 分隔符 - 当前分隔符 - 点击分隔符将其移除。至少需要一个分隔符。 + 當前分隔符 + 點擊分隔符將其移除。至少需要一個分隔符。 添加新分隔符 例如:/ 或 ; 添加分隔符 - 默认分隔符 + 默認分隔符 重置分隔符? - 这将清除您所有自定义的分隔符并恢复默认设置。此操作无法撤销。 - 分隔符已重置为默认 - 至少需要一个分隔符 + 這將清除您所有自定義的分隔符並恢復默認設置。此操作無法撤銷。 + 分隔符已重置爲默認 + 至少需要一個分隔符 已添加分隔符 - 分隔符已存在或无效 + 分隔符已存在或無效 空格 - 单词分隔符 - 当前单词分隔符 - 当这些关键词前后有空格时,会分割艺术家名称。匹配不区分大小写。点击以移除。 - 未配置单词分隔符 - 添加新单词分隔符 + 單詞分隔符 + 當前單詞分隔符 + 當這些關鍵詞前後有空格時,會分割藝術家名稱。匹配不區分大小寫。點擊以移除。 + 未配置單詞分隔符 + 添加新單詞分隔符 例如:feat. 或 ft. - 添加单词分隔符 - 单词分隔符的工作原理 - 单词分隔符不区分大小写,且要求前后有空格。\n\n单字符分隔符(如 \"x\")需要在两侧都留有空格以避免误匹配。\n\n示例:\n \"Drake feat. Rihanna\" -> Drake, Rihanna\n \"Marshmello x Bastille\" -> Marshmello, Bastille\n \"A vs. B\" -> A, B - 重置单词分隔符? - 这将清除您所有自定义的单词分隔符并恢复默认关键词。此操作无法撤销。 - 已添加单词分隔符 - 已存在或无效 - 单词分隔符已重置为默认 + 添加單詞分隔符 + 單詞分隔符的工作原理 + 單詞分隔符不區分大小寫,且要求前後有空格。\n\n單字符分隔符(如 \"x\")需要在兩側都留有空格以避免誤匹配。\n\n示例:\n \"Drake feat. Rihanna\" -> Drake, Rihanna\n \"Marshmello x Bastille\" -> Marshmello, Bastille\n \"A vs. B\" -> A, B + 重置單詞分隔符? + 這將清除您所有自定義的單詞分隔符並恢復默認關鍵詞。此操作無法撤銷。 + 已添加單詞分隔符 + 已存在或無效 + 單詞分隔符已重置爲默認 - 正在准备同步 - 正在读取 MediaStore - 正在处理曲目 - 正在保存到数据库 - 正在扫描歌词文件 - 正在清理专辑封面缓存 - 正在同步云端源 + 正在準備同步 + 正在讀取 MediaStore + 正在處理曲目 + 正在保存到數據庫 + 正在掃描歌詞文件 + 正在清理專輯封面緩存 + 正在同步雲端源 正在完成同步 %1$s • %2$d%% (%3$d/%4$d) %1$s… - 全局主题 - 应用语言 - 选择应用界面的语言。 - 跟随系统 - 英语 - 西班牙语 - 德语 - 法语 - 俄语 - 简体中文 + 全局主題 + 應用語言 + 選擇應用界面的語言。 + 跟隨系統 + 英語 + 西班牙語 + 德語 + 法語 + 俄語 + 簡體中文 繁體中文 日語 越南語 印尼語 - 意大利语 - 韩语 - 挪威语(Bokmål) - 土耳其语 - 应用主题 - 在浅色、深色之间切换,或跟随系统外观。 - 浅色主题 - 深色主题 - 跟随系统 - 使用平滑圆角 - 使用复杂的圆角形状以有效提升美观度,但可能会影响低端设备的性能 - 关闭模糊效果 - 在应用中关闭模糊效果以节省电量和资源。 - 显示滚动条 - 在音乐列表中显示侧边滚动条,便于快速滚动。 + 意大利語 + 韓語 + 挪威語(Bokmål) + 土耳其語 + 應用主題 + 在淺色、深色之間切換,或跟隨系統外觀。 + 淺色主題 + 深色主題 + 跟隨系統 + 使用平滑圓角 + 使用複雜的圓角形狀以有效提升美觀度,但可能會影響低端設備的性能 + 關閉模糊效果 + 在應用中關閉模糊效果以節省電量和資源。 + 顯示滾動條 + 在音樂列表中顯示側邊滾動條,便於快速滾動。 正在播放 - 播放器主题 - 选择悬浮播放器的外观。 - 专辑封面 - 系统动态主题 - 显示播放器文件信息 - 在播放器进度部分显示编解码器、比特率和采样率。 - 专辑封面调色板样式 - 当前:%1$s。打开实时预览并选择样式。 - 轮播图样式 - 选择专辑轮播图的外观。 - 无预览 (No Peek) - 单预览 (One Peek) - 首页拼图 - 拼图模式 - 为“你的推荐”拼图选择形状排列方式。 - 自动轮换模式 - 每次访问首页时循环切换拼图模式。 - 导航栏 - 导航栏样式 - 选择导航栏的外观。 - 默认 - 全宽 - 紧凑模式 - 仅显示图标并降低导航栏高度。 - 导航栏圆角半径 - 调整导航栏的圆角半径。 - 歌词界面 - 沉浸式歌词 - 自动隐藏控件并放大文本。 - 自动隐藏延迟 - 控件隐藏前的时间。 + 播放器主題 + 選擇懸浮播放器的外觀。 + 專輯封面 + 系統動態主題 + 顯示播放器文件信息 + 在播放器進度部分顯示編解碼器、比特率和採樣率。 + 專輯封面調色板樣式 + 當前:%1$s。打開實時預覽並選擇樣式。 + 輪播圖樣式 + 選擇專輯輪播圖的外觀。 + 無預覽 (No Peek) + 單預覽 (One Peek) + 首頁拼圖 + 拼圖模式 + 爲“你的推薦”拼圖選擇形狀排列方式。 + 自動輪換模式 + 每次訪問首頁時循環切換拼圖模式。 + 導航欄 + 導航欄樣式 + 選擇導航欄的外觀。 + 默認 + 全寬 + 緊湊模式 + 僅顯示圖標並降低導航欄高度。 + 導航欄圓角半徑 + 調整導航欄的圓角半徑。 + 歌詞界面 + 沉浸式歌詞 + 自動隱藏控件並放大文本。 + 自動隱藏延遲 + 控件隱藏前的時間。 3秒 4秒 5秒 6秒 - 应用导航 - 默认选项卡 - 选择默认启动时的选项卡。 - 首页 + 應用導航 + 默認選項卡 + 選擇默認啓動時的選項卡。 + 首頁 搜索 - 媒体库 - 媒体库导航 - 选择在媒体库选项卡之间移动的方式。 - 标签栏(默认) - 紧凑胶囊与网格 + 媒體庫 + 媒體庫導航 + 選擇在媒體庫選項卡之間移動的方式。 + 標籤欄(默認) + 緊湊膠囊與網格 - 颜色 - 调色板样式 - 为播放器界面选择专辑颜色。 + 顏色 + 調色板樣式 + 爲播放器界面選擇專輯顏色。 柔和 (Tonal Spot) - 平衡且平静。 - 鲜艳 (Vibrant) - 高饱和度点缀。 - 表现力 (Expressive) - 大胆的色调变化和对比。 + 平衡且平靜。 + 鮮豔 (Vibrant) + 高飽和度點綴。 + 表現力 (Expressive) + 大膽的色調變化和對比。 混搭 (Fruit Salad) - 俏皮的旋转点缀。 - 色彩准确度 - 0 表示保持当前调整。值越高越接近专辑封面的主色调。 - 当前 - 更准确 - 0 • 当前 + 俏皮的旋轉點綴。 + 色彩準確度 + 0 表示保持當前調整。值越高越接近專輯封面的主色調。 + 當前 + 更準確 + 0 • 當前 %1$d • 微妙 %1$d • 平衡 - %1$d • 精确 + %1$d • 精確 - 调整圆角半径 - 使导航栏形状的圆角与设备的物理圆角相匹配,以获得无缝的外观。 - 圆角半径 + 調整圓角半徑 + 使導航欄形狀的圓角與設備的物理圓角相匹配,以獲得無縫的外觀。 + 圓角半徑 %1$d dp - 后台播放 - 关闭后继续播放 - 如果关闭此项,从最近任务中移除应用将停止播放。 - 电池优化 - 禁用电池优化以防止播放中断。 - 电池优化已被禁用 - 无法打开电池设置 - 音量标准化 (ReplayGain) - 启用 ReplayGain - 使用音频文件中的 ReplayGain 元数据标准化音量。 + 後臺播放 + 關閉後繼續播放 + 如果關閉此項,從最近任務中移除應用將停止播放。 + 電池優化 + 禁用電池優化以防止播放中斷。 + 電池優化已被禁用 + 無法打開電池設置 + 音量標準化 (ReplayGain) + 啓用 ReplayGain + 使用音頻文件中的 ReplayGain 元數據標準化音量。 增益模式 - 曲目:对每首歌曲进行标准化。专辑:按专辑进行标准化。 + 曲目:對每首歌曲進行標準化。專輯:按專輯進行標準化。 曲目 - 专辑 + 專輯 投屏 - 连接/断开投屏时自动播放 - 切换投屏连接后立即开始播放。 - 耳机 - 耳机重新连接时恢复播放 - 如果播放因拔出耳机而暂停,则在重新连接时自动恢复。 - 队列与过渡 + 連接/斷開投屏時自動播放 + 切換投屏連接後立即開始播放。 + 耳機 + 耳機重新連接時恢復播放 + 如果播放因拔出耳機而暫停,則在重新連接時自動恢復。 + 隊列與過渡 淡入淡出 - 启用歌曲之间的平滑过渡。 - 淡入淡出时长 + 啓用歌曲之間的平滑過渡。 + 淡入淡出時長 Hi-Fi 模式 - 浮点 32 位音频输出。如果您的设备播放卡顿,请禁用此项。 - 此设备不支持(PCM_FLOAT AudioTrack 不可用)。 - 持久随机播放 - 即使关闭应用后也能记住随机播放设置。 - 显示队列历史记录 - 在队列中显示以前播放过的歌曲。 + 浮點 32 位音頻輸出。如果您的設備播放卡頓,請禁用此項。 + 此設備不支持(PCM_FLOAT AudioTrack 不可用)。 + 持久隨機播放 + 即使關閉應用後也能記住隨機播放設置。 + 顯示隊列歷史記錄 + 在隊列中顯示以前播放過的歌曲。 - 文件夹 - 返回手势控制文件夹 - 在“文件夹”选项卡中,系统返回手势会在离开媒体库之前导航文件夹层级。 - 播放器手势 - 点击背景关闭播放器 - 点击模糊的背景以关闭播放器面板。 - 触感反馈 - 触感反馈 - 在整个应用中启用震动反馈。 + 文件夾 + 返回手勢控制文件夾 + 在“文件夾”選項卡中,系統返回手勢會在離開媒體庫之前導航文件夾層級。 + 播放器手勢 + 點擊背景關閉播放器 + 點擊模糊的背景以關閉播放器面板。 + 觸感反饋 + 觸感反饋 + 在整個應用中啓用震動反饋。 AI 提供商 提供商 - 选择您的 AI 提供商 + 選擇您的 AI 提供商 安全 Token 模式 - 开启 — 快速且低成本。向 AI 发送最少的数据(约 1K token)。 - 关闭 — 深度上下文。发送完整的听歌偏好资料(约 8K token)以获得更丰富的结果。 - 凭据 - %1$s API 密钥 - 从 %1$s 获取 + 開啓 — 快速且低成本。向 AI 發送最少的數據(約 1K token)。 + 關閉 — 深度上下文。發送完整的聽歌偏好資料(約 8K token)以獲得更豐富的結果。 + 憑據 + %1$s API 密鑰 + 從 %1$s 獲取 Google AI Studio (aistudio.google.com) - DeepSeek 平台 (api.deepseek.com) - Groq 控制台 (console.groq.com) - Mistral AI 平台 (console.mistral.ai) + DeepSeek 平臺 (api.deepseek.com) + Groq 控制檯 (console.groq.com) + Mistral AI 平臺 (console.mistral.ai) NVIDIA Build (build.nvidia.com) - 月之暗面 (Moonshot) AI 平台 (platform.moonshot.cn) - 智谱 AI 开放平台 (bigmodel.cn) - OpenAI 平台 (platform.openai.com) - 模型选择 - 正在加载可用模型… - 加载模型失败 + 月之暗面 (Moonshot) AI 平臺 (platform.moonshot.cn) + 智譜 AI 開放平臺 (bigmodel.cn) + OpenAI 平臺 (platform.openai.com) + 模型選擇 + 正在加載可用模型… + 加載模型失敗 AI 模型 - 选择一个模型。 - 输入 API 密钥 - 提示词行为 - 系统提示词 (System Prompt) - 自定义 AI 的行为方式。 - 预设提示词 - 输入系统提示词… - 专业策展人 (Professional Curator) - 你是“Vibe-Engine”,一位世界级的音乐策展人和声音心流大师。你的目标是构建无缝、高保真的听觉体验。请优先考虑和声兼容性、符合逻辑的 BPM 过渡,并在熟悉的挚爱与富有品味、逻辑驱动的发现之间取得精妙平衡。 - 创意先锋 (Creative Maverick) - 你是一位前卫的音乐探索者,专注于“意想不到的凝聚力”。你的任务是通过识别非显而易见的声音共性来打破传统的流派界限。在保持令人惊讶但不可否认的过渡逻辑的同时,优先考虑罕见的深度曲目、实验性纹理和艺术新意。 - 严谨图书管理员 (Strict Librarian) - 你是一位精准的音乐数据库架构师。你的逻辑由绝对的元数据精确性和严格的分类界限所驱动。尽量减少算法发现,优先保持严格的流派一致性、能量水平匹配,并最大程度地检索出用户定义的高度具体的偏好。 - 氛围向导 (Atmospheric Guide) - 你是环境纹理和低能量心流的大师。专心挑选能够促进“深度专注”或“宁静”状态的曲目。优先选择原声的温暖感、极简主义的编曲和柔和的过渡,同时严格避免瞬态强烈的声音或动态范围的突然转变。 - 声音狂热者 (Sonic Enthusiast) - 你是一位专注于制作复杂性和配器的发烧友分析师。优先选择具有高动态范围、复杂多重节奏和卓越声场质量的曲目。偏好那些奖励听众关注技术保真度和编曲细节的主动聆听作品。 - 能量催化剂 (Energy Catalyst) - 你是一个高动量的节奏生成器。你的哲学以驱动感强的低音线、强烈的打击乐和极具感染力的律动为中心。优先考虑高 BPM 的夜店兼容性、切分音能量和持续的节奏张力,以使听众的心率和动力保持在最高水平。 - AI 使用报告 - 总消耗量 - %1$s token 追踪\n提示:%2$s | 输出:%3$s | 思考:%4$s - 清除日志 - AI 活动日志 (%1$d) + 選擇一個模型。 + 輸入 API 密鑰 + 提示詞行爲 + 系統提示詞 (System Prompt) + 自定義 AI 的行爲方式。 + 預設提示詞 + 輸入系統提示詞… + 專業策展人 (Professional Curator) + 你是“Vibe-Engine”,一位世界級的音樂策展人和聲音心流大師。你的目標是構建無縫、高保真的聽覺體驗。請優先考慮和聲兼容性、符合邏輯的 BPM 過渡,並在熟悉的摯愛與富有品味、邏輯驅動的發現之間取得精妙平衡。 + 創意先鋒 (Creative Maverick) + 你是一位前衛的音樂探索者,專注於“意想不到的凝聚力”。你的任務是通過識別非顯而易見的聲音共性來打破傳統的流派界限。在保持令人驚訝但不可否認的過渡邏輯的同時,優先考慮罕見的深度曲目、實驗性紋理和藝術新意。 + 嚴謹圖書管理員 (Strict Librarian) + 你是一位精準的音樂數據庫架構師。你的邏輯由絕對的元數據精確性和嚴格的分類界限所驅動。儘量減少算法發現,優先保持嚴格的流派一致性、能量水平匹配,並最大程度地檢索出用戶定義的高度具體的偏好。 + 氛圍嚮導 (Atmospheric Guide) + 你是環境紋理和低能量心流的大師。專心挑選能夠促進“深度專注”或“寧靜”狀態的曲目。優先選擇原聲的溫暖感、極簡主義的編曲和柔和的過渡,同時嚴格避免瞬態強烈的聲音或動態範圍的突然轉變。 + 聲音狂熱者 (Sonic Enthusiast) + 你是一位專注於製作複雜性和配器的發燒友分析師。優先選擇具有高動態範圍、複雜多重節奏和卓越聲場質量的曲目。偏好那些獎勵聽衆關注技術保真度和編曲細節的主動聆聽作品。 + 能量催化劑 (Energy Catalyst) + 你是一個高動量的節奏生成器。你的哲學以驅動感強的低音線、強烈的打擊樂和極具感染力的律動爲中心。優先考慮高 BPM 的夜店兼容性、切分音能量和持續的節奏張力,以使聽衆的心率和動力保持在最高水平。 + AI 使用報告 + 總消耗量 + %1$s token 追蹤\n提示:%2$s | 輸出:%3$s | 思考:%4$s + 清除日誌 + AI 活動日誌 (%1$d) %1$s · %2$s - 显示 - 隐藏 + 顯示 + 隱藏 - 备份的工作原理 - 选择版块,导出一个 .pxpl 文件,并在稍后导入它。恢复操作仅替换您选择的版块。 - 创建备份 - 导出备份 - 未选择版块。 - 已选择所有版块。 - 已选择 %1$d 个版块,共 %2$d 个。 - %1$s 创建一个 .pxpl 备份文件。 - 选择并导出 - 恢复备份 - 导入备份 - 选择并恢复 - 浏览或从最近的备份中选择。所选数据将替换当前数据。 + 備份的工作原理 + 選擇版塊,導出一個 .pxpl 文件,並在稍後導入它。恢復操作僅替換您選擇的版塊。 + 創建備份 + 導出備份 + 未選擇版塊。 + 已選擇所有版塊。 + 已選擇 %1$d 個版塊,共 %2$d 個。 + %1$s 創建一個 .pxpl 備份文件。 + 選擇並導出 + 恢復備份 + 導入備份 + 選擇並恢復 + 瀏覽或從最近的備份中選擇。所選數據將替換當前數據。 - 确切选择您想包含在备份包中的内容。 - 选择一个 .pxpl 备份文件进行检查。您将在下一步选择要恢复的版块。 - 已选择 %1$d 个版块,共 %2$d 个 - 已选择 %1$d 个模块,共 %2$d 个 - 最近的备份 - 没有最近的备份 - 以前导入的备份将显示在这里。 - %1$d 个条目 · 将替换当前数据 - 导出 .pxpl - 恢复所选项 - 正在传输… + 確切選擇您想包含在備份包中的內容。 + 選擇一個 .pxpl 備份文件進行檢查。您將在下一步選擇要恢復的版塊。 + 已選擇 %1$d 個版塊,共 %2$d 個 + 已選擇 %1$d 個模塊,共 %2$d 個 + 最近的備份 + 沒有最近的備份 + 以前導入的備份將顯示在這裏。 + %1$d 個條目 · 將替換當前數據 + 導出 .pxpl + 恢復所選項 + 正在傳輸… PixelPlayer_Backup_%1$d.pxpl - 正在创建备份 - 正在恢复备份 + 正在創建備份 + 正在恢復備份 %1$d%% %1$s • %2$s - 正在导出 - 正在导入 - 正在恢复 - 从历史记录中移除 - 正在检查… - 浏览文件 + 正在導出 + 正在導入 + 正在恢復 + 從歷史記錄中移除 + 正在檢查… + 瀏覽文件 第 %1$d 步,共 %2$d 步 - 恢复模块 - 备份详情 - 创建时间 - 应用版本 - 架构 (Schema) - 设备 + 恢復模塊 + 備份詳情 + 創建時間 + 應用版本 + 架構 (Schema) + 設備 未知 · %1$s - %1$d 个模块 · v%2$s · 架构 v%3$d + %1$d 個模塊 · v%2$s · 架構 v%3$d \? - 全选 - 取消全选 + 全選 + 取消全選 - 无效备份:%1$s - 正在准备恢复 - 正在启动恢复任务。 - 正在准备备份 - 正在启动备份任务。 - 备份恢复成功 - 恢复完成,但存在一些未解决的问题。 - 无法完成恢复:%1$s - 恢复失败:%1$s - 数据导出成功 - 导出失败:%1$s - 数据恢复成功 - 恢复完成,存在未解决的问题。失败:%1$s + 無效備份:%1$s + 正在準備恢復 + 正在啓動恢復任務。 + 正在準備備份 + 正在啓動備份任務。 + 備份恢復成功 + 恢復完成,但存在一些未解決的問題。 + 無法完成恢復:%1$s + 恢復失敗:%1$s + 數據導出成功 + 導出失敗:%1$s + 數據恢復成功 + 恢復完成,存在未解決的問題。失敗:%1$s v%1$d %1$s %2$s - 实验性功能 - 实验性 - 播放器界面加载实验和开关。 - 测试设置流程 - 启动新手引导设置界面进行测试。 - 维护 - 强制重新生成 Daily Mix - 立即重新创建每日推荐播放列表。 + 實驗性功能 + 實驗性 + 播放器界面加載實驗和開關。 + 測試設置流程 + 啓動新手引導設置界面進行測試。 + 維護 + 強制重新生成 Daily Mix + 立即重新創建每日推薦播放列表。 重新生成 Daily Mix 重新生成 Daily Mix? - 这将丢弃当前的推荐,并根据最近的听歌习惯生成一个新的。 - Daily Mix 重新生成已开始 - 强制重新生成统计数据 - 清除缓存并重新计算播放统计数据。 + 這將丟棄當前的推薦,並根據最近的聽歌習慣生成一個新的。 + Daily Mix 重新生成已開始 + 強制重新生成統計數據 + 清除緩存並重新計算播放統計數據。 重新生成 - 正在处理… - 重新生成统计数据 - 重新生成统计数据? - 这将清除统计数据缓存并强制从数据库历史记录重新计算。 - 统计数据重新生成已开始 - 强制重新生成专辑调色板 - 重建每张专辑封面的所有缓存调色板变体,或选择单个进行刷新。 + 正在處理… + 重新生成統計數據 + 重新生成統計數據? + 這將清除統計數據緩存並強制從數據庫歷史記錄重新計算。 + 統計數據重新生成已開始 + 強制重新生成專輯調色板 + 重建每張專輯封面的所有緩存調色板變體,或選擇單個進行刷新。 全部重新生成 - 重新生成所有专辑调色板? - 这将清除缓存的主题数据,并重建 %1$d 个唯一专辑封面的所有调色板样式。 + 重新生成所有專輯調色板? + 這將清除緩存的主題數據,並重建 %1$d 個唯一專輯封面的所有調色板樣式。 正在重新生成… - 正在重新生成专辑调色板… - 正在重建 %1$d 个唯一专辑封面的缓存调色板变体。在大型媒体库中这可能需要一些时间。 - 已完成 %1$d 个,共 %2$d 个 - 已重新生成 %1$d 个专辑封面调色板 - 已重新生成 %1$d 个(共 %2$d 个)专辑封面调色板 - 选择歌曲 - 选择一首歌曲以清除缓存的主题数据,并从专辑封面重新生成所有调色板样式。 - 按标题、艺术家或专辑搜索 - 没有与您搜索匹配的歌曲。 - 未找到带有专辑封面的歌曲。 - 正在重新生成调色板… - 已为 %1$s 重新生成调色板 - 无法为 %1$s 重新生成调色板 - 诊断 - 触发测试崩溃 - 模拟一次崩溃以测试崩溃报告系统。 - 测试崩溃是从开发者选项触发的 - 这是为了测试崩溃报告系统的有意行为 + 正在重新生成專輯調色板… + 正在重建 %1$d 個唯一專輯封面的緩存調色板變體。在大型媒體庫中這可能需要一些時間。 + 已完成 %1$d 個,共 %2$d 個 + 已重新生成 %1$d 個專輯封面調色板 + 已重新生成 %1$d 個(共 %2$d 個)專輯封面調色板 + 選擇歌曲 + 選擇一首歌曲以清除緩存的主題數據,並從專輯封面重新生成所有調色板樣式。 + 按標題、藝術家或專輯搜索 + 沒有與您搜索匹配的歌曲。 + 未找到帶有專輯封面的歌曲。 + 正在重新生成調色板… + 已爲 %1$s 重新生成調色板 + 無法爲 %1$s 重新生成調色板 + 診斷 + 觸發測試崩潰 + 模擬一次崩潰以測試崩潰報告系統。 + 測試崩潰是從開發者選項觸發的 - 這是爲了測試崩潰報告系統的有意行爲 - 实验性 - 播放器界面加载调整 - 动画歌词(高端设备) - 为歌词使用弹簧动画和视觉效果。在低端设备上可能导致掉帧。 - 歌词模糊效果 - 对非活动歌词应用景深模糊。 - 模糊强度 - 调整模糊效果的强度。 + 實驗性 + 播放器界面加載調整 + 動畫歌詞(高端設備) + 爲歌詞使用彈簧動畫和視覺效果。在低端設備上可能導致掉幀。 + 歌詞模糊效果 + 對非活動歌詞應用景深模糊。 + 模糊強度 + 調整模糊效果的強度。 %1$.1fx - 第 1 步 · 选择要延迟的内容 - 延迟所有内容 - 在面板背景完全展开之前,保留所有播放器内容。 - 专辑轮播 - 在面板展开前延迟专辑封面和轮播图。 - 歌曲元数据 - 延迟标题、艺术家以及歌词/队列操作。 - 进度条 - 在展开完成之前延迟时间轴和时间标签。 + 第 1 步 · 選擇要延遲的內容 + 延遲所有內容 + 在面板背景完全展開之前,保留所有播放器內容。 + 專輯輪播 + 在面板展開前延遲專輯封面和輪播圖。 + 歌曲元數據 + 延遲標題、藝術家以及歌詞/隊列操作。 + 進度條 + 在展開完成之前延遲時間軸和時間標籤。 播放控制 - 延迟播放/暂停、快进/快退和收藏控件。 - 所有延迟组件均处于活动状态。禁用“延迟所有内容”以自定义各个部分。 - 第 2 步 · 配置占位图行为 - 对延迟项使用占位图 - 在组件等待展开时渲染轻量级占位图,以保持布局稳定。 - 第 3 步 · 选择占位图切换为真实内容的时机 - 选择一种模式。阈值模式使用滑块;拖动释放模式等待直到您释放面板手势。 - 启用至少一个延迟组件以解锁触发模式。 - 阈值 - 使用展开百分比。 - 拖动释放 - 仅在手势释放后切换。 - 展开阈值 - 在延迟组件变得可见之前,面板必须展开的程度。 - 内容在 %1$d%% 展开时出现 - 在播放器关闭时也应用 - 使用关闭阈值在折叠时切换回占位图。 - 关闭阈值 - 在占位图再次接管之前需要折叠多少程度。 - 占位图在折叠 %1$d%% 后出现 - 拖动释放模式会绕过阈值和关闭行为。交换仅在面板拖动手势结束时发生。 - 使占位图透明 - 占位图保留其布局空间,但变为不可见。 - 视觉质量 - 专辑封面分辨率 + 延遲播放/暫停、快進/快退和收藏控件。 + 所有延遲組件均處於活動狀態。禁用“延遲所有內容”以自定義各個部分。 + 第 2 步 · 配置佔位圖行爲 + 對延遲項使用佔位圖 + 在組件等待展開時渲染輕量級佔位圖,以保持佈局穩定。 + 第 3 步 · 選擇佔位圖切換爲真實內容的時機 + 選擇一種模式。閾值模式使用滑塊;拖動釋放模式等待直到您釋放面板手勢。 + 啓用至少一個延遲組件以解鎖觸發模式。 + 閾值 + 使用展開百分比。 + 拖動釋放 + 僅在手勢釋放後切換。 + 展開閾值 + 在延遲組件變得可見之前,面板必須展開的程度。 + 內容在 %1$d%% 展開時出現 + 在播放器關閉時也應用 + 使用關閉閾值在摺疊時切換回佔位圖。 + 關閉閾值 + 在佔位圖再次接管之前需要摺疊多少程度。 + 佔位圖在摺疊 %1$d%% 後出現 + 拖動釋放模式會繞過閾值和關閉行爲。交換僅在面板拖動手勢結束時發生。 + 使佔位圖透明 + 佔位圖保留其佈局空間,但變爲不可見。 + 視覺質量 + 專輯封面分辨率 低 (256px) - 更好的性能 中等 (512px) - 均衡 - 高 (800px) - 最佳质量 - 原图 - 最高质量 + 高 (800px) - 最佳質量 + 原圖 - 最高質量 - 播放需检查 - 准备就绪可播放 + 播放需檢查 + 準備就緒可播放 -- 格式 - 硬件解码 + 硬件解碼 本地歌曲 - 本地音乐存储 - 音乐大小 + 本地音樂存儲 + 音樂大小 %1$d 首本地歌曲 - 可用空间 - 总计 %1$s - 音乐占用 - 设备已用 + 可用空間 + 總計 %1$s + 音樂佔用 + 設備已用 %1$d%% <1% - %1$d 首云端歌曲 - %1$d 个文件不可读 - 播放路径 + %1$d 首雲端歌曲 + %1$d 個文件不可讀 + 播放路徑 - 采样率 + 採樣率 %1$d Hz - 每个缓冲区 %1$d 帧 - Hi-Fi PCM 浮点 - 32 位浮点输出路径 - 低延迟支持 + 每個緩衝區 %1$d 幀 + Hi-Fi PCM 浮點 + 32 位浮點輸出路徑 + 低延遲支持 Pro Audio 支持 - 内存 - 可用(总计 %1$s) - 支持硬件卸载的格式 - 没有压缩格式报告支持硬件卸载 (hardware offload)。 - 还有 +%1$d 个 - 检测到的输出 - 内置输出 - 蓝牙音频 - USB 音频 - 有线音频 - 数字输出 - 其他输出 - Android 未报告任何输出路由。 + 內存 + 可用(總計 %1$s) + 支持硬件卸載的格式 + 沒有壓縮格式報告支持硬件卸載 (hardware offload)。 + 還有 +%1$d 個 + 檢測到的輸出 + 內置輸出 + 藍牙音頻 + USB 音頻 + 有線音頻 + 數字輸出 + 其他輸出 + Android 未報告任何輸出路由。 ExoPlayer 引擎 - %1$s 个渲染器 + %1$s 個渲染器 格式兼容性 %1$d 首支持的曲目 - %1$d 个未知格式 - 未报告解码器 - 硬件解码器 - 软件解码器 - 硬件卸载 - 媒体库中有 %1$d 首 - 兼容性检测结果 - 无重大不兼容问题 - 您已索引的曲目与 Android 报告此设备支持的解码器相匹配。 - %1$d 首曲目可能无法原生解码 - 需检查的格式:%1$s。 - %1$d 首本地曲目可能会被重采样 - 媒体库采样率达到 %1$d Hz,高于当前的输出采样率。 - %1$d 首曲目具有未知的元数据 - 进行完整的媒体库重新扫描可以填补缺失的 MIME、比特率和采样率数据。 - 设备信息 - 制造商 - 型号 + %1$d 個未知格式 + 未報告解碼器 + 硬件解碼器 + 軟件解碼器 + 硬件卸載 + 媒體庫中有 %1$d 首 + 兼容性檢測結果 + 無重大不兼容問題 + 您已索引的曲目與 Android 報告此設備支持的解碼器相匹配。 + %1$d 首曲目可能無法原生解碼 + 需檢查的格式:%1$s。 + %1$d 首本地曲目可能會被重採樣 + 媒體庫採樣率達到 %1$d Hz,高於當前的輸出採樣率。 + %1$d 首曲目具有未知的元數據 + 進行完整的媒體庫重新掃描可以填補缺失的 MIME、比特率和採樣率數據。 + 設備信息 + 製造商 + 型號 品牌 - 设备 + 設備 Android 版本 SDK 版本 硬件 - 性能报告 - 生成一份可分享的诊断报告,以帮助我们分类播放或扫描卡顿问题。该报告仅包含设备、媒体库和计时数据 — 不包含文件路径、标题或艺术家。 - 生成报告 + 性能報告 + 生成一份可分享的診斷報告,以幫助我們分類播放或掃描卡頓問題。該報告僅包含設備、媒體庫和計時數據 — 不包含文件路徑、標題或藝術家。 + 生成報告 重新生成 - 复制 + 複製 分享 - 报告已复制到剪贴板 - PixelPlayer 性能报告 - 高级性能诊断 - 默认关闭。用于记录短暂卡顿时间线,便于测试版问题排查。 + 報告已複製到剪貼板 + PixelPlayer 性能報告 + 高級性能診斷 + 默認關閉。用於記錄短暫卡頓時間線,便於測試版問題排查。 有效期至 %1$s - 立即标记卡顿 - 已标记卡顿时刻 + 立即標記卡頓 + 已標記卡頓時刻 - 已连接的账户 - 管理关联的提供商并将每个集成功能掌握在自己手中。 - 已关联服务 - 活跃 + 已連接的賬戶 + 管理關聯的提供商並將每個集成功能掌握在自己手中。 + 已關聯服務 + 活躍 可用 - 敬请期待 - 已连接 - 敬请期待 - 打开服务 + 敬請期待 + 已連接 + 敬請期待 + 打開服務 正在登出… - 尚未关联任何账户 - 连接一个提供商即可在此界面对其进行管理。 - 连接 %1$s - %1$s(敬请期待) - Google Drive 功能即将推出。 - 目前无法打开此屏幕。 + 尚未關聯任何賬戶 + 連接一個提供商即可在此界面對其進行管理。 + 連接 %1$s + %1$s(敬請期待) + Google Drive 功能即將推出。 + 目前無法打開此屏幕。 - 关于 + 關於 PixelPlayer - 由社区共同打造的开源音乐播放器。 + 由社區共同打造的開源音樂播放器。 版本 v%1$s - 开源 - 社区优先 - Material 3 表现力设计 - 目前未找到贡献者。请稍后再试。 - 维护者 - PixelPlayer 背后的开发者。 - 社区焦点 - 感谢对本项目有重大影响的合作者。 - 开源贡献者 - 来自 GitHub 的实时贡献者名单。 - %1$d 次贡献 + 開源 + 社區優先 + Material 3 表現力設計 + 目前未找到貢獻者。請稍後再試。 + 維護者 + PixelPlayer 背後的開發者。 + 社區焦點 + 感謝對本項目有重大影響的合作者。 + 許可和通知 + 依賴項和早期貢獻的歸屬。 + 開源許可 + 依賴許可和第三方通知 + 開源許可 + MIT 風格通知 + 第三方通知 + 關閉 + 無法加載捆綁的第三方通知。 + 開源貢獻者 + 來自 GitHub 的實時貢獻者名單。 + %1$d 次貢獻 GitHub - 代码仓库 + 代碼倉庫 Telegram 支持 - 打开 GitHub 仓库 - 加入 Telegram 社区 - 打开 GitHub 个人资料 - 打开 Telegram - %1$s 的头像 - %1$s 的图标 + 打開 GitHub 倉庫 + 加入 Telegram 社區 + 打開 GitHub 個人資料 + 打開 Telegram + %1$s 的頭像 + %1$s 的圖標 音量 - 音量为零时暂停 - 当音量设置为 0 时自动暂停播放 + 音量爲零時暫停 + 當音量設置爲 0 時自動暫停播放 diff --git a/app/src/main/res/values-zh-rTW/strings_widget.xml b/app/src/main/res/values-zh-rTW/strings_widget.xml index 5a2fd3c03d..a4c0b8d567 100644 --- a/app/src/main/res/values-zh-rTW/strings_widget.xml +++ b/app/src/main/res/values-zh-rTW/strings_widget.xml @@ -1,17 +1,17 @@ - 可根据尺寸自适应的响应式小组件 - 紧凑型播放栏 - 包含随机和循环播放的完整控件 - 极简方形播放器 + 可根據尺寸自適應的響應式小組件 + 緊湊型播放欄 + 包含隨機和循環播放的完整控件 + 極簡方形播放器 - 点击打开 - 专辑封面 - 专辑封面占位图 + 點擊打開 + 專輯封面 + 專輯封面佔位圖 - 点击播放 - 歌曲标题 - 艺术家 + 點擊播放 + 歌曲標題 + 藝術家 - 进度条,%1$d%% + 進度條,%1$d%% \ No newline at end of file From 55746f68b3751a07a2e9a160496851218032ed42 Mon Sep 17 00:00:00 2001 From: Dae Euhwa Date: Wed, 29 Jul 2026 17:09:10 -0500 Subject: [PATCH 63/73] fix(ai): fix Ollama config - no API key required, correct default URL - Ollama is a local inference engine that does not require an API key - Changed default URL from https://api.ollama.ai/v1 to http://localhost:11434/v1 - Merged master to get MultiLangRomanizer detection methods (isThai, isArabic, etc.) --- .../theveloper/pixelplay/data/ai/provider/AiClientFactory.kt | 2 +- .../com/theveloper/pixelplay/data/ai/provider/AiProvider.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/theveloper/pixelplay/data/ai/provider/AiClientFactory.kt b/app/src/main/java/com/theveloper/pixelplay/data/ai/provider/AiClientFactory.kt index 6eec6d11e6..54a049f236 100644 --- a/app/src/main/java/com/theveloper/pixelplay/data/ai/provider/AiClientFactory.kt +++ b/app/src/main/java/com/theveloper/pixelplay/data/ai/provider/AiClientFactory.kt @@ -83,7 +83,7 @@ class AiClientFactory @Inject constructor( ) AiProvider.OLLAMA -> GenericOpenAiClient( apiKey = apiKey, - baseUrl = "https://api.ollama.ai/v1", + baseUrl = "http://localhost:11434/v1", defaultModelId = "llama3", providerName = provider.displayName, client = sharedHttpClient diff --git a/app/src/main/java/com/theveloper/pixelplay/data/ai/provider/AiProvider.kt b/app/src/main/java/com/theveloper/pixelplay/data/ai/provider/AiProvider.kt index 1b3caf0bd2..37417d1b0e 100644 --- a/app/src/main/java/com/theveloper/pixelplay/data/ai/provider/AiProvider.kt +++ b/app/src/main/java/com/theveloper/pixelplay/data/ai/provider/AiProvider.kt @@ -15,7 +15,7 @@ enum class AiProvider( GLM("Zhipu GLM", requiresApiKey = true, description = "GLM-4 — bilingual (Chinese/English) models by Zhipu AI"), OPENAI("OpenAI", requiresApiKey = true, description = "GPT-4o, GPT-4.1, o3, o4-mini — industry-standard models"), OPENROUTER("OpenRouter", requiresApiKey = true, description = "Single API for 300+ models across all major providers"), - OLLAMA("Ollama", requiresApiKey = true, hasConfigurableUrl = true, description = "Ollama-compatible API endpoint (configurable URL)"), + OLLAMA("Ollama", requiresApiKey = false, hasConfigurableUrl = true, description = "Ollama-compatible API endpoint (configurable URL)"), CUSTOM("Custom Provider", requiresApiKey = true, hasConfigurableUrl = true, description = "Any OpenAI-compatible API endpoint"); companion object { From cc36421ec4acc768baed64579ed9fc20a390aa42 Mon Sep 17 00:00:00 2001 From: Dae Euhwa Date: Wed, 29 Jul 2026 17:09:59 -0500 Subject: [PATCH 64/73] fix(perf): remove dead code (PlatformUtils.kt), merge master for all fixes - Removed unused PlatformUtils.kt (never imported/referenced) - Merged master which includes fix/compilation, fix/lyrics, feat/settings, feat/ai - All songCountFlow and getSongCountFlow references resolved --- .../pixelplay/utils/PlatformUtils.kt | 62 ------------------- 1 file changed, 62 deletions(-) delete mode 100644 app/src/main/java/com/theveloper/pixelplay/utils/PlatformUtils.kt diff --git a/app/src/main/java/com/theveloper/pixelplay/utils/PlatformUtils.kt b/app/src/main/java/com/theveloper/pixelplay/utils/PlatformUtils.kt deleted file mode 100644 index 817cd8697a..0000000000 --- a/app/src/main/java/com/theveloper/pixelplay/utils/PlatformUtils.kt +++ /dev/null @@ -1,62 +0,0 @@ -package com.theveloper.pixelplay.utils - -import android.os.Build - -object PlatformUtils { - fun isUbuntuTouch(): Boolean { - return try { - val properties = System.getProperties() - properties.getProperty("os.name", "") - .contains("ubuntu", ignoreCase = true) || - properties.getProperty("os.version", "") - .contains("ubuntu", ignoreCase = true) || - Build.DISPLAY.contains("ubuntu", ignoreCase = true) || - Build.HOST?.contains("ubuntu", ignoreCase = true) == true || - Build.FINGERPRINT?.contains("ubuntu", ignoreCase = true) == true || - Build.MODEL?.contains("ubuntu", ignoreCase = true) == true - } catch (_: Exception) { - false - } - } - - fun isWaydroid(): Boolean { - return try { - val properties = System.getProperties() - properties.getProperty("java.vendor.url", "") - .contains("waydroid", ignoreCase = true) || - Build.HOST?.contains("waydroid", ignoreCase = true) == true - } catch (_: Exception) { - false - } - } - - fun isRunningOnLinux(): Boolean { - return try { - val osName = System.getProperty("os.name") ?: "" - osName.contains("linux", ignoreCase = true) - } catch (_: Exception) { - false - } - } - - fun isEmulatedStorage(): Boolean { - return Build.DEVICE?.contains("generic", ignoreCase = true) == true || - Build.PRODUCT?.contains("generic", ignoreCase = true) == true || - Build.HARDWARE?.contains("goldfish", ignoreCase = true) == true || - Build.FINGERPRINT?.contains("generic", ignoreCase = true) == true - } - - val isLowRamDevice: Boolean get() = runCatching { - val activityManager = android.app.ActivityManager::class.java - .getMethod("isLowRamDevice") - activityManager.invoke(null) as? Boolean ?: false - }.getOrDefault(false) - - val totalMemoryMb: Long get() = runCatching { - val info = android.app.ActivityManager.MemoryInfo() - android.app.ActivityManager::class.java - .getMethod("getMemoryInfo", android.app.ActivityManager.MemoryInfo::class.java) - .invoke(null, info) - info.totalMem / (1024 * 1024) - }.getOrDefault(0L) -} From 1c896195cf652a6b0235fd85798551a622ba90d8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 29 Jul 2026 22:27:45 +0000 Subject: [PATCH 65/73] chore(deps): bump the gradle-dependencies group with 3 updates Bumps the gradle-dependencies group with 3 updates: androidx.constraintlayout:constraintlayout-compose, androidx.compose.material3:material3 and androidx.baselineprofile. Updates `androidx.constraintlayout:constraintlayout-compose` from 1.1.1 to 1.1.2 Updates `androidx.compose.material3:material3` from 1.5.0-alpha24 to 1.5.0-alpha25 Updates `androidx.baselineprofile` from 1.5.0-alpha07 to 1.5.0-beta01 --- updated-dependencies: - dependency-name: androidx.constraintlayout:constraintlayout-compose dependency-version: 1.1.2 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: gradle-dependencies - dependency-name: androidx.compose.material3:material3 dependency-version: 1.5.0-alpha25 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: gradle-dependencies - dependency-name: androidx.baselineprofile dependency-version: 1.5.0-beta01 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: gradle-dependencies ... Signed-off-by: dependabot[bot] --- gradle/libs.versions.toml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 221b502293..b3b1b193ac 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -12,7 +12,7 @@ codeview = "1.3.9" coilCompose = "2.7.0" composeMaterialIcons = "1.7.8" composeUi = "1.11.4" -constraintlayoutCompose = "1.1.1" +constraintlayoutCompose = "1.1.2" coreSplashscreen = "1.2.0" desugarJdkLibs = "2.1.5" duktapeAndroid = "1.4.0" @@ -36,7 +36,7 @@ lifecycleRuntimeKtx = "2.11.0" activityCompose = "1.13.0" composeBom = "2026.06.01" material = "1.14.0" -material3 = "1.5.0-alpha24" +material3 = "1.5.0-alpha25" media = "1.8.0" media3Session = "1.10.1" media3Transformer = "1.10.1" @@ -100,7 +100,7 @@ playServicesCastFramework = "22.3.1" navigationRuntimeKtx = "2.9.8" uiautomator = "2.4.0" benchmarkMacroJunit4 = "1.4.1" -baselineprofile = "1.5.0-alpha07" +baselineprofile = "1.5.0-beta01" profileinstaller = "1.4.1" pagingCommon = "3.3.6" From a26c3f8eb30f7a399c1d59982ff6bc721023ba72 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 31 Jul 2026 22:25:47 +0000 Subject: [PATCH 66/73] chore(deps): bump the github-actions group across 1 directory with 2 updates Bumps the github-actions group with 2 updates in the / directory: [actions/setup-java](https://github.com/actions/setup-java) and [github/codeql-action](https://github.com/github/codeql-action). Updates `actions/setup-java` from 5 to 5.6.0 - [Release notes](https://github.com/actions/setup-java/releases) - [Commits](https://github.com/actions/setup-java/compare/v5...v5.6.0) Updates `github/codeql-action` from 4 to 4.37.3 - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/v4...v4.37.3) --- updated-dependencies: - dependency-name: actions/setup-java dependency-version: 5.6.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: github-actions - dependency-name: github/codeql-action dependency-version: 4.37.3 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: github-actions ... Signed-off-by: dependabot[bot] --- .github/workflows/codeql.yml | 6 +++--- .github/workflows/nightly-apk.yml | 2 +- .github/workflows/phone-debug.yml | 2 +- .github/workflows/phone-release.yml | 2 +- .github/workflows/wearos-apk.yml | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 2841e1ae20..a11ced09ba 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -24,7 +24,7 @@ jobs: uses: actions/checkout@v7 - name: Set up JDK 21 - uses: actions/setup-java@v5 + uses: actions/setup-java@v5.6.0 with: distribution: 'temurin' java-version: '21' @@ -40,7 +40,7 @@ jobs: cache-provider: 'enhanced' - name: Initialize CodeQL - uses: github/codeql-action/init@v4 + uses: github/codeql-action/init@v4.37.3 with: languages: java-kotlin build-mode: manual @@ -49,6 +49,6 @@ jobs: run: ${{ github.workspace }}/gradlew :app:assembleDebug :wear:assembleDebug --no-daemon --no-build-cache - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v4 + uses: github/codeql-action/analyze@v4.37.3 with: category: "/language:java-kotlin" diff --git a/.github/workflows/nightly-apk.yml b/.github/workflows/nightly-apk.yml index c3b4a3bfef..e82b9d3f28 100644 --- a/.github/workflows/nightly-apk.yml +++ b/.github/workflows/nightly-apk.yml @@ -88,7 +88,7 @@ jobs: uses: actions/checkout@v7 - name: Set up JDK 21 - uses: actions/setup-java@v5 + uses: actions/setup-java@v5.6.0 with: distribution: temurin java-version: '21' diff --git a/.github/workflows/phone-debug.yml b/.github/workflows/phone-debug.yml index 4c54d81b15..c9d37e2b9c 100644 --- a/.github/workflows/phone-debug.yml +++ b/.github/workflows/phone-debug.yml @@ -21,7 +21,7 @@ jobs: uses: actions/checkout@v7 - name: Set up JDK 21 - uses: actions/setup-java@v5 + uses: actions/setup-java@v5.6.0 with: distribution: temurin java-version: '21' diff --git a/.github/workflows/phone-release.yml b/.github/workflows/phone-release.yml index 34774a81bf..6b26eb14d6 100644 --- a/.github/workflows/phone-release.yml +++ b/.github/workflows/phone-release.yml @@ -21,7 +21,7 @@ jobs: uses: actions/checkout@v7 - name: Set up JDK 21 - uses: actions/setup-java@v5 + uses: actions/setup-java@v5.6.0 with: distribution: temurin java-version: '21' diff --git a/.github/workflows/wearos-apk.yml b/.github/workflows/wearos-apk.yml index c0218dfa99..1bd8dfe1e6 100644 --- a/.github/workflows/wearos-apk.yml +++ b/.github/workflows/wearos-apk.yml @@ -21,7 +21,7 @@ jobs: uses: actions/checkout@v7 - name: Set up JDK 21 - uses: actions/setup-java@v5 + uses: actions/setup-java@v5.6.0 with: distribution: temurin java-version: '21' From 9be3422068df21ca6b2951181c048c9c9a7393a6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 4 Aug 2026 22:26:03 +0000 Subject: [PATCH 67/73] chore(deps): bump the github-actions group across 1 directory with 2 updates Bumps the github-actions group with 2 updates in the / directory: [actions/setup-java](https://github.com/actions/setup-java) and [github/codeql-action](https://github.com/github/codeql-action). Updates `actions/setup-java` from 5.6.0 to 5.7.0 - [Release notes](https://github.com/actions/setup-java/releases) - [Commits](https://github.com/actions/setup-java/compare/v5.6.0...v5.7.0) Updates `github/codeql-action` from 4.37.3 to 4.37.4 - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/v4.37.3...v4.37.4) --- updated-dependencies: - dependency-name: actions/setup-java dependency-version: 5.7.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: github-actions - dependency-name: github/codeql-action dependency-version: 4.37.4 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: github-actions ... Signed-off-by: dependabot[bot] --- .github/workflows/codeql.yml | 6 +++--- .github/workflows/nightly-apk.yml | 2 +- .github/workflows/phone-debug.yml | 2 +- .github/workflows/phone-release.yml | 2 +- .github/workflows/wearos-apk.yml | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index a11ced09ba..686149cc8f 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -24,7 +24,7 @@ jobs: uses: actions/checkout@v7 - name: Set up JDK 21 - uses: actions/setup-java@v5.6.0 + uses: actions/setup-java@v5.7.0 with: distribution: 'temurin' java-version: '21' @@ -40,7 +40,7 @@ jobs: cache-provider: 'enhanced' - name: Initialize CodeQL - uses: github/codeql-action/init@v4.37.3 + uses: github/codeql-action/init@v4.37.4 with: languages: java-kotlin build-mode: manual @@ -49,6 +49,6 @@ jobs: run: ${{ github.workspace }}/gradlew :app:assembleDebug :wear:assembleDebug --no-daemon --no-build-cache - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v4.37.3 + uses: github/codeql-action/analyze@v4.37.4 with: category: "/language:java-kotlin" diff --git a/.github/workflows/nightly-apk.yml b/.github/workflows/nightly-apk.yml index e82b9d3f28..c532ad1a18 100644 --- a/.github/workflows/nightly-apk.yml +++ b/.github/workflows/nightly-apk.yml @@ -88,7 +88,7 @@ jobs: uses: actions/checkout@v7 - name: Set up JDK 21 - uses: actions/setup-java@v5.6.0 + uses: actions/setup-java@v5.7.0 with: distribution: temurin java-version: '21' diff --git a/.github/workflows/phone-debug.yml b/.github/workflows/phone-debug.yml index c9d37e2b9c..8bec0f7e69 100644 --- a/.github/workflows/phone-debug.yml +++ b/.github/workflows/phone-debug.yml @@ -21,7 +21,7 @@ jobs: uses: actions/checkout@v7 - name: Set up JDK 21 - uses: actions/setup-java@v5.6.0 + uses: actions/setup-java@v5.7.0 with: distribution: temurin java-version: '21' diff --git a/.github/workflows/phone-release.yml b/.github/workflows/phone-release.yml index 6b26eb14d6..2378c9fd08 100644 --- a/.github/workflows/phone-release.yml +++ b/.github/workflows/phone-release.yml @@ -21,7 +21,7 @@ jobs: uses: actions/checkout@v7 - name: Set up JDK 21 - uses: actions/setup-java@v5.6.0 + uses: actions/setup-java@v5.7.0 with: distribution: temurin java-version: '21' diff --git a/.github/workflows/wearos-apk.yml b/.github/workflows/wearos-apk.yml index 1bd8dfe1e6..a09939e01d 100644 --- a/.github/workflows/wearos-apk.yml +++ b/.github/workflows/wearos-apk.yml @@ -21,7 +21,7 @@ jobs: uses: actions/checkout@v7 - name: Set up JDK 21 - uses: actions/setup-java@v5.6.0 + uses: actions/setup-java@v5.7.0 with: distribution: temurin java-version: '21' From b2447ed6f9bd4138b4b141c5b0770f5c2af50848 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 6 Aug 2026 22:31:56 +0000 Subject: [PATCH 68/73] chore(deps): bump the gradle-dependencies group across 1 directory with 5 updates Bumps the gradle-dependencies group with 5 updates in the / directory: | Package | From | To | | --- | --- | --- | | [com.google.genai:google-genai](https://github.com/googleapis/java-genai) | `1.63.0` | `1.64.0` | | [io.ktor:ktor-server-core-jvm](https://github.com/ktorio/ktor) | `3.5.1` | `3.5.2` | | [io.ktor:ktor-server-netty-jvm](https://github.com/ktorio/ktor) | `3.5.1` | `3.5.2` | | [io.ktor:ktor-server-cio-jvm](https://github.com/ktorio/ktor) | `3.5.1` | `3.5.2` | | [com.google.devtools.ksp](https://github.com/google/ksp) | `2.3.10` | `2.3.11` | Updates `com.google.genai:google-genai` from 1.63.0 to 1.64.0 - [Release notes](https://github.com/googleapis/java-genai/releases) - [Changelog](https://github.com/googleapis/java-genai/blob/main/CHANGELOG.md) - [Commits](https://github.com/googleapis/java-genai/compare/v1.63.0...v1.64.0) Updates `io.ktor:ktor-server-core-jvm` from 3.5.1 to 3.5.2 - [Release notes](https://github.com/ktorio/ktor/releases) - [Changelog](https://github.com/ktorio/ktor/blob/main/CHANGELOG.md) - [Commits](https://github.com/ktorio/ktor/compare/3.5.1...3.5.2) Updates `io.ktor:ktor-server-netty-jvm` from 3.5.1 to 3.5.2 - [Release notes](https://github.com/ktorio/ktor/releases) - [Changelog](https://github.com/ktorio/ktor/blob/main/CHANGELOG.md) - [Commits](https://github.com/ktorio/ktor/compare/3.5.1...3.5.2) Updates `io.ktor:ktor-server-cio-jvm` from 3.5.1 to 3.5.2 - [Release notes](https://github.com/ktorio/ktor/releases) - [Changelog](https://github.com/ktorio/ktor/blob/main/CHANGELOG.md) - [Commits](https://github.com/ktorio/ktor/compare/3.5.1...3.5.2) Updates `io.ktor:ktor-server-netty-jvm` from 3.5.1 to 3.5.2 - [Release notes](https://github.com/ktorio/ktor/releases) - [Changelog](https://github.com/ktorio/ktor/blob/main/CHANGELOG.md) - [Commits](https://github.com/ktorio/ktor/compare/3.5.1...3.5.2) Updates `io.ktor:ktor-server-cio-jvm` from 3.5.1 to 3.5.2 - [Release notes](https://github.com/ktorio/ktor/releases) - [Changelog](https://github.com/ktorio/ktor/blob/main/CHANGELOG.md) - [Commits](https://github.com/ktorio/ktor/compare/3.5.1...3.5.2) Updates `com.google.devtools.ksp` from 2.3.10 to 2.3.11 - [Release notes](https://github.com/google/ksp/releases) - [Commits](https://github.com/google/ksp/compare/2.3.10...2.3.11) --- updated-dependencies: - dependency-name: com.google.genai:google-genai dependency-version: 1.64.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: gradle-dependencies - dependency-name: io.ktor:ktor-server-core-jvm dependency-version: 3.5.2 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: gradle-dependencies - dependency-name: io.ktor:ktor-server-netty-jvm dependency-version: 3.5.2 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: gradle-dependencies - dependency-name: io.ktor:ktor-server-cio-jvm dependency-version: 3.5.2 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: gradle-dependencies - dependency-name: io.ktor:ktor-server-netty-jvm dependency-version: 3.5.2 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: gradle-dependencies - dependency-name: io.ktor:ktor-server-cio-jvm dependency-version: 3.5.2 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: gradle-dependencies - dependency-name: com.google.devtools.ksp dependency-version: 2.3.11 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: gradle-dependencies ... Signed-off-by: dependabot[bot] --- gradle/libs.versions.toml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index b3b1b193ac..489046a769 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -3,7 +3,7 @@ accompanistDrawablepainter = "0.37.3" aboutLibraries = "15.0.4" agp = "9.3.1" app = "1.7.0" -googleGenai = "1.63.0" +googleGenai = "1.64.0" googlePlayServicesCast = "22.3.1" animation = "1.11.4" appcompat = "1.7.1" @@ -22,7 +22,7 @@ graphicsShapes = "1.1.0" gson = "2.14.0" hiltAndroid = "2.60.1" hiltNavigationCompose = "1.4.0" -ktor = "3.5.1" +ktor = "3.5.2" kotlin = "2.4.10" coreKtx = "1.19.0" junit = "4.13.2" @@ -78,7 +78,7 @@ dagger = "2.60.1" javax-inject = "1" # Annotations procesing -ksp = "2.3.10" +ksp = "2.3.11" smoothCornerRectAndroidCompose = "v1.0.0" spleeterAndroidIos = "1.0.2" tensorflowLite = "2.17.0" From ba1b2939580f222cfe4e5abab61c4d7f799d4c92 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 8 Aug 2026 13:39:10 +0000 Subject: [PATCH 69/73] chore(deps): bump netty from 4.2.16.Final to 4.2.17.Final Bumps `netty` from 4.2.16.Final to 4.2.17.Final. Updates `io.netty:netty-common` from 4.2.16.Final to 4.2.17.Final - [Release notes](https://github.com/netty/netty/releases) - [Commits](https://github.com/netty/netty/compare/netty-4.2.16.Final...netty-4.2.17.Final) Updates `io.netty:netty-handler` from 4.2.16.Final to 4.2.17.Final - [Release notes](https://github.com/netty/netty/releases) - [Commits](https://github.com/netty/netty/compare/netty-4.2.16.Final...netty-4.2.17.Final) Updates `io.netty:netty-codec-http` from 4.2.16.Final to 4.2.17.Final - [Release notes](https://github.com/netty/netty/releases) - [Commits](https://github.com/netty/netty/compare/netty-4.2.16.Final...netty-4.2.17.Final) Updates `io.netty:netty-codec-http2` from 4.2.16.Final to 4.2.17.Final - [Release notes](https://github.com/netty/netty/releases) - [Commits](https://github.com/netty/netty/compare/netty-4.2.16.Final...netty-4.2.17.Final) --- updated-dependencies: - dependency-name: io.netty:netty-common dependency-version: 4.2.17.Final dependency-type: direct:production - dependency-name: io.netty:netty-handler dependency-version: 4.2.17.Final dependency-type: direct:production - dependency-name: io.netty:netty-codec-http dependency-version: 4.2.17.Final dependency-type: direct:production - dependency-name: io.netty:netty-codec-http2 dependency-version: 4.2.17.Final dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 489046a769..b1d382dda4 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -64,7 +64,7 @@ junit5 = "6.1.2" kuromoji = "0.9.0" pinyin4j = "2.5.1" securityCrypto = "1.1.0" -netty = "4.2.16.Final" +netty = "4.2.17.Final" bouncycastle = "1.85" commons-lang3 = "3.20.0" jdom2 = "2.0.6.1" From 23c23bff907512be158e03b9d908569415d8cf78 Mon Sep 17 00:00:00 2001 From: Dae Euhwa Date: Sat, 8 Aug 2026 08:53:16 -0500 Subject: [PATCH 70/73] fix(security): remove hardcoded Telegram API key defaults Remove the hardcoded fallback Telegram API ID and API hash that were embedded in build.gradle.kts. The build now fails with a clear error if these values are not configured in local.properties. This prevents credentials from being committed to version control. --- app/build.gradle.kts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 806448f975..a52fd8bda2 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -110,9 +110,9 @@ android { testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" val telegramApiId = localProperties.getProperty("TELEGRAM_API_ID")?.ifEmpty { null } - ?: "2040" + ?: error("TELEGRAM_API_ID not found in local.properties. Add TELEGRAM_API_ID= to local.properties.") val telegramApiHash = localProperties.getProperty("TELEGRAM_API_HASH")?.ifEmpty { null } - ?: "b18441a1ff607e10a989891a5462e627" + ?: error("TELEGRAM_API_HASH not found in local.properties. Add TELEGRAM_API_HASH= to local.properties.") buildConfigField("int", "TELEGRAM_API_ID", telegramApiId) buildConfigField("String", "TELEGRAM_API_HASH", "\"$telegramApiHash\"") } From 545fd8f2aaa776a350d2a94f5ed4b02fd889d66f Mon Sep 17 00:00:00 2001 From: Dae Euhwa Date: Sat, 8 Aug 2026 09:04:52 -0500 Subject: [PATCH 71/73] refactor(service): extract PlaybackSnapshotManager and WidgetArtworkResolver from MusicService Extract ~440 lines of widget artwork I/O and playback snapshot persistence logic from MusicService into two focused, testable classes: - WidgetArtworkResolver: handles artwork URI resolution, byte loading, caching, and failure retry for Glance home-screen widgets - PlaybackSnapshotManager: manages capture, persistence, and debounced scheduling of the playback queue snapshot used for state restoration MusicService now delegates to these components via lazy-initialized fields, keeping only the orchestration glue (restorePlaybackQueueSnapshotIfNeeded) that requires deep player access. --- .../pixelplay/data/service/MusicService.kt | 517 ++---------------- .../playback/PlaybackSnapshotManager.kt | 234 ++++++++ .../service/widget/WidgetArtworkResolver.kt | 332 +++++++++++ 3 files changed, 604 insertions(+), 479 deletions(-) create mode 100644 app/src/main/java/com/theveloper/pixelplay/data/service/playback/PlaybackSnapshotManager.kt create mode 100644 app/src/main/java/com/theveloper/pixelplay/data/service/widget/WidgetArtworkResolver.kt diff --git a/app/src/main/java/com/theveloper/pixelplay/data/service/MusicService.kt b/app/src/main/java/com/theveloper/pixelplay/data/service/MusicService.kt index adab461ca8..30deb2ac3d 100644 --- a/app/src/main/java/com/theveloper/pixelplay/data/service/MusicService.kt +++ b/app/src/main/java/com/theveloper/pixelplay/data/service/MusicService.kt @@ -46,7 +46,6 @@ import com.theveloper.pixelplay.MainActivity import com.theveloper.pixelplay.R import com.theveloper.pixelplay.data.diagnostics.PerformanceMetrics import com.theveloper.pixelplay.data.model.PlayerInfo -import com.theveloper.pixelplay.data.model.PlaybackQueueItemSnapshot import com.theveloper.pixelplay.data.model.PlaybackQueueSnapshot import com.theveloper.pixelplay.data.preferences.EqualizerPreferencesRepository import com.theveloper.pixelplay.data.preferences.ThemePreferencesRepository @@ -79,6 +78,8 @@ import com.theveloper.pixelplay.data.preferences.ThemePreference import com.theveloper.pixelplay.data.service.auto.AutoMediaBrowseTree import com.theveloper.pixelplay.data.service.wear.buildWearThemePalette import com.theveloper.pixelplay.data.service.wear.WearStatePublisher +import com.theveloper.pixelplay.data.service.playback.PlaybackSnapshotManager +import com.theveloper.pixelplay.data.service.widget.WidgetArtworkResolver import com.theveloper.pixelplay.presentation.viewmodel.ColorSchemePair import com.theveloper.pixelplay.shared.WearIntents import com.theveloper.pixelplay.utils.ArtworkTransportSanitizer @@ -86,55 +87,9 @@ import com.theveloper.pixelplay.utils.MediaItemBuilder import com.theveloper.pixelplay.data.navidrome.NavidromeRepository import com.theveloper.pixelplay.di.AppScope import com.theveloper.pixelplay.presentation.viewmodel.ListeningStatsTracker -import java.io.ByteArrayOutputStream -import java.net.HttpURLConnection -import java.net.URL -import java.util.concurrent.atomic.AtomicInteger -import coil.imageLoader -import coil.request.CachePolicy -import coil.request.ImageRequest -import coil.size.Precision - import javax.inject.Inject import androidx.core.net.toUri -// Acciones personalizadas para compatibilidad con el widget existente - -suspend fun loadArtworkBytesViaCoil(context: Context, uri: Uri): ByteArray? { - val appContext = context.applicationContext - val request = ImageRequest.Builder(appContext) - .data(uri) - .size( - ArtworkTransportSanitizer.WIDGET_CONFIG.maxDimensionPx, - ArtworkTransportSanitizer.WIDGET_CONFIG.maxDimensionPx, - ) - .precision(Precision.INEXACT) - .allowHardware(false) - .memoryCachePolicy(CachePolicy.ENABLED) - .networkCachePolicy(CachePolicy.ENABLED) - .build() - - return runCatching { - val drawable = appContext.imageLoader.execute(request).drawable ?: return@runCatching null - val fallbackSizePx = ArtworkTransportSanitizer.WIDGET_CONFIG.maxDimensionPx - val bitmap = drawable.toBitmap( - width = drawable.intrinsicWidth.takeIf { it > 0 } ?: fallbackSizePx, - height = drawable.intrinsicHeight.takeIf { it > 0 } ?: fallbackSizePx, - config = Bitmap.Config.ARGB_8888, - ) - val encodedBytes = ByteArrayOutputStream().use { output -> - bitmap.compress(Bitmap.CompressFormat.JPEG, 92, output) - output.toByteArray() - } - ArtworkTransportSanitizer.sanitizeEncodedBytes( - data = encodedBytes, - config = ArtworkTransportSanitizer.WIDGET_CONFIG, - ) - }.getOrElse { error -> - Timber.tag("MusicService_PixelPlay").w(error, "Artwork read failed via Coil for uri=%s", uri) - null - } -} @UnstableApi @@ -223,10 +178,23 @@ class MusicService : MediaLibraryService() { resolveCurrentMediaIdForWear = { resolveCurrentMediaIdForWear() }, ) } - private var playbackSnapshotPersistJob: Job? = null - private var playbackSnapshotUnloadWriteJob: Job? = null - private var isRestoringPlaybackSnapshot = false - private var isPlaybackUnloadInProgress = false + private val widgetArtworkResolver by lazy { + WidgetArtworkResolver( + context = applicationContext, + engine = engine, + musicRepository = musicRepository, + ) + } + private val snapshotManager by lazy { + PlaybackSnapshotManager( + context = this, + engine = engine, + userPreferencesRepository = userPreferencesRepository, + scope = serviceScope, + ).also { + it.setShuffleStateProvider { isManualShuffleEnabled } + } + } private val audioManager by lazy { getSystemService(Context.AUDIO_SERVICE) as AudioManager } @@ -261,10 +229,6 @@ class MusicService : MediaLibraryService() { const val ACTION_SLEEP_TIMER_EXPIRED = "com.theveloper.pixelplay.ACTION_SLEEP_TIMER_EXPIRED" const val EXTRA_FORCE_FOREGROUND_ON_START = "com.theveloper.pixelplay.extra.FORCE_FOREGROUND_ON_START" - // Queue/index/flags snapshot is only used for restore on process death. A full-queue - // JSON+DataStore rewrite on every Media3 event (track transition fires 3-4 listeners - // within ~200ms) is unnecessary work. 1500ms coalesces those without harming restore. - private const val PLAYBACK_SNAPSHOT_DEBOUNCE_MS = 1500L private const val MEDIA_SESSION_BUTTON_DEBOUNCE_MS = 250L private const val DEFERRED_SERVICE_STARTUP_WORK_DELAY_MS = 1_000L private const val PAUSED_RESTORE_PREPARE_QUEUE_LIMIT = 50 @@ -292,8 +256,6 @@ class MusicService : MediaLibraryService() { private const val AUTO_CONTEXT_ALBUM = "album" private const val AUTO_CONTEXT_ARTIST = "artist" private const val AUTO_CONTEXT_PLAYLIST = "playlist" - private const val DEFAULT_STREAM_BUFFER_SIZE = 8 * 1024 - private const val WIDGET_ART_FAILURE_RETRY_MS = 30_000L private const val HEADSET_RECONNECT_RESUME_WINDOW_MS = 15_000L fun markPendingMediaButtonForegroundStart() { @@ -447,7 +409,7 @@ class MusicService : MediaLibraryService() { controller.initialize() serviceScope.launch { delay(DEFERRED_SERVICE_STARTUP_WORK_DELAY_MS) - if (!isPlaybackUnloadInProgress && mediaSession != null) { + if (!snapshotManager.isUnloadInProgress && mediaSession != null) { castSyncCoordinator.start() } } @@ -455,8 +417,8 @@ class MusicService : MediaLibraryService() { serviceScope.launch { musicRepository.telegramRepository.downloadCompleted.collect { - if (isCurrentWidgetArtworkBackedByTelegram()) { - invalidateCachedWidgetArtwork() + if (widgetArtworkResolver.isCurrentWidgetArtworkBackedByTelegram()) { + widgetArtworkResolver.invalidateCachedWidgetArtwork() widgetUpdateManager.requestWithFollowUp() } } @@ -1495,7 +1457,6 @@ class MusicService : MediaLibraryService() { listeningStatsTracker.finalizeCurrentSession(forceSynchronousPersistence = true) reportNavidromePlayback("stopped") stopNavidromePlaybackReporting() - playbackSnapshotPersistJob?.cancel() mediaSessionButtonRefreshJob?.cancel() followUpMediaSessionUiRefreshJob?.cancel() widgetUpdateManager.cancel() @@ -1537,7 +1498,7 @@ class MusicService : MediaLibraryService() { // (TRIM_MEMORY_BACKGROUND = 40, TRIM_MEMORY_COMPLETE = 80, etc.). if (level >= 10 /* TRIM_MEMORY_RUNNING_LOW */) { Timber.tag(TAG).d("onTrimMemory(level=%d): releasing widget bitmap caches", level) - invalidateCachedWidgetArtwork() + widgetArtworkResolver.invalidateCachedWidgetArtwork() // Drop the stale PlayerInfo copy so its embedded ByteArray is GC-eligible. // The next widget update cycle will rebuild it from scratch. widgetUpdateManager.clearCachedState() @@ -1629,101 +1590,7 @@ class MusicService : MediaLibraryService() { } private fun schedulePlaybackSnapshotPersist(immediate: Boolean = false) { - if (isPlaybackUnloadInProgress) { - return - } - playbackSnapshotPersistJob?.cancel() - playbackSnapshotPersistJob = serviceScope.launch { - if (!immediate) { - delay(PLAYBACK_SNAPSHOT_DEBOUNCE_MS) - } - persistPlaybackSnapshot() - } - } - - private suspend fun persistPlaybackSnapshot(playWhenReadyOverride: Boolean? = null) { - if (isRestoringPlaybackSnapshot) return - val snapshot = capturePlaybackSnapshot(playWhenReadyOverride) - runCatching { - userPreferencesRepository.setPlaybackQueueSnapshot(snapshot) - }.onFailure { e -> - Timber.tag(TAG).w(e, "Failed to persist playback snapshot") - } - } - - private suspend fun capturePlaybackSnapshot(playWhenReadyOverride: Boolean? = null): PlaybackQueueSnapshot? = - withContext(Dispatchers.Main.immediate) { - capturePlaybackSnapshotFromPlayer(playWhenReadyOverride) - } - - private fun capturePlaybackSnapshotFromPlayer( - playWhenReadyOverride: Boolean? = null - ): PlaybackQueueSnapshot? { - val player = engine.masterPlayer - val mediaItemCount = player.mediaItemCount - if (mediaItemCount <= 0) { - return null - } - - val snapshotItems = ArrayList(mediaItemCount) - for (index in 0 until mediaItemCount) { - val mediaItem = player.getMediaItemAt(index) - val metadata = mediaItem.mediaMetadata - val uri = mediaItem.localConfiguration?.uri?.toString() - ?: metadata.extras?.getString(MediaItemBuilder.EXTERNAL_EXTRA_CONTENT_URI) - - if (mediaItem.mediaId.isBlank() || uri.isNullOrBlank()) { - continue - } - - val durationMs = metadata.extras - ?.getLong(MediaItemBuilder.EXTERNAL_EXTRA_DURATION) - ?.takeIf { it > 0L } - - snapshotItems.add( - PlaybackQueueItemSnapshot( - mediaId = mediaItem.mediaId, - uri = uri, - title = metadata.title?.toString(), - artist = metadata.artist?.toString(), - albumTitle = metadata.albumTitle?.toString(), - artworkUri = resolveStoredArtworkUriString(metadata), - durationMs = durationMs, - ) - ) - } - - if (snapshotItems.isEmpty()) { - return null - } - - val currentMediaId = player.currentMediaItem?.mediaId - val indexFromMediaId = currentMediaId - ?.let { id -> snapshotItems.indexOfFirst { it.mediaId == id } } - ?.takeIf { it >= 0 } - - val safeCurrentIndex = when { - indexFromMediaId != null -> indexFromMediaId - player.currentMediaItemIndex in snapshotItems.indices -> player.currentMediaItemIndex - else -> 0 - } - - val safeRepeatMode = when (player.repeatMode) { - Player.REPEAT_MODE_OFF, - Player.REPEAT_MODE_ONE, - Player.REPEAT_MODE_ALL -> player.repeatMode - else -> Player.REPEAT_MODE_OFF - } - - return PlaybackQueueSnapshot( - items = snapshotItems, - currentMediaId = currentMediaId, - currentIndex = safeCurrentIndex, - currentPositionMs = player.currentPosition.coerceAtLeast(0L), - playWhenReady = playWhenReadyOverride ?: player.playWhenReady, - repeatMode = safeRepeatMode, - shuffleEnabled = isManualShuffleEnabled, - ) + snapshotManager.schedulePersist(immediate) } private suspend fun restorePlaybackQueueSnapshotIfNeeded() { @@ -1745,7 +1612,7 @@ class MusicService : MediaLibraryService() { }.getOrDefault(keepPlayingInBackground) val shouldRestorePlaying = snapshot.playWhenReady && allowBackgroundPlayback - val restoredItems = snapshot.items.mapNotNull(::buildMediaItemFromSnapshot) + val restoredItems = snapshot.items.mapNotNull { snapshotManager.buildMediaItemFromSnapshot(it) } if (restoredItems.isEmpty()) { userPreferencesRepository.setPlaybackQueueSnapshot(null) return @@ -1781,7 +1648,7 @@ class MusicService : MediaLibraryService() { else -> Player.REPEAT_MODE_OFF } - isRestoringPlaybackSnapshot = true + snapshotManager.beginRestore() try { player.setMediaItems( preparedItems, @@ -1800,7 +1667,7 @@ class MusicService : MediaLibraryService() { player.playWhenReady = false } } finally { - isRestoringPlaybackSnapshot = false + snapshotManager.endRestore() } } @@ -1810,44 +1677,7 @@ class MusicService : MediaLibraryService() { snapshot.currentIndex, shouldRestorePlaying ) - schedulePlaybackSnapshotPersist(immediate = true) - } - - private fun buildMediaItemFromSnapshot(snapshotItem: PlaybackQueueItemSnapshot): MediaItem? { - if (snapshotItem.mediaId.isBlank() || snapshotItem.uri.isBlank()) { - return null - } - - val metadataBuilder = MediaMetadata.Builder() - snapshotItem.title?.takeIf { it.isNotBlank() }?.let { metadataBuilder.setTitle(it) } - snapshotItem.artist?.takeIf { it.isNotBlank() }?.let { metadataBuilder.setArtist(it) } - snapshotItem.albumTitle?.takeIf { it.isNotBlank() }?.let { metadataBuilder.setAlbumTitle(it) } - MediaItemBuilder.externalControllerArtworkUri(this, snapshotItem.artworkUri) - ?.let { metadataBuilder.setArtworkUri(it) } - - val extras = Bundle().apply { - putBoolean( - MediaItemBuilder.EXTERNAL_EXTRA_FLAG, - snapshotItem.mediaId.startsWith("external:") - ) - putString(MediaItemBuilder.EXTERNAL_EXTRA_CONTENT_URI, snapshotItem.uri) - snapshotItem.albumTitle?.takeIf { it.isNotBlank() }?.let { - putString(MediaItemBuilder.EXTERNAL_EXTRA_ALBUM, it) - } - snapshotItem.artworkUri?.takeIf { it.isNotBlank() }?.let { - putString(MediaItemBuilder.EXTERNAL_EXTRA_ALBUM_ART, it) - } - snapshotItem.durationMs?.takeIf { it > 0L }?.let { - putLong(MediaItemBuilder.EXTERNAL_EXTRA_DURATION, it) - } - } - metadataBuilder.setExtras(extras) - - return MediaItem.Builder() - .setMediaId(snapshotItem.mediaId) - .setUri(MediaItemBuilder.playbackUri(snapshotItem.uri)) - .setMediaMetadata(metadataBuilder.build()) - .build() + snapshotManager.schedulePersist(immediate = true) } private fun getOpenAppPendingIntent(): PendingIntent { @@ -1962,7 +1792,7 @@ class MusicService : MediaLibraryService() { var title = currentItem?.mediaMetadata?.title?.toString().orEmpty() var artist = currentItem?.mediaMetadata?.artist?.toString().orEmpty() var mediaId = currentItem?.mediaId - var artworkUri = resolveWidgetArtworkUriCandidates(currentItem?.mediaMetadata).firstOrNull() + var artworkUri = widgetArtworkResolver.resolveWidgetArtworkUriCandidates(currentItem?.mediaMetadata).firstOrNull() var artworkData = currentItem?.mediaMetadata?.artworkData castSyncCoordinator.resolveRemoteSnapshot()?.let { remote -> @@ -1987,11 +1817,11 @@ class MusicService : MediaLibraryService() { shuffleEnabled = remote.isShuffleEnabled } - val artworkCandidates = resolveWidgetArtworkUriCandidates( + val artworkCandidates = widgetArtworkResolver.resolveWidgetArtworkUriCandidates( metadata = currentItem?.mediaMetadata, preferredArtworkUri = artworkUri, ) - val (artBytes, artUriString) = getAlbumArtForWidget( + val (artBytes, artUriString) = widgetArtworkResolver.getAlbumArtForWidget( mediaId = mediaId, embeddedArt = artworkData, artUris = artworkCandidates, @@ -2087,13 +1917,13 @@ class MusicService : MediaLibraryService() { val mediaItem = window.mediaItem val songId = mediaItem.mediaId.toLongOrNull() if (songId != null) { - val initialQueueArtworkUri = resolveWidgetArtworkUriCandidates(mediaItem.mediaMetadata) + val initialQueueArtworkUri = widgetArtworkResolver.resolveWidgetArtworkUriCandidates(mediaItem.mediaMetadata) .firstOrNull() val queueArtworkUri = when { - initialQueueArtworkUri == null -> resolveRepositoryArtworkUri(mediaItem.mediaId) + initialQueueArtworkUri == null -> widgetArtworkResolver.resolveRepositoryArtworkUri(mediaItem.mediaId) initialQueueArtworkUri.scheme?.lowercase() == "content" && initialQueueArtworkUri.authority == "$packageName.provider" -> - resolveRepositoryArtworkUri(mediaItem.mediaId) ?: initialQueueArtworkUri + widgetArtworkResolver.resolveRepositoryArtworkUri(mediaItem.mediaId) ?: initialQueueArtworkUri else -> initialQueueArtworkUri } queueItems.add( @@ -2143,256 +1973,6 @@ class MusicService : MediaLibraryService() { private var cachedSchemePaletteStyle: AlbumArtPaletteStyle? = null private var cachedSchemeColorAccuracy: Int = AlbumArtColorAccuracy.DEFAULT private var cachedColorSchemePair: ColorSchemePair? = null - private var cachedWidgetArtSourceKey: String? = null - private var cachedWidgetArtResolvedUri: String? = null - private var cachedWidgetArtBytes: ByteArray? = null - private var cachedWidgetArtLoadFailureKey: String? = null - private var cachedWidgetArtLoadFailureAtMs: Long = 0L - - private fun invalidateCachedWidgetArtwork() { - cachedWidgetArtSourceKey = null - cachedWidgetArtResolvedUri = null - cachedWidgetArtBytes = null - cachedWidgetArtLoadFailureKey = null - cachedWidgetArtLoadFailureAtMs = 0L - } - - private fun isCurrentWidgetArtworkBackedByTelegram(): Boolean { - val currentItem = engine.masterPlayer.currentMediaItem ?: return false - val metadata = currentItem.mediaMetadata - val contentUriString = currentItem.localConfiguration?.uri?.toString() - ?: metadata.extras?.getString(MediaItemBuilder.EXTERNAL_EXTRA_CONTENT_URI) - val artworkUriString = resolveStoredArtworkUriString(metadata) - return contentUriString?.startsWith("telegram://") == true || - artworkUriString?.startsWith("telegram_art://") == true - } - - private suspend fun getAlbumArtForWidget( - mediaId: String?, - embeddedArt: ByteArray?, - artUris: List, - ): Pair = withContext(Dispatchers.IO) { - // Try embedded art first — but fall through to URI loading if sanitization fails - val sanitizedFromEmbedded = embeddedArt?.takeIf { it.isNotEmpty() }?.let { bytes -> - runCatching { - ArtworkTransportSanitizer.sanitizeEncodedBytes( - data = bytes, - config = ArtworkTransportSanitizer.WIDGET_CONFIG, - ) - }.getOrNull() - } - val candidateUriStrings = LinkedHashSet().apply { - artUris.forEach { candidate -> - candidate.toString() - .takeIf { it.isNotBlank() } - ?.let(::add) - } - }.toList() - val preferredUriString = candidateUriStrings.firstOrNull() - val sourceKey = buildWidgetArtworkSourceKey( - mediaId = mediaId, - candidateUriStrings = candidateUriStrings, - ) - - if (sanitizedFromEmbedded != null) { - cachedWidgetArtSourceKey = sourceKey - cachedWidgetArtResolvedUri = preferredUriString - cachedWidgetArtBytes = sanitizedFromEmbedded - cachedWidgetArtLoadFailureKey = null - cachedWidgetArtLoadFailureAtMs = 0L - return@withContext sanitizedFromEmbedded to preferredUriString - } - - if (sourceKey != null && sourceKey == cachedWidgetArtSourceKey && cachedWidgetArtBytes != null) { - return@withContext cachedWidgetArtBytes to (cachedWidgetArtResolvedUri ?: preferredUriString) - } - if (sourceKey != null && sourceKey == cachedWidgetArtLoadFailureKey) { - val failureAgeMs = SystemClock.elapsedRealtime() - cachedWidgetArtLoadFailureAtMs - if (failureAgeMs < WIDGET_ART_FAILURE_RETRY_MS) { - return@withContext null to preferredUriString - } - } - - val repositoryArtUriString = if (mediaId.isNullOrBlank()) { - null - } else { - resolveRepositoryArtworkUri(mediaId)?.toString() - } - val resolvedUriStrings = LinkedHashSet().apply { - addAll(candidateUriStrings) - repositoryArtUriString - ?.takeIf { it.isNotBlank() } - ?.let(::add) - } - - for (candidateUriString in resolvedUriStrings) { - val candidateUri = parseArtworkUriString(candidateUriString) ?: continue - val loadedBytes = loadArtworkBytesForWidget(candidateUri) - if (loadedBytes != null) { - cachedWidgetArtSourceKey = sourceKey - cachedWidgetArtResolvedUri = candidateUriString - cachedWidgetArtBytes = loadedBytes - cachedWidgetArtLoadFailureKey = null - cachedWidgetArtLoadFailureAtMs = 0L - return@withContext loadedBytes to candidateUriString - } - } - - cachedWidgetArtLoadFailureKey = sourceKey - cachedWidgetArtLoadFailureAtMs = SystemClock.elapsedRealtime() - return@withContext null to (repositoryArtUriString ?: preferredUriString) - } - - private fun resolveStoredArtworkUriString(metadata: MediaMetadata?): String? { - metadata ?: return null - return metadata.extras - ?.getString(MediaItemBuilder.EXTERNAL_EXTRA_ALBUM_ART) - ?.takeIf { it.isNotBlank() } - ?: metadata.artworkUri - ?.toString() - ?.takeIf { it.isNotBlank() } - } - - private fun resolveWidgetArtworkUriCandidates( - metadata: MediaMetadata?, - preferredArtworkUri: Uri? = null, - ): List { - val candidates = LinkedHashSet() - preferredArtworkUri - ?.toString() - ?.takeIf { it.isNotBlank() } - ?.let(candidates::add) - resolveStoredArtworkUriString(metadata)?.let(candidates::add) - metadata?.artworkUri - ?.toString() - ?.takeIf { it.isNotBlank() } - ?.let(candidates::add) - return candidates.mapNotNull(::parseArtworkUriString) - } - - private fun parseArtworkUriString(rawArtworkUri: String?): Uri? { - if (rawArtworkUri.isNullOrBlank()) { - return null - } - - return MediaItemBuilder.artworkUri(rawArtworkUri) - ?: if (rawArtworkUri.startsWith("/")) { - Uri.fromFile(java.io.File(rawArtworkUri)) - } else { - runCatching { rawArtworkUri.toUri() }.getOrNull() - } - } - - private fun buildWidgetArtworkSourceKey( - mediaId: String?, - candidateUriStrings: List, - ): String? { - val normalizedMediaId = mediaId?.takeIf { it.isNotBlank() } - if (normalizedMediaId == null && candidateUriStrings.isEmpty()) { - return null - } - return buildString { - normalizedMediaId?.let { - append("mediaId=") - append(it) - } - if (candidateUriStrings.isNotEmpty()) { - if (isNotEmpty()) append('|') - append(candidateUriStrings.joinToString(separator = ",")) - } - } - } - - private fun resolveArtworkUri(metadata: MediaMetadata?): Uri? { - metadata ?: return null - metadata.artworkUri?.let { return it } - val extrasUri = metadata.extras - ?.getString(MediaItemBuilder.EXTERNAL_EXTRA_ALBUM_ART) - ?.takeIf { it.isNotBlank() } - ?: return null - return parseArtworkUriString(extrasUri) - } - - private suspend fun resolveRepositoryArtworkUri(mediaId: String?): Uri? { - val songId = mediaId?.takeIf { it.isNotBlank() } ?: return null - val song = withContext(Dispatchers.IO) { - musicRepository.getSong(songId).first() - } ?: return null - - return MediaItemBuilder.artworkUri(song.albumArtUriString) - ?: song.albumArtUriString - ?.takeIf { it.isNotBlank() } - ?.let { raw -> - if (raw.startsWith("/")) Uri.fromFile(java.io.File(raw)) - else runCatching { Uri.parse(raw) }.getOrNull() - } - } - - public suspend fun loadArtworkBytesForWidget(uri: Uri): ByteArray? { - val uriString = uri.toString() - val scheme = uri.scheme?.lowercase() - val isLocalArtworkUri = com.theveloper.pixelplay.utils.LocalArtworkUri.isLocalArtworkUri(uriString) - return when { - isLocalArtworkUri || scheme == "content" || scheme == "file" || scheme == "android.resource" -> { - runCatching { - AlbumArtUtils.openArtworkInputStream(applicationContext, uri)?.use { input -> - readBytesCapped(input, ArtworkTransportSanitizer.WIDGET_CONFIG.sourceBytesLimit) - ?.let { bytes -> - ArtworkTransportSanitizer.sanitizeEncodedBytes( - data = bytes, - config = ArtworkTransportSanitizer.WIDGET_CONFIG, - ) - } - } - }.getOrElse { error -> - Timber.tag(TAG).w(error, "Widget artwork read failed for local uri=%s", uri) - null - } - } - scheme == "http" || scheme == "https" -> { - var connection: HttpURLConnection? = null - try { - connection = (URL(uriString).openConnection() as? HttpURLConnection) - ?: return null - connection.connectTimeout = 4_000 - connection.readTimeout = 6_000 - connection.instanceFollowRedirects = true - connection.doInput = true - connection.inputStream.use { input -> - readBytesCapped(input, ArtworkTransportSanitizer.WIDGET_CONFIG.sourceBytesLimit) - ?.let { bytes -> - ArtworkTransportSanitizer.sanitizeEncodedBytes( - data = bytes, - config = ArtworkTransportSanitizer.WIDGET_CONFIG, - ) - } - } - } catch (error: Exception) { - Timber.tag(TAG).w(error, "Widget artwork read failed for remote uri=%s", uri) - null - } finally { - connection?.disconnect() - } - } - else -> loadArtworkBytesViaCoil(applicationContext, uri) - } - } - - private fun readBytesCapped(input: java.io.InputStream, maxBytes: Int): ByteArray? { - // Pre-size to 4× the read-buffer to reduce reallocation churn on typical album art - // (50–300 KB). Still far below the maxBytes cap enforced in the loop below. - val output = ByteArrayOutputStream(DEFAULT_STREAM_BUFFER_SIZE * 4) - val buffer = ByteArray(DEFAULT_STREAM_BUFFER_SIZE) - var totalRead = 0 - while (true) { - val read = input.read(buffer) - if (read <= 0) break - totalRead += read - if (totalRead > maxBytes) return null - output.write(buffer, 0, read) - } - return output.toByteArray().takeIf { it.isNotEmpty() } - } fun isSongFavorite(songId: String?): Boolean { return songId != null && favoriteSongIds.contains(songId) @@ -2497,11 +2077,10 @@ class MusicService : MediaLibraryService() { "Stopping playback and unloading service. reason=%s", reason ) - isPlaybackUnloadInProgress = true + snapshotManager.isUnloadInProgress = true followUpMediaSessionUiRefreshJob?.cancel() mediaSessionButtonRefreshJob?.cancel() widgetUpdateManager.cancel() - playbackSnapshotPersistJob?.cancel() val sessionToRelease = mediaSession val player = sessionToRelease?.player ?: engine.masterPlayer @@ -2511,9 +2090,9 @@ class MusicService : MediaLibraryService() { endOfTrackTimerSongId = null if (preservePlaybackSnapshot) { - persistPlaybackSnapshotOnUnload() + snapshotManager.persistOnUnload() } else { - clearPlaybackSnapshotOnUnload() + snapshotManager.clearOnUnload() } listeningStatsTracker.finalizeCurrentSession(forceSynchronousPersistence = true) @@ -2528,26 +2107,6 @@ class MusicService : MediaLibraryService() { stopSelf() } - private fun persistPlaybackSnapshotOnUnload() { - val snapshot = capturePlaybackSnapshotFromPlayer(playWhenReadyOverride = false) - writePlaybackSnapshotOnUnload(snapshot) - } - - private fun clearPlaybackSnapshotOnUnload() { - writePlaybackSnapshotOnUnload(null) - } - - private fun writePlaybackSnapshotOnUnload(snapshot: PlaybackQueueSnapshot?) { - playbackSnapshotUnloadWriteJob?.cancel() - playbackSnapshotUnloadWriteJob = appScope.launch { - runCatching { - userPreferencesRepository.setPlaybackQueueSnapshot(snapshot) - }.onFailure { e -> - Timber.tag(TAG).w(e, "Failed to persist playback snapshot during unload") - } - } - } - private fun refreshMediaSessionUiWithFollowUp( session: MediaSession, delayMs: Long = 250L @@ -2716,7 +2275,7 @@ class MusicService : MediaLibraryService() { val providerAuthority = "$packageName.provider" val artworkAuthority = "$packageName.artwork" mediaItems.forEach { mediaItem -> - val artworkUri = resolveArtworkUri(mediaItem.mediaMetadata) ?: return@forEach + val artworkUri = widgetArtworkResolver.resolveArtworkUri(mediaItem.mediaMetadata) ?: return@forEach val authority = artworkUri.authority if (artworkUri.scheme?.lowercase() != "content" || (authority != providerAuthority && authority != artworkAuthority) diff --git a/app/src/main/java/com/theveloper/pixelplay/data/service/playback/PlaybackSnapshotManager.kt b/app/src/main/java/com/theveloper/pixelplay/data/service/playback/PlaybackSnapshotManager.kt new file mode 100644 index 0000000000..11cbc5a875 --- /dev/null +++ b/app/src/main/java/com/theveloper/pixelplay/data/service/playback/PlaybackSnapshotManager.kt @@ -0,0 +1,234 @@ +package com.theveloper.pixelplay.data.service.playback + +import android.content.Context +import android.os.Bundle +import androidx.media3.common.Player +import com.theveloper.pixelplay.data.model.PlaybackQueueItemSnapshot +import com.theveloper.pixelplay.data.model.PlaybackQueueSnapshot +import com.theveloper.pixelplay.data.preferences.UserPreferencesRepository +import com.theveloper.pixelplay.data.service.player.DualPlayerEngine +import com.theveloper.pixelplay.utils.MediaItemBuilder +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import timber.log.Timber + +/** + * Manages capture, persistence, and debounced scheduling of the playback + * queue snapshot used to restore playback state across process restarts. + * + * Extracted from [MusicService] to isolate snapshot I/O from the service's + * media-session lifecycle management. + */ +class PlaybackSnapshotManager( + private val context: Context, + private val engine: DualPlayerEngine, + private val userPreferencesRepository: UserPreferencesRepository, + private val scope: CoroutineScope, +) { + + private var persistJob: Job? = null + private var unloadWriteJob: Job? = null + var isRestoring: Boolean = false + private set + var isUnloadInProgress: Boolean = false + private set + + fun schedulePersist(immediate: Boolean = false) { + if (isUnloadInProgress) return + persistJob?.cancel() + persistJob = scope.launch { + if (!immediate) { + delay(SNAPSHOT_DEBOUNCE_MS) + } + persist() + } + } + + suspend fun persist(playWhenReadyOverride: Boolean? = null) { + if (isRestoring) return + val snapshot = capture(playWhenReadyOverride) + runCatching { + userPreferencesRepository.setPlaybackQueueSnapshot(snapshot) + }.onFailure { e -> + Timber.tag(TAG).w(e, "Failed to persist playback snapshot") + } + } + + suspend fun capture(playWhenReadyOverride: Boolean? = null): PlaybackQueueSnapshot? = + withContext(Dispatchers.Main.immediate) { + captureFromPlayer(playWhenReadyOverride) + } + + fun captureFromPlayer( + playWhenReadyOverride: Boolean? = null + ): PlaybackQueueSnapshot? { + val player = engine.masterPlayer + val mediaItemCount = player.mediaItemCount + if (mediaItemCount <= 0) { + return null + } + + val snapshotItems = ArrayList(mediaItemCount) + for (index in 0 until mediaItemCount) { + val mediaItem = player.getMediaItemAt(index) + val metadata = mediaItem.mediaMetadata + val uri = mediaItem.localConfiguration?.uri?.toString() + ?: metadata.extras?.getString(MediaItemBuilder.EXTERNAL_EXTRA_CONTENT_URI) + + if (mediaItem.mediaId.isBlank() || uri.isNullOrBlank()) { + continue + } + + val durationMs = metadata.extras + ?.getLong(MediaItemBuilder.EXTERNAL_EXTRA_DURATION) + ?.takeIf { it > 0L } + + snapshotItems.add( + PlaybackQueueItemSnapshot( + mediaId = mediaItem.mediaId, + uri = uri, + title = metadata.title?.toString(), + artist = metadata.artist?.toString(), + albumTitle = metadata.albumTitle?.toString(), + artworkUri = resolveStoredArtworkUri(metadata), + durationMs = durationMs, + ) + ) + } + + if (snapshotItems.isEmpty()) { + return null + } + + val currentMediaId = player.currentMediaItem?.mediaId + val indexFromMediaId = currentMediaId + ?.let { id -> snapshotItems.indexOfFirst { it.mediaId == id } } + ?.takeIf { it >= 0 } + + val safeCurrentIndex = when { + indexFromMediaId != null -> indexFromMediaId + player.currentMediaItemIndex in snapshotItems.indices -> player.currentMediaItemIndex + else -> 0 + } + + val safeRepeatMode = when (player.repeatMode) { + Player.REPEAT_MODE_OFF, + Player.REPEAT_MODE_ONE, + Player.REPEAT_MODE_ALL -> player.repeatMode + else -> Player.REPEAT_MODE_OFF + } + + return PlaybackQueueSnapshot( + items = snapshotItems, + currentMediaId = currentMediaId, + currentIndex = safeCurrentIndex, + currentPositionMs = player.currentPosition.coerceAtLeast(0L), + playWhenReady = playWhenReadyOverride ?: player.playWhenReady, + repeatMode = safeRepeatMode, + shuffleEnabled = isShuffleEnabled(), + ) + } + + fun persistOnUnload() { + val snapshot = captureFromPlayer(playWhenReadyOverride = false) + writeOnUnload(snapshot) + } + + fun clearOnUnload() { + writeOnUnload(null) + } + + private fun writeOnUnload(snapshot: PlaybackQueueSnapshot?) { + unloadWriteJob?.cancel() + unloadWriteJob = scope.launch { + runCatching { + userPreferencesRepository.setPlaybackQueueSnapshot(snapshot) + }.onFailure { e -> + Timber.tag(TAG).w(e, "Failed to persist playback snapshot during unload") + } + } + } + + fun beginRestore() { + isRestoring = true + } + + fun endRestore() { + isRestoring = false + } + + /** + * Build a [MediaItem] from a snapshot item for queue restoration. + * Kept here because it depends on [MediaItemBuilder] which is a utility. + */ + fun buildMediaItemFromSnapshot(snapshotItem: PlaybackQueueItemSnapshot): android.media3.common.MediaItem? { + if (snapshotItem.mediaId.isBlank() || snapshotItem.uri.isBlank()) { + return null + } + + val metadataBuilder = androidx.media3.common.MediaMetadata.Builder() + snapshotItem.title?.takeIf { it.isNotBlank() }?.let { metadataBuilder.setTitle(it) } + snapshotItem.artist?.takeIf { it.isNotBlank() }?.let { metadataBuilder.setArtist(it) } + snapshotItem.albumTitle?.takeIf { it.isNotBlank() }?.let { metadataBuilder.setAlbumTitle(it) } + MediaItemBuilder.externalControllerArtworkUri(context, snapshotItem.artworkUri) + ?.let { metadataBuilder.setArtworkUri(it) } + + val extras = Bundle().apply { + putBoolean( + MediaItemBuilder.EXTERNAL_EXTRA_FLAG, + snapshotItem.mediaId.startsWith("external:") + ) + putString(MediaItemBuilder.EXTERNAL_EXTRA_CONTENT_URI, snapshotItem.uri) + snapshotItem.albumTitle?.takeIf { it.isNotBlank() }?.let { + putString(MediaItemBuilder.EXTERNAL_EXTRA_ALBUM, it) + } + snapshotItem.artworkUri?.takeIf { it.isNotBlank() }?.let { + putString(MediaItemBuilder.EXTERNAL_EXTRA_ALBUM_ART, it) + } + snapshotItem.durationMs?.takeIf { it > 0L }?.let { + putLong(MediaItemBuilder.EXTERNAL_EXTRA_DURATION, it) + } + } + metadataBuilder.setExtras(extras) + + return android.media3.common.MediaItem.Builder() + .setMediaId(snapshotItem.mediaId) + .setUri(MediaItemBuilder.playbackUri(snapshotItem.uri)) + .setMediaMetadata(metadataBuilder.build()) + .build() + } + + /** + * Placeholder for shuffle state — overridden by MusicService lambda. + * Set by MusicService via [setShuffleStateProvider]. + */ + private var shuffleStateProvider: () -> Boolean = { false } + + fun setShuffleStateProvider(provider: () -> Boolean) { + shuffleStateProvider = provider + } + + private fun isShuffleEnabled(): Boolean = shuffleStateProvider() + + /** + * Reads the stored artwork URI from media item metadata. + */ + private fun resolveStoredArtworkUri(metadata: androidx.media3.common.MediaMetadata?): String? { + metadata ?: return null + return metadata.extras + ?.getString(MediaItemBuilder.EXTERNAL_EXTRA_ALBUM_ART) + ?.takeIf { it.isNotBlank() } + ?: metadata.artworkUri + ?.toString() + ?.takeIf { it.isNotBlank() } + } + + companion object { + private const val TAG = "PlaybackSnapshotManager" + private const val SNAPSHOT_DEBOUNCE_MS = 1500L + } +} diff --git a/app/src/main/java/com/theveloper/pixelplay/data/service/widget/WidgetArtworkResolver.kt b/app/src/main/java/com/theveloper/pixelplay/data/service/widget/WidgetArtworkResolver.kt new file mode 100644 index 0000000000..69df8fa7e8 --- /dev/null +++ b/app/src/main/java/com/theveloper/pixelplay/data/service/widget/WidgetArtworkResolver.kt @@ -0,0 +1,332 @@ +package com.theveloper.pixelplay.data.service.widget + +import android.content.Context +import android.graphics.Bitmap +import android.net.Uri +import android.os.SystemClock +import androidx.media3.common.MediaMetadata +import androidx.media3.common.util.UnstableApi +import coil.imageLoader +import coil.request.CachePolicy +import coil.request.ImageRequest +import coil.size.Precision +import com.theveloper.pixelplay.data.model.Song +import com.theveloper.pixelplay.data.repository.MusicRepository +import com.theveloper.pixelplay.data.service.player.DualPlayerEngine +import com.theveloper.pixelplay.utils.AlbumArtUtils +import com.theveloper.pixelplay.utils.ArtworkTransportSanitizer +import com.theveloper.pixelplay.utils.LocalArtworkUri +import com.theveloper.pixelplay.utils.MediaItemBuilder +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.withContext +import timber.log.Timber +import java.io.ByteArrayOutputStream +import java.io.InputStream +import java.net.HttpURLConnection +import java.net.URL + +/** + * Resolves, caches, and loads artwork bytes for Glance home-screen widgets. + * + * Extracted from [MusicService] to isolate widget artwork I/O concerns + * (URI resolution, byte loading, failure caching) from the service's + * media-session lifecycle management. + */ +@UnstableApi +class WidgetArtworkResolver( + private val context: Context, + private val engine: DualPlayerEngine, + private val musicRepository: MusicRepository, +) { + + private var cachedWidgetArtSourceKey: String? = null + private var cachedWidgetArtResolvedUri: String? = null + private var cachedWidgetArtBytes: ByteArray? = null + private var cachedWidgetArtLoadFailureKey: String? = null + private var cachedWidgetArtLoadFailureAtMs: Long = 0L + + fun invalidateCachedWidgetArtwork() { + cachedWidgetArtSourceKey = null + cachedWidgetArtResolvedUri = null + cachedWidgetArtBytes = null + cachedWidgetArtLoadFailureKey = null + cachedWidgetArtLoadFailureAtMs = 0L + } + + fun isCurrentWidgetArtworkBackedByTelegram(): Boolean { + val currentItem = engine.masterPlayer.currentMediaItem ?: return false + val metadata = currentItem.mediaMetadata + val contentUriString = currentItem.localConfiguration?.uri?.toString() + ?: metadata.extras?.getString(MediaItemBuilder.EXTERNAL_EXTRA_CONTENT_URI) + val artworkUriString = resolveStoredArtworkUriString(metadata) + return contentUriString?.startsWith("telegram://") == true || + artworkUriString?.startsWith("telegram_art://") == true + } + + suspend fun getAlbumArtForWidget( + mediaId: String?, + embeddedArt: ByteArray?, + artUris: List, + ): Pair = withContext(Dispatchers.IO) { + val sanitizedFromEmbedded = embeddedArt?.takeIf { it.isNotEmpty() }?.let { bytes -> + runCatching { + ArtworkTransportSanitizer.sanitizeEncodedBytes( + data = bytes, + config = ArtworkTransportSanitizer.WIDGET_CONFIG, + ) + }.getOrNull() + } + val candidateUriStrings = LinkedHashSet().apply { + artUris.forEach { candidate -> + candidate.toString() + .takeIf { it.isNotBlank() } + ?.let(::add) + } + }.toList() + val preferredUriString = candidateUriStrings.firstOrNull() + val sourceKey = buildWidgetArtworkSourceKey( + mediaId = mediaId, + candidateUriStrings = candidateUriStrings, + ) + + if (sanitizedFromEmbedded != null) { + cachedWidgetArtSourceKey = sourceKey + cachedWidgetArtResolvedUri = preferredUriString + cachedWidgetArtBytes = sanitizedFromEmbedded + cachedWidgetArtLoadFailureKey = null + cachedWidgetArtLoadFailureAtMs = 0L + return@withContext sanitizedFromEmbedded to preferredUriString + } + + if (sourceKey != null && sourceKey == cachedWidgetArtSourceKey && cachedWidgetArtBytes != null) { + return@withContext cachedWidgetArtBytes to (cachedWidgetArtResolvedUri ?: preferredUriString) + } + if (sourceKey != null && sourceKey == cachedWidgetArtLoadFailureKey) { + val failureAgeMs = SystemClock.elapsedRealtime() - cachedWidgetArtLoadFailureAtMs + if (failureAgeMs < WIDGET_ART_FAILURE_RETRY_MS) { + return@withContext null to preferredUriString + } + } + + val repositoryArtUriString = if (mediaId.isNullOrBlank()) { + null + } else { + resolveRepositoryArtworkUri(mediaId)?.toString() + } + val resolvedUriStrings = LinkedHashSet().apply { + addAll(candidateUriStrings) + repositoryArtUriString + ?.takeIf { it.isNotBlank() } + ?.let(::add) + } + + for (candidateUriString in resolvedUriStrings) { + val candidateUri = parseArtworkUriString(candidateUriString) ?: continue + val loadedBytes = loadArtworkBytes(candidateUri) + if (loadedBytes != null) { + cachedWidgetArtSourceKey = sourceKey + cachedWidgetArtResolvedUri = candidateUriString + cachedWidgetArtBytes = loadedBytes + cachedWidgetArtLoadFailureKey = null + cachedWidgetArtLoadFailureAtMs = 0L + return@withContext loadedBytes to candidateUriString + } + } + + cachedWidgetArtLoadFailureKey = sourceKey + cachedWidgetArtLoadFailureAtMs = SystemClock.elapsedRealtime() + return@withContext null to (repositoryArtUriString ?: preferredUriString) + } + + fun resolveStoredArtworkUriString(metadata: MediaMetadata?): String? { + metadata ?: return null + return metadata.extras + ?.getString(MediaItemBuilder.EXTERNAL_EXTRA_ALBUM_ART) + ?.takeIf { it.isNotBlank() } + ?: metadata.artworkUri + ?.toString() + ?.takeIf { it.isNotBlank() } + } + + fun resolveWidgetArtworkUriCandidates( + metadata: MediaMetadata?, + preferredArtworkUri: Uri? = null, + ): List { + val candidates = LinkedHashSet() + preferredArtworkUri + ?.toString() + ?.takeIf { it.isNotBlank() } + ?.let(candidates::add) + resolveStoredArtworkUriString(metadata)?.let(candidates::add) + metadata?.artworkUri + ?.toString() + ?.takeIf { it.isNotBlank() } + ?.let(candidates::add) + return candidates.mapNotNull(::parseArtworkUriString) + } + + fun resolveArtworkUri(metadata: MediaMetadata?): Uri? { + metadata ?: return null + metadata.artworkUri?.let { return it } + val extrasUri = metadata.extras + ?.getString(MediaItemBuilder.EXTERNAL_EXTRA_ALBUM_ART) + ?.takeIf { it.isNotBlank() } + ?: return null + return parseArtworkUriString(extrasUri) + } + + suspend fun loadArtworkBytes(uri: Uri): ByteArray? { + val uriString = uri.toString() + val scheme = uri.scheme?.lowercase() + val isLocalArtworkUri = LocalArtworkUri.isLocalArtworkUri(uriString) + return when { + isLocalArtworkUri || scheme == "content" || scheme == "file" || scheme == "android.resource" -> { + runCatching { + AlbumArtUtils.openArtworkInputStream(context, uri)?.use { input -> + readBytesCapped(input, ArtworkTransportSanitizer.WIDGET_CONFIG.sourceBytesLimit) + ?.let { bytes -> + ArtworkTransportSanitizer.sanitizeEncodedBytes( + data = bytes, + config = ArtworkTransportSanitizer.WIDGET_CONFIG, + ) + } + } + }.getOrElse { error -> + Timber.tag(TAG).w(error, "Widget artwork read failed for local uri=%s", uri) + null + } + } + scheme == "http" || scheme == "https" -> { + var connection: HttpURLConnection? = null + try { + connection = (URL(uriString).openConnection() as? HttpURLConnection) + ?: return null + connection.connectTimeout = 4_000 + connection.readTimeout = 6_000 + connection.instanceFollowRedirects = true + connection.doInput = true + connection.inputStream.use { input -> + readBytesCapped(input, ArtworkTransportSanitizer.WIDGET_CONFIG.sourceBytesLimit) + ?.let { bytes -> + ArtworkTransportSanitizer.sanitizeEncodedBytes( + data = bytes, + config = ArtworkTransportSanitizer.WIDGET_CONFIG, + ) + } + } + } catch (error: Exception) { + Timber.tag(TAG).w(error, "Widget artwork read failed for remote uri=%s", uri) + null + } finally { + connection?.disconnect() + } + } + else -> loadArtworkBytesViaCoil(context, uri) + } + } + + private fun parseArtworkUriString(rawArtworkUri: String?): Uri? { + if (rawArtworkUri.isNullOrBlank()) { + return null + } + + return MediaItemBuilder.artworkUri(rawArtworkUri) + ?: if (rawArtworkUri.startsWith("/")) { + Uri.fromFile(java.io.File(rawArtworkUri)) + } else { + runCatching { Uri.parse(rawArtworkUri) }.getOrNull() + } + } + + private fun buildWidgetArtworkSourceKey( + mediaId: String?, + candidateUriStrings: List, + ): String? { + val normalizedMediaId = mediaId?.takeIf { it.isNotBlank() } + if (normalizedMediaId == null && candidateUriStrings.isEmpty()) { + return null + } + return buildString { + normalizedMediaId?.let { + append("mediaId=") + append(it) + } + if (candidateUriStrings.isNotEmpty()) { + if (isNotEmpty()) append('|') + append(candidateUriStrings.joinToString(separator = ",")) + } + } + } + + suspend fun resolveRepositoryArtworkUri(mediaId: String?): Uri? { + val songId = mediaId?.takeIf { it.isNotBlank() } ?: return null + val song = withContext(Dispatchers.IO) { + musicRepository.getSong(songId).first() + } ?: return null + + return MediaItemBuilder.artworkUri(song.albumArtUriString) + ?: song.albumArtUriString + ?.takeIf { it.isNotBlank() } + ?.let { raw -> + if (raw.startsWith("/")) Uri.fromFile(java.io.File(raw)) + else runCatching { Uri.parse(raw) }.getOrNull() + } + } + + private fun readBytesCapped(input: InputStream, maxBytes: Int): ByteArray? { + val output = ByteArrayOutputStream(DEFAULT_STREAM_BUFFER_SIZE * 4) + val buffer = ByteArray(DEFAULT_STREAM_BUFFER_SIZE) + var totalRead = 0 + while (true) { + val read = input.read(buffer) + if (read <= 0) break + totalRead += read + if (totalRead > maxBytes) return null + output.write(buffer, 0, read) + } + return output.toByteArray().takeIf { it.isNotEmpty() } + } + + companion object { + private const val TAG = "WidgetArtworkResolver" + private const val DEFAULT_STREAM_BUFFER_SIZE = 8 * 1024 + private const val WIDGET_ART_FAILURE_RETRY_MS = 30_000L + } +} + +suspend fun loadArtworkBytesViaCoil(context: Context, uri: Uri): ByteArray? { + val appContext = context.applicationContext + val request = ImageRequest.Builder(appContext) + .data(uri) + .size( + ArtworkTransportSanitizer.WIDGET_CONFIG.maxDimensionPx, + ArtworkTransportSanitizer.WIDGET_CONFIG.maxDimensionPx, + ) + .precision(Precision.INEXACT) + .allowHardware(false) + .memoryCachePolicy(CachePolicy.ENABLED) + .networkCachePolicy(CachePolicy.ENABLED) + .build() + + return runCatching { + val drawable = appContext.imageLoader.execute(request).drawable ?: return@runCatching null + val fallbackSizePx = ArtworkTransportSanitizer.WIDGET_CONFIG.maxDimensionPx + val bitmap = drawable.toBitmap( + width = drawable.intrinsicWidth.takeIf { it > 0 } ?: fallbackSizePx, + height = drawable.intrinsicHeight.takeIf { it > 0 } ?: fallbackSizePx, + config = Bitmap.Config.ARGB_8888, + ) + val encodedBytes = ByteArrayOutputStream().use { output -> + bitmap.compress(Bitmap.CompressFormat.JPEG, 92, output) + output.toByteArray() + } + ArtworkTransportSanitizer.sanitizeEncodedBytes( + data = encodedBytes, + config = ArtworkTransportSanitizer.WIDGET_CONFIG, + ) + }.getOrElse { error -> + Timber.tag("WidgetArtworkResolver").w(error, "Artwork read failed via Coil for uri=%s", uri) + null + } +} From 7c02f1b13189972d78653f37663ca6010ac8b18c Mon Sep 17 00:00:00 2001 From: Dae Euhwa Date: Sat, 8 Aug 2026 09:12:15 -0500 Subject: [PATCH 72/73] refactor(service): extract SleepTimerController, HeadsetReconnectMonitor, NavidromePlaybackReporter Extract ~150 lines of sleep timer scheduling, headset reconnect monitoring, and Navidrome playback reporting from MusicService into focused components: - SleepTimerController: alarm-based and end-of-track timer logic - HeadsetReconnectMonitor: audio device change monitoring with resume-after-reconnect - NavidromePlaybackReporter: scrobble and periodic playback-state reporting MusicService now delegates to these components via lazy-initialized fields. --- .../pixelplay/data/service/MusicService.kt | 255 ++++-------------- .../playback/HeadsetReconnectMonitor.kt | 107 ++++++++ .../playback/NavidromePlaybackReporter.kt | 86 ++++++ .../service/playback/SleepTimerController.kt | 126 +++++++++ 4 files changed, 368 insertions(+), 206 deletions(-) create mode 100644 app/src/main/java/com/theveloper/pixelplay/data/service/playback/HeadsetReconnectMonitor.kt create mode 100644 app/src/main/java/com/theveloper/pixelplay/data/service/playback/NavidromePlaybackReporter.kt create mode 100644 app/src/main/java/com/theveloper/pixelplay/data/service/playback/SleepTimerController.kt diff --git a/app/src/main/java/com/theveloper/pixelplay/data/service/MusicService.kt b/app/src/main/java/com/theveloper/pixelplay/data/service/MusicService.kt index 30deb2ac3d..5d47f41542 100644 --- a/app/src/main/java/com/theveloper/pixelplay/data/service/MusicService.kt +++ b/app/src/main/java/com/theveloper/pixelplay/data/service/MusicService.kt @@ -79,6 +79,9 @@ import com.theveloper.pixelplay.data.service.auto.AutoMediaBrowseTree import com.theveloper.pixelplay.data.service.wear.buildWearThemePalette import com.theveloper.pixelplay.data.service.wear.WearStatePublisher import com.theveloper.pixelplay.data.service.playback.PlaybackSnapshotManager +import com.theveloper.pixelplay.data.service.playback.HeadsetReconnectMonitor +import com.theveloper.pixelplay.data.service.playback.NavidromePlaybackReporter +import com.theveloper.pixelplay.data.service.playback.SleepTimerController import com.theveloper.pixelplay.data.service.widget.WidgetArtworkResolver import com.theveloper.pixelplay.presentation.viewmodel.ColorSchemePair import com.theveloper.pixelplay.shared.WearIntents @@ -156,7 +159,6 @@ class MusicService : MediaLibraryService() { private val alarmManager by lazy { getSystemService(Context.ALARM_SERVICE) as AlarmManager } - private var endOfTrackTimerSongId: String? = null // Cast remote-session synchronization, extracted to a standalone coordinator. // Lazily built so the Hilt-injected listeningStatsTracker is ready before first use. private val castSyncCoordinator by lazy { @@ -195,12 +197,34 @@ class MusicService : MediaLibraryService() { it.setShuffleStateProvider { isManualShuffleEnabled } } } + private val sleepTimerController by lazy { + SleepTimerController( + context = this, + alarmManager = alarmManager, + actionSleepTimerExpired = ACTION_SLEEP_TIMER_EXPIRED, + currentPlayer = { mediaSession?.player }, + currentMediaId = { mediaSession?.player?.currentMediaItem?.mediaId }, + ) + } + private val headsetReconnectMonitor by lazy { + HeadsetReconnectMonitor( + audioManager = audioManager, + engine = engine, + isResumeEnabled = { resumeOnHeadsetReconnectEnabled }, + currentPlayer = { engine.masterPlayer }, + ) + } + private val navidromeReporter by lazy { + NavidromePlaybackReporter( + navidromeRepository = navidromeRepository, + engine = engine, + serviceScope = serviceScope, + appScope = appScope, + ) + } private val audioManager by lazy { getSystemService(Context.AUDIO_SERVICE) as AudioManager } - private var headsetReconnectCallback: AudioDeviceCallback? = null - private var shouldResumeAfterHeadsetReconnect = false - private var lastNoisyPauseRealtimeMs = 0L private var resumeOnHeadsetReconnectEnabled = false private var pauseOnVolumeZeroEnabled = false private var temporaryForegroundStartedInOnCreate = false @@ -915,82 +939,17 @@ class MusicService : MediaLibraryService() { return hasWearHints } - private fun createSleepTimerPendingIntent(): PendingIntent { - val intent = Intent(this, SleepTimerReceiver::class.java).apply { - action = ACTION_SLEEP_TIMER_EXPIRED - setPackage(packageName) - } - return PendingIntent.getBroadcast( - this, - 0, - intent, - PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE, - ) - } + private fun createSleepTimerPendingIntent() = sleepTimerController.createSleepTimerPendingIntent() - private fun cancelDurationSleepTimerInternal() { - alarmManager.cancel(createSleepTimerPendingIntent()) - } + private fun cancelDurationSleepTimerInternal() = sleepTimerController.cancelDurationSleepTimerInternal() - private fun setDurationSleepTimer(minutes: Int) { - if (minutes <= 0) { - cancelSleepTimers() - return - } - endOfTrackTimerSongId = null - val triggerAtMillis = System.currentTimeMillis() + (minutes * 60_000L) - val pendingIntent = createSleepTimerPendingIntent() + private fun setDurationSleepTimer(minutes: Int) = sleepTimerController.setDurationSleepTimer(minutes) - try { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - if (alarmManager.canScheduleExactAlarms()) { - alarmManager.setExactAndAllowWhileIdle( - AlarmManager.RTC_WAKEUP, - triggerAtMillis, - pendingIntent, - ) - } else { - alarmManager.setAndAllowWhileIdle( - AlarmManager.RTC_WAKEUP, - triggerAtMillis, - pendingIntent, - ) - } - } else - alarmManager.setExactAndAllowWhileIdle( - AlarmManager.RTC_WAKEUP, - triggerAtMillis, - pendingIntent, - ) - Timber.tag(TAG).d("Sleep timer set from Wear for %d minutes", minutes) - } catch (e: SecurityException) { - Timber.tag(TAG).w(e, "Exact alarm denied; using inexact sleep timer") - alarmManager.set(AlarmManager.RTC_WAKEUP, triggerAtMillis, pendingIntent) - } - } + private fun setEndOfTrackSleepTimer(enabled: Boolean) = sleepTimerController.setEndOfTrackSleepTimer(enabled) - private fun setEndOfTrackSleepTimer(enabled: Boolean) { - if (!enabled) { - endOfTrackTimerSongId = null - Timber.tag(TAG).d("End-of-track timer disabled from Wear") - return - } - cancelDurationSleepTimerInternal() - val currentSongId = mediaSession?.player?.currentMediaItem?.mediaId - if (currentSongId.isNullOrBlank()) { - endOfTrackTimerSongId = null - Timber.tag(TAG).d("End-of-track timer ignored: no active song") - return - } - endOfTrackTimerSongId = currentSongId - Timber.tag(TAG).d("End-of-track timer set from Wear for mediaId=%s", currentSongId) - } + private fun cancelSleepTimers() = sleepTimerController.cancelSleepTimers() - private fun cancelSleepTimers() { - cancelDurationSleepTimerInternal() - endOfTrackTimerSongId = null - Timber.tag(TAG).d("Sleep timers cancelled from Wear") - } + private val endOfTrackTimerSongId: String? get() = sleepTimerController.endOfTrackTimerSongId private fun startTemporaryForegroundForCommand() { val notification = NotificationCompat.Builder( @@ -1149,64 +1108,16 @@ class MusicService : MediaLibraryService() { return startCommandResult } - private fun getNavidromeId(mediaItem: MediaItem?): String? { - if (mediaItem == null) return null - return mediaItem.mediaMetadata.extras?.getString(MediaItemBuilder.EXTERNAL_EXTRA_NAVIDROME_ID) - ?: mediaItem.mediaId.let { if (it.startsWith("navidrome_")) it.substringAfter("navidrome_") else null } - ?: mediaItem.mediaMetadata.extras?.getString(MediaItemBuilder.EXTERNAL_EXTRA_CONTENT_URI)?.let { - if (it.startsWith("navidrome://")) it.substringAfter("navidrome://") else null - } - } - - private fun isNavidromeMediaItem(mediaItem: MediaItem?): Boolean { - return getNavidromeId(mediaItem) != null - } - - private fun reportNavidromePlayback(state: String, mediaItem: MediaItem? = engine.masterPlayer.currentMediaItem) { - val player = engine.masterPlayer - // Ensure we capture player state on main thread to avoid IllegalStateException - val targetItem = mediaItem ?: return - val navidromeId = getNavidromeId(targetItem) ?: return - - // If reporting for current item, use player position. - // If reporting "stopped" for a transition, use the item's duration as final position. - val positionMs = if (targetItem === player.currentMediaItem) { - player.currentPosition - } else { - targetItem.mediaMetadata.extras?.getLong(MediaItemBuilder.EXTERNAL_EXTRA_DURATION) ?: 0L - } - val playbackRate = player.playbackParameters.speed + private fun getNavidromeId(mediaItem: MediaItem?) = navidromeReporter.getNavidromeId(mediaItem) - // Use appScope for the network call so it survives if serviceScope is cancelled - appScope.launch(Dispatchers.IO) { - navidromeRepository.reportPlayback( - navidromeId = navidromeId, - positionMs = positionMs, - state = state, - playbackRate = playbackRate - ) - } - } + private fun isNavidromeMediaItem(mediaItem: MediaItem?) = navidromeReporter.isNavidromeMediaItem(mediaItem) - private var navidromePlaybackReportJob: Job? = null + private fun reportNavidromePlayback(state: String, mediaItem: MediaItem? = engine.masterPlayer.currentMediaItem) = + navidromeReporter.reportPlayback(state, mediaItem) - private fun startNavidromePlaybackReporting() { - navidromePlaybackReportJob?.cancel() - navidromePlaybackReportJob = serviceScope.launch { - while (true) { - delay(30_000) // Report every 30 seconds - val player = engine.masterPlayer - if (player.isPlaying && isNavidromeMediaItem(player.currentMediaItem)) { - reportNavidromePlayback("playing") - } - } - } - } + private fun startNavidromePlaybackReporting() = navidromeReporter.startReporting() - private fun stopNavidromePlaybackReporting() { - navidromePlaybackReportJob?.cancel() - navidromePlaybackReportJob = null - } + private fun stopNavidromePlaybackReporting() = navidromeReporter.stopReporting() private val playerListener = object : Player.Listener { override fun onVolumeChanged(volume: Float) { @@ -1256,9 +1167,7 @@ class MusicService : MediaLibraryService() { playWhenReady -> clearHeadsetReconnectResume() !resumeOnHeadsetReconnectEnabled -> clearHeadsetReconnectResume() reason == Player.PLAY_WHEN_READY_CHANGE_REASON_AUDIO_BECOMING_NOISY -> { - shouldResumeAfterHeadsetReconnect = true - lastNoisyPauseRealtimeMs = SystemClock.elapsedRealtime() - Timber.tag(TAG).d("Marked playback for headset reconnect resume") + headsetReconnectMonitor.markNoisyPause() } else -> clearHeadsetReconnectResume() } @@ -1284,7 +1193,7 @@ class MusicService : MediaLibraryService() { } } - endOfTrackTimerSongId = null + cancelSleepTimers() reportNavidromePlayback("stopped") stopNavidromePlaybackReporting() } else { @@ -1356,7 +1265,7 @@ class MusicService : MediaLibraryService() { stopNavidromePlaybackReporting() } - val eotTargetSongId = endOfTrackTimerSongId + val eotTargetSongId = sleepTimerController.endOfTrackTimerSongId if (!eotTargetSongId.isNullOrBlank()) { if (reason == Player.MEDIA_ITEM_TRANSITION_REASON_AUTO) { val previousSongId = engine.masterPlayer.run { @@ -1367,13 +1276,13 @@ class MusicService : MediaLibraryService() { } } if (previousSongId == eotTargetSongId) { - endOfTrackTimerSongId = null + cancelSleepTimers() engine.masterPlayer.seekTo(0L) engine.masterPlayer.pause() Timber.tag(TAG).d("Paused playback at end of track from Wear timer") } } else if (mediaItem?.mediaId != eotTargetSongId) { - endOfTrackTimerSongId = null + cancelSleepTimers() Timber.tag(TAG).d("Cleared end-of-track timer after manual track change") } } @@ -1505,25 +1414,11 @@ class MusicService : MediaLibraryService() { } } - private fun registerHeadsetReconnectMonitor() { - val callback = object : AudioDeviceCallback() { - override fun onAudioDevicesAdded(addedDevices: Array) { - if (!addedDevices.any(::isReconnectableHeadsetOutput)) return - maybeResumeAfterHeadsetReconnect() - } - } + private fun registerHeadsetReconnectMonitor() = headsetReconnectMonitor.register() - audioManager.registerAudioDeviceCallback(callback, null) - headsetReconnectCallback = callback - } + private fun unregisterHeadsetReconnectMonitor() = headsetReconnectMonitor.unregister() - private fun unregisterHeadsetReconnectMonitor() { - headsetReconnectCallback?.let { callback -> - runCatching { audioManager.unregisterAudioDeviceCallback(callback) } - } - headsetReconnectCallback = null - clearHeadsetReconnectResume() - } + private fun clearHeadsetReconnectResume() = headsetReconnectMonitor.clearResume() private fun registerSystemVolumeObserver() { contentResolver.registerContentObserver( @@ -1537,58 +1432,6 @@ class MusicService : MediaLibraryService() { runCatching { contentResolver.unregisterContentObserver(systemVolumeObserver) } } - private fun maybeResumeAfterHeadsetReconnect() { - if (!resumeOnHeadsetReconnectEnabled || !shouldResumeAfterHeadsetReconnect) return - - val elapsedSinceNoisyPause = SystemClock.elapsedRealtime() - lastNoisyPauseRealtimeMs - if (elapsedSinceNoisyPause > HEADSET_RECONNECT_RESUME_WINDOW_MS) { - clearHeadsetReconnectResume() - return - } - - if (!hasReconnectableHeadsetOutput()) { - return - } - - val player = engine.masterPlayer - if ( - player.currentMediaItem == null || - player.playWhenReady || - player.playbackState == Player.STATE_IDLE || - player.playbackState == Player.STATE_ENDED - ) { - clearHeadsetReconnectResume() - return - } - - Timber.tag(TAG).d("Resuming playback after headset reconnect") - clearHeadsetReconnectResume() - player.play() - } - - private fun hasReconnectableHeadsetOutput(): Boolean { - return audioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS) - .any(::isReconnectableHeadsetOutput) - } - - private fun isReconnectableHeadsetOutput(device: AudioDeviceInfo): Boolean { - return when (device.type) { - AudioDeviceInfo.TYPE_WIRED_HEADSET, - AudioDeviceInfo.TYPE_WIRED_HEADPHONES, - AudioDeviceInfo.TYPE_USB_HEADSET, - AudioDeviceInfo.TYPE_BLUETOOTH_A2DP, - AudioDeviceInfo.TYPE_BLUETOOTH_SCO, - AudioDeviceInfo.TYPE_BLE_HEADSET, - AudioDeviceInfo.TYPE_BLE_SPEAKER -> true - else -> false - } - } - - private fun clearHeadsetReconnectResume() { - shouldResumeAfterHeadsetReconnect = false - lastNoisyPauseRealtimeMs = 0L - } - private fun schedulePlaybackSnapshotPersist(immediate: Boolean = false) { snapshotManager.schedulePersist(immediate) } @@ -2087,7 +1930,7 @@ class MusicService : MediaLibraryService() { clearHeadsetReconnectResume() cancelDurationSleepTimerInternal() - endOfTrackTimerSongId = null + cancelSleepTimers() if (preservePlaybackSnapshot) { snapshotManager.persistOnUnload() diff --git a/app/src/main/java/com/theveloper/pixelplay/data/service/playback/HeadsetReconnectMonitor.kt b/app/src/main/java/com/theveloper/pixelplay/data/service/playback/HeadsetReconnectMonitor.kt new file mode 100644 index 0000000000..75a7d7df5e --- /dev/null +++ b/app/src/main/java/com/theveloper/pixelplay/data/service/playback/HeadsetReconnectMonitor.kt @@ -0,0 +1,107 @@ +package com.theveloper.pixelplay.data.service.playback + +import android.media.AudioDeviceCallback +import android.media.AudioDeviceInfo +import android.media.AudioManager +import android.os.SystemClock +import androidx.media3.common.Player +import com.theveloper.pixelplay.data.service.player.DualPlayerEngine +import timber.log.Timber + +/** + * Monitors audio device changes and resumes playback after headset reconnection. + * + * Extracted from [MusicService] to isolate headset monitoring from the + * service's media-session lifecycle management. + */ +class HeadsetReconnectMonitor( + private val audioManager: AudioManager, + private val engine: DualPlayerEngine, + private val isResumeEnabled: () -> Boolean, + private val currentPlayer: () -> Player?, +) { + + private var callback: AudioDeviceCallback? = null + private var shouldResume = false + private var lastNoisyPauseRealtimeMs = 0L + + fun register() { + val deviceCallback = object : AudioDeviceCallback() { + override fun onAudioDevicesAdded(addedDevices: Array) { + if (!addedDevices.any(::isReconnectableHeadsetOutput)) return + maybeResume() + } + } + audioManager.registerAudioDeviceCallback(deviceCallback, null) + callback = deviceCallback + } + + fun unregister() { + callback?.let { c -> + runCatching { audioManager.unregisterAudioDeviceCallback(c) } + } + callback = null + clearResume() + } + + fun markNoisyPause() { + shouldResume = true + lastNoisyPauseRealtimeMs = SystemClock.elapsedRealtime() + Timber.tag(TAG).d("Marked playback for headset reconnect resume") + } + + fun clearResume() { + shouldResume = false + lastNoisyPauseRealtimeMs = 0L + } + + private fun maybeResume() { + if (!isResumeEnabled() || !shouldResume) return + + val elapsedSinceNoisyPause = SystemClock.elapsedRealtime() - lastNoisyPauseRealtimeMs + if (elapsedSinceNoisyPause > RESUME_WINDOW_MS) { + clearResume() + return + } + + if (!hasReconnectableHeadsetOutput()) return + + val player = currentPlayer() ?: return + if ( + player.currentMediaItem == null || + player.playWhenReady || + player.playbackState == Player.STATE_IDLE || + player.playbackState == Player.STATE_ENDED + ) { + clearResume() + return + } + + Timber.tag(TAG).d("Resuming playback after headset reconnect") + clearResume() + player.play() + } + + private fun hasReconnectableHeadsetOutput(): Boolean { + return audioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS) + .any(::isReconnectableHeadsetOutput) + } + + private fun isReconnectableHeadsetOutput(device: AudioDeviceInfo): Boolean { + return when (device.type) { + AudioDeviceInfo.TYPE_WIRED_HEADSET, + AudioDeviceInfo.TYPE_WIRED_HEADPHONES, + AudioDeviceInfo.TYPE_USB_HEADSET, + AudioDeviceInfo.TYPE_BLUETOOTH_A2DP, + AudioDeviceInfo.TYPE_BLUETOOTH_SCO, + AudioDeviceInfo.TYPE_BLE_HEADSET, + AudioDeviceInfo.TYPE_BLE_SPEAKER -> true + else -> false + } + } + + companion object { + private const val TAG = "HeadsetReconnectMonitor" + private const val RESUME_WINDOW_MS = 15_000L + } +} diff --git a/app/src/main/java/com/theveloper/pixelplay/data/service/playback/NavidromePlaybackReporter.kt b/app/src/main/java/com/theveloper/pixelplay/data/service/playback/NavidromePlaybackReporter.kt new file mode 100644 index 0000000000..5e11cb899a --- /dev/null +++ b/app/src/main/java/com/theveloper/pixelplay/data/service/playback/NavidromePlaybackReporter.kt @@ -0,0 +1,86 @@ +package com.theveloper.pixelplay.data.service.playback + +import androidx.media3.common.MediaItem +import com.theveloper.pixelplay.data.navidrome.NavidromeRepository +import com.theveloper.pixelplay.data.service.player.DualPlayerEngine +import com.theveloper.pixelplay.utils.MediaItemBuilder +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import timber.log.Timber + +/** + * Handles Navidrome scrobble/playback reporting: periodic "playing" reports + * and "stopped"/"paused" reports on state changes. + * + * Extracted from [MusicService] to isolate Navidrome-specific reporting + * from the service's media-session lifecycle management. + */ +class NavidromePlaybackReporter( + private val navidromeRepository: NavidromeRepository, + private val engine: DualPlayerEngine, + private val serviceScope: CoroutineScope, + private val appScope: CoroutineScope, +) { + + private var reportJob: Job? = null + + fun getNavidromeId(mediaItem: MediaItem?): String? { + mediaItem ?: return null + return mediaItem.mediaMetadata.extras?.getString(MediaItemBuilder.EXTERNAL_EXTRA_SONG_ID) + ?: mediaItem.mediaId.let { if (it.startsWith("navidrome_")) it.substringAfter("navidrome_") else null } + ?: mediaItem.mediaMetadata.extras?.getString(MediaItemBuilder.EXTERNAL_EXTRA_CONTENT_URI)?.let { + if (it.startsWith("navidrome://")) it.substringAfter("navidrome://") else null + } + } + + fun isNavidromeMediaItem(mediaItem: MediaItem?): Boolean { + return getNavidromeId(mediaItem) != null + } + + fun reportPlayback(state: String, mediaItem: MediaItem? = engine.masterPlayer.currentMediaItem) { + val player = engine.masterPlayer + val targetItem = mediaItem ?: return + val navidromeId = getNavidromeId(targetItem) ?: return + + val positionMs = if (targetItem === player.currentMediaItem) { + player.currentPosition + } else { + targetItem.mediaMetadata.extras?.getLong(MediaItemBuilder.EXTERNAL_EXTRA_DURATION) ?: 0L + } + val playbackRate = player.playbackParameters.speed + + appScope.launch(Dispatchers.IO) { + navidromeRepository.reportPlayback( + navidromeId = navidromeId, + positionMs = positionMs, + state = state, + playbackRate = playbackRate + ) + } + } + + fun startReporting() { + reportJob?.cancel() + reportJob = serviceScope.launch { + while (true) { + delay(30_000) + val player = engine.masterPlayer + if (player.isPlaying && isNavidromeMediaItem(player.currentMediaItem)) { + reportPlayback("playing") + } + } + } + } + + fun stopReporting() { + reportJob?.cancel() + reportJob = null + } + + companion object { + private const val TAG = "NavidromePlaybackReporter" + } +} diff --git a/app/src/main/java/com/theveloper/pixelplay/data/service/playback/SleepTimerController.kt b/app/src/main/java/com/theveloper/pixelplay/data/service/playback/SleepTimerController.kt new file mode 100644 index 0000000000..126b1579ba --- /dev/null +++ b/app/src/main/java/com/theveloper/pixelplay/data/service/playback/SleepTimerController.kt @@ -0,0 +1,126 @@ +package com.theveloper.pixelplay.data.service.playback + +import android.app.AlarmManager +import android.app.PendingIntent +import android.content.Context +import android.content.Intent +import android.os.Build +import androidx.media3.common.Player +import com.theveloper.pixelplay.data.service.SleepTimerReceiver +import timber.log.Timber + +/** + * Manages duration-based and end-of-track sleep timers. + * + * Extracted from [MusicService] to isolate timer scheduling and + * cancellation logic from the service's media-session lifecycle. + */ +class SleepTimerController( + private val context: Context, + private val alarmManager: AlarmManager, + private val actionSleepTimerExpired: String, + private val currentPlayer: () -> Player?, + private val currentMediaId: () -> String?, +) { + + var endOfTrackTimerSongId: String? = null + private set + + fun createSleepTimerPendingIntent(): PendingIntent { + val intent = Intent(context, SleepTimerReceiver::class.java).apply { + action = actionSleepTimerExpired + setPackage(context.packageName) + } + return PendingIntent.getBroadcast( + context, + 0, + intent, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE, + ) + } + + fun cancelDurationSleepTimerInternal() { + alarmManager.cancel(createSleepTimerPendingIntent()) + } + + fun setDurationSleepTimer(minutes: Int) { + if (minutes <= 0) { + cancelSleepTimers() + return + } + endOfTrackTimerSongId = null + val triggerAtMillis = System.currentTimeMillis() + (minutes * 60_000L) + val pendingIntent = createSleepTimerPendingIntent() + + try { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + if (alarmManager.canScheduleExactAlarms()) { + alarmManager.setExactAndAllowWhileIdle( + AlarmManager.RTC_WAKEUP, + triggerAtMillis, + pendingIntent, + ) + } else { + alarmManager.setAndAllowWhileIdle( + AlarmManager.RTC_WAKEUP, + triggerAtMillis, + pendingIntent, + ) + } + } else + alarmManager.setExactAndAllowWhileIdle( + AlarmManager.RTC_WAKEUP, + triggerAtMillis, + pendingIntent, + ) + Timber.tag(TAG).d("Sleep timer set for %d minutes", minutes) + } catch (e: SecurityException) { + Timber.tag(TAG).w(e, "Exact alarm denied; using inexact sleep timer") + alarmManager.set(AlarmManager.RTC_WAKEUP, triggerAtMillis, pendingIntent) + } + } + + fun setEndOfTrackSleepTimer(enabled: Boolean) { + if (!enabled) { + endOfTrackTimerSongId = null + Timber.tag(TAG).d("End-of-track timer disabled") + return + } + cancelDurationSleepTimerInternal() + val songId = currentMediaId() + if (songId.isNullOrBlank()) { + endOfTrackTimerSongId = null + Timber.tag(TAG).d("End-of-track timer ignored: no active song") + return + } + endOfTrackTimerSongId = songId + Timber.tag(TAG).d("End-of-track timer set for mediaId=%s", songId) + } + + fun cancelSleepTimers() { + cancelDurationSleepTimerInternal() + endOfTrackTimerSongId = null + Timber.tag(TAG).d("Sleep timers cancelled") + } + + /** + * Checks if the current track matches the end-of-track timer and pauses if so. + * Returns true if the timer fired, false otherwise. + */ + fun checkEndOfTrackTimer(): Boolean { + val timerSongId = endOfTrackTimerSongId ?: return false + val player = currentPlayer() ?: return false + val currentId = player.currentMediaItem?.mediaId + if (currentId == timerSongId && player.isPlaying) { + player.pause() + endOfTrackTimerSongId = null + Timber.tag(TAG).d("End-of-track timer fired: paused playback") + return true + } + return false + } + + companion object { + private const val TAG = "SleepTimerController" + } +} From 62e4de4078dba0c02b788f9f6d2cbe0cf683503b Mon Sep 17 00:00:00 2001 From: Dae Euhwa Date: Sat, 8 Aug 2026 09:15:14 -0500 Subject: [PATCH 73/73] refactor(service): extract CountedPlayController from MusicService Extract counted-play state tracking (play N times then pause) from MusicService into a focused CountedPlayController class. The Auto queue resolver and MediaSession UI refresher remain in MusicService as their tight coupling to MediaSession callbacks, widget manager, and service scope means extraction would add indirection without decoupling. --- .../pixelplay/data/service/MusicService.kt | 85 ++---------------- .../service/playback/CountedPlayController.kt | 86 +++++++++++++++++++ 2 files changed, 92 insertions(+), 79 deletions(-) create mode 100644 app/src/main/java/com/theveloper/pixelplay/data/service/playback/CountedPlayController.kt diff --git a/app/src/main/java/com/theveloper/pixelplay/data/service/MusicService.kt b/app/src/main/java/com/theveloper/pixelplay/data/service/MusicService.kt index 5d47f41542..6cb5fda92f 100644 --- a/app/src/main/java/com/theveloper/pixelplay/data/service/MusicService.kt +++ b/app/src/main/java/com/theveloper/pixelplay/data/service/MusicService.kt @@ -82,6 +82,7 @@ import com.theveloper.pixelplay.data.service.playback.PlaybackSnapshotManager import com.theveloper.pixelplay.data.service.playback.HeadsetReconnectMonitor import com.theveloper.pixelplay.data.service.playback.NavidromePlaybackReporter import com.theveloper.pixelplay.data.service.playback.SleepTimerController +import com.theveloper.pixelplay.data.service.playback.CountedPlayController import com.theveloper.pixelplay.data.service.widget.WidgetArtworkResolver import com.theveloper.pixelplay.presentation.viewmodel.ColorSchemePair import com.theveloper.pixelplay.shared.WearIntents @@ -150,12 +151,6 @@ class MusicService : MediaLibraryService() { private var persistentShuffleEnabled = false // Holds the previous main-thread UncaughtExceptionHandler so we can restore it in onDestroy. private var previousMainThreadExceptionHandler: Thread.UncaughtExceptionHandler? = null - // --- Counted Play State --- - private var countedPlayActive = false - private var countedPlayTarget = 0 - private var countedPlayCount = 0 - private var countedOriginalId: String? = null - private var countedPlayListener: Player.Listener? = null private val alarmManager by lazy { getSystemService(Context.ALARM_SERVICE) as AlarmManager } @@ -222,6 +217,9 @@ class MusicService : MediaLibraryService() { appScope = appScope, ) } + private val countedPlayController by lazy { + CountedPlayController(engine = engine) + } private val audioManager by lazy { getSystemService(Context.AUDIO_SERVICE) as AudioManager } @@ -2226,80 +2224,9 @@ class MusicService : MediaLibraryService() { // ------------------------ // Counted Play Controls // ------------------------ - fun startCountedPlay(count: Int) { - val player = engine.masterPlayer - val currentItem = player.currentMediaItem ?: return - - stopCountedPlay() // reset previous - - countedPlayTarget = count - countedPlayCount = 1 - countedOriginalId = currentItem.mediaId - countedPlayActive = true - - // Force repeat-one - player.repeatMode = Player.REPEAT_MODE_ONE - - val listener = object : Player.Listener { - - override fun onPositionDiscontinuity( - oldPosition: Player.PositionInfo, - newPosition: Player.PositionInfo, - reason: Int - ) { - if (!countedPlayActive) return - - if (reason == Player.DISCONTINUITY_REASON_AUTO_TRANSITION) { - countedPlayCount++ - - if (countedPlayCount > countedPlayTarget) { - player.pause() - stopCountedPlay() - return - } - } - } - - override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) { - if (!countedPlayActive) return + fun startCountedPlay(count: Int) = countedPlayController.start(count) - // If user manually changes the song -> cancel - if (mediaItem?.mediaId != countedOriginalId) { - stopCountedPlay() - } - } - - override fun onRepeatModeChanged(repeatMode: Int) { - // User explicitly changed repeat mode while counted play is active: - // cancel counted play and accept the new mode instead of fighting back. - if (countedPlayActive && repeatMode != Player.REPEAT_MODE_ONE) { - stopCountedPlay(restoreRepeatMode = false) - } - } - } - - countedPlayListener = listener - player.addListener(listener) - } - - fun stopCountedPlay(restoreRepeatMode: Boolean = true) { - if (!countedPlayActive) return - - countedPlayActive = false - countedPlayTarget = 0 - countedPlayCount = 0 - countedOriginalId = null - - countedPlayListener?.let { - engine.masterPlayer.removeListener(it) - } - countedPlayListener = null - - // Restore normal repeat mode (OFF) only when not triggered by a user repeat-mode change - if (restoreRepeatMode) { - engine.masterPlayer.repeatMode = Player.REPEAT_MODE_OFF - } - } + fun stopCountedPlay(restoreRepeatMode: Boolean = true) = countedPlayController.stop(restoreRepeatMode) /** * Bridges a suspend block into a [ListenableFuture] for Media3 callback methods. diff --git a/app/src/main/java/com/theveloper/pixelplay/data/service/playback/CountedPlayController.kt b/app/src/main/java/com/theveloper/pixelplay/data/service/playback/CountedPlayController.kt new file mode 100644 index 0000000000..8083850f9d --- /dev/null +++ b/app/src/main/java/com/theveloper/pixelplay/data/service/playback/CountedPlayController.kt @@ -0,0 +1,86 @@ +package com.theveloper.pixelplay.data.service.playback + +import androidx.media3.common.MediaItem +import androidx.media3.common.Player +import com.theveloper.pixelplay.data.service.player.DualPlayerEngine + +/** + * Manages "counted play" — play a track N times then pause. + * Used by the notification and Wear for "play 1/3/5 times" features. + * + * Extracted from [MusicService] to isolate counted-play state tracking + * from the service's media-session lifecycle management. + */ +class CountedPlayController( + private val engine: DualPlayerEngine, +) { + + private var active = false + private var target = 0 + private var count = 0 + private var originalId: String? = null + private var listener: Player.Listener? = null + + fun start(count: Int) { + val player = engine.masterPlayer + val currentItem = player.currentMediaItem ?: return + + stop() + + target = count + this.count = 1 + originalId = currentItem.mediaId + active = true + + player.repeatMode = Player.REPEAT_MODE_ONE + + val l = object : Player.Listener { + override fun onPositionDiscontinuity( + oldPosition: Player.PositionInfo, + newPosition: Player.PositionInfo, + reason: Int + ) { + if (!active) return + if (reason == Player.DISCONTINUITY_REASON_AUTO_TRANSITION) { + this@CountedPlayController.count++ + if (this@CountedPlayController.count > target) { + player.pause() + stop() + } + } + } + + override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) { + if (!active) return + if (mediaItem?.mediaId != originalId) { + stop() + } + } + + override fun onRepeatModeChanged(repeatMode: Int) { + if (active && repeatMode != Player.REPEAT_MODE_ONE) { + stop(restoreRepeatMode = false) + } + } + } + + listener = l + player.addListener(l) + } + + fun stop(restoreRepeatMode: Boolean = true) { + if (!active) return + + active = false + target = 0 + count = 0 + originalId = null + + listener?.let { engine.masterPlayer.removeListener(it) } + listener = null + + if (restoreRepeatMode) { + engine.masterPlayer.repeatMode = Player.REPEAT_MODE_OFF + } + } +}