worldhopper/lib/screens/servers/server_list_screen.dart
Felipe M. 7d5c854dc9 feat: i18n (#2)
Reviewed-on: #2
Co-authored-by: Felipe M. <me@fmartingr.com>
Co-committed-by: Felipe M. <me@fmartingr.com>
2026-02-12 22:41:22 +01:00

172 lines
5.9 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import 'package:worldhopper/l10n/app_localizations.dart';
import 'package:worldhopper/config/constants.dart';
import 'package:worldhopper/helpers/snackbar_helper.dart';
import 'package:worldhopper/providers/server_provider.dart';
import 'package:worldhopper/providers/connectivity_provider.dart';
import 'package:worldhopper/widgets/server_card.dart';
import 'package:worldhopper/widgets/worldhopper_app_bar.dart';
/// Screen displaying the list of configured OPDS servers
class ServerListScreen extends ConsumerWidget {
const ServerListScreen({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final l10n = AppLocalizations.of(context);
final serversAsync = ref.watch(serverListProvider);
return Scaffold(
appBar: WorldhopperAppBar(
title: Text(l10n.serverListTitle),
),
body: serversAsync.when(
data: (servers) {
if (servers.isEmpty) {
return _buildEmptyState(context, l10n);
}
return RefreshIndicator(
onRefresh: () async {
ref.invalidate(serverListProvider);
},
child: ListView.builder(
padding: const EdgeInsets.all(16),
itemCount: servers.length,
itemBuilder: (context, index) {
final server = servers[index];
return ServerCard(
server: server,
onTap: () {
// Check connectivity before browsing server
final connectivityState =
ref.read(connectivityStateProvider);
final isOnline = connectivityState.whenOrNull(
data: (online) => online,
) ??
true;
if (!isOnline) {
// Block navigation when offline
context.showInfoSnackBar(
l10n.serverListOffline,
duration: const Duration(seconds: 3),
);
return;
}
// Navigate to library browser
context.push('/browse/${server.id}');
},
onEdit: () {
context.push(
'${AppConstants.routeEditServer}/${server.id}',
);
},
onDelete: () async {
final confirmed =
await _confirmDelete(context, l10n, server.name);
if (confirmed) {
await ref
.read(serverNotifierProvider.notifier)
.deleteServer(server.id);
if (context.mounted) {
context
.showInfoSnackBar(l10n.serverDeleted(server.name));
}
}
},
);
},
),
);
},
loading: () => const Center(child: CircularProgressIndicator()),
error: (error, stack) => Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.error_outline, size: 48, color: Colors.red),
const SizedBox(height: 16),
Text(l10n.serverListErrorLoading(error.toString())),
const SizedBox(height: 16),
ElevatedButton(
onPressed: () => ref.invalidate(serverListProvider),
child: Text(l10n.serverListRetry),
),
],
),
),
),
floatingActionButton: FloatingActionButton.extended(
onPressed: () {
context.push(AppConstants.routeAddServer);
},
icon: const Icon(Icons.add),
label: Text(l10n.serverListAddServer),
),
);
}
Widget _buildEmptyState(BuildContext context, AppLocalizations l10n) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.cloud_off,
size: 100,
color:
Theme.of(context).colorScheme.secondary.withValues(alpha: 0.5),
),
const SizedBox(height: 24),
Text(
l10n.serverListEmpty,
style: Theme.of(context).textTheme.headlineSmall,
),
const SizedBox(height: 8),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 32),
child: Text(
l10n.serverListEmptySubtitle,
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Theme.of(context)
.colorScheme
.onSurface
.withValues(alpha: 0.7),
),
),
),
],
),
);
}
Future<bool> _confirmDelete(
BuildContext context, AppLocalizations l10n, String serverName) async {
final result = await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: Text(l10n.serverDeleteTitle),
content: Text(l10n.serverDeleteConfirmation(serverName)),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(false),
child: Text(l10n.commonCancel),
),
FilledButton(
onPressed: () => Navigator.of(context).pop(true),
style: FilledButton.styleFrom(
backgroundColor: Theme.of(context).colorScheme.error,
),
child: Text(l10n.commonDelete),
),
],
),
);
return result ?? false;
}
}