Testfilter, Listenbreite

This commit is contained in:
2026-06-26 20:53:21 +02:00
parent a6bd718157
commit a66ed3e6cf
3 changed files with 199 additions and 40 deletions
@@ -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<MovieListScreen> createState() => _MovieListScreenState();
}
class _MovieListScreenState extends ConsumerState<MovieListScreen> {
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 {
);
}
}
@@ -51,7 +51,20 @@ class _SeriesListScreenState extends ConsumerState<SeriesListScreen> {
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<SeriesListScreen> {
_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<SeriesListScreen> {
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<SeriesListScreen> {
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<SeriesListScreen> {
));
}
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<SeriesListScreen> {
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<SeriesListScreen> {
// 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 = <String>[];
final byKey = <String, Map<String, dynamic>>{};
@@ -989,6 +1067,11 @@ class _SeriesListScreenState extends ConsumerState<SeriesListScreen> {
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 = <int>{};
@@ -1912,6 +1995,3 @@ Future<void> openDownload() async {
@@ -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;
}
}