feat: Add Nanaxe server with Docker, GitHub Actions CI/CD, and auto-deployment

- 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
This commit is contained in:
2026-04-19 22:18:25 +05:00
parent 52aea9ac87
commit f1e0f6b5ef
56 changed files with 8892 additions and 397 deletions
+179
View File
@@ -0,0 +1,179 @@
import 'package:flutter/material.dart';
/// Карточка статистики
class StatCard extends StatelessWidget {
final String title;
final String value;
final IconData icon;
final Color? color;
final VoidCallback? onTap;
const StatCard({
super.key,
required this.title,
required this.value,
required this.icon,
this.color,
this.onTap,
});
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onTap,
child: 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: [
Icon(icon, color: color ?? Colors.blue),
const SizedBox(height: 12),
Text(
value,
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
color: color ?? Colors.blue,
),
),
const SizedBox(height: 4),
Text(
title,
style: TextStyle(
color: Colors.grey[600],
fontSize: 12,
),
),
],
),
),
);
}
}
/// Пустое состояние
class EmptyState extends StatelessWidget {
final IconData icon;
final String title;
final String subtitle;
final Widget? action;
const EmptyState({
super.key,
required this.icon,
required this.title,
required this.subtitle,
this.action,
});
@override
Widget build(BuildContext context) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(icon, size: 80, color: Colors.grey[400]),
const SizedBox(height: 16),
Text(
title,
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: Colors.grey[600],
),
),
const SizedBox(height: 8),
Text(
subtitle,
textAlign: TextAlign.center,
style: TextStyle(color: Colors.grey[500]),
),
if (action != null) ...[
const SizedBox(height: 24),
action!,
],
],
),
);
}
}
/// Загрузочный экран
class LoadingScreen extends StatelessWidget {
final String? message;
const LoadingScreen({super.key, this.message});
@override
Widget build(BuildContext context) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const CircularProgressIndicator(),
if (message != null) ...[
const SizedBox(height: 16),
Text(
message!,
style: TextStyle(color: Colors.grey[600]),
),
],
],
),
);
}
}
/// Экран ошибки
class ErrorScreen extends StatelessWidget {
final String message;
final VoidCallback? onRetry;
const ErrorScreen({
super.key,
required this.message,
this.onRetry,
});
@override
Widget build(BuildContext context) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.error_outline, size: 64, color: Colors.red),
const SizedBox(height: 16),
Text(
'Ошибка',
style: Theme.of(context).textTheme.titleLarge,
),
const SizedBox(height: 8),
Text(
message,
textAlign: TextAlign.center,
style: TextStyle(color: Colors.grey[600]),
),
if (onRetry != null) ...[
const SizedBox(height: 24),
ElevatedButton.icon(
onPressed: onRetry,
icon: const Icon(Icons.refresh),
label: const Text('Повторить'),
),
],
],
),
);
}
}