diff --git a/.gitignore b/.gitignore
new file mode 100644
index 00000000..a300c329
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,71 @@
+# Miscellaneous
+*.class
+*.log
+*.pyc
+*.swp
+.DS_Store
+.atom/
+.buildlog/
+.history
+.svn/
+
+# IntelliJ related
+*.iml
+*.ipr
+*.iws
+.idea/
+
+# Visual Studio Code related
+.vscode/
+
+# Flutter/Dart/Pub related
+**/doc/api/
+.dart_tool/
+.flutter-plugins
+.flutter-plugins-dependencies
+.packages
+.pub-cache/
+.pub/
+/build/
+
+# Android related
+**/android/**/gradle-wrapper.jar
+**/android/.gradle
+**/android/captures/
+**/android/gradlew
+**/android/gradlew.bat
+**/android/local.properties
+**/android/**/GeneratedPluginRegistrant.java
+
+# iOS/XCode related
+**/ios/**/*.mode1v3
+**/ios/**/*.mode2v3
+**/ios/**/*.moved-aside
+**/ios/**/*.pbxuser
+**/ios/**/*.perspectivev3
+**/ios/**/*sync/
+**/ios/**/.sconsign.dblite
+**/ios/**/.tags*
+**/ios/**/.vagrant/
+**/ios/**/DerivedData/
+**/ios/**/Icon?
+**/ios/**/Pods/
+**/ios/**/.symlinks/
+**/ios/**/profile
+**/ios/**/xcuserdata
+**/ios/.generated/
+**/ios/Flutter/App.framework
+**/ios/Flutter/Flutter.framework
+**/ios/Flutter/Generated.xcconfig
+**/ios/Flutter/app.flx
+**/ios/Flutter/app.zip
+**/ios/Flutter/flutter_assets/
+**/ios/ServiceDefinitions.json
+**/ios/Runner/GeneratedPluginRegistrant.*
+
+# Exceptions to above rules.
+!**/ios/**/default.mode1v3
+!**/ios/**/default.mode2v3
+!**/ios/**/default.pbxuser
+!**/ios/**/default.perspectivev3
+!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages
diff --git a/.metadata b/.metadata
new file mode 100644
index 00000000..033ad2af
--- /dev/null
+++ b/.metadata
@@ -0,0 +1,10 @@
+# This file tracks properties of this Flutter project.
+# Used by Flutter tool to assess capabilities and perform upgrades etc.
+#
+# This file should be version controlled and should not be manually edited.
+
+version:
+  revision: 7a4c33425ddd78c54aba07d86f3f9a4a0051769b
+  channel: stable
+
+project_type: app
diff --git a/README.md b/README.md
index 24c18e38..9c2491ff 100644
--- a/README.md
+++ b/README.md
@@ -1,3 +1,11 @@
 # InvenTreee Mobile App
 
-The InvenTree mobile / tablet application (supports Android + iOS) is a companion app for the [InvenTree stock management system](https://github.com/inventree/InvenTree).
+The InvenTree mobile / tablet application is a companion app for the [InvenTree stock management system](https://github.com/inventree/InvenTree).
+
+Written in the [Flutter](https://flutter.dev/) environment, the app provides native support for Android and iOS devices.
+
+## Features
+
+`TODO`
+
+
diff --git a/android/app/build.gradle b/android/app/build.gradle
new file mode 100644
index 00000000..b4fad3b1
--- /dev/null
+++ b/android/app/build.gradle
@@ -0,0 +1,65 @@
+def localProperties = new Properties()
+def localPropertiesFile = rootProject.file('local.properties')
+if (localPropertiesFile.exists()) {
+    localPropertiesFile.withReader('UTF-8') { reader ->
+        localProperties.load(reader)
+    }
+}
+
+def flutterRoot = localProperties.getProperty('flutter.sdk')
+if (flutterRoot == null) {
+    throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.")
+}
+
+def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
+if (flutterVersionCode == null) {
+    flutterVersionCode = '1'
+}
+
+def flutterVersionName = localProperties.getProperty('flutter.versionName')
+if (flutterVersionName == null) {
+    flutterVersionName = '1.0'
+}
+
+apply plugin: 'com.android.application'
+apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
+
+android {
+    compileSdkVersion 28
+
+    packagingOptions {
+        exclude 'META-INF/proguard/androidx-annotations.pro'
+    }
+
+    lintOptions {
+        disable 'InvalidPackage'
+    }
+
+    defaultConfig {
+        // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
+        applicationId "inventree.inventree_app"
+        minSdkVersion 16
+        targetSdkVersion 28
+        versionCode flutterVersionCode.toInteger()
+        versionName flutterVersionName
+        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
+    }
+
+    buildTypes {
+        release {
+            // TODO: Add your own signing config for the release build.
+            // Signing with the debug keys for now, so `flutter run --release` works.
+            signingConfig signingConfigs.debug
+        }
+    }
+}
+
+flutter {
+    source '../..'
+}
+
+dependencies {
+    testImplementation 'junit:junit:4.12'
+    androidTestImplementation 'com.android.support.test:runner:1.0.2'
+    androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
+}
diff --git a/android/app/src/debug/AndroidManifest.xml b/android/app/src/debug/AndroidManifest.xml
new file mode 100644
index 00000000..4c9a6baa
--- /dev/null
+++ b/android/app/src/debug/AndroidManifest.xml
@@ -0,0 +1,7 @@
+
+    
+    
+
diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
new file mode 100644
index 00000000..c8646a61
--- /dev/null
+++ b/android/app/src/main/AndroidManifest.xml
@@ -0,0 +1,33 @@
+
+
+    
+    
+        
+            
+            
+            
+                
+                
+            
+        
+    
+
diff --git a/android/app/src/main/java/inventree/inventree_app/MainActivity.java b/android/app/src/main/java/inventree/inventree_app/MainActivity.java
new file mode 100644
index 00000000..460b0e13
--- /dev/null
+++ b/android/app/src/main/java/inventree/inventree_app/MainActivity.java
@@ -0,0 +1,13 @@
+package inventree.inventree_app;
+
+import android.os.Bundle;
+import io.flutter.app.FlutterActivity;
+import io.flutter.plugins.GeneratedPluginRegistrant;
+
+public class MainActivity extends FlutterActivity {
+  @Override
+  protected void onCreate(Bundle savedInstanceState) {
+    super.onCreate(savedInstanceState);
+    GeneratedPluginRegistrant.registerWith(this);
+  }
+}
diff --git a/android/app/src/main/res/drawable/launch_background.xml b/android/app/src/main/res/drawable/launch_background.xml
new file mode 100644
index 00000000..304732f8
--- /dev/null
+++ b/android/app/src/main/res/drawable/launch_background.xml
@@ -0,0 +1,12 @@
+
+
+
+    +
+    
+    
+
diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 00000000..dcd3a75f
Binary files /dev/null and b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 00000000..8886e2f1
Binary files /dev/null and b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 00000000..716f30fb
Binary files /dev/null and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 00000000..52b0957c
Binary files /dev/null and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 00000000..a3d7ddae
Binary files /dev/null and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/android/app/src/main/res/values/styles.xml b/android/app/src/main/res/values/styles.xml
new file mode 100644
index 00000000..00fa4417
--- /dev/null
+++ b/android/app/src/main/res/values/styles.xml
@@ -0,0 +1,8 @@
+
+
+    
+
diff --git a/android/app/src/profile/AndroidManifest.xml b/android/app/src/profile/AndroidManifest.xml
new file mode 100644
index 00000000..4c9a6baa
--- /dev/null
+++ b/android/app/src/profile/AndroidManifest.xml
@@ -0,0 +1,7 @@
+
+    
+    
+
diff --git a/android/build.gradle b/android/build.gradle
new file mode 100644
index 00000000..bb8a3038
--- /dev/null
+++ b/android/build.gradle
@@ -0,0 +1,29 @@
+buildscript {
+    repositories {
+        google()
+        jcenter()
+    }
+
+    dependencies {
+        classpath 'com.android.tools.build:gradle:3.2.1'
+    }
+}
+
+allprojects {
+    repositories {
+        google()
+        jcenter()
+    }
+}
+
+rootProject.buildDir = '../build'
+subprojects {
+    project.buildDir = "${rootProject.buildDir}/${project.name}"
+}
+subprojects {
+    project.evaluationDependsOn(':app')
+}
+
+task clean(type: Delete) {
+    delete rootProject.buildDir
+}
diff --git a/android/gradle.properties b/android/gradle.properties
new file mode 100644
index 00000000..7be3d8b4
--- /dev/null
+++ b/android/gradle.properties
@@ -0,0 +1,2 @@
+org.gradle.jvmargs=-Xmx1536M
+android.enableR8=true
diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 00000000..2819f022
--- /dev/null
+++ b/android/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+#Fri Jun 23 08:50:38 CEST 2017
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.2-all.zip
diff --git a/android/settings.gradle b/android/settings.gradle
new file mode 100644
index 00000000..5a2f14fb
--- /dev/null
+++ b/android/settings.gradle
@@ -0,0 +1,15 @@
+include ':app'
+
+def flutterProjectRoot = rootProject.projectDir.parentFile.toPath()
+
+def plugins = new Properties()
+def pluginsFile = new File(flutterProjectRoot.toFile(), '.flutter-plugins')
+if (pluginsFile.exists()) {
+    pluginsFile.withReader('UTF-8') { reader -> plugins.load(reader) }
+}
+
+plugins.each { name, path ->
+    def pluginDirectory = flutterProjectRoot.resolve(path).resolve('android').toFile()
+    include ":$name"
+    project(":$name").projectDir = pluginDirectory
+}
diff --git a/assets/image/icon.png b/assets/image/icon.png
new file mode 100644
index 00000000..200b6acd
Binary files /dev/null and b/assets/image/icon.png differ
diff --git a/ios/Flutter/AppFrameworkInfo.plist b/ios/Flutter/AppFrameworkInfo.plist
new file mode 100644
index 00000000..9367d483
--- /dev/null
+++ b/ios/Flutter/AppFrameworkInfo.plist
@@ -0,0 +1,26 @@
+
+
+
+
+  CFBundleDevelopmentRegion
+  en
+  CFBundleExecutable
+  App
+  CFBundleIdentifier
+  io.flutter.flutter.app
+  CFBundleInfoDictionaryVersion
+  6.0
+  CFBundleName
+  App
+  CFBundlePackageType
+  FMWK
+  CFBundleShortVersionString
+  1.0
+  CFBundleSignature
+  ????
+  CFBundleVersion
+  1.0
+  MinimumOSVersion
+  8.0
+
+
diff --git a/ios/Flutter/Debug.xcconfig b/ios/Flutter/Debug.xcconfig
new file mode 100644
index 00000000..592ceee8
--- /dev/null
+++ b/ios/Flutter/Debug.xcconfig
@@ -0,0 +1 @@
+#include "Generated.xcconfig"
diff --git a/ios/Flutter/Release.xcconfig b/ios/Flutter/Release.xcconfig
new file mode 100644
index 00000000..592ceee8
--- /dev/null
+++ b/ios/Flutter/Release.xcconfig
@@ -0,0 +1 @@
+#include "Generated.xcconfig"
diff --git a/ios/Flutter/flutter_export_environment.sh b/ios/Flutter/flutter_export_environment.sh
new file mode 100644
index 00000000..5be78977
--- /dev/null
+++ b/ios/Flutter/flutter_export_environment.sh
@@ -0,0 +1,10 @@
+#!/bin/sh
+# This is a generated file; do not edit or check into version control.
+export "FLUTTER_ROOT=C:\flutter"
+export "FLUTTER_APPLICATION_PATH=C:\inventree-app"
+export "FLUTTER_TARGET=lib\main.dart"
+export "FLUTTER_BUILD_DIR=build"
+export "SYMROOT=${SOURCE_ROOT}/../build\ios"
+export "FLUTTER_FRAMEWORK_DIR=C:\flutter\bin\cache\artifacts\engine\ios"
+export "FLUTTER_BUILD_NAME=1.0.0"
+export "FLUTTER_BUILD_NUMBER=1"
diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj
new file mode 100644
index 00000000..3a4c6fd9
--- /dev/null
+++ b/ios/Runner.xcodeproj/project.pbxproj
@@ -0,0 +1,506 @@
+// !$*UTF8*$!
+{
+	archiveVersion = 1;
+	classes = {
+	};
+	objectVersion = 46;
+	objects = {
+
+/* Begin PBXBuildFile section */
+		1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
+		3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
+		3B80C3941E831B6300D905FE /* App.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3B80C3931E831B6300D905FE /* App.framework */; };
+		3B80C3951E831B6300D905FE /* App.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3B80C3931E831B6300D905FE /* App.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
+		9705A1C61CF904A100538489 /* Flutter.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9740EEBA1CF902C7004384FC /* Flutter.framework */; };
+		9705A1C71CF904A300538489 /* Flutter.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 9740EEBA1CF902C7004384FC /* Flutter.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
+		9740EEB41CF90195004384FC /* Debug.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = 9740EEB21CF90195004384FC /* Debug.xcconfig */; };
+		978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */; };
+		97C146F31CF9000F007C117D /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 97C146F21CF9000F007C117D /* main.m */; };
+		97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
+		97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
+		97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
+/* End PBXBuildFile section */
+
+/* Begin PBXCopyFilesBuildPhase section */
+		9705A1C41CF9048500538489 /* Embed Frameworks */ = {
+			isa = PBXCopyFilesBuildPhase;
+			buildActionMask = 2147483647;
+			dstPath = "";
+			dstSubfolderSpec = 10;
+			files = (
+				3B80C3951E831B6300D905FE /* App.framework in Embed Frameworks */,
+				9705A1C71CF904A300538489 /* Flutter.framework in Embed Frameworks */,
+			);
+			name = "Embed Frameworks";
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+/* End PBXCopyFilesBuildPhase section */
+
+/* Begin PBXFileReference section */
+		1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; };
+		1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; };
+		3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; };
+		3B80C3931E831B6300D905FE /* App.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = App.framework; path = Flutter/App.framework; sourceTree = ""; };
+		7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; };
+		7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; };
+		7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; };
+		9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; };
+		9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; };
+		9740EEBA1CF902C7004384FC /* Flutter.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Flutter.framework; path = Flutter/Flutter.framework; sourceTree = ""; };
+		97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
+		97C146F21CF9000F007C117D /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; };
+		97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; };
+		97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
+		97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; };
+		97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
+/* End PBXFileReference section */
+
+/* Begin PBXFrameworksBuildPhase section */
+		97C146EB1CF9000F007C117D /* Frameworks */ = {
+			isa = PBXFrameworksBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+				9705A1C61CF904A100538489 /* Flutter.framework in Frameworks */,
+				3B80C3941E831B6300D905FE /* App.framework in Frameworks */,
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+/* End PBXFrameworksBuildPhase section */
+
+/* Begin PBXGroup section */
+		9740EEB11CF90186004384FC /* Flutter */ = {
+			isa = PBXGroup;
+			children = (
+				3B80C3931E831B6300D905FE /* App.framework */,
+				3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
+				9740EEBA1CF902C7004384FC /* Flutter.framework */,
+				9740EEB21CF90195004384FC /* Debug.xcconfig */,
+				7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
+				9740EEB31CF90195004384FC /* Generated.xcconfig */,
+			);
+			name = Flutter;
+			sourceTree = "";
+		};
+		97C146E51CF9000F007C117D = {
+			isa = PBXGroup;
+			children = (
+				9740EEB11CF90186004384FC /* Flutter */,
+				97C146F01CF9000F007C117D /* Runner */,
+				97C146EF1CF9000F007C117D /* Products */,
+				CF3B75C9A7D2FA2A4C99F110 /* Frameworks */,
+			);
+			sourceTree = "";
+		};
+		97C146EF1CF9000F007C117D /* Products */ = {
+			isa = PBXGroup;
+			children = (
+				97C146EE1CF9000F007C117D /* Runner.app */,
+			);
+			name = Products;
+			sourceTree = "";
+		};
+		97C146F01CF9000F007C117D /* Runner */ = {
+			isa = PBXGroup;
+			children = (
+				7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */,
+				7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */,
+				97C146FA1CF9000F007C117D /* Main.storyboard */,
+				97C146FD1CF9000F007C117D /* Assets.xcassets */,
+				97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
+				97C147021CF9000F007C117D /* Info.plist */,
+				97C146F11CF9000F007C117D /* Supporting Files */,
+				1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */,
+				1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,
+			);
+			path = Runner;
+			sourceTree = "";
+		};
+		97C146F11CF9000F007C117D /* Supporting Files */ = {
+			isa = PBXGroup;
+			children = (
+				97C146F21CF9000F007C117D /* main.m */,
+			);
+			name = "Supporting Files";
+			sourceTree = "";
+		};
+/* End PBXGroup section */
+
+/* Begin PBXNativeTarget section */
+		97C146ED1CF9000F007C117D /* Runner */ = {
+			isa = PBXNativeTarget;
+			buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
+			buildPhases = (
+				9740EEB61CF901F6004384FC /* Run Script */,
+				97C146EA1CF9000F007C117D /* Sources */,
+				97C146EB1CF9000F007C117D /* Frameworks */,
+				97C146EC1CF9000F007C117D /* Resources */,
+				9705A1C41CF9048500538489 /* Embed Frameworks */,
+				3B06AD1E1E4923F5004D2608 /* Thin Binary */,
+			);
+			buildRules = (
+			);
+			dependencies = (
+			);
+			name = Runner;
+			productName = Runner;
+			productReference = 97C146EE1CF9000F007C117D /* Runner.app */;
+			productType = "com.apple.product-type.application";
+		};
+/* End PBXNativeTarget section */
+
+/* Begin PBXProject section */
+		97C146E61CF9000F007C117D /* Project object */ = {
+			isa = PBXProject;
+			attributes = {
+				LastUpgradeCheck = 0910;
+				ORGANIZATIONNAME = "The Chromium Authors";
+				TargetAttributes = {
+					97C146ED1CF9000F007C117D = {
+						CreatedOnToolsVersion = 7.3.1;
+					};
+				};
+			};
+			buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */;
+			compatibilityVersion = "Xcode 3.2";
+			developmentRegion = English;
+			hasScannedForEncodings = 0;
+			knownRegions = (
+				en,
+				Base,
+			);
+			mainGroup = 97C146E51CF9000F007C117D;
+			productRefGroup = 97C146EF1CF9000F007C117D /* Products */;
+			projectDirPath = "";
+			projectRoot = "";
+			targets = (
+				97C146ED1CF9000F007C117D /* Runner */,
+			);
+		};
+/* End PBXProject section */
+
+/* Begin PBXResourcesBuildPhase section */
+		97C146EC1CF9000F007C117D /* Resources */ = {
+			isa = PBXResourcesBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+				97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */,
+				3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,
+				9740EEB41CF90195004384FC /* Debug.xcconfig in Resources */,
+				97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,
+				97C146FC1CF9000F007C117D /* Main.storyboard in Resources */,
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+/* End PBXResourcesBuildPhase section */
+
+/* Begin PBXShellScriptBuildPhase section */
+		3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
+			isa = PBXShellScriptBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+			);
+			inputPaths = (
+			);
+			name = "Thin Binary";
+			outputPaths = (
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+			shellPath = /bin/sh;
+			shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" thin";
+		};
+		9740EEB61CF901F6004384FC /* Run Script */ = {
+			isa = PBXShellScriptBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+			);
+			inputPaths = (
+			);
+			name = "Run Script";
+			outputPaths = (
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+			shellPath = /bin/sh;
+			shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
+		};
+/* End PBXShellScriptBuildPhase section */
+
+/* Begin PBXSourcesBuildPhase section */
+		97C146EA1CF9000F007C117D /* Sources */ = {
+			isa = PBXSourcesBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+				978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */,
+				97C146F31CF9000F007C117D /* main.m in Sources */,
+				1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+/* End PBXSourcesBuildPhase section */
+
+/* Begin PBXVariantGroup section */
+		97C146FA1CF9000F007C117D /* Main.storyboard */ = {
+			isa = PBXVariantGroup;
+			children = (
+				97C146FB1CF9000F007C117D /* Base */,
+			);
+			name = Main.storyboard;
+			sourceTree = "";
+		};
+		97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = {
+			isa = PBXVariantGroup;
+			children = (
+				97C147001CF9000F007C117D /* Base */,
+			);
+			name = LaunchScreen.storyboard;
+			sourceTree = "";
+		};
+/* End PBXVariantGroup section */
+
+/* Begin XCBuildConfiguration section */
+		249021D3217E4FDB00AE95B9 /* Profile */ = {
+			isa = XCBuildConfiguration;
+			baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
+			buildSettings = {
+				ALWAYS_SEARCH_USER_PATHS = NO;
+				CLANG_ANALYZER_NONNULL = YES;
+				CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
+				CLANG_CXX_LIBRARY = "libc++";
+				CLANG_ENABLE_MODULES = YES;
+				CLANG_ENABLE_OBJC_ARC = YES;
+				CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+				CLANG_WARN_BOOL_CONVERSION = YES;
+				CLANG_WARN_COMMA = YES;
+				CLANG_WARN_CONSTANT_CONVERSION = YES;
+				CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+				CLANG_WARN_EMPTY_BODY = YES;
+				CLANG_WARN_ENUM_CONVERSION = YES;
+				CLANG_WARN_INFINITE_RECURSION = YES;
+				CLANG_WARN_INT_CONVERSION = YES;
+				CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+				CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+				CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+				CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+				CLANG_WARN_STRICT_PROTOTYPES = YES;
+				CLANG_WARN_SUSPICIOUS_MOVE = YES;
+				CLANG_WARN_UNREACHABLE_CODE = YES;
+				CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+				"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
+				COPY_PHASE_STRIP = NO;
+				DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
+				ENABLE_NS_ASSERTIONS = NO;
+				ENABLE_STRICT_OBJC_MSGSEND = YES;
+				GCC_C_LANGUAGE_STANDARD = gnu99;
+				GCC_NO_COMMON_BLOCKS = YES;
+				GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+				GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+				GCC_WARN_UNDECLARED_SELECTOR = YES;
+				GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+				GCC_WARN_UNUSED_FUNCTION = YES;
+				GCC_WARN_UNUSED_VARIABLE = YES;
+				IPHONEOS_DEPLOYMENT_TARGET = 8.0;
+				MTL_ENABLE_DEBUG_INFO = NO;
+				SDKROOT = iphoneos;
+				TARGETED_DEVICE_FAMILY = "1,2";
+				VALIDATE_PRODUCT = YES;
+			};
+			name = Profile;
+		};
+		249021D4217E4FDB00AE95B9 /* Profile */ = {
+			isa = XCBuildConfiguration;
+			baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
+			buildSettings = {
+				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+				CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
+				DEVELOPMENT_TEAM = S8QB4VV633;
+				ENABLE_BITCODE = NO;
+				FRAMEWORK_SEARCH_PATHS = (
+					"$(inherited)",
+					"$(PROJECT_DIR)/Flutter",
+				);
+				INFOPLIST_FILE = Runner/Info.plist;
+				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
+				LIBRARY_SEARCH_PATHS = (
+					"$(inherited)",
+					"$(PROJECT_DIR)/Flutter",
+				);
+				PRODUCT_BUNDLE_IDENTIFIER = inventree.inventreeApp;
+				PRODUCT_NAME = "$(TARGET_NAME)";
+				VERSIONING_SYSTEM = "apple-generic";
+			};
+			name = Profile;
+		};
+		97C147031CF9000F007C117D /* Debug */ = {
+			isa = XCBuildConfiguration;
+			baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
+			buildSettings = {
+				ALWAYS_SEARCH_USER_PATHS = NO;
+				CLANG_ANALYZER_NONNULL = YES;
+				CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
+				CLANG_CXX_LIBRARY = "libc++";
+				CLANG_ENABLE_MODULES = YES;
+				CLANG_ENABLE_OBJC_ARC = YES;
+				CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+				CLANG_WARN_BOOL_CONVERSION = YES;
+				CLANG_WARN_COMMA = YES;
+				CLANG_WARN_CONSTANT_CONVERSION = YES;
+				CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+				CLANG_WARN_EMPTY_BODY = YES;
+				CLANG_WARN_ENUM_CONVERSION = YES;
+				CLANG_WARN_INFINITE_RECURSION = YES;
+				CLANG_WARN_INT_CONVERSION = YES;
+				CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+				CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+				CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+				CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+				CLANG_WARN_STRICT_PROTOTYPES = YES;
+				CLANG_WARN_SUSPICIOUS_MOVE = YES;
+				CLANG_WARN_UNREACHABLE_CODE = YES;
+				CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+				"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
+				COPY_PHASE_STRIP = NO;
+				DEBUG_INFORMATION_FORMAT = dwarf;
+				ENABLE_STRICT_OBJC_MSGSEND = YES;
+				ENABLE_TESTABILITY = YES;
+				GCC_C_LANGUAGE_STANDARD = gnu99;
+				GCC_DYNAMIC_NO_PIC = NO;
+				GCC_NO_COMMON_BLOCKS = YES;
+				GCC_OPTIMIZATION_LEVEL = 0;
+				GCC_PREPROCESSOR_DEFINITIONS = (
+					"DEBUG=1",
+					"$(inherited)",
+				);
+				GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+				GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+				GCC_WARN_UNDECLARED_SELECTOR = YES;
+				GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+				GCC_WARN_UNUSED_FUNCTION = YES;
+				GCC_WARN_UNUSED_VARIABLE = YES;
+				IPHONEOS_DEPLOYMENT_TARGET = 8.0;
+				MTL_ENABLE_DEBUG_INFO = YES;
+				ONLY_ACTIVE_ARCH = YES;
+				SDKROOT = iphoneos;
+				TARGETED_DEVICE_FAMILY = "1,2";
+			};
+			name = Debug;
+		};
+		97C147041CF9000F007C117D /* Release */ = {
+			isa = XCBuildConfiguration;
+			baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
+			buildSettings = {
+				ALWAYS_SEARCH_USER_PATHS = NO;
+				CLANG_ANALYZER_NONNULL = YES;
+				CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
+				CLANG_CXX_LIBRARY = "libc++";
+				CLANG_ENABLE_MODULES = YES;
+				CLANG_ENABLE_OBJC_ARC = YES;
+				CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+				CLANG_WARN_BOOL_CONVERSION = YES;
+				CLANG_WARN_COMMA = YES;
+				CLANG_WARN_CONSTANT_CONVERSION = YES;
+				CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+				CLANG_WARN_EMPTY_BODY = YES;
+				CLANG_WARN_ENUM_CONVERSION = YES;
+				CLANG_WARN_INFINITE_RECURSION = YES;
+				CLANG_WARN_INT_CONVERSION = YES;
+				CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+				CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+				CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+				CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+				CLANG_WARN_STRICT_PROTOTYPES = YES;
+				CLANG_WARN_SUSPICIOUS_MOVE = YES;
+				CLANG_WARN_UNREACHABLE_CODE = YES;
+				CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+				"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
+				COPY_PHASE_STRIP = NO;
+				DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
+				ENABLE_NS_ASSERTIONS = NO;
+				ENABLE_STRICT_OBJC_MSGSEND = YES;
+				GCC_C_LANGUAGE_STANDARD = gnu99;
+				GCC_NO_COMMON_BLOCKS = YES;
+				GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+				GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+				GCC_WARN_UNDECLARED_SELECTOR = YES;
+				GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+				GCC_WARN_UNUSED_FUNCTION = YES;
+				GCC_WARN_UNUSED_VARIABLE = YES;
+				IPHONEOS_DEPLOYMENT_TARGET = 8.0;
+				MTL_ENABLE_DEBUG_INFO = NO;
+				SDKROOT = iphoneos;
+				TARGETED_DEVICE_FAMILY = "1,2";
+				VALIDATE_PRODUCT = YES;
+			};
+			name = Release;
+		};
+		97C147061CF9000F007C117D /* Debug */ = {
+			isa = XCBuildConfiguration;
+			baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
+			buildSettings = {
+				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+				CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
+				ENABLE_BITCODE = NO;
+				FRAMEWORK_SEARCH_PATHS = (
+					"$(inherited)",
+					"$(PROJECT_DIR)/Flutter",
+				);
+				INFOPLIST_FILE = Runner/Info.plist;
+				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
+				LIBRARY_SEARCH_PATHS = (
+					"$(inherited)",
+					"$(PROJECT_DIR)/Flutter",
+				);
+				PRODUCT_BUNDLE_IDENTIFIER = inventree.inventreeApp;
+				PRODUCT_NAME = "$(TARGET_NAME)";
+				VERSIONING_SYSTEM = "apple-generic";
+			};
+			name = Debug;
+		};
+		97C147071CF9000F007C117D /* Release */ = {
+			isa = XCBuildConfiguration;
+			baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
+			buildSettings = {
+				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+				CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
+				ENABLE_BITCODE = NO;
+				FRAMEWORK_SEARCH_PATHS = (
+					"$(inherited)",
+					"$(PROJECT_DIR)/Flutter",
+				);
+				INFOPLIST_FILE = Runner/Info.plist;
+				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
+				LIBRARY_SEARCH_PATHS = (
+					"$(inherited)",
+					"$(PROJECT_DIR)/Flutter",
+				);
+				PRODUCT_BUNDLE_IDENTIFIER = inventree.inventreeApp;
+				PRODUCT_NAME = "$(TARGET_NAME)";
+				VERSIONING_SYSTEM = "apple-generic";
+			};
+			name = Release;
+		};
+/* End XCBuildConfiguration section */
+
+/* Begin XCConfigurationList section */
+		97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = {
+			isa = XCConfigurationList;
+			buildConfigurations = (
+				97C147031CF9000F007C117D /* Debug */,
+				97C147041CF9000F007C117D /* Release */,
+				249021D3217E4FDB00AE95B9 /* Profile */,
+			);
+			defaultConfigurationIsVisible = 0;
+			defaultConfigurationName = Release;
+		};
+		97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = {
+			isa = XCConfigurationList;
+			buildConfigurations = (
+				97C147061CF9000F007C117D /* Debug */,
+				97C147071CF9000F007C117D /* Release */,
+				249021D4217E4FDB00AE95B9 /* Profile */,
+			);
+			defaultConfigurationIsVisible = 0;
+			defaultConfigurationName = Release;
+		};
+/* End XCConfigurationList section */
+	};
+	rootObject = 97C146E61CF9000F007C117D /* Project object */;
+}
diff --git a/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata
new file mode 100644
index 00000000..1d526a16
--- /dev/null
+++ b/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata
@@ -0,0 +1,7 @@
+
+
+   
+   
+
diff --git a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
new file mode 100644
index 00000000..786d6aad
--- /dev/null
+++ b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
@@ -0,0 +1,93 @@
+
+
+   
+      
+         
+            
+            
+         
+      
+   
+   
+      
+      
+      
+         
+         
+      
+      
+      
+   
+   
+      
+         
+         
+      
+      
+      
+   
+   
+      
+         
+         
+      
+   
+   
+   
+   
+   
+
diff --git a/ios/Runner.xcworkspace/contents.xcworkspacedata b/ios/Runner.xcworkspace/contents.xcworkspacedata
new file mode 100644
index 00000000..1d526a16
--- /dev/null
+++ b/ios/Runner.xcworkspace/contents.xcworkspacedata
@@ -0,0 +1,7 @@
+
+
+   
+   
+
diff --git a/ios/Runner/AppDelegate.h b/ios/Runner/AppDelegate.h
new file mode 100644
index 00000000..36e21bbf
--- /dev/null
+++ b/ios/Runner/AppDelegate.h
@@ -0,0 +1,6 @@
+#import 
+#import 
+
+@interface AppDelegate : FlutterAppDelegate
+
+@end
diff --git a/ios/Runner/AppDelegate.m b/ios/Runner/AppDelegate.m
new file mode 100644
index 00000000..59a72e90
--- /dev/null
+++ b/ios/Runner/AppDelegate.m
@@ -0,0 +1,13 @@
+#include "AppDelegate.h"
+#include "GeneratedPluginRegistrant.h"
+
+@implementation AppDelegate
+
+- (BOOL)application:(UIApplication *)application
+    didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
+  [GeneratedPluginRegistrant registerWithRegistry:self];
+  // Override point for customization after application launch.
+  return [super application:application didFinishLaunchingWithOptions:launchOptions];
+}
+
+@end
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json
new file mode 100644
index 00000000..d36b1fab
--- /dev/null
+++ b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json
@@ -0,0 +1,122 @@
+{
+  "images" : [
+    {
+      "size" : "20x20",
+      "idiom" : "iphone",
+      "filename" : "Icon-App-20x20@2x.png",
+      "scale" : "2x"
+    },
+    {
+      "size" : "20x20",
+      "idiom" : "iphone",
+      "filename" : "Icon-App-20x20@3x.png",
+      "scale" : "3x"
+    },
+    {
+      "size" : "29x29",
+      "idiom" : "iphone",
+      "filename" : "Icon-App-29x29@1x.png",
+      "scale" : "1x"
+    },
+    {
+      "size" : "29x29",
+      "idiom" : "iphone",
+      "filename" : "Icon-App-29x29@2x.png",
+      "scale" : "2x"
+    },
+    {
+      "size" : "29x29",
+      "idiom" : "iphone",
+      "filename" : "Icon-App-29x29@3x.png",
+      "scale" : "3x"
+    },
+    {
+      "size" : "40x40",
+      "idiom" : "iphone",
+      "filename" : "Icon-App-40x40@2x.png",
+      "scale" : "2x"
+    },
+    {
+      "size" : "40x40",
+      "idiom" : "iphone",
+      "filename" : "Icon-App-40x40@3x.png",
+      "scale" : "3x"
+    },
+    {
+      "size" : "60x60",
+      "idiom" : "iphone",
+      "filename" : "Icon-App-60x60@2x.png",
+      "scale" : "2x"
+    },
+    {
+      "size" : "60x60",
+      "idiom" : "iphone",
+      "filename" : "Icon-App-60x60@3x.png",
+      "scale" : "3x"
+    },
+    {
+      "size" : "20x20",
+      "idiom" : "ipad",
+      "filename" : "Icon-App-20x20@1x.png",
+      "scale" : "1x"
+    },
+    {
+      "size" : "20x20",
+      "idiom" : "ipad",
+      "filename" : "Icon-App-20x20@2x.png",
+      "scale" : "2x"
+    },
+    {
+      "size" : "29x29",
+      "idiom" : "ipad",
+      "filename" : "Icon-App-29x29@1x.png",
+      "scale" : "1x"
+    },
+    {
+      "size" : "29x29",
+      "idiom" : "ipad",
+      "filename" : "Icon-App-29x29@2x.png",
+      "scale" : "2x"
+    },
+    {
+      "size" : "40x40",
+      "idiom" : "ipad",
+      "filename" : "Icon-App-40x40@1x.png",
+      "scale" : "1x"
+    },
+    {
+      "size" : "40x40",
+      "idiom" : "ipad",
+      "filename" : "Icon-App-40x40@2x.png",
+      "scale" : "2x"
+    },
+    {
+      "size" : "76x76",
+      "idiom" : "ipad",
+      "filename" : "Icon-App-76x76@1x.png",
+      "scale" : "1x"
+    },
+    {
+      "size" : "76x76",
+      "idiom" : "ipad",
+      "filename" : "Icon-App-76x76@2x.png",
+      "scale" : "2x"
+    },
+    {
+      "size" : "83.5x83.5",
+      "idiom" : "ipad",
+      "filename" : "Icon-App-83.5x83.5@2x.png",
+      "scale" : "2x"
+    },
+    {
+      "size" : "1024x1024",
+      "idiom" : "ios-marketing",
+      "filename" : "Icon-App-1024x1024@1x.png",
+      "scale" : "1x"
+    }
+  ],
+  "info" : {
+    "version" : 1,
+    "author" : "xcode"
+  }
+}
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png
new file mode 100644
index 00000000..a19857c5
Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png
new file mode 100644
index 00000000..1ab64271
Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png
new file mode 100644
index 00000000..8302b562
Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png
new file mode 100644
index 00000000..b0f40233
Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png
new file mode 100644
index 00000000..70f1840b
Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png
new file mode 100644
index 00000000..c489b362
Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png
new file mode 100644
index 00000000..0ec032c4
Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png
new file mode 100644
index 00000000..8302b562
Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png
new file mode 100644
index 00000000..f9d8a8fa
Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png
new file mode 100644
index 00000000..9e128eb8
Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png
new file mode 100644
index 00000000..9e128eb8
Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png
new file mode 100644
index 00000000..d202acc2
Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png
new file mode 100644
index 00000000..b491f39b
Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png
new file mode 100644
index 00000000..ac3ab80b
Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png
new file mode 100644
index 00000000..9e6af884
Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ
diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json
new file mode 100644
index 00000000..0bedcf2f
--- /dev/null
+++ b/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json
@@ -0,0 +1,23 @@
+{
+  "images" : [
+    {
+      "idiom" : "universal",
+      "filename" : "LaunchImage.png",
+      "scale" : "1x"
+    },
+    {
+      "idiom" : "universal",
+      "filename" : "LaunchImage@2x.png",
+      "scale" : "2x"
+    },
+    {
+      "idiom" : "universal",
+      "filename" : "LaunchImage@3x.png",
+      "scale" : "3x"
+    }
+  ],
+  "info" : {
+    "version" : 1,
+    "author" : "xcode"
+  }
+}
diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png
new file mode 100644
index 00000000..9da19eac
Binary files /dev/null and b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png differ
diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png
new file mode 100644
index 00000000..9da19eac
Binary files /dev/null and b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png differ
diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png
new file mode 100644
index 00000000..9da19eac
Binary files /dev/null and b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png differ
diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md
new file mode 100644
index 00000000..89c2725b
--- /dev/null
+++ b/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md
@@ -0,0 +1,5 @@
+# Launch Screen Assets
+
+You can customize the launch screen with your own desired assets by replacing the image files in this directory.
+
+You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images.
\ No newline at end of file
diff --git a/ios/Runner/Base.lproj/LaunchScreen.storyboard b/ios/Runner/Base.lproj/LaunchScreen.storyboard
new file mode 100644
index 00000000..f2e259c7
--- /dev/null
+++ b/ios/Runner/Base.lproj/LaunchScreen.storyboard
@@ -0,0 +1,37 @@
+
+
+    
+        
+        
+    
+    
+        
+        
+            
+                
+                    
+                        
+                        
+                    
+                    
+                        
+                        
+                            
+                            
+                        
+                        
+                        
+                            
+                            
+                        
+                    
+                
+                
+            
+            
+        
+    
+    
+        
+    
+
diff --git a/ios/Runner/Base.lproj/Main.storyboard b/ios/Runner/Base.lproj/Main.storyboard
new file mode 100644
index 00000000..f3c28516
--- /dev/null
+++ b/ios/Runner/Base.lproj/Main.storyboard
@@ -0,0 +1,26 @@
+
+
+    
+        
+        
+    
+    
+        
+        
+            
+                
+                    
+                        
+                        
+                    
+                    
+                        
+                        
+                        
+                    
+                
+                
+            
+        
+    
+
diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist
new file mode 100644
index 00000000..b2bacc3e
--- /dev/null
+++ b/ios/Runner/Info.plist
@@ -0,0 +1,45 @@
+
+
+
+
+	CFBundleDevelopmentRegion
+	en
+	CFBundleExecutable
+	$(EXECUTABLE_NAME)
+	CFBundleIdentifier
+	$(PRODUCT_BUNDLE_IDENTIFIER)
+	CFBundleInfoDictionaryVersion
+	6.0
+	CFBundleName
+	inventree_app
+	CFBundlePackageType
+	APPL
+	CFBundleShortVersionString
+	$(FLUTTER_BUILD_NAME)
+	CFBundleSignature
+	????
+	CFBundleVersion
+	$(FLUTTER_BUILD_NUMBER)
+	LSRequiresIPhoneOS
+	
+	UILaunchStoryboardName
+	LaunchScreen
+	UIMainStoryboardFile
+	Main
+	UISupportedInterfaceOrientations
+	
+		UIInterfaceOrientationPortrait
+		UIInterfaceOrientationLandscapeLeft
+		UIInterfaceOrientationLandscapeRight
+	
+	UISupportedInterfaceOrientations~ipad
+	
+		UIInterfaceOrientationPortrait
+		UIInterfaceOrientationPortraitUpsideDown
+		UIInterfaceOrientationLandscapeLeft
+		UIInterfaceOrientationLandscapeRight
+	
+	UIViewControllerBasedStatusBarAppearance
+	
+
+
diff --git a/ios/Runner/main.m b/ios/Runner/main.m
new file mode 100644
index 00000000..dff6597e
--- /dev/null
+++ b/ios/Runner/main.m
@@ -0,0 +1,9 @@
+#import 
+#import 
+#import "AppDelegate.h"
+
+int main(int argc, char* argv[]) {
+  @autoreleasepool {
+    return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
+  }
+}
diff --git a/lib/api.dart b/lib/api.dart
new file mode 100644
index 00000000..169535ec
--- /dev/null
+++ b/lib/api.dart
@@ -0,0 +1,313 @@
+import 'dart:async';
+import 'dart:convert';
+import 'dart:io';
+
+import 'package:flutter/material.dart';
+import 'package:flutter_advanced_networkimage/provider.dart';
+import 'package:image/image.dart';
+
+import 'package:path/path.dart' as path;
+import 'package:http/http.dart' as http;
+import 'package:shared_preferences/shared_preferences.dart';
+
+
+/**
+ * InvenTree API - Access to the InvenTree REST interface.
+ *
+ * InvenTree implements token-based authentication, which is
+ * initialised using a username:password combination.
+ */
+
+
+class InvenTreeAPI {
+
+  // Endpoint for requesting an API token
+  static const _URL_GET_TOKEN = "user/token/";
+  static const _URL_GET_VERSION = "";
+
+  // Base URL for InvenTree API e.g. http://192.168.120.10:8000
+  String _BASE_URL = "";
+
+  // Accessors for various url endpoints
+  String get baseUrl {
+    String url = _BASE_URL;
+
+    if (!url.endsWith("/")) {
+      url += "/";
+    }
+
+    return url;
+  }
+
+  String _makeUrl(String url) {
+    if (url.startsWith('/')) {
+      url = url.substring(1, url.length);
+    }
+
+    url = url.replaceAll('//', '/');
+
+    return baseUrl + url;
+  }
+
+  String get apiUrl {
+    return _makeUrl("/api/");
+  }
+
+  String get imageUrl {
+    return _makeUrl("/image/");
+  }
+
+  String makeApiUrl(String endpoint) {
+
+    return apiUrl + endpoint;
+  }
+
+  String makeUrl(String endpoint) {
+    return _makeUrl(endpoint);
+  }
+
+  String _username = "";
+  String _password = "";
+
+  // Authentication token (initially empty, must be requested)
+  String _token = "";
+
+  // Server version information
+  String _version;
+
+  // Getter for server version information
+  String get version => _version;
+
+  // Connection status flag - set once connection has been validated
+  bool _connected = false;
+
+  bool get connected {
+    return _connected && baseUrl.isNotEmpty && _token.isNotEmpty;
+  }
+
+  // Ensure we only ever create a single instance of the API class
+  static final InvenTreeAPI _api = new InvenTreeAPI._internal();
+
+  factory InvenTreeAPI() { return _api; }
+
+  InvenTreeAPI._internal();
+
+  Future connect() async {
+
+    var prefs = await SharedPreferences.getInstance();
+
+    String server = prefs.getString("server");
+    String username = prefs.getString("username");
+    String password = prefs.getString("password");
+
+    return connectToServer(server, username, password);
+  }
+
+  Future connectToServer(String address, String username, String password) async {
+
+    /* Address is the base address for the InvenTree server,
+     * e.g. http://127.0.0.1:8000
+     */
+
+    String errorMessage = "";
+
+    address = address.trim();
+    username = username.trim();
+
+    if (address.isEmpty || username.isEmpty || password.isEmpty) {
+      errorMessage = "Server Error: Empty details supplied";
+      print(errorMessage);
+      throw errorMessage;
+    }
+
+    if (!address.endsWith('/')) {
+      address = address + '/';
+    }
+
+    // TODO - Better URL validation
+
+    /*
+     * - If not a valid URL, return error
+     * - If no port supplied, append a default port
+     */
+
+    _BASE_URL = address;
+    _username = username;
+    _password = password;
+
+    _connected = false;
+
+    print("Connecting to " + apiUrl + " -> " + username + ":" + password);
+
+    // TODO - Add connection timeout
+
+    var response = await get("").timeout(Duration(seconds: 10)).catchError((error) {
+
+      if (error is SocketException) {
+        errorMessage = "Could not connect to server.";
+        print(errorMessage);
+        throw errorMessage;
+      } else {
+        // Unknown error type, re-throw error
+        throw error;
+      }
+    });
+
+    if (response.statusCode != 200) {
+      print("Invalid status code: " + response.statusCode.toString());
+      return false;
+    }
+
+    var data = json.decode(response.body);
+
+    print("Response from server: $data");
+
+    // We expect certain response from the server
+    if (!data.containsKey("server") || !data.containsKey("version")) {
+      errorMessage = "Server resonse contained incorrect data";
+      print(errorMessage);
+      throw errorMessage;
+    }
+
+    print("Server: " + data["server"]);
+    print("Version: " + data["version"]);
+
+    _version = data["version"];
+
+    // Request token from the server if we do not already have one
+    if (_token.isNotEmpty) {
+      print("Already have token - $_token");
+      return true;
+    }
+
+    // Clear out the token
+    _token = "";
+
+    response = await post(_URL_GET_TOKEN, body: {"username": _username, "password": _password}).catchError((error) {
+      print("Error requesting token:");
+      print(error);
+      return false;
+    });
+
+    if (response.statusCode != 200) {
+      print("Invalid status code: " + response.statusCode.toString());
+      return false;
+    } else {
+      var data = json.decode(response.body);
+
+      if (!data.containsKey("token")) {
+        print("No token provided in response");
+        return false;
+      }
+
+      // Return the received token
+      _token = data["token"];
+      print("Received token - $_token");
+
+      _connected = true;
+
+      return true;
+    };
+  }
+
+  // Perform a PATCH request
+  Future patch(String url, {Map body}) async {
+
+    var _url = makeApiUrl(url);
+    var _headers = defaultHeaders();
+    var _body = Map();
+
+    // Copy across provided data
+    body.forEach((K, V) => _body[K] = V);
+
+    print("PATCH: " + _url);
+
+    final response = await http.patch(_url,
+      headers: _headers,
+      body: _body,
+    );
+
+    return response;
+  }
+
+  // Perform a POST request
+  Future post(String url, {Map body}) async {
+
+    var _url = makeApiUrl(url);
+    var _headers = defaultHeaders();
+    var _body = Map();
+
+    // Copy across provided data
+    body.forEach((K, V) => _body[K] = V);
+
+    print("POST: " + _url);
+
+    return http.post(_url,
+      headers: _headers,
+      body: _body,
+    );
+  }
+
+  // Perform a GET request
+  Future get(String url, {Map params}) async {
+
+    var _url = makeApiUrl(url);
+    var _headers = defaultHeaders();
+
+    // If query parameters are supplied, form a query string
+    if (params != null && params.isNotEmpty) {
+      String query = '?';
+
+      params.forEach((K, V) => query += K + '=' + V + '&');
+
+      _url += query;
+    }
+
+    // Remove extraneous character if present
+    if (_url.endsWith('&')) {
+      _url = _url.substring(0, _url.length - 1);
+    }
+
+    print("GET: " + _url);
+
+    return http.get(_url, headers: _headers);
+  }
+
+  Map defaultHeaders() {
+
+    var headers = Map();
+
+    headers[HttpHeaders.authorizationHeader] = _authorizationHeader();
+    //headers['Authorization'] = _authorizationHeader();
+
+    return headers;
+  }
+
+  String _authorizationHeader () {
+    if (_token.isNotEmpty) {
+      return "Token $_token";
+    } else {
+      return "Basic " + base64Encode(utf8.encode('$_username:$_password'));
+    }
+  }
+
+  static String get staticImage => "/static/img/blank_image.png";
+
+  static String get staticThumb => "/static/img/blank_image.thumbnail.png";
+
+  /*
+   * Get an image from the server (or, from cache)
+   */
+  AdvancedNetworkImage getImage(String imageUrl) {
+
+    if (imageUrl.isEmpty) {
+      imageUrl = staticImage;
+    }
+
+    return new AdvancedNetworkImage(makeUrl(imageUrl),
+      header: defaultHeaders(),
+      useDiskCache: true,
+      cacheRule: CacheRule(maxAge: const Duration(days: 5)),
+    );
+  }
+}
\ No newline at end of file
diff --git a/lib/barcode.dart b/lib/barcode.dart
new file mode 100644
index 00000000..7716d7de
--- /dev/null
+++ b/lib/barcode.dart
@@ -0,0 +1,70 @@
+import 'package:flutter/cupertino.dart';
+import 'package:flutter/material.dart';
+import 'package:qr_utils/qr_utils.dart';
+
+import 'package:InvenTree/inventree/stock.dart';
+import 'package:InvenTree/inventree/part.dart';
+
+import 'package:InvenTree/widget/location_display.dart';
+import 'package:InvenTree/widget/part_display.dart';
+import 'package:InvenTree/widget/category_display.dart';
+import 'package:InvenTree/widget/stock_display.dart';
+
+import 'dart:convert';
+
+void scanQrCode(BuildContext context) async {
+
+  QrUtils.scanQR.then((String result) {
+
+    print("Scanned: $result");
+
+    // Look for JSON data in the result...
+    final data = json.decode(result);
+
+    // Look for an 'InvenTree' style barcode
+    if ((data['tool'] ?? '').toString().toLowerCase() == 'inventree') {
+      _handleInvenTreeBarcode(context, data);
+    }
+
+    // Unknown barcode style!
+    else {
+     showDialog(
+       context: context,
+       child: new SimpleDialog(
+         title: new Text("Unknown barcode"),
+         children: [
+           Text("Data: $result"),
+         ]
+       )
+     );
+    }
+
+  });
+}
+
+void _handleInvenTreeBarcode(BuildContext context, Map data) {
+
+  final String codeType = (data['type'] ?? '').toString().toLowerCase();
+
+  final int pk = (data['id'] ?? -1) as int;
+
+  if (codeType == 'stocklocation') {
+
+    // Try to open a stock location...
+    InvenTreeStockLocation().get(pk).then((var loc) {
+      if (loc is InvenTreeStockLocation) {
+        Navigator.push(context, MaterialPageRoute(builder: (context) => LocationDisplayWidget(loc)));
+      }
+    });
+
+  } else if (codeType == 'stockitem') {
+    InvenTreeStockItem().get(pk).then((var item) {
+      Navigator.push(context, MaterialPageRoute(builder: (context) => StockItemDisplayWidget(item)));
+    });
+  } else if (codeType == 'part') {
+    InvenTreePart().get(pk).then((var part) {
+      Navigator.push(context,
+          MaterialPageRoute(builder: (context) => PartDisplayWidget(part)));
+    });
+  }
+}
\ No newline at end of file
diff --git a/lib/generated/i18n.dart b/lib/generated/i18n.dart
new file mode 100644
index 00000000..2dcf8369
--- /dev/null
+++ b/lib/generated/i18n.dart
@@ -0,0 +1,122 @@
+import 'dart:async';
+
+import 'package:flutter/foundation.dart';
+import 'package:flutter/material.dart';
+
+// ignore_for_file: non_constant_identifier_names
+// ignore_for_file: camel_case_types
+// ignore_for_file: prefer_single_quotes
+
+// This file is automatically generated. DO NOT EDIT, all your changes would be lost.
+class S implements WidgetsLocalizations {
+  const S();
+
+  static const GeneratedLocalizationsDelegate delegate =
+    GeneratedLocalizationsDelegate();
+
+  static S of(BuildContext context) => Localizations.of
(context, S);
+
+  @override
+  TextDirection get textDirection => TextDirection.ltr;
+
+}
+
+class $en extends S {
+  const $en();
+}
+
+class GeneratedLocalizationsDelegate extends LocalizationsDelegate {
+  const GeneratedLocalizationsDelegate();
+
+  List get supportedLocales {
+    return const [
+      Locale("en", ""),
+    ];
+  }
+
+  LocaleListResolutionCallback listResolution({Locale fallback, bool withCountry = true}) {
+    return (List locales, Iterable supported) {
+      if (locales == null || locales.isEmpty) {
+        return fallback ?? supported.first;
+      } else {
+        return _resolve(locales.first, fallback, supported, withCountry);
+      }
+    };
+  }
+
+  LocaleResolutionCallback resolution({Locale fallback, bool withCountry = true}) {
+    return (Locale locale, Iterable supported) {
+      return _resolve(locale, fallback, supported, withCountry);
+    };
+  }
+
+  @override
+  Future load(Locale locale) {
+    final String lang = getLang(locale);
+    if (lang != null) {
+      switch (lang) {
+        case "en":
+          return SynchronousFuture(const $en());
+        default:
+          // NO-OP.
+      }
+    }
+    return SynchronousFuture(const S());
+  }
+
+  @override
+  bool isSupported(Locale locale) => _isSupported(locale, true);
+
+  @override
+  bool shouldReload(GeneratedLocalizationsDelegate old) => false;
+
+  ///
+  /// Internal method to resolve a locale from a list of locales.
+  ///
+  Locale _resolve(Locale locale, Locale fallback, Iterable supported, bool withCountry) {
+    if (locale == null || !_isSupported(locale, withCountry)) {
+      return fallback ?? supported.first;
+    }
+
+    final Locale languageLocale = Locale(locale.languageCode, "");
+    if (supported.contains(locale)) {
+      return locale;
+    } else if (supported.contains(languageLocale)) {
+      return languageLocale;
+    } else {
+      final Locale fallbackLocale = fallback ?? supported.first;
+      return fallbackLocale;
+    }
+  }
+
+  ///
+  /// Returns true if the specified locale is supported, false otherwise.
+  ///
+  bool _isSupported(Locale locale, bool withCountry) {
+    if (locale != null) {
+      for (Locale supportedLocale in supportedLocales) {
+        // Language must always match both locales.
+        if (supportedLocale.languageCode != locale.languageCode) {
+          continue;
+        }
+
+        // If country code matches, return this locale.
+        if (supportedLocale.countryCode == locale.countryCode) {
+          return true;
+        }
+
+        // If no country requirement is requested, check if this locale has no country.
+        if (true != withCountry && (supportedLocale.countryCode == null || supportedLocale.countryCode.isEmpty)) {
+          return true;
+        }
+      }
+    }
+    return false;
+  }
+}
+
+String getLang(Locale l) => l == null
+  ? null
+  : l.countryCode != null && l.countryCode.isEmpty
+    ? l.languageCode
+    : l.toString();
diff --git a/lib/inventree/company.dart b/lib/inventree/company.dart
new file mode 100644
index 00000000..31a1d14c
--- /dev/null
+++ b/lib/inventree/company.dart
@@ -0,0 +1,45 @@
+import 'model.dart';
+
+
+/*
+ * The InvenTreeCompany class repreents the Company model in the InvenTree database.
+ */
+class InvenTreeCompany extends InvenTreeModel {
+  @override
+  String URL = "company/";
+
+  InvenTreeCompany() : super();
+
+  InvenTreeCompany.fromJson(Map json) : super.fromJson(json) {
+    // TODO
+  }
+
+  @override
+  InvenTreeModel createFromJson(Map json) {
+    var company = InvenTreeCompany.fromJson(json);
+
+    return company;
+  }
+}
+
+
+/*
+ * The InvenTreeSupplierPart class represents the SupplierPart model in the InvenTree database
+ */
+class InvenTreeSupplierPart extends InvenTreeModel {
+  @override
+  String url = "company/part/";
+
+  InvenTreeSupplierPart() : super();
+
+  InvenTreeSupplierPart.fromJson(Map json) : super.fromJson(json) {
+    // TODO
+  }
+
+  @override
+  InvenTreeModel createFromJson(Map json) {
+    var part = InvenTreeSupplierPart.fromJson(json);
+
+    return part;
+  }
+}
\ No newline at end of file
diff --git a/lib/inventree/model.dart b/lib/inventree/model.dart
new file mode 100644
index 00000000..8bf3bee0
--- /dev/null
+++ b/lib/inventree/model.dart
@@ -0,0 +1,167 @@
+import 'package:InvenTree/api.dart';
+
+import 'dart:convert';
+
+import 'package:path/path.dart' as path;
+import 'package:http/http.dart' as http;
+
+
+/**
+ * The InvenTreeModel class provides a base-level object
+ * for interacting with InvenTree data.
+ */
+class InvenTreeModel {
+
+  // Override the endpoint URL for each subclass
+  String URL = "";
+
+  // JSON data which defines this object
+  Map jsondata = {};
+
+  // Accessor for the API
+  var api = InvenTreeAPI();
+
+  // Default empty object constructor
+  InvenTreeModel() {
+    jsondata.clear();
+  }
+
+  // Construct an InvenTreeModel from a JSON data object
+  InvenTreeModel.fromJson(Map json) {
+
+    // Store the json object
+    jsondata = json;
+
+  }
+
+  int get pk => jsondata['pk'] ?? -1;
+
+  // Some common accessors
+  String get name => jsondata['name'] ?? '';
+
+  String get description => jsondata['description'] ?? '';
+
+  int get parentId => jsondata['parent'] ?? -1;
+
+  // Create a new object from JSON data (not a constructor!)
+  InvenTreeModel createFromJson(Map json) {
+
+      var obj = InvenTreeModel.fromJson(json);
+
+      return obj;
+  }
+
+  String get url{ return path.join(URL, pk.toString()); }
+
+  /*
+  // Search this Model type in the database
+  Future> search(String searchTerm) async {
+
+    String addr = url + "?search=" + search;
+
+    print("Searching endpoint: $url");
+
+    // TODO - Add "timeout"
+    // TODO - Add error catching
+
+    var response =
+
+  }
+  */
+
+  // Return the detail view for the associated pk
+  Future get(int pk) async {
+
+    // TODO - Add "timeout"
+    // TODO - Add error catching
+
+    var addr = path.join(URL, pk.toString());
+
+    if (!addr.endsWith("/")) {
+      addr += "/";
+    }
+
+    var response = await InvenTreeAPI().get(addr);
+
+    if (response.statusCode != 200) {
+      print("Error retrieving data");
+      return null;
+    }
+
+    final data = json.decode(response.body);
+
+    return createFromJson(data);
+  }
+
+  // Return list of objects from the database, with optional filters
+  Future> list({Map filters}) async {
+
+    if (filters == null) {
+      filters = {};
+    }
+
+    print("Listing endpoint: $URL");
+
+    // TODO - Add "timeout"
+    // TODO - Add error catching
+
+    var response = await InvenTreeAPI().get(URL, params:filters);
+
+    // A list of "InvenTreeModel" items
+    List results = new List();
+
+    if (response.statusCode != 200) {
+      print("Error retreiving data");
+      return results;
+    }
+
+    final data = json.decode(response.body);
+
+    // TODO - handle possible error cases:
+    // - No data receieved
+    // - Data is not a list of maps
+
+    for (var d in data) {
+
+      // Create a new object (of the current class type
+      InvenTreeModel obj = createFromJson(d);
+
+      if (obj != null) {
+        results.add(obj);
+      }
+    }
+
+    return results;
+  }
+
+
+  // Provide a listing of objects at the endpoint
+  // TODO - Static function which returns a list of objects (of this class)
+
+  // TODO - Define a 'delete' function
+
+  // TODO - Define a 'save' / 'update' function
+
+  // Override this function for each sub-class
+  bool matchAgainstString(String filter) => false;
+
+  // Filter this item against a list of provided filters
+  // Each filter must be matched
+  // Used for (e.g.) filtering returned results
+  bool filter(String filterString) {
+
+    List filters = filterString.trim().toLowerCase().split(" ");
+
+    for (var f in filters) {
+      if (!matchAgainstString(f)) {
+        return false;
+      }
+    }
+
+    return true;
+  }
+
+
+}
+
+
diff --git a/lib/inventree/part.dart b/lib/inventree/part.dart
new file mode 100644
index 00000000..5c9403c2
--- /dev/null
+++ b/lib/inventree/part.dart
@@ -0,0 +1,74 @@
+import 'package:InvenTree/api.dart';
+
+import 'model.dart';
+import 'dart:io';
+
+import 'package:path/path.dart' as path;
+import 'package:http/http.dart' as http;
+
+class InvenTreePartCategory extends InvenTreeModel {
+  @override
+  String URL = "part/category/";
+
+  String get pathstring => jsondata['pathstring'] ?? '';
+
+  InvenTreePartCategory() : super();
+
+  InvenTreePartCategory.fromJson(Map json) : super.fromJson(json) {
+
+  }
+
+  @override
+  InvenTreeModel createFromJson(Map json) {
+    var cat = InvenTreePartCategory.fromJson(json);
+
+    // TODO ?
+
+    return cat;
+  }
+}
+
+
+class InvenTreePart extends InvenTreeModel {
+
+  @override
+  String URL = "part/";
+
+  int get categoryId => jsondata['category'] as int ?? -1;
+
+  String get categoryName => jsondata['category__name'] ?? '';
+
+  String get _image  => jsondata['image'] ?? '';
+
+  String get _thumbnail => jsondata['thumbnail'] ?? '';
+
+  // Return a path to the image for this Part
+  String get image {
+    // Use thumbnail as a backup
+    String img = _image.isNotEmpty ? _image : _thumbnail;
+
+    return img.isNotEmpty ? img : InvenTreeAPI.staticImage;
+  }
+
+  // Return a path to the thumbnail for this part
+  String get thumbnail {
+    // Use image as a backup
+    String img = _thumbnail.isNotEmpty ? _thumbnail : _image;
+
+    return img.isNotEmpty ? img : InvenTreeAPI.staticThumb;
+  }
+
+  InvenTreePart() : super();
+
+  InvenTreePart.fromJson(Map json) : super.fromJson(json) {
+    // TODO
+  }
+
+  @override
+  InvenTreeModel createFromJson(Map json) {
+
+    var part = InvenTreePart.fromJson(json);
+
+    return part;
+  }
+}
\ No newline at end of file
diff --git a/lib/inventree/stock.dart b/lib/inventree/stock.dart
new file mode 100644
index 00000000..842f32b8
--- /dev/null
+++ b/lib/inventree/stock.dart
@@ -0,0 +1,75 @@
+import 'model.dart';
+
+import 'package:InvenTree/api.dart';
+
+class InvenTreeStockItem extends InvenTreeModel {
+  @override
+  String URL = "stock/";
+
+  InvenTreeStockItem() : super();
+
+  InvenTreeStockItem.fromJson(Map json) : super.fromJson(json) {
+    // TODO
+  }
+
+  String get partName => jsondata['part__name'] as String ?? '';
+
+  String get partDescription => jsondata['part__description'] as String ?? '';
+
+  String get partThumbnail => jsondata['part__thumbnail'] as String ?? InvenTreeAPI.staticThumb;
+
+  int get serialNumber => jsondata['serial'] as int ?? null;
+
+  double get quantity => jsondata['quantity'] as double ?? 0.0;
+
+  int get locationId => jsondata['location'] as int ?? -1;
+
+  String get displayQuantity {
+    // Display either quantity or serial number!
+
+    if (serialNumber != null) {
+      return "SN: $serialNumber";
+    } else {
+      return quantity.toString().trim();
+    }
+  }
+
+  @override
+  InvenTreeModel createFromJson(Map json) {
+    var item = InvenTreeStockItem.fromJson(json);
+
+    // TODO?
+
+    return item;
+  }
+}
+
+
+class InvenTreeStockLocation extends InvenTreeModel {
+  @override
+  String URL = "stock/location/";
+
+  InvenTreeStockLocation() : super();
+
+  InvenTreeStockLocation.fromJson(Map json) : super.fromJson(json) {
+
+  }
+
+  @override
+  InvenTreeModel createFromJson(Map json) {
+
+    var loc = InvenTreeStockLocation.fromJson(json);
+
+    return loc;
+  }
+
+  @override
+  bool matchAgainstString(String filter) {
+
+    if (name.toLowerCase().contains(filter)) return true;
+
+    if (description.toLowerCase().contains(filter)) return true;
+
+    return false;
+  }
+}
\ No newline at end of file
diff --git a/lib/login_settings.dart b/lib/login_settings.dart
new file mode 100644
index 00000000..f5a3b0b2
--- /dev/null
+++ b/lib/login_settings.dart
@@ -0,0 +1,143 @@
+import 'package:flutter/material.dart';
+import 'package:shared_preferences/shared_preferences.dart';
+
+import 'api.dart';
+import 'preferences.dart';
+
+class InvenTreeLoginSettingsWidget extends StatefulWidget {
+
+  @override
+  _InvenTreeLoginSettingsState createState() => _InvenTreeLoginSettingsState();
+}
+
+
+class _InvenTreeLoginSettingsState extends State {
+
+  final GlobalKey _formKey = new GlobalKey();
+
+  String _addr;
+  String _user;
+  String _pass;
+
+  String _validateServer(String value) {
+
+    if (value.isEmpty) {
+      return 'Server cannot be empty';
+    }
+
+    if (!value.startsWith("http:") && !value.startsWith("https:")) {
+      return 'Server must start with http[s]';
+    }
+
+    return null;
+  }
+
+  String _validateUsername(String value) {
+    if (value.isEmpty) {
+      return 'Username cannot be empty';
+    }
+
+    return null;
+  }
+
+  String _validatePassword(String value) {
+    if (value.isEmpty) {
+      return 'Password cannot be empty';
+    }
+
+    return null;
+  }
+
+  @override
+  void initState() {
+    load();
+  }
+
+  @override
+  Widget build(BuildContext context) {
+
+    final Size screenSize = MediaQuery.of(context).size;
+
+    load();
+
+    return Scaffold(
+      appBar: AppBar(
+        title: Text("Login Settings"),
+      ),
+      body: new Container(
+        padding: new EdgeInsets.all(20.0),
+        child: new Form(
+          key: _formKey,
+          child: new ListView(
+            children: [
+              Text("Server"),
+              new TextFormField(
+                initialValue: _addr,
+                decoration: InputDecoration(
+                  hintText: "127.0.0.1:8000",
+                  labelText: "Server:Port",
+                ),
+                validator: _validateServer,
+                onSaved: (String value) {
+                  _addr = value;
+                },
+              ),
+              Text("Login Details"),
+              TextFormField(
+                initialValue: _user,
+                decoration: InputDecoration(
+                  hintText: "Username",
+                  labelText: "Username",
+                ),
+                validator: _validateUsername,
+                onSaved: (String value) {
+                  _user = value;
+                }
+              ),
+              TextFormField(
+                initialValue: _pass,
+                obscureText: true,
+                decoration: InputDecoration(
+                  hintText: "Password",
+                  labelText: "Password",
+                ),
+                validator: _validatePassword,
+                onSaved: (String value) {
+                  _pass = value;
+                },
+              ),
+              Container(
+                width: screenSize.width,
+                child: RaisedButton(
+                  child: Text("Login"),
+                  onPressed: this.save,
+                )
+              )
+            ],
+          )
+        )
+      )
+    );
+  }
+
+  void load() async {
+    SharedPreferences prefs = await SharedPreferences.getInstance();
+
+    _addr = prefs.getString('server');
+    _user = prefs.getString('username');
+    _pass = prefs.getString('password');
+
+    // Refresh the widget
+    setState(() {
+    });
+  }
+
+  void save() async {
+    if (_formKey.currentState.validate()) {
+      _formKey.currentState.save();
+
+      await InvenTreeUserPreferences().saveLoginDetails(_addr, _user, _pass);
+
+    }
+  }
+}
\ No newline at end of file
diff --git a/lib/main.dart b/lib/main.dart
new file mode 100644
index 00000000..26877035
--- /dev/null
+++ b/lib/main.dart
@@ -0,0 +1,307 @@
+import 'dart:async';
+
+import 'package:InvenTree/inventree/stock.dart';
+import 'package:InvenTree/widget/category_display.dart';
+import 'package:InvenTree/widget/location_display.dart';
+import 'package:InvenTree/widget/drawer.dart';
+import 'package:flutter/cupertino.dart';
+import 'package:flutter/material.dart';
+import 'package:shared_preferences/shared_preferences.dart';
+
+import 'barcode.dart';
+
+import 'dart:convert';
+
+import 'settings.dart';
+import 'api.dart';
+import 'preferences.dart';
+
+import 'package:InvenTree/inventree/part.dart';
+
+void main() async {
+
+  // await PrefService.init(prefix: "inventree_");
+
+  WidgetsFlutterBinding.ensureInitialized();
+
+  // Load login details
+  InvenTreeUserPreferences().loadLoginDetails();
+
+  runApp(MyApp());
+}
+
+class MyApp extends StatelessWidget {
+  // This widget is the root of your application.
+
+  @override
+  Widget build(BuildContext context) {
+    return MaterialApp(
+      title: 'InvenTree',
+      theme: ThemeData(
+        // This is the theme of your application.
+        //
+        // Try running your application with "flutter run". You'll see the
+        // application has a blue toolbar. Then, without quitting the app, try
+        // changing the primarySwatch below to Colors.green and then invoke
+        // "hot reload" (press "r" in the console where you ran "flutter run",
+        // or simply save your changes to "hot reload" in a Flutter IDE).
+        // Notice that the counter didn't reset back to zero; the application
+        // is not restarted.
+        primarySwatch: Colors.lightGreen,
+      ),
+      home: MyHomePage(title: 'InvenTree'),
+    );
+  }
+}
+
+
+class ProductList extends StatelessWidget {
+  final List _parts;
+
+  ProductList(this._parts);
+
+  Widget _buildPart(BuildContext context, int index) {
+    InvenTreePart part;
+
+    if (index < _parts.length) {
+      part = _parts[index];
+    }
+
+    return Card(
+        child: Column(
+            children: [
+              Text('${part.name} - ${part.description}'),
+            ]
+        )
+    );
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return ListView.builder(itemBuilder: _buildPart, itemCount: _parts.length);
+  }
+}
+
+
+class MyHomePage extends StatefulWidget {
+  MyHomePage({Key key, this.title}) : super(key: key);
+
+
+  // This widget is the home page of your application. It is stateful, meaning
+  // that it has a State object (defined below) that contains fields that affect
+  // how it looks.
+
+  // This class is the configuration for the state. It holds the values (in this
+  // case the title) provided by the parent (in this case the App widget) and
+  // used by the build method of the State. Fields in a Widget subclass are
+  // always marked "final".
+
+  final String title;
+
+  @override
+  _MyHomePageState createState() => _MyHomePageState();
+}
+
+class _MyHomePageState extends State {
+
+  _MyHomePageState() : super() {
+    _checkServerConnection();
+  }
+
+  String _serverAddress = "";
+
+  String _serverStatus = "Connecting to server";
+
+  String _serverMessage = "";
+
+  bool _serverConnection = false;
+
+  Color _serverStatusColor = Color.fromARGB(255, 50, 50, 250);
+
+  /*
+   * Test the server connection
+   */
+  void _checkServerConnection() async {
+
+    var prefs = await SharedPreferences.getInstance();
+
+    print("Checking server connection");
+
+    _serverAddress = prefs.getString("server");
+
+    InvenTreeAPI().connect().then((bool result) {
+      print("Connection status: $result");
+      _serverConnection = result;
+
+      if (_serverConnection) {
+        _serverStatus = "Connected to server: $_serverAddress";
+        _serverMessage = "";
+        _serverStatusColor = Color.fromARGB(255, 50, 250, 50);
+      } else {
+        _serverStatus = "Could not connect to server: $_serverAddress";
+        _serverStatusColor = Color.fromARGB(255, 250, 50, 50);
+      }
+
+      setState(() {});
+
+    }).catchError((e) {
+      _serverConnection = false;
+      _serverStatusColor = Color.fromARGB(255, 250, 50, 50);
+
+      _serverStatus = "Error connecting to $_serverAddress";
+
+      if (e is TimeoutException) {
+        _serverMessage = "No response from server";
+      } else {
+        _serverMessage = e.toString();
+      }
+
+      print("Server error: $_serverMessage");
+
+      setState(() {});
+    });
+  }
+
+  void _search() {
+    // TODO
+  }
+
+  void _scan() {
+    scanQrCode(context);
+  }
+
+  void _parts() {
+    Navigator.push(context, MaterialPageRoute(builder: (context) => CategoryDisplayWidget(null)));
+  }
+
+  void _stock() {
+    Navigator.push(context, MaterialPageRoute(builder: (context) => LocationDisplayWidget(null)));
+  }
+
+  void _suppliers() {
+    // TODO
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    // This method is rerun every time setState is called, for instance as done
+    // by the _incrementCounter method above.
+    //
+    // The Flutter framework has been optimized to make rerunning build methods
+    // fast, so that you can just rebuild anything that needs updating rather
+    // than having to individually change instances of widgets.
+    return Scaffold(
+      appBar: AppBar(
+        // Here we take the value from the MyHomePage object that was created by
+        // the App.build method, and use it to set our appbar title.
+        title: Text(widget.title),
+        actions: [
+          IconButton(
+            icon: Icon(Icons.search),
+            tooltip: 'Search',
+            onPressed: null,
+          ),
+        ],
+      ),
+      drawer: new InvenTreeDrawer(context),
+      body: Center(
+        // Center is a layout widget. It takes a single child and positions it
+        // in the middle of the parent.
+        child: Column(
+          mainAxisAlignment: MainAxisAlignment.center,
+          children: [
+            Spacer(),
+            Row(
+              mainAxisAlignment: MainAxisAlignment.spaceEvenly,
+              children: [
+                Column(
+                  children: [
+                   IconButton(
+                     icon: new Icon(Icons.search),
+                     tooltip: 'Search',
+                     onPressed: _search,
+                   ),
+                   Text("Search"),
+                  ],
+                ),
+                Column(
+                  children: [
+                    IconButton(
+                      icon: new Icon(Icons.search),
+                      tooltip: 'Scan Barcode',
+                      onPressed: _scan,
+                    ),
+                    Text("Scan Barcode"),
+                  ],
+                ),
+              ],
+            ),
+            Spacer(),
+            Row(
+              mainAxisAlignment: MainAxisAlignment.spaceEvenly,
+              children: [
+                Column(
+                  children: [
+                    IconButton(
+                      icon: new Icon(Icons.category),
+                      tooltip: 'Parts',
+                      onPressed: _parts,
+                    ),
+                    Text("Parts"),
+                  ],
+                ),
+                Column(
+                  children: [
+                    IconButton(
+                      icon: new Icon(Icons.map),
+                      tooltip: 'Stock',
+                      onPressed: _stock,
+                    ),
+                    Text('Stock'),
+                  ],
+                ),
+                Column(
+                  children: [
+                    IconButton(
+                      icon: new Icon(Icons.business),
+                      tooltip: 'Suppliers',
+                      onPressed: _suppliers,
+                    ),
+                    Text("Suppliers"),
+                  ]
+                )
+              ],
+            ),
+            Spacer(),
+            Row(
+              mainAxisAlignment: MainAxisAlignment.center,
+              crossAxisAlignment: CrossAxisAlignment.center,
+              children: [
+                Expanded(
+                  child: Card(
+                    child: Column(
+                    mainAxisAlignment: MainAxisAlignment.center,
+                    crossAxisAlignment: CrossAxisAlignment.center,
+                    children: [
+                      Text('$_serverStatus',
+                        style: TextStyle(
+                          color: _serverStatusColor,
+                          ),
+                        ),
+                      Text('$_serverMessage',
+                        style: TextStyle(
+                          color: _serverStatusColor,
+                        ),
+                      ),
+                    ],
+                    ),
+                  ),
+                ),
+              ],
+            ),
+          ],
+        ),
+      ),
+    );
+  }
+}
diff --git a/lib/preferences.dart b/lib/preferences.dart
new file mode 100644
index 00000000..53aa19bc
--- /dev/null
+++ b/lib/preferences.dart
@@ -0,0 +1,45 @@
+import 'package:shared_preferences/shared_preferences.dart';
+import 'api.dart';
+
+
+class InvenTreeUserPreferences {
+
+  static const String _SERVER = 'server';
+  static const String _USERNAME = 'username';
+  static const String _PASSWORD = 'password';
+
+  // Ensure we only ever create a single instance of the preferences class
+  static final InvenTreeUserPreferences _api = new InvenTreeUserPreferences._internal();
+
+  factory InvenTreeUserPreferences() {
+    return _api;
+  }
+
+  InvenTreeUserPreferences._internal();
+
+  // Load saved login details, and attempt connection
+  void loadLoginDetails() async {
+
+    print("Loading login details");
+
+    SharedPreferences prefs = await SharedPreferences.getInstance();
+
+    var server = prefs.getString(_SERVER) ?? '';
+    var username = prefs.getString(_USERNAME) ?? '';
+    var password = prefs.getString(_PASSWORD) ?? '';
+
+    await InvenTreeAPI().connectToServer(server, username, password);
+  }
+
+  void saveLoginDetails(String server, String username, String password) async {
+
+    SharedPreferences prefs = await SharedPreferences.getInstance();
+
+    await prefs.setString(_SERVER, server);
+    await prefs.setString(_USERNAME, username);
+    await prefs.setString(_PASSWORD, password);
+
+    // Reconnect the API
+    await InvenTreeAPI().connectToServer(server, username, password);
+  }
+}
\ No newline at end of file
diff --git a/lib/settings.dart b/lib/settings.dart
new file mode 100644
index 00000000..4514d88e
--- /dev/null
+++ b/lib/settings.dart
@@ -0,0 +1,76 @@
+import 'package:flutter/material.dart';
+
+import 'login_settings.dart';
+
+import 'package:package_info/package_info.dart';
+
+class InvenTreeSettingsWidget extends StatefulWidget {
+  // InvenTree settings view
+
+  @override
+  _InvenTreeSettingsState createState() => _InvenTreeSettingsState();
+
+}
+
+
+class _InvenTreeSettingsState extends State {
+  @override
+  Widget build(BuildContext context) {
+    return Scaffold(
+      appBar: AppBar(
+        title: Text("InvenTree Settings"),
+      ),
+      body: Center(
+        child: ListView(
+          children: [
+            ListTile(
+                title: Text("Server Settings"),
+                subtitle: Text("Configure server and login settings"),
+                onTap: _editServerSettings,
+            ),
+            Divider(),
+            ListTile(
+              title: Text("About"),
+              subtitle: Text("App details"),
+              onTap: _about,
+            ),
+          ],
+        )
+      )
+    );
+  }
+
+  void _editServerSettings() {
+    Navigator.push(context, MaterialPageRoute(builder: (context) => InvenTreeLoginSettingsWidget()));
+  }
+
+  void _about() async {
+
+    PackageInfo.fromPlatform().then((PackageInfo info) {
+      showDialog(
+        context: context,
+        child: new SimpleDialog(
+            title: new Text("About InvenTree"),
+            children: [
+                ListTile(
+                  title: Text("App Name"),
+                  subtitle: Text("${info.appName}"),
+                ),
+                ListTile(
+                  title: Text("App Version"),
+                  subtitle: Text("${info.version}"),
+                ),
+                ListTile(
+                  title: Text("Package Name"),
+                  subtitle: Text("${info.packageName}"),
+                ),
+                ListTile(
+                  title: Text("Build Number"),
+                  subtitle: Text("${info.buildNumber}")
+                ),
+            ]
+        ),
+      );
+    });
+  }
+}
\ No newline at end of file
diff --git a/lib/widget/category_display.dart b/lib/widget/category_display.dart
new file mode 100644
index 00000000..7a30fd91
--- /dev/null
+++ b/lib/widget/category_display.dart
@@ -0,0 +1,197 @@
+
+import 'package:InvenTree/api.dart';
+import 'package:InvenTree/inventree/part.dart';
+
+import 'package:InvenTree/widget/part_display.dart';
+import 'package:InvenTree/widget/drawer.dart';
+
+import 'package:flutter/cupertino.dart';
+import 'package:flutter/foundation.dart';
+import 'package:flutter/material.dart';
+
+import 'package:flutter_advanced_networkimage/provider.dart';
+
+class CategoryDisplayWidget extends StatefulWidget {
+
+  CategoryDisplayWidget(this.category, {Key key}) : super(key: key);
+
+  final InvenTreePartCategory category;
+
+  final String title = "Category";
+
+  @override
+  _CategoryDisplayState createState() => _CategoryDisplayState(category);
+}
+
+
+class _CategoryDisplayState extends State {
+
+  _CategoryDisplayState(this.category) {
+    _requestData();
+  }
+
+  // The local InvenTreePartCategory object
+  final InvenTreePartCategory category;
+
+  List _subcategories = List();
+
+  List _parts = List();
+
+  String get _titleString {
+
+    if (category == null) {
+      return "Part Categories";
+    } else {
+      return "Part Category '${category.name}'";
+    }
+  }
+
+  /*
+   * Request data from the server
+   */
+  void _requestData() {
+
+    int pk = category?.pk ?? -1;
+
+    // Request a list of sub-categories under this one
+    InvenTreePartCategory().list(filters: {"parent": "$pk"}).then((var cats) {
+      _subcategories.clear();
+
+      for (var cat in cats) {
+        if (cat is InvenTreePartCategory) {
+          _subcategories.add(cat);
+        }
+      }
+
+      // Update state
+      setState(() {});
+    });
+
+    // Request a list of parts under this category
+    InvenTreePart().list(filters: {"category": "$pk"}).then((var parts) {
+      _parts.clear();
+
+      for (var part in parts) {
+        if (part is InvenTreePart) {
+          _parts.add(part);
+        }
+      }
+
+      // Update state
+      setState(() {});
+    });
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return Scaffold(
+      appBar: AppBar(
+        title: Text(_titleString),
+      ),
+      drawer: new InvenTreeDrawer(context),
+      body: Center(
+        child: Column(
+          mainAxisAlignment: MainAxisAlignment.center,
+          children: [
+            Text(
+              "Subcategories - ${_subcategories.length}",
+              textAlign: TextAlign.left,
+              style: TextStyle(fontWeight: FontWeight.bold),
+            ),
+            Expanded(child: SubcategoryList(_subcategories)),
+            Divider(),
+            Text("Parts - ${_parts.length}",
+              textAlign: TextAlign.left,
+              style: TextStyle(fontWeight: FontWeight.bold),
+            ),
+            Expanded(child: PartList(_parts)),
+          ]
+        )
+      )
+    );
+  }
+}
+
+
+/*
+ * Builder for displaying a list of PartCategory objects
+ */
+class SubcategoryList extends StatelessWidget {
+  final List _categories;
+
+  SubcategoryList(this._categories);
+
+  void _openCategory(BuildContext context, int pk) {
+
+    // Attempt to load the sub-category.
+    InvenTreePartCategory().get(pk).then((var cat) {
+      if (cat is InvenTreePartCategory) {
+
+        Navigator.push(context, MaterialPageRoute(builder: (context) => CategoryDisplayWidget(cat)));
+      }
+    });
+  }
+
+  Widget _build(BuildContext context, int index) {
+    InvenTreePartCategory cat = _categories[index];
+
+    return ListTile(
+      title: Text("${cat.name}"),
+      subtitle: Text("${cat.description}"),
+      onTap: () {
+        _openCategory(context, cat.pk);
+      }
+    );
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return ListView.builder(itemBuilder: _build, itemCount: _categories.length);
+  }
+}
+
+
+/*
+ * Builder for displaying a list of Part objects
+ */
+class PartList extends StatelessWidget {
+  final List _parts;
+
+  PartList(this._parts);
+
+  void _openPart(BuildContext context, int pk) {
+    // Attempt to load the part information
+    InvenTreePart().get(pk).then((var part) {
+      if (part is InvenTreePart) {
+
+        Navigator.push(context, MaterialPageRoute(builder: (context) => PartDisplayWidget(part)));
+      }
+    });
+  }
+
+  Widget _build(BuildContext context, int index) {
+    InvenTreePart part;
+
+    if (index < _parts.length) {
+      part = _parts[index];
+    }
+
+    return ListTile(
+      title: Text("${part.name}"),
+      subtitle: Text("${part.description}"),
+      leading: Image(
+        image: InvenTreeAPI().getImage(part.thumbnail),
+        width: 48,
+      ),
+      onTap: () {
+        _openPart(context, part.pk);
+      },
+    );
+
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return ListView.builder(itemBuilder: _build, itemCount: _parts.length);
+  }
+}
diff --git a/lib/widget/drawer.dart b/lib/widget/drawer.dart
new file mode 100644
index 00000000..1bec27c2
--- /dev/null
+++ b/lib/widget/drawer.dart
@@ -0,0 +1,114 @@
+import 'package:InvenTree/barcode.dart';
+import 'package:flutter/material.dart';
+
+import 'package:InvenTree/widget/category_display.dart';
+import 'package:InvenTree/widget/location_display.dart';
+
+import 'package:InvenTree/settings.dart';
+
+class InvenTreeDrawer extends StatelessWidget {
+
+  final BuildContext context;
+
+  InvenTreeDrawer(this.context);
+
+  void _closeDrawer() {
+    // Close the drawer
+    Navigator.of(context).pop();
+  }
+
+  /*
+   * Return to the 'home' screen.
+   * This will empty the navigation stack.
+   */
+  void _home() {
+    _closeDrawer();
+
+    Navigator.pushNamedAndRemoveUntil(context, "/", (r) => false);
+  }
+
+  /*
+   * Launch the camera to scan a QR code.
+   * Upon successful scan, data are passed off to be decoded.
+   */
+  void _scan() async {
+
+    _closeDrawer();
+    scanQrCode(context);
+  }
+
+  /*
+   * Display the top-level PartCategory list
+   */
+  void _showParts() {
+
+    _closeDrawer();
+    Navigator.push(context, MaterialPageRoute(builder: (context) => CategoryDisplayWidget(null)));
+  }
+
+  /*
+   * Display the top-level StockLocation list
+   */
+  void _showStock() {
+    _closeDrawer();
+    Navigator.push(context, MaterialPageRoute(builder: (context) => LocationDisplayWidget(null)));
+  }
+
+  /*
+   * Load settings widget
+   */
+  void _settings() {
+    _closeDrawer();
+    Navigator.push(context, MaterialPageRoute(builder: (context) => InvenTreeSettingsWidget()));
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return  Drawer(
+        child: new ListView(
+            children: [
+              new ListTile(
+                leading: new Image.asset(
+                  "assets/image/icon.png",
+                  fit: BoxFit.scaleDown,
+                ),
+                title: new Text("InvenTree"),
+                onTap: _home,
+              ),
+              new Divider(),
+              new ListTile(
+                title: new Text("Search"),
+                leading: new Icon(Icons.search),
+                onTap: null,
+              ),
+              new ListTile(
+                title: new Text("Scan"),
+                onTap: _scan,
+                leading: new Icon(Icons.search),
+              ),
+              new Divider(),
+              new ListTile(
+                title: new Text("Parts"),
+                leading: new Icon(Icons.category),
+                onTap: _showParts,
+              ),
+              new ListTile(
+                title: new Text("Stock"),
+                onTap: _showStock,
+              ),
+              new ListTile(
+                title: new Text("Suppliers"),
+                leading: new Icon(Icons.business),
+                onTap: null,
+              ),
+              new Divider(),
+              new ListTile(
+                title: new Text("Settings"),
+                leading: new Icon(Icons.settings),
+                onTap: _settings,
+              ),
+            ]
+        )
+    );
+  }
+}
\ No newline at end of file
diff --git a/lib/widget/location_display.dart b/lib/widget/location_display.dart
new file mode 100644
index 00000000..86d8a59c
--- /dev/null
+++ b/lib/widget/location_display.dart
@@ -0,0 +1,197 @@
+import 'package:InvenTree/api.dart';
+import 'package:InvenTree/inventree/stock.dart';
+import 'package:InvenTree/widget/drawer.dart';
+import 'package:InvenTree/widget/stock_display.dart';
+import 'package:flutter/cupertino.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter/foundation.dart';
+
+class LocationDisplayWidget extends StatefulWidget {
+
+  LocationDisplayWidget(this.location, {Key key}) : super(key: key);
+
+  final InvenTreeStockLocation location;
+
+  final String title = "Location";
+
+  @override
+  _LocationDisplayState createState() => _LocationDisplayState(location);
+}
+
+
+class _LocationDisplayState extends State {
+
+  _LocationDisplayState(this.location) {
+    _requestData();
+  }
+
+  final InvenTreeStockLocation location;
+
+  List _sublocations = List();
+
+  String _locationFilter = '';
+
+  List get sublocations {
+    
+    if (_locationFilter.isEmpty || _sublocations.isEmpty) {
+      return _sublocations;
+    } else {
+      return _sublocations.where((loc) => loc.filter(_locationFilter)).toList();
+    }
+  }
+
+  List _items = List();
+
+  String get _title {
+
+    if (location == null) {
+      return "Location:";
+    } else {
+      return "Stock Location '${location.name}'";
+    }
+  }
+
+  void _requestData() {
+
+    int pk = location?.pk ?? -1;
+
+    // Request a list of sub-locations under this one
+    InvenTreeStockLocation().list(filters: {"parent": "$pk"}).then((var locs) {
+      _sublocations.clear();
+
+      for (var loc in locs) {
+        if (loc is InvenTreeStockLocation) {
+          _sublocations.add(loc);
+        }
+      }
+
+      setState(() {});
+
+    // Request a list of stock-items under this one
+    InvenTreeStockItem().list(filters: {"location": "$pk"}).then((var items) {
+      _items.clear();
+
+      for (var item in items) {
+        if (item is InvenTreeStockItem) {
+          _items.add(item);
+        }
+      }
+
+      setState(() {});
+    });
+
+    });
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return Scaffold(
+      appBar: AppBar(
+        title: Text(_title),
+      ),
+      drawer: new InvenTreeDrawer(context),
+      body: Center(
+        child: Column(
+          mainAxisAlignment: MainAxisAlignment.center,
+          children: [
+            Text(
+              "Sublocations - ${_sublocations.length}",
+              textAlign: TextAlign.left,
+              style: TextStyle(fontWeight: FontWeight.bold),
+            ),
+            TextField(
+              decoration: InputDecoration(
+                hintText: "Filter locations",
+              ),
+              onChanged: (text) {
+                setState(() {
+                  _locationFilter = text.trim().toLowerCase();
+                });
+              },
+            ),
+            Expanded(child: SublocationList(sublocations)),
+            Divider(),
+            Text(
+              "Stock Items - ${_items.length}",
+              textAlign: TextAlign.left,
+              style: TextStyle(fontWeight: FontWeight.bold),
+            ),
+            Expanded(child: StockList(_items)),
+          ],
+        )
+      ),
+    );
+  }
+}
+
+
+class SublocationList extends StatelessWidget {
+  final List _locations;
+
+  SublocationList(this._locations);
+
+  void _openLocation(BuildContext context, int pk) {
+
+    InvenTreeStockLocation().get(pk).then((var loc) {
+      if (loc is InvenTreeStockLocation) {
+
+        Navigator.push(context, MaterialPageRoute(builder: (context) => LocationDisplayWidget(loc)));
+      }
+    });
+  }
+
+  Widget _build(BuildContext context, int index) {
+    InvenTreeStockLocation loc = _locations[index];
+
+    return ListTile(
+      title: Text('${loc.name}'),
+      subtitle: Text("${loc.description}"),
+      onTap: () {
+        _openLocation(context, loc.pk);
+      },
+    );
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return ListView.builder(itemBuilder: _build, itemCount: _locations.length);
+  }
+}
+
+class StockList extends StatelessWidget {
+  final List _items;
+
+  StockList(this._items);
+
+  void _openItem(BuildContext context, int pk) {
+    InvenTreeStockItem().get(pk).then((var item) {
+      if (item is InvenTreeStockItem) {
+        Navigator.push(context, MaterialPageRoute(builder: (context) => StockItemDisplayWidget(item)));
+      }
+    });
+  }
+
+  Widget _build(BuildContext context, int index) {
+    InvenTreeStockItem item = _items[index];
+
+    return ListTile(
+      title: Text("${item.partName}"),
+      subtitle: Text("${item.partDescription}"),
+      leading: Image(
+        image: InvenTreeAPI().getImage(item.partThumbnail),
+        width: 48,
+      ),
+      trailing: Text("${item.displayQuantity}",
+        style: TextStyle(fontWeight: FontWeight.bold),
+      ),
+      onTap: () {
+        _openItem(context, item.pk);
+      },
+    );
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return ListView.builder(itemBuilder: _build, itemCount: _items.length);
+  }
+}
\ No newline at end of file
diff --git a/lib/widget/part_display.dart b/lib/widget/part_display.dart
new file mode 100644
index 00000000..725caf17
--- /dev/null
+++ b/lib/widget/part_display.dart
@@ -0,0 +1,54 @@
+
+import 'package:InvenTree/inventree/part.dart';
+import 'package:flutter/cupertino.dart';
+import 'package:flutter/foundation.dart';
+import 'package:flutter/material.dart';
+
+import 'package:InvenTree/widget/drawer.dart';
+
+class PartDisplayWidget extends StatefulWidget {
+
+  PartDisplayWidget(this.part, {Key key}) : super(key: key);
+
+  final InvenTreePart part;
+
+  @override
+  _PartDisplayState createState() => _PartDisplayState(part);
+
+}
+
+
+class _PartDisplayState extends State {
+
+  _PartDisplayState(this.part) {
+    // TODO
+  }
+
+  final InvenTreePart part;
+
+  String get _title {
+    if (part == null) {
+      return "Part";
+    } else {
+      return "Part '${part.name}'";
+    }
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return Scaffold(
+      appBar: AppBar(
+        title: Text(_title),
+      ),
+      drawer: new InvenTreeDrawer(context),
+      body: Center(
+        child: Column(
+          mainAxisAlignment: MainAxisAlignment.center,
+          children: [
+            Text("Description: ${part.description}"),
+          ]
+        ),
+      )
+    );
+  }
+}
\ No newline at end of file
diff --git a/lib/widget/stock_display.dart b/lib/widget/stock_display.dart
new file mode 100644
index 00000000..45c9e041
--- /dev/null
+++ b/lib/widget/stock_display.dart
@@ -0,0 +1,53 @@
+
+
+import 'package:InvenTree/inventree/stock.dart';
+import 'package:flutter/cupertino.dart';
+import 'package:flutter/material.dart';
+
+import 'package:InvenTree/widget/drawer.dart';
+
+class StockItemDisplayWidget extends StatefulWidget {
+
+  StockItemDisplayWidget(this.item, {Key key}) : super(key: key);
+
+  final InvenTreeStockItem item;
+
+  @override
+  _StockItemDisplayState createState() => _StockItemDisplayState(item);
+}
+
+
+class _StockItemDisplayState extends State {
+
+  _StockItemDisplayState(this.item) {
+    // TODO
+  }
+
+  final InvenTreeStockItem item;
+
+  String get _title {
+    if (item == null) {
+      return "Stock Item";
+    } else {
+      return "Item: x ${item.partName}";
+    }
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return Scaffold(
+      appBar: AppBar(
+        title: Text(_title),
+      ),
+      drawer: new InvenTreeDrawer(context),
+      body: Center(
+        child: Column(
+          mainAxisAlignment: MainAxisAlignment.center,
+          children: [
+            Text("Stock Item: hello"),
+          ],
+        )
+      )
+    );
+  }
+}
\ No newline at end of file
diff --git a/pubspec.lock b/pubspec.lock
new file mode 100644
index 00000000..c8d23e33
--- /dev/null
+++ b/pubspec.lock
@@ -0,0 +1,341 @@
+# Generated by pub
+# See https://dart.dev/tools/pub/glossary#lockfile
+packages:
+  archive:
+    dependency: transitive
+    description:
+      name: archive
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "2.0.11"
+  args:
+    dependency: transitive
+    description:
+      name: args
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "1.5.2"
+  async:
+    dependency: transitive
+    description:
+      name: async
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "2.4.0"
+  boolean_selector:
+    dependency: transitive
+    description:
+      name: boolean_selector
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "1.0.5"
+  charcode:
+    dependency: transitive
+    description:
+      name: charcode
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "1.1.2"
+  collection:
+    dependency: transitive
+    description:
+      name: collection
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "1.14.11"
+  convert:
+    dependency: transitive
+    description:
+      name: convert
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "2.1.1"
+  crypto:
+    dependency: transitive
+    description:
+      name: crypto
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "2.1.3"
+  cupertino_icons:
+    dependency: "direct main"
+    description:
+      name: cupertino_icons
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "0.1.3"
+  flutter:
+    dependency: "direct main"
+    description: flutter
+    source: sdk
+    version: "0.0.0"
+  flutter_advanced_networkimage:
+    dependency: "direct main"
+    description:
+      name: flutter_advanced_networkimage
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "0.7.0"
+  flutter_launcher_icons:
+    dependency: "direct dev"
+    description:
+      name: flutter_launcher_icons
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "0.7.4"
+  flutter_svg:
+    dependency: transitive
+    description:
+      name: flutter_svg
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "0.17.3+1"
+  flutter_test:
+    dependency: "direct dev"
+    description: flutter
+    source: sdk
+    version: "0.0.0"
+  flutter_web_plugins:
+    dependency: transitive
+    description: flutter
+    source: sdk
+    version: "0.0.0"
+  http:
+    dependency: "direct main"
+    description:
+      name: http
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "0.12.0+4"
+  http_parser:
+    dependency: transitive
+    description:
+      name: http_parser
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "3.1.4"
+  image:
+    dependency: transitive
+    description:
+      name: image
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "2.1.4"
+  matcher:
+    dependency: transitive
+    description:
+      name: matcher
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "0.12.6"
+  meta:
+    dependency: transitive
+    description:
+      name: meta
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "1.1.8"
+  package_info:
+    dependency: "direct main"
+    description:
+      name: package_info
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "0.4.0+16"
+  path:
+    dependency: transitive
+    description:
+      name: path
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "1.6.4"
+  path_drawing:
+    dependency: transitive
+    description:
+      name: path_drawing
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "0.4.1"
+  path_parsing:
+    dependency: transitive
+    description:
+      name: path_parsing
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "0.1.4"
+  path_provider:
+    dependency: transitive
+    description:
+      name: path_provider
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "1.6.5"
+  path_provider_macos:
+    dependency: transitive
+    description:
+      name: path_provider_macos
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "0.0.4"
+  path_provider_platform_interface:
+    dependency: transitive
+    description:
+      name: path_provider_platform_interface
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "1.0.1"
+  pedantic:
+    dependency: transitive
+    description:
+      name: pedantic
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "1.8.0+1"
+  petitparser:
+    dependency: transitive
+    description:
+      name: petitparser
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "2.4.0"
+  platform:
+    dependency: transitive
+    description:
+      name: platform
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "2.2.1"
+  plugin_platform_interface:
+    dependency: transitive
+    description:
+      name: plugin_platform_interface
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "1.0.2"
+  preferences:
+    dependency: "direct main"
+    description:
+      name: preferences
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "5.1.0"
+  qr_utils:
+    dependency: "direct main"
+    description:
+      name: qr_utils
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "0.1.4"
+  quiver:
+    dependency: transitive
+    description:
+      name: quiver
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "2.0.5"
+  shared_preferences:
+    dependency: "direct main"
+    description:
+      name: shared_preferences
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "0.5.6+3"
+  shared_preferences_macos:
+    dependency: transitive
+    description:
+      name: shared_preferences_macos
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "0.0.1+6"
+  shared_preferences_platform_interface:
+    dependency: transitive
+    description:
+      name: shared_preferences_platform_interface
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "1.0.3"
+  shared_preferences_web:
+    dependency: transitive
+    description:
+      name: shared_preferences_web
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "0.1.2+4"
+  sky_engine:
+    dependency: transitive
+    description: flutter
+    source: sdk
+    version: "0.0.99"
+  source_span:
+    dependency: transitive
+    description:
+      name: source_span
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "1.5.5"
+  stack_trace:
+    dependency: transitive
+    description:
+      name: stack_trace
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "1.9.3"
+  stream_channel:
+    dependency: transitive
+    description:
+      name: stream_channel
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "2.0.0"
+  string_scanner:
+    dependency: transitive
+    description:
+      name: string_scanner
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "1.0.5"
+  term_glyph:
+    dependency: transitive
+    description:
+      name: term_glyph
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "1.1.0"
+  test_api:
+    dependency: transitive
+    description:
+      name: test_api
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "0.2.11"
+  typed_data:
+    dependency: transitive
+    description:
+      name: typed_data
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "1.1.6"
+  vector_math:
+    dependency: transitive
+    description:
+      name: vector_math
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "2.0.8"
+  xml:
+    dependency: transitive
+    description:
+      name: xml
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "3.5.0"
+  yaml:
+    dependency: transitive
+    description:
+      name: yaml
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "2.2.0"
+sdks:
+  dart: ">=2.4.0 <3.0.0"
+  flutter: ">=1.12.13+hotfix.4 <2.0.0"
diff --git a/pubspec.yaml b/pubspec.yaml
new file mode 100644
index 00000000..29eebb06
--- /dev/null
+++ b/pubspec.yaml
@@ -0,0 +1,90 @@
+name: InvenTree
+description: InvenTree stock management
+
+# The following defines the version and build number for your application.
+# A version number is three numbers separated by dots, like 1.2.43
+# followed by an optional build number separated by a +.
+# Both the version and the builder number may be overridden in flutter
+# build by specifying --build-name and --build-number, respectively.
+# In Android, build-name is used as versionName while build-number used as versionCode.
+# Read more about Android versioning at https://developer.android.com/studio/publish/versioning
+# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
+# Read more about iOS versioning at
+# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
+version: 1.0.0+1
+
+environment:
+  sdk: ">=2.1.0 <3.0.0"
+
+dependencies:
+  flutter:
+    sdk: flutter
+
+  # The following adds the Cupertino Icons font to your application.
+  # Use with the CupertinoIcons class for iOS style icons.
+  cupertino_icons: ^0.1.2
+  http: ^0.12.0+2
+  shared_preferences: ^0.5.3+1
+
+  flutter_advanced_networkimage: any
+
+  preferences: ^5.1.0
+
+  qr_utils: ^0.1.4
+
+  package_info: ^0.4.0+16
+
+dev_dependencies:
+  flutter_test:
+    sdk: flutter
+  flutter_launcher_icons:
+
+flutter_icons:
+  android: true
+  ios: true
+  image_path: "assets/image/icon.png"
+
+# For information on the generic Dart part of this file, see the
+# following page: https://www.dartlang.org/tools/pub/pubspec
+
+# The following section is specific to Flutter.
+flutter:
+
+  # The following line ensures that the Material Icons font is
+  # included with your application, so that you can use the icons in
+  # the material Icons class.
+  uses-material-design: true
+
+  assets:
+    - assets/image/icon.png
+
+  # To add assets to your application, add an assets section, like this:
+  # assets:
+  #  - images/a_dot_burr.jpeg
+  #  - images/a_dot_ham.jpeg
+
+  # An image asset can refer to one or more resolution-specific "variants", see
+  # https://flutter.dev/assets-and-images/#resolution-aware.
+
+  # For details regarding adding assets from package dependencies, see
+  # https://flutter.dev/assets-and-images/#from-packages
+
+  # To add custom fonts to your application, add a fonts section here,
+  # in this "flutter" section. Each entry in this list should have a
+  # "family" key with the font family name, and a "fonts" key with a
+  # list giving the asset and other descriptors for the font. For
+  # example:
+  # fonts:
+  #   - family: Schyler
+  #     fonts:
+  #       - asset: fonts/Schyler-Regular.ttf
+  #       - asset: fonts/Schyler-Italic.ttf
+  #         style: italic
+  #   - family: Trajan Pro
+  #     fonts:
+  #       - asset: fonts/TrajanPro.ttf
+  #       - asset: fonts/TrajanPro_Bold.ttf
+  #         weight: 700
+  #
+  # For details regarding fonts from package dependencies,
+  # see https://flutter.dev/custom-fonts/#from-packages
diff --git a/res/values/strings_en.arb b/res/values/strings_en.arb
new file mode 100644
index 00000000..e69de29b
diff --git a/test/widget_test.dart b/test/widget_test.dart
new file mode 100644
index 00000000..0459f35f
--- /dev/null
+++ b/test/widget_test.dart
@@ -0,0 +1,30 @@
+// This is a basic Flutter widget test.
+//
+// To perform an interaction with a widget in your test, use the WidgetTester
+// utility that Flutter provides. 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.
+
+import 'package:flutter/material.dart';
+import 'package:flutter_test/flutter_test.dart';
+
+import 'package:inventree_app/main.dart';
+
+void main() {
+  testWidgets('Counter increments smoke test', (WidgetTester tester) async {
+    // Build our app and trigger a frame.
+    await tester.pumpWidget(MyApp());
+
+    // Verify that our counter starts at 0.
+    expect(find.text('0'), findsOneWidget);
+    expect(find.text('1'), findsNothing);
+
+    // Tap the '+' icon and trigger a frame.
+    await tester.tap(find.byIcon(Icons.add));
+    await tester.pump();
+
+    // Verify that our counter has incremented.
+    expect(find.text('0'), findsNothing);
+    expect(find.text('1'), findsOneWidget);
+  });
+}