Add web profile config and branded PWA icon
This commit is contained in:
+180
-10
@@ -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,
|
||||
|
||||
@@ -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';
|
||||
|
||||
Reference in New Issue
Block a user