You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

234 lines
8.3 KiB
Dart

import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../shared/providers.dart';
import '../data/game_repository.dart';
class GamesAddScreen extends ConsumerStatefulWidget {
const GamesAddScreen({super.key});
@override
ConsumerState<GamesAddScreen> createState() => _GamesAddScreenState();
}
class _GamesAddScreenState extends ConsumerState<GamesAddScreen> {
final _queryCtrl = TextEditingController();
bool _loading = false;
List<Map<String, dynamic>> _results = const [];
final Set<int> _selected = {};
Set<int> _existing = {};
Future<void> _loadExisting() async {
final backend = ref.read(backendApiProvider);
const pageSize = 500;
var offset = 0;
final ids = <int>{};
while (true) {
final page = await backend.getGames(offset: offset, limit: pageSize);
for (final g in page) {
final v = (g['igdb_id'] as num?)?.toInt();
if (v != null) ids.add(v);
}
if (page.length < pageSize) break;
offset += pageSize;
}
setState(() {
_existing = ids;
});
}
Future<void> _search() async {
final q = _queryCtrl.text.trim();
if (q.isEmpty) return;
setState(() {
_loading = true;
_results = const [];
_selected.clear();
});
try {
await _loadExisting();
final igdb = ref.read(igdbApiProvider);
final items = await igdb.searchGames(q, limit: 30, lang: 'de');
final filtered = items.where((g) {
final id = (g['id'] as num?)?.toInt();
if (id == null) return true;
return !_existing.contains(id);
}).toList();
setState(() => _results = filtered);
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context)
.showSnackBar(SnackBar(content: Text('Suche fehlgeschlagen: $e')));
}
} finally {
if (mounted) setState(() => _loading = false);
}
}
Future<void> _addSelected() async {
if (_selected.isEmpty) return;
setState(() => _loading = true);
final messenger = ScaffoldMessenger.of(context);
try {
final backend = ref.read(backendApiProvider);
int ok = 0;
for (final id in _selected) {
final game = _results.firstWhere((g) => (g['id'] as num).toInt() == id);
final name = (game['name'] ?? '') as String;
final original = (game['original_name'] ?? '') as String;
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,
name: name,
originalName: original.isNotEmpty ? original : null,
status: 0,
note: null,
lang: 'de',
title: title.isNotEmpty ? title : null,
summary: summary.isNotEmpty ? summary : null,
coverUrl: cover,
releaseYear: releaseYear,
);
ok++;
}
// ignore: unused_result
ref.invalidate(gamesStreamProvider);
// ignore: unused_result
ref.invalidate(gamesProvider);
messenger.showSnackBar(SnackBar(content: Text('$ok Spiel(e) hinzugefügt')));
if (mounted) Navigator.of(context).pop();
} catch (e) {
messenger.showSnackBar(SnackBar(content: Text('Hinzufügen fehlgeschlagen: $e')));
} finally {
if (mounted) setState(() => _loading = false);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Neue Spiele hinzufügen (IGDB)'),
actions: [
if (_selected.isNotEmpty)
TextButton.icon(
onPressed: _loading ? null : _addSelected,
icon: const Icon(Icons.add),
label: Text('Hinzufügen (${_selected.length})'),
)
],
),
body: Padding(
padding: const EdgeInsets.all(12),
child: Column(
children: [
Row(
children: [
Expanded(
child: TextField(
controller: _queryCtrl,
decoration: const InputDecoration(
hintText: 'Spiele suchen …',
prefixIcon: Icon(Icons.search),
),
onSubmitted: (_) => _search(),
),
),
const SizedBox(width: 8),
FilledButton.icon(
onPressed: _loading ? null : _search,
icon: const Icon(Icons.search),
label: const Text('Suchen'),
),
],
),
const SizedBox(height: 12),
if (_loading) const LinearProgressIndicator(),
const SizedBox(height: 8),
Expanded(
child: _results.isEmpty
? const Center(child: Text('Keine Ergebnisse'))
: ListView.separated(
itemCount: _results.length,
separatorBuilder: (_, __) => const Divider(height: 1),
itemBuilder: (_, i) {
final g = _results[i];
final id = (g['id'] as num).toInt();
final sel = _selected.contains(id);
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(() {
if (sel) {
_selected.remove(id);
} else {
_selected.add(id);
}
});
},
leading: cover != null
? ClipRRect(
borderRadius: BorderRadius.circular(6),
child: CachedNetworkImage(
imageUrl: cover,
width: 60,
height: 80,
fit: BoxFit.cover,
),
)
: const SizedBox(width: 60, height: 80),
title: Text(
releaseYear != null ? '$title ($releaseYear)' : title,
),
subtitle: summary.isNotEmpty
? Text(
summary,
maxLines: 3,
overflow: TextOverflow.ellipsis,
)
: null,
isThreeLine: summary.isNotEmpty,
trailing: Checkbox(
value: sel,
onChanged: (v) {
setState(() {
if (v == true) {
_selected.add(id);
} else {
_selected.remove(id);
}
});
},
),
);
},
),
),
],
),
),
floatingActionButton: FloatingActionButton.extended(
onPressed: _selected.isEmpty || _loading ? null : _addSelected,
icon: const Icon(Icons.add),
label: Text('Hinzufügen (${_selected.length})'),
),
);
}
}