refactor(ajustes): split remaining Settings sections into pushed screens
This commit is contained in:
@@ -55,6 +55,9 @@
|
||||
"settingsSubtitle": "Fine-grained sound control, backups, and custom stations.",
|
||||
"settingsGroupAudioTitle": "AUDIO",
|
||||
"settingsGroupStationsTitle": "STATIONS",
|
||||
"settingsGroupRecordingsTitle": "RECORDINGS & MUSIC",
|
||||
"settingsGroupApplicationTitle": "APPLICATION",
|
||||
"infoSectionTitle": "Info",
|
||||
"languageSectionTitle": "Language",
|
||||
"languageSectionDescription": "Choose how the app language is displayed.",
|
||||
"languageSystemDefault": "System",
|
||||
|
||||
@@ -55,6 +55,9 @@
|
||||
"settingsSubtitle": "Control fino de sonido, copias de seguridad y emisoras personalizadas.",
|
||||
"settingsGroupAudioTitle": "AUDIO",
|
||||
"settingsGroupStationsTitle": "EMISORAS",
|
||||
"settingsGroupRecordingsTitle": "GRABACIONES Y MÚSICA",
|
||||
"settingsGroupApplicationTitle": "APLICACIÓN",
|
||||
"infoSectionTitle": "Información",
|
||||
"languageSectionTitle": "Idioma",
|
||||
"languageSectionDescription": "Elegí cómo se muestra el idioma de la app.",
|
||||
"languageSystemDefault": "Sistema",
|
||||
|
||||
@@ -286,6 +286,24 @@ abstract class AppLocalizations {
|
||||
/// **'EMISORAS'**
|
||||
String get settingsGroupStationsTitle;
|
||||
|
||||
/// No description provided for @settingsGroupRecordingsTitle.
|
||||
///
|
||||
/// In es, this message translates to:
|
||||
/// **'GRABACIONES Y MÚSICA'**
|
||||
String get settingsGroupRecordingsTitle;
|
||||
|
||||
/// No description provided for @settingsGroupApplicationTitle.
|
||||
///
|
||||
/// In es, this message translates to:
|
||||
/// **'APLICACIÓN'**
|
||||
String get settingsGroupApplicationTitle;
|
||||
|
||||
/// No description provided for @infoSectionTitle.
|
||||
///
|
||||
/// In es, this message translates to:
|
||||
/// **'Información'**
|
||||
String get infoSectionTitle;
|
||||
|
||||
/// No description provided for @languageSectionTitle.
|
||||
///
|
||||
/// In es, this message translates to:
|
||||
|
||||
@@ -105,6 +105,15 @@ class AppLocalizationsAr extends AppLocalizations {
|
||||
@override
|
||||
String get settingsGroupStationsTitle => 'EMISORAS';
|
||||
|
||||
@override
|
||||
String get settingsGroupRecordingsTitle => 'GRABACIONES Y MÚSICA';
|
||||
|
||||
@override
|
||||
String get settingsGroupApplicationTitle => 'APLICACIÓN';
|
||||
|
||||
@override
|
||||
String get infoSectionTitle => 'Información';
|
||||
|
||||
@override
|
||||
String get languageSectionTitle => 'اللغة';
|
||||
|
||||
|
||||
@@ -106,6 +106,15 @@ class AppLocalizationsBn extends AppLocalizations {
|
||||
@override
|
||||
String get settingsGroupStationsTitle => 'EMISORAS';
|
||||
|
||||
@override
|
||||
String get settingsGroupRecordingsTitle => 'GRABACIONES Y MÚSICA';
|
||||
|
||||
@override
|
||||
String get settingsGroupApplicationTitle => 'APLICACIÓN';
|
||||
|
||||
@override
|
||||
String get infoSectionTitle => 'Información';
|
||||
|
||||
@override
|
||||
String get languageSectionTitle => 'ভাষা';
|
||||
|
||||
|
||||
@@ -105,6 +105,15 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
@override
|
||||
String get settingsGroupStationsTitle => 'EMISORAS';
|
||||
|
||||
@override
|
||||
String get settingsGroupRecordingsTitle => 'GRABACIONES Y MÚSICA';
|
||||
|
||||
@override
|
||||
String get settingsGroupApplicationTitle => 'APLICACIÓN';
|
||||
|
||||
@override
|
||||
String get infoSectionTitle => 'Información';
|
||||
|
||||
@override
|
||||
String get languageSectionTitle => 'Sprache';
|
||||
|
||||
|
||||
@@ -105,6 +105,15 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
@override
|
||||
String get settingsGroupStationsTitle => 'STATIONS';
|
||||
|
||||
@override
|
||||
String get settingsGroupRecordingsTitle => 'RECORDINGS & MUSIC';
|
||||
|
||||
@override
|
||||
String get settingsGroupApplicationTitle => 'APPLICATION';
|
||||
|
||||
@override
|
||||
String get infoSectionTitle => 'Info';
|
||||
|
||||
@override
|
||||
String get languageSectionTitle => 'Language';
|
||||
|
||||
|
||||
@@ -105,6 +105,15 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
@override
|
||||
String get settingsGroupStationsTitle => 'EMISORAS';
|
||||
|
||||
@override
|
||||
String get settingsGroupRecordingsTitle => 'GRABACIONES Y MÚSICA';
|
||||
|
||||
@override
|
||||
String get settingsGroupApplicationTitle => 'APLICACIÓN';
|
||||
|
||||
@override
|
||||
String get infoSectionTitle => 'Información';
|
||||
|
||||
@override
|
||||
String get languageSectionTitle => 'Idioma';
|
||||
|
||||
|
||||
@@ -106,6 +106,15 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
@override
|
||||
String get settingsGroupStationsTitle => 'EMISORAS';
|
||||
|
||||
@override
|
||||
String get settingsGroupRecordingsTitle => 'GRABACIONES Y MÚSICA';
|
||||
|
||||
@override
|
||||
String get settingsGroupApplicationTitle => 'APLICACIÓN';
|
||||
|
||||
@override
|
||||
String get infoSectionTitle => 'Información';
|
||||
|
||||
@override
|
||||
String get languageSectionTitle => 'Langue';
|
||||
|
||||
|
||||
@@ -105,6 +105,15 @@ class AppLocalizationsHi extends AppLocalizations {
|
||||
@override
|
||||
String get settingsGroupStationsTitle => 'EMISORAS';
|
||||
|
||||
@override
|
||||
String get settingsGroupRecordingsTitle => 'GRABACIONES Y MÚSICA';
|
||||
|
||||
@override
|
||||
String get settingsGroupApplicationTitle => 'APLICACIÓN';
|
||||
|
||||
@override
|
||||
String get infoSectionTitle => 'Información';
|
||||
|
||||
@override
|
||||
String get languageSectionTitle => 'भाषा';
|
||||
|
||||
|
||||
@@ -106,6 +106,15 @@ class AppLocalizationsId extends AppLocalizations {
|
||||
@override
|
||||
String get settingsGroupStationsTitle => 'EMISORAS';
|
||||
|
||||
@override
|
||||
String get settingsGroupRecordingsTitle => 'GRABACIONES Y MÚSICA';
|
||||
|
||||
@override
|
||||
String get settingsGroupApplicationTitle => 'APLICACIÓN';
|
||||
|
||||
@override
|
||||
String get infoSectionTitle => 'Información';
|
||||
|
||||
@override
|
||||
String get languageSectionTitle => 'Bahasa';
|
||||
|
||||
|
||||
@@ -105,6 +105,15 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
@override
|
||||
String get settingsGroupStationsTitle => 'EMISORAS';
|
||||
|
||||
@override
|
||||
String get settingsGroupRecordingsTitle => 'GRABACIONES Y MÚSICA';
|
||||
|
||||
@override
|
||||
String get settingsGroupApplicationTitle => 'APLICACIÓN';
|
||||
|
||||
@override
|
||||
String get infoSectionTitle => 'Información';
|
||||
|
||||
@override
|
||||
String get languageSectionTitle => 'Lingua';
|
||||
|
||||
|
||||
@@ -103,6 +103,15 @@ class AppLocalizationsJa extends AppLocalizations {
|
||||
@override
|
||||
String get settingsGroupStationsTitle => 'EMISORAS';
|
||||
|
||||
@override
|
||||
String get settingsGroupRecordingsTitle => 'GRABACIONES Y MÚSICA';
|
||||
|
||||
@override
|
||||
String get settingsGroupApplicationTitle => 'APLICACIÓN';
|
||||
|
||||
@override
|
||||
String get infoSectionTitle => 'Información';
|
||||
|
||||
@override
|
||||
String get languageSectionTitle => '言語';
|
||||
|
||||
|
||||
@@ -105,6 +105,15 @@ class AppLocalizationsPt extends AppLocalizations {
|
||||
@override
|
||||
String get settingsGroupStationsTitle => 'EMISORAS';
|
||||
|
||||
@override
|
||||
String get settingsGroupRecordingsTitle => 'GRABACIONES Y MÚSICA';
|
||||
|
||||
@override
|
||||
String get settingsGroupApplicationTitle => 'APLICACIÓN';
|
||||
|
||||
@override
|
||||
String get infoSectionTitle => 'Información';
|
||||
|
||||
@override
|
||||
String get languageSectionTitle => 'Idioma';
|
||||
|
||||
|
||||
@@ -105,6 +105,15 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
@override
|
||||
String get settingsGroupStationsTitle => 'EMISORAS';
|
||||
|
||||
@override
|
||||
String get settingsGroupRecordingsTitle => 'GRABACIONES Y MÚSICA';
|
||||
|
||||
@override
|
||||
String get settingsGroupApplicationTitle => 'APLICACIÓN';
|
||||
|
||||
@override
|
||||
String get infoSectionTitle => 'Información';
|
||||
|
||||
@override
|
||||
String get languageSectionTitle => 'Язык';
|
||||
|
||||
|
||||
@@ -103,6 +103,15 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
@override
|
||||
String get settingsGroupStationsTitle => 'EMISORAS';
|
||||
|
||||
@override
|
||||
String get settingsGroupRecordingsTitle => 'GRABACIONES Y MÚSICA';
|
||||
|
||||
@override
|
||||
String get settingsGroupApplicationTitle => 'APLICACIÓN';
|
||||
|
||||
@override
|
||||
String get infoSectionTitle => 'Información';
|
||||
|
||||
@override
|
||||
String get languageSectionTitle => '语言';
|
||||
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:share_plus/share_plus.dart' show Share, XFile;
|
||||
|
||||
import '../../estado/estado_radio.dart';
|
||||
import '../../l10n/gen/app_localizations.dart';
|
||||
import '../../widgets/pluri_glass_surface.dart';
|
||||
import '../../widgets/pluri_layout.dart';
|
||||
import '../../widgets/pluri_push_scaffold.dart';
|
||||
|
||||
/// APLICACIÓN group · "Copia de seguridad" (design ADR-3). Body moved
|
||||
/// verbatim from the former `_SeccionBackup` in `pantalla_ajustes.dart` —
|
||||
/// only the panel header's icon and title were removed (the pushed screen's
|
||||
/// title now carries them); every method below is unchanged.
|
||||
class PantallaAjustesBackup extends StatelessWidget {
|
||||
const PantallaAjustesBackup({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context);
|
||||
return PluriPushScaffold(
|
||||
title: l10n.backupSectionTitle,
|
||||
body: ListView(
|
||||
padding: PluriLayout.pageContentPadding,
|
||||
children: const [_CuerpoBackup()],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _CuerpoBackup extends StatelessWidget {
|
||||
const _CuerpoBackup();
|
||||
|
||||
Future<void> _exportar(BuildContext context) async {
|
||||
final l10n = AppLocalizations.of(context);
|
||||
try {
|
||||
final estado = context.read<EstadoRadio>();
|
||||
// JSON serialization is owned by ServicioExportImport (S4-R4).
|
||||
final json = await estado.exportarConfigJson();
|
||||
|
||||
final dir = await getTemporaryDirectory();
|
||||
final file = File('${dir.path}/pluriwave-backup.json');
|
||||
await file.writeAsString(json);
|
||||
|
||||
await Share.shareXFiles(
|
||||
[XFile(file.path)],
|
||||
subject: l10n.backupShareSubject,
|
||||
text: l10n.backupShareText(DateTime.now().toLocal()),
|
||||
);
|
||||
} catch (e) {
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(l10n.backupExportError(e.toString()))),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _importar(BuildContext context) async {
|
||||
final l10n = AppLocalizations.of(context);
|
||||
try {
|
||||
final result = await FilePicker.platform.pickFiles(
|
||||
type: FileType.custom,
|
||||
allowedExtensions: ['json'],
|
||||
);
|
||||
if (result == null || result.files.single.path == null) return;
|
||||
|
||||
final file = File(result.files.single.path!);
|
||||
final contenido = await file.readAsString();
|
||||
if (!context.mounted) return;
|
||||
// Parsing is owned by ServicioExportImport (S4-R4): null = malformed.
|
||||
final json = context.read<EstadoRadio>().parsearConfigJson(contenido);
|
||||
if (json == null) {
|
||||
throw const FormatException('invalid backup file');
|
||||
}
|
||||
|
||||
if (context.mounted) {
|
||||
final confirmar = await showDialog<bool>(
|
||||
context: context,
|
||||
builder:
|
||||
(ctx) => AlertDialog(
|
||||
title: Text(AppLocalizations.of(ctx).backupImportTitle),
|
||||
content: Text(
|
||||
AppLocalizations.of(ctx).backupImportConfirmMessage,
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(ctx, false),
|
||||
child: Text(AppLocalizations.of(ctx).cancelAction),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () => Navigator.pop(ctx, true),
|
||||
child: Text(AppLocalizations.of(ctx).backupImportTitle),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
if (confirmar != true) return;
|
||||
if (context.mounted) {
|
||||
final estado = context.read<EstadoRadio>();
|
||||
final messenger = ScaffoldMessenger.of(context);
|
||||
await estado.importarConfig(json);
|
||||
messenger.showSnackBar(
|
||||
SnackBar(content: Text(l10n.backupImportSuccess)),
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(l10n.backupImportError(e.toString()))),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return PluriGlassSurface(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
ListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
leading: const Icon(Icons.upload_outlined),
|
||||
title: Text(AppLocalizations.of(context).backupExportTitle),
|
||||
subtitle: Text(AppLocalizations.of(context).backupExportSubtitle),
|
||||
onTap: () => _exportar(context),
|
||||
),
|
||||
ListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
leading: const Icon(Icons.download_outlined),
|
||||
title: Text(AppLocalizations.of(context).backupImportTitle),
|
||||
subtitle: Text(AppLocalizations.of(context).backupImportSubtitle),
|
||||
onTap: () => _importar(context),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,217 @@
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../../estado/estado_grabacion.dart';
|
||||
import '../../l10n/gen/app_localizations.dart';
|
||||
import '../../widgets/pluri_glass_surface.dart';
|
||||
import '../../widgets/pluri_layout.dart';
|
||||
import '../../widgets/pluri_push_scaffold.dart';
|
||||
|
||||
/// GRABACIONES Y MÚSICA group · "Grabaciones" (design ADR-3). Body moved
|
||||
/// verbatim from the former `_SeccionGrabaciones` in `pantalla_ajustes.dart`
|
||||
/// — only the panel header's icon and title were removed (the pushed
|
||||
/// screen's title now carries them); every method below is unchanged.
|
||||
///
|
||||
/// Known pre-existing bug, deliberately NOT fixed here (out of scope, moved
|
||||
/// verbatim, tracked separately): [_editarTamanoMaximo] disposes its
|
||||
/// [TextEditingController] immediately after `showModalBottomSheet` resolves,
|
||||
/// racing the sheet's own close animation — the same shape of bug already
|
||||
/// documented for `_editarGrupo` in `pantalla_ajustes_grupos_favoritos.dart`.
|
||||
class PantallaAjustesGrabaciones extends StatelessWidget {
|
||||
const PantallaAjustesGrabaciones({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context);
|
||||
return PluriPushScaffold(
|
||||
title: l10n.recordingsSectionTitle,
|
||||
body: ListView(
|
||||
padding: PluriLayout.pageContentPadding,
|
||||
children: const [_CuerpoGrabaciones()],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _CuerpoGrabaciones extends StatelessWidget {
|
||||
const _CuerpoGrabaciones();
|
||||
|
||||
Future<void> _seleccionarRuta(BuildContext context) async {
|
||||
final estado = context.read<EstadoGrabacion>();
|
||||
final messenger = ScaffoldMessenger.of(context);
|
||||
final l10n = AppLocalizations.of(context);
|
||||
final ruta = await FilePicker.platform.getDirectoryPath(
|
||||
dialogTitle: l10n.recordingsFolderDialogTitle,
|
||||
);
|
||||
if (ruta == null) return;
|
||||
try {
|
||||
await estado.cambiarDirectorio(ruta);
|
||||
if (!context.mounted) return;
|
||||
messenger.showSnackBar(
|
||||
SnackBar(content: Text(l10n.recordingsPathUpdated)),
|
||||
);
|
||||
} catch (e) {
|
||||
if (!context.mounted) return;
|
||||
messenger.showSnackBar(
|
||||
SnackBar(content: Text(l10n.recordingsPathSaveError(e.toString()))),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _restaurarRuta(BuildContext context) async {
|
||||
final estado = context.read<EstadoGrabacion>();
|
||||
final messenger = ScaffoldMessenger.of(context);
|
||||
final l10n = AppLocalizations.of(context);
|
||||
await estado.restaurarDirectorio();
|
||||
if (!context.mounted) return;
|
||||
messenger.showSnackBar(
|
||||
SnackBar(content: Text(l10n.recordingsDefaultFolderRestored)),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _abrirCarpeta(BuildContext context) async {
|
||||
final estado = context.read<EstadoGrabacion>();
|
||||
final messenger = ScaffoldMessenger.of(context);
|
||||
final l10n = AppLocalizations.of(context);
|
||||
try {
|
||||
final abierto = await estado.abrirDirectorio();
|
||||
if (!context.mounted) return;
|
||||
if (!abierto) {
|
||||
messenger.showSnackBar(
|
||||
SnackBar(content: Text(l10n.recordingsOpenFolderError(l10n.dash))),
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
if (!context.mounted) return;
|
||||
messenger.showSnackBar(
|
||||
SnackBar(content: Text(l10n.recordingsOpenFolderError(e.toString()))),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _editarTamanoMaximo(BuildContext context) async {
|
||||
final estado = context.read<EstadoGrabacion>();
|
||||
final l10n = AppLocalizations.of(context);
|
||||
final actualMb = _bytesAMegabytes(estado.maxBytes);
|
||||
final controller = TextEditingController(text: actualMb.toString());
|
||||
|
||||
final nuevoMb = await showModalBottomSheet<int>(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
showDragHandle: true,
|
||||
builder: (ctx) {
|
||||
final bottom = MediaQuery.viewInsetsOf(ctx).bottom;
|
||||
return Padding(
|
||||
padding: EdgeInsets.fromLTRB(20, 0, 20, bottom + 24),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
l10n.recordingsMaxSizeDialogTitle,
|
||||
style: Theme.of(ctx).textTheme.titleLarge,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextField(
|
||||
controller: controller,
|
||||
autofocus: true,
|
||||
keyboardType: TextInputType.number,
|
||||
decoration: InputDecoration(
|
||||
labelText: l10n.recordingsMaxSizeMbLabel,
|
||||
border: const OutlineInputBorder(),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
FilledButton.icon(
|
||||
onPressed: () {
|
||||
final value = int.tryParse(controller.text.trim());
|
||||
if (value == null || value <= 0) return;
|
||||
Navigator.of(ctx).pop(value);
|
||||
},
|
||||
icon: const Icon(Icons.save_rounded),
|
||||
label: Text(l10n.saveQuickAccessButton),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
controller.dispose();
|
||||
if (nuevoMb == null || !context.mounted) return;
|
||||
await estado.cambiarMaxBytes(nuevoMb * 1024 * 1024);
|
||||
if (!context.mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(l10n.recordingsMaxSizeSaved(nuevoMb))),
|
||||
);
|
||||
}
|
||||
|
||||
int _bytesAMegabytes(int bytes) =>
|
||||
(bytes / (1024 * 1024)).round().clamp(1, 1048576);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// Recording state lives in EstadoGrabacion (S4-R2): this section only
|
||||
// rebuilds on recording changes, never on playback notifications.
|
||||
final estado = context.watch<EstadoGrabacion>();
|
||||
final l10n = AppLocalizations.of(context);
|
||||
|
||||
return PluriGlassSurface(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
FutureBuilder<String>(
|
||||
future: estado.directorioEfectivo(),
|
||||
builder:
|
||||
(ctx, snap) => ListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
leading: const Icon(Icons.folder_outlined),
|
||||
title: Text(l10n.recordingsFolderTitle),
|
||||
subtitle: Text(
|
||||
snap.data ?? l10n.recordingsPathCalculating,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
),
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 8,
|
||||
children: [
|
||||
OutlinedButton.icon(
|
||||
icon: const Icon(Icons.folder_open_rounded),
|
||||
label: Text(l10n.recordingsChangePath),
|
||||
onPressed: () => _seleccionarRuta(context),
|
||||
),
|
||||
FilledButton.tonalIcon(
|
||||
icon: const Icon(Icons.folder_copy_rounded),
|
||||
label: Text(l10n.recordingsOpenFolder),
|
||||
onPressed: () => _abrirCarpeta(context),
|
||||
),
|
||||
IconButton.filledTonal(
|
||||
tooltip: l10n.recordingsUseDefaultPath,
|
||||
icon: const Icon(Icons.restore_rounded),
|
||||
onPressed: () => _restaurarRuta(context),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
ListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
leading: const Icon(Icons.sd_storage_rounded),
|
||||
title: Text(l10n.recordingsMaxSizeTitle),
|
||||
subtitle: Text(
|
||||
l10n.recordingsMaxSizeSubtitle(_bytesAMegabytes(estado.maxBytes)),
|
||||
),
|
||||
onTap: () => _editarTamanoMaximo(context),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
l10n.recordingsOriginalStreamHint,
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../../estado/estado_idioma.dart';
|
||||
import '../../l10n/gen/app_localizations.dart';
|
||||
import '../../widgets/pluri_glass_surface.dart';
|
||||
import '../../widgets/pluri_layout.dart';
|
||||
import '../../widgets/pluri_push_scaffold.dart';
|
||||
|
||||
/// APLICACIÓN group · "Idioma" (design ADR-3). Body moved verbatim from the
|
||||
/// former `_SeccionIdioma` (+ `_IdiomaDisponible`) in `pantalla_ajustes.dart`
|
||||
/// — only the panel header's icon and title were removed (the pushed
|
||||
/// screen's title now carries them); every method and the language list
|
||||
/// below is unchanged.
|
||||
class PantallaAjustesIdioma extends StatelessWidget {
|
||||
const PantallaAjustesIdioma({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context);
|
||||
return PluriPushScaffold(
|
||||
title: l10n.languageSectionTitle,
|
||||
body: ListView(
|
||||
padding: PluriLayout.pageContentPadding,
|
||||
children: const [_CuerpoIdioma()],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _CuerpoIdioma extends StatelessWidget {
|
||||
const _CuerpoIdioma();
|
||||
|
||||
static const _codigoSistema = 'system';
|
||||
static const _idiomas = [
|
||||
_IdiomaDisponible(Locale('en'), 'English'),
|
||||
_IdiomaDisponible(Locale('es'), 'Español'),
|
||||
_IdiomaDisponible(Locale('zh'), '中文'),
|
||||
_IdiomaDisponible(Locale('hi'), 'हिन्दी'),
|
||||
_IdiomaDisponible(Locale('ar'), 'العربية'),
|
||||
_IdiomaDisponible(Locale('pt'), 'Português'),
|
||||
_IdiomaDisponible(Locale('fr'), 'Français'),
|
||||
_IdiomaDisponible(Locale('ru'), 'Русский'),
|
||||
_IdiomaDisponible(Locale('de'), 'Deutsch'),
|
||||
_IdiomaDisponible(Locale('ja'), '日本語'),
|
||||
_IdiomaDisponible(Locale('id'), 'Bahasa Indonesia'),
|
||||
_IdiomaDisponible(Locale('bn'), 'বাংলা'),
|
||||
_IdiomaDisponible(Locale('it'), 'Italiano'),
|
||||
];
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context);
|
||||
final estadoIdioma = context.watch<EstadoIdioma>();
|
||||
final locale = estadoIdioma.localeSeleccionado;
|
||||
final valorActual = locale == null ? _codigoSistema : _codigoLocale(locale);
|
||||
|
||||
return PluriGlassSurface(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
l10n.languageSectionDescription,
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
DropdownButtonFormField<String>(
|
||||
initialValue: valorActual,
|
||||
decoration: InputDecoration(
|
||||
labelText: l10n.languageSectionTitle,
|
||||
border: const OutlineInputBorder(),
|
||||
),
|
||||
items: [
|
||||
DropdownMenuItem(
|
||||
value: _codigoSistema,
|
||||
child: Text(l10n.languageSystemDefault),
|
||||
),
|
||||
for (final idioma in _idiomas)
|
||||
DropdownMenuItem(
|
||||
value: _codigoLocale(idioma.locale),
|
||||
child: Text(idioma.nombreNativo),
|
||||
),
|
||||
],
|
||||
onChanged: (codigo) async {
|
||||
if (codigo == null) return;
|
||||
if (codigo == _codigoSistema) {
|
||||
await context.read<EstadoIdioma>().seleccionarSistema();
|
||||
if (!context.mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(l10n.languageUpdatedSystem)),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
final idioma = _idiomas.firstWhere(
|
||||
(item) => _codigoLocale(item.locale) == codigo,
|
||||
orElse: () => _idiomas.first,
|
||||
);
|
||||
await context.read<EstadoIdioma>().seleccionarLocale(
|
||||
idioma.locale,
|
||||
);
|
||||
if (!context.mounted) return;
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(l10n.languageUpdated(idioma.nombreNativo)),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
static String _codigoLocale(Locale locale) {
|
||||
final countryCode = locale.countryCode;
|
||||
if (countryCode == null || countryCode.isEmpty) {
|
||||
return locale.languageCode;
|
||||
}
|
||||
return '${locale.languageCode}_$countryCode';
|
||||
}
|
||||
}
|
||||
|
||||
class _IdiomaDisponible {
|
||||
const _IdiomaDisponible(this.locale, this.nombreNativo);
|
||||
|
||||
final Locale locale;
|
||||
final String nombreNativo;
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../../estado/estado_radio.dart';
|
||||
import '../../l10n/gen/app_localizations.dart';
|
||||
import '../../widgets/pluri_glass_surface.dart';
|
||||
import '../../widgets/pluri_icon.dart';
|
||||
import '../../widgets/pluri_layout.dart';
|
||||
import '../../widgets/pluri_onboarding_dialog.dart';
|
||||
import '../../widgets/pluri_push_scaffold.dart';
|
||||
|
||||
/// APLICACIÓN group · "Info" (design ADR-3). Body moved verbatim from the
|
||||
/// former `_SeccionInfo` in `pantalla_ajustes.dart`. Unlike the other four
|
||||
/// sections in this batch, `_SeccionInfo` never had its own header
|
||||
/// icon+title row — its first tile (app name + version) already served that
|
||||
/// role — so there is no header row to strip here; the body below is
|
||||
/// unchanged in full. `infoSectionTitle` is the one new ARB key this screen
|
||||
/// needed, since no existing in-body header string covers a bare "Info"
|
||||
/// label (see WU3b's apply-progress note).
|
||||
class PantallaAjustesInfo extends StatelessWidget {
|
||||
const PantallaAjustesInfo({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context);
|
||||
return PluriPushScaffold(
|
||||
title: l10n.infoSectionTitle,
|
||||
body: ListView(
|
||||
padding: PluriLayout.pageContentPadding,
|
||||
children: const [_CuerpoInfo()],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _CuerpoInfo extends StatelessWidget {
|
||||
const _CuerpoInfo();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Consumer<EstadoRadio>(
|
||||
builder:
|
||||
(ctx, estado, _) => PluriGlassSurface(
|
||||
child: Column(
|
||||
children: [
|
||||
FutureBuilder<PackageInfo>(
|
||||
future: PackageInfo.fromPlatform(),
|
||||
builder: (ctx, snap) {
|
||||
final version =
|
||||
snap.hasData
|
||||
? 'v${snap.data!.version}+${snap.data!.buildNumber}'
|
||||
: AppLocalizations.of(ctx).appVersionLoading;
|
||||
return ListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
leading: const PluriIcon(
|
||||
glyph: PluriIconGlyph.settings,
|
||||
variant: PluriIconVariant.filled,
|
||||
),
|
||||
title: Text(AppLocalizations.of(ctx).appTitle),
|
||||
subtitle: Text(
|
||||
AppLocalizations.of(ctx).appVersionSubtitle(version),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
FutureBuilder<int>(
|
||||
future: estado.favoritos.obtenerTodos().then((l) => l.length),
|
||||
builder:
|
||||
(ctx, snap) => ListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
leading: const Icon(Icons.favorite_outline_rounded),
|
||||
title: Text(
|
||||
AppLocalizations.of(ctx).savedFavoritesTitle,
|
||||
),
|
||||
trailing: Text(
|
||||
snap.data?.toString() ??
|
||||
AppLocalizations.of(ctx).dash,
|
||||
style: Theme.of(ctx).textTheme.bodyLarge,
|
||||
),
|
||||
),
|
||||
),
|
||||
ListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
leading: const Icon(Icons.help_outline_rounded),
|
||||
title: Text(AppLocalizations.of(ctx).helpTitle),
|
||||
subtitle: Text(AppLocalizations.of(ctx).helpSubtitle),
|
||||
trailing: const Icon(Icons.chevron_right_rounded),
|
||||
onTap: () => PluriOnboardingDialog.mostrar(ctx),
|
||||
),
|
||||
ListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
leading: const Icon(Icons.verified_outlined),
|
||||
title: Text(AppLocalizations.of(ctx).stationFilterTitle),
|
||||
subtitle: Text(
|
||||
AppLocalizations.of(ctx).stationFilterSubtitle,
|
||||
),
|
||||
trailing: Icon(
|
||||
Icons.check_circle_rounded,
|
||||
color: Theme.of(ctx).colorScheme.secondary,
|
||||
),
|
||||
),
|
||||
ListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
leading: const Icon(Icons.music_note_outlined),
|
||||
title: Text(AppLocalizations.of(ctx).backgroundAudioTitle),
|
||||
subtitle: Text(
|
||||
AppLocalizations.of(ctx).backgroundAudioSubtitle,
|
||||
),
|
||||
trailing: Icon(
|
||||
Icons.check_circle_rounded,
|
||||
color: Theme.of(ctx).colorScheme.secondary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../l10n/gen/app_localizations.dart';
|
||||
import '../../servicios/musica_local_auto.dart';
|
||||
import '../../widgets/pluri_glass_surface.dart';
|
||||
import '../../widgets/pluri_layout.dart';
|
||||
import '../../widgets/pluri_push_scaffold.dart';
|
||||
|
||||
/// GRABACIONES Y MÚSICA group · "Música local" (design ADR-3). Body moved
|
||||
/// verbatim from the former `_SeccionMusicaLocal` in `pantalla_ajustes.dart`
|
||||
/// — only the panel header's icon and title were removed (the pushed
|
||||
/// screen's title now carries them). Deliberately does NOT use
|
||||
/// `FilePicker.platform` (see tasks.md "Grounding corrections") —
|
||||
/// [FuenteMusicaLocalAutoImpl.elegirCarpeta] calls the native
|
||||
/// `pickMusicFolder` channel method directly, since it needs a
|
||||
/// persistable-grant SAF tree URI, not a plain filesystem path.
|
||||
class PantallaAjustesMusicaLocal extends StatelessWidget {
|
||||
const PantallaAjustesMusicaLocal({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context);
|
||||
return PluriPushScaffold(
|
||||
title: l10n.localMusicSectionTitle,
|
||||
body: ListView(
|
||||
padding: PluriLayout.pageContentPadding,
|
||||
children: const [_CuerpoMusicaLocal()],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _CuerpoMusicaLocal extends StatefulWidget {
|
||||
const _CuerpoMusicaLocal();
|
||||
|
||||
@override
|
||||
State<_CuerpoMusicaLocal> createState() => _CuerpoMusicaLocalState();
|
||||
}
|
||||
|
||||
class _CuerpoMusicaLocalState extends State<_CuerpoMusicaLocal> {
|
||||
final _fuente = FuenteMusicaLocalAutoImpl();
|
||||
late Future<String?> _carpetaActual;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_carpetaActual = _fuente.carpetaActual();
|
||||
}
|
||||
|
||||
Future<void> _elegirCarpeta(BuildContext context) async {
|
||||
final messenger = ScaffoldMessenger.of(context);
|
||||
final l10n = AppLocalizations.of(context);
|
||||
try {
|
||||
final uri = await _fuente.elegirCarpeta();
|
||||
if (uri == null) return; // Cancelled — no snackbar, matches the SAF
|
||||
// picker's own "nothing changed" affordance.
|
||||
if (!context.mounted) return;
|
||||
setState(() {
|
||||
_carpetaActual = Future.value(uri);
|
||||
});
|
||||
messenger.showSnackBar(
|
||||
SnackBar(content: Text(l10n.localMusicFolderUpdated)),
|
||||
);
|
||||
} catch (e) {
|
||||
if (!context.mounted) return;
|
||||
messenger.showSnackBar(
|
||||
SnackBar(content: Text(l10n.localMusicFolderSaveError(e.toString()))),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context);
|
||||
|
||||
return PluriGlassSurface(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
l10n.localMusicSectionDescription,
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
FutureBuilder<String?>(
|
||||
future: _carpetaActual,
|
||||
builder: (ctx, snap) {
|
||||
final carpeta = snap.data;
|
||||
return ListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
leading: const Icon(Icons.folder_outlined),
|
||||
title: Text(l10n.localMusicFolderTitle),
|
||||
subtitle: Text(
|
||||
(carpeta == null || carpeta.isEmpty)
|
||||
? l10n.localMusicFolderNotConfigured
|
||||
: nombreCarpetaDesdeUri(
|
||||
carpeta,
|
||||
nombreGenerico: l10n.localMusicFolderGenericName,
|
||||
),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
FutureBuilder<String?>(
|
||||
future: _carpetaActual,
|
||||
builder: (ctx, snap) {
|
||||
final configurada = (snap.data ?? '').isNotEmpty;
|
||||
return Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: OutlinedButton.icon(
|
||||
icon: const Icon(Icons.folder_open_rounded),
|
||||
label: Text(
|
||||
configurada
|
||||
? l10n.localMusicChangePath
|
||||
: l10n.localMusicChoosePath,
|
||||
),
|
||||
onPressed: () => _elegirCarpeta(context),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,28 +1,19 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:share_plus/share_plus.dart' show Share, XFile;
|
||||
|
||||
import '../estado/estado_grabacion.dart';
|
||||
import '../estado/estado_idioma.dart';
|
||||
import '../estado/estado_radio.dart';
|
||||
import '../l10n/gen/app_localizations.dart';
|
||||
import '../servicios/musica_local_auto.dart';
|
||||
import '../widgets/pluri_glass_surface.dart';
|
||||
import '../widgets/pluri_icon.dart';
|
||||
import '../widgets/pluri_layout.dart';
|
||||
import '../widgets/pluri_onboarding_dialog.dart';
|
||||
import '../widgets/pluri_premium_widgets.dart';
|
||||
import '../widgets/pluri_push_scaffold.dart';
|
||||
import 'ajustes/pantalla_ajustes_backup.dart';
|
||||
import 'ajustes/pantalla_ajustes_ecualizador.dart';
|
||||
import 'ajustes/pantalla_ajustes_emisora_preferida.dart';
|
||||
import 'ajustes/pantalla_ajustes_emisoras_personalizadas.dart';
|
||||
import 'ajustes/pantalla_ajustes_grabaciones.dart';
|
||||
import 'ajustes/pantalla_ajustes_grupos_favoritos.dart';
|
||||
import 'ajustes/pantalla_ajustes_idioma.dart';
|
||||
import 'ajustes/pantalla_ajustes_info.dart';
|
||||
import 'ajustes/pantalla_ajustes_musica_local.dart';
|
||||
import 'ajustes/pantalla_ajustes_orden_listas.dart';
|
||||
import 'ajustes/pantalla_ajustes_salida_audio.dart';
|
||||
import 'ajustes/pantalla_ajustes_timer_sueno.dart';
|
||||
@@ -56,13 +47,11 @@ class PantallaAjustes extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
/// Design ADR-3: the AUDIO and EMISORAS groups are grouped nav rows only —
|
||||
/// each [FilaAjuste] pushes its own detail screen via
|
||||
/// `PluriPushScaffold.push`, carrying zero inline controls in the root.
|
||||
///
|
||||
/// The remaining sections (GRABACIONES Y MÚSICA, APLICACIÓN) still render
|
||||
/// inline here pending WU3b, which decomposes them the same way. Sleep timer
|
||||
/// and backup/restore stay reachable throughout — nothing is dropped.
|
||||
/// Design ADR-3: the Settings root is grouped nav rows only — each
|
||||
/// [FilaAjuste] pushes its own detail screen via `PluriPushScaffold.push`,
|
||||
/// carrying zero inline controls. All 12 sections are now decomposed across
|
||||
/// WU3a (AUDIO, EMISORAS) and WU3b (GRABACIONES Y MÚSICA, APLICACIÓN); sleep
|
||||
/// timer and backup/restore stay reachable throughout — nothing was dropped.
|
||||
class _AjustesContent extends StatelessWidget {
|
||||
const _AjustesContent();
|
||||
|
||||
@@ -147,642 +136,63 @@ class _AjustesContent extends StatelessWidget {
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
const _SeccionGrabaciones(),
|
||||
GrupoAjustes(
|
||||
titulo: l10n.settingsGroupRecordingsTitle,
|
||||
filas: [
|
||||
FilaAjuste(
|
||||
icon: Icons.radio_button_checked_rounded,
|
||||
titulo: l10n.recordingsSectionTitle,
|
||||
onTap:
|
||||
() => PluriPushScaffold.push(
|
||||
context,
|
||||
(_) => const PantallaAjustesGrabaciones(),
|
||||
),
|
||||
),
|
||||
FilaAjuste(
|
||||
icon: Icons.library_music_outlined,
|
||||
titulo: l10n.localMusicSectionTitle,
|
||||
onTap:
|
||||
() => PluriPushScaffold.push(
|
||||
context,
|
||||
(_) => const PantallaAjustesMusicaLocal(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
const _SeccionMusicaLocal(),
|
||||
const SizedBox(height: 12),
|
||||
const _SeccionIdioma(),
|
||||
const SizedBox(height: 12),
|
||||
const _SeccionBackup(),
|
||||
const SizedBox(height: 12),
|
||||
const _SeccionInfo(),
|
||||
GrupoAjustes(
|
||||
titulo: l10n.settingsGroupApplicationTitle,
|
||||
filas: [
|
||||
FilaAjuste(
|
||||
icon: Icons.language_rounded,
|
||||
titulo: l10n.languageSectionTitle,
|
||||
onTap:
|
||||
() => PluriPushScaffold.push(
|
||||
context,
|
||||
(_) => const PantallaAjustesIdioma(),
|
||||
),
|
||||
),
|
||||
FilaAjuste(
|
||||
icon: Icons.backup_outlined,
|
||||
titulo: l10n.backupSectionTitle,
|
||||
onTap:
|
||||
() => PluriPushScaffold.push(
|
||||
context,
|
||||
(_) => const PantallaAjustesBackup(),
|
||||
),
|
||||
),
|
||||
FilaAjuste(
|
||||
icon: Icons.info_outline_rounded,
|
||||
titulo: l10n.infoSectionTitle,
|
||||
onTap:
|
||||
() => PluriPushScaffold.push(
|
||||
context,
|
||||
(_) => const PantallaAjustesInfo(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _SeccionGrabaciones extends StatelessWidget {
|
||||
const _SeccionGrabaciones();
|
||||
|
||||
Future<void> _seleccionarRuta(BuildContext context) async {
|
||||
final estado = context.read<EstadoGrabacion>();
|
||||
final messenger = ScaffoldMessenger.of(context);
|
||||
final l10n = AppLocalizations.of(context);
|
||||
final ruta = await FilePicker.platform.getDirectoryPath(
|
||||
dialogTitle: l10n.recordingsFolderDialogTitle,
|
||||
);
|
||||
if (ruta == null) return;
|
||||
try {
|
||||
await estado.cambiarDirectorio(ruta);
|
||||
if (!context.mounted) return;
|
||||
messenger.showSnackBar(
|
||||
SnackBar(content: Text(l10n.recordingsPathUpdated)),
|
||||
);
|
||||
} catch (e) {
|
||||
if (!context.mounted) return;
|
||||
messenger.showSnackBar(
|
||||
SnackBar(content: Text(l10n.recordingsPathSaveError(e.toString()))),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _restaurarRuta(BuildContext context) async {
|
||||
final estado = context.read<EstadoGrabacion>();
|
||||
final messenger = ScaffoldMessenger.of(context);
|
||||
final l10n = AppLocalizations.of(context);
|
||||
await estado.restaurarDirectorio();
|
||||
if (!context.mounted) return;
|
||||
messenger.showSnackBar(
|
||||
SnackBar(content: Text(l10n.recordingsDefaultFolderRestored)),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _abrirCarpeta(BuildContext context) async {
|
||||
final estado = context.read<EstadoGrabacion>();
|
||||
final messenger = ScaffoldMessenger.of(context);
|
||||
final l10n = AppLocalizations.of(context);
|
||||
try {
|
||||
final abierto = await estado.abrirDirectorio();
|
||||
if (!context.mounted) return;
|
||||
if (!abierto) {
|
||||
messenger.showSnackBar(
|
||||
SnackBar(content: Text(l10n.recordingsOpenFolderError(l10n.dash))),
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
if (!context.mounted) return;
|
||||
messenger.showSnackBar(
|
||||
SnackBar(content: Text(l10n.recordingsOpenFolderError(e.toString()))),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _editarTamanoMaximo(BuildContext context) async {
|
||||
final estado = context.read<EstadoGrabacion>();
|
||||
final l10n = AppLocalizations.of(context);
|
||||
final actualMb = _bytesAMegabytes(estado.maxBytes);
|
||||
final controller = TextEditingController(text: actualMb.toString());
|
||||
|
||||
final nuevoMb = await showModalBottomSheet<int>(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
showDragHandle: true,
|
||||
builder: (ctx) {
|
||||
final bottom = MediaQuery.viewInsetsOf(ctx).bottom;
|
||||
return Padding(
|
||||
padding: EdgeInsets.fromLTRB(20, 0, 20, bottom + 24),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
l10n.recordingsMaxSizeDialogTitle,
|
||||
style: Theme.of(ctx).textTheme.titleLarge,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextField(
|
||||
controller: controller,
|
||||
autofocus: true,
|
||||
keyboardType: TextInputType.number,
|
||||
decoration: InputDecoration(
|
||||
labelText: l10n.recordingsMaxSizeMbLabel,
|
||||
border: const OutlineInputBorder(),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
FilledButton.icon(
|
||||
onPressed: () {
|
||||
final value = int.tryParse(controller.text.trim());
|
||||
if (value == null || value <= 0) return;
|
||||
Navigator.of(ctx).pop(value);
|
||||
},
|
||||
icon: const Icon(Icons.save_rounded),
|
||||
label: Text(l10n.saveQuickAccessButton),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
controller.dispose();
|
||||
if (nuevoMb == null || !context.mounted) return;
|
||||
await estado.cambiarMaxBytes(nuevoMb * 1024 * 1024);
|
||||
if (!context.mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(l10n.recordingsMaxSizeSaved(nuevoMb))),
|
||||
);
|
||||
}
|
||||
|
||||
int _bytesAMegabytes(int bytes) =>
|
||||
(bytes / (1024 * 1024)).round().clamp(1, 1048576);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// Recording state lives in EstadoGrabacion (S4-R2): this section only
|
||||
// rebuilds on recording changes, never on playback notifications.
|
||||
final estado = context.watch<EstadoGrabacion>();
|
||||
final l10n = AppLocalizations.of(context);
|
||||
|
||||
return PluriGlassSurface(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
const Icon(Icons.radio_button_checked_rounded),
|
||||
const SizedBox(width: 12),
|
||||
Text(
|
||||
l10n.recordingsSectionTitle,
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
],
|
||||
),
|
||||
FutureBuilder<String>(
|
||||
future: estado.directorioEfectivo(),
|
||||
builder:
|
||||
(ctx, snap) => ListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
leading: const Icon(Icons.folder_outlined),
|
||||
title: Text(l10n.recordingsFolderTitle),
|
||||
subtitle: Text(
|
||||
snap.data ?? l10n.recordingsPathCalculating,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
),
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 8,
|
||||
children: [
|
||||
OutlinedButton.icon(
|
||||
icon: const Icon(Icons.folder_open_rounded),
|
||||
label: Text(l10n.recordingsChangePath),
|
||||
onPressed: () => _seleccionarRuta(context),
|
||||
),
|
||||
FilledButton.tonalIcon(
|
||||
icon: const Icon(Icons.folder_copy_rounded),
|
||||
label: Text(l10n.recordingsOpenFolder),
|
||||
onPressed: () => _abrirCarpeta(context),
|
||||
),
|
||||
IconButton.filledTonal(
|
||||
tooltip: l10n.recordingsUseDefaultPath,
|
||||
icon: const Icon(Icons.restore_rounded),
|
||||
onPressed: () => _restaurarRuta(context),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
ListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
leading: const Icon(Icons.sd_storage_rounded),
|
||||
title: Text(l10n.recordingsMaxSizeTitle),
|
||||
subtitle: Text(
|
||||
l10n.recordingsMaxSizeSubtitle(_bytesAMegabytes(estado.maxBytes)),
|
||||
),
|
||||
onTap: () => _editarTamanoMaximo(context),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
l10n.recordingsOriginalStreamHint,
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Local-music root-folder picker (android-auto-local-music task 9),
|
||||
/// mirroring [_SeccionGrabaciones]'s shape: `PluriGlassSurface` card,
|
||||
/// `FutureBuilder`-driven current-folder display, a single action button and
|
||||
/// snackbar feedback. Deliberately does NOT use `FilePicker.platform` (see
|
||||
/// tasks.md "Grounding corrections") — [FuenteMusicaLocalAutoImpl.elegirCarpeta]
|
||||
/// calls the NEW native `pickMusicFolder` channel method directly, since it
|
||||
/// needs a persistable-grant SAF tree URI, not a plain filesystem path.
|
||||
class _SeccionMusicaLocal extends StatefulWidget {
|
||||
const _SeccionMusicaLocal();
|
||||
|
||||
@override
|
||||
State<_SeccionMusicaLocal> createState() => _SeccionMusicaLocalState();
|
||||
}
|
||||
|
||||
class _SeccionMusicaLocalState extends State<_SeccionMusicaLocal> {
|
||||
final _fuente = FuenteMusicaLocalAutoImpl();
|
||||
late Future<String?> _carpetaActual;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_carpetaActual = _fuente.carpetaActual();
|
||||
}
|
||||
|
||||
Future<void> _elegirCarpeta(BuildContext context) async {
|
||||
final messenger = ScaffoldMessenger.of(context);
|
||||
final l10n = AppLocalizations.of(context);
|
||||
try {
|
||||
final uri = await _fuente.elegirCarpeta();
|
||||
if (uri == null) return; // Cancelled — no snackbar, matches the SAF
|
||||
// picker's own "nothing changed" affordance.
|
||||
if (!context.mounted) return;
|
||||
setState(() {
|
||||
_carpetaActual = Future.value(uri);
|
||||
});
|
||||
messenger.showSnackBar(
|
||||
SnackBar(content: Text(l10n.localMusicFolderUpdated)),
|
||||
);
|
||||
} catch (e) {
|
||||
if (!context.mounted) return;
|
||||
messenger.showSnackBar(
|
||||
SnackBar(content: Text(l10n.localMusicFolderSaveError(e.toString()))),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context);
|
||||
|
||||
return PluriGlassSurface(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
const Icon(Icons.library_music_outlined),
|
||||
const SizedBox(width: 12),
|
||||
Text(
|
||||
l10n.localMusicSectionTitle,
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
l10n.localMusicSectionDescription,
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
FutureBuilder<String?>(
|
||||
future: _carpetaActual,
|
||||
builder: (ctx, snap) {
|
||||
final carpeta = snap.data;
|
||||
return ListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
leading: const Icon(Icons.folder_outlined),
|
||||
title: Text(l10n.localMusicFolderTitle),
|
||||
subtitle: Text(
|
||||
(carpeta == null || carpeta.isEmpty)
|
||||
? l10n.localMusicFolderNotConfigured
|
||||
: nombreCarpetaDesdeUri(
|
||||
carpeta,
|
||||
nombreGenerico: l10n.localMusicFolderGenericName,
|
||||
),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
FutureBuilder<String?>(
|
||||
future: _carpetaActual,
|
||||
builder: (ctx, snap) {
|
||||
final configurada = (snap.data ?? '').isNotEmpty;
|
||||
return Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: OutlinedButton.icon(
|
||||
icon: const Icon(Icons.folder_open_rounded),
|
||||
label: Text(
|
||||
configurada
|
||||
? l10n.localMusicChangePath
|
||||
: l10n.localMusicChoosePath,
|
||||
),
|
||||
onPressed: () => _elegirCarpeta(context),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _SeccionIdioma extends StatelessWidget {
|
||||
const _SeccionIdioma();
|
||||
|
||||
static const _codigoSistema = 'system';
|
||||
static const _idiomas = [
|
||||
_IdiomaDisponible(Locale('en'), 'English'),
|
||||
_IdiomaDisponible(Locale('es'), 'Español'),
|
||||
_IdiomaDisponible(Locale('zh'), '中文'),
|
||||
_IdiomaDisponible(Locale('hi'), 'हिन्दी'),
|
||||
_IdiomaDisponible(Locale('ar'), 'العربية'),
|
||||
_IdiomaDisponible(Locale('pt'), 'Português'),
|
||||
_IdiomaDisponible(Locale('fr'), 'Français'),
|
||||
_IdiomaDisponible(Locale('ru'), 'Русский'),
|
||||
_IdiomaDisponible(Locale('de'), 'Deutsch'),
|
||||
_IdiomaDisponible(Locale('ja'), '日本語'),
|
||||
_IdiomaDisponible(Locale('id'), 'Bahasa Indonesia'),
|
||||
_IdiomaDisponible(Locale('bn'), 'বাংলা'),
|
||||
_IdiomaDisponible(Locale('it'), 'Italiano'),
|
||||
];
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context);
|
||||
final estadoIdioma = context.watch<EstadoIdioma>();
|
||||
final locale = estadoIdioma.localeSeleccionado;
|
||||
final valorActual = locale == null ? _codigoSistema : _codigoLocale(locale);
|
||||
|
||||
return PluriGlassSurface(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
const Icon(Icons.language_rounded),
|
||||
const SizedBox(width: 12),
|
||||
Text(
|
||||
l10n.languageSectionTitle,
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
l10n.languageSectionDescription,
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
DropdownButtonFormField<String>(
|
||||
initialValue: valorActual,
|
||||
decoration: InputDecoration(
|
||||
labelText: l10n.languageSectionTitle,
|
||||
border: const OutlineInputBorder(),
|
||||
),
|
||||
items: [
|
||||
DropdownMenuItem(
|
||||
value: _codigoSistema,
|
||||
child: Text(l10n.languageSystemDefault),
|
||||
),
|
||||
for (final idioma in _idiomas)
|
||||
DropdownMenuItem(
|
||||
value: _codigoLocale(idioma.locale),
|
||||
child: Text(idioma.nombreNativo),
|
||||
),
|
||||
],
|
||||
onChanged: (codigo) async {
|
||||
if (codigo == null) return;
|
||||
if (codigo == _codigoSistema) {
|
||||
await context.read<EstadoIdioma>().seleccionarSistema();
|
||||
if (!context.mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(l10n.languageUpdatedSystem)),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
final idioma = _idiomas.firstWhere(
|
||||
(item) => _codigoLocale(item.locale) == codigo,
|
||||
orElse: () => _idiomas.first,
|
||||
);
|
||||
await context.read<EstadoIdioma>().seleccionarLocale(
|
||||
idioma.locale,
|
||||
);
|
||||
if (!context.mounted) return;
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(l10n.languageUpdated(idioma.nombreNativo)),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
static String _codigoLocale(Locale locale) {
|
||||
final countryCode = locale.countryCode;
|
||||
if (countryCode == null || countryCode.isEmpty) {
|
||||
return locale.languageCode;
|
||||
}
|
||||
return '${locale.languageCode}_$countryCode';
|
||||
}
|
||||
}
|
||||
|
||||
class _IdiomaDisponible {
|
||||
const _IdiomaDisponible(this.locale, this.nombreNativo);
|
||||
|
||||
final Locale locale;
|
||||
final String nombreNativo;
|
||||
}
|
||||
|
||||
class _SeccionBackup extends StatelessWidget {
|
||||
const _SeccionBackup();
|
||||
|
||||
Future<void> _exportar(BuildContext context) async {
|
||||
final l10n = AppLocalizations.of(context);
|
||||
try {
|
||||
final estado = context.read<EstadoRadio>();
|
||||
// JSON serialization is owned by ServicioExportImport (S4-R4).
|
||||
final json = await estado.exportarConfigJson();
|
||||
|
||||
final dir = await getTemporaryDirectory();
|
||||
final file = File('${dir.path}/pluriwave-backup.json');
|
||||
await file.writeAsString(json);
|
||||
|
||||
await Share.shareXFiles(
|
||||
[XFile(file.path)],
|
||||
subject: l10n.backupShareSubject,
|
||||
text: l10n.backupShareText(DateTime.now().toLocal()),
|
||||
);
|
||||
} catch (e) {
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(l10n.backupExportError(e.toString()))),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _importar(BuildContext context) async {
|
||||
final l10n = AppLocalizations.of(context);
|
||||
try {
|
||||
final result = await FilePicker.platform.pickFiles(
|
||||
type: FileType.custom,
|
||||
allowedExtensions: ['json'],
|
||||
);
|
||||
if (result == null || result.files.single.path == null) return;
|
||||
|
||||
final file = File(result.files.single.path!);
|
||||
final contenido = await file.readAsString();
|
||||
if (!context.mounted) return;
|
||||
// Parsing is owned by ServicioExportImport (S4-R4): null = malformed.
|
||||
final json = context.read<EstadoRadio>().parsearConfigJson(contenido);
|
||||
if (json == null) {
|
||||
throw const FormatException('invalid backup file');
|
||||
}
|
||||
|
||||
if (context.mounted) {
|
||||
final confirmar = await showDialog<bool>(
|
||||
context: context,
|
||||
builder:
|
||||
(ctx) => AlertDialog(
|
||||
title: Text(AppLocalizations.of(ctx).backupImportTitle),
|
||||
content: Text(
|
||||
AppLocalizations.of(ctx).backupImportConfirmMessage,
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(ctx, false),
|
||||
child: Text(AppLocalizations.of(ctx).cancelAction),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () => Navigator.pop(ctx, true),
|
||||
child: Text(AppLocalizations.of(ctx).backupImportTitle),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
if (confirmar != true) return;
|
||||
if (context.mounted) {
|
||||
final estado = context.read<EstadoRadio>();
|
||||
final messenger = ScaffoldMessenger.of(context);
|
||||
await estado.importarConfig(json);
|
||||
messenger.showSnackBar(
|
||||
SnackBar(content: Text(l10n.backupImportSuccess)),
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(l10n.backupImportError(e.toString()))),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return PluriGlassSurface(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
const Icon(Icons.backup_outlined),
|
||||
const SizedBox(width: 12),
|
||||
Text(
|
||||
AppLocalizations.of(context).backupSectionTitle,
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
],
|
||||
),
|
||||
ListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
leading: const Icon(Icons.upload_outlined),
|
||||
title: Text(AppLocalizations.of(context).backupExportTitle),
|
||||
subtitle: Text(AppLocalizations.of(context).backupExportSubtitle),
|
||||
onTap: () => _exportar(context),
|
||||
),
|
||||
ListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
leading: const Icon(Icons.download_outlined),
|
||||
title: Text(AppLocalizations.of(context).backupImportTitle),
|
||||
subtitle: Text(AppLocalizations.of(context).backupImportSubtitle),
|
||||
onTap: () => _importar(context),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _SeccionInfo extends StatelessWidget {
|
||||
const _SeccionInfo();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Consumer<EstadoRadio>(
|
||||
builder:
|
||||
(ctx, estado, _) => PluriGlassSurface(
|
||||
child: Column(
|
||||
children: [
|
||||
FutureBuilder<PackageInfo>(
|
||||
future: PackageInfo.fromPlatform(),
|
||||
builder: (ctx, snap) {
|
||||
final version =
|
||||
snap.hasData
|
||||
? 'v${snap.data!.version}+${snap.data!.buildNumber}'
|
||||
: AppLocalizations.of(ctx).appVersionLoading;
|
||||
return ListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
leading: const PluriIcon(
|
||||
glyph: PluriIconGlyph.settings,
|
||||
variant: PluriIconVariant.filled,
|
||||
),
|
||||
title: Text(AppLocalizations.of(ctx).appTitle),
|
||||
subtitle: Text(
|
||||
AppLocalizations.of(ctx).appVersionSubtitle(version),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
FutureBuilder<int>(
|
||||
future: estado.favoritos.obtenerTodos().then((l) => l.length),
|
||||
builder:
|
||||
(ctx, snap) => ListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
leading: const Icon(Icons.favorite_outline_rounded),
|
||||
title: Text(
|
||||
AppLocalizations.of(ctx).savedFavoritesTitle,
|
||||
),
|
||||
trailing: Text(
|
||||
snap.data?.toString() ??
|
||||
AppLocalizations.of(ctx).dash,
|
||||
style: Theme.of(ctx).textTheme.bodyLarge,
|
||||
),
|
||||
),
|
||||
),
|
||||
ListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
leading: const Icon(Icons.help_outline_rounded),
|
||||
title: Text(AppLocalizations.of(ctx).helpTitle),
|
||||
subtitle: Text(AppLocalizations.of(ctx).helpSubtitle),
|
||||
trailing: const Icon(Icons.chevron_right_rounded),
|
||||
onTap: () => PluriOnboardingDialog.mostrar(ctx),
|
||||
),
|
||||
ListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
leading: const Icon(Icons.verified_outlined),
|
||||
title: Text(AppLocalizations.of(ctx).stationFilterTitle),
|
||||
subtitle: Text(
|
||||
AppLocalizations.of(ctx).stationFilterSubtitle,
|
||||
),
|
||||
trailing: Icon(
|
||||
Icons.check_circle_rounded,
|
||||
color: Theme.of(ctx).colorScheme.secondary,
|
||||
),
|
||||
),
|
||||
ListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
leading: const Icon(Icons.music_note_outlined),
|
||||
title: Text(AppLocalizations.of(ctx).backgroundAudioTitle),
|
||||
subtitle: Text(
|
||||
AppLocalizations.of(ctx).backgroundAudioSubtitle,
|
||||
),
|
||||
trailing: Icon(
|
||||
Icons.check_circle_rounded,
|
||||
color: Theme.of(ctx).colorScheme.secondary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user