10 Commits
Author SHA1 Message Date
FreeTLab 42d35a2541 fix(buscar): filter pills, Idioma chip, results eyebrow, states screen
Audit 6.2 (t4:292-293): active filter pills are brand-teal tinted with
a 15px close glyph and radius 10 -- was Material's own Chip theming.

Audit 6.3 (t4:294-295): an "Idioma" entry chip is now always reachable
once a search is active -- there was no standalone entry point for
language filtering before (only bundled inside "Filtros"). Opens the
same existing filter sheet rather than a new idioma-only picker.

Audit 6.4 (t4:299): the results-count line now uses eyebrowLabel
styling -- was labelLarge (14/w800).

Audit 13.3/13.4 (t4:649-651): the search-results loading skeleton
gains the missing "BUSCANDO EMISORAS..." eyebrow, and its row gap is
now 4px, not 10.

Audit 13.5/13.6 (t4:657-668): a new purpose-built _TarjetaSinResultados
replaces PluriEmptyState ONLY in the search-no-results branch (that
shared widget is left untouched for its other unrelated call sites --
favorites empty state, the discovery grid). The title now quotes the
typed query ("Sin resultados para <<jazzz>>"), and the clear-filters
pill sits INSIDE the card. New ARB key searchNoResultsForQueryTitle.

Item 6.1 (compact 60px active-search header replacing the persistent
glass pill) is NOT implemented: existing tests confirmed the quality
(bitrate) filter has NO entry point anywhere in the app besides the
header's "Filtros" pill (added first via _abrirYSeleccionarCalidad
while a country filter is ALREADY active, i.e. mid active-search).
Hiding PluriRootHeader during active search would make quality
filtering unreachable -- a real capability regression the task's own
"presentation changes, capability does not" principle forbids.
Re-plumbing where that filter lives is a bigger change than this
visual-fidelity pass should make unilaterally.
2026-07-30 17:04:44 +02:00
FreeTLab f61b0b9163 fix(vacaciones): header Add action, info banner, dashed CTA, collapse
Audit 9b.1 (t4:446): a solid brand-teal "Add" header action -- the
prototype's own mid-page CTA (audit 9b.6, still present, now dashed)
is a SECOND, additional entry point, not a replacement.

Audit 9b.2 (t4:448): the teal explanatory banner ("alarms marked
pause-during-vacations won't ring...") is now ALWAYS visible -- never
rendered before. New ARB key vacationExplainerBanner, all 13 locales.

Audit 9b.6 (t4:487): the bottom CTA is now dashed-border with a
date_range glyph -- was a solid OutlinedButton with an add glyph.
Reuses the dashed-painter shape already established in
pantalla_favoritos.dart's custom-station CTA (audit 4.5), duplicated
rather than shared.

Audit 9b.7 (t4:489): "Rangos pasados" is now a collapsible row --
icon, title, count, chevron -- COLLAPSED by default, expanding on tap.
Was always fully expanded inline. Updated the pre-existing widget test
to tap-then-assert instead of asserting immediate visibility.

Item 9b.3 (the eyebrow literally reading "EN CURSO") is NOT
implemented: the eyebrow STYLING is already correct (audit 9b.4), and
the residual copy gap is a shared ARB string
(vacationSummaryActiveCountdown) also used compactly in
pantalla_alarmas.dart's vacation summary row -- diverging its wording
just for this screen's eyebrow, or forcing a shoutier tone into that
other compact usage, is not worth it for a trivial-rated copy nuance.
2026-07-30 16:53:04 +02:00
FreeTLab 36d7d5f692 fix(alarma-sonando): date line, additive snooze qualifier
Audit 9.4 (t4:419): "Lunes, 3 de agosto" now renders between the
schedule pill and the hero time -- purely additive, a new sibling
Text touching neither element. New formato_fechas.dart helper
fechaLargaConDiaSemana (locale-aware via DateFormat.MMMMEEEEd).

Audit 9.10 (t4:433): the highlighted snooze tile gains a "usual"
qualifier (new ARB key alarmSnoozeUsualLabel) alongside the original
flat label -- resolved DIFFERENTLY than its sibling 9.9 (permanently
rejected, id 2525): instead of splitting the flat
alarmSnoozeOptionLabel string into two differently-sized Text nodes
(which would make it vanish from the render tree the protected
dismiss-guard test locates via find.text), a SEPARATE small Text is
added alongside it. The original label stays a single, untouched Text
node, still inside the same FilledButton the guard taps.

Verified against the full ringing-screen test surface (34 tests
across 5 files, including the protected dismiss-guard file) -- all
green, dismiss-guard file reconfirmed byte-identical to main.
2026-07-30 16:45:29 +02:00
FreeTLab dcb415b3f8 fix(alarmas-editor): REPETIR eyebrow, grouped card, volume row
Audit 8.5 (t4:381): the "REPETIR" eyebrow above the weekday circles --
never rendered before. New ARB key alarmRepeatSectionLabel.

Audit 8.6 (t4:392-401): the station picker, volume, fade-in and
vacation toggle now share ONE bordered card with sangred divider lines
-- were four separately-spaced widgets. Restyled _CampoSelectorEmisora
as a flat nav row (icon/bold label/muted value/chevron, matching
FilaAjuste's own convention) so it fits inside the card instead of
drawing its own outlined InputDecoration chrome -- used by BOTH the
primary and fallback (Advanced) station fields. Snooze duration, "use
current station", and the Advanced section stay OUTSIDE the card,
exactly where they were (audit 8.8's own documented deliberate
extras). A transparent Material sits inside the card's coloured box so
the fade-in ListTile and the vacation SwitchListTile still paint their
ink/background correctly (the same class of constraint already
documented for PluriGlassSurface elsewhere in this codebase).

Audit 8.7 (t4:396): the volume row is now a compact 112px track with a
live percentage label -- was a bare full-width Slider with no visible
value. New ArtB key alarmVolumeLabel.

Item 8.8 (extra name/type-selector/snooze-selector/"use current
station"/Advanced fields) stays exactly as documented: deliberate,
required by the hard constraint that the editor keep its date field,
fallback-station picker and sound dropdown.
2026-07-30 16:42:04 +02:00
FreeTLab c7d137c82e fix(alarmas): New pill, banner layout, vacation pill, card recurrence
Audit 7.1 (t4:325): the "New" action is a solid brand-teal pill with a
plain add glyph -- was a tonal button with auto_awesome.

Audit 7.2 (t4:326-330): the next-alarm banner is warmCoral-tinted with
the Skip chip BESIDE the text on the same row -- was an opaque default
card with the skip action stacked below as an OutlinedButton.

Audit 7.3 (t4:332): the vacation row gains a trailing "d-d MON"
date-range pill for the active-or-next range, built from the existing
EstadoAlarmas.rangoVacacionesActivo/vacacionesProximas() accessors --
new formato_fechas.dart helper, no new state.

Audit 7.4 (t4:337-345): the alarm card now shows a recurrence label
next to the giant time (reusing the existing oneTimeOption/
dailyOption/weekdaysOption strings) and a themed station-icon slot
next to the station name. The real per-station favicon is NOT
rendered here -- same network-image hazard already documented for the
ringing screen's audit 9.2 (Emisora.favicon is a network URL;
Image.network hangs widget tests without a mocked HttpClient). The
custom switch shape (52x32/26px thumb) is also left as Switch.adaptive
-- a disclosed sub-gap, not a silent drop.

Item 7.6 (_AccesoDiagnostico, an Android-reliability debug row) is a
pre-approved addition not in the prototype -- informational only, no
action needed.
2026-07-30 16:30:20 +02:00
FreeTLab dcd8488874 fix(favoritos): header actions row, chip colours
Audit 4.1 (t4:216): the manage-groups (create_new_folder) and sort
(swap_vert) actions now live in PluriRootHeader's own actions slot as
two icon buttons -- were an ActionChip inside the chip strip plus a
PopupMenuButton sharing a Row with it. The prototype's own back arrow
stays absent (binding decision: this root keeps its bottom tab bar,
unlike the prototype's pushed-with-back-arrow shape) -- this closes
the remaining gap in what was a partial fix.

Audit 4.2 (t4:219-221): group filter chips are solid brand teal with
dark text when active, listSurface + a faint border when not -- was
Material's own ChoiceChip theming (electricMagenta@24% selected).

Updated pantalla_favoritos_test.dart's two "Manage lists" text finders
to locate the relocated action by key instead (the action is now an
icon-only IconButton with a tooltip, not a labelled chip).
2026-07-30 16:22:12 +02:00
FreeTLab 94b1e901d1 fix(paises): eyebrows outside the card, country count, header search
Audit 5.4 (t4:254/260): "Tus idiomas" and "Todos" eyebrows now sit
OUTSIDE any card at title-tier (20px) padding, styled with
eyebrowLabel -- were titleMedium w900 inside a PluriGlassSurface.
"Todos" also gains its missing total count ("{title} · {count}").

Audit 5.7 (t4:252): the header gained a real `search` action -- toggles
an inline TextField that filters the country list by name or ISO
code, not a decorative no-op button. New ARB key countriesSearchHint.

Item 5.6 (station count as a subtitle line, not a trailing widget) was
already fixed as an undocumented side effect of Tier3's row rebuild
(3303bd3) -- reconfirmed by direct read, no change needed here.
2026-07-30 16:18:44 +02:00
FreeTLab 533896b9fa fix(ecualizador): header switch, banner radius, preset chips, glow thumb
Audit 11.1 (t4:566): the master enable switch moves to
PluriPushScaffold's header actions -- was the body's first
SwitchListTile row. Its realtime/pending explainer subtitle stays
behind as a plain caption so no information is lost.

Audit 11.2 (t4:571): explainer banner radius is 16 (a local one-off,
matching neither of the 3 named tokens), not radiusSm's 14.

Audit 11.3 (t4:574-577): preset chips are solid brand-teal with dark
text when active, listSurface + a faint border when not -- was
Material's own ChoiceChip theming (primaryContainer/grey).

Audit 11.5 (t4:585): band sliders use a new 20x20 _GlowSliderThumbShape
(brand-teal blurred glow + solid thumb), replacing the Material
default round thumb.

Audit 11.7 (t4:584): the dB label is brand teal at 90% alpha, not
liveGreen -- a leftover wrong colour family 11.6's slider-only fix
never touched.

Updated 2 pre-existing tests (pantalla_ajustes_ecualizador_test.dart,
pantalla_ajustes_test.dart) to locate the enable switch by key instead
of by the "Enable equalizer" text it no longer renders next to.
2026-07-30 16:14:11 +02:00
FreeTLab b5b9829faa fix(ajustes): group eyebrow outside the card, per-row accent icons
Audit 10.2 (t4:511): GrupoAjustes's eyebrow now sits OUTSIDE the
PluriGlassSurface, at title-tier (20px) padding -- it used to share
the card's own 16px content padding.

Audit 10.5 (t4:514/526): FilaAjuste gains an optional iconColor.
Wired on the two rows the prototype actually accents: AUDIO's
Ecualizador (brand cyan) and STATIONS' Grupos de favoritos (warmCoral).

Item 10.10 (two extra AUDIO rows -- "Calidad de streaming" and
"Reproduccion sin interrupciones") is NOT implemented: neither concept
has any backing state or service anywhere in this app today. Building
them for real means inventing two new persisted settings, and a
genuine streaming-quality preference would plausibly need to touch
the protected servicio_audio.dart to actually affect playback --
out of scope for a visual-fidelity pass. Decorative rows that open
nothing would be dead UI, which is worse than leaving the gap
disclosed.
2026-07-30 16:05:28 +02:00
FreeTLab e036f99a61 fix(bienvenida,grabaciones): headline size, CTA radius, storage caption
Screen 14 / audit 14.3+14.7: welcome headline now 34px/ls-1.2 (was
headlineMedium's 28/ls-1.0), CTA restyled to a radius-18 rounded
rectangle instead of Material 3's default StadiumBorder (t4:696,715).

Screen 12 / audit 12.2+12.3: the recordings storage card now shows the
bold "X of Y used" headline ABOVE a 6px/radius-3 bar, followed by a
real folder-path + purge-policy caption below it -- was the bar first
with the "used" string as its only (small, generic) caption (t4:613).
New ARB key recordingsLibraryStorageFolderCaption, translated to all
13 locales.

Item 12.5 (folder/max-size settings living behind the header action
instead of inline on this screen) is a deliberate structural split
from WU15b; the audit itself scopes it out of this pass.
2026-07-30 16:00:30 +02:00
54 changed files with 2955 additions and 370 deletions
+9
View File
@@ -262,8 +262,11 @@
"searchCountryFilterLabel": "البلد",
"searchLanguageFilterLabel": "اللغة",
"searchMinQualityFilterLabel": "الحد الأدنى للجودة",
"searchLoadingStationsLabel": "جارٍ البحث عن محطات…",
"searchEmptyTitle": "ابحث عن محطة",
"searchNoResultsTitle": "لا توجد نتائج",
"searchNoResultsForQueryTitle": "لا توجد نتائج لـ «{query}»",
"@searchNoResultsForQueryTitle": {"placeholders": {"query": {}}},
"searchEmptySubtitle": "استخدم الشريط العلوي أو الشرائح لاكتشاف إشارات من كل العالم.",
"searchNoResultsSubtitle": "جرّب إزالة الفلاتر أو كتابة اسم آخر للعثور على إشارة نشطة.",
"countrySpain": "إسبانيا",
@@ -440,6 +443,7 @@
}
}
},
"alarmSnoozeUsualLabel": "المعتاد",
"saveFavoritesAlarmHint": "احفظ محطات في المفضلة لاستخدامها كمنبه موسيقي.",
"useCurrentStationAction": "استخدام المحطة الحالية",
"playDuringVacations": "الرنين أثناء الإجازات",
@@ -589,6 +593,8 @@
"days": {}
}
},
"alarmRepeatSectionLabel": "تكرار",
"alarmVolumeLabel": "مستوى الصوت",
"androidReliabilityTitle": "مراجعة موثوقية Android",
"closeAction": "إغلاق",
"customOption": "مخصص",
@@ -705,6 +711,7 @@
"alarmVolumeRisingStatus": "رفع مستوى الصوت تدريجيًا",
"countriesAllTitle": "كل الدول",
"countriesScreenTitle": "الدول",
"countriesSearchHint": "الدولة أو الرمز...",
"countriesYourLanguagesTitle": "لغاتك",
"customStationsAddCta": "إضافة محطة مخصصة",
"equalizerActiveOutputDefault": "مكبر صوت هذا الجهاز",
@@ -746,6 +753,7 @@
"recordingRenameEmptyError": "أدخل اسمًا",
"recordingRenameLabel": "الاسم",
"recordingsLibraryEmptySubtitle": "التسجيلات التي تحفظها ستظهر هنا.",
"recordingsLibraryStorageFolderCaption": "Music/PluriWave · يتم حذف الأقدم عند بلوغ الحد",
"recordingsLibraryEmptyTitle": "لا توجد تسجيلات بعد",
"recordingsLibrarySettingsTooltip": "إعدادات التسجيل",
"recordingsLibraryStorageCaption": "تم استخدام {usedMb} ميغابايت من أصل {totalMb} ميغابايت",
@@ -780,6 +788,7 @@
"times": {}
}
},
"vacationExplainerBanner": "خلال هذه الفترات، لن تُصدر المنبهات المُعلَّمة بـ «إيقاف أثناء الإجازة» صوتًا. أما المُعلَّمة بـ «تشغيل دائمًا» فلن تتأثر.",
"vacationNoActiveRangeHint": "لا توجد فترة إجازة نشطة الآن.",
"vacationPastSectionTitle": "الفترات السابقة",
"vacationRangesCount": "{count} فترة",
+9
View File
@@ -262,8 +262,11 @@
"searchCountryFilterLabel": "দেশ",
"searchLanguageFilterLabel": "ভাষা",
"searchMinQualityFilterLabel": "ন্যূনতম গুণমান",
"searchLoadingStationsLabel": "স্টেশন খোঁজা হচ্ছে…",
"searchEmptyTitle": "একটি স্টেশন খুঁজুন",
"searchNoResultsTitle": "কোনো ফলাফল নেই",
"searchNoResultsForQueryTitle": "\"{query}\"-এর জন্য কোনো ফলাফল নেই",
"@searchNoResultsForQueryTitle": {"placeholders": {"query": {}}},
"searchEmptySubtitle": "উপরের বার বা চিপ ব্যবহার করে সারা বিশ্বের সিগন্যাল আবিষ্কার করুন।",
"searchNoResultsSubtitle": "সক্রিয় সিগন্যাল পেতে ফিল্টার সরিয়ে বা অন্য নাম লিখে দেখুন।",
"countrySpain": "স্পেন",
@@ -440,6 +443,7 @@
}
}
},
"alarmSnoozeUsualLabel": "সাধারণ",
"saveFavoritesAlarmHint": "সুরেলা অ্যালার্ম হিসেবে ব্যবহার করতে প্রিয়তে স্টেশন সংরক্ষণ করুন।",
"useCurrentStationAction": "বর্তমান স্টেশন ব্যবহার করুন",
"playDuringVacations": "ছুটিতে বাজান",
@@ -589,6 +593,8 @@
"days": {}
}
},
"alarmRepeatSectionLabel": "পুনরাবৃত্তি",
"alarmVolumeLabel": "ভলিউম",
"androidReliabilityTitle": "Android নির্ভরযোগ্যতা দেখুন",
"closeAction": "বন্ধ করুন",
"customOption": "কাস্টম",
@@ -705,6 +711,7 @@
"alarmVolumeRisingStatus": "ভলিউম বাড়ছে",
"countriesAllTitle": "সব দেশ",
"countriesScreenTitle": "দেশসমূহ",
"countriesSearchHint": "দেশ বা কোড...",
"countriesYourLanguagesTitle": "আপনার ভাষাসমূহ",
"customStationsAddCta": "নিজস্ব স্টেশন যোগ করুন",
"equalizerActiveOutputDefault": "এই ডিভাইসের স্পিকার",
@@ -746,6 +753,7 @@
"recordingRenameEmptyError": "একটি নাম লিখুন",
"recordingRenameLabel": "নাম",
"recordingsLibraryEmptySubtitle": "আপনি যেসব রেকর্ডিং সংরক্ষণ করবেন তা এখানে দেখা যাবে।",
"recordingsLibraryStorageFolderCaption": "Music/PluriWave · সীমা পৌঁছালে সবচেয়ে পুরনোগুলো মুছে যায়",
"recordingsLibraryEmptyTitle": "এখনও কোনো রেকর্ডিং নেই",
"recordingsLibrarySettingsTooltip": "রেকর্ডিং সেটিংস",
"recordingsLibraryStorageCaption": "{totalMb} MB-এর মধ্যে {usedMb} MB ব্যবহৃত",
@@ -780,6 +788,7 @@
"times": {}
}
},
"vacationExplainerBanner": "এই সময়কালে \"ছুটিতে বিরতি\" হিসেবে চিহ্নিত অ্যালার্মগুলো বাজবে না। \"সবসময় বাজবে\" হিসেবে চিহ্নিত অ্যালার্মগুলো প্রভাবিত হবে না।",
"vacationNoActiveRangeHint": "এই মুহূর্তে কোনো সক্রিয় ছুটির সময়সীমা নেই।",
"vacationPastSectionTitle": "অতীত সময়সীমা",
"vacationRangesCount": "{count}টি সময়সীমা",
+9
View File
@@ -262,8 +262,11 @@
"searchCountryFilterLabel": "Land",
"searchLanguageFilterLabel": "Sprache",
"searchMinQualityFilterLabel": "Mindestqualität",
"searchLoadingStationsLabel": "SENDER WERDEN GESUCHT…",
"searchEmptyTitle": "Suche nach einem Sender",
"searchNoResultsTitle": "Keine Ergebnisse",
"searchNoResultsForQueryTitle": "Keine Ergebnisse für „{query}“",
"@searchNoResultsForQueryTitle": {"placeholders": {"query": {}}},
"searchEmptySubtitle": "Nutze die obere Leiste oder die Chips, um Sender aus aller Welt zu entdecken.",
"searchNoResultsSubtitle": "Versuche, Filter zu entfernen oder einen anderen Namen einzugeben, um einen aktiven Sender zu finden.",
"countrySpain": "Spanien",
@@ -440,6 +443,7 @@
}
}
},
"alarmSnoozeUsualLabel": "üblich",
"saveFavoritesAlarmHint": "Speichere Sender in Favoriten, um sie als musikalischen Alarm zu verwenden.",
"useCurrentStationAction": "Aktuellen Sender verwenden",
"playDuringVacations": "Während der Ferien läuten",
@@ -589,6 +593,8 @@
"days": {}
}
},
"alarmRepeatSectionLabel": "WIEDERHOLEN",
"alarmVolumeLabel": "Lautstärke",
"androidReliabilityTitle": "Android-Zuverlässigkeit prüfen",
"closeAction": "Schließen",
"customOption": "Benutzerdefiniert",
@@ -705,6 +711,7 @@
"alarmVolumeRisingStatus": "Lautstärke wird erhöht",
"countriesAllTitle": "Alle Länder",
"countriesScreenTitle": "Länder",
"countriesSearchHint": "Land oder Code...",
"countriesYourLanguagesTitle": "Deine Sprachen",
"customStationsAddCta": "Benutzerdefinierten Sender hinzufügen",
"equalizerActiveOutputDefault": "Der Lautsprecher dieses Geräts",
@@ -746,6 +753,7 @@
"recordingRenameEmptyError": "Gib einen Namen ein",
"recordingRenameLabel": "Name",
"recordingsLibraryEmptySubtitle": "Aufnahmen, die du speicherst, erscheinen hier.",
"recordingsLibraryStorageFolderCaption": "Music/PluriWave · älteste werden bei Erreichen des Limits gelöscht",
"recordingsLibraryEmptyTitle": "Noch keine Aufnahmen",
"recordingsLibrarySettingsTooltip": "Aufnahmeeinstellungen",
"recordingsLibraryStorageCaption": "{usedMb} MB von {totalMb} MB belegt",
@@ -780,6 +788,7 @@
"times": {}
}
},
"vacationExplainerBanner": "Während dieser Zeiträume klingeln Alarme mit der Markierung \"im Urlaub pausieren\" nicht. Als \"immer klingeln\" markierte Alarme sind davon nicht betroffen.",
"vacationNoActiveRangeHint": "Momentan ist kein aktiver Urlaubszeitraum vorhanden.",
"vacationPastSectionTitle": "Vergangene Zeiträume",
"vacationRangesCount": "{count} Zeiträume",
+9
View File
@@ -233,6 +233,7 @@
}
}
},
"recordingsLibraryStorageFolderCaption": "Music/PluriWave · purges oldest at limit",
"recordingsLibraryEmptyTitle": "No recordings yet",
"recordingsLibraryEmptySubtitle": "Recordings you save will appear here.",
"recordingActionRename": "Rename",
@@ -315,13 +316,17 @@
"searchCountryFilterLabel": "Country",
"searchLanguageFilterLabel": "Language",
"searchMinQualityFilterLabel": "Minimum quality",
"searchLoadingStationsLabel": "SEARCHING FOR STATIONS…",
"searchEmptyTitle": "Search for a station",
"searchNoResultsTitle": "No results",
"searchNoResultsForQueryTitle": "No results for \"{query}\"",
"@searchNoResultsForQueryTitle": {"placeholders": {"query": {}}},
"searchEmptySubtitle": "Use the top bar or chips to discover stations from around the world.",
"searchNoResultsSubtitle": "Try removing filters or typing another name to find an active station.",
"searchResultsCount": "{count, plural, =1{1 result} other{{count} results}}",
"searchClearFiltersAction": "{count, plural, =1{Clear filter} other{Clear {count} filters}}",
"countriesScreenTitle": "Countries",
"countriesSearchHint": "Country or code...",
"countriesYourLanguagesTitle": "Your languages",
"countriesAllTitle": "All countries",
"radioCountriesError": "We couldn't load the countries.",
@@ -509,6 +514,7 @@
}
}
},
"alarmSnoozeUsualLabel": "usual",
"saveFavoritesAlarmHint": "Save stations in Favorites to use them as a music alarm.",
"useCurrentStationAction": "Use current station",
"playDuringVacations": "Play during vacations",
@@ -570,6 +576,7 @@
"vacationUpcomingSectionTitle": "SCHEDULED",
"vacationPastSectionTitle": "Past ranges",
"addVacationRangeCta": "Add range",
"vacationExplainerBanner": "During these ranges, alarms marked \"pause during vacations\" won't ring. Alarms marked \"always ring\" are not affected.",
"vacationNoActiveRangeHint": "No active vacation range right now.",
"vacationsDefaultName": "Vacation",
"newVacationRangeTitle": "New vacation range",
@@ -698,6 +705,8 @@
"days": {}
}
},
"alarmRepeatSectionLabel": "REPEAT",
"alarmVolumeLabel": "Volume",
"androidReliabilityTitle": "Review Android reliability",
"closeAction": "Close",
"customOption": "Custom",
+9
View File
@@ -233,6 +233,7 @@
}
}
},
"recordingsLibraryStorageFolderCaption": "Music/PluriWave · se borran las más antiguas al llegar al límite",
"recordingsLibraryEmptyTitle": "Todavía no hay grabaciones",
"recordingsLibraryEmptySubtitle": "Las grabaciones que guardes van a aparecer acá.",
"recordingActionRename": "Renombrar",
@@ -315,13 +316,17 @@
"searchCountryFilterLabel": "País",
"searchLanguageFilterLabel": "Idioma",
"searchMinQualityFilterLabel": "Calidad mínima",
"searchLoadingStationsLabel": "BUSCANDO EMISORAS…",
"searchEmptyTitle": "Buscá una emisora",
"searchNoResultsTitle": "Sin resultados",
"searchNoResultsForQueryTitle": "Sin resultados para «{query}»",
"@searchNoResultsForQueryTitle": {"placeholders": {"query": {}}},
"searchEmptySubtitle": "Usá la barra superior o los chips para descubrir señales de todo el mundo.",
"searchNoResultsSubtitle": "Probá quitar filtros o escribir otro nombre para encontrar una señal activa.",
"searchResultsCount": "{count, plural, =1{1 resultado} other{{count} resultados}}",
"searchClearFiltersAction": "{count, plural, =1{Quitar el filtro} other{Quitar los {count} filtros}}",
"countriesScreenTitle": "Países",
"countriesSearchHint": "País o código...",
"countriesYourLanguagesTitle": "Tus idiomas",
"countriesAllTitle": "Todos los países",
"radioCountriesError": "No pudimos cargar los países.",
@@ -509,6 +514,7 @@
}
}
},
"alarmSnoozeUsualLabel": "habitual",
"saveFavoritesAlarmHint": "Guardá emisoras en Favoritos para usarlas como alarma musical.",
"useCurrentStationAction": "Usar emisora actual",
"playDuringVacations": "Sonar durante vacaciones",
@@ -570,6 +576,7 @@
"vacationUpcomingSectionTitle": "PROGRAMADOS",
"vacationPastSectionTitle": "Rangos pasados",
"addVacationRangeCta": "Añadir rango",
"vacationExplainerBanner": "Durante estos rangos no suenan las alarmas marcadas como \"pausar en vacaciones\". Las marcadas como \"sonar siempre\" no se ven afectadas.",
"vacationNoActiveRangeHint": "No hay un rango de vacaciones activo ahora mismo.",
"vacationsDefaultName": "Vacaciones",
"newVacationRangeTitle": "Nuevo rango de vacaciones",
@@ -654,6 +661,8 @@
"@alarmScheduleOnce": {"placeholders": {"date": {}}},
"alarmScheduleWeekdays": "Días: {days}",
"@alarmScheduleWeekdays": {"placeholders": {"days": {}}},
"alarmRepeatSectionLabel": "REPETIR",
"alarmVolumeLabel": "Volumen",
"androidReliabilityTitle": "Revisar fiabilidad Android",
"closeAction": "Cerrar",
"customOption": "Personalizada",
+9
View File
@@ -262,8 +262,11 @@
"searchCountryFilterLabel": "Pays",
"searchLanguageFilterLabel": "Langue",
"searchMinQualityFilterLabel": "Qualité minimale",
"searchLoadingStationsLabel": "RECHERCHE DE STATIONS…",
"searchEmptyTitle": "Recherchez une station",
"searchNoResultsTitle": "Aucun résultat",
"searchNoResultsForQueryTitle": "Aucun résultat pour « {query} »",
"@searchNoResultsForQueryTitle": {"placeholders": {"query": {}}},
"searchEmptySubtitle": "Utilisez la barre du haut ou les pastilles pour découvrir des stations du monde entier.",
"searchNoResultsSubtitle": "Essayez de retirer des filtres ou de saisir un autre nom pour trouver une station active.",
"countrySpain": "Espagne",
@@ -440,6 +443,7 @@
}
}
},
"alarmSnoozeUsualLabel": "habituel",
"saveFavoritesAlarmHint": "Enregistrez des stations dans les Favoris pour les utiliser comme alarme musicale.",
"useCurrentStationAction": "Utiliser la station actuelle",
"playDuringVacations": "Sonner pendant les vacances",
@@ -589,6 +593,8 @@
"days": {}
}
},
"alarmRepeatSectionLabel": "RÉPÉTER",
"alarmVolumeLabel": "Volume",
"androidReliabilityTitle": "Vérifier la fiabilité Android",
"closeAction": "Fermer",
"customOption": "Personnalisée",
@@ -705,6 +711,7 @@
"alarmVolumeRisingStatus": "Augmentation du volume",
"countriesAllTitle": "Tous les pays",
"countriesScreenTitle": "Pays",
"countriesSearchHint": "Pays ou code...",
"countriesYourLanguagesTitle": "Vos langues",
"customStationsAddCta": "Ajouter une station personnalisée",
"equalizerActiveOutputDefault": "Le haut-parleur de cet appareil",
@@ -746,6 +753,7 @@
"recordingRenameEmptyError": "Saisissez un nom",
"recordingRenameLabel": "Nom",
"recordingsLibraryEmptySubtitle": "Les enregistrements que vous sauvegardez apparaîtront ici.",
"recordingsLibraryStorageFolderCaption": "Music/PluriWave · supprime les plus anciens à la limite",
"recordingsLibraryEmptyTitle": "Aucun enregistrement pour l'instant",
"recordingsLibrarySettingsTooltip": "Paramètres d'enregistrement",
"recordingsLibraryStorageCaption": "{usedMb} Mo sur {totalMb} Mo utilisés",
@@ -780,6 +788,7 @@
"times": {}
}
},
"vacationExplainerBanner": "Pendant ces periodes, les alarmes marquees \"pause pendant les vacances\" ne sonnent pas. Celles marquees \"toujours sonner\" ne sont pas concernees.",
"vacationNoActiveRangeHint": "Aucune période de vacances active pour le moment.",
"vacationPastSectionTitle": "Périodes passées",
"vacationRangesCount": "{count} périodes",
+9
View File
@@ -262,8 +262,11 @@
"searchCountryFilterLabel": "देश",
"searchLanguageFilterLabel": "भाषा",
"searchMinQualityFilterLabel": "न्यूनतम गुणवत्ता",
"searchLoadingStationsLabel": "स्टेशन खोजे जा रहे हैं…",
"searchEmptyTitle": "एक स्टेशन खोजें",
"searchNoResultsTitle": "कोई परिणाम नहीं",
"searchNoResultsForQueryTitle": "\"{query}\" के लिए कोई परिणाम नहीं",
"@searchNoResultsForQueryTitle": {"placeholders": {"query": {}}},
"searchEmptySubtitle": "दुनिया भर के सिग्नल खोजने के लिए ऊपर की बार या चिप्स इस्तेमाल करें।",
"searchNoResultsSubtitle": "सक्रिय सिग्नल पाने के लिए फ़िल्टर हटाएँ या कोई दूसरा नाम लिखें।",
"countrySpain": "स्पेन",
@@ -440,6 +443,7 @@
}
}
},
"alarmSnoozeUsualLabel": "सामान्य",
"saveFavoritesAlarmHint": "उन्हें संगीतमय अलार्म के रूप में इस्तेमाल करने के लिए स्टेशन पसंदीदा में सहेजें।",
"useCurrentStationAction": "वर्तमान स्टेशन इस्तेमाल करें",
"playDuringVacations": "छुट्टियों में बजाएँ",
@@ -589,6 +593,8 @@
"days": {}
}
},
"alarmRepeatSectionLabel": "दोहराएं",
"alarmVolumeLabel": "आवाज़",
"androidReliabilityTitle": "Android विश्वसनीयता जाँचें",
"closeAction": "बंद करें",
"customOption": "कस्टम",
@@ -705,6 +711,7 @@
"alarmVolumeRisingStatus": "आवाज़ बढ़ाई जा रही है",
"countriesAllTitle": "सभी देश",
"countriesScreenTitle": "देश",
"countriesSearchHint": "देश या कोड...",
"countriesYourLanguagesTitle": "आपकी भाषाएं",
"customStationsAddCta": "मनचाहा स्टेशन जोड़ें",
"equalizerActiveOutputDefault": "इस डिवाइस का स्पीकर",
@@ -746,6 +753,7 @@
"recordingRenameEmptyError": "एक नाम डालें",
"recordingRenameLabel": "नाम",
"recordingsLibraryEmptySubtitle": "आप जो रिकॉर्डिंग सहेजेंगे, वे यहां दिखाई देंगी।",
"recordingsLibraryStorageFolderCaption": "Music/PluriWave · सीमा पूरी होने पर सबसे पुरानी रिकॉर्डिंग हटा दी जाती हैं",
"recordingsLibraryEmptyTitle": "अभी तक कोई रिकॉर्डिंग नहीं",
"recordingsLibrarySettingsTooltip": "रिकॉर्डिंग सेटिंग्स",
"recordingsLibraryStorageCaption": "{totalMb} MB में से {usedMb} MB इस्तेमाल हुआ",
@@ -780,6 +788,7 @@
"times": {}
}
},
"vacationExplainerBanner": "इन अवधियों के दौरान \"छुट्टी में रोकें\" के रूप में चिह्नित अलार्म नहीं बजेंगे। \"हमेशा बजाएं\" के रूप में चिह्नित अलार्म पर कोई असर नहीं पड़ता।",
"vacationNoActiveRangeHint": "अभी कोई सक्रिय छुट्टी अवधि नहीं है।",
"vacationPastSectionTitle": "पिछली अवधियां",
"vacationRangesCount": "{count} अवधियां",
+9
View File
@@ -262,8 +262,11 @@
"searchCountryFilterLabel": "Negara",
"searchLanguageFilterLabel": "Bahasa",
"searchMinQualityFilterLabel": "Kualitas minimum",
"searchLoadingStationsLabel": "MENCARI STASIUN…",
"searchEmptyTitle": "Cari stasiun",
"searchNoResultsTitle": "Tidak ada hasil",
"searchNoResultsForQueryTitle": "Tidak ada hasil untuk \"{query}\"",
"@searchNoResultsForQueryTitle": {"placeholders": {"query": {}}},
"searchEmptySubtitle": "Gunakan bilah atas atau chip untuk menemukan sinyal dari seluruh dunia.",
"searchNoResultsSubtitle": "Coba hapus filter atau tulis nama lain untuk menemukan sinyal aktif.",
"countrySpain": "Spanyol",
@@ -440,6 +443,7 @@
}
}
},
"alarmSnoozeUsualLabel": "biasa",
"saveFavoritesAlarmHint": "Simpan stasiun ke Favorit untuk digunakan sebagai alarm musik.",
"useCurrentStationAction": "Gunakan stasiun saat ini",
"playDuringVacations": "Bunyi saat liburan",
@@ -589,6 +593,8 @@
"days": {}
}
},
"alarmRepeatSectionLabel": "ULANGI",
"alarmVolumeLabel": "Volume",
"androidReliabilityTitle": "Tinjau keandalan Android",
"closeAction": "Tutup",
"customOption": "Kustom",
@@ -705,6 +711,7 @@
"alarmVolumeRisingStatus": "Menaikkan volume",
"countriesAllTitle": "Semua negara",
"countriesScreenTitle": "Negara",
"countriesSearchHint": "Negara atau kode...",
"countriesYourLanguagesTitle": "Bahasa Anda",
"customStationsAddCta": "Tambahkan stasiun khusus",
"equalizerActiveOutputDefault": "Speaker perangkat ini",
@@ -746,6 +753,7 @@
"recordingRenameEmptyError": "Masukkan nama",
"recordingRenameLabel": "Nama",
"recordingsLibraryEmptySubtitle": "Rekaman yang Anda simpan akan muncul di sini.",
"recordingsLibraryStorageFolderCaption": "Music/PluriWave · yang terlama dihapus saat mencapai batas",
"recordingsLibraryEmptyTitle": "Belum ada rekaman",
"recordingsLibrarySettingsTooltip": "Pengaturan rekaman",
"recordingsLibraryStorageCaption": "{usedMb} MB dari {totalMb} MB terpakai",
@@ -780,6 +788,7 @@
"times": {}
}
},
"vacationExplainerBanner": "Selama periode ini, alarm yang ditandai \"jeda saat liburan\" tidak akan berbunyi. Alarm yang ditandai \"selalu berbunyi\" tidak terpengaruh.",
"vacationNoActiveRangeHint": "Tidak ada periode liburan aktif saat ini.",
"vacationPastSectionTitle": "Periode lampau",
"vacationRangesCount": "{count} periode",
+9
View File
@@ -262,8 +262,11 @@
"searchCountryFilterLabel": "Paese",
"searchLanguageFilterLabel": "Lingua",
"searchMinQualityFilterLabel": "Qualità minima",
"searchLoadingStationsLabel": "RICERCA STAZIONI IN CORSO…",
"searchEmptyTitle": "Cerca un'emittente",
"searchNoResultsTitle": "Nessun risultato",
"searchNoResultsForQueryTitle": "Nessun risultato per \"{query}\"",
"@searchNoResultsForQueryTitle": {"placeholders": {"query": {}}},
"searchEmptySubtitle": "Usa la barra in alto o i chip per scoprire emittenti da tutto il mondo.",
"searchNoResultsSubtitle": "Prova a rimuovere i filtri o a digitare un altro nome per trovare un'emittente attiva.",
"countrySpain": "Spagna",
@@ -440,6 +443,7 @@
}
}
},
"alarmSnoozeUsualLabel": "abituale",
"saveFavoritesAlarmHint": "Salva emittenti nei Preferiti per usarle come sveglia musicale.",
"useCurrentStationAction": "Usa emittente attuale",
"playDuringVacations": "Suona durante le vacanze",
@@ -589,6 +593,8 @@
"days": {}
}
},
"alarmRepeatSectionLabel": "RIPETI",
"alarmVolumeLabel": "Volume",
"androidReliabilityTitle": "Controlla affidabilità Android",
"closeAction": "Chiudi",
"customOption": "Personalizzata",
@@ -705,6 +711,7 @@
"alarmVolumeRisingStatus": "Aumento del volume",
"countriesAllTitle": "Tutti i paesi",
"countriesScreenTitle": "Paesi",
"countriesSearchHint": "Paese o codice...",
"countriesYourLanguagesTitle": "Le tue lingue",
"customStationsAddCta": "Aggiungi emittente personalizzata",
"equalizerActiveOutputDefault": "L'altoparlante di questo dispositivo",
@@ -746,6 +753,7 @@
"recordingRenameEmptyError": "Inserisci un nome",
"recordingRenameLabel": "Nome",
"recordingsLibraryEmptySubtitle": "Le registrazioni che salvi appariranno qui.",
"recordingsLibraryStorageFolderCaption": "Music/PluriWave · elimina le più vecchie al raggiungimento del limite",
"recordingsLibraryEmptyTitle": "Ancora nessuna registrazione",
"recordingsLibrarySettingsTooltip": "Impostazioni di registrazione",
"recordingsLibraryStorageCaption": "{usedMb} MB di {totalMb} MB usati",
@@ -780,6 +788,7 @@
"times": {}
}
},
"vacationExplainerBanner": "Durante questi periodi le sveglie contrassegnate come \"pausa in vacanza\" non suonano. Quelle contrassegnate come \"suona sempre\" non sono interessate.",
"vacationNoActiveRangeHint": "Nessun periodo di vacanza attivo al momento.",
"vacationPastSectionTitle": "Periodi passati",
"vacationRangesCount": "{count} periodi",
+9
View File
@@ -262,8 +262,11 @@
"searchCountryFilterLabel": "国",
"searchLanguageFilterLabel": "言語",
"searchMinQualityFilterLabel": "最低品質",
"searchLoadingStationsLabel": "局を検索中…",
"searchEmptyTitle": "局を検索",
"searchNoResultsTitle": "結果がありません",
"searchNoResultsForQueryTitle": "「{query}」の結果はありません",
"@searchNoResultsForQueryTitle": {"placeholders": {"query": {}}},
"searchEmptySubtitle": "上部バーやチップを使って、世界中の電波を見つけましょう。",
"searchNoResultsSubtitle": "有効な電波を見つけるには、フィルターを外すか別の名前を入力してください。",
"countrySpain": "スペイン",
@@ -440,6 +443,7 @@
}
}
},
"alarmSnoozeUsualLabel": "通常",
"saveFavoritesAlarmHint": "音楽アラームとして使うには、局をお気に入りに保存してください。",
"useCurrentStationAction": "現在の局を使用",
"playDuringVacations": "休暇中も鳴らす",
@@ -589,6 +593,8 @@
"days": {}
}
},
"alarmRepeatSectionLabel": "繰り返し",
"alarmVolumeLabel": "音量",
"androidReliabilityTitle": "Androidの信頼性を確認",
"closeAction": "閉じる",
"customOption": "カスタム",
@@ -705,6 +711,7 @@
"alarmVolumeRisingStatus": "音量を上げています",
"countriesAllTitle": "すべての国",
"countriesScreenTitle": "国",
"countriesSearchHint": "国名またはコード...",
"countriesYourLanguagesTitle": "あなたの言語",
"customStationsAddCta": "カスタム局を追加",
"equalizerActiveOutputDefault": "この端末のスピーカー",
@@ -746,6 +753,7 @@
"recordingRenameEmptyError": "名前を入力してください",
"recordingRenameLabel": "名前",
"recordingsLibraryEmptySubtitle": "保存した録音はここに表示されます。",
"recordingsLibraryStorageFolderCaption": "Music/PluriWave・上限に達すると古い順に削除されます",
"recordingsLibraryEmptyTitle": "まだ録音がありません",
"recordingsLibrarySettingsTooltip": "録音設定",
"recordingsLibraryStorageCaption": "{totalMb} MB 中 {usedMb} MB 使用済み",
@@ -780,6 +788,7 @@
"times": {}
}
},
"vacationExplainerBanner": "この期間中は「休暇中は一時停止」に設定したアラームは鳴りません。「常に鳴らす」に設定したアラームには影響しません。",
"vacationNoActiveRangeHint": "現在アクティブな休暇期間はありません。",
"vacationPastSectionTitle": "過去の期間",
"vacationRangesCount": "{count} 件の期間",
+9
View File
@@ -262,8 +262,11 @@
"searchCountryFilterLabel": "País",
"searchLanguageFilterLabel": "Idioma",
"searchMinQualityFilterLabel": "Qualidade mínima",
"searchLoadingStationsLabel": "PROCURANDO EMISSORAS…",
"searchEmptyTitle": "Busque uma estação",
"searchNoResultsTitle": "Sem resultados",
"searchNoResultsForQueryTitle": "Nenhum resultado para \"{query}\"",
"@searchNoResultsForQueryTitle": {"placeholders": {"query": {}}},
"searchEmptySubtitle": "Use a barra superior ou os chips para descobrir estações do mundo todo.",
"searchNoResultsSubtitle": "Tente remover filtros ou digitar outro nome para encontrar uma estação ativa.",
"countrySpain": "Espanha",
@@ -440,6 +443,7 @@
}
}
},
"alarmSnoozeUsualLabel": "usual",
"saveFavoritesAlarmHint": "Salve estações nos Favoritos para usá-las como alarme musical.",
"useCurrentStationAction": "Usar estação atual",
"playDuringVacations": "Tocar durante as férias",
@@ -589,6 +593,8 @@
"days": {}
}
},
"alarmRepeatSectionLabel": "REPETIÇÃO",
"alarmVolumeLabel": "Volume",
"androidReliabilityTitle": "Revisar confiabilidade Android",
"closeAction": "Fechar",
"customOption": "Personalizada",
@@ -705,6 +711,7 @@
"alarmVolumeRisingStatus": "Aumentando o volume",
"countriesAllTitle": "Todos os países",
"countriesScreenTitle": "Países",
"countriesSearchHint": "País ou código...",
"countriesYourLanguagesTitle": "Seus idiomas",
"customStationsAddCta": "Adicionar estação personalizada",
"equalizerActiveOutputDefault": "O alto-falante deste dispositivo",
@@ -746,6 +753,7 @@
"recordingRenameEmptyError": "Digite um nome",
"recordingRenameLabel": "Nome",
"recordingsLibraryEmptySubtitle": "As gravações que você salvar vão aparecer aqui.",
"recordingsLibraryStorageFolderCaption": "Music/PluriWave · apaga as mais antigas ao atingir o limite",
"recordingsLibraryEmptyTitle": "Ainda não há gravações",
"recordingsLibrarySettingsTooltip": "Configurações de gravação",
"recordingsLibraryStorageCaption": "{usedMb} MB de {totalMb} MB usados",
@@ -780,6 +788,7 @@
"times": {}
}
},
"vacationExplainerBanner": "Durante esses periodos, os alarmes marcados como \"pausar nas ferias\" nao tocam. Os marcados como \"tocar sempre\" nao sao afetados.",
"vacationNoActiveRangeHint": "Não há período de férias ativo no momento.",
"vacationPastSectionTitle": "Períodos passados",
"vacationRangesCount": "{count} períodos",
+9
View File
@@ -262,8 +262,11 @@
"searchCountryFilterLabel": "Страна",
"searchLanguageFilterLabel": "Язык",
"searchMinQualityFilterLabel": "Минимальное качество",
"searchLoadingStationsLabel": "ПОИСК СТАНЦИЙ…",
"searchEmptyTitle": "Найдите станцию",
"searchNoResultsTitle": "Нет результатов",
"searchNoResultsForQueryTitle": "Нет результатов по запросу «{query}»",
"@searchNoResultsForQueryTitle": {"placeholders": {"query": {}}},
"searchEmptySubtitle": "Используйте верхнюю строку или чипы, чтобы находить сигналы со всего мира.",
"searchNoResultsSubtitle": "Попробуйте убрать фильтры или ввести другое название, чтобы найти активный сигнал.",
"countrySpain": "Испания",
@@ -440,6 +443,7 @@
}
}
},
"alarmSnoozeUsualLabel": "обычно",
"saveFavoritesAlarmHint": "Сохраните станции в избранное, чтобы использовать их как музыкальный будильник.",
"useCurrentStationAction": "Использовать текущую станцию",
"playDuringVacations": "Звонить во время отпусков",
@@ -589,6 +593,8 @@
"days": {}
}
},
"alarmRepeatSectionLabel": "ПОВТОР",
"alarmVolumeLabel": "Громкость",
"androidReliabilityTitle": "Проверить надёжность Android",
"closeAction": "Закрыть",
"customOption": "Своя",
@@ -705,6 +711,7 @@
"alarmVolumeRisingStatus": "Громкость нарастает",
"countriesAllTitle": "Все страны",
"countriesScreenTitle": "Страны",
"countriesSearchHint": "Страна или код...",
"countriesYourLanguagesTitle": "Ваши языки",
"customStationsAddCta": "Добавить пользовательскую станцию",
"equalizerActiveOutputDefault": "Динамик этого устройства",
@@ -746,6 +753,7 @@
"recordingRenameEmptyError": "Введите название",
"recordingRenameLabel": "Название",
"recordingsLibraryEmptySubtitle": "Записи, которые вы сохраните, появятся здесь.",
"recordingsLibraryStorageFolderCaption": "Music/PluriWave · при достижении лимита удаляются старые записи",
"recordingsLibraryEmptyTitle": "Записей пока нет",
"recordingsLibrarySettingsTooltip": "Настройки записи",
"recordingsLibraryStorageCaption": "Использовано {usedMb} МБ из {totalMb} МБ",
@@ -780,6 +788,7 @@
"times": {}
}
},
"vacationExplainerBanner": "В эти периоды будильники с пометкой «пауза на время отпуска» не звонят. Будильники с пометкой «звонить всегда» это не затрагивает.",
"vacationNoActiveRangeHint": "Сейчас нет активного периода отпуска.",
"vacationPastSectionTitle": "Прошедшие периоды",
"vacationRangesCount": "{count} периодов",
+9
View File
@@ -262,8 +262,11 @@
"searchCountryFilterLabel": "国家/地区",
"searchLanguageFilterLabel": "语言",
"searchMinQualityFilterLabel": "最低质量",
"searchLoadingStationsLabel": "正在搜索电台…",
"searchEmptyTitle": "搜索电台",
"searchNoResultsTitle": "没有结果",
"searchNoResultsForQueryTitle": "未找到与\"{query}\"相关的结果",
"@searchNoResultsForQueryTitle": {"placeholders": {"query": {}}},
"searchEmptySubtitle": "使用顶部搜索栏或筛选标签,发现世界各地的电台信号。",
"searchNoResultsSubtitle": "尝试减少筛选条件,或换个名称搜索,找到正在播出的电台。",
"countrySpain": "西班牙",
@@ -440,6 +443,7 @@
}
}
},
"alarmSnoozeUsualLabel": "常用",
"saveFavoritesAlarmHint": "将电台保存到收藏,即可把它们用作音乐闹钟。",
"useCurrentStationAction": "使用当前电台",
"playDuringVacations": "假期期间响铃",
@@ -589,6 +593,8 @@
"days": {}
}
},
"alarmRepeatSectionLabel": "重复",
"alarmVolumeLabel": "音量",
"androidReliabilityTitle": "检查 Android 可靠性",
"closeAction": "关闭",
"customOption": "自定义",
@@ -705,6 +711,7 @@
"alarmVolumeRisingStatus": "音量渐强中",
"countriesAllTitle": "所有国家",
"countriesScreenTitle": "国家",
"countriesSearchHint": "国家或代码...",
"countriesYourLanguagesTitle": "你的语言",
"customStationsAddCta": "添加自定义电台",
"equalizerActiveOutputDefault": "此设备的扬声器",
@@ -746,6 +753,7 @@
"recordingRenameEmptyError": "请输入名称",
"recordingRenameLabel": "名称",
"recordingsLibraryEmptySubtitle": "你保存的录音会显示在这里。",
"recordingsLibraryStorageFolderCaption": "Music/PluriWave · 达到上限时会删除最旧的文件",
"recordingsLibraryEmptyTitle": "暂无录音",
"recordingsLibrarySettingsTooltip": "录音设置",
"recordingsLibraryStorageCaption": "已使用 {usedMb} MB / 共 {totalMb} MB",
@@ -780,6 +788,7 @@
"times": {}
}
},
"vacationExplainerBanner": "在这些时间段内,标记为“假期暂停”的闹钟不会响铃;标记为“始终响铃”的闹钟不受影响。",
"vacationNoActiveRangeHint": "目前没有正在生效的假期时间段。",
"vacationPastSectionTitle": "过往时间段",
"vacationRangesCount": "{count} 个时间段",
+18
View File
@@ -22,3 +22,21 @@ String diaMesLocalizado(String localeTag, DateTime fecha) =>
/// audit 9b.4 (t4:459), the small caption under the day-month.
String nombreDiaSemanaLocalizado(String localeTag, DateTime fecha) =>
DateFormat.EEEE(localeTag).format(fecha);
/// Full weekday + month + day, locale-aware (e.g. "lunes, 3 de agosto" for
/// `es`, "Monday, August 3" for `en`) — audit 9.4 (t4:419), the ringing
/// screen's date line between the schedule pill and the hero time.
String fechaLargaConDiaSemana(String localeTag, DateTime fecha) =>
DateFormat.MMMMEEEEd(localeTag).format(fecha);
/// Short "dd MON" range pill, locale-aware month abbreviation, uppercased
/// (e.g. "418 AGO" for `es`, "418 AUG" for `en`) — audit 7.3 (t4:332),
/// the vacation-row date-range pill on the Alarmas root. Always labels the
/// range with the END date's month: vacation ranges are short (days to a
/// couple of weeks), so a cross-month span is the rare case, and the
/// prototype itself only ever shows a single abbreviation.
String rangoFechasCorto(String localeTag, DateTime inicio, DateTime fin) {
final dia = DateFormat.d(localeTag);
final mes = DateFormat.MMM(localeTag).format(fin).toUpperCase();
return '${dia.format(inicio)}${dia.format(fin)} $mes';
}
+48
View File
@@ -880,6 +880,12 @@ abstract class AppLocalizations {
/// **'{usedMb} MB de {totalMb} MB usados'**
String recordingsLibraryStorageCaption(int usedMb, int totalMb);
/// No description provided for @recordingsLibraryStorageFolderCaption.
///
/// In es, this message translates to:
/// **'Music/PluriWave · se borran las más antiguas al llegar al límite'**
String get recordingsLibraryStorageFolderCaption;
/// No description provided for @recordingsLibraryEmptyTitle.
///
/// In es, this message translates to:
@@ -1186,6 +1192,12 @@ abstract class AppLocalizations {
/// **'Calidad mínima'**
String get searchMinQualityFilterLabel;
/// No description provided for @searchLoadingStationsLabel.
///
/// In es, this message translates to:
/// **'BUSCANDO EMISORAS…'**
String get searchLoadingStationsLabel;
/// No description provided for @searchEmptyTitle.
///
/// In es, this message translates to:
@@ -1198,6 +1210,12 @@ abstract class AppLocalizations {
/// **'Sin resultados'**
String get searchNoResultsTitle;
/// No description provided for @searchNoResultsForQueryTitle.
///
/// In es, this message translates to:
/// **'Sin resultados para «{query}»'**
String searchNoResultsForQueryTitle(Object query);
/// No description provided for @searchEmptySubtitle.
///
/// In es, this message translates to:
@@ -1228,6 +1246,12 @@ abstract class AppLocalizations {
/// **'Países'**
String get countriesScreenTitle;
/// No description provided for @countriesSearchHint.
///
/// In es, this message translates to:
/// **'País o código...'**
String get countriesSearchHint;
/// No description provided for @countriesYourLanguagesTitle.
///
/// In es, this message translates to:
@@ -1882,6 +1906,12 @@ abstract class AppLocalizations {
/// **'{minutes} min'**
String alarmSnoozeOptionLabel(int minutes);
/// No description provided for @alarmSnoozeUsualLabel.
///
/// In es, this message translates to:
/// **'habitual'**
String get alarmSnoozeUsualLabel;
/// No description provided for @saveFavoritesAlarmHint.
///
/// In es, this message translates to:
@@ -2024,6 +2054,12 @@ abstract class AppLocalizations {
/// **'Añadir rango'**
String get addVacationRangeCta;
/// No description provided for @vacationExplainerBanner.
///
/// In es, this message translates to:
/// **'Durante estos rangos no suenan las alarmas marcadas como \"pausar en vacaciones\". Las marcadas como \"sonar siempre\" no se ven afectadas.'**
String get vacationExplainerBanner;
/// No description provided for @vacationNoActiveRangeHint.
///
/// In es, this message translates to:
@@ -2462,6 +2498,18 @@ abstract class AppLocalizations {
/// **'Días: {days}'**
String alarmScheduleWeekdays(Object days);
/// No description provided for @alarmRepeatSectionLabel.
///
/// In es, this message translates to:
/// **'REPETIR'**
String get alarmRepeatSectionLabel;
/// No description provided for @alarmVolumeLabel.
///
/// In es, this message translates to:
/// **'Volumen'**
String get alarmVolumeLabel;
/// No description provided for @androidReliabilityTitle.
///
/// In es, this message translates to:
+28
View File
@@ -441,6 +441,10 @@ class AppLocalizationsAr extends AppLocalizations {
return 'تم استخدام $usedMb ميغابايت من أصل $totalMb ميغابايت';
}
@override
String get recordingsLibraryStorageFolderCaption =>
'Music/PluriWave · يتم حذف الأقدم عند بلوغ الحد';
@override
String get recordingsLibraryEmptyTitle => 'لا توجد تسجيلات بعد';
@@ -612,12 +616,20 @@ class AppLocalizationsAr extends AppLocalizations {
@override
String get searchMinQualityFilterLabel => 'الحد الأدنى للجودة';
@override
String get searchLoadingStationsLabel => 'جارٍ البحث عن محطات…';
@override
String get searchEmptyTitle => 'ابحث عن محطة';
@override
String get searchNoResultsTitle => 'لا توجد نتائج';
@override
String searchNoResultsForQueryTitle(Object query) {
return 'لا توجد نتائج لـ «$query»';
}
@override
String get searchEmptySubtitle =>
'استخدم الشريط العلوي أو الشرائح لاكتشاف إشارات من كل العالم.';
@@ -655,6 +667,9 @@ class AppLocalizationsAr extends AppLocalizations {
@override
String get countriesScreenTitle => 'الدول';
@override
String get countriesSearchHint => 'الدولة أو الرمز...';
@override
String get countriesYourLanguagesTitle => 'لغاتك';
@@ -1014,6 +1029,9 @@ class AppLocalizationsAr extends AppLocalizations {
return '$minutes د';
}
@override
String get alarmSnoozeUsualLabel => 'المعتاد';
@override
String get saveFavoritesAlarmHint =>
'احفظ محطات في المفضلة لاستخدامها كمنبه موسيقي.';
@@ -1102,6 +1120,10 @@ class AppLocalizationsAr extends AppLocalizations {
@override
String get addVacationRangeCta => 'إضافة فترة';
@override
String get vacationExplainerBanner =>
'خلال هذه الفترات، لن تُصدر المنبهات المُعلَّمة بـ «إيقاف أثناء الإجازة» صوتًا. أما المُعلَّمة بـ «تشغيل دائمًا» فلن تتأثر.';
@override
String get vacationNoActiveRangeHint => 'لا توجد فترة إجازة نشطة الآن.';
@@ -1351,6 +1373,12 @@ class AppLocalizationsAr extends AppLocalizations {
return 'الأيام: $days';
}
@override
String get alarmRepeatSectionLabel => 'تكرار';
@override
String get alarmVolumeLabel => 'مستوى الصوت';
@override
String get androidReliabilityTitle => 'مراجعة موثوقية Android';
+28
View File
@@ -446,6 +446,10 @@ class AppLocalizationsBn extends AppLocalizations {
return '$totalMb MB-এর মধ্যে $usedMb MB ব্যবহৃত';
}
@override
String get recordingsLibraryStorageFolderCaption =>
'Music/PluriWave · সীমা পৌঁছালে সবচেয়ে পুরনোগুলো মুছে যায়';
@override
String get recordingsLibraryEmptyTitle => 'এখনও কোনো রেকর্ডিং নেই';
@@ -619,12 +623,20 @@ class AppLocalizationsBn extends AppLocalizations {
@override
String get searchMinQualityFilterLabel => 'ন্যূনতম গুণমান';
@override
String get searchLoadingStationsLabel => 'স্টেশন খোঁজা হচ্ছে…';
@override
String get searchEmptyTitle => 'একটি স্টেশন খুঁজুন';
@override
String get searchNoResultsTitle => 'কোনো ফলাফল নেই';
@override
String searchNoResultsForQueryTitle(Object query) {
return '\"$query\"-এর জন্য কোনো ফলাফল নেই';
}
@override
String get searchEmptySubtitle =>
'উপরের বার বা চিপ ব্যবহার করে সারা বিশ্বের সিগন্যাল আবিষ্কার করুন।';
@@ -658,6 +670,9 @@ class AppLocalizationsBn extends AppLocalizations {
@override
String get countriesScreenTitle => 'দেশসমূহ';
@override
String get countriesSearchHint => 'দেশ বা কোড...';
@override
String get countriesYourLanguagesTitle => 'আপনার ভাষাসমূহ';
@@ -1019,6 +1034,9 @@ class AppLocalizationsBn extends AppLocalizations {
return '$minutes মিনিট';
}
@override
String get alarmSnoozeUsualLabel => 'সাধারণ';
@override
String get saveFavoritesAlarmHint =>
'সুরেলা অ্যালার্ম হিসেবে ব্যবহার করতে প্রিয়তে স্টেশন সংরক্ষণ করুন।';
@@ -1108,6 +1126,10 @@ class AppLocalizationsBn extends AppLocalizations {
@override
String get addVacationRangeCta => 'সময়সীমা যোগ করুন';
@override
String get vacationExplainerBanner =>
'এই সময়কালে \"ছুটিতে বিরতি\" হিসেবে চিহ্নিত অ্যালার্মগুলো বাজবে না। \"সবসময় বাজবে\" হিসেবে চিহ্নিত অ্যালার্মগুলো প্রভাবিত হবে না।';
@override
String get vacationNoActiveRangeHint =>
'এই মুহূর্তে কোনো সক্রিয় ছুটির সময়সীমা নেই।';
@@ -1360,6 +1382,12 @@ class AppLocalizationsBn extends AppLocalizations {
return 'দিন: $days';
}
@override
String get alarmRepeatSectionLabel => 'পুনরাবৃত্তি';
@override
String get alarmVolumeLabel => 'ভলিউম';
@override
String get androidReliabilityTitle => 'Android নির্ভরযোগ্যতা দেখুন';
+28
View File
@@ -449,6 +449,10 @@ class AppLocalizationsDe extends AppLocalizations {
return '$usedMb MB von $totalMb MB belegt';
}
@override
String get recordingsLibraryStorageFolderCaption =>
'Music/PluriWave · älteste werden bei Erreichen des Limits gelöscht';
@override
String get recordingsLibraryEmptyTitle => 'Noch keine Aufnahmen';
@@ -622,12 +626,20 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get searchMinQualityFilterLabel => 'Mindestqualität';
@override
String get searchLoadingStationsLabel => 'SENDER WERDEN GESUCHT…';
@override
String get searchEmptyTitle => 'Suche nach einem Sender';
@override
String get searchNoResultsTitle => 'Keine Ergebnisse';
@override
String searchNoResultsForQueryTitle(Object query) {
return 'Keine Ergebnisse für „$query';
}
@override
String get searchEmptySubtitle =>
'Nutze die obere Leiste oder die Chips, um Sender aus aller Welt zu entdecken.';
@@ -661,6 +673,9 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get countriesScreenTitle => 'Länder';
@override
String get countriesSearchHint => 'Land oder Code...';
@override
String get countriesYourLanguagesTitle => 'Deine Sprachen';
@@ -1022,6 +1037,9 @@ class AppLocalizationsDe extends AppLocalizations {
return '$minutes Min.';
}
@override
String get alarmSnoozeUsualLabel => 'üblich';
@override
String get saveFavoritesAlarmHint =>
'Speichere Sender in Favoriten, um sie als musikalischen Alarm zu verwenden.';
@@ -1110,6 +1128,10 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get addVacationRangeCta => 'Zeitraum hinzufügen';
@override
String get vacationExplainerBanner =>
'Während dieser Zeiträume klingeln Alarme mit der Markierung \"im Urlaub pausieren\" nicht. Als \"immer klingeln\" markierte Alarme sind davon nicht betroffen.';
@override
String get vacationNoActiveRangeHint =>
'Momentan ist kein aktiver Urlaubszeitraum vorhanden.';
@@ -1368,6 +1390,12 @@ class AppLocalizationsDe extends AppLocalizations {
return 'Tage: $days';
}
@override
String get alarmRepeatSectionLabel => 'WIEDERHOLEN';
@override
String get alarmVolumeLabel => 'Lautstärke';
@override
String get androidReliabilityTitle => 'Android-Zuverlässigkeit prüfen';
+28
View File
@@ -443,6 +443,10 @@ class AppLocalizationsEn extends AppLocalizations {
return '$usedMb MB of $totalMb MB used';
}
@override
String get recordingsLibraryStorageFolderCaption =>
'Music/PluriWave · purges oldest at limit';
@override
String get recordingsLibraryEmptyTitle => 'No recordings yet';
@@ -615,12 +619,20 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get searchMinQualityFilterLabel => 'Minimum quality';
@override
String get searchLoadingStationsLabel => 'SEARCHING FOR STATIONS…';
@override
String get searchEmptyTitle => 'Search for a station';
@override
String get searchNoResultsTitle => 'No results';
@override
String searchNoResultsForQueryTitle(Object query) {
return 'No results for \"$query\"';
}
@override
String get searchEmptySubtitle =>
'Use the top bar or chips to discover stations from around the world.';
@@ -654,6 +666,9 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get countriesScreenTitle => 'Countries';
@override
String get countriesSearchHint => 'Country or code...';
@override
String get countriesYourLanguagesTitle => 'Your languages';
@@ -1014,6 +1029,9 @@ class AppLocalizationsEn extends AppLocalizations {
return '$minutes min';
}
@override
String get alarmSnoozeUsualLabel => 'usual';
@override
String get saveFavoritesAlarmHint =>
'Save stations in Favorites to use them as a music alarm.';
@@ -1102,6 +1120,10 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get addVacationRangeCta => 'Add range';
@override
String get vacationExplainerBanner =>
'During these ranges, alarms marked \"pause during vacations\" won\'t ring. Alarms marked \"always ring\" are not affected.';
@override
String get vacationNoActiveRangeHint => 'No active vacation range right now.';
@@ -1352,6 +1374,12 @@ class AppLocalizationsEn extends AppLocalizations {
return 'Days: $days';
}
@override
String get alarmRepeatSectionLabel => 'REPEAT';
@override
String get alarmVolumeLabel => 'Volume';
@override
String get androidReliabilityTitle => 'Review Android reliability';
+28
View File
@@ -447,6 +447,10 @@ class AppLocalizationsEs extends AppLocalizations {
return '$usedMb MB de $totalMb MB usados';
}
@override
String get recordingsLibraryStorageFolderCaption =>
'Music/PluriWave · se borran las más antiguas al llegar al límite';
@override
String get recordingsLibraryEmptyTitle => 'Todavía no hay grabaciones';
@@ -620,12 +624,20 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String get searchMinQualityFilterLabel => 'Calidad mínima';
@override
String get searchLoadingStationsLabel => 'BUSCANDO EMISORAS…';
@override
String get searchEmptyTitle => 'Buscá una emisora';
@override
String get searchNoResultsTitle => 'Sin resultados';
@override
String searchNoResultsForQueryTitle(Object query) {
return 'Sin resultados para «$query»';
}
@override
String get searchEmptySubtitle =>
'Usá la barra superior o los chips para descubrir señales de todo el mundo.';
@@ -659,6 +671,9 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String get countriesScreenTitle => 'Países';
@override
String get countriesSearchHint => 'País o código...';
@override
String get countriesYourLanguagesTitle => 'Tus idiomas';
@@ -1019,6 +1034,9 @@ class AppLocalizationsEs extends AppLocalizations {
return '$minutes min';
}
@override
String get alarmSnoozeUsualLabel => 'habitual';
@override
String get saveFavoritesAlarmHint =>
'Guardá emisoras en Favoritos para usarlas como alarma musical.';
@@ -1107,6 +1125,10 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String get addVacationRangeCta => 'Añadir rango';
@override
String get vacationExplainerBanner =>
'Durante estos rangos no suenan las alarmas marcadas como \"pausar en vacaciones\". Las marcadas como \"sonar siempre\" no se ven afectadas.';
@override
String get vacationNoActiveRangeHint =>
'No hay un rango de vacaciones activo ahora mismo.';
@@ -1362,6 +1384,12 @@ class AppLocalizationsEs extends AppLocalizations {
return 'Días: $days';
}
@override
String get alarmRepeatSectionLabel => 'REPETIR';
@override
String get alarmVolumeLabel => 'Volumen';
@override
String get androidReliabilityTitle => 'Revisar fiabilidad Android';
+28
View File
@@ -451,6 +451,10 @@ class AppLocalizationsFr extends AppLocalizations {
return '$usedMb Mo sur $totalMb Mo utilisés';
}
@override
String get recordingsLibraryStorageFolderCaption =>
'Music/PluriWave · supprime les plus anciens à la limite';
@override
String get recordingsLibraryEmptyTitle =>
'Aucun enregistrement pour l\'instant';
@@ -624,12 +628,20 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get searchMinQualityFilterLabel => 'Qualité minimale';
@override
String get searchLoadingStationsLabel => 'RECHERCHE DE STATIONS…';
@override
String get searchEmptyTitle => 'Recherchez une station';
@override
String get searchNoResultsTitle => 'Aucun résultat';
@override
String searchNoResultsForQueryTitle(Object query) {
return 'Aucun résultat pour « $query »';
}
@override
String get searchEmptySubtitle =>
'Utilisez la barre du haut ou les pastilles pour découvrir des stations du monde entier.';
@@ -663,6 +675,9 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get countriesScreenTitle => 'Pays';
@override
String get countriesSearchHint => 'Pays ou code...';
@override
String get countriesYourLanguagesTitle => 'Vos langues';
@@ -1024,6 +1039,9 @@ class AppLocalizationsFr extends AppLocalizations {
return '$minutes min';
}
@override
String get alarmSnoozeUsualLabel => 'habituel';
@override
String get saveFavoritesAlarmHint =>
'Enregistrez des stations dans les Favoris pour les utiliser comme alarme musicale.';
@@ -1113,6 +1131,10 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get addVacationRangeCta => 'Ajouter une période';
@override
String get vacationExplainerBanner =>
'Pendant ces periodes, les alarmes marquees \"pause pendant les vacances\" ne sonnent pas. Celles marquees \"toujours sonner\" ne sont pas concernees.';
@override
String get vacationNoActiveRangeHint =>
'Aucune période de vacances active pour le moment.';
@@ -1372,6 +1394,12 @@ class AppLocalizationsFr extends AppLocalizations {
return 'Jours : $days';
}
@override
String get alarmRepeatSectionLabel => 'RÉPÉTER';
@override
String get alarmVolumeLabel => 'Volume';
@override
String get androidReliabilityTitle => 'Vérifier la fiabilité Android';
+28
View File
@@ -444,6 +444,10 @@ class AppLocalizationsHi extends AppLocalizations {
return '$totalMb MB में से $usedMb MB इस्तेमाल हुआ';
}
@override
String get recordingsLibraryStorageFolderCaption =>
'Music/PluriWave · सीमा पूरी होने पर सबसे पुरानी रिकॉर्डिंग हटा दी जाती हैं';
@override
String get recordingsLibraryEmptyTitle => 'अभी तक कोई रिकॉर्डिंग नहीं';
@@ -616,12 +620,20 @@ class AppLocalizationsHi extends AppLocalizations {
@override
String get searchMinQualityFilterLabel => 'न्यूनतम गुणवत्ता';
@override
String get searchLoadingStationsLabel => 'स्टेशन खोजे जा रहे हैं…';
@override
String get searchEmptyTitle => 'एक स्टेशन खोजें';
@override
String get searchNoResultsTitle => 'कोई परिणाम नहीं';
@override
String searchNoResultsForQueryTitle(Object query) {
return '\"$query\" के लिए कोई परिणाम नहीं';
}
@override
String get searchEmptySubtitle =>
'दुनिया भर के सिग्नल खोजने के लिए ऊपर की बार या चिप्स इस्तेमाल करें।';
@@ -655,6 +667,9 @@ class AppLocalizationsHi extends AppLocalizations {
@override
String get countriesScreenTitle => 'देश';
@override
String get countriesSearchHint => 'देश या कोड...';
@override
String get countriesYourLanguagesTitle => 'आपकी भाषाएं';
@@ -1015,6 +1030,9 @@ class AppLocalizationsHi extends AppLocalizations {
return '$minutes मिनट';
}
@override
String get alarmSnoozeUsualLabel => 'सामान्य';
@override
String get saveFavoritesAlarmHint =>
'उन्हें संगीतमय अलार्म के रूप में इस्तेमाल करने के लिए स्टेशन पसंदीदा में सहेजें।';
@@ -1103,6 +1121,10 @@ class AppLocalizationsHi extends AppLocalizations {
@override
String get addVacationRangeCta => 'अवधि जोड़ें';
@override
String get vacationExplainerBanner =>
'इन अवधियों के दौरान \"छुट्टी में रोकें\" के रूप में चिह्नित अलार्म नहीं बजेंगे। \"हमेशा बजाएं\" के रूप में चिह्नित अलार्म पर कोई असर नहीं पड़ता।';
@override
String get vacationNoActiveRangeHint => 'अभी कोई सक्रिय छुट्टी अवधि नहीं है।';
@@ -1355,6 +1377,12 @@ class AppLocalizationsHi extends AppLocalizations {
return 'दिन: $days';
}
@override
String get alarmRepeatSectionLabel => 'दोहराएं';
@override
String get alarmVolumeLabel => 'आवाज़';
@override
String get androidReliabilityTitle => 'Android विश्वसनीयता जाँचें';
+28
View File
@@ -444,6 +444,10 @@ class AppLocalizationsId extends AppLocalizations {
return '$usedMb MB dari $totalMb MB terpakai';
}
@override
String get recordingsLibraryStorageFolderCaption =>
'Music/PluriWave · yang terlama dihapus saat mencapai batas';
@override
String get recordingsLibraryEmptyTitle => 'Belum ada rekaman';
@@ -617,12 +621,20 @@ class AppLocalizationsId extends AppLocalizations {
@override
String get searchMinQualityFilterLabel => 'Kualitas minimum';
@override
String get searchLoadingStationsLabel => 'MENCARI STASIUN…';
@override
String get searchEmptyTitle => 'Cari stasiun';
@override
String get searchNoResultsTitle => 'Tidak ada hasil';
@override
String searchNoResultsForQueryTitle(Object query) {
return 'Tidak ada hasil untuk \"$query\"';
}
@override
String get searchEmptySubtitle =>
'Gunakan bilah atas atau chip untuk menemukan sinyal dari seluruh dunia.';
@@ -655,6 +667,9 @@ class AppLocalizationsId extends AppLocalizations {
@override
String get countriesScreenTitle => 'Negara';
@override
String get countriesSearchHint => 'Negara atau kode...';
@override
String get countriesYourLanguagesTitle => 'Bahasa Anda';
@@ -1018,6 +1033,9 @@ class AppLocalizationsId extends AppLocalizations {
return '$minutes mnt';
}
@override
String get alarmSnoozeUsualLabel => 'biasa';
@override
String get saveFavoritesAlarmHint =>
'Simpan stasiun ke Favorit untuk digunakan sebagai alarm musik.';
@@ -1107,6 +1125,10 @@ class AppLocalizationsId extends AppLocalizations {
@override
String get addVacationRangeCta => 'Tambahkan periode';
@override
String get vacationExplainerBanner =>
'Selama periode ini, alarm yang ditandai \"jeda saat liburan\" tidak akan berbunyi. Alarm yang ditandai \"selalu berbunyi\" tidak terpengaruh.';
@override
String get vacationNoActiveRangeHint =>
'Tidak ada periode liburan aktif saat ini.';
@@ -1363,6 +1385,12 @@ class AppLocalizationsId extends AppLocalizations {
return 'Hari: $days';
}
@override
String get alarmRepeatSectionLabel => 'ULANGI';
@override
String get alarmVolumeLabel => 'Volume';
@override
String get androidReliabilityTitle => 'Tinjau keandalan Android';
+28
View File
@@ -448,6 +448,10 @@ class AppLocalizationsIt extends AppLocalizations {
return '$usedMb MB di $totalMb MB usati';
}
@override
String get recordingsLibraryStorageFolderCaption =>
'Music/PluriWave · elimina le più vecchie al raggiungimento del limite';
@override
String get recordingsLibraryEmptyTitle => 'Ancora nessuna registrazione';
@@ -622,12 +626,20 @@ class AppLocalizationsIt extends AppLocalizations {
@override
String get searchMinQualityFilterLabel => 'Qualità minima';
@override
String get searchLoadingStationsLabel => 'RICERCA STAZIONI IN CORSO…';
@override
String get searchEmptyTitle => 'Cerca un\'emittente';
@override
String get searchNoResultsTitle => 'Nessun risultato';
@override
String searchNoResultsForQueryTitle(Object query) {
return 'Nessun risultato per \"$query\"';
}
@override
String get searchEmptySubtitle =>
'Usa la barra in alto o i chip per scoprire emittenti da tutto il mondo.';
@@ -661,6 +673,9 @@ class AppLocalizationsIt extends AppLocalizations {
@override
String get countriesScreenTitle => 'Paesi';
@override
String get countriesSearchHint => 'Paese o codice...';
@override
String get countriesYourLanguagesTitle => 'Le tue lingue';
@@ -1023,6 +1038,9 @@ class AppLocalizationsIt extends AppLocalizations {
return '$minutes min';
}
@override
String get alarmSnoozeUsualLabel => 'abituale';
@override
String get saveFavoritesAlarmHint =>
'Salva emittenti nei Preferiti per usarle come sveglia musicale.';
@@ -1112,6 +1130,10 @@ class AppLocalizationsIt extends AppLocalizations {
@override
String get addVacationRangeCta => 'Aggiungi periodo';
@override
String get vacationExplainerBanner =>
'Durante questi periodi le sveglie contrassegnate come \"pausa in vacanza\" non suonano. Quelle contrassegnate come \"suona sempre\" non sono interessate.';
@override
String get vacationNoActiveRangeHint =>
'Nessun periodo di vacanza attivo al momento.';
@@ -1371,6 +1393,12 @@ class AppLocalizationsIt extends AppLocalizations {
return 'Giorni: $days';
}
@override
String get alarmRepeatSectionLabel => 'RIPETI';
@override
String get alarmVolumeLabel => 'Volume';
@override
String get androidReliabilityTitle => 'Controlla affidabilità Android';
+28
View File
@@ -431,6 +431,10 @@ class AppLocalizationsJa extends AppLocalizations {
return '$totalMb MB 中 $usedMb MB 使用済み';
}
@override
String get recordingsLibraryStorageFolderCaption =>
'Music/PluriWave・上限に達すると古い順に削除されます';
@override
String get recordingsLibraryEmptyTitle => 'まだ録音がありません';
@@ -594,12 +598,20 @@ class AppLocalizationsJa extends AppLocalizations {
@override
String get searchMinQualityFilterLabel => '最低品質';
@override
String get searchLoadingStationsLabel => '局を検索中…';
@override
String get searchEmptyTitle => '局を検索';
@override
String get searchNoResultsTitle => '結果がありません';
@override
String searchNoResultsForQueryTitle(Object query) {
return '$query」の結果はありません';
}
@override
String get searchEmptySubtitle => '上部バーやチップを使って、世界中の電波を見つけましょう。';
@@ -630,6 +642,9 @@ class AppLocalizationsJa extends AppLocalizations {
@override
String get countriesScreenTitle => '';
@override
String get countriesSearchHint => '国名またはコード...';
@override
String get countriesYourLanguagesTitle => 'あなたの言語';
@@ -985,6 +1000,9 @@ class AppLocalizationsJa extends AppLocalizations {
return '$minutes分';
}
@override
String get alarmSnoozeUsualLabel => '通常';
@override
String get saveFavoritesAlarmHint => '音楽アラームとして使うには、局をお気に入りに保存してください。';
@@ -1070,6 +1088,10 @@ class AppLocalizationsJa extends AppLocalizations {
@override
String get addVacationRangeCta => '期間を追加';
@override
String get vacationExplainerBanner =>
'この期間中は「休暇中は一時停止」に設定したアラームは鳴りません。「常に鳴らす」に設定したアラームには影響しません。';
@override
String get vacationNoActiveRangeHint => '現在アクティブな休暇期間はありません。';
@@ -1312,6 +1334,12 @@ class AppLocalizationsJa extends AppLocalizations {
return '曜日: $days';
}
@override
String get alarmRepeatSectionLabel => '繰り返し';
@override
String get alarmVolumeLabel => '音量';
@override
String get androidReliabilityTitle => 'Androidの信頼性を確認';
+28
View File
@@ -446,6 +446,10 @@ class AppLocalizationsPt extends AppLocalizations {
return '$usedMb MB de $totalMb MB usados';
}
@override
String get recordingsLibraryStorageFolderCaption =>
'Music/PluriWave · apaga as mais antigas ao atingir o limite';
@override
String get recordingsLibraryEmptyTitle => 'Ainda não há gravações';
@@ -619,12 +623,20 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get searchMinQualityFilterLabel => 'Qualidade mínima';
@override
String get searchLoadingStationsLabel => 'PROCURANDO EMISSORAS…';
@override
String get searchEmptyTitle => 'Busque uma estação';
@override
String get searchNoResultsTitle => 'Sem resultados';
@override
String searchNoResultsForQueryTitle(Object query) {
return 'Nenhum resultado para \"$query\"';
}
@override
String get searchEmptySubtitle =>
'Use a barra superior ou os chips para descobrir estações do mundo todo.';
@@ -658,6 +670,9 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get countriesScreenTitle => 'Países';
@override
String get countriesSearchHint => 'País ou código...';
@override
String get countriesYourLanguagesTitle => 'Seus idiomas';
@@ -1018,6 +1033,9 @@ class AppLocalizationsPt extends AppLocalizations {
return '$minutes min';
}
@override
String get alarmSnoozeUsualLabel => 'usual';
@override
String get saveFavoritesAlarmHint =>
'Salve estações nos Favoritos para usá-las como alarme musical.';
@@ -1106,6 +1124,10 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get addVacationRangeCta => 'Adicionar período';
@override
String get vacationExplainerBanner =>
'Durante esses periodos, os alarmes marcados como \"pausar nas ferias\" nao tocam. Os marcados como \"tocar sempre\" nao sao afetados.';
@override
String get vacationNoActiveRangeHint =>
'Não há período de férias ativo no momento.';
@@ -1360,6 +1382,12 @@ class AppLocalizationsPt extends AppLocalizations {
return 'Dias: $days';
}
@override
String get alarmRepeatSectionLabel => 'REPETIÇÃO';
@override
String get alarmVolumeLabel => 'Volume';
@override
String get androidReliabilityTitle => 'Revisar confiabilidade Android';
+28
View File
@@ -446,6 +446,10 @@ class AppLocalizationsRu extends AppLocalizations {
return 'Использовано $usedMb МБ из $totalMb МБ';
}
@override
String get recordingsLibraryStorageFolderCaption =>
'Music/PluriWave · при достижении лимита удаляются старые записи';
@override
String get recordingsLibraryEmptyTitle => 'Записей пока нет';
@@ -618,12 +622,20 @@ class AppLocalizationsRu extends AppLocalizations {
@override
String get searchMinQualityFilterLabel => 'Минимальное качество';
@override
String get searchLoadingStationsLabel => 'ПОИСК СТАНЦИЙ…';
@override
String get searchEmptyTitle => 'Найдите станцию';
@override
String get searchNoResultsTitle => 'Нет результатов';
@override
String searchNoResultsForQueryTitle(Object query) {
return 'Нет результатов по запросу «$query»';
}
@override
String get searchEmptySubtitle =>
'Используйте верхнюю строку или чипы, чтобы находить сигналы со всего мира.';
@@ -659,6 +671,9 @@ class AppLocalizationsRu extends AppLocalizations {
@override
String get countriesScreenTitle => 'Страны';
@override
String get countriesSearchHint => 'Страна или код...';
@override
String get countriesYourLanguagesTitle => 'Ваши языки';
@@ -1020,6 +1035,9 @@ class AppLocalizationsRu extends AppLocalizations {
return '$minutes мин';
}
@override
String get alarmSnoozeUsualLabel => 'обычно';
@override
String get saveFavoritesAlarmHint =>
'Сохраните станции в избранное, чтобы использовать их как музыкальный будильник.';
@@ -1108,6 +1126,10 @@ class AppLocalizationsRu extends AppLocalizations {
@override
String get addVacationRangeCta => 'Добавить период';
@override
String get vacationExplainerBanner =>
'В эти периоды будильники с пометкой «пауза на время отпуска» не звонят. Будильники с пометкой «звонить всегда» это не затрагивает.';
@override
String get vacationNoActiveRangeHint =>
'Сейчас нет активного периода отпуска.';
@@ -1364,6 +1386,12 @@ class AppLocalizationsRu extends AppLocalizations {
return 'Дни: $days';
}
@override
String get alarmRepeatSectionLabel => 'ПОВТОР';
@override
String get alarmVolumeLabel => 'Громкость';
@override
String get androidReliabilityTitle => 'Проверить надёжность Android';
+28
View File
@@ -429,6 +429,10 @@ class AppLocalizationsZh extends AppLocalizations {
return '已使用 $usedMb MB / 共 $totalMb MB';
}
@override
String get recordingsLibraryStorageFolderCaption =>
'Music/PluriWave · 达到上限时会删除最旧的文件';
@override
String get recordingsLibraryEmptyTitle => '暂无录音';
@@ -592,12 +596,20 @@ class AppLocalizationsZh extends AppLocalizations {
@override
String get searchMinQualityFilterLabel => '最低质量';
@override
String get searchLoadingStationsLabel => '正在搜索电台…';
@override
String get searchEmptyTitle => '搜索电台';
@override
String get searchNoResultsTitle => '没有结果';
@override
String searchNoResultsForQueryTitle(Object query) {
return '未找到与\"$query\"相关的结果';
}
@override
String get searchEmptySubtitle => '使用顶部搜索栏或筛选标签,发现世界各地的电台信号。';
@@ -628,6 +640,9 @@ class AppLocalizationsZh extends AppLocalizations {
@override
String get countriesScreenTitle => '国家';
@override
String get countriesSearchHint => '国家或代码...';
@override
String get countriesYourLanguagesTitle => '你的语言';
@@ -981,6 +996,9 @@ class AppLocalizationsZh extends AppLocalizations {
return '$minutes 分钟';
}
@override
String get alarmSnoozeUsualLabel => '常用';
@override
String get saveFavoritesAlarmHint => '将电台保存到收藏,即可把它们用作音乐闹钟。';
@@ -1066,6 +1084,10 @@ class AppLocalizationsZh extends AppLocalizations {
@override
String get addVacationRangeCta => '添加时间段';
@override
String get vacationExplainerBanner =>
'在这些时间段内,标记为“假期暂停”的闹钟不会响铃;标记为“始终响铃”的闹钟不受影响。';
@override
String get vacationNoActiveRangeHint => '目前没有正在生效的假期时间段。';
@@ -1307,6 +1329,12 @@ class AppLocalizationsZh extends AppLocalizations {
return '星期:$days';
}
@override
String get alarmRepeatSectionLabel => '重复';
@override
String get alarmVolumeLabel => '音量';
@override
String get androidReliabilityTitle => '检查 Android 可靠性';
@@ -8,6 +8,7 @@ import '../../estado/estado_radio.dart';
import '../../l10n/gen/app_localizations.dart';
import '../../modelos/preset_ecualizador.dart';
import '../../tema/pluriwave_theme.dart';
import '../../tema/pluriwave_tokens.dart';
import '../../widgets/ecualizador_widget.dart';
import '../../widgets/pluri_glass_surface.dart';
import '../../widgets/pluri_layout.dart';
@@ -36,13 +37,33 @@ class PantallaAjustesEcualizador extends StatelessWidget {
const PantallaAjustesEcualizador({super.key});
@override
Widget build(BuildContext context) => PluriPushScaffold(
title: AppLocalizations.of(context).equalizerTitle,
body: ListView(
padding: PluriLayout.pageContentPadding,
children: const [_CuerpoEcualizador()],
),
);
Widget build(BuildContext context) {
// Audit 11.1 (t4 line 566): the master enable switch lives in the
// HEADER, not as the body's first row -- read here (a second,
// cheap watch alongside _CuerpoEcualizadorState's own Consumer2)
// purely to feed PluriPushScaffold.actions.
final eq = context.watch<EstadoEcualizador>();
return PluriPushScaffold(
title: AppLocalizations.of(context).equalizerTitle,
actions: [
Padding(
padding: const EdgeInsets.only(right: 8),
child: Switch(
key: const ValueKey('eq-master-switch'),
value: eq.activo,
onChanged: eq.cambiarActivo,
// t4 line 569: brand-teal track, white thumb -- the default
// Material thumb colour already renders white when "on".
activeTrackColor: PluriWaveTokens.brand,
),
),
],
body: ListView(
padding: PluriLayout.pageContentPadding,
children: const [_CuerpoEcualizador()],
),
);
}
}
class _CuerpoEcualizador extends StatefulWidget {
@@ -83,17 +104,18 @@ class _CuerpoEcualizadorState extends State<_CuerpoEcualizador> {
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
SwitchListTile.adaptive(
contentPadding: EdgeInsets.zero,
title: Text(l10n.equalizerEnable),
subtitle: Text(
disponible
? l10n.equalizerRealtimeSubtitle
: l10n.equalizerPendingSubtitle,
),
value: eq.activo,
onChanged: eq.cambiarActivo,
// Audit 11.1 (t4 line 566): the enable switch itself now lives
// in PluriPushScaffold's header (see PantallaAjustesEcualizador
// above) -- this stays behind only as the explanatory caption
// the old SwitchListTile's subtitle carried, so that
// information is not lost.
Text(
disponible
? l10n.equalizerRealtimeSubtitle
: l10n.equalizerPendingSubtitle,
style: Theme.of(ctx).textTheme.bodySmall,
),
const SizedBox(height: 12),
if (mostrarModoPorEmisora) ...[
const SizedBox(height: 8),
SwitchListTile.adaptive(
@@ -164,6 +186,12 @@ class _BannerExplicacionBase extends StatelessWidget {
final AppLocalizations l10n;
/// Audit 11.2 (t4 line 571): 16 -- doesn't match any of
/// [PluriWaveTokens]'s three named radii (14/18/30), so this stays a
/// local one-off constant (same precedent as `_stopButtonRadius` in
/// `pantalla_alarma_sonando.dart`).
static const _bannerRadius = 16.0;
@override
Widget build(BuildContext context) {
final tokens = context.pluriTokens;
@@ -172,7 +200,7 @@ class _BannerExplicacionBase extends StatelessWidget {
padding: const EdgeInsets.all(14),
decoration: BoxDecoration(
color: tokens.liveGreen.withValues(alpha: 0.09),
borderRadius: BorderRadius.circular(tokens.radiusSm),
borderRadius: BorderRadius.circular(_bannerRadius),
border: Border.all(color: tokens.liveGreen.withValues(alpha: 0.26)),
),
child: Row(
+40 -15
View File
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import '../../../tema/pluriwave_theme.dart';
import '../../../widgets/pluri_glass_surface.dart';
import '../../../widgets/pluri_layout.dart';
/// Design ADR-3: the two nav-row primitives every Settings detail screen is
/// reached through. [GrupoAjustes] is a single [PluriGlassSurface] card
@@ -27,20 +28,36 @@ class GrupoAjustes extends StatelessWidget {
@override
Widget build(BuildContext context) {
final type = context.pluriType;
return PluriGlassSurface(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(titulo, style: type.eyebrowLabel),
const SizedBox(height: 4),
for (var i = 0; i < filas.length; i++) ...[
// S10 (Tier 4 visual fidelity): the prototype insets its row
// divider by 47px (t4 line 516), not full-bleed.
if (i > 0) const Divider(height: 1, indent: 47),
filas[i],
],
],
),
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Audit 10.2 (t4 line 511): the group eyebrow sits OUTSIDE the
// card entirely, at title-tier (20px) padding -- it used to live
// INSIDE the PluriGlassSurface, sharing the card's own 16px
// padding.
Padding(
padding: const EdgeInsets.fromLTRB(
PluriLayout.titleHorizontal,
0,
PluriLayout.titleHorizontal,
6,
),
child: Text(titulo, style: type.eyebrowLabel),
),
PluriGlassSurface(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
for (var i = 0; i < filas.length; i++) ...[
// S10 (Tier 4 visual fidelity): the prototype insets its
// row divider by 47px (t4 line 516), not full-bleed.
if (i > 0) const Divider(height: 1, indent: 47),
filas[i],
],
],
),
),
],
);
}
}
@@ -56,6 +73,7 @@ class FilaAjuste extends StatelessWidget {
required this.titulo,
required this.onTap,
this.valor,
this.iconColor,
});
final IconData icon;
@@ -68,6 +86,13 @@ class FilaAjuste extends StatelessWidget {
/// "no current value to show" — the row renders exactly as before.
final String? valor;
/// Audit 10.5 (t4 lines 514/516/522 — equalizer `#21D4D9`, hd
/// `#7EE4C2`, folder `#F4B860`): only the first row or two of a group
/// carries an accent colour in the prototype; every other row's icon
/// stays the ambient default. Null (the vast majority of rows) means
/// "no accent" — the icon renders exactly as before.
final Color? iconColor;
@override
Widget build(BuildContext context) {
final type = context.pluriType;
@@ -76,7 +101,7 @@ class FilaAjuste extends StatelessWidget {
contentPadding: EdgeInsets.zero,
// 10.6 (Tier 4 visual fidelity): the prototype's row icon is 21px (t4
// line 514), not Material's 24px default.
leading: Icon(icon, size: 21),
leading: Icon(icon, size: 21, color: iconColor),
// 10.8 (Tier 4 visual fidelity): the prototype's row title is
// 14px/w700 (t4 line 514); cardTitle is 14.5/w700 — a one-off
// override, not a new PluriWaveTypography style (mirrors the
+7
View File
@@ -9,6 +9,7 @@ import '../l10n/display_names.dart';
import '../l10n/gen/app_localizations.dart';
import '../modelos/archivo_grabacion.dart';
import '../modelos/emisora.dart';
import '../tema/pluriwave_tokens.dart';
import '../widgets/pluri_layout.dart';
import '../widgets/pluri_push_scaffold.dart';
import '../widgets/pluri_root_header.dart';
@@ -106,6 +107,9 @@ class _AjustesContent extends StatelessWidget {
filas: [
FilaAjuste(
icon: Icons.equalizer_rounded,
// Audit 10.5 (t4 line 514): the first AUDIO row's icon is
// brand cyan.
iconColor: PluriWaveTokens.brand,
titulo: l10n.equalizerTitle,
valor:
ecualizadorActivo
@@ -149,6 +153,9 @@ class _AjustesContent extends StatelessWidget {
filas: [
FilaAjuste(
icon: Icons.playlist_add_check_circle_rounded,
// Audit 10.5 (t4 line 526): the first STATIONS row's icon
// is warmCoral (the prototype's own `folder` row accent).
iconColor: PluriWaveTokens.dark.warmCoral,
titulo: l10n.favoriteGroupsTitle,
valor: '$gruposCount',
onTap:
+48 -1
View File
@@ -225,6 +225,25 @@ class _PantallaAlarmaSonandoState extends State<PantallaAlarmaSonando> {
tokens: tokens,
),
const SizedBox(height: 22),
// Audit 9.4 (t4 line 419): "Lunes, 3 de agosto" between
// the pill and the hero time -- never rendered before.
// Purely additive: a new sibling Text, touching neither
// the pill above nor the hero time below.
Text(
fechaLargaConDiaSemana(
Localizations.localeOf(context).toString(),
DateTime.now(),
),
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: Theme.of(
context,
).colorScheme.onSurface.withValues(alpha: 0.6),
),
),
const SizedBox(height: 6),
FittedBox(
fit: BoxFit.scaleDown,
child: Text(
@@ -689,6 +708,20 @@ class _FilaSnoozeFija extends StatelessWidget {
final forma = RoundedRectangleBorder(
borderRadius: BorderRadius.circular(tokens.radiusMd),
);
// Audit 9.10 (t4 line 433): the prototype stacks a big NUMBER over a
// small "min · habitual" unit — the SAME text-splitting conflict as
// 9.9 (permanently rejected, Engram id 2525): this flat string is
// exactly what the protected dismiss-guard test locates via
// `find.text(l10n.alarmSnoozeOptionLabel(N))` in four places, and
// splitting it into two differently-styled Text nodes would make
// that flat value vanish from the render tree.
//
// Resolved differently here than 9.9: rather than splitting THIS
// string, an entirely SEPARATE small qualifier Text is added
// alongside it (only on the destacado tile) — the original flat
// label stays a single, untouched, unstyled-differently Text node,
// still the exact widget the guard finds and taps. This delivers
// the "habitual" qualifier without the conflict 9.9 hit.
final etiqueta = Text(l10n.alarmSnoozeOptionLabel(minutos));
return Expanded(
flex: esDestacado ? 3 : 2,
@@ -703,7 +736,21 @@ class _FilaSnoozeFija extends StatelessWidget {
foregroundColor: tokens.deepViolet,
shape: forma,
),
child: etiqueta,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
etiqueta,
Text(
l10n.alarmSnoozeUsualLabel,
style: TextStyle(
fontSize: 11,
fontWeight: FontWeight.w800,
color: tokens.deepViolet.withValues(alpha: 0.75),
),
),
],
),
)
: OutlinedButton(
onPressed: () => onPosponer(minutos),
+476 -170
View File
@@ -45,9 +45,26 @@ class PantallaAlarmas extends StatelessWidget {
title: l10n.alarmScreenTitle,
onSleepTimer: () => showPluriSleepTimerSheet(context),
actions: [
FilledButton.tonalIcon(
// Audit 7.1 (t4:325): a solid brand-teal pill with a plain
// `add` glyph -- was a tonal button with `auto_awesome`.
FilledButton.icon(
style: FilledButton.styleFrom(
backgroundColor: PluriWaveTokens.brand,
foregroundColor: const Color(0xFF062126),
padding: const EdgeInsets.symmetric(
horizontal: 14,
vertical: 9,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
textStyle: const TextStyle(
fontSize: 13,
fontWeight: FontWeight.w800,
),
),
onPressed: () => _abrirEditor(context),
icon: const Icon(Icons.auto_awesome_rounded, size: 18),
icon: const Icon(Icons.add_rounded, size: 18),
label: Text(l10n.createAlarmAction),
),
],
@@ -105,6 +122,7 @@ class _PanelProximaAlarma extends StatelessWidget {
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context);
final tokens = context.pluriTokens;
final proxima = estado.proximaAlarma;
final activasSinProxima =
estado.alarmas
@@ -112,56 +130,87 @@ class _PanelProximaAlarma extends StatelessWidget {
.length;
final proximaProgramable = proxima?.proximaProgramable;
return PluriGlassSurface(
glowColor: context.pluriTokens.warmCoral.withValues(alpha: 0.28),
child: Row(
children: [
_AssetIcon(
'assets/icons/alarmas/alarm_music.png',
size: 72,
semanticLabel: l10n.alarmIconLabel,
),
const SizedBox(width: 14),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
proxima == null
? activasSinProxima > 0
? l10n.activeAlarmsWithoutNextTitle
: l10n.noActiveAlarms
: l10n.nextAlarmTitle,
style: Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w900,
),
),
const SizedBox(height: 4),
Text(
proxima == null
? activasSinProxima > 0
? l10n.activeAlarmsWithoutNextSubtitle(
activasSinProxima,
)
: l10n.createAlarmHint
: '${_nombreVisibleAlarma(l10n, proxima)} · ${_fechaHora(l10n, proximaProgramable!)}',
),
if (proxima != null) ...[
const SizedBox(height: 10),
Align(
alignment: Alignment.centerLeft,
child: OutlinedButton.icon(
key: const ValueKey('hero-skip-next'),
onPressed: () => _saltarDesdeHero(context, proxima),
icon: const Icon(Icons.skip_next_rounded, size: 18),
label: Text(l10n.alarmHeroSkipAction),
// Audit 7.2 (t4:326-330): warmCoral-tinted card, `alarm_on` icon at
// 26px, and the "Saltar" chip BESIDE the text on the same row -- was
// an opaque default card with a 72px PNG and the skip action stacked
// BELOW the text as an OutlinedButton.
return DecoratedBox(
key: const ValueKey('next-alarm-banner'),
decoration: BoxDecoration(
color: tokens.warmCoral.withValues(alpha: 0.13),
borderRadius: BorderRadius.circular(20),
border: Border.all(color: tokens.warmCoral.withValues(alpha: 0.34)),
),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Icon(Icons.alarm_on, size: 26, color: tokens.warmCoral),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(
proxima == null
? activasSinProxima > 0
? l10n.activeAlarmsWithoutNextTitle
: l10n.noActiveAlarms
: l10n.nextAlarmTitle,
style: Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w900,
),
),
const SizedBox(height: 4),
Text(
proxima == null
? activasSinProxima > 0
? l10n.activeAlarmsWithoutNextSubtitle(
activasSinProxima,
)
: l10n.createAlarmHint
: '${_nombreVisibleAlarma(l10n, proxima)} · ${_fechaHora(l10n, proximaProgramable!)}',
),
],
],
),
),
if (proxima != null) ...[
const SizedBox(width: 8),
_ChipSaltar(onTap: () => _saltarDesdeHero(context, proxima)),
],
],
),
),
);
}
}
/// Audit 7.2 (t4:329): `padding:8px 12px;radius:10;rgba(255,255,255,.08)` --
/// plain text, no icon, unlike the previous `OutlinedButton.icon`.
class _ChipSaltar extends StatelessWidget {
const _ChipSaltar({required this.onTap});
final VoidCallback onTap;
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context);
return Material(
color: Colors.white.withValues(alpha: 0.08),
borderRadius: BorderRadius.circular(10),
child: InkWell(
key: const ValueKey('hero-skip-next'),
borderRadius: BorderRadius.circular(10),
onTap: onTap,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
child: Text(
l10n.alarmHeroSkipAction,
style: const TextStyle(fontSize: 12, fontWeight: FontWeight.w800),
),
],
),
),
);
}
@@ -243,17 +292,83 @@ class _TarjetaAlarma extends StatelessWidget {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
_hora(alarma),
style: Theme.of(
context,
).textTheme.displaySmall?.copyWith(
fontWeight: FontWeight.w900,
letterSpacing: -1.5,
),
// Audit 7.4 (t4:339): the recurrence label sits on
// the SAME baseline as the giant time -- it used to
// be missing entirely.
Row(
crossAxisAlignment: CrossAxisAlignment.baseline,
textBaseline: TextBaseline.alphabetic,
children: [
Text(
_hora(alarma),
style: Theme.of(
context,
).textTheme.displaySmall?.copyWith(
fontWeight: FontWeight.w900,
letterSpacing: -1.5,
),
),
const SizedBox(width: 8),
Text(
_recurrenciaCorta(l10n, alarma),
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w700,
color: Theme.of(
context,
).colorScheme.onSurface.withValues(alpha: 0.5),
),
),
],
),
const SizedBox(height: 7),
// Audit 7.4 (t4:341): a small station-art slot
// inline with the name -- was the name alone.
// `Emisora.favicon` is a network URL (the same
// hazard documented for the ringing screen's audit
// 9.2 -- `Image.network` here would hang widget
// tests without a mocked HttpClient), so this is a
// themed fallback icon, not the real per-station
// artwork, mirroring the recordings-row precedent
// (`pantalla_grabaciones.dart`'s `_FilaGrabacion`).
Row(
children: [
if (alarma.emisora != null) ...[
DecoratedBox(
key: const ValueKey('tarjeta-alarma-arte'),
decoration: BoxDecoration(
color: tokens.listSurface,
borderRadius: BorderRadius.circular(6),
),
child: SizedBox(
width: 22,
height: 22,
child: Icon(
Icons.radio_rounded,
size: 14,
color: Theme.of(context)
.colorScheme
.onSurface
.withValues(alpha: 0.75),
),
),
),
const SizedBox(width: 7),
],
Flexible(
child: Text(
estacion,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 12.5,
fontWeight: FontWeight.w600,
color: Theme.of(context).colorScheme.onSurface
.withValues(alpha: 0.75),
),
),
),
],
),
const SizedBox(height: 2),
Text(estacion, overflow: TextOverflow.ellipsis),
],
),
),
@@ -532,6 +647,15 @@ class _EditorAlarmaSheetState extends State<_EditorAlarmaSheet> {
(value) => setState(() => _tipo = value.first),
),
const SizedBox(height: 10),
// Audit 8.5 (t4 line 381): the "REPETIR" eyebrow above the
// weekday circles -- never rendered anywhere before.
Padding(
padding: const EdgeInsets.fromLTRB(4, 0, 4, 8),
child: Text(
l10n.alarmRepeatSectionLabel,
style: context.pluriType.eyebrowLabel,
),
),
// WU10: weekday circles are now ALWAYS visible (previously
// only inserted into the tree in diasSemana mode) — matching
// the mockup, which shows them unconditionally under the
@@ -573,80 +697,117 @@ class _EditorAlarmaSheetState extends State<_EditorAlarmaSheet> {
const SizedBox(height: 12),
_vistaProximaEjecucion(l10n),
const SizedBox(height: 14),
_SectionLabel(
icon: 'assets/icons/alarmas/fallback_sound.png',
text: l10n.soundAndVolumeSection,
),
Slider(
value: _volumen,
// S2-R11: floor lowered from 0.25 to 0.0.
min: 0,
max: 1,
divisions: 20,
label: '${(_volumen * 100).round()}%',
onChanged: (value) => setState(() => _volumen = value),
),
const SizedBox(height: 8),
ListTile(
contentPadding: EdgeInsets.zero,
title: Text(
l10n.alarmFadeInTitle,
style: context.pluriType.cardTitle,
// Audit 8.6 (t4 lines 392-401): the station picker, volume,
// fade-in and vacation toggle now share ONE bordered card
// with sangred divider lines between rows -- matching the
// prototype's own single grouped block -- instead of
// sitting bare on the sheet, each with its own gaps. The
// extra fields the prototype does NOT show (name, type
// selector, snooze selector, "use current station",
// Advanced) stay OUTSIDE this card, exactly where they
// were (documented as deliberate in audit 8.8 / WU10).
DecoratedBox(
key: const ValueKey('alarm-editor-grouped-card'),
decoration: BoxDecoration(
color: context.pluriTokens.listSurface,
borderRadius: BorderRadius.circular(18),
border: Border.all(
color: Colors.white.withValues(alpha: 0.08),
),
),
subtitle: Text(
_fadeInSegundos == 0
? l10n.alarmFadeInOff
: l10n.alarmFadeInSummary(_fadeInSegundos),
// A transparent Material sits directly inside the coloured
// box, closer to the fade-in ListTile and the vacation
// SwitchListTile than the DecoratedBox's own opaque fill
// -- both paint their ink/background on the NEAREST
// Material ancestor, and without this the box's colour
// would hide those effects (Flutter's own debug-mode
// check for exactly this).
child: Material(
type: MaterialType.transparency,
child: Column(
children: [
// S2-R9: searchable bottom-sheet picker instead of a
// dropdown, for the primary station. The backup
// (fallback) picker moves into the Advanced section
// below (WU10) — the primary choice stays a
// top-level field, only its secondary/backup sibling
// is now one tap further away.
_CampoSelectorEmisora(
key: const ValueKey('alarm-station-field'),
label: l10n.favoriteStationLabel,
icon: Icons.radio_rounded,
value:
_emisora == null
? l10n.noStationUseInternalSound
: localizedStationName(
l10n,
_emisora!.nombre,
),
onTap:
() => _elegirEmisora(
favoritas,
seleccionar:
(emisora) =>
setState(() => _emisora = emisora),
),
),
const Divider(height: 1, indent: 66),
// Audit 8.7 (t4 line 396): a compact 112px track with
// a trailing percentage label -- was a bare
// full-width Slider with no visible value.
_FilaVolumen(
volumen: _volumen,
onChanged:
(value) => setState(() => _volumen = value),
),
const Divider(height: 1, indent: 46),
Padding(
padding: const EdgeInsets.fromLTRB(14, 4, 14, 4),
child: ListTile(
contentPadding: EdgeInsets.zero,
title: Text(
l10n.alarmFadeInTitle,
style: context.pluriType.cardTitle,
),
subtitle: Text(
_fadeInSegundos == 0
? l10n.alarmFadeInOff
: l10n.alarmFadeInSummary(_fadeInSegundos),
),
),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 10),
child: Slider(
value: _fadeInSegundos.toDouble(),
min: 0,
max: 60,
divisions: 60,
label: '${_fadeInSegundos}s',
onChanged:
(value) => setState(
() => _fadeInSegundos = value.round(),
),
),
),
const Divider(height: 1, indent: 46),
SwitchListTile.adaptive(
value: _sonarEnVacaciones,
onChanged:
(value) =>
setState(() => _sonarEnVacaciones = value),
secondary: _AssetIcon(
'assets/icons/alarmas/vacation_wave.png',
size: 42,
semanticLabel: l10n.vacationIconLabel,
),
title: Text(l10n.playDuringVacations),
subtitle: Text(l10n.playDuringVacationsHint),
),
],
),
),
),
Slider(
value: _fadeInSegundos.toDouble(),
min: 0,
max: 60,
divisions: 60,
label: '${_fadeInSegundos}s',
onChanged:
(value) =>
setState(() => _fadeInSegundos = value.round()),
),
ListTile(
contentPadding: EdgeInsets.zero,
title: Text(l10n.alarmSnoozeDurationTitle),
subtitle: Text(l10n.alarmSnoozeOptionLabel(_snoozeMinutos)),
),
SegmentedButton<int>(
segments: [
for (final minutos in _opcionesSnooze())
ButtonSegment(
value: minutos,
label: Text(l10n.alarmSnoozeOptionLabel(minutos)),
),
],
selected: {_snoozeMinutos},
onSelectionChanged:
(value) => setState(() => _snoozeMinutos = value.first),
),
const SizedBox(height: 8),
// S2-R9: searchable bottom-sheet picker instead of a dropdown,
// for the primary station. The backup (fallback) picker moves
// into the Advanced section below (WU10) — the primary choice
// stays a top-level field, only its secondary/backup sibling
// is now one tap further away.
_CampoSelectorEmisora(
key: const ValueKey('alarm-station-field'),
label: l10n.favoriteStationLabel,
icon: Icons.radio_rounded,
value:
_emisora == null
? l10n.noStationUseInternalSound
: localizedStationName(l10n, _emisora!.nombre),
onTap:
() => _elegirEmisora(
favoritas,
seleccionar:
(emisora) => setState(() => _emisora = emisora),
),
),
if (favoritas.isEmpty) ...[
const SizedBox(height: 6),
Text(l10n.saveFavoritesAlarmHint),
@@ -663,19 +824,23 @@ class _EditorAlarmaSheetState extends State<_EditorAlarmaSheet> {
),
),
],
const SizedBox(height: 8),
SwitchListTile.adaptive(
const SizedBox(height: 12),
ListTile(
contentPadding: EdgeInsets.zero,
value: _sonarEnVacaciones,
onChanged:
(value) => setState(() => _sonarEnVacaciones = value),
secondary: _AssetIcon(
'assets/icons/alarmas/vacation_wave.png',
size: 42,
semanticLabel: l10n.vacationIconLabel,
),
title: Text(l10n.playDuringVacations),
subtitle: Text(l10n.playDuringVacationsHint),
title: Text(l10n.alarmSnoozeDurationTitle),
subtitle: Text(l10n.alarmSnoozeOptionLabel(_snoozeMinutos)),
),
SegmentedButton<int>(
segments: [
for (final minutos in _opcionesSnooze())
ButtonSegment(
value: minutos,
label: Text(l10n.alarmSnoozeOptionLabel(minutos)),
),
],
selected: {_snoozeMinutos},
onSelectionChanged:
(value) => setState(() => _snoozeMinutos = value.first),
),
const SizedBox(height: 8),
// WU10 (native-alarms delta — Alarm Editor Preserves Date,
@@ -918,18 +1083,122 @@ class _CampoSelectorEmisora extends StatelessWidget {
final String value;
final VoidCallback onTap;
/// Audit 8.6 (t4:394): a flat nav row -- icon, bold label, muted value,
/// chevron -- matching the same convention `FilaAjuste` uses everywhere
/// else in Settings, so this fits cleanly inside the grouped card below
/// instead of drawing its own outlined `InputDecoration` chrome.
@override
Widget build(BuildContext context) {
return InkWell(
borderRadius: BorderRadius.circular(12),
onTap: onTap,
child: InputDecorator(
decoration: InputDecoration(
labelText: label,
prefixIcon: Icon(icon),
suffixIcon: const Icon(Icons.arrow_drop_down_rounded),
final type = context.pluriType;
return Material(
type: MaterialType.transparency,
child: InkWell(
onTap: onTap,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 11),
child: Row(
children: [
Icon(icon, size: 20),
const SizedBox(width: 12),
Expanded(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(label, style: type.cardTitle),
Text(
value,
overflow: TextOverflow.ellipsis,
style: type.bodyStrong.copyWith(
color: Theme.of(
context,
).colorScheme.onSurface.withValues(alpha: 0.55),
),
),
],
),
),
Icon(
Icons.chevron_right_rounded,
size: 19,
color: Theme.of(
context,
).colorScheme.onSurface.withValues(alpha: 0.4),
),
],
),
),
child: Text(value, overflow: TextOverflow.ellipsis),
),
);
}
}
/// Audit 8.7 (t4 line 396): a compact volume row -- icon, label, a 112px
/// track, and a trailing "80%" -- was a bare full-width `Slider` with no
/// visible current value. Constrains a real, still-draggable `Slider`
/// (not a static bar) to the prototype's 112px track width via a
/// `SliderTheme` + fixed-width `SizedBox`, preserving drag interactivity.
class _FilaVolumen extends StatelessWidget {
const _FilaVolumen({required this.volumen, required this.onChanged});
final double volumen;
final ValueChanged<double> onChanged;
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context);
final onSurface = Theme.of(context).colorScheme.onSurface;
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 4),
child: Row(
children: [
Icon(
Icons.volume_up_rounded,
size: 20,
color: onSurface.withValues(alpha: 0.7),
),
const SizedBox(width: 12),
Expanded(
child: Text(
l10n.alarmVolumeLabel,
style: context.pluriType.cardTitle,
),
),
SizedBox(
width: 112,
height: 24,
child: SliderTheme(
data: SliderTheme.of(context).copyWith(
trackHeight: 4,
activeTrackColor: PluriWaveTokens.brand,
inactiveTrackColor: Colors.white.withValues(alpha: 0.14),
thumbShape: const RoundSliderThumbShape(enabledThumbRadius: 7),
overlayShape: const RoundSliderOverlayShape(overlayRadius: 14),
),
child: Slider(
value: volumen,
// S2-R11: floor lowered from 0.25 to 0.0.
min: 0,
max: 1,
divisions: 20,
label: '${(volumen * 100).round()}%',
onChanged: onChanged,
),
),
),
SizedBox(
width: 34,
child: Text(
'${(volumen * 100).round()}%',
textAlign: TextAlign.right,
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w800,
color: onSurface.withValues(alpha: 0.7),
),
),
),
],
),
);
}
@@ -1089,6 +1358,12 @@ class _PanelVacaciones extends StatelessWidget {
final l10n = AppLocalizations.of(context);
final tokens = context.pluriTokens;
final resumen = _resumenVacaciones(l10n, estado.vacaciones);
// Audit 7.3 (t4:332): a trailing date-range pill ("4-18 AGO") for the
// active-or-next range -- never rendered anywhere before.
final proximas = estado.vacacionesProximas();
final rangoRelevante =
estado.rangoVacacionesActivo() ??
(proximas.isEmpty ? null : proximas.first);
return PluriGlassSurface(
glowColor: PluriWaveTokens.skyBlue.withValues(alpha: 0.22),
padding: EdgeInsets.zero,
@@ -1122,6 +1397,10 @@ class _PanelVacaciones extends StatelessWidget {
],
),
),
if (rangoRelevante != null) ...[
const SizedBox(width: 8),
_PildoraFechasVacaciones(rango: rangoRelevante),
],
const Icon(Icons.chevron_right_rounded),
],
),
@@ -1168,6 +1447,40 @@ class _PanelVacaciones extends StatelessWidget {
}
}
/// Audit 7.3 (t4:332): "4-18 AGO" -- `radius:999`, `liveGreen@.16` fill,
/// `liveGreen@.38` border, 10.5px/w800 in `liveGreen` (matches the
/// prototype's own `rgba(126,228,194,...)` teal, not the alarm banner's
/// warmCoral).
class _PildoraFechasVacaciones extends StatelessWidget {
const _PildoraFechasVacaciones({required this.rango});
final RangoVacaciones rango;
@override
Widget build(BuildContext context) {
final tokens = context.pluriTokens;
final locale = AppLocalizations.of(context).localeName;
return DecoratedBox(
decoration: BoxDecoration(
color: tokens.liveGreen.withValues(alpha: 0.16),
borderRadius: BorderRadius.circular(999),
border: Border.all(color: tokens.liveGreen.withValues(alpha: 0.38)),
),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 9, vertical: 4),
child: Text(
rangoFechasCorto(locale, rango.inicioDia, rango.finDia),
style: TextStyle(
fontSize: 10.5,
fontWeight: FontWeight.w800,
color: tokens.liveGreen,
),
),
),
);
}
}
/// Audit 8.4 (t4 lines 383-390): one circular weekday button in the alarm
/// editor's REPETIR row — was a `FilterChip`. `aspect-ratio:1` in the
/// prototype is achieved here by the caller wrapping each instance in an
@@ -1287,27 +1600,6 @@ class _PickerButton extends StatelessWidget {
}
}
class _SectionLabel extends StatelessWidget {
const _SectionLabel({required this.icon, required this.text});
final String icon;
final String text;
@override
Widget build(BuildContext context) {
return Row(
children: [
_AssetIcon(icon, size: 34),
const SizedBox(width: 8),
// WU10: swapped the raw TextTheme lookup for the named type-scale
// token (cosmetic only — same weight class, now shared with every
// other card/section title in the redesign).
Text(text, style: context.pluriType.cardTitle),
],
);
}
}
class _NoticeLine extends StatelessWidget {
const _NoticeLine({super.key, required this.icon, required this.text});
@@ -1375,3 +1667,17 @@ String _weekdayShort(AppLocalizations l10n, int day) => switch (day) {
// S5-R4: short dates follow the active locale (en-US = M/D/Y, ja = Y/M/D).
String _fechaCorta(AppLocalizations l10n, DateTime fecha) =>
fechaCortaLocalizada(l10n.localeName, fecha);
/// Audit 7.4 (t4:339): a compact recurrence label next to the alarm card's
/// giant time. Reuses the SAME generic labels the editor's own
/// `TipoProgramacionAlarma` `SegmentedButton` already shows (`oneTimeOption`
/// / `dailyOption` / `weekdaysOption`) rather than inventing a new, more
/// specific ARB string -- honest given the space (12px, next to a 34px
/// time) genuinely only fits a short word, not a full weekday list.
String _recurrenciaCorta(AppLocalizations l10n, AlarmaMusical alarma) {
return switch (alarma.tipoProgramacion) {
TipoProgramacionAlarma.diaria => l10n.dailyOption,
TipoProgramacionAlarma.diasSemana => l10n.weekdaysOption,
TipoProgramacionAlarma.unica => l10n.oneTimeOption,
};
}
+15 -1
View File
@@ -92,10 +92,16 @@ class PantallaBienvenida extends StatelessWidget {
const SizedBox(height: 22),
Text(
l10n.welcomeHeadline,
// Audit 14.3 (t4 line 696): 34px/ls-1.2, not
// headlineMedium's 28/ls-1.0 — a local override,
// matching the precedent already set by every
// other one-off size correction in this screen
// (14.4-14.6 below).
style: theme.textTheme.headlineMedium?.copyWith(
fontSize: 34,
fontWeight: FontWeight.w800,
height: 1.05,
letterSpacing: -1.0,
letterSpacing: -1.2,
),
),
const SizedBox(height: 12),
@@ -143,6 +149,14 @@ class PantallaBienvenida extends StatelessWidget {
height: 58,
child: FilledButton(
onPressed: () => _empezar(context),
// Audit 14.7 (t4 line 715): radius 18 — Material 3's
// default FilledButton shape is a fully-round
// StadiumBorder, which the prototype does not draw.
style: FilledButton.styleFrom(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(18),
),
),
child: Text(
l10n.welcomeCtaLabel,
style: const TextStyle(
+184 -39
View File
@@ -253,10 +253,25 @@ class _PantallaBuscarState extends State<PantallaBuscar> {
}),
];
if (pills.isEmpty && estado.resultados.isEmpty) {
return const SizedBox.shrink();
}
// Audit 6.3 (t4:294-295): "Idioma" is an always-reachable entry chip
// once a search is active -- there was no standalone entry point for
// language filtering before this (only bundled inside "Filtros").
// Opens the SAME existing filter sheet (all 3 sections) rather than a
// new idioma-only picker: "Ordenar" is a genuinely separate control
// (not part of that sheet), and "Filtros" itself must stay reachable
// from the header regardless of active-filter state -- the quality
// (bitrate) filter has no OTHER entry point anywhere in the app, so
// this chip is additive, not a replacement.
final entryChips = <Widget>[
ActionChip(
label: Text(l10n.searchLanguageFilterLabel),
onPressed: _abrirFiltros,
),
];
// Audit 6.3: entryChips is never empty (always carries "Idioma"), so
// this row now always renders while a search is active -- the old
// "nothing to show" early return no longer applies.
return Padding(
padding: const EdgeInsets.fromLTRB(
PluriLayout.horizontal,
@@ -267,18 +282,16 @@ class _PantallaBuscarState extends State<PantallaBuscar> {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (pills.isNotEmpty)
Wrap(spacing: 8, runSpacing: 8, children: pills),
if (pills.isNotEmpty && estado.resultados.isNotEmpty)
Wrap(spacing: 8, runSpacing: 8, children: [...pills, ...entryChips]),
if (!estado.cargando && estado.resultados.isNotEmpty) ...[
const SizedBox(height: 10),
if (!estado.cargando && estado.resultados.isNotEmpty)
Row(
children: [
// Audit 6.4 (t4:299): eyebrow styling (11/w800/ls.06em) --
// was labelLarge (14/w800).
Text(
l10n.searchResultsCount(estado.resultados.length),
style: theme.textTheme.labelLarge?.copyWith(
fontWeight: FontWeight.w800,
),
style: context.pluriType.eyebrowLabel,
),
const Spacer(),
PopupMenuButton<OrdenEmisoras>(
@@ -306,16 +319,28 @@ class _PantallaBuscarState extends State<PantallaBuscar> {
),
],
),
],
],
),
);
}
/// Audit 6.2 (t4:292-293): brand-teal tinted, radius 10, an inline 15px
/// close glyph -- was Material's own `Chip` theming.
Widget _pillFiltro(String label, VoidCallback onDeleted) {
return Chip(
label: Text(label),
labelStyle: const TextStyle(
fontWeight: FontWeight.w800,
color: Color(0xFFF2F7FA),
),
backgroundColor: PluriWaveTokens.brand.withValues(alpha: 0.2),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
side: BorderSide(color: PluriWaveTokens.brand.withValues(alpha: 0.45)),
),
onDeleted: onDeleted,
deleteIcon: const Icon(Icons.close, size: 18),
deleteIcon: const Icon(Icons.close, size: 15),
visualDensity: VisualDensity.compact,
);
}
@@ -521,10 +546,21 @@ class _PantallaBuscarState extends State<PantallaBuscar> {
return Padding(
padding: const EdgeInsets.all(PluriLayout.horizontal),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Audit 13.3 (t4:649): the "BUSCANDO EMISORAS…" eyebrow --
// never rendered before.
Padding(
padding: const EdgeInsets.only(bottom: 10),
child: Text(
l10n.searchLoadingStationsLabel,
style: context.pluriType.eyebrowLabel,
),
),
for (var i = 0; i < 4; i++) ...[
const TarjetaEmisoraShimmer(esCompacta: true),
if (i < 3) const SizedBox(height: 10),
// Audit 13.4 (t4:651): 4px between skeleton rows, not 10.
if (i < 3) const SizedBox(height: 4),
],
],
),
@@ -535,36 +571,40 @@ class _PantallaBuscarState extends State<PantallaBuscar> {
if (resultados.isEmpty) {
final sinFiltros = _controller.text.isEmpty && _filtrosActivosCount == 0;
return Column(
children: [
SizedBox(
height: 260,
child: PluriEmptyState(
glyph: PluriIconGlyph.search,
title:
sinFiltros
? l10n.searchEmptyTitle
: l10n.searchNoResultsTitle,
subtitle:
sinFiltros
? l10n.searchEmptySubtitle
: l10n.searchNoResultsSubtitle,
),
),
// Audit 13.5/13.6 (t4:657-668): a purpose-built card for the
// search-no-results state -- centred, `listSurface`, the title
// QUOTES the typed query, and the clear-filters pill sits INSIDE
// the card. A NEW widget, not a restyle of the shared
// `PluriEmptyState` (used by several OTHER unrelated empty states
// across the app -- favorites, the discovery grid -- which this
// item does not touch).
final query = _controller.text.trim();
return Padding(
padding: const EdgeInsets.symmetric(horizontal: PluriLayout.horizontal),
child: _TarjetaSinResultados(
titulo:
sinFiltros
? l10n.searchEmptyTitle
: query.isNotEmpty
? l10n.searchNoResultsForQueryTitle(query)
: l10n.searchNoResultsTitle,
subtitulo:
sinFiltros
? l10n.searchEmptySubtitle
: l10n.searchNoResultsSubtitle,
// task 6.6 / spec "One-Tap Clear-All-Filters on Empty Results":
// only offered once 1+ pill-filters are active AND the search
// came back empty — not merely "no query typed yet".
if (_filtrosActivosCount > 0)
Padding(
padding: const EdgeInsets.only(bottom: 16),
child: OutlinedButton(
onPressed: _quitarTodosLosFiltros,
child: Text(
l10n.searchClearFiltersAction(_filtrosActivosCount),
),
),
),
],
accionQuitarFiltros:
_filtrosActivosCount > 0
? (
etiqueta: l10n.searchClearFiltersAction(
_filtrosActivosCount,
),
onTap: _quitarTodosLosFiltros,
)
: null,
),
);
}
@@ -1213,6 +1253,111 @@ class _ChipShimmer extends StatelessWidget {
}
}
/// Audit 13.5/13.6 (t4:657-668): search-no-results card -- centred,
/// `listSurface`, radius 22 (matches neither of the other 2 named token
/// radii, so this is a local one-off like `_CeldaExplorarPor`'s own),
/// with the clear-filters pill living INSIDE the card rather than below
/// it.
class _TarjetaSinResultados extends StatelessWidget {
const _TarjetaSinResultados({
required this.titulo,
required this.subtitulo,
this.accionQuitarFiltros,
});
final String titulo;
final String subtitulo;
final ({String etiqueta, VoidCallback onTap})? accionQuitarFiltros;
static const _radio = 22.0;
@override
Widget build(BuildContext context) {
final accion = accionQuitarFiltros;
return DecoratedBox(
key: const ValueKey('search-no-results-card'),
decoration: BoxDecoration(
color: PluriWaveTokens.dark.listSurface,
borderRadius: BorderRadius.circular(_radio),
border: Border.all(color: Colors.white.withValues(alpha: 0.07)),
),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 26),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.search_off_rounded,
size: 40,
color: PluriWaveTokens.brand.withValues(alpha: 0.6),
),
const SizedBox(height: 12),
Text(
titulo,
textAlign: TextAlign.center,
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w800),
),
const SizedBox(height: 5),
Text(
subtitulo,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 12.5,
height: 1.5,
color: Theme.of(
context,
).colorScheme.onSurface.withValues(alpha: 0.6),
),
),
if (accion != null) ...[
const SizedBox(height: 14),
Material(
color: Colors.transparent,
child: InkWell(
borderRadius: BorderRadius.circular(12),
onTap: accion.onTap,
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 15,
vertical: 9,
),
decoration: BoxDecoration(
color: PluriWaveTokens.brand.withValues(alpha: 0.18),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: PluriWaveTokens.brand.withValues(alpha: 0.4),
),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(
Icons.filter_alt_off_rounded,
size: 17,
color: PluriWaveTokens.brand,
),
const SizedBox(width: 7),
Text(
accion.etiqueta,
style: const TextStyle(
fontSize: 12.5,
fontWeight: FontWeight.w800,
color: PluriWaveTokens.brand,
),
),
],
),
),
),
),
],
],
),
),
);
}
}
/// Audit 3.2 (t4 lines 167-170): one "Explorar por" grid cell — icon,
/// title (13.5/w800) and subtitle (11/55%). Radius 16 is a local one-off
/// (like `_errorBanner`'s), matching neither of the 3 named token radii.
+61 -36
View File
@@ -6,6 +6,7 @@ import '../l10n/display_names.dart';
import '../l10n/gen/app_localizations.dart';
import '../modelos/emisora.dart';
import '../modelos/grupo_favoritos.dart';
import '../tema/pluriwave_tokens.dart';
import '../widgets/fila_emisora_plana.dart';
import '../widgets/pluri_icon.dart';
import '../widgets/pluri_layout.dart';
@@ -186,22 +187,23 @@ class _PantallaFavoritosState extends State<PantallaFavoritos> {
children: [
// S1/S2 (Tier 1 visual fidelity): see the empty-state branch
// above — PluriScreenHeader is retired everywhere.
//
// Audit 4.1 (t4:216): the prototype's two header icon actions
// (create_new_folder, swap_vert) now live in PluriRootHeader's
// own actions slot -- they used to be scattered as an
// ActionChip inside the chip strip and a PopupMenuButton
// sharing a Row with it. The back arrow the prototype also
// draws stays absent (binding decision: this root keeps its
// bottom tab bar, unlike the prototype's own pushed shape).
PluriRootHeader(
title: l10n.favoritesTitle,
onSleepTimer: () => showPluriSleepTimerSheet(context),
),
const SizedBox(height: 12),
Row(
children: [
Expanded(
child: _FilaChipsGrupos(
grupos: gruposVisibles,
favoritos: favoritos,
seleccionado: seleccionEfectiva,
onSeleccionar:
(id) => setState(() => _grupoSeleccionadoId = id),
onGestionar: _abrirGestionDeListas,
),
actions: [
IconButton(
key: const ValueKey('favorites-manage-groups-action'),
icon: const Icon(Icons.create_new_folder_rounded),
tooltip: l10n.favoriteGroupsManage,
onPressed: _abrirGestionDeListas,
),
PopupMenuButton<OrdenEmisoras>(
icon: const Icon(Icons.swap_vert_rounded),
@@ -221,6 +223,13 @@ class _PantallaFavoritosState extends State<PantallaFavoritos> {
),
],
),
const SizedBox(height: 12),
_FilaChipsGrupos(
grupos: gruposVisibles,
favoritos: favoritos,
seleccionado: seleccionEfectiva,
onSeleccionar: (id) => setState(() => _grupoSeleccionadoId = id),
),
],
),
),
@@ -256,18 +265,45 @@ class _FilaChipsGrupos extends StatelessWidget {
required this.favoritos,
required this.seleccionado,
required this.onSeleccionar,
required this.onGestionar,
});
final List<GrupoFavoritos> grupos;
final List<Emisora> favoritos;
final String? seleccionado;
final ValueChanged<String?> onSeleccionar;
final VoidCallback onGestionar;
String _nombreVisible(AppLocalizations l10n, GrupoFavoritos grupo) =>
grupo.esSinAsignar ? l10n.favoriteGroupsUnassigned : grupo.nombre;
/// Audit 4.2 (t4:219-221): active `#21D4D9`/`#062126` w800, inactive
/// `listSurface` + a faint border / w700 -- was Material's own
/// `ChoiceChip` theming (a plain checkbox-style selected fill).
Widget _chip({
required String label,
required bool selected,
required VoidCallback onTap,
}) {
return ChoiceChip(
label: Text(label),
labelStyle: TextStyle(
fontWeight: selected ? FontWeight.w800 : FontWeight.w700,
color: selected ? const Color(0xFF062126) : const Color(0xFFF2F7FA),
),
selected: selected,
showCheckmark: false,
selectedColor: PluriWaveTokens.brand,
backgroundColor: PluriWaveTokens.dark.listSurface,
side: BorderSide(
color:
selected
? Colors.transparent
: Colors.white.withValues(alpha: 0.09),
),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
onSelected: (_) => onTap(),
);
}
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context);
@@ -278,38 +314,27 @@ class _FilaChipsGrupos extends StatelessWidget {
children: [
Padding(
padding: const EdgeInsets.only(right: 8),
child: ChoiceChip(
label: Text(
l10n.favoriteGroupsChipLabel(
l10n.favoritesFilterAllLabel,
favoritos.length,
),
child: _chip(
label: l10n.favoriteGroupsChipLabel(
l10n.favoritesFilterAllLabel,
favoritos.length,
),
selected: seleccionado == null,
onSelected: (_) => onSeleccionar(null),
onTap: () => onSeleccionar(null),
),
),
for (final grupo in grupos)
Padding(
padding: const EdgeInsets.only(right: 8),
child: ChoiceChip(
label: Text(
l10n.favoriteGroupsChipLabel(
_nombreVisible(l10n, grupo),
favoritos
.where((e) => e.grupoFavoritosId == grupo.id)
.length,
),
child: _chip(
label: l10n.favoriteGroupsChipLabel(
_nombreVisible(l10n, grupo),
favoritos.where((e) => e.grupoFavoritosId == grupo.id).length,
),
selected: seleccionado == grupo.id,
onSelected: (_) => onSeleccionar(grupo.id),
onTap: () => onSeleccionar(grupo.id),
),
),
ActionChip(
avatar: const Icon(Icons.add_rounded, size: 18),
label: Text(l10n.favoriteGroupsManage),
onPressed: onGestionar,
),
],
),
);
+20 -5
View File
@@ -308,13 +308,28 @@ class _BarraDeAlmacenamiento extends StatelessWidget {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ClipRRect(
borderRadius: BorderRadius.circular(8),
child: LinearProgressIndicator(value: fraccion, minHeight: 8),
),
const SizedBox(height: 8),
// Audit 12.2 (t4 line 613): the bold "used of total" headline
// sits ABOVE the bar -- was the bar first, then this same string
// rendered small below it as the only caption.
Text(
l10n.recordingsLibraryStorageCaption(usadoMb, totalMb),
style: Theme.of(
context,
).textTheme.bodySmall?.copyWith(fontWeight: FontWeight.w800),
),
const SizedBox(height: 8),
// Audit 12.2: 6px bar, radius 3 (was minHeight 8, radius 8).
ClipRRect(
borderRadius: BorderRadius.circular(3),
child: LinearProgressIndicator(value: fraccion, minHeight: 6),
),
const SizedBox(height: 7),
// Audit 12.3 (t4 line 613): the real caption names the folder
// and the purge policy -- the generic "X of Y used" line moved
// up to become the headline above, it never described either of
// those.
Text(
l10n.recordingsLibraryStorageFolderCaption,
style: Theme.of(context).textTheme.bodySmall,
),
],
+136 -47
View File
@@ -4,8 +4,8 @@ import 'package:provider/provider.dart';
import '../estado/estado_busqueda.dart';
import '../l10n/gen/app_localizations.dart';
import '../modelos/pais_radio.dart';
import '../tema/pluriwave_theme.dart';
import '../tema/pluriwave_tokens.dart';
import '../widgets/pluri_glass_surface.dart';
import '../widgets/pluri_layout.dart';
import '../widgets/pluri_push_scaffold.dart';
@@ -30,6 +30,12 @@ class PantallaPaises extends StatefulWidget {
}
class _PantallaPaisesState extends State<PantallaPaises> {
/// Audit 5.7 (t4:252): the header's `search` action. Null means "not
/// searching" — an ephemeral UI concern (design's "State is for
/// ephemeral UI only" ruling), never persisted.
bool _buscando = false;
final _controladorBusqueda = TextEditingController();
/// One representative country per app-supported locale (the same 13
/// locales as `pantalla_ajustes_idioma.dart`'s `_idiomas` list). "Tus
/// idiomas" is named by the proposal/spec but its derivation is not
@@ -65,22 +71,73 @@ class _PantallaPaisesState extends State<PantallaPaises> {
});
}
@override
void dispose() {
_controladorBusqueda.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context);
final estado = context.watch<EstadoBusqueda>();
final query = _controladorBusqueda.text.trim().toLowerCase();
final paisesFiltrados =
query.isEmpty
? estado.paises
: estado.paises
.where(
(p) =>
p.nombre.toLowerCase().contains(query) ||
p.codigoIso.toLowerCase().contains(query),
)
.toList();
return PluriPushScaffold(
title: l10n.countriesScreenTitle,
// Audit 5.7 (t4:252): a `search` header action -- toggles an inline
// filter field over the SAME country list, rather than a decorative
// no-op button. `PluriPushScaffold.titleOverride` stays reserved for
// its one documented exception (the player's "EN DIRECTO" pill) --
// the search field lives in the body instead, not the AppBar title.
actions: [
IconButton(
key: const ValueKey('countries-search-toggle'),
icon: Icon(_buscando ? Icons.close_rounded : Icons.search_rounded),
tooltip: l10n.navSearch,
onPressed:
() => setState(() {
_buscando = !_buscando;
if (!_buscando) _controladorBusqueda.clear();
}),
),
],
body:
estado.cargandoPaises && estado.paises.isEmpty
? const Center(child: CircularProgressIndicator())
: ListView(
padding: PluriLayout.pageContentPadding,
children: [
_seccionTusIdiomas(context, estado.paises, l10n),
const SizedBox(height: 16),
_seccionTodos(context, estado.paises, l10n),
if (_buscando)
Padding(
padding: const EdgeInsets.only(bottom: 12),
child: TextField(
key: const ValueKey('countries-search-field'),
controller: _controladorBusqueda,
autofocus: true,
decoration: InputDecoration(
hintText: l10n.countriesSearchHint,
prefixIcon: const Icon(Icons.search_rounded),
),
onChanged: (_) => setState(() {}),
),
),
if (query.isEmpty) ...[
_seccionTusIdiomas(context, estado.paises, l10n),
const SizedBox(height: 16),
_seccionTodos(context, estado.paises, l10n),
] else
_seccionTodos(context, paisesFiltrados, l10n),
],
),
);
@@ -104,31 +161,45 @@ class _PantallaPaisesState extends State<PantallaPaises> {
if (destacados.isEmpty) return const SizedBox.shrink();
final theme = Theme.of(context);
return PluriGlassSurface(
padding: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
l10n.countriesYourLanguagesTitle,
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w900,
),
final type = context.pluriType;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Audit 5.4 (t4:254): an eyebrow OUTSIDE any card, title-tier
// (20px) padding -- was titleMedium w900 inside a PluriGlassSurface.
Padding(
padding: const EdgeInsets.fromLTRB(
PluriLayout.titleHorizontal,
0,
PluriLayout.titleHorizontal,
8,
),
const SizedBox(height: 8),
// Item 24 / audit 5.1 (t4:255-258): a column of tappable ISO
// rows, not a Wrap of non-interactive Chips.
for (final pais in destacados)
_FilaPais(
pais: pais,
l10n: l10n,
// Item 24 / audit 5.5 (t4:256): the first row is highlighted.
destacado: pais == destacados.first,
onTap: () => _seleccionar(pais),
),
],
),
child: Text(
l10n.countriesYourLanguagesTitle,
style: type.eyebrowLabel,
),
),
Padding(
padding: const EdgeInsets.symmetric(
horizontal: PluriLayout.rowHorizontal,
),
child: Column(
children: [
// Item 24 / audit 5.1 (t4:255-258): a column of tappable ISO
// rows, not a Wrap of non-interactive Chips.
for (final pais in destacados)
_FilaPais(
pais: pais,
l10n: l10n,
// Item 24 / audit 5.5 (t4:256): the first row is
// highlighted.
destacado: pais == destacados.first,
onTap: () => _seleccionar(pais),
),
],
),
),
],
);
}
@@ -137,26 +208,44 @@ class _PantallaPaisesState extends State<PantallaPaises> {
List<PaisRadio> paises,
AppLocalizations l10n,
) {
final theme = Theme.of(context);
return PluriGlassSurface(
padding: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
l10n.countriesAllTitle,
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w900,
),
final type = context.pluriType;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Audit 5.4 (t4:260-261): "TODOS · 238" -- an eyebrow OUTSIDE any
// card, carrying the total country count, which never rendered
// anywhere before.
Padding(
padding: const EdgeInsets.fromLTRB(
PluriLayout.titleHorizontal,
0,
PluriLayout.titleHorizontal,
8,
),
const SizedBox(height: 4),
// Item 24 / audit 5.1-5.3 (t4:262-269): the same tappable ISO row
// as "Tus idiomas" -- not the previous ListTile, which had no ISO
// column and no onTap.
for (final pais in paises)
_FilaPais(pais: pais, l10n: l10n, onTap: () => _seleccionar(pais)),
],
),
child: Text(
'${l10n.countriesAllTitle} · ${paises.length}',
style: type.eyebrowLabel,
),
),
Padding(
padding: const EdgeInsets.symmetric(
horizontal: PluriLayout.rowHorizontal,
),
child: Column(
children: [
// Item 24 / audit 5.1-5.3 (t4:262-269): the same tappable ISO
// row as "Tus idiomas" -- not the previous ListTile, which
// had no ISO column and no onTap.
for (final pais in paises)
_FilaPais(
pais: pais,
l10n: l10n,
onTap: () => _seleccionar(pais),
),
],
),
),
],
);
}
}
+240 -23
View File
@@ -31,9 +31,40 @@ class PantallaVacaciones extends StatelessWidget {
return PluriPushScaffold(
title: l10n.vacationRangesTitle,
// Audit 9b.1 (t4:446): a solid brand-teal "Add" header action --
// the prototype's OWN mid-page CTA (audit 9b.6, still present below,
// now dashed) is a SECOND, additional entry point in the prototype,
// not a replacement for this one.
actions: [
Padding(
padding: const EdgeInsets.only(right: 8),
child: FilledButton.icon(
key: const ValueKey('vacation-add-header'),
style: FilledButton.styleFrom(
backgroundColor: PluriWaveTokens.brand,
foregroundColor: const Color(0xFF062126),
padding: const EdgeInsets.symmetric(horizontal: 13, vertical: 8),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(11),
),
textStyle: const TextStyle(
fontSize: 12.5,
fontWeight: FontWeight.w800,
),
),
onPressed: () => _abrirAlta(context),
icon: const Icon(Icons.add_rounded, size: 17),
label: Text(l10n.addAction),
),
),
],
body: ListView(
padding: PluriLayout.pageContentPadding,
children: [
// Audit 9b.2 (t4:448): the explanatory banner is ALWAYS visible
// -- never rendered anywhere before.
_BannerExplicativo(texto: l10n.vacationExplainerBanner),
const SizedBox(height: 18),
if (activo != null)
_HeroRangoActivo(estado: estado, rango: activo)
else
@@ -41,15 +72,10 @@ class PantallaVacaciones extends StatelessWidget {
const SizedBox(height: 16),
_SeccionProgramados(proximas: proximas),
const SizedBox(height: 16),
Align(
alignment: Alignment.centerLeft,
child: OutlinedButton.icon(
onPressed: () => _abrirAlta(context),
icon: const Icon(Icons.add_rounded),
label: Text(l10n.addVacationRangeCta),
),
),
const SizedBox(height: 16),
// Audit 9b.6 (t4:487): a dashed border + `date_range` icon --
// was a solid `OutlinedButton` with an `add` glyph.
_CtaAnadirRango(onTap: () => _abrirAlta(context)),
const SizedBox(height: 14),
_SeccionRangosPasados(pasadas: pasadas),
],
),
@@ -67,6 +93,136 @@ class PantallaVacaciones extends StatelessWidget {
}
}
/// Audit 9b.2 (t4:448): teal-tinted explainer banner, always visible above
/// the active-range hero.
class _BannerExplicativo extends StatelessWidget {
const _BannerExplicativo({required this.texto});
final String texto;
@override
Widget build(BuildContext context) {
final tokens = context.pluriTokens;
return DecoratedBox(
decoration: BoxDecoration(
color: tokens.liveGreen.withValues(alpha: 0.09),
borderRadius: BorderRadius.circular(16),
border: Border.all(color: tokens.liveGreen.withValues(alpha: 0.26)),
),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 15, vertical: 13),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(Icons.info_outline_rounded, size: 20, color: tokens.liveGreen),
const SizedBox(width: 11),
Expanded(
child: Text(
texto,
style: TextStyle(
fontSize: 12,
height: 1.5,
color: Theme.of(
context,
).colorScheme.onSurface.withValues(alpha: 0.72),
),
),
),
],
),
),
);
}
}
/// Audit 9b.6 (t4:487): dashed-border CTA with a `date_range` glyph --
/// reuses the same dashed-painter shape already established in
/// `pantalla_favoritos.dart`'s custom-station CTA (audit 4.5), duplicated
/// rather than shared (small, self-contained, matching this codebase's own
/// precedent for tiny per-screen painters).
class _CtaAnadirRango extends StatelessWidget {
const _CtaAnadirRango({required this.onTap});
final VoidCallback onTap;
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context);
final colorTexto = Theme.of(
context,
).colorScheme.onSurface.withValues(alpha: 0.6);
return CustomPaint(
painter: _DashedBorderPainter(
color: Colors.white.withValues(alpha: 0.16),
),
child: Material(
color: Colors.transparent,
child: InkWell(
borderRadius: BorderRadius.circular(16),
onTap: onTap,
child: Padding(
padding: const EdgeInsets.all(15),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.date_range_rounded, size: 20, color: colorTexto),
const SizedBox(width: 8),
Text(
l10n.addVacationRangeCta,
style: TextStyle(
fontSize: 13.5,
fontWeight: FontWeight.w800,
color: colorTexto,
),
),
],
),
),
),
),
);
}
}
class _DashedBorderPainter extends CustomPainter {
const _DashedBorderPainter({required this.color});
final Color color;
static const _radius = 16.0;
static const _dashWidth = 6.0;
static const _gapWidth = 4.0;
@override
void paint(Canvas canvas, Size size) {
final rrect = RRect.fromRectAndRadius(
Offset.zero & size,
const Radius.circular(_radius),
);
final path = Path()..addRRect(rrect);
final paint =
Paint()
..color = color
..style = PaintingStyle.stroke
..strokeWidth = 1.5;
for (final metric in path.computeMetrics()) {
var distance = 0.0;
while (distance < metric.length) {
final next = distance + _dashWidth;
canvas.drawPath(
metric.extractPath(distance, next.clamp(0.0, metric.length)),
paint,
);
distance = next + _gapWidth;
}
}
}
@override
bool shouldRepaint(covariant _DashedBorderPainter oldDelegate) =>
oldDelegate.color != color;
}
/// Active-range hero: name, days-remaining countdown (reusing WU8's own
/// `vacationSummaryActiveCountdown` string — same concept, bigger stage),
/// a start/end date pair (item 21 / audit 9b.4, replacing the former
@@ -184,31 +340,92 @@ class _SeccionProgramados extends StatelessWidget {
}
}
class _SeccionRangosPasados extends StatelessWidget {
/// Audit 9b.7 (t4:489): a collapsible row -- icon, title, count, chevron
/// -- COLLAPSED by default; tapping reveals the full list below it. Was
/// always fully expanded inline.
class _SeccionRangosPasados extends StatefulWidget {
const _SeccionRangosPasados({required this.pasadas});
final List<RangoVacaciones> pasadas;
@override
State<_SeccionRangosPasados> createState() => _SeccionRangosPasadosState();
}
class _SeccionRangosPasadosState extends State<_SeccionRangosPasados> {
/// Ephemeral UI state only (design's "State is for ephemeral UI only"
/// ruling) -- collapsed by default, matching the prototype's own count
/// only row.
bool _expandido = false;
@override
Widget build(BuildContext context) {
if (pasadas.isEmpty) return const SizedBox.shrink();
if (widget.pasadas.isEmpty) return const SizedBox.shrink();
final l10n = AppLocalizations.of(context);
final type = context.pluriType;
return PluriGlassSurface(
padding: EdgeInsets.zero,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(l10n.vacationPastSectionTitle, style: type.eyebrowLabel),
const SizedBox(height: 8),
// No prototype-specified header text exists for an already-ended
// range, so `encabezado` is omitted rather than invented.
PluriPanelColumn(
gap: 10,
children: [
for (final rango in pasadas)
_TarjetaRangoVacaciones(rango: rango),
],
Material(
type: MaterialType.transparency,
child: InkWell(
onTap: () => setState(() => _expandido = !_expandido),
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 14,
vertical: 12,
),
child: Row(
children: [
Icon(
Icons.history_rounded,
size: 20,
color: Theme.of(
context,
).colorScheme.onSurface.withValues(alpha: 0.6),
),
const SizedBox(width: 12),
Expanded(
child: Text(
l10n.vacationPastSectionTitle,
style: context.pluriType.cardTitle,
),
),
Text(
'${widget.pasadas.length}',
style: TextStyle(
fontSize: 12.5,
color: Theme.of(
context,
).colorScheme.onSurface.withValues(alpha: 0.5),
),
),
const SizedBox(width: 4),
Icon(
_expandido
? Icons.expand_less_rounded
: Icons.chevron_right_rounded,
size: 19,
color: Theme.of(
context,
).colorScheme.onSurface.withValues(alpha: 0.4),
),
],
),
),
),
),
if (_expandido)
Padding(
padding: const EdgeInsets.fromLTRB(16, 0, 16, 16),
child: PluriPanelColumn(
gap: 10,
children: [
for (final rango in widget.pasadas)
_TarjetaRangoVacaciones(rango: rango),
],
),
),
],
),
);
+75 -5
View File
@@ -113,6 +113,10 @@ class _EcualizadorWidgetState extends State<EcualizadorWidget> {
theme.colorScheme.surfaceContainerHighest,
overlayColor: PluriWaveTokens.brand
.withValues(alpha: 0.15),
// Audit 11.5 (t4 line 585): a 20x20 thumb
// with a 14px brand-teal glow -- was the
// Material default round thumb shape.
thumbShape: const _GlowSliderThumbShape(),
),
child: Slider(
value: _bandas[i],
@@ -130,8 +134,12 @@ class _EcualizadorWidgetState extends State<EcualizadorWidget> {
),
Text(
'${_bandas[i].toStringAsFixed(1)}dB',
// Audit 11.7 (t4 line 584): the prototype's dB
// label is brand teal at 90% alpha -- `liveGreen`
// is the LIVE-badge colour, an unrelated wrong
// family untouched by 11.6's slider-only fix.
style: theme.textTheme.labelSmall?.copyWith(
color: tokens.liveGreen,
color: PluriWaveTokens.brand.withValues(alpha: 0.9),
fontWeight: FontWeight.w700,
),
),
@@ -154,6 +162,50 @@ class _EcualizadorWidgetState extends State<EcualizadorWidget> {
}
}
/// Audit 11.5 (t4 line 585): `width:6px;border-radius:3px` track with a
/// `20x20` thumb carrying `box-shadow:0 0 14px rgba(33,212,217,.6)` --
/// Material's stock `RoundSliderThumbShape` has neither the exact size nor
/// a coloured glow (its own elevation shadow is a neutral drop shadow, not
/// brand-tinted). Paints a soft blurred glow first, then the solid thumb
/// on top, both centred on the slider's reported thumb position.
class _GlowSliderThumbShape extends SliderComponentShape {
const _GlowSliderThumbShape();
static const _radius = 10.0;
@override
Size getPreferredSize(bool isEnabled, bool isDiscrete) =>
const Size(_radius * 2, _radius * 2);
@override
void paint(
PaintingContext context,
Offset center, {
required Animation<double> activationAnimation,
required Animation<double> enableAnimation,
required bool isDiscrete,
required TextPainter labelPainter,
required RenderBox parentBox,
required SliderThemeData sliderTheme,
required TextDirection textDirection,
required double value,
required double textScaleFactor,
required Size sizeWithOverflow,
}) {
final canvas = context.canvas;
final color = sliderTheme.thumbColor ?? PluriWaveTokens.brand;
final glowPaint =
Paint()
..color = color.withValues(alpha: 0.6)
..maskFilter = const MaskFilter.blur(BlurStyle.normal, 7);
canvas.drawCircle(center, _radius + 4, glowPaint);
final thumbPaint = Paint()..color = color;
canvas.drawCircle(center, _radius, thumbPaint);
}
}
String _nombrePreset(AppLocalizations l10n, String nombre) {
return switch (nombre) {
'Flat' => l10n.equalizerPresetFlat,
@@ -188,7 +240,6 @@ class PresetsEcualizadorWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final l10n = AppLocalizations.of(context);
final todos = [...PresetEcualizador.presets, ...personalizados];
return Wrap(
@@ -197,13 +248,32 @@ class PresetsEcualizadorWidget extends StatelessWidget {
children:
todos.map((p) {
final selected = p.nombre == presetActual.nombre;
// Audit 11.3 (t4 lines 574-577): a solid brand-teal chip with
// dark text when active, `listSurface` + a faint border when
// not -- was Material's own `ChoiceChip` theming
// (`primaryContainer` selected / translucent grey unselected).
return ChoiceChip(
label: Text(_nombrePreset(l10n, p.nombre)),
labelStyle: TextStyle(
fontWeight: FontWeight.w800,
color:
selected
? const Color(0xFF062126)
: const Color(0xFFF2F7FA),
),
selected: selected,
showCheckmark: false,
selectedColor: theme.colorScheme.primaryContainer,
backgroundColor: theme.colorScheme.surfaceContainerHighest
.withValues(alpha: 0.32),
selectedColor: PluriWaveTokens.brand,
backgroundColor: PluriWaveTokens.dark.listSurface,
side: BorderSide(
color:
selected
? Colors.transparent
: Colors.white.withValues(alpha: 0.09),
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
onSelected: (_) => onSeleccionar(p),
);
}).toList(),
@@ -9,6 +9,7 @@ import 'package:pluriwave/modelos/dispositivo_audio.dart';
import 'package:pluriwave/modelos/emisora.dart';
import 'package:pluriwave/modelos/preset_ecualizador.dart';
import 'package:pluriwave/pantallas/ajustes/pantalla_ajustes_ecualizador.dart';
import 'package:pluriwave/tema/pluriwave_tokens.dart';
import 'package:pluriwave/widgets/ecualizador_widget.dart';
import 'package:pluriwave/widgets/pluri_glass_surface.dart';
import 'package:pluriwave/widgets/pluri_push_scaffold.dart';
@@ -119,9 +120,8 @@ void main() {
expect((appBar.title as Text).data, equals('Equalizer'));
});
testWidgets('moved control still responds: enable switch toggles activo', (
tester,
) async {
testWidgets('moved control still responds: enable switch toggles activo '
'(audit 11.1: now in the header, t4 line 566)', (tester) async {
_suppressListTileInkAssertion();
final estado = await crearEstado();
addTearDown(estado.dispose);
@@ -130,7 +130,7 @@ void main() {
await tester.pumpAndSettle();
final before = estado.ecualizador.activo;
await tester.tap(find.text('Enable equalizer'));
await tester.tap(find.byKey(const ValueKey('eq-master-switch')));
await tester.pumpAndSettle();
expect(estado.ecualizador.activo, equals(!before));
@@ -447,6 +447,74 @@ void main() {
},
);
testWidgets(
'visual fidelity (audit 11.1): the master switch is a header action, '
'not the body\'s first row (t4 line 566)',
(tester) async {
_suppressListTileInkAssertion();
final estado = await crearEstado();
addTearDown(estado.dispose);
await tester.pumpWidget(buildScreen(estado));
await tester.pumpAndSettle();
expect(
find.descendant(
of: find.byType(AppBar),
matching: find.byKey(const ValueKey('eq-master-switch')),
),
findsOneWidget,
reason: 'the master switch lives in the AppBar, not the body',
);
expect(
find.descendant(
of: find.byType(ListView),
matching: find.byKey(const ValueKey('eq-master-switch')),
),
findsNothing,
);
},
);
testWidgets(
'visual fidelity (audit 11.2): the explainer banner radius is 16, '
'not radiusSm\'s 14 (t4 line 571)',
(tester) async {
_suppressListTileInkAssertion();
final estado = await crearEstado();
addTearDown(estado.dispose);
await tester.pumpWidget(buildScreen(estado));
await tester.pumpAndSettle();
final banner = tester.widget<Container>(
find.byKey(const Key('eq-base-explainer-banner')),
);
final decoration = banner.decoration as BoxDecoration;
expect(
(decoration.borderRadius as BorderRadius).topLeft,
const Radius.circular(16),
);
},
);
testWidgets('visual fidelity (audit 11.3): the active preset chip is solid '
'brand teal with dark text (t4 lines 574-577)', (tester) async {
_suppressListTileInkAssertion();
final estado = await crearEstado();
addTearDown(estado.dispose);
await tester.pumpWidget(buildScreen(estado));
await tester.pumpAndSettle();
final chip = tester.widget<ChoiceChip>(
find.widgetWithText(ChoiceChip, 'Flat'),
);
expect(chip.selected, isTrue);
expect(chip.selectedColor, PluriWaveTokens.brand);
expect(chip.labelStyle?.color, const Color(0xFF062126));
});
testWidgets(
'Guardar como preset with a whitespace-only name shows the same validation message',
(tester) async {
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:pluriwave/pantallas/ajustes/widgets/fila_ajuste.dart';
import 'package:pluriwave/tema/pluriwave_theme.dart';
import 'package:pluriwave/widgets/pluri_glass_surface.dart';
/// S8 (Tier 1 visual fidelity): the prototype puts a trailing "current
/// value" on nearly every settings row, 13px `rgba(242,247,250,.55)` (t4
@@ -122,4 +123,81 @@ void main() {
expect(divider.indent, 47);
},
);
testWidgets('visual fidelity (audit 10.5): an explicit iconColor tints the '
'leading icon (t4 lines 514/516/522 -- equalizer/hd/folder accents)', (
tester,
) async {
await tester.pumpWidget(
host(
FilaAjuste(
icon: Icons.equalizer_rounded,
titulo: 'Ecualizador base',
iconColor: const Color(0xFF21D4D9),
onTap: () {},
),
),
);
final icon = tester.widget<Icon>(find.byIcon(Icons.equalizer_rounded));
expect(icon.color, const Color(0xFF21D4D9));
});
testWidgets(
'visual fidelity (audit 10.5): omitting iconColor keeps the default '
'(unchanged pre-10.5 behaviour)',
(tester) async {
await tester.pumpWidget(
host(
FilaAjuste(
icon: Icons.language_rounded,
titulo: 'Idioma',
onTap: () {},
),
),
);
final icon = tester.widget<Icon>(find.byIcon(Icons.language_rounded));
expect(icon.color, isNull);
},
);
testWidgets(
'visual fidelity (audit 10.2): the group eyebrow sits OUTSIDE the '
'card, at title-tier (20px) padding -- not inside the '
'PluriGlassSurface (t4 line 511)',
(tester) async {
_suppressListTileInkAssertion();
await tester.pumpWidget(
host(
GrupoAjustes(
titulo: 'AUDIO',
filas: [
FilaAjuste(
icon: Icons.equalizer_rounded,
titulo: 'Uno',
onTap: () {},
),
],
),
),
);
expect(
find.ancestor(
of: find.text('AUDIO'),
matching: find.byType(PluriGlassSurface),
),
findsNothing,
reason: 'the eyebrow must not be a descendant of the card',
);
final eyebrowLeft = tester.getTopLeft(find.text('AUDIO')).dx;
expect(
eyebrowLeft,
20,
reason: 'S5 title tier -- PluriLayout.titleHorizontal',
);
},
);
}
+3 -2
View File
@@ -162,9 +162,10 @@ void main() {
await pumpStable(tester);
// Pushed, not index-switched: exactly one PluriPushScaffold now exists,
// and its moved control (the enable switch) is reachable.
// and its moved control (the enable switch, audit 11.1 -- now a
// header action, t4 line 566) is reachable.
expect(find.byType(PluriPushScaffold), findsOneWidget);
expect(find.text('Enable equalizer'), findsOneWidget);
expect(find.byKey(const ValueKey('eq-master-switch')), findsOneWidget);
});
testWidgets('tapping the Orden de listas row pushes its detail screen', (
@@ -0,0 +1,162 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:pluriwave/estado/estado_alarmas.dart';
import 'package:pluriwave/estado/estado_radio.dart';
import 'package:pluriwave/l10n/formato_fechas.dart';
import 'package:pluriwave/l10n/gen/app_localizations.dart';
import 'package:pluriwave/modelos/alarma_musical.dart';
import 'package:pluriwave/modelos/emisora.dart';
import 'package:pluriwave/pantallas/pantalla_alarma_sonando.dart';
import 'package:pluriwave/servicios/servicio_alarmas.dart';
import 'package:pluriwave/servicios/servicio_audio.dart';
import 'package:pluriwave/tema/pluriwave_theme.dart';
import 'package:provider/provider.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../helpers/fakes.dart';
import '../helpers/fakes_alarmas.dart';
/// Closing batch (items 9.4, 9.10, audit id 2521) -- the ringing screen.
/// Mirrors `pantalla_alarma_sonando_tier4_test.dart`'s own harness (kept as
/// a local copy, not a shared import, matching this file's established
/// precedent of small helper duplication over cross-test-file coupling).
Future<void> _montarPantalla(WidgetTester tester) async {
tester.view.physicalSize = const Size(1440, 3200);
tester.view.devicePixelRatio = 1.0;
addTearDown(tester.view.resetPhysicalSize);
addTearDown(tester.view.resetDevicePixelRatio);
final audio = FakeServicioAudio();
audio.emitirEstado(EstadoReproduccion.reproduciendo);
final radio = EstadoRadio(
audio: audio,
favoritos: FakeServicioFavoritos(),
radio: FakeServicioRadio(),
servicioEcualizador: FakeServicioEcualizador(),
servicioGrabacion: FakeServicioGrabacionRadioInactiva(),
iniciarAutomaticamente: false,
);
addTearDown(radio.dispose);
final android = FakePuertoAlarmasAndroid();
final estadoAlarmas = EstadoAlarmas(
servicio: ServicioAlarmas(reloj: () => DateTime(2026, 6, 11, 7, 0)),
android: android,
iniciarAutomaticamente: false,
);
addTearDown(estadoAlarmas.dispose);
addTearDown(android.dispose);
await estadoAlarmas.guardarAlarma(
const AlarmaMusical(
id: 'tier5-1',
nombre: 'Despertar',
hora: 7,
minuto: 30,
tipoProgramacion: TipoProgramacionAlarma.diaria,
diasSemana: [],
snoozeMinutos: 10,
emisora: Emisora(
uuid: 'e1',
nombre: 'Radio Uno',
url: 'https://radio.example/stream',
),
),
);
await tester.pumpWidget(
MultiProvider(
providers: [
ChangeNotifierProvider<EstadoRadio>.value(value: radio),
ChangeNotifierProvider<EstadoAlarmas>.value(value: estadoAlarmas),
],
child: MaterialApp(
locale: const Locale('es'),
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
theme: PluriWaveTheme.dark(),
home: const SizedBox.shrink(),
),
),
);
final navigator = tester.state<NavigatorState>(find.byType(Navigator));
unawaited(
navigator.push(
MaterialPageRoute<void>(
builder:
(_) => PantallaAlarmaSonando(alarma: estadoAlarmas.alarmas.single),
fullscreenDialog: true,
),
),
);
await tester.pump();
await tester.pump(const Duration(milliseconds: 50));
}
void main() {
setUp(() {
SharedPreferences.setMockInitialValues({});
});
testWidgets('visual fidelity (audit 9.4): the date line renders between the '
'schedule pill and the hero time (t4:419)', (tester) async {
await _montarPantalla(tester);
final localeTag =
Localizations.localeOf(
tester.element(find.byType(PantallaAlarmaSonando)),
).toString();
final esperado = fechaLargaConDiaSemana(localeTag, DateTime.now());
expect(find.text(esperado), findsOneWidget);
// Order: pill above the date line, date line above the hero time.
final pillY =
tester
.getBottomLeft(find.byKey(const ValueKey('ringing-schedule-pill')))
.dy;
final dateY = tester.getTopLeft(find.text(esperado)).dy;
final timeY =
tester.getTopLeft(find.byKey(const ValueKey('ringing-hero-time'))).dy;
expect(pillY <= dateY, isTrue);
expect(dateY <= timeY, isTrue);
// Regression guard: pumpAndSettle must still complete (purely
// additive static text, no new animation).
await tester.pumpAndSettle();
});
testWidgets(
'visual fidelity (audit 9.10): the highlighted snooze tile carries a '
'"usual" qualifier ADDITIVELY -- the original flat label the dismiss '
'guard finds by text stays fully intact',
(tester) async {
await _montarPantalla(tester);
final l10n = AppLocalizations.of(
tester.element(find.byType(PantallaAlarmaSonando)),
);
// The ORIGINAL flat label must still resolve to exactly one Text --
// this IS the dismiss-guard's own finder contract.
expect(find.text(l10n.alarmSnoozeOptionLabel(10)), findsOneWidget);
// The additive qualifier renders too, but only on the destacado tile.
expect(find.text(l10n.alarmSnoozeUsualLabel), findsOneWidget);
expect(find.text(l10n.alarmSnoozeOptionLabel(3)), findsOneWidget);
expect(find.text(l10n.alarmSnoozeOptionLabel(5)), findsOneWidget);
// Regression guard: the exact hazard 9.9/9.10 share -- pumpAndSettle
// must still complete, and the flat label must remain the SAME
// widget the dismiss guard taps (still inside a FilledButton).
expect(
find.ancestor(
of: find.text(l10n.alarmSnoozeOptionLabel(10)),
matching: find.byType(FilledButton),
),
findsOneWidget,
);
await tester.pumpAndSettle();
},
);
}
@@ -400,6 +400,66 @@ void main() {
);
},
);
testWidgets('8.5: the "REPETIR" eyebrow sits above the weekday circles', (
tester,
) async {
await _abrirEditor(tester);
expect(find.text(l10n.alarmRepeatSectionLabel), findsOneWidget);
final eyebrowY =
tester.getBottomLeft(find.text(l10n.alarmRepeatSectionLabel)).dy;
final circlesY =
tester
.getTopLeft(find.byKey(const ValueKey('alarm-weekday-circles')))
.dy;
expect(eyebrowY <= circlesY, isTrue);
});
testWidgets(
'8.6: the station field, volume, fade-in and vacation toggle share '
'ONE grouped card',
(tester) async {
await _abrirEditor(tester);
final tarjeta = find.byKey(const ValueKey('alarm-editor-grouped-card'));
expect(tarjeta, findsOneWidget);
expect(
find.descendant(
of: tarjeta,
matching: find.byKey(const ValueKey('alarm-station-field')),
),
findsOneWidget,
);
expect(
find.descendant(of: tarjeta, matching: find.byType(Slider)),
findsNWidgets(2),
reason: 'volume + fade-in sliders both live inside the card',
);
expect(
find.descendant(of: tarjeta, matching: find.byType(SwitchListTile)),
findsOneWidget,
reason: 'the vacation toggle lives inside the card',
);
// Snooze duration is a deliberate EXTRA (audit 8.8), kept OUTSIDE
// the prototype-mandated 4-row card.
expect(
find.descendant(
of: tarjeta,
matching: find.text(l10n.alarmSnoozeDurationTitle),
),
findsNothing,
);
},
);
testWidgets('8.7: the volume row shows a live percentage label next to a '
'compact track', (tester) async {
await _abrirEditor(tester);
expect(find.text(l10n.alarmVolumeLabel), findsOneWidget);
expect(find.text('85%'), findsOneWidget);
});
});
group('WU8 — tarjeta de alarma simplificada', () {
@@ -0,0 +1,193 @@
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:pluriwave/estado/estado_alarmas.dart';
import 'package:pluriwave/estado/estado_radio.dart';
import 'package:pluriwave/l10n/gen/app_localizations.dart';
import 'package:pluriwave/modelos/alarma_musical.dart';
import 'package:pluriwave/modelos/emisora.dart';
import 'package:pluriwave/pantallas/pantalla_alarmas.dart';
import 'package:pluriwave/servicios/servicio_alarmas.dart';
import 'package:pluriwave/tema/pluriwave_tokens.dart';
import 'package:provider/provider.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../helpers/fakes.dart';
import '../helpers/fakes_alarmas.dart';
/// Tier 5 visual-fidelity closeout (items 7.1-7.4, audit id 2521) --
/// screens 4-14 batch.
void main() {
setUp(() {
SharedPreferences.setMockInitialValues({});
});
Future<void> montar(
WidgetTester tester, {
required EstadoAlarmas estadoAlarmas,
EstadoRadio? radio,
}) async {
tester.view.physicalSize = const Size(1440, 3200);
tester.view.devicePixelRatio = 1.0;
addTearDown(tester.view.resetPhysicalSize);
addTearDown(tester.view.resetDevicePixelRatio);
final estadoRadio =
radio ??
EstadoRadio(
audio: FakeServicioAudio(),
favoritos: FakeServicioFavoritos(),
radio: FakeServicioRadio(),
servicioEcualizador: FakeServicioEcualizador(),
servicioGrabacion: FakeServicioGrabacionRadioInactiva(),
iniciarAutomaticamente: false,
);
if (radio == null) addTearDown(estadoRadio.dispose);
await tester.pumpWidget(
MultiProvider(
providers: [
ChangeNotifierProvider<EstadoRadio>.value(value: estadoRadio),
ChangeNotifierProvider<EstadoAlarmas>.value(value: estadoAlarmas),
],
child: MaterialApp(
locale: const Locale('en'),
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
home: const Scaffold(body: PantallaAlarmas()),
),
),
);
await tester.pump();
await tester.pump(const Duration(milliseconds: 100));
}
EstadoAlarmas crearEstado() {
final android = FakePuertoAlarmasAndroid();
final estado = EstadoAlarmas(
servicio: ServicioAlarmas(reloj: () => DateTime(2026, 6, 11, 6, 0)),
android: android,
iniciarAutomaticamente: false,
);
return estado;
}
group('visual fidelity (audit 7.1): "New" action', () {
testWidgets('is a solid brand-teal pill with an add glyph, not the tonal '
'auto_awesome button (t4:325)', (tester) async {
final estado = crearEstado();
addTearDown(estado.dispose);
await montar(tester, estadoAlarmas: estado);
expect(find.byIcon(Icons.auto_awesome_rounded), findsNothing);
expect(find.byIcon(Icons.add_rounded), findsOneWidget);
final l10n = lookupAppLocalizations(const Locale('en'));
final boton = tester.widget<FilledButton>(find.byType(FilledButton));
final resuelto = boton.style?.backgroundColor?.resolve(<WidgetState>{});
expect(resuelto, PluriWaveTokens.brand);
expect(find.text(l10n.createAlarmAction), findsOneWidget);
});
});
group('visual fidelity (audit 7.2): next-alarm banner', () {
testWidgets(
'is warmCoral-tinted and the Skip chip sits BESIDE the text, not '
'below it (t4:327-331)',
(tester) async {
final estado = crearEstado();
addTearDown(estado.dispose);
await estado.guardarAlarma(
const AlarmaMusical(
id: 'proxima',
nombre: 'Despertar',
hora: 7,
minuto: 30,
tipoProgramacion: TipoProgramacionAlarma.diaria,
diasSemana: [],
),
);
await montar(tester, estadoAlarmas: estado);
final l10n = lookupAppLocalizations(const Locale('en'));
final banner = tester.widget<DecoratedBox>(
find.byKey(const ValueKey('next-alarm-banner')),
);
final decoration = banner.decoration as BoxDecoration;
expect(
decoration.color,
const Color(0xFFF4B860).withValues(alpha: 0.13),
);
final skipY = tester.getCenter(find.text(l10n.alarmHeroSkipAction)).dy;
final titleY = tester.getCenter(find.byIcon(Icons.alarm_on)).dy;
expect(
(skipY - titleY).abs() < 4,
isTrue,
reason: 'the Skip chip sits on the SAME row as the icon/text',
);
},
);
});
group('visual fidelity (audit 7.3): vacation row date pill', () {
testWidgets('shows the upcoming range as a "d-d MON" pill (t4:332)', (
tester,
) async {
final estado = crearEstado();
addTearDown(estado.dispose);
await estado.crearRangoVacaciones(
estado.servicio.crearRangoVacaciones(
inicio: DateTime(2026, 8, 4),
fin: DateTime(2026, 8, 18),
nombre: 'Summer',
),
);
await montar(tester, estadoAlarmas: estado);
expect(find.text('418 AUG'), findsOneWidget);
});
});
group('visual fidelity (audit 7.4): alarm card', () {
testWidgets(
'shows a recurrence label next to the time and a station icon next '
'to the station name (t4:337-345)',
(tester) async {
final estado = crearEstado();
addTearDown(estado.dispose);
await estado.guardarAlarma(
const AlarmaMusical(
id: 'tarjeta',
nombre: 'Despertar',
hora: 7,
minuto: 30,
tipoProgramacion: TipoProgramacionAlarma.diaria,
diasSemana: [],
emisora: Emisora(
uuid: 'e1',
nombre: 'Radio Uno',
url: 'https://radio.example/stream',
),
),
);
await montar(tester, estadoAlarmas: estado);
final l10n = lookupAppLocalizations(const Locale('en'));
expect(
find.descendant(
of: find.byKey(const ValueKey('tarjeta-alarma-tarjeta')),
matching: find.text(l10n.dailyOption),
),
findsOneWidget,
);
expect(
find.descendant(
of: find.byKey(const ValueKey('tarjeta-alarma-tarjeta')),
matching: find.byKey(const ValueKey('tarjeta-alarma-arte')),
),
findsOneWidget,
);
},
);
});
}
@@ -133,6 +133,42 @@ void main() {
},
);
testWidgets('visual fidelity (audit 14.3): the headline is 34px/w800/ls-1.2 '
'(t4 line 696), not headlineMedium\'s 28', (tester) async {
final navegacion = EstadoNavegacionRaiz();
addTearDown(navegacion.dispose);
await tester.pumpWidget(_app(navegacion: navegacion));
await tester.pumpAndSettle();
final headline = tester.widget<Text>(find.text('Tu mundo, en directo'));
expect(headline.style?.fontSize, 34);
expect(headline.style?.fontWeight, FontWeight.w800);
expect(headline.style?.letterSpacing, -1.2);
});
testWidgets('visual fidelity (audit 14.7): the CTA is a radius-18 rounded '
'rectangle (t4 line 715), not the default fully-round StadiumBorder', (
tester,
) async {
final navegacion = EstadoNavegacionRaiz();
addTearDown(navegacion.dispose);
await tester.pumpWidget(_app(navegacion: navegacion));
await tester.pumpAndSettle();
final boton = tester.widget<FilledButton>(find.byType(FilledButton));
final forma = boton.style?.shape?.resolve(<WidgetState>{});
expect(
forma,
isA<RoundedRectangleBorder>().having(
(s) => (s.borderRadius as BorderRadius).topLeft,
'topLeft radius',
const Radius.circular(18),
),
);
});
testWidgets('rendered content contains no monetization strings', (
tester,
) async {
@@ -89,4 +89,55 @@ void main() {
expect(find.byType(TarjetaEmisoraShimmer), findsWidgets);
expect(find.byType(CircularProgressIndicator), findsNothing);
});
testWidgets('visual fidelity (audit 13.3/13.4): the "BUSCANDO EMISORAS..." '
'eyebrow renders above the loading rows, spaced 4px apart (t4:649-651)', (
tester,
) async {
final busqueda = _BusquedaCargando();
addTearDown(busqueda.dispose);
final estado = EstadoRadio(
audio: FakeServicioAudio(),
favoritos: FakeServicioFavoritos(),
radio: FakeServicioRadio(),
servicioEcualizador: FakeServicioEcualizador(),
servicioGrabacion: FakeServicioGrabacionRadio(),
resolverArchivoCustom: () async => throw UnimplementedError(),
iniciarAutomaticamente: false,
);
addTearDown(estado.dispose);
await tester.pumpWidget(
MultiProvider(
providers: [
ChangeNotifierProvider<EstadoRadio>.value(value: estado),
ListenableProvider<EstadoEcualizador>.value(
value: estado.ecualizador,
),
ListenableProvider<EstadoGrabacion>.value(value: estado.grabacion),
ListenableProvider<EstadoBusqueda>.value(value: busqueda),
],
child: MaterialApp(
locale: const Locale('es'),
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
home: const Scaffold(body: PantallaBuscar()),
),
),
);
await tester.pump();
await tester.enterText(find.byType(SearchBar), 'jazz');
await tester.pump();
final l10n = AppLocalizations.of(
tester.element(find.byType(PantallaBuscar)),
);
expect(find.text(l10n.searchLoadingStationsLabel), findsOneWidget);
final filas = find.byType(TarjetaEmisoraShimmer);
expect(filas, findsWidgets);
final primeraAbajo = tester.getBottomLeft(filas.at(0)).dy;
final segundaArriba = tester.getTopLeft(filas.at(1)).dy;
expect(segundaArriba - primeraAbajo, 4);
});
}
+133
View File
@@ -181,6 +181,101 @@ void main() {
);
});
group('visual fidelity (audit 6.2/6.3/6.4)', () {
testWidgets(
'6.2: an active filter pill is brand-teal tinted with an inline '
'close glyph (t4:292-293)',
(tester) async {
_setLargeSurfaceSize(tester);
final estado = _crearEstado(
radio: FakeServicioRadio(
busqueda: [emisoraDemo(uuid: 'es-1', nombre: 'Radio Espana')],
),
);
addTearDown(estado.dispose);
await tester.runAsync(estado.inicializar);
await tester.pumpWidget(_conProviders(estado, _testApp()));
await _pumpStableFrame(tester);
final l10n = _l10nDe(tester);
await _abrirYSeleccionarPais(tester, l10n.countrySpain);
await _pumpStableFrame(tester);
final chip = tester.widget<Chip>(
find.ancestor(
of: find.text(l10n.countrySpain),
matching: find.byType(Chip),
),
);
expect(
chip.backgroundColor,
const Color(0xFF21D4D9).withValues(alpha: 0.2),
);
expect(
(chip.shape as RoundedRectangleBorder?)?.side.color,
const Color(0xFF21D4D9).withValues(alpha: 0.45),
);
},
);
testWidgets(
'6.3: an "Idioma" entry chip is always reachable once a search is '
'active, opening the filters sheet (t4:294-295)',
(tester) async {
_setLargeSurfaceSize(tester);
final estado = _crearEstado(
radio: FakeServicioRadio(
busqueda: [emisoraDemo(uuid: 'r-1', nombre: 'Radio Uno')],
),
);
addTearDown(estado.dispose);
await tester.runAsync(estado.inicializar);
await tester.pumpWidget(_conProviders(estado, _testApp()));
await _pumpStableFrame(tester);
await tester.enterText(find.byType(SearchBar), 'radio');
await tester.testTextInput.receiveAction(TextInputAction.done);
await _pumpStableFrame(tester);
final l10n = _l10nDe(tester);
expect(find.text(l10n.searchLanguageFilterLabel), findsOneWidget);
await tester.tap(find.text(l10n.searchLanguageFilterLabel));
await tester.pumpAndSettle();
expect(find.text(l10n.searchCountryFilterLabel), findsOneWidget);
},
);
testWidgets(
'6.4: the results-count eyebrow uses eyebrowLabel styling (t4:299)',
(tester) async {
_setLargeSurfaceSize(tester);
final estado = _crearEstado(
radio: FakeServicioRadio(
busqueda: [emisoraDemo(uuid: 'r-1', nombre: 'Radio Uno')],
),
);
addTearDown(estado.dispose);
await tester.runAsync(estado.inicializar);
await tester.pumpWidget(_conProviders(estado, _testApp()));
await _pumpStableFrame(tester);
await tester.enterText(find.byType(SearchBar), 'radio');
await tester.testTextInput.receiveAction(TextInputAction.done);
await _pumpStableFrame(tester);
final l10n = _l10nDe(tester);
final texto = tester.widget<Text>(
find.text(l10n.searchResultsCount(1)),
);
expect(texto.style?.fontSize, 11);
expect(texto.style?.fontWeight, FontWeight.w800);
},
);
});
group('Buscar — Ordenar (client-side, WU6)', () {
testWidgets(
'cada opcion renderizada de Ordenar corresponde a un caso real de '
@@ -774,6 +869,44 @@ void main() {
// Hazard: `pumpAndSettle()` never terminates while this card's rotating
// ring animates -- every assertion below uses a bounded `pump()` once
// `reconectando` is emitted, never `_pumpStableFrame`'s `pumpAndSettle`.
group('visual fidelity (audit 13.5/13.6)', () {
testWidgets(
'13.5/13.6: the no-results card quotes the query, and the "clear '
'filters" pill sits INSIDE the same card (t4:657-663)',
(tester) async {
_setLargeSurfaceSize(tester);
final estado = _crearEstado(radio: FakeServicioRadio(busqueda: []));
addTearDown(estado.dispose);
await tester.runAsync(estado.inicializar);
await tester.pumpWidget(_conProviders(estado, _testApp()));
await _pumpStableFrame(tester);
await _abrirYSeleccionarPais(tester, _l10nDe(tester).countrySpain);
await _pumpStableFrame(tester);
await tester.enterText(find.byType(SearchBar), 'jazzz');
await tester.testTextInput.receiveAction(TextInputAction.done);
await _pumpStableFrame(tester);
final l10n = _l10nDe(tester);
expect(
find.text(l10n.searchNoResultsForQueryTitle('jazzz')),
findsOneWidget,
);
final tarjeta = find.byKey(const ValueKey('search-no-results-card'));
expect(tarjeta, findsOneWidget);
expect(
find.descendant(
of: tarjeta,
matching: find.text(l10n.searchClearFiltersAction(1)),
),
findsOneWidget,
reason: 't4:667 the clear-filters pill sits INSIDE the card',
);
},
);
});
group('Item 25 -- reconnect card (audit 13.2)', () {
testWidgets(
'shows the station name, "Reconectando...", and a stop button while '
+36 -2
View File
@@ -357,7 +357,12 @@ void main() {
await tester.pumpWidget(buildScreen(estado));
await pumpStable(tester);
await tester.tap(find.text('Manage lists'));
// Audit 4.1 (t4:216): the manage-groups action moved into the
// header as a create_new_folder icon button, replacing the old
// "Manage lists" ActionChip in the chip strip.
await tester.tap(
find.byKey(const ValueKey('favorites-manage-groups-action')),
);
await pumpStable(tester);
// "Group Management Reachable from Favoritos": the SAME screen
@@ -385,7 +390,10 @@ void main() {
await tester.pumpAndSettle();
expect(find.byType(PantallaAjustesGruposFavoritos), findsNothing);
expect(find.text('Manage lists'), findsOneWidget);
expect(
find.byKey(const ValueKey('favorites-manage-groups-action')),
findsOneWidget,
);
await estado.crearGrupoFavoritos('Road trip');
await pumpStable(tester);
@@ -494,6 +502,32 @@ void main() {
},
);
});
group('visual fidelity (audit 4.2)', () {
testWidgets('the active group chip is solid brand teal with dark text; '
'inactive chips use listSurface (t4:219-221)', (tester) async {
setLargeSurface(tester);
_suppressListTileInkAssertion();
final estado = await crearEstadoConFavoritos();
addTearDown(estado.dispose);
await tester.pumpWidget(buildScreen(estado));
await pumpStable(tester);
final activo = tester.widget<ChoiceChip>(
find.widgetWithText(ChoiceChip, 'All · 3'),
);
expect(activo.selected, isTrue);
expect(activo.selectedColor, const Color(0xFF21D4D9));
expect(activo.labelStyle?.color, const Color(0xFF062126));
final inactivo = tester.widget<ChoiceChip>(
find.widgetWithText(ChoiceChip, 'Rock · 2'),
);
expect(inactivo.selected, isFalse);
expect(inactivo.backgroundColor, const Color(0xFF102532));
});
});
}
void setLargeSurface(WidgetTester tester) {
@@ -173,6 +173,49 @@ void main() {
expect(find.text('84 MB of 200 MB used'), findsOneWidget);
});
testWidgets(
'visual fidelity (audit 12.2/12.3): the storage card shows the bold '
'"used of total" headline ABOVE the bar, then a folder-path + purge '
'caption below it (t4 line 613)',
(tester) async {
final estado = EstadoGrabacion(
servicio: _FakeServicioGrabacionConArchivos([
fijaA,
fijaB,
fijaC,
], maxBytesFijo: 200 * 1024 * 1024),
);
addTearDown(estado.dispose);
await tester.pumpWidget(
buildScreen(
estado: estado,
reproductor: _ReproductorGrabacionesFake(const {}),
),
);
await pumpStable(tester);
final headline = tester.widget<Text>(find.text('84 MB of 200 MB used'));
expect(headline.style?.fontWeight, FontWeight.w800);
expect(
find.text('Music/PluriWave · purges oldest at limit'),
findsOneWidget,
);
final headlineY = tester.getTopLeft(find.text('84 MB of 200 MB used')).dy;
final barY = tester.getTopLeft(find.byType(LinearProgressIndicator)).dy;
final captionY =
tester
.getTopLeft(find.text('Music/PluriWave · purges oldest at limit'))
.dy;
expect(
headlineY < barY && barY < captionY,
isTrue,
reason: 'headline, then bar, then caption -- top to bottom',
);
},
);
testWidgets('15.2-A: 3 recording fixtures render as 3 rows', (tester) async {
final estado = EstadoGrabacion(
servicio: _FakeServicioGrabacionConArchivos([
+64 -1
View File
@@ -4,6 +4,7 @@ import 'package:pluriwave/estado/estado_busqueda.dart';
import 'package:pluriwave/l10n/gen/app_localizations.dart';
import 'package:pluriwave/modelos/pais_radio.dart';
import 'package:pluriwave/pantallas/pantalla_paises.dart';
import 'package:pluriwave/widgets/pluri_glass_surface.dart';
import 'package:pluriwave/widgets/pluri_push_scaffold.dart';
import 'package:provider/provider.dart';
@@ -62,7 +63,9 @@ void main() {
tester.element(find.byType(PantallaPaises)),
);
expect(find.text(l10n.countriesYourLanguagesTitle), findsOneWidget);
expect(find.text(l10n.countriesAllTitle), findsOneWidget);
// Audit 5.4: "Todos" now carries the total count as part of the
// same eyebrow string ("{title} · {count}"), not a bare title.
expect(find.textContaining(l10n.countriesAllTitle), findsOneWidget);
// Full alphabetical list: all 3 fetched countries render.
expect(find.text('Argentina'), findsOneWidget);
@@ -201,6 +204,66 @@ void main() {
);
});
group('visual fidelity (audit 5.4/5.7)', () {
testWidgets(
'the section eyebrows sit OUTSIDE any card, styled as an eyebrow '
'label, and "Todos" carries the total country count (t4:254/260)',
(tester) async {
final estado = EstadoBusqueda(
radio: FakeServicioRadio(
paises: const [
PaisRadio(nombre: 'Spain', codigoIso: 'ES', numeroEmisoras: 482),
PaisRadio(
nombre: 'Argentina',
codigoIso: 'AR',
numeroEmisoras: 120,
),
PaisRadio(nombre: 'France', codigoIso: 'FR', numeroEmisoras: 75),
],
),
);
addTearDown(estado.dispose);
await tester.pumpWidget(buildScreen(estado));
await pumpEstable(tester);
final l10n = AppLocalizations.of(
tester.element(find.byType(PantallaPaises)),
);
final eyebrow = tester.widget<Text>(
find.text(l10n.countriesYourLanguagesTitle),
);
expect(eyebrow.style?.fontSize, 11);
expect(eyebrow.style?.fontWeight, FontWeight.w800);
expect(
find.ancestor(
of: find.text(l10n.countriesYourLanguagesTitle),
matching: find.byType(PluriGlassSurface),
),
findsNothing,
reason: 'the eyebrow must not be a descendant of any card',
);
expect(
find.textContaining('${l10n.countriesAllTitle} · 3'),
findsOneWidget,
);
},
);
testWidgets('the header carries a search action (t4:252)', (tester) async {
final estado = EstadoBusqueda(radio: FakeServicioRadio(paises: const []));
addTearDown(estado.dispose);
await tester.pumpWidget(buildScreen(estado));
await pumpEstable(tester);
expect(find.byIcon(Icons.search_rounded), findsOneWidget);
});
});
testWidgets(
'cargarPaises no se re-dispara si ya hay datos en caché al reconstruir',
(tester) async {
+103 -1
View File
@@ -180,7 +180,8 @@ void main() {
});
testWidgets(
'los rangos pasados aparecen bajo el encabezado "Rangos pasados"',
'los rangos pasados aparecen bajo el encabezado "Rangos pasados" tras '
'expandir la fila (audit 9b.7: colapsada por defecto, t4:489)',
(tester) async {
final estado = await _crearEstado(
vacaciones: [
@@ -201,6 +202,10 @@ void main() {
tester.element(find.byType(PantallaVacaciones)),
);
expect(find.text(l10n.vacationPastSectionTitle), findsOneWidget);
await tester.tap(find.text(l10n.vacationPastSectionTitle));
await _pumpEstable(tester);
expect(find.text('Rango viejo'), findsOneWidget);
},
);
@@ -343,4 +348,101 @@ void main() {
);
},
);
group('visual fidelity (audit 9b.1/9b.2/9b.6/9b.7)', () {
testWidgets(
'9b.1: a header "Add" action is reachable and opens the same form '
'as the CTA (t4:446)',
(tester) async {
final estado = await _crearEstado();
addTearDown(estado.dispose);
await tester.pumpWidget(_buildScreen(estado));
await _pumpEstable(tester);
final l10n = AppLocalizations.of(
tester.element(find.byType(PantallaVacaciones)),
);
final appBar = tester.widget<AppBar>(find.byType(AppBar));
expect(appBar.actions, isNotNull);
expect(appBar.actions, isNotEmpty);
await tester.tap(find.byKey(const ValueKey('vacation-add-header')));
await _pumpEstable(tester);
expect(find.text(l10n.newVacationRangeTitle), findsOneWidget);
},
);
testWidgets(
'9b.2: the explanatory info banner is always visible (t4:448)',
(tester) async {
final estado = await _crearEstado();
addTearDown(estado.dispose);
await tester.pumpWidget(_buildScreen(estado));
await _pumpEstable(tester);
final l10n = AppLocalizations.of(
tester.element(find.byType(PantallaVacaciones)),
);
expect(find.text(l10n.vacationExplainerBanner), findsOneWidget);
},
);
testWidgets(
'9b.6: the bottom CTA has a dashed border and a date_range icon '
'(t4:487)',
(tester) async {
final estado = await _crearEstado();
addTearDown(estado.dispose);
await tester.pumpWidget(_buildScreen(estado));
await _pumpEstable(tester);
expect(find.byIcon(Icons.date_range_rounded), findsOneWidget);
// Exactly ONE add_rounded on the whole screen -- the header's OWN
// "Add" action (audit 9b.1). The bottom CTA no longer uses it.
expect(find.byIcon(Icons.add_rounded), findsOneWidget);
},
);
testWidgets(
'9b.7: "Rangos pasados" is collapsed by default, showing a count, '
'and expands on tap (t4:489)',
(tester) async {
final estado = await _crearEstado(
vacaciones: [
RangoVacaciones(
id: 'pa1',
nombre: 'Rango viejo',
inicio: _hoyDia.subtract(const Duration(days: 30)),
fin: _hoyDia.subtract(const Duration(days: 20)),
),
],
);
addTearDown(estado.dispose);
await tester.pumpWidget(_buildScreen(estado));
await _pumpEstable(tester);
final l10n = AppLocalizations.of(
tester.element(find.byType(PantallaVacaciones)),
);
expect(find.text(l10n.vacationPastSectionTitle), findsOneWidget);
expect(find.text('1'), findsOneWidget);
expect(
find.text('Rango viejo'),
findsNothing,
reason:
'collapsed by default -- matches the prototype count-only row',
);
await tester.tap(find.text(l10n.vacationPastSectionTitle));
await _pumpEstable(tester);
expect(find.text('Rango viejo'), findsOneWidget);
},
);
});
}
+26
View File
@@ -136,4 +136,30 @@ void main() {
'EQ bands with brand teal #21D4D9 (t4 line 585)',
);
});
testWidgets(
'visual fidelity (audit 11.5): the thumb is a 20x20 custom glow shape, '
'not the Material default (t4 line 585)',
(tester) async {
await tester.pumpWidget(buildWidget());
final tema = SliderTheme.of(tester.element(find.byType(Slider).first));
final size = tema.thumbShape?.getPreferredSize(true, false);
expect(
size,
const Size(20, 20),
reason: 't4 line 585: a 20x20 thumb with a 14px brand-teal glow',
);
},
);
testWidgets('visual fidelity (audit 11.7): the dB label is brand teal, not '
'liveGreen (t4 line 584)', (tester) async {
await tester.pumpWidget(buildWidget());
final label = tester.widget<Text>(find.text('0.0dB').first);
expect(label.style?.color, PluriWaveTokens.brand.withValues(alpha: 0.9));
expect(label.style?.color, isNot(PluriWaveTokens.dark.liveGreen));
});
}