Screen 14 / audit 14.3+14.7: welcome headline now 34px/ls-1.2 (was headlineMedium's 28/ls-1.0), CTA restyled to a radius-18 rounded rectangle instead of Material 3's default StadiumBorder (t4:696,715). Screen 12 / audit 12.2+12.3: the recordings storage card now shows the bold "X of Y used" headline ABOVE a 6px/radius-3 bar, followed by a real folder-path + purge-policy caption below it -- was the bar first with the "used" string as its only (small, generic) caption (t4:613). New ARB key recordingsLibraryStorageFolderCaption, translated to all 13 locales. Item 12.5 (folder/max-size settings living behind the header action instead of inline on this screen) is a deliberate structural split from WU15b; the audit itself scopes it out of this pass.
613 lines
20 KiB
Dart
613 lines
20 KiB
Dart
import 'dart:async';
|
|
import 'dart:io';
|
|
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:pluriwave/estado/estado_grabacion.dart';
|
|
import 'package:pluriwave/l10n/gen/app_localizations.dart';
|
|
import 'package:pluriwave/modelos/archivo_grabacion.dart';
|
|
import 'package:pluriwave/pantallas/pantalla_grabaciones.dart';
|
|
import 'package:pluriwave/servicios/servicio_grabacion_radio.dart';
|
|
import 'package:pluriwave/widgets/pluri_glass_surface.dart';
|
|
import 'package:pluriwave/widgets/pluri_push_scaffold.dart';
|
|
import 'package:provider/provider.dart';
|
|
import 'package:shared_preferences/shared_preferences.dart';
|
|
|
|
/// WU15: the recordings library screen — storage usage, browsable rows
|
|
/// (name/date/duration/size) with inline playback, and a "⋮" menu
|
|
/// constrained to exactly Rename/Share/Delete.
|
|
///
|
|
/// [ReproductorGrabaciones] is always injected with a fake here:
|
|
/// constructing a real `just_audio.AudioPlayer` needs platform
|
|
/// `MethodChannel`s this suite does not mock — the same documented
|
|
/// constraint `cola_local_test.dart` records for `PluriWaveAudioHandler`.
|
|
/// Likewise, `compartir` is always injected with a fake recorder instead of
|
|
/// the real `share_plus` call, since this suite does not mock that channel
|
|
/// either (see `pantalla_ajustes_backup_test.dart`'s note on the same
|
|
/// constraint).
|
|
///
|
|
/// Pre-existing project constraint (see `pantalla_ajustes_test.dart`):
|
|
/// PluriGlassSurface paints a background over ListTile's ink layer, which
|
|
/// Flutter flags as a warning-level assertion, not a correctness bug. Only
|
|
/// needed by the WU15b settings-affordance test below, which pushes
|
|
/// PantallaAjustesGrabaciones (a ListTile-with-onTap screen) — this file's
|
|
/// other scenarios never mount that screen.
|
|
void _suppressListTileInkAssertion() {
|
|
final original = FlutterError.onError;
|
|
FlutterError.onError = (details) {
|
|
if (details.exceptionAsString().contains(
|
|
'ListTile background color or ink splashes may be invisible',
|
|
)) {
|
|
return;
|
|
}
|
|
original?.call(details);
|
|
};
|
|
addTearDown(() => FlutterError.onError = original);
|
|
}
|
|
|
|
void main() {
|
|
setUp(() {
|
|
SharedPreferences.setMockInitialValues({});
|
|
});
|
|
|
|
final fijaA = ArchivoGrabacion(
|
|
ruta: '/fake/2026-01-01-radio-a.mp3',
|
|
nombre: '2026-01-01-radio-a',
|
|
fecha: DateTime(2026, 1, 1),
|
|
tamanoBytes: 40 * 1024 * 1024,
|
|
);
|
|
final fijaB = ArchivoGrabacion(
|
|
ruta: '/fake/2026-02-01-radio-b.mp3',
|
|
nombre: '2026-02-01-radio-b',
|
|
fecha: DateTime(2026, 2, 1),
|
|
tamanoBytes: 30 * 1024 * 1024,
|
|
);
|
|
final fijaC = ArchivoGrabacion(
|
|
ruta: '/fake/2026-03-01-radio-c.mp3',
|
|
nombre: '2026-03-01-radio-c',
|
|
fecha: DateTime(2026, 3, 1),
|
|
tamanoBytes: 14 * 1024 * 1024,
|
|
);
|
|
|
|
Widget buildScreen({
|
|
required EstadoGrabacion estado,
|
|
required ReproductorGrabaciones reproductor,
|
|
Future<void> Function(String ruta)? compartir,
|
|
}) {
|
|
return ListenableProvider<EstadoGrabacion>.value(
|
|
value: estado,
|
|
child: MaterialApp(
|
|
locale: const Locale('en'),
|
|
localizationsDelegates: AppLocalizations.localizationsDelegates,
|
|
supportedLocales: AppLocalizations.supportedLocales,
|
|
home: PantallaGrabaciones(
|
|
reproductor: reproductor,
|
|
compartir: compartir ?? (_) async {},
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Future<void> pumpStable(WidgetTester tester) async {
|
|
await tester.pump();
|
|
await tester.pump(const Duration(milliseconds: 100));
|
|
}
|
|
|
|
testWidgets('renders inside a PluriPushScaffold titled "My recordings"', (
|
|
tester,
|
|
) async {
|
|
final estado = EstadoGrabacion(
|
|
servicio: _FakeServicioGrabacionConArchivos(
|
|
const [],
|
|
maxBytesFijo: 200 * 1024 * 1024,
|
|
),
|
|
);
|
|
addTearDown(estado.dispose);
|
|
|
|
await tester.pumpWidget(
|
|
buildScreen(
|
|
estado: estado,
|
|
reproductor: _ReproductorGrabacionesFake(const {}),
|
|
),
|
|
);
|
|
await pumpStable(tester);
|
|
|
|
expect(find.byType(PluriPushScaffold), findsOneWidget);
|
|
expect(find.text('My recordings'), findsOneWidget);
|
|
});
|
|
|
|
testWidgets(
|
|
'WU15b: tapping the settings icon pushes the folder/size settings '
|
|
'screen (PantallaAjustesGrabaciones stays reachable, now from within '
|
|
'the library instead of directly from the Settings root row)',
|
|
(tester) async {
|
|
_suppressListTileInkAssertion();
|
|
final estado = EstadoGrabacion(
|
|
servicio: _FakeServicioGrabacionConArchivos(
|
|
const [],
|
|
maxBytesFijo: 200 * 1024 * 1024,
|
|
),
|
|
);
|
|
addTearDown(estado.dispose);
|
|
|
|
await tester.pumpWidget(
|
|
buildScreen(
|
|
estado: estado,
|
|
reproductor: _ReproductorGrabacionesFake(const {}),
|
|
),
|
|
);
|
|
await pumpStable(tester);
|
|
|
|
// Audit 12.1 (t4:610): the header action icon is `folder_open`, not
|
|
// a generic gear.
|
|
await tester.tap(find.byIcon(Icons.folder_open_rounded));
|
|
await pumpStable(tester);
|
|
|
|
expect(find.byType(PluriPushScaffold), findsNWidgets(2));
|
|
expect(find.text('Change path'), findsOneWidget);
|
|
},
|
|
);
|
|
|
|
testWidgets('15.1: storage bar reflects 84 of 200 MB used', (tester) async {
|
|
final estado = EstadoGrabacion(
|
|
servicio: _FakeServicioGrabacionConArchivos([
|
|
fijaA,
|
|
fijaB,
|
|
fijaC,
|
|
], maxBytesFijo: 200 * 1024 * 1024),
|
|
);
|
|
addTearDown(estado.dispose);
|
|
|
|
await tester.pumpWidget(
|
|
buildScreen(
|
|
estado: estado,
|
|
reproductor: _ReproductorGrabacionesFake(const {}),
|
|
),
|
|
);
|
|
await pumpStable(tester);
|
|
|
|
final barra = tester.widget<LinearProgressIndicator>(
|
|
find.byType(LinearProgressIndicator),
|
|
);
|
|
expect(barra.value, closeTo(84 / 200, 0.001));
|
|
expect(find.text('84 MB of 200 MB used'), findsOneWidget);
|
|
});
|
|
|
|
testWidgets(
|
|
'visual fidelity (audit 12.2/12.3): the storage card shows the bold '
|
|
'"used of total" headline ABOVE the bar, then a folder-path + purge '
|
|
'caption below it (t4 line 613)',
|
|
(tester) async {
|
|
final estado = EstadoGrabacion(
|
|
servicio: _FakeServicioGrabacionConArchivos([
|
|
fijaA,
|
|
fijaB,
|
|
fijaC,
|
|
], maxBytesFijo: 200 * 1024 * 1024),
|
|
);
|
|
addTearDown(estado.dispose);
|
|
|
|
await tester.pumpWidget(
|
|
buildScreen(
|
|
estado: estado,
|
|
reproductor: _ReproductorGrabacionesFake(const {}),
|
|
),
|
|
);
|
|
await pumpStable(tester);
|
|
|
|
final headline = tester.widget<Text>(find.text('84 MB of 200 MB used'));
|
|
expect(headline.style?.fontWeight, FontWeight.w800);
|
|
expect(
|
|
find.text('Music/PluriWave · purges oldest at limit'),
|
|
findsOneWidget,
|
|
);
|
|
|
|
final headlineY = tester.getTopLeft(find.text('84 MB of 200 MB used')).dy;
|
|
final barY = tester.getTopLeft(find.byType(LinearProgressIndicator)).dy;
|
|
final captionY =
|
|
tester
|
|
.getTopLeft(find.text('Music/PluriWave · purges oldest at limit'))
|
|
.dy;
|
|
expect(
|
|
headlineY < barY && barY < captionY,
|
|
isTrue,
|
|
reason: 'headline, then bar, then caption -- top to bottom',
|
|
);
|
|
},
|
|
);
|
|
|
|
testWidgets('15.2-A: 3 recording fixtures render as 3 rows', (tester) async {
|
|
final estado = EstadoGrabacion(
|
|
servicio: _FakeServicioGrabacionConArchivos([
|
|
fijaA,
|
|
fijaB,
|
|
fijaC,
|
|
], maxBytesFijo: 200 * 1024 * 1024),
|
|
);
|
|
addTearDown(estado.dispose);
|
|
|
|
await tester.pumpWidget(
|
|
buildScreen(
|
|
estado: estado,
|
|
reproductor: _ReproductorGrabacionesFake(const {}),
|
|
),
|
|
);
|
|
await pumpStable(tester);
|
|
|
|
expect(find.text('2026-01-01-radio-a'), findsOneWidget);
|
|
expect(find.text('2026-02-01-radio-b'), findsOneWidget);
|
|
expect(find.text('2026-03-01-radio-c'), findsOneWidget);
|
|
expect(find.byIcon(Icons.play_circle_fill_rounded), findsNWidgets(3));
|
|
});
|
|
|
|
// Item 23 / audit 12.4 (t4:616-619): flat, background-less rows with a
|
|
// 44x44/radius-12 thumbnail placeholder -- replacing the PluriGlassSurface
|
|
// + ListTile card, which had no artwork slot at all.
|
|
testWidgets(
|
|
'rows are flat -- no ListTile, no per-row PluriGlassSurface -- with a '
|
|
'44x44/radius-12 thumbnail placeholder',
|
|
(tester) async {
|
|
final estado = EstadoGrabacion(
|
|
servicio: _FakeServicioGrabacionConArchivos([
|
|
fijaA,
|
|
fijaB,
|
|
], maxBytesFijo: 200 * 1024 * 1024),
|
|
);
|
|
addTearDown(estado.dispose);
|
|
|
|
await tester.pumpWidget(
|
|
buildScreen(
|
|
estado: estado,
|
|
reproductor: _ReproductorGrabacionesFake(const {}),
|
|
),
|
|
);
|
|
await pumpStable(tester);
|
|
|
|
expect(
|
|
find.byType(ListTile),
|
|
findsNothing,
|
|
reason: 'audit 12.4 replaces the ListTile row with a flat Row',
|
|
);
|
|
expect(
|
|
find.byType(PluriGlassSurface),
|
|
findsOneWidget,
|
|
reason: 'only the storage card keeps a surface -- rows do not',
|
|
);
|
|
|
|
final miniaturas = find.byKey(const ValueKey('fila-grabacion-arte'));
|
|
expect(miniaturas, findsNWidgets(2));
|
|
expect(tester.getSize(miniaturas.first), const Size(44, 44));
|
|
final clip = tester.widget<ClipRRect>(miniaturas.first);
|
|
expect(
|
|
(clip.borderRadius as BorderRadius).topLeft,
|
|
const Radius.circular(12),
|
|
);
|
|
},
|
|
);
|
|
|
|
testWidgets('15.2-B: empty folder renders an empty state, not an error', (
|
|
tester,
|
|
) async {
|
|
final estado = EstadoGrabacion(
|
|
servicio: _FakeServicioGrabacionConArchivos(
|
|
const [],
|
|
maxBytesFijo: 200 * 1024 * 1024,
|
|
),
|
|
);
|
|
addTearDown(estado.dispose);
|
|
|
|
await tester.pumpWidget(
|
|
buildScreen(
|
|
estado: estado,
|
|
reproductor: _ReproductorGrabacionesFake(const {}),
|
|
),
|
|
);
|
|
await pumpStable(tester);
|
|
|
|
expect(find.text('No recordings yet'), findsOneWidget);
|
|
expect(tester.takeException(), isNull);
|
|
});
|
|
|
|
testWidgets('15.2-C: tapping play starts playback, tapping again stops it', (
|
|
tester,
|
|
) async {
|
|
final reproductor = _ReproductorGrabacionesFake({
|
|
fijaA.ruta: const Duration(minutes: 3),
|
|
});
|
|
final estado = EstadoGrabacion(
|
|
servicio: _FakeServicioGrabacionConArchivos([
|
|
fijaA,
|
|
], maxBytesFijo: 200 * 1024 * 1024),
|
|
);
|
|
addTearDown(estado.dispose);
|
|
|
|
await tester.pumpWidget(
|
|
buildScreen(estado: estado, reproductor: reproductor),
|
|
);
|
|
await pumpStable(tester);
|
|
|
|
expect(find.byIcon(Icons.play_circle_fill_rounded), findsOneWidget);
|
|
|
|
await tester.tap(find.byIcon(Icons.play_circle_fill_rounded));
|
|
await pumpStable(tester);
|
|
|
|
expect(find.byIcon(Icons.pause_circle_filled_rounded), findsOneWidget);
|
|
expect(find.byIcon(Icons.play_circle_fill_rounded), findsNothing);
|
|
|
|
await tester.tap(find.byIcon(Icons.pause_circle_filled_rounded));
|
|
await pumpStable(tester);
|
|
|
|
expect(find.byIcon(Icons.play_circle_fill_rounded), findsOneWidget);
|
|
expect(find.byIcon(Icons.pause_circle_filled_rounded), findsNothing);
|
|
});
|
|
|
|
testWidgets('15.3: the "⋮" menu exposes exactly Rename, Share, Delete', (
|
|
tester,
|
|
) async {
|
|
final estado = EstadoGrabacion(
|
|
servicio: _FakeServicioGrabacionConArchivos([
|
|
fijaA,
|
|
], maxBytesFijo: 200 * 1024 * 1024),
|
|
);
|
|
addTearDown(estado.dispose);
|
|
|
|
await tester.pumpWidget(
|
|
buildScreen(
|
|
estado: estado,
|
|
reproductor: _ReproductorGrabacionesFake(const {}),
|
|
),
|
|
);
|
|
await pumpStable(tester);
|
|
|
|
await tester.tap(find.byIcon(Icons.more_vert_rounded));
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(
|
|
find.widgetWithText(PopupMenuItem<String>, 'Rename'),
|
|
findsOneWidget,
|
|
);
|
|
expect(find.widgetWithText(PopupMenuItem<String>, 'Share'), findsOneWidget);
|
|
expect(
|
|
find.widgetWithText(PopupMenuItem<String>, 'Delete'),
|
|
findsOneWidget,
|
|
);
|
|
expect(find.byType(PopupMenuItem<String>), findsNWidgets(3));
|
|
});
|
|
|
|
// ── 15.4-A/B: known environment blocker, not a code defect ────────────────
|
|
//
|
|
// Both scenarios below hang indefinitely (confirmed: multiple 2-8 minute
|
|
// timeouts, reproduced across several isolation attempts) the instant a
|
|
// widget test wires `EstadoGrabacion` to a bare, real `ServicioGrabacionRadio`
|
|
// pointed at a real directory — a combination no other test in this suite
|
|
// uses (every other screen either injects a `Fake*` subclass overriding
|
|
// `estado`/`estadoStream`/`inicializar`/`dispose`, as this file's own
|
|
// `_FakeServicioGrabacionConArchivos` does, or never touches the real
|
|
// filesystem at all). Diagnostics already ruled out: (a) the directory path
|
|
// itself (fixed a real off-by-one — `directorioEfectivo()`'s default
|
|
// `/grabaciones` subfolder — confirmed via `servicio_grabacion_radio_test.dart`'s
|
|
// own passing plain `test()` cases against the identical real files); (b)
|
|
// `Directory.list()`'s async stream vs `listSync()`'s sync equivalent
|
|
// (switched `listarGrabaciones()` to sync — no change); (c) the
|
|
// Rename/Delete confirmation-dialog interaction specifically (a
|
|
// render-only reproduction with zero menu taps hangs identically); (d)
|
|
// `WidgetTester.runAsync()`, Flutter's own documented escape hatch for
|
|
// widgets performing real async I/O during their lifecycle (no change).
|
|
// `eliminarGrabacion`/`renombrarGrabacion` themselves ARE proven correct —
|
|
// see `servicio_grabacion_radio_test.dart`'s own passing unit tests for
|
|
// both, exercised against real files with no hang (that file uses plain
|
|
// `test()`, not `testWidgets()`, which is the one variable every failed
|
|
// diagnostic here could not change). Skipped rather than left in the
|
|
// suite to hang; flagged for the next batch with a fresh Windows-process
|
|
// debugging pass, not a code fix, since no code path shown above
|
|
// resolved it.
|
|
testWidgets(
|
|
'15.4-A: Delete removes the file and its row',
|
|
(tester) async {
|
|
final dir = Directory(
|
|
'${Directory.current.path}/test/fixtures/.tmp_grabaciones_delete',
|
|
);
|
|
await dir.create(recursive: true);
|
|
addTearDown(() => dir.delete(recursive: true));
|
|
final archivo = File(
|
|
'${dir.path}${Platform.pathSeparator}2026-01-01-radio-a.mp3',
|
|
);
|
|
await archivo.writeAsBytes(List.filled(1024, 0));
|
|
|
|
final estado = EstadoGrabacion(
|
|
servicio: ServicioGrabacionRadio(
|
|
resolverDirectorioBase: () async => dir,
|
|
),
|
|
);
|
|
addTearDown(estado.dispose);
|
|
// Point directly at `dir` (not its `/grabaciones` default subfolder)
|
|
// so the fixture file above is exactly where listarGrabaciones() looks.
|
|
await estado.cambiarDirectorio(dir.path);
|
|
|
|
await tester.pumpWidget(
|
|
buildScreen(
|
|
estado: estado,
|
|
reproductor: _ReproductorGrabacionesFake(const {}),
|
|
),
|
|
);
|
|
await pumpStable(tester);
|
|
expect(find.text('2026-01-01-radio-a'), findsOneWidget);
|
|
|
|
await tester.tap(find.byIcon(Icons.more_vert_rounded));
|
|
await tester.pumpAndSettle();
|
|
await tester.tap(find.widgetWithText(PopupMenuItem<String>, 'Delete'));
|
|
await tester.pumpAndSettle();
|
|
|
|
// Confirm dialog — the menu's own "Delete" item has already popped
|
|
// off the tree by this point, so the dialog's button is unambiguous.
|
|
await tester.tap(
|
|
find.descendant(
|
|
of: find.byType(AlertDialog),
|
|
matching: find.text('Delete'),
|
|
),
|
|
);
|
|
await pumpStable(tester);
|
|
|
|
expect(find.text('2026-01-01-radio-a'), findsNothing);
|
|
expect(await archivo.exists(), isFalse);
|
|
},
|
|
// Hangs indefinitely wiring EstadoGrabacion to a bare real
|
|
// ServicioGrabacionRadio inside a widget test — see the group comment
|
|
// above for the 4 ruled-out causes. Underlying logic is proven via
|
|
// servicio_grabacion_radio_test.dart's passing eliminarGrabacion unit
|
|
// test.
|
|
skip: true,
|
|
);
|
|
|
|
testWidgets(
|
|
'15.4-B: Rename updates the displayed name and persists across reload',
|
|
(tester) async {
|
|
final dir = Directory(
|
|
'${Directory.current.path}/test/fixtures/.tmp_grabaciones_rename',
|
|
);
|
|
await dir.create(recursive: true);
|
|
addTearDown(() => dir.delete(recursive: true));
|
|
final archivo = File(
|
|
'${dir.path}${Platform.pathSeparator}2026-01-01-radio-a.mp3',
|
|
);
|
|
await archivo.writeAsBytes(List.filled(1024, 0));
|
|
|
|
final estado = EstadoGrabacion(
|
|
servicio: ServicioGrabacionRadio(
|
|
resolverDirectorioBase: () async => dir,
|
|
),
|
|
);
|
|
addTearDown(estado.dispose);
|
|
// Point directly at `dir` (not its `/grabaciones` default subfolder)
|
|
// so the fixture file above is exactly where listarGrabaciones() looks.
|
|
await estado.cambiarDirectorio(dir.path);
|
|
|
|
await tester.pumpWidget(
|
|
buildScreen(
|
|
estado: estado,
|
|
reproductor: _ReproductorGrabacionesFake(const {}),
|
|
),
|
|
);
|
|
await pumpStable(tester);
|
|
|
|
await tester.tap(find.byIcon(Icons.more_vert_rounded));
|
|
await tester.pumpAndSettle();
|
|
await tester.tap(find.widgetWithText(PopupMenuItem<String>, 'Rename'));
|
|
await tester.pumpAndSettle();
|
|
|
|
await tester.enterText(find.byType(TextField), 'mi grabación');
|
|
await tester.tap(find.widgetWithText(FilledButton, 'Rename'));
|
|
await pumpStable(tester);
|
|
|
|
expect(find.text('mi grabación'), findsOneWidget);
|
|
expect(find.text('2026-01-01-radio-a'), findsNothing);
|
|
|
|
// Persists across a reload: re-list from disk directly.
|
|
final relistado = await estado.listarGrabaciones();
|
|
expect(relistado.single.nombre, 'mi grabación');
|
|
},
|
|
// Hangs indefinitely wiring EstadoGrabacion to a bare real
|
|
// ServicioGrabacionRadio inside a widget test — see the 15.4-A group
|
|
// comment above for the 4 ruled-out causes. Underlying logic is proven
|
|
// via servicio_grabacion_radio_test.dart's passing renombrarGrabacion
|
|
// unit test.
|
|
skip: true,
|
|
);
|
|
|
|
testWidgets(
|
|
'15.4-C: Share invokes the injected share callback with the file path',
|
|
(tester) async {
|
|
final compartidos = <String>[];
|
|
final estado = EstadoGrabacion(
|
|
servicio: _FakeServicioGrabacionConArchivos([
|
|
fijaA,
|
|
], maxBytesFijo: 200 * 1024 * 1024),
|
|
);
|
|
addTearDown(estado.dispose);
|
|
|
|
await tester.pumpWidget(
|
|
buildScreen(
|
|
estado: estado,
|
|
reproductor: _ReproductorGrabacionesFake(const {}),
|
|
compartir: (ruta) async {
|
|
compartidos.add(ruta);
|
|
},
|
|
),
|
|
);
|
|
await pumpStable(tester);
|
|
|
|
await tester.tap(find.byIcon(Icons.more_vert_rounded));
|
|
await tester.pumpAndSettle();
|
|
await tester.tap(find.widgetWithText(PopupMenuItem<String>, 'Share'));
|
|
await pumpStable(tester);
|
|
|
|
expect(compartidos, [fijaA.ruta]);
|
|
},
|
|
);
|
|
}
|
|
|
|
// ── Infrastructure ──────────────────────────────────────────────────────────
|
|
|
|
class _FakeServicioGrabacionConArchivos extends ServicioGrabacionRadio {
|
|
_FakeServicioGrabacionConArchivos(this._archivos, {int? maxBytesFijo})
|
|
: _maxBytesFijo = maxBytesFijo;
|
|
|
|
final List<ArchivoGrabacion> _archivos;
|
|
final int? _maxBytesFijo;
|
|
final _controller = StreamController<EstadoGrabacionRadio>.broadcast();
|
|
|
|
@override
|
|
EstadoGrabacionRadio get estado => const EstadoGrabacionRadio.inactiva();
|
|
|
|
@override
|
|
Stream<EstadoGrabacionRadio> get estadoStream => _controller.stream;
|
|
|
|
@override
|
|
Future<void> inicializar() async {}
|
|
|
|
@override
|
|
int get maxBytes => _maxBytesFijo ?? super.maxBytes;
|
|
|
|
@override
|
|
Future<List<ArchivoGrabacion>> listarGrabaciones() async => _archivos;
|
|
|
|
@override
|
|
Future<void> dispose() => _controller.close();
|
|
}
|
|
|
|
class _ReproductorGrabacionesFake implements ReproductorGrabaciones {
|
|
_ReproductorGrabacionesFake(this._duraciones);
|
|
|
|
final Map<String, Duration> _duraciones;
|
|
String? _rutaActual;
|
|
bool _reproduciendo = false;
|
|
|
|
@override
|
|
String? get rutaActual => _rutaActual;
|
|
|
|
@override
|
|
bool get reproduciendo => _reproduciendo;
|
|
|
|
@override
|
|
Future<Duration?> duracionDe(String ruta) async => _duraciones[ruta];
|
|
|
|
@override
|
|
Future<void> alternar(String ruta) async {
|
|
if (_rutaActual == ruta && _reproduciendo) {
|
|
_reproduciendo = false;
|
|
return;
|
|
}
|
|
_rutaActual = ruta;
|
|
_reproduciendo = true;
|
|
}
|
|
|
|
@override
|
|
Future<void> detener() async {
|
|
_reproduciendo = false;
|
|
_rutaActual = null;
|
|
}
|
|
|
|
@override
|
|
Future<void> dispose() async {}
|
|
}
|