fix(favoritos,grabaciones): replace glass cards with flat rows

Item 23 / audit 4.3, 12.4 (t4:226-232, 616-619): Favoritos and
Grabaciones rows were full glass cards / ListTiles with two stacked
buttons and no artwork slot. Replace with flat, background-less rows
via a new shared FilaEmisoraPlana widget (square art, name+meta, a
circular play affordance) plus a bespoke Grabaciones row (44x12
placeholder art -- recordings carry no per-station favicon, so this
is a themed fallback, not invented artwork).

Favoritos keeps "Move to list" / "Remove from favorites" behind an
overflow menu (same underlying methods, unchanged) instead of two
always-visible buttons, since dropping either would be a functional
regression the prototype's own row doesn't have to solve for.

Also 12.1 (t4:610): the Grabaciones header action is folder_open, not
a generic gear.
This commit is contained in:
2026-07-30 11:46:55 +02:00
parent 4537497e83
commit 955682271c
6 changed files with 727 additions and 92 deletions
@@ -6,7 +6,9 @@ import 'package:pluriwave/estado/estado_radio.dart';
import 'package:pluriwave/l10n/gen/app_localizations.dart';
import 'package:pluriwave/pantallas/ajustes/pantalla_ajustes_grupos_favoritos.dart';
import 'package:pluriwave/pantallas/pantalla_favoritos.dart';
import 'package:pluriwave/widgets/fila_emisora_plana.dart';
import 'package:pluriwave/widgets/pluri_push_scaffold.dart';
import 'package:pluriwave/widgets/tarjeta_emisora.dart';
import 'package:provider/provider.dart';
import 'package:shared_preferences/shared_preferences.dart';
@@ -381,6 +383,90 @@ void main() {
expect(find.widgetWithText(TextFormField, 'Name *'), findsOneWidget);
});
// Item 23 / audit 4.3 (t4:226-232): flat, background-less rows with a
// square thumbnail and a circular play affordance, replacing the full
// glass TarjetaEmisora card + two stacked filledTonal buttons.
group('Item 23 -- flat rows (audit 4.3)', () {
testWidgets(
'each row is a flat FilaEmisoraPlana, not the full glass card',
(tester) async {
setLargeSurface(tester);
_suppressListTileInkAssertion();
final estado = await crearEstadoConFavoritos();
addTearDown(estado.dispose);
await tester.pumpWidget(buildScreen(estado));
await pumpStable(tester);
expect(find.byType(FilaEmisoraPlana), findsNWidgets(3));
expect(
find.byType(TarjetaEmisora),
findsNothing,
reason: 'audit 4.3 replaces the full glass card with a flat row',
);
expect(
find.byIcon(Icons.drag_indicator_rounded),
findsNWidgets(3),
reason: 't4:227 drag_indicator, not drag_handle',
);
expect(find.byIcon(Icons.drag_handle_rounded), findsNothing);
expect(
find.byType(BotonReproducirCircular),
findsNWidgets(3),
reason: 't4:230 a circular play affordance per row',
);
},
);
testWidgets(
'the overflow menu offers exactly "Move to list" and "Remove from '
'favorites"',
(tester) async {
setLargeSurface(tester);
_suppressListTileInkAssertion();
final estado = await crearEstadoConFavoritos();
addTearDown(estado.dispose);
await tester.pumpWidget(buildScreen(estado));
await pumpStable(tester);
await tester.tap(find.byIcon(Icons.more_vert_rounded).first);
await tester.pumpAndSettle();
expect(find.text('Move to list'), findsOneWidget);
expect(find.text('Remove from favorites'), findsOneWidget);
expect(find.byType(PopupMenuItem<String>), findsNWidgets(2));
},
);
testWidgets(
'"Remove from favorites" still calls the same removal path as before',
(tester) async {
setLargeSurface(tester);
_suppressListTileInkAssertion();
final estado = await crearEstadoConFavoritos();
addTearDown(estado.dispose);
await tester.pumpWidget(buildScreen(estado));
await pumpStable(tester);
// Invoke the callback directly rather than opening the overlay and
// tapping its rendered item by screen position: leaving that modal
// route open while the tap it triggers removes and unmounts the
// very row that anchors it is a real hang hazard, confirmed while
// developing this test (multi-minute stall, same class of risk as
// the project's other documented `pumpAndSettle` traps).
final boton = tester.widget<PopupMenuButton<String>>(
find.byType(PopupMenuButton<String>).first,
);
boton.onSelected!('remove');
await pumpStable(tester);
expect(find.text('Station A'), findsNothing);
},
);
});
}
void setLargeSurface(WidgetTester tester) {
+49 -1
View File
@@ -8,6 +8,7 @@ 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';
@@ -137,7 +138,9 @@ void main() {
);
await pumpStable(tester);
await tester.tap(find.byIcon(Icons.settings_outlined));
// 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));
@@ -194,6 +197,51 @@ void main() {
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 {
+146
View File
@@ -0,0 +1,146 @@
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:pluriwave/estado/estado_radio.dart';
import 'package:pluriwave/l10n/gen/app_localizations.dart';
import 'package:pluriwave/tema/pluriwave_tokens.dart';
import 'package:pluriwave/widgets/fila_emisora_plana.dart';
import 'package:pluriwave/widgets/tarjeta_emisora.dart';
import 'package:provider/provider.dart';
import '../helpers/fakes.dart';
import '../helpers/fakes_alarmas.dart';
EstadoRadio _estado() => EstadoRadio(
audio: FakeServicioAudio(),
favoritos: FakeServicioFavoritos(),
radio: FakeServicioRadio(),
servicioEcualizador: FakeServicioEcualizador(),
servicioGrabacion: FakeServicioGrabacionRadioInactiva(),
iniciarAutomaticamente: false,
);
Widget _host(EstadoRadio estado, Widget child) {
return ChangeNotifierProvider<EstadoRadio>.value(
value: estado,
child: MaterialApp(
locale: const Locale('en'),
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
home: Scaffold(body: child),
),
);
}
/// Item 23 / audit 4.3 + 6.5 (t4:226-232, 302-306): the shared flat row
/// backing Favoritos and search results.
void main() {
testWidgets(
'renders a flat, background-less row -- no TarjetaEmisora underneath',
(tester) async {
final estado = _estado();
addTearDown(estado.dispose);
final emisora = emisoraDemo(uuid: 'a', nombre: 'Station A');
await tester.pumpWidget(
_host(estado, FilaEmisoraPlana(emisora: emisora, meta: 'Rock - Spain')),
);
await tester.pump();
expect(find.text('Station A'), findsOneWidget);
expect(find.text('Rock - Spain'), findsOneWidget);
expect(
find.byType(TarjetaEmisora),
findsNothing,
reason: 'a flat row does not reuse the full glass card',
);
},
);
testWidgets('the thumbnail is 48x48 with a 12 radius (t4:226, t4:302)', (
tester,
) async {
final estado = _estado();
addTearDown(estado.dispose);
final emisora = emisoraDemo(uuid: 'a', nombre: 'Station A');
await tester.pumpWidget(
_host(estado, FilaEmisoraPlana(emisora: emisora, meta: '')),
);
await tester.pump();
final arte = find.byKey(const ValueKey('fila-emisora-plana-arte'));
expect(tester.getSize(arte), const Size(48, 48));
final clip = tester.widget<ClipRRect>(arte);
expect(
(clip.borderRadius as BorderRadius).topLeft,
const Radius.circular(12),
);
});
testWidgets('omits the meta line entirely when empty (no stray gap)', (
tester,
) async {
final estado = _estado();
addTearDown(estado.dispose);
final emisora = emisoraDemo(uuid: 'a', nombre: 'Station A');
await tester.pumpWidget(
_host(estado, FilaEmisoraPlana(emisora: emisora, meta: '')),
);
await tester.pump();
expect(find.text('Station A'), findsOneWidget);
// Only the title Text should exist in the info column -- no empty
// second Text node.
expect(find.text(''), findsNothing);
});
testWidgets('BotonReproducirCircular is 44x44, brand-teal-tinted background, '
'22px play_arrow icon (t4:230, t4:305)', (tester) async {
var tapped = false;
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: BotonReproducirCircular(onPressed: () => tapped = true),
),
),
);
await tester.pump();
final boton = find.byKey(const ValueKey('boton-reproducir-circular'));
final material = tester.widget<Material>(boton);
expect(material.color, PluriWaveTokens.brand.withValues(alpha: 0.14));
expect(material.shape, const CircleBorder());
final icon = tester.widget<Icon>(find.byIcon(Icons.play_arrow_rounded));
expect(icon.size, 22);
expect(icon.color, PluriWaveTokens.brand);
await tester.tap(find.byIcon(Icons.play_arrow_rounded));
expect(tapped, isTrue);
});
testWidgets(
'BotonFavoritoEmisora toggles favourite status and shows a snackbar '
'(t4:304)',
(tester) async {
final estado = _estado();
addTearDown(estado.dispose);
final emisora = emisoraDemo(uuid: 'a', nombre: 'Station A');
await tester.pumpWidget(
_host(estado, BotonFavoritoEmisora(emisora: emisora)),
);
await tester.pump();
expect(find.byIcon(Icons.favorite_outline_rounded), findsOneWidget);
await tester.tap(find.byIcon(Icons.favorite_outline_rounded));
await tester.pump();
await tester.pump();
expect(find.byIcon(Icons.favorite_rounded), findsOneWidget);
expect(await estado.esFavorito('a'), isTrue);
},
);
}