f1e0f6b5ef
- Add complete server infrastructure with Gitea, API, PostgreSQL - Implement Obsidian-compatible markdown mindmap converter - Add Flutter service for server synchronization - Configure GitHub Actions for automated build and deploy - Add production docker-compose with health checks - Include VPS setup and deployment scripts - Add comprehensive deployment documentation
465 lines
15 KiB
Dart
465 lines
15 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
import 'package:reactive_mind_map/reactive_mind_map.dart' as rm;
|
|
import 'dart:convert';
|
|
import 'package:shared_preferences/shared_preferences.dart';
|
|
|
|
class MindMapSettingsScreen extends ConsumerStatefulWidget {
|
|
final String projectId;
|
|
|
|
const MindMapSettingsScreen({super.key, required this.projectId});
|
|
|
|
@override
|
|
ConsumerState<MindMapSettingsScreen> createState() =>
|
|
_MindMapSettingsScreenState();
|
|
}
|
|
|
|
class _MindMapSettingsScreenState extends ConsumerState<MindMapSettingsScreen> {
|
|
rm.MindMapLayout _defaultLayout = rm.MindMapLayout.radial;
|
|
rm.NodeShape _defaultShape = rm.NodeShape.roundedRectangle;
|
|
rm.CameraFocus _defaultCamera = rm.CameraFocus.fitAll;
|
|
bool _autoExpandNodes = true;
|
|
bool _showConnections = true;
|
|
bool _enableShadows = true;
|
|
double _connectionWidth = 2.0;
|
|
Color _connectionColor = const Color(0xFFB0BEC5);
|
|
List<Color> _nodeColors = const [
|
|
Color(0xFF2196F3),
|
|
Color(0xFF4CAF50),
|
|
Color(0xFFFF9800),
|
|
Color(0xFFE91E63),
|
|
Color(0xFF9C27B0),
|
|
];
|
|
bool _isLoading = true;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_loadSettings();
|
|
}
|
|
|
|
Future<void> _loadSettings() async {
|
|
try {
|
|
final prefs = await SharedPreferences.getInstance();
|
|
setState(() {
|
|
_defaultLayout = rm.MindMapLayout.values[
|
|
prefs.getInt('${widget.projectId}_layout') ?? 0];
|
|
_defaultShape = rm.NodeShape.values[
|
|
prefs.getInt('${widget.projectId}_shape') ?? 0];
|
|
_defaultCamera = rm.CameraFocus.values[
|
|
prefs.getInt('${widget.projectId}_camera') ?? 0];
|
|
_autoExpandNodes =
|
|
prefs.getBool('${widget.projectId}_auto_expand') ?? true;
|
|
_showConnections =
|
|
prefs.getBool('${widget.projectId}_show_connections') ?? true;
|
|
_enableShadows =
|
|
prefs.getBool('${widget.projectId}_shadows') ?? true;
|
|
_connectionWidth =
|
|
prefs.getDouble('${widget.projectId}_conn_width') ?? 2.0;
|
|
_isLoading = false;
|
|
});
|
|
} catch (e) {
|
|
setState(() {
|
|
_isLoading = false;
|
|
});
|
|
}
|
|
}
|
|
|
|
Future<void> _saveSettings() async {
|
|
try {
|
|
final prefs = await SharedPreferences.getInstance();
|
|
await prefs.setInt(
|
|
'${widget.projectId}_layout', _defaultLayout.index);
|
|
await prefs.setInt('${widget.projectId}_shape', _defaultShape.index);
|
|
await prefs.setInt(
|
|
'${widget.projectId}_camera', _defaultCamera.index);
|
|
await prefs.setBool(
|
|
'${widget.projectId}_auto_expand', _autoExpandNodes);
|
|
await prefs.setBool(
|
|
'${widget.projectId}_show_connections', _showConnections);
|
|
await prefs.setBool('${widget.projectId}_shadows', _enableShadows);
|
|
await prefs.setDouble(
|
|
'${widget.projectId}_conn_width', _connectionWidth);
|
|
|
|
if (mounted) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(
|
|
content: Text('✅ Настройки сохранены'),
|
|
backgroundColor: Colors.green,
|
|
),
|
|
);
|
|
}
|
|
} catch (e) {
|
|
if (mounted) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(
|
|
content: Text('❌ Ошибка: $e'),
|
|
backgroundColor: Colors.red,
|
|
),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
Future<void> _exportMindMap() async {
|
|
// TODO: Экспорт mind map в JSON файл
|
|
if (mounted) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(
|
|
content: Text('📤 Экспорт в разработке'),
|
|
backgroundColor: Colors.orange,
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
Future<void> _importMindMap() async {
|
|
// TODO: Импорт mind map из JSON файла
|
|
if (mounted) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(
|
|
content: Text('📥 Импорт в разработке'),
|
|
backgroundColor: Colors.orange,
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
if (_isLoading) {
|
|
return const Scaffold(
|
|
body: Center(child: CircularProgressIndicator()),
|
|
);
|
|
}
|
|
|
|
return Scaffold(
|
|
appBar: AppBar(
|
|
title: const Text('Настройки Mind Map'),
|
|
actions: [
|
|
IconButton(
|
|
icon: const Icon(Icons.save),
|
|
onPressed: _saveSettings,
|
|
tooltip: 'Сохранить',
|
|
),
|
|
],
|
|
),
|
|
body: ListView(
|
|
padding: const EdgeInsets.all(16),
|
|
children: [
|
|
_buildSection(
|
|
'Layout',
|
|
[
|
|
_buildDropdown<rm.MindMapLayout>(
|
|
'Layout по умолчанию',
|
|
_defaultLayout,
|
|
rm.MindMapLayout.values,
|
|
(value) => setState(() => _defaultLayout = value!),
|
|
labels: const {
|
|
rm.MindMapLayout.radial: 'Радиальный',
|
|
rm.MindMapLayout.right: 'Вправо',
|
|
rm.MindMapLayout.left: 'Влево',
|
|
rm.MindMapLayout.horizontal: 'Горизонтальный',
|
|
rm.MindMapLayout.vertical: 'Вертикальный',
|
|
},
|
|
),
|
|
_buildDropdown<rm.NodeShape>(
|
|
'Форма узлов',
|
|
_defaultShape,
|
|
rm.NodeShape.values,
|
|
(value) => setState(() => _defaultShape = value!),
|
|
labels: const {
|
|
rm.NodeShape.roundedRectangle: 'Скругленный прямоугольник',
|
|
rm.NodeShape.circle: 'Круг',
|
|
rm.NodeShape.diamond: 'Ромб',
|
|
rm.NodeShape.hexagon: 'Шестиугольник',
|
|
rm.NodeShape.ellipse: 'Эллипс',
|
|
rm.NodeShape.rectangle: 'Прямоугольник',
|
|
},
|
|
),
|
|
_buildDropdown<rm.CameraFocus>(
|
|
'Фокус камеры',
|
|
_defaultCamera,
|
|
rm.CameraFocus.values,
|
|
(value) => setState(() => _defaultCamera = value!),
|
|
labels: const {
|
|
rm.CameraFocus.fitAll: 'Вместить всё',
|
|
rm.CameraFocus.rootNode: 'Корневой узел',
|
|
rm.CameraFocus.center: 'Центр',
|
|
rm.CameraFocus.firstLeaf: 'Первый лист',
|
|
},
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 24),
|
|
_buildSection(
|
|
'Поведение',
|
|
[
|
|
SwitchListTile(
|
|
title: const Text('Авто-раскрытие узлов'),
|
|
subtitle: const Text('Раскрывать все узлы при загрузке'),
|
|
value: _autoExpandNodes,
|
|
onChanged: (value) =>
|
|
setState(() => _autoExpandNodes = value),
|
|
),
|
|
SwitchListTile(
|
|
title: const Text('Показывать соединения'),
|
|
value: _showConnections,
|
|
onChanged: (value) =>
|
|
setState(() => _showConnections = value),
|
|
),
|
|
SwitchListTile(
|
|
title: const Text('Тени узлов'),
|
|
value: _enableShadows,
|
|
onChanged: (value) => setState(() => _enableShadows = value),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 24),
|
|
_buildSection(
|
|
'Соединительные линии',
|
|
[
|
|
ListTile(
|
|
title: const Text('Толщина линий'),
|
|
subtitle: Text('${_connectionWidth.toStringAsFixed(1)}px'),
|
|
trailing: SizedBox(
|
|
width: 150,
|
|
child: Slider(
|
|
value: _connectionWidth,
|
|
min: 1.0,
|
|
max: 5.0,
|
|
divisions: 8,
|
|
label: _connectionWidth.toStringAsFixed(1),
|
|
onChanged: (value) =>
|
|
setState(() => _connectionWidth = value),
|
|
),
|
|
),
|
|
),
|
|
ListTile(
|
|
title: const Text('Цвет линий'),
|
|
trailing: GestureDetector(
|
|
onTap: () => _showColorPicker('connection'),
|
|
child: Container(
|
|
width: 40,
|
|
height: 40,
|
|
decoration: BoxDecoration(
|
|
color: _connectionColor,
|
|
borderRadius: BorderRadius.circular(8),
|
|
border: Border.all(color: Colors.grey),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 24),
|
|
_buildSection(
|
|
'Цвета узлов',
|
|
[
|
|
Wrap(
|
|
spacing: 8,
|
|
runSpacing: 8,
|
|
children: List.generate(_nodeColors.length, (index) {
|
|
return GestureDetector(
|
|
onTap: () => _showColorPicker('node_$index'),
|
|
child: Container(
|
|
width: 50,
|
|
height: 50,
|
|
decoration: BoxDecoration(
|
|
color: _nodeColors[index],
|
|
borderRadius: BorderRadius.circular(8),
|
|
border: Border.all(color: Colors.grey),
|
|
),
|
|
child: Center(
|
|
child: Text(
|
|
'${index + 1}',
|
|
style: const TextStyle(
|
|
color: Colors.white,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}),
|
|
),
|
|
const SizedBox(height: 8),
|
|
Text(
|
|
'Нажмите на цвет чтобы изменить',
|
|
style: TextStyle(color: Colors.grey[600], fontSize: 12),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 32),
|
|
_buildSection(
|
|
'Экспорт / Импорт',
|
|
[
|
|
Row(
|
|
children: [
|
|
Expanded(
|
|
child: ElevatedButton.icon(
|
|
onPressed: _exportMindMap,
|
|
icon: const Icon(Icons.download),
|
|
label: const Text('Экспорт JSON'),
|
|
),
|
|
),
|
|
const SizedBox(width: 16),
|
|
Expanded(
|
|
child: OutlinedButton.icon(
|
|
onPressed: _importMindMap,
|
|
icon: const Icon(Icons.upload),
|
|
label: const Text('Импорт JSON'),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 32),
|
|
_buildResetButton(),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildSection(String title, List<Widget> children) {
|
|
return Container(
|
|
padding: const EdgeInsets.all(16),
|
|
decoration: BoxDecoration(
|
|
color: Theme.of(context).cardColor,
|
|
borderRadius: BorderRadius.circular(12),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.black.withOpacity(0.05),
|
|
blurRadius: 8,
|
|
offset: const Offset(0, 2),
|
|
),
|
|
],
|
|
),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
title,
|
|
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
const SizedBox(height: 16),
|
|
...children,
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildDropdown<T>(
|
|
String label,
|
|
T value,
|
|
List<T> items,
|
|
void Function(T?) onChanged, {
|
|
Map<T, String>? labels,
|
|
}) {
|
|
return Padding(
|
|
padding: const EdgeInsets.symmetric(vertical: 8),
|
|
child: DropdownButtonFormField<T>(
|
|
value: value,
|
|
decoration: InputDecoration(
|
|
labelText: label,
|
|
border: const OutlineInputBorder(),
|
|
),
|
|
items: items.map((item) {
|
|
return DropdownMenuItem(
|
|
value: item,
|
|
child: Text(labels?[item] ?? item.toString().split('.').last),
|
|
);
|
|
}).toList(),
|
|
onChanged: onChanged,
|
|
),
|
|
);
|
|
}
|
|
|
|
void _showColorPicker(String type) async {
|
|
final colors = [
|
|
const Color(0xFF2196F3),
|
|
const Color(0xFF4CAF50),
|
|
const Color(0xFFFF9800),
|
|
const Color(0xFFE91E63),
|
|
const Color(0xFF9C27B0),
|
|
const Color(0xFF00BCD4),
|
|
const Color(0xFFF44336),
|
|
const Color(0xFF795548),
|
|
const Color(0xFF607D8B),
|
|
const Color(0xFFB0BEC5),
|
|
];
|
|
|
|
final selected = await showDialog<Color>(
|
|
context: context,
|
|
builder: (context) => AlertDialog(
|
|
title: const Text('Выберите цвет'),
|
|
content: Wrap(
|
|
spacing: 8,
|
|
runSpacing: 8,
|
|
children: colors.map((color) {
|
|
return GestureDetector(
|
|
onTap: () => Navigator.pop(context, color),
|
|
child: Container(
|
|
width: 50,
|
|
height: 50,
|
|
decoration: BoxDecoration(
|
|
color: color,
|
|
borderRadius: BorderRadius.circular(8),
|
|
border: Border.all(color: Colors.grey),
|
|
),
|
|
),
|
|
);
|
|
}).toList(),
|
|
),
|
|
actions: [
|
|
TextButton(
|
|
onPressed: () => Navigator.pop(context),
|
|
child: const Text('Отмена'),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
|
|
if (selected != null) {
|
|
setState(() {
|
|
if (type == 'connection') {
|
|
_connectionColor = selected;
|
|
} else if (type.startsWith('node_')) {
|
|
final index = int.parse(type.split('_')[1]);
|
|
_nodeColors[index] = selected;
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
Widget _buildResetButton() {
|
|
return OutlinedButton.icon(
|
|
onPressed: () {
|
|
setState(() {
|
|
_defaultLayout = rm.MindMapLayout.radial;
|
|
_defaultShape = rm.NodeShape.roundedRectangle;
|
|
_defaultCamera = rm.CameraFocus.fitAll;
|
|
_autoExpandNodes = true;
|
|
_showConnections = true;
|
|
_enableShadows = true;
|
|
_connectionWidth = 2.0;
|
|
_connectionColor = const Color(0xFFB0BEC5);
|
|
_nodeColors = const [
|
|
Color(0xFF2196F3),
|
|
Color(0xFF4CAF50),
|
|
Color(0xFFFF9800),
|
|
Color(0xFFE91E63),
|
|
Color(0xFF9C27B0),
|
|
];
|
|
});
|
|
},
|
|
icon: const Icon(Icons.restore, color: Colors.red),
|
|
label: const Text('Сбросить настройки',
|
|
style: TextStyle(color: Colors.red)),
|
|
);
|
|
}
|
|
}
|