import 'dart:convert'; import 'dart:ui'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import '../../../backend/backend.dart'; import 'providers_controller.dart'; class ProviderFormScreen extends ConsumerStatefulWidget { const ProviderFormScreen({this.initialConfig, super.key}); final ContentProviderConfig? initialConfig; @override ConsumerState createState() => _ProviderFormScreenState(); } class _ProviderFormScreenState extends ConsumerState { late final TextEditingController _name; late final TextEditingController _baseUrl; late final TextEditingController _priority; late final TextEditingController _timeout; late final TextEditingController _headers; late final TextEditingController _apiKey; late final TextEditingController _userId; late final TextEditingController _login; late String _apiType; late bool _enabled; bool _obscureApiKey = true; bool _isSaving = false; @override void initState() { super.initState(); final config = widget.initialConfig; _name = TextEditingController(text: config?.name); _baseUrl = TextEditingController(text: config?.baseUrl); _priority = TextEditingController(text: (config?.priority ?? 10).toString()); _timeout = TextEditingController(text: (config?.timeoutSeconds ?? 20).toString()); _headers = TextEditingController( text: jsonEncode(_visibleHeaders(config?.customHeaders ?? const {})), ); _apiKey = TextEditingController(text: config?.customHeaders['query.api_key']); _userId = TextEditingController(text: config?.customHeaders['query.user_id']); _login = TextEditingController(text: config?.customHeaders['query.login']); _apiType = config?.apiType ?? 'gelbooru'; _enabled = config?.enabled ?? true; } @override void dispose() { _name.dispose(); _baseUrl.dispose(); _priority.dispose(); _timeout.dispose(); _headers.dispose(); _apiKey.dispose(); _userId.dispose(); _login.dispose(); super.dispose(); } void _applyPreset({ required String name, required String baseUrl, required String apiType, }) { HapticFeedback.selectionClick(); setState(() { _name.text = name; _baseUrl.text = baseUrl; _apiType = apiType; }); } @override Widget build(BuildContext context) { final theme = Theme.of(context); final isDark = theme.brightness == Brightness.dark; final isRu = Localizations.maybeLocaleOf(context)?.languageCode == 'ru'; final isEdit = widget.initialConfig != null; final bottomInset = MediaQuery.paddingOf(context).bottom; return Scaffold( extendBodyBehindAppBar: true, appBar: PreferredSize( preferredSize: const Size.fromHeight(60), child: ClipRRect( child: BackdropFilter( filter: ImageFilter.blur(sigmaX: 18, sigmaY: 18), child: AppBar( backgroundColor: isDark ? theme.colorScheme.surface.withValues(alpha: 0.75) : Colors.white.withValues(alpha: 0.85), surfaceTintColor: Colors.transparent, elevation: 0, leading: IconButton( icon: const Icon(Icons.arrow_back_ios_new_rounded, size: 20), onPressed: () { if (Navigator.of(context).canPop()) { Navigator.of(context).pop(); } else { context.go('/providers'); } }, tooltip: isRu ? 'Назад' : 'Back', ), title: Text( isEdit ? (isRu ? 'Редактирование источника' : 'Edit Provider') : (isRu ? 'Новый источник' : 'New Provider'), style: theme.textTheme.titleMedium?.copyWith( fontWeight: FontWeight.w700, letterSpacing: -0.2, ), ), ), ), ), ), body: Align( alignment: Alignment.topCenter, child: ConstrainedBox( constraints: const BoxConstraints(maxWidth: 820), child: ListView( padding: EdgeInsets.fromLTRB( 16, MediaQuery.paddingOf(context).top + 68, 16, 140 + bottomInset, ), children: [ // Presets bar if creating a new provider if (!isEdit) ...[ Text( isRu ? 'Быстрые пресеты' : 'Quick Presets', style: theme.textTheme.labelMedium?.copyWith( fontWeight: FontWeight.w700, color: theme.colorScheme.onSurfaceVariant, letterSpacing: -0.1, ), ), const SizedBox(height: 8), SingleChildScrollView( scrollDirection: Axis.horizontal, child: Row( children: [ _PresetChip( name: 'Gelbooru', color: const Color(0xFF10B981), onTap: () => _applyPreset( name: 'Gelbooru', baseUrl: 'https://gelbooru.com', apiType: 'gelbooru', ), ), const SizedBox(width: 8), _PresetChip( name: 'Rule34', color: const Color(0xFF84CC16), onTap: () => _applyPreset( name: 'Rule34', baseUrl: 'https://api.rule34.xxx', apiType: 'rule34', ), ), const SizedBox(width: 8), _PresetChip( name: 'Danbooru', color: const Color(0xFF3B82F6), onTap: () => _applyPreset( name: 'Danbooru', baseUrl: 'https://danbooru.donmai.us', apiType: 'danbooru', ), ), const SizedBox(width: 8), _PresetChip( name: 'e621', color: const Color(0xFFF59E0B), onTap: () => _applyPreset( name: 'e621', baseUrl: 'https://e621.net', apiType: 'e621', ), ), const SizedBox(width: 8), _PresetChip( name: 'Yande.re', color: const Color(0xFF8B5CF6), onTap: () => _applyPreset( name: 'Yande.re', baseUrl: 'https://yande.re', apiType: 'moebooru', ), ), const SizedBox(width: 8), _PresetChip( name: 'Konachan', color: const Color(0xFFEC4899), onTap: () => _applyPreset( name: 'Konachan', baseUrl: 'https://konachan.net', apiType: 'moebooru', ), ), const SizedBox(width: 8), _PresetChip( name: 'Paheal', color: const Color(0xFFF97316), onTap: () => _applyPreset( name: 'Rule34 Paheal', baseUrl: 'https://rule34.paheal.net', apiType: 'paheal', ), ), ], ), ), const SizedBox(height: 16), ], // Section 1: General Parameters _FormLiquidCard( icon: Icons.tune_rounded, accentColor: const Color(0xFF6366F1), title: isRu ? 'Основные параметры' : 'General Configuration', subtitle: isRu ? 'Имя, адрес сервера и тип API движка' : 'Name, server endpoint and API engine', child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ _GlassTextField( controller: _name, label: isRu ? 'Название источника' : 'Provider Name', hint: 'e.g. Gelbooru, Rule34', icon: Icons.label_rounded, ), const SizedBox(height: 14), _GlassTextField( controller: _baseUrl, label: isRu ? 'Базовый URL (Base URL)' : 'Base URL', hint: 'https://example.com', icon: Icons.link_rounded, ), const SizedBox(height: 14), // Dropdown API Type _GlassDropdown( label: isRu ? 'Тип API движка' : 'API Engine Type', value: _apiType, onChanged: (val) { if (val != null) setState(() => _apiType = val); }, items: const [ DropdownMenuItem( value: 'gelbooru', child: Text('Gelbooru (compatible)'), ), DropdownMenuItem( value: 'rule34', child: Text('Rule34 (compatible)'), ), DropdownMenuItem( value: 'danbooru', child: Text('Danbooru (compatible)'), ), DropdownMenuItem( value: 'e621', child: Text('e621 / e926'), ), DropdownMenuItem( value: 'moebooru', child: Text('Moebooru (Yande.re, Konachan)'), ), DropdownMenuItem( value: 'paheal', child: Text('Rule34 Paheal HTML'), ), DropdownMenuItem( value: 'realbooru_html', child: Text('Realbooru HTML'), ), DropdownMenuItem( value: 'custom', child: Text('Custom REST API'), ), ], ), const SizedBox(height: 14), // Enabled Switch Tile Container( padding: const EdgeInsets.symmetric( horizontal: 14, vertical: 8), decoration: BoxDecoration( color: isDark ? Colors.black.withValues(alpha: 0.15) : Colors.white.withValues(alpha: 0.50), borderRadius: BorderRadius.circular(16), border: Border.all( color: isDark ? Colors.white.withValues(alpha: 0.08) : Colors.black.withValues(alpha: 0.05), ), ), child: Row( children: [ Icon( Icons.power_settings_new_rounded, size: 20, color: _enabled ? const Color(0xFF10B981) : theme.colorScheme.onSurfaceVariant, ), const SizedBox(width: 12), Expanded( child: Text( isRu ? 'Включить этот источник' : 'Enable provider', style: TextStyle( fontSize: 14, fontWeight: FontWeight.w600, color: theme.colorScheme.onSurface, ), ), ), Switch.adaptive( value: _enabled, onChanged: (val) { HapticFeedback.selectionClick(); setState(() => _enabled = val); }, activeThumbColor: const Color(0xFF10B981), activeTrackColor: const Color(0xFF10B981).withValues(alpha: 0.45), ), ], ), ), ], ), ), const SizedBox(height: 18), // Section 2: Priority & Timeout _FormLiquidCard( icon: Icons.speed_rounded, accentColor: const Color(0xFF10B981), title: isRu ? 'Приоритет и производительность' : 'Priority & Performance', subtitle: isRu ? 'Порядок выдачи постов и максимальное время ответа' : 'Dispatch priority and maximum network wait time', child: Row( children: [ Expanded( child: _GlassTextField( controller: _priority, label: isRu ? 'Приоритет (Priority)' : 'Priority', hint: '10', icon: Icons.trending_up_rounded, keyboardType: TextInputType.number, ), ), const SizedBox(width: 12), Expanded( child: _GlassTextField( controller: _timeout, label: isRu ? 'Таймаут (сек)' : 'Timeout (s)', hint: '20', icon: Icons.timer_outlined, keyboardType: TextInputType.number, ), ), ], ), ), const SizedBox(height: 18), // Section 3: Credentials & API Keys _FormLiquidCard( icon: Icons.vpn_key_rounded, accentColor: const Color(0xFFF59E0B), title: isRu ? 'Ключи доступа и аккаунт' : 'API Keys & Authentication', subtitle: isRu ? 'Необходимы для снятия ограничений на поиск и теги' : 'Required for unrestricted queries and higher limits', child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ // Info Notice Container( margin: const EdgeInsets.only(bottom: 14), padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: const Color(0xFFF59E0B) .withValues(alpha: isDark ? 0.14 : 0.08), borderRadius: BorderRadius.circular(14), border: Border.all( color: const Color(0xFFF59E0B) .withValues(alpha: isDark ? 0.35 : 0.22), ), ), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Icon( Icons.info_outline_rounded, size: 18, color: Color(0xFFF59E0B), ), const SizedBox(width: 10), Expanded( child: Text( isRu ? 'Для Gelbooru и Rule34: User ID и API Key можно найти в настройках профиля (Account -> Options).' : 'For Gelbooru & Rule34: Find your User ID and API Key in Account -> Options.', style: TextStyle( fontSize: 12, color: theme.colorScheme.onSurfaceVariant, height: 1.35, ), ), ), ], ), ), _GlassTextField( controller: _apiKey, label: 'API Key', hint: 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx', icon: Icons.key_rounded, obscureText: _obscureApiKey, suffixIcon: IconButton( icon: Icon( _obscureApiKey ? Icons.visibility_outlined : Icons.visibility_off_outlined, size: 18, ), onPressed: () => setState(() => _obscureApiKey = !_obscureApiKey), ), ), const SizedBox(height: 14), _GlassTextField( controller: _userId, label: 'User ID', hint: '123456', icon: Icons.badge_outlined, keyboardType: TextInputType.number, ), const SizedBox(height: 14), _GlassTextField( controller: _login, label: isRu ? 'Логин (Login)' : 'Login Username', hint: 'username', icon: Icons.account_circle_outlined, ), ], ), ), const SizedBox(height: 18), // Section 4: Custom Headers JSON _FormLiquidCard( icon: Icons.terminal_rounded, accentColor: const Color(0xFF06B6D4), title: isRu ? 'Заголовки JSON' : 'Custom Headers (JSON)', subtitle: isRu ? 'Дополнительные HTTP заголовки в формате JSON' : 'Optional custom request headers in JSON map format', child: _GlassTextField( controller: _headers, label: isRu ? 'Пользовательские заголовки' : 'Headers JSON', hint: '{"User-Agent": "Prisma/3.0"}', icon: Icons.code_rounded, minLines: 2, maxLines: 5, isMonospace: true, ), ), const SizedBox(height: 24), // Save Action Button Container( height: 52, decoration: BoxDecoration( gradient: const LinearGradient( colors: [Color(0xFF6366F1), Color(0xFF8B5CF6)], begin: Alignment.topLeft, end: Alignment.bottomRight, ), borderRadius: BorderRadius.circular(18), boxShadow: [ BoxShadow( color: const Color(0xFF6366F1).withValues(alpha: 0.38), blurRadius: 16, offset: const Offset(0, 4), ), ], ), child: Material( color: Colors.transparent, borderRadius: BorderRadius.circular(18), child: InkWell( borderRadius: BorderRadius.circular(18), onTap: _isSaving ? null : _save, child: Center( child: _isSaving ? const SizedBox( width: 22, height: 22, child: CircularProgressIndicator( strokeWidth: 2.5, color: Colors.white, ), ) : Row( mainAxisSize: MainAxisSize.min, children: [ const Icon( Icons.check_rounded, size: 20, color: Colors.white, ), const SizedBox(width: 8), Text( isRu ? 'Сохранить провайдер' : 'Save Provider', style: const TextStyle( fontSize: 16, fontWeight: FontWeight.w700, color: Colors.white, letterSpacing: -0.2, ), ), ], ), ), ), ), ), const SizedBox(height: 12), // Cancel Button Center( child: TextButton( onPressed: () { if (Navigator.of(context).canPop()) { Navigator.of(context).pop(); } else { context.go('/providers'); } }, child: Text( isRu ? 'Отмена' : 'Cancel', style: TextStyle( fontSize: 14, fontWeight: FontWeight.w600, color: theme.colorScheme.onSurfaceVariant, ), ), ), ), ], ), ), ), ); } Future _save() async { final nameText = _name.text.trim(); if (nameText.isEmpty) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Укажите название провайдера')), ); return; } setState(() => _isSaving = true); HapticFeedback.lightImpact(); try { final now = DateTime.now(); final id = widget.initialConfig?.id ?? nameText.toLowerCase().replaceAll(RegExp(r'[^a-z0-9]+'), '_'); Map headersJson = {}; try { if (_headers.text.trim().isNotEmpty) { headersJson = jsonDecode(_headers.text.trim()) as Map; } } catch (_) { // Fallback to empty if parse error } final customHeaders = Map.from( headersJson.map((k, v) => MapEntry(k.toString(), v.toString())), ); void putQuery(String key, String value) { final trimmed = value.trim(); if (trimmed.isNotEmpty) customHeaders['query.$key'] = trimmed; } putQuery('api_key', _apiKey.text); putQuery('user_id', _userId.text); putQuery('login', _login.text); final config = ContentProviderConfig( id: id, name: nameText, baseUrl: _baseUrl.text.trim(), apiType: _apiType, enabled: _enabled, priority: int.tryParse(_priority.text) ?? 10, timeoutSeconds: int.tryParse(_timeout.text) ?? 20, customHeaders: customHeaders, createdAt: widget.initialConfig?.createdAt ?? now, updatedAt: now, ); await ref.read(providersControllerProvider.notifier).save(config); if (mounted) { if (Navigator.of(context).canPop()) { Navigator.of(context).pop(); } else { context.go('/providers'); } } } finally { if (mounted) setState(() => _isSaving = false); } } Map _visibleHeaders(Map headers) { return Map.from(headers) ..removeWhere((key, _) => key.startsWith('query.')); } } class _PresetChip extends StatelessWidget { const _PresetChip({ required this.name, required this.color, required this.onTap, }); final String name; final Color color; final VoidCallback onTap; @override Widget build(BuildContext context) { final isDark = Theme.of(context).brightness == Brightness.dark; return Material( color: Colors.transparent, borderRadius: BorderRadius.circular(14), child: InkWell( borderRadius: BorderRadius.circular(14), onTap: onTap, child: Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 7), decoration: BoxDecoration( color: color.withValues(alpha: isDark ? 0.16 : 0.10), borderRadius: BorderRadius.circular(14), border: Border.all( color: color.withValues(alpha: isDark ? 0.40 : 0.28), width: 1, ), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Container( width: 6, height: 6, decoration: BoxDecoration( color: color, shape: BoxShape.circle, ), ), const SizedBox(width: 6), Text( name, style: TextStyle( fontSize: 12, fontWeight: FontWeight.w700, color: color, ), ), ], ), ), ), ); } } class _FormLiquidCard extends StatelessWidget { const _FormLiquidCard({ required this.icon, required this.accentColor, required this.title, required this.subtitle, required this.child, }); final IconData icon; final Color accentColor; final String title; final String subtitle; final Widget child; @override Widget build(BuildContext context) { final theme = Theme.of(context); final isDark = theme.brightness == Brightness.dark; return Container( decoration: BoxDecoration( borderRadius: BorderRadius.circular(24), boxShadow: [ BoxShadow( color: isDark ? Colors.black.withValues(alpha: 0.30) : accentColor.withValues(alpha: 0.08), blurRadius: 18, offset: const Offset(0, 6), spreadRadius: -2, ), ], ), child: ClipRRect( borderRadius: BorderRadius.circular(24), child: Container( padding: const EdgeInsets.all(18), decoration: BoxDecoration( borderRadius: BorderRadius.circular(24), gradient: LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, colors: isDark ? [ theme.colorScheme.surfaceContainerHigh .withValues(alpha: 0.65), theme.colorScheme.surfaceContainerLow .withValues(alpha: 0.40), ] : [ Colors.white.withValues(alpha: 0.90), Colors.white.withValues(alpha: 0.75), ], ), border: Border.all( color: isDark ? Colors.white.withValues(alpha: 0.14) : Colors.white.withValues(alpha: 0.85), width: 1.1, ), ), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ // Header Row( children: [ Container( width: 36, height: 36, decoration: BoxDecoration( gradient: LinearGradient( colors: [ accentColor, accentColor.withValues(alpha: 0.80), ], ), borderRadius: BorderRadius.circular(11), boxShadow: [ BoxShadow( color: accentColor.withValues(alpha: 0.32), blurRadius: 8, offset: const Offset(0, 2), ), ], ), child: Center( child: Icon(icon, size: 18, color: Colors.white), ), ), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( title, style: theme.textTheme.titleSmall?.copyWith( fontWeight: FontWeight.w700, letterSpacing: -0.2, ), ), const SizedBox(height: 2), Text( subtitle, style: theme.textTheme.bodySmall?.copyWith( color: theme.colorScheme.onSurfaceVariant, ), ), ], ), ), ], ), const SizedBox(height: 16), child, ], ), ), ), ); } } class _GlassTextField extends StatelessWidget { const _GlassTextField({ required this.controller, required this.label, required this.hint, required this.icon, this.keyboardType, this.obscureText = false, this.suffixIcon, this.minLines = 1, this.maxLines = 1, this.isMonospace = false, }); final TextEditingController controller; final String label; final String hint; final IconData icon; final TextInputType? keyboardType; final bool obscureText; final Widget? suffixIcon; final int minLines; final int maxLines; final bool isMonospace; @override Widget build(BuildContext context) { final theme = Theme.of(context); final isDark = theme.brightness == Brightness.dark; return TextField( controller: controller, keyboardType: keyboardType, obscureText: obscureText, minLines: minLines, maxLines: maxLines, style: TextStyle( fontSize: 14, fontFamily: isMonospace ? 'monospace' : null, ), decoration: InputDecoration( labelText: label, hintText: hint, hintStyle: TextStyle( fontSize: 13, color: theme.colorScheme.onSurfaceVariant.withValues(alpha: 0.5), ), prefixIcon: Icon(icon, size: 18), suffixIcon: suffixIcon, filled: true, fillColor: isDark ? Colors.black.withValues(alpha: 0.22) : theme.colorScheme.surfaceContainerHighest.withValues(alpha: 0.45), contentPadding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12), border: OutlineInputBorder( borderRadius: BorderRadius.circular(16), borderSide: BorderSide( color: isDark ? Colors.white.withValues(alpha: 0.10) : Colors.black.withValues(alpha: 0.08), ), ), enabledBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(16), borderSide: BorderSide( color: isDark ? Colors.white.withValues(alpha: 0.10) : Colors.black.withValues(alpha: 0.08), ), ), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(16), borderSide: BorderSide( color: theme.colorScheme.primary, width: 1.4, ), ), ), ); } } class _GlassDropdown extends StatelessWidget { const _GlassDropdown({ required this.label, required this.value, required this.items, required this.onChanged, }); final String label; final String value; final List> items; final ValueChanged onChanged; @override Widget build(BuildContext context) { final theme = Theme.of(context); final isDark = theme.brightness == Brightness.dark; return DropdownButtonFormField( isExpanded: true, initialValue: value, items: items, onChanged: onChanged, dropdownColor: isDark ? theme.colorScheme.surfaceContainerHighest : Colors.white, borderRadius: BorderRadius.circular(16), decoration: InputDecoration( labelText: label, prefixIcon: const Icon(Icons.hub_outlined, size: 18), filled: true, fillColor: isDark ? Colors.black.withValues(alpha: 0.22) : theme.colorScheme.surfaceContainerHighest.withValues(alpha: 0.45), contentPadding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12), border: OutlineInputBorder( borderRadius: BorderRadius.circular(16), borderSide: BorderSide( color: isDark ? Colors.white.withValues(alpha: 0.10) : Colors.black.withValues(alpha: 0.08), ), ), enabledBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(16), borderSide: BorderSide( color: isDark ? Colors.white.withValues(alpha: 0.10) : Colors.black.withValues(alpha: 0.08), ), ), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(16), borderSide: BorderSide( color: theme.colorScheme.primary, width: 1.4, ), ), ), ); } }