Series Details Screen

This commit is contained in:
2025-10-30 15:21:38 +01:00
parent a77e36c6ec
commit fccaeb8f7a
@@ -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<SeriesDetailScreen> {
String? _origDownloadPath;
bool? _origCliffhanger;
late final TextEditingController _downloadCtrl;
String? _overview;
List<Map<String, dynamic>> _cast = const [];
List<Map<String, dynamic>> _crew = const [];
@override
void initState() {
@@ -72,6 +76,35 @@ class _SeriesDetailScreenState extends ConsumerState<SeriesDetailScreen> {
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<String, dynamic>;
_overview = (m['overview'] as String?) ?? _overview;
final credits = m['credits'] as Map<String, dynamic>?;
if (credits != null) {
final c = (credits['cast'] as List? ?? const [])
.cast()
.map((e) => Map<String, dynamic>.from(e as Map))
.toList();
final allCrew = (credits['crew'] as List? ?? const [])
.cast()
.map((e) => Map<String, dynamic>.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<SeriesDetailScreen> {
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<SeriesDetailScreen> {
}
}
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<Map<String, dynamic>> 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);