Add multi calendar support and protected web app

This commit is contained in:
zsolt
2026-08-13 14:49:00 +02:00
parent 17e83c032a
commit a6403bf9bb
51 changed files with 220126 additions and 31 deletions
+102 -6
View File
@@ -11,6 +11,7 @@ class _OtthonAppState extends State<OtthonApp> {
final store = AppJsonStore();
final weatherService = WeatherService();
List<Person> people = defaultPeople();
List<AppCalendar> appCalendars = defaultAppCalendars();
List<GoogleCalendarConnection> googleConnections = defaultGoogleConnections();
List<CalendarEvent> syncedEvents = [];
List<String> todoSuggestions = [];
@@ -38,6 +39,7 @@ class _OtthonAppState extends State<OtthonApp> {
if (!mounted) return;
setState(() {
people = settings.people;
appCalendars = settings.appCalendars;
googleConnections = settings.googleConnections;
syncedEvents = settings.syncedEvents;
todoSuggestions = settings.todoSuggestions;
@@ -54,6 +56,7 @@ class _OtthonAppState extends State<OtthonApp> {
await store.save(
AppSettings(
people: people,
appCalendars: appCalendars,
themeId: themeId,
languageCode: languageCode,
googleConnections: googleConnections,
@@ -98,6 +101,11 @@ class _OtthonAppState extends State<OtthonApp> {
_saveSettings();
}
void _updateAppCalendars(List<AppCalendar> nextCalendars) {
setState(() => appCalendars = nextCalendars);
_saveSettings();
}
void _updateTheme(String nextThemeId) {
setState(() => themeId = nextThemeId);
_saveSettings();
@@ -177,6 +185,7 @@ class _OtthonAppState extends State<OtthonApp> {
),
home: HomeShell(
people: people,
appCalendars: appCalendars,
googleConnections: googleConnections,
syncedEvents: syncedEvents,
todoSuggestions: todoSuggestions,
@@ -187,6 +196,7 @@ class _OtthonAppState extends State<OtthonApp> {
languageCode: languageCode,
loaded: loaded,
onPeopleChanged: _updatePeople,
onAppCalendarsChanged: _updateAppCalendars,
onThemeChanged: _updateTheme,
onLanguageChanged: _updateLanguage,
onGoogleConnectionsChanged: _updateGoogleConnections,
@@ -202,6 +212,7 @@ class HomeShell extends StatefulWidget {
const HomeShell({
super.key,
required this.people,
required this.appCalendars,
required this.googleConnections,
required this.syncedEvents,
required this.todoSuggestions,
@@ -212,6 +223,7 @@ class HomeShell extends StatefulWidget {
required this.languageCode,
required this.loaded,
required this.onPeopleChanged,
required this.onAppCalendarsChanged,
required this.onThemeChanged,
required this.onLanguageChanged,
required this.onGoogleConnectionsChanged,
@@ -221,6 +233,7 @@ class HomeShell extends StatefulWidget {
});
final List<Person> people;
final List<AppCalendar> appCalendars;
final List<GoogleCalendarConnection> googleConnections;
final List<CalendarEvent> syncedEvents;
final List<String> todoSuggestions;
@@ -231,6 +244,7 @@ class HomeShell extends StatefulWidget {
final String languageCode;
final bool loaded;
final ValueChanged<List<Person>> onPeopleChanged;
final ValueChanged<List<AppCalendar>> onAppCalendarsChanged;
final ValueChanged<String> onThemeChanged;
final ValueChanged<String> onLanguageChanged;
final ValueChanged<List<GoogleCalendarConnection>> onGoogleConnectionsChanged;
@@ -363,25 +377,51 @@ class _HomeShellState extends State<HomeShell> {
Widget build(BuildContext context) {
final wide = MediaQuery.sizeOf(context).width >= 850;
final text = AppText.of(widget.languageCode);
final mainCalendarEvents = allEvents
.where((event) => event.appCalendarId == null)
.toList();
final secondaryCalendarEvents = allEvents
.where((event) => event.appCalendarId != null)
.toList();
final labels = [
text.calendar,
...widget.appCalendars.map((calendar) => calendar.name),
text.shopping,
text.stock,
text.familyMembers,
text.style,
text.accounts,
];
final navIcons = [
icons.first,
...List.filled(widget.appCalendars.length, Icons.event_note_outlined),
...icons.skip(1),
];
final body = IndexedStack(
index: selected,
children: [
CalendarPage(
events: allEvents,
events: mainCalendarEvents,
people: widget.people,
weather: widget.weather,
onAdd: _addEvent,
secondaryEvents: secondaryCalendarEvents,
appCalendars: widget.appCalendars,
onAdd: () => _addEvent(),
onEdit: _editEvent,
onAssignPerson: _assignSyncedEventToPerson,
),
...widget.appCalendars.map(
(calendar) => CalendarPage(
title: calendar.name,
events: allEvents
.where((event) => event.appCalendarId == calendar.id)
.toList(),
people: widget.people,
weather: widget.weather,
onAdd: () => _addEvent(appCalendarId: calendar.id),
onEdit: _editEvent,
),
),
ShoppingPage(
items: shopping,
languageCode: widget.languageCode,
@@ -413,6 +453,7 @@ class _HomeShellState extends State<HomeShell> {
connections: widget.googleConnections,
cachedEvents: widget.syncedEvents,
people: widget.people,
appCalendars: widget.appCalendars,
jarvisSettings: widget.jarvisSettings,
serverClientId: widget.googleServerClientId,
languageCode: widget.languageCode,
@@ -517,6 +558,23 @@ class _HomeShellState extends State<HomeShell> {
],
),
),
IconButton(
tooltip: '\u00daj napt\u00e1r',
onPressed: _addAppCalendar,
icon: const Icon(
Icons.add_circle_outline,
size: 19,
),
style: IconButton.styleFrom(
backgroundColor: Colors.white,
foregroundColor: sage,
minimumSize: const Size(30, 30),
tapTargetSize:
MaterialTapTargetSize.shrinkWrap,
padding: EdgeInsets.zero,
),
),
const SizedBox(width: 4),
IconButton(
tooltip: text.jarvisSettings,
onPressed: _openJarvisSettings,
@@ -540,8 +598,8 @@ class _HomeShellState extends State<HomeShell> {
destinations: List.generate(
labels.length,
(i) => NavigationRailDestination(
icon: Icon(icons[i]),
selectedIcon: Icon(icons[i]),
icon: Icon(navIcons[i]),
selectedIcon: Icon(navIcons[i]),
label: Text(labels[i]),
),
),
@@ -728,7 +786,8 @@ class _HomeShellState extends State<HomeShell> {
destinations: List.generate(
labels.length,
(i) => NavigationDestination(
icon: Icon(icons[i]),
icon: Icon(navIcons[i]),
selectedIcon: Icon(navIcons[i]),
label: labels[i],
),
),
@@ -756,6 +815,30 @@ class _HomeShellState extends State<HomeShell> {
}
}
Future<void> _addAppCalendar() async {
final data = await showDialog<List<String>>(
context: context,
builder: (_) => const QuickAddDialog(
title: 'Napt\u00e1r hozz\u00e1ad\u00e1sa',
firstLabel: 'Napt\u00e1r neve',
secondLabel: 'Le\u00edr\u00e1s (opcion\u00e1lis)',
),
);
final name = data == null ? '' : data.first.trim();
if (name.isEmpty) return;
final color =
appCalendarColors[widget.appCalendars.length %
appCalendarColors.length];
final nextCalendar = AppCalendar(
id: uniqueId(),
name: name,
colorValue: color.toARGB32(),
);
widget.onAppCalendarsChanged([...widget.appCalendars, nextCalendar]);
setState(() => selected = widget.appCalendars.length + 1);
}
Future<void> _openJarvisSettings() async {
final result = await showDialog<JarvisSettings>(
context: context,
@@ -963,11 +1046,12 @@ class _HomeShellState extends State<HomeShell> {
);
}
Future<void> _addEvent() async {
Future<void> _addEvent({String? appCalendarId}) async {
final data = await showDialog<EventInput>(
context: context,
builder: (_) => EventAddDialog(
people: widget.people,
appCalendars: widget.appCalendars,
todoSuggestions: widget.todoSuggestions,
),
);
@@ -986,6 +1070,7 @@ class _HomeShellState extends State<HomeShell> {
.color,
personId: assignedPersonId,
date: data.date,
appCalendarId: data.appCalendarId ?? appCalendarId,
note: data.note,
hasTodos: data.hasTodos,
todos: data.todos,
@@ -1001,6 +1086,7 @@ class _HomeShellState extends State<HomeShell> {
context: context,
builder: (_) => EventAddDialog(
people: widget.people,
appCalendars: widget.appCalendars,
event: event,
todoSuggestions: widget.todoSuggestions,
),
@@ -1017,6 +1103,8 @@ class _HomeShellState extends State<HomeShell> {
date: data.date,
color: color,
personId: assignedPersonId,
appCalendarId: data.appCalendarId,
clearAppCalendar: data.appCalendarId == null,
note: data.note,
hasTodos: data.hasTodos,
todos: data.todos,
@@ -1032,6 +1120,14 @@ class _HomeShellState extends State<HomeShell> {
});
if (event.isSynced) {
if (widget.googleConnections.isNotEmpty) {
final currentConnection = widget.googleConnections.first;
final nextEventPersonIds = {...currentConnection.eventPersonIds};
nextEventPersonIds[importedEventKey(event)] = assignedPersonId;
widget.onGoogleConnectionsChanged([
currentConnection.copyWith(eventPersonIds: nextEventPersonIds),
]);
}
final nextSyncedEvents = [
for (final syncedEvent in widget.syncedEvents)
if (identical(syncedEvent, event)) updated else syncedEvent,