Add web profile config and branded PWA icon

This commit is contained in:
zsolt
2026-08-13 15:08:42 +02:00
parent a6403bf9bb
commit 5e75c01b3f
19 changed files with 46337 additions and 46053 deletions
+1
View File
@@ -35,6 +35,7 @@ migrate_working_dir/
*.tgz
/deploy/gitlab/
/deploy/gitea/
/deploy/calendar-web/app/runtime-config.json
# Symbolication related
app.*.symbols
Binary file not shown.

Before

Width:  |  Height:  |  Size: 917 B

After

Width:  |  Height:  |  Size: 436 B

+1 -1
View File
@@ -37,6 +37,6 @@ _flutter.buildConfig = {"engineRevision":"a10d8ac38de835021c8d2f920dbf50a920ccc0
_flutter.loader.load({
serviceWorkerSettings: {
serviceWorkerVersion: "128966474" /* Flutter's service worker is deprecated and will be removed in a future Flutter release. */
serviceWorkerVersion: "2721834507" /* Flutter's service worker is deprecated and will be removed in a future Flutter release. */
}
});
Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.1 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.5 KiB

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 11 KiB

+3 -3
View File
@@ -18,18 +18,18 @@
<meta charset="UTF-8">
<meta content="IE=Edge" http-equiv="X-UA-Compatible">
<meta name="description" content="A new Flutter project.">
<meta name="description" content="KözösTér családi naptár és otthoni organizer.">
<!-- iOS meta tags & icons -->
<meta name="mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black">
<meta name="apple-mobile-web-app-title" content="otthon_naptar">
<meta name="apple-mobile-web-app-title" content="KözösTér">
<link rel="apple-touch-icon" href="icons/Icon-192.png">
<!-- Favicon -->
<link rel="icon" type="image/png" href="favicon.png"/>
<title>otthon_naptar</title>
<title>KözösTér</title>
<link rel="manifest" href="manifest.json">
</head>
<body>
File diff suppressed because one or more lines are too long
+5 -5
View File
@@ -1,11 +1,11 @@
{
"name": "otthon_naptar",
"short_name": "otthon_naptar",
"name": "KözösTér",
"short_name": "KözösTér",
"start_url": ".",
"display": "standalone",
"background_color": "#0175C2",
"theme_color": "#0175C2",
"description": "A new Flutter project.",
"background_color": "#F7F5EF",
"theme_color": "#18332D",
"description": "Családi naptár és otthoni organizer.",
"orientation": "portrait-primary",
"prefer_related_applications": false,
"icons": [
+180 -10
View File
@@ -50,6 +50,7 @@ class _OtthonAppState extends State<OtthonApp> {
loaded = true;
});
_loadRemoteAppConfig();
_loadWebRuntimeConfig();
}
Future<void> _saveSettings() async {
@@ -96,6 +97,42 @@ class _OtthonAppState extends State<OtthonApp> {
}
}
Future<void> _loadWebRuntimeConfig() async {
if (!kIsWeb) return;
try {
final response = await http
.get(Uri.parse('/runtime-config.json'))
.timeout(const Duration(seconds: 8));
if (response.statusCode != 200) return;
final payload = jsonDecode(response.body);
if (payload is! Map<String, dynamic>) return;
final apiUrl = (payload['jarvisApiUrl'] as String?)?.trim();
final apiToken = (payload['jarvisApiToken'] as String?)?.trim();
final workspaceToken = (payload['workspaceToken'] as String?)?.trim();
if (apiUrl == null ||
apiUrl.isEmpty ||
apiToken == null ||
apiToken.isEmpty ||
workspaceToken == null ||
workspaceToken.isEmpty) {
return;
}
final nextJarvisSettings = jarvisSettings.copyWith(
apiUrl: apiUrl,
apiToken: apiToken,
workspaceToken: workspaceToken,
);
if (!mounted) return;
setState(() => jarvisSettings = nextJarvisSettings);
await _saveSettings();
} catch (_) {
// Weben is maradjon offline-képes: ha nincs runtime config, a helyi cache él tovább.
}
}
void _updatePeople(List<Person> nextPeople) {
setState(() => people = nextPeople);
_saveSettings();
@@ -260,6 +297,7 @@ class _HomeShellState extends State<HomeShell> {
int selected = 0;
bool jarvisBackendOnline = false;
bool workspaceSyncing = false;
String? autoLoadedWorkspaceToken;
Timer? jarvisBackendTimer;
@override
@@ -272,12 +310,35 @@ class _HomeShellState extends State<HomeShell> {
);
}
@override
void didUpdateWidget(covariant HomeShell oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.jarvisSettings.workspaceToken !=
widget.jarvisSettings.workspaceToken ||
oldWidget.jarvisSettings.apiToken != widget.jarvisSettings.apiToken ||
oldWidget.jarvisSettings.apiUrl != widget.jarvisSettings.apiUrl) {
_checkJarvisBackend();
_downloadWebWorkspaceIfNeeded();
}
}
@override
void dispose() {
jarvisBackendTimer?.cancel();
super.dispose();
}
void _downloadWebWorkspaceIfNeeded() {
if (!kIsWeb) return;
final workspaceToken = widget.jarvisSettings.workspaceToken?.trim();
if (workspaceToken == null || workspaceToken.isEmpty) return;
if (widget.jarvisSettings.apiToken.trim().isEmpty) return;
if (autoLoadedWorkspaceToken == workspaceToken) return;
autoLoadedWorkspaceToken = workspaceToken;
Future.microtask(() => _downloadWorkspaceState(silent: true));
}
final events = <CalendarEvent>[
CalendarEvent(
'Csal\u00e1di reggeli',
@@ -463,6 +524,20 @@ class _HomeShellState extends State<HomeShell> {
],
);
final content = wide
? body
: Column(
children: [
JarvisTopStatusBar(
online: jarvisBackendOnline,
workspaceToken: widget.jarvisSettings.workspaceToken,
onSettings: _openJarvisSettings,
onRefresh: _checkJarvisBackend,
),
Expanded(child: body),
],
);
return Scaffold(
body: SafeArea(
child: Row(
@@ -774,7 +849,7 @@ class _HomeShellState extends State<HomeShell> {
],
),
),
Expanded(child: body),
Expanded(child: content),
],
),
),
@@ -854,23 +929,24 @@ class _HomeShellState extends State<HomeShell> {
_checkJarvisBackend();
}
Future<void> _downloadWorkspaceState() async {
final workspaceToken = widget.jarvisSettings.workspaceToken?.trim();
Future<void> _downloadWorkspaceState({bool silent = false}) async {
final settings = widget.jarvisSettings;
final workspaceToken = settings.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;
}
setState(() => workspaceSyncing = true);
try {
final baseUri = Uri.parse(
normalizeJarvisUrl(widget.jarvisSettings.apiUrl),
);
final baseUri = Uri.parse(normalizeJarvisUrl(settings.apiUrl));
final response = await http
.get(
baseUri.resolve('/api/workspace/state'),
headers: {
'X-Otthon-Token': widget.jarvisSettings.apiToken,
'X-Otthon-Token': settings.apiToken,
'X-Workspace-Token': workspaceToken,
},
)
@@ -915,9 +991,13 @@ class _HomeShellState extends State<HomeShell> {
..addAll(nextStock);
});
widget.onPeopleChanged(nextPeople);
_showWorkspaceMessage('K\u00f6z\u00f6s munkat\u00e9r let\u00f6ltve.');
if (!silent) {
_showWorkspaceMessage('K\u00f6z\u00f6s munkat\u00e9r let\u00f6ltve.');
}
} catch (error) {
_showWorkspaceMessage('Workspace hiba: $error');
if (!silent) {
_showWorkspaceMessage('Workspace hiba: $error');
}
} finally {
if (mounted) setState(() => workspaceSyncing = false);
}
@@ -1505,6 +1585,96 @@ class CompactLanguageSwitcher extends StatelessWidget {
}
}
class JarvisTopStatusBar extends StatelessWidget {
const JarvisTopStatusBar({
super.key,
required this.online,
required this.workspaceToken,
required this.onSettings,
required this.onRefresh,
});
final bool online;
final String? workspaceToken;
final VoidCallback onSettings;
final VoidCallback onRefresh;
@override
Widget build(BuildContext context) {
final workspace = workspaceToken?.trim();
return Container(
margin: const EdgeInsets.fromLTRB(12, 10, 12, 0),
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: .92),
borderRadius: BorderRadius.circular(18),
border: Border.all(color: sage.withValues(alpha: .12)),
boxShadow: [
BoxShadow(
color: sage.withValues(alpha: .08),
blurRadius: 16,
offset: const Offset(0, 8),
),
],
),
child: Row(
children: [
Container(
width: 10,
height: 10,
decoration: BoxDecoration(
color: online ? Colors.green : Colors.redAccent,
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: (online ? Colors.green : Colors.redAccent).withValues(
alpha: .28,
),
blurRadius: 8,
),
],
),
),
const SizedBox(width: 8),
Expanded(
child: Text(
'JARVIS ${online ? 'online' : 'offline'}'
'${workspace == null || workspace.isEmpty ? '' : ' · $workspace'}',
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
color: ink,
fontSize: 12,
fontWeight: FontWeight.w900,
),
),
),
IconButton(
tooltip: 'Friss\u00edt\u00e9s',
onPressed: onRefresh,
icon: const Icon(Icons.refresh, size: 18),
style: IconButton.styleFrom(
minimumSize: const Size(30, 30),
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
padding: EdgeInsets.zero,
),
),
IconButton(
tooltip: 'JARVIS be\u00e1ll\u00edt\u00e1sok',
onPressed: onSettings,
icon: const Icon(Icons.settings_outlined, size: 18),
style: IconButton.styleFrom(
minimumSize: const Size(30, 30),
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
padding: EdgeInsets.zero,
),
),
],
),
);
}
}
class _CompactLanguagePill extends StatelessWidget {
const _CompactLanguagePill({
required this.selected,
+1
View File
@@ -3,6 +3,7 @@ import 'dart:io';
import 'dart:async';
import 'dart:math' as math;
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 917 B

After

Width:  |  Height:  |  Size: 436 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.1 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.5 KiB

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 11 KiB

+3 -3
View File
@@ -18,18 +18,18 @@
<meta charset="UTF-8">
<meta content="IE=Edge" http-equiv="X-UA-Compatible">
<meta name="description" content="A new Flutter project.">
<meta name="description" content="KözösTér családi naptár és otthoni organizer.">
<!-- iOS meta tags & icons -->
<meta name="mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black">
<meta name="apple-mobile-web-app-title" content="otthon_naptar">
<meta name="apple-mobile-web-app-title" content="KözösTér">
<link rel="apple-touch-icon" href="icons/Icon-192.png">
<!-- Favicon -->
<link rel="icon" type="image/png" href="favicon.png"/>
<title>otthon_naptar</title>
<title>KözösTér</title>
<link rel="manifest" href="manifest.json">
</head>
<body>
+5 -5
View File
@@ -1,11 +1,11 @@
{
"name": "otthon_naptar",
"short_name": "otthon_naptar",
"name": "KözösTér",
"short_name": "KözösTér",
"start_url": ".",
"display": "standalone",
"background_color": "#0175C2",
"theme_color": "#0175C2",
"description": "A new Flutter project.",
"background_color": "#F7F5EF",
"theme_color": "#18332D",
"description": "Családi naptár és otthoni organizer.",
"orientation": "portrait-primary",
"prefer_related_applications": false,
"icons": [