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
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})'),
|
|
),
|
|
);
|
|
}
|
|
}
|