import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../app/error_message.dart'; import '../app/load_state.dart'; import '../models/api_key_info.dart'; import '../providers/providers.dart'; import '../widgets/load_error_view.dart'; class ApiKeysScreen extends ConsumerStatefulWidget { const ApiKeysScreen({super.key}); @override ConsumerState createState() => _ApiKeysScreenState(); } class _ApiKeysScreenState extends ConsumerState { final Set _busyKeys = {}; String? _lastCreatedKey; @override void initState() { super.initState(); _load(); } Future _load() async { await ref.read(apiKeysProvider.notifier).load(); } @override Widget build(BuildContext context) { final keysState = ref.watch(apiKeysProvider); final keys = keysState.data; return Scaffold( appBar: AppBar(title: const Text('API-КЛЮЧИ')), body: Column( children: [ if (_lastCreatedKey != null) _LastCreatedKeyBanner( keyValue: _lastCreatedKey!, onDismiss: () => setState(() => _lastCreatedKey = null), ), Expanded(child: _buildContent(keysState, keys)), ], ), floatingActionButton: FloatingActionButton( backgroundColor: Colors.deepOrange, onPressed: _showCreateDialog, child: const Icon(Icons.add), ), ); } Widget _buildContent( LoadState> keysState, List keys, ) { if ((keysState.isIdle || keysState.isLoading) && keys.isEmpty) { return const Center( child: CircularProgressIndicator(color: Colors.deepOrange), ); } if (keysState.hasError && keys.isEmpty) { return LoadErrorView( title: 'Не удалось загрузить API-ключи', message: keysState.errorMessage, icon: Icons.vpn_key, onRetry: _load, ); } if (keys.isEmpty) { return const Center( child: Text( 'Гостевых ключей пока нет', style: TextStyle(color: Colors.white54), ), ); } final hasStatusHeader = keysState.isLoading || keysState.hasError; final statusHeaderCount = hasStatusHeader ? 1 : 0; return RefreshIndicator( color: Colors.deepOrange, onRefresh: _load, child: ListView.builder( physics: const AlwaysScrollableScrollPhysics(), padding: const EdgeInsets.all(12), itemCount: keys.length + statusHeaderCount, itemBuilder: (context, index) { if (hasStatusHeader && index == 0) { if (keysState.isLoading) { return const Padding( padding: EdgeInsets.only(bottom: 12), child: LinearProgressIndicator(color: Colors.deepOrange), ); } return LoadErrorBanner( title: 'Не удалось обновить API-ключи', message: keysState.errorMessage, onRetry: _load, ); } final keyData = keys[index - statusHeaderCount]; final busy = _busyKeys.contains(keyData.key); return _ApiKeyCard( data: keyData, busy: busy, onRevoke: busy ? null : () => _revokeKey(keyData), onActivate: busy ? null : () => _activateKey(keyData), ); }, ), ); } Future _showCreateDialog() async { final formKey = GlobalKey(); final nameCtrl = TextEditingController(); var isAdmin = false; var isCreating = false; await showDialog( context: context, builder: (ctx) => StatefulBuilder( builder: (ctx, setDialogState) => AlertDialog( title: const Text('Новый API-ключ'), content: Form( key: formKey, autovalidateMode: AutovalidateMode.onUserInteraction, child: Column( mainAxisSize: MainAxisSize.min, children: [ TextFormField( controller: nameCtrl, decoration: const InputDecoration( labelText: 'Имя ключа', hintText: 'Например: Гость', ), autofocus: true, validator: (value) { final normalized = value?.trim() ?? ''; if (normalized.isEmpty) { return 'Укажите имя ключа'; } if (normalized.length < 2) { return 'Слишком короткое имя'; } return null; }, ), const SizedBox(height: 12), SwitchListTile( title: const Text('Дать права администратора'), subtitle: const Text( 'Используйте только для доверенных людей', ), value: isAdmin, activeThumbColor: Colors.deepOrange, onChanged: isCreating ? null : (value) => setDialogState(() => isAdmin = value), contentPadding: EdgeInsets.zero, ), ], ), ), actions: [ TextButton( onPressed: isCreating ? null : () => Navigator.of(ctx).pop(), child: const Text('Отмена'), ), ElevatedButton( style: ElevatedButton.styleFrom( backgroundColor: Colors.deepOrange, ), onPressed: isCreating ? null : () async { if (!formKey.currentState!.validate()) { return; } setDialogState(() => isCreating = true); try { final key = await ref .read(apiKeysProvider.notifier) .create(nameCtrl.text.trim(), isAdmin: isAdmin); if (!mounted) return; setState(() => _lastCreatedKey = key); if (ctx.mounted) { Navigator.of(ctx).pop(); } ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Ключ создан'), duration: Duration(seconds: 1), ), ); } catch (e) { if (!ctx.mounted) return; setDialogState(() => isCreating = false); ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text( 'Ошибка создания ключа: ${describeLoadError(e)}', ), ), ); } }, child: isCreating ? const SizedBox( width: 18, height: 18, child: CircularProgressIndicator( strokeWidth: 2, color: Colors.white, ), ) : const Text('Создать'), ), ], ), ), ); nameCtrl.dispose(); } Future _revokeKey(ApiKeyInfo data) async { final confirmed = await showDialog( context: context, builder: (ctx) => AlertDialog( title: const Text('Отозвать ключ?'), content: Text('Ключ "${data.name}" перестанет работать.'), actions: [ TextButton( onPressed: () => Navigator.of(ctx).pop(false), child: const Text('Отмена'), ), TextButton( onPressed: () => Navigator.of(ctx).pop(true), child: const Text( 'Отозвать', style: TextStyle(color: Colors.redAccent), ), ), ], ), ); if (confirmed != true) { return; } setState(() => _busyKeys.add(data.key)); try { await ref.read(apiKeysProvider.notifier).revoke(data.key); if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Ключ отозван'), duration: Duration(seconds: 1), ), ); } catch (e) { if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Ошибка отзыва ключа: ${describeLoadError(e)}')), ); } finally { if (mounted) { setState(() => _busyKeys.remove(data.key)); } } } Future _activateKey(ApiKeyInfo data) async { setState(() => _busyKeys.add(data.key)); try { await ref.read(apiKeysProvider.notifier).activate(data.key); if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Ключ снова активен'), duration: Duration(seconds: 1), ), ); } catch (e) { if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Ошибка активации ключа: ${describeLoadError(e)}'), ), ); } finally { if (mounted) { setState(() => _busyKeys.remove(data.key)); } } } } class _LastCreatedKeyBanner extends StatelessWidget { final String keyValue; final VoidCallback onDismiss; const _LastCreatedKeyBanner({ required this.keyValue, required this.onDismiss, }); @override Widget build(BuildContext context) { return Container( margin: const EdgeInsets.all(12), padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: Colors.deepOrange.withValues(alpha: 0.15), borderRadius: BorderRadius.circular(12), border: Border.all(color: Colors.deepOrange.withValues(alpha: 0.3)), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ const Expanded( child: Text( 'Новый ключ создан. Скопируйте его сейчас, потом приложение уже не покажет полный токен.', style: TextStyle( fontSize: 13, fontWeight: FontWeight.bold, color: Colors.deepOrange, ), ), ), IconButton( onPressed: onDismiss, icon: const Icon(Icons.close, size: 18), tooltip: 'Скрыть', ), ], ), const SizedBox(height: 8), Row( children: [ Expanded( child: Text( keyValue, style: const TextStyle( fontSize: 12, fontFamily: 'monospace', color: Colors.white70, ), maxLines: 3, overflow: TextOverflow.ellipsis, ), ), IconButton( icon: const Icon(Icons.copy, size: 20), onPressed: () { Clipboard.setData(ClipboardData(text: keyValue)); ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Ключ скопирован'), duration: Duration(seconds: 1), ), ); }, ), ], ), ], ), ); } } class _ApiKeyCard extends StatelessWidget { final ApiKeyInfo data; final bool busy; final VoidCallback? onRevoke; final VoidCallback? onActivate; const _ApiKeyCard({ required this.data, required this.busy, required this.onRevoke, required this.onActivate, }); @override Widget build(BuildContext context) { return Card( margin: const EdgeInsets.only(bottom: 8), child: ListTile( leading: Icon( Icons.vpn_key, color: data.isActive ? (data.isAdmin ? Colors.amber : Colors.deepOrange) : Colors.white24, ), title: Row( children: [ Expanded( child: Text( data.name, style: TextStyle( fontWeight: FontWeight.bold, color: data.isActive ? Colors.white : Colors.white38, ), ), ), if (data.isAdmin) ...[ const SizedBox(width: 8), Container( padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), decoration: BoxDecoration( color: Colors.amber.withValues(alpha: 0.2), borderRadius: BorderRadius.circular(4), ), child: const Text( 'admin', style: TextStyle(fontSize: 10, color: Colors.amber), ), ), ], if (!data.isActive) ...[ const SizedBox(width: 8), Container( padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), decoration: BoxDecoration( color: Colors.redAccent.withValues(alpha: 0.2), borderRadius: BorderRadius.circular(4), ), child: const Text( 'отозван', style: TextStyle(fontSize: 10, color: Colors.redAccent), ), ), ], ], ), subtitle: data.formattedCreatedAt.isNotEmpty ? Text( 'Создан: ${data.formattedCreatedAt}', style: const TextStyle(fontSize: 11, color: Colors.white30), ) : null, trailing: busy ? const SizedBox( width: 20, height: 20, child: CircularProgressIndicator( strokeWidth: 2, color: Colors.deepOrange, ), ) : IconButton( icon: Icon( data.isActive ? Icons.block : Icons.check_circle_outline, size: 20, color: data.isActive ? Colors.redAccent : Colors.green, ), tooltip: data.isActive ? 'Отозвать' : 'Активировать', onPressed: data.isActive ? onRevoke : onActivate, ), ), ); } }