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); var builder = WebApplication.CreateBuilder(args);
builder.Services.AddOpenApi(); 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 => builder.Services.AddHttpClient("open-food-facts", client =>
{ {
client.BaseAddress = new Uri("https://world.openfoodfacts.org/"); client.BaseAddress = new Uri("https://world.openfoodfacts.org/");
@@ -52,6 +65,7 @@ if (app.Environment.IsDevelopment())
app.MapOpenApi(); app.MapOpenApi();
} }
app.UseCors("CalendarWeb");
app.UseAuthentication(); app.UseAuthentication();
app.UseAuthorization(); app.UseAuthorization();
+1 -1
View File
@@ -37,6 +37,6 @@ _flutter.buildConfig = {"engineRevision":"a10d8ac38de835021c8d2f920dbf50a920ccc0
_flutter.loader.load({ _flutter.loader.load({
serviceWorkerSettings: { 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; 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 { location = /health {
access_log off; access_log off;
add_header Content-Type text/plain; add_header Content-Type text/plain;
return 200 "ok\n"; return 200 "ok\n";
} }
} }
+44 -1
View File
@@ -520,6 +520,8 @@ class _HomeShellState extends State<HomeShell> {
languageCode: widget.languageCode, languageCode: widget.languageCode,
onChanged: widget.onGoogleConnectionsChanged, onChanged: widget.onGoogleConnectionsChanged,
onSyncedEventsChanged: widget.onSyncedEventsChanged, onSyncedEventsChanged: widget.onSyncedEventsChanged,
onWorkspaceStateShouldUpload: () =>
_uploadWorkspaceState(silent: true),
), ),
], ],
); );
@@ -969,6 +971,27 @@ class _HomeShellState extends State<HomeShell> {
.map(Person.fromJson) .map(Person.fromJson)
.toList() .toList()
: widget.people; : 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 final nextShopping = state['shopping'] is List
? (state['shopping'] as List) ? (state['shopping'] as List)
.whereType<Map<String, dynamic>>() .whereType<Map<String, dynamic>>()
@@ -991,6 +1014,10 @@ class _HomeShellState extends State<HomeShell> {
..addAll(nextStock); ..addAll(nextStock);
}); });
widget.onPeopleChanged(nextPeople); widget.onPeopleChanged(nextPeople);
widget.onAppCalendarsChanged(nextAppCalendars);
widget.onGoogleConnectionsChanged(nextGoogleConnections);
widget.onSyncedEventsChanged(nextSyncedEvents);
widget.onTodoSuggestionsChanged(nextTodoSuggestions);
if (!silent) { if (!silent) {
_showWorkspaceMessage('K\u00f6z\u00f6s munkat\u00e9r let\u00f6ltve.'); _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(); final workspaceToken = widget.jarvisSettings.workspaceToken?.trim();
if (workspaceToken == null || workspaceToken.isEmpty) { if (workspaceToken == null || workspaceToken.isEmpty) {
if (!silent) {
_showWorkspaceMessage('Adj meg k\u00f6z\u00f6s munkat\u00e9r tokent.'); _showWorkspaceMessage('Adj meg k\u00f6z\u00f6s munkat\u00e9r tokent.');
}
return; return;
} }
@@ -1019,6 +1048,16 @@ class _HomeShellState extends State<HomeShell> {
'version': 1, 'version': 1,
'updatedAt': DateTime.now().toIso8601String(), 'updatedAt': DateTime.now().toIso8601String(),
'people': widget.people.map((person) => person.toJson()).toList(), '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(), 'shopping': shopping.map((item) => item.toJson()).toList(),
'stock': stock.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) { if (response.statusCode != 200) {
throw Exception('workspace PUT ${response.statusCode}'); throw Exception('workspace PUT ${response.statusCode}');
} }
if (!silent) {
_showWorkspaceMessage('K\u00f6z\u00f6s munkat\u00e9r felt\u00f6ltve.'); _showWorkspaceMessage('K\u00f6z\u00f6s munkat\u00e9r felt\u00f6ltve.');
}
} catch (error) { } catch (error) {
if (!silent) {
_showWorkspaceMessage('Workspace hiba: $error'); _showWorkspaceMessage('Workspace hiba: $error');
}
} finally { } finally {
if (mounted) setState(() => workspaceSyncing = false); if (mounted) setState(() => workspaceSyncing = false);
} }
+50 -18
View File
@@ -741,9 +741,10 @@ class WeekBoard extends StatelessWidget {
], ],
), ),
const SizedBox(height: 10), const SizedBox(height: 10),
Row( LayoutBuilder(
crossAxisAlignment: CrossAxisAlignment.start, builder: (context, constraints) {
children: List.generate(7, (index) { final vertical = constraints.maxWidth < 760;
final columns = List.generate(7, (index) {
final date = weekStart.add(Duration(days: index)); final date = weekStart.add(Duration(days: index));
final dayEvents = final dayEvents =
events events
@@ -753,10 +754,7 @@ class WeekBoard extends StatelessWidget {
final daySecondaryEvents = secondaryEvents final daySecondaryEvents = secondaryEvents
.where((event) => isSameDate(event.date, date)) .where((event) => isSameDate(event.date, date))
.toList(); .toList();
return Expanded( return WeekDayColumn(
child: Padding(
padding: EdgeInsets.only(right: index == 6 ? 0 : 6),
child: WeekDayColumn(
title: days[index], title: days[index],
dateLabel: formatShortDate(date), dateLabel: formatShortDate(date),
active: selectedDay == index, active: selectedDay == index,
@@ -764,11 +762,35 @@ class WeekBoard extends StatelessWidget {
secondaryEvents: daySecondaryEvents, secondaryEvents: daySecondaryEvents,
appCalendars: appCalendars, appCalendars: appCalendars,
people: people, people: people,
compact: vertical,
onTap: () => onSelectDay(index), 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.secondaryEvents,
required this.appCalendars, required this.appCalendars,
required this.people, required this.people,
this.compact = false,
required this.onTap, required this.onTap,
}); });
@@ -797,6 +820,7 @@ class WeekDayColumn extends StatelessWidget {
final List<CalendarEvent> secondaryEvents; final List<CalendarEvent> secondaryEvents;
final List<AppCalendar> appCalendars; final List<AppCalendar> appCalendars;
final List<Person> people; final List<Person> people;
final bool compact;
final VoidCallback onTap; final VoidCallback onTap;
@override @override
@@ -807,7 +831,7 @@ class WeekDayColumn extends StatelessWidget {
child: AnimatedContainer( child: AnimatedContainer(
duration: const Duration(milliseconds: 180), duration: const Duration(milliseconds: 180),
width: double.infinity, width: double.infinity,
constraints: const BoxConstraints(minHeight: 190), constraints: BoxConstraints(minHeight: compact ? 92 : 190),
padding: const EdgeInsets.all(8), padding: const EdgeInsets.all(8),
decoration: BoxDecoration( decoration: BoxDecoration(
color: active ? mint : const Color(0xFFF8F8F5), color: active ? mint : const Color(0xFFF8F8F5),
@@ -820,7 +844,10 @@ class WeekDayColumn extends StatelessWidget {
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text( Row(
children: [
Expanded(
child: Text(
title, title,
maxLines: 1, maxLines: 1,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
@@ -830,10 +857,13 @@ class WeekDayColumn extends StatelessWidget {
fontWeight: FontWeight.w900, fontWeight: FontWeight.w900,
), ),
), ),
const SizedBox(height: 7), ),
const SizedBox(width: 8),
Container( Container(
width: double.infinity, padding: EdgeInsets.symmetric(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 7), horizontal: compact ? 10 : 8,
vertical: compact ? 5 : 7,
),
alignment: Alignment.center, alignment: Alignment.center,
decoration: BoxDecoration( decoration: BoxDecoration(
color: active ? sage : Colors.white, color: active ? sage : Colors.white,
@@ -855,12 +885,14 @@ class WeekDayColumn extends StatelessWidget {
softWrap: false, softWrap: false,
style: TextStyle( style: TextStyle(
color: active ? Colors.white : sage, color: active ? Colors.white : sage,
fontSize: 17, fontSize: compact ? 15 : 17,
letterSpacing: .5, letterSpacing: .5,
fontWeight: FontWeight.w900, fontWeight: FontWeight.w900,
), ),
), ),
), ),
],
),
const SizedBox(height: 10), const SizedBox(height: 10),
if (secondaryEvents.isNotEmpty) ...[ if (secondaryEvents.isNotEmpty) ...[
CalendarDayIndicatorStrip( CalendarDayIndicatorStrip(
@@ -870,8 +902,8 @@ class WeekDayColumn extends StatelessWidget {
const SizedBox(height: 8), const SizedBox(height: 8),
], ],
if (events.isEmpty) if (events.isEmpty)
const SizedBox( SizedBox(
height: 92, height: compact ? 32 : 92,
child: Center( child: Center(
child: Text( child: Text(
'nincs program', 'nincs program',
+90
View File
@@ -1057,6 +1057,31 @@ class _BarcodeScanDialogState extends State<BarcodeScanDialog> {
content: SizedBox( content: SizedBox(
width: 520, width: 520,
height: 360, height: 360,
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(
'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( child: ClipRRect(
borderRadius: BorderRadius.circular(18), borderRadius: BorderRadius.circular(18),
child: Stack( child: Stack(
@@ -1064,6 +1089,11 @@ class _BarcodeScanDialogState extends State<BarcodeScanDialog> {
children: [ children: [
MobileScanner( MobileScanner(
controller: controller, controller: controller,
placeholderBuilder: (context) =>
const ScannerPermissionHint(),
errorBuilder: (context, error) =>
ScannerPermissionHint(error: error),
onDetectError: (error, stackTrace) {},
onDetect: (capture) { onDetect: (capture) {
if (handled) return; if (handled) return;
for (final code in capture.barcodes) { for (final code in capture.barcodes) {
@@ -1101,6 +1131,9 @@ class _BarcodeScanDialogState extends State<BarcodeScanDialog> {
), ),
), ),
), ),
],
),
),
actions: [ actions: [
TextButton( TextButton(
onPressed: () => Navigator.pop(context), onPressed: () => Navigator.pop(context),
@@ -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 = typedef JarvisRegisterCallback =
Future<JarvisSettings> Function(JarvisSettings settings, String deviceName); Future<JarvisSettings> Function(JarvisSettings settings, String deviceName);
typedef JarvisStatusCallback = typedef JarvisStatusCallback =
+3
View File
@@ -12,6 +12,7 @@ class AccountsPage extends StatefulWidget {
required this.languageCode, required this.languageCode,
required this.onChanged, required this.onChanged,
required this.onSyncedEventsChanged, required this.onSyncedEventsChanged,
this.onWorkspaceStateShouldUpload,
}); });
final List<GoogleCalendarConnection> connections; final List<GoogleCalendarConnection> connections;
@@ -23,6 +24,7 @@ class AccountsPage extends StatefulWidget {
final String languageCode; final String languageCode;
final ValueChanged<List<GoogleCalendarConnection>> onChanged; final ValueChanged<List<GoogleCalendarConnection>> onChanged;
final ValueChanged<List<CalendarEvent>> onSyncedEventsChanged; final ValueChanged<List<CalendarEvent>> onSyncedEventsChanged;
final VoidCallback? onWorkspaceStateShouldUpload;
@override @override
State<AccountsPage> createState() => _AccountsPageState(); 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.' : '${synced.length} Google esem\u00e9ny szinkroniz\u00e1lva \u00e9s JSON cache-be mentve.'
'${syncDetails.isEmpty ? '' : ' R\u00e9szletek: ${syncDetails.join('; ')}.'}'; '${syncDetails.isEmpty ? '' : ' R\u00e9szletek: ${syncDetails.join('; ')}.'}';
}); });
widget.onWorkspaceStateShouldUpload?.call();
} catch (error) { } catch (error) {
setState(() { setState(() {
syncing = false; syncing = false;