Improve web calendar layout and shared sync state
This commit is contained in:
@@ -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();
|
||||
|
||||
|
||||
Binary file not shown.
@@ -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. */
|
||||
}
|
||||
});
|
||||
|
||||
+40248
-40153
File diff suppressed because one or more lines are too long
@@ -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
@@ -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
@@ -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
@@ -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 =
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user