Improve web calendar layout and shared sync state

This commit is contained in:
zsolt
2026-08-13 15:25:50 +02:00
parent 5e75c01b3f
commit 332657fc99
9 changed files with 40551 additions and 40265 deletions
+14
View File
@@ -12,6 +12,19 @@ using Microsoft.EntityFrameworkCore;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddOpenApi();
builder.Services.AddCors(options =>
{
options.AddPolicy("CalendarWeb", policy =>
{
policy
.WithOrigins(
"https://calendar.a-web.hu",
"http://localhost:5000",
"http://localhost:8080")
.AllowAnyHeader()
.AllowAnyMethod();
});
});
builder.Services.AddHttpClient("open-food-facts", client =>
{
client.BaseAddress = new Uri("https://world.openfoodfacts.org/");
@@ -52,6 +65,7 @@ if (app.Environment.IsDevelopment())
app.MapOpenApi();
}
app.UseCors("CalendarWeb");
app.UseAuthentication();
app.UseAuthorization();
+1 -1
View File
@@ -37,6 +37,6 @@ _flutter.buildConfig = {"engineRevision":"a10d8ac38de835021c8d2f920dbf50a920ccc0
_flutter.loader.load({
serviceWorkerSettings: {
serviceWorkerVersion: "2721834507" /* Flutter's service worker is deprecated and will be removed in a future Flutter release. */
serviceWorkerVersion: "1030817064" /* Flutter's service worker is deprecated and will be removed in a future Flutter release. */
}
});
File diff suppressed because one or more lines are too long
+10 -1
View File
@@ -8,10 +8,19 @@ server {
try_files $uri $uri/ /index.html;
}
location = /favicon.png {
add_header Cache-Control "no-cache, no-store, must-revalidate";
try_files /favicon.png =404;
}
location = /manifest.json {
add_header Cache-Control "no-cache, no-store, must-revalidate";
try_files /manifest.json =404;
}
location = /health {
access_log off;
add_header Content-Type text/plain;
return 200 "ok\n";
}
}
+47 -4
View File
@@ -520,6 +520,8 @@ class _HomeShellState extends State<HomeShell> {
languageCode: widget.languageCode,
onChanged: widget.onGoogleConnectionsChanged,
onSyncedEventsChanged: widget.onSyncedEventsChanged,
onWorkspaceStateShouldUpload: () =>
_uploadWorkspaceState(silent: true),
),
],
);
@@ -969,6 +971,27 @@ class _HomeShellState extends State<HomeShell> {
.map(Person.fromJson)
.toList()
: widget.people;
final nextAppCalendars = state['appCalendars'] is List
? (state['appCalendars'] as List)
.whereType<Map<String, dynamic>>()
.map(AppCalendar.fromJson)
.toList()
: widget.appCalendars;
final nextGoogleConnections = state['googleConnections'] is List
? (state['googleConnections'] as List)
.whereType<Map<String, dynamic>>()
.map(GoogleCalendarConnection.fromJson)
.toList()
: widget.googleConnections;
final nextSyncedEvents = state['syncedEvents'] is List
? (state['syncedEvents'] as List)
.whereType<Map<String, dynamic>>()
.map(CalendarEvent.fromJson)
.toList()
: widget.syncedEvents;
final nextTodoSuggestions = state['todoSuggestions'] is List
? (state['todoSuggestions'] as List).whereType<String>().toList()
: widget.todoSuggestions;
final nextShopping = state['shopping'] is List
? (state['shopping'] as List)
.whereType<Map<String, dynamic>>()
@@ -991,6 +1014,10 @@ class _HomeShellState extends State<HomeShell> {
..addAll(nextStock);
});
widget.onPeopleChanged(nextPeople);
widget.onAppCalendarsChanged(nextAppCalendars);
widget.onGoogleConnectionsChanged(nextGoogleConnections);
widget.onSyncedEventsChanged(nextSyncedEvents);
widget.onTodoSuggestionsChanged(nextTodoSuggestions);
if (!silent) {
_showWorkspaceMessage('K\u00f6z\u00f6s munkat\u00e9r let\u00f6ltve.');
}
@@ -1003,10 +1030,12 @@ class _HomeShellState extends State<HomeShell> {
}
}
Future<void> _uploadWorkspaceState() async {
Future<void> _uploadWorkspaceState({bool silent = false}) async {
final workspaceToken = widget.jarvisSettings.workspaceToken?.trim();
if (workspaceToken == null || workspaceToken.isEmpty) {
_showWorkspaceMessage('Adj meg k\u00f6z\u00f6s munkat\u00e9r tokent.');
if (!silent) {
_showWorkspaceMessage('Adj meg k\u00f6z\u00f6s munkat\u00e9r tokent.');
}
return;
}
@@ -1019,6 +1048,16 @@ class _HomeShellState extends State<HomeShell> {
'version': 1,
'updatedAt': DateTime.now().toIso8601String(),
'people': widget.people.map((person) => person.toJson()).toList(),
'appCalendars': widget.appCalendars
.map((calendar) => calendar.toJson())
.toList(),
'googleConnections': widget.googleConnections
.map((connection) => connection.toJson())
.toList(),
'syncedEvents': widget.syncedEvents
.map((event) => event.toJson())
.toList(),
'todoSuggestions': widget.todoSuggestions,
'shopping': shopping.map((item) => item.toJson()).toList(),
'stock': stock.map((item) => item.toJson()).toList(),
});
@@ -1036,9 +1075,13 @@ class _HomeShellState extends State<HomeShell> {
if (response.statusCode != 200) {
throw Exception('workspace PUT ${response.statusCode}');
}
_showWorkspaceMessage('K\u00f6z\u00f6s munkat\u00e9r felt\u00f6ltve.');
if (!silent) {
_showWorkspaceMessage('K\u00f6z\u00f6s munkat\u00e9r felt\u00f6ltve.');
}
} catch (error) {
_showWorkspaceMessage('Workspace hiba: $error');
if (!silent) {
_showWorkspaceMessage('Workspace hiba: $error');
}
} finally {
if (mounted) setState(() => workspaceSyncing = false);
}
+101 -69
View File
@@ -741,34 +741,56 @@ class WeekBoard extends StatelessWidget {
],
),
const SizedBox(height: 10),
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: List.generate(7, (index) {
final date = weekStart.add(Duration(days: index));
final dayEvents =
events
.where((event) => isSameDate(event.date, date))
.toList()
..sort((a, b) => a.start.compareTo(b.start));
final daySecondaryEvents = secondaryEvents
.where((event) => isSameDate(event.date, date))
.toList();
return Expanded(
child: Padding(
padding: EdgeInsets.only(right: index == 6 ? 0 : 6),
child: WeekDayColumn(
title: days[index],
dateLabel: formatShortDate(date),
active: selectedDay == index,
events: dayEvents,
secondaryEvents: daySecondaryEvents,
appCalendars: appCalendars,
people: people,
onTap: () => onSelectDay(index),
),
),
LayoutBuilder(
builder: (context, constraints) {
final vertical = constraints.maxWidth < 760;
final columns = List.generate(7, (index) {
final date = weekStart.add(Duration(days: index));
final dayEvents =
events
.where((event) => isSameDate(event.date, date))
.toList()
..sort((a, b) => a.start.compareTo(b.start));
final daySecondaryEvents = secondaryEvents
.where((event) => isSameDate(event.date, date))
.toList();
return WeekDayColumn(
title: days[index],
dateLabel: formatShortDate(date),
active: selectedDay == index,
events: dayEvents,
secondaryEvents: daySecondaryEvents,
appCalendars: appCalendars,
people: people,
compact: vertical,
onTap: () => onSelectDay(index),
);
});
if (vertical) {
return Column(
children: [
for (var i = 0; i < columns.length; i++) ...[
columns[i],
if (i != columns.length - 1) const SizedBox(height: 8),
],
],
);
}
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
for (var i = 0; i < columns.length; i++)
Expanded(
child: Padding(
padding: EdgeInsets.only(right: i == 6 ? 0 : 6),
child: columns[i],
),
),
],
);
}),
},
),
],
),
@@ -787,6 +809,7 @@ class WeekDayColumn extends StatelessWidget {
required this.secondaryEvents,
required this.appCalendars,
required this.people,
this.compact = false,
required this.onTap,
});
@@ -797,6 +820,7 @@ class WeekDayColumn extends StatelessWidget {
final List<CalendarEvent> secondaryEvents;
final List<AppCalendar> appCalendars;
final List<Person> people;
final bool compact;
final VoidCallback onTap;
@override
@@ -807,7 +831,7 @@ class WeekDayColumn extends StatelessWidget {
child: AnimatedContainer(
duration: const Duration(milliseconds: 180),
width: double.infinity,
constraints: const BoxConstraints(minHeight: 190),
constraints: BoxConstraints(minHeight: compact ? 92 : 190),
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: active ? mint : const Color(0xFFF8F8F5),
@@ -820,46 +844,54 @@ class WeekDayColumn extends StatelessWidget {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
color: ink,
fontSize: 14,
fontWeight: FontWeight.w900,
),
),
const SizedBox(height: 7),
Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 7),
alignment: Alignment.center,
decoration: BoxDecoration(
color: active ? sage : Colors.white,
borderRadius: BorderRadius.circular(14),
boxShadow: active
? [
BoxShadow(
color: sage.withValues(alpha: .18),
blurRadius: 12,
offset: const Offset(0, 5),
),
]
: null,
),
child: Text(
dateLabel,
maxLines: 1,
overflow: TextOverflow.fade,
softWrap: false,
style: TextStyle(
color: active ? Colors.white : sage,
fontSize: 17,
letterSpacing: .5,
fontWeight: FontWeight.w900,
Row(
children: [
Expanded(
child: Text(
title,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
color: ink,
fontSize: 14,
fontWeight: FontWeight.w900,
),
),
),
),
const SizedBox(width: 8),
Container(
padding: EdgeInsets.symmetric(
horizontal: compact ? 10 : 8,
vertical: compact ? 5 : 7,
),
alignment: Alignment.center,
decoration: BoxDecoration(
color: active ? sage : Colors.white,
borderRadius: BorderRadius.circular(14),
boxShadow: active
? [
BoxShadow(
color: sage.withValues(alpha: .18),
blurRadius: 12,
offset: const Offset(0, 5),
),
]
: null,
),
child: Text(
dateLabel,
maxLines: 1,
overflow: TextOverflow.fade,
softWrap: false,
style: TextStyle(
color: active ? Colors.white : sage,
fontSize: compact ? 15 : 17,
letterSpacing: .5,
fontWeight: FontWeight.w900,
),
),
),
],
),
const SizedBox(height: 10),
if (secondaryEvents.isNotEmpty) ...[
@@ -870,8 +902,8 @@ class WeekDayColumn extends StatelessWidget {
const SizedBox(height: 8),
],
if (events.isEmpty)
const SizedBox(
height: 92,
SizedBox(
height: compact ? 32 : 92,
child: Center(
child: Text(
'nincs program',
+127 -37
View File
@@ -1057,48 +1057,81 @@ class _BarcodeScanDialogState extends State<BarcodeScanDialog> {
content: SizedBox(
width: 520,
height: 360,
child: ClipRRect(
borderRadius: BorderRadius.circular(18),
child: Stack(
fit: StackFit.expand,
children: [
MobileScanner(
controller: controller,
onDetect: (capture) {
if (handled) return;
for (final code in capture.barcodes) {
final value = code.rawValue;
if (value == null || value.trim().isEmpty) continue;
handled = true;
Navigator.pop(context, value);
break;
}
},
),
const Align(
alignment: Alignment.bottomCenter,
child: Padding(
padding: EdgeInsets.all(12),
child: DecoratedBox(
decoration: BoxDecoration(
color: Colors.black54,
borderRadius: BorderRadius.all(Radius.circular(16)),
),
child: Padding(
padding: EdgeInsets.symmetric(
horizontal: 12,
vertical: 8,
),
child: Column(
children: [
if (kIsWeb)
Container(
margin: const EdgeInsets.only(bottom: 10),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: mint,
borderRadius: BorderRadius.circular(16),
),
child: const Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(Icons.info_outline, color: sage, size: 20),
SizedBox(width: 8),
Expanded(
child: Text(
'Tartsd a vonalk\u00f3dot az el\u0151lapi kamera el\u00e9.',
style: TextStyle(color: Colors.white),
'Weben a kamera enged\u00e9lyt a b\u00f6ng\u00e9sz\u0151 k\u00e9ri. iPhone/iPad Safari alatt: aA / Webhelybe\u00e1ll\u00edt\u00e1sok / Kamera: Enged\u00e9lyez\u00e9s, majd t\u00f6ltsd \u00fajra az oldalt.',
style: TextStyle(color: ink, fontSize: 12.5),
),
),
),
],
),
),
],
),
Expanded(
child: ClipRRect(
borderRadius: BorderRadius.circular(18),
child: Stack(
fit: StackFit.expand,
children: [
MobileScanner(
controller: controller,
placeholderBuilder: (context) =>
const ScannerPermissionHint(),
errorBuilder: (context, error) =>
ScannerPermissionHint(error: error),
onDetectError: (error, stackTrace) {},
onDetect: (capture) {
if (handled) return;
for (final code in capture.barcodes) {
final value = code.rawValue;
if (value == null || value.trim().isEmpty) continue;
handled = true;
Navigator.pop(context, value);
break;
}
},
),
const Align(
alignment: Alignment.bottomCenter,
child: Padding(
padding: EdgeInsets.all(12),
child: DecoratedBox(
decoration: BoxDecoration(
color: Colors.black54,
borderRadius: BorderRadius.all(Radius.circular(16)),
),
child: Padding(
padding: EdgeInsets.symmetric(
horizontal: 12,
vertical: 8,
),
child: Text(
'Tartsd a vonalk\u00f3dot az el\u0151lapi kamera el\u00e9.',
style: TextStyle(color: Colors.white),
),
),
),
),
),
],
),
),
),
],
),
),
actions: [
@@ -1111,6 +1144,63 @@ class _BarcodeScanDialogState extends State<BarcodeScanDialog> {
}
}
class ScannerPermissionHint extends StatelessWidget {
const ScannerPermissionHint({super.key, this.error});
final MobileScannerException? error;
@override
Widget build(BuildContext context) {
return Container(
color: const Color(0xFF172B27),
padding: const EdgeInsets.all(20),
child: Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 360),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(
Icons.photo_camera_outlined,
color: Colors.white,
size: 42,
),
const SizedBox(height: 14),
Text(
error == null
? 'Kamera enged\u00e9ly k\u00e9r\u00e9se...'
: 'A kamera nem el\u00e9rhet\u0151',
textAlign: TextAlign.center,
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.w900,
fontSize: 18,
),
),
const SizedBox(height: 8),
Text(
error == null
? 'Ha a b\u00f6ng\u00e9sz\u0151 r\u00e1k\u00e9rdez, enged\u00e9lyezd a kamer\u00e1t a vonalk\u00f3d beolvas\u00e1s\u00e1hoz.'
: 'Enged\u00e9lyezd a kamer\u00e1t a b\u00f6ng\u00e9sz\u0151 webhelybe\u00e1ll\u00edt\u00e1sain\u00e1l, majd nyisd meg \u00fajra a scannert.',
textAlign: TextAlign.center,
style: const TextStyle(color: Colors.white70, height: 1.35),
),
if (error != null) ...[
const SizedBox(height: 10),
Text(
error!.errorCode.name,
textAlign: TextAlign.center,
style: const TextStyle(color: Colors.white38, fontSize: 11),
),
],
],
),
),
),
);
}
}
typedef JarvisRegisterCallback =
Future<JarvisSettings> Function(JarvisSettings settings, String deviceName);
typedef JarvisStatusCallback =
+3
View File
@@ -12,6 +12,7 @@ class AccountsPage extends StatefulWidget {
required this.languageCode,
required this.onChanged,
required this.onSyncedEventsChanged,
this.onWorkspaceStateShouldUpload,
});
final List<GoogleCalendarConnection> connections;
@@ -23,6 +24,7 @@ class AccountsPage extends StatefulWidget {
final String languageCode;
final ValueChanged<List<GoogleCalendarConnection>> onChanged;
final ValueChanged<List<CalendarEvent>> onSyncedEventsChanged;
final VoidCallback? onWorkspaceStateShouldUpload;
@override
State<AccountsPage> createState() => _AccountsPageState();
@@ -429,6 +431,7 @@ class _AccountsPageState extends State<AccountsPage>
: '${synced.length} Google esem\u00e9ny szinkroniz\u00e1lva \u00e9s JSON cache-be mentve.'
'${syncDetails.isEmpty ? '' : ' R\u00e9szletek: ${syncDetails.join('; ')}.'}';
});
widget.onWorkspaceStateShouldUpload?.call();
} catch (error) {
setState(() {
syncing = false;