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
263 lines
8.0 KiB
Dart
263 lines
8.0 KiB
Dart
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 ConsumerStatefulWidget {
|
|
const AlarmsScreen({super.key});
|
|
|
|
@override
|
|
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('Создать'),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
@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: Icon(
|
|
alarm.isEnabled ? Icons.alarm : Icons.alarm_off,
|
|
color: alarm.isEnabled ? Colors.blue : Colors.grey,
|
|
),
|
|
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),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|