Serie Context Menu

This commit is contained in:
2025-11-04 17:33:12 +01:00
parent 982654273b
commit 3086a751ec
11 changed files with 929 additions and 423 deletions
+4 -4
View File
@@ -58,11 +58,11 @@ class _HomeTabsState extends State<_HomeTabs>
),
body: TabBarView(
controller: _controller,
children: const [
MovieListScreen(),
children: [
const MovieListScreen(),
SeriesListScreen(),
ImportScreen(),
PingTestScreen(),
const ImportScreen(),
const PingTestScreen(),
],
),
);
+6 -4
View File
@@ -86,11 +86,10 @@ class BackendApi {
// ignore: avoid_print
final sc = res?.statusCode;
final data = res?.data;
print('Backend HTTP ${sc}. URL: ${_dio.options.baseUrl}, payload: $payload, data: ${data}');
if (map != null && map.containsKey('error')) {
throw Exception('Backend HTTP ${sc}: ${map['error']}');
throw Exception('Backend HTTP $sc: ${map['error']}');
}
throw Exception('Backend HTTP ${sc}: ${data}');
throw Exception('Backend HTTP $sc: $data');
}
if (map == null) {
throw Exception('Backend: Unexpected response format');
@@ -224,5 +223,8 @@ class BackendApi {
final map = await _post({'action': 'get_show_episodes', 'show_id': showId});
return (map['items'] as List).cast<Map<String, dynamic>>();
}
}
Future<void> deleteShow(int showId) async {
await _post({'action': 'delete_show', 'show_id': showId});
}
}
+1
View File
@@ -1,5 +1,6 @@
/// Globale Konfiguration. Standard: von `--dart-define` lesen.
/// Fallbacks sind hilfreich für lokale Tests.
library;
class AppConfig {
static const backendBaseUrl = String.fromEnvironment(
+1
View File
@@ -1,3 +1,4 @@
// ignore: constant_identifier_names
enum ItemStatus { Init, Progress, Done }
ItemStatus statusFromString(String? s) {
+2 -7
View File
@@ -1,7 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:dio/dio.dart';
import '../../core/api/tmdb_api.dart';
import '../../core/api/backend_api.dart';
import '../shared/providers.dart';
@@ -20,7 +19,7 @@ class _ImportScreenState extends ConsumerState<ImportScreen> {
String _log = '';
bool _busy = false;
void _append(String msg) => setState(() => _log += msg + '\n');
void _append(String msg) => setState(() => _log += '$msg\n');
Future<void> _importMovies() async {
setState(() => _busy = true);
@@ -58,7 +57,7 @@ class _ImportScreenState extends ConsumerState<ImportScreen> {
try {
_append('Serie $showId: TMDB laden …');
final showJson = await tmdb.getShow(showId);
print('SHOW JSON: $showJson'); // Debug-Ausgabe
// Debug-Ausgabe
await backend.upsertShow(showJson);
_append('Serie $showId: Show OK ✓');
@@ -88,10 +87,6 @@ class _ImportScreenState extends ConsumerState<ImportScreen> {
} catch (e) {
// 👇 Hier kommt der erweiterte Catch hin!
if (e is DioException) {
print('❗ TMDB DioException für $showId');
print('➡️ Request: ${e.requestOptions.uri}');
print('➡️ Response: ${e.response?.data}');
print('➡️ Status: ${e.response?.statusCode}');
}
_append('Serie $showId: Fehler → $e');
}
@@ -2,7 +2,6 @@ import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../core/status.dart';
import '../../shared/providers.dart';
import '../data/movie_repository.dart';
@@ -18,7 +18,6 @@ class MovieDetailScreen extends ConsumerStatefulWidget {
class _MovieDetailScreenState extends ConsumerState<MovieDetailScreen> {
late ItemStatus _status;
Map<String, dynamic>? _tmdb;
bool _loading = false;
String? _resolution;
late ItemStatus _origStatus;
late String? _origResolution;
@@ -34,7 +33,6 @@ class _MovieDetailScreenState extends ConsumerState<MovieDetailScreen> {
}
Future<void> _loadTmdb() async {
setState(() => _loading = true);
try {
final tmdb = ref.read(tmdbApiProvider);
final data = await tmdb.getMovie(widget.movie.tmdbId);
@@ -42,7 +40,9 @@ class _MovieDetailScreenState extends ConsumerState<MovieDetailScreen> {
} catch (e) {
// ignore error, keep UI functional
} finally {
if (mounted) setState(() => _loading = false);
if (mounted){
}
}
}
@@ -95,26 +95,26 @@ class _MovieDetailScreenState extends ConsumerState<MovieDetailScreen> {
tooltip: 'TMDB aktualisieren',
icon: const Icon(Icons.sync),
onPressed: () async {
// Capture messenger before async gaps to avoid context use across awaits
final messenger = ScaffoldMessenger.of(context);
try {
final tmdb = ref.read(tmdbApiProvider);
final json = await tmdb.getMovie(m.tmdbId);
await ref.read(backendApiProvider).upsertMovie(json);
if (!mounted) return;
// Update local TMDB data for immediate UI refresh
if (mounted) setState(() => _tmdb = json);
setState(() => _tmdb = json);
// Refresh movie list so the list reflects new metadata
// ignore: unused_result
ref.invalidate(moviesProvider);
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('TMDB Daten aktualisiert')),
);
}
messenger.showSnackBar(
const SnackBar(content: Text('TMDB Daten aktualisiert')),
);
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('TMDB Update fehlgeschlagen: $e')),
);
}
// Even if the widget unmounted, showing a SnackBar via captured messenger is safe
messenger.showSnackBar(
SnackBar(content: Text('TMDB Update fehlgeschlagen: $e')),
);
}
},
)
@@ -202,16 +202,6 @@ class MovieListScreen extends ConsumerWidget {
);
}
IconData _statusIcon(ItemStatus s) {
switch (s) {
case ItemStatus.Init:
return Icons.hourglass_empty;
case ItemStatus.Progress:
return Icons.downloading;
case ItemStatus.Done:
return Icons.check_circle;
}
}
Color? _statusBg(ItemStatus s, BuildContext context) {
switch (s) {
@@ -31,7 +31,11 @@ final seriesGroupedProvider =
continue; // retry same offset with smaller page
}
// If we already have some data, return partial results to unblock UI
if (items.isNotEmpty) break; else rethrow;
if (items.isNotEmpty) {
break;
} else {
rethrow;
}
}
consecutiveFailures = 0;
if (page.isEmpty) break;
@@ -214,35 +214,6 @@ class _SeriesDetailScreenState extends ConsumerState<SeriesDetailScreen> {
bool _saved = false;
void _setEpisodeStatus(EpisodeItem ep, ItemStatus s) {
setState(() {
_pending[ep.id] = s;
// reflect change in local list so radios don't jump back after save
_seasons = {
for (final e in _seasons!.entries)
e.key: e.value
.map((it) => it.id == ep.id
? EpisodeItem(
id: it.id,
episodeNumber: it.episodeNumber,
seasonNumber: it.seasonNumber,
showName: it.showName,
name: it.name,
status: s,
resolution: it.resolution,
firstAirYear: it.firstAirYear,
showJson: it.showJson,
showStatus: it.showStatus,
showCliffhanger: it.showCliffhanger,
posterPath: it.posterPath,
showId: it.showId,
downloadPath: it.downloadPath,
)
: it)
.toList(),
};
});
}
Future<void> _setSeasonAll(int season, ItemStatus s) async {
final eps = _seasons?[season] ?? const <EpisodeItem>[];
@@ -285,10 +256,13 @@ class _SeriesDetailScreenState extends ConsumerState<SeriesDetailScreen> {
_resolution != _origResolution ||
_downloadPath != _origDownloadPath ||
_cliffhanger != _origCliffhanger;
return WillPopScope(
onWillPop: () async {
Navigator.of(context).pop(_saved ? 'updated' : null);
return false;
return PopScope(
canPop: true,
onPopInvokedWithResult: (didPop, result) async {
if (didPop) return;
if (context.mounted) {
Navigator.of(context).pop(_saved ? 'updated' : null);
}
},
child: Scaffold(
extendBodyBehindAppBar: true,
@@ -303,6 +277,7 @@ class _SeriesDetailScreenState extends ConsumerState<SeriesDetailScreen> {
onPressed: _tmdbId == null
? null
: () async {
final messenger = ScaffoldMessenger.of(context);
try {
final tmdb = ref.read(tmdbApiProvider);
final backend = ref.read(backendApiProvider);
@@ -353,17 +328,13 @@ class _SeriesDetailScreenState extends ConsumerState<SeriesDetailScreen> {
// ignore: unused_result
ref.invalidate(seriesGroupedProvider);
await _load();
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('TMDB Daten aktualisiert')),
);
}
messenger.showSnackBar(
const SnackBar(content: Text('TMDB Daten aktualisiert')),
);
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('TMDB Update fehlgeschlagen: $e')),
);
}
messenger.showSnackBar(
SnackBar(content: Text('TMDB Update fehlgeschlagen: $e')),
);
}
},
),
@@ -573,7 +544,7 @@ class _SeriesDetailScreenState extends ConsumerState<SeriesDetailScreen> {
Radio<ItemStatus>(
value: rowStatus,
groupValue: (_pending[ep.id] ?? ep.status),
fillColor: const MaterialStatePropertyAll(Colors.white),
fillColor: const WidgetStatePropertyAll(Colors.white),
onChanged: (v) {
setState(() => _pending[ep.id] = rowStatus);
},
File diff suppressed because it is too large Load Diff