Files
999/lib/screens/settings_screen.dart
T

928 lines
36 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import 'dart:convert';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import '../models/interpreter_persona.dart';
import '../services/settings_repository.dart';
import '../services/words_repository.dart';
import '../services/true_random_service.dart';
import '../widgets/gradient_star_badge.dart';
import 'manual_dive_screen.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: 22),
const _ManualDiveEntry(),
const SizedBox(height: 22),
const _SymbolDescriptionsBackupSection(),
const SizedBox(height: 24),
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<void> _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),
),
),
),
),
],
);
}
}
class _ManualDiveEntry extends StatelessWidget {
const _ManualDiveEntry();
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Material(
color: Colors.transparent,
child: InkWell(
borderRadius: BorderRadius.circular(18),
onTap: () {
HapticFeedback.selectionClick();
Navigator.of(context).push(
MaterialPageRoute(builder: (_) => const ManualDiveScreen()),
);
},
child: Ink(
padding: const EdgeInsets.all(14),
decoration: BoxDecoration(
color: const Color(0xFF17192C),
borderRadius: BorderRadius.circular(18),
border: Border.all(color: const Color(0xFFFFD54F).withValues(alpha: 0.45)),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Ручной обогатитель (Паспорт 999)',
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w900,
color: const Color(0xFFFFD54F),
),
),
const SizedBox(height: 6),
Text(
'Система окошек по 6 разделам и 78 предпромптов: копируй строки, вставляй ответы или разбирай Markdown целиком.',
style: theme.textTheme.bodyMedium?.copyWith(height: 1.35),
),
],
),
),
),
);
}
}
class _SymbolDescriptionsBackupSection extends StatefulWidget {
const _SymbolDescriptionsBackupSection();
@override
State<_SymbolDescriptionsBackupSection> createState() =>
_SymbolDescriptionsBackupSectionState();
}
class _SymbolDescriptionsBackupSectionState
extends State<_SymbolDescriptionsBackupSection> {
final _repo = WordsRepository();
Future<void> _exportCustomEdits() async {
final editsCount = _repo.customEditsCount;
final jsonStr = _repo.exportCustomEditsJson();
final defaultFileName = 'oracle_999_custom_symbols_${WordsRepository.utcDateStamp()}.json';
_showExportDialog(
title: 'Экспорт новых описаний',
subtitle: editsCount > 0
? 'Выгрузка $editsCount отредактированных символов со всеми статьями'
: 'Пользовательских правок пока нет, файл содержит структуру выгрузки',
fileName: defaultFileName,
content: jsonStr,
itemCount: editsCount,
);
}
Future<void> _exportFullLexicon() async {
final jsonStr = _repo.exportFullLexiconJson();
final defaultFileName = 'oracle_999_full_canon_1000_${WordsRepository.utcDateStamp()}.json';
_showExportDialog(
title: 'Экспорт полной базы (1000 символов)',
subtitle: 'Полный корпус всех 1000 знаков Оракула с каноном, статьями и формулами',
fileName: defaultFileName,
content: jsonStr,
itemCount: 1000,
);
}
Future<void> _showExportDialog({
required String title,
required String subtitle,
required String fileName,
required String content,
required int itemCount,
}) async {
HapticFeedback.lightImpact();
final bytes = utf8.encode(content).length;
final sizeKb = (bytes / 1024).toStringAsFixed(1);
showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: const Color(0xFF151829),
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(24)),
),
builder: (context) {
return Padding(
padding: EdgeInsets.fromLTRB(
20,
16,
20,
MediaQuery.of(context).viewInsets.bottom + 28,
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Center(
child: Container(
width: 44,
height: 4,
decoration: BoxDecoration(
color: const Color(0xFFFFD54F).withValues(alpha: 0.4),
borderRadius: BorderRadius.circular(2),
),
),
),
const SizedBox(height: 16),
Row(
children: [
const Icon(Icons.file_download_outlined, color: Color(0xFFFFD54F), size: 26),
const SizedBox(width: 10),
Expanded(
child: Text(
title,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.w900,
color: Color(0xFFFFF9C4),
),
),
),
],
),
const SizedBox(height: 6),
Text(
subtitle,
style: const TextStyle(fontSize: 13, color: Color(0xFF8C93B8), height: 1.35),
),
const SizedBox(height: 16),
Container(
padding: const EdgeInsets.all(14),
decoration: BoxDecoration(
color: const Color(0xFF0F111E),
borderRadius: BorderRadius.circular(14),
border: Border.all(color: const Color(0xFF2A2D4A)),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text('Имя файла:', style: TextStyle(color: Color(0xFF8C93B8), fontSize: 12)),
Text(fileName, style: const TextStyle(color: Color(0xFFFFD54F), fontWeight: FontWeight.bold, fontSize: 12)),
],
),
const SizedBox(height: 6),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text('Объём данных:', style: TextStyle(color: Color(0xFF8C93B8), fontSize: 12)),
Text('$itemCount знаков • $sizeKb КБ', style: const TextStyle(color: Colors.white, fontSize: 12)),
],
),
],
),
),
const SizedBox(height: 18),
FilledButton.icon(
icon: const Icon(Icons.copy_rounded, color: Colors.black),
label: const Text(
'Скопировать весь JSON в буфер',
style: TextStyle(fontWeight: FontWeight.bold, color: Colors.black),
),
style: FilledButton.styleFrom(
backgroundColor: const Color(0xFFFFD54F),
padding: const EdgeInsets.symmetric(vertical: 14),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)),
),
onPressed: () {
Clipboard.setData(ClipboardData(text: content));
HapticFeedback.mediumImpact();
Navigator.pop(context);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Row(
children: [
const Icon(Icons.check_circle_rounded, color: Color(0xFFFFD54F), size: 20),
const SizedBox(width: 10),
Expanded(
child: Text(
'Файл ($sizeKb КБ, $itemCount знаков) скопирован в буфер обмена! Перенесите его в новую версию через "Импорт".',
style: const TextStyle(fontWeight: FontWeight.w600),
),
),
],
),
duration: const Duration(seconds: 4),
behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
),
);
},
),
const SizedBox(height: 10),
OutlinedButton.icon(
icon: const Icon(Icons.save_alt_rounded, color: Color(0xFFFFD54F)),
label: const Text(
'Сохранить в папку Загрузки (Download)',
style: TextStyle(fontWeight: FontWeight.bold, color: Color(0xFFFFD54F)),
),
style: OutlinedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 14),
side: const BorderSide(color: Color(0xFFFFD54F), width: 1.2),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)),
),
onPressed: () async {
final savedPath = await _saveContentToDownloads(fileName, content);
if (!context.mounted) return;
Navigator.pop(context);
HapticFeedback.mediumImpact();
if (savedPath != null) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Файл успешно сохранён: $savedPath'),
backgroundColor: const Color(0xFF1E2238),
duration: const Duration(seconds: 4),
behavior: SnackBarBehavior.floating,
),
);
} else {
Clipboard.setData(ClipboardData(text: content));
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Папка Download недоступна напрямую. Файл скопирован в буфер обмена!'),
duration: Duration(seconds: 4),
behavior: SnackBarBehavior.floating,
),
);
}
},
),
],
),
);
},
);
}
Future<String?> _saveContentToDownloads(String fileName, String content) async {
try {
final candidates = <String>[];
if (Platform.isAndroid) {
candidates.add('/storage/emulated/0/Download');
candidates.add('/sdcard/Download');
candidates.add('/storage/emulated/0/Documents');
} else if (Platform.isLinux || Platform.isMacOS) {
final home = Platform.environment['HOME'];
if (home != null) {
candidates.add('$home/Downloads');
candidates.add('$home/Documents');
}
} else if (Platform.isWindows) {
final userProfile = Platform.environment['USERPROFILE'];
if (userProfile != null) {
candidates.add('$userProfile/Downloads');
candidates.add('$userProfile/Documents');
}
}
for (final dirPath in candidates) {
final dir = Directory(dirPath);
if (dir.existsSync()) {
final file = File('${dir.path}/$fileName');
await file.writeAsString(content, flush: true);
return file.path;
}
}
for (final dirPath in candidates) {
try {
final dir = Directory(dirPath);
await dir.create(recursive: true);
final file = File('${dir.path}/$fileName');
await file.writeAsString(content, flush: true);
return file.path;
} catch (_) {}
}
return null;
} catch (_) {
return null;
}
}
Future<void> _openImportDialog() async {
final textController = TextEditingController();
showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: const Color(0xFF151829),
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(24)),
),
builder: (context) {
return StatefulBuilder(
builder: (context, setModalState) {
return Padding(
padding: EdgeInsets.fromLTRB(
20,
16,
20,
MediaQuery.of(context).viewInsets.bottom + 24,
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Center(
child: Container(
width: 44,
height: 4,
decoration: BoxDecoration(
color: const Color(0xFFFFD54F).withValues(alpha: 0.4),
borderRadius: BorderRadius.circular(2),
),
),
),
const SizedBox(height: 16),
const Row(
children: [
Icon(Icons.file_upload_outlined, color: Color(0xFFFFD54F), size: 26),
SizedBox(width: 10),
Text(
'Импорт описаний в эту версию',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w900,
color: Color(0xFFFFF9C4),
),
),
],
),
const SizedBox(height: 6),
const Text(
'Вставьте JSON-файл описаний, выгруженный из предыдущей версии приложения.',
style: TextStyle(fontSize: 13, color: Color(0xFF8C93B8), height: 1.35),
),
const SizedBox(height: 14),
TextField(
controller: textController,
maxLines: 6,
style: const TextStyle(fontSize: 12.5, fontFamily: 'monospace'),
decoration: InputDecoration(
hintText: 'Вставьте JSON сюда...',
hintStyle: const TextStyle(color: Color(0xFF555B7A)),
filled: true,
fillColor: const Color(0xFF0F111E),
border: OutlineInputBorder(borderRadius: BorderRadius.circular(14)),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(14),
borderSide: const BorderSide(color: Color(0xFF2A2D4A)),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(14),
borderSide: const BorderSide(color: Color(0xFFFFD54F)),
),
),
),
const SizedBox(height: 12),
Row(
children: [
Expanded(
child: OutlinedButton.icon(
icon: const Icon(Icons.paste_rounded, size: 18),
label: const Text('Вставить из буфера'),
style: OutlinedButton.styleFrom(
foregroundColor: const Color(0xFFFFD54F),
side: const BorderSide(color: Color(0xFFFFD54F), width: 1.1),
padding: const EdgeInsets.symmetric(vertical: 12),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
),
onPressed: () async {
final data = await Clipboard.getData('text/plain');
if (data?.text != null) {
textController.text = data!.text!;
HapticFeedback.lightImpact();
}
},
),
),
const SizedBox(width: 10),
Expanded(
child: FilledButton.icon(
icon: const Icon(Icons.check_rounded, color: Colors.black, size: 18),
label: const Text('Применить', style: TextStyle(fontWeight: FontWeight.bold, color: Colors.black)),
style: FilledButton.styleFrom(
backgroundColor: const Color(0xFFFFD54F),
padding: const EdgeInsets.symmetric(vertical: 12),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
),
onPressed: () async {
final raw = textController.text.trim();
if (raw.isEmpty) return;
final count = await _repo.importCustomEditsJson(raw);
if (!context.mounted) return;
Navigator.pop(context);
setState(() {});
HapticFeedback.heavyImpact();
if (count > 0) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Row(
children: [
const Icon(Icons.check_circle_rounded, color: Color(0xFFFFD54F), size: 20),
const SizedBox(width: 10),
Expanded(
child: Text(
'Успешно импортировано $count описаний символов! Они активированы в базе.',
style: const TextStyle(fontWeight: FontWeight.w600),
),
),
],
),
backgroundColor: const Color(0xFF1E2238),
duration: const Duration(seconds: 4),
behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
),
);
} else {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Не удалось распознать формат JSON или нет данных о символах.'),
backgroundColor: Color(0xFF3E1E22),
duration: Duration(seconds: 3),
behavior: SnackBarBehavior.floating,
),
);
}
},
),
),
],
),
],
),
);
},
);
},
);
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final editsCount = _repo.customEditsCount;
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: const Color(0xFF151829),
borderRadius: BorderRadius.circular(18),
border: Border.all(
color: const Color(0xFFFFD54F).withValues(alpha: 0.3),
width: 1.2,
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
width: 38,
height: 38,
alignment: Alignment.center,
decoration: BoxDecoration(
color: const Color(0xFF2A2D4A),
borderRadius: BorderRadius.circular(10),
),
child: const Icon(Icons.auto_stories_outlined, color: Color(0xFFFFD54F), size: 20),
),
const SizedBox(width: 12),
const Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'База и статьи символов',
style: TextStyle(
fontWeight: FontWeight.w900,
fontSize: 16,
color: Color(0xFFFFF9C4),
),
),
Text(
'Перенос описаний между версиями',
style: TextStyle(fontSize: 12, color: Color(0xFF8C93B8)),
),
],
),
),
],
),
const SizedBox(height: 12),
Text(
'Выгрузите все отредактированные и обогащённые описания одним файлом JSON. В новой версии приложения нажмите «Импортировать» и вставьте файл — все ваши статьи мгновенно встанут на свои места.',
style: TextStyle(
fontSize: 13,
height: 1.4,
color: theme.colorScheme.onSurface.withValues(alpha: 0.8),
),
),
const SizedBox(height: 12),
Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
decoration: BoxDecoration(
color: const Color(0xFF0F111E),
borderRadius: BorderRadius.circular(10),
border: Border.all(color: const Color(0xFF2A2D4A)),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Expanded(
child: Text('Пользовательских статей/правок:', style: TextStyle(color: Color(0xFF8C93B8), fontSize: 12)),
),
const SizedBox(width: 8),
Text(
'$editsCount знаков',
style: const TextStyle(color: Color(0xFFFFD54F), fontWeight: FontWeight.bold, fontSize: 13),
),
],
),
),
const SizedBox(height: 14),
Row(
children: [
Expanded(
child: FilledButton.icon(
onPressed: _exportCustomEdits,
icon: const Icon(Icons.file_download_outlined, color: Colors.black, size: 18),
label: const Text(
'Выгрузить новые',
style: TextStyle(fontWeight: FontWeight.bold, color: Colors.black, fontSize: 12.5),
),
style: FilledButton.styleFrom(
backgroundColor: const Color(0xFFFFD54F),
padding: const EdgeInsets.symmetric(vertical: 12),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
),
),
),
const SizedBox(width: 8),
Expanded(
child: OutlinedButton.icon(
onPressed: _openImportDialog,
icon: const Icon(Icons.file_upload_outlined, color: Color(0xFFFFD54F), size: 18),
label: const Text(
'Импортировать',
style: TextStyle(fontWeight: FontWeight.bold, color: Color(0xFFFFD54F), fontSize: 12.5),
),
style: OutlinedButton.styleFrom(
side: const BorderSide(color: Color(0xFFFFD54F), width: 1.1),
padding: const EdgeInsets.symmetric(vertical: 12),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
),
),
),
],
),
const SizedBox(height: 8),
SizedBox(
width: double.infinity,
child: TextButton.icon(
onPressed: _exportFullLexicon,
icon: const Icon(Icons.inventory_2_outlined, size: 16, color: Color(0xFF80DEEA)),
label: const Text(
'Выгрузить полный канон (все 1000 символов одним файлом)',
style: TextStyle(fontSize: 12, color: Color(0xFF80DEEA)),
),
),
),
],
),
);
}
}