import 'dart:convert'; import 'package:flutter/material.dart'; import '../routes.dart'; import '../services/api_auth_service.dart'; import '../services/deck_storage.dart'; import '../services/remote_deck_service.dart'; import '../utils/connection_error.dart'; import '../utils/top_snackbar.dart'; String _apiErrorMessage(RemoteDeckException e) { if (e.statusCode == 401 || e.statusCode == 403) { return 'Session expired. Please log in again.'; } if (e.statusCode >= 500) { return 'Connection to server has broken. Check API URL and network.'; } try { final map = jsonDecode(e.body) as Map?; final msg = map?['error'] as String? ?? map?['message'] as String?; if (msg != null && msg.isNotEmpty) return msg; } catch (_) {} return 'Failed to load community decks.'; } class CommunityDecksScreen extends StatefulWidget { const CommunityDecksScreen({super.key, this.showAppBar = true}); final bool showAppBar; @override State createState() => CommunityDecksScreenState(); } class CommunityDecksScreenState extends State { List _decks = []; bool _loading = true; String? _error; final DeckStorage _storage = DeckStorage(); final RemoteDeckService _remote = RemoteDeckService.instance; @override void initState() { super.initState(); _load(); } void refresh() { _load(); } /// Published deck ids (community list id) that we already have in My decks: /// - copied_from_deck_id when we added from Community, or /// - server_deck_id when we own/synced that deck (e.g. our own published deck). Future> _localPublishedDeckIdsWeHave() async { await _storage.initialize(); final allDecks = await _storage.getAllDecks(); final ids = {}; for (final deck in allDecks) { final sync = _storage.getDeckSyncMetadataSync(deck.id); if (sync == null) continue; final serverId = sync['server_deck_id']?.toString().trim(); if (serverId != null && serverId.isNotEmpty) ids.add(serverId); final fromId = sync['copied_from_deck_id']?.toString().trim(); if (fromId != null && fromId.isNotEmpty) ids.add(fromId); } return ids; } Future _load() async { final user = ApiAuthService.instance.currentUser.value; if (user == null) { setState(() { _loading = false; _decks = []; _error = null; }); return; } setState(() { _loading = true; _error = null; }); try { await _storage.initialize(); if (!mounted) return; final list = await _remote.getPublishedDecks(); if (!mounted) return; final localCopyIds = await _localPublishedDeckIdsWeHave(); if (!mounted) return; final merged = list.map((d) { final publishedId = d.id.toString().trim(); final inMyDecks = publishedId.isNotEmpty && localCopyIds.contains(publishedId); return RemoteDeckListItem( id: d.id, title: d.title, description: d.description, questionCount: d.questionCount, userHasThis: inMyDecks, needsUpdate: d.needsUpdate, copiedFromDeckId: d.copiedFromDeckId, ownerDisplayName: d.ownerDisplayName, averageRating: d.averageRating, ratingCount: d.ratingCount, ); }).toList(); if (mounted) { setState(() { _decks = merged; _loading = false; _error = null; }); } } on RemoteDeckException catch (e) { if (mounted) { setState(() { _loading = false; _error = _apiErrorMessage(e); }); } } catch (e) { if (mounted) { setState(() { _loading = false; _error = connectionErrorMessage(e); }); } } } Future _addToMyDecks(RemoteDeckListItem item) async { if (ApiAuthService.instance.currentUser.value == null) { Navigator.pushNamed(context, Routes.login).then((_) => _load()); return; } try { final newId = await _remote.copyDeck(item.id); final deck = await _remote.getDeck(newId); final syncMetadata = { 'server_deck_id': newId, 'owner_id': ApiAuthService.instance.currentUser.value!.id, 'copied_from_deck_id': item.id, 'copied_from_version': null, 'published': false, 'needs_update': false, }; _storage.saveDeckSync(deck, syncMetadata: syncMetadata); if (mounted) { showTopSnackBar( context, message: 'Added "${deck.title}" to your decks', backgroundColor: Colors.green, ); _load(); } } on RemoteDeckException catch (e) { if (mounted) { showTopSnackBar( context, message: e.statusCode == 401 ? 'Session expired. Please log in again.' : 'Could not add deck.', backgroundColor: Colors.red, ); } } catch (e) { if (mounted) { showTopSnackBar( context, message: connectionErrorMessage(e), backgroundColor: Colors.red, ); } } } @override Widget build(BuildContext context) { final user = ApiAuthService.instance.currentUser.value; if (user == null) { final body = Center( child: Padding( padding: const EdgeInsets.all(24), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Text( 'Log in to browse and add community decks.', style: Theme.of(context).textTheme.bodyLarge, textAlign: TextAlign.center, ), const SizedBox(height: 24), FilledButton( onPressed: () => Navigator.pushNamed(context, Routes.login) .then((_) => _load()), child: const Text('Log in'), ), ], ), ), ); if (widget.showAppBar) { return Scaffold(appBar: AppBar(title: const Text('Community decks')), body: body); } return body; } final body = _loading ? const Center(child: CircularProgressIndicator()) : _error != null ? Center( child: Padding( padding: const EdgeInsets.all(24), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Text(_error!, textAlign: TextAlign.center), const SizedBox(height: 16), Row( mainAxisAlignment: MainAxisAlignment.center, children: [ FilledButton( onPressed: _load, child: const Text('Retry'), ), if (_error!.contains('Session expired')) ...[ const SizedBox(width: 12), FilledButton.tonal( onPressed: () async { await ApiAuthService.instance.logout(); if (!mounted) return; Navigator.pushNamed(context, Routes.login) .then((_) => _load()); }, child: const Text('Log in again'), ), ], ], ), ], ), ), ) : _decks.isEmpty ? Center( child: Text( 'No published decks yet.', style: Theme.of(context).textTheme.bodyLarge, ), ) : RefreshIndicator( onRefresh: _load, child: ListView.builder( padding: const EdgeInsets.all(16), itemCount: _decks.length, itemBuilder: (context, index) { final d = _decks[index]; return Card( margin: const EdgeInsets.only(bottom: 12), child: ListTile( title: Text(d.title), subtitle: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ if (d.description.isNotEmpty) Text( d.description, maxLines: 2, overflow: TextOverflow.ellipsis, ), const SizedBox(height: 4), Text( '${d.questionCount} questions' '${d.ownerDisplayName != null ? ' ยท ${d.ownerDisplayName}' : ''}', style: Theme.of(context).textTheme.bodySmall, ), ], ), isThreeLine: true, trailing: d.userHasThis ? const Chip( label: Text('In my decks'), padding: EdgeInsets.symmetric( horizontal: 8, vertical: 4, ), ) : FilledButton.tonal( onPressed: () => _addToMyDecks(d), child: const Text('Add'), ), ), ); }, ), ); if (widget.showAppBar) { return Scaffold(appBar: AppBar(title: const Text('Community decks')), body: body); } return body; } }