From 13aaad98d0f3fa32449d74f262275fde6eb5adc3 Mon Sep 17 00:00:00 2001 From: Rebornloki Date: Sat, 13 Jun 2026 09:39:20 +0100 Subject: [PATCH 01/72] Update French changelog strings and translations --- .../main/res/values-fr/strings_changelogs.xml | 230 ++++++++++-------- 1 file changed, 134 insertions(+), 96 deletions(-) diff --git a/app/src/main/res/values-fr/strings_changelogs.xml b/app/src/main/res/values-fr/strings_changelogs.xml index 380efb769..9aabbbb0c 100644 --- a/app/src/main/res/values-fr/strings_changelogs.xml +++ b/app/src/main/res/values-fr/strings_changelogs.xml @@ -4,129 +4,167 @@ Voir sur GitHub Améliorations Corrections - Nouveautés - Ajouté + Quoi de neuf + Ajouts + - Support Chromecast pour diffuser l\'audio depuis votre appareil. + Prise en charge de Chromecast pour diffuser l’audio depuis votre appareil. Journal des modifications intégré pour vous tenir informé des dernières fonctionnalités. - Support des fichiers .LRC, intégrés et externes. + Prise en charge des fichiers .LRC, intégrés et externes. Support des paroles hors ligne. - Paroles synchronisées (synchronisées avec le titre). - Nouvel écran pour voir la file d\'attente complète. - Réorganiser et supprimer des titres de la file d\'attente. - Gestes du mini-lecteur (glisser vers le bas pour fermer). - Ajout de plus d\'animations Material. - Nouveaux paramètres pour personnaliser l\'apparence. - Nouveaux paramètres pour vider le cache. - + Paroles synchronisées avec la musique. + Nouvel écran pour afficher la file d’attente complète. + Réorganisation et suppression de morceaux dans la file d’attente. + Gestes du mini-lecteur (balayer vers le bas pour fermer). + Ajout de nouvelles animations Material. + Nouveaux paramètres pour personnaliser l’apparence. + Nouveau paramètre pour vider le cache. + + - Refonte complète de l\'interface utilisateur. + Refonte complète de l’interface utilisateur. Refonte complète du lecteur. - Améliorations de performance dans la bibliothèque. - Vitesse de démarrage de l\'application améliorée. - L\'IA fournit maintenant de meilleurs résultats. + Amélioration des performances de la bibliothèque. + Amélioration de la vitesse de démarrage de l’application. + L’IA fournit désormais de meilleurs résultats. + - Correction de divers bugs dans l\'éditeur de tags. - Correction d\'un bug où la notification de lecture ne se fermait pas. - Correction de plusieurs bugs qui faisaient planter l\'application. + Correction de divers bugs dans l’éditeur de tags. + Correction d’un bug où la notification de lecture ne se supprimait pas. + Correction de plusieurs bugs provoquant des plantages. + - Introduction d\'un centre de statistiques d\'écoute plus riche avec des analyses plus approfondies de vos sessions. - Lancement d\'un lecteur rapide flottant pour ouvrir et prévisualiser instantanément les fichiers locaux. - Ajout d\'un onglet dossiers avec un navigateur en arborescence et une vue prête pour playlist. + Introduction d’un hub de statistiques d’écoute plus riche avec des analyses détaillées. + Ajout d’un lecteur flottant rapide pour ouvrir et prévisualiser les fichiers locaux. + Ajout d’un onglet dossiers avec navigation en arbre et vue playlist. + - Interface Material 3 globale affinée pour une expérience plus épurée et cohérente. - L\'édition de métadonnées supporte maintenant le changement de pochette. - Animations et transitions adoucies dans toute l\'application pour une navigation plus fluide. - Mise en page de l\'écran artiste améliorée avec plus de détails et de peaufinage. - Génération DailyMix et YourMix améliorée avec des sélections plus intelligentes et diversifiées. - Renforcement de la génération de playlist IA. - Pertinence et présentation de la recherche améliorées pour une découverte plus rapide. - Support élargi pour une plus large gamme de formats de fichiers audio. - + Amélioration de l’interface Material 3 pour une expérience plus cohérente. + L’édition des métadonnées prend désormais en charge la pochette d’album. + Animations et transitions plus fluides dans toute l’application. + Amélioration de la page artiste avec plus de détails et de finition. + Amélioration des playlists DailyMix et YourMix avec des sélections plus variées. + Amélioration du moteur de génération de playlists IA. + Recherche améliorée pour une découverte plus rapide. + Support élargi pour davantage de formats audio. + + - Problèmes de métadonnées résolus pour que les détails des titres restent précis partout. - Raccourcis de notification restaurés pour revenir de manière fiable à la lecture. + Correction de problèmes de métadonnées incohérentes. + Restauration des raccourcis de notification vers la lecture. + Refonte majeure de la navigation - Nouvel explorateur de fichiers pour choisir les répertoires sources - Nouvelles fonctionnalités de connectivité et de diffusion - Continuité transparente entre appareils distants - Transition sans coupure entre les titres + Nouveau explorateur de fichiers pour choisir les dossiers sources + Nouvelles fonctionnalités de connectivité et de casting + Continuité fluide entre appareils distants + Transitions sans coupure entre les morceaux Contrôle du fondu enchaîné - Nouvelle fonctionnalité de transitions personnalisées (uniquement pour les playlists) - Continuer la lecture après avoir fermé l\'application - Optimisations de l\'interface - Fonctionnalité de statistiques améliorée - Contrôle de la file d\'attente repensé avec plus de fonctionnalités - Support amélioré de différents types de fichiers pour la lecture et l\'édition de métadonnées - Contrôleur d\'autorisations amélioré - Corrections de bugs mineurs - + Nouvelle fonctionnalité de transitions personnalisées (playlists uniquement) + Lecture continue après fermeture de l’application + Optimisations de l’interface + Amélioration des statistiques + Contrôle de file d’attente repensé avec plus de fonctionnalités + Meilleur support des types de fichiers et métadonnées + Amélioration du gestionnaire de permissions + Corrections mineures de bugs + + - Mise à jour de l\'interface Material 3 Expressive - Égaliseur 10 bandes & Effets - Nouveau flux de synchronisation de bibliothèque - Intégration IA (Modèles Gemini) - Import/Export playlist M3U - Intégration des pochettes d\'artistes Deezer - Pochettes de playlist personnalisées - Refonte de l\'architecture des paramètres - Animations file d\'attente & lecteur - Profils de référence & performances - Système de paroles amélioré avec décalage de synchronisation - + Mise à jour de l’interface Material 3 Expressive + Égaliseur 10 bandes & effets + Nouveau flux de synchronisation de la bibliothèque + Intégration IA (modèles Gemini) + Import/export de playlists M3U + Intégration des images d’artistes Deezer + Pochettes de playlists personnalisées + Refonte de l’architecture des paramètres + Animations du lecteur et de la file d’attente + Optimisation des performances (Baseline Profiles) + Meilleur système de paroles avec décalage synchronisé + + - Améliorations de la stabilité de la diffusion - Stabilité du panneau lecteur - Corrections de bugs générales & nettoyage + Amélioration de la stabilité du casting + Stabilité du lecteur améliorée + Corrections générales de bugs + - Le support Android Auto est maintenant disponible pour la lecture en voiture. - Le support Wear OS est actif, avec de meilleurs contrôles de lecture montre-téléphone. - Les intégrations cloud ont été élargies avec Telegram, NetEase, QQ Music et des améliorations Google Drive. - Écoutés récemment et restauration persistante de la file d\'attente gardent votre session d\'écoute prête. - Sauvegarde & Restauration v3 et outils de gestion de compte sont maintenant inclus. - Les paroles sont devenues plus intelligentes avec la recherche manuelle de secours et des améliorations de stockage. - + Support Android Auto pour la lecture en voiture. + Support Wear OS avec meilleurs contrôles lecture montre-téléphone. + Intégrations cloud étendues (Telegram, NetEase, QQ Music, Google Drive). + Historique récent et restauration de file d’attente persistante. + Outils de sauvegarde & restauration v3 et gestion de compte. + Recherche de paroles améliorée avec fallback manuel. + + - Grande passe de performance sur le démarrage, la bibliothèque, la file d\'attente et les interactions avec le lecteur. - Les interfaces Lecteur, Diffusion, Paroles, Artiste et Genre ont été repensées pour une utilisation plus fluide. - Les flux de navigation et de recherche sont plus fiables, avec une gestion des routes plus sûre. - Compatibilité de lecture audio améliorée pour plus d\'appareils et de formats. - Les flux de sélection multiple ont été élargis aux titres, albums et playlists. + Amélioration majeure des performances globales. + Refonte des écrans lecteur, casting, paroles, artiste et genres. + Navigation et recherche plus fiables. + Compatibilité audio améliorée pour plus d’appareils. + Meilleure prise en charge de la sélection multiple. + - Le comportement de la file d\'attente et de l\'aléatoire est maintenant plus stable et prévisible. - Plusieurs cas limites de lecture en arrière-plan et de diffusion ont été corrigés. - Minuteur de sommeil, navigation de l\'onglet Fichiers et problèmes de crash artiste d\'album corrigés. - Le chargement du widget et la stabilité du service ont été améliorés pour réduire les problèmes de surchauffe/mémoire. - Corrections de bugs générales et peaufinage de l\'interface dans toute l\'application. + Stabilité améliorée de la file d’attente et du mode aléatoire. + Correction des problèmes de lecture en arrière-plan et casting. + Correction du minuteur de sommeil et des crashs fichiers/artistes. + Amélioration des widgets et stabilité des services. + Corrections générales et polish UI. + - Wear OS : Transfert de musique, lecture locale, synchronisation de la file d\'attente et contrôle à distance depuis la montre. - IA : Intégration de Groq AI et OpenRouter (expérimental) avec optimisation des jetons. - Cloud : Ajout du support de Jellyfin. - Paroles : Traduction synchronisée avec interrupteur dédié, support du format Kugou LRC, personnalisation de l\'alignement du texte et amélioration du chargement à distance. - UI/UX : Mode barre de navigation compacte, thèmes dynamiques depuis la palette des pochettes d\'album, défilement pour les titres longs et nouvelles options de tri. - Telegram : Support natif des sujets et modes d\'affichage améliorés. - + Wear OS : transfert de musique, lecture locale, synchronisation de file et contrôle à distance. + IA : intégration Groq AI et OpenRouter (expérimental). + Cloud : ajout du support Jellyfin. + Paroles : traduction synchronisée, support Kugou LRC, alignement et chargement amélioré. + UI/UX : barre compacte, thèmes dynamiques, titre défilant, tri amélioré. + Telegram : support des topics natifs. + + - Moteur audio : Refonte complète avec support de plus de formats (MIDI, ALAC, M4A) et optimisation des décodeurs. - Efficacité : Réduction drastique de la consommation d\'énergie, corrections de la surchauffe et optimisation des tâches en arrière-plan (SyncWorker). - Base de données : Optimisations massives des requêtes et refonte du cache des pochettes pour éviter la perte de données. - Démarrage : Temps de chargement amélioré grâce à l\'optimisation des profils de référence (Baseline Profile). + Moteur audio entièrement refait avec meilleurs formats (MIDI, ALAC, M4A). + Réduction drastique de la consommation énergétique. + Optimisation des bases de données et du cache. + Amélioration du temps de démarrage via Baseline Profiles. + - Lecture : Correction des saccades en Opus/MP3, erreurs de ReplayGain pendant les fondus enchaînés et problèmes de démarrage sur les décodeurs Samsung. - Stabilité : Élimination des plantages au démarrage, de la navigation par artiste et sur les appareils Android 12+. - Interface : Correction du clignotement des pochettes, du dépassement de texte pour les scripts non latins et du comportement de la barre de navigation/du mini-lecteur. - Sécurité : Sécurisation renforcée de la gestion des identifiants, des autorisations de stockage et de la communication avec le serveur multimédia. + Correction des problèmes de lecture et de stuttering. + Correction des crashs sur Android et Samsung decoders. + Correction des problèmes UI (cover art, texte, navigation). + Sécurisation du stockage et des communications médias. + - Traduction : Espagnol, Français, Russe, Chinois simplifié, Indonésien, Italien - - \ No newline at end of file + Localisation : espagnol, français, russe, chinois simplifié, indonésien, italien. + + + + Intégration Google Drive avec gestion de lecture. + Édition en lot des métadonnées. + Traduction IA des paroles avec options Wear OS. + Outil de diagnostic de latence et sélection multiple dans la recherche. + Support arabe et turc avec options réseau local HTTP. + + + + Économie de batterie drastique. + Optimisation de la gestion de file d’attente. + Animations Material 3 Expressive améliorées. + Synchronisation bibliothèque optimisée. + + + + Correction des problèmes de lecture et buffering. + Correction de la suppression de chansons externes. + Correction des crashs et problèmes mémoire Wear OS et mobile. + + + From 5196043fa8679124cc66ccf47f746e5f9500fa1a Mon Sep 17 00:00:00 2001 From: Rebornloki Date: Sat, 13 Jun 2026 09:40:31 +0100 Subject: [PATCH 02/72] Revise Italian changelog strings for clarity Updated Italian strings in changelogs for improved clarity and consistency. --- .../main/res/values-it/strings_changelogs.xml | 234 ++++++++++-------- 1 file changed, 136 insertions(+), 98 deletions(-) diff --git a/app/src/main/res/values-it/strings_changelogs.xml b/app/src/main/res/values-it/strings_changelogs.xml index bc4f2f71b..d7ce29f70 100644 --- a/app/src/main/res/values-it/strings_changelogs.xml +++ b/app/src/main/res/values-it/strings_changelogs.xml @@ -1,132 +1,170 @@ - Changelog - Visualizza su GitHub + Registro modifiche + Vedi su GitHub Miglioramenti Correzioni Novità - Aggiunto + Aggiunti + - Supporto Chromecast per trasmettere l\'audio dal tuo dispositivo. - Changelog in-app per tenerti aggiornato sulle ultime funzioni. + Supporto Chromecast per trasmettere audio dal dispositivo. + Changelog integrato per tenerti aggiornato sulle ultime funzionalità. Supporto per file .LRC, sia incorporati che esterni. - Supporto per i testi offline. - Testi sincronizzati (sincronizzati con il brano). + Supporto per testi offline. + Testi sincronizzati con la musica. Nuova schermata per visualizzare la coda completa. - Riordina e rimuovi brani dalla coda. - Gesti del mini-riproduttore (scorrimento verso il basso per chiudere). - Aggiunte altre animazioni Material. - Nuove impostazioni per personalizzare l\'aspetto e lo stile. - Nuove impostazioni per cancellare la cache. + Riordino e rimozione dei brani dalla coda. + Gesti del mini-player (scorri verso il basso per chiudere). + Aggiunte nuove animazioni Material. + Nuove impostazioni per personalizzare l’aspetto. + Nuova impostazione per svuotare la cache. + - Riprogettazione completa dell\'interfaccia utente. - Riprogettazione completa del riproduttore. - Miglioramenti delle prestazioni nella libreria. - Velocità di avvio dell\'applicazione migliorata. - L\'IA ora fornisce risultati migliori. + Restyling completo dell’interfaccia utente. + Restyling completo del player. + Miglioramento delle prestazioni della libreria. + Miglioramento della velocità di avvio dell’app. + L’IA ora fornisce risultati migliori. + - Corretti vari bug nell\'editor dei tag. - Corretto un bug per cui la notifica di riproduzione non si cancellava. - Corretti diversi bug che causavano il crash dell\'app. + Corretti vari bug nell’editor dei tag. + Corretto un bug per cui la notifica di riproduzione non veniva rimossa. + Corretti diversi bug che causavano crash dell’app. + - Introdotto un hub di statistiche di ascolto più ricco con approfondimenti dettagliati sulle tue sessioni. - Lanciato un lettore rapido fluttuante per aprire e visualizzare in anteprima istantaneamente i file locali. - Aggiunta una scheda cartelle con navigazione ad albero e vista predisposta per le playlist. + Introdotto un hub statistiche di ascolto più ricco con analisi approfondite. + Rilasciato un mini-player flottante per aprire e visualizzare file locali. + Aggiunta scheda cartelle con navigazione ad albero e vista playlist. + - Raffinata l\'interfaccia utente generale Material 3 per un\'esperienza più pulita e coesa. - La modifica dei metadati ora supporta il cambio della copertina. - Animazioni e transizioni più fluide in tutta l\'app per una navigazione più fluida. - Migliorato il layout della schermata dell\'artista con dettagli più ricchi e rifiniture. - Aggiornata la generazione di DailyMix e YourMix con selezioni più intelligenti e varie. - Potenziata la generazione di playlist tramite IA. - Migliorata la rilevanza e la presentazione dei risultati di ricerca per una scoperta più rapida. - Esteso il supporto a una gamma più ampia di formati di file audio. - + Interfaccia Material 3 migliorata per un’esperienza più coerente. + Modifica metadati ora supporta la copertina album. + Animazioni e transizioni più fluide in tutta l’app. + Migliorata la schermata artista con più dettagli. + Migliorati DailyMix e YourMix con selezioni più varie. + Potenziata la generazione playlist IA. + Ricerca migliorata per risultati più rapidi. + Supporto esteso a più formati audio. + + - Risolti i problemi con i metadati in modo che i dettagli dei brani rimangano accurati ovunque. - Ripristinate le scorciatoie delle notifiche in modo che ritornino in modo affidabile alla riproduzione. + Risolti problemi di incoerenza nei metadati. + Ripristinati i collegamenti rapidi alle notifiche. + - Importante riprogettazione della navigazione - Nuovo esploratore di file per scegliere le directory sorgente - Nuove funzionalità di connettività e trasmissione (casting) - Continuità perfetta tra dispositivi remoti - Transizione senza interruzioni (gapless) tra i brani - Controllo della dissolvenza incrociata (crossfade) - Nuova funzione di transizioni personalizzate (solo per playlist) - Continua la riproduzione anche dopo la chiusura dell\'app - Ottimizzazioni dell\'interfaccia utente - Funzionalità di statistica migliorata - Controllo della coda riprogettato con più funzioni - Supporto migliorato per diversi tipi di file per la riproduzione e la modifica dei metadati - Gestione dei permessi migliorata - Correzioni di bug minori - + Grande rinnovamento della navigazione + Nuovo file explorer per scegliere le cartelle sorgente + Nuove funzionalità di connettività e casting + Continuità fluida tra dispositivi remoti + Transizioni senza interruzioni tra i brani + Controllo crossfade + Nuove transizioni personalizzate (solo playlist) + Riproduzione continua dopo la chiusura dell’app + Ottimizzazioni UI + Miglioramento statistiche + Controllo coda riprogettato con più funzioni + Migliorato supporto file e metadati + Migliorato sistema permessi + Correzioni minori di bug + + - Aggiornamento dell\'interfaccia utente Material 3 Expressive - Equalizzatore a 10 bande ed effetti - Nuovo flusso di sincronizzazione della libreria + Aggiornamento Material 3 Expressive UI + Equalizzatore a 10 bande & effetti + Nuovo flusso di sincronizzazione libreria Integrazione IA (modelli Gemini) - Importazione/esportazione di playlist M3U - Integrazione delle immagini degli artisti da Deezer - Copertine personalizzate per le playlist - Rifattorizzazione dell\'architettura delle impostazioni - Animazioni per la coda e il riproduttore - Profili Baseline e prestazioni - Sistema di testi migliorato con regolazione del ritardo di sincronizzazione - + Import/Export playlist M3U + Integrazione artwork artisti Deezer + Copertine playlist personalizzate + Refactor architettura impostazioni + Animazioni player e coda + Ottimizzazione prestazioni (Baseline Profiles) + Sistema testi migliorato con offset sincronizzato + + - Miglioramenti della stabilità della trasmissione (casting) - Stabilità del pannello del riproduttore - Correzioni di bug generali e pulizia + Migliorata stabilità del casting + Migliorata stabilità del player + Correzioni generali di bug + - Il supporto Android Auto è ora disponibile per la riproduzione in auto. - Il supporto Wear OS è attivo, inclusi migliori controlli di riproduzione tra smartwatch e telefono. - Integrazioni cloud ampliate con miglioramenti per Telegram, NetEase, QQ Music e Google Drive. - La sezione Ascoltati di recente e il ripristino persistente della coda mantengono pronta la sessione di ascolto. - Ora sono inclusi il Backup e Ripristino v3 e gli strumenti di gestione dell\'account. - I testi sono diventati più intelligenti con ricerca manuale alternativa e miglioramenti di archiviazione. - + Supporto Android Auto per la riproduzione in auto. + Supporto Wear OS con controlli migliorati tra orologio e telefono. + Integrazioni cloud estese (Telegram, NetEase, QQ Music, Google Drive). + Ripristino cronologia recente e coda persistente. + Backup & Restore v3 e gestione account. + Ricerca testi migliorata con fallback manuale. + + - Grande ottimizzazione delle prestazioni per avvio, libreria, coda e interazioni con il riproduttore. - Le schermate di Riproduttore, Cast, Testi, Artista e Genere sono state riprogettate per un uso più fluido. - I flussi di navigazione e ricerca sono più affidabili, con una gestione dei percorsi più sicura. - Compatibilità della riproduzione audio migliorata per più dispositivi e formati. - I flussi di selezione multipla sono stati estesi a brani, album e playlist. + Grande miglioramento delle prestazioni globali. + Restyling di player, casting, testi, artista e generi. + Navigazione e ricerca più affidabili. + Compatibilità audio migliorata. + Migliorato supporto selezione multipla. + - Il comportamento della coda e della riproduzione casuale è ora più stabile e prevedibile. - Risolti diversi casi limite per la trasmissione e la riproduzione in background. - Risolti i problemi relativi a timer di spegnimento, navigazione della scheda File e crash dell\'artista dell\'album. - Migliorata la stabilità del servizio e il caricamento dei widget per ridurre i problemi di surriscaldamento e memoria. - Correzioni di bug generali e rifiniture dell\'interfaccia utente in tutta l\'app. + Migliorata stabilità coda e shuffle. + Corretti problemi di riproduzione in background e casting. + Corretti crash su sleep timer e file/artisti. + Migliorati widget e stabilità servizi. + Correzioni generali UI. + - Wear OS: Trasferimento musica, riproduzione locale, sincronizzazione della coda e controllo remoto da smartwatch. - IA: Integrazione di Groq AI e OpenRouter (sperimentale) con ottimizzazione dei token. - Cloud: Aggiunto il supporto a Jellyfin. - Testi: Traduzione sincronizzata con interruttore dedicato, supporto per il formato Kugou LRC, personalizzazione dell\'allineamento del testo e caricamento remoto migliorato. - UI/UX: Modalità barra di navigazione compatta, temi dinamici dalla tavolozza dei colori dell\'album, testo scorrevole per titoli lunghi e nuove opzioni di ordinamento. - Telegram: Supporto nativo per gli argomenti (topics) e modalità di visualizzazione migliorate. - + Wear OS: trasferimento musica, riproduzione locale, sync coda e controllo remoto. + IA: integrazione Groq AI e OpenRouter (sperimentale). + Cloud: aggiunto supporto Jellyfin. + Testi: traduzione sincronizzata, supporto Kugou LRC, allineamento migliorato. + UI/UX: barra compatta, temi dinamici, marquee, nuovi ordinamenti. + Telegram: supporto topic nativi. + + - Motore audio: Revisione completa con supporto per più formati (MIDI, ALAC, M4A) e ottimizzazione dei decodificatori. - Efficienza: Riduzione drastica del consumo energetico, risoluzione del surriscaldamento e ottimizzazione delle attività in background (SyncWorker). - Database: Ottimizzazioni massicce delle query e cache delle copertine riprogettata per prevenire la perdita di dati. - Avvio: Tempo di caricamento migliorato tramite l\'ottimizzazione di Baseline Profile. + Motore audio completamente rifatto con supporto ampliato (MIDI, ALAC, M4A). + Ridotto drasticamente consumo energetico. + Ottimizzate query database e cache artwork. + Migliorato tempo di avvio tramite Baseline Profiles. + - Riproduzione: Risolti i micro-scatti con Opus/MP3, gli errori ReplayGain durante le dissolvenze incrociate e i problemi di avvio sui decodificatori Samsung. - Stabilità: Eliminati i crash all\'avvio, nella navigazione dell\'artista e sui dispositivi con Android 12+. - Interfaccia utente: Corretto lo sfarfallio delle copertine, il superamento dei limiti di testo per le scritture non latine e il comportamento della barra di navigazione/mini-riproduttore. - Sicurezza: Maggiore protezione nella gestione delle credenziali, dei permessi di archiviazione e della comunicazione con il server multimediale. + Corretti problemi di stuttering e riproduzione. + Corretti crash su Android e decoder Samsung. + Corretti problemi UI (copertine, testo, navigazione). + Migliorata sicurezza storage e comunicazione media. + - Localizzazione: Spagnolo, Francese, Russo, Cinese semplificato, Indonesiano, Italiano - - \ No newline at end of file + Localizzazione: spagnolo, francese, russo, cinese semplificato, indonesiano, italiano. + + + + Integrazione Google Drive con gestione riproduzione. + Modifica batch dei metadati. + Traduzione testi IA con opzioni Wear OS. + Strumento diagnostica lag e selezione multipla ricerca. + Supporto arabo e turco con opzioni rete locale HTTP. + + + + Risparmio batteria drastico. + Ottimizzazione gestione coda. + Animazioni Material 3 Expressive migliorate. + Sync libreria ottimizzato. + + + + Corretti problemi di buffering e riproduzione. + Corretto sync eliminazione brani esterni. + Corretti crash e problemi memoria su Wear OS e mobile. + + + From 31c9678123777faef579e74dc99b913d8fab1b62 Mon Sep 17 00:00:00 2001 From: Rebornloki Date: Sat, 13 Jun 2026 11:42:02 +0100 Subject: [PATCH 03/72] Update beta version to 0.7.5 in strings resource --- app/src/main/res/values/strings_home_screen.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/main/res/values/strings_home_screen.xml b/app/src/main/res/values/strings_home_screen.xml index b60cde200..2a7e3ad80 100644 --- a/app/src/main/res/values/strings_home_screen.xml +++ b/app/src/main/res/values/strings_home_screen.xml @@ -9,9 +9,9 @@ Stream music from your cloud accounts - Beta 0.7.0 + Beta 0.7.5 β - Welcome to PixelPlayer 0.7.0-beta + 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. @@ -274,4 +274,4 @@ %1$d Song %1$d Songs Week %1$d - \ No newline at end of file + From 70c42fc3c8548722b76d1a06585df02605a23f46 Mon Sep 17 00:00:00 2001 From: Rebornloki Date: Sat, 13 Jun 2026 11:42:45 +0100 Subject: [PATCH 04/72] Update beta version to 0.7.5 in Italian strings --- app/src/main/res/values-it/strings_home_screen.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/main/res/values-it/strings_home_screen.xml b/app/src/main/res/values-it/strings_home_screen.xml index 51cd56f73..c53d26ccf 100644 --- a/app/src/main/res/values-it/strings_home_screen.xml +++ b/app/src/main/res/values-it/strings_home_screen.xml @@ -9,9 +9,9 @@ Riproduci in streaming la musica dai tuoi account cloud - Beta 0.7.0 + Beta 0.7.5 β - Benvenuto in PixelPlayer 0.7.0-beta + Benvenuto in PixelPlayer 0.7.5-beta Stai utilizzando una build beta che potrebbe contenere bug, crash o funzionalità sperimentali. Aiutaci a migliorare segnalando i problemi. Cosa aspettarsi Bug, crash o funzionalità incomplete possono verificarsi inaspettatamente. @@ -274,4 +274,4 @@ %1$d brano %1$d brani Settimana %1$d - \ No newline at end of file + From ddade1d053c754b550dd5453165174d365c38200 Mon Sep 17 00:00:00 2001 From: Rebornloki Date: Sat, 13 Jun 2026 11:44:59 +0100 Subject: [PATCH 05/72] Update strings_home_screen.xml [ci skip] --- app/src/main/res/values-fr/strings_home_screen.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/main/res/values-fr/strings_home_screen.xml b/app/src/main/res/values-fr/strings_home_screen.xml index 4b9fd3019..ceb04518a 100644 --- a/app/src/main/res/values-fr/strings_home_screen.xml +++ b/app/src/main/res/values-fr/strings_home_screen.xml @@ -9,9 +9,9 @@ Diffusez de la musique depuis vos comptes cloud - Bêta 0.7.0 + Bêta 0.7.5 β - Bienvenue dans PixelPlayer 0.7.0-bêta + Bienvenue dans PixelPlayer 0.7.5-bêta Vous utilisez une version bêta qui peut contenir des bugs, des plantages ou des fonctionnalités expérimentales. Aidez-nous à nous améliorer en signalant les problèmes. À quoi s\'attendre Des bugs, des plantages ou des fonctionnalités incomplètes peuvent survenir de manière inattendue. @@ -274,4 +274,4 @@ %1$d chanson %1$d chansons Semaine %1$d - \ No newline at end of file + From 698ae1039184ab81dc6a69b5bdebd1a63908ff68 Mon Sep 17 00:00:00 2001 From: Rebornloki Date: Sat, 13 Jun 2026 11:46:34 +0100 Subject: [PATCH 06/72] Update beta version strings to 0.7.5 --- app/src/main/res/values-ko/strings_home_screen.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/main/res/values-ko/strings_home_screen.xml b/app/src/main/res/values-ko/strings_home_screen.xml index ece00d63f..f831b8995 100644 --- a/app/src/main/res/values-ko/strings_home_screen.xml +++ b/app/src/main/res/values-ko/strings_home_screen.xml @@ -9,9 +9,9 @@ 클라우드 계정에서 음악 스트리밍 - Beta 0.7.0 + Beta 0.7.5 β - PixelPlayer 0.7.0-beta에 오신 것을 환영합니다 + PixelPlayer 0.7.5-beta에 오신 것을 환영합니다 버그, 충돌 또는 실험적 기능이 포함되어 있을 수 있는 베타 빌드를 사용 중입니다. 문제를 보고하여 앱을 개선할 수 있도록 도와주세요. 주요 변경 사항 버그, 충돌 또는 불완전한 기능이 예기치 않게 발생할 수 있습니다. @@ -273,4 +273,4 @@ %1$d곡 %1$d곡 %1$d주차 - \ No newline at end of file + From 7e346a3039b8d32fd252f01a062487221be23b2e Mon Sep 17 00:00:00 2001 From: Rebornloki Date: Sat, 13 Jun 2026 11:47:31 +0100 Subject: [PATCH 07/72] Update beta version strings to 0.7.5 --- app/src/main/res/values-in/strings_home_screen.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/main/res/values-in/strings_home_screen.xml b/app/src/main/res/values-in/strings_home_screen.xml index 0af328bde..7a5e0de11 100644 --- a/app/src/main/res/values-in/strings_home_screen.xml +++ b/app/src/main/res/values-in/strings_home_screen.xml @@ -9,9 +9,9 @@ Alirkan musik dari akun cloud Anda - Beta 0.7.0 + Beta 0.7.5 β - Selamat datang di PixelPlayer 0.7.0-beta + Selamat datang di PixelPlayer 0.7.5-beta Anda menggunakan versi beta yang mungkin berisi bug, crash, atau fitur eksperimental. Bantu kami meningkatkannya dengan melaporkan masalah. Yang perlu diharapkan Bug, crash, atau fitur yang belum selesai dapat terjadi sewaktu-waktu. @@ -274,4 +274,4 @@ %1$d Lagu %1$d Lagu Minggu %1$d - \ No newline at end of file + From 2c376a85f4fa3e4dffdaac7f84a6ba1500c84e97 Mon Sep 17 00:00:00 2001 From: Rebornloki Date: Sat, 13 Jun 2026 11:48:27 +0100 Subject: [PATCH 08/72] Update beta version strings to 0.7.5 --- app/src/main/res/values-nb/strings_home_screen.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/main/res/values-nb/strings_home_screen.xml b/app/src/main/res/values-nb/strings_home_screen.xml index e1f64c77a..ee172a5bc 100644 --- a/app/src/main/res/values-nb/strings_home_screen.xml +++ b/app/src/main/res/values-nb/strings_home_screen.xml @@ -9,9 +9,9 @@ Strøm musikk fra dine skykontoer - Beta 0.7.0 + Beta 0.7.5 β - Velkommen til PixelPlayer 0.7.0-beta + Velkommen til PixelPlayer 0.7.5-beta Du bruker en betaversjon som kan inneholde feil, krasj eller eksperimentelle funksjoner. Hjelp oss å forbedre ved å rapportere problemer. Hva du kan forvente Feil, krasj eller uferdige funksjoner kan oppstå uventet. @@ -274,4 +274,4 @@ %1$d sang %1$d sanger Uke %1$d - \ No newline at end of file + From df83b66d2ca2885207c962c0baa31d879a10aa77 Mon Sep 17 00:00:00 2001 From: Rebornloki Date: Sat, 13 Jun 2026 11:49:30 +0100 Subject: [PATCH 09/72] Update beta version strings to 0.7.5 in russian strings --- app/src/main/res/values-ru/strings_home_screen.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/main/res/values-ru/strings_home_screen.xml b/app/src/main/res/values-ru/strings_home_screen.xml index b14b73cd7..8e0229903 100644 --- a/app/src/main/res/values-ru/strings_home_screen.xml +++ b/app/src/main/res/values-ru/strings_home_screen.xml @@ -9,9 +9,9 @@ Слушайте музыку из своих облачных аккаунтов - Бета 0.7.0 + Бета 0.7.5 β - Добро пожаловать в PixelPlayer 0.7.0-beta + Добро пожаловать в PixelPlayer 0.7.5-beta Вы используете бета-версию, которая может содержать ошибки, сбои или экспериментальные функции. Помогите нам улучшить приложение, сообщая о проблемах. Чего ожидать Ошибки, сбои или незавершённые функции могут возникать неожиданно. @@ -276,4 +276,4 @@ %1$d песня %1$d песен Неделя %1$d - \ No newline at end of file + From f99cb205a310a6d7d092c2a6e262aeb5a5e04d8c Mon Sep 17 00:00:00 2001 From: Rebornloki Date: Sat, 13 Jun 2026 12:05:57 +0100 Subject: [PATCH 10/72] Update beta version strings to 0.7.5 --- app/src/main/res/values-tr/strings_home_screen.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/main/res/values-tr/strings_home_screen.xml b/app/src/main/res/values-tr/strings_home_screen.xml index 49de86173..2bf26ab40 100644 --- a/app/src/main/res/values-tr/strings_home_screen.xml +++ b/app/src/main/res/values-tr/strings_home_screen.xml @@ -9,9 +9,9 @@ Bulut hesaplarınızdan müzik akışı yapın - Beta 0.7.0 + Beta 0.7.5 β - PixelPlayer 0.7.0-beta\'ya Hoş Geldiniz + PixelPlayer 0.7.5-beta\'ya Hoş Geldiniz Hata, çökme veya deneysel özellikler içerebilecek bir beta yapısı kullanıyorsunuz. Sorunları bildirerek geliştirmemize yardımcı olun. Ne beklemeli Beklenmedik hatalar, çökmeler veya tamamlanmamış özellikler olabilir. @@ -274,4 +274,4 @@ %1$d Şarkı %1$d Şarkı %1$d. Hafta - \ No newline at end of file + From b44ba5d49294c9170ddfe10ccde6b30d8bef0423 Mon Sep 17 00:00:00 2001 From: Rebornloki Date: Sat, 13 Jun 2026 12:07:55 +0100 Subject: [PATCH 11/72] Update beta version strings to 0.7.5 --- app/src/main/res/values-zh-rCN/strings_home_screen.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/main/res/values-zh-rCN/strings_home_screen.xml b/app/src/main/res/values-zh-rCN/strings_home_screen.xml index 78a2f155d..46a70f806 100644 --- a/app/src/main/res/values-zh-rCN/strings_home_screen.xml +++ b/app/src/main/res/values-zh-rCN/strings_home_screen.xml @@ -9,9 +9,9 @@ 从您的云端账户串流音乐 - Beta 0.7.0 + Beta 0.7.5 β - 欢迎使用 PixelPlayer 0.7.0-beta + 欢迎使用 PixelPlayer 0.7.5-beta 您正在使用的是可能包含错误、崩溃或实验性功能的测试版本。请报告问题以帮助我们改进。 预期情况 可能会意外出现错误、崩溃或未完成的功能。 @@ -274,4 +274,4 @@ %1$d 首歌曲 %1$d 首歌曲 第 %1$d 周 - \ No newline at end of file + From 3cc47dbf6972bd15eb3ba3f7bfb7dfd65381820f Mon Sep 17 00:00:00 2001 From: Rebornloki Date: Sat, 13 Jun 2026 12:09:09 +0100 Subject: [PATCH 12/72] Update beta version strings to 0.7.5 --- app/src/main/res/values-es/strings_home_screen.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/main/res/values-es/strings_home_screen.xml b/app/src/main/res/values-es/strings_home_screen.xml index f48c8c03e..0ce25db99 100644 --- a/app/src/main/res/values-es/strings_home_screen.xml +++ b/app/src/main/res/values-es/strings_home_screen.xml @@ -9,9 +9,9 @@ Transmite música desde tus cuentas en la nube - Beta 0.7.0 + Beta 0.7.5 β - Bienvenido a PixelPlayer 0.7.0-beta + Bienvenido a PixelPlayer 0.7.5-beta Estás usando una versión beta que puede contener errores, fallos o funciones experimentales. Ayúdanos a mejorar informando de los problemas. Qué esperar Pueden ocurrir errores, fallos o funciones incompletas de forma inesperada. @@ -274,4 +274,4 @@ %1$d canción %1$d canciones Semana %1$d - \ No newline at end of file + From 5325ea269067772e2511264bf45dab2e5bfbc235 Mon Sep 17 00:00:00 2001 From: Rebornloki Date: Sat, 13 Jun 2026 12:10:16 +0100 Subject: [PATCH 13/72] Update strings_home_screen.xml --- app/src/main/res/values-de/strings_home_screen.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/main/res/values-de/strings_home_screen.xml b/app/src/main/res/values-de/strings_home_screen.xml index 5d87a6d3c..61bc62114 100644 --- a/app/src/main/res/values-de/strings_home_screen.xml +++ b/app/src/main/res/values-de/strings_home_screen.xml @@ -9,9 +9,9 @@ Musik aus deinen Cloud-Accounts streamen - Beta 0.7.0 + Beta 0.7.5 β - Willkommen bei PixelPlayer 0.7.0-beta + Willkommen bei PixelPlayer 0.7.5-beta Du verwendest eine Beta-Version, die Fehler, Abstürze oder experimentelle Funktionen enthalten kann. Hilf uns bei der Verbesserung, indem du Probleme meldest. Was dich erwartet Fehler, Abstürze oder unvollständige Funktionen können unerwartet auftreten. @@ -274,4 +274,4 @@ %1$d Song %1$d Songs Woche %1$d - \ No newline at end of file + From 432796a5ae0f751ae6d7d4e39034b8006e968826 Mon Sep 17 00:00:00 2001 From: Rebornloki Date: Sat, 13 Jun 2026 19:10:06 +0100 Subject: [PATCH 14/72] Update French lyrics mode strings for consistency --- app/src/main/res/values-fr/strings_player.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/res/values-fr/strings_player.xml b/app/src/main/res/values-fr/strings_player.xml index 674747df4..da510f96e 100644 --- a/app/src/main/res/values-fr/strings_player.xml +++ b/app/src/main/res/values-fr/strings_player.xml @@ -129,8 +129,8 @@ Paroles Chargement des paroles… - Synchronisées - Statiques + Synchronisé + Statique Options des paroles −.5 −.1 From 402e45770039555e1f6898311c5832225f68c852 Mon Sep 17 00:00:00 2001 From: VoidX3D Date: Mon, 15 Jun 2026 18:59:43 +0545 Subject: [PATCH 15/72] refactor(ai): rename AiOrchestrator to AiHandler Renamed the central AI orchestration class from AiOrchestrator to AiHandler and updated all references across the codebase. --- .../pixelplay/data/ai/{AiOrchestrator.kt => AiHandler.kt} | 8 ++++---- .../theveloper/pixelplay/data/ai/AiMetadataGenerator.kt | 4 ++-- .../theveloper/pixelplay/data/ai/AiPlaylistGenerator.kt | 4 ++-- .../theveloper/pixelplay/data/ai/GeminiModelService.kt | 4 ++-- .../pixelplay/data/preferences/AiPreferencesRepository.kt | 2 +- .../java/com/theveloper/pixelplay/data/worker/AiWorker.kt | 6 +++--- .../pixelplay/presentation/viewmodel/AiStateHolder.kt | 4 ++-- 7 files changed, 16 insertions(+), 16 deletions(-) rename app/src/main/java/com/theveloper/pixelplay/data/ai/{AiOrchestrator.kt => AiHandler.kt} (97%) diff --git a/app/src/main/java/com/theveloper/pixelplay/data/ai/AiOrchestrator.kt b/app/src/main/java/com/theveloper/pixelplay/data/ai/AiHandler.kt similarity index 97% rename from app/src/main/java/com/theveloper/pixelplay/data/ai/AiOrchestrator.kt rename to app/src/main/java/com/theveloper/pixelplay/data/ai/AiHandler.kt index b3109247b..46cddb604 100644 --- a/app/src/main/java/com/theveloper/pixelplay/data/ai/AiOrchestrator.kt +++ b/app/src/main/java/com/theveloper/pixelplay/data/ai/AiHandler.kt @@ -19,7 +19,7 @@ import javax.inject.Inject import javax.inject.Singleton @Singleton -class AiOrchestrator @Inject constructor( +class AiHandler @Inject constructor( private val preferencesRepo: AiPreferencesRepository, private val clientFactory: AiClientFactory, private val cacheDao: AiCacheDao, @@ -235,7 +235,7 @@ class AiOrchestrator @Inject constructor( ) ) }.onFailure { error -> - Timber.tag("AiOrchestrator").e(error, "Failed to persist AI usage") + Timber.tag("AiHandler").e(error, "Failed to persist AI usage") } } @@ -244,7 +244,7 @@ class AiOrchestrator @Inject constructor( } 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) - Timber.tag("AiOrchestrator").w(e, "Provider ${provider.name} failed: ${failure.message}") + 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. if (failure.shouldCooldown()) { @@ -268,7 +268,7 @@ class AiOrchestrator @Inject constructor( "AI generation failed after trying ${failedProviders.size} providers:\n${failedProviders.joinToString("\n• ", prefix = "• ")}" } - Timber.tag("AiOrchestrator").e("All providers failed. Details: %s", failedProviders.joinToString(" | ")) + Timber.tag("AiHandler").e("All providers failed. Details: %s", failedProviders.joinToString(" | ")) throw Exception(errorMessage) } } diff --git a/app/src/main/java/com/theveloper/pixelplay/data/ai/AiMetadataGenerator.kt b/app/src/main/java/com/theveloper/pixelplay/data/ai/AiMetadataGenerator.kt index f67ccb324..eaa67258c 100644 --- a/app/src/main/java/com/theveloper/pixelplay/data/ai/AiMetadataGenerator.kt +++ b/app/src/main/java/com/theveloper/pixelplay/data/ai/AiMetadataGenerator.kt @@ -17,7 +17,7 @@ data class SongMetadata( ) class AiMetadataGenerator @Inject constructor( - private val aiOrchestrator: AiOrchestrator, + private val aiHandler: AiHandler, private val json: Json ) { private fun cleanJson(jsonString: String): String { @@ -45,7 +45,7 @@ class AiMetadataGenerator @Inject constructor( """.trimIndent() - val responseText = aiOrchestrator.generateContent(fullPrompt, AiSystemPromptType.METADATA) + val responseText = aiHandler.generateContent(fullPrompt, AiSystemPromptType.METADATA) if (responseText.isBlank()) { Timber.e("AI returned an empty or null response.") return Result.failure(Exception("AI returned an empty response.")) diff --git a/app/src/main/java/com/theveloper/pixelplay/data/ai/AiPlaylistGenerator.kt b/app/src/main/java/com/theveloper/pixelplay/data/ai/AiPlaylistGenerator.kt index 7017ec6a6..3e2e5a524 100644 --- a/app/src/main/java/com/theveloper/pixelplay/data/ai/AiPlaylistGenerator.kt +++ b/app/src/main/java/com/theveloper/pixelplay/data/ai/AiPlaylistGenerator.kt @@ -11,7 +11,7 @@ import kotlin.math.max class AiPlaylistGenerator @Inject constructor( private val dailyMixManager: DailyMixManager, - private val aiOrchestrator: AiOrchestrator, + private val aiHandler: AiHandler, private val digestGenerator: UserProfileDigestGenerator, private val preferencesRepo: AiPreferencesRepository, private val json: Json @@ -73,7 +73,7 @@ class AiPlaylistGenerator @Inject constructor( """.trimIndent() - val responseText = aiOrchestrator.generateContent(fullPrompt, type) + val responseText = aiHandler.generateContent(fullPrompt, type) val songIds = extractPlaylistSongIds(responseText) 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 b6d943af8..c97959f5c 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 @@ -15,7 +15,7 @@ data class GeminiModel( @Singleton class GeminiModelService @Inject constructor( - private val orchestrator: AiOrchestrator, + private val handler: AiHandler, private val digestGenerator: UserProfileDigestGenerator, private val musicRepository: MusicRepository, private val workerManager: AiWorkerManager @@ -122,7 +122,7 @@ class GeminiModelService @Inject constructor( digestGenerator.generateDigest(allSongs) } else "" - return orchestrator.generateContent( + return handler.generateContent( prompt = prompt, type = type, temperature = temperature, 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 d339efbba..a09389b4a 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 @@ -41,7 +41,7 @@ class AiPreferencesRepository @Inject constructor( fun getSystemPrompt(provider: AiProvider) = stringPreferencesKey("${provider.name.lowercase()}_system_prompt") } - // Generic accessors for AiOrchestrator + // Generic accessors for AiHandler fun getApiKey(provider: AiProvider): Flow = dataStore.data.map { preferences -> preferences[Keys.getApiKey(provider)]?.trim() ?: "" } diff --git a/app/src/main/java/com/theveloper/pixelplay/data/worker/AiWorker.kt b/app/src/main/java/com/theveloper/pixelplay/data/worker/AiWorker.kt index a19c57615..0b1441d97 100644 --- a/app/src/main/java/com/theveloper/pixelplay/data/worker/AiWorker.kt +++ b/app/src/main/java/com/theveloper/pixelplay/data/worker/AiWorker.kt @@ -7,7 +7,7 @@ import androidx.work.CoroutineWorker import androidx.work.WorkerParameters import androidx.work.workDataOf import com.theveloper.pixelplay.data.ai.AiNotificationManager -import com.theveloper.pixelplay.data.ai.AiOrchestrator +import com.theveloper.pixelplay.data.ai.AiHandler import com.theveloper.pixelplay.data.ai.AiSystemPromptType import com.theveloper.pixelplay.data.ai.UserProfileDigestGenerator import com.theveloper.pixelplay.data.model.Song @@ -24,7 +24,7 @@ import timber.log.Timber class AiWorker @AssistedInject constructor( @Assisted appContext: Context, @Assisted workerParams: WorkerParameters, - private val orchestrator: AiOrchestrator, + private val handler: AiHandler, private val notificationManager: AiNotificationManager, private val musicRepository: MusicRepository, private val digestGenerator: UserProfileDigestGenerator, @@ -75,7 +75,7 @@ class AiWorker @AssistedInject constructor( digestGenerator.generateDigest(allSongs, isSafe) } else "" - val result = orchestrator.generateContent( + val result = handler.generateContent( prompt = prompt, type = type, temperature = temp, 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 ac467232d..948a0ad27 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 @@ -35,7 +35,7 @@ class AiStateHolder @Inject constructor( private val playlistPreferencesRepository: PlaylistPreferencesRepository, private val dailyMixStateHolder: DailyMixStateHolder, private val notificationManager: AiNotificationManager, - private val aiOrchestrator: com.theveloper.pixelplay.data.ai.AiOrchestrator + private val aiHandler: com.theveloper.pixelplay.data.ai.AiHandler ) { // State // AI State Management: Observables for tracking background generation progress @@ -363,7 +363,7 @@ Lyrics to translate: $lyricsText """.trimIndent() - val response = aiOrchestrator.generateContent( + val response = aiHandler.generateContent( prompt = prompt, type = AiSystemPromptType.GENERAL, temperature = 0.1f From 71dd8fb850dd7a4082010bba32745b03302e99b4 Mon Sep 17 00:00:00 2001 From: VoidX3D Date: Mon, 15 Jun 2026 19:00:30 +0545 Subject: [PATCH 16/72] feat(ai): add UnifiedModelFilter for consistent model filtering across all providers - Create UnifiedModelFilter utility that filters out embedding, image, TTS, speech, moderation, vision-only, and other non-chat models - Update GeminiAiClient to use UnifiedModelFilter instead of hardcoded markers - Update GenericOpenAiClient to use UnifiedModelFilter instead of inline filter --- .../data/ai/provider/GeminiAiClient.kt | 10 ++----- .../data/ai/provider/GenericOpenAiClient.kt | 4 +-- .../data/ai/provider/UnifiedModelFilter.kt | 30 +++++++++++++++++++ 3 files changed, 33 insertions(+), 11 deletions(-) create mode 100644 app/src/main/java/com/theveloper/pixelplay/data/ai/provider/UnifiedModelFilter.kt 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 b730603cb..30d638b59 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 @@ -25,12 +25,7 @@ class GeminiAiClient(private val apiKey: String) : AiClient { private const val DEFAULT_GEMINI_MODEL = "gemini-3.1-flash-lite" private const val BASE_URL = "https://generativelanguage.googleapis.com/v1beta" - // 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" - ) + } private val httpClient = OkHttpClient.Builder() @@ -260,8 +255,7 @@ class GeminiAiClient(private val apiKey: String) : AiClient { } private fun isNonChatModel(modelName: String): Boolean { - val lower = modelName.lowercase() - return NON_CHAT_MARKERS.any { lower.contains(it) } + return !UnifiedModelFilter.isModelUsableForChat(modelName) } private fun getDefaultModels(): List { 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 658906dd2..511a76328 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 @@ -140,9 +140,7 @@ class GenericOpenAiClient( val responseBody = response.body.string() val modelsResponse = json.decodeFromString(responseBody) - modelsResponse.data.map { it.id }.filter { - !it.contains("whisper") && !it.contains("embed") && !it.contains("tts") - } + modelsResponse.data.map { it.id }.let { UnifiedModelFilter.filterChatModels(it) } } catch (e: Exception) { listOf(defaultModelId) } diff --git a/app/src/main/java/com/theveloper/pixelplay/data/ai/provider/UnifiedModelFilter.kt b/app/src/main/java/com/theveloper/pixelplay/data/ai/provider/UnifiedModelFilter.kt new file mode 100644 index 000000000..d72d1786f --- /dev/null +++ b/app/src/main/java/com/theveloper/pixelplay/data/ai/provider/UnifiedModelFilter.kt @@ -0,0 +1,30 @@ +package com.theveloper.pixelplay.data.ai.provider + +object UnifiedModelFilter { + private val UNSUITABLE_PATTERNS = listOf( + "embedding", "embed", "aqa", "imagen", "image-generation", + "tts", "text-to-speech", "speech", "audio", "whisper", + "veo", "vision-only", "learnlm-embedding", "moderation", + "dall-e", "stable-diffusion", "sdxl", "kandinsky", + "upscale", "background", "remove-background", + "segment", "detect", "classify", "object-detection" + ) + + fun isModelUsableForChat(modelName: String): Boolean { + val lower = modelName.lowercase() + return UNSUITABLE_PATTERNS.none { lower.contains(it) } + } + + fun filterChatModels(models: List): List { + return models.filter { isModelUsableForChat(it) } + } + + fun filterChatModelsWithDefaults( + apiModels: List, + defaultModels: List + ): List { + return (apiModels.filter { isModelUsableForChat(it) } + defaultModels) + .distinct() + .sorted() + } +} From d599acbe9434f33e9608748056980c2a404d006b Mon Sep 17 00:00:00 2001 From: VoidX3D Date: Mon, 15 Jun 2026 19:00:49 +0545 Subject: [PATCH 17/72] refactor(ai): unify DeepSeek/Groq/Mistral to GenericOpenAiClient These providers all use OpenAI-compatible APIs. Switching from dedicated client classes to GenericOpenAiClient eliminates duplicate code. The old class files are kept on disk but no longer referenced. --- .../data/ai/provider/AiClientFactory.kt | 21 ++++++++++++++++--- 1 file changed, 18 insertions(+), 3 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 a1c29211e..c7591f390 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 @@ -22,9 +22,24 @@ class AiClientFactory @Inject constructor() { return when (provider) { AiProvider.GEMINI -> GeminiAiClient(apiKey) - AiProvider.DEEPSEEK -> DeepSeekAiClient(apiKey) - AiProvider.GROQ -> GroqAiClient(apiKey) - AiProvider.MISTRAL -> MistralAiClient(apiKey) + AiProvider.DEEPSEEK -> GenericOpenAiClient( + apiKey = apiKey, + baseUrl = "https://api.deepseek.com", + defaultModelId = "deepseek-chat", + providerName = "DeepSeek" + ) + AiProvider.GROQ -> GenericOpenAiClient( + apiKey = apiKey, + baseUrl = "https://api.groq.com/openai/v1", + defaultModelId = "llama-3.1-8b-instant", + providerName = "Groq" + ) + AiProvider.MISTRAL -> GenericOpenAiClient( + apiKey = apiKey, + baseUrl = "https://api.mistral.ai/v1", + defaultModelId = "mistral-large-latest", + providerName = "Mistral" + ) AiProvider.NVIDIA -> GenericOpenAiClient( apiKey = apiKey, baseUrl = "https://integrate.api.nvidia.com/v1", From 5d08f1be71b20c065a78a109538fbfe988fbabb8 Mon Sep 17 00:00:00 2001 From: VoidX3D Date: Mon, 15 Jun 2026 19:02:07 +0545 Subject: [PATCH 18/72] feat(ai): add CUSTOM provider entry to AiProvider enum Add CUSTOM provider with hasConfigurableUrl=true and requiresApiKey=true for user-configured self-hosted/custom API endpoints. --- .../com/theveloper/pixelplay/data/ai/provider/AiProvider.kt | 5 +++-- 1 file changed, 3 insertions(+), 2 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 f0f7b91dd..adc08789b 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,7 +3,7 @@ package com.theveloper.pixelplay.data.ai.provider /** * Enum representing available AI providers */ -enum class AiProvider(val displayName: String, val requiresApiKey: Boolean) { +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), @@ -12,7 +12,8 @@ enum class AiProvider(val displayName: String, val requiresApiKey: Boolean) { KIMI("Kimi (Moonshot)", requiresApiKey = true), GLM("Zhipu GLM", requiresApiKey = true), OPENAI("OpenAI", requiresApiKey = true), - OPENROUTER("OpenRouter", requiresApiKey = true); + OPENROUTER("OpenRouter", requiresApiKey = true), + CUSTOM("Custom Provider", requiresApiKey = true, hasConfigurableUrl = true); companion object { fun fromString(value: String): AiProvider { From b1f3c80417d3522c8ffd905e68b2a94a4461f920 Mon Sep 17 00:00:00 2001 From: VoidX3D Date: Mon, 15 Jun 2026 19:02:12 +0545 Subject: [PATCH 19/72] feat(ai): add CUSTOM provider and createClientWithUrl to AiClientFactory CUSTOM provider uses GenericOpenAiClient with an empty default URL (user configures it via settings). createClientWithUrl allows creating a client with a custom base URL for configurable-URL providers. --- .../pixelplay/data/ai/provider/AiClientFactory.kt | 11 +++++++++++ 1 file changed, 11 insertions(+) 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 c7591f390..1e4190050 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 @@ -70,6 +70,17 @@ class AiClientFactory @Inject constructor() { defaultModelId = "google/gemini-2.0-flash-lite-preview-02-05:free", providerName = "OpenRouter" ) + AiProvider.CUSTOM -> GenericOpenAiClient( + apiKey = apiKey, + baseUrl = "", + defaultModelId = "", + providerName = "Custom Provider" + ) } } + + fun createClientWithUrl(provider: AiProvider, apiKey: String, baseUrl: String): AiClient { + val displayName = provider.displayName + return GenericOpenAiClient(apiKey, baseUrl.trimEnd('/'), "", displayName) + } } From a5fdfeff616c679082208c1060ca7ee4cee61560 Mon Sep 17 00:00:00 2001 From: VoidX3D Date: Mon, 15 Jun 2026 19:02:17 +0545 Subject: [PATCH 20/72] feat(ai): add CUSTOM to provider fallback chain in AiProviderSupport --- .../theveloper/pixelplay/data/ai/provider/AiProviderSupport.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/theveloper/pixelplay/data/ai/provider/AiProviderSupport.kt b/app/src/main/java/com/theveloper/pixelplay/data/ai/provider/AiProviderSupport.kt index 386758356..7a8d8de2c 100644 --- a/app/src/main/java/com/theveloper/pixelplay/data/ai/provider/AiProviderSupport.kt +++ b/app/src/main/java/com/theveloper/pixelplay/data/ai/provider/AiProviderSupport.kt @@ -88,7 +88,8 @@ internal object AiProviderSupport { AiProvider.OPENROUTER, AiProvider.NVIDIA, AiProvider.KIMI, - AiProvider.GLM + AiProvider.GLM, + AiProvider.CUSTOM ) return buildList { From cf7f4b50521a09cff38982ab24a1f251b2a245e2 Mon Sep 17 00:00:00 2001 From: VoidX3D Date: Mon, 15 Jun 2026 19:02:23 +0545 Subject: [PATCH 21/72] feat(ai): add base URL support and CUSTOM provider prefs to AiPreferencesRepository Add getBaseUrl/setBaseUrl generic accessors for configurable-URL providers. Add customApiKey, customModel, customSystemPrompt, customBaseUrl convenience flows for the CUSTOM provider. --- .../data/preferences/AiPreferencesRepository.kt | 13 +++++++++++++ 1 file changed, 13 insertions(+) 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 a09389b4a..ad1cdc5e6 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 @@ -39,6 +39,7 @@ class AiPreferencesRepository @Inject constructor( fun getApiKey(provider: AiProvider) = stringPreferencesKey("${provider.name.lowercase()}_api_key") fun getModel(provider: AiProvider) = stringPreferencesKey("${provider.name.lowercase()}_model") fun getSystemPrompt(provider: AiProvider) = stringPreferencesKey("${provider.name.lowercase()}_system_prompt") + fun getBaseUrl(provider: AiProvider) = stringPreferencesKey("${provider.name.lowercase()}_base_url") } // Generic accessors for AiHandler @@ -53,6 +54,9 @@ class AiPreferencesRepository @Inject constructor( preferences[Keys.getSystemPrompt(provider)] ?: DEFAULT_SYSTEM_PROMPT } + fun getBaseUrl(provider: AiProvider): Flow = + dataStore.data.map { preferences -> preferences[Keys.getBaseUrl(provider)] ?: "" } + suspend fun setApiKey(provider: AiProvider, apiKey: String) { dataStore.edit { preferences -> preferences[Keys.getApiKey(provider)] = apiKey.trim() } } @@ -71,6 +75,10 @@ class AiPreferencesRepository @Inject constructor( } } + suspend fun setBaseUrl(provider: AiProvider, url: String) { + dataStore.edit { preferences -> preferences[Keys.getBaseUrl(provider)] = url.trim() } + } + // Convenience properties for legacy compatibility (e.g. PlayerViewModel) val geminiApiKey: Flow = getApiKey(AiProvider.GEMINI) val geminiModel: Flow = getModel(AiProvider.GEMINI) @@ -108,6 +116,11 @@ class AiPreferencesRepository @Inject constructor( val openrouterModel: Flow = getModel(AiProvider.OPENROUTER) val openrouterSystemPrompt: Flow = getSystemPrompt(AiProvider.OPENROUTER) + val customApiKey: Flow = getApiKey(AiProvider.CUSTOM) + val customModel: Flow = getModel(AiProvider.CUSTOM) + val customSystemPrompt: Flow = getSystemPrompt(AiProvider.CUSTOM) + val customBaseUrl: Flow = getBaseUrl(AiProvider.CUSTOM) + val aiProvider: Flow = dataStore.data.map { preferences -> preferences[Keys.AI_PROVIDER] ?: "GEMINI" } From c8fd759ec41b6915b3db7d5bdb20a4d369662183 Mon Sep 17 00:00:00 2001 From: VoidX3D Date: Mon, 15 Jun 2026 19:02:34 +0545 Subject: [PATCH 22/72] feat(ai): add OLLAMA provider entry to AiProvider enum Ollama is a cloud API-based provider (requires API key, fixed URL), separate from the CUSTOM provider which allows custom endpoints. --- .../java/com/theveloper/pixelplay/data/ai/provider/AiProvider.kt | 1 + 1 file changed, 1 insertion(+) 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 adc08789b..229f1d314 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 @@ -13,6 +13,7 @@ enum class AiProvider(val displayName: String, val requiresApiKey: Boolean, val GLM("Zhipu GLM", requiresApiKey = true), OPENAI("OpenAI", requiresApiKey = true), OPENROUTER("OpenRouter", requiresApiKey = true), + OLLAMA("Ollama", requiresApiKey = true), CUSTOM("Custom Provider", requiresApiKey = true, hasConfigurableUrl = true); companion object { From c83cc4f673dbe1a75bdb7e8c787e5db28829419c Mon Sep 17 00:00:00 2001 From: VoidX3D Date: Mon, 15 Jun 2026 19:02:55 +0545 Subject: [PATCH 23/72] feat(ai): add OLLAMA provider implementation to AiClientFactory --- .../pixelplay/data/ai/provider/AiClientFactory.kt | 6 ++++++ 1 file changed, 6 insertions(+) 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 1e4190050..4322ac24e 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 @@ -70,6 +70,12 @@ class AiClientFactory @Inject constructor() { defaultModelId = "google/gemini-2.0-flash-lite-preview-02-05:free", providerName = "OpenRouter" ) + AiProvider.OLLAMA -> GenericOpenAiClient( + apiKey = apiKey, + baseUrl = "https://api.ollama.ai/v1", + defaultModelId = "llama3", + providerName = "Ollama" + ) AiProvider.CUSTOM -> GenericOpenAiClient( apiKey = apiKey, baseUrl = "", From f13e7dabc0cc35001c3c2436709a793c23df4989 Mon Sep 17 00:00:00 2001 From: VoidX3D Date: Mon, 15 Jun 2026 19:03:23 +0545 Subject: [PATCH 24/72] feat(ai): add OLLAMA to provider fallback chain --- .../theveloper/pixelplay/data/ai/provider/AiProviderSupport.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/java/com/theveloper/pixelplay/data/ai/provider/AiProviderSupport.kt b/app/src/main/java/com/theveloper/pixelplay/data/ai/provider/AiProviderSupport.kt index 7a8d8de2c..82c61f6a9 100644 --- a/app/src/main/java/com/theveloper/pixelplay/data/ai/provider/AiProviderSupport.kt +++ b/app/src/main/java/com/theveloper/pixelplay/data/ai/provider/AiProviderSupport.kt @@ -89,6 +89,7 @@ internal object AiProviderSupport { AiProvider.NVIDIA, AiProvider.KIMI, AiProvider.GLM, + AiProvider.OLLAMA, AiProvider.CUSTOM ) From 452dcfe928cc00d75c8d0f1fc561dfda3705e747 Mon Sep 17 00:00:00 2001 From: VoidX3D Date: Mon, 15 Jun 2026 19:03:37 +0545 Subject: [PATCH 25/72] feat(ai): add OLLAMA provider convenience flows to AiPreferencesRepository --- .../pixelplay/data/preferences/AiPreferencesRepository.kt | 4 ++++ 1 file changed, 4 insertions(+) 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 ad1cdc5e6..f30843e51 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 @@ -116,6 +116,10 @@ class AiPreferencesRepository @Inject constructor( val openrouterModel: Flow = getModel(AiProvider.OPENROUTER) val openrouterSystemPrompt: Flow = getSystemPrompt(AiProvider.OPENROUTER) + val ollamaApiKey: Flow = getApiKey(AiProvider.OLLAMA) + val ollamaModel: Flow = getModel(AiProvider.OLLAMA) + val ollamaSystemPrompt: Flow = getSystemPrompt(AiProvider.OLLAMA) + val customApiKey: Flow = getApiKey(AiProvider.CUSTOM) val customModel: Flow = getModel(AiProvider.CUSTOM) val customSystemPrompt: Flow = getSystemPrompt(AiProvider.CUSTOM) From 3abd56bd565d5eab5f0b09ba130b17bd6a10a07f Mon Sep 17 00:00:00 2001 From: VoidX3D Date: Mon, 15 Jun 2026 19:04:03 +0545 Subject: [PATCH 26/72] feat(ai): add SearchableModelSelector composable with search bar A new composable that opens a ModalBottomSheet with a searchable LazyColumn of AI models. Includes search filtering, model count display, and visual selection state. --- .../screens/SettingsComponents.kt | 177 +++++++++++++++++- 1 file changed, 175 insertions(+), 2 deletions(-) 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 b13cad46c..3db71c961 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 @@ -362,14 +362,187 @@ fun ExpressiveSettingsGroup( ) { Column( modifier = modifier - .clip(RoundedCornerShape(24.dp)) // Large corners for the group + .clip(RoundedCornerShape(24.dp)) .background(Color.Transparent), - //verticalArrangement = Arrangement.spacedBy(4.dp) ) { content() } } +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun SearchableModelSelector( + label: String, + description: String, + models: List, + selectedModelName: String, + onModelSelected: (String) -> Unit, + leadingIcon: @Composable () -> Unit +) { + var showSheet by remember { mutableStateOf(false) } + var searchQuery by remember { mutableStateOf("") } + val selectedDisplayName = models.find { it.name == selectedModelName }?.displayName ?: selectedModelName + + 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 models...") }, + 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 filteredModels = remember(models, searchQuery) { + if (searchQuery.isBlank()) models + else models.filter { + it.name.contains(searchQuery, ignoreCase = true) || + it.displayName.contains(searchQuery, ignoreCase = true) + } + } + + Text( + text = "${filteredModels.size} model${if (filteredModels.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(filteredModels, key = { it.name }) { model -> + val isSelected = model.name == selectedModelName + Surface( + color = if (isSelected) MaterialTheme.colorScheme.primaryContainer + else MaterialTheme.colorScheme.surfaceContainerHigh, + shape = RoundedCornerShape(10.dp), + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 4.dp) + .clickable { + onModelSelected(model.name) + showSheet = false + searchQuery = "" + } + ) { + Row( + modifier = Modifier.padding(16.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Column(modifier = Modifier.weight(1f)) { + Text( + text = model.displayName, + style = MaterialTheme.typography.bodyLarge, + color = if (isSelected) MaterialTheme.colorScheme.onPrimaryContainer + else MaterialTheme.colorScheme.onSurface + ) + Text( + text = model.name, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + if (isSelected) { + Icon( + imageVector = Icons.Rounded.CheckCircle, + contentDescription = "Selected", + tint = MaterialTheme.colorScheme.primary + ) + } + } + } + } + } + } + } + } +} + @Composable fun SliderSettingsItem( label: String, From 775766de375412890579d325cdefc72400f73020 Mon Sep 17 00:00:00 2001 From: VoidX3D Date: Mon, 15 Jun 2026 19:05:34 +0545 Subject: [PATCH 27/72] feat(ai): add Ollama/Custom provider flows and base URL state to SettingsViewModel --- .../viewmodel/SettingsViewModel.kt | 57 ++++++++++++++++++- 1 file changed, 56 insertions(+), 1 deletion(-) 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 d07d8b3df..41f4a5e01 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 @@ -275,6 +275,30 @@ class SettingsViewModel @Inject constructor( 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) + if (p.hasConfigurableUrl) aiPreferencesRepository.getBaseUrl(p) + else flowOf("") + } + .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), "") + fun onAiApiKeyChange(apiKey: String) { viewModelScope.launch { val providerStr = aiProvider.value @@ -349,6 +373,25 @@ class SettingsViewModel @Inject constructor( 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) + } + } fun onAiModelChange(model: String) { viewModelScope.launch { @@ -366,6 +409,8 @@ class SettingsViewModel @Inject constructor( 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 { @@ -383,6 +428,8 @@ class SettingsViewModel @Inject constructor( 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 { @@ -400,6 +447,8 @@ class SettingsViewModel @Inject constructor( 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 { @@ -1149,7 +1198,13 @@ class SettingsViewModel @Inject constructor( val models = if (provider == AiProvider.GEMINI) { geminiModelService.fetchAvailableModels(apiKey).getOrThrow() } else { - val aiClient = aiClientFactory.createClient(provider, apiKey) + val baseUrl = if (provider.hasConfigurableUrl) + aiPreferencesRepository.getBaseUrl(provider).first() + else "" + val aiClient = if (provider.hasConfigurableUrl) + aiClientFactory.createClientWithUrl(provider, apiKey, baseUrl) + else + aiClientFactory.createClient(provider, apiKey) aiClient.getAvailableModels(apiKey) .map { it.trim() } .filter { it.isNotBlank() } From ee57fae98a9adbfa69078d51c11a913e8d4105d8 Mon Sep 17 00:00:00 2001 From: VoidX3D Date: Mon, 15 Jun 2026 19:07:02 +0545 Subject: [PATCH 28/72] feat(ui): add OLLAMA/CUSTOM provider labels, SearchableModelSelector, and base URL config field --- .../screens/SettingsCategoryScreen.kt | 22 +++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) 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 d1ff2628b..379d9c11d 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 @@ -950,6 +950,8 @@ fun SettingsCategoryScreen( com.theveloper.pixelplay.data.ai.provider.AiProvider.GLM -> stringResource(R.string.settings_ai_source_glm) com.theveloper.pixelplay.data.ai.provider.AiProvider.OPENAI -> stringResource(R.string.settings_ai_source_openai) com.theveloper.pixelplay.data.ai.provider.AiProvider.OPENROUTER -> "OpenRouter (openrouter.ai)" + com.theveloper.pixelplay.data.ai.provider.AiProvider.OLLAMA -> "Ollama (cloud)" + com.theveloper.pixelplay.data.ai.provider.AiProvider.CUSTOM -> "Custom Provider" } AiApiKeyItem( @@ -999,18 +1001,30 @@ fun SettingsCategoryScreen( ) } } else if (uiState.availableModels.isNotEmpty()) { - ThemeSelectorItem( + SearchableModelSelector( label = stringResource(R.string.settings_ai_model_title), description = stringResource(R.string.settings_ai_model_subtitle), - options = uiState.availableModels.associate { it.name to it.displayName }, - selectedKey = currentAiModel.ifEmpty { uiState.availableModels.firstOrNull()?.name ?: "" }, - onSelectionChanged = { settingsViewModel.onAiModelChange(it) }, + models = uiState.availableModels, + selectedModelName = currentAiModel.ifEmpty { uiState.availableModels.firstOrNull()?.name ?: "" }, + onModelSelected = { settingsViewModel.onAiModelChange(it) }, leadingIcon = { Icon(Icons.Rounded.Science, null, tint = MaterialTheme.colorScheme.secondary) } ) } } } + // Base URL Section (only for configurable URL providers) + if (provider.hasConfigurableUrl) { + SettingsSubsection(title = "API Base URL") { + AiApiKeyItem( + apiKey = customBaseUrl, + onApiKeySave = { settingsViewModel.onCustomBaseUrlChange(it) }, + title = "Base URL", + subtitle = "e.g. https://api.example.com/v1" + ) + } + } + // Prompt Behavior Section SettingsSubsection( title = stringResource(R.string.settings_prompt_behavior_section), From ca27d419aaf73be7359c42f689e57db8e5175218 Mon Sep 17 00:00:00 2001 From: VoidX3D Date: Mon, 15 Jun 2026 19:07:13 +0545 Subject: [PATCH 29/72] chore: remove unused DeepSeekAiClient, GroqAiClient, MistralAiClient (unified into GenericOpenAiClient) --- .../data/ai/provider/DeepSeekAiClient.kt | 171 ------------------ .../data/ai/provider/GroqAiClient.kt | 170 ----------------- .../data/ai/provider/MistralAiClient.kt | 169 ----------------- 3 files changed, 510 deletions(-) delete mode 100644 app/src/main/java/com/theveloper/pixelplay/data/ai/provider/DeepSeekAiClient.kt delete mode 100644 app/src/main/java/com/theveloper/pixelplay/data/ai/provider/GroqAiClient.kt delete mode 100644 app/src/main/java/com/theveloper/pixelplay/data/ai/provider/MistralAiClient.kt diff --git a/app/src/main/java/com/theveloper/pixelplay/data/ai/provider/DeepSeekAiClient.kt b/app/src/main/java/com/theveloper/pixelplay/data/ai/provider/DeepSeekAiClient.kt deleted file mode 100644 index afb84b3ea..000000000 --- a/app/src/main/java/com/theveloper/pixelplay/data/ai/provider/DeepSeekAiClient.kt +++ /dev/null @@ -1,171 +0,0 @@ -package com.theveloper.pixelplay.data.ai.provider - -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext -import kotlinx.serialization.Serializable -import kotlinx.serialization.json.Json -import okhttp3.MediaType.Companion.toMediaType -import okhttp3.OkHttpClient -import okhttp3.Request -import okhttp3.RequestBody.Companion.toRequestBody -import java.util.concurrent.TimeUnit - -/** - * DeepSeek AI provider implementation - * Uses OpenAI-compatible API - */ -class DeepSeekAiClient(private val apiKey: String) : AiClient { - - companion object { - private const val DEFAULT_DEEPSEEK_MODEL = "deepseek-chat" - private const val BASE_URL = "https://api.deepseek.com" - } - - @Serializable - data class ChatMessage(val role: String, val content: String) - - @Serializable - data class ChatRequest( - val model: String, - val messages: List, - val temperature: Double = 0.7 - ) - - @Serializable - data class ChatChoice(val message: ChatMessage) - - @Serializable - data class ChatResponse(val choices: List) - - @Serializable - data class ModelItem(val id: String) - - @Serializable - 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 - } - - override suspend fun generateContent( - model: String, - systemPrompt: String, - prompt: String, - temperature: Float - ): String { - return withContext(Dispatchers.IO) { - val resolvedModel = model.ifBlank { DEFAULT_DEEPSEEK_MODEL } - val messagesList = mutableListOf() - if (systemPrompt.isNotBlank()) { - messagesList.add(ChatMessage(role = "system", content = systemPrompt)) - } - messagesList.add(ChatMessage(role = "user", content = prompt)) - - val requestBody = ChatRequest( - model = resolvedModel, - messages = messagesList, - temperature = temperature.toDouble() - ) - - val jsonBody = json.encodeToString(ChatRequest.serializer(), requestBody) - val body = jsonBody.toRequestBody("application/json".toMediaType()) - - val request = Request.Builder() - .url("$BASE_URL/chat/completions") - .addHeader("Authorization", "Bearer $apiKey") - .addHeader("Content-Type", "application/json") - .post(body) - .build() - - try { - client.newCall(request).execute().use { response -> - val responseBody = response.body.string() - - if (!response.isSuccessful) { - throw AiProviderSupport.createException( - providerName = "DeepSeek", - statusCode = response.code, - transportMessage = response.message, - responseBody = responseBody, - requestedModel = resolvedModel - ) - } - - val chatResponse = json.decodeFromString(responseBody) - chatResponse.choices.firstOrNull()?.message?.content - ?: throw AiProviderSupport.createException( - providerName = "DeepSeek", - statusCode = response.code, - transportMessage = "Response had no content", - responseBody = responseBody, - requestedModel = resolvedModel - ) - } - } catch (e: Exception) { - throw AiProviderSupport.wrapThrowable("DeepSeek", e, resolvedModel) - } - } - } - - override suspend fun countTokens(model: String, systemPrompt: String, prompt: String): Int { - // DeepSeek estimation - return (systemPrompt.length + prompt.length) / 4 - } - - override suspend fun getAvailableModels(apiKey: String): List { - return withContext(Dispatchers.IO) { - try { - val request = Request.Builder() - .url("$BASE_URL/models") - .addHeader("Authorization", "Bearer $apiKey") - .get() - .build() - - val response = client.newCall(request).execute() - - if (!response.isSuccessful) { - return@withContext getDefaultModels() - } - - val responseBody = response.body.string() - val modelsResponse = json.decodeFromString(responseBody) - modelsResponse.data.map { it.id } - } catch (e: Exception) { - getDefaultModels() - } - } - } - - override suspend fun validateApiKey(apiKey: String): Boolean { - return withContext(Dispatchers.IO) { - try { - val request = Request.Builder() - .url("$BASE_URL/models") - .addHeader("Authorization", "Bearer $apiKey") - .get() - .build() - - val response = client.newCall(request).execute() - response.isSuccessful - } catch (e: Exception) { - false - } - } - } - - override fun getDefaultModel(): String = DEFAULT_DEEPSEEK_MODEL - - private fun getDefaultModels(): List { - return listOf( - "deepseek-chat", - "deepseek-reasoner" - ) - } -} diff --git a/app/src/main/java/com/theveloper/pixelplay/data/ai/provider/GroqAiClient.kt b/app/src/main/java/com/theveloper/pixelplay/data/ai/provider/GroqAiClient.kt deleted file mode 100644 index 0adf6cf70..000000000 --- a/app/src/main/java/com/theveloper/pixelplay/data/ai/provider/GroqAiClient.kt +++ /dev/null @@ -1,170 +0,0 @@ -package com.theveloper.pixelplay.data.ai.provider - -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext -import kotlinx.serialization.Serializable -import kotlinx.serialization.json.Json -import okhttp3.MediaType.Companion.toMediaType -import okhttp3.OkHttpClient -import okhttp3.Request -import okhttp3.RequestBody.Companion.toRequestBody -import java.util.concurrent.TimeUnit - -class GroqAiClient(private val apiKey: String) : AiClient { - - companion object { - private const val DEFAULT_MODEL = "llama-3.1-8b-instant" - private const val BASE_URL = "https://api.groq.com/openai/v1" - } - - @Serializable - private data class ChatMessage(val role: String, val content: String) - - @Serializable - private data class ChatRequest( - val model: String, - val messages: List, - val temperature: Double = 0.7 - ) - - @Serializable - private data class ChatChoice(val message: ChatMessage) - - @Serializable - private data class ChatResponse(val choices: List) - - @Serializable - private data class ModelItem(val id: String) - - @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 - } - - override suspend fun generateContent( - model: String, - systemPrompt: String, - prompt: String, - temperature: Float - ): String { - return withContext(Dispatchers.IO) { - val resolvedModel = model.ifBlank { DEFAULT_MODEL } - val messagesList = mutableListOf() - if (systemPrompt.isNotBlank()) { - messagesList.add(ChatMessage(role = "system", content = systemPrompt)) - } - messagesList.add(ChatMessage(role = "user", content = prompt)) - - val requestBody = ChatRequest( - model = resolvedModel, - messages = messagesList, - temperature = temperature.toDouble() - ) - - val jsonBody = json.encodeToString(ChatRequest.serializer(), requestBody) - val body = jsonBody.toRequestBody("application/json".toMediaType()) - - val request = Request.Builder() - .url("$BASE_URL/chat/completions") - .addHeader("Authorization", "Bearer $apiKey") - .addHeader("Content-Type", "application/json") - .post(body) - .build() - - try { - client.newCall(request).execute().use { response -> - val responseBody = response.body.string() - - if (!response.isSuccessful) { - throw AiProviderSupport.createException( - providerName = "Groq", - statusCode = response.code, - transportMessage = response.message, - responseBody = responseBody, - requestedModel = resolvedModel - ) - } - - val chatResponse = json.decodeFromString(responseBody) - chatResponse.choices.firstOrNull()?.message?.content - ?: throw AiProviderSupport.createException( - providerName = "Groq", - statusCode = response.code, - transportMessage = "Response had no content", - responseBody = responseBody, - requestedModel = resolvedModel - ) - } - } catch (e: Exception) { - throw AiProviderSupport.wrapThrowable("Groq", e, resolvedModel) - } - } - } - - override suspend fun countTokens(model: String, systemPrompt: String, prompt: String): Int { - // Groq doesn't provide a native token counting endpoint, so we estimate. - // Rule of thumb: 1 token ≈ 4 characters for English text. - return (systemPrompt.length + prompt.length) / 4 - } - - override suspend fun getAvailableModels(apiKey: String): List { - return withContext(Dispatchers.IO) { - try { - val request = Request.Builder() - .url("$BASE_URL/models") - .addHeader("Authorization", "Bearer $apiKey") - .get() - .build() - - val response = client.newCall(request).execute() - - if (!response.isSuccessful) { - return@withContext getDefaultModels() - } - - val responseBody = response.body.string() - val modelsResponse = json.decodeFromString(responseBody) - modelsResponse.data.map { it.id }.filter { !it.contains("whisper") } - } catch (e: Exception) { - getDefaultModels() - } - } - } - - override suspend fun validateApiKey(apiKey: String): Boolean { - return withContext(Dispatchers.IO) { - try { - val request = Request.Builder() - .url("$BASE_URL/models") - .addHeader("Authorization", "Bearer $apiKey") - .get() - .build() - - val response = client.newCall(request).execute() - response.isSuccessful - } catch (e: Exception) { - false - } - } - } - - override fun getDefaultModel(): String = DEFAULT_MODEL - - private fun getDefaultModels(): List { - return listOf( - "llama-3.1-8b-instant", - "llama-3.3-70b-versatile", - "mixtral-8x7b-32768", - "gemma2-9b-it" - ) - } -} diff --git a/app/src/main/java/com/theveloper/pixelplay/data/ai/provider/MistralAiClient.kt b/app/src/main/java/com/theveloper/pixelplay/data/ai/provider/MistralAiClient.kt deleted file mode 100644 index a4d166e2a..000000000 --- a/app/src/main/java/com/theveloper/pixelplay/data/ai/provider/MistralAiClient.kt +++ /dev/null @@ -1,169 +0,0 @@ -package com.theveloper.pixelplay.data.ai.provider - -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext -import kotlinx.serialization.Serializable -import kotlinx.serialization.json.Json -import okhttp3.MediaType.Companion.toMediaType -import okhttp3.OkHttpClient -import okhttp3.Request -import okhttp3.RequestBody.Companion.toRequestBody -import java.util.concurrent.TimeUnit - -class MistralAiClient(private val apiKey: String) : AiClient { - - companion object { - private const val DEFAULT_MODEL = "mistral-large-latest" - private const val BASE_URL = "https://api.mistral.ai/v1" - } - - @Serializable - private data class ChatMessage(val role: String, val content: String) - - @Serializable - private data class ChatRequest( - val model: String, - val messages: List, - val temperature: Double = 0.7 - ) - - @Serializable - private data class ChatChoice(val message: ChatMessage) - - @Serializable - private data class ChatResponse(val choices: List) - - @Serializable - private data class ModelItem(val id: String) - - @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 - } - - override suspend fun generateContent( - model: String, - systemPrompt: String, - prompt: String, - temperature: Float - ): String { - return withContext(Dispatchers.IO) { - val resolvedModel = model.ifBlank { DEFAULT_MODEL } - val messagesList = mutableListOf() - if (systemPrompt.isNotBlank()) { - messagesList.add(ChatMessage(role = "system", content = systemPrompt)) - } - messagesList.add(ChatMessage(role = "user", content = prompt)) - - val requestBody = ChatRequest( - model = resolvedModel, - messages = messagesList, - temperature = temperature.toDouble() - ) - - val jsonBody = json.encodeToString(ChatRequest.serializer(), requestBody) - val body = jsonBody.toRequestBody("application/json".toMediaType()) - - val request = Request.Builder() - .url("$BASE_URL/chat/completions") - .addHeader("Authorization", "Bearer $apiKey") - .addHeader("Content-Type", "application/json") - .post(body) - .build() - - try { - client.newCall(request).execute().use { response -> - val responseBody = response.body.string() - - if (!response.isSuccessful) { - throw AiProviderSupport.createException( - providerName = "Mistral", - statusCode = response.code, - transportMessage = response.message, - responseBody = responseBody, - requestedModel = resolvedModel - ) - } - - val chatResponse = json.decodeFromString(responseBody) - chatResponse.choices.firstOrNull()?.message?.content - ?: throw AiProviderSupport.createException( - providerName = "Mistral", - statusCode = response.code, - transportMessage = "Response had no content", - responseBody = responseBody, - requestedModel = resolvedModel - ) - } - } catch (e: Exception) { - throw AiProviderSupport.wrapThrowable("Mistral", e, resolvedModel) - } - } - } - - override suspend fun countTokens(model: String, systemPrompt: String, prompt: String): Int { - // Mistral estimation - return (systemPrompt.length + prompt.length) / 4 - } - - override suspend fun getAvailableModels(apiKey: String): List { - return withContext(Dispatchers.IO) { - try { - val request = Request.Builder() - .url("$BASE_URL/models") - .addHeader("Authorization", "Bearer $apiKey") - .get() - .build() - - val response = client.newCall(request).execute() - - if (!response.isSuccessful) { - return@withContext getDefaultModels() - } - - val responseBody = response.body.string() - val modelsResponse = json.decodeFromString(responseBody) - modelsResponse.data.map { it.id } - } catch (e: Exception) { - getDefaultModels() - } - } - } - - override suspend fun validateApiKey(apiKey: String): Boolean { - return withContext(Dispatchers.IO) { - try { - val request = Request.Builder() - .url("$BASE_URL/models") - .addHeader("Authorization", "Bearer $apiKey") - .get() - .build() - - val response = client.newCall(request).execute() - response.isSuccessful - } catch (e: Exception) { - false - } - } - } - - override fun getDefaultModel(): String = DEFAULT_MODEL - - private fun getDefaultModels(): List { - return listOf( - "mistral-large-latest", - "mistral-small-latest", - "open-mixtral-8x22b", - "open-mixtral-8x7b" - ) - } -} From bd711b5cac022f7c117f3ba7038311480e35d63a Mon Sep 17 00:00:00 2001 From: VoidX3D Date: Mon, 15 Jun 2026 19:11:31 +0545 Subject: [PATCH 30/72] feat(ai): add topP, topK, maxTokens, presencePenalty, frequencyPenalty to AiClient interface --- .../pixelplay/data/ai/provider/AiClient.kt | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) 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 413a0fb2f..348547ee6 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 @@ -5,20 +5,17 @@ package com.theveloper.pixelplay.data.ai.provider * Defines common operations for text generation and metadata completion */ interface AiClient { - - /** - * Generate text content based on a prompt - * @param model The model identifier to use - * @param systemPrompt The system prompt instructions - * @param prompt The input prompt - * @param temperature Creativity control (0.0 to 1.0) - * @return Generated text response - */ + suspend fun generateContent( model: String, systemPrompt: String, prompt: String, - temperature: Float = 0.7f + temperature: Float = 0.7f, + topP: Float = 0.95f, + topK: Int = 64, + maxTokens: Int = 4096, + presencePenalty: Float = 0.0f, + frequencyPenalty: Float = 0.0f ): String /** From 70c756bd2a0e64e7ae979b6b3f3f90072b2b9815 Mon Sep 17 00:00:00 2001 From: VoidX3D Date: Mon, 15 Jun 2026 19:12:04 +0545 Subject: [PATCH 31/72] feat(ai): add topP, maxTokens, presencePenalty, frequencyPenalty to GenericOpenAiClient ChatRequest --- .../data/ai/provider/GenericOpenAiClient.kt | 20 ++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) 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 511a76328..fd0fb1c1d 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 @@ -2,6 +2,7 @@ package com.theveloper.pixelplay.data.ai.provider import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext +import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable import kotlinx.serialization.json.Json import okhttp3.MediaType.Companion.toMediaType @@ -27,7 +28,11 @@ class GenericOpenAiClient( private data class ChatRequest( val model: String, val messages: List, - val temperature: Double = 0.7 + val temperature: Double = 0.7, + @SerialName("top_p") val topP: Double? = null, + @SerialName("max_tokens") val maxTokens: Int? = null, + @SerialName("presence_penalty") val presencePenalty: Double? = null, + @SerialName("frequency_penalty") val frequencyPenalty: Double? = null ) @Serializable @@ -57,7 +62,12 @@ class GenericOpenAiClient( model: String, systemPrompt: String, prompt: String, - temperature: Float + temperature: Float, + topP: Float, + topK: Int, + maxTokens: Int, + presencePenalty: Float, + frequencyPenalty: Float ): String { return withContext(Dispatchers.IO) { val resolvedModel = model.ifBlank { defaultModelId } @@ -70,7 +80,11 @@ class GenericOpenAiClient( val requestBody = ChatRequest( model = resolvedModel, messages = messagesList, - temperature = temperature.toDouble() + temperature = temperature.toDouble(), + topP = topP.toDouble(), + maxTokens = maxTokens.takeIf { it > 0 }, + presencePenalty = presencePenalty.toDouble(), + frequencyPenalty = frequencyPenalty.toDouble() ) val jsonBody = json.encodeToString(ChatRequest.serializer(), requestBody) From 479104fdf468d59bc65380b7f5b62c16cbc3b92a Mon Sep 17 00:00:00 2001 From: VoidX3D Date: Mon, 15 Jun 2026 19:12:29 +0545 Subject: [PATCH 32/72] feat(ai): add topP, topK, maxTokens, presencePenalty, frequencyPenalty to GeminiAiClient --- .../data/ai/provider/GeminiAiClient.kt | 21 ++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) 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 30d638b59..c2288296a 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 @@ -50,7 +50,10 @@ class GeminiAiClient(private val apiKey: String) : AiClient { private data class GenerationConfig( val temperature: Double, val topK: Int = 64, - val topP: Double = 0.95 + val topP: Double = 0.95, + @SerialName("maxOutputTokens") val maxOutputTokens: Int = 8192, + @SerialName("presencePenalty") val presencePenalty: Double? = null, + @SerialName("frequencyPenalty") val frequencyPenalty: Double? = null ) @Serializable @@ -81,7 +84,12 @@ class GeminiAiClient(private val apiKey: String) : AiClient { model: String, systemPrompt: String, prompt: String, - temperature: Float + temperature: Float, + topP: Float, + topK: Int, + maxTokens: Int, + presencePenalty: Float, + frequencyPenalty: Float ): String { return withContext(Dispatchers.IO) { val resolvedModel = model.ifBlank { DEFAULT_GEMINI_MODEL } @@ -91,7 +99,14 @@ class GeminiAiClient(private val apiKey: String) : AiClient { systemInstruction = systemPrompt .takeIf { it.isNotBlank() } ?.let { Content(parts = listOf(Part(it))) }, - generationConfig = GenerationConfig(temperature = temperature.toDouble()) + generationConfig = GenerationConfig( + temperature = temperature.toDouble(), + topK = topK, + topP = topP.toDouble(), + maxOutputTokens = maxTokens, + presencePenalty = presencePenalty.toDouble().takeIf { it != 0.0 }, + frequencyPenalty = frequencyPenalty.toDouble().takeIf { it != 0.0 } + ) ) val jsonBody = json.encodeToString(GenerateRequest.serializer(), requestBody) From 9ddb02b3a7137655a4cc9e6afda3d1cab2af58d0 Mon Sep 17 00:00:00 2001 From: VoidX3D Date: Mon, 15 Jun 2026 19:13:05 +0545 Subject: [PATCH 33/72] feat(ai): add generation parameter and song data configuration preferences Adds DataStore-backed preferences for temperature, topP, topK, maxTokens, presencePenalty, frequencyPenalty, sample size, digest mode (safe/full), and extended fields toggle. --- .../preferences/AiPreferencesRepository.kt | 74 +++++++++++++++++++ 1 file changed, 74 insertions(+) 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 f30843e51..964875d04 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 @@ -4,6 +4,8 @@ import androidx.datastore.core.DataStore import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.core.booleanPreferencesKey import androidx.datastore.preferences.core.edit +import androidx.datastore.preferences.core.floatPreferencesKey +import androidx.datastore.preferences.core.intPreferencesKey import androidx.datastore.preferences.core.stringPreferencesKey import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map @@ -35,6 +37,15 @@ class AiPreferencesRepository @Inject constructor( private object Keys { val AI_PROVIDER = stringPreferencesKey("ai_provider") val SAFE_TOKEN_LIMIT = booleanPreferencesKey("safe_token_limit") + val AI_TEMPERATURE = floatPreferencesKey("ai_temperature") + val AI_TOP_P = floatPreferencesKey("ai_top_p") + val AI_TOP_K = intPreferencesKey("ai_top_k") + val AI_MAX_TOKENS = intPreferencesKey("ai_max_tokens") + val AI_PRESENCE_PENALTY = floatPreferencesKey("ai_presence_penalty") + val AI_FREQUENCY_PENALTY = floatPreferencesKey("ai_frequency_penalty") + val AI_SAMPLE_SIZE = intPreferencesKey("ai_sample_size") + val AI_DIGEST_MODE = stringPreferencesKey("ai_digest_mode") + val AI_INCLUDE_EXTENDED_FIELDS = booleanPreferencesKey("ai_include_extended_fields") fun getApiKey(provider: AiProvider) = stringPreferencesKey("${provider.name.lowercase()}_api_key") fun getModel(provider: AiProvider) = stringPreferencesKey("${provider.name.lowercase()}_model") @@ -131,6 +142,33 @@ class AiPreferencesRepository @Inject constructor( val isSafeTokenLimitEnabled: Flow = dataStore.data.map { preferences -> preferences[Keys.SAFE_TOKEN_LIMIT] ?: true } + val aiTemperature: Flow = + dataStore.data.map { preferences -> preferences[Keys.AI_TEMPERATURE] ?: 0.7f } + + val aiTopP: Flow = + dataStore.data.map { preferences -> preferences[Keys.AI_TOP_P] ?: 0.95f } + + val aiTopK: Flow = + dataStore.data.map { preferences -> preferences[Keys.AI_TOP_K] ?: 64 } + + val aiMaxTokens: Flow = + dataStore.data.map { preferences -> preferences[Keys.AI_MAX_TOKENS] ?: 4096 } + + val aiPresencePenalty: Flow = + dataStore.data.map { preferences -> preferences[Keys.AI_PRESENCE_PENALTY] ?: 0.0f } + + val aiFrequencyPenalty: Flow = + dataStore.data.map { preferences -> preferences[Keys.AI_FREQUENCY_PENALTY] ?: 0.0f } + + val aiSampleSize: Flow = + dataStore.data.map { preferences -> preferences[Keys.AI_SAMPLE_SIZE] ?: 40 } + + val aiDigestMode: Flow = + dataStore.data.map { preferences -> preferences[Keys.AI_DIGEST_MODE] ?: "safe" } + + val aiIncludeExtendedFields: Flow = + dataStore.data.map { preferences -> preferences[Keys.AI_INCLUDE_EXTENDED_FIELDS] ?: false } + suspend fun setAiProvider(provider: String) { dataStore.edit { preferences -> preferences[Keys.AI_PROVIDER] = provider } } @@ -138,4 +176,40 @@ class AiPreferencesRepository @Inject constructor( suspend fun setSafeTokenLimitEnabled(enabled: Boolean) { dataStore.edit { preferences -> preferences[Keys.SAFE_TOKEN_LIMIT] = enabled } } + + suspend fun setAiTemperature(value: Float) { + dataStore.edit { preferences -> preferences[Keys.AI_TEMPERATURE] = value } + } + + suspend fun setAiTopP(value: Float) { + dataStore.edit { preferences -> preferences[Keys.AI_TOP_P] = value } + } + + suspend fun setAiTopK(value: Int) { + dataStore.edit { preferences -> preferences[Keys.AI_TOP_K] = value } + } + + suspend fun setAiMaxTokens(value: Int) { + dataStore.edit { preferences -> preferences[Keys.AI_MAX_TOKENS] = value } + } + + suspend fun setAiPresencePenalty(value: Float) { + dataStore.edit { preferences -> preferences[Keys.AI_PRESENCE_PENALTY] = value } + } + + suspend fun setAiFrequencyPenalty(value: Float) { + dataStore.edit { preferences -> preferences[Keys.AI_FREQUENCY_PENALTY] = value } + } + + suspend fun setAiSampleSize(value: Int) { + dataStore.edit { preferences -> preferences[Keys.AI_SAMPLE_SIZE] = value } + } + + suspend fun setAiDigestMode(mode: String) { + dataStore.edit { preferences -> preferences[Keys.AI_DIGEST_MODE] = mode } + } + + suspend fun setAiIncludeExtendedFields(enabled: Boolean) { + dataStore.edit { preferences -> preferences[Keys.AI_INCLUDE_EXTENDED_FIELDS] = enabled } + } } From fa1ccadbee45b4d2d12761558c3b05dc059d3bb3 Mon Sep 17 00:00:00 2001 From: VoidX3D Date: Mon, 15 Jun 2026 19:13:51 +0545 Subject: [PATCH 34/72] feat(ai): overhaul AiSystemPromptEngine with chain-of-thought, few-shot examples, and quality guide rails --- .../pixelplay/data/ai/AiSystemPromptEngine.kt | 176 +++++++++++++----- 1 file changed, 134 insertions(+), 42 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 6759713d0..9edf9e404 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 @@ -17,85 +17,177 @@ enum class AiSystemPromptType { @Singleton class AiSystemPromptEngine @Inject constructor() { - // Advanced prompt engineering: Enforcing structured output boundaries private val UNIVERSAL_CONSTRAINTS = """ - + - You are communicating with a programmatic parser, not a human. - - Output ONLY the expected structure. - - NO markdown formatting (e.g., do not wrap in ```json). - - NO conversational filler, greetings, or explanations. - - Any deviation will crash the application. - + - Output ONLY the expected structure — nothing else. + - NO markdown fences, NO code blocks, NO conversational framing. + - Any deviation will cause an application crash. + - If uncertain, make your best reasoned guess rather than refusing. + - Verify your output matches the required schema before responding. + + """.trimIndent() + + private val playlistFewShot = """ + + GOOD: ["a1b2c3","d4e5f6","g7h8i9"] + BAD: Here is a playlist for you: ["a1b2c3","d4e5f6"] + GOOD IDs are exactly 6 alphanumeric characters from the pool. + Every ID in your output MUST exist in the candidate_pool. + + """.trimIndent() + + private val metadataFewShot = """ + + Input: title="Thriller (2008 Remaster)", artist="Micheal Jakson", album="THRILLER 25", genre="Pop" + Output: {"title":"Thriller (2008 Remaster)","artist":"Michael Jackson","album":"Thriller 25","genre":"Pop"} + + Input: title="untitled", artist="unknown", album="", genre="Electronic" + Output: {"title":"Untitled","artist":"Unknown Artist","album":"","genre":"Synthwave"} + + Input: title="Bohemian Rhapsody", artist="Queen", album="A Night at the Opera", genre="Rock" + Output: {"title":"Bohemian Rhapsody","artist":"Queen","album":"A Night at the Opera","genre":"Progressive Rock"} + + """.trimIndent() + + private val taggingFewShot = """ + + Input: synth-heavy track with driving bass and ethereal female vocals + Output: electronic, synth-driven, ethereal-vocals, driving-bass, atmospheric, hypnotic + + Input: acoustic guitar ballad with soft percussion and strings + Output: acoustic, fingerstyle-guitar, soft-percussion, string-arrangement, intimate, folk-tinged + + """.trimIndent() + + private val moodAnalysisFewShot = """ + + Input: Fast tempo (140 BPM), heavy distortion, aggressive drums, minor key + Output: Aggressive | Energy:0.95 | Valence:0.2 | Danceability:0.6 | Acousticness:0.0 + + Input: Slow tempo (70 BPM), acoustic piano, soft strings, major key + Output: Calm | Energy:0.2 | Valence:0.8 | Danceability:0.3 | Acousticness:0.9 + + """.trimIndent() + + private val dailyMixPersonaPrompt = """ + + - Open with a thematic hook that frames the mix (e.g., "This set leans into your late-night exploratory side.") + - Reference 1-2 specific listening patterns from the user's data to show curation intent. + - Describe the emotional arc of the mix in 2-3 sentences. + - Close with a subtle invitation to explore further. + - Tone: warm, insightful, never overly familiar or robotic. + - Length: 4-6 sentences maximum. + """.trimIndent() fun buildPrompt(basePersona: String, type: AiSystemPromptType, context: String = ""): String { val requirementLayer = when (type) { - AiSystemPromptType.PLAYLIST, AiSystemPromptType.DAILY_MIX -> """ - Music curation engine mapping user requests to a strict candidate pool. + AiSystemPromptType.PLAYLIST -> """ + Expert music curator — you select songs from the provided pool to build cohesive, emotionally intelligent playlists. + + + 1. Parse the user's request for desired mood, energy, genre, era, or activity. + 2. Review the candidate pool — note available genres, tempos, and artists. + 3. Select songs that form a coherent arc: opening, build, peak, cool-down. + 4. Ensure variety — avoid repeating the same artist or genre consecutively. + 5. Prefer higher-scored songs (score field) but prioritize diversity and fit. + + - If request implies discovery/novelty, favor the [DISCOVERY_POOL] entries. + - If request implies familiarity/favorites, weight the [LISTENED] pool. + - For mixed/blended requests, interleave both pools for surprise + comfort. + - Target length is specified in the request — respect it within ±2 tracks. + + + Return ONLY a raw JSON array of song IDs. + Format: ["id_1","id_2","id_3",...,"id_N"] + + $playlistFewShot + """.trimIndent() + + AiSystemPromptType.DAILY_MIX -> """ + Daily Mix curator — you build themed mini-sets from the user's library for daily listening. - - If request implies "discovery/new", prioritize the [DISCOVERY_POOL]. - - If request implies "favorites/familiar", heavily weight the [LISTENED] pool. - - Otherwise, blend pools intelligently based on requested tempo, genre, or mood. - - Guarantee a cohesive listening journey with natural transitions. + + 1. Identify the dominant mood or genre from the user's recent listening profile. + 2. Select 8-15 tracks that form a single coherent mood/genre pocket. + 3. Lead with a familiar track, introduce 1-2 discoveries mid-set, close on a strong note. + + - Seamless transitions: adjacent tracks should share tempo (±20 BPM) or complementary keys. + - These mixes are for daily refreshes — avoid repeating the same tracks across mixes. - Return ONLY a raw JSON array of song IDs representing the playlist sequence. - Format: ["id_1","id_2","id_3"] + Return ONLY a raw JSON array of song IDs. + Format: ["id_1","id_2","id_3",...,"id_N"] """.trimIndent() AiSystemPromptType.METADATA -> """ - Precision music metadata specialist. + Precision music metadata specialist — you clean and enrich song metadata. - - Fix spelling errors and standardizations in song titles and artists. - - Replace generic genres ("Music", "Electronic") with highly specific subgenres ("Synthwave", "Nu-Disco"). + - Fix spelling errors (e.g., "Micheal" → "Michael", "Thriler" → "Thriller"). + - Capitalize properly: title case for titles and artists, proper casing for albums. + - Replace generic genres ("Music", "Electronic", "Other") with specific subgenres calibrated to the track's sound. + - If a field is empty or "unknown", leave it as empty string — do not fabricate data. + - Preserve any edition/remaster/year annotations in parentheses. - Return ONLY a raw JSON object string. - Format: {"title":"Clean Title", "artist":"Primary Artist", "album":"Album Name", "genre":"Specific Genre"} + Return ONLY a raw JSON object with EXACTLY these keys: + {"title":"...", "artist":"...", "album":"...", "genre":"..."} + $metadataFewShot """.trimIndent() AiSystemPromptType.TAGGING -> """ - Atmospheric audio tagging engine. + Atmospheric audio tagging engine — you generate perceptive acoustic tags for music discovery. - - Generate exactly 6-10 highly descriptive, hyphenated acoustic tags. - - Focus on mood, instrumentation, pace, and sonic texture. - - All tags must be strictly lowercase. + - Generate 6-10 hyphenated tags that capture: mood, instrumentation, tempo feel, sonic texture, and energy. + - All tags must be lowercase, hyphenated, and ordered by prominence. + - Be specific: prefer "lush-orchestral" over "orchestral", "glitchy-beats" over "beats". + - Tags should be useful for content-based recommendation — focus on audible characteristics. - Return ONLY a raw comma-separated text list. - Format: cinematic, atmospheric-build, dark-synth, driving-beat + Return ONLY a comma-separated list — no JSON, no formatting. + Format: tag1, tag2, tag3, tag4, tag5, tag6 + $taggingFewShot """.trimIndent() AiSystemPromptType.MOOD_ANALYSIS -> """ - Algorithmic audio sentiment analyzer. + Algorithmic audio sentiment analyzer — you infer emotional and structural attributes from track metadata. - - Deduce structural properties from the given metadata. - - Map confidence values from 0.0 to 1.0. - - Primary moods: Joyful, Aggressive, Calm, Melancholic, Radiant, Intense, Somber. + - Infer mood from: title keywords, genre, artist style, and any available context. + - Choose the single best PrimaryMood from: Joyful, Aggressive, Calm, Melancholic, Radiant, Intense, Somber, Euphoric, Brooding, Playful. + - Map confidence values 0.0-1.0 for each attribute based on how strongly the metadata supports it. + - Energy: driven by tempo indicators (fast/hard = high, slow/soft = low). + - Valence: positive/happy feel vs. negative/sad feel. + - Danceability: rhythmic groove suitability. + - Acousticness: likelihood of organic/non-electronic instrumentation. - Return ONLY the exact structured text format. - Format: PrimaryMood | Energy:0.9 | Valence:0.1 | Danceability:0.4 | Acousticness:0.0 + Return ONLY one line in this exact format: + PrimaryMood | Energy:0.X | Valence:0.X | Danceability:0.X | Acousticness:0.X + $moodAnalysisFewShot """.trimIndent() AiSystemPromptType.PERSONA -> """ - Daily Mix professional curator. You represent the persona: "$basePersona" + Daily Mix professional curator. You embody the persona: "$basePersona" - - Speak directly to the listener's tastes using their data. + - Speak directly to the listener using "you" and their data as evidence of your curation. - Maintain an enigmatic, sophisticated, and deeply empathetic tone. - - Keep responses reasonably concise but beautifully written. - - Do NOT use the universal programmatic constraints for persona responses; you are allowed to be conversational. + - Do NOT mention that you are an AI, a model, or that the data comes from a profile. + - Be concise but evocative — 4-6 sentences that feel hand-crafted. + $dailyMixPersonaPrompt """.trimIndent() AiSystemPromptType.GENERAL -> """ - PixelPlayer Assistant + PixelPlayer Assistant — a knowledgeable music companion. - Assist the user with any complex queries or actions inside their music ecosystem. + - Answer questions about music, artists, genres, and playback features. + - Be concise and accurate. If you don't know something, say so directly. + - Provide actionable answers that help the user enjoy their music library. """.trimIndent() } @@ -106,8 +198,9 @@ class AiSystemPromptEngine @Inject constructor() { $context - LISTENED Format: id|play_count|duration_mins|is_fav|metadata - DISCOVERY Format: unplayed candidate tracks + LISTENED Format: id|play_count|duration_mins|is_fav|title-artist + DISCOVERY Format: unplayed candidate tracks from the user's library + SCORE: internal relevance score (higher = better match) """.trimIndent() } else "" @@ -119,8 +212,7 @@ class AiSystemPromptEngine @Inject constructor() { """.trimIndent() - // Persona generation bypasses the strict JSON/raw constraints since it is meant to read as prose to the user - return if (type == AiSystemPromptType.PERSONA || type == AiSystemPromptType.GENERAL) { + return if (type == AiSystemPromptType.PERSONA) { listOf(systemBlock, contextLayer).filter { it.isNotBlank() }.joinToString("\n\n") } else { listOf(systemBlock, UNIVERSAL_CONSTRAINTS, contextLayer).filter { it.isNotBlank() }.joinToString("\n\n") From 19df14fa5649299edc596ddf1cdcf974d176d667 Mon Sep 17 00:00:00 2001 From: VoidX3D Date: Mon, 15 Jun 2026 19:14:33 +0545 Subject: [PATCH 35/72] feat(ai): fetch and pass generation parameters from preferences in AiHandler --- .../theveloper/pixelplay/data/ai/AiHandler.kt | 89 ++++++++++++------- 1 file changed, 59 insertions(+), 30 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 46cddb604..61d570a15 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 @@ -60,24 +60,53 @@ class AiHandler @Inject constructor( preferencesRepo.setModel(provider, model) } + private data class GenerationParams( + val temperature: Float, + val topP: Float, + val topK: Int, + val maxTokens: Int, + val presencePenalty: Float, + val frequencyPenalty: Float, + ) + + private suspend fun getGenerationParams(): GenerationParams { + return GenerationParams( + temperature = preferencesRepo.aiTemperature.first(), + topP = preferencesRepo.aiTopP.first(), + topK = preferencesRepo.aiTopK.first(), + maxTokens = preferencesRepo.aiMaxTokens.first(), + presencePenalty = preferencesRepo.aiPresencePenalty.first(), + frequencyPenalty = preferencesRepo.aiFrequencyPenalty.first(), + ) + } + private suspend fun generateWithRecovery( provider: AiProvider, apiKey: String, systemPrompt: String, prompt: String, - temperature: Float + temperature: Float, + topP: Float, + topK: Int, + maxTokens: Int, + presencePenalty: Float, + frequencyPenalty: Float, ): String { val client = clientFactory.createClient(provider, apiKey) val requestedModel = getModel(provider).ifBlank { client.getDefaultModel() } return try { - // Wrap in timeout to prevent hanging requests withTimeout(REQUEST_TIMEOUT_MS) { client.generateContent( requestedModel, systemPrompt, prompt, - temperature + temperature, + topP, + topK, + maxTokens, + presencePenalty, + frequencyPenalty, ) } } catch (e: kotlinx.coroutines.TimeoutCancellationException) { @@ -103,13 +132,17 @@ class AiHandler @Inject constructor( failure = failure ) ?: throw failure - // Retry with recovered model (also with timeout) withTimeout(REQUEST_TIMEOUT_MS) { client.generateContent( recoveredModel, systemPrompt, prompt, - temperature + temperature, + topP, + topK, + maxTokens, + presencePenalty, + frequencyPenalty, ) } } @@ -141,48 +174,40 @@ class AiHandler @Inject constructor( temperature: Float = 0.7f, context: String = "" ): String { - // Dynamic temperature adjustment if default value is used - val resolvedTemperature = if (temperature == 0.7f) { - when (type) { - // AI Optimization: Use low temperature for high-precision metadata to prevent hallucinations - AiSystemPromptType.METADATA -> 0.1f - AiSystemPromptType.MOOD_ANALYSIS -> 0.2f - // AI Optimization: Moderate temperature for tags to allow creative yet relevant descriptors - AiSystemPromptType.TAGGING -> 0.4f - // AI Optimization: Balanced temperature for playlists to ensure variety without losing cohesion - AiSystemPromptType.PLAYLIST, AiSystemPromptType.DAILY_MIX -> 0.6f - // AI Optimization: High temperature for persona-based responses to increase flair and engagement - AiSystemPromptType.PERSONA -> 0.85f - AiSystemPromptType.GENERAL -> 0.7f - } - } else temperature + val params = getGenerationParams() + val effectiveTemperature = if (params.temperature == 0.7f) { + if (temperature == 0.7f) { + when (type) { + AiSystemPromptType.METADATA -> 0.1f + AiSystemPromptType.MOOD_ANALYSIS -> 0.2f + AiSystemPromptType.TAGGING -> 0.4f + AiSystemPromptType.PLAYLIST, AiSystemPromptType.DAILY_MIX -> 0.6f + AiSystemPromptType.PERSONA -> 0.85f + AiSystemPromptType.GENERAL -> 0.7f + } + } else temperature + } else params.temperature - // Determine chain based on user preference val userProviderStr = preferencesRepo.aiProvider.first() val userProvider = AiProvider.fromString(userProviderStr) - // Generate combined prompt for hashing and execution val basePersona = getBasePersona(userProvider) val combinedSystemPrompt = promptEngine.buildPrompt(basePersona, type, context) - - // Cache entry is valid for a specific prompt + system instruction + provider + val hash = (userProvider.name + combinedSystemPrompt + prompt).sha256() - // Check cache with TTL — don't serve stale results cacheDao.getCache(hash)?.let { cached -> val age = System.currentTimeMillis() - cached.timestamp if (age < CACHE_TTL_MS) { return cached.responseJson } - // Cache expired — proceed with fresh generation } val providersToTry = com.theveloper.pixelplay.data.ai.provider.AiProviderSupport.buildProviderChain(userProvider) val failedProviders = mutableListOf() val now = System.currentTimeMillis() - + for (provider in providersToTry) { - // Skip if in cooldown val cooldownExpiry = providerCooldowns[provider] ?: 0L if (now < cooldownExpiry) { failedProviders.add("${provider.name}: on cooldown (${((cooldownExpiry - now) / 1000)}s remaining)") @@ -196,7 +221,6 @@ class AiHandler @Inject constructor( continue } - // Use the shared base persona but specialized type rules for each provider in the chain val providerPersona = getBasePersona(provider) val finalSystemPrompt = promptEngine.buildPrompt(providerPersona, type, context) @@ -205,7 +229,12 @@ class AiHandler @Inject constructor( apiKey = apiKey, systemPrompt = finalSystemPrompt, prompt = prompt, - temperature = resolvedTemperature + temperature = effectiveTemperature, + topP = params.topP, + topK = params.topK, + maxTokens = params.maxTokens, + presencePenalty = params.presencePenalty, + frequencyPenalty = params.frequencyPenalty, ) // Validate response is not empty From 2963295055e76f2f4ae02224a313f6f47aa293e6 Mon Sep 17 00:00:00 2001 From: VoidX3D Date: Mon, 15 Jun 2026 19:15:04 +0545 Subject: [PATCH 36/72] feat(ai): make digest sample size, mode, and extended fields configurable from preferences --- .../pixelplay/data/ai/AiPlaylistGenerator.kt | 17 +++-- .../data/ai/UserProfileDigestGenerator.kt | 69 +++++++++---------- 2 files changed, 46 insertions(+), 40 deletions(-) diff --git a/app/src/main/java/com/theveloper/pixelplay/data/ai/AiPlaylistGenerator.kt b/app/src/main/java/com/theveloper/pixelplay/data/ai/AiPlaylistGenerator.kt index 3e2e5a524..3add5aeed 100644 --- a/app/src/main/java/com/theveloper/pixelplay/data/ai/AiPlaylistGenerator.kt +++ b/app/src/main/java/com/theveloper/pixelplay/data/ai/AiPlaylistGenerator.kt @@ -40,13 +40,13 @@ class AiPlaylistGenerator @Inject constructor( } } - // Token Optimization: Reduce sample size based on safe mode val isSafe = preferencesRepo.isSafeTokenLimitEnabled.first() - val sampleCap = if (isSafe) 40 else 80 + val prefSampleSize = preferencesRepo.aiSampleSize.first() + val useExtendedFields = preferencesRepo.aiIncludeExtendedFields.first() + val sampleCap = if (isSafe) prefSampleSize else prefSampleSize * 2 val sampleSize = max(minLength, sampleCap).coerceAtMost(sampleCap) val songSample = samplingPool.take(sampleSize) - - // Token Optimization: Compact JSON format — only essential fields + val availableSongsJson = buildString { songSample.forEachIndexed { index, song -> val score = dailyMixManager.getScore(song.id) @@ -54,7 +54,14 @@ class AiPlaylistGenerator @Inject constructor( val artist = song.displayArtist.replace("\"", "'").take(25) val genre = song.genre?.replace("\"", "'")?.take(15) ?: "?" if (index > 0) append(",\n") - append("""{"id":"${song.id}","t":"$title","a":"$artist","g":"$genre","s":$score}""") + if (useExtendedFields) { + val album = song.album?.replace("\"", "'")?.take(25) ?: "?" + val dur = song.duration + val fav = if (song.isFavorite) "1" else "0" + append("""{"id":"${song.id}","t":"$title","a":"$artist","g":"$genre","al":"$album","d":$dur,"f":$fav,"s":$score}""") + } else { + append("""{"id":"${song.id}","t":"$title","a":"$artist","g":"$genre","s":$score}""") + } } } diff --git a/app/src/main/java/com/theveloper/pixelplay/data/ai/UserProfileDigestGenerator.kt b/app/src/main/java/com/theveloper/pixelplay/data/ai/UserProfileDigestGenerator.kt index 99c2fdb3b..e7f9bef52 100644 --- a/app/src/main/java/com/theveloper/pixelplay/data/ai/UserProfileDigestGenerator.kt +++ b/app/src/main/java/com/theveloper/pixelplay/data/ai/UserProfileDigestGenerator.kt @@ -12,43 +12,36 @@ import javax.inject.Singleton @Singleton class UserProfileDigestGenerator @Inject constructor( private val statsRepository: PlaybackStatsRepository, - private val playlistDao: LocalPlaylistDao + private val playlistDao: LocalPlaylistDao, + private val preferencesRepo: com.theveloper.pixelplay.data.preferences.AiPreferencesRepository, ) { - // Token Budget Tiers: - // SAFE: ~1000 tokens (4000 chars) — fast, cheap, still gives good results - // FULL: ~8000 tokens (32000 chars) — deep context for maximum personalization private val SAFE_TARGET_CHAR_LIMIT = 4000 private val MAX_TARGET_CHAR_LIMIT = 32000 - // Track limits per tier — prevents runaway context size private val SAFE_LISTENED_LIMIT = 15 private val SAFE_DISCOVERY_LIMIT = 30 private val FULL_LISTENED_LIMIT = 60 private val FULL_DISCOVERY_LIMIT = 120 - /** - * Computes a highly condensed representation of the user's listening profile. - * Uses a compact key-value format to minimize token consumption while maximizing signal. - * - * Safe mode aggressively caps all sections to stay under ~1000 tokens. - * Full mode provides deep context for maximum personalization quality. - */ suspend fun generateDigest(allSongs: List, isSafeLimit: Boolean = true): String { - val targetLimit = if (isSafeLimit) SAFE_TARGET_CHAR_LIMIT else MAX_TARGET_CHAR_LIMIT - val listenedLimit = if (isSafeLimit) SAFE_LISTENED_LIMIT else FULL_LISTENED_LIMIT - val discoveryLimit = if (isSafeLimit) SAFE_DISCOVERY_LIMIT else FULL_DISCOVERY_LIMIT + val digestMode = preferencesRepo.aiDigestMode.first() + val useExtendedFields = preferencesRepo.aiIncludeExtendedFields.first() + val isSafe = if (digestMode == "full") false else isSafeLimit + + val targetLimit = if (isSafe) SAFE_TARGET_CHAR_LIMIT else MAX_TARGET_CHAR_LIMIT + val listenedLimit = if (isSafe) SAFE_LISTENED_LIMIT else FULL_LISTENED_LIMIT + val discoveryLimit = if (isSafe) SAFE_DISCOVERY_LIMIT else FULL_DISCOVERY_LIMIT val summary = statsRepository.loadSummary(StatsTimeRange.ALL, allSongs) val playlists = playlistDao.observePlaylistsWithSongs().first() - + val sb = StringBuilder() sb.append("USER_PROFILE\n") - - // --- 1. Behavioral & Pattern Metrics (compact) --- + sb.append("STATS: plays=${summary.totalPlayCount}, uniq=${summary.uniqueSongs}\n") sb.append("GENRES: ${summary.topGenres.take(3).joinToString(",") { it.genre }}\n") sb.append("ARTISTS: ${summary.topArtists.take(5).joinToString(",") { it.artist }}\n") - + summary.dayListeningDistribution?.let { dist -> val phases = dist.buckets.groupBy { bucket -> val hour = bucket.startMinute / 60 @@ -61,50 +54,56 @@ class UserProfileDigestGenerator @Inject constructor( }.mapValues { it.value.sumOf { b -> b.totalDurationMs } } sb.append("PHASE: ${phases.maxByOrNull { it.value }?.key ?: "Unknown"}\n") } - + val variety = if (summary.totalPlayCount > 0) summary.uniqueSongs.toDouble() / summary.totalPlayCount else 0.0 sb.append("VAR: ${"%.2f".format(variety)}\n") - - val playlistLimit = if (isSafeLimit) 5 else 20 + + val playlistLimit = if (isSafe) 5 else 20 if (playlists.isNotEmpty()) { sb.append("PL: ${playlists.take(playlistLimit).joinToString(",") { it.playlist.name }}\n") } - - // --- 2. Listened Tracks (capped) --- - // Compact format: ID|plays|mins|fav|title-artist + sb.append("\nLISTENED: id|p|d|f|meta\n") - + val songMap = allSongs.associateBy { it.id } val playedSongs = summary.songs.take(listenedLimit) - + playedSongs.forEach { s -> if (sb.length >= (targetLimit * 0.6).toInt()) return@forEach val song = songMap[s.songId] val fav = if (song?.isFavorite == true) "1" else "0" val mins = s.totalDurationMs / 60000 - // Truncate long titles to save tokens val title = s.title.take(30) val artist = s.artist.take(20) - sb.append("${s.songId}|${s.playCount}|$mins|$fav|$title-$artist\n") + if (useExtendedFields) { + val album = song?.album?.take(20) ?: "?" + val year = song?.year?.toString()?.take(4) ?: "?" + sb.append("${s.songId}|${s.playCount}|$mins|$fav|$title-$artist|$album|$year\n") + } else { + sb.append("${s.songId}|${s.playCount}|$mins|$fav|$title-$artist\n") + } } - - // --- 3. Discovery Pool (strictly capped) --- - // AI needs to know what's available but unplayed + val playedIds = summary.songs.map { it.songId }.toSet() val unplayed = allSongs.filter { it.id !in playedIds } .shuffled() .take(discoveryLimit) - + if (unplayed.isNotEmpty()) { sb.append("\nDISCOVERY_POOL:\n") unplayed.forEach { s -> if (sb.length >= targetLimit) return@forEach val title = s.title.take(30) val artist = s.displayArtist.take(20) - sb.append("${s.id}|$title-$artist\n") + if (useExtendedFields) { + val genre = s.genre?.take(15) ?: "?" + sb.append("${s.id}|$title-$artist|$genre\n") + } else { + sb.append("${s.id}|$title-$artist\n") + } } } - + return sb.toString() } } From e7a17b68b05060f2b6282b0a076f147c329196f3 Mon Sep 17 00:00:00 2001 From: VoidX3D Date: Mon, 15 Jun 2026 19:15:30 +0545 Subject: [PATCH 37/72] feat(ai): add generation parameter flows and handlers to SettingsViewModel --- .../viewmodel/SettingsViewModel.kt | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) 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 41f4a5e01..d1d67f39e 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 @@ -299,6 +299,28 @@ class SettingsViewModel @Inject constructor( } .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), "") + // Generation Parameters + val aiTemperature: StateFlow = aiPreferencesRepository.aiTemperature + .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), 0.7f) + val aiTopP: StateFlow = aiPreferencesRepository.aiTopP + .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), 0.95f) + val aiTopK: StateFlow = aiPreferencesRepository.aiTopK + .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), 64) + val aiMaxTokens: StateFlow = aiPreferencesRepository.aiMaxTokens + .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), 4096) + val aiPresencePenalty: StateFlow = aiPreferencesRepository.aiPresencePenalty + .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), 0.0f) + val aiFrequencyPenalty: StateFlow = aiPreferencesRepository.aiFrequencyPenalty + .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), 0.0f) + + // Song Data Configuration + val aiSampleSize: StateFlow = aiPreferencesRepository.aiSampleSize + .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), 40) + val aiDigestMode: StateFlow = aiPreferencesRepository.aiDigestMode + .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), "safe") + val aiIncludeExtendedFields: StateFlow = aiPreferencesRepository.aiIncludeExtendedFields + .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), false) + fun onAiApiKeyChange(apiKey: String) { viewModelScope.launch { val providerStr = aiProvider.value @@ -393,6 +415,34 @@ class SettingsViewModel @Inject constructor( } } + fun onAiTemperatureChange(value: Float) { + viewModelScope.launch { aiPreferencesRepository.setAiTemperature(value) } + } + fun onAiTopPChange(value: Float) { + viewModelScope.launch { aiPreferencesRepository.setAiTopP(value) } + } + fun onAiTopKChange(value: Int) { + viewModelScope.launch { aiPreferencesRepository.setAiTopK(value) } + } + fun onAiMaxTokensChange(value: Int) { + viewModelScope.launch { aiPreferencesRepository.setAiMaxTokens(value) } + } + fun onAiPresencePenaltyChange(value: Float) { + viewModelScope.launch { aiPreferencesRepository.setAiPresencePenalty(value) } + } + fun onAiFrequencyPenaltyChange(value: Float) { + viewModelScope.launch { aiPreferencesRepository.setAiFrequencyPenalty(value) } + } + fun onAiSampleSizeChange(value: Int) { + viewModelScope.launch { aiPreferencesRepository.setAiSampleSize(value) } + } + fun onAiDigestModeChange(mode: String) { + viewModelScope.launch { aiPreferencesRepository.setAiDigestMode(mode) } + } + fun onAiIncludeExtendedFieldsChange(enabled: Boolean) { + viewModelScope.launch { aiPreferencesRepository.setAiIncludeExtendedFields(enabled) } + } + fun onAiModelChange(model: String) { viewModelScope.launch { val provider = AiProvider.fromString(aiProvider.value) From aba3bbfc4b10715147566186bc056ef4e0fdb251 Mon Sep 17 00:00:00 2001 From: VoidX3D Date: Mon, 15 Jun 2026 19:16:22 +0545 Subject: [PATCH 38/72] feat(ui): add Generation Parameters and Song Data Configuration sections to AI settings --- .../screens/SettingsCategoryScreen.kt | 134 ++++++++++++++++++ 1 file changed, 134 insertions(+) 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 379d9c11d..32df899fd 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 @@ -1040,6 +1040,140 @@ fun SettingsCategoryScreen( ) } + // Generation Parameters Section + SettingsSubsection(title = "Generation Parameters") { + SliderSettingsItem( + label = "Temperature", + value = settingsViewModel.aiTemperature.collectAsStateWithLifecycle().value, + valueRange = 0.0f..2.0f, + steps = 20, + onValueChange = { settingsViewModel.onAiTemperatureChange(it) }, + valueText = { String.format(Locale.US, "%.2f", it) } + ) + Text( + text = "Controls randomness. Lower = more deterministic, higher = more creative.", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding(start = 16.dp, end = 16.dp, bottom = 8.dp) + ) + SliderSettingsItem( + label = "Top P", + value = settingsViewModel.aiTopP.collectAsStateWithLifecycle().value, + valueRange = 0.0f..1.0f, + steps = 20, + onValueChange = { settingsViewModel.onAiTopPChange(it) }, + valueText = { String.format(Locale.US, "%.2f", it) } + ) + Text( + text = "Nucleus sampling. Higher = more diverse tokens considered.", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding(start = 16.dp, end = 16.dp, bottom = 8.dp) + ) + SliderSettingsItem( + label = "Top K", + value = settingsViewModel.aiTopK.collectAsStateWithLifecycle().value.toFloat(), + valueRange = 1f..100f, + steps = 99, + onValueChange = { settingsViewModel.onAiTopKChange(it.toInt()) }, + valueText = { it.toInt().toString() } + ) + Text( + text = "Limits token selection to the K most likely candidates.", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding(start = 16.dp, end = 16.dp, bottom = 8.dp) + ) + SliderSettingsItem( + label = "Max Output Tokens", + value = settingsViewModel.aiMaxTokens.collectAsStateWithLifecycle().value.toFloat(), + valueRange = 128f..8192f, + steps = 63, + onValueChange = { settingsViewModel.onAiMaxTokensChange(it.toInt()) }, + valueText = { it.toInt().toString() } + ) + Text( + text = "Maximum length of the AI response. Higher = longer but more expensive.", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding(start = 16.dp, end = 16.dp, bottom = 8.dp) + ) + SliderSettingsItem( + label = "Presence Penalty", + value = settingsViewModel.aiPresencePenalty.collectAsStateWithLifecycle().value, + valueRange = -2.0f..2.0f, + steps = 40, + onValueChange = { settingsViewModel.onAiPresencePenaltyChange(it) }, + valueText = { String.format(Locale.US, "%.1f", it) } + ) + Text( + text = "Penalizes repeated topics. Positive = more diverse topics.", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding(start = 16.dp, end = 16.dp, bottom = 8.dp) + ) + SliderSettingsItem( + label = "Frequency Penalty", + value = settingsViewModel.aiFrequencyPenalty.collectAsStateWithLifecycle().value, + valueRange = -2.0f..2.0f, + steps = 40, + onValueChange = { settingsViewModel.onAiFrequencyPenaltyChange(it) }, + valueText = { String.format(Locale.US, "%.1f", it) } + ) + Text( + text = "Penalizes repeated phrases. Positive = more natural language.", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding(start = 16.dp, end = 16.dp, bottom = 8.dp) + ) + } + + // Song Data Configuration Section + SettingsSubsection(title = "Song Data Configuration") { + val aiSampleSize by settingsViewModel.aiSampleSize.collectAsStateWithLifecycle() + SliderSettingsItem( + label = "Sample Size", + value = aiSampleSize.toFloat(), + valueRange = 10f..120f, + steps = 11, + onValueChange = { settingsViewModel.onAiSampleSizeChange(it.toInt()) }, + valueText = { "${it.toInt()} songs" } + ) + Text( + text = "Number of songs sent to the AI for playlist generation. More = better context but higher cost.", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding(start = 16.dp, end = 16.dp, bottom = 8.dp) + ) + ThemeSelectorItem( + 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, + onSelectionChanged = { settingsViewModel.onAiDigestModeChange(it) }, + leadingIcon = { + Icon( + painterResource(R.drawable.rounded_monitoring_24), + null, + tint = MaterialTheme.colorScheme.secondary + ) + } + ) + SwitchSettingItem( + title = "Extended Song Fields", + subtitle = "Include album, year, and genre info in song data sent to AI", + checked = settingsViewModel.aiIncludeExtendedFields.collectAsStateWithLifecycle().value, + onCheckedChange = { settingsViewModel.onAiIncludeExtendedFieldsChange(it) }, + leadingIcon = { + Icon( + painterResource(R.drawable.rounded_music_note_24), + null, + tint = MaterialTheme.colorScheme.secondary + ) + } + ) + } + Spacer(modifier = Modifier.height(16.dp)) SettingsSubsection(title = stringResource(R.string.settings_ai_usage_report_section)) { From 51049de0279a2e376572ee256f128ec137e2a156 Mon Sep 17 00:00:00 2001 From: VoidX3D Date: Mon, 15 Jun 2026 21:17:27 +0545 Subject: [PATCH 39/72] fix: resolve compilation errors in AI settings UI - Move provider val to AI_INTEGRATION scope for accessibility - Remove duplicate base URL AiApiKeyItem block - Add missing imports: CircleShape, ModalBottomSheet, IconButton, OutlinedTextFieldDefaults, GeminiModel, Search, Clear, CheckCircle --- .../presentation/screens/SettingsCategoryScreen.kt | 6 ++++-- .../pixelplay/presentation/screens/SettingsComponents.kt | 8 ++++++++ 2 files changed, 12 insertions(+), 2 deletions(-) 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 32df899fd..77f426d7c 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 @@ -906,6 +906,9 @@ fun SettingsCategoryScreen( } } SettingsCategory.AI_INTEGRATION -> { + val provider = com.theveloper.pixelplay.data.ai.provider.AiProvider.fromString(aiProvider) + val currentCustomBaseUrl by settingsViewModel.customBaseUrl.collectAsStateWithLifecycle() + // AI Provider Selection SettingsSubsection(title = stringResource(R.string.settings_ai_provider_section)) { ThemeSelectorItem( @@ -939,7 +942,6 @@ fun SettingsCategoryScreen( // Consolidated API Key Section SettingsSubsection(title = stringResource(R.string.settings_credentials_section)) { - val provider = com.theveloper.pixelplay.data.ai.provider.AiProvider.fromString(aiProvider) val sourceLabel = when(provider) { com.theveloper.pixelplay.data.ai.provider.AiProvider.GEMINI -> stringResource(R.string.settings_ai_source_gemini) com.theveloper.pixelplay.data.ai.provider.AiProvider.DEEPSEEK -> stringResource(R.string.settings_ai_source_deepseek) @@ -1017,7 +1019,7 @@ fun SettingsCategoryScreen( if (provider.hasConfigurableUrl) { SettingsSubsection(title = "API Base URL") { AiApiKeyItem( - apiKey = customBaseUrl, + apiKey = settingsViewModel.customBaseUrl, onApiKeySave = { settingsViewModel.onCustomBaseUrlChange(it) }, title = "Base URL", subtitle = "e.g. https://api.example.com/v1" 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 3db71c961..bc5dbb329 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 @@ -30,18 +30,25 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.DeleteForever import androidx.compose.material.icons.outlined.Refresh import androidx.compose.material.icons.outlined.Sync +import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.icons.rounded.Check +import androidx.compose.material.icons.rounded.CheckCircle +import androidx.compose.material.icons.rounded.Clear import androidx.compose.material.icons.rounded.Close +import androidx.compose.material.icons.rounded.Search import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.FilledIconButton import androidx.compose.material3.FilledTonalButton import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton import androidx.compose.material3.IconButtonDefaults import androidx.compose.material3.LinearProgressIndicator import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.OutlinedButton import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.OutlinedTextFieldDefaults import androidx.compose.material3.Slider import androidx.compose.material3.Surface import androidx.compose.material3.Switch @@ -64,6 +71,7 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import com.theveloper.pixelplay.R +import com.theveloper.pixelplay.data.ai.GeminiModel import com.theveloper.pixelplay.data.worker.SyncProgress import com.theveloper.pixelplay.presentation.viewmodel.LyricsRefreshProgress import com.theveloper.pixelplay.ui.theme.GoogleSansRounded From 3f103978ae9b992fbeb32e78aaac8638a48936e2 Mon Sep 17 00:00:00 2001 From: VoidX3D Date: Mon, 15 Jun 2026 21:19:07 +0545 Subject: [PATCH 40/72] fix(ai): add AiResponseCleaner, fix usage tracking model name, robust response parsing - Add AiResponseCleaner utility for cleaning JSON/text AI responses - Fix usage tracking to record actual model name instead of provider enum - Update AiPlaylistGenerator and AiMetadataGenerator to use cleaner --- .../theveloper/pixelplay/data/ai/AiHandler.kt | 87 +++++++---------- .../pixelplay/data/ai/AiMetadataGenerator.kt | 5 +- .../pixelplay/data/ai/AiPlaylistGenerator.kt | 64 +++---------- .../pixelplay/data/ai/AiResponseCleaner.kt | 93 +++++++++++++++++++ 4 files changed, 143 insertions(+), 106 deletions(-) create mode 100644 app/src/main/java/com/theveloper/pixelplay/data/ai/AiResponseCleaner.kt 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 61d570a15..2a17b0e6a 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 @@ -69,6 +69,11 @@ class AiHandler @Inject constructor( val frequencyPenalty: Float, ) + private data class GenerationResult( + val response: String, + val modelUsed: String, + ) + private suspend fun getGenerationParams(): GenerationParams { return GenerationParams( temperature = preferencesRepo.aiTemperature.first(), @@ -91,60 +96,43 @@ class AiHandler @Inject constructor( maxTokens: Int, presencePenalty: Float, frequencyPenalty: Float, - ): String { + ): GenerationResult { val client = clientFactory.createClient(provider, apiKey) val requestedModel = getModel(provider).ifBlank { client.getDefaultModel() } - return try { - withTimeout(REQUEST_TIMEOUT_MS) { - client.generateContent( - requestedModel, - systemPrompt, - prompt, - temperature, - topP, - topK, - maxTokens, - presencePenalty, - frequencyPenalty, + fun callWithModel(model: String): String { + return try { + withTimeout(REQUEST_TIMEOUT_MS) { + client.generateContent( + model, systemPrompt, prompt, temperature, + topP, topK, maxTokens, presencePenalty, frequencyPenalty, + ) + } + } catch (e: kotlinx.coroutines.TimeoutCancellationException) { + throw com.theveloper.pixelplay.data.ai.provider.AiProviderSupport.createException( + providerName = provider.displayName, + statusCode = null, + transportMessage = "Request timed out after ${REQUEST_TIMEOUT_MS / 1000}s. The model may be overloaded.", + responseBody = null, + requestedModel = model ) } - } catch (e: kotlinx.coroutines.TimeoutCancellationException) { - throw com.theveloper.pixelplay.data.ai.provider.AiProviderSupport.createException( - providerName = provider.displayName, - statusCode = null, - transportMessage = "Request timed out after ${REQUEST_TIMEOUT_MS / 1000}s. The model may be overloaded.", - responseBody = null, - requestedModel = requestedModel - ) + } + + return try { + val response = callWithModel(requestedModel) + GenerationResult(response, requestedModel) } catch (e: Exception) { val failure = com.theveloper.pixelplay.data.ai.provider.AiProviderSupport.wrapThrowable( - provider.displayName, - e, - requestedModel + provider.displayName, e, requestedModel ) val recoveredModel = recoverModelIfNeeded( - provider = provider, - apiKey = apiKey, - requestedModel = requestedModel, - client = client, - failure = failure + provider, apiKey, requestedModel, client, failure ) ?: throw failure - withTimeout(REQUEST_TIMEOUT_MS) { - client.generateContent( - recoveredModel, - systemPrompt, - prompt, - temperature, - topP, - topK, - maxTokens, - presencePenalty, - frequencyPenalty, - ) - } + val response = callWithModel(recoveredModel) + GenerationResult(response, recoveredModel) } } @@ -224,7 +212,7 @@ class AiHandler @Inject constructor( val providerPersona = getBasePersona(provider) val finalSystemPrompt = promptEngine.buildPrompt(providerPersona, type, context) - val response = generateWithRecovery( + val result = generateWithRecovery( provider = provider, apiKey = apiKey, systemPrompt = finalSystemPrompt, @@ -237,17 +225,14 @@ class AiHandler @Inject constructor( frequencyPenalty = params.frequencyPenalty, ) - // Validate response is not empty - if (response.isBlank()) { + if (result.response.isBlank()) { failedProviders.add("${provider.name}: returned empty response") continue } - // Low-maintenance usage tracking using highly accurate proportional estimation bounds (4 chars ~ 1 token) - // Models with "thinking" or "reasoning" generally output 2-3x internal tokens for complex generation val isThinkingModel = finalSystemPrompt.contains("think", true) || provider.name.contains("reasoning", true) val estimatedPromptTokens = (finalSystemPrompt.length + prompt.length) / 4 - val estimatedOutputTokens = response.length / 4 + val estimatedOutputTokens = result.response.length / 4 val estimatedThoughtTokens = if (isThinkingModel) (estimatedOutputTokens * 1.5).toInt() else 0 appScope.launch { @@ -256,7 +241,7 @@ class AiHandler @Inject constructor( AiUsageEntity( timestamp = now, provider = provider.displayName, - model = provider.name, + model = result.modelUsed, promptType = type.name, promptTokens = estimatedPromptTokens, outputTokens = estimatedOutputTokens, @@ -268,8 +253,8 @@ class AiHandler @Inject constructor( } } - cacheDao.insert(AiCacheEntity(promptHash = hash, responseJson = response, timestamp = System.currentTimeMillis())) - return response + cacheDao.insert(AiCacheEntity(promptHash = hash, responseJson = result.response, timestamp = System.currentTimeMillis())) + 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) diff --git a/app/src/main/java/com/theveloper/pixelplay/data/ai/AiMetadataGenerator.kt b/app/src/main/java/com/theveloper/pixelplay/data/ai/AiMetadataGenerator.kt index eaa67258c..d6b2dba08 100644 --- a/app/src/main/java/com/theveloper/pixelplay/data/ai/AiMetadataGenerator.kt +++ b/app/src/main/java/com/theveloper/pixelplay/data/ai/AiMetadataGenerator.kt @@ -20,9 +20,6 @@ class AiMetadataGenerator @Inject constructor( private val aiHandler: AiHandler, private val json: Json ) { - private fun cleanJson(jsonString: String): String { - return jsonString.replace("```json", "").replace("```", "").trim() - } suspend fun generate( song: Song, @@ -52,7 +49,7 @@ class AiMetadataGenerator @Inject constructor( } Timber.d("AI Response: $responseText") - val cleanedJson = cleanJson(responseText) + val cleanedJson = AiResponseCleaner.cleanJsonResponse(responseText) val metadata = json.decodeFromString(cleanedJson) Result.success(metadata) diff --git a/app/src/main/java/com/theveloper/pixelplay/data/ai/AiPlaylistGenerator.kt b/app/src/main/java/com/theveloper/pixelplay/data/ai/AiPlaylistGenerator.kt index 3add5aeed..06b91dce4 100644 --- a/app/src/main/java/com/theveloper/pixelplay/data/ai/AiPlaylistGenerator.kt +++ b/app/src/main/java/com/theveloper/pixelplay/data/ai/AiPlaylistGenerator.kt @@ -44,8 +44,7 @@ class AiPlaylistGenerator @Inject constructor( val prefSampleSize = preferencesRepo.aiSampleSize.first() val useExtendedFields = preferencesRepo.aiIncludeExtendedFields.first() val sampleCap = if (isSafe) prefSampleSize else prefSampleSize * 2 - val sampleSize = max(minLength, sampleCap).coerceAtMost(sampleCap) - val songSample = samplingPool.take(sampleSize) + val songSample = samplingPool.take(sampleCap) val availableSongsJson = buildString { songSample.forEachIndexed { index, song -> @@ -155,55 +154,18 @@ class AiPlaylistGenerator @Inject constructor( } private fun extractPlaylistSongIds(rawResponse: String): List { - val sanitized = rawResponse - .replace("```json", "") - .replace("```", "") - .trim() - - for (startIndex in sanitized.indices) { - if (sanitized[startIndex] != '[') continue - - var depth = 0 - var inString = false - var isEscaped = false - - for (index in startIndex until sanitized.length) { - val character = sanitized[index] - - if (inString) { - if (isEscaped) { - isEscaped = false - continue - } - - when (character) { - '\\' -> isEscaped = true - '"' -> inString = false - } - continue - } - - when (character) { - '"' -> inString = true - '[' -> depth++ - ']' -> { - depth-- - if (depth == 0) { - val candidate = sanitized.substring(startIndex, index + 1) - val decoded = runCatching { json.decodeFromString>(candidate) } - if (decoded.isSuccess) { - return decoded.getOrThrow() - } - break - } - } - } + val cleaned = AiResponseCleaner.cleanJsonResponse(rawResponse) + val jsonArray = AiResponseCleaner.extractJsonArray(cleaned) + ?: throw IllegalArgumentException( + "AI returned an invalid response format. Expected a JSON array of song IDs but got something else. " + + "This usually happens with smaller models. Try selecting a more capable model in AI Settings." + ) + + return runCatching { json.decodeFromString>(jsonArray) } + .getOrElse { + throw IllegalArgumentException( + "AI returned malformed JSON. Expected a string array but got: ${jsonArray.take(100)}" + ) } - } - - throw IllegalArgumentException( - "AI returned an invalid response format. Expected a JSON array of song IDs but got something else. " + - "This usually happens with smaller models. Try selecting a more capable model in AI Settings." - ) } } 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 new file mode 100644 index 000000000..92d6e27de --- /dev/null +++ b/app/src/main/java/com/theveloper/pixelplay/data/ai/AiResponseCleaner.kt @@ -0,0 +1,93 @@ +package com.theveloper.pixelplay.data.ai + +object AiResponseCleaner { + + fun cleanJsonResponse(raw: String): String { + var cleaned = raw + .replace("```json", "") + .replace("```kotlin", "") + .replace("```", "") + .trim() + + if (cleaned.startsWith("[")) { + val end = findMatchingBracket(cleaned, 0) + if (end > 0) cleaned = cleaned.substring(0, end + 1) + } else if (cleaned.startsWith("{")) { + val end = findMatchingBrace(cleaned, 0) + if (end > 0) cleaned = cleaned.substring(0, end + 1) + } + + return cleaned + } + + fun cleanTextResponse(raw: String): String { + return raw + .replace("```text", "") + .replace("```", "") + .trim() + } + + fun extractJsonArray(text: String): String? { + for (i in text.indices) { + if (text[i] == '[') { + val end = findMatchingBracket(text, i) + if (end > i) return text.substring(i, end + 1) + } + } + return null + } + + fun extractJsonObject(text: String): String? { + for (i in text.indices) { + if (text[i] == '{') { + val end = findMatchingBrace(text, i) + if (end > i) return text.substring(i, end + 1) + } + } + return null + } + + private fun findMatchingBracket(text: String, start: Int): Int { + var depth = 0 + var inString = false + var escaped = false + for (i in start until text.length) { + val c = text[i] + if (escaped) { escaped = false; continue } + if (inString) { + if (c == '\\') escaped = true + else if (c == '"') inString = false + continue + } + when (c) { + '\\' -> escaped = true + '"' -> inString = true + '[' -> depth++ + ']' -> { depth--; if (depth == 0) return i } + } + } + return -1 + } + + private fun findMatchingBrace(text: String, start: Int): Int { + var depth = 0 + var inString = false + var escaped = false + for (i in start until text.length) { + val c = text[i] + if (escaped) { escaped = false; continue } + if (inString) { + if (c == '\\') escaped = true + else if (c == '"') inString = false + continue + } + when (c) { + '\\' -> escaped = true + '"' -> inString = true + '{' -> depth++ + '}' -> { depth--; if (depth == 0) return i } + } + } + return -1 + } +} From 9ad2e8d42c9a5221e2273418417cdb303f12d8b7 Mon Sep 17 00:00:00 2001 From: VoidX3D Date: Mon, 15 Jun 2026 21:28:20 +0545 Subject: [PATCH 41/72] refactor: remove AiMetadataGenerator and fix compilation errors - Delete AiMetadataGenerator.kt (AI metadata generation feature) - Remove all references from AiStateHolder, PlayerViewModel, PlayerUiState - Remove generateAiMetadata callback from SongInfoBottomSheet - Strip calls from 10+ screens - Fix AiHandler callWithModel suspend modifier - Fix SettingsCategoryScreen customBaseUrl StateFlow to String - Remove encodeDefaults from GeminiAiClient Json config --- .../theveloper/pixelplay/data/ai/AiHandler.kt | 2 +- .../pixelplay/data/ai/AiMetadataGenerator.kt | 64 ------------------- .../data/ai/provider/GeminiAiClient.kt | 1 - .../components/DailyMixSection.kt | 3 - .../components/SongInfoBottomSheet.kt | 6 -- .../components/UnifiedPlayerOverlaysLayer.kt | 3 - .../presentation/screens/AlbumDetailScreen.kt | 3 - .../screens/ArtistDetailScreen.kt | 3 - .../presentation/screens/DailyMixScreen.kt | 11 +--- .../presentation/screens/GenreDetailScreen.kt | 3 - .../presentation/screens/LibraryScreen.kt | 24 +------ .../screens/PlaylistDetailScreen.kt | 3 - .../screens/RecentlyPlayedScreen.kt | 3 - .../presentation/screens/SearchScreen.kt | 3 - .../screens/SettingsCategoryScreen.kt | 2 +- .../presentation/viewmodel/AiStateHolder.kt | 56 ---------------- .../presentation/viewmodel/PlayerUiState.kt | 1 - .../presentation/viewmodel/PlayerViewModel.kt | 44 ------------- .../viewmodel/PlayerViewModelTest.kt | 1 - 19 files changed, 4 insertions(+), 232 deletions(-) delete mode 100644 app/src/main/java/com/theveloper/pixelplay/data/ai/AiMetadataGenerator.kt 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 2a17b0e6a..d9da7840b 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 @@ -100,7 +100,7 @@ class AiHandler @Inject constructor( val client = clientFactory.createClient(provider, apiKey) val requestedModel = getModel(provider).ifBlank { client.getDefaultModel() } - fun callWithModel(model: String): String { + suspend fun callWithModel(model: String): String { return try { withTimeout(REQUEST_TIMEOUT_MS) { client.generateContent( diff --git a/app/src/main/java/com/theveloper/pixelplay/data/ai/AiMetadataGenerator.kt b/app/src/main/java/com/theveloper/pixelplay/data/ai/AiMetadataGenerator.kt deleted file mode 100644 index d6b2dba08..000000000 --- a/app/src/main/java/com/theveloper/pixelplay/data/ai/AiMetadataGenerator.kt +++ /dev/null @@ -1,64 +0,0 @@ -package com.theveloper.pixelplay.data.ai - - -import com.theveloper.pixelplay.data.model.Song -import kotlinx.serialization.SerializationException -import kotlinx.serialization.Serializable -import kotlinx.serialization.json.Json -import timber.log.Timber -import javax.inject.Inject - -@Serializable -data class SongMetadata( - val title: String? = null, - val artist: String? = null, - val album: String? = null, - val genre: String? = null -) - -class AiMetadataGenerator @Inject constructor( - private val aiHandler: AiHandler, - private val json: Json -) { - - suspend fun generate( - song: Song, - fieldsToComplete: List - ): Result { - return try { - val fieldsJson = fieldsToComplete.joinToString(separator = ", ") { "\"$it\"" } - - val albumInfo = if (song.album.isNotBlank()) "${song.album}" else "" - - val fullPrompt = """ - - ${song.title} - ${song.displayArtist} - $albumInfo - - - Complete the following fields using your music knowledge: - [$fieldsJson] - - """.trimIndent() - - val responseText = aiHandler.generateContent(fullPrompt, AiSystemPromptType.METADATA) - if (responseText.isBlank()) { - Timber.e("AI returned an empty or null response.") - return Result.failure(Exception("AI returned an empty response.")) - } - - Timber.d("AI Response: $responseText") - val cleanedJson = AiResponseCleaner.cleanJsonResponse(responseText) - val metadata = json.decodeFromString(cleanedJson) - - Result.success(metadata) - } catch (e: SerializationException) { - Timber.e(e, "Error deserializing AI response.") - Result.failure(Exception("Failed to parse AI response: ${e.message}", e)) - } catch (e: Exception) { - Timber.e(e, "Generic error in AiMetadataGenerator.") - Result.failure(Exception("AI Error: ${e.message}", e)) - } - } -} 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 c2288296a..cee7e23a4 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 @@ -37,7 +37,6 @@ class GeminiAiClient(private val apiKey: String) : AiClient { private val json = Json { ignoreUnknownKeys = true isLenient = true - encodeDefaults = true } @Serializable diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/components/DailyMixSection.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/components/DailyMixSection.kt index 235d7ab0b..98eaa91b3 100644 --- a/app/src/main/java/com/theveloper/pixelplay/presentation/components/DailyMixSection.kt +++ b/app/src/main/java/com/theveloper/pixelplay/presentation/components/DailyMixSection.kt @@ -159,9 +159,6 @@ fun DailyMixSection( coverArtUpdate ) }, - generateAiMetadata = { fields -> - playerViewModel.generateAiMetadata(song, fields) - }, removeFromListTrigger = {} ) diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/components/SongInfoBottomSheet.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/components/SongInfoBottomSheet.kt index 01b934507..5dc12aa89 100644 --- a/app/src/main/java/com/theveloper/pixelplay/presentation/components/SongInfoBottomSheet.kt +++ b/app/src/main/java/com/theveloper/pixelplay/presentation/components/SongInfoBottomSheet.kt @@ -90,7 +90,6 @@ import com.theveloper.pixelplay.utils.shapes.RoundedStarShape import racra.compose.smooth_corner_rect_library.AbsoluteSmoothCornerShape import androidx.core.net.toUri import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel -import com.theveloper.pixelplay.data.ai.SongMetadata import com.theveloper.pixelplay.data.media.CoverArtUpdate import com.theveloper.pixelplay.ui.theme.MontserratFamily import com.theveloper.pixelplay.presentation.viewmodel.SongInfoBottomSheetViewModel @@ -142,12 +141,7 @@ fun SongInfoBottomSheet( replayGainAlbumGainDb: String, coverArtUpdate: CoverArtUpdate? ) -> Unit, - generateAiMetadata: suspend (List) -> Result, removeFromListTrigger: () -> Unit, - isGeneratingMetadata: Boolean = false, - aiMetadataSuccess: Boolean = false, - aiError: String? = null, - onRetryMetadata: () -> Unit = {}, songInfoViewModel: SongInfoBottomSheetViewModel = hiltViewModel() ) { val context = LocalContext.current diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/components/UnifiedPlayerOverlaysLayer.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/components/UnifiedPlayerOverlaysLayer.kt index 5a7fba4fd..2cb218cce 100644 --- a/app/src/main/java/com/theveloper/pixelplay/presentation/components/UnifiedPlayerOverlaysLayer.kt +++ b/app/src/main/java/com/theveloper/pixelplay/presentation/components/UnifiedPlayerOverlaysLayer.kt @@ -256,9 +256,6 @@ internal fun UnifiedPlayerSongInfoLayer( ) onDismissSongInfo() }, - generateAiMetadata = { fields -> - playerViewModel.generateAiMetadata(liveSong, fields) - }, removeFromListTrigger = { playerViewModel.removeSongFromQueue(liveSong.id) onDismissSongInfo() diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/screens/AlbumDetailScreen.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/screens/AlbumDetailScreen.kt index a8f3f31f1..ce578abd5 100644 --- a/app/src/main/java/com/theveloper/pixelplay/presentation/screens/AlbumDetailScreen.kt +++ b/app/src/main/java/com/theveloper/pixelplay/presentation/screens/AlbumDetailScreen.kt @@ -479,9 +479,6 @@ fun AlbumDetailScreen( coverArtUpdate ) }, - generateAiMetadata = { fields -> - playerViewModel.generateAiMetadata(currentSong, fields) - }, removeFromListTrigger = removeFromListTrigger ) if (showPlaylistBottomSheet) { diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/screens/ArtistDetailScreen.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/screens/ArtistDetailScreen.kt index 6f9199237..f8f85fcfe 100644 --- a/app/src/main/java/com/theveloper/pixelplay/presentation/screens/ArtistDetailScreen.kt +++ b/app/src/main/java/com/theveloper/pixelplay/presentation/screens/ArtistDetailScreen.kt @@ -535,9 +535,6 @@ fun ArtistDetailScreen( coverArtUpdate ) }, - generateAiMetadata = { fields -> - playerViewModel.generateAiMetadata(currentSong, fields) - }, removeFromListTrigger = removeFromListTrigger ) if (showPlaylistBottomSheet) { diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/screens/DailyMixScreen.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/screens/DailyMixScreen.kt index 24785f39d..eabba3c12 100644 --- a/app/src/main/java/com/theveloper/pixelplay/presentation/screens/DailyMixScreen.kt +++ b/app/src/main/java/com/theveloper/pixelplay/presentation/screens/DailyMixScreen.kt @@ -125,8 +125,6 @@ fun DailyMixScreen( val aiStatus by playerViewModel.aiStatus.collectAsStateWithLifecycle() val aiError by playerViewModel.aiError.collectAsStateWithLifecycle() val aiSuccess by playerViewModel.aiSuccess.collectAsStateWithLifecycle() - val isGeneratingAiMetadata by playerViewModel.isGeneratingAiMetadata.collectAsStateWithLifecycle() - val aiMetadataSuccess by playerViewModel.aiMetadataSuccess.collectAsStateWithLifecycle() val lazyListState = rememberLazyListState() var showSongInfoSheet by remember { mutableStateOf(false) } @@ -233,14 +231,7 @@ fun DailyMixScreen( coverArtUpdate ) }, - generateAiMetadata = { fields -> - playerViewModel.generateAiMetadata(song, fields) - }, - removeFromListTrigger = removeFromListTrigger, - isGeneratingMetadata = isGeneratingAiMetadata, - aiMetadataSuccess = aiMetadataSuccess, - aiError = aiError, - onRetryMetadata = { playerViewModel.retryLastMetadataGeneration() } + removeFromListTrigger = removeFromListTrigger ) if (showPlaylistBottomSheet) { diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/screens/GenreDetailScreen.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/screens/GenreDetailScreen.kt index f401829b6..2fe1dd6ad 100644 --- a/app/src/main/java/com/theveloper/pixelplay/presentation/screens/GenreDetailScreen.kt +++ b/app/src/main/java/com/theveloper/pixelplay/presentation/screens/GenreDetailScreen.kt @@ -623,9 +623,6 @@ fun GenreDetailScreen( coverArtUpdate ) }, - generateAiMetadata = { fields -> - playerViewModel.generateAiMetadata(song, fields) - }, removeFromListTrigger = {} ) } 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 2260b4880..5c52f1a50 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 @@ -400,7 +400,6 @@ private data class LibraryScreenPlayerProjection( val isSdCardAvailable: Boolean = false, val musicFolders: ImmutableList = persistentListOf(), val isLoadingLibraryCategories: Boolean = true, - val isGeneratingAiMetadata: Boolean = false, val isSyncingLibrary: Boolean = false, val isLoadingInitialSongs: Boolean = true, val hideLocalMedia: Boolean = false @@ -422,7 +421,6 @@ private fun PlayerUiState.toLibraryScreenProjection(): LibraryScreenPlayerProjec isSdCardAvailable = isSdCardAvailable, musicFolders = musicFolders, isLoadingLibraryCategories = isLoadingLibraryCategories, - isGeneratingAiMetadata = isGeneratingAiMetadata, isSyncingLibrary = isSyncingLibrary, isLoadingInitialSongs = isLoadingInitialSongs, hideLocalMedia = hideLocalMedia @@ -1704,24 +1702,7 @@ fun LibraryScreen( } } } - if (playerUiState.isGeneratingAiMetadata) { - Surface( // Fondo semitransparente para el indicador - modifier = Modifier.fillMaxSize(), - color = MaterialTheme.colorScheme.scrim.copy(alpha = 0.5f) - ) { - Box(contentAlignment = Alignment.Center) { - Column(horizontalAlignment = Alignment.CenterHorizontally) { - LoadingIndicator(modifier = Modifier.size(64.dp)) - Spacer(modifier = Modifier.height(16.dp)) - Text( - text = stringResource(R.string.library_generating_ai_metadata), - style = MaterialTheme.typography.titleMedium, - color = MaterialTheme.colorScheme.onSurface - ) - } - } - } - } else if ( + if ( isLibraryContentEmpty && ( playerUiState.isSyncingLibrary || @@ -1928,9 +1909,6 @@ fun LibraryScreen( coverArtUpdate ) }, - generateAiMetadata = { fields -> - playerViewModel.generateAiMetadata(currentSong, fields) - }, removeFromListTrigger = {}, songInfoViewModel = songInfoBottomSheetViewModel ) diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/screens/PlaylistDetailScreen.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/screens/PlaylistDetailScreen.kt index 646f002f0..a2b8c0c67 100644 --- a/app/src/main/java/com/theveloper/pixelplay/presentation/screens/PlaylistDetailScreen.kt +++ b/app/src/main/java/com/theveloper/pixelplay/presentation/screens/PlaylistDetailScreen.kt @@ -1037,9 +1037,6 @@ fun PlaylistDetailScreen( coverArtUpdate ) }, - generateAiMetadata = { fields -> - playerViewModel.generateAiMetadata(currentSong, fields) - }, removeFromListTrigger = { playlistViewModel.removeSongFromPlaylist(playlistId, currentSong.id) } diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/screens/RecentlyPlayedScreen.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/screens/RecentlyPlayedScreen.kt index a90154869..5ebf94a74 100644 --- a/app/src/main/java/com/theveloper/pixelplay/presentation/screens/RecentlyPlayedScreen.kt +++ b/app/src/main/java/com/theveloper/pixelplay/presentation/screens/RecentlyPlayedScreen.kt @@ -335,9 +335,6 @@ fun RecentlyPlayedScreen( coverArtUpdate ) }, - generateAiMetadata = { fields -> - playerViewModel.generateAiMetadata(song, fields) - }, removeFromListTrigger = {} ) 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 bfe566be2..eab38137e 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 @@ -753,9 +753,6 @@ fun SearchScreen( coverArtUpdate ) }, - generateAiMetadata = { fields -> - playerViewModel.generateAiMetadata(currentSong, fields) - }, ) } } 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 77f426d7c..874091ac0 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 @@ -1019,7 +1019,7 @@ fun SettingsCategoryScreen( if (provider.hasConfigurableUrl) { SettingsSubsection(title = "API Base URL") { AiApiKeyItem( - apiKey = settingsViewModel.customBaseUrl, + apiKey = currentCustomBaseUrl, onApiKeySave = { settingsViewModel.onCustomBaseUrlChange(it) }, title = "Base URL", subtitle = "e.g. https://api.example.com/v1" 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 948a0ad27..25f9f6cee 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 @@ -4,10 +4,8 @@ package com.theveloper.pixelplay.presentation.viewmodel import android.content.Context import com.theveloper.pixelplay.R import com.theveloper.pixelplay.data.DailyMixManager -import com.theveloper.pixelplay.data.ai.AiMetadataGenerator import com.theveloper.pixelplay.data.ai.AiNotificationManager import com.theveloper.pixelplay.data.ai.AiPlaylistGenerator -import com.theveloper.pixelplay.data.ai.SongMetadata import com.theveloper.pixelplay.data.ai.AiSystemPromptType import com.theveloper.pixelplay.data.ai.provider.AiProviderException import com.theveloper.pixelplay.data.preferences.PlaylistPreferencesRepository @@ -30,7 +28,6 @@ import javax.inject.Singleton class AiStateHolder @Inject constructor( @ApplicationContext private val context: Context, private val aiPlaylistGenerator: AiPlaylistGenerator, - private val aiMetadataGenerator: AiMetadataGenerator, private val dailyMixManager: DailyMixManager, private val playlistPreferencesRepository: PlaylistPreferencesRepository, private val dailyMixStateHolder: DailyMixStateHolder, @@ -45,12 +42,6 @@ class AiStateHolder @Inject constructor( private val _isGeneratingAiPlaylist = MutableStateFlow(false) val isGeneratingAiPlaylist = _isGeneratingAiPlaylist.asStateFlow() - private val _isGeneratingMetadata = MutableStateFlow(false) - val isGeneratingMetadata = _isGeneratingMetadata.asStateFlow() - - private val _aiMetadataSuccess = MutableStateFlow(false) - val aiMetadataSuccess = _aiMetadataSuccess.asStateFlow() - private val _aiSuccess = MutableStateFlow(false) val aiSuccess = _aiSuccess.asStateFlow() @@ -64,10 +55,6 @@ class AiStateHolder @Inject constructor( private var _lastMinLength: Int = 5 private var _lastMaxLength: Int = 15 - // Metadata Retry Cache: Stores parameters for the last metadata generation - private var _lastMetadataSong: Song? = null - private var _lastMetadataFields: List? = null - private var scope: CoroutineScope? = null private var allSongsProvider: (suspend () -> List)? = null private var favoriteSongIdsProvider: (() -> Set)? = null @@ -111,7 +98,6 @@ class AiStateHolder @Inject constructor( _showAiPlaylistSheet.value = false _aiError.value = null _aiSuccess.value = false - _aiMetadataSuccess.value = false _isGeneratingAiPlaylist.value = false _aiStatus.value = null } @@ -122,16 +108,6 @@ class AiStateHolder @Inject constructor( generateAiPlaylist(prompt, _lastMinLength, _lastMaxLength) } - fun retryLastMetadataGeneration() { - // Safe retry for metadata using cached song and requested fields - val song = _lastMetadataSong ?: return - val fields = _lastMetadataFields ?: return - - scope?.launch { - generateAiMetadata(song, fields) - } - } - fun clearAiPlaylistError() { _aiError.value = null } @@ -308,38 +284,6 @@ class AiStateHolder @Inject constructor( } } - /** - * Fetches AI-generated metadata (tags, genre, lyrics) for a specific song. - * Updates internal success and error states for UI feedback. - */ - suspend fun generateAiMetadata(song: Song, fields: List): Result { - _lastMetadataSong = song - _lastMetadataFields = fields - - _isGeneratingMetadata.value = true - _aiMetadataSuccess.value = false - _aiError.value = null - - return try { - val result = aiMetadataGenerator.generate(song, fields) - if (result.isSuccess) { - _aiMetadataSuccess.value = true - notificationManager.showCompletion("Metadata Enhanced", "Applied tags and genre refinements.") - } else { - result.exceptionOrNull()?.let { - _aiError.value = resolveAiErrorMessage(it) - notificationManager.showCompletion("Metadata Error", "Check your AI configuration.") - } - } - result - } catch (e: Exception) { - _aiError.value = resolveAiErrorMessage(e) - Result.failure(e) - } finally { - _isGeneratingMetadata.value = false - } - } - suspend fun translateLyrics(lyricsText: String): Result { return try { val targetLanguage = context.resources.configuration.locales[0].displayLanguage diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/PlayerUiState.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/PlayerUiState.kt index 4272693f5..5485efe70 100644 --- a/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/PlayerUiState.kt +++ b/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/PlayerUiState.kt @@ -51,7 +51,6 @@ data class PlayerUiState( val folderBackGestureNavigationEnabled: Boolean = true, val currentSongSortOption: SortOption = SortOption.SongTitleAZ, // val songCount: Int = 0, // REMOVED - val isGeneratingAiMetadata: Boolean = false, val searchHistory: ImmutableList = persistentListOf(), val searchQuery: String = "", val isSyncingLibrary: Boolean = 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 d6829b847..3868e2eae 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 @@ -37,7 +37,6 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.common.util.concurrent.Futures import com.google.common.util.concurrent.ListenableFuture import com.theveloper.pixelplay.R -import com.theveloper.pixelplay.data.ai.SongMetadata import com.theveloper.pixelplay.data.media.CoverArtUpdate import com.theveloper.pixelplay.data.model.Album import com.theveloper.pixelplay.data.model.Artist @@ -171,14 +170,6 @@ private data class SortOptionsSnapshot( val favoriteSort: SortOption, ) -private data class AiUiSnapshot( - val showAiPlaylistSheet: Boolean, - val isGeneratingAiPlaylist: Boolean, - val aiStatus: String?, - val aiError: String?, - val isGeneratingAiMetadata: Boolean, -) - @UnstableApi @SuppressLint("LogNotTimber") @OptIn(coil.annotation.ExperimentalCoilApi::class, ExperimentalCoroutinesApi::class) @@ -446,10 +437,6 @@ class PlayerViewModel @Inject constructor( val aiStatus: StateFlow = aiStateHolder.aiStatus val aiError: StateFlow = aiStateHolder.aiError - // AI Metadata Generation States - val isGeneratingAiMetadata: StateFlow = aiStateHolder.isGeneratingMetadata - val aiMetadataSuccess: StateFlow = aiStateHolder.aiMetadataSuccess - private val _selectedSongForInfo = MutableStateFlow(null) val selectedSongForInfo: StateFlow = _selectedSongForInfo.asStateFlow() @@ -1779,29 +1766,6 @@ class PlayerViewModel @Inject constructor( openPlayerSheetCallback = { _isSheetVisible.value = true } ) - // Collect AiStateHolder flows - viewModelScope.launch { - combine( - aiStateHolder.showAiPlaylistSheet, - aiStateHolder.isGeneratingAiPlaylist, - aiStateHolder.aiStatus, - aiStateHolder.aiError, - aiStateHolder.isGeneratingMetadata, - ) { show, generating, status, error, generatingMetadata -> - AiUiSnapshot( - showAiPlaylistSheet = show, - isGeneratingAiPlaylist = generating, - aiStatus = status, - aiError = error, - isGeneratingAiMetadata = generatingMetadata - ) - }.collect { snapshot -> - _playerUiState.update { - it.copy(isGeneratingAiMetadata = snapshot.isGeneratingAiMetadata) - } - } - } - // Initialize LibraryStateHolder libraryStateHolder.initialize(viewModelScope) @@ -2611,10 +2575,6 @@ class PlayerViewModel @Inject constructor( aiStateHolder.retryLastPlaylistGeneration() } - fun retryLastMetadataGeneration() { - aiStateHolder.retryLastMetadataGeneration() - } - fun clearQueueExceptCurrent() { mediaController?.let { controller -> val currentSongIndex = controller.currentMediaItemIndex @@ -2886,10 +2846,6 @@ class PlayerViewModel @Inject constructor( }.getOrDefault(false) } - suspend fun generateAiMetadata(song: Song, fields: List): Result { - return aiStateHolder.generateAiMetadata(song, fields) - } - private fun updateSongInStates( updatedSong: Song, newLyrics: Lyrics? = null, 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 41c9b2494..7996811c6 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 @@ -180,7 +180,6 @@ class PlayerViewModelTest { every { mockAiStateHolder.showAiPlaylistSheet } returns MutableStateFlow(false) every { mockAiStateHolder.isGeneratingAiPlaylist } returns MutableStateFlow(false) every { mockAiStateHolder.aiError } returns MutableStateFlow(null) - every { mockAiStateHolder.isGeneratingMetadata } returns MutableStateFlow(false) every { mockAiStateHolder.initialize(any(), any(), any(), any(), any(), any()) } just runs every { mockCastStateHolder.castSession } returns _castSessionFlow From e7713164e1f08adc59ab2a173bc5f158cb375124 Mon Sep 17 00:00:00 2001 From: VoidX3D Date: Mon, 15 Jun 2026 21:29:18 +0545 Subject: [PATCH 42/72] refactor: improve translateLyrics prompt with XML structure - Restructure prompt with , , , sections - Clarify timestamp preservation and ALREADY_IN_TARGET_LANGUAGE behavior --- .../presentation/viewmodel/AiStateHolder.kt | 33 ++++++++++--------- 1 file changed, 17 insertions(+), 16 deletions(-) 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 25f9f6cee..ad68cea83 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 @@ -288,23 +288,24 @@ class AiStateHolder @Inject constructor( return try { val targetLanguage = context.resources.configuration.locales[0].displayLanguage val prompt = """ -Translate the provided song lyrics into $targetLanguage. - -Keep every timestamp exactly unchanged. - -If the lyrics are ALREADY mostly in $targetLanguage, output ONLY the exact phrase "ALREADY_IN_TARGET_LANGUAGE" without any other text. - -For each original line, output the original line first, then on the next line output the $targetLanguage translation with the same timestamp. - -Do not add any extra text, explanations, numbering, labels, or formatting. -Do not remove, merge, split, or reorder lines. - -Output only: -[timestamp] original text -[timestamp] translated text - -Lyrics to translate: +Translate song lyrics into $targetLanguage. + + +- 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. +- NEVER add explanations, labels, numbering, section headers, or formatting. +- NEVER remove, merge, split, or reorder lines. +- If lyrics are ALREADY mostly in $targetLanguage, output ONLY: ALREADY_IN_TARGET_LANGUAGE + + + +[original timestamp] original text +[same timestamp] translated text + + + $lyricsText + """.trimIndent() val response = aiHandler.generateContent( From 49d890ebcaf0faf303f5218032158f492137b94c Mon Sep 17 00:00:00 2001 From: VoidX3D Date: Mon, 15 Jun 2026 21:47:04 +0545 Subject: [PATCH 43/72] fix: restore playlist generation state tracking - Add AiUiSnapshot data class (showAiPlaylistSheet, isGeneratingAiPlaylist, aiStatus, aiError) - Re-add combine in PlayerViewModel init to collect AiStateHolder flows into PlayerUiState - Add showAiPlaylistSheet, isGeneratingAiPlaylist, aiStatus, aiError fields to PlayerUiState --- .../presentation/viewmodel/PlayerUiState.kt | 4 +++ .../presentation/viewmodel/PlayerViewModel.kt | 33 +++++++++++++++++++ 2 files changed, 37 insertions(+) diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/PlayerUiState.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/PlayerUiState.kt index 5485efe70..7da60dfe4 100644 --- a/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/PlayerUiState.kt +++ b/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/PlayerUiState.kt @@ -22,6 +22,10 @@ data class PlayerUiState( // val artists: ImmutableList = persistentListOf(), // REMOVED val searchResults: ImmutableList = persistentListOf(), val musicFolders: ImmutableList = persistentListOf(), + val showAiPlaylistSheet: Boolean = false, + val isGeneratingAiPlaylist: Boolean = false, + val aiStatus: String? = null, + val aiError: String? = null, val sortOption: SortOption = SortOption.SongDefaultOrder, val isLoadingInitialSongs: Boolean = true, val isLoadingLibrary: Boolean = true, 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 3868e2eae..f552dbfb6 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 @@ -162,6 +162,13 @@ private fun moveQueueIndex(index: Int, fromIndex: Int, toIndex: Int): Int { } } +private data class AiUiSnapshot( + val showAiPlaylistSheet: Boolean, + val isGeneratingAiPlaylist: Boolean, + val aiStatus: String?, + val aiError: String?, +) + private data class SortOptionsSnapshot( val songSort: SortOption, val albumSort: SortOption, @@ -1766,6 +1773,32 @@ class PlayerViewModel @Inject constructor( openPlayerSheetCallback = { _isSheetVisible.value = true } ) + // Collect AiStateHolder flows for playlist generation state + viewModelScope.launch { + combine( + aiStateHolder.showAiPlaylistSheet, + aiStateHolder.isGeneratingAiPlaylist, + aiStateHolder.aiStatus, + aiStateHolder.aiError, + ) { show, generating, status, error -> + AiUiSnapshot( + showAiPlaylistSheet = show, + isGeneratingAiPlaylist = generating, + aiStatus = status, + aiError = error + ) + }.collect { snapshot -> + _playerUiState.update { + it.copy( + showAiPlaylistSheet = snapshot.showAiPlaylistSheet, + isGeneratingAiPlaylist = snapshot.isGeneratingAiPlaylist, + aiStatus = snapshot.aiStatus, + aiError = snapshot.aiError + ) + } + } + } + // Initialize LibraryStateHolder libraryStateHolder.initialize(viewModelScope) From 43d79ca91bf60afbfc06011d95a4cd56d11f241a Mon Sep 17 00:00:00 2001 From: VoidX3D Date: Mon, 15 Jun 2026 22:02:32 +0545 Subject: [PATCH 44/72] fix: update API key messages and enhance AI provider key handling --- .../presentation/components/PlaylistBottomSheet.kt | 4 ++-- .../presentation/screens/LibraryScreen.kt | 2 +- .../presentation/viewmodel/PlayerViewModel.kt | 14 ++++++++++++-- 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/components/PlaylistBottomSheet.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/components/PlaylistBottomSheet.kt index 2ceed9d9f..6a3dc19f3 100644 --- a/app/src/main/java/com/theveloper/pixelplay/presentation/components/PlaylistBottomSheet.kt +++ b/app/src/main/java/com/theveloper/pixelplay/presentation/components/PlaylistBottomSheet.kt @@ -63,7 +63,7 @@ fun PlaylistBottomSheet( currentPlaylistId: String? = null ) { val playlistCreatedAndSongsAddedMessage = stringResource(R.string.playlist_sheet_created_and_songs_added) - val setGeminiApiKeyFirstMessage = stringResource(R.string.library_toast_set_gemini_api_key_first) + val setAiProviderApiKeyFirstMessage = stringResource(R.string.library_toast_set_ai_provider_api_key_first) val songAddedToPlaylistsMessage = stringResource(R.string.playlist_sheet_song_added_to_playlists) val commonSavedMessage = stringResource(R.string.common_saved) val saveActionText = stringResource(R.string.common_save) @@ -214,7 +214,7 @@ fun PlaylistBottomSheet( if (hasActiveAiProviderApiKey) { playerViewModel.showAiPlaylistSheet() } else { - playerViewModel.sendToast(setGeminiApiKeyFirstMessage) + playerViewModel.sendToast(setAiProviderApiKeyFirstMessage) } } ) 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 5c52f1a50..5d37d029e 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 @@ -1772,7 +1772,7 @@ fun LibraryScreen( playerViewModel.clearAiPlaylistError() showCreateAiPlaylistDialog = true } else { - Toast.makeText(context, context.getString(R.string.library_toast_set_gemini_api_key_first), Toast.LENGTH_SHORT).show() + Toast.makeText(context, context.getString(R.string.library_toast_set_ai_provider_api_key_first), Toast.LENGTH_SHORT).show() } }, onCreate = { name, imageUri, color, icon, songIds, cropScale, cropPanX, cropPanY, shapeType, d1, d2, d3, d4, smartRuleKey -> 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 f552dbfb6..4ddadbede 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 @@ -498,7 +498,10 @@ class PlayerViewModel @Inject constructor( aiPreferencesRepository.nvidiaApiKey, aiPreferencesRepository.kimiApiKey, aiPreferencesRepository.glmApiKey, - aiPreferencesRepository.openaiApiKey + aiPreferencesRepository.openaiApiKey, + aiPreferencesRepository.ollamaApiKey, + aiPreferencesRepository.customApiKey, + aiPreferencesRepository.openrouterApiKey ) { values -> val provider = values[0] val gemini = values[1] @@ -509,7 +512,11 @@ class PlayerViewModel @Inject constructor( val kimi = values[6] val glm = values[7] val openai = values[8] + val ollama = values[9] + val custom = values[10] + val openrouter = values[11] when (provider) { + "GEMINI" -> gemini.isNotBlank() "DEEPSEEK" -> deepseek.isNotBlank() "GROQ" -> groq.isNotBlank() "MISTRAL" -> mistral.isNotBlank() @@ -517,7 +524,10 @@ class PlayerViewModel @Inject constructor( "KIMI" -> kimi.isNotBlank() "GLM" -> glm.isNotBlank() "OPENAI" -> openai.isNotBlank() - else -> gemini.isNotBlank() + "OPENROUTER" -> openrouter.isNotBlank() + "OLLAMA" -> ollama.isNotBlank() + "CUSTOM" -> custom.isNotBlank() + else -> false } }.distinctUntilChanged() .stateIn( From 97f6f34d8f37add43d27f39679708ec130c5af86 Mon Sep 17 00:00:00 2001 From: Ayaan Date: Wed, 17 Jun 2026 14:33:31 +0530 Subject: [PATCH 45/72] ui: fix layout jump when collapsing telegram channel topics Replaced Arrangement.spacedBy with manual Spacers in ExpressiveChannelItem to prevent a sudden 14dp height jump when the topics list finishes its exit animation. By moving the spacer inside AnimatedVisibility, the vertical gap now shrinks smoothly along with the content. --- .../dashboard/TelegramDashboardScreen.kt | 25 +++++++++++++------ 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/telegram/dashboard/TelegramDashboardScreen.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/telegram/dashboard/TelegramDashboardScreen.kt index e239280b8..3f97e6fda 100644 --- a/app/src/main/java/com/theveloper/pixelplay/presentation/telegram/dashboard/TelegramDashboardScreen.kt +++ b/app/src/main/java/com/theveloper/pixelplay/presentation/telegram/dashboard/TelegramDashboardScreen.kt @@ -348,15 +348,20 @@ fun TelegramDashboardScreen( ) }, confirmButton = { - TextButton(onClick = { - viewModel.removeChannel(channel.chatId) - channelPendingRemoval = null - }) { + FilledTonalButton( + onClick = { + viewModel.removeChannel(channel.chatId) + channelPendingRemoval = null + }, + colors = androidx.compose.material3.ButtonDefaults.filledTonalButtonColors( + containerColor = MaterialTheme.colorScheme.errorContainer, + contentColor = MaterialTheme.colorScheme.onErrorContainer + ) + ) { Text( text = stringResource(R.string.telegram_remove_channel_confirm_action), fontFamily = GoogleSansRounded, - fontWeight = FontWeight.SemiBold, - color = MaterialTheme.colorScheme.error + fontWeight = FontWeight.SemiBold ) } }, @@ -427,8 +432,7 @@ private fun ExpressiveChannelItem( Column( modifier = Modifier .fillMaxWidth() - .padding(horizontal = 16.dp, vertical = 14.dp), - verticalArrangement = Arrangement.spacedBy(14.dp) + .padding(horizontal = 16.dp, vertical = 14.dp) ) { // ── Channel header row ────────────────────────────────────── Row( @@ -486,6 +490,8 @@ private fun ExpressiveChannelItem( } } + Spacer(modifier = Modifier.height(14.dp)) + // ── Meta pills ────────────────────────────────────────────── FlowRow( horizontalArrangement = Arrangement.spacedBy(8.dp), @@ -515,6 +521,8 @@ private fun ExpressiveChannelItem( } } + Spacer(modifier = Modifier.height(14.dp)) + // ── Action buttons ────────────────────────────────────────── Row( modifier = Modifier.fillMaxWidth(), @@ -593,6 +601,7 @@ private fun ExpressiveChannelItem( modifier = Modifier.fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(0.dp) ) { + Spacer(modifier = Modifier.height(14.dp)) HorizontalDivider( modifier = Modifier.padding(vertical = 4.dp), color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.5f) From 10661f7a0d4bc5a6214e33bba67c392e3b911c8b Mon Sep 17 00:00:00 2001 From: Ayaan Date: Wed, 17 Jun 2026 16:01:43 +0530 Subject: [PATCH 46/72] f eat(ui): add social chips to About screen for GitHub and Telegram links --- .../presentation/screens/AboutScreen.kt | 81 ++++++++++++++++++- 1 file changed, 80 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/screens/AboutScreen.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/screens/AboutScreen.kt index fdb1b7b19..459e68aaf 100644 --- a/app/src/main/java/com/theveloper/pixelplay/presentation/screens/AboutScreen.kt +++ b/app/src/main/java/com/theveloper/pixelplay/presentation/screens/AboutScreen.kt @@ -83,6 +83,9 @@ import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.Role +import androidx.compose.ui.semantics.clearAndSetSemantics +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.role import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.Dp @@ -130,7 +133,6 @@ private val CoreMaintainer = Contributor( avatarUrl = "https://avatars.githubusercontent.com/u/26845343?v=4", iconRes = R.drawable.round_developer_board_24, githubUrl = "https://github.com/theovilardo", - telegramUrl = "https://t.me/thevelopersupport", ) private val PinnedCommunityMembers = listOf( @@ -497,6 +499,7 @@ private fun AboutHeroCard( ) { val heroShape = AbsoluteSmoothCornerShape(30.dp, 60) val haptic = LocalHapticFeedback.current + val context = LocalContext.current Surface( modifier = modifier, @@ -578,6 +581,82 @@ private fun AboutHeroCard( Spacer(modifier = Modifier.height(12.dp)) CommunitySignalsRow() + + Spacer(modifier = Modifier.height(12.dp)) + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + SocialChip( + label = stringResource(R.string.about_github_label), + subtitle = stringResource(R.string.about_github_subtitle), + iconRes = R.drawable.github, + contentDescription = stringResource(R.string.about_cd_open_github_repo), + onClick = { openUrl(context, "https://github.com/theovilardo/PixelPlayer") }, + modifier = Modifier.weight(1f), + ) + SocialChip( + label = stringResource(R.string.about_telegram_label), + subtitle = stringResource(R.string.about_telegram_subtitle), + iconRes = R.drawable.telegram, + contentDescription = stringResource(R.string.about_cd_join_telegram), + onClick = { openUrl(context, "https://t.me/thevelopersupport") }, + modifier = Modifier.weight(1f), + ) + } + } + } + } +} + +@Composable +private fun SocialChip( + label: String, + subtitle: String, + @DrawableRes iconRes: Int, + contentDescription: String, + onClick: () -> Unit, + modifier: Modifier = Modifier, +) { + Surface( + onClick = onClick, + modifier = modifier + .height(52.dp) + .clearAndSetSemantics { + this.contentDescription = contentDescription + this.role = Role.Button + }, + shape = AbsoluteSmoothCornerShape(14.dp, 60), + color = MaterialTheme.colorScheme.surfaceContainerHigh.copy(alpha = 0.92f), + tonalElevation = 1.dp, + ) { + Row( + modifier = Modifier.padding(horizontal = 12.dp), + horizontalArrangement = Arrangement.Start, + verticalAlignment = Alignment.CenterVertically, + ) { + Icon( + painter = painterResource(iconRes), + contentDescription = null, + modifier = Modifier.size(20.dp), + tint = MaterialTheme.colorScheme.primary, + ) + Spacer(modifier = Modifier.width(10.dp)) + Column( + verticalArrangement = Arrangement.Center + ) { + Text( + text = label, + style = MaterialTheme.typography.labelLarge, + fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.onSurface, + ) + Text( + text = subtitle, + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) } } } From 4616616cf99f20d8b48a1fad4461b9a0688acb29 Mon Sep 17 00:00:00 2001 From: Ayaan Date: Wed, 17 Jun 2026 16:02:25 +0530 Subject: [PATCH 47/72] strings: add labels and content descriptions for About screen social links --- app/src/main/res/values/strings_settings.xml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/src/main/res/values/strings_settings.xml b/app/src/main/res/values/strings_settings.xml index 298046dec..b99b40174 100644 --- a/app/src/main/res/values/strings_settings.xml +++ b/app/src/main/res/values/strings_settings.xml @@ -633,6 +633,12 @@ 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 From 7d2014159e2862e05cebbb5c7ef45d7a75eb2b70 Mon Sep 17 00:00:00 2001 From: Ayaan Date: Wed, 17 Jun 2026 16:02:55 +0530 Subject: [PATCH 48/72] strings(es): translate About screen social links to Spanish --- app/src/main/res/values-es/strings_settings.xml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/src/main/res/values-es/strings_settings.xml b/app/src/main/res/values-es/strings_settings.xml index b66c4aa60..f39e8bb03 100644 --- a/app/src/main/res/values-es/strings_settings.xml +++ b/app/src/main/res/values-es/strings_settings.xml @@ -633,6 +633,12 @@ Colaboradores de código abierto Lista de colaboradores en vivo desde GitHub. %1$d contrib. + GitHub + Repositorio + Telegram + Soporte + Abrir repositorio de GitHub + Unirse a la comunidad de Telegram Abrir perfil de GitHub Abrir Telegram Avatar de %1$s From bff3ef946e338a02a7030868b90e4e785ab6f439 Mon Sep 17 00:00:00 2001 From: Ayaan Date: Wed, 17 Jun 2026 16:23:55 +0530 Subject: [PATCH 49/72] strings: localize GitHub and Telegram social chips for Arabic --- app/src/main/res/values-ar/strings_settings.xml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/src/main/res/values-ar/strings_settings.xml b/app/src/main/res/values-ar/strings_settings.xml index 78d187b68..6e3958ad4 100644 --- a/app/src/main/res/values-ar/strings_settings.xml +++ b/app/src/main/res/values-ar/strings_settings.xml @@ -297,4 +297,10 @@ وحدات عدد %1$d · إصدار %2$s · إصدار المخطط البرمجي %3$d Korean (الكورية) Norwegian (النرويجية بوكمول) + GitHub + مستودع الكود + تليجرام + الدعم + فتح مستودع GitHub + الانضمام إلى مجتمع تليجرام From bf45b6fea6049853f8d44f84b4f01b2998f398fb Mon Sep 17 00:00:00 2001 From: Ayaan Date: Wed, 17 Jun 2026 16:24:22 +0530 Subject: [PATCH 50/72] strings: localize GitHub and Telegram social chips for German --- app/src/main/res/values-de/strings_settings.xml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/src/main/res/values-de/strings_settings.xml b/app/src/main/res/values-de/strings_settings.xml index 69f37192e..e77d6dd5d 100644 --- a/app/src/main/res/values-de/strings_settings.xml +++ b/app/src/main/res/values-de/strings_settings.xml @@ -633,6 +633,12 @@ Open-Source-Mitwirkende Aktuelle Mitwirkenden-Liste von GitHub. %1$d Beiträge + GitHub + Repository + Telegram + Support + GitHub-Repository öffnen + Der Telegram-Community beitreten GitHub-Profil öffnen Telegram öffnen Avatar von %1$s From f9da38654140d7f9ae0fa7c8bf0d0db242997a96 Mon Sep 17 00:00:00 2001 From: Ayaan Date: Wed, 17 Jun 2026 16:24:41 +0530 Subject: [PATCH 51/72] strings: localize GitHub and Telegram social chips for French --- app/src/main/res/values-fr/strings_settings.xml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/src/main/res/values-fr/strings_settings.xml b/app/src/main/res/values-fr/strings_settings.xml index 13f14c868..8c306031b 100644 --- a/app/src/main/res/values-fr/strings_settings.xml +++ b/app/src/main/res/values-fr/strings_settings.xml @@ -629,6 +629,12 @@ Contributeurs open source Liste des contributeurs en direct de GitHub. %1$d contrib. + GitHub + Dépôt + Telegram + Support + Ouvrir le dépôt GitHub + Rejoindre la communauté Telegram Ouvrir le profil GitHub Ouvrir Telegram Avatar de %1$s From 8ff5822d37df8cacab8ae0455c66763c6426a5cc Mon Sep 17 00:00:00 2001 From: Ayaan Date: Wed, 17 Jun 2026 16:24:59 +0530 Subject: [PATCH 52/72] strings: localize GitHub and Telegram social chips for Indonesian --- app/src/main/res/values-in/strings_settings.xml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/src/main/res/values-in/strings_settings.xml b/app/src/main/res/values-in/strings_settings.xml index 6aab6877b..09e2d508c 100644 --- a/app/src/main/res/values-in/strings_settings.xml +++ b/app/src/main/res/values-in/strings_settings.xml @@ -629,6 +629,12 @@ Kontributor open source Daftar kontributor langsung dari GitHub. %1$d kontrib. + GitHub + Repositori + Telegram + Dukungan + Buka repositori GitHub + Bergabung dengan komunitas Telegram Buka profil GitHub Buka Telegram Avatar %1$s From b2379165aadd0574793ee078c5979059d2f5d583 Mon Sep 17 00:00:00 2001 From: Ayaan Date: Wed, 17 Jun 2026 16:25:17 +0530 Subject: [PATCH 53/72] strings: localize GitHub and Telegram social chips for Italian --- app/src/main/res/values-it/strings_settings.xml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/src/main/res/values-it/strings_settings.xml b/app/src/main/res/values-it/strings_settings.xml index 234a0e911..41b7de815 100644 --- a/app/src/main/res/values-it/strings_settings.xml +++ b/app/src/main/res/values-it/strings_settings.xml @@ -633,6 +633,12 @@ Contributori open source Lista contributori live da GitHub. %1$d contrib. + GitHub + Repository + Telegram + Supporto + Apri repository GitHub + Unisciti alla community di Telegram Apri profilo GitHub Apri Telegram Avatar di %1$s From 8334d640ca2c74072eadbe6dcc8a82179c8dea9d Mon Sep 17 00:00:00 2001 From: Ayaan Date: Wed, 17 Jun 2026 16:25:41 +0530 Subject: [PATCH 54/72] strings: localize GitHub and Telegram social chips for Korean --- app/src/main/res/values-ko/strings_settings.xml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/src/main/res/values-ko/strings_settings.xml b/app/src/main/res/values-ko/strings_settings.xml index bcc63f057..4c28f7c23 100644 --- a/app/src/main/res/values-ko/strings_settings.xml +++ b/app/src/main/res/values-ko/strings_settings.xml @@ -633,6 +633,12 @@ 오픈 소스 기여자 GitHub의 실시간 기여자 목록입니다. 기여 %1$d회 + GitHub + 저장소 + Telegram + 지원 + GitHub 저장소 열기 + Telegram 커뮤니티 가입 GitHub 프로필 열기 Telegram 열기 %1$s 아바타 From 7fc08838a444fae7e082184a6f6a15fe6ac66118 Mon Sep 17 00:00:00 2001 From: Ayaan Date: Wed, 17 Jun 2026 16:25:58 +0530 Subject: [PATCH 55/72] =?UTF-8?q?strings:=20localize=20GitHub=20and=20Tele?= =?UTF-8?q?gram=20social=20chips=20for=20Norwegian=20Bokm=C3=A5l?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/src/main/res/values-nb/strings_settings.xml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/src/main/res/values-nb/strings_settings.xml b/app/src/main/res/values-nb/strings_settings.xml index fd101cb66..b6812bd47 100644 --- a/app/src/main/res/values-nb/strings_settings.xml +++ b/app/src/main/res/values-nb/strings_settings.xml @@ -633,6 +633,12 @@ Bidragsytere til åpen kildekode Live bidragsyterliste fra GitHub. %1$d bidrag. + GitHub + Kodelager + Telegram + Støtte + Åpne GitHub-kodelager + Bli med i Telegram-samfunnet Åpne GitHub-profil Åpne Telegram Avatar av %1$s From 5e905f193acb267b84931bbd27a9a17d696338c2 Mon Sep 17 00:00:00 2001 From: Ayaan Date: Wed, 17 Jun 2026 16:26:13 +0530 Subject: [PATCH 56/72] strings: localize GitHub and Telegram social chips for Russian --- app/src/main/res/values-ru/strings_settings.xml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/src/main/res/values-ru/strings_settings.xml b/app/src/main/res/values-ru/strings_settings.xml index 997331e07..75182d311 100644 --- a/app/src/main/res/values-ru/strings_settings.xml +++ b/app/src/main/res/values-ru/strings_settings.xml @@ -633,6 +633,12 @@ Участники open source Актуальный список участников с GitHub. %1$d вклад. + GitHub + Репозиторий + Telegram + Поддержка + Открыть репозиторий GitHub + Присоединиться к сообществу Telegram Открыть профиль GitHub Открыть Telegram Аватар %1$s From 1a1b43ca27b5f88bad5424acd3d26365db51eb0e Mon Sep 17 00:00:00 2001 From: Ayaan Date: Wed, 17 Jun 2026 16:26:31 +0530 Subject: [PATCH 57/72] strings: localize GitHub and Telegram social chips for Turkish --- app/src/main/res/values-tr/strings_settings.xml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/src/main/res/values-tr/strings_settings.xml b/app/src/main/res/values-tr/strings_settings.xml index 1f4dc593c..3fc129112 100644 --- a/app/src/main/res/values-tr/strings_settings.xml +++ b/app/src/main/res/values-tr/strings_settings.xml @@ -633,6 +633,12 @@ Açık kaynak katkıda bulunanlar GitHub\'dan canlı katkıda bulunanlar listesi. %1$d katkı + GitHub + Depo + Telegram + Destek + GitHub deposunu aç + Telegram topluluğuna katıl GitHub profilini aç Telegram\'ı aç %1$s avatarı From d3e73c0e3e998aba23c462f4381f146ec2ff4c6e Mon Sep 17 00:00:00 2001 From: Ayaan Date: Wed, 17 Jun 2026 16:26:51 +0530 Subject: [PATCH 58/72] strings: localize GitHub and Telegram social chips for Chinese (Simplified) --- app/src/main/res/values-zh-rCN/strings_settings.xml | 6 ++++++ 1 file changed, 6 insertions(+) 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 0822b25e0..d54bde659 100644 --- a/app/src/main/res/values-zh-rCN/strings_settings.xml +++ b/app/src/main/res/values-zh-rCN/strings_settings.xml @@ -633,6 +633,12 @@ 开源贡献者 来自 GitHub 的实时贡献者名单。 %1$d 次贡献 + GitHub + 代码仓库 + Telegram + 支持 + 打开 GitHub 仓库 + 加入 Telegram 社区 打开 GitHub 个人资料 打开 Telegram %1$s 的头像 From 0d671d4e82aded610d57801a0dda6fcdd98f5c44 Mon Sep 17 00:00:00 2001 From: Shinichi Fujimoto Date: Thu, 18 Jun 2026 14:45:47 +0900 Subject: [PATCH 59/72] feat(i18n): add Japanese translation files (values-ja/) Translate all 10 string resource files into Japanese (~1,470 strings). Japanese plurals use quantity="other" only, which covers all cases. Co-Authored-By: Claude Sonnet 4.6 --- .idea/deploymentTargetSelector.xml | 29 - 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 + 11 files changed, 2351 insertions(+), 29 deletions(-) delete mode 100644 .idea/deploymentTargetSelector.xml 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/.idea/deploymentTargetSelector.xml b/.idea/deploymentTargetSelector.xml deleted file mode 100644 index d8b7d40b0..000000000 --- a/.idea/deploymentTargetSelector.xml +++ /dev/null @@ -1,29 +0,0 @@ - - - - - - - - - - - \ No newline at end of file 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 000000000..5dedb3c73 --- /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 000000000..85544d5ba --- /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 000000000..a2e5bac75 --- /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 000000000..3c36811ad --- /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 000000000..ac2021f3e --- /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 000000000..4c7c2d6ca --- /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 000000000..eae601ef3 --- /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 000000000..be88a28c0 --- /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 000000000..ee41a6b0d --- /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 000000000..c0d6f4787 --- /dev/null +++ b/app/src/main/res/values-ja/strings_widget.xml @@ -0,0 +1,17 @@ + + + サイズに合わせて自動調整するウィジェット + コンパクトなプレイヤーバー + シャッフルとリピートを含むフルコントロール + ミニマリストな正方形プレイヤー + + タップして開く + アルバムアート + アルバムアートのプレースホルダー + + タップして再生 + 曲のタイトル + アーティスト + + 進行バー、%1$d%% + From c7e2b0f8fb3660c4ec5aff2270bad9bc75d0b785 Mon Sep 17 00:00:00 2001 From: Shinichi Fujimoto Date: Thu, 18 Jun 2026 14:45:52 +0900 Subject: [PATCH 60/72] feat(i18n): register Japanese locale in AppLanguage and locales_config - Add JAPANESE("ja", R.string.settings_language_japanese) to AppLanguage enum - Add to locales_config.xml Co-Authored-By: Claude Sonnet 4.6 --- .../com/theveloper/pixelplay/data/preferences/AppLanguage.kt | 1 + app/src/main/res/xml/locales_config.xml | 3 ++- 2 files changed, 3 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 5630700f9..3303ab89a 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 @@ -16,6 +16,7 @@ enum class AppLanguage(val tag: String, @StringRes val labelRes: Int) { 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), + JAPANESE("ja", R.string.settings_language_japanese), TURKISH("tr", R.string.settings_language_turkish); companion object { diff --git a/app/src/main/res/xml/locales_config.xml b/app/src/main/res/xml/locales_config.xml index 12f03ebe0..933ea17f9 100644 --- a/app/src/main/res/xml/locales_config.xml +++ b/app/src/main/res/xml/locales_config.xml @@ -3,4 +3,5 @@ - \ No newline at end of file + + From 0deef6e8dfc7ff1cac94dbdb02176f31cb7abf2c Mon Sep 17 00:00:00 2001 From: Shinichi Fujimoto Date: Thu, 18 Jun 2026 14:46:00 +0900 Subject: [PATCH 61/72] feat(i18n): add settings_language_japanese label to all language files MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add the "Japanese" language label in each language's own translation: en=Japanese, de=Japanisch, es=Japonés, fr=Japonais, in=Jepang, it=Giapponese, ko=일본어, nb=Japansk, ru=Японский, tr=Japonca, zh=日语 Co-Authored-By: Claude Sonnet 4.6 --- app/src/main/res/values-de/strings_settings.xml | 1 + app/src/main/res/values-es/strings_settings.xml | 1 + app/src/main/res/values-fr/strings_settings.xml | 1 + app/src/main/res/values-in/strings_settings.xml | 1 + app/src/main/res/values-it/strings_settings.xml | 1 + app/src/main/res/values-ko/strings_settings.xml | 1 + app/src/main/res/values-nb/strings_settings.xml | 1 + app/src/main/res/values-ru/strings_settings.xml | 1 + app/src/main/res/values-tr/strings_settings.xml | 1 + app/src/main/res/values-zh-rCN/strings_settings.xml | 1 + app/src/main/res/values/strings_settings.xml | 1 + 11 files changed, 11 insertions(+) diff --git a/app/src/main/res/values-de/strings_settings.xml b/app/src/main/res/values-de/strings_settings.xml index 69f37192e..59027ac46 100644 --- a/app/src/main/res/values-de/strings_settings.xml +++ b/app/src/main/res/values-de/strings_settings.xml @@ -174,6 +174,7 @@ Koreanisch Norwegisch Bokmål Türkisch + Japanisch 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 b66c4aa60..bf74b25b7 100644 --- a/app/src/main/res/values-es/strings_settings.xml +++ b/app/src/main/res/values-es/strings_settings.xml @@ -174,6 +174,7 @@ Coreano Noruego (Bokmål) Turco + Japonés 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 13f14c868..d6359457b 100644 --- a/app/src/main/res/values-fr/strings_settings.xml +++ b/app/src/main/res/values-fr/strings_settings.xml @@ -170,6 +170,7 @@ Coréen Norvégien (Bokmål) Turc + Japonais 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 6aab6877b..26e7679c0 100644 --- a/app/src/main/res/values-in/strings_settings.xml +++ b/app/src/main/res/values-in/strings_settings.xml @@ -170,6 +170,7 @@ Korea Norwegia (Bokmål) Turki + Jepang 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 234a0e911..f28b06d68 100644 --- a/app/src/main/res/values-it/strings_settings.xml +++ b/app/src/main/res/values-it/strings_settings.xml @@ -174,6 +174,7 @@ Coreano Norvegese (Bokmål) Turco + Giapponese Tema app Passa tra chiaro, scuro o segui l\'aspetto di sistema. Tema chiaro diff --git a/app/src/main/res/values-ko/strings_settings.xml b/app/src/main/res/values-ko/strings_settings.xml index bcc63f057..a3ba06bfb 100644 --- a/app/src/main/res/values-ko/strings_settings.xml +++ b/app/src/main/res/values-ko/strings_settings.xml @@ -174,6 +174,7 @@ 한국어 노르웨이어 (Bokmål) 터키어 + 일본어 앱 테마 밝은 테마, 어두운 테마 또는 시스템 설정 따르기 중에서 선택하세요. 밝은 테마 diff --git a/app/src/main/res/values-nb/strings_settings.xml b/app/src/main/res/values-nb/strings_settings.xml index fd101cb66..c9eb4dd85 100644 --- a/app/src/main/res/values-nb/strings_settings.xml +++ b/app/src/main/res/values-nb/strings_settings.xml @@ -174,6 +174,7 @@ Koreansk Norsk bokmål Tyrkisk + Japansk App-tema Bytt mellom lyst, mørkt eller følg systemets utseende. Lyst tema diff --git a/app/src/main/res/values-ru/strings_settings.xml b/app/src/main/res/values-ru/strings_settings.xml index 997331e07..eb4f529f2 100644 --- a/app/src/main/res/values-ru/strings_settings.xml +++ b/app/src/main/res/values-ru/strings_settings.xml @@ -174,6 +174,7 @@ Корейский Норвежский (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 1f4dc593c..0dffd3442 100644 --- a/app/src/main/res/values-tr/strings_settings.xml +++ b/app/src/main/res/values-tr/strings_settings.xml @@ -174,6 +174,7 @@ Korece Norveççe (Bokmål) Türkçe + Japonca 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-zh-rCN/strings_settings.xml b/app/src/main/res/values-zh-rCN/strings_settings.xml index 0822b25e0..af4e5422e 100644 --- a/app/src/main/res/values-zh-rCN/strings_settings.xml +++ b/app/src/main/res/values-zh-rCN/strings_settings.xml @@ -174,6 +174,7 @@ 韩语 挪威语(Bokmål) 土耳其语 + 日语 应用主题 在浅色、深色之间切换,或跟随系统外观。 浅色主题 diff --git a/app/src/main/res/values/strings_settings.xml b/app/src/main/res/values/strings_settings.xml index 298046dec..de56b98d8 100644 --- a/app/src/main/res/values/strings_settings.xml +++ b/app/src/main/res/values/strings_settings.xml @@ -174,6 +174,7 @@ Korean Norwegian (Bokmål) Türkçe + Japanese App Theme Switch between light, dark, or follow system appearance. Light Theme From fd0a3cbcf736f19ae57c57ec2906f77cb47cfdbd Mon Sep 17 00:00:00 2001 From: amreldeeb Date: Thu, 18 Jun 2026 21:28:48 +0300 Subject: [PATCH 62/72] fix(playlists): prevent lost updates when editing playlist songs concurrently PlaylistPreferencesRepository edited playlists with an unsynchronized read-modify-write: userPlaylistsFlow.first() -> modify list -> updatePlaylist(). Removing several songs in quick succession fired concurrent coroutines that each read the same snapshot, so the last write won and the other removals were silently dropped. The Playlists-menu song count (songIds.size, read from the DB) then stayed stuck high, while the playlist detail screen still looked correct because it updates optimistically per tap. Serialize the read-modify-write editors (add/remove/reorder/rename/updatePlaylist /removeSongFromAllPlaylists) behind a coroutine Mutex so each edit reads and writes atomically. A private updatePlaylistLocked avoids re-entrant locking. Add an instrumentation regression test covering sequential add/remove, the concurrent-removal race, and the exact issue #2391 reproduction (fails before this change, passes after). Fixes #2391 --- .../data/repository/PlaylistSongCountTest.kt | 136 ++++++++++++++++++ .../PlaylistPreferencesRepository.kt | 73 ++++++---- 2 files changed, 184 insertions(+), 25 deletions(-) create mode 100644 app/src/androidTest/java/com/theveloper/pixelplay/data/repository/PlaylistSongCountTest.kt diff --git a/app/src/androidTest/java/com/theveloper/pixelplay/data/repository/PlaylistSongCountTest.kt b/app/src/androidTest/java/com/theveloper/pixelplay/data/repository/PlaylistSongCountTest.kt new file mode 100644 index 000000000..ed7e1eebc --- /dev/null +++ b/app/src/androidTest/java/com/theveloper/pixelplay/data/repository/PlaylistSongCountTest.kt @@ -0,0 +1,136 @@ +package com.theveloper.pixelplay.data.repository + +import android.content.Context +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.PreferenceDataStoreFactory +import androidx.datastore.preferences.preferencesDataStoreFile +import androidx.room.Room +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.theveloper.pixelplay.data.database.LocalPlaylistDao +import com.theveloper.pixelplay.data.database.PixelPlayDatabase +import com.theveloper.pixelplay.data.preferences.PlaylistPreferencesRepository +import com.theveloper.pixelplay.data.preferences.UserPreferencesRepository +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.test.runTest +import kotlinx.serialization.json.Json +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +/** + * Regression test for issue #2391: + * "Playlist song count doesn't update when removing songs — only when adding." + * + * Exercises the real PlaylistPreferencesRepository against an in-memory Room DB to + * verify that the song count exposed by userPlaylistsFlow (used by the Playlists menu) + * reflects removals as well as additions. + */ +@RunWith(AndroidJUnit4::class) +class PlaylistSongCountTest { + + private lateinit var db: PixelPlayDatabase + private lateinit var dao: LocalPlaylistDao + private lateinit var dataStore: DataStore + private lateinit var repo: PlaylistPreferencesRepository + + @Before + fun setup() { + val context = ApplicationProvider.getApplicationContext() + db = Room.inMemoryDatabaseBuilder(context, PixelPlayDatabase::class.java) + .addCallback(PixelPlayDatabase.createRuntimeArtifactsCallback()) + .allowMainThreadQueries() + .build() + dao = db.localPlaylistDao() + dataStore = PreferenceDataStoreFactory.create { + context.preferencesDataStoreFile("test_settings_${System.nanoTime()}") + } + val userPrefs = UserPreferencesRepository(dataStore, Json { ignoreUnknownKeys = true }) + repo = PlaylistPreferencesRepository(dao, userPrefs) + } + + @After + fun teardown() { + db.close() + } + + private suspend fun countFor(playlistId: String): Int = + repo.userPlaylistsFlow.first().first { it.id == playlistId }.songIds.size + + @Test + fun menuSongCount_reflectsAddAndRemove() = runTest { + val playlist = repo.createPlaylist(name = "J-Pop", songIds = listOf("10", "20", "30")) + assertEquals("initial count", 3, countFor(playlist.id)) + + // Remove a song — the bug report says this does NOT update the count. + repo.removeSongFromPlaylist(playlist.id, "20") + assertEquals("after removing one song", 2, countFor(playlist.id)) + + // Remove another. + repo.removeSongFromPlaylist(playlist.id, "30") + assertEquals("after removing a second song", 1, countFor(playlist.id)) + + // Adding works per the report — verify it still does. + repo.addSongsToPlaylist(playlist.id, listOf("40")) + assertEquals("after adding one song", 2, countFor(playlist.id)) + } + + /** + * Reproduces the real-world trigger for issue #2391: removing several songs in + * quick succession. Each edit does an unsynchronized read-modify-write + * (userPlaylistsFlow.first() -> modify -> updatePlaylist), so concurrent removals + * all read the same original list and the last writer wins, silently dropping the + * other removals. The Playlists-menu count (songIds.size) then stays stuck high. + */ + @Test + fun concurrentRemovals_doNotLoseUpdates() = runBlocking { + val playlist = repo.createPlaylist( + name = "Race", + songIds = listOf("1", "2", "3", "4", "5") + ) + assertEquals(5, countFor(playlist.id)) + + // Remove four songs concurrently — "remove one or two of them", fast. + coroutineScope { + listOf("1", "2", "3", "4").forEach { id -> + launch(Dispatchers.IO) { repo.removeSongFromPlaylist(playlist.id, id) } + } + } + + assertEquals("All concurrent removals must persist", 1, countFor(playlist.id)) + } + + /** + * Walks the exact reproduction from issue #2391, asserting the fixed behaviour: + * the song count stays accurate after a quick removal of "one or two" songs, and + * a later addition does not preserve a phantom difference. + */ + @Test + fun issue2391_quickRemoveThenAdd_keepsCountAccurate() = runBlocking { + // Steps 2-3: create a playlist and add a few songs. + val playlist = repo.createPlaylist( + name = "J-Pop", + songIds = listOf("1", "2", "3", "4", "5", "6") + ) + assertEquals(6, countFor(playlist.id)) + + // Step 4: remove one or two of them — quickly, as fast taps do. + coroutineScope { + launch(Dispatchers.IO) { repo.removeSongFromPlaylist(playlist.id, "2") } + launch(Dispatchers.IO) { repo.removeSongFromPlaylist(playlist.id, "4") } + } + // Step 5: the menu count must reflect BOTH removals (the bug left it stuck high). + assertEquals("count after removing two songs", 4, countFor(playlist.id)) + + // Steps 6-7: adding more must not carry over a phantom difference. + repo.addSongsToPlaylist(playlist.id, listOf("7", "8")) + assertEquals("count after adding two songs", 6, countFor(playlist.id)) + } +} diff --git a/app/src/main/java/com/theveloper/pixelplay/data/preferences/PlaylistPreferencesRepository.kt b/app/src/main/java/com/theveloper/pixelplay/data/preferences/PlaylistPreferencesRepository.kt index 647a68163..eb072ce1a 100644 --- a/app/src/main/java/com/theveloper/pixelplay/data/preferences/PlaylistPreferencesRepository.kt +++ b/app/src/main/java/com/theveloper/pixelplay/data/preferences/PlaylistPreferencesRepository.kt @@ -21,6 +21,11 @@ class PlaylistPreferencesRepository @Inject constructor( private val userPreferencesRepository: UserPreferencesRepository ) { private val migrationMutex = Mutex() + // Serializes read-modify-write edits to playlists. Without this, concurrent edits + // (e.g. removing several songs in quick succession) each read the same snapshot via + // userPlaylistsFlow.first() and the last writer wins, silently dropping the other + // edits — which left the Playlists-menu song count stuck high. See issue #2391. + private val editMutex = Mutex() @Volatile private var migrationChecked = false @@ -92,16 +97,26 @@ class PlaylistPreferencesRepository @Inject constructor( } suspend fun renamePlaylist(playlistId: String, newName: String) { - ensureMigratedIfNeeded() - val existing = userPlaylistsFlow.first().find { it.id == playlistId } ?: return - val updated = existing.copy( - name = newName, - lastModified = System.currentTimeMillis() - ) - localPlaylistDao.upsertPlaylist(updated.toEntity()) + editMutex.withLock { + ensureMigratedIfNeeded() + val existing = userPlaylistsFlow.first().find { it.id == playlistId } ?: return + val updated = existing.copy( + name = newName, + lastModified = System.currentTimeMillis() + ) + localPlaylistDao.upsertPlaylist(updated.toEntity()) + } } suspend fun updatePlaylist(playlist: Playlist) { + editMutex.withLock { + updatePlaylistLocked(playlist) + } + } + + // Persists a playlist and its songs. Caller must hold [editMutex] so the + // surrounding read-modify-write stays atomic. + private suspend fun updatePlaylistLocked(playlist: Playlist) { ensureMigratedIfNeeded() val updated = playlist.copy(lastModified = System.currentTimeMillis()) localPlaylistDao.upsertPlaylist(updated.toEntity()) @@ -109,10 +124,12 @@ class PlaylistPreferencesRepository @Inject constructor( } suspend fun addSongsToPlaylist(playlistId: String, songIdsToAdd: List) { - ensureMigratedIfNeeded() - val existing = userPlaylistsFlow.first().find { it.id == playlistId } ?: return - val merged = (existing.songIds + songIdsToAdd).distinct() - updatePlaylist(existing.copy(songIds = merged)) + editMutex.withLock { + ensureMigratedIfNeeded() + val existing = userPlaylistsFlow.first().find { it.id == playlistId } ?: return + val merged = (existing.songIds + songIdsToAdd).distinct() + updatePlaylistLocked(existing.copy(songIds = merged)) + } } suspend fun addOrRemoveSongFromPlaylists(songId: String, playlistIds: List): MutableList { @@ -137,15 +154,19 @@ class PlaylistPreferencesRepository @Inject constructor( } suspend fun removeSongFromPlaylist(playlistId: String, songIdToRemove: String) { - ensureMigratedIfNeeded() - val existing = userPlaylistsFlow.first().find { it.id == playlistId } ?: return - updatePlaylist(existing.copy(songIds = existing.songIds.filterNot { it == songIdToRemove })) + editMutex.withLock { + ensureMigratedIfNeeded() + val existing = userPlaylistsFlow.first().find { it.id == playlistId } ?: return + updatePlaylistLocked(existing.copy(songIds = existing.songIds.filterNot { it == songIdToRemove })) + } } suspend fun reorderSongsInPlaylist(playlistId: String, newSongOrderIds: List) { - ensureMigratedIfNeeded() - val existing = userPlaylistsFlow.first().find { it.id == playlistId } ?: return - updatePlaylist(existing.copy(songIds = newSongOrderIds)) + editMutex.withLock { + ensureMigratedIfNeeded() + val existing = userPlaylistsFlow.first().find { it.id == playlistId } ?: return + updatePlaylistLocked(existing.copy(songIds = newSongOrderIds)) + } } suspend fun setPlaylistSongOrderMode(playlistId: String, modeValue: String) = @@ -177,15 +198,17 @@ class PlaylistPreferencesRepository @Inject constructor( } suspend fun removeSongFromAllPlaylists(songId: String) { - ensureMigratedIfNeeded() - val playlists = userPlaylistsFlow.first() - playlists.forEach { playlist -> - if (songId in playlist.songIds) { - updatePlaylist( - playlist.copy( - songIds = playlist.songIds.filterNot { it == songId } + editMutex.withLock { + ensureMigratedIfNeeded() + val playlists = userPlaylistsFlow.first() + playlists.forEach { playlist -> + if (songId in playlist.songIds) { + updatePlaylistLocked( + playlist.copy( + songIds = playlist.songIds.filterNot { it == songId } + ) ) - ) + } } } } From 1bbfde2e473621b7448a022be50584be01081737 Mon Sep 17 00:00:00 2001 From: lostf1sh Date: Fri, 19 Jun 2026 17:58:48 +0300 Subject: [PATCH 63/72] Refine artist parsing and album display metadata --- .idea/deploymentTargetSelector.xml | 4 +- .../pixelplay/data/database/MusicDao.kt | 4 ++ .../pixelplay/data/gdrive/GDriveRepository.kt | 11 ++--- .../data/media/SongMetadataEditor.kt | 3 ++ .../preferences/UserPreferencesRepository.kt | 13 +++++- .../pixelplay/data/stream/CloudMusicUtils.kt | 12 +++--- .../data/worker/AlbumGroupingUtils.kt | 12 +++++- .../pixelplay/data/worker/SyncWorker.kt | 4 +- .../UserPreferencesRepositoryTest.kt | 28 +++++++++++++ .../data/stream/CloudMusicUtilsTest.kt | 23 +++++++++++ .../data/worker/AlbumGroupingUtilsTest.kt | 18 ++++++++ .../data/worker/ArtistParsingUtilsTest.kt | 41 +++++++++++++++++++ 12 files changed, 153 insertions(+), 20 deletions(-) create mode 100644 app/src/test/java/com/theveloper/pixelplay/data/stream/CloudMusicUtilsTest.kt diff --git a/.idea/deploymentTargetSelector.xml b/.idea/deploymentTargetSelector.xml index d8b7d40b0..c932c1186 100644 --- a/.idea/deploymentTargetSelector.xml +++ b/.idea/deploymentTargetSelector.xml @@ -2,7 +2,7 @@ - + - + diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/telegram/dashboard/TelegramDashboardScreen.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/telegram/dashboard/TelegramDashboardScreen.kt index 3f97e6fda..ebf271854 100644 --- a/app/src/main/java/com/theveloper/pixelplay/presentation/telegram/dashboard/TelegramDashboardScreen.kt +++ b/app/src/main/java/com/theveloper/pixelplay/presentation/telegram/dashboard/TelegramDashboardScreen.kt @@ -354,8 +354,8 @@ fun TelegramDashboardScreen( channelPendingRemoval = null }, colors = androidx.compose.material3.ButtonDefaults.filledTonalButtonColors( - containerColor = MaterialTheme.colorScheme.errorContainer, - contentColor = MaterialTheme.colorScheme.onErrorContainer + containerColor = MaterialTheme.colorScheme.error, + contentColor = MaterialTheme.colorScheme.onError ) ) { Text( From c800c1e17478ac5c0da773bdf51494b99a379165 Mon Sep 17 00:00:00 2001 From: Ayaan Date: Tue, 23 Jun 2026 17:51:22 +0530 Subject: [PATCH 70/72] fix(library): show home icon in folder breadcrumbs when at root level --- .../presentation/components/subcomps/LibraryActionRow.kt | 7 ++++++- 1 file changed, 6 insertions(+), 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 fd82b4fe9..fb04d8a3d 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 @@ -39,6 +39,7 @@ import androidx.compose.material.icons.rounded.Shuffle import androidx.compose.material.icons.rounded.FilterList import androidx.compose.material.icons.rounded.Cloud import androidx.compose.material.icons.rounded.Dataset +import androidx.compose.material.icons.rounded.Home import androidx.compose.material.icons.rounded.PhoneAndroid import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.ExperimentalMaterial3Api @@ -451,7 +452,11 @@ fun Breadcrumbs( modifier = Modifier.size(36.dp), enabled = currentFolder != null ) { - Icon(Icons.AutoMirrored.Rounded.ArrowBack, contentDescription = stringResource(R.string.common_back)) + val icon = if (currentFolder == null) Icons.Rounded.Home else Icons.AutoMirrored.Rounded.ArrowBack + Icon( + imageVector = icon, + contentDescription = stringResource(if (currentFolder == null) R.string.nav_bar_home else R.string.common_back) + ) } Spacer(Modifier.width(8.dp)) From fa51e4d0ea006c7f6a029ffd0de27358f9e40db4 Mon Sep 17 00:00:00 2001 From: Shinichi Fujimoto Date: Thu, 25 Jun 2026 15:33:16 +0900 Subject: [PATCH 71/72] feat(i18n): add missing Japanese translations for About and Volume settings Co-Authored-By: Claude Sonnet 4.6 --- app/src/main/res/values-ja/strings_settings.xml | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/app/src/main/res/values-ja/strings_settings.xml b/app/src/main/res/values-ja/strings_settings.xml index ee41a6b0d..d48dfc9c8 100644 --- a/app/src/main/res/values-ja/strings_settings.xml +++ b/app/src/main/res/values-ja/strings_settings.xml @@ -634,8 +634,19 @@ オープンソースコントリビューター GitHub からのライブコントリビューターリスト。 %1$d 回のコントリビューション + GitHub + リポジトリ + Telegram + サポート + GitHub リポジトリを開く + Telegram コミュニティに参加 GitHub プロフィールを開く Telegram を開く %1$s のアバター %1$s のアイコン + + + 音量 + 音量ゼロで一時停止 + 音量が 0 に設定されたとき、再生を自動的に一時停止します。 From ff6901b62a75b9c83b721562d8df03c51b5d352f Mon Sep 17 00:00:00 2001 From: Shinichi Fujimoto Date: Thu, 25 Jun 2026 15:36:48 +0900 Subject: [PATCH 72/72] fix(i18n): keep Material 3 Expressive in English in Japanese locale Co-Authored-By: Claude Sonnet 4.6 --- app/src/main/res/values-ja/strings_settings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/res/values-ja/strings_settings.xml b/app/src/main/res/values-ja/strings_settings.xml index d48dfc9c8..294ef388c 100644 --- a/app/src/main/res/values-ja/strings_settings.xml +++ b/app/src/main/res/values-ja/strings_settings.xml @@ -625,7 +625,7 @@ バージョン v%1$s オープンソース コミュニティファースト - Material 3 エクスプレッシブ + Material 3 Expressive 現在コントリビューターが見つかりません。後でもう一度お試しください。 メンテナー PixelPlayer の開発者。