From 01e9b309b4977cf241290a9e2e4306284b1c0b75 Mon Sep 17 00:00:00 2001
From: Felix Bruns <felix@bruns.hamburg>
Date: Thu, 9 May 2024 12:31:42 +0200
Subject: [PATCH] Initial commit

Contains a skeleton which displays VMs using cards and has a settings page to configure host+user+pass.
---
 README.md                                     |  25 ++-
 analysis_options.yaml                         |   4 +
 assets/images/2.0x/flutter_logo.png           | Bin 0 -> 619 bytes
 assets/images/3.0x/flutter_logo.png           | Bin 0 -> 810 bytes
 assets/images/flutter_logo.png                | Bin 0 -> 419 bytes
 lib/main.dart                                 |  20 +++
 lib/src/app.dart                              |  65 ++++++++
 .../proxmox_lister_list_view.dart             |  44 ++++++
 lib/src/proxmox_lister/proxomx_vm.dart        |  12 ++
 lib/src/proxmox_lister/vm_card.dart           |  57 +++++++
 lib/src/proxmox_webservice/service.dart       |  16 ++
 lib/src/settings/settings_controller.dart     |  67 ++++++++
 lib/src/settings/settings_service.dart        |  41 +++++
 lib/src/settings/settings_view.dart           |  83 ++++++++++
 linux/.gitignore                              |   1 +
 linux/CMakeLists.txt                          | 145 ++++++++++++++++++
 linux/flutter/CMakeLists.txt                  |  88 +++++++++++
 linux/flutter/generated_plugin_registrant.cc  |  11 ++
 linux/flutter/generated_plugin_registrant.h   |  15 ++
 linux/flutter/generated_plugins.cmake         |  23 +++
 linux/main.cc                                 |   6 +
 linux/my_application.cc                       | 124 +++++++++++++++
 linux/my_application.h                        |  18 +++
 pi_dashboard.iml                              |  17 ++
 pubspec.yaml                                  |  35 +++++
 test/unit_test.dart                           |  15 ++
 test/widget_test.dart                         |  31 ++++
 27 files changed, 962 insertions(+), 1 deletion(-)
 create mode 100644 analysis_options.yaml
 create mode 100644 assets/images/2.0x/flutter_logo.png
 create mode 100644 assets/images/3.0x/flutter_logo.png
 create mode 100644 assets/images/flutter_logo.png
 create mode 100644 lib/main.dart
 create mode 100644 lib/src/app.dart
 create mode 100644 lib/src/proxmox_lister/proxmox_lister_list_view.dart
 create mode 100644 lib/src/proxmox_lister/proxomx_vm.dart
 create mode 100644 lib/src/proxmox_lister/vm_card.dart
 create mode 100644 lib/src/proxmox_webservice/service.dart
 create mode 100644 lib/src/settings/settings_controller.dart
 create mode 100644 lib/src/settings/settings_service.dart
 create mode 100644 lib/src/settings/settings_view.dart
 create mode 100644 linux/.gitignore
 create mode 100644 linux/CMakeLists.txt
 create mode 100644 linux/flutter/CMakeLists.txt
 create mode 100644 linux/flutter/generated_plugin_registrant.cc
 create mode 100644 linux/flutter/generated_plugin_registrant.h
 create mode 100644 linux/flutter/generated_plugins.cmake
 create mode 100644 linux/main.cc
 create mode 100644 linux/my_application.cc
 create mode 100644 linux/my_application.h
 create mode 100644 pi_dashboard.iml
 create mode 100644 pubspec.yaml
 create mode 100644 test/unit_test.dart
 create mode 100644 test/widget_test.dart

diff --git a/README.md b/README.md
index 5687a28..ed49ff7 100644
--- a/README.md
+++ b/README.md
@@ -1,3 +1,26 @@
 # proxmox-dashboard
 
-Just a small flutter dashboard to view my local proxmox server
\ No newline at end of file
+A very minimal dashboard to display on my Raspberry Pi using [flutter-pi](https://github.com/ardera/flutter-pi).
+
+## Building
+
+Just as any other flutter project build it using
+```
+flutter build linux
+```
+
+Currently, only linux is supported because that's what I am building on and targetting.
+
+Running the app can be done via
+```
+flutter run
+```
+
+## Deploying
+
+To run the app on the raspberrypi boot it into CLI and follow the instructions from [flutter-pi](https://github.com/ardera/flutter-pi).
+
+Copy the build over to the pi and start it using:
+```
+flutter-pi <path-to-flutter-package>
+```
\ No newline at end of file
diff --git a/analysis_options.yaml b/analysis_options.yaml
new file mode 100644
index 0000000..0bd999b
--- /dev/null
+++ b/analysis_options.yaml
@@ -0,0 +1,4 @@
+include: package:flutter_lints/flutter.yaml
+
+linter:
+  rules:
diff --git a/assets/images/2.0x/flutter_logo.png b/assets/images/2.0x/flutter_logo.png
new file mode 100644
index 0000000000000000000000000000000000000000..b65164de707ffeaf0adfca5fca65532bf97e6903
GIT binary patch
literal 619
zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H3?#oinD`S&H3s;ExB}^b{r?#vrgK>I2kif@
zvH816@%GSTKN!MhbHpsw+WI}@_)o2^Ujq(*4>|fh<k%0!@L5d3GbPj-q?GD{kNyZf
z3KW^m6f%n?U^=_k6fTzuJdS<5w!M5--2!GE!UnCPPE*9Rnk3a5q?GGqOncQAKiAy$
zO=szIy&d2DKxY2vJfPwXbc9$*kY6x^fP#X8gF}8j5HvJ2?7t5Ng+aUZ85kIwJzX3_
zDsH{Ke%<e|gFx%UrPEt4h&wTH`{kVaZLh#`y6EJzz&A4|Ua;hU@P2x}jagdP*?pe7
z?M-Z&`Q`0uetdX%xE%*0?VH1`t8&xrkFIWB|5|$Wo6B4lwO)+t=j%WCR?PaRZkNL$
zf6)^^J~C<AhbcU7=XT=Xnew|N`jWGr-ToVX?A{+&cLi;D{4z<?ZfU~v%O`{W)Ew-%
zd^YG$%)u3xF9!W7Ie6mo)u2Bi2VY#i8T6;(Aj|UIOMh%^@~o5f)2Q3g@b<dugLbZz
zzi*i`>mwB2f9E#&lh@dHUo7LNZ{xgp@r+-ynauvLP<s3LM#0-TkEXxg&3v}0Xm8YA
zEwP-MvrV_IYiHD~ZMt?{^TjVI*4p@e2`{|GbAHzqZq^Ss`11GDNy~EGunXVsPcIJl
zU$x-1xPGr*_=R7!v8xt*?7qHGFF4?L>wNoN4|Z_>tNAZ;@h5j(?S79JcUYfB`=#qy
r6fZFpo3n_4Rl|XiE8s&sR~^H;NW15^d4qg`5ys%@>gTe~DWM4f{@o*g

literal 0
HcmV?d00001

diff --git a/assets/images/3.0x/flutter_logo.png b/assets/images/3.0x/flutter_logo.png
new file mode 100644
index 0000000000000000000000000000000000000000..97e5dc9af6e1093966c5cd42d608fd6fbb6a0c59
GIT binary patch
literal 810
zcmeAS@N?(olHy`uVBq!ia0vp^2SAvE8Azrw%`pX16#+gWu0UF2`*(-^-y@EH58eMI
z^w{s<Lq9_geb?ObjUjBd*4FPKM}LMK|H%|FTfm}IN~JdB=y$cCMVi~bg&zA6a^weN
z_-v+-Su6q5*}bN4xlG`3?BliV<+JJ*FzXNsT_9}GDyG#Wsoo%^TqkQcRb$P2pho+>
z-$IW5@LT%57HGF<NswPKgMxyBfqg(iK|K&O0KxqG_ut>UA^wwrfyu|y#WAGf*4wMk
zgAN-AxF%k#KCURBVA1=%-ucp;9Uj}ZG#l?Wes0(B%({NdIoXW<{g3WiupaK0x3By2
z<KyGw6f)l5KiOLGT<eeiqw;uvka71L?%$RAewusBkEN_te>O4s)_FGS)mJr|)n_%@
z)kihD)q6Gi)z>tJ)#o(E)yFiZ)%!H&)t5Jx)rUK-{vXl2fA4~;^<VZ}c#yCC;=}pp
z+`Rt^9GAyOe7OHFz_BGh<ir2EddDg80T<fuhYP&)*L!jNzD~zCb+Io`*NdzCn9TL-
zb2#^;Kb)+!zfQAu{(I3>_xE>GN_~ps{eO!b9`tLyxW8Xh;HNSd-@|xOj=ytR+do>D
zt9>mOkvaAL$-iq%!vCf*DgR4ol&oKHV8Z?Agj4S|4W8wT%=mnri|3a%tJ>eCOpE`;
zHb(y6c3?w&Y{ISmIsp&Mrw9G7`Sv8_P1LLTesVhEI)8;vvu5vq*tGV3&cSo}TNXSq
zj|})It+V3iSy7We$GB|jf?4DCFKlYK@8ftOf60QQ_A~Fi*3bCZZ_jpf?zg4|^)^x|
zt2OmGe$>0=M;cybjjOklN_(oQ_u<Fq^Q+=Y9S^_1tgW}==jZhg6}Wv;9goiMzu#Q7
zlr{DLtHbYS>}uL;@3elyf)C=)PemO(7H=26jVtWmubrDh3}(04)hM&xSK7O%X|>Mc
j1|}|z1*pUejUUWgK0Q%uH(H?zOn3~Qu6{1-oD!M<&k2w@

literal 0
HcmV?d00001

diff --git a/assets/images/flutter_logo.png b/assets/images/flutter_logo.png
new file mode 100644
index 0000000000000000000000000000000000000000..b5c6ca710c937527ca8d2d344214243d6db33832
GIT binary patch
literal 419
zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I3?%1nZ+ru!@&kNAT!FN~`u7^!zegPZ&Ja3_
z)pe?<YJKSbFAQO`723{fZT%i{^k>NNpCL!ThaUauw)u_bwr{%Izl9$A!5B7M$govP
zxh~|$561A>Od+#a0;aQjP2sie<+JJ*(`u4bZve_F*PR4v(b@jZ=Md1SAK%^B{{yWQ
zFA4GsW)M(NFaUyj5NMdc{{H>@?}1?b(XAk7pYe2Y42d}W_VP)gCIbQ2ix!U>4q9?~
zIsEybZ^G7cAu^%-PQ=yx*;eIK#gYnUFW+9QvQNJH+nbx4)6dKOIC(3+<zw~#(0|vu
zEiU|KUDM9G=e@YbJ>3g6;V=GFge(48{@_RM&lLju^uE+^au+>tK7OIONyesA;#Zub
zQs3&YevL0~Gu<?AWyt52uy492BGh>E6t~Fo%7BFztX3q{1RVU;;PiN_-8qleWA`_&
zw({X*ef?P{^t~Y0vFF>PCUdo}=4?%^3QRb0$VpT0kLyEL`Hh?APM<!{6zDGoPgg&e
IbxsLQ04<Qa^#A|>

literal 0
HcmV?d00001

diff --git a/lib/main.dart b/lib/main.dart
new file mode 100644
index 0000000..eb568f2
--- /dev/null
+++ b/lib/main.dart
@@ -0,0 +1,20 @@
+import 'package:flutter/material.dart';
+
+import 'src/app.dart';
+import 'src/settings/settings_controller.dart';
+import 'src/settings/settings_service.dart';
+
+void main() async {
+  // Set up the SettingsController, which will glue user settings to multiple
+  // Flutter Widgets.
+  final settingsController = SettingsController(SettingsService());
+
+  // Load the user's preferred theme while the splash screen is displayed.
+  // This prevents a sudden theme change when the app is first displayed.
+  await settingsController.loadSettings();
+
+  // Run the app and pass in the SettingsController. The app listens to the
+  // SettingsController for changes, then passes it further down to the
+  // SettingsView.
+  runApp(MyApp(settingsController: settingsController));
+}
diff --git a/lib/src/app.dart b/lib/src/app.dart
new file mode 100644
index 0000000..c5f6086
--- /dev/null
+++ b/lib/src/app.dart
@@ -0,0 +1,65 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_localizations/flutter_localizations.dart';
+import 'package:pi_dashboard/src/proxmox_lister/proxmox_lister_list_view.dart';
+
+import 'settings/settings_controller.dart';
+import 'settings/settings_view.dart';
+
+/// The Widget that configures your application.
+class MyApp extends StatelessWidget {
+  const MyApp({
+    super.key,
+    required this.settingsController,
+  });
+
+  final SettingsController settingsController;
+
+  @override
+  Widget build(BuildContext context) {
+    // Glue the SettingsController to the MaterialApp.
+    //
+    // The ListenableBuilder Widget listens to the SettingsController for changes.
+    // Whenever the user updates their settings, the MaterialApp is rebuilt.
+    return ListenableBuilder(
+      listenable: settingsController,
+      builder: (BuildContext context, Widget? child) {
+        return MaterialApp(
+          // Providing a restorationScopeId allows the Navigator built by the
+          // MaterialApp to restore the navigation stack when a user leaves and
+          // returns to the app after it has been killed while running in the
+          // background.
+          restorationScopeId: 'app',
+
+          supportedLocales: const [
+            Locale('en', ''), // English, no country code
+          ],
+
+          // Define a light and dark color theme. Then, read the user's
+          // preferred ThemeMode (light, dark, or system default) from the
+          // SettingsController to display the correct theme.
+          theme: ThemeData(),
+          darkTheme: ThemeData.dark(),
+          themeMode: settingsController.themeMode,
+
+          // Define a function to handle named routes in order to support
+          // Flutter web url navigation and deep linking.
+          onGenerateRoute: (RouteSettings routeSettings) {
+            return MaterialPageRoute<void>(
+              settings: routeSettings,
+              builder: (BuildContext context) {
+                switch (routeSettings.name) {
+                  case SettingsView.routeName:
+                    return SettingsView(controller: settingsController);
+                  case ProxmoxListerView.routeName:
+                    return ProxmoxListerView();
+                  default:
+                    return ProxmoxListerView();
+                }
+              },
+            );
+          },
+        );
+      },
+    );
+  }
+}
diff --git a/lib/src/proxmox_lister/proxmox_lister_list_view.dart b/lib/src/proxmox_lister/proxmox_lister_list_view.dart
new file mode 100644
index 0000000..d313585
--- /dev/null
+++ b/lib/src/proxmox_lister/proxmox_lister_list_view.dart
@@ -0,0 +1,44 @@
+import 'package:flutter/material.dart';
+import '../settings/settings_view.dart';
+import 'vm_card.dart';
+import 'proxomx_vm.dart';
+
+class ProxmoxListerView extends StatelessWidget {
+  ProxmoxListerView({
+    super.key,
+    this.vms = const [ProxmoxVM(id: 1, name: "template")],
+  });
+
+  static const routeName = '/proxmox_lister';
+
+  List<ProxmoxVM> vms;
+
+  @override
+  Widget build(BuildContext context) {
+    return Scaffold(
+        appBar: AppBar(
+          title: const Text("Proxmox VMs"),
+          actions: [
+            IconButton(
+              icon: const Icon(Icons.sync),
+              onPressed: () {
+                Navigator.restorablePushNamed(context, SettingsView.routeName);
+              },
+            ),
+            IconButton(
+              icon: const Icon(Icons.settings),
+              onPressed: () {},
+            ),
+          ],
+        ),
+        body: ListView.builder(
+          restorationId: "proxmoxVMLister",
+          itemCount: vms.length,
+          itemBuilder: (BuildContext ctx, int index) {
+            final vm = vms[index];
+
+            return ProxmoxVmCard(vm: vm);
+          },
+        ));
+  }
+}
diff --git a/lib/src/proxmox_lister/proxomx_vm.dart b/lib/src/proxmox_lister/proxomx_vm.dart
new file mode 100644
index 0000000..99879d6
--- /dev/null
+++ b/lib/src/proxmox_lister/proxomx_vm.dart
@@ -0,0 +1,12 @@
+
+class ProxmoxVM {
+  const ProxmoxVM({
+    this.id = 0,
+    this.name = "",
+    this.isRunning = true,
+  });
+
+  final int id;
+  final String name;
+  final bool isRunning;
+}
\ No newline at end of file
diff --git a/lib/src/proxmox_lister/vm_card.dart b/lib/src/proxmox_lister/vm_card.dart
new file mode 100644
index 0000000..2782111
--- /dev/null
+++ b/lib/src/proxmox_lister/vm_card.dart
@@ -0,0 +1,57 @@
+import 'package:flutter/material.dart';
+import 'package:pi_dashboard/src/proxmox_lister/proxomx_vm.dart';
+
+class RunningIndicator extends StatelessWidget {
+  const RunningIndicator({
+    super.key,
+    this.isRunning = false,
+  });
+  final bool isRunning;
+
+  @override
+  Widget build(BuildContext ctx) {
+    return Text(
+      isRunning ? "RUNNING" : "STOPPED",
+      style: TextStyle(
+        color: isRunning ? Colors.lightGreen[800] : Colors.blueGrey[600],
+        fontWeight: FontWeight.bold,
+      ),
+    );
+  }
+}
+
+class ProxmoxVmCard extends StatelessWidget {
+  ProxmoxVmCard({
+    super.key,
+    this.vm = const ProxmoxVM(),
+  });
+
+  final ProxmoxVM vm;
+
+  @override
+  Widget build(_) {
+    return Card(
+      child: Column(
+        children: [
+          ListTile(
+            leading: const Icon(Icons.dns),
+            title: Row(
+              children: [
+                Text(vm.name),
+                const Spacer(),
+                RunningIndicator(isRunning: vm.isRunning),
+              ]
+            ),
+            subtitle: Row(
+              children: [
+                Text("ID: ${vm.id.toString()}"),
+                const Spacer(),
+                TextButton(onPressed: () => {}, child: const Icon(Icons.power_settings_new)),
+              ],
+            ),
+          ),
+        ],
+      ),
+    );
+  }
+}
\ No newline at end of file
diff --git a/lib/src/proxmox_webservice/service.dart b/lib/src/proxmox_webservice/service.dart
new file mode 100644
index 0000000..42db019
--- /dev/null
+++ b/lib/src/proxmox_webservice/service.dart
@@ -0,0 +1,16 @@
+class ProxmoxWebService {
+  ProxmoxWebService({
+    this.hostname = "",
+    this.username = "",
+    this.password = "",
+  });
+
+  final String hostname;
+  final String username;
+  final String password;
+
+  Future<void> authenticate() async {
+    
+  }
+
+}
\ No newline at end of file
diff --git a/lib/src/settings/settings_controller.dart b/lib/src/settings/settings_controller.dart
new file mode 100644
index 0000000..4af9e1b
--- /dev/null
+++ b/lib/src/settings/settings_controller.dart
@@ -0,0 +1,67 @@
+import 'package:flutter/material.dart';
+
+import 'settings_service.dart';
+
+/// A class that many Widgets can interact with to read user settings, update
+/// user settings, or listen to user settings changes.
+///
+/// Controllers glue Data Services to Flutter Widgets. The SettingsController
+/// uses the SettingsService to store and retrieve user settings.
+class SettingsController with ChangeNotifier {
+  SettingsController(this._settingsService);
+
+  final SettingsService _settingsService;
+
+  Future<void> loadSettings() async {
+    _themeMode = await _settingsService.themeMode();
+    _hostname = await _settingsService.keyStr("hostname");
+    _username = await _settingsService.keyStr("username");
+    _password = await _settingsService.keyStr("password");
+    // Important! Inform listeners a change has occurred.
+    notifyListeners();
+  }
+
+  late ThemeMode _themeMode;
+  ThemeMode get themeMode => _themeMode;
+  late String _hostname;
+  String get hostname => _hostname;
+  late String _username;
+  String get username => _username;
+  late String _password;
+  String get password => _password;
+
+  Future<void> _setKey(String key, String value) async {
+    notifyListeners();
+    await _settingsService.setKeyStr(key, value);
+  }
+
+  Future<void> setHostname(String newHostname) async {
+    _hostname = newHostname;
+    await _setKey("hostname", hostname);
+  }
+  Future<void> setUsername(String newUsername) async {
+    _username = newUsername;
+    await _setKey("username", username);
+  }
+  Future<void> setPassword(String newPassword) async {
+    _password = newPassword;
+    await _setKey("password", password);
+  }
+  /// Update and persist the ThemeMode based on the user's selection.
+  Future<void> updateThemeMode(ThemeMode? newThemeMode) async {
+    if (newThemeMode == null) return;
+
+    // Do not perform any work if new and old ThemeMode are identical
+    if (newThemeMode == _themeMode) return;
+
+    // Otherwise, store the new ThemeMode in memory
+    _themeMode = newThemeMode;
+
+    // Important! Inform listeners a change has occurred.
+    notifyListeners();
+
+    // Persist the changes to a local database or the internet using the
+    // SettingService.
+    await _settingsService.updateThemeMode(newThemeMode);
+  }
+}
diff --git a/lib/src/settings/settings_service.dart b/lib/src/settings/settings_service.dart
new file mode 100644
index 0000000..28f7d5e
--- /dev/null
+++ b/lib/src/settings/settings_service.dart
@@ -0,0 +1,41 @@
+import 'package:flutter/cupertino.dart';
+import 'package:flutter/material.dart';
+import 'package:shared_preferences/shared_preferences.dart';
+
+/// A service that stores and retrieves user settings.
+///
+/// By default, this class does not persist user settings. If you'd like to
+/// persist the user settings locally, use the shared_preferences package. If
+/// you'd like to store settings on a web server, use the http package.
+class SettingsService {
+  /// Loads the User's preferred ThemeMode from local or remote storage.
+  Future<ThemeMode> themeMode() async {
+    final SharedPreferences prefs = await SharedPreferences.getInstance();
+    final themeIdx = prefs.getInt("theme");
+    if (themeIdx == null) return ThemeMode.system;
+
+    return ThemeMode.values[themeIdx];
+  }
+
+  /// Persists the user's preferred ThemeMode to local or remote storage.
+  Future<void> updateThemeMode(ThemeMode theme) async {
+    final SharedPreferences prefs = await SharedPreferences.getInstance();
+    prefs.setInt("theme", theme.index);
+  }
+
+  Future<String> keyStr(String key) async {
+    final SharedPreferences prefs = await SharedPreferences.getInstance();
+    final res = prefs.getString(key);
+    if (res == null) return "";
+    return res;
+  }
+  Future<void> setKeyStr(String key, String value) async {
+    final SharedPreferences prefs = await SharedPreferences.getInstance();
+    final res = await prefs.setString(key, value);
+    if(res) {
+      print("successfully saved");
+      print(key);
+      print(value);
+    }
+  }
+}
diff --git a/lib/src/settings/settings_view.dart b/lib/src/settings/settings_view.dart
new file mode 100644
index 0000000..353569a
--- /dev/null
+++ b/lib/src/settings/settings_view.dart
@@ -0,0 +1,83 @@
+import 'package:flutter/cupertino.dart';
+import 'package:flutter/material.dart';
+import 'package:pi_dashboard/src/settings/settings_service.dart';
+
+import 'settings_controller.dart';
+
+/// Displays the various settings that can be customized by the user.
+///
+/// When a user changes a setting, the SettingsController is updated and
+/// Widgets that listen to the SettingsController are rebuilt.
+class SettingsView extends StatelessWidget {
+  const SettingsView({super.key, required this.controller});
+
+  static const routeName = '/settings';
+
+  final SettingsController controller;
+
+  @override
+  Widget build(BuildContext context) {
+    return Scaffold(
+      appBar: AppBar(
+        title: const Text('Settings'),
+      ),
+      body: Padding(
+        padding: const EdgeInsets.all(16),
+        // Glue the SettingsController to the theme selection DropdownButton.
+        //
+        // When a user selects a theme from the dropdown list, the
+        // SettingsController is updated, which rebuilds the MaterialApp.
+        child: Column(children: [
+          DropdownButton<ThemeMode>(
+            // Read the selected themeMode from the controller
+            value: controller.themeMode,
+            // Call the updateThemeMode method any time the user selects a theme.
+            onChanged: controller.updateThemeMode,
+            items: const [
+              DropdownMenuItem(
+                value: ThemeMode.system,
+                child: Text('System Theme'),
+              ),
+              DropdownMenuItem(
+                value: ThemeMode.light,
+                child: Text('Light Theme'),
+              ),
+              DropdownMenuItem(
+                value: ThemeMode.dark,
+                child: Text('Dark Theme'),
+              )
+            ],
+          ),
+          TextFormField(
+            decoration: const InputDecoration(
+              border: UnderlineInputBorder(),
+              labelText: "Hostname",
+            ),          
+            initialValue: controller.hostname,
+            onChanged: controller.setHostname,
+          ),
+          TextFormField(
+            decoration: const InputDecoration(
+              border: UnderlineInputBorder(),
+              labelText: "Username",
+            ),
+            initialValue: controller.username,
+            onChanged: controller.setUsername,
+          ),
+          TextFormField(
+            decoration: const InputDecoration(
+              border: UnderlineInputBorder(),
+              labelText: "Password",
+            ),
+            obscureText: true,
+            enableSuggestions: false,
+            autocorrect: false,
+            initialValue: controller.password,
+            onChanged: controller.setPassword,
+          ),
+        ],)
+
+      ),
+    );
+  }
+}
diff --git a/linux/.gitignore b/linux/.gitignore
new file mode 100644
index 0000000..d3896c9
--- /dev/null
+++ b/linux/.gitignore
@@ -0,0 +1 @@
+flutter/ephemeral
diff --git a/linux/CMakeLists.txt b/linux/CMakeLists.txt
new file mode 100644
index 0000000..9f7b941
--- /dev/null
+++ b/linux/CMakeLists.txt
@@ -0,0 +1,145 @@
+# Project-level configuration.
+cmake_minimum_required(VERSION 3.10)
+project(runner LANGUAGES CXX)
+
+# The name of the executable created for the application. Change this to change
+# the on-disk name of your application.
+set(BINARY_NAME "pi_dashboard")
+# The unique GTK application identifier for this application. See:
+# https://wiki.gnome.org/HowDoI/ChooseApplicationID
+set(APPLICATION_ID "com.example.pi_dashboard")
+
+# Explicitly opt in to modern CMake behaviors to avoid warnings with recent
+# versions of CMake.
+cmake_policy(SET CMP0063 NEW)
+
+# Load bundled libraries from the lib/ directory relative to the binary.
+set(CMAKE_INSTALL_RPATH "$ORIGIN/lib")
+
+# Root filesystem for cross-building.
+if(FLUTTER_TARGET_PLATFORM_SYSROOT)
+  set(CMAKE_SYSROOT ${FLUTTER_TARGET_PLATFORM_SYSROOT})
+  set(CMAKE_FIND_ROOT_PATH ${CMAKE_SYSROOT})
+  set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
+  set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)
+  set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
+  set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
+endif()
+
+# Define build configuration options.
+if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
+  set(CMAKE_BUILD_TYPE "Debug" CACHE
+    STRING "Flutter build mode" FORCE)
+  set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS
+    "Debug" "Profile" "Release")
+endif()
+
+# Compilation settings that should be applied to most targets.
+#
+# Be cautious about adding new options here, as plugins use this function by
+# default. In most cases, you should add new options to specific targets instead
+# of modifying this function.
+function(APPLY_STANDARD_SETTINGS TARGET)
+  target_compile_features(${TARGET} PUBLIC cxx_std_14)
+  target_compile_options(${TARGET} PRIVATE -Wall -Werror)
+  target_compile_options(${TARGET} PRIVATE "$<$<NOT:$<CONFIG:Debug>>:-O3>")
+  target_compile_definitions(${TARGET} PRIVATE "$<$<NOT:$<CONFIG:Debug>>:NDEBUG>")
+endfunction()
+
+# Flutter library and tool build rules.
+set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter")
+add_subdirectory(${FLUTTER_MANAGED_DIR})
+
+# System-level dependencies.
+find_package(PkgConfig REQUIRED)
+pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0)
+
+add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}")
+
+# Define the application target. To change its name, change BINARY_NAME above,
+# not the value here, or `flutter run` will no longer work.
+#
+# Any new source files that you add to the application should be added here.
+add_executable(${BINARY_NAME}
+  "main.cc"
+  "my_application.cc"
+  "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc"
+)
+
+# Apply the standard set of build settings. This can be removed for applications
+# that need different build settings.
+apply_standard_settings(${BINARY_NAME})
+
+# Add dependency libraries. Add any application-specific dependencies here.
+target_link_libraries(${BINARY_NAME} PRIVATE flutter)
+target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK)
+
+# Run the Flutter tool portions of the build. This must not be removed.
+add_dependencies(${BINARY_NAME} flutter_assemble)
+
+# Only the install-generated bundle's copy of the executable will launch
+# correctly, since the resources must in the right relative locations. To avoid
+# people trying to run the unbundled copy, put it in a subdirectory instead of
+# the default top-level location.
+set_target_properties(${BINARY_NAME}
+  PROPERTIES
+  RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/intermediates_do_not_run"
+)
+
+
+# Generated plugin build rules, which manage building the plugins and adding
+# them to the application.
+include(flutter/generated_plugins.cmake)
+
+
+# === Installation ===
+# By default, "installing" just makes a relocatable bundle in the build
+# directory.
+set(BUILD_BUNDLE_DIR "${PROJECT_BINARY_DIR}/bundle")
+if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT)
+  set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE)
+endif()
+
+# Start with a clean build bundle directory every time.
+install(CODE "
+  file(REMOVE_RECURSE \"${BUILD_BUNDLE_DIR}/\")
+  " COMPONENT Runtime)
+
+set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data")
+set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}/lib")
+
+install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}"
+  COMPONENT Runtime)
+
+install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}"
+  COMPONENT Runtime)
+
+install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
+  COMPONENT Runtime)
+
+foreach(bundled_library ${PLUGIN_BUNDLED_LIBRARIES})
+  install(FILES "${bundled_library}"
+    DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
+    COMPONENT Runtime)
+endforeach(bundled_library)
+
+# Copy the native assets provided by the build.dart from all packages.
+set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/linux/")
+install(DIRECTORY "${NATIVE_ASSETS_DIR}"
+   DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
+   COMPONENT Runtime)
+
+# Fully re-copy the assets directory on each build to avoid having stale files
+# from a previous install.
+set(FLUTTER_ASSET_DIR_NAME "flutter_assets")
+install(CODE "
+  file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\")
+  " COMPONENT Runtime)
+install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}"
+  DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime)
+
+# Install the AOT library on non-Debug builds only.
+if(NOT CMAKE_BUILD_TYPE MATCHES "Debug")
+  install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
+    COMPONENT Runtime)
+endif()
diff --git a/linux/flutter/CMakeLists.txt b/linux/flutter/CMakeLists.txt
new file mode 100644
index 0000000..d5bd016
--- /dev/null
+++ b/linux/flutter/CMakeLists.txt
@@ -0,0 +1,88 @@
+# This file controls Flutter-level build steps. It should not be edited.
+cmake_minimum_required(VERSION 3.10)
+
+set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral")
+
+# Configuration provided via flutter tool.
+include(${EPHEMERAL_DIR}/generated_config.cmake)
+
+# TODO: Move the rest of this into files in ephemeral. See
+# https://github.com/flutter/flutter/issues/57146.
+
+# Serves the same purpose as list(TRANSFORM ... PREPEND ...),
+# which isn't available in 3.10.
+function(list_prepend LIST_NAME PREFIX)
+    set(NEW_LIST "")
+    foreach(element ${${LIST_NAME}})
+        list(APPEND NEW_LIST "${PREFIX}${element}")
+    endforeach(element)
+    set(${LIST_NAME} "${NEW_LIST}" PARENT_SCOPE)
+endfunction()
+
+# === Flutter Library ===
+# System-level dependencies.
+find_package(PkgConfig REQUIRED)
+pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0)
+pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0)
+pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0)
+
+set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/libflutter_linux_gtk.so")
+
+# Published to parent scope for install step.
+set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE)
+set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE)
+set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE)
+set(AOT_LIBRARY "${PROJECT_DIR}/build/lib/libapp.so" PARENT_SCOPE)
+
+list(APPEND FLUTTER_LIBRARY_HEADERS
+  "fl_basic_message_channel.h"
+  "fl_binary_codec.h"
+  "fl_binary_messenger.h"
+  "fl_dart_project.h"
+  "fl_engine.h"
+  "fl_json_message_codec.h"
+  "fl_json_method_codec.h"
+  "fl_message_codec.h"
+  "fl_method_call.h"
+  "fl_method_channel.h"
+  "fl_method_codec.h"
+  "fl_method_response.h"
+  "fl_plugin_registrar.h"
+  "fl_plugin_registry.h"
+  "fl_standard_message_codec.h"
+  "fl_standard_method_codec.h"
+  "fl_string_codec.h"
+  "fl_value.h"
+  "fl_view.h"
+  "flutter_linux.h"
+)
+list_prepend(FLUTTER_LIBRARY_HEADERS "${EPHEMERAL_DIR}/flutter_linux/")
+add_library(flutter INTERFACE)
+target_include_directories(flutter INTERFACE
+  "${EPHEMERAL_DIR}"
+)
+target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}")
+target_link_libraries(flutter INTERFACE
+  PkgConfig::GTK
+  PkgConfig::GLIB
+  PkgConfig::GIO
+)
+add_dependencies(flutter flutter_assemble)
+
+# === Flutter tool backend ===
+# _phony_ is a non-existent file to force this command to run every time,
+# since currently there's no way to get a full input/output list from the
+# flutter tool.
+add_custom_command(
+  OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS}
+    ${CMAKE_CURRENT_BINARY_DIR}/_phony_
+  COMMAND ${CMAKE_COMMAND} -E env
+    ${FLUTTER_TOOL_ENVIRONMENT}
+    "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh"
+      ${FLUTTER_TARGET_PLATFORM} ${CMAKE_BUILD_TYPE}
+  VERBATIM
+)
+add_custom_target(flutter_assemble DEPENDS
+  "${FLUTTER_LIBRARY}"
+  ${FLUTTER_LIBRARY_HEADERS}
+)
diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc
new file mode 100644
index 0000000..e71a16d
--- /dev/null
+++ b/linux/flutter/generated_plugin_registrant.cc
@@ -0,0 +1,11 @@
+//
+//  Generated file. Do not edit.
+//
+
+// clang-format off
+
+#include "generated_plugin_registrant.h"
+
+
+void fl_register_plugins(FlPluginRegistry* registry) {
+}
diff --git a/linux/flutter/generated_plugin_registrant.h b/linux/flutter/generated_plugin_registrant.h
new file mode 100644
index 0000000..e0f0a47
--- /dev/null
+++ b/linux/flutter/generated_plugin_registrant.h
@@ -0,0 +1,15 @@
+//
+//  Generated file. Do not edit.
+//
+
+// clang-format off
+
+#ifndef GENERATED_PLUGIN_REGISTRANT_
+#define GENERATED_PLUGIN_REGISTRANT_
+
+#include <flutter_linux/flutter_linux.h>
+
+// Registers Flutter plugins.
+void fl_register_plugins(FlPluginRegistry* registry);
+
+#endif  // GENERATED_PLUGIN_REGISTRANT_
diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake
new file mode 100644
index 0000000..2e1de87
--- /dev/null
+++ b/linux/flutter/generated_plugins.cmake
@@ -0,0 +1,23 @@
+#
+# Generated file, do not edit.
+#
+
+list(APPEND FLUTTER_PLUGIN_LIST
+)
+
+list(APPEND FLUTTER_FFI_PLUGIN_LIST
+)
+
+set(PLUGIN_BUNDLED_LIBRARIES)
+
+foreach(plugin ${FLUTTER_PLUGIN_LIST})
+  add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin})
+  target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin)
+  list(APPEND PLUGIN_BUNDLED_LIBRARIES $<TARGET_FILE:${plugin}_plugin>)
+  list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries})
+endforeach(plugin)
+
+foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST})
+  add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin})
+  list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries})
+endforeach(ffi_plugin)
diff --git a/linux/main.cc b/linux/main.cc
new file mode 100644
index 0000000..e7c5c54
--- /dev/null
+++ b/linux/main.cc
@@ -0,0 +1,6 @@
+#include "my_application.h"
+
+int main(int argc, char** argv) {
+  g_autoptr(MyApplication) app = my_application_new();
+  return g_application_run(G_APPLICATION(app), argc, argv);
+}
diff --git a/linux/my_application.cc b/linux/my_application.cc
new file mode 100644
index 0000000..fe95dae
--- /dev/null
+++ b/linux/my_application.cc
@@ -0,0 +1,124 @@
+#include "my_application.h"
+
+#include <flutter_linux/flutter_linux.h>
+#ifdef GDK_WINDOWING_X11
+#include <gdk/gdkx.h>
+#endif
+
+#include "flutter/generated_plugin_registrant.h"
+
+struct _MyApplication {
+  GtkApplication parent_instance;
+  char** dart_entrypoint_arguments;
+};
+
+G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION)
+
+// Implements GApplication::activate.
+static void my_application_activate(GApplication* application) {
+  MyApplication* self = MY_APPLICATION(application);
+  GtkWindow* window =
+      GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application)));
+
+  // Use a header bar when running in GNOME as this is the common style used
+  // by applications and is the setup most users will be using (e.g. Ubuntu
+  // desktop).
+  // If running on X and not using GNOME then just use a traditional title bar
+  // in case the window manager does more exotic layout, e.g. tiling.
+  // If running on Wayland assume the header bar will work (may need changing
+  // if future cases occur).
+  gboolean use_header_bar = TRUE;
+#ifdef GDK_WINDOWING_X11
+  GdkScreen* screen = gtk_window_get_screen(window);
+  if (GDK_IS_X11_SCREEN(screen)) {
+    const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen);
+    if (g_strcmp0(wm_name, "GNOME Shell") != 0) {
+      use_header_bar = FALSE;
+    }
+  }
+#endif
+  if (use_header_bar) {
+    GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new());
+    gtk_widget_show(GTK_WIDGET(header_bar));
+    gtk_header_bar_set_title(header_bar, "pi_dashboard");
+    gtk_header_bar_set_show_close_button(header_bar, TRUE);
+    gtk_window_set_titlebar(window, GTK_WIDGET(header_bar));
+  } else {
+    gtk_window_set_title(window, "pi_dashboard");
+  }
+
+  gtk_window_set_default_size(window, 1280, 720);
+  gtk_widget_show(GTK_WIDGET(window));
+
+  g_autoptr(FlDartProject) project = fl_dart_project_new();
+  fl_dart_project_set_dart_entrypoint_arguments(project, self->dart_entrypoint_arguments);
+
+  FlView* view = fl_view_new(project);
+  gtk_widget_show(GTK_WIDGET(view));
+  gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view));
+
+  fl_register_plugins(FL_PLUGIN_REGISTRY(view));
+
+  gtk_widget_grab_focus(GTK_WIDGET(view));
+}
+
+// Implements GApplication::local_command_line.
+static gboolean my_application_local_command_line(GApplication* application, gchar*** arguments, int* exit_status) {
+  MyApplication* self = MY_APPLICATION(application);
+  // Strip out the first argument as it is the binary name.
+  self->dart_entrypoint_arguments = g_strdupv(*arguments + 1);
+
+  g_autoptr(GError) error = nullptr;
+  if (!g_application_register(application, nullptr, &error)) {
+     g_warning("Failed to register: %s", error->message);
+     *exit_status = 1;
+     return TRUE;
+  }
+
+  g_application_activate(application);
+  *exit_status = 0;
+
+  return TRUE;
+}
+
+// Implements GApplication::startup.
+static void my_application_startup(GApplication* application) {
+  //MyApplication* self = MY_APPLICATION(object);
+
+  // Perform any actions required at application startup.
+
+  G_APPLICATION_CLASS(my_application_parent_class)->startup(application);
+}
+
+// Implements GApplication::shutdown.
+static void my_application_shutdown(GApplication* application) {
+  //MyApplication* self = MY_APPLICATION(object);
+
+  // Perform any actions required at application shutdown.
+
+  G_APPLICATION_CLASS(my_application_parent_class)->shutdown(application);
+}
+
+// Implements GObject::dispose.
+static void my_application_dispose(GObject* object) {
+  MyApplication* self = MY_APPLICATION(object);
+  g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev);
+  G_OBJECT_CLASS(my_application_parent_class)->dispose(object);
+}
+
+static void my_application_class_init(MyApplicationClass* klass) {
+  G_APPLICATION_CLASS(klass)->activate = my_application_activate;
+  G_APPLICATION_CLASS(klass)->local_command_line = my_application_local_command_line;
+  G_APPLICATION_CLASS(klass)->startup = my_application_startup;
+  G_APPLICATION_CLASS(klass)->shutdown = my_application_shutdown;
+  G_OBJECT_CLASS(klass)->dispose = my_application_dispose;
+}
+
+static void my_application_init(MyApplication* self) {}
+
+MyApplication* my_application_new() {
+  return MY_APPLICATION(g_object_new(my_application_get_type(),
+                                     "application-id", APPLICATION_ID,
+                                     "flags", G_APPLICATION_NON_UNIQUE,
+                                     nullptr));
+}
diff --git a/linux/my_application.h b/linux/my_application.h
new file mode 100644
index 0000000..72271d5
--- /dev/null
+++ b/linux/my_application.h
@@ -0,0 +1,18 @@
+#ifndef FLUTTER_MY_APPLICATION_H_
+#define FLUTTER_MY_APPLICATION_H_
+
+#include <gtk/gtk.h>
+
+G_DECLARE_FINAL_TYPE(MyApplication, my_application, MY, APPLICATION,
+                     GtkApplication)
+
+/**
+ * my_application_new:
+ *
+ * Creates a new Flutter-based application.
+ *
+ * Returns: a new #MyApplication.
+ */
+MyApplication* my_application_new();
+
+#endif  // FLUTTER_MY_APPLICATION_H_
diff --git a/pi_dashboard.iml b/pi_dashboard.iml
new file mode 100644
index 0000000..f66303d
--- /dev/null
+++ b/pi_dashboard.iml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<module type="JAVA_MODULE" version="4">
+  <component name="NewModuleRootManager" inherit-compiler-output="true">
+    <exclude-output />
+    <content url="file://$MODULE_DIR$">
+      <sourceFolder url="file://$MODULE_DIR$/lib" isTestSource="false" />
+      <sourceFolder url="file://$MODULE_DIR$/test" isTestSource="true" />
+      <excludeFolder url="file://$MODULE_DIR$/.dart_tool" />
+      <excludeFolder url="file://$MODULE_DIR$/.idea" />
+      <excludeFolder url="file://$MODULE_DIR$/build" />
+    </content>
+    <orderEntry type="sourceFolder" forTests="false" />
+    <orderEntry type="library" name="Dart SDK" level="project" />
+    <orderEntry type="library" name="Flutter Plugins" level="project" />
+    <orderEntry type="library" name="Dart Packages" level="project" />
+  </component>
+</module>
diff --git a/pubspec.yaml b/pubspec.yaml
new file mode 100644
index 0000000..8b5b657
--- /dev/null
+++ b/pubspec.yaml
@@ -0,0 +1,35 @@
+name: pi_dashboard
+description: "A new Flutter project."
+
+# Prevent accidental publishing to pub.dev.
+publish_to: 'none'
+
+version: 1.0.0+1
+
+environment:
+  sdk: '>=3.3.4 <4.0.0'
+
+dependencies:
+  flutter:
+    sdk: flutter
+  flutter_localizations:
+    sdk: flutter
+  http: ^1.2.1
+  shared_preferences:
+
+  flutter_gen: any
+dev_dependencies:
+  flutter_test:
+    sdk: flutter
+
+  flutter_lints: ^3.0.0
+
+flutter:
+  uses-material-design: true
+
+  # Enable generation of localized Strings from arb files.
+  generate: true
+
+  assets:
+    # Add assets from the images directory to the application.
+    - assets/images/
diff --git a/test/unit_test.dart b/test/unit_test.dart
new file mode 100644
index 0000000..e100eb0
--- /dev/null
+++ b/test/unit_test.dart
@@ -0,0 +1,15 @@
+// This is an example unit test.
+//
+// A unit test tests a single function, method, or class. To learn more about
+// writing unit tests, visit
+// https://flutter.dev/docs/cookbook/testing/unit/introduction
+
+import 'package:flutter_test/flutter_test.dart';
+
+void main() {
+  group('Plus Operator', () {
+    test('should add two numbers together', () {
+      expect(1 + 1, 2);
+    });
+  });
+}
diff --git a/test/widget_test.dart b/test/widget_test.dart
new file mode 100644
index 0000000..13ad8b9
--- /dev/null
+++ b/test/widget_test.dart
@@ -0,0 +1,31 @@
+// This is an example Flutter widget test.
+//
+// To perform an interaction with a widget in your test, use the WidgetTester
+// utility in the flutter_test package. For example, you can send tap and scroll
+// gestures. You can also use WidgetTester to find child widgets in the widget
+// tree, read text, and verify that the values of widget properties are correct.
+//
+// Visit https://flutter.dev/docs/cookbook/testing/widget/introduction for
+// more information about Widget testing.
+
+import 'package:flutter/material.dart';
+import 'package:flutter_test/flutter_test.dart';
+
+void main() {
+  group('MyWidget', () {
+    testWidgets('should display a string of text', (WidgetTester tester) async {
+      // Define a Widget
+      const myWidget = MaterialApp(
+        home: Scaffold(
+          body: Text('Hello'),
+        ),
+      );
+
+      // Build myWidget and trigger a frame.
+      await tester.pumpWidget(myWidget);
+
+      // Verify myWidget shows some text
+      expect(find.byType(Text), findsOneWidget);
+    });
+  });
+}