diff --git a/lib/features/series/presentation/series_detail_screen.dart b/lib/features/series/presentation/series_detail_screen.dart index 1c12e92..04bb5b4 100644 --- a/lib/features/series/presentation/series_detail_screen.dart +++ b/lib/features/series/presentation/series_detail_screen.dart @@ -1,3 +1,4 @@ +import 'dart:convert'; import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -36,6 +37,9 @@ class _SeriesDetailScreenState extends ConsumerState { String? _origDownloadPath; bool? _origCliffhanger; late final TextEditingController _downloadCtrl; + String? _overview; + List> _cast = const []; + List> _crew = const []; @override void initState() { @@ -72,6 +76,35 @@ class _SeriesDetailScreenState extends ConsumerState { if (_downloadCtrl.text != (_downloadPath ?? '')) { _downloadCtrl.text = _downloadPath ?? ''; } + // parse overview/cast/crew from show_json + if (first.showJson != null && first.showJson!.isNotEmpty) { + try { + final m = jsonDecode(first.showJson!) as Map; + _overview = (m['overview'] as String?) ?? _overview; + final credits = m['credits'] as Map?; + if (credits != null) { + final c = (credits['cast'] as List? ?? const []) + .cast() + .map((e) => Map.from(e as Map)) + .toList(); + final allCrew = (credits['crew'] as List? ?? const []) + .cast() + .map((e) => Map.from(e as Map)) + .toList(); + _cast = c.take(12).toList(); + // Prefer key roles; if empty, fall back to first crew entries + const preferredJobs = { + 'Showrunner', 'Director', 'Writer', 'Screenplay', 'Story', 'Teleplay', 'Executive Producer', 'Producer' + }; + final preferred = allCrew.where((m) => + preferredJobs.contains((m['job'] ?? '').toString()) || + (m['department'] ?? '').toString() == 'Directing' || + (m['department'] ?? '').toString() == 'Writing'); + final chosen = preferred.isNotEmpty ? preferred : allCrew; + _crew = chosen.take(12).toList(); + } + } catch (_) {} + } } } }); @@ -257,6 +290,17 @@ class _SeriesDetailScreenState extends ConsumerState { children: [ _header(context), const SizedBox(height: 16), + if ((_overview ?? '').isNotEmpty) + _overviewBox(context), + if (_cast.isNotEmpty) ...[ + const SizedBox(height: 16), + _peopleScroller(context, title: 'Cast', list: _cast, subtitleKey: 'character'), + ], + if (_crew.isNotEmpty) ...[ + const SizedBox(height: 16), + _peopleScroller(context, title: 'Crew', list: _crew, subtitleKey: 'job'), + ], + const SizedBox(height: 16), if (seasons == null) const Center(child: CircularProgressIndicator()) else ...[ @@ -430,6 +474,85 @@ class _SeriesDetailScreenState extends ConsumerState { } } + Widget _overviewBox(BuildContext context) { + return Container( + width: double.infinity, + decoration: BoxDecoration( + color: Colors.black.withOpacity(0.35), + borderRadius: BorderRadius.circular(12), + ), + padding: const EdgeInsets.all(12), + child: DefaultTextStyle( + style: const TextStyle(color: Colors.white), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('Beschreibung', style: Theme.of(context).textTheme.titleMedium?.copyWith(color: Colors.white)), + const SizedBox(height: 6), + Text(_overview ?? ''), + ], + ), + ), + ); + } + + Widget _peopleScroller(BuildContext context, {required String title, required List> list, required String subtitleKey}) { + return Container( + width: double.infinity, + decoration: BoxDecoration( + color: Colors.black.withOpacity(0.35), + borderRadius: BorderRadius.circular(12), + ), + padding: const EdgeInsets.all(12), + child: DefaultTextStyle( + style: const TextStyle(color: Colors.white), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(title, style: Theme.of(context).textTheme.titleMedium?.copyWith(color: Colors.white)), + const SizedBox(height: 8), + SizedBox( + height: 150, + child: ListView.separated( + scrollDirection: Axis.horizontal, + itemCount: list.length, + separatorBuilder: (_, __) => const SizedBox(width: 8), + itemBuilder: (_, i) { + final p = list[i]; + final name = p['name']?.toString() ?? ''; + final sub = p[subtitleKey]?.toString() ?? ''; + final profile = p['profile_path']?.toString(); + return SizedBox( + width: 100, + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + ClipRRect( + borderRadius: BorderRadius.circular(8), + child: profile != null + ? Image.network('https://image.tmdb.org/t/p/w185$profile', width: 100, height: 96, fit: BoxFit.cover) + : Container( + width: 100, + height: 96, + color: Colors.black12, + child: const Icon(Icons.person, size: 40), + ), + ), + const SizedBox(height: 6), + Text(name, maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle(fontWeight: FontWeight.w600, color: Colors.white)), + Text(sub, maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle(fontSize: 12, color: Colors.white70)), + ], + ), + ); + }, + ), + ), + ], + ), + ), + ); + } + Widget _resolutionBadge(String? res, BuildContext context) { if (res == null || res.isEmpty) return const SizedBox.shrink(); final m = RegExp(r"\d+").firstMatch(res);