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:
+242
-31
@@ -1,49 +1,260 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import '../models/alarm_model.dart';
|
||||
import '../core/notification_service.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
class AlarmsScreen extends StatelessWidget {
|
||||
class AlarmsScreen extends ConsumerStatefulWidget {
|
||||
const AlarmsScreen({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Будильники'),
|
||||
),
|
||||
body: ListView.builder(
|
||||
padding: const EdgeInsets.all(16),
|
||||
itemCount: 5,
|
||||
itemBuilder: (context, index) {
|
||||
return _AlarmCard(index: index);
|
||||
},
|
||||
),
|
||||
floatingActionButton: FloatingActionButton.extended(
|
||||
onPressed: () {},
|
||||
icon: const Icon(Icons.add),
|
||||
label: const Text('Новый будильник'),
|
||||
ConsumerState<AlarmsScreen> createState() => _AlarmsScreenState();
|
||||
}
|
||||
|
||||
class _AlarmsScreenState extends ConsumerState<AlarmsScreen> {
|
||||
List<AlarmModel> _alarms = [];
|
||||
bool _isLoading = true;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadAlarms();
|
||||
}
|
||||
|
||||
Future<void> _loadAlarms() async {
|
||||
setState(() => _isLoading = true);
|
||||
// TODO: Загрузить из Firestore/Hive
|
||||
setState(() {
|
||||
_alarms = [];
|
||||
_isLoading = false;
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _toggleAlarm(AlarmModel alarm) async {
|
||||
final updated = alarm.copyWith(isEnabled: !alarm.isEnabled);
|
||||
setState(() {
|
||||
final index = _alarms.indexWhere((a) => a.id == alarm.id);
|
||||
if (index != -1) {
|
||||
_alarms[index] = updated;
|
||||
}
|
||||
});
|
||||
|
||||
if (updated.isEnabled) {
|
||||
await notificationService.scheduleNotification(
|
||||
id: updated.hashCode,
|
||||
title: updated.title,
|
||||
body: 'Напоминание: ${updated.title}',
|
||||
scheduledDate: updated.dateTime,
|
||||
);
|
||||
} else {
|
||||
await notificationService.cancelNotification(updated.hashCode);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _deleteAlarm(String id) async {
|
||||
final alarm = _alarms.firstWhere((a) => a.id == id);
|
||||
await notificationService.cancelNotification(alarm.hashCode);
|
||||
|
||||
setState(() {
|
||||
_alarms.removeWhere((a) => a.id == id);
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _showAddAlarmDialog() async {
|
||||
final titleController = TextEditingController();
|
||||
DateTime selectedDate = DateTime.now();
|
||||
TimeOfDay selectedTime = TimeOfDay.now();
|
||||
|
||||
await showDialog(
|
||||
context: context,
|
||||
builder: (context) => StatefulBuilder(
|
||||
builder: (context, setDialogState) => AlertDialog(
|
||||
title: const Text('Новый будильник'),
|
||||
content: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
TextField(
|
||||
controller: titleController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Название',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.calendar_today),
|
||||
title: const Text('Дата'),
|
||||
subtitle: Text(
|
||||
'${selectedDate.day}.${selectedDate.month}.${selectedDate.year}',
|
||||
),
|
||||
trailing: const Icon(Icons.arrow_forward_ios, size: 16),
|
||||
onTap: () async {
|
||||
final picked = await showDatePicker(
|
||||
context: context,
|
||||
initialDate: selectedDate,
|
||||
firstDate: DateTime.now(),
|
||||
lastDate: DateTime.now().add(const Duration(days: 365)),
|
||||
);
|
||||
if (picked != null) {
|
||||
setDialogState(() => selectedDate = picked);
|
||||
}
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.access_time),
|
||||
title: const Text('Время'),
|
||||
subtitle: Text(selectedTime.format(context)),
|
||||
trailing: const Icon(Icons.arrow_forward_ios, size: 16),
|
||||
onTap: () async {
|
||||
final picked = await showTimePicker(
|
||||
context: context,
|
||||
initialTime: selectedTime,
|
||||
);
|
||||
if (picked != null) {
|
||||
setDialogState(() => selectedTime = picked);
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text('Отмена'),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
if (titleController.text.isEmpty) return;
|
||||
|
||||
final dateTime = DateTime(
|
||||
selectedDate.year,
|
||||
selectedDate.month,
|
||||
selectedDate.day,
|
||||
selectedTime.hour,
|
||||
selectedTime.minute,
|
||||
);
|
||||
|
||||
final alarm = AlarmModel(
|
||||
id: const Uuid().v4(),
|
||||
title: titleController.text,
|
||||
dateTime: dateTime,
|
||||
isEnabled: true,
|
||||
createdAt: DateTime.now(),
|
||||
updatedAt: DateTime.now(),
|
||||
);
|
||||
|
||||
setState(() {
|
||||
_alarms.add(alarm);
|
||||
});
|
||||
|
||||
notificationService.scheduleNotification(
|
||||
id: alarm.hashCode,
|
||||
title: alarm.title,
|
||||
body: 'Будильник: ${alarm.title}',
|
||||
scheduledDate: alarm.dateTime,
|
||||
);
|
||||
|
||||
Navigator.pop(context);
|
||||
},
|
||||
child: const Text('Создать'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _AlarmCard extends StatelessWidget {
|
||||
final int index;
|
||||
|
||||
const _AlarmCard({required this.index});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (_isLoading) {
|
||||
return const Scaffold(
|
||||
body: Center(child: CircularProgressIndicator()),
|
||||
);
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Будильники'),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.refresh),
|
||||
onPressed: _loadAlarms,
|
||||
tooltip: 'Обновить',
|
||||
),
|
||||
],
|
||||
),
|
||||
body: _alarms.isEmpty
|
||||
? _buildEmptyState()
|
||||
: ListView.builder(
|
||||
padding: const EdgeInsets.all(16),
|
||||
itemCount: _alarms.length,
|
||||
itemBuilder: (context, index) {
|
||||
final alarm = _alarms[index];
|
||||
return _buildAlarmCard(alarm);
|
||||
},
|
||||
),
|
||||
floatingActionButton: FloatingActionButton(
|
||||
onPressed: _showAddAlarmDialog,
|
||||
child: const Icon(Icons.add),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildEmptyState() {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(Icons.alarm, size: 100, color: Colors.grey[400]),
|
||||
const SizedBox(height: 24),
|
||||
Text(
|
||||
'Нет будильников',
|
||||
style: TextStyle(fontSize: 20, color: Colors.grey[600]),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Создайте первый будильник',
|
||||
style: TextStyle(color: Colors.grey[500]),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildAlarmCard(AlarmModel alarm) {
|
||||
return Card(
|
||||
margin: const EdgeInsets.only(bottom: 12),
|
||||
child: ListTile(
|
||||
leading: CircleAvatar(
|
||||
backgroundColor: index % 2 == 0 ? Colors.blue : Colors.grey,
|
||||
child: const Icon(Icons.alarm, color: Colors.white),
|
||||
leading: Icon(
|
||||
alarm.isEnabled ? Icons.alarm : Icons.alarm_off,
|
||||
color: alarm.isEnabled ? Colors.blue : Colors.grey,
|
||||
),
|
||||
title: Text('Будильник #${index + 1}'),
|
||||
subtitle: Text('${8 + index}:00'),
|
||||
trailing: Switch(
|
||||
value: index % 2 == 0,
|
||||
onChanged: (value) {},
|
||||
title: Text(
|
||||
alarm.title,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: alarm.isEnabled ? null : Colors.grey,
|
||||
),
|
||||
),
|
||||
subtitle: Text(
|
||||
'${alarm.dateTime.day}.${alarm.dateTime.month}.${alarm.dateTime.year} '
|
||||
'${alarm.dateTime.hour.toString().padLeft(2, '0')}:'
|
||||
'${alarm.dateTime.minute.toString().padLeft(2, '0')}',
|
||||
),
|
||||
trailing: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Switch(
|
||||
value: alarm.isEnabled,
|
||||
onChanged: (_) => _toggleAlarm(alarm),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.delete, color: Colors.red),
|
||||
onPressed: () => _deleteAlarm(alarm.id),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user