Add Google Calendar integration with sync
This commit is contained in:
@@ -0,0 +1,244 @@
|
||||
import 'dart:convert';
|
||||
import 'package:googleapis/calendar/v3.dart' as gcal;
|
||||
import 'package:googleapis_auth/auth_io.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import '../models/calendar_event_model.dart';
|
||||
|
||||
class GoogleCalendarService {
|
||||
static const List<String> _scopes = [
|
||||
gcal.CalendarApi.calendarEventsScope,
|
||||
gcal.CalendarApi.calendarReadonlyScope,
|
||||
];
|
||||
|
||||
String? _accessToken;
|
||||
String? _refreshToken;
|
||||
DateTime? _tokenExpiry;
|
||||
gcal.CalendarApi? _calendarApi;
|
||||
|
||||
// Авторизация через OAuth2
|
||||
Future<void> authorize(String clientId, String clientSecret, String refreshToken) async {
|
||||
_refreshToken = refreshToken;
|
||||
|
||||
final credentials = ServiceAccountCredentials(
|
||||
jsonDecode(jsonEncode({
|
||||
'client_id': clientId,
|
||||
'client_secret': clientSecret,
|
||||
'refresh_token': refreshToken,
|
||||
'type': 'authorized_user',
|
||||
})),
|
||||
);
|
||||
|
||||
final client = await clientViaServiceAccount(credentials, _scopes);
|
||||
_calendarApi = gcal.CalendarApi(client);
|
||||
}
|
||||
|
||||
// Получение списка событий
|
||||
Future<List<CalendarEventModel>> getEvents({
|
||||
required DateTime timeMin,
|
||||
required DateTime timeMax,
|
||||
String calendarId = 'primary',
|
||||
}) async {
|
||||
if (_calendarApi == null) {
|
||||
throw Exception('Not authorized');
|
||||
}
|
||||
|
||||
try {
|
||||
final events = await _calendarApi!.events.list(
|
||||
calendarId,
|
||||
timeMin: timeMin.toUtc(),
|
||||
timeMax: timeMax.toUtc(),
|
||||
orderBy: 'startTime',
|
||||
singleEvents: true,
|
||||
);
|
||||
|
||||
return events.items?.map((event) => _convertToEventModel(event)).toList() ?? [];
|
||||
} catch (e) {
|
||||
print('Error fetching events: $e');
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// Создание события
|
||||
Future<CalendarEventModel> createEvent({
|
||||
required String title,
|
||||
required DateTime startTime,
|
||||
required DateTime endTime,
|
||||
String? description,
|
||||
String? location,
|
||||
List<String>? attendeeEmails,
|
||||
String calendarId = 'primary',
|
||||
}) async {
|
||||
if (_calendarApi == null) {
|
||||
throw Exception('Not authorized');
|
||||
}
|
||||
|
||||
final event = gcal.Event((e) {
|
||||
e.summary = title;
|
||||
e.description = description;
|
||||
e.location = location;
|
||||
e.start = gcal.EventDateTime((dt) {
|
||||
dt.dateTime = startTime.toUtc();
|
||||
});
|
||||
e.end = gcal.EventDateTime((dt) {
|
||||
dt.dateTime = endTime.toUtc();
|
||||
});
|
||||
|
||||
if (attendeeEmails != null && attendeeEmails.isNotEmpty) {
|
||||
e.attendees = attendeeEmails
|
||||
.map((email) => gcal.EventAttendee((a) => a.email = email))
|
||||
.toList();
|
||||
}
|
||||
});
|
||||
|
||||
final createdEvent = await _calendarApi!.events.insert(event, calendarId);
|
||||
return _convertToEventModel(createdEvent);
|
||||
}
|
||||
|
||||
// Обновление события
|
||||
Future<CalendarEventModel> updateEvent(
|
||||
String eventId, {
|
||||
String? title,
|
||||
DateTime? startTime,
|
||||
DateTime? endTime,
|
||||
String? description,
|
||||
String? location,
|
||||
String calendarId = 'primary',
|
||||
}) async {
|
||||
if (_calendarApi == null) {
|
||||
throw Exception('Not authorized');
|
||||
}
|
||||
|
||||
// Сначала получаем текущее событие
|
||||
final existingEvent = await _calendarApi!.events.get(calendarId, eventId);
|
||||
|
||||
final updatedEvent = gcal.Event((e) {
|
||||
e.summary = title ?? existingEvent.summary;
|
||||
e.description = description ?? existingEvent.description;
|
||||
e.location = location ?? existingEvent.location;
|
||||
|
||||
if (startTime != null) {
|
||||
e.start = gcal.EventDateTime((dt) {
|
||||
dt.dateTime = startTime.toUtc();
|
||||
});
|
||||
} else {
|
||||
e.start = existingEvent.start;
|
||||
}
|
||||
|
||||
if (endTime != null) {
|
||||
e.end = gcal.EventDateTime((dt) {
|
||||
dt.dateTime = endTime.toUtc();
|
||||
});
|
||||
} else {
|
||||
e.end = existingEvent.end;
|
||||
}
|
||||
});
|
||||
|
||||
final result = await _calendarApi!.events.update(updatedEvent, calendarId, eventId);
|
||||
return _convertToEventModel(result);
|
||||
}
|
||||
|
||||
// Удаление события
|
||||
Future<void> deleteEvent(String eventId, String calendarId = 'primary') async {
|
||||
if (_calendarApi == null) {
|
||||
throw Exception('Not authorized');
|
||||
}
|
||||
|
||||
await _calendarApi!.events.delete(calendarId, eventId);
|
||||
}
|
||||
|
||||
// Конвертация Google Event в нашу модель
|
||||
CalendarEventModel _convertToEventModel(gcal.Event event) {
|
||||
final startTime = event.start?.dateTime ?? event.start?.date ?? DateTime.now();
|
||||
final endTime = event.end?.dateTime ?? event.end?.date ?? DateTime.now();
|
||||
|
||||
return CalendarEventModel(
|
||||
id: event.id ?? '',
|
||||
title: event.summary ?? 'Без названия',
|
||||
description: event.description,
|
||||
location: event.location ?? '',
|
||||
startTime: startTime.toLocal(),
|
||||
endTime: endTime.toLocal(),
|
||||
isAllDay: event.start?.date != null,
|
||||
googleEventId: event.id,
|
||||
attendeeEmails: event.attendees?.map((a) => a.email ?? '').toList() ?? [],
|
||||
color: _getColorFromEvent(event),
|
||||
status: event.status ?? 'confirmed',
|
||||
createdBy: event.creator?.email ?? '',
|
||||
createdAt: event.created ?? DateTime.now(),
|
||||
updatedAt: event.updated ?? DateTime.now(),
|
||||
);
|
||||
}
|
||||
|
||||
String _getColorFromEvent(gcal.Event event) {
|
||||
// Google Calendar использует colorId для цветов
|
||||
switch (event.colorId) {
|
||||
case '1': return '#a4bdfc'; // lavender
|
||||
case '2': return '#7ae7bf'; // sage
|
||||
case '3': return '#dbadff'; // grape
|
||||
case '4': return '#ff887c'; // flamo
|
||||
case '5': return '#fbd75b'; // banana
|
||||
case '6': return '#ffef8f'; // tangerine
|
||||
case '7': return '#dc2127'; // tomato
|
||||
case '8': return '#b9e4e0'; // peacock
|
||||
case '9': return '#e1e1e1'; // graphite
|
||||
case '10': return '#5484ed'; // blueberry
|
||||
case '11': return '#51b749'; // basil
|
||||
default: return '#2196F3'; // default blue
|
||||
}
|
||||
}
|
||||
|
||||
// Получение URL для OAuth авторизации
|
||||
static String getAuthUrl(String clientId, String redirectUri) {
|
||||
final scopesEncoded = _scopes.join('%20');
|
||||
return 'https://accounts.google.com/o/oauth2/auth'
|
||||
'?client_id=$clientId'
|
||||
'&redirect_uri=$redirectUri'
|
||||
'&scope=$scopesEncoded'
|
||||
'&response_type=code'
|
||||
'&access_type=offline'
|
||||
'&prompt=consent';
|
||||
}
|
||||
|
||||
// Обмен кода на токен
|
||||
Future<Map<String, dynamic>> exchangeCodeForToken({
|
||||
required String code,
|
||||
required String clientId,
|
||||
required String clientSecret,
|
||||
required String redirectUri,
|
||||
}) async {
|
||||
final response = await http.post(
|
||||
Uri.parse('https://oauth2.googleapis.com/token'),
|
||||
body: {
|
||||
'code': code,
|
||||
'client_id': clientId,
|
||||
'client_secret': clientSecret,
|
||||
'redirect_uri': redirectUri,
|
||||
'grant_type': 'authorization_code',
|
||||
},
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final data = jsonDecode(response.body);
|
||||
return {
|
||||
'accessToken': data['access_token'],
|
||||
'refreshToken': data['refresh_token'],
|
||||
'expiry': DateTime.now().add(Duration(seconds: data['expires_in'])),
|
||||
};
|
||||
} else {
|
||||
throw Exception('Failed to exchange code: ${response.body}');
|
||||
}
|
||||
}
|
||||
|
||||
// Проверка авторизации
|
||||
bool get isAuthorized => _calendarApi != null;
|
||||
|
||||
// Выход
|
||||
void logout() {
|
||||
_calendarApi = null;
|
||||
_accessToken = null;
|
||||
_refreshToken = null;
|
||||
_tokenExpiry = null;
|
||||
}
|
||||
}
|
||||
|
||||
final googleCalendarService = GoogleCalendarService();
|
||||
@@ -1,8 +1,145 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:table_calendar/table_calendar.dart';
|
||||
import '../models/calendar_event_model.dart';
|
||||
import '../core/google_calendar_service.dart';
|
||||
|
||||
class CalendarScreen extends StatelessWidget {
|
||||
class CalendarScreen extends ConsumerStatefulWidget {
|
||||
const CalendarScreen({super.key});
|
||||
|
||||
@override
|
||||
ConsumerState<CalendarScreen> createState() => _CalendarScreenState();
|
||||
}
|
||||
|
||||
class _CalendarScreenState extends ConsumerState<CalendarScreen> {
|
||||
DateTime _focusedDay = DateTime.now();
|
||||
DateTime? _selectedDay;
|
||||
List<CalendarEventModel> _events = [];
|
||||
bool _isLoading = false;
|
||||
bool _isSyncing = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_selectedDay = _focusedDay;
|
||||
_loadEvents();
|
||||
}
|
||||
|
||||
Future<void> _loadEvents() async {
|
||||
setState(() {
|
||||
_isLoading = true;
|
||||
});
|
||||
|
||||
try {
|
||||
// Загружаем события за месяц
|
||||
final timeMin = DateTime(_focusedDay.year, _focusedDay.month, 1);
|
||||
final timeMax =
|
||||
DateTime(_focusedDay.year, _focusedDay.month + 1, 0, 23, 59);
|
||||
|
||||
// TODO: Загрузить из Firestore или Google Calendar
|
||||
// Пока используем тестовые данные
|
||||
await Future.delayed(const Duration(milliseconds: 500));
|
||||
|
||||
setState(() {
|
||||
_events = _generateMockEvents();
|
||||
_isLoading = false;
|
||||
});
|
||||
} catch (e) {
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
});
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('Ошибка: $e')),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
List<CalendarEventModel> _generateMockEvents() {
|
||||
final now = DateTime.now();
|
||||
return [
|
||||
CalendarEventModel(
|
||||
id: '1',
|
||||
title: 'Командная встреча',
|
||||
startTime: DateTime(now.year, now.month, now.day, 10, 0),
|
||||
endTime: DateTime(now.year, now.month, now.day, 11, 0),
|
||||
color: '#2196F3',
|
||||
description: 'Еженедельный синк с командой',
|
||||
createdBy: 'user1',
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
),
|
||||
CalendarEventModel(
|
||||
id: '2',
|
||||
title: 'Дедлайн проекта',
|
||||
startTime: DateTime(now.year, now.month, now.day + 2, 18, 0),
|
||||
endTime: DateTime(now.year, now.month, now.day + 2, 19, 0),
|
||||
color: '#F44336',
|
||||
description: 'Сдача CRM проекта',
|
||||
createdBy: 'user1',
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
),
|
||||
CalendarEventModel(
|
||||
id: '3',
|
||||
title: 'Обед с клиентом',
|
||||
startTime: DateTime(now.year, now.month, now.day + 5, 13, 0),
|
||||
endTime: DateTime(now.year, now.month, now.day + 5, 14, 0),
|
||||
color: '#4CAF50',
|
||||
location: 'Ресторан "Москва"',
|
||||
createdBy: 'user1',
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
Future<void> _syncWithGoogleCalendar() async {
|
||||
setState(() {
|
||||
_isSyncing = true;
|
||||
});
|
||||
|
||||
try {
|
||||
// TODO: Реальная синхронизация с Google Calendar
|
||||
await googleCalendarService.getEvents(
|
||||
timeMin: DateTime(_focusedDay.year, _focusedDay.month, 1),
|
||||
timeMax: DateTime(_focusedDay.year, _focusedDay.month + 1, 0, 23, 59),
|
||||
);
|
||||
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('✅ Синхронизация успешна'),
|
||||
backgroundColor: Colors.green,
|
||||
),
|
||||
);
|
||||
await _loadEvents();
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('❌ Ошибка синхронизации: $e'),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
setState(() {
|
||||
_isSyncing = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
List<CalendarEventModel> _getEventsForDay(DateTime day) {
|
||||
return _events.where((event) {
|
||||
return event.startTime.year == day.year &&
|
||||
event.startTime.month == day.month &&
|
||||
event.startTime.day == day.day;
|
||||
}).toList();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
@@ -10,62 +147,248 @@ class CalendarScreen extends StatelessWidget {
|
||||
title: const Text('Календарь'),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.sync),
|
||||
onPressed: () {
|
||||
// Синхронизация с Google Calendar
|
||||
},
|
||||
icon: _isSyncing
|
||||
? const SizedBox(
|
||||
width: 20,
|
||||
height: 20,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
)
|
||||
: const Icon(Icons.sync),
|
||||
onPressed: _isSyncing ? null : () => _syncWithGoogleCalendar(),
|
||||
tooltip: 'Синхронизировать с Google Calendar',
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.add),
|
||||
onPressed: () => _showAddEventDialog(),
|
||||
tooltip: 'Добавить событие',
|
||||
),
|
||||
],
|
||||
),
|
||||
body: Column(
|
||||
body: _isLoading
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: Column(
|
||||
children: [
|
||||
_buildCalendarHeader(),
|
||||
Expanded(
|
||||
child: ListView.builder(
|
||||
padding: const EdgeInsets.all(16),
|
||||
itemCount: 5,
|
||||
itemBuilder: (context, index) {
|
||||
return _CalendarEventCard(index: index);
|
||||
TableCalendar(
|
||||
firstDay: DateTime.utc(2020, 1, 1),
|
||||
lastDay: DateTime.utc(2030, 12, 31),
|
||||
focusedDay: _focusedDay,
|
||||
selectedDayPredicate: (day) => isSameDay(_selectedDay, day),
|
||||
eventLoader: _getEventsForDay,
|
||||
onDaySelected: (selectedDay, focusedDay) {
|
||||
setState(() {
|
||||
_selectedDay = selectedDay;
|
||||
_focusedDay = focusedDay;
|
||||
});
|
||||
},
|
||||
onFormatChanged: (format) {
|
||||
// Обработка изменения формата
|
||||
},
|
||||
onPageChanged: (focusedDay) {
|
||||
_focusedDay = focusedDay;
|
||||
_loadEvents();
|
||||
},
|
||||
calendarStyle: CalendarStyle(
|
||||
outsideDaysVisible: false,
|
||||
weekendTextStyle: const TextStyle(color: Colors.red),
|
||||
holidayTextStyle: const TextStyle(color: Colors.red),
|
||||
selectedDecoration: const BoxDecoration(
|
||||
color: Colors.blue,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
todayDecoration: BoxDecoration(
|
||||
color: Colors.blue.withOpacity(0.3),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
markerDecoration: const BoxDecoration(
|
||||
color: Colors.orange,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
headerStyle: const HeaderStyle(
|
||||
formatButtonVisible: false,
|
||||
titleCentered: true,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
const Divider(),
|
||||
Expanded(
|
||||
child: _buildEventList(),
|
||||
),
|
||||
],
|
||||
),
|
||||
floatingActionButton: FloatingActionButton(
|
||||
onPressed: () {},
|
||||
onPressed: () => _showAddEventDialog(),
|
||||
child: const Icon(Icons.add),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCalendarHeader() {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
Widget _buildEventList() {
|
||||
final selectedEvents = _getEventsForDay(_selectedDay ?? _focusedDay);
|
||||
|
||||
if (selectedEvents.isEmpty) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.chevron_left),
|
||||
onPressed: () {},
|
||||
const Icon(
|
||||
Icons.event_busy,
|
||||
size: 64,
|
||||
color: Colors.grey,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const Text(
|
||||
'Март 2026',
|
||||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||
'Нет событий на этот день',
|
||||
style: TextStyle(color: Colors.grey, fontSize: 16),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.chevron_right),
|
||||
onPressed: () {},
|
||||
const SizedBox(height: 8),
|
||||
TextButton.icon(
|
||||
onPressed: () => _showAddEventDialog(),
|
||||
icon: const Icon(Icons.add),
|
||||
label: const Text('Добавить событие'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return ListView.builder(
|
||||
padding: const EdgeInsets.all(16),
|
||||
itemCount: selectedEvents.length,
|
||||
itemBuilder: (context, index) {
|
||||
final event = selectedEvents[index];
|
||||
return _EventCard(event: event);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
class _CalendarEventCard extends StatelessWidget {
|
||||
final int index;
|
||||
void _showAddEventDialog() {
|
||||
final titleController = TextEditingController();
|
||||
final descriptionController = TextEditingController();
|
||||
final locationController = TextEditingController();
|
||||
|
||||
const _CalendarEventCard({required this.index});
|
||||
TimeOfDay _startTime = const TimeOfDay(hour: 10, minute: 0);
|
||||
TimeOfDay _endTime = const TimeOfDay(hour: 11, minute: 0);
|
||||
|
||||
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(),
|
||||
prefixIcon: Icon(Icons.title),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextField(
|
||||
controller: descriptionController,
|
||||
maxLines: 3,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Описание',
|
||||
border: OutlineInputBorder(),
|
||||
prefixIcon: Icon(Icons.description),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextField(
|
||||
controller: locationController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Место',
|
||||
border: OutlineInputBorder(),
|
||||
prefixIcon: Icon(Icons.location_on),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: ListTile(
|
||||
title: const Text('Начало'),
|
||||
subtitle: Text(_startTime.format(context)),
|
||||
leading: const Icon(Icons.access_time),
|
||||
onTap: () async {
|
||||
final picked = await showTimePicker(
|
||||
context: context,
|
||||
initialTime: _startTime,
|
||||
);
|
||||
if (picked != null) {
|
||||
setDialogState(() {
|
||||
_startTime = picked;
|
||||
});
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: ListTile(
|
||||
title: const Text('Конец'),
|
||||
subtitle: Text(_endTime.format(context)),
|
||||
leading: const Icon(Icons.access_time),
|
||||
onTap: () async {
|
||||
final picked = await showTimePicker(
|
||||
context: context,
|
||||
initialTime: _endTime,
|
||||
);
|
||||
if (picked != null) {
|
||||
setDialogState(() {
|
||||
_endTime = picked;
|
||||
});
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text('Отмена'),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () async {
|
||||
if (titleController.text.isEmpty) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Введите название')),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: Сохранить событие
|
||||
Navigator.pop(context);
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('✅ Событие создано'),
|
||||
backgroundColor: Colors.green,
|
||||
),
|
||||
);
|
||||
|
||||
await _loadEvents();
|
||||
},
|
||||
child: const Text('Создать'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _EventCard extends StatelessWidget {
|
||||
final CalendarEventModel event;
|
||||
|
||||
const _EventCard({required this.event});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -76,19 +399,63 @@ class _CalendarEventCard extends StatelessWidget {
|
||||
width: 4,
|
||||
height: 40,
|
||||
decoration: BoxDecoration(
|
||||
color: _getEventColor(index),
|
||||
color: _hexToColor(event.color),
|
||||
borderRadius: BorderRadius.circular(2),
|
||||
),
|
||||
),
|
||||
title: Text('Событие #${index + 1}'),
|
||||
subtitle: Text('10:00 - 11:00'),
|
||||
trailing: const Icon(Icons.more_vert),
|
||||
title: Text(
|
||||
event.title,
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
subtitle: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'${_formatTime(event.startTime)} - ${_formatTime(event.endTime)}',
|
||||
style: const TextStyle(color: Colors.grey),
|
||||
),
|
||||
if (event.location.isNotEmpty) ...[
|
||||
const SizedBox(height: 2),
|
||||
Row(
|
||||
children: [
|
||||
const Icon(Icons.location_on, size: 12, color: Colors.grey),
|
||||
const SizedBox(width: 4),
|
||||
Flexible(
|
||||
child: Text(
|
||||
event.location,
|
||||
style: const TextStyle(color: Colors.grey, fontSize: 12),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
trailing: PopupMenuButton(
|
||||
onSelected: (value) {
|
||||
// TODO: Действия с событием
|
||||
},
|
||||
itemBuilder: (context) => [
|
||||
const PopupMenuItem(value: 'edit', child: Text('Редактировать')),
|
||||
const PopupMenuItem(value: 'delete', child: Text('Удалить')),
|
||||
],
|
||||
),
|
||||
isThreeLine: true,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Color _getEventColor(int index) {
|
||||
final colors = [Colors.blue, Colors.green, Colors.orange, Colors.purple];
|
||||
return colors[index % colors.length];
|
||||
Color _hexToColor(String hex) {
|
||||
hex = hex.replaceAll('#', '');
|
||||
if (hex.length == 6) {
|
||||
return Color(int.parse('0xFF$hex'));
|
||||
}
|
||||
return Colors.blue;
|
||||
}
|
||||
|
||||
String _formatTime(DateTime time) {
|
||||
return '${time.hour.toString().padLeft(2, '0')}:${time.minute.toString().padLeft(2, '0')}';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1096,6 +1096,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.1"
|
||||
simple_gesture_detector:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: simple_gesture_detector
|
||||
sha256: ba2cd5af24ff20a0b8d609cec3f40e5b0744d2a71804a2616ae086b9c19d19a3
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.2.1"
|
||||
sky_engine:
|
||||
dependency: transitive
|
||||
description: flutter
|
||||
@@ -1181,6 +1189,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "25.2.7"
|
||||
table_calendar:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: table_calendar
|
||||
sha256: b2896b7c86adf3a4d9c911d860120fe3dbe03c85db43b22fd61f14ee78cdbb63
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.3"
|
||||
term_glyph:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
||||
@@ -30,6 +30,7 @@ dependencies:
|
||||
# Google Calendar
|
||||
googleapis: ^13.2.0
|
||||
googleapis_auth: ^1.6.0
|
||||
table_calendar: ^3.0.9
|
||||
|
||||
# AI Services
|
||||
http: ^1.2.0
|
||||
|
||||
Reference in New Issue
Block a user