Add web profile config and branded PWA icon
@@ -35,6 +35,7 @@ migrate_working_dir/
|
|||||||
*.tgz
|
*.tgz
|
||||||
/deploy/gitlab/
|
/deploy/gitlab/
|
||||||
/deploy/gitea/
|
/deploy/gitea/
|
||||||
|
/deploy/calendar-web/app/runtime-config.json
|
||||||
|
|
||||||
# Symbolication related
|
# Symbolication related
|
||||||
app.*.symbols
|
app.*.symbols
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 917 B After Width: | Height: | Size: 436 B |
@@ -37,6 +37,6 @@ _flutter.buildConfig = {"engineRevision":"a10d8ac38de835021c8d2f920dbf50a920ccc0
|
|||||||
|
|
||||||
_flutter.loader.load({
|
_flutter.loader.load({
|
||||||
serviceWorkerSettings: {
|
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. */
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 5.2 KiB After Width: | Height: | Size: 4.7 KiB |
|
Before Width: | Height: | Size: 8.1 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 5.5 KiB After Width: | Height: | Size: 4.7 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 11 KiB |
@@ -18,18 +18,18 @@
|
|||||||
|
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta content="IE=Edge" http-equiv="X-UA-Compatible">
|
<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 -->
|
<!-- iOS meta tags & icons -->
|
||||||
<meta name="mobile-web-app-capable" content="yes">
|
<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-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">
|
<link rel="apple-touch-icon" href="icons/Icon-192.png">
|
||||||
|
|
||||||
<!-- Favicon -->
|
<!-- Favicon -->
|
||||||
<link rel="icon" type="image/png" href="favicon.png"/>
|
<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">
|
<link rel="manifest" href="manifest.json">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
{
|
{
|
||||||
"name": "otthon_naptar",
|
"name": "KözösTér",
|
||||||
"short_name": "otthon_naptar",
|
"short_name": "KözösTér",
|
||||||
"start_url": ".",
|
"start_url": ".",
|
||||||
"display": "standalone",
|
"display": "standalone",
|
||||||
"background_color": "#0175C2",
|
"background_color": "#F7F5EF",
|
||||||
"theme_color": "#0175C2",
|
"theme_color": "#18332D",
|
||||||
"description": "A new Flutter project.",
|
"description": "Családi naptár és otthoni organizer.",
|
||||||
"orientation": "portrait-primary",
|
"orientation": "portrait-primary",
|
||||||
"prefer_related_applications": false,
|
"prefer_related_applications": false,
|
||||||
"icons": [
|
"icons": [
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ class _OtthonAppState extends State<OtthonApp> {
|
|||||||
loaded = true;
|
loaded = true;
|
||||||
});
|
});
|
||||||
_loadRemoteAppConfig();
|
_loadRemoteAppConfig();
|
||||||
|
_loadWebRuntimeConfig();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _saveSettings() async {
|
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) {
|
void _updatePeople(List<Person> nextPeople) {
|
||||||
setState(() => people = nextPeople);
|
setState(() => people = nextPeople);
|
||||||
_saveSettings();
|
_saveSettings();
|
||||||
@@ -260,6 +297,7 @@ class _HomeShellState extends State<HomeShell> {
|
|||||||
int selected = 0;
|
int selected = 0;
|
||||||
bool jarvisBackendOnline = false;
|
bool jarvisBackendOnline = false;
|
||||||
bool workspaceSyncing = false;
|
bool workspaceSyncing = false;
|
||||||
|
String? autoLoadedWorkspaceToken;
|
||||||
Timer? jarvisBackendTimer;
|
Timer? jarvisBackendTimer;
|
||||||
|
|
||||||
@override
|
@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
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
jarvisBackendTimer?.cancel();
|
jarvisBackendTimer?.cancel();
|
||||||
super.dispose();
|
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>[
|
final events = <CalendarEvent>[
|
||||||
CalendarEvent(
|
CalendarEvent(
|
||||||
'Csal\u00e1di reggeli',
|
'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(
|
return Scaffold(
|
||||||
body: SafeArea(
|
body: SafeArea(
|
||||||
child: Row(
|
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();
|
_checkJarvisBackend();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _downloadWorkspaceState() async {
|
Future<void> _downloadWorkspaceState({bool silent = false}) async {
|
||||||
final workspaceToken = widget.jarvisSettings.workspaceToken?.trim();
|
final settings = widget.jarvisSettings;
|
||||||
|
final workspaceToken = settings.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;
|
||||||
}
|
}
|
||||||
|
|
||||||
setState(() => workspaceSyncing = true);
|
setState(() => workspaceSyncing = true);
|
||||||
try {
|
try {
|
||||||
final baseUri = Uri.parse(
|
final baseUri = Uri.parse(normalizeJarvisUrl(settings.apiUrl));
|
||||||
normalizeJarvisUrl(widget.jarvisSettings.apiUrl),
|
|
||||||
);
|
|
||||||
final response = await http
|
final response = await http
|
||||||
.get(
|
.get(
|
||||||
baseUri.resolve('/api/workspace/state'),
|
baseUri.resolve('/api/workspace/state'),
|
||||||
headers: {
|
headers: {
|
||||||
'X-Otthon-Token': widget.jarvisSettings.apiToken,
|
'X-Otthon-Token': settings.apiToken,
|
||||||
'X-Workspace-Token': workspaceToken,
|
'X-Workspace-Token': workspaceToken,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -915,9 +991,13 @@ class _HomeShellState extends State<HomeShell> {
|
|||||||
..addAll(nextStock);
|
..addAll(nextStock);
|
||||||
});
|
});
|
||||||
widget.onPeopleChanged(nextPeople);
|
widget.onPeopleChanged(nextPeople);
|
||||||
|
if (!silent) {
|
||||||
_showWorkspaceMessage('K\u00f6z\u00f6s munkat\u00e9r let\u00f6ltve.');
|
_showWorkspaceMessage('K\u00f6z\u00f6s munkat\u00e9r let\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);
|
||||||
}
|
}
|
||||||
@@ -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 {
|
class _CompactLanguagePill extends StatelessWidget {
|
||||||
const _CompactLanguagePill({
|
const _CompactLanguagePill({
|
||||||
required this.selected,
|
required this.selected,
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import 'dart:io';
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:math' as math;
|
import 'dart:math' as math;
|
||||||
|
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 917 B After Width: | Height: | Size: 436 B |
|
Before Width: | Height: | Size: 5.2 KiB After Width: | Height: | Size: 4.7 KiB |
|
Before Width: | Height: | Size: 8.1 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 5.5 KiB After Width: | Height: | Size: 4.7 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 11 KiB |
@@ -18,18 +18,18 @@
|
|||||||
|
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta content="IE=Edge" http-equiv="X-UA-Compatible">
|
<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 -->
|
<!-- iOS meta tags & icons -->
|
||||||
<meta name="mobile-web-app-capable" content="yes">
|
<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-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">
|
<link rel="apple-touch-icon" href="icons/Icon-192.png">
|
||||||
|
|
||||||
<!-- Favicon -->
|
<!-- Favicon -->
|
||||||
<link rel="icon" type="image/png" href="favicon.png"/>
|
<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">
|
<link rel="manifest" href="manifest.json">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
{
|
{
|
||||||
"name": "otthon_naptar",
|
"name": "KözösTér",
|
||||||
"short_name": "otthon_naptar",
|
"short_name": "KözösTér",
|
||||||
"start_url": ".",
|
"start_url": ".",
|
||||||
"display": "standalone",
|
"display": "standalone",
|
||||||
"background_color": "#0175C2",
|
"background_color": "#F7F5EF",
|
||||||
"theme_color": "#0175C2",
|
"theme_color": "#18332D",
|
||||||
"description": "A new Flutter project.",
|
"description": "Családi naptár és otthoni organizer.",
|
||||||
"orientation": "portrait-primary",
|
"orientation": "portrait-primary",
|
||||||
"prefer_related_applications": false,
|
"prefer_related_applications": false,
|
||||||
"icons": [
|
"icons": [
|
||||||
|
|||||||