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:
+278
-270
@@ -1,57 +1,37 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import '../providers/mindmap_provider.dart';
|
||||
import '../models/mind_map_model.dart';
|
||||
import 'package:firebase_auth/firebase_auth.dart';
|
||||
import '../providers/mind_maps_provider.dart';
|
||||
import '../models/mindmap_model.dart';
|
||||
import '../core/admin_access_service.dart';
|
||||
import 'mind_map_viewer_screen.dart';
|
||||
import 'mind_map_editor_screen.dart';
|
||||
import 'mind_map_interactive_screen.dart';
|
||||
|
||||
class MindMapScreen extends ConsumerStatefulWidget {
|
||||
class MindMapScreen extends ConsumerWidget {
|
||||
const MindMapScreen({super.key});
|
||||
|
||||
@override
|
||||
ConsumerState<MindMapScreen> createState() => _MindMapScreenState();
|
||||
}
|
||||
|
||||
class _MindMapScreenState extends ConsumerState<MindMapScreen> {
|
||||
bool _isEditing = false;
|
||||
bool _isGenerating = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final projectId = 'default'; // TODO: Получить текущий проект
|
||||
final mindMapState = ref.watch(mindMapProvider(projectId));
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final mindMapsAsync = ref.watch(mindMapsProvider);
|
||||
final isAdmin = ref.watch(isAdminProvider);
|
||||
final currentUserEmail = FirebaseAuth.instance.currentUser?.email ?? '';
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Mind-карта'),
|
||||
title: const Text('Mind Maps'),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: Icon(_isEditing ? Icons.check : Icons.edit),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_isEditing = !_isEditing;
|
||||
});
|
||||
},
|
||||
tooltip: _isEditing ? 'Готово' : 'Редактировать',
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.auto_awesome),
|
||||
onPressed: _isGenerating ? null : () => _generateWithAI(projectId),
|
||||
tooltip: 'AI Генерация',
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.add),
|
||||
onPressed: () => _showAddNodeDialog(context, projectId, null),
|
||||
tooltip: 'Добавить узел',
|
||||
),
|
||||
if (isAdmin)
|
||||
IconButton(
|
||||
icon: const Icon(Icons.admin_panel_settings),
|
||||
onPressed: () => _showAdminOptions(context),
|
||||
tooltip: 'Админ панель',
|
||||
),
|
||||
],
|
||||
),
|
||||
body: mindMapState.when(
|
||||
data: (mindMapData) {
|
||||
if (mindMapData.nodes.isEmpty) {
|
||||
return _buildEmptyState(context, projectId);
|
||||
}
|
||||
|
||||
return _buildSimpleMindMap(mindMapData);
|
||||
},
|
||||
body: mindMapsAsync.when(
|
||||
data: (mindMaps) =>
|
||||
_buildContent(context, mindMaps, isAdmin, currentUserEmail, ref),
|
||||
loading: () => const Center(child: CircularProgressIndicator()),
|
||||
error: (error, stack) => Center(
|
||||
child: Column(
|
||||
@@ -62,121 +42,185 @@ class _MindMapScreenState extends ConsumerState<MindMapScreen> {
|
||||
Text('Ошибка: $error'),
|
||||
const SizedBox(height: 16),
|
||||
ElevatedButton(
|
||||
onPressed: () => ref.refresh(mindMapProvider(projectId)),
|
||||
onPressed: () => ref.invalidate(mindMapsProvider),
|
||||
child: const Text('Повторить'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
floatingActionButton: _isEditing
|
||||
? FloatingActionButton.extended(
|
||||
onPressed: () => _showAddNodeDialog(context, projectId, null),
|
||||
icon: const Icon(Icons.add),
|
||||
label: const Text('Добавить узел'),
|
||||
)
|
||||
: null,
|
||||
floatingActionButton: FloatingActionButton.extended(
|
||||
onPressed: () => _showCreateOptions(context, ref),
|
||||
icon: const Icon(Icons.add),
|
||||
label: const Text('Создать'),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Простое отображение дерева (замена GraphView)
|
||||
Widget _buildSimpleMindMap(MindMapData mindMapData) {
|
||||
return ListView.builder(
|
||||
Widget _buildContent(
|
||||
BuildContext context,
|
||||
List<MindMapModel> mindMaps,
|
||||
bool isAdmin,
|
||||
String currentUserEmail,
|
||||
WidgetRef ref,
|
||||
) {
|
||||
// Разделяем на основные и личные
|
||||
final mainMaps = mindMaps.where((m) => m.isMain).toList();
|
||||
final personalMaps = mindMaps.where((m) => !m.isMain).toList();
|
||||
|
||||
if (mindMaps.isEmpty) {
|
||||
return _buildEmptyState(context, ref);
|
||||
}
|
||||
|
||||
return ListView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
itemCount: mindMapData.nodes.length,
|
||||
itemBuilder: (context, index) {
|
||||
final node = mindMapData.nodes[index];
|
||||
return _buildTreeNodeWidget(node, mindMapData);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTreeNodeWidget(MindMapNode node, MindMapData mindMapData) {
|
||||
final children = mindMapData.getChildren(node.id);
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
margin: EdgeInsets.only(left: node.level * 20.0),
|
||||
decoration: BoxDecoration(
|
||||
color: _hexToColor(node.color),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
_getIconData(node.icon),
|
||||
color: Colors.white,
|
||||
size: 20,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
node.title,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (_isEditing)
|
||||
IconButton(
|
||||
icon: const Icon(Icons.delete, color: Colors.white),
|
||||
onPressed: () => _confirmDeleteNode(node),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (children.isNotEmpty) ...[
|
||||
const SizedBox(height: 8),
|
||||
...children.map((child) => _buildTreeNodeWidget(child, mindMapData)),
|
||||
// Основные карты
|
||||
if (mainMaps.isNotEmpty) ...[
|
||||
_buildSectionHeader('Основные', Icons.star, Colors.amber),
|
||||
...mainMaps.map((map) =>
|
||||
_buildMindMapCard(context, map, isAdmin, currentUserEmail, ref)),
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
|
||||
// Личные карты
|
||||
if (personalMaps.isNotEmpty) ...[
|
||||
_buildSectionHeader('Личные', Icons.person, Colors.blue),
|
||||
...personalMaps.map((map) =>
|
||||
_buildMindMapCard(context, map, isAdmin, currentUserEmail, ref)),
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildEmptyState(BuildContext context, String projectId) {
|
||||
Widget _buildSectionHeader(String title, IconData icon, Color color) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 12),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(icon, color: color, size: 24),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
title,
|
||||
style: const TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMindMapCard(
|
||||
BuildContext context,
|
||||
MindMapModel mindMap,
|
||||
bool isAdmin,
|
||||
String currentUserEmail,
|
||||
WidgetRef ref,
|
||||
) {
|
||||
final isOwner = mindMap.ownerEmail == currentUserEmail;
|
||||
final canDelete = isAdmin || isOwner;
|
||||
final canEdit = isAdmin || isOwner;
|
||||
|
||||
return Card(
|
||||
margin: const EdgeInsets.only(bottom: 12),
|
||||
child: ListTile(
|
||||
leading: CircleAvatar(
|
||||
backgroundColor: mindMap.isMain ? Colors.amber : Colors.blue,
|
||||
child: Icon(
|
||||
mindMap.isMain ? Icons.star : Icons.account_tree,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
title: Text(
|
||||
mindMap.title,
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
subtitle: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (mindMap.description.isNotEmpty)
|
||||
Text(mindMap.description,
|
||||
maxLines: 1, overflow: TextOverflow.ellipsis),
|
||||
const SizedBox(height: 4),
|
||||
Text('Владелец: ${mindMap.ownerEmail}'),
|
||||
Text(
|
||||
'${mindMap.nodeCount} узлов • ${_formatDate(mindMap.createdAt)}'),
|
||||
],
|
||||
),
|
||||
trailing: PopupMenuButton<String>(
|
||||
onSelected: (value) => _handleAction(context, value, mindMap, ref),
|
||||
itemBuilder: (context) => [
|
||||
PopupMenuItem(
|
||||
value: 'view',
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.visibility, size: 20),
|
||||
const SizedBox(width: 12),
|
||||
const Text('Открыть'),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (canEdit)
|
||||
PopupMenuItem(
|
||||
value: 'edit',
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.edit, size: 20),
|
||||
const SizedBox(width: 12),
|
||||
const Text('Редактировать'),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (canDelete)
|
||||
PopupMenuItem(
|
||||
value: 'delete',
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.delete, color: Colors.red, size: 20),
|
||||
const SizedBox(width: 12),
|
||||
const Text('Удалить', style: TextStyle(color: Colors.red)),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
onTap: () => _openMindMap(context, mindMap),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildEmptyState(BuildContext context, WidgetRef ref) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.account_tree,
|
||||
size: 100,
|
||||
color: Colors.grey,
|
||||
),
|
||||
Icon(Icons.account_tree, size: 100, color: Colors.grey[400]),
|
||||
const SizedBox(height: 24),
|
||||
const Text(
|
||||
'Mind-карта пуста',
|
||||
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
|
||||
Text(
|
||||
'Нет mind карт',
|
||||
style: TextStyle(fontSize: 20, color: Colors.grey[600]),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
const Text(
|
||||
'Создайте структуру проекта',
|
||||
style: TextStyle(color: Colors.grey),
|
||||
Text(
|
||||
'Создайте первую карту вручную или с помощью AI',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(color: Colors.grey[500]),
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
ElevatedButton.icon(
|
||||
onPressed: () => _showAddNodeDialog(context, projectId, null),
|
||||
onPressed: () => _showCreateManualDialog(context, ref),
|
||||
icon: const Icon(Icons.add),
|
||||
label: const Text('Добавить вручную'),
|
||||
label: const Text('Вручную'),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
OutlinedButton.icon(
|
||||
onPressed:
|
||||
_isGenerating ? null : () => _generateWithAI(projectId),
|
||||
icon: _isGenerating
|
||||
? const SizedBox(
|
||||
width: 20,
|
||||
height: 20,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
)
|
||||
: const Icon(Icons.auto_awesome),
|
||||
FilledButton.icon(
|
||||
onPressed: () => _showCreateAIDialog(context, ref),
|
||||
icon: const Icon(Icons.auto_awesome),
|
||||
label: const Text('AI Генерация'),
|
||||
),
|
||||
],
|
||||
@@ -186,159 +230,133 @@ class _MindMapScreenState extends ConsumerState<MindMapScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _generateWithAI(String projectId) async {
|
||||
setState(() {
|
||||
_isGenerating = true;
|
||||
});
|
||||
void _showCreateOptions(BuildContext context, WidgetRef ref) {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
builder: (context) => SafeArea(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
ListTile(
|
||||
leading: const Icon(Icons.add, color: Colors.blue),
|
||||
title: const Text('Создать вручную'),
|
||||
subtitle: const Text('Пустая mind map'),
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
_showCreateManualDialog(context, ref);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.auto_awesome, color: Colors.purple),
|
||||
title: const Text('AI Генерация'),
|
||||
subtitle: const Text('С помощью DeepSeek'),
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
_showCreateAIDialog(context, ref);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
final projectName = await _showProjectNameDialog();
|
||||
if (projectName == null || projectName.isEmpty) return;
|
||||
void _showCreateManualDialog(BuildContext context, WidgetRef ref) {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => const MindMapEditorScreen(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final projectDescription = await _showProjectDescriptionDialog();
|
||||
void _showCreateAIDialog(BuildContext context, WidgetRef ref) {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => const MindMapEditorScreen(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
await ref.read(mindMapProvider(projectId).notifier).generateMindMapWithAI(
|
||||
projectName: projectName,
|
||||
description: projectDescription ?? '',
|
||||
);
|
||||
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('✅ Mind-карта сгенерирована!'),
|
||||
backgroundColor: Colors.green,
|
||||
),
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('❌ Ошибка: $e'),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
setState(() {
|
||||
_isGenerating = false;
|
||||
});
|
||||
void _handleAction(
|
||||
BuildContext context,
|
||||
String action,
|
||||
MindMapModel mindMap,
|
||||
WidgetRef ref,
|
||||
) {
|
||||
switch (action) {
|
||||
case 'view':
|
||||
_openMindMap(context, mindMap);
|
||||
break;
|
||||
case 'edit':
|
||||
_editMindMap(context, mindMap, ref);
|
||||
break;
|
||||
case 'delete':
|
||||
_confirmDelete(context, mindMap, ref);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Future<String?> _showProjectNameDialog() {
|
||||
final controller = TextEditingController();
|
||||
return showDialog<String>(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('Название проекта'),
|
||||
content: TextField(
|
||||
controller: controller,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Введите название',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
autofocus: true,
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text('Отмена'),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () => Navigator.pop(context, controller.text),
|
||||
child: const Text('Генерировать'),
|
||||
),
|
||||
],
|
||||
void _openMindMap(BuildContext context, MindMapModel mindMap) {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => MindMapInteractiveScreen(mindMap: mindMap),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<String?> _showProjectDescriptionDialog() {
|
||||
final controller = TextEditingController();
|
||||
return showDialog<String>(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('Описание проекта'),
|
||||
content: TextField(
|
||||
controller: controller,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Описание (опционально)',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
maxLines: 3,
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context, ''),
|
||||
child: const Text('Пропустить'),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () => Navigator.pop(context, controller.text),
|
||||
child: const Text('Генерировать'),
|
||||
),
|
||||
],
|
||||
void _editMindMap(BuildContext context, MindMapModel mindMap, WidgetRef ref) {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => MindMapEditorScreen(mindMap: mindMap),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showAddNodeDialog(
|
||||
BuildContext context, String projectId, String? parentId) {
|
||||
final titleController = TextEditingController();
|
||||
|
||||
void _confirmDelete(
|
||||
BuildContext context, MindMapModel mindMap, WidgetRef ref) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('Добавить узел'),
|
||||
content: TextField(
|
||||
controller: titleController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Название',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
autofocus: true,
|
||||
),
|
||||
title: const Text('Удалить mind map?'),
|
||||
content: Text(
|
||||
'"${mindMap.title}" будет удалена навсегда. Это действие нельзя отменить.'),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text('Отмена'),
|
||||
),
|
||||
ElevatedButton(
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
if (titleController.text.isEmpty) return;
|
||||
try {
|
||||
await ref
|
||||
.read(mindMapsNotifierProvider.notifier)
|
||||
.deleteMindMap(mindMap.id, mindMap.ownerEmail);
|
||||
|
||||
await ref.read(mindMapProvider(projectId).notifier).addNode(
|
||||
title: titleController.text,
|
||||
parentId: parentId,
|
||||
if (context.mounted) {
|
||||
Navigator.pop(context);
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('✅ Mind map удалена'),
|
||||
backgroundColor: Colors.green,
|
||||
),
|
||||
);
|
||||
|
||||
if (context.mounted) {
|
||||
Navigator.pop(context);
|
||||
}
|
||||
} catch (e) {
|
||||
if (context.mounted) {
|
||||
Navigator.pop(context);
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('❌ Ошибка: $e'),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
child: const Text('Добавить'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _confirmDeleteNode(MindMapNode node) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('Удалить узел?'),
|
||||
content: const Text('Все дочерние узлы также будут удалены'),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text('Отмена'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
// TODO: Удалить узел
|
||||
Navigator.pop(context);
|
||||
},
|
||||
style: TextButton.styleFrom(foregroundColor: Colors.red),
|
||||
child: const Text('Удалить'),
|
||||
),
|
||||
@@ -347,26 +365,16 @@ class _MindMapScreenState extends ConsumerState<MindMapScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
Color _hexToColor(String hex) {
|
||||
hex = hex.replaceAll('#', '');
|
||||
if (hex.length == 6) {
|
||||
return Color(int.parse('0xFF$hex'));
|
||||
}
|
||||
return Colors.blue;
|
||||
void _showAdminOptions(BuildContext context) {
|
||||
// TODO: Открыть админ панель
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Админ панель в разработке')),
|
||||
);
|
||||
}
|
||||
|
||||
IconData _getIconData(String iconName) {
|
||||
switch (iconName) {
|
||||
case 'folder':
|
||||
return Icons.folder;
|
||||
case 'task':
|
||||
return Icons.task;
|
||||
case 'star':
|
||||
return Icons.star;
|
||||
case 'flag':
|
||||
return Icons.flag;
|
||||
default:
|
||||
return Icons.circle;
|
||||
}
|
||||
String _formatDate(DateTime date) {
|
||||
return '${date.day.toString().padLeft(2, '0')}.'
|
||||
'${date.month.toString().padLeft(2, '0')}.'
|
||||
'${date.year}';
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user