From 5b08bcd93a5df9846d77a88e13fbb27b5c9e4cbf Mon Sep 17 00:00:00 2001 From: Ada Phillips Date: Wed, 29 May 2024 16:23:32 -0400 Subject: [PATCH 01/82] error_handling: add error_handler and better logging Add new ErrorHandler class Route ErrorHandler methods as flutterError.onError and platformDispatcher .onError handler Modify Logger to write to stdout as well as log file Change default log file location Refactor api_services methods to log more information regarding api calls, and surface exceptions Add missing api methods (manual home, manual command, config) Fix delete api method --- lib/api_services/api_services.dart | 214 ++++++++++++++++------------- lib/main.dart | 17 ++- lib/util/error_handler.dart | 17 +++ 3 files changed, 154 insertions(+), 94 deletions(-) create mode 100644 lib/util/error_handler.dart diff --git a/lib/api_services/api_services.dart b/lib/api_services/api_services.dart index 9c2bcad..7308324 100644 --- a/lib/api_services/api_services.dart +++ b/lib/api_services/api_services.dart @@ -18,17 +18,22 @@ import 'dart:convert'; import 'package:flutter/foundation.dart'; +import 'package:logging/logging.dart'; import 'package:http/http.dart' as http; class ApiService { + static final _logger = Logger('ApiService'); // For Debugging Purposes: TheContrappostoShop Internal Debug API URL (Simulated Odyssey) // During production, this will be the actual Odyssey API URL (currently assuming to localhost) - static const String apiUrl = kDebugMode ? "https://dev.plyktra.de" : 'http://127.0.0.1:12357'; + static const String apiUrl = + kDebugMode ? "https://dev.plyktra.de" : 'http://127.0.0.1:12357'; // Method for creating a Uri object based on http or https protocol - static Uri dynUri(String apiUrl, String path, Map queryParams) { + static Uri dynUri( + String apiUrl, String path, Map queryParams) { if (queryParams.containsKey('file_path')) { - queryParams['file_path'] = queryParams['file_path'].toString().replaceAll('//', ''); + queryParams['file_path'] = + queryParams['file_path'].toString().replaceAll('//', ''); } if (apiUrl.startsWith('https://')) { @@ -44,64 +49,99 @@ class ApiService { /// GET METHODS TO ODYSSEY /// - // Get current status of the printer - static Future> getStatus() async { - final response = await http.get( - dynUri(apiUrl, '/status', {}), - ); + static Future odysseyGet( + String endpoint, Map queryParams) async { + var uri = dynUri(apiUrl, endpoint, queryParams); + _logger.fine('Odyssey GET $uri'); + + final response = await http.get(uri); if (response.statusCode == 200) { - return json.decode(response.body); + return response; } else { - throw Exception('Failed to fetch status'); + throw Exception('Odyssey GET call failed: $response'); } } + static Future odysseyPost( + String endpoint, Map queryParams) async { + var uri = dynUri(apiUrl, endpoint, queryParams); + _logger.fine('Odyssey POST $uri'); + + final response = await http.post(uri); + + if (response.statusCode == 200) { + return response; + } else { + throw Exception('Odyssey POST call failed: $response'); + } + } + + static Future odysseyDelete( + String endpoint, Map queryParams) async { + var uri = dynUri(apiUrl, endpoint, queryParams); + _logger.fine('Odyssey DELETE $uri'); + + final response = await http.delete(uri); + + if (response.statusCode == 200) { + return response; + } else { + throw Exception('Odyssey DELETE call failed: $response'); + } + } + + // Get current status of the printer + static Future> getStatus() async { + _logger.info("getStatus"); + final response = await odysseyGet('/status', {}); + return json.decode(response.body); + } + + // Get current status of the printer + static Future> getConfig() async { + _logger.info("getConfig"); + final response = await odysseyGet('/config', {}); + return json.decode(response.body); + } + // Get list of files and directories in a specific location with pagination // Takes 3 parameters : location [string], pageSize [int] and pageIndex [int] static Future> listItems( String location, int pageSize, int pageIndex, String subdirectory) async { + _logger.info( + "listItems location=$location pageSize=$pageSize pageIndex=$pageIndex subdirectory=$subdirectory"); final queryParams = { "location": location, "subdirectory": subdirectory, "page_index": pageIndex.toString(), "page_size": pageSize.toString(), }; - final response = await http.get(dynUri(apiUrl, '/files', queryParams)); - if (response.statusCode == 200) { - // TODO check the response sent by odyssey - return json.decode(response.body); - } else { - throw Exception('Failed to fetch status'); - } + final response = await odysseyGet('/files', queryParams); + return json.decode(response.body); } // Get file metadata // Takes 2 parameters : location [string] and filePath [String] - static Future> getFileMetadata(String location, String filePath) async { + static Future> getFileMetadata( + String location, String filePath) async { + _logger.info("getFileMetadata location=$location filePath=$filePath"); final queryParams = {"location": location, "file_path": filePath}; - final response = await http.get(dynUri(apiUrl, '/file/metadata', queryParams)); - if (response.statusCode == 200) { - // TODO check the response sent by odyssey - return json.decode(response.body); - } else { - throw Exception('Failed to fetch status'); - } + final response = await odysseyGet('/file/metadata', queryParams); + return json.decode(response.body); } // Get file thumbnail // Takes 2 parameters : location [string] and filePath [String] - static Future getFileThumbnail(String location, String filePath) async { + static Future getFileThumbnail( + String location, String filePath) async { + _logger.info("getFileThumbnail location=$location filePath=$filePath"); final queryParams = {"location": location, "file_path": filePath}; - final response = await http.get(dynUri(apiUrl, '/file/thumbnail', queryParams)); - if (response.statusCode == 200) { - return response.bodyBytes; - } else { - throw Exception('Failed to fetch thumbnail'); - } + final response = await odysseyGet('/file/thumbnail', queryParams); + return response.bodyBytes; } /// @@ -111,98 +151,88 @@ class ApiService { // Start printing a given file // Takes 2 parameters : location [string] and filePath [String] static Future startPrint(String location, String filePath) async { - final response = await http.post( - dynUri(apiUrl, '/print/start', { - 'location': location, - 'file_path': filePath, - }), - ); - - if (response.statusCode != 200) { - throw Exception('Failed to post data'); - } + _logger.info("startPrint location=$location filePath=$filePath"); + + final queryParams = { + 'location': location, + 'file_path': filePath, + }; + + await odysseyPost('/print/start', queryParams); } // Cancel the print - static Future cancelPrint() async { - final response = await http.post(dynUri(apiUrl, '/print/cancel', {})); + static Future cancelPrint() async { + _logger.info("cancelPrint"); - if (response.statusCode != 200) { - // TODO check the response sent by odyssey - throw Exception('Failed to post data'); - } + await odysseyPost('/print/cancel', {}); } // Pause the print - static Future pausePrint() async { - final response = await http.post(dynUri(apiUrl, '/print/pause', {})); + static Future pausePrint() async { + _logger.info("pausePrint"); - if (response.statusCode != 200) { - // TODO check the response sent by odyssey - throw Exception('Failed to post data'); - } + await odysseyPost('/print/pause', {}); } // Resume the print - static Future resumePrint() async { - final response = await http.post(dynUri(apiUrl, '/print/resume', {})); + static Future resumePrint() async { + _logger.info("resumePrint"); - if (response.statusCode != 200) { - // TODO check the response sent by odyssey - throw Exception('Failed to post data'); - } + await odysseyPost('/print/resume', {}); } // Move the Z axis // Takes 1 param height [double] which is the desired position of the Z axis static Future> move(double height) async { - final response = await http.post( - dynUri(apiUrl, '/manual', {}), - headers: {"Content-Type": "application/json"}, - body: json.encode({'z': height}), - ); + _logger.info("move height=$height"); - if (response.statusCode == 200) { - // TODO check the response sent by odyssey - return json.decode(response.body); - } else { - throw Exception('Failed to post data'); - } + final response = await odysseyPost('/manual', {'z': height}); + return json.decode(response.body); } // Toggle cure // Takes 1 param cure [bool] which define if we start or stop the curing static Future> manualCure(bool cure) async { - final response = await http.post( - dynUri(apiUrl, '/manual', {}), - headers: {"Content-Type": "application/json"}, - body: json.encode({'cure': cure}), - ); + _logger.info("manualCure cure=$cure"); - if (response.statusCode == 200) { - // TODO check the response sent by odyssey - return json.decode(response.body); - } else { - throw Exception('Failed to post data'); - } + final response = await odysseyPost('/manual', {'cure': cure}); + return json.decode(response.body); + } + + // Home Z axis + static Future> manualHome() async { + _logger.info("manualHome"); + + final response = await odysseyPost('/manual/home', {}); + return json.decode(response.body); + } + + // Issue hardware-layer command + // Takes 1 param command [String] which holds the command to run + static Future> manualCommand(String command) async { + _logger.info("manualCommand"); + + final response = + await odysseyPost('/manual/hardware_command', {'command': command}); + return json.decode(response.body); } /// /// DELETE METHODS TO ODYSSEY /// - // TODO: Implement deleteFile method properly // Delete a file // Takes 2 parameters : location [string] and filePath [String] - static Future> deleteFile(String location, String filename) async { - final queryParams = {"location": location, "filename": filename}; - final response = await http.delete(dynUri(apiUrl, '/files', queryParams)); + static Future> deleteFile( + String location, String filePath) async { + _logger.info("deleteFile location=$location fileName=$filePath"); + final queryParams = { + 'location': location, + 'file_path': filePath, + }; - if (response.statusCode == 200) { - // TODO check the response sent by odyssey - return json.decode(response.body); - } else { - throw Exception('Failed to fetch status'); - } + final response = await odysseyDelete('/files', queryParams); + return json.decode(response.body); } } diff --git a/lib/main.dart b/lib/main.dart index f1e0795..91f8666 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -29,6 +29,7 @@ import 'package:orion/settings/calibrate_screen.dart'; import 'package:orion/settings/wifi_screen.dart'; import 'package:orion/settings/about_screen.dart'; import 'package:orion/themes/themes.dart'; +import 'package:orion/util/error_handler.dart'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; @@ -39,13 +40,25 @@ import 'package:logging/logging.dart'; void main() { WidgetsFlutterBinding.ensureInitialized(); + FlutterError.onError = (details) { + FlutterError.presentError(details); + ErrorHandler.onErrorDetails(details); + }; + PlatformDispatcher.instance.onError = (error, stack) { + ErrorHandler.onError(error, stack); + return true; + }; + Logger.root.level = Level.ALL; // Log all log levels Logger.root.onRecord.listen((record) async { - Directory logDir = await getApplicationDocumentsDirectory(); + Directory logDir = await getApplicationSupportDirectory(); File logFile = File('${logDir.path}/app.log'); + stdout.writeln( + '${record.time}\t[${record.loggerName}]\t${record.level.name}\t${record.message}'); final sink = logFile.openWrite(mode: FileMode.append); - sink.writeln('${record.level.name}: ${record.time}: ${record.message}'); + sink.writeln( + '${record.time}\t[${record.loggerName}]\t${record.level.name}\t${record.message}'); await sink.close(); }); runApp(const Orion()); diff --git a/lib/util/error_handler.dart b/lib/util/error_handler.dart new file mode 100644 index 0000000..0342729 --- /dev/null +++ b/lib/util/error_handler.dart @@ -0,0 +1,17 @@ +import 'package:flutter/foundation.dart'; +import 'package:logging/logging.dart'; + +class ErrorHandler { + static final _logger = Logger('ErrorHandler'); + + static void onError(Object error, StackTrace stackTrace) { + _logger.severe("Error encountered:", error, stackTrace); + return; + } + + static void onErrorDetails(FlutterErrorDetails details) { + _logger.severe( + "Flutter error encountered:", details.exception, details.stack); + return; + } +} From 91f037ca3bf58e1f9134885504d2d534ddadc9af Mon Sep 17 00:00:00 2001 From: Ada Phillips Date: Mon, 3 Jun 2024 21:35:14 -0400 Subject: [PATCH 02/82] github_workflows: Redo tar process to remove redundant paths (#9) * github_workflows: Redo tar process to remove redundant paths Re-implement build compression to avoid carrying toplevel dir into tar file --- .github/workflows/build.yml | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 5e41f15..483a0e2 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -26,9 +26,11 @@ jobs: - name: Build, Copy, and Compress armv7 run: | + BUILD_NAME=orion_armv7 flutterpi_tool build --release - mv build/flutter_assets orion_armv7 - tar -czvf orion_armv7.tar.gz orion_armv7 + mv build/flutter_assets $BUILD_NAME + ( cd $BUILD_NAME && tar -czvf $BUILD_NAME.tar.gz * ) + cp $BUILD_NAME/$BUILD_NAME.tar.gz $BUILD_NAME.tar.gz - uses: actions/upload-artifact@v4.3.1 with: @@ -37,9 +39,11 @@ jobs: - name: Build, Copy, and Compress aarch64 run: | + BUILD_NAME=orion_aarch64 flutterpi_tool build --arch=arm64 --release - mv build/flutter_assets orion_aarch64 - tar -czvf orion_aarch64.tar.gz orion_aarch64 + mv build/flutter_assets $BUILD_NAME + ( cd $BUILD_NAME && tar -czvf $BUILD_NAME.tar.gz * ) + cp $BUILD_NAME/$BUILD_NAME.tar.gz $BUILD_NAME.tar.gz - uses: actions/upload-artifact@v4.3.1 with: @@ -48,9 +52,11 @@ jobs: - name: Build, Copy, and Compress x64 run: | + BUILD_NAME=orion_x64 flutterpi_tool build --arch=x64 --release - mv build/flutter_assets orion_x64 - tar -czvf orion_x64.tar.gz orion_x64 + mv build/flutter_assets $BUILD_NAME + ( cd $BUILD_NAME && tar -czvf $BUILD_NAME.tar.gz * ) + cp $BUILD_NAME/$BUILD_NAME.tar.gz $BUILD_NAME.tar.gz - uses: actions/upload-artifact@v4.3.1 with: From 5622e14df0c4bf305eeef38da900ac4d291b97fc Mon Sep 17 00:00:00 2001 From: Paul_GD Date: Sat, 8 Jun 2024 04:01:33 +0200 Subject: [PATCH 03/82] feat(home): change layout of buttons - removed separate potrait mode - replace Status with Tools --- lib/home/home_screen.dart | 166 ++++++++++++++++++-------------------- 1 file changed, 77 insertions(+), 89 deletions(-) diff --git a/lib/home/home_screen.dart b/lib/home/home_screen.dart index bf0701b..e59b6cb 100644 --- a/lib/home/home_screen.dart +++ b/lib/home/home_screen.dart @@ -25,11 +25,22 @@ class HomeScreen extends StatelessWidget { const HomeScreen({super.key}); @override Widget build(BuildContext context) { - Size homeBtnSize = const Size(220, 130); + Size homeBtnSize = const Size(double.infinity, double.infinity); + double iconSize = + MediaQuery.of(context).size.width * 0.1; // 10% of screen width + double textSize = + MediaQuery.of(context).size.width * 0.05; // 5% of screen width final theme = Theme.of(context).copyWith( elevatedButtonTheme: ElevatedButtonThemeData( style: ButtonStyle( + shape: MaterialStateProperty.resolveWith( + (Set states) { + return RoundedRectangleBorder( + borderRadius: BorderRadius.circular(15), + ); + }, + ), minimumSize: MaterialStateProperty.resolveWith( (Set states) { return homeBtnSize; @@ -65,96 +76,73 @@ class HomeScreen extends StatelessWidget { body: Center( child: LayoutBuilder( builder: (BuildContext context, BoxConstraints constraints) { - if (MediaQuery.of(context).orientation == Orientation.landscape) { - return Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - ElevatedButton( - style: theme.elevatedButtonTheme.style, - onPressed: () => context.go('/status'), - child: const Column( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon(Icons.info_outline, size: 48), - Text('Status', style: TextStyle(fontSize: 24)), - ], - ), + return Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const SizedBox(height: 5), + Expanded( + child: Row( + children: [ + const SizedBox(width: 20), + Expanded( + child: ElevatedButton( + style: theme.elevatedButtonTheme.style, + onPressed: () => context.go('/gridfiles'), + child: const Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.print_outlined, size: 52), + Text('Print', style: TextStyle(fontSize: 28)), + ], + ), + ), + ), + const SizedBox(width: 20), + ], ), - const SizedBox(width: 20), - ElevatedButton( - style: theme.elevatedButtonTheme.style, - onPressed: () => context.go('/gridfiles'), - child: const Column( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon(Icons.folder_open_outlined, size: 48), - Text('Print Files', style: TextStyle(fontSize: 24)), - ], - ), - ), - const SizedBox(width: 20), - ElevatedButton( - style: theme.elevatedButtonTheme.style, - onPressed: () => context.go('/settings'), - child: const Column( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon(Icons.settings_outlined, size: 48), - Text('Settings', style: TextStyle(fontSize: 24)), - ], - ), - ), - ], - ); - } else { - // Vertical layout - return Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - ElevatedButton( - style: theme.elevatedButtonTheme.style, - onPressed: () => context.go('/status'), - child: const Column( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon(Icons.info_outline, size: 48), - Text('Status', style: TextStyle(fontSize: 24)), - ], - ), - ), - const SizedBox(height: 20), - ElevatedButton( - style: theme.elevatedButtonTheme.style, - onPressed: () => context.go('/gridfiles'), - child: const Column( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon(Icons.folder_open_outlined, size: 48), - Text('Print Files', style: TextStyle(fontSize: 24)), - ], - ), - ), - const SizedBox(height: 20), - ElevatedButton( - style: theme.elevatedButtonTheme.style, - onPressed: () => context.go('/settings'), - child: const Column( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon(Icons.settings_outlined, size: 48), - Text('Settings', style: TextStyle(fontSize: 24)), - ], - ), + ), + const SizedBox(height: 20), + Expanded( + child: Row( + children: [ + const SizedBox(width: 20), + Expanded( + child: ElevatedButton( + style: theme.elevatedButtonTheme.style, + onPressed: () => context.go('/tools'), + child: const Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.handyman_outlined, size: 52), + Text('Tools', style: TextStyle(fontSize: 28)), + ], + ), + ), + ), + const SizedBox(width: 20), + Expanded( + child: ElevatedButton( + style: theme.elevatedButtonTheme.style, + onPressed: () => context.go('/settings'), + child: const Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.settings_outlined, size: 52), + Text('Settings', style: TextStyle(fontSize: 28)), + ], + ), + ), + ), + const SizedBox(width: 20), + ], ), - ], - ); - } + ), + const SizedBox(height: 20), + ], + ); }, ), ), From c6c5877c6d74c1bc6097a1db923d56b9c549e814 Mon Sep 17 00:00:00 2001 From: Paul_GD Date: Sat, 8 Jun 2024 04:02:44 +0200 Subject: [PATCH 04/82] fix(orion_kb): make scrollController optional --- lib/util/orion_kb/orion_textfield_spawn.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/util/orion_kb/orion_textfield_spawn.dart b/lib/util/orion_kb/orion_textfield_spawn.dart index 6d43b00..5a997ff 100644 --- a/lib/util/orion_kb/orion_textfield_spawn.dart +++ b/lib/util/orion_kb/orion_textfield_spawn.dart @@ -27,7 +27,7 @@ class SpawnOrionTextField extends StatefulWidget { final bool isHidden; final bool noShove; final Function(String) onChanged; - final ScrollController scrollController; + final ScrollController? scrollController; const SpawnOrionTextField({ super.key, @@ -36,7 +36,7 @@ class SpawnOrionTextField extends StatefulWidget { this.isHidden = false, this.noShove = false, this.onChanged = _defaultOnChanged, - required this.scrollController, + this.scrollController, }); // Do nothing From 6c84f094593b0cf9e0230769dfcc1bff44c5d2ce Mon Sep 17 00:00:00 2001 From: Paul_GD Date: Sat, 8 Jun 2024 04:03:23 +0200 Subject: [PATCH 05/82] refactor(themes): unify light and dark theme, larger fonts --- lib/themes/themes.dart | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/themes/themes.dart b/lib/themes/themes.dart index beb77a6..48edc75 100644 --- a/lib/themes/themes.dart +++ b/lib/themes/themes.dart @@ -24,7 +24,7 @@ final ThemeData themeLight = ThemeData( brightness: Brightness.light, ), appBarTheme: const AppBarTheme( - titleTextStyle: TextStyle(fontSize: 26, color: Colors.black), + titleTextStyle: TextStyle(fontSize: 30, color: Colors.black), centerTitle: true, toolbarHeight: 65, iconTheme: IconThemeData(size: 30), @@ -33,8 +33,8 @@ final ThemeData themeLight = ThemeData( bodyMedium: TextStyle(fontSize: 20), ), bottomNavigationBarTheme: const BottomNavigationBarThemeData( - selectedLabelStyle: TextStyle(fontSize: 16), - unselectedLabelStyle: TextStyle(fontSize: 16), + selectedLabelStyle: TextStyle(fontSize: 18), + unselectedLabelStyle: TextStyle(fontSize: 18), selectedIconTheme: IconThemeData(size: 30), unselectedIconTheme: IconThemeData(size: 30), ), @@ -53,7 +53,7 @@ final ThemeData themeDark = ThemeData( brightness: Brightness.dark, ), appBarTheme: const AppBarTheme( - titleTextStyle: TextStyle(fontSize: 26, color: Colors.white), + titleTextStyle: TextStyle(fontSize: 30, color: Colors.white), centerTitle: true, toolbarHeight: 65, iconTheme: IconThemeData(size: 30), @@ -62,8 +62,8 @@ final ThemeData themeDark = ThemeData( bodyMedium: TextStyle(fontSize: 20), ), bottomNavigationBarTheme: const BottomNavigationBarThemeData( - selectedLabelStyle: TextStyle(fontSize: 16), - unselectedLabelStyle: TextStyle(fontSize: 16), + selectedLabelStyle: TextStyle(fontSize: 18), + unselectedLabelStyle: TextStyle(fontSize: 18), selectedIconTheme: IconThemeData(size: 30), unselectedIconTheme: IconThemeData(size: 30), ), From ede3c6b7a8f71632584a83db0e22d580e1e6d00d Mon Sep 17 00:00:00 2001 From: Paul_GD Date: Sat, 8 Jun 2024 04:06:04 +0200 Subject: [PATCH 06/82] feat(util): implement OrionConfig - implement OrionListTile for GeneralCfgScreen --- lib/util/orion_config.dart | 157 ++++++++++++++++++++++++++++++++++ lib/util/orion_list_tile.dart | 61 +++++++++++++ 2 files changed, 218 insertions(+) create mode 100644 lib/util/orion_config.dart create mode 100644 lib/util/orion_list_tile.dart diff --git a/lib/util/orion_config.dart b/lib/util/orion_config.dart new file mode 100644 index 0000000..db9005c --- /dev/null +++ b/lib/util/orion_config.dart @@ -0,0 +1,157 @@ +/* +* Orion - Config +* Copyright (C) 2024 TheContrappostoShop +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see . +*/ + +import 'dart:io'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:logging/logging.dart'; +import 'package:path/path.dart' as path; +import 'dart:convert'; + +class OrionConfig { + final _logger = Logger('OrionConfig'); + late final String _configPath; + + OrionConfig() { + _configPath = Platform.environment['ORION_CFG'] ?? '.'; + } + + ThemeMode getThemeMode() { + var config = _getConfig(); + var themeMode = config['general']?['themeMode'] ?? 'light'; + return themeMode == 'dark' ? ThemeMode.dark : ThemeMode.light; + } + + void setThemeMode(ThemeMode themeMode) { + var config = _getConfig(); + config['general'] ??= {}; + config['general']['themeMode'] = + themeMode == ThemeMode.dark ? 'dark' : 'light'; + _writeConfig(config); + } + + void setFlag(String flagName, bool value, {String category = 'general'}) { + var config = _getConfig(); + config[category] ??= {}; + config[category][flagName] = value; + _logger.config('setFlag: $flagName to $value'); + + _writeConfig(config); + } + + void setString(String key, String value, {String category = 'general'}) { + var config = _getConfig(); + config[category] ??= {}; + config[category][key] = value; + + if (value == '') { + _logger.config('setString: cleared $key'); + } else { + _logger.config('setString: $key to ${value == '' ? 'NULL' : value}'); + } + + _writeConfig(config); + } + + bool getFlag(String flagName, {String category = 'general'}) { + var config = _getConfig(); + return config[category]?[flagName] ?? false; + } + + String getString(String key, {String category = 'general'}) { + var config = _getConfig(); + return config[category]?[key] ?? ''; + } + + void toggleFlag(String flagName, {String category = 'general'}) { + bool currentValue = getFlag(flagName, category: category); + setFlag(flagName, !currentValue, category: category); + } + + Map _getConfig() { + var fullPath = path.join(_configPath, 'orion.cfg'); + var configFile = File(fullPath); + + if (!configFile.existsSync() || configFile.readAsStringSync().isEmpty) { + var defaultConfig = { + 'general': { + 'themeMode': 'dark', + }, + 'advanced': {}, + }; + _writeConfig(defaultConfig); + return defaultConfig; + } + + return json.decode(configFile.readAsStringSync()); + } + + void _writeConfig(Map config) { + var fullPath = path.join(_configPath, 'orion.cfg'); + var configFile = File(fullPath); + var encoder = const JsonEncoder.withIndent(' '); + configFile.writeAsStringSync(encoder.convert(config)); + } + + void blowUp(BuildContext context, String imagePath) { + _logger.severe('Blowing up the app'); + showDialog( + context: context, + barrierDismissible: false, + builder: (BuildContext context) { + return FutureBuilder( + future: Future.delayed(const Duration(seconds: 4)), + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return SafeArea( + child: Dialog( + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.zero), + insetPadding: EdgeInsets.zero, + backgroundColor: Theme.of(context).colorScheme.background, + child: const Center( + child: SizedBox( + height: 75, + width: 75, + child: CircularProgressIndicator()), + ), + ), + ); + } else { + Future.delayed(const Duration(seconds: 10), () { + Navigator.of(context).pop(true); + }); + return SafeArea( + child: Dialog( + insetPadding: EdgeInsets.zero, + backgroundColor: Colors.transparent, + child: Image.asset( + imagePath, + fit: BoxFit.fill, + width: MediaQuery.of(context).size.width, + height: MediaQuery.of(context).size.height, + ), + ), + ); + } + }, + ); + }, + ); + } +} diff --git a/lib/util/orion_list_tile.dart b/lib/util/orion_list_tile.dart new file mode 100644 index 0000000..4afb831 --- /dev/null +++ b/lib/util/orion_list_tile.dart @@ -0,0 +1,61 @@ +/* +* Orion - Orion List Tile +* Copyright (C) 2024 TheContrappostoShop (PaulGD03) +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see . +*/ + +import 'package:flutter/material.dart'; +import 'package:phosphor_flutter/phosphor_flutter.dart'; + +class OrionListTile extends StatelessWidget { + final String title; + final dynamic icon; + final bool value; + final bool ignoreColor; + final Function(bool) onChanged; + + const OrionListTile({ + super.key, + required this.title, + required this.icon, + required this.value, + this.ignoreColor = false, + required this.onChanged, + }); + + @override + Widget build(BuildContext context) { + return ListTile( + title: Text( + title, + style: + TextStyle(fontSize: 24.0, color: ignoreColor ? Colors.white : null), + ), + trailing: Transform.scale( + scale: 1.2, // adjust this value to change the size of the Switch + child: Switch( + value: value, + onChanged: onChanged, + ), + ), + leading: icon is IconData + ? Icon(icon, size: 24.0, color: ignoreColor ? Colors.white : null) + : icon is Function + ? PhosphorIcon(icon(PhosphorIconsStyle.bold), + size: 24.0, color: ignoreColor ? Colors.white : null) + : null, + ); + } +} From 8b23df6dfaa8a729b559280e9a604234f4a3379d Mon Sep 17 00:00:00 2001 From: Paul_GD Date: Sat, 8 Jun 2024 04:06:49 +0200 Subject: [PATCH 07/82] feat(settings): implement GeneralCfgScreen - saved to orion.cfg - added bsod.png easter egg --- assets/images/bsod.png | Bin 0 -> 4065 bytes lib/settings/general_screen.dart | 373 +++++++++++++++++++++++++++++++ 2 files changed, 373 insertions(+) create mode 100644 assets/images/bsod.png create mode 100644 lib/settings/general_screen.dart diff --git a/assets/images/bsod.png b/assets/images/bsod.png new file mode 100644 index 0000000000000000000000000000000000000000..3dd56caba51805ada56a73ef0a6751b2c7153b80 GIT binary patch literal 4065 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. +*/ + +import 'dart:math'; + +import 'package:flutter/material.dart'; +import 'package:orion/util/orion_config.dart'; +import 'package:orion/util/orion_kb/orion_keyboard_expander.dart'; +import 'package:orion/util/orion_kb/orion_textfield_spawn.dart'; +import 'package:orion/util/orion_list_tile.dart'; +import 'package:phosphor_flutter/phosphor_flutter.dart'; +import 'package:provider/provider.dart'; + +class GeneralCfgScreen extends StatefulWidget { + const GeneralCfgScreen({super.key}); + + @override + GeneralCfgScreenState createState() => GeneralCfgScreenState(); +} + +class GeneralCfgScreenState extends State { + late ThemeMode themeMode; + late bool useUsbByDefault; + late bool useCustomUrl; + late String customUrl; + late bool developerMode; + late bool verboseLogging; + late bool selfDestructMode; + + final ScrollController _scrollController = ScrollController(); + + final OrionConfig config = OrionConfig(); + + final GlobalKey urlTextFieldKey = + GlobalKey(); + + @override + void initState() { + super.initState(); + final OrionConfig config = OrionConfig(); + themeMode = config.getThemeMode(); + useUsbByDefault = config.getFlag('useUsbByDefault'); + useCustomUrl = config.getFlag('useCustomUrl', category: 'advanced'); + customUrl = config.getString('customUrl', category: 'advanced'); + developerMode = config.getFlag('developerMode', category: 'advanced'); + verboseLogging = config.getFlag('verboseLogging', category: 'developer'); + selfDestructMode = + config.getFlag('selfDestructMode', category: 'topsecret'); + } + + bool shouldDestruct() { + final _rand = Random(); + if (selfDestructMode && _rand.nextInt(100) < 2) { + setState(() { + selfDestructMode = false; + }); + return true; + } + return !selfDestructMode; + } + + bool isJune() { + final now = DateTime.now(); + return now.month == 6; + } + + @override + Widget build(BuildContext context) { + final changeThemeMode = Provider.of(context); + + return Scaffold( + body: Padding( + padding: const EdgeInsets.only(left: 5, right: 5, top: 5), + child: ListView( + controller: _scrollController, + children: [ + if (isJune()) + Dismissible( + key: UniqueKey(), + direction: DismissDirection.horizontal, + onDismissed: (direction) {}, + background: Container(color: Colors.transparent), + child: const Card.outlined( + elevation: 1, + child: Padding( + padding: EdgeInsets.only( + left: 16, right: 16, top: 10, bottom: 10), + child: ListTile( + title: Text( + 'Happy Pride Month!', + style: TextStyle(fontSize: 24), + ), + leading: Icon(Icons.favorite, color: Colors.pink), + ), + ), + ), + ), + if (shouldDestruct()) + Card( + elevation: 1, + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular( + 10), // match this with your Card's border radius + gradient: LinearGradient( + colors: [ + Colors.red, + Colors.orange, + Colors.yellow, + Colors.green, + Colors.blue, + Colors.indigo, + Colors.purple + ] + .map((color) => Color.lerp(color, Colors.black, 0.25)) + .where((color) => color != null) + .cast() + .toList(), + ), + ), + child: Padding( + padding: const EdgeInsets.all(16), + child: OrionListTile( + ignoreColor: true, + title: 'Self-Destruct Mode', + icon: PhosphorIcons.skull, + value: selfDestructMode, + onChanged: (bool value) { + setState(() { + selfDestructMode = value; + config.setFlag('selfDestructMode', selfDestructMode, + category: 'topsecret'); + config.blowUp(context, 'assets/images/bsod.png'); + }); + }, + ), + ), + ), + ), + Card.outlined( + elevation: 1, + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'General', + style: TextStyle( + fontSize: 28.0, + ), + ), + const SizedBox(height: 20.0), + OrionListTile( + title: 'Dark Mode', + icon: PhosphorIcons.moonStars, + value: themeMode == ThemeMode.dark, + onChanged: (bool value) { + setState(() { + themeMode = value ? ThemeMode.dark : ThemeMode.light; + }); + changeThemeMode(themeMode); + }, + ), + const SizedBox(height: 15.0), + OrionListTile( + title: 'Use USB by Default', + icon: PhosphorIcons.usb, + value: useUsbByDefault, + onChanged: (bool value) { + setState(() { + useUsbByDefault = value; + config.setFlag('useUsbByDefault', useUsbByDefault); + }); + }, + ), + ], + ), + ), + ), + Card.outlined( + elevation: 1, + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Advanced', + style: TextStyle( + fontSize: 28.0, + ), + ), + const SizedBox(height: 20.0), + OrionListTile( + title: 'Use Custom Odyssey URL', + icon: PhosphorIcons.network, + value: useCustomUrl, + onChanged: (bool value) { + setState(() { + useCustomUrl = value; + config.setFlag('useCustomUrl', useCustomUrl, + category: 'advanced'); + }); + }, + ), + if (useCustomUrl) const SizedBox(height: 25.0), + if (useCustomUrl) + Row( + children: [ + Expanded( + child: SizedBox( + height: 55, + child: ElevatedButton( + style: ElevatedButton.styleFrom(elevation: 3), + onPressed: () { + showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + title: const Center( + child: Text('Custom Odyssey URL')), + content: SizedBox( + width: MediaQuery.of(context) + .size + .width * + 0.5, + child: SingleChildScrollView( + child: Column( + children: [ + SpawnOrionTextField( + key: urlTextFieldKey, + keyboardHint: 'Enter URL', + locale: + Localizations.localeOf( + context) + .toString(), + scrollController: + _scrollController, + ), + OrionKbExpander( + textFieldKey: + urlTextFieldKey), + ], + ), + ), + ), + actions: [ + TextButton( + onPressed: () { + Navigator.of(context).pop(); + }, + child: const Text('Close'), + ), + TextButton( + onPressed: () { + setState(() { + customUrl = urlTextFieldKey + .currentState! + .getCurrentText(); + config.setString( + 'customUrl', customUrl, + category: 'advanced'); + }); + Navigator.of(context).pop(); + }, + child: const Text('Confirm'), + ), + ], + ); + }, + ); + }, + child: Text( + customUrl == '' ? 'Set URL' : customUrl, + style: const TextStyle(fontSize: 22)), + ), + ), + ), + const SizedBox(width: 10), + Expanded( + child: SizedBox( + height: 55, + child: ElevatedButton( + style: ElevatedButton.styleFrom(elevation: 3), + onPressed: customUrl == '' + ? null + : () { + setState(() { + customUrl = ''; + config.setString( + 'customUrl', customUrl, + category: 'advanced'); + }); + }, + child: const Text('Clear URL', + style: TextStyle(fontSize: 22)), + ), + ), + ), + ], + ), + if (developerMode) const SizedBox(height: 25.0), + if (developerMode) + OrionListTile( + title: 'Developer Mode', + icon: PhosphorIcons.code, + value: developerMode, + onChanged: (bool value) { + setState(() { + developerMode = value; + config.setFlag('developerMode', developerMode, + category: 'advanced'); + }); + }, + ), + ], + ), + ), + ), + if (developerMode) + Card.outlined( + elevation: 1, + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Developer', + style: TextStyle( + fontSize: 28.0, + ), + ), + const SizedBox(height: 20.0), + OrionListTile( + title: 'Verbose Logging [WIP]', + icon: PhosphorIcons.bug, + value: verboseLogging, + onChanged: (bool value) { + null; + /*setState(() { + verboseLogging = value; + config.setFlag('verboseLogging', developerMode, + category: 'developer'); + });*/ + }, + ), + ], + ), + ), + ), + ], + ), + ), + ); + } +} From d47016f04415d537b12b3c8dae41734ff55ea0f4 Mon Sep 17 00:00:00 2001 From: Paul_GD Date: Sat, 8 Jun 2024 04:07:56 +0200 Subject: [PATCH 08/82] feat(settings): added Developer Mode hidden toggle to AbouScreen - tap the QR code on the AboutScreen 5 times to enable Developer Mode --- .gitignore | 2 + lib/settings/about_screen.dart | 112 +++++++++++++++++++++++---------- macos/Podfile.lock | 11 +--- pubspec.lock | 56 +++++++++++++++++ pubspec.yaml | 2 + 5 files changed, 142 insertions(+), 41 deletions(-) diff --git a/.gitignore b/.gitignore index 89139f4..0f3fa80 100644 --- a/.gitignore +++ b/.gitignore @@ -45,3 +45,5 @@ app.*.map.json /android/key.properties .VSCodeCounter + +orion.cfg diff --git a/lib/settings/about_screen.dart b/lib/settings/about_screen.dart index fb44eb5..84acba7 100644 --- a/lib/settings/about_screen.dart +++ b/lib/settings/about_screen.dart @@ -23,7 +23,10 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; import 'package:orion/pubspec.dart'; +import 'package:orion/themes/themes.dart'; +import 'package:orion/util/orion_config.dart'; import 'package:qr_flutter/qr_flutter.dart'; +import 'package:toastification/toastification.dart'; Future getVersionNumber() async { return Pubspec.version; @@ -36,12 +39,15 @@ class AboutScreen extends StatefulWidget { _AboutScreenState createState() => _AboutScreenState(); } -/// The about screen class _AboutScreenState extends State { double leftPadding = 0; double rightPadding = 0; + int qrTapCount = 0; + bool developerMode = false; Color? _standardColor = Colors.white.withOpacity(0.0); + Toastification toastification = Toastification(); + final GlobalKey textKey1 = GlobalKey(); final GlobalKey textKey2 = GlobalKey(); final GlobalKey textKey3 = GlobalKey(); @@ -70,12 +76,58 @@ class _AboutScreenState extends State { }); }); + void handleQrTap() { + setState(() { + if (OrionConfig().getFlag('developerMode', category: 'advanced')) { + toastification.show( + context: context, + type: ToastificationType.success, + style: ToastificationStyle.fillColored, + autoCloseDuration: const Duration(seconds: 2), + title: const Text('You are already a developer'), + alignment: Alignment.topCenter, + primaryColor: Colors.green, + backgroundColor: Theme.of(context).colorScheme.surface.withBrightness(1.35), + foregroundColor: Theme.of(context).colorScheme.onSurface, + ); + } else { + qrTapCount++; + if (qrTapCount >= 5) { + OrionConfig().setFlag('developerMode', true, category: 'advanced'); + toastification.show( + context: context, + type: ToastificationType.success, + style: ToastificationStyle.fillColored, + autoCloseDuration: const Duration(seconds: 2), + title: const Text('Developer Mode Activated: You are now a developer!'), + alignment: Alignment.topCenter, + primaryColor: Colors.green, + backgroundColor: Theme.of(context).colorScheme.surface.withBrightness(1.35), + foregroundColor: Theme.of(context).colorScheme.onSurface, + ); + } else { + toastification.show( + context: context, + type: ToastificationType.info, + style: ToastificationStyle.flatColored, + autoCloseDuration: const Duration(seconds: 2), + title: Text( + 'You are ${5 - qrTapCount} ${5 - qrTapCount == 1 ? 'tap' : 'taps'} away from becoming a developer'), + alignment: Alignment.topCenter, + primaryColor: Theme.of(context).colorScheme.primary, + backgroundColor: Theme.of(context).colorScheme.surface.withBrightness(1.35), + foregroundColor: Theme.of(context).colorScheme.onSurface, + showProgressBar: false, + ); + } + } + }); + } + const String title = kDebugMode ? 'Debug Machine' : 'Prometheus mSLA'; - const String serialNumber = - kDebugMode ? 'S/N: DBG-0001-001' : 'No S/N Available'; + const String serialNumber = kDebugMode ? 'S/N: DBG-0001-001' : 'No S/N Available'; const String apiVersion = kDebugMode ? 'Odyssey: Simulated' : 'Odyssey: ?'; - const String boardType = - kDebugMode ? 'Hardware: Debugger' : 'Hardware: Apollo 3.5.2'; + const String boardType = kDebugMode ? 'Hardware: Debugger' : 'Hardware: Apollo 3.5.2'; return Scaffold( body: Stack( @@ -91,10 +143,7 @@ class _AboutScreenState extends State { child: Text( title, key: textKey1, - style: TextStyle( - fontSize: 24, - fontWeight: FontWeight.bold, - color: _standardColor), + style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold, color: _standardColor), ), ), ), @@ -121,21 +170,18 @@ class _AboutScreenState extends State { child: FittedBox( child: FutureBuilder( future: getVersionNumber(), - builder: (BuildContext context, - AsyncSnapshot snapshot) { + builder: (BuildContext context, AsyncSnapshot snapshot) { if (snapshot.hasData) { return Text( 'Orion: ${snapshot.data}', key: textKey3, - style: - TextStyle(fontSize: 20, color: _standardColor), + style: TextStyle(fontSize: 20, color: _standardColor), ); } else { return Text( 'Orion: N/A', key: textKey3, - style: - TextStyle(fontSize: 20, color: _standardColor), + style: TextStyle(fontSize: 20, color: _standardColor), ); } }, @@ -173,23 +219,25 @@ class _AboutScreenState extends State { ), ], ), - Align( - alignment: Alignment.centerRight, - child: Padding( - padding: EdgeInsets.only(right: rightPadding), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - QrImageView( - data: 'https://github.com/TheContrappostoShop/Orion', - version: QrVersions.auto, - size: 220, - eyeStyle: QrEyeStyle(color: _standardColor), - dataModuleStyle: QrDataModuleStyle( - color: _standardColor, - dataModuleShape: QrDataModuleShape.circle), - ), - ], + GestureDetector( + onTap: handleQrTap, + child: Align( + alignment: Alignment.centerRight, + child: Padding( + padding: EdgeInsets.only(right: rightPadding), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + QrImageView( + data: 'https://github.com/TheContrappostoShop/Orion', + version: QrVersions.auto, + size: 220, + eyeStyle: QrEyeStyle(color: _standardColor), + dataModuleStyle: + QrDataModuleStyle(color: _standardColor, dataModuleShape: QrDataModuleShape.circle), + ), + ], + ), ), ), ), diff --git a/macos/Podfile.lock b/macos/Podfile.lock index 90ce51f..683f245 100644 --- a/macos/Podfile.lock +++ b/macos/Podfile.lock @@ -5,9 +5,6 @@ PODS: - path_provider_foundation (0.0.1): - Flutter - FlutterMacOS - - shared_preferences_foundation (0.0.1): - - Flutter - - FlutterMacOS - url_launcher_macos (0.0.1): - FlutterMacOS - window_size (0.0.2): @@ -17,7 +14,6 @@ DEPENDENCIES: - FlutterMacOS (from `Flutter/ephemeral`) - package_info (from `Flutter/ephemeral/.symlinks/plugins/package_info/macos`) - path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin`) - - shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin`) - url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`) - window_size (from `Flutter/ephemeral/.symlinks/plugins/window_size/macos`) @@ -28,8 +24,6 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral/.symlinks/plugins/package_info/macos path_provider_foundation: :path: Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin - shared_preferences_foundation: - :path: Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin url_launcher_macos: :path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos window_size: @@ -38,9 +32,8 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 package_info: 6eba2fd8d3371dda2d85c8db6fe97488f24b74b2 - path_provider_foundation: 3784922295ac71e43754bd15e0653ccfd36a147c - shared_preferences_foundation: b4c3b4cddf1c21f02770737f147a3f5da9d39695 - url_launcher_macos: d2691c7dd33ed713bf3544850a623080ec693d95 + path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 + url_launcher_macos: 5f437abeda8c85500ceb03f5c1938a8c5a705399 window_size: 339dafa0b27a95a62a843042038fa6c3c48de195 PODFILE CHECKSUM: ae543142af37865437ba92bdbda0c110b25e440b diff --git a/pubspec.lock b/pubspec.lock index 4db26de..ac63daa 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -137,6 +137,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.0.0" + equatable: + dependency: transitive + description: + name: equatable + sha256: c2b87cb7756efdf69892005af546c56c0b5037f54d2a88269b4f347a505e3ca2 + url: "https://pub.dev" + source: hosted + version: "2.0.5" fake_async: dependency: transitive description: @@ -161,6 +169,14 @@ packages: url: "https://pub.dev" source: hosted version: "7.0.0" + fixnum: + dependency: transitive + description: + name: fixnum + sha256: "25517a4deb0c03aa0f32fd12db525856438902d9c16536311e76cdc57b31d7d1" + url: "https://pub.dev" + source: hosted + version: "1.1.0" flutter: dependency: "direct main" description: flutter @@ -232,6 +248,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.0.2" + iconsax_flutter: + dependency: transitive + description: + name: iconsax_flutter + sha256: "95b65699da8ea98f87c5d232f06b0debaaf1ec1332b697e4d90969ec9a93037d" + url: "https://pub.dev" + source: hosted + version: "1.0.0" ini: dependency: "direct main" description: @@ -408,6 +432,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.2.1" + pausable_timer: + dependency: transitive + description: + name: pausable_timer + sha256: "6ef1a95441ec3439de6fb63f39a011b67e693198e7dae14e20675c3c00e86074" + url: "https://pub.dev" + source: hosted + version: "3.1.0+3" permission_handler: dependency: "direct main" description: @@ -557,6 +589,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.10.0" + sprintf: + dependency: transitive + description: + name: sprintf + sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23" + url: "https://pub.dev" + source: hosted + version: "7.0.0" stack_trace: dependency: transitive description: @@ -597,6 +637,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.6.1" + toastification: + dependency: "direct main" + description: + name: toastification + sha256: "5e751acc2fb5b8d008138dac255d62290fde4e5a24824f29809ac098c3dfe395" + url: "https://pub.dev" + source: hosted + version: "2.0.0" typed_data: dependency: transitive description: @@ -669,6 +717,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.1.1" + uuid: + dependency: transitive + description: + name: uuid + sha256: "814e9e88f21a176ae1359149021870e87f7cddaf633ab678a5d2b0bff7fd1ba8" + url: "https://pub.dev" + source: hosted + version: "4.4.0" vector_graphics: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 9410205..8f7e295 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -63,6 +63,7 @@ dependencies: pubspec_extract: ^2.0.5 flutter_markdown: ^0.6.23 logging: ^1.2.0 + toastification: ^2.0.0 dev_dependencies: flutter_test: @@ -92,6 +93,7 @@ flutter: assets: - assets/images/opensource.svg - assets/images/placeholder.png + - assets/images/bsod.png - README.md - CHANGELOG.md From 4042a7a27829b5143cb21840c5ddec20aafcd280 Mon Sep 17 00:00:00 2001 From: Paul_GD Date: Sat, 8 Jun 2024 04:08:57 +0200 Subject: [PATCH 09/82] feat(main): allow setting the theme through OrionConfig --- lib/main.dart | 18 ++++++++- lib/settings/debug_screen.dart | 69 +++++++++++----------------------- 2 files changed, 39 insertions(+), 48 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index 91f8666..2594ddd 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -33,6 +33,8 @@ import 'package:orion/util/error_handler.dart'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; +import 'package:orion/tools/tools_screen.dart'; +import 'package:orion/util/orion_config.dart'; import 'package:path_provider/path_provider.dart'; import 'package:window_size/window_size.dart'; import 'package:provider/provider.dart'; @@ -127,6 +129,11 @@ final GoRouter _router = GoRouter( ); }, ), + GoRoute( + path: 'tools', + builder: (BuildContext context, GoRouterState state) { + return const ToolsScreen(); + }), ], ), ], @@ -142,12 +149,21 @@ class Orion extends StatefulWidget { } class OrionState extends State { - ThemeMode _themeMode = ThemeMode.system; + late ThemeMode _themeMode; + late OrionConfig _config; + + @override + void initState() { + super.initState(); + _config = OrionConfig(); + _themeMode = _config.getThemeMode(); + } void changeThemeMode(ThemeMode themeMode) { setState(() { _themeMode = themeMode; }); + _config.setThemeMode(themeMode); } @override diff --git a/lib/settings/debug_screen.dart b/lib/settings/debug_screen.dart index c9f23aa..169877e 100644 --- a/lib/settings/debug_screen.dart +++ b/lib/settings/debug_screen.dart @@ -16,6 +16,7 @@ * along with this program. If not, see . */ +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:orion/util/orion_kb/orion_keyboard_expander.dart'; import 'package:orion/util/orion_kb/orion_textfield_spawn.dart'; @@ -30,19 +31,13 @@ class DebugScreen extends StatefulWidget { } class DebugScreenState extends State { - final GlobalKey debugTextFieldKey = - GlobalKey(); - final GlobalKey dialogTextFieldKey = - GlobalKey(); + final GlobalKey debugTextFieldKey = GlobalKey(); + final GlobalKey dialogTextFieldKey = GlobalKey(); final ScrollController _scrollController = ScrollController(); - bool themeToggle = true; - @override void didChangeDependencies() { super.didChangeDependencies(); - final brightness = MediaQuery.of(context).platformBrightness; - themeToggle = brightness == Brightness.dark; } @override @@ -68,8 +63,7 @@ class DebugScreenState extends State { ), ), Padding( - padding: const EdgeInsets.symmetric( - horizontal: 30, vertical: 15), + padding: const EdgeInsets.symmetric(horizontal: 30, vertical: 15), child: SpawnOrionTextField( key: debugTextFieldKey, keyboardHint: "Debug Test Field", @@ -78,20 +72,22 @@ class DebugScreenState extends State { isHidden: true, ), ), - /*Padding( + Padding( padding: const EdgeInsets.all(10), child: ElevatedButton( - onPressed: () { - String text = debugTextFieldKey.currentState! - .getCurrentText(); - if (kDebugMode) { - print("[Debug]: DebugTestField content: $text"); - } - debugTextFieldKey.currentState!.clearText(); - }, - child: - const Text('[Debug] Read TextField to Console')), - ),*/ + onPressed: () { + String text = debugTextFieldKey.currentState!.getCurrentText(); + if (kDebugMode) { + print("[Debug]: DebugTestField content: $text"); + } + debugTextFieldKey.currentState!.clearText(); + }, + child: const Text( + '[Debug] Read TextField to Console', + style: TextStyle(fontSize: 18), + ), + ), + ), Padding( padding: const EdgeInsets.all(10), child: ElevatedButton( @@ -100,24 +96,19 @@ class DebugScreenState extends State { context: context, builder: (BuildContext context) { return AlertDialog( - title: const Center( - child: Text('WiFi-Test-0123 Password')), + title: const Center(child: Text('WiFi-Test-0123 Password')), content: SizedBox( - width: MediaQuery.of(context).size.width * - 0.5, // Half the screen width + width: MediaQuery.of(context).size.width * 0.5, // Half the screen width child: SingleChildScrollView( child: Column( children: [ SpawnOrionTextField( key: dialogTextFieldKey, keyboardHint: "Enter Password", - locale: - Localizations.localeOf(context) - .toString(), + locale: Localizations.localeOf(context).toString(), scrollController: _scrollController, ), - OrionKbExpander( - textFieldKey: dialogTextFieldKey), + OrionKbExpander(textFieldKey: dialogTextFieldKey), ], ), ), @@ -146,22 +137,6 @@ class DebugScreenState extends State { ), ), ), - Padding( - padding: const EdgeInsets.all(10), - child: Switch( - value: themeToggle, - onChanged: (bool value) { - setState( - () { - themeToggle = value; - widget.changeThemeMode(themeToggle - ? ThemeMode.dark - : ThemeMode.light); - }, - ); - }, - ), - ), OrionKbExpander(textFieldKey: debugTextFieldKey) ], ), From 052c80fe4d52ef969a5851f1ef03e9bd5d9b35a9 Mon Sep 17 00:00:00 2001 From: Paul_GD Date: Sat, 8 Jun 2024 04:11:04 +0200 Subject: [PATCH 10/82] feat(api_services): add customUrl support - add usbAvailable check - make all Futures non-static - the default address is http://localhost:12357 --- lib/api_services/api_services.dart | 74 +++++++++++++++++++++--------- 1 file changed, 52 insertions(+), 22 deletions(-) diff --git a/lib/api_services/api_services.dart b/lib/api_services/api_services.dart index 7308324..2e52be7 100644 --- a/lib/api_services/api_services.dart +++ b/lib/api_services/api_services.dart @@ -20,13 +20,25 @@ import 'dart:convert'; import 'package:flutter/foundation.dart'; import 'package:logging/logging.dart'; import 'package:http/http.dart' as http; +import 'package:orion/util/orion_config.dart'; class ApiService { static final _logger = Logger('ApiService'); - // For Debugging Purposes: TheContrappostoShop Internal Debug API URL (Simulated Odyssey) - // During production, this will be the actual Odyssey API URL (currently assuming to localhost) - static const String apiUrl = - kDebugMode ? "https://dev.plyktra.de" : 'http://127.0.0.1:12357'; + + late String apiUrl; + late String customUrl; + late bool useCustomUrl; + + ApiService() { + try { + OrionConfig config = OrionConfig(); + customUrl = config.getString('customUrl', category: 'advanced'); + useCustomUrl = config.getFlag('useCustomUrl', category: 'advanced'); + apiUrl = useCustomUrl ? customUrl : 'http://localhost:12357'; + } catch (e) { + throw Exception('Failed to load orion.cfg: $e'); + } + } // Method for creating a Uri object based on http or https protocol static Uri dynUri( @@ -49,7 +61,7 @@ class ApiService { /// GET METHODS TO ODYSSEY /// - static Future odysseyGet( + Future odysseyGet( String endpoint, Map queryParams) async { var uri = dynUri(apiUrl, endpoint, queryParams); _logger.fine('Odyssey GET $uri'); @@ -63,7 +75,7 @@ class ApiService { } } - static Future odysseyPost( + Future odysseyPost( String endpoint, Map queryParams) async { var uri = dynUri(apiUrl, endpoint, queryParams); _logger.fine('Odyssey POST $uri'); @@ -77,7 +89,7 @@ class ApiService { } } - static Future odysseyDelete( + Future odysseyDelete( String endpoint, Map queryParams) async { var uri = dynUri(apiUrl, endpoint, queryParams); _logger.fine('Odyssey DELETE $uri'); @@ -92,14 +104,14 @@ class ApiService { } // Get current status of the printer - static Future> getStatus() async { + Future> getStatus() async { _logger.info("getStatus"); final response = await odysseyGet('/status', {}); return json.decode(response.body); } // Get current status of the printer - static Future> getConfig() async { + Future> getConfig() async { _logger.info("getConfig"); final response = await odysseyGet('/config', {}); return json.decode(response.body); @@ -107,7 +119,7 @@ class ApiService { // Get list of files and directories in a specific location with pagination // Takes 3 parameters : location [string], pageSize [int] and pageIndex [int] - static Future> listItems( + Future> listItems( String location, int pageSize, int pageIndex, String subdirectory) async { _logger.info( "listItems location=$location pageSize=$pageSize pageIndex=$pageIndex subdirectory=$subdirectory"); @@ -122,9 +134,28 @@ class ApiService { return json.decode(response.body); } + // Method to check if USB is available + Future usbAvailable() async { + try { + await listItems('Local', 1, 0, ''); + } catch (e) { + _logger.severe('Failed to list items on Internal: $e'); + return false; + } + + try { + // Try to list items on Usb + await listItems('Usb', 1, 0, ''); + return true; // If successful, return true + } catch (e) { + _logger.severe('Failed to list items on Usb: $e'); + return false; // If unsuccessful, return false + } + } + // Get file metadata // Takes 2 parameters : location [string] and filePath [String] - static Future> getFileMetadata( + Future> getFileMetadata( String location, String filePath) async { _logger.info("getFileMetadata location=$location filePath=$filePath"); final queryParams = {"location": location, "file_path": filePath}; @@ -135,8 +166,7 @@ class ApiService { // Get file thumbnail // Takes 2 parameters : location [string] and filePath [String] - static Future getFileThumbnail( - String location, String filePath) async { + Future getFileThumbnail(String location, String filePath) async { _logger.info("getFileThumbnail location=$location filePath=$filePath"); final queryParams = {"location": location, "file_path": filePath}; @@ -150,7 +180,7 @@ class ApiService { // Start printing a given file // Takes 2 parameters : location [string] and filePath [String] - static Future startPrint(String location, String filePath) async { + Future startPrint(String location, String filePath) async { _logger.info("startPrint location=$location filePath=$filePath"); final queryParams = { @@ -162,21 +192,21 @@ class ApiService { } // Cancel the print - static Future cancelPrint() async { + Future cancelPrint() async { _logger.info("cancelPrint"); await odysseyPost('/print/cancel', {}); } // Pause the print - static Future pausePrint() async { + Future pausePrint() async { _logger.info("pausePrint"); await odysseyPost('/print/pause', {}); } // Resume the print - static Future resumePrint() async { + Future resumePrint() async { _logger.info("resumePrint"); await odysseyPost('/print/resume', {}); @@ -184,7 +214,7 @@ class ApiService { // Move the Z axis // Takes 1 param height [double] which is the desired position of the Z axis - static Future> move(double height) async { + Future> move(double height) async { _logger.info("move height=$height"); final response = await odysseyPost('/manual', {'z': height}); @@ -193,7 +223,7 @@ class ApiService { // Toggle cure // Takes 1 param cure [bool] which define if we start or stop the curing - static Future> manualCure(bool cure) async { + Future> manualCure(bool cure) async { _logger.info("manualCure cure=$cure"); final response = await odysseyPost('/manual', {'cure': cure}); @@ -201,7 +231,7 @@ class ApiService { } // Home Z axis - static Future> manualHome() async { + Future> manualHome() async { _logger.info("manualHome"); final response = await odysseyPost('/manual/home', {}); @@ -210,7 +240,7 @@ class ApiService { // Issue hardware-layer command // Takes 1 param command [String] which holds the command to run - static Future> manualCommand(String command) async { + Future> manualCommand(String command) async { _logger.info("manualCommand"); final response = @@ -224,7 +254,7 @@ class ApiService { // Delete a file // Takes 2 parameters : location [string] and filePath [String] - static Future> deleteFile( + Future> deleteFile( String location, String filePath) async { _logger.info("deleteFile location=$location fileName=$filePath"); final queryParams = { From 8b1b451f8a6e75b21a493382ecfc713da8a018d0 Mon Sep 17 00:00:00 2001 From: Paul_GD Date: Sat, 8 Jun 2024 04:13:16 +0200 Subject: [PATCH 11/82] chore(util, files): make api usage non-static - add USB unavailable status to GridFilesScreem --- lib/files/details_screen.dart | 98 +++++++++++----- lib/files/grid_files_screen.dart | 192 ++++++++++++++++++++----------- lib/util/sl1_thumbnail.dart | 3 +- 3 files changed, 199 insertions(+), 94 deletions(-) diff --git a/lib/files/details_screen.dart b/lib/files/details_screen.dart index 38bc76b..125db64 100644 --- a/lib/files/details_screen.dart +++ b/lib/files/details_screen.dart @@ -30,7 +30,11 @@ class DetailScreen extends StatefulWidget { final String fileSubdirectory; final String fileLocation; - const DetailScreen({super.key, required this.fileName, required this.fileSubdirectory, required this.fileLocation}); + const DetailScreen( + {super.key, + required this.fileName, + required this.fileSubdirectory, + required this.fileLocation}); @override DetailScreenState createState() => DetailScreenState(); @@ -42,6 +46,8 @@ class DetailScreen extends StatefulWidget { class DetailScreenState extends State { final _logger = Logger('DetailScreen'); + final ApiService _api = ApiService(); + double leftPadding = 0; double rightPadding = 0; @@ -78,28 +84,41 @@ class DetailScreenState extends State { Future _initFileDetails() async { try { - final fileDetails = await ApiService.getFileMetadata( + final fileDetails = await _api.getFileMetadata( widget.fileLocation, - [(DetailScreen._isDefaultDir(widget.fileSubdirectory) ? '' : widget.fileSubdirectory), widget.fileName] - .join(DetailScreen._isDefaultDir(widget.fileSubdirectory) ? '' : '/'), + [ + (DetailScreen._isDefaultDir(widget.fileSubdirectory) + ? '' + : widget.fileSubdirectory), + widget.fileName + ].join(DetailScreen._isDefaultDir(widget.fileSubdirectory) ? '' : '/'), ); String tempFileName = fileDetails['file_data']['name'] ?? 'Placeholder'; String tempFileSize = - (fileDetails['file_data']['file_size'] / 1024 / 1024).toStringAsFixed(2) + ' MB'; // convert to MB + (fileDetails['file_data']['file_size'] / 1024 / 1024) + .toStringAsFixed(2) + + ' MB'; // convert to MB String tempFileExtension = path.extension(tempFileName); - String tempLayerHeight = '${fileDetails['layer_height'].toStringAsFixed(3)} mm'; - String tempModifiedDate = DateTime.fromMillisecondsSinceEpoch(fileDetails['file_data']['last_modified'] * 1000) + String tempLayerHeight = + '${fileDetails['layer_height'].toStringAsFixed(3)} mm'; + String tempModifiedDate = DateTime.fromMillisecondsSinceEpoch( + fileDetails['file_data']['last_modified'] * 1000) .toString(); // convert to milliseconds - String tempMaterialName = 'N/A'; // this information is not provided by the API + String tempMaterialName = + 'N/A'; // this information is not provided by the API String tempThumbnailPath = await ThumbnailUtil.extractThumbnail( - widget.fileLocation, widget.fileSubdirectory, widget.fileName); // fetch thumbnail from API + widget.fileLocation, + widget.fileSubdirectory, + widget.fileName); // fetch thumbnail from API double tempPrintTimeInSeconds = fileDetails['print_time']; - Duration printDuration = Duration(seconds: tempPrintTimeInSeconds.toInt()); + Duration printDuration = + Duration(seconds: tempPrintTimeInSeconds.toInt()); String tempPrintTime = '${printDuration.inHours.remainder(24).toString().padLeft(2, '0')}:${printDuration.inMinutes.remainder(60).toString().padLeft(2, '0')}:${printDuration.inSeconds.remainder(60).toString().padLeft(2, '0')}'; double tempMaterialVolumeInMilliliters = fileDetails['used_material']; - String tempMaterialVolume = '${tempMaterialVolumeInMilliliters.toStringAsFixed(2)} mL'; + String tempMaterialVolume = + '${tempMaterialVolumeInMilliliters.toStringAsFixed(2)} mL'; setState(() { fileName = tempFileName; @@ -138,7 +157,14 @@ class DetailScreenState extends State { ); } else { WidgetsBinding.instance.addPostFrameCallback((_) { - final keys = [textKey1, textKey2, textKey3, textKey4, textKey5, textKey6]; + final keys = [ + textKey1, + textKey2, + textKey3, + textKey4, + textKey5, + textKey6 + ]; double maxWidth = 0; for (var key in keys) { @@ -156,7 +182,8 @@ class DetailScreenState extends State { rightPadding = leftPadding; setState(() { - opacity = 1.0; // Set opacity to 1 after sizes have been calculated + opacity = + 1.0; // Set opacity to 1 after sizes have been calculated }); }); @@ -176,9 +203,13 @@ class DetailScreenState extends State { Align( alignment: Alignment.centerLeft, child: Padding( - padding: EdgeInsets.only(left: leftPadding <= 0 ? leftPadding : leftPadding - 10), + padding: EdgeInsets.only( + left: leftPadding <= 0 + ? leftPadding + : leftPadding - 10), child: ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 300), + constraints: + const BoxConstraints(maxWidth: 300), child: Card.outlined( elevation: 1, child: Padding( @@ -189,21 +220,31 @@ class DetailScreenState extends State { TextSpan( children: [ TextSpan( - text: fileName.length >= 12 ? '${fileName.substring(0, 12)}...' : fileName, + text: fileName.length >= 12 + ? '${fileName.substring(0, 12)}...' + : fileName, style: TextStyle( fontSize: 24, fontWeight: FontWeight.bold, - color: Theme.of(context).colorScheme.primary), + color: Theme.of(context) + .colorScheme + .primary), ), TextSpan( text: ' - ', - style: - TextStyle(fontSize: 24, color: Theme.of(context).colorScheme.primary), + style: TextStyle( + fontSize: 24, + color: Theme.of(context) + .colorScheme + .primary), ), TextSpan( text: fileSize, - style: - TextStyle(fontSize: 24, color: Theme.of(context).colorScheme.primary), + style: TextStyle( + fontSize: 24, + color: Theme.of(context) + .colorScheme + .primary), ), ], ), @@ -284,7 +325,8 @@ class DetailScreenState extends State { child: Padding( padding: const EdgeInsets.all(4.5), child: ClipRRect( - borderRadius: BorderRadius.circular(7.75), + borderRadius: + BorderRadius.circular(7.75), child: Image.file( File(thumbnailPath), width: 220, @@ -297,9 +339,11 @@ class DetailScreenState extends State { child: Padding( padding: const EdgeInsets.all(4.5), child: ClipRRect( - borderRadius: BorderRadius.circular(7.75), + borderRadius: + BorderRadius.circular(7.75), child: const Image( - image: AssetImage('assets/images/placeholder.png'), + image: AssetImage( + 'assets/images/placeholder.png'), width: 220, height: 220, ), @@ -344,7 +388,8 @@ class DetailScreenState extends State { child: ElevatedButton( onPressed: () { String subdirectory = widget.fileSubdirectory; - ApiService.startPrint(widget.fileLocation, path.join(subdirectory, widget.fileName)); + _api.startPrint(widget.fileLocation, + path.join(subdirectory, widget.fileName)); Navigator.push( context, MaterialPageRoute( @@ -356,7 +401,8 @@ class DetailScreenState extends State { style: ElevatedButton.styleFrom( minimumSize: Size( 0, // Subtract the padding on both sides - Theme.of(context).appBarTheme.toolbarHeight as double, + Theme.of(context).appBarTheme.toolbarHeight + as double, ), ), child: const Text( diff --git a/lib/files/grid_files_screen.dart b/lib/files/grid_files_screen.dart index 93e0de3..667efc4 100644 --- a/lib/files/grid_files_screen.dart +++ b/lib/files/grid_files_screen.dart @@ -31,6 +31,7 @@ import 'package:orion/files/details_screen.dart'; import 'package:orion/util/orion_api_filesystem/orion_api_directory.dart'; import 'package:orion/util/orion_api_filesystem/orion_api_file.dart'; import 'package:orion/util/orion_api_filesystem/orion_api_item.dart'; +import 'package:orion/util/orion_config.dart'; import 'package:orion/util/sl1_thumbnail.dart'; import 'package:path/path.dart' as path; import 'package:phosphor_flutter/phosphor_flutter.dart'; @@ -44,7 +45,9 @@ class GridFilesScreen extends StatefulWidget { } class GridFilesScreenState extends State { - final _logger = Logger('GridFilesScreen'); + final _logger = Logger('GridFiles'); + final ApiService _api = ApiService(); + late String _directory = ''; late String _subdirectory = ''; late String _defaultDirectory = ''; @@ -52,12 +55,14 @@ class GridFilesScreenState extends State { late List _items = []; late Future> _itemsFuture = Future.value([]); - late Completer> _itemsCompleter = Completer>(); + late Completer> _itemsCompleter = + Completer>(); String location = ''; //bool _sortByAlpha = true; //bool _sortAscending = true; bool _isUSB = false; + bool _usbAvailable = false; bool _apiErrorState = false; bool _isLoading = false; bool _isNavigating = false; @@ -65,6 +70,8 @@ class GridFilesScreenState extends State { @override void initState() { super.initState(); + final OrionConfig config = OrionConfig(); + _isUSB = config.getFlag('useUsbByDefault'); WidgetsBinding.instance.addPostFrameCallback((_) async { if (_defaultDirectory.isEmpty) { final items = await _getItems('', true); @@ -86,7 +93,12 @@ class GridFilesScreenState extends State { //_toggleSortOrder(); } - Future> _getItems(String directory, [bool init = false]) async { + Future> _getItems(String directory, + [bool init = false]) async { + _logger.warning( + await _api.usbAvailable() ? 'USB Available' : 'USB Not Available'); + _usbAvailable = await _api.usbAvailable(); + if (!_usbAvailable) _isUSB = false; try { setState(() { _isLoading = true; @@ -100,7 +112,8 @@ class GridFilesScreenState extends State { location = _isUSB ? 'Usb' : 'Local'; - final itemResponse = await ApiService.listItems(location, 100, 0, _subdirectory); + final itemResponse = + await _api.listItems(location, 100, 0, _subdirectory); final List files = (itemResponse['files'] as List) .where((item) => item != null) @@ -117,13 +130,13 @@ class GridFilesScreenState extends State { _parentPath = items.first.parentPath; } - if (kDebugMode) { + /*if (kDebugMode) { print('---------------------------------------'); print("Device: ${_isUSB ? 'USB' : 'Internal'}"); print("Parent Path: $_parentPath"); print("Subdirectory: $_subdirectory"); print("Fetched: ${files.length} files and ${dirs.length} directories."); - } + }*/ setState(() { _isLoading = false; @@ -159,7 +172,10 @@ class GridFilesScreenState extends State { onPressed: () { Navigator.of(context).pop(); }, - child: const Text('Close'), + child: const Text( + 'Close', + style: TextStyle(fontSize: 20), + ), ), ], ); @@ -267,7 +283,8 @@ class GridFilesScreenState extends State { ? const Center(child: CircularProgressIndicator()) : FutureBuilder>( future: _itemsCompleter.future, - builder: (BuildContext context, AsyncSnapshot> snapshot) { + builder: (BuildContext context, + AsyncSnapshot> snapshot) { if (snapshot.connectionState == ConnectionState.waiting || snapshot.connectionState == ConnectionState.none) { return const Center( @@ -280,14 +297,18 @@ class GridFilesScreenState extends State { } else { _items = snapshot.data!; return Padding( - padding: const EdgeInsets.only(left: 10, right: 10, bottom: 10), + padding: + const EdgeInsets.only(left: 10, right: 10, bottom: 10), child: GridView.builder( controller: _scrollController, gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( childAspectRatio: 1.03, mainAxisSpacing: 5, crossAxisSpacing: 5, - crossAxisCount: MediaQuery.of(context).orientation == Orientation.landscape ? 4 : 2), + crossAxisCount: MediaQuery.of(context).orientation == + Orientation.landscape + ? 4 + : 2), itemCount: _items.length + 1, itemBuilder: (BuildContext context, int index) { if (index == 0) { @@ -296,44 +317,54 @@ class GridFilesScreenState extends State { elevation: 1, child: InkWell( borderRadius: BorderRadius.circular(10), - onTap: _directory == _defaultDirectory - ? () async { - _isUSB = !_isUSB; - _itemsFuture = _getItems(_directory); - _itemsCompleter = Completer>(); - final items = await _itemsFuture; - _itemsCompleter.complete(items); - setState(() { - _items = items; - }); - } - : () async { - try { - _scrollController.jumpTo(0); - final parentDirectory = path.dirname(_directory); - _directory = parentDirectory; - setState(() { - _isNavigating = true; - }); - _itemsFuture = _getItems(parentDirectory); - _itemsCompleter = Completer>(); - final items = await _itemsFuture; - _itemsCompleter.complete(items); - setState(() { - _items = items; - _isNavigating = false; - }); - } catch (e) { - _logger.severe('Failed to navigate to parent directory', e); - if (e is FileSystemException) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Operation not permitted'), - ), - ); + onTap: !_usbAvailable && !_isUSB + ? null + : _directory == _defaultDirectory + ? () async { + _isUSB = !_isUSB; + _itemsFuture = _getItems(_directory); + _itemsCompleter = + Completer>(); + final items = await _itemsFuture; + _itemsCompleter.complete(items); + setState(() { + _items = items; + }); } - } - }, + : () async { + try { + _scrollController.jumpTo(0); + final parentDirectory = + path.dirname(_directory); + _directory = parentDirectory; + setState(() { + _isNavigating = true; + }); + _itemsFuture = + _getItems(parentDirectory); + _itemsCompleter = + Completer>(); + final items = await _itemsFuture; + _itemsCompleter.complete(items); + setState(() { + _items = items; + _isNavigating = false; + }); + } catch (e) { + _logger.severe( + 'Failed to navigate to parent directory', + e); + if (e is FileSystemException) { + ScaffoldMessenger.of(context) + .showSnackBar( + const SnackBar( + content: Text( + 'Operation not permitted'), + ), + ); + } + } + }, child: GridTile( footer: Card( color: Colors.transparent, @@ -343,13 +374,16 @@ class GridFilesScreenState extends State { title: AutoSizeText( _directory == _defaultDirectory ? _isUSB == false - ? 'Switch to USB' + ? _usbAvailable + ? 'Switch to USB' + : 'USB unavailable' : 'Switch to Internal' : 'Parent Directory', textAlign: TextAlign.center, maxLines: 2, minFontSize: 18, - style: const TextStyle(fontSize: 24, color: Colors.grey), + style: const TextStyle( + fontSize: 24, color: Colors.grey), ), ), ), @@ -358,7 +392,9 @@ class GridFilesScreenState extends State { child: PhosphorIcon( _directory == _defaultDirectory ? _isUSB == false - ? PhosphorIcons.usb() + ? _usbAvailable + ? PhosphorIcons.usb() + : PhosphorIcons.xCircle() : PhosphorIcons.hardDrives() : PhosphorIcons.arrowUUpLeft(), size: 75, @@ -385,7 +421,8 @@ class GridFilesScreenState extends State { _isNavigating = true; }); _itemsFuture = _getItems(item.path); - _itemsCompleter = Completer>(); + _itemsCompleter = + Completer>(); final items = await _itemsFuture; _itemsCompleter.complete(items); setState(() { @@ -407,7 +444,8 @@ class GridFilesScreenState extends State { }, // File name that hovers over the file child: _isNavigating - ? const Center(child: CircularProgressIndicator()) + ? const Center( + child: CircularProgressIndicator()) : GridTile( footer: Card( shape: const RoundedRectangleBorder( @@ -417,7 +455,9 @@ class GridFilesScreenState extends State { ), ), color: item is OrionApiFile - ? Theme.of(context).cardColor.withOpacity(0.65) + ? Theme.of(context) + .cardColor + .withOpacity(0.65) : Colors.transparent, elevation: item is OrionApiFile ? 2 : 0, child: GridTileBar( @@ -428,38 +468,56 @@ class GridFilesScreenState extends State { minFontSize: 20, style: TextStyle( fontSize: 24, - color: Theme.of(context).textTheme.bodyLarge!.color, + color: Theme.of(context) + .textTheme + .bodyLarge! + .color, ), ), ), ), child: item is OrionApiDirectory ? IconTheme( - data: const IconThemeData(color: Colors.grey), + data: const IconThemeData( + color: Colors.grey), child: Padding( - padding: const EdgeInsets.only(bottom: 15), - child: PhosphorIcon(PhosphorIcons.folder(), size: 75), + padding: const EdgeInsets.only( + bottom: 15), + child: PhosphorIcon( + PhosphorIcons.folder(), + size: 75), ), ) : Padding( - padding: const EdgeInsets.all(4.5), + padding: + const EdgeInsets.all(4.5), child: FutureBuilder( - future: ThumbnailUtil.extractThumbnail( + future: ThumbnailUtil + .extractThumbnail( location, _subdirectory, fileName, ), - builder: (BuildContext context, AsyncSnapshot snapshot) { - if (snapshot.connectionState == ConnectionState.waiting) { + builder: (BuildContext context, + AsyncSnapshot + snapshot) { + if (snapshot + .connectionState == + ConnectionState.waiting) { return const Padding( - padding: EdgeInsets.all(60), - child: CircularProgressIndicator()); - } else if (snapshot.error != null) { - return const Icon(Icons.error); + padding: + EdgeInsets.all(60), + child: + CircularProgressIndicator()); + } else if (snapshot.error != + null) { + return const Icon( + Icons.error); } else { return ClipRRect( - borderRadius: BorderRadius.circular( - 7.75), // Adjust the border radius as needed + borderRadius: + BorderRadius.circular( + 7.75), // Adjust the border radius as needed child: Image.file( File(snapshot.data!), fit: BoxFit diff --git a/lib/util/sl1_thumbnail.dart b/lib/util/sl1_thumbnail.dart index 196d606..9403f3d 100644 --- a/lib/util/sl1_thumbnail.dart +++ b/lib/util/sl1_thumbnail.dart @@ -23,11 +23,12 @@ import 'package:path_provider/path_provider.dart'; class ThumbnailUtil { static final _logger = Logger('ThumbnailUtil'); + static final ApiService _api = ApiService(); static Future extractThumbnail(String location, String subdirectory, String filename) async { try { String finalLocation = _isDefaultDir(subdirectory) ? filename : [subdirectory, filename].join('/'); - final bytes = await ApiService.getFileThumbnail(location, finalLocation); + final bytes = await _api.getFileThumbnail(location, finalLocation); final tempDir = await getTemporaryDirectory(); final orionTmpDir = Directory('${tempDir.path}/oriontmp/$finalLocation'); From b442f3627fdae23c3a7c611652919344586ac586 Mon Sep 17 00:00:00 2001 From: Paul_GD Date: Sat, 8 Jun 2024 04:13:34 +0200 Subject: [PATCH 12/82] feat(tools): add placeholder ToolsScreen --- lib/tools/tools_screen.dart | 63 +++++++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 lib/tools/tools_screen.dart diff --git a/lib/tools/tools_screen.dart b/lib/tools/tools_screen.dart new file mode 100644 index 0000000..806cd24 --- /dev/null +++ b/lib/tools/tools_screen.dart @@ -0,0 +1,63 @@ +/* +* Orion - Tools Screen +* Copyright (C) 2024 TheContrappostoShop +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see . +*/ + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:logging/logging.dart'; +import 'package:orion/api_services/api_services.dart'; + +class ToolsScreen extends StatefulWidget { + const ToolsScreen({super.key}); + + @override + _ToolsScreenState createState() => _ToolsScreenState(); +} + +class _ToolsScreenState extends State { + final Logger _logger = Logger('ToolsScreen'); + final ApiService _api = ApiService(); + + @override + void initState() { + super.initState(); + } + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Tools'), + ), + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: const [ + Text( + 'Tools Screen', + ), + ], + ), + ), + ); + } +} From 21cccec48c685eae28a7445407636e01b71f3206 Mon Sep 17 00:00:00 2001 From: Paul_GD Date: Sat, 8 Jun 2024 04:14:38 +0200 Subject: [PATCH 13/82] feat(settings): replace Calibrat with General tab --- lib/settings/settings_screen.dart | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/lib/settings/settings_screen.dart b/lib/settings/settings_screen.dart index 207c0d8..ff29a94 100644 --- a/lib/settings/settings_screen.dart +++ b/lib/settings/settings_screen.dart @@ -1,6 +1,6 @@ /* -* Orion - Seattings Screen -* Copyright (C) 2024 TheContrappostoShop (PaulGD0, shifubrams) +* Orion - Settings Screen +* Copyright (C) 2024 TheContrappostoShop * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -20,14 +20,13 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:orion/pubspec.dart'; import 'package:orion/settings/debug_screen.dart'; +import 'package:orion/settings/general_screen.dart'; import 'package:orion/util/markdown_screen.dart'; +import 'package:orion/settings/wifi_screen.dart'; +import 'package:orion/settings/about_screen.dart'; import 'package:provider/provider.dart'; import 'package:about/about.dart'; -import 'calibrate_screen.dart'; -import 'wifi_screen.dart'; -import 'about_screen.dart'; - class SettingsScreen extends StatefulWidget { const SettingsScreen({super.key}); @@ -77,7 +76,7 @@ class SettingsScreenState extends State { 'Version {{ version }}, Build {{ buildNumber }}', applicationName: 'Orion', applicationLegalese: - 'GPLv3 - Copyright © TheContrappostoShop {{ year buildType }}', + 'GPLv3 - Copyright © TheContrappostoShop {{ year }}', children: [ Padding( padding: const EdgeInsets.only(left: 10, right: 10), @@ -124,7 +123,7 @@ class SettingsScreenState extends State { ], ), body: _selectedIndex == 0 - ? const CalibrateScreen() + ? const GeneralCfgScreen() : _selectedIndex == 1 ? const WifiScreen() : _selectedIndex == 2 @@ -135,7 +134,7 @@ class SettingsScreenState extends State { items: const [ BottomNavigationBarItem( icon: Icon(Icons.settings), - label: 'Calibrate', + label: 'General', ), BottomNavigationBarItem( icon: Icon(Icons.network_wifi), From a7a3a359f3af15087acf00b22e0770fb99b1ba1b Mon Sep 17 00:00:00 2001 From: Paul_GD Date: Sat, 8 Jun 2024 04:15:43 +0200 Subject: [PATCH 14/82] fix(status): ensure thumbnail is only acquired once - change API usage to be non-static --- lib/status/status_screen.dart | 229 +++++++++++++++++++++++----------- 1 file changed, 158 insertions(+), 71 deletions(-) diff --git a/lib/status/status_screen.dart b/lib/status/status_screen.dart index 0d1f398..6f27d62 100644 --- a/lib/status/status_screen.dart +++ b/lib/status/status_screen.dart @@ -1,6 +1,6 @@ /* * Orion - Status Screen -* Copyright (C) 2024 TheContrappostoShop (PaulGD0, shifubrams) +* Copyright (C) 2024 TheContrappostoShop * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -36,8 +36,11 @@ class StatusScreen extends StatefulWidget { StatusScreenState createState() => StatusScreenState(); } -class StatusScreenState extends State with SingleTickerProviderStateMixin { +class StatusScreenState extends State + with SingleTickerProviderStateMixin { final _logger = Logger('StatusScreen'); + final ApiService _api = ApiService(); + double leftPadding = 0; double rightPadding = 0; @@ -53,17 +56,18 @@ class StatusScreenState extends State with SingleTickerProviderSta Future? _initStatusDetailsFuture; Map? status; double opacity = 0.0; - bool isPausing = false; - bool isCanceling = false; - Timer? timer; + bool isPausing = false; // Flag to check if pausing is in progress + bool isCanceling = false; // Flag to check if canceling is in progress + bool isThumbnailFetched = false; // Flag to check if thumbnail is fetched + Timer? timer; // Timer for fetching status @override void initState() { super.initState(); _initStatusDetailsFuture = getStatus(); newPrintNotifier = ValueNotifier(widget.newPrint); - - timer = Timer.periodic(const Duration(seconds: 1), (Timer t) => getStatus()); + timer = Timer.periodic(const Duration(seconds: 1), + (Timer t) => getStatus()); // Fetch status every second } @override @@ -74,23 +78,31 @@ class StatusScreenState extends State with SingleTickerProviderSta Future getStatus() async { try { - status = await ApiService.getStatus(); + status = await _api.getStatus(); if (status != null) { if (status!['status'] == 'Printing' || status!['status'] == 'Idle') { - if (status!['print_data'] != null && status!['print_data']['file_data'] != null) { - String? thumbnailFullPath = status!['print_data']['file_data']['path']; + if (status!['print_data'] != null && + status!['print_data']['file_data'] != null) { + String? thumbnailFullPath = + status!['print_data']['file_data']['path']; String? fileName = status!['print_data']['file_data']['name']; - String location = status!['print_data']['file_data']['location_category'] ?? 'Local'; - if (thumbnailFullPath != null && fileName != null) { + String location = status!['print_data']['file_data'] + ['location_category'] ?? + 'Local'; + if (thumbnailFullPath != null && + fileName != null && + !isThumbnailFetched) { String thumbnailSubdir = '/'; if (thumbnailFullPath.contains('/')) { - thumbnailSubdir = thumbnailFullPath.substring(0, thumbnailFullPath.lastIndexOf('/')); + thumbnailSubdir = thumbnailFullPath.substring( + 0, thumbnailFullPath.lastIndexOf('/')); } thumbnailNotifier.value = await ThumbnailUtil.extractThumbnail( location, thumbnailSubdir, fileName, ); + isThumbnailFetched = true; } } else { thumbnailNotifier.value = null; @@ -105,10 +117,12 @@ class StatusScreenState extends State with SingleTickerProviderSta } } + // Method to get the status color based on the status Color getStatusColor() { final Map statusColor = { - 'Printing': - Theme.of(context).brightness == Brightness.dark ? Theme.of(context).colorScheme.primary : Colors.black54, + 'Printing': Theme.of(context).brightness == Brightness.dark + ? Theme.of(context).colorScheme.primary + : Colors.black54, 'Idle': Colors.greenAccent, 'Shutdown': Colors.red, 'Canceled': Colors.red, @@ -119,19 +133,24 @@ class StatusScreenState extends State with SingleTickerProviderSta }; String printStatus = status!['status']; - bool curing = status!['physical_state']['curing'] ?? false; // Default to false if 'curing' is null + bool curing = status!['physical_state']['curing'] ?? + false; // Default to false if 'curing' is null bool paused = status!['paused'] ?? false; bool canceled = status!['layer'] == null; if (curing) { - return statusColor['Curing'] ?? Colors.black; // Default to black if 'Curing' is not in the map + return statusColor['Curing'] ?? + Colors.black; // Default to black if 'Curing' is not in the map } else if (paused) { - return statusColor['Pause'] ?? Colors.black; // Default to black if 'Pause' is not in the map + return statusColor['Pause'] ?? + Colors.black; // Default to black if 'Pause' is not in the map } else if (canceled) { - return statusColor['Canceled'] ?? Colors.black; // Default to black if 'Canceled' is not in the map + return statusColor['Canceled'] ?? + Colors.black; // Default to black if 'Canceled' is not in the map } else { - return statusColor[printStatus] ?? Colors.black; // Default to black if the status is not in the map + return statusColor[printStatus] ?? + Colors.black; // Default to black if the status is not in the map } } @@ -153,7 +172,9 @@ class StatusScreenState extends State with SingleTickerProviderSta ), ); } else { - if (status != null && status!['print_data'] == null && newPrintNotifier.value == false) { + if (status != null && + status!['print_data'] == null && + newPrintNotifier.value == false) { return Scaffold( appBar: AppBar( title: const Text('No Print Data Available'), @@ -163,7 +184,8 @@ class StatusScreenState extends State with SingleTickerProviderSta onPressed: () { Navigator.push( context, - MaterialPageRoute(builder: (context) => const GridFilesScreen()), + MaterialPageRoute( + builder: (context) => const GridFilesScreen()), ); }, child: const Padding( @@ -176,7 +198,8 @@ class StatusScreenState extends State with SingleTickerProviderSta ), ); } else if (status != null && - (status!['layer'] == null || status!['layer'] == status!['print_data']['layer_count']) && + (status!['layer'] == null || + status!['layer'] == status!['print_data']['layer_count']) && newPrintNotifier.value == true) { return Scaffold( appBar: AppBar( @@ -235,7 +258,8 @@ class StatusScreenState extends State with SingleTickerProviderSta rightPadding = leftPadding; setState(() { - opacity = 1.0; // Set opacity to 1 after sizes have been calculated + opacity = + 1.0; // Set opacity to 1 after sizes have been calculated }); }); @@ -243,8 +267,10 @@ class StatusScreenState extends State with SingleTickerProviderSta Duration duration = Duration(seconds: totalSeconds); String twoDigits(int n) => n.toString().padLeft(2, "0"); - String twoDigitMinutes = twoDigits(duration.inMinutes.remainder(60)); - String twoDigitSeconds = twoDigits(duration.inSeconds.remainder(60)); + String twoDigitMinutes = + twoDigits(duration.inMinutes.remainder(60)); + String twoDigitSeconds = + twoDigits(duration.inSeconds.remainder(60)); return Scaffold( appBar: AppBar( @@ -252,14 +278,19 @@ class StatusScreenState extends State with SingleTickerProviderSta title: RichText( text: TextSpan( children: [ - TextSpan(text: 'Print Status', style: Theme.of(context).appBarTheme.titleTextStyle), - TextSpan(text: ' - ', style: Theme.of(context).appBarTheme.titleTextStyle), + TextSpan( + text: 'Print Status', + style: Theme.of(context).appBarTheme.titleTextStyle), + TextSpan( + text: ' - ', + style: Theme.of(context).appBarTheme.titleTextStyle), TextSpan( text: isCanceling && status!['layer'] != null ? 'Canceling' : status!['layer'] == null ? 'Canceled' - : isPausing == true && status!['paused'] == false + : isPausing == true && + status!['paused'] == false ? 'Pausing' : status!['paused'] == true ? 'Paused' @@ -282,13 +313,18 @@ class StatusScreenState extends State with SingleTickerProviderSta mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center, children: [ - if (status!['status'] == 'Printing' || status!['status'] == 'Idle') ...[ + if (status!['status'] == 'Printing' || + status!['status'] == 'Idle') ...[ Align( alignment: Alignment.centerLeft, child: Padding( - padding: EdgeInsets.only(left: leftPadding <= 0 ? leftPadding : leftPadding - 10), + padding: EdgeInsets.only( + left: leftPadding <= 0 + ? leftPadding + : leftPadding - 10), child: ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 300), + constraints: + const BoxConstraints(maxWidth: 300), child: Card.outlined( key: textKey1, elevation: 1, @@ -299,7 +335,9 @@ class StatusScreenState extends State with SingleTickerProviderSta minFontSize: 16, '${status!['print_data']['file_data']['name']}', style: TextStyle( - fontSize: 24, fontWeight: FontWeight.bold, color: getStatusColor()), + fontSize: 24, + fontWeight: FontWeight.bold, + color: getStatusColor()), overflow: TextOverflow.ellipsis, ), ), @@ -364,10 +402,14 @@ class StatusScreenState extends State with SingleTickerProviderSta padding: EdgeInsets.only(right: rightPadding), child: ValueListenableBuilder( valueListenable: thumbnailNotifier, - builder: (BuildContext context, String? thumbnail, Widget? child) { + builder: (BuildContext context, String? thumbnail, + Widget? child) { double progress = 0.0; - if (status!['layer'] != null && status!['print_data']['layer_count'] != null) { - progress = status!['layer'] / status!['print_data']['layer_count']; + if (status!['layer'] != null && + status!['print_data']['layer_count'] != + null) { + progress = status!['layer'] / + status!['print_data']['layer_count']; } return thumbnail != null ? Stack( @@ -375,15 +417,18 @@ class StatusScreenState extends State with SingleTickerProviderSta Card( key: previewKey, child: Padding( - padding: const EdgeInsets.all(4.5), + padding: + const EdgeInsets.all(4.5), child: ClipRRect( - borderRadius: BorderRadius.circular(7.75), + borderRadius: + BorderRadius.circular(7.75), child: Image.file( File(thumbnail), width: 220, height: 220, color: Colors.black, - colorBlendMode: BlendMode.saturation, + colorBlendMode: + BlendMode.saturation, ), ), ), @@ -391,9 +436,11 @@ class StatusScreenState extends State with SingleTickerProviderSta Card( //key: previewKey, child: Padding( - padding: const EdgeInsets.all(4.5), + padding: + const EdgeInsets.all(4.5), child: ClipRRect( - borderRadius: BorderRadius.circular(7.75), + borderRadius: + BorderRadius.circular(7.75), child: Stack( children: [ Image.file( @@ -405,14 +452,22 @@ class StatusScreenState extends State with SingleTickerProviderSta width: 220, height: 220, decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.bottomCenter, - end: Alignment.topCenter, + gradient: + LinearGradient( + begin: Alignment + .bottomCenter, + end: Alignment + .topCenter, colors: [ Colors.transparent, - Colors.black.withOpacity(0.65), + Colors.black + .withOpacity( + 0.65), + ], + stops: [ + (progress), + (progress) ], - stops: [(progress), (progress)], ), ), ), @@ -423,10 +478,12 @@ class StatusScreenState extends State with SingleTickerProviderSta right: 0, child: Center( child: StatusCard( - isCanceling: isCanceling, + isCanceling: + isCanceling, isPausing: isPausing, progress: progress, - statusColor: getStatusColor(), + statusColor: + getStatusColor(), status: status!, ), ), @@ -442,9 +499,11 @@ class StatusScreenState extends State with SingleTickerProviderSta child: Padding( padding: const EdgeInsets.all(4.5), child: ClipRRect( - borderRadius: BorderRadius.circular(7.75), + borderRadius: + BorderRadius.circular(7.75), child: const Image( - image: AssetImage('assets/images/placeholder.png'), + image: AssetImage( + 'assets/images/placeholder.png'), width: 220, height: 220, ), @@ -472,9 +531,11 @@ class StatusScreenState extends State with SingleTickerProviderSta children: [ Expanded( child: ElevatedButton( - onPressed: isCanceling == true && status!['layer'] != null + onPressed: isCanceling == true && + status!['layer'] != null ? null - : status!['layer'] == null || status!['status'] == 'Idle' + : status!['layer'] == null || + status!['status'] == 'Idle' ? null : () { showDialog( @@ -482,57 +543,78 @@ class StatusScreenState extends State with SingleTickerProviderSta builder: (BuildContext context) { return Dialog( child: SizedBox( - width: MediaQuery.of(context).size.width * 0.5, // 80% of screen width + width: MediaQuery.of(context) + .size + .width * + 0.5, // 80% of screen width child: Column( mainAxisSize: MainAxisSize.min, children: [ const SizedBox(height: 10), const Padding( - padding: EdgeInsets.all(8.0), + padding: + EdgeInsets.all(8.0), child: Text( 'Options', - style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold), + style: TextStyle( + fontSize: 24, + fontWeight: + FontWeight.bold), ), ), const SizedBox(height: 10), Padding( - padding: const EdgeInsets.only(left: 20, right: 20), + padding: + const EdgeInsets.only( + left: 20, + right: 20), child: SizedBox( height: 65, width: double.infinity, child: ElevatedButton( onPressed: () { - Navigator.pop(context); + Navigator.pop( + context); Navigator.push( context, MaterialPageRoute( - builder: (context) => const SettingsScreen()), + builder: + (context) => + const SettingsScreen()), ); }, child: const Text( 'Settings', - style: TextStyle(fontSize: 24), + style: TextStyle( + fontSize: 24), ), ), ), ), - const SizedBox(height: 20), // Add some spacing between the buttons + const SizedBox( + height: + 20), // Add some spacing between the buttons Padding( - padding: const EdgeInsets.only(left: 20, right: 20), + padding: + const EdgeInsets.only( + left: 20, + right: 20), child: SizedBox( height: 65, width: double.infinity, child: ElevatedButton( onPressed: () { - Navigator.pop(context); - ApiService.cancelPrint(); + Navigator.pop( + context); + _api.cancelPrint(); setState(() { isCanceling = true; }); }, child: const Text( 'Cancel Print', - style: TextStyle(fontSize: 24), + style: TextStyle( + fontSize: 24), ), ), ), @@ -548,7 +630,8 @@ class StatusScreenState extends State with SingleTickerProviderSta style: ElevatedButton.styleFrom( minimumSize: Size( 0, // Subtract the padding on both sides - Theme.of(context).appBarTheme.toolbarHeight as double, + Theme.of(context).appBarTheme.toolbarHeight + as double, ), ), child: const Text( @@ -560,13 +643,15 @@ class StatusScreenState extends State with SingleTickerProviderSta const SizedBox(width: 20), Expanded( child: ElevatedButton( - onPressed: isCanceling == true && status!['layer'] != null + onPressed: isCanceling == true && + status!['layer'] != null ? null : isPausing == true && status!['paused'] == false ? null : status!['layer'] == null ? () { - Navigator.popUntil(context, ModalRoute.withName('/')); + Navigator.popUntil(context, + ModalRoute.withName('/')); } : status!['status'] == 'Idle' ? () { @@ -574,12 +659,12 @@ class StatusScreenState extends State with SingleTickerProviderSta } : () { if (status!['paused'] == true) { - ApiService.resumePrint(); + _api.resumePrint(); setState(() { isPausing = false; }); } else { - ApiService.pausePrint(); + _api.pausePrint(); setState(() { isPausing = true; }); @@ -588,11 +673,13 @@ class StatusScreenState extends State with SingleTickerProviderSta style: ElevatedButton.styleFrom( minimumSize: Size( 0, // Subtract the padding on both sides - Theme.of(context).appBarTheme.toolbarHeight as double, + Theme.of(context).appBarTheme.toolbarHeight + as double, ), ), child: Text( - status!['layer'] == null || status!['status'] == 'Idle' + status!['layer'] == null || + status!['status'] == 'Idle' ? 'Return to Home' : status!['paused'] == true ? 'Resume' From ad652a767a6e7a28cec81036c4d4f2f279c5c99a Mon Sep 17 00:00:00 2001 From: Paul_GD Date: Sat, 8 Jun 2024 04:23:16 +0200 Subject: [PATCH 15/82] fix(files): ensure that the back button still works if USB is unavailable --- lib/files/grid_files_screen.dart | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/files/grid_files_screen.dart b/lib/files/grid_files_screen.dart index 667efc4..d39b213 100644 --- a/lib/files/grid_files_screen.dart +++ b/lib/files/grid_files_screen.dart @@ -1,6 +1,6 @@ /* * Orion - Grid Files Screen -* Copyright (C) 2024 TheContrappostoShop (PaulGD0, shifubrams) +* Copyright (C) 2024 TheContrappostoShop * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -317,7 +317,9 @@ class GridFilesScreenState extends State { elevation: 1, child: InkWell( borderRadius: BorderRadius.circular(10), - onTap: !_usbAvailable && !_isUSB + onTap: !_usbAvailable && + !_isUSB && + _directory == _defaultDirectory ? null : _directory == _defaultDirectory ? () async { From 4f8b953f95340761e04600badf8c6415d4cb530a Mon Sep 17 00:00:00 2001 From: Paul_GD Date: Sat, 8 Jun 2024 04:29:09 +0200 Subject: [PATCH 16/82] style(all): remove person-specific copyrighting - please refer to the contributors to see all relevant persons. - this helps to prevent the need to keep the copyright notices updated at all times. --- lib/api_services/api_services.dart | 2 +- lib/files/details_screen.dart | 2 +- lib/files/files_screen.dart | 2 +- lib/files/search_file_screen.dart | 2 +- lib/home/home_screen.dart | 2 +- lib/main.dart | 2 +- lib/settings/about_screen.dart | 39 ++++++++++++------- lib/settings/calibrate_screen.dart | 2 +- lib/settings/debug_screen.dart | 27 ++++++++----- lib/settings/wifi_screen.dart | 2 +- lib/status/error_print_failure.dart | 2 +- lib/status/fatal_error_screen.dart | 2 +- lib/status/normal_error_screen.dart | 2 +- lib/themes/themes.dart | 2 +- lib/util/error_handler.dart | 18 +++++++++ lib/util/locales/de_DE.orionkb.dart | 2 +- lib/util/locales/en_US.orionkb.dart | 2 +- lib/util/localization.dart | 2 +- lib/util/markdown_screen.dart | 2 +- .../orion_api_directory.dart | 2 +- .../orion_api_filesystem/orion_api_file.dart | 2 +- .../orion_api_filesystem/orion_api_item.dart | 2 +- lib/util/orion_kb/orion_keyboard.dart | 2 +- .../orion_kb/orion_keyboard_expander.dart | 2 +- lib/util/orion_kb/orion_keyboard_modal.dart | 2 +- lib/util/orion_kb/orion_textfield.dart | 2 +- lib/util/orion_kb/orion_textfield_spawn.dart | 2 +- lib/util/orion_list_tile.dart | 4 +- lib/util/sl1_thumbnail.dart | 12 ++++-- lib/util/status_card.dart | 8 ++-- 30 files changed, 101 insertions(+), 55 deletions(-) diff --git a/lib/api_services/api_services.dart b/lib/api_services/api_services.dart index 2e52be7..f3faea7 100644 --- a/lib/api_services/api_services.dart +++ b/lib/api_services/api_services.dart @@ -1,6 +1,6 @@ /* * Orion - Odyssey API Service -* Copyright (C) 2024 TheContrappostoShop (PaulGD0, shifubrams) +* Copyright (C) 2024 TheContrappostoShop * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/lib/files/details_screen.dart b/lib/files/details_screen.dart index 125db64..54f8dc1 100644 --- a/lib/files/details_screen.dart +++ b/lib/files/details_screen.dart @@ -1,6 +1,6 @@ /* * Orion - Detail Screen -* Copyright (C) 2024 TheContrappostoShop (PaulGD0, shifubrams) +* Copyright (C) 2024 TheContrappostoShop * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/lib/files/files_screen.dart b/lib/files/files_screen.dart index 61a5577..0beb517 100644 --- a/lib/files/files_screen.dart +++ b/lib/files/files_screen.dart @@ -1,6 +1,6 @@ /* * Orion - Files Screen -* Copyright (C) 2024 TheContrappostoShop (PaulGD0, shifubrams) +* Copyright (C) 2024 TheContrappostoShop * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/lib/files/search_file_screen.dart b/lib/files/search_file_screen.dart index dd8c686..1ab5103 100644 --- a/lib/files/search_file_screen.dart +++ b/lib/files/search_file_screen.dart @@ -1,6 +1,6 @@ /* * Orion - Search File Screen -* Copyright (C) 2024 TheContrappostoShop (PaulGD0, shifubrams) +* Copyright (C) 2024 TheContrappostoShop * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/lib/home/home_screen.dart b/lib/home/home_screen.dart index e59b6cb..403ed85 100644 --- a/lib/home/home_screen.dart +++ b/lib/home/home_screen.dart @@ -1,6 +1,6 @@ /* * Orion - Home Screen -* Copyright (C) 2024 TheContrappostoShop (PaulGD0, shifubrams) +* Copyright (C) 2024 TheContrappostoShop * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/lib/main.dart b/lib/main.dart index 2594ddd..8b27ae8 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,6 +1,6 @@ /* * Orion - An open-source user interface for the Odyssey 3d-printing engine. -* Copyright (C) 2024 TheContrappostoShop (PaulGD0, shifubrams) +* Copyright (C) 2024 TheContrappostoShop * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/lib/settings/about_screen.dart b/lib/settings/about_screen.dart index 84acba7..4130704 100644 --- a/lib/settings/about_screen.dart +++ b/lib/settings/about_screen.dart @@ -1,6 +1,6 @@ /* * Orion - About Screen -* Copyright (C) 2024 TheContrappostoShop (PaulGD0, shifubrams) +* Copyright (C) 2024 TheContrappostoShop * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -87,7 +87,8 @@ class _AboutScreenState extends State { title: const Text('You are already a developer'), alignment: Alignment.topCenter, primaryColor: Colors.green, - backgroundColor: Theme.of(context).colorScheme.surface.withBrightness(1.35), + backgroundColor: + Theme.of(context).colorScheme.surface.withBrightness(1.35), foregroundColor: Theme.of(context).colorScheme.onSurface, ); } else { @@ -99,10 +100,12 @@ class _AboutScreenState extends State { type: ToastificationType.success, style: ToastificationStyle.fillColored, autoCloseDuration: const Duration(seconds: 2), - title: const Text('Developer Mode Activated: You are now a developer!'), + title: const Text( + 'Developer Mode Activated: You are now a developer!'), alignment: Alignment.topCenter, primaryColor: Colors.green, - backgroundColor: Theme.of(context).colorScheme.surface.withBrightness(1.35), + backgroundColor: + Theme.of(context).colorScheme.surface.withBrightness(1.35), foregroundColor: Theme.of(context).colorScheme.onSurface, ); } else { @@ -115,7 +118,8 @@ class _AboutScreenState extends State { 'You are ${5 - qrTapCount} ${5 - qrTapCount == 1 ? 'tap' : 'taps'} away from becoming a developer'), alignment: Alignment.topCenter, primaryColor: Theme.of(context).colorScheme.primary, - backgroundColor: Theme.of(context).colorScheme.surface.withBrightness(1.35), + backgroundColor: + Theme.of(context).colorScheme.surface.withBrightness(1.35), foregroundColor: Theme.of(context).colorScheme.onSurface, showProgressBar: false, ); @@ -125,9 +129,11 @@ class _AboutScreenState extends State { } const String title = kDebugMode ? 'Debug Machine' : 'Prometheus mSLA'; - const String serialNumber = kDebugMode ? 'S/N: DBG-0001-001' : 'No S/N Available'; + const String serialNumber = + kDebugMode ? 'S/N: DBG-0001-001' : 'No S/N Available'; const String apiVersion = kDebugMode ? 'Odyssey: Simulated' : 'Odyssey: ?'; - const String boardType = kDebugMode ? 'Hardware: Debugger' : 'Hardware: Apollo 3.5.2'; + const String boardType = + kDebugMode ? 'Hardware: Debugger' : 'Hardware: Apollo 3.5.2'; return Scaffold( body: Stack( @@ -143,7 +149,10 @@ class _AboutScreenState extends State { child: Text( title, key: textKey1, - style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold, color: _standardColor), + style: TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + color: _standardColor), ), ), ), @@ -170,18 +179,21 @@ class _AboutScreenState extends State { child: FittedBox( child: FutureBuilder( future: getVersionNumber(), - builder: (BuildContext context, AsyncSnapshot snapshot) { + builder: (BuildContext context, + AsyncSnapshot snapshot) { if (snapshot.hasData) { return Text( 'Orion: ${snapshot.data}', key: textKey3, - style: TextStyle(fontSize: 20, color: _standardColor), + style: + TextStyle(fontSize: 20, color: _standardColor), ); } else { return Text( 'Orion: N/A', key: textKey3, - style: TextStyle(fontSize: 20, color: _standardColor), + style: + TextStyle(fontSize: 20, color: _standardColor), ); } }, @@ -233,8 +245,9 @@ class _AboutScreenState extends State { version: QrVersions.auto, size: 220, eyeStyle: QrEyeStyle(color: _standardColor), - dataModuleStyle: - QrDataModuleStyle(color: _standardColor, dataModuleShape: QrDataModuleShape.circle), + dataModuleStyle: QrDataModuleStyle( + color: _standardColor, + dataModuleShape: QrDataModuleShape.circle), ), ], ), diff --git a/lib/settings/calibrate_screen.dart b/lib/settings/calibrate_screen.dart index 0a02554..900ba22 100644 --- a/lib/settings/calibrate_screen.dart +++ b/lib/settings/calibrate_screen.dart @@ -1,6 +1,6 @@ /* * Orion - Calibrate Screen -* Copyright (C) 2024 TheContrappostoShop (PaulGD0, shifubrams) +* Copyright (C) 2024 TheContrappostoShop * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/lib/settings/debug_screen.dart b/lib/settings/debug_screen.dart index 169877e..2288d4d 100644 --- a/lib/settings/debug_screen.dart +++ b/lib/settings/debug_screen.dart @@ -1,6 +1,6 @@ /* * Orion - Debug Screen -* Copyright (C) 2024 TheContrappostoShop (PaulGD0, shifubrams) +* Copyright (C) 2024 TheContrappostoShop * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -31,8 +31,10 @@ class DebugScreen extends StatefulWidget { } class DebugScreenState extends State { - final GlobalKey debugTextFieldKey = GlobalKey(); - final GlobalKey dialogTextFieldKey = GlobalKey(); + final GlobalKey debugTextFieldKey = + GlobalKey(); + final GlobalKey dialogTextFieldKey = + GlobalKey(); final ScrollController _scrollController = ScrollController(); @override @@ -63,7 +65,8 @@ class DebugScreenState extends State { ), ), Padding( - padding: const EdgeInsets.symmetric(horizontal: 30, vertical: 15), + padding: const EdgeInsets.symmetric( + horizontal: 30, vertical: 15), child: SpawnOrionTextField( key: debugTextFieldKey, keyboardHint: "Debug Test Field", @@ -76,7 +79,8 @@ class DebugScreenState extends State { padding: const EdgeInsets.all(10), child: ElevatedButton( onPressed: () { - String text = debugTextFieldKey.currentState!.getCurrentText(); + String text = + debugTextFieldKey.currentState!.getCurrentText(); if (kDebugMode) { print("[Debug]: DebugTestField content: $text"); } @@ -96,19 +100,24 @@ class DebugScreenState extends State { context: context, builder: (BuildContext context) { return AlertDialog( - title: const Center(child: Text('WiFi-Test-0123 Password')), + title: const Center( + child: Text('WiFi-Test-0123 Password')), content: SizedBox( - width: MediaQuery.of(context).size.width * 0.5, // Half the screen width + width: MediaQuery.of(context).size.width * + 0.5, // Half the screen width child: SingleChildScrollView( child: Column( children: [ SpawnOrionTextField( key: dialogTextFieldKey, keyboardHint: "Enter Password", - locale: Localizations.localeOf(context).toString(), + locale: + Localizations.localeOf(context) + .toString(), scrollController: _scrollController, ), - OrionKbExpander(textFieldKey: dialogTextFieldKey), + OrionKbExpander( + textFieldKey: dialogTextFieldKey), ], ), ), diff --git a/lib/settings/wifi_screen.dart b/lib/settings/wifi_screen.dart index 7467d40..40f1ab8 100644 --- a/lib/settings/wifi_screen.dart +++ b/lib/settings/wifi_screen.dart @@ -1,6 +1,6 @@ /* * Orion - WiFi Screen -* Copyright (C) 2024 TheContrappostoShop (PaulGD0, shifubrams) +* Copyright (C) 2024 TheContrappostoShop * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/lib/status/error_print_failure.dart b/lib/status/error_print_failure.dart index 25b00bf..f45e2b0 100644 --- a/lib/status/error_print_failure.dart +++ b/lib/status/error_print_failure.dart @@ -1,6 +1,6 @@ /* * Orion - Print Error Screen -* Copyright (C) 2024 TheContrappostoShop (PaulGD0, shifubrams) +* Copyright (C) 2024 TheContrappostoShop * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/lib/status/fatal_error_screen.dart b/lib/status/fatal_error_screen.dart index 65c1aa3..e4d468f 100644 --- a/lib/status/fatal_error_screen.dart +++ b/lib/status/fatal_error_screen.dart @@ -1,6 +1,6 @@ /* * Orion - Fatal Error Screen -* Copyright (C) 2024 TheContrappostoShop (PaulGD0, shifubrams) +* Copyright (C) 2024 TheContrappostoShop * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/lib/status/normal_error_screen.dart b/lib/status/normal_error_screen.dart index d15530b..b91c2c8 100644 --- a/lib/status/normal_error_screen.dart +++ b/lib/status/normal_error_screen.dart @@ -1,6 +1,6 @@ /* * Orion - Normal Error Screen -* Copyright (C) 2024 TheContrappostoShop (PaulGD0, shifubrams) +* Copyright (C) 2024 TheContrappostoShop * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/lib/themes/themes.dart b/lib/themes/themes.dart index 48edc75..5abed25 100644 --- a/lib/themes/themes.dart +++ b/lib/themes/themes.dart @@ -1,6 +1,6 @@ /* * Orion - Themes -* Copyright (C) 2024 TheContrappostoShop (PaulGD0, shifubrams) +* Copyright (C) 2024 TheContrappostoShop * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/lib/util/error_handler.dart b/lib/util/error_handler.dart index 0342729..958d6ac 100644 --- a/lib/util/error_handler.dart +++ b/lib/util/error_handler.dart @@ -1,3 +1,21 @@ +/* +* Orion - Error Handler +* Copyright (C) 2024 TheContrappostoShop +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see . +*/ + import 'package:flutter/foundation.dart'; import 'package:logging/logging.dart'; diff --git a/lib/util/locales/de_DE.orionkb.dart b/lib/util/locales/de_DE.orionkb.dart index 7bfcc60..6a56248 100644 --- a/lib/util/locales/de_DE.orionkb.dart +++ b/lib/util/locales/de_DE.orionkb.dart @@ -1,6 +1,6 @@ /* * Orion - de_DE Keyboard Layout -* Copyright (C) 2024 TheContrappostoShop (PaulGD0, shifubrams) +* Copyright (C) 2024 TheContrappostoShop * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/lib/util/locales/en_US.orionkb.dart b/lib/util/locales/en_US.orionkb.dart index bd45f28..01ecf07 100644 --- a/lib/util/locales/en_US.orionkb.dart +++ b/lib/util/locales/en_US.orionkb.dart @@ -1,6 +1,6 @@ /* * Orion - en_US Keyboard Layout -* Copyright (C) 2024 TheContrappostoShop (PaulGD0, shifubrams) +* Copyright (C) 2024 TheContrappostoShop * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/lib/util/localization.dart b/lib/util/localization.dart index 5fceec5..8210801 100644 --- a/lib/util/localization.dart +++ b/lib/util/localization.dart @@ -1,6 +1,6 @@ /* * Orion - Localization -* Copyright (C) 2024 TheContrappostoShop (PaulGD0, shifubrams) +* Copyright (C) 2024 TheContrappostoShop * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/lib/util/markdown_screen.dart b/lib/util/markdown_screen.dart index 4d6a518..61fe5ed 100644 --- a/lib/util/markdown_screen.dart +++ b/lib/util/markdown_screen.dart @@ -1,6 +1,6 @@ /* * Orion - Markdown Screen -* Copyright (C) 2024 TheContrappostoShop (PaulGD0, shifubrams) +* Copyright (C) 2024 TheContrappostoShop * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/lib/util/orion_api_filesystem/orion_api_directory.dart b/lib/util/orion_api_filesystem/orion_api_directory.dart index e9eb069..d5a10f5 100644 --- a/lib/util/orion_api_filesystem/orion_api_directory.dart +++ b/lib/util/orion_api_filesystem/orion_api_directory.dart @@ -1,6 +1,6 @@ /* * Orion - Orion API Directory -* Copyright (C) 2024 TheContrappostoShop (PaulGD0, shifubrams) +* Copyright (C) 2024 TheContrappostoShop * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/lib/util/orion_api_filesystem/orion_api_file.dart b/lib/util/orion_api_filesystem/orion_api_file.dart index a043cd8..e081bac 100644 --- a/lib/util/orion_api_filesystem/orion_api_file.dart +++ b/lib/util/orion_api_filesystem/orion_api_file.dart @@ -1,6 +1,6 @@ /* * Orion - Orion API File -* Copyright (C) 2024 TheContrappostoShop (PaulGD0, shifubrams) +* Copyright (C) 2024 TheContrappostoShop * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/lib/util/orion_api_filesystem/orion_api_item.dart b/lib/util/orion_api_filesystem/orion_api_item.dart index 5628977..05e1791 100644 --- a/lib/util/orion_api_filesystem/orion_api_item.dart +++ b/lib/util/orion_api_filesystem/orion_api_item.dart @@ -1,6 +1,6 @@ /* * Orion - Orion API Item -* Copyright (C) 2024 TheContrappostoShop (PaulGD0, shifubrams) +* Copyright (C) 2024 TheContrappostoShop * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/lib/util/orion_kb/orion_keyboard.dart b/lib/util/orion_kb/orion_keyboard.dart index 18a50b4..67010c0 100644 --- a/lib/util/orion_kb/orion_keyboard.dart +++ b/lib/util/orion_kb/orion_keyboard.dart @@ -1,6 +1,6 @@ /* * Orion - Orion Keyboard -* Copyright (C) 2024 TheContrappostoShop (PaulGD0, shifubrams) +* Copyright (C) 2024 TheContrappostoShop * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/lib/util/orion_kb/orion_keyboard_expander.dart b/lib/util/orion_kb/orion_keyboard_expander.dart index 9b7ee0d..87061a7 100644 --- a/lib/util/orion_kb/orion_keyboard_expander.dart +++ b/lib/util/orion_kb/orion_keyboard_expander.dart @@ -1,6 +1,6 @@ /* * Orion - Orion Keyboard Expander -* Copyright (C) 2024 TheContrappostoShop (PaulGD0, shifubrams) +* Copyright (C) 2024 TheContrappostoShop * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/lib/util/orion_kb/orion_keyboard_modal.dart b/lib/util/orion_kb/orion_keyboard_modal.dart index 02339ab..6f03d91 100644 --- a/lib/util/orion_kb/orion_keyboard_modal.dart +++ b/lib/util/orion_kb/orion_keyboard_modal.dart @@ -1,6 +1,6 @@ /* * Orion - Orion Keyboard Modal -* Copyright (C) 2024 TheContrappostoShop (PaulGD0, shifubrams) +* Copyright (C) 2024 TheContrappostoShop * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/lib/util/orion_kb/orion_textfield.dart b/lib/util/orion_kb/orion_textfield.dart index ec19131..b56367f 100644 --- a/lib/util/orion_kb/orion_textfield.dart +++ b/lib/util/orion_kb/orion_textfield.dart @@ -1,6 +1,6 @@ /* * Orion - Orion Textfield -* Copyright (C) 2024 TheContrappostoShop (PaulGD0, shifubrams) +* Copyright (C) 2024 TheContrappostoShop * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/lib/util/orion_kb/orion_textfield_spawn.dart b/lib/util/orion_kb/orion_textfield_spawn.dart index 5a997ff..c78756d 100644 --- a/lib/util/orion_kb/orion_textfield_spawn.dart +++ b/lib/util/orion_kb/orion_textfield_spawn.dart @@ -1,6 +1,6 @@ /* * Orion - Orion Textfield Spawner -* Copyright (C) 2024 TheContrappostoShop (PaulGD0, shifubrams) +* Copyright (C) 2024 TheContrappostoShop * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/lib/util/orion_list_tile.dart b/lib/util/orion_list_tile.dart index 4afb831..7da5bbc 100644 --- a/lib/util/orion_list_tile.dart +++ b/lib/util/orion_list_tile.dart @@ -1,6 +1,6 @@ /* -* Orion - Orion List Tile -* Copyright (C) 2024 TheContrappostoShop (PaulGD03) +* Orion - List Tile +* Copyright (C) 2024 TheContrappostoShop * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/lib/util/sl1_thumbnail.dart b/lib/util/sl1_thumbnail.dart index 9403f3d..edb983c 100644 --- a/lib/util/sl1_thumbnail.dart +++ b/lib/util/sl1_thumbnail.dart @@ -1,6 +1,6 @@ /* * Orion - Thumbnail Util -* Copyright (C) 2024 TheContrappostoShop (PaulGD0) +* Copyright (C) 2024 TheContrappostoShop * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -25,9 +25,12 @@ class ThumbnailUtil { static final _logger = Logger('ThumbnailUtil'); static final ApiService _api = ApiService(); - static Future extractThumbnail(String location, String subdirectory, String filename) async { + static Future extractThumbnail( + String location, String subdirectory, String filename) async { try { - String finalLocation = _isDefaultDir(subdirectory) ? filename : [subdirectory, filename].join('/'); + String finalLocation = _isDefaultDir(subdirectory) + ? filename + : [subdirectory, filename].join('/'); final bytes = await _api.getFileThumbnail(location, finalLocation); final tempDir = await getTemporaryDirectory(); @@ -51,7 +54,8 @@ class ThumbnailUtil { // If the total size exceeds 100MB, delete the oldest files if (totalSize > 100 * 1024 * 1024) { - files.sort((a, b) => a.statSync().modified.compareTo(b.statSync().modified)); + files.sort( + (a, b) => a.statSync().modified.compareTo(b.statSync().modified)); while (totalSize > 100 * 1024 * 1024 && files.isNotEmpty) { int fileSize = await (files.first as File).length(); await files.first.delete(); diff --git a/lib/util/status_card.dart b/lib/util/status_card.dart index 2318b86..d7089ee 100644 --- a/lib/util/status_card.dart +++ b/lib/util/status_card.dart @@ -1,6 +1,6 @@ /* * Orion - Status Card -* Copyright (C) 2024 TheContrappostoShop (PaulGD0) +* Copyright (C) 2024 TheContrappostoShop * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -54,7 +54,8 @@ class StatusCardState extends State { // When paused, show current print progress. // When canceled, show a full circle. final circleProgress = - (widget.isPausing && widget.status['paused'] != true) || (widget.isCanceling && widget.status['layer'] != null) + (widget.isPausing && widget.status['paused'] != true) || + (widget.isCanceling && widget.status['layer'] != null) ? null : widget.progress == 0.0 ? 1.0 @@ -105,7 +106,8 @@ class StatusCardState extends State { child: CircularProgressIndicator( value: circleProgress, strokeWidth: 6, - valueColor: AlwaysStoppedAnimation(widget.statusColor), + valueColor: + AlwaysStoppedAnimation(widget.statusColor), backgroundColor: widget.statusColor.withOpacity(0.5), ), ), From 6105180eab8ad87c092321464d690092f1fd6920 Mon Sep 17 00:00:00 2001 From: Paul_GD Date: Mon, 10 Jun 2024 02:52:57 +0200 Subject: [PATCH 17/82] fix(api_services): handle empty responses in manual commands --- lib/api_services/api_services.dart | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/api_services/api_services.dart b/lib/api_services/api_services.dart index f3faea7..2181f88 100644 --- a/lib/api_services/api_services.dart +++ b/lib/api_services/api_services.dart @@ -218,7 +218,7 @@ class ApiService { _logger.info("move height=$height"); final response = await odysseyPost('/manual', {'z': height}); - return json.decode(response.body); + return json.decode(response.body == '' ? '{}' : response.body); } // Toggle cure @@ -227,7 +227,7 @@ class ApiService { _logger.info("manualCure cure=$cure"); final response = await odysseyPost('/manual', {'cure': cure}); - return json.decode(response.body); + return json.decode(response.body == '' ? '{}' : response.body); } // Home Z axis @@ -235,7 +235,7 @@ class ApiService { _logger.info("manualHome"); final response = await odysseyPost('/manual/home', {}); - return json.decode(response.body); + return json.decode(response.body == '' ? '{}' : response.body); } // Issue hardware-layer command @@ -245,7 +245,7 @@ class ApiService { final response = await odysseyPost('/manual/hardware_command', {'command': command}); - return json.decode(response.body); + return json.decode(response.body == '' ? '{}' : response.body); } /// From 319f021c4cfd230c77502a9ade440002537f45d6 Mon Sep 17 00:00:00 2001 From: Paul_GD Date: Mon, 10 Jun 2024 02:53:34 +0200 Subject: [PATCH 18/82] fix(status): reset thumbnail status when starting new print --- lib/status/status_screen.dart | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/status/status_screen.dart b/lib/status/status_screen.dart index 6f27d62..bc8f7b1 100644 --- a/lib/status/status_screen.dart +++ b/lib/status/status_screen.dart @@ -66,6 +66,9 @@ class StatusScreenState extends State super.initState(); _initStatusDetailsFuture = getStatus(); newPrintNotifier = ValueNotifier(widget.newPrint); + if (widget.newPrint == true) { + isThumbnailFetched = false; + } timer = Timer.periodic(const Duration(seconds: 1), (Timer t) => getStatus()); // Fetch status every second } From f59a75a2c1b9baf17a8b6db16c2df927171bd883 Mon Sep 17 00:00:00 2001 From: Paul_GD Date: Mon, 10 Jun 2024 02:54:34 +0200 Subject: [PATCH 19/82] feat(home): add Power Options dialog to HomeScreen - when using a custom (remote) Odyssey URL, only FIRMWARE_RESTART is available - missing remote command endpoint --- lib/home/home_screen.dart | 148 +++++++++++++++++++++++++++++++------- 1 file changed, 123 insertions(+), 25 deletions(-) diff --git a/lib/home/home_screen.dart b/lib/home/home_screen.dart index 403ed85..b59ccb1 100644 --- a/lib/home/home_screen.dart +++ b/lib/home/home_screen.dart @@ -17,19 +17,29 @@ */ import 'dart:async'; +import 'dart:io'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; +import 'package:orion/api_services/api_services.dart'; +import 'package:orion/main.dart'; +import 'package:orion/util/orion_config.dart'; -class HomeScreen extends StatelessWidget { +class HomeScreen extends StatefulWidget { const HomeScreen({super.key}); + + @override + HomeScreenState createState() => HomeScreenState(); +} + +class HomeScreenState extends State { + final ApiService _api = ApiService(); + final OrionConfig _config = OrionConfig(); + bool isRemote = false; + @override Widget build(BuildContext context) { Size homeBtnSize = const Size(double.infinity, double.infinity); - double iconSize = - MediaQuery.of(context).size.width * 0.1; // 10% of screen width - double textSize = - MediaQuery.of(context).size.width * 0.05; // 5% of screen width final theme = Theme.of(context).copyWith( elevatedButtonTheme: ElevatedButtonThemeData( @@ -52,26 +62,114 @@ class HomeScreen extends StatelessWidget { return Scaffold( appBar: AppBar( - title: Stack( - alignment: Alignment.center, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - Padding( - padding: EdgeInsets.only( - left: MediaQuery.of(context).size.width * 0.02), - child: const LiveClock(), - ), - ], - ), - const Text( - 'Prometheus mSLA', - textAlign: TextAlign.center, - ), - ], + title: const Text( + 'Prometheus mSLA', + textAlign: TextAlign.center, ), centerTitle: true, + leadingWidth: 120, + leading: const Center( + child: Padding( + padding: EdgeInsets.only(left: 15), + child: LiveClock(), + ), + ), + actions: [ + Padding( + padding: const EdgeInsets.only(right: 25), + child: InkWell( + onTap: () { + isRemote = + _config.getFlag('useCustomUrl', category: 'advanced'); + showDialog( + context: context, + builder: (BuildContext context) { + return Dialog( + child: SizedBox( + width: MediaQuery.of(context).size.width * + 0.5, // 80% of screen width + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const SizedBox(height: 10), + Padding( + padding: const EdgeInsets.all(8.0), + child: Text( + 'Power Options ${isRemote ? '(Remote)' : '(Local)'}', + style: const TextStyle( + fontSize: 24, fontWeight: FontWeight.bold), + ), + ), + const SizedBox(height: 10), + Padding( + padding: + const EdgeInsets.only(left: 20, right: 20), + child: SizedBox( + height: 65, + width: double.infinity, + child: ElevatedButton( + onPressed: () { + _api.manualCommand('FIRMWARE_RESTART'); + }, + child: const Text( + 'Firmware Restart', + style: TextStyle(fontSize: 24), + ), + ), + ), + ), + const SizedBox( + height: + 20), // Add some spacing between the buttons + if (!isRemote) + Padding( + padding: + const EdgeInsets.only(left: 20, right: 20), + child: SizedBox( + height: 65, + width: double.infinity, + child: ElevatedButton( + onPressed: () { + Process.run('sudo', ['reboot', 'now']); + }, + child: const Text( + 'Reboot System', + style: TextStyle(fontSize: 24), + ), + ), + ), + ), + if (!isRemote) const SizedBox(height: 20), + if (!isRemote) + Padding( + padding: + const EdgeInsets.only(left: 20, right: 20), + child: SizedBox( + height: 65, + width: double.infinity, + child: ElevatedButton( + onPressed: () { + Process.run('sudo', ['shutdown', 'now']); + }, + child: const Text( + 'Shutdown System', + style: TextStyle(fontSize: 24), + ), + ), + ), + ), + if (!isRemote) const SizedBox(height: 20), + ], + ), + ), + ); + }, + ); + }, + child: const Icon(Icons.power_settings_new_outlined, size: 38), + ), + ), + ], ), body: Center( child: LayoutBuilder( @@ -182,7 +280,7 @@ class LiveClockState extends State { @override Widget build(BuildContext context) { return Text( - '${_dateTime.hour.toString().padLeft(2, '0')}:${_dateTime.minute.toString().padLeft(2, '0')}', - ); + '${_dateTime.hour.toString().padLeft(2, '0')}:${_dateTime.minute.toString().padLeft(2, '0')}', + style: const TextStyle(fontSize: 28)); } } From d600d309afd00ae492bcc25ddbb50d37f9b4a92b Mon Sep 17 00:00:00 2001 From: Paul_GD Date: Sat, 15 Jun 2024 17:04:55 +0200 Subject: [PATCH 20/82] feat(error_handling): imlement ErrorDialog - provides list of different error codes - ErrorHandler was also moved into error_handling --- lib/main.dart | 2 +- lib/util/error_handling/error_details.dart | 83 +++++++++++++++++++ lib/util/error_handling/error_dialog.dart | 50 +++++++++++ .../{ => error_handling}/error_handler.dart | 0 4 files changed, 134 insertions(+), 1 deletion(-) create mode 100644 lib/util/error_handling/error_details.dart create mode 100644 lib/util/error_handling/error_dialog.dart rename lib/util/{ => error_handling}/error_handler.dart (100%) diff --git a/lib/main.dart b/lib/main.dart index 8b27ae8..0892529 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -29,7 +29,7 @@ import 'package:orion/settings/calibrate_screen.dart'; import 'package:orion/settings/wifi_screen.dart'; import 'package:orion/settings/about_screen.dart'; import 'package:orion/themes/themes.dart'; -import 'package:orion/util/error_handler.dart'; +import 'package:orion/util/error_handling/error_handler.dart'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; diff --git a/lib/util/error_handling/error_details.dart b/lib/util/error_handling/error_details.dart new file mode 100644 index 0000000..ac377e5 --- /dev/null +++ b/lib/util/error_handling/error_details.dart @@ -0,0 +1,83 @@ +/* +* Orion - Error Details +* Copyright (C) 2024 TheContrappostoShop +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see . +*/ + +class ErrorDetails { + final String title; + final String message; + + ErrorDetails(this.title, this.message); +} + +final Map errorLookupTable = { + 'default': ErrorDetails( + 'Unknown Error', + 'An unknown error has occurred. Please contact support.', + ), + 'PINK-CARROT': ErrorDetails( + 'Odyssey API Error', + 'An Error has occurred while fetching files!\n' + 'Please ensure that Odyssey is running and accessible.\n\n' + 'If the issue persists, please contact support.\n' + 'Error Code: PINK-CARROT', + ), + 'BLUE-BANANA': ErrorDetails( + 'Network Error', + 'A network error has occurred. Please check your internet connection and try again.\n\n' + 'Error Code: BLUE-BANANA', + ), + 'RED-APPLE': ErrorDetails( + 'Resin Level Low', + 'The resin level is too low. Please refill the resin tank.\n\n' + 'Error Code: RED-APPLE', + ), + 'GREEN-GRAPE': ErrorDetails( + 'Print Failure', + 'The print has failed. Please check the model and try again.\n\n' + 'Error Code: GREEN-GRAPE', + ), + 'YELLOW-LEMON': ErrorDetails( + 'Temperature Error', + 'The temperature is outside the acceptable range. Please check the printer environment.\n\n' + 'Error Code: YELLOW-LEMON', + ), + 'ORANGE-ORANGE': ErrorDetails( + 'UV Light Error', + 'The UV light is not functioning correctly. Please check the light source.\n\n' + 'Error Code: ORANGE-ORANGE', + ), + 'PURPLE-PLUM': ErrorDetails( + 'Build Plate Error', + 'The build plate is not correctly calibrated. Please recalibrate the build plate.\n\n' + 'Error Code: PURPLE-PLUM', + ), + 'BROWN-BEAR': ErrorDetails( + 'Firmware Update Error', + 'There was an error during the firmware update. Please try again.\n\n' + 'Error Code: BROWN-BEAR', + ), + 'BLACK-BERRY': ErrorDetails( + 'File Error', + 'The selected file cannot be read. Please check the file and try again.\n\n' + 'Error Code: BLACK-BERRY', + ), + 'WHITE-WOLF': ErrorDetails( + 'Sensor Error', + 'A sensor is not working correctly. Please check the printer sensors.\n\n' + 'Error Code: WHITE-WOLF', + ), +}; diff --git a/lib/util/error_handling/error_dialog.dart b/lib/util/error_handling/error_dialog.dart new file mode 100644 index 0000000..880dec5 --- /dev/null +++ b/lib/util/error_handling/error_dialog.dart @@ -0,0 +1,50 @@ +/* +* Orion - Error Dialog +* Copyright (C) 2024 TheContrappostoShop +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see . +*/ + +import 'package:flutter/material.dart'; +import 'package:orion/util/error_handling/error_details.dart'; + +void showErrorDialog(BuildContext context, String errorCode) { + ErrorDetails? errorDetails = + errorLookupTable[errorCode] ?? errorLookupTable['default']; + + WidgetsBinding.instance.addPostFrameCallback((_) { + showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + title: Text(errorDetails!.title), + content: Text( + errorDetails.message, + style: const TextStyle(color: Colors.grey), + ), + actions: [ + TextButton( + onPressed: () { + Navigator.of(context).pop(); + }, + child: const Text( + 'Close', + style: TextStyle(fontSize: 20), + ), + ), + ], + ); + }); + }); +} diff --git a/lib/util/error_handler.dart b/lib/util/error_handling/error_handler.dart similarity index 100% rename from lib/util/error_handler.dart rename to lib/util/error_handling/error_handler.dart From 92ca99e66771f39fa33b214ac9ae84810aea5b6b Mon Sep 17 00:00:00 2001 From: Paul_GD Date: Sat, 15 Jun 2024 17:12:47 +0200 Subject: [PATCH 21/82] style(orionpi): remove name-specific copyright --- orionpi.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/orionpi.sh b/orionpi.sh index 6cc79a7..7f710f3 100755 --- a/orionpi.sh +++ b/orionpi.sh @@ -1,7 +1,7 @@ #!/bin/bash # # Orion - An open-source user interface for the Odyssey 3d-printing engine. -# Copyright (C) 2024 TheContrappostoShop (PaulGD0, shifubrams) +# Copyright (C) 2024 TheContrappostoShop # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by From d2fa547d119d81c1f762bb9c0836eb489b9b6ed6 Mon Sep 17 00:00:00 2001 From: Paul_GD Date: Sat, 15 Jun 2024 17:13:44 +0200 Subject: [PATCH 22/82] feat(api_services): implement displayTest method - supports White, Blank, Grid, Measure - fix manual commands by adding toString conversion --- lib/api_services/api_services.dart | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/lib/api_services/api_services.dart b/lib/api_services/api_services.dart index 2181f88..ffd05a1 100644 --- a/lib/api_services/api_services.dart +++ b/lib/api_services/api_services.dart @@ -217,7 +217,7 @@ class ApiService { Future> move(double height) async { _logger.info("move height=$height"); - final response = await odysseyPost('/manual', {'z': height}); + final response = await odysseyPost('/manual', {'z': height.toString()}); return json.decode(response.body == '' ? '{}' : response.body); } @@ -226,7 +226,7 @@ class ApiService { Future> manualCure(bool cure) async { _logger.info("manualCure cure=$cure"); - final response = await odysseyPost('/manual', {'cure': cure}); + final response = await odysseyPost('/manual', {'cure': cure.toString()}); return json.decode(response.body == '' ? '{}' : response.body); } @@ -248,6 +248,18 @@ class ApiService { return json.decode(response.body == '' ? '{}' : response.body); } + // Display a test pattern on the screen + // Takes 1 param test [String] which holds the test to display + Future displayTest(String test) async { + _logger.info("displayTest test=$test"); + + final queryParams = { + 'test': test, + }; + + await odysseyPost('/manual/display_test', queryParams); + } + /// /// DELETE METHODS TO ODYSSEY /// From 173c9f79d4799b3d873ae9763345c7d2df534ae1 Mon Sep 17 00:00:00 2001 From: Paul_GD Date: Sat, 15 Jun 2024 17:14:21 +0200 Subject: [PATCH 23/82] refactor(files): replace error dialog with ErrorDialog widget. --- lib/files/grid_files_screen.dart | 42 +++----------------------------- 1 file changed, 3 insertions(+), 39 deletions(-) diff --git a/lib/files/grid_files_screen.dart b/lib/files/grid_files_screen.dart index d39b213..5231908 100644 --- a/lib/files/grid_files_screen.dart +++ b/lib/files/grid_files_screen.dart @@ -23,11 +23,11 @@ import 'dart:async'; import 'dart:io'; import 'package:auto_size_text/auto_size_text.dart'; -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:logging/logging.dart'; import 'package:orion/api_services/api_services.dart'; import 'package:orion/files/details_screen.dart'; +import 'package:orion/util/error_handling/error_dialog.dart'; import 'package:orion/util/orion_api_filesystem/orion_api_directory.dart'; import 'package:orion/util/orion_api_filesystem/orion_api_file.dart'; import 'package:orion/util/orion_api_filesystem/orion_api_item.dart'; @@ -51,7 +51,6 @@ class GridFilesScreenState extends State { late String _directory = ''; late String _subdirectory = ''; late String _defaultDirectory = ''; - late String _parentPath = ''; late List _items = []; late Future> _itemsFuture = Future.value([]); @@ -126,9 +125,7 @@ class GridFilesScreenState extends State { .toList(); final List items = [...dirs, ...files]; - if (items.isNotEmpty) { - _parentPath = items.first.parentPath; - } + if (items.isNotEmpty) {} /*if (kDebugMode) { print('---------------------------------------'); @@ -148,42 +145,11 @@ class GridFilesScreenState extends State { _isLoading = false; }); _apiErrorState = true; - _showErrorDialog(); + showErrorDialog(context, 'PINK-CARROT'); return []; } } - void _showErrorDialog() { - if (_apiErrorState) { - WidgetsBinding.instance.addPostFrameCallback((_) { - showDialog( - context: context, - builder: (BuildContext context) { - return AlertDialog( - title: const Text('Odyssey API Error'), - content: const Text( - 'An Error has occurred while fetching files!\n' - 'Please ensure that Odyssey is running and accessible.\n\n' - 'If the issue persists, please contact support.\n' - 'Error Code: PINK-CARROT', - style: TextStyle(color: Colors.grey)), - actions: [ - TextButton( - onPressed: () { - Navigator.of(context).pop(); - }, - child: const Text( - 'Close', - style: TextStyle(fontSize: 20), - ), - ), - ], - ); - }); - }); - } - } - /*void _toggleSortOrder() { setState(() { _items.sort((a, b) { @@ -212,8 +178,6 @@ class GridFilesScreenState extends State { @override Widget build(BuildContext context) { - //_showErrorDialog(); - return Scaffold( appBar: AppBar( title: Text( From 93e57353fdb9c7499f0caacea69a988c27263bb0 Mon Sep 17 00:00:00 2001 From: Paul_GD Date: Sat, 15 Jun 2024 17:14:53 +0200 Subject: [PATCH 24/82] style(settings): change rand to public --- lib/settings/general_screen.dart | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/settings/general_screen.dart b/lib/settings/general_screen.dart index 6430c6c..78cc71e 100644 --- a/lib/settings/general_screen.dart +++ b/lib/settings/general_screen.dart @@ -64,8 +64,8 @@ class GeneralCfgScreenState extends State { } bool shouldDestruct() { - final _rand = Random(); - if (selfDestructMode && _rand.nextInt(100) < 2) { + final rand = Random(); + if (selfDestructMode && rand.nextInt(100) < 2) { setState(() { selfDestructMode = false; }); @@ -85,7 +85,7 @@ class GeneralCfgScreenState extends State { return Scaffold( body: Padding( - padding: const EdgeInsets.only(left: 5, right: 5, top: 5), + padding: const EdgeInsets.only(left: 16, right: 16, top: 5), child: ListView( controller: _scrollController, children: [ From f95eca229ec96085a0a1bf8dd24f7f83d32a7159 Mon Sep 17 00:00:00 2001 From: Paul_GD Date: Sat, 15 Jun 2024 17:18:20 +0200 Subject: [PATCH 25/82] feat(tools): implement MoveZScreen & ExposureScreen - ToolsScreen now uses bottomNavigationBar - MoveZScreen: using manual gcode commands for now, might change with API changes. - ExposureScreen: make sure to manually turn off Persistent mode after usage! --- lib/tools/exposure_screen.dart | 355 +++++++++++++++++++++++++++++++++ lib/tools/move_z_screen.dart | 248 +++++++++++++++++++++++ lib/tools/tools_screen.dart | 57 ++++-- pubspec.lock | 2 +- pubspec.yaml | 1 + 5 files changed, 641 insertions(+), 22 deletions(-) create mode 100644 lib/tools/exposure_screen.dart create mode 100644 lib/tools/move_z_screen.dart diff --git a/lib/tools/exposure_screen.dart b/lib/tools/exposure_screen.dart new file mode 100644 index 0000000..963f29e --- /dev/null +++ b/lib/tools/exposure_screen.dart @@ -0,0 +1,355 @@ +/* +* Orion - Exposure Screen +* Copyright (C) 2024 TheContrappostoShop +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see . +*/ + +import 'dart:async'; +import 'package:async/async.dart'; + +import 'package:flutter/material.dart'; +import 'package:logging/logging.dart'; +import 'package:orion/api_services/api_services.dart'; +import 'package:orion/util/error_handling/error_dialog.dart'; +import 'package:phosphor_flutter/phosphor_flutter.dart'; + +class ExposureScreen extends StatefulWidget { + const ExposureScreen({super.key}); + + @override + ExposureScreenState createState() => ExposureScreenState(); +} + +class ExposureScreenState extends State { + final _logger = Logger('Exposure'); + final ApiService _api = ApiService(); + CancelableOperation? _exposureOperation; + Completer? _exposureCompleter; + + int exposureTime = 3; + bool _apiErrorState = false; + + void exposeScreen(String type) { + try { + _logger.info('Testing exposure for $exposureTime seconds'); + _api.displayTest(type); + _api.manualCure(true); + showExposureDialog(context, exposureTime); + _exposureCompleter = Completer(); + _exposureOperation = CancelableOperation.fromFuture( + Future.any([ + Future.delayed(Duration(seconds: exposureTime)), + _exposureCompleter!.future, + ]).then((_) { + _api.manualCure(false); + }), + ); + } catch (e) { + setState(() { + _apiErrorState = true; + showErrorDialog(context, 'PINK-CARROT'); + }); + _logger.severe('Failed to test exposure: $e'); + } + } + + void showExposureDialog(BuildContext context, int countdownTime) { + _logger.info('Showing countdown dialog'); + + showDialog( + context: context, + barrierDismissible: false, + builder: (BuildContext context) { + return StreamBuilder( + stream: Stream.periodic(const Duration(milliseconds: 1), + (i) => countdownTime * 1000 - i).take((countdownTime * 1000) + 1), + initialData: + countdownTime * 1000, // Provide an initial countdown value + builder: (context, snapshot) { + if (snapshot.data == 0) { + Future.delayed(Duration.zero, () { + Navigator.of(context, rootNavigator: true).pop(true); + }); + return Container(); // Return an empty container when the countdown is over + } else { + return SafeArea( + child: Dialog( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + ), // Rounded corners for the dialog + insetPadding: + const EdgeInsets.all(20), // Padding around the dialog + child: Padding( + padding: + const EdgeInsets.all(20.0), // Padding inside the dialog + child: Column( + mainAxisSize: MainAxisSize + .min, // To make the dialog as big as its children + children: [ + const Text( + 'Exposing', + style: TextStyle( + fontSize: 24, + fontWeight: FontWeight + .bold), // Title with larger, bold text + ), + const SizedBox( + height: + 20), // Space between the title and the progress indicator + Padding( + padding: const EdgeInsets.only( + left: 20.0, right: 20.0, top: 15.0, bottom: 20.0), + child: Stack( + alignment: Alignment.center, + children: [ + SizedBox( + height: + 180, // Make the progress indicator larger + width: + 180, // Make the progress indicator larger + child: CircularProgressIndicator( + value: + snapshot.data! / (countdownTime * 1000), + strokeWidth: + 12, // Make the progress indicator thicker + ), + ), + Padding( + padding: const EdgeInsets.all(10.0), + child: (snapshot.data! / 1000) < 999 + ? Text( + '${(snapshot.data! / 1000).toStringAsFixed(0)} / $countdownTime', + style: const TextStyle(fontSize: 36), + ) + : const Text( + 'Testing', + style: TextStyle(fontSize: 30), + ), + ), + ], + ), + ), + const SizedBox(height: 20), + ElevatedButton( + style: ElevatedButton.styleFrom( + minimumSize: const Size(0, 70)), + onPressed: () { + _exposureOperation?.cancel(); + _exposureCompleter?.complete(); + Navigator.of(context, rootNavigator: true) + .pop(true); + }, + child: const Text( + 'Stop Exposure', + style: TextStyle(fontSize: 20), + ), + ), + ], + ), + ), + ), + ); + } + }, + ); + }, + ); + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context).copyWith( + elevatedButtonTheme: ElevatedButtonThemeData( + style: ButtonStyle( + shape: MaterialStateProperty.resolveWith( + (Set states) { + return RoundedRectangleBorder( + borderRadius: BorderRadius.circular(15), + ); + }, + ), + minimumSize: MaterialStateProperty.resolveWith( + (Set states) { + return const Size(double.infinity, double.infinity); + }, + ), + ), + ), + ); + + return Scaffold( + body: Padding( + padding: const EdgeInsets.all(20.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + // ... + Expanded( + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + Expanded( + child: Card.outlined( + elevation: 1, + child: Column( + children: [ + const Padding( + padding: EdgeInsetsDirectional.all(2), + child: Text( + 'Test Patterns', + style: TextStyle( + fontSize: 22, + fontWeight: FontWeight + .bold), // Adjust the style as needed + ), + ), + const Divider(height: 1), + Expanded( + child: Row( + children: [ + Expanded( + child: ElevatedButton( + onPressed: _apiErrorState + ? null + : () => exposeScreen('Grid'), + style: theme.elevatedButtonTheme.style, + child: const PhosphorIcon( + PhosphorIconsFill.checkerboard, + size: 40), + ), + ), + const VerticalDivider( + width: 1, + ), + Expanded( + child: ElevatedButton( + onPressed: _apiErrorState + ? null + : () => exposeScreen('Dimensions'), + style: theme.elevatedButtonTheme.style, + child: const PhosphorIcon( + PhosphorIconsFill.ruler, + size: 40), + ), + ), + const VerticalDivider( + width: 1, + ), + Expanded( + child: ElevatedButton( + onPressed: _apiErrorState + ? null + : () => exposeScreen('Blank'), + style: theme.elevatedButtonTheme.style, + child: PhosphorIcon(PhosphorIcons.square(), + size: 40), + ), + ), + ], + ), + ), + ], + ), + ), + ), + const SizedBox(height: 20), + Expanded( + child: Card.outlined( + child: ElevatedButton( + onPressed: + _apiErrorState ? null : () => exposeScreen('White'), + style: theme.elevatedButtonTheme.style, + child: const Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.cleaning_services), + SizedBox(width: 10), + Text( + 'Clean Vat', + style: TextStyle( + fontSize: 26, + ), + ), + ], + ), + ), + ), + ), + ], + ), + ), + // ... + const SizedBox(width: 30), + Expanded( + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + const Text( + 'Exposure Time', + style: TextStyle(fontSize: 22, fontWeight: FontWeight.bold), + textAlign: TextAlign.center, + ), + const SizedBox(height: 5), + ...[3, 10, 30, 'Persistent'].expand((value) { + return [ + Flexible( + child: ChoiceChip( + label: SizedBox( + width: double.infinity, + child: Text( + value is int ? '$value Seconds' : value as String, + textAlign: TextAlign.center, + style: const TextStyle( + fontSize: 22, // Adjust the font size here. + ), + ), + ), + selected: exposureTime == + (value is int + ? value + : (value == 'Persistent' + ? 999999 + : int.parse(value as String))), + onSelected: _apiErrorState + ? null + : (selected) { + if (selected) { + setState(() { + exposureTime = value is int + ? value + : (value == 'Persistent' + ? 999999 + : int.parse(value as String)); + }); + } + }, + ), + ), + const SizedBox(height: 10), + ]; + }).toList() + ..removeLast(), + ], + ), + ), + const SizedBox( + height: 10, + ), + ], + ), + ), + ); + } +} diff --git a/lib/tools/move_z_screen.dart b/lib/tools/move_z_screen.dart new file mode 100644 index 0000000..d83a458 --- /dev/null +++ b/lib/tools/move_z_screen.dart @@ -0,0 +1,248 @@ +/* +* Orion - Move Z Screen +* Copyright (C) 2024 TheContrappostoShop +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see . +*/ + +import 'package:flutter/material.dart'; +import 'package:logging/logging.dart'; +import 'package:orion/api_services/api_services.dart'; +import 'package:orion/util/error_handling/error_dialog.dart'; + +class MoveZScreen extends StatefulWidget { + const MoveZScreen({super.key}); + + @override + MoveZScreenState createState() => MoveZScreenState(); +} + +class MoveZScreenState extends State { + final _logger = Logger('MoveZScreen'); + final ApiService _api = ApiService(); + + double maxZ = 0.0; + double step = 0.1; + bool _apiErrorState = false; + + Future moveZ(double distance) async { + try { + _logger.info('Moving Z by $distance'); + _api.manualCommand('G91'); + await Future.delayed(const Duration(milliseconds: 200)); + _api.move(distance); + } catch (e) { + _logger.severe('Failed to move Z: $e'); + } + } + + void getMaxZ() async { + try { + Map config = await _api.getConfig(); + setState(() { + maxZ = config['printer']['max_z']; + }); + } catch (e) { + setState(() { + _apiErrorState = true; + }); + if (mounted) showErrorDialog(context, 'PINK-CARROT'); + _logger.severe('Failed to get max Z: $e'); + } + } + + @override + void initState() { + super.initState(); + getMaxZ(); + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context).copyWith( + elevatedButtonTheme: ElevatedButtonThemeData( + style: ButtonStyle( + shape: MaterialStateProperty.resolveWith( + (Set states) { + return RoundedRectangleBorder( + borderRadius: BorderRadius.circular(15), + ); + }, + ), + minimumSize: MaterialStateProperty.resolveWith( + (Set states) { + return const Size(double.infinity, double.infinity); + }, + ), + ), + ), + ); + + return Scaffold( + body: Padding( + padding: const EdgeInsets.all(20.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + Expanded( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Expanded( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + Expanded( + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + Expanded( + child: ElevatedButton( + onPressed: + _apiErrorState ? null : () => moveZ(step), + style: theme.elevatedButtonTheme.style, + child: + const Icon(Icons.arrow_upward, size: 50), + ), + ), + const SizedBox(height: 30), + Expanded( + child: ElevatedButton( + onPressed: _apiErrorState + ? null + : () => moveZ(-step), + style: theme.elevatedButtonTheme.style, + child: const Icon(Icons.arrow_downward, + size: 50), + ), + ), + ], + ), + ), + const SizedBox(width: 30), + Expanded( + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [0.1, 1.0, 10.0, 50.0].expand((value) { + return [ + Flexible( + child: ChoiceChip( + label: SizedBox( + width: double.infinity, + child: Text( + '$value mm', + textAlign: TextAlign.center, + style: const TextStyle( + fontSize: + 22, // Adjust the font size here. + ), + ), + ), + selected: step == value, + onSelected: _apiErrorState + ? null + : (selected) { + if (selected) { + setState(() { + step = value; + }); + } + }, + ), + ), + const SizedBox(height: 20), + ]; + }).toList() + ..removeLast(), + ), + ), + ], + ), + ), + ], + ), + ), + const SizedBox(width: 30), + Expanded( + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + Expanded( + child: ElevatedButton( + onPressed: _apiErrorState + ? null + : () async { + _logger.info('Moving to ZMAX'); + _api.manualCommand('G90'); + await Future.delayed( + const Duration(milliseconds: 200)); + _api.manualCommand('G1 Z$maxZ F3000'); + }, + style: theme.elevatedButtonTheme.style, + child: const Text( + 'Move to Top', + style: TextStyle( + fontSize: 24, + ), + ), + ), + ), + const SizedBox(height: 25), + Expanded( + child: ElevatedButton( + onPressed: _apiErrorState + ? null + : () { + _logger.info('Moving to ZMIN'); + _api.manualHome(); + }, + style: theme.elevatedButtonTheme.style, + child: const Text( + 'Move to Home', + style: TextStyle( + fontSize: 24, + ), + ), + ), + ), + const SizedBox(height: 25), + Expanded( + child: ElevatedButton( + onPressed: _apiErrorState + ? null + : () { + _logger.severe('EMERGENCY STOP'); + _api.manualCommand('M112'); + }, + style: theme.elevatedButtonTheme.style, + child: Text( + 'Emergency Stop', + style: TextStyle( + fontSize: 24, + color: _apiErrorState + ? null + : Theme.of(context).colorScheme.onErrorContainer, + ), + ), + ), + ), + ], + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/tools/tools_screen.dart b/lib/tools/tools_screen.dart index 806cd24..eb998d8 100644 --- a/lib/tools/tools_screen.dart +++ b/lib/tools/tools_screen.dart @@ -16,30 +16,29 @@ * along with this program. If not, see . */ -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:logging/logging.dart'; -import 'package:orion/api_services/api_services.dart'; +import 'package:orion/tools/exposure_screen.dart'; +import 'package:orion/tools/move_z_screen.dart'; class ToolsScreen extends StatefulWidget { const ToolsScreen({super.key}); @override - _ToolsScreenState createState() => _ToolsScreenState(); + ToolsScreenState createState() => ToolsScreenState(); } -class _ToolsScreenState extends State { - final Logger _logger = Logger('ToolsScreen'); - final ApiService _api = ApiService(); +class ToolsScreenState extends State { + int _selectedIndex = 0; - @override - void initState() { - super.initState(); + void _onItemTapped(int index) { + setState(() { + _selectedIndex = index; + }); } @override - void didChangeDependencies() { - super.didChangeDependencies(); + void initState() { + super.initState(); } @override @@ -48,15 +47,31 @@ class _ToolsScreenState extends State { appBar: AppBar( title: const Text('Tools'), ), - body: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: const [ - Text( - 'Tools Screen', - ), - ], - ), + body: _selectedIndex == 0 + ? const MoveZScreen() + : _selectedIndex == 1 + ? const ExposureScreen() + : const MoveZScreen(), + bottomNavigationBar: BottomNavigationBar( + type: BottomNavigationBarType.fixed, + items: const [ + BottomNavigationBarItem( + icon: Icon(Icons.height), + label: 'Move Z', + ), + BottomNavigationBarItem( + icon: Icon(Icons.lightbulb), + label: 'Exposure', + ), + /*BottomNavigationBarItem( + icon: Icon(Icons.check), + label: 'Self Test', + ),*/ + ], + currentIndex: _selectedIndex, + selectedItemColor: Theme.of(context).colorScheme.primary, + onTap: _onItemTapped, + unselectedItemColor: Theme.of(context).colorScheme.secondary, ), ); } diff --git a/pubspec.lock b/pubspec.lock index ac63daa..4a592fd 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -42,7 +42,7 @@ packages: source: hosted version: "2.5.0" async: - dependency: transitive + dependency: "direct main" description: name: async sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" diff --git a/pubspec.yaml b/pubspec.yaml index 8f7e295..771119b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -64,6 +64,7 @@ dependencies: flutter_markdown: ^0.6.23 logging: ^1.2.0 toastification: ^2.0.0 + async: ^2.11.0 dev_dependencies: flutter_test: From fed2980d97f2c4205b55d7ba58e4ba2f75694b27 Mon Sep 17 00:00:00 2001 From: Paul_GD Date: Mon, 24 Jun 2024 21:28:19 +0200 Subject: [PATCH 26/82] feat(tools): add API check for ExposureScreen - improve looks - added exposure detail to dialog --- lib/tools/exposure_screen.dart | 63 +++++++++++++++++++++++++--------- 1 file changed, 47 insertions(+), 16 deletions(-) diff --git a/lib/tools/exposure_screen.dart b/lib/tools/exposure_screen.dart index 963f29e..fef64d7 100644 --- a/lib/tools/exposure_screen.dart +++ b/lib/tools/exposure_screen.dart @@ -46,7 +46,7 @@ class ExposureScreenState extends State { _logger.info('Testing exposure for $exposureTime seconds'); _api.displayTest(type); _api.manualCure(true); - showExposureDialog(context, exposureTime); + showExposureDialog(context, exposureTime, type: type); _exposureCompleter = Completer(); _exposureOperation = CancelableOperation.fromFuture( Future.any([ @@ -65,7 +65,8 @@ class ExposureScreenState extends State { } } - void showExposureDialog(BuildContext context, int countdownTime) { + void showExposureDialog(BuildContext context, int countdownTime, + {String? type}) { _logger.info('Showing countdown dialog'); showDialog( @@ -98,9 +99,13 @@ class ExposureScreenState extends State { mainAxisSize: MainAxisSize .min, // To make the dialog as big as its children children: [ - const Text( - 'Exposing', - style: TextStyle( + Text( + type == 'White' + ? 'Cleaning' + : type != null + ? 'Testing $type' + : 'Exposing', + style: const TextStyle( fontSize: 24, fontWeight: FontWeight .bold), // Title with larger, bold text @@ -130,8 +135,9 @@ class ExposureScreenState extends State { padding: const EdgeInsets.all(10.0), child: (snapshot.data! / 1000) < 999 ? Text( - '${(snapshot.data! / 1000).toStringAsFixed(0)} / $countdownTime', - style: const TextStyle(fontSize: 36), + (snapshot.data! / 1000) + .toStringAsFixed(0), + style: const TextStyle(fontSize: 50), ) : const Text( 'Testing', @@ -143,17 +149,22 @@ class ExposureScreenState extends State { ), const SizedBox(height: 20), ElevatedButton( - style: ElevatedButton.styleFrom( - minimumSize: const Size(0, 70)), onPressed: () { - _exposureOperation?.cancel(); - _exposureCompleter?.complete(); + try { + _exposureOperation?.cancel(); + _exposureCompleter?.complete(); + } catch (e) { + _logger.severe('Failed to stop exposure: $e'); + } Navigator.of(context, rootNavigator: true) .pop(true); }, - child: const Text( - 'Stop Exposure', - style: TextStyle(fontSize: 20), + child: const Padding( + padding: EdgeInsets.all(15.0), + child: Text( + 'Stop Exposure', + style: TextStyle(fontSize: 24), + ), ), ), ], @@ -168,16 +179,35 @@ class ExposureScreenState extends State { ); } + @override + void initState() { + super.initState(); + getApiStatus(); + } + + Future getApiStatus() async { + try { + Map config = await _api.getConfig(); + } catch (e) { + setState(() { + _apiErrorState = true; + showErrorDialog(context, 'PINK-CARROT'); + }); + _logger.severe('Failed to get config: $e'); + } + } + @override Widget build(BuildContext context) { final theme = Theme.of(context).copyWith( elevatedButtonTheme: ElevatedButtonThemeData( style: ButtonStyle( + backgroundColor: MaterialStateProperty.all(Colors.transparent), shape: MaterialStateProperty.resolveWith( (Set states) { return RoundedRectangleBorder( - borderRadius: BorderRadius.circular(15), - ); + borderRadius: BorderRadius.circular(15), + side: const BorderSide(color: Colors.transparent)); }, ), minimumSize: MaterialStateProperty.resolveWith( @@ -267,6 +297,7 @@ class ExposureScreenState extends State { const SizedBox(height: 20), Expanded( child: Card.outlined( + elevation: 1, child: ElevatedButton( onPressed: _apiErrorState ? null : () => exposeScreen('White'), From 45c67972feb4279742fcd7ae360e119ebab14585 Mon Sep 17 00:00:00 2001 From: Paul_GD Date: Mon, 24 Jun 2024 21:29:13 +0200 Subject: [PATCH 27/82] chore(apple): update iOS and macOS dependencies - removed WiFi methods for macOS, they did not work. --- ios/Flutter/AppFrameworkInfo.plist | 2 +- ios/Podfile.lock | 19 +++++----- ios/Runner.xcodeproj/project.pbxproj | 26 +++++++++++--- .../xcshareddata/xcschemes/Runner.xcscheme | 2 +- macos/Runner/AppDelegate.swift | 35 ------------------- 5 files changed, 33 insertions(+), 51 deletions(-) diff --git a/ios/Flutter/AppFrameworkInfo.plist b/ios/Flutter/AppFrameworkInfo.plist index 9625e10..7c56964 100644 --- a/ios/Flutter/AppFrameworkInfo.plist +++ b/ios/Flutter/AppFrameworkInfo.plist @@ -21,6 +21,6 @@ CFBundleVersion 1.0 MinimumOSVersion - 11.0 + 12.0 diff --git a/ios/Podfile.lock b/ios/Podfile.lock index f032ba3..152374e 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -5,11 +5,10 @@ PODS: - path_provider_foundation (0.0.1): - Flutter - FlutterMacOS - - permission_handler_apple (9.1.1): + - permission_handler_apple (9.3.0): - Flutter - - shared_preferences_foundation (0.0.1): + - url_launcher_ios (0.0.1): - Flutter - - FlutterMacOS - wifi_info_flutter (0.0.1): - Flutter @@ -18,7 +17,7 @@ DEPENDENCIES: - package_info (from `.symlinks/plugins/package_info/ios`) - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) - permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`) - - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`) + - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) - wifi_info_flutter (from `.symlinks/plugins/wifi_info_flutter/ios`) EXTERNAL SOURCES: @@ -30,17 +29,17 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/path_provider_foundation/darwin" permission_handler_apple: :path: ".symlinks/plugins/permission_handler_apple/ios" - shared_preferences_foundation: - :path: ".symlinks/plugins/shared_preferences_foundation/darwin" + url_launcher_ios: + :path: ".symlinks/plugins/url_launcher_ios/ios" wifi_info_flutter: :path: ".symlinks/plugins/wifi_info_flutter/ios" SPEC CHECKSUMS: - Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854 + Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 package_info: 873975fc26034f0b863a300ad47e7f1ac6c7ec62 - path_provider_foundation: 29f094ae23ebbca9d3d0cec13889cd9060c0e943 - permission_handler_apple: e76247795d700c14ea09e3a2d8855d41ee80a2e6 - shared_preferences_foundation: 5b919d13b803cadd15ed2dc053125c68730e5126 + path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 + permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2 + url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe wifi_info_flutter: 55d4c2469034f5b5c38063ccefa62f708a503a2b PODFILE CHECKSUM: 5a367937f10bf0c459576e5e472a1159ee029c13 diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 8b036a4..1ebea9e 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -140,6 +140,7 @@ 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, DA330F61AACB41AB17D3BC23 /* [CP] Embed Pods Frameworks */, + 44806EE1381CFE2E6DA9D56F /* [CP] Copy Pods Resources */, ); buildRules = ( ); @@ -156,7 +157,7 @@ 97C146E61CF9000F007C117D /* Project object */ = { isa = PBXProject; attributes = { - LastUpgradeCheck = 1430; + LastUpgradeCheck = 1510; ORGANIZATIONNAME = ""; TargetAttributes = { 97C146ED1CF9000F007C117D = { @@ -236,6 +237,23 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; }; + 44806EE1381CFE2E6DA9D56F /* [CP] Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Copy Pods Resources"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; 9740EEB61CF901F6004384FC /* Run Script */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; @@ -343,7 +361,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; @@ -424,7 +442,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -473,7 +491,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; diff --git a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index a6b826d..5e31d3d 100644 --- a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ Void in - // Note: this method is invoked on the UI thread. - // Handle the method call - print("Received method call: \(call.method)") - if call.method == "networks" { - print("Calling getWifiNetworks()") - result(self.getWifiNetworks()) - } else { - print("Method not recognized, calling getWifiNetworks() by default") - result(self.getWifiNetworks()) - } - }) - } - - func getWifiNetworks() -> [String] { - print("In getWifiNetworks()") - let wifiClient = CWWiFiClient.shared() - let interfaces = wifiClient.interfaces() - var networks: [String] = [] - - interfaces?.forEach { interface in - do { - let networkList = try interface.scanForNetworks(withName: nil) - networkList.forEach { network in - print("Found network: \(network.ssid ?? "")") - networks.append(network.ssid ?? "") - } - } catch { - print("Error: \(error)") - } - } - return networks } } \ No newline at end of file From 8dfb1b09765e22dff0f7a708bfcd4fd855dc155f Mon Sep 17 00:00:00 2001 From: Paul_GD Date: Mon, 24 Jun 2024 21:30:35 +0200 Subject: [PATCH 28/82] feat(settings): implement initial proper WiFi screen - macOS uses fake network for UI testing - no password checking yet. --- lib/settings/wifi_screen.dart | 444 +++++++++++++++++++++++++++------- 1 file changed, 351 insertions(+), 93 deletions(-) diff --git a/lib/settings/wifi_screen.dart b/lib/settings/wifi_screen.dart index 40f1ab8..7eabe2e 100644 --- a/lib/settings/wifi_screen.dart +++ b/lib/settings/wifi_screen.dart @@ -19,10 +19,12 @@ // ignore_for_file: avoid_print, use_build_context_synchronously, library_private_types_in_public_api, unused_field import 'dart:io'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; - -const MethodChannel macplatform = MethodChannel('orion.macplatform.channel'); +import 'package:logging/logging.dart'; +import 'package:orion/util/orion_kb/orion_keyboard_expander.dart'; +import 'package:orion/util/orion_kb/orion_textfield_spawn.dart'; class WifiScreen extends StatefulWidget { const WifiScreen({super.key}); @@ -36,6 +38,10 @@ class _WifiScreenState extends State { String? currentWifiSSID; Future>>? _networksFuture; Color? _standardColor = Colors.white.withOpacity(0.0); + final Logger _logger = Logger('Wifi'); + + final GlobalKey wifiPasswordKey = + GlobalKey(); @override void initState() { @@ -48,8 +54,22 @@ class _WifiScreenState extends State { _networksFuture = _getWifiNetworks(); } + Future _getIPAddress() async { + String ipAddress; + try { + final List networkInterfaces = + await NetworkInterface.list(type: InternetAddressType.IPv4); + ipAddress = networkInterfaces.first.addresses.first.address; + } on PlatformException catch (e) { + print('Failed to get IP Address: $e'); + ipAddress = 'Failed to get IP Address'; + } + return ipAddress; + } + Icon getSignalStrengthIcon(dynamic signalStrengthReceived, String platform) { int signalStrength = 0; + try { signalStrength = int.parse(signalStrengthReceived); } catch (e) { @@ -57,58 +77,92 @@ class _WifiScreenState extends State { return const Icon(Icons.warning_rounded); } - var icons = { - 80: Icon(Icons.network_wifi_rounded, color: Colors.green[300]), - 60: Icon(Icons.network_wifi_3_bar_rounded, color: Colors.green[300]), - 40: Icon(Icons.network_wifi_2_bar_rounded, color: Colors.orange[300]), - 20: Icon(Icons.network_wifi_1_bar_rounded, color: Colors.orange[300]), - 0: Icon(Icons.warning_rounded, color: Colors.red[300]), + // Define the icons for each platform + final Map linuxIcons = { + 100: Icon(Icons.network_wifi_rounded, color: Colors.green[300], size: 30), + 80: Icon(Icons.network_wifi_rounded, color: Colors.green[300], size: 30), + 60: Icon(Icons.network_wifi_3_bar_rounded, + color: Colors.green[300], size: 30), + 40: Icon(Icons.network_wifi_2_bar_rounded, + color: Colors.orange[300], size: 30), + 20: Icon(Icons.network_wifi_1_bar_rounded, + color: Colors.orange[300], size: 30), + 0: Icon(Icons.warning_rounded, color: Colors.red[300], size: 30), }; - if (platform == 'linux') { - for (var threshold in icons.keys.toList().reversed) { - if (signalStrength >= threshold) { - return icons[threshold]!; - } - } - } else { - icons = { - -0: Icon(Icons.network_wifi_rounded, color: Colors.green[300]), - -50: Icon(Icons.network_wifi_3_bar_rounded, color: Colors.green[300]), - -70: Icon(Icons.network_wifi_2_bar_rounded, color: Colors.orange[300]), - -90: Icon(Icons.warning_rounded, color: Colors.red[300]), - }; + final Map otherIcons = { + 10: Icon(Icons.network_wifi_rounded, color: Colors.green[300], size: 30), + 0: Icon(Icons.network_wifi_rounded, color: Colors.green[300], size: 30), + -50: Icon(Icons.network_wifi_3_bar_rounded, + color: Colors.green[300], size: 30), + -70: Icon(Icons.network_wifi_2_bar_rounded, + color: Colors.orange[300], size: 30), + -90: Icon(Icons.warning_rounded, color: Colors.red[300], size: 30), + }; - signalStrength = signalStrength.abs(); - for (var threshold in icons.keys.toList().reversed) { - if (signalStrength >= threshold.abs()) { - return icons[threshold]!; - } + // Choose the correct map of icons based on the platform + final Map icons = platform == 'linux' ? linuxIcons : otherIcons; + + // Find the first icon where the signal strength is less than or equal to the threshold + for (var threshold in icons.keys.toList().reversed) { + if (signalStrength <= threshold) { + return icons[threshold]!; } } + + // If no icon was found, return a warning icon return const Icon(Icons.warning_rounded); } late String platform; - Future>> _getWifiNetworks() async { + Future>> _getWifiNetworks( + {bool alreadyConnected = false}) async { wifiNetworks.clear(); try { + if (Theme.of(context).platform == TargetPlatform.macOS && + !alreadyConnected) { + currentWifiSSID = 'test'; + } ProcessResult? result; - ProcessResult? currentSSID; switch (Theme.of(context).platform) { case TargetPlatform.macOS: platform = 'macos'; - final List networks = - await macplatform.invokeMethod('networks'); - currentSSID = - await Process.run('networksetup', ['-getairportnetwork', 'en0']); - final match = RegExp(r'Current Wi-Fi Network: (.*)') - .firstMatch(currentSSID.stdout.toString()); - currentWifiSSID = match?.group(1)?.trim() ?? ''; - return networks.map((ssid) => {'SSID': ssid, 'SIGNAL': '0'}).toList(); + final List> networks = []; + // Generate fake output + for (int i = 0; i < 10; i++) { + networks.add({ + 'SSID': 'Network $i', + 'SIGNAL': + '${-30 - i * 5}', // Signal strength decreases with each network + 'BSSID': '00:0a:95:9d:68:1$i', + 'RSSI': '${-30 - i * 5}', + 'CHANNEL': '${1 + i}', + 'HT': 'Y', + 'CC': 'US', + 'SECURITY': '(WPA2)' + }); + } + //if (!alreadyConnected) currentWifiSSID = networks.first['SSID']; + return networks; case TargetPlatform.linux: platform = 'linux'; + // Get the current Wi-Fi network + await Process.run('sudo', ['nmcli', 'device', 'wifi', 'rescan']); + _logger.info('Rescanning Wi-Fi networks'); result = await Process.run('nmcli', ['device', 'wifi', 'list']); + try { + var result = await Process.run( + 'nmcli', ['-t', '-f', 'active,ssid', 'dev', 'wifi']); + var lines = result.stdout.toString().split('\n'); + var activeNetworkLine = + lines.firstWhere((line) => line.startsWith('yes:')); + var activeNetworkSSID = activeNetworkLine.split(':')[1]; + if (!alreadyConnected) currentWifiSSID = activeNetworkSSID; + _logger.info(activeNetworkSSID); + } catch (e) { + _logger.severe('Failed to get current Wi-Fi network: $e'); + } + _logger.info('Getting Wi-Fi networks'); break; default: } @@ -124,7 +178,10 @@ class _WifiScreenState extends State { r'^\s*(.+?)\s{2,}(.+?)\s{2,}([^]+?)\s{2,}([^]+?)\s{2,}([^]+)$'); break; case TargetPlatform.linux: - pattern = RegExp(''); + pattern = RegExp( + r"(?:(\*)\s+)?([0-9A-Fa-f:]{17})\s+(.*?)\s+(Infra)\s+(\d+)\s+([\d\sMbit/s]+)\s+(\d+)\s+([\w▂▄▆█_]+)\s+(.*)", + multiLine: true, + ); break; default: } @@ -134,20 +191,27 @@ class _WifiScreenState extends State { final RegExpMatch? match = pattern.firstMatch(lines[i]); if (match != null) { - /*if (kDebugMode) { - print('---------------------------'); - for (int i = 1; i < match.groupCount; i++) { - print('Group $i: ${match.group(i)}'); - } - }*/ + print('---------------------------'); + for (int i = 1; i < match.groupCount; i++) { + print('Group $i: ${match.group(i)}'); + } - networks.add({ - 'SSID': match.group(1) ?? '', - 'SIGNAL': match.group(2) ?? '', - }); + if (platform == 'macos') { + networks.add({ + 'SSID': match.group(1) ?? '', + 'SIGNAL': match.group(2) ?? '', + 'SECURITY': match.group(8) ?? '', + }); + } else if (platform == 'linux') { + networks.add({ + 'SSID': match.group(3) ?? '', + 'SIGNAL': match.group(7) ?? '', + 'SECURITY': match.group(9) ?? '', + }); + } } } - print('Current Network: $currentWifiSSID'); + // Sort by strongest signal networks.sort((a, b) { if (a['SSID'] == currentWifiSSID) { @@ -171,6 +235,24 @@ class _WifiScreenState extends State { } } + void connectToNetwork(String ssid, String password) async { + try { + final result = await Process.run('sudo', + ['nmcli', 'dev', 'wifi', 'connect', ssid, 'password', password]); + if (result.exitCode == 0) { + setState(() { + currentWifiSSID = ssid; + _networksFuture = _getWifiNetworks(alreadyConnected: true); + }); + _logger.info('Connected to $ssid'); + } else { + _logger.warning('Failed to connect to $ssid'); + } + } catch (e) { + _logger.warning('Failed to connect to Wi-Fi network: $e'); + } + } + List> mergeNetworks(List> networks) { var mergedNetworks = >{}; @@ -191,6 +273,18 @@ class _WifiScreenState extends State { return mergedNetworks.values.toList(); } + String signalStrengthToQuality(int signalStrength) { + if (signalStrength >= -50) { + return 'Perfect'; + } else if (signalStrength >= -60) { + return 'Good'; + } else if (signalStrength >= -70) { + return 'Fair'; + } else { + return 'Weak'; + } + } + @override Widget build(BuildContext context) { _standardColor = Theme.of(context).textTheme.bodyLarge!.color; @@ -206,56 +300,220 @@ class _WifiScreenState extends State { } else { final List> networks = snapshot.data ?? []; final String currentSSID = currentWifiSSID ?? ''; - return ListView.builder( - itemCount: networks.length, - itemBuilder: (context, index) { - final network = networks[index]; - return ListTile( - key: ValueKey(network['SSID']), - title: Text( - '${network['SSID']}', - style: TextStyle( - color: network['SSID'] == currentSSID - ? Theme.of(context) - .colorScheme - .primary - .withAlpha(130) - : null, - ), - ), - subtitle: Text( - 'Signal Strength: ${network['SIGNAL']} dBm', - style: TextStyle( - color: network['SSID'] == currentSSID - ? Theme.of(context) - .colorScheme - .primary - .withAlpha(130) - : null, + if (currentSSID.isNotEmpty) { + return FutureBuilder( + future: _getIPAddress(), + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return const Center(child: CircularProgressIndicator()); + } else { + if (snapshot.hasError) { + return Center(child: Text('Error: ${snapshot.error}')); + } else { + final String ipAddress = snapshot.data ?? ''; + return Center( + child: Padding( + padding: const EdgeInsets.only(left: 16, right: 16), + child: Column( + children: [ + const Spacer(), + Card.outlined( + elevation: 5, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const SizedBox(height: 10), + RichText( + text: TextSpan( + style: DefaultTextStyle.of(context) + .style, + children: [ + TextSpan( + text: 'Connected: ', + style: TextStyle( + fontSize: 26, + fontWeight: FontWeight.bold, + color: Theme.of(context) + .colorScheme + .primary, + ), + ), + TextSpan( + text: currentSSID, + style: TextStyle( + fontSize: 26, + fontWeight: FontWeight.normal, + color: Theme.of(context) + .colorScheme + .primary, + ), + ), + ], + ), + ), + const SizedBox(height: 10), + const Divider(height: 1), + const SizedBox(height: 25), + Text( + 'IP Address: $ipAddress', + style: const TextStyle(fontSize: 24), + ), + const SizedBox(height: 25), + Text( + 'Signal Strength: ${signalStrengthToQuality(int.parse(networks.first['SIGNAL']!))} [${networks.first['SIGNAL']}]', + style: const TextStyle(fontSize: 24), + ), + const SizedBox(height: 25), + ElevatedButton( + onPressed: () async { + try { + //await Process.run('nmcli', ['dev', 'disconnect', 'iface', 'wlan0']); + setState(() { + currentWifiSSID = null; + _networksFuture = + _getWifiNetworks( + alreadyConnected: true); + }); + } catch (e) { + print( + 'Failed to disconnect Wi-Fi: $e'); + } + }, + child: const Padding( + padding: EdgeInsets.all(15), + child: Text( + 'Disconnect', + style: TextStyle(fontSize: 24), + ), + ), + ), + const SizedBox(height: 25), + ], + ), + ), + const Spacer(), + ], + ), + ), + ); + } + } + }, + ); + } else { + return ListView.builder( + itemCount: networks.length, + itemBuilder: (context, index) { + final network = networks[index]; + return Padding( + padding: + const EdgeInsets.only(left: 16, right: 16, top: 5), + child: Card.outlined( + elevation: 1, + child: ListTile( + key: ValueKey(network['SSID']), + title: Text( + '${network['SSID']}', + style: TextStyle( + fontSize: 22, + color: network['SSID'] == currentSSID + ? Theme.of(context) + .colorScheme + .primary + .withAlpha(130) + : null, + ), + ), + subtitle: Text( + 'Signal Strength: ${network['SIGNAL']} dBm', + style: TextStyle( + fontSize: 18, + color: network['SSID'] == currentSSID + ? Theme.of(context) + .colorScheme + .primary + .withAlpha(130) + : null, + ), + ), + trailing: getSignalStrengthIcon( + network['SIGNAL']!, platform), + onTap: () { + showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + title: Center( + child: Text( + 'Connect to ${network['SSID']}')), + content: SizedBox( + width: + MediaQuery.of(context).size.width * 0.5, + child: SingleChildScrollView( + child: Column( + children: [ + SpawnOrionTextField( + key: wifiPasswordKey, + keyboardHint: 'Enter Password', + locale: + Localizations.localeOf(context) + .toString(), + ), + OrionKbExpander( + textFieldKey: wifiPasswordKey), + ], + ), + ), + ), + actions: [ + TextButton( + onPressed: () { + Navigator.of(context).pop(); + }, + child: const Text('Close', + style: TextStyle(fontSize: 20)), + ), + TextButton( + onPressed: () { + connectToNetwork( + network['SSID']!, + wifiPasswordKey.currentState! + .getCurrentText()); + Navigator.of(context).pop(); + }, + child: const Text('Confirm', + style: TextStyle(fontSize: 20)), + ), + ], + ); + }, + ); + }, + ), ), - ), - trailing: - getSignalStrengthIcon(network['SIGNAL']!, platform), - // Add onTap logic to connect to the selected network if needed - onTap: () { - // Your logic to connect to this network - }, - ); - }, - ); + ); + }, + ); + } } } }, ), - floatingActionButton: FloatingActionButton( - onPressed: () async { - await Future.delayed(const Duration(milliseconds: 100)); - setState(() { - _networksFuture = _getWifiNetworks(); - }); - }, - child: const Icon(Icons.refresh_rounded), - ), + floatingActionButton: currentWifiSSID != null + ? null + : SizedBox( + height: 70, + width: 70, + child: FloatingActionButton( + onPressed: () async { + await Future.delayed(const Duration(milliseconds: 100)); + setState(() { + _networksFuture = _getWifiNetworks(); + }); + }, + child: const Icon(Icons.refresh_rounded, size: 40), + ), + ), ); } } From 8ec4cd55dfd47617bfce2f3b1e31b792c7b5844c Mon Sep 17 00:00:00 2001 From: Paul_GD Date: Wed, 26 Jun 2024 01:15:21 +0200 Subject: [PATCH 29/82] fix(files): refresh now properly fetches new item list --- lib/files/grid_files_screen.dart | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/lib/files/grid_files_screen.dart b/lib/files/grid_files_screen.dart index 5231908..baf011f 100644 --- a/lib/files/grid_files_screen.dart +++ b/lib/files/grid_files_screen.dart @@ -86,10 +86,28 @@ class GridFilesScreenState extends State { }); } - void refresh() async { - await _getItems(_directory); + Future refresh() async { //_sortAscending = !_sortAscending; //_toggleSortOrder(); + setState(() { + _isLoading = true; // Indicate loading state + }); + try { + final items = + await _getItems(_directory, false); // Fetch latest items from API + _itemsCompleter = Completer>(); // Reset the completer + _itemsCompleter.complete(items); // Complete with new items + setState(() { + _items = items; // Update items + _isLoading = false; // Reset loading state + }); + } catch (e) { + setState(() { + _apiErrorState = true; + showErrorDialog(context, 'PINK-CARROT'); + _isLoading = false; + }); + } } Future> _getItems(String directory, From 724bb0114c918d20330e033ceffb72a45a790dd3 Mon Sep 17 00:00:00 2001 From: Paul_GD Date: Mon, 1 Jul 2024 15:29:05 +0200 Subject: [PATCH 30/82] bump(version): update to v0.3.0 --- lib/pubspec.dart | 21 ++++++++++++--------- pubspec.yaml | 2 +- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/lib/pubspec.dart b/lib/pubspec.dart index 61f1755..311ab05 100644 --- a/lib/pubspec.dart +++ b/lib/pubspec.dart @@ -3,7 +3,7 @@ // ignore_for_file: public_member_api_docs, constant_identifier_names, avoid_classes_with_only_static_members mixin Pubspec { - static final buildDate = DateTime.utc(2024, 5, 7, 18, 3, 6); + static final buildDate = DateTime.utc(2024, 6, 25, 23, 41, 21); static const name = 'orion'; @@ -12,19 +12,19 @@ mixin Pubspec { static const publish_to = 'none'; - static const versionFull = '0.2.0+1'; + static const versionFull = '0.3.0+SELFCOMPILED'; - static const version = '0.2.0'; + static const version = '0.3.0'; - static const versionSmall = '0.2'; + static const versionSmall = '0.3'; static const versionMajor = 0; - static const versionMinor = 2; + static const versionMinor = 3; static const versionPatch = 0; - static const versionBuild = 1; + static const versionBuild = 0; static const versionPreRelease = ''; @@ -39,11 +39,10 @@ mixin Pubspec { 'sdk': 'flutter', }, 'cupertino_icons': '^1.0.2', - 'device_preview': '^1.1.0', 'english_words': '^4.0.0', 'flutter_svg': '^2.0.9', 'glob': '^2.1.2', - 'go_router': '^12.1.3', + 'go_router': '^14.1.2', 'http': '^1.2.1', 'ini': '^2.1.0', 'intl': '^0.18.1', @@ -69,13 +68,16 @@ mixin Pubspec { 'about': '^2.1.3', 'pubspec_extract': '^2.0.5', 'flutter_markdown': '^0.6.23', + 'logging': '^1.2.0', + 'toastification': '^2.0.0', + 'async': '^2.11.0', }; static const dev_dependencies = { 'flutter_test': { 'sdk': 'flutter', }, - 'flutter_lints': '^3.0.1', + 'flutter_lints': '^4.0.0', }; static const flutter = { @@ -83,6 +85,7 @@ mixin Pubspec { 'assets': [ 'assets/images/opensource.svg', 'assets/images/placeholder.png', + 'assets/images/bsod.png', 'README.md', 'CHANGELOG.md', ], diff --git a/pubspec.yaml b/pubspec.yaml index 771119b..44aba1f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -17,7 +17,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # In Windows, build-name is used as the major, minor, and patch parts # of the product and file versions while build-number is used as the build suffix. -version: 0.2.0+1 +version: 0.3.0+SELFCOMPILED environment: sdk: ">=2.18.5 <3.0.0" From 2fb3d31b20b2ac9fd043c3ced365a2cdd7162ef1 Mon Sep 17 00:00:00 2001 From: Paul_GD Date: Mon, 1 Jul 2024 15:29:42 +0200 Subject: [PATCH 31/82] feat(settings): implement proper WiFi connect and disconnect logic --- lib/settings/wifi_screen.dart | 314 ++++++++++++++++++++++------------ 1 file changed, 202 insertions(+), 112 deletions(-) diff --git a/lib/settings/wifi_screen.dart b/lib/settings/wifi_screen.dart index 7eabe2e..d73f461 100644 --- a/lib/settings/wifi_screen.dart +++ b/lib/settings/wifi_screen.dart @@ -19,6 +19,7 @@ // ignore_for_file: avoid_print, use_build_context_synchronously, library_private_types_in_public_api, unused_field import 'dart:io'; +import 'dart:math'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -40,6 +41,9 @@ class _WifiScreenState extends State { Color? _standardColor = Colors.white.withOpacity(0.0); final Logger _logger = Logger('Wifi'); + final ValueNotifier _isConnecting = ValueNotifier(false); + bool _connectionFailed = false; + final GlobalKey wifiPasswordKey = GlobalKey(); @@ -59,9 +63,13 @@ class _WifiScreenState extends State { try { final List networkInterfaces = await NetworkInterface.list(type: InternetAddressType.IPv4); - ipAddress = networkInterfaces.first.addresses.first.address; + if (networkInterfaces.isNotEmpty) { + ipAddress = networkInterfaces.first.addresses.first.address; + } else { + throw Exception('No network interfaces found.'); + } } on PlatformException catch (e) { - print('Failed to get IP Address: $e'); + _logger.warning('Failed to get IP Address: $e'); ipAddress = 'Failed to get IP Address'; } return ipAddress; @@ -73,7 +81,7 @@ class _WifiScreenState extends State { try { signalStrength = int.parse(signalStrengthReceived); } catch (e) { - print(e); + _logger.warning(e); return const Icon(Icons.warning_rounded); } @@ -115,14 +123,15 @@ class _WifiScreenState extends State { } late String platform; + Future>> _getWifiNetworks( {bool alreadyConnected = false}) async { wifiNetworks.clear(); try { - if (Theme.of(context).platform == TargetPlatform.macOS && + /*if (Theme.of(context).platform == TargetPlatform.macOS && !alreadyConnected) { currentWifiSSID = 'test'; - } + }*/ ProcessResult? result; switch (Theme.of(context).platform) { case TargetPlatform.macOS: @@ -130,8 +139,9 @@ class _WifiScreenState extends State { final List> networks = []; // Generate fake output for (int i = 0; i < 10; i++) { + int rand = Random().nextInt(100); networks.add({ - 'SSID': 'Network $i', + 'SSID': 'Network $rand', 'SIGNAL': '${-30 - i * 5}', // Signal strength decreases with each network 'BSSID': '00:0a:95:9d:68:1$i', @@ -161,6 +171,7 @@ class _WifiScreenState extends State { _logger.info(activeNetworkSSID); } catch (e) { _logger.severe('Failed to get current Wi-Fi network: $e'); + setState(() {}); } _logger.info('Getting Wi-Fi networks'); break; @@ -191,10 +202,10 @@ class _WifiScreenState extends State { final RegExpMatch? match = pattern.firstMatch(lines[i]); if (match != null) { - print('---------------------------'); + /*print('---------------------------'); for (int i = 1; i < match.groupCount; i++) { print('Group $i: ${match.group(i)}'); - } + }*/ if (platform == 'macos') { networks.add({ @@ -236,6 +247,9 @@ class _WifiScreenState extends State { } void connectToNetwork(String ssid, String password) async { + setState(() { + _isConnecting.value = true; // Start of connection attempt + }); try { final result = await Process.run('sudo', ['nmcli', 'dev', 'wifi', 'connect', ssid, 'password', password]); @@ -245,11 +259,25 @@ class _WifiScreenState extends State { _networksFuture = _getWifiNetworks(alreadyConnected: true); }); _logger.info('Connected to $ssid'); + Navigator.of(context).pop(); + _isConnecting.value = false; } else { _logger.warning('Failed to connect to $ssid'); + setState(() { + _isConnecting.value = false; + _connectionFailed = true; + }); } } catch (e) { _logger.warning('Failed to connect to Wi-Fi network: $e'); + setState(() { + _isConnecting.value = false; + _connectionFailed = true; + }); + } finally { + setState(() { + _isConnecting.value = false; // End of connection attempt + }); } } @@ -367,7 +395,14 @@ class _WifiScreenState extends State { ElevatedButton( onPressed: () async { try { - //await Process.run('nmcli', ['dev', 'disconnect', 'iface', 'wlan0']); + _logger.info('Disconnecting Wi-Fi'); + await Process.run('sudo', [ + 'nmcli', + 'dev', + 'disconnect', + 'iface', + 'wlan0' + ]); setState(() { currentWifiSSID = null; _networksFuture = @@ -375,7 +410,7 @@ class _WifiScreenState extends State { alreadyConnected: true); }); } catch (e) { - print( + _logger.warning( 'Failed to disconnect Wi-Fi: $e'); } }, @@ -401,119 +436,174 @@ class _WifiScreenState extends State { }, ); } else { - return ListView.builder( - itemCount: networks.length, - itemBuilder: (context, index) { - final network = networks[index]; - return Padding( - padding: - const EdgeInsets.only(left: 16, right: 16, top: 5), - child: Card.outlined( - elevation: 1, - child: ListTile( - key: ValueKey(network['SSID']), - title: Text( - '${network['SSID']}', - style: TextStyle( - fontSize: 22, - color: network['SSID'] == currentSSID - ? Theme.of(context) - .colorScheme - .primary - .withAlpha(130) - : null, + return RefreshIndicator( + onRefresh: () async { + setState(() { + _networksFuture = _getWifiNetworks(); + }); + }, + child: ListView.builder( + itemCount: networks.length, + itemBuilder: (context, index) { + final network = networks[index]; + return Padding( + padding: + const EdgeInsets.only(left: 16, right: 16, top: 5), + child: Card.outlined( + elevation: 1, + child: ListTile( + key: ValueKey(network['SSID']), + title: Text( + '${network['SSID']}', + style: TextStyle( + fontSize: 22, + color: network['SSID'] == currentSSID + ? Theme.of(context) + .colorScheme + .primary + .withAlpha(130) + : null, + ), ), - ), - subtitle: Text( - 'Signal Strength: ${network['SIGNAL']} dBm', - style: TextStyle( - fontSize: 18, - color: network['SSID'] == currentSSID - ? Theme.of(context) - .colorScheme - .primary - .withAlpha(130) - : null, + subtitle: Text( + 'Signal Strength: ${network['SIGNAL']} dBm', + style: TextStyle( + fontSize: 18, + color: network['SSID'] == currentSSID + ? Theme.of(context) + .colorScheme + .primary + .withAlpha(130) + : null, + ), ), - ), - trailing: getSignalStrengthIcon( - network['SIGNAL']!, platform), - onTap: () { - showDialog( - context: context, - builder: (BuildContext context) { - return AlertDialog( - title: Center( - child: Text( - 'Connect to ${network['SSID']}')), - content: SizedBox( - width: - MediaQuery.of(context).size.width * 0.5, - child: SingleChildScrollView( - child: Column( - children: [ - SpawnOrionTextField( - key: wifiPasswordKey, - keyboardHint: 'Enter Password', - locale: - Localizations.localeOf(context) - .toString(), - ), - OrionKbExpander( - textFieldKey: wifiPasswordKey), - ], + trailing: getSignalStrengthIcon( + network['SIGNAL']!, platform), + onTap: () { + showDialog( + barrierDismissible: false, + context: context, + builder: (BuildContext context) { + return AlertDialog( + title: Center( + child: Text( + 'Connect to ${network['SSID']}')), + content: SizedBox( + width: MediaQuery.of(context).size.width * + 0.5, + child: ValueListenableBuilder( + valueListenable: + _isConnecting, // Listen to _isConnecting ValueNotifier + builder: + (context, isConnecting, child) { + // Based on isConnecting value, show CircularProgressIndicator or form + return Stack( + alignment: Alignment.center, + children: [ + Opacity( + opacity: + isConnecting ? 0.0 : 1.0, + child: SingleChildScrollView( + child: Column( + children: [ + SpawnOrionTextField( + key: wifiPasswordKey, + keyboardHint: + 'Enter Password', + locale: Localizations + .localeOf( + context) + .toString(), + ), + if (_connectionFailed) + const SizedBox( + height: 20, + ), + if (_connectionFailed) + const Text( + 'Connection failed. Please try again.', + style: TextStyle( + color: Colors.red, + fontSize: 20, + ), + ), + OrionKbExpander( + textFieldKey: + wifiPasswordKey), + ], + ), + ), + ), + IgnorePointer( + child: Opacity( + opacity: + isConnecting ? 1.0 : 0.0, + child: const SizedBox( + height: 60, + width: 60, + child: + CircularProgressIndicator(), + ), + ), + ), + ], + ); + }, ), ), - ), - actions: [ - TextButton( - onPressed: () { - Navigator.of(context).pop(); - }, - child: const Text('Close', - style: TextStyle(fontSize: 20)), - ), - TextButton( - onPressed: () { - connectToNetwork( - network['SSID']!, - wifiPasswordKey.currentState! - .getCurrentText()); - Navigator.of(context).pop(); - }, - child: const Text('Confirm', - style: TextStyle(fontSize: 20)), - ), - ], - ); - }, - ); - }, + actions: [ + TextButton( + onPressed: () { + Navigator.of(context).pop(); + setState(() { + _isConnecting.value = false; + _connectionFailed = false; + }); + }, + child: const Text('Close', + style: TextStyle(fontSize: 20)), + ), + TextButton( + onPressed: () { + if (!_isConnecting.value) { + _isConnecting.value = + true; // Indicate that a connection attempt is starting + if (Theme.of(context).platform == + TargetPlatform.linux) { + connectToNetwork( + network['SSID']!, + wifiPasswordKey.currentState! + .getCurrentText(), + ); + } else { + Future.delayed( + const Duration(seconds: 3), + () { + Navigator.of(context).pop(); + _isConnecting.value = false; + }); + } + } + }, + child: const Text('Confirm', + style: TextStyle(fontSize: 20)), + ), + ], + ); + }, + ); + }, + ), ), - ), - ); - }, + ); + }, + ), ); } } } }, ), - floatingActionButton: currentWifiSSID != null - ? null - : SizedBox( - height: 70, - width: 70, - child: FloatingActionButton( - onPressed: () async { - await Future.delayed(const Duration(milliseconds: 100)); - setState(() { - _networksFuture = _getWifiNetworks(); - }); - }, - child: const Icon(Icons.refresh_rounded, size: 40), - ), - ), ); } } From d6a82a481a338c9a3f98f1464037d947e573356c Mon Sep 17 00:00:00 2001 From: Paul_GD Date: Mon, 1 Jul 2024 15:30:26 +0200 Subject: [PATCH 32/82] feat(settings): change About page to show commit when compiled by Actions --- lib/settings/settings_screen.dart | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/lib/settings/settings_screen.dart b/lib/settings/settings_screen.dart index ff29a94..bd72cba 100644 --- a/lib/settings/settings_screen.dart +++ b/lib/settings/settings_screen.dart @@ -70,10 +70,13 @@ class SettingsScreenState extends State { values: { 'version': Pubspec.version, 'buildNumber': Pubspec.versionBuild.toString(), + 'commit': Pubspec.versionFull.toString().split('+')[1] == + 'SELFCOMPILED' + ? 'Local Build' + : 'Commit ${Pubspec.versionFull.toString().split('+')[1]}', 'year': DateTime.now().year.toString(), }, - applicationVersion: - 'Version {{ version }}, Build {{ buildNumber }}', + applicationVersion: 'Version {{ version }} - {{ commit }}', applicationName: 'Orion', applicationLegalese: 'GPLv3 - Copyright © TheContrappostoShop {{ year }}', From 0b1c0907137872aa46714763b2a1315eaa64953b Mon Sep 17 00:00:00 2001 From: Paul_GD Date: Mon, 1 Jul 2024 15:31:04 +0200 Subject: [PATCH 33/82] feat(files): implement delete logic for SL1 files - not working yet, pending API changes. --- lib/files/details_screen.dart | 45 ++++++++++++++++++++++------------- 1 file changed, 29 insertions(+), 16 deletions(-) diff --git a/lib/files/details_screen.dart b/lib/files/details_screen.dart index 54f8dc1..b4f7650 100644 --- a/lib/files/details_screen.dart +++ b/lib/files/details_screen.dart @@ -363,15 +363,24 @@ class DetailScreenState extends State { opacity: opacity, child: Padding( padding: EdgeInsets.only( - left: (leftPadding - 10) < 0 ? 0 : leftPadding - 10, - right: (rightPadding - 10) < 0 ? 0 : rightPadding - 10, - bottom: 40, - top: 20), + left: (leftPadding - 10) < 0 ? 0 : leftPadding - 10, + right: (rightPadding - 10) < 0 ? 0 : rightPadding - 10, + bottom: 40, + top: 20, + ), child: Row( children: [ ElevatedButton( - onPressed: null, - // TODO: Add delete logic here + onPressed: () { + String subdirectory = widget.fileSubdirectory; + try { + _api.deleteFile(widget.fileLocation, + path.join(subdirectory, widget.fileName)); + } catch (e) { + _logger.severe('Failed to delete file', e); + } + Navigator.pop(context); + }, style: ElevatedButton.styleFrom( minimumSize: Size( 120, // Subtract the padding on both sides @@ -387,16 +396,20 @@ class DetailScreenState extends State { Expanded( child: ElevatedButton( onPressed: () { - String subdirectory = widget.fileSubdirectory; - _api.startPrint(widget.fileLocation, - path.join(subdirectory, widget.fileName)); - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => const StatusScreen( - newPrint: true, - ), - )); + try { + String subdirectory = widget.fileSubdirectory; + _api.startPrint(widget.fileLocation, + path.join(subdirectory, widget.fileName)); + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const StatusScreen( + newPrint: true, + ), + )); + } catch (e) { + _logger.severe('Failed to start print', e); + } }, style: ElevatedButton.styleFrom( minimumSize: Size( From 9638a4db4575610a1534b9d62ca2d7ba0756bbaf Mon Sep 17 00:00:00 2001 From: Paul_GD Date: Wed, 17 Jul 2024 23:38:40 +0200 Subject: [PATCH 34/82] feat(core): Add AtkinsonHyperlegible font family and styles - Add AtkinsonHyperlegible font family to pubspec.yaml - Include regular, bold, italic, and bold italic variants - Update themeLight and themeDark to use AtkinsonHyperlegible font - Update text styles to use AtkinsonHyperlegible font --- assets/fonts/AtkinsonHyperlegible-Bold.ttf | Bin 0 -> 55256 bytes .../fonts/AtkinsonHyperlegible-BoldItalic.ttf | Bin 0 -> 55288 bytes assets/fonts/AtkinsonHyperlegible-Italic.ttf | Bin 0 -> 54892 bytes assets/fonts/AtkinsonHyperlegible-Regular.ttf | Bin 0 -> 54348 bytes assets/fonts/OFL.txt | 92 ++++++++++++++++++ lib/themes/themes.dart | 34 +++++-- pubspec.yaml | 13 +++ 7 files changed, 131 insertions(+), 8 deletions(-) create mode 100644 assets/fonts/AtkinsonHyperlegible-Bold.ttf create mode 100644 assets/fonts/AtkinsonHyperlegible-BoldItalic.ttf create mode 100644 assets/fonts/AtkinsonHyperlegible-Italic.ttf create mode 100644 assets/fonts/AtkinsonHyperlegible-Regular.ttf create mode 100644 assets/fonts/OFL.txt diff --git a/assets/fonts/AtkinsonHyperlegible-Bold.ttf b/assets/fonts/AtkinsonHyperlegible-Bold.ttf new file mode 100644 index 0000000000000000000000000000000000000000..ca3f613847de62b8da83d2dab500ec391c8fe477 GIT binary patch literal 55256 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 Ca=@Jc literal 0 HcmV?d00001 diff --git a/assets/fonts/AtkinsonHyperlegible-BoldItalic.ttf b/assets/fonts/AtkinsonHyperlegible-BoldItalic.ttf new file mode 100644 index 0000000000000000000000000000000000000000..2804fdb8f06c328afb96409d7498c6f5f287b79c GIT binary patch literal 55288 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 z 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 N)RR(%LbP=Ae*iZmk*xp# literal 0 HcmV?d00001 diff --git a/assets/fonts/OFL.txt b/assets/fonts/OFL.txt new file mode 100644 index 0000000..2befc89 --- /dev/null +++ b/assets/fonts/OFL.txt @@ -0,0 +1,92 @@ +Copyright 2020 Braille Institute of America, Inc. + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/lib/themes/themes.dart b/lib/themes/themes.dart index 5abed25..820bb79 100644 --- a/lib/themes/themes.dart +++ b/lib/themes/themes.dart @@ -19,22 +19,31 @@ import 'package:flutter/material.dart'; final ThemeData themeLight = ThemeData( + fontFamily: 'AtkinsonHyperlegible', colorScheme: ColorScheme.fromSeed( seedColor: const Color(0xff6750a4), brightness: Brightness.light, ), appBarTheme: const AppBarTheme( - titleTextStyle: TextStyle(fontSize: 30, color: Colors.black), + titleTextStyle: TextStyle( + fontFamily: 'AtkinsonHyperlegible', + fontSize: 30, + color: Colors.black, + ), centerTitle: true, toolbarHeight: 65, iconTheme: IconThemeData(size: 30), ), textTheme: const TextTheme( - bodyMedium: TextStyle(fontSize: 20), + bodyMedium: TextStyle(fontFamily: 'AtkinsonHyperlegible', fontSize: 20), + titleLarge: TextStyle( + fontFamily: 'AtkinsonHyperlegible', fontSize: 20), // For AppBar title ), bottomNavigationBarTheme: const BottomNavigationBarThemeData( - selectedLabelStyle: TextStyle(fontSize: 18), - unselectedLabelStyle: TextStyle(fontSize: 18), + selectedLabelStyle: + TextStyle(fontFamily: 'AtkinsonHyperlegible', fontSize: 18), + unselectedLabelStyle: + TextStyle(fontFamily: 'AtkinsonHyperlegible', fontSize: 18), selectedIconTheme: IconThemeData(size: 30), unselectedIconTheme: IconThemeData(size: 30), ), @@ -48,22 +57,31 @@ final ThemeData themeLight = ThemeData( ); final ThemeData themeDark = ThemeData( + fontFamily: 'AtkinsonHyperlegible', colorScheme: ColorScheme.fromSeed( seedColor: const Color(0xff6750a4), brightness: Brightness.dark, ), appBarTheme: const AppBarTheme( - titleTextStyle: TextStyle(fontSize: 30, color: Colors.white), + titleTextStyle: TextStyle( + fontFamily: 'AtkinsonHyperlegible', + fontSize: 30, + color: Colors.white, + ), centerTitle: true, toolbarHeight: 65, iconTheme: IconThemeData(size: 30), ), textTheme: const TextTheme( - bodyMedium: TextStyle(fontSize: 20), + bodyMedium: TextStyle(fontFamily: 'AtkinsonHyperlegible', fontSize: 20), + titleLarge: TextStyle( + fontFamily: 'AtkinsonHyperlegible', fontSize: 20), // For AppBar title ), bottomNavigationBarTheme: const BottomNavigationBarThemeData( - selectedLabelStyle: TextStyle(fontSize: 18), - unselectedLabelStyle: TextStyle(fontSize: 18), + selectedLabelStyle: + TextStyle(fontFamily: 'AtkinsonHyperlegible', fontSize: 18), + unselectedLabelStyle: + TextStyle(fontFamily: 'AtkinsonHyperlegible', fontSize: 18), selectedIconTheme: IconThemeData(size: 30), unselectedIconTheme: IconThemeData(size: 30), ), diff --git a/pubspec.yaml b/pubspec.yaml index 44aba1f..15f77e0 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -95,9 +95,22 @@ flutter: - assets/images/opensource.svg - assets/images/placeholder.png - assets/images/bsod.png + - assets/images/thumbnail800x480.png - README.md - CHANGELOG.md + fonts: + - family: AtkinsonHyperlegible + fonts: + - asset: assets/fonts/AtkinsonHyperlegible-Regular.ttf + - asset: assets/fonts/AtkinsonHyperlegible-Bold.ttf + weight: 700 + - asset: assets/fonts/AtkinsonHyperlegible-Italic.ttf + style: italic + - asset: assets/fonts/AtkinsonHyperlegible-BoldItalic.ttf + weight: 700 + style: italic + # An image asset can refer to one or more resolution-specific "variants", see # https://flutter.dev/assets-and-images/#resolution-aware From 69cc64a128bba7136d1f4ac67928e318690ee686 Mon Sep 17 00:00:00 2001 From: Paul_GD Date: Wed, 17 Jul 2024 23:41:53 +0200 Subject: [PATCH 35/82] refactor(util): Improve StatusCard UI and performance - Increase font size and icon size for better visibility - Adjust padding and layout for improved aesthetics - Use a circular card shape with rounded corners - Optimize code for better performance and readability --- lib/util/status_card.dart | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/lib/util/status_card.dart b/lib/util/status_card.dart index d7089ee..89c1746 100644 --- a/lib/util/status_card.dart +++ b/lib/util/status_card.dart @@ -59,7 +59,7 @@ class StatusCardState extends State { ? null : widget.progress == 0.0 ? 1.0 - : widget.progress; + : 1.0; // If the print is active, not paused, canceled or finished, it is active. final isActive = (widget.isPausing == false && @@ -73,9 +73,9 @@ class StatusCardState extends State { ? Stack( children: [ Text( - '${(widget.progress * 100).toStringAsFixed(0)}%', + (widget.progress * 100).toStringAsFixed(0), style: TextStyle( - fontSize: 38, + fontSize: 75, foreground: Paint() ..style = PaintingStyle.stroke ..strokeWidth = 5 @@ -83,17 +83,20 @@ class StatusCardState extends State { ), ), Text( - '${(widget.progress * 100).toStringAsFixed(0)}%', + (widget.progress * 100).toStringAsFixed(0), style: TextStyle( - fontSize: 38, + fontSize: 75, color: Theme.of(context).colorScheme.primary, ), ), ], ) - : Card.outlined( + : Card( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(999), + ), child: Padding( - padding: const EdgeInsets.all(5.0), + padding: const EdgeInsets.all(2.0), child: Stack( children: [ Positioned( @@ -102,7 +105,7 @@ class StatusCardState extends State { left: 0, right: 0, child: Padding( - padding: const EdgeInsets.all(7), + padding: const EdgeInsets.all(10), child: CircularProgressIndicator( value: circleProgress, strokeWidth: 6, @@ -113,11 +116,11 @@ class StatusCardState extends State { ), ), Padding( - padding: const EdgeInsets.all(12), + padding: const EdgeInsets.all(25), child: Icon( cardIcon.icon, color: widget.statusColor, - size: 42, + size: 70, ), ) ], From b1b49150544a0fda19d0473b96f450d8066eeb92 Mon Sep 17 00:00:00 2001 From: Paul_GD Date: Wed, 17 Jul 2024 23:42:32 +0200 Subject: [PATCH 36/82] chore(settings): Update About page to display commit information when compiled by Actions --- lib/settings/settings_screen.dart | 129 ++++++++++++++++-------------- 1 file changed, 68 insertions(+), 61 deletions(-) diff --git a/lib/settings/settings_screen.dart b/lib/settings/settings_screen.dart index bd72cba..8d41ae7 100644 --- a/lib/settings/settings_screen.dart +++ b/lib/settings/settings_screen.dart @@ -58,70 +58,77 @@ class SettingsScreenState extends State { title: const Text('Settings'), actions: [ Padding( - padding: const EdgeInsets.only(right: 15.0), - child: IconButton( - icon: const Icon( - Icons.info, - ), - iconSize: 35, - onPressed: () { - showAboutPage( - context: context, - values: { - 'version': Pubspec.version, - 'buildNumber': Pubspec.versionBuild.toString(), - 'commit': Pubspec.versionFull.toString().split('+')[1] == - 'SELFCOMPILED' - ? 'Local Build' - : 'Commit ${Pubspec.versionFull.toString().split('+')[1]}', - 'year': DateTime.now().year.toString(), - }, - applicationVersion: 'Version {{ version }} - {{ commit }}', - applicationName: 'Orion', - applicationLegalese: - 'GPLv3 - Copyright © TheContrappostoShop {{ year }}', - children: [ - Padding( - padding: const EdgeInsets.only(left: 10, right: 10), - child: Card( - child: ListTile( - leading: const Icon(Icons.list, size: 30), - title: const Text( - 'Changelog', - style: TextStyle(fontSize: 24), - ), - onTap: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => const MarkdownScreen( - filename: 'CHANGELOG.md'), - ), - ); + padding: const EdgeInsets.only(right: 16.0), + child: _selectedIndex == 2 + ? IconButton( + icon: const Icon( + Icons.info, + ), + iconSize: 35, + onPressed: () { + showAboutPage( + context: context, + values: { + 'version': Pubspec.version, + 'buildNumber': Pubspec.versionBuild.toString(), + 'commit': Pubspec.versionFull + .toString() + .split('+')[1] == + 'SELFCOMPILED' + ? 'Local Build' + : 'Commit ${Pubspec.versionFull.toString().split('+')[1]}', + 'year': DateTime.now().year.toString(), }, - )), - ), - const Padding( - padding: EdgeInsets.all(10), - child: Card( - child: LicensesPageListTile( - title: Text( - 'Open-Source Licenses', - style: TextStyle(fontSize: 24), + applicationVersion: + 'Version {{ version }} - {{ commit }}', + applicationName: 'Orion', + applicationLegalese: + 'GPLv3 - Copyright © TheContrappostoShop {{ year }}', + children: [ + Padding( + padding: + const EdgeInsets.only(left: 10, right: 10), + child: Card( + child: ListTile( + leading: const Icon(Icons.list, size: 30), + title: const Text( + 'Changelog', + style: TextStyle(fontSize: 24), + ), + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => + const MarkdownScreen( + filename: 'CHANGELOG.md'), + ), + ); + }, + )), ), - icon: Icon( - Icons.favorite, - size: 30, + const Padding( + padding: EdgeInsets.all(10), + child: Card( + child: LicensesPageListTile( + title: Text( + 'Open-Source Licenses', + style: TextStyle(fontSize: 24), + ), + icon: Icon( + Icons.favorite, + size: 30, + ), + ), + ), ), - ), - ), - ), - ], - applicationIcon: const FlutterLogo( - size: 100, - )); - }, - ), + ], + applicationIcon: const FlutterLogo( + size: 100, + )); + }, + ) + : null, ), ], ), From 637498c094feb87d52fe1ecf18d1424c82433964 Mon Sep 17 00:00:00 2001 From: Paul_GD Date: Wed, 17 Jul 2024 23:43:14 +0200 Subject: [PATCH 37/82] feat(api_services): implement error handling for file deletion The code changes in `api_services.dart` implement error handling for file deletion. Previously, the code would make a delete request to `/files`, but now it makes the request to `/file`. If an error occurs during the deletion process, the code logs the error and throws an exception. This commit message follows the established convention of starting with a type prefix (`feat`) followed by the affected module (`api_services`). It provides a concise summary of the changes made and their purpose. --- lib/api_services/api_services.dart | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/lib/api_services/api_services.dart b/lib/api_services/api_services.dart index ffd05a1..b8ff37c 100644 --- a/lib/api_services/api_services.dart +++ b/lib/api_services/api_services.dart @@ -274,7 +274,12 @@ class ApiService { 'file_path': filePath, }; - final response = await odysseyDelete('/files', queryParams); - return json.decode(response.body); + try { + final response = await odysseyDelete('/file', queryParams); + return json.decode(response.body); + } catch (e) { + _logger.severe('Failed to delete file: $e'); + throw Exception('Failed to delete file: $e'); + } } } From b1c21fae05427d0095148892adb17c55e150b440 Mon Sep 17 00:00:00 2001 From: Paul_GD Date: Wed, 17 Jul 2024 23:44:02 +0200 Subject: [PATCH 38/82] feat(files): update font family and styles for file names --- lib/files/grid_files_screen.dart | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/lib/files/grid_files_screen.dart b/lib/files/grid_files_screen.dart index baf011f..359bb5c 100644 --- a/lib/files/grid_files_screen.dart +++ b/lib/files/grid_files_screen.dart @@ -367,7 +367,9 @@ class GridFilesScreenState extends State { maxLines: 2, minFontSize: 18, style: const TextStyle( - fontSize: 24, color: Colors.grey), + fontSize: 24, + color: Colors.grey, + fontFamily: 'AtkinsonHyperlegible'), ), ), ), @@ -451,12 +453,13 @@ class GridFilesScreenState extends State { maxLines: 2, minFontSize: 20, style: TextStyle( - fontSize: 24, - color: Theme.of(context) - .textTheme - .bodyLarge! - .color, - ), + fontSize: 24, + color: Theme.of(context) + .textTheme + .bodyLarge! + .color, + fontFamily: + 'AtkinsonHyperlegible'), ), ), ), From a3ef8b6f8ecfe8880ca13b006a23fa465b3ed34b Mon Sep 17 00:00:00 2001 From: Paul_GD Date: Wed, 17 Jul 2024 23:45:43 +0200 Subject: [PATCH 39/82] dev(assets): add placeholder 800x480 thumbnail --- assets/images/thumbnail800x480.png | Bin 0 -> 52565 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 assets/images/thumbnail800x480.png diff --git a/assets/images/thumbnail800x480.png b/assets/images/thumbnail800x480.png new file mode 100644 index 0000000000000000000000000000000000000000..ef95b5ac43f2c2a16df3766a54224f9f54168bbc GIT binary patch literal 52565 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 f z<&_m@LBiYwEKAatxAVz^I=eBZpLM7r$GAABj%a(4OOiL`i`igw0S5Pm=|D6aWDv~? z%_SW&kC5=Ol%fL0jWv+K-dJF;ad#OP2@iS{nffoNv8dXmxdqyrK3F;y7~Gg!ZUObF z4c5caOA2t%Ik|=)GIZTX8vE)6J>u0BLPiXDk-fc^*!EtnXOGG=jSV1=cp*-`_a3aB z`mPepzchNU_J%~U?VlPLp1ts;+T6w7jyR*Wt3bmIHH2o*zM?=KU}nhG`pKyhqFd2i*!M|^Gq7$1f7F{iDx^MP7PH)xgjbpn#%ttRZz ziV&LBN-InDEiw)R8PH(N;%W+T2u+&`P_xuK*)?nW-Ae|4y$YVya!epS1x7YD)Dt;Z zVLE{7;xJ9=e3r{J#7$(l@T~uNE>{Ve?a(#0d>VdAYHdT@IpB-DSraU8+ zU}NYHKO?C_+h_#+Jcb=ALm%gYy+|3l^K9?u@M}M6$MIprbU2{;SRu)JZxb#)n1Q)l z?DKit1HSxnZyGxun`)}baY63Jak-+n?=@*jRM6m7*(qArWve8O87W6rNL*i&@RM7|<3! zX3Iy-f3bLD@7q$SKlX2fdZ%)x%7Y2flzyg#CJqtWJnP_>sSFVU_B7;7^!kZ{^;u3h ztL0r``ek+~+YtR~PYyy_+7r3UFKa)SJcPtC_8}FY4c+zX!@lhAgk6r1z{AM`5sOn@ zO@~(+lFtUOX2yY+vma|!JP*;V8(pRQ!8yW-l$C-Qz`N}go9}*EhzZZ5snt|6?P1b@ zwZ!H>lZIw~Nzb<|yTswnDt$@5&i&arx}trb?kvw zqvy)6kFw?B<(K-7-SV=eURbdn^B>mtwQ59@1{f!pv8^Um%(!|7A7D;Q4ZUP)56X+3 zn8mlp_*C-_gR6}Tei^g+(b{1u8f43LP!n5ob_m?&{hV{^`F0QUwNEhjk`2Pgjp@4B z0p-35T$NzSfUO5FE!`Id>xEN^V(9|0k5pHOAzW3DvVMv2hvPa2tgdk1k+r{|F>%_w z-Na8rGdO;MiW%W|{B5xOntW6&dQ3e4qPl+I8_p0RHvViAJ&ZuFSaYZIf%^C$RZ4G1 zdBb#((6M%$i&)A4Ns-a_zh$IwL2*^G<`)N@+hi_C1BO%g7V0T~(c!qF*17pl2bsj3!>^khU2UYoklS~Xu83$i za_^ODctQCogTWAs~FWs(~jU#^}X`x?Rh^=%a^PT><+I&&Bu(53R{d+*zN z{bqVIeJSrwHJ7!bs<4E3ks)2(2uKku zQ|hCRX|k%<-LtFd?X?cJ3$Y%D!JDQv<$B$wNImD6l8P}`Bj1vCH7+|SKBN*k-Bn-G zmAGJi2fIg{(V32;(PK@Ykv8L21bFt^rwt%<^_efwtgn~(**(R?3j5UWoK|#Esa)JwdruCeZNIdf-L>V!8SSB1I1ZY#xtM#K6z0SG ziuZ3rd1BCw7dFcQs=|$>DetdqZ(}D$o|7gd)Q;SJqC8SAmh=dQEMwdqK`u-~=0|MNIZH{BXmX{D(Txg5H-aUc$0M{Bjb`*(rh z0Yqwt;%HqJ@63WzTOUMPY7`UZ+1bGvP*~1d{5`Vr#MBEbJB$$tH0pPxxgnV|<8 zt+V3NvcyV3d^K*_&SMo)k@=i5a&{jXa23$RfbF}E4L4iMA`#zIDGQnkrK>zP7lhcb zF~jekpFa+EEH*O}ihYDrK3%nO(pHY&3C-r6RqLQaqBz<@v4hADcir_q=F>>uG>Tu_ z*>Q@?-q%!GB7jo+q=)LNfv@PWnVvF4FB+3~QVXuNzqy!r@~zw?(?T3~;2AhE;8{Z^ z=-%WQ{=LY=@YzfbkpA0M`%H&nDfPPjJ@9}58A-az1*~NDR3Zo3zun#)s%*D(iF5PpD)ZB-)Kaxk z%Zbxkry{#Jf+2XvpQ251$G-1E#5r@UB-vo-ngG*R2|l(*^CC>|`fpzrB6;dlq`qHX zM&5hDC5ym4{Pb~WTWijXcYf?;Y?^&&(~QC*vM z)N7V)qV*EroLZ2k)*bTLRfsX4ehlWxSX#mWo|~;G?)f)xPT6gvg1Dv5IGmmWA^)`w zl=z=hr>E8^a0xJ(jBVDXjP`K*@k7bRktEYNYGQW~0lgxWm<9N^N zQwXmkue z#_!JL{?(XPOvp_{)X*d0op#(&Cmt#$WLvimzr|`XyZm;6SvqK!&*-FOmu-Rs6JsVz z4Dzx+!gyW*AUttyx4UZXAAECvy8SWo8A|e|tbHI0{%DluBoe1IL-_UeO^{kFN(LbITovYP>)&!Xfd|w=c+;PZS2hZo`a&{kkUJ}s-nkRZlga{Ze z$$%m}!?K;}Nf07dGWwaiI4N-j$G^7Ws1weWhOs6HD(?(C>TJofu`;PX{vP8RS zL`mg3r;lx|{nN)$e2_%znVGl8kEg@wY8{BS-z$%QfNgvX7>C2&dYuiuihC@xlfNIC zjvAh3OYFKGt(#vvg$C&)p-S0)w0mk%7n~ZvL17`{@{@)vu~Y_m_=TFAxGbr4S`AtZwMg?|%wLUNZHL5+v!*EhObs6rg0K>N1 z1x>>=I#320;~EWYt&x1I2HSGepKFo}#Gh7(dnpEOBBlpF+(8%fPaDFzlN)ci{nq<>^fVU6&%Lovv!Y@1ZJpUX7MfrguY5bo>q_Zy&D+7&+cC1U+yt{)`!b)S?9_lwgksfs ze<1%=_sdd4uBo@IFMNZke>hZZwE+&Y>}R#7rqZ=#K1wceZG3!lv42=k|Qj zDMVkfOjakHUU;cDdSRa*v&9D$(i7o}(a~63p zG$D-FqxX-FqQ**o0e&W~{8IC3;HT)GVljj`VTJr#4Kw+qj_JvlLeFkKioaVmrTl;F?yt~UMVd;M-)Q|f1ARJ!s^8swM5vL|l3GT!i zhxPq6tVbTnJYgoS9-v~b(HL)~tL#~1ZJxI*jJCbBZgBHl+Z0wF5g^Woywb>3jUTin zX;sPPwpU=NP(#3951EELGz|GD#o=BqIGv}Q|0T3yyd6j3w-zk9_THK;z$yN_QGCSK z+6>e*e9K|*yaH%HV7V+mwUyVuoV~X6pBeh}e`e@YTk%PnPo|=41*k|R<3#Ki83g*u z>BGq*%Q)IAUwL#=vmt*8q8z6uU%B!FU((->7P7BAbrX%N*)YII+?BPny>WwvY53I_ zUlvV~RG3D5za&Ih)tQ;pYEK-jU6^+kBv?ef{%~igIYr_ucNQOU*O!iJ7rVKIx}eUy ze`hZ{`SP(jXg6f1Reu2(!tPf>3H958P?J2hy-(nt2He=d+(5VBt`3xmC7pb63rAZP zW<~#+*Al>aF8JPgbxArW>y73C>a)b^5jxfuZYl-A5otMxD|326N^VEHP(j)K;Mxp= zX}>B@ICR4~J|l8PmQbOgVYJ{JzM`h9F0e;j(}8HjVRFFYM_)KYl9za`{~Bs~KM4KP z^Ce<>gUR)T5LQk2P)1LhQA50D*IAE!=k8>te<>RWy#z)}ZG)wu%W=x1Y*5z8iza0& zBD6U*l6j_vewiXr*h*=P(^O0uuhrdzzRKT>uV-aC&RuMkzYN+barMuZm8?fST7xc| z3$scHh9+P};88;da#!8FqQ^Q+j6R^|B1e9MqV|44xj2MX`)3HNeCQVm`CECIu!npB zNaQmY`>FZwBJdoF9M7)!QTz>yA1n!9*>mQIWM$f4+zm4mEK^LfP5nrqodLaoE0%Pg zh}OTUXxOKe9Pv$h(VNDgEyUl~%)NX_+WD|rSZp~LmUYDPTZa2O$dSJzTgb(0ke5-J zGCXPJ)jL&!d($1cGC4K&1<4O`XW~+It>9_luTfp7s$!lT1EBw!moi1s%$~?GW9An; zS5#u7qvlz0AI`p?HankP=tk;>zauH>wdcT8?(M}oT^%~D#G!Ny)^ww#MtphlO(YHK zCdfJ{TRf+chq!K0dg`>t;UuvLZzfX95xbAKYuWLS(Ig0`z=D^(=~)X;k)-L=n60)) ze26{3l7Jx-q@qCnRxS#=$~RmiLKq)wPyA6JKHs)fE{+tZ&qLbarFe0(<7_Qz@z5DD zDlE_plcXkaSni05_ln!SsQunj_oXaFj_9@GjXJB}wvNU&A z*uhDI-ls=S_QwAFv-0D045coP&6e|EN1POgu7LMo2WtN2+$yX9Ko7*du|q&7=M?dL zWps3XHu%I7)!Ik^vC_?Z;i$oidow$bXdy-{8XNOv6~i%~ZTJMuNZme~Z{N2c^3Xn< z2fV5oT^hJcFl@+NhxG~T?zBs7ew04yVTb~uRkm=)v7L$$UtcH?yUoo9eM#7N@A@;b z+>oz;(Gf^I^4W%H#fMLS9~c|jq|~KE5S178ge7lqrf*hCtjD;ep)iF`2kVcM*IU6e zY|VJIVQ7O|g|(12K|Mc8$X`(z<0x~Cfm5CUTG(A?Oa1o#k8F1i#G&%xfQa!CDPRp) zmL37O($6nZ`*d2oQv^m;9!-k0LTD@U(5D-(a#WTc$tL0X*m@D8)-3oCLX|WU3q+P% zSdv!~-ZS{hg`Oy8Rfoqcsz0Cpjey~Mhp+lNUF-LD95Xa$(poiO$XAF}y%m|x^yMU6 z<1IFn*#R9ewuRnd%+%4Aj3G;oZGM(!M{Q#!hEb1uz|H%nuac*a!{SvgLzV7n;54~$ z@%fVsgG<&G{tA_E0q+vL5c47G_SCmbM8-VrP;q#&uX(O{j|76>kQX`aI{Cs5qQ?Ls zMOjvfnS`!kr|JEwEO_Q>&RdSJVSB+gdOT4rllShS)FY%QK zm+R7BPq*wZ}Qer!fxb*O@F4tN7}D|?5`E52$q+FU4}9SLsD&_}=K$$Ntz&$Y_!cD*=-_s)fLB zPpKWbcEs~VEA~E5kB&rtoXtHe^||gJSHdohoVq9kBVWojs4TvA6PMLsHXU=rw(8WE z52eZJ79v}H@j3F>Yu&9H`bq^>1HU&vuesw07sKni!A59*U*Oh*nI26daknkIG z?^t?$;Uw_(OnkcZ{wPqjO{_N{*^lxZ3tw&`_%i^`YjOPROrqQTM; zi>U^HP?j8X<6-Hy2t0RrJ zb!gv8CkQLe6Z5z^@<<@B(7RkjUxPEN1j{XUvDx6FV-LBf-KHhQXC2Zbcvsij5nkU_ zqUs;*F6GJ?RIcI&!{j?!Vs1wzM`lJCePRRcI5E}Ln8n_$i94Jp{b0DksI`;|fJiLd zY0|7Ujh6YG52>U8iU%<11Jvq4SRi|epf)OueK{4zRpfl*@UR^e^!qzAF}(DQC0i%c zSI^dgfX=DPH&yQOG=>~B(~7k$=B(($xjSvm_k5K;K(gc`8(v~?#a$X%{&BF6frE?Q zsP_6U7e_%(7&en_hb-50_|8+#u1?$C|D{Y2+L}JUUiI2 z7MhMrDFA9S5>ef}>+=P5M&vD;^%}kSu`yzl=$psS*W~ zFAj9gD?h(>$q;H%q~^D>jG31P6q)BH#X=-3(+iG#S;{$dk`y0Xtbr3N-dP~_G9E*4 z2w4ZPo1sUlG~(mMw)Crep$PFG|Nd^D6W{5;cvZLUT{=j2E#HObpSO{!CvRRj5jq`H zleh>SJT-JTys zfS0L2p0XfADYB_2Vq-t&FgGe4^wl?tzsI!hK zv+m~;5k5=7wFQ;;EJdNq4ue!2X}1&D9nnfTj6<}*?I0c zUVbh4=2x)Z!XVBI0xw>byV(QC}e% zaWiZn+xh-7QviM`&KoVHNVa|(m1}^Mb(FFCWVmpONrLibRyQ*!!p`4@?AG0|R4xkk ziu(rZ>f6{AJ6@L0yQu4b(C%wFmA_7vXJI7m1D)R^ zHb_wg*WyEXE6=J>8zG59JR!=o;-3}buTJdEVC>5!F1A^U@)-z!3Ie~$Gk=L%!Kt?% zf#S`-n0@l^#)DR-@BoUale$uaZ-U_rQt7XeQ@hNN2%>YJIsC#}L zn27*-lfK{?HNO*Sh0|lOynYoY%8QfZ-{Gj(6FvPLVhD-?PZ!tC4xabM(2KLIt2zfd zoAJMaf0gK}(+I#qNNq8kbDOkkOC4SbgfZYPhHeyBQs`f3@{6;>P*EVrnqeE@jKM z*CXPYIUy{<>GG1T$7aQX2sjJff2Eh~ulobSJg`(^9|s)4S5TGT6X5!icT|d^(Xs&s zGTh?d&S~}wUPRz1_o!c${}X(qUya2v993!^{8q?v<8T^H)iES0|6db3G1mk_T+v$%SOiKg1zJbymKQ%t*+Wa5(1 z{XJwM2g9q7d>i=m;u3QTyp%c+h@H2zi8m3zT=n59{>~T8+CRE}FP9084Q1skh+6yY z59g*4$3@tq)cLMskXRXGjPltyc|JCPZFXoA157t6I+GOgJ?H2kNz7&<%_2;Fp3zTc9k4o%Jr;u#$6|LQmIS{9@%@Qfz1)Z3URzJ=pO4Vr9=zS z8A)GGI4V&4Tw;^0pZ<$Rh*twnz&*?t-4ZHNIjA%2*<)78o9egwPdM3fWi^prV5b>F zG2V$bgwd{$X6{ge@uFXR_1}Z5${%hsM)Kl%op=i>cANyiKOosCB`lvxz*H)#M&&P4 z8|+EL;g5u%i z*Bzq1+bY6!lOnrt`uUeBG8BSvH@nQh5tzS$9KRuY$v~U$*jQXQ9B=VR`w1s8JfPUr zFdG+3vEZRY{fmX)aM_RLGZGqq4uvXzbwrAabK_J|l6{YH!N8-ch7UxK-% zK3M<^Oj6?K7G|=`l=bV5?-Eh`Z}lz*3j(`Jk80QN7I`~DDc1NbC&|@MA`vTcsM=Ez zft7TYY`|ptM;@dPZ7JijgtaAOGrw=T$T;MIucS$4nrbRex3ku%D%*|__=JUGPV*GX zp(y{_oyg<-xB(YDjjLet;|pb%kxBF^7u_*8kB$R(x)TD%!qXk@t9@m09fWng2-K;r#Dmbx54AJTt@r805fr_f zfR;DMgXQvvH6D(}LKO7>kg#t6=UTWZcI4@jP!CmM{tWzFGN?m&%98c z91GG%Cy!V%IMD`{y3>gB4tfXmN$8oJdOJvhh#bzAkch3ZU?H0~>6X?bC6uop$-Hix zoJ^z)>(-XLkjK+N?zt{r;-?di>qE2LM z(ya`qPK$)NR-vx{96hjrkJKBx3t=2xT@o^wp`?Rfd^ia$%*;j$ZFZI+R~Cm@skp7= z1}2HCEn;_9Dr}c>&w`N9xdweIn3R$w)^3!jtY_})5n!7V#6l0(Cp%77$$BZB97}Wt zdPkhE34r7^?8g^G#UnO{p9#Tn%p@5&m4cp#%XbuV&H=C!^H=Fe98y)*(X+t`hEDk# zB|1T8@Dpk8x4BCLqv3f!1-S6x(yCW=whyK~FIJY6W3A>>Z`u9DNrCDSKCNZg-julEQr zXirk$KeH=!W@7l~{#3WqAd~TPpamt9Q7%SH?RU0} zAecZWOP6t;k)Bxhay;*w^GvvqSFE-8&!3h6m0LQ)TE5-s_n<~ns1r{l&|x_X^j`KwmjFj3lY zEPo^wKSb3x<7hbj?k6-EGj`n%=KkrzDS!{%ti^BWfT=Nn@0=PfXq$YOXkfh zwE6upV&F*8#n&gUEZ>F`Pn7>7BuPk|#hb-;{?tcG4qe!N_*lpP77LMfNx@oJJ0@Jz zhH9)RlI(2)6A|R5%b-kj%}DC6d2>>#f5j`C{{ULen1t$t>TJwlQ+WU2hui(r!*HUz zygJYi@4wGog;8GK5oiER8}8)SFmaP+|3}M0MBGnExeZh>BmYsWPZ&ah-i7Y-yK^Mx zfri_=A%VYvFJMf>IqWVo?(#Vq>m1H(#YEoq%1voB*K-X`^o%!M*5V=?P9fw~r$}Z{ z37UA^*htRRvq6%_l{V09`lfstg?ncysrHA_a48@8{4sPsqBv#a+oN>ugdqvHsK1t!K1ay~TeBE?cNCU~| z&Aq`wjfarvBj8H<1rI4LEhJ!~2u4ns6vbFeuTU74%<8@r&>J`S4b}jy^bKGoEbWMSWbuJHJ z-9);A*+N7jE4st_dr6W3Iu6}>H587;2dAC2@nlIAWJs!c>!uE8@0sBY+>TI@mfWf< z)U5ANRF6JY! zV}ZENnA$nzmq?#rZYXy7+(;tBg-Noc=quCW^o0{rdT&hbgWin*1PkxB^ZbYwFV}v? zO`t5VyT!#V5#TVi6kvdh5jie6yxGqdHo6n*TeVsYvTv*RD0H*zUIQ?ilp|ufu;HJx zc5hxXQ3s4j{n$=5a7l|Xy}*XIAR{(iKe~>9P_hXyq$T5w*etnVlJk=)sW~I}Tb@2A zn^<^Qqoj6px-+?*gH?aL4^n>jz`VH7S~8)}>5H1Hk1dlZ_`01ipVwoYFdF8lm&~@a zO-OfM?DQ%Zk;(U(#>%nwzZ`-Ya#%+V8|6+9^AlGu_uMtk>*%#h$Z(d9utAA$esFDd zc}LBFpw6tXfDk!rDFg-{bW8qX2sYz)iv6_C{g2=s_-t$Z)YZ+`M}GXV>bf1saz^v$ z?{|ZLIq0X3Wb9whqjk7`w+?}P~JNk5ub4VLu$ zlu1y`G_3L8zqvz4e?16im$U@Yq1muI{7|CY`mxc#490Stc9 z8S>D3`8#dv0*RcWyJt?bj@e%5(P`On_S+7!fAdm^s!W@kzYHTOOVZ4}VR+AEA3TlM zBe7l+H*x?F7?JUN+{W6vB(H~c^k^kB(!?a$tk0&f4_1CqdB=a6+)n*dP;dsGB*Yb( zb;rZ%CXUL));lJ}BCPzJT{tA|k%w)Gc}`rz>EYX^Xz9sHhupG?L)7WLf!#Deea%i( z@{ZA|+42k0;++mG;)c2|Mo1iZVtm){BlK1u1$Y7|yvVGLLc_Zz(SrV{>*naS*=r~D zCV6XaO{EBEoizJC+F}0Y^(B?H7{PY>-uTTUGUm;6}@XFkxXK z)z!_C^t90$$Wi*P<-zojpLNM9?L8vZDX5LYR$Rx5`VT@(h0u1td zVHZ6aF?dHCWe;48A4OKd%&V46UufoMo3yCBxQ#;sHor{b2?{HbQgSBJ-|Z=RK-Tdr z7DnZOGw!y~MQ?C5C>pZJKO0xhtV*XfT(&$WVZ?mZ;Lo{V=R9ef4I&P2YYO|2(>D!_ z@Y+=PFq_WV;*VWp=W$sEz4o=L0quew#wco>bzNNp=Hx@x?B#P7dcUJraSF8Wwi1l^L`p8+X&!a31e<4%tz&I^BHsQH?Z zv+Sw=FiZDaz?i8I{V3{y0p++1l~LRB#E>HK_1FNTtrG}z8J@- z9xt(uO0#}2N6R2Y=<KUh;kU3#Zt3u%+J9l8Jhw z9~94AeLdYJNmWp5vAS!T76EN9?YrjpSSEyr(=_Af9{{Y#p8)tN=mOCMEH94$cD?6t1Bvoc6!gw9&v>xz5|S*Ho~9kh*|tE%dKe}5ZB2zy zfS9bxjcjDr`Z@ZdDM2dp{TP4zlun}{jNb=xKY=WqPtp~jeb#{S(_M+1E`L61KaYPW zXWJgLXe=6#a~4Jn_}KJ%%>l}&bCjD}ap2u6x-2WwA+e)*HW*@D$$h_FBV6;{=CqmFD>XD<8!$3Q~;`A z8}vYa>d|p>Sq|#tPV|x#ziT%WRlT-&q=y7+L&l$V?SNb0f?&CWEI;xdc7BmqyDL}L z#YJ^JF2TVu{Lp6nxW^bm*8cDlPu4$84e2?Ce1R)c9)G^-&fZh@PhPwH0w1R|(}kxt zq8f*9CZgs55KHnDu$D3DT?gY^6>EPJq+UB;FU36Sk^oP`)2mqY<$C`#1Gvm@84Ln`oEbt8Lk?$ z#U4+m_DwkEo!m>1^=HFjS#LiCNgWBA)JVfaj#S(hyY%ITC-rd3MX00DASOC%$P$jZ zXWYMcEU^`QbU&r4g#*RQgtFk>S05S=Oh%L^=H2>m$b-t_nai9v{gGzFGhvt#H69 zfoou%_=>&3Rvu>w6^I^RM*7pWj&3_!-3XK4kulO|5E9(~#V=a`BZwL^mUsJe;qO>X z3dG%y@ZK}H7X@hO(Ud8dsp5{G${ljeOx^#n<<(oB3J51&i;4=D?O)g@+B-LZVF}X^ zsWVa&j-HUPfzat24$0giCQ#5+*i$u(#Ft4%w}B@orO!qS{sIX@PIRg(hVOors>%*y z#+-QM9+-D4&qY->)$dz1gnPO_IDqFZ7{_WH^gcy~N)0 zkR$M%(wgvY5OyPTa3vgYUym@mVSjj4J1Sg^uuj_0*~v#!yXRq>NxzdTQRuHJV+e(Q zlS22REz50&He0C0X--0wo4V80#yUa%uwUU|LNINx60Cp;a(oXbuIiq;9?=h>?_)V? z&0d<_CseZKt8_NiZ?AI)1bP&hYe^?{2k6;S_tn7AWdFcp?+TFxb=QQZEDCq$hkmkYAAn-s z<;Xg~Nt1B&{SEmu1tmN4MfZ4c)V}3A!aGo^U|K!F6P=%L`j%Hm)slxDH2{l0b+?rn z+r~-g+VUm`e2Vt!+N~EQ3j^6 zS(~@^`leSAx~|EUi}jqtkKR_cQ5ILA+dYO_WFboZ{v<##-2TX9F#m6(n3sj_=!eSd zpS{+Zs2^_^EeoK9wSJt@R=m%dane9nT>0^LUh7;hpzy2)u5Uj*7cv|6%onVuAE(+$ z3blp%Kp07~et6;K2F1fqy@@ONiQ)|!KV@NkTpUnVD~j?Z|2G9Fe+xp^0DK6;0V@P;_-e>$nt4sIu>(i;&uXDU#Yqs>)HRm1W?v`(L2t%jE!X*`C8g zayJ3r@_7r$Xpn9kI?@F79Y}%TZf~bJRfDlrRX_fYKlD1p6}}12%nW^H#bhC*J4rQB zBI`k-_5G6o((SfGfryP^vyUCYOw&$;9;knl&-qs#`0!#MDGx?`7Ec1Ymg65I-D^V! z32gQE#Sx`Clgyx1^$pY5z1V3n$?7DX%)ZTe62LR6Ci1vyBwPmOz3wLeRYe)EI`|8WJcTGvMvnYRyhDixrae?|YCS<5c?nr0?Os==WSX@0# zilh$tcIBzsAFc;ut@lm%eG8;*w*YH98RF)z9xXdaYio~R3W(l_V3K;q+|;uITqbT zIW2G-Zag&pP)@g;ILoiO3z_hReefa4tv}^`m9rn`P8*jjduEmPK}hzmqSd_?V;Z|FAH7s~ zScJ}iB;KF7a`}P$VVgXX&D`Fny_IkG!KQ|hTfF9Ih3blGtI9W2M$l5M2d~6Pf{8qk zSm;ou$q}fIZQ8)s8?kf*5>s$G(_2~s#$XS|^g21c^hN8*!N5Zc>by?fI|I)Cpcxp} zEe1s#XAA}>8NC}C0(hLq0F-Zd?&j+3xUR?%!0{!Ud7XuW-+PpnTAh@f<4Uvszn-oGo~rl%pX=K55|Me)u(HY?U87QwkjP3# zWMxK1ZWOnaRJLTxiWFtIwO`~JJHSGf0{=RD^*&v`zd=ks}g-tQv% zhd*Ql^fwQDDmo9u+}fUZG3j|3gwzZA44lrEX$y)E!fg+Kx>Lq@iNRBge_IO1PjUa4 z8Z%SgTZKhJ4%;g~EaF_Ffw7T(b2(VvE7;Lg7G;Qwgs{Cb&701XAYp#~xgV+P3Lk;q z*|j+*h1hca!KRtUi)I@PP{1He0q<_u!)z>Q6<~$5pku z^g@{P4=8Nyj3)8?nMhdq+}BD_zwnv%Y!m(A06|WjJ`q0r^;B*a`Ll~x2PZEPLNQEa zJ*f$X2b3?2s?C1TRL18?Q8M-T5a{{-_R^ixB(P>D@+Q7G7ka`F5054X5G8M;*@an< zy}xQaWHD(>zt9id^a?s|_BO2{hnes!nv1?$3UM8mg^VZHD_c_qw7SO`A)7!7r{+1m z=sNCQWo>ykO>iowQC;qp!SYm#G+tQa!@BJS{f(k(D=Y6s>JajW?0!AEVgT z#;B=i;on`26>1Y_g!< zh=L=(N^Z)fbRu*5^(S#hgQ@RMwJeb{AE{o8z8@5+_5)k$Gty$g`u5MlFV5aNYPt&o zX@{J@r9b*`L+_ZByg;F*{EsU$*b#4!c<+@is&$oydh7@XQhQw_qjsdm9 z-t2K#LU6o3C830GK4f&BJ&HeJVk8nKd?ZxvjH(s9e-7QJKxZrjid^N@weNGB9UIJ`vk~ZcMy?l$`5idK#tT z;y}vD6%{Cg00zynUHGk zG%$Xbdzo4*Q@z}p1t~Qb8j{157!5J01tSXq>C6$7$klJ1@*;J@CPF4hzF&??HLPh_ z>Ff;FVie@&8Bo(5@y111erb`FF6Ek9d&tk4$u`kN-FJw+!&(K?P5Iy>z^$+q&k2iU zLIdY6xO1!B$2`lK(xM|=#;JWg9}*=i609B*IJByW6Tr*V+;I%w<}nkLEgSCGPg$N- zN=Pte`NE-?^35Xw%NXvdyt{h#0h=flPFwV}=EIaU+$)-W^Td?NjgO(!(J2}jO^nTy zYH+26>cMMF$vj&1fwF&XtBkpJ{t6Vn#Bku222e)-e)18N_2~BQT3?z=M>6t;o3JVW zaB*YVgjr$uHS?*d>07tDQXWPo?~6I!Lbs?#7BRm4qwW^ZxhHWdSZmzSa_bFWV4Z=NSni+^;!MVjATdtdu#Q=8k_u%+3q$ZX`qn}6a z6I&L3c&f&?Uh%`D3aOQ?MZ)!MHUcZbDeOpA;LgzK9+0As>x-bMQ=LF+-|rNRNcXoO zH|i_01(oZANmVG3|MfJ(q>nFLlPY%ChXOZ0iWdAn*@jhMBPf!QA`}>lme`yP+ld-M zPZ%=9O){_Deer@_GDzSW%gB4lk17~VTKt>f7e5k?AAJyns3upgjuImd&+}6c=peO& zhAD}oGY3kAPDL?$U0u<#ZC3n{%XrrM#00<`XZL6J;|HCa9jvO>iV3&*Mg|r0y%k? zxbz=f4Oy}q_sJ6wO$&> zDV{KqBu~8?Y*x&y?M$zzke1hI^qp{0kAwM0l!a&gd$Gf2kK|#rPRE&A7 zuO}tRK2OpT@ICjP{I-4$3Xe#x4Sg8aP-^Y;`uN;1Q$h$gUy=0D|wiso7mNt#3rDspqto`Urp;b)(JI1-M zsJax*|A06qD=rtbM~)u_YKaZclpVd#Vo0WH zf~BUH#gZzjbdaZV+>$(bi^u#KH_LCCLrmQ;gpmSAfzx%E#jNrdUina2`GILxW8A@Z zv-(|_sYTmA;(l*x=KZB)sisj7*AzNT0OC+;Je#X!z68BEPZ$$YTdYpof6deT_5f7HfaWjSM;Y|ZCu-*GXfefLQKa<{;Z zhCr*)aqkBU$Kq}D9=_?(z0ReGRfiZbQll|K0Dudfox)haR#dwwaqx8I!?V^Qs>^c5G(*1{=;Oioto`erw+| zGn}At$BLWJQRaL3YqsIFf)Md8CPIc6B2bh4sDf>SWaY~wi|cZ9)}4|yHT(cnU=f(n zbnQMB_4>{L*D&RVM^b`w&Fd42>vd)lSfHCq0k)3`)HK}%l}GtTUgU*$%PdOG@^(#K zx>A$f_XJA5L`%BaaQ%Vu?++pBE{wGJ&j4R}6!y?RV>#dK)NsiDXH`-@On3xOq_&ie zqDU5|JMpr*`b@{?2!n>DZ668N?Ft)4 z@i4FwC7D67z=d~YR^DqLYUj7NhjTh7Cz26q7UbJTE(Z76VtsFify9lq{Qk(3Zj)*_Fwai$^Iu;(kJd=;xbBQ?O~A;HLP%uJ-fBh0Lkz2xkT z)!4-`FTAf#3;lY%-?xB`UlJxGR-C0Ow%?Gkik9lY++fI=ed?1_Y-|q^!H077OTLMifh~{61zzwRK>{&bTUi zQz4mnFDdGe`DGu8y@w*I>>;#O6Q@^3TT|+PAIMjDU(-0uAlr;iPC>SHOHCDj^inI# z_)}R7AglzGbit@-6vh7m#Y*z<>}TbMAW3jD<{<*Ar>+789e&o zT76c^tT&rX*#x8IvQ78)6}SvfNoZZRW-3%17$6O8eOz1G6889dM zB5nkCiS*y=N@4Az_Bq{MeyJ0S2~W`-_Aj~T;|I0GQaK>N&he6(1eDb)zMdEoE#8f+ zg%e#;C?dMga1I+Y>|Inry!pbuVjnW&-r{a3W&p7d$_<0DI{WIE(+!y0S1~_Y03Izw zBGU>)K72z>|H1tkmaeVMN)#aYwKF7zGV?upDM=&UJJMlI)es9&j*nl^X)1s)>%-(b z!$7z_yZB8nGbUYmR1LyxoS-L~!esgI8Q#ERIIob*Di4Z0Y^*&lAouqX92^NV76urb zULR57L5MFwP*UG|4UsoPFmKW3LN?-mhlusz@|$E?c4ZUL6@Tkx@q4s;YvRN)8OBWoE*26 zh!%`FghZZu)QElPmVXNBo`A&~sOh=i)ZswUPBw}hA&p}3M^(RA7rnZOJV9BOnWQJqL>L?|$?M0sIl)Nv*uqGmPqiL$ zpP67R^)g1gd+d!Bm8R;up4t1`Lf6pR;^zP|^>j6S`V4O^6=@&~AzaKj6z`U29mOx; z1v6oyp)VF%f^3&m@vdr^aOJ^w-)S=XkwPcempL4mLJ=K+)7K0Ng`9~fD`5wE=Lot0 zR1~j>9_Rgx=HKb{5E=@+DG(8@Y+R;?M>~$m3*fB1STPYwLc@*#r$JO^>^=Ep{v#|R zMd@MXms4azGaFwyP&PdKK(2*c{haf^Zj56I>kC7D(>wH|OZ+N`G+z();yIMNn1)nh z6wi6wR+XtK+W&k|Pvh5lldWNHA2(eo5RS-^4K-+qXahb3aEs8FGFi)g9?ad11M7&Y zB|(Mo-jS^i`>+z5&9Ft;AZuPK-ADE9tm=eIR_*)|KB9bdilM3@(AHLuk^OxpU?N!} z%X=#wv%3M58jUi6e^GWW%NP;}SG7{%Z=Nw|Gvg+9hE3;yrq+gJDVb(_EsyvJ_kyy| z;;(;QU?r$yS`OGHsZ5o(*)jlVmWNSUU)ao)%o(|t-4@~B6i9QbdBE-~Q!I!}xaY?!bo zUtk)8uROVP62eD%?1Y9SL4En-I%rzCO5&P<{Rhg*>}{H`_re$R6-xFzx2ppFT?8$D z1oI;pt7uuQ_fOp{8YA$cWk|9lXTgM$w5h!~Fm{Y{+XDD+=~8mGu`(s!4BspCn+@mu zRA>Izx831+J(%+maD)sXYqxZkOrXAW&f(f@z#H6=NmJ=Tx2dj_KN*1O zS(;ezHwo*945Wi1n=$`U#tnM!CiiYu zAv!O2)r&?pAxYm;%l_7%chojcURaBpo)|OYQ~gyj;47W`-;EH*{Git@=Br533Kk9Q zK;cAn)P0HntNre+4RHp==EP*jJ0n0j&bpy`li5VSH|$87?33f(R$V|O+fnf?BoMU; z=*ul*-^TpPdcY++*9`fYeBPRZXcZkJ0)3h+Lp;#EPV9gr{i9mjCH3afzUh}Py~R7R zb7jd1!KRo^WX|tK*|gDr-FjDCYyIt3j3@INve(lK&W2SWSynx^2&0M;pvp^K;rwI~ z!(KMQTc#HSWrfg0lmV-%??UNYYs*D+ONhQ87dZ4a1x?Wo-&Y~@1(*R`U@3S+b;UIV zIx~CFJhz}W>eDuMr%q&AiOnI`E!y{ zRcB~P>y7OPqIEKE0M_v@edo7xbs@D!1BG-@K*&Wq*PXL2jDtBhVxu#JoDLcMw>C>( zoSjxNza29Gl#4T%xt0(#ZCoVCZvJXp88YcfS?2~|Sr@PB$vnt@bjY|&Vby2|r2QmH zEN^NLjEVlwws`g&n0)?yZl~=XoY5!;N>YuSO@B`20~=;Yy70TL;c^6}!VGBnIUdda zjL!M^#_L2g@rC7`sfjKfa|70JL63i?0)iPvrqhEZo8r$X`+h!iT|>SCC$7b}V+Z1a z`qLSl%3C<`AC7M4+!pvp4A^}uoqj+8nG555&qkPUKHePs2K1GlX2$v=7akrbSGP+4 ziX@ONvSK1m-7Q!fYVx*GWo(G{?>3Dx3uQI9GRGd5!dboeC=E1}d>8l{SsmW0Kp9wR8{pZ$a_=vodU2CpNAJHS ziG~560x9TKB<1lT%6lMmK7-j`5X!)0so{lE`)0{LXdF9he%+15Cg;I*vm$K*$ z*-IgA-H;5)?f-7u(cwxxwW5RU5qvP&n|yS{Vt!@h*S5cdJNS~g;ZK>IZc+XxgfsAYnw9SlLziq=GGze ziV+3`!R&5dS;k1e_-^<8l=*N&)V{jc%H4d(s$^U zd)N4N!ruULCwkO{AsyR4j@ZotKg9o@Au+bf=D5|w?$t!;lS~on4CCN53kU%By6DQ* zH3QRO{lfP%T;IoC()uw{`?VC8g6J-3zxtoy(W-Zw8+>1|wBz)V7a9Fo_=PWv3hbaM znvt6sk47_cCMXXZt03OpBZNbrwL`#}|E;tU-G5J=fagVxNzoVlGG0tIYgGRXa7B(U za^j;|^&RnrlON7R@ISVm3oFmH!ZdKGu_^9WtX!h~^xvu`qFO2){oTxm*UF@m$Pc9M3qD=~JwZv!tabJJ&m8`4FRo?uzqR_qXy7ab zyO{r#EGqlGd^<_HZtl zKkZQ7nH%V~u?VRP{W~ZvK8VfH%Ew|Lq1bhvAIkLU7jXBG23BRFj_tcE7uX?nKasX) z=@90Z6|i8MHits>wh+`|b-waupT_9DY=sgR8!WE*Y3kw~kIuwbhd$^2 zT@a+zkPqdkexL-G49UUuD|NQ9h6=L4Hhj!L*PyLr!>wXj__!D-U74; z64Th}O|h{%FBdQ&K&0~R-UzG7r^fj!Xgl52=+Vm*(Z z{Wo)#fZ%vCSOCBJOU~@(K!NJc+}l7fjVKdyt}L;Ix5g;<=fy|}0>)URhACCdAZ~M% zayNU6XX#7GXX~)=^3J^Z|vIkm$%ACUvZ(3E};e(hCW}}n>u}B+uPuw*1YPU-%%aLY_9(ne-;kH;v~e> zEN-+`1)K?Be`NkwtYD=a9K9F5#1is?HeLeXpqyy#4%X!r)q@pOD&YJZb{YD7)m_qQ zX&t>&I0MDd6m`ArOVT!y-mU-LH4Ku{HwfzHe_%OeJj0aC98r?ao1E}wnQ12ERSAI9 z9bV=rJ}-k9>y>ZEgkB1N{M{*h5~pJ)Cau(pPp2V#`R9z@1-jQnFLpD_h3jj`H`KTn zQq=?qoRFYs@U>l*Yo%>>)&+OgC<@%5cK(Oa0kzs*9t69*H5>nM2d(CRZryzj!jw#S z*<#(m0Z)n6Unq6{Mc}ON!QOqs9d%)Cl9sdv%BUzDHhqsC@>32kWt}Z9mnm$ry%c^& zQ+VFZL*AO^Zp|uB_TT4&g^zf#CKPOSwm)A+j_qGA`74D~_6EPQi6vRuoWtUOaDVcD z9n>;Z@&Ypl+%ApgT4gu+62>PG?A!gf7sGnL3fmDrvC%U2Gr8GVE8s}H zmEyRo=2r?{BrRzdU_bA}GRaBA2(Q)1-q;h;ZT~Fv9Sr%C$8_RTY+G47ss^BRnGXZB zig+a!T@0(!P}fX1^CN;`ofto<5UA(D6w?}tPJ)y~SLfHaL&4DtsLn!*5mqQ=c39oW>}OOU)*fo7a3fx4 zA+C5kSk#Nw1(l`EHBrih#OBmSgcgx=@4^JB1jJ1_v`?rt# zExfXH7+i3}!ZqS0f>|YFK7>jfXcOu$udaG29pL}xh6EyS;%=|qN^)cv&#{71Ybs6H zZ$xeRzZFKAUfteiGNO^T@1?pJ9ERVoMeJ28-RFO6)-a}Jflh+{W`V~8;VKV^3<<6w zGZ=Lt007_AyO#nB*ws0~^CvsNSMPHrW5@&(s#)tKo+ z21|&th6n`{QAJOMU}nV>QO7xxD%Qi*dL}~3jGF!I6SJ3Oq~QRuj5Rq5g7Rb#-EFU> zw8wk96Q(X(hLhAS^Y8z;MHi74m!O^gg*D;y*vuJBCIZ|0I#Cb|w0=PzH$EQCDT+T^ zbJndLm_%$J)P;5`-FTPSmiBMr1tJR3;qA6DqFfuwT%Hh`yWYl&R2hKQw`~fOrPK7G zY!LA)#0Ydy@waiHW9gKD4UK8VzBykx7Bo;O386^zm5BVYOvwmp$I;On~l)F;f=<&gwshJ zf5c-|z0nH93e-{{h}lF&n!_|jDnuxvT6qthdSeC!4gkx@csu5lR7QKsqgtKI!dQWq zLPaVIFxdj%-JyT?&p#0$=IIE{bgu?PJNVKBGCeMK5ilhD=ou0AA@%^t#8R?;Jh{S6 zlw?ARqHD$hU^+kneDEZuJL0R&;eS24RD3_2zcK4{qK)qvT3*XGN+S^&!t+!rEey`h zpEPJA#b-GOcM?(wg}`PCp~x5BbM;kDV41W-Rybmv;KoPX$F|&OzFG1HNS(A_&R||w zH!NUYyJP}GAB86BWRGFqa4=nXV7k`m8LaE&7E_@1FacY|(HsrL3x#B<^Ab%=&z1%W zl&0V#*gQbqM0adG^yr|ud1B{6o-o{SRjK-0%~P24+DAWQ{Y62lHT;EbA-C}}n|On% zui2=)mg$07WbWi%DqFo(%m7y$l$24)G* zq?AME!>w#N`+}K(vo@bz@)5hP+2RXT1J?7`($1b@xb}%ieOeP;ZCS`8#w#T%y6k^# zO?LVZpOX^R^t}%VWZuDvc+l&DQd*j!`5MvU)LD~`Me&Q%VR zPQFEdiN?Kv3DHVT6#`nB%spOf@^P7lH2nN(994@M?_I}IZHXt8M(o~lrIZbj&?^?| zd$nz)r6)hz7Z`ApR;@)(fmHLnj#%fDfA0+HPu*uFv?WRTTqyb;P;^_D(UUI&&xblH z-ldeV6z%?G!Ii(e4j}t1rVkP{rgY})?|BABxXZ*{qN-;(tRIjOY&x4&WtDU5;UP?MyFa0B4F@}L;2=OcfLk(_T*&Vns75;q}`w6CXX|~Rlj4TjgCz1SPfR-biMr|T(!fiqc>xBq)aBn1Xm;ay) zAh>_}FkpF$RK0bw%A<}BtRe=_L9q`SQXvKmkO3Cjoa&-o{&}&xA?Yzw<2PTXBxrym zXnXamwe=L11Rk6JUA4Qn?{zDYHL;yy%Va&=BS_>^!s- zPTo;#NWX^BT((*(-Cu>~NvQj@Tulcfk#ry%Nh{K+tRTOJ)_Nrp6gXFWh89 z%hrvdDC%eX9Rt>`Xz6<%*F_ET7;#%ur|qgkR>gd!o!gF<*TXX%XTk2RT(=gJP;boO@<4=X zXWY-et^EKZP)?)%MNjibpX%OH)MWwE14OgLzEbBRY!qCOCw*TI6Bh%|O|{G_ec9*p zJhH1&^0478WKIFBNG?6eM=~3{(I`yeLhTQnZ3+a6Ewlb&LH0iA-d^4@vDyFl_3I_u zmCs3gt1;GcCBnC5|GC4D1Al)H3}X<;u;$A#5EgiYJ?cN*cO`8!P2&lMS@wObs6Le- zWC_V|2d0HiD#->eKYRO(+R4CV$C^#K2B!7OIWR}}+vhuPufGjU;|1!^x0X!Eg`(Z< zxPpB&N5vM6EN=ajc;UBD`lq=WbsXI`3MZ2S_X1tT^Y=e*=2zYV!kpWSR3&zs(^t7#F5`UUU0c-DQr?vE2;^@tF3E2|EQHX@sWq%wyD~u0$rppv@PhPEWM&IU|<3D*U z@2z7XB33ZC-eE*A>pi{9`YMf?4f7Be`tI^!iYP5391(>{JUL(!L+yce4 zgwnlzxm~pz+I=e4Q^)NMmDUcRi$~=)J?TfM0fd1W){p=Qa0XbZ_S(6%hGi!NbFEz- z{ux*RzZ3@qnI8GdowN2tS!b4m?8ye^l#u$e#PXM_(-WTy*$CrgxP5CaY1~M2dn}Vi z(!hZX@k;>^9Y2k8`!&50rvl1T%cCor;6vR+uZ_WcMMIe|vE1N)9m>2Mj=>htLIY(l z?9O+(odShYVg@zg@2aLUDR62_Wk4m12#=*Q_qmv9slH=VXr zWPv%zL3Db#(eL-|-dm=lalKbRk467F`3TSWEG+AuFY%kZ;YK>IWeH@d<8t?T9_7Xqdz{4`*eSQJtf^fdk~T; z#@GnwRiHSlK5?Ij?W>r#YoP4w9i}dgxnAhJF*{*J;Dj{b33Pu{c#GK9d%rAl7A?zo zmB=ypp&6>+5`yEuUc1UuuelYA0O4&-H;&fW;lJIem!O z^=n{`@7~Kl!`E}UuS42WK}f4hQvCsE@X6E20MeCwLc`|!DuiYdZ>X;m&T;iQ z5(G(KFTMavWlYuo2lARmT1;cxmHO=K>~qIjINf(GoK*-D+XSbi$j-*2qcL(+^FJ+) zS`4FG9kOxv(%UdKC>pML$&?VEGCfP#8{3*JAkZB6?d4A9M?tLlk&u5r*80x+o78*r z%(tYJO(^*I+f~^;!ctwLABwjmv3Jv^2$l@R+@(^uBKS+z_ICTd`{Sp!mc+3UuVBbo44^L^uPm1bxbn_iaTaCSKD>8O`nwVhnXF!ql?ve?6|M-`o8;SDJZozC z!qEN8<+oQfPK$4EWcG?_1~LVDGg5Pqb6i9iY&J)a*x3;1<+h3zH}2mFxVKq+E0c0? zD0PTcvw6v}tKd(Wg18{PcY3vfrrcDY*Xp*?)NVM|sY|MS(@g!q@UoOWxtreK{-G*O zJ2$e{a)`!9^UcG0!4fj&gsF3x2`kV@hkU2W=J-3ZIqE94ubz^P|1NfQcEudxJ5(P? zxw|23^J9a>a}K{r4Cm)ih+nNb))Bj##JAlwm-M;d6fyG6@~y*^7R&r$=O{|Dw-xqO zME?w&c=;zJT-{sp)8XxbODtvg<}0B)YiQJH>6&4gemtl|V8p{3k)FoD z4o?bd<|rBr8)qO|1}KKSGldX!htggHL+vnANaPs!v#`|f8&=fe!4>jaOPAO)J-l^j z>Z4bqj@)iNP{Hl?_IeL5Z5(;JMO8G{kwP&jQdD>qBvJIF;rNXAi(yh#ge=_+xBxtmSnL5CS z49pwaVwCUC8c;N_>O1)C)kY#x*MeICX5;7Uz^v)-jqSHqwVS>Q21eVP9le6yoAQ zgm&(#+c}+#7(ZHQ1~$_s9h(?*T?qzfb9N4!`O4#wu*ueV~E z*t6|pXG=ND_J!$-Kj1!w&%6doLRAXs=nqg#cia>|H_gxPI7f^H`5{gve0VL2H8EI6 zE{jn~hvf5F&ox9WkBcXT&eld%MZJOVaN$9sB%_I%Bn{N7x%O_>MRBaA~zZOmmeEI*h&}~kE9nD zWg|Q5H*9zKK-8S^-6R=#v z!a6DznH;iIG9|qaFu2~aOQMv7i%hT4?HVfgXq#_g45V4LTK72CGbrC*eq)%_*{;U0 z;C^qAyMHWG{$OwDLHzcf#noV?jKN}Y*oi&gN(N0i78EvmRsvAF-mM{GJ%GcRK8pH$8Zj>FEc>gVzBiH-il`o^g@fC>4JjtG|W^; z24vUH2Y!7$W#+;N>`#HR`EsGS?r5nN8GH^PKxiP2?$R^1{>+hZF?i@t8Em4^O+#{m zKQhj$6rmpXid{e0agx)0Fuon_yQ^ESYC=LV=kPao3W(K*wo*Vd#U2di+QFBCmCW)% zu${6m+6+b#Y8wEUBvZ0(VbJO!=l3-r1-=o#KqpfIvZ~2RZ*ur9G3w8-TJ&~>#8kV20^^O}rQ!=E4GHyq)q87y;Aj-N(ZqG*_mU%k zxL-m$uy^+y{T;_STynRQY@_v@MCEEs{27-%w2r;cP`d z#_!?SmtUUPot7xy|1x`+_S#I8k;PWnUqr{+{flOG0m~>9gXM~)OX=a3uc{F3(|H(aPF$FiAyox34Nh{;)>FB!iJ<9~m`yJ7&VMkiT6_9DX>LqeWW)1U65 zl2&$3_Rme|7D(oR+13`&$r~l2Vcpv5c+||M2KGXjo9P4}eE}Fgq6oI(Qy2(Ps#3_wv zlQPIsl^8HIYskX!^vZk)-(Ri#4jT70lD6PbUmSvwSm%UA)zjm3E)5CozPoxE59~@&wOLIEreodGEtYMEQvJ^=}WtY|t_ zjrSnyLL9(k9VIq=?}76^m{VYq!cQ<=>lM9Q`{=rJLk>sPBpWW5t!^y3j}rSgfjkDM zW=HdQ(tI~kpW%(zp550YJU6n3@gkPw#C`#LS)tA~yj%##X^~8=_hb+-UqNU|xuA1t znTT9$-VTV~@)ZPa%Zok7I{>6H(S@94dGSNg|enc3P`U=i){18FA7El=0V{AISYQ#T!MxJv~aJT{}9xs-y~>*?{kL0{e9yAmzfb%_g|+4 z(l0a>P13kLg{?WsJv0H5ifAM~6BSr2QAl@k{i(1xA(?5h01AiMg=7L;!;#f#&4AI} ziYTMv5QIo4>GWlKsyhr<#S-c{_P_5j&QyC`I~Lt(2Z&e7U{h5-S-xy85y~7XjtH>Y z9N^abLu4u&h&aP}W*xOrU#HDV0cX}0c|>G!4mVZckC?c+Y}rN(kE|- zj6X)N_kbvW2^^&H<5N;xX4$M5w>HX{C=vYm6y}rQfLeE0DZh@C4b3k87P)p5s-006 z2hu~~`)X9L!C|!ES3#3xBtV*edSD8R`q`iSOFhbnDjniV%ZoJAFhw-_M^toK4|8YQ zyIwB5!B|m(dP`O>x+P&_&*mYqkqv>b-j&HEE>=_dW9fD-|xDc_5 zs!c4j^3fXZc_m8cMhIYAo-hg?%8Nj~2H$(+U$Jzg~s{Ip>EBFsSb<{cnI#ME}_O^WJ-*#En~xVzgHShaUTSZSeWuIcV?AD`P%-Lg_O@ z?W<#ZG?fPBJ}QCwxh1=8mlgaV-35FE_r#+E|u~ zdV%BO(8az%40DiDxFI3k<9q;R&`@Y7mf*Vc`X-T${mfcTJ;vdPy~uv0sYgzXY4;dw zdo{`x2(D{gPK-D+m7)Y}q8)WUBicf4=rzLKv6-tsk@8MLjv6JE5HFmt>1+hfjO!Fu zfV_|L(FD}FsHLHYcTE)gp?ygpI-7S63d3=qp>>9MuL#|2f zLcig{V!d9-;lM*AD-Xf4{^D7kFo98S^4O2#FIh69BjJX9rnn;-coVP^Pibo(smS$_ z34DooFc}TQ{5^sZ&-eYOO!^q&24CIJctODX6WsuJmH*bV1i2iO+0^kVg6j%Cp&k+z zdm&>JYLswJai7A1f-BW~*xL4BPef4;H>|8hm0U>A7!mBqn5?2^ur!k061=s!x!K&> z+FD>wTbi7DJ23E1Ts#@&3o~V9<=%%larI9uE!3syAmsm6$wkjPlPs^DMZ}!R-Vw^qKf&&YsLA)=Z7Gz z-QyS{2Yf=y``eM!#HnRTzi?Ff#aBNnW}q>{I2_Ift$GjSw<6o)P;6vY;>!4-AZ@g$ zT99JLi`tWfho@=tfBTU)7{re2GveJcRMVB*)rd;*v@h?1n@RO3E5Lv29Wji~jJNn7@Frs}c3630;dZRUy zg=H;BEXsUea_A^} zR1Ib|g;y=$@JGypHAa&(!{OzXE^f~*i#ZyM9uq@tRI-rHK8^NkR{X zF@haFz~g70)M6eqhTlptA8zhE~P^lt{jm-gS(yl#SzNReILpx`Cx`m~; z%($JaRIiQuu#A$F-1sB@*8LwbS%3Nz35ES?OnDDX1zyf*cx2V70Q~#;g^bLKXT+M0%xc$d8HYB*!wkc2?ET!{?7l0}6jSKqMA|>n7@&N| zp7H(TM|N)KaJU~sF^*x9M`O59_L zp0B!O_RS-G+h$86$s>s*F&7%$-q49fY}sNO`Nm6)N689iPk;9DlQ*0zI$}3Pk*gLM z;t<*Nc4~e z%jlHZ?pyWX8?v=gb7lSPz5Km4hig?_M)NUJX=7(^GLm4;w(%w=Vg-~4P4Vd-{5M!) zoVc`XBwT3Nx5eA}%g3b4b$U;KioNW5sQeXWqB;DaXt-=)`SmEiV**hEYN#O50r!z=RV_N=((i)+|FHcV0_EI@gyp~elAG#@~e((>}DU%h6TKml`UY! zeM1zUSg+8IGg7sjy#IT-w?Si}Z?667p<0y%cDc~YR6T}#=1;={+Kaxn<#=ZralABQ zG5auwk@95_Zru&S{`guj(0u(SaiFhHMDI$4gWI`l&WyPy5>7aJ^#2_2AFLh#XEsPQ zP6|BRlIDQtYvzJ8$6Sg2c>*PNZ3_r4? zc#2q3Pp+d~xIM=6X-Mdeg3s|P|G@)>&Px-FAymj? zeN;M1pN)m%m~polvdy`Z?E~?DeHEnExn2|w9a=(reY1j0fc!l}0g;-@zPS~1bKkID z1vfGO^mh1HIOH~mxw6oLsu);*Z~AESu!)+W@}W6vPGdPLYL^qswyyJzltD@%wqT*x zt*4Q4+Wh^|7&kjAAx^4&LAg|NRIjYS6S3?D+pTC%r3ntoe#{dJ?@*a^v2Kf%`zIc^ zRJ~BYIff|Q!A=>Rg4ND#Hm!JEhTMn`by0dx=l;({F|iidBsi9hnD<4zA%~?_p2%x! zlcUP|o=319)9Bo)1$KV$(-55IT??^)Py&9s64DE^GuG`)zWJRRLv9_IW>;=hbL;2o_|p}ixV__@h$C#sfO@?C+B zB(z}M5sw`;1TV(z3v{t?n3g0~VlgW~JZDo-J%N}= Date: Wed, 17 Jul 2024 23:47:16 +0200 Subject: [PATCH 40/82] feat(files, settings, status): redesign UI to be in line with the rest of Orion - there may be issues when scaling to different aspect ratios: to be tested --- lib/files/details_screen.dart | 553 ++++++++++------------ lib/settings/about_screen.dart | 408 ++++++++-------- lib/status/status_screen.dart | 829 ++++++++++++++++----------------- 3 files changed, 856 insertions(+), 934 deletions(-) diff --git a/lib/files/details_screen.dart b/lib/files/details_screen.dart index b4f7650..fdb9899 100644 --- a/lib/files/details_screen.dart +++ b/lib/files/details_screen.dart @@ -30,11 +30,12 @@ class DetailScreen extends StatefulWidget { final String fileSubdirectory; final String fileLocation; - const DetailScreen( - {super.key, - required this.fileName, - required this.fileSubdirectory, - required this.fileLocation}); + const DetailScreen({ + super.key, + required this.fileName, + required this.fileSubdirectory, + required this.fileLocation, + }); @override DetailScreenState createState() => DetailScreenState(); @@ -48,17 +49,6 @@ class DetailScreenState extends State { final _logger = Logger('DetailScreen'); final ApiService _api = ApiService(); - double leftPadding = 0; - double rightPadding = 0; - - final GlobalKey textKey1 = GlobalKey(); - final GlobalKey textKey2 = GlobalKey(); - final GlobalKey textKey3 = GlobalKey(); - final GlobalKey textKey4 = GlobalKey(); - final GlobalKey textKey5 = GlobalKey(); - final GlobalKey textKey6 = GlobalKey(); - final GlobalKey previewKey = GlobalKey(); - FileStat? fileStat; String fileName = ''; // path.basename(widget.file.path) String layerHeight = ''; // layerHeight @@ -73,8 +63,8 @@ class DetailScreenState extends State { double materialVolumeInMilliliters = 0; // usedMaterial in milliliters late ValueNotifier> thumbnailFutureNotifier; + // ignore: unused_field Future? _initFileDetailsFuture; - double opacity = 0.0; @override void initState() { @@ -140,312 +130,251 @@ class DetailScreenState extends State { @override Widget build(BuildContext context) { - return FutureBuilder( - future: _initFileDetailsFuture, - builder: (BuildContext context, AsyncSnapshot snapshot) { - if (snapshot.connectionState == ConnectionState.waiting) { - return const Scaffold( - body: Center( - child: CircularProgressIndicator(), - ), - ); - } else if (snapshot.hasError) { - return Scaffold( - body: Center( - child: Text('Error: ${snapshot.error}'), - ), - ); - } else { - WidgetsBinding.instance.addPostFrameCallback((_) { - final keys = [ - textKey1, - textKey2, - textKey3, - textKey4, - textKey5, - textKey6 - ]; - double maxWidth = 0; - - for (var key in keys) { - final width = key.currentContext?.size?.width ?? 0; - if (width > maxWidth) { - maxWidth = width; - } - } - - final previewWidth = previewKey.currentContext?.size?.width ?? 0; - - final screenWidth = MediaQuery.of(context).size.width; - leftPadding = (screenWidth - maxWidth - previewWidth) / 3; - if (leftPadding < 0) leftPadding = 0; - rightPadding = leftPadding; - - setState(() { - opacity = - 1.0; // Set opacity to 1 after sizes have been calculated - }); - }); + bool isLandScape = + MediaQuery.of(context).orientation == Orientation.landscape; + return Scaffold( + appBar: AppBar( + title: const Text('File Details'), + centerTitle: true, + ), + body: Center( + child: LayoutBuilder( + builder: (BuildContext context, BoxConstraints constraints) { + return isLandScape + ? Padding( + padding: + const EdgeInsets.only(left: 16, right: 16, bottom: 20), + child: buildLandscapeLayout(context)) + : Padding( + padding: + const EdgeInsets.only(left: 16, right: 16, bottom: 20), + child: buildPortraitLayout(context)); + }, + ), + ), + ); + } - return Scaffold( - appBar: AppBar( - title: const Text('File Details'), - ), - body: Opacity( - opacity: opacity, - child: LayoutBuilder( - builder: (BuildContext context, BoxConstraints constraints) { - return Stack( - children: [ - Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Align( - alignment: Alignment.centerLeft, - child: Padding( - padding: EdgeInsets.only( - left: leftPadding <= 0 - ? leftPadding - : leftPadding - 10), - child: ConstrainedBox( - constraints: - const BoxConstraints(maxWidth: 300), - child: Card.outlined( - elevation: 1, - child: Padding( - padding: const EdgeInsets.all(10), - child: AutoSizeText.rich( - maxLines: 1, - minFontSize: 16, - TextSpan( - children: [ - TextSpan( - text: fileName.length >= 12 - ? '${fileName.substring(0, 12)}...' - : fileName, - style: TextStyle( - fontSize: 24, - fontWeight: FontWeight.bold, - color: Theme.of(context) - .colorScheme - .primary), - ), - TextSpan( - text: ' - ', - style: TextStyle( - fontSize: 24, - color: Theme.of(context) - .colorScheme - .primary), - ), - TextSpan( - text: fileSize, - style: TextStyle( - fontSize: 24, - color: Theme.of(context) - .colorScheme - .primary), - ), - ], - ), - ), - ), - ), - ), - ), - ), - const SizedBox(height: 15), - Align( - alignment: Alignment.centerLeft, - child: Padding( - padding: EdgeInsets.only(left: leftPadding), - child: FittedBox( - child: Text( - 'Layer Height: $layerHeight', - key: textKey2, - style: const TextStyle(fontSize: 20), - ), - ), - ), - ), - const SizedBox(height: 15), - Align( - alignment: Alignment.centerLeft, - child: Padding( - padding: EdgeInsets.only(left: leftPadding), - child: FittedBox( - child: Text( - 'Material: ${materialName.split('@0.')[0]}', - key: textKey3, - style: const TextStyle(fontSize: 20), - ), - ), - ), - ), - const SizedBox(height: 15), - Align( - alignment: Alignment.centerLeft, - child: Padding( - padding: EdgeInsets.only(left: leftPadding), - child: FittedBox( - child: Text( - 'Print Time: $printTime', - key: textKey4, - style: const TextStyle(fontSize: 20), - ), - ), - ), - ), - const SizedBox(height: 15), - Align( - alignment: Alignment.centerLeft, - child: Padding( - padding: EdgeInsets.only(left: leftPadding), - child: FittedBox( - child: Text( - 'Material Usage: $materialVolume', - key: textKey5, - style: const TextStyle(fontSize: 20), - ), - ), - ), - ), - ], - ), - Align( - alignment: Alignment.centerRight, - child: Padding( - padding: EdgeInsets.only(right: rightPadding), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - thumbnailPath.isNotEmpty - ? Card( - key: previewKey, - child: Padding( - padding: const EdgeInsets.all(4.5), - child: ClipRRect( - borderRadius: - BorderRadius.circular(7.75), - child: Image.file( - File(thumbnailPath), - width: 220, - height: 220, - ), - ), - ), - ) - : Card( - child: Padding( - padding: const EdgeInsets.all(4.5), - child: ClipRRect( - borderRadius: - BorderRadius.circular(7.75), - child: const Image( - image: AssetImage( - 'assets/images/placeholder.png'), - width: 220, - height: 220, - ), - ), - ), - ), - ], - ), - ), - ), - ], - ); - }, - ), - ), - bottomNavigationBar: Opacity( - opacity: opacity, - child: Padding( - padding: EdgeInsets.only( - left: (leftPadding - 10) < 0 ? 0 : leftPadding - 10, - right: (rightPadding - 10) < 0 ? 0 : rightPadding - 10, - bottom: 40, - top: 20, - ), - child: Row( + Widget buildPortraitLayout(BuildContext context) { + return Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + buildNameCard(fileName), + const SizedBox(height: 16), + Expanded( + child: Column( children: [ - ElevatedButton( - onPressed: () { - String subdirectory = widget.fileSubdirectory; - try { - _api.deleteFile(widget.fileLocation, - path.join(subdirectory, widget.fileName)); - } catch (e) { - _logger.severe('Failed to delete file', e); - } - Navigator.pop(context); - }, - style: ElevatedButton.styleFrom( - minimumSize: Size( - 120, // Subtract the padding on both sides - Theme.of(context).appBarTheme.toolbarHeight as double, - ), - ), - child: const Text( - 'Delete', - style: TextStyle(fontSize: 20), - ), - ), - const SizedBox(width: 20), - Expanded( - child: ElevatedButton( - onPressed: () { - try { - String subdirectory = widget.fileSubdirectory; - _api.startPrint(widget.fileLocation, - path.join(subdirectory, widget.fileName)); - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => const StatusScreen( - newPrint: true, - ), - )); - } catch (e) { - _logger.severe('Failed to start print', e); - } - }, - style: ElevatedButton.styleFrom( - minimumSize: Size( - 0, // Subtract the padding on both sides - Theme.of(context).appBarTheme.toolbarHeight - as double, - ), + buildThumbnailView(context), + const Spacer(), + Row( + children: [ + Expanded( + child: buildInfoCard('Layer Height', layerHeight), ), - child: const Text( - 'Print', - style: TextStyle(fontSize: 24), + Expanded( + child: buildInfoCard('Material & Volume', + '$materialName - $materialVolume'), ), - ), + ], ), - const SizedBox(width: 20), - ElevatedButton( - onPressed: null, - // TODO: Add edit logic here - style: ElevatedButton.styleFrom( - minimumSize: Size( - 120, // Subtract the padding on both sides - Theme.of(context).appBarTheme.toolbarHeight as double, - ), + const SizedBox(height: 5), + Row(children: [ + Expanded( + child: buildInfoCard('Print Time', printTime), ), - child: const Text( - 'Edit', - style: TextStyle(fontSize: 20), + Expanded( + child: buildInfoCard('File Size', fileSize), ), - ), + ]), + const SizedBox(height: 5), + buildInfoCard('Modified Date', modifiedDate), + const Spacer(), + buildPrintButtons(), ], ), ), + ], + ), + ) + ], + ); + } + + Widget buildLandscapeLayout(BuildContext context) { + return Column( + children: [ + Expanded( + child: Row( + children: [ + Expanded( + flex: 1, + child: ListView( + children: [ + buildNameCard(fileName), + buildInfoCard('Layer Height', layerHeight), + buildInfoCard( + 'Material & Volume', '$materialName - $materialVolume'), + buildInfoCard('Print Time', printTime), + buildInfoCard('Modified Date', modifiedDate), + buildInfoCard('File Size', fileSize), + ], + ), + ), + const SizedBox(width: 16.0), + Flexible( + flex: 0, + child: buildThumbnailView(context), + ), + ], + ), + ), + const SizedBox(height: 20), + Padding( + padding: const EdgeInsets.only(left: 5.0, right: 5.0), + child: buildPrintButtons(), + ), + ], + ); + } + + Widget buildInfoCard(String title, String subtitle) { + return Card.outlined( + elevation: 1.0, + child: ListTile( + title: Text(title), + subtitle: Text(subtitle), + ), + ); + } + + Widget buildNameCard(String title) { + return Card.outlined( + elevation: 1.0, + child: ListTile( + title: AutoSizeText.rich( + maxLines: 1, + minFontSize: 16, + TextSpan( + children: [ + TextSpan( + text: fileName.length >= 12 + ? '${fileName.substring(0, 12)}...' + : fileName, + style: TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + color: Theme.of(context).colorScheme.primary), + ), + ], + ), + ), + ), + ); + } + + Widget buildThumbnailView(BuildContext context) { + return Center( + child: Card.outlined( + elevation: 1.0, + child: Padding( + padding: const EdgeInsets.all(4.5), + child: ClipRRect( + borderRadius: BorderRadius.circular(7.75), + child: false //thumbnailPath.isNotEmpty + ? Image.file(File(thumbnailPath)) + : Image.asset( + 'assets/images/thumbnail800x480.png', + ), + ), + ), + ), + ); + } + + Widget buildPrintButtons() { + return Row( + children: [ + ElevatedButton( + onPressed: () { + String subdirectory = widget.fileSubdirectory; + try { + _api.deleteFile(widget.fileLocation, + path.join(subdirectory, widget.fileName)); + _logger.info('File deleted successfully'); + } catch (e) { + _logger.severe('Failed to delete file', e); + } + Navigator.pop(context); + }, + style: ElevatedButton.styleFrom( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(15), + ), + minimumSize: Size( + 0, + Theme.of(context).appBarTheme.toolbarHeight as double, + ), + ), + child: const Text( + 'Delete', + style: TextStyle(fontSize: 20), + ), + ), + const SizedBox(width: 20), + Expanded( + child: ElevatedButton( + onPressed: () { + try { + String subdirectory = widget.fileSubdirectory; + _api.startPrint(widget.fileLocation, + path.join(subdirectory, widget.fileName)); + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const StatusScreen( + newPrint: true, + ), + )); + } catch (e) { + _logger.severe('Failed to start print', e); + } + }, + style: ElevatedButton.styleFrom( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(15), + ), + minimumSize: Size( + 0, // Subtract the padding on both sides + Theme.of(context).appBarTheme.toolbarHeight as double, + ), + ), + child: const Text( + 'Print', + style: TextStyle(fontSize: 24), + ), + ), + ), + const SizedBox(width: 20), + ElevatedButton( + onPressed: null, + style: ElevatedButton.styleFrom( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(15), + ), + minimumSize: Size( + 120, // Subtract the padding on both sides + Theme.of(context).appBarTheme.toolbarHeight as double, ), - ); - } - }, + ), + child: const Text( + 'Edit', + style: TextStyle(fontSize: 20), + ), + ), + ], ); } } diff --git a/lib/settings/about_screen.dart b/lib/settings/about_screen.dart index 4130704..35b50d5 100644 --- a/lib/settings/about_screen.dart +++ b/lib/settings/about_screen.dart @@ -16,75 +16,224 @@ * along with this program. If not, see . */ -// ignore_for_file: avoid_print -// ignore_for_file: library_private_types_in_public_api - import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/scheduler.dart'; +import 'package:logging/logging.dart'; import 'package:orion/pubspec.dart'; import 'package:orion/themes/themes.dart'; import 'package:orion/util/orion_config.dart'; import 'package:qr_flutter/qr_flutter.dart'; import 'package:toastification/toastification.dart'; +import 'dart:io'; + +Logger _logger = Logger('AboutScreen'); + +Future executeCommand(String command, List arguments) async { + final result = await Process.run(command, arguments); + if (result.exitCode == 0) { + return result.stdout.trim(); + } else { + throw Exception( + 'Failed to execute command: $command ${arguments.join(" ")}\nError: ${result.stderr}'); + } +} + +Future getRaspberryPiModel() async { + try { + final model = await executeCommand('cat', ['/proc/device-tree/model']); + _logger.info('Raspberry Pi model: $model'); + return model.trim(); + } catch (e) { + _logger.warning('Error getting Raspberry Pi model: $e'); + return 'Unknown Model'; + } +} Future getVersionNumber() async { - return Pubspec.version; + return 'Orion ${Pubspec.version}' ' - Odyssey 1.0.0'; } class AboutScreen extends StatefulWidget { const AboutScreen({super.key}); @override - _AboutScreenState createState() => _AboutScreenState(); + AboutScreenState createState() => AboutScreenState(); } -class _AboutScreenState extends State { - double leftPadding = 0; - double rightPadding = 0; +class AboutScreenState extends State { int qrTapCount = 0; - bool developerMode = false; - Color? _standardColor = Colors.white.withOpacity(0.0); - Toastification toastification = Toastification(); - final GlobalKey textKey1 = GlobalKey(); - final GlobalKey textKey2 = GlobalKey(); - final GlobalKey textKey3 = GlobalKey(); - final GlobalKey textKey4 = GlobalKey(); - final GlobalKey textKey5 = GlobalKey(); - final GlobalKey textKey6 = GlobalKey(); - @override Widget build(BuildContext context) { - SchedulerBinding.instance.addPostFrameCallback((_) { - final keys = [textKey1, textKey2, textKey3, textKey4, textKey5, textKey6]; - double maxWidth = 0; + bool isLandscape = + MediaQuery.of(context).orientation == Orientation.landscape; + return Scaffold( + body: Center( + child: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.only(left: 16.0, right: 16.0), + child: isLandscape + ? buildLandscapeLayout(context) + : buildPortraitLayout(context), + ), + ), + ), + ); + } - for (var key in keys) { - final width = key.currentContext?.size?.width ?? 0; - if (width > maxWidth) { - maxWidth = width; - } - } + Widget buildPortraitLayout(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + buildNameCard('Prometheus mSLA'), + buildInfoCard( + 'Serial Number', kDebugMode ? 'DBG-0001-001' : 'BLEEDING-EDGE'), + buildVersionCard(), + buildHardwareCard(), + const SizedBox(height: 16), + buildQrView(context), + ], + ); + } - final screenWidth = MediaQuery.of(context).size.width; - setState(() { - leftPadding = (screenWidth - maxWidth - 220) / 3; - rightPadding = leftPadding; - _standardColor = Theme.of(context).textTheme.bodyLarge!.color; - }); - }); + Widget buildLandscapeLayout(BuildContext context) { + return Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + buildNameCard('Prometheus mSLA'), + buildInfoCard('Serial Number', + kDebugMode ? 'DBG-0001-001' : 'BLEEDING-EDGE'), + buildVersionCard(), + buildHardwareCard(), + ], + ), + ), + const SizedBox(width: 16), + buildQrView(context), + ], + ); + } + + Widget buildInfoCard(String title, String subtitle) { + return Card.outlined( + elevation: 1.0, + child: ListTile( + title: Text(title), + subtitle: Text(subtitle), + ), + ); + } + + Widget buildVersionCard() { + return Card.outlined( + elevation: 1.0, + child: ListTile( + title: const Text('UI & API Version'), + subtitle: FutureBuilder( + future: getVersionNumber(), + builder: (BuildContext context, AsyncSnapshot snapshot) { + return Text(snapshot.data ?? 'N/A'); + }, + ), + ), + ); + } + + Widget buildHardwareCard() { + return Card.outlined( + elevation: 1.0, + child: ListTile( + title: const Text('Hardware (Local)'), + subtitle: FutureBuilder( + future: getRaspberryPiModel(), + builder: (BuildContext context, AsyncSnapshot snapshot) { + // Sanitize the model string to remove non-printable characters before logging and displaying + final sanitizedModel = + snapshot.data?.replaceAll(RegExp(r'[^\x20-\x7E]'), '') ?? 'N/A'; + _logger.info('Raspberry Pi model: $sanitizedModel'); + return Text(sanitizedModel); + }, + ), + ), + ); + } + + Widget buildNameCard(String title) { + return Card.outlined( + elevation: 1.0, + child: ListTile( + title: Text( + title, + style: TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + color: Theme.of(context).colorScheme.primary), + ), + ), + ); + } - void handleQrTap() { - setState(() { - if (OrionConfig().getFlag('developerMode', category: 'advanced')) { + Widget buildQrView(BuildContext context) { + return Center( + child: GestureDetector( + onTap: handleQrTap, + child: ClipRRect( + borderRadius: BorderRadius.circular(12), + child: Card.outlined( + elevation: 1.0, + child: Padding( + padding: const EdgeInsets.all(16.0), + child: QrImageView( + data: 'https://github.com/TheContrappostoShop/Orion', + version: QrVersions.auto, + size: 250, + eyeStyle: QrEyeStyle( + color: Theme.of(context).colorScheme.onSurface, + ), + dataModuleStyle: QrDataModuleStyle( + color: Theme.of(context).colorScheme.onSurface, + dataModuleShape: QrDataModuleShape.circle, + ), + ), + ), + ), + ), + ), + ); + } + + void handleQrTap() { + setState(() { + if (OrionConfig().getFlag('developerMode', category: 'advanced')) { + toastification.show( + context: context, + type: ToastificationType.success, + style: ToastificationStyle.fillColored, + autoCloseDuration: const Duration(seconds: 2), + title: const Text('You are already a developer', + style: TextStyle(fontSize: 18)), + alignment: Alignment.topCenter, + primaryColor: Colors.green, + backgroundColor: + Theme.of(context).colorScheme.surface.withBrightness(1.35), + foregroundColor: Theme.of(context).colorScheme.onSurface, + ); + } else { + qrTapCount++; + if (qrTapCount >= 5) { + OrionConfig().setFlag('developerMode', true, category: 'advanced'); toastification.show( context: context, type: ToastificationType.success, style: ToastificationStyle.fillColored, autoCloseDuration: const Duration(seconds: 2), - title: const Text('You are already a developer'), + title: const Text( + 'Developer Mode Activated: You are now a developer!', + style: TextStyle(fontSize: 18)), alignment: Alignment.topCenter, primaryColor: Colors.green, backgroundColor: @@ -92,170 +241,23 @@ class _AboutScreenState extends State { foregroundColor: Theme.of(context).colorScheme.onSurface, ); } else { - qrTapCount++; - if (qrTapCount >= 5) { - OrionConfig().setFlag('developerMode', true, category: 'advanced'); - toastification.show( - context: context, - type: ToastificationType.success, - style: ToastificationStyle.fillColored, - autoCloseDuration: const Duration(seconds: 2), - title: const Text( - 'Developer Mode Activated: You are now a developer!'), - alignment: Alignment.topCenter, - primaryColor: Colors.green, - backgroundColor: - Theme.of(context).colorScheme.surface.withBrightness(1.35), - foregroundColor: Theme.of(context).colorScheme.onSurface, - ); - } else { - toastification.show( - context: context, - type: ToastificationType.info, - style: ToastificationStyle.flatColored, - autoCloseDuration: const Duration(seconds: 2), - title: Text( - 'You are ${5 - qrTapCount} ${5 - qrTapCount == 1 ? 'tap' : 'taps'} away from becoming a developer'), - alignment: Alignment.topCenter, - primaryColor: Theme.of(context).colorScheme.primary, - backgroundColor: - Theme.of(context).colorScheme.surface.withBrightness(1.35), - foregroundColor: Theme.of(context).colorScheme.onSurface, - showProgressBar: false, - ); - } + toastification.show( + context: context, + type: ToastificationType.info, + style: ToastificationStyle.flatColored, + autoCloseDuration: const Duration(seconds: 2), + title: Text( + 'You are ${5 - qrTapCount} ${5 - qrTapCount == 1 ? 'tap' : 'taps'} away from becoming a developer', + style: const TextStyle(fontSize: 18)), + alignment: Alignment.topCenter, + primaryColor: Theme.of(context).colorScheme.primary, + backgroundColor: + Theme.of(context).colorScheme.surface.withBrightness(1.35), + foregroundColor: Theme.of(context).colorScheme.onSurface, + showProgressBar: false, + ); } - }); - } - - const String title = kDebugMode ? 'Debug Machine' : 'Prometheus mSLA'; - const String serialNumber = - kDebugMode ? 'S/N: DBG-0001-001' : 'No S/N Available'; - const String apiVersion = kDebugMode ? 'Odyssey: Simulated' : 'Odyssey: ?'; - const String boardType = - kDebugMode ? 'Hardware: Debugger' : 'Hardware: Apollo 3.5.2'; - - return Scaffold( - body: Stack( - children: [ - Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Align( - alignment: Alignment.centerLeft, - child: Padding( - padding: EdgeInsets.only(left: leftPadding), - child: FittedBox( - child: Text( - title, - key: textKey1, - style: TextStyle( - fontSize: 24, - fontWeight: FontWeight.bold, - color: _standardColor), - ), - ), - ), - ), - const SizedBox(height: 20), - Align( - alignment: Alignment.centerLeft, - child: Padding( - padding: EdgeInsets.only(left: leftPadding), - child: FittedBox( - child: Text( - serialNumber, - key: textKey2, - style: TextStyle(fontSize: 20, color: _standardColor), - ), - ), - ), - ), - const SizedBox(height: 15), - Align( - alignment: Alignment.centerLeft, - child: Padding( - padding: EdgeInsets.only(left: leftPadding), - child: FittedBox( - child: FutureBuilder( - future: getVersionNumber(), - builder: (BuildContext context, - AsyncSnapshot snapshot) { - if (snapshot.hasData) { - return Text( - 'Orion: ${snapshot.data}', - key: textKey3, - style: - TextStyle(fontSize: 20, color: _standardColor), - ); - } else { - return Text( - 'Orion: N/A', - key: textKey3, - style: - TextStyle(fontSize: 20, color: _standardColor), - ); - } - }, - ), - ), - ), - ), - const SizedBox(height: 15), - Align( - alignment: Alignment.centerLeft, - child: Padding( - padding: EdgeInsets.only(left: leftPadding), - child: FittedBox( - child: Text( - apiVersion, - key: textKey4, - style: TextStyle(fontSize: 20, color: _standardColor), - ), - ), - ), - ), - const SizedBox(height: 15), - Align( - alignment: Alignment.centerLeft, - child: Padding( - padding: EdgeInsets.only(left: leftPadding), - child: FittedBox( - child: Text( - boardType, - key: textKey5, - style: TextStyle(fontSize: 20, color: _standardColor), - ), - ), - ), - ), - ], - ), - GestureDetector( - onTap: handleQrTap, - child: Align( - alignment: Alignment.centerRight, - child: Padding( - padding: EdgeInsets.only(right: rightPadding), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - QrImageView( - data: 'https://github.com/TheContrappostoShop/Orion', - version: QrVersions.auto, - size: 220, - eyeStyle: QrEyeStyle(color: _standardColor), - dataModuleStyle: QrDataModuleStyle( - color: _standardColor, - dataModuleShape: QrDataModuleShape.circle), - ), - ], - ), - ), - ), - ), - ], - ), - ); + } + }); } } diff --git a/lib/status/status_screen.dart b/lib/status/status_screen.dart index bc8f7b1..acb9893 100644 --- a/lib/status/status_screen.dart +++ b/lib/status/status_screen.dart @@ -41,15 +41,14 @@ class StatusScreenState extends State final _logger = Logger('StatusScreen'); final ApiService _api = ApiService(); - double leftPadding = 0; - double rightPadding = 0; + String fileName = ''; - final GlobalKey textKey1 = GlobalKey(); - final GlobalKey textKey2 = GlobalKey(); - final GlobalKey textKey3 = GlobalKey(); - final GlobalKey textKey4 = GlobalKey(); - final GlobalKey textKey5 = GlobalKey(); - final GlobalKey previewKey = GlobalKey(); + int totalSeconds = 0; + Duration duration = const Duration(); + String twoDigits = ''; + String twoDigitHours = ''; + String twoDigitMinutes = ''; + String twoDigitSeconds = ''; final ValueNotifier thumbnailNotifier = ValueNotifier(null); late ValueNotifier newPrintNotifier = ValueNotifier(false); @@ -88,13 +87,11 @@ class StatusScreenState extends State status!['print_data']['file_data'] != null) { String? thumbnailFullPath = status!['print_data']['file_data']['path']; - String? fileName = status!['print_data']['file_data']['name']; + fileName = status!['print_data']['file_data']['name']; String location = status!['print_data']['file_data'] ['location_category'] ?? 'Local'; - if (thumbnailFullPath != null && - fileName != null && - !isThumbnailFetched) { + if (thumbnailFullPath != null && !isThumbnailFetched) { String thumbnailSubdir = '/'; if (thumbnailFullPath.contains('/')) { thumbnailSubdir = thumbnailFullPath.substring( @@ -236,44 +233,17 @@ class StatusScreenState extends State if (status != null && status!['status'] == 'Printing') { newPrintNotifier.value = false; } - WidgetsBinding.instance.addPostFrameCallback((_) { - final keys = [ - textKey1, - textKey2, - textKey3, - textKey4, - textKey5, - ]; - double maxWidth = 0; - for (var key in keys) { - final width = key.currentContext?.size?.width ?? 0; - if (width > maxWidth) { - maxWidth = width; - } - } - - final previewWidth = previewKey.currentContext?.size?.width ?? 0; - - final screenWidth = MediaQuery.of(context).size.width; - leftPadding = (screenWidth - maxWidth - previewWidth) / 3; - if (leftPadding < 0) leftPadding = 0; - rightPadding = leftPadding; - - setState(() { - opacity = - 1.0; // Set opacity to 1 after sizes have been calculated - }); - }); + bool isLandScape = + MediaQuery.of(context).orientation == Orientation.landscape; - int totalSeconds = status!['print_data']['print_time'].toInt(); - Duration duration = Duration(seconds: totalSeconds); + totalSeconds = status!['print_data']['print_time'].toInt(); + duration = Duration(seconds: totalSeconds); + twoDigits(int n) => n.toString().padLeft(2, "0"); - String twoDigits(int n) => n.toString().padLeft(2, "0"); - String twoDigitMinutes = - twoDigits(duration.inMinutes.remainder(60)); - String twoDigitSeconds = - twoDigits(duration.inSeconds.remainder(60)); + twoDigitHours = twoDigits(duration.inHours.remainder(24)); + twoDigitMinutes = twoDigits(duration.inMinutes.remainder(60)); + twoDigitSeconds = twoDigits(duration.inSeconds.remainder(60)); return Scaffold( appBar: AppBar( @@ -306,399 +276,420 @@ class StatusScreenState extends State ), ), ), - body: Opacity( - opacity: opacity, + body: Center( child: LayoutBuilder( builder: (BuildContext context, BoxConstraints constraints) { - return Stack( + return isLandScape + ? Padding( + padding: const EdgeInsets.only( + left: 16, right: 16, bottom: 20), + child: buildLandscapeLayout(context)) + : Padding( + padding: const EdgeInsets.only( + left: 16, right: 16, bottom: 20), + child: buildPortraitLayout(context)); + }, + ), + ), + ); + } + } + }, + ); + } + + Widget buildPortraitLayout(BuildContext context) { + return Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + buildNameCard(fileName), + const SizedBox(height: 16), + Expanded( + child: Column( + children: [ + buildThumbnailView(context), + const Spacer(), + Row( children: [ - Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - if (status!['status'] == 'Printing' || - status!['status'] == 'Idle') ...[ - Align( - alignment: Alignment.centerLeft, - child: Padding( - padding: EdgeInsets.only( - left: leftPadding <= 0 - ? leftPadding - : leftPadding - 10), - child: ConstrainedBox( - constraints: - const BoxConstraints(maxWidth: 300), - child: Card.outlined( - key: textKey1, - elevation: 1, - child: Padding( - padding: const EdgeInsets.all(10), - child: AutoSizeText( - maxLines: 2, - minFontSize: 16, - '${status!['print_data']['file_data']['name']}', - style: TextStyle( - fontSize: 24, - fontWeight: FontWeight.bold, - color: getStatusColor()), - overflow: TextOverflow.ellipsis, - ), - ), + Expanded( + child: buildInfoCard('Current Z Position', + '${(status!['physical_state']['z'] as double).toStringAsFixed(3)} mm'), + ), + Expanded( + child: buildInfoCard('Print Layers', + '${status!['layer'] == null ? '-' : status!['layer'] + 1 ?? '-'} / ${status!['print_data']['layer_count'] + 1}'), + ), + ], + ), + const SizedBox(height: 5), + buildInfoCard('Estimated Print Time', + '$twoDigitHours:$twoDigitMinutes:$twoDigitSeconds'), + const SizedBox(height: 5), + buildInfoCard('Estimated Volume', + '${(status!['print_data']['used_material'] as double).toStringAsFixed(2)} mL'), + const Spacer(), + buildButtons(), + ], + ), + ), + ], + ), + ) + ], + ); + } + + Widget buildLandscapeLayout(BuildContext context) { + return Column( + children: [ + Expanded( + child: Row( + children: [ + Expanded( + flex: 1, + child: ListView( + children: [ + buildNameCard(fileName), + buildInfoCard('Current Z Position', + '${(status!['physical_state']['z'] as double).toStringAsFixed(3)} mm'), + buildInfoCard('Print Layers', + '${status!['layer'] == null ? '-' : status!['layer'] + 1 ?? '-'} / ${status!['print_data']['layer_count'] + 1}'), + buildInfoCard('Estimated Print Time', + '$twoDigitHours:$twoDigitMinutes:$twoDigitSeconds'), + buildInfoCard('Estimated Volume', + '${(status!['print_data']['used_material'] as double).toStringAsFixed(2)} mL') + ], + ), + ), + const SizedBox(width: 16.0), + Flexible( + flex: 0, + child: buildThumbnailView(context), + ), + ], + ), + ), + const SizedBox(height: 20), + Padding( + padding: const EdgeInsets.only(left: 5, right: 5), + child: buildButtons(), + ), + ], + ); + } + + Widget buildInfoCard(String title, String subtitle) { + return Card.outlined( + elevation: 1.0, + child: ListTile( + title: Text(title), + subtitle: Text(subtitle), + ), + ); + } + + Widget buildNameCard(String title) { + return Card.outlined( + elevation: 1.0, + child: ListTile( + title: AutoSizeText.rich( + maxLines: 1, + minFontSize: 16, + TextSpan( + children: [ + TextSpan( + text: fileName.length >= 12 + ? '${fileName.substring(0, 12)}...' + : fileName, + style: TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + color: getStatusColor(), + ), + ), + ], + ), + ), + ), + ); + } + + Widget buildThumbnailView(BuildContext context) { + return ValueListenableBuilder( + valueListenable: thumbnailNotifier, + builder: (BuildContext context, String? thumbnail, Widget? child) { + double progress = 0.0; + if (status!['layer'] != null && + status!['print_data']['layer_count'] != null) { + progress = status!['layer'] / status!['print_data']['layer_count']; + } + return Center( + child: Stack( + children: [ + Card.outlined( + elevation: 1.0, + child: Padding( + padding: const EdgeInsets.all(4.5), + child: ClipRRect( + borderRadius: BorderRadius.circular(7.75), + child: Stack( + children: [ + // Grayscale image + ColorFiltered( + colorFilter: const ColorFilter.matrix([ + 0.2126, 0.7152, 0.0722, 0, 0, // + 0.2126, 0.7152, 0.0722, 0, 0, + 0.2126, 0.7152, 0.0722, 0, 0, + 0, 0, 0, 1, 0, + ]), + child: + false //thumbnail != null && thumbnail.isNotEmpty + ? Image.file( + File(thumbnail!), + fit: BoxFit.cover, + ) + : Image.asset( + 'assets/images/thumbnail800x480.png', + fit: BoxFit.cover, ), - ), - ), - ), - const SizedBox(height: 15), - Align( - alignment: Alignment.centerLeft, - child: Padding( - padding: EdgeInsets.only(left: leftPadding), - child: Text( - 'Z Position: ${(status!['physical_state']['z'] as double).toStringAsFixed(3)} mm', - style: const TextStyle(fontSize: 20), - key: textKey2, - ), - ), - ), - const SizedBox(height: 15), - Align( - alignment: Alignment.centerLeft, - child: Padding( - padding: EdgeInsets.only(left: leftPadding), - child: Text( - 'Layer: ${status!['layer'] == null ? '-' : status!['layer'] + 1 ?? '-'} / ${status!['print_data']['layer_count'] + 1}', - style: const TextStyle(fontSize: 20), - key: textKey3, - ), - ), - ), - const SizedBox(height: 15), - Align( - alignment: Alignment.centerLeft, - child: Padding( - padding: EdgeInsets.only(left: leftPadding), - child: Text( - 'Printing Time: ${twoDigits(duration.inHours)}:$twoDigitMinutes:$twoDigitSeconds', - style: const TextStyle(fontSize: 20), - key: textKey4, - ), - ), - ), - const SizedBox(height: 15), - Align( - alignment: Alignment.centerLeft, - child: Padding( - padding: EdgeInsets.only(left: leftPadding), - child: Text( - 'Material: ${(status!['print_data']['used_material'] as double).toStringAsFixed(2)} mL', - style: const TextStyle(fontSize: 20), - key: textKey5, - ), - ), - ), - ], - ], ), - Align( - alignment: Alignment.centerRight, - child: Padding( - padding: EdgeInsets.only(right: rightPadding), - child: ValueListenableBuilder( - valueListenable: thumbnailNotifier, - builder: (BuildContext context, String? thumbnail, - Widget? child) { - double progress = 0.0; - if (status!['layer'] != null && - status!['print_data']['layer_count'] != - null) { - progress = status!['layer'] / - status!['print_data']['layer_count']; - } - return thumbnail != null - ? Stack( - children: [ - Card( - key: previewKey, - child: Padding( - padding: - const EdgeInsets.all(4.5), - child: ClipRRect( - borderRadius: - BorderRadius.circular(7.75), - child: Image.file( - File(thumbnail), - width: 220, - height: 220, - color: Colors.black, - colorBlendMode: - BlendMode.saturation, - ), - ), - ), - ), - Card( - //key: previewKey, - child: Padding( - padding: - const EdgeInsets.all(4.5), - child: ClipRRect( - borderRadius: - BorderRadius.circular(7.75), - child: Stack( - children: [ - Image.file( - File(thumbnail), - width: 220, - height: 220, - ), - Container( - width: 220, - height: 220, - decoration: BoxDecoration( - gradient: - LinearGradient( - begin: Alignment - .bottomCenter, - end: Alignment - .topCenter, - colors: [ - Colors.transparent, - Colors.black - .withOpacity( - 0.65), - ], - stops: [ - (progress), - (progress) - ], - ), - ), - ), - Positioned( - top: 0, - bottom: 0, - left: 0, - right: 0, - child: Center( - child: StatusCard( - isCanceling: - isCanceling, - isPausing: isPausing, - progress: progress, - statusColor: - getStatusColor(), - status: status!, - ), - ), - ), - ], - ), - ), - ), - ), - ], - ) - : Card( - child: Padding( - padding: const EdgeInsets.all(4.5), - child: ClipRRect( - borderRadius: - BorderRadius.circular(7.75), - child: const Image( - image: AssetImage( - 'assets/images/placeholder.png'), - width: 220, - height: 220, - ), + Positioned.fill( + child: Container( + color: Colors.black.withOpacity(0.2), + ), + ), + // Colored image revealed based on progress + Positioned.fill( + child: Align( + alignment: Alignment.bottomCenter, + child: ClipRect( + child: Align( + alignment: Alignment.bottomCenter, + heightFactor: progress, + child: + false //thumbnail != null && thumbnail.isNotEmpty + ? Image.file( + File(thumbnail!), + fit: BoxFit.cover, + ) + : Image.asset( + 'assets/images/thumbnail800x480.png', + fit: BoxFit.cover, ), - ), - ); - }, + ), ), ), ), ], - ); - }, + ), + ), + ), + ), + Positioned.fill( + right: 15, + child: Center( + child: StatusCard( + isCanceling: isCanceling, + isPausing: isPausing, + progress: progress, + statusColor: getStatusColor(), + status: status!, + ), ), ), - bottomNavigationBar: Opacity( - opacity: opacity, + Positioned( + top: 0, + bottom: 0, + right: 0, child: Padding( - padding: EdgeInsets.only( - left: (leftPadding - 10) < 0 ? 0 : leftPadding - 10, - right: (rightPadding - 10) < 0 ? 0 : rightPadding - 10, - bottom: 40, - top: 20), - child: Row( - children: [ - Expanded( - child: ElevatedButton( - onPressed: isCanceling == true && - status!['layer'] != null - ? null - : status!['layer'] == null || - status!['status'] == 'Idle' - ? null - : () { - showDialog( - context: context, - builder: (BuildContext context) { - return Dialog( - child: SizedBox( - width: MediaQuery.of(context) - .size - .width * - 0.5, // 80% of screen width - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - const SizedBox(height: 10), - const Padding( - padding: - EdgeInsets.all(8.0), - child: Text( - 'Options', - style: TextStyle( - fontSize: 24, - fontWeight: - FontWeight.bold), - ), - ), - const SizedBox(height: 10), - Padding( - padding: - const EdgeInsets.only( - left: 20, - right: 20), - child: SizedBox( - height: 65, - width: double.infinity, - child: ElevatedButton( - onPressed: () { - Navigator.pop( - context); - Navigator.push( - context, - MaterialPageRoute( - builder: - (context) => - const SettingsScreen()), - ); - }, - child: const Text( - 'Settings', - style: TextStyle( - fontSize: 24), - ), - ), - ), - ), - const SizedBox( - height: - 20), // Add some spacing between the buttons - Padding( - padding: - const EdgeInsets.only( - left: 20, - right: 20), - child: SizedBox( - height: 65, - width: double.infinity, - child: ElevatedButton( - onPressed: () { - Navigator.pop( - context); - _api.cancelPrint(); - setState(() { - isCanceling = true; - }); - }, - child: const Text( - 'Cancel Print', - style: TextStyle( - fontSize: 24), - ), - ), - ), - ), - const SizedBox(height: 20), - ], - ), - ), - ); - }, - ); - }, - style: ElevatedButton.styleFrom( - minimumSize: Size( - 0, // Subtract the padding on both sides - Theme.of(context).appBarTheme.toolbarHeight - as double, - ), - ), - child: const Text( - 'Options', - style: TextStyle(fontSize: 24), - ), - ), + padding: const EdgeInsets.all(2), + child: Card( + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.only( + topRight: Radius.circular(9.75), + bottomRight: Radius.circular(9.75), ), - const SizedBox(width: 20), - Expanded( - child: ElevatedButton( - onPressed: isCanceling == true && - status!['layer'] != null - ? null - : isPausing == true && status!['paused'] == false - ? null - : status!['layer'] == null - ? () { - Navigator.popUntil(context, - ModalRoute.withName('/')); - } - : status!['status'] == 'Idle' - ? () { - Navigator.pop(context); - } - : () { - if (status!['paused'] == true) { - _api.resumePrint(); - setState(() { - isPausing = false; - }); - } else { - _api.pausePrint(); - setState(() { - isPausing = true; - }); - } - }, - style: ElevatedButton.styleFrom( - minimumSize: Size( - 0, // Subtract the padding on both sides - Theme.of(context).appBarTheme.toolbarHeight - as double, - ), - ), - child: Text( - status!['layer'] == null || - status!['status'] == 'Idle' - ? 'Return to Home' - : status!['paused'] == true - ? 'Resume' - : 'Pause', - style: const TextStyle(fontSize: 24), + ), + child: Padding( + padding: const EdgeInsets.all(2.5), + child: ClipRRect( + borderRadius: const BorderRadius.only( + topRight: Radius.circular(7.75), + bottomRight: Radius.circular(7.75), + ), + child: RotatedBox( + quarterTurns: 3, + child: LinearProgressIndicator( + minHeight: 30, + color: getStatusColor(), + value: progress, ), ), ), - ], + ), ), ), ), - ); - } - } + ], + ), + ); }, ); } + + Widget buildButtons() { + return Row( + children: [ + Expanded( + child: ElevatedButton( + onPressed: isCanceling == true && status!['layer'] != null + ? null + : status!['layer'] == null || status!['status'] == 'Idle' + ? null + : () { + showDialog( + context: context, + builder: (BuildContext context) { + return Dialog( + child: SizedBox( + width: MediaQuery.of(context).size.width * + 0.5, // 80% of screen width + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const SizedBox(height: 10), + const Padding( + padding: EdgeInsets.all(8.0), + child: Text( + 'Options', + style: TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold), + ), + ), + const SizedBox(height: 10), + Padding( + padding: const EdgeInsets.only( + left: 20, right: 20), + child: SizedBox( + height: 65, + width: double.infinity, + child: ElevatedButton( + onPressed: () { + Navigator.pop(context); + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => + const SettingsScreen()), + ); + }, + child: const Text( + 'Settings', + style: TextStyle(fontSize: 24), + ), + ), + ), + ), + const SizedBox( + height: + 20), // Add some spacing between the buttons + Padding( + padding: const EdgeInsets.only( + left: 20, right: 20), + child: SizedBox( + height: 65, + width: double.infinity, + child: ElevatedButton( + onPressed: () { + Navigator.pop(context); + _api.cancelPrint(); + setState(() { + isCanceling = true; + }); + }, + child: const Text( + 'Cancel Print', + style: TextStyle(fontSize: 24), + ), + ), + ), + ), + const SizedBox(height: 20), + ], + ), + ), + ); + }, + ); + }, + style: ElevatedButton.styleFrom( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(15), + ), + minimumSize: Size( + 0, // Subtract the padding on both sides + Theme.of(context).appBarTheme.toolbarHeight as double, + ), + ), + child: const Text( + 'Options', + style: TextStyle(fontSize: 24), + ), + ), + ), + const SizedBox(width: 20), + Expanded( + child: ElevatedButton( + onPressed: isCanceling == true && status!['layer'] != null + ? null + : isPausing == true && status!['paused'] == false + ? null + : status!['layer'] == null + ? () { + Navigator.popUntil( + context, ModalRoute.withName('/')); + } + : status!['status'] == 'Idle' + ? () { + Navigator.pop(context); + } + : () { + if (status!['paused'] == true) { + _api.resumePrint(); + setState(() { + isPausing = false; + }); + } else { + _api.pausePrint(); + setState(() { + isPausing = true; + }); + } + }, + style: ElevatedButton.styleFrom( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(15), + ), + minimumSize: Size( + 0, // Subtract the padding on both sides + Theme.of(context).appBarTheme.toolbarHeight as double, + ), + ), + child: Text( + status!['layer'] == null || status!['status'] == 'Idle' + ? 'Return to Home' + : status!['paused'] == true + ? 'Resume' + : 'Pause', + style: const TextStyle(fontSize: 24), + ), + ), + ), + ], + ); + } } From e9dbfb97e996107d08058f40061a222bd36d8ba7 Mon Sep 17 00:00:00 2001 From: Paul_GD Date: Tue, 23 Jul 2024 02:14:01 +0200 Subject: [PATCH 41/82] feat(api_services): update getFileThumbnail to include size parameter The code changes in `api_services.dart` modify the `getFileThumbnail` method to include a new `size` parameter. This allows the caller to specify the desired size of the thumbnail. The method now logs the `location`, `filePath`, and `size` parameters before making the API request. --- lib/api_services/api_services.dart | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/lib/api_services/api_services.dart b/lib/api_services/api_services.dart index b8ff37c..1699d28 100644 --- a/lib/api_services/api_services.dart +++ b/lib/api_services/api_services.dart @@ -166,9 +166,15 @@ class ApiService { // Get file thumbnail // Takes 2 parameters : location [string] and filePath [String] - Future getFileThumbnail(String location, String filePath) async { - _logger.info("getFileThumbnail location=$location filePath=$filePath"); - final queryParams = {"location": location, "file_path": filePath}; + Future getFileThumbnail( + String location, String filePath, String size) async { + _logger.info( + "getFileThumbnail location=$location filePath=$filePath size=$size"); + final queryParams = { + "location": location, + "file_path": filePath, + "size": size + }; final response = await odysseyGet('/file/thumbnail', queryParams); return response.bodyBytes; From 2dc7fff97d8ae7918b174cad1e2fac22019a2c44 Mon Sep 17 00:00:00 2001 From: Paul_GD Date: Tue, 23 Jul 2024 02:14:51 +0200 Subject: [PATCH 42/82] fix(util): update StatusCard to display progress percentage The code changes in `status_card.dart` update the `StatusCard` widget to display the progress percentage as a text overlay on top of the card. This was accidentally removed in a previous commit. --- lib/util/status_card.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/util/status_card.dart b/lib/util/status_card.dart index 89c1746..93c3ff4 100644 --- a/lib/util/status_card.dart +++ b/lib/util/status_card.dart @@ -73,7 +73,7 @@ class StatusCardState extends State { ? Stack( children: [ Text( - (widget.progress * 100).toStringAsFixed(0), + '${(widget.progress * 100).toStringAsFixed(0)}%', style: TextStyle( fontSize: 75, foreground: Paint() @@ -83,7 +83,7 @@ class StatusCardState extends State { ), ), Text( - (widget.progress * 100).toStringAsFixed(0), + '${(widget.progress * 100).toStringAsFixed(0)}%', style: TextStyle( fontSize: 75, color: Theme.of(context).colorScheme.primary, From 43c0e1888e4c22c682b5bd70590425cd121fa55f Mon Sep 17 00:00:00 2001 From: Paul_GD Date: Tue, 23 Jul 2024 02:15:10 +0200 Subject: [PATCH 43/82] feat(util): add size parameter to extractThumbnail method The code changes in `sl1_thumbnail.dart` add a new optional `size` parameter to the `extractThumbnail` method in the `ThumbnailUtil` class. This allows the caller to specify the desired size of the thumbnail when retrieving the file from the API. The method now accepts the `size` parameter and passes it to the `getFileThumbnail` method in the `ApiService` class. --- lib/util/sl1_thumbnail.dart | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/lib/util/sl1_thumbnail.dart b/lib/util/sl1_thumbnail.dart index edb983c..b3a0ba1 100644 --- a/lib/util/sl1_thumbnail.dart +++ b/lib/util/sl1_thumbnail.dart @@ -26,12 +26,13 @@ class ThumbnailUtil { static final ApiService _api = ApiService(); static Future extractThumbnail( - String location, String subdirectory, String filename) async { + String location, String subdirectory, String filename, + {String size = "Small"}) async { try { String finalLocation = _isDefaultDir(subdirectory) ? filename : [subdirectory, filename].join('/'); - final bytes = await _api.getFileThumbnail(location, finalLocation); + final bytes = await _api.getFileThumbnail(location, finalLocation, size); final tempDir = await getTemporaryDirectory(); final orionTmpDir = Directory('${tempDir.path}/oriontmp/$finalLocation'); @@ -39,7 +40,9 @@ class ThumbnailUtil { await orionTmpDir.create(recursive: true); } - final filePath = '${orionTmpDir.path}/thumbnail400x400.png'; + final filePath = size == "Small" + ? '${orionTmpDir.path}/thumbnail400x400.png' + : '${orionTmpDir.path}/thumbnail840x400.png'; final outputFile = File(filePath); outputFile.writeAsBytesSync(bytes); From 361ba913125e8fcdf0a30508d87d0759a1b0f1e1 Mon Sep 17 00:00:00 2001 From: Paul_GD Date: Tue, 23 Jul 2024 02:16:43 +0200 Subject: [PATCH 44/82] feat(files, status): update thumbnails in DetailsScreen and StatusScreen to be of large size. This new size will improve the visuals of the design overhaul. --- lib/files/details_screen.dart | 11 ++++---- lib/status/status_screen.dart | 48 ++++++++++++++++++----------------- 2 files changed, 30 insertions(+), 29 deletions(-) diff --git a/lib/files/details_screen.dart b/lib/files/details_screen.dart index fdb9899..05c5b75 100644 --- a/lib/files/details_screen.dart +++ b/lib/files/details_screen.dart @@ -98,9 +98,8 @@ class DetailScreenState extends State { String tempMaterialName = 'N/A'; // this information is not provided by the API String tempThumbnailPath = await ThumbnailUtil.extractThumbnail( - widget.fileLocation, - widget.fileSubdirectory, - widget.fileName); // fetch thumbnail from API + widget.fileLocation, widget.fileSubdirectory, widget.fileName, + size: 'Large'); // fetch thumbnail from API double tempPrintTimeInSeconds = fileDetails['print_time']; Duration printDuration = Duration(seconds: tempPrintTimeInSeconds.toInt()); @@ -283,10 +282,10 @@ class DetailScreenState extends State { padding: const EdgeInsets.all(4.5), child: ClipRRect( borderRadius: BorderRadius.circular(7.75), - child: false //thumbnailPath.isNotEmpty + child: thumbnailPath.isNotEmpty ? Image.file(File(thumbnailPath)) - : Image.asset( - 'assets/images/thumbnail800x480.png', + : const Center( + child: CircularProgressIndicator(), ), ), ), diff --git a/lib/status/status_screen.dart b/lib/status/status_screen.dart index acb9893..9d1451c 100644 --- a/lib/status/status_screen.dart +++ b/lib/status/status_screen.dart @@ -91,7 +91,9 @@ class StatusScreenState extends State String location = status!['print_data']['file_data'] ['location_category'] ?? 'Local'; - if (thumbnailFullPath != null && !isThumbnailFetched) { + if (thumbnailFullPath != null && + !isThumbnailFetched && + status!['status'] == 'Printing') { String thumbnailSubdir = '/'; if (thumbnailFullPath.contains('/')) { thumbnailSubdir = thumbnailFullPath.substring( @@ -101,6 +103,7 @@ class StatusScreenState extends State location, thumbnailSubdir, fileName, + size: 'Large', ); isThumbnailFetched = true; } @@ -310,8 +313,11 @@ class StatusScreenState extends State Expanded( child: Column( children: [ + const Spacer(), buildThumbnailView(context), const Spacer(), + buildNameCard(fileName), + const Spacer(), Row( children: [ Expanded( @@ -401,7 +407,7 @@ class StatusScreenState extends State TextSpan( children: [ TextSpan( - text: fileName.length >= 12 + text: fileName.length >= 4 ? '${fileName.substring(0, 12)}...' : fileName, style: TextStyle( @@ -445,20 +451,18 @@ class StatusScreenState extends State 0.2126, 0.7152, 0.0722, 0, 0, 0, 0, 0, 1, 0, ]), - child: - false //thumbnail != null && thumbnail.isNotEmpty - ? Image.file( - File(thumbnail!), - fit: BoxFit.cover, - ) - : Image.asset( - 'assets/images/thumbnail800x480.png', - fit: BoxFit.cover, - ), + child: thumbnail != null && thumbnail.isNotEmpty + ? Image.file( + File(thumbnail), + fit: BoxFit.cover, + ) + : const Center( + child: CircularProgressIndicator(), + ), ), Positioned.fill( child: Container( - color: Colors.black.withOpacity(0.2), + color: Colors.black.withOpacity(0.35), ), ), // Colored image revealed based on progress @@ -469,16 +473,14 @@ class StatusScreenState extends State child: Align( alignment: Alignment.bottomCenter, heightFactor: progress, - child: - false //thumbnail != null && thumbnail.isNotEmpty - ? Image.file( - File(thumbnail!), - fit: BoxFit.cover, - ) - : Image.asset( - 'assets/images/thumbnail800x480.png', - fit: BoxFit.cover, - ), + child: thumbnail != null && thumbnail.isNotEmpty + ? Image.file( + File(thumbnail), + fit: BoxFit.cover, + ) + : const Center( + child: CircularProgressIndicator(), + ), ), ), ), From 1c0b3b778b32855339bb36ed0019a966c41e4974 Mon Sep 17 00:00:00 2001 From: Paul_GD Date: Tue, 23 Jul 2024 02:36:35 +0200 Subject: [PATCH 45/82] feat(files): add automatic refresh after deleting file. This will remove any confusion on if a file deletion has been successful or not. --- lib/files/details_screen.dart | 3 ++- lib/files/grid_files_screen.dart | 6 +++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/lib/files/details_screen.dart b/lib/files/details_screen.dart index 05c5b75..01b9c10 100644 --- a/lib/files/details_screen.dart +++ b/lib/files/details_screen.dart @@ -303,10 +303,11 @@ class DetailScreenState extends State { _api.deleteFile(widget.fileLocation, path.join(subdirectory, widget.fileName)); _logger.info('File deleted successfully'); + Navigator.pop(context, true); } catch (e) { _logger.severe('Failed to delete file', e); + Navigator.pop(context, false); } - Navigator.pop(context); }, style: ElevatedButton.styleFrom( shape: RoundedRectangleBorder( diff --git a/lib/files/grid_files_screen.dart b/lib/files/grid_files_screen.dart index 359bb5c..99475b0 100644 --- a/lib/files/grid_files_screen.dart +++ b/lib/files/grid_files_screen.dart @@ -425,7 +425,11 @@ class GridFilesScreenState extends State { fileLocation: location, ), ), - ); + ).then((result) { + if (result == true) { + refresh(); + } + }); } }, // File name that hovers over the file From 432b16e96951ab1ed3fa2e09d18d0a5ae60fae88 Mon Sep 17 00:00:00 2001 From: Paul_GD Date: Tue, 23 Jul 2024 03:42:53 +0200 Subject: [PATCH 46/82] feat(util): add HoldButton widget for long-press functionality The code changes in `hold_button.dart` add a new `HoldButton` widget that provides long-press functionality. When the button is pressed and held for a specified duration, the `onPressed` callback is triggered. This widget is useful for implementing actions that require a long press, such as confirming a delete operation or triggering a complex action. --- lib/util/hold_button.dart | 110 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 110 insertions(+) create mode 100644 lib/util/hold_button.dart diff --git a/lib/util/hold_button.dart b/lib/util/hold_button.dart new file mode 100644 index 0000000..ca44a3b --- /dev/null +++ b/lib/util/hold_button.dart @@ -0,0 +1,110 @@ +import 'package:flutter/material.dart'; + +class HoldButton extends StatefulWidget { + final VoidCallback onPressed; + final Widget child; + final ButtonStyle? style; + final Duration duration; + + const HoldButton({ + super.key, + required this.onPressed, + required this.child, + this.style, + this.duration = const Duration(seconds: 3), + }); + + @override + HoldButtonState createState() => HoldButtonState(); +} + +class HoldButtonState extends State + with SingleTickerProviderStateMixin { + late AnimationController _controller; + + @override + void initState() { + super.initState(); + _controller = AnimationController( + vsync: this, + duration: widget.duration, + ); + + _controller.addStatusListener((status) { + if (status == AnimationStatus.completed) { + widget.onPressed(); + } + }); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + void _onTapDown(TapDownDetails details) { + _controller.forward(); + } + + void _onTapUp(TapUpDetails details) { + _controller.reverse(); + } + + void _onTapCancel() { + _controller.reverse(); + } + + @override + Widget build(BuildContext context) { + BorderRadiusGeometry borderRadius = BorderRadius.circular(100); + + if (widget.style?.shape?.resolve({}) is RoundedRectangleBorder) { + borderRadius = + (widget.style?.shape?.resolve({}) as RoundedRectangleBorder) + .borderRadius; + } + + return GestureDetector( + onTapDown: _onTapDown, + onTapUp: _onTapUp, + onTapCancel: _onTapCancel, + child: AnimatedBuilder( + animation: _controller, + builder: (context, child) { + return IntrinsicWidth( + child: IntrinsicHeight( + child: Stack( + alignment: Alignment.center, + children: [ + SizedBox( + width: double.infinity, + height: double.infinity, + child: ElevatedButton( + onPressed: () {}, + style: widget.style, + child: widget.child, + ), + ), + Positioned.fill( + child: IgnorePointer( + child: ClipRRect( + borderRadius: borderRadius, + child: LinearProgressIndicator( + value: _controller.value, + backgroundColor: Colors.transparent, + valueColor: AlwaysStoppedAnimation( + Colors.black.withOpacity(0.5)), + ), + ), + ), + ), + ], + ), + ), + ); + }, + ), + ); + } +} From 657d5704c47c3df4483676581e58cc21fa564401 Mon Sep 17 00:00:00 2001 From: Paul_GD Date: Tue, 23 Jul 2024 03:44:26 +0200 Subject: [PATCH 47/82] feat(files, settings, status): add HoldButton for irreversible actions - This change will ensure that irreversible actions are not triggered by accidental button taps. - Fixed StatusScreen filename limit typo --- lib/files/details_screen.dart | 3 ++- lib/home/home_screen.dart | 7 ++++--- lib/settings/debug_screen.dart | 7 +++++++ lib/status/status_screen.dart | 8 +++++--- 4 files changed, 18 insertions(+), 7 deletions(-) diff --git a/lib/files/details_screen.dart b/lib/files/details_screen.dart index 01b9c10..01b5714 100644 --- a/lib/files/details_screen.dart +++ b/lib/files/details_screen.dart @@ -21,6 +21,7 @@ import 'package:auto_size_text/auto_size_text.dart'; import 'package:logging/logging.dart'; import 'package:orion/api_services/api_services.dart'; import 'package:orion/status/status_screen.dart'; +import 'package:orion/util/hold_button.dart'; import 'package:orion/util/sl1_thumbnail.dart'; import 'package:path/path.dart' as path; import 'package:flutter/material.dart'; @@ -296,7 +297,7 @@ class DetailScreenState extends State { Widget buildPrintButtons() { return Row( children: [ - ElevatedButton( + HoldButton( onPressed: () { String subdirectory = widget.fileSubdirectory; try { diff --git a/lib/home/home_screen.dart b/lib/home/home_screen.dart index b59ccb1..fd4fde5 100644 --- a/lib/home/home_screen.dart +++ b/lib/home/home_screen.dart @@ -23,6 +23,7 @@ import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:orion/api_services/api_services.dart'; import 'package:orion/main.dart'; +import 'package:orion/util/hold_button.dart'; import 'package:orion/util/orion_config.dart'; class HomeScreen extends StatefulWidget { @@ -107,7 +108,7 @@ class HomeScreenState extends State { child: SizedBox( height: 65, width: double.infinity, - child: ElevatedButton( + child: HoldButton( onPressed: () { _api.manualCommand('FIRMWARE_RESTART'); }, @@ -128,7 +129,7 @@ class HomeScreenState extends State { child: SizedBox( height: 65, width: double.infinity, - child: ElevatedButton( + child: HoldButton( onPressed: () { Process.run('sudo', ['reboot', 'now']); }, @@ -147,7 +148,7 @@ class HomeScreenState extends State { child: SizedBox( height: 65, width: double.infinity, - child: ElevatedButton( + child: HoldButton( onPressed: () { Process.run('sudo', ['shutdown', 'now']); }, diff --git a/lib/settings/debug_screen.dart b/lib/settings/debug_screen.dart index 2288d4d..546fadb 100644 --- a/lib/settings/debug_screen.dart +++ b/lib/settings/debug_screen.dart @@ -18,6 +18,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:orion/util/hold_button.dart'; import 'package:orion/util/orion_kb/orion_keyboard_expander.dart'; import 'package:orion/util/orion_kb/orion_textfield_spawn.dart'; @@ -146,6 +147,12 @@ class DebugScreenState extends State { ), ), ), + HoldButton( + onPressed: () { + print("onHoldComplete"); + }, + child: const Text('Hold Button Test'), + ), OrionKbExpander(textFieldKey: debugTextFieldKey) ], ), diff --git a/lib/status/status_screen.dart b/lib/status/status_screen.dart index 9d1451c..ec9b3e4 100644 --- a/lib/status/status_screen.dart +++ b/lib/status/status_screen.dart @@ -25,6 +25,7 @@ import 'package:orion/api_services/api_services.dart'; import 'package:orion/files/grid_files_screen.dart'; import 'package:orion/settings/settings_screen.dart'; import 'package:orion/themes/themes.dart'; +import 'package:orion/util/hold_button.dart'; import 'package:orion/util/sl1_thumbnail.dart'; import 'package:orion/util/status_card.dart'; @@ -407,8 +408,8 @@ class StatusScreenState extends State TextSpan( children: [ TextSpan( - text: fileName.length >= 4 - ? '${fileName.substring(0, 12)}...' + text: fileName.length >= 14 + ? '${fileName.substring(0, 14)}...' : fileName, style: TextStyle( fontSize: 24, @@ -605,7 +606,8 @@ class StatusScreenState extends State child: SizedBox( height: 65, width: double.infinity, - child: ElevatedButton( + child: HoldButton( + duration: const Duration(seconds: 5), onPressed: () { Navigator.pop(context); _api.cancelPrint(); From f4835a46d395d57a46e03ce18daa856e2a98b7c4 Mon Sep 17 00:00:00 2001 From: Paul_GD Date: Fri, 26 Jul 2024 15:44:12 +0200 Subject: [PATCH 48/82] feat(util): add support for displaying Markdown files in MarkdownScreen - Added support for displaying Markdown files in the MarkdownScreen widget. - Added optional `filename` and `changelog` parameters to the MarkdownScreen constructor. - If `changelog` is provided, it will be displayed as Markdown data. - If `filename` is provided, the file contents will be loaded and displayed as Markdown data. - Added `_getMarkdownStyleSheet` method to customize the Markdown styling based on the app's theme. --- lib/util/markdown_screen.dart | 71 ++++++++++++++++++++--------------- 1 file changed, 41 insertions(+), 30 deletions(-) diff --git a/lib/util/markdown_screen.dart b/lib/util/markdown_screen.dart index 61fe5ed..1305dd2 100644 --- a/lib/util/markdown_screen.dart +++ b/lib/util/markdown_screen.dart @@ -21,46 +21,57 @@ import 'package:flutter/services.dart'; import 'package:flutter_markdown/flutter_markdown.dart'; class MarkdownScreen extends StatelessWidget { - final String filename; + final String? filename; + final String? changelog; - const MarkdownScreen({super.key, required this.filename}); + const MarkdownScreen({super.key, this.filename, this.changelog}); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( - title: Text(filename), + title: Text(filename ?? 'Changelog'), ), body: Padding( padding: const EdgeInsets.all(12.0), - child: FutureBuilder( - future: rootBundle.loadString(filename), - builder: (BuildContext context, AsyncSnapshot snapshot) { - if (snapshot.connectionState == ConnectionState.done) { - return Markdown( - data: snapshot.data ?? '', - styleSheet: Theme.of(context).brightness == Brightness.dark - ? MarkdownStyleSheet.fromTheme(Theme.of(context)).copyWith( - code: const TextStyle( - color: Colors.limeAccent, - backgroundColor: Colors.black, - fontFamily: 'monospace', - ), - ) - : MarkdownStyleSheet.fromTheme(Theme.of(context)).copyWith( - code: const TextStyle( - color: Colors.deepPurple, - backgroundColor: Colors.white, - fontFamily: 'monospace', - ), - ), - ); - } else { - return const CircularProgressIndicator(); - } - }, - ), + child: changelog != null + ? Markdown( + data: changelog!, + styleSheet: _getMarkdownStyleSheet(context), + ) + : FutureBuilder( + future: rootBundle.loadString(filename!), + builder: + (BuildContext context, AsyncSnapshot snapshot) { + if (snapshot.connectionState == ConnectionState.done) { + return Markdown( + data: snapshot.data ?? '', + styleSheet: _getMarkdownStyleSheet(context), + ); + } else { + return const CircularProgressIndicator(); + } + }, + ), ), ); } + + MarkdownStyleSheet _getMarkdownStyleSheet(BuildContext context) { + return Theme.of(context).brightness == Brightness.dark + ? MarkdownStyleSheet.fromTheme(Theme.of(context)).copyWith( + code: const TextStyle( + color: Colors.limeAccent, + backgroundColor: Colors.black, + fontFamily: 'monospace', + ), + ) + : MarkdownStyleSheet.fromTheme(Theme.of(context)).copyWith( + code: const TextStyle( + color: Colors.deepPurple, + backgroundColor: Colors.white, + fontFamily: 'monospace', + ), + ); + } } From b2c7c1a07515bfb43cdaf95d84af92357ebd3a91 Mon Sep 17 00:00:00 2001 From: Paul_GD Date: Fri, 26 Jul 2024 15:49:29 +0200 Subject: [PATCH 49/82] feat(util): update HoldButton to use theme color for opacity The code changes in `hold_button.dart` update the `HoldButton` widget to use the primary color from the app's theme for setting the opacity of the progress indicator. This ensures consistency with the overall design and improves the visual appeal of the button. --- lib/util/hold_button.dart | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/util/hold_button.dart b/lib/util/hold_button.dart index ca44a3b..b09e04b 100644 --- a/lib/util/hold_button.dart +++ b/lib/util/hold_button.dart @@ -94,7 +94,11 @@ class HoldButtonState extends State value: _controller.value, backgroundColor: Colors.transparent, valueColor: AlwaysStoppedAnimation( - Colors.black.withOpacity(0.5)), + Theme.of(context) + .colorScheme + .primary + .withOpacity(0.5), + ), ), ), ), From 00b5f29eec48fd3bab973ba5fe969d850a6c6cda Mon Sep 17 00:00:00 2001 From: Paul_GD Date: Fri, 26 Jul 2024 15:49:46 +0200 Subject: [PATCH 50/82] refactor(util): remove unused import in orion_config.dart --- lib/util/orion_config.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/util/orion_config.dart b/lib/util/orion_config.dart index db9005c..2264969 100644 --- a/lib/util/orion_config.dart +++ b/lib/util/orion_config.dart @@ -17,7 +17,6 @@ */ import 'dart:io'; -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:logging/logging.dart'; import 'package:path/path.dart' as path; From 3b5b51a72d88290119e76b147430c61a3cf35371 Mon Sep 17 00:00:00 2001 From: Paul_GD Date: Fri, 26 Jul 2024 15:51:31 +0200 Subject: [PATCH 51/82] chore(tools): update button styles in ExposureScreen The code changes in `exposure_screen.dart` update the button styles in the `ExposureScreen` class. The buttons for different exposure types now have different shapes, with rounded corners on the top-left and bottom-right for the "Grid" button, rounded corners on the top-right and bottom-left for the "Dimensions" button, and no rounded corners for the "Blank" button. This improves the visual appeal and provides a clearer visual distinction between the buttons. --- lib/tools/exposure_screen.dart | 43 ++++++++++++++++++++++++++++++---- 1 file changed, 38 insertions(+), 5 deletions(-) diff --git a/lib/tools/exposure_screen.dart b/lib/tools/exposure_screen.dart index fef64d7..23c108a 100644 --- a/lib/tools/exposure_screen.dart +++ b/lib/tools/exposure_screen.dart @@ -187,7 +187,7 @@ class ExposureScreenState extends State { Future getApiStatus() async { try { - Map config = await _api.getConfig(); + await _api.getConfig(); } catch (e) { setState(() { _apiErrorState = true; @@ -202,7 +202,6 @@ class ExposureScreenState extends State { final theme = Theme.of(context).copyWith( elevatedButtonTheme: ElevatedButtonThemeData( style: ButtonStyle( - backgroundColor: MaterialStateProperty.all(Colors.transparent), shape: MaterialStateProperty.resolveWith( (Set states) { return RoundedRectangleBorder( @@ -254,7 +253,19 @@ class ExposureScreenState extends State { onPressed: _apiErrorState ? null : () => exposeScreen('Grid'), - style: theme.elevatedButtonTheme.style, + style: theme.elevatedButtonTheme.style + ?.copyWith( + shape: MaterialStateProperty.resolveWith< + OutlinedBorder?>( + (Set states) { + return const RoundedRectangleBorder( + borderRadius: BorderRadius.only( + bottomLeft: Radius.circular(15), + ), + ); + }, + ), + ), child: const PhosphorIcon( PhosphorIconsFill.checkerboard, size: 40), @@ -268,7 +279,17 @@ class ExposureScreenState extends State { onPressed: _apiErrorState ? null : () => exposeScreen('Dimensions'), - style: theme.elevatedButtonTheme.style, + style: theme.elevatedButtonTheme.style + ?.copyWith( + shape: MaterialStateProperty.resolveWith< + OutlinedBorder?>( + (Set states) { + return RoundedRectangleBorder( + borderRadius: + BorderRadius.circular(0)); + }, + ), + ), child: const PhosphorIcon( PhosphorIconsFill.ruler, size: 40), @@ -282,7 +303,19 @@ class ExposureScreenState extends State { onPressed: _apiErrorState ? null : () => exposeScreen('Blank'), - style: theme.elevatedButtonTheme.style, + style: theme.elevatedButtonTheme.style + ?.copyWith( + shape: MaterialStateProperty.resolveWith< + OutlinedBorder?>( + (Set states) { + return const RoundedRectangleBorder( + borderRadius: BorderRadius.only( + bottomRight: Radius.circular(15), + ), + ); + }, + ), + ), child: PhosphorIcon(PhosphorIcons.square(), size: 40), ), From 224365e72080d800ffb7503140485b98f5f24011 Mon Sep 17 00:00:00 2001 From: Paul_GD Date: Fri, 26 Jul 2024 15:52:09 +0200 Subject: [PATCH 52/82] chore(files): make TODOs more descriptive Still awaiting API changes --- lib/files/grid_files_screen.dart | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/files/grid_files_screen.dart b/lib/files/grid_files_screen.dart index 99475b0..7f65cf2 100644 --- a/lib/files/grid_files_screen.dart +++ b/lib/files/grid_files_screen.dart @@ -210,7 +210,7 @@ class GridFilesScreenState extends State { color: Color.fromARGB(255, 99, 99, 99)), iconSize: 35, onPressed: () { - // TODO: Re-implement search + // TODO (?): Re-implement search /*Navigator.push( context, MaterialPageRoute( @@ -227,7 +227,7 @@ class GridFilesScreenState extends State { color: Color.fromARGB(255, 99, 99, 99)), iconSize: 35, onPressed: () { - // TODO: Implement in API + // TODO: Implement Alpha Sorting in API /*_sortByAlpha = true; _toggleSortOrder();*/ }, @@ -240,7 +240,7 @@ class GridFilesScreenState extends State { color: Color.fromARGB(255, 99, 99, 99)), iconSize: 35, onPressed: () { - // TODO: Implement in API + // TODO: Implement Date Sorting in API /*_sortByAlpha = false; _toggleSortOrder();*/ }, From 7c404d8e354b48464c9736a7181fefbae8a5d8b0 Mon Sep 17 00:00:00 2001 From: Paul_GD Date: Fri, 26 Jul 2024 15:54:16 +0200 Subject: [PATCH 53/82] chore(settings): remove unused import and update padding in wifi_screen.dart --- lib/settings/wifi_screen.dart | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/lib/settings/wifi_screen.dart b/lib/settings/wifi_screen.dart index d73f461..31a10e7 100644 --- a/lib/settings/wifi_screen.dart +++ b/lib/settings/wifi_screen.dart @@ -20,7 +20,6 @@ import 'dart:io'; import 'dart:math'; -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:logging/logging.dart'; @@ -447,8 +446,7 @@ class _WifiScreenState extends State { itemBuilder: (context, index) { final network = networks[index]; return Padding( - padding: - const EdgeInsets.only(left: 16, right: 16, top: 5), + padding: const EdgeInsets.only(left: 16, right: 16), child: Card.outlined( elevation: 1, child: ListTile( From a6aa4266c51a55552828b0c87aeda0fb9aff67fa Mon Sep 17 00:00:00 2001 From: Paul_GD Date: Fri, 26 Jul 2024 16:04:06 +0200 Subject: [PATCH 54/82] chore(home): remove unused import in home_screen.dart --- lib/home/home_screen.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/home/home_screen.dart b/lib/home/home_screen.dart index fd4fde5..6b1b8f0 100644 --- a/lib/home/home_screen.dart +++ b/lib/home/home_screen.dart @@ -22,7 +22,6 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:orion/api_services/api_services.dart'; -import 'package:orion/main.dart'; import 'package:orion/util/hold_button.dart'; import 'package:orion/util/orion_config.dart'; From 21deeca14cb66ed922fb16bb34e64174dd747a16 Mon Sep 17 00:00:00 2001 From: Paul_GD Date: Fri, 26 Jul 2024 16:04:17 +0200 Subject: [PATCH 55/82] chore(settings): comment out debug print statement in DebugScreenState --- lib/settings/debug_screen.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/settings/debug_screen.dart b/lib/settings/debug_screen.dart index 546fadb..bc34cf2 100644 --- a/lib/settings/debug_screen.dart +++ b/lib/settings/debug_screen.dart @@ -149,7 +149,7 @@ class DebugScreenState extends State { ), HoldButton( onPressed: () { - print("onHoldComplete"); + //print("onHoldComplete"); }, child: const Text('Hold Button Test'), ), From d0eb183700fdd0ef315f4c6bd37e5df5fd528c2c Mon Sep 17 00:00:00 2001 From: Paul_GD Date: Fri, 26 Jul 2024 16:13:18 +0200 Subject: [PATCH 56/82] feat(core: update build.yml to add commit hash --- .github/workflows/build.yml | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 483a0e2..2e58337 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -13,7 +13,7 @@ jobs: - uses: seanmiddleditch/gha-setup-ninja@master - uses: subosito/flutter-action@v2 with: - channel: 'stable' # or: 'beta', 'dev', 'master' (or 'main') + channel: "stable" # or: 'beta', 'dev', 'master' (or 'main') - run: sudo apt-get update && sudo apt-get install -y gtk+-3.0 - run: flutter pub global activate flutterpi_tool - run: flutter build linux @@ -24,6 +24,17 @@ jobs: with: cmd: yq '.version' pubspec.yaml + - name: Update Version with Commit Hash + run: | + VERSION=$(yq '.version' pubspec.yaml) + COMMIT_HASH=${GITHUB_SHA::7} + NEW_VERSION=${VERSION/+SELFCOMPILED/+${COMMIT_HASH}} + yq -i ".version = \"$NEW_VERSION\"" pubspec.yaml + echo "Updated version to $NEW_VERSION" + + - name: Run pubspec_extract + run: flutter pub run pubspec_extract + - name: Build, Copy, and Compress armv7 run: | BUILD_NAME=orion_armv7 @@ -31,12 +42,10 @@ jobs: mv build/flutter_assets $BUILD_NAME ( cd $BUILD_NAME && tar -czvf $BUILD_NAME.tar.gz * ) cp $BUILD_NAME/$BUILD_NAME.tar.gz $BUILD_NAME.tar.gz - - uses: actions/upload-artifact@v4.3.1 with: name: orion_armv7 path: orion_armv7/* - - name: Build, Copy, and Compress aarch64 run: | BUILD_NAME=orion_aarch64 @@ -44,12 +53,10 @@ jobs: mv build/flutter_assets $BUILD_NAME ( cd $BUILD_NAME && tar -czvf $BUILD_NAME.tar.gz * ) cp $BUILD_NAME/$BUILD_NAME.tar.gz $BUILD_NAME.tar.gz - - uses: actions/upload-artifact@v4.3.1 with: name: orion_aarch64 path: orion_aarch64/* - - name: Build, Copy, and Compress x64 run: | BUILD_NAME=orion_x64 @@ -57,12 +64,10 @@ jobs: mv build/flutter_assets $BUILD_NAME ( cd $BUILD_NAME && tar -czvf $BUILD_NAME.tar.gz * ) cp $BUILD_NAME/$BUILD_NAME.tar.gz $BUILD_NAME.tar.gz - - uses: actions/upload-artifact@v4.3.1 with: name: orion_x64 path: orion_x64/* - - uses: ncipollo/release-action@v1.14.0 if: github.ref == 'refs/heads/main' with: From ecd78047a2c03271ba65ac1b8eb103e517f42701 Mon Sep 17 00:00:00 2001 From: Paul_GD Date: Fri, 26 Jul 2024 16:13:53 +0200 Subject: [PATCH 57/82] feat(settings): add settings for beta branch override and beta check overrides --- lib/settings/general_screen.dart | 150 +++++++++++++++++++++++++++++-- 1 file changed, 144 insertions(+), 6 deletions(-) diff --git a/lib/settings/general_screen.dart b/lib/settings/general_screen.dart index 78cc71e..ce6f740 100644 --- a/lib/settings/general_screen.dart +++ b/lib/settings/general_screen.dart @@ -17,14 +17,15 @@ */ import 'dart:math'; - import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +import 'package:phosphor_flutter/phosphor_flutter.dart'; + import 'package:orion/util/orion_config.dart'; import 'package:orion/util/orion_kb/orion_keyboard_expander.dart'; import 'package:orion/util/orion_kb/orion_textfield_spawn.dart'; import 'package:orion/util/orion_list_tile.dart'; -import 'package:phosphor_flutter/phosphor_flutter.dart'; -import 'package:provider/provider.dart'; class GeneralCfgScreen extends StatefulWidget { const GeneralCfgScreen({super.key}); @@ -39,6 +40,9 @@ class GeneralCfgScreenState extends State { late bool useCustomUrl; late String customUrl; late bool developerMode; + late bool betaOverride; + late bool overrideUpdateCheck; + late String overrideBranch; late bool verboseLogging; late bool selfDestructMode; @@ -48,6 +52,8 @@ class GeneralCfgScreenState extends State { final GlobalKey urlTextFieldKey = GlobalKey(); + final GlobalKey branchTextFieldKey = + GlobalKey(); @override void initState() { @@ -58,6 +64,10 @@ class GeneralCfgScreenState extends State { useCustomUrl = config.getFlag('useCustomUrl', category: 'advanced'); customUrl = config.getString('customUrl', category: 'advanced'); developerMode = config.getFlag('developerMode', category: 'advanced'); + betaOverride = config.getFlag('betaOverride', category: 'developer'); + overrideUpdateCheck = + config.getFlag('overrideUpdateCheck', category: 'developer'); + overrideBranch = config.getString('overrideBranch', category: 'developer'); verboseLogging = config.getFlag('verboseLogging', category: 'developer'); selfDestructMode = config.getFlag('selfDestructMode', category: 'topsecret'); @@ -348,19 +358,147 @@ class GeneralCfgScreenState extends State { ), ), const SizedBox(height: 20.0), + OrionListTile( + title: 'Beta Override', + icon: PhosphorIcons.download(), + value: betaOverride, + onChanged: (bool value) { + setState(() { + betaOverride = value; + config.setFlag('betaOverride', betaOverride, + category: 'developer'); + }); + }, + ), + if (betaOverride) const SizedBox(height: 20.0), + if (betaOverride) + Row( + children: [ + Expanded( + child: SizedBox( + height: 55, + child: ElevatedButton( + style: ElevatedButton.styleFrom(elevation: 3), + onPressed: () { + showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + title: const Center( + child: Text('Override Branch')), + content: SizedBox( + width: MediaQuery.of(context) + .size + .width * + 0.5, + child: SingleChildScrollView( + child: Column( + children: [ + SpawnOrionTextField( + key: branchTextFieldKey, + keyboardHint: + 'Enter Branch', + locale: + Localizations.localeOf( + context) + .toString(), + scrollController: + _scrollController, + ), + OrionKbExpander( + textFieldKey: + branchTextFieldKey), + ], + ), + ), + ), + actions: [ + TextButton( + onPressed: () { + Navigator.of(context).pop(); + }, + child: const Text('Close'), + ), + TextButton( + onPressed: () { + setState(() { + overrideBranch = + branchTextFieldKey + .currentState! + .getCurrentText(); + config.setString( + 'overrideBranch', + overrideBranch, + category: 'developer'); + }); + Navigator.of(context).pop(); + }, + child: const Text('Confirm'), + ), + ], + ); + }, + ); + }, + child: Text( + overrideBranch == '' + ? 'Set Branch' + : overrideBranch, + style: const TextStyle(fontSize: 22)), + ), + ), + ), + const SizedBox(width: 10), + Expanded( + child: SizedBox( + height: 55, + child: ElevatedButton( + style: ElevatedButton.styleFrom(elevation: 3), + onPressed: overrideBranch == '' + ? null + : () { + setState(() { + overrideBranch = ''; + config.setString('overrideBranch', + overrideBranch, + category: 'developer'); + }); + }, + child: const Text('Clear Branch', + style: TextStyle(fontSize: 22)), + ), + ), + ), + ], + ), + const SizedBox(height: 20.0), + OrionListTile( + title: 'Ignore Update Check', + icon: PhosphorIcons.warning(), + value: overrideUpdateCheck, + onChanged: (bool value) { + setState(() { + overrideUpdateCheck = value; + config.setFlag( + 'overrideUpdateCheck', overrideUpdateCheck, + category: 'developer'); + }); + }, + ), + /*const SizedBox(height: 20.0), OrionListTile( title: 'Verbose Logging [WIP]', icon: PhosphorIcons.bug, value: verboseLogging, onChanged: (bool value) { null; - /*setState(() { + setState(() { verboseLogging = value; config.setFlag('verboseLogging', developerMode, category: 'developer'); - });*/ + }); }, - ), + ),*/ ], ), ), From eca770ca73c26b9f8cbc58427fd30973564bb1c9 Mon Sep 17 00:00:00 2001 From: Paul_GD Date: Fri, 26 Jul 2024 16:14:09 +0200 Subject: [PATCH 58/82] feat(settings): add UpdateScreen to SettingsScreen This commit adds the UpdateScreen widget to the SettingsScreen. The UpdateScreen will display information about available updates for the app. This feature allows users to easily check for updates and stay up-to-date with the latest version of the app. --- lib/settings/settings_screen.dart | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/lib/settings/settings_screen.dart b/lib/settings/settings_screen.dart index 8d41ae7..7437f94 100644 --- a/lib/settings/settings_screen.dart +++ b/lib/settings/settings_screen.dart @@ -18,14 +18,17 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +import 'package:about/about.dart'; + import 'package:orion/pubspec.dart'; +import 'package:orion/settings/about_screen.dart'; import 'package:orion/settings/debug_screen.dart'; import 'package:orion/settings/general_screen.dart'; -import 'package:orion/util/markdown_screen.dart'; +import 'package:orion/settings/update_screen.dart'; import 'package:orion/settings/wifi_screen.dart'; -import 'package:orion/settings/about_screen.dart'; -import 'package:provider/provider.dart'; -import 'package:about/about.dart'; +import 'package:orion/util/markdown_screen.dart'; class SettingsScreen extends StatefulWidget { const SettingsScreen({super.key}); @@ -138,7 +141,9 @@ class SettingsScreenState extends State { ? const WifiScreen() : _selectedIndex == 2 ? const AboutScreen() - : DebugScreen(changeThemeMode: changeThemeMode), + : _selectedIndex == 3 + ? const UpdateScreen() + : DebugScreen(changeThemeMode: changeThemeMode), bottomNavigationBar: BottomNavigationBar( type: BottomNavigationBarType.fixed, items: const [ @@ -154,6 +159,7 @@ class SettingsScreenState extends State { icon: Icon(Icons.info), label: 'About', ), + BottomNavigationBarItem(icon: Icon(Icons.update), label: 'Updates'), if (kDebugMode) BottomNavigationBarItem( icon: Icon(Icons.bug_report), From 48ac25bf1b9ceb61fd1cfe6a1b6df34d3c437c79 Mon Sep 17 00:00:00 2001 From: Paul_GD Date: Fri, 26 Jul 2024 16:14:31 +0200 Subject: [PATCH 59/82] feat(settings): Add UpdateScreen to SettingsScreen This commit adds the UpdateScreen widget to the SettingsScreen. The UpdateScreen will display information about available updates for the app. This feature allows users to easily check for updates and stay up-to-date with the latest version of the app. --- lib/settings/update_screen.dart | 481 ++++++++++++++++++++++++++++++++ 1 file changed, 481 insertions(+) create mode 100644 lib/settings/update_screen.dart diff --git a/lib/settings/update_screen.dart b/lib/settings/update_screen.dart new file mode 100644 index 0000000..65685ef --- /dev/null +++ b/lib/settings/update_screen.dart @@ -0,0 +1,481 @@ +/* +* Orion - Update Screen +* Copyright (C) 2024 TheContrappostoShop +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see . +*/ + +import 'dart:convert'; +import 'dart:io'; +import 'dart:math'; + +import 'package:flutter/material.dart'; +import 'package:http/http.dart' as http; +import 'package:logging/logging.dart'; +import 'package:orion/pubspec.dart'; +import 'package:orion/util/markdown_screen.dart'; +import 'package:orion/util/orion_config.dart'; +import 'package:phosphor_flutter/phosphor_flutter.dart'; + +class UpdateScreen extends StatefulWidget { + const UpdateScreen({super.key}); + + @override + UpdateScreenState createState() => UpdateScreenState(); +} + +class UpdateScreenState extends State { + bool _isLoading = true; + bool _isUpdateAvailable = false; + bool _isFirmwareSpoofingEnabled = false; + bool _betaUpdatesOverride = false; + bool _rateLimitExceeded = false; + + String _latestVersion = ''; + String _commitDate = ''; + String _releaseNotes = ''; + String _currentVersion = ''; + String _branch = 'dev'; + String _assetUrl = ''; + + final Logger _logger = Logger('UpdateScreen'); + final OrionConfig _config = OrionConfig(); + + @override + void initState() { + super.initState(); + _initUpdateCheck(); + _isFirmwareSpoofingEnabled = + _config.getFlag('overrideUpdateCheck', category: 'developer'); + _betaUpdatesOverride = + _config.getFlag('betaOverride', category: 'developer'); + _branch = _config.getString('overrideBranch', category: 'developer'); + _logger.info('Firmware spoofing enabled: $_isFirmwareSpoofingEnabled'); + _logger.info('Beta updates override enabled: $_betaUpdatesOverride'); + _logger.info('Branch override: $_branch'); + } + + Future _initUpdateCheck() async { + await _getCurrentAppVersion(); + await _checkForUpdates(_branch); + } + + Future _getCurrentAppVersion() async { + try { + setState(() { + _currentVersion = Pubspec.versionFull; + _logger.info('Current version: $_currentVersion'); + }); + } catch (e) { + setState(() { + _logger.warning('Failed to get current app version'); + }); + } + } + + Future _checkForUpdates(String branch) async { + if (_betaUpdatesOverride) { + await _checkForBERUpdates(branch); + } else { + const String url = + 'https://api.github.com/repos/thecontrappostoshop/orion/releases/latest'; + int retryCount = 0; + const int maxRetries = 3; + const int initialDelay = 750; + while (retryCount < maxRetries) { + try { + final response = await http.get(Uri.parse(url)); + if (response.statusCode == 200) { + final jsonResponse = json.decode(response.body); + final String latestVersion = jsonResponse['tag_name'] + .replaceAll('v', ''); // Remove 'v' prefix if present + final String releaseNotes = jsonResponse['body']; + _logger.info('Latest version: $latestVersion'); + if (_isNewerVersion(latestVersion, _currentVersion)) { + // Find the asset URL for orion_aarch64.tar.gz + final asset = jsonResponse['assets'].firstWhere( + (asset) => asset['name'] == 'orion_aarch64.tar.gz', + orElse: () => null); + final String assetUrl = + asset != null ? asset['browser_download_url'] : ''; + setState(() { + _latestVersion = latestVersion; + _releaseNotes = releaseNotes; + _isLoading = false; + _isUpdateAvailable = true; + _assetUrl = assetUrl; // Set the asset URL + }); + } else { + setState(() { + _isLoading = false; + _isUpdateAvailable = false; + }); + } + return; // Exit the function after successful fetch + } else if (response.statusCode == 403 && + response.headers['x-ratelimit-remaining'] == '0') { + _logger.warning('Rate limit exceeded, retrying...'); + setState(() { + _rateLimitExceeded = true; + }); + await Future.delayed(Duration( + milliseconds: initialDelay * pow(2, retryCount).toInt())); + retryCount++; + } else { + setState(() { + _logger.warning('Failed to fetch updates'); + _isLoading = false; + }); + return; // Exit the function after failure + } + } catch (e) { + _logger.warning(e.toString()); + setState(() { + _isLoading = false; + }); + return; // Exit the function after failure + } + } + } + } + + Future _checkForBERUpdates(String branch) async { + if (branch.isEmpty) { + _logger.warning('Branch name is empty'); + branch = 'dev'; + } + String url = + 'https://api.github.com/repos/thecontrappostoshop/orion/releases'; + int retryCount = 0; + const int maxRetries = 3; + const int initialDelay = 750; // Initial delay in milliseconds + while (retryCount < maxRetries) { + try { + final response = await http.get(Uri.parse(url)); + if (response.statusCode == 200) { + final jsonResponse = json.decode(response.body) as List; + final preRelease = jsonResponse.firstWhere( + (release) => + release['prerelease'] == true && + release['tag_name'].contains(branch), + orElse: () => null); + if (preRelease != null) { + final String latestVersion = preRelease['tag_name']; + final String commitSha = preRelease['target_commitish']; + final commitUrl = + 'https://api.github.com/repos/thecontrappostoshop/orion/commits/$commitSha'; + final commitResponse = await http.get(Uri.parse(commitUrl)); + if (commitResponse.statusCode == 200) { + final commitJson = json.decode(commitResponse.body); + final String shortCommitSha = + commitJson['sha'].substring(0, 7); // Get short commit SHA + final String commitMessage = commitJson['commit']['message']; + final String commitDate = commitJson['commit']['committer'] + ['date']; // Fetch commit date + + // Find the asset URL for orion_aarch64.tar.gz + final asset = preRelease['assets'].firstWhere( + (asset) => asset['name'] == 'orion_aarch64.tar.gz', + orElse: () => null); + final String assetUrl = + asset != null ? asset['browser_download_url'] : ''; + + _logger.info('Latest pre-release version: $latestVersion'); + setState(() { + _latestVersion = + '$shortCommitSha (BRANCH_$branch)'; // Append branch name + _releaseNotes = commitMessage; + _commitDate = commitDate; // Store commit date + _isLoading = false; + _isUpdateAvailable = true; + _rateLimitExceeded = false; + _assetUrl = assetUrl; // Set the asset URL + }); + return; // Exit the function after successful fetch + } else { + _logger.warning( + 'Failed to fetch commit details, status code: ${commitResponse.statusCode}'); + setState(() { + _isLoading = false; + _rateLimitExceeded = false; + }); + return; // Exit the function after failure + } + } else { + _logger.warning('No pre-release found for branch: $branch'); + setState(() { + _isLoading = false; + _rateLimitExceeded = false; + }); + return; // Exit the function after no pre-release found + } + } else if (response.statusCode == 403 && + response.headers['x-ratelimit-remaining'] == '0') { + _logger.warning('Rate limit exceeded, retrying...'); + setState(() { + _rateLimitExceeded = true; + }); + await Future.delayed(Duration( + milliseconds: initialDelay * pow(2, retryCount).toInt())); + retryCount++; + } else { + _logger.warning( + 'Failed to fetch updates, status code: ${response.statusCode}'); + setState(() { + _isLoading = false; + _rateLimitExceeded = false; + }); + return; // Exit the function after failure + } + } catch (e) { + _logger.warning(e.toString()); + setState(() { + _isLoading = false; + _rateLimitExceeded = false; + }); + return; // Exit the function after failure + } + } + } + + bool _isNewerVersion(String latestVersion, String currentVersion) { + _logger.info('Firmware spoofing enabled: $_isFirmwareSpoofingEnabled'); + if (_isFirmwareSpoofingEnabled) return true; + // Split the version and build numbers + List latestVersionParts = latestVersion.split('+')[0].split('.'); + List currentVersionParts = currentVersion.split('+')[0].split('.'); + + // Convert version parts to integers for comparison + List latestNumbers = latestVersionParts.map(int.parse).toList(); + List currentNumbers = currentVersionParts.map(int.parse).toList(); + + // Compare major, minor, and patch numbers + for (int i = 0; i < min(latestNumbers.length, currentNumbers.length); i++) { + if (latestNumbers[i] > currentNumbers[i]) { + return true; + } else if (latestNumbers[i] < currentNumbers[i]) { + return false; + } + } + + // If versions are equal, compare build numbers if present + if (latestVersion.contains('+') && currentVersion.contains('+')) { + String latestBuild = latestVersion; + String currentBuild = currentVersion.split('+')[1]; + // Attempt to compare build numbers as integers if possible + try { + int latestBuildNumber = int.parse(latestBuild); + int currentBuildNumber = int.parse(currentBuild); + return latestBuildNumber > currentBuildNumber; + } catch (e) { + // If build numbers are not integers, compare them as strings + return latestBuild.compareTo(currentBuild) > 0; + } + } + + // Versions are equal and no build number to compare + return false; + } + + void _viewChangelog() { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => MarkdownScreen(changelog: _releaseNotes), + ), + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + body: ListView( + padding: const EdgeInsets.only( + left: 16.0, right: 16.0, bottom: 16.0, top: 5.0), + children: [ + Card.outlined( + elevation: 1, + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (_rateLimitExceeded) ...[ + const Row( + children: [ + Icon(Icons.error, color: Colors.red, size: 30), + SizedBox(width: 10), + Text('Rate Limit Exceeded!', + style: TextStyle( + fontSize: 26, fontWeight: FontWeight.bold)), + ], + ), + const Divider(), + const Text('Please try again later.', + style: TextStyle(fontSize: 20)), + ] else if (_isLoading) ...[ + const Center(child: CircularProgressIndicator()), + ] else if (_isUpdateAvailable) ...[ + Row( + children: [ + _betaUpdatesOverride + ? PhosphorIcon(PhosphorIcons.knife()) + : Icon(Icons.system_update, + color: Theme.of(context).colorScheme.primary, + size: 30), + const SizedBox(width: 10), + Text( + _betaUpdatesOverride + ? 'Bleeding Edge Available!' + : 'UI Update Available!', + style: const TextStyle( + fontSize: 26, fontWeight: FontWeight.bold)), + ], + ), + const Divider(), + Text( + _betaUpdatesOverride + ? 'Latest Commit: $_latestVersion' + : 'Latest Version: ${_latestVersion.split('+')[0]}', + style: const TextStyle(fontSize: 22)), + const SizedBox(height: 10), + Text( + _betaUpdatesOverride + ? 'Commit Date: ${_commitDate.split('T')[0]}' // Display commit date if beta updates are enabled + : 'Release Date: ${_getFormattedDate()}', + style: const TextStyle(fontSize: 20, color: Colors.grey), + ), + const SizedBox(height: 10), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: ElevatedButton.icon( + style: ElevatedButton.styleFrom( + elevation: 3, + minimumSize: + const Size.fromHeight(65), // Set height to 65 + ), + onPressed: _viewChangelog, + icon: const Icon(Icons.article), + label: const Text('View Changelog', + style: TextStyle(fontSize: 24)), + ), + ), + const SizedBox( + width: 10), // Add some space between the buttons + Expanded( + child: ElevatedButton.icon( + style: ElevatedButton.styleFrom( + elevation: 3, + minimumSize: + const Size.fromHeight(65), // Set height to 65 + ), + onPressed: _performUpdate, + icon: const Icon(Icons.download), + label: const Text('Download Update', + style: TextStyle(fontSize: 24)), + ), + ), + ], + ), + ] else ...[ + const Row( + children: [ + Icon(Icons.check_circle, color: Colors.green, size: 30), + SizedBox(width: 10), + Text('Orion is up to date!', + style: TextStyle( + fontSize: 26, fontWeight: FontWeight.bold)), + ], + ), + const Divider(), + Text('Current Version: ${_currentVersion.split('+')[0]}', + style: const TextStyle(fontSize: 20)), + ], + ], + ), + ), + ), + // Placeholder for Odyssey updater - pending API changes + const Card.outlined( + elevation: 1, + child: Padding( + padding: EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('Odyssey Updater', style: TextStyle(fontSize: 24)), + SizedBox(height: 10), + // Dummy content, replace with actual data when available + Text('Coming soon...', style: TextStyle(fontSize: 20)), + ], + ), + ), + ), + ], + ), + ); + } + + String _getFormattedDate() { + // Placeholder function to return a formatted date + return '2023-10-01'; + } + + Future _performUpdate() async { + const String upgradeFolder = '/home/pi/orion/upgrade/'; + const String upgradeScript = '/home/pi/orion/upgrade.sh'; + const String downloadPath = '$upgradeFolder/orion_aarch64.tar.gz'; + + if (_assetUrl.isEmpty) { + _logger.warning('Asset URL is empty'); + return; + } + + _logger.info('Downloading from $_assetUrl'); + + try { + // Purge and recreate the upgrade folder + final upgradeDir = Directory(upgradeFolder); + if (await upgradeDir.exists()) { + try { + await upgradeDir.delete(recursive: true); + } catch (e) { + _logger.warning('Could not purge upgrade directory'); + } + } + await upgradeDir.create(recursive: true); + + // Download the update file + final response = await http.get(Uri.parse(_assetUrl)); + if (response.statusCode == 200) { + final file = File(downloadPath); + await file.writeAsBytes(response.bodyBytes); + + // Execute the upgrade script + final result = await Process.run(upgradeScript, []); + if (result.exitCode == 0) { + _logger.info('Update script executed successfully'); + } else { + _logger.warning('Update script failed: ${result.stderr}'); + } + } else { + _logger.warning('Failed to download update file'); + } + } catch (e) { + _logger.warning('Update failed: $e'); + } + } +} From 4c7b230134f38d71a8ed965c0fc85b1357acb6ea Mon Sep 17 00:00:00 2001 From: Paul_GD Date: Fri, 26 Jul 2024 21:04:24 +0200 Subject: [PATCH 60/82] chore(orionpi.sh): update file paths in orionpi.sh for deployment to Raspberry Pi --- orionpi.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/orionpi.sh b/orionpi.sh index 7f710f3..a3fef4c 100755 --- a/orionpi.sh +++ b/orionpi.sh @@ -168,7 +168,7 @@ if [ "$run_only" != true ]; then start_msg="Copying Files to Target" printf "%s" "$start_msg" start_time=$(date +%s) - (sshpass -p "$password" scp -r ./build/flutter_assets $user@$ip:/home/$user/orionpi & show_scroller $! "$start_msg") + (sshpass -p "$password" scp -r ./build/flutter_assets $user@$ip:/home/$user/orion & show_scroller $! "$start_msg") wait $! end_time=$(date +%s) print_done "Done. [$((end_time - start_time))s]" $((end_time - start_time)) @@ -178,7 +178,7 @@ fi printf "\n\r[\033[0;32m✓\033[0m]\033[0;32m%s\033[0m\n" " Running OrionPi on Raspberry Pi!" printf "\r[i]"" Press \033[0;31mCtrl+C\033[0m to disconnect.\n\n" if [ "$release" = true ]; then - sshpass -p "$password" ssh $user@$ip 'flutter-pi --release --pixelformat=RGB565 /home/pi/orionpi/flutter_assets' + sshpass -p "$password" ssh $user@$ip 'flutter-pi --release --pixelformat=RGB565 /home/pi/orion' else - sshpass -p "$password" ssh $user@$ip 'flutter-pi --pixelformat=RGB565 /home/pi/orionpi/flutter_assets' + sshpass -p "$password" ssh $user@$ip 'flutter-pi --pixelformat=RGB565 /home/pi/orion' fi From da26fe68da7299c0c9db43944f109e792eddaeb6 Mon Sep 17 00:00:00 2001 From: Paul_GD Date: Fri, 26 Jul 2024 21:11:37 +0200 Subject: [PATCH 61/82] feat(settings): implement Orion update logic --- lib/settings/update_screen.dart | 89 ++++++++++++++++++++++++++++----- 1 file changed, 76 insertions(+), 13 deletions(-) diff --git a/lib/settings/update_screen.dart b/lib/settings/update_screen.dart index 65685ef..eba4f79 100644 --- a/lib/settings/update_screen.dart +++ b/lib/settings/update_screen.dart @@ -150,6 +150,13 @@ class UpdateScreenState extends State { } } + bool isCurrentCommitUpToDate(String commitSha) { + _logger.info('Current commit SHA: ${_currentVersion.split('+')[1]}'); + _logger.info('Latest commit SHA: $commitSha'); + if (_isFirmwareSpoofingEnabled) return false; + return commitSha == _currentVersion.split('+')[1]; + } + Future _checkForBERUpdates(String branch) async { if (branch.isEmpty) { _logger.warning('Branch name is empty'); @@ -184,13 +191,23 @@ class UpdateScreenState extends State { final String commitDate = commitJson['commit']['committer'] ['date']; // Fetch commit date + if (isCurrentCommitUpToDate(shortCommitSha)) { + _logger.info( + 'Current version is up-to-date with the latest pre-release.'); + setState(() { + _isLoading = false; + _isUpdateAvailable = false; + _rateLimitExceeded = false; + }); + return; // Exit the function if the current version is up-to-date + } + // Find the asset URL for orion_aarch64.tar.gz final asset = preRelease['assets'].firstWhere( (asset) => asset['name'] == 'orion_aarch64.tar.gz', orElse: () => null); final String assetUrl = asset != null ? asset['browser_download_url'] : ''; - _logger.info('Latest pre-release version: $latestVersion'); setState(() { _latestVersion = @@ -252,6 +269,7 @@ class UpdateScreenState extends State { bool _isNewerVersion(String latestVersion, String currentVersion) { _logger.info('Firmware spoofing enabled: $_isFirmwareSpoofingEnabled'); if (_isFirmwareSpoofingEnabled) return true; + // Split the version and build numbers List latestVersionParts = latestVersion.split('+')[0].split('.'); List currentVersionParts = currentVersion.split('+')[0].split('.'); @@ -269,7 +287,6 @@ class UpdateScreenState extends State { } } - // If versions are equal, compare build numbers if present if (latestVersion.contains('+') && currentVersion.contains('+')) { String latestBuild = latestVersion; String currentBuild = currentVersion.split('+')[1]; @@ -391,17 +408,24 @@ class UpdateScreenState extends State { ], ), ] else ...[ - const Row( + Row( children: [ - Icon(Icons.check_circle, color: Colors.green, size: 30), - SizedBox(width: 10), - Text('Orion is up to date!', - style: TextStyle( + const Icon(Icons.check_circle, + color: Colors.green, size: 30), + const SizedBox(width: 10), + Text( + _betaUpdatesOverride + ? 'Bleeding Edge is up to date!' + : 'Orion is up to date!', + style: const TextStyle( fontSize: 26, fontWeight: FontWeight.bold)), ], ), const Divider(), - Text('Current Version: ${_currentVersion.split('+')[0]}', + Text( + _betaUpdatesOverride + ? 'Current Version: $_currentVersion (BRANCH_$_branch)' + : 'Current Version: ${_currentVersion.split('+')[0]}', style: const TextStyle(fontSize: 20)), ], ], @@ -435,9 +459,10 @@ class UpdateScreenState extends State { } Future _performUpdate() async { - const String upgradeFolder = '/home/pi/orion/upgrade/'; - const String upgradeScript = '/home/pi/orion/upgrade.sh'; + const String upgradeFolder = '/home/pi/orion_upgrade/'; const String downloadPath = '$upgradeFolder/orion_aarch64.tar.gz'; + const String orionFolder = '/home/pi/orion/'; + const String backupFolder = '/home/pi/orion_backup/'; if (_assetUrl.isEmpty) { _logger.warning('Asset URL is empty'); @@ -464,12 +489,50 @@ class UpdateScreenState extends State { final file = File(downloadPath); await file.writeAsBytes(response.bodyBytes); - // Execute the upgrade script - final result = await Process.run(upgradeScript, []); + // Copy /home/pi/orion/ to /home/pi/orion_backup/ + final orionDir = Directory(orionFolder); + final backupDir = Directory(backupFolder); + if (await backupDir.exists()) { + final deleteResult = + await Process.run('sudo', ['rm', '-R', '-rf', backupFolder]); + if (deleteResult.exitCode != 0) { + _logger.warning( + 'Failed to delete backup directory: ${deleteResult.stderr}'); + return; + } + } + if (await orionDir.exists()) { + final renameResult = await Process.run( + 'sudo', ['cp', '-r', orionFolder, backupFolder]); + if (renameResult.exitCode != 0) { + _logger.warning( + 'Failed to rename Orion directory: ${renameResult.stderr}'); + return; + } + } else { + _logger.warning('Orion directory does not exist, skipping backup'); + } + + // Ensure the orion directory exists + await orionDir.create(recursive: true); + + // Extract the downloaded orion_aarch64.tar.gz to /home/pi/orion/ + final result = + await Process.run('tar', ['-xzf', downloadPath, '-C', orionFolder]); if (result.exitCode == 0) { _logger.info('Update script executed successfully'); + + // Restart the orion.service + final restartResult = await Process.run( + 'sudo', ['systemctl', 'restart', 'orion.service']); + if (restartResult.exitCode == 0) { + _logger.info('Orion service restarted successfully'); + } else { + _logger.warning( + 'Failed to restart Orion service: ${restartResult.stderr}'); + } } else { - _logger.warning('Update script failed: ${result.stderr}'); + _logger.warning('Failed to extract update file: ${result.stderr}'); } } else { _logger.warning('Failed to download update file'); From 67bab8a4966ed724c7a5b8fc67149077ce6f7d40 Mon Sep 17 00:00:00 2001 From: Paul_GD Date: Fri, 26 Jul 2024 21:42:13 +0200 Subject: [PATCH 62/82] fix(settings): update Orion update logic to use sudo for extracting tar file --- lib/settings/update_screen.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/settings/update_screen.dart b/lib/settings/update_screen.dart index eba4f79..cb35be4 100644 --- a/lib/settings/update_screen.dart +++ b/lib/settings/update_screen.dart @@ -517,8 +517,8 @@ class UpdateScreenState extends State { await orionDir.create(recursive: true); // Extract the downloaded orion_aarch64.tar.gz to /home/pi/orion/ - final result = - await Process.run('tar', ['-xzf', downloadPath, '-C', orionFolder]); + final result = await Process.run('sudo', + ['tar', '--overwrite', '-xzf', downloadPath, '-C', orionFolder]); if (result.exitCode == 0) { _logger.info('Update script executed successfully'); From b245af226ec05b08f995e6062a877b86f74ac496 Mon Sep 17 00:00:00 2001 From: Paul_GD Date: Sat, 27 Jul 2024 01:54:38 +0200 Subject: [PATCH 63/82] fix(settings): update asset URL for orion_armv7.tar.gz fix(settings): fix cp recursive flag --- lib/settings/update_screen.dart | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/lib/settings/update_screen.dart b/lib/settings/update_screen.dart index cb35be4..7d2ad02 100644 --- a/lib/settings/update_screen.dart +++ b/lib/settings/update_screen.dart @@ -103,9 +103,9 @@ class UpdateScreenState extends State { final String releaseNotes = jsonResponse['body']; _logger.info('Latest version: $latestVersion'); if (_isNewerVersion(latestVersion, _currentVersion)) { - // Find the asset URL for orion_aarch64.tar.gz + // Find the asset URL for orion_armv7.tar.gz final asset = jsonResponse['assets'].firstWhere( - (asset) => asset['name'] == 'orion_aarch64.tar.gz', + (asset) => asset['name'] == 'orion_armv7.tar.gz', orElse: () => null); final String assetUrl = asset != null ? asset['browser_download_url'] : ''; @@ -202,9 +202,9 @@ class UpdateScreenState extends State { return; // Exit the function if the current version is up-to-date } - // Find the asset URL for orion_aarch64.tar.gz + // Find the asset URL for orion_armv7.tar.gz final asset = preRelease['assets'].firstWhere( - (asset) => asset['name'] == 'orion_aarch64.tar.gz', + (asset) => asset['name'] == 'orion_armv7.tar.gz', orElse: () => null); final String assetUrl = asset != null ? asset['browser_download_url'] : ''; @@ -460,7 +460,7 @@ class UpdateScreenState extends State { Future _performUpdate() async { const String upgradeFolder = '/home/pi/orion_upgrade/'; - const String downloadPath = '$upgradeFolder/orion_aarch64.tar.gz'; + const String downloadPath = '$upgradeFolder/orion_armv7.tar.gz'; const String orionFolder = '/home/pi/orion/'; const String backupFolder = '/home/pi/orion_backup/'; @@ -503,7 +503,7 @@ class UpdateScreenState extends State { } if (await orionDir.exists()) { final renameResult = await Process.run( - 'sudo', ['cp', '-r', orionFolder, backupFolder]); + 'sudo', ['cp', '-R', orionFolder, backupFolder]); if (renameResult.exitCode != 0) { _logger.warning( 'Failed to rename Orion directory: ${renameResult.stderr}'); @@ -516,13 +516,14 @@ class UpdateScreenState extends State { // Ensure the orion directory exists await orionDir.create(recursive: true); - // Extract the downloaded orion_aarch64.tar.gz to /home/pi/orion/ + // Extract the downloaded orion_armv7.tar.gz to /home/pi/orion/ final result = await Process.run('sudo', ['tar', '--overwrite', '-xzf', downloadPath, '-C', orionFolder]); if (result.exitCode == 0) { _logger.info('Update script executed successfully'); // Restart the orion.service + _logger.info('Restarting Orion service...'); final restartResult = await Process.run( 'sudo', ['systemctl', 'restart', 'orion.service']); if (restartResult.exitCode == 0) { From 5ade43dd76eb61a1f1a1e636edeefd7545ffced8 Mon Sep 17 00:00:00 2001 From: Paul_GD Date: Sat, 27 Jul 2024 03:16:06 +0200 Subject: [PATCH 64/82] fix(util): improve CircularProgressIndicator styling --- lib/util/orion_config.dart | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/lib/util/orion_config.dart b/lib/util/orion_config.dart index 2264969..2e24298 100644 --- a/lib/util/orion_config.dart +++ b/lib/util/orion_config.dart @@ -125,9 +125,12 @@ class OrionConfig { backgroundColor: Theme.of(context).colorScheme.background, child: const Center( child: SizedBox( - height: 75, - width: 75, - child: CircularProgressIndicator()), + height: 75, + width: 75, + child: CircularProgressIndicator( + strokeWidth: 6, + ), + ), ), ), ); From 10ba375921b2952d72e76e802edfeda6f88cdbd3 Mon Sep 17 00:00:00 2001 From: Paul_GD Date: Sat, 27 Jul 2024 03:16:40 +0200 Subject: [PATCH 65/82] fix(settings): add update dialog add support for different user names --- lib/settings/update_screen.dart | 106 +++++++++++++++++++++++++++++--- 1 file changed, 99 insertions(+), 7 deletions(-) diff --git a/lib/settings/update_screen.dart b/lib/settings/update_screen.dart index 7d2ad02..3d451c9 100644 --- a/lib/settings/update_screen.dart +++ b/lib/settings/update_screen.dart @@ -399,7 +399,9 @@ class UpdateScreenState extends State { minimumSize: const Size.fromHeight(65), // Set height to 65 ), - onPressed: _performUpdate, + onPressed: () async { + _performUpdate(context); + }, icon: const Icon(Icons.download), label: const Text('Download Update', style: TextStyle(fontSize: 24)), @@ -458,11 +460,14 @@ class UpdateScreenState extends State { return '2023-10-01'; } - Future _performUpdate() async { - const String upgradeFolder = '/home/pi/orion_upgrade/'; - const String downloadPath = '$upgradeFolder/orion_armv7.tar.gz'; - const String orionFolder = '/home/pi/orion/'; - const String backupFolder = '/home/pi/orion_backup/'; + Future _performUpdate(BuildContext context) async { + final String localUser = Platform.environment['USER'] ?? + 'pi'; // Fallback to 'pi' if the USER environment variable is not found + + final String upgradeFolder = '/home/$localUser/orion_upgrade/'; + final String downloadPath = '$upgradeFolder/orion_armv7.tar.gz'; + final String orionFolder = '/home/$localUser/orion/'; + final String backupFolder = '/home/$localUser/orion_backup/'; if (_assetUrl.isEmpty) { _logger.warning('Asset URL is empty'); @@ -471,6 +476,9 @@ class UpdateScreenState extends State { _logger.info('Downloading from $_assetUrl'); + // Show the update dialog + _showUpdateDialog(context, 'Starting update...'); + try { // Purge and recreate the upgrade folder final upgradeDir = Directory(upgradeFolder); @@ -483,12 +491,22 @@ class UpdateScreenState extends State { } await upgradeDir.create(recursive: true); + // Update dialog text + _updateDialogText(context, 'Downloading update file...'); + + Future.delayed(const Duration(seconds: 1)); + // Download the update file final response = await http.get(Uri.parse(_assetUrl)); if (response.statusCode == 200) { final file = File(downloadPath); await file.writeAsBytes(response.bodyBytes); + // Update dialog text + _updateDialogText(context, 'Backing up current installation...'); + + Future.delayed(const Duration(seconds: 1)); + // Copy /home/pi/orion/ to /home/pi/orion_backup/ final orionDir = Directory(orionFolder); final backupDir = Directory(backupFolder); @@ -498,6 +516,7 @@ class UpdateScreenState extends State { if (deleteResult.exitCode != 0) { _logger.warning( 'Failed to delete backup directory: ${deleteResult.stderr}'); + _dismissUpdateDialog(context); return; } } @@ -507,6 +526,7 @@ class UpdateScreenState extends State { if (renameResult.exitCode != 0) { _logger.warning( 'Failed to rename Orion directory: ${renameResult.stderr}'); + _dismissUpdateDialog(context); return; } } else { @@ -516,14 +536,23 @@ class UpdateScreenState extends State { // Ensure the orion directory exists await orionDir.create(recursive: true); + // Update dialog text + _updateDialogText(context, 'Extracting update file...'); + + Future.delayed(const Duration(seconds: 2)); + // Extract the downloaded orion_armv7.tar.gz to /home/pi/orion/ final result = await Process.run('sudo', ['tar', '--overwrite', '-xzf', downloadPath, '-C', orionFolder]); if (result.exitCode == 0) { _logger.info('Update script executed successfully'); + // Update dialog text + _updateDialogText(context, 'Restarting Orion service...'); + + Future.delayed(const Duration(seconds: 2)); + // Restart the orion.service - _logger.info('Restarting Orion service...'); final restartResult = await Process.run( 'sudo', ['systemctl', 'restart', 'orion.service']); if (restartResult.exitCode == 0) { @@ -540,6 +569,69 @@ class UpdateScreenState extends State { } } catch (e) { _logger.warning('Update failed: $e'); + } finally { + // Dismiss the update dialog + _dismissUpdateDialog(context); + } + } + + Future _showUpdateDialog(BuildContext context, String message) { + return showDialog( + context: context, + barrierDismissible: false, + builder: (BuildContext context) { + return SafeArea( + child: Dialog( + backgroundColor: Colors.transparent, + insetPadding: EdgeInsets.zero, + child: Container( + width: MediaQuery.of(context).size.width, + height: MediaQuery.of(context).size.height, + color: Theme.of(context).colorScheme.background, + child: Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const SizedBox( + height: 75, + width: 75, + child: CircularProgressIndicator( + strokeWidth: 6, + ), + ), + const SizedBox(height: 60), + Text( + message, + textAlign: TextAlign.center, + style: const TextStyle(fontSize: 32), + ), + ], + ), + ), + ), + ); + }, + ); + } + + void _updateDialogText(BuildContext context, String message) { + if (Navigator.of(context).canPop()) { + // Show the new dialog first + _showUpdateDialog(context, message).then((_) { + // Pop the old dialog after the new one has been rendered + if (Navigator.of(context).canPop()) { + Navigator.of(context).pop(); + } + }); + } else { + // If there's no dialog to pop, just show the new one + _showUpdateDialog(context, message); + } + } + + void _dismissUpdateDialog(BuildContext context) { + if (Navigator.of(context).canPop()) { + Navigator.of(context).pop(); } } } From 910ea590f056faa24babc3719c3a52f990e29d9b Mon Sep 17 00:00:00 2001 From: Paul_GD Date: Sat, 27 Jul 2024 03:54:27 +0200 Subject: [PATCH 66/82] fix(settings): add chown action to fix permissions --- lib/settings/update_screen.dart | 39 ++++++++++++++++++++++----------- 1 file changed, 26 insertions(+), 13 deletions(-) diff --git a/lib/settings/update_screen.dart b/lib/settings/update_screen.dart index 3d451c9..2af7a15 100644 --- a/lib/settings/update_screen.dart +++ b/lib/settings/update_screen.dart @@ -494,7 +494,7 @@ class UpdateScreenState extends State { // Update dialog text _updateDialogText(context, 'Downloading update file...'); - Future.delayed(const Duration(seconds: 1)); + await Future.delayed(const Duration(seconds: 1)); // Download the update file final response = await http.get(Uri.parse(_assetUrl)); @@ -505,7 +505,7 @@ class UpdateScreenState extends State { // Update dialog text _updateDialogText(context, 'Backing up current installation...'); - Future.delayed(const Duration(seconds: 1)); + await Future.delayed(const Duration(seconds: 1)); // Copy /home/pi/orion/ to /home/pi/orion_backup/ final orionDir = Directory(orionFolder); @@ -539,7 +539,7 @@ class UpdateScreenState extends State { // Update dialog text _updateDialogText(context, 'Extracting update file...'); - Future.delayed(const Duration(seconds: 2)); + await Future.delayed(const Duration(seconds: 2)); // Extract the downloaded orion_armv7.tar.gz to /home/pi/orion/ final result = await Process.run('sudo', @@ -547,19 +547,32 @@ class UpdateScreenState extends State { if (result.exitCode == 0) { _logger.info('Update script executed successfully'); - // Update dialog text - _updateDialogText(context, 'Restarting Orion service...'); + _updateDialogText(context, 'Setting permissions...'); - Future.delayed(const Duration(seconds: 2)); + await Future.delayed(const Duration(seconds: 2)); - // Restart the orion.service - final restartResult = await Process.run( - 'sudo', ['systemctl', 'restart', 'orion.service']); - if (restartResult.exitCode == 0) { - _logger.info('Orion service restarted successfully'); + final chownResult = await Process.run( + 'sudo', ['chown', '-R', '$localUser:$localUser', orionFolder]); + if (chownResult.exitCode == 0) { + // Ensure extraction has fully finished + await Future.delayed(const Duration(seconds: 2)); + + // Update dialog text + _updateDialogText(context, 'Restarting Orion service...'); + + await Future.delayed(const Duration(seconds: 2)); + + // Restart the orion.service + final restartResult = await Process.run( + 'sudo', ['systemctl', 'restart', 'orion.service']); + if (restartResult.exitCode == 0) { + _logger.info('Orion service restarted successfully'); + } else { + _logger.warning( + 'Failed to restart Orion service: ${restartResult.stderr}'); + } } else { - _logger.warning( - 'Failed to restart Orion service: ${restartResult.stderr}'); + _logger.warning('Failed to set permissions: ${chownResult.stderr}'); } } else { _logger.warning('Failed to extract update file: ${result.stderr}'); From 3e776c30adc28a86b27ff7b2f59d8fa5fe7f2ef5 Mon Sep 17 00:00:00 2001 From: Paul_GD Date: Sat, 27 Jul 2024 05:54:29 +0200 Subject: [PATCH 67/82] fix(settings): change update logic to use an external script. This change will eliminate any FS related issues in Orion to cause crashing. --- lib/settings/update_screen.dart | 121 ++++++++++++++++---------------- 1 file changed, 60 insertions(+), 61 deletions(-) diff --git a/lib/settings/update_screen.dart b/lib/settings/update_screen.dart index 2af7a15..00a117c 100644 --- a/lib/settings/update_screen.dart +++ b/lib/settings/update_screen.dart @@ -461,13 +461,13 @@ class UpdateScreenState extends State { } Future _performUpdate(BuildContext context) async { - final String localUser = Platform.environment['USER'] ?? - 'pi'; // Fallback to 'pi' if the USER environment variable is not found - + final String localUser = Platform.environment['USER'] ?? 'pi'; final String upgradeFolder = '/home/$localUser/orion_upgrade/'; final String downloadPath = '$upgradeFolder/orion_armv7.tar.gz'; final String orionFolder = '/home/$localUser/orion/'; + final String newOrionFolder = '/home/$localUser/orion_new/'; final String backupFolder = '/home/$localUser/orion_backup/'; + final String scriptPath = '$upgradeFolder/update_orion.sh'; if (_assetUrl.isEmpty) { _logger.warning('Asset URL is empty'); @@ -491,6 +491,16 @@ class UpdateScreenState extends State { } await upgradeDir.create(recursive: true); + final newDir = Directory(newOrionFolder); + if (await newDir.exists()) { + try { + await newDir.delete(recursive: true); + } catch (e) { + _logger.warning('Could not purge new Orion directory'); + } + } + await newDir.create(recursive: true); + // Update dialog text _updateDialogText(context, 'Downloading update file...'); @@ -503,79 +513,68 @@ class UpdateScreenState extends State { await file.writeAsBytes(response.bodyBytes); // Update dialog text - _updateDialogText(context, 'Backing up current installation...'); + _updateDialogText(context, 'Extracting update file...'); await Future.delayed(const Duration(seconds: 1)); - // Copy /home/pi/orion/ to /home/pi/orion_backup/ - final orionDir = Directory(orionFolder); - final backupDir = Directory(backupFolder); - if (await backupDir.exists()) { - final deleteResult = - await Process.run('sudo', ['rm', '-R', '-rf', backupFolder]); - if (deleteResult.exitCode != 0) { - _logger.warning( - 'Failed to delete backup directory: ${deleteResult.stderr}'); - _dismissUpdateDialog(context); - return; - } - } - if (await orionDir.exists()) { - final renameResult = await Process.run( - 'sudo', ['cp', '-R', orionFolder, backupFolder]); - if (renameResult.exitCode != 0) { - _logger.warning( - 'Failed to rename Orion directory: ${renameResult.stderr}'); - _dismissUpdateDialog(context); - return; - } - } else { - _logger.warning('Orion directory does not exist, skipping backup'); + // Extract the update to the new directory + final extractResult = await Process.run('sudo', + ['tar', '--overwrite', '-xzf', downloadPath, '-C', newOrionFolder]); + if (extractResult.exitCode != 0) { + _logger.warning( + 'Failed to extract update file: ${extractResult.stderr}'); + _dismissUpdateDialog(context); + return; } - // Ensure the orion directory exists - await orionDir.create(recursive: true); + // Create the update script + final scriptContent = ''' +#!/bin/bash - // Update dialog text - _updateDialogText(context, 'Extracting update file...'); +# Variables +local_user=$localUser +orion_folder=$orionFolder +new_orion_folder=$newOrionFolder +upgrade_folder=$upgradeFolder +backup_folder=$backupFolder - await Future.delayed(const Duration(seconds: 2)); +# Backup the current Orion directory +sudo cp -R \$orion_folder \$backup_folder - // Extract the downloaded orion_armv7.tar.gz to /home/pi/orion/ - final result = await Process.run('sudo', - ['tar', '--overwrite', '-xzf', downloadPath, '-C', orionFolder]); - if (result.exitCode == 0) { - _logger.info('Update script executed successfully'); +# Remove the old Orion directory +sudo rm -R \$orion_folder - _updateDialogText(context, 'Setting permissions...'); +# Restore config file +sudo cp \$backup_folder/orion.cfg \$new_orion_folder - await Future.delayed(const Duration(seconds: 2)); +# Move the new Orion directory to the original location +sudo mv \$new_orion_folder \$orion_folder - final chownResult = await Process.run( - 'sudo', ['chown', '-R', '$localUser:$localUser', orionFolder]); - if (chownResult.exitCode == 0) { - // Ensure extraction has fully finished - await Future.delayed(const Duration(seconds: 2)); +# Delete the upgrade and new folder +sudo rm -R \$upgrade_folder - // Update dialog text - _updateDialogText(context, 'Restarting Orion service...'); +# Fix permissions +sudo chown -R \$local_user:\$local_user \$orion_folder - await Future.delayed(const Duration(seconds: 2)); +# Restart the Orion service +sudo systemctl restart orion.service +'''; - // Restart the orion.service - final restartResult = await Process.run( - 'sudo', ['systemctl', 'restart', 'orion.service']); - if (restartResult.exitCode == 0) { - _logger.info('Orion service restarted successfully'); - } else { - _logger.warning( - 'Failed to restart Orion service: ${restartResult.stderr}'); - } - } else { - _logger.warning('Failed to set permissions: ${chownResult.stderr}'); - } + final scriptFile = File(scriptPath); + await scriptFile.writeAsString(scriptContent); + await Process.run('chmod', ['+x', scriptPath]); + + // Update dialog text + _updateDialogText(context, 'Executing update script...'); + + await Future.delayed(const Duration(seconds: 2)); + + // Execute the update script + final result = await Process.run('nohup', ['sudo', scriptPath]); + if (result.exitCode == 0) { + _logger.info('Update script executed successfully'); } else { - _logger.warning('Failed to extract update file: ${result.stderr}'); + _logger.warning('Failed to execute update script: ${result.stderr}'); } } else { _logger.warning('Failed to download update file'); From df6cd36fec226714eab66cd6c40a45343835b9fb Mon Sep 17 00:00:00 2001 From: Paul_GD Date: Sat, 27 Jul 2024 14:11:33 +0200 Subject: [PATCH 68/82] feat(settings): remove CalibrateScreen This commit removes the CalibrateScreen widget from the settings module. The CalibrateScreen was no longer needed and has been deleted to simplify the codebase. --- lib/main.dart | 11 ++-------- lib/settings/calibrate_screen.dart | 35 ------------------------------ 2 files changed, 2 insertions(+), 44 deletions(-) delete mode 100644 lib/settings/calibrate_screen.dart diff --git a/lib/main.dart b/lib/main.dart index 0892529..8be7229 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -19,14 +19,13 @@ import 'dart:io'; import 'package:flutter/foundation.dart'; -import 'package:orion/files/grid_files_screen.dart'; import 'package:orion/home/home_screen.dart'; +import 'package:orion/settings/wifi_screen.dart'; import 'package:orion/status/status_screen.dart'; import 'package:orion/files/files_screen.dart'; +import 'package:orion/files/grid_files_screen.dart'; import 'package:orion/settings/settings_screen.dart'; -import 'package:orion/settings/calibrate_screen.dart'; -import 'package:orion/settings/wifi_screen.dart'; import 'package:orion/settings/about_screen.dart'; import 'package:orion/themes/themes.dart'; import 'package:orion/util/error_handling/error_handler.dart'; @@ -101,12 +100,6 @@ final GoRouter _router = GoRouter( return const SettingsScreen(); }, routes: [ - GoRoute( - path: 'calibrate', - builder: (BuildContext context, GoRouterState state) { - return const CalibrateScreen(); - }, - ), GoRoute( path: 'wifi', builder: (BuildContext context, GoRouterState state) { diff --git a/lib/settings/calibrate_screen.dart b/lib/settings/calibrate_screen.dart deleted file mode 100644 index 900ba22..0000000 --- a/lib/settings/calibrate_screen.dart +++ /dev/null @@ -1,35 +0,0 @@ -/* -* Orion - Calibrate Screen -* Copyright (C) 2024 TheContrappostoShop -* -* This program is free software: you can redistribute it and/or modify -* it under the terms of the GNU General Public License as published by -* the Free Software Foundation, either version 3 of the License, or -* (at your option) any later version. -* -* This program is distributed in the hope that it will be useful, -* but WITHOUT ANY WARRANTY; without even the implied warranty of -* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -* GNU General Public License for more details. -* -* You should have received a copy of the GNU General Public License -* along with this program. If not, see . -*/ - -import 'package:flutter/material.dart'; - -/// The calibrate screen -class CalibrateScreen extends StatelessWidget { - /// Constructs a [CalibrateScreen] - const CalibrateScreen({super.key}); - - @override - Widget build(BuildContext context) { - return const Center( - child: Text( - 'Calibrate', - style: TextStyle(fontSize: 24), - ), - ); - } -} From d95580c9f07c0e64207e36d6189b2e5f6469daef Mon Sep 17 00:00:00 2001 From: Paul_GD Date: Sat, 27 Jul 2024 14:43:16 +0200 Subject: [PATCH 69/82] feat(settings): ensure old update backup is deleted before new backup is made --- lib/settings/update_screen.dart | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/lib/settings/update_screen.dart b/lib/settings/update_screen.dart index 00a117c..979ce79 100644 --- a/lib/settings/update_screen.dart +++ b/lib/settings/update_screen.dart @@ -1,3 +1,5 @@ +// ignore_for_file: use_build_context_synchronously + /* * Orion - Update Screen * Copyright (C) 2024 TheContrappostoShop @@ -538,6 +540,11 @@ new_orion_folder=$newOrionFolder upgrade_folder=$upgradeFolder backup_folder=$backupFolder +# If previous backup exists, delete it +if [ -d \$backup_folder ]; then + sudo rm -R \$backup_folder +fi + # Backup the current Orion directory sudo cp -R \$orion_folder \$backup_folder From fa8d1cf22a8f15a7c639c4e7e87a3bf6c55c4443 Mon Sep 17 00:00:00 2001 From: Ada Phillips Date: Sat, 27 Jul 2024 20:26:10 -0400 Subject: [PATCH 70/82] Update update_screen.dart (#12) * Update update_screen.dart Updated branch specifications to be release specifications, without presuming the `BRANCH_` prefix and without .contains() matching release/branch names --- lib/settings/update_screen.dart | 29 ++++++++++++++--------------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/lib/settings/update_screen.dart b/lib/settings/update_screen.dart index 979ce79..33f18e6 100644 --- a/lib/settings/update_screen.dart +++ b/lib/settings/update_screen.dart @@ -48,7 +48,7 @@ class UpdateScreenState extends State { String _commitDate = ''; String _releaseNotes = ''; String _currentVersion = ''; - String _branch = 'dev'; + String _release = 'BRANCH_dev'; String _assetUrl = ''; final Logger _logger = Logger('UpdateScreen'); @@ -62,15 +62,15 @@ class UpdateScreenState extends State { _config.getFlag('overrideUpdateCheck', category: 'developer'); _betaUpdatesOverride = _config.getFlag('betaOverride', category: 'developer'); - _branch = _config.getString('overrideBranch', category: 'developer'); + _release = _config.getString('overrideBranch', category: 'developer'); _logger.info('Firmware spoofing enabled: $_isFirmwareSpoofingEnabled'); _logger.info('Beta updates override enabled: $_betaUpdatesOverride'); - _logger.info('Branch override: $_branch'); + _logger.info('Release channel override: $_release'); } Future _initUpdateCheck() async { await _getCurrentAppVersion(); - await _checkForUpdates(_branch); + await _checkForUpdates(_release); } Future _getCurrentAppVersion() async { @@ -86,9 +86,9 @@ class UpdateScreenState extends State { } } - Future _checkForUpdates(String branch) async { + Future _checkForUpdates(String release) async { if (_betaUpdatesOverride) { - await _checkForBERUpdates(branch); + await _checkForBERUpdates(release); } else { const String url = 'https://api.github.com/repos/thecontrappostoshop/orion/releases/latest'; @@ -159,10 +159,10 @@ class UpdateScreenState extends State { return commitSha == _currentVersion.split('+')[1]; } - Future _checkForBERUpdates(String branch) async { - if (branch.isEmpty) { - _logger.warning('Branch name is empty'); - branch = 'dev'; + Future _checkForBERUpdates(String release) async { + if (release.isEmpty) { + _logger.warning('release name is empty'); + release = 'BRANCH_dev'; } String url = 'https://api.github.com/repos/thecontrappostoshop/orion/releases'; @@ -176,8 +176,7 @@ class UpdateScreenState extends State { final jsonResponse = json.decode(response.body) as List; final preRelease = jsonResponse.firstWhere( (release) => - release['prerelease'] == true && - release['tag_name'].contains(branch), + release['tag_name'].equals(release), orElse: () => null); if (preRelease != null) { final String latestVersion = preRelease['tag_name']; @@ -213,7 +212,7 @@ class UpdateScreenState extends State { _logger.info('Latest pre-release version: $latestVersion'); setState(() { _latestVersion = - '$shortCommitSha (BRANCH_$branch)'; // Append branch name + '$shortCommitSha ($release)'; // Append release name _releaseNotes = commitMessage; _commitDate = commitDate; // Store commit date _isLoading = false; @@ -232,7 +231,7 @@ class UpdateScreenState extends State { return; // Exit the function after failure } } else { - _logger.warning('No pre-release found for branch: $branch'); + _logger.warning('No release found named $release'); setState(() { _isLoading = false; _rateLimitExceeded = false; @@ -428,7 +427,7 @@ class UpdateScreenState extends State { const Divider(), Text( _betaUpdatesOverride - ? 'Current Version: $_currentVersion (BRANCH_$_branch)' + ? 'Current Version: $_currentVersion ($_release)' : 'Current Version: ${_currentVersion.split('+')[0]}', style: const TextStyle(fontSize: 20)), ], From 7f2ed3ebce94e4c68e502f4ddf3a278d3f834cf5 Mon Sep 17 00:00:00 2001 From: Paul_GD Date: Sun, 28 Jul 2024 03:47:02 +0200 Subject: [PATCH 71/82] feat(github): update version extraction in build workflow remove +BUILD from the tag creation --- .github/workflows/build.yml | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 2e58337..8136a87 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -20,9 +20,11 @@ jobs: - name: Get Version id: get_version - uses: mikefarah/yq@master - with: - cmd: yq '.version' pubspec.yaml + run: | + VERSION=$(yq '.version' pubspec.yaml) + # Strip build metadata if it exists + VERSION=$(echo $VERSION | sed 's/+.*//') + echo "::set-output name=result::$VERSION" - name: Update Version with Commit Hash run: | From ecae2d2c1ca4d8ae6596cfc052d7d9c1768cf6cb Mon Sep 17 00:00:00 2001 From: Paul_GD Date: Sun, 28 Jul 2024 03:47:35 +0200 Subject: [PATCH 72/82] feat(settings): add pre-release information to update screen Display the latest pre-release version and its release notes on the update screen. Also, show the pre-release date if beta updates are enabled. This enables rollbacks. Fixed release dates --- lib/settings/update_screen.dart | 51 ++++++++++++++++++++++----------- 1 file changed, 34 insertions(+), 17 deletions(-) diff --git a/lib/settings/update_screen.dart b/lib/settings/update_screen.dart index 33f18e6..eb02f88 100644 --- a/lib/settings/update_screen.dart +++ b/lib/settings/update_screen.dart @@ -43,9 +43,11 @@ class UpdateScreenState extends State { bool _isFirmwareSpoofingEnabled = false; bool _betaUpdatesOverride = false; bool _rateLimitExceeded = false; + bool _preRelease = false; String _latestVersion = ''; String _commitDate = ''; + String _releaseDate = ''; String _releaseNotes = ''; String _currentVersion = ''; String _release = 'BRANCH_dev'; @@ -103,6 +105,7 @@ class UpdateScreenState extends State { final String latestVersion = jsonResponse['tag_name'] .replaceAll('v', ''); // Remove 'v' prefix if present final String releaseNotes = jsonResponse['body']; + final String releaseDate = jsonResponse['published_at']; _logger.info('Latest version: $latestVersion'); if (_isNewerVersion(latestVersion, _currentVersion)) { // Find the asset URL for orion_armv7.tar.gz @@ -114,6 +117,7 @@ class UpdateScreenState extends State { setState(() { _latestVersion = latestVersion; _releaseNotes = releaseNotes; + _releaseDate = releaseDate; _isLoading = false; _isUpdateAvailable = true; _assetUrl = assetUrl; // Set the asset URL @@ -174,13 +178,13 @@ class UpdateScreenState extends State { final response = await http.get(Uri.parse(url)); if (response.statusCode == 200) { final jsonResponse = json.decode(response.body) as List; - final preRelease = jsonResponse.firstWhere( - (release) => - release['tag_name'].equals(release), + final releaseItem = jsonResponse.firstWhere( + (releaseItem) => releaseItem['tag_name'] == release, orElse: () => null); - if (preRelease != null) { - final String latestVersion = preRelease['tag_name']; - final String commitSha = preRelease['target_commitish']; + + if (releaseItem != null) { + final String latestVersion = releaseItem['tag_name']; + final String commitSha = releaseItem['target_commitish']; final commitUrl = 'https://api.github.com/repos/thecontrappostoshop/orion/commits/$commitSha'; final commitResponse = await http.get(Uri.parse(commitUrl)); @@ -204,21 +208,25 @@ class UpdateScreenState extends State { } // Find the asset URL for orion_armv7.tar.gz - final asset = preRelease['assets'].firstWhere( + final asset = releaseItem['assets'].firstWhere( (asset) => asset['name'] == 'orion_armv7.tar.gz', orElse: () => null); final String assetUrl = asset != null ? asset['browser_download_url'] : ''; _logger.info('Latest pre-release version: $latestVersion'); + final bool preRelease = releaseItem['prerelease']; + _logger.info('Pre-release: $preRelease'); setState(() { _latestVersion = '$shortCommitSha ($release)'; // Append release name - _releaseNotes = commitMessage; + _releaseNotes = + preRelease ? commitMessage : releaseItem['body']; _commitDate = commitDate; // Store commit date _isLoading = false; _isUpdateAvailable = true; _rateLimitExceeded = false; _assetUrl = assetUrl; // Set the asset URL + _preRelease = preRelease; }); return; // Exit the function after successful fetch } else { @@ -348,14 +356,26 @@ class UpdateScreenState extends State { Row( children: [ _betaUpdatesOverride - ? PhosphorIcon(PhosphorIcons.knife()) + ? _preRelease + ? PhosphorIcon( + PhosphorIcons.knife(), + color: Colors.red, + size: 30, + ) + : PhosphorIcon( + PhosphorIcons.arrowCounterClockwise(), + color: + Theme.of(context).colorScheme.primary, + size: 30) : Icon(Icons.system_update, color: Theme.of(context).colorScheme.primary, size: 30), const SizedBox(width: 10), Text( _betaUpdatesOverride - ? 'Bleeding Edge Available!' + ? _preRelease + ? 'Bleeding Edge Available!' + : 'Rollback Available!' : 'UI Update Available!', style: const TextStyle( fontSize: 26, fontWeight: FontWeight.bold)), @@ -364,14 +384,16 @@ class UpdateScreenState extends State { const Divider(), Text( _betaUpdatesOverride - ? 'Latest Commit: $_latestVersion' + ? _preRelease + ? 'Latest Commit: $_latestVersion' + : 'Rollback to: ${_latestVersion.split('(')[1].split(')')[0]}' : 'Latest Version: ${_latestVersion.split('+')[0]}', style: const TextStyle(fontSize: 22)), const SizedBox(height: 10), Text( _betaUpdatesOverride ? 'Commit Date: ${_commitDate.split('T')[0]}' // Display commit date if beta updates are enabled - : 'Release Date: ${_getFormattedDate()}', + : 'Release Date: ${_releaseDate.split('T')[0]}', style: const TextStyle(fontSize: 20, color: Colors.grey), ), const SizedBox(height: 10), @@ -456,11 +478,6 @@ class UpdateScreenState extends State { ); } - String _getFormattedDate() { - // Placeholder function to return a formatted date - return '2023-10-01'; - } - Future _performUpdate(BuildContext context) async { final String localUser = Platform.environment['USER'] ?? 'pi'; final String upgradeFolder = '/home/$localUser/orion_upgrade/'; From 33361993cb531877e6c98e11f17f0e86504f1573 Mon Sep 17 00:00:00 2001 From: Paul_GD Date: Sun, 28 Jul 2024 15:31:41 +0200 Subject: [PATCH 73/82] chore(settings): update release override functions to properly reflect their functionality --- lib/settings/general_screen.dart | 59 ++++++++++++++++++-------------- lib/settings/update_screen.dart | 4 +-- 2 files changed, 35 insertions(+), 28 deletions(-) diff --git a/lib/settings/general_screen.dart b/lib/settings/general_screen.dart index ce6f740..ca7a8fe 100644 --- a/lib/settings/general_screen.dart +++ b/lib/settings/general_screen.dart @@ -40,9 +40,9 @@ class GeneralCfgScreenState extends State { late bool useCustomUrl; late String customUrl; late bool developerMode; - late bool betaOverride; + late bool releaseOverride; late bool overrideUpdateCheck; - late String overrideBranch; + late String overrideRelease; late bool verboseLogging; late bool selfDestructMode; @@ -64,10 +64,11 @@ class GeneralCfgScreenState extends State { useCustomUrl = config.getFlag('useCustomUrl', category: 'advanced'); customUrl = config.getString('customUrl', category: 'advanced'); developerMode = config.getFlag('developerMode', category: 'advanced'); - betaOverride = config.getFlag('betaOverride', category: 'developer'); + releaseOverride = config.getFlag('releaseOverride', category: 'developer'); overrideUpdateCheck = config.getFlag('overrideUpdateCheck', category: 'developer'); - overrideBranch = config.getString('overrideBranch', category: 'developer'); + overrideRelease = + config.getString('overrideRelease', category: 'developer'); verboseLogging = config.getFlag('verboseLogging', category: 'developer'); selfDestructMode = config.getFlag('selfDestructMode', category: 'topsecret'); @@ -275,7 +276,8 @@ class GeneralCfgScreenState extends State { onPressed: () { Navigator.of(context).pop(); }, - child: const Text('Close'), + child: const Text('Close', + style: TextStyle(fontSize: 20)), ), TextButton( onPressed: () { @@ -289,7 +291,8 @@ class GeneralCfgScreenState extends State { }); Navigator.of(context).pop(); }, - child: const Text('Confirm'), + child: const Text('Confirm', + style: TextStyle(fontSize: 20)), ), ], ); @@ -359,19 +362,19 @@ class GeneralCfgScreenState extends State { ), const SizedBox(height: 20.0), OrionListTile( - title: 'Beta Override', + title: 'Release Tag Override', icon: PhosphorIcons.download(), - value: betaOverride, + value: releaseOverride, onChanged: (bool value) { setState(() { - betaOverride = value; - config.setFlag('betaOverride', betaOverride, + releaseOverride = value; + config.setFlag('releaseOverride', releaseOverride, category: 'developer'); }); }, ), - if (betaOverride) const SizedBox(height: 20.0), - if (betaOverride) + if (releaseOverride) const SizedBox(height: 20.0), + if (releaseOverride) Row( children: [ Expanded( @@ -417,23 +420,27 @@ class GeneralCfgScreenState extends State { onPressed: () { Navigator.of(context).pop(); }, - child: const Text('Close'), + child: const Text('Close', + style: + TextStyle(fontSize: 20)), ), TextButton( onPressed: () { setState(() { - overrideBranch = + overrideRelease = branchTextFieldKey .currentState! .getCurrentText(); config.setString( - 'overrideBranch', - overrideBranch, + 'overrideRelease', + overrideRelease, category: 'developer'); }); Navigator.of(context).pop(); }, - child: const Text('Confirm'), + child: const Text('Confirm', + style: + TextStyle(fontSize: 20)), ), ], ); @@ -441,9 +448,9 @@ class GeneralCfgScreenState extends State { ); }, child: Text( - overrideBranch == '' - ? 'Set Branch' - : overrideBranch, + overrideRelease == '' + ? 'Set Release Tag' + : overrideRelease, style: const TextStyle(fontSize: 22)), ), ), @@ -454,17 +461,17 @@ class GeneralCfgScreenState extends State { height: 55, child: ElevatedButton( style: ElevatedButton.styleFrom(elevation: 3), - onPressed: overrideBranch == '' + onPressed: overrideRelease == '' ? null : () { setState(() { - overrideBranch = ''; - config.setString('overrideBranch', - overrideBranch, + overrideRelease = ''; + config.setString('overrideRelease', + overrideRelease, category: 'developer'); }); }, - child: const Text('Clear Branch', + child: const Text('Clear Release Tag', style: TextStyle(fontSize: 22)), ), ), @@ -473,7 +480,7 @@ class GeneralCfgScreenState extends State { ), const SizedBox(height: 20.0), OrionListTile( - title: 'Ignore Update Check', + title: 'Force Update', icon: PhosphorIcons.warning(), value: overrideUpdateCheck, onChanged: (bool value) { diff --git a/lib/settings/update_screen.dart b/lib/settings/update_screen.dart index eb02f88..ffe57b4 100644 --- a/lib/settings/update_screen.dart +++ b/lib/settings/update_screen.dart @@ -63,8 +63,8 @@ class UpdateScreenState extends State { _isFirmwareSpoofingEnabled = _config.getFlag('overrideUpdateCheck', category: 'developer'); _betaUpdatesOverride = - _config.getFlag('betaOverride', category: 'developer'); - _release = _config.getString('overrideBranch', category: 'developer'); + _config.getFlag('releaseOverride', category: 'developer'); + _release = _config.getString('overrideRelease', category: 'developer'); _logger.info('Firmware spoofing enabled: $_isFirmwareSpoofingEnabled'); _logger.info('Beta updates override enabled: $_betaUpdatesOverride'); _logger.info('Release channel override: $_release'); From 37de81b78eb25fb00ed38fafd53659a6fb6e6026 Mon Sep 17 00:00:00 2001 From: Paul_GD Date: Sat, 3 Aug 2024 01:36:16 +0200 Subject: [PATCH 74/82] fix(github): replace deprecated set-output usage https://github.blog/changelog/2022-10-11-github-actions-deprecating-save-state-and-set-output-commands/ --- .github/workflows/build.yml | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 8136a87..4bd4a59 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -24,7 +24,7 @@ jobs: VERSION=$(yq '.version' pubspec.yaml) # Strip build metadata if it exists VERSION=$(echo $VERSION | sed 's/+.*//') - echo "::set-output name=result::$VERSION" + echo "result=$VERSION" >> $GITHUB_OUTPUT - name: Update Version with Commit Hash run: | @@ -44,10 +44,12 @@ jobs: mv build/flutter_assets $BUILD_NAME ( cd $BUILD_NAME && tar -czvf $BUILD_NAME.tar.gz * ) cp $BUILD_NAME/$BUILD_NAME.tar.gz $BUILD_NAME.tar.gz + - uses: actions/upload-artifact@v4.3.1 with: name: orion_armv7 path: orion_armv7/* + - name: Build, Copy, and Compress aarch64 run: | BUILD_NAME=orion_aarch64 @@ -55,10 +57,12 @@ jobs: mv build/flutter_assets $BUILD_NAME ( cd $BUILD_NAME && tar -czvf $BUILD_NAME.tar.gz * ) cp $BUILD_NAME/$BUILD_NAME.tar.gz $BUILD_NAME.tar.gz + - uses: actions/upload-artifact@v4.3.1 with: name: orion_aarch64 path: orion_aarch64/* + - name: Build, Copy, and Compress x64 run: | BUILD_NAME=orion_x64 @@ -66,10 +70,12 @@ jobs: mv build/flutter_assets $BUILD_NAME ( cd $BUILD_NAME && tar -czvf $BUILD_NAME.tar.gz * ) cp $BUILD_NAME/$BUILD_NAME.tar.gz $BUILD_NAME.tar.gz + - uses: actions/upload-artifact@v4.3.1 with: name: orion_x64 path: orion_x64/* + - uses: ncipollo/release-action@v1.14.0 if: github.ref == 'refs/heads/main' with: @@ -78,6 +84,7 @@ jobs: skipIfReleaseExists: true generateReleaseNotes: true commit: ${{github.sha}} + - uses: ncipollo/release-action@v1.14.0 if: github.ref != 'refs/heads/main' with: @@ -87,4 +94,4 @@ jobs: allowUpdates: true prerelease: true generateReleaseNotes: true - commit: ${{github.sha}} + commit: ${{github.sha}} \ No newline at end of file From 3c43a7188c7203dcd4ef451cbb37a50a623d66b1 Mon Sep 17 00:00:00 2001 From: Paul_GD Date: Sat, 3 Aug 2024 01:37:47 +0200 Subject: [PATCH 75/82] chore(assets): remove unused thumbnail placeholder --- assets/images/thumbnail800x480.png | Bin 52565 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 assets/images/thumbnail800x480.png diff --git a/assets/images/thumbnail800x480.png b/assets/images/thumbnail800x480.png deleted file mode 100644 index ef95b5ac43f2c2a16df3766a54224f9f54168bbc..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 52565 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 f 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 Date: Sat, 3 Aug 2024 01:43:10 +0200 Subject: [PATCH 76/82] chore(files): remove debug print statements in GridFilesScreen --- lib/files/grid_files_screen.dart | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/lib/files/grid_files_screen.dart b/lib/files/grid_files_screen.dart index 7f65cf2..b0ce90a 100644 --- a/lib/files/grid_files_screen.dart +++ b/lib/files/grid_files_screen.dart @@ -145,14 +145,6 @@ class GridFilesScreenState extends State { final List items = [...dirs, ...files]; if (items.isNotEmpty) {} - /*if (kDebugMode) { - print('---------------------------------------'); - print("Device: ${_isUSB ? 'USB' : 'Internal'}"); - print("Parent Path: $_parentPath"); - print("Subdirectory: $_subdirectory"); - print("Fetched: ${files.length} files and ${dirs.length} directories."); - }*/ - setState(() { _isLoading = false; }); @@ -168,6 +160,7 @@ class GridFilesScreenState extends State { } } + // TODO: Re-implement sorting /*void _toggleSortOrder() { setState(() { _items.sort((a, b) { From 811e9fca935c91187b64a20e65b130c7424ae331 Mon Sep 17 00:00:00 2001 From: Paul_GD Date: Sat, 3 Aug 2024 02:02:19 +0200 Subject: [PATCH 77/82] chore(settings): add TODO markers for Odyssey versioning and updating --- lib/settings/about_screen.dart | 1 + lib/settings/update_screen.dart | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/settings/about_screen.dart b/lib/settings/about_screen.dart index 35b50d5..575a4af 100644 --- a/lib/settings/about_screen.dart +++ b/lib/settings/about_screen.dart @@ -49,6 +49,7 @@ Future getRaspberryPiModel() async { } } +// TODO: Implement Odyssey version fetching, awaiting API Future getVersionNumber() async { return 'Orion ${Pubspec.version}' ' - Odyssey 1.0.0'; } diff --git a/lib/settings/update_screen.dart b/lib/settings/update_screen.dart index ffe57b4..f0703a3 100644 --- a/lib/settings/update_screen.dart +++ b/lib/settings/update_screen.dart @@ -457,7 +457,7 @@ class UpdateScreenState extends State { ), ), ), - // Placeholder for Odyssey updater - pending API changes + // TODO: Placeholder for Odyssey updater - pending API changes const Card.outlined( elevation: 1, child: Padding( From 17dd19d100d306003192341c4a4ca3d23aad545d Mon Sep 17 00:00:00 2001 From: Paul_GD Date: Sat, 3 Aug 2024 02:03:28 +0200 Subject: [PATCH 78/82] chore(wifi_screen): remove commented code and unused variable --- lib/settings/wifi_screen.dart | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/lib/settings/wifi_screen.dart b/lib/settings/wifi_screen.dart index 31a10e7..9a94185 100644 --- a/lib/settings/wifi_screen.dart +++ b/lib/settings/wifi_screen.dart @@ -127,10 +127,6 @@ class _WifiScreenState extends State { {bool alreadyConnected = false}) async { wifiNetworks.clear(); try { - /*if (Theme.of(context).platform == TargetPlatform.macOS && - !alreadyConnected) { - currentWifiSSID = 'test'; - }*/ ProcessResult? result; switch (Theme.of(context).platform) { case TargetPlatform.macOS: @@ -151,7 +147,6 @@ class _WifiScreenState extends State { 'SECURITY': '(WPA2)' }); } - //if (!alreadyConnected) currentWifiSSID = networks.first['SSID']; return networks; case TargetPlatform.linux: platform = 'linux'; @@ -201,11 +196,6 @@ class _WifiScreenState extends State { final RegExpMatch? match = pattern.firstMatch(lines[i]); if (match != null) { - /*print('---------------------------'); - for (int i = 1; i < match.groupCount; i++) { - print('Group $i: ${match.group(i)}'); - }*/ - if (platform == 'macos') { networks.add({ 'SSID': match.group(1) ?? '', From 1b774fbcd46410040f5d20e12f4ed3612ec64140 Mon Sep 17 00:00:00 2001 From: Paul_GD Date: Sat, 3 Aug 2024 02:05:16 +0200 Subject: [PATCH 79/82] chore(tools): add TODO marker for Self-Test --- lib/tools/tools_screen.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/tools/tools_screen.dart b/lib/tools/tools_screen.dart index eb998d8..aa7533b 100644 --- a/lib/tools/tools_screen.dart +++ b/lib/tools/tools_screen.dart @@ -63,6 +63,7 @@ class ToolsScreenState extends State { icon: Icon(Icons.lightbulb), label: 'Exposure', ), + // TODO: Implement Self Test /*BottomNavigationBarItem( icon: Icon(Icons.check), label: 'Self Test', From 571fe619796851c487f8b885cbd517a8cfc4a7d3 Mon Sep 17 00:00:00 2001 From: Paul_GD Date: Sat, 3 Aug 2024 02:13:35 +0200 Subject: [PATCH 80/82] chore(pubspec): remove unused thumbnail placeholder from pubspec --- macos/Podfile.lock | 2 +- pubspec.yaml | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/macos/Podfile.lock b/macos/Podfile.lock index 683f245..a6c46ed 100644 --- a/macos/Podfile.lock +++ b/macos/Podfile.lock @@ -38,4 +38,4 @@ SPEC CHECKSUMS: PODFILE CHECKSUM: ae543142af37865437ba92bdbda0c110b25e440b -COCOAPODS: 1.14.3 +COCOAPODS: 1.15.2 diff --git a/pubspec.yaml b/pubspec.yaml index 15f77e0..efb906c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -95,7 +95,6 @@ flutter: - assets/images/opensource.svg - assets/images/placeholder.png - assets/images/bsod.png - - assets/images/thumbnail800x480.png - README.md - CHANGELOG.md From 34f8b4bbb34367a14d65e2d1ba2c5495ad53d45e Mon Sep 17 00:00:00 2001 From: Paul_GD Date: Sat, 3 Aug 2024 03:24:26 +0200 Subject: [PATCH 81/82] fix(util): remove redundant tenary check in circleProgress --- lib/util/status_card.dart | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/lib/util/status_card.dart b/lib/util/status_card.dart index 93c3ff4..965baf4 100644 --- a/lib/util/status_card.dart +++ b/lib/util/status_card.dart @@ -57,9 +57,7 @@ class StatusCardState extends State { (widget.isPausing && widget.status['paused'] != true) || (widget.isCanceling && widget.status['layer'] != null) ? null - : widget.progress == 0.0 - ? 1.0 - : 1.0; + : widget.progress; // If the print is active, not paused, canceled or finished, it is active. final isActive = (widget.isPausing == false && From d51d0b3020a30981e5f31d693921ff2f66f1146d Mon Sep 17 00:00:00 2001 From: Paul_GD Date: Sat, 3 Aug 2024 03:26:13 +0200 Subject: [PATCH 82/82] chore(orionpi.sh): update SSH command in orionpi.sh to use the correct user directory --- orionpi.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/orionpi.sh b/orionpi.sh index a3fef4c..8442ac5 100755 --- a/orionpi.sh +++ b/orionpi.sh @@ -178,7 +178,7 @@ fi printf "\n\r[\033[0;32m✓\033[0m]\033[0;32m%s\033[0m\n" " Running OrionPi on Raspberry Pi!" printf "\r[i]"" Press \033[0;31mCtrl+C\033[0m to disconnect.\n\n" if [ "$release" = true ]; then - sshpass -p "$password" ssh $user@$ip 'flutter-pi --release --pixelformat=RGB565 /home/pi/orion' + sshpass -p "$password" ssh $user@$ip 'flutter-pi --release --pixelformat=RGB565 /home/$user/orion' else - sshpass -p "$password" ssh $user@$ip 'flutter-pi --pixelformat=RGB565 /home/pi/orion' + sshpass -p "$password" ssh $user@$ip 'flutter-pi --pixelformat=RGB565 /home/$user/orion' fi