diff --git a/assets/images/logos_compacted.png b/assets/images/logos_compacted.png new file mode 100644 index 0000000..9c3593d Binary files /dev/null and b/assets/images/logos_compacted.png differ diff --git a/lib/pages/info_page.dart b/lib/pages/info_page.dart new file mode 100644 index 0000000..26e0b94 --- /dev/null +++ b/lib/pages/info_page.dart @@ -0,0 +1,99 @@ +import 'package:flutter/material.dart'; +import 'package:lg_space_visualizations/pages/template_page.dart'; +import 'package:lg_space_visualizations/utils/styles.dart'; + +/// A [InfoPage] widget that displays information about the Space Visualizations project. +/// +/// It includes a description of the project and the logos. +class InfoPage extends StatefulWidget { + const InfoPage({super.key}); + + @override + _InfoPageState createState() => _InfoPageState(); +} + +class _InfoPageState extends State { + @override + Widget build(BuildContext context) { + return TemplatePage( + title: 'Info', + showTopBar: true, + children: [ + Expanded( + flex: 3, + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(borderRadius), + color: backgroundColor, + ), + child: Padding( + padding: EdgeInsets.all(spaceBetweenWidgets), + child: Column( + children: [ + Image.asset('assets/images/logo.png', fit: BoxFit.contain), + const Spacer(), + Image.asset('assets/images/logos_compacted.png', + fit: BoxFit.contain), + ], + ), + ), + ), + ), + SizedBox(width: spaceBetweenWidgets), + Expanded( + flex: 7, + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(borderRadius), + color: backgroundColor, + ), + padding: EdgeInsets.only( + left: spaceBetweenWidgets, + right: spaceBetweenWidgets, + bottom: spaceBetweenWidgets, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Center( + child: Text( + "Space Visualizations", + style: hugeTitle.apply(color: primaryColor), + ), + ), + Transform.translate( + offset: const Offset(0, -25), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Container( + color: primaryColor, + height: 10, + width: 175, + margin: const EdgeInsets.only(right: 25), + ), + Text( + "for Liquid Galaxy", + style: bigTitle.apply(color: primaryColor), + ), + Container( + color: primaryColor, + height: 10, + width: 175, + margin: const EdgeInsets.only(left: 25), + ), + ], + ), + ), + Text( + """This project aims to build an educational application dedicated to visualizing orbits and the Mars 2020 mission, utilizing the Liquid Galaxy platform to provide immersive space exploration experiences. The app enables users to see and understand different orbits, such as GPS, QZSS, Graveyard and more in detail. Additionally, it showcases the Mars 2020 mission by featuring interactive 3D models of the Perseverance Rover and the Ingenuity Drone. Users can follow their paths on Mars, view photos taken by the rover, and discover technical details about the mission.""", + style: middleText, + ), + ], + ), + ), + ), + ], + ); + } +} diff --git a/lib/pages/services_page.dart b/lib/pages/services_page.dart new file mode 100644 index 0000000..6f4c4ad --- /dev/null +++ b/lib/pages/services_page.dart @@ -0,0 +1,232 @@ +import 'package:flutter/material.dart'; +import 'package:lg_space_visualizations/pages/template_page.dart'; +import 'package:lg_space_visualizations/utils/styles.dart'; +import 'package:lg_space_visualizations/widget/button.dart'; +import 'package:lg_space_visualizations/widget/custom_icon.dart'; +import 'package:lg_space_visualizations/utils/lg_connection.dart'; +import 'package:lg_space_visualizations/widget/custom_dialog.dart'; + +/// A [ServicesPage] widget for managing services related to the Liquid Galaxy system. +/// +/// Those services include relaunching, clearing KMLs, rebooting, setting refresh.They are used for managing the system remotely. +class ServicesPage extends StatefulWidget { + const ServicesPage({super.key}); + + @override + _ServicesPageState createState() => _ServicesPageState(); +} + +class _ServicesPageState extends State { + /// Displays a dialog indicating that the Liquid Galaxy is not connected. + void showNotConnectedDialog(BuildContext context) { + showDialog( + context: context, + builder: (BuildContext context) { + return const CustomDialog( + title: 'Error', + content: 'The Liquid Galaxy is not connected.\nPlease connect to the rig and try again.', + iconName: 'error', + ); + }, + ); + } + + @override + Widget build(BuildContext context) { + return TemplatePage( + title: 'Services', + children: [ + Expanded( + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(borderRadius), + color: backgroundColor, + ), + padding: EdgeInsets.only( + top: spaceBetweenWidgets, + left: 2 * spaceBetweenWidgets, + right: 2 * spaceBetweenWidgets, + bottom: spaceBetweenWidgets, + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + _buildServiceButton( + context, + icon: 'relaunch', + text: 'LG\nRELAUNCH', + onPressed: () async { + if (await lgConnection.isConnected()) { + lgConnection.relaunch(); + showDialog( + context: context, + builder: (BuildContext context) { + return const CustomDialog( + title: 'Reboot', + content: 'The relaunch command has been sent.\nThe Liquid Galaxy will relaunch in a few seconds.', + iconName: 'relaunch', + ); + }, + ); + } else { + showNotConnectedDialog(context); + } + }, + ), + _buildServiceButton( + context, + icon: 'clear', + text: 'CLEAR\nKML', + onPressed: () async { + if (await lgConnection.isConnected()) { + lgConnection.clearKml(); + showDialog( + context: context, + builder: (BuildContext context) { + return const CustomDialog( + title: 'Clear KML', + content: 'The Clear KML command has been sent to the Liquid Galaxy.\nThe KMLs will be cleared in a few seconds.', + iconName: 'clear', + ); + }, + ); + } else { + showNotConnectedDialog(context); + } + }, + ), + _buildServiceButton( + context, + icon: 'reboot', + text: 'LG\nREBOOT', + onPressed: () async { + if (await lgConnection.isConnected()) { + lgConnection.reboot(); + showDialog( + context: context, + builder: (BuildContext context) { + return const CustomDialog( + title: 'Reboot', + content: 'The reboot command has been sent.\nThe Liquid Galaxy will reboot in a few seconds.', + iconName: 'reboot', + ); + }, + ); + } else { + showNotConnectedDialog(context); + } + }, + ), + ], + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + _buildServiceButton( + context, + icon: 'setrefresh', + text: 'SET\nREFRESH', + onPressed: () async { + if (await lgConnection.isConnected()) { + lgConnection.setRefresh(); + showDialog( + context: context, + builder: (BuildContext context) { + return const CustomDialog( + title: 'Set Refresh', + content: 'The Set Refresh command has been sent.\nThe Liquid Galaxy will set refresh and reboot in a few seconds.', + iconName: 'setrefresh', + ); + }, + ); + } else { + showNotConnectedDialog(context); + } + }, + ), + _buildServiceButton( + context, + icon: 'resetrefresh', + text: 'RESET\nREFRESH', + onPressed: () async { + if (await lgConnection.isConnected()) { + lgConnection.resetRefresh(); + showDialog( + context: context, + builder: (BuildContext context) { + return const CustomDialog( + title: 'Reset Refresh', + content: 'The Reset Refresh command has been sent.\nThe Liquid Galaxy will reset the refresh and reboot in a few seconds.', + iconName: 'resetrefresh', + ); + }, + ); + } else { + showNotConnectedDialog(context); + } + }, + ), + _buildServiceButton( + context, + icon: 'shutdown', + text: 'LG\nSHUTDOWN', + onPressed: () async { + if (await lgConnection.isConnected()) { + lgConnection.shutdown(); + showDialog( + context: context, + builder: (BuildContext context) { + return const CustomDialog( + title: 'Shutdown', + content: 'The shutdown command has been sent.\nThe Liquid Galaxy will shutdown in a few seconds.', + iconName: 'shutdown', + ); + }, + ); + } else { + showNotConnectedDialog(context); + } + }, + ), + ], + ), + ], + ), + ), + ), + ], + ); + } + + /// Builds a service button for the services page. + /// + /// [context] is the build context. + /// [icon] is the name of the icon to display. + /// [text] is the text to display on the button. + /// [onPressed] is the function to call when the button is pressed. + Widget _buildServiceButton(BuildContext context, + {required String icon, + required String text, + required VoidCallback onPressed}) { + return SizedBox( + height: 250, + width: 250, + child: Button( + icon: CustomIcon( + name: icon, + size: 90, + color: backgroundColor, + ), + text: text, + color: secondaryColor, + multiLine: true, + bold: true, + borderRadius: BorderRadius.circular(borderRadius), + onPressed: onPressed, + ), + ); + } +} diff --git a/lib/pages/settings_page.dart b/lib/pages/settings_page.dart new file mode 100644 index 0000000..7e0d1ee --- /dev/null +++ b/lib/pages/settings_page.dart @@ -0,0 +1,338 @@ +import 'package:flutter/material.dart'; +import 'package:lg_space_visualizations/utils/styles.dart'; +import 'package:lg_space_visualizations/pages/template_page.dart'; +import 'package:lg_space_visualizations/widget/custom_icon.dart'; +import 'package:lg_space_visualizations/widget/button.dart'; +import 'package:lg_space_visualizations/widget/input.dart'; +import 'package:lg_space_visualizations/widget/custom_dialog.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:lg_space_visualizations/utils/lg_connection.dart'; + +/// A [SettingsPage] widget for configuring settings related to the Liquid Galaxy connection. +class SettingsPage extends StatefulWidget { + const SettingsPage({super.key}); + + @override + _SettingsPageState createState() => _SettingsPageState(); +} + +class _SettingsPageState extends State { + /// Controller for the username input field. + final TextEditingController usernameController = TextEditingController(); + + /// Controller for the IP address input field. + final TextEditingController ipController = TextEditingController(); + + /// Controller for the port input field. + final TextEditingController portController = TextEditingController(); + + /// Controller for the password input field. + final TextEditingController passwordController = TextEditingController(); + + /// Controller for the NASA API key input field. + final TextEditingController apiKeyController = TextEditingController(); + + /// Indicates whether the fields have been loaded with saved preferences data. + bool loaded = false; + + /// Updates the input fields with saved preferences data. + updateFields() async { + SharedPreferences prefs = await SharedPreferences.getInstance(); + + usernameController.text = prefs.getString('lg_username') ?? ''; + ipController.text = prefs.getString('lg_ip') ?? ''; + portController.text = + prefs.containsKey('lg_port') ? prefs.getInt('lg_port').toString() : ''; + passwordController.text = prefs.getString('lg_password') ?? ''; + apiKeyController.text = prefs.getString('nasa_key') ?? ''; + + loaded = true; + } + + @override + void initState() { + super.initState(); + updateFields(); + } + + @override + Widget build(BuildContext context) { + bool isKeyboardVisible = MediaQuery.of(context).viewInsets.bottom != 0; + + return TemplatePage( + title: 'Settings', + children: [ + Expanded( + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(borderRadius), + color: backgroundColor, + ), + padding: EdgeInsets.only( + top: spaceBetweenWidgets, + left: 2 * spaceBetweenWidgets, + right: 2 * spaceBetweenWidgets, + bottom: spaceBetweenWidgets, + ), + child: Column( + children: [ + Align( + alignment: Alignment.centerLeft, + child: Text( + 'Connect to the Liquid Galaxy', + style: middleTitle, + textAlign: TextAlign.left, + ), + ), + SizedBox(height: spaceBetweenWidgets), + Expanded( + child: SingleChildScrollView( + child: Column( + children: [ + _buildRow( + context, + icon: 'user', + label: 'Username', + controller: usernameController, + hintText: 'Enter Liquid Galaxy username', + inputType: TextInputType.text, + id: 'lg_username', + dialogTitle: 'Username', + dialogContent: + 'Enter the username of the Liquid Galaxy. This is the username\nof the master computer that is running the Liquid Galaxy.', + ), + SizedBox(height: spaceBetweenWidgets), + _buildRow( + context, + icon: 'ip', + label: 'IP', + controller: ipController, + hintText: 'Enter Liquid Galaxy IP address', + inputType: TextInputType.number, + id: 'lg_ip', + dialogTitle: 'IP Address', + dialogContent: + 'Enter the IP address of the Liquid Galaxy. This is the IP\nof the master computer running Liquid Galaxy.', + ), + SizedBox(height: spaceBetweenWidgets), + _buildRow( + context, + icon: 'ethernet', + label: 'Port', + controller: portController, + hintText: 'Enter Liquid Galaxy Port', + inputType: TextInputType.number, + id: 'lg_port', + dialogTitle: 'Port', + dialogContent: + 'Enter the port of the SSH service of the Liquid\nGalaxy master. Default is 22', + ), + SizedBox(height: spaceBetweenWidgets), + _buildRow( + context, + icon: 'locker', + label: 'Password', + controller: passwordController, + hintText: 'Enter password', + inputType: TextInputType.text, + id: 'lg_password', + secure: true, + dialogTitle: 'Password', + dialogContent: + 'Enter the password of the Liquid Galaxy. This is the password\nof the master computer running Liquid Galaxy.', + ), + SizedBox(height: spaceBetweenWidgets), + _buildRow( + context, + icon: 'api', + label: 'Nasa API Key', + controller: apiKeyController, + hintText: 'Enter Nasa API Key (optional)', + inputType: TextInputType.text, + id: 'nasa_key', + secure: true, + dialogTitle: 'Nasa API Key', + dialogContent: + 'Enter the NASA API key. This is optional and is used to\nfetch data for the MARS 2020 section.', + ), + ], + ), + ), + ), + if (!isKeyboardVisible) _buildButtons(context), + ], + ), + ), + ), + ], + ); + } + + /// Builds a row containing an icon, label, input field, and info button. + Widget _buildRow(BuildContext context, + {required String icon, + required String label, + required TextEditingController controller, + required String hintText, + required TextInputType inputType, + required String id, + bool secure = false, + required String dialogTitle, + required String dialogContent}) { + return Row( + children: [ + CustomIcon(name: icon, size: 50, color: secondaryColor), + SizedBox(width: spaceBetweenWidgets), + SizedBox( + width: 160, + child: Text(label, style: bigText, textAlign: TextAlign.left), + ), + Input( + controller: controller, + hintText: hintText, + inputType: inputType, + id: id, + secure: secure, + ), + SizedBox(width: spaceBetweenWidgets), + Button( + icon: CustomIcon( + name: 'info', size: 40, color: secondaryColor), + onPressed: () { + showDialog( + context: context, + builder: (BuildContext context) { + return CustomDialog( + title: dialogTitle, + content: dialogContent, + iconName: 'info'); + }, + ); + }, + ), + ], + ); + } + + /// Builds the connect and disconnect buttons. + Widget _buildButtons(BuildContext context) { + return Column( + children: [ + const SizedBox(height: 30), + SizedBox( + height: 50, + child: Button( + icon: CustomIcon( + name: 'connect', + size: 40, + color: backgroundColor, + ), + text: 'Connect', + color: secondaryColor, + borderRadius: BorderRadius.circular(borderRadius), + onPressed: () async { + SharedPreferences prefs = await SharedPreferences.getInstance(); + + if (usernameController.text.isEmpty || + ipController.text.isEmpty || + portController.text.isEmpty || + passwordController.text.isEmpty) { + showDialog( + context: context, + builder: (BuildContext context) { + return const CustomDialog( + title: 'Missing Fields', + content: 'Please fill in all the required fields.', + iconName: 'error', + ); + }, + ); + } else { + prefs.setString('lg_username', usernameController.text); + prefs.setString('lg_ip', ipController.text); + prefs.setInt('lg_port', int.parse(portController.text)); + prefs.setString('lg_password', passwordController.text); + // TODO encrypt api key + prefs.setString('nasa_key', apiKeyController.text); + showDialog( + context: context, + builder: (BuildContext context) { + return FutureBuilder( + future: lgConnection.connect(), + builder: (context, snapshot) { + if (snapshot.hasData) { + if (snapshot.data!) { + return const CustomDialog( + title: 'Connection successful', + content: + 'Liquid Galaxy connected successfully.', + iconName: 'connect', + ); + } else { + return const CustomDialog( + title: 'Connection Error', + content: + 'An error occurred while connecting.\nPlease check the Liquid Galaxy status\nand verify the connection settings.', + iconName: 'error', + ); + } + } else { + return Center( + child: SizedBox( + width: 100.0, + height: 100.0, + child: CircularProgressIndicator( + strokeWidth: 8, + color: backgroundColor))); + } + }); + }, + ); + } + }, + ), + ), + SizedBox(height: spaceBetweenWidgets / 1.5), + SizedBox( + height: 50, + child: Button( + icon: CustomIcon( + name: 'disconnect', + size: 40, + color: backgroundColor, + ), + text: 'Disconnect', + color: secondaryColor, + borderRadius: BorderRadius.circular(borderRadius), + onPressed: () async { + if (await lgConnection.isConnected()) { + await lgConnection.disconnect(); + showDialog( + context: context, + builder: (BuildContext context) { + return const CustomDialog( + title: 'Disconnected', + content: 'Liquid Galaxy disconnected successfully.', + iconName: 'disconnect', + ); + }, + ); + } else { + showDialog( + context: context, + builder: (BuildContext context) { + return const CustomDialog( + title: 'Not Connected', + content: 'Liquid Galaxy is already disconnected.', + iconName: 'error', + ); + }, + ); + } + }, + ), + ), + ], + ); + } +} diff --git a/lib/utils/costants.dart b/lib/utils/costants.dart new file mode 100644 index 0000000..46fa159 --- /dev/null +++ b/lib/utils/costants.dart @@ -0,0 +1,7 @@ +/// The URL of the master +String lgUrl = 'lg1:81'; + +/// Images url +String logosUrl = 'https://i.ibb.co/1JW3Dvq/logos-2.png'; +String droneImageUrl = 'https://i.ibb.co/qB5kHm3/licensed-image.jpg'; + diff --git a/lib/utils/kml/ballon_maker.dart b/lib/utils/kml/ballon_maker.dart new file mode 100644 index 0000000..6f25d59 --- /dev/null +++ b/lib/utils/kml/ballon_maker.dart @@ -0,0 +1,67 @@ +import 'kml_makers.dart'; +import 'package:lg_space_visualizations/utils/costants.dart'; + +/// Class responsible for generating KML balloons for various visualizations. +class BalloonMaker { + + /// Generates a KML balloon for the Perseverance Rover. + /// + /// The balloon contains an image and a description of the Perseverance Rover. + /// + /// Returns a string containing the KML balloon. + static String generatePerseveranceRoverBalloon() { + // TODO: change the image and description + return KMLMakers.screenOverlayBalloon( + '''
+
+
+ +
+

Perseverance Rover

+

+ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam risus leo, rutrum nec sollicitudin porta, fermentum ac dolor. Sed sed egestas nibh. Morbi augue justo, malesuada finibus felis ac, molestie pharetra sem. Vivamus interdum mi magna, ut auctor nequeì maximus non. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae. +

+

+

Space Visualizations | Liquid Galaxy | GSoC 2024

+
+
'''); + } + + /// Generates a KML balloon for the Ingenuity Helicopter. + /// + /// The balloon contains an image and a description of the Ingenuity Helicopter. + /// + /// Returns a string containing the KML balloon. + static String generateIngenuityHelicopterBalloon() { + // TODO: change the image and description + return KMLMakers.screenOverlayBalloon( + '''
+
+
+ +
+

Ingenuity Drone

+

+ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam risus leo, rutrum nec sollicitudin porta, fermentum ac dolor. Sed sed egestas nibh. Morbi augue justo, malesuada finibus felis ac, molestie pharetra sem. Vivamus interdum mi magna, ut auctor nequeì maximus non. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae. +

+

+

Space Visualizations | Liquid Galaxy | GSoC 2024

+
+
'''); + } + + /// Generates a KML balloon with a custom [description] and [name] of an orbit. + /// + /// Returns a string containing the KML balloon. + static String generateOrbitBalloon(String name, String description) { + return KMLMakers.screenOverlayBalloon( + '''
+
+

$name

+

$description

+

+

Space Visualizations | Liquid Galaxy | GSoC 2024

+
+
'''); + } +} diff --git a/lib/utils/kml/kml_makers.dart b/lib/utils/kml/kml_makers.dart new file mode 100644 index 0000000..e85db18 --- /dev/null +++ b/lib/utils/kml/kml_makers.dart @@ -0,0 +1,103 @@ +/// This class is used to generate the KML code from the given parameters. +class KMLMakers { + /// Generates KML code for a screen overlay image. + /// + /// [imageUrl] is the URL of the image to be overlaid. + /// [factor] is the scaling factor for the image size. + /// + /// Returns a string containing the KML code for the screen overlay image. + static String screenOverlayImage(String imageUrl, double factor) => + ''' + + +'''; + + /// Generates KML code for a balloon overlay. + /// + /// The balloon overlay includes the HTML content provided. + /// + /// [htmlContent] is a string that contains the HTML content to be displayed in the balloon overlay. + /// + /// Returns a string containing the KML code for the balloon overlay. + static String screenOverlayBalloon(String htmlContent) { + return ''' + + + Balloon + 1 + + #purple_paddle + 1 + + +'''; + } + + /// Generates a blank KML document. + /// + /// [id] is the identifier for the document. + /// + /// Returns a string containing the KML code for a blank document. + static String generateBlank(String id) => + ''' + + + +'''; + + /// Generates KML code for a LookAt element with linear motion. + /// + /// [latitude] is the latitude of the location. + /// [longitude] is the longitude of the location. + /// [zoom] is the zoom level. + /// [tilt] is the tilt angle. + /// [bearing] is the bearing angle. + /// + /// Returns a string containing the KML code for the LookAt element. + static String lookAtLinear(double latitude, double longitude, double zoom, double tilt, double bearing) => + '$longitude$latitude$zoom$tilt$bearingrelativeToGround'; + + /// Generates KML code for an orbiting LookAt element with linear motion. + /// + /// [latitude] is the latitude of the location. + /// [longitude] is the longitude of the location. + /// [zoom] is the zoom level. + /// [tilt] is the tilt angle. + /// [bearing] is the bearing angle. + /// + /// Returns a string containing the KML code for the orbiting LookAt element. + static String orbitLookAtLinear(double latitude, double longitude, double zoom, double tilt, double bearing) => + '60smooth$longitude$latitude$zoom$tilt$bearingrelativeToGround'; + + /// Generates KML code for an instant LookAt element with linear motion. + /// + /// [latitude] is the latitude of the location. + /// [longitude] is the longitude of the location. + /// [zoom] is the zoom level. + /// [tilt] is the tilt angle. + /// [bearing] is the bearing angle. + /// + /// Returns a string containing the KML code for the instant LookAt element. + static String lookAtLinearInstant(double latitude, double longitude, double zoom, double tilt, double bearing) => + '0.5smooth$longitude$latitude$zoom$tilt$bearingrelativeToGround'; +} diff --git a/lib/utils/lg_connection.dart b/lib/utils/lg_connection.dart new file mode 100644 index 0000000..17c6c4a --- /dev/null +++ b/lib/utils/lg_connection.dart @@ -0,0 +1,309 @@ +import 'dart:io'; +import 'package:lg_space_visualizations/utils/costants.dart'; +import 'package:ssh2/ssh2.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:lg_space_visualizations/utils/kml/kml_makers.dart'; + +/// Global instance of [LGConnection]. +LGConnection lgConnection = LGConnection(); + +/// Manages a connection to the Liquid Galaxy system using SSH. +class LGConnection { + /// SSH client for managing the connection. + SSHClient? client; + + /// Number of screens in the Liquid Galaxy system. + int? screenAmount; + + /// Creates an [LGConnection] instance. + LGConnection(); + + /// Checks if the SSH client is connected. + /// + /// Returns `true` if connected, `false` otherwise. + Future isConnected() async { + return client == null ? false : await client!.isConnected(); + } + + /// Disconnects the SSH client if connected. + Future disconnect() async { + if (await isConnected()) { + await client!.disconnect(); + } + } + + /// Connects to the Liquid Galaxy system + /// + /// Returns `true` if the connection is successful, `false` otherwise. + Future connect() async { + SharedPreferences prefs = await SharedPreferences.getInstance(); + + if (!prefs.containsKey('lg_ip') || + !prefs.containsKey('lg_port') || + !prefs.containsKey('lg_username') || + !prefs.containsKey('lg_password')) { + return false; + } + + client = SSHClient( + host: prefs.getString('lg_ip')!, + port: prefs.getInt('lg_port')!, + username: prefs.getString('lg_username')!, + passwordOrKey: prefs.getString('lg_password')!, + ); + + try { + final connectionResult = + await client!.connect().timeout(const Duration(seconds: 8)); + if (connectionResult == 'session_connected') { + screenAmount = await getScreenAmount(); + await showLogos(); + return true; + } else { + return false; + } + } catch (e) { + return false; + } + } + + /// Retrieves the number of screens in the Liquid Galaxy system. + /// + /// Returns the number of screens, or default value 1 if not connected or on failure + Future getScreenAmount() async { + if (await isConnected() == false) { + return 1; + } + + String screenAmount = await client! + .execute("grep -oP '(?<=DHCP_LG_FRAMES_MAX=).*' personavars.txt") ?? + '1'; + + return int.parse(screenAmount); + } + + /// Sends a KML file to the Liquid Galaxy system. + /// + /// [kml] is the KML content to send. + Future sendKml(String kml) async { + if (await isConnected() == false) { + return; + } + + const fileName = 'upload.kml'; + + final directory = await getApplicationDocumentsDirectory(); + final file = File('${directory.path}/$fileName'); + file.writeAsStringSync(kml); + + await client!.connectSFTP(); + await client!.sftpUpload( + path: file.path, + toPath: '/var/www/html'); + await client! + .execute('echo "http://{$lgUrl}/$fileName" > /var/www/html/kmls.txt'); + } + + /// Sends a KML file to a specific slave screen. + /// + /// [screenNumber] is the screen number. + /// [kml] is the KML content to send. + Future sendKMLToSlave(int screenNumber, String kml) async { + if (await isConnected() == false) { + return; + } + + try { + await client! + .execute("echo '$kml' > /var/www/html/kml/slave_$screenNumber.kml"); + } catch (e) { + print(e); + } + } + + /// Gets the left screen number. + int get leftScreen { + if (screenAmount == 1) { + return 1; + } + return screenAmount ?? 5; + } + + /// Gets the right screen number. + int get rightScreen { + if (screenAmount == 1) { + return 1; + } + return (screenAmount ?? 4) - 1; + } + + /// Relaunches the Liquid Galaxy services. + Future relaunch() async { + if (await isConnected() == false) { + return; + } + + final pw = client!.passwordOrKey; + final user = client!.username; + + for (var i = screenAmount ?? 5; i >= 1; i--) { + try { + final relaunchCommand = """RELAUNCH_CMD="\\ +if [ -f /etc/init/lxdm.conf ]; then + export SERVICE=lxdm +elif [ -f /etc/init/lightdm.conf ]; then + export SERVICE=lightdm +else + exit 1 +fi +if [[ \\\$(service \\\$SERVICE status) =~ 'stop' ]]; then + echo $pw | sudo -S service \\\${SERVICE} start +else + echo $pw | sudo -S service \\\${SERVICE} restart +fi +" && sshpass -p $pw ssh -x -t lg@lg$i "\$RELAUNCH_CMD\""""; + await client! + .execute('"/home/$user/bin/lg-relaunch" > /home/$user/log.txt'); + await client!.execute(relaunchCommand); + } catch (e) { + print(e); + } + } + } + + /// Clears the KML files from the Liquid Galaxy system. + /// + /// [keepLogos] indicates whether to keep the logo overlays. + Future clearKml({bool keepLogos = false}) async { + if (await isConnected() == false) { + return; + } + String query = + 'echo "exittour=true" > /tmp/query.txt && > /var/www/html/kmls.txt'; + + for (var i = 2; i <= (screenAmount ?? 5); i++) { + String blankKml = KMLMakers.generateBlank('slave_$i'); + query += " && echo '$blankKml' > /var/www/html/kml/slave_$i.kml"; + } + + await client!.execute(query); + + if (keepLogos) { + await showLogos(); + } + } + + /// Display the logos on the last screen of the Liquid Galaxy. + Future showLogos() async { + if (await isConnected() == false) { + return; + } + + await sendKMLToSlave( + leftScreen, + KMLMakers.screenOverlayImage( + logosUrl, 4032 / 4024)); + } + + /// Reboots the Liquid Galaxy system. + Future reboot() async { + if (await isConnected() == false) { + return; + } + + final pw = client!.passwordOrKey; + + for (var i = (screenAmount ?? 5); i >= 1; i--) { + try { + await client! + .execute('sshpass -p $pw ssh -t lg$i "echo $pw | sudo -S reboot"'); + } catch (e) { + // ignore: avoid_print + print(e); + } + } + } + + /// Sets the refresh interval + Future setRefresh() async { + if (await isConnected() == false) { + return; + } + + final pw = client!.passwordOrKey; + + const search = '##LG_PHPIFACE##kml\\/slave_{{slave}}.kml<\\/href>'; + const replace = + '##LG_PHPIFACE##kml\\/slave_{{slave}}.kml<\\/href>onInterval<\\/refreshMode>2<\\/refreshInterval>'; + final command = + 'echo $pw | sudo -S sed -i "s/$search/$replace/" ~/earth/kml/slave/myplaces.kml'; + + final clear = + 'echo $pw | sudo -S sed -i "s/$replace/$search/" ~/earth/kml/slave/myplaces.kml'; + + for (var i = 2; i <= (screenAmount ?? 5); i++) { + final clearCmd = clear.replaceAll('{{slave}}', i.toString()); + final cmd = command.replaceAll('{{slave}}', i.toString()); + String query = 'sshpass -p $pw ssh -t lg$i \'{{cmd}}\''; + + try { + await client!.execute(query.replaceAll('{{cmd}}', clearCmd)); + await client!.execute(query.replaceAll('{{cmd}}', cmd)); + } catch (e) { + // ignore: avoid_print + print(e); + } + } + + await reboot(); + } + + /// Resets the refresh interval + Future resetRefresh() async { + if (await isConnected() == false) { + return; + } + + final pw = client!.passwordOrKey; + + const search = + '##LG_PHPIFACE##kml\\/slave_{{slave}}.kml<\\/href>onInterval<\\/refreshMode>2<\\/refreshInterval>'; + const replace = '##LG_PHPIFACE##kml\\/slave_{{slave}}.kml<\\/href>'; + + final clear = + 'echo $pw | sudo -S sed -i "s/$search/$replace/" ~/earth/kml/slave/myplaces.kml'; + + for (var i = 2; i <= (screenAmount ?? 5); i++) { + final cmd = clear.replaceAll('{{slave}}', i.toString()); + String query = 'sshpass -p $pw ssh -t lg$i \'$cmd\''; + + try { + await client!.execute(query); + } catch (e) { + // ignore: avoid_print + print(e); + } + } + + await reboot(); + } + + /// Shuts down the Liquid Galaxy system. + Future shutdown() async { + if (await isConnected() == false) { + return; + } + + final pw = client!.passwordOrKey; + + for (var i = screenAmount ?? 5; i >= 1; i--) { + try { + await client!.execute( + 'sshpass -p $pw ssh -t lg$i "echo $pw | sudo -S poweroff"'); + } catch (e) { + print(e); + } + } + } +} diff --git a/lib/utils/routes.dart b/lib/utils/routes.dart index 8dc2c32..0473f38 100644 --- a/lib/utils/routes.dart +++ b/lib/utils/routes.dart @@ -1,6 +1,9 @@ import 'package:flutter/material.dart'; import 'package:lg_space_visualizations/pages/home_page.dart'; import 'package:lg_space_visualizations/pages/splash_screen.dart'; +import 'package:lg_space_visualizations/pages/services_page.dart'; +import 'package:lg_space_visualizations/pages/settings_page.dart'; +import 'package:lg_space_visualizations/pages/info_page.dart'; /// Generates a [Route] for the application based on the provided [RouteSettings]. /// @@ -18,6 +21,18 @@ Route makeRoute(RouteSettings settings) { // Route for the splash screen. builder = (BuildContext context) => const SplashPage(); break; + case '/settings': + // Route for the settings page. + builder = (BuildContext context) => const SettingsPage(); + break; + case '/services': + // Route for the services page. + builder = (BuildContext context) => const ServicesPage(); + break; + case '/info': + // Route for the info screen. + builder = (BuildContext context) => const InfoPage(); + break; default: // Default route if no match is found, redirects to the home page. builder = (BuildContext context) => const HomePage(); diff --git a/lib/widget/bottom_bar.dart b/lib/widget/bottom_bar.dart index 57dd969..0c43435 100644 --- a/lib/widget/bottom_bar.dart +++ b/lib/widget/bottom_bar.dart @@ -3,101 +3,113 @@ import 'package:lg_space_visualizations/utils/styles.dart'; import 'package:lg_space_visualizations/widget/button.dart'; import 'package:lg_space_visualizations/widget/custom_icon.dart'; import 'package:lg_space_visualizations/widget/led_status.dart'; +import 'package:lg_space_visualizations/utils/lg_connection.dart'; /// Bottom navigation bar with various buttons and a status indicator. /// /// The [BottomBar] contains buttons for navigating to the home, settings, service, and info pages, /// with a LED status indicator. It provides a common bottom navigation interface across the application. -class BottomBar extends StatelessWidget { +class BottomBar extends StatefulWidget { const BottomBar({super.key}); + @override + _BottomBarState createState() => _BottomBarState(); +} + +class _BottomBarState extends State { @override Widget build(BuildContext context) { + final currentRoute = ModalRoute.of(context)?.settings.name; + return Container( margin: EdgeInsets.only( - bottom: spaceBetweenWidgets, - left: spaceBetweenWidgets, - right: spaceBetweenWidgets, - ), + bottom: spaceBetweenWidgets, + left: spaceBetweenWidgets, + right: spaceBetweenWidgets), height: barHeight, decoration: BoxDecoration( borderRadius: BorderRadius.circular(borderRadius), color: backgroundColor, ), child: Padding( - padding: const EdgeInsets.only(left: 20, right: 20, top: 5, bottom: 5), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Button( - borderRadius: BorderRadius.circular(borderRadius), - color: secondaryColor, - icon: CustomIcon( - name: 'home', - width: 35, - height: 35, - color: backgroundColor, - ), - padding: - const EdgeInsets.only(left: 25, right: 25, top: 8, bottom: 8), - onPressed: () { - // Navigate to home only if the current route is not home ('/') - if (ModalRoute.of(context)!.settings.name != '/') { - Navigator.pushNamed(context, '/'); - } - }, - ), - const Spacer(), - Button( - color: Colors.transparent, - borderRadius: BorderRadius.circular(0), - icon: CustomIcon( - name: "settings", - color: secondaryColor, - width: 40, - height: 40, - ), - onPressed: () { - // Navigate to settings only if the current route is not settings ('/settings') - if (ModalRoute.of(context)!.settings.name != '/settings') { - Navigator.pushNamed(context, '/settings'); - } - }, - ), - Container(width: 35), - Button( - color: Colors.transparent, - borderRadius: BorderRadius.circular(0), - icon: CustomIcon( - name: "services", - color: secondaryColor, - width: 40, - height: 40, - ), - onPressed: () { - // Navigate to service only if the current route is not service ('/service') - if (ModalRoute.of(context)!.settings.name != '/service') { - Navigator.pushNamed(context, '/service'); - } - }, - ), - Container(width: 35), - Button( - color: Colors.transparent, - borderRadius: BorderRadius.circular(0), - icon: CustomIcon( - name: "info", - color: secondaryColor, - width: 40, - height: 40, - ), - onPressed: () {}, - ), - Container(width: 40), - const LedStatus(status: false, size: 35) - ], - ), - ), + padding: + const EdgeInsets.only(left: 20, right: 20, top: 5, bottom: 5), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Button( + borderRadius: BorderRadius.circular(borderRadius), + color: secondaryColor, + icon: CustomIcon( + name: 'home', + size: 35, + color: backgroundColor, + ), + padding: const EdgeInsets.only( + left: 25, right: 25, top: 8, bottom: 8), + onPressed: () { + // Navigate to home only if the current route is not home ('/') + if (currentRoute != '/') { + Navigator.pushNamed(context, '/'); + } + }), + const Spacer(), + Button( + color: Colors.transparent, + borderRadius: BorderRadius.circular(0), + icon: CustomIcon( + name: "settings", + color: secondaryColor, + size: 40, + ), + onPressed: () { + // Navigate to settings only if the current route is not settings ('/settings') + if (currentRoute != '/settings') { + Navigator.pushNamed(context, '/settings'); + } + }), + Container(width: 35), + Button( + color: Colors.transparent, + borderRadius: BorderRadius.circular(0), + icon: CustomIcon( + name: "services", + color: secondaryColor, + size: 40, + ), + onPressed: () { + // Navigate to service only if the current route is not service ('/service') + if (currentRoute != '/services') { + Navigator.pushNamed(context, '/services'); + } + }), + Container(width: 35), + Button( + color: Colors.transparent, + borderRadius: BorderRadius.circular(0), + icon: CustomIcon( + name: "info", + color: secondaryColor, + size: 40, + ), + onPressed: () { + // Navigate to info only if the current route is not info ('/info') + if (currentRoute != '/info') { + Navigator.pushNamed(context, '/info'); + } + }), + Container(width: 40), + FutureBuilder( + future: lgConnection.isConnected(), + builder: (context, snapshot) { + if (snapshot.hasData) { + return LedStatus(status: snapshot.data!, size: 35); + } else { + return const LedStatus(status: false, size: 35); + } + }) + ], + )), ); } } diff --git a/lib/widget/custom_dialog.dart b/lib/widget/custom_dialog.dart new file mode 100644 index 0000000..2b1afbb --- /dev/null +++ b/lib/widget/custom_dialog.dart @@ -0,0 +1,62 @@ +import 'package:flutter/material.dart'; +import 'package:lg_space_visualizations/widget/button.dart'; +import 'package:lg_space_visualizations/widget/custom_icon.dart'; +import 'package:lg_space_visualizations/utils/styles.dart'; + +/// A [CustomDialog] widget that displays a [title], an optional [content] message, and an [CustomIcon] defined from [iconName]. +/// +/// The dialog also includes a "Back" button that closes the dialog when pressed. +class CustomDialog extends StatelessWidget { + /// The title text to display in the dialog. + final String title; + + /// The content text to display in the dialog. Can be null. + final String? content; + + /// The name of the icon to display next to the title. + final String iconName; + + const CustomDialog({ + super.key, + required this.title, + this.content, + required this.iconName, + }); + + @override + Widget build(BuildContext context) { + return AlertDialog( + title: Row( + children: [ + CustomIcon( + name: iconName, + size: 30, + color: secondaryColor, + ), + const SizedBox(width: 10), + Text(title, style: middleTitle), + ], + ), + content: content != null ? Text(content!, style: middleText) : null, + actions: [ + SizedBox( + height: 50, + child: Button( + icon: CustomIcon( + name: 'back', + size: 30, + color: backgroundColor, + ), + text: 'Back', + color: secondaryColor, + onPressed: () { + Navigator.of(context).pop(); + }, + padding: const EdgeInsets.all(10), + borderRadius: BorderRadius.circular(borderRadius), + ), + ), + ], + ); + } +} diff --git a/lib/widget/custom_icon.dart b/lib/widget/custom_icon.dart index 7f00942..e77534d 100644 --- a/lib/widget/custom_icon.dart +++ b/lib/widget/custom_icon.dart @@ -3,16 +3,13 @@ import 'package:flutter/material.dart'; /// A widget that displays a custom icon from the asset folder. /// /// The [CustomIcon] widget allows you to display a custom icon by providing the -/// [name] of the icon file with its [width], [height], and [color]. +/// [name] of the icon file with its [size], and [color]. class CustomIcon extends StatelessWidget { /// The name of the icon file (without extension) to be displayed. final String name; - /// The width of the icon. - final double width; - - /// The height of the icon. - final double height; + /// The size of the icon. + final double size; /// The color to apply to the icon. final Color color; @@ -20,8 +17,7 @@ class CustomIcon extends StatelessWidget { const CustomIcon({ super.key, required this.name, - required this.width, - required this.height, + required this.size, required this.color, }); @@ -29,8 +25,7 @@ class CustomIcon extends StatelessWidget { Widget build(BuildContext context) { return Image.asset( 'assets/icons/$name.png', - width: width, - height: height, + height: size, color: color, ); } diff --git a/lib/widget/input.dart b/lib/widget/input.dart new file mode 100644 index 0000000..bf17a63 --- /dev/null +++ b/lib/widget/input.dart @@ -0,0 +1,101 @@ +import 'package:flutter/material.dart'; +import 'package:lg_space_visualizations/widget/button.dart'; +import 'package:lg_space_visualizations/utils/styles.dart'; +import 'package:lg_space_visualizations/widget/custom_icon.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +/// A custom [Input] widget that supports secure text entry and saves input to shared preferences. +/// +/// The [hintText], [inputType], [controller], and [id] parameters are required. +/// The [secure] parameter defaults to `false`. +class Input extends StatefulWidget { + /// The hint text to display in the input field. + final String hintText; + + /// The type of keyboard to use for the input field. + final TextInputType inputType; + + /// The controller to manage the input field's text. + final TextEditingController controller; + + /// Whether the input field should have an obscure function. + final bool secure; + + /// The unique identifier for saving the input field's value to shared preferences. + final String id; + + const Input({ + super.key, + required this.hintText, + required this.inputType, + required this.controller, + required this.id, + this.secure = false, + }); + + @override + _InputState createState() => _InputState(); +} + +class _InputState extends State { + /// Indicates whether the text should be hidden. + bool hide = false; + + /// Initializes the state of the widget. + /// + /// Sets the [hide] variable to the value of the [secure] property of the widget. + @override + void initState() { + super.initState(); + hide = widget.secure; + } + + @override + Widget build(BuildContext context) { + return Expanded( + child: TextField( + controller: widget.controller, + keyboardType: widget.inputType, + obscureText: hide, + onChanged: (text) async { + SharedPreferences prefs = await SharedPreferences.getInstance(); + if (widget.controller.text.isNotEmpty) { + prefs.setString(widget.id, widget.controller.text); + } else { + prefs.remove(widget.id); + } + }, + decoration: InputDecoration( + suffixIcon: widget.secure + ? Button( + icon: CustomIcon( + size: 10, + name: hide ? "see" : "hide", + color: secondaryColor, + ), + color: Colors.transparent, + onPressed: () { + setState(() { + hide = !hide; + }); + }, + padding: const EdgeInsets.all(10), + ) + : null, + border: OutlineInputBorder( + borderSide: BorderSide.none, + borderRadius: BorderRadius.circular(10.0), + ), + hintText: widget.hintText, + filled: true, + fillColor: grey, + contentPadding: const EdgeInsets.only( + left: 20.0, top: 15.0, bottom: 15.0, right: 20.0), + hintStyle: TextStyle( + color: primaryColor, + ), + ), + ), + ); + } +} diff --git a/lib/widget/top_bar.dart b/lib/widget/top_bar.dart index df27eaa..3c8479b 100644 --- a/lib/widget/top_bar.dart +++ b/lib/widget/top_bar.dart @@ -42,8 +42,7 @@ class TopBar extends StatelessWidget implements PreferredSizeWidget { color: backgroundColor, icon: CustomIcon( name: 'back', - width: 50, - height: 50, + size: 50, color: primaryColor, ), onPressed: () { diff --git a/pubspec.lock b/pubspec.lock index d3f6f2a..4e853b8 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -41,6 +41,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.18.0" + crypto: + dependency: transitive + description: + name: crypto + sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab + url: "https://pub.dev" + source: hosted + version: "3.0.3" cupertino_icons: dependency: "direct main" description: @@ -57,6 +65,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.1" + ffi: + dependency: transitive + description: + name: ffi + sha256: "493f37e7df1804778ff3a53bd691d8692ddf69702cf4c1c1096a2e41b4779e21" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + file: + dependency: transitive + description: + name: file + sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c" + url: "https://pub.dev" + source: hosted + version: "7.0.0" flutter: dependency: "direct main" description: flutter @@ -75,6 +99,11 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" leak_tracker: dependency: transitive description: @@ -139,6 +168,126 @@ packages: url: "https://pub.dev" source: hosted version: "1.9.0" + path_provider: + dependency: "direct main" + description: + name: path_provider + sha256: c9e7d3a4cd1410877472158bee69963a4579f78b68c65a2b7d40d1a7a88bb161 + url: "https://pub.dev" + source: hosted + version: "2.1.3" + path_provider_android: + dependency: transitive + description: + name: path_provider_android + sha256: a248d8146ee5983446bf03ed5ea8f6533129a12b11f12057ad1b4a67a2b3b41d + url: "https://pub.dev" + source: hosted + version: "2.2.4" + path_provider_foundation: + dependency: transitive + description: + name: path_provider_foundation + sha256: f234384a3fdd67f989b4d54a5d73ca2a6c422fa55ae694381ae0f4375cd1ea16 + url: "https://pub.dev" + source: hosted + version: "2.4.0" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 + url: "https://pub.dev" + source: hosted + version: "2.2.1" + path_provider_platform_interface: + dependency: transitive + description: + name: path_provider_platform_interface + sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + path_provider_windows: + dependency: transitive + description: + name: path_provider_windows + sha256: "8bc9f22eee8690981c22aa7fc602f5c85b497a6fb2ceb35ee5a5e5ed85ad8170" + url: "https://pub.dev" + source: hosted + version: "2.2.1" + platform: + dependency: transitive + description: + name: platform + sha256: "12220bb4b65720483f8fa9450b4332347737cf8213dd2840d8b2c823e47243ec" + url: "https://pub.dev" + source: hosted + version: "3.1.4" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" + url: "https://pub.dev" + source: hosted + version: "2.1.8" + shared_preferences: + dependency: "direct main" + description: + name: shared_preferences + sha256: d3bbe5553a986e83980916ded2f0b435ef2e1893dfaa29d5a7a790d0eca12180 + url: "https://pub.dev" + source: hosted + version: "2.2.3" + shared_preferences_android: + dependency: transitive + description: + name: shared_preferences_android + sha256: "1ee8bf911094a1b592de7ab29add6f826a7331fb854273d55918693d5364a1f2" + url: "https://pub.dev" + source: hosted + version: "2.2.2" + shared_preferences_foundation: + dependency: transitive + description: + name: shared_preferences_foundation + sha256: "0a8a893bf4fd1152f93fec03a415d11c27c74454d96e2318a7ac38dd18683ab7" + url: "https://pub.dev" + source: hosted + version: "2.4.0" + shared_preferences_linux: + dependency: transitive + description: + name: shared_preferences_linux + sha256: "9f2cbcf46d4270ea8be39fa156d86379077c8a5228d9dfdb1164ae0bb93f1faa" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + shared_preferences_platform_interface: + dependency: transitive + description: + name: shared_preferences_platform_interface + sha256: "22e2ecac9419b4246d7c22bfbbda589e3acf5c0351137d87dd2939d984d37c3b" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + shared_preferences_web: + dependency: transitive + description: + name: shared_preferences_web + sha256: "9aee1089b36bd2aafe06582b7d7817fd317ef05fc30e6ba14bff247d0933042a" + url: "https://pub.dev" + source: hosted + version: "2.3.0" + shared_preferences_windows: + dependency: transitive + description: + name: shared_preferences_windows + sha256: "841ad54f3c8381c480d0c9b508b89a34036f512482c407e6df7a9c4aa2ef8f59" + url: "https://pub.dev" + source: hosted + version: "2.3.2" sky_engine: dependency: transitive description: flutter @@ -152,6 +301,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.10.0" + ssh2: + dependency: "direct main" + description: + name: ssh2 + sha256: ef00a87a03248cf7cd375322c26ad7465520041cdd0e5cb6d2cd356d57fb59ff + url: "https://pub.dev" + source: hosted + version: "2.2.3" stack_trace: dependency: transitive description: @@ -192,6 +349,22 @@ packages: url: "https://pub.dev" source: hosted version: "0.6.1" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c + url: "https://pub.dev" + source: hosted + version: "1.3.2" + uuid: + dependency: transitive + description: + name: uuid + sha256: "648e103079f7c64a36dc7d39369cabb358d377078a051d6ae2ad3aa539519313" + url: "https://pub.dev" + source: hosted + version: "3.0.7" vector_math: dependency: transitive description: @@ -208,5 +381,30 @@ packages: url: "https://pub.dev" source: hosted version: "13.0.0" + web: + dependency: transitive + description: + name: web + sha256: "97da13628db363c635202ad97068d47c5b8aa555808e7a9411963c533b449b27" + url: "https://pub.dev" + source: hosted + version: "0.5.1" + win32: + dependency: transitive + description: + name: win32 + sha256: "0eaf06e3446824099858367950a813472af675116bf63f008a4c2a75ae13e9cb" + url: "https://pub.dev" + source: hosted + version: "5.5.0" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + sha256: faea9dee56b520b55a566385b84f2e8de55e7496104adada9962e0bd11bcff1d + url: "https://pub.dev" + source: hosted + version: "1.0.4" sdks: dart: ">=3.3.0 <4.0.0" + flutter: ">=3.19.0" diff --git a/pubspec.yaml b/pubspec.yaml index cdc7553..e0ef6d3 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -16,7 +16,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.1.0+1 +version: 0.2.0+1 environment: sdk: '>=3.3.0 <4.0.0' @@ -35,6 +35,9 @@ dependencies: # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons. cupertino_icons: ^1.0.6 + shared_preferences: ^2.2.3 + ssh2: ^2.2.3 + path_provider: ^2.1.3 dev_dependencies: flutter_test: