@ -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 < List < int > > (
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 = < int > [ ] ;
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 < void > 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 < String , dynamic > ) [
' 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 < Map < String , dynamic > > ( ) ;
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 = < DataCell > [
@ -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 < Map < String , dynamic > > items ) {
final filter = ref . watch ( episodeFilterProvider ) ;
bool _includeByFilter ( Map < String , dynamic > m ) {
int initTotal = 0 ;
int progTotal = 0 ;
int doneTotal = 0 ;
List < Map < String , dynamic > > seasons ;
if ( m [ ' seasons ' ] is List ) {
seasons = ( m [ ' seasons ' ] as List ) . cast < Map < String , dynamic > > ( ) ;
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 = < String > [ ] ;
final byKey = < String , Map < String , dynamic > > { } ;
@ -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 = < int , int > { } ;
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 \n Progress: $ progress \n Done: $ 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 < EpisodeItem > eps ) {
final inits = < int > [ ] ;
final progs = < int > [ ] ;
final dones = < int > [ ] ;
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 < int > l ) = > l . isEmpty ? ' - ' : l . join ( ' , ' ) ;
return ' Init: ${ fmt ( inits ) } \n Progress: ${ fmt ( progs ) } \n Done: ${ fmt ( dones ) } ' ;
}
extension _SeriesContextMenu on SeriesListScreen {
List < Widget > _seriesMenuItems (
BuildContext context ,
@ -1456,3 +1467,4 @@ extension _SeriesContextMenu on SeriesListScreen {