feat(auto): equalizer enable/disable and preset cycling from the car
Expose the equalizer's on/off toggle and preset choice as PlaybackStateCompat
custom actions on the now-playing screen. The redesign's removal of the
in-car equalizer FOLDER from the browse tree stays as-is (2403da3) -- this
is a different surface (playback screen custom actions, not a browse
folder) and does not reintroduce it.
Deliberately just 2 actions -- an on/off toggle plus a cycling preset
action, not one action per preset -- since Android Auto only surfaces a
limited number of custom actions. Both reuse the existing
setEcualizadorActivo/aplicarPreset entry points (the same ones
EstadoEcualizador's phone settings screen uses), so a car tap and a phone
tap behave identically and both keep the action labels in sync. Reuses the
bundled ic_stat_pluriwave drawable (the notification's own equalizer-bars
icon) -- zero new native assets. The 5-band constraint is untouched.
New pure, unit-tested functions in servicio_audio.dart: presetSiguiente,
nombrePresetVisible, controlesEcualizadorPersonalizados. New ARB keys
(eqCustomActionEnableLabel/DisableLabel/PresetLabel) across all 13 locales,
regenerated via flutter gen-l10n.
This commit is contained in:
+11
-1
@@ -830,5 +830,15 @@
|
||||
"welcomeHeadline": "عالمك، على الهواء مباشرة",
|
||||
"yourStationsTitle": "محطاتك",
|
||||
"nowListeningLabel": "الاستماع الآن",
|
||||
"popularNowTitle": "الأكثر شيوعًا الآن"
|
||||
"popularNowTitle": "الأكثر شيوعًا الآن",
|
||||
"eqCustomActionEnableLabel": "تفعيل الموازن",
|
||||
"eqCustomActionDisableLabel": "إيقاف الموازن",
|
||||
"eqCustomActionPresetLabel": "الإعداد المسبق: {preset}",
|
||||
"@eqCustomActionPresetLabel": {
|
||||
"placeholders": {
|
||||
"preset": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+11
-1
@@ -830,5 +830,15 @@
|
||||
"welcomeHeadline": "আপনার বিশ্ব, সরাসরি",
|
||||
"yourStationsTitle": "আপনার স্টেশন",
|
||||
"nowListeningLabel": "এখন শোনা হচ্ছে",
|
||||
"popularNowTitle": "এখন জনপ্রিয়"
|
||||
"popularNowTitle": "এখন জনপ্রিয়",
|
||||
"eqCustomActionEnableLabel": "ইকুয়ালাইজার চালু করুন",
|
||||
"eqCustomActionDisableLabel": "ইকুয়ালাইজার বন্ধ করুন",
|
||||
"eqCustomActionPresetLabel": "প্রিসেট: {preset}",
|
||||
"@eqCustomActionPresetLabel": {
|
||||
"placeholders": {
|
||||
"preset": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+11
-1
@@ -830,5 +830,15 @@
|
||||
"welcomeHeadline": "Deine Welt, live",
|
||||
"yourStationsTitle": "Deine Sender",
|
||||
"nowListeningLabel": "Läuft gerade",
|
||||
"popularNowTitle": "Jetzt beliebt"
|
||||
"popularNowTitle": "Jetzt beliebt",
|
||||
"eqCustomActionEnableLabel": "Equalizer aktivieren",
|
||||
"eqCustomActionDisableLabel": "Equalizer deaktivieren",
|
||||
"eqCustomActionPresetLabel": "Voreinstellung: {preset}",
|
||||
"@eqCustomActionPresetLabel": {
|
||||
"placeholders": {
|
||||
"preset": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+11
-1
@@ -830,5 +830,15 @@
|
||||
"welcomeBullet2Subtitle": "Your favorites and local music in the car",
|
||||
"welcomeBullet3Title": "Music alarms",
|
||||
"welcomeBullet3Subtitle": "With gradual volume rise and vacation mode",
|
||||
"welcomeCtaLabel": "Start listening"
|
||||
"welcomeCtaLabel": "Start listening",
|
||||
"eqCustomActionEnableLabel": "Enable equalizer",
|
||||
"eqCustomActionDisableLabel": "Disable equalizer",
|
||||
"eqCustomActionPresetLabel": "Preset: {preset}",
|
||||
"@eqCustomActionPresetLabel": {
|
||||
"placeholders": {
|
||||
"preset": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+11
-1
@@ -789,5 +789,15 @@
|
||||
"welcomeBullet2Subtitle": "Tus favoritas y tu música local en el auto",
|
||||
"welcomeBullet3Title": "Alarmas musicales",
|
||||
"welcomeBullet3Subtitle": "Con subida progresiva y modo vacaciones",
|
||||
"welcomeCtaLabel": "Empezar a escuchar"
|
||||
"welcomeCtaLabel": "Empezar a escuchar",
|
||||
"eqCustomActionEnableLabel": "Activar ecualizador",
|
||||
"eqCustomActionDisableLabel": "Desactivar ecualizador",
|
||||
"eqCustomActionPresetLabel": "Preset: {preset}",
|
||||
"@eqCustomActionPresetLabel": {
|
||||
"placeholders": {
|
||||
"preset": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+11
-1
@@ -830,5 +830,15 @@
|
||||
"welcomeHeadline": "Votre monde, en direct",
|
||||
"yourStationsTitle": "Vos stations",
|
||||
"nowListeningLabel": "En cours d'écoute",
|
||||
"popularNowTitle": "Populaire maintenant"
|
||||
"popularNowTitle": "Populaire maintenant",
|
||||
"eqCustomActionEnableLabel": "Activer l'égaliseur",
|
||||
"eqCustomActionDisableLabel": "Désactiver l'égaliseur",
|
||||
"eqCustomActionPresetLabel": "Préréglage : {preset}",
|
||||
"@eqCustomActionPresetLabel": {
|
||||
"placeholders": {
|
||||
"preset": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+11
-1
@@ -830,5 +830,15 @@
|
||||
"welcomeHeadline": "आपकी दुनिया, लाइव",
|
||||
"yourStationsTitle": "आपके स्टेशन",
|
||||
"nowListeningLabel": "अभी सुन रहे हैं",
|
||||
"popularNowTitle": "अभी लोकप्रिय"
|
||||
"popularNowTitle": "अभी लोकप्रिय",
|
||||
"eqCustomActionEnableLabel": "इक्वलाइज़र चालू करें",
|
||||
"eqCustomActionDisableLabel": "इक्वलाइज़र बंद करें",
|
||||
"eqCustomActionPresetLabel": "प्रीसेट: {preset}",
|
||||
"@eqCustomActionPresetLabel": {
|
||||
"placeholders": {
|
||||
"preset": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+11
-1
@@ -830,5 +830,15 @@
|
||||
"welcomeHeadline": "Duniamu, secara langsung",
|
||||
"yourStationsTitle": "Stasiun Anda",
|
||||
"nowListeningLabel": "Sedang mendengarkan",
|
||||
"popularNowTitle": "Populer sekarang"
|
||||
"popularNowTitle": "Populer sekarang",
|
||||
"eqCustomActionEnableLabel": "Aktifkan equalizer",
|
||||
"eqCustomActionDisableLabel": "Nonaktifkan equalizer",
|
||||
"eqCustomActionPresetLabel": "Prasetel: {preset}",
|
||||
"@eqCustomActionPresetLabel": {
|
||||
"placeholders": {
|
||||
"preset": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+11
-1
@@ -830,5 +830,15 @@
|
||||
"welcomeHeadline": "Il tuo mondo, in diretta",
|
||||
"yourStationsTitle": "Le tue emittenti",
|
||||
"nowListeningLabel": "In ascolto ora",
|
||||
"popularNowTitle": "Popolari ora"
|
||||
"popularNowTitle": "Popolari ora",
|
||||
"eqCustomActionEnableLabel": "Attiva equalizzatore",
|
||||
"eqCustomActionDisableLabel": "Disattiva equalizzatore",
|
||||
"eqCustomActionPresetLabel": "Preset attivo: {preset}",
|
||||
"@eqCustomActionPresetLabel": {
|
||||
"placeholders": {
|
||||
"preset": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+11
-1
@@ -830,5 +830,15 @@
|
||||
"welcomeHeadline": "あなたの世界を、ライブで",
|
||||
"yourStationsTitle": "あなたの局",
|
||||
"nowListeningLabel": "再生中",
|
||||
"popularNowTitle": "今人気"
|
||||
"popularNowTitle": "今人気",
|
||||
"eqCustomActionEnableLabel": "イコライザーをオンにする",
|
||||
"eqCustomActionDisableLabel": "イコライザーをオフにする",
|
||||
"eqCustomActionPresetLabel": "プリセット: {preset}",
|
||||
"@eqCustomActionPresetLabel": {
|
||||
"placeholders": {
|
||||
"preset": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+11
-1
@@ -830,5 +830,15 @@
|
||||
"welcomeHeadline": "Seu mundo, ao vivo",
|
||||
"yourStationsTitle": "Suas estações",
|
||||
"nowListeningLabel": "Ouvindo agora",
|
||||
"popularNowTitle": "Populares agora"
|
||||
"popularNowTitle": "Populares agora",
|
||||
"eqCustomActionEnableLabel": "Ativar equalizador",
|
||||
"eqCustomActionDisableLabel": "Desativar equalizador",
|
||||
"eqCustomActionPresetLabel": "Predefinição: {preset}",
|
||||
"@eqCustomActionPresetLabel": {
|
||||
"placeholders": {
|
||||
"preset": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+11
-1
@@ -830,5 +830,15 @@
|
||||
"welcomeHeadline": "Ваш мир, в прямом эфире",
|
||||
"yourStationsTitle": "Ваши станции",
|
||||
"nowListeningLabel": "Сейчас слушаете",
|
||||
"popularNowTitle": "Популярно сейчас"
|
||||
"popularNowTitle": "Популярно сейчас",
|
||||
"eqCustomActionEnableLabel": "Включить эквалайзер",
|
||||
"eqCustomActionDisableLabel": "Выключить эквалайзер",
|
||||
"eqCustomActionPresetLabel": "Пресет: {preset}",
|
||||
"@eqCustomActionPresetLabel": {
|
||||
"placeholders": {
|
||||
"preset": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+11
-1
@@ -830,5 +830,15 @@
|
||||
"welcomeHeadline": "你的世界,直播中",
|
||||
"yourStationsTitle": "你的电台",
|
||||
"nowListeningLabel": "正在收听",
|
||||
"popularNowTitle": "当前热门"
|
||||
"popularNowTitle": "当前热门",
|
||||
"eqCustomActionEnableLabel": "启用均衡器",
|
||||
"eqCustomActionDisableLabel": "关闭均衡器",
|
||||
"eqCustomActionPresetLabel": "预设:{preset}",
|
||||
"@eqCustomActionPresetLabel": {
|
||||
"placeholders": {
|
||||
"preset": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3049,6 +3049,24 @@ abstract class AppLocalizations {
|
||||
/// In es, this message translates to:
|
||||
/// **'Empezar a escuchar'**
|
||||
String get welcomeCtaLabel;
|
||||
|
||||
/// No description provided for @eqCustomActionEnableLabel.
|
||||
///
|
||||
/// In es, this message translates to:
|
||||
/// **'Activar ecualizador'**
|
||||
String get eqCustomActionEnableLabel;
|
||||
|
||||
/// No description provided for @eqCustomActionDisableLabel.
|
||||
///
|
||||
/// In es, this message translates to:
|
||||
/// **'Desactivar ecualizador'**
|
||||
String get eqCustomActionDisableLabel;
|
||||
|
||||
/// No description provided for @eqCustomActionPresetLabel.
|
||||
///
|
||||
/// In es, this message translates to:
|
||||
/// **'Preset: {preset}'**
|
||||
String eqCustomActionPresetLabel(String preset);
|
||||
}
|
||||
|
||||
class _AppLocalizationsDelegate
|
||||
|
||||
@@ -1676,4 +1676,15 @@ class AppLocalizationsAr extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get welcomeCtaLabel => 'ابدأ الاستماع';
|
||||
|
||||
@override
|
||||
String get eqCustomActionEnableLabel => 'تفعيل الموازن';
|
||||
|
||||
@override
|
||||
String get eqCustomActionDisableLabel => 'إيقاف الموازن';
|
||||
|
||||
@override
|
||||
String eqCustomActionPresetLabel(String preset) {
|
||||
return 'الإعداد المسبق: $preset';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1685,4 +1685,15 @@ class AppLocalizationsBn extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get welcomeCtaLabel => 'শোনা শুরু করুন';
|
||||
|
||||
@override
|
||||
String get eqCustomActionEnableLabel => 'ইকুয়ালাইজার চালু করুন';
|
||||
|
||||
@override
|
||||
String get eqCustomActionDisableLabel => 'ইকুয়ালাইজার বন্ধ করুন';
|
||||
|
||||
@override
|
||||
String eqCustomActionPresetLabel(String preset) {
|
||||
return 'প্রিসেট: $preset';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1698,4 +1698,15 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get welcomeCtaLabel => 'Jetzt hören';
|
||||
|
||||
@override
|
||||
String get eqCustomActionEnableLabel => 'Equalizer aktivieren';
|
||||
|
||||
@override
|
||||
String get eqCustomActionDisableLabel => 'Equalizer deaktivieren';
|
||||
|
||||
@override
|
||||
String eqCustomActionPresetLabel(String preset) {
|
||||
return 'Voreinstellung: $preset';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1678,4 +1678,15 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get welcomeCtaLabel => 'Start listening';
|
||||
|
||||
@override
|
||||
String get eqCustomActionEnableLabel => 'Enable equalizer';
|
||||
|
||||
@override
|
||||
String get eqCustomActionDisableLabel => 'Disable equalizer';
|
||||
|
||||
@override
|
||||
String eqCustomActionPresetLabel(String preset) {
|
||||
return 'Preset: $preset';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1692,4 +1692,15 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get welcomeCtaLabel => 'Empezar a escuchar';
|
||||
|
||||
@override
|
||||
String get eqCustomActionEnableLabel => 'Activar ecualizador';
|
||||
|
||||
@override
|
||||
String get eqCustomActionDisableLabel => 'Desactivar ecualizador';
|
||||
|
||||
@override
|
||||
String eqCustomActionPresetLabel(String preset) {
|
||||
return 'Preset: $preset';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1701,4 +1701,15 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get welcomeCtaLabel => 'Commencer à écouter';
|
||||
|
||||
@override
|
||||
String get eqCustomActionEnableLabel => 'Activer l\'égaliseur';
|
||||
|
||||
@override
|
||||
String get eqCustomActionDisableLabel => 'Désactiver l\'égaliseur';
|
||||
|
||||
@override
|
||||
String eqCustomActionPresetLabel(String preset) {
|
||||
return 'Préréglage : $preset';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1680,4 +1680,15 @@ class AppLocalizationsHi extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get welcomeCtaLabel => 'सुनना शुरू करें';
|
||||
|
||||
@override
|
||||
String get eqCustomActionEnableLabel => 'इक्वलाइज़र चालू करें';
|
||||
|
||||
@override
|
||||
String get eqCustomActionDisableLabel => 'इक्वलाइज़र बंद करें';
|
||||
|
||||
@override
|
||||
String eqCustomActionPresetLabel(String preset) {
|
||||
return 'प्रीसेट: $preset';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1688,4 +1688,15 @@ class AppLocalizationsId extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get welcomeCtaLabel => 'Mulai mendengarkan';
|
||||
|
||||
@override
|
||||
String get eqCustomActionEnableLabel => 'Aktifkan equalizer';
|
||||
|
||||
@override
|
||||
String get eqCustomActionDisableLabel => 'Nonaktifkan equalizer';
|
||||
|
||||
@override
|
||||
String eqCustomActionPresetLabel(String preset) {
|
||||
return 'Prasetel: $preset';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1700,4 +1700,15 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get welcomeCtaLabel => 'Inizia ad ascoltare';
|
||||
|
||||
@override
|
||||
String get eqCustomActionEnableLabel => 'Attiva equalizzatore';
|
||||
|
||||
@override
|
||||
String get eqCustomActionDisableLabel => 'Disattiva equalizzatore';
|
||||
|
||||
@override
|
||||
String eqCustomActionPresetLabel(String preset) {
|
||||
return 'Preset attivo: $preset';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1631,4 +1631,15 @@ class AppLocalizationsJa extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get welcomeCtaLabel => '聴き始める';
|
||||
|
||||
@override
|
||||
String get eqCustomActionEnableLabel => 'イコライザーをオンにする';
|
||||
|
||||
@override
|
||||
String get eqCustomActionDisableLabel => 'イコライザーをオフにする';
|
||||
|
||||
@override
|
||||
String eqCustomActionPresetLabel(String preset) {
|
||||
return 'プリセット: $preset';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1688,4 +1688,15 @@ class AppLocalizationsPt extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get welcomeCtaLabel => 'Começar a ouvir';
|
||||
|
||||
@override
|
||||
String get eqCustomActionEnableLabel => 'Ativar equalizador';
|
||||
|
||||
@override
|
||||
String get eqCustomActionDisableLabel => 'Desativar equalizador';
|
||||
|
||||
@override
|
||||
String eqCustomActionPresetLabel(String preset) {
|
||||
return 'Predefinição: $preset';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1692,4 +1692,15 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get welcomeCtaLabel => 'Начать слушать';
|
||||
|
||||
@override
|
||||
String get eqCustomActionEnableLabel => 'Включить эквалайзер';
|
||||
|
||||
@override
|
||||
String get eqCustomActionDisableLabel => 'Выключить эквалайзер';
|
||||
|
||||
@override
|
||||
String eqCustomActionPresetLabel(String preset) {
|
||||
return 'Пресет: $preset';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1623,4 +1623,15 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get welcomeCtaLabel => '开始收听';
|
||||
|
||||
@override
|
||||
String get eqCustomActionEnableLabel => '启用均衡器';
|
||||
|
||||
@override
|
||||
String get eqCustomActionDisableLabel => '关闭均衡器';
|
||||
|
||||
@override
|
||||
String eqCustomActionPresetLabel(String preset) {
|
||||
return '预设:$preset';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,6 +100,91 @@ Emisora emisoraDesdeMediaItem(MediaItem mediaItem) {
|
||||
);
|
||||
}
|
||||
|
||||
/// Custom-action names for the equalizer's `PlaybackStateCompat` custom
|
||||
/// actions on the now-playing screen (Design "EQ custom actions", item 4).
|
||||
/// Public consts so tests and this file's own `customAction` dispatch share
|
||||
/// the exact same literals; distinct from every browse-tree media-id prefix
|
||||
/// in `navegacion_auto.dart` (they live in a completely different
|
||||
/// `MediaControl`/`customAction` namespace, never compared against a
|
||||
/// media id).
|
||||
const accionEqToggle = 'eq_toggle';
|
||||
const accionEqPresetSiguiente = 'eq_preset_siguiente';
|
||||
|
||||
/// Advances to the NEXT factory preset after [actual] in [presets] order
|
||||
/// (Design "EQ custom actions — cycling presets", item 4): wraps around
|
||||
/// after the last one. When [actual] is not found in [presets] (e.g. a
|
||||
/// user-tweaked "Personalizado" preset from `EstadoEcualizador.cambiarBanda`),
|
||||
/// starts from the FIRST preset rather than throwing — cycling from an
|
||||
/// unknown state always lands somewhere sane. Pure, no I/O.
|
||||
///
|
||||
/// [presets] defaults to [PresetEcualizador.presets] — not a literal default
|
||||
/// value, since that field is `static final` (not `const`) and Dart default
|
||||
/// parameter values must be compile-time constants.
|
||||
PresetEcualizador presetSiguiente(
|
||||
PresetEcualizador actual, {
|
||||
List<PresetEcualizador>? presets,
|
||||
}) {
|
||||
final lista = presets ?? PresetEcualizador.presets;
|
||||
final indice = lista.indexWhere((p) => p == actual);
|
||||
if (indice == -1) return lista.first;
|
||||
return lista[(indice + 1) % lista.length];
|
||||
}
|
||||
|
||||
/// Localizes a preset's raw `nombre` for the equalizer custom action's
|
||||
/// label (Design "EQ custom actions", item 4) — mirrors
|
||||
/// `ecualizador_widget.dart`'s private `_nombrePreset` mapping (duplicated
|
||||
/// rather than shared: that file is UI-widget layer, this one is the
|
||||
/// service/handler layer, and the mapping is a single small switch, not
|
||||
/// worth a cross-layer import for). An unrecognized name (e.g. a future
|
||||
/// user-named custom preset) falls through to the raw name verbatim.
|
||||
String nombrePresetVisible(AppLocalizations l10n, String nombre) {
|
||||
return switch (nombre) {
|
||||
'Flat' => l10n.equalizerPresetFlat,
|
||||
'Rock' => l10n.equalizerPresetRock,
|
||||
'Pop' => l10n.equalizerPresetPop,
|
||||
'Bass Boost' => l10n.equalizerPresetBassBoost,
|
||||
'Jazz' => l10n.equalizerPresetJazz,
|
||||
'Voz' => l10n.equalizerPresetVoice,
|
||||
'Personalizado' => l10n.equalizerPresetCustom,
|
||||
_ => nombre,
|
||||
};
|
||||
}
|
||||
|
||||
/// Builds the equalizer's custom-action `MediaControl`s for the now-playing
|
||||
/// screen (Design "EQ custom actions", item 4) — deliberately just 2: an
|
||||
/// on/off toggle plus a cycling-preset action, NOT one action per preset,
|
||||
/// since Android Auto only surfaces a limited number of custom actions.
|
||||
/// Empty when [disponible] is false (gate on EQ availability, mirrors the
|
||||
/// existing `debeReaplicarEcualizador`/`_eqDisponible` gate) — a device
|
||||
/// without the native Equalizer effect gets no EQ actions at all, not
|
||||
/// broken ones. Reuses the SAME bundled `ic_stat_pluriwave` drawable the
|
||||
/// notification's own status-bar icon already uses (an equalizer-bars
|
||||
/// glyph) — zero new native assets. Pure, no handler dependency.
|
||||
List<MediaControl> controlesEcualizadorPersonalizados({
|
||||
required bool disponible,
|
||||
required bool activo,
|
||||
required PresetEcualizador presetActual,
|
||||
required AppLocalizations l10n,
|
||||
}) {
|
||||
if (!disponible) return const [];
|
||||
return [
|
||||
MediaControl.custom(
|
||||
androidIcon: 'drawable/ic_stat_pluriwave',
|
||||
label: activo
|
||||
? l10n.eqCustomActionDisableLabel
|
||||
: l10n.eqCustomActionEnableLabel,
|
||||
name: accionEqToggle,
|
||||
),
|
||||
MediaControl.custom(
|
||||
androidIcon: 'drawable/ic_stat_pluriwave',
|
||||
label: l10n.eqCustomActionPresetLabel(
|
||||
nombrePresetVisible(l10n, presetActual.nombre),
|
||||
),
|
||||
name: accionEqPresetSiguiente,
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
/// Wrapper de alto nivel para el UI.
|
||||
class ServicioAudio {
|
||||
PluriWaveAudioHandler get _handler {
|
||||
@@ -319,12 +404,10 @@ class PluriWaveAudioHandler extends BaseAudioHandler
|
||||
final colaActiva = _colaLocal != null;
|
||||
playbackState.add(
|
||||
playbackState.value.copyWith(
|
||||
controls: [
|
||||
if (colaActiva) MediaControl.skipToPrevious,
|
||||
if (playing) MediaControl.pause else MediaControl.play,
|
||||
MediaControl.stop,
|
||||
if (colaActiva) MediaControl.skipToNext,
|
||||
],
|
||||
controls: _controlesTransporte(
|
||||
colaActiva: colaActiva,
|
||||
playing: playing,
|
||||
),
|
||||
systemActions: {
|
||||
MediaAction.seek,
|
||||
MediaAction.stop,
|
||||
@@ -369,6 +452,49 @@ class PluriWaveAudioHandler extends BaseAudioHandler
|
||||
});
|
||||
}
|
||||
|
||||
/// The full transport `controls` list for a `playbackState` push (item 4):
|
||||
/// the existing skip/play-pause/stop set, plus the equalizer's custom
|
||||
/// actions appended at the end. Appending (rather than interleaving) keeps
|
||||
/// [MediaControl.skipToPrevious]/play-pause/stop/[MediaControl.skipToNext]
|
||||
/// at their existing indices 0-3, so `androidCompactActionIndices`
|
||||
/// (`[colaActiva ? 1 : 0]`) stays correct unchanged.
|
||||
List<MediaControl> _controlesTransporte({
|
||||
required bool colaActiva,
|
||||
required bool playing,
|
||||
}) => [
|
||||
if (colaActiva) MediaControl.skipToPrevious,
|
||||
if (playing) MediaControl.pause else MediaControl.play,
|
||||
MediaControl.stop,
|
||||
if (colaActiva) MediaControl.skipToNext,
|
||||
..._controlesEqPersonalizados(),
|
||||
];
|
||||
|
||||
List<MediaControl> _controlesEqPersonalizados() =>
|
||||
controlesEcualizadorPersonalizados(
|
||||
disponible: _eqDisponible,
|
||||
activo: _ecualizadorActivo,
|
||||
presetActual: _presetActual,
|
||||
l10n: _textos,
|
||||
);
|
||||
|
||||
/// Re-pushes `playbackState` with a freshly built controls list (item 4):
|
||||
/// called whenever EQ availability/enabled/preset state changes outside a
|
||||
/// player-state transition (a custom-action tap, or a phone-side preset/
|
||||
/// toggle change), so the equalizer custom actions' label and current-
|
||||
/// preset name stay in sync on the now-playing screen without waiting for
|
||||
/// an unrelated player event. Idempotent and cheap (no native calls) —
|
||||
/// safe to call from any EQ state-changing path.
|
||||
void _actualizarControlesEq() {
|
||||
playbackState.add(
|
||||
playbackState.value.copyWith(
|
||||
controls: _controlesTransporte(
|
||||
colaActiva: _colaLocal != null,
|
||||
playing: playbackState.value.playing,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Gestiona cualquier error de reproducción de ExoPlayer.
|
||||
///
|
||||
/// Network-class failures while the user still intends to play enter the
|
||||
@@ -754,6 +880,11 @@ class PluriWaveAudioHandler extends BaseAudioHandler
|
||||
} catch (_) {
|
||||
_eqDisponible = false;
|
||||
}
|
||||
// Item 4: an availability flip (e.g. a station switch that lands on a
|
||||
// device without the native Equalizer effect) must show/hide the EQ
|
||||
// custom actions immediately, not wait for a coincidental later
|
||||
// player-state event.
|
||||
_actualizarControlesEq();
|
||||
}
|
||||
|
||||
/// Pure re-apply decision for a native session-id emission. No side effects.
|
||||
@@ -772,25 +903,31 @@ class PluriWaveAudioHandler extends BaseAudioHandler
|
||||
/// Aplica un preset al ecualizador nativo Android.
|
||||
Future<void> aplicarPreset(PresetEcualizador preset) async {
|
||||
_presetActual = preset;
|
||||
if (!_eqDisponible) return;
|
||||
try {
|
||||
await _eq.setEnabled(_ecualizadorActivo);
|
||||
if (!_ecualizadorActivo) return;
|
||||
final params = await _eq.parameters;
|
||||
for (
|
||||
int i = 0;
|
||||
i < params.bands.length && i < preset.bandas.length;
|
||||
i++
|
||||
) {
|
||||
await params.bands[i].setGain(
|
||||
_mapearGananciaNativa(
|
||||
preset.bandas[i],
|
||||
minDecibels: params.minDecibels,
|
||||
maxDecibels: params.maxDecibels,
|
||||
),
|
||||
);
|
||||
}
|
||||
} catch (_) {}
|
||||
if (_eqDisponible) {
|
||||
try {
|
||||
await _eq.setEnabled(_ecualizadorActivo);
|
||||
if (_ecualizadorActivo) {
|
||||
final params = await _eq.parameters;
|
||||
for (
|
||||
int i = 0;
|
||||
i < params.bands.length && i < preset.bandas.length;
|
||||
i++
|
||||
) {
|
||||
await params.bands[i].setGain(
|
||||
_mapearGananciaNativa(
|
||||
preset.bandas[i],
|
||||
minDecibels: params.minDecibels,
|
||||
maxDecibels: params.maxDecibels,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
// Item 4: keeps the EQ custom action's preset-cycle label in sync
|
||||
// regardless of WHO changed the preset (a car customAction tap or the
|
||||
// phone settings screen via EstadoEcualizador) — single chokepoint.
|
||||
_actualizarControlesEq();
|
||||
}
|
||||
|
||||
/// Ajusta una banda individual.
|
||||
@@ -826,13 +963,18 @@ class PluriWaveAudioHandler extends BaseAudioHandler
|
||||
|
||||
Future<void> setEcualizadorActivo(bool activo) async {
|
||||
_ecualizadorActivo = activo;
|
||||
if (!_eqDisponible) return;
|
||||
try {
|
||||
await _eq.setEnabled(activo);
|
||||
if (activo) {
|
||||
await aplicarPreset(_presetActual);
|
||||
}
|
||||
} catch (_) {}
|
||||
if (_eqDisponible) {
|
||||
try {
|
||||
await _eq.setEnabled(activo);
|
||||
if (activo) {
|
||||
await aplicarPreset(_presetActual);
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
// Item 4: keeps the EQ custom action's on/off label in sync regardless
|
||||
// of WHO toggled it (a car customAction tap or the phone settings
|
||||
// screen via EstadoEcualizador).
|
||||
_actualizarControlesEq();
|
||||
}
|
||||
|
||||
Future<void> setVolumen(double vol) async {
|
||||
@@ -929,6 +1071,27 @@ class PluriWaveAudioHandler extends BaseAudioHandler
|
||||
await _reproducirEntradaCola(anterior.actual);
|
||||
}
|
||||
|
||||
/// Dispatches the equalizer's 2 custom actions (item 4, Design "EQ custom
|
||||
/// actions"): `accionEqToggle` flips on/off, `accionEqPresetSiguiente`
|
||||
/// cycles to the next factory preset. Both delegate to the existing
|
||||
/// [setEcualizadorActivo]/[aplicarPreset] — the SAME entry points the
|
||||
/// phone settings screen uses via `EstadoEcualizador` — so a car tap and a
|
||||
/// phone tap have identical effects and both refresh the custom action's
|
||||
/// label via `_actualizarControlesEq()` (already wired into those two
|
||||
/// methods). Any other [name] is a no-op — never throws.
|
||||
@override
|
||||
Future<dynamic> customAction(
|
||||
String name, [
|
||||
Map<String, dynamic>? extras,
|
||||
]) async {
|
||||
switch (name) {
|
||||
case accionEqToggle:
|
||||
await setEcualizadorActivo(!_ecualizadorActivo);
|
||||
case accionEqPresetSiguiente:
|
||||
await aplicarPreset(presetSiguiente(_presetActual));
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> onTaskRemoved() async {
|
||||
await stop();
|
||||
|
||||
@@ -0,0 +1,189 @@
|
||||
import 'dart:ui' show Locale;
|
||||
|
||||
import 'package:audio_service/audio_service.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:pluriwave/l10n/gen/app_localizations.dart';
|
||||
import 'package:pluriwave/modelos/preset_ecualizador.dart';
|
||||
import 'package:pluriwave/servicios/servicio_audio.dart';
|
||||
|
||||
/// Item 4 (Android Auto: equalizer custom actions) — the pure, handler-
|
||||
/// independent half of the fix. `PluriWaveAudioHandler` cannot be
|
||||
/// instantiated in unit tests (a real `just_audio.AudioPlayer` requires
|
||||
/// platform MethodChannels), so the preset-cycling decision, the preset-name
|
||||
/// localization and the `MediaControl` list construction are extracted as
|
||||
/// pure top-level functions here. The handler's own `customAction` dispatch
|
||||
/// and `playbackState` wiring are static-review-only, same as the existing
|
||||
/// EQ re-apply/session-id wiring.
|
||||
void main() {
|
||||
final l10n = lookupAppLocalizations(const Locale('es'));
|
||||
|
||||
group('presetSiguiente (item 4 — cycling presets)', () {
|
||||
test('advances to the next preset in order', () {
|
||||
expect(
|
||||
presetSiguiente(PresetEcualizador.flat),
|
||||
PresetEcualizador.rock,
|
||||
);
|
||||
expect(
|
||||
presetSiguiente(PresetEcualizador.rock),
|
||||
PresetEcualizador.pop,
|
||||
);
|
||||
});
|
||||
|
||||
test('wraps around after the last preset', () {
|
||||
expect(
|
||||
presetSiguiente(PresetEcualizador.presets.last),
|
||||
PresetEcualizador.presets.first,
|
||||
);
|
||||
});
|
||||
|
||||
test(
|
||||
'an unknown/custom preset (e.g. a user-tweaked "Personalizado" band '
|
||||
'set) starts from the FIRST preset instead of throwing',
|
||||
() {
|
||||
final personalizado = PresetEcualizador(
|
||||
nombre: 'Personalizado',
|
||||
bandas: [1.0, 2.0, 3.0, 4.0, 5.0],
|
||||
);
|
||||
|
||||
expect(
|
||||
presetSiguiente(personalizado),
|
||||
PresetEcualizador.presets.first,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
test('respects an injected presets list instead of the default 6', () {
|
||||
final propios = [PresetEcualizador.jazz, PresetEcualizador.voz];
|
||||
|
||||
expect(
|
||||
presetSiguiente(PresetEcualizador.jazz, presets: propios),
|
||||
PresetEcualizador.voz,
|
||||
);
|
||||
expect(
|
||||
presetSiguiente(PresetEcualizador.voz, presets: propios),
|
||||
PresetEcualizador.jazz,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
group('nombrePresetVisible (item 4)', () {
|
||||
test('maps every factory preset name to its localized ARB string', () {
|
||||
expect(nombrePresetVisible(l10n, 'Flat'), l10n.equalizerPresetFlat);
|
||||
expect(nombrePresetVisible(l10n, 'Rock'), l10n.equalizerPresetRock);
|
||||
expect(nombrePresetVisible(l10n, 'Pop'), l10n.equalizerPresetPop);
|
||||
expect(
|
||||
nombrePresetVisible(l10n, 'Bass Boost'),
|
||||
l10n.equalizerPresetBassBoost,
|
||||
);
|
||||
expect(nombrePresetVisible(l10n, 'Jazz'), l10n.equalizerPresetJazz);
|
||||
expect(nombrePresetVisible(l10n, 'Voz'), l10n.equalizerPresetVoice);
|
||||
expect(
|
||||
nombrePresetVisible(l10n, 'Personalizado'),
|
||||
l10n.equalizerPresetCustom,
|
||||
);
|
||||
});
|
||||
|
||||
test('an unrecognized name falls through verbatim', () {
|
||||
expect(nombrePresetVisible(l10n, 'Mi Preset Guardado'), 'Mi Preset Guardado');
|
||||
});
|
||||
});
|
||||
|
||||
group('controlesEcualizadorPersonalizados (item 4)', () {
|
||||
test('empty when the equalizer is not available on this device', () {
|
||||
final controles = controlesEcualizadorPersonalizados(
|
||||
disponible: false,
|
||||
activo: true,
|
||||
presetActual: PresetEcualizador.flat,
|
||||
l10n: l10n,
|
||||
);
|
||||
|
||||
expect(controles, isEmpty);
|
||||
});
|
||||
|
||||
test(
|
||||
'exactly 2 custom actions when available: on/off toggle + preset '
|
||||
'cycle -- Android Auto shows a limited number of custom actions, so '
|
||||
'this is deliberately NOT one action per preset',
|
||||
() {
|
||||
final controles = controlesEcualizadorPersonalizados(
|
||||
disponible: true,
|
||||
activo: true,
|
||||
presetActual: PresetEcualizador.rock,
|
||||
l10n: l10n,
|
||||
);
|
||||
|
||||
expect(controles, hasLength(2));
|
||||
expect(controles.every((c) => c.action == MediaAction.custom), isTrue);
|
||||
},
|
||||
);
|
||||
|
||||
test('toggle label reflects ON -> shows "disable" action', () {
|
||||
final controles = controlesEcualizadorPersonalizados(
|
||||
disponible: true,
|
||||
activo: true,
|
||||
presetActual: PresetEcualizador.flat,
|
||||
l10n: l10n,
|
||||
);
|
||||
final toggle = controles.firstWhere(
|
||||
(c) => c.customAction?.name == accionEqToggle,
|
||||
);
|
||||
|
||||
expect(toggle.label, l10n.eqCustomActionDisableLabel);
|
||||
});
|
||||
|
||||
test('toggle label reflects OFF -> shows "enable" action', () {
|
||||
final controles = controlesEcualizadorPersonalizados(
|
||||
disponible: true,
|
||||
activo: false,
|
||||
presetActual: PresetEcualizador.flat,
|
||||
l10n: l10n,
|
||||
);
|
||||
final toggle = controles.firstWhere(
|
||||
(c) => c.customAction?.name == accionEqToggle,
|
||||
);
|
||||
|
||||
expect(toggle.label, l10n.eqCustomActionEnableLabel);
|
||||
});
|
||||
|
||||
test('preset-cycle label shows the CURRENT preset localized name', () {
|
||||
final controles = controlesEcualizadorPersonalizados(
|
||||
disponible: true,
|
||||
activo: true,
|
||||
presetActual: PresetEcualizador.jazz,
|
||||
l10n: l10n,
|
||||
);
|
||||
final ciclo = controles.firstWhere(
|
||||
(c) => c.customAction?.name == accionEqPresetSiguiente,
|
||||
);
|
||||
|
||||
expect(
|
||||
ciclo.label,
|
||||
l10n.eqCustomActionPresetLabel(l10n.equalizerPresetJazz),
|
||||
);
|
||||
});
|
||||
|
||||
test('both actions reuse the bundled notification drawable (zero new '
|
||||
'native assets)', () {
|
||||
final controles = controlesEcualizadorPersonalizados(
|
||||
disponible: true,
|
||||
activo: true,
|
||||
presetActual: PresetEcualizador.flat,
|
||||
l10n: l10n,
|
||||
);
|
||||
|
||||
expect(
|
||||
controles.every((c) => c.androidIcon == 'drawable/ic_stat_pluriwave'),
|
||||
isTrue,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
group('action name constants (item 4 — collision-free with car-tree ids)', () {
|
||||
test('names are non-empty, distinct, and do not collide with any '
|
||||
'existing browse-tree media-id prefix', () {
|
||||
expect(accionEqToggle, isNotEmpty);
|
||||
expect(accionEqPresetSiguiente, isNotEmpty);
|
||||
expect(accionEqToggle, isNot(equals(accionEqPresetSiguiente)));
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user