From 13aaad98d0f3fa32449d74f262275fde6eb5adc3 Mon Sep 17 00:00:00 2001 From: Rebornloki Date: Sat, 13 Jun 2026 09:39:20 +0100 Subject: [PATCH 01/99] 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/99] 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/99] 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/99] 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/99] 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/99] 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/99] 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/99] 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/99] 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/99] 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/99] 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/99] 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/99] 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 c0063e7be9fb281063a00483ccf0d395f6c8048d Mon Sep 17 00:00:00 2001 From: adlifarizi Date: Sat, 13 Jun 2026 21:15:28 +0700 Subject: [PATCH 14/99] feat: add Arabic language support and restructure localized strings for the Arabic resource directory --- .../pixelplay/data/preferences/AppLanguage.kt | 3 +- app/src/main/res/values-ar/plurals.xml | 75 -- app/src/main/res/values-ar/strings.xml | 335 +++---- app/src/main/res/values-ar/strings_auth.xml | 74 -- .../main/res/values-ar/strings_changelogs.xml | 150 +++ .../res/values-ar/strings_cloud_services.xml | 236 +++++ .../main/res/values-ar/strings_components.xml | 190 ---- .../main/res/values-ar/strings_equalizer.xml | 57 ++ .../res/values-ar/strings_home_screen.xml | 281 ++++++ .../main/res/values-ar/strings_library.xml | 598 ++++++++++++ app/src/main/res/values-ar/strings_player.xml | 210 +++++ .../strings_presentation_batch_a.xml | 17 - .../strings_presentation_batch_b.xml | 88 -- .../strings_presentation_batch_c.xml | 83 -- .../strings_presentation_batch_d.xml | 124 --- .../strings_presentation_batch_e.xml | 160 ---- .../strings_presentation_batch_f.xml | 239 ----- .../strings_presentation_batch_g.xml | 636 ------------- .../main/res/values-ar/strings_screens.xml | 418 ++++----- .../main/res/values-ar/strings_settings.xml | 872 ++++++++++++------ app/src/main/res/values-ar/strings_widget.xml | 17 + app/src/main/res/values/strings_settings.xml | 1 + 22 files changed, 2471 insertions(+), 2393 deletions(-) delete mode 100644 app/src/main/res/values-ar/plurals.xml delete mode 100644 app/src/main/res/values-ar/strings_auth.xml create mode 100644 app/src/main/res/values-ar/strings_changelogs.xml create mode 100644 app/src/main/res/values-ar/strings_cloud_services.xml delete mode 100644 app/src/main/res/values-ar/strings_components.xml create mode 100644 app/src/main/res/values-ar/strings_equalizer.xml create mode 100644 app/src/main/res/values-ar/strings_home_screen.xml create mode 100644 app/src/main/res/values-ar/strings_library.xml create mode 100644 app/src/main/res/values-ar/strings_player.xml delete mode 100644 app/src/main/res/values-ar/strings_presentation_batch_a.xml delete mode 100644 app/src/main/res/values-ar/strings_presentation_batch_b.xml delete mode 100644 app/src/main/res/values-ar/strings_presentation_batch_c.xml delete mode 100644 app/src/main/res/values-ar/strings_presentation_batch_d.xml delete mode 100644 app/src/main/res/values-ar/strings_presentation_batch_e.xml delete mode 100644 app/src/main/res/values-ar/strings_presentation_batch_f.xml delete mode 100644 app/src/main/res/values-ar/strings_presentation_batch_g.xml create mode 100644 app/src/main/res/values-ar/strings_widget.xml 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..fb51de77b 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,7 +16,8 @@ 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), - TURKISH("tr", R.string.settings_language_turkish); + TURKISH("tr", R.string.settings_language_turkish), + ARABIC("ar", R.string.settings_language_arabic); companion object { val supportedLanguageTags: Set = values().map { it.tag }.toSet() diff --git a/app/src/main/res/values-ar/plurals.xml b/app/src/main/res/values-ar/plurals.xml deleted file mode 100644 index 038673ef3..000000000 --- a/app/src/main/res/values-ar/plurals.xml +++ /dev/null @@ -1,75 +0,0 @@ - - - - لا يتم مشاركة أي قوائم تشغيل - جاري مشاركة قائمة تشغيل واحدة - جاري مشاركة قائمتي تشغيل - جاري مشاركة %d قوائم تشغيل - جاري مشاركة %d قائمة تشغيل - جاري مشاركة %d قائمة تشغيل - - - لم يتم تصدير أي قوائم تشغيل إلى %2$s - تم تصدير قائمة تشغيل واحدة إلى %2$s - تم تصدير قائمتي تشغيل إلى %2$s - تم تصدير %1$d قوائم تشغيل إلى %2$s - تم تصدير %1$d قائمة تشغيل إلى %2$s - تم تصدير %1$d قائمة تشغيل إلى %2$s - - - لم يتم إضافة أي أغنية إلى قائمة الانتظار - تمت إضافة أغنية واحدة إلى قائمة الانتظار - تمت إضافة أغنيتين إلى قائمة الانتظار - تمت إضافة %d أغانٍ إلى قائمة الانتظار - تمت إضافة %d أغنية إلى قائمة الانتظار - تمت إضافة %d أغنية إلى قائمة الانتظار - - - لن يتم تشغيل أي أغنية تالياً - سيتم تشغيل أغنية واحدة تالياً - سيتم تشغيل أغنيتين تالياً - سيتم تشغيل %d أغانٍ تالياً - سيتم تشغيل %d أغنية تالياً - سيتم تشغيل %d أغنية تالياً - - - لم يتم إضافة أي أغنية إلى المفضلة - تمت إضافة أغنية واحدة إلى المفضلة - تمت إضافة أغنيتين إلى المفضلة - تمت إضافة %d أغانٍ إلى المفضلة - تمت إضافة %d أغنية إلى المفضلة - تمت إضافة %d أغنية إلى المفضلة - - - لم يتم إزالة أي أغنية من المفضلة - تمت إزالة أغنية واحدة من المفضلة - تمت إزالة أغنيتين من المفضلة - تمت إزالة %d أغانٍ من المفضلة - تمت إزالة %d أغنية من المفضلة - تمت إزالة %d أغنية من المفضلة - - - لم يتم حذف أي ملف - تم حذف ملف واحد - تم حذف ملفين - تم حذف %d ملفات - تم حذف %d ملفاً - تم حذف %d ملفاً - - - هل تريد حذف الملفات؟ - هل تريد حذف أغنية واحدة؟ - هل تريد حذف أغنيتين؟ - هل تريد حذف %d أغانٍ؟ - هل تريد حذف %d أغنية؟ - هل تريد حذف %d أغنية؟ - - - لم يتم تحديد أي مسار - تم تحديد مسار واحد - تم تحديد مسارين - تم تحديد %d مسارات - تم تحديد %d مساراً - تم تحديد %d مساراً - - diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index 04844b159..87194b4a5 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -1,247 +1,128 @@ + PixelPlayer + مشغل موسيقى تغيير اسم التطبيق لقد قمنا بتغيير اسم تطبيقنا من PixelPlay إلى PixelPlayer بسبب مشكلة تتعلق بالعلامة التجارية. استمتع بالاستماع! - لا تظهره مرة أخرى - تجاهل + لا تظهره مرة أخرى + + + الرئيسية + البحث + المكتبة + + مطلوب إذن خاص لتعديل البيانات الوصفية للأغاني (ملفات .mp3)، يحتاج PixelPlayer إلى صلاحية وصول خاصة لجميع الملفات. يتيح لنا هذا تعديل علامات المسارات الصوتية مباشرة. يرجى منح هذا الإذن في الشاشة التالية لتفعيل تعديل البيانات الوصفية. منح الإذن - الوصول إلى جميع الملفات - خطأ - موافق - إلغاء - استيراد - بحث - - كلمات الأغنية - إغلاق قائمة كلمات الأغنية - جاري تحميل كلمات الأغنية… - تعذر العثور على كلمات لهذه الأغنية. - الكلمات مقدمة من - https://lrclib.net/ - لم يتم العثور على كلمات الأغنية - هل تود البحث عن كلمات الأغنية عبر الإنترنت؟ - لم نتمكن من العثور على كلمات الأغنية تلقائياً. يمكنك تعديل العنوان أو اسم الفنان والمحاولة بالبحث يدوياً. - فشل البحث عن كلمات الأغنية - فشل جلب كلمات الأغنية من الخادم - انتهت مهلة الاتصال. يرجى التحقق من اتصالك بالإنترنت. - خطأ في الشبكة. يرجى التحقق من اتصالك بالإنترنت. - خطأ في الخادم (رمز %d). يرجى المحاولة مرة أخرى لاحقاً. - تم العثور على %d من التطابقات - تم البحث عن \"%s\" - جاري البحث عن كلمات الأغنية… - كلمات الأغنية متوفرة بالفعل. تم تخطي الجلب عبر الإنترنت. - الكلمات المضمنة موجودة بالفعل. تم تخطي الجلب عبر الإنترنت. - ملف الكلمات المحلي (.lrc) موجود بالفعل. تم تخطي الجلب عبر الإنترنت. - إظهار خيارات كلمات الأغنية - فتح أداة الاختيار دائماً بدلاً من التطبيق التلقائي لأول تطابق - حفظ كلمات الأغنية كملف .lrc - حفظ كلمات الأغنية - اختر النسخة المراد حفظها: - مزامنة (مع الطوابع الزمنية) - عادية (نص فقط) - تم حفظ كلمات الأغنية بنجاح - فشل حفظ كلمات الأغنية - لا توجد كلمات أغنية متاحة لحفظها - إعادة تعيين الكلمات المستوردة - إزاحة مزامنة الكلمات - %+.1fs - إعادة تعيين - أبكر - أقرب - جاري فحص ملفات الموسيقى… - جاري معالجة الملفات… - %1$d من أصل %2$d ملفاً - جاري مزامنة المكتبة… - اكتملت المزامنة - انتظار… - جاري مزامنة المكتبة… - جاري الإنهاء في الخلفية… - جاري فحص كلمات الأغاني… - جاري تنظيف ذاكرة التخزين المؤقت لأغلفة الألبومات… - جاري المزامنة مع المصادر السحابية… - مسار مجهول - فنان مجهول - ألبوم مجهول - اختر فناناً - افتح أي فنان منسوب إليه هذا المسار. - فنان واحد - %1$d فنانين - الفنان الرئيسي - صفحة الفنان + تشغيل سريع تعذر فتح ملف الصوت هذا. - فتح المشغل الكامل - إغلاق المشغل العائم - إغلاق المشغل - المسار السابق - المسار التالي - إيقاف مؤقت - تشغيل - لم يتم العثور على قائمة التشغيل. - القرص %d - - يرجى تكوين مفتاح API صالح لمزود الذكاء الاصطناعي المحدد في الإعدادات. - خطأ في الذكاء الاصطناعي: %s - رفض مزود الذكاء الاصطناعي المحدد الطلب لعدم وجود رصيد أو حصة متاحة في الحساب. - لم يعد نموذج الذكاء الاصطناعي المحدد متاحاً. حاول PixelPlayer التبديل تلقائياً إلى نموذج مدعوم. - لم يتمكن الذكاء الاصطناعي من العثور على أي أغانٍ بناءً على طلبك. - اكتب فكرة لـ "مزيجك اليومي" - تم تحديث المزيج اليومي بواسطة الذكاء الاصطناعي - لم يتمكن الذكاء الاصطناعي من العثور على أغانٍ لهذا المزيج - - الترجمة بواسطة الذكاء الاصطناعي - تحتوي كلمات هذه الأغنية على ترجمة بالفعل - كلمات هذه الأغنية مكتوبة بهذه اللغة بالفعل - لم يتم تكوين مفتاح الـ API - تمت ترجمة كلمات الأغنية بنجاح! - جاري ترجمة الكلمات عبر الذكاء الاصطناعي... + فتح المشغل الكامل + تشغيل عشوائي تشغيل عشوائي لجميع الأغاني - قائمة التشغيل - آخر قائمة تشغيل تم تشغيلها - تشغيل عشوائي للكل آخر قائمة تشغيل لا توجد قائمة تشغيل متاحة لفتحها - معرف الألبوم غير صالح - لم يتم العثور على معرف الألبوم - خطأ أثناء تحميل بيانات الألبوم: %s - لم يتم العثور على الألبوم - تعذر التحديث: %s - معرف الفنان غير صالح - لم يتم العثور على معرف الفنان - خطأ أثناء تحميل بيانات الفنان: %s - تعذر العثور على الفنان - لم يتم العثور على أغانٍ صالحة للتشغيل - - أداة ذكية مستجيبة تتكيف مع حجمها - شريط مشغل مضغوط - عناصر تحكم كاملة مع التشغيل العشوائي والتكرار - مشغل مربع بسيط + + فتح متجر Play + متابعة النسخة التجريبية + سيتم تفعيل رابط متجر Play من تكوين GitHub. + PixelPlayer متاح الآن على Google Play + استخدم القناة المستقرة على Google Play للحصول على التحديثات الرسمية بينما نبقي البناء التجريبي نشطاً. + PixelPlayer + إعلان الإصدار + قريباً + + + شكراً لك على استخدام PixelPlayer! + أعلى %1$d + إغلاق + النتيجة + المستوى %1$d + القلوب + اكتمل المستوى! + انتهت اللعبة + النتيجة: %1$d + المحاولة مجدداً؟ + المستوى التالي + إعادة تشغيل اللعبة + اضغط لإعادة الإطلاق + تشغيل موسيقى عشوائية + كسارة الطوب + أعلى نتيجة %1$d + لعب + اسحب لتحريك المضرب + + + إغلاق المشغل جاري معالجة إجراء التشغيل… - - لا توجد قوائم تشغيل لمشاركتها - مشاركة قوائم التشغيل - فشلت المشاركة: %1$s - لا توجد قوائم تشغيل لتصديرها - فشل التصدير: %1$s - Music/PixelPlayer Exports - يرجى تكوين مفتاح Gemini API في الإعدادات. - خطأ غير معروف - - جاري إرسال %1$d من الأغاني إلى الساعة - جاري الإرسال إلى الساعة - اكتمل النقل - فشل النقل - تم إلغاء النقل - جاري تحضير النقل للساعة - عمليات نقل عدد %1$d - جاري بدء النقل… - توجد عدة عمليات نقل نشطة - جاري تحضير النقل… - جاري النقل - مكتمل - فاشل - ملغي - جاري التحضير - جاري البدء - عمليات نقل الساعة - يعرض التقدم المباشر لنقل الموسيقى من الهاتف إلى الساعة - - خادم وسائط البث (Cast) - جاري البث إلى الجهاز - تزويد جهاز البث بالوسائط - %1$s: %2$s - التقديم والتأخير غير متاح مؤقتاً لصيغة الصوت هذه أثناء البث لأنها قد تتسبب في تعطل جلسة البث. - - نسخة احتياطية غير صالحة: %1$s - جاري تحضير الاستعادة - جاري بدء مهمة الاستعادة. - جاري تحضير النسخ الاحتياطي - جاري بدء مهمة النسخ الاحتياطي. - تم استعادة النسخة الاحتياطية بنجاح - اكتملت الاستعادة مع وجود بعض المشكلات غير المحلولة. - تعذر إكمال الاستعادة: %1$s - فشلت الاستعادة: %1$s - تم تصدير البيانات بنجاح - فشل التصدير: %1$s - تم استعادة البيانات بنجاح - اكتملت الاستعادة مع وجود مشكلات غير محلولة. الفشل: %1$s - فشل تحميل النماذج - تم إطلاق تعطل تجريبي من خيارات المطور - هذا الإجراء مقصود لاختبار نظام تقارير الأعطال - - لم يتم العثور على الأغنية في القائمة الحالية - تعذر تحديد موقع الأغنية - لم يتم العثور على أغانٍ في المكتبة - توقف التشغيل: انتهى %1$s (نهاية المسار). - مسار - لا توجد أغانٍ لتشغيلها عشوائياً. - الألبومات المحددة - لم يتم العثور على أغانٍ قابلة للتشغيل في الألبومات المحددة - تم إدراج أول %1$d ألبومات فقط في قائمة الانتظار - تم إدراج %1$d ألبومات في قائمة الانتظار (%2$d أغنية) - تعذر إدراج الألبومات المحددة في قائمة الانتظار - جميع الأغاني موجودة بالفعل في المفضلة - لم تكن هناك أي أغانٍ في المفضلة - جاري إنشاء ملف ZIP… - فشلت المشاركة: %1$s - لا يمكن حذف الأغنية التي يتم تشغيلها حالياً - تم حذف %1$d ملفات (تم تخطي %2$d - قيد التشغيل) - تم حذف %1$d من أصل %2$d ملفاً - فشل حذف الملفات - تم حذف الملف - تعذر حذف الملف أو أن الملف غير موجود - تم إلغاء الحذف - تم رفض الإذن - لا يمكن تعديل الملفات - تم رفض الإذن - لا يمكن حفظ كلمات الأغنية - تم رفض الإذن - لا يمكن تعديل هذا الملف - تم تحديث البيانات الوصفية بنجاح - جاري تحديث %1$d من الأغاني… - تم تحديث %1$d من الأغاني بنجاح! - تم تحديث %1$d أغنية. فشل: %2$d - تم استعادة قائمة التشغيل - سيتم حذف هذه الأغاني نهائياً من جهازك ولا يمكن استعادتها. - حذف - - %1$d دقائق - نهاية المسار - تم ضبط المؤقت لمدة %1$d دقائق. - تم إلغاء المؤقت. - لا يمكن تفعيل خيار نهاية المسار: لا توجد أغنية نشطة حالياً. - تم إلغاء تفعيل مؤقت نهاية المسار: تغيرت الأغنية من %1$s إلى %2$s. - سيتوقف التشغيل عند نهاية المسار. - المسار السابق - المسار الحالي - مؤقت النوم - المؤقت - نهاية المسار الحالي - وقت مخصص - إلغاء المؤقت - ضبط مدة مخصصة - عدد مرات التشغيل: %1$s - مرة واحدة - تشغيل المفتاح - %1$d%% - إصدار %1$d - %1$s %2$s - - تعديل %d أغنية - سيتم تحديث الحقول المعدلة فقط. اترك الحقول فارغة للاحتفاظ بالقيم الحالية. - (قيم مختلطة) - (اختياري - اتركه فارغاً للتخطي) - تم تحديث %d أغنية بنجاح - تم تحديث %1$d من أصل %2$d أغنية. تعذر تعديل بعض الملفات. - فشل تحديث الأغاني - - تعديل غلاف المجموعة - سيؤدي هذا إلى استبدال غلاف الألبوم لجميع الأغاني المحددة وعددها %d - تعيين غلاف الألبوم للكل - إزالة جميع أغلفة الألبومات - (أغلفة متعددة مختلفة) خطأ في التشغيل: %1$s + + + رجوع + موافق + إلغاء + تجاهل + خطأ + البحث + مسح البحث + الكل + تأكيد + تم الحفظ! + محدد + %1$d%% + الفنان + تحديد الكل + مسح + خطأ غير معروف + + + حفظ + تم + إعادة تعيين + تطبيق + خلط عشوائي + نسخ + مشاركة + تراجع + استيراد + حذف + تصدير + دمج + إعادة تسمية + إنشاء + كلمات الأغنية + الإعدادات + غلاف الألبوم + قائمة التشغيل + مسار مجهول + فنان مجهول + ألبوم مجهول + إغلاق + إضافة + إزالة + تشغيل + المسار السابق + المسار التالي + المفضلة + إيقاف مؤقت + تكرار + خيارات + خلط عشوائي + مزيد من الخيارات لـ %1$s + توسيع القائمة + التالي + إنهاء + إعادة تعيين للوضع الافتراضي + تصدير الكل + دمج الكل + مشاركة الكل + تشغيل الألبوم + تشغيل الألبوم عشوائياً + غلاف الألبوم لـ %1$s diff --git a/app/src/main/res/values-ar/strings_auth.xml b/app/src/main/res/values-ar/strings_auth.xml deleted file mode 100644 index e6ea6fed8..000000000 --- a/app/src/main/res/values-ar/strings_auth.xml +++ /dev/null @@ -1,74 +0,0 @@ - - - - رجوع - إظهار كلمة المرور - إخفاء كلمة المرور - جاري الاتصال… - اتصال - تفاصيل الاتصال - أدخل رابط الخادم (URL) وبيانات اعتماد الحساب. - رابط الخادم (URL) - اسم المستخدم - كلمة المرور - أدخل كلمة المرور - admin - مرحباً، %1$s! - - - Subsonic / Navidrome - اتصل بخادم الموسيقى المستضاف ذاتياً - يدعم خوادم Navidrome وAirsonic وGonic وAmpache والخوادم الأخرى المتوافقة مع واجهة برمجة تطبيقات Subsonic. - https://music.example.com - استخدم عنوان الخادم الكامل الذي يبدأ بـ //:https. - هذا هو اسم حسابك على Subsonic أو Navidrome. - كلمة مرور التطبيق (App password) تعمل أيضاً إذا كان خادمك يدعمها. - كلمة مرور التطبيق (App password) تعمل أيضاً إذا كان خادمك يدعمها. - ملء تلقائي لـ //:https - متوافق مع Navidrome وGonic وAirsonic والخوادم الأخرى المتوافقة مع Subsonic - شعار Navidrome - شعار Subsonic - - - Jellyfin - يتصل بخوادم Jellyfin. يتم دعم كل من HTTP وHTTPS للوصول عبر الشبكة المحلية. - اتصل بخادم وسائط Jellyfin الخاص بك - أدخل رابط خادم Jellyfin وبيانات اعتماد الحساب. - http://192.168.1.100:8096 - الرابط الكامل لخادم Jellyfin الخاص بك، شاملاً منفذ الاتصال (Port). - اسم المستخدم لحساب Jellyfin الخاص بك. - كلمة المرور لحساب Jellyfin الخاص بك. - ملء تلقائي لـ //:http - يتصل بخوادم Jellyfin لبث مكتبتك الموسيقية - شعار Jellyfin - - - تم توصيل Google Drive بنجاح! - Google Drive - - - الخروج من تسجيل دخول NetEase؟ - الخروج من تسجيل دخول QQ Music؟ - يمكنك العودة لاحقاً. سيتم تجاهل حالة الصفحة الحالية عند الإغلاق. - خروج - بقاء - تسجيل الدخول إلى NetEase Music - تسجيل الدخول إلى QQ Music - رجوع للخلف في الويب - تقدم للأمام في الويب - تحديث - فتح الصفحة الرئيسية - جاري الحفظ… - تم - إعادة المحاولة - + - انتهت مهلة تحميل الصفحة. يمكنك إعادة المحاولة دون فقدان تقدمك. - تعذر قراءة ملفات تعريف الارتباط (Cookies) الخاصة بالجلسة. - تستغرق الصفحة وقتاً طويلاً للتحميل. استخدم التحديث أو جرب شبكة أخرى. - فشل تحميل WebView. - خطأ HTTP %1$d أثناء تحميل NetEase. - خطأ HTTP %1$d أثناء تحميل QQ Music. - لم يتم العثور على ملفات تعريف الارتباط. سجل الدخول أولاً. - لم يتم رصد تسجيل الدخول بعد. أكمل تسجيل الدخول إلى NetEase قبل الضغط على "تم". - لم يتم رصد تسجيل الدخول بعد. أكمل تسجيل الدخول إلى QQ Music قبل الضغط على "تم". - diff --git a/app/src/main/res/values-ar/strings_changelogs.xml b/app/src/main/res/values-ar/strings_changelogs.xml new file mode 100644 index 000000000..23a16e194 --- /dev/null +++ b/app/src/main/res/values-ar/strings_changelogs.xml @@ -0,0 +1,150 @@ + + + سجل التغييرات + عرض على GitHub + التحسينات + الإصلاحات + ما الجديد + تمت إضافة + + دعم Chromecast لبث الصوت من جهازك. + سجل التغييرات داخل التطبيق لإبقائك على اطلاع بآخر الميزات. + دعم ملفات LRC، سواء كانت مدمجة أو خارجية. + دعم كلمات الأغاني دون اتصال بالإنترنت. + كلمات أغاني متزامنة (متطابقة مع الأغنية). + شاشة جديدة لعرض كامل قائمة الانتظار. + إعادة ترتيب وإزالة الأغاني من قائمة الانتظار. + إيماءات المشغل المصغر (السحب للأسفل للإغلاق). + إضافة المزيد من رسوم Material المتحركة. + إعدادات جديدة لتخصيص المظهر والإحساس العام. + إعدادات جديدة لمسح ذاكرة التخزين المؤقت. + + + إعادة تصميم كاملة لواجهة المستخدم. + إعادة تصميم كاملة للمشغل. + تحسينات الأداء في المكتبة الموسيقية. + تحسين سرعة تشغيل التطبيق عند البدء. + الذكاء الاصطناعي يقدم الآن نتائج أفضل. + + + إصلاح أخطاء مختلفة في محرر العلامات (Tags). + إصلاح مشكلة عدم اختفاء إشعار التشغيل. + إصلاح عدة أخطاء كانت تتسبب في توقف التطبيق فجأة. + + + تقديم مركز إحصائيات استماع أكثر ثراءً مع رؤى عميقة لجلساتك. + إطلاق مشغل سريع عائم لفتح ومعاينة الملفات المحلية على الفور. + إضافة تبويب المجلدات مع مستكشف بنمط شجري وعرض جاهز لقوائم التشغيل. + + + تحسين واجهة Material 3 بالكامل لتوفير تجربة أنظف وأكثر تماسكاً. + تحرير البيانات الوصفية يدعم الآن تغيير غلاف الألبوم. + تنعيم الرسوم المتحركة والانتقالات عبر التطبيق لتنقل أكثر انسيابية. + تحسين تخطيط شاشة الفنان مع تفاصيل أكثر ثراءً ولمسات جمالية. + ترقية توليد DailyMix و YourMix باختيارات أكثر ذكاءً وتنوعاً. + تعزيز توليد قوائم التشغيل بواسطة الذكاء الاصطناعي. + تحسين صلة نتائج البحث وعرضها لاكتشاف أسرع. + توسيع الدعم لنطاق أوسع من تنسيقات الملفات الصوتية. + + + حل مشكلات البيانات الوصفية الغريبة لتبقى تفاصيل الأغاني دقيقة في كل مكان. + استعادة اختصارات الإشعارات لتعود بشكل موثوق إلى شاشة التشغيل. + + + إعادة تصميم كبرى لنظام التنقل + مستكشف ملفات جديد لاختيار مجلدات المصدر + وظائف اتصال وبث جديدة + استمرارية سلسة بين الأجهزة عن بعد + انتقال بدون فجوات (Gapless) بين الأغاني + عنصر التحكم في التلاشي المتبادل (Crossfade) + ميزة الانتقالات المخصصة الجديدة (لقوائم التشغيل فقط) + استمرار التشغيل بعد إغلاق التطبيق + تحسينات واجهة المستخدم + ميزة إحصائيات محسنة + إعادة تصميم التحكم في قائمة الانتظار مع المزيد من الميزات + تحسين دعم أنواع الملفات المختلفة للتشغيل وتعديل البيانات الوصفية + تحسين متحكم الأذونات + إصلاحات طفيفة للأخطاء + + + تحديث واجهة المستخدم التعبيرية Material 3 Expressive + معادل صوتي ذو 10 نطاقات وتأثيرات صوتية + تدفق مزامنة جديد للمكتبة الموسيقية + التكامل مع الذكاء الاصطناعي (نماذج Gemini) + استيراد وتصدير قوائم التشغيل بصيغة M3U + تكامل أغلفة الفنانين من منصة Deezer + أغلفة مخصصة لقوائم التشغيل + إعادة هيكلة معمارية الإعدادات + رسوم متحركة جديدة لقائمة الانتظار والمشغل + ملفات التعريف الأساسية (Baseline Profiles) وتحسين الأداء + نظام أفضل لكلمات الأغاني مع إزاحة التزامن + + + تحسينات استقرار بث الصوت (Casting) + استقرار لوحة المشغل السفلية + إصلاحات عامة للأخطاء وتنظيف الكود + + + دعم Android Auto متاح الآن للتشغيل داخل السيارة. + دعم Wear OS بات نشطاً، بما في ذلك عناصر تحكم أفضل للتشغيل من الساعة إلى الهاتف. + توسيع التكامل السحابي مع تحسينات لـ Telegram و NetEase و QQ Music و Google Drive. + ميزتا "المشغلة حديثاً" واستعادة قائمة الانتظار الدائمة تبقيان جلسة استماعك جاهزة. + تم تضمين ميزات النسخ الاحتياطي والاستعادة v3 وأدوات إدارة الحساب. + أصبحت كلمات الأغاني أكثر ذكاءً مع دعم البحث اليدوي الاحتياطي وتحسينات التخزين. + + + تحديث شامل للأداء عبر بدء التشغيل، المكتبة، قائمة الانتظار، وتفاعلات المشغل. + إعادة تصميم واجهات المشغل، البث، الكلمات، الفنان، والنوع لتوفير استخدام أكثر سلاسة. + أصبحت تدفقات التنقل والبحث أكثر موثوقية مع معالجة أكثر أماناً للمسارات. + تحسين توافق تشغيل الصوت لمزيد من الأجهزة والتنسيقات. + توسيع سير عمل التحديد المتعدد عبر الأغاني والألبومات وقوائم التشغيل. + + + أصبح سلوك قائمة الانتظار والخلط العشوائي أكثر استقراراً وقابلية للتنبؤ. + إصلاح العديد من الحالات النادرة في التشغيل الخلفي وبث الصوت (Casting). + إصلاح مشكلات مؤقت النوم، والتنقل في تبويب الملفات، وحالات توقف فنان الألبوم المفاجئ. + تحسين تحميل الويدجت واستقرار الخدمة لتقليل مشكلات الحرارة والذاكرة. + إصلاحات عامة للأخطاء وتحسينات جمالية لواجهة مستخدم التطبيق. + + + Wear OS: نقل الموسيقى، التشغيل المحلي، مزامنة قائمة الانتظار، والتحكم عن بعد من الساعة. + الذكاء الاصطناعي: تكامل Groq AI و OpenRouter (تجريبي) مع تحسين استهلاك الرموز (Tokens). + السحاب: إضافة دعم Jellyfin. + كلمات الأغاني: ترجمة متزامنة مع مفتاح تبديل مخصص، دعم تنسيق Kugou LRC، تخصيص محاذاة النص، وتحسين التحميل عن بعد. + واجهة المستخدم/تجربة المستخدم: وضع شريط التنقل المدمج، سمات ديناميكية من لوحة ألوان غلاف الألبوم، نص متحرك (Marquee) للعناوين الطويلة، وخيارات فرز جديدة. + تليجرام: دعم أصلي للمواضيع (Topics) وأنماط عرض محسنة. + + + المحرك الصوتي: إصلاح شامل مع دعم المزيد من التنسيقات (MIDI, ALAC, M4A) وتحسين برنامج فك الترميز. + الكفاءة: تقليل جذري في استهلاك الطاقة، إصلاحات للحرارة الزائدة، وتحسين المهام الخلفية (SyncWorker). + قاعدة البيانات: تحسينات هائلة على الاستعلامات وإعادة تصميم ذاكرة التخزين المؤقت للأغلفة لمنع فقدان البيانات. + بدء التشغيل: تحسين وقت التحميل عبر تهيئة Baseline Profile. + + + التشغيل: إصلاح التقطع في Opus/MP3، أخطاء ReplayGain أثناء التلاشي المتبادل، ومشكلات بدء التشغيل على مفككات ترميز Samsung. + الاستقرار: القضاء على حالات التوقف المفاجئ عند البدء، وأثناء التنقل بين الفنانين، وعلى أجهزة أندرويد 12+. + واجهة المستخدم: إصلاح وميض الأغلفة، وتداخل النصوص في النصوص غير اللاتينية، وسلوك شريط التنقل/المشغل المصغر. + الأمان: تعزيز التعامل مع بيانات الاعتماد، أذونات التخزين، والاتصال بخادم الوسائط. + + + التوطين: الإسبانية، الفرنسية، الروسية، الصينية المبسطة، الإندونيسية، الإيطالية + + + تكامل Google Drive مع إدارة دورة حياة المشغل. + تعديل البيانات الوصفية للأغاني دفعة واحدة (العلامات وغلاف الألبوم). + ترجمة كلمات الأغاني بالذكاء الاصطناعي مع تفضيلات Wear OS القابلة للتخصيص. + أداة تشخيص البطء والتحديد المتعدد في شاشة البحث. + دعم اللغتين العربية والتركية، مع خيارات الشبكة المحلية لعناوين URL (HTTP) الموطنة. + + + توفير جذري في طاقة البطارية (الإرسال المباشر للصوت وبوابات استقصاء واجهة المستخدم). + تحسين إدارة قائمة الانتظار (إضافة أسرع وفهرسة صريحة). + رسوم متحركة تعبيرية من Material 3 لشاشات الانتقال. + إعادة هيكلة مزامنة المكتبة عبر فحص مقيد السرعة. + + + حل مشاكل التقطع/التخطي وتأخر التشغيل ومشكلات التخزين المؤقت. + إصلاح مزامنة حذف الأغاني الخارجية واتساق البيانات الوصفية. + إصلاح مشكلات الذاكرة، وحالات التوقف المفاجئ، وأخطاء التخطيط على Wear OS والهاتف. + + \ No newline at end of file diff --git a/app/src/main/res/values-ar/strings_cloud_services.xml b/app/src/main/res/values-ar/strings_cloud_services.xml new file mode 100644 index 000000000..f266f69ad --- /dev/null +++ b/app/src/main/res/values-ar/strings_cloud_services.xml @@ -0,0 +1,236 @@ + + + + تسجيل الدخول إلى Telegram + أنت تقوم بتعديل رقمك الآن. إرسال الرمز مجدداً سيحل محل الرمز السابق. + جاري العمل… + جاري تهيئة Telegram… + جاري تسجيل الخروج… + جاري إغلاق الجلسة… + تم إغلاق الجلسة. أعد فتح تسجيل الدخول للمتابعة. + جاري تحضير جلسة Telegram آمنة… + بانتظار استجابة Telegram… + ربط Telegram + قم بربط حساب Telegram لبث الموسيقى مباشرة من قنواتك ومحادثاتك. + رقم الهاتف + أدخل رقم Telegram الخاص بك. يمكنك العودة وتعديله لاحقاً. + رقم الهاتف + 1 + 5551234567 + إرسال الرمز + رمز التحقق + أدخل الرمز الذي وصلك من Telegram. إذا كان الرقم خاطئاً، عد للخلف لتعديله. + الرمز + 12345 + تعديل الهاتف + إعادة إرسال الرمز + التحقق من الرمز + التحقق بخطوتين (كلمة المرور) + أدخل كلمة مرور Telegram الخاصة بك. لا يزال بإمكانك العودة لتصحيح رقمك. + كلمة المرور + التحقق من كلمة المرور + يرجى الانتظار… + + + قنوات Telegram + إضافة قناة + قناة Telegram عامة + جاري المزامنة + المزامنة الآن + طي المواضيع + إظهار المواضيع + خيارات القناة + المواضيع + جاري مزامنة القناة + جاري تحديث الأغاني من Telegram + جلب أحدث الأغاني من هذه القناة + إزالة القناة + إيقاف المزامنة وحذف الأغاني المخزنة مؤقتاً + حذف القناة؟ + ستتوقف مزامنة %1$s وسيتم حذف جميع الأغاني المخزنة مؤقتاً من هذه القناة. + إزالة + لم يتم مزامنة أي قنوات بعد + أضف قنوات Telegram عامة لمزامنة\nمكتبتك الموسيقية + إضافة قناة + لم تُزامن مطلقاً + تمت المزامنة %1$s + + + إضافة قناة + ابحث عن قناة Telegram عامة لمزامنة موسيقاها + اسم_القناة@ أو الرابط + جاري البحث… + البحث عن قناة + أدخل اسم المستخدم لقناة عامة أو الرابط الخاص بها\nلمزامنة ملفاتها الصوتية + + + لا توجد أغانٍ + أغنية واحدة (%d) + أغنيتان (%d) + %d أغانٍ + %d أغنية + %d أغنية + + + لا توجد مواضيع + موضوع واحد (%d) + موضوعان (%d) + %d مواضيع + %d موضوعاً + %d موضوع + + + + Subsonic + إدارة خوادم Navidrome وAirsonic والخوادم الأخرى المتوافقة مع Subsonic. + + + اضغط على مزامنة لجلب قوائم تشغيل Jellyfin الخاصة بك + إدارة اتصال خادم Jellyfin الخاص بك. + + + مجلدات الموسيقى + اضغط على + لإضافة مجلد Drive + لا توجد مجلدات مضافة بعد + تمت مزامنة %1$d مجلدات + إضافة مجلد + + + اختر نوع قائمة التشغيل + اختر قوائم التشغيل لمزامنتها: + جميع قوائم التشغيل + المنشأة والمجمعة + قوائم التشغيل المنشأة + قوائم التشغيل المجمعة + + + تمت مزامنة %1$d قائمة تشغيل + قوائم التشغيل + مزامنة + لم يتم مزامنة أي قوائم تشغيل بعد + اضغط على مزامنة لجلب قوائم التشغيل الخاصة بك + إجراءات سريعة + مزامنة المكتبة + إلغاء الاتصال + %1$d أغنية + + + جاري المزامنة + جاري مزامنة المكتبة… + جاري جلب قوائم التشغيل… + جاري مزامنة قائمة التشغيل: %1$s + جاري تحديث المكتبة المحلية… + اكتملت المزامنة + جاري جلب قائمة الألبومات… + جاري جلب الأغاني من %1$s… + جاري حفظ %1$d أغنية في قاعدة البيانات… + لا توجد أغانٍ في المكتبة + اكتملت مزامنة المكتبة + جاري المزامنة… + خطأ: %1$s + + + مزامنة + مزامنة الكل + تسجيل الخروج + مزامنة جميع قوائم التشغيل + الصورة الشخصية للمستخدم + + + لا يوجد اتصال بالإنترنت + يتطلب هذا المحتوى اتصالاً بالإنترنت. يرجى التحقق من إعدادات الشبكة والمحاولة مجدداً. + أنت غير متصل بالإنترنت + يرجى التحقق من اتصالك بالإنترنت والمحاولة مجدداً للوصول إلى هذا المحتوى. + + + اتصال + جاري الاتصال… + أدخل رابط الخادم (URL) وبيانات اعتماد الحساب. + تفاصيل الاتصال + إخفاء كلمة المرور + كلمة المرور + أدخل كلمة المرور + ملء تلقائي لـ //:http + رابط الخادم (URL) + إظهار كلمة المرور + Telegram + اسم المستخدم + admin + مرحباً، %1$s! + + + متوافق مع Navidrome وGonic وAirsonic والخوادم الأخرى المتوافقة مع Subsonic + يدعم خوادم Navidrome وAirsonic وGonic وAmpache والخوادم الأخرى المتوافقة مع واجهة برمجة تطبيقات Subsonic. + كلمة مرور التطبيق (App password) تعمل أيضاً إذا كان خادمك يدعمها. + ملء تلقائي لـ //:https + اتصل بخادم الموسيقى المستضاف ذاتياً + Navidrome + استخدم عنوان الخادم الكامل الذي يبدأ بـ //:https. + https://music.example.com + هذا هو اسم حسابك على Subsonic أو Navidrome. + Subsonic / Navidrome + Subsonic + + + أدخل رابط خادم Jellyfin وبيانات اعتماد الحساب. + يتصل بخوادم Jellyfin لبث مكتبتك الموسيقية + يتصل بخوادم Jellyfin. يتم دعم كل من HTTP وHTTPS للوصول عبر الشبكة المحلية. + شعار Jellyfin + كلمة المرور لحساب Jellyfin الخاص بك. + اتصل بخادم وسائط Jellyfin الخاص بك + Jellyfin + الرابط الكامل لخادم Jellyfin الخاص بك، شاملاً منفذ الاتصال (Port). + http://192.168.1.100:8096 + اسم المستخدم لحساب Jellyfin الخاص بك. + + + بث ملفات الموسيقى مباشرة من حساب Google Drive الخاص بك + ربط Google Drive + تم توصيل Google Drive بنجاح! + إنشاء مجلد \"PixelPlayer Music\" + أنشئ مجلداً جديداً هنا للموسيقى الخاصة بك + لا توجد مجلدات هنا + فتح المجلد + اختر أو أنشئ مجلداً لاستخدامه كمصدر للموسيقى الخاصة بك + اختر مجلد الموسيقى + جاري إعداد Google Drive… + تسجيل الدخول باستخدام Google + Google Drive + استخدام + + + تعذر قراءة ملفات تعريف الارتباط (Cookies) الخاصة بالجلسة. + تم + خروج + لم يتم العثور على ملفات تعريف الارتباط. سجل الدخول أولاً. + تستغرق الصفحة وقتاً طويلاً للتحميل. استخدم التحديث أو جرب شبكة أخرى. + + + جاري الحفظ… + بقاء + انتهت مهلة تحميل الصفحة. يمكنك إعادة المحاولة دون فقدان تقدمك. + رجوع للخلف في الويب + يمكنك العودة لاحقاً. سيتم تجاهل حالة الصفحة الحالية عند الإغلاق. + تقدم للأمام في الويب + تحديث + إعادة المحاولة + فتح الصفحة الرئيسية + فشل تحميل WebView. + + + فشل في قراءة ملفات تعريف ارتباط NetEase: %1$s + الخروج من تسجيل دخول NetEase؟ + خطأ HTTP %1$d أثناء تحميل NetEase. + لم يتم رصد تسجيل الدخول بعد. أكمل تسجيل الدخول إلى NetEase قبل الضغط على "تم". + تسجيل الدخول إلى NetEase Music + ملاحظة أمان: يتم إدخال كلمة المرور الخاصة بك فقط داخل صفحات ويب NetEase. يقوم PixelPlayer بحفظ ملفات تعريف ارتباط الجلسة (MUSIC_U) لمزامنة مكتبتك الموسيقية. + NetEase Music + + + فشل في قراءة ملفات تعريف ارتباط QQ Music: %1$s + الخروج من تسجيل دخول QQ Music؟ + خطأ HTTP %1$d أثناء تحميل QQ Music. + لم يتم رصد تسجيل الدخول بعد. أكمل تسجيل الدخول إلى QQ Music قبل الضغط على "تم". + تسجيل الدخول إلى QQ Music + ملاحظة أمان: يتم إدخال كلمة المرور الخاصة بك فقط داخل صفحات ويب QQ Music. يقوم PixelPlayer بحفظ ملفات تعريف ارتباط الجلسة (Cookies) لمزامنة مكتبتك الموسيقية. + QQ Music + \ No newline at end of file diff --git a/app/src/main/res/values-ar/strings_components.xml b/app/src/main/res/values-ar/strings_components.xml deleted file mode 100644 index 4f8baf086..000000000 --- a/app/src/main/res/values-ar/strings_components.xml +++ /dev/null @@ -1,190 +0,0 @@ - - - انقر للفتح - غلاف الألبوم - مواضع غلاف الألبوم - المفضلة - تشغيل - إيقاف مؤقت - انقر للتشغيل - عنوان الأغنية - الفنان - تكرار - شريط التقدم، %1$d بالمئة - - - المظهر - المحاذاة - عناصر التحكم - إعادة تعيين كلمات الأغاني؟ - هل أنت متأكد من أنك تريد إعادة تعيين كلمات هذه الأغنية؟ - إخفاء عناصر تحكم المزامنة - ضبط المزامنة - إظهار اللتنّة (Romanization) - إظهار الترجمات - تعطيل الوضع الغامر (لمرة واحدة) - إبقاء الشاشة قيد التشغيل - محاذاة الكلمات لليسار - محاذاة الكلمات للوسط - محاذاة الكلمات لليمين - - - لا يوجد اتصال بالإنترنت - يتطلب هذا المحتوى اتصالاً بالإنترنت. يرجى التحقق من إعدادات الشبكة والمحاولة مرة أخرى. - أنت غير متصل بالإنترنت - يرجى التحقق من اتصالك بالإنترنت والمحاولة مرة أخرى للوصول إلى هذا المحتوى. - - - حفظ موازن مخصص - أدخل اسماً لإعداد موازن الصوت المخصص الجديد. - اسم الإعداد المسبق - إعادة تسمية الإعداد المسبق - لا يمكن أن يكون الاسم فارغاً - حفظ - حفظ كجديد - إعادة تسمية - - - تم تحديث البيانات الوصفية بنجاح! - البيانات الوصفية بالذكاء الاصطناعي - جاري مراجعة دليل المزيج اليومي (Daily Mix)… - مراجعة وتعديل التفاصيل التي تم إنشاؤها - العنوان - الفنان - الألبوم - فنان الألبوم - النوع - الملحن - إعادة المحاولة - تطبيق التغييرات - - - تعديل البيانات الوصفية للأغنية - قد يؤثر تعديل البيانات الوصفية للأغنية على كيفية عرضها وتنظيمها في مكتبتك. هذه التغييرات دائمة وقد لا يمكن التراجع عنها. - فهمت ذلك - معلومات - تعديل الأغنية - استخدام ذكاء Gemini الاصطناعي - إظهار المعلومات - رقم المسار - رقم القرص - ميزة ReplayGain للمسار (ديسيبل) - ميزة ReplayGain للألبوم (ديسيبل) - -6.50 - -8.20 - ميزة ReplayGain للمسار - ميزة ReplayGain للألبوم - العنوان - رقم المسار - رقم القرص - البحث عن كلمات الأغاني على lrclib.net - غلاف الألبوم - اختر صورة مربعة وقم بضبطها لتظهر بشكل ممتاز في جميع أنحاء التطبيق. - تغيير غلاف الألبوم - حذف غلاف الألبوم - معاينة الغلاف الجديد - غلاف الأغنية الحالي - ضبط غلاف الألبوم - استخدم إيماءات القرص والسحب لتحديد الإطار المثالي. - تطبيق غلاف الألبوم - تعذر تحميل الصورة المحددة - - - مشاركة ملف الأغنية عبر - تشغيل الأغنية - مشاركة ملف الأغنية - إضافة إلى قائمة الانتظار - التشغيل تالياً في قائمة الانتظار - إضافة إلى قائمة التشغيل - إضافة إلى قائمة الانتظار - التالي - جاري التحقق من الساعة… - جاري النقل %1$d%% - جاري النقل إلى الساعة… - النقل قيد التنفيذ الآن - إرسال إلى الساعة - الساعة غير متاحة - إرسال الأغنية إلى الساعة - الساعة غير متاحة - تعيين كـ - تعيين كنغمة صوتية - اختر كيفية استخدام هذه الأغنية كنغمة للنظام - تعيين كنغمة رنين - تعيين الأغنية كنغمة رنين - استخدام هذه الأغنية كـ - اختر المكان الذي يجب أن يقوم PixelPlayer بتثبيت هذا الصوت فيه. - نغمة رنين الهاتف - المكالمات الواردة - صوت الإشعارات - الرسائل وتنبيهات التطبيقات - صوت المنبه - منبهات الساعة - تأكيد تغيير الصوت - هل تريد تعيين \"%1$s\" كـ %2$s الخاص بك؟ - تعيين الصوت - تم تعيين \"%1$s\" كـ %2$s الخاص بك - نغمة رنين - صوت إشعارات - صوت منبه - يرجى تمكين إذن "تعديل إعدادات النظام"، ثم العودة إلى PixelPlayer لإنهاء الإجراء تلقائياً. - لم يتم تمكين إذن تعديل إعدادات النظام. - تم تعيين \"%1$s\" كنغمة رنين لك - يمكن استخدام الأغاني المحلية فقط كنغمات رنين. - تعذر إعداد ملف الصوت هذا كنغمة رنين. - تعذر تعيين نغمة الرنين: %1$s - المدة - معلومات الأغنية - المدة - النوع - الألبوم - الفنان - صيغة الصوت - المزود - الملف - تعديل البيانات الوصفية للأغنية - إزالة من المفضلة - إضافة إلى المفضلة - الخيارات - الخيارات - التفاصيل - المعلومات - علامة تبويب التفاصيل - - - %1$d أغنية - تم تحديدها - تشغيل الكل - تشغيل الكل - إعجاب بالكل - إلغاء الإعجاب بالكل - مشاركة الكل كملف ZIP - إضافة الكل إلى قائمة الانتظار - حذف الكل - حذف الكل - - تم تجاهل قائمة التشغيل - تراجع - مزج DJ (Mashup) - قائمة تشغيل جديدة - اسم قائمة التشغيل - قائمة التشغيل الخاصة بي - إنشاء - إضافة %1$d أغانٍ إلى… - اختر قوائم التشغيل - البحث عن قوائم التشغيل… - - %1$d قائمة تشغيل - تصدير الكل - دمج الكل - مشاركة الكل - تصدير - دمج - - إعادة ترتيب علامات تبويب المكتبة - إعادة تعيين الترتيب - هل تريد إعادة تعيين ترتيب علامات التبويب إلى الوضع الافتراضي؟ - جاري إعادة ترتيب علامات التبويب… - مقبض السحب - إعادة تعيين - تم - diff --git a/app/src/main/res/values-ar/strings_equalizer.xml b/app/src/main/res/values-ar/strings_equalizer.xml new file mode 100644 index 000000000..e2138ea2e --- /dev/null +++ b/app/src/main/res/values-ar/strings_equalizer.xml @@ -0,0 +1,57 @@ + + + + لا يمكن أن يكون الاسم فارغاً + إعادة تسمية + + + تغيير وضع العرض + تعطيل موازن الصوت + تمكين موازن الصوت + تعديل + تعديل الإعدادات المسبقة + إعداد مخصص + الإعدادات المسبقة + تحديث + تضخيم الباس (Bass Boost) + المحاكي المحيطي (Virtualizer) + جهارة الصوت (Loudness) + غير مدعوم + غير مدعوم على هذا الجهاز + مستوى الصوت + الاستجابة الترددية + هرتز + الباس (الترددات المنخفضة) + الترددات المتوسطة المنخفضة + الترددات المتوسطة المرتفعة + التريبل (الترددات الحادة) + الباس / منخفض + متوسط / مرتفع + صفحة %1$d + إعادة تعيين المدة + حفظ كجديد + + + التفضيلات المسبقة المحفوظة + لم يتم حفظ تفضيلات مخصصة بعد. + إلغاء التثبيت + تثبيت + إعادة تسمية + حذف + + + حفظ الإعداد المسبق المخصص + أدخل اسماً لإعداد موازن الصوت المخصص الخاص بك. + اسم الإعداد المسبق + إعادة تسمية الإعداد المسبق + + + إدارة الإعدادات المسبقة + اسحب لإعادة الترتيب • اضغط على العين للإظهار أو الإخفاء + إعادة الترتيب + إعادة تعيين الإعدادات المسبقة + سيؤدي هذا إلى استعادة الترتيب الافتراضي وظهور الإعدادات المسبقة. هل ترغب في المتابعة؟ + إعادة تعيين للوضع الافتراضي + ظاهر + مخفي + diff --git a/app/src/main/res/values-ar/strings_home_screen.xml b/app/src/main/res/values-ar/strings_home_screen.xml new file mode 100644 index 000000000..99ea0157d --- /dev/null +++ b/app/src/main/res/values-ar/strings_home_screen.xml @@ -0,0 +1,281 @@ + + + + β + تجريبي + البث السحابي + سجل التغييرات + البث السحابي + بث الموسيقى من حساباتك السحابية + + + الإصدار التجريبي 0.7.5 + β + مرحباً بك في PixelPlayer 0.7.5-beta + أنت تستخدم بناءً تجريبياً قد يحتوي على أخطاء، أو حالات توقف مفاجئ، أو ميزات تجريبية. ساعدنا في التحسين من خلال الإبلاغ عن المشكلات. + ماذا تتوقع + قد تحدث أخطاء، توقفات مفاجئة، أو ميزات غير مكتملة بشكل غير متوقع. + بعض الميزات قد تتغير أو تُزال دون إشعار مسبق. + قد تكون النسخ التجريبية غير مستقرة مقارنة بالإصدارات الرسمية. + تحقق دائماً من التحديثات sebelum الإبلاغ عن مشكلة معروفة. + ما يمكن أن تغيره، تعلبه أو تحسنه النسخ التجريبية أثناء الاختبار. + اختصار مشكلات GitHub + ابحث أولاً، ثم افتح تقريراً مركزاً للأخطاء، التوقفات المفاجئة، الطلبات، أو الاستفسارات. + فتح المشكلات الحالية + الإبلاغ عن مشكلة أو توقف مفاجئ + شاركنا خطوات إعادة إنتاج المشكلة، النتائج المتوقعة، النتائج الفعلية، وتفاصيل جهازك/نظام التشغيل. + كيفية الإبلاغ + قائمة مراجعة سريعة قبل فتح تذكرة مشكلة جديدة. + قبل فتح تذكرة مشكلة + ابحث في المشكلات المفتوحة والمغلقة الحالية لتجنب التكرار. + حدث إلى آخر إصدار من PixelPlayer وتأكد من استمرار حدوث المشكلة. + أعد تشغيل التطبيق وتأكد من بقاء المشكلة قائمّة. + حاول تكرار حدوث المشكلة واكتب الخطوات الدقيقة لذلك. + ما هو نوع المشكلة؟ + تقرير خطأ برمي (Bug): شيء ما يتصرف بشكل غير صحيح. + طلب ميزة: إضافة ميزة جديدة أو تحسين. + سؤال: استخدم قسم المناقشات إذا كان مفعلاً، أو افتح تذكرة بعلامة سؤال. + تقرير خطأ برمجى + انسخ هذه الحقول عندما يتصرف شيء ما بشكل غير صحيح أو يتوقف فجأة. + تقرير خطأ + ملخص قصير: + السلوك المتوقع: + السلوك الحالي: + خطوات التشغيل/إعادة الإنتاج: 1. 2. 3. + كم مرة يحدث ذلك؟ دائماً / أحياناً / نادراً. + لقطة شاشة / فيديو: إن وجد. + السجلات / تتبع الكومة (Stack trace): إن وجد. + البيئة البرمجية + إصدار PixelPlayer: + مصدر التثبيت: إصدار GitHub، بناء تصحيح خطأ، بناء ليلي، إلخ. + إصدار أندرويد: + موديل الجهاز: + سياق إضافي: استخدام بطاقة SD، إعدادات خاصة، أذونات، إلخ. + طلب ميزة جديد + انسخ هذه الحقول عندما ترغب في طلب ميزة جديدة أو تحسين. + بيان المشكلة: ما هي المشكلة التي تحاول حلها؟ + الحل المقترح: كيف يجب أن تعمل الميزة؟ + البدائل المدروسة: هل توجد أي مقاربات أخرى؟ + النطاق: ما هي الشاشات أو التدفقات المتأثرة؟ + نموذج مبدئي (Mockup) أو صورة مرجعية إن وجدت. + العناوين، الخصوصية والنطاق + اجعل التقرير سهلاً للفرز وآمناً للمشاركة. + عناوين جيدة للمشكلات + معادل الصوت: مؤشر الإزاحة يتغير عند تبديل تبويب التفضيلات + البحث: قائمة السجل لا تظهر عند الاستعلام الفارغ + ميزة: إضافة خيار فرز قائمة التشغيل حسب "المضافة حديثاً" + يرجى تجنب + التقارير العامة مثل "إنه لا يعمل". + جمع مشكلات متعددة غير مترابطة في تذكرة واحدة. + السجلات أو لقطات الشاشة غير المظللة التي تحتوي على بيانات خاصة. + ملاحظة الخصوصية + قبل نشر السجلات، لقطات الشاشة، أو الفيديوهات، قم بإزالة أي معلومات شخصية أو خاصة. + البناء الليلي (Nightly builds) + كيف تختلف البناءات الليلية عن الإصدارات الرسمية، وماذا تضمن عندما تتعطل. + يتم إنشاء البناءات الليلية من آخر التزامات برمجية (Commit)، وقد تحتوي على تغييرات غير مكتملة، أخطاء مؤقتة، أو تراجعات في الأداء. إنها تجريبية أكثر من الإصدارات الرسمية. + يمكنك الوصول إليها من ملحقات سير عمل GitHub Actions الخاصة بالمستودع إن وجدت. + الإبلاغ عن مشكلات البناء الليلي + عند الإبلاغ عن مشكلة من بناء ليلي، اذكر دائماً أن ذلك حدث في نسخة ليلية وليس في إصدار رسمي. يرجى تضمين تاريخ البناء، اسم أو رقم تشغيل سير العمل، أو معرف الالتزام (Commit SHA) إن أمكن. وتحقق أيضاً مما إذا كانت نفس المشكلة تحدث في أحدث إصدار رسمي. + التحديث إلى Beta 0.5.0 + يُوصى بتثبيت نظيف + If كنت قادماً من الإصدار التجريبي 0.5.0، فقد يتطلب هذا التحديث بيانات مكتبة جديدة بدلاً من الحالة القديمة المخزنة مؤقتاً. + إذا بدت البيانات الوصفية أو إدخالات المكتبة خاطئة + البيانات الوصفية الخاطئة للأغاني، أو عدم تطابق الفنانين أو الألبومات، أو الإدخالات التي تبدو مكررة تعني عادةً أن التثبيت النظيف هو الحل المناسب. + لا تظهر هذا مجدداً + فهمت ذلك + + + عذراً! حدث خطأ ما + تعطل التطبيق خلال جلستك الأخيرة. ساعدنا في إصلاح هذا من خلال مشاركة تقرير التعطل. + التاريخ: %1$s + الخطأ: + تتبع الكومة (معاينة): + سجل التعطل + تم نسخ سجل التعطل إلى الحافظة + تقرير تعطل PixelPlayer + مشاركة تقرير التعطل + + + مزج DJ (Mashup) + + + المزيج\nالخاص بك + لا توجد بيانات لعرضها بعد + سيظهر المزيج الخاص بك هنا عندما يجد PixelPlayer أغانٍ أو يقوم بمزامنة أحد المصادر. + تحديث + + + المزيج اليومي + بناءً على تاريخ الاستماع + تحقق من كامل المزيج اليومي + المزيج اليومي (Daily Mix) + + + المزيج اليومي + + لا توجد أغانٍ • %2$s + أغنية واحدة • %2$s + أغنيتان • %2$s + %1$d أغانٍ • %2$s + %1$d أغنية • %2$s + %1$d أغنية • %2$s + + تشغيل + صانع قوائم التشغيل بالذكاء الاصطناعي + + + كيف يتم بناء المزيج اليومي الخاص بك + يتم بناء المزيج اليومي الخاص بك (Daily Mix) بناءً على أغانيك المفضلة والأكثر تشغيلاً. نقوم أيضاً بإضافة مسارات من فنانين وأنواع موسيقية تحبها لتتمكن من اكتشاف موسيقى جديدة. + أخبر الذكاء الاصطناعي بما تود الاستماع إليه اليوم + نحن نستخدم عينة صغيرة للحفاظ على انخفاض استهلاك البيانات والتكلفة + جاري التحديث… + تحديث المزيج اليومي + + + منسقة بشكل مثالي + المزيج اليومي + رحلتك الصوتية جاهزة الآن + صانع قوائم التشغيل بالذكاء الاصطناعي + صف الأجواء أو الحالة المزاجية أو النشاط، ودع الذكاء الاصطناعي يتولى تنسيق قائمة التشغيل المثالية من مكتبتك. + حجم قائمة التشغيل + الحد الأدنى للأغاني + الحد الأقصى للأغاني + مثال: أجواء مسائية هادئة، طاقة حماسية للتمارين… + انقر لإعادة المحاولة + تم توليف رحلتك الصوتية! + جاهز للتشغيل + جاري الإنشاء… + إنشاء قائمة التشغيل + + + المشغلة حديثاً + + + المشغلة حديثاً + تشغيل الأحدث + لا توجد أغانٍ مشغلة مؤخراً في %1$s + قم بتغيير النطاق الزمني أو تشغيل المزيد من الأغاني لملء هذا الجدول الزمني. + المشغلة حديثاً + اليوم + أمس + + + إحصائيات الاستماع + إجمالي التشغيل + المعدل يومياً + المسار الأعلى + %1$s • %2$d تشغيل + + + إحصائيات الاستماع + تحديث إحصائيات الاستماع + اليوم + الأسبوع الحالي + الشهر الحالي + السنة الحالية + كل الأوقات + الاستماع + مرات التشغيل + الخط الزمني للاستماع + وقت الاستماع + إجمالي وقت الاستماع الذي تم تسجيله في النطاق المحدد. + عدد مرات التشغيل + عدد الجلسات التي أكملتها لكل شريحة زمنية. + معدل الجلسة + متوسط مدة الاستماع لكل شريحة زمنية. + مقسمة إلى فترات مدتها 4 ساعات للكشف عن إيقاعك اليومي. + تسهل الأشرطة اليومية مقارنة عادات الاستماع من أسبوع لآخر. + توضح الأشرطة الأسبوعية اتجاهات الشهر وتطورها. + تظهر الأشرطة الشهرية التغيرات الموسمية على مدار السنة. + تختصر الأشرطة السنوية كامل تاريخ الاستماع الخاص بك. + لا توجد بيانات استماع بعد + اضغط على زر التشغيل لبدء بناء خطك الزمني للاستماع + الإيقاع اليومي + الإيقاع الأسبوعي + الإيقاع الشهري + نظرة عامة على السنة + التطور على مر الوقت + مجمعة في شرائح مدتها 4 ساعات + مجمعة حسب أيام الأسبوع + مجمعة حسب أسبوع الشهر + مجمعة حسب الشهر + مجمعة حسب السنة + شريحة الذروة + %1$d تشغيل + + الفئات الأعلى + قارن بين طرق استماعك عبر الأنواع الموسيقية، الفنانين، الألبومات، والأغاني. + النوع + الفنان + الألبوم + الأغنية + الاستماع حسب النوع + الاستماع حسب الفنان + الاستماع حسب الألبوم + الاستماع حسب الأغنية + %1$d تشغيل • %2$d فنان + %1$d تشغيل • %2$d مسار + لا توجد بيانات فئات بعد + اضغط على زر التشغيل لإظهار أهم فئات الاستماع لديك + عادات الاستماع + لا توجد عادات استماع بعد + سنقوم بإظهار عادات الاستماع الخاصة بك بمجرد أن نتعرف على ذوقك بشكل أفضل. + إجمالي الجلسات + معدل الجلسة + أطول جلسة + جلسة/يوم + اليوم الأكثر نشاطاً + لم يتم التشغيل بعد + فترة الذروة الزمنية + أبرز الفنانين + لا يوجد فنانون بارزون + استمر في الاستماع وسيظهر فنانوك المفضلون هنا. + ? + %1$d. %2$s + أبرز الألبومات + لا توجد ألبومات بارزة + الألبومات التي تعيد الاستماع إليها كثيراً ستظهر هنا. + %1$d. %2$s + تركيز المسارات + كيفية توزيع وقت استماعك على المسارات الأعلى لديك. + لا توجد بيانات تركيز بعد + قم بتشغيل المزيد من المسارات لترى مدى تركيز استماعك. + الأعلى 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$d أيام + منذ ساعة واحدة + منذ %1$d ساعات + منذ دقيقة واحدة + منذ %1$d دقائق + %1$d أغنية + %1$d أغانٍ + الأسبوع %1$d + \ No newline at end of file diff --git a/app/src/main/res/values-ar/strings_library.xml b/app/src/main/res/values-ar/strings_library.xml new file mode 100644 index 000000000..9a2f71120 --- /dev/null +++ b/app/src/main/res/values-ar/strings_library.xml @@ -0,0 +1,598 @@ + + + + المكتبة + علامات تبويب المكتبة + الانتقال مباشرة إلى أي علامة تبويب أو إعادة ترتيبها. + إعادة ترتيب علامات التبويب + + + الأغاني + الألبومات + الفنانون + قوائم التشغيل + المجلدات + المفضلة + + + تم إنشاء قائمة التشغيل بنجاح + يرجى إعداد مفتاح API لمزود الذكاء الاصطناعي أولاً + يرجى تعيين مفتاح API لـ Gemini أولاً + تمت الإضافة إلى قائمة الانتظار + سيتم التشغيل تالياً + + + نقل إلى الساعة + الإعدادات + تعديل + إعادة ترتيب علامات التبويب + توسيع القائمة + + + يمكنك تحديد ما يصل إلى %1$d ألبومات + مجلد + مجلد + + + فرز حسب + عرض + عرض قوائم التشغيل + شبكة + قائمة + الذاكرة الداخلية + بطاقة SD + بطاقة SD غير متاحة حالياً. + سحابي + قنوات Telegram السحابية + عرض المواضيع + القنوات + المواضيع + كلاهما + سحابي + سحابي فقط + + + جاري إنشاء البيانات الوصفية بالذكاء الاصطناعي… + + + خطأ أثناء تحميل الأغاني + خطأ أثناء تحميل الألبومات + خطأ أثناء تحميل الفنانين + إعادة المحاولة + + + لم يتم العثور على أغانٍ في مكتبتك. + جرّب إعادة فحص مكتبتك من الإعدادات إذا كانت لديك ملفات موسيقى على جهازك. + لم يتم العثور على أغانٍ + + + جديد + إنشاء قائمة تشغيل جديدة + استيراد قائمة تشغيل M3U + تحديد موقع الأغنية الحالية + جميع الأغاني + سحابي + محلي + خيارات الفرز + + + الكل + إلغاء التحديد + المزيد من الخيارات + + + جاري فحص ملفات الموسيقى… + جاري معالجة الملفات… + %1$d من أصل %2$d ملفاً + جاري مزامنة المكتبة… + اكتملت المزامنة + انتظار… + جاري مزامنة المكتبة… + جاري تنظيف ذاكرة التخزين المؤقت لأغلفة الألبومات… + جاري المزامنة مع المصادر السحابية… + جاري فحص كلمات الأغاني… + + + لا توجد أغانٍ بعد + أضف موسيقى إلى جهازك أو قم بمزامنة مصدر سحابي لبدء الاستماع. + لم يتم العثور على أغانٍ محلية + جرّب فلتراً آخر للمصادر أو أعد فحص مكتبة جهازك. + لم يتم العثور على أغانٍ سحابية + قم بمزامنة أغانٍ من Telegram أو NetEase، أو تحوّل إلى المصدر المحلي. + لا توجد ألبومات متاحة + ستظهر الألبومات هنا بمجرد أن تحتوي مكتبتك على مسارات مجمعة. + لم يتم العثور على ألبومات محلية + يلزم وجود أغانٍ محلية لإنشاء مجموعات ألبومات محلية. + لم يتم العثور على ألبومات سحابية + ستظهر الأغاني السحابية التي تحتوي على بيانات الألبوم هنا بعد المزامنة. + لا يوجد فنانون متاحون + يتم عرض الفنانين بعد فهرسة الأغاني من أي مصدر. + لم يتم العثور على فنانين محليين + لا تتوفر بيانات وصفية للفنانين للأغاني المحلية في الوقت الحالي. + لم يتم العثور على فنانين سحابيين + تظهر إدخالات الفنانين السحابيين عند مزامنة الأغاني عن بُعد. + لا توجد أغانٍ مفضلة بعد + انقر على أيقونة القلب أثناء تشغيل أي أغنية لحفظها هنا. + لا توجد أغانٍ محلية مفضلة + قم بتغيير فلتر المصدر أو أضف إعجاباً بالأغاني الموجودة على جهازك. + لا توجد أغانٍ سحابية مفضلة + أضف إعجاباً بمسارات Telegram أو NetEase لرؤيتها في هذا العرض. + لم يتم العثور على مجلدات + ستظهر مجلدات وحدة التخزين الداخلية التي تحتوي على موسيقى هنا. + لا توجد قوائم تشغيل بعد + أنشئ قائمة تشغيلك الأولى لتنظيم مكتبتك الموسيقية. + + + تعديل البيانات الوصفية للأغنية + تشغيل + تشغيل الأغنية + تشغيل الكل + تشغيل الكل + إضافة إلى المفضلة + إعجاب بالكل + إزالة من المفضلة + إلغاء الإعجاب بالكل + مشاركة ملف الأغنية عبر + مشاركة ملف الأغنية + مشاركة الكل كملف ZIP + تعذر مشاركة الأغنية: %1$s + إضافة إلى قائمة الانتظار + إضافة إلى قائمة الانتظار + التالي + التشغيل تالياً في قائمة الانتظار + إضافة إلى قائمة التشغيل + حذف + حذف الكل + جاري التحقق من الساعة + جاري النقل %1$d%% + جاري النقل إلى الساعة + عملية النقل مستمرة + إرسال إلى الساعة + الساعة غير متاحة + إرسال الأغنية إلى الساعة + الساعة غير متاحة + تعيين كـ + تعيين كصوت + اختر كيفية استخدام هذه الأغنية كصوت للنظام + استخدام هذه الأغنية كـ + اختر أين يجب على PixelPlayer تثبيت هذا الصوت. + نغمة رنين الهاتف + المكالمات الواردة + صوت الإشعارات + الرسائل وتنبيهات التطبيق + صوت المنبه + منبهات الساعة + تأكيد تغيير الصوت + هل تريد تعيين \"%1$s\" كـ %2$s الخاص بك؟ + تعيين الصوت + تم تعيين \"%1$s\" كـ %2$s الخاص بك + نغمة رنين + صوت إشعارات + صوت منبه + يرجى تمكين إذن \"تعديل إعدادات النظام\"، ثم العودة إلى PixelPlayer لإنهاء الإجراء تلقائياً. + لم يتم تمكين إذن تعديل إعدادات النظام. + تم تعيين \"%1$s\" كنغمة رنين لك + يمكن استخدام الأغاني المحلية فقط كنغمات رنين. + تعذر إعداد ملف الصوت هذا كنغمة رنين. + تعذر تعيين نغمة الرنين: %1$s + الخيارات + الخيارات + المعلومات + المعلومات + المدة + النوع + الألبوم + الفنان + معلومات الأغنية + المزود + الملف + %1$d أغنية + تم تحديدها + %1$d قائمة تشغيل + %1$d ألبوم + تم تحديدها + الحد الأقصى: %1$d ألبومات في كل تحديد. + الترتيب في قائمة الانتظار والتشغيل يتبع ترتيب تحديدك. + %1$d نوع موسيقي + تم تحديدها + إجراء عمليات جماعية على جميع الأغاني ضمن هذه الأنواع. + + + الترتيب الافتراضي + العنوان (أ-ي) + العنوان (ي-أ) + الفنان + الفنان (ي-أ) + الألبوم + الألبوم (ي-أ) + تاريخ الإضافة + تاريخ الإضافة (الأقدم أولاً) + المدة + المدة (الأقصر أولاً) + سنة الإصدار + سنة الإصدار (الأقدم أولاً) + أقل عدد أغانٍ + أكثر عدد أغانٍ + الاسم (أ-ي) + الاسم (ي-أ) + عدد الأغاني (الأكثر) + عدد الأغاني (الأقل) + تاريخ الإنشاء + تاريخ الإنشاء (الأقدم أولاً) + تاريخ الإعجاب + تاريخ الإعجاب (الأقدم أولاً) + أقل عدد مجلدات فرعية + أكثر عدد مجلدات فرعية + + + العنوان + الفنان + الألبوم + تاريخ الإضافة + المدة + سنة الإصدار + عدد الأغاني + الاسم + عدد الأغاني + تاريخ الإنشاء + تاريخ الإعجاب + عدد المجلدات الفرعية + + + المصدر + الترتيب + تنازلي + تصاعدي + الترتيب الأصلي + اضغط للتبديل إلى التصاعدي + اضغط للتبديل إلى التنازلي + هذا الفرز يحافظ على ترتيبه الأصلي + المفتاح قيد التشغيل + + + إعادة ترتيب علامات تبويب المكتبة + إعادة تعيين الترتيب + هل تريد إعادة تعيين ترتيب علامات التبويب إلى الوضع الافتراضي؟ + جاري إعادة ترتيب علامات التبويب… + مقبض السحب + + + اختر فناناً + فنان واحد + %1$d فنانين + الفنان الرئيسي + صفحة الفنان + + + إلغاء النقل + %1$s / %2$s + يظهر التقدم المباشر لعمليات نقل الموسيقى من الهاتف إلى الساعة + عمليات النقل إلى الساعة + جاري الإرسال إلى الساعة + تم الإلغاء + تم إلغاء النقل + اكتمل النقل + تم بنجاح + فشل النقل + فشل النقل + عمليات نقل نشطة متعددة + %1$s • %2$s + جاري التحضير + جاري تحضير نقل الساعة + جاري تحضير النقل… + جاري إرسال %1$d أغنية إلى الساعة + جاري الإرسال إلى الساعة + جاري بدء النقل… + جاري البدء + جاري النقل + %1$d عمليات نقل + + + تعديل الأغنية + إظهار المعلومات + تعديل البيانات الوصفية للأغنية + قد يؤثر تعديل البيانات الوصفية للأغنية على كيفية عرضها وتنظيمها in مكتبتك. هذه التغييرات دائمة وقد لا يمكن التراجع عنها. + فهمت ذلك + معلومات + غلاف الألبوم + اختر صورة مربعة وقم بضبطها لتظهر بشكل ممتاز في جميع أنحاء التطبيق. + تغيير غلاف الألبوم + حذف غلاف الألبوم + العنوان + الفنان + الألبوم + فنان الألبوم + النوع + الملحن + رقم المسار + رقم القرص + ميزة ReplayGain للمسار (ديسيبل) + ميزة ReplayGain للألبوم (ديسيبل) + -6.50 + -8.20 + معاينة الغلاف الجديد + غلاف الأغنية الحالي + ضبط غلاف الألبوم + استخدم إيماءات القرص والسحب لتحديد الإطار المثالي. + تطبيق غلاف الألبوم + تعذر تحميل الصورة المحددة + البحث عن كلمات الأغاني على lrclib.net + + + تعديل %d أغنية + سيتم تحديث الحقول المعدلة فقط. اترك الحقول فارغة للاحتفاظ بالقيم الحالية. + (قيم مختلطة) + (اختياري - اتركه فارغاً للتخطي) + تم تحديث %d أغنية بنجاح + تم تحديث %1$d من أصل %2$d أغنية. تعذر تعديل بعض الملفات. + فشل تحديث الأغاني + تعديل غلاف المجموعة + سيؤدي هذا إلى استبدال غلاف الألبوم لجميع الأغاني المحددة وعددها %d + تعيين غلاف الألبوم للكل + إزالة جميع أغلفة الألبومات + (أغلفة متعددة مختلفة) + + + تم تجاهل قائمة التشغيل + + + إنشاء قائمة تشغيل + اختر طريقة الإنشاء. + يدوي + تصميم الغلاف، الأيقونة، الشكل واختيار الأغاني بنفسك. + بالذكاء الاصطناعي + توليد قائمة تشغيل منسقة بعناصر تحكم متقدمة. + يتطلب تكوين مفتاح Gemini API في الإعدادات. + إعداد مفتاح API + + + معمل قوائم التشغيل بالذكاء الاصطناعي + إعادة تعيين + جاري الإنشاء… + إنشاء + الهدف والأجواء + اسم قائمة التشغيل (اختياري) + ما هي الأجواء التي ترغب بها في قائمة التشغيل هذه؟ + مثال: قيادة وقت الغروب مع ألحان سينث دافئة + الاتجاه الفني + الحالة المزاجية + النشاط + الحقبة الزمنية + محرك التنسيق + الحيوية والطاقة + تتحكم في حدة الأغاني وإيقاعها. 1 = هادئ/بطيء، 5 = حماسي جداً/سريع. + عمق الاكتشاف + تتحكم في مدى معرفتك بالاختيارات. 1 = المفضلة الأكثر تشغيلاً، 5 = أغانٍ نادرة ولم تسمعها كثيراً. + أقل عدد أغانٍ + أقصى عدد أغانٍ + الفلاتر + أنواع موسيقية مفضلة (اختياري) + مثال: سينث ويف، إندي بوب + أنواع موسيقية تتجنبها (اختياري) + مثال: ميتال، هارد تراب + اللغة المفضلة (اختياري) + مثال: الإنجليزية، العربية، معزوفات موسيقية + إعطاء الأولوية للمفضلة + تجنب الكلمات غير اللائقة (Explicit) + معاينة الأمر (Prompt) + سيظهر أمرك النهائي هنا بمجرد إضافة تفضيلاتك. + تنسيق بدقة متناهية + حدد المزاج، النشاط، القيود، وعمق الاختيارات. + سيقوم الذكاء الاصطناعي باختيار الأغاني من مكتبتك المحلية فقط. + يرجى إضافة توجيه واحد على الأقل للذكاء الاصطناعي. + يرجى تعيين نطاق أغانٍ صالح. + 5/%1$d + مخصص… + إدخال قيمة مخصصة + أدخل قيمتك المخصصة + + + أي حقبة + الطلب الأساسي: %1$s. + المزاج المستهدف: %1$s. + سياق النشاط: %1$s. + التركيز على الحقبة: %1$s. + إعطاء الأولوية للأنواع: %1$s. + تجنب الأنواع: %1$s. + اللغة المفضلة: %1$s. + مستوى الطاقة المستهدف: 5/%1$d. + هدف الاكتشاف: 5/%1$d حيث 1 تعني مألوف و5 تعني اختيارات عميقة ونادرة. + إعطاء الأولوية للأغاني القريبة من مفضلات المستمع كلما أمكن ذلك. + تجنب الأغاني ذات الكلمات غير اللائقة كلما توفرت بدائل. + الحفاظ على سلاسة الانتقالات وتجنب التكرار المتتالي لنفس الفنان. + + هادئ (Chill) + حماسي (Energetic) + سعيد (Happy) + داكن/غامض (Dark) + رومانسي (Romantic) + شجي/ميلانكولي (Melancholic) + + + تمارين رياضية (Workout) + تركيز (Focus) + رحلة على الطريق (Road trip) + حفلة (Party) + دراسة (Study) + وقت متأخر من الليل (Late night) + + + @string/playlist_creation_ai_era_any + السبعينات + الثمانينات + التسعينات + الألفينات (2000s) + العقد 2010 + العقد 2020 + + + + لم يتم إنشاء أي قائمة تشغيل. + المس زر \"قائمة تشغيل جديدة\" للبدء. + قائمة تشغيل جديدة + اسم قائمة التشغيل + قائمة التشغيل الخاصة بي + + + إضافة %1$d أغانٍ إلى… + اختر قوائم التشغيل + البحث عن قوائم التشغيل… + تمت إضافة الأغاني إلى قوائم التشغيل + تم إنشاء قائمة التشغيل وإضافة الأغاني + وحدة التخزين الداخلية + + + إضافة أغانٍ + إضافة الأغاني المحددة + إضافة + بحث أو تصفية الأغاني… + المفضلة + فشل تحميل الأغاني + تحميل المزيد + + + دمج قوائم التشغيل + أدخل اسماً لقائمة التشغيل المدمجة: + قائمة تشغيل مدمجة + سيؤدي هذا إلى دمج %1$d من قوائم التشغيل المحددة في قائمة واحدة. + + + لم يتم العثور على أغانٍ صالحة للتشغيل + لم يتم العثور على الأغنية في القائمة الحالية + تعذر تحديد موقع الأغنية + لم يتم العثور على أغانٍ في المكتبة + توقف التشغيل: انتهى %1$s (نهاية المسار). + مسار + لا توجد أغانٍ لتشغيلها عشوائياً. + الألبومات المحددة + لم يتم العثور على أغانٍ قابلة للتشغيل في الألبومات المحددة + لم يتم العثور على أغانٍ قابلة للتشغيل في الأنواع المحددة + تم إدراج أول %1$d ألبومات فقط في قائمة الانتظار + تم إدراج %1$d ألبومات في قائمة الانتظار (%2$d أغنية) + تعذر إدراج الألبومات المحددة في قائمة الانتظار + جميع الأغاني موجودة بالفعل في المفضلة + لم تكن هناك أي أغانٍ في المفضلة + جاري إنشاء ملف ZIP… + فشلت المشاركة: %1$s + + لم يتم إضافة أي أغنية إلى قائمة الانتظار + تمت إضافة أغنية واحدة إلى قائمة الانتظار + تمت إضافة أغنيتين إلى قائمة الانتظار + تمت إضافة %d أغانٍ إلى قائمة الانتظار + تمت إضافة %d أغنية إلى قائمة الانتظار + تمت إضافة %d أغنية إلى قائمة الانتظار + + + لن يتم تشغيل أي أغنية تالياً + سيتم تشغيل أغنية واحدة تالياً + سيتم تشغيل أغنيتين تالياً + سيتم تشغيل %d أغانٍ تالياً + سيتم تشغيل %d أغنية تالياً + سيتم تشغيل %d أغنية تالياً + + + لم يتم إضافة أي أغنية إلى المفضلة + تمت إضافة أغنية واحدة إلى المفضلة + تمت إضافة أغنيتين إلى المفضلة + تمت إضافة %d أغانٍ إلى المفضلة + تمت إضافة %d أغنية إلى المفضلة + تمت إضافة %d أغنية إلى المفضلة + + + لم يتم إزالة أي أغنية من المفضلة + تمت إزالة أغنية واحدة من المفضلة + تمت إزالة أغنيتين من المفضلة + تمت إزالة %d أغانٍ من المفضلة + تمت إزالة %d أغنية من المفضلة + تمت إزالة %d أغنية من المفضلة + + + + لا توجد قوائم تشغيل لمشاركتها + مشاركة قوائم التشغيل + فشلت المشاركة: %1$s + لا توجد قوائم تشغيل لتصديرها + فشل التصدير: %1$s + Music/PixelPlayer Exports + يرجى تكوين مفتاح Gemini API في الإعدادات. + تم استعادة قائمة التشغيل + + لا يتم مشاركة أي قوائم تشغيل + جاري مشاركة قائمة تشغيل واحدة + جاري مشاركة قائمتي تشغيل + جاري مشاركة %d قوائم تشغيل + جاري مشاركة %d قائمة تشغيل + جاري مشاركة %d قائمة تشغيل + + + لم يتم تصدير أي قوائم تشغيل إلى %2$s + تم تصدير قائمة تشغيل واحدة إلى %2$s + تم تصدير قائمتي تشغيل إلى %2$s + تم تصدير %1$d قوائم تشغيل إلى %2$s + تم تصدير %1$d قائمة تشغيل إلى %2$s + تم تصدير %1$d قائمة تشغيل إلى %2$s + + + + معرف الألبوم غير صالح + لم يتم العثور على معرف الألبوم + خطأ أثناء تحميل بيانات الألبوم: %s + لم يتم العثور على الألبوم + + + معرف الفنان غير صالح + لم يتم العثور على معرف الفنان + خطأ أثناء تحميل بيانات الفنان: %s + تعذر العثور على الفنان + + + لا يمكن حذف الأغنية التي يتم تشغيلها حالياً + تم حذف %1$d ملفات (تم تخطي %2$d - قيد التشغيل) + تم حذف %1$d من أصل %2$d ملفاً + فشل حذف الملفات + تم حذف الملف + تعذر حذف الملف أو أن الملف غير موجود + تم إلغاء الحذف + حذف الأغنية؟ + \"%1$s\" بواسطة %2$s\n\nسيتم حذف هذه الأغنية نهائياً من جهازك ولا يمكن استعادتها. + سيتم حذف هذه الأغاني نهائياً من جهازك ولا يمكن استعادتها. + + لم يتم حذف أي ملف + تم حذف ملف واحد + تم حذف ملفين + تم حذف %d ملفات + تم حذف %d ملفاً + تم حذف %d ملفاً + + + هل تريد حذف الملفات؟ + هل تريد حذف أغنية واحدة؟ + هل تريد حذف أغنيتين؟ + هل تريد حذف %d أغانٍ؟ + هل تريد حذف %d أغنية؟ + هل تريد حذف %d أغنية؟ + + + + تم تحديث البيانات الوصفية بنجاح + جاري تحديث %1$d أغنية… + تم تحديث %1$d أغنية بنجاح! + تم تحديث %1$d أغنية. فشل: %2$d + تم حفظ كلمات الأغاني بنجاح + فشل حفظ كلمات الأغاني + لا توجد كلمات أغانٍ متاحة لحفظها + تم رفض الإذن - لا يمكن تعديل الملفات + تم رفض الإذن - لا يمكن حفظ كلمات الأغاني + تم رفض الإذن - لا يمكن تعديل هذا الملف + + + يرجى تكوين مفتاح API صالح لمزود الذكاء الاصطناعي المحدد في الإعدادات. + خطأ في الذكاء الاصطناعي: %s + رفض مزود الذكاء الاصطناعي المحدد الطلب لعدم وجود رصيد أو حصة كافية في الحساب. + نموذج الذكاء الاصطناعي المحدد لم يعد متاحاً. حاول PixelPlayer التبديل إلى نموذج مدعوم تلقائياً. + لم يتمكن الذكاء الاصطناعي من العثور على أي أغنية تناسب طلبك. + اكتب فكرة للمزيج اليومي الخاص بك + تم تحديث المزيج اليومي بواسطة الذكاء الاصطناعي + تعذر التحديث: %s + لم يتمكن الذكاء الاصطناعي من العثور على أغانٍ لهذا المزيج + \ No newline at end of file diff --git a/app/src/main/res/values-ar/strings_player.xml b/app/src/main/res/values-ar/strings_player.xml new file mode 100644 index 000000000..50376c35e --- /dev/null +++ b/app/src/main/res/values-ar/strings_player.xml @@ -0,0 +1,210 @@ + + + + طي المشغل + جاري التشغيل + البث السحابي + بث + بلوتوث + التشغيل المحلي + جاري الاتصال… + فتح قائمة الانتظار + + + استعد للاتصال + اسمح لـ PixelPlayer برؤية أجهزتك القريبة وشبكة الـ Wi-Fi الحالية حتى نتمكن من إبقاء البث وصوت البلوتوث ومكبرات الصوت متزامنة. + الأجهزة القريبة + مطلوب لقراءة والتحكم في معدات صوت البلوتوث المتصلة. + الموقع لشبكة الـ Wi‑Fi + يتطلب نظام أندرويد إذن الموقع لمشاركة شبكة الـ Wi-Fi الحالية (SSID) حتى نتمكن من العثور على أجهزة البث المتوافقة. + السماح بالوصول + نحن نستخدم هذه الأذونات فقط لربط الأجهزة — البث، والتحكم في مكبرات الصوت القريبة، وإبقاء الصوت متزامناً. + توصيل الجهاز + جاري الفحص بالقرب منك + جلسة بث + جاري الاتصال + متصل + هذا الهاتف + صوت البلوتوث + تشغيل محلي + جاري التشغيل + موقوف مؤقتاً + مستوى صوت الجهاز + مستوى صوت الهاتف + %1$d/%2$d + مستوى البطارية + مستوى الصوت + إلغاء الاتصال + الاتصالية + تشغيل الـ Wi-Fi أو البلوتوث + تحديث الاتصالات + Wi-Fi + متوقف + يعمل + متصل + بلوتوث + متوقف + يعمل + متصل + الأجهزة القريبة + تحديث الأجهزة + متصل + جاري الاتصال + متاح للاتصال + متاح + جاري الاتصال… + جاري البحث عن أجهزة… + تأكد من أن التلفزيون أو مكبر الصوت قيد التشغيل ومتصل بنفس شبكة الـ Wi‑Fi. + عناصر التحكم + الأجهزة + + + خادم وسائط البث (Cast) + جاري البث إلى الجهاز + تزويد جهاز البث بالوسائط + %1$s: %2$s + التقديم والتأخير غير متاح مؤقتاً لصيغة الصوت هذه أثناء البث لأنها قد تتسبب في تعطل جلسة البث. + + + مؤقت النوم + المؤقت + %1$d دقائق + تم ضبط المؤقت لمدة %1$d دقائق. + مرة واحدة + + لا توجد مرات تشغيل + مرة واحدة (%d) + مرتان (%d) + %d مرات + %d مرة + %d مرة + + عدد مرات التشغيل: %1$s + نهاية المسار الحالي + سيتوقف التشغيل عند نهاية المسار. + تشغيل المفتاح + وقت مخصص + إلغاء المؤقت + نهاية المسار + تم إلغاء المؤقت. + لا يمكن تفعيل خيار نهاية المسار: لا توجد أغنية نشطة حالياً. + تم إلغاء تفعيل مؤقت نهاية المسار: تغيرت الأغنية من %1$s إلى %2$s. + المسار السابق + المسار الحالي + ضبط مدة مخصصة + + + التالي في القائمة + قائمة الانتظار فارغة حالياً. + + لا توجد مسارات منتظرة. + مسار واحد منتظر. + مساران منتظران. + %d مسارات منتظرة. + %d مساراً منتظراً. + %d مسار منتظر. + + قائمة الانتظار + قائمة الانتظار فارغة. + إعادة ترتيب الأغنية + تبديل التشغيل العشوائي + تبديل التكرار + مؤقت النوم + إجراءات قائمة الانتظار + تحديد موقع الأغنية الحالية + مسح قائمة الانتظار + مسح قائمة الانتظار + هل أنت متأكد من أنك تريد مسح جميع الأغاني من قائمة الانتظار باستثناء الأغنية الحالية؟ + حفظ كقائمة تشغيل + قائمة انتظار %1$s + قائمة الانتظار الحالية + تجاهل الأغنية + تمت الإزالة + حفظ كقائمة تشغيل + إلغاء تحديد الكل + اسم قائمة التشغيل + ابحث عن أغانٍ لتضمينها… + لا توجد أغانٍ تطابق \"%1$s\" + + لم يتم تحديد أي أغنية + تم تحديد أغنية واحدة + تم تحديد أغنيتين + تم تحديد %d أغانٍ + تم تحديد %d أغنية + تم تحديد %d أغنية + + حفظ باسم: %1$s + أدخل اسماً لقائمة التشغيل + إزالة من قائمة التشغيل + المزيد من الخيارات لـ %1$s + + + كلمات الأغنية + جاري تحميل كلمات الأغنية… + متزامنة + عادية + خيارات كلمات الأغنية + −.٥ + −.١ + +.١ + +.٥ + ٠ ثانية + %1$+.1f ث + + + فشل البحث عن كلمات الأغنية + فشل جلب كلمات الأغنية من الخادم + انتهت مهلة الاتصال. يرجى التحقق من اتصالك بالإنترنت. + خطأ في الشبكة. يرجى التحقق من اتصالك بالإنترنت. + خطأ في الخادم (رمز %d). يرجى المحاولة مرة أخرى لاحقاً. + + + كلمات الأغنية متوفرة بالفعل. تم تخطي الجلب عبر الإنترنت. + الكلمات المضمنة موجودة بالفعل. تم تخطي الجلب عبر الإنترنت. + ملف الكلمات المحلي (.lrc) موجود بالفعل. تم تخطي الجلب عبر الإنترنت. + + + حفظ كلمات الأغنية + الترجمة بواسطة الذكاء الاصطناعي + تحتوي كلمات هذه الأغنية على ترجمة بالفعل + كلمات هذه الأغنية مكتوبة بهذه اللغة بالفعل + لم يتم تكوين مفتاح الـ API + تمت ترجمة كلمات الأغنية بنجاح! + جاري ترجمة الكلمات عبر الذكاء الاصطناعي... + إعادة تعيين الكلمات المستوردة + إعادة تعيين كلمات الأغاني؟ + هل أنت متأكد من أنك تريد إعادة تعيين كلمات هذه الأغنية؟ + المظهر + المحاذاة + محاذاة الكلمات لليسار + محاذاة الكلمات للوسط + محاذاة الكلمات لليمين + عناصر التحكم + ضبط المزامنة + إخفاء عناصر تحكم المزامنة + إظهار اللتنّة (Romanization) + إظهار الترجمات + تعطيل الوضع الغامر (لمرة واحدة) + إبقاء الشاشة قيد التشغيل + + + حفظ كلمات الأغنية + اختر النسخة المراد حفظها: + مزامنة (مع الطوابع الزمنية) + عادية (نص فقط) + + + هل تود البحث عن كلمات الأغنية عبر الإنترنت؟ + إظهار خيارات كلمات الأغنية + فتح أداة الاختيار دائماً بدلاً من التطبيق التلقائي لأول تطابق + جاري البحث عن كلمات الأغنية… + لم يتم العثور على كلمات الأغنية + لم نتمكن من العثور على كلمات الأغنية تلقائياً. يمكنك تعديل العنوان أو اسم الفنان والمحاولة بالبحث يدوياً. + عنوان الأغنية + الفنان (اختياري) + تم العثور على %d من التطابقات + متزامنة + %1$s • %2$s + الكلمات مقدمة من + https://lrclib.net/ + diff --git a/app/src/main/res/values-ar/strings_presentation_batch_a.xml b/app/src/main/res/values-ar/strings_presentation_batch_a.xml deleted file mode 100644 index 904610ae4..000000000 --- a/app/src/main/res/values-ar/strings_presentation_batch_a.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - ملاحظة أمان: يتم إدخال كلمة المرور الخاصة بك فقط داخل صفحات ويب QQ Music. يقوم PixelPlayer بحفظ ملفات تعريف ارتباط الجلسة (Cookies) لمزامنة مكتبتك الموسيقية. - ملاحظة أمان: يتم إدخال كلمة المرور الخاصة بك فقط داخل صفحات ويب NetEase. يقوم PixelPlayer بحفظ ملفات تعريف ارتباط الجلسة (MUSIC_U) لمزامنة مكتبتك الموسيقية. - فشل في قراءة ملفات تعريف ارتباط QQ Music: %1$s - فشل في قراءة ملفات تعريف ارتباط NetEase: %1$s - - جاري إعداد Google Drive… - ربط Google Drive - بث ملفات الموسيقى مباشرة من حساب Google Drive الخاص بك - تسجيل الدخول باستخدام Google - اختر مجلد الموسيقى - اختر أو أنشئ مجلداً لاستخدامه كمصدر للموسيقى الخاصة بك - إنشاء مجلد \"PixelPlayer Music\" - أنشئ مجلداً جديداً هنا للموسيقى الخاصة بك - لا توجد مجلدات هنا - diff --git a/app/src/main/res/values-ar/strings_presentation_batch_b.xml b/app/src/main/res/values-ar/strings_presentation_batch_b.xml deleted file mode 100644 index a0713c170..000000000 --- a/app/src/main/res/values-ar/strings_presentation_batch_b.xml +++ /dev/null @@ -1,88 +0,0 @@ - - - - الخدمات المرتبطة - الحسابات المتصلة - إدارة المزودين المرتبطين وإبقاء كل عملية ربط تحت سيطرتك. - نشط - متاح - قريباً - متصل - فتح الخدمة - قريباً - جاري تسجيل الخروج… - لا توجد حسابات مرتبطة بعد - قم بربط أحد المزودين لتتمكن من إدارته من هذه الشاشة. - ربط %1$s - %1$s (قريباً) - Telegram - NetEase Music - - - فرز الأغاني - المزيد من الخيارات - تشغيل - إضافة أغانٍ - إضافة - إزالة الأغاني - إعادة ترتيب الأغاني - إعادة ترتيب - إعادة ترتيب الأغنية - قائمة التشغيل هذه فارغة. - هذا المجلد لا يحتوي على أغانٍ. - انقر على "إضافة أغانٍ" للبدء. - خيارات قائمة التشغيل - تعديل قائمة التشغيل - حذف قائمة التشغيل - تعيين الانتقال الافتراضي - تصدير قائمة التشغيل - حذف قائمة التشغيل؟ - هل أنت متأكد من أنك تريد حذف قائمة التشغيل هذه؟ - إعادة تسمية قائمة التشغيل - الاسم الجديد - - - المزيج اليومي - - - اختر الأغاني - اختر النوع - البحث عن الأغاني - تحديد الكل - مسح - النوع: %1$s - اختر نوعاً - الملء السريع - إضافة نوع مخصص - نوع جديد - إضافة نوع مخصص - اسم النوع - اختر أيقونة - - - المشغلة حديثاً - تشغيل الأحدث - لا توجد أغانٍ مشغلة مؤخراً في %1$s - قم بتغيير النطاق الزمني أو تشغيل المزيد من الأغاني لملء هذا الجدول الزمني. - المشغلة حديثاً - اليوم - أمس - - - ضبط نصف قطر الزوايا - قم بمطابقة زوايا شريط التنقل مع الزوايا الفيزيائية لجهازك للحصول على مظهر متناسق وانسيابي. - نصف قطر الزوايا - %1$d dp - - - تشغيل %1$s عشوائياً - - - لا توجد أغانٍ • %2$s - أغنية واحدة • %2$s - أغنيتان • %2$s - %1$d أغانٍ • %2$s - %1$d أغنية • %2$s - %1$d أغنية • %2$s - - diff --git a/app/src/main/res/values-ar/strings_presentation_batch_c.xml b/app/src/main/res/values-ar/strings_presentation_batch_c.xml deleted file mode 100644 index 23cf57e71..000000000 --- a/app/src/main/res/values-ar/strings_presentation_batch_c.xml +++ /dev/null @@ -1,83 +0,0 @@ - - - - خطأ أثناء تحميل الأغاني - خطأ أثناء تحميل الألبومات - خطأ أثناء تحميل الفنانين - إعادة المحاولة - - - لم يتم العثور على أغانٍ في مكتبتك. - جرّب إعادة فحص مكتبتك من الإعدادات إذا كانت لديك ملفات موسيقى على جهازك. - لم يتم العثور على أغانٍ - - - جديد - إنشاء قائمة تشغيل جديدة - استيراد قائمة تشغيل M3U - تحديد موقع الأغنية الحالية - جميع الأغاني - سحابي - محلي - خيارات الفرز - - - متزامنة - الفنان (اختياري) - - - إضافة أغانٍ - إضافة الأغاني المحددة - إضافة - بحث أو تصفية الأغاني… - المحبوبة - فشل تحميل الأغاني - تحميل المزيد - - - ذكاء اصطناعي - منسقة بشكل مثالي - المزيج اليومي - رحلتك الصوتية جاهزة الآن - صانع قوائم التشغيل بالذكاء الاصطناعي - صف الأجواء أو الحالة المزاجية أو النشاط، ودع الذكاء الاصطناعي يتولى تنسيق قائمة التشغيل المثالية من مكتبتك. - حجم قائمة التشغيل - الحد الأدنى للأغاني - الحد الأقصى للأغاني - مثال: أجواء مسائية هادئة، طاقة حماسية للتمارين… - انقر لإعادة المحاولة - تم توليف رحلتك الصوتية! - جاري الإنشاء… - جاهز للتشغيل - إنشاء قائمة التشغيل - - - لا توجد أغانٍ بعد - أضف موسيقى إلى جهازك أو قم بمزامنة مصدر سحابي لبدء الاستماع. - لم يتم العثور على أغانٍ محلية - جرّب فلتراً آخر للمصادر أو أعد فحص مكتبة جهازك. - لم يتم العثور على أغانٍ سحابية - قم بمزامنة أغانٍ من Telegram أو NetEase، أو تحوّل إلى المصدر المحلي. - لا توجد ألبومات متاحة - ستظهر الألبومات هنا بمجرد أن تحتوي مكتبتك على مسارات مجمعة. - لم يتم العثور على ألبومات محلية - يلزم وجود أغانٍ محلية لإنشاء مجموعات ألبومات محلية. - لم يتم العثور على ألبومات سحابية - ستظهر الأغاني السحابية التي تحتوي على بيانات الألبوم هنا بعد المزامنة. - لا يوجد فنانون متاحون - يتم عرض الفنانين بعد فهرسة الأغاني من أي مصدر. - لم يتم العثور على فنانين محليين - لا تتوفر بيانات وصفية للفنانين للأغاني المحلية في الوقت الحالي. - لم يتم العثور على فنانين سحابيين - تظهر إدخالات الفنانين السحابيين عند مزامنة الأغاني عن بُعد. - لا توجد أغانٍ مفضلة بعد - انقر على أيقونة القلب أثناء تشغيل أي أغنية لحفظها هنا. - لا توجد أغانٍ محلية مفضلة - قم بتغيير فلتر المصدر أو أضف إعجاباً بالأغاني الموجودة على جهازك. - لا توجد أغانٍ سحابية مفضلة - أضف إعجاباً بمسارات Telegram أو NetEase لرؤيتها في هذا العرض. - لم يتم العثور على مجلدات - ستظهر مجلدات وحدة التخزين الداخلية التي تحتوي على موسيقى هنا. - لا توجد قوائم تشغيل بعد - أنشئ قائمة تشغيلك الأولى لتنظيم مكتبتك الموسيقية. - diff --git a/app/src/main/res/values-ar/strings_presentation_batch_d.xml b/app/src/main/res/values-ar/strings_presentation_batch_d.xml deleted file mode 100644 index 45159b522..000000000 --- a/app/src/main/res/values-ar/strings_presentation_batch_d.xml +++ /dev/null @@ -1,124 +0,0 @@ - - - المكتبة - نقل إلى الساعة - الإعدادات - تعديل - إعادة ترتيب علامات التبويب - فرز حسب - سحابي - عرض - قنوات Telegram السحابية - عرض قوائم التشغيل - شبكة - قائمة - الذاكرة الداخلية - بطاقة SD - بطاقة SD غير متاحة حالياً. - عرض المواضيع - القنوات - المواضيع - كلاهما - سحابي - سحابي فقط - جاري إنشاء البيانات الوصفية بالذكاء الاصطناعي… - يمكنك تحديد ما يصل إلى %1$d ألبومات - مجلد - توسيع القائمة - علامات تبويب المكتبة - الانتقال مباشرة إلى أي علامة تبويب أو إعادة ترتيبها. - إعادة ترتيب علامات التبويب - مجلد - - جاري الإرسال إلى الساعة - جاري بدء النقل… - جاري النقل - تم بنجاح - فشل النقل - تم الإلغاء - جاري التحضير - جاري تحضير النقل… - إلغاء النقل - - دمج قوائم التشغيل - أدخل اسماً لقائمة التشغيل المدمجة: - قائمة تشغيل مدمجة - سيؤدي هذا إلى دمج %1$d من قوائم التشغيل المحددة في قائمة واحدة. - - مساحة الـ DJ - جاري التحميل… - منصة (Deck) %1$d - تحميل أغنية - لم يتم تحميل أي أغنية - - ميزة فصل المسارات الصوتيّة (Stems) غير متاحة بعد. - مستوى الصوت - السرعة - ممازج الصوت (Crossfader) - منصة 1 - منصة 2 - اختر أغنية - - تغيير وضع العرض - تعطيل موازن الصوت - تمكين موازن الصوت - تعديل - تعديل الإعدادات المسبقة - إعداد مخصص - الإعدادات المسبقة - تحديث - تضخيم الباس (Bass Boost) - المحاكي المحيطي (Virtualizer) - جهارة الصوت (Loudness) - غير مدعوم - غير مدعوم على هذا الجهاز - مستوى الصوت - الاستجابة الترددية - هرتز - الباس (الترددات المنخفضة) - الترددات المتوسطة المنخفضة - الترددات المتوسطة المرتفعة - التريبل (الترددات الحادة) - الباس / منخفض - متوسط / مرتفع - صفحة %1$d - إعادة تعيين المدة - - يتم استخدام الإعدادات الافتراضية العامة - تم حفظ التغييرات بنجاح - قواعد قائمة التشغيل - الانتقالات العامة - حفظ - تخصيص السلوك الافتراضي لقائمة التشغيل هذه تحديداً. - يطبق هذا التكوين على جميع مصادر التشغيل ما لم يتم تجاوزه. - حالة التنشيط - الافتراضي العام - تابع للإعداد العام - تجاوز مخصص - افتراضي قائمة التشغيل - تجاوز مخصص - قم بالتمكين لتعيين قواعد خاصة بقائمة التشغيل هذه. - نمط الانتقال - كيفية تداخل المسارات الصوتية معاً - التداخل المتلاشي (Crossfade) - بدون انتقال - مدة الانتقال - إجمالي التداخل %1$d ثوانٍ - إعادة تعيين - الأغنية الحالية - الأغنية التالية - ستتداخل المسارات لمدة %1$d ثوانٍ - منحنيات مستوى الصوت - ضبط ميل وتلاشي الصوت بدقة - تلاشي للخارج (Fade Out) - تلاشي للداخل (Fade In) - - تشغيل %1$s - طي %1$s - توسيع %1$s - تعديل صورة الفنان - تغيير الصورة - إعادة تعيين للوضع الافتراضي - تشغيل أغاني الفنان عشوائياً - الفنان - diff --git a/app/src/main/res/values-ar/strings_presentation_batch_e.xml b/app/src/main/res/values-ar/strings_presentation_batch_e.xml deleted file mode 100644 index 22192fce5..000000000 --- a/app/src/main/res/values-ar/strings_presentation_batch_e.xml +++ /dev/null @@ -1,160 +0,0 @@ - - - - قائمة الانتظار فارغة. - إجراءات قائمة الانتظار - مسح قائمة الانتظار - حفظ كقائمة تشغيل - تحديد موقع الأغنية الحالية - قائمة انتظار %1$s - قائمة الانتظار الحالية - تمت الإزالة - مسح قائمة الانتظار - هل أنت متأكد من أنك تريد مسح جميع الأغاني من قائمة الانتظار باستثناء الأغنية الحالية؟ - التالي في القائمة - قائمة الانتظار فارغة حالياً. - قائمة الانتظار - تبديل التشغيل العشوائي - تبديل التكرار - مؤقت النوم - حفظ كقائمة تشغيل - إلغاء تحديد الكل - اسم قائمة التشغيل - ابحث عن أغانٍ لتضمينها… - حفظ باسم: %1$s - أدخل اسماً لقائمة التشغيل - لا توجد أغانٍ تطابق \"%1$s\" - تجاهل الأغنية - إزالة من قائمة التشغيل - المزيد من الخيارات لـ %1$s - - - لا توجد مسارات منتظرة. - مسار واحد منتظر. - مساران منتظران. - %d مسارات منتظرة. - %d مساراً منتظراً. - %d مسار منتظر. - - - لم يتم تحديد أي أغنية - تم تحديد أغنية واحدة - تم تحديد أغنيتين - تم تحديد %d أغانٍ - تم تحديد %d أغنية - تم تحديد %d أغنية - - - - لم يتم إنشاء أي قائمة تشغيل بعد. - المس زر "قائمة تشغيل جديدة" للبدء. - - - إنشاء قائمة تشغيل - اختر طريقة الإنشاء. - يدوياً - صمم الغلاف، الأيقونة، الشكل، واشحن الأغاني بنفسك. - بالذكاء الاصطناعي - أنشئ قائمة تشغيل منسقة ومخصصة عبر خيارات متقدمة. - تتطلب هذه الميزة تهيئة مفتاح Gemini API في الإعدادات. - إعداد مفتاح API - - - معمل قوائم التشغيل بالذكاء الاصطناعي - إعادة تعيين - جاري الإنشاء… - إنشاء - الهدف والأجواء - اسم قائمة التشغيل (اختياري) - ما هي الأجواء التي ترغب بها في قائمة التشغيل هذه؟ - مثال: قيادة وقت الغروب مع ألحان سينث دافئة - الاتجاه الفني - الحالة المزاجية - النشاط - الحقبة الزمنية - محرك التنسيق - الحيوية والطاقة - تتحكم في حدة الأغاني وإيقاعها. 1 = هادئ/بطيء، 5 = حماسي جداً/سريع. - عمق الاكتشاف - تتحكم في مدى معرفتك بالاختيارات. 1 = المفضلة الأكثر تشغيلاً، 5 = أغانٍ نادرة ولم تسمعها كثيراً. - أقل عدد أغانٍ - أقصى عدد أغانٍ - الفلاتر - أنواع موسيقية مفضلة (اختياري) - مثال: سينث ويف، إندي بوب - أنواع موسيقية تتجنبها (اختياري) - مثال: ميتال، هارد تراب - اللغة المفضلة (اختياري) - مثال: الإنجليزية، العربية، معزوفات موسيقية - إعطاء الأولوية للمفضلة - تجنب الكلمات غير لائقة (Explicit) - معاينة الأمر (Prompt) - سيظهر أمرك النهائي هنا بمجرد إضافة تفضيلاتك. - تنسيق بدقة متناهية - حدد المزاج، النشاط، القيود، وعمق الاختيارات. - سيقوم الذكاء الاصطناعي باختيار الأغاني من مكتبتك المحلية فقط. - يرجى إضافة توجيه واحد على الأقل للذكاء الاصطناعي. - يرجى تعيين نطاق أغانٍ صالح. - 5/%1$d - مخصص… - إدخال قيمة مخصصة - أدخل قيمتك المخصصة - - - أي حقبة - الطلب الأساسي: %1$s. - المزاج المستهدف: %1$s. - سياق النشاط: %1$s. - التركيز على الحقبة: %1$s. - إعطاء الأولوية للأنواع: %1$s. - تجنب الأنواع: %1$s. - اللغة المفضلة: %1$s. - مستوى الطاقة المستهدف: 5/%1$d. - هدف الاكتشاف: 5/%1$d حيث 1 تعني مألوف و5 تعني اختيارات عميقة ونادرة. - إعطاء الأولوية للأغاني القريبة من مفضلات المستمع كلما أمكن ذلك. - تجنب الأغاني ذات الكلمات غير اللائقة كلما توفرت بدائل. - الحفاظ على سلاسة الانتقالات وتجنب التكرار المتتالي لنفس الفنان. - - - هادئ (Chill) - حماسي (Energetic) - سعيد (Happy) - داكن/غامض (Dark) - رومانسي (Romantic) - شجي/ميلانكولي (Melancholic) - - - تمارين رياضية (Workout) - تركيز (Focus) - رحلة على الطريق (Road trip) - حفلة (Party) - دراسة (Study) - وقت متأخر من الليل (Late night) - - - @string/presentation_batch_e_ai_era_any - السبعينات - الثمانينات - التسعينات - الألفينات (2000s) - العقد 2010 - العقد 2020 - - - - إعادة تعيين الإعدادات المسبقة - سيؤدي هذا إلى استعادة الترتيب الافتراضي وظهور الإعدادات المسبقة. هل تريد المتابعة؟ - إدارة الإعدادات المسبقة - اسحب لإعادة الترتيب • انقر على أيقونة العين للإظهار أو الإخفاء - إعادة تعيين للوضع الافتراضي - مرئي - مخفي - - - كيف يتم بناء المزيج اليومي الخاص بك - يتم بناء المزيج اليومي الخاص بك (Daily Mix) بناءً على أغانيك المفضلة والأكثر تشغيلاً. نقوم أيضاً بإضافة مسارات من فنانين وأنواع موسيقية تحبها لتتمكن من اكتشاف موسيقى جديدة. - أخبر الذكاء الاصطناعي بما تود الاستماع إليه اليوم - نحن نستخدم عينة صغيرة للحفاظ على انخفاض استهلاك البيانات والتكلفة - جاري التحديث… - تحديث المزيج اليومي - diff --git a/app/src/main/res/values-ar/strings_presentation_batch_f.xml b/app/src/main/res/values-ar/strings_presentation_batch_f.xml deleted file mode 100644 index 8dc2ea76c..000000000 --- a/app/src/main/res/values-ar/strings_presentation_batch_f.xml +++ /dev/null @@ -1,239 +0,0 @@ - - - محدد - تحديث المكتبة - فحص المكتبة بأكملها بحثاً عن الملفات الجديدة والمعدلة. - إعادة فحص كاملة - إعادة بناء قاعدة البيانات - جاري تحضير المزامنة - جاري قراءة مخزن الوسائط (MediaStore) - جاري معالجة المسارات - جاري الحفظ في قاعدة البيانات - جاري فحص ملفات كلمات الأغاني (LRC) - جاري تنظيف ذاكرة التخزين المؤقت لأغلفة الألبومات - جاري مزامنة المصادر السحابية - جاري إتمام المزامنة - %1$s • %2$d%% (%3$d/%4$d) - %1$s… - تحديث كلمات الأغاني - جلب كلمات الأغاني تلقائياً لجميع الأغاني باستخدام lrclib. - تحديث كلمات الأغاني - جاري معالجة %1$d من أصل %2$d أغنية - أدخل مفتاح API - حفظ - تم الحفظ! - الأوامر الجاهزة (Presets) - أدخل أمر النظام المستهدف… - إعادة تعيين - المنسق المحترف (Professional Curator) - أنت \'Vibe-Engine\'، منسق موسيقى عالمي وخبير في التدفق الصوتي الانسيابي. هدفك هو بناء تجارب استماع سلسة وعالية الدقة. أعطِ الأولوية للتوافق الهارموني، والانتقالات المنطقية لسرعة الإيقاع (BPM)، والتوازن المدروس بين الأغاني المألوفة المفضلة والاكتشافات الذكية المبنية على نمط الاستماع. - المستكشف المبتكر (Creative Maverick) - أنت مستكشف موسيقى طليعي متخصص في صياغة \'التناغم غير المتوقع\'. مهمتك هي كسر حدود الأنواع الموسيقية التقليدية عبر تحديد ترابطات صوتية غير ظاهرة. أعطِ الأولوية للاختيارات النادرة والعميقة، والتركيبات التجريبية، والتجديد الفني مع الحفاظ على منطق انتقال مفاجئ ومذهل في نفس الوقت. - أمين المكتبة الصارم (Strict Librarian) - أنت مهندس دقيق لقواعد البيانات الموسيقية. منطقك مدفوع بالدقة المطلقة للبيانات الوصفية والالتزام الصارم بالتصنيفات. قلل من المكتشفات الخوارزمية العشوائية لصالح التناسق التام للنوع الموسيقي، ومطابقة مستويات الطاقة، وتعظيم استدعاء التفضيلات المحددة بدقة من قبل المستخدم. - الدليل الأجاوائي (Atmospheric Guide) - أنت خبير في التراكيب الصوتية المحيطية والتدفقات الموسيقية الهادئة. ركز حصرياً على المسارات التي تساعد على الدخول في حالة من \'التركيز العميق\' أو \'السكينة\'. أعطِ الأولوية للدفء الصوتي الآلاتي، والتوزيعات البسيطة، والانتقالات اللطيفة، مع تجنب الأصوات الحادة أو التحولات المفاجئة في النطاق الديناميكي للصوت. - عاشق الهندسية الصوتية (Sonic Enthusiast) - أنت محلل صوتي مهتم بتعقيد الإنتاج والآلات الموسيقية. أعطِ الأولوية للمسارات التي تتميز بنطاق ديناميكي واسع، والإيقاعات المتعددة المعقدة، وجودة المسرح الصوتي الفائقة. فضّل المقطوعات التي تتطلب استماعاً نشطاً وتكافئ المستمع عند الانتباه إلى التفاصيل التقنية وتفاصيل التوزيع الصوتي. - محفز الطاقة (Energy Catalyst) - أنت مولد إيقاعات عالي الحماس والزخم. ترتكز فلسفتك على خطوط الباس القوية، وشدة الإيقاع، والنغمات الجذابة. أعطِ الأولوية للمسارات المتوافقة مع أجواء النوادي ذات الإيقاع السريع (High-BPM)، والطاقة المتزامنة، والتوتر الإيقاعي المستمر للحفاظ على نبض المستمع وتحفيزه في ذروة مستوياته. - - قائمة تشغيل ذكية جديدة - قائمة تشغيل جديدة - إضافة أغانٍ - الرجوع أو الإلغاء - التالي - إنشاء - تعديل قائمة التشغيل - إغلاق - تأكيد القص - تجميعة صور منشأة تلقائياً - إضافة صورة - اختر صورة - تغيير - إزالة - اسم قائمة التشغيل - مزيجي الرائع - تعديل الغلاف - ضبط غلافك الفني - استخدم إيماءات التكبير والسحب للحصول على الإطار المثالي - يدوي - ذكي - الإنشاء باستخدام الذكاء الاصطناعي - قاعدة ذكية - الافتراضي - صورة - أيقونة - لون الخلفية - رمز الأيقونة - نمط الشكل - معلمات الشكل - نصف قطر الزوايا - النعومة - الأضلاع - الانحناء - الدوران - المقاس - الأكثر تشغيلاً - المسارات الأكثر تشغيلاً لديك. - المشغلة حديثاً - الأغاني التي استمعت إليها مؤخراً. - المفضلات المنسية - المسارات المفضلة التي لم تقم بتشغيلها منذ فترة. - جواهر جديدة - المسارات المضافة حديثاً مع نسب تشغيل منخفضة. - - نمط لوحة الألوان (Palette) - اختر ألوان الألبوم لواجهة مستخدم المشغل. - الألوان - تطبيق - متوازن وهادئ. - لمسات حيوية عالية التشبع. - تحولات جريئة في الدرجات والتباين. - لمسات حيوية مبهجة ومائلة. - بقعة نغمية (Tonal Spot) - حيوي (Vibrant) - تعبيري (Expressive) - سلطة فواكه (Fruit Salad) - دقة الألوان - القيمة 0 تحافظ على الضبط الحالي. القيم الأعلى تلتزم بشكل أقرب بالدرجة المهيمنة لغلاف الألبوم. - الحالي - أكثر دقة - 0 • الحالي - %1$d • طفيف - %1$d • متوازن - %1$d • دقيق - - تعديلات تحميل واجهة المشغل - كلمات الأغاني المتحركة (للأجهزة القوية) - تستخدم تأثيرات بصرية ورسوم زنبركية متحركة للكلمات. قد تسبب سقوطاً في معدل الإطارات على الأجهزة الضعيفة. - تأثير تمويه كلمات الأغاني (Blur) - يطبق تمويهاً لعمق الحقل على الكلمات غير النشطة حالياً. - قوة التمويه - ضبط كثافة تأثير التمويه. - %1$.1fx - الخطوة 1 · اختر ما تريد تأخيره - تأخير كل شيء - تجميد محتوى المشغل بالكامل حتى تتمدد خلفية اللوحة بالكامل. - العرض الدوار للألبومات - تأخير عرض غلاف الألبوم والعرض الدوار حتى تتمدد اللوحة السفلية. - البيانات الوصفية للأغنية - تأخير العنوان، الفنان، وإجراءات الكلمات/قائمة الانتظار. - شريط التقدم - تأخير الخط الزمني وعلامات الوقت حتى يكتمل التمدد. - عناصر التحكم في التشغيل - تأخير أزرار التشغيل/الإيقاف المؤقت، التقديم، وعناصر الإعجاب. - جميع المكونات المؤجلة نشطة حالياً. قم بتعطيل \"تأخير كل شيء\" لتخصيص كل جزء على حدة. - الخطوة 2 · تكوين سلوك العناصر النائبة (Placeholders) - استخدام عناصر نائبة للمكونات المؤجلة - الحفاظ على استقرار الواجهة عبر عرض عناصر نائبة خفيفة الوزن أثناء انتظار المكونات للتمدد. - الخطوة 3 · اختر وقت تحول العناصر النائبة إلى المحتوى الحقيقي - اختر وضعاً واحداً. يعتمد وضع العتبة على أشرطة التمرير؛ بينما ينتظر وضع إفلات السحب حتى تترك إيماءة اللوحة. - قم بتمكين مكون مؤجل واحد على الأقل لإلغاء قفل وضع التفعيل. - العتبة (Threshold) - يعتمد على النسبة المئوية للتمدد. - إفلات السحب - يتحول فقط بعد إفلات إيماءة السحب. - عتبة التمدد - مدى التمدد المطلوب للوحة قبل أن تصبح المكونات المؤجلة مرئية. - يظهر المحتوى عند تمدد بنسبة %1$d%% - التطبيق أيضاً عند إغلاق المشغل - استخدام عتبة الإغلاق للتحول مجدداً إلى العناصر النائبة أثناء الطي. - عتبة الإغلاق - مقدار الطي المطلوب قبل أن تتولى العناصر النائبة العرض مرة أخرى. - تظهر العناصر النائبة بعد طي بنسبة %1$d%% - يتجاوز وضع إفلات السحب العتبات وسلوك الإغلاق. يحدث التبديل فقط عندما تنتهي إيماءة سحب اللوحة. - جعل العناصر النائبة شفافة - تحتفظ العناصر النائبة بمساحة تخطيطها ولكن تصبح غير مرئية. - الجودة البصرية - دقة غلاف الألبوم - ميزات تجريبية - منخفضة (256 بكسل) - أداء أفضل - متوسطة (512 بكسل) - متوازنة - عالية (800 بكسل) - جودة أفضل - الأصلية - الجودة القصوى - - %1$d%% - %1$s • %2$s - · %1$s - \? - - تسجيل الدخول إلى Telegram - أنت تقوم بتعديل رقمك الآن. إرسال الرمز مجدداً سيحل محل الرمز السابق. - جاري العمل… - جاري تهيئة Telegram… - جاري تسجيل الخروج… - جاري إغلاق الجلسة… - تم إغلاق الجلسة. أعد فتح تسجيل الدخول للمتابعة. - جاري تحضير جلسة Telegram آمنة… - بانتظار استجابة Telegram… - ربط Telegram - قم بربط حساب Telegram لبث الموسيقى مباشرة من قنواتك ومحادثاتك. - رقم الهاتف - أدخل رقم Telegram الخاص بك. يمكنك العودة وتعديله لاحقاً. - رقم الهاتف - 1 - 5551234567 - إرسال الرمز - رمز التحقق - أدخل الرمز الذي وصلك من Telegram. إذا كان الرقم خاطئاً، عد للخلف لتعديله. - الرمز - 12345 - تعديل الهاتف - إعادة إرسال الرمز - التحقق من الرمز - التحقق بخطوتين (كلمة المرور) - أدخل كلمة مرور Telegram الخاصة بك. لا يزال بإمكانك العودة لتصحيح رقمك. - كلمة المرور - التحقق من كلمة المرور - يرجى الانتظار… - - قنوات Telegram - إضافة قناة - قناة Telegram عامة - جاري المزامنة - المزامنة الآن - طي المواضيع - إظهار المواضيع - خيارات القناة - المواضيع - جاري مزامنة القناة - جاري تحديث الأغاني من Telegram - جلب أحدث الأغاني من هذه القناة - إزالة القناة - إيقاف المزامنة وحذف الأغاني المخزنة مؤقتاً - حذف القناة؟ - ستتوقف مزامنة %1$s وسيتم حذف جميع الأغاني المخزنة مؤقتاً من هذه القناة. - إزالة - لم يتم مزامنة أي قنوات بعد - أضف قنوات Telegram عامة لمزامنة\nمكتبتك الموسيقية - إضافة قناة - لم تُزامن مطلقاً - تمت المزامنة %1$s - - إضافة قناة - ابحث عن قناة Telegram عامة لمزامنة موسيقاها - اسم_القناة@ أو الرابط - بحث - جاري البحث… - البحث عن قناة - أدخل اسم المستخدم لقناة عامة أو الرابط الخاص بها\nلمزامنة ملفاتها الصوتية - تم - - - لا توجد أغانٍ - أغنية واحدة (%d) - أغنيتان (%d) - %d أغانٍ - %d أغنية - %d أغنية - - - لا توجد مواضيع - موضوع واحد (%d) - موضوعان (%d) - %d مواضيع - %d موضوعاً - %d موضوع - - diff --git a/app/src/main/res/values-ar/strings_presentation_batch_g.xml b/app/src/main/res/values-ar/strings_presentation_batch_g.xml deleted file mode 100644 index 579f9860e..000000000 --- a/app/src/main/res/values-ar/strings_presentation_batch_g.xml +++ /dev/null @@ -1,636 +0,0 @@ - - - - اليوم - الأسبوع الحالي - الشهر الحالي - السنة الحالية - كل الأوقات - إحصائيات الاستماع - تحديث إحصائيات الاستماع - الاستماع - مرات التشغيل - - عادات الاستماع - لا توجد عادات استماع بعد - سنقوم بإظهار عادات الاستماع الخاصة بك بمجرد أن نتعرف على ذوقك بشكل أفضل. - إجمالي الجلسات - معدل الجلسة - أطول جلسة - جلسة/يوم - اليوم الأكثر نشاطاً - لم يتم التشغيل بعد - فترة الذروة الزمنية - وقت الاستماع - إجمالي وقت الاستماع الذي تم تسجيله في النطاق المحدد. - عدد مرات التشغيل - عدد الجلسات التي أكملتها لكل شريحة زمنية. - معدل الجلسة - متوسط مدة الاستماع لكل شريحة زمنية. - %1$d تشغيل - الخط الزمني للاستماع - لا توجد بيانات استماع بعد - اضغط على زر التشغيل لبدء بناء خطك الزمني للاستماع - الإيقاع اليومي - الإيقاع الأسبوعي - الإيقاع الشهري - نظرة عامة على السنة - التطور على مر الوقت - مجمعة في شرائح مدتها 4 ساعات - مجمعة حسب أيام الأسبوع - مجمعة حسب أسبوع الشهر - مجمعة حسب الشهر - مجمعة حسب السنة - شريحة الذروة - مقسمة إلى فترات مدتها 4 ساعات للكشف عن إيقاعك اليومي. - تسهل الأشرطة اليومية مقارنة عادات الاستماع من أسبوع لآخر. - توضح الأشرطة الأسبوعية اتجاهات الشهر وتطورها. - تظهر الأشرطة الشهرية التغيرات الموسمية على مدار السنة. - تختصر الأشرطة السنوية كامل تاريخ الاستماع الخاص بك. - الفئات الأعلى - قارن بين طرق استماعك عبر الأنواع الموسيقية، الفنانين، الألبومات، والأغاني. - %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 - \? - - - معلومات الجهاز - برامج ترميز الصوت المدعومة (Codecs) - مخرج الصوت - محرك ExoPlayer - معدل العينة - الإطارات لكل مخزن مؤقت - دعم زمن الانتقال المنخفض - دعم الصوت الاحترافي (Pro Audio) - الإصدار - المصّيرات النشطة - عدادات فك الترميز - %1$d هرتز - نعم - لا - مسرع بواسطة الأجهزة - الشركة المصنعة - الموديل - العلامة التجارية - الجهاز - إصدار أندرويد - إصدار SDK - المكونات المادية (Hardware) - - - هذا الجهاز - -- - جاهز للتشغيل - التشغيل يتطلب مراجعة - التنسيقات - أجهزة فك الترميز المادية - الأغاني المحلية - مساحة تخزين الموسيقى المحلية - حجم الموسيقى - %1$d أغنية محلية - المتاح - الإجمالي %1$s - البصمة التخزينية للموسيقى - المستخدم من الجهاز - %1$d%% - <1% - %1$d أغنية سحابية - %1$d ملف غير قابل للقراءة - مسار التشغيل - %1$d إطار لكل مخزن مؤقت - Hi-Fi PCM Float - مسار مخرج 32-بت عائم - الذاكرة - متاح من أصل %1$s - التنسيقات الجاهزة للإرسال المباشر (Offload) - لم تبلغ أي تنسيقات مضغوطة عن دعم ميزة الـ hardware offload. - المخارج المكتشفة - لم يتم الإبلاغ عن أي مسارات إخراج بواسطة أندرويد. - %1$s مصّيرات - توافق التنسيقات - %1$d مسار مدعوم - %1$d تنسيق غير معروف - لم يتم الإبلاغ عن برنامج فك ترميز - فك ترميز مادي (Hardware) - فك ترميز برمجى (Software) - إرسال مباشر (Offload) - %1$d في المكتبة - تقرير الأداء - قم بإنشاء تقرير تشخيصي قابل للمشاركة لمساعدتنا في تصنيف مشكلات بطء التشغيل أو الفحص. يحتوي التقرير فقط على بيانات الجهاز، المكتبة، والتوقيت — لا يتضمن مسارات ملفات أو عناوين أو فنانين. - إنشاء التقرير - إعادة إنشاء - نسخ - مشاركة - تم نسخ التقرير إلى الحافظة - تقرير أداء PixelPlay - نتائج التوافق - لا توجد حالات عدم توافق رئيسية - تتطابق مساراتك المفهرسة مع برامج فك الترميز التي يبلغ عنها نظام أندرويد في هذا الجهاز. - قد لا يتم فك ترميز %1$d مسار بشكل أصلي - التنسيقات التي تحتاج لمراجعة: %1$s. - قد يتم إعادة عينة %1$d مسار محلي - تصل المكتبة إلى %1$d هرتز، وهو أعلى من معدل عينة المخرج الحالي. - تمتلك %1$d مسارات بيانات وصفية غير معروفة - يمكن لإعادة فحص المكتبة بالكامل ملء بيانات MIME ومعدل البت ومعدل العينة المفقودة. - +%1$d أكثر - المكبر المدمج - صوت البلوتوث - صوت USB - سماعة سلكية - مخرج رقمي - مخرج آخر - - - الإدخال (Input) - الإخراج (Output) - التفكير (Thought) - %1$s: %2$s - MMM dd، HH:mm - تحليل الفنانين المتعددين - محددات الرموز - الحالي: %1$s - محددات الكلمات - لا يوجد - الحالي: %1$s - - تكوين - استخراج الفنانين من العنوان - اكتشاف عبارات .feat و .ft و with في عناوين الأغاني - تنظيم المكتبة - التجميع حسب فنان الألبوم - إظهار ألبومات العمل المشترك تحت اسم الفنان الرئيسي - حول تحليل الفنانين المتعددين - يقوم PixelPlayer بفصل علامات الفنانين باستخدام محددات الرموز مثل (/, ;, &) ومحددات الكلمات مثل (feat., ft., vs., x). يتم مطابقة محددات الكلمات دون الحساسية لحالة الأحرف.\n\nتكتشف ميزة "استخراج الفنانين من العنوان" الأنماط مثل (feat. Artist) في عناوين الأغاني.\n\nيمكن استخدام الشرطة المائلة الخلفية (\\) لتخطي محددات الرموز. - - أمثلة - \"Artist1/Artist2\" - Artist1، Artist2 - \"Drake feat. Rihanna\" - Drake، Rihanna - \"Marshmello x Bastille\" - Marshmello، Bastille - \"Song (ft. B)\" بواسطة A - A، B - \"AC\\DC\" - AC/DC (تم تخطي المحدد) - الفنانون - إعادة الفحص مطلوبة - تغيرت إعدادات الفنانين. أعد فحص مكتبتك لتطبيق التغييرات. - جاري الفحص… - إعادة الفحص - - - β - تجريبي (Beta) - تليجرام - سجل التغييرات - الإعدادات - متزامنة - ثابتة - خيارات كلمات الأغاني - البث السحابي - بث الموسيقى مباشرة من حساباتك السحابية - المصدر - الترتيب - تنازلي - تصاعدي - الترتيب الأصلي - اضغط للتبديل إلى التصاعدي - اضغط للتبديل إلى التنازلي - هذا الفرز يحافظ على ترتيبه الأصلي - المفتاح مفعل - - - إغلاق - تحديث - تم - تم - كل شيء مسموح به افتراضياً. اضغط مطولاً على أي مجلد لتمييزه كـ مستبعد من الفحص. - لا توجد مجلدات فرعية هنا - الانتقال للأعلى - الانتقال إلى الدليل الرئيسي - - - المزيج اليومي (Daily Mix) - المزيج اليومي - بناءً على تاريخ الاستماع - تحقق من كامل المزيج اليومي - أغنية محددة - أغنية محددة - مشاركة المحدد - إعجاب بالمحدد - تشغيل - الكل - إلغاء تحديد الكل - خيارات إضافية - خيارات - +%1$d - %1$s • %2$s - محدد - خيارات إضافية لـ %1$s - غلاف الألبوم لـ %1$s - جاري التشغيل - %1$d%% - - - إحصائيات الاستماع - إجمالي التشغيل - المعدل يومياً - المسار الأعلى - %1$s • %2$d تشغيل - المشغلة حديثاً - −.٥ - −.١ - +.١ - +.٥ - ٠ ثانية - %1$+.1f ث - - - فتح متجر Play - متابعة النسخة التجريبية - سيتم تفعيل رابط متجر Play من تكوين GitHub. - PixelPlayer متاح الآن على Google Play - استخدم القناة المستقرة على Google Play للحصول على التحديثات الرسمية بينما نبقي البناء التجريبي نشطاً. - PixelPlayer - إعلان الإصدار - قريباً - - - فرز وتشغيل - خلط عشوائي - فرز حسب - الفنان - الألبوم - العنوان - محدد - سجل التغييرات - عرض على GitHub - التفضيلات المسبقة المحفوظة - لم يتم حفظ تفضيلات مخصصة بعد. - إلغاء التثبيت - تثبيت - إعادة تسمية - حذف - - - الإصدار التجريبي 0.7.0 - مرحباً بك في PixelPlayer 0.7.0-beta - أنت تستخدم بناءً تجريبياً قد يحتوي على أخطاء، أو حالات توقف مفاجئ، أو ميزات تجريبية. ساعدنا في التحسين من خلال الإبلاغ عن المشكلات. - ماذا تتوقع - قد تحدث أخطاء، توقفات مفاجئة، أو ميزات غير مكتملة بشكل غير متوقع. - بعض الميزات قد تتغير أو تُزال دون إشعار مسبق. - قد تكون النسخ التجريبية غير مستقرة مقارنة بالإصدارات الرسمية. - تحقق دائماً من التحديثات قبل الإبلاغ عن مشكلة معروفة. - ما يمكن أن تغيره، تعلبه أو تحسنه النسخ التجريبية أثناء الاختبار. - اختصار مشكلات GitHub - ابحث أولاً، ثم افتح تقريراً مركزاً للأخطاء، التوقفات المفاجئة، الطلبات، أو الاستفسارات. - فتح المشكلات الحالية - الإبلاغ عن مشكلة أو توقف مفاجئ - شاركنا خطوات إعادة إنتاج المشكلة، النتائج المتوقعة، النتائج الفعلية، وتفاصيل جهازك/نظام التشغيل. - كيفية الإبلاغ - قائمة مراجعة سريعة قبل فتح تذكرة مشكلة جديدة. - قبل فتح تذكرة مشكلة - ابحث في المشكلات المفتوحة والمغلقة الحالية لتجنب التكرار. - حدث إلى آخر إصدار من PixelPlayer وتأكد من استمرار حدوث المشكلة. - أعد تشغيل التطبيق وتأكد من بقاء المشكلة قائمّة. - حاول تكرار حدوث المشكلة واكتب الخطوات الدقيقة لذلك. - ما هو نوع المشكلة؟ - تقرير خطأ برمي (Bug): شيء ما يتصرف بشكل غير صحيح. - طلب ميزة: إضافة ميزة جديدة أو تحسين. - سؤال: استخدم قسم المناقشات إذا كان مفعلاً، أو افتح تذكرة بعلامة سؤال. - تقرير خطأ برمجى - انسخ هذه الحقول عندما يتصرف شيء ما بشكل غير صحيح أو يتوقف فجأة. - تقرير خطأ - ملخص قصير: - السلوك المتوقع: - السلوك الحالي: - خطوات التشغيل/إعادة الإنتاج: 1. 2. 3. - كم مرة يحدث ذلك؟ دائماً / أحياناً / نادراً. - لقطة شاشة / فيديو: إن وجد. - السجلات / تتبع الكومة (Stack trace): إن وجد. - البيئة البرمجية - إصدار PixelPlayer: - مصدر التثبيت: إصدار GitHub، بناء تصحيح خطأ، بناء ليلي، إلخ. - إصدار أندرويد: - موديل الجهاز: - سياق إضافي: استخدام بطاقة SD، إعدادات خاصة، أذونات، إلخ. - طلب ميزة جديد - انسخ هذه الحقول عندما ترغب في طلب ميزة جديدة أو تحسين. - بيان المشكلة: ما هي المشكلة التي تحاول حلها؟ - الحل المقترح: كيف يجب أن تعمل الميزة؟ - البدائل المدروسة: هل توجد أي مقاربات أخرى؟ - النطاق: ما هي الشاشات أو التدفقات المتأثرة؟ - نموذج مبدئي (Mockup) أو صورة مرجعية إن وجدت. - العناوين، الخصوصية والنطاق - اجعل التقرير سهلاً للفرز وآمناً للمشاركة. - عناوين جيدة للمشكلات - معادل الصوت: مؤشر الإزاحة يتغير عند تبديل تبويب التفضيلات - البحث: قائمة السجل لا تظهر عند الاستعلام الفارغ - ميزة: إضافة خيار فرز قائمة التشغيل حسب "المضافة حديثاً" - يرجى تجنب - التقارير العامة مثل "إنه لا يعمل". - جمع مشكلات متعددة غير مترابطة في تذكرة واحدة. - السجلات أو لقطات الشاشة غير المظللة التي تحتوي على بيانات خاصة. - ملاحظة الخصوصية - قبل نشر السجلات، لقطات الشاشة، أو الفيديوهات، قم بإزالة أي معلومات شخصية أو خاصة. - - - البناء الليلي (Nightly builds) - كيف تختلف البناءات الليلية عن الإصدارات الرسمية، وماذا تضمن عندما تتعطل. - يتم إنشاء البناءات الليلية من آخر التزامات برمجية (Commit)، وقد تحتوي على تغييرات غير مكتملة، أخطاء مؤقتة، أو تراجعات في الأداء. إنها تجريبية أكثر من الإصدارات الرسمية. - يمكنك الوصول إليها من ملحقات سير عمل GitHub Actions الخاصة بالمستودع إن وجدت. - الإبلاغ عن مشكلات البناء الليلي - عند الإبلاغ عن مشكلة من بناء ليلي، اذكر دائماً أن ذلك حدث في نسخة ليلية وليس في إصدار رسمي. يرجى تضمين تاريخ البناء، اسم أو رقم تشغيل سير العمل، أو معرف الالتزام (Commit SHA) إن أمكن. وتحقق أيضاً مما إذا كانت نفس المشكلة تحدث في أحدث إصدار رسمي. - التحديث إلى Beta 0.5.0 - يُوصى بتثبيت نظيف - إذا كنت قادماً من الإصدار التجريبي 0.5.0، فقد يتطلب هذا التحديث بيانات مكتبة جديدة بدلاً من الحالة القديمة المخزنة مؤقتاً. - إذا بدت البيانات الوصفية أو إدخالات المكتبة خاطئة - البيانات الوصفية الخاطئة للأغاني، أو عدم تطابق الفنانين أو الألبومات، أو الإدخالات التي تبدو مكررة تعني عادةً أن التثبيت النظيف هو الحل المناسب. - لا تظهر هذا مجدداً - فهمت ذلك - - - %1$d ألبومات - محدد - ميزة (إضافة للقائمة وتشغيل) تحترم ترتيب تحديدك تماماً. - الحد الأقصى: %1$d ألبومات لكل تحديد. - إضافة إلى قائمة الانتظار وتشغيل - PixelPlayer - مشغل موسيقى - أعلى %1$d - إغلاق - النتيجة - المستوى %1$d - القلوب - اكتمل المستوى! - انتهت اللعبة - النتيجة: %1$d - المحاولة مجدداً؟ - المستوى التالي - إعادة تشغيل اللعبة - اضغط لإعادة الإطلاق - تشغيل موسيقى عشوائية - كسارة الطوب - أعلى نتيجة %1$d - لعب - اسحب لتحريك المضرب - استعادة الوحدات - جاري الاستعادة - استعادة المحدد - تفاصيل النسخة الاحتياطية - تم الإنشاء - إصدار التطبيق - المخطط (Schema) - الجهاز - غير معروف - تم تحديد %1$d من أصل %2$d وحدة - النقل جارٍ الآن… - تحديد الكل - مسح التحديد - %1$d إدخالات · سوف تستبدل البيانات الحالية - - - بث سحابي - طي المشغل - بث بـ (Cast) - بلوتوث - تشغيل محلي - جاري الاتصال… - قائمة الانتظار - كلمات الأغاني - جلسة بث - جاري الاتصال - متصل - هذا الهاتف - صوت البلوتوث - تشغيل محلي - جاري التشغيل - موقوف مؤقتاً - استعد للاتصال - اسمح لـ PixelPlayer برؤية أجهزتك القريبة وشبكة الـ Wi-Fi الحالية حتى نتمكن من إبقاء البث وصوت البلوتوث ومكبرات الصوت متزامنة. - الأجهزة القريبة - مطلوب لقراءة والتحكم في معدات صوت البلوتوث المتصلة. - الموقع لشبكة الـ Wi‑Fi - يتطلب نظام أندرويد إذن الموقع لمشاركة شبكة الـ Wi-Fi الحالية (SSID) حتى نتمكن من العثور على أجهزة البث المتوافقة. - السماح بالوصول - نحن نستخدم هذه الأذونات فقط لربط الأجهزة — البث، والتحكم في مكبرات الصوت القريبة، وإبقاء الصوت متزامناً. - توصيل الجهاز - جاري الفحص بالقرب منك - عناصر التحكم - الأجهزة - الاتصالية - تشغيل الـ Wi-Fi أو البلوتوث - إدارة الشبكات النشطة وإعادة الفحص - تحديث الاتصالات - تحديث الأجهزة - الأجهزة القريبة - الأجهزة القريبة - مطلوب لاكتشاف والتحكم في أجهزة صوت البلوتوث المتصلة. - اضغط للاتصال - لا توجد أجهزة بعد - إلغاء الاتصال - مستوى صوت الجهاز - مستوى صوت الهاتف - جاري البحث عن أجهزة… - تأكد من أن التلفزيون أو مكبر الصوت قيد التشغيل ومتصل بنفس شبكة الـ Wi‑Fi. - متصل - متاح للاتصال - جاري الاتصال - متاح - مستوى البطارية - مستوى الصوت - Wi-Fi - متوقف - متصل - يعمل - بلوتوث - متصل - يعمل - متوقف - الاتصالات متوقفة - قم بتشغيل الـ Wi‑Fi أو البلوتوث لاكتشاف الأجهزة القريبة - تشغيل الـ Wi‑Fi - فتح البلوتوث - إلغاء الاتصال - جاري الاتصال... - - - أبرز الميزات - التحسينات - الإصلاحات - ما الجديد - ما الجديد - تمت إضافة - تغيير - تم إصلاح - - - دعم Android Auto متاح الآن للتشغيل داخل السيارة. - دعم Wear OS بات نشطاً، بما في ذلك عناصر تحكم أفضل للتشغيل من الساعة إلى الهاتف. - توسيع التكامل السحابي مع تحسينات لـ Telegram و NetEase و QQ Music و Google Drive. - ميزتا "المشغلة حديثاً" واستعادة قائمة الانتظار الدائمة تبقيان جلسة استماعك جاهزة. - تم تضمين ميزات النسخ الاحتياطي والاستعادة v3 وأدوات إدارة الحساب. - أصبحت كلمات الأغاني أكثر ذكاءً مع دعم البحث اليدوي الاحتياطي وتحسينات التخزين. - - - تحديث شامل للأداء عبر بدء التشغيل، المكتبة، قائمة الانتظار، وتفاعلات المشغل. - إعادة تصميم واجهات المشغل، البث، الكلمات، الفنان، والنوع لتوفير استخدام أكثر سلاسة. - أصبحت تدفقات التنقل والبحث أكثر موثوقية مع معالجة أكثر أماناً للمسارات. - تحسين توافق تشغيل الصوت لمزيد من الأجهزة والتنسيقات. - توسيع سير عمل التحديد المتعدد عبر الأغاني والألبومات وقوائم التشغيل. - - - أصبح سلوك قائمة الانتظار والخلط العشوائي أكثر استقراراً وقابلية للتنبؤ. - إصلاح العديد من الحالات النادرة في التشغيل الخلفي وبث الصوت (Casting). - إصلاح مشكلات مؤقت النوم، والتنقل في تبويب الملفات، وحالات توقف فنان الألبوم المفاجئ. - تحسين تحميل الويدجت واستقرار الخدمة لتقليل مشكلات الحرارة والذاكرة. - إصلاحات عامة للأخطاء وتحسينات جمالية لواجهة مستخدم التطبيق. - - - تحديث واجهة المستخدم التعبيرية Material 3 Expressive - معادل صوتي ذو 10 نطاقات وتأثيرات صوتية - تدفق مزامنة جديد للمكتبة الموسيقية - التكامل مع الذكاء الاصطناعي (نماذج Gemini) - استيراد وتصدير قوائم التشغيل بصيغة M3U - تكامل أغلفة الفنانين من منصة Deezer - أغلفة مخصصة لقوائم التشغيل - - - إعادة هيكلة معمارية الإعدادات - رسوم متحركة جديدة لقائمة الانتظار والمشغل - ملفات التعريف الأساسية (Baseline Profiles) وتحسين الأداء - نظام أفضل لكلمات الأغاني مع إزاحة التزامن - - - تحسينات استقرار بث الصوت (Casting) - استقرار لوحة المشغل السفلية - إصلاحات عامة للأخطاء وتنظيف الكود - - - إعادة تصميم كبرى لنظام التنقل - مستكشف ملفات جديد لاختيار مجلدات المصدر - وظائف اتصال وبث جديدة - استمرارية سلسة بين الأجهزة عن بعد - انتقال بدون فجوات (Gapless) بين الأغاني - عنصر التحكم في التلاشي المتبادل (Crossfade) - ميزة الانتقالات المخصصة الجديدة (لقوائم التشغيل فقط) - استمرار التشغيل بعد إغلاق التطبيق - تحسينات واجهة المستخدم - ميزة إحصائيات محسنة - إعادة تصميم التحكم في قائمة الانتظار مع المزيد من الميزات - تحسين دعم أنواع الملفات المختلفة للتشغيل وتعديل البيانات الوصفية - تحسين متحكم الأذونات - إصلاحات طفيفة للأخطاء - - - تقديم مركز إحصائيات استماع أكثر ثراءً مع رؤى عميقة لجلساتك. - إطلاق مشغل سريع عائم لفتح ومعاينة الملفات المحلية على الفور. - إضافة تبويب المجلدات مع مستكشف بنمط شجري وعرض جاهز لقوائم التشغيل. - - - تحسين واجهة Material 3 بالكامل لتوفير تجربة أنظف وأكثر تماسكاً. - تحرير البيانات الوصفية يدعم الآن تغيير غلاف الألبوم. - تنعيم الرسوم المتحركة والانتقالات عبر التطبيق لتنقل أكثر انسيابية. - تحسين تخطيط شاشة الفنان مع تفاصيل أكثر ثراءً ولمسات جمالية. - ترقية توليد DailyMix و YourMix باختيارات أكثر ذكاءً وتنوعاً. - تعزيز توليد قوائم التشغيل بواسطة الذكاء الاصطناعي. - تحسين صلة نتائج البحث وعرضها لاكتشاف أسرع. - توسيع الدعم لنطاق أوسع من تنسيقات الملفات الصوتية. - - - حل مشكلات البيانات الوصفية الغريبة لتبقى تفاصيل الأغاني دقيقة في كل مكان. - استعادة اختصارات الإشعارات لتعود بشكل موثوق إلى شاشة التشغيل. - - - دعم Chromecast لبث الصوت من جهازك. - سجل التغييرات داخل التطبيق لإبقائك على اطلاع بآخر الميزات. - دعم ملفات LRC، سواء كانت مدمجة أو خارجية. - دعم كلمات الأغاني دون اتصال بالإنترنت. - كلمات أغاني متزامنة (متطابقة مع الأغنية). - شاشة جديدة لعرض كامل قائمة الانتظار. - إعادة ترتيب وإزالة الأغاني من قائمة الانتظار. - إيماءات المشغل المصغر (السحب للأسفل للإغلاق). - إضافة المزيد من رسوم Material المتحركة. - إعدادات جديدة لتخصيص المظهر والإحساس العام. - إعدادات جديدة لمسح ذاكرة التخزين المؤقت. - - - إعادة تصميم كاملة لواجهة المستخدم. - إعادة تصميم كاملة للمشغل. - تحسينات الأداء في المكتبة الموسيقية. - تحسين سرعة تشغيل التطبيق عند البدء. - الذكاء الاصطناعي يقدم الآن نتائج أفضل. - - - إصلاح أخطاء مختلفة في محرر العلامات (Tags). - إصلاح مشكلة عدم اختفاء إشعار التشغيل. - إصلاح عدة أخطاء كانت تتسبب في توقف التطبيق فجأة. - - - Wear OS: نقل الموسيقى، التشغيل المحلي، مزامنة قائمة الانتظار، والتحكم عن بعد من الساعة. - الذكاء الاصطناعي: تكامل Groq AI و OpenRouter (تجريبي) مع تحسين استهلاك الرموز (Tokens). - السحاب: إضافة دعم Jellyfin. - كلمات الأغاني: ترجمة متزامنة مع مفتاح تبديل مخصص، دعم تنسيق Kugou LRC، تخصيص محاذاة النص، وتحسين التحميل عن بعد. - واجهة المستخدم/تجربة المستخدم: وضع شريط التنقل المدمج، سمات ديناميكية من لوحة ألوان غلاف الألبوم، نص متحرك (Marquee) للعناوين الطويلة، وخيارات فرز جديدة. - تليجرام: دعم أصلي للمواضيع (Topics) وأنماط عرض محسنة. - - - المحرك الصوتي: إصلاح شامل مع دعم المزيد من التنسيقات (MIDI, ALAC, M4A) وتحسين برنامج فك الترميز. - الكفاءة: تقليل جذري في استهلاك الطاقة، إصلاحات للحرارة الزائدة، وتحسين المهام الخلفية (SyncWorker). - قاعدة البيانات: تحسينات هائلة على الاستعلامات وإعادة تصميم ذاكرة التخزين المؤقت للأغلفة لمنع فقدان البيانات. - بدء التشغيل: تحسين وقت التحميل عبر تهيئة Baseline Profile. - - - التتشغيل: إصلاح التقطع في Opus/MP3، أخطاء ReplayGain أثناء التلاشي المتبادل، ومشكلات بدء التشغيل على مفككات ترميز Samsung. - الاستقرار: القضاء على حالات التوقف المفاجئ عند البدء، وأثناء التنقل بين الفنانين، وعلى أجهزة أندرويد 12+. - واجهة المستخدم: إصلاح وميض الأغلفة، وتداخل النصوص في النصوص غير اللاتينية، وسلوك شريط التنقل/المشغل المصغر. - الأمان: تعزيز التعامل مع بيانات الاعتماد، أذونات التخزين، والاتصال بخادم الوسائط. - - - العربية - Spanish - French - Russian - Simplified Chinese - Indonesian - Italian - - diff --git a/app/src/main/res/values-ar/strings_screens.xml b/app/src/main/res/values-ar/strings_screens.xml index 3570cde6d..abb331cac 100644 --- a/app/src/main/res/values-ar/strings_screens.xml +++ b/app/src/main/res/values-ar/strings_screens.xml @@ -1,100 +1,17 @@ - - خطأ: معرف النوع (Genre ID) مفقود - شكراً لك على استخدام PixelPlayer! + + خطأ: معرّف النوع مفقود - - محددات الكلمات الحالية - هذه الكلمات المفتاحية تفصل أسماء الفنانين عندما تكون محاطة بمسافات. يتم مطابقتها دون تفرقة بين الأحرف الكبيرة والصغيرة. اضغط للحذف. - لم يتم تهيئة أي محددات كلمات - إضافة محدد كلمات جديد - مثال: .feat أو .ft - كيف تعمل محددات الكلمات - يتم مطابقة محددات الكلمات دون تفرقة بين الأحرف الكبيرة والصغيرة مع وجود مسافات حولها.\n\nالمحددات المكونة من حرف واحد (مثل \"x\") تتطلب مسافات من كلا الجانبين لتجنب المطابقات الخاطئة.\n\nأمثلة:\n \"Drake feat. Rihanna\" -> Drake, Rihanna\n \"Marshmello x Bastille\" -> Marshmello, Bastille\n \"A vs. B\" -> A, B - محددات الكلمات - إعادة تعيين محددات الكلمات؟ - سيؤدي هذا إلى مسح جميع محددات الكلمات المخصصة واستعادة الكلمات المفتاحية الافتراضية. لا يمكن التراجع عن هذا الإجراء. - تمت إضافة محدد الكلمات - موجود بالفعل أو غير صالح - تمت إعادة تعيين محددات الكلمات إلى الافتراضية - إعادة تعيين - - - المحددات الحالية - انقر على محدد لإزالته. يلزم وجود محدد واحد على الأقل. - إضافة محدد جديد - مثال: / أو ; - المحددات الافتراضية - إعادة تعيين المحددات؟ - سيؤدي هذا إلى مسح جميع المحددات المخصصة واستعادة المحددات الافتراضية. لا يمكن التراجع عن هذا الإجراء. - تمت إعادة تعيين المحددات إلى الافتراضية - يلزم وجود محدد واحد على الأقل - تمت إضافة المحدد - المحدد موجود بالفعل أو غير صالح - المحددات - مسافة - إضافة محدد - - - خدمة Google Drive قادمة قريباً. - تعذر فتح هذه الشاشة في الوقت الحالي. - - + + هيا بنا! + الخطوة %1$d من أصل %2$d + يرجى منح الإذن المطلوب أولاً. + يرجى منح جميع الأذونات المطلوبة. مرحباً بك في β تجريبي (Beta) دعنا نقوم بإعداد كل شيء من أجلك. - جاري التحقق من حزمة النسخة الاحتياطية… - مظهر التطبيق - اختر المظهر الذي تريده قبل البدء في استكشاف مكتبتك. - يمكنك تغيير هذا لاحقاً من الإعدادات > المظهر > مظهر التطبيق. - موصى به - تخطيط المكتبة - اختر الطريقة المفضلّة لديك للتنقل في مكتبتك. - الوضع المدمج - يمكنك تغيير هذا لاحقاً من الإعدادات > المظهر > التنقل في المكتبة. - المكتبة - الأغاني - الألبومات - الفنانون - كل شيء جاهز! - أنت مستعد الآن للاستمتاع بموسيقاك. - استعادة النسخة الاحتياطية - راجع ما تريد استيراده قبل إنهاء الإعداد. - تم تحديد %1$d من أصل %2$d من الوحدات - تم الإنشاء في %1$s - نسخة احتياطية من الإصدار %1$s - إصدار غير معروف - هيا بنا! - الخطوة %1$d من أصل %2$d - التنقل داخل التطبيق - اختر نمط شريط التنقل السفلي. - النمط الافتراضي - يمكنك تغيير هذا لاحقاً من الإعدادات > المظهر > نمط شريط التنقل. - تخطي في الوقت الحالي - تخطي / ليس الآن - جاري الاستعادة - استعادة المحدد - تخصيص نصف قطر الزوايا - يرجى منح الإذن المطلوب أولاً. - يرجى منح جميع الأذونات المطلوبة. - يرجى منح أذونات التخزين أولاً - تعذر فتح إعدادات البطارية - - - توسيع القائمة - التالي - إنهاء - إغلاق - إزالة - إضافة محدد كلمات - إعادة تعيين الافتراضيات - - - المجلدات المستبعدة - يتم فحص جميع المجلدات افتراضياً. اختر أي مواقع تريد تجاهلها عند بناء مكتبتك. - اختر المجلدات لتجاهلها إذن الوصول إلى الوسائط يحتاج PixelPlayer إلى الوصول إلى ملفاتك الصوتية لبناء مكتبتك الموسيقية. تم منح الإذن @@ -102,151 +19,226 @@ الإشعارات قم بتمكين الإشعارات للتحكم في موسيقاك من شاشة القفل ولوحة الإشعارات. تمكين الإشعارات - التنبيهات والتذكيرات - اختياري، ولكن موصى به إذا كنت تستخدم مؤقت النوم وتريد أن يقوم PixelPlayer بإيقاف التشغيل في الوقت المحدد تماماً. - منح الإذن هل لديك نسخة احتياطية؟ إذا كان لديك نسخة احتياطية من PixelPlayer بالفعل، فاستعدها الآن لتخطي معظم خطوات الإعداد المتبقية على هذا الجهاز. + استيراد نسخة احتياطية جاري فحص النسخة الاحتياطية + جاري التحقق من حزمة النسخة الاحتياطية… جاري استعادة النسخة الاحتياطية - استيراد نسخة احتياطية + تخطي / ليس الآن + استعادة النسخة الاحتياطية + راجع ما تريد استيراده قبل إنهاء الإعداد. + تم تحديد %1$d من أصل %2$d من الوحدات + تم الإنشاء في %1$s + نسخة احتياطية من الإصدار %1$s + إصدار غير معروف + استعادة المحدد + جاري الاستعادة + المجلدات المستبعدة + يتم فحص جميع المجلدات افتراضياً. اختر أي مواقع تريد تجاهلها عند بناء مكتبتك. + اختر المجلدات لتجاهلها + يرجى منح أذونات التخزين أولاً + مظهر التطبيق + اختر المظهر الذي تريده قبل البدء في استكشاف مكتبتك. داكن المظهر الداكن الافتراضي لـ Material 3 في تطبيق PixelPlayer. فاتح مظهر Material 3 أكثر سطوعاً في جميع أنحاء التطبيق. تتبع النظام مطابقة إعداد المظهر الحالي لهاتفك. + موصى به + يمكنك تغيير هذا لاحقاً من الإعدادات > المظهر > مظهر التطبيق. + تخطيط المكتبة + اختر الطريقة المفضلّة لديك للتنقل في مكتبتك. + الأغاني + الوضع المدمج يتم استخدام شريط التنقل الكبسولة المدمج يتم استخدام صف علامات التبويب القياسي - الأغاني + الأغاني + الألبومات + الفنانون + يمكنك تغيير هذا لاحقاً من الإعدادات > المظهر > التنقل في المكتبة. + التنقل داخل التطبيق + اختر نمط شريط التنقل السفلي. + النمط الافتراضي + شريط كبسولة عائم بزوايا مستديرة + شريط قياسي بالعرض الكامل + تخصيص نصف قطر الزاوية + يمكنك تغيير هذا لاحقاً من الإعدادات > المظهر > نمط شريط التنقل. + التنبيهات والتذكيرات + اختياري، ولكن موصى به إذا كنت تستخدم مؤقت النوم وتريد أن يقوم PixelPlayer بإيقاف التشغيل في الوقت المحدد تماماً. + منح الإذن تحسين استهلاك البطارية تقوم بعض أجهزة Android بإغلاق تطبيقات الخلفية بشكل حاد. قم بتعطيل تحسين البطارية لتطبيق PixelPlayer لمنع انقطاع التشغيل غير المتوقع. تعطيل التحسين - شريط كبسولة عائم بزوايا مستديرة - شريط قياسي بالعرض الكامل - - - حذف الأغنية؟ - \"%1$s\" بواسطة %2$s\n\nسيتم حذف هذه الأغنية نهائياً من جهازك ولا يمكن استعادتها. - - - المزيج\nالخاص بك - لا توجد بيانات لعرضها بعد - سيظهر المزيج الخاص بك هنا عندما يجد PixelPlayer أغانٍ أو يقوم بمزامنة أحد المصادر. - تحديث - تشغيل عشوائي - غلاف ألبوم لـ %1$s - خيارات - ملء سريع للنوع - فنان عام - تشغيل الألبوم - تشغيل الألبوم عشوائياً - غلاف %1$s - %1$s · %2$s - تشغيل/إيقاف مؤقت - غلاف الأغنية - - - عذراً! حدث خطأ ما - تعطل التطبيق خلال جلستك الأخيرة. ساعدنا في إصلاح هذا من خلال مشاركة تقرير التعطل. - التاريخ: %1$s - الخطأ: - تتبع الكومة (معاينة): - سجل التعطل - تم نسخ سجل التعطل إلى الحافظة - تقرير تعطل PixelPlayer - مشاركة تقرير التعطل - نسخ - مشاركة + كل شيء جاهز! + أنت مستعد الآن للاستمتاع بموسيقاك. - + بحث… - بحث - مسح البحث - عمليات البحث الأخيرة - مسح الكل - السجل - حذف عنصر من سجل البحث - لا توجد نتائج + بحث + مسح البحث + عمليات البحث الأخيرة + مسح الكل + السجل + حذف عنصر من سجل البحث + لا توجد نتائج لا توجد نتائج لـ \"%1$s\" لم يتم العثور على شيء جرّب مصطلح بحث آخر أو تحقق من الفلاتر الخاصة بك. لم يتم العثور على نتائج. + تصفح حسب النوع الموسيقي + لا توجد أنواع موسيقية متاحة. + + + تشغيل %1$s + طي %1$s + توسيع %1$s + تعديل صورة الفنان + تغيير الصورة + إعادة تعيين للوضع الافتراضي + تشغيل أغاني الفنان عشوائياً + + + القرص %d + غلاف %1$s + %1$s · %2$s - - تصفح حسب النوع - لا توجد أنواع متاحة. + + لم يتم العثور على قائمة التشغيل. + قائمة التشغيل هذه فارغة. + اضغط على \'إضافة أغاني\' للبدء. + هذا المجلد لا يحتوي على أغانٍ. + فرز الأغاني + مزيد من الخيارات + خيارات قائمة التشغيل + تعديل قائمة التشغيل + حذف قائمة التشغيل + حذف قائمة التشغيل؟ + هل أنت متأكد من أنك تريد حذف قائمة التشغيل هذه؟ + تعيين الانتقال الافتراضي + تصدير قائمة التشغيل + %1$s • %2$s + تشغيل + إضافة + إضافة أغاني + إزالة + إزالة أغاني + إعادة الترتيب + إعادة ترتيب الأغاني - - لم يتم العثور على مساهمين حالياً. يرجى المحاولة مرة أخرى لاحقاً. - PixelPlayer - مشغل موسيقى مفتوح المصدر تم بناؤه مع مجتمعه. - الإصدار v%1$s - %1$d مساهمة - حول التطبيق - المشرف الرئيسي - الشخص الذي يقف وراء PixelPlayer. - أضواء على المجتمع - تقدير وتكريم للمتعاونين ذوي التأثير الكبير. - المساهمون في المشروع مفتوح المصدر - قائمة المساهمين المباشرة من GitHub. - مفتوح المصدر - المجتمع أولاً - تصميم Material 3 معبر - فتح ملف GitHub الشخصي - فتح Telegram - الصورة الشخصية لـ %1$s - أيقونة %1$s + + الانتقالات العامة + قواعد قائمة التشغيل + يطبق هذا التكوين على جميع مصادر التشغيل ما لم يتم تجاوزه. + تخصيص السلوك الافتراضي لقائمة التشغيل هذه تحديداً. + حالة التنشيط + الافتراضي العام + افتراضي قائمة التشغيل + تابع للإعداد العام + تجاوز مخصص + تجاوز مخصص + قم بالتمكين لتعيين قواعد خاصة بقائمة التشغيل هذه. + يتم استخدام الإعدادات الافتراضية العامة + تم حفظ التغييرات بنجاح + نمط الانتقال + كيفية تداخل المسارات الصوتية معاً + بدون انتقال + التدخل المتلاشي (Crossfade) + مدة الانتقال + إجمالي التداخل %1$d ثوانٍ + إعادة تعيين + الأغنية الحالية + الأغنية التالية + ستتداخل المسارات لمدة %1$d ثوانٍ + منحنيات مستوى الصوت + ضبط ميل وتلاشي الصوت بدقة + تلاشي للخارج (Fade Out) + تلاشي للداخل (Fade In) - - Subsonic - تم مزامنة %1$d قائمة تشغيل - تم مزامنة %1$d مجلد - قوائم التشغيل - مجلدات الموسيقى - مزامنة - لم يتم مزامنة أي قوائم تشغيل بعد - اضغط على مزامنة لجلب قوائم التشغيل الخاصة بك - اضغط على مزامنة لجلب قوائم تشغيل Jellyfin الخاصة بك - لم يتم إضافة مجلدات بعد - انقر على + لإضافة مجلد من Drive - إجراءات سريعة - إدارة خوادم Navidrome وAirsonic والخوادم الأخرى المتوافقة مع Subsonic. - إدارة اتصال خادم Jellyfin الخاص بك. - جاري المزامنة - مزامنة المكتبة - قطع الاتصال - جاري مزامنة المكتبة… - جاري جلب قوائم التشغيل… - جاري مزامنة قائمة التشغيل: %1$s - جاري تحديث المكتبة المحلية… - اكتملت المزامنة - جاري جلب قائمة الألبومات… - جاري جلب الأغاني من %1$s… - جاري حفظ %1$d أغانٍ في قاعدة البيانات… - لم يتم العثور على أغانٍ في المكتبة - اكتملت مزامنة المكتبة - %1$d أغانٍ - مزامنة - مزامنة الكل - إضافة مجلد - تسجيل الخروج - NetEase Music - QQ Music - مزامنة جميع قوائم التشغيل - خطأ: %1$s - جاري المزامنة… - اختر نوع قائمة التشغيل - اختر قوائم التشغيل المراد مزامنتها: - جميع قوائم التشغيل - المنشأة والمجمعة - قوائم التشغيل المنشأة - قوائم التشغيل المجمعة - الصورة الشخصية للمستخدم - تم إنشاء قائمة التشغيل بنجاح - يرجى تعيين مفتاح API لمزود الذكاء الاصطناعي أولاً - يرجى تعيين مفتاح API لـ Gemini أولاً - تمت الإضافة إلى قائمة الانتظار - سيتم التشغيل تالياً - تعذر مشاركة الأغنية: %1$s + + قائمة تشغيل ذكية جديدة + قائمة تشغيل جديدة + إضافة أغانٍ + الرجوع أو الإلغاء + التالي + إنشاء + تعديل قائمة التشغيل + تجميعة صور منشأة تلقائياً + إضافة صورة + اختر صورة + تغيير + إزالة + اسم قائمة التشغيل + مزيجي الرائع + تعديل الغلاف + ضبط غلافك الفني + استخدم إيماءات التكبير والسحب للحصول على الإطار المثالي + يدوي + ذكي + الإنشاء باستخدام الذكاء الاصطناعي + قاعدة ذكية + الافتراضي + صورة + أيقونة + لون الخلفية + رمز الأيقونة + نمط الشكل + معلمات الشكل + نصف قطر الزوايا + النعومة + الأضلاع + الانحناء + الدوران + المقاس + الأكثر تشغيلاً + المسارات الأكثر تشغيلاً لديك. + المشغلة حديثاً + الأغاني التي استمعت إليها مؤخراً. + المفضلات المنسية + المسارات المفضلة التي لم تقم بتشغيلها منذ فترة. + جواهر جديدة + المسارات المضافة حديثاً مع نسب تشغيل منخفضة. + + + ملء سريع للنوع + فرز وتشغيل + تشغيل عشوائي + فرز حسب + الفنان + الألبوم + العنوان + فنان عام + تشغيل %1$s عشوائياً + + + اختر الأغاني + اختر النوع + البحث عن أغاني + نوع جديد + إضافة نوع مخصص + إضافة نوع مخصص + اسم النوع + اختر أيقونة + النوع: %1$s + اختر نوعاً + الملء السريع + + + مساحة الدي جي + جاري التحميل… + منصة %1$d + تحميل الأغنية + لم يتم تحميل أي أغنية + + عزل المسارات (Stem separation) غير متاح بعد. + مستوى الصوت + السرعة + مخفف التداخل (Crossfader) + المنصة 1 + المنصة 2 + اختر أغنية + تشغيل/إيقاف مؤقت + غلاف الأغنية + x%1$.2f diff --git a/app/src/main/res/values-ar/strings_settings.xml b/app/src/main/res/values-ar/strings_settings.xml index 78d187b68..fdf62db5a 100644 --- a/app/src/main/res/values-ar/strings_settings.xml +++ b/app/src/main/res/values-ar/strings_settings.xml @@ -1,11 +1,8 @@ - الإعدادات - الحسابات - إدارة خدمات Telegram، وGoogle Drive، وNetEase وغيرها - - إدارة الموسيقى - إدارة المجلدات، تحديث المكتبة، وخيارات التحليل البرمجي + + إدارة الموسيقى + إدارة المجلدات، تحديث المكتبة، وخيارات التحليل البرمجي المظهر السمات (الثيمات)، التخطيط، والأنماط المرئية التشغيل @@ -22,279 +19,622 @@ ضبط ترددات الصوت والمستويات المسبقة قدرات الجهاز مواصفات الصوت، برامج الترميز (Codecs)، ومعلومات فك التشفير + الحسابات + إدارة خدمات Telegram، وGoogle Drive، وNetEase وغيرها حول التطبيق معلومات التطبيق، الإصدار، والحقوق - مفعل - معطل - مُمكن - مُعطل - فتح - تحديد الكل - مسح التحديد - إغلاق التنويه + + مفعل + معطل + مُمكن + مُعطل + فتح + تحديد الكل + مسح التحديد + إغلاق التنويه + + + بنية المكتبة + المجلدات المستبعدة + سيتم تخطي المجلدات الموجودة هنا أثناء فحص مكتبتك. + الفنانون + خيارات تنظيم وتحليل البيانات للفنانين المتعددين. + التصفية + الحد الأدنى لمدة الأغنية + الحد الأدنى للمسارات في الألبوم + حد ذاكرة التخزين المؤقت لأغلفة الألبومات + المزامنة والفحص + تحديث المكتبة + فحص المكتبة بالكامل بحثاً عن الملفات الجديدة والمعدلة. + إجراء فحص كامل جديد + جاري إجراء فحص كامل + بدأ الفحص الكامل من جديد… + انتهت مزامنة المكتبة + إعادة بناء قاعدة البيانات + إعادة بناء قاعدة البيانات؟ + سيؤدي هذا إلى إعادة بناء مكتبة الموسيقى الخاصة بك بالكامل من الصفر. ستفقد جميع كلمات الأغاني المستوردة، والمفضلة، والبيانات الوصفية المخصصة. لا يمكن التراجع عن هذا الإجراء. + إعادة البناء + جاري إعادة بناء قاعدة البيانات + جاري إعادة بناء قاعدة البيانات… + فحص تلقائي لملفات .lrc + فحص وتعيين ملفات الكلمات المزامنة (.lrc) المتواجدة في نفس المجلد تلقائياً أثناء المزامنة. + إدارة كلمات الأغاني + أولوية مصدر كلمات الأغاني + اختر المصدر الذي يفضله التطبيق أولاً عند جلب الكلمات. + المضمنة أولاً (Embedded) + عبر الإنترنت أولاً + الملف المحلي (.lrc) أولاً + إعادة تعيين الكلمات المستوردة + حذف جميع كلمات الأغاني المستوردة من قاعدة البيانات. + إعادة تعيين كلمات الأغاني المستوردة؟ + لا يمكن التراجع عن هذا الإجراء لاحقاً. + + + تحديث + كل شيء مسموح به افتراضياً. اضغط على مجلد لاستبعاده من الفحص. + لا توجد مجلدات فرعية هنا + الانتقال لأعلى + الذهاب للمجلد الرئيسي + + + مطلوب إعادة الفحص + تغيرت إعدادات الفنان. أعد فحص مكتبتك لتطبيق التغييرات. + إعادة الفحص + جاري الفحص… + تحليل الفنانين المتعددين + محددات الحروف + الحالي: %1$s + محددات الكلمات + لا يوجد + الحالي: %1$s + تهيئة + استخراج الفنانين من العنوان + كشف feat.، ft.، with في عناوين الأغاني + تنظيم المكتبة + تجميع حسب فنان الألبوم + إظهار ألبومات التعاون المشترك تحت الفنان الرئيسي + حول تحليل الفنانين المتعددين + + يقوم PixelPlayer بتقسيم علامات الفنانين باستخدام محددات الحروف (/, ;, &) ومحددات الكلمات (feat., ft., vs., x). يتم مطابقة محددات الكلمات بغض النظر عن حالة الأحرف (كبيرة/صغيرة). + ميزة \"استخراج الفنانين من العنوان\" تكشف أنماطاً مثل (feat. Artist) في عناوين الأغاني. + يمكن استخدام الشرطة المائلة العكسية (\\) لتجاوز محددات الحروف. + + أمثلة + \u2192 + \u266A + \"Artist1/Artist2\" + Artist1، Artist2 + \"Drake feat. Rihanna\" + Drake، Rihanna + \"Marshmello x Bastille\" + Marshmello، Bastille + \"Song (ft. B)\" by A + A، B + \"AC\\DC\" + AC/DC (مستثنى) + + + المحددات + المحددات الحالية + انقر فوق محدد لإزالته. يلزم وجود محدد واحد على الأقل. + إضافة محدد جديد + مثال: / أو ; + إضافة محدد + المحددات الافتراضية + إعادة تعيين المحددات؟ + سيؤدي هذا إلى مسح جميع المحددات المخصصة واستعادة المحددات الافتراضية. لا يمكن التراجع عن هذا الإجراء. + تم إعادة تعيين المحددات إلى القيم الافتراضية + يلزم وجود محدد واحد على الأقل + تم إضافة المحدد + المحدد موجود بالفعل أو غير صالح + مسافة + + + محددات الكلمات + محددات الكلمات الحالية + تقوم هذه الكلمات المفتاحية بتقسيم أسماء الفنانين عندما تكون محاطة بمسافات. مطابقتها غير حساسة لحالة الأحرف. اضغط للإزالة. + لم يتم تكوين أي محددات للكلمات + إضافة محدد كلمات جديد + مثال: feat. أو ft. + إضافة محدد كلمات + كيف تعمل محددات الكلمات + يتم مطابقة محددات الكلمات بغض النظر عن حالة الأحرف مع وجود مسافات حولها.\n\nالمحددات المكونة من حرف واحد (مثل \"x\") تتطلب وجود مسافات على كلا الجانبين لتجنب المطابقات الخاطئة.\n\nأمثلة:\n \"Drake feat. Rihanna\" -> Drake، Rihanna\n \"Marshmello x Bastille\" -> Marshmello، Bastille\n \"A vs. B\" -> A، B + إعادة تعيين محددات الكلمات؟ + سيؤدي هذا إلى مسح جميع محددات الكلمات المخصصة واستعادة الكلمات المفتاحية الافتراضية. لا يمكن التراجع عن هذا الإجراء. + تم إضافة محدد الكلمات + موجود بالفعل أو غير صالح + تم إعادة تعيين محددات الكلمات إلى القيم الافتراضية + + + تحضير المزامنة + جاري قراءة MediaStore + جاري معالجة المسارات + جاري الحفظ في قاعدة البيانات + جاري فحص ملفات كلمات الأغاني (LRC) + جاري تنظيف ذاكرة التخزين المؤقت لأغلفة الألبومات + جاري المزامنة مع المصادر السحابية + جاري إكمال المزامنة + %1$s • %2$d%% (%3$d/%4$d) + %1$s… + + + المظهر العام + لغة التطبيق + اختر اللغة المستخدمة في واجهة التطبيق بكاملها. + افتراضية النظام + English (الإنجليزية) + Español (الإسبانية) + Deutsch (الألمانية) + Français (الفرنسية) + Русский (الروسية) + 简体中文 (الصينية المبسطة) + Bahasa Indonesia (الإندونيسية) + Italiano (الإيطالية) + Korean (الكورية) + Norwegian (النرويجية بوكمول) + Türkçe (التركية) + مظهر التطبيق + التنقل بين المظهر الفاتح، الداكن، أو تتبع النظام. + المظهر الفاتح + المظهر الداكن + حسب النظام + حواف ناعمة ومرنة + استخدام زوايا وحواف دائرية معقدة لتحسين الجمالية؛ قد يؤثر على الأداء في الأجهزة الضعيفة + تعطيل تأثيرات الضبابية (Blur) + إيقاف تشغيل تأثيرات التغبيش والضبابية لتوفير البطارية وموارد الجهاز. + إظهار شريط التمرير + عرض شريط تمرير جانبي في قوائم الموسيقى للتنقل السريع + شاشة المشغل الحالي + مظهر المشغل + اختر مظهر شريط المشغل العائم. + غلاف الألبوم + ديناميكي حسب النظام + إظهار معلومات ملف الموسيقى + عرض الترميز، ومعدل البت، ومعدل العينة في قسم تقدم المشغل. + نمط لوحة غلاف الألبوم + الحالي: %1$s. افتح المعاينة الحية لتحديد النمط. + نمط العرض الدائري (Carousel) + اختر نمط وطريقة عرض دائرة الألبومات. + بدون إلقاء نظرة خاطفة + إلقاء نظرة خاطفة واحدة + تجميعة الصفحة الرئيسية + نمط التجميعة + اختر ترتيب الأشكال لتجميعة صور "المزيج الخاص بك". + تدوير الأنماط تلقائياً + تغيير أنماط التجميعة بشكل دوري في كل مرة تزور فيها الصفحة الرئيسية. + شريط التنقل + نمط شريط التنقل + اختر مظهر ونمط شريط التنقل السفلي. + الافتراضي + العرض الكامل + الوضع المدمج + إظهار الأيقونات فقط وتقليل ارتفاع شريط التنقل. + نصف قطر زوايا شريط التنقل + ضبط درجة تدوير زوايا شريط التنقل. + شاشة كلمات الأغاني + كلمات غامرة وملء الشاشة + إخفاء عناصر التحكم تلقائياً وتكبير حجم الخط. + مهلة الإخفاء التلقائي + الوقت المستغرق قبل إخفاء عناصر التحكم. + 3 ثوانٍ + 4 ثوانٍ + 5 ثوانٍ + 6 ثوانٍ + التنقل داخل التطبيق + علامة التبويب الافتراضية + اختر علامة التبويب الافتراضية عند تشغيل التطبيق. + الرئيسية + البحث + المكتبة + التنقل في المكتبة + اختر طريقة التنقل بين علامات تبويب المكتبة الموسيقية. + صف علامات التبويب (افتراضي) + شريط كبسولة مدمج وشبكة + + + الألوان + نمط لوحة الألوان + اختر ألوان الألبوم لواجهة مستخدم المشغل. + نقطة نغمية (Tonal Spot) + متوازنة وهادئة. + نابضة بالحياة (Vibrant) + لمسات ملونة ذات تشبع عالٍ. + تعبيرية (Expressive) + تحولات لونية جريئة وتباين قوي. + سلطة فواكه (Fruit Salad) + لمسات ملونة مبهجة مائلة. + دقة الألوان + 0 يحافظ على الضبط الحالي. القيم الأعلى تظل أقرب إلى اللون المهيمن لغلاف الألبوم. + الحالي + أكثر دقة + 0 • الحالي + %1$d • خفيف + %1$d • متوازن + %1$d • دقيق - بنية المكتبة - المجلدات المستبعدة - سيتم تخطي المجلدات الموجودة هنا أثناء فحص مكتبتك. - الفنانون - خيارات تنظيم وتحليل البيانات للفنانين المتعددين. - التصفية - الحد الأدنى لمدة الأغنية - الحد الأدنى للمسارات في الألبوم - حد ذاكرة التخزين المؤقت لأغلفة الألبومات - الحد الأقصى لحجم الذاكرة التخزينية قبل حذف الصور الأقدم تلقائياً - المزامنة والفحص - إجراء فحص كامل جديد - انتهت مزامنة المكتبة - بدأ الفحص الكامل من جديد… - فحص تلقائي لملفات .lrc - فحص وتعيين ملفات الكلمات المزامنة (.lrc) المتواجدة في نفس المجلد تلقائياً أثناء المزامنة. - إدارة كلمات الأغاني - أولوية مصدر كلمات الأغاني - اختر المصدر الذي يفضله التطبيق أولاً عند جلب الكلمات. - المضمنة أولاً (Embedded) - عبر الإنترنت أولاً - الملف المحلي (.lrc) أولاً - إعادة تعيين الكلمات المستوردة - حذف جميع كلمات الأغاني المستوردة من قاعدة البيانات. + + ضبط نصف قطر الزاوية + طابق زوايا شكل شريط التنقل مع الزوايا الفيزيائية لجهازك للحصول على مظهر سلس ومترابط. + نصف قطر الزاوية + %1$d dp - المظهر العام - لغة التطبيق - اختر اللغة المستخدمة في واجهة التطبيق بكاملها. - افتراضية النظام - English (الإنجليزية) - Español (الإسبانية) - Deutsch (الألمانية) - Français (الفرنسية) - Русский (الروسية) - 简体中文 (الصينية المبسطة) - Bahasa Indonesia (الإندونيسية) - Italiano (الإيطالية) - Türkçe (التركية) - مظهر التطبيق - التنقل بين المظهر الفاتح، الداكن، أو تتبع النظام. - المظهر الفاتح - المظهر الداكن - حسب النظام - حواف ناعمة ومرنة - استخدام زوايا وحواف دائرية معقدة لتحسين الجمالية؛ قد يؤثر على الأداء في الأجهزة الضعيفة - تعطيل تأثيرات الضبابية (Blur) - إيقاف تشغيل تأثيرات التغبيش والضبابية لتوفير البطارية وموارد الجهاز. - إظهار شريط التمرير - عرض شريط تمرير جانبي في قوائم الموسيقى للتنقل السريع - شاشة المشغل الحالي - مظهر المشغل - اختر مظهر شريط المشغل العائم. - غلاف الألبوم - ديناميكي حسب النظام - إظهار معلومات ملف الموسيقى - عرض الترميز، ومعدل البت، ومعدل العينة في قسم تقدم المشغل. - نمط لوحة غلاف الألبوم - الحالي: %1$s. افتح المعاينة الحية لتحديد النمط. - نمط العرض الدائري (Carousel) - اختر نمط وطريقة عرض دائرة الألبومات. - بدون إلقاء نظرة خاطفة - إلقاء نظرة خاطفة واحدة - تجميعة الصفحة الرئيسية - نمط التجميعة - اختر ترتيب الأشكال لتجميعة صور "المزيج الخاص بك". - تدوير الأنماط تلقائياً - تغيير أنماط التجميعة بشكل دوري في كل مرة تزور فيها الصفحة الرئيسية. - شريط التنقل - نمط شريط التنقل - اختر مظهر ونمط شريط التنقل السفلي. - الافتراضي - العرض الكامل - الوضع المدمج - إظهار الأيقونات فقط وتقليل ارتفاع شريط التنقل. - نصف قطر زوايا شريط التنقل - ضبط درجة تدوير زوايا شريط التنقل. - شاشة كلمات الأغاني - كلمات غامرة وملء الشاشة - إخفاء عناصر التحكم تلقائياً وتكبير حجم الخط. - مهلة الإخفاء التلقائي - الوقت المستغرق قبل إخفاء عناصر التحكم. - 3 ثوانٍ - 4 ثوانٍ - 5 ثوانٍ - 6 ثوانٍ - التنقل داخل التطبيق - علامة التبويب الافتراضية - اختر علامة التبويب الافتراضية عند تشغيل التطبيق. - الرئيسية - التنقل في المكتبة - اختر طريقة التنقل بين علامات تبويب المكتبة الموسيقية. - صف علامات التبويب (افتراضي) - شريط كبسولة مدمج وشبكة + + التشغيل في الخلفية + استمرار التشغيل بعد الإغلاق + إذا تم إيقافه، فإن إزالة التطبيق من التطبيقات الحديثة سيؤدي لإيقاف التشغيل. + تحسين استهلاك البطارية + تعطيل تحسين البطارية لمنع انقطاع التشغيل في الخلفية. + تم تعطيل تحسين البطارية بالفعل + تعذر فتح إعدادات البطارية + موازنة مستوى الصوت (ReplayGain) + تفعيل ميزة ReplayGain + تثبيت وموازنة مستويات الصوت تلقائياً باستخدام بيانات ReplayGain الوصفية المرفقة بالملفات. + وضع زيادة الكسب (Gain Mode) + مسار: موازنة كل أغنية على حدة. ألبوم: موازنة الصوت لكل ألبوم بالكامل. + مسار + ألبوم + البث (Cast) + التشغيل التلقائي عند الاتصال أو قطع البث + بدء التشغيل مباشرة بعد تبديل اتصالات البث والأجهزة. + سماعات الرأس + استئناف التشغيل عند إعادة توصيل السماعات + إذا توقف التشغيل مؤقتاً بسبب فصل السماعة، فسيتم الاستئناف تلقائياً بمجرد توصيلها مجدداً. + قائمة الانتظار والانتقالات + التداخل (Crossfade) + تفعيل الانتقال السلس والمندمج بين الأغاني المتتالية. + مدة التداخل (Crossfade) + وضع الصوت عالي الدقة (Hi-Fi) + مخرج صوتي بصيغة Float 32-bit. قم بتعطيله إذا لاحظت تقطيعاً في الصوت على جهازك. + غير مدعوم على هذا الجهاز (خاصية PCM_FLOAT AudioTrack غير متوفرة). + التشغيل العشوائي الدائم + تذكر إعداد التشغيل العشوائي والاحتفاظ به حتى بعد إغلاق التطبيق. + إظهار سجل قائمة الانتظار + عرض الأغاني التي تم تشغيلها سابقاً داخل قائمة الانتظار. - التشغيل في الخلفية - استمرار التشغيل بعد الإغلاق - إذا تم إيقافه، فإن إزالة التطبيق من التطبيقات الحديثة سيؤدي لإيقاف التشغيل. - تحسين استهلاك البطارية - تعطيل تحسين البطارية لمنع انقطاع التشغيل في الخلفية. - تم تعطيل تحسين البطارية بالفعل - موازنة مستوى الصوت (ReplayGain) - تفعيل ميزة ReplayGain - تثبيت وموازنة مستويات الصوت تلقائياً باستخدام بيانات ReplayGain الوصفية المرفقة بالملفات. - وضع زيادة الكسب (Gain Mode) - مسار: موازنة كل أغنية على حدة. ألبوم: موازنة الصوت لكل ألبوم بالكامل. - مسار - ألبوم - البث (Cast) - التشغيل التلقائي عند الاتصال أو قطع البث - بدء التشغيل مباشرة بعد تبديل اتصالات البث والأجهزة. - سماعات الرأس - استئناف التشغيل عند إعادة توصيل السماعات - إذا توقف التشغيل مؤقتاً بسبب فصل السماعة، فسيتم الاستئناف تلقائياً بمجرد توصيلها مجدداً. - قائمة الانتظار والانتقالات - التداخل (Crossfade) - تفعيل الانتقال السلس والمندمج بين الأغاني المتتالية. - مدة التداخل (Crossfade) - وضع الصوت عالي الدقة (Hi-Fi) - مخرج صوتي بصيغة Float 32-bit. قم بتعطيله إذا لاحظت تقطيعاً في الصوت على جهازك. - غير مدعوم على هذا الجهاز (خاصية PCM_FLOAT AudioTrack غير متوفرة). - التشغيل العشوائي الدائم - تذكر إعداد التشغيل العشوائي والاحتفاظ به حتى بعد إغلاق التطبيق. - إظهار سجل قائمة الانتظار - عرض الأغاني التي تم تشغيلها سابقاً داخل قائمة الانتظار. + + المجلدات + إيماءة الرجوع تتحكم بالمجلدات + في تبويب المجلدات، يؤدي الرجوع للتنقل عبر تسلسل المجلدات أولاً قبل الخروج من المكتبة. + إيماءات المشغل + النقر على الخلفية يغلق المشغل + النقر على الخلفية الضبابية يؤدي لإغلاق صفحة المشغل المنبثقة. + المؤثرات اللمسية + الاستجابة اللمسية (الاهتزاز) + تفعيل اهتزازات الاستجابة الخفيفة عبر أرجاء التطبيق. - المجلدات - إيماءة الرجوع تتحكم بالمجلدات - في تبويب المجلدات، يؤدي الرجوع للتنقل عبر تسلسل المجلدات أولاً قبل الخروج من المكتبة. - إيماءات المشغل - النقر على الخلفية يغلق المشغل - النقر على الخلفية الضبابية يؤدي لإغلاق صفحة المشغل المنبثقة. - المؤثرات اللمسية - الاستجابة اللمسية (الاهتزاز) - تفعيل اهتزازات الاستجابة الخفيفة عبر أرجاء التطبيق. + + مزود الذكاء الاصطناعي + المزود + اختر مزود خدمة الذكاء الاصطناعي الخاص بك + وضع الرموز الآمن (Safe Token) + مفعل — سريع وموفر. يرسل بيانات قليلة ومحدودة (~1K رموز) إلى الذكاء الاصطناعي. + معطل — سياق عميق. يرسل ملف الاستماع الكامل بالكامل (~8K رموز) لنتائج أكثر ثراءً ودقة. + بيانات الاعتماد + مفتاح API لـ %1$s + احصل عليه من %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) + اختيار النموذج + جاري تحميل النماذج المتاحة… + فشل جلب النماذج + نموذج الذكاء الاصطناعي + اختر نموذجاً. + أدخل مفتاح API + سلوك الأوامر (Prompt) + الأمر البرمجي للنظام + تخصيص وتحديد كيفية تصرف واستجابة الذكاء الاصطناعي. + الأوامر المحددة مسبقاً + أدخل الأمر البرمجي للنظام… + منسق محترف (Professional Curator) + أنت \'Vibe-Engine\'، منسق موسيقى عالمي وخبير في الانسياب الصوتي. هدفك هو بناء تجارب استماع سلسة وعالية الدقة. أعطِ الأولوية للتوافق التناغمي، والانتقالات المنطقية لسرعة الإيقاع (BPM)، والتوازن المتطور بين الأغاني المفضلة المألوفة والاكتشاف الذكي القائم على المنطق. + مستكشف مبتكر (Creative Maverick) + أنت مستكشف موسيقى طليعي متخصص في \'الترابط غير المتوقع\'. مهمتك هي كسر حدود الأنواع التقليدية من خلال تحديد توازيات صوتية غير واضحة. أعطِ الأولوية للأغاني العميقة النادرة، والتراكيب التجريبية، والجدة الفنية مع الحفاظ على منطق انتقال مفاجئ ولكنه لا غبار عليه. + أمين مكتبة صارم (Strict Librarian) + أنت مهندس قواعد بيانات موسيقية دقيق للغاية. منطقك مدفوع بدقة البيانات الوصفية المطلقة والالتزام الفئوي الصارم. قلل من الاكتشاف الخوارزمي لصالح اتساق النوع الموسيقي الصارم، ومطابقة مستوى الطاقة، وتوسيع نطاق استرداد تفضيلات المستخدم المحددة بدقة. + دليل الأجواء الهادئة (Atmospheric Guide) + أنت خبير في التراكيب الصوتية المحيطة والانسياب منخفض الطاقة. ركز حصرياً على المسارات التي تسهل حالة \'التركيز العميق\' أو \'الهدوء\'. أعطِ الأولوية للدفء الصوتي، والترتيبات البسيطة، والانتقالات اللطيفة، مع تجنب الأصوات الحادة أو التحولات المفاجئة في النطاف الديناميكي تماماً. + عاشق الصوتيات (Sonic Enthusiast) + أنت محلل محب للصوتيات تركز على تعقيد الإنتاج والآلات الموسيقية. أعطِ الأولوية للمسارات التي تتميز بنطاق ديناميكي واسع، وإيقاعات معقدة متعددة، وجودة صوت استثنائية. فضل المقطوعات التي تتطلب استماعاً نشطاً وتكافئ المستمع على الانتباه لتفاصيل الدقة الفنية والتوزيع. + محفز الطاقة (Energy Catalyst) + أنت مولد إيقاع عالي النشاط. تتمحور فلسفتك حول خطوط الباص القوية، والكثافة الإيقاعية، والأخاديد الجذابة. أعطِ الأولوية للمقطوعات السريعة المتوافقة مع أجواء النوادي (High-BPM)، والطاقة المتزامنة، والتوتر الإيقاعي المستمر للحفاظ على معدل ضربات قلب المستمع وحافزه في أعلى مستوى. + تقرير استخدام الذكاء الاصطناعي + إجمالي الاستهلاك + تتبع رموز %1$s \nالمدخلات (Prompt): %2$s | المخرجات: %3$s | التفكير: %4$s + مسح السجلات + سجل نشاط الذكاء الاصطناعي (%1$d) + %1$s · %2$s + إظهار + إخفاء - مزود الذكاء الاصطناعي - المزود - اختر مزود خدمة الذكاء الاصطناعي الخاص بك - وضع الرموز الآمن (Safe Token) - مفعل — سريع وموفر. يرسل بيانات قليلة ومحدودة (~1K رموز) إلى الذكاء الاصطناعي. - معطل — سياق عميق. يرسل ملف الاستماع الكامل بالكامل (~8K رموز) لنتائج أكثر ثراءً ودقة. - بيانات الاعتماد - مفتاح API لـ %1$s - احصل عليه من %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) - اختيار النموذج - جاري تحميل النماذج المتاحة… - نموذج الذكاء الاصطناعي - اختر نموذجاً. - سلوك الأوامر (Prompt) - الأمر البرمجي للنظام - تخصيص وتحديد كيفية تصرف واستجابة الذكاء الاصطناعي. - تقرير استخدام الذكاء الاصطناعي - إجمالي الاستهلاك - تتبع رموز %1$s \nالمدخلات (Prompt): %2$s | المخرجات: %3$s | التفكير: %4$s + + كيفية عمل النسخ الاحتياطي + اختر الأقسام، وقم بتصدير ملف بصيغة .pxpl، واستورده لاحقاً. الاستعادة ستقوم فقط باستبدال الأقسام التي تحددها بنفسك. + إنشاء نسخة احتياطية + تصدير النسخة الاحتياطية + لم يتم اختيار أي قسم. + تم اختيار جميع الأقسام. + تم اختيار %1$d من أصل %2$d من الأقسام. + %1$s ينشئ ملف نسخة احتياطية .pxpl. + تحديد وتصدير + استعادة النسخة الاحتياطية + استيراد نسخة احتياطية + تحديد واستعادة + تصفح أو اختر من النسخ الاحتياطية الأخيرة. البيانات المحددة ستستبدل البيانات الحالية. - إنشاء نسخة احتياطية - تصدير النسخة الاحتياطية - يقوم بإنشاء ملف نسخة احتياطية بصيغة %1$s .pxpl. - استعادة النسخة الاحتياطية - استيراد نسخة احتياطية - تصفح أو اختر من النسخ الاحتياطية الأخيرة. البيانات المحددة ستستبدل البيانات الحالية. - الميزات التجريبية - تجريبي - تجارب ومفاتيح تبديل لتحميل واجهة المستخدم للمشغل. - اختبار تدفق الإعداد الأولية - تشغيل شاشة الإعداد والترحيب المبدئية لأغراض الاختبار. - الصيانة - فرض إعادة توليد المزيج اليومي - إعادة إنشاء قائمة تشغيل المزيج اليومي فوراً. - فرض إعادة توليد الإحصائيات - مسح ذاكرة التخزين المؤقت وإعادة حساب إحصاءات التشغيل بالكامل. - فرض إعادة توليد لوحة ألوان الألبوم - التشخيص والأعطال - إطلاق تعطل تجريبي - محاكاة تعطل مفاجئ للتطبيق لاختبار نظام تقارير الأخطاء. - التطبيق - حول PixelPlayer - إصدار التطبيق، الحقوق والمساهمين، والمزيد. + + اختر بدقة ما ترغب في تضمينه داخل حزمة النسخ الاحتياطي. + اختر ملف نسخة احتياطية بصيغة .pxpl لفحصه. ستتمكن من اختيار الأقسام المراد استعادتها في الخطوة التالية. + تم تحديد %1$d من أصل %2$d أقسام + تم تحديد %1$d من أصل %2$d وحدات + النسخ الاحتياطية الأخيرة + لا توجد نسخ احتياطية حديثة + النسخ الاحتياطية التي تم استيرادها سابقاً ستظهر هنا. + إدخالات عدد %1$d · ستستبدل البيانات الحالية بالكامل + تصدير ملف .pxpl + استعادة المحدد + عملية النقل جارية… + PixelPlayer_Backup_%1$d.pxpl + جاري إنشاء النسخة الاحتياطية + جاري استعادة النسخة الاحتياطية + %1$d%% + %1$s • %2$s + جاري التصدير + جاري الاستيراد + جاري الاستعادة + إزالة من السجل + جاري الفحص المعمق… + تصفح لاختيار الملف + الخطوة %1$d من أصل %2$d + استعادة الوحدات + تفاصيل النسخة الاحتياطية + تم الإنشاء + إصدار التطبيق + المخطط البرمجي (Schema) + الجهاز + غير معروف + · %1$s + وحدات عدد %1$d · إصدار %2$s · إصدار المخطط البرمجي %3$d + \? + تحديد الكل + مسح التحديد - لم يتم اختيار أي قسم. - تم اختيار جميع الأقسام. - تم اختيار %1$d من أصل %2$d من الأقسام. - كيفية عمل النسخ الاحتياطي - اختر الأقسام، وقم بتصدير ملف بصيغة .pxpl، واستورده لاحقاً. الاستعادة ستقوم فقط باستبدال الأقسام التي تحددها بنفسك. - اختر بدقة ما ترغب في تضمينه داخل حزمة النسخ الاحتياطي. - تصدير ملف .pxpl - تم تحديد %1$d من أصل %2$d أقسام - عملية النقل جارية… - جاري التصدير - جاري الاستيراد - جاري إنشاء النسخة الاحتياطية - جاري استعادة النسخة الاحتياطية - الخطوة %1$d من أصل %2$d - إدخالات عدد %1$d · ستستبدل البيانات الحالية بالكامل + + نسخة احتياطية غير صالحة: %1$s + تحضير الاستعادة + بدء مهمة الاستعادة. + تحضير النسخ الاحتياطي + بدء مهمة النسخ الاحتياطي. + تم استعادة النسخة الاحتياطية بنجاح + اكتملت الاستعادة مع وجود بعض المشكلات غير المحلولة. + تعذر إكمال الاستعادة: %1$s + فشلت الاستعادة: %1$s + تم تصدير البيانات بنجاح + فشل التصدير: %1$s + تم استعادة البيانات بنجاح + اكتملت الاستعادة مع وجود بعض المشكلات غير المحلولة. فشل: %1$s + إصدار %1$d + %1$s %2$s - تم إعادة توليد لوحة الألوان لـ %1$s - تعذر إعادة توليد لوحة الألوان لـ %1$s - جاري إعادة توليد لوحات ألوان الألبومات… - إعادة توليد جميع لوحات ألوان الألبومات؟ - إعادة بناء متغيرات الألوان المخزنة مؤقتاً لـ %1$d من أغلفة الألبومات الفريدة. قد يستغرق هذا الإجراء بعض الوقت في المكتبات الكبيرة. - سيؤدي هذا إلى مسح بيانات السمات المخزنة مؤقتاً وإعادة بناء جميع أنماط لوحات الألوان لـ %1$d من أغلفة الألبومات الفريدة. - اكتمل %1$d من أصل %2$d - جاري العمل… - إعادة التوليد - تم إعادة توليد %1$d من لوحات ألوان الألبومات - تم إعادة توليد %1$d من أصل %2$d من لوحات ألوان الألبومات + + الميزات التجريبية + تجريبي + تجارب ومفاتيح تبديل لتحميل واجهة المستخدم للمشغل. + اختبار تدفق الإعداد الأولية + تشغيل شاشة الإعداد والترحيب المبدئية لأغراض الاختبار. + الصيانة + فرض إعادة توليد المزيج اليومي + إعادة إنشاء قائمة تشغيل المزيج اليومي فوراً. + إعادة توليد المزيج اليومي + إعادة توليد المزيج اليومي؟ + سيؤدي هذا إلى تجاهل المزيج الحالي وإنشاء مزيج جديد بناءً على عادات الاستماع الأخيرة. + بدأت عملية إعادة توليد المزيج اليومي + فرض إعادة توليد الإحصائيات + مسح ذاكرة التخزين المؤقت وإعادة حساب إحصاءات التشغيل بالكامل. + إعادة التوليد + جاري العمل… + إعادة توليد الإحصائيات + إعادة توليد الإحصائيات؟ + سيؤدي هذا إلى مسح ذاكرة التخزين المؤقت للإحصاءات وفرض إعادة الحساب من سجل قاعدة البيانات. + بدأت عملية إعادة توليد الإحصائيات + فرض إعادة توليد لوحة ألوان الألبوم + إعادة بناء جميع متغيرات لوحة الألوان المخزنة مؤقتاً لكل غلاف ألبوم، أو اختر أغنية واحدة لتحديثها. + إعادة توليد الكل + إعادة توليد جميع لوحات ألوان الألبومات؟ + سيؤدي هذا إلى مسح بيانات السمات المخزنة مؤقتاً وإعادة بناء جميع أنماط لوحات الألوان لـ %1$d من أغلفة الألبومات الفريدة. + جاري إعادة التوليد… + جاري إعادة توليد لوحات ألوان الألبومات… + إعادة بناء متغيرات الألوان المخزنة مؤقتاً لـ %1$d من أغلفة الألبومات الفريدة. قد يستغرق هذا الإجراء بعض الوقت في المكتبات الكبيرة. + اكتمل %1$d من أصل %2$d + تم إعادة توليد %1$d من لوحات ألوان الألبومات + تم إعادة توليد %1$d من أصل %2$d من لوحات ألوان الألبومات + اختر الأغنية + اختر أغنية لمسح بيانات المظهر المخزنة مؤقتاً وإعادة توليد جميع أنماط الألوان من غلاف الألبوم. + البحث حسب العنوان، أو الفنان، أو الألبوم + لا توجد أغانٍ تطابق بحثك. + لم يتم العثور على أي أغانٍ تحتوي على غلاف ألبوم. + جاري إعادة توليد لوحة الألوان… + تم إعادة توليد لوحة الألوان لـ %1$s + تعذر إعادة توليد لوحة الألوان لـ %1$s + التشخيص والأعطال + إطلاق تعطل تجريبي + محاكاة تعطل مفاجئ للتطبيق لاختبار نظام تقارير الأخطاء. + حدث تعطل تجريبي من خيارات المطورين - هذا الإجراء مقصود لاختبار نظام الإبلاغ عن الأعطال. - إعادة تعيين كلمات الأغاني المستوردة؟ - لا يمكن التراجع عن هذا الإجراء لاحقاً. - تأكيد - إعادة بناء قاعدة البيانات؟ - سيؤدي هذا إلى إعادة بناء مكتبة الموسيقى الخاصة بك بالكامل من الصفر. ستفقد جميع كلمات الأغاني المستوردة، والمفضلة، والبيانات الوصفية المخصصة. لا يمكن التراجع عن هذا الإجراء. - إعادة البناء - جاري إعادة بناء قاعدة البيانات - جاري إعادة بناء قاعدة البيانات… - إعادة توليد المزيج اليومي؟ - سيؤدي هذا إلى تجاهل المزيج الحالي وإنشاء مزيج جديد بناءً على عادات الاستماع الأخيرة. - بدأت عملية إعادة توليد المزيج اليومي - إعادة توليد الإحصائيات؟ - سيؤدي هذا إلى مسح ذاكرة التخزين المؤقت للإحصاءات وفرض إعادة الحساب من سجل قاعدة البيانات. - بدأت عملية إعادة توليد الإحصائيات - PixelPlayer_Backup_%1$d.pxpl + + تجريبي + تعديلات تحميل واجهة المشغل + كلمات أغاني متحركة (للأجهزة القوية) + يستخدم رسوماً متحركة وتأثيرات بصرية لكلمات الأغاني. قد يؤدي إلى انخفاض معدل الإطارات في الأجهزة الضعيفة. + تأثير الضبابية للكلمات + تطبيق ضبابية عمق المجال على كلمات الأغاني غير النشطة. + قوة الضبابية + ضبط شدة تأثير الضبابية. + %1$.1fx + الخطوة 1 · اختر ما تريد تأخيره + تأخير كل شيء + تأخير محتوى المشغل بالكامل حتى تتمدد خلفية الشاشة بالكامل. + معرض الألبومات الدائري + تأخير غلاف الألبوم والمعرض حتى يتمدد المشغل. + البيانات الوصفية للأغنية + تأخير العنوان، الفنان، وإجراءات الكلمات/قائمة الانتظار. + شريط التقدم + تأخير الجدول الزمني وعلامات الوقت حتى تكتمل عملية التمدد. + عناصر التحكم في التشغيل + تأخير التشغيل/الإيقاف المؤقت، التمرير، وعناصر التحكم بالمفضلة. + جميع المكونات المؤجلة نشطة. عطل \"تأخير كل شيء\" لتخصيص كل جزء على حدة. + الخطوة 2 · تكوين سلوك العناصر النائبة (Placeholders) + استخدام عناصر نائبة للمكونات المؤجلة + الحفاظ على استقرار التخطيط من خلال عرض عناصر نائبة خفيفة الوزن أثناء انتظار المكونات للتمدد. + الخطوة 3 · اختر وقت تبديل العناصر النائبة بالمحتوى الحقيقي + اختر وضعاً واحداً. يستخدم وضع العتبة (Threshold) أشرطة التمرير؛ بينما ينتظر وضع تحرير السحب (Drag release) حتى تترك إيماءة الشاشة. + قم بتمكين مكون مؤجل واحد على الأقل لفتح وضع التفعيل. + العتبة (Threshold) + يستخدم النسبة المئوية للتمدد. + تحرير السحب + يتم التبديل فقط بعد ترك الإيماءة. + عتبة التمدد + مدى التمدد المطلوب للمشغل قبل أن تصبح المكونات المؤجلة مرئية. + يظهر المحتوى عند تمدد بنسبة %1$d%% + تطبيق أيضاً عند إغلاق المشغل + استخدم عتبة الإغلاق للعودة إلى العناصر النائبة أثناء طي المشغل. + عتبة الإغلاق + مدى الطي المطلوب قبل أن تحل العناصر النائبة محل المحتوى مرة أخرى. + تظهر العناصر النائبة بعد طي بنسبة %1$d%% + يتجاوز وضع تحرير السحب العتبات وسلوك الإغلاق. يحدث التبديل فقط عند انتهاء إيماءة سحب المشغل. + جعل العناصر النائبة شفافة + تحتفظ العناصر النائبة بمساحة تخطيطها ولكنها تصبح غير مرئية. + الجودة البصرية + دقة غلاف الألبوم + منخفضة (256 بكسل) - أداء أفضل + متوسطة (512 بكسل) - متوازنة + عالية (800 بكسل) - أفضل جودة + الأصلية - جودة قصوى - إعادة توليد المزيج اليومي - إعادة توليد الإحصائيات - لم يتم العثور على أي أغانٍ تحتوي على غلاف ألبوم. - إعادة بناء جميع متغيرات لوحة الألوان المخزنة مؤقتاً لكل غلاف ألبوم، أو اختر أغنية واحدة لتحديثها. - إعادة توليد الكل - جاري إعادة التوليد… - اختر الأغنية + + التشغيل يحتاج إلى مراجعة + جاهز للتشغيل + -- + الصيغ + أجهزة فك ترميز العتاد (HW) + الأغاني المحلية + مساحة تخزين الموسيقى المحلية + حجم الموسيقى + %1$d أغنية محلية + المتاح + الإجمالي %1$s + حجم استهلاك الموسيقى + المستخدم من الجهاز + %1$d%% + أقل من 1% + %1$d أغنية سحابية + %1$d ملفات غير مقروءة + مسار التشغيل + نعم + لا + معدل العينة + %1$d هرتز + %1$d إطارات لكل مخزن مؤقت + تنسيق Hi-Fi PCM Float + مسار إخراج عائم 32 بت + دعم زمن الانتقال المنخفض (Low Latency) + دعم الصوت الاحترافي (Pro Audio) + الذاكرة + متاح من أصل %1$s + الصيغ الجاهزة لتخفيف حمل المعالجة (Offload) + لم تبلغ أي صيغة مضغوطة عن دعم ميزة Offload العتادية. + +%1$d إضافية + مخرجات الصوت المكتشفة + مكبر الصوت المدمج + صوت البلوتوث + صوت USB + سماعة سلكية + مخرج رقمي + مخرج آخر + لم يتم الإبلاغ عن أي مسارات إخراج بواسطة نظام Android. + محرك ExoPlayer + %1$s معالجات الرسوم والترجمة (Renderers) + توافق الصيغ + %1$d مسارات مدعومة + %1$d صيغ غير معروفة + لم يتم الإبلاغ عن وحدة فك ترميز + فك ترميز عتادي + فك ترميز برمجي + تخفيف حمل المعالجة (Offload) + %1$d في المكتبة + نتائج التوافق + لا توجد مشاكل توافق رئيسية + تتطابق مساراتك المفهرسة مع أجهزة فك الترميز التي يبلغ عنها نظام Android على هذا الجهاز. + قد لا يتم فك ترميز %1$d مسارات بشكل أصلي + صيغ للمراجعة: %1$s. + قد يتم إعادة أخذ عينات %1$d من المسارات المحلية + تصل المكتبة إلى %1$d هرتز، وهو أعلى من معدل عينة المخرج الحالي. + %1$d من المسارات تحتوي على بيانات وصفية غير معروفة + يمكن أن يؤدي إجراء فحص كامل للمكتبة إلى ملء بيانات MIME ومعدل البت ومعدل العينات المفقودة. + معلومات الجهاز + الشركة المصنعة + الموديل + العلامة التجارية + الجهاز + إصدار Android + إصدار SDK + العتاد + تقرير الأداء + قم بإنشاء تقرير تشخيصي قابل للمشاركة لمساعدتنا في تصنيف تأخر التشغيل أو الفحص. يحتوي التقرير فقط على بيانات الجهاز والمكتبة والتوقيت — لا يحتوي على مسارات الملفات أو العناوين أو الفنانين. + إنشاء التقرير + إعادة توليد + نسخ + مشاركة + Report copied to clipboard + تقرير أداء PixelPlay + تشخيصات الأداء المتقدمة + معطل افتراضياً. يسجل جدولاً زمنياً قصيراً للتأخر لاستكشاف الأخطاء وإصلاحها في النسخة التجريبية. + نشط حتى %1$s + تحديد التأخر الآن + تم تحديد لحظة التأخر - مسح السجلات - سجل نشاط الذكاء الاصطناعي (%1$d) - إظهار - إخفاء - تحديد وتصدير - تحديد واستعادة + + الحسابات المتصلة + إدارة المزودين المرتبطين والتحكم الكامل في كل عملية دمج وتكامل. + الخدمات المرتبطة + نشط + متاح + قريباً + متصل + قريباً + فتح الخدمة + جاري تسجيل الخروج… + لا توجد حسابات مرتبطة بعد + قم بربط أحد المزودين لإدارته من هذه الشاشة. + ربط %1$s + %1$s (قريباً) + خدمة Google Drive ستتوفر قريباً. + تعذر فتح هذه الشاشة في الوقت الحالي. - استيراد نسخة احتياطية - جاري الفحص المعمق… - تصفح لاختيار الملف - اختر ملف نسخة احتياطية بصيغة .pxpl لفحصه. ستتمكن من اختيار الأقسام المراد استعادتها في الخطوة التالية. - النسخ الاحتياطية الأخيرة - لا توجد نسخ احتياطية حديثة - النسخ الاحتياطية التي تم استيرادها سابقاً ستظهر هنا. - فرض إعادة توليد لوحة ألوان الألبوم - اختر أغنية لمسح بيانات المظهر المخزنة مؤقتاً وإعادة توليد جميع أنماط الألوان من غلاف الألبوم. - البحث حسب العنوان، أو الفنان، أو الألبوم - جاري إعادة توليد لوحة الألوان… - لا توجد أغانٍ تطابق بحثك. - إزالة من السجل - مسح البحث - وحدات عدد %1$d · إصدار %2$s · إصدار المخطط البرمجي %3$d - Korean (الكورية) - Norwegian (النرويجية بوكمول) + + حول التطبيق + PixelPlayer + مشغل موسيقى مفتوح المصدر تم بناؤه بمساعدة مجتمعه. + الإصدار v%1$s + مفتوح المصدر + المجتمع أولاً + تصميم Material 3 تعبيري + لم يتم العثور على مساهمين في الوقت الحالي. يرجى المحاولة مرة أخرى لاحقاً. + المطور الأساسي + الشخص المسؤول عن تطوير PixelPlayer. + أبرز المساهمين في المجتمع + تقدير للمتعاونين ذوي التأثير الكبير. + المساهمون في المشروع المفتوح المصدر + قائمة حية بالمساهمين مباشرة من GitHub. + %1$d مساهمة + فتح الملف الشخصي على GitHub + فتح Telegram + الصورة الرمزية لـ %1$s + أيقونة %1$s diff --git a/app/src/main/res/values-ar/strings_widget.xml b/app/src/main/res/values-ar/strings_widget.xml new file mode 100644 index 000000000..ed8705870 --- /dev/null +++ b/app/src/main/res/values-ar/strings_widget.xml @@ -0,0 +1,17 @@ + + + أداة ذكية مستجيبة تتكيف مع حجمها + شريط مشغل مضغوط + عناصر تحكم كاملة مع التشغيل العشوائي والتكرار + مشغل مربع بسيط + + انقر للفتح + غلاف الألبوم + مواضع غلاف الألبوم + + انقر للتشغيل + عنوان الأغنية + الفنان + + شريط التقدم، %1$d بالمئة + \ No newline at end of file diff --git a/app/src/main/res/values/strings_settings.xml b/app/src/main/res/values/strings_settings.xml index 298046dec..1b327476d 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 + العربية App Theme Switch between light, dark, or follow system appearance. Light Theme From 77ca1d855d86bf46e3f61ca3ad3a57293f7855fd Mon Sep 17 00:00:00 2001 From: adlifarizi Date: Sat, 13 Jun 2026 22:48:17 +0700 Subject: [PATCH 15/99] feat: add version 0.7.5 changelog entries and refine Indonesia translation strings --- .../main/res/values-de/strings_changelogs.xml | 18 ++++++++++++++++++ .../main/res/values-es/strings_changelogs.xml | 18 ++++++++++++++++++ app/src/main/res/values-in/strings.xml | 2 +- .../main/res/values-in/strings_changelogs.xml | 18 ++++++++++++++++++ app/src/main/res/values-in/strings_library.xml | 4 ++-- .../main/res/values-ko/strings_changelogs.xml | 18 ++++++++++++++++++ .../main/res/values-nb/strings_changelogs.xml | 18 ++++++++++++++++++ .../main/res/values-ru/strings_changelogs.xml | 18 ++++++++++++++++++ .../main/res/values-tr/strings_changelogs.xml | 18 ++++++++++++++++++ .../res/values-zh-rCN/strings_changelogs.xml | 18 ++++++++++++++++++ 10 files changed, 147 insertions(+), 3 deletions(-) diff --git a/app/src/main/res/values-de/strings_changelogs.xml b/app/src/main/res/values-de/strings_changelogs.xml index b5e894776..f70432e6c 100644 --- a/app/src/main/res/values-de/strings_changelogs.xml +++ b/app/src/main/res/values-de/strings_changelogs.xml @@ -129,4 +129,22 @@ Lokalisierung: Spanisch, Französisch, Russisch, Vereinfachtes Chinesisch, Indonesisch, Italienisch + + Google Drive-Integration mit Player-Lebenszyklusverwaltung. + Massenbearbeitung von Song-Metadaten (Tags und Cover-Art). + KI-Lyrics-Übersetzung mit anpassbaren Wear OS-Einstellungen. + Verzögerungsdiagnosetool und Mehrfachauswahl auf dem Suchbildschirm. + Arabisch- & Türkisch-Unterstützung mit lokalisierten HTTP-URL-Optionen für lokale Netzwerke. + + + Drastische Akkueinsparung (Audio-Offload und UI-Polling-Gates). + Optimierte Queue-Verwaltung (schnellere Einfügungen und explizite Indizierung). + Material 3 Expressive-Bewegungsanimationen für Übergangsbildschirme. + Refaktorisierte Bibliotheks-Synchronisation via gedrosseltem Scannen. + + + Wiedergabeverzögerungen (Ruckeln/Überspringen) und Pufferungsprobleme behoben. + Synchronisation beim Löschen externer Songs und Metadaten-Konsistenz behoben. + Speicherprobleme, Abstürze und Layout-Fehler auf Wear OS und Smartphone behoben. + \ No newline at end of file diff --git a/app/src/main/res/values-es/strings_changelogs.xml b/app/src/main/res/values-es/strings_changelogs.xml index 22836ca8e..4942adfe6 100644 --- a/app/src/main/res/values-es/strings_changelogs.xml +++ b/app/src/main/res/values-es/strings_changelogs.xml @@ -129,4 +129,22 @@ Localización: Español, francés, ruso, chino simplificado, indonesio, italiano + + Integración de Google Drive con gestión del ciclo de vida del reproductor. + Edición por lotes de metadatos de canciones (etiquetas y carátulas). + Traducción de letras por IA con preferencias personalizables de Wear OS. + Herramienta de diagnóstico de retraso y selección múltiple en la pantalla de búsqueda. + Soporte para árabe y turco, con opciones de red local de URL http localizadas. + + + Ahorro drástico de batería (descarga de audio y puertas de sondeo de interfaz de usuario). + Gestión de cola optimizada (inserciones más rápidas e indexación explícita). + Animaciones de movimiento expresivas de Material 3 para pantallas de transición. + Refactorización de la sincronización de la biblioteca mediante escaneo limitado. + + + Se resolvieron los retrasos de reproducción (saltos/tartamudeos) y problemas de almacenamiento en búfer. + Se corrigió la sincronización de eliminación de canciones externas y la consistencia de metadatos. + Se corrigieron problemas de memoria, cierres inesperados y fallos de diseño en Wear OS y teléfono. + \ No newline at end of file diff --git a/app/src/main/res/values-in/strings.xml b/app/src/main/res/values-in/strings.xml index cccb78d7f..64bf20824 100644 --- a/app/src/main/res/values-in/strings.xml +++ b/app/src/main/res/values-in/strings.xml @@ -99,7 +99,7 @@ Lirik Pengaturan Sampul Album - Daftar Putar + Playlist Trek tidak dikenal Artis tidak dikenal Album tidak dikenal diff --git a/app/src/main/res/values-in/strings_changelogs.xml b/app/src/main/res/values-in/strings_changelogs.xml index 74b9d5f0f..f244cae28 100644 --- a/app/src/main/res/values-in/strings_changelogs.xml +++ b/app/src/main/res/values-in/strings_changelogs.xml @@ -129,4 +129,22 @@ Lokalisasi: Spanish, French, Russian, Simplified Chinese, Indonesia, Italian + + Integrasi Google Drive dengan manajemen siklus hidup pemutar. + Pengeditan metadata lagu massal (tag dan gambar sampul). + Terjemahan lirik AI dengan preferensi Wear OS yang dapat disesuaikan. + Alat diagnosis lag dan multi-seleksi di layar Pencarian. + Dukungan bahasa Arab & Turki, dengan opsi jaringan lokal URL HTTP yang dilokalkan. + + + Penghematan baterai drastis (offload audio dan gerbang polling UI). + Manajemen antrean yang dioptimalkan (penyisipan lebih cepat dan pengindeksan eksplisit). + Animasi gerakan Material 3 Expressive untuk layar transisi. + Refaktor sinkronisasi pustaka melalui pemindaian terbatasi (throttled). + + + Menyelesaikan lag pemutaran yang tersendat/terlompat dan masalah buffering. + Memperbaiki sinkronisasi penghapusan lagu eksternal dan konsistensi metadata. + Memperbaiki masalah memori, crash, dan gangguan tata letak pada Wear OS dan ponsel. + \ No newline at end of file diff --git a/app/src/main/res/values-in/strings_library.xml b/app/src/main/res/values-in/strings_library.xml index 9fa5ca168..97e27f38b 100644 --- a/app/src/main/res/values-in/strings_library.xml +++ b/app/src/main/res/values-in/strings_library.xml @@ -192,7 +192,7 @@ %1$d ALBUM terpilih Batas: %1$d album per pilihan. - Antrekan + putar menghormati urutan pilihan Anda. + Antrekan + putar berdasarkan urutan pilihan Anda. %1$d GENRE terpilih Lakukan operasi batch pada semua lagu dalam genre ini. @@ -499,7 +499,7 @@ Gagal mengekspor: %1$s Musik/Ekspor PixelPlayer Silakan konfigurasi API key Gemini Anda di Pengaturan. - Daftar putar dipulihkan + Playlist dipulihkan Membagikan %d playlist Membagikan %d playlist diff --git a/app/src/main/res/values-ko/strings_changelogs.xml b/app/src/main/res/values-ko/strings_changelogs.xml index acb78fe7b..bd7f2e1ed 100644 --- a/app/src/main/res/values-ko/strings_changelogs.xml +++ b/app/src/main/res/values-ko/strings_changelogs.xml @@ -129,4 +129,22 @@ 현지화: 스페인어, 프랑스어, 러시아어, 중국어(간체), 인도네시아어, 이탈리아어 + + 플레이어 수명 주기 관리와 Google Drive 연동. + 노래 메타데이터 일괄 편집 (태그 및 커버 아트). + 맞춤 설정 가능한 Wear OS 환경설정이 포함된 AI 가사 번역. + 검색 화면에서 랙 진단 도구 및 다중 선택. + 아랍어 및 터키어 지원, 현지화된 http URL 로컬 네트워크 옵션 제공. + + + 획기적인 배터리 절약 (오디오 오프로드 및 UI 폴링 게이트). + 최적화된 대기열 관리 (빠른 삽입 및 명시적 인덱싱). + 전환 화면을 위한 Material 3 Expressive 모션 애니메이션. + 제한된 스캔(throttled scanning)을 통한 라이브러리 동기화 리팩토링. + + + 재생 중 끊김/건너뜀 지연 및 버퍼링 문제 해결. + 외부 곡 삭제 동기화 및 메타데이터 일관성 수정. + Wear OS 및 휴대폰의 메모리 문제, 크래시 및 레이아웃 오류 수정. + \ No newline at end of file diff --git a/app/src/main/res/values-nb/strings_changelogs.xml b/app/src/main/res/values-nb/strings_changelogs.xml index c12abe637..46d9f2744 100644 --- a/app/src/main/res/values-nb/strings_changelogs.xml +++ b/app/src/main/res/values-nb/strings_changelogs.xml @@ -129,4 +129,22 @@ Lokalisering: Spansk, Fransk, Russisk, Forenklet kinesisk, Indonesisk, Italiensk + + Google Drive-integrasjon med spillerens livssyklushåndtering. + Masseredigering av sangmetadata (tagger og coverbilde). + AI-sangtekstoversettelse med tilpassbare Wear OS-preferanser. + Diagnoseverktøy for forsinkelse og flervalg på søkeskjermen. + Støtte for arabisk & tyrkisk, med lokaliserte http URL-alternativer for lokalt nettverk. + + + Drastisk batterisparing (lydoffload og UI-pollingporter). + Optimalisert køhåndtering (raskere innsettinger og eksplisitt indeksering). + Material 3 Expressive-bevegelsesanimasjoner for overgangsskjermer. + Refaktorert biblioteksynkronisering via begrenset skanning. + + + Løst hakking/hopping under avspilling og bufferproblemer. + Fikset synkronisering av sletting av eksterne sanger og konsistens i metadata. + Fikset minneproblemer, krasj og layoutfeil på Wear OS og telefon. + \ No newline at end of file diff --git a/app/src/main/res/values-ru/strings_changelogs.xml b/app/src/main/res/values-ru/strings_changelogs.xml index 1e30b701c..187dd7402 100644 --- a/app/src/main/res/values-ru/strings_changelogs.xml +++ b/app/src/main/res/values-ru/strings_changelogs.xml @@ -129,4 +129,22 @@ Локализация: испанский, французский, русский, упрощённый китайский, индонезийский, итальянский + + Интеграция с Google Drive с управлением жизненным циклом плеера. + Пакетное редактирование метаданных песен (теги и обложки). + Перевод текстов песен с помощью ИИ с настраиваемыми предпочтениями Wear OS. + Инструмент диагностики задержек и множественный выбор на экране поиска. + Поддержка арабского и турецкого языков с локализованными параметрами локальной сети для http-адресов. + + + Значительное энергосбережение (разгрузка аудио и оптимизация опроса интерфейса). + Оптимизированное управление очередью (ускоренная вставка и явная индексация). + Выразительные анимации движения Material 3 для экранов перехода. + Рефакторинг синхронизации медиатеки с ограничением частоты сканирования. + + + Устранены заикания, пропуски воспроизведения и проблемы с буферизацией. + Исправлена синхронизация при удалении внешних песен и согласованность метаданных. + Исправлены проблемы с памятью, сбои и ошибки макета на Wear OS и телефоне. + \ No newline at end of file diff --git a/app/src/main/res/values-tr/strings_changelogs.xml b/app/src/main/res/values-tr/strings_changelogs.xml index be468d7c7..aa9d06800 100644 --- a/app/src/main/res/values-tr/strings_changelogs.xml +++ b/app/src/main/res/values-tr/strings_changelogs.xml @@ -129,4 +129,22 @@ Yerelleştirme: İspanyolca, Fransızca, Rusça, Basitleştirilmiş Çince, Endonezce, İtalyanca + + Oynatıcı yaşam döngüsü yönetimiyle Google Drive entegrasyonu. + Toplu şarkı meta verisi düzenleme (etiketler ve kapak resmi). + Özelleştirilebilir Wear OS tercihleriyle yapay zeka şarkı sözü çevirisi. + Arama ekranında gecikme teşhis aracı ve çoklu seçim. + Yerelleştirilmiş http URL yerel ağ seçenekleriyle Arapça & Türkçe desteği. + + + Büyük pil tasarrufu (ses aktarımı ve kullanıcı arayüzü yoklama geçitleri). + Optimize edilmiş kuyruk yönetimi (daha hızlı eklemeler ve açık dizin oluşturma). + Geçiş ekranları için Material 3 Ekspresif hareket animasyonları. + Sınırlandırılmış tarama yoluyla kütüphane senkronizasyonu yeniden yapılandırıldı. + + + Oynatmada takılma/atlama gecikmeleri ve arabelleğe alma sorunları çözüldü. + Harici şarkı silme senkronizasyonu ve meta veri tutarlılığı düzeltildi. + Wear OS ve telefonda bellek sorunları, çökmeler ve düzen hataları düzeltildi. + \ No newline at end of file diff --git a/app/src/main/res/values-zh-rCN/strings_changelogs.xml b/app/src/main/res/values-zh-rCN/strings_changelogs.xml index b0b82e4a3..345246805 100644 --- a/app/src/main/res/values-zh-rCN/strings_changelogs.xml +++ b/app/src/main/res/values-zh-rCN/strings_changelogs.xml @@ -129,4 +129,22 @@ 本地化:西班牙语、法语、俄语、简体中文、印度尼西亚语、意大利语 + + Google Drive 集成,支持播放器生命周期管理。 + 批量编辑歌曲元数据(标签和封面)。 + AI 歌词翻译,支持自定义 Wear OS 偏好设置。 + 搜索界面新增卡顿诊断工具和多选功能。 + 支持阿拉伯语和土耳其语,并提供本地化 http URL 局域网选项。 + + + 大幅省电(音频卸载和 UI 轮询门槛)。 + 优化队列管理(更快的插入和显式索引)。 + 适用于过渡界面的 Material 3 表达性运动动画。 + 通过限制扫描频率重构媒体库同步。 + + + 解决了播放卡顿/跳音延迟和缓冲问题。 + 修复了外部歌曲删除同步和元数据一致性问题。 + 修复了 Wear OS 和手机上的内存问题、崩溃和布局异常。 + \ No newline at end of file From c58161011f52893708e640be307d3ec13578fb17 Mon Sep 17 00:00:00 2001 From: adlifarizi Date: Sat, 13 Jun 2026 22:32:43 +0700 Subject: [PATCH 16/99] refactor: replace language string resources with native names in AppLanguage enum and remove legacy translation keys --- .../pixelplay/data/preferences/AppLanguage.kt | 32 +++++++++---------- .../main/res/values-ar/strings_settings.xml | 11 ------- .../main/res/values-de/strings_settings.xml | 11 ------- .../main/res/values-es/strings_settings.xml | 11 ------- .../main/res/values-fr/strings_settings.xml | 11 ------- .../main/res/values-in/strings_settings.xml | 11 ------- .../main/res/values-it/strings_settings.xml | 11 ------- .../main/res/values-ko/strings_settings.xml | 11 ------- .../main/res/values-nb/strings_settings.xml | 11 ------- .../main/res/values-ru/strings_settings.xml | 11 ------- .../main/res/values-tr/strings_settings.xml | 11 ------- .../res/values-zh-rCN/strings_settings.xml | 11 ------- app/src/main/res/values/strings_settings.xml | 12 ------- 13 files changed, 16 insertions(+), 149 deletions(-) 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 fb51de77b..4e64f0fed 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 @@ -4,29 +4,29 @@ import android.content.Context import androidx.annotation.StringRes import com.theveloper.pixelplay.R -enum class AppLanguage(val tag: String, @StringRes val labelRes: Int) { - SYSTEM("", R.string.settings_language_system), - ENGLISH("en", R.string.settings_language_english), - GERMAN("de", R.string.settings_language_german), - SPANISH("es", R.string.settings_language_spanish), - FRENCH("fr", R.string.settings_language_french), - INDONESIAN("in", R.string.settings_language_indonesian), - ITALIAN("it", R.string.settings_language_italian), - KOREAN("ko", R.string.settings_language_korean), - NORWEGIAN_BOKMAL("nb", R.string.settings_language_norwegian_bokmal), - RUSSIAN("ru", R.string.settings_language_russian), - SIMPLIFIED_CHINESE("zh-CN", R.string.settings_language_chinese), - TURKISH("tr", R.string.settings_language_turkish), - ARABIC("ar", R.string.settings_language_arabic); +enum class AppLanguage(val tag: String, val nativeName: String, @StringRes val labelRes: Int?) { + SYSTEM("", "", R.string.settings_language_system), + ENGLISH("en", "English", null), + GERMAN("de", "Deutsch", null), + SPANISH("es", "Español", null), + FRENCH("fr", "Français", null), + INDONESIAN("in", "Bahasa Indonesia", null), + ITALIAN("it", "Italiano", null), + KOREAN("ko", "한국어", null), + NORWEGIAN_BOKMAL("nb", "Norsk bokmål", null), + RUSSIAN("ru", "Русский", null), + SIMPLIFIED_CHINESE("zh-CN", "简体中文", null), + TURKISH("tr", "Türkçe", null), + ARABIC("ar", "العربية", null); companion object { val supportedLanguageTags: Set = values().map { it.tag }.toSet() fun getLanguageOptions(context: Context): Map { - val systemOption = SYSTEM.tag to context.getString(SYSTEM.labelRes) + val systemOption = SYSTEM.tag to (SYSTEM.labelRes?.let { context.getString(it) } ?: "") val otherOptions = values() .filter { it != SYSTEM } - .map { it.tag to context.getString(it.labelRes) } + .map { it.tag to it.nativeName } .sortedBy { it.second.lowercase() } val result = LinkedHashMap() diff --git a/app/src/main/res/values-ar/strings_settings.xml b/app/src/main/res/values-ar/strings_settings.xml index fdf62db5a..a1aa5c29b 100644 --- a/app/src/main/res/values-ar/strings_settings.xml +++ b/app/src/main/res/values-ar/strings_settings.xml @@ -163,17 +163,6 @@ لغة التطبيق اختر اللغة المستخدمة في واجهة التطبيق بكاملها. افتراضية النظام - English (الإنجليزية) - Español (الإسبانية) - Deutsch (الألمانية) - Français (الفرنسية) - Русский (الروسية) - 简体中文 (الصينية المبسطة) - Bahasa Indonesia (الإندونيسية) - Italiano (الإيطالية) - Korean (الكورية) - Norwegian (النرويجية بوكمول) - Türkçe (التركية) مظهر التطبيق التنقل بين المظهر الفاتح، الداكن، أو تتبع النظام. المظهر الفاتح diff --git a/app/src/main/res/values-de/strings_settings.xml b/app/src/main/res/values-de/strings_settings.xml index 69f37192e..745c5d752 100644 --- a/app/src/main/res/values-de/strings_settings.xml +++ b/app/src/main/res/values-de/strings_settings.xml @@ -163,17 +163,6 @@ Sprache Sprache der App-Oberfläche festlegen. Systemstandard - English - Español - Deutsch - Français - Русский - 简体中文 - Bahasa Indonesia - Italiano - Koreanisch - Norwegisch Bokmål - Türkisch 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..584f19229 100644 --- a/app/src/main/res/values-es/strings_settings.xml +++ b/app/src/main/res/values-es/strings_settings.xml @@ -163,17 +163,6 @@ Idioma de la app Elige el idioma usado en toda la interfaz de la aplicación. Predeterminado del sistema - Inglés - Español - Alemán - Francés - Ruso - Chino simplificado - Indonesio - Italiano - Coreano - Noruego (Bokmål) - Turco 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..70f716366 100644 --- a/app/src/main/res/values-fr/strings_settings.xml +++ b/app/src/main/res/values-fr/strings_settings.xml @@ -159,17 +159,6 @@ Langue de l\'application Choisissez la langue utilisée dans l\'interface de l\'application. Système par défaut - Anglais - Espagnol - Allemand - Français - Russe - Chinois simplifié - Indonésien - Italien - Coréen - Norvégien (Bokmål) - Turc 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..65146caf6 100644 --- a/app/src/main/res/values-in/strings_settings.xml +++ b/app/src/main/res/values-in/strings_settings.xml @@ -159,17 +159,6 @@ Bahasa Aplikasi Pilih bahasa yang digunakan di seluruh antarmuka aplikasi. Default sistem - Inggris - Spanyol - Jerman - Prancis - Rusia - Tionghoa (Sederhana) - Indonesia - Italia - Korea - Norwegia (Bokmål) - Turki 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..1eefe57be 100644 --- a/app/src/main/res/values-it/strings_settings.xml +++ b/app/src/main/res/values-it/strings_settings.xml @@ -163,17 +163,6 @@ Lingua app Scegli la lingua usata nell\'interfaccia dell\'app. Predefinita sistema - Inglese - Spagnolo - Tedesco - Francese - Russo - Cinese semplificato - Indonesiano - Italiano - Coreano - Norvegese (Bokmål) - Turco 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..f917faad0 100644 --- a/app/src/main/res/values-ko/strings_settings.xml +++ b/app/src/main/res/values-ko/strings_settings.xml @@ -163,17 +163,6 @@ 앱 언어 앱 인터페이스에서 사용할 언어를 선택하세요. 시스템 기본값 - 영어 - 스페인어 - 독일 사람 - 프랑스어 - 러시아어 - 중국어(간체) - 인도네시아어 - 이탈리아어 - 한국어 - 노르웨이어 (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..1c8a71735 100644 --- a/app/src/main/res/values-nb/strings_settings.xml +++ b/app/src/main/res/values-nb/strings_settings.xml @@ -163,17 +163,6 @@ App-språk Velg språket som skal brukes i appen. Systemstandard - Engelsk - Spansk - Tysk - Fransk - Russisk - Kinesisk (forenklet) - Indonesisk - Italiensk - Koreansk - Norsk bokmål - Tyrkisk 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..958c023df 100644 --- a/app/src/main/res/values-ru/strings_settings.xml +++ b/app/src/main/res/values-ru/strings_settings.xml @@ -163,17 +163,6 @@ Язык приложения Выберите язык интерфейса приложения. Системный по умолчанию - Английский - Испанский - Немецкий - Французский - Русский - Китайский - Индонезийский - Итальянский - Корейский - Норвежский (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..2c8ddb582 100644 --- a/app/src/main/res/values-tr/strings_settings.xml +++ b/app/src/main/res/values-tr/strings_settings.xml @@ -163,17 +163,6 @@ Uygulama Dili Uygulama arayüzünde kullanılacak dili seçin. Sistem varsayılanı - İngilizce - Español - Almanca - Fransızca - Rusça - Basitleştirilmiş Çince - Endonezce - İtalyanca - Korece - Norveççe (Bokmål) - Türkçe 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..939cc7929 100644 --- a/app/src/main/res/values-zh-rCN/strings_settings.xml +++ b/app/src/main/res/values-zh-rCN/strings_settings.xml @@ -163,17 +163,6 @@ 应用语言 选择应用界面的语言。 跟随系统 - 英语 - 西班牙语 - 德语 - 法语 - 俄语 - 简体中文 - 印尼语 - 意大利语 - 韩语 - 挪威语(Bokmål) - 土耳其语 应用主题 在浅色、深色之间切换,或跟随系统外观。 浅色主题 diff --git a/app/src/main/res/values/strings_settings.xml b/app/src/main/res/values/strings_settings.xml index 1b327476d..35b492a7d 100644 --- a/app/src/main/res/values/strings_settings.xml +++ b/app/src/main/res/values/strings_settings.xml @@ -163,18 +163,6 @@ App Language Choose the language used across the app interface. System default - English - Español - Deutsch - Français - Русский - 简体中文 - Bahasa Indonesia - Italiano - Korean - Norwegian (Bokmål) - Türkçe - العربية App Theme Switch between light, dark, or follow system appearance. Light Theme From 432796a5ae0f751ae6d7d4e39034b8006e968826 Mon Sep 17 00:00:00 2001 From: Rebornloki Date: Sat, 13 Jun 2026 19:10:06 +0100 Subject: [PATCH 17/99] 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 5bca9746158f97e522e23eab4694f3786f65f6dc Mon Sep 17 00:00:00 2001 From: retrozinndev Date: Sat, 13 Jun 2026 22:26:56 -0300 Subject: [PATCH 18/99] feat(i18n): init pt-BR localization --- app/src/main/res/values-pt-BR/plurals.xml | 39 +++ app/src/main/res/values-pt-BR/strings.xml | 231 ++++++++++++++++++ .../main/res/values-pt-BR/strings_auth.xml | 74 ++++++ .../res/values-pt-BR/strings_components.xml | 189 ++++++++++++++ 4 files changed, 533 insertions(+) create mode 100644 app/src/main/res/values-pt-BR/plurals.xml create mode 100644 app/src/main/res/values-pt-BR/strings.xml create mode 100644 app/src/main/res/values-pt-BR/strings_auth.xml create mode 100644 app/src/main/res/values-pt-BR/strings_components.xml diff --git a/app/src/main/res/values-pt-BR/plurals.xml b/app/src/main/res/values-pt-BR/plurals.xml new file mode 100644 index 000000000..a8377c592 --- /dev/null +++ b/app/src/main/res/values-pt-BR/plurals.xml @@ -0,0 +1,39 @@ + + + + Compartilhando %d playlist + Compartilhando %d playlists + + + %1$d playlist exportada para %2$s + %1$d playlists exportadas para %2$s + + + %d faixa adicionada à fila + %d faixas adicionadas à fila + + + %d faixa vai tocar depois + %d faixas vão tocar depois + + + %d faixa adicionada aos favoritos + %d faixas adicionadas aos favoritos + + + %d faixa removida dos favoritos + %d faixas removidas dos favoritos + + + %d arquivo apagado + %d arquivos apagados + + + Apagar %d faixa? + Apagar %d faixas? + + + %d vez + %d vezes + + diff --git a/app/src/main/res/values-pt-BR/strings.xml b/app/src/main/res/values-pt-BR/strings.xml new file mode 100644 index 000000000..bd8531e00 --- /dev/null +++ b/app/src/main/res/values-pt-BR/strings.xml @@ -0,0 +1,231 @@ + + PixelPlayer + Mudança no nome do App + Nós alteramos o nome do app de PixelPlay para PixelPlayer devido à um problema de marca registrada. Continue tocando! + Não mostrar novamente + Dispensar + Permissão Especial Necessária + Para editar metadados de faixas(arquivos .mp3), o PixelPlayer precisa de acesso à todos os seus arquivos. Isso nos permite editar as tags da faixa diretamente. Por favor conceda tal permissão na próxima página para habilitar a edição de metadados. + Conceder permissão + Acesso à Todos os Arquivos + Erro + OK + Cancelar + Importar + Pesquisar + + Letras + Fechar página de letras + Carregando letras… + Não foi possível encontrar letras para essa faixa. + Letras disponibilizada por + https://lrclib.net/ + Letras não encontradas + Gostaria de procurar por letras online? + Não encontramos letras automaticamente. Você pode editar o título e o artista ou pesquisar manualmente. + Falha na procura de letras + Falha ao procurar letras no remoto + A conexão excedeu do tempo limite. Por favor verifique sua conexão com a internet. + Erro de rede. Por favor verifique sua conexão com a internet. + Erro de servidor (código %d). Por favor, tente novamente mais tarde. + %d resultado(s) encontrado(s) + Pesquisou por \"%s\" + Procurando por letras… + Letras já encontradas. A procura online foi pulada. + Letras embutidas encontradas. A procura online foi pulada. + Arquivo local (.lrc) encontrado. Online fetch skipped. + Mostrar opções das letras + Sempre escolher manualmente o resultado ao invés de selecionar automaticamente + Salvar letras como .lrc + Salvar letras + Escolha qual versão salvar: + Sincronizada (com timestamps) + Simples (apenas texto) + Letras salvas com sucesso + Falha ao salvar letras + Sem letras disponíveis para salvar + Redefinir letra importada + Atraso de sincronização de letra + %+.1fs + Redefinir + Mais cedo + Mais tarde + + Escaneando arquivos de faixas… + Processando arquivos… + %1$d de %2$d arquivos + Sincronizando… + Sincronização completa + Aguardando… + Sincronizando biblioteca… + Finalizando em segundo plano… + Escaneando letras… + Limpando cache de arte de album… + Sincronizando fontes da nuvem… + Faixa desconhecida + Artista desconhecido + Album desconhecido + Escolha um artista + Veja qualquer um dos artistas creditados nesta faixa. + 1 artista + %1$d artistas + Artista principal + Página de artista + Tocar agora + Não foi possível abrir o arquivo de áudio. + Abrir player completo + Fechar player flutuante + Fechar player + Faixa anterior + Próxima faixa + Pausar playback + Tocar + Playlist não encontrada. + Disco %d + + Por favor configure uma chave de API válida nas configurações para o provedor de IA selecionado. + Erro de IA: %s + O provedor de IA selecionado rejeitou o pedido pois a conta não possui créditos ou passou do limite de uso. + O modelo de IA selecionado não está mais disponível. O PixelPlayer tentou mudar para um modelo alternativo automaticamente. + A IA não encontrou nenhuma faixa de acordo com seu prompt. + Escreva uma ideia para seu Mix Diário + Mix Diário atualizado com IA + A IA não conseguiu encontrar faixas para esse mix + + Ordem Aleatória + Aleatorizar todas as faixas + Playlist + Última playlist tocada + + Aleatorizar todas + Última Playlist + Nenhuma playlist encontrada para abrir + + ID de Álbum inválido + ID de Álbum não encontrado + Falha ao carregar dados de álbum: %s + Álbum não encontrado + Não foi possível atualizar: %s + ID de Artista inválido + ID de artista não encontrado + Falha ao carregar dados de artista: %s + Não foi possível encontrar o artista + Nenhuma faixa válida encontrada para tocar + + Widget responsivo que se adapta ao tamanho + Barra do player compacta + Controles completos com Ordem Aleatória e Repetir + Player quadrado minimalista + Processando ação de playback… + + + Nenhuma playlist para compartilhar + Compartilhar playlists + Compartilhamento falhou: %1$s + Nenhuma playlist para exportar + Exportação falhou: %1$s + Exportações de faixa do PixelPlayer + Por favor configure sua chave de API Gemini nas Configurações. + Erro desconhecido + + + Enviando %1$d faixa(s) para relógio + Enviando para relógio + Transferência completa + Transferência falhou + Transferência cancelada + Preparando transferência para relógio + %1$d transferência(s) + Iniciando transferência… + Múltiplas transferências ativas + Preparando transferência… + Transferindo + Completo + Falhou + Cancelado + Preparando + Iniciando + Transferências para relógio + Mostra o progresso em tempo real da transferência de faixas para do celular para o relógio + + + Servidor de transmição de mídia + Transmissão para dispositivo + Serving media to Cast device + %1$s: %2$s + A navegação está temporariamente indisponível em transmissões com este formato de áudio porque poderia travar a sessão de transmissão. + + + Backup inválido: %1$s + Preparando recuperação + Preparando tarefa de recuperação. + Preparando backup + Iniciando tarefa de backup. + Beckup restaurado com sucesso + Restauração completa com alguns problemas. + Não foi possível completar a restauração: %1$s + Restauração falhou: %1$s + Dados exportados com sucesso + Exportação falhou: %1$s + Dados restaurados com sucesso + Restauração completa com alguns problemas. Falhou: %1$s + Falha ao carregar modelos + Crash de Teste engatilhado por meio das Opções de Desenvolvedor - Isso é intencional para testar o sistema de relatório de crash + + + Faixa não encontrada na lista atual + Não foi possível encontrar faixa + Nenhuma faixa encontrada na biblioteca + Playback parou: %1$s terminou (Fim de Faixa). + Faixa + Não há faixas para aleatorizar. + Álbuns Selecionados + Nenhuma faixa tocável encontrada nos álbuns selecionados + Apenas os primeiros %1$d álbuns foram adicionados à fila + %1$d álbuns adicionados à fila (%2$d faixas) + Não foi possível adicionar os álbuns selecionados à fila + Todas as faixas já estão favoritadas + Não haviam faixas favoritadas + Criando arquivo ZIP… + Falha ao compartilhar: %1$s + Não é possível apagar a faixa que está tocando + %1$d arquivos apagados (%2$d pulado - tocando) + %1$d de %2$d arquivos apagados + Falha ao apagar arquivos + Arquivo detectado + Não foi possível apagar arquivo ou não foi encontrado + Eliminação de arquivo cancelada + Permissão negada – não é possível editar arquivos + Permissão negada – não é possível salvar letras + Permissão negada – não é possível editar este arquivo + Metadados atualizados com sucesso + Atualizando %1$d faixas… + %1$d faixas atualizadas com sucesso! + %1$d faixas atualizadas. Falhas: %2$d + Playlist restaurada + Essas faixas serão apagadas permanentemente do seu dispositivo e não será possível recuperá-las. + Apagar + + + %1$d minutos + Fim de faixa + Temporizador configurado para %1$d minutos. + Temporizador cancelado. + Não é possível habilitar fim de faixa: nenhuma faixa ativa. + Temporizador de fim de faixa desativado: faixa alterada de %1$s para %2$s. + O playback irá parar no fim da faixa. + Faixa anterior + Faixa atual + Temporizador de Descanso + Temporizador + Fim da faixa atual + Tempo personalizado + Cancelar temporizador + Definir duração personalizada + Contagem de toque: %1$s + 1 vez + Alternar em + %1$d%% + v%1$d + %1$s %2$s + diff --git a/app/src/main/res/values-pt-BR/strings_auth.xml b/app/src/main/res/values-pt-BR/strings_auth.xml new file mode 100644 index 000000000..ad193a5f3 --- /dev/null +++ b/app/src/main/res/values-pt-BR/strings_auth.xml @@ -0,0 +1,74 @@ + + + + Voltar + Mostrar senha + Esconder senha + Conectando… + Conectar + Detalhes de conexão + Insira o URL do servidor e as credenciais de conta. + URL do Servidor + Nome de Usuário + Senha + Digite a senha + admin + Bem-vindo(a), %1$s! + + + Subsonic / Navidrome + Conecte com seu servidor self-hosted de música + Suporta Navidrome, Airsonic, Gonic, Ampache e outros servidores compatíveis com a API Subsonic. + https://music.example.com + Utilize o endereço base https:// completo do servidor. + Esse é o nome de usuário da sua conta Subsonic ou Navidrome. + Senha do App também funciona se o seu servidor suporta isso. + Senha do App também funciona se o seu servidor suporta isso. + Pré-adicionar https:// + Compatível com Navidrome, Gonic, Airsonic, e outros servidores compatíveis com Subsonic + Navidrome + Subsonic + + + Jellyfin + Conecta com servidores Jellyfin. Ambos HTTP e HTTPS são suportados para acesso pela rede local. + Conectar com seu servidor de mídia Jellyfin + Insira o URL do seu servidor Jellyfin e credenciais de conta. + http://192.168.1.100:8096 + URL completa do seu servidor Jellyfin, includindo a porta. + Seu nome de usuário da conta Jellyfin. + Sua senha da conta Jellyfin. + Pré-adicionar http:// + Conecta com servidores Jellyfin para fazer streaming da sua biblioteca de músicas + Jellyfin + + + Google Drive conectado! + Google Drive + + + Sair do login NetEase? + Sair do login QQ Music? + Você pode voltar mais tarde. O estado da página atual será descartado quando fechar. + Sair + Continuar + Fazer login em NetEase Music + Fazer login em QQ Music + Voltar Web + Avançar Web + Recarregar + Abrir início + Salvando… + Pronto + Tentar novamente + + + O carregamento da página passou do tempo limite. Você pode tentar novamente sem perder seu progresso. + Não foi possível ler os cookies da sessão. + A página está demorando muito para carregar. Recarregue ou utilize outra conexão de rede. + Carregamento do WebView falhou. + HTTP %1$d enquanto carregando NetEase. + HTTP %1$d enquanto carregando QQ Music. + Nenhum cookie encontrado. Faça login primeiro. + Login ainda não detectado. Finalize o login NetEase antes de pressionar Pronto. + Login ainda não detectado. Finalize o login QQ Music antes de pressionar Pronto. + diff --git a/app/src/main/res/values-pt-BR/strings_components.xml b/app/src/main/res/values-pt-BR/strings_components.xml new file mode 100644 index 000000000..b1221d0eb --- /dev/null +++ b/app/src/main/res/values-pt-BR/strings_components.xml @@ -0,0 +1,189 @@ + + + Clique para abrir + Arte de álbum + Área reservada da arte de álbum + Favorito + Tocar + Pausar + Clique para tocar + Título da faixa + Artista + Repetir + Barra de progresso, %1$d por cento + + + Aparência + Alinhamento + Controles + Resetar letras? + Tem certeza de que deseja resetar as letras para essa faixa? + Esconder controles de sincronização + Ajustar sincronização + Mostrar romanização + Mostrar tradução + Desabilitar imersão (único) + Manter tela ligada + Alinhar letras à esquerda + Alinhar letras ao centro + Alinha letras à direita + + + Sem conexão com a internet + Esse conteúdo depende de uma conexão à internet. Por favor, cheque suas configurações de rede e tente novamente. + Você está offline + Por favor, cheque sua conexão com a internet e tente novamente para acessar esse conteúdo. + + + Salvar predefinição personalizada + Insira um nome para sua predefinição de equalizador personalizada. + Nome da predefinição + Renomear predefinição + Nome não pode estar vazio + Salvar + Renomear + + + Etiquetado perfeitamente! + Metadados de IA + Consultando o guia de Mix Diário… + Revise e refine os detalhes gerados + Título + Artista + Álbum + Artista do álbum + Gênero + Compositor + Tentar novamente + Aplicar mudanças + + + Editando metadados da faixa + Editar os metadados de uma faixa pode alterar como ela é mostrada e organizada em sua biblioteca. As mudanças são permanentes e talvez sejam irreversíveis. + Entendido + Informação + Editar faixa + Usar IA Gemini + Mostrar informação + Número da faixa + Número do disco + ReplayGain da faixa (dB) + ReplayGain do álbum (dB) + -6.50 + -8.20 + ReplayGain da faixa + ReplayGain do álbum + Título + Número da faixa + Número do disco + Pesquisar letras em lrclib.net + Arte da capa + Selecione uma imagem quadrada e dê um toque nela para que a faixa fique bonita pelo app. + Mudar arte de capa + Apagar arte de capa + Pré-visualização da nova arte de capa + Arte de capa da faixa atual + Ajustar sua arte de capa + Utilize gestos de pinça e arraste para encontrar o enquadramento desejado. + Aplicar arte de capa + Não foi possível carregar a imagem selecionada + + + Compartilhar arquivo de faixa via + Tocar faixa + Compartilhar arquivo da faixa + Adicionar à fila + Tocar como próxima da fila + Adicionar à playlist + Adicionar à fila + Próximo + Verificando relógio + Transferindo %1$d%% + Transferindo para relógio + Transferência em progresso + Enviar para relógio + Relógio não disponível + Enviar faixa para o relógio + Relógio não disponível + Definir como + Definir como som + Escolha como usar essa faixa como som do sistema + Definir como toque + Definir faixa como toque + Usar essa faixa como + Escolha onde PixelPlayer deve armazenar esse som. + Toque de telefone + Chamadas recebidas + Som de notificação + Alertas de mensagem e de apps + Toque de alarme + Alarmes do relógio + Confirmar mudança do som + Definir \"%1$s\" como seu %2$s? + Definir som + Definir \"%1$s\" como seu %2$s + toque de telefone + som de notificação + toque de alarme + Habilite \"Mudar configurações do sistema\", e retorne ao PixelPlayer para continuar automaticamente. + \"Mudar configurações do sistema\" não foi habilitado. + Definir \"%1$s\" como seu toque de telefone + Apenas faixas locais podem ser utilizadas como toque de telefone. + Não foi possível preparar a faixa para usar como toque de telefone. + Não foi possível definir como toque de telefone: %1$s + Duração + Informações da faixa + Duração + Gênero + Álbum + Artista + Formato de áudio + Provedor + Arquivo + Editar metadados da faixa + Remover dos favoritos + Adicionar aos favoritos + Opções + OPÇÕES + Detalhes + INFO + Detalhes + + + %1$d FAIXAS + selecionada(s) + Tocar todas + Tocar todas + Curtir todas + Descurtir todas + Compartilhar todas como ZIP + Adicionar todas à fila + Apagar todas + Apagar todas + + Playlist dispensada + Desfazer + Mashup do DJ + Nova playlist + Nome da playlist + Minha playlist + Criar + Adicionar %1$d faixas em… + Selecionar playlists + Pesquisar playlists… + + %1$d PLAYLISTS + Exportar todas + Mesclar todas + Compartilhar todas + Exportar + Mesclar + + Reordenar abas da biblioteca + Resetar ordem + Restaurar a ordem padrão das abas? + Reordenando abas… + Mover + Resetar + Pronto + From a3335e2bad7cd7e617626158529ce83e8b37e407 Mon Sep 17 00:00:00 2001 From: ZL114514 <114651468+ZL114514@users.noreply.github.com> Date: Sun, 14 Jun 2026 18:38:07 +0800 Subject: [PATCH 19/99] Feat: the opt for tablet on StartPage Add widthIn(max = 540.dp) to dialogs and several setup pages to constrain content on wide screens and import required layout helpers. Rename the SineWaveLine parameter from `waves` to `wavesDensity`, update its KDoc and default, and adjust the theta calculation to scale density relative to a 380dp baseline width. Update all call sites (BetaInfoBottomSheet, ChangelogBottomSheet, PlaylistContainer, SetupScreen, etc.) to use `wavesDensity`. Also small UI tweaks in SetupScreen: comment out CenterHorizontally alignment, reduce a spacer height, and set the welcome artwork to requiredWidth(380.dp). These changes improve responsive layout behavior and make the sine wave drawing density consistent across different widths. --- .../components/BackupModuleSelectionDialog.kt | 3 +- .../components/BetaInfoBottomSheet.kt | 2 +- .../components/ChangelogBottomSheet.kt | 2 +- .../components/FileExplorerBottomSheet.kt | 3 +- .../components/PlaylistContainer.kt | 33 +++++++++++++++++++ .../components/subcomps/SineWaveLine.kt | 8 ++--- .../presentation/screens/SetupScreen.kt | 21 ++++++++---- 7 files changed, 58 insertions(+), 14 deletions(-) diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/components/BackupModuleSelectionDialog.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/components/BackupModuleSelectionDialog.kt index 1584be7e5..8b7b7af7d 100644 --- a/app/src/main/java/com/theveloper/pixelplay/presentation/components/BackupModuleSelectionDialog.kt +++ b/app/src/main/java/com/theveloper/pixelplay/presentation/components/BackupModuleSelectionDialog.kt @@ -76,6 +76,7 @@ import java.text.SimpleDateFormat import java.util.Date import java.util.Locale import androidx.compose.animation.animateColorAsState +import androidx.compose.foundation.layout.widthIn import androidx.compose.ui.res.stringResource @OptIn( @@ -135,7 +136,7 @@ fun BackupModuleSelectionDialog( label = "import_module_selection_dialog" ) { Surface( - modifier = Modifier.fillMaxSize(), + modifier = Modifier.fillMaxSize().widthIn(max = 540.dp), color = MaterialTheme.colorScheme.surfaceContainerLowest ) { Scaffold( diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/components/BetaInfoBottomSheet.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/components/BetaInfoBottomSheet.kt index 0370ac54d..8e46e4a5a 100644 --- a/app/src/main/java/com/theveloper/pixelplay/presentation/components/BetaInfoBottomSheet.kt +++ b/app/src/main/java/com/theveloper/pixelplay/presentation/components/BetaInfoBottomSheet.kt @@ -127,7 +127,7 @@ fun BetaInfoBottomSheet(modifier: Modifier = Modifier) { alpha = 0.95f, strokeWidth = 4.dp, amplitude = 4.dp, - waves = 7.6f, + wavesDensity = 7.6f, phase = 0f ) } diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/components/ChangelogBottomSheet.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/components/ChangelogBottomSheet.kt index 5d8a3860d..1ef3a16e9 100644 --- a/app/src/main/java/com/theveloper/pixelplay/presentation/components/ChangelogBottomSheet.kt +++ b/app/src/main/java/com/theveloper/pixelplay/presentation/components/ChangelogBottomSheet.kt @@ -165,7 +165,7 @@ fun ChangelogBottomSheet( alpha = 0.95f, strokeWidth = 4.dp, amplitude = 4.dp, - waves = 7.6f, + wavesDensity = 7.6f, phase = 0f ) diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/components/FileExplorerBottomSheet.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/components/FileExplorerBottomSheet.kt index e1aed1b4c..43d85a34d 100644 --- a/app/src/main/java/com/theveloper/pixelplay/presentation/components/FileExplorerBottomSheet.kt +++ b/app/src/main/java/com/theveloper/pixelplay/presentation/components/FileExplorerBottomSheet.kt @@ -28,6 +28,7 @@ import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.systemBars +import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.items @@ -141,7 +142,7 @@ fun FileExplorerDialog( label = "file_explorer_dialog" ) { Surface( - modifier = Modifier.fillMaxSize(), + modifier = Modifier.fillMaxSize().widthIn(max = 540.dp), color = MaterialTheme.colorScheme.surfaceContainerLow ) { FileExplorerContent( diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/components/PlaylistContainer.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/components/PlaylistContainer.kt index ca39bb451..a90efbfa3 100644 --- a/app/src/main/java/com/theveloper/pixelplay/presentation/components/PlaylistContainer.kt +++ b/app/src/main/java/com/theveloper/pixelplay/presentation/components/PlaylistContainer.kt @@ -135,6 +135,7 @@ fun PlaylistContainer( horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(14.dp) ) { +<<<<<<< Updated upstream Surface( shape = CircleShape, color = MaterialTheme.colorScheme.secondaryContainer.copy(alpha = 0.55f), @@ -171,6 +172,38 @@ fun PlaylistContainer( textAlign = TextAlign.Center ) } +======= + SineWaveLine( + modifier = Modifier + .fillMaxWidth() + .height(32.dp) + .padding(horizontal = 8.dp), + color = MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.8f), + alpha = 0.95f, + strokeWidth = 3.dp, + amplitude = 4.dp, + wavesDensity = 7.6f, + phase = 0f + ) + Spacer(Modifier.height(16.dp)) + Icon( + Icons.AutoMirrored.Rounded.PlaylistPlay, + contentDescription = null, + modifier = Modifier.size(48.dp), + tint = MaterialTheme.colorScheme.onSurfaceVariant + ) + Spacer(Modifier.height(8.dp)) + Text( + stringResource(R.string.presentation_batch_e_no_playlist_created), + style = MaterialTheme.typography.titleMedium + ) + Spacer(Modifier.height(6.dp)) + Text( + stringResource(R.string.presentation_batch_e_new_playlist_hint), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) +>>>>>>> Stashed changes } } } else { diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/components/subcomps/SineWaveLine.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/components/subcomps/SineWaveLine.kt index ea703dad3..55e24441c 100644 --- a/app/src/main/java/com/theveloper/pixelplay/presentation/components/subcomps/SineWaveLine.kt +++ b/app/src/main/java/com/theveloper/pixelplay/presentation/components/subcomps/SineWaveLine.kt @@ -22,7 +22,7 @@ import kotlin.math.sin * @param alpha Opacidad (0f..1f). * @param strokeWidth Grosor de la línea (Dp). * @param amplitude Amplitud de la onda (Dp) — la altura máxima desde el centro. - * @param waves Número de ondas completas a lo largo del ancho (ej: 1f = una onda). + * @param wavesDensity Density of wave (float) - as the number in standard screen width 380dp * @param phase Desplazamiento de fase estático (radianes). Se usa solo si animate = false. * @param animate Si es true, activa una animación de desplazamiento infinita. * @param animationDurationMillis Duración en milisegundos de un ciclo completo de animación. @@ -36,7 +36,7 @@ fun SineWaveLine( alpha: Float = 1f, strokeWidth: Dp = 2.dp, amplitude: Dp = 8.dp, - waves: Float = 2f, + wavesDensity: Float = 7.6f, phase: Float = 0f, animate: Boolean? = false, animationDurationMillis: Int = 2000, @@ -80,8 +80,8 @@ fun SineWaveLine( moveTo(0f, centerY + (ampPx * sin(currentPhase))) for (i in 1 until samples) { val x = i * step - // theta recorre 0..(2π * waves) - val theta = (x / w) * (2f * PI.toFloat() * waves) + currentPhase + // theta recorre 0..(2π * wavesDensity) + val theta = (x / w) * (2f * PI.toFloat() * (wavesDensity) * size.width / 380.dp.toPx()) + currentPhase val y = centerY + ampPx * sin(theta) lineTo(x, y) } diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/screens/SetupScreen.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/screens/SetupScreen.kt index 510010631..44f93c9b7 100644 --- a/app/src/main/java/com/theveloper/pixelplay/presentation/screens/SetupScreen.kt +++ b/app/src/main/java/com/theveloper/pixelplay/presentation/screens/SetupScreen.kt @@ -56,8 +56,10 @@ import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.navigationBars import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.requiredWidth import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.PagerState import androidx.compose.foundation.pager.rememberPagerState @@ -643,7 +645,7 @@ private fun isIgnoringBatteryOptimizationsNow(context: Context): Boolean { @Composable fun WelcomePage() { Column( - horizontalAlignment = Alignment.CenterHorizontally, + //horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.SpaceBetween, modifier = Modifier .fillMaxSize() @@ -672,7 +674,7 @@ fun WelcomePage() { ), ) } - Spacer(modifier = Modifier.height(10.dp)) + Spacer(modifier = Modifier.height(4.dp)) Surface( shape = CircleShape, color = MaterialTheme.colorScheme.surface, @@ -707,7 +709,7 @@ fun WelcomePage() { .clip(RoundedCornerShape(20.dp)) ){ MaterialYouVectorDrawable( - modifier = Modifier.fillMaxSize(), + modifier = Modifier.requiredWidth(380.dp).align(Alignment.CenterEnd), drawableResId = R.drawable.welcome_art ) SineWaveLine( @@ -722,7 +724,7 @@ fun WelcomePage() { alpha = 0.95f, strokeWidth = 16.dp, amplitude = 4.dp, - waves = 7.6f, + wavesDensity = 7.6f, phase = 0f ) Box( @@ -748,7 +750,7 @@ fun WelcomePage() { alpha = 0.95f, strokeWidth = 4.dp, amplitude = 4.dp, - waves = 7.6f, + wavesDensity = 7.6f, phase = 0f ) } @@ -1005,6 +1007,7 @@ fun ThemeSelectionPage( horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.SpaceBetween, modifier = Modifier + .widthIn(max = 540.dp) .fillMaxSize() .padding(24.dp) ) { @@ -1188,6 +1191,7 @@ fun LibraryLayoutPage( horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.SpaceBetween, modifier = Modifier + .widthIn(max = 540.dp) .fillMaxSize() .padding(24.dp) ) { @@ -1527,6 +1531,7 @@ fun FinishPage() { horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center, modifier = Modifier + .widthIn(max = 540.dp) .fillMaxSize() .padding(16.dp) ) { @@ -1556,6 +1561,7 @@ fun PermissionPageLayout( horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.SpaceBetween, modifier = Modifier + .widthIn(max = 540.dp) .fillMaxSize() .padding(24.dp) ) { @@ -1654,7 +1660,7 @@ private fun SetupRestoreDialog( ) ) { Surface( - modifier = Modifier.fillMaxSize(), + modifier = Modifier.fillMaxSize().widthIn(max = 540.dp), color = MaterialTheme.colorScheme.surfaceContainerLowest ) { Scaffold( @@ -1924,6 +1930,7 @@ fun LibraryNavigationPillSetupShow( // IntrinsicSize.Min en el Row + fillMaxHeight en los hijos asegura misma altura Row( modifier = Modifier + .widthIn(max = 540.dp) .padding(start = 4.dp) .height(IntrinsicSize.Min), verticalAlignment = Alignment.CenterVertically, @@ -2219,6 +2226,7 @@ fun NavBarLayoutPage( horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.SpaceBetween, modifier = Modifier + .widthIn(max = 540.dp) .fillMaxSize() .padding(24.dp) ) { @@ -2348,6 +2356,7 @@ fun NavBarPreview(isDefault: Boolean) { containerColor = MaterialTheme.colorScheme.surfaceBright ), modifier = Modifier + .widthIn(max = 540.dp) .fillMaxWidth() .height(200.dp) // Taller to show bottom part clearly .padding(horizontal = 8.dp) From 9e0d181f31ed7d1fa0b82bd39a9b5ad35cd90e39 Mon Sep 17 00:00:00 2001 From: ZL114514 <114651468+ZL114514@users.noreply.github.com> Date: Sun, 14 Jun 2026 21:24:18 +0800 Subject: [PATCH 20/99] fix: clean the unstashed conflict f --- .../pixelplay/presentation/components/PlaylistContainer.kt | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/components/PlaylistContainer.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/components/PlaylistContainer.kt index a90efbfa3..9fe225aa7 100644 --- a/app/src/main/java/com/theveloper/pixelplay/presentation/components/PlaylistContainer.kt +++ b/app/src/main/java/com/theveloper/pixelplay/presentation/components/PlaylistContainer.kt @@ -86,6 +86,7 @@ import com.theveloper.pixelplay.utils.formatSongCount import com.theveloper.pixelplay.ui.theme.GoogleSansRounded import com.theveloper.pixelplay.ui.theme.LocalShowScrollbar import androidx.compose.ui.text.style.TextAlign +import com.theveloper.pixelplay.presentation.components.subcomps.SineWaveLine import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.distinctUntilChanged @@ -135,7 +136,6 @@ fun PlaylistContainer( horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(14.dp) ) { -<<<<<<< Updated upstream Surface( shape = CircleShape, color = MaterialTheme.colorScheme.secondaryContainer.copy(alpha = 0.55f), @@ -172,7 +172,6 @@ fun PlaylistContainer( textAlign = TextAlign.Center ) } -======= SineWaveLine( modifier = Modifier .fillMaxWidth() @@ -203,7 +202,6 @@ fun PlaylistContainer( style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant ) ->>>>>>> Stashed changes } } } else { From 919a4f42e82aacc21b534cd7426ce7b230f835d0 Mon Sep 17 00:00:00 2001 From: ZL114514 <114651468+ZL114514@users.noreply.github.com> Date: Sun, 14 Jun 2026 21:33:04 +0800 Subject: [PATCH 21/99] Update PlaylistContainer.kt f again --- .../components/PlaylistContainer.kt | 31 ------------------- 1 file changed, 31 deletions(-) diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/components/PlaylistContainer.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/components/PlaylistContainer.kt index 9fe225aa7..ca39bb451 100644 --- a/app/src/main/java/com/theveloper/pixelplay/presentation/components/PlaylistContainer.kt +++ b/app/src/main/java/com/theveloper/pixelplay/presentation/components/PlaylistContainer.kt @@ -86,7 +86,6 @@ import com.theveloper.pixelplay.utils.formatSongCount import com.theveloper.pixelplay.ui.theme.GoogleSansRounded import com.theveloper.pixelplay.ui.theme.LocalShowScrollbar import androidx.compose.ui.text.style.TextAlign -import com.theveloper.pixelplay.presentation.components.subcomps.SineWaveLine import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.distinctUntilChanged @@ -172,36 +171,6 @@ fun PlaylistContainer( textAlign = TextAlign.Center ) } - SineWaveLine( - modifier = Modifier - .fillMaxWidth() - .height(32.dp) - .padding(horizontal = 8.dp), - color = MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.8f), - alpha = 0.95f, - strokeWidth = 3.dp, - amplitude = 4.dp, - wavesDensity = 7.6f, - phase = 0f - ) - Spacer(Modifier.height(16.dp)) - Icon( - Icons.AutoMirrored.Rounded.PlaylistPlay, - contentDescription = null, - modifier = Modifier.size(48.dp), - tint = MaterialTheme.colorScheme.onSurfaceVariant - ) - Spacer(Modifier.height(8.dp)) - Text( - stringResource(R.string.presentation_batch_e_no_playlist_created), - style = MaterialTheme.typography.titleMedium - ) - Spacer(Modifier.height(6.dp)) - Text( - stringResource(R.string.presentation_batch_e_new_playlist_hint), - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) } } } else { From 5e00b0bffa6698323181765bee5f4b89a11c3f5d Mon Sep 17 00:00:00 2001 From: ZL114514 <114651468+ZL114514@users.noreply.github.com> Date: Mon, 15 Jun 2026 14:52:02 +0800 Subject: [PATCH 22/99] feat: NavigationRail for tablet still working on the min width and the back animation --- .../com/theveloper/pixelplay/MainActivity.kt | 115 ++++++- .../components/PlayerInternalNavigationBar.kt | 306 ++++++++++++++++++ .../components/PlaylistContainer.kt | 2 + .../components/UnifiedPlayerSheetLayers.kt | 10 +- .../components/UnifiedPlayerSheetShared.kt | 2 + .../components/UnifiedPlayerSheetV2.kt | 8 +- 6 files changed, 422 insertions(+), 21 deletions(-) diff --git a/app/src/main/java/com/theveloper/pixelplay/MainActivity.kt b/app/src/main/java/com/theveloper/pixelplay/MainActivity.kt index 503ac7a21..ec33ffa8d 100644 --- a/app/src/main/java/com/theveloper/pixelplay/MainActivity.kt +++ b/app/src/main/java/com/theveloper/pixelplay/MainActivity.kt @@ -1,4 +1,4 @@ -package com.theveloper.pixelplay +package com.theveloper.pixelplay import com.theveloper.pixelplay.presentation.navigation.navigateSafely @@ -46,7 +46,9 @@ import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.asPaddingValues import androidx.compose.foundation.layout.fillMaxSize @@ -54,6 +56,8 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.navigationBars import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.calculateStartPadding +import androidx.compose.foundation.layout.calculateEndPadding import androidx.compose.material3.CircularWavyProgressIndicator import androidx.compose.material3.DrawerValue import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @@ -80,16 +84,20 @@ import androidx.compose.ui.draw.clipToBounds import androidx.compose.ui.graphics.RectangleShape import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.layout.onSizeChanged +import androidx.compose.ui.layout.layout import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.platform.LocalView import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.foundation.gestures.detectTapGestures +import androidx.compose.foundation.layout.widthIn import com.theveloper.pixelplay.presentation.viewmodel.PlayerSheetState import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.lerp import androidx.core.net.toUri @@ -122,6 +130,7 @@ import com.theveloper.pixelplay.presentation.components.DrawerDestination import com.theveloper.pixelplay.presentation.components.MiniPlayerBottomSpacer import com.theveloper.pixelplay.presentation.components.MiniPlayerHeight import com.theveloper.pixelplay.presentation.components.PlayerInternalNavigationBar +import com.theveloper.pixelplay.presentation.components.PlayerInternalNavigationRail import com.theveloper.pixelplay.presentation.components.PlayStoreAnnouncementDefaults import com.theveloper.pixelplay.presentation.components.PlayStoreAnnouncementDialog import com.theveloper.pixelplay.presentation.components.PlayStoreAnnouncementUiModel @@ -608,6 +617,11 @@ class MainActivity : ComponentActivity() { private fun MainUI(playerViewModel: PlayerViewModel, navController: NavHostController) { Trace.beginSection("MainActivity.MainUI") + val configuration = LocalConfiguration.current + val useNavigationRail = remember(configuration) { + configuration.screenWidthDp > 600 + } + val commonNavItems = remember { persistentListOf( BottomNavItem("Home", R.string.nav_bar_home, R.drawable.rounded_home_24, R.drawable.home_24_rounded_filled, Screen.Home), @@ -703,8 +717,14 @@ class MainActivity : ComponentActivity() { ) val bottomBarPadding = animatedBottomBarPadding val navBarHeight = resolveNavBarSurfaceHeight(navBarStyle, systemNavBarInset, navBarCompactMode) - val navBarOccupiedHeight by remember(systemNavBarInset, navBarCompactMode) { - derivedStateOf { resolveNavBarOccupiedHeight(systemNavBarInset, navBarCompactMode) } + val navBarOccupiedHeight by remember(systemNavBarInset, navBarCompactMode, useNavigationRail) { + derivedStateOf { + if (useNavigationRail) { + systemNavBarInset + } else { + resolveNavBarOccupiedHeight(systemNavBarInset, navBarCompactMode) + } + } } val navBarVisibilityProgressState = animateFloatAsState( targetValue = if (shouldHideNavigationBar) 0f else 1f, @@ -798,7 +818,7 @@ class MainActivity : ComponentActivity() { Scaffold( modifier = Modifier.fillMaxSize(), bottomBar = { - if (shouldRenderNavigationBar) { + if (shouldRenderNavigationBar && !useNavigationRail) { val currentSongId by remember { playerViewModel.stablePlayerState .map { it.currentSong?.id } @@ -839,6 +859,7 @@ class MainActivity : ComponentActivity() { Box( modifier = Modifier .fillMaxWidth() + .widthIn(max = 540.dp) .height(navBarOccupiedHeight) .clipToBounds() ) { @@ -850,6 +871,7 @@ class MainActivity : ComponentActivity() { modifier = Modifier .align(Alignment.BottomCenter) .fillMaxWidth() + .widthIn(max = 540.dp) .padding(bottom = bottomBarPadding) .onSizeChanged { componentHeightPx = it.height } .graphicsLayer { @@ -870,7 +892,7 @@ class MainActivity : ComponentActivity() { .padding(horizontal = horizontalPadding) .graphicsLayer { // Animated corner shape resolved in the draw phase: - // animating the radius re-clips this layer only — no + // animating the radius re-clips this layer only 閳?no // recomposition and no layout pass for the bar. val fraction = playerViewModel.playerContentExpansionFraction.value val safeFraction = fraction.coerceIn(0f, 1f) @@ -902,20 +924,56 @@ class MainActivity : ComponentActivity() { compactMode = navBarCompactMode, bottomBarPadding = bottomBarPadding, onSearchIconDoubleTap = onSearchIconDoubleTap, - modifier = Modifier.fillMaxSize() + modifier = Modifier.fillMaxSize().widthIn(max = 540.dp) ) } } } } ) { innerPadding -> - BoxWithConstraints(modifier = Modifier.fillMaxSize()) { - val density = LocalDensity.current - val containerHeight = this.maxHeight - val screenHeightPx = remember(containerHeight, density) { - with(density) { containerHeight.toPx() } + val appNavigationPadding = remember(innerPadding, useNavigationRail, shouldRenderNavigationBar) { + if (useNavigationRail && shouldRenderNavigationBar) { + androidx.compose.foundation.layout.PaddingValues( + start = innerPadding.calculateStartPadding(LayoutDirection.Ltr) + 80.dp, + top = innerPadding.calculateTopPadding(), + end = innerPadding.calculateEndPadding(LayoutDirection.Ltr), + bottom = innerPadding.calculateBottomPadding() + ) + } else { + innerPadding } + } + Row(modifier = Modifier.fillMaxSize()) { + if (useNavigationRail && shouldRenderNavigationBar) { + PlayerInternalNavigationRail( + navController = navController, + navItems = commonNavItems, + currentRoute = currentRoute, + onSearchIconDoubleTap = { playerViewModel.onSearchNavIconDoubleTapped() }, + onOpenSidebar = { scope.launch { drawerState.open() } }, + modifier = Modifier + .layout { measurable, constraints -> + val expansionHide = playerViewModel.playerContentExpansionFraction.value.coerceIn(0f, 1f) + val routeHide = (1f - navBarVisibilityProgressState.value).coerceIn(0f, 1f) + val hideFraction = maxOf(expansionHide, routeHide) + val placeable = measurable.measure(constraints) + val shrinkBy = (placeable.width * hideFraction).roundToInt() + layout(placeable.width - shrinkBy, placeable.height) { + placeable.placeRelative(0, 0) + } + } + .graphicsLayer { + // reading state here is fine: graphicsLayer runs per frame, not per layout + val expansionHide = playerViewModel.playerContentExpansionFraction.value.coerceIn(0f, 1f) + val routeHide = (1f - navBarVisibilityProgressState.value).coerceIn(0f, 1f) + val hideFraction = maxOf(expansionHide, routeHide) + // size.width is already reduced by the layout modifier above + translationX = -size.width * hideFraction + alpha = 1f - hideFraction + } + ) + } val showPlayerContentInitially by remember { playerViewModel.stablePlayerState .map { it.currentSong?.id != null } @@ -925,7 +983,30 @@ class MainActivity : ComponentActivity() { val shouldHideMiniPlayer by remember(currentRoute) { derivedStateOf { currentRoute in routesWithHiddenMiniPlayer } } - + val density = LocalDensity.current + val collapsedMaxWidthDp by remember { + derivedStateOf { + val expansionHide = playerViewModel.playerContentExpansionFraction.value.coerceIn(0f, 1f) + val routeHide = (1f - navBarVisibilityProgressState.value).coerceIn(0f, 1f) + val hideFraction = maxOf(expansionHide, routeHide) + lerp(450.dp, 2000.dp, hideFraction) + } + } + BoxWithConstraints( + modifier = Modifier + .fillMaxSize() + .then( + if (useNavigationRail) { + Modifier.widthIn(max = if (showPlayerContentInitially && !shouldHideMiniPlayer) collapsedMaxWidthDp else Dp.Infinity) + } else { + Modifier.widthIn(max = 540.dp) + } + ) + ) { + val containerHeight = this.maxHeight + val screenHeightPx = remember(containerHeight, density) { + with(density) { containerHeight.toPx() } + } val miniPlayerH = with(density) { MiniPlayerHeight.toPx() } val totalSheetHeightWhenContentCollapsedPx = if (showPlayerContentInitially && !shouldHideMiniPlayer) miniPlayerH else 0f @@ -967,7 +1048,7 @@ class MainActivity : ComponentActivity() { AppNavigation( playerViewModel = playerViewModel, navController = navController, - paddingValues = innerPadding, + paddingValues = appNavigationPadding, userPreferencesRepository = userPreferencesRepository, onSearchBarActiveChange = { isSearchBarActive = it }, onOpenSidebar = { scope.launch { drawerState.open() } } @@ -979,7 +1060,7 @@ class MainActivity : ComponentActivity() { playerViewModel.playerContentExpansionFraction.value > 0.01f } } - AnimatedVisibility( + androidx.compose.animation.AnimatedVisibility( visible = isExpandedOrExpanding, enter = fadeIn(animationSpec = tween(durationMillis = 350)), exit = fadeOut(animationSpec = tween(durationMillis = 350)), @@ -1008,7 +1089,8 @@ class MainActivity : ComponentActivity() { hideMiniPlayer = shouldHideMiniPlayer, containerHeight = containerHeight, navController = navController, - isNavBarHidden = isNavBarEffectivelyHidden + isNavBarHidden = isNavBarEffectivelyHidden, + isNavRailHidden = useNavigationRail ) val dismissUndoBarSlice by remember { @@ -1028,7 +1110,7 @@ class MainActivity : ComponentActivity() { { playerViewModel.hideDismissUndoBar() } } - AnimatedVisibility( + androidx.compose.animation.AnimatedVisibility( visible = dismissUndoBarSlice.isVisible, enter = slideInVertically(initialOffsetY = { it }) + fadeIn(), exit = slideOutVertically(targetOffsetY = { -it }) + fadeOut(), @@ -1061,6 +1143,7 @@ class MainActivity : ComponentActivity() { } } } + } } Trace.endSection() diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/components/PlayerInternalNavigationBar.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/components/PlayerInternalNavigationBar.kt index 651c5f26e..86b1a3dbe 100644 --- a/app/src/main/java/com/theveloper/pixelplay/presentation/components/PlayerInternalNavigationBar.kt +++ b/app/src/main/java/com/theveloper/pixelplay/presentation/components/PlayerInternalNavigationBar.kt @@ -3,17 +3,46 @@ package com.theveloper.pixelplay.presentation.components import com.theveloper.pixelplay.presentation.navigation.navigateToTopLevelSafely import android.os.SystemClock +import androidx.compose.animation.animateColorAsState +import androidx.compose.animation.core.CubicBezierEasing +import androidx.compose.animation.core.Spring +import androidx.compose.animation.core.spring +import androidx.compose.animation.core.tween +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.scaleIn +import androidx.compose.animation.scaleOut +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.asPaddingValues +import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.navigationBars import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.LocalContentColor import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.NavigationBarDefaults +import androidx.compose.material3.ProvideTextStyle +import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -22,12 +51,21 @@ import androidx.compose.runtime.rememberUpdatedState import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.graphicsLayer 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.semantics +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import androidx.navigation.NavHostController import com.theveloper.pixelplay.BottomNavItem +import com.theveloper.pixelplay.R import com.theveloper.pixelplay.data.preferences.NavBarStyle import com.theveloper.pixelplay.presentation.components.scoped.CustomNavigationBarItem import com.theveloper.pixelplay.presentation.navigation.Screen @@ -245,3 +283,271 @@ fun PlayerInternalNavigationBar( modifier = modifier ) } + +@Composable +fun ColumnScope.CustomNavigationRailItem( + selected: Boolean, + onClick: () -> Unit, + icon: @Composable () -> Unit, + selectedIcon: @Composable () -> Unit, + modifier: Modifier = Modifier, + enabled: Boolean = true, + label: @Composable (() -> Unit)? = null, + contentDescription: String? = null, + alwaysShowLabel: Boolean = true, + selectedIconColor: androidx.compose.ui.graphics.Color = MaterialTheme.colorScheme.onSecondaryContainer, + unselectedIconColor: androidx.compose.ui.graphics.Color = MaterialTheme.colorScheme.onSurfaceVariant, + selectedTextColor: androidx.compose.ui.graphics.Color = MaterialTheme.colorScheme.onSurface, + unselectedTextColor: androidx.compose.ui.graphics.Color = MaterialTheme.colorScheme.onSurfaceVariant, + indicatorColor: androidx.compose.ui.graphics.Color = MaterialTheme.colorScheme.secondaryContainer, + interactionSource: MutableInteractionSource = remember { MutableInteractionSource() } +) { + val iconColor by animateColorAsState( + targetValue = if (selected) selectedIconColor else unselectedIconColor, + animationSpec = tween(durationMillis = 150), + label = "iconColor" + ) + + val textColor by animateColorAsState( + targetValue = if (selected) selectedTextColor else unselectedTextColor, + animationSpec = tween(durationMillis = 150), + label = "textColor" + ) + + val iconScale by animateFloatAsState( + targetValue = if (selected) 1.1f else 1f, + animationSpec = spring( + dampingRatio = Spring.DampingRatioMediumBouncy, + stiffness = Spring.StiffnessMedium + ), + label = "iconScale" + ) + + val showLabel = label != null && (alwaysShowLabel || selected) + val indicatorWidth = 64.dp + val indicatorHeight = 32.dp + val iconWidth = 48.dp + val iconHeight = 24.dp + val indicatorPadding = 4.dp + val indicatorShape = RoundedCornerShape(16.dp) + val iconShape = RoundedCornerShape(12.dp) + + Column( + modifier = modifier + .fillMaxWidth() + .padding(vertical = 12.dp) + .clickable( + onClick = onClick, + enabled = enabled, + role = Role.Tab, + interactionSource = interactionSource, + indication = null + ) + .semantics { + if (contentDescription != null) { + this.contentDescription = contentDescription + } + }, + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + Box( + contentAlignment = Alignment.Center, + modifier = Modifier.size(indicatorWidth, indicatorHeight) + ) { + androidx.compose.animation.AnimatedVisibility( + visible = selected, + enter = fadeIn(animationSpec = tween(100)) + scaleIn(animationSpec = spring(dampingRatio = Spring.DampingRatioMediumBouncy, stiffness = Spring.StiffnessLow)), + exit = fadeOut(animationSpec = tween(100)) + scaleOut(animationSpec = tween(100, easing = CubicBezierEasing(0.5f, 0f, 0.75f, 0f))) + ) { + Box( + modifier = Modifier + .fillMaxSize() + .padding(horizontal = indicatorPadding) + .background( + color = indicatorColor, + shape = indicatorShape + ) + ) + } + + Box( + contentAlignment = Alignment.Center, + modifier = Modifier + .size(iconWidth, iconHeight) + .clip(iconShape) + .graphicsLayer { + scaleX = iconScale + scaleY = iconScale + } + ) { + CompositionLocalProvider(LocalContentColor provides iconColor) { + Box( + modifier = Modifier.clearAndSetSemantics { + if (showLabel) { } + } + ) { + if (selected) selectedIcon() else icon() + } + } + } + } + + androidx.compose.animation.AnimatedVisibility( + visible = showLabel, + enter = fadeIn(animationSpec = tween(200, delayMillis = 50)), + exit = fadeOut(animationSpec = tween(100)) + ) { + Spacer(modifier = Modifier.height(4.dp)) + Box( + modifier = Modifier.padding(top = 4.dp) + ) { + ProvideTextStyle( + value = MaterialTheme.typography.labelMedium.copy( + color = textColor, + fontSize = 13.sp, + fontWeight = if (selected) FontWeight.Medium else FontWeight.Normal + ) + ) { + label?.invoke() + } + } + } + } +} + +@Composable +fun PlayerInternalNavigationRail( + navController: NavHostController, + navItems: ImmutableList, + currentRoute: String?, + modifier: Modifier = Modifier, + onSearchIconDoubleTap: () -> Unit = {}, + onOpenSidebar: () -> Unit = {} +) { + val latestCurrentRoute by rememberUpdatedState(currentRoute) + val latestOnSearchIconDoubleTap by rememberUpdatedState(onSearchIconDoubleTap) + val latestNavigationEnabled by rememberUpdatedState(currentRoute != null) + + Surface( + modifier = modifier + .fillMaxHeight() + .width(80.dp), + color = NavigationBarDefaults.containerColor, + tonalElevation = 3.dp + ) { + Column( + modifier = Modifier + .fillMaxHeight() + .padding(vertical = 24.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + IconButton( + onClick = onOpenSidebar, + modifier = Modifier.padding(bottom = 16.dp) + ) { + Icon( + painter = painterResource(id = R.drawable.rounded_menu_24), + contentDescription = "Open Drawer", + tint = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + + val scope = rememberCoroutineScope() + var lastSearchTapTimestamp by remember { mutableStateOf(0L) } + + navItems.forEach { item -> + val isSelected = currentRoute != null && currentRoute == item.screen.route + val selectedColor = MaterialTheme.colorScheme.primary + val unselectedColor = MaterialTheme.colorScheme.onSurfaceVariant + val indicatorColorFromTheme = MaterialTheme.colorScheme.secondaryContainer + + val iconPainterResId = if (isSelected && item.selectedIconResId != null && item.selectedIconResId != 0) { + item.selectedIconResId + } else { + item.iconResId + } + val localizedLabel = stringResource(id = item.labelResId) + val iconLambda: @Composable () -> Unit = remember(iconPainterResId, localizedLabel) { + { + Icon( + painter = painterResource(id = iconPainterResId), + contentDescription = localizedLabel + ) + } + } + val selectedIconLambda: @Composable () -> Unit = remember(iconPainterResId, localizedLabel) { + { + Icon( + painter = painterResource(id = iconPainterResId), + contentDescription = localizedLabel + ) + } + } + val labelLambda: @Composable () -> Unit = remember(localizedLabel) { + { Text(localizedLabel) } + } + + val onClickLambda: () -> Unit = remember(item.screen.route, navController, scope) { + click@{ + if (!latestNavigationEnabled) { + lastSearchTapTimestamp = 0L + return@click + } + + val itemRoute = item.screen.route + val isSearchTab = itemRoute == Screen.Search.route + val isAlreadySelected = latestCurrentRoute == itemRoute + + if (isSearchTab) { + val now = SystemClock.elapsedRealtime() + val isDoubleTap = now - lastSearchTapTimestamp <= 350L + lastSearchTapTimestamp = now + + if (!isAlreadySelected) { + if (!navController.navigateToTopLevelSafely(itemRoute)) { + lastSearchTapTimestamp = 0L + return@click + } + } + + if (isDoubleTap) { + lastSearchTapTimestamp = 0L + if (isAlreadySelected) { + latestOnSearchIconDoubleTap() + } else { + scope.launch { + delay(160L) + latestOnSearchIconDoubleTap() + } + } + } + } else if (!isAlreadySelected) { + lastSearchTapTimestamp = 0L + navController.navigateToTopLevelSafely(itemRoute) + } else { + lastSearchTapTimestamp = 0L + } + } + } + + CustomNavigationRailItem( + selected = isSelected, + onClick = onClickLambda, + enabled = currentRoute != null, + icon = iconLambda, + selectedIcon = selectedIconLambda, + label = labelLambda, + contentDescription = localizedLabel, + alwaysShowLabel = true, + selectedIconColor = selectedColor, + unselectedIconColor = unselectedColor, + selectedTextColor = selectedColor, + unselectedTextColor = unselectedColor, + indicatorColor = indicatorColorFromTheme + ) + } + } + } +} diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/components/PlaylistContainer.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/components/PlaylistContainer.kt index ca39bb451..cff5d286c 100644 --- a/app/src/main/java/com/theveloper/pixelplay/presentation/components/PlaylistContainer.kt +++ b/app/src/main/java/com/theveloper/pixelplay/presentation/components/PlaylistContainer.kt @@ -23,6 +23,7 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.rememberLazyListState @@ -577,6 +578,7 @@ fun CreatePlaylistDialogRedesigned( ) { Column( modifier = Modifier + .widthIn(max = 540.dp) .padding(24.dp) .fillMaxWidth() ) { diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/components/UnifiedPlayerSheetLayers.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/components/UnifiedPlayerSheetLayers.kt index 2aed35222..5f6915fc4 100644 --- a/app/src/main/java/com/theveloper/pixelplay/presentation/components/UnifiedPlayerSheetLayers.kt +++ b/app/src/main/java/com/theveloper/pixelplay/presentation/components/UnifiedPlayerSheetLayers.kt @@ -9,6 +9,7 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.requiredHeight import androidx.compose.material3.ColorScheme import androidx.compose.runtime.Composable @@ -69,7 +70,8 @@ internal fun BoxScope.UnifiedPlayerMiniAndFullLayers( onQueueDragStart: () -> Unit, onQueueDrag: (Float) -> Unit, onQueueRelease: (Float, Float) -> Unit, - onShowCastClicked: () -> Unit + onShowCastClicked: () -> Unit, + isNavRailHidden: Boolean ) { currentSong?.let { currentSongNonNull -> miniPlayerScheme?.let { readyScheme -> @@ -116,7 +118,7 @@ internal fun BoxScope.UnifiedPlayerMiniAndFullLayers( .zIndex(miniPlayerZIndex) ) { val isMiniPlayerVisible by remember { - derivedStateOf { playerContentExpansionFraction.value < 0.01f } + derivedStateOf { playerContentExpansionFraction.value < 0.000001f } //0.01f is really huge for it } MiniPlayerContentInternal( song = currentSongNonNull, @@ -127,7 +129,9 @@ internal fun BoxScope.UnifiedPlayerMiniAndFullLayers( onPrevious = { playerViewModel.previousSong() }, onNext = { playerViewModel.nextSong() }, canScroll = isMiniPlayerVisible && infrequentPlayerState.isPlaying, - modifier = Modifier.fillMaxSize() + modifier = Modifier.fillMaxSize().then( + if (isNavRailHidden && !isMiniPlayerVisible) Modifier.padding(end = 80.dp) else Modifier + ) ) } } diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/components/UnifiedPlayerSheetShared.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/components/UnifiedPlayerSheetShared.kt index 59ae7c316..71f733653 100644 --- a/app/src/main/java/com/theveloper/pixelplay/presentation/components/UnifiedPlayerSheetShared.kt +++ b/app/src/main/java/com/theveloper/pixelplay/presentation/components/UnifiedPlayerSheetShared.kt @@ -18,6 +18,7 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.safeDrawing import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.rounded.Pause @@ -83,6 +84,7 @@ internal fun MiniPlayerContentInternal( Row( modifier = modifier .fillMaxWidth() + .widthIn(max = 450.dp) .height(MiniPlayerHeight) .padding(start = 10.dp, end = 12.dp), verticalAlignment = Alignment.CenterVertically diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/components/UnifiedPlayerSheetV2.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/components/UnifiedPlayerSheetV2.kt index d692ed9fd..e0494ef6c 100644 --- a/app/src/main/java/com/theveloper/pixelplay/presentation/components/UnifiedPlayerSheetV2.kt +++ b/app/src/main/java/com/theveloper/pixelplay/presentation/components/UnifiedPlayerSheetV2.kt @@ -22,6 +22,7 @@ import androidx.compose.foundation.layout.height import androidx.compose.ui.layout.layout import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.widthIn import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.material3.MotionScheme @@ -115,7 +116,8 @@ fun UnifiedPlayerSheetV2( collapsedStateHorizontalPadding: Dp = 12.dp, navController: NavHostController, hideMiniPlayer: Boolean = false, - isNavBarHidden: Boolean = false + isNavBarHidden: Boolean = false, + isNavRailHidden: Boolean = false ) { val context = LocalContext.current val lifecycleOwner = LocalLifecycleOwner.current @@ -620,6 +622,7 @@ fun UnifiedPlayerSheetV2( Surface( modifier = Modifier .fillMaxWidth() + .widthIn(max = 450.dp) .layout { measurable, constraints -> val translationY = visualSheetTranslationYProvider().roundToInt() val overshoot = if (currentSheetContentState == PlayerSheetState.EXPANDED && !isDragging) { @@ -772,7 +775,8 @@ fun UnifiedPlayerSheetV2( onQueueDragStart = sheetActionHandlers.beginQueueDrag, onQueueDrag = sheetActionHandlers.dragQueueBy, onQueueRelease = sheetActionHandlers.endQueueDrag, - onShowCastClicked = castSheetState.openCastSheet + onShowCastClicked = castSheetState.openCastSheet, + isNavRailHidden = isNavRailHidden, ) } } From e26ee58c06abd061167accd8101e9c36c9da9cc8 Mon Sep 17 00:00:00 2001 From: ZL114514 <114651468+ZL114514@users.noreply.github.com> Date: Mon, 15 Jun 2026 16:50:37 +0800 Subject: [PATCH 23/99] feat: predictive back animation handler --- .../components/UnifiedPlayerSheetV2.kt | 61 ++++++++++++++++++- 1 file changed, 60 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/components/UnifiedPlayerSheetV2.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/components/UnifiedPlayerSheetV2.kt index e0494ef6c..9e3a735e7 100644 --- a/app/src/main/java/com/theveloper/pixelplay/presentation/components/UnifiedPlayerSheetV2.kt +++ b/app/src/main/java/com/theveloper/pixelplay/presentation/components/UnifiedPlayerSheetV2.kt @@ -1,4 +1,4 @@ -package com.theveloper.pixelplay.presentation.components +package com.theveloper.pixelplay.presentation.components import android.widget.Toast import com.theveloper.pixelplay.presentation.components.ExpressiveOfflineDialog @@ -501,6 +501,8 @@ fun UnifiedPlayerSheetV2( val queuePredictiveBackProgress = remember { Animatable(0f) } var queuePredictiveBackSwipeEdge by remember { mutableStateOf(null) } + val playerContentPredictiveBackProgress = remember { Animatable(0f) } + var playerContentPredictiveBackSwipeEdge by remember { mutableStateOf(null) } val sheetOverlayState = rememberSheetOverlayState( density = density, @@ -835,6 +837,63 @@ fun UnifiedPlayerSheetV2( sheetActionHandlers.animateQueueSheet(false) } } + val playerContentExpansionPredictiveBackEnabled by remember( + showPlayerContentArea, + currentSheetContentState, + isQueueVisible, + castSheetState.showCastSheet, + internalIsKeyboardVisible, + playerContentExpansionFraction + ) { + derivedStateOf { + showPlayerContentArea && + currentSheetContentState == PlayerSheetState.EXPANDED && + !isQueueVisible && + !castSheetState.showCastSheet && + !internalIsKeyboardVisible && + playerContentExpansionFraction.value > 0.00001f + } + } + + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { + PredictiveBackHandler(enabled = playerContentExpansionPredictiveBackEnabled) { progressFlow -> + try { + val startingExpansionFraction = playerContentExpansionFraction.value + progressFlow.collect { backEvent -> + playerContentPredictiveBackSwipeEdge = backEvent.swipeEdge + playerContentPredictiveBackProgress.snapTo(backEvent.progress) + val contractedFraction = ((1f - backEvent.progress) * startingExpansionFraction).coerceIn(0f, 1f) + playerViewModel.playerContentExpansionFraction.snapTo(contractedFraction) + } + scope.launch { + launch { + playerViewModel.collapsePlayerSheet() + } + launch { + playerContentPredictiveBackProgress.animateTo( + targetValue = 0f, + animationSpec = tween(ANIMATION_DURATION_MS) + ) + playerContentPredictiveBackSwipeEdge = null + playerViewModel.playerContentExpansionFraction.snapTo(0f) + } + } + } catch (_: kotlin.coroutines.cancellation.CancellationException) { + scope.launch { + playerContentPredictiveBackProgress.animateTo( + targetValue = 0f, + animationSpec = tween(ANIMATION_DURATION_MS) + ) + playerContentPredictiveBackSwipeEdge = null + playerViewModel.playerContentExpansionFraction.animateTo( + targetValue = 1f, + animationSpec = tween(ANIMATION_DURATION_MS) + ) + } + } + } + } + val queuePredictiveBackSwipeEdgeState = rememberUpdatedState(queuePredictiveBackSwipeEdge) From 13fab2ac72c89ab59120fbf6f95101a9abca1196 Mon Sep 17 00:00:00 2001 From: ZL114514 <114651468+ZL114514@users.noreply.github.com> Date: Mon, 15 Jun 2026 19:44:37 +0800 Subject: [PATCH 24/99] opt: add padding animation back handler --- .../components/UnifiedPlayerSheetV2.kt | 59 +------------------ .../PlayerSheetPredictiveBackHandler.kt | 11 +++- 2 files changed, 11 insertions(+), 59 deletions(-) diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/components/UnifiedPlayerSheetV2.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/components/UnifiedPlayerSheetV2.kt index 9e3a735e7..21718a40d 100644 --- a/app/src/main/java/com/theveloper/pixelplay/presentation/components/UnifiedPlayerSheetV2.kt +++ b/app/src/main/java/com/theveloper/pixelplay/presentation/components/UnifiedPlayerSheetV2.kt @@ -501,8 +501,6 @@ fun UnifiedPlayerSheetV2( val queuePredictiveBackProgress = remember { Animatable(0f) } var queuePredictiveBackSwipeEdge by remember { mutableStateOf(null) } - val playerContentPredictiveBackProgress = remember { Animatable(0f) } - var playerContentPredictiveBackSwipeEdge by remember { mutableStateOf(null) } val sheetOverlayState = rememberSheetOverlayState( density = density, @@ -821,6 +819,7 @@ fun UnifiedPlayerSheetV2( ) queuePredictiveBackSwipeEdge = null } + } } catch (_: kotlin.coroutines.cancellation.CancellationException) { scope.launch { @@ -837,62 +836,6 @@ fun UnifiedPlayerSheetV2( sheetActionHandlers.animateQueueSheet(false) } } - val playerContentExpansionPredictiveBackEnabled by remember( - showPlayerContentArea, - currentSheetContentState, - isQueueVisible, - castSheetState.showCastSheet, - internalIsKeyboardVisible, - playerContentExpansionFraction - ) { - derivedStateOf { - showPlayerContentArea && - currentSheetContentState == PlayerSheetState.EXPANDED && - !isQueueVisible && - !castSheetState.showCastSheet && - !internalIsKeyboardVisible && - playerContentExpansionFraction.value > 0.00001f - } - } - - if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { - PredictiveBackHandler(enabled = playerContentExpansionPredictiveBackEnabled) { progressFlow -> - try { - val startingExpansionFraction = playerContentExpansionFraction.value - progressFlow.collect { backEvent -> - playerContentPredictiveBackSwipeEdge = backEvent.swipeEdge - playerContentPredictiveBackProgress.snapTo(backEvent.progress) - val contractedFraction = ((1f - backEvent.progress) * startingExpansionFraction).coerceIn(0f, 1f) - playerViewModel.playerContentExpansionFraction.snapTo(contractedFraction) - } - scope.launch { - launch { - playerViewModel.collapsePlayerSheet() - } - launch { - playerContentPredictiveBackProgress.animateTo( - targetValue = 0f, - animationSpec = tween(ANIMATION_DURATION_MS) - ) - playerContentPredictiveBackSwipeEdge = null - playerViewModel.playerContentExpansionFraction.snapTo(0f) - } - } - } catch (_: kotlin.coroutines.cancellation.CancellationException) { - scope.launch { - playerContentPredictiveBackProgress.animateTo( - targetValue = 0f, - animationSpec = tween(ANIMATION_DURATION_MS) - ) - playerContentPredictiveBackSwipeEdge = null - playerViewModel.playerContentExpansionFraction.animateTo( - targetValue = 1f, - animationSpec = tween(ANIMATION_DURATION_MS) - ) - } - } - } - } val queuePredictiveBackSwipeEdgeState = rememberUpdatedState(queuePredictiveBackSwipeEdge) diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/components/scoped/PlayerSheetPredictiveBackHandler.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/components/scoped/PlayerSheetPredictiveBackHandler.kt index fd533e570..f763b7174 100644 --- a/app/src/main/java/com/theveloper/pixelplay/presentation/components/scoped/PlayerSheetPredictiveBackHandler.kt +++ b/app/src/main/java/com/theveloper/pixelplay/presentation/components/scoped/PlayerSheetPredictiveBackHandler.kt @@ -1,4 +1,4 @@ -package com.theveloper.pixelplay.presentation.components.scoped +package com.theveloper.pixelplay.presentation.components.scoped import android.os.Build import androidx.activity.compose.BackHandler @@ -34,9 +34,12 @@ internal fun PlayerSheetPredictiveBackHandler( if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { PredictiveBackHandler(enabled = enabled) { progressFlow -> try { + val startingExpansionFraction = playerViewModel.playerContentExpansionFraction.value progressFlow.collect { backEvent -> onSwipeEdgeChanged(backEvent.swipeEdge) playerViewModel.updatePredictiveBackCollapseFraction(backEvent.progress) + val contractedFraction = ((1f - backEvent.progress) * startingExpansionFraction).coerceIn(0f, 1f) + playerViewModel.playerContentExpansionFraction.snapTo(contractedFraction) } scope.launch { val progressAtRelease = playerViewModel.predictiveBackCollapseFraction.value @@ -48,6 +51,7 @@ internal fun PlayerSheetPredictiveBackHandler( ) playerViewModel.updatePredictiveBackCollapseFraction(1f) playerViewModel.collapsePlayerSheet() + playerViewModel.playerContentExpansionFraction.snapTo(0f) playerViewModel.updatePredictiveBackCollapseFraction(0f) onSwipeEdgeChanged(null) } @@ -62,8 +66,13 @@ internal fun PlayerSheetPredictiveBackHandler( if (playerViewModel.sheetState.value == PlayerSheetState.EXPANDED) { playerViewModel.expandPlayerSheet() + playerViewModel.playerContentExpansionFraction.animateTo( + targetValue = 1f, + animationSpec = tween(animationDurationMs) + ) } else { playerViewModel.collapsePlayerSheet() + playerViewModel.playerContentExpansionFraction.snapTo(0f) } onSwipeEdgeChanged(null) From 402e45770039555e1f6898311c5832225f68c852 Mon Sep 17 00:00:00 2001 From: VoidX3D Date: Mon, 15 Jun 2026 18:59:43 +0545 Subject: [PATCH 25/99] 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 26/99] 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 27/99] 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 28/99] 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 29/99] 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 30/99] 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 31/99] 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 32/99] 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 33/99] 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 34/99] 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 35/99] 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 36/99] 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 37/99] 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 38/99] 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 39/99] 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 40/99] 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 41/99] 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 42/99] 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 43/99] 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 44/99] 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 45/99] 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 46/99] 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 47/99] 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 48/99] 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 49/99] 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 50/99] 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 51/99] 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 52/99] 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 53/99] 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 54/99] 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 c5e92a8aac43b70ffec0414cf5df1bdb14c469cf Mon Sep 17 00:00:00 2001 From: Ayaan Date: Tue, 16 Jun 2026 03:33:28 +0530 Subject: [PATCH 55/99] =?UTF-8?q?feat(removing=20library=20tabs):=20implem?= =?UTF-8?q?ent=20customizable=20tab=20management=20(add/remove)=20?= =?UTF-8?q?=E2=80=A2=20Added=20add/remove=20buttons=20to=20allow=20users?= =?UTF-8?q?=20to=20add,=20remove,=20the=20tabs=20from=20the=20library=20sc?= =?UTF-8?q?reen.=20=E2=80=A2=20Implemented=20persistent=20storage=20for=20?= =?UTF-8?q?library=20tab=20configurations=20in=20UserPreferencesRepository?= =?UTF-8?q?.=20=E2=80=A2=20Updated=20LibraryViewModel/PlayerViewModel=20to?= =?UTF-8?q?=20handle=20saving=20and=20resetting=20tab=20orders.=20?= =?UTF-8?q?=E2=80=A2=20Modified=20LibraryScreen=20to=20dynamically=20rende?= =?UTF-8?q?r=20the=20navigation=20pager=20based=20on=20the=20user's=20tab?= =?UTF-8?q?=20preferences.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../pixelplay/data/model/LibraryTabId.kt | 1 + .../preferences/UserPreferencesRepository.kt | 13 +- .../components/ReorderTabsSheet.kt | 180 +++++++++++++++--- .../presentation/screens/LibraryScreen.kt | 7 +- .../presentation/viewmodel/PlayerViewModel.kt | 55 ++++-- .../main/res/values-es/strings_library.xml | 5 +- app/src/main/res/values/strings_library.xml | 5 +- 7 files changed, 222 insertions(+), 44 deletions(-) diff --git a/app/src/main/java/com/theveloper/pixelplay/data/model/LibraryTabId.kt b/app/src/main/java/com/theveloper/pixelplay/data/model/LibraryTabId.kt index 76efcc3ae..16b96ee56 100644 --- a/app/src/main/java/com/theveloper/pixelplay/data/model/LibraryTabId.kt +++ b/app/src/main/java/com/theveloper/pixelplay/data/model/LibraryTabId.kt @@ -19,6 +19,7 @@ enum class LibraryTabId( LIKED("LIKED", "LIKED", R.string.library_tab_liked, SortOption.LikedSongDateLiked); companion object { + val defaultOrder: List = entries.toList() fun fromStorageKey(key: String): LibraryTabId = entries.firstOrNull { it.storageKey == key } ?: SONGS } diff --git a/app/src/main/java/com/theveloper/pixelplay/data/preferences/UserPreferencesRepository.kt b/app/src/main/java/com/theveloper/pixelplay/data/preferences/UserPreferencesRepository.kt index 9efca552a..0a796a8ba 100644 --- a/app/src/main/java/com/theveloper/pixelplay/data/preferences/UserPreferencesRepository.kt +++ b/app/src/main/java/com/theveloper/pixelplay/data/preferences/UserPreferencesRepository.kt @@ -129,6 +129,7 @@ class UserPreferencesRepository @Inject constructor( // Transition val GLOBAL_TRANSITION_SETTINGS = stringPreferencesKey("global_transition_settings_json") val LIBRARY_TABS_ORDER = stringPreferencesKey("library_tabs_order") + val LIBRARY_HIDDEN_TABS = stringSetPreferencesKey("library_hidden_tabs") val IS_FOLDER_FILTER_ACTIVE = booleanPreferencesKey("is_folder_filter_active") val IS_FOLDERS_PLAYLIST_VIEW = booleanPreferencesKey("is_folders_playlist_view") val SHOW_TELEGRAM_CLOUD_PLAYLISTS = booleanPreferencesKey("show_telegram_cloud_playlists") @@ -873,8 +874,18 @@ suspend fun markDirectoryRulesVersionApplied(version: Int) { dataStore.edit { it[PreferencesKeys.LIBRARY_TABS_ORDER] = order } } + val libraryHiddenTabsFlow: Flow> = + pref { it[PreferencesKeys.LIBRARY_HIDDEN_TABS] ?: emptySet() } + + suspend fun setLibraryHiddenTabs(hiddenTabs: Set) { + dataStore.edit { it[PreferencesKeys.LIBRARY_HIDDEN_TABS] = hiddenTabs } + } + suspend fun resetLibraryTabsOrder() { - dataStore.edit { it.remove(PreferencesKeys.LIBRARY_TABS_ORDER) } + dataStore.edit { + it.remove(PreferencesKeys.LIBRARY_TABS_ORDER) + it.remove(PreferencesKeys.LIBRARY_HIDDEN_TABS) + } } suspend fun migrateTabOrder() { diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/components/ReorderTabsSheet.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/components/ReorderTabsSheet.kt index 641d2f567..91bb960fb 100644 --- a/app/src/main/java/com/theveloper/pixelplay/presentation/components/ReorderTabsSheet.kt +++ b/app/src/main/java/com/theveloper/pixelplay/presentation/components/ReorderTabsSheet.kt @@ -13,13 +13,16 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.Add import androidx.compose.material.icons.rounded.Check +import androidx.compose.material.icons.rounded.Clear import androidx.compose.material.icons.rounded.DragIndicator import androidx.compose.material3.AlertDialog import androidx.compose.material3.ContainedLoadingIndicator @@ -53,7 +56,7 @@ import androidx.compose.ui.unit.dp import androidx.core.view.ViewCompat import android.view.HapticFeedbackConstants import com.theveloper.pixelplay.R -import com.theveloper.pixelplay.presentation.library.LibraryTabId +import com.theveloper.pixelplay.data.model.LibraryTabId import com.theveloper.pixelplay.presentation.utils.LocalAppHapticsConfig import com.theveloper.pixelplay.presentation.utils.performAppCompatHapticFeedback import com.theveloper.pixelplay.ui.theme.GoogleSansRounded @@ -63,21 +66,25 @@ import racra.compose.smooth_corner_rect_library.AbsoluteSmoothCornerShape import sh.calvin.reorderable.ReorderableItem import sh.calvin.reorderable.rememberReorderableLazyListState import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow @OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class) @Composable fun ReorderTabsSheet( - tabs: List, - onReorder: (List) -> Unit, + visibleTabs: List, + hiddenTabs: List, + onSave: (visible: List, hidden: Set) -> Unit, onReset: () -> Unit, onDismiss: () -> Unit ) { var showResetDialog by remember { mutableStateOf(false) } - var localTabs by remember { mutableStateOf(tabs) } + var localVisibleTabs by remember { mutableStateOf(visibleTabs) } + var localHiddenTabs by remember { mutableStateOf(hiddenTabs) } - LaunchedEffect(tabs) { - localTabs = tabs + LaunchedEffect(visibleTabs, hiddenTabs) { + localVisibleTabs = visibleTabs + localHiddenTabs = hiddenTabs } if (showResetDialog) { @@ -89,7 +96,7 @@ fun ReorderTabsSheet( TextButton( onClick = { onReset() - localTabs = tabs + // Local state will be updated by the LaunchedEffect when visibleTabs/hiddenTabs change via VM showResetDialog = false } ) { @@ -114,15 +121,30 @@ fun ReorderTabsSheet( val reorderableState = rememberReorderableLazyListState( onMove = { from, to -> - localTabs = localTabs.toMutableList().apply { - add(to.index, removeAt(from.index)) + val fromKey = from.key as? String ?: return@rememberReorderableLazyListState + val toKey = to.key as? String ?: return@rememberReorderableLazyListState + + // Only move if both items are in the visible section + if (fromKey.startsWith("v_") && toKey.startsWith("v_")) { + val fromId = fromKey.removePrefix("v_") + val toId = toKey.removePrefix("v_") + + val fromIdx = localVisibleTabs.indexOf(fromId) + val toIdx = localVisibleTabs.indexOf(toId) + + if (fromIdx != -1 && toIdx != -1) { + localVisibleTabs = localVisibleTabs.toMutableList().apply { + add(toIdx, removeAt(fromIdx)) + } + // Haptic feedback on reorder + performAppCompatHapticFeedback( + view, + appHapticsConfig, + HapticFeedbackConstants.CLOCK_TICK, + HapticFeedbackConstants.FLAG_IGNORE_GLOBAL_SETTING + ) + } } - // Haptic feedback on reorder - performAppCompatHapticFeedback( - view, - appHapticsConfig, - HapticFeedbackConstants.FLAG_IGNORE_GLOBAL_SETTING - ) }, lazyListState = listState ) @@ -152,13 +174,13 @@ fun ReorderTabsSheet( floatingActionButton = { FloatingToolBar( modifier = Modifier, - onReset = { showResetDialog = true }, // This will now trigger the dialog + onReset = { showResetDialog = true }, onDismiss = onDismiss, onClick = { scope.launch { isLoading = true - delay(700) // Simulate network/db operation - onReorder(localTabs) + delay(400) // Visual confirmation + onSave(localVisibleTabs, localHiddenTabs.toSet()) isLoading = false onDismiss() } @@ -183,11 +205,12 @@ fun ReorderTabsSheet( LazyColumn( state = listState, modifier = Modifier.fillMaxSize().padding(horizontal = 14.dp), - contentPadding = PaddingValues(bottom = 100.dp, top = 8.dp), + contentPadding = PaddingValues(bottom = 150.dp, top = 8.dp), verticalArrangement = Arrangement.spacedBy(8.dp) ) { - items(localTabs, key = { it }) { tab -> - ReorderableItem(reorderableState, key = tab) { isDragging -> + + items(localVisibleTabs, key = { "v_$it" }) { tab -> + ReorderableItem(reorderableState, key = "v_$tab") { isDragging -> LaunchedEffect(isDragging) { if (isDragging) { performAppCompatHapticFeedback( @@ -201,12 +224,13 @@ fun ReorderTabsSheet( Surface( modifier = Modifier .fillMaxWidth() + .height(60.dp) .clip(CircleShape), shadowElevation = if (isDragging) 4.dp else 0.dp, color = MaterialTheme.colorScheme.surfaceContainerLowest ) { Row( - modifier = Modifier.padding(horizontal = 16.dp, vertical = 18.dp), + modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp), verticalAlignment = Alignment.CenterVertically ) { Icon( @@ -216,11 +240,111 @@ fun ReorderTabsSheet( ) Spacer(modifier = Modifier.width(16.dp)) Text( - text = LibraryTabId.fromStableKey(tab) - ?.let { stringResource(it.labelRes) } - ?: tab, - style = MaterialTheme.typography.bodyLarge + text = LibraryTabId.fromStorageKey(tab) + .let { stringResource(it.titleRes) }, + style = MaterialTheme.typography.bodyLarge, + modifier = Modifier.weight(1f) + ) + + if (localVisibleTabs.size > 2) { + Surface( + onClick = { + performAppCompatHapticFeedback( + view, + appHapticsConfig, + HapticFeedbackConstants.CLOCK_TICK, + HapticFeedbackConstants.FLAG_IGNORE_GLOBAL_SETTING + ) + localVisibleTabs = localVisibleTabs.filter { it != tab } + localHiddenTabs = localHiddenTabs + tab + }, + modifier = Modifier.size(36.dp), + shape = AbsoluteSmoothCornerShape(12.dp, 60), + color = MaterialTheme.colorScheme.errorContainer.copy(alpha = 0.4f) + ) { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + Icon( + imageVector = Icons.Rounded.Clear, + contentDescription = stringResource(R.string.reorder_tabs_cd_remove_tab), + tint = MaterialTheme.colorScheme.error, + modifier = Modifier.size(20.dp) + ) + } + } + } else { + Spacer(modifier = Modifier.width(36.dp)) + } + } + } + } + } + + if (localHiddenTabs.isNotEmpty()) { + item(key = "h_hidden") { + Text( + text = stringResource(R.string.reorder_tabs_hidden_section), + style = MaterialTheme.typography.titleMedium, + fontFamily = GoogleSansRounded, + fontWeight = FontWeight.Bold, + modifier = Modifier.padding(start = 8.dp, top = 16.dp, bottom = 4.dp), + color = MaterialTheme.colorScheme.secondary + ) + } + + items(localHiddenTabs, key = { "h_$it" }) { tab -> + Surface( + modifier = Modifier + .fillMaxWidth() + .height(60.dp) + .clip(CircleShape), + color = MaterialTheme.colorScheme.surfaceContainerLowest + ) { + Row( + modifier = Modifier.padding(horizontal = 16.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + imageVector = Icons.Rounded.DragIndicator, + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.38f) + ) + Spacer(modifier = Modifier.width(16.dp)) + Text( + text = LibraryTabId.fromStorageKey(tab) + .let { stringResource(it.titleRes) }, + style = MaterialTheme.typography.bodyLarge, + modifier = Modifier.weight(1f) ) + Surface( + onClick = { + performAppCompatHapticFeedback( + view, + appHapticsConfig, + HapticFeedbackConstants.CLOCK_TICK, + HapticFeedbackConstants.FLAG_IGNORE_GLOBAL_SETTING + ) + localHiddenTabs = localHiddenTabs.filter { it != tab } + localVisibleTabs = localVisibleTabs + tab + }, + modifier = Modifier.size(36.dp), + shape = AbsoluteSmoothCornerShape(12.dp, 60), + color = MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.4f) + ) { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + Icon( + imageVector = Icons.Rounded.Add, + contentDescription = stringResource(R.string.reorder_tabs_cd_add_tab), + tint = MaterialTheme.colorScheme.primary, + modifier = Modifier.size(20.dp) + ) + } + } } } } @@ -266,7 +390,7 @@ fun FloatingToolBar( ) { IconButton( modifier = Modifier.align(Alignment.CenterVertically), - onClick = onReset // This now calls the lambda from the parent + onClick = onReset ) { Icon( painter = painterResource(R.drawable.outline_restart_alt_24), @@ -285,4 +409,4 @@ fun FloatingToolBar( ) } } -} \ No newline at end of file +} 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..e1c46e818 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 @@ -2094,10 +2094,13 @@ fun LibraryScreen( } if (showReorderTabsSheet) { + val hiddenTabs by playerViewModel.hiddenLibraryTabsFlow.collectAsStateWithLifecycle() ReorderTabsSheet( - tabs = tabTitles, - onReorder = { newOrder -> + visibleTabs = tabTitles, + hiddenTabs = hiddenTabs, + onSave = { newOrder, newHidden -> playerViewModel.saveLibraryTabsOrder(newOrder) + playerViewModel.saveLibraryHiddenTabs(newHidden) }, onReset = { playerViewModel.resetLibraryTabsOrder() 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..fa7d009bc 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 @@ -1055,19 +1055,46 @@ class PlayerViewModel @Inject constructor( initialValue = 0 // Default to Songs tab ) - val libraryTabsFlow: StateFlow> = userPreferencesRepository.libraryTabsOrderFlow - .map { orderJson -> - if (orderJson != null) { - try { - Json.decodeFromString>(orderJson) - } catch (e: Exception) { - listOf("SONGS", "ALBUMS", "ARTIST", "PLAYLISTS", "FOLDERS", "LIKED") - } - } else { - listOf("SONGS", "ALBUMS", "ARTIST", "PLAYLISTS", "FOLDERS", "LIKED") + val libraryTabsFlow: StateFlow> = combine( + userPreferencesRepository.libraryTabsOrderFlow, + userPreferencesRepository.libraryHiddenTabsFlow + ) { orderJson, hiddenTabs -> + val allTabsInOrder = if (orderJson != null) { + try { + Json.decodeFromString>(orderJson) + } catch (e: Exception) { + LibraryTabId.defaultOrder.map { it.storageKey } } + } else { + LibraryTabId.defaultOrder.map { it.storageKey } } - .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), listOf("SONGS", "ALBUMS", "ARTIST", "PLAYLISTS", "FOLDERS", "LIKED")) + + // Ensure all available tabs are present (e.g. new tabs from app updates) + val availableKeys = LibraryTabId.defaultOrder.map { it.storageKey } + val mergedOrder = (allTabsInOrder + availableKeys).distinct() + + mergedOrder.filter { it !in hiddenTabs } + }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), LibraryTabId.defaultOrder.map { it.storageKey }) + + val hiddenLibraryTabsFlow: StateFlow> = combine( + userPreferencesRepository.libraryTabsOrderFlow, + userPreferencesRepository.libraryHiddenTabsFlow + ) { orderJson, hiddenTabs -> + val allTabsInOrder = if (orderJson != null) { + try { + Json.decodeFromString>(orderJson) + } catch (e: Exception) { + LibraryTabId.defaultOrder.map { it.storageKey } + } + } else { + LibraryTabId.defaultOrder.map { it.storageKey } + } + + val availableKeys = LibraryTabId.defaultOrder.map { it.storageKey } + val mergedOrder = (allTabsInOrder + availableKeys).distinct() + + mergedOrder.filter { it in hiddenTabs } + }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyList()) private val _loadedTabs = MutableStateFlow(emptySet()) private var lastBlockedDirectories: Set? = null @@ -2803,6 +2830,12 @@ class PlayerViewModel @Inject constructor( } } + fun saveLibraryHiddenTabs(hiddenTabs: Set) { + viewModelScope.launch { + userPreferencesRepository.setLibraryHiddenTabs(hiddenTabs) + } + } + fun resetLibraryTabsOrder() { viewModelScope.launch { userPreferencesRepository.resetLibraryTabsOrder() diff --git a/app/src/main/res/values-es/strings_library.xml b/app/src/main/res/values-es/strings_library.xml index f32199d42..fb099bfdc 100644 --- a/app/src/main/res/values-es/strings_library.xml +++ b/app/src/main/res/values-es/strings_library.xml @@ -25,7 +25,6 @@ Transferencia al reloj Ajustes Editar - Reorder pestañas Expandir menú @@ -255,6 +254,10 @@ ¿Restablecer el orden de las pestañas al predeterminado? Reordenando pestañas… Control de arrastre + Pestañas visibles + Pestañas eliminadas + Eliminar pestaña + Añadir pestaña Elige un artista diff --git a/app/src/main/res/values/strings_library.xml b/app/src/main/res/values/strings_library.xml index 8b7c6c2c6..f8781bc52 100644 --- a/app/src/main/res/values/strings_library.xml +++ b/app/src/main/res/values/strings_library.xml @@ -25,7 +25,6 @@ Watch transfer Settings Edit - Reorder tabs Expand menu @@ -255,6 +254,10 @@ Reset tab order to the default? Reordering tabs… Drag handle + Visible Tabs + Removed Tabs + Remove tab + Add tab Pick an Artist From 61ea8191e136f99ec8d43b2b18cbd8ef822e1836 Mon Sep 17 00:00:00 2001 From: Ayaan Date: Tue, 16 Jun 2026 03:46:04 +0530 Subject: [PATCH 56/99] minor fix --- app/src/main/res/values-es/strings_library.xml | 1 + app/src/main/res/values/strings_library.xml | 1 + 2 files changed, 2 insertions(+) diff --git a/app/src/main/res/values-es/strings_library.xml b/app/src/main/res/values-es/strings_library.xml index fb099bfdc..62906018a 100644 --- a/app/src/main/res/values-es/strings_library.xml +++ b/app/src/main/res/values-es/strings_library.xml @@ -25,6 +25,7 @@ Transferencia al reloj Ajustes Editar + Reordenar pestañas Expandir menú diff --git a/app/src/main/res/values/strings_library.xml b/app/src/main/res/values/strings_library.xml index f8781bc52..21f700026 100644 --- a/app/src/main/res/values/strings_library.xml +++ b/app/src/main/res/values/strings_library.xml @@ -25,6 +25,7 @@ Watch transfer Settings Edit + Reorder tabs Expand menu From ca21993eabcbbfceb32cc54bf252cd51b340ff00 Mon Sep 17 00:00:00 2001 From: Hisham-Alzamzami Date: Wed, 17 Jun 2026 04:06:07 +0300 Subject: [PATCH 57/99] Remove old Arabic translation --- app/src/main/res/values-ar/plurals.xml | 75 --- app/src/main/res/values-ar/strings.xml | 247 ------- app/src/main/res/values-ar/strings_auth.xml | 74 -- .../main/res/values-ar/strings_components.xml | 190 ------ .../strings_presentation_batch_a.xml | 17 - .../strings_presentation_batch_b.xml | 88 --- .../strings_presentation_batch_c.xml | 83 --- .../strings_presentation_batch_d.xml | 124 ---- .../strings_presentation_batch_e.xml | 160 ----- .../strings_presentation_batch_f.xml | 239 ------- .../strings_presentation_batch_g.xml | 636 ------------------ .../main/res/values-ar/strings_screens.xml | 252 ------- .../main/res/values-ar/strings_settings.xml | 300 --------- 13 files changed, 2485 deletions(-) delete mode 100644 app/src/main/res/values-ar/plurals.xml delete mode 100644 app/src/main/res/values-ar/strings.xml delete mode 100644 app/src/main/res/values-ar/strings_auth.xml delete mode 100644 app/src/main/res/values-ar/strings_components.xml delete mode 100644 app/src/main/res/values-ar/strings_presentation_batch_a.xml delete mode 100644 app/src/main/res/values-ar/strings_presentation_batch_b.xml delete mode 100644 app/src/main/res/values-ar/strings_presentation_batch_c.xml delete mode 100644 app/src/main/res/values-ar/strings_presentation_batch_d.xml delete mode 100644 app/src/main/res/values-ar/strings_presentation_batch_e.xml delete mode 100644 app/src/main/res/values-ar/strings_presentation_batch_f.xml delete mode 100644 app/src/main/res/values-ar/strings_presentation_batch_g.xml delete mode 100644 app/src/main/res/values-ar/strings_screens.xml delete mode 100644 app/src/main/res/values-ar/strings_settings.xml diff --git a/app/src/main/res/values-ar/plurals.xml b/app/src/main/res/values-ar/plurals.xml deleted file mode 100644 index 038673ef3..000000000 --- a/app/src/main/res/values-ar/plurals.xml +++ /dev/null @@ -1,75 +0,0 @@ - - - - لا يتم مشاركة أي قوائم تشغيل - جاري مشاركة قائمة تشغيل واحدة - جاري مشاركة قائمتي تشغيل - جاري مشاركة %d قوائم تشغيل - جاري مشاركة %d قائمة تشغيل - جاري مشاركة %d قائمة تشغيل - - - لم يتم تصدير أي قوائم تشغيل إلى %2$s - تم تصدير قائمة تشغيل واحدة إلى %2$s - تم تصدير قائمتي تشغيل إلى %2$s - تم تصدير %1$d قوائم تشغيل إلى %2$s - تم تصدير %1$d قائمة تشغيل إلى %2$s - تم تصدير %1$d قائمة تشغيل إلى %2$s - - - لم يتم إضافة أي أغنية إلى قائمة الانتظار - تمت إضافة أغنية واحدة إلى قائمة الانتظار - تمت إضافة أغنيتين إلى قائمة الانتظار - تمت إضافة %d أغانٍ إلى قائمة الانتظار - تمت إضافة %d أغنية إلى قائمة الانتظار - تمت إضافة %d أغنية إلى قائمة الانتظار - - - لن يتم تشغيل أي أغنية تالياً - سيتم تشغيل أغنية واحدة تالياً - سيتم تشغيل أغنيتين تالياً - سيتم تشغيل %d أغانٍ تالياً - سيتم تشغيل %d أغنية تالياً - سيتم تشغيل %d أغنية تالياً - - - لم يتم إضافة أي أغنية إلى المفضلة - تمت إضافة أغنية واحدة إلى المفضلة - تمت إضافة أغنيتين إلى المفضلة - تمت إضافة %d أغانٍ إلى المفضلة - تمت إضافة %d أغنية إلى المفضلة - تمت إضافة %d أغنية إلى المفضلة - - - لم يتم إزالة أي أغنية من المفضلة - تمت إزالة أغنية واحدة من المفضلة - تمت إزالة أغنيتين من المفضلة - تمت إزالة %d أغانٍ من المفضلة - تمت إزالة %d أغنية من المفضلة - تمت إزالة %d أغنية من المفضلة - - - لم يتم حذف أي ملف - تم حذف ملف واحد - تم حذف ملفين - تم حذف %d ملفات - تم حذف %d ملفاً - تم حذف %d ملفاً - - - هل تريد حذف الملفات؟ - هل تريد حذف أغنية واحدة؟ - هل تريد حذف أغنيتين؟ - هل تريد حذف %d أغانٍ؟ - هل تريد حذف %d أغنية؟ - هل تريد حذف %d أغنية؟ - - - لم يتم تحديد أي مسار - تم تحديد مسار واحد - تم تحديد مسارين - تم تحديد %d مسارات - تم تحديد %d مساراً - تم تحديد %d مساراً - - diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml deleted file mode 100644 index 04844b159..000000000 --- a/app/src/main/res/values-ar/strings.xml +++ /dev/null @@ -1,247 +0,0 @@ - - PixelPlayer - تغيير اسم التطبيق - لقد قمنا بتغيير اسم تطبيقنا من PixelPlay إلى PixelPlayer بسبب مشكلة تتعلق بالعلامة التجارية. استمتع بالاستماع! - لا تظهره مرة أخرى - تجاهل - مطلوب إذن خاص - لتعديل البيانات الوصفية للأغاني (ملفات .mp3)، يحتاج PixelPlayer إلى صلاحية وصول خاصة لجميع الملفات. يتيح لنا هذا تعديل علامات المسارات الصوتية مباشرة. يرجى منح هذا الإذن في الشاشة التالية لتفعيل تعديل البيانات الوصفية. - منح الإذن - الوصول إلى جميع الملفات - خطأ - موافق - إلغاء - استيراد - بحث - - كلمات الأغنية - إغلاق قائمة كلمات الأغنية - جاري تحميل كلمات الأغنية… - تعذر العثور على كلمات لهذه الأغنية. - الكلمات مقدمة من - https://lrclib.net/ - لم يتم العثور على كلمات الأغنية - هل تود البحث عن كلمات الأغنية عبر الإنترنت؟ - لم نتمكن من العثور على كلمات الأغنية تلقائياً. يمكنك تعديل العنوان أو اسم الفنان والمحاولة بالبحث يدوياً. - فشل البحث عن كلمات الأغنية - فشل جلب كلمات الأغنية من الخادم - انتهت مهلة الاتصال. يرجى التحقق من اتصالك بالإنترنت. - خطأ في الشبكة. يرجى التحقق من اتصالك بالإنترنت. - خطأ في الخادم (رمز %d). يرجى المحاولة مرة أخرى لاحقاً. - تم العثور على %d من التطابقات - تم البحث عن \"%s\" - جاري البحث عن كلمات الأغنية… - كلمات الأغنية متوفرة بالفعل. تم تخطي الجلب عبر الإنترنت. - الكلمات المضمنة موجودة بالفعل. تم تخطي الجلب عبر الإنترنت. - ملف الكلمات المحلي (.lrc) موجود بالفعل. تم تخطي الجلب عبر الإنترنت. - إظهار خيارات كلمات الأغنية - فتح أداة الاختيار دائماً بدلاً من التطبيق التلقائي لأول تطابق - حفظ كلمات الأغنية كملف .lrc - حفظ كلمات الأغنية - اختر النسخة المراد حفظها: - مزامنة (مع الطوابع الزمنية) - عادية (نص فقط) - تم حفظ كلمات الأغنية بنجاح - فشل حفظ كلمات الأغنية - لا توجد كلمات أغنية متاحة لحفظها - إعادة تعيين الكلمات المستوردة - إزاحة مزامنة الكلمات - %+.1fs - إعادة تعيين - أبكر - أقرب - - جاري فحص ملفات الموسيقى… - جاري معالجة الملفات… - %1$d من أصل %2$d ملفاً - جاري مزامنة المكتبة… - اكتملت المزامنة - انتظار… - جاري مزامنة المكتبة… - جاري الإنهاء في الخلفية… - جاري فحص كلمات الأغاني… - جاري تنظيف ذاكرة التخزين المؤقت لأغلفة الألبومات… - جاري المزامنة مع المصادر السحابية… - مسار مجهول - فنان مجهول - ألبوم مجهول - اختر فناناً - افتح أي فنان منسوب إليه هذا المسار. - فنان واحد - %1$d فنانين - الفنان الرئيسي - صفحة الفنان - تشغيل سريع - تعذر فتح ملف الصوت هذا. - فتح المشغل الكامل - إغلاق المشغل العائم - إغلاق المشغل - المسار السابق - المسار التالي - إيقاف مؤقت - تشغيل - لم يتم العثور على قائمة التشغيل. - القرص %d - - يرجى تكوين مفتاح API صالح لمزود الذكاء الاصطناعي المحدد في الإعدادات. - خطأ في الذكاء الاصطناعي: %s - رفض مزود الذكاء الاصطناعي المحدد الطلب لعدم وجود رصيد أو حصة متاحة في الحساب. - لم يعد نموذج الذكاء الاصطناعي المحدد متاحاً. حاول PixelPlayer التبديل تلقائياً إلى نموذج مدعوم. - لم يتمكن الذكاء الاصطناعي من العثور على أي أغانٍ بناءً على طلبك. - اكتب فكرة لـ "مزيجك اليومي" - تم تحديث المزيج اليومي بواسطة الذكاء الاصطناعي - لم يتمكن الذكاء الاصطناعي من العثور على أغانٍ لهذا المزيج - - الترجمة بواسطة الذكاء الاصطناعي - تحتوي كلمات هذه الأغنية على ترجمة بالفعل - كلمات هذه الأغنية مكتوبة بهذه اللغة بالفعل - لم يتم تكوين مفتاح الـ API - تمت ترجمة كلمات الأغنية بنجاح! - جاري ترجمة الكلمات عبر الذكاء الاصطناعي... - - تشغيل عشوائي - تشغيل عشوائي لجميع الأغاني - قائمة التشغيل - آخر قائمة تشغيل تم تشغيلها - - تشغيل عشوائي للكل - آخر قائمة تشغيل - لا توجد قائمة تشغيل متاحة لفتحها - - معرف الألبوم غير صالح - لم يتم العثور على معرف الألبوم - خطأ أثناء تحميل بيانات الألبوم: %s - لم يتم العثور على الألبوم - تعذر التحديث: %s - معرف الفنان غير صالح - لم يتم العثور على معرف الفنان - خطأ أثناء تحميل بيانات الفنان: %s - تعذر العثور على الفنان - لم يتم العثور على أغانٍ صالحة للتشغيل - - أداة ذكية مستجيبة تتكيف مع حجمها - شريط مشغل مضغوط - عناصر تحكم كاملة مع التشغيل العشوائي والتكرار - مشغل مربع بسيط - جاري معالجة إجراء التشغيل… - - لا توجد قوائم تشغيل لمشاركتها - مشاركة قوائم التشغيل - فشلت المشاركة: %1$s - لا توجد قوائم تشغيل لتصديرها - فشل التصدير: %1$s - Music/PixelPlayer Exports - يرجى تكوين مفتاح Gemini API في الإعدادات. - خطأ غير معروف - - جاري إرسال %1$d من الأغاني إلى الساعة - جاري الإرسال إلى الساعة - اكتمل النقل - فشل النقل - تم إلغاء النقل - جاري تحضير النقل للساعة - عمليات نقل عدد %1$d - جاري بدء النقل… - توجد عدة عمليات نقل نشطة - جاري تحضير النقل… - جاري النقل - مكتمل - فاشل - ملغي - جاري التحضير - جاري البدء - عمليات نقل الساعة - يعرض التقدم المباشر لنقل الموسيقى من الهاتف إلى الساعة - - خادم وسائط البث (Cast) - جاري البث إلى الجهاز - تزويد جهاز البث بالوسائط - %1$s: %2$s - التقديم والتأخير غير متاح مؤقتاً لصيغة الصوت هذه أثناء البث لأنها قد تتسبب في تعطل جلسة البث. - - نسخة احتياطية غير صالحة: %1$s - جاري تحضير الاستعادة - جاري بدء مهمة الاستعادة. - جاري تحضير النسخ الاحتياطي - جاري بدء مهمة النسخ الاحتياطي. - تم استعادة النسخة الاحتياطية بنجاح - اكتملت الاستعادة مع وجود بعض المشكلات غير المحلولة. - تعذر إكمال الاستعادة: %1$s - فشلت الاستعادة: %1$s - تم تصدير البيانات بنجاح - فشل التصدير: %1$s - تم استعادة البيانات بنجاح - اكتملت الاستعادة مع وجود مشكلات غير محلولة. الفشل: %1$s - فشل تحميل النماذج - تم إطلاق تعطل تجريبي من خيارات المطور - هذا الإجراء مقصود لاختبار نظام تقارير الأعطال - - لم يتم العثور على الأغنية في القائمة الحالية - تعذر تحديد موقع الأغنية - لم يتم العثور على أغانٍ في المكتبة - توقف التشغيل: انتهى %1$s (نهاية المسار). - مسار - لا توجد أغانٍ لتشغيلها عشوائياً. - الألبومات المحددة - لم يتم العثور على أغانٍ قابلة للتشغيل في الألبومات المحددة - تم إدراج أول %1$d ألبومات فقط في قائمة الانتظار - تم إدراج %1$d ألبومات في قائمة الانتظار (%2$d أغنية) - تعذر إدراج الألبومات المحددة في قائمة الانتظار - جميع الأغاني موجودة بالفعل في المفضلة - لم تكن هناك أي أغانٍ في المفضلة - جاري إنشاء ملف ZIP… - فشلت المشاركة: %1$s - لا يمكن حذف الأغنية التي يتم تشغيلها حالياً - تم حذف %1$d ملفات (تم تخطي %2$d - قيد التشغيل) - تم حذف %1$d من أصل %2$d ملفاً - فشل حذف الملفات - تم حذف الملف - تعذر حذف الملف أو أن الملف غير موجود - تم إلغاء الحذف - تم رفض الإذن - لا يمكن تعديل الملفات - تم رفض الإذن - لا يمكن حفظ كلمات الأغنية - تم رفض الإذن - لا يمكن تعديل هذا الملف - تم تحديث البيانات الوصفية بنجاح - جاري تحديث %1$d من الأغاني… - تم تحديث %1$d من الأغاني بنجاح! - تم تحديث %1$d أغنية. فشل: %2$d - تم استعادة قائمة التشغيل - سيتم حذف هذه الأغاني نهائياً من جهازك ولا يمكن استعادتها. - حذف - - %1$d دقائق - نهاية المسار - تم ضبط المؤقت لمدة %1$d دقائق. - تم إلغاء المؤقت. - لا يمكن تفعيل خيار نهاية المسار: لا توجد أغنية نشطة حالياً. - تم إلغاء تفعيل مؤقت نهاية المسار: تغيرت الأغنية من %1$s إلى %2$s. - سيتوقف التشغيل عند نهاية المسار. - المسار السابق - المسار الحالي - مؤقت النوم - المؤقت - نهاية المسار الحالي - وقت مخصص - إلغاء المؤقت - ضبط مدة مخصصة - عدد مرات التشغيل: %1$s - مرة واحدة - تشغيل المفتاح - %1$d%% - إصدار %1$d - %1$s %2$s - - تعديل %d أغنية - سيتم تحديث الحقول المعدلة فقط. اترك الحقول فارغة للاحتفاظ بالقيم الحالية. - (قيم مختلطة) - (اختياري - اتركه فارغاً للتخطي) - تم تحديث %d أغنية بنجاح - تم تحديث %1$d من أصل %2$d أغنية. تعذر تعديل بعض الملفات. - فشل تحديث الأغاني - - تعديل غلاف المجموعة - سيؤدي هذا إلى استبدال غلاف الألبوم لجميع الأغاني المحددة وعددها %d - تعيين غلاف الألبوم للكل - إزالة جميع أغلفة الألبومات - (أغلفة متعددة مختلفة) - خطأ في التشغيل: %1$s - diff --git a/app/src/main/res/values-ar/strings_auth.xml b/app/src/main/res/values-ar/strings_auth.xml deleted file mode 100644 index e6ea6fed8..000000000 --- a/app/src/main/res/values-ar/strings_auth.xml +++ /dev/null @@ -1,74 +0,0 @@ - - - - رجوع - إظهار كلمة المرور - إخفاء كلمة المرور - جاري الاتصال… - اتصال - تفاصيل الاتصال - أدخل رابط الخادم (URL) وبيانات اعتماد الحساب. - رابط الخادم (URL) - اسم المستخدم - كلمة المرور - أدخل كلمة المرور - admin - مرحباً، %1$s! - - - Subsonic / Navidrome - اتصل بخادم الموسيقى المستضاف ذاتياً - يدعم خوادم Navidrome وAirsonic وGonic وAmpache والخوادم الأخرى المتوافقة مع واجهة برمجة تطبيقات Subsonic. - https://music.example.com - استخدم عنوان الخادم الكامل الذي يبدأ بـ //:https. - هذا هو اسم حسابك على Subsonic أو Navidrome. - كلمة مرور التطبيق (App password) تعمل أيضاً إذا كان خادمك يدعمها. - كلمة مرور التطبيق (App password) تعمل أيضاً إذا كان خادمك يدعمها. - ملء تلقائي لـ //:https - متوافق مع Navidrome وGonic وAirsonic والخوادم الأخرى المتوافقة مع Subsonic - شعار Navidrome - شعار Subsonic - - - Jellyfin - يتصل بخوادم Jellyfin. يتم دعم كل من HTTP وHTTPS للوصول عبر الشبكة المحلية. - اتصل بخادم وسائط Jellyfin الخاص بك - أدخل رابط خادم Jellyfin وبيانات اعتماد الحساب. - http://192.168.1.100:8096 - الرابط الكامل لخادم Jellyfin الخاص بك، شاملاً منفذ الاتصال (Port). - اسم المستخدم لحساب Jellyfin الخاص بك. - كلمة المرور لحساب Jellyfin الخاص بك. - ملء تلقائي لـ //:http - يتصل بخوادم Jellyfin لبث مكتبتك الموسيقية - شعار Jellyfin - - - تم توصيل Google Drive بنجاح! - Google Drive - - - الخروج من تسجيل دخول NetEase؟ - الخروج من تسجيل دخول QQ Music؟ - يمكنك العودة لاحقاً. سيتم تجاهل حالة الصفحة الحالية عند الإغلاق. - خروج - بقاء - تسجيل الدخول إلى NetEase Music - تسجيل الدخول إلى QQ Music - رجوع للخلف في الويب - تقدم للأمام في الويب - تحديث - فتح الصفحة الرئيسية - جاري الحفظ… - تم - إعادة المحاولة - + - انتهت مهلة تحميل الصفحة. يمكنك إعادة المحاولة دون فقدان تقدمك. - تعذر قراءة ملفات تعريف الارتباط (Cookies) الخاصة بالجلسة. - تستغرق الصفحة وقتاً طويلاً للتحميل. استخدم التحديث أو جرب شبكة أخرى. - فشل تحميل WebView. - خطأ HTTP %1$d أثناء تحميل NetEase. - خطأ HTTP %1$d أثناء تحميل QQ Music. - لم يتم العثور على ملفات تعريف الارتباط. سجل الدخول أولاً. - لم يتم رصد تسجيل الدخول بعد. أكمل تسجيل الدخول إلى NetEase قبل الضغط على "تم". - لم يتم رصد تسجيل الدخول بعد. أكمل تسجيل الدخول إلى QQ Music قبل الضغط على "تم". - diff --git a/app/src/main/res/values-ar/strings_components.xml b/app/src/main/res/values-ar/strings_components.xml deleted file mode 100644 index 4f8baf086..000000000 --- a/app/src/main/res/values-ar/strings_components.xml +++ /dev/null @@ -1,190 +0,0 @@ - - - انقر للفتح - غلاف الألبوم - مواضع غلاف الألبوم - المفضلة - تشغيل - إيقاف مؤقت - انقر للتشغيل - عنوان الأغنية - الفنان - تكرار - شريط التقدم، %1$d بالمئة - - - المظهر - المحاذاة - عناصر التحكم - إعادة تعيين كلمات الأغاني؟ - هل أنت متأكد من أنك تريد إعادة تعيين كلمات هذه الأغنية؟ - إخفاء عناصر تحكم المزامنة - ضبط المزامنة - إظهار اللتنّة (Romanization) - إظهار الترجمات - تعطيل الوضع الغامر (لمرة واحدة) - إبقاء الشاشة قيد التشغيل - محاذاة الكلمات لليسار - محاذاة الكلمات للوسط - محاذاة الكلمات لليمين - - - لا يوجد اتصال بالإنترنت - يتطلب هذا المحتوى اتصالاً بالإنترنت. يرجى التحقق من إعدادات الشبكة والمحاولة مرة أخرى. - أنت غير متصل بالإنترنت - يرجى التحقق من اتصالك بالإنترنت والمحاولة مرة أخرى للوصول إلى هذا المحتوى. - - - حفظ موازن مخصص - أدخل اسماً لإعداد موازن الصوت المخصص الجديد. - اسم الإعداد المسبق - إعادة تسمية الإعداد المسبق - لا يمكن أن يكون الاسم فارغاً - حفظ - حفظ كجديد - إعادة تسمية - - - تم تحديث البيانات الوصفية بنجاح! - البيانات الوصفية بالذكاء الاصطناعي - جاري مراجعة دليل المزيج اليومي (Daily Mix)… - مراجعة وتعديل التفاصيل التي تم إنشاؤها - العنوان - الفنان - الألبوم - فنان الألبوم - النوع - الملحن - إعادة المحاولة - تطبيق التغييرات - - - تعديل البيانات الوصفية للأغنية - قد يؤثر تعديل البيانات الوصفية للأغنية على كيفية عرضها وتنظيمها في مكتبتك. هذه التغييرات دائمة وقد لا يمكن التراجع عنها. - فهمت ذلك - معلومات - تعديل الأغنية - استخدام ذكاء Gemini الاصطناعي - إظهار المعلومات - رقم المسار - رقم القرص - ميزة ReplayGain للمسار (ديسيبل) - ميزة ReplayGain للألبوم (ديسيبل) - -6.50 - -8.20 - ميزة ReplayGain للمسار - ميزة ReplayGain للألبوم - العنوان - رقم المسار - رقم القرص - البحث عن كلمات الأغاني على lrclib.net - غلاف الألبوم - اختر صورة مربعة وقم بضبطها لتظهر بشكل ممتاز في جميع أنحاء التطبيق. - تغيير غلاف الألبوم - حذف غلاف الألبوم - معاينة الغلاف الجديد - غلاف الأغنية الحالي - ضبط غلاف الألبوم - استخدم إيماءات القرص والسحب لتحديد الإطار المثالي. - تطبيق غلاف الألبوم - تعذر تحميل الصورة المحددة - - - مشاركة ملف الأغنية عبر - تشغيل الأغنية - مشاركة ملف الأغنية - إضافة إلى قائمة الانتظار - التشغيل تالياً في قائمة الانتظار - إضافة إلى قائمة التشغيل - إضافة إلى قائمة الانتظار - التالي - جاري التحقق من الساعة… - جاري النقل %1$d%% - جاري النقل إلى الساعة… - النقل قيد التنفيذ الآن - إرسال إلى الساعة - الساعة غير متاحة - إرسال الأغنية إلى الساعة - الساعة غير متاحة - تعيين كـ - تعيين كنغمة صوتية - اختر كيفية استخدام هذه الأغنية كنغمة للنظام - تعيين كنغمة رنين - تعيين الأغنية كنغمة رنين - استخدام هذه الأغنية كـ - اختر المكان الذي يجب أن يقوم PixelPlayer بتثبيت هذا الصوت فيه. - نغمة رنين الهاتف - المكالمات الواردة - صوت الإشعارات - الرسائل وتنبيهات التطبيقات - صوت المنبه - منبهات الساعة - تأكيد تغيير الصوت - هل تريد تعيين \"%1$s\" كـ %2$s الخاص بك؟ - تعيين الصوت - تم تعيين \"%1$s\" كـ %2$s الخاص بك - نغمة رنين - صوت إشعارات - صوت منبه - يرجى تمكين إذن "تعديل إعدادات النظام"، ثم العودة إلى PixelPlayer لإنهاء الإجراء تلقائياً. - لم يتم تمكين إذن تعديل إعدادات النظام. - تم تعيين \"%1$s\" كنغمة رنين لك - يمكن استخدام الأغاني المحلية فقط كنغمات رنين. - تعذر إعداد ملف الصوت هذا كنغمة رنين. - تعذر تعيين نغمة الرنين: %1$s - المدة - معلومات الأغنية - المدة - النوع - الألبوم - الفنان - صيغة الصوت - المزود - الملف - تعديل البيانات الوصفية للأغنية - إزالة من المفضلة - إضافة إلى المفضلة - الخيارات - الخيارات - التفاصيل - المعلومات - علامة تبويب التفاصيل - - - %1$d أغنية - تم تحديدها - تشغيل الكل - تشغيل الكل - إعجاب بالكل - إلغاء الإعجاب بالكل - مشاركة الكل كملف ZIP - إضافة الكل إلى قائمة الانتظار - حذف الكل - حذف الكل - - تم تجاهل قائمة التشغيل - تراجع - مزج DJ (Mashup) - قائمة تشغيل جديدة - اسم قائمة التشغيل - قائمة التشغيل الخاصة بي - إنشاء - إضافة %1$d أغانٍ إلى… - اختر قوائم التشغيل - البحث عن قوائم التشغيل… - - %1$d قائمة تشغيل - تصدير الكل - دمج الكل - مشاركة الكل - تصدير - دمج - - إعادة ترتيب علامات تبويب المكتبة - إعادة تعيين الترتيب - هل تريد إعادة تعيين ترتيب علامات التبويب إلى الوضع الافتراضي؟ - جاري إعادة ترتيب علامات التبويب… - مقبض السحب - إعادة تعيين - تم - diff --git a/app/src/main/res/values-ar/strings_presentation_batch_a.xml b/app/src/main/res/values-ar/strings_presentation_batch_a.xml deleted file mode 100644 index 904610ae4..000000000 --- a/app/src/main/res/values-ar/strings_presentation_batch_a.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - ملاحظة أمان: يتم إدخال كلمة المرور الخاصة بك فقط داخل صفحات ويب QQ Music. يقوم PixelPlayer بحفظ ملفات تعريف ارتباط الجلسة (Cookies) لمزامنة مكتبتك الموسيقية. - ملاحظة أمان: يتم إدخال كلمة المرور الخاصة بك فقط داخل صفحات ويب NetEase. يقوم PixelPlayer بحفظ ملفات تعريف ارتباط الجلسة (MUSIC_U) لمزامنة مكتبتك الموسيقية. - فشل في قراءة ملفات تعريف ارتباط QQ Music: %1$s - فشل في قراءة ملفات تعريف ارتباط NetEase: %1$s - - جاري إعداد Google Drive… - ربط Google Drive - بث ملفات الموسيقى مباشرة من حساب Google Drive الخاص بك - تسجيل الدخول باستخدام Google - اختر مجلد الموسيقى - اختر أو أنشئ مجلداً لاستخدامه كمصدر للموسيقى الخاصة بك - إنشاء مجلد \"PixelPlayer Music\" - أنشئ مجلداً جديداً هنا للموسيقى الخاصة بك - لا توجد مجلدات هنا - diff --git a/app/src/main/res/values-ar/strings_presentation_batch_b.xml b/app/src/main/res/values-ar/strings_presentation_batch_b.xml deleted file mode 100644 index a0713c170..000000000 --- a/app/src/main/res/values-ar/strings_presentation_batch_b.xml +++ /dev/null @@ -1,88 +0,0 @@ - - - - الخدمات المرتبطة - الحسابات المتصلة - إدارة المزودين المرتبطين وإبقاء كل عملية ربط تحت سيطرتك. - نشط - متاح - قريباً - متصل - فتح الخدمة - قريباً - جاري تسجيل الخروج… - لا توجد حسابات مرتبطة بعد - قم بربط أحد المزودين لتتمكن من إدارته من هذه الشاشة. - ربط %1$s - %1$s (قريباً) - Telegram - NetEase Music - - - فرز الأغاني - المزيد من الخيارات - تشغيل - إضافة أغانٍ - إضافة - إزالة الأغاني - إعادة ترتيب الأغاني - إعادة ترتيب - إعادة ترتيب الأغنية - قائمة التشغيل هذه فارغة. - هذا المجلد لا يحتوي على أغانٍ. - انقر على "إضافة أغانٍ" للبدء. - خيارات قائمة التشغيل - تعديل قائمة التشغيل - حذف قائمة التشغيل - تعيين الانتقال الافتراضي - تصدير قائمة التشغيل - حذف قائمة التشغيل؟ - هل أنت متأكد من أنك تريد حذف قائمة التشغيل هذه؟ - إعادة تسمية قائمة التشغيل - الاسم الجديد - - - المزيج اليومي - - - اختر الأغاني - اختر النوع - البحث عن الأغاني - تحديد الكل - مسح - النوع: %1$s - اختر نوعاً - الملء السريع - إضافة نوع مخصص - نوع جديد - إضافة نوع مخصص - اسم النوع - اختر أيقونة - - - المشغلة حديثاً - تشغيل الأحدث - لا توجد أغانٍ مشغلة مؤخراً في %1$s - قم بتغيير النطاق الزمني أو تشغيل المزيد من الأغاني لملء هذا الجدول الزمني. - المشغلة حديثاً - اليوم - أمس - - - ضبط نصف قطر الزوايا - قم بمطابقة زوايا شريط التنقل مع الزوايا الفيزيائية لجهازك للحصول على مظهر متناسق وانسيابي. - نصف قطر الزوايا - %1$d dp - - - تشغيل %1$s عشوائياً - - - لا توجد أغانٍ • %2$s - أغنية واحدة • %2$s - أغنيتان • %2$s - %1$d أغانٍ • %2$s - %1$d أغنية • %2$s - %1$d أغنية • %2$s - - diff --git a/app/src/main/res/values-ar/strings_presentation_batch_c.xml b/app/src/main/res/values-ar/strings_presentation_batch_c.xml deleted file mode 100644 index 23cf57e71..000000000 --- a/app/src/main/res/values-ar/strings_presentation_batch_c.xml +++ /dev/null @@ -1,83 +0,0 @@ - - - - خطأ أثناء تحميل الأغاني - خطأ أثناء تحميل الألبومات - خطأ أثناء تحميل الفنانين - إعادة المحاولة - - - لم يتم العثور على أغانٍ في مكتبتك. - جرّب إعادة فحص مكتبتك من الإعدادات إذا كانت لديك ملفات موسيقى على جهازك. - لم يتم العثور على أغانٍ - - - جديد - إنشاء قائمة تشغيل جديدة - استيراد قائمة تشغيل M3U - تحديد موقع الأغنية الحالية - جميع الأغاني - سحابي - محلي - خيارات الفرز - - - متزامنة - الفنان (اختياري) - - - إضافة أغانٍ - إضافة الأغاني المحددة - إضافة - بحث أو تصفية الأغاني… - المحبوبة - فشل تحميل الأغاني - تحميل المزيد - - - ذكاء اصطناعي - منسقة بشكل مثالي - المزيج اليومي - رحلتك الصوتية جاهزة الآن - صانع قوائم التشغيل بالذكاء الاصطناعي - صف الأجواء أو الحالة المزاجية أو النشاط، ودع الذكاء الاصطناعي يتولى تنسيق قائمة التشغيل المثالية من مكتبتك. - حجم قائمة التشغيل - الحد الأدنى للأغاني - الحد الأقصى للأغاني - مثال: أجواء مسائية هادئة، طاقة حماسية للتمارين… - انقر لإعادة المحاولة - تم توليف رحلتك الصوتية! - جاري الإنشاء… - جاهز للتشغيل - إنشاء قائمة التشغيل - - - لا توجد أغانٍ بعد - أضف موسيقى إلى جهازك أو قم بمزامنة مصدر سحابي لبدء الاستماع. - لم يتم العثور على أغانٍ محلية - جرّب فلتراً آخر للمصادر أو أعد فحص مكتبة جهازك. - لم يتم العثور على أغانٍ سحابية - قم بمزامنة أغانٍ من Telegram أو NetEase، أو تحوّل إلى المصدر المحلي. - لا توجد ألبومات متاحة - ستظهر الألبومات هنا بمجرد أن تحتوي مكتبتك على مسارات مجمعة. - لم يتم العثور على ألبومات محلية - يلزم وجود أغانٍ محلية لإنشاء مجموعات ألبومات محلية. - لم يتم العثور على ألبومات سحابية - ستظهر الأغاني السحابية التي تحتوي على بيانات الألبوم هنا بعد المزامنة. - لا يوجد فنانون متاحون - يتم عرض الفنانين بعد فهرسة الأغاني من أي مصدر. - لم يتم العثور على فنانين محليين - لا تتوفر بيانات وصفية للفنانين للأغاني المحلية في الوقت الحالي. - لم يتم العثور على فنانين سحابيين - تظهر إدخالات الفنانين السحابيين عند مزامنة الأغاني عن بُعد. - لا توجد أغانٍ مفضلة بعد - انقر على أيقونة القلب أثناء تشغيل أي أغنية لحفظها هنا. - لا توجد أغانٍ محلية مفضلة - قم بتغيير فلتر المصدر أو أضف إعجاباً بالأغاني الموجودة على جهازك. - لا توجد أغانٍ سحابية مفضلة - أضف إعجاباً بمسارات Telegram أو NetEase لرؤيتها في هذا العرض. - لم يتم العثور على مجلدات - ستظهر مجلدات وحدة التخزين الداخلية التي تحتوي على موسيقى هنا. - لا توجد قوائم تشغيل بعد - أنشئ قائمة تشغيلك الأولى لتنظيم مكتبتك الموسيقية. - diff --git a/app/src/main/res/values-ar/strings_presentation_batch_d.xml b/app/src/main/res/values-ar/strings_presentation_batch_d.xml deleted file mode 100644 index 45159b522..000000000 --- a/app/src/main/res/values-ar/strings_presentation_batch_d.xml +++ /dev/null @@ -1,124 +0,0 @@ - - - المكتبة - نقل إلى الساعة - الإعدادات - تعديل - إعادة ترتيب علامات التبويب - فرز حسب - سحابي - عرض - قنوات Telegram السحابية - عرض قوائم التشغيل - شبكة - قائمة - الذاكرة الداخلية - بطاقة SD - بطاقة SD غير متاحة حالياً. - عرض المواضيع - القنوات - المواضيع - كلاهما - سحابي - سحابي فقط - جاري إنشاء البيانات الوصفية بالذكاء الاصطناعي… - يمكنك تحديد ما يصل إلى %1$d ألبومات - مجلد - توسيع القائمة - علامات تبويب المكتبة - الانتقال مباشرة إلى أي علامة تبويب أو إعادة ترتيبها. - إعادة ترتيب علامات التبويب - مجلد - - جاري الإرسال إلى الساعة - جاري بدء النقل… - جاري النقل - تم بنجاح - فشل النقل - تم الإلغاء - جاري التحضير - جاري تحضير النقل… - إلغاء النقل - - دمج قوائم التشغيل - أدخل اسماً لقائمة التشغيل المدمجة: - قائمة تشغيل مدمجة - سيؤدي هذا إلى دمج %1$d من قوائم التشغيل المحددة في قائمة واحدة. - - مساحة الـ DJ - جاري التحميل… - منصة (Deck) %1$d - تحميل أغنية - لم يتم تحميل أي أغنية - - ميزة فصل المسارات الصوتيّة (Stems) غير متاحة بعد. - مستوى الصوت - السرعة - ممازج الصوت (Crossfader) - منصة 1 - منصة 2 - اختر أغنية - - تغيير وضع العرض - تعطيل موازن الصوت - تمكين موازن الصوت - تعديل - تعديل الإعدادات المسبقة - إعداد مخصص - الإعدادات المسبقة - تحديث - تضخيم الباس (Bass Boost) - المحاكي المحيطي (Virtualizer) - جهارة الصوت (Loudness) - غير مدعوم - غير مدعوم على هذا الجهاز - مستوى الصوت - الاستجابة الترددية - هرتز - الباس (الترددات المنخفضة) - الترددات المتوسطة المنخفضة - الترددات المتوسطة المرتفعة - التريبل (الترددات الحادة) - الباس / منخفض - متوسط / مرتفع - صفحة %1$d - إعادة تعيين المدة - - يتم استخدام الإعدادات الافتراضية العامة - تم حفظ التغييرات بنجاح - قواعد قائمة التشغيل - الانتقالات العامة - حفظ - تخصيص السلوك الافتراضي لقائمة التشغيل هذه تحديداً. - يطبق هذا التكوين على جميع مصادر التشغيل ما لم يتم تجاوزه. - حالة التنشيط - الافتراضي العام - تابع للإعداد العام - تجاوز مخصص - افتراضي قائمة التشغيل - تجاوز مخصص - قم بالتمكين لتعيين قواعد خاصة بقائمة التشغيل هذه. - نمط الانتقال - كيفية تداخل المسارات الصوتية معاً - التداخل المتلاشي (Crossfade) - بدون انتقال - مدة الانتقال - إجمالي التداخل %1$d ثوانٍ - إعادة تعيين - الأغنية الحالية - الأغنية التالية - ستتداخل المسارات لمدة %1$d ثوانٍ - منحنيات مستوى الصوت - ضبط ميل وتلاشي الصوت بدقة - تلاشي للخارج (Fade Out) - تلاشي للداخل (Fade In) - - تشغيل %1$s - طي %1$s - توسيع %1$s - تعديل صورة الفنان - تغيير الصورة - إعادة تعيين للوضع الافتراضي - تشغيل أغاني الفنان عشوائياً - الفنان - diff --git a/app/src/main/res/values-ar/strings_presentation_batch_e.xml b/app/src/main/res/values-ar/strings_presentation_batch_e.xml deleted file mode 100644 index 22192fce5..000000000 --- a/app/src/main/res/values-ar/strings_presentation_batch_e.xml +++ /dev/null @@ -1,160 +0,0 @@ - - - - قائمة الانتظار فارغة. - إجراءات قائمة الانتظار - مسح قائمة الانتظار - حفظ كقائمة تشغيل - تحديد موقع الأغنية الحالية - قائمة انتظار %1$s - قائمة الانتظار الحالية - تمت الإزالة - مسح قائمة الانتظار - هل أنت متأكد من أنك تريد مسح جميع الأغاني من قائمة الانتظار باستثناء الأغنية الحالية؟ - التالي في القائمة - قائمة الانتظار فارغة حالياً. - قائمة الانتظار - تبديل التشغيل العشوائي - تبديل التكرار - مؤقت النوم - حفظ كقائمة تشغيل - إلغاء تحديد الكل - اسم قائمة التشغيل - ابحث عن أغانٍ لتضمينها… - حفظ باسم: %1$s - أدخل اسماً لقائمة التشغيل - لا توجد أغانٍ تطابق \"%1$s\" - تجاهل الأغنية - إزالة من قائمة التشغيل - المزيد من الخيارات لـ %1$s - - - لا توجد مسارات منتظرة. - مسار واحد منتظر. - مساران منتظران. - %d مسارات منتظرة. - %d مساراً منتظراً. - %d مسار منتظر. - - - لم يتم تحديد أي أغنية - تم تحديد أغنية واحدة - تم تحديد أغنيتين - تم تحديد %d أغانٍ - تم تحديد %d أغنية - تم تحديد %d أغنية - - - - لم يتم إنشاء أي قائمة تشغيل بعد. - المس زر "قائمة تشغيل جديدة" للبدء. - - - إنشاء قائمة تشغيل - اختر طريقة الإنشاء. - يدوياً - صمم الغلاف، الأيقونة، الشكل، واشحن الأغاني بنفسك. - بالذكاء الاصطناعي - أنشئ قائمة تشغيل منسقة ومخصصة عبر خيارات متقدمة. - تتطلب هذه الميزة تهيئة مفتاح Gemini API في الإعدادات. - إعداد مفتاح API - - - معمل قوائم التشغيل بالذكاء الاصطناعي - إعادة تعيين - جاري الإنشاء… - إنشاء - الهدف والأجواء - اسم قائمة التشغيل (اختياري) - ما هي الأجواء التي ترغب بها في قائمة التشغيل هذه؟ - مثال: قيادة وقت الغروب مع ألحان سينث دافئة - الاتجاه الفني - الحالة المزاجية - النشاط - الحقبة الزمنية - محرك التنسيق - الحيوية والطاقة - تتحكم في حدة الأغاني وإيقاعها. 1 = هادئ/بطيء، 5 = حماسي جداً/سريع. - عمق الاكتشاف - تتحكم في مدى معرفتك بالاختيارات. 1 = المفضلة الأكثر تشغيلاً، 5 = أغانٍ نادرة ولم تسمعها كثيراً. - أقل عدد أغانٍ - أقصى عدد أغانٍ - الفلاتر - أنواع موسيقية مفضلة (اختياري) - مثال: سينث ويف، إندي بوب - أنواع موسيقية تتجنبها (اختياري) - مثال: ميتال، هارد تراب - اللغة المفضلة (اختياري) - مثال: الإنجليزية، العربية، معزوفات موسيقية - إعطاء الأولوية للمفضلة - تجنب الكلمات غير لائقة (Explicit) - معاينة الأمر (Prompt) - سيظهر أمرك النهائي هنا بمجرد إضافة تفضيلاتك. - تنسيق بدقة متناهية - حدد المزاج، النشاط، القيود، وعمق الاختيارات. - سيقوم الذكاء الاصطناعي باختيار الأغاني من مكتبتك المحلية فقط. - يرجى إضافة توجيه واحد على الأقل للذكاء الاصطناعي. - يرجى تعيين نطاق أغانٍ صالح. - 5/%1$d - مخصص… - إدخال قيمة مخصصة - أدخل قيمتك المخصصة - - - أي حقبة - الطلب الأساسي: %1$s. - المزاج المستهدف: %1$s. - سياق النشاط: %1$s. - التركيز على الحقبة: %1$s. - إعطاء الأولوية للأنواع: %1$s. - تجنب الأنواع: %1$s. - اللغة المفضلة: %1$s. - مستوى الطاقة المستهدف: 5/%1$d. - هدف الاكتشاف: 5/%1$d حيث 1 تعني مألوف و5 تعني اختيارات عميقة ونادرة. - إعطاء الأولوية للأغاني القريبة من مفضلات المستمع كلما أمكن ذلك. - تجنب الأغاني ذات الكلمات غير اللائقة كلما توفرت بدائل. - الحفاظ على سلاسة الانتقالات وتجنب التكرار المتتالي لنفس الفنان. - - - هادئ (Chill) - حماسي (Energetic) - سعيد (Happy) - داكن/غامض (Dark) - رومانسي (Romantic) - شجي/ميلانكولي (Melancholic) - - - تمارين رياضية (Workout) - تركيز (Focus) - رحلة على الطريق (Road trip) - حفلة (Party) - دراسة (Study) - وقت متأخر من الليل (Late night) - - - @string/presentation_batch_e_ai_era_any - السبعينات - الثمانينات - التسعينات - الألفينات (2000s) - العقد 2010 - العقد 2020 - - - - إعادة تعيين الإعدادات المسبقة - سيؤدي هذا إلى استعادة الترتيب الافتراضي وظهور الإعدادات المسبقة. هل تريد المتابعة؟ - إدارة الإعدادات المسبقة - اسحب لإعادة الترتيب • انقر على أيقونة العين للإظهار أو الإخفاء - إعادة تعيين للوضع الافتراضي - مرئي - مخفي - - - كيف يتم بناء المزيج اليومي الخاص بك - يتم بناء المزيج اليومي الخاص بك (Daily Mix) بناءً على أغانيك المفضلة والأكثر تشغيلاً. نقوم أيضاً بإضافة مسارات من فنانين وأنواع موسيقية تحبها لتتمكن من اكتشاف موسيقى جديدة. - أخبر الذكاء الاصطناعي بما تود الاستماع إليه اليوم - نحن نستخدم عينة صغيرة للحفاظ على انخفاض استهلاك البيانات والتكلفة - جاري التحديث… - تحديث المزيج اليومي - diff --git a/app/src/main/res/values-ar/strings_presentation_batch_f.xml b/app/src/main/res/values-ar/strings_presentation_batch_f.xml deleted file mode 100644 index 8dc2ea76c..000000000 --- a/app/src/main/res/values-ar/strings_presentation_batch_f.xml +++ /dev/null @@ -1,239 +0,0 @@ - - - محدد - تحديث المكتبة - فحص المكتبة بأكملها بحثاً عن الملفات الجديدة والمعدلة. - إعادة فحص كاملة - إعادة بناء قاعدة البيانات - جاري تحضير المزامنة - جاري قراءة مخزن الوسائط (MediaStore) - جاري معالجة المسارات - جاري الحفظ في قاعدة البيانات - جاري فحص ملفات كلمات الأغاني (LRC) - جاري تنظيف ذاكرة التخزين المؤقت لأغلفة الألبومات - جاري مزامنة المصادر السحابية - جاري إتمام المزامنة - %1$s • %2$d%% (%3$d/%4$d) - %1$s… - تحديث كلمات الأغاني - جلب كلمات الأغاني تلقائياً لجميع الأغاني باستخدام lrclib. - تحديث كلمات الأغاني - جاري معالجة %1$d من أصل %2$d أغنية - أدخل مفتاح API - حفظ - تم الحفظ! - الأوامر الجاهزة (Presets) - أدخل أمر النظام المستهدف… - إعادة تعيين - المنسق المحترف (Professional Curator) - أنت \'Vibe-Engine\'، منسق موسيقى عالمي وخبير في التدفق الصوتي الانسيابي. هدفك هو بناء تجارب استماع سلسة وعالية الدقة. أعطِ الأولوية للتوافق الهارموني، والانتقالات المنطقية لسرعة الإيقاع (BPM)، والتوازن المدروس بين الأغاني المألوفة المفضلة والاكتشافات الذكية المبنية على نمط الاستماع. - المستكشف المبتكر (Creative Maverick) - أنت مستكشف موسيقى طليعي متخصص في صياغة \'التناغم غير المتوقع\'. مهمتك هي كسر حدود الأنواع الموسيقية التقليدية عبر تحديد ترابطات صوتية غير ظاهرة. أعطِ الأولوية للاختيارات النادرة والعميقة، والتركيبات التجريبية، والتجديد الفني مع الحفاظ على منطق انتقال مفاجئ ومذهل في نفس الوقت. - أمين المكتبة الصارم (Strict Librarian) - أنت مهندس دقيق لقواعد البيانات الموسيقية. منطقك مدفوع بالدقة المطلقة للبيانات الوصفية والالتزام الصارم بالتصنيفات. قلل من المكتشفات الخوارزمية العشوائية لصالح التناسق التام للنوع الموسيقي، ومطابقة مستويات الطاقة، وتعظيم استدعاء التفضيلات المحددة بدقة من قبل المستخدم. - الدليل الأجاوائي (Atmospheric Guide) - أنت خبير في التراكيب الصوتية المحيطية والتدفقات الموسيقية الهادئة. ركز حصرياً على المسارات التي تساعد على الدخول في حالة من \'التركيز العميق\' أو \'السكينة\'. أعطِ الأولوية للدفء الصوتي الآلاتي، والتوزيعات البسيطة، والانتقالات اللطيفة، مع تجنب الأصوات الحادة أو التحولات المفاجئة في النطاق الديناميكي للصوت. - عاشق الهندسية الصوتية (Sonic Enthusiast) - أنت محلل صوتي مهتم بتعقيد الإنتاج والآلات الموسيقية. أعطِ الأولوية للمسارات التي تتميز بنطاق ديناميكي واسع، والإيقاعات المتعددة المعقدة، وجودة المسرح الصوتي الفائقة. فضّل المقطوعات التي تتطلب استماعاً نشطاً وتكافئ المستمع عند الانتباه إلى التفاصيل التقنية وتفاصيل التوزيع الصوتي. - محفز الطاقة (Energy Catalyst) - أنت مولد إيقاعات عالي الحماس والزخم. ترتكز فلسفتك على خطوط الباس القوية، وشدة الإيقاع، والنغمات الجذابة. أعطِ الأولوية للمسارات المتوافقة مع أجواء النوادي ذات الإيقاع السريع (High-BPM)، والطاقة المتزامنة، والتوتر الإيقاعي المستمر للحفاظ على نبض المستمع وتحفيزه في ذروة مستوياته. - - قائمة تشغيل ذكية جديدة - قائمة تشغيل جديدة - إضافة أغانٍ - الرجوع أو الإلغاء - التالي - إنشاء - تعديل قائمة التشغيل - إغلاق - تأكيد القص - تجميعة صور منشأة تلقائياً - إضافة صورة - اختر صورة - تغيير - إزالة - اسم قائمة التشغيل - مزيجي الرائع - تعديل الغلاف - ضبط غلافك الفني - استخدم إيماءات التكبير والسحب للحصول على الإطار المثالي - يدوي - ذكي - الإنشاء باستخدام الذكاء الاصطناعي - قاعدة ذكية - الافتراضي - صورة - أيقونة - لون الخلفية - رمز الأيقونة - نمط الشكل - معلمات الشكل - نصف قطر الزوايا - النعومة - الأضلاع - الانحناء - الدوران - المقاس - الأكثر تشغيلاً - المسارات الأكثر تشغيلاً لديك. - المشغلة حديثاً - الأغاني التي استمعت إليها مؤخراً. - المفضلات المنسية - المسارات المفضلة التي لم تقم بتشغيلها منذ فترة. - جواهر جديدة - المسارات المضافة حديثاً مع نسب تشغيل منخفضة. - - نمط لوحة الألوان (Palette) - اختر ألوان الألبوم لواجهة مستخدم المشغل. - الألوان - تطبيق - متوازن وهادئ. - لمسات حيوية عالية التشبع. - تحولات جريئة في الدرجات والتباين. - لمسات حيوية مبهجة ومائلة. - بقعة نغمية (Tonal Spot) - حيوي (Vibrant) - تعبيري (Expressive) - سلطة فواكه (Fruit Salad) - دقة الألوان - القيمة 0 تحافظ على الضبط الحالي. القيم الأعلى تلتزم بشكل أقرب بالدرجة المهيمنة لغلاف الألبوم. - الحالي - أكثر دقة - 0 • الحالي - %1$d • طفيف - %1$d • متوازن - %1$d • دقيق - - تعديلات تحميل واجهة المشغل - كلمات الأغاني المتحركة (للأجهزة القوية) - تستخدم تأثيرات بصرية ورسوم زنبركية متحركة للكلمات. قد تسبب سقوطاً في معدل الإطارات على الأجهزة الضعيفة. - تأثير تمويه كلمات الأغاني (Blur) - يطبق تمويهاً لعمق الحقل على الكلمات غير النشطة حالياً. - قوة التمويه - ضبط كثافة تأثير التمويه. - %1$.1fx - الخطوة 1 · اختر ما تريد تأخيره - تأخير كل شيء - تجميد محتوى المشغل بالكامل حتى تتمدد خلفية اللوحة بالكامل. - العرض الدوار للألبومات - تأخير عرض غلاف الألبوم والعرض الدوار حتى تتمدد اللوحة السفلية. - البيانات الوصفية للأغنية - تأخير العنوان، الفنان، وإجراءات الكلمات/قائمة الانتظار. - شريط التقدم - تأخير الخط الزمني وعلامات الوقت حتى يكتمل التمدد. - عناصر التحكم في التشغيل - تأخير أزرار التشغيل/الإيقاف المؤقت، التقديم، وعناصر الإعجاب. - جميع المكونات المؤجلة نشطة حالياً. قم بتعطيل \"تأخير كل شيء\" لتخصيص كل جزء على حدة. - الخطوة 2 · تكوين سلوك العناصر النائبة (Placeholders) - استخدام عناصر نائبة للمكونات المؤجلة - الحفاظ على استقرار الواجهة عبر عرض عناصر نائبة خفيفة الوزن أثناء انتظار المكونات للتمدد. - الخطوة 3 · اختر وقت تحول العناصر النائبة إلى المحتوى الحقيقي - اختر وضعاً واحداً. يعتمد وضع العتبة على أشرطة التمرير؛ بينما ينتظر وضع إفلات السحب حتى تترك إيماءة اللوحة. - قم بتمكين مكون مؤجل واحد على الأقل لإلغاء قفل وضع التفعيل. - العتبة (Threshold) - يعتمد على النسبة المئوية للتمدد. - إفلات السحب - يتحول فقط بعد إفلات إيماءة السحب. - عتبة التمدد - مدى التمدد المطلوب للوحة قبل أن تصبح المكونات المؤجلة مرئية. - يظهر المحتوى عند تمدد بنسبة %1$d%% - التطبيق أيضاً عند إغلاق المشغل - استخدام عتبة الإغلاق للتحول مجدداً إلى العناصر النائبة أثناء الطي. - عتبة الإغلاق - مقدار الطي المطلوب قبل أن تتولى العناصر النائبة العرض مرة أخرى. - تظهر العناصر النائبة بعد طي بنسبة %1$d%% - يتجاوز وضع إفلات السحب العتبات وسلوك الإغلاق. يحدث التبديل فقط عندما تنتهي إيماءة سحب اللوحة. - جعل العناصر النائبة شفافة - تحتفظ العناصر النائبة بمساحة تخطيطها ولكن تصبح غير مرئية. - الجودة البصرية - دقة غلاف الألبوم - ميزات تجريبية - منخفضة (256 بكسل) - أداء أفضل - متوسطة (512 بكسل) - متوازنة - عالية (800 بكسل) - جودة أفضل - الأصلية - الجودة القصوى - - %1$d%% - %1$s • %2$s - · %1$s - \? - - تسجيل الدخول إلى Telegram - أنت تقوم بتعديل رقمك الآن. إرسال الرمز مجدداً سيحل محل الرمز السابق. - جاري العمل… - جاري تهيئة Telegram… - جاري تسجيل الخروج… - جاري إغلاق الجلسة… - تم إغلاق الجلسة. أعد فتح تسجيل الدخول للمتابعة. - جاري تحضير جلسة Telegram آمنة… - بانتظار استجابة Telegram… - ربط Telegram - قم بربط حساب Telegram لبث الموسيقى مباشرة من قنواتك ومحادثاتك. - رقم الهاتف - أدخل رقم Telegram الخاص بك. يمكنك العودة وتعديله لاحقاً. - رقم الهاتف - 1 - 5551234567 - إرسال الرمز - رمز التحقق - أدخل الرمز الذي وصلك من Telegram. إذا كان الرقم خاطئاً، عد للخلف لتعديله. - الرمز - 12345 - تعديل الهاتف - إعادة إرسال الرمز - التحقق من الرمز - التحقق بخطوتين (كلمة المرور) - أدخل كلمة مرور Telegram الخاصة بك. لا يزال بإمكانك العودة لتصحيح رقمك. - كلمة المرور - التحقق من كلمة المرور - يرجى الانتظار… - - قنوات Telegram - إضافة قناة - قناة Telegram عامة - جاري المزامنة - المزامنة الآن - طي المواضيع - إظهار المواضيع - خيارات القناة - المواضيع - جاري مزامنة القناة - جاري تحديث الأغاني من Telegram - جلب أحدث الأغاني من هذه القناة - إزالة القناة - إيقاف المزامنة وحذف الأغاني المخزنة مؤقتاً - حذف القناة؟ - ستتوقف مزامنة %1$s وسيتم حذف جميع الأغاني المخزنة مؤقتاً من هذه القناة. - إزالة - لم يتم مزامنة أي قنوات بعد - أضف قنوات Telegram عامة لمزامنة\nمكتبتك الموسيقية - إضافة قناة - لم تُزامن مطلقاً - تمت المزامنة %1$s - - إضافة قناة - ابحث عن قناة Telegram عامة لمزامنة موسيقاها - اسم_القناة@ أو الرابط - بحث - جاري البحث… - البحث عن قناة - أدخل اسم المستخدم لقناة عامة أو الرابط الخاص بها\nلمزامنة ملفاتها الصوتية - تم - - - لا توجد أغانٍ - أغنية واحدة (%d) - أغنيتان (%d) - %d أغانٍ - %d أغنية - %d أغنية - - - لا توجد مواضيع - موضوع واحد (%d) - موضوعان (%d) - %d مواضيع - %d موضوعاً - %d موضوع - - diff --git a/app/src/main/res/values-ar/strings_presentation_batch_g.xml b/app/src/main/res/values-ar/strings_presentation_batch_g.xml deleted file mode 100644 index 579f9860e..000000000 --- a/app/src/main/res/values-ar/strings_presentation_batch_g.xml +++ /dev/null @@ -1,636 +0,0 @@ - - - - اليوم - الأسبوع الحالي - الشهر الحالي - السنة الحالية - كل الأوقات - إحصائيات الاستماع - تحديث إحصائيات الاستماع - الاستماع - مرات التشغيل - - عادات الاستماع - لا توجد عادات استماع بعد - سنقوم بإظهار عادات الاستماع الخاصة بك بمجرد أن نتعرف على ذوقك بشكل أفضل. - إجمالي الجلسات - معدل الجلسة - أطول جلسة - جلسة/يوم - اليوم الأكثر نشاطاً - لم يتم التشغيل بعد - فترة الذروة الزمنية - وقت الاستماع - إجمالي وقت الاستماع الذي تم تسجيله في النطاق المحدد. - عدد مرات التشغيل - عدد الجلسات التي أكملتها لكل شريحة زمنية. - معدل الجلسة - متوسط مدة الاستماع لكل شريحة زمنية. - %1$d تشغيل - الخط الزمني للاستماع - لا توجد بيانات استماع بعد - اضغط على زر التشغيل لبدء بناء خطك الزمني للاستماع - الإيقاع اليومي - الإيقاع الأسبوعي - الإيقاع الشهري - نظرة عامة على السنة - التطور على مر الوقت - مجمعة في شرائح مدتها 4 ساعات - مجمعة حسب أيام الأسبوع - مجمعة حسب أسبوع الشهر - مجمعة حسب الشهر - مجمعة حسب السنة - شريحة الذروة - مقسمة إلى فترات مدتها 4 ساعات للكشف عن إيقاعك اليومي. - تسهل الأشرطة اليومية مقارنة عادات الاستماع من أسبوع لآخر. - توضح الأشرطة الأسبوعية اتجاهات الشهر وتطورها. - تظهر الأشرطة الشهرية التغيرات الموسمية على مدار السنة. - تختصر الأشرطة السنوية كامل تاريخ الاستماع الخاص بك. - الفئات الأعلى - قارن بين طرق استماعك عبر الأنواع الموسيقية، الفنانين، الألبومات، والأغاني. - %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 - \? - - - معلومات الجهاز - برامج ترميز الصوت المدعومة (Codecs) - مخرج الصوت - محرك ExoPlayer - معدل العينة - الإطارات لكل مخزن مؤقت - دعم زمن الانتقال المنخفض - دعم الصوت الاحترافي (Pro Audio) - الإصدار - المصّيرات النشطة - عدادات فك الترميز - %1$d هرتز - نعم - لا - مسرع بواسطة الأجهزة - الشركة المصنعة - الموديل - العلامة التجارية - الجهاز - إصدار أندرويد - إصدار SDK - المكونات المادية (Hardware) - - - هذا الجهاز - -- - جاهز للتشغيل - التشغيل يتطلب مراجعة - التنسيقات - أجهزة فك الترميز المادية - الأغاني المحلية - مساحة تخزين الموسيقى المحلية - حجم الموسيقى - %1$d أغنية محلية - المتاح - الإجمالي %1$s - البصمة التخزينية للموسيقى - المستخدم من الجهاز - %1$d%% - <1% - %1$d أغنية سحابية - %1$d ملف غير قابل للقراءة - مسار التشغيل - %1$d إطار لكل مخزن مؤقت - Hi-Fi PCM Float - مسار مخرج 32-بت عائم - الذاكرة - متاح من أصل %1$s - التنسيقات الجاهزة للإرسال المباشر (Offload) - لم تبلغ أي تنسيقات مضغوطة عن دعم ميزة الـ hardware offload. - المخارج المكتشفة - لم يتم الإبلاغ عن أي مسارات إخراج بواسطة أندرويد. - %1$s مصّيرات - توافق التنسيقات - %1$d مسار مدعوم - %1$d تنسيق غير معروف - لم يتم الإبلاغ عن برنامج فك ترميز - فك ترميز مادي (Hardware) - فك ترميز برمجى (Software) - إرسال مباشر (Offload) - %1$d في المكتبة - تقرير الأداء - قم بإنشاء تقرير تشخيصي قابل للمشاركة لمساعدتنا في تصنيف مشكلات بطء التشغيل أو الفحص. يحتوي التقرير فقط على بيانات الجهاز، المكتبة، والتوقيت — لا يتضمن مسارات ملفات أو عناوين أو فنانين. - إنشاء التقرير - إعادة إنشاء - نسخ - مشاركة - تم نسخ التقرير إلى الحافظة - تقرير أداء PixelPlay - نتائج التوافق - لا توجد حالات عدم توافق رئيسية - تتطابق مساراتك المفهرسة مع برامج فك الترميز التي يبلغ عنها نظام أندرويد في هذا الجهاز. - قد لا يتم فك ترميز %1$d مسار بشكل أصلي - التنسيقات التي تحتاج لمراجعة: %1$s. - قد يتم إعادة عينة %1$d مسار محلي - تصل المكتبة إلى %1$d هرتز، وهو أعلى من معدل عينة المخرج الحالي. - تمتلك %1$d مسارات بيانات وصفية غير معروفة - يمكن لإعادة فحص المكتبة بالكامل ملء بيانات MIME ومعدل البت ومعدل العينة المفقودة. - +%1$d أكثر - المكبر المدمج - صوت البلوتوث - صوت USB - سماعة سلكية - مخرج رقمي - مخرج آخر - - - الإدخال (Input) - الإخراج (Output) - التفكير (Thought) - %1$s: %2$s - MMM dd، HH:mm - تحليل الفنانين المتعددين - محددات الرموز - الحالي: %1$s - محددات الكلمات - لا يوجد - الحالي: %1$s - - تكوين - استخراج الفنانين من العنوان - اكتشاف عبارات .feat و .ft و with في عناوين الأغاني - تنظيم المكتبة - التجميع حسب فنان الألبوم - إظهار ألبومات العمل المشترك تحت اسم الفنان الرئيسي - حول تحليل الفنانين المتعددين - يقوم PixelPlayer بفصل علامات الفنانين باستخدام محددات الرموز مثل (/, ;, &) ومحددات الكلمات مثل (feat., ft., vs., x). يتم مطابقة محددات الكلمات دون الحساسية لحالة الأحرف.\n\nتكتشف ميزة "استخراج الفنانين من العنوان" الأنماط مثل (feat. Artist) في عناوين الأغاني.\n\nيمكن استخدام الشرطة المائلة الخلفية (\\) لتخطي محددات الرموز. - - أمثلة - \"Artist1/Artist2\" - Artist1، Artist2 - \"Drake feat. Rihanna\" - Drake، Rihanna - \"Marshmello x Bastille\" - Marshmello، Bastille - \"Song (ft. B)\" بواسطة A - A، B - \"AC\\DC\" - AC/DC (تم تخطي المحدد) - الفنانون - إعادة الفحص مطلوبة - تغيرت إعدادات الفنانين. أعد فحص مكتبتك لتطبيق التغييرات. - جاري الفحص… - إعادة الفحص - - - β - تجريبي (Beta) - تليجرام - سجل التغييرات - الإعدادات - متزامنة - ثابتة - خيارات كلمات الأغاني - البث السحابي - بث الموسيقى مباشرة من حساباتك السحابية - المصدر - الترتيب - تنازلي - تصاعدي - الترتيب الأصلي - اضغط للتبديل إلى التصاعدي - اضغط للتبديل إلى التنازلي - هذا الفرز يحافظ على ترتيبه الأصلي - المفتاح مفعل - - - إغلاق - تحديث - تم - تم - كل شيء مسموح به افتراضياً. اضغط مطولاً على أي مجلد لتمييزه كـ مستبعد من الفحص. - لا توجد مجلدات فرعية هنا - الانتقال للأعلى - الانتقال إلى الدليل الرئيسي - - - المزيج اليومي (Daily Mix) - المزيج اليومي - بناءً على تاريخ الاستماع - تحقق من كامل المزيج اليومي - أغنية محددة - أغنية محددة - مشاركة المحدد - إعجاب بالمحدد - تشغيل - الكل - إلغاء تحديد الكل - خيارات إضافية - خيارات - +%1$d - %1$s • %2$s - محدد - خيارات إضافية لـ %1$s - غلاف الألبوم لـ %1$s - جاري التشغيل - %1$d%% - - - إحصائيات الاستماع - إجمالي التشغيل - المعدل يومياً - المسار الأعلى - %1$s • %2$d تشغيل - المشغلة حديثاً - −.٥ - −.١ - +.١ - +.٥ - ٠ ثانية - %1$+.1f ث - - - فتح متجر Play - متابعة النسخة التجريبية - سيتم تفعيل رابط متجر Play من تكوين GitHub. - PixelPlayer متاح الآن على Google Play - استخدم القناة المستقرة على Google Play للحصول على التحديثات الرسمية بينما نبقي البناء التجريبي نشطاً. - PixelPlayer - إعلان الإصدار - قريباً - - - فرز وتشغيل - خلط عشوائي - فرز حسب - الفنان - الألبوم - العنوان - محدد - سجل التغييرات - عرض على GitHub - التفضيلات المسبقة المحفوظة - لم يتم حفظ تفضيلات مخصصة بعد. - إلغاء التثبيت - تثبيت - إعادة تسمية - حذف - - - الإصدار التجريبي 0.7.0 - مرحباً بك في PixelPlayer 0.7.0-beta - أنت تستخدم بناءً تجريبياً قد يحتوي على أخطاء، أو حالات توقف مفاجئ، أو ميزات تجريبية. ساعدنا في التحسين من خلال الإبلاغ عن المشكلات. - ماذا تتوقع - قد تحدث أخطاء، توقفات مفاجئة، أو ميزات غير مكتملة بشكل غير متوقع. - بعض الميزات قد تتغير أو تُزال دون إشعار مسبق. - قد تكون النسخ التجريبية غير مستقرة مقارنة بالإصدارات الرسمية. - تحقق دائماً من التحديثات قبل الإبلاغ عن مشكلة معروفة. - ما يمكن أن تغيره، تعلبه أو تحسنه النسخ التجريبية أثناء الاختبار. - اختصار مشكلات GitHub - ابحث أولاً، ثم افتح تقريراً مركزاً للأخطاء، التوقفات المفاجئة، الطلبات، أو الاستفسارات. - فتح المشكلات الحالية - الإبلاغ عن مشكلة أو توقف مفاجئ - شاركنا خطوات إعادة إنتاج المشكلة، النتائج المتوقعة، النتائج الفعلية، وتفاصيل جهازك/نظام التشغيل. - كيفية الإبلاغ - قائمة مراجعة سريعة قبل فتح تذكرة مشكلة جديدة. - قبل فتح تذكرة مشكلة - ابحث في المشكلات المفتوحة والمغلقة الحالية لتجنب التكرار. - حدث إلى آخر إصدار من PixelPlayer وتأكد من استمرار حدوث المشكلة. - أعد تشغيل التطبيق وتأكد من بقاء المشكلة قائمّة. - حاول تكرار حدوث المشكلة واكتب الخطوات الدقيقة لذلك. - ما هو نوع المشكلة؟ - تقرير خطأ برمي (Bug): شيء ما يتصرف بشكل غير صحيح. - طلب ميزة: إضافة ميزة جديدة أو تحسين. - سؤال: استخدم قسم المناقشات إذا كان مفعلاً، أو افتح تذكرة بعلامة سؤال. - تقرير خطأ برمجى - انسخ هذه الحقول عندما يتصرف شيء ما بشكل غير صحيح أو يتوقف فجأة. - تقرير خطأ - ملخص قصير: - السلوك المتوقع: - السلوك الحالي: - خطوات التشغيل/إعادة الإنتاج: 1. 2. 3. - كم مرة يحدث ذلك؟ دائماً / أحياناً / نادراً. - لقطة شاشة / فيديو: إن وجد. - السجلات / تتبع الكومة (Stack trace): إن وجد. - البيئة البرمجية - إصدار PixelPlayer: - مصدر التثبيت: إصدار GitHub، بناء تصحيح خطأ، بناء ليلي، إلخ. - إصدار أندرويد: - موديل الجهاز: - سياق إضافي: استخدام بطاقة SD، إعدادات خاصة، أذونات، إلخ. - طلب ميزة جديد - انسخ هذه الحقول عندما ترغب في طلب ميزة جديدة أو تحسين. - بيان المشكلة: ما هي المشكلة التي تحاول حلها؟ - الحل المقترح: كيف يجب أن تعمل الميزة؟ - البدائل المدروسة: هل توجد أي مقاربات أخرى؟ - النطاق: ما هي الشاشات أو التدفقات المتأثرة؟ - نموذج مبدئي (Mockup) أو صورة مرجعية إن وجدت. - العناوين، الخصوصية والنطاق - اجعل التقرير سهلاً للفرز وآمناً للمشاركة. - عناوين جيدة للمشكلات - معادل الصوت: مؤشر الإزاحة يتغير عند تبديل تبويب التفضيلات - البحث: قائمة السجل لا تظهر عند الاستعلام الفارغ - ميزة: إضافة خيار فرز قائمة التشغيل حسب "المضافة حديثاً" - يرجى تجنب - التقارير العامة مثل "إنه لا يعمل". - جمع مشكلات متعددة غير مترابطة في تذكرة واحدة. - السجلات أو لقطات الشاشة غير المظللة التي تحتوي على بيانات خاصة. - ملاحظة الخصوصية - قبل نشر السجلات، لقطات الشاشة، أو الفيديوهات، قم بإزالة أي معلومات شخصية أو خاصة. - - - البناء الليلي (Nightly builds) - كيف تختلف البناءات الليلية عن الإصدارات الرسمية، وماذا تضمن عندما تتعطل. - يتم إنشاء البناءات الليلية من آخر التزامات برمجية (Commit)، وقد تحتوي على تغييرات غير مكتملة، أخطاء مؤقتة، أو تراجعات في الأداء. إنها تجريبية أكثر من الإصدارات الرسمية. - يمكنك الوصول إليها من ملحقات سير عمل GitHub Actions الخاصة بالمستودع إن وجدت. - الإبلاغ عن مشكلات البناء الليلي - عند الإبلاغ عن مشكلة من بناء ليلي، اذكر دائماً أن ذلك حدث في نسخة ليلية وليس في إصدار رسمي. يرجى تضمين تاريخ البناء، اسم أو رقم تشغيل سير العمل، أو معرف الالتزام (Commit SHA) إن أمكن. وتحقق أيضاً مما إذا كانت نفس المشكلة تحدث في أحدث إصدار رسمي. - التحديث إلى Beta 0.5.0 - يُوصى بتثبيت نظيف - إذا كنت قادماً من الإصدار التجريبي 0.5.0، فقد يتطلب هذا التحديث بيانات مكتبة جديدة بدلاً من الحالة القديمة المخزنة مؤقتاً. - إذا بدت البيانات الوصفية أو إدخالات المكتبة خاطئة - البيانات الوصفية الخاطئة للأغاني، أو عدم تطابق الفنانين أو الألبومات، أو الإدخالات التي تبدو مكررة تعني عادةً أن التثبيت النظيف هو الحل المناسب. - لا تظهر هذا مجدداً - فهمت ذلك - - - %1$d ألبومات - محدد - ميزة (إضافة للقائمة وتشغيل) تحترم ترتيب تحديدك تماماً. - الحد الأقصى: %1$d ألبومات لكل تحديد. - إضافة إلى قائمة الانتظار وتشغيل - PixelPlayer - مشغل موسيقى - أعلى %1$d - إغلاق - النتيجة - المستوى %1$d - القلوب - اكتمل المستوى! - انتهت اللعبة - النتيجة: %1$d - المحاولة مجدداً؟ - المستوى التالي - إعادة تشغيل اللعبة - اضغط لإعادة الإطلاق - تشغيل موسيقى عشوائية - كسارة الطوب - أعلى نتيجة %1$d - لعب - اسحب لتحريك المضرب - استعادة الوحدات - جاري الاستعادة - استعادة المحدد - تفاصيل النسخة الاحتياطية - تم الإنشاء - إصدار التطبيق - المخطط (Schema) - الجهاز - غير معروف - تم تحديد %1$d من أصل %2$d وحدة - النقل جارٍ الآن… - تحديد الكل - مسح التحديد - %1$d إدخالات · سوف تستبدل البيانات الحالية - - - بث سحابي - طي المشغل - بث بـ (Cast) - بلوتوث - تشغيل محلي - جاري الاتصال… - قائمة الانتظار - كلمات الأغاني - جلسة بث - جاري الاتصال - متصل - هذا الهاتف - صوت البلوتوث - تشغيل محلي - جاري التشغيل - موقوف مؤقتاً - استعد للاتصال - اسمح لـ PixelPlayer برؤية أجهزتك القريبة وشبكة الـ Wi-Fi الحالية حتى نتمكن من إبقاء البث وصوت البلوتوث ومكبرات الصوت متزامنة. - الأجهزة القريبة - مطلوب لقراءة والتحكم في معدات صوت البلوتوث المتصلة. - الموقع لشبكة الـ Wi‑Fi - يتطلب نظام أندرويد إذن الموقع لمشاركة شبكة الـ Wi-Fi الحالية (SSID) حتى نتمكن من العثور على أجهزة البث المتوافقة. - السماح بالوصول - نحن نستخدم هذه الأذونات فقط لربط الأجهزة — البث، والتحكم في مكبرات الصوت القريبة، وإبقاء الصوت متزامناً. - توصيل الجهاز - جاري الفحص بالقرب منك - عناصر التحكم - الأجهزة - الاتصالية - تشغيل الـ Wi-Fi أو البلوتوث - إدارة الشبكات النشطة وإعادة الفحص - تحديث الاتصالات - تحديث الأجهزة - الأجهزة القريبة - الأجهزة القريبة - مطلوب لاكتشاف والتحكم في أجهزة صوت البلوتوث المتصلة. - اضغط للاتصال - لا توجد أجهزة بعد - إلغاء الاتصال - مستوى صوت الجهاز - مستوى صوت الهاتف - جاري البحث عن أجهزة… - تأكد من أن التلفزيون أو مكبر الصوت قيد التشغيل ومتصل بنفس شبكة الـ Wi‑Fi. - متصل - متاح للاتصال - جاري الاتصال - متاح - مستوى البطارية - مستوى الصوت - Wi-Fi - متوقف - متصل - يعمل - بلوتوث - متصل - يعمل - متوقف - الاتصالات متوقفة - قم بتشغيل الـ Wi‑Fi أو البلوتوث لاكتشاف الأجهزة القريبة - تشغيل الـ Wi‑Fi - فتح البلوتوث - إلغاء الاتصال - جاري الاتصال... - - - أبرز الميزات - التحسينات - الإصلاحات - ما الجديد - ما الجديد - تمت إضافة - تغيير - تم إصلاح - - - دعم Android Auto متاح الآن للتشغيل داخل السيارة. - دعم Wear OS بات نشطاً، بما في ذلك عناصر تحكم أفضل للتشغيل من الساعة إلى الهاتف. - توسيع التكامل السحابي مع تحسينات لـ Telegram و NetEase و QQ Music و Google Drive. - ميزتا "المشغلة حديثاً" واستعادة قائمة الانتظار الدائمة تبقيان جلسة استماعك جاهزة. - تم تضمين ميزات النسخ الاحتياطي والاستعادة v3 وأدوات إدارة الحساب. - أصبحت كلمات الأغاني أكثر ذكاءً مع دعم البحث اليدوي الاحتياطي وتحسينات التخزين. - - - تحديث شامل للأداء عبر بدء التشغيل، المكتبة، قائمة الانتظار، وتفاعلات المشغل. - إعادة تصميم واجهات المشغل، البث، الكلمات، الفنان، والنوع لتوفير استخدام أكثر سلاسة. - أصبحت تدفقات التنقل والبحث أكثر موثوقية مع معالجة أكثر أماناً للمسارات. - تحسين توافق تشغيل الصوت لمزيد من الأجهزة والتنسيقات. - توسيع سير عمل التحديد المتعدد عبر الأغاني والألبومات وقوائم التشغيل. - - - أصبح سلوك قائمة الانتظار والخلط العشوائي أكثر استقراراً وقابلية للتنبؤ. - إصلاح العديد من الحالات النادرة في التشغيل الخلفي وبث الصوت (Casting). - إصلاح مشكلات مؤقت النوم، والتنقل في تبويب الملفات، وحالات توقف فنان الألبوم المفاجئ. - تحسين تحميل الويدجت واستقرار الخدمة لتقليل مشكلات الحرارة والذاكرة. - إصلاحات عامة للأخطاء وتحسينات جمالية لواجهة مستخدم التطبيق. - - - تحديث واجهة المستخدم التعبيرية Material 3 Expressive - معادل صوتي ذو 10 نطاقات وتأثيرات صوتية - تدفق مزامنة جديد للمكتبة الموسيقية - التكامل مع الذكاء الاصطناعي (نماذج Gemini) - استيراد وتصدير قوائم التشغيل بصيغة M3U - تكامل أغلفة الفنانين من منصة Deezer - أغلفة مخصصة لقوائم التشغيل - - - إعادة هيكلة معمارية الإعدادات - رسوم متحركة جديدة لقائمة الانتظار والمشغل - ملفات التعريف الأساسية (Baseline Profiles) وتحسين الأداء - نظام أفضل لكلمات الأغاني مع إزاحة التزامن - - - تحسينات استقرار بث الصوت (Casting) - استقرار لوحة المشغل السفلية - إصلاحات عامة للأخطاء وتنظيف الكود - - - إعادة تصميم كبرى لنظام التنقل - مستكشف ملفات جديد لاختيار مجلدات المصدر - وظائف اتصال وبث جديدة - استمرارية سلسة بين الأجهزة عن بعد - انتقال بدون فجوات (Gapless) بين الأغاني - عنصر التحكم في التلاشي المتبادل (Crossfade) - ميزة الانتقالات المخصصة الجديدة (لقوائم التشغيل فقط) - استمرار التشغيل بعد إغلاق التطبيق - تحسينات واجهة المستخدم - ميزة إحصائيات محسنة - إعادة تصميم التحكم في قائمة الانتظار مع المزيد من الميزات - تحسين دعم أنواع الملفات المختلفة للتشغيل وتعديل البيانات الوصفية - تحسين متحكم الأذونات - إصلاحات طفيفة للأخطاء - - - تقديم مركز إحصائيات استماع أكثر ثراءً مع رؤى عميقة لجلساتك. - إطلاق مشغل سريع عائم لفتح ومعاينة الملفات المحلية على الفور. - إضافة تبويب المجلدات مع مستكشف بنمط شجري وعرض جاهز لقوائم التشغيل. - - - تحسين واجهة Material 3 بالكامل لتوفير تجربة أنظف وأكثر تماسكاً. - تحرير البيانات الوصفية يدعم الآن تغيير غلاف الألبوم. - تنعيم الرسوم المتحركة والانتقالات عبر التطبيق لتنقل أكثر انسيابية. - تحسين تخطيط شاشة الفنان مع تفاصيل أكثر ثراءً ولمسات جمالية. - ترقية توليد DailyMix و YourMix باختيارات أكثر ذكاءً وتنوعاً. - تعزيز توليد قوائم التشغيل بواسطة الذكاء الاصطناعي. - تحسين صلة نتائج البحث وعرضها لاكتشاف أسرع. - توسيع الدعم لنطاق أوسع من تنسيقات الملفات الصوتية. - - - حل مشكلات البيانات الوصفية الغريبة لتبقى تفاصيل الأغاني دقيقة في كل مكان. - استعادة اختصارات الإشعارات لتعود بشكل موثوق إلى شاشة التشغيل. - - - دعم Chromecast لبث الصوت من جهازك. - سجل التغييرات داخل التطبيق لإبقائك على اطلاع بآخر الميزات. - دعم ملفات LRC، سواء كانت مدمجة أو خارجية. - دعم كلمات الأغاني دون اتصال بالإنترنت. - كلمات أغاني متزامنة (متطابقة مع الأغنية). - شاشة جديدة لعرض كامل قائمة الانتظار. - إعادة ترتيب وإزالة الأغاني من قائمة الانتظار. - إيماءات المشغل المصغر (السحب للأسفل للإغلاق). - إضافة المزيد من رسوم Material المتحركة. - إعدادات جديدة لتخصيص المظهر والإحساس العام. - إعدادات جديدة لمسح ذاكرة التخزين المؤقت. - - - إعادة تصميم كاملة لواجهة المستخدم. - إعادة تصميم كاملة للمشغل. - تحسينات الأداء في المكتبة الموسيقية. - تحسين سرعة تشغيل التطبيق عند البدء. - الذكاء الاصطناعي يقدم الآن نتائج أفضل. - - - إصلاح أخطاء مختلفة في محرر العلامات (Tags). - إصلاح مشكلة عدم اختفاء إشعار التشغيل. - إصلاح عدة أخطاء كانت تتسبب في توقف التطبيق فجأة. - - - Wear OS: نقل الموسيقى، التشغيل المحلي، مزامنة قائمة الانتظار، والتحكم عن بعد من الساعة. - الذكاء الاصطناعي: تكامل Groq AI و OpenRouter (تجريبي) مع تحسين استهلاك الرموز (Tokens). - السحاب: إضافة دعم Jellyfin. - كلمات الأغاني: ترجمة متزامنة مع مفتاح تبديل مخصص، دعم تنسيق Kugou LRC، تخصيص محاذاة النص، وتحسين التحميل عن بعد. - واجهة المستخدم/تجربة المستخدم: وضع شريط التنقل المدمج، سمات ديناميكية من لوحة ألوان غلاف الألبوم، نص متحرك (Marquee) للعناوين الطويلة، وخيارات فرز جديدة. - تليجرام: دعم أصلي للمواضيع (Topics) وأنماط عرض محسنة. - - - المحرك الصوتي: إصلاح شامل مع دعم المزيد من التنسيقات (MIDI, ALAC, M4A) وتحسين برنامج فك الترميز. - الكفاءة: تقليل جذري في استهلاك الطاقة، إصلاحات للحرارة الزائدة، وتحسين المهام الخلفية (SyncWorker). - قاعدة البيانات: تحسينات هائلة على الاستعلامات وإعادة تصميم ذاكرة التخزين المؤقت للأغلفة لمنع فقدان البيانات. - بدء التشغيل: تحسين وقت التحميل عبر تهيئة Baseline Profile. - - - التتشغيل: إصلاح التقطع في Opus/MP3، أخطاء ReplayGain أثناء التلاشي المتبادل، ومشكلات بدء التشغيل على مفككات ترميز Samsung. - الاستقرار: القضاء على حالات التوقف المفاجئ عند البدء، وأثناء التنقل بين الفنانين، وعلى أجهزة أندرويد 12+. - واجهة المستخدم: إصلاح وميض الأغلفة، وتداخل النصوص في النصوص غير اللاتينية، وسلوك شريط التنقل/المشغل المصغر. - الأمان: تعزيز التعامل مع بيانات الاعتماد، أذونات التخزين، والاتصال بخادم الوسائط. - - - العربية - Spanish - French - Russian - Simplified Chinese - Indonesian - Italian - - diff --git a/app/src/main/res/values-ar/strings_screens.xml b/app/src/main/res/values-ar/strings_screens.xml deleted file mode 100644 index 3570cde6d..000000000 --- a/app/src/main/res/values-ar/strings_screens.xml +++ /dev/null @@ -1,252 +0,0 @@ - - - - خطأ: معرف النوع (Genre ID) مفقود - شكراً لك على استخدام PixelPlayer! - - - محددات الكلمات الحالية - هذه الكلمات المفتاحية تفصل أسماء الفنانين عندما تكون محاطة بمسافات. يتم مطابقتها دون تفرقة بين الأحرف الكبيرة والصغيرة. اضغط للحذف. - لم يتم تهيئة أي محددات كلمات - إضافة محدد كلمات جديد - مثال: .feat أو .ft - كيف تعمل محددات الكلمات - يتم مطابقة محددات الكلمات دون تفرقة بين الأحرف الكبيرة والصغيرة مع وجود مسافات حولها.\n\nالمحددات المكونة من حرف واحد (مثل \"x\") تتطلب مسافات من كلا الجانبين لتجنب المطابقات الخاطئة.\n\nأمثلة:\n \"Drake feat. Rihanna\" -> Drake, Rihanna\n \"Marshmello x Bastille\" -> Marshmello, Bastille\n \"A vs. B\" -> A, B - محددات الكلمات - إعادة تعيين محددات الكلمات؟ - سيؤدي هذا إلى مسح جميع محددات الكلمات المخصصة واستعادة الكلمات المفتاحية الافتراضية. لا يمكن التراجع عن هذا الإجراء. - تمت إضافة محدد الكلمات - موجود بالفعل أو غير صالح - تمت إعادة تعيين محددات الكلمات إلى الافتراضية - إعادة تعيين - - - المحددات الحالية - انقر على محدد لإزالته. يلزم وجود محدد واحد على الأقل. - إضافة محدد جديد - مثال: / أو ; - المحددات الافتراضية - إعادة تعيين المحددات؟ - سيؤدي هذا إلى مسح جميع المحددات المخصصة واستعادة المحددات الافتراضية. لا يمكن التراجع عن هذا الإجراء. - تمت إعادة تعيين المحددات إلى الافتراضية - يلزم وجود محدد واحد على الأقل - تمت إضافة المحدد - المحدد موجود بالفعل أو غير صالح - المحددات - مسافة - إضافة محدد - - - خدمة Google Drive قادمة قريباً. - تعذر فتح هذه الشاشة في الوقت الحالي. - - - مرحباً بك في - β - تجريبي (Beta) - دعنا نقوم بإعداد كل شيء من أجلك. - جاري التحقق من حزمة النسخة الاحتياطية… - مظهر التطبيق - اختر المظهر الذي تريده قبل البدء في استكشاف مكتبتك. - يمكنك تغيير هذا لاحقاً من الإعدادات > المظهر > مظهر التطبيق. - موصى به - تخطيط المكتبة - اختر الطريقة المفضلّة لديك للتنقل في مكتبتك. - الوضع المدمج - يمكنك تغيير هذا لاحقاً من الإعدادات > المظهر > التنقل في المكتبة. - المكتبة - الأغاني - الألبومات - الفنانون - كل شيء جاهز! - أنت مستعد الآن للاستمتاع بموسيقاك. - استعادة النسخة الاحتياطية - راجع ما تريد استيراده قبل إنهاء الإعداد. - تم تحديد %1$d من أصل %2$d من الوحدات - تم الإنشاء في %1$s - نسخة احتياطية من الإصدار %1$s - إصدار غير معروف - هيا بنا! - الخطوة %1$d من أصل %2$d - التنقل داخل التطبيق - اختر نمط شريط التنقل السفلي. - النمط الافتراضي - يمكنك تغيير هذا لاحقاً من الإعدادات > المظهر > نمط شريط التنقل. - تخطي في الوقت الحالي - تخطي / ليس الآن - جاري الاستعادة - استعادة المحدد - تخصيص نصف قطر الزوايا - يرجى منح الإذن المطلوب أولاً. - يرجى منح جميع الأذونات المطلوبة. - يرجى منح أذونات التخزين أولاً - تعذر فتح إعدادات البطارية - - - توسيع القائمة - التالي - إنهاء - إغلاق - إزالة - إضافة محدد كلمات - إعادة تعيين الافتراضيات - - - المجلدات المستبعدة - يتم فحص جميع المجلدات افتراضياً. اختر أي مواقع تريد تجاهلها عند بناء مكتبتك. - اختر المجلدات لتجاهلها - إذن الوصول إلى الوسائط - يحتاج PixelPlayer إلى الوصول إلى ملفاتك الصوتية لبناء مكتبتك الموسيقية. - تم منح الإذن - منح إذن الوسائط - الإشعارات - قم بتمكين الإشعارات للتحكم في موسيقاك من شاشة القفل ولوحة الإشعارات. - تمكين الإشعارات - التنبيهات والتذكيرات - اختياري، ولكن موصى به إذا كنت تستخدم مؤقت النوم وتريد أن يقوم PixelPlayer بإيقاف التشغيل في الوقت المحدد تماماً. - منح الإذن - هل لديك نسخة احتياطية؟ - إذا كان لديك نسخة احتياطية من PixelPlayer بالفعل، فاستعدها الآن لتخطي معظم خطوات الإعداد المتبقية على هذا الجهاز. - جاري فحص النسخة الاحتياطية - جاري استعادة النسخة الاحتياطية - استيراد نسخة احتياطية - داكن - المظهر الداكن الافتراضي لـ Material 3 في تطبيق PixelPlayer. - فاتح - مظهر Material 3 أكثر سطوعاً في جميع أنحاء التطبيق. - تتبع النظام - مطابقة إعداد المظهر الحالي لهاتفك. - يتم استخدام شريط التنقل الكبسولة المدمج - يتم استخدام صف علامات التبويب القياسي - الأغاني - تحسين استهلاك البطارية - تقوم بعض أجهزة Android بإغلاق تطبيقات الخلفية بشكل حاد. قم بتعطيل تحسين البطارية لتطبيق PixelPlayer لمنع انقطاع التشغيل غير المتوقع. - تعطيل التحسين - شريط كبسولة عائم بزوايا مستديرة - شريط قياسي بالعرض الكامل - - - حذف الأغنية؟ - \"%1$s\" بواسطة %2$s\n\nسيتم حذف هذه الأغنية نهائياً من جهازك ولا يمكن استعادتها. - - - المزيج\nالخاص بك - لا توجد بيانات لعرضها بعد - سيظهر المزيج الخاص بك هنا عندما يجد PixelPlayer أغانٍ أو يقوم بمزامنة أحد المصادر. - تحديث - تشغيل عشوائي - غلاف ألبوم لـ %1$s - خيارات - ملء سريع للنوع - فنان عام - تشغيل الألبوم - تشغيل الألبوم عشوائياً - غلاف %1$s - %1$s · %2$s - تشغيل/إيقاف مؤقت - غلاف الأغنية - - - عذراً! حدث خطأ ما - تعطل التطبيق خلال جلستك الأخيرة. ساعدنا في إصلاح هذا من خلال مشاركة تقرير التعطل. - التاريخ: %1$s - الخطأ: - تتبع الكومة (معاينة): - سجل التعطل - تم نسخ سجل التعطل إلى الحافظة - تقرير تعطل PixelPlayer - مشاركة تقرير التعطل - نسخ - مشاركة - - - بحث… - بحث - مسح البحث - عمليات البحث الأخيرة - مسح الكل - السجل - حذف عنصر من سجل البحث - لا توجد نتائج - لا توجد نتائج لـ \"%1$s\" - لم يتم العثور على شيء - جرّب مصطلح بحث آخر أو تحقق من الفلاتر الخاصة بك. - لم يتم العثور على نتائج. - - - تصفح حسب النوع - لا توجد أنواع متاحة. - - - لم يتم العثور على مساهمين حالياً. يرجى المحاولة مرة أخرى لاحقاً. - PixelPlayer - مشغل موسيقى مفتوح المصدر تم بناؤه مع مجتمعه. - الإصدار v%1$s - %1$d مساهمة - حول التطبيق - المشرف الرئيسي - الشخص الذي يقف وراء PixelPlayer. - أضواء على المجتمع - تقدير وتكريم للمتعاونين ذوي التأثير الكبير. - المساهمون في المشروع مفتوح المصدر - قائمة المساهمين المباشرة من GitHub. - مفتوح المصدر - المجتمع أولاً - تصميم Material 3 معبر - فتح ملف GitHub الشخصي - فتح Telegram - الصورة الشخصية لـ %1$s - أيقونة %1$s - - - Subsonic - تم مزامنة %1$d قائمة تشغيل - تم مزامنة %1$d مجلد - قوائم التشغيل - مجلدات الموسيقى - مزامنة - لم يتم مزامنة أي قوائم تشغيل بعد - اضغط على مزامنة لجلب قوائم التشغيل الخاصة بك - اضغط على مزامنة لجلب قوائم تشغيل Jellyfin الخاصة بك - لم يتم إضافة مجلدات بعد - انقر على + لإضافة مجلد من Drive - إجراءات سريعة - إدارة خوادم Navidrome وAirsonic والخوادم الأخرى المتوافقة مع Subsonic. - إدارة اتصال خادم Jellyfin الخاص بك. - جاري المزامنة - مزامنة المكتبة - قطع الاتصال - جاري مزامنة المكتبة… - جاري جلب قوائم التشغيل… - جاري مزامنة قائمة التشغيل: %1$s - جاري تحديث المكتبة المحلية… - اكتملت المزامنة - جاري جلب قائمة الألبومات… - جاري جلب الأغاني من %1$s… - جاري حفظ %1$d أغانٍ في قاعدة البيانات… - لم يتم العثور على أغانٍ في المكتبة - اكتملت مزامنة المكتبة - %1$d أغانٍ - مزامنة - مزامنة الكل - إضافة مجلد - تسجيل الخروج - NetEase Music - QQ Music - مزامنة جميع قوائم التشغيل - خطأ: %1$s - جاري المزامنة… - اختر نوع قائمة التشغيل - اختر قوائم التشغيل المراد مزامنتها: - جميع قوائم التشغيل - المنشأة والمجمعة - قوائم التشغيل المنشأة - قوائم التشغيل المجمعة - الصورة الشخصية للمستخدم - تم إنشاء قائمة التشغيل بنجاح - يرجى تعيين مفتاح API لمزود الذكاء الاصطناعي أولاً - يرجى تعيين مفتاح API لـ Gemini أولاً - تمت الإضافة إلى قائمة الانتظار - سيتم التشغيل تالياً - تعذر مشاركة الأغنية: %1$s - diff --git a/app/src/main/res/values-ar/strings_settings.xml b/app/src/main/res/values-ar/strings_settings.xml deleted file mode 100644 index 78d187b68..000000000 --- a/app/src/main/res/values-ar/strings_settings.xml +++ /dev/null @@ -1,300 +0,0 @@ - - - الإعدادات - الحسابات - إدارة خدمات Telegram، وGoogle Drive، وNetEase وغيرها - - إدارة الموسيقى - إدارة المجلدات، تحديث المكتبة، وخيارات التحليل البرمجي - المظهر - السمات (الثيمات)، التخطيط، والأنماط المرئية - التشغيل - سلوك الصوت، التداخل (Crossfade)، والتشغيل في الخلفية - السلوك - الإيماءات، التفاعل اللمسي، وسلوك التنقل - دمج الذكاء الاصطناعي (β) - مزودو الذكاء الاصطناعي، مفاتيح الـ API، وإعدادات النموذج - النسخ الاحتياطي والاستعادة - تصدير واستعادة بيانات تطبيقك الشخصية - خيارات المطورين - الميزات التجريبية وإصلاح الأخطاء - معادل الصوت (Equalizer) - ضبط ترددات الصوت والمستويات المسبقة - قدرات الجهاز - مواصفات الصوت، برامج الترميز (Codecs)، ومعلومات فك التشفير - حول التطبيق - معلومات التطبيق، الإصدار، والحقوق - - مفعل - معطل - مُمكن - مُعطل - فتح - تحديد الكل - مسح التحديد - إغلاق التنويه - - بنية المكتبة - المجلدات المستبعدة - سيتم تخطي المجلدات الموجودة هنا أثناء فحص مكتبتك. - الفنانون - خيارات تنظيم وتحليل البيانات للفنانين المتعددين. - التصفية - الحد الأدنى لمدة الأغنية - الحد الأدنى للمسارات في الألبوم - حد ذاكرة التخزين المؤقت لأغلفة الألبومات - الحد الأقصى لحجم الذاكرة التخزينية قبل حذف الصور الأقدم تلقائياً - المزامنة والفحص - إجراء فحص كامل جديد - انتهت مزامنة المكتبة - بدأ الفحص الكامل من جديد… - فحص تلقائي لملفات .lrc - فحص وتعيين ملفات الكلمات المزامنة (.lrc) المتواجدة في نفس المجلد تلقائياً أثناء المزامنة. - إدارة كلمات الأغاني - أولوية مصدر كلمات الأغاني - اختر المصدر الذي يفضله التطبيق أولاً عند جلب الكلمات. - المضمنة أولاً (Embedded) - عبر الإنترنت أولاً - الملف المحلي (.lrc) أولاً - إعادة تعيين الكلمات المستوردة - حذف جميع كلمات الأغاني المستوردة من قاعدة البيانات. - - المظهر العام - لغة التطبيق - اختر اللغة المستخدمة في واجهة التطبيق بكاملها. - افتراضية النظام - English (الإنجليزية) - Español (الإسبانية) - Deutsch (الألمانية) - Français (الفرنسية) - Русский (الروسية) - 简体中文 (الصينية المبسطة) - Bahasa Indonesia (الإندونيسية) - Italiano (الإيطالية) - Türkçe (التركية) - مظهر التطبيق - التنقل بين المظهر الفاتح، الداكن، أو تتبع النظام. - المظهر الفاتح - المظهر الداكن - حسب النظام - حواف ناعمة ومرنة - استخدام زوايا وحواف دائرية معقدة لتحسين الجمالية؛ قد يؤثر على الأداء في الأجهزة الضعيفة - تعطيل تأثيرات الضبابية (Blur) - إيقاف تشغيل تأثيرات التغبيش والضبابية لتوفير البطارية وموارد الجهاز. - إظهار شريط التمرير - عرض شريط تمرير جانبي في قوائم الموسيقى للتنقل السريع - شاشة المشغل الحالي - مظهر المشغل - اختر مظهر شريط المشغل العائم. - غلاف الألبوم - ديناميكي حسب النظام - إظهار معلومات ملف الموسيقى - عرض الترميز، ومعدل البت، ومعدل العينة في قسم تقدم المشغل. - نمط لوحة غلاف الألبوم - الحالي: %1$s. افتح المعاينة الحية لتحديد النمط. - نمط العرض الدائري (Carousel) - اختر نمط وطريقة عرض دائرة الألبومات. - بدون إلقاء نظرة خاطفة - إلقاء نظرة خاطفة واحدة - تجميعة الصفحة الرئيسية - نمط التجميعة - اختر ترتيب الأشكال لتجميعة صور "المزيج الخاص بك". - تدوير الأنماط تلقائياً - تغيير أنماط التجميعة بشكل دوري في كل مرة تزور فيها الصفحة الرئيسية. - شريط التنقل - نمط شريط التنقل - اختر مظهر ونمط شريط التنقل السفلي. - الافتراضي - العرض الكامل - الوضع المدمج - إظهار الأيقونات فقط وتقليل ارتفاع شريط التنقل. - نصف قطر زوايا شريط التنقل - ضبط درجة تدوير زوايا شريط التنقل. - شاشة كلمات الأغاني - كلمات غامرة وملء الشاشة - إخفاء عناصر التحكم تلقائياً وتكبير حجم الخط. - مهلة الإخفاء التلقائي - الوقت المستغرق قبل إخفاء عناصر التحكم. - 3 ثوانٍ - 4 ثوانٍ - 5 ثوانٍ - 6 ثوانٍ - التنقل داخل التطبيق - علامة التبويب الافتراضية - اختر علامة التبويب الافتراضية عند تشغيل التطبيق. - الرئيسية - التنقل في المكتبة - اختر طريقة التنقل بين علامات تبويب المكتبة الموسيقية. - صف علامات التبويب (افتراضي) - شريط كبسولة مدمج وشبكة - - التشغيل في الخلفية - استمرار التشغيل بعد الإغلاق - إذا تم إيقافه، فإن إزالة التطبيق من التطبيقات الحديثة سيؤدي لإيقاف التشغيل. - تحسين استهلاك البطارية - تعطيل تحسين البطارية لمنع انقطاع التشغيل في الخلفية. - تم تعطيل تحسين البطارية بالفعل - موازنة مستوى الصوت (ReplayGain) - تفعيل ميزة ReplayGain - تثبيت وموازنة مستويات الصوت تلقائياً باستخدام بيانات ReplayGain الوصفية المرفقة بالملفات. - وضع زيادة الكسب (Gain Mode) - مسار: موازنة كل أغنية على حدة. ألبوم: موازنة الصوت لكل ألبوم بالكامل. - مسار - ألبوم - البث (Cast) - التشغيل التلقائي عند الاتصال أو قطع البث - بدء التشغيل مباشرة بعد تبديل اتصالات البث والأجهزة. - سماعات الرأس - استئناف التشغيل عند إعادة توصيل السماعات - إذا توقف التشغيل مؤقتاً بسبب فصل السماعة، فسيتم الاستئناف تلقائياً بمجرد توصيلها مجدداً. - قائمة الانتظار والانتقالات - التداخل (Crossfade) - تفعيل الانتقال السلس والمندمج بين الأغاني المتتالية. - مدة التداخل (Crossfade) - وضع الصوت عالي الدقة (Hi-Fi) - مخرج صوتي بصيغة Float 32-bit. قم بتعطيله إذا لاحظت تقطيعاً في الصوت على جهازك. - غير مدعوم على هذا الجهاز (خاصية PCM_FLOAT AudioTrack غير متوفرة). - التشغيل العشوائي الدائم - تذكر إعداد التشغيل العشوائي والاحتفاظ به حتى بعد إغلاق التطبيق. - إظهار سجل قائمة الانتظار - عرض الأغاني التي تم تشغيلها سابقاً داخل قائمة الانتظار. - - المجلدات - إيماءة الرجوع تتحكم بالمجلدات - في تبويب المجلدات، يؤدي الرجوع للتنقل عبر تسلسل المجلدات أولاً قبل الخروج من المكتبة. - إيماءات المشغل - النقر على الخلفية يغلق المشغل - النقر على الخلفية الضبابية يؤدي لإغلاق صفحة المشغل المنبثقة. - المؤثرات اللمسية - الاستجابة اللمسية (الاهتزاز) - تفعيل اهتزازات الاستجابة الخفيفة عبر أرجاء التطبيق. - - مزود الذكاء الاصطناعي - المزود - اختر مزود خدمة الذكاء الاصطناعي الخاص بك - وضع الرموز الآمن (Safe Token) - مفعل — سريع وموفر. يرسل بيانات قليلة ومحدودة (~1K رموز) إلى الذكاء الاصطناعي. - معطل — سياق عميق. يرسل ملف الاستماع الكامل بالكامل (~8K رموز) لنتائج أكثر ثراءً ودقة. - بيانات الاعتماد - مفتاح API لـ %1$s - احصل عليه من %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) - اختيار النموذج - جاري تحميل النماذج المتاحة… - نموذج الذكاء الاصطناعي - اختر نموذجاً. - سلوك الأوامر (Prompt) - الأمر البرمجي للنظام - تخصيص وتحديد كيفية تصرف واستجابة الذكاء الاصطناعي. - تقرير استخدام الذكاء الاصطناعي - إجمالي الاستهلاك - تتبع رموز %1$s \nالمدخلات (Prompt): %2$s | المخرجات: %3$s | التفكير: %4$s - - إنشاء نسخة احتياطية - تصدير النسخة الاحتياطية - يقوم بإنشاء ملف نسخة احتياطية بصيغة %1$s .pxpl. - استعادة النسخة الاحتياطية - استيراد نسخة احتياطية - تصفح أو اختر من النسخ الاحتياطية الأخيرة. البيانات المحددة ستستبدل البيانات الحالية. - الميزات التجريبية - تجريبي - تجارب ومفاتيح تبديل لتحميل واجهة المستخدم للمشغل. - اختبار تدفق الإعداد الأولية - تشغيل شاشة الإعداد والترحيب المبدئية لأغراض الاختبار. - الصيانة - فرض إعادة توليد المزيج اليومي - إعادة إنشاء قائمة تشغيل المزيج اليومي فوراً. - فرض إعادة توليد الإحصائيات - مسح ذاكرة التخزين المؤقت وإعادة حساب إحصاءات التشغيل بالكامل. - فرض إعادة توليد لوحة ألوان الألبوم - التشخيص والأعطال - إطلاق تعطل تجريبي - محاكاة تعطل مفاجئ للتطبيق لاختبار نظام تقارير الأخطاء. - التطبيق - حول PixelPlayer - إصدار التطبيق، الحقوق والمساهمين، والمزيد. - - لم يتم اختيار أي قسم. - تم اختيار جميع الأقسام. - تم اختيار %1$d من أصل %2$d من الأقسام. - كيفية عمل النسخ الاحتياطي - اختر الأقسام، وقم بتصدير ملف بصيغة .pxpl، واستورده لاحقاً. الاستعادة ستقوم فقط باستبدال الأقسام التي تحددها بنفسك. - اختر بدقة ما ترغب في تضمينه داخل حزمة النسخ الاحتياطي. - تصدير ملف .pxpl - تم تحديد %1$d من أصل %2$d أقسام - عملية النقل جارية… - جاري التصدير - جاري الاستيراد - جاري إنشاء النسخة الاحتياطية - جاري استعادة النسخة الاحتياطية - الخطوة %1$d من أصل %2$d - إدخالات عدد %1$d · ستستبدل البيانات الحالية بالكامل - - تم إعادة توليد لوحة الألوان لـ %1$s - تعذر إعادة توليد لوحة الألوان لـ %1$s - جاري إعادة توليد لوحات ألوان الألبومات… - إعادة توليد جميع لوحات ألوان الألبومات؟ - إعادة بناء متغيرات الألوان المخزنة مؤقتاً لـ %1$d من أغلفة الألبومات الفريدة. قد يستغرق هذا الإجراء بعض الوقت في المكتبات الكبيرة. - سيؤدي هذا إلى مسح بيانات السمات المخزنة مؤقتاً وإعادة بناء جميع أنماط لوحات الألوان لـ %1$d من أغلفة الألبومات الفريدة. - اكتمل %1$d من أصل %2$d - جاري العمل… - إعادة التوليد - تم إعادة توليد %1$d من لوحات ألوان الألبومات - تم إعادة توليد %1$d من أصل %2$d من لوحات ألوان الألبومات - - إعادة تعيين كلمات الأغاني المستوردة؟ - لا يمكن التراجع عن هذا الإجراء لاحقاً. - تأكيد - إعادة بناء قاعدة البيانات؟ - سيؤدي هذا إلى إعادة بناء مكتبة الموسيقى الخاصة بك بالكامل من الصفر. ستفقد جميع كلمات الأغاني المستوردة، والمفضلة، والبيانات الوصفية المخصصة. لا يمكن التراجع عن هذا الإجراء. - إعادة البناء - جاري إعادة بناء قاعدة البيانات - جاري إعادة بناء قاعدة البيانات… - إعادة توليد المزيج اليومي؟ - سيؤدي هذا إلى تجاهل المزيج الحالي وإنشاء مزيج جديد بناءً على عادات الاستماع الأخيرة. - بدأت عملية إعادة توليد المزيج اليومي - إعادة توليد الإحصائيات؟ - سيؤدي هذا إلى مسح ذاكرة التخزين المؤقت للإحصاءات وفرض إعادة الحساب من سجل قاعدة البيانات. - بدأت عملية إعادة توليد الإحصائيات - PixelPlayer_Backup_%1$d.pxpl - - إعادة توليد المزيج اليومي - إعادة توليد الإحصائيات - لم يتم العثور على أي أغانٍ تحتوي على غلاف ألبوم. - إعادة بناء جميع متغيرات لوحة الألوان المخزنة مؤقتاً لكل غلاف ألبوم، أو اختر أغنية واحدة لتحديثها. - إعادة توليد الكل - جاري إعادة التوليد… - اختر الأغنية - - مسح السجلات - سجل نشاط الذكاء الاصطناعي (%1$d) - إظهار - إخفاء - تحديد وتصدير - تحديد واستعادة - - استيراد نسخة احتياطية - جاري الفحص المعمق… - تصفح لاختيار الملف - اختر ملف نسخة احتياطية بصيغة .pxpl لفحصه. ستتمكن من اختيار الأقسام المراد استعادتها في الخطوة التالية. - النسخ الاحتياطية الأخيرة - لا توجد نسخ احتياطية حديثة - النسخ الاحتياطية التي تم استيرادها سابقاً ستظهر هنا. - فرض إعادة توليد لوحة ألوان الألبوم - اختر أغنية لمسح بيانات المظهر المخزنة مؤقتاً وإعادة توليد جميع أنماط الألوان من غلاف الألبوم. - البحث حسب العنوان، أو الفنان، أو الألبوم - جاري إعادة توليد لوحة الألوان… - لا توجد أغانٍ تطابق بحثك. - إزالة من السجل - مسح البحث - وحدات عدد %1$d · إصدار %2$s · إصدار المخطط البرمجي %3$d - Korean (الكورية) - Norwegian (النرويجية بوكمول) - From 358702f80913b7d965f0195753965573615756dd Mon Sep 17 00:00:00 2001 From: Hisham-Alzamzami Date: Wed, 17 Jun 2026 04:09:47 +0300 Subject: [PATCH 58/99] Add Arabic localization for app strings --- app/src/main/res/values-ar/strings.xml | 128 +++++++++++++++++++++++++ 1 file changed, 128 insertions(+) create mode 100644 app/src/main/res/values-ar/strings.xml diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml new file mode 100644 index 000000000..d7e6c67c6 --- /dev/null +++ b/app/src/main/res/values-ar/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 + + + رجوع + حسناً + إلغاء + تجاهل + خطأ + بحث + مسح البحث + الكل + تأكيد + تم الحفظ! + محدد + %1$d%% + الفنان + تحديد الكل + مسح + خطأ غير معروف + + + حفظ + تم + إعادة تعيين + تطبيق + خلط + نسخ + مشاركة + تراجع + استيراد + حذف + تصدير + دمج + إعادة تسمية + إنشاء + كلمات الأغاني + الإعدادات + غلاف الألبوم + قائمة تشغيل + مسار مجهول + فنان مجهول + ألبوم مجهول + إغلاق + إضافة + إزالة + تشغيل + المسار السابق + المسار التالي + المفضلة + إيقاف مؤقت + تكرار + خيارات + تشغيل عشوائي + المزيد من الخيارات لـ %1$s + توسيع القائمة + التالي + إنهاء + إعادة تعيين الافتراضيات + تصدير الكل + دمج الكل + مشاركة الكل + تشغيل الألبوم + تشغيل الألبوم عشوائياً + غلاف الألبوم لـ %1$s + From 266e46605b36a3147007942392e58aaba7bb98db Mon Sep 17 00:00:00 2001 From: Hisham-Alzamzami Date: Wed, 17 Jun 2026 04:12:19 +0300 Subject: [PATCH 59/99] Add new Arabic translation --- app/src/main/res/values-ar/strings.xml | 2 +- .../main/res/values-ar/strings_changelogs.xml | 169 +++++ .../res/values-ar/strings_cloud_services.xml | 78 +++ .../main/res/values-ar/strings_equalizer.xml | 45 ++ .../res/values-ar/strings_home_screen.xml | 273 ++++++++ .../main/res/values-ar/strings_library.xml | 597 ++++++++++++++++ app/src/main/res/values-ar/strings_player.xml | 210 ++++++ .../main/res/values-ar/strings_screens.xml | 244 +++++++ .../main/res/values-ar/strings_settings.xml | 643 ++++++++++++++++++ app/src/main/res/values-ar/strings_widget.xml | 17 + 10 files changed, 2277 insertions(+), 1 deletion(-) create mode 100644 app/src/main/res/values-ar/strings_changelogs.xml create mode 100644 app/src/main/res/values-ar/strings_cloud_services.xml create mode 100644 app/src/main/res/values-ar/strings_equalizer.xml create mode 100644 app/src/main/res/values-ar/strings_home_screen.xml create mode 100644 app/src/main/res/values-ar/strings_library.xml create mode 100644 app/src/main/res/values-ar/strings_player.xml create mode 100644 app/src/main/res/values-ar/strings_screens.xml create mode 100644 app/src/main/res/values-ar/strings_settings.xml create mode 100644 app/src/main/res/values-ar/strings_widget.xml diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index d7e6c67c6..3a6030840 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -125,4 +125,4 @@ تشغيل الألبوم تشغيل الألبوم عشوائياً غلاف الألبوم لـ %1$s - + \ No newline at end of file diff --git a/app/src/main/res/values-ar/strings_changelogs.xml b/app/src/main/res/values-ar/strings_changelogs.xml new file mode 100644 index 000000000..372fc56f0 --- /dev/null +++ b/app/src/main/res/values-ar/strings_changelogs.xml @@ -0,0 +1,169 @@ + + + سجل التغييرات + عرض على GitHub + التحسينات + الإصلاحات + ما الجديد + تمت إضافة + + + دعم Chromecast لبث الصوت من جهازك. + سجل تغييرات داخل التطبيق لإبقائك على اطلاع بأحدث الميزات. + دعم ملفات .LRC، المضمنة والخارجية على حد سواء. + دعم كلمات الأغاني دون اتصال بالإنترنت. + كلمات أغاني متزامنة (متوافقة مع توقيت الأغنية). + شاشة جديدة لعرض قائمة الانتظار كاملة. + إعادة ترتيب وإزالة الأغاني من قائمة الانتظار. + إيماءات المشغل المصغر (السحب لأسفل للإغلاق). + إضافة المزيد من حركات وتأثيرات Material الأنيميشن. + إعدادات جديدة لتخصيص المظهر العام واختيار الألوان. + إعدادات جديدة لمسح ذاكرة التخزين المؤقت. + + + + إعادة تصميم شاملة لواجهة المستخدم. + إعادة تصميم شاملة للمشغل الموسيقي. + تحسينات في الأداء داخل المكتبة الموسيقية. + تحسين سرعة تشغيل وإقلاع التطبيق. + الذكاء الاصطناعي يقدم الآن نتائج أفضل. + + + + إصلاح أخطاء متنوعة في محرر العلامات الوصفية (Tags). + إصلاح مشكلة عدم اختفاء إشعار التشغيل من لوحة الإشعارات. + إصلاح عدة أخطاء كانت تتسبب في توقف التطبيق عن العمل اضطرارياً. + + + + تقديم مركز إحصاءات استماع أكثر ثراءً مع تحليلات أعمق لجلساتك الموسيقية. + إطلاق مشغل سريع عائم لفتح ومعاينة الملفات المحلية على الفور. + إضافة تبويب المجلدات مع مستكشف شجري وعرض جاهز لقوائم التشغيل. + + + + تحسين واجهة المستخدم الشاملة لـ Material 3 لتقديم تجربة أنظف وأكثر تماسكاً. + تعديل البيانات الوصفية يدعم الآن تغيير غلاف الألبوم. + تنعيم الحركات والانتقالات عبر التطبيق لضمان تنقل أكثر سلاسة وفوقية. + تحسين تخطيط شاشة الفنان مع تفاصيل غنية ولمسات جمالية. + ترقية ميزة توليد قوائم DailyMix و YourMix باختيارات أذكى وأكثر تنوعاً. + تعزيز ميزة توليد قوائم التشغيل بواسطة الذكاء الاصطناعي. + تحسين دقة البحث وعرض النتائج لاكتشاف أسرع للمحتوى. + توسيع الدعم ليشمل مجموعة أوسع من صيغ وتنسيقات الملفات الصوتية. + + + + حل مشكلات البيانات الوصفية المفاجئة لتبقى تفاصيل الأغنية دقيقة في كل مكان. + استعادة اختصارات الإشعارات لتعود بشكل موثوق ومباشر إلى شاشة التشغيل. + + + + إعادة تصميم كبرى لنظام التنقل داخل التطبيق + مستكشف ملفات جديد لاختيار مجلدات المصادر + ميزات اتصال وبث (Casting) جديدة + استمرارية سلسة للتشغيل بين الأجهزة عن بعد + انتقال بدون فجوات (Gapless) بين الأغاني + التحكم في التداخل الصوتي (Crossfade) + ميزة الانتقالات المخصصة الجديدة (لقوائم التشغيل فقط) + استمرار التشغيل حتى بعد إغلاق التطبيق + تحسينات وتحسين أداء واجهة المستخدم + تحسين ميزة إحصاءات الاستماع + إعادة تصميم التحكم في قائمة الانتظار مع المزيد من الميزات + تحسين دعم أنواع الملفات المختلفة للتشغيل وتعديل البيانات الوصفية + تحسين نظام التحكم في الأذونات والصلاحيات + إصلاحات للأخطاء الطفيفة + + + + تحديث واجهة المستخدم التعبيرية لـ Material 3 Expressive + معادل صوت بـ 10 حزم ترددية وتأثيرات صوتية متنوعة + نظام تدفق جديد لمزامنة المكتبة الموسيقية + تكامل مع الذكاء الاصطناعي (نماذج Gemini) + استيراد وتصدير قوائم التشغيل بصيغة M3U + دمج وتضمين صور الفنانين من منصة Deezer + أغلفة مخصصة لقوائم التشغيل + إعادة هيكلة وتطوير بنية الإعدادات + تأثيرات حركية جديدة لقائمة الانتظار والمشغل + تحسين الأداء العام واعتماد ملفات التعريف الأساسية (Baseline Profiles) + نظام كلمات أغاني أفضل مع إمكانية تعديل إزاحة التزامن (Sync Offset) + + + + تحسينات على استقرار وثبات ميزة البث (Casting) + تحسين استقرار لوحة المشغل السفلى (Player Sheet) + إصلاحات عامة للأخطاء وتنظيف الكود + + + + دعم Android Auto متاح الآن للتشغيل داخل السيارة. + إطلاق دعم Wear OS رسمياً، بما في ذلك عناصر تحكم أفضل للتشغيل من الساعة إلى الهاتف. + توسيع نطاق التكامل السحابي مع تحسينات لـ Telegram و NetEase و QQ Music و Google Drive. + ميزتا \"المشغلة مؤخراً\" واستعادة قائمة الانتظار المستمرة تبقيان جلسة استماعك جاهزة دائماً. + تضمين أدوات النسخ الاحتياطي والاستعادة (الإصدار الثالث v3) وأدوات إدارة الحسابات. + إضافة ميزة ذكية للبحث اليدوي عن كلمات الأغاني عند فشل البحث التلقائي مع تحسين التخزين. + + + + قفزة كبيرة في الأداء تشمل الإقلاع، المكتبة، قائمة الانتظار، وتفاعلات المشغل. + إعادة تصميم واجهات المشغل، البث، كلمات الأغاني، الفنانين، والأنواع لتجربة أكثر سلاسة. + تدفقات التنقل والبحث أصبحت أكثر موثوقية مع معالجة آمنة للمسارات البرمجية. + تحسين التوافقية للتشغيل الصوتي ليشمل المزيد من الأجهزة وصيغ الصوت. + توسيع سير عمل التحديد المتعدد ليشمل الأغاني، الألبومات، وقوائم التشغيل. + + + + سلوك قائمة الانتظار والخلط العشوائي أصبح الآن أكثر استقراراً وقابلية للتنبؤ. + إصلاح العديد من الحالات النادرة المتعلقة بالتشغيل في الخلفية والبث (Casting). + إصلاح مشاكل توقف مؤقت النوم، تصفح تبويب الملفات، وحالات الانهيار عند تصفح فنان الألبوم. + تحسين تحميل الودجت واستقرار الخدمة لتقليل استهلاك الذاكرة وسخونة الجهاز. + إصلاحات عامة للأخطاء ولمسات جمالية على واجهة المستخدم في مختلف أنحاء التطبيق. + + + + نظام Wear OS: نقل الموسيقى، التشغيل المحلي، مزامنة قائمة الانتظار، والتحكم عن بعد من الساعة. + الذكاء الاصطناعي: دمج Groq AI و OpenRouter (تجريبي) مع تحسين استهلاك الرموز (Tokens). + السحاب: إضافة دعم خوادم Jellyfin. + كلمات الأغاني: ترجمة متزامنة مع مفتاح تبديل مخصص، دعم صيغة Kugou LRC، تخصيص محاذاة النص، وتحسين التحميل عن بعد. + واجهة المستخدم: وضع شريط التنقل المدمج، سمات ديناميكية مستوحاة من ألوان غلاف الألبوم، نص متحرك (Marquee) للعناوين الطويلة، وخيارات فرز جديدة. + تيليجرام: دعم أصلي للمواضيع (Topics) وأنماط عرض محسنة. + + + + محرك الصوت: إعادة هيكلة شاملة للمحرك مع دعم صيغ إضافية (MIDI, ALAC, M4A) وتحسين برامج فك التشفير. + الكفاءة: تقليص حاد في استهلاك طاقة البطارية، علاج مشكلات السخونة، وتحسين المهام في الخلفية (SyncWorker). + قاعدة البيانات: تحسينات هائلة على الاستعلامات وإعادة تصميم ذاكرة التخزين المؤقت للأغلفة لمنع فقدان البيانات. + الإقلاع: تحسين وقت تحميل وتشغيل التطبيق عبر تحسين ملفات التعريف الأساسية (Baseline Profile). + + + + التشغيل: إصلاح التقطع في صيغ Opus/MP3، أخطاء ReplayGain أثناء التداخل الصوتي، ومشاكل التشغيل في أجهزة فك التشفير من سامسونج. + الاستقرار: القضاء على حالات الانهيار عند بدء التشغيل، تصفح الفنانين، وعلى الأجهزة التي تعمل بنظام Android 12+. + الواجهة: إصلاح وميض أغلفة الألبومات، تداخل النصوص في الخطوط غير اللاتينية، وسلوك شريط التنقل والمشغل المصغر. + الأمان: تعزيز حماية وإدارة بيانات الاعتماد، أذونات التخزين، والاتصال بخوادم الوسائط. + + + + اللغات المحلية: الإسبانية، الفرنسية، الروسية، الصينية المبسطة، الإندونيسية، الإيطالية + + + + التكامل مع Google Drive مع إدارة دورة حياة المشغل. + تعديل جماعي للبيانات الوصفية للأغاني (العلامات الوصفية وأغلفة الألبومات). + ترجمة كلمات الأغاني بالذكاء الاصطناعي مع تفضيلات Wear OS القابلة للتخصيص. + أداة تشخيص تعليق وبطء التطبيق والتحديد المتعدد في شاشة البحث. + دعم اللغتين العربية والتركية، مع خيارات مخصصة لعناوين http المحلية في الشبكة الداخلية. + + + + توفير كبير في طاقة البطارية (تعليق المهام الصوتية بذكاء وبوابات فحص واجهة المستخدم). + تحسين إدارة قائمة الانتظار (إدراج أسرع وفهرسة صريحة للمسارات). + تأثيرات حركية تعبيرية للحركة من Material 3 لشاشات الانتقال. + إعادة هيكلة مزامنة المكتبة عبر جدولة الفحص الذكي المقيد. + + + + حل مشكلات ليد التقطع/تخطي المسارات أثناء التشغيل وبطء تحميل البافيرنج. + إصلاح مزامنة حذف الأغاني الخارجية واتساق البيانات الوصفية. + إصلاح مشكلات الذاكرة، الانهيارات، وعيوب التخطيط في نظام Wear OS والهواتف. + + \ No newline at end of file diff --git a/app/src/main/res/values-ar/strings_cloud_services.xml b/app/src/main/res/values-ar/strings_cloud_services.xml new file mode 100644 index 000000000..1979d917a --- /dev/null +++ b/app/src/main/res/values-ar/strings_cloud_services.xml @@ -0,0 +1,78 @@ + + + تسجيل الدخول إلى تيليجرام + أنت تقوم بتعديل رقمك الآن. إرسال الرمز مجدداً سيؤدي إلى استبدال الرمز السابق. + جاري العمل… + جاري تهيئة تيليجرام… + جاري تسجيل الخروج… + جاري إغلاق الجلسة… + تم إغلاق الجلسة. يرجى إعادة فتح تسجيل الدخول للمتابعة. + جاري إعداد جلسة تيليجرام الآمنة… + في انتظار رد تيليجرام… + ربط تيليجرام + قم بربط حساب تيليجرام لبث الموسيقى من قنواتك ومحادثاتك. + رقم الهاتف + أدخل رقم تيليجرام الخاص بك. يمكنك العودة وتعديله لاحقاً. + رقم الهاتف + 1 + 5551234567 + إرسال الرمز + رمز التحقق + أدخل رمز التحقق المرسل إلى تطبيق تيليجرام الخاص بك. + رمز التحقق + كلمة المرور (التحقق بخطوتين) + حسابك محمي بكلمة مرور إضافية للتحقق بخطوتين. يرجى إدخالها أدناه. + كلمة المرور + تسجيل الدخول + تم ربط حساب تيليجرام بنجاح! + القنوات المتزامنة + إدارة القنوات العامة التي يتم جلب ملفات الصوت منها تلقائياً. + + قطع اتصال %1$s؟ + سيؤدي هذا إلى إزالة الحساب ومسح جميع الأغاني المستوردة من هذا المصدر من مكتبتك المزامنة. + قطع الاتصال + متصل كـ %1$s + متصل + حساب مجهول + + ربط Google Drive + قم بربط حساب جوجل لبث وتشغيل ملفات الموسيقى المخزنة في سحابتك. + جاري إعداد اتصال Google Drive… + في انتظار مصادقة جوجل… + جاري مزامنة ملفات Google Drive… + فشلت مصادقة Google Drive: %1$s + فشل فحص مجلدات Google Drive. + + ربط خادم Jellyfin + قم بالاتصال بخادم Jellyfin الشخصي الخاص بك لبث مكتبتك الموسيقية بالكامل. + رابط الخادم (Server URL) + https://jellyfin.example.com:8096 + اسم المستخدم + اسم المستخدم + كلمة المرور + كلمة المرور + الاتصال بالخادم + جاري الاتصال بخادم Jellyfin… + فشل الاتصال بخادم Jellyfin. يرجى التحقق من الرابط والشبكة. + اسم المستخدم أو كلمة المرور غير صحيحة لخادم Jellyfin. + + تم + جاري تحميل صفحة تسجيل الدخول الآمنة… + فشل تحميل واجهة الـ WebView. + + فشل قراءة ملفات تعريف الارتباط (Cookies) لـ NetEase: %1$s + الخروج من تسجيل دخول NetEase؟ + خطأ HTTP %1$d أثناء تحميل NetEase. + لم يتم رصد تسجيل الدخول بعد. أكمل تسجيل الدخول في NetEase قبل الضغط على \"تم\". + تسجيل الدخول إلى NetEase Music + ملاحظة أمنية: يتم إدخال كلمة المرور الخاصة بك فقط داخل صفحات ويب NetEase الرسمية. يقوم PixelPlayer بحفظ ملفات تعريف ارتباط الجلسة (MUSIC_U) فقط لمزامنة مكتبتك الموسيقية. + موسيقى NetEase + + فشل قراءة ملفات تعريف الارتباط (Cookies) لـ QQ Music: %1$s + الخروج من تسجيل دخول QQ Music؟ + خطأ HTTP %1$d أثناء تحميل QQ Music. + لم يتم رصد تسجيل الدخول بعد. أكمل تسجيل الدخول في QQ Music قبل الضغط على \"تم\". + تسجيل الدخول إلى QQ Music + ملاحظة أمنية: يتم إدخال كلمة المرور الخاصة بك فقط داخل صفحات ويب QQ Music الرسمية. يقوم PixelPlayer بحفظ ملفات تعريف ارتباط الجلسة المصادق عليها فقط لمزامنة مكتبتك الموسيقية. + موسيقى QQ Music + \ No newline at end of file diff --git a/app/src/main/res/values-ar/strings_equalizer.xml b/app/src/main/res/values-ar/strings_equalizer.xml new file mode 100644 index 000000000..d5dd0557e --- /dev/null +++ b/app/src/main/res/values-ar/strings_equalizer.xml @@ -0,0 +1,45 @@ + + + لا يمكن أن يكون الاسم فارغاً + إعادة تسمية + + تغيير وضع العرض + تعطيل معادل الصوت + تفعيل معادل الصوت + تعديل + تعديل الأنماط المسبقة + نمط مخصص + الأنماط المسبقة + تحديث + تضخيم الباس (Bass Boost) + المجسم (Virtualizer) + جهارة الصوت (Loudness) + غير مدعوم + غير مدعوم على هذا الجهاز + مستوى الصوت + استجابة التردد + هرتز + كيلوهرتز + حفظ كجديد + + الأنماط المحفوظة + لم يتم حفظ أي أنماط مخصصة بعد. + إلغاء التثبيت + تثبيت + إعادة تسمية + حذف + + حفظ نمط مخصص + أدخل اسماً لنمط معادل الصوت المخصص الجديد. + اسم النمط + إعادة تسمية النمط + + إدارة الأنماط المسبقة + اسحب لإعادة الترتيب • اضغط على العين للإظهار أو الإخفاء + إعادة الترتيب + إعادة تعيين الأنماط + سيؤدي هذا إلى استعادة الترتيب والظهور الافتراضي للأنماط المسبقة. هل تريد المتابعة؟ + إعادة تعيين إلى الافتراضي + مرئي + مخفي + \ No newline at end of file diff --git a/app/src/main/res/values-ar/strings_home_screen.xml b/app/src/main/res/values-ar/strings_home_screen.xml new file mode 100644 index 000000000..d29658600 --- /dev/null +++ b/app/src/main/res/values-ar/strings_home_screen.xml @@ -0,0 +1,273 @@ + + + β + تجريبي (Beta) + البث السحابي + سجل التغييرات + البث السحابي + بث الموسيقى من حساباتك السحابية + + الإصدار التجريبي Beta 0.7.0 + β + مرحباً بك في PixelPlayer 0.7.0-beta + أنت تستخدم إصداراً تجريبياً قد يحتوي على أخطاء، أو حالات انهيار، أو ميزات تجريبية. ساعدنا في التحسين من خلال الإبلاغ عن المشكلات. + ما الذي يجب توقعه + قد تحدث أخطاء، أو حالات انهيار، أو ميزات غير مكتملة بشكل غير متوقع. + قد تتغير بعض الميزات أو تتم إزالتها دون إشعار مسبق. + قد تكون الإصدارات التجريبية أقل استقراراً من الإصدارات الرسمية النهائية. + تحقق دائماً من وجود تحديثات قبل الإبلاغ عن مشكلة معروفة. + ما يمكن أن تغيره الإصدارات التجريبية، أو تعطلها، أو تحسنها أثناء الاختبار. + اختصار مشكلات GitHub + ابحث أولاً، ثم افتح تقريراً مركزاً ومحدداً للأخطاء، أو الانهيارات، أو الطلبات، أو الأسئلة. + عرض المشكلات الحالية + الإبلاغ عن مشكلة أو انهيار + شارك خطوات إعادة إنتاج المشكلة، والنتائج المتوقعة، والنتائج الفعلية، وتفاصيل جهازك/نظام تشغيلك. + كيفية الإبلاغ + قائمة مرجعية سريعة قبل فتح تذكرة مشكلة جديدة. + قبل فتح تذكرة مشكلة + ابحث في المشكلات الحالية المفتوحة والمغلقة لتجنب التكرار. + قم بالتحديث إلى أحدث إصدار من PixelPlayer وتأكد من أن المشكلة لا تزال تحدث. + أعد تشغيل التطبيق وتأكد من استمرار المشكلة. + حاول إعادة إنتاج المشكلة واكتب الخطوات الدقيقة لذلك. + ما هو نوع المشكلة؟ + تقرير عن خطأ (Bug): شيء ما يتصرف بشكل غير صحيح. + طلب ميزة (Feature): إضافة ميزة جديدة أو تحسين. + سؤال: استخدم المناقشات (Discussions) إذا كانت مفعلة، أو افتح تذكرة مع تسمية سؤال. + تقرير عن خطأ + انسخ هذه الحقول عندما يتصرف التطبيق بشكل خاطئ أو ينهار. + تقرير خطأ برمجي + ملخص قصير: + السلوك المتوقع: + السلوك الحالي: + خطوات التشغيل/إعادة الإنتاج: 1. 2. 3. + كم مرة يحدث ذلك؟ دائماً / أحياناً / نادراً. + لقطة شاشة / فيديو: إن وجد. + السجلات / تتبع المكدس (Stack trace): إن وجد. + البيئة والمنصة + إصدار PixelPlayer: + مصدر التثبيت: إصدار GitHub، نسخة المطور (debug)، نسخة ليلية (nightly)، إلخ. + إصدار أندرويد: + طراز الجهاز: + سياق إضافي: استخدام بطاقة SD، إعدادات خاصة، أذونات، إلخ. + طلب ميزة جديدة + انسخ هذه الحقول عندما تريد ميزة جديدة أو تحسيناً. + شرح المشكلة: ما هي المشكلة التي تحاول حلها؟ + الحل المقترح: كيف ينبغي أن تعمل الميزة؟ + البدائل المدروسة: هل هناك أي طرق أخرى؟ + النطاق: ما هي الشاشات أو التدفقات المتأثرة؟ + تصميم تجريبي أو صورة مرجعية إن وجدت. + العناوين، الخصوصية، والنطاق + اجعل التقرير سهلاً في الفرز وآمناً للمشاركة. + عناوين جيدة للتذاكر + معادل الصوت: مؤشر التردد يتحرك عند التبديل بين تبويبات الأنماط + البحث: قائمة السجل لا تظهر عند البحث بنص فارغ + ميزة: إضافة خيار فرز قوائم التشغيل بحسب \"المضافة حديثاً\" + يرجى تجنب + التقارير العامة والمبهمة مثل \"التطبيق لا يعمل\". + طرح مشكلات متعددة غير مترابطة في تذكرة واحدة. + إرسال سجلات غير منقحة أو لقطات شاشة تحتوي على بيانات خاصة. + ملاحظة بشأن الخصوصية + قبل نشر السجلات أو لقطات الشاشة أو مقاطع الفيديو، يرجى إزالة أي معلومات شخصية أو خاصة. + الإصدارات الليلية (Nightly) + كيف تختلف النسخ الليلية عن الرسمية، وما الذي يجب تضمينه عندما تتوقف عن العمل. + يتم توليد الإصدارات الليلية تلقائياً من آخر التغييرات البرمجية المرفوعة، وقد تحتوي على ميزات غير مكتملة، أو أخطاء مؤقتة، أو تراجع في الأداء. إنها أكثر تجريبية من الإصدارات الرسمية. + يمكنك الوصول إليها عبر ملفات الـ Artifacts الخاصة بـ GitHub Actions في مستودع التطبيق إن وجدت. + الإبلاغ عن مشاكل النسخ الليلية + عند الإبلاغ عن مشكلة في إصدار ليلي، اذكر دائماً أن المشكلة حدثت في النسخة الليلية وليس في الإصدار الرسمي. قم بتضمين تاريخ البناء، أو اسم ورقم تشغيل الـ Workflow، أو رمز الـ Commit (SHA) إن أمكن. وتحقق أيضاً مما إذا كانت نفس المشكلة تحدث في أحدث إصدار رسمي. + التحديث إلى النسخة التجريبية Beta 0.5.0 + يُنصح بتثبيت نظيف (Clean Install) + إذا كنت قادماً من إصدار تجريبي قديم، فقد يتطلب هذا التحديث تهيئة بيانات جديدة للمكتبة بدلاً من الاعتماد على البيانات القديمة المخزنة مؤقتاً. + إذا ظهرت البيانات الوصفية أو عناصر المكتبة بشكل خاطئ + البيانات الوصفية الخاطئة للأغاني، أو عدم تطابق الفنانين والألبومات، أو تكرار العناصر، يعني عادةً أن التثبيت النظيف هو الحل الأمثل. + لا تظهر ثانية + فهمت ذلك + + عذراً! حدث خطأ ما + تعرض التطبيق لانهيار غير متوقع في جلستك الأخيرة. ساعدنا في حل المشكلة عبر مشاركة تقرير الانهيار. + التاريخ: %1$s + الخطأ: + تتبع المكدس (معاينة Stack trace): + سجل الانهيار + تم نسخ سجل الانهيار إلى الحافظة + تقرير انهيار PixelPlayer + مشاركة تقرير الانهيار + + DJ Mashup + + المزيج\nالخاص بك + لا توجد بيانات لعرضها بعد + سيظهر المزيج الخاص بك هنا عندما يجد PixelPlayer أغاني أو يزامن أحد المصادر. + تحديث + + مزيجك اليومي (DAILY MIX) + بناءً على سجل استماعك + تفقد كل قوائم المزيج اليومي + مزيج يومي + + المزيج اليومي + + %1$d أغنية • %2$s + أغنية واحدة • %2$s + أغنيتان • %2$s + %1$d أغاني • %2$s + %1$d أغنية • %2$s + %1$d أغنية • %2$s + + تشغيل + مولد قوائم التشغيل بالذكاء الاصطناعي + + كيف يتم بناء مزيجك اليومي + يتم بناء مزيجك اليومي من أغانيك المفضلة والأكثر تشغيلاً. نضيف أيضاً مسارات من فنانين وأنواع موسيقية تحبها لتكتشف موسيقى جديدة. + أخبر الذكاء الاصطناعي بما تود الاستماع إليه اليوم + نحن نستخدم عينة صغيرة للحفاظ على انخفاض استهلاك الموارد + جاري التحديث… + تحديث المزيج اليومي + + + منسقة بشكل مثالي + المزيج اليومي + رحلتك الصوتية جاهزة الآن + مولد قوائم التشغيل بالذكاء الاصطناعي + صف الأجواء أو المزاج أو النشاط الذي تقوم به، ودع الذكاء الاصطناعي ينسق لك قائمة تشغيل مثالية من مكتبتك. + حجم قائمة التشغيل + أقل عدد أغانٍ + أقصى عدد أغانٍ + مثال: أجواء مسائية هادئة، طاقة حماسية للتمارين… + انقر لإعادة المحاولة + تم توليد رحلتك الصوتية بنجاح! + جاهز للتشغيل + جاري التوليد… + توليد قائمة تشغيل + + + المشغلة حديثاً + + + المشغلة حديثاً + تشغيل الأحدث + لا توجد أغانٍ مشغلة مؤخراً في %1$s + قم بتغيير النطاق الزمني أو شغّل المزيد من الأغاني لملء هذا الجدول الزمني. + المشغلة حديثاً + اليوم + أمس + + + إحصاءات الاستماع + إجمالي التشغيل + المعدل اليومي + الأغنية الأكثر تشغيلاً + %1$s • %2$d تشغيل + + + إحصاءات الاستماع + تحديث إحصاءات الاستماع + اليوم + هذا الأسبوع حتى اليوم + هذا الشهر حتى اليوم + هذه السنة حتى اليوم + كل الأوقات + الاستماع + مرات التشغيل + مخطط الاستماع الزمني + وقت الاستماع + إجمالي وقت الاستماع الذي تم تسجيله في النطاق المحدد. + عدد مرات التشغيل + كم عدد جلسات الاستماع التي أكملتها لكل قسم. + معدل الجلسة + متوسط مدة الاستماع لكل جلسة. + مقسمة إلى فترات مدتها 4 ساعات للكشف عن إيقاعك اليومي. + تسهل الأعمدة اليومية مقارنة عاداتك من أسبوع لآخر. + توضح الأعمدة الأسبوعية اتجاه الاستماع خلال الشهر. + توضح الأعمدة الشهرية التغيرات الموسمية على مدار العام. + تلخص الأعمدة السنوية سجل استماعك الكامل. + لا توجد بيانات استماع بعد + اضغط على زر التشغيل لبدء بناء مخططك الزمني للاستماع + الإيقاع اليومي + الإيقاع الأسبوعي + الإيقاع الشهري + لمحة عن السنة + التطور العام عبر الوقت + مجمعة في فترات من 4 ساعات + مجمعة بحسب أيام الأسبوع + مجمعة بحسب أسابيع الشهر + مجمعة بحسب الشهر + مجمعة بحسب السنة + فترة الذروة + %1$d تشغيل + + أعلى الفئات + قارن طريقة استماعك بين الأنواع الموسيقية، الفنانين، الألبومات، والأغاني. + النوع + الفنان + الألبوم + الأغنية + الاستماع بحسب النوع + الاستماع بحسب الفنان + الاستماع بحسب الألبوم + الاستماع بحسب الأغنية + %1$d تشغيل • %2$d فنان + %1$d تشغيل • %2$d مسار + لا توجد بيانات فئات بعد + اضغط على زر التشغيل لإظهار أبرز تصنيفات استماعك + عادات الاستماع + لا توجد عادات مسجلة بعد + سنقوم بإظهار عادات استماعك بمجرد أن نتعرف على ذوقك الموسيقي بشكل أفضل. + إجمالي الجلسات + معدل الجلسة + أطول جلسة + جلسة/يوم + اليوم الأكثر نشاطاً + لا يوجد تشغيل بعد + فترة الاستماع الذروة + أبرز الفنانين + لا يوجد فنانون بارزون + استمر في الاستماع وسيظهر فنانوك المفضلون هنا. + \? + %1$d. %2$s + أبرز الألبومات + لا توجد ألبومات بارزة + الألبومات التي تعيد زيارتها بكثرة ستظهر هنا. + %1$d. %2$s + تركيز المسارات + كيف يتوزع وقت استماعك على مساراتك المفضلة والأكثر تشغيلاً. + لا توجد بيانات تركيز بعد + شغّل المزيد من المسارات لترى مدى تركيز استماعك. + الأعلى 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$d أيام + منذ ساعة واحدة + منذ %1$d ساعات + منذ دقيقة واحدة + منذ %1$d دقائق + أغنية %1$d + %1$d أغنية + الأسبوع %1$d + \ No newline at end of file diff --git a/app/src/main/res/values-ar/strings_library.xml b/app/src/main/res/values-ar/strings_library.xml new file mode 100644 index 000000000..905bca5b0 --- /dev/null +++ b/app/src/main/res/values-ar/strings_library.xml @@ -0,0 +1,597 @@ + + + + المكتبة + تبويبات المكتبة + انتقل مباشرة إلى أي تبويب أو أعد ترتيبها. + إعادة ترتيب التبويبات + + + الأغاني + الألبومات + الفنانون + قوائم التشغيل + المجلدات + المفضلة + + + تم إنشاء قائمة التشغيل بنجاح + يرجى تعيين مفتاح واجهة برمجة التطبيقات (API Key) لمزود الذكاء الاصطناعي أولاً + يرج تعيين مفتاح واجهة برمجة التطبيقات (API Key) لـ Gemini أولاً + تمت الإضافة إلى قائمة الانتظار + سيتم التشغيل تالياً + + + مراقبة النقل + الإعدادات + تعديل + إعادة ترتيب التبويبات + توسيع القائمة + + + يمكنك تحديد ما يصل إلى %1$d ألبومات + مجلد + مجلد + + + فرز حسب + المظهر + عرض قائمة التشغيل + شبكة + قائمة + الذاكرة الداخلية + بطاقة SD + بطاقة SD غير متوفرة حالياً. + السحابة + قنوات تليجرام السحابية + عرض المواضيع + القنوات + المواضيع + كلاهما + السحابة + السحابة فقط + + + جاري توليد البيانات الوصفية بالذكاء الاصطناعي… + + + خطأ أثناء تحميل الأغاني + خطأ أثناء تحميل الألبومات + خطأ أثناء تحميل الفنانين + إعادة المحاولة + + + لم يتم العثور على أغانٍ في مكتبتك. + جرّب إعادة فحص مكتبتك من الإعدادات إذا كانت لديك موسيقى على جهازك. + لم يتم العثور على أغانٍ + + + جديد + إنشاء قائمة تشغيل جديدة + استيراد قائمة تشغيل M3U + تحديد موقع الأغنية الحالية + كل الأغاني + سحابي + محلي + خيارات الفرز + + الكل + إلغاء التحديد + المزيد من الخيارات + + + جاري فحص ملفات الموسيقى… + جاري معالجة الملفات… + %1$d من %2$d ملفات + جاري مزامنة المكتبة… + اكتملت المزامنة + في الانتظار… + جاري مزامنة المكتبة… + جاري تنظيف ذاكرة التخزين المؤقت لغلاف الألبومات… + جاري مزامنة المصادر السحابية… + جاري فحص كلمات الأغاني… + + + لا توجد أغانٍ بعد + أضف موسيقى إلى جهازك أو قم بمزامنة مصدر سحابي لبدء الاستماع. + لم يتم العثور على أغانٍ محلية + جرّب تصفية مصدر آخر أو أعد فحص مكتبة الجهاز. + لم يتم العثور على أغانٍ سحابية + قم بمزامنة أغاني تليجرام أو NetEase، أو تحوّل إلى المصدر المحلي. + لا توجد ألبومات متوفرة + ستظهر الألبومات هنا بمجرد أن تحتوي مكتبتك على مسارات مجمعة. + لم يتم العثور على ألبومات محلية + المسارات المحلية مطلوبة لإنشاء مجموعات ألبومات محلية. + لم يتم العثور على ألبومات سحابية + ستظهر الأغاني السحابية التي تحتوي على بيانات الألبوم هنا بعد المزامنة. + لا يوجد فنانون متوفرون + يظهر الفنانون بعد فهرسة الأغاني من أي مصدر. + لم يتم العثور على فنانين محليين + لا تتوفر بيانات وصفية للفنانين للأغاني المحلية حالياً. + لم يتم العثور على فنانين سحابيين + تظهر إدخالات الفنانين السحابيين عند مزامنة الأغاني عن بُعد. + لا توجد أغانٍ مفضلة بعد + اضغط على أيقونة القلب أثناء تشغيل أغنية لحفظها هنا. + لا توجد أغانٍ محلية مفضلة + قم بتغيير تصفية المصدر أو أضف أغانٍ من جهازك إلى المفضلة. + لا توجد أغانٍ سحابية مفضلة + أضف مسارات تليجرام أو NetEase إلى المفضلة لرؤيتها في هذا العرض. + لم يتم العثور على مجلدات + ستظهر مجلدات وحدة التخزين الداخلية التي تحتوي على موسيقى هنا. + لا توجد قوائم تشغيل بعد + أنشئ أول قائمة تشغيل لتنظيم مكتبتك. + + + تعديل البيانات الوصفية للأغنية + تشغيل + تشغيل الأغنية + تشغيل الكل + تشغيل الكل + إضافة إلى المفضلة + إضافة الكل إلى المفضلة + إزالة من المفضلة + إزالة الكل من المفضلة + مشاركة ملف الأغنية عبر + مشاركة ملف الأغنية + مشاركة الكل كملف ZIP + تعذر مشاركة الأغنية: %1$s + إضافة إلى قائمة الانتظار + إضافة إلى قائمة الانتظار + التالي + التشغيل تالياً في قائمة الانتظار + إضافة إلى قائمة التشغيل + حذف + حذف الكل + جاري التحقق من الساعة + جاري النقل %1$d%% + جاري النقل إلى الساعة + عملية النقل جارية + إرسال إلى الساعة + الساعة غير متوفرة + إرسال الأغنية إلى الساعة + الساعة غير متوفرة + تعيين كـ + تعيين كنغمة نظام + اختر كيفية استخدام هذه الأغنية كنغمة للنظام + استخدام هذه الأغنية كـ + اختر المكان الذي يجب أن يقوم PixelPlayer بتثبيت هذا الصوت فيه. + نغمة رنين الهاتف + المكالمات الواردة + صوت الإشعار + الرسائل وتنبيهات التطبيقات + صوت المنبه + منبهات الساعة + تأكيد تغيير الصوت + هل تريد تعيين \"%1$s\" كـ %2$s الخاصة بك؟ + تعيين الصوت + تم تعيين \"%1$s\" كـ %2$s الخاصة بك + نغمة رنين + صوت إشعار + صوت منبه + يرجى تفعيل خيار \"تعديل إعدادات النظام\"، ثم العودة إلى PixelPlayer للإكمال تلقائياً. + لم يتم تفعيل صلاحية تعديل إعدادات النظام. + تم تعيين \"%1$s\" كنغمة رنين خاصة بك + يمكن استخدام الأغاني المحلية فقط كنغمات رنين. + تعذر إعداد ملف الصوت هذا ليصبح نغمة رنين. + تعذر تعيين نغمة الرنين: %1$s + الخيارات + الخيارات + التفاصيل + التفاصيل + المدة + النوع + الألبوم + الفنان + معلومات الأغنية + المزود + الملف + %1$d أغنية + تم تحديدها + %1$d قائمة تشغيل + %1$d ألبوم + تم تحديدها + الحد الأقصى: %1$d ألبومات لكل تحديد. + عمليات الإضافة لقائمة الانتظار والتشغيل تحترم ترتيب تحديدك. + %1$d نوع موسيقي + تم تحديدها + إجراء عمليات دفعة واحدة على جميع الأغاني ضمن هذه الأنواع الموسيقية. + + + الترتيب الافتراضي + العنوان (أ-ي) + العنوان (ي-أ) + الفنان + الفنان (ي-أ) + الألبوم + الألبوم (ي-أ) + تاريخ الإضافة + تاريخ الإضافة (الأقدم أولاً) + المدة + المدة (الأقصر أولاً) + سنة الإصدار + سنة الإصدار (الأقدم أولاً) + الأقل أغانٍ + الأكثر أغانٍ + الاسم (أ-ي) + الاسم (ي-أ) + عدد الأغاني (الأكثر) + عدد الأغاني (الأقل) + تاريخ الإنشاء + تاريخ الإنشاء (الأقدم أولاً) + تاريخ الإعجاب + تاريخ الإعجاب (الأقدم أولاً) + الأقل مجلدات فرعية + الأكثر مجلدات فرعية + + + العنوان + الفنان + الألبوم + تاريخ الإضافة + المدة + سنة الإصدار + عدد الأغاني + الاسم + عدد الأغاني + تاريخ الإنشاء + تاريخ الإعجاب + عدد المجلدات الفرعية + + + المصدر + الترتيب + تنازلي + تصاعدي + الترتيب الأصلي + انقر للتبديل إلى التصاعدي + انقر للتبديل إلى التنازلي + هذا الفرز يحافظ على ترتيبه الأصلي + المفتاح مفعّل + + + إعادة ترتيب تبويبات المكتبة + إعادة تعيين الترتيب + هل تريد إعادة تعيين ترتيب التبويبات إلى الوضع الافتراضي؟ + جاري إعادة ترتيب التبويبات… + مقبض السحب + + + اختر فناناً + فنان واحد + %1$d فنانين + الفنان الرئيسي + صفحة الفنان + + + إلغاء النقل + %1$s / %2$s + يعرض التقدم المباشر لعمليات نقل الموسيقى من الهاتف إلى الساعة + نقل البيانات إلى الساعة + جاري الإرسال إلى الساعة + تم الإلغاء + تم إلغاء النقل + اكتمل النقل + مكتمل + فشل النقل + فشل عملية النقل + توجد عدة عمليات نقل نشطة + %1$s • %2$s + جاري التحضير + جاري تحضير النقل إلى الساعة + جاري تحضير النقل… + جاري إرسال %1$d أغنية إلى الساعة + جاري الإرسال إلى الساعة + جاري بدء النقل… + جاري البدء + جاري النقل + عدد عمليات النقل: %1$d + + + تعديل الأغنية + إظهار المعلومات + تعديل البيانات الوصفية للأغنية + قد يؤثر تعديل البيانات الوصفية للأغنية على كيفية عرضها وتنظيمها في مكتبتك. هذه التغييرات دائمة وقد لا يمكن التراجع عنها. + فهمت ذلك + معلومات + غلاف الألبوم + اختر صورة مربعة وقم بضبطها بدقة ليظهر غلاف الألبوم بشكل رائع في جميع أنحاء التطبيق. + تغيير غلاف الألبوم + حذف غلاف الألبوم + العنوان + الفنان + الألبوم + فنان الألبوم + النوع + الملحن + رقم المسار + رقم القرص + تعديل الصوت للمسار ReplayGain (ديسيبل) + تعديل الصوت للألبوم ReplayGain (ديسيبل) + -6.50 + -8.20 + معاينة غلاف الألبوم الجديد + غلاف الألبوم الحالي للأغنية + ضبط غلاف الألبوم + استخدم إيماءات القرص والسحب للوصول إلى الإطار المثالي. + تطبيق غلاف الألبوم + تعذر تحميل الصورة المحددة + البحث عن كلمات الأغاني على lrclib.net + + + تعديل %d أغنية + سيتم تحديث الحقول المعدلة فقط. اترك الحقول فارغة للاحتفاظ بالقيم الحالية. + (قيم مختلطة) + (اختياري - اترك الحقل فارغاً للتخطي) + تم تحديث %d أغنية بنجاح + تم تحديث %1$d من أصل %2$d أغنية. تعذر تعديل بعض الملفات. + فشل تحديث الأغاني + غلاف الألبوم للمجموعة + سيؤدي هذا إلى استبدال غلاف الألبوم لجميع الأغاني المحددة البالغ عددها %d + تعيين غلاف الألبوم للكل + إزالة جميع أغلفة الألبومات + (أغلفة متعددة ومختلفة) + + + تم تجاهل قائمة التشغيل + + + إنشاء قائمة تشغيل + اختر طريقة الإنشاء. + يدوي + صمم الغلاف والأيقونة والشكل واشمل الأغاني بنفسك. + بواسطة الذكاء الاصطناعي + توليد قائمة تشغيل منسقة مع خيارات تحكم متقدمة. + يتطلب تهيئة مفتاح واجهة برمجة التطبيقات (Gemini API key) في الإعدادات. + إعداد مفتاح API + + + مختبر قوائم التشغيل بالذكاء الاصطناعي + إعادة تعيين + جاري التوليد… + توليد + الهدف + اسم قائمة التشغيل (اختياري) + ما هو الطابع الذي تريده لقائمة التشغيل هذه؟ + مثال: قيادة وقت الغروب مع أنغام هادئة + الاتجاه + الحالة المزاجية + النشاط + الحقبة الزمنية + محرك التنسيق + الحيوية + التحكم في إيقاع وسرعة الأغاني. 1 = هادئ/بطيء، 5 = حيوية عالية/سريع. + الاستكشاف + التحكم في مدى معرفتك بالاختيارات. 1 = الأغاني المفضلة الأكثر تشغيلاً، 5 = أغاني نادرة وغير مشغلة بكثرة. + أقل عدد أغانٍ + أقصى عدد أغانٍ + الفلاتر + منح الأولوية لأنواع موسيقية (اختياري) + مثال: synthwave, indie pop + تجنب أنواع موسيقية (اختياري) + مثال: metal, hard trap + اللغة المفضلة (اختياري) + مثال: الإنجليزية، العربية، معزوفة موسيقية + منح الأولوية للمفضلة + تجنب الكلمات غير اللائقة + معاينة الأمر (Prompt) + سيظهر أمرك النهائي هنا بمجرد إضافة تفضيلاتك. + تنسيق بدقة عالية + حدد الحالة المزاجية، النشاط، القيود، والعمق. + سيستخدم الذكاء الاصطناعي الأغاني من مكتبتك المحلية فقط. + أضف توجيهاً واحداً على الأقل للذكاء الاصطناعي. + يرجى تحديد نطاق أغانٍ صالح. + 5/%1$d + مخصص… + أدخل قيمة مخصصة + أدخل قيمتك المخصصة + + + أي حقبة + الطلب الأساسي: %1$s. + المزاج المستهدف: %1$s. + سياق النشاط: %1$s. + التركيز على الحقبة: %1$s. + الأولوية للأنواع: %1$s. + تجنب الأنواع: %1$s. + اللغة المفضلة: %1$s. + مستوى الحيوية المستهدف: 5/%1$d. + مستوى الاستكشاف المستهدف: 5/%1$d حيث 1 مألوف و 5 أغاني نادرة. + منح الأولوية للأغاني الأقرب لمفضلات المستمع عندما يكون ذلك ممكناً. + تجنب الكلمات غير اللائقة كلما توفرت بدائل. + الحفاظ على سلاسة الانتقالات وتجنب تكرار نفس الفنان بشكل متتابع. + + هادئ + حيوي + سعيد + غامض + رومانسي + كئيب + + + تمارين رياضية + تركيز + رحلة على الطريق + حفلة + دراسة + وقت متأخر من الليل + + + @string/playlist_creation_ai_era_any + السبعينات + الثمانينات + التسعينات + الألفينات + 2010s + 2020s + + + + لم يتم إنشاء أي قائمة تشغيل. + المس زر \"قائمة تشغيل جديدة\" للبدء. + قائمة تشغيل جديدة + اسم قائمة التشغيل + قائمة التشغيل الخاصة بي + + + إضافة %1$d أغنية إلى… + اختر قوائم التشغيل + البحث عن قوائم التشغيل… + تمت إضافة الأغاني إلى قوائم التشغيل + تم إنشاء قائمة التشغيل وإضافة الأغاني إليها + وحدة التخزين الداخلية + + + إضافة أغانٍ + إضافة الأغاني المحددة + إضافة + بحث أو تصفية الأغاني… + المفضلة + فشل تحميل الأغاني + تحميل المزيد + + + دمج قوائم التشغيل + أدخل اسماً لقائمة التشغيل المدمجة: + قائمة تشغيل مدمجة + سيؤدي هذا إلى دمج %1$d من قوائم التشغيل المحددة في قائمة واحدة. + + + لم يتم العثور على أغانٍ صالحة لتشغيلها + الأغنية غير موجودة في القائمة الحالية + تعذر تحديد موقع الأغنية + لا توجد أغانٍ في المكتبة + توقف التشغيل: انتهى %1$s (نهاية المسار). + مسار + لا توجد أغانٍ لخلطها. + الألبومات المحددة + لم يتم العثور على أغانٍ قابلة للتشغيل في الألبومات المحددة + لم يتم العثور على أغانٍ قابلة للتشغيل في الأنواع الموسيقية المحددة + تمت إضافة أول %1$d ألبومات فقط إلى قائمة الانتظار + تمت إضافة %1$d ألبومات إلى قائمة الانتظار (%2$d أغنية) + تعذر إضافة الألبومات المحددة إلى قائمة الانتظار + جميع الأغاني موجودة بالفعل في المفضلة + لم تكن أي من الأغاني في المفضلة + جاري إنشاء ملف ZIP… + فشلت المشاركة: %1$s + + لم تضف أي أغنية لقائمة الانتظار + تمت إضافة أغنية واحدة إلى قائمة الانتظار + تمت إضافة أغنيتين إلى قائمة الانتظار + تمت إضافة %d أغانٍ إلى قائمة الانتظار + تمت إضافة %d أغنية إلى قائمة الانتظار + تمت إضافة %d أغنية إلى قائمة الانتظار + + + لن يتم تشغيل أي أغنية تالياً + ستعمل أغنية واحدة تالياً + ستعمل أغنيتان تالياً + ستعمل %d أغانٍ تالياً + ستعمل %d أغنية تالياً + ستعمل %d أغنية تالياً + + + لم تضف أي أغنية للمفضلة + تمت إضافة أغنية واحدة إلى المفضلة + تمت إضافة أغنيتين إلى المفضلة + تمت إضافة %d أغانٍ إلى المفضلة + تمت إضافة %d أغنية إلى المفضلة + تمت إضافة %d أغنية إلى المفضلة + + + لم تزل أي أغنية من المفضلة + تمت إزالة أغنية واحدة من المفضلة + تمت إزالة أغنيتين من المفضلة + تمت إزالة %d أغانٍ من المفضلة + تمت إزالة %d أغنية من المفضلة + تمت إزالة %d أغنية من المفضلة + + + + لا توجد قوائم تشغيل لمشاركتها + مشاركة قوائم التشغيل + فشلت المشاركة: %1$s + لا توجد قوائم تشغيل لتصديرها + فشل التصدير: %1$s + الموسيقى/PixelPlayer Exports + يرجى تهيئة مفتاح Gemini API في الإعدادات. + تمت استعادة قائمة التشغيل + + لا توجد قوائم تشغيل للمشاركة + جاري مشاركة قائمة تشغيل واحدة + جاري مشاركة قائمتي تشغيل + جاري مشاركة %d قوائم تشغيل + جاري مشاركة %d قائمة تشغيل + جاري مشاركة %d قائمة تشغيل + + + لم يتم تصدير أي قائمة تشغيل + تم تصدير قائمة تشغيل واحدة إلى %2$s + تم تصدير قائمتي تشغيل إلى %2$s + تم تصدير %1$d قوائم تشغيل إلى %2$s + تم تصدير %1$d قائمة تشغيل إلى %2$s + تم تصدير %1$d قائمة تشغيل إلى %2$s + + + + معرف ألبوم غير صالح + لم يتم العثور على معرف الألبوم + خطأ أثناء تحميل بيانات الألبوم: %s + لم يتم العثور على الألبوم + + + معرف فنان غير صالح + لم يتم العثور على معرف الفنان + خطأ أثناء تحميل بيانات الفنان: %s + تعذر العثور على الفنان + + + لا يمكن حذف الأغنية التي يتم تشغيلها حالياً + تم حذف %1$d ملفات (تم تخطي %2$d ملفات - قيد التشغيل) + تم حذف %1$d من أصل %2$d ملفات + فشل حذف الملفات + تم حذف الملف + تعذر حذف الملف أو أنه غير موجود + تم إلغاء الحذف + هل تريد حذف الأغنية؟ + \"%1$s\" بواسطة %2$s\n\nسيتم حذف هذه الأغنية نهائياً من جهازك ولا يمكن استعادتها. + سيتم حذف هذه الأغاني نهائياً من جهازك ولا يمكن استعادتها. + + لم يتم حذف أي ملف + تم حذف ملف واحد + تم حذف ملفين + تم حذف %d ملفات + تم حذف %d ملفاً + تم حذف %d ملفاً + + + هل تريد حذف الأغاني؟ + هل تريد حذف أغنية واحدة؟ + هل تريد حذف أغنيتين؟ + هل تريد حذف %d أغانٍ؟ + هل تريد حذف %d أغنية؟ + هل تريد حذف %d أغنية؟ + + + + تم تحديث البيانات الوصفية بنجاح + جاري تحديث %1$d أغنية… + تم تحديث %1$d أغنية بنجاح! + تم تحديث %1$d أغنية. الفاشلة: %2$d + تم حفظ كلمات الأغنية بنجاح + فشل حفظ كلمات الأغنية + لا توجد كلمات متاحة لحفظها + تم رفض الإذن – لا يمكن تعديل الملفات + تم رفض الإذن – لا يمكن حفظ كلمات الأغاني + تم رفض الإذن – لا يمكن تعديل هذا الملف + + + يرجى تهيئة مفتاح API صالح لمزود الذكاء الاصطناعي المحدد في الإعدادات. + خطأ في الذكاء الاصطناعي: %s + رفض مزود الذكاء الاصطناعي الطلب لعدم وجود رصيد كافٍ أو حصة استخدام متوفرة بالحساب. + نموذج الذكاء الاصطناعي المحدد لم يعد متوفراً. حاول PixelPlayer التبديل تلقائياً إلى نموذج مدعوم. + لم يتمكن الذكاء الاصطناعي من العثور على أي أغانٍ تناسب طلبك. + اكتب فكرة لـ \"الميكس اليومي\" الخاص بك + تم تحديث الميكس اليومي بواسطة الذكاء الاصطناعي + تعذر التحديث: %s + لم يتمكن الذكاء الاصطناعي من العثور على أغانٍ لهذا الميكس + \ No newline at end of file diff --git a/app/src/main/res/values-ar/strings_player.xml b/app/src/main/res/values-ar/strings_player.xml new file mode 100644 index 000000000..38450d7e6 --- /dev/null +++ b/app/src/main/res/values-ar/strings_player.xml @@ -0,0 +1,210 @@ + + + + طي المشغل + يتم تشغيله الآن + بث سحابي + بث لاسلكي (Cast) + بلوتوث + تشغيل محلي + جاري الاتصال… + فتح قائمة الانتظار + + + الاستعداد للاتصال + اسمح لـ PixelPlayer برؤية الأجهزة القريبة وشبكة الـ Wi‑Fi الحالية حتى نتمكن من الحفاظ على مزامنة البث، وصوت البلوتوث، ومكبرات الصوت. + الأجهزة القريبة + مطلوبة لقراءة والتحكم في أجهزة الصوت المتصلة عبر البلوتوث. + موقع شبكة الـ Wi‑Fi + يتطلب نظام أندرويد إذون الموقع لمشاركة شبكة الـ Wi‑Fi (SSID) التي تتصل بها لتحديد أجهزة البث المتوافقة. + السماح بالوصول + نحن نستخدم هذه الصلاحيات فقط لربط الأجهزة البينية — كالبث، والتحكم في مكبرات الصوت القريبة، ومزامنة الصوت. + توصيل الجهاز + جاري الفحص بالقرب منك + جلسة البث + جاري الاتصال + تم الاتصال + هذا الهاتف + صوت البلوتوث + تشغيل محلي + جاري التشغيل + متوقف مؤقتاً + مستوى صوت الجهاز + مستوى صوت الهاتف + %1$d/%2$d + مستوى البطارية + مستوى الصوت + قطع الاتصال + الاتصال + قم بتفعيل الـ Wi-Fi أو البلوتوث + تحديث الاتصالات + Wi-Fi + مغلق + مفعّل + متصل + بلوتوث + مغلق + مفعّل + متصل + الأجهزة القريبة + تحديث الأجهزة + متصل + جاري الاتصال + متاح للاتصال + متاح + جاري الاتصال... + جاري البحث عن أجهزة… + تأكد من أن التلفزيون أو مكبر الصوت قيد التشغيل ومتصل بنفس شبكة الـ Wi‑Fi. + عناصر التحكم + الأجهزة + + + خادم وسائط البث (Cast) + جاري البث إلى الجهاز + تقديم الوسائط إلى جهاز البث + %1$s: %2$s + التقديم والتأخير غير متاح مؤقتاً لتنسيق الصوت هذا أثناء البث لأنه قد يتسبب في إنهاء جلسة البث قسرياً. + + + مؤقت النوم + المؤقت + %1$d دقيقة + تم ضبط المؤقت لمدة %1$d دقيقة. + مرة واحدة + + ولا مرة + مرة واحدة + مرتين + %d مرات + %d مرة + %d مرة + + عدد مرات التشغيل: %1$s + نهاية المسار الحالي + سيتوقف التشغيل عند نهاية المسار. + تفعيل المفتاح + وقت مخصص + إلغاء المؤقت + نهاية المسار + تم إلغاء المؤقت. + لا يمكن تفعيل خيار نهاية المسار: لا توجد أغنية نشطة حالياً. + تم إلغاء تفعيل مؤقت نهاية المسار: تغيرت الأغنية من %1$s إلى %2$s. + المسار السابق + المسار الحالي + ضبط مدة مخصصة + + + التالي في قائمة الانتظار + قائمة الانتظار فارغة حالياً. + + لا توجد مسارات مجهزة. + مسار واحد مجهز. + مساران مجهزان. + %d مسارات مجهزة. + %d مساراً مجهزاً. + %d مساراً مجهزاً. + + قائمة الانتظار + قائمة الانتظار فارغة. + إعادة ترتيب الأغنية + تبديل الوضع العشوائي + تبديل وضع التكرار + مؤقت النوم + المزيد من الإجراءات + تحديد موقع الأغنية الحالية + مسح قائمة الانتظار + مسح قائمة الانتظار + هل أنت متأكد من أنك تريد مسح جميع الأغاني من قائمة الانتظار باستثناء الأغنية الحالية؟ + حفظ كقائمة تشغيل + قائمة انتظار %1$s + قائمة الانتظار الحالية + تجاهل الأغنية + تمت إزالته + حفظ كقائمة تشغيل + إلغاء تحديد الكل + اسم قائمة التشغيل + البحث عن أغاني لتضمينها… + لا توجد أغاني تطابق \"%1$s\" + + لم يتم تحديد أي أغنية + تم تحديد أغنية واحدة + تم تحديد أغنيتين + تم تحديد %d أغانٍ + تم تحديد %d أغنية + تم تحديد %d أغنية + + حفظ باسم: %1$s + أدخل اسماً لقائمة التشغيل + إزالة من قائمة التشغيل + المزيد من الخيارات لـ %1$s + + + كلمات الأغاني + جاري تحميل كلمات الأغنية… + متزامنة + ثابتة + خيارات كلمات الأغاني + −0.5 + −0.1 + +0.1 + +0.5 + 0 ثانية + %1$+.1f ثانية + + + فشل البحث عن كلمات الأغنية + فشل جلب كلمات الأغنية من الخادم البعيد + انتهت مهلة الاتصال. يرجى التحقق من اتصالك بالإنترنت. + خطأ في الشبكة. يرجى التحقق من اتصالك بالإنترنت. + خطأ في الخادم (رمز %d). يرجى المحاولة مرة أخرى لاحقاً. + + + كلمات الأغنية متوفرة بالفعل. تم تخطي الجلب عبر الإنترنت. + تم العثور على كلمات مدمجة بالفعل. تم تخطي الجلب عبر الإنترنت. + تم العثور على ملف كلمات محلي (.lrc) بالفعل. تم تخطي الجلب عبر الإنترنت. + + + حفظ كلمات الأغنية + الترجمة بواسطة الذكاء الاصطناعي + تحتوي هذه الكلمات على ترجمة بالفعل + هذه الكلمات مكتوبة بالفعل بهذه اللغة + لم يتم تهيئة واجهة برمجة التطبيقات (API) + تمت ترجمة كلمات الأغنية بنجاح! + جاري ترجمة كلمات الأغنية… + إعادة تعيين الكلمات المستوردة + إعادة تعيين كلمات الأغنية؟ + هل أنت متأكد من أنك تريد إعادة تعيين كلمات هذه الأغنية؟ + المظهر + المحاذاة + محاذاة الكلمات لليسار + محاذاة الكلمات للوسط + محاذاة الكلمات لليمين + عناصر التحكم + ضبط المزامنة + إخفاء عناصر تحكم المزامنة + إظهار اللتننة (Romanization) + إظهار الترجمات + إيقاف الوضع الغامر (لمرة واحدة) + إبقاء الشاشة قيد التشغيل + + + حفظ كلمات الأغنية + اختر النسخة المراد حفظها: + متزامنة (مع الطوابع الزمنية) + عادية (نص فقط) + + + هل تود البحث عن كلمات الأغنية عبر الإنترنت؟ + إظهار خيارات كلمات الأغاني + فتح نافذة الاختيار دائماً بدلاً من التطبيق التلقائي لأول نتيجة مطابقة + جاري البحث عن كلمات الأغنية… + لم يتم العثور على كلمات الأغنية + لم نتمكن من العثور على الكلمات تلقائياً. يمكنك تعديل العنوان أو اسم الفنان والمحاولة بالبحث يدوياً. + عنوان الأغنية + الفنان (اختياري) + تم العثور على %d نتيجة مطابقة + متزامنة + %1$s • %2$s + تتوفر كلمات الأغاني بواسطة + https://lrclib.net/ + \ No newline at end of file diff --git a/app/src/main/res/values-ar/strings_screens.xml b/app/src/main/res/values-ar/strings_screens.xml new file mode 100644 index 000000000..f3bad8c12 --- /dev/null +++ b/app/src/main/res/values-ar/strings_screens.xml @@ -0,0 +1,244 @@ + + + + خطأ: معرف النوع الموسيقي مفقود + + + لنبدأ! + الخطوة %1$d من %2$d + يرجى منح الصلاحية المطلوبة أولاً. + يرجى منح جميع الصلاحيات المطلوبة. + مرحباً بك في + β + تجريبي (Beta) + دعنا نجهز كل شيء من أجلك. + صلاحية الوصول إلى الوسائط + يحتاج PixelPlayer إلى الوصول إلى ملفاتك الصوتية لبناء مكتبتك الموسيقية. + تم منح الصلاحية + منح صلاحية الوسائط + الإشعارات + قم بتفعيل الإشعارات للتحكم في الموسيقى من شاشة القفل ولوحة الإشعارات. + تفعيل الإشعارات + هل لديك نسخة احتياطية؟ + إذا كان لديك نسخة احتياطية سابقة من PixelPlayer، قم باستعادتها الآن وتخطي معظم خطوات الإعداد المتبقية على هذا الجهاز. + استيراد نسخة احتياطية + فحص النسخة الاحتياطية + جاري التحقق من حزمة النسخة الاحتياطية… + جاري استعادة النسخة الاحتياطية + تخطي / ليس الآن + استعادة النسخة الاحتياطية + راجع ما تريد استيراده قبل إنهاء الإعداد. + تم تحديد %1$d من أصل %2$d من الأقسام + تاريخ الإنشاء %1$s + نسخة احتياطية من الإصدار %1$s + إصدار غير معروف + استعادة المحدد + جاري الاستعادة + المجلدات المستبعدة + يتم فحص جميع المجلدات افتراضياً. اختر أي مواقع ترغب في تجاهلها عند بناء مكتبتك. + اختر المجلدات لتجاهلها + امنح صلاحيات وحدة التخزين أولاً + مظهر التطبيق + اختر المظهر الذي تريده قبل البدء في استكشاف مكتبتك. + داكن + المظهر الداكن الافتراضي لـ Material 3 في PixelPlayer. + فاتح + مظهر Material 3 أكثر سطوعاً في جميع أنحاء التطبيق. + حسب نظام التشغيل + مطابقة إعداد المظهر الحالي لهاتفك. + موصى به + يمكنك تغيير هذا لاحقاً من الإعدادات > المظهر > مظهر التطبيق. + تخطيط المكتبة + اختر الطريقة المفضلة لديك للتنقل داخل المكتبة. + الأغاني + الوضع المدمج + استخدام شريط التنقل البيضاوي المصغر + استخدام صف التبويبات القياسي + الأغاني + الألبومات + الفنانون + يمكنك تغيير هذا لاحقاً من الإعدادات > المظهر > التنقل في المكتبة. + التنقل في التطبيق + اختر نمط شريط التنقل السفلي. + النمط الافتراضي + شريط عائم بيضاوي بزوايا مستديرة + شريط قياسي بعرض الشاشة الكامل + تخصيص نصف قطر الزوايا + يمكنك تغيير هذا لاحقاً من الإعدادات > المظهر > نمط شريط التنقل. + المنبهات والتذكيرات + اختياري، ولكن موصى به إذا كنت تستخدم مؤقت النوم وتريد أن يقوم PixelPlayer بإيقاف التشغيل في الوقت المحدد تماماً. + منح الصلاحية + تحسين استهلاك البطارية + تقوم بعض أجهزة أندرويد بإغلاق تطبيقات الخلفية بشكل قسري. يرجى إيقاف تحسين البطارية لتطبيق PixelPlayer لمنع أي انقطاع غير متوقع أثناء التشغيل. + إيقاف تحسين البطارية + كل شيء جاهز! + أنت مستعد الآن للاستمتاع بموسيقاك. + + + بحث… + بحث + مسح نص البحث + عمليات البحث الأخيرة + مسح الكل + سجل البحث + حذف عنصر من سجل البحث + لا توجد نتائج + لا توجد نتائج لـ \"%1$s\" + لم يتم العثور على شيء + جرّب استخدام كلمة بحث أخرى أو تحقق من الفلاتر الخاصة بك. + لم يتم العثور على نتائج. + تصفح حسب النوع الموسيقي + لا توجد أنواع موسيقية متوفرة. + + + تشغيل %1$s + طي %1$s + توسيع %1$s + تعديل صورة الفنان + تغيير الصورة + إعادة تعيين إلى الافتراضي + تشغيل عشوائي للفنان + + + القرص %d + غلاف %1$s + %1$s · %2$s + + + لم يتم العثور على قائمة التشغيل. + قائمة التشغيل هذه فارغة. + المس \"إضافة أغانٍ\" للبدء. + هذا المجلد لا يحتوي على أغانٍ. + فرز الأغاني + المزيد من الخيارات + خيارات قائمة التشغيل + تعديل قائمة التشغيل + حذف قائمة التشغيل + هل تريد حذف قائمة التشغيل؟ + هل أنت متأكد من أنك تريد حذف قائمة التشغيل هذه؟ + تعيين الانتقال الافتراضي + تصدير قائمة التشغيل + %1$s • %2$s + تشغيل + إضافة + إضافة أغانٍ + إزالة + إزالة الأغاني + إعادة ترتيب + إعادة ترتيب الأغاني + + + الانتقالات العامة + قواعد قائمة التشغيل + يتم تطبيق هذه الإعدادات على جميع مصادر التشغيل ما لم يتم تجاوزها. + تهيئة السلوك الافتراضي لقائمة التشغيل هذه تحديداً. + حالة التفعيل + الوضع الافتراضي العام + الوضع الافتراضي لقائمة التشغيل + تابع للإعدادات العامة + تجاوز مخصص + تجاوز مخصص + قم بالتفعيل لتعيين قواعد خاصة بقائمة التشغيل هذه. + يتم استخدام الإعدادات الافتراضية العامة + تم حفظ التغييرات بنجاح + نمط الانتقال + كيفية تداخل المسارات الصوتية معاً + بدون انتقال + تلاشي متبادل (Crossfade) + مدة الانتقال + إجمالي التداخل %1$d ثوانٍ + إعادة تعيين الانتقال + الأغنية الحالية + الأغنية التالية + ستتداخل المسارات الصوتية لمدة %1$d ثوانٍ + منحنيات الصوت + ضبط ميل وتدرج الصوت بدقة + تلاشٍ تدريجي للخارج (Fade Out) + تلاشٍ تدريجي للداخل (Fade In) + + + قائمة تشغيل ذكية جديدة + قائمة تشغيل جديدة + إضافة أغانٍ + رجوع أو إلغاء + التالي + إنشاء + تعديل قائمة التشغيل + تجميعة صور تلقائية + إضافة صورة + اختر صورة + تغيير + إزالة + اسم قائمة التشغيل + الميكس الرائع الخاص بي + تعديل الغلاف + اضبط صورة الغلاف + استخدم إيماءات القرص والسحب للوصول إلى الإطار المثالي + يدوي + ذكي + التوليد بالذكاء الاصطناعي + قاعدة ذكية + الافتراضي + صورة + أيقونة + لون الخلفية + رمز الأيقونة + نمط الشكل + خصائص الشكل + نصف قطر الزوايا + النعومة + الأضلاع + المنحنى + التدوير + الحجم + الأكثر تشغيلاً + المسارات الموسيقية الأكثر استماعاً لديك. + المشغلة حديثاً + الأغاني التي استمعت إليها في الآونة الأخيرة. + مفضلات منسية + مساراتك المفضلة التي لم تقم بتشغيلها منذ فترة. + جواهر جديدة + المسارات المضافة حديثاً ذات نسب تشغيل منخفضة. + + + تعبئة سريعة للنوع الموسيقي + فرز وتشغيل + خلط عشوائي + فرز حسب + الفنان + الألبوم + العنوان + فنان عام + خلط عشوائي لـ %1$s + + + اختر الأغاني + اختر النوع الموسيقي + بحث عن الأغاني + نوع موسيقي جديد + إضافة مخصص + إضافة نوع موسيقي مخصص + اسم النوع الموسيقي + اختر أيقونة + النوع الموسيقي: %1$s + اختر نوعاً موسيقياً + تعبئة سريعة + + + مساحة الـ DJ + جاري التحميل… + المنصة %1$d + تحميل أغنية + لم يتم تحميل أي أغنية + + ميزة فصل المسارات (Stems) غير متوفرة بعد. + مستوى الصوت + السرعة + ممازج الصوت (Crossfader) + المنصة 1 + المنصة 2 + اختر أغنية + تشغيل/إيقاف مؤقت + غلاف الأغنية + x%1$.2f + \ No newline at end of file diff --git a/app/src/main/res/values-ar/strings_settings.xml b/app/src/main/res/values-ar/strings_settings.xml new file mode 100644 index 000000000..ee27fe09a --- /dev/null +++ b/app/src/main/res/values-ar/strings_settings.xml @@ -0,0 +1,643 @@ + + + + إدارة الموسيقى + إدارة المجلدات، تحديث المكتبة، خيارات التحليل + المظهر + السمات، التخطيط، والأنماط المرئية + التشغيل + سلوك الصوت، الانتقال التلاشي، والتشغيل في الخلفية + السلوك + الإيماءات، الاهتزاز، وسلوك التنقل + تكامل الذكاء الاصطناعي (تجريبي) + مزودو الذكاء الاصطناعي، مفاتيح API، وإعدادات النماذج + النسخ الاحتياطي والاستعادة + تصدير واستعادة بيانات التطبيق الشخصية + خيارات المطور + الميزات التجريبية وتصحيح الأخطاء + معادل الصوت + ضبط ترددات الصوت والإعدادات المسبقة + إمكانيات الجهاز + مواصفات الصوت، برامج الترميز، ومعلومات وحدة فك الترميز + الحسابات + إدارة Telegram وGoogle Drive وNetEase والمزيد من الخدمات + حول + معلومات التطبيق، الإصدار، والإشادات + + + تشغيل + إيقاف + مفعل + معطل + فتح + تحديد الكل + مسح التحديد + إغلاق الإشعار + + + هيكل المكتبة + المجلدات المستثناة + سيتم تخطي المجلدات هنا عند فحص المكتبة. + الفنانون + خيارات تحليل وتنظيم الفنانين المتعددين. + التصفية + الحد الأدنى لمدة الأغنية + الحد الأدنى للمقاطع لكل ألبوم + حد ذاكرة التخزين المؤقت لغلاف الألبوم + المزامنة والفحص + تحديث المكتبة + فحص المكتبة بالكامل بحثًا عن الملفات الجديدة والمعدلة. + إعادة فحص كاملة + جاري إجراء إعادة فحص كاملة + بدأت إعادة الفحص الكاملة… + تمت مزامنة المكتبة + إعادة بناء قاعدة البيانات + إعادة بناء قاعدة البيانات؟ + سيؤدي هذا إلى إعادة بناء مكتبة الموسيقى الخاصة بك بالكامل من الصفر. سيتم فقدان جميع الكلمات المستوردة والمفضلات والبيانات الوصفية المخصصة. لا يمكن التراجع عن هذا الإجراء. + إعادة بناء + جاري إعادة بناء قاعدة البيانات + جاري إعادة بناء قاعدة البيانات… + فحص ملفات .lrc تلقائيًا + فحص وتعيين ملفات .lrc الموجودة في نفس المجلد تلقائيًا أثناء مزامنة المكتبة. + إدارة كلمات الأغاني + أولوية مصدر الكلمات + اختر المصدر الذي سيتم تجربته أولاً عند جلب الكلمات. + المضمنة أولاً + عبر الإنترنت أولاً + المحلية (lrc.) أولاً + إعادة تعيين الكلمات المستوردة + إزالة جميع الكلمات المستوردة من قاعدة البيانات. + إعادة تعيين الكلمات المستوردة؟ + لا يمكن التراجع عن هذا الإجراء. + + + تحديث + كل شيء مسموح به افتراضيًا. انقر على مجلد لتعليمه كمستثنى من الفحص. + لا توجد مجلدات فرعية هنا + انتقل لأعلى + انتقل إلى الجذر + + + إعادة الفحص مطلوبة + تم تغيير إعدادات الفنان. أعد فحص مكتبتك لتطبيق التغييرات. + إعادة فحص + جاري الفحص… + تحليل الفنانين المتعددين + فواصل الأحرف + الحالية: %1$s + فواصل الكلمات + لا شيء + الحالية: %1$s + تكوين + استخراج الفنانين من العنوان + اكتشاف الكلمات مثل feat.، ft.، with في عناوين الأغاني + تنظيم المكتبة + تجميع حسب فنان الألبوم + إظهار ألبومات التعاون تحت الفنان الرئيسي + حول تحليل الفنانين المتعددين + + يقسم PixelPlayer علامات الفنان باستخدام فواصل الأحرف (/, ;, &) وفواصل الكلمات (feat.، ft.، vs.، x). تتم مطابقة فواصل الكلمات بدون تمييز لحالة الأحرف. + يكتشف \"استخراج الفنانين من العنوان\" أنماطًا مثل (feat. Artist) في عناوين الأغاني. + يمكن استخدام الشرطة المائلة العكسية (\) للهروب من فواصل الأحرف. + + أمثلة + \u2192 + \u266A + \"فنان1/فنان2\" + فنان1، فنان2 + \"ديويت feat. ريانا\" + ديويت، ريانا + \"مارشميلو x باستيل\" + مارشميلو، باستيل + \"أغنية (ft. ب) \" بواسطة أ + أ، ب + \"AC\\DC\" + AC/DC (تم الهروب) + + + الفواصل + الفواصل الحالية + انقر على فاصل لإزالته. هناك حاجة إلى فاصل واحد على الأقل. + إضافة فاصل جديد + مثال: / أو ; + إضافة فاصل + الفواصل الافتراضية + إعادة تعيين الفواصل؟ + سيؤدي هذا إلى مسح جميع الفواصل المخصصة واستعادة الفواصل الافتراضية. لا يمكن التراجع عن هذا الإجراء. + تمت إعادة تعيين الفواصل إلى الافتراضية + هناك حاجة إلى فاصل واحد على الأقل + تمت إضافة الفاصل + الفاصل موجود بالفعل أو غير صالح + مسافة + + + فواصل الكلمات + فواصل الكلمات الحالية + هذه الكلمات المفتاحية تقسم أسماء الفنانين عندما تكون محاطة بمسافات. تتم المطابقة بدون تمييز لحالة الأحرف. انقر للإزالة. + لم يتم تكوين أي فواصل كلمات + إضافة فاصل كلمات جديد + مثال: feat. أو ft. + إضافة فاصل كلمات + كيف تعمل فواصل الكلمات + تتم مطابقة فواصل الكلمات بدون تمييز لحالة الأحرف مع وجود مسافات حولها.\n\nتتطلب الفواصل ذات الحرف الواحد (مثل \"x\") مسافات على كلا الجانبين لتجنب المطابقات الخاطئة.\n\nأمثلة:\n \"ديويت feat. ريانا\" -> ديويت، ريانا\n \"مارشميلو x باستيل\" -> مارشميلو، باستيل\n \"أ ضد ب\" -> أ، ب + إعادة تعيين فواصل الكلمات؟ + سيؤدي هذا إلى مسح جميع فواصل الكلمات المخصصة واستعادة الكلمات المفتاحية الافتراضية. لا يمكن التراجع عن هذا الإجراء. + تمت إضافة فاصل الكلمات + موجود بالفعل أو غير صالح + تمت إعادة تعيين فواصل الكلمات إلى الافتراضية + + + تحضير المزامنة + قراءة MediaStore + معالجة المقاطع + حفظ في قاعدة البيانات + فحص ملفات الكلمات + تنظيف ذاكرة التخزين المؤقت لغلاف الألبوم + مزامنة المصادر السحابية + إكمال المزامنة + %1$s • %2$d%% (%3$d/%4$d) + %1$s… + + + السمة العامة + لغة التطبيق + اختر اللغة المستخدمة في واجهة التطبيق. + افتراضي النظام + الإنجليزية + الإسبانية + الألمانية + الفرنسية + الروسية + الصينية المبسطة + الإندونيسية + الإيطالية + الكورية + النرويجية (بوكمول) + التركية + العربية + سمة التطبيق + التبديل بين السمة الفاتحة أو الداكنة أو اتباع مظهر النظام. + سمة فاتحة + سمة داكنة + اتباع النظام + استخدام زوايا ناعمة + استخدم زوايا ذات أشكال معقدة لتحسين الجماليات بشكل فعال، ولكن قد يؤثر على الأداء على الأجهزة منخفضة المواصفات + تعطيل تأثيرات التمويه + إيقاف تشغيل تأثيرات التمويه في جميع أنحاء التطبيق لتوفير البطارية والموارد. + إظهار شريط التمرير + عرض شريط تمرير جانبي على قوائم الموسيقى للتمرير السريع + التشغيل الآن + سمة المشغل + اختر مظهر المشغل العائم. + غلاف الألبوم + ديناميكي النظام + إظهار معلومات ملف المشغل + إظهار برنامج الترميز ومعدل البت ومعدل العينة في قسم تقدم المشغل. + نمط لوحة ألوان غلاف الألبوم + الحالي: %1$s. افتح معاينة مباشرة واختر النمط. + نمط البانوراما الدائرية + اختر مظهر البانوراما الدائرية للألبوم. + بدون إطلالة + إطلالة واحدة + مجموعة الصور الرئيسية + نمط المجموعة + اختر ترتيب الأشكال لمجموعة مزيجك الخاص. + تدوير الأنماط تلقائيًا + التنقل بين أنماط المجموعة في كل مرة تزور فيها الصفحة الرئيسية. + + + شريط التنقل + نمط شريط التنقل + اختر مظهر شريط التنقل. + افتراضي + عرض كامل + الوضع المضغوط + إظهار الأيقونات فقط وتقليل ارتفاع شريط التنقل. + نصف قطر زاوية شريط التنقل + ضبط نصف قطر زاوية شريط التنقل. + شاشة الكلمات + كلمات غامرة + إخفاء عناصر التحكم تلقائيًا وتكبير النص. + تأخير الإخفاء التلقائي + الوقت قبل إخفاء عناصر التحكم. + 3 ثوانٍ + 4 ثوانٍ + 5 ثوانٍ + 6 ثوانٍ + التنقل في التطبيق + التبويب الافتراضي + اختر تبويب بدء التشغيل الافتراضي. + الرئيسية + بحث + المكتبة + التنقل في المكتبة + اختر كيفية التنقل بين تبويبات المكتبة. + صف التبويبات (افتراضي) + قرص مضغوط وشبكة + + + الألوان + نمط اللوحة + اختر ألوان الألبوم لواجهة المشغل. + بقعة نغمية + متوازنة وهادئة. + زاهية + لمسات عالية التشبع. + تعبيرية + تحولات جريئة في الدرجة والتباين. + سلطة فواكه + لمسات مرحة متناوبة. + دقة الألوان + 0 يحتفظ بالضبط الحالي. القيم الأعلى تبقى أقرب إلى الدرجة السائدة لغلاف الألبوم. + الحالي + أكثر دقة + 0 • الحالي + %1$d • خفيف + %1$d • متوازن + %1$d • دقيق + + + ضبط نصف قطر الزاوية + طابق زوايا شكل شريط التنقل مع الزوايا المادية لجهازك للحصول على مظهر سلس. + نصف قطر الزاوية + %1$d dp + + + التشغيل في الخلفية + الاستمرار في التشغيل بعد الإغلاق + إذا تم الإيقاف، فإن إزالة التطبيق من التطبيقات الأخيرة ستوقف التشغيل. + تحسين البطارية + تعطيل تحسين البطارية لمنع انقطاعات التشغيل. + تحسين البطارية معطل بالفعل + تعذر فتح إعدادات البطارية + تطبيع مستوى الصوت (ReplayGain) + تمكين ReplayGain + تطبيع مستويات الصوت باستخدام بيانات ReplayGain من ملفات الصوت. + وضع الكسب + المقطع: تطبيع كل أغنية. الألبوم: تطبيع لكل ألبوم. + مقطع + ألبوم + الإرسال + التشغيل التلقائي عند توصيل/فصل الإرسال + بدء التشغيل فورًا بعد تبديل اتصالات الإرسال. + سماعات الرأس + استئناف التشغيل عند إعادة توصيل سماعات الرأس + إذا تم إيقاف التشغيل مؤقتًا بسبب إزالة سماعات الرأس، فاستأنف تلقائيًا عند إعادة توصيلها. + قائمة الانتظار والانتقالات + انتقال تلاشي + تمكين الانتقال السلس بين الأغاني. + مدة الانتقال التلاشي + وضع Hi-Fi + إخراج صوت عائم 32 بت. قم بتعطيله إذا كان التشغيل متقطعًا على جهازك. + غير مدعوم على هذا الجهاز (PCM_FLOAT AudioTrack غير متوفر). + تشغيل عشوائي مستمر + تذكر إعداد التشغيل العشوائي حتى بعد إغلاق التطبيق. + إظهار سجل قائمة الانتظار + إظهار الأغاني التي تم تشغيلها مسبقًا في قائمة الانتظار. + + + المجلدات + تتحكم إيماءة الرجوع في المجلدات + في علامة تبويب المجلدات، يؤدي الرجوع بالنظام إلى التنقل في مكدس المجلدات قبل مغادرة المكتبة. + إيماءات المشغل + النقر على الخلفية يغلق المشغل + انقر على الخلفية الضبابية لإغلاق لوحة المشغل. + اللمسة الاهتزازية + ردود الفعل اللمسية + تمكين ردود فعل الاهتزاز في جميع أنحاء التطبيق. + + + مزود الذكاء الاصطناعي + المزود + اختر مزود الذكاء الاصطناعي الخاص بك + وضع الرمز الآمن + تشغيل — سريع ورخيص. يرسل الحد الأدنى من البيانات (~1 ألف رمز) إلى الذكاء الاصطناعي. + إيقاف — سياق عميق. يرسل ملف الاستماع الكامل (~8 آلاف رمز) للحصول على نتائج أكثر ثراءً. + بيانات الاعتماد + مفتاح API %1$s + احصل عليه من %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) + اختيار النموذج + جاري تحميل النماذج المتاحة… + فشل تحميل النماذج + نموذج الذكاء الاصطناعي + حدد نموذجًا. + أدخل مفتاح API + سلوك المطالبة + مطالبة النظام + تخصيص كيفية تصرف الذكاء الاصطناعي. + المطالبات المسبقة الإعداد + أدخل مطالبة النظام… + منسق محترف + أنت \'محرك الإيقاع\'، منسق موسيقي عالمي المستوى وسيد التدفق الصوتي. هدفك هو بناء تجارب استماع سلسة وعالية الدقة. إعطاء الأولوية للتوافق التوافقي، والانتقالات المنطقية لسرعة الإيقاع، والتوازن المتطور بين المفضلات المألوفة والاكتشاف المنطقي والراقي. + مبدع متمرد + أنت مستكشف موسيقي طليعي متخصص في \'التماسك غير المتوقع\'. مهمتك هي كسر الحدود التقليدية بين الأنواع من خلال تحديد أوجه التشابه الصوتي غير الواضحة. إعطاء الأولوية للمقطوعات النادرة، والأنسجة التجريبية، والجدة الفنية مع الحفاظ على منطق انتقالي مفاجئ ولكن لا يمكن إنكاره. + أمين مكتبة صارم + أنت مهندس قاعدة بيانات موسيقية دقيق. يعتمد منطقك على دقة البيانات الوصفية المطلقة والالتزام الصارم بالفئات. تقليل الاكتشاف الخوارزمي لصالح الاتساق الصارم بين الأنواع، ومطابقة مستوى الطاقة، وزيادة استرجاع التفضيلات المحددة للغاية من قبل المستخدم. + دليل الأجواء + أنت سيد الأنسجة المحيطة والتدفق منخفض الطاقة. ركز حصريًا على المسارات التي تسهل حالة \'التركيز العميق\' أو \'السكينة\'. إعطاء الأولوية للدفء الصوتي، والترتيبات البسيطة، والانتقالات اللطيفة، مع تجنب الأصوات العابرة عالية العابرة أو التحولات المفاجئة في النطاق الديناميكي. + عاشق الصوتيات + أنت محلل لعشاق الصوت يركز على تعقيد الإنتاج والآلات الموسيقية. إعطاء الأولوية للمسارات التي تتميز بنطاق ديناميكي عالٍ، وإيقاعات متعددة معقدة، وجودة مجال صوتي فائقة. تفضل مقطوعات الاستماع النشط التي تكافئ المستخدم على الاهتمام بالدقة التقنية وتفاصيل الترتيب. + محفز الطاقة + أنت مولد إيقاع عالي الزخم. تتمحور فلسفتك حول خطوط الجهير القوية، والكثافة الإيقاعية، والإيقاعات المعدية. إعطاء الأولوية للتوافق عالي السرعة مع النادي، والطاقة المتزامنة، والتوتر الإيقاعي المستمر للحفاظ على معدل ضربات قلب المستخدم وتحفيزه عند المستوى الأقصى. + تقرير استخدام الذكاء الاصطناعي + الاستهلاك الإجمالي + تتبع %1$s رمزًا\nالمطالبة: %2$s | الإخراج: %3$s | الفكر: %4$s + مسح السجلات + سجل نشاط الذكاء الاصطناعي (%1$d) + %1$s · %2$s + إظهار + إخفاء + + + كيف يعمل النسخ الاحتياطي + اختر الأقسام، وقم بتصدير ملف .pxpl، واستورده لاحقًا. لا تؤدي الاستعادة إلا إلى استبدال الأقسام التي تحددها. + إنشاء نسخة احتياطية + تصدير النسخة الاحتياطية + لم يتم تحديد أي أقسام. + تم تحديد جميع الأقسام. + تم تحديد %1$d من أصل %2$d قسمًا. + %1$s ينشئ ملف نسخ احتياطي بصيغة .pxpl. + تحديد وتصدير + استعادة النسخة الاحتياطية + استيراد النسخة الاحتياطية + تحديد واستعادة + تصفح أو اختر من النسخ الاحتياطية الحديثة. ستستبدل البيانات المحددة البيانات الحالية. + + + اختر بالضبط ما تريد تضمينه في حزمة النسخ الاحتياطي. + حدد ملف نسخ احتياطي بصيغة .pxpl لفحصه. ستختار الأقسام التي تريد استعادتها في الخطوة التالية. + تم تحديد %1$d من أصل %2$d قسمًا + تم تحديد %1$d من أصل %2$d وحدة + النسخ الاحتياطية الحديثة + لا توجد نسخ احتياطية حديثة + ستظهر النسخ الاحتياطية المستوردة مسبقًا هنا. + %1$d إدخال(إدخالات) • سيحل محل البيانات الحالية + تصدير .pxpl + استعادة المحدد + جاري النقل… + PixelPlayer_Backup_%1$d.pxpl + إنشاء نسخة احتياطية + استعادة النسخة الاحتياطية + %1$d%% + %1$s • %2$s + جارٍ التصدير + جارٍ الاستيراد + جارٍ الاستعادة + إزالة من السجل + جارٍ الفحص… + تصفح ملف + الخطوة %1$d من %2$d + استعادة الوحدات + تفاصيل النسخة الاحتياطية + تم الإنشاء + إصدار التطبيق + المخطط + الجهاز + غير معروف + · %1$s + %1$d وحدة(وحدات) · الإصدار %2$s · المخطط v%3$d + + تحديد الكل + مسح التحديد + + + نسخة احتياطية غير صالحة: %1$s + تحضير الاستعادة + بدء مهمة الاستعادة. + تحضير النسخ الاحتياطي + بدء مهمة النسخ الاحتياطي. + تمت استعادة النسخة الاحتياطية بنجاح + اكتملت الاستعادة مع بعض المشكلات التي لم يتم حلها. + تعذر إكمال الاستعادة: %1$s + فشلت الاستعادة: %1$s + تم تصدير البيانات بنجاح + فشل التصدير: %1$s + تمت استعادة البيانات بنجاح + اكتملت الاستعادة بمشكلات لم تحل. فشل: %1$s + الإصدار v%1$d + %1$s %2$s + + + تجارب + تجريبي + تجارب تحميل واجهة المشغل وتبديلاته. + اختبار تدفق الإعداد + تشغيل شاشة إعداد البدء للاختبار. + الصيانة + فرض إعادة توليد المزيج اليومي + إعادة إنشاء قائمة تشغيل المزيج اليومي فورًا. + إعادة توليد المزيج اليومي + إعادة توليد المزيج اليومي؟ + سيؤدي هذا إلى تجاهل المزيج الحالي وإنشاء مزيج جديد بناءً على عادات الاستماع الأخيرة. + بدأت إعادة توليد المزيج اليومي + فرض إعادة توليد الإحصائيات + مسح ذاكرة التخزين المؤقت وإعادة حساب إحصائيات التشغيل. + إعادة توليد + جارٍ العمل… + إعادة توليد الإحصائيات + إعادة توليد الإحصائيات؟ + سيؤدي هذا إلى مسح ذاكرة التخزين المؤقت للإحصائيات وفرض إعادة حساب من سجل قاعدة البيانات. + بدأت إعادة توليد الإحصائيات + فرض إعادة توليد لوحة ألوان الألبوم + إعادة بناء جميع متغيرات اللوحة المخزنة مؤقتًا لكل غلاف ألبوم، أو اختر واحدًا لتحديثه. + إعادة توليد الكل + إعادة توليد جميع لوحات ألوان الألبوم؟ + سيؤدي هذا إلى مسح بيانات السمة المخزنة مؤقتًا وإعادة بناء جميع أنماط اللوحة لـ %1$d غلاف ألبوم فريد. + جارٍ إعادة التوليد… + جارٍ إعادة توليد لوحات ألوان الألبوم… + إعادة بناء متغيرات اللوحة المخزنة مؤقتًا لـ %1$d غلاف ألبوم فريد. قد يستغرق هذا بعض الوقت في المكتبات الكبيرة. + اكتمل %1$d من %2$d + تم إعادة توليد %1$d لوحة ألوان غلاف ألبوم + تم إعادة توليد %1$d من أصل %2$d لوحة ألوان غلاف ألبوم + اختر أغنية + حدد أغنية لمسح بيانات السمة المخزنة مؤقتًا وإعادة توليد جميع أنماط اللوحة من غلاف الألبوم. + البحث حسب العنوان أو الفنان أو الألبوم + لا توجد أغانٍ تطابق بحثك. + لم يتم العثور على أغانٍ بها غلاف ألبوم. + جارٍ إعادة توليد اللوحة… + تم إعادة توليد اللوحة لـ %1$s + تعذر إعادة توليد اللوحة لـ %1$s + التشخيص + اختبار تشغيل تعطل + محاكاة تعطل لاختبار نظام الإبلاغ عن الأعطال. + تم تشغيل اختبار التعطل من خيارات المطور - هذا مقصود لاختبار نظام الإبلاغ عن الأعطال + + + تجريبي + تحسينات تحميل واجهة المشغل + كلمات متحركة (للأجهزة عالية المواصفات) + يستخدم رسومًا متحركة مرنة وتأثيرات بصرية للكلمات. قد يتسبب في تساقط الإطارات على الأجهزة منخفضة المواصفات. + تأثير تمويه الكلمات + يطبق تمويهًا لعمق المجال على الكلمات غير النشطة. + قوة التمويه + ضبط شدة تأثير التمويه. + %1$.1f× + الخطوة 1 · اختر ما تريد تأخيره + تأخير كل شيء + تعليق محتوى المشغل بالكامل حتى يتم توسيع خلفية اللوحة بالكامل. + البانوراما الدائرية للألبوم + تأخير غلاف الألبوم والبانيوراما الدائرية حتى يتم توسيع اللوحة. + البيانات الوصفية للأغنية + تأخير العنوان والفنان وإجراءات الكلمات/قائمة الانتظار. + شريط التقدم + تأخير الجدول الزمني وتسميات الوقت حتى اكتمال التوسيع. + عناصر التحكم في التشغيل + تأخير عناصر تحكم التشغيل/الإيقاف المؤقت والبحث والمفضلة. + جميع المكونات المؤجلة نشطة. قم بتعطيل \"تأخير كل شيء\" لتخصيص كل جزء. + الخطوة 2 · تكوين سلوك العناصر النائبة + استخدام عناصر نائبة للعناصر المؤجلة + الحفاظ على استقرار التخطيط عن طريق عرض عناصر نائبة خفيفة الوزن أثناء انتظار المكونات للتوسيع. + الخطوة 3 · اختر متى تتحول العناصر النائبة إلى محتوى حقيقي + حدد وضعًا واحدًا. يستخدم وضع العتبة أشرطة التمرير؛ ينتظر وضع تحرير السحب حتى تحرر إيماءة اللوحة. + قم بتمكين مكون مؤجل واحد على الأقل لفتح وضع المشغل. + العتبة + يستخدم نسبة التوسيع. + تحرير السحب + يتغير فقط بعد تحرير الإيماءة. + عتبة التوسيع + مدى توسع اللوحة قبل أن تصبح المكونات المؤجلة مرئية. + يظهر المحتوى عند توسيع %1$d%% + التطبيق أيضًا عند إغلاق المشغل + استخدام عتبة الإغلاق للعودة إلى العناصر النائبة أثناء التصغير. + عتبة الإغلاق + مقدار التصغير المطلوب قبل أن تتولى العناصر النائبة زمام الأمور مرة أخرى. + تظهر العناصر النائبة بعد تصغير %1$d%% + يتجاوز وضع تحرير السحب العتبات وسلوك الإغلاق. يحدث التبديل فقط عند انتهاء إيماءة سحب اللوحة. + جعل العناصر النائبة شفافة + تحتفظ العناصر النائبة بمساحة التخطيط الخاصة بها ولكنها تصبح غير مرئية. + الجودة البصرية + دقة غلاف الألبوم + منخفضة (256 بكسل) - أداء أفضل + متوسطة (512 بكسل) - متوازن + عالية (800 بكسل) - أفضل جودة + أصلية - أقصى جودة + + + التشغيل يحتاج إلى مراجعة + جاهز للتشغيل + -- + التنسيقات + وحدات فك ترميز الأجهزة + الأغاني المحلية + تخزين الموسيقى المحلية + حجم الموسيقى + %1$d أغنية محلية + متاح + إجمالي %1$s + بصمة الموسيقى + المستخدم من الجهاز + %1$d%% + <1% + %1$d أغنية سحابية + %1$d ملف غير قابل للقراءة + مسار التشغيل + نعم + لا + معدل العينة + %1$d هرتز + %1$d إطارًا لكل مخزن مؤقت + Hi-Fi PCM Float + مسار إخراج عائم 32 بت + دعم زمن الوصول المنخفض + دعم الصوت الاحترافي + الذاكرة + متاح من %1$s + تنسيقات جاهزة للتفريغ + لم يتم الإبلاغ عن أي تنسيق مضغوط يدعم تفريغ الأجهزة. + +%1$d المزيد + المخرجات المكتشفة + المخرجات المدمجة + صوت البلوتوث + صوت USB + صوت سلكي + إخراج رقمي + إخراج آخر + لم يتم الإبلاغ عن أي مسارات إخراج بواسطة Android. + محرك ExoPlayer + %1$s عارضًا + توافق التنسيق + %1$d مسارًا مدعومًا + %1$d تنسيق غير معروف + لم يتم الإبلاغ عن أي وحدة فك ترميز + وحدة فك ترميز الأجهزة + وحدة فك ترميز البرامج + تفريغ + %1$d في المكتبة + نتائج التوافق + لا توجد تعارضات كبيرة + تتطابق مساراتك المفهرسة مع وحدات فك الترميز التي أبلغ عنها Android على هذا الجهاز. + %1$d مسارًا قد لا يتم فك ترميزها أصليًا + التنسيقات التي يجب مراجعتها: %1$s. + قد تتم إعادة تشكيل %1$d مسارًا محليًا + تصل المكتبة إلى %1$d هرتز، أعلى من معدل عينة الإخراج الحالي. + %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 + \ No newline at end of file diff --git a/app/src/main/res/values-ar/strings_widget.xml b/app/src/main/res/values-ar/strings_widget.xml new file mode 100644 index 000000000..ef6e7152c --- /dev/null +++ b/app/src/main/res/values-ar/strings_widget.xml @@ -0,0 +1,17 @@ + + + ودجت مستجيب يتكيف تلقائياً مع حجمه + شريط مشغل مدمج + عناصر تحكم كاملة مع خياري الخلط والتكرار + مشغل مربع بسيط وموجز + + انقر للفتح + غلاف الألبوم + مواضع غلاف الألبوم المؤقتة + + انقر للتشغيل + عنوان الأغنية + الفنان + + شريط التقدم، %1$d بالمئة + \ No newline at end of file From e899976bf7fa9fe09ed3625f12e7376f61c87d43 Mon Sep 17 00:00:00 2001 From: Hisham-Alzamzami Date: Wed, 17 Jun 2026 05:13:26 +0300 Subject: [PATCH 60/99] Add Arabic language support to AppLanguage --- .../com/theveloper/pixelplay/data/preferences/AppLanguage.kt | 3 ++- 1 file changed, 2 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..fb51de77b 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,7 +16,8 @@ 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), - TURKISH("tr", R.string.settings_language_turkish); + TURKISH("tr", R.string.settings_language_turkish), + ARABIC("ar", R.string.settings_language_arabic); companion object { val supportedLanguageTags: Set = values().map { it.tag }.toSet() From f534598ae4e3ba8a52eb3eab51b7f2bb2fbeb9c5 Mon Sep 17 00:00:00 2001 From: Hisham-Alzamzami Date: Wed, 17 Jun 2026 06:45:33 +0300 Subject: [PATCH 61/99] Add Arabic language support to settings --- app/src/main/res/values/strings_settings.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/res/values/strings_settings.xml b/app/src/main/res/values/strings_settings.xml index 298046dec..1b327476d 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 + العربية App Theme Switch between light, dark, or follow system appearance. Light Theme From 97f6f34d8f37add43d27f39679708ec130c5af86 Mon Sep 17 00:00:00 2001 From: Ayaan Date: Wed, 17 Jun 2026 14:33:31 +0530 Subject: [PATCH 62/99] 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 63/99] 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 64/99] 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 65/99] 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 66/99] 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 67/99] 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 68/99] 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 69/99] 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 70/99] 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 71/99] 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 72/99] =?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 73/99] 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 74/99] 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 75/99] 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 4bec3e7ea9d8a0e0e76a2d9912bc5b8981d84378 Mon Sep 17 00:00:00 2001 From: ZL114514 <114651468+ZL114514@users.noreply.github.com> Date: Wed, 17 Jun 2026 20:45:45 +0800 Subject: [PATCH 76/99] feat: home screen box column --- .../presentation/screens/HomeScreen.kt | 413 +++++++++++------- 1 file changed, 267 insertions(+), 146 deletions(-) diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/screens/HomeScreen.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/screens/HomeScreen.kt index ba622628f..76610dff8 100644 --- a/app/src/main/java/com/theveloper/pixelplay/presentation/screens/HomeScreen.kt +++ b/app/src/main/java/com/theveloper/pixelplay/presentation/screens/HomeScreen.kt @@ -9,6 +9,7 @@ import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row @@ -49,6 +50,7 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.derivedStateOf import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.compose.runtime.getValue +import androidx.compose.runtime.key import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -111,6 +113,7 @@ import com.theveloper.pixelplay.presentation.viewmodel.PlayerViewModel import com.theveloper.pixelplay.presentation.viewmodel.SettingsViewModel import com.theveloper.pixelplay.presentation.viewmodel.StatsViewModel import com.theveloper.pixelplay.ui.theme.ExpTitleTypography +import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.delay @@ -121,6 +124,12 @@ import racra.compose.smooth_corner_rect_library.AbsoluteSmoothCornerShape import androidx.compose.ui.res.stringResource private const val HomeLoadingPlaceholderMinDurationMillis = 1200L +private val HomeTabletBreakpoint = 600.dp + +private data class HomeTabletModule( + val key: String, + val content: @Composable () -> Unit +) // Modern HomeScreen with collapsible top bar and staggered grid layout @androidx.annotation.OptIn(UnstableApi::class) @@ -305,6 +314,111 @@ fun HomeScreen( val shouldShowCleanInstallDisclaimer = settingsUiState.beta05CleanInstallDisclaimerDismissed == false && !cleanInstallDisclaimerDismissedThisSession + val yourMixModule: @Composable () -> Unit = { + HomeYourMixModule( + yourMixSongs = yourMixSongs, + song = yourMixSong, + isShuffleEnabled = isShuffleEnabled, + shouldShowYourMixLoadingPlaceholder = shouldShowYourMixLoadingPlaceholder, + onRefresh = { + homePlaceholderRefreshGeneration++ + settingsViewModel.refreshLibrary() + playerViewModel.forceUpdateDailyMix() + }, + onPlayShuffled = { + if (usesFallbackHomeMix) { + playerViewModel.shuffleAllSongs(queueName = "Your Mix") + } else { + playerViewModel.playSongsShuffled( + songsToPlay = yourMixSongs, + queueName = "Your Mix", + startAtZero = true, + ) + } + } + ) + } + val albumArtCollageModule: @Composable () -> Unit = { + HomeAlbumArtCollageModule( + songs = yourMixSongs, + basePattern = settingsUiState.collagePattern, + isAutoRotate = settingsUiState.collageAutoRotate, + onSongClick = { song -> + if (usesFallbackHomeMix) { + playerViewModel.showAndPlaySongFromLibrary(song, queueName = "Your Mix") + } else { + playerViewModel.showAndPlaySong(song, yourMixSongs, "Your Mix") + } + } + ) + } + val dailyMixModule: @Composable () -> Unit = { + DailyMixSection( + songs = dailyMixSongs, + onClickOpen = { + navController.navigateSafely(Screen.DailyMixScreen.route) + }, + onNavigateToAlbum = { song -> + navController.navigateSafelyReplacing( + route = Screen.AlbumDetail.createRoute(song.albumId), + patternToPop = Screen.AlbumDetail.route + ) + }, + onNavigateToArtist = { song -> + navController.navigateSafelyReplacing( + route = Screen.ArtistDetail.createRoute(song.artistId), + patternToPop = Screen.ArtistDetail.route + ) + }, + onNavigateToGenre = { song -> + song.genre?.let { + navController.navigateSafely(Screen.GenreDetail.createRoute(java.net.URLEncoder.encode(it, "UTF-8"))) + } + }, + playerViewModel = playerViewModel + ) + } + val recentlyPlayedModule: @Composable () -> Unit = { + RecentlyPlayedSection( + songs = recentlyPlayedSongs, + onSongClick = { song -> + if (recentlyPlayedQueue.isNotEmpty()) { + playerViewModel.playSongs( + songsToPlay = recentlyPlayedQueue, + startSong = song, + queueName = "Recently Played" + ) + } + }, + onOpenAllClick = { + navController.navigateSafely(Screen.RecentlyPlayed.route) + }, + themeStateHolder = playerViewModel.themeStateHolder, + currentSongId = currentSong?.id, + contentPadding = PaddingValues(start = 8.dp, end = 24.dp) + ) + } + val statsModule: @Composable () -> Unit = { + StatsOverviewCard( + summary = homeStatsOverview, + onClick = { navController.navigateSafely(Screen.Stats.route) } + ) + } + val tabletModules = buildList { + add(HomeTabletModule(key = "tablet_your_mix", content = yourMixModule)) + if (yourMixSongs.isNotEmpty()) { + add(HomeTabletModule(key = "tablet_album_art_collage", content = albumArtCollageModule)) + } + if (dailyMixSongs.isNotEmpty()) { + add(HomeTabletModule(key = "tablet_daily_mix", content = dailyMixModule)) + } + if (recentlyPlayedSongs.size >= RecentlyPlayedSectionMinSongsToShow) { + add(HomeTabletModule(key = "tablet_recently_played", content = recentlyPlayedModule)) + } + if (homeStatsOverview != null) { + add(HomeTabletModule(key = "tablet_listening_stats", content = statsModule)) + } + } Box( modifier = Modifier.fillMaxSize() @@ -332,163 +446,76 @@ fun HomeScreen( ) } ) { innerPadding -> - LazyColumn( - state = listState, + BoxWithConstraints( modifier = Modifier .fillMaxSize() - .background(MaterialTheme.colorScheme.background), - contentPadding = PaddingValues( - top = innerPadding.calculateTopPadding(), - bottom = paddingValuesParent.calculateBottomPadding() - + 38.dp + bottomPadding - ), - verticalArrangement = Arrangement.spacedBy(24.dp) + .background(MaterialTheme.colorScheme.background) ) { - if (yourMixSongs.isEmpty()) { - item( - key = "your_mix_placeholder", - contentType = "your_mix_placeholder" - ) { - if (shouldShowYourMixLoadingPlaceholder) { - YourMixLoadingPlaceholder() - } else { - YourMixEmptyPlaceholder( - onRefresh = { - homePlaceholderRefreshGeneration++ - settingsViewModel.refreshLibrary() - playerViewModel.forceUpdateDailyMix() - } - ) + val isTabletLayout = maxWidth >= HomeTabletBreakpoint + + LazyColumn( + state = listState, + modifier = Modifier.fillMaxSize(), + contentPadding = PaddingValues( + top = innerPadding.calculateTopPadding(), + bottom = paddingValuesParent.calculateBottomPadding() + + 38.dp + bottomPadding + ), + verticalArrangement = Arrangement.spacedBy(if (isTabletLayout) 20.dp else 24.dp) + ) { + if (isTabletLayout) { + item( + key = tabletModules.joinToString( + separator = "_", + prefix = "tablet_columns_" + ) { it.key }, + contentType = "tablet_module_columns" + ) { + HomeTabletModuleColumns(modules = tabletModules) + } + } else { + item( + key = if (yourMixSongs.isEmpty()) "your_mix_placeholder" else "your_mix_header", + contentType = if (yourMixSongs.isEmpty()) "your_mix_placeholder" else "your_mix_header" + ) { + yourMixModule() } - } - } else { - item( - key = "your_mix_header", - contentType = "your_mix_header" - ) { - YourMixHeader( - song = yourMixSong, - isShuffleEnabled = isShuffleEnabled, - onPlayShuffled = { - if (usesFallbackHomeMix) { - playerViewModel.shuffleAllSongs(queueName = "Your Mix") - } else { - playerViewModel.playSongsShuffled( - songsToPlay = yourMixSongs, - queueName = "Your Mix", - startAtZero = true, - ) - } - } - ) - } - } - // Collage - if (yourMixSongs.isNotEmpty()) { - item( - key = "album_art_collage", - contentType = "album_art_collage" - ) { - val basePattern = settingsUiState.collagePattern - val isAutoRotate = settingsUiState.collageAutoRotate - val patterns = remember { CollagePattern.entries } - - val activePattern = if (isAutoRotate) { - var rotationIndex by rememberSaveable { mutableIntStateOf(-1) } - LaunchedEffect(Unit) { rotationIndex++ } - remember(rotationIndex) { - patterns[rotationIndex.coerceAtLeast(0) % patterns.size] + if (yourMixSongs.isNotEmpty()) { + item( + key = "album_art_collage", + contentType = "album_art_collage" + ) { + albumArtCollageModule() } - } else { - basePattern } - AlbumArtCollage( - modifier = Modifier.fillMaxWidth(), - songs = yourMixSongs, - padding = 14.dp, - height = 400.dp, - pattern = activePattern, - onSongClick = { song -> - if (usesFallbackHomeMix) { - playerViewModel.showAndPlaySongFromLibrary(song, queueName = "Your Mix") - } else { - playerViewModel.showAndPlaySong(song, yourMixSongs, "Your Mix") - } + if (dailyMixSongs.isNotEmpty()) { + item( + key = "daily_mix_section", + contentType = "daily_mix_section" + ) { + dailyMixModule() } - ) - } - } - - // Daily Mix - if (dailyMixSongs.isNotEmpty()) { - item( - key = "daily_mix_section", - contentType = "daily_mix_section" - ) { - DailyMixSection( - songs = dailyMixSongs, - onClickOpen = { - navController.navigateSafely(Screen.DailyMixScreen.route) - }, - onNavigateToAlbum = { song -> - navController.navigateSafelyReplacing( - route = Screen.AlbumDetail.createRoute(song.albumId), - patternToPop = Screen.AlbumDetail.route - ) - }, - onNavigateToArtist = { song -> - navController.navigateSafelyReplacing( - route = Screen.ArtistDetail.createRoute(song.artistId), - patternToPop = Screen.ArtistDetail.route - ) - }, - onNavigateToGenre = { song -> - song.genre?.let { - navController.navigateSafely(Screen.GenreDetail.createRoute(java.net.URLEncoder.encode(it, "UTF-8"))) - } - }, - playerViewModel = playerViewModel - ) - } - } + } - if (recentlyPlayedSongs.size >= RecentlyPlayedSectionMinSongsToShow) { - item( - key = "recently_played_section", - contentType = "recently_played_section" - ) { - RecentlyPlayedSection( - songs = recentlyPlayedSongs, - onSongClick = { song -> - if (recentlyPlayedQueue.isNotEmpty()) { - playerViewModel.playSongs( - songsToPlay = recentlyPlayedQueue, - startSong = song, - queueName = "Recently Played" - ) - } - }, - onOpenAllClick = { - navController.navigateSafely(Screen.RecentlyPlayed.route) - }, - themeStateHolder = playerViewModel.themeStateHolder, - currentSongId = currentSong?.id, - contentPadding = PaddingValues(start = 8.dp, end = 24.dp) - ) - } - } + if (recentlyPlayedSongs.size >= RecentlyPlayedSectionMinSongsToShow) { + item( + key = "recently_played_section", + contentType = "recently_played_section" + ) { + recentlyPlayedModule() + } + } - if (homeStatsOverview != null) { - item( - key = "listening_stats_preview", - contentType = "listening_stats_preview" - ) { - StatsOverviewCard( - summary = homeStatsOverview, - onClick = { navController.navigateSafely(Screen.Stats.route) } - ) + if (homeStatsOverview != null) { + item( + key = "listening_stats_preview", + contentType = "listening_stats_preview" + ) { + statsModule() + } + } } } } @@ -585,6 +612,100 @@ fun HomeScreen( } } +@Composable +private fun HomeTabletModuleColumns( + modules: List +) { + val leftColumnModules = modules.filterIndexed { index, _ -> index % 2 == 0 } + val rightColumnModules = modules.filterIndexed { index, _ -> index % 2 == 1 } + + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 8.dp), + horizontalArrangement = Arrangement.spacedBy(16.dp), + verticalAlignment = Alignment.Top + ) { + HomeTabletModuleColumn( + modules = leftColumnModules, + modifier = Modifier.weight(1f) + ) + HomeTabletModuleColumn( + modules = rightColumnModules, + modifier = Modifier.weight(1f) + ) + } +} + +@Composable +private fun HomeTabletModuleColumn( + modules: List, + modifier: Modifier = Modifier +) { + Column( + modifier = modifier, + verticalArrangement = Arrangement.spacedBy(20.dp) + ) { + modules.forEach { module -> + key(module.key) { + module.content() + } + } + } +} + +@Composable +private fun HomeYourMixModule( + yourMixSongs: ImmutableList, + song: String, + isShuffleEnabled: Boolean, + shouldShowYourMixLoadingPlaceholder: Boolean, + onRefresh: () -> Unit, + onPlayShuffled: () -> Unit +) { + if (yourMixSongs.isEmpty()) { + if (shouldShowYourMixLoadingPlaceholder) { + YourMixLoadingPlaceholder() + } else { + YourMixEmptyPlaceholder(onRefresh = onRefresh) + } + } else { + YourMixHeader( + song = song, + isShuffleEnabled = isShuffleEnabled, + onPlayShuffled = onPlayShuffled + ) + } +} + +@Composable +private fun HomeAlbumArtCollageModule( + songs: ImmutableList, + basePattern: CollagePattern, + isAutoRotate: Boolean, + onSongClick: (Song) -> Unit +) { + val patterns = remember { CollagePattern.entries } + val activePattern = if (isAutoRotate) { + var rotationIndex by rememberSaveable { mutableIntStateOf(-1) } + LaunchedEffect(Unit) { rotationIndex++ } + remember(rotationIndex) { + patterns[rotationIndex.coerceAtLeast(0) % patterns.size] + } + } else { + basePattern + } + + AlbumArtCollage( + modifier = Modifier.fillMaxWidth(), + songs = songs, + padding = 14.dp, + height = 400.dp, + pattern = activePattern, + onSongClick = onSongClick + ) +} + @OptIn(ExperimentalMaterial3ExpressiveApi::class) @Composable private fun YourMixLoadingPlaceholder() { From 0d671d4e82aded610d57801a0dda6fcdd98f5c44 Mon Sep 17 00:00:00 2001 From: Shinichi Fujimoto Date: Thu, 18 Jun 2026 14:45:47 +0900 Subject: [PATCH 77/99] 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 78/99] 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 79/99] 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 b93d644855eacc289068417748484000763d1eaf Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 18 Jun 2026 06:59:03 +0000 Subject: [PATCH 80/99] chore(deps): bump com.google.genai:google-genai Bumps the gradle-dependencies group with 1 update in the / directory: [com.google.genai:google-genai](https://github.com/googleapis/java-genai). Updates `com.google.genai:google-genai` from 1.58.0 to 1.59.0 - [Release notes](https://github.com/googleapis/java-genai/releases) - [Changelog](https://github.com/googleapis/java-genai/blob/main/CHANGELOG.md) - [Commits](https://github.com/googleapis/java-genai/compare/v1.58.0...v1.59.0) --- updated-dependencies: - dependency-name: com.google.genai:google-genai dependency-version: 1.59.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: gradle-dependencies ... Signed-off-by: dependabot[bot] --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index e653436ec..c7433f277 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -2,7 +2,7 @@ accompanistDrawablepainter = "0.37.3" agp = "9.2.1" app = "1.7.0" -googleGenai = "1.58.0" +googleGenai = "1.59.0" googlePlayServicesCast = "22.3.1" animation = "1.11.3" appcompat = "1.7.1" From fd0a3cbcf736f19ae57c57ec2906f77cb47cfdbd Mon Sep 17 00:00:00 2001 From: amreldeeb Date: Thu, 18 Jun 2026 21:28:48 +0300 Subject: [PATCH 81/99] 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 bb898f3efeff7ac8e01aef50769b36ab769154a2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 18 Jun 2026 22:23:48 +0000 Subject: [PATCH 82/99] chore(deps): bump actions/checkout in the github-actions group Bumps the github-actions group with 1 update: [actions/checkout](https://github.com/actions/checkout). Updates `actions/checkout` from 6 to 7 - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v6...v7) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: '7' dependency-type: direct:production update-type: version-update:semver-major dependency-group: github-actions ... Signed-off-by: dependabot[bot] --- .github/workflows/codeql.yml | 2 +- .github/workflows/nightly-apk.yml | 2 +- .github/workflows/phone-debug.yml | 2 +- .github/workflows/phone-release.yml | 2 +- .github/workflows/wearos-apk.yml | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index b2793cfd8..115355c14 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -21,7 +21,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v6 + uses: actions/checkout@v7 - name: Set up JDK 21 uses: actions/setup-java@v5 diff --git a/.github/workflows/nightly-apk.yml b/.github/workflows/nightly-apk.yml index 2c6f7728f..7d7831616 100644 --- a/.github/workflows/nightly-apk.yml +++ b/.github/workflows/nightly-apk.yml @@ -85,7 +85,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v6 + uses: actions/checkout@v7 - name: Set up JDK 21 uses: actions/setup-java@v5 diff --git a/.github/workflows/phone-debug.yml b/.github/workflows/phone-debug.yml index 3f7d70fb2..ddf07be71 100644 --- a/.github/workflows/phone-debug.yml +++ b/.github/workflows/phone-debug.yml @@ -18,7 +18,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v6 + uses: actions/checkout@v7 - name: Set up JDK 21 uses: actions/setup-java@v5 diff --git a/.github/workflows/phone-release.yml b/.github/workflows/phone-release.yml index 1c33965a3..87b2f6a0a 100644 --- a/.github/workflows/phone-release.yml +++ b/.github/workflows/phone-release.yml @@ -18,7 +18,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v6 + uses: actions/checkout@v7 - name: Set up JDK 21 uses: actions/setup-java@v5 diff --git a/.github/workflows/wearos-apk.yml b/.github/workflows/wearos-apk.yml index 6e7b53b6d..afc488b21 100644 --- a/.github/workflows/wearos-apk.yml +++ b/.github/workflows/wearos-apk.yml @@ -18,7 +18,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v6 + uses: actions/checkout@v7 - name: Set up JDK 21 uses: actions/setup-java@v5 From dcf2291d1a0aa906916695af18c5408582f7bb75 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 18 Jun 2026 22:27:06 +0000 Subject: [PATCH 83/99] chore(deps): bump com.google.genai:google-genai Bumps the gradle-dependencies group with 1 update: [com.google.genai:google-genai](https://github.com/googleapis/java-genai). Updates `com.google.genai:google-genai` from 1.58.0 to 1.59.0 - [Release notes](https://github.com/googleapis/java-genai/releases) - [Changelog](https://github.com/googleapis/java-genai/blob/main/CHANGELOG.md) - [Commits](https://github.com/googleapis/java-genai/compare/v1.58.0...v1.59.0) --- updated-dependencies: - dependency-name: com.google.genai:google-genai dependency-version: 1.59.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: gradle-dependencies ... Signed-off-by: dependabot[bot] --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index e653436ec..c7433f277 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -2,7 +2,7 @@ accompanistDrawablepainter = "0.37.3" agp = "9.2.1" app = "1.7.0" -googleGenai = "1.58.0" +googleGenai = "1.59.0" googlePlayServicesCast = "22.3.1" animation = "1.11.3" appcompat = "1.7.1" From 9a262d91427d50e2a338d9b7ebaa9ef08aa5f5a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?D=D0=B0=D0=B5=20=D0=95u=D0=BD=D1=88=D0=B0?= <134977461+daedaevibin@users.noreply.github.com> Date: Fri, 19 Jun 2026 02:08:10 +0000 Subject: [PATCH 84/99] Security hardening, error handling, AI client refactoring, test coverage Security: - Fix OkHttp response body leaks in AI clients by using .use{} blocks - All HTTP responses now properly closed in OpenAiCompatibleClient base Error handling: - Add Timber logging to silent catch blocks in EqualizerPreferencesRepository, PlaylistViewModel, LyricsStateHolder, FileExplorerStateHolder, DeviceCapabilitiesViewModel, PhoneDirectWatchTransferCoordinator Refactoring: - Extract OpenAiCompatibleClient base class from DeepSeek, Groq, Mistral, GenericOpenAi clients (~600 lines of duplicated code removed) - Extract ServerUrlUtils for shared URL normalization/validation logic used by NavidromeCredentials and JellyfinCredentials Tests: - Add AiClientFactoryTest (all providers, blank key validation) - Add OpenAiCompatibleClientTest (config, model filtering, token counting) - Add CloudMusicUtilsTest (JSON parsing, artist name parsing) - Add ServerUrlUtilsTest (URL normalization, validation, local network checks) Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../data/ai/provider/DeepSeekAiClient.kt | 172 +---------------- .../data/ai/provider/GenericOpenAiClient.kt | 171 ++--------------- .../data/ai/provider/GroqAiClient.kt | 175 ++---------------- .../data/ai/provider/MistralAiClient.kt | 175 +----------------- .../ai/provider/OpenAiCompatibleClient.kt | 168 +++++++++++++++++ .../jellyfin/model/JellyfinCredentials.kt | 35 +--- .../navidrome/model/NavidromeCredentials.kt | 35 ++-- .../EqualizerPreferencesRepository.kt | 13 +- .../PhoneDirectWatchTransferCoordinator.kt | 3 +- .../viewmodel/DeviceCapabilitiesViewModel.kt | 4 +- .../viewmodel/FileExplorerStateHolder.kt | 4 +- .../viewmodel/LyricsStateHolder.kt | 4 +- .../viewmodel/PlaylistViewModel.kt | 5 +- .../pixelplay/utils/ServerUrlUtils.kt | 44 +++++ .../data/ai/provider/AiClientFactoryTest.kt | 83 +++++++++ .../ai/provider/OpenAiCompatibleClientTest.kt | 69 +++++++ .../data/stream/CloudMusicUtilsTest.kt | 85 +++++++++ .../pixelplay/utils/ServerUrlUtilsTest.kt | 104 +++++++++++ 18 files changed, 633 insertions(+), 716 deletions(-) create mode 100644 app/src/main/java/com/theveloper/pixelplay/data/ai/provider/OpenAiCompatibleClient.kt create mode 100644 app/src/main/java/com/theveloper/pixelplay/utils/ServerUrlUtils.kt create mode 100644 app/src/test/java/com/theveloper/pixelplay/data/ai/provider/AiClientFactoryTest.kt create mode 100644 app/src/test/java/com/theveloper/pixelplay/data/ai/provider/OpenAiCompatibleClientTest.kt create mode 100644 app/src/test/java/com/theveloper/pixelplay/data/stream/CloudMusicUtilsTest.kt create mode 100644 app/src/test/java/com/theveloper/pixelplay/utils/ServerUrlUtilsTest.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 index afb84b3ea..f6b830452 100644 --- 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 @@ -1,171 +1,9 @@ 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 DeepSeekAiClient(apiKey: String) : OpenAiCompatibleClient(apiKey) { -/** - * 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" - ) - } + override val providerName = "DeepSeek" + override val baseUrl = "https://api.deepseek.com" + override val defaultModel = "deepseek-chat" + override val defaultModels = listOf("deepseek-chat", "deepseek-reasoner") } 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..5f7503873 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 @@ -1,171 +1,30 @@ 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 /** * A generic AI client for OpenAI-compatible APIs (NVIDIA, Kimi, GLM, etc.) */ class GenericOpenAiClient( - private val apiKey: String, - private val baseUrl: String, + apiKey: String, + override val baseUrl: String, private val defaultModelId: String, - private val providerName: String = "OpenAI" -) : AiClient { - - @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 { defaultModelId } - 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 requestBuilder = Request.Builder() - .url("${baseUrl.trimEnd('/')}/chat/completions") - .addHeader("Authorization", "Bearer $apiKey") - .addHeader("Content-Type", "application/json") - - if (providerName.equals("OpenRouter", ignoreCase = true)) { - requestBuilder.addHeader("HTTP-Referer", "https://github.com/theovilardo/PixelPlayer") - requestBuilder.addHeader("X-Title", "PixelPlayer") - } - - val request = requestBuilder.post(body).build() + override val providerName: String = "OpenAI" +) : OpenAiCompatibleClient(apiKey) { - try { - client.newCall(request).execute().use { response -> - val responseBody = response.body.string() + override val defaultModel: String get() = defaultModelId + override val defaultModels: List get() = listOf(defaultModelId) - if (!response.isSuccessful) { - throw AiProviderSupport.createException( - providerName = providerName, - statusCode = response.code, - transportMessage = response.message, - responseBody = responseBody, - requestedModel = resolvedModel - ) - } - - val chatResponse = json.decodeFromString(responseBody) - chatResponse.choices.firstOrNull()?.message?.content - ?: throw AiProviderSupport.createException( - providerName = providerName, - statusCode = response.code, - transportMessage = "Response had no content", - responseBody = responseBody, - requestedModel = resolvedModel - ) - } - } catch (e: Exception) { - throw AiProviderSupport.wrapThrowable(providerName, e, resolvedModel) - } + override fun decorateRequest(builder: Request.Builder): Request.Builder { + if (providerName.equals("OpenRouter", ignoreCase = true)) { + builder.addHeader("HTTP-Referer", "https://github.com/theovilardo/PixelPlayer") + builder.addHeader("X-Title", "PixelPlayer") } + return builder } - - override suspend fun countTokens(model: String, systemPrompt: String, prompt: String): Int { - // Estimation for generic providers - return (systemPrompt.length + prompt.length) / 4 - } - - override suspend fun getAvailableModels(apiKey: String): List { - return withContext(Dispatchers.IO) { - try { - val request = Request.Builder() - .url("${baseUrl.trimEnd('/')}/models") - .addHeader("Authorization", "Bearer $apiKey") - .get() - .build() - - val response = client.newCall(request).execute() - - if (!response.isSuccessful) { - return@withContext listOf(defaultModelId) - } - - 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") - } - } catch (e: Exception) { - listOf(defaultModelId) - } - } - } - - override suspend fun validateApiKey(apiKey: String): Boolean { - return withContext(Dispatchers.IO) { - try { - // Try a simple models list check as validation - val request = Request.Builder() - .url("${baseUrl.trimEnd('/')}/models") - .addHeader("Authorization", "Bearer $apiKey") - .get() - .build() - - val response = client.newCall(request).execute() - response.isSuccessful - } catch (e: Exception) { - false - } + + override fun filterModels(models: List): List = + models.filter { + !it.contains("whisper") && !it.contains("embed") && !it.contains("tts") } - } - - override fun getDefaultModel(): String = defaultModelId } 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 index 0adf6cf70..bbd5a1f8d 100644 --- 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 @@ -1,170 +1,17 @@ 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(apiKey: String) : OpenAiCompatibleClient(apiKey) { -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 + override val providerName = "Groq" + override val baseUrl = "https://api.groq.com/openai/v1" + override val defaultModel = "llama-3.1-8b-instant" + override val defaultModels = listOf( + "llama-3.1-8b-instant", + "llama-3.3-70b-versatile", + "mixtral-8x7b-32768", + "gemma2-9b-it" ) - - @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" - ) - } + override fun filterModels(models: List): List = + models.filter { !it.contains("whisper") } } 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 index a4d166e2a..666c50540 100644 --- 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 @@ -1,169 +1,14 @@ 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 +class MistralAiClient(apiKey: String) : OpenAiCompatibleClient(apiKey) { + + override val providerName = "Mistral" + override val baseUrl = "https://api.mistral.ai/v1" + override val defaultModel = "mistral-large-latest" + override val defaultModels = listOf( + "mistral-large-latest", + "mistral-small-latest", + "open-mixtral-8x22b", + "open-mixtral-8x7b" ) - - @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" - ) - } } diff --git a/app/src/main/java/com/theveloper/pixelplay/data/ai/provider/OpenAiCompatibleClient.kt b/app/src/main/java/com/theveloper/pixelplay/data/ai/provider/OpenAiCompatibleClient.kt new file mode 100644 index 000000000..ab8c8df95 --- /dev/null +++ b/app/src/main/java/com/theveloper/pixelplay/data/ai/provider/OpenAiCompatibleClient.kt @@ -0,0 +1,168 @@ +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 + +/** + * Base class for AI providers that expose an OpenAI-compatible chat/completions API. + * + * Subclasses only need to supply [providerName], [baseUrl], [defaultModel], + * [defaultModels], and optionally [modelFilter] or [decorateRequest]. + */ +abstract class OpenAiCompatibleClient(private val apiKey: String) : AiClient { + + protected abstract val providerName: String + protected abstract val baseUrl: String + protected abstract val defaultModel: String + protected abstract val defaultModels: List + + @Serializable + protected data class ChatMessage(val role: String, val content: String) + + @Serializable + protected data class ChatRequest( + val model: String, + val messages: List, + val temperature: Double = 0.7 + ) + + @Serializable + protected data class ChatChoice(val message: ChatMessage) + + @Serializable + protected data class ChatResponse(val choices: List) + + @Serializable + protected data class ModelItem(val id: String) + + @Serializable + protected data class ModelsResponse(val data: List) + + protected val client: OkHttpClient = OkHttpClient.Builder() + .connectTimeout(30, TimeUnit.SECONDS) + .readTimeout(60, TimeUnit.SECONDS) + .writeTimeout(30, TimeUnit.SECONDS) + .build() + + protected val json: Json = Json { + ignoreUnknownKeys = true + isLenient = true + } + + protected open fun decorateRequest(builder: Request.Builder): Request.Builder = builder + + protected open fun filterModels(models: List): List = models + + override suspend fun generateContent( + model: String, + systemPrompt: String, + prompt: String, + temperature: Float + ): String { + return withContext(Dispatchers.IO) { + val resolvedModel = model.ifBlank { defaultModel } + 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 builder = Request.Builder() + .url("${baseUrl.trimEnd('/')}/chat/completions") + .addHeader("Authorization", "Bearer $apiKey") + .addHeader("Content-Type", "application/json") + + val request = decorateRequest(builder).post(body).build() + + try { + client.newCall(request).execute().use { response -> + val responseBody = response.body.string() + + if (!response.isSuccessful) { + throw AiProviderSupport.createException( + providerName = providerName, + statusCode = response.code, + transportMessage = response.message, + responseBody = responseBody, + requestedModel = resolvedModel + ) + } + + val chatResponse = json.decodeFromString(responseBody) + chatResponse.choices.firstOrNull()?.message?.content + ?: throw AiProviderSupport.createException( + providerName = providerName, + statusCode = response.code, + transportMessage = "Response had no content", + responseBody = responseBody, + requestedModel = resolvedModel + ) + } + } catch (e: Exception) { + throw AiProviderSupport.wrapThrowable(providerName, e, resolvedModel) + } + } + } + + override suspend fun countTokens(model: String, systemPrompt: String, prompt: String): Int { + return (systemPrompt.length + prompt.length) / 4 + } + + override suspend fun getAvailableModels(apiKey: String): List { + return withContext(Dispatchers.IO) { + try { + val request = Request.Builder() + .url("${baseUrl.trimEnd('/')}/models") + .addHeader("Authorization", "Bearer $apiKey") + .get() + .build() + + client.newCall(request).execute().use { response -> + if (!response.isSuccessful) { + return@withContext defaultModels + } + + val responseBody = response.body.string() + val modelsResponse = json.decodeFromString(responseBody) + filterModels(modelsResponse.data.map { it.id }) + } + } catch (e: Exception) { + defaultModels + } + } + } + + override suspend fun validateApiKey(apiKey: String): Boolean { + return withContext(Dispatchers.IO) { + try { + val request = Request.Builder() + .url("${baseUrl.trimEnd('/')}/models") + .addHeader("Authorization", "Bearer $apiKey") + .get() + .build() + + client.newCall(request).execute().use { it.isSuccessful } + } catch (e: Exception) { + false + } + } + } + + override fun getDefaultModel(): String = defaultModel +} diff --git a/app/src/main/java/com/theveloper/pixelplay/data/jellyfin/model/JellyfinCredentials.kt b/app/src/main/java/com/theveloper/pixelplay/data/jellyfin/model/JellyfinCredentials.kt index 84514af91..bf472a68a 100644 --- a/app/src/main/java/com/theveloper/pixelplay/data/jellyfin/model/JellyfinCredentials.kt +++ b/app/src/main/java/com/theveloper/pixelplay/data/jellyfin/model/JellyfinCredentials.kt @@ -1,8 +1,7 @@ package com.theveloper.pixelplay.data.jellyfin.model -import com.theveloper.pixelplay.data.stream.CloudStreamSecurity +import com.theveloper.pixelplay.utils.ServerUrlUtils import okhttp3.HttpUrl -import okhttp3.HttpUrl.Companion.toHttpUrlOrNull data class JellyfinCredentials( val serverUrl: String, @@ -29,35 +28,11 @@ data class JellyfinCredentials( get() = !accessToken.isNullOrBlank() && !userId.isNullOrBlank() val normalizedHttpUrlOrNull: HttpUrl? - get() { - val trimmed = serverUrl.trim().trimEnd('/') - // Auto-prepend https:// if no scheme is provided - val withScheme = if (!trimmed.startsWith("http://", ignoreCase = true) && - !trimmed.startsWith("https://", ignoreCase = true) - ) { - "https://$trimmed" - } else { - trimmed - } - return withScheme.toHttpUrlOrNull() - } + get() = ServerUrlUtils.normalizeHttpUrl(serverUrl) val normalizedServerUrl: String - get() = normalizedHttpUrlOrNull?.toString()?.trimEnd('/') ?: serverUrl.trim().trimEnd('/') + get() = ServerUrlUtils.normalizeServerUrl(serverUrl) - fun connectionValidationError(): String? { - val parsed = normalizedHttpUrlOrNull - ?: return "Invalid server URL format" - - if (parsed.username.isNotEmpty() || parsed.password.isNotEmpty()) { - return "Server URL must not contain embedded credentials" - } - - // Warn about cleartext HTTP on public hosts - if (!parsed.isHttps && !CloudStreamSecurity.isLocalOrPrivateHost(parsed.host)) { - return "Use https:// for remote Jellyfin servers. HTTP is only allowed for local network addresses." - } - - return null - } + fun connectionValidationError(): String? = + ServerUrlUtils.connectionValidationError(serverUrl, "Jellyfin") } diff --git a/app/src/main/java/com/theveloper/pixelplay/data/navidrome/model/NavidromeCredentials.kt b/app/src/main/java/com/theveloper/pixelplay/data/navidrome/model/NavidromeCredentials.kt index 2742fd5bd..8a4db443a 100644 --- a/app/src/main/java/com/theveloper/pixelplay/data/navidrome/model/NavidromeCredentials.kt +++ b/app/src/main/java/com/theveloper/pixelplay/data/navidrome/model/NavidromeCredentials.kt @@ -1,8 +1,7 @@ package com.theveloper.pixelplay.data.navidrome.model +import com.theveloper.pixelplay.utils.ServerUrlUtils import okhttp3.HttpUrl -import okhttp3.HttpUrl.Companion.toHttpUrlOrNull -import com.theveloper.pixelplay.data.stream.CloudStreamSecurity /** * Represents authentication credentials for a Navidrome/Subsonic server. @@ -49,38 +48,26 @@ data class NavidromeCredentials( * Returns the parsed and normalized server URL, or null if it is invalid. */ val normalizedHttpUrlOrNull: HttpUrl? - get() { - val trimmed = serverUrl.trim().trimEnd('/') - // Auto-prepend https:// if no scheme is provided - val withScheme = if (!trimmed.startsWith("http://", ignoreCase = true) && - !trimmed.startsWith("https://", ignoreCase = true) - ) { - "https://$trimmed" - } else { - trimmed - } - return withScheme.toHttpUrlOrNull() - } + get() = ServerUrlUtils.normalizeHttpUrl(serverUrl) /** * Returns the normalized server URL (without trailing slash). */ val normalizedServerUrl: String - get() = normalizedHttpUrlOrNull?.toString()?.trimEnd('/') ?: serverUrl.trim().trimEnd('/') + get() = ServerUrlUtils.normalizeServerUrl(serverUrl) /** * Returns a validation error for connection setup, or null when the URL is acceptable. */ fun connectionValidationError(requireHttps: Boolean = true): String? { - val httpUrl = normalizedHttpUrlOrNull ?: return "Enter a valid server URL." - if (httpUrl.username.isNotEmpty() || httpUrl.password.isNotEmpty()) { - return "Server URL must not include embedded credentials." - } - if (requireHttps && !httpUrl.isHttps && - !CloudStreamSecurity.isLocalOrPrivateHost(httpUrl.host) - ) { - return "Use an https:// server URL for remote Navidrome/Subsonic servers. HTTP is only allowed for local network addresses." + if (!requireHttps) { + val httpUrl = normalizedHttpUrlOrNull ?: return "Enter a valid server URL." + if (httpUrl.username.isNotEmpty() || httpUrl.password.isNotEmpty()) { + return "Server URL must not include embedded credentials." + } + return null } - return null + return ServerUrlUtils.connectionValidationError(serverUrl, "Navidrome/Subsonic") + ?.let { if (it == "Invalid server URL format") "Enter a valid server URL." else it } } } diff --git a/app/src/main/java/com/theveloper/pixelplay/data/preferences/EqualizerPreferencesRepository.kt b/app/src/main/java/com/theveloper/pixelplay/data/preferences/EqualizerPreferencesRepository.kt index 9fbe6a162..f4f1a89b1 100644 --- a/app/src/main/java/com/theveloper/pixelplay/data/preferences/EqualizerPreferencesRepository.kt +++ b/app/src/main/java/com/theveloper/pixelplay/data/preferences/EqualizerPreferencesRepository.kt @@ -13,6 +13,7 @@ import kotlinx.coroutines.flow.map import kotlinx.serialization.decodeFromString import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json +import timber.log.Timber import javax.inject.Inject import javax.inject.Singleton @@ -44,7 +45,8 @@ class EqualizerPreferencesRepository @Inject constructor( if (modeString != null) { try { EqualizerViewMode.valueOf(modeString) - } catch (_: Exception) { + } catch (e: Exception) { + Timber.w(e, "Failed to parse equalizer view mode") EqualizerViewMode.SLIDERS } } else { @@ -71,7 +73,8 @@ class EqualizerPreferencesRepository @Inject constructor( decoded.isEmpty() -> List(10) { 0 } else -> decoded + List(10 - decoded.size) { 0 } } - } catch (_: Exception) { + } catch (e: Exception) { + Timber.w(e, "Failed to parse equalizer custom bands") List(10) { 0 } } } else { @@ -120,7 +123,8 @@ class EqualizerPreferencesRepository @Inject constructor( if (jsonString != null) { try { json.decodeFromString>(jsonString) - } catch (_: Exception) { + } catch (e: Exception) { + Timber.w(e, "Failed to parse custom presets") emptyList() } } else { @@ -133,7 +137,8 @@ class EqualizerPreferencesRepository @Inject constructor( if (jsonString != null) { try { json.decodeFromString>(jsonString) - } catch (_: Exception) { + } catch (e: Exception) { + Timber.w(e, "Failed to parse pinned presets") EqualizerPreset.ALL_PRESETS.map { it.name } } } else { diff --git a/app/src/main/java/com/theveloper/pixelplay/data/service/wear/PhoneDirectWatchTransferCoordinator.kt b/app/src/main/java/com/theveloper/pixelplay/data/service/wear/PhoneDirectWatchTransferCoordinator.kt index 7f935a080..35182504f 100644 --- a/app/src/main/java/com/theveloper/pixelplay/data/service/wear/PhoneDirectWatchTransferCoordinator.kt +++ b/app/src/main/java/com/theveloper/pixelplay/data/service/wear/PhoneDirectWatchTransferCoordinator.kt @@ -309,7 +309,8 @@ class PhoneDirectWatchTransferCoordinator @Inject constructor( contentResolver.openAssetFileDescriptor(uri, "r")?.use { afd -> afd.length != 0L } ?: false - } catch (_: Exception) { + } catch (e: Exception) { + Timber.w(e, "Failed to verify content URI accessibility") false } } diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/DeviceCapabilitiesViewModel.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/DeviceCapabilitiesViewModel.kt index 69cb6573e..1ba0cff07 100644 --- a/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/DeviceCapabilitiesViewModel.kt +++ b/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/DeviceCapabilitiesViewModel.kt @@ -3,6 +3,7 @@ package com.theveloper.pixelplay.presentation.viewmodel import android.app.ActivityManager import android.content.Context import android.content.pm.PackageManager +import timber.log.Timber import android.media.AudioDeviceInfo import android.media.AudioFormat import android.media.AudioManager @@ -313,7 +314,8 @@ class DeviceCapabilitiesViewModel @Inject constructor( val instances = try { codecInfo.getCapabilitiesForType(codecInfo.supportedTypes.first { it.startsWith("audio/") }) .maxSupportedInstances - } catch (_: Exception) { + } catch (e: Exception) { + Timber.w(e, "Failed to get codec capabilities for %s", codecInfo.name) -1 } diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/FileExplorerStateHolder.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/FileExplorerStateHolder.kt index b43ff8724..e55ab940e 100644 --- a/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/FileExplorerStateHolder.kt +++ b/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/FileExplorerStateHolder.kt @@ -3,6 +3,7 @@ package com.theveloper.pixelplay.presentation.viewmodel import android.content.Context import android.os.Environment import android.provider.MediaStore +import timber.log.Timber import com.theveloper.pixelplay.data.preferences.UserPreferencesRepository import com.theveloper.pixelplay.utils.DirectoryRuleResolver import com.theveloper.pixelplay.utils.StorageInfo @@ -576,7 +577,8 @@ class FileExplorerStateHolder( } catch (error: CancellationException) { prefetchedDirectoryKeys.remove(targetKey) throw error - } catch (_: Throwable) { + } catch (e: Throwable) { + Timber.w(e, "Failed to prefetch directory %s", targetKey) prefetchedDirectoryKeys.remove(targetKey) } } diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/LyricsStateHolder.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/LyricsStateHolder.kt index 87985fd6e..5a49929e0 100644 --- a/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/LyricsStateHolder.kt +++ b/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/LyricsStateHolder.kt @@ -13,6 +13,7 @@ import com.theveloper.pixelplay.data.repository.MusicRepository import com.theveloper.pixelplay.data.repository.NoLyricsFoundException import com.theveloper.pixelplay.utils.LyricsImportSecurity import com.theveloper.pixelplay.utils.LyricsImportValidationResult +import timber.log.Timber import com.theveloper.pixelplay.utils.LyricsUtils import com.theveloper.pixelplay.utils.ValidatedLyricsImport import java.io.File @@ -135,7 +136,8 @@ class LyricsStateHolder @Inject constructor( } } catch (cancellation: CancellationException) { throw cancellation - } catch (_: Exception) { + } catch (e: Exception) { + Timber.w(e, "Failed to load lyrics for song %s", song.title) null } diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/PlaylistViewModel.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/PlaylistViewModel.kt index 8c5c620ef..489891ae5 100644 --- a/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/PlaylistViewModel.kt +++ b/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/PlaylistViewModel.kt @@ -28,6 +28,7 @@ import kotlinx.coroutines.withContext import kotlinx.coroutines.Dispatchers import java.io.OutputStreamWriter import android.content.Context +import timber.log.Timber import android.graphics.Bitmap import android.graphics.ImageDecoder import android.os.Build @@ -615,7 +616,7 @@ class PlaylistViewModel @Inject constructor( // Optional: Delete old file if it was a local file managed by us currentPlaylist.coverImageUri?.let { oldPath -> if (oldPath.contains("playlist_cover_")) { - try { File(oldPath).delete() } catch (e: Exception) {} + try { File(oldPath).delete() } catch (e: Exception) { Timber.w(e, "Failed to delete old playlist cover") } } } savedCoverPath = newPath @@ -624,7 +625,7 @@ class PlaylistViewModel @Inject constructor( // Explicitly removed currentPlaylist.coverImageUri?.let { oldPath -> if (oldPath.contains("playlist_cover_")) { - try { File(oldPath).delete() } catch (e: Exception) {} + try { File(oldPath).delete() } catch (e: Exception) { Timber.w(e, "Failed to delete old playlist cover") } } } savedCoverPath = null diff --git a/app/src/main/java/com/theveloper/pixelplay/utils/ServerUrlUtils.kt b/app/src/main/java/com/theveloper/pixelplay/utils/ServerUrlUtils.kt new file mode 100644 index 000000000..5d7ea8f68 --- /dev/null +++ b/app/src/main/java/com/theveloper/pixelplay/utils/ServerUrlUtils.kt @@ -0,0 +1,44 @@ +package com.theveloper.pixelplay.utils + +import com.theveloper.pixelplay.data.stream.CloudStreamSecurity +import okhttp3.HttpUrl +import okhttp3.HttpUrl.Companion.toHttpUrlOrNull + +/** + * Shared URL normalization and validation for self-hosted media server credentials + * (Navidrome/Subsonic, Jellyfin, etc.). + */ +object ServerUrlUtils { + + fun normalizeHttpUrl(serverUrl: String): HttpUrl? { + val trimmed = serverUrl.trim().trimEnd('/') + val withScheme = if (!trimmed.startsWith("http://", ignoreCase = true) && + !trimmed.startsWith("https://", ignoreCase = true) + ) { + "https://$trimmed" + } else { + trimmed + } + return withScheme.toHttpUrlOrNull() + } + + fun normalizeServerUrl(serverUrl: String): String { + return normalizeHttpUrl(serverUrl)?.toString()?.trimEnd('/') + ?: serverUrl.trim().trimEnd('/') + } + + fun connectionValidationError(serverUrl: String, serverLabel: String = "server"): String? { + val parsed = normalizeHttpUrl(serverUrl) + ?: return "Invalid server URL format" + + if (parsed.username.isNotEmpty() || parsed.password.isNotEmpty()) { + return "Server URL must not contain embedded credentials." + } + + if (!parsed.isHttps && !CloudStreamSecurity.isLocalOrPrivateHost(parsed.host)) { + return "Use https:// for remote $serverLabel servers. HTTP is only allowed for local network addresses." + } + + return null + } +} diff --git a/app/src/test/java/com/theveloper/pixelplay/data/ai/provider/AiClientFactoryTest.kt b/app/src/test/java/com/theveloper/pixelplay/data/ai/provider/AiClientFactoryTest.kt new file mode 100644 index 000000000..0745d5727 --- /dev/null +++ b/app/src/test/java/com/theveloper/pixelplay/data/ai/provider/AiClientFactoryTest.kt @@ -0,0 +1,83 @@ +package com.theveloper.pixelplay.data.ai.provider + +import com.google.common.truth.Truth.assertThat +import org.junit.Test + +class AiClientFactoryTest { + + private val factory = AiClientFactory() + + @Test + fun `createClient returns GeminiAiClient for GEMINI`() { + val client = factory.createClient(AiProvider.GEMINI, "test-key") + assertThat(client).isInstanceOf(GeminiAiClient::class.java) + } + + @Test + fun `createClient returns DeepSeekAiClient for DEEPSEEK`() { + val client = factory.createClient(AiProvider.DEEPSEEK, "test-key") + assertThat(client).isInstanceOf(DeepSeekAiClient::class.java) + } + + @Test + fun `createClient returns GroqAiClient for GROQ`() { + val client = factory.createClient(AiProvider.GROQ, "test-key") + assertThat(client).isInstanceOf(GroqAiClient::class.java) + } + + @Test + fun `createClient returns MistralAiClient for MISTRAL`() { + val client = factory.createClient(AiProvider.MISTRAL, "test-key") + assertThat(client).isInstanceOf(MistralAiClient::class.java) + } + + @Test + fun `createClient returns GenericOpenAiClient for NVIDIA`() { + val client = factory.createClient(AiProvider.NVIDIA, "test-key") + assertThat(client).isInstanceOf(GenericOpenAiClient::class.java) + } + + @Test + fun `createClient returns GenericOpenAiClient for OPENAI`() { + val client = factory.createClient(AiProvider.OPENAI, "test-key") + assertThat(client).isInstanceOf(GenericOpenAiClient::class.java) + } + + @Test + fun `createClient returns GenericOpenAiClient for OPENROUTER`() { + val client = factory.createClient(AiProvider.OPENROUTER, "test-key") + assertThat(client).isInstanceOf(GenericOpenAiClient::class.java) + } + + @Test(expected = IllegalArgumentException::class) + fun `createClient throws for blank API key`() { + factory.createClient(AiProvider.GEMINI, "") + } + + @Test(expected = IllegalArgumentException::class) + fun `createClient throws for whitespace-only API key`() { + factory.createClient(AiProvider.DEEPSEEK, " ") + } + + @Test + fun `all providers return correct default models`() { + for (provider in AiProvider.entries) { + val client = factory.createClient(provider, "test-key") + val defaultModel = client.getDefaultModel() + assertThat(defaultModel).isNotEmpty() + } + } + + @Test + fun `OpenAI-compatible clients inherit from OpenAiCompatibleClient`() { + val deepseek = factory.createClient(AiProvider.DEEPSEEK, "k") + val groq = factory.createClient(AiProvider.GROQ, "k") + val mistral = factory.createClient(AiProvider.MISTRAL, "k") + val generic = factory.createClient(AiProvider.OPENAI, "k") + + assertThat(deepseek).isInstanceOf(OpenAiCompatibleClient::class.java) + assertThat(groq).isInstanceOf(OpenAiCompatibleClient::class.java) + assertThat(mistral).isInstanceOf(OpenAiCompatibleClient::class.java) + assertThat(generic).isInstanceOf(OpenAiCompatibleClient::class.java) + } +} diff --git a/app/src/test/java/com/theveloper/pixelplay/data/ai/provider/OpenAiCompatibleClientTest.kt b/app/src/test/java/com/theveloper/pixelplay/data/ai/provider/OpenAiCompatibleClientTest.kt new file mode 100644 index 000000000..9f8e90eee --- /dev/null +++ b/app/src/test/java/com/theveloper/pixelplay/data/ai/provider/OpenAiCompatibleClientTest.kt @@ -0,0 +1,69 @@ +package com.theveloper.pixelplay.data.ai.provider + +import com.google.common.truth.Truth.assertThat +import org.junit.Test + +class OpenAiCompatibleClientTest { + + @Test + fun `DeepSeek has correct provider config`() { + val client = DeepSeekAiClient("key") + assertThat(client.getDefaultModel()).isEqualTo("deepseek-chat") + } + + @Test + fun `Groq has correct provider config`() { + val client = GroqAiClient("key") + assertThat(client.getDefaultModel()).isEqualTo("llama-3.1-8b-instant") + } + + @Test + fun `Groq filters out whisper models`() { + val client = GroqAiClient("key") + val filtered = client.filterModels( + listOf("llama-3.1-8b-instant", "whisper-large-v3", "mixtral-8x7b-32768") + ) + assertThat(filtered).containsExactly("llama-3.1-8b-instant", "mixtral-8x7b-32768") + } + + @Test + fun `Mistral has correct provider config`() { + val client = MistralAiClient("key") + assertThat(client.getDefaultModel()).isEqualTo("mistral-large-latest") + } + + @Test + fun `GenericOpenAiClient uses provided default model`() { + val client = GenericOpenAiClient( + apiKey = "key", + baseUrl = "https://api.example.com/v1", + defaultModelId = "custom-model", + providerName = "TestProvider" + ) + assertThat(client.getDefaultModel()).isEqualTo("custom-model") + } + + @Test + fun `GenericOpenAiClient filters embed and tts models`() { + val client = GenericOpenAiClient( + apiKey = "key", + baseUrl = "https://api.example.com/v1", + defaultModelId = "gpt-4o", + providerName = "TestProvider" + ) + val filtered = client.filterModels( + listOf("gpt-4o", "text-embedding-3-small", "tts-1", "whisper-1", "gpt-4o-mini") + ) + assertThat(filtered).containsExactly("gpt-4o", "gpt-4o-mini") + } + + @Test + fun `countTokens returns approximate token count`() { + val client = DeepSeekAiClient("key") + // 8 chars system + 12 chars prompt = 20 chars / 4 = 5 tokens + val tokens = kotlinx.coroutines.runBlocking { + client.countTokens("model", "12345678", "123456789012") + } + assertThat(tokens).isEqualTo(5) + } +} diff --git a/app/src/test/java/com/theveloper/pixelplay/data/stream/CloudMusicUtilsTest.kt b/app/src/test/java/com/theveloper/pixelplay/data/stream/CloudMusicUtilsTest.kt new file mode 100644 index 000000000..5883d54a1 --- /dev/null +++ b/app/src/test/java/com/theveloper/pixelplay/data/stream/CloudMusicUtilsTest.kt @@ -0,0 +1,85 @@ +package com.theveloper.pixelplay.data.stream + +import com.google.common.truth.Truth.assertThat +import org.junit.Test + +class CloudMusicUtilsTest { + + @Test + fun `jsonToMap parses valid JSON object`() { + val result = CloudMusicUtils.jsonToMap("""{"key1":"val1","key2":"val2"}""") + assertThat(result).containsExactly("key1", "val1", "key2", "val2") + } + + @Test + fun `jsonToMap handles empty object`() { + val result = CloudMusicUtils.jsonToMap("{}") + assertThat(result).isEmpty() + } + + @Test + fun `jsonToMap returns empty string for null values`() { + val result = CloudMusicUtils.jsonToMap("""{"key":null}""") + assertThat(result).containsEntry("key", "") + } + + @Test + fun `parseArtistNames splits on comma`() { + val result = CloudMusicUtils.parseArtistNames("Artist A, Artist B") + assertThat(result).containsExactly("Artist A", "Artist B") + } + + @Test + fun `parseArtistNames splits on ampersand`() { + val result = CloudMusicUtils.parseArtistNames("A & B") + assertThat(result).containsExactly("A", "B") + } + + @Test + fun `parseArtistNames splits on slash`() { + val result = CloudMusicUtils.parseArtistNames("A/B/C") + assertThat(result).containsExactly("A", "B", "C") + } + + @Test + fun `parseArtistNames splits on semicolon`() { + val result = CloudMusicUtils.parseArtistNames("A;B") + assertThat(result).containsExactly("A", "B") + } + + @Test + fun `parseArtistNames splits on CJK comma`() { + val result = CloudMusicUtils.parseArtistNames("A、B") + assertThat(result).containsExactly("A", "B") + } + + @Test + fun `parseArtistNames deduplicates names`() { + val result = CloudMusicUtils.parseArtistNames("A, A, B") + assertThat(result).containsExactly("A", "B") + } + + @Test + fun `parseArtistNames returns Unknown Artist for blank input`() { + assertThat(CloudMusicUtils.parseArtistNames("")).containsExactly("Unknown Artist") + assertThat(CloudMusicUtils.parseArtistNames(" ")).containsExactly("Unknown Artist") + } + + @Test + fun `parseArtistNames returns single artist for simple name`() { + val result = CloudMusicUtils.parseArtistNames("Taylor Swift") + assertThat(result).containsExactly("Taylor Swift") + } + + @Test + fun `parseArtistNames handles mixed delimiters`() { + val result = CloudMusicUtils.parseArtistNames("A, B & C/D") + assertThat(result).containsExactly("A", "B", "C", "D") + } + + @Test + fun `parseArtistNames trims whitespace around names`() { + val result = CloudMusicUtils.parseArtistNames(" A , B ") + assertThat(result).containsExactly("A", "B") + } +} diff --git a/app/src/test/java/com/theveloper/pixelplay/utils/ServerUrlUtilsTest.kt b/app/src/test/java/com/theveloper/pixelplay/utils/ServerUrlUtilsTest.kt new file mode 100644 index 000000000..62150c72c --- /dev/null +++ b/app/src/test/java/com/theveloper/pixelplay/utils/ServerUrlUtilsTest.kt @@ -0,0 +1,104 @@ +package com.theveloper.pixelplay.utils + +import com.google.common.truth.Truth.assertThat +import org.junit.Test + +class ServerUrlUtilsTest { + + @Test + fun `normalizeHttpUrl adds https scheme when absent`() { + val result = ServerUrlUtils.normalizeHttpUrl("music.example.com") + assertThat(result).isNotNull() + assertThat(result!!.scheme).isEqualTo("https") + assertThat(result.host).isEqualTo("music.example.com") + } + + @Test + fun `normalizeHttpUrl preserves explicit http scheme`() { + val result = ServerUrlUtils.normalizeHttpUrl("http://192.168.1.100:8096") + assertThat(result).isNotNull() + assertThat(result!!.scheme).isEqualTo("http") + assertThat(result.host).isEqualTo("192.168.1.100") + assertThat(result.port).isEqualTo(8096) + } + + @Test + fun `normalizeHttpUrl preserves https scheme`() { + val result = ServerUrlUtils.normalizeHttpUrl("https://music.example.com") + assertThat(result).isNotNull() + assertThat(result!!.scheme).isEqualTo("https") + } + + @Test + fun `normalizeHttpUrl trims whitespace and trailing slashes`() { + val result = ServerUrlUtils.normalizeHttpUrl(" https://music.example.com/ ") + assertThat(result).isNotNull() + assertThat(result!!.host).isEqualTo("music.example.com") + } + + @Test + fun `normalizeHttpUrl returns null for empty string`() { + assertThat(ServerUrlUtils.normalizeHttpUrl("")).isNull() + } + + @Test + fun `normalizeServerUrl returns clean URL without trailing slash`() { + val result = ServerUrlUtils.normalizeServerUrl("https://music.example.com/") + assertThat(result).doesNotEndWith("/") + assertThat(result).startsWith("https://") + } + + @Test + fun `normalizeServerUrl falls back to trimmed input for invalid URLs`() { + val result = ServerUrlUtils.normalizeServerUrl("") + assertThat(result).isEmpty() + } + + @Test + fun `connectionValidationError returns null for valid https URL`() { + val error = ServerUrlUtils.connectionValidationError("https://music.example.com") + assertThat(error).isNull() + } + + @Test + fun `connectionValidationError rejects embedded credentials`() { + val error = ServerUrlUtils.connectionValidationError("https://user:pass@music.example.com") + assertThat(error).contains("credentials") + } + + @Test + fun `connectionValidationError rejects http on public hosts`() { + val error = ServerUrlUtils.connectionValidationError("http://music.example.com") + assertThat(error).contains("https") + } + + @Test + fun `connectionValidationError allows http on local network`() { + val error = ServerUrlUtils.connectionValidationError("http://192.168.1.100:8096") + assertThat(error).isNull() + } + + @Test + fun `connectionValidationError allows http on localhost`() { + val error = ServerUrlUtils.connectionValidationError("http://localhost:8096") + assertThat(error).isNull() + } + + @Test + fun `connectionValidationError allows http on 127-0-0-1`() { + val error = ServerUrlUtils.connectionValidationError("http://127.0.0.1:4533") + assertThat(error).isNull() + } + + @Test + fun `connectionValidationError uses serverLabel in message`() { + val error = ServerUrlUtils.connectionValidationError("http://public.example.com", "Jellyfin") + assertThat(error).contains("Jellyfin") + } + + @Test + fun `connectionValidationError rejects invalid URL`() { + val error = ServerUrlUtils.connectionValidationError("not a url at all ://") + assertThat(error).contains("Invalid") + } +} From 478d795ba351957296362b61518166aea5e16919 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?D=D0=B0=D0=B5=20=D0=95u=D0=BD=D1=88=D0=B0?= <134977461+daedaevibin@users.noreply.github.com> Date: Fri, 19 Jun 2026 02:13:58 +0000 Subject: [PATCH 85/99] Fix JVM signature clash: rename defaultModel to providerDefaultModel The abstract val 'defaultModel' generated a getDefaultModel() JVM getter that clashed with the AiClient interface's getDefaultModel() function. Renamed to 'providerDefaultModel' and 'providerDefaultModels' to avoid the accidental override. Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../pixelplay/data/ai/provider/DeepSeekAiClient.kt | 4 ++-- .../data/ai/provider/GenericOpenAiClient.kt | 4 ++-- .../pixelplay/data/ai/provider/GroqAiClient.kt | 4 ++-- .../pixelplay/data/ai/provider/MistralAiClient.kt | 4 ++-- .../data/ai/provider/OpenAiCompatibleClient.kt | 12 ++++++------ 5 files changed, 14 insertions(+), 14 deletions(-) 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 index f6b830452..4ad6a1962 100644 --- 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 @@ -4,6 +4,6 @@ class DeepSeekAiClient(apiKey: String) : OpenAiCompatibleClient(apiKey) { override val providerName = "DeepSeek" override val baseUrl = "https://api.deepseek.com" - override val defaultModel = "deepseek-chat" - override val defaultModels = listOf("deepseek-chat", "deepseek-reasoner") + override val providerDefaultModel = "deepseek-chat" + override val providerDefaultModels = listOf("deepseek-chat", "deepseek-reasoner") } 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 5f7503873..bd676cc05 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 @@ -12,8 +12,8 @@ class GenericOpenAiClient( override val providerName: String = "OpenAI" ) : OpenAiCompatibleClient(apiKey) { - override val defaultModel: String get() = defaultModelId - override val defaultModels: List get() = listOf(defaultModelId) + override val providerDefaultModel: String get() = defaultModelId + override val providerDefaultModels: List get() = listOf(defaultModelId) override fun decorateRequest(builder: Request.Builder): Request.Builder { if (providerName.equals("OpenRouter", ignoreCase = true)) { 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 index bbd5a1f8d..ee4f55b38 100644 --- 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 @@ -4,8 +4,8 @@ class GroqAiClient(apiKey: String) : OpenAiCompatibleClient(apiKey) { override val providerName = "Groq" override val baseUrl = "https://api.groq.com/openai/v1" - override val defaultModel = "llama-3.1-8b-instant" - override val defaultModels = listOf( + override val providerDefaultModel = "llama-3.1-8b-instant" + override val providerDefaultModels = listOf( "llama-3.1-8b-instant", "llama-3.3-70b-versatile", "mixtral-8x7b-32768", 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 index 666c50540..6fda0230a 100644 --- 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 @@ -4,8 +4,8 @@ class MistralAiClient(apiKey: String) : OpenAiCompatibleClient(apiKey) { override val providerName = "Mistral" override val baseUrl = "https://api.mistral.ai/v1" - override val defaultModel = "mistral-large-latest" - override val defaultModels = listOf( + override val providerDefaultModel = "mistral-large-latest" + override val providerDefaultModels = listOf( "mistral-large-latest", "mistral-small-latest", "open-mixtral-8x22b", diff --git a/app/src/main/java/com/theveloper/pixelplay/data/ai/provider/OpenAiCompatibleClient.kt b/app/src/main/java/com/theveloper/pixelplay/data/ai/provider/OpenAiCompatibleClient.kt index ab8c8df95..881f453d0 100644 --- a/app/src/main/java/com/theveloper/pixelplay/data/ai/provider/OpenAiCompatibleClient.kt +++ b/app/src/main/java/com/theveloper/pixelplay/data/ai/provider/OpenAiCompatibleClient.kt @@ -20,8 +20,8 @@ abstract class OpenAiCompatibleClient(private val apiKey: String) : AiClient { protected abstract val providerName: String protected abstract val baseUrl: String - protected abstract val defaultModel: String - protected abstract val defaultModels: List + protected abstract val providerDefaultModel: String + protected abstract val providerDefaultModels: List @Serializable protected data class ChatMessage(val role: String, val content: String) @@ -67,7 +67,7 @@ abstract class OpenAiCompatibleClient(private val apiKey: String) : AiClient { temperature: Float ): String { return withContext(Dispatchers.IO) { - val resolvedModel = model.ifBlank { defaultModel } + val resolvedModel = model.ifBlank { providerDefaultModel } val messagesList = mutableListOf() if (systemPrompt.isNotBlank()) { messagesList.add(ChatMessage(role = "system", content = systemPrompt)) @@ -135,7 +135,7 @@ abstract class OpenAiCompatibleClient(private val apiKey: String) : AiClient { client.newCall(request).execute().use { response -> if (!response.isSuccessful) { - return@withContext defaultModels + return@withContext providerDefaultModels } val responseBody = response.body.string() @@ -143,7 +143,7 @@ abstract class OpenAiCompatibleClient(private val apiKey: String) : AiClient { filterModels(modelsResponse.data.map { it.id }) } } catch (e: Exception) { - defaultModels + providerDefaultModels } } } @@ -164,5 +164,5 @@ abstract class OpenAiCompatibleClient(private val apiKey: String) : AiClient { } } - override fun getDefaultModel(): String = defaultModel + override fun getDefaultModel(): String = providerDefaultModel } From 801d9ae1a6441f19dfa10b75f186a1322953a20e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?D=D0=B0=D0=B5=20=D0=95u=D0=BD=D1=88=D0=B0?= <134977461+daedaevibin@users.noreply.github.com> Date: Fri, 19 Jun 2026 02:30:58 +0000 Subject: [PATCH 86/99] Additional quality, security, and performance improvements MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Security: - Fix OkHttp response body leaks in GDriveApiService (4 methods) - Fix JSON injection in GDriveApiService.createFolder — use JSONObject builder instead of string interpolation - Harden GDriveApiService.getAuthHeader to require a token instead of silently sending 'Bearer ' with empty token Performance: - Cache 6 Regex patterns in LyricsRepositoryImpl companion object that were recompiled on every call to normalizeForMatch/timingVariantTokens/ looksLikeFlattenedWordByWordCache Thread safety: - Add @Volatile to GDriveStreamProxy's server, actualPort, startJob fields accessed from multiple threads Error handling: - Add Timber.w logging to 12 more silent catch blocks across backup (BackupManager, BackupWriter, BackupHistoryRepository, LegacyPayloadAdapter), database (PixelPlayDatabase migrations, SongEntity), network (NeteaseApiService, QQSignGenerator, QqMusicRepository), presentation (ThemeStateHolder, SongRemovalStateHolder), and utils (MediaMetadataRetrieverPool) Code quality: - Remove dead try/catch in ChangelogBottomSheet.openUrl that caught and redid the exact same operation - Make filterModels internal for testability Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../ai/provider/OpenAiCompatibleClient.kt | 2 +- .../pixelplay/data/backup/BackupManager.kt | 7 +- .../data/backup/format/BackupWriter.kt | 4 +- .../backup/format/LegacyPayloadAdapter.kt | 3 +- .../backup/history/BackupHistoryRepository.kt | 7 +- .../data/database/PixelPlayDatabase.kt | 10 +-- .../pixelplay/data/database/SongEntity.kt | 4 +- .../pixelplay/data/gdrive/GDriveApiService.kt | 68 +++++++++++-------- .../data/gdrive/GDriveStreamProxy.kt | 6 +- .../data/network/netease/NeteaseApiService.kt | 3 +- .../data/qqmusic/QqMusicRepository.kt | 3 +- .../data/remote/qqmusic/QQSignGenerator.kt | 3 +- .../data/repository/LyricsRepositoryImpl.kt | 18 +++-- .../components/ChangelogBottomSheet.kt | 2 +- .../viewmodel/SongRemovalStateHolder.kt | 4 +- .../viewmodel/ThemeStateHolder.kt | 5 +- .../utils/MediaMetadataRetrieverPool.kt | 5 +- 17 files changed, 95 insertions(+), 59 deletions(-) diff --git a/app/src/main/java/com/theveloper/pixelplay/data/ai/provider/OpenAiCompatibleClient.kt b/app/src/main/java/com/theveloper/pixelplay/data/ai/provider/OpenAiCompatibleClient.kt index 881f453d0..daa5a6e83 100644 --- a/app/src/main/java/com/theveloper/pixelplay/data/ai/provider/OpenAiCompatibleClient.kt +++ b/app/src/main/java/com/theveloper/pixelplay/data/ai/provider/OpenAiCompatibleClient.kt @@ -58,7 +58,7 @@ abstract class OpenAiCompatibleClient(private val apiKey: String) : AiClient { protected open fun decorateRequest(builder: Request.Builder): Request.Builder = builder - protected open fun filterModels(models: List): List = models + internal open fun filterModels(models: List): List = models override suspend fun generateContent( model: String, diff --git a/app/src/main/java/com/theveloper/pixelplay/data/backup/BackupManager.kt b/app/src/main/java/com/theveloper/pixelplay/data/backup/BackupManager.kt index 6472ab11a..27afc146b 100644 --- a/app/src/main/java/com/theveloper/pixelplay/data/backup/BackupManager.kt +++ b/app/src/main/java/com/theveloper/pixelplay/data/backup/BackupManager.kt @@ -3,6 +3,7 @@ package com.theveloper.pixelplay.data.backup import android.content.Context import android.net.Uri import android.os.Build +import timber.log.Timber import com.theveloper.pixelplay.data.backup.format.BackupReader import com.theveloper.pixelplay.data.backup.format.BackupWriter import com.theveloper.pixelplay.data.backup.history.BackupHistoryRepository @@ -66,7 +67,7 @@ class BackupManager @Inject constructor( // Build manifest val packageInfo = try { context.packageManager.getPackageInfo(context.packageName, 0) - } catch (_: Exception) { null } + } catch (e: Exception) { Timber.w(e, "Failed to get package info"); null } val manifest = BackupManifest( schemaVersion = BackupManifest.CURRENT_SCHEMA_VERSION, @@ -183,8 +184,8 @@ class BackupManager @Inject constructor( appVersion = plan.manifest.appVersion ) ) - } catch (_: Exception) { - // Non-critical; don't fail restore because of history persistence + } catch (e: Exception) { + Timber.w(e, "Failed to persist restore history entry") } } diff --git a/app/src/main/java/com/theveloper/pixelplay/data/backup/format/BackupWriter.kt b/app/src/main/java/com/theveloper/pixelplay/data/backup/format/BackupWriter.kt index 92b01734f..c9d5a36f1 100644 --- a/app/src/main/java/com/theveloper/pixelplay/data/backup/format/BackupWriter.kt +++ b/app/src/main/java/com/theveloper/pixelplay/data/backup/format/BackupWriter.kt @@ -5,6 +5,7 @@ import android.net.Uri import com.google.gson.Gson import com.theveloper.pixelplay.data.backup.model.BackupManifest import com.theveloper.pixelplay.data.backup.model.BackupModuleInfo +import timber.log.Timber import com.theveloper.pixelplay.di.BackupGson import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.Dispatchers @@ -85,7 +86,8 @@ class BackupWriter @Inject constructor( } else { 1 } - } catch (_: Exception) { + } catch (e: Exception) { + Timber.w(e, "Failed to count items in backup module JSON") 0 } } diff --git a/app/src/main/java/com/theveloper/pixelplay/data/backup/format/LegacyPayloadAdapter.kt b/app/src/main/java/com/theveloper/pixelplay/data/backup/format/LegacyPayloadAdapter.kt index 486462f7c..96fd3771c 100644 --- a/app/src/main/java/com/theveloper/pixelplay/data/backup/format/LegacyPayloadAdapter.kt +++ b/app/src/main/java/com/theveloper/pixelplay/data/backup/format/LegacyPayloadAdapter.kt @@ -4,6 +4,7 @@ import com.google.gson.Gson import com.google.gson.JsonArray import com.google.gson.JsonObject import com.google.gson.JsonParser +import timber.log.Timber import com.theveloper.pixelplay.data.backup.model.BackupManifest import com.theveloper.pixelplay.data.backup.model.BackupModuleInfo import com.theveloper.pixelplay.data.backup.model.DeviceInfo @@ -120,7 +121,7 @@ class LegacyPayloadAdapter @Inject constructor() { } else { 1 } - } catch (_: Exception) { 0 } + } catch (e: Exception) { Timber.w(e, "Failed to count legacy backup entries"); 0 } } private fun sha256(data: ByteArray): String { diff --git a/app/src/main/java/com/theveloper/pixelplay/data/backup/history/BackupHistoryRepository.kt b/app/src/main/java/com/theveloper/pixelplay/data/backup/history/BackupHistoryRepository.kt index 2d1b03374..4deec0c1d 100644 --- a/app/src/main/java/com/theveloper/pixelplay/data/backup/history/BackupHistoryRepository.kt +++ b/app/src/main/java/com/theveloper/pixelplay/data/backup/history/BackupHistoryRepository.kt @@ -8,6 +8,7 @@ import com.google.gson.Gson import com.google.gson.reflect.TypeToken import com.theveloper.pixelplay.data.backup.model.BackupHistoryEntry import com.theveloper.pixelplay.di.BackupGson +import timber.log.Timber import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map import javax.inject.Inject @@ -30,7 +31,8 @@ class BackupHistoryRepository @Inject constructor( if (json != null) { try { gson.fromJson>(json, listType) - } catch (_: Exception) { + } catch (e: Exception) { + Timber.w(e, "Failed to parse backup history") emptyList() } } else { @@ -68,7 +70,8 @@ class BackupHistoryRepository @Inject constructor( val json = preferences[BACKUP_HISTORY_KEY] ?: return emptyList() return try { gson.fromJson(json, listType) - } catch (_: Exception) { + } catch (e: Exception) { + Timber.w(e, "Failed to read backup history") emptyList() } } diff --git a/app/src/main/java/com/theveloper/pixelplay/data/database/PixelPlayDatabase.kt b/app/src/main/java/com/theveloper/pixelplay/data/database/PixelPlayDatabase.kt index 094929dd9..947f17208 100644 --- a/app/src/main/java/com/theveloper/pixelplay/data/database/PixelPlayDatabase.kt +++ b/app/src/main/java/com/theveloper/pixelplay/data/database/PixelPlayDatabase.kt @@ -3,6 +3,7 @@ package com.theveloper.pixelplay.data.database import androidx.room.Database import androidx.room.RoomDatabase import androidx.room.migration.Migration +import timber.log.Timber import androidx.sqlite.db.SupportSQLiteDatabase @Database( @@ -759,9 +760,8 @@ abstract class PixelPlayDatabase : RoomDatabase() { try { db.execSQL("ALTER TABLE songs ADD COLUMN date_added INTEGER NOT NULL DEFAULT 0") - } catch (_: Exception) { - // Some restored databases report the right version but still carry - // a drifted songs table. If ALTER TABLE did not stick, rebuild it. + } catch (e: Exception) { + Timber.w(e, "ALTER TABLE songs ADD date_added failed; will recreate table") } if ("date_added" !in getTableColumns(db, "songs")) { @@ -1133,8 +1133,8 @@ abstract class PixelPlayDatabase : RoomDatabase() { if ("disc_number" !in columns) { try { db.execSQL("ALTER TABLE songs ADD COLUMN disc_number INTEGER DEFAULT null") - } catch (_: Exception) { - // Restored/drifted databases may already contain a partially applied column. + } catch (e: Exception) { + Timber.w(e, "ALTER TABLE songs ADD disc_number failed; may already exist") } } diff --git a/app/src/main/java/com/theveloper/pixelplay/data/database/SongEntity.kt b/app/src/main/java/com/theveloper/pixelplay/data/database/SongEntity.kt index 19f948b79..d0df39b26 100644 --- a/app/src/main/java/com/theveloper/pixelplay/data/database/SongEntity.kt +++ b/app/src/main/java/com/theveloper/pixelplay/data/database/SongEntity.kt @@ -3,6 +3,7 @@ package com.theveloper.pixelplay.data.database import androidx.room.ColumnInfo import androidx.room.Entity import androidx.room.ForeignKey +import timber.log.Timber import androidx.room.Index import androidx.room.PrimaryKey import com.theveloper.pixelplay.data.model.ArtistRef @@ -171,7 +172,8 @@ private fun parseArtistsJson(json: String?): List { isPrimary = obj.optBoolean("primary", false) ) } - } catch (_: Exception) { + } catch (e: Exception) { + Timber.w(e, "Failed to parse artist refs JSON") emptyList() } } diff --git a/app/src/main/java/com/theveloper/pixelplay/data/gdrive/GDriveApiService.kt b/app/src/main/java/com/theveloper/pixelplay/data/gdrive/GDriveApiService.kt index e6ffdf81c..dd64dad4a 100644 --- a/app/src/main/java/com/theveloper/pixelplay/data/gdrive/GDriveApiService.kt +++ b/app/src/main/java/com/theveloper/pixelplay/data/gdrive/GDriveApiService.kt @@ -6,6 +6,8 @@ import okhttp3.MediaType.Companion.toMediaType import okhttp3.OkHttpClient import okhttp3.Request import okhttp3.RequestBody.Companion.toRequestBody +import org.json.JSONArray +import org.json.JSONObject import timber.log.Timber import javax.inject.Inject import javax.inject.Singleton @@ -31,7 +33,11 @@ class GDriveApiService @Inject constructor( fun hasToken(): Boolean = !accessToken.isNullOrBlank() - fun getAuthHeader(): String = "Bearer ${accessToken ?: ""}" + fun getAuthHeader(): String { + val token = accessToken + require(!token.isNullOrBlank()) { "GDrive access token not set" } + return "Bearer $token" + } fun getStreamUrl(fileId: String): String { return "${GDriveConstants.DRIVE_API_BASE}/files/$fileId?alt=media" @@ -94,8 +100,12 @@ class GDriveApiService @Inject constructor( */ suspend fun createFolder(name: String, parentId: String = "root"): String { return withContext(Dispatchers.IO) { - val json = """{"name":"$name","mimeType":"application/vnd.google-apps.folder","parents":["$parentId"]}""" - val body = json.toRequestBody("application/json".toMediaType()) + val jsonBody = JSONObject().apply { + put("name", name) + put("mimeType", "application/vnd.google-apps.folder") + put("parents", JSONArray().put(parentId)) + }.toString() + val body = jsonBody.toRequestBody("application/json".toMediaType()) val request = Request.Builder() .url("${GDriveConstants.DRIVE_API_BASE}/files") @@ -103,14 +113,15 @@ class GDriveApiService @Inject constructor( .post(body) .build() - val response = okHttpClient.newCall(request).execute() - val responseBody = response.body.string() - Timber.d("GDriveApi createFolder: code=${response.code}, body=${responseBody.take(200)}") + okHttpClient.newCall(request).execute().use { response -> + val responseBody = response.body.string() + Timber.d("GDriveApi createFolder: code=${response.code}, body=${responseBody.take(200)}") - if (!response.isSuccessful) { - throw Exception("Drive API error ${response.code}: $responseBody") + if (!response.isSuccessful) { + throw Exception("Drive API error ${response.code}: $responseBody") + } + responseBody } - responseBody } } @@ -136,14 +147,15 @@ class GDriveApiService @Inject constructor( .post(formBody) .build() - val response = okHttpClient.newCall(request).execute() - val responseBody = response.body.string() - Timber.d("GDriveApi exchangeAuthCode: code=${response.code}") + okHttpClient.newCall(request).execute().use { response -> + val responseBody = response.body.string() + Timber.d("GDriveApi exchangeAuthCode: code=${response.code}") - if (!response.isSuccessful) { - throw Exception("Token exchange failed ${response.code}: $responseBody") + if (!response.isSuccessful) { + throw Exception("Token exchange failed ${response.code}: $responseBody") + } + responseBody } - responseBody } } @@ -168,14 +180,15 @@ class GDriveApiService @Inject constructor( .post(formBody) .build() - val response = okHttpClient.newCall(request).execute() - val responseBody = response.body.string() - Timber.d("GDriveApi refreshToken: code=${response.code}") + okHttpClient.newCall(request).execute().use { response -> + val responseBody = response.body.string() + Timber.d("GDriveApi refreshToken: code=${response.code}") - if (!response.isSuccessful) { - throw Exception("Token refresh failed ${response.code}: $responseBody") + if (!response.isSuccessful) { + throw Exception("Token refresh failed ${response.code}: $responseBody") + } + responseBody } - responseBody } } @@ -194,14 +207,15 @@ class GDriveApiService @Inject constructor( .get() .build() - val response = okHttpClient.newCall(request).execute() - val responseBody = response.body.string() - Timber.d("GDriveApi GET ${url.take(80)}: code=${response.code}") + okHttpClient.newCall(request).execute().use { response -> + val responseBody = response.body.string() + Timber.d("GDriveApi GET ${url.take(80)}: code=${response.code}") - if (!response.isSuccessful) { - throw Exception("Drive API error ${response.code}: $responseBody") + if (!response.isSuccessful) { + throw Exception("Drive API error ${response.code}: $responseBody") + } + responseBody } - responseBody } } } diff --git a/app/src/main/java/com/theveloper/pixelplay/data/gdrive/GDriveStreamProxy.kt b/app/src/main/java/com/theveloper/pixelplay/data/gdrive/GDriveStreamProxy.kt index 7014ddd09..9ad245b58 100644 --- a/app/src/main/java/com/theveloper/pixelplay/data/gdrive/GDriveStreamProxy.kt +++ b/app/src/main/java/com/theveloper/pixelplay/data/gdrive/GDriveStreamProxy.kt @@ -51,10 +51,10 @@ class GDriveStreamProxy @Inject constructor( ) } - private var server: EmbeddedServer? = null - private var actualPort: Int = 0 + @Volatile private var server: EmbeddedServer? = null + @Volatile private var actualPort: Int = 0 private val proxyScope = CoroutineScope(SupervisorJob() + Dispatchers.IO) - private var startJob: Job? = null + @Volatile private var startJob: Job? = null fun isReady(): Boolean = actualPort > 0 diff --git a/app/src/main/java/com/theveloper/pixelplay/data/network/netease/NeteaseApiService.kt b/app/src/main/java/com/theveloper/pixelplay/data/network/netease/NeteaseApiService.kt index 5c5b4da22..26370d178 100644 --- a/app/src/main/java/com/theveloper/pixelplay/data/network/netease/NeteaseApiService.kt +++ b/app/src/main/java/com/theveloper/pixelplay/data/network/netease/NeteaseApiService.kt @@ -313,7 +313,8 @@ class NeteaseApiService @Inject constructor() { resp = call() } resp - } catch (_: Exception) { + } catch (e: Exception) { + Timber.w(e, "$TAG: retry after session warm-up failed") resp } } diff --git a/app/src/main/java/com/theveloper/pixelplay/data/qqmusic/QqMusicRepository.kt b/app/src/main/java/com/theveloper/pixelplay/data/qqmusic/QqMusicRepository.kt index 32487f396..697489281 100644 --- a/app/src/main/java/com/theveloper/pixelplay/data/qqmusic/QqMusicRepository.kt +++ b/app/src/main/java/com/theveloper/pixelplay/data/qqmusic/QqMusicRepository.kt @@ -613,7 +613,8 @@ class QqMusicRepository @Inject constructor( val result = String(decoded, Charsets.UTF_8) // Verify the decoded result contains actual readable text if (result.isNotBlank() && !result.contains('\u0000')) result else input - } catch (_: Exception) { + } catch (e: Exception) { + Timber.w(e, "Failed to decode base64 artist name") input } } diff --git a/app/src/main/java/com/theveloper/pixelplay/data/remote/qqmusic/QQSignGenerator.kt b/app/src/main/java/com/theveloper/pixelplay/data/remote/qqmusic/QQSignGenerator.kt index 3a1995d00..7563c0619 100644 --- a/app/src/main/java/com/theveloper/pixelplay/data/remote/qqmusic/QQSignGenerator.kt +++ b/app/src/main/java/com/theveloper/pixelplay/data/remote/qqmusic/QQSignGenerator.kt @@ -118,7 +118,8 @@ class QQSignGenerator(private val context: Context) { if (raw == null || raw == "null" || raw.isBlank()) return null return try { if (raw.startsWith('"')) JSONArray("[$raw]").getString(0) else raw - } catch (_: Exception) { + } catch (e: Exception) { + Timber.w(e, "Failed to decode evaluate result") raw } } diff --git a/app/src/main/java/com/theveloper/pixelplay/data/repository/LyricsRepositoryImpl.kt b/app/src/main/java/com/theveloper/pixelplay/data/repository/LyricsRepositoryImpl.kt index 05e357d05..b1941fdef 100644 --- a/app/src/main/java/com/theveloper/pixelplay/data/repository/LyricsRepositoryImpl.kt +++ b/app/src/main/java/com/theveloper/pixelplay/data/repository/LyricsRepositoryImpl.kt @@ -136,6 +136,12 @@ class LyricsRepositoryImpl @Inject constructor( private val BRACKETED_QUALIFIER_REGEX = Regex("""[\(\[\{\uFF08\uFF3B\uFF5B\u3010\u300E\u300C\u3014\u3008\u300A]([^)\]\}\uFF09\uFF3D\uFF5D\u3011\u300F\u300D\u3015\u3009\u300B]*)[\)\]\}\uFF09\uFF3D\uFF5D\u3011\u300F\u300D\u3015\u3009\u300B]""") private val FEATURE_QUALIFIER_REGEX = Regex("""\b(feat(?:uring)?|ft)\.?\b""", RegexOption.IGNORE_CASE) private val TITLE_SEPARATOR_REGEX = Regex("""\s*[-\u2013\u2014:\uFF0D\u00B7\u30FB]\s*""") + private val MASH_UP_REGEX = Regex("""\bmash\s+up\b""") + private val DIACRITICS_REGEX = Regex("""\p{Mn}+""") + private val APOSTROPHE_REGEX = Regex("""[\u2019'`]""") + private val NON_ALNUM_REGEX = Regex("""[^\p{L}\p{N}]+""") + private val WHITESPACE_COLLAPSE_REGEX = Regex("""\s+""") + private val LONG_LATIN_RUN_REGEX = Regex("""[A-Za-z]{10,}""") private val TIMING_VARIANT_KEYWORDS = setOf( "remix", "mix", @@ -799,7 +805,7 @@ class LyricsRepositoryImpl @Inject constructor( .filter { it in TIMING_VARIANT_KEYWORDS } .toMutableSet() - if (Regex("""\bmash\s+up\b""").containsMatchIn(normalized)) { + if (MASH_UP_REGEX.containsMatchIn(normalized)) { variants += "mashup" } if ("versus" in tokens || "vs" in tokens) { @@ -854,14 +860,14 @@ class LyricsRepositoryImpl @Inject constructor( private fun normalizeForMatch(value: String): String { val withoutDiacritics = Normalizer.normalize(value.lowercase(Locale.ROOT), Normalizer.Form.NFD) - .replace(Regex("""\p{Mn}+"""), "") + .replace(DIACRITICS_REGEX, "") return withoutDiacritics .replace("&", " and ") - .replace(Regex("""[\u2019'`]"""), "") - .replace(Regex("""[^\p{L}\p{N}]+"""), " ") + .replace(APOSTROPHE_REGEX, "") + .replace(NON_ALNUM_REGEX, " ") .trim() - .replace(Regex("""\s+"""), " ") + .replace(WHITESPACE_COLLAPSE_REGEX, " ") } private fun isUnknownArtist(value: String): Boolean = @@ -1142,7 +1148,7 @@ class LyricsRepositoryImpl @Inject constructor( val text = line.line if (text.isBlank() || text.any { it.isWhitespace() }) continue - val hasLongLatinRun = Regex("[A-Za-z]{10,}").containsMatchIn(text) + val hasLongLatinRun = LONG_LATIN_RUN_REGEX.containsMatchIn(text) if (hasLongLatinRun) { suspiciousLines += 1 if (suspiciousLines >= 2) return true diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/components/ChangelogBottomSheet.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/components/ChangelogBottomSheet.kt index 5d8a3860d..0818f90fc 100644 --- a/app/src/main/java/com/theveloper/pixelplay/presentation/components/ChangelogBottomSheet.kt +++ b/app/src/main/java/com/theveloper/pixelplay/presentation/components/ChangelogBottomSheet.kt @@ -346,7 +346,7 @@ fun VersionBadge( } private fun openUrl(context: Context, url: String) { - val uri = try { url.toUri() } catch (_: Throwable) { url.toUri() } + val uri = url.toUri() val intent = Intent(Intent.ACTION_VIEW, uri) intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) try { diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/SongRemovalStateHolder.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/SongRemovalStateHolder.kt index 91e2d858f..8399c559d 100644 --- a/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/SongRemovalStateHolder.kt +++ b/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/SongRemovalStateHolder.kt @@ -2,6 +2,7 @@ package com.theveloper.pixelplay.presentation.viewmodel import android.app.Activity import android.content.IntentSender +import timber.log.Timber import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.theveloper.pixelplay.R import com.theveloper.pixelplay.data.model.Song @@ -92,7 +93,8 @@ class SongRemovalStateHolder @Inject constructor( dialog.show() userChoice.await() - } catch (_: Exception) { + } catch (e: Exception) { + Timber.w(e, "Failed to show song removal confirmation dialog") false } } diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/ThemeStateHolder.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/ThemeStateHolder.kt index a2bb2213a..10428c1a4 100644 --- a/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/ThemeStateHolder.kt +++ b/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/ThemeStateHolder.kt @@ -3,6 +3,7 @@ package com.theveloper.pixelplay.presentation.viewmodel import android.net.Uri import android.content.ComponentCallbacks2 import android.os.Trace +import timber.log.Timber import androidx.compose.ui.graphics.Color import com.theveloper.pixelplay.data.preferences.AlbumArtColorAccuracy import com.theveloper.pixelplay.data.preferences.AlbumArtPaletteStyle @@ -181,8 +182,8 @@ class ThemeStateHolder @Inject constructor( paletteStyle = currentPaletteStyle, colorAccuracyLevel = currentPaletteAccuracy ) - } catch (_: Exception) { - // Ignore or log + } catch (e: Exception) { + Timber.w(e, "Failed to generate color scheme for %s", uriString) } finally { val targets = synchronized(pendingAlbumColorSchemeLock) { pendingAlbumColorSchemeTargets.remove(uriString)?.toList().orEmpty() diff --git a/app/src/main/java/com/theveloper/pixelplay/utils/MediaMetadataRetrieverPool.kt b/app/src/main/java/com/theveloper/pixelplay/utils/MediaMetadataRetrieverPool.kt index 8ea8dffb5..c81d500f0 100644 --- a/app/src/main/java/com/theveloper/pixelplay/utils/MediaMetadataRetrieverPool.kt +++ b/app/src/main/java/com/theveloper/pixelplay/utils/MediaMetadataRetrieverPool.kt @@ -1,6 +1,7 @@ package com.theveloper.pixelplay.utils import android.media.MediaMetadataRetriever +import timber.log.Timber import java.util.concurrent.atomic.AtomicInteger /** @@ -40,8 +41,8 @@ object MediaMetadataRetrieverPool { internal fun release(retriever: MediaMetadataRetriever) { try { retriever.release() - } catch (_: Exception) { - // Ignore release errors + } catch (e: Exception) { + Timber.w(e, "Failed to release MediaMetadataRetriever") } finally { createdCount.decrementAndGet() } From b568ba3521b50568c25a9c592daaa68ae66e922a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?D=D0=B0=D0=B5=20=D0=95u=D0=BD=D1=88=D0=B0?= <134977461+daedaevibin@users.noreply.github.com> Date: Fri, 19 Jun 2026 02:42:31 +0000 Subject: [PATCH 87/99] Fix Netty version, parameter ordering, imports, and Spanish comments MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Security: - Update Netty constraint from non-existent 4.2.28.Final to latest actual release 4.2.15.Final to address Dependabot alerts #36, #37, #38 Bug fix: - Fix AbsoluteSmoothCornerShape parameter ordering in NavBarCornerRadiusScreen non-full-width preview — smoothness params were misaligned with their corner radius params Code quality: - Sort imports alphabetically in GenreCategoriesGrid; add explicit imports for FilledIconButton, Icon, IconButtonDefaults to replace inline FQN usage - Translate all Spanish comments to English in SongEntity, PlaylistViewModel, OtherShapes, GenreCategoriesGrid Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../pixelplay/data/database/SongEntity.kt | 16 ++- .../screens/NavBarCornerRadiusScreen.kt | 10 +- .../search/components/GenreCategoriesGrid.kt | 36 +++---- .../viewmodel/PlaylistViewModel.kt | 5 +- .../pixelplay/utils/shapes/OtherShapes.kt | 99 +++++++------------ gradle/libs.versions.toml | 2 +- 6 files changed, 70 insertions(+), 98 deletions(-) diff --git a/app/src/main/java/com/theveloper/pixelplay/data/database/SongEntity.kt b/app/src/main/java/com/theveloper/pixelplay/data/database/SongEntity.kt index d0df39b26..4368e0f1d 100644 --- a/app/src/main/java/com/theveloper/pixelplay/data/database/SongEntity.kt +++ b/app/src/main/java/com/theveloper/pixelplay/data/database/SongEntity.kt @@ -58,15 +58,13 @@ object SourceType { entity = AlbumEntity::class, parentColumns = ["id"], childColumns = ["album_id"], - onDelete = ForeignKey.CASCADE // Si un álbum se borra, sus canciones también + onDelete = ForeignKey.CASCADE // Deleting an album cascades to its songs ), ForeignKey( entity = ArtistEntity::class, parentColumns = ["id"], childColumns = ["artist_id"], - onDelete = ForeignKey.SET_NULL // Si un artista se borra, el artist_id de la canción se pone a null - // o podrías elegir CASCADE si las canciones no deben existir sin artista. - // SET_NULL es más flexible si las canciones pueden ser de "Artista Desconocido". + onDelete = ForeignKey.SET_NULL // Nullify artist_id when artist is deleted (keeps song as "Unknown Artist") ) ] ) @@ -77,7 +75,7 @@ data class SongEntity( @ColumnInfo(name = "artist_id") val artistId: Long, // Primary artist ID for backward compatibility @ColumnInfo(name = "album_artist") val albumArtist: String? = null, // Album artist from metadata @ColumnInfo(name = "album_name") val albumName: String, - @ColumnInfo(name = "album_id") val albumId: Long, // index = true eliminado + @ColumnInfo(name = "album_id") val albumId: Long, @ColumnInfo(name = "content_uri_string") val contentUriString: String, @ColumnInfo(name = "album_art_uri_string") val albumArtUriString: String?, @ColumnInfo(name = "duration") val duration: Long, @@ -214,9 +212,8 @@ fun List.toSongs(): List { return this.map { it.toSong() } } -// El modelo Song usa id como String, pero la entidad lo necesita como Long (de MediaStore) -// El modelo Song no tiene filePath, así que no se puede mapear desde ahí directamente. -// filePath y parentDirectoryPath se poblarán desde MediaStore en el SyncWorker. +// Song model uses String id but the entity needs Long (from MediaStore). +// filePath and parentDirectoryPath are populated from MediaStore in SyncWorker. fun Song.toEntity(filePathFromMediaStore: String, parentDirFromMediaStore: String): SongEntity { return SongEntity( id = this.id.toLong(), @@ -254,8 +251,7 @@ data class SongSummary( val duration: Long ) -// Sobrecarga o alternativa si los paths no están disponibles o no son necesarios al convertir de Modelo a Entidad -// (menos probable que se use si la entidad siempre requiere los paths) +// Fallback when file paths are unavailable during Song-to-Entity conversion. fun Song.toEntityWithoutPaths(): SongEntity { return SongEntity( id = this.id.toLong(), diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/screens/NavBarCornerRadiusScreen.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/screens/NavBarCornerRadiusScreen.kt index 199bbfe47..608709172 100644 --- a/app/src/main/java/com/theveloper/pixelplay/presentation/screens/NavBarCornerRadiusScreen.kt +++ b/app/src/main/java/com/theveloper/pixelplay/presentation/screens/NavBarCornerRadiusScreen.kt @@ -342,13 +342,13 @@ fun NavBarCornerRadiusContent( } else { AbsoluteSmoothCornerShape( cornerRadiusTL = 10.dp, - smoothnessAsPercentBL = 60, - cornerRadiusTR = 10.dp, - smoothnessAsPercentBR = 60, - cornerRadiusBR = sliderValue.dp, smoothnessAsPercentTL = 60, + cornerRadiusTR = 10.dp, + smoothnessAsPercentTR = 60, cornerRadiusBL = sliderValue.dp, - smoothnessAsPercentTR = 60 + smoothnessAsPercentBL = 60, + cornerRadiusBR = sliderValue.dp, + smoothnessAsPercentBR = 60 ) } ) { diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/screens/search/components/GenreCategoriesGrid.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/screens/search/components/GenreCategoriesGrid.kt index a81e6a4ef..7d9a3ec47 100644 --- a/app/src/main/java/com/theveloper/pixelplay/presentation/screens/search/components/GenreCategoriesGrid.kt +++ b/app/src/main/java/com/theveloper/pixelplay/presentation/screens/search/components/GenreCategoriesGrid.kt @@ -1,8 +1,12 @@ package com.theveloper.pixelplay.presentation.screens.search.components -import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.compose.animation.core.animateDpAsState +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.tween import androidx.compose.foundation.background +import androidx.compose.foundation.border import androidx.compose.foundation.clickable +import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.BoxWithConstraints @@ -22,9 +26,13 @@ import androidx.compose.foundation.lazy.grid.items import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.rounded.ViewList +import androidx.compose.material.icons.rounded.CheckCircle import androidx.compose.material.icons.rounded.GridView import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults +import androidx.compose.material3.FilledIconButton +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButtonDefaults import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -34,13 +42,18 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.scale import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.rememberTextMeasurer import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.media3.common.util.UnstableApi +import com.theveloper.pixelplay.R import com.theveloper.pixelplay.data.model.Genre import com.theveloper.pixelplay.presentation.components.MiniPlayerHeight import com.theveloper.pixelplay.presentation.components.SmartImage @@ -49,18 +62,7 @@ import com.theveloper.pixelplay.presentation.components.resolveNavBarOccupiedHei import com.theveloper.pixelplay.presentation.utils.GenreIconProvider import com.theveloper.pixelplay.presentation.viewmodel.PlayerViewModel import com.theveloper.pixelplay.ui.theme.LocalPixelPlayDarkTheme -import androidx.compose.ui.res.stringResource -import com.theveloper.pixelplay.R import racra.compose.smooth_corner_rect_library.AbsoluteSmoothCornerShape -import androidx.compose.ui.input.pointer.pointerInput -import androidx.compose.foundation.gestures.detectTapGestures -import androidx.compose.ui.draw.scale -import androidx.compose.foundation.border -import androidx.compose.material.icons.rounded.CheckCircle -import androidx.compose.material3.Icon -import androidx.compose.animation.core.animateDpAsState -import androidx.compose.animation.core.animateFloatAsState -import androidx.compose.animation.core.tween @OptIn(UnstableApi::class) @Composable @@ -137,15 +139,15 @@ fun GenreCategoriesGrid( label = "shapeAnimation" ) - androidx.compose.material3.FilledIconButton( + FilledIconButton( onClick = { playerViewModel.toggleGenreViewMode() }, - colors = androidx.compose.material3.IconButtonDefaults.filledIconButtonColors( + colors = IconButtonDefaults.filledIconButtonColors( containerColor = MaterialTheme.colorScheme.secondaryContainer, contentColor = MaterialTheme.colorScheme.onSecondaryContainer ), shape = RoundedCornerShape(animatedCornerRadius.value) ) { - androidx.compose.material3.Icon( + Icon( imageVector = if (isGridView) Icons.AutoMirrored.Rounded.ViewList else Icons.Rounded.GridView, contentDescription = "Toggle Grid/List View" ) @@ -274,7 +276,7 @@ private fun GenreCard( ) } - // Imagen del género en esquina inferior derecha + // Genre image in bottom-right corner Box( modifier = Modifier .size(90.dp) @@ -292,7 +294,7 @@ private fun GenreCard( ) } - // Nombre del género en esquina superior izquierda + // Genre name in top-left corner Column( modifier = Modifier .align(Alignment.TopStart) diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/PlaylistViewModel.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/PlaylistViewModel.kt index 489891ae5..45721f166 100644 --- a/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/PlaylistViewModel.kt +++ b/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/PlaylistViewModel.kt @@ -247,7 +247,7 @@ class PlaylistViewModel @Inject constructor( PlaylistSongsOrderMode.Manual -> songsList } - // La actualización del UI se hace en el hilo principal + // Update UI on the main thread _uiState.update { it.copy( currentPlaylistDetails = playlist, @@ -269,8 +269,7 @@ class PlaylistViewModel @Inject constructor( currentPlaylistDetails = null, currentPlaylistSongs = emptyList() ) - } // Mantener isLoading en false - // Opcional: podrías establecer un error o un estado específico de "no encontrado" + } } } } catch (e: Exception) { diff --git a/app/src/main/java/com/theveloper/pixelplay/utils/shapes/OtherShapes.kt b/app/src/main/java/com/theveloper/pixelplay/utils/shapes/OtherShapes.kt index d5dc24bd3..27243f125 100644 --- a/app/src/main/java/com/theveloper/pixelplay/utils/shapes/OtherShapes.kt +++ b/app/src/main/java/com/theveloper/pixelplay/utils/shapes/OtherShapes.kt @@ -33,10 +33,7 @@ fun createHexagonShape() = object : Shape { } } -// Implementaciones similares para createRoundedTriangleShape, createSemiCircleShape -// (Estas pueden ser más complejas dependiendo del diseño exacto que quieras) - -// Ejemplo simple de triángulo redondeado (tendrías que ajustarlo) +// Simple rounded triangle shape fun createRoundedTriangleShape() = object : Shape { override fun createOutline(size: androidx.compose.ui.geometry.Size, layoutDirection: LayoutDirection, density: Density): Outline { return Outline.Generic(Path().apply { @@ -46,21 +43,9 @@ fun createRoundedTriangleShape() = object : Shape { path.lineTo(0f, size.height) path.close() - // Para redondear las esquinas, podrías usar CornerPathEffect en un Modifier.drawBehind, - // o construir la forma con arcos y líneas. Clipping con Shape solo recorta. - // Una forma simple es usar un RoundRect para el clip con radios grandes, pero no es un triángulo real. - // Para un triángulo redondeado preciso, tendrías que dibujar la forma con arcos. - // Por ahora, dejaremos el clip simple o necesitarás una implementación más avanzada. - - // Alternativa simple: clip a un rectángulo con esquinas redondeadas - // return Outline.Rounded(RoundRect(0f, 0f, size.width, size.height, CornerRadius(16f, 16f))) - // Esto no es un triángulo. Necesitas una implementación real de forma de triángulo redondeado. - // Por simplicidad en este ejemplo, usaremos formas más estándar o de la librería. - - // Para el ejemplo, simplemente usaremos un triángulo básico sin redondeo complejo en el clip. - // Si necesitas triángulos redondeados reales, busca implementaciones más avanzadas. + // Basic triangle without rounded corners for clipping. moveTo(size.width / 2f, 0f) - lineTo(size.width, size.height * 0.8f) // Ajuste para que la base no llegue hasta abajo + lineTo(size.width, size.height * 0.8f) lineTo(0f, size.height * 0.8f) close() @@ -68,25 +53,23 @@ fun createRoundedTriangleShape() = object : Shape { } } -// Ejemplo simple de Semicírculo (tendrías que ajustarlo) +// Simple semicircle shape fun createSemiCircleShape() = object : Shape { override fun createOutline(size: androidx.compose.ui.geometry.Size, layoutDirection: LayoutDirection, density: Density): Outline { return Outline.Generic(Path().apply { arcTo( - rect = Rect(0f, 0f, size.width, size.width), // Un círculo basado en el ancho + rect = Rect(0f, 0f, size.width, size.width), startAngleDegrees = 0f, sweepAngleDegrees = 180f, forceMoveTo = false ) - lineTo(size.width / 2f, size.width / 2f) // Dibuja una línea hacia el centro si necesitas cerrarlo como pastel - close() // Opcional: cierra la forma + lineTo(size.width / 2f, size.width / 2f) + close() }) } } -/** - * Crea una forma de hexágono con esquinas redondeadas. - */ +/** Hexagon shape with rounded corners. */ fun createRoundedHexagonShape(cornerRadius: Dp) = object : Shape { override fun createOutline( size: Size, @@ -99,7 +82,7 @@ fun createRoundedHexagonShape(cornerRadius: Dp) = object : Shape { val radius = min(width, height) / 2f val cornerRadiusPx = with(density) { cornerRadius.toPx() } - // Puntos del hexágono sin redondear + // Unrounded hexagon vertices val points = (0..5).map { i -> val angle = PI / 3 * i Offset( @@ -108,7 +91,7 @@ fun createRoundedHexagonShape(cornerRadius: Dp) = object : Shape { ) } - // Movemos al primer punto con un offset para empezar el arco + // Move to first point offset for arc start moveTo(points[0].x + cornerRadiusPx * cos(PI / 3.0).toFloat(), points[0].y + cornerRadiusPx * sin(PI / 3.0).toFloat()) for (i in 0..5) { @@ -116,10 +99,10 @@ fun createRoundedHexagonShape(cornerRadius: Dp) = object : Shape { val p2 = points[(i + 1) % 6] val p3 = points[(i + 2) % 6] - // Línea hacia el punto de inicio del arco + // Line to arc start point lineTo(p2.x - cornerRadiusPx * cos(PI / 3.0).toFloat(), p2.y - cornerRadiusPx * sin(PI / 3.0).toFloat()) - // Arco en la esquina + // Corner arc arcTo( rect = Rect( left = p2.x - cornerRadiusPx, @@ -127,8 +110,8 @@ fun createRoundedHexagonShape(cornerRadius: Dp) = object : Shape { right = p2.x + cornerRadiusPx, bottom = p2.y + cornerRadiusPx ), - startAngleDegrees = (i * 60 + 30).toFloat(), // Ángulo de inicio del arco - sweepAngleDegrees = 60f, // Ángulo del arco + startAngleDegrees = (i * 60 + 30).toFloat(), + sweepAngleDegrees = 60f, forceMoveTo = false ) } @@ -137,10 +120,7 @@ fun createRoundedHexagonShape(cornerRadius: Dp) = object : Shape { } } -/** - * Crea una forma de triángulo con esquinas redondeadas. - * Implementación simple para clipping. - */ +/** Rounded-corner triangle shape for clipping. */ fun createRoundedTriangleShape(cornerRadius: Dp) = object : Shape { override fun createOutline(size: Size, layoutDirection: LayoutDirection, density: Density): Outline { return Outline.Generic(Path().apply { @@ -148,43 +128,38 @@ fun createRoundedTriangleShape(cornerRadius: Dp) = object : Shape { val height = size.height val cornerRadiusPx = with(density) { cornerRadius.toPx() } - // Puntos del triángulo - val p1 = Offset(width / 2f, 0f) // Superior - val p2 = Offset(width, height) // Inferior derecha - val p3 = Offset(0f, height) // Inferior izquierda + // Triangle vertices + val p1 = Offset(width / 2f, 0f) // Top + val p2 = Offset(width, height) // Bottom-right + val p3 = Offset(0f, height) // Bottom-left - // Para simplificar el redondeo en el clip, usaremos arcos. - // Esto no es un triángulo perfecto con arcos tangentes, sino un enfoque práctico para clipping. - - // Calcula puntos de control para los arcos + // Control points for corner arcs val control12 = Offset(p1.x + (p2.x - p1.x) * 0.8f, p1.y + (p2.y - p1.y) * 0.8f) val control23 = Offset(p2.x + (p3.x - p2.x) * 0.2f, p2.y + (p3.y - p2.y) * 0.2f) val control31 = Offset(p3.x + (p1.x - p3.x) * 0.8f, p3.y + (p1.y - p3.y) * 0.8f) - moveTo(p1.x, p1.y + cornerRadiusPx * 2) // Empieza un poco más abajo del vértice superior + moveTo(p1.x, p1.y + cornerRadiusPx * 2) - // Arco superior derecha + // Top-right arc quadraticTo(p1.x, p1.y, p1.x + cornerRadiusPx * sqrt(2f), p1.y + cornerRadiusPx * sqrt(2f)) lineTo(p2.x - cornerRadiusPx * sqrt(2f), p2.y - cornerRadiusPx * sqrt(2f)) - // Arco inferior derecha + // Bottom-right arc quadraticTo(p2.x, p2.y, p2.x - cornerRadiusPx * sqrt(2f), p2.y + cornerRadiusPx * sqrt(2f)) lineTo(p3.x + cornerRadiusPx * sqrt(2f), p3.y + cornerRadiusPx * sqrt(2f)) - // Arco inferior izquierda + // Bottom-left arc quadraticTo(p3.x, p3.y, p3.x + cornerRadiusPx * sqrt(2f), p3.y - cornerRadiusPx * sqrt(2f)) lineTo(p1.x - cornerRadiusPx * sqrt(2f), p1.y + cornerRadiusPx * sqrt(2f)) - close() // Cierra la forma + close() }) } } -/** - * Crea una forma de semicírculo con una base ligeramente redondeada. - */ +/** Semicircle shape with a slightly rounded base. */ fun createSemiCircleShape(cornerRadius: Dp) = object : Shape { override fun createOutline(size: Size, layoutDirection: LayoutDirection, density: Density): Outline { return Outline.Generic(Path().apply { @@ -193,40 +168,40 @@ fun createSemiCircleShape(cornerRadius: Dp) = object : Shape { val radius = width / 2f val cornerRadiusPx = with(density) { cornerRadius.toPx() } - // Arco superior (semicírculo) + // Top semicircle arc arcTo( - rect = Rect(0f, 0f, width, width), // Un círculo basado en el ancho + rect = Rect(0f, 0f, width, width), startAngleDegrees = 0f, sweepAngleDegrees = 180f, forceMoveTo = false ) - // Base (línea con arcos en los extremos) + // Base line with arcs at both ends val startBaseX = 0f + cornerRadiusPx val endBaseX = width - cornerRadiusPx - val baseY = width / 2f // La base está a la mitad del diámetro del círculo + val baseY = width / 2f - lineTo(endBaseX, baseY) // Línea hacia el final de la base + lineTo(endBaseX, baseY) - // Arco inferior derecho + // Bottom-right arc arcTo( rect = Rect(endBaseX - cornerRadiusPx, baseY - cornerRadiusPx, endBaseX + cornerRadiusPx, baseY + cornerRadiusPx), startAngleDegrees = 90f, - sweepAngleDegrees = -90f, // Arco hacia abajo + sweepAngleDegrees = -90f, forceMoveTo = false ) - lineTo(startBaseX, baseY + cornerRadiusPx) // Línea inferior + lineTo(startBaseX, baseY + cornerRadiusPx) - // Arco inferior izquierdo + // Bottom-left arc arcTo( rect = Rect(startBaseX - cornerRadiusPx, baseY - cornerRadiusPx, startBaseX + cornerRadiusPx, baseY + cornerRadiusPx), startAngleDegrees = 180f, - sweepAngleDegrees = -90f, // Arco hacia abajo + sweepAngleDegrees = -90f, forceMoveTo = false ) - close() // Cierra la forma + close() }) } } \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index e653436ec..b6efb2156 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -65,7 +65,7 @@ junit5 = "6.1.0" kuromoji = "0.9.0" pinyin4j = "2.5.1" securityCrypto = "1.1.0" -netty = "4.2.28.Final" +netty = "4.2.15.Final" bouncycastle = "1.84" commons-lang3 = "3.20.0" jdom2 = "2.0.6.1" From 36361c79bde6496954bd7406459b0b3fa133be72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?D=D0=B0=D0=B5=20=D0=95u=D0=BD=D1=88=D0=B0?= <134977461+daedaevibin@users.noreply.github.com> Date: Fri, 19 Jun 2026 03:04:08 +0000 Subject: [PATCH 88/99] fix(i18n): rename values-pt-BR to values-pt-rBR for Android resource convention Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- app/src/main/res/{values-pt-BR => values-pt-rBR}/plurals.xml | 0 app/src/main/res/{values-pt-BR => values-pt-rBR}/strings.xml | 0 app/src/main/res/{values-pt-BR => values-pt-rBR}/strings_auth.xml | 0 .../res/{values-pt-BR => values-pt-rBR}/strings_components.xml | 0 4 files changed, 0 insertions(+), 0 deletions(-) rename app/src/main/res/{values-pt-BR => values-pt-rBR}/plurals.xml (100%) rename app/src/main/res/{values-pt-BR => values-pt-rBR}/strings.xml (100%) rename app/src/main/res/{values-pt-BR => values-pt-rBR}/strings_auth.xml (100%) rename app/src/main/res/{values-pt-BR => values-pt-rBR}/strings_components.xml (100%) diff --git a/app/src/main/res/values-pt-BR/plurals.xml b/app/src/main/res/values-pt-rBR/plurals.xml similarity index 100% rename from app/src/main/res/values-pt-BR/plurals.xml rename to app/src/main/res/values-pt-rBR/plurals.xml diff --git a/app/src/main/res/values-pt-BR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml similarity index 100% rename from app/src/main/res/values-pt-BR/strings.xml rename to app/src/main/res/values-pt-rBR/strings.xml diff --git a/app/src/main/res/values-pt-BR/strings_auth.xml b/app/src/main/res/values-pt-rBR/strings_auth.xml similarity index 100% rename from app/src/main/res/values-pt-BR/strings_auth.xml rename to app/src/main/res/values-pt-rBR/strings_auth.xml diff --git a/app/src/main/res/values-pt-BR/strings_components.xml b/app/src/main/res/values-pt-rBR/strings_components.xml similarity index 100% rename from app/src/main/res/values-pt-BR/strings_components.xml rename to app/src/main/res/values-pt-rBR/strings_components.xml From 1bbfde2e473621b7448a022be50584be01081737 Mon Sep 17 00:00:00 2001 From: lostf1sh Date: Fri, 19 Jun 2026 17:58:48 +0300 Subject: [PATCH 89/99] 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 @@ - + - +