Series Detail Screen
This commit is contained in:
@@ -186,5 +186,21 @@ class BackendApi {
|
||||
final v = map['id'];
|
||||
return v == null ? null : (v as num).toInt();
|
||||
}
|
||||
|
||||
Future<void> setShowMeta({
|
||||
required int showId,
|
||||
String? resolution,
|
||||
String? downloadPath,
|
||||
bool? cliffhanger,
|
||||
}) async {
|
||||
final payload = <String, dynamic>{
|
||||
'action': 'set_show_meta',
|
||||
'show_id': showId,
|
||||
if (resolution != null) 'resolution': resolution,
|
||||
if (downloadPath != null) 'download_path': downloadPath,
|
||||
if (cliffhanger != null) 'cliffhanger': cliffhanger,
|
||||
};
|
||||
await _post(payload);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -13,6 +13,8 @@ class EpisodeItem {
|
||||
final String? showStatus;
|
||||
final bool? showCliffhanger;
|
||||
final String? posterPath;
|
||||
final int? showId;
|
||||
final String? downloadPath;
|
||||
|
||||
EpisodeItem({
|
||||
required this.id,
|
||||
@@ -27,6 +29,8 @@ class EpisodeItem {
|
||||
this.showStatus,
|
||||
this.showCliffhanger,
|
||||
this.posterPath,
|
||||
this.showId,
|
||||
this.downloadPath,
|
||||
});
|
||||
|
||||
factory EpisodeItem.fromJson(Map<String, dynamic> j) => EpisodeItem(
|
||||
@@ -42,6 +46,8 @@ class EpisodeItem {
|
||||
showStatus: j['show_status'] as String?,
|
||||
showCliffhanger: _parseBool(j['show_cliffhanger']),
|
||||
posterPath: j['poster_path'] as String?,
|
||||
showId: (j['show_id'] as num?)?.toInt(),
|
||||
downloadPath: j['download_path'] as String?,
|
||||
);
|
||||
|
||||
static bool? _parseBool(dynamic v) {
|
||||
|
||||
@@ -0,0 +1,474 @@
|
||||
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/series_repository.dart';
|
||||
import '../data/episode_model.dart';
|
||||
|
||||
class SeriesDetailScreen extends ConsumerStatefulWidget {
|
||||
final String showName;
|
||||
final int? year;
|
||||
final String? resolution;
|
||||
final String? posterPath;
|
||||
const SeriesDetailScreen({
|
||||
super.key,
|
||||
required this.showName,
|
||||
this.year,
|
||||
this.resolution,
|
||||
this.posterPath,
|
||||
});
|
||||
|
||||
@override
|
||||
ConsumerState<SeriesDetailScreen> createState() => _SeriesDetailScreenState();
|
||||
}
|
||||
|
||||
class _SeriesDetailScreenState extends ConsumerState<SeriesDetailScreen> {
|
||||
Map<int, List<EpisodeItem>>? _seasons; // season -> episodes
|
||||
String? _resolution;
|
||||
String? _downloadPath;
|
||||
bool? _cliffhanger;
|
||||
int? _showId;
|
||||
final Map<int, ItemStatus> _pending = {}; // epId -> new status
|
||||
bool _saving = false;
|
||||
String? _origResolution;
|
||||
String? _origDownloadPath;
|
||||
bool? _origCliffhanger;
|
||||
late final TextEditingController _downloadCtrl;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_resolution = widget.resolution;
|
||||
_downloadCtrl = TextEditingController(text: _downloadPath ?? '');
|
||||
_downloadCtrl.addListener(() {
|
||||
final v = _downloadCtrl.text;
|
||||
if (v != _downloadPath) {
|
||||
setState(() => _downloadPath = v);
|
||||
}
|
||||
});
|
||||
_load();
|
||||
}
|
||||
|
||||
Future<void> _load() async {
|
||||
final grouped = await ref.read(seriesGroupedProvider.future);
|
||||
final data = grouped.data[widget.showName];
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_seasons = data;
|
||||
if (data != null) {
|
||||
// read show-level meta from first episode available
|
||||
final flat = data.values.expand((e) => e);
|
||||
final first = flat.isEmpty ? null : flat.first;
|
||||
if (first != null) {
|
||||
_resolution ??= first.resolution;
|
||||
_downloadPath ??= first.downloadPath;
|
||||
_cliffhanger ??= first.showCliffhanger;
|
||||
_showId ??= first.showId;
|
||||
_origResolution ??= _resolution;
|
||||
_origDownloadPath ??= _downloadPath;
|
||||
_origCliffhanger ??= _cliffhanger;
|
||||
if (_downloadCtrl.text != (_downloadPath ?? '')) {
|
||||
_downloadCtrl.text = _downloadPath ?? '';
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _save() async {
|
||||
if (_saving) return;
|
||||
setState(() => _saving = true);
|
||||
try {
|
||||
final api = ref.read(backendApiProvider);
|
||||
// ensure we have a showId from current data if missing
|
||||
if (_showId == null && _seasons != null && _seasons!.isNotEmpty) {
|
||||
final flat = _seasons!.values.expand((e) => e);
|
||||
final first = flat.isEmpty ? null : flat.first;
|
||||
if (first != null) {
|
||||
_showId = first.showId;
|
||||
}
|
||||
}
|
||||
// apply episode changes
|
||||
final futures = <Future>[];
|
||||
_pending.forEach((epId, st) {
|
||||
futures.add(api.setStatus(type: 'episode', refId: epId, status: st.name));
|
||||
});
|
||||
// show meta
|
||||
if (_showId != null && (_resolution != _origResolution || _downloadPath != _origDownloadPath || _cliffhanger != _origCliffhanger)) {
|
||||
futures.add(api.setShowMeta(
|
||||
showId: _showId!,
|
||||
resolution: _resolution,
|
||||
downloadPath: _downloadPath,
|
||||
cliffhanger: _cliffhanger,
|
||||
));
|
||||
}
|
||||
await Future.wait(futures);
|
||||
// refresh list/table
|
||||
// ignore: unused_result
|
||||
ref.invalidate(seriesGroupedProvider);
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_pending.clear();
|
||||
_origResolution = _resolution;
|
||||
_origDownloadPath = _downloadPath;
|
||||
_origCliffhanger = _cliffhanger;
|
||||
});
|
||||
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Gespeichert')));
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Speichern fehlgeschlagen: $e')));
|
||||
}
|
||||
} finally {
|
||||
if (mounted) setState(() => _saving = 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>[];
|
||||
if (eps.isEmpty) return;
|
||||
setState(() {
|
||||
_seasons = {
|
||||
for (final e in _seasons!.entries)
|
||||
e.key: e.key == season
|
||||
? e.value
|
||||
.map((it) => 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,
|
||||
))
|
||||
.toList()
|
||||
: e.value,
|
||||
};
|
||||
for (final ep in eps) {
|
||||
_pending[ep.id] = s;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final seasons = _seasons;
|
||||
final bgPoster = widget.posterPath;
|
||||
final dirty = _pending.isNotEmpty ||
|
||||
_resolution != _origResolution ||
|
||||
_downloadPath != _origDownloadPath ||
|
||||
_cliffhanger != _origCliffhanger;
|
||||
return Scaffold(
|
||||
extendBodyBehindAppBar: true,
|
||||
appBar: AppBar(
|
||||
backgroundColor: Colors.transparent,
|
||||
elevation: 0,
|
||||
title: Text(widget.showName),
|
||||
actions: [
|
||||
if (dirty)
|
||||
IconButton(
|
||||
icon: const Icon(Icons.save),
|
||||
onPressed: _saving ? null : _save,
|
||||
tooltip: 'Speichern',
|
||||
),
|
||||
],
|
||||
),
|
||||
floatingActionButton: dirty
|
||||
? Padding(
|
||||
padding: EdgeInsets.only(
|
||||
bottom: MediaQuery.of(context).viewInsets.bottom > 0
|
||||
? MediaQuery.of(context).viewInsets.bottom + 8
|
||||
: 0),
|
||||
child: FloatingActionButton.extended(
|
||||
onPressed: _saving ? null : _save,
|
||||
icon: const Icon(Icons.save),
|
||||
label:
|
||||
_saving ? const Text('Speichern...') : const Text('Speichern'),
|
||||
),
|
||||
)
|
||||
: null,
|
||||
body: Container(
|
||||
decoration: bgPoster != null
|
||||
? BoxDecoration(
|
||||
image: DecorationImage(
|
||||
image: NetworkImage('https://image.tmdb.org/t/p/w342$bgPoster'),
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
)
|
||||
: null,
|
||||
foregroundDecoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [
|
||||
Colors.black.withOpacity(0.2),
|
||||
Colors.black.withOpacity(0.6),
|
||||
],
|
||||
),
|
||||
),
|
||||
child: SingleChildScrollView(
|
||||
padding: EdgeInsets.only(
|
||||
top: MediaQuery.of(context).padding.top + kToolbarHeight + 12,
|
||||
left: 12,
|
||||
right: 12,
|
||||
bottom: 24,
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_header(context),
|
||||
const SizedBox(height: 16),
|
||||
if (seasons == null)
|
||||
const Center(child: CircularProgressIndicator())
|
||||
else ...[
|
||||
for (final sNo in (seasons.keys.toList()..sort()))
|
||||
_seasonBox(context, sNo, seasons[sNo]!)
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _header(BuildContext context) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black.withOpacity(0.35),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (widget.posterPath != null)
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: CachedNetworkImage(
|
||||
imageUrl: 'https://image.tmdb.org/t/p/w342${widget.posterPath}',
|
||||
width: 120,
|
||||
height: 180,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
if (widget.posterPath != null) const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: DefaultTextStyle(
|
||||
style: const TextStyle(color: Colors.white),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
widget.year != null ? '${widget.showName} (${widget.year})' : widget.showName,
|
||||
style: Theme.of(context).textTheme.titleLarge?.copyWith(color: Colors.white),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
children: [
|
||||
GestureDetector(
|
||||
onTap: _chooseResolution,
|
||||
child: _resolutionBadge(_resolution, context),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
TextField(
|
||||
controller: _downloadCtrl,
|
||||
style: const TextStyle(color: Colors.white),
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Download Path',
|
||||
labelStyle: TextStyle(color: Colors.white70),
|
||||
enabledBorder: UnderlineInputBorder(borderSide: BorderSide(color: Colors.white54)),
|
||||
focusedBorder: UnderlineInputBorder(borderSide: BorderSide(color: Colors.white)),
|
||||
),
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
Checkbox(
|
||||
value: _cliffhanger ?? false,
|
||||
onChanged: (v) => setState(() => _cliffhanger = v ?? false),
|
||||
checkColor: Colors.white,
|
||||
side: const BorderSide(color: Colors.white70),
|
||||
),
|
||||
const Text('Cliffhanger', style: TextStyle(color: Colors.white)),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _seasonBox(BuildContext context, int seasonNo, List<EpisodeItem> episodes) {
|
||||
final episodesSorted = [...episodes]..sort((a, b) => a.episodeNumber.compareTo(b.episodeNumber));
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 12),
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black.withOpacity(0.35),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: DefaultTextStyle(
|
||||
style: const TextStyle(color: Colors.white),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('Staffel $seasonNo', style: Theme.of(context).textTheme.titleMedium?.copyWith(color: Colors.white)),
|
||||
const SizedBox(height: 8),
|
||||
_statusMatrix(episodesSorted, ItemStatus.Init, 'Init'),
|
||||
const SizedBox(height: 8),
|
||||
_statusMatrix(episodesSorted, ItemStatus.Progress, 'Progress'),
|
||||
const SizedBox(height: 8),
|
||||
_statusMatrix(episodesSorted, ItemStatus.Done, 'Done'),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _statusMatrix(List<EpisodeItem> eps, ItemStatus rowStatus, String label) {
|
||||
return SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 110,
|
||||
child: OutlinedButton(
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor: Colors.white,
|
||||
side: const BorderSide(color: Colors.white70),
|
||||
visualDensity: VisualDensity.compact,
|
||||
),
|
||||
onPressed: () => _setSeasonAll(eps.first.seasonNumber, rowStatus),
|
||||
child: Text('Alle $label'),
|
||||
)),
|
||||
const SizedBox(width: 12),
|
||||
for (final ep in eps) ...[
|
||||
SizedBox(
|
||||
width: 36,
|
||||
child: Column(
|
||||
children: [
|
||||
Radio<ItemStatus>(
|
||||
value: rowStatus,
|
||||
groupValue: (_pending[ep.id] ?? ep.status),
|
||||
fillColor: const MaterialStatePropertyAll(Colors.white),
|
||||
onChanged: (v) {
|
||||
setState(() => _pending[ep.id] = rowStatus);
|
||||
},
|
||||
),
|
||||
Text('${ep.episodeNumber}', style: const TextStyle(fontSize: 10, color: Colors.white70)),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _chooseResolution() async {
|
||||
const options = ['320p', '576p', '720p', '1080p', '2160p'];
|
||||
final selected = await showDialog<String>(
|
||||
context: context,
|
||||
builder: (ctx) => SimpleDialog(
|
||||
backgroundColor: Colors.black.withOpacity(0.85),
|
||||
title: const Text('Resolution wählen', style: TextStyle(color: Colors.white)),
|
||||
children: [
|
||||
for (final opt in options)
|
||||
SimpleDialogOption(
|
||||
onPressed: () => Navigator.of(ctx).pop(opt),
|
||||
child: Text(opt, style: const TextStyle(color: Colors.white)),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
if (selected != null) {
|
||||
setState(() => _resolution = selected);
|
||||
}
|
||||
}
|
||||
|
||||
Widget _resolutionBadge(String? res, BuildContext context) {
|
||||
if (res == null || res.isEmpty) return const SizedBox.shrink();
|
||||
final m = RegExp(r"\d+").firstMatch(res);
|
||||
final v = m != null ? int.tryParse(m.group(0)!) : null;
|
||||
IconData? icon;
|
||||
Color color = Theme.of(context).colorScheme.primary;
|
||||
String label = res;
|
||||
if (v != null) {
|
||||
if (v >= 2000) {
|
||||
icon = Icons.four_k;
|
||||
color = Colors.deepOrange;
|
||||
label = '${v}p';
|
||||
} else if (v >= 1000) {
|
||||
icon = Icons.hd;
|
||||
color = Colors.blue;
|
||||
label = '${v}p';
|
||||
} else {
|
||||
icon = Icons.sd_card;
|
||||
color = Colors.grey;
|
||||
label = '${v}p';
|
||||
}
|
||||
}
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black.withOpacity(0.45),
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
border: Border.all(color: Colors.white.withOpacity(0.6)),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (icon != null) Icon(icon, size: 16, color: color),
|
||||
if (icon != null) const SizedBox(width: 6),
|
||||
const Text('Auflösung:', style: TextStyle(color: Colors.white70, fontSize: 12)),
|
||||
const SizedBox(width: 4),
|
||||
Text(label, style: const TextStyle(fontSize: 13, color: Colors.white)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import '../../../core/status.dart';
|
||||
import '../../movies/presentation/widgets/status_chip.dart';
|
||||
import '../data/series_repository.dart';
|
||||
import 'widgets/episode_status_strip.dart';
|
||||
import 'series_detail_screen.dart';
|
||||
|
||||
class SeriesListScreen extends ConsumerWidget {
|
||||
const SeriesListScreen({super.key});
|
||||
@@ -160,7 +161,21 @@ class SeriesListScreen extends ConsumerWidget {
|
||||
}
|
||||
}
|
||||
|
||||
rows.add(DataRow(cells: cells));
|
||||
rows.add(DataRow(
|
||||
cells: cells,
|
||||
onSelectChanged: (_) {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (_) => SeriesDetailScreen(
|
||||
showName: showName,
|
||||
year: year,
|
||||
resolution: resolution,
|
||||
posterPath: posterPath,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
));
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
@@ -172,6 +187,7 @@ class SeriesListScreen extends ConsumerWidget {
|
||||
dataRowMinHeight: 88,
|
||||
dataRowMaxHeight: 96,
|
||||
columnSpacing: 16,
|
||||
showCheckboxColumn: false,
|
||||
);
|
||||
|
||||
return SingleChildScrollView(
|
||||
|
||||
@@ -247,6 +247,8 @@ try {
|
||||
sh.resolution AS resolution,
|
||||
sh.poster_path AS poster_path,
|
||||
sh.first_air_year AS first_air_year,
|
||||
sh.id AS show_id,
|
||||
sh.download_path AS download_path,
|
||||
JSON_UNQUOTE(JSON_EXTRACT(sh.json, '$.status')) AS show_status,
|
||||
sh.cliffhanger AS show_cliffhanger,
|
||||
sh.json AS show_json,
|
||||
@@ -256,6 +258,8 @@ try {
|
||||
sh.resolution AS resolution,
|
||||
sh.poster_path AS poster_path,
|
||||
NULL AS first_air_year,
|
||||
sh.id AS show_id,
|
||||
sh.download_path AS download_path,
|
||||
JSON_UNQUOTE(JSON_EXTRACT(sh.json, '$.status')) AS show_status,
|
||||
sh.cliffhanger AS show_cliffhanger,
|
||||
sh.json AS show_json,
|
||||
@@ -266,6 +270,8 @@ try {
|
||||
sh.resolution AS resolution,
|
||||
sh.poster_path AS poster_path,
|
||||
sh.first_air_year AS first_air_year,
|
||||
sh.id AS show_id,
|
||||
sh.download_path AS download_path,
|
||||
sh.cliffhanger AS show_cliffhanger,
|
||||
sh.json AS show_json,
|
||||
se.season_number, sh.name AS show_name ".$base;
|
||||
@@ -274,6 +280,8 @@ try {
|
||||
sh.resolution AS resolution,
|
||||
sh.poster_path AS poster_path,
|
||||
NULL AS first_air_year,
|
||||
sh.id AS show_id,
|
||||
sh.download_path AS download_path,
|
||||
sh.cliffhanger AS show_cliffhanger,
|
||||
sh.json AS show_json,
|
||||
se.season_number, sh.name AS show_name ".$base;
|
||||
@@ -357,6 +365,22 @@ try {
|
||||
resp(['ok' => true, 'id' => $id ? (int)$id : null]);
|
||||
}
|
||||
|
||||
case 'set_show_meta': {
|
||||
$showId = (int)($in['show_id'] ?? 0);
|
||||
if (!$showId) fail('bad params');
|
||||
$fields = [];
|
||||
$params = [];
|
||||
if (isset($in['resolution'])) { $fields[] = 'resolution = ?'; $params[] = $in['resolution']; }
|
||||
if (array_key_exists('download_path', $in)) { $fields[] = 'download_path = ?'; $params[] = $in['download_path']; }
|
||||
if (isset($in['cliffhanger'])) { $fields[] = 'cliffhanger = ?'; $params[] = (int)!!$in['cliffhanger']; }
|
||||
if (empty($fields)) fail('no fields');
|
||||
$sql = 'UPDATE shows SET '.implode(', ', $fields).' WHERE id = ?';
|
||||
$params[] = $showId;
|
||||
$stmt = $pdo->prepare($sql);
|
||||
$stmt->execute($params);
|
||||
resp(['ok'=>true]);
|
||||
}
|
||||
|
||||
case 'ping': {
|
||||
resp([
|
||||
'ok' => true,
|
||||
|
||||
Reference in New Issue
Block a user