feat(grabaciones): add recordings library screen

This commit is contained in:
2026-07-28 23:01:33 +02:00
parent c1903623be
commit 589fc54580
23 changed files with 1825 additions and 13 deletions
@@ -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);