feat(grabaciones): add recordings library screen
This commit is contained in:
@@ -7,6 +7,7 @@ import 'package:flutter/services.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
import '../l10n/gen/app_localizations.dart';
|
||||
import '../modelos/archivo_grabacion.dart';
|
||||
import '../modelos/emisora.dart';
|
||||
import '../servicios/servicio_grabacion_radio.dart';
|
||||
|
||||
@@ -101,6 +102,25 @@ class EstadoGrabacion extends ChangeNotifier {
|
||||
|
||||
Future<String> directorioEfectivo() => servicio.directorioEfectivo();
|
||||
|
||||
/// WU15, recordings-library: browsable listing of recording files
|
||||
/// already on disk. Thin delegate — no additional logic.
|
||||
Future<List<ArchivoGrabacion>> listarGrabaciones() =>
|
||||
servicio.listarGrabaciones();
|
||||
|
||||
/// WU15: deletes [ruta] and notifies listeners so the library screen's
|
||||
/// row disappears.
|
||||
Future<void> eliminarGrabacion(String ruta) async {
|
||||
await servicio.eliminarGrabacion(ruta);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// WU15: renames the recording at [ruta] to [nuevoNombre] and notifies
|
||||
/// listeners so the library screen reflects the new name.
|
||||
Future<void> renombrarGrabacion(String ruta, String nuevoNombre) async {
|
||||
await servicio.renombrarGrabacion(ruta, nuevoNombre);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<bool> abrirDirectorio() async {
|
||||
final ruta = await directorioEfectivo();
|
||||
await Directory(ruta).create(recursive: true);
|
||||
|
||||
@@ -209,6 +209,28 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"recordingsLibraryTitle": "My recordings",
|
||||
"recordingsLibraryStorageCaption": "{usedMb} MB of {totalMb} MB used",
|
||||
"@recordingsLibraryStorageCaption": {
|
||||
"placeholders": {
|
||||
"usedMb": {
|
||||
"type": "int"
|
||||
},
|
||||
"totalMb": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"recordingsLibraryEmptyTitle": "No recordings yet",
|
||||
"recordingsLibraryEmptySubtitle": "Recordings you save will appear here.",
|
||||
"recordingActionRename": "Rename",
|
||||
"recordingActionShare": "Share",
|
||||
"recordingActionDelete": "Delete",
|
||||
"recordingRenameDialogTitle": "Rename recording",
|
||||
"recordingRenameLabel": "Name",
|
||||
"recordingRenameEmptyError": "Enter a name",
|
||||
"recordingDeleteConfirmTitle": "Delete recording?",
|
||||
"recordingDeleteConfirmMessage": "This can't be undone.",
|
||||
"stationOrderTitle": "Station order",
|
||||
"stationOrderByName": "By name",
|
||||
"stationOrderByQuality": "By quality",
|
||||
|
||||
@@ -209,6 +209,28 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"recordingsLibraryTitle": "Mis grabaciones",
|
||||
"recordingsLibraryStorageCaption": "{usedMb} MB de {totalMb} MB usados",
|
||||
"@recordingsLibraryStorageCaption": {
|
||||
"placeholders": {
|
||||
"usedMb": {
|
||||
"type": "int"
|
||||
},
|
||||
"totalMb": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"recordingsLibraryEmptyTitle": "Todavía no hay grabaciones",
|
||||
"recordingsLibraryEmptySubtitle": "Las grabaciones que guardes van a aparecer acá.",
|
||||
"recordingActionRename": "Renombrar",
|
||||
"recordingActionShare": "Compartir",
|
||||
"recordingActionDelete": "Eliminar",
|
||||
"recordingRenameDialogTitle": "Renombrar grabación",
|
||||
"recordingRenameLabel": "Nombre",
|
||||
"recordingRenameEmptyError": "Ingresá un nombre",
|
||||
"recordingDeleteConfirmTitle": "¿Eliminar grabación?",
|
||||
"recordingDeleteConfirmMessage": "Esta acción no se puede deshacer.",
|
||||
"stationOrderTitle": "Orden de emisoras",
|
||||
"stationOrderByName": "Por nombre",
|
||||
"stationOrderByQuality": "Por calidad",
|
||||
|
||||
@@ -796,6 +796,78 @@ abstract class AppLocalizations {
|
||||
/// **'Límite de grabación actualizado a {size} MB'**
|
||||
String recordingsMaxSizeSaved(int size);
|
||||
|
||||
/// No description provided for @recordingsLibraryTitle.
|
||||
///
|
||||
/// In es, this message translates to:
|
||||
/// **'Mis grabaciones'**
|
||||
String get recordingsLibraryTitle;
|
||||
|
||||
/// No description provided for @recordingsLibraryStorageCaption.
|
||||
///
|
||||
/// In es, this message translates to:
|
||||
/// **'{usedMb} MB de {totalMb} MB usados'**
|
||||
String recordingsLibraryStorageCaption(int usedMb, int totalMb);
|
||||
|
||||
/// No description provided for @recordingsLibraryEmptyTitle.
|
||||
///
|
||||
/// In es, this message translates to:
|
||||
/// **'Todavía no hay grabaciones'**
|
||||
String get recordingsLibraryEmptyTitle;
|
||||
|
||||
/// No description provided for @recordingsLibraryEmptySubtitle.
|
||||
///
|
||||
/// In es, this message translates to:
|
||||
/// **'Las grabaciones que guardes van a aparecer acá.'**
|
||||
String get recordingsLibraryEmptySubtitle;
|
||||
|
||||
/// No description provided for @recordingActionRename.
|
||||
///
|
||||
/// In es, this message translates to:
|
||||
/// **'Renombrar'**
|
||||
String get recordingActionRename;
|
||||
|
||||
/// No description provided for @recordingActionShare.
|
||||
///
|
||||
/// In es, this message translates to:
|
||||
/// **'Compartir'**
|
||||
String get recordingActionShare;
|
||||
|
||||
/// No description provided for @recordingActionDelete.
|
||||
///
|
||||
/// In es, this message translates to:
|
||||
/// **'Eliminar'**
|
||||
String get recordingActionDelete;
|
||||
|
||||
/// No description provided for @recordingRenameDialogTitle.
|
||||
///
|
||||
/// In es, this message translates to:
|
||||
/// **'Renombrar grabación'**
|
||||
String get recordingRenameDialogTitle;
|
||||
|
||||
/// No description provided for @recordingRenameLabel.
|
||||
///
|
||||
/// In es, this message translates to:
|
||||
/// **'Nombre'**
|
||||
String get recordingRenameLabel;
|
||||
|
||||
/// No description provided for @recordingRenameEmptyError.
|
||||
///
|
||||
/// In es, this message translates to:
|
||||
/// **'Ingresá un nombre'**
|
||||
String get recordingRenameEmptyError;
|
||||
|
||||
/// No description provided for @recordingDeleteConfirmTitle.
|
||||
///
|
||||
/// In es, this message translates to:
|
||||
/// **'¿Eliminar grabación?'**
|
||||
String get recordingDeleteConfirmTitle;
|
||||
|
||||
/// No description provided for @recordingDeleteConfirmMessage.
|
||||
///
|
||||
/// In es, this message translates to:
|
||||
/// **'Esta acción no se puede deshacer.'**
|
||||
String get recordingDeleteConfirmMessage;
|
||||
|
||||
/// No description provided for @stationOrderTitle.
|
||||
///
|
||||
/// In es, this message translates to:
|
||||
|
||||
@@ -395,6 +395,46 @@ class AppLocalizationsAr extends AppLocalizations {
|
||||
return 'تم تحديث حد التسجيل إلى $size ميغابايت';
|
||||
}
|
||||
|
||||
@override
|
||||
String get recordingsLibraryTitle => 'Mis grabaciones';
|
||||
|
||||
@override
|
||||
String recordingsLibraryStorageCaption(int usedMb, int totalMb) {
|
||||
return '$usedMb MB de $totalMb MB usados';
|
||||
}
|
||||
|
||||
@override
|
||||
String get recordingsLibraryEmptyTitle => 'Todavía no hay grabaciones';
|
||||
|
||||
@override
|
||||
String get recordingsLibraryEmptySubtitle =>
|
||||
'Las grabaciones que guardes van a aparecer acá.';
|
||||
|
||||
@override
|
||||
String get recordingActionRename => 'Renombrar';
|
||||
|
||||
@override
|
||||
String get recordingActionShare => 'Compartir';
|
||||
|
||||
@override
|
||||
String get recordingActionDelete => 'Eliminar';
|
||||
|
||||
@override
|
||||
String get recordingRenameDialogTitle => 'Renombrar grabación';
|
||||
|
||||
@override
|
||||
String get recordingRenameLabel => 'Nombre';
|
||||
|
||||
@override
|
||||
String get recordingRenameEmptyError => 'Ingresá un nombre';
|
||||
|
||||
@override
|
||||
String get recordingDeleteConfirmTitle => '¿Eliminar grabación?';
|
||||
|
||||
@override
|
||||
String get recordingDeleteConfirmMessage =>
|
||||
'Esta acción no se puede deshacer.';
|
||||
|
||||
@override
|
||||
String get stationOrderTitle => 'ترتيب المحطات';
|
||||
|
||||
|
||||
@@ -398,6 +398,46 @@ class AppLocalizationsBn extends AppLocalizations {
|
||||
return 'রেকর্ডিং সীমা $size MB-এ আপডেট হয়েছে';
|
||||
}
|
||||
|
||||
@override
|
||||
String get recordingsLibraryTitle => 'Mis grabaciones';
|
||||
|
||||
@override
|
||||
String recordingsLibraryStorageCaption(int usedMb, int totalMb) {
|
||||
return '$usedMb MB de $totalMb MB usados';
|
||||
}
|
||||
|
||||
@override
|
||||
String get recordingsLibraryEmptyTitle => 'Todavía no hay grabaciones';
|
||||
|
||||
@override
|
||||
String get recordingsLibraryEmptySubtitle =>
|
||||
'Las grabaciones que guardes van a aparecer acá.';
|
||||
|
||||
@override
|
||||
String get recordingActionRename => 'Renombrar';
|
||||
|
||||
@override
|
||||
String get recordingActionShare => 'Compartir';
|
||||
|
||||
@override
|
||||
String get recordingActionDelete => 'Eliminar';
|
||||
|
||||
@override
|
||||
String get recordingRenameDialogTitle => 'Renombrar grabación';
|
||||
|
||||
@override
|
||||
String get recordingRenameLabel => 'Nombre';
|
||||
|
||||
@override
|
||||
String get recordingRenameEmptyError => 'Ingresá un nombre';
|
||||
|
||||
@override
|
||||
String get recordingDeleteConfirmTitle => '¿Eliminar grabación?';
|
||||
|
||||
@override
|
||||
String get recordingDeleteConfirmMessage =>
|
||||
'Esta acción no se puede deshacer.';
|
||||
|
||||
@override
|
||||
String get stationOrderTitle => 'স্টেশনের ক্রম';
|
||||
|
||||
|
||||
@@ -401,6 +401,46 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
return 'Aufnahmelimit auf $size MB aktualisiert';
|
||||
}
|
||||
|
||||
@override
|
||||
String get recordingsLibraryTitle => 'Mis grabaciones';
|
||||
|
||||
@override
|
||||
String recordingsLibraryStorageCaption(int usedMb, int totalMb) {
|
||||
return '$usedMb MB de $totalMb MB usados';
|
||||
}
|
||||
|
||||
@override
|
||||
String get recordingsLibraryEmptyTitle => 'Todavía no hay grabaciones';
|
||||
|
||||
@override
|
||||
String get recordingsLibraryEmptySubtitle =>
|
||||
'Las grabaciones que guardes van a aparecer acá.';
|
||||
|
||||
@override
|
||||
String get recordingActionRename => 'Renombrar';
|
||||
|
||||
@override
|
||||
String get recordingActionShare => 'Compartir';
|
||||
|
||||
@override
|
||||
String get recordingActionDelete => 'Eliminar';
|
||||
|
||||
@override
|
||||
String get recordingRenameDialogTitle => 'Renombrar grabación';
|
||||
|
||||
@override
|
||||
String get recordingRenameLabel => 'Nombre';
|
||||
|
||||
@override
|
||||
String get recordingRenameEmptyError => 'Ingresá un nombre';
|
||||
|
||||
@override
|
||||
String get recordingDeleteConfirmTitle => '¿Eliminar grabación?';
|
||||
|
||||
@override
|
||||
String get recordingDeleteConfirmMessage =>
|
||||
'Esta acción no se puede deshacer.';
|
||||
|
||||
@override
|
||||
String get stationOrderTitle => 'Senderreihenfolge';
|
||||
|
||||
|
||||
@@ -396,6 +396,45 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
return 'Recording limit updated to $size MB';
|
||||
}
|
||||
|
||||
@override
|
||||
String get recordingsLibraryTitle => 'My recordings';
|
||||
|
||||
@override
|
||||
String recordingsLibraryStorageCaption(int usedMb, int totalMb) {
|
||||
return '$usedMb MB of $totalMb MB used';
|
||||
}
|
||||
|
||||
@override
|
||||
String get recordingsLibraryEmptyTitle => 'No recordings yet';
|
||||
|
||||
@override
|
||||
String get recordingsLibraryEmptySubtitle =>
|
||||
'Recordings you save will appear here.';
|
||||
|
||||
@override
|
||||
String get recordingActionRename => 'Rename';
|
||||
|
||||
@override
|
||||
String get recordingActionShare => 'Share';
|
||||
|
||||
@override
|
||||
String get recordingActionDelete => 'Delete';
|
||||
|
||||
@override
|
||||
String get recordingRenameDialogTitle => 'Rename recording';
|
||||
|
||||
@override
|
||||
String get recordingRenameLabel => 'Name';
|
||||
|
||||
@override
|
||||
String get recordingRenameEmptyError => 'Enter a name';
|
||||
|
||||
@override
|
||||
String get recordingDeleteConfirmTitle => 'Delete recording?';
|
||||
|
||||
@override
|
||||
String get recordingDeleteConfirmMessage => 'This can\'t be undone.';
|
||||
|
||||
@override
|
||||
String get stationOrderTitle => 'Station order';
|
||||
|
||||
|
||||
@@ -399,6 +399,46 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
return 'Límite de grabación actualizado a $size MB';
|
||||
}
|
||||
|
||||
@override
|
||||
String get recordingsLibraryTitle => 'Mis grabaciones';
|
||||
|
||||
@override
|
||||
String recordingsLibraryStorageCaption(int usedMb, int totalMb) {
|
||||
return '$usedMb MB de $totalMb MB usados';
|
||||
}
|
||||
|
||||
@override
|
||||
String get recordingsLibraryEmptyTitle => 'Todavía no hay grabaciones';
|
||||
|
||||
@override
|
||||
String get recordingsLibraryEmptySubtitle =>
|
||||
'Las grabaciones que guardes van a aparecer acá.';
|
||||
|
||||
@override
|
||||
String get recordingActionRename => 'Renombrar';
|
||||
|
||||
@override
|
||||
String get recordingActionShare => 'Compartir';
|
||||
|
||||
@override
|
||||
String get recordingActionDelete => 'Eliminar';
|
||||
|
||||
@override
|
||||
String get recordingRenameDialogTitle => 'Renombrar grabación';
|
||||
|
||||
@override
|
||||
String get recordingRenameLabel => 'Nombre';
|
||||
|
||||
@override
|
||||
String get recordingRenameEmptyError => 'Ingresá un nombre';
|
||||
|
||||
@override
|
||||
String get recordingDeleteConfirmTitle => '¿Eliminar grabación?';
|
||||
|
||||
@override
|
||||
String get recordingDeleteConfirmMessage =>
|
||||
'Esta acción no se puede deshacer.';
|
||||
|
||||
@override
|
||||
String get stationOrderTitle => 'Orden de emisoras';
|
||||
|
||||
|
||||
@@ -403,6 +403,46 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
return 'Limite d’enregistrement mise à jour à $size Mo';
|
||||
}
|
||||
|
||||
@override
|
||||
String get recordingsLibraryTitle => 'Mis grabaciones';
|
||||
|
||||
@override
|
||||
String recordingsLibraryStorageCaption(int usedMb, int totalMb) {
|
||||
return '$usedMb MB de $totalMb MB usados';
|
||||
}
|
||||
|
||||
@override
|
||||
String get recordingsLibraryEmptyTitle => 'Todavía no hay grabaciones';
|
||||
|
||||
@override
|
||||
String get recordingsLibraryEmptySubtitle =>
|
||||
'Las grabaciones que guardes van a aparecer acá.';
|
||||
|
||||
@override
|
||||
String get recordingActionRename => 'Renombrar';
|
||||
|
||||
@override
|
||||
String get recordingActionShare => 'Compartir';
|
||||
|
||||
@override
|
||||
String get recordingActionDelete => 'Eliminar';
|
||||
|
||||
@override
|
||||
String get recordingRenameDialogTitle => 'Renombrar grabación';
|
||||
|
||||
@override
|
||||
String get recordingRenameLabel => 'Nombre';
|
||||
|
||||
@override
|
||||
String get recordingRenameEmptyError => 'Ingresá un nombre';
|
||||
|
||||
@override
|
||||
String get recordingDeleteConfirmTitle => '¿Eliminar grabación?';
|
||||
|
||||
@override
|
||||
String get recordingDeleteConfirmMessage =>
|
||||
'Esta acción no se puede deshacer.';
|
||||
|
||||
@override
|
||||
String get stationOrderTitle => 'Ordre des stations';
|
||||
|
||||
|
||||
@@ -396,6 +396,46 @@ class AppLocalizationsHi extends AppLocalizations {
|
||||
return 'रिकॉर्डिंग सीमा $size MB पर अपडेट हुई';
|
||||
}
|
||||
|
||||
@override
|
||||
String get recordingsLibraryTitle => 'Mis grabaciones';
|
||||
|
||||
@override
|
||||
String recordingsLibraryStorageCaption(int usedMb, int totalMb) {
|
||||
return '$usedMb MB de $totalMb MB usados';
|
||||
}
|
||||
|
||||
@override
|
||||
String get recordingsLibraryEmptyTitle => 'Todavía no hay grabaciones';
|
||||
|
||||
@override
|
||||
String get recordingsLibraryEmptySubtitle =>
|
||||
'Las grabaciones que guardes van a aparecer acá.';
|
||||
|
||||
@override
|
||||
String get recordingActionRename => 'Renombrar';
|
||||
|
||||
@override
|
||||
String get recordingActionShare => 'Compartir';
|
||||
|
||||
@override
|
||||
String get recordingActionDelete => 'Eliminar';
|
||||
|
||||
@override
|
||||
String get recordingRenameDialogTitle => 'Renombrar grabación';
|
||||
|
||||
@override
|
||||
String get recordingRenameLabel => 'Nombre';
|
||||
|
||||
@override
|
||||
String get recordingRenameEmptyError => 'Ingresá un nombre';
|
||||
|
||||
@override
|
||||
String get recordingDeleteConfirmTitle => '¿Eliminar grabación?';
|
||||
|
||||
@override
|
||||
String get recordingDeleteConfirmMessage =>
|
||||
'Esta acción no se puede deshacer.';
|
||||
|
||||
@override
|
||||
String get stationOrderTitle => 'स्टेशन क्रम';
|
||||
|
||||
|
||||
@@ -397,6 +397,46 @@ class AppLocalizationsId extends AppLocalizations {
|
||||
return 'Batas rekaman diperbarui menjadi $size MB';
|
||||
}
|
||||
|
||||
@override
|
||||
String get recordingsLibraryTitle => 'Mis grabaciones';
|
||||
|
||||
@override
|
||||
String recordingsLibraryStorageCaption(int usedMb, int totalMb) {
|
||||
return '$usedMb MB de $totalMb MB usados';
|
||||
}
|
||||
|
||||
@override
|
||||
String get recordingsLibraryEmptyTitle => 'Todavía no hay grabaciones';
|
||||
|
||||
@override
|
||||
String get recordingsLibraryEmptySubtitle =>
|
||||
'Las grabaciones que guardes van a aparecer acá.';
|
||||
|
||||
@override
|
||||
String get recordingActionRename => 'Renombrar';
|
||||
|
||||
@override
|
||||
String get recordingActionShare => 'Compartir';
|
||||
|
||||
@override
|
||||
String get recordingActionDelete => 'Eliminar';
|
||||
|
||||
@override
|
||||
String get recordingRenameDialogTitle => 'Renombrar grabación';
|
||||
|
||||
@override
|
||||
String get recordingRenameLabel => 'Nombre';
|
||||
|
||||
@override
|
||||
String get recordingRenameEmptyError => 'Ingresá un nombre';
|
||||
|
||||
@override
|
||||
String get recordingDeleteConfirmTitle => '¿Eliminar grabación?';
|
||||
|
||||
@override
|
||||
String get recordingDeleteConfirmMessage =>
|
||||
'Esta acción no se puede deshacer.';
|
||||
|
||||
@override
|
||||
String get stationOrderTitle => 'Urutan stasiun';
|
||||
|
||||
|
||||
@@ -399,6 +399,46 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
return 'Limite di registrazione aggiornato a $size MB';
|
||||
}
|
||||
|
||||
@override
|
||||
String get recordingsLibraryTitle => 'Mis grabaciones';
|
||||
|
||||
@override
|
||||
String recordingsLibraryStorageCaption(int usedMb, int totalMb) {
|
||||
return '$usedMb MB de $totalMb MB usados';
|
||||
}
|
||||
|
||||
@override
|
||||
String get recordingsLibraryEmptyTitle => 'Todavía no hay grabaciones';
|
||||
|
||||
@override
|
||||
String get recordingsLibraryEmptySubtitle =>
|
||||
'Las grabaciones que guardes van a aparecer acá.';
|
||||
|
||||
@override
|
||||
String get recordingActionRename => 'Renombrar';
|
||||
|
||||
@override
|
||||
String get recordingActionShare => 'Compartir';
|
||||
|
||||
@override
|
||||
String get recordingActionDelete => 'Eliminar';
|
||||
|
||||
@override
|
||||
String get recordingRenameDialogTitle => 'Renombrar grabación';
|
||||
|
||||
@override
|
||||
String get recordingRenameLabel => 'Nombre';
|
||||
|
||||
@override
|
||||
String get recordingRenameEmptyError => 'Ingresá un nombre';
|
||||
|
||||
@override
|
||||
String get recordingDeleteConfirmTitle => '¿Eliminar grabación?';
|
||||
|
||||
@override
|
||||
String get recordingDeleteConfirmMessage =>
|
||||
'Esta acción no se puede deshacer.';
|
||||
|
||||
@override
|
||||
String get stationOrderTitle => 'Ordine emittenti';
|
||||
|
||||
|
||||
@@ -386,6 +386,46 @@ class AppLocalizationsJa extends AppLocalizations {
|
||||
return '録音上限を $size MB に更新しました';
|
||||
}
|
||||
|
||||
@override
|
||||
String get recordingsLibraryTitle => 'Mis grabaciones';
|
||||
|
||||
@override
|
||||
String recordingsLibraryStorageCaption(int usedMb, int totalMb) {
|
||||
return '$usedMb MB de $totalMb MB usados';
|
||||
}
|
||||
|
||||
@override
|
||||
String get recordingsLibraryEmptyTitle => 'Todavía no hay grabaciones';
|
||||
|
||||
@override
|
||||
String get recordingsLibraryEmptySubtitle =>
|
||||
'Las grabaciones que guardes van a aparecer acá.';
|
||||
|
||||
@override
|
||||
String get recordingActionRename => 'Renombrar';
|
||||
|
||||
@override
|
||||
String get recordingActionShare => 'Compartir';
|
||||
|
||||
@override
|
||||
String get recordingActionDelete => 'Eliminar';
|
||||
|
||||
@override
|
||||
String get recordingRenameDialogTitle => 'Renombrar grabación';
|
||||
|
||||
@override
|
||||
String get recordingRenameLabel => 'Nombre';
|
||||
|
||||
@override
|
||||
String get recordingRenameEmptyError => 'Ingresá un nombre';
|
||||
|
||||
@override
|
||||
String get recordingDeleteConfirmTitle => '¿Eliminar grabación?';
|
||||
|
||||
@override
|
||||
String get recordingDeleteConfirmMessage =>
|
||||
'Esta acción no se puede deshacer.';
|
||||
|
||||
@override
|
||||
String get stationOrderTitle => '局の並び順';
|
||||
|
||||
|
||||
@@ -398,6 +398,46 @@ class AppLocalizationsPt extends AppLocalizations {
|
||||
return 'Limite de gravação atualizado para $size MB';
|
||||
}
|
||||
|
||||
@override
|
||||
String get recordingsLibraryTitle => 'Mis grabaciones';
|
||||
|
||||
@override
|
||||
String recordingsLibraryStorageCaption(int usedMb, int totalMb) {
|
||||
return '$usedMb MB de $totalMb MB usados';
|
||||
}
|
||||
|
||||
@override
|
||||
String get recordingsLibraryEmptyTitle => 'Todavía no hay grabaciones';
|
||||
|
||||
@override
|
||||
String get recordingsLibraryEmptySubtitle =>
|
||||
'Las grabaciones que guardes van a aparecer acá.';
|
||||
|
||||
@override
|
||||
String get recordingActionRename => 'Renombrar';
|
||||
|
||||
@override
|
||||
String get recordingActionShare => 'Compartir';
|
||||
|
||||
@override
|
||||
String get recordingActionDelete => 'Eliminar';
|
||||
|
||||
@override
|
||||
String get recordingRenameDialogTitle => 'Renombrar grabación';
|
||||
|
||||
@override
|
||||
String get recordingRenameLabel => 'Nombre';
|
||||
|
||||
@override
|
||||
String get recordingRenameEmptyError => 'Ingresá un nombre';
|
||||
|
||||
@override
|
||||
String get recordingDeleteConfirmTitle => '¿Eliminar grabación?';
|
||||
|
||||
@override
|
||||
String get recordingDeleteConfirmMessage =>
|
||||
'Esta acción no se puede deshacer.';
|
||||
|
||||
@override
|
||||
String get stationOrderTitle => 'Ordem das estações';
|
||||
|
||||
|
||||
@@ -399,6 +399,46 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
return 'Лимит записи обновлён до $size МБ';
|
||||
}
|
||||
|
||||
@override
|
||||
String get recordingsLibraryTitle => 'Mis grabaciones';
|
||||
|
||||
@override
|
||||
String recordingsLibraryStorageCaption(int usedMb, int totalMb) {
|
||||
return '$usedMb MB de $totalMb MB usados';
|
||||
}
|
||||
|
||||
@override
|
||||
String get recordingsLibraryEmptyTitle => 'Todavía no hay grabaciones';
|
||||
|
||||
@override
|
||||
String get recordingsLibraryEmptySubtitle =>
|
||||
'Las grabaciones que guardes van a aparecer acá.';
|
||||
|
||||
@override
|
||||
String get recordingActionRename => 'Renombrar';
|
||||
|
||||
@override
|
||||
String get recordingActionShare => 'Compartir';
|
||||
|
||||
@override
|
||||
String get recordingActionDelete => 'Eliminar';
|
||||
|
||||
@override
|
||||
String get recordingRenameDialogTitle => 'Renombrar grabación';
|
||||
|
||||
@override
|
||||
String get recordingRenameLabel => 'Nombre';
|
||||
|
||||
@override
|
||||
String get recordingRenameEmptyError => 'Ingresá un nombre';
|
||||
|
||||
@override
|
||||
String get recordingDeleteConfirmTitle => '¿Eliminar grabación?';
|
||||
|
||||
@override
|
||||
String get recordingDeleteConfirmMessage =>
|
||||
'Esta acción no se puede deshacer.';
|
||||
|
||||
@override
|
||||
String get stationOrderTitle => 'Порядок станций';
|
||||
|
||||
|
||||
@@ -384,6 +384,46 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
return '录音限制已更新为 $size MB';
|
||||
}
|
||||
|
||||
@override
|
||||
String get recordingsLibraryTitle => 'Mis grabaciones';
|
||||
|
||||
@override
|
||||
String recordingsLibraryStorageCaption(int usedMb, int totalMb) {
|
||||
return '$usedMb MB de $totalMb MB usados';
|
||||
}
|
||||
|
||||
@override
|
||||
String get recordingsLibraryEmptyTitle => 'Todavía no hay grabaciones';
|
||||
|
||||
@override
|
||||
String get recordingsLibraryEmptySubtitle =>
|
||||
'Las grabaciones que guardes van a aparecer acá.';
|
||||
|
||||
@override
|
||||
String get recordingActionRename => 'Renombrar';
|
||||
|
||||
@override
|
||||
String get recordingActionShare => 'Compartir';
|
||||
|
||||
@override
|
||||
String get recordingActionDelete => 'Eliminar';
|
||||
|
||||
@override
|
||||
String get recordingRenameDialogTitle => 'Renombrar grabación';
|
||||
|
||||
@override
|
||||
String get recordingRenameLabel => 'Nombre';
|
||||
|
||||
@override
|
||||
String get recordingRenameEmptyError => 'Ingresá un nombre';
|
||||
|
||||
@override
|
||||
String get recordingDeleteConfirmTitle => '¿Eliminar grabación?';
|
||||
|
||||
@override
|
||||
String get recordingDeleteConfirmMessage =>
|
||||
'Esta acción no se puede deshacer.';
|
||||
|
||||
@override
|
||||
String get stationOrderTitle => '电台排序';
|
||||
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
/// A single recorded audio file on disk, as listed by
|
||||
/// `ServicioGrabacionRadio.listarGrabaciones()` (WU15, recordings-library
|
||||
/// spec, "Browsable Recordings List"). Pure filesystem metadata only — no
|
||||
/// embedded-audio decoding here; duration is resolved separately and
|
||||
/// lazily by the screen's own playback abstraction, since decoding audio
|
||||
/// is not something this service conceptually does today.
|
||||
class ArchivoGrabacion {
|
||||
const ArchivoGrabacion({
|
||||
required this.ruta,
|
||||
required this.nombre,
|
||||
required this.fecha,
|
||||
required this.tamanoBytes,
|
||||
});
|
||||
|
||||
/// Full filesystem path — the identity used for playback, rename and
|
||||
/// delete.
|
||||
final String ruta;
|
||||
|
||||
/// Display name: the filename without its extension.
|
||||
final String nombre;
|
||||
|
||||
/// Last-modified timestamp, used as the recording's date.
|
||||
final DateTime fecha;
|
||||
|
||||
final int tamanoBytes;
|
||||
}
|
||||
@@ -0,0 +1,437 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:just_audio/just_audio.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:share_plus/share_plus.dart' show Share, XFile;
|
||||
|
||||
import '../estado/estado_grabacion.dart';
|
||||
import '../l10n/gen/app_localizations.dart';
|
||||
import '../modelos/archivo_grabacion.dart';
|
||||
import '../widgets/pluri_glass_surface.dart';
|
||||
import '../widgets/pluri_icon.dart';
|
||||
import '../widgets/pluri_layout.dart';
|
||||
import '../widgets/pluri_premium_widgets.dart';
|
||||
import '../widgets/pluri_push_scaffold.dart';
|
||||
|
||||
/// Inline-preview playback + duration lookup for a single recording file at
|
||||
/// a time (WU15, recordings-library spec — "Row playback starts and
|
||||
/// stops"). Kept separate from `ServicioAudio` (never touched — that class
|
||||
/// is coupled to live radio-stream transport/reconnect, unrelated to
|
||||
/// previewing an already-finished local recording).
|
||||
///
|
||||
/// The real implementation ([_ReproductorGrabacionesJustAudio]) wraps
|
||||
/// `just_audio.AudioPlayer`, which needs platform `MethodChannel`s this
|
||||
/// suite does not mock — the same constraint `cola_local_test.dart`
|
||||
/// documents for `PluriWaveAudioHandler` — so it is static-review-only.
|
||||
/// Every test in `pantalla_grabaciones_test.dart` injects a fake instead.
|
||||
abstract class ReproductorGrabaciones {
|
||||
/// Path currently loaded/playing, or null.
|
||||
String? get rutaActual;
|
||||
|
||||
/// True while [rutaActual] is actively playing (not just loaded/paused).
|
||||
bool get reproduciendo;
|
||||
|
||||
/// Loads [ruta]'s metadata and returns its duration, without playing it.
|
||||
Future<Duration?> duracionDe(String ruta);
|
||||
|
||||
/// Starts playback of [ruta]. If [ruta] is already the one playing, this
|
||||
/// pauses it instead — the row's play/pause affordance is a toggle.
|
||||
Future<void> alternar(String ruta);
|
||||
|
||||
Future<void> detener();
|
||||
Future<void> dispose();
|
||||
}
|
||||
|
||||
class _ReproductorGrabacionesJustAudio implements ReproductorGrabaciones {
|
||||
final AudioPlayer _player = AudioPlayer();
|
||||
String? _rutaActual;
|
||||
|
||||
@override
|
||||
String? get rutaActual => _rutaActual;
|
||||
|
||||
@override
|
||||
bool get reproduciendo => _player.playing;
|
||||
|
||||
@override
|
||||
Future<Duration?> duracionDe(String ruta) async {
|
||||
final sonda = AudioPlayer();
|
||||
try {
|
||||
return await sonda.setFilePath(ruta);
|
||||
} finally {
|
||||
await sonda.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> alternar(String ruta) async {
|
||||
if (_rutaActual == ruta && _player.playing) {
|
||||
await _player.pause();
|
||||
return;
|
||||
}
|
||||
if (_rutaActual != ruta) {
|
||||
await _player.setFilePath(ruta);
|
||||
_rutaActual = ruta;
|
||||
}
|
||||
await _player.play();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> detener() async {
|
||||
await _player.stop();
|
||||
_rutaActual = null;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> dispose() => _player.dispose();
|
||||
}
|
||||
|
||||
/// WU15: the recordings library — storage usage, browsable rows with
|
||||
/// inline playback, and a "⋮" menu constrained to exactly
|
||||
/// Rename/Share/Delete (`recordings-library` spec). Distinct from
|
||||
/// `PantallaAjustesGrabaciones` (WU3b), which is the folder/size-limit
|
||||
/// SETTINGS screen, not this browsable file list.
|
||||
class PantallaGrabaciones extends StatefulWidget {
|
||||
const PantallaGrabaciones({
|
||||
super.key,
|
||||
ReproductorGrabaciones? reproductor,
|
||||
Future<void> Function(String ruta)? compartir,
|
||||
}) : _reproductorInyectado = reproductor,
|
||||
_compartirInyectado = compartir;
|
||||
|
||||
final ReproductorGrabaciones? _reproductorInyectado;
|
||||
final Future<void> Function(String ruta)? _compartirInyectado;
|
||||
|
||||
@override
|
||||
State<PantallaGrabaciones> createState() => _PantallaGrabacionesState();
|
||||
}
|
||||
|
||||
class _PantallaGrabacionesState extends State<PantallaGrabaciones> {
|
||||
late final ReproductorGrabaciones _reproductor =
|
||||
widget._reproductorInyectado ?? _ReproductorGrabacionesJustAudio();
|
||||
late final Future<void> Function(String ruta) _compartir =
|
||||
widget._compartirInyectado ?? (ruta) => Share.shareXFiles([XFile(ruta)]);
|
||||
|
||||
late Future<List<ArchivoGrabacion>> _grabaciones;
|
||||
final Map<String, Future<Duration?>> _duracionCache = {};
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_recargar();
|
||||
}
|
||||
|
||||
void _recargar() {
|
||||
_duracionCache.clear();
|
||||
_grabaciones = context.read<EstadoGrabacion>().listarGrabaciones();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
unawaited(_reproductor.dispose());
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<Duration?> _duracionPara(String ruta) =>
|
||||
_duracionCache.putIfAbsent(ruta, () => _reproductor.duracionDe(ruta));
|
||||
|
||||
Future<void> _alternarReproduccion(String ruta) async {
|
||||
await _reproductor.alternar(ruta);
|
||||
if (!mounted) return;
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
Future<void> _manejarAccion(String accion, ArchivoGrabacion archivo) async {
|
||||
// Yield one microtask before opening any dialog: `PopupMenuButton`'s own
|
||||
// route is still popping off the Navigator at the moment `onSelected`
|
||||
// fires, and pushing a new route (showDialog) synchronously against
|
||||
// that in-flight pop can race its close transition.
|
||||
await Future<void>.delayed(Duration.zero);
|
||||
if (accion == 'rename') {
|
||||
await _renombrar(archivo);
|
||||
return;
|
||||
}
|
||||
if (accion == 'share') {
|
||||
await _compartir(archivo.ruta);
|
||||
return;
|
||||
}
|
||||
if (accion == 'delete') {
|
||||
await _eliminar(archivo);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _renombrar(ArchivoGrabacion archivo) async {
|
||||
final nuevoNombre = await showDialog<String>(
|
||||
context: context,
|
||||
builder: (_) => _DialogoRenombrarGrabacion(nombreActual: archivo.nombre),
|
||||
);
|
||||
if (nuevoNombre == null || nuevoNombre.trim().isEmpty) return;
|
||||
if (!mounted) return;
|
||||
await context.read<EstadoGrabacion>().renombrarGrabacion(
|
||||
archivo.ruta,
|
||||
nuevoNombre.trim(),
|
||||
);
|
||||
if (!mounted) return;
|
||||
setState(_recargar);
|
||||
}
|
||||
|
||||
Future<void> _eliminar(ArchivoGrabacion archivo) async {
|
||||
final l10n = AppLocalizations.of(context);
|
||||
final confirmar = await showDialog<bool>(
|
||||
context: context,
|
||||
builder:
|
||||
(ctx) => AlertDialog(
|
||||
title: Text(l10n.recordingDeleteConfirmTitle),
|
||||
content: Text(l10n.recordingDeleteConfirmMessage),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(ctx, false),
|
||||
child: Text(l10n.cancelAction),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () => Navigator.pop(ctx, true),
|
||||
child: Text(l10n.recordingActionDelete),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
if (confirmar != true) return;
|
||||
if (!mounted) return;
|
||||
await context.read<EstadoGrabacion>().eliminarGrabacion(archivo.ruta);
|
||||
if (!mounted) return;
|
||||
setState(_recargar);
|
||||
}
|
||||
|
||||
String _formatearDuracion(Duration? d) {
|
||||
if (d == null) return '--:--';
|
||||
final m = d.inMinutes.remainder(60).toString().padLeft(2, '0');
|
||||
final s = d.inSeconds.remainder(60).toString().padLeft(2, '0');
|
||||
final h = d.inHours;
|
||||
return h > 0 ? '$h:$m:$s' : '$m:$s';
|
||||
}
|
||||
|
||||
String _formatearFecha(DateTime fecha) {
|
||||
final dia = fecha.day.toString().padLeft(2, '0');
|
||||
final mes = fecha.month.toString().padLeft(2, '0');
|
||||
return '$dia/$mes/${fecha.year}';
|
||||
}
|
||||
|
||||
String _formatearBytes(int bytes) {
|
||||
if (bytes < 1024) return '$bytes B';
|
||||
if (bytes < 1024 * 1024) return '${(bytes / 1024).toStringAsFixed(1)} KB';
|
||||
return '${(bytes / (1024 * 1024)).toStringAsFixed(1)} MB';
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context);
|
||||
|
||||
return PluriPushScaffold(
|
||||
title: l10n.recordingsLibraryTitle,
|
||||
body: FutureBuilder<List<ArchivoGrabacion>>(
|
||||
future: _grabaciones,
|
||||
builder: (context, snap) {
|
||||
final archivos = snap.data ?? const <ArchivoGrabacion>[];
|
||||
return ListView(
|
||||
padding: PluriLayout.pageContentPadding,
|
||||
children: [
|
||||
_BarraDeAlmacenamiento(archivos: archivos),
|
||||
const SizedBox(height: 12),
|
||||
if (snap.connectionState == ConnectionState.done &&
|
||||
archivos.isEmpty)
|
||||
PluriEmptyState(
|
||||
glyph: PluriIconGlyph.player,
|
||||
title: l10n.recordingsLibraryEmptyTitle,
|
||||
subtitle: l10n.recordingsLibraryEmptySubtitle,
|
||||
)
|
||||
else
|
||||
for (final archivo in archivos)
|
||||
_FilaGrabacion(
|
||||
archivo: archivo,
|
||||
reproduciendo:
|
||||
_reproductor.rutaActual == archivo.ruta &&
|
||||
_reproductor.reproduciendo,
|
||||
duracion: _duracionPara(archivo.ruta),
|
||||
formatearDuracion: _formatearDuracion,
|
||||
formatearFecha: _formatearFecha,
|
||||
formatearBytes: _formatearBytes,
|
||||
onAlternarReproduccion:
|
||||
() => _alternarReproduccion(archivo.ruta),
|
||||
onAccionMenu: (accion) => _manejarAccion(accion, archivo),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _BarraDeAlmacenamiento extends StatelessWidget {
|
||||
const _BarraDeAlmacenamiento({required this.archivos});
|
||||
|
||||
final List<ArchivoGrabacion> archivos;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context);
|
||||
final estado = context.watch<EstadoGrabacion>();
|
||||
final usadoBytes = archivos.fold<int>(0, (s, a) => s + a.tamanoBytes);
|
||||
final totalBytes = estado.maxBytes <= 0 ? 1 : estado.maxBytes;
|
||||
final fraccion = (usadoBytes / totalBytes).clamp(0.0, 1.0);
|
||||
final usadoMb = (usadoBytes / (1024 * 1024)).round();
|
||||
final totalMb = (totalBytes / (1024 * 1024)).round();
|
||||
|
||||
return PluriGlassSurface(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: LinearProgressIndicator(value: fraccion, minHeight: 8),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
l10n.recordingsLibraryStorageCaption(usadoMb, totalMb),
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _FilaGrabacion extends StatelessWidget {
|
||||
const _FilaGrabacion({
|
||||
required this.archivo,
|
||||
required this.reproduciendo,
|
||||
required this.duracion,
|
||||
required this.formatearDuracion,
|
||||
required this.formatearFecha,
|
||||
required this.formatearBytes,
|
||||
required this.onAlternarReproduccion,
|
||||
required this.onAccionMenu,
|
||||
});
|
||||
|
||||
final ArchivoGrabacion archivo;
|
||||
final bool reproduciendo;
|
||||
final Future<Duration?> duracion;
|
||||
final String Function(Duration?) formatearDuracion;
|
||||
final String Function(DateTime) formatearFecha;
|
||||
final String Function(int) formatearBytes;
|
||||
final VoidCallback onAlternarReproduccion;
|
||||
final ValueChanged<String> onAccionMenu;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context);
|
||||
|
||||
return PluriGlassSurface(
|
||||
child: ListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
leading: IconButton(
|
||||
icon: Icon(
|
||||
reproduciendo
|
||||
? Icons.pause_circle_filled_rounded
|
||||
: Icons.play_circle_fill_rounded,
|
||||
),
|
||||
onPressed: onAlternarReproduccion,
|
||||
),
|
||||
title: Text(archivo.nombre),
|
||||
subtitle: FutureBuilder<Duration?>(
|
||||
future: duracion,
|
||||
builder: (context, snap) {
|
||||
return Text(
|
||||
'${formatearFecha(archivo.fecha)} · '
|
||||
'${formatearDuracion(snap.data)} · '
|
||||
'${formatearBytes(archivo.tamanoBytes)}',
|
||||
);
|
||||
},
|
||||
),
|
||||
trailing: PopupMenuButton<String>(
|
||||
icon: const Icon(Icons.more_vert_rounded),
|
||||
onSelected: onAccionMenu,
|
||||
itemBuilder:
|
||||
(context) => [
|
||||
PopupMenuItem(
|
||||
value: 'rename',
|
||||
child: Text(l10n.recordingActionRename),
|
||||
),
|
||||
PopupMenuItem(
|
||||
value: 'share',
|
||||
child: Text(l10n.recordingActionShare),
|
||||
),
|
||||
PopupMenuItem(
|
||||
value: 'delete',
|
||||
child: Text(l10n.recordingActionDelete),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Owns its own [TextEditingController] and disposes it in its own
|
||||
/// [State.dispose] — the SAFE pattern documented for this codebase (see
|
||||
/// `_DialogoEdicionDispositivo` in `pantalla_ajustes_salida_audio.dart`),
|
||||
/// deliberately NOT the pre-existing anti-pattern (dispose right after the
|
||||
/// sheet/dialog Future resolves, racing the close animation) already
|
||||
/// tracked elsewhere in this codebase as a separate, un-fixed defect.
|
||||
class _DialogoRenombrarGrabacion extends StatefulWidget {
|
||||
const _DialogoRenombrarGrabacion({required this.nombreActual});
|
||||
|
||||
final String nombreActual;
|
||||
|
||||
@override
|
||||
State<_DialogoRenombrarGrabacion> createState() =>
|
||||
_DialogoRenombrarGrabacionState();
|
||||
}
|
||||
|
||||
class _DialogoRenombrarGrabacionState
|
||||
extends State<_DialogoRenombrarGrabacion> {
|
||||
late final _controller = TextEditingController(text: widget.nombreActual);
|
||||
String? _error;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _confirmar(AppLocalizations l10n) {
|
||||
final valor = _controller.text.trim();
|
||||
if (valor.isEmpty) {
|
||||
setState(() => _error = l10n.recordingRenameEmptyError);
|
||||
return;
|
||||
}
|
||||
Navigator.pop(context, valor);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context);
|
||||
return AlertDialog(
|
||||
title: Text(l10n.recordingRenameDialogTitle),
|
||||
content: TextField(
|
||||
controller: _controller,
|
||||
autofocus: true,
|
||||
decoration: InputDecoration(
|
||||
labelText: l10n.recordingRenameLabel,
|
||||
errorText: _error,
|
||||
border: const OutlineInputBorder(),
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: Text(l10n.cancelAction),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () => _confirmar(l10n),
|
||||
child: Text(l10n.recordingActionRename),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -3,10 +3,12 @@ import 'dart:io';
|
||||
import 'dart:ui' show Locale;
|
||||
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
import '../l10n/gen/app_localizations.dart';
|
||||
import '../modelos/archivo_grabacion.dart';
|
||||
import '../modelos/emisora.dart';
|
||||
|
||||
enum EstadoGrabacionRadioTipo {
|
||||
@@ -173,6 +175,66 @@ class ServicioGrabacionRadio {
|
||||
_emitir(_estado);
|
||||
}
|
||||
|
||||
/// Lists the recording files currently on disk in the effective
|
||||
/// directory (WU15, recordings-library spec "Browsable Recordings List").
|
||||
/// Pure filesystem read — no audio decoding, no duration. Never throws: a
|
||||
/// missing directory (never recorded yet) degrades to an empty list, not
|
||||
/// an error, matching the spec's "No recordings (edge case)" scenario.
|
||||
/// Sorted most-recent-first by last-modified time.
|
||||
///
|
||||
/// Uses the SYNC `listSync`/`statSync` filesystem calls deliberately, not
|
||||
/// `Directory.list()`'s async stream — the latter did not resolve inside
|
||||
/// `flutter_test`'s fake-async zone in this sandbox (confirmed: a widget
|
||||
/// test calling through to the stream-based version hung indefinitely,
|
||||
/// while the sync calls resolve immediately), mirroring this project's
|
||||
/// existing `Directory.systemTemp` async-hang precedent for a different
|
||||
/// dart:io API shape.
|
||||
Future<List<ArchivoGrabacion>> listarGrabaciones() async {
|
||||
final ruta = await directorioEfectivo();
|
||||
final directorio = Directory(ruta);
|
||||
if (!directorio.existsSync()) return const [];
|
||||
|
||||
final archivos = <ArchivoGrabacion>[];
|
||||
for (final entidad in directorio.listSync()) {
|
||||
if (entidad is! File) continue;
|
||||
final stat = entidad.statSync();
|
||||
archivos.add(
|
||||
ArchivoGrabacion(
|
||||
ruta: entidad.path,
|
||||
nombre: p.basenameWithoutExtension(entidad.path),
|
||||
fecha: stat.modified,
|
||||
tamanoBytes: stat.size,
|
||||
),
|
||||
);
|
||||
}
|
||||
archivos.sort((a, b) => b.fecha.compareTo(a.fecha));
|
||||
return archivos;
|
||||
}
|
||||
|
||||
/// Deletes the recording at [ruta] (WU15, "Delete removes the file and
|
||||
/// its row"). File-lifecycle management for a file this service already
|
||||
/// created — not a new capability. A missing file is treated as
|
||||
/// already-deleted, never an error.
|
||||
Future<void> eliminarGrabacion(String ruta) async {
|
||||
final archivo = File(ruta);
|
||||
if (await archivo.exists()) {
|
||||
await archivo.delete();
|
||||
}
|
||||
}
|
||||
|
||||
/// Renames the recording at [ruta] to [nuevoNombre], preserving its
|
||||
/// original extension (WU15, "Rename updates the displayed name").
|
||||
/// Returns the new full path.
|
||||
Future<String> renombrarGrabacion(String ruta, String nuevoNombre) async {
|
||||
final original = File(ruta);
|
||||
final nuevaRuta = p.join(
|
||||
p.dirname(ruta),
|
||||
'$nuevoNombre${p.extension(ruta)}',
|
||||
);
|
||||
final renombrado = await original.rename(nuevaRuta);
|
||||
return renombrado.path;
|
||||
}
|
||||
|
||||
Future<void> guardarMaxBytes(int bytes) async {
|
||||
if (bytes <= 0) {
|
||||
throw ArgumentError(_textos.recordingMaxSizeInvalidError);
|
||||
|
||||
Reference in New Issue
Block a user