more game details
This commit is contained in:
@@ -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 (_) {
|
||||
|
||||
Reference in New Issue
Block a user