more game details
This commit is contained in:
@@ -296,6 +296,7 @@ class BackendApi {
|
||||
String? coverUrl,
|
||||
int? releaseYear,
|
||||
bool? locked,
|
||||
Map<String, dynamic>? json,
|
||||
}) async {
|
||||
final map = await _post({
|
||||
'action': 'set_game_status',
|
||||
@@ -311,6 +312,7 @@ class BackendApi {
|
||||
if (coverUrl != null) 'cover_url': coverUrl,
|
||||
if (releaseYear != null) 'release_year': releaseYear,
|
||||
if (locked != null) 'locked': locked,
|
||||
if (json != null) 'json': json,
|
||||
});
|
||||
return (map['game'] as Map).cast<String, dynamic>();
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import 'dart:convert';
|
||||
import '../../../core/status.dart';
|
||||
|
||||
class Game {
|
||||
@@ -12,6 +13,7 @@ class Game {
|
||||
final String? coverUrl;
|
||||
final int? releaseYear;
|
||||
final bool locked;
|
||||
final Map<String, dynamic>? rawJson;
|
||||
|
||||
Game({
|
||||
required this.id,
|
||||
@@ -25,9 +27,18 @@ class Game {
|
||||
this.coverUrl,
|
||||
this.releaseYear,
|
||||
this.locked = false,
|
||||
this.rawJson,
|
||||
});
|
||||
|
||||
factory Game.fromJson(Map<String, dynamic> j) {
|
||||
Map<String, dynamic>? parsedJson;
|
||||
final jsonString = j['json'] as String?;
|
||||
if (jsonString != null && jsonString.isNotEmpty) {
|
||||
try {
|
||||
parsedJson = Map<String, dynamic>.from(
|
||||
jsonDecode(jsonString) as Map<dynamic, dynamic>);
|
||||
} catch (_) {}
|
||||
}
|
||||
return Game(
|
||||
id: (j['id'] as num).toInt(),
|
||||
igdbId: (j['igdb_id'] as num).toInt(),
|
||||
@@ -40,6 +51,7 @@ class Game {
|
||||
coverUrl: j['cover_url'] as String?,
|
||||
releaseYear: (j['release_year'] as num?)?.toInt(),
|
||||
locked: ((j['loc_locked'] ?? j['locked'] ?? 0) as num?)?.toInt() == 1,
|
||||
rawJson: parsedJson,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ class _GamesDetailScreenState extends ConsumerState<GamesDetailScreen> {
|
||||
late TextEditingController _summaryCtrl;
|
||||
String _title = '';
|
||||
String _summary = '';
|
||||
Map<String, dynamic>? _rawJson;
|
||||
late String _origTitle;
|
||||
late String _origSummary;
|
||||
bool _lockTexts = false;
|
||||
@@ -36,6 +37,7 @@ class _GamesDetailScreenState extends ConsumerState<GamesDetailScreen> {
|
||||
_origStatus = widget.game.status;
|
||||
_title = (widget.game.localizedTitle?.isNotEmpty ?? false) ? widget.game.localizedTitle! : widget.game.name;
|
||||
_summary = widget.game.summary ?? '';
|
||||
_rawJson = widget.game.rawJson;
|
||||
_lockTexts = widget.game.locked;
|
||||
_origTitle = _title;
|
||||
_origSummary = _summary;
|
||||
@@ -92,6 +94,7 @@ class _GamesDetailScreenState extends ConsumerState<GamesDetailScreen> {
|
||||
coverUrl: _game.coverUrl,
|
||||
releaseYear: _game.releaseYear,
|
||||
locked: _lockTexts,
|
||||
json: _rawJson,
|
||||
);
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
@@ -107,6 +110,7 @@ class _GamesDetailScreenState extends ConsumerState<GamesDetailScreen> {
|
||||
coverUrl: _game.coverUrl,
|
||||
releaseYear: _game.releaseYear,
|
||||
locked: _lockTexts,
|
||||
rawJson: _rawJson,
|
||||
);
|
||||
_title = title;
|
||||
_summary = summary;
|
||||
@@ -193,6 +197,10 @@ class _GamesDetailScreenState extends ConsumerState<GamesDetailScreen> {
|
||||
children: [
|
||||
_header(context, g),
|
||||
const SizedBox(height: 16),
|
||||
if (_detailsData().isNotEmpty) ...[
|
||||
_detailsSection(context),
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
_summaryCard(context),
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
@@ -343,6 +351,81 @@ class _GamesDetailScreenState extends ConsumerState<GamesDetailScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, List<String>> _detailsData() {
|
||||
final root = _rawJson;
|
||||
if (root == null) return {};
|
||||
List<String> namesOf(String key) {
|
||||
final list = (root[key] as List?) ?? const [];
|
||||
return list
|
||||
.whereType<Map>()
|
||||
.map((e) => (e['name'] ?? '').toString())
|
||||
.where((e) => e.isNotEmpty)
|
||||
.toSet()
|
||||
.toList();
|
||||
}
|
||||
|
||||
List<String> involvedWhere(String flagKey) {
|
||||
final list = (root['involved_companies'] as List?) ?? const [];
|
||||
return list
|
||||
.whereType<Map>()
|
||||
.where((e) => (e[flagKey] ?? false) == true)
|
||||
.map((e) => ((e['company'] as Map?)?['name'] ?? '').toString())
|
||||
.where((e) => e.isNotEmpty)
|
||||
.toSet()
|
||||
.toList();
|
||||
}
|
||||
|
||||
return {
|
||||
'Main Developers': involvedWhere('developer'),
|
||||
'Publishers': involvedWhere('publisher'),
|
||||
'Genres': namesOf('genres'),
|
||||
'Game Modes': namesOf('game_modes'),
|
||||
'Themes': namesOf('themes'),
|
||||
'Player Perspectives': namesOf('player_perspectives'),
|
||||
'Game Engine': namesOf('game_engines'),
|
||||
}..removeWhere((_, v) => v.isEmpty);
|
||||
}
|
||||
|
||||
Widget _detailsSection(BuildContext context) {
|
||||
final data = _detailsData();
|
||||
if (data.isEmpty) return const SizedBox.shrink();
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black.withOpacity(0.6),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: DefaultTextStyle(
|
||||
style: const TextStyle(color: Colors.white),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('Details', style: Theme.of(context).textTheme.titleMedium?.copyWith(color: Colors.white)),
|
||||
const SizedBox(height: 8),
|
||||
for (final entry in data.entries) ...[
|
||||
Text(entry.key, style: const TextStyle(fontWeight: FontWeight.w600)),
|
||||
const SizedBox(height: 4),
|
||||
Wrap(
|
||||
spacing: 6,
|
||||
runSpacing: -6,
|
||||
children: entry.value
|
||||
.map((v) => Chip(
|
||||
backgroundColor: Colors.black54,
|
||||
shape: StadiumBorder(side: BorderSide(color: Colors.white.withOpacity(0.2))),
|
||||
label: Text(v, style: const TextStyle(color: Colors.white)),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4),
|
||||
))
|
||||
.toList(),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _refreshFromIgdb() async {
|
||||
final messenger = ScaffoldMessenger.of(context);
|
||||
try {
|
||||
@@ -356,6 +439,7 @@ class _GamesDetailScreenState extends ConsumerState<GamesDetailScreen> {
|
||||
// If not locked, always replace with fresh IGDB texts
|
||||
final summary = _lockTexts ? _summary : igdbSummary;
|
||||
final title = _lockTexts ? _title : (igdbTitle.isNotEmpty ? igdbTitle : _title);
|
||||
_rawJson = data;
|
||||
final firstRelease = data['first_release_date'];
|
||||
int? year;
|
||||
if (firstRelease is num) {
|
||||
@@ -369,12 +453,13 @@ class _GamesDetailScreenState extends ConsumerState<GamesDetailScreen> {
|
||||
status: _status.index,
|
||||
lang: 'de',
|
||||
// Wenn nicht gesperrt, die aktuellen Texte (aus IGDB oder lokal) zurückschreiben
|
||||
title: title,
|
||||
summary: summary,
|
||||
coverUrl: cover,
|
||||
releaseYear: year,
|
||||
locked: _lockTexts,
|
||||
);
|
||||
title: title,
|
||||
summary: summary,
|
||||
coverUrl: cover,
|
||||
releaseYear: year,
|
||||
locked: _lockTexts,
|
||||
json: _rawJson,
|
||||
);
|
||||
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
@@ -390,6 +475,7 @@ class _GamesDetailScreenState extends ConsumerState<GamesDetailScreen> {
|
||||
coverUrl: cover,
|
||||
releaseYear: year ?? _game.releaseYear,
|
||||
locked: _lockTexts,
|
||||
rawJson: _rawJson,
|
||||
);
|
||||
_title = title;
|
||||
_summary = summary;
|
||||
|
||||
@@ -23,6 +23,21 @@ class GamesScreen extends ConsumerWidget {
|
||||
}
|
||||
}
|
||||
|
||||
String? _engine(Game g) {
|
||||
final root = g.rawJson;
|
||||
if (root == null) return null;
|
||||
final engines = root['game_engines'];
|
||||
if (engines is List && engines.isNotEmpty) {
|
||||
final first = engines.first;
|
||||
if (first is Map && (first['name'] ?? '').toString().isNotEmpty) {
|
||||
return first['name'].toString();
|
||||
}
|
||||
final s = engines.first.toString();
|
||||
if (s.isNotEmpty) return s;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final filter = ref.watch(gameFilterProvider);
|
||||
@@ -109,12 +124,37 @@ class GamesScreen extends ConsumerWidget {
|
||||
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,
|
||||
subtitle: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 140,
|
||||
child: _engine(g) != null
|
||||
? Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade200,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Center(
|
||||
child: Text(
|
||||
_engine(g)!,
|
||||
style: const TextStyle(fontSize: 12),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
)
|
||||
: const SizedBox.shrink(),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
(g.summary?.isNotEmpty ?? false) ? g.summary! : (g.note ?? ''),
|
||||
maxLines: 3,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
trailing: Text(g.status.name),
|
||||
),
|
||||
@@ -160,6 +200,7 @@ class GamesScreen extends ConsumerWidget {
|
||||
coverUrl: cover,
|
||||
releaseYear: year,
|
||||
locked: g.locked,
|
||||
json: data,
|
||||
);
|
||||
// ignore: unused_result
|
||||
ref.invalidate(gamesStreamProvider);
|
||||
@@ -216,6 +257,7 @@ class GamesScreen extends ConsumerWidget {
|
||||
coverUrl: cover,
|
||||
releaseYear: year,
|
||||
locked: g.locked,
|
||||
json: data,
|
||||
);
|
||||
ok++;
|
||||
} catch (_) {
|
||||
|
||||
+12
-2
@@ -84,11 +84,14 @@ function saveGameStatus(int $igdbId, string $name, ?string $originalName, int $s
|
||||
$pdo = getDb();
|
||||
$hasCover = gameHasColumn('cover_url');
|
||||
$hasReleaseYear = gameHasColumn('release_year');
|
||||
$hasJson = gameHasColumn('json');
|
||||
$releaseYear = $GLOBALS['__release_year'] ?? null;
|
||||
$gameJson = $GLOBALS['__game_json'] ?? null;
|
||||
|
||||
$fields = ['igdb_id', 'name', 'original_name', 'status', 'note'];
|
||||
if ($hasCover) $fields[] = 'cover_url';
|
||||
if ($hasReleaseYear) $fields[] = 'release_year';
|
||||
if ($hasJson) $fields[] = 'json';
|
||||
|
||||
$placeholders = array_map(fn($f) => ':'.$f, $fields);
|
||||
$updates = [];
|
||||
@@ -115,6 +118,7 @@ function saveGameStatus(int $igdbId, string $name, ?string $originalName, int $s
|
||||
case 'note': $params[':note'] = $note; break;
|
||||
case 'cover_url': $params[':cover_url'] = $coverUrl; break;
|
||||
case 'release_year': $params[':release_year'] = $releaseYear; break;
|
||||
case 'json': $params[':json'] = $gameJson; break;
|
||||
}
|
||||
}
|
||||
$stmt->execute($params);
|
||||
@@ -483,7 +487,10 @@ if (isset($_GET['action'])) {
|
||||
$id = (int)$idParam;
|
||||
if ($id <= 0) error_response('Invalid \"id\"', 400);
|
||||
$body = sprintf(
|
||||
'fields id,name,summary,storyline,first_release_date,genres.name,platforms.name,cover.image_id,screenshots.image_id,involved_companies.company.name,websites.url,websites.category,age_ratings.rating,age_ratings.category,language_supports.language; where id = %d;',
|
||||
'fields id,name,summary,storyline,first_release_date,genres.name,platforms.name,cover.image_id,screenshots.image_id,'
|
||||
.'involved_companies.company.name,involved_companies.developer,involved_companies.publisher,'
|
||||
.'websites.url,websites.category,age_ratings.rating,age_ratings.category,language_supports.language,'
|
||||
.'game_modes.name,themes.name,player_perspectives.name,game_engines.name; where id = %d;',
|
||||
$id
|
||||
);
|
||||
$results = igdbRequest('/games', $body);
|
||||
@@ -1081,7 +1088,8 @@ try {
|
||||
loc.title AS loc_title,
|
||||
loc.summary AS loc_summary,
|
||||
loc.storyline AS loc_storyline,
|
||||
loc.locked AS loc_locked
|
||||
loc.locked AS loc_locked,
|
||||
g.json AS json
|
||||
FROM game g
|
||||
LEFT JOIN igdb_localizations loc
|
||||
ON loc.igdb_id = g.igdb_id AND loc.lang = ?
|
||||
@@ -1350,7 +1358,9 @@ try {
|
||||
$lockFlag = array_key_exists('locked', $in) ? (int)!!$in['locked'] : null;
|
||||
$coverUrl = isset($in['cover_url']) ? trim($in['cover_url']) : null;
|
||||
$releaseYear = isset($in['release_year']) ? (int)$in['release_year'] : null;
|
||||
$gameJson = isset($in['json']) ? $in['json'] : null;
|
||||
$GLOBALS['__release_year'] = $releaseYear;
|
||||
$GLOBALS['__game_json'] = $gameJson !== null ? json_encode($gameJson, JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES) : null;
|
||||
if ($igdbId <= 0) fail('Invalid "igdb_id"');
|
||||
if ($name === '') fail('Missing "name"');
|
||||
if ($status < 0 || $status > 2) fail('Invalid "status" (0,1,2)');
|
||||
|
||||
Reference in New Issue
Block a user