diff --git a/lib/app.dart b/lib/app.dart index 5e824ff..b7b1618 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -2,7 +2,6 @@ import 'package:flutter/material.dart'; import 'features/movies/presentation/movie_list_screen.dart'; import 'features/series/presentation/series_list_screen.dart'; import 'features/import/import_screen.dart'; -import 'features/ping/ping_test_screen.dart'; class MultimediaApp extends StatelessWidget { const MultimediaApp({super.key}); @@ -32,7 +31,7 @@ class _HomeTabsState extends State<_HomeTabs> @override void initState() { super.initState(); - _controller = TabController(length: 4, vsync: this); + _controller = TabController(length: 3, vsync: this); } @override @@ -52,7 +51,6 @@ class _HomeTabsState extends State<_HomeTabs> Tab(text: 'Filme'), Tab(text: 'Serien'), Tab(text: 'Import'), - Tab(text: 'Ping'), ], ), ), @@ -62,7 +60,6 @@ class _HomeTabsState extends State<_HomeTabs> const MovieListScreen(), SeriesListScreen(), const ImportScreen(), - const PingTestScreen(), ], ), ); diff --git a/lib/features/movies/presentation/movie_add_screen.dart b/lib/features/movies/presentation/movie_add_screen.dart index 6b65398..cb4ef6d 100644 --- a/lib/features/movies/presentation/movie_add_screen.dart +++ b/lib/features/movies/presentation/movie_add_screen.dart @@ -145,6 +145,7 @@ class _MovieAddScreenState extends ConsumerState { final year = release.length >= 4 ? release.substring(0, 4) : ''; final poster = m['poster_path'] as String?; final sel = _selected.contains(id); + final overview = (m['overview'] as String?) ?? ''; return ListTile( onTap: () { setState(() { @@ -167,6 +168,14 @@ class _MovieAddScreenState extends ConsumerState { ) : const SizedBox(width: 50, height: 75), title: Text(year.isNotEmpty ? '$title ($year)' : title), + subtitle: overview.isNotEmpty + ? Text( + overview, + maxLines: 2, + overflow: TextOverflow.ellipsis, + ) + : null, + isThreeLine: overview.isNotEmpty, trailing: Checkbox( value: sel, onChanged: (v) { diff --git a/lib/features/series/data/series_repository.dart b/lib/features/series/data/series_repository.dart index aac2db1..8a8715e 100644 --- a/lib/features/series/data/series_repository.dart +++ b/lib/features/series/data/series_repository.dart @@ -8,7 +8,6 @@ final episodeFilterProvider = StateProvider((_) => null); final seriesGroupedProvider = FutureProvider.autoDispose((ref) async { final backend = ref.watch(backendApiProvider); - final st = ref.watch(episodeFilterProvider); int pageSize = 1000; var offset = 0; final items = []; @@ -18,8 +17,8 @@ final seriesGroupedProvider = while (true) { List> page; try { + // Always fetch all episodes; apply filtering at series-level in UI. page = await backend.getEpisodes( - status: st?.name, offset: offset, limit: pageSize, ); diff --git a/lib/features/series/presentation/series_add_screen.dart b/lib/features/series/presentation/series_add_screen.dart index b60c1b1..518e530 100644 --- a/lib/features/series/presentation/series_add_screen.dart +++ b/lib/features/series/presentation/series_add_screen.dart @@ -89,9 +89,11 @@ class _SeriesAddScreenState extends ConsumerState { if (mounted) setState(() {}); } catch (_) {} } - // Refresh series list/table + // Refresh series list/table (both grouped and summary views) // ignore: unused_result ref.invalidate(seriesGroupedProvider); + // ignore: unused_result + ref.invalidate(seriesSummaryProvider); messenger.showSnackBar( SnackBar(content: Text('$ok Serie(n) hinzugefügt'))); if (mounted) Navigator.of(context).pop(); @@ -158,6 +160,7 @@ class _SeriesAddScreenState extends ConsumerState { final year = firstAir.length >= 4 ? firstAir.substring(0, 4) : ''; final poster = m['poster_path'] as String?; final sel = _selected.contains(id); + final overview = (m['overview'] as String?) ?? ''; return ListTile( onTap: () { setState(() { @@ -179,8 +182,15 @@ class _SeriesAddScreenState extends ConsumerState { ), ) : const SizedBox(width: 50, height: 75), - title: - Text(year.isNotEmpty ? '$title ($year)' : title), + title: Text(year.isNotEmpty ? '$title ($year)' : title), + subtitle: overview.isNotEmpty + ? Text( + overview, + maxLines: 2, + overflow: TextOverflow.ellipsis, + ) + : null, + isThreeLine: overview.isNotEmpty, trailing: Checkbox( value: sel, onChanged: (v) { diff --git a/lib/features/series/presentation/series_detail_screen.dart b/lib/features/series/presentation/series_detail_screen.dart index 6272639..6d7beb5 100644 --- a/lib/features/series/presentation/series_detail_screen.dart +++ b/lib/features/series/presentation/series_detail_screen.dart @@ -507,11 +507,19 @@ class _SeriesDetailScreenState extends ConsumerState { children: [ Text('Staffel $seasonNo', style: Theme.of(context).textTheme.titleMedium?.copyWith(color: Colors.white)), const SizedBox(height: 8), - _statusMatrix(episodesSorted, ItemStatus.Init, 'Init'), - const SizedBox(height: 8), - _statusMatrix(episodesSorted, ItemStatus.Progress, 'Progress'), - const SizedBox(height: 8), - _statusMatrix(episodesSorted, ItemStatus.Done, 'Done'), + SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _statusMatrix(episodesSorted, ItemStatus.Init, 'Init'), + const SizedBox(height: 8), + _statusMatrix(episodesSorted, ItemStatus.Progress, 'Progress'), + const SizedBox(height: 8), + _statusMatrix(episodesSorted, ItemStatus.Done, 'Done'), + ], + ), + ), ], ), ), @@ -519,9 +527,7 @@ class _SeriesDetailScreenState extends ConsumerState { } Widget _statusMatrix(List eps, ItemStatus rowStatus, String label) { - return SingleChildScrollView( - scrollDirection: Axis.horizontal, - child: Row( + return Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ SizedBox( @@ -555,8 +561,7 @@ class _SeriesDetailScreenState extends ConsumerState { ), ], ], - ), - ); + ); } Future _chooseResolution() async { diff --git a/lib/features/series/presentation/series_list_screen.dart b/lib/features/series/presentation/series_list_screen.dart index 42a1100..01b9aab 100644 --- a/lib/features/series/presentation/series_list_screen.dart +++ b/lib/features/series/presentation/series_list_screen.dart @@ -175,123 +175,6 @@ class SeriesListScreen extends ConsumerWidget { ], ), ), - const widgets.SizedBox(height: 6), - Align( - alignment: Alignment.centerRight, - child: TextButton.icon( - icon: const Icon(Icons.build), - label: const Text('TMDB: Serie reparieren (IDs)'), - onPressed: () async { - final ctrl = TextEditingController(); - final ids = await showDialog>( - context: context, - builder: (ctx) => AlertDialog( - title: - const Text('TMDB IDs (Komma/Leerzeichen getrennt)'), - content: TextField( - controller: ctrl, - decoration: - const InputDecoration(hintText: 'z.B. 1396, 1399'), - autofocus: true, - ), - actions: [ - TextButton( - onPressed: () => Navigator.of(ctx).pop(), - child: const Text('Abbrechen')), - FilledButton( - onPressed: () { - final parts = ctrl.text.split(RegExp(r'[\s,;]+')); - final list = []; - for (final p in parts) { - final v = int.tryParse(p.trim()); - if (v != null) list.add(v); - } - Navigator.of(ctx).pop(list); - }, - child: const Text('Start'), - ), - ], - ), - ); - if (ids == null || ids.isEmpty) return; - if (!context.mounted) return; - await showDialog( - context: context, - barrierDismissible: false, - builder: (ctx) { - String current = ''; - int idx = 0; - final int total = ids.length; - final tmdb = ref.read(tmdbApiProvider); - final backend = ref.read(backendApiProvider); - return StatefulBuilder(builder: (ctx, setState) { - Future run() async { - try { - for (final tmdbId in ids) { - idx++; - current = 'tmdb:$tmdbId'; - setState(() {}); - try { - final showJson = await tmdb.getShow(tmdbId); - await backend.upsertShow(showJson); - final seasons = - (showJson['seasons'] as List? ?? const []) - .where((s) => - (s['season_number'] ?? -1) is num) - .map((s) => (s as Map)[ - 'season_number'] as int) - .toList(); - final dbShowId = - await backend.getShowDbIdByTmdbId(tmdbId); - if (dbShowId == null) continue; - for (final sNo in seasons) { - final seasonJson = - await tmdb.getSeason(tmdbId, sNo); - final seasonId = await backend.upsertSeason( - dbShowId, seasonJson); - final eps = - (seasonJson['episodes'] as List? ?? - const []) - .cast>(); - for (final e in eps) { - await backend.upsertEpisode(seasonId, e); - } - } - } catch (_) {} - } - // ignore: unused_result - ref.invalidate(seriesGroupedProvider); - } finally { - if (ctx.mounted) Navigator.of(ctx).pop(); - } - } - - if (idx == 0) { - // ignore: discarded_futures - run(); - } - return AlertDialog( - title: const Text('TMDB Reparatur'), - content: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text('Aktualisiere: $idx/$total'), - const widgets.SizedBox(height: 8), - Text(current, - maxLines: 2, overflow: TextOverflow.ellipsis), - const widgets.SizedBox(height: 12), - LinearProgressIndicator( - value: total == 0 ? null : idx / total), - ], - ), - ); - }); - }, - ); - }, - ), - ), const widgets.SizedBox(height: 12), Expanded( child: Builder(builder: (_) { @@ -419,6 +302,23 @@ class SeriesListScreen extends ConsumerWidget { } else { bg = Colors.green; } + + // Apply series-level filter rules + final f = filter; + final include = () { + if (f == null) return true; + switch (f) { + case ItemStatus.Progress: + return anyProgress; + case ItemStatus.Init: + return anyInit && !anyProgress; + case ItemStatus.Done: + return !anyInit && !anyProgress; + } + }(); + if (!include) { + continue; // skip this series row + } // Context menu handled by MenuAnchor (no manual showMenu) final cells = [ @@ -733,19 +633,25 @@ class SeriesListScreen extends ConsumerWidget { onPressed: () async { final messenger = ScaffoldMessenger.of(context); if (downloadPath != null && downloadPath.isNotEmpty) { final ok = await openFolder(downloadPath); if (!ok) { messenger.showSnackBar(const SnackBar(content: Text('Pfad kann nicht geöffnet werden')));} } else { messenger.showSnackBar(const SnackBar(content: Text('Kein Download Path vorhanden')));} }, ), ], - builder: (ctx, controller, child) => GestureDetector( - behavior: HitTestBehavior.opaque, - onSecondaryTapDown: (_) => controller.open(), - onSecondaryTapUp: (_) => controller.open(), - onLongPress: () => controller.open(), - child: EpisodeStatusStrip( - episodes: eps, - barWidth: 7, - barHeight: 25, - spacing: 0, + builder: (ctx, controller, child) => GestureDetector( + behavior: HitTestBehavior.opaque, + onSecondaryTapDown: (_) => controller.open(), + onSecondaryTapUp: (_) => controller.open(), + onLongPress: () => controller.open(), + child: Tooltip( + message: _tooltipForEpisodes(eps), + child: Align( + alignment: Alignment.centerLeft, + child: EpisodeStatusStrip( + episodes: eps, + barWidth: 5, + barHeight: 25, + spacing: 0, + ), ), ), ), + ), )); } } @@ -795,6 +701,72 @@ class SeriesListScreen extends ConsumerWidget { Widget _buildFromSummary( BuildContext context, WidgetRef ref, List> items) { + final filter = ref.watch(episodeFilterProvider); + + bool _includeByFilter(Map m) { + int initTotal = 0; + int progTotal = 0; + int doneTotal = 0; + + List> seasons; + if (m['seasons'] is List) { + seasons = (m['seasons'] as List).cast>(); + for (final s in seasons) { + initTotal += (s['init'] as num?)?.toInt() ?? 0; + progTotal += (s['progress'] as num?)?.toInt() ?? 0; + doneTotal += (s['done'] as num?)?.toInt() ?? 0; + } + } else if (m['season_status'] is String) { + final str = m['season_status'] as String; + for (final part in str.split('|')) { + if (part.isEmpty) continue; + final seg = part.split(':'); + if (seg.length < 2) continue; + final counts = seg[1].split(','); + initTotal += counts.isNotEmpty ? int.tryParse(counts[0]) ?? 0 : 0; + progTotal += counts.length > 1 ? int.tryParse(counts[1]) ?? 0 : 0; + doneTotal += counts.length > 2 ? int.tryParse(counts[2]) ?? 0 : 0; + } + } else if (m['seasons_eps'] is String) { + // Fallback: derive counts from compact episode list + final se = (m['seasons_eps'] as String); + for (final part in se.split(';')) { + if (part.isEmpty) continue; + final seg = part.split(':'); + if (seg.length < 2) continue; + final list = seg[1]; + for (final eSeg in list.split(',')) { + if (eSeg.isEmpty) continue; + final kv = eSeg.split('|'); + final stCode = (kv.length > 1 ? int.tryParse(kv[1]) : 0) ?? 0; + if (stCode == 2) { + doneTotal++; + } else if (stCode == 1) { + progTotal++; + } else { + initTotal++; + } + } + } + } + + final anyInit = initTotal > 0; + final anyProgress = progTotal > 0; + final anyDone = doneTotal > 0; + + if (filter == null) return true; + switch (filter) { + case ItemStatus.Progress: + return anyProgress; + case ItemStatus.Init: + return anyInit && !anyProgress; + case ItemStatus.Done: + return !anyInit && !anyProgress && anyDone; + } + } + + // Apply series-level filter + items = items.where(_includeByFilter).toList(); // Build and sort keys final keys = []; final byKey = >{}; @@ -831,7 +803,7 @@ class SeriesListScreen extends ConsumerWidget { final seasonCols = allSeasons.toList()..sort(); // Compute per-season column widths based on widest row (episode count) - const double barW = 7; + const double barW = 5; const int cap = 120; final maxSquares = {}; for (final m in items) { @@ -1112,11 +1084,17 @@ class SeriesListScreen extends ConsumerWidget { onSecondaryTapDown: (_) => controller.open(), onSecondaryTapUp: (_) => controller.open(), onLongPress: () => controller.open(), - child: EpisodeStatusStrip( - episodes: eps, - barWidth: 7, - barHeight: 25, - spacing: 0, + child: Tooltip( + message: _tooltipForEpisodes(eps), + child: Align( + alignment: Alignment.centerLeft, + child: EpisodeStatusStrip( + episodes: eps, + barWidth: 5, + barHeight: 25, + spacing: 0, + ), + ), ), ), ); @@ -1162,14 +1140,20 @@ class SeriesListScreen extends ConsumerWidget { final init = (match['init'] as num?)?.toInt() ?? 0; final progress = (match['progress'] as num?)?.toInt() ?? 0; final done = (match['done'] as num?)?.toInt() ?? 0; - return SeasonStatusBar( - seasonNumber: s, - init: init, - progress: progress, - done: done, - barWidth: 7, - barHeight: 25, - cap: 120, + return Tooltip( + message: 'Init: $init\nProgress: $progress\nDone: $done', + child: Align( + alignment: Alignment.centerLeft, + child: SeasonStatusBar( + seasonNumber: s, + init: init, + progress: progress, + done: done, + barWidth: 5, + barHeight: 25, + cap: 120, + ), + ), ); }), ), @@ -1236,7 +1220,10 @@ class SeriesListScreen extends ConsumerWidget { child: ListView.separated( itemCount: keys.length, separatorBuilder: (_, __) => const Divider(height: 1), - itemBuilder: (_, i) => buildRow(keys[i]), + itemBuilder: (_, i) => widgets.SizedBox( + height: 88, + child: buildRow(keys[i]), + ), ), ), ], @@ -1321,6 +1308,30 @@ class SeriesListScreen extends ConsumerWidget { } } +String _tooltipForEpisodes(List eps) { + final inits = []; + final progs = []; + final dones = []; + for (final e in eps) { + switch (e.status) { + case ItemStatus.Init: + inits.add(e.episodeNumber); + break; + case ItemStatus.Progress: + progs.add(e.episodeNumber); + break; + case ItemStatus.Done: + dones.add(e.episodeNumber); + break; + } + } + inits.sort(); + progs.sort(); + dones.sort(); + String fmt(List l) => l.isEmpty ? '-' : l.join(', '); + return 'Init: ${fmt(inits)}\nProgress: ${fmt(progs)}\nDone: ${fmt(dones)}'; +} + extension _SeriesContextMenu on SeriesListScreen { List _seriesMenuItems( BuildContext context, @@ -1456,3 +1467,4 @@ extension _SeriesContextMenu on SeriesListScreen { +