From 2dbc0f630fd9c58750fe99005efa2fd475b8788e Mon Sep 17 00:00:00 2001 From: Herwig Birke Date: Tue, 2 Dec 2025 15:39:22 +0100 Subject: [PATCH] delete games --- lib/core/api/backend_api.dart | 6 + lib/core/api/igdb_api.dart | 20 ++ lib/features/games/data/game_model.dart | 3 + .../games/presentation/games_add_screen.dart | 15 +- .../presentation/games_detail_screen.dart | 275 ++++++++++++++++++ .../games/presentation/games_screen.dart | 160 ++++++++-- lib/php/multimedia.php | 158 +++++++--- 7 files changed, 569 insertions(+), 68 deletions(-) create mode 100644 lib/features/games/presentation/games_detail_screen.dart diff --git a/lib/core/api/backend_api.dart b/lib/core/api/backend_api.dart index 46419ee..dac6f02 100644 --- a/lib/core/api/backend_api.dart +++ b/lib/core/api/backend_api.dart @@ -279,6 +279,10 @@ class BackendApi { await _post({'action': 'delete_show', 'show_id': showId}); } + Future deleteGame(int igdbId) async { + await _post({'action': 'delete_game', 'igdb_id': igdbId}); + } + Future> setGameStatus({ required int igdbId, required String name, @@ -290,6 +294,7 @@ class BackendApi { String? summary, String? storyline, String? coverUrl, + int? releaseYear, }) async { final map = await _post({ 'action': 'set_game_status', @@ -303,6 +308,7 @@ class BackendApi { if (summary != null) 'summary': summary, if (storyline != null) 'storyline': storyline, if (coverUrl != null) 'cover_url': coverUrl, + if (releaseYear != null) 'release_year': releaseYear, }); return (map['game'] as Map).cast(); } diff --git a/lib/core/api/igdb_api.dart b/lib/core/api/igdb_api.dart index 00a3f05..551a0a8 100644 --- a/lib/core/api/igdb_api.dart +++ b/lib/core/api/igdb_api.dart @@ -34,4 +34,24 @@ class IgdbApi { } throw Exception('IGDB search: unerwartetes Antwortformat'); } + + Future> getGameDetails(int igdbId, {String lang = 'de'}) async { + final res = await _dio.get( + '', + queryParameters: { + 'action': 'details', + 'id': igdbId, + 'lang': lang, + 'check_external': 1, + }, + ); + if (res.statusCode != 200) { + throw Exception('IGDB details HTTP ${res.statusCode}'); + } + final data = res.data; + if (data is Map && data['game'] is Map) { + return (data['game'] as Map).cast(); + } + throw Exception('IGDB details: unerwartetes Antwortformat'); + } } diff --git a/lib/features/games/data/game_model.dart b/lib/features/games/data/game_model.dart index 20652bd..3e6daa4 100644 --- a/lib/features/games/data/game_model.dart +++ b/lib/features/games/data/game_model.dart @@ -10,6 +10,7 @@ class Game { final String? note; final String? summary; final String? coverUrl; + final int? releaseYear; Game({ required this.id, @@ -21,6 +22,7 @@ class Game { this.note, this.summary, this.coverUrl, + this.releaseYear, }); factory Game.fromJson(Map j) { @@ -34,6 +36,7 @@ class Game { note: j['note'] as String?, summary: j['loc_summary'] as String?, coverUrl: j['cover_url'] as String?, + releaseYear: (j['release_year'] as num?)?.toInt(), ); } diff --git a/lib/features/games/presentation/games_add_screen.dart b/lib/features/games/presentation/games_add_screen.dart index 46b6221..96067d0 100644 --- a/lib/features/games/presentation/games_add_screen.dart +++ b/lib/features/games/presentation/games_add_screen.dart @@ -54,6 +54,11 @@ class _GamesAddScreenState extends ConsumerState { final summary = (game['summary'] ?? '') as String; final title = (game['name'] ?? '') as String; final cover = game['cover_url'] as String?; + final fr = game['first_release_date']; + int? releaseYear; + if (fr is num) { + releaseYear = DateTime.fromMillisecondsSinceEpoch(fr.toInt() * 1000).year; + } if (name.isEmpty) continue; await backend.setGameStatus( igdbId: id, @@ -65,6 +70,7 @@ class _GamesAddScreenState extends ConsumerState { title: title.isNotEmpty ? title : null, summary: summary.isNotEmpty ? summary : null, coverUrl: cover, + releaseYear: releaseYear, ); ok++; } @@ -135,6 +141,11 @@ class _GamesAddScreenState extends ConsumerState { final title = (g['name'] ?? '') as String; final summary = (g['summary'] ?? '') as String; final cover = g['cover_url'] as String?; + final fr = g['first_release_date']; + int? releaseYear; + if (fr is num) { + releaseYear = DateTime.fromMillisecondsSinceEpoch(fr.toInt() * 1000).year; + } return ListTile( onTap: () { setState(() { @@ -156,7 +167,9 @@ class _GamesAddScreenState extends ConsumerState { ), ) : const SizedBox(width: 60, height: 80), - title: Text(title), + title: Text( + releaseYear != null ? '$title ($releaseYear)' : title, + ), subtitle: summary.isNotEmpty ? Text( summary, diff --git a/lib/features/games/presentation/games_detail_screen.dart b/lib/features/games/presentation/games_detail_screen.dart new file mode 100644 index 0000000..ee1bbf4 --- /dev/null +++ b/lib/features/games/presentation/games_detail_screen.dart @@ -0,0 +1,275 @@ +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/game_model.dart'; +import '../data/game_repository.dart'; + +class GamesDetailScreen extends ConsumerStatefulWidget { + final Game game; + const GamesDetailScreen({super.key, required this.game}); + + @override + ConsumerState createState() => _GamesDetailScreenState(); +} + +class _GamesDetailScreenState extends ConsumerState { + late Game _game; + late ItemStatus _status; + late ItemStatus _origStatus; + + @override + void initState() { + super.initState(); + _game = widget.game; + _status = widget.game.status; + _origStatus = widget.game.status; + } + + bool get _dirty => _status != _origStatus; + + Future _save() async { + final messenger = ScaffoldMessenger.of(context); + try { + final backend = ref.read(backendApiProvider); + await backend.setGameStatus( + igdbId: _game.igdbId, + name: _game.name, + originalName: _game.originalName, + status: _status.index, + lang: 'de', + title: _game.localizedTitle ?? _game.name, + summary: _game.summary, + coverUrl: _game.coverUrl, + releaseYear: _game.releaseYear, + ); + if (!mounted) return; + setState(() => _origStatus = _status); + // ignore: unused_result + ref.invalidate(gamesStreamProvider); + // ignore: unused_result + ref.invalidate(gamesProvider); + messenger.showSnackBar(const SnackBar(content: Text('Gespeichert'))); + } catch (e) { + messenger.showSnackBar(SnackBar(content: Text('Speichern fehlgeschlagen: $e'))); + } + } + + @override + Widget build(BuildContext context) { + final g = _game; + final cover = g.coverUrl; + return Scaffold( + extendBodyBehindAppBar: true, + appBar: AppBar( + backgroundColor: Colors.transparent, + elevation: 0, + title: Text(g.displayName), + actions: [ + IconButton( + icon: const Icon(Icons.sync), + tooltip: 'IGDB aktualisieren', + onPressed: _refreshFromIgdb, + ), + ], + ), + floatingActionButton: _dirty + ? FloatingActionButton.extended( + onPressed: _save, + icon: const Icon(Icons.save), + label: const Text('Speichern'), + ) + : null, + body: Stack( + children: [ + Positioned.fill( + child: Container( + decoration: cover != null && cover.isNotEmpty + ? BoxDecoration( + image: DecorationImage( + image: CachedNetworkImageProvider(cover), + fit: BoxFit.cover, + ), + ) + : null, + ), + ), + Positioned.fill( + child: Container( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + Colors.black.withOpacity(0.25), + Colors.black.withOpacity(0.65), + ], + ), + ), + ), + ), + Positioned.fill( + 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, g), + const SizedBox(height: 16), + if ((g.summary ?? '').isNotEmpty) _summaryCard(context, g.summary!), + ], + ), + ), + ), + ], + ), + ); + } + + Widget _header(BuildContext context, Game g) { + 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 (g.coverUrl != null && g.coverUrl!.isNotEmpty) + ClipRRect( + borderRadius: BorderRadius.circular(8), + child: CachedNetworkImage( + imageUrl: g.coverUrl!, + width: 120, + height: 170, + fit: BoxFit.cover, + ), + ), + if (g.coverUrl != null && g.coverUrl!.isNotEmpty) const SizedBox(width: 12), + Expanded( + child: DefaultTextStyle( + style: const TextStyle(color: Colors.white), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + g.releaseYear != null ? '${g.displayName} (${g.releaseYear})' : g.displayName, + style: Theme.of(context).textTheme.titleLarge?.copyWith(color: Colors.white), + ), + if (g.originalName != null && g.originalName!.isNotEmpty) ...[ + const SizedBox(height: 4), + Text( + g.originalName!, + style: const TextStyle(color: Colors.white70, fontStyle: FontStyle.italic), + ), + ], + const SizedBox(height: 12), + _statusSelector(_status), + ], + ), + ), + ), + ], + ), + ); + } + + Widget _summaryCard(BuildContext context, String summary) { + 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(summary), + ], + ), + ), + ); + } + + Widget _statusSelector(ItemStatus current) { + return Wrap( + spacing: 8, + children: [ + for (final s in ItemStatus.values) + ChoiceChip( + label: Text(s.name), + selected: current == s, + onSelected: (sel) { + if (sel) setState(() => _status = s); + }, + ), + ], + ); + } + + Future _refreshFromIgdb() async { + final messenger = ScaffoldMessenger.of(context); + try { + final igdb = ref.read(igdbApiProvider); + final data = await igdb.getGameDetails(_game.igdbId, lang: 'de'); + final cover = (data['cover'] is Map && data['cover']['image_id'] != null) + ? 'https://images.igdb.com/igdb/image/upload/t_cover_big/${data['cover']['image_id']}.jpg' + : _game.coverUrl; + final summary = data['summary'] as String? ?? _game.summary; + final title = data['name'] as String? ?? _game.name; + final firstRelease = data['first_release_date']; + int? year; + if (firstRelease is num) { + year = DateTime.fromMillisecondsSinceEpoch(firstRelease.toInt() * 1000).year; + } + + await ref.read(backendApiProvider).setGameStatus( + igdbId: _game.igdbId, + name: title, + originalName: _game.originalName, + status: _status.index, + lang: 'de', + title: title, + summary: summary, + coverUrl: cover, + releaseYear: year, + ); + + if (!mounted) return; + setState(() { + _game = Game( + id: _game.id, + igdbId: _game.igdbId, + name: title, + localizedTitle: title, + originalName: _game.originalName, + status: _status, + note: _game.note, + summary: summary, + coverUrl: cover, + releaseYear: year ?? _game.releaseYear, + ); + }); + // ignore: unused_result + ref.invalidate(gamesStreamProvider); + // ignore: unused_result + ref.invalidate(gamesProvider); + messenger.showSnackBar(const SnackBar(content: Text('IGDB aktualisiert'))); + } catch (e) { + messenger.showSnackBar(SnackBar(content: Text('IGDB Update fehlgeschlagen: $e'))); + } + } +} diff --git a/lib/features/games/presentation/games_screen.dart b/lib/features/games/presentation/games_screen.dart index 0b224ca..7e5a662 100644 --- a/lib/features/games/presentation/games_screen.dart +++ b/lib/features/games/presentation/games_screen.dart @@ -2,22 +2,24 @@ 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/game_repository.dart'; +import '../data/game_model.dart'; import 'games_add_screen.dart'; import '../../movies/presentation/widgets/status_chip.dart'; +import 'games_detail_screen.dart'; class GamesScreen extends ConsumerWidget { const GamesScreen({super.key}); Color _statusBg(ItemStatus status, BuildContext context) { switch (status) { + case ItemStatus.Init: + return Colors.grey.shade200; case ItemStatus.Progress: - return Theme.of(context).colorScheme.secondaryContainer.withOpacity(0.3); + return Colors.blue; case ItemStatus.Done: - return Theme.of(context).colorScheme.primaryContainer.withOpacity(0.35); - case ItemStatus.Init: - default: - return Colors.transparent; + return Colors.green; } } @@ -69,31 +71,43 @@ class GamesScreen extends ConsumerWidget { separatorBuilder: (_, __) => const Divider(height: 1), itemBuilder: (_, i) { final g = sorted[i]; - return ListTile( - tileColor: _statusBg(g.status, context), - leading: g.coverUrl != null && g.coverUrl!.isNotEmpty - ? ClipRRect( - borderRadius: BorderRadius.circular(6), - child: CachedNetworkImage( - imageUrl: g.coverUrl!, - width: 50, - height: 70, - fit: BoxFit.cover, + return GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () { + Navigator.of(context).push( + MaterialPageRoute(builder: (_) => GamesDetailScreen(game: g)), + ); + }, + onLongPressStart: (d) => _showContextMenu(context, ref, g, d.globalPosition), + onSecondaryTapDown: (d) => _showContextMenu(context, ref, g, d.globalPosition), + child: ListTile( + tileColor: _statusBg(g.status, context), + leading: g.coverUrl != null && g.coverUrl!.isNotEmpty + ? ClipRRect( + borderRadius: BorderRadius.circular(6), + child: CachedNetworkImage( + imageUrl: g.coverUrl!, + width: 50, + height: 70, + fit: BoxFit.cover, + ), + ) + : CircleAvatar( + backgroundColor: Theme.of(context).colorScheme.secondaryContainer, + child: const Icon(Icons.sports_esports), ), - ) - : CircleAvatar( - backgroundColor: Theme.of(context).colorScheme.secondaryContainer, - child: const Icon(Icons.sports_esports), - ), - title: Text(g.displayName), - subtitle: Text( - (g.summary?.isNotEmpty ?? false) - ? g.summary! - : (g.note ?? ''), - maxLines: 3, - overflow: TextOverflow.ellipsis, + title: Text( + g.releaseYear != null ? '${g.displayName} (${g.releaseYear})' : g.displayName, + ), + subtitle: Text( + (g.summary?.isNotEmpty ?? false) + ? g.summary! + : (g.note ?? ''), + maxLines: 3, + overflow: TextOverflow.ellipsis, + ), + trailing: Text(g.status.name), ), - trailing: Text(g.status.name), ); }, ), @@ -107,4 +121,94 @@ class GamesScreen extends ConsumerWidget { ), ); } + + Future _updateGame(BuildContext context, WidgetRef ref, Game g) async { + final messenger = ScaffoldMessenger.of(context); + try { + final igdb = ref.read(igdbApiProvider); + final data = await igdb.getGameDetails(g.igdbId, lang: 'de'); + final cover = (data['cover'] is Map && data['cover']['image_id'] != null) + ? 'https://images.igdb.com/igdb/image/upload/t_cover_big/${data['cover']['image_id']}.jpg' + : g.coverUrl; + final summary = data['summary'] as String? ?? g.summary; + final title = data['name'] as String? ?? g.name; + final fr = data['first_release_date']; + int? year; + if (fr is num) { + year = DateTime.fromMillisecondsSinceEpoch(fr.toInt() * 1000).year; + } + await ref.read(backendApiProvider).setGameStatus( + igdbId: g.igdbId, + name: title, + originalName: g.originalName, + status: g.status.index, + lang: 'de', + title: title, + summary: summary, + coverUrl: cover, + releaseYear: year, + ); + // ignore: unused_result + ref.invalidate(gamesStreamProvider); + // ignore: unused_result + ref.invalidate(gamesProvider); + messenger.showSnackBar(const SnackBar(content: Text('Update abgeschlossen'))); + } catch (e) { + messenger.showSnackBar(SnackBar(content: Text('Update fehlgeschlagen: $e'))); + } + } + + Future _deleteGame(BuildContext context, WidgetRef ref, Game g) async { + final ok = await showDialog( + context: context, + builder: (ctx) => AlertDialog( + title: const Text('Spiel löschen?'), + content: Text('\"${g.displayName}\" wirklich löschen?'), + actions: [ + TextButton(onPressed: () => Navigator.of(ctx).pop(false), child: const Text('Abbrechen')), + TextButton(onPressed: () => Navigator.of(ctx).pop(true), child: const Text('Löschen')), + ], + ), + ); + if (ok != true) return; + final messenger = ScaffoldMessenger.of(context); + try { + await ref.read(backendApiProvider).deleteGame(g.igdbId); + // ignore: unused_result + ref.invalidate(gamesStreamProvider); + // ignore: unused_result + ref.invalidate(gamesProvider); + messenger.showSnackBar(const SnackBar(content: Text('Spiel gelöscht'))); + } catch (e) { + messenger.showSnackBar(SnackBar(content: Text('Löschen fehlgeschlagen: $e'))); + } + } + + Future _showContextMenu(BuildContext context, WidgetRef ref, Game g, Offset pos) async { + final selected = await showMenu<_GameAction>( + context: context, + position: RelativeRect.fromLTRB(pos.dx, pos.dy, pos.dx, pos.dy), + items: const [ + PopupMenuItem( + value: _GameAction.update, + child: Text('Update'), + ), + PopupMenuItem( + value: _GameAction.delete, + child: Text('Delete'), + ), + ], + ); + if (selected == null) return; + switch (selected) { + case _GameAction.update: + await _updateGame(context, ref, g); + break; + case _GameAction.delete: + await _deleteGame(context, ref, g); + break; + } + } } + +enum _GameAction { update, delete } diff --git a/lib/php/multimedia.php b/lib/php/multimedia.php index 3da17e6..4cb4d3b 100644 --- a/lib/php/multimedia.php +++ b/lib/php/multimedia.php @@ -82,49 +82,40 @@ function getDb(): PDO { // Shared game helpers (usable for both GET and POST actions) function saveGameStatus(int $igdbId, string $name, ?string $originalName, int $status, ?string $note = null, ?string $coverUrl = null): array { $pdo = getDb(); - static $hasCover = null; - if ($hasCover === null) { - try { - $pdo->query("SHOW COLUMNS FROM game LIKE 'cover_url'"); - $hasCover = true; - } catch (Throwable $e) { - $hasCover = false; - } + $hasCover = gameHasColumn('cover_url'); + $hasReleaseYear = gameHasColumn('release_year'); + $releaseYear = $GLOBALS['__release_year'] ?? null; + + $fields = ['igdb_id', 'name', 'original_name', 'status', 'note']; + if ($hasCover) $fields[] = 'cover_url'; + if ($hasReleaseYear) $fields[] = 'release_year'; + + $placeholders = array_map(fn($f) => ':'.$f, $fields); + $updates = []; + foreach ($fields as $f) { + if ($f === 'igdb_id') continue; + $updates[] = "$f = VALUES($f)"; } - $sql = $hasCover - ? ' - INSERT INTO game (igdb_id, name, original_name, status, note, cover_url) - VALUES (:igdb_id, :name, :original_name, :status, :note, :cover_url) - ON DUPLICATE KEY UPDATE - name = VALUES(name), - original_name = VALUES(original_name), - status = VALUES(status), - note = VALUES(note), - cover_url = VALUES(cover_url), - updated_at = CURRENT_TIMESTAMP - ' - : ' - INSERT INTO game (igdb_id, name, original_name, status, note) - VALUES (:igdb_id, :name, :original_name, :status, :note) - ON DUPLICATE KEY UPDATE - name = VALUES(name), - original_name = VALUES(original_name), - status = VALUES(status), - note = VALUES(note), - updated_at = CURRENT_TIMESTAMP - '; + $sql = sprintf( + 'INSERT INTO game (%s) VALUES (%s) ON DUPLICATE KEY UPDATE %s', + implode(', ', $fields), + implode(', ', $placeholders), + implode(', ', $updates) . ', updated_at = CURRENT_TIMESTAMP' + ); $stmt = $pdo->prepare($sql); - $params = [ - ':igdb_id' => $igdbId, - ':name' => $name, - ':original_name' => $originalName, - ':status' => $status, - ':note' => $note, - ]; - if ($hasCover) { - $params[':cover_url'] = $coverUrl; + $params = []; + foreach ($fields as $f) { + switch ($f) { + case 'igdb_id': $params[':igdb_id'] = $igdbId; break; + case 'name': $params[':name'] = $name; break; + case 'original_name': $params[':original_name'] = $originalName; break; + case 'status': $params[':status'] = $status; break; + case 'note': $params[':note'] = $note; break; + case 'cover_url': $params[':cover_url'] = $coverUrl; break; + case 'release_year': $params[':release_year'] = $releaseYear; break; + } } $stmt->execute($params); $stmt = $pdo->prepare('SELECT * FROM game WHERE igdb_id = :id LIMIT 1'); @@ -139,6 +130,26 @@ function getGameByIgdbId(int $igdbId): ?array { return $stmt->fetch() ?: null; } +function gameColumns(): array { + static $cols = null; + if ($cols !== null) return $cols; + $cols = []; + try { + $pdo = getDb(); + $stmt = $pdo->query('DESCRIBE game'); + foreach ($stmt as $row) { + if (isset($row['Field'])) $cols[] = $row['Field']; + } + } catch (Throwable $e) { + $cols = []; + } + return $cols; +} + +function gameHasColumn(string $col): bool { + return in_array($col, gameColumns(), true); +} + function getLocalization(int $igdbId, string $lang = 'de'): ?array { $pdo = getDb(); $stmt = $pdo->prepare('SELECT * FROM igdb_localizations WHERE igdb_id = :id AND lang = :lang LIMIT 1'); @@ -563,6 +574,8 @@ if (isset($_GET['action'])) { $status = (int)($data['status'] ?? 0); $note = isset($data['note']) ? trim($data['note']) : ''; $coverUrl = isset($data['cover_url']) ? trim($data['cover_url']) : null; + $releaseYear = isset($data['release_year']) ? (int)$data['release_year'] : null; + $GLOBALS['__release_year'] = $releaseYear; if ($igdbId <= 0) error_response('Invalid \"igdb_id\"', 400); if ($name === '') error_response('Missing \"name\"', 400); if ($status < 0 || $status > 2) error_response('Invalid \"status\" (0, 1, 2 allowed)', 400); @@ -570,6 +583,27 @@ if (isset($_GET['action'])) { respond(['success' => true, 'game' => $game]); break; + case 'delete_game': + if ($_SERVER['REQUEST_METHOD'] !== 'POST') error_response('Use POST', 405); + $igdbId = (int)($in['igdb_id'] ?? 0); + if ($igdbId <= 0) fail('Invalid \"igdb_id\"'); + $pdo->beginTransaction(); + try { + // remove localizations first + $stmt = $pdo->prepare('DELETE FROM igdb_localizations WHERE igdb_id = ?'); + $stmt->execute([$igdbId]); + // remove game entry + $stmt = $pdo->prepare('DELETE FROM game WHERE igdb_id = ?'); + $stmt->execute([$igdbId]); + $pdo->commit(); + resp(['ok' => true]); + } catch (Throwable $e) { + $pdo->rollBack(); + if (MM_DEBUG) fail('delete failed: '.$e->getMessage(), 500); + fail('delete failed', 500); + } + break; + case 'get_game': $idParam = $_GET['igdb_id'] ?? ''; if ($idParam === '') error_response('Missing \"igdb_id\"', 400); @@ -579,6 +613,24 @@ if (isset($_GET['action'])) { respond(['found' => (bool)$game, 'game' => $game]); break; + case 'delete_game': + $idParam = $_GET['igdb_id'] ?? ''; + $igdbId = (int)$idParam; + if ($igdbId <= 0) error_response('Invalid \"igdb_id\"', 400); + $pdo->beginTransaction(); + try { + $stmt = $pdo->prepare('DELETE FROM igdb_localizations WHERE igdb_id = ?'); + $stmt->execute([$igdbId]); + $stmt = $pdo->prepare('DELETE FROM game WHERE igdb_id = ?'); + $stmt->execute([$igdbId]); + $pdo->commit(); + respond(['ok' => true]); + } catch (Throwable $e) { + $pdo->rollBack(); + error_response('delete failed', 500); + } + break; + case 'wiki_test': $title = $_GET['title'] ?? ''; if ($title === '') error_response('Missing \"title\"', 400); @@ -1007,7 +1059,11 @@ try { $s = strtolower($status); if ($s==='init') $statusVal = 0; elseif ($s==='progress') $statusVal = 1; elseif ($s==='done') $statusVal = 2; } + $hasRelease = gameHasColumn('release_year'); + $hasCover = gameHasColumn('cover_url'); $sql = "SELECT g.*, + ".($hasRelease ? 'g.release_year' : 'NULL AS release_year').", + ".($hasCover ? 'g.cover_url' : 'NULL AS cover_url').", CASE g.status WHEN 1 THEN 'Progress' WHEN 2 THEN 'Done' ELSE 'Init' END AS status, loc.title AS loc_title, loc.summary AS loc_summary, @@ -1264,6 +1320,7 @@ try { 'origin' => $origin, 'php' => PHP_VERSION, ]); + break; } case 'set_game_status': { @@ -1277,6 +1334,8 @@ try { $locSummary = isset($in['summary']) ? trim($in['summary']) : ''; $locStory = isset($in['storyline']) ? trim($in['storyline']) : ''; $coverUrl = isset($in['cover_url']) ? trim($in['cover_url']) : null; + $releaseYear = isset($in['release_year']) ? (int)$in['release_year'] : null; + $GLOBALS['__release_year'] = $releaseYear; if ($igdbId <= 0) fail('Invalid "igdb_id"'); if ($name === '') fail('Missing "name"'); if ($status < 0 || $status > 2) fail('Invalid "status" (0,1,2)'); @@ -1285,6 +1344,26 @@ try { saveLocalization($igdbId, $lang, $locTitle ?: null, $locSummary ?: null, $locStory ?: null, null); } resp(['ok' => true, 'game' => $game]); + break; + } + + case 'delete_game': { + $igdbId = (int)($in['igdb_id'] ?? 0); + if ($igdbId <= 0) fail('Invalid "igdb_id"'); + $pdo->beginTransaction(); + try { + $stmt = $pdo->prepare('DELETE FROM igdb_localizations WHERE igdb_id = ?'); + $stmt->execute([$igdbId]); + $stmt = $pdo->prepare('DELETE FROM game WHERE igdb_id = ?'); + $stmt->execute([$igdbId]); + $pdo->commit(); + resp(['ok' => true]); + } catch (Throwable $e) { + $pdo->rollBack(); + if (MM_DEBUG) fail('delete failed: '.$e->getMessage(), 500); + fail('delete failed', 500); + } + break; } case 'get_game': { @@ -1292,6 +1371,7 @@ try { if ($igdbId <= 0) fail('Invalid "igdb_id"'); $game = getGameByIgdbId($igdbId); resp(['ok' => true, 'found' => (bool)$game, 'game' => $game]); + break; } default: fail('unknown action');