From 0e993ce3aba813533fec1f8410759c6c00dd2322 Mon Sep 17 00:00:00 2001 From: ben Date: Wed, 11 Feb 2026 12:59:52 +0100 Subject: [PATCH] fix --- falcon_gui/lib/graph_editor/graph_editor.dart | 6 +- falcon_gui/lib/graph_editor/status_bar.dart | 6 +- falcon_gui/lib/main.dart | 6 +- .../lib/settings/local_backend_settings.dart | 148 ++++++++++++++++++ falcon_gui/lib/settings/settings_view.dart | 22 +-- .../lib/settings/theme_mode_setting.dart | 29 +--- falcon_gui/lib/state/falcon_manager.dart | 22 ++- falcon_gui/lib/utils/file_picker.dart | 21 +++ falcon_gui/lib/utils/local_config.dart | 87 ++++++---- falcon_gui/lib/utils/misc.dart | 22 +++ 10 files changed, 297 insertions(+), 72 deletions(-) create mode 100644 falcon_gui/lib/settings/local_backend_settings.dart diff --git a/falcon_gui/lib/graph_editor/graph_editor.dart b/falcon_gui/lib/graph_editor/graph_editor.dart index 90a2d34..07e1cb5 100644 --- a/falcon_gui/lib/graph_editor/graph_editor.dart +++ b/falcon_gui/lib/graph_editor/graph_editor.dart @@ -61,7 +61,11 @@ class _GraphEditorState extends State { Expanded( child: Stack( children: [ - const EditorView(), + Listener( + onPointerDown: (_) => + setState(() => _activeCategory = null), + child: const EditorView(), + ), if (_activeCategory != null) ...[ Positioned( top: 0, diff --git a/falcon_gui/lib/graph_editor/status_bar.dart b/falcon_gui/lib/graph_editor/status_bar.dart index f7b7ea9..d828a0f 100644 --- a/falcon_gui/lib/graph_editor/status_bar.dart +++ b/falcon_gui/lib/graph_editor/status_bar.dart @@ -24,7 +24,9 @@ class StatusBar extends StatelessWidget { Widget build(BuildContext context) { return MultiListener( builder: (context) { - return Container( + return AnimatedContainer( + duration: const Duration(milliseconds: 500), + height: 24, color: _stateColor(falconManager.falconState), child: Row( @@ -204,7 +206,7 @@ Color _stateColor(FalconState falconState) => switch (falconState) { FalconState.noGraph => Colors.orange, FalconState.constructing => Colors.purple, FalconState.preparing => Colors.indigo, - FalconState.stopping => Colors.red, + FalconState.stopping => Colors.orangeAccent, FalconState.error => Colors.redAccent, FalconState.starting => Colors.blueAccent, }; diff --git a/falcon_gui/lib/main.dart b/falcon_gui/lib/main.dart index 4a0c41c..4fa44ef 100644 --- a/falcon_gui/lib/main.dart +++ b/falcon_gui/lib/main.dart @@ -3,7 +3,6 @@ import 'dart:io'; import 'package:falcon_gui/dialogs/on_close_gui_dialog.dart'; import 'package:falcon_gui/graph_editor/graph_editor.dart'; -import 'package:falcon_gui/settings/theme_mode_setting.dart'; import 'package:falcon_gui/state/falcon_manager.dart'; import 'package:falcon_gui/state/graph_manager.dart'; import 'package:falcon_gui/utils/local_config.dart'; @@ -45,7 +44,6 @@ Future _entrypoint() async { await LocalConfigManager.loadConfig(); - await setThemeModeFromConfig(); _listenForLoadedGraphFile(); unawaited(falconManager.initialize()); @@ -80,13 +78,13 @@ class DesktopApp extends StatelessWidget { @override Widget build(BuildContext context) { return AnimatedBuilder( - animation: themeNotifier, + animation: localConfigNotifier, builder: (context, _) { return MaterialApp( debugShowCheckedModeBanner: false, home: const RootPage(), navigatorKey: globalNavigatorKey, - themeMode: themeNotifier.value, + themeMode: themeModeFromString(localConfigNotifier.value.themeMode), theme: FalconTheme(Theme.of(context).textTheme).light(), darkTheme: FalconTheme(Theme.of(context).textTheme).dark(), ); diff --git a/falcon_gui/lib/settings/local_backend_settings.dart b/falcon_gui/lib/settings/local_backend_settings.dart new file mode 100644 index 0000000..c370f37 --- /dev/null +++ b/falcon_gui/lib/settings/local_backend_settings.dart @@ -0,0 +1,148 @@ +import 'package:falcon_gui/utils/file_picker.dart'; +import 'package:falcon_gui/utils/local_config.dart'; +import 'package:falcon_gui/utils/theme.dart'; +import 'package:flutter/material.dart'; +import 'package:remixicon/remixicon.dart'; + +enum _PathConfigType { resources, output, logs } + +class LocalBackendSettings extends StatelessWidget { + const LocalBackendSettings({super.key}); + + Future _onPathSelect(_PathConfigType type) async { + switch (type) { + case _PathConfigType.resources: + final result = await FalconFilePicker.pickDirectory( + initialDirectory: + localConfigNotifier.value.serverSideStorageResources, + dialogTitle: 'Select Resources Directory', + ); + + if (result != null) { + await LocalConfigManager.setServerSideStorageResources(result.path); + } + case _PathConfigType.output: + final result = await FalconFilePicker.pickDirectory( + initialDirectory: + localConfigNotifier.value.serverSideStorageEnvironment, + dialogTitle: 'Select Output Directory', + ); + + if (result != null) { + await LocalConfigManager.setServerSideStorageEnvironment(result.path); + } + case _PathConfigType.logs: + final result = await FalconFilePicker.pickDirectory( + initialDirectory: localConfigNotifier.value.loggingPath, + dialogTitle: 'Select Logs Directory', + ); + + if (result != null) { + await LocalConfigManager.setLoggingPath(result.path); + } + } + } + + @override + Widget build(BuildContext context) { + return ValueListenableBuilder( + valueListenable: localConfigNotifier, + builder: (context, localConfig, _) { + return Padding( + padding: const EdgeInsets.all(8), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Local Backend Configuration', + style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold), + ), + const Text( + 'Changes will take effect after restarting the local backend.', + ), + const SizedBox(height: 8), + Container( + decoration: BoxDecoration( + color: context.c.surfaceContainerLow, + borderRadius: BorderRadius.circular(8), + border: Border.all(color: context.c.outlineVariant), + ), + padding: const EdgeInsets.all(8), + width: 600, + + child: Column( + children: [ + _PathSettingRow( + label: 'Resources', + path: localConfig.serverSideStorageResources, + onSelect: () => _onPathSelect(_PathConfigType.resources), + ), + const SizedBox(height: 16), + _PathSettingRow( + label: 'Output', + path: localConfig.serverSideStorageEnvironment, + onSelect: () => _onPathSelect(_PathConfigType.output), + ), + const SizedBox(height: 16), + _PathSettingRow( + label: 'Logs', + path: localConfig.loggingPath, + onSelect: () => _onPathSelect(_PathConfigType.logs), + ), + ], + ), + ), + ], + ), + ); + }, + ); + } +} + +class _PathSettingRow extends StatelessWidget { + const _PathSettingRow({ + required this.label, + required this.path, + required this.onSelect, + }); + + final String label; + final String path; + final VoidCallback onSelect; + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + label, + style: TextStyle( + fontWeight: FontWeight.w600, + color: context.c.onSurfaceVariant, + ), + ), + + Row( + children: [ + Expanded( + child: Text( + path, + style: TextStyle( + fontSize: 14, + color: context.c.onSurfaceVariant, + ), + ), + ), + const SizedBox(width: 8), + IconButton( + icon: const Icon(RemixIcons.folder_2_line), + onPressed: onSelect, + ), + ], + ), + ], + ); + } +} diff --git a/falcon_gui/lib/settings/settings_view.dart b/falcon_gui/lib/settings/settings_view.dart index c90eda1..260490a 100644 --- a/falcon_gui/lib/settings/settings_view.dart +++ b/falcon_gui/lib/settings/settings_view.dart @@ -1,4 +1,5 @@ import 'package:falcon_gui/dialogs/dialog_view.dart'; +import 'package:falcon_gui/settings/local_backend_settings.dart'; import 'package:falcon_gui/settings/theme_mode_setting.dart'; import 'package:flutter/material.dart'; @@ -7,16 +8,19 @@ class SettingsView extends StatelessWidget { @override Widget build(BuildContext context) { - return const DialogView( + return DialogView( title: 'Settings', - content: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // FalconProcessPriorityStatus(), - // SizedBox(height: 12), - ThemeModeSetting(), - SizedBox(height: 12), - ], + content: ConstrainedBox( + constraints: const BoxConstraints(minWidth: 600), + child: const Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + LocalBackendSettings(), + SizedBox(height: 12), + ThemeModeSetting(), + SizedBox(height: 12), + ], + ), ), ); } diff --git a/falcon_gui/lib/settings/theme_mode_setting.dart b/falcon_gui/lib/settings/theme_mode_setting.dart index e06f839..de72a19 100644 --- a/falcon_gui/lib/settings/theme_mode_setting.dart +++ b/falcon_gui/lib/settings/theme_mode_setting.dart @@ -1,25 +1,9 @@ import 'dart:async'; import 'package:falcon_gui/utils/local_config.dart'; -import 'package:falcon_gui/utils/logger.dart'; +import 'package:falcon_gui/utils/misc.dart'; import 'package:flutter/material.dart'; -final ValueNotifier themeNotifier = ValueNotifier(ThemeMode.system); - -Future setThemeModeFromConfig() async { - try { - final themeModeName = localConfig.themeMode; - final themeMode = themeModeName == 'light' - ? ThemeMode.light - : themeModeName == 'dark' - ? ThemeMode.dark - : ThemeMode.system; - themeNotifier.value = themeMode; - } catch (e, s) { - logError('Error loading theme mode from shared preferences: $e', s); - } -} - Future _saveThemeModeToLocalConfig(ThemeMode mode) async { final modeName = switch (mode) { ThemeMode.light => 'light', @@ -35,9 +19,9 @@ class ThemeModeSetting extends StatelessWidget { @override Widget build(BuildContext context) { // use CupertinoSegmentedControl to show 3 options: light, dark, system - return ValueListenableBuilder( - valueListenable: themeNotifier, - builder: (context, mode, _) { + return ValueListenableBuilder( + valueListenable: localConfigNotifier, + builder: (context, localConfig, _) { return Padding( padding: const EdgeInsets.symmetric(horizontal: 8), child: Column( @@ -63,10 +47,11 @@ class ThemeModeSetting extends StatelessWidget { label: Text('System'), ), ], - selected: {mode}, + selected: { + themeModeFromString(localConfig.themeMode), + }, onSelectionChanged: (newSelection) { final newMode = newSelection.first; - themeNotifier.value = newMode; unawaited(_saveThemeModeToLocalConfig(newMode)); }, ), diff --git a/falcon_gui/lib/state/falcon_manager.dart b/falcon_gui/lib/state/falcon_manager.dart index 1baa371..010f6f4 100644 --- a/falcon_gui/lib/state/falcon_manager.dart +++ b/falcon_gui/lib/state/falcon_manager.dart @@ -74,15 +74,17 @@ class FalconManager extends ChangeNotifier { // let user choose remote vs local on startup await initLocalBackend(); - final lastGraphPath = localConfig.lastOpenedGraph; + var initialFile = _defaultGraphFile; + + final lastGraphPath = localConfigNotifier.value.lastOpenedGraph; + if (lastGraphPath != null) { final file = File(lastGraphPath); if (file.existsSync()) { - unawaited(falconManager.loadFile(file: file)); + initialFile = file; } - } else { - unawaited(loadFile(file: _defaultGraphFile)); } + unawaited(loadFile(file: initialFile)); } Future openFile() async { @@ -190,6 +192,8 @@ class FalconManager extends ChangeNotifier { _localFalconBackendPid = existingPid; } else { + // TODO(ben): start process using linux command and pipe the + // output to a file in logs directory final localBackendProcess = await Process.start( _falconBackendBinPath, [ @@ -346,12 +350,18 @@ class FalconManager extends ChangeNotifier { return; } + final graphAsYaml = graph.toYaml(); + await _currentGraphFile!.writeAsString(graphAsYaml); + unawaited(_sendGraphToBackend(graphAsYaml)); + } + + Future _sendGraphToBackend(String graphAsYaml) async { if (falconState == FalconState.unknown) { // TODO(ben): dont create infinite Futures, instead, overwrite // the previous one, perhaps just use the debouncer Future.delayed( const Duration(milliseconds: 100), - () => onGraphChanged(graph), + () => _sendGraphToBackend(graphAsYaml), ); return; } @@ -359,13 +369,11 @@ class FalconManager extends ChangeNotifier { if (falconState != FalconState.noGraph) { await sendCommand(FalconZmqCommand.graphDestroy); } - final graphAsYaml = graph.toYaml(); if (graphAsYaml.trim().isEmpty) { return; } - await _currentGraphFile!.writeAsString(graphAsYaml); await _falconZMQ!.sendCommandParts( FalconZmqCommand.graphBuild(_currentGraphFile!.absolute.path), ); diff --git a/falcon_gui/lib/utils/file_picker.dart b/falcon_gui/lib/utils/file_picker.dart index ebe1ea4..bfa6a37 100644 --- a/falcon_gui/lib/utils/file_picker.dart +++ b/falcon_gui/lib/utils/file_picker.dart @@ -77,4 +77,25 @@ class FalconFilePicker { return null; } + + static Future pickDirectory({ + required String initialDirectory, + required String dialogTitle, + }) async { + try { + final result = await FilePicker.platform.getDirectoryPath( + initialDirectory: Directory(initialDirectory).absolute.path, + dialogTitle: dialogTitle, + lockParentWindow: true, + ); + + if (result != null) { + return Directory(result); + } + } catch (e, s) { + logError('Error picking directory: $e', s); + } + + return null; + } } diff --git a/falcon_gui/lib/utils/local_config.dart b/falcon_gui/lib/utils/local_config.dart index 1ee7c53..84a8394 100644 --- a/falcon_gui/lib/utils/local_config.dart +++ b/falcon_gui/lib/utils/local_config.dart @@ -3,12 +3,12 @@ import 'dart:io'; import 'package:falcon_gui/utils/logger.dart'; +import 'package:falcon_gui/utils/misc.dart'; import 'package:flutter/foundation.dart'; import 'package:yaml/yaml.dart'; import 'package:yaml_writer/yaml_writer.dart'; -late LocalConfig _config; -LocalConfig get localConfig => _config; +ValueNotifier localConfigNotifier = ValueNotifier(LocalConfig()); abstract class LocalConfigManager { static final File _configFile = kDebugMode @@ -16,15 +16,46 @@ abstract class LocalConfigManager { : File('config.yaml'); static Future setThemeMode(String modeName) async { - if (localConfig.themeMode != modeName) { - _config = _config.copyWith(themeMode: modeName); + if (localConfigNotifier.value.themeMode != modeName) { + localConfigNotifier.value = localConfigNotifier.value.copyWith( + themeMode: modeName, + ); await _saveConfig(); } } static Future setLastOpenedGraphFilePath(String path) async { - if (localConfig.lastOpenedGraph != path) { - _config = _config.copyWith(lastOpenedGraph: path); + if (localConfigNotifier.value.lastOpenedGraph != path) { + localConfigNotifier.value = localConfigNotifier.value.copyWith( + lastOpenedGraph: path, + ); + await _saveConfig(); + } + } + + static Future setServerSideStorageResources(String path) async { + if (localConfigNotifier.value.serverSideStorageResources != path) { + localConfigNotifier.value = localConfigNotifier.value.copyWith( + serverSideStorageResources: path, + ); + await _saveConfig(); + } + } + + static Future setServerSideStorageEnvironment(String path) async { + if (localConfigNotifier.value.serverSideStorageEnvironment != path) { + localConfigNotifier.value = localConfigNotifier.value.copyWith( + serverSideStorageEnvironment: path, + ); + await _saveConfig(); + } + } + + static Future setLoggingPath(String path) async { + if (localConfigNotifier.value.loggingPath != path) { + localConfigNotifier.value = localConfigNotifier.value.copyWith( + loggingPath: path, + ); await _saveConfig(); } } @@ -35,23 +66,23 @@ abstract class LocalConfigManager { if (await _configFile.exists()) { final content = await _configFile.readAsString(); final yamlMap = loadYaml(content) as YamlMap; - _config = LocalConfig.fromMap( + localConfigNotifier.value = LocalConfig.fromMap( _yamlMapToMap(yamlMap) as Map, ); } else { - _config = LocalConfig(); + localConfigNotifier.value = LocalConfig(); await _saveConfig(); } } catch (e, s) { logError('Error loading local config: $e', s); - _config = LocalConfig(); + localConfigNotifier.value = LocalConfig(); } } static Future _saveConfig() async { try { final yamlWriter = YamlWriter(); - final yamlString = yamlWriter.write(_config.toMap()); + final yamlString = yamlWriter.write(localConfigNotifier.value.toMap()); await _configFile.writeAsString(yamlString); } catch (e, s) { logError('Error saving local config: $e', s); @@ -74,9 +105,9 @@ class LocalConfig { LocalConfig({ this.themeMode = 'system', this.networkPort = 5555, - this.loggingPath = '~/falcon/output/logs/', - this.serverSideStorageEnvironment = '~/falcon/output/', - this.serverSideStorageResources = '~/falcon/resources/', + this.loggingPath = r'$HOME/falcon/output/logs/', + this.serverSideStorageEnvironment = r'$HOME/falcon/output/', + this.serverSideStorageResources = r'$HOME/falcon/resources/', this.lastOpenedGraph, this.graphFile, this.graphAutostart, @@ -93,11 +124,13 @@ class LocalConfig { return LocalConfig( networkPort: network?['port'] as int? ?? 5555, - loggingPath: logging?['path'] as String? ?? '~/falcon/output/logs/', + loggingPath: logging?['path'] as String? ?? r'$HOME/falcon/output/logs/', serverSideStorageEnvironment: - serverSideStorage?['environment'] as String? ?? '~/falcon/output/', + serverSideStorage?['environment'] as String? ?? + r'$HOME/falcon/output/', serverSideStorageResources: - serverSideStorage?['resources'] as String? ?? '~/falcon/resources/', + serverSideStorage?['resources'] as String? ?? + r'$HOME/falcon/resources/', themeMode: ui?['theme_mode'] as String? ?? 'system', graphFile: graph?['file'] as String?, graphAutostart: graph?['autostart'] as bool?, @@ -115,31 +148,27 @@ class LocalConfig { final bool? graphAutostart; @Deprecated("Do not use this field. It's for falcon backend.") final bool? debugEnabled; - @Deprecated("Do not use this field. It's for falcon backend.") final int networkPort; - @Deprecated("Do not use this field. It's for falcon backend.") final String loggingPath; - @Deprecated("Do not use this field. It's for falcon backend.") final String serverSideStorageEnvironment; - @Deprecated("Do not use this field. It's for falcon backend.") final String serverSideStorageResources; Map toMap() { return { 'graph': { - 'file': graphFile, - 'autostart': graphAutostart, + if (graphFile != null) 'file': graphFile!.absolutePath, + if (graphAutostart != null) 'autostart': graphAutostart, }, - 'debug': {'enabled': debugEnabled}, + if (debugEnabled != null) 'debug': {'enabled': debugEnabled}, 'network': {'port': networkPort}, - 'logging': {'path': loggingPath}, + 'logging': {'path': loggingPath.absolutePath}, 'server_side_storage': { - 'environment': serverSideStorageEnvironment, - 'resources': serverSideStorageResources, + 'environment': serverSideStorageEnvironment.absolutePath, + 'resources': serverSideStorageResources.absolutePath, }, 'ui': { 'theme_mode': themeMode, - 'last_opened_graph': lastOpenedGraph, + 'last_opened_graph': lastOpenedGraph?.absolutePath, }, }; } @@ -170,3 +199,7 @@ class LocalConfig { ); } } + +extension _AbsolutePath on String { + String get absolutePath => getAbsolutePathForUbuntu(this); +} diff --git a/falcon_gui/lib/utils/misc.dart b/falcon_gui/lib/utils/misc.dart index f688751..adf0237 100644 --- a/falcon_gui/lib/utils/misc.dart +++ b/falcon_gui/lib/utils/misc.dart @@ -109,3 +109,25 @@ class ClickableIcon extends StatelessWidget { ); } } + +String getAbsolutePathForUbuntu(String input) { + var path = input.replaceAll(r'$HOME', ubuntuHomePath.path); + + if (path.startsWith('~')) { + path = path.replaceFirst('~', ubuntuHomePath.path); + } + + return Directory(path).absolute.path; +} + +ThemeMode themeModeFromString(String mode) { + switch (mode) { + case 'light': + return ThemeMode.light; + case 'dark': + return ThemeMode.dark; + case 'system': + default: + return ThemeMode.system; + } +}