Series Details Screen
This commit is contained in:
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user