NowPlayingScreen: von Scaffold/Route zu Overlay-Inhalt in HomeShell umgebaut
This commit is contained in:
+29
-1
@@ -20,6 +20,7 @@ import 'library/search_screen.dart';
|
|||||||
import 'player/audio_effects.dart';
|
import 'player/audio_effects.dart';
|
||||||
import 'player/audio_handler.dart';
|
import 'player/audio_handler.dart';
|
||||||
import 'player/mini_player.dart';
|
import 'player/mini_player.dart';
|
||||||
|
import 'player/now_playing_screen.dart';
|
||||||
import 'player/player_expansion_controller.dart';
|
import 'player/player_expansion_controller.dart';
|
||||||
import 'services/baka_auth.dart';
|
import 'services/baka_auth.dart';
|
||||||
import 'services/logger_service.dart';
|
import 'services/logger_service.dart';
|
||||||
@@ -304,7 +305,26 @@ class _HomeShellState extends State<HomeShell>
|
|||||||
),
|
),
|
||||||
child: const MiniPlayer(),
|
child: const MiniPlayer(),
|
||||||
),
|
),
|
||||||
// NowPlayingScreen-Overlay-Inhalt folgt in Task 5 hier.
|
AnimatedBuilder(
|
||||||
|
animation: _expansion,
|
||||||
|
builder: (context, child) {
|
||||||
|
final p = _expansion.progress;
|
||||||
|
return Positioned.fromRect(
|
||||||
|
rect: Rect.lerp(_miniRect(context), _vollbildRect(context), p)!,
|
||||||
|
child: Visibility(
|
||||||
|
visible: p > 0,
|
||||||
|
maintainState: true, // _CoverGrundState bleibt erhalten, siehe Spec
|
||||||
|
maintainAnimation: true,
|
||||||
|
child: Opacity(
|
||||||
|
// Detail-Inhalt blendet erst ab p > 0.3 ein, siehe Spec.
|
||||||
|
opacity: ((p - 0.3) / 0.7).clamp(0.0, 1.0),
|
||||||
|
child: child,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: const NowPlayingScreen(),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
// Haarlinie darüber: ohne sie geht die Leiste auf schwarzem Grund
|
// Haarlinie darüber: ohne sie geht die Leiste auf schwarzem Grund
|
||||||
@@ -349,4 +369,12 @@ class _HomeShellState extends State<HomeShell>
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Rect _miniRect(BuildContext context) {
|
||||||
|
final size = MediaQuery.sizeOf(context);
|
||||||
|
final unten = size.height - MiniPlayer.hoehe - kBottomNavigationBarHeight;
|
||||||
|
return Rect.fromLTWH(0, unten, size.width, MiniPlayer.hoehe);
|
||||||
|
}
|
||||||
|
|
||||||
|
Rect _vollbildRect(BuildContext context) => Offset.zero & MediaQuery.sizeOf(context);
|
||||||
}
|
}
|
||||||
|
|||||||
+123
-126
@@ -2,11 +2,11 @@ import 'dart:async';
|
|||||||
|
|
||||||
import 'package:audio_service/audio_service.dart';
|
import 'package:audio_service/audio_service.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
import '../library/database.dart';
|
import '../library/database.dart';
|
||||||
import '../services/navidrome_service.dart';
|
import '../services/navidrome_service.dart';
|
||||||
import '../shared/cover.dart';
|
|
||||||
import '../shared/favorite_button.dart';
|
import '../shared/favorite_button.dart';
|
||||||
import '../shared/server_favorite_button.dart';
|
import '../shared/server_favorite_button.dart';
|
||||||
import '../shared/theme.dart';
|
import '../shared/theme.dart';
|
||||||
@@ -14,9 +14,9 @@ import 'audio_handler.dart';
|
|||||||
import 'cover_farbe.dart';
|
import 'cover_farbe.dart';
|
||||||
import 'lrc.dart';
|
import 'lrc.dart';
|
||||||
import 'queue_screen.dart';
|
import 'queue_screen.dart';
|
||||||
import 'wischen.dart';
|
|
||||||
|
|
||||||
/// Vollbild-Wiedergabe: Cover, Titel, Fortschritt, Transport-Controls.
|
/// Inhalt des Vollbild-Players — wird von HomeShell in ein per progress
|
||||||
|
/// interpoliertes Rechteck gesetzt, ist selbst kein Scaffold/keine Route.
|
||||||
class NowPlayingScreen extends StatelessWidget {
|
class NowPlayingScreen extends StatelessWidget {
|
||||||
const NowPlayingScreen({super.key});
|
const NowPlayingScreen({super.key});
|
||||||
|
|
||||||
@@ -31,117 +31,135 @@ class NowPlayingScreen extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final handler = context.read<MeloAudioHandler>();
|
final handler = context.read<MeloAudioHandler>();
|
||||||
return _CoverGrund(
|
// Ersetzt, was bisher implizit über Scaffold/AppBar(backgroundColor:
|
||||||
|
// transparent) auf dem durchgehend dunklen Theme lief (siehe
|
||||||
|
// MeloTheme.dark/CLAUDE.md „Dark Theme“) — ohne Scaffold/AppBar muss die
|
||||||
|
// Status-Icon-Helligkeit hier explizit gesetzt werden, sonst bleiben die
|
||||||
|
// Icons je nach Systemzustand zufällig dunkel auf dunklem Grund.
|
||||||
|
return AnnotatedRegion<SystemUiOverlayStyle>(
|
||||||
|
value: SystemUiOverlayStyle.light,
|
||||||
|
child: _CoverGrund(
|
||||||
handler: handler,
|
handler: handler,
|
||||||
child: Scaffold(
|
child: Material(
|
||||||
// Der Grund kommt aus dem Coverbild — deshalb hier durchsichtig.
|
color: Colors.transparent,
|
||||||
backgroundColor: Colors.transparent,
|
|
||||||
extendBodyBehindAppBar: true,
|
|
||||||
appBar: AppBar(
|
|
||||||
backgroundColor: Colors.transparent,
|
|
||||||
actions: [
|
|
||||||
StreamBuilder<MediaItem?>(
|
|
||||||
stream: handler.mediaItem,
|
|
||||||
builder: (context, snapshot) {
|
|
||||||
final item = snapshot.data;
|
|
||||||
if (item == null) return const SizedBox.shrink();
|
|
||||||
// Bei Server-Titeln die Navidrome-ID, nicht die Abspiel-Adresse:
|
|
||||||
// mit der fragte die App den Server nach einem Titel namens
|
|
||||||
// "https://…" und bekam nie einen Text.
|
|
||||||
final songId = songIdOf(item) ?? navidromeIdOf(item) ?? '';
|
|
||||||
return IconButton(
|
|
||||||
tooltip: songId.isEmpty
|
|
||||||
? 'Kein Songtext für diesen Titel verfügbar'
|
|
||||||
: 'Songtext',
|
|
||||||
icon: const Icon(Icons.lyrics),
|
|
||||||
onPressed: songId.isEmpty
|
|
||||||
? null
|
|
||||||
: () => _showLyrics(context, songId),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
_SleepTimerButton(handler: handler),
|
|
||||||
IconButton(
|
|
||||||
tooltip: 'Warteschlange',
|
|
||||||
icon: const Icon(Icons.queue_music),
|
|
||||||
onPressed: () => Navigator.push(
|
|
||||||
context,
|
|
||||||
MaterialPageRoute(builder: (_) => const QueueScreen()),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
body: GestureDetector(
|
|
||||||
// Wie bei Apple Music/Spotify: den Vollbild-Player von praktisch
|
|
||||||
// jeder Stelle nach unten wegziehen schließt ihn wieder — dieselbe
|
|
||||||
// Schwelle wie beim Mini-Player, damit kein zaghaftes Scrollen die
|
|
||||||
// Ansicht versehentlich zuklappt.
|
|
||||||
onVerticalDragEnd: (details) {
|
|
||||||
final geschwindigkeit = details.primaryVelocity ?? 0;
|
|
||||||
if (geschwindigkeit > wischSchwelle) Navigator.maybePop(context);
|
|
||||||
},
|
|
||||||
child: SafeArea(
|
child: SafeArea(
|
||||||
child: StreamBuilder<MediaItem?>(
|
child: Column(
|
||||||
|
children: [
|
||||||
|
_VollbildLeiste(handler: handler, onLyrics: _showLyrics),
|
||||||
|
Expanded(
|
||||||
|
child: GestureDetector(
|
||||||
|
onVerticalDragStart: (_) {}, // s. Task 8
|
||||||
|
child: StreamBuilder<MediaItem?>(
|
||||||
|
stream: handler.mediaItem,
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
final item = snapshot.data;
|
||||||
|
if (item == null) {
|
||||||
|
return const Center(child: Text('Nichts in Wiedergabe'));
|
||||||
|
}
|
||||||
|
return LayoutBuilder(
|
||||||
|
builder: (context, raum) {
|
||||||
|
final nebeneinander = raum.maxWidth > raum.maxHeight;
|
||||||
|
final inhalt = _Angaben(item: item, handler: handler);
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 24),
|
||||||
|
child: nebeneinander
|
||||||
|
? Row(children: [
|
||||||
|
Expanded(child: Center(child: _CoverPlatzhalter())),
|
||||||
|
const SizedBox(width: MeloSpace.lg),
|
||||||
|
Expanded(
|
||||||
|
child: Center(
|
||||||
|
child: ConstrainedBox(
|
||||||
|
constraints: const BoxConstraints(maxWidth: 420),
|
||||||
|
child: inhalt,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
])
|
||||||
|
: Column(children: [
|
||||||
|
Expanded(child: Center(child: _CoverPlatzhalter())),
|
||||||
|
const SizedBox(height: MeloSpace.lg),
|
||||||
|
Center(
|
||||||
|
child: ConstrainedBox(
|
||||||
|
constraints: const BoxConstraints(maxWidth: 520),
|
||||||
|
child: inhalt,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gemeinsamer Name des Coverbilds in Mini-Player und Vollbild — bis Task 6
|
||||||
|
/// (WanderndesCover) den bisherigen Hero-Übergang vollständig ersetzt hat,
|
||||||
|
/// nutzt `mini_player.dart` diesen Namen noch für sein eigenes Hero-Cover.
|
||||||
|
const coverHeldenName = 'melo-cover';
|
||||||
|
|
||||||
|
/// Schlüssel des Cover-Platzhalters — Task 6 misst darüber die tatsächliche
|
||||||
|
/// Vollbild-Position/-Größe des Covers (`RenderBox.localToGlobal`), statt
|
||||||
|
/// `NowPlayingScreen`s responsives Layout (Row/Column-Weiche, AspectRatio)
|
||||||
|
/// von außen nachzurechnen.
|
||||||
|
final coverPlatzhalterKey = GlobalKey();
|
||||||
|
|
||||||
|
/// Platzhalter im Layout — das eigentliche Cover rendert `WanderndesCover`
|
||||||
|
/// (Task 6) in einer eigenen Ebene über allem, damit es unabhängig vom
|
||||||
|
/// restlichen Inhalt zwischen Mini- und Vollbild-Rechteck wandern kann.
|
||||||
|
class _CoverPlatzhalter extends StatelessWidget {
|
||||||
|
const _CoverPlatzhalter();
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) =>
|
||||||
|
AspectRatio(key: coverPlatzhalterKey, aspectRatio: 1, child: const SizedBox());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Die bisherigen AppBar-Actions (Songtext, Sleep-Timer, Warteschlange) als
|
||||||
|
/// eigene Kopfzeile statt echter AppBar — eine Route/Scaffold gibt es hier
|
||||||
|
/// nicht mehr.
|
||||||
|
class _VollbildLeiste extends StatelessWidget {
|
||||||
|
const _VollbildLeiste({required this.handler, required this.onLyrics});
|
||||||
|
final MeloAudioHandler handler;
|
||||||
|
final void Function(BuildContext, String) onLyrics;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
|
children: [
|
||||||
|
StreamBuilder<MediaItem?>(
|
||||||
stream: handler.mediaItem,
|
stream: handler.mediaItem,
|
||||||
builder: (context, snapshot) {
|
builder: (context, snapshot) {
|
||||||
final item = snapshot.data;
|
final item = snapshot.data;
|
||||||
if (item == null) {
|
if (item == null) return const SizedBox.shrink();
|
||||||
return const Center(child: Text('Nichts in Wiedergabe'));
|
final songId = songIdOf(item) ?? navidromeIdOf(item) ?? '';
|
||||||
}
|
return IconButton(
|
||||||
return LayoutBuilder(
|
tooltip: songId.isEmpty
|
||||||
builder: (context, raum) {
|
? 'Kein Songtext für diesen Titel verfügbar'
|
||||||
// Im Querformat ist die Höhe knapp: Cover und Bedienung
|
: 'Songtext',
|
||||||
// untereinander ließen vom Cover einen Streifen übrig oder
|
icon: const Icon(Icons.lyrics),
|
||||||
// schoben die Knöpfe aus dem Bild. Nebeneinander bleibt beides
|
onPressed: songId.isEmpty ? null : () => onLyrics(context, songId),
|
||||||
// benutzbar. Dieselbe Aufteilung hilft auf einem Tablet.
|
|
||||||
final nebeneinander = raum.maxWidth > raum.maxHeight;
|
|
||||||
final inhalt = _Angaben(item: item, handler: handler);
|
|
||||||
return Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 24),
|
|
||||||
child: nebeneinander
|
|
||||||
? Row(
|
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
child: Center(child: _Cover(item: item)),
|
|
||||||
),
|
|
||||||
const SizedBox(width: MeloSpace.lg),
|
|
||||||
// Der Inhalt bekommt eine feste Obergrenze: auf
|
|
||||||
// einem breiten Bildschirm zerrte er sonst über
|
|
||||||
// die ganze Breite und wäre schlecht lesbar.
|
|
||||||
Expanded(
|
|
||||||
child: Center(
|
|
||||||
child: ConstrainedBox(
|
|
||||||
constraints:
|
|
||||||
const BoxConstraints(maxWidth: 420),
|
|
||||||
child: inhalt,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
: Column(
|
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
child: Center(child: _Cover(item: item)),
|
|
||||||
),
|
|
||||||
const SizedBox(height: MeloSpace.lg),
|
|
||||||
Center(
|
|
||||||
child: ConstrainedBox(
|
|
||||||
constraints:
|
|
||||||
const BoxConstraints(maxWidth: 520),
|
|
||||||
child: inhalt,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
_SleepTimerButton(handler: handler),
|
||||||
|
IconButton(
|
||||||
|
tooltip: 'Warteschlange',
|
||||||
|
icon: const Icon(Icons.queue_music),
|
||||||
|
onPressed: () => Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(builder: (_) => const QueueScreen()),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -289,27 +307,6 @@ class _Herz extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Gemeinsamer Name des Coverbilds in Mini-Player und Vollbild — daran
|
|
||||||
/// erkennt Flutter, dass es dasselbe Bild ist, und lässt es zwischen beiden
|
|
||||||
/// wandern statt einen neuen Bildschirm aufspringen zu lassen.
|
|
||||||
const coverHeldenName = 'melo-cover';
|
|
||||||
|
|
||||||
class _Cover extends StatelessWidget {
|
|
||||||
const _Cover({required this.item});
|
|
||||||
final MediaItem item;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return AspectRatio(
|
|
||||||
aspectRatio: 1,
|
|
||||||
child: Hero(
|
|
||||||
tag: coverHeldenName,
|
|
||||||
child: CoverImage(artUri: item.artUri, radius: 16),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _ProgressBar extends StatelessWidget {
|
class _ProgressBar extends StatelessWidget {
|
||||||
const _ProgressBar({required this.handler});
|
const _ProgressBar({required this.handler});
|
||||||
final MeloAudioHandler handler;
|
final MeloAudioHandler handler;
|
||||||
|
|||||||
@@ -89,34 +89,30 @@ void main() {
|
|||||||
metadataEdited: false,
|
metadataEdited: false,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Öffnet den Vollbild-Player über denselben Weg wie in
|
// Startet die Wiedergabe über denselben Weg wie in
|
||||||
// song_list_navigation_test.dart (Tap auf eine SongZeile) — dieser Weg ist
|
// song_list_navigation_test.dart (Tap auf eine SongZeile) — dieser Weg ist
|
||||||
// dort nachweislich unproblematisch, im Unterschied zu einem direkten
|
// dort nachweislich unproblematisch und übt die echte SongZeile→MediaItem
|
||||||
// Aufbau von NowPlayingScreen mit vorab befülltem MediaItem-Stream.
|
// -Umwandlung. Seit dem Umbau auf PlayerExpansionController (Plan
|
||||||
//
|
// 2026-08-29-blur-oeffnen-transition, Task 3/5) ist NowPlayingScreen kein
|
||||||
// Seit dem Umbau auf PlayerExpansionController (Plan
|
// per Navigator.push geöffneter Screen mehr, sondern reiner Inhalt — hier
|
||||||
// 2026-08-29-blur-oeffnen-transition, Task 3) zeigt der Tap allein noch
|
// deshalb direkt in einen eigenen Scaffold gepumpt (Task 5 Step 3), statt
|
||||||
// keine sichtbare Oberfläche — das Overlay-Rendering kommt erst mit
|
// eine Route zu pushen. Der Handler behält den zuvor gestarteten Titel in
|
||||||
// Task 4/5 ("Bekannte Zwischenlücke", siehe Plan). Für die Prüfung von
|
// seinem mediaItem-Stream, den NowPlayingScreen darüber sofort sieht.
|
||||||
// NowPlayingScreens eigenem Layout (Herz-Position) wird die Route hier
|
|
||||||
// deshalb zusätzlich, unabhängig vom Produktions-Pfad, direkt geöffnet.
|
|
||||||
// Sobald Task 5 NowPlayingScreen zu Overlay-Inhalt umbaut, muss dieser
|
|
||||||
// Test ohnehin überarbeitet werden (Task 5 durchsucht dafür selbst
|
|
||||||
// `test -iname "*now_playing*"`, was diese Datei einschließt).
|
|
||||||
Future<void> oeffnePlayer(WidgetTester tester) async {
|
Future<void> oeffnePlayer(WidgetTester tester) async {
|
||||||
final expansion = PlayerExpansionController(vsync: tester);
|
final expansion = PlayerExpansionController(vsync: tester);
|
||||||
addTearDown(expansion.dispose);
|
addTearDown(expansion.dispose);
|
||||||
|
final providers = [
|
||||||
|
Provider<MeloDb>.value(value: db),
|
||||||
|
Provider<MeloAudioHandler>.value(value: handler),
|
||||||
|
ChangeNotifierProvider<CategoryService>.value(value: kategorien),
|
||||||
|
ChangeNotifierProvider<PlaylistService>.value(value: playlists),
|
||||||
|
ChangeNotifierProvider<AppSettings>.value(value: einstellungen),
|
||||||
|
ChangeNotifierProvider<PlayerExpansionController>.value(
|
||||||
|
value: expansion),
|
||||||
|
];
|
||||||
await tester.pumpWidget(
|
await tester.pumpWidget(
|
||||||
MultiProvider(
|
MultiProvider(
|
||||||
providers: [
|
providers: providers,
|
||||||
Provider<MeloDb>.value(value: db),
|
|
||||||
Provider<MeloAudioHandler>.value(value: handler),
|
|
||||||
ChangeNotifierProvider<CategoryService>.value(value: kategorien),
|
|
||||||
ChangeNotifierProvider<PlaylistService>.value(value: playlists),
|
|
||||||
ChangeNotifierProvider<AppSettings>.value(value: einstellungen),
|
|
||||||
ChangeNotifierProvider<PlayerExpansionController>.value(
|
|
||||||
value: expansion),
|
|
||||||
],
|
|
||||||
child: MaterialApp(
|
child: MaterialApp(
|
||||||
home: Scaffold(
|
home: Scaffold(
|
||||||
body: SongZeile(
|
body: SongZeile(
|
||||||
@@ -133,8 +129,13 @@ void main() {
|
|||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
expect(expansion.progress, 1.0);
|
expect(expansion.progress, 1.0);
|
||||||
|
|
||||||
Navigator.of(tester.element(find.byType(SongZeile))).push(
|
await tester.pumpWidget(
|
||||||
MaterialPageRoute(builder: (_) => const NowPlayingScreen()),
|
MultiProvider(
|
||||||
|
providers: providers,
|
||||||
|
child: MaterialApp(
|
||||||
|
home: Scaffold(body: const NowPlayingScreen()),
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
}
|
}
|
||||||
@@ -154,12 +155,11 @@ void main() {
|
|||||||
|
|
||||||
expect(find.byType(NowPlayingScreen), findsOneWidget);
|
expect(find.byType(NowPlayingScreen), findsOneWidget);
|
||||||
|
|
||||||
// Nicht mehr in der AppBar:
|
// NowPlayingScreen ist seit dem Overlay-Umbau (Task 5) kein Scaffold/
|
||||||
final herzInAppBar = find.descendant(
|
// keine Route mehr und hat gar keine AppBar mehr — die frühere
|
||||||
of: find.byType(AppBar),
|
// "nicht mehr in der AppBar"-Prüfung ist jetzt trivial durch den Umbau
|
||||||
matching: find.byType(FavoriteButton),
|
// selbst erfüllt.
|
||||||
);
|
expect(find.byType(AppBar), findsNothing);
|
||||||
expect(herzInAppBar, findsNothing);
|
|
||||||
|
|
||||||
// Aber sichtbar auf dem Bildschirm, in derselben Row wie der Titel.
|
// Aber sichtbar auf dem Bildschirm, in derselben Row wie der Titel.
|
||||||
final titelRow = find
|
final titelRow = find
|
||||||
|
|||||||
Reference in New Issue
Block a user