From a66ed3e6cfcb6fb297821f8b1e9d64a30a2f5e36 Mon Sep 17 00:00:00 2001 From: Herwig Birke Date: Fri, 26 Jun 2026 20:53:21 +0200 Subject: [PATCH] Testfilter, Listenbreite --- .../presentation/movie_list_screen.dart | 88 ++++++++++- .../presentation/series_list_screen.dart | 146 ++++++++++++++---- .../presentation/widgets/summary_table.dart | 5 +- 3 files changed, 199 insertions(+), 40 deletions(-) diff --git a/lib/features/movies/presentation/movie_list_screen.dart b/lib/features/movies/presentation/movie_list_screen.dart index b3b2069..4cd23a2 100644 --- a/lib/features/movies/presentation/movie_list_screen.dart +++ b/lib/features/movies/presentation/movie_list_screen.dart @@ -11,11 +11,45 @@ import 'movie_add_screen.dart'; enum _ExportFormat { csv, xlsx } -class MovieListScreen extends ConsumerWidget { +class MovieListScreen extends ConsumerStatefulWidget { const MovieListScreen({super.key}); @override - Widget build(BuildContext context, WidgetRef ref) { + ConsumerState createState() => _MovieListScreenState(); +} + +class _MovieListScreenState extends ConsumerState { + final TextEditingController _moviePrefixController = TextEditingController(); + String _moviePrefix = ''; + + @override + void initState() { + super.initState(); + _moviePrefixController.addListener(() { + final next = _moviePrefixController.text; + if (next == _moviePrefix) return; + setState(() { + _moviePrefix = next; + }); + }); + } + + @override + void dispose() { + _moviePrefixController.dispose(); + super.dispose(); + } + + String get _normalizedMoviePrefix => _moviePrefix.trim().toLowerCase(); + + bool _matchesMoviePrefix(String title) { + final prefix = _normalizedMoviePrefix; + if (prefix.isEmpty) return true; + return title.trim().toLowerCase().startsWith(prefix); + } + + @override + Widget build(BuildContext context) { final filter = ref.watch(movieFilterProvider); final moviesAsync = ref.watch(moviesStreamProvider); @@ -24,9 +58,43 @@ class MovieListScreen extends ConsumerWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - StatusChip( - selected: filter, - onChanged: (f) => ref.read(movieFilterProvider.notifier).state = f, + LayoutBuilder( + builder: (context, constraints) { + final searchWidth = constraints.maxWidth >= 360 + ? 280.0 + : constraints.maxWidth; + + return Wrap( + spacing: 12, + runSpacing: 8, + crossAxisAlignment: WrapCrossAlignment.center, + children: [ + StatusChip( + selected: filter, + onChanged: (f) => ref.read(movieFilterProvider.notifier).state = f, + ), + SizedBox( + width: searchWidth, + child: TextField( + controller: _moviePrefixController, + decoration: InputDecoration( + hintText: 'Filme filtern...', + prefixIcon: const Icon(Icons.search), + isDense: true, + border: const OutlineInputBorder(), + suffixIcon: _moviePrefix.isEmpty + ? null + : IconButton( + onPressed: _moviePrefixController.clear, + icon: const Icon(Icons.clear), + tooltip: 'Filter loeschen', + ), + ), + ), + ), + ], + ); + }, ), const SizedBox(height: 8), Align( @@ -137,8 +205,15 @@ class MovieListScreen extends ConsumerWidget { Expanded( child: moviesAsync.when( data: (items) { - final itemsSorted = [...items] + final itemsSorted = items + .where((m) => _matchesMoviePrefix(m.title)) + .toList() ..sort((a, b) => a.title.toLowerCase().compareTo(b.title.toLowerCase())); + if (itemsSorted.isEmpty) { + return const Center( + child: Text('Keine Filme fuer den aktuellen Filter.'), + ); + } return RefreshIndicator( onRefresh: () async { // ignore: unused_result @@ -398,3 +473,4 @@ class MovieListScreen extends ConsumerWidget { ); } } + diff --git a/lib/features/series/presentation/series_list_screen.dart b/lib/features/series/presentation/series_list_screen.dart index 5dbfef6..2432576 100644 --- a/lib/features/series/presentation/series_list_screen.dart +++ b/lib/features/series/presentation/series_list_screen.dart @@ -51,7 +51,20 @@ class _SeriesListScreenState extends ConsumerState { final ScrollController _summaryVController = ScrollController(); final ScrollController _groupedHController = ScrollController(); final ScrollController _summaryHController = ScrollController(); + final TextEditingController _seriesPrefixController = TextEditingController(); + String _seriesPrefix = ''; + @override + void initState() { + super.initState(); + _seriesPrefixController.addListener(() { + final next = _seriesPrefixController.text; + if (next == _seriesPrefix) return; + setState(() { + _seriesPrefix = next; + }); + }); + } @override void dispose() { @@ -59,9 +72,18 @@ class _SeriesListScreenState extends ConsumerState { _summaryVController.dispose(); _groupedHController.dispose(); _summaryHController.dispose(); + _seriesPrefixController.dispose(); super.dispose(); } + String get _normalizedSeriesPrefix => _seriesPrefix.trim().toLowerCase(); + + bool _matchesSeriesPrefix(String? name) { + final prefix = _normalizedSeriesPrefix; + if (prefix.isEmpty) return true; + return (name ?? '').trim().toLowerCase().startsWith(prefix); + } + @override Widget build(BuildContext context) { final filter = ref.watch(episodeFilterProvider); @@ -74,10 +96,44 @@ class _SeriesListScreenState extends ConsumerState { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - StatusChip( - selected: filter, - onChanged: (f) => - ref.read(episodeFilterProvider.notifier).state = f, + LayoutBuilder( + builder: (context, constraints) { + final searchWidth = constraints.maxWidth >= 360 + ? 280.0 + : constraints.maxWidth; + + return Wrap( + spacing: 12, + runSpacing: 8, + crossAxisAlignment: WrapCrossAlignment.center, + children: [ + StatusChip( + selected: filter, + onChanged: (f) => + ref.read(episodeFilterProvider.notifier).state = f, + ), + SizedBox( + width: searchWidth, + child: TextField( + controller: _seriesPrefixController, + decoration: InputDecoration( + hintText: 'Serien filtern...', + prefixIcon: const Icon(Icons.search), + isDense: true, + border: const OutlineInputBorder(), + suffixIcon: _seriesPrefix.isEmpty + ? null + : IconButton( + onPressed: _seriesPrefixController.clear, + icon: const Icon(Icons.clear), + tooltip: 'Filter loeschen', + ), + ), + ), + ), + ], + ); + }, ), const widgets.SizedBox(height: 8), Align( @@ -413,6 +469,10 @@ class _SeriesListScreenState extends ConsumerState { bg = Colors.green; } + if (!_matchesSeriesPrefix(displayName)) { + continue; + } + // Apply series-level filter rules final f = filter; final include = () { @@ -857,6 +917,12 @@ class _SeriesListScreenState extends ConsumerState { )); } + if (rows.isEmpty) { + return const Center( + child: Text('Keine Serien fuer den aktuellen Filter.'), + ); + } + final table = DataTable( columns: buildColumns(), rows: rows, @@ -867,36 +933,45 @@ class _SeriesListScreenState extends ConsumerState { showCheckboxColumn: false, ); - return Stack( - children: [ - Scrollbar( - controller: _groupedHController, - child: SingleChildScrollView( - controller: _groupedHController, - scrollDirection: Axis.horizontal, - child: Scrollbar( - controller: _groupedVController, + return LayoutBuilder( + builder: (context, constraints) { + return Stack( + children: [ + Scrollbar( + controller: _groupedHController, child: SingleChildScrollView( - controller: _groupedVController, - child: table, + controller: _groupedHController, + scrollDirection: Axis.horizontal, + child: ConstrainedBox( + constraints: BoxConstraints( + minWidth: constraints.maxWidth, + ), + child: Scrollbar( + controller: _groupedVController, + child: SingleChildScrollView( + controller: _groupedVController, + child: table, + ), + ), + ), ), ), - ), - ), - Positioned( - right: 0, - top: 0, - bottom: 0, - child: SizedBox( - width: 10, - child: AlwaysOnScrollbar( - controller: _groupedVController, - axis: Axis.vertical, - thickness: 10, + Positioned( + right: 0, + top: 0, + bottom: 0, + child: SizedBox( + width: 10, + child: AlwaysOnScrollbar( + controller: _groupedVController, + axis: Axis.vertical, + thickness: 10, + ), + ), ), - ), - ), - ], + ], + ); + }, ); }, ); @@ -976,6 +1051,9 @@ class _SeriesListScreenState extends ConsumerState { // Apply series-level filter items = items.where(includeByFilter).toList(); + items = items + .where((m) => _matchesSeriesPrefix(m['name'] as String?)) + .toList(); // Build and sort keys final keys = []; final byKey = >{}; @@ -989,6 +1067,11 @@ class _SeriesListScreenState extends ConsumerState { String displayOf(String rowKey) => rowKey.split('##').first; keys.sort((a, b) => displayOf(a).toLowerCase().compareTo(displayOf(b).toLowerCase())); + if (keys.isEmpty) { + return const Center( + child: Text('Keine Serien fuer den aktuellen Filter.'), + ); + } // Determine all season columns to create a fixed horizontal grid final allSeasons = {}; @@ -1912,6 +1995,3 @@ Future openDownload() async { - - - diff --git a/lib/features/series/presentation/widgets/summary_table.dart b/lib/features/series/presentation/widgets/summary_table.dart index 05c94db..4260609 100644 --- a/lib/features/series/presentation/widgets/summary_table.dart +++ b/lib/features/series/presentation/widgets/summary_table.dart @@ -34,7 +34,9 @@ class SummaryTable extends StatelessWidget { controller: summaryHController, scrollDirection: Axis.horizontal, child: widgets.SizedBox( - width: tableWidth, + width: tableWidth < constraints.maxWidth + ? constraints.maxWidth + : tableWidth, height: constraints.maxHeight, child: Column( children: [ @@ -85,3 +87,4 @@ class SummaryTable extends StatelessWidget { return tableWidth; } } +