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'; /// Экран управления гостевыми API-ключами. /// Доступен только администраторам. class ApiKeysScreen extends ConsumerStatefulWidget { const ApiKeysScreen({super.key}); @override ConsumerState createState() => _ApiKeysScreenState(); } class _ApiKeysScreenState extends ConsumerState { 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!), Expanded(child: _buildContent(keysState, keys)), ], ), floatingActionButton: FloatingActionButton( backgroundColor: Colors.deepOrange, onPressed: () => _showCreateDialog(context), 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 keyIndex = index - statusHeaderCount; final key = keys[keyIndex]; return _ApiKeyCard( data: key, onRevoke: () => _revokeKey(key), onActivate: () => _activateKey(key), ); }, ), ); } void _showCreateDialog(BuildContext context) { final nameCtrl = TextEditingController(); bool isAdmin = false; showDialog( context: context, builder: (ctx) => StatefulBuilder( builder: (ctx, setDialogState) => AlertDialog( title: const Text('Новый API-ключ'), content: Column( mainAxisSize: MainAxisSize.min, children: [ TextField( controller: nameCtrl, decoration: const InputDecoration( labelText: 'Имя ключа', hintText: 'Например: "Гость"', ), autofocus: true, ), const SizedBox(height: 12), SwitchListTile( title: const Text('Администратор'), value: isAdmin, activeThumbColor: Colors.deepOrange, onChanged: (v) => setDialogState(() => isAdmin = v), contentPadding: EdgeInsets.zero, ), ], ), actions: [ TextButton( onPressed: () => Navigator.of(ctx).pop(), child: const Text('Отмена'), ), ElevatedButton( style: ElevatedButton.styleFrom( backgroundColor: Colors.deepOrange, ), onPressed: () async { final name = nameCtrl.text.trim(); if (name.isEmpty) return; Navigator.of(ctx).pop(); final messenger = ScaffoldMessenger.of(context); try { final key = await ref .read(apiKeysProvider.notifier) .create(name, isAdmin: isAdmin); if (!mounted) return; setState(() => _lastCreatedKey = key); } catch (e) { if (!mounted) return; messenger.showSnackBar( SnackBar( content: Text( 'Ошибка создания ключа: ${describeLoadError(e)}', ), ), ); } }, child: const Text('Создать'), ), ], ), ), ); } 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) { try { await ref.read(apiKeysProvider.notifier).revoke(data.key); } catch (e) { if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Ошибка отзыва ключа: ${describeLoadError(e)}'), ), ); } } } Future _activateKey(ApiKeyInfo data) async { try { await ref.read(apiKeysProvider.notifier).activate(data.key); if (mounted) { 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)}'), ), ); } } } class _LastCreatedKeyBanner extends StatelessWidget { final String keyValue; const _LastCreatedKeyBanner({required this.keyValue}); @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: [ const Text( 'Новый ключ создан! Скопируйте его сейчас:', style: TextStyle( fontSize: 13, fontWeight: FontWeight.bold, color: Colors.deepOrange, ), ), const SizedBox(height: 8), Row( children: [ Expanded( child: Text( keyValue, style: const TextStyle( fontSize: 12, fontFamily: 'monospace', color: Colors.white70, ), maxLines: 2, 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), ), ); }, ), ], ), ], ), ); } } /// Карточка одного API-ключа class _ApiKeyCard extends StatelessWidget { final ApiKeyInfo data; final VoidCallback onRevoke; final VoidCallback onActivate; const _ApiKeyCard({ required this.data, 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: [ 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: data.isActive ? IconButton( icon: const Icon( Icons.block, size: 20, color: Colors.redAccent, ), tooltip: 'Отозвать', onPressed: onRevoke, ) : IconButton( icon: const Icon( Icons.check_circle_outline, size: 20, color: Colors.green, ), tooltip: 'Активировать', onPressed: onActivate, ), ), ); } }