fix(eq): split the equalizer body into 4 cards, add Restablecer a plano

Audit 11.10/11.8 (t4 lines 570-602): the prototype draws the explainer
banner, the band sliders, "Salida activa + Guardar como preset +
Restablecer a plano" and "Emisoras con ajuste propio" as 4 separate
cards. The build nested everything in one outer PluriGlassSurface, so
it read as a single merged block, and had no reset-to-flat action at
all.

Removes the outer wrapper from _CuerpoEcualizador. EcualizadorWidget
already draws its own card (design ADR-5, unchanged); the explainer
banner already has its own tinted background (unchanged); "Emisoras
con ajuste propio" now wraps itself in its own PluriGlassSurface; a
new _TarjetaSalidaYAcciones groups Salida activa with two tappable
rows — "Guardar como preset" (relocated from a floating end-aligned
button) and the new "Restablecer a plano", which applies
PresetEcualizador.flat via the existing EstadoEcualizador.cambiarPreset
— the same call the "Plano" preset chip already makes, not a new
capability.

The 5-slider band count, servicio_ecualizador.dart and
preset_ecualizador.dart are untouched.
This commit is contained in:
2026-07-30 00:57:29 +02:00
parent e5d461af53
commit 653848899f
29 changed files with 373 additions and 96 deletions
+1
View File
@@ -711,6 +711,7 @@
"equalizerActiveOutputLabel": "المخرج النشط", "equalizerActiveOutputLabel": "المخرج النشط",
"equalizerBaseExplainer": "هذا هو المعادل الأساسي: يُطبَّق على كل محطة لا تملك إعدادًا خاصًا بها. يُحدَّد معادل محطة معينة من شاشة تشغيلها الخاصة، وله الأولوية على هذا المعادل.", "equalizerBaseExplainer": "هذا هو المعادل الأساسي: يُطبَّق على كل محطة لا تملك إعدادًا خاصًا بها. يُحدَّد معادل محطة معينة من شاشة تشغيلها الخاصة، وله الأولوية على هذا المعادل.",
"equalizerSaveAsPresetAction": "حفظ كإعداد مسبق", "equalizerSaveAsPresetAction": "حفظ كإعداد مسبق",
"equalizerResetToFlatAction": "إعادة الضبط إلى مسطح",
"equalizerSavePresetConfirm": "حفظ", "equalizerSavePresetConfirm": "حفظ",
"equalizerSavePresetDialogTitle": "حفظ كإعداد مسبق", "equalizerSavePresetDialogTitle": "حفظ كإعداد مسبق",
"equalizerSavePresetEmptyNameError": "أدخل اسمًا للإعداد المسبق.", "equalizerSavePresetEmptyNameError": "أدخل اسمًا للإعداد المسبق.",
+1
View File
@@ -711,6 +711,7 @@
"equalizerActiveOutputLabel": "সক্রিয় আউটপুট", "equalizerActiveOutputLabel": "সক্রিয় আউটপুট",
"equalizerBaseExplainer": "এটি বেস ইকুয়ালাইজার: এটি নিজস্ব সেটিং নেই এমন সব স্টেশনে প্রযোজ্য। কোনো নির্দিষ্ট স্টেশনের নিজস্ব EQ তার প্লেব্যাক স্ক্রিন থেকে ঠিক করা হয় এবং এটির চেয়ে অগ্রাধিকার পায়।", "equalizerBaseExplainer": "এটি বেস ইকুয়ালাইজার: এটি নিজস্ব সেটিং নেই এমন সব স্টেশনে প্রযোজ্য। কোনো নির্দিষ্ট স্টেশনের নিজস্ব EQ তার প্লেব্যাক স্ক্রিন থেকে ঠিক করা হয় এবং এটির চেয়ে অগ্রাধিকার পায়।",
"equalizerSaveAsPresetAction": "প্রিসেট হিসেবে সংরক্ষণ করুন", "equalizerSaveAsPresetAction": "প্রিসেট হিসেবে সংরক্ষণ করুন",
"equalizerResetToFlatAction": "সমতলে পুনরায় সেট করুন",
"equalizerSavePresetConfirm": "সংরক্ষণ করুন", "equalizerSavePresetConfirm": "সংরক্ষণ করুন",
"equalizerSavePresetDialogTitle": "প্রিসেট হিসেবে সংরক্ষণ করুন", "equalizerSavePresetDialogTitle": "প্রিসেট হিসেবে সংরক্ষণ করুন",
"equalizerSavePresetEmptyNameError": "প্রিসেটের জন্য একটি নাম লিখুন।", "equalizerSavePresetEmptyNameError": "প্রিসেটের জন্য একটি নাম লিখুন।",
+1
View File
@@ -711,6 +711,7 @@
"equalizerActiveOutputLabel": "Aktiver Ausgang", "equalizerActiveOutputLabel": "Aktiver Ausgang",
"equalizerBaseExplainer": "Dies ist der Basis-Equalizer: Er gilt für alle Sender ohne eigene Einstellung. Der eigene EQ eines Senders wird auf dessen eigenem Wiedergabebildschirm festgelegt und hat Vorrang vor diesem hier.", "equalizerBaseExplainer": "Dies ist der Basis-Equalizer: Er gilt für alle Sender ohne eigene Einstellung. Der eigene EQ eines Senders wird auf dessen eigenem Wiedergabebildschirm festgelegt und hat Vorrang vor diesem hier.",
"equalizerSaveAsPresetAction": "Als Preset speichern", "equalizerSaveAsPresetAction": "Als Preset speichern",
"equalizerResetToFlatAction": "Auf neutral zurücksetzen",
"equalizerSavePresetConfirm": "Speichern", "equalizerSavePresetConfirm": "Speichern",
"equalizerSavePresetDialogTitle": "Als Preset speichern", "equalizerSavePresetDialogTitle": "Als Preset speichern",
"equalizerSavePresetEmptyNameError": "Gib einen Namen für das Preset ein.", "equalizerSavePresetEmptyNameError": "Gib einen Namen für das Preset ein.",
+1
View File
@@ -102,6 +102,7 @@
"equalizerStationsWithOwnEqSubtitle": "Ignore this base equalizer", "equalizerStationsWithOwnEqSubtitle": "Ignore this base equalizer",
"equalizerStationsWithOwnEqEmpty": "No stations have their own EQ yet.", "equalizerStationsWithOwnEqEmpty": "No stations have their own EQ yet.",
"equalizerSaveAsPresetAction": "Save as preset", "equalizerSaveAsPresetAction": "Save as preset",
"equalizerResetToFlatAction": "Reset to flat",
"equalizerSavePresetDialogTitle": "Save as preset", "equalizerSavePresetDialogTitle": "Save as preset",
"equalizerSavePresetNameLabel": "Preset name", "equalizerSavePresetNameLabel": "Preset name",
"equalizerSavePresetEmptyNameError": "Enter a name for the preset.", "equalizerSavePresetEmptyNameError": "Enter a name for the preset.",
+1
View File
@@ -102,6 +102,7 @@
"equalizerStationsWithOwnEqSubtitle": "Ignoran este ecualizador base", "equalizerStationsWithOwnEqSubtitle": "Ignoran este ecualizador base",
"equalizerStationsWithOwnEqEmpty": "Ninguna emisora tiene ajuste propio todavía.", "equalizerStationsWithOwnEqEmpty": "Ninguna emisora tiene ajuste propio todavía.",
"equalizerSaveAsPresetAction": "Guardar como preset", "equalizerSaveAsPresetAction": "Guardar como preset",
"equalizerResetToFlatAction": "Restablecer a plano",
"equalizerSavePresetDialogTitle": "Guardar como preset", "equalizerSavePresetDialogTitle": "Guardar como preset",
"equalizerSavePresetNameLabel": "Nombre del preset", "equalizerSavePresetNameLabel": "Nombre del preset",
"equalizerSavePresetEmptyNameError": "Ingresá un nombre para el preset.", "equalizerSavePresetEmptyNameError": "Ingresá un nombre para el preset.",
+1
View File
@@ -711,6 +711,7 @@
"equalizerActiveOutputLabel": "Sortie active", "equalizerActiveOutputLabel": "Sortie active",
"equalizerBaseExplainer": "Voici l'égaliseur de base : il s'applique à toutes les stations sans réglage propre. L'EQ propre à une station est défini depuis son propre écran de lecture et prime sur celui-ci.", "equalizerBaseExplainer": "Voici l'égaliseur de base : il s'applique à toutes les stations sans réglage propre. L'EQ propre à une station est défini depuis son propre écran de lecture et prime sur celui-ci.",
"equalizerSaveAsPresetAction": "Enregistrer comme Preset", "equalizerSaveAsPresetAction": "Enregistrer comme Preset",
"equalizerResetToFlatAction": "Réinitialiser à plat",
"equalizerSavePresetConfirm": "Enregistrer", "equalizerSavePresetConfirm": "Enregistrer",
"equalizerSavePresetDialogTitle": "Enregistrer comme Preset", "equalizerSavePresetDialogTitle": "Enregistrer comme Preset",
"equalizerSavePresetEmptyNameError": "Saisissez un nom pour le Preset.", "equalizerSavePresetEmptyNameError": "Saisissez un nom pour le Preset.",
+1
View File
@@ -711,6 +711,7 @@
"equalizerActiveOutputLabel": "सक्रिय आउटपुट", "equalizerActiveOutputLabel": "सक्रिय आउटपुट",
"equalizerBaseExplainer": "यह बेस इक्वलाइज़र है: यह उन सभी स्टेशन पर लागू होता है जिनकी अपनी सेटिंग नहीं है। किसी खास स्टेशन का अपना EQ उसकी प्लेबैक स्क्रीन से तय होता है और इस पर प्राथमिकता रखता है।", "equalizerBaseExplainer": "यह बेस इक्वलाइज़र है: यह उन सभी स्टेशन पर लागू होता है जिनकी अपनी सेटिंग नहीं है। किसी खास स्टेशन का अपना EQ उसकी प्लेबैक स्क्रीन से तय होता है और इस पर प्राथमिकता रखता है।",
"equalizerSaveAsPresetAction": "प्रीसेट के रूप में सहेजें", "equalizerSaveAsPresetAction": "प्रीसेट के रूप में सहेजें",
"equalizerResetToFlatAction": "समतल पर रीसेट करें",
"equalizerSavePresetConfirm": "सहेजें", "equalizerSavePresetConfirm": "सहेजें",
"equalizerSavePresetDialogTitle": "प्रीसेट के रूप में सहेजें", "equalizerSavePresetDialogTitle": "प्रीसेट के रूप में सहेजें",
"equalizerSavePresetEmptyNameError": "प्रीसेट के लिए एक नाम डालें।", "equalizerSavePresetEmptyNameError": "प्रीसेट के लिए एक नाम डालें।",
+1
View File
@@ -711,6 +711,7 @@
"equalizerActiveOutputLabel": "Output aktif", "equalizerActiveOutputLabel": "Output aktif",
"equalizerBaseExplainer": "Ini adalah equalizer dasar: berlaku untuk semua stasiun yang belum punya pengaturan sendiri. EQ khusus suatu stasiun diatur dari layar pemutarannya sendiri dan mengesampingkan yang ini.", "equalizerBaseExplainer": "Ini adalah equalizer dasar: berlaku untuk semua stasiun yang belum punya pengaturan sendiri. EQ khusus suatu stasiun diatur dari layar pemutarannya sendiri dan mengesampingkan yang ini.",
"equalizerSaveAsPresetAction": "Simpan sebagai Preset", "equalizerSaveAsPresetAction": "Simpan sebagai Preset",
"equalizerResetToFlatAction": "Setel ulang ke datar",
"equalizerSavePresetConfirm": "Simpan", "equalizerSavePresetConfirm": "Simpan",
"equalizerSavePresetDialogTitle": "Simpan sebagai Preset", "equalizerSavePresetDialogTitle": "Simpan sebagai Preset",
"equalizerSavePresetEmptyNameError": "Masukkan nama untuk Preset.", "equalizerSavePresetEmptyNameError": "Masukkan nama untuk Preset.",
+1
View File
@@ -711,6 +711,7 @@
"equalizerActiveOutputLabel": "Uscita attiva", "equalizerActiveOutputLabel": "Uscita attiva",
"equalizerBaseExplainer": "Questo è l'equalizzatore di base: si applica a tutte le emittenti senza un'impostazione propria. L'EQ di un'emittente specifica si imposta dalla sua schermata di riproduzione e ha la precedenza su questo.", "equalizerBaseExplainer": "Questo è l'equalizzatore di base: si applica a tutte le emittenti senza un'impostazione propria. L'EQ di un'emittente specifica si imposta dalla sua schermata di riproduzione e ha la precedenza su questo.",
"equalizerSaveAsPresetAction": "Salva come Preset", "equalizerSaveAsPresetAction": "Salva come Preset",
"equalizerResetToFlatAction": "Ripristina piatto",
"equalizerSavePresetConfirm": "Salva", "equalizerSavePresetConfirm": "Salva",
"equalizerSavePresetDialogTitle": "Salva come Preset", "equalizerSavePresetDialogTitle": "Salva come Preset",
"equalizerSavePresetEmptyNameError": "Inserisci un nome per il Preset.", "equalizerSavePresetEmptyNameError": "Inserisci un nome per il Preset.",
+1
View File
@@ -711,6 +711,7 @@
"equalizerActiveOutputLabel": "アクティブな出力", "equalizerActiveOutputLabel": "アクティブな出力",
"equalizerBaseExplainer": "これはベースイコライザーです。専用設定を持たないすべての局に適用されます。特定の局専用のEQはその局の再生画面から設定でき、こちらより優先されます。", "equalizerBaseExplainer": "これはベースイコライザーです。専用設定を持たないすべての局に適用されます。特定の局専用のEQはその局の再生画面から設定でき、こちらより優先されます。",
"equalizerSaveAsPresetAction": "プリセットとして保存", "equalizerSaveAsPresetAction": "プリセットとして保存",
"equalizerResetToFlatAction": "フラットにリセット",
"equalizerSavePresetConfirm": "保存", "equalizerSavePresetConfirm": "保存",
"equalizerSavePresetDialogTitle": "プリセットとして保存", "equalizerSavePresetDialogTitle": "プリセットとして保存",
"equalizerSavePresetEmptyNameError": "プリセットの名前を入力してください。", "equalizerSavePresetEmptyNameError": "プリセットの名前を入力してください。",
+1
View File
@@ -711,6 +711,7 @@
"equalizerActiveOutputLabel": "Saída ativa", "equalizerActiveOutputLabel": "Saída ativa",
"equalizerBaseExplainer": "Este é o equalizador base: aplica-se a todas as estações sem ajuste próprio. O EQ próprio de uma estação é definido na sua própria tela de reprodução e tem prioridade sobre este.", "equalizerBaseExplainer": "Este é o equalizador base: aplica-se a todas as estações sem ajuste próprio. O EQ próprio de uma estação é definido na sua própria tela de reprodução e tem prioridade sobre este.",
"equalizerSaveAsPresetAction": "Salvar como Preset", "equalizerSaveAsPresetAction": "Salvar como Preset",
"equalizerResetToFlatAction": "Repor para plano",
"equalizerSavePresetConfirm": "Salvar", "equalizerSavePresetConfirm": "Salvar",
"equalizerSavePresetDialogTitle": "Salvar como Preset", "equalizerSavePresetDialogTitle": "Salvar como Preset",
"equalizerSavePresetEmptyNameError": "Digite um nome para o Preset.", "equalizerSavePresetEmptyNameError": "Digite um nome para o Preset.",
+1
View File
@@ -711,6 +711,7 @@
"equalizerActiveOutputLabel": "Активный выход", "equalizerActiveOutputLabel": "Активный выход",
"equalizerBaseExplainer": "Это базовый эквалайзер: он применяется ко всем станциям без собственной настройки. Свой EQ конкретной станции задаётся на экране её воспроизведения и имеет приоритет над этим.", "equalizerBaseExplainer": "Это базовый эквалайзер: он применяется ко всем станциям без собственной настройки. Свой EQ конкретной станции задаётся на экране её воспроизведения и имеет приоритет над этим.",
"equalizerSaveAsPresetAction": "Сохранить как пресет", "equalizerSaveAsPresetAction": "Сохранить как пресет",
"equalizerResetToFlatAction": "Сбросить на плоский",
"equalizerSavePresetConfirm": "Сохранить", "equalizerSavePresetConfirm": "Сохранить",
"equalizerSavePresetDialogTitle": "Сохранить как пресет", "equalizerSavePresetDialogTitle": "Сохранить как пресет",
"equalizerSavePresetEmptyNameError": "Введите название пресета.", "equalizerSavePresetEmptyNameError": "Введите название пресета.",
+1
View File
@@ -711,6 +711,7 @@
"equalizerActiveOutputLabel": "当前输出", "equalizerActiveOutputLabel": "当前输出",
"equalizerBaseExplainer": "这是基础均衡器:适用于所有没有专属设置的电台。某个电台的专属EQ可在其自己的播放界面中设置,并优先于此设置生效。", "equalizerBaseExplainer": "这是基础均衡器:适用于所有没有专属设置的电台。某个电台的专属EQ可在其自己的播放界面中设置,并优先于此设置生效。",
"equalizerSaveAsPresetAction": "保存为预设", "equalizerSaveAsPresetAction": "保存为预设",
"equalizerResetToFlatAction": "重置为平坦",
"equalizerSavePresetConfirm": "保存", "equalizerSavePresetConfirm": "保存",
"equalizerSavePresetDialogTitle": "保存为预设", "equalizerSavePresetDialogTitle": "保存为预设",
"equalizerSavePresetEmptyNameError": "请输入预设名称。", "equalizerSavePresetEmptyNameError": "请输入预设名称。",
+6
View File
@@ -538,6 +538,12 @@ abstract class AppLocalizations {
/// **'Guardar como preset'** /// **'Guardar como preset'**
String get equalizerSaveAsPresetAction; String get equalizerSaveAsPresetAction;
/// No description provided for @equalizerResetToFlatAction.
///
/// In es, this message translates to:
/// **'Restablecer a plano'**
String get equalizerResetToFlatAction;
/// No description provided for @equalizerSavePresetDialogTitle. /// No description provided for @equalizerSavePresetDialogTitle.
/// ///
/// In es, this message translates to: /// In es, this message translates to:
+3
View File
@@ -246,6 +246,9 @@ class AppLocalizationsAr extends AppLocalizations {
@override @override
String get equalizerSaveAsPresetAction => 'حفظ كإعداد مسبق'; String get equalizerSaveAsPresetAction => 'حفظ كإعداد مسبق';
@override
String get equalizerResetToFlatAction => 'إعادة الضبط إلى مسطح';
@override @override
String get equalizerSavePresetDialogTitle => 'حفظ كإعداد مسبق'; String get equalizerSavePresetDialogTitle => 'حفظ كإعداد مسبق';
+3
View File
@@ -250,6 +250,9 @@ class AppLocalizationsBn extends AppLocalizations {
@override @override
String get equalizerSaveAsPresetAction => 'প্রিসেট হিসেবে সংরক্ষণ করুন'; String get equalizerSaveAsPresetAction => 'প্রিসেট হিসেবে সংরক্ষণ করুন';
@override
String get equalizerResetToFlatAction => 'সমতলে পুনরায় সেট করুন';
@override @override
String get equalizerSavePresetDialogTitle => 'প্রিসেট হিসেবে সংরক্ষণ করুন'; String get equalizerSavePresetDialogTitle => 'প্রিসেট হিসেবে সংরক্ষণ করুন';
+3
View File
@@ -250,6 +250,9 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get equalizerSaveAsPresetAction => 'Als Preset speichern'; String get equalizerSaveAsPresetAction => 'Als Preset speichern';
@override
String get equalizerResetToFlatAction => 'Auf neutral zurücksetzen';
@override @override
String get equalizerSavePresetDialogTitle => 'Als Preset speichern'; String get equalizerSavePresetDialogTitle => 'Als Preset speichern';
+3
View File
@@ -247,6 +247,9 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get equalizerSaveAsPresetAction => 'Save as preset'; String get equalizerSaveAsPresetAction => 'Save as preset';
@override
String get equalizerResetToFlatAction => 'Reset to flat';
@override @override
String get equalizerSavePresetDialogTitle => 'Save as preset'; String get equalizerSavePresetDialogTitle => 'Save as preset';
+3
View File
@@ -249,6 +249,9 @@ class AppLocalizationsEs extends AppLocalizations {
@override @override
String get equalizerSaveAsPresetAction => 'Guardar como preset'; String get equalizerSaveAsPresetAction => 'Guardar como preset';
@override
String get equalizerResetToFlatAction => 'Restablecer a plano';
@override @override
String get equalizerSavePresetDialogTitle => 'Guardar como preset'; String get equalizerSavePresetDialogTitle => 'Guardar como preset';
+3
View File
@@ -251,6 +251,9 @@ class AppLocalizationsFr extends AppLocalizations {
@override @override
String get equalizerSaveAsPresetAction => 'Enregistrer comme Preset'; String get equalizerSaveAsPresetAction => 'Enregistrer comme Preset';
@override
String get equalizerResetToFlatAction => 'Réinitialiser à plat';
@override @override
String get equalizerSavePresetDialogTitle => 'Enregistrer comme Preset'; String get equalizerSavePresetDialogTitle => 'Enregistrer comme Preset';
+3
View File
@@ -249,6 +249,9 @@ class AppLocalizationsHi extends AppLocalizations {
@override @override
String get equalizerSaveAsPresetAction => 'प्रीसेट के रूप में सहेजें'; String get equalizerSaveAsPresetAction => 'प्रीसेट के रूप में सहेजें';
@override
String get equalizerResetToFlatAction => 'समतल पर रीसेट करें';
@override @override
String get equalizerSavePresetDialogTitle => 'प्रीसेट के रूप में सहेजें'; String get equalizerSavePresetDialogTitle => 'प्रीसेट के रूप में सहेजें';
+3
View File
@@ -249,6 +249,9 @@ class AppLocalizationsId extends AppLocalizations {
@override @override
String get equalizerSaveAsPresetAction => 'Simpan sebagai Preset'; String get equalizerSaveAsPresetAction => 'Simpan sebagai Preset';
@override
String get equalizerResetToFlatAction => 'Setel ulang ke datar';
@override @override
String get equalizerSavePresetDialogTitle => 'Simpan sebagai Preset'; String get equalizerSavePresetDialogTitle => 'Simpan sebagai Preset';
+3
View File
@@ -250,6 +250,9 @@ class AppLocalizationsIt extends AppLocalizations {
@override @override
String get equalizerSaveAsPresetAction => 'Salva come Preset'; String get equalizerSaveAsPresetAction => 'Salva come Preset';
@override
String get equalizerResetToFlatAction => 'Ripristina piatto';
@override @override
String get equalizerSavePresetDialogTitle => 'Salva come Preset'; String get equalizerSavePresetDialogTitle => 'Salva come Preset';
+3
View File
@@ -239,6 +239,9 @@ class AppLocalizationsJa extends AppLocalizations {
@override @override
String get equalizerSaveAsPresetAction => 'プリセットとして保存'; String get equalizerSaveAsPresetAction => 'プリセットとして保存';
@override
String get equalizerResetToFlatAction => 'フラットにリセット';
@override @override
String get equalizerSavePresetDialogTitle => 'プリセットとして保存'; String get equalizerSavePresetDialogTitle => 'プリセットとして保存';
+3
View File
@@ -248,6 +248,9 @@ class AppLocalizationsPt extends AppLocalizations {
@override @override
String get equalizerSaveAsPresetAction => 'Salvar como Preset'; String get equalizerSaveAsPresetAction => 'Salvar como Preset';
@override
String get equalizerResetToFlatAction => 'Repor para plano';
@override @override
String get equalizerSavePresetDialogTitle => 'Salvar como Preset'; String get equalizerSavePresetDialogTitle => 'Salvar como Preset';
+3
View File
@@ -250,6 +250,9 @@ class AppLocalizationsRu extends AppLocalizations {
@override @override
String get equalizerSaveAsPresetAction => 'Сохранить как пресет'; String get equalizerSaveAsPresetAction => 'Сохранить как пресет';
@override
String get equalizerResetToFlatAction => 'Сбросить на плоский';
@override @override
String get equalizerSavePresetDialogTitle => 'Сохранить как пресет'; String get equalizerSavePresetDialogTitle => 'Сохранить как пресет';
+3
View File
@@ -238,6 +238,9 @@ class AppLocalizationsZh extends AppLocalizations {
@override @override
String get equalizerSaveAsPresetAction => '保存为预设'; String get equalizerSaveAsPresetAction => '保存为预设';
@override
String get equalizerResetToFlatAction => '重置为平坦';
@override @override
String get equalizerSavePresetDialogTitle => '保存为预设'; String get equalizerSavePresetDialogTitle => '保存为预设';
@@ -6,6 +6,7 @@ import 'package:provider/provider.dart';
import '../../estado/estado_ecualizador.dart'; import '../../estado/estado_ecualizador.dart';
import '../../estado/estado_radio.dart'; import '../../estado/estado_radio.dart';
import '../../l10n/gen/app_localizations.dart'; import '../../l10n/gen/app_localizations.dart';
import '../../modelos/preset_ecualizador.dart';
import '../../tema/pluriwave_theme.dart'; import '../../tema/pluriwave_theme.dart';
import '../../widgets/ecualizador_widget.dart'; import '../../widgets/ecualizador_widget.dart';
import '../../widgets/pluri_glass_surface.dart'; import '../../widgets/pluri_glass_surface.dart';
@@ -22,6 +23,15 @@ import '../../widgets/pluri_push_scaffold.dart';
/// base-vs-per-station explainer, the "Salida activa" summary row, the /// base-vs-per-station explainer, the "Salida activa" summary row, the
/// "Emisoras con ajuste propio" drill-down, and the "Guardar como preset" /// "Emisoras con ajuste propio" drill-down, and the "Guardar como preset"
/// custom-preset flow — see `_CuerpoEcualizador` below. /// custom-preset flow — see `_CuerpoEcualizador` below.
///
/// Audit 11.10/11.8 (t4 lines 570-602, item 19): the prototype draws 4
/// SEPARATE cards (explainer banner / band sliders / "Salida activa +
/// Guardar como preset + Restablecer a plano" / "Emisoras con ajuste
/// propio"), not one shared glass surface wrapping everything — and adds
/// a "Restablecer a plano" action the build never had. `_CuerpoEcualizador`
/// no longer wraps its children in one outer `PluriGlassSurface`; each
/// section now either already draws its own background
/// ([EcualizadorWidget], unchanged) or gained one here.
class PantallaAjustesEcualizador extends StatelessWidget { class PantallaAjustesEcualizador extends StatelessWidget {
const PantallaAjustesEcualizador({super.key}); const PantallaAjustesEcualizador({super.key});
@@ -70,67 +80,66 @@ class _CuerpoEcualizadorState extends State<_CuerpoEcualizador> {
emisoraActual != null && estado.emisoraActualEsFavorita; emisoraActual != null && estado.emisoraActualEsFavorita;
final usandoEqPropio = eq.emisoraActualTienePresetPropio; final usandoEqPropio = eq.emisoraActualTienePresetPropio;
return PluriGlassSurface( return Column(
child: Column( crossAxisAlignment: CrossAxisAlignment.stretch,
crossAxisAlignment: CrossAxisAlignment.stretch, children: [
children: [ SwitchListTile.adaptive(
contentPadding: EdgeInsets.zero,
title: Text(l10n.equalizerEnable),
subtitle: Text(
disponible
? l10n.equalizerRealtimeSubtitle
: l10n.equalizerPendingSubtitle,
),
value: eq.activo,
onChanged: eq.cambiarActivo,
),
if (mostrarModoPorEmisora) ...[
const SizedBox(height: 8),
SwitchListTile.adaptive( SwitchListTile.adaptive(
contentPadding: EdgeInsets.zero, contentPadding: EdgeInsets.zero,
title: Text(l10n.equalizerEnable), title: Text(l10n.equalizerPerStationTitle),
subtitle: Text( subtitle: Text(
disponible usandoEqPropio
? l10n.equalizerRealtimeSubtitle ? l10n.equalizerPerStationActive(emisoraActual.nombre)
: l10n.equalizerPendingSubtitle, : l10n.equalizerPerStationMain(emisoraActual.nombre),
),
value: eq.activo,
onChanged: eq.cambiarActivo,
),
if (mostrarModoPorEmisora) ...[
const SizedBox(height: 8),
SwitchListTile.adaptive(
contentPadding: EdgeInsets.zero,
title: Text(l10n.equalizerPerStationTitle),
subtitle: Text(
usandoEqPropio
? l10n.equalizerPerStationActive(emisoraActual.nombre)
: l10n.equalizerPerStationMain(emisoraActual.nombre),
),
value: usandoEqPropio,
onChanged:
(usarPropio) =>
eq.cambiarModoEmisoraActual(usarPropio: usarPropio),
),
],
const SizedBox(height: 12),
_BannerExplicacionBase(l10n: l10n),
const SizedBox(height: 8),
_FilaSalidaActiva(eq: eq, l10n: l10n),
const SizedBox(height: 4),
_FilaEmisorasConAjustePropio(eq: eq, estado: estado, l10n: l10n),
const SizedBox(height: 12),
PresetsEcualizadorWidget(
presetActual: eq.presetActual,
personalizados: eq.presetsPersonalizados,
onSeleccionar: (p) => eq.cambiarPreset(p),
),
const SizedBox(height: 12),
EcualizadorWidget(
preset: eq.presetActual,
habilitado: eq.activo,
onCambio: (p) => eq.cambiarPreset(p),
),
const SizedBox(height: 12),
Align(
alignment: Alignment.centerRight,
child: TextButton.icon(
key: const Key('eq-save-preset-action'),
onPressed: () => _abrirDialogoGuardarPreset(context, eq),
icon: const Icon(Icons.bookmark_add_outlined, size: 20),
label: Text(l10n.equalizerSaveAsPresetAction),
), ),
value: usandoEqPropio,
onChanged:
(usarPropio) =>
eq.cambiarModoEmisoraActual(usarPropio: usarPropio),
), ),
], ],
), const SizedBox(height: 12),
// CARD 1 (t4 line 571) — already its own tinted Container.
_BannerExplicacionBase(l10n: l10n),
const SizedBox(height: 16),
// Bare row, no card (t4 lines 573-578) — unchanged.
PresetsEcualizadorWidget(
presetActual: eq.presetActual,
personalizados: eq.presetsPersonalizados,
onSeleccionar: (p) => eq.cambiarPreset(p),
),
const SizedBox(height: 14),
// CARD 2 (t4 lines 580-590) — EcualizadorWidget already draws
// its own PluriGlassSurface (design ADR-5), unchanged here.
EcualizadorWidget(
preset: eq.presetActual,
habilitado: eq.activo,
onCambio: (p) => eq.cambiarPreset(p),
),
const SizedBox(height: 14),
// CARD 3 (t4 lines 592-598): Salida activa / Guardar como
// preset / Restablecer a plano, grouped.
_TarjetaSalidaYAcciones(
eq: eq,
l10n: l10n,
onGuardarPreset: () => _abrirDialogoGuardarPreset(context, eq),
),
const SizedBox(height: 14),
// CARD 4 (t4 lines 600-602) — its own PluriGlassSurface now.
_FilaEmisorasConAjustePropio(eq: eq, estado: estado, l10n: l10n),
],
); );
}, },
); );
@@ -204,7 +213,7 @@ class _FilaSalidaActiva extends StatelessWidget {
return Padding( return Padding(
key: const Key('eq-active-output-row'), key: const Key('eq-active-output-row'),
padding: const EdgeInsets.symmetric(vertical: 4), padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12),
child: Row( child: Row(
children: [ children: [
const Icon(Icons.speaker_group_rounded, size: 20), const Icon(Icons.speaker_group_rounded, size: 20),
@@ -230,9 +239,107 @@ class _FilaSalidaActiva extends StatelessWidget {
} }
} }
/// Audit 11.8 (t4 lines 592-598): groups "Salida activa", "Guardar como
/// preset" and the entirely-missing "Restablecer a plano" into ONE card —
/// the build previously drew the first as a bare row and the second as a
/// floating, right-aligned button at the bottom of the whole screen.
/// "Restablecer a plano" is not a new capability: `PresetEcualizador.flat`
/// is the SAME preset the "Plano" chip in [PresetsEcualizadorWidget]
/// already applies via `EstadoEcualizador.cambiarPreset` — this is just a
/// second, prototype-mandated entry point to it.
class _TarjetaSalidaYAcciones extends StatelessWidget {
const _TarjetaSalidaYAcciones({
required this.eq,
required this.l10n,
required this.onGuardarPreset,
});
final EstadoEcualizador eq;
final AppLocalizations l10n;
final VoidCallback onGuardarPreset;
@override
Widget build(BuildContext context) {
return PluriGlassSurface(
key: const Key('eq-card-output-actions'),
borderRadius: BorderRadius.circular(context.pluriTokens.radiusMd),
padding: EdgeInsets.zero,
child: Column(
children: [
_FilaSalidaActiva(eq: eq, l10n: l10n),
const Divider(height: 1),
_FilaAccionTarjeta(
key: const Key('eq-save-preset-action'),
icon: Icons.bookmark_add_outlined,
label: l10n.equalizerSaveAsPresetAction,
trailing: const Icon(Icons.chevron_right_rounded, size: 19),
onTap: onGuardarPreset,
),
const Divider(height: 1),
_FilaAccionTarjeta(
key: const Key('eq-reset-flat-action'),
icon: Icons.restart_alt_rounded,
label: l10n.equalizerResetToFlatAction,
onTap: () => eq.cambiarPreset(PresetEcualizador.flat),
),
],
),
);
}
}
/// One tappable icon+label row inside [_TarjetaSalidaYAcciones] — shared
/// shape for "Guardar como preset" and "Restablecer a plano" (t4 lines
/// 595, 597: identical row layout, only "Guardar..." has a trailing
/// chevron since it opens a dialog rather than acting immediately).
class _FilaAccionTarjeta extends StatelessWidget {
const _FilaAccionTarjeta({
super.key,
required this.icon,
required this.label,
required this.onTap,
this.trailing,
});
final IconData icon;
final String label;
final VoidCallback onTap;
final Widget? trailing;
@override
Widget build(BuildContext context) {
return Material(
type: MaterialType.transparency,
child: InkWell(
onTap: onTap,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 13),
child: Row(
children: [
Icon(icon, size: 21),
const SizedBox(width: 12),
Expanded(
child: Text(
label,
style: Theme.of(context).textTheme.bodyMedium,
),
),
if (trailing != null) trailing!,
],
),
),
),
);
}
}
/// "Emisoras con ajuste propio" drill-down row (spec `eq-custom-presets` /// "Emisoras con ajuste propio" drill-down row (spec `eq-custom-presets`
/// "Stations-With-Own-EQ Drill-Down"): sourced from the existing /// "Stations-With-Own-EQ Drill-Down"): sourced from the existing
/// `presetsPorEmisora` map, no new state. /// `presetsPorEmisora` map, no new state.
///
/// Audit 11.10 (t4 lines 600-602): CARD 4 — now wraps itself in its own
/// `PluriGlassSurface` instead of being one more row inside a shared
/// glass block.
class _FilaEmisorasConAjustePropio extends StatelessWidget { class _FilaEmisorasConAjustePropio extends StatelessWidget {
const _FilaEmisorasConAjustePropio({ const _FilaEmisorasConAjustePropio({
required this.eq, required this.eq,
@@ -247,48 +354,53 @@ class _FilaEmisorasConAjustePropio extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final uuids = eq.presetsPorEmisora.keys.toList(); final uuids = eq.presetsPorEmisora.keys.toList();
return Material( return PluriGlassSurface(
type: MaterialType.transparency, key: const Key('eq-card-stations-own-eq'),
child: InkWell( borderRadius: BorderRadius.circular(context.pluriTokens.radiusMd),
key: const Key('eq-stations-own-eq-row'), padding: EdgeInsets.zero,
borderRadius: BorderRadius.circular(context.pluriTokens.radiusSm), child: Material(
onTap: type: MaterialType.transparency,
() => PluriPushScaffold.push( child: InkWell(
context, key: const Key('eq-stations-own-eq-row'),
(_) => _PantallaEmisorasConAjustePropio( borderRadius: BorderRadius.circular(context.pluriTokens.radiusMd),
uuids: uuids, onTap:
nombrePorUuid: (uuid) => _resolverNombreEmisora(estado, uuid), () => PluriPushScaffold.push(
), context,
), (_) => _PantallaEmisorasConAjustePropio(
child: Padding( uuids: uuids,
padding: const EdgeInsets.symmetric(vertical: 8), nombrePorUuid: (uuid) => _resolverNombreEmisora(estado, uuid),
child: Row(
children: [
const Icon(Icons.tune_rounded, size: 20),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
l10n.equalizerStationsWithOwnEqTitle,
style: Theme.of(context).textTheme.bodyMedium,
),
Text(
l10n.equalizerStationsWithOwnEqSubtitle,
style: Theme.of(context).textTheme.bodySmall,
),
],
), ),
), ),
Text( child: Padding(
key: const Key('eq-stations-own-eq-count'), padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 13),
'${uuids.length}', child: Row(
style: Theme.of(context).textTheme.bodySmall, children: [
), const Icon(Icons.tune_rounded, size: 20),
const SizedBox(width: 4), const SizedBox(width: 12),
const Icon(Icons.chevron_right_rounded, size: 20), Expanded(
], child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
l10n.equalizerStationsWithOwnEqTitle,
style: Theme.of(context).textTheme.bodyMedium,
),
Text(
l10n.equalizerStationsWithOwnEqSubtitle,
style: Theme.of(context).textTheme.bodySmall,
),
],
),
),
Text(
key: const Key('eq-stations-own-eq-count'),
'${uuids.length}',
style: Theme.of(context).textTheme.bodySmall,
),
const SizedBox(width: 4),
const Icon(Icons.chevron_right_rounded, size: 20),
],
),
), ),
), ),
), ),
@@ -9,6 +9,8 @@ import 'package:pluriwave/modelos/dispositivo_audio.dart';
import 'package:pluriwave/modelos/emisora.dart'; import 'package:pluriwave/modelos/emisora.dart';
import 'package:pluriwave/modelos/preset_ecualizador.dart'; import 'package:pluriwave/modelos/preset_ecualizador.dart';
import 'package:pluriwave/pantallas/ajustes/pantalla_ajustes_ecualizador.dart'; import 'package:pluriwave/pantallas/ajustes/pantalla_ajustes_ecualizador.dart';
import 'package:pluriwave/widgets/ecualizador_widget.dart';
import 'package:pluriwave/widgets/pluri_glass_surface.dart';
import 'package:pluriwave/widgets/pluri_push_scaffold.dart'; import 'package:pluriwave/widgets/pluri_push_scaffold.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
@@ -249,6 +251,12 @@ void main() {
equals('2'), equals('2'),
); );
// Item 19's 4-card restructure made the page taller — the row now
// sits below the default 800x600 test viewport.
await tester.ensureVisible(
find.byKey(const Key('eq-stations-own-eq-row')),
);
await tester.pumpAndSettle();
await tester.tap(find.byKey(const Key('eq-stations-own-eq-row'))); await tester.tap(find.byKey(const Key('eq-stations-own-eq-row')));
await tester.pumpAndSettle(); await tester.pumpAndSettle();
@@ -270,6 +278,12 @@ void main() {
await tester.pumpWidget(buildScreen(estado)); await tester.pumpWidget(buildScreen(estado));
await tester.pumpAndSettle(); await tester.pumpAndSettle();
// Item 19's 4-card restructure made the page taller — the row now
// sits below the default 800x600 test viewport.
await tester.ensureVisible(
find.byKey(const Key('eq-stations-own-eq-row')),
);
await tester.pumpAndSettle();
await tester.tap(find.byKey(const Key('eq-stations-own-eq-row'))); await tester.tap(find.byKey(const Key('eq-stations-own-eq-row')));
await tester.pumpAndSettle(); await tester.pumpAndSettle();
@@ -340,6 +354,99 @@ void main() {
}, },
); );
testWidgets('visual fidelity (audit 11.10): the explainer banner and the '
'stations-with-own-EQ row are SEPARATE cards, not one shared glass '
'surface', (tester) async {
_suppressListTileInkAssertion();
final estado = await crearEstado();
addTearDown(estado.dispose);
await tester.pumpWidget(buildScreen(estado));
await tester.pumpAndSettle();
final explainerCard = find.ancestor(
of: find.byKey(const Key('eq-base-explainer-banner')),
matching: find.byType(PluriGlassSurface),
);
expect(
find.descendant(
of: explainerCard,
matching: find.byKey(const Key('eq-stations-own-eq-row')),
),
findsNothing,
reason:
't4 lines 571/600-602: the prototype draws these as 2 of 4 '
'separate cards, not one merged block',
);
// EcualizadorWidget already wraps itself in its own PluriGlassSurface
// (design ADR-5) — this stays true once the OUTER wrapper is gone.
expect(
find.descendant(
of: find.byType(EcualizadorWidget),
matching: find.byType(PluriGlassSurface),
),
findsOneWidget,
);
});
testWidgets(
'visual fidelity (audit 11.8): Salida activa, Guardar como preset and '
'Restablecer a plano are grouped into ONE card',
(tester) async {
_suppressListTileInkAssertion();
final estado = await crearEstado();
addTearDown(estado.dispose);
await tester.pumpWidget(buildScreen(estado));
await tester.pumpAndSettle();
final outputCard = find.ancestor(
of: find.byKey(const Key('eq-active-output-row')),
matching: find.byType(PluriGlassSurface),
);
expect(outputCard, findsOneWidget);
expect(
find.descendant(
of: outputCard,
matching: find.byKey(const Key('eq-save-preset-action')),
),
findsOneWidget,
reason: 't4 lines 592-598: one grouped card for all 3 rows',
);
expect(
find.descendant(
of: outputCard,
matching: find.byKey(const Key('eq-reset-flat-action')),
),
findsOneWidget,
);
},
);
testWidgets(
'visual fidelity (audit 11.8): tapping Restablecer a plano resets the '
'active preset to Flat',
(tester) async {
_suppressListTileInkAssertion();
final estado = await crearEstado();
addTearDown(estado.dispose);
await estado.ecualizador.cambiarPreset(PresetEcualizador.rock);
expect(estado.ecualizador.presetActual, PresetEcualizador.rock);
await tester.pumpWidget(buildScreen(estado));
await tester.pumpAndSettle();
await tester.ensureVisible(
find.byKey(const Key('eq-reset-flat-action')),
);
await tester.pumpAndSettle();
await tester.tap(find.byKey(const Key('eq-reset-flat-action')));
await tester.pumpAndSettle();
expect(estado.ecualizador.presetActual, PresetEcualizador.flat);
},
);
testWidgets( testWidgets(
'Guardar como preset with a whitespace-only name shows the same validation message', 'Guardar como preset with a whitespace-only name shows the same validation message',
(tester) async { (tester) async {