fix(i18n): normalize translations and fallbacks
This commit is contained in:
@@ -11,7 +11,7 @@ import 'package:uuid/uuid.dart';
|
||||
|
||||
import '../estado/estado_idioma.dart';
|
||||
import '../estado/estado_radio.dart';
|
||||
import '../l10n/app_localizations_ext.dart';
|
||||
import '../l10n/display_names.dart';
|
||||
import '../l10n/gen/app_localizations.dart';
|
||||
import '../modelos/emisora.dart';
|
||||
import '../modelos/grupo_favoritos.dart';
|
||||
@@ -291,7 +291,7 @@ class _SeccionTimerSueno extends StatelessWidget {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
'${l10n.saveQuickAccessButton}: ${_formatearDuracionTimer(duracion)}',
|
||||
'${l10n.saveQuickAccessButton}: ${_formatearDuracionTimer(l10n, duracion)}',
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -336,7 +336,10 @@ class _SeccionTimerSueno extends StatelessWidget {
|
||||
for (final segundos in presets)
|
||||
InputChip(
|
||||
label: Text(
|
||||
_formatearDuracionTimer(Duration(seconds: segundos)),
|
||||
_formatearDuracionTimer(
|
||||
l10n,
|
||||
Duration(seconds: segundos),
|
||||
),
|
||||
),
|
||||
onDeleted:
|
||||
presets.length <= 1
|
||||
@@ -907,7 +910,7 @@ class _SeccionEmisoraPreferida extends StatelessWidget {
|
||||
DropdownMenuItem<String>(
|
||||
value: emisora.uuid,
|
||||
child: Text(
|
||||
emisora.nombre,
|
||||
localizedStationName(l10n, emisora.nombre),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
@@ -923,8 +926,12 @@ class _SeccionEmisoraPreferida extends StatelessWidget {
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
favoritas.any((e) => e.uuid == preferida.uuid)
|
||||
? l10n.preferredStationCurrent(preferida.nombre)
|
||||
: l10n.preferredStationAutoUsing(preferida.nombre),
|
||||
? l10n.preferredStationCurrent(
|
||||
localizedStationName(l10n, preferida.nombre),
|
||||
)
|
||||
: l10n.preferredStationAutoUsing(
|
||||
localizedStationName(l10n, preferida.nombre),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Align(
|
||||
@@ -1001,7 +1008,12 @@ class _SeccionEmisoras extends StatelessWidget {
|
||||
ListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
leading: const Icon(Icons.radio),
|
||||
title: Text(emisora.nombre),
|
||||
title: Text(
|
||||
localizedStationName(
|
||||
AppLocalizations.of(context),
|
||||
emisora.nombre,
|
||||
),
|
||||
),
|
||||
subtitle: Text(
|
||||
emisora.url,
|
||||
maxLines: 1,
|
||||
@@ -1179,11 +1191,7 @@ class _SeccionBackup extends StatelessWidget {
|
||||
} catch (e) {
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
l10n.backupExportError(e.toString()),
|
||||
),
|
||||
),
|
||||
SnackBar(content: Text(l10n.backupExportError(e.toString()))),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1229,20 +1237,14 @@ class _SeccionBackup extends StatelessWidget {
|
||||
final messenger = ScaffoldMessenger.of(context);
|
||||
await estado.importarConfig(json);
|
||||
messenger.showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(l10n.backupImportSuccess),
|
||||
),
|
||||
SnackBar(content: Text(l10n.backupImportSuccess)),
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
l10n.backupImportError(e.toString()),
|
||||
),
|
||||
),
|
||||
SnackBar(content: Text(l10n.backupImportError(e.toString()))),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1324,7 +1326,8 @@ class _SeccionInfo extends StatelessWidget {
|
||||
AppLocalizations.of(ctx).savedFavoritesTitle,
|
||||
),
|
||||
trailing: Text(
|
||||
snap.data?.toString() ?? AppLocalizations.of(ctx).dash,
|
||||
snap.data?.toString() ??
|
||||
AppLocalizations.of(ctx).dash,
|
||||
style: Theme.of(ctx).textTheme.bodyLarge,
|
||||
),
|
||||
),
|
||||
@@ -1362,15 +1365,27 @@ class _SeccionInfo extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
String _formatearDuracionTimer(Duration duracion) {
|
||||
String _formatearDuracionTimer(
|
||||
AppLocalizations l10n,
|
||||
Duration duracion,
|
||||
) {
|
||||
final horas = duracion.inHours;
|
||||
final minutos = duracion.inMinutes.remainder(60);
|
||||
final segundos = duracion.inSeconds.remainder(60);
|
||||
if (horas > 0) {
|
||||
return '${horas}h ${minutos.toString().padLeft(2, '0')}m ${segundos.toString().padLeft(2, '0')}s';
|
||||
return l10n.durationHoursMinutesSeconds(
|
||||
horas,
|
||||
minutos.toString().padLeft(2, '0'),
|
||||
segundos.toString().padLeft(2, '0'),
|
||||
);
|
||||
}
|
||||
if (minutos > 0) {
|
||||
return segundos == 0 ? '$minutos min' : '${minutos}m ${segundos}s';
|
||||
return segundos == 0
|
||||
? l10n.durationMinutesOnly(minutos)
|
||||
: l10n.durationMinutesSeconds(
|
||||
minutos,
|
||||
segundos.toString().padLeft(2, '0'),
|
||||
);
|
||||
}
|
||||
return '$segundos s';
|
||||
return l10n.durationSecondsOnly(segundos);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import 'dart:async';
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:just_audio/just_audio.dart';
|
||||
@@ -6,7 +6,7 @@ import 'package:provider/provider.dart';
|
||||
|
||||
import '../estado/estado_alarmas.dart';
|
||||
import '../estado/estado_radio.dart';
|
||||
import '../l10n/app_localizations_ext.dart';
|
||||
import '../l10n/display_names.dart';
|
||||
import '../l10n/gen/app_localizations.dart';
|
||||
import '../modelos/alarma_musical.dart';
|
||||
import '../servicios/servicio_audio.dart';
|
||||
@@ -183,7 +183,7 @@ class _PantallaAlarmaSonandoState extends State<PantallaAlarmaSonando> {
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
alarma.nombre,
|
||||
localizedAlarmName(l10n, alarma.nombre),
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
),
|
||||
@@ -213,11 +213,10 @@ class _PantallaAlarmaSonandoState extends State<PantallaAlarmaSonando> {
|
||||
}
|
||||
|
||||
String _assetFallback(SonidoInternoAlarma sonido) => switch (sonido) {
|
||||
SonidoInternoAlarma.amanecer => 'assets/audio/alarm_amanecer.wav',
|
||||
SonidoInternoAlarma.campanaSuave =>
|
||||
'assets/audio/alarm_campana_suave.wav',
|
||||
SonidoInternoAlarma.pulsoDigital => 'assets/audio/alarm_pulso_digital.wav',
|
||||
};
|
||||
SonidoInternoAlarma.amanecer => 'assets/audio/alarm_amanecer.wav',
|
||||
SonidoInternoAlarma.campanaSuave => 'assets/audio/alarm_campana_suave.wav',
|
||||
SonidoInternoAlarma.pulsoDigital => 'assets/audio/alarm_pulso_digital.wav',
|
||||
};
|
||||
|
||||
String _hora(AlarmaMusical alarma) =>
|
||||
'${alarma.hora.toString().padLeft(2, '0')}:${alarma.minuto.toString().padLeft(2, '0')}';
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'package:provider/provider.dart';
|
||||
|
||||
import '../estado/estado_alarmas.dart';
|
||||
import '../estado/estado_radio.dart';
|
||||
import '../l10n/display_names.dart';
|
||||
import '../l10n/app_localizations_ext.dart';
|
||||
import '../l10n/gen/app_localizations.dart';
|
||||
import '../modelos/alarma_musical.dart';
|
||||
@@ -83,9 +84,10 @@ class _PanelProximaAlarma extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context);
|
||||
final proxima = estado.proximaAlarma;
|
||||
final activasSinProxima = estado.alarmas
|
||||
.where((a) => a.activa && a.proximaProgramable == null)
|
||||
.length;
|
||||
final activasSinProxima =
|
||||
estado.alarmas
|
||||
.where((a) => a.activa && a.proximaProgramable == null)
|
||||
.length;
|
||||
final proximaProgramable = proxima?.proximaProgramable;
|
||||
|
||||
return PluriGlassSurface(
|
||||
@@ -102,24 +104,21 @@ class _PanelProximaAlarma extends StatelessWidget {
|
||||
proxima == null
|
||||
? activasSinProxima > 0
|
||||
? l10n.activeAlarmsWithoutNextTitle
|
||||
: l10n.activeAlarmsNoneTitle
|
||||
: l10n.noActiveAlarms
|
||||
: l10n.nextAlarmTitle,
|
||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.w900,
|
||||
),
|
||||
fontWeight: FontWeight.w900,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
proxima == null
|
||||
? activasSinProxima > 0
|
||||
? l10n.activeAlarmsWithoutNextSubtitle(
|
||||
activasSinProxima,
|
||||
)
|
||||
activasSinProxima,
|
||||
)
|
||||
: l10n.createAlarmHint
|
||||
: l10n.alarmNextSummary(
|
||||
proxima.nombre,
|
||||
_fechaHora(l10n, proximaProgramable!),
|
||||
),
|
||||
: '${_nombreVisibleAlarma(l10n, proxima)} · ${_fechaHora(l10n, proximaProgramable!)}',
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -166,7 +165,7 @@ class _TarjetaAlarma extends StatelessWidget {
|
||||
letterSpacing: -1,
|
||||
),
|
||||
),
|
||||
Text(alarma.nombre),
|
||||
Text(_nombreVisibleAlarma(l10n, alarma)),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -198,7 +197,7 @@ class _TarjetaAlarma extends StatelessWidget {
|
||||
),
|
||||
_InfoChip(
|
||||
icon: Icons.trending_up_rounded,
|
||||
label: l10n.fadeInSeconds(alarma.fadeInSegundos),
|
||||
label: l10n.alarmFadeInLabel(alarma.fadeInSegundos),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -263,11 +262,11 @@ class _TarjetaAlarma extends StatelessWidget {
|
||||
actualizada?.proximaProgramable == null
|
||||
? l10n.alarmSkippedNoNextSnackbar
|
||||
: l10n.alarmSkippedReturnsSnackbar(
|
||||
_fechaHora(
|
||||
l10n,
|
||||
actualizada!.proximaProgramable!,
|
||||
),
|
||||
_fechaHora(
|
||||
l10n,
|
||||
actualizada!.proximaProgramable!,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -302,10 +301,12 @@ class _TarjetaAlarma extends StatelessWidget {
|
||||
}
|
||||
if (actual != null) {
|
||||
if (alarma.proximaProgramable == null) {
|
||||
return l10n.alarmVacationPausedNoNext(actual.nombre);
|
||||
return l10n.alarmVacationPausedNoNext(
|
||||
_nombreVisibleVacaciones(l10n, actual),
|
||||
);
|
||||
}
|
||||
return l10n.alarmVacationPausedReturns(
|
||||
actual.nombre,
|
||||
_nombreVisibleVacaciones(l10n, actual),
|
||||
_fechaHora(l10n, alarma.proximaProgramable!),
|
||||
);
|
||||
}
|
||||
@@ -357,7 +358,10 @@ class _EditorAlarmaSheetState extends State<_EditorAlarmaSheet> {
|
||||
final l10n = AppLocalizations.of(context);
|
||||
final ahora = DateTime.now().add(const Duration(minutes: 5));
|
||||
_nombreController = TextEditingController(
|
||||
text: alarma?.nombre ?? l10n.defaultAlarmName,
|
||||
text:
|
||||
alarma == null
|
||||
? l10n.defaultAlarmName
|
||||
: _nombreVisibleAlarma(l10n, alarma),
|
||||
);
|
||||
_hora = TimeOfDay(
|
||||
hour: alarma?.hora ?? ahora.hour,
|
||||
@@ -419,7 +423,9 @@ class _EditorAlarmaSheetState extends State<_EditorAlarmaSheet> {
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text(
|
||||
widget.alarma == null ? l10n.newAlarmTitle : l10n.editAlarmTitle,
|
||||
widget.alarma == null
|
||||
? l10n.newAlarmTitle
|
||||
: l10n.editAlarmTitle,
|
||||
style: Theme.of(context).textTheme.titleLarge?.copyWith(
|
||||
fontWeight: FontWeight.w900,
|
||||
),
|
||||
@@ -442,7 +448,7 @@ class _EditorAlarmaSheetState extends State<_EditorAlarmaSheet> {
|
||||
Expanded(
|
||||
child: _PickerButton(
|
||||
icon: Icons.schedule_rounded,
|
||||
label: l10n.timeLabel,
|
||||
label: l10n.timeField,
|
||||
value: _hora.format(context),
|
||||
onTap: _elegirHora,
|
||||
),
|
||||
@@ -451,7 +457,7 @@ class _EditorAlarmaSheetState extends State<_EditorAlarmaSheet> {
|
||||
Expanded(
|
||||
child: _PickerButton(
|
||||
icon: Icons.event_rounded,
|
||||
label: l10n.dateLabel,
|
||||
label: l10n.dateField,
|
||||
value: _fechaCorta(_fecha),
|
||||
onTap:
|
||||
_tipo == TipoProgramacionAlarma.unica
|
||||
@@ -488,7 +494,7 @@ class _EditorAlarmaSheetState extends State<_EditorAlarmaSheet> {
|
||||
children: [
|
||||
for (var i = DateTime.monday; i <= DateTime.sunday; i++)
|
||||
FilterChip(
|
||||
label: Text(l10n.weekdayShort(i)),
|
||||
label: Text(_weekdayShort(l10n, i)),
|
||||
selected: _diasSemana.contains(i),
|
||||
onSelected:
|
||||
(selected) => setState(() {
|
||||
@@ -503,7 +509,7 @@ class _EditorAlarmaSheetState extends State<_EditorAlarmaSheet> {
|
||||
const SizedBox(height: 14),
|
||||
_SectionLabel(
|
||||
icon: 'assets/icons/alarmas/fallback_sound.png',
|
||||
text: l10n.soundAndVolumeTitle,
|
||||
text: l10n.soundAndVolumeSection,
|
||||
),
|
||||
Slider(
|
||||
value: _volumen,
|
||||
@@ -520,7 +526,7 @@ class _EditorAlarmaSheetState extends State<_EditorAlarmaSheet> {
|
||||
subtitle: Text(
|
||||
_fadeInSegundos == 0
|
||||
? l10n.alarmFadeInOff
|
||||
: l10n.alarmFadeInProgress(_fadeInSegundos),
|
||||
: l10n.alarmFadeInSummary(_fadeInSegundos),
|
||||
),
|
||||
),
|
||||
Slider(
|
||||
@@ -530,13 +536,12 @@ class _EditorAlarmaSheetState extends State<_EditorAlarmaSheet> {
|
||||
divisions: 60,
|
||||
label: '${_fadeInSegundos}s',
|
||||
onChanged:
|
||||
(value) =>
|
||||
setState(() => _fadeInSegundos = value.round()),
|
||||
(value) => setState(() => _fadeInSegundos = value.round()),
|
||||
),
|
||||
DropdownButtonFormField<SonidoInternoAlarma>(
|
||||
initialValue: _sonidoInterno,
|
||||
decoration: InputDecoration(
|
||||
labelText: l10n.soundInternalSafe,
|
||||
labelText: l10n.internalSafeSoundLabel,
|
||||
),
|
||||
items: [
|
||||
DropdownMenuItem(
|
||||
@@ -574,7 +579,7 @@ class _EditorAlarmaSheetState extends State<_EditorAlarmaSheet> {
|
||||
DropdownMenuItem<String>(
|
||||
value: emisora.uuid,
|
||||
child: Text(
|
||||
emisora.nombre,
|
||||
localizedStationName(l10n, emisora.nombre),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
@@ -608,7 +613,8 @@ class _EditorAlarmaSheetState extends State<_EditorAlarmaSheet> {
|
||||
SwitchListTile.adaptive(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
value: _sonarEnVacaciones,
|
||||
onChanged: (value) => setState(() => _sonarEnVacaciones = value),
|
||||
onChanged:
|
||||
(value) => setState(() => _sonarEnVacaciones = value),
|
||||
secondary: const _AssetIcon(
|
||||
'assets/icons/alarmas/vacation_wave.png',
|
||||
size: 42,
|
||||
@@ -648,7 +654,9 @@ class _EditorAlarmaSheetState extends State<_EditorAlarmaSheet> {
|
||||
Future<void> _guardar() async {
|
||||
if (_tipo == TipoProgramacionAlarma.diasSemana && _diasSemana.isEmpty) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(AppLocalizations.of(context).chooseOneWeekdayError)),
|
||||
SnackBar(
|
||||
content: Text(AppLocalizations.of(context).chooseOneWeekdayError),
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -711,9 +719,18 @@ class _AccesoDiagnostico extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context);
|
||||
final diag = estado.diagnostico;
|
||||
final exactStatus = diag?.puedeProgramarExactas == true ? l10n.statusOk : l10n.statusPending;
|
||||
final notificationStatus = diag?.notificacionesPermitidas == true ? l10n.statusOk : l10n.statusPending;
|
||||
final screenStatus = diag?.puedeUsarPantallaCompleta == true ? l10n.statusOk : l10n.statusPending;
|
||||
final exactStatus =
|
||||
diag?.puedeProgramarExactas == true
|
||||
? l10n.statusOk
|
||||
: l10n.statusPending;
|
||||
final notificationStatus =
|
||||
diag?.notificacionesPermitidas == true
|
||||
? l10n.statusOk
|
||||
: l10n.statusPending;
|
||||
final screenStatus =
|
||||
diag?.puedeUsarPantallaCompleta == true
|
||||
? l10n.statusOk
|
||||
: l10n.statusPending;
|
||||
return TextButton.icon(
|
||||
icon: const _AssetIcon(
|
||||
'assets/icons/alarmas/android_reliability.png',
|
||||
@@ -723,10 +740,10 @@ class _AccesoDiagnostico extends StatelessWidget {
|
||||
diag == null
|
||||
? l10n.androidReliabilityTitle
|
||||
: l10n.androidReliabilityStatus(
|
||||
exactStatus,
|
||||
notificationStatus,
|
||||
screenStatus,
|
||||
),
|
||||
exactStatus,
|
||||
notificationStatus,
|
||||
screenStatus,
|
||||
),
|
||||
),
|
||||
onPressed: () async {
|
||||
if (diag != null && !diag.puedeProgramarExactas) {
|
||||
@@ -784,18 +801,18 @@ class _PanelVacaciones extends StatelessWidget {
|
||||
const SizedBox(height: 8),
|
||||
Text(l10n.vacationRangesHint),
|
||||
if (vacaciones.isEmpty)
|
||||
Text(l10n.vacationRangesEmpty)
|
||||
Text(l10n.noVacationRangesLoaded)
|
||||
else
|
||||
for (final rango in vacaciones)
|
||||
ListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
leading: const Icon(Icons.event_busy_rounded),
|
||||
title: Text(rango.nombre),
|
||||
title: Text(_nombreVisibleVacaciones(l10n, rango)),
|
||||
subtitle: Text(
|
||||
'${_fechaCorta(rango.inicioDia)} → ${_fechaCorta(rango.finDia)}',
|
||||
),
|
||||
trailing: IconButton(
|
||||
tooltip: l10n.deleteRangeAction,
|
||||
tooltip: l10n.deleteRangeTooltip,
|
||||
onPressed: () => estado.eliminarRangoVacaciones(rango.id),
|
||||
icon: const Icon(Icons.delete_outline_rounded),
|
||||
),
|
||||
@@ -1057,23 +1074,42 @@ class _EmptyAlarmas extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
String _nombreVisibleAlarma(AppLocalizations l10n, AlarmaMusical alarma) {
|
||||
return localizedAlarmName(l10n, alarma.nombre);
|
||||
}
|
||||
|
||||
String _nombreVisibleVacaciones(AppLocalizations l10n, RangoVacaciones rango) {
|
||||
return localizedVacationName(l10n, rango.nombre);
|
||||
}
|
||||
|
||||
String _hora(AlarmaMusical alarma) =>
|
||||
'${alarma.hora.toString().padLeft(2, '0')}:${alarma.minuto.toString().padLeft(2, '0')}';
|
||||
|
||||
String _programacion(AppLocalizations l10n, AlarmaMusical alarma) {
|
||||
return switch (alarma.tipoProgramacion) {
|
||||
TipoProgramacionAlarma.unica =>
|
||||
l10n.alarmScheduleOnce(_fechaCorta(alarma.fechaUnica ?? DateTime.now())),
|
||||
TipoProgramacionAlarma.unica => l10n.alarmScheduleOnce(
|
||||
_fechaCorta(alarma.fechaUnica ?? DateTime.now()),
|
||||
),
|
||||
TipoProgramacionAlarma.diaria => l10n.dailyOption,
|
||||
TipoProgramacionAlarma.diasSemana =>
|
||||
l10n.alarmScheduleWeekdays(
|
||||
alarma.diasSemana.map(l10n.weekdayShort).join(', '),
|
||||
),
|
||||
TipoProgramacionAlarma.diasSemana => l10n.alarmScheduleWeekdays(
|
||||
alarma.diasSemana.map((day) => _weekdayShort(l10n, day)).join(', '),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
String _fechaHora(AppLocalizations l10n, DateTime fecha) =>
|
||||
l10n.dateTimeSentence(fecha);
|
||||
|
||||
String _weekdayShort(AppLocalizations l10n, int day) => switch (day) {
|
||||
DateTime.monday => l10n.weekdayShortMonday,
|
||||
DateTime.tuesday => l10n.weekdayShortTuesday,
|
||||
DateTime.wednesday => l10n.weekdayShortWednesday,
|
||||
DateTime.thursday => l10n.weekdayShortThursday,
|
||||
DateTime.friday => l10n.weekdayShortFriday,
|
||||
DateTime.saturday => l10n.weekdayShortSaturday,
|
||||
DateTime.sunday => l10n.weekdayShortSunday,
|
||||
_ => '?',
|
||||
};
|
||||
|
||||
String _fechaCorta(DateTime fecha) =>
|
||||
'${fecha.day.toString().padLeft(2, '0')}/${fecha.month.toString().padLeft(2, '0')}/${fecha.year}';
|
||||
|
||||
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../estado/estado_radio.dart';
|
||||
import '../l10n/display_names.dart';
|
||||
import '../l10n/gen/app_localizations.dart';
|
||||
import '../modelos/emisora.dart';
|
||||
import '../modelos/grupo_favoritos.dart';
|
||||
@@ -212,10 +213,14 @@ class _FavoritoItem extends StatelessWidget {
|
||||
);
|
||||
if (!context.mounted) return;
|
||||
final destino = grupos.firstWhere((g) => g.id == seleccionado);
|
||||
final stationName = localizedStationName(l10n, emisora.nombre);
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
l10n.favoriteGroupsAssigned(emisora.nombre, _nombreVisible(l10n, destino)),
|
||||
l10n.favoriteGroupsAssigned(
|
||||
stationName,
|
||||
_nombreVisible(l10n, destino),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -224,11 +229,12 @@ class _FavoritoItem extends StatelessWidget {
|
||||
Future<void> _eliminar(BuildContext context) async {
|
||||
final l10n = AppLocalizations.of(context);
|
||||
final estado = context.read<EstadoRadio>();
|
||||
final stationName = localizedStationName(l10n, emisora.nombre);
|
||||
await estado.favoritos.eliminar(emisora.uuid);
|
||||
await estado.cargarFavoritos();
|
||||
if (!context.mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(l10n.favoritesRemovedMessage(emisora.nombre))),
|
||||
SnackBar(content: Text(l10n.favoritesRemovedMessage(stationName))),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@ import 'package:provider/provider.dart';
|
||||
import 'package:shimmer/shimmer.dart' as shimmer;
|
||||
|
||||
import '../estado/estado_radio.dart';
|
||||
import '../l10n/app_localizations_ext.dart';
|
||||
import '../l10n/gen/app_localizations.dart';
|
||||
import '../widgets/pluri_glass_surface.dart';
|
||||
import '../widgets/pluri_icon.dart';
|
||||
@@ -56,7 +55,12 @@ class _PantallaInicioState extends State<PantallaInicio> {
|
||||
if (estado.error != null)
|
||||
SliverToBoxAdapter(child: _errorBanner(estado, theme, l10n)),
|
||||
SliverPadding(
|
||||
padding: const EdgeInsets.fromLTRB(PluriLayout.horizontal, 0, PluriLayout.horizontal, PluriLayout.bottomChromeInset),
|
||||
padding: const EdgeInsets.fromLTRB(
|
||||
PluriLayout.horizontal,
|
||||
0,
|
||||
PluriLayout.horizontal,
|
||||
PluriLayout.bottomChromeInset,
|
||||
),
|
||||
sliver: _gridEmisoras(estado, l10n),
|
||||
),
|
||||
],
|
||||
@@ -80,14 +84,11 @@ class _PantallaInicioState extends State<PantallaInicio> {
|
||||
children: [
|
||||
PluriStatusPill(
|
||||
icon: Icons.public_rounded,
|
||||
label: l10n.homeStationsCount(estado.emisorasInicio.length),
|
||||
label: l10n.stationsCount(estado.emisorasInicio.length),
|
||||
accent: Theme.of(context).colorScheme.secondary,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
PluriStatusPill(
|
||||
icon: Icons.hd_rounded,
|
||||
label: l10n.qualityHd,
|
||||
),
|
||||
PluriStatusPill(icon: Icons.hd_rounded, label: l10n.qualityHd),
|
||||
],
|
||||
),
|
||||
);
|
||||
@@ -100,7 +101,12 @@ class _PantallaInicioState extends State<PantallaInicio> {
|
||||
) {
|
||||
final pais = estado.paisCercanoDetectado;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.fromLTRB(PluriLayout.horizontal, 8, PluriLayout.horizontal, 0),
|
||||
padding: const EdgeInsets.fromLTRB(
|
||||
PluriLayout.horizontal,
|
||||
8,
|
||||
PluriLayout.horizontal,
|
||||
0,
|
||||
),
|
||||
child: PluriGlassSurface(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Column(
|
||||
@@ -117,16 +123,18 @@ class _PantallaInicioState extends State<PantallaInicio> {
|
||||
),
|
||||
),
|
||||
TextButton.icon(
|
||||
onPressed: estado.cargandoCercanas
|
||||
? null
|
||||
: estado.cargarEmisorasCercanas,
|
||||
icon: estado.cargandoCercanas
|
||||
? const SizedBox(
|
||||
width: 16,
|
||||
height: 16,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
)
|
||||
: const Icon(Icons.my_location_rounded, size: 18),
|
||||
onPressed:
|
||||
estado.cargandoCercanas
|
||||
? null
|
||||
: estado.cargarEmisorasCercanas,
|
||||
icon:
|
||||
estado.cargandoCercanas
|
||||
? const SizedBox(
|
||||
width: 16,
|
||||
height: 16,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
)
|
||||
: const Icon(Icons.my_location_rounded, size: 18),
|
||||
label: Text(l10n.detectAction),
|
||||
),
|
||||
],
|
||||
@@ -172,7 +180,12 @@ class _PantallaInicioState extends State<PantallaInicio> {
|
||||
AppLocalizations l10n,
|
||||
) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.fromLTRB(PluriLayout.horizontal, 8, PluriLayout.horizontal, 0),
|
||||
padding: const EdgeInsets.fromLTRB(
|
||||
PluriLayout.horizontal,
|
||||
8,
|
||||
PluriLayout.horizontal,
|
||||
0,
|
||||
),
|
||||
child: PluriGlassSurface(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Column(
|
||||
@@ -202,8 +215,7 @@ class _PantallaInicioState extends State<PantallaInicio> {
|
||||
size: 18,
|
||||
),
|
||||
label: Text(e.nombre, maxLines: 1),
|
||||
onPressed:
|
||||
() => reproducirMinimizado(context, e),
|
||||
onPressed: () => reproducirMinimizado(context, e),
|
||||
).animate().fadeIn(delay: (i * 50).ms);
|
||||
},
|
||||
),
|
||||
@@ -220,7 +232,12 @@ class _PantallaInicioState extends State<PantallaInicio> {
|
||||
AppLocalizations l10n,
|
||||
) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.fromLTRB(PluriLayout.horizontal, 16, PluriLayout.horizontal, 8),
|
||||
padding: const EdgeInsets.fromLTRB(
|
||||
PluriLayout.horizontal,
|
||||
16,
|
||||
PluriLayout.horizontal,
|
||||
8,
|
||||
),
|
||||
child: PluriGlassSurface(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Column(
|
||||
@@ -235,7 +252,7 @@ class _PantallaInicioState extends State<PantallaInicio> {
|
||||
_generos.map((g) {
|
||||
final seleccionado = _generoSeleccionado == g;
|
||||
return FilterChip(
|
||||
label: Text(l10n.genreName(g)),
|
||||
label: Text(_genreName(l10n, g)),
|
||||
selected: seleccionado,
|
||||
onSelected: (_) {
|
||||
setState(() {
|
||||
@@ -332,6 +349,21 @@ class _PantallaInicioState extends State<PantallaInicio> {
|
||||
}
|
||||
}
|
||||
|
||||
String _genreName(AppLocalizations l10n, String tag) => switch (tag) {
|
||||
'pop' => l10n.genrePop,
|
||||
'rock' => l10n.genreRock,
|
||||
'jazz' => l10n.genreJazz,
|
||||
'classical' => l10n.genreClassical,
|
||||
'electronic' => l10n.genreElectronic,
|
||||
'news' => l10n.genreNews,
|
||||
'talk' => l10n.genreTalk,
|
||||
'hip-hop' => l10n.genreHipHop,
|
||||
'country' => l10n.genreCountry,
|
||||
'metal' => l10n.genreMetal,
|
||||
'reggae' => l10n.genreReggae,
|
||||
'latin' => l10n.genreLatin,
|
||||
_ => tag,
|
||||
};
|
||||
|
||||
class _ChipShimmer extends StatelessWidget {
|
||||
final ThemeData theme;
|
||||
|
||||
@@ -5,7 +5,6 @@ import 'package:provider/provider.dart';
|
||||
import 'package:shimmer/shimmer.dart';
|
||||
|
||||
import '../estado/estado_radio.dart';
|
||||
import '../l10n/app_localizations_ext.dart';
|
||||
import '../l10n/gen/app_localizations.dart';
|
||||
import '../modelos/emisora.dart';
|
||||
import '../servicios/servicio_audio.dart';
|
||||
@@ -115,7 +114,10 @@ class _PantallaReproductorState extends State<PantallaReproductor>
|
||||
: Icons.favorite_outline_rounded,
|
||||
color: esFavorito ? theme.colorScheme.error : null,
|
||||
),
|
||||
tooltip: esFavorito ? l10n.favoritesRemoveTooltip : l10n.favoritesAddTooltip,
|
||||
tooltip:
|
||||
esFavorito
|
||||
? l10n.favoritesRemoveTooltip
|
||||
: l10n.favoritesAddTooltip,
|
||||
onPressed: () async => estado.toggleFavorito(emisoraActiva),
|
||||
),
|
||||
],
|
||||
@@ -195,7 +197,7 @@ class _PantallaReproductorState extends State<PantallaReproductor>
|
||||
if (e.bitrate != null && e.bitrate! > 0) parts.add('${e.bitrate} kbps');
|
||||
return parts.isEmpty
|
||||
? AppLocalizations.of(context).qualityUnknown
|
||||
: AppLocalizations.of(context).qualityOriginal(parts.join(' ? '));
|
||||
: AppLocalizations.of(context).qualityOriginal(parts.join(' · '));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -385,14 +387,16 @@ class _GrabacionWidget extends StatelessWidget {
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
activa ? l10n.recordingActiveTitle : l10n.recordingDirectTitle,
|
||||
activa
|
||||
? l10n.recordingActiveTitle
|
||||
: l10n.recordingDirectTitle,
|
||||
style: theme.textTheme.titleSmall?.copyWith(
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
activa
|
||||
? '${_formatearDuracion(grabacion.transcurrido)} · ${_formatearBytes(grabacion.bytes)}'
|
||||
? '${_formatearDuracion(l10n, grabacion.transcurrido)} · ${_formatearBytes(grabacion.bytes)}'
|
||||
: l10n.recordingsOriginalStreamHint,
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: theme.colorScheme.onSurface.withValues(alpha: 0.7),
|
||||
@@ -440,7 +444,9 @@ class _GrabacionWidget extends StatelessWidget {
|
||||
if (!context.mounted) return;
|
||||
if (!abierto) {
|
||||
messenger.showSnackBar(
|
||||
SnackBar(content: Text(AppLocalizations.of(context).recordingsOpenLatestError)),
|
||||
SnackBar(
|
||||
content: Text(AppLocalizations.of(context).recordingsOpenLatestError),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -452,7 +458,9 @@ class _GrabacionWidget extends StatelessWidget {
|
||||
if (!abierto) {
|
||||
messenger.showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(AppLocalizations.of(context).recordingsOpenFolderPlainError),
|
||||
content: Text(
|
||||
AppLocalizations.of(context).recordingsOpenFolderPlainError,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -493,7 +501,15 @@ class _GrabacionWidget extends StatelessWidget {
|
||||
),
|
||||
for (final opcion in _opciones)
|
||||
ActionChip(
|
||||
label: Text(opcion.label),
|
||||
label: Text(
|
||||
opcion.duracion.inMinutes > 0
|
||||
? AppLocalizations.of(
|
||||
ctx,
|
||||
).durationMinutesOnly(opcion.duracion.inMinutes)
|
||||
: AppLocalizations.of(
|
||||
ctx,
|
||||
).durationSecondsOnly(opcion.duracion.inSeconds),
|
||||
),
|
||||
onPressed: () {
|
||||
estado.iniciarGrabacion(duracion: opcion.duracion);
|
||||
Navigator.pop(ctx);
|
||||
@@ -533,7 +549,9 @@ class _GrabacionWidget extends StatelessWidget {
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
controller: minutosCtrl,
|
||||
decoration: InputDecoration(labelText: AppLocalizations.of(ctx).minutesLabel),
|
||||
decoration: InputDecoration(
|
||||
labelText: AppLocalizations.of(ctx).minutesLabel,
|
||||
),
|
||||
keyboardType: TextInputType.number,
|
||||
validator: (value) => _validarNumero(ctx, value),
|
||||
),
|
||||
@@ -542,7 +560,9 @@ class _GrabacionWidget extends StatelessWidget {
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
controller: segundosCtrl,
|
||||
decoration: InputDecoration(labelText: AppLocalizations.of(ctx).secondsLabel),
|
||||
decoration: InputDecoration(
|
||||
labelText: AppLocalizations.of(ctx).secondsLabel,
|
||||
),
|
||||
keyboardType: TextInputType.number,
|
||||
validator: (value) => _validarNumero(ctx, value),
|
||||
),
|
||||
@@ -585,11 +605,14 @@ class _GrabacionWidget extends StatelessWidget {
|
||||
return null;
|
||||
}
|
||||
|
||||
String _formatearDuracion(Duration d) {
|
||||
String _formatearDuracion(AppLocalizations l10n, Duration d) {
|
||||
final h = d.inHours;
|
||||
final m = d.inMinutes.remainder(60).toString().padLeft(2, '0');
|
||||
final s = d.inSeconds.remainder(60).toString().padLeft(2, '0');
|
||||
return h > 0 ? '${h}h ${m}m ${s}s' : '${m}m ${s}s';
|
||||
if (h > 0) {
|
||||
return l10n.durationHoursMinutesSeconds(h, m, s);
|
||||
}
|
||||
return l10n.durationMinutesSeconds(m, s);
|
||||
}
|
||||
|
||||
String _formatearBytes(int bytes) {
|
||||
@@ -600,17 +623,16 @@ class _GrabacionWidget extends StatelessWidget {
|
||||
}
|
||||
|
||||
class _OpcionGrabacion {
|
||||
const _OpcionGrabacion(this.label, this.duracion);
|
||||
final String label;
|
||||
const _OpcionGrabacion(this.duracion);
|
||||
final Duration duracion;
|
||||
}
|
||||
|
||||
const _opciones = [
|
||||
_OpcionGrabacion('30 s', Duration(seconds: 30)),
|
||||
_OpcionGrabacion('1 min', Duration(minutes: 1)),
|
||||
_OpcionGrabacion('5 min', Duration(minutes: 5)),
|
||||
_OpcionGrabacion('15 min', Duration(minutes: 15)),
|
||||
_OpcionGrabacion('30 min', Duration(minutes: 30)),
|
||||
_OpcionGrabacion(Duration(seconds: 30)),
|
||||
_OpcionGrabacion(Duration(minutes: 1)),
|
||||
_OpcionGrabacion(Duration(minutes: 5)),
|
||||
_OpcionGrabacion(Duration(minutes: 15)),
|
||||
_OpcionGrabacion(Duration(minutes: 30)),
|
||||
];
|
||||
|
||||
class _Controles extends StatelessWidget {
|
||||
@@ -643,7 +665,7 @@ class _Controles extends StatelessWidget {
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
l10n.playerPlaybackErrorTitle,
|
||||
l10n.audioErrorCannotPlay,
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
color: theme.colorScheme.error,
|
||||
),
|
||||
@@ -784,7 +806,14 @@ class _TimerWidget extends StatelessWidget {
|
||||
final t = snap.data ?? Duration.zero;
|
||||
final m = t.inMinutes.remainder(60).toString().padLeft(2, '0');
|
||||
final s = t.inSeconds.remainder(60).toString().padLeft(2, '0');
|
||||
final label = t.inHours > 0 ? '${t.inHours}h ${m}m' : '${m}m ${s}s';
|
||||
final label =
|
||||
t.inHours > 0
|
||||
? AppLocalizations.of(context).durationHoursMinutesSeconds(
|
||||
t.inHours,
|
||||
m,
|
||||
s,
|
||||
)
|
||||
: AppLocalizations.of(context).durationMinutesSeconds(m, s);
|
||||
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
@@ -833,7 +862,11 @@ class _TimerWidget extends StatelessWidget {
|
||||
opcionesTimer
|
||||
.map(
|
||||
(min) => ActionChip(
|
||||
label: Text('$min min'),
|
||||
label: Text(
|
||||
AppLocalizations.of(
|
||||
ctx,
|
||||
).durationMinutesOnly(min),
|
||||
),
|
||||
onPressed: () {
|
||||
estado.iniciarTimer(min);
|
||||
Navigator.pop(ctx);
|
||||
|
||||
Reference in New Issue
Block a user