import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import '../models/interpreter_persona.dart'; import '../services/settings_repository.dart'; import '../services/true_random_service.dart'; import '../widgets/gradient_star_badge.dart'; class SettingsScreen extends StatelessWidget { const SettingsScreen({super.key}); @override Widget build(BuildContext context) { final theme = Theme.of(context); final settings = SettingsRepository(); return Scaffold( appBar: AppBar( title: const Row( children: [ GradientStar(size: 24), SizedBox(width: 10), Text( 'Настройки', style: TextStyle(fontWeight: FontWeight.w900), ), ], ), ), body: ListenableBuilder( listenable: settings, builder: (context, _) { final selected = settings.personaId; return ListView( padding: const EdgeInsets.fromLTRB(16, 8, 16, 24), children: [ Text( 'Голос толкователя', style: theme.textTheme.titleMedium?.copyWith( fontWeight: FontWeight.w900, color: const Color(0xFFFFD54F), letterSpacing: 0.3, ), ), const SizedBox(height: 6), Text( 'От этого зависит, как ИИ свяжет выпавшие знаки с вопросом. Базовый голос — прямой, без тумана.', style: theme.textTheme.bodyMedium?.copyWith( height: 1.35, color: theme.colorScheme.onSurface.withValues(alpha: 0.78), ), ), const SizedBox(height: 14), for (final persona in InterpreterPersona.all) ...[ _PersonaCard( persona: persona, selected: persona.id == selected, onTap: () { HapticFeedback.selectionClick(); settings.setPersonaId(persona.id); }, ), const SizedBox(height: 10), ], const SizedBox(height: 18), const _RandomOrgKeySection(), ], ); }, ), ); } } class _PersonaCard extends StatelessWidget { final InterpreterPersona persona; final bool selected; final VoidCallback onTap; const _PersonaCard({ required this.persona, required this.selected, required this.onTap, }); @override Widget build(BuildContext context) { final theme = Theme.of(context); final isDark = theme.brightness == Brightness.dark; return Material( color: Colors.transparent, child: InkWell( onTap: onTap, borderRadius: BorderRadius.circular(18), child: AnimatedContainer( duration: const Duration(milliseconds: 180), padding: const EdgeInsets.all(14), decoration: BoxDecoration( color: isDark ? const Color(0xFF17192C) : theme.colorScheme.surfaceContainerHigh, borderRadius: BorderRadius.circular(18), border: Border.all( color: selected ? const Color(0xFFFFD54F) : const Color(0xFFFFD54F).withValues(alpha: 0.22), width: selected ? 1.6 : 1.0, ), boxShadow: selected ? [ BoxShadow( color: const Color(0xFFFFD54F).withValues(alpha: 0.14), blurRadius: 12, ), ] : null, ), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( width: 42, height: 42, alignment: Alignment.center, decoration: BoxDecoration( color: const Color(0xFF2A2D4A), borderRadius: BorderRadius.circular(12), ), child: Icon(persona.icon, color: const Color(0xFFFFD54F), size: 22), ), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Expanded( child: Text( persona.title, style: const TextStyle( fontWeight: FontWeight.w900, fontSize: 15.5, color: Color(0xFFFFF9C4), ), ), ), Icon( selected ? Icons.radio_button_checked_rounded : Icons.radio_button_off_rounded, size: 20, color: selected ? const Color(0xFFFFD54F) : theme.colorScheme.outline, ), ], ), const SizedBox(height: 4), Text( persona.subtitle, style: TextStyle( fontSize: 13, height: 1.35, color: theme.colorScheme.onSurface.withValues(alpha: 0.78), ), ), ], ), ), ], ), ), ), ); } } class _RandomOrgKeySection extends StatefulWidget { const _RandomOrgKeySection(); @override State<_RandomOrgKeySection> createState() => _RandomOrgKeySectionState(); } class _RandomOrgKeySectionState extends State<_RandomOrgKeySection> { late final TextEditingController _controller; bool _obscure = true; bool _saving = false; @override void initState() { super.initState(); _controller = TextEditingController( text: SettingsRepository().randomOrgApiKey, ); } @override void dispose() { _controller.dispose(); super.dispose(); } Future _save() async { setState(() => _saving = true); await SettingsRepository().setRandomOrgApiKey(_controller.text); TrueRandomService.instance.invalidatePool(); TrueRandomService.instance.prefetch(); if (!mounted) return; setState(() => _saving = false); HapticFeedback.mediumImpact(); ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text( SettingsRepository().hasRandomOrgApiKey ? 'Ключ RANDOM.ORG сохранён. Броски идут через JSON-RPC.' : 'Ключ снят. Броски пойдут через бесплатный HTTP или локальный жребий.', ), behavior: SnackBarBehavior.floating, ), ); } @override Widget build(BuildContext context) { final theme = Theme.of(context); final settings = SettingsRepository(); return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'RANDOM.ORG', style: theme.textTheme.titleMedium?.copyWith( fontWeight: FontWeight.w900, color: const Color(0xFFFFD54F), letterSpacing: 0.3, ), ), const SizedBox(height: 6), Text( 'В запрос уходит UUID apiKey, не hashed key с дашборда. ' 'Developer-ключ бесплатный, 1000 запросов/день.\n' 'Матрица 1000 слов пересобирается раз в сутки (полночь UTC) тем же API: ' 'перестановка 0–999 без повторов.', style: theme.textTheme.bodyMedium?.copyWith( height: 1.35, color: theme.colorScheme.onSurface.withValues(alpha: 0.78), ), ), const SizedBox(height: 12), TextField( controller: _controller, obscureText: _obscure, autocorrect: false, enableSuggestions: false, keyboardType: TextInputType.visiblePassword, decoration: InputDecoration( labelText: 'API key (UUID)', hintText: 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx', helperText: settings.hasRandomOrgApiKey ? 'Сейчас: ${settings.randomOrgApiKeyMasked}' : 'Пусто — запасной HTTP без ключа', filled: true, fillColor: const Color(0xFF17192C), suffixIcon: IconButton( tooltip: _obscure ? 'Показать' : 'Скрыть', icon: Icon( _obscure ? Icons.visibility_outlined : Icons.visibility_off_outlined, color: const Color(0xFFFFD54F), ), onPressed: () => setState(() => _obscure = !_obscure), ), border: OutlineInputBorder(borderRadius: BorderRadius.circular(16)), enabledBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(16), borderSide: BorderSide( color: const Color(0xFFFFD54F).withValues(alpha: 0.25), ), ), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(16), borderSide: const BorderSide(color: Color(0xFFFFD54F), width: 1.5), ), ), ), const SizedBox(height: 10), SizedBox( width: double.infinity, child: FilledButton.icon( onPressed: _saving ? null : _save, icon: _saving ? const SizedBox( width: 16, height: 16, child: CircularProgressIndicator(strokeWidth: 2), ) : const Icon(Icons.save_outlined), label: Text(_saving ? 'Сохранение…' : 'Сохранить ключ'), style: FilledButton.styleFrom( backgroundColor: const Color(0xFF2A2D4A), foregroundColor: const Color(0xFFFFF9C4), padding: const EdgeInsets.symmetric(vertical: 14), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(16), side: const BorderSide(color: Color(0xFFFFD54F), width: 1.2), ), ), ), ), ], ); } }