diff --git a/.gitignore b/.gitignore index 228c2c7..54a10d1 100644 --- a/.gitignore +++ b/.gitignore @@ -234,4 +234,6 @@ fabric.properties !**/ios/**/default.perspectivev3 !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages -# End of https://www.gitignore.io/api/android,flutter,androidstudio \ No newline at end of file +# End of https://www.gitignore.io/api/android,flutter,androidstudio +lib/common/felixApiCreds.dart +android/app/src/main/AndroidManifest.xml diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml index 30aa626..681f41a 100644 --- a/.idea/codeStyles/Project.xml +++ b/.idea/codeStyles/Project.xml @@ -1,29 +1,116 @@ - - - - - - - - - - + + + + + +
+ + + + xmlns:android + + ^$ + + + +
+
+ + + + xmlns:.* + + ^$ + + + BY_NAME + +
+
+ + + + .*:id + + http://schemas.android.com/apk/res/android + + + +
+
+ + + + .*:name + + http://schemas.android.com/apk/res/android + + + +
+
+ + + + name + + ^$ + + + +
+
+ + + + style + + ^$ + + + +
+
+ + + + .* + + ^$ + + + BY_NAME + +
+
+ + + + .* + + http://schemas.android.com/apk/res/android + + + ANDROID_ATTRIBUTE_ORDER + +
+
+ + + + .* + + .* + + + BY_NAME + +
+
+
+
\ No newline at end of file diff --git a/.idea/runConfigurations/main_dart.xml b/.idea/runConfigurations/main_dart.xml index aab7b5c..934fdfc 100644 --- a/.idea/runConfigurations/main_dart.xml +++ b/.idea/runConfigurations/main_dart.xml @@ -1,6 +1,6 @@ - + \ No newline at end of file diff --git a/README.md b/README.md index 6551d8f..3580c7d 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,21 @@ Follow these steps to get the project up-and-running: 1. If this is your first Flutter project, install the [Flutter SDK](https://flutter.dev/docs/get-started/test-drive) 1. Add the line `flutter.sdk=⟨ path to Flutter SDK ⟩` to the file `androud/local.properties` +## Building + +To build an apk from dev, use: +``` +flutter build apk -t lib/main_dev.dart +``` + +## Debugging + +``` +// debug +import 'package:flutter/foundation.dart'; +debugPrint('$foo'); +``` + ## Further Reading A few resources to get you started if this is your first Flutter project: @@ -23,7 +38,10 @@ For help getting started with Flutter, view our [online documentation](https://flutter.io/docs), which offers tutorials, samples, guidance on mobile development, and a full API reference. +https://github.com/putraxor/flutter-login-ui +https://github.com/pbirdsall/medium_splash_tokenauth + Related repos: - [`Pear-Trading/FoodLoop-Web`](https://github.com/Pear-Trading/FoodLoop-Web) -- [`Pear-Trading/Foodloop-Server`](https://github.com/Pear-Trading/Foodloop-Server) +- [`Pear-Trading/Foodloop-Server`](https://github.com/Pear-Trading/Foodloop-Server) \ No newline at end of file diff --git a/analysis_options.yaml b/analysis_options.yaml new file mode 100644 index 0000000..84426a6 --- /dev/null +++ b/analysis_options.yaml @@ -0,0 +1,18 @@ +include: package:pedantic/analysis_options.yaml + +analyzer: + exclude: + - lib/src/locations.g.dart + +linter: + rules: + - always_declare_return_types + - camel_case_types + - empty_constructor_bodies + - annotate_overrides + - avoid_init_to_null + - constant_identifier_names + - one_member_abstracts + - slash_for_doc_comments + - sort_constructors_first + - unnecessary_brace_in_string_interps \ No newline at end of file diff --git a/android/app/build.gradle b/android/app/build.gradle index c4b5c4a..865c574 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -25,7 +25,7 @@ apply plugin: 'com.android.application' apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" android { - compileSdkVersion 27 + compileSdkVersion 28 lintOptions { disable 'InvalidPackage' diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 7949887..53c13db 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -13,11 +13,18 @@ additional functionality it is fine to subclass or reimplement FlutterApplication and put your custom class here. --> - + + + + + + + + + android:name="io.flutter.embedding.android.SplashScreenDrawable" + android:resource="@drawable/launch_background" /> + + + + diff --git a/android/app/src/main/java/uk/co/localspend/localspend/MainActivity.java b/android/app/src/main/java/uk/co/localspend/localspend/MainActivity.java index 361cc1c..238a3b4 100644 --- a/android/app/src/main/java/uk/co/localspend/localspend/MainActivity.java +++ b/android/app/src/main/java/uk/co/localspend/localspend/MainActivity.java @@ -1,13 +1,6 @@ package uk.co.localspend.localspend; -import android.os.Bundle; -import io.flutter.app.FlutterActivity; -import io.flutter.plugins.GeneratedPluginRegistrant; +import io.flutter.embedding.android.FlutterActivity; 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 index 304732f..331d9ff 100644 --- a/android/app/src/main/res/drawable/launch_background.xml +++ b/android/app/src/main/res/drawable/launch_background.xml @@ -4,9 +4,9 @@ - + --> diff --git a/android/app/src/main/res/drawable/normal_background.xml b/android/app/src/main/res/drawable/normal_background.xml new file mode 100644 index 0000000..331d9ff --- /dev/null +++ b/android/app/src/main/res/drawable/normal_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 index db77bb4..f9bbd0e 100644 Binary files a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-ldpi/ic_launcher.png b/android/app/src/main/res/mipmap-ldpi/ic_launcher.png new file mode 100644 index 0000000..7c4b3d3 Binary files /dev/null and b/android/app/src/main/res/mipmap-ldpi/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 index 17987b7..f9de61e 100644 Binary files a/android/app/src/main/res/mipmap-mdpi/ic_launcher.png 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 index 09d4391..7aead9b 100644 Binary files a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png 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 index d5f1c8d..8331d35 100644 Binary files a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png 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 index 4d6372e..3f53403 100644 Binary files a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap/launch_image.png b/android/app/src/main/res/mipmap/launch_image.png new file mode 100644 index 0000000..e8d5eb3 Binary files /dev/null and b/android/app/src/main/res/mipmap/launch_image.png differ diff --git a/android/app/src/main/res/playstore-icon.png b/android/app/src/main/res/playstore-icon.png new file mode 100644 index 0000000..a0475ae Binary files /dev/null and b/android/app/src/main/res/playstore-icon.png differ diff --git a/android/app/src/main/res/values/styles.xml b/android/app/src/main/res/values/styles.xml index 00fa441..2032af9 100644 --- a/android/app/src/main/res/values/styles.xml +++ b/android/app/src/main/res/values/styles.xml @@ -5,4 +5,8 @@ Flutter draws its first frame --> @drawable/launch_background + + diff --git a/android/build.gradle b/android/build.gradle index 541636c..f44bb13 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -26,4 +26,4 @@ subprojects { task clean(type: Delete) { delete rootProject.buildDir -} +} \ No newline at end of file diff --git a/android/gradle.properties b/android/gradle.properties index 8bd86f6..a673820 100644 --- a/android/gradle.properties +++ b/android/gradle.properties @@ -1 +1,4 @@ org.gradle.jvmargs=-Xmx1536M +android.useAndroidX=true +android.enableJetifier=true +android.enableR8=true diff --git a/assets/Consolas.ttf b/assets/Consolas.ttf new file mode 100644 index 0000000..a79deb5 Binary files /dev/null and b/assets/Consolas.ttf differ diff --git a/assets/images/cat.png b/assets/images/cat.png new file mode 100644 index 0000000..5843924 Binary files /dev/null and b/assets/images/cat.png differ diff --git a/assets/images/launch_image.png b/assets/images/launch_image.png new file mode 100644 index 0000000..0a4042b Binary files /dev/null and b/assets/images/launch_image.png differ diff --git a/assets/images/shadowcat-long.png b/assets/images/shadowcat-long.png new file mode 100644 index 0000000..5fc9df8 Binary files /dev/null and b/assets/images/shadowcat-long.png differ diff --git a/assets/images/text.png b/assets/images/text.png new file mode 100644 index 0000000..f83d20c Binary files /dev/null and b/assets/images/text.png differ diff --git a/ios/Flutter/Debug.xcconfig b/ios/Flutter/Debug.xcconfig index 592ceee..e8efba1 100644 --- a/ios/Flutter/Debug.xcconfig +++ b/ios/Flutter/Debug.xcconfig @@ -1 +1,2 @@ +#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" #include "Generated.xcconfig" diff --git a/ios/Flutter/Release.xcconfig b/ios/Flutter/Release.xcconfig index 592ceee..399e934 100644 --- a/ios/Flutter/Release.xcconfig +++ b/ios/Flutter/Release.xcconfig @@ -1 +1,2 @@ +#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" #include "Generated.xcconfig" diff --git a/ios/Podfile b/ios/Podfile new file mode 100644 index 0000000..671bf46 --- /dev/null +++ b/ios/Podfile @@ -0,0 +1,69 @@ +# Uncomment this line to define a global platform for your project +platform :ios, '9.0' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def parse_KV_file(file, separator='=') + file_abs_path = File.expand_path(file) + if !File.exists? file_abs_path + return []; + end + pods_ary = [] + skip_line_start_symbols = ["#", "/"] + File.foreach(file_abs_path) { |line| + next if skip_line_start_symbols.any? { |symbol| line =~ /^\s*#{symbol}/ } + plugin = line.split(pattern=separator) + if plugin.length == 2 + podname = plugin[0].strip() + path = plugin[1].strip() + podpath = File.expand_path("#{path}", file_abs_path) + pods_ary.push({:name => podname, :path => podpath}); + else + puts "Invalid plugin specification: #{line}" + end + } + return pods_ary +end + +target 'Runner' do + # Prepare symlinks folder. We use symlinks to avoid having Podfile.lock + # referring to absolute paths on developers' machines. + system('rm -rf .symlinks') + system('mkdir -p .symlinks/plugins') + + # Flutter Pods + generated_xcode_build_settings = parse_KV_file('./Flutter/Generated.xcconfig') + if generated_xcode_build_settings.empty? + puts "Generated.xcconfig must exist. If you're running pod install manually, make sure flutter packages get is executed first." + end + generated_xcode_build_settings.map { |p| + if p[:name] == 'FLUTTER_FRAMEWORK_DIR' + symlink = File.join('.symlinks', 'flutter') + File.symlink(File.dirname(p[:path]), symlink) + pod 'Flutter', :path => File.join(symlink, File.basename(p[:path])) + end + } + + # Plugin Pods + plugin_pods = parse_KV_file('../.flutter-plugins') + plugin_pods.map { |p| + symlink = File.join('.symlinks', 'plugins', p[:name]) + File.symlink(p[:path], symlink) + pod p[:name], :path => File.join(symlink, 'ios') + } +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + target.build_configurations.each do |config| + config.build_settings['ENABLE_BITCODE'] = 'NO' + end + end +end diff --git a/ios/Podfile.lock b/ios/Podfile.lock new file mode 100644 index 0000000..e0081c9 --- /dev/null +++ b/ios/Podfile.lock @@ -0,0 +1,45 @@ +PODS: + - Flutter (1.0.0) + - google_maps_flutter (0.0.1): + - Flutter + - GoogleMaps + - GoogleMaps (3.3.0): + - GoogleMaps/Maps (= 3.3.0) + - GoogleMaps/Base (3.3.0) + - GoogleMaps/Maps (3.3.0): + - GoogleMaps/Base + - shared_preferences (0.0.1): + - Flutter + - url_launcher (0.0.1): + - Flutter + +DEPENDENCIES: + - Flutter (from `.symlinks/flutter/ios-profile`) + - google_maps_flutter (from `.symlinks/plugins/google_maps_flutter/ios`) + - shared_preferences (from `.symlinks/plugins/shared_preferences/ios`) + - url_launcher (from `.symlinks/plugins/url_launcher/ios`) + +SPEC REPOS: + https://github.com/cocoapods/specs.git: + - GoogleMaps + +EXTERNAL SOURCES: + Flutter: + :path: ".symlinks/flutter/ios-profile" + google_maps_flutter: + :path: ".symlinks/plugins/google_maps_flutter/ios" + shared_preferences: + :path: ".symlinks/plugins/shared_preferences/ios" + url_launcher: + :path: ".symlinks/plugins/url_launcher/ios" + +SPEC CHECKSUMS: + Flutter: 58dd7d1b27887414a370fcccb9e645c08ffd7a6a + google_maps_flutter: 78a52114c898b42ea647919679a4c58b70abe876 + GoogleMaps: cfee83da305b9aaeccf92c24ac79df11c3003492 + shared_preferences: 1feebfa37bb57264736e16865e7ffae7fc99b523 + url_launcher: 0067ddb8f10d36786672aa0722a21717dba3a298 + +PODFILE CHECKSUM: 3389836f37640698630b8f0670315d626d5f1469 + +COCOAPODS: 1.7.5 diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 408648a..f0bdeff 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -8,10 +8,10 @@ /* Begin PBXBuildFile section */ 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; - 2D5378261FAA1A9400D5DBA9 /* flutter_assets in Resources */ = {isa = PBXBuildFile; fileRef = 2D5378251FAA1A9400D5DBA9 /* flutter_assets */; }; 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, ); }; }; + 9548094CC0738B936AB604F6 /* libPods-Runner.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 9DB0D87E6339E9CE2E335662 /* libPods-Runner.a */; }; 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 */; }; @@ -38,11 +38,12 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 0C47C2DEBD545D4054D9940A /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; 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 = ""; }; - 2D5378251FAA1A9400D5DBA9 /* flutter_assets */ = {isa = PBXFileReference; lastKnownFileType = folder; name = flutter_assets; path = Flutter/flutter_assets; sourceTree = SOURCE_ROOT; }; 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 = ""; }; + 510C088F759CD31C97486879 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; 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 = ""; }; @@ -55,6 +56,8 @@ 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 = ""; }; + 9DB0D87E6339E9CE2E335662 /* libPods-Runner.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Runner.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + C40D35B4079C018C62D759E0 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -64,16 +67,34 @@ files = ( 9705A1C61CF904A100538489 /* Flutter.framework in Frameworks */, 3B80C3941E831B6300D905FE /* App.framework in Frameworks */, + 9548094CC0738B936AB604F6 /* libPods-Runner.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 24EB461E21BAD0D319745BEA /* Frameworks */ = { + isa = PBXGroup; + children = ( + 9DB0D87E6339E9CE2E335662 /* libPods-Runner.a */, + ); + name = Frameworks; + sourceTree = ""; + }; + 55C1C6FB63DDD235539F7C5D /* Pods */ = { + isa = PBXGroup; + children = ( + 510C088F759CD31C97486879 /* Pods-Runner.debug.xcconfig */, + 0C47C2DEBD545D4054D9940A /* Pods-Runner.release.xcconfig */, + C40D35B4079C018C62D759E0 /* Pods-Runner.profile.xcconfig */, + ); + path = Pods; + sourceTree = ""; + }; 9740EEB11CF90186004384FC /* Flutter */ = { isa = PBXGroup; children = ( - 2D5378251FAA1A9400D5DBA9 /* flutter_assets */, 3B80C3931E831B6300D905FE /* App.framework */, 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, 9740EEBA1CF902C7004384FC /* Flutter.framework */, @@ -90,7 +111,8 @@ 9740EEB11CF90186004384FC /* Flutter */, 97C146F01CF9000F007C117D /* Runner */, 97C146EF1CF9000F007C117D /* Products */, - CF3B75C9A7D2FA2A4C99F110 /* Frameworks */, + 55C1C6FB63DDD235539F7C5D /* Pods */, + 24EB461E21BAD0D319745BEA /* Frameworks */, ); sourceTree = ""; }; @@ -133,12 +155,15 @@ isa = PBXNativeTarget; buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( + 8850D50C44B3BB3289D14523 /* [CP] Check Pods Manifest.lock */, 9740EEB61CF901F6004384FC /* Run Script */, 97C146EA1CF9000F007C117D /* Sources */, 97C146EB1CF9000F007C117D /* Frameworks */, 97C146EC1CF9000F007C117D /* Resources */, 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + ACE3DC71CDB8FA5537935604 /* [CP] Embed Pods Frameworks */, + E6C2807EE928990FB790046F /* [CP] Copy Pods Resources */, ); buildRules = ( ); @@ -160,6 +185,8 @@ TargetAttributes = { 97C146ED1CF9000F007C117D = { CreatedOnToolsVersion = 7.3.1; + DevelopmentTeam = G78D5X4L92; + ProvisioningStyle = Automatic; }; }; }; @@ -168,6 +195,7 @@ developmentRegion = English; hasScannedForEncodings = 0; knownRegions = ( + English, en, Base, ); @@ -190,7 +218,6 @@ 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, 9740EEB41CF90195004384FC /* Debug.xcconfig in Resources */, 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, - 2D5378261FAA1A9400D5DBA9 /* flutter_assets in Resources */, 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -212,6 +239,28 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" thin"; }; + 8850D50C44B3BB3289D14523 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; 9740EEB61CF901F6004384FC /* Run Script */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; @@ -226,6 +275,42 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; }; + ACE3DC71CDB8FA5537935604 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh", + "${PODS_ROOT}/../.symlinks/flutter/ios-profile/Flutter.framework", + ); + name = "[CP] Embed Pods Frameworks"; + outputPaths = ( + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Flutter.framework", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + E6C2807EE928990FB790046F /* [CP] Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh", + "${PODS_ROOT}/GoogleMaps/Maps/Frameworks/GoogleMaps.framework/Resources/GoogleMaps.bundle", + ); + name = "[CP] Copy Pods Resources"; + outputPaths = ( + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/GoogleMaps.bundle", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ @@ -314,14 +399,17 @@ baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_IDENTITY = "iPhone Developer"; + CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - DEVELOPMENT_TEAM = S8QB4VV633; + DEVELOPMENT_TEAM = G78D5X4L92; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", "$(PROJECT_DIR)/Flutter", ); INFOPLIST_FILE = Runner/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; LIBRARY_SEARCH_PATHS = ( "$(inherited)", @@ -329,6 +417,7 @@ ); PRODUCT_BUNDLE_IDENTIFIER = uk.co.localspend.localSpend; PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; VERSIONING_SYSTEM = "apple-generic"; }; name = Profile; @@ -440,13 +529,17 @@ baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_IDENTITY = "iPhone Developer"; + CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = G78D5X4L92; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", "$(PROJECT_DIR)/Flutter", ); INFOPLIST_FILE = Runner/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; LIBRARY_SEARCH_PATHS = ( "$(inherited)", @@ -454,6 +547,7 @@ ); PRODUCT_BUNDLE_IDENTIFIER = uk.co.localspend.localSpend; PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; VERSIONING_SYSTEM = "apple-generic"; }; name = Debug; @@ -463,13 +557,17 @@ baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_IDENTITY = "iPhone Developer"; + CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = G78D5X4L92; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", "$(PROJECT_DIR)/Flutter", ); INFOPLIST_FILE = Runner/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; LIBRARY_SEARCH_PATHS = ( "$(inherited)", @@ -477,6 +575,7 @@ ); PRODUCT_BUNDLE_IDENTIFIER = uk.co.localspend.localSpend; PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; VERSIONING_SYSTEM = "apple-generic"; }; name = Release; diff --git a/ios/Runner.xcworkspace/contents.xcworkspacedata b/ios/Runner.xcworkspace/contents.xcworkspacedata index 1d526a1..21a3cc1 100644 --- a/ios/Runner.xcworkspace/contents.xcworkspacedata +++ b/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -4,4 +4,7 @@ + + diff --git a/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..949b678 --- /dev/null +++ b/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + BuildSystemType + Original + + diff --git a/ios/Runner/AppDelegate.m b/ios/Runner/AppDelegate.m index 59a72e9..85625ee 100644 --- a/ios/Runner/AppDelegate.m +++ b/ios/Runner/AppDelegate.m @@ -1,10 +1,12 @@ #include "AppDelegate.h" #include "GeneratedPluginRegistrant.h" +#import "GoogleMaps/GoogleMaps.h" @implementation AppDelegate - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { + [GMSServices provideAPIKey:@"AIzaSyBkVgDYRQoKjCWlGMyl3V6ROzmLEsa5a0w"]; [GeneratedPluginRegistrant registerWithRegistry:self]; // Override point for customization after application launch. return [super application:application didFinishLaunchingWithOptions:launchOptions]; diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json old mode 100644 new mode 100755 index d36b1fa..99f7a0c --- a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json +++ b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -1,122 +1,128 @@ { - "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" + "images":[ + { + "idiom":"iphone", + "size":"20x20", + "scale":"2x", + "filename":"Icon-App-20x20@2x.png" + }, + { + "idiom":"iphone", + "size":"20x20", + "scale":"3x", + "filename":"Icon-App-20x20@3x.png" + }, + { + "idiom":"iphone", + "size":"29x29", + "scale":"1x", + "filename":"Icon-App-29x29@1x.png" + }, + { + "idiom":"iphone", + "size":"29x29", + "scale":"2x", + "filename":"Icon-App-29x29@2x.png" + }, + { + "idiom":"iphone", + "size":"29x29", + "scale":"3x", + "filename":"Icon-App-29x29@3x.png" + }, + { + "idiom":"iphone", + "size":"40x40", + "scale":"2x", + "filename":"Icon-App-40x40@2x.png" + }, + { + "idiom":"iphone", + "size":"40x40", + "scale":"3x", + "filename":"Icon-App-40x40@3x.png" + }, + { + "idiom":"iphone", + "size":"60x60", + "scale":"2x", + "filename":"Icon-App-60x60@2x.png" + }, + { + "idiom":"iphone", + "size":"60x60", + "scale":"3x", + "filename":"Icon-App-60x60@3x.png" + }, + { + "idiom":"iphone", + "size":"76x76", + "scale":"2x", + "filename":"Icon-App-76x76@2x.png" + }, + { + "idiom":"ipad", + "size":"20x20", + "scale":"1x", + "filename":"Icon-App-20x20@1x.png" + }, + { + "idiom":"ipad", + "size":"20x20", + "scale":"2x", + "filename":"Icon-App-20x20@2x.png" + }, + { + "idiom":"ipad", + "size":"29x29", + "scale":"1x", + "filename":"Icon-App-29x29@1x.png" + }, + { + "idiom":"ipad", + "size":"29x29", + "scale":"2x", + "filename":"Icon-App-29x29@2x.png" + }, + { + "idiom":"ipad", + "size":"40x40", + "scale":"1x", + "filename":"Icon-App-40x40@1x.png" + }, + { + "idiom":"ipad", + "size":"40x40", + "scale":"2x", + "filename":"Icon-App-40x40@2x.png" + }, + { + "idiom":"ipad", + "size":"76x76", + "scale":"1x", + "filename":"Icon-App-76x76@1x.png" + }, + { + "idiom":"ipad", + "size":"76x76", + "scale":"2x", + "filename":"Icon-App-76x76@2x.png" + }, + { + "idiom":"ipad", + "size":"83.5x83.5", + "scale":"2x", + "filename":"Icon-App-83.5x83.5@2x.png" + }, + { + "size" : "1024x1024", + "idiom" : "ios-marketing", + "scale" : "1x", + "filename" : "ItunesArtwork@2x.png" + } + ], + "info":{ + "version":1, + "author":"makeappicon" } - ], - "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 deleted file mode 100644 index 3d43d11..0000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png and /dev/null 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 index 28c6bf0..5ee49da 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png 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 index 2ccbfd9..6f8b2f7 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png 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 index f091b6b..03d722e 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png 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 index 4cde121..8ae29cd 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png 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 index d0ef06e..92a930f 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png 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 index dcdc230..04d612f 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png 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 index 2ccbfd9..6f8b2f7 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png 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 index c8f9ed8..2737ff0 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png 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 index a6d6b86..2c3aa9f 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png 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 index a6d6b86..2c3aa9f 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png 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 index 75b2d16..ecda742 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png 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 index c4df70d..318214d 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png 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 index 6a84f41..a36ac91 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png 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 index d0e1f58..4348e41 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/ItunesArtwork@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/ItunesArtwork@2x.png new file mode 100644 index 0000000..93c3539 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/ItunesArtwork@2x.png differ diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index 72abeec..6bf45ff 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -2,8 +2,12 @@ + io.flutter.embedded_views_preview + CFBundleDevelopmentRegion en + CFBundleDisplayName + Local Spend Tracker CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier @@ -26,6 +30,10 @@ LaunchScreen UIMainStoryboardFile Main + UIStatusBarHidden + + UIStatusBarStyle + UIStatusBarStyleDarkContent UISupportedInterfaceOrientations UIInterfaceOrientationPortrait diff --git a/lib/common/apifunctions/categories.dart b/lib/common/apifunctions/categories.dart new file mode 100644 index 0000000..160ed7e --- /dev/null +++ b/lib/common/apifunctions/categories.dart @@ -0,0 +1,68 @@ +import 'dart:convert'; +import 'dart:async'; +import 'package:http/http.dart' as http; +import 'package:local_spend/common/functions/get_token.dart'; + +Future> getCategories() async { + const url = "https://dev.localspend.co.uk/api/search/category"; + var token; + + await getToken().then((result) { + token = result; + }); + + Map body = { + "session_key": token, + }; + + final response = await http.post( + url, + body: json.encode(body), + ); + +// print(response.body); + + if (response.statusCode == 200) { + //request successful + List categories = new List(); + Map responseMap = json.decode(response.body); + + var categoriesJSON = responseMap['categories']; + + //nice. + // so the response needs to be decoded iteratively, like + // categories.add(new Category(name: categoriesJSON[i.toString()], index: i.toString())); + +// print(categoriesJSON['11']); // prints "Banana" + + int i = 1; // starts on 1. that was annoying to debug! + while (true) { + if (categoriesJSON[i.toString()] != null) { +// print("Iteration " + i.toString()); +// print(categoriesJSON[i.toString()]); + categories.add(categoriesJSON[i.toString()]); +// print(categories.last); + i++; + } else { +// print("Number of categories: " + (i - 1).toString()); +// print("categoriesJSON[" + i.toString() + ".toString()] == null"); + break; + } + } // does this until error, which then tells it that there is no more JSON left + +// print(categories[11].name); + +// decodedResponse.forEach((key, value) { +//// print(key + ": " + value); +// categories.add(new Category(name: value, index: key)); +// }); + +// print(categories[10].name.toString()); // prints "Banana" + + return categories; // categories is List + // Category is defined at the top^^ + } else { + // not successful + return null; + } +} diff --git a/lib/common/apifunctions/find_organisations.dart b/lib/common/apifunctions/find_organisations.dart new file mode 100644 index 0000000..1b2ad84 --- /dev/null +++ b/lib/common/apifunctions/find_organisations.dart @@ -0,0 +1,85 @@ +import 'dart:convert'; +import 'dart:async'; +import 'package:http/http.dart' as http; +import 'package:local_spend/common/functions/get_token.dart'; + +class Organisation { + Organisation( + this.id, + this.name, + this.postcode, + this.streetName, + this.town, + ); + + var id = 0; + var name = ""; + var postcode = ""; + var streetName = ""; //street_name + var town = ""; +} + +class Organisations { + List getTestData() { + var numItems = 10; + var itemsList = new List(); + + for (int i = 0; i < numItems; i++) { + itemsList.add(new Organisation(i, "Payee " + (i + 1).toString(), + "tee hee hee", "yeet street", "Robloxia")); + } + + return itemsList; + } + + List _jsonToOrganisations(String json) { + Map decoded = jsonDecode(json); + List validated = decoded['validated']; + List organisationsMaps = new List(); + validated.forEach((element) => organisationsMaps.add(element)); + List organisations = new List(); + + for (var i = 0; i < organisationsMaps.length; i++) { + final params = organisationsMaps[i].values.toList(); + + var newOrganisation = new Organisation( + params[0].toInt(), + params[1].toString(), + params[2].toString(), // oof + params[3].toString(), // this could be improved... + params[4].toString(), + ); + + organisations.add(newOrganisation); + } + + return organisations; + } + + Future> findOrganisations(String search) async { + final url = "https://dev.localspend.co.uk/api/search"; + var token; + + await getToken().then((result) { + token = result; + }); + + Map body = { + "search_name": search, + "session_key": token, + }; + + final response = await http.post( + url, + body: json.encode(body), + ); + + if (response.statusCode == 200) { + //request successful + return _jsonToOrganisations(response.body); + } else { + // not successful + return null; + } + } +} diff --git a/lib/common/apifunctions/get_graph_data.dart b/lib/common/apifunctions/get_graph_data.dart new file mode 100644 index 0000000..f1fbedd --- /dev/null +++ b/lib/common/apifunctions/get_graph_data.dart @@ -0,0 +1,270 @@ +import 'dart:convert'; +import 'dart:async'; +import 'package:http/http.dart' as http; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:charts_flutter/flutter.dart' as charts; + +/// Customer graph types: https://dev.localspend.co.uk/api/v1/customer/graphs +/// - total_last_week +/// - avg_spend_last_week +/// - total_last_month +/// - avg_spend_last_month + +/// Organisations' graphs types: to fetch, POST to https://dev.localspend.co.uk/api/stats/[name] as {"session_key":"[boop beep]"} +/// - organisations_all : organisation +/// - pies : organisation/pies +/// - snippets : organisation/snippets +/// - graphs : organisation/graphs +/// - {"graph":"customers_last_7_days","session_key":"[bleep]"} +/// - {"graph":"customers_last_30_days","session_key":"[blah]"} +/// - {"graph":"sales_last_7_days","session_key":"[bloop]"} +/// - {"graph":"sales_last_7_days","session_key":"[reee]"} +/// - {"graph":"purchases_last_7_days","session_key":"[yee]"} +/// - {"graph":"purchases_last_30_days","session_key":"[yah]"} +/// - {"graph":"purchases_all;","session_key":"[kappa]"} // I don't think this one works +/// +/// HTTP POST request sample: +/// {"graph":"total_last_week","session_key":"blahblahblah"} + +class OrganisationGraph { + OrganisationGraph(this.chartType, {this.graphsType = ""}); + + String graphsType = + ""; // type of graph, eg customers_last_7_days, sales_last_30_days, purchases_last_30_days etc + String + chartType; // type of chart, eg organisations_all, pies, snippets or graphs + + List> graph; + + List cachedData; + + bool loaded = false; + + Future getGraphData() async { + if (loaded) { + this.graph = [ + new charts.Series( + id: 'Chart', + colorFn: (_, __) => charts.MaterialPalette.blue.shadeDefault, + domainFn: (TimeSeriesCustomersOrSales spend, _) => spend.time, + measureFn: (TimeSeriesCustomersOrSales spend, _) => + spend.numberOfStuff, + data: cachedData, + ) + ]; + return null; + } + + String url = "https://dev.localspend.co.uk/api/v1/organisation/"; + + if (!(this.chartType == "organisations_all")) { + url += this.chartType; + } + + SharedPreferences preferences = await SharedPreferences.getInstance(); + Map body; + + if (this.chartType == "graphs") { + body = { + 'graph': this.graphsType, + 'session_key': preferences.get('LastToken'), + }; + } else { + body = { + 'session_key': preferences.get('LastToken'), + }; + } + + print(url); + print(json.encode(body).toString()); + + final response = await http.post( + url, + body: json.encode(body), + ); + + try { + if (response.statusCode == 200) { + final responseJson = jsonDecode(response.body); + final List labels = responseJson['graph']['labels']; + final List data = responseJson['graph']['data']; + + List graphDataList = + new List(); + + for (int i = 0; i < labels.length; i++) { + graphDataList.add(new TimeSeriesCustomersOrSales( + data[i] * 1.00, DateTime.parse(labels[i]))); + } + + this.cachedData = graphDataList; + this.loaded = true; + + this.graph = [ + new charts.Series( + id: 'Chart', + colorFn: (_, __) => charts.MaterialPalette.blue.shadeDefault, + domainFn: (TimeSeriesCustomersOrSales spend, _) => spend.time, + measureFn: (TimeSeriesCustomersOrSales spend, _) => + spend.numberOfStuff, + data: graphDataList, + ) + ]; + return this.graph; + } else { + final errorMessage = json.decode(response.body)['message']; + print("Error: " + errorMessage); + this.graph = null; + } + } catch (error) { + print(error.toString()); + } + } +} + +class GraphData { + GraphData( + this.chartType, + ); + + var chartType; + List> graph; + + List cachedData; + bool loaded = false; + + Future>> setGraphData() async { + if (loaded) { + this.graph = [ + new charts.Series( + id: 'Spend', + colorFn: (_, __) => charts.MaterialPalette.blue.shadeDefault, + domainFn: (TimeSeriesSpend spend, _) => spend.time, + measureFn: (TimeSeriesSpend spend, _) => spend.spend, + data: cachedData, + ) + ]; + return null; + } + + final url = "https://dev.localspend.co.uk/api/v1/customer/graphs"; + SharedPreferences preferences = await SharedPreferences.getInstance(); + + Map body = { + 'graph': this.chartType, + 'session_key': preferences.get('LastToken'), + }; + + final response = await http.post( + url, + body: json.encode(body), + ); + + if (response.statusCode == 200) { + final responseJson = jsonDecode(response.body); + final List labels = responseJson['graph']['labels']; + final List data = responseJson['graph']['data']; + + List timeSeriesSpendList = new List(); + + for (int i = 0; i < labels.length; i++) { + timeSeriesSpendList.add( + new TimeSeriesSpend(data[i] * 1.00, DateTime.parse(labels[i]))); +// print(timeSeriesSpendList[i].time.toString() + " : " + timeSeriesSpendList[i].spend.toString()); + } + + cachedData = timeSeriesSpendList; + loaded = true; + + this.graph = [ + new charts.Series( + id: 'Spend', + colorFn: (_, __) => charts.MaterialPalette.blue.shadeDefault, + domainFn: (TimeSeriesSpend spend, _) => spend.time, + measureFn: (TimeSeriesSpend spend, _) => spend.spend, + data: timeSeriesSpendList, + ) + ]; + return this.graph; + } else { + final errorMessage = json.decode(response.body)['message']; + print("Error: " + errorMessage); + + this.graph = null; + return null; + } + } + + Future>> getGraphData() async { + if (loaded == true) { + return [ + new charts.Series( + id: 'Spend', + colorFn: (_, __) => charts.MaterialPalette.blue.shadeDefault, + domainFn: (TimeSeriesSpend spend, _) => spend.time, + measureFn: (TimeSeriesSpend spend, _) => spend.spend, + data: cachedData, + ) + ]; + } + + final url = "https://dev.localspend.co.uk/api/v1/customer/graphs"; + SharedPreferences preferences = await SharedPreferences.getInstance(); + + Map body = { + 'graph': this.chartType, + 'session_key': preferences.get('LastToken'), + }; + + final response = await http.post( + url, + body: json.encode(body), + ); + + if (response.statusCode == 200) { + final responseJson = jsonDecode(response.body); + final List labels = responseJson['graph']['labels']; + final List data = responseJson['graph']['data']; + + List timeSeriesSpendList = new List(); + + for (int i = 0; i < labels.length; i++) { + timeSeriesSpendList.add( + new TimeSeriesSpend(data[i] * 1.00, DateTime.parse(labels[i]))); +// print(timeSeriesSpendList[i].time.toString() + " : " + timeSeriesSpendList[i].spend.toString()); + } + + cachedData = timeSeriesSpendList; + loaded = true; + + return [ + new charts.Series( + id: 'Spend', + colorFn: (_, __) => charts.MaterialPalette.blue.shadeDefault, + domainFn: (TimeSeriesSpend spend, _) => spend.time, + measureFn: (TimeSeriesSpend spend, _) => spend.spend, + data: timeSeriesSpendList, + ) + ]; + } else { + final errorMessage = json.decode(response.body)['message']; + print("Error: " + errorMessage); + + return null; + } + } +} + +class TimeSeriesSpend { + TimeSeriesSpend(this.spend, this.time); + + final DateTime time; + final double spend; +} + +class TimeSeriesCustomersOrSales { + TimeSeriesCustomersOrSales(this.numberOfStuff, this.time); + + final DateTime time; + final double numberOfStuff; +} diff --git a/lib/common/apifunctions/get_map_data.dart b/lib/common/apifunctions/get_map_data.dart new file mode 100644 index 0000000..032e74f --- /dev/null +++ b/lib/common/apifunctions/get_map_data.dart @@ -0,0 +1,87 @@ +import 'dart:convert'; +import 'dart:io'; +import 'package:http/http.dart' as http; +import 'package:json_annotation/json_annotation.dart'; +import 'package:local_spend/common/apifunctions/find_organisations.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:google_maps_flutter/google_maps_flutter.dart' as gmaps; + +// /v1/supplier/location + +@JsonSerializable() +class LatLng { + LatLng({ + this.lat, + this.lng + }); + + final double lat, lng; +} + +@JsonSerializable() +class Region { + Region({ + this.coords, + this.id, + this.name, + this.zoom + }); + + final LatLng coords; + final String id, name; + final double zoom; +} + +@JsonSerializable() +class Location { + Location({ + this.organisation, + this.lat, + this.lng + }); + + final Organisation organisation; + final double lat, lng; +} + +@JsonSerializable() +class Locations { + Locations({ + this.locations, + this.regions + }); + + final List locations; + final List regions; +} + +Future getLocations(gmaps.LatLng ne, gmaps.LatLng sw) async { + const pearLocationsURL = 'https://dev.localspend.co.uk/api/v1/supplier/location'; + SharedPreferences preferences = await SharedPreferences.getInstance(); + + Map> mapData = { + 'session_key': preferences.get('LastToken'), + 'north_east': { + 'latitude': ne.latitude, + 'longitude': ne.longitude + }, + 'south_west': { + 'latitude': sw.latitude, + 'longitude': sw.longitude + }, + }; + + final response = await http.post( + pearLocationsURL, + body: json.encode(mapData), + ); + + if (response.statusCode == 200) { + print(response.body.toString()); + } else { + print(response.body.toString()); + throw HttpException( + 'Error - ' + response.reasonPhrase, + ); + } +} \ No newline at end of file diff --git a/lib/common/apifunctions/request_login_api.dart b/lib/common/apifunctions/request_login_api.dart new file mode 100644 index 0000000..552332e --- /dev/null +++ b/lib/common/apifunctions/request_login_api.dart @@ -0,0 +1,75 @@ +import 'dart:async'; +import 'dart:convert'; + +import 'package:flutter/material.dart'; +import 'package:http/http.dart' as http; +import 'package:local_spend/common/functions/save_current_login.dart'; +import 'package:local_spend/model/json/login_model.dart'; + +Future _incorrectDialog(BuildContext context, bool isLoginWrong) async { + return showDialog( + context: context, + barrierDismissible: true, + builder: (BuildContext context) { + return AnimatedContainer( + duration: Duration(seconds: 2), + child: AlertDialog( + title: Text("Uh-oh!"), + content: Text(isLoginWrong + ? "Incorrect login details. Please try again." + : "Our servers are having issues at the moment; sorry for the inconvenience. Please try again later."), + actions: [ + FlatButton( + child: Text('OK'), + onPressed: () { + Navigator.of(context).pop(); + }, + ), + ], + ), + ); + }, + ); +} + +Future requestLoginAPI( + BuildContext context, String email, String password) async { + final url = "https://dev.localspend.co.uk/api/login"; + + Map body = { + 'email': email, + 'password': password, + }; + + try { + final response = await http + .post( + url, + body: json.encode(body), + ) + .timeout(Duration(seconds: 5)); + + if (response.statusCode == 200) { + final responseJson = json.decode(response.body); + + saveCurrentLogin(responseJson, body["email"]); + await Navigator.of(context).pushReplacementNamed('/HomePage'); + + return LoginModel.fromJson(responseJson); + } else { + final responseJson = json.decode(response.body); + + saveCurrentLogin(responseJson, body["email"]); + + await _incorrectDialog(context, true); + + return null; + } + } on TimeoutException catch (_) { + await _incorrectDialog(context, false); + } catch (error) { + debugPrint(error.toString()); + await _incorrectDialog(context, false); + } + return null; +} diff --git a/lib/common/apifunctions/request_logout_api.dart b/lib/common/apifunctions/request_logout_api.dart new file mode 100644 index 0000000..471ce9f --- /dev/null +++ b/lib/common/apifunctions/request_logout_api.dart @@ -0,0 +1,29 @@ +import 'dart:async'; +import 'dart:convert'; + +import 'package:http/http.dart' as http; +import 'package:local_spend/common/functions/get_token.dart'; +import 'package:local_spend/common/functions/save_logout.dart'; + +Future requestLogoutAPI() async { + saveLogout(); + + final url = "https://dev.localspend.co.uk/api/logout"; + + var token; + + await getToken().then((result) { + token = result; + }); + + Map body = { + "Token": token, + }; + + await http.post( + url, + body: json.encode(body), + ); + + return true; +} diff --git a/lib/common/apifunctions/submit_receipt_api.dart b/lib/common/apifunctions/submit_receipt_api.dart new file mode 100644 index 0000000..f6761b4 --- /dev/null +++ b/lib/common/apifunctions/submit_receipt_api.dart @@ -0,0 +1,76 @@ +import 'dart:async'; +import 'dart:convert'; + +import 'package:flutter/material.dart'; +import 'package:http/http.dart' as http; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:local_spend/common/functions/show_dialog_single_button.dart'; +import 'package:local_spend/model/json/login_model.dart'; + +class Receipt { + var amount = ""; + var time = ""; + var street = ""; + var category = ""; + var organisationName = ""; + var postcode = ""; + var recurring = ""; + var town = ""; + + var essential = "false"; +} + +Future submitReceiptAPI( + BuildContext context, Receipt receipt) async { + //var apiUrl = ConfigWrapper.of(context).apiKey; + final url = "https://dev.localspend.co.uk/api/upload"; + + SharedPreferences preferences = await SharedPreferences.getInstance(); + + Map body = { + 'transaction_type': "3", + 'transaction_value': receipt.amount, + 'purchase_time': receipt.time, + 'category': receipt.category, + 'essential': receipt.essential, + 'organisation_name': receipt.organisationName, + 'recurring': receipt.recurring, + 'street_name': receipt.street, + 'postcode': receipt.postcode, + 'town': receipt.town, + 'session_key': preferences.get('LastToken'), + }; + +// debugPrint('$body'); + debugPrint(json.encode(body)); + + final response = await http.post( + url, + body: json.encode(body), + ); + +// debugPrint(response.body); + + if (response.statusCode == 200) { + final responseJson = json.decode(response.body); + +// print(responseJson[0]); + + showDialogSingleButton( + context, + responseJson[0] == "" ? responseJson[0] : "Upload Successful", + "Transaction successfully submitted to server", + "OK"); + return LoginModel.fromJson(responseJson); + } else { + final responseJson = json.decode(response.body); + + showDialogSingleButton( + context, + "Unable to Submit Receipt", +// "You may have supplied an invalid 'Email' / 'Password' combination. Please try again or email an administrator.", + "Message from server: " + responseJson[1], + "OK"); + return null; + } +} diff --git a/lib/common/functions/customAbout.dart b/lib/common/functions/customAbout.dart new file mode 100644 index 0000000..ba6168d --- /dev/null +++ b/lib/common/functions/customAbout.dart @@ -0,0 +1,534 @@ +// Copyright 2016 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; +//import 'dart:developer' show Timeline, Flow; +import 'dart:developer' as dev; +import 'dart:io' show Platform; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/scheduler.dart'; +import 'package:flutter/widgets.dart' hide Flow; +import 'package:flutter/material.dart'; + +/// A [ListTile] that shows an about box. +/// +/// This widget is often added to an app's [Drawer]. When tapped it shows +/// an about box dialog with [showAboutDialog]. +/// +/// The about box will include a button that shows licenses for software used by +/// the application. The licenses shown are those returned by the +/// [LicenseRegistry] API, which can be used to add more licenses to the list. +/// +/// If your application does not have a [Drawer], you should provide an +/// affordance to call [showAboutDialog] or (at least) [showLicensePage]. +class AboutListTile extends StatelessWidget { + /// Creates a list tile for showing an about box. + /// + /// The arguments are all optional. The application name, if omitted, will be + /// derived from the nearest [Title] widget. The version, icon, and legalese + /// values default to the empty string. + const AboutListTile({ + Key key, + this.icon = const Icon(null), + this.child, + this.applicationName, + this.applicationVersion, + this.applicationIcon, + this.applicationLegalese, + this.aboutBoxChildren, + }) : super(key: key); + + /// The icon to show for this drawer item. + /// + /// By default no icon is shown. + /// + /// This is not necessarily the same as the image shown in the dialog box + /// itself; which is controlled by the [applicationIcon] property. + final Widget icon; + + /// The label to show on this drawer item. + /// + /// Defaults to a text widget that says "About Foo" where "Foo" is the + /// application name specified by [applicationName]. + final Widget child; + + /// The name of the application. + /// + /// This string is used in the default label for this drawer item (see + /// [child]) and as the caption of the [AboutDialog] that is shown. + /// + /// Defaults to the value of [Title.title], if a [Title] widget can be found. + /// Otherwise, defaults to [Platform.resolvedExecutable]. + final String applicationName; + + /// The version of this build of the application. + /// + /// This string is shown under the application name in the [AboutDialog]. + /// + /// Defaults to the empty string. + final String applicationVersion; + + /// The icon to show next to the application name in the [AboutDialog]. + /// + /// By default no icon is shown. + /// + /// Typically this will be an [ImageIcon] widget. It should honor the + /// [IconTheme]'s [IconThemeData.size]. + /// + /// This is not necessarily the same as the icon shown on the drawer item + /// itself, which is controlled by the [icon] property. + final Widget applicationIcon; + + /// A string to show in small print in the [AboutDialog]. + /// + /// Typically this is a copyright notice. + /// + /// Defaults to the empty string. + final String applicationLegalese; + + /// Widgets to add to the [AboutDialog] after the name, version, and legalese. + /// + /// This could include a link to a Web site, some descriptive text, credits, + /// or other information to show in the about box. + /// + /// Defaults to nothing. + final List aboutBoxChildren; + + @override + Widget build(BuildContext context) { + assert(debugCheckHasMaterial(context)); + assert(debugCheckHasMaterialLocalizations(context)); + return ListTile( + leading: icon, + title: child ?? + Text(MaterialLocalizations.of(context).aboutListTileTitle( + applicationName ?? _defaultApplicationName(context))), + onTap: () { + showAboutDialog( + context: context, + applicationName: applicationName, + applicationVersion: applicationVersion, + applicationIcon: applicationIcon, + applicationLegalese: applicationLegalese, + children: aboutBoxChildren, + ); + }, + ); + } +} + +/// Displays an [AboutDialog], which describes the application and provides a +/// button to show licenses for software used by the application. +/// +/// The arguments correspond to the properties on [AboutDialog]. +/// +/// If the application has a [Drawer], consider using [AboutListTile] instead +/// of calling this directly. +/// +/// If you do not need an about box in your application, you should at least +/// provide an affordance to call [showLicensePage]. +/// +/// The licenses shown on the [LicensePage] are those returned by the +/// [LicenseRegistry] API, which can be used to add more licenses to the list. +/// +/// The `context` argument is passed to [showDialog], the documentation for +/// which discusses how it is used. +void showAboutDialog({ + @required BuildContext context, + String applicationName, + String applicationVersion, + Widget applicationIcon, + String applicationLegalese, + List children, +}) { + assert(context != null); + showDialog( + context: context, + builder: (BuildContext context) { + return AboutDialog( + applicationName: applicationName, + applicationVersion: applicationVersion, + applicationIcon: applicationIcon, + applicationLegalese: applicationLegalese, + children: children, + ); + }, + ); +} + +/// Displays a [LicensePage], which shows licenses for software used by the +/// application. +/// +/// The arguments correspond to the properties on [LicensePage]. +/// +/// If the application has a [Drawer], consider using [AboutListTile] instead +/// of calling this directly. +/// +/// The [AboutDialog] shown by [showAboutDialog] includes a button that calls +/// [showLicensePage]. +/// +/// The licenses shown on the [LicensePage] are those returned by the +/// [LicenseRegistry] API, which can be used to add more licenses to the list. +void showLicensePage({ + @required BuildContext context, + String applicationName, + String applicationVersion, + Widget applicationIcon, + String applicationLegalese, +}) { + assert(context != null); + Navigator.push( + context, + MaterialPageRoute( + builder: (BuildContext context) => LicensePage( + applicationName: applicationName, + applicationVersion: applicationVersion, + applicationLegalese: applicationLegalese, + ))); +} + +/// An about box. This is a dialog box with the application's icon, name, +/// version number, and copyright, plus a button to show licenses for software +/// used by the application. +/// +/// To show an [AboutDialog], use [showAboutDialog]. +/// +/// If the application has a [Drawer], the [AboutListTile] widget can make the +/// process of showing an about dialog simpler. +/// +/// The [AboutDialog] shown by [showAboutDialog] includes a button that calls +/// [showLicensePage]. +/// +/// The licenses shown on the [LicensePage] are those returned by the +/// [LicenseRegistry] API, which can be used to add more licenses to the list. +class AboutDialog extends StatelessWidget { + /// Creates an about box. + /// + /// The arguments are all optional. The application name, if omitted, will be + /// derived from the nearest [Title] widget. The version, icon, and legalese + /// values default to the empty string. + const AboutDialog({ + Key key, + this.applicationName, + this.applicationVersion, + this.applicationIcon, + this.applicationLegalese, + this.children, + }) : super(key: key); + + /// The name of the application. + /// + /// Defaults to the value of [Title.title], if a [Title] widget can be found. + /// Otherwise, defaults to [Platform.resolvedExecutable]. + final String applicationName; + + /// The version of this build of the application. + /// + /// This string is shown under the application name. + /// + /// Defaults to the empty string. + final String applicationVersion; + + /// The icon to show next to the application name. + /// + /// By default no icon is shown. + /// + /// Typically this will be an [ImageIcon] widget. It should honor the + /// [IconTheme]'s [IconThemeData.size]. + final Widget applicationIcon; + + /// A string to show in small print. + /// + /// Typically this is a copyright notice. + /// + /// Defaults to the empty string. + final String applicationLegalese; + + /// Widgets to add to the dialog box after the name, version, and legalese. + /// + /// This could include a link to a Web site, some descriptive text, credits, + /// or other information to show in the about box. + /// + /// Defaults to nothing. + final List children; + + @override + Widget build(BuildContext context) { + assert(debugCheckHasMaterialLocalizations(context)); + final String name = applicationName ?? _defaultApplicationName(context); + final String version = + applicationVersion ?? _defaultApplicationVersion(context); + final Widget icon = applicationIcon ?? _defaultApplicationIcon(context); + List body = []; + if (icon != null) { + body.add( + IconTheme(data: const IconThemeData(size: 45.0), child: icon), + ); + } + + body.add(Expanded( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 24.0), + child: ListBody( + children: [ + Container( + child: Text(name, style: Theme.of(context).textTheme.title), + ), + Text(version, style: Theme.of(context).textTheme.body1), + Text(applicationLegalese ?? '', + style: Theme.of(context).textTheme.caption), + ], + ), + ), + )); + body = [ + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: body, + ), + ]; + if (children != null) body.addAll(children); + return AlertDialog( + content: SingleChildScrollView( + child: ListBody(children: body), + ), + actions: [ + FlatButton( + child: + Text(MaterialLocalizations.of(context).viewLicensesButtonLabel), + onPressed: () { + showLicensePage( + context: context, + applicationName: applicationName, + applicationVersion: applicationVersion, + applicationIcon: applicationIcon, + applicationLegalese: applicationLegalese, + ); + }, + ), + FlatButton( + child: Text(MaterialLocalizations.of(context).closeButtonLabel), + onPressed: () { + Navigator.pop(context); + }, + ), + ], + ); + } +} + +/// A page that shows licenses for software used by the application. +/// +/// To show a [LicensePage], use [showLicensePage]. +/// +/// The [AboutDialog] shown by [showAboutDialog] and [AboutListTile] includes +/// a button that calls [showLicensePage]. +/// +/// The licenses shown on the [LicensePage] are those returned by the +/// [LicenseRegistry] API, which can be used to add more licenses to the list. +class LicensePage extends StatefulWidget { + /// Creates a page that shows licenses for software used by the application. + /// + /// The arguments are all optional. The application name, if omitted, will be + /// derived from the nearest [Title] widget. The version and legalese values + /// default to the empty string. + /// + /// The licenses shown on the [LicensePage] are those returned by the + /// [LicenseRegistry] API, which can be used to add more licenses to the list. + const LicensePage({ + Key key, + this.applicationName, + this.applicationVersion, + this.applicationLegalese, + }) : super(key: key); + + /// The name of the application. + /// + /// Defaults to the value of [Title.title], if a [Title] widget can be found. + /// Otherwise, defaults to [Platform.resolvedExecutable]. + final String applicationName; + + /// The version of this build of the application. + /// + /// This string is shown under the application name. + /// + /// Defaults to the empty string. + final String applicationVersion; + + /// A string to show in small print. + /// + /// Typically this is a copyright notice. + /// + /// Defaults to the empty string. + final String applicationLegalese; + + @override + _LicensePageState createState() => _LicensePageState(); +} + +class _LicensePageState extends State { + @override + void initState() { + super.initState(); + _initLicenses(); + } + + final List _licenses = []; + bool _loaded = false; + + Future _initLicenses() async { + int debugFlowId = -1; + assert(() { + final dev.Flow flow = dev.Flow.begin(); + dev.Timeline.timeSync('_initLicenses()', () {}, flow: flow); + debugFlowId = flow.id; + return true; + }()); + await for (LicenseEntry license in LicenseRegistry.licenses) { + if (!mounted) { + return; + } + assert(() { + dev.Timeline.timeSync('_initLicenses()', () {}, + flow: dev.Flow.step(debugFlowId)); + return true; + }()); + final List paragraphs = + await SchedulerBinding.instance.scheduleTask>( + license.paragraphs.toList, + Priority.animation, + debugLabel: 'License', + ); + setState(() { + _licenses.add(const Padding( + padding: EdgeInsets.symmetric(vertical: 18.0), + child: Text( + '🍀‬', // That's U+1F340. Could also use U+2766 (❦) if U+1F340 doesn't work everywhere. + textAlign: TextAlign.center, + ), + )); + _licenses.add(Container( + decoration: const BoxDecoration( + border: Border(bottom: BorderSide(width: 0.0))), + child: Text( + license.packages.join(', '), + style: const TextStyle(fontWeight: FontWeight.bold), + textAlign: TextAlign.center, + ), + )); + for (LicenseParagraph paragraph in paragraphs) { + if (paragraph.indent == LicenseParagraph.centeredIndent) { + _licenses.add(Padding( + padding: const EdgeInsets.only(top: 16.0), + child: Text( + paragraph.text, + style: const TextStyle(fontWeight: FontWeight.bold), + textAlign: TextAlign.center, + ), + )); + } else { + assert(paragraph.indent >= 0); + _licenses.add(Padding( + padding: EdgeInsetsDirectional.only( + top: 8.0, start: 16.0 * paragraph.indent), + child: Text(paragraph.text), + )); + } + } + }); + } + setState(() { + _loaded = true; + }); + assert(() { + dev.Timeline.timeSync('Build scheduled', () {}, + flow: dev.Flow.end(debugFlowId)); + return true; + }()); + } + + @override + Widget build(BuildContext context) { + assert(debugCheckHasMaterialLocalizations(context)); + final String name = + widget.applicationName ?? _defaultApplicationName(context); + final String version = + widget.applicationVersion ?? _defaultApplicationVersion(context); + final MaterialLocalizations localizations = + MaterialLocalizations.of(context); + final List contents = [ + Text(name, + style: Theme.of(context).textTheme.headline, + textAlign: TextAlign.center), + Text(version, + style: Theme.of(context).textTheme.body1, + textAlign: TextAlign.center), + Container(height: 18.0), + Text(widget.applicationLegalese ?? '', + style: Theme.of(context).textTheme.caption, + textAlign: TextAlign.center), + Container(height: 18.0), + Text('Powered by Flutter', + style: Theme.of(context).textTheme.body1, + textAlign: TextAlign.center), + Container(height: 24.0), + ]; + contents.addAll(_licenses); + if (!_loaded) { + contents.add(const Padding( + padding: EdgeInsets.symmetric(vertical: 24.0), + child: Center( + child: CircularProgressIndicator(), + ), + )); + } + return Scaffold( + appBar: AppBar( + title: Text(localizations.licensesPageTitle), + ), + // All of the licenses page text is English. We don't want localized text + // or text direction. + body: Localizations.override( + locale: const Locale('en', 'US'), + context: context, + child: DefaultTextStyle( + style: Theme.of(context).textTheme.caption, + child: SafeArea( + bottom: false, + child: Scrollbar( + child: ListView( + padding: + const EdgeInsets.symmetric(horizontal: 8.0, vertical: 12.0), + children: contents, + ), + ), + ), + ), + ), + ); + } +} + +String _defaultApplicationName(BuildContext context) { + // This doesn't handle the case of the application's title dynamically + // changing. In theory, we should make Title expose the current application + // title using an InheritedWidget, and so forth. However, in practice, if + // someone really wants their application title to change dynamically, they + // can provide an explicit applicationName to the widgets defined in this + // file, instead of relying on the default. + final Title ancestorTitle = context.ancestorWidgetOfExactType(Title); + return ancestorTitle?.title ?? + Platform.resolvedExecutable.split(Platform.pathSeparator).last; +} + +String _defaultApplicationVersion(BuildContext context) { + // TODO(ianh): Get this from the embedder somehow. + return ''; +} + +Widget _defaultApplicationIcon(BuildContext context) { + // TODO(ianh): Get this from the embedder somehow. + return null; +} diff --git a/lib/common/functions/feedback.dart b/lib/common/functions/feedback.dart new file mode 100644 index 0000000..921c752 --- /dev/null +++ b/lib/common/functions/feedback.dart @@ -0,0 +1,58 @@ +import 'package:flutter/material.dart'; + +final TextEditingController _feedbackController = TextEditingController(); + +void feedback(BuildContext context) { + // flutter defined function + showDialog( + context: context, + builder: (BuildContext context) { + // return object of type Dialog + return AlertDialog( + title: new Text("Feedback"), + content: Column( + children: [ + Text("We would love to hear your thoughts."), + Text("\nThis section needs to be fixed."), + Container( + margin: const EdgeInsets.fromLTRB(0, 20, 0, 0), + width: 200, + height: 70, + child: new TextField( + controller: _feedbackController, + decoration: InputDecoration( + hintText: 'Enter your feedback here', + ), + // obscureText: true, + autocorrect: true, + maxLines: 20, + style: TextStyle( + fontSize: 18.0, + color: Colors.grey[800], + fontWeight: FontWeight.bold, + ), + onSubmitted: (_) { + submit(_feedbackController.text); + }, + ), + ), + new Container( + padding: const EdgeInsets.fromLTRB(0, 10, 0, 0), + child: new FlatButton( + child: new Text("Submit"), + onPressed: () { + submit(_feedbackController.text); + Navigator.of(context).pop(); + }, + ), + ), + ], + ), + ); + }, + ); +} + +void submit(String feedback) { + debugPrint(feedback); +} diff --git a/lib/common/functions/get_token.dart b/lib/common/functions/get_token.dart new file mode 100644 index 0000000..71a1fa5 --- /dev/null +++ b/lib/common/functions/get_token.dart @@ -0,0 +1,9 @@ +import 'package:shared_preferences/shared_preferences.dart'; +import 'dart:async'; + +Future getToken() async { + SharedPreferences preferences = await SharedPreferences.getInstance(); + + String getToken = preferences.getString("LastToken"); + return getToken; +} diff --git a/lib/common/functions/logout.dart b/lib/common/functions/logout.dart new file mode 100644 index 0000000..128eab8 --- /dev/null +++ b/lib/common/functions/logout.dart @@ -0,0 +1,17 @@ +import 'package:flutter/material.dart'; +import 'package:local_spend/common/apifunctions/request_logout_api.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +void logout(context) { + _clearLoginDetails().then((_) { + requestLogoutAPI(); + Navigator.of(context).pushReplacementNamed('/LoginPage'); + }); +} + +Future _clearLoginDetails() async { + SharedPreferences preferences = await SharedPreferences.getInstance(); + + await preferences.setString('username', ""); + await preferences.setString('password', ""); +} diff --git a/lib/common/functions/save_current_login.dart b/lib/common/functions/save_current_login.dart new file mode 100644 index 0000000..824e09b --- /dev/null +++ b/lib/common/functions/save_current_login.dart @@ -0,0 +1,29 @@ +import 'package:local_spend/model/json/login_model.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +void saveCurrentLogin(Map responseJson, loginEmail) async { + SharedPreferences preferences = await SharedPreferences.getInstance(); + + var user; + if ((responseJson != null && responseJson.isNotEmpty)) { + user = LoginModel.fromJson(responseJson).userName; + } else { + user = ""; + } + var token = (responseJson != null && responseJson.isNotEmpty) + ? LoginModel.fromJson(responseJson).token + : ""; + var email = (loginEmail != null) ? loginEmail : ""; + var userType = (responseJson != null && responseJson.isNotEmpty) + ? LoginModel.fromJson(responseJson).userType + : ""; + + await preferences.setString( + 'LastUser', (user != null && user.length > 0) ? user : ""); + await preferences.setString( + 'LastToken', (token != null && token.isNotEmpty) ? token : ""); + await preferences.setString( + 'LastEmail', (email != null && email.length > 0) ? email : ""); + await preferences.setString('LastUserType', + (userType != null && userType.isNotEmpty) ? userType : ""); +} diff --git a/lib/common/functions/save_logout.dart b/lib/common/functions/save_logout.dart new file mode 100644 index 0000000..4d5e419 --- /dev/null +++ b/lib/common/functions/save_logout.dart @@ -0,0 +1,9 @@ +import 'package:shared_preferences/shared_preferences.dart'; + +void saveLogout() async { + SharedPreferences preferences = await SharedPreferences.getInstance(); + + await preferences.setString('LastUser', ""); + await preferences.setString('LastToken', ""); + await preferences.setString('LastEmail', ""); +} diff --git a/lib/common/functions/showDialogTwoButtons.dart b/lib/common/functions/showDialogTwoButtons.dart new file mode 100644 index 0000000..5949a64 --- /dev/null +++ b/lib/common/functions/showDialogTwoButtons.dart @@ -0,0 +1,34 @@ +import 'package:flutter/material.dart'; + +void showDialogTwoButtons(BuildContext context, String title, String message, + String buttonLabel1, String buttonLabel2, Function action) { + // flutter defined function + showDialog( + context: context, + builder: (BuildContext context) { + // return object of type Dialog + return AlertDialog( + title: new Text(title), + content: new Text(message), + actions: [ + // usually buttons at the bottom of the dialog + + new FlatButton( + child: new Text(buttonLabel1), + onPressed: () { + Navigator.of(context).pop(); + }, + ), + + new FlatButton( + child: new Text(buttonLabel2), + onPressed: () { + Navigator.of(context).pop(); + action(context); + }, + ), + ], + ); + }, + ); +} diff --git a/lib/common/functions/show_dialog_single_button.dart b/lib/common/functions/show_dialog_single_button.dart new file mode 100644 index 0000000..b9d64e8 --- /dev/null +++ b/lib/common/functions/show_dialog_single_button.dart @@ -0,0 +1,25 @@ +import 'package:flutter/material.dart'; + +void showDialogSingleButton( + BuildContext context, String title, String message, String buttonLabel) { + // flutter defined function + showDialog( + context: context, + builder: (BuildContext context) { + // return object of type Dialog + return AlertDialog( + title: new Text(title), + content: new Text(message), + actions: [ + // usually buttons at the bottom of the dialog + new FlatButton( + child: new Text(buttonLabel), + onPressed: () { + Navigator.of(context).pop(); + }, + ), + ], + ); + }, + ); +} diff --git a/lib/common/platform/platform_scaffold.dart b/lib/common/platform/platform_scaffold.dart new file mode 100644 index 0000000..2b2b0e6 --- /dev/null +++ b/lib/common/platform/platform_scaffold.dart @@ -0,0 +1,73 @@ +import 'dart:io'; + +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; + +class PlatformScaffold extends StatelessWidget { + PlatformScaffold( + {this.key, + this.appBar, + this.body, + this.floatingActionButton, + this.floatingActionButtonLocation, + this.floatingActionButtonAnimator, + this.persistentFooterButtons, + this.drawer, + this.endDrawer, + this.bottomNavigationBar, + this.backgroundColor, + this.resizeToAvoidBottomPadding = true, + this.primary = true}) + : assert(primary != null), + super(key: key); + + @override + final Key key; + final PreferredSizeWidget appBar; + final Widget body; + final Widget floatingActionButton; + final FloatingActionButtonLocation floatingActionButtonLocation; + final FloatingActionButtonAnimator floatingActionButtonAnimator; + final List persistentFooterButtons; + final Widget drawer; + final Widget endDrawer; + final Widget bottomNavigationBar; + final Color backgroundColor; + final bool resizeToAvoidBottomPadding; + final bool primary; + + @override + Widget build(BuildContext context) { + return Platform.isIOS + ? Scaffold( + key: key, + appBar: appBar, + body: body, + floatingActionButton: floatingActionButton, + persistentFooterButtons: persistentFooterButtons, + floatingActionButtonLocation: floatingActionButtonLocation, + floatingActionButtonAnimator: floatingActionButtonAnimator, + drawer: endDrawer, + endDrawer: drawer, + bottomNavigationBar: bottomNavigationBar, + backgroundColor: backgroundColor, + resizeToAvoidBottomPadding: resizeToAvoidBottomPadding, + primary: primary, + ) + : Scaffold( + key: key, + appBar: appBar, + body: body, + floatingActionButton: floatingActionButton, + persistentFooterButtons: persistentFooterButtons, + floatingActionButtonLocation: floatingActionButtonLocation, + floatingActionButtonAnimator: floatingActionButtonAnimator, + drawer: drawer, + endDrawer: endDrawer, + bottomNavigationBar: bottomNavigationBar, + backgroundColor: backgroundColor, + resizeToAvoidBottomPadding: resizeToAvoidBottomPadding, + primary: primary, + ); + } +} diff --git a/lib/common/widgets/animatedGradientButton.dart b/lib/common/widgets/animatedGradientButton.dart new file mode 100644 index 0000000..aca5789 --- /dev/null +++ b/lib/common/widgets/animatedGradientButton.dart @@ -0,0 +1,40 @@ +import 'package:flutter/material.dart'; +import 'package:simple_animations/simple_animations.dart'; + +class AnimatedBackground extends StatelessWidget { + AnimatedBackground( + this.animateColors, + this.lastColor, + this.begin, + this.end, + this.duration, + ); + + final List animateColors; + final Color lastColor; + final Alignment begin, end; + final int duration; + + @override + Widget build(BuildContext context) { + final tween = MultiTrackTween([ + Track("color1").add(Duration(seconds: this.duration), + ColorTween(begin: this.animateColors[0], end: this.animateColors[1])), + ]); + + return ControlledAnimation( + playback: Playback.MIRROR, + tween: tween, + duration: tween.duration, + builder: (context, animation) { + return Container( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: this.begin, + end: this.end, + colors: [animation["color1"], this.lastColor])), + ); + }, + ); + } +} diff --git a/lib/common/widgets/charts/auto_label.dart b/lib/common/widgets/charts/auto_label.dart new file mode 100644 index 0000000..85174d0 --- /dev/null +++ b/lib/common/widgets/charts/auto_label.dart @@ -0,0 +1,73 @@ +/// Donut chart with labels example. This is a simple pie chart with a hole in +/// the middle. +import 'package:charts_flutter/flutter.dart' as charts; +import 'package:flutter/material.dart'; + +class DonutAutoLabelChart extends StatelessWidget { + DonutAutoLabelChart(this.seriesList, {this.animate}); + + /// Creates a [PieChart] with sample data and no transition. + factory DonutAutoLabelChart.withSampleData() { + return new DonutAutoLabelChart( + _createSampleData(), + // Disable animations for image tests. + animate: true, + ); + } + + final List seriesList; + final bool animate; + + @override + Widget build(BuildContext context) { + return new charts.PieChart(seriesList, + animate: animate, + // Configure the width of the pie slices to 60px. The remaining space in + // the chart will be left as a hole in the center. + // + // [ArcLabelDecorator] will automatically position the label inside the + // arc if the label will fit. If the label will not fit, it will draw + // outside of the arc with a leader line. Labels can always display + // inside or outside using [LabelPosition]. + // + // Text style for inside / outside can be controlled independently by + // setting [insideLabelStyleSpec] and [outsideLabelStyleSpec]. + // + // Example configuring different styles for inside/outside: + // new charts.ArcLabelDecorator( + // insideLabelStyleSpec: new charts.TextStyleSpec(...), + // outsideLabelStyleSpec: new charts.TextStyleSpec(...)), + defaultRenderer: new charts.ArcRendererConfig( + arcWidth: 60, + arcRendererDecorators: [new charts.ArcLabelDecorator()])); + } + + /// Create one series with sample hard coded data. + static List> _createSampleData() { + final data = [ + new LinearSales("Other", 17), + new LinearSales("Mars", 26), + new LinearSales("Morecambe", 35), + new LinearSales("Lancaster", 48), + ]; + + return [ + new charts.Series( + id: 'Sales', + domainFn: (LinearSales sales, _) => sales.key, + measureFn: (LinearSales sales, _) => sales.sales, + data: data, + // Set a label accessor to control the text of the arc label. + labelAccessorFn: (LinearSales row, _) => '${row.key}: ${row.sales}', + ) + ]; + } +} + +/// Sample linear data type. +class LinearSales { + LinearSales(this.key, this.sales); + + final String key; + final int sales; +} diff --git a/lib/common/widgets/charts/chart_builder.dart b/lib/common/widgets/charts/chart_builder.dart new file mode 100644 index 0000000..1b6514e --- /dev/null +++ b/lib/common/widgets/charts/chart_builder.dart @@ -0,0 +1,18 @@ +import 'package:flutter/material.dart'; +import 'package:local_spend/common/widgets/charts/time_series_simple.dart'; + +class TimeSeries extends StatelessWidget { + TimeSeries({ + this.chartDataName, + }); + + final String chartDataName; + + @override + Widget build(BuildContext context) { + return new Container( + padding: EdgeInsets.symmetric(horizontal: 7.5, vertical: 7.5), + child: SimpleTimeSeriesChart.withSampleData(), + ); + } +} diff --git a/lib/common/widgets/charts/donut_chart.dart b/lib/common/widgets/charts/donut_chart.dart new file mode 100644 index 0000000..1b4fe11 --- /dev/null +++ b/lib/common/widgets/charts/donut_chart.dart @@ -0,0 +1,54 @@ +/// Donut chart example. This is a simple pie chart with a hole in the middle. +import 'package:charts_flutter/flutter.dart' as charts; +import 'package:flutter/material.dart'; + +class DonutPieChart extends StatelessWidget { + DonutPieChart(this.seriesList, {this.animate}); + + /// Creates a [PieChart] with sample data and no transition. + factory DonutPieChart.withSampleData() { + return new DonutPieChart( + _createSampleData(), + animate: true, + ); + } + + final List seriesList; + final bool animate; + + @override + Widget build(BuildContext context) { + return new charts.PieChart(seriesList, + animate: animate, + // Configure the width of the pie slices to 60px. The remaining space in + // the chart will be left as a hole in the center. + defaultRenderer: new charts.ArcRendererConfig(arcWidth: 60)); + } + + /// Create one series with sample hard coded data. + static List> _createSampleData() { + final data = [ + new LinearSales(0, 100), + new LinearSales(1, 75), + new LinearSales(2, 25), + new LinearSales(3, 5), + ]; + + return [ + new charts.Series( + id: 'Sales', + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + data: data, + ) + ]; + } +} + +/// Sample linear data type. +class LinearSales { + LinearSales(this.year, this.sales); + + final int year; + final int sales; +} diff --git a/lib/common/widgets/charts/grouped_bar_chart.dart b/lib/common/widgets/charts/grouped_bar_chart.dart new file mode 100644 index 0000000..e069a0c --- /dev/null +++ b/lib/common/widgets/charts/grouped_bar_chart.dart @@ -0,0 +1,80 @@ +/// Bar chart example +import 'package:flutter/material.dart'; +import 'package:charts_flutter/flutter.dart' as charts; + +class GroupedBarChart extends StatelessWidget { + GroupedBarChart(this.seriesList, {this.animate}); + + factory GroupedBarChart.withSampleData() { + return new GroupedBarChart( + _createSampleData(), + // Disable animations for image tests. + animate: false, + ); + } + + final List seriesList; + final bool animate; + + @override + Widget build(BuildContext context) { + return new charts.BarChart( + seriesList, + animate: animate, + barGroupingType: charts.BarGroupingType.grouped, + ); + } + + /// Create series list with multiple series + static List> _createSampleData() { + final desktopSalesData = [ + new OrdinalSales('2014', 5), + new OrdinalSales('2015', 25), + new OrdinalSales('2016', 100), + new OrdinalSales('2017', 75), + ]; + + final tabletSalesData = [ + new OrdinalSales('2014', 25), + new OrdinalSales('2015', 50), + new OrdinalSales('2016', 10), + new OrdinalSales('2017', 20), + ]; + + final mobileSalesData = [ + new OrdinalSales('2014', 10), + new OrdinalSales('2015', 15), + new OrdinalSales('2016', 50), + new OrdinalSales('2017', 45), + ]; + + return [ + new charts.Series( + id: 'Desktop', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: desktopSalesData, + ), + new charts.Series( + id: 'Tablet', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: tabletSalesData, + ), + new charts.Series( + id: 'Mobile', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: mobileSalesData, + ), + ]; + } +} + +/// Sample ordinal data type. +class OrdinalSales { + OrdinalSales(this.year, this.sales); + + final String year; + final int sales; +} diff --git a/lib/common/widgets/charts/numeric_line_bar_combo.dart b/lib/common/widgets/charts/numeric_line_bar_combo.dart new file mode 100644 index 0000000..a0e71b4 --- /dev/null +++ b/lib/common/widgets/charts/numeric_line_bar_combo.dart @@ -0,0 +1,94 @@ +/// Example of a numeric combo chart with two series rendered as bars, and a +/// third rendered as a line. +import 'package:charts_flutter/flutter.dart' as charts; +import 'package:flutter/material.dart'; + +class NumericComboLineBarChart extends StatelessWidget { + NumericComboLineBarChart(this.seriesList, {this.animate}); + + /// Creates a [LineChart] with sample data and no transition. + factory NumericComboLineBarChart.withSampleData() { + return new NumericComboLineBarChart( + _createSampleData(), + // Disable animations for image tests. + animate: false, + ); + } + + final List seriesList; + final bool animate; + + @override + Widget build(BuildContext context) { + return new charts.NumericComboChart(seriesList, + animate: animate, + // Configure the default renderer as a line renderer. This will be used + // for any series that does not define a rendererIdKey. + defaultRenderer: new charts.LineRendererConfig(), + // Custom renderer configuration for the bar series. + customSeriesRenderers: [ + new charts.BarRendererConfig( + // ID used to link series to this renderer. + customRendererId: 'customBar') + ]); + } + + /// Create one series with sample hard coded data. + static List> _createSampleData() { + final desktopSalesData = [ + new LinearSales(0, 5), + new LinearSales(1, 25), + new LinearSales(2, 100), + new LinearSales(3, 75), + ]; + + final tableSalesData = [ + new LinearSales(0, 5), + new LinearSales(1, 25), + new LinearSales(2, 100), + new LinearSales(3, 75), + ]; + + final mobileSalesData = [ + new LinearSales(0, 10), + new LinearSales(1, 50), + new LinearSales(2, 200), + new LinearSales(3, 150), + ]; + + return [ + new charts.Series( + id: 'Desktop', + colorFn: (_, __) => charts.MaterialPalette.blue.shadeDefault, + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + data: desktopSalesData, + ) + // Configure our custom bar renderer for this series. + ..setAttribute(charts.rendererIdKey, 'customBar'), + new charts.Series( + id: 'Tablet', + colorFn: (_, __) => charts.MaterialPalette.red.shadeDefault, + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + data: tableSalesData, + ) + // Configure our custom bar renderer for this series. + ..setAttribute(charts.rendererIdKey, 'customBar'), + new charts.Series( + id: 'Mobile', + colorFn: (_, __) => charts.MaterialPalette.green.shadeDefault, + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + data: mobileSalesData), + ]; + } +} + +/// Sample linear data type. +class LinearSales { + LinearSales(this.year, this.sales); + + final int year; + final int sales; +} diff --git a/lib/common/widgets/charts/outside_label.dart b/lib/common/widgets/charts/outside_label.dart new file mode 100644 index 0000000..cd32404 --- /dev/null +++ b/lib/common/widgets/charts/outside_label.dart @@ -0,0 +1,68 @@ +/// Simple pie chart with outside labels example. +import 'package:charts_flutter/flutter.dart' as charts; +import 'package:flutter/material.dart'; + +class PieOutsideLabelChart extends StatelessWidget { + PieOutsideLabelChart(this.seriesList, {this.animate}); + + /// Creates a [PieChart] with sample data and no transition. + factory PieOutsideLabelChart.withSampleData() { + return new PieOutsideLabelChart( + _createSampleData(), + // Disable animations for image tests. + animate: true, + ); + } + + final List seriesList; + final bool animate; + + @override + Widget build(BuildContext context) { + return new charts.PieChart(seriesList, + animate: animate, + // Add an [ArcLabelDecorator] configured to render labels outside of the + // arc with a leader line. + // + // Text style for inside / outside can be controlled independently by + // setting [insideLabelStyleSpec] and [outsideLabelStyleSpec]. + // + // Example configuring different styles for inside/outside: + // new charts.ArcLabelDecorator( + // insideLabelStyleSpec: new charts.TextStyleSpec(...), + // outsideLabelStyleSpec: new charts.TextStyleSpec(...)), + defaultRenderer: new charts.ArcRendererConfig(arcRendererDecorators: [ + new charts.ArcLabelDecorator( + labelPosition: charts.ArcLabelPosition.outside) + ])); + } + + /// Create one series with sample hard coded data. + static List> _createSampleData() { + final data = [ + new LinearSales(0, 23), + new LinearSales(1, 44), + new LinearSales(2, 25), + new LinearSales(3, 15), + ]; + + return [ + new charts.Series( + id: 'Sales', + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + data: data, + // Set a label accessor to control the text of the arc label. + labelAccessorFn: (LinearSales row, _) => '${row.year}: ${row.sales}', + ) + ]; + } +} + +/// Sample linear data type. +class LinearSales { + LinearSales(this.year, this.sales); + + final int year; + final int sales; +} diff --git a/lib/common/widgets/charts/scatter_bucketingAxis_legend.dart b/lib/common/widgets/charts/scatter_bucketingAxis_legend.dart new file mode 100644 index 0000000..f59ea0a --- /dev/null +++ b/lib/common/widgets/charts/scatter_bucketingAxis_legend.dart @@ -0,0 +1,135 @@ +/// Example of a scatter plot chart with a bucketing measure axis and a legend. +/// +/// A bucketing measure axis positions all values beneath a certain threshold +/// into a reserved space on the axis range. The label for the bucket line will +/// be drawn in the middle of the bucket range, rather than aligned with the +/// gridline for that value's position on the scale. +import 'package:charts_flutter/flutter.dart' as charts; +import 'package:flutter/material.dart'; + +class BucketingAxisScatterPlotChart extends StatelessWidget { + BucketingAxisScatterPlotChart(this.seriesList, {this.animate}); + + /// Creates a [ScatterPlotChart] with sample data and no transition. + factory BucketingAxisScatterPlotChart.withSampleData() { + return new BucketingAxisScatterPlotChart( + _createSampleData(), + // Disable animations for image tests. + animate: false, + ); + } + + final List seriesList; + final bool animate; + + @override + Widget build(BuildContext context) { + return new charts.ScatterPlotChart(seriesList, + // Set up a bucketing axis that will place all values below 0.1 (10%) + // into a bucket at the bottom of the chart. + // + // Configure a tick count of 3 so that we get 100%, 50%, and the + // threshold. + primaryMeasureAxis: new charts.BucketingAxisSpec( + threshold: 0.1, + tickProviderSpec: new charts.BucketingNumericTickProviderSpec( + desiredTickCount: 3)), + // Add a series legend to display the series names. + behaviors: [ + new charts.SeriesLegend(position: charts.BehaviorPosition.end), + ], + animate: animate); + } + + /// Create one series with sample hard coded data. + static List> _createSampleData() { + final myFakeDesktopData = [ + new LinearSales(52, 0.75, 14.0), + ]; + + final myFakeTabletData = [ + new LinearSales(45, 0.3, 18.0), + ]; + + final myFakeMobileData = [ + new LinearSales(56, 0.8, 17.0), + ]; + + final myFakeChromebookData = [ + new LinearSales(25, 0.6, 13.0), + ]; + + final myFakeHomeData = [ + new LinearSales(34, 0.5, 15.0), + ]; + + final myFakeOtherData = [ + new LinearSales(10, 0.25, 15.0), + new LinearSales(12, 0.075, 14.0), + new LinearSales(13, 0.225, 15.0), + new LinearSales(16, 0.03, 14.0), + new LinearSales(24, 0.04, 13.0), + new LinearSales(37, 0.1, 14.5), + ]; + + return [ + new charts.Series( + id: 'Cheese', + colorFn: (LinearSales sales, _) => + charts.MaterialPalette.blue.shadeDefault, + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.revenueShare, + radiusPxFn: (LinearSales sales, _) => sales.radius, + data: myFakeDesktopData), + new charts.Series( + id: 'Carrots', + colorFn: (LinearSales sales, _) => + charts.MaterialPalette.red.shadeDefault, + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.revenueShare, + radiusPxFn: (LinearSales sales, _) => sales.radius, + data: myFakeTabletData), + new charts.Series( + id: 'Cucumbers', + colorFn: (LinearSales sales, _) => + charts.MaterialPalette.green.shadeDefault, + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.revenueShare, + radiusPxFn: (LinearSales sales, _) => sales.radius, + data: myFakeMobileData), + new charts.Series( + id: 'Crayons', + colorFn: (LinearSales sales, _) => + charts.MaterialPalette.purple.shadeDefault, + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.revenueShare, + radiusPxFn: (LinearSales sales, _) => sales.radius, + data: myFakeChromebookData), + new charts.Series( + id: 'Celery', + colorFn: (LinearSales sales, _) => + charts.MaterialPalette.indigo.shadeDefault, + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.revenueShare, + radiusPxFn: (LinearSales sales, _) => sales.radius, + data: myFakeHomeData), + new charts.Series( + id: 'Cauliflower', + colorFn: (LinearSales sales, _) => + charts.MaterialPalette.gray.shadeDefault, + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.revenueShare, + radiusPxFn: (LinearSales sales, _) => sales.radius, + data: myFakeOtherData), + ]; + } +} + +/// Sample linear data type. +class LinearSales { + LinearSales(this.year, this.revenueShare, this.radius); + + final int year; + final double revenueShare; + final double radius; +} diff --git a/lib/common/widgets/charts/series_legend_with_measures.dart b/lib/common/widgets/charts/series_legend_with_measures.dart new file mode 100644 index 0000000..11c5ab6 --- /dev/null +++ b/lib/common/widgets/charts/series_legend_with_measures.dart @@ -0,0 +1,128 @@ +/// Bar chart with example of a legend with customized position, justification, +/// desired max rows, and padding. These options are shown as an example of how +/// to use the customizations, they do not necessary have to be used together in +/// this way. Choosing [end] as the position does not require the justification +/// to also be [endDrawArea]. +import 'package:flutter/material.dart'; +import 'package:charts_flutter/flutter.dart' as charts; + +/// Example that shows how to build a series legend that shows measure values +/// when a datum is selected. +/// +/// Also shows the option to provide a custom measure formatter. +class LegendWithMeasures extends StatelessWidget { + LegendWithMeasures(this.seriesList, {this.animate}); + + factory LegendWithMeasures.withSampleData() { + return new LegendWithMeasures( + _createSampleData(), + // Disable animations for image tests. + animate: false, + ); + } + + final List seriesList; + final bool animate; + + @override + Widget build(BuildContext context) { + return new charts.BarChart( + seriesList, + animate: animate, + barGroupingType: charts.BarGroupingType.grouped, + // Add the legend behavior to the chart to turn on legends. + // This example shows how to optionally show measure and provide a custom + // formatter. + behaviors: [ + new charts.SeriesLegend( + // Positions for "start" and "end" will be left and right respectively + // for widgets with a build context that has directionality ltr. + // For rtl, "start" and "end" will be right and left respectively. + // Since this example has directionality of ltr, the legend is + // positioned on the right side of the chart. + position: charts.BehaviorPosition.end, + // By default, if the position of the chart is on the left or right of + // the chart, [horizontalFirst] is set to false. This means that the + // legend entries will grow as new rows first instead of a new column. + horizontalFirst: false, + // This defines the padding around each legend entry. + cellPadding: new EdgeInsets.only(right: 4.0, bottom: 4.0), + // Set show measures to true to display measures in series legend, + // when the datum is selected. + showMeasures: true, + // Optionally provide a measure formatter to format the measure value. + // If none is specified the value is formatted as a decimal. + measureFormatter: (num value) { + return value == null ? '-' : '${value}k'; + }, + ), + ], + ); + } + + /// Create series list with multiple series + static List> _createSampleData() { + final desktopSalesData = [ + new OrdinalSales('2014', 5), + new OrdinalSales('2015', 25), + new OrdinalSales('2016', 100), + new OrdinalSales('2017', 75), + ]; + + final tabletSalesData = [ + new OrdinalSales('2014', 25), + new OrdinalSales('2015', 50), + // Purposely have a null data for 2016 to show the null value format. + new OrdinalSales('2017', 20), + ]; + + final mobileSalesData = [ + new OrdinalSales('2014', 10), + new OrdinalSales('2015', 15), + new OrdinalSales('2016', 50), + new OrdinalSales('2017', 45), + ]; + + final otherSalesData = [ + new OrdinalSales('2014', 20), + new OrdinalSales('2015', 35), + new OrdinalSales('2016', 15), + new OrdinalSales('2017', 10), + ]; + + return [ + new charts.Series( + id: 'Lancaster', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: desktopSalesData, + ), + new charts.Series( + id: 'Morecambe', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: tabletSalesData, + ), + new charts.Series( + id: 'Mars', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: mobileSalesData, + ), + new charts.Series( + id: 'Other', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: otherSalesData, + ), + ]; + } +} + +/// Sample ordinal data type. +class OrdinalSales { + OrdinalSales(this.year, this.sales); + + final String year; + final int sales; +} diff --git a/lib/common/widgets/charts/time_series_simple.dart b/lib/common/widgets/charts/time_series_simple.dart new file mode 100644 index 0000000..aa733a2 --- /dev/null +++ b/lib/common/widgets/charts/time_series_simple.dart @@ -0,0 +1,59 @@ +/// Timeseries chart example +import 'package:charts_flutter/flutter.dart' as charts; +import 'package:flutter/material.dart'; + +class SimpleTimeSeriesChart extends StatelessWidget { + SimpleTimeSeriesChart(this.seriesList, {this.animate}); + + /// Creates a [TimeSeriesChart] with sample data and no transition. + factory SimpleTimeSeriesChart.withSampleData() { + return new SimpleTimeSeriesChart( + _createSampleData(), + // Disable animations for image tests. + animate: true, + ); + } + + final List seriesList; + final bool animate; + + @override + Widget build(BuildContext context) { + return new charts.TimeSeriesChart( + seriesList, + animate: animate, + // Optionally pass in a [DateTimeFactory] used by the chart. The factory + // should create the same type of [DateTime] as the data provided. If none + // specified, the default creates local date time. + dateTimeFactory: const charts.LocalDateTimeFactory(), + ); + } + + /// Create one series with sample hard coded data. + static List> _createSampleData() { + final data = [ + new TimeSeriesSales(new DateTime(2017, 9, 19), 5), + new TimeSeriesSales(new DateTime(2017, 9, 26), 25), + new TimeSeriesSales(new DateTime(2017, 10, 3), 100), + new TimeSeriesSales(new DateTime(2017, 10, 10), 75), + ]; + + return [ + new charts.Series( + id: 'Sales', + colorFn: (_, __) => charts.MaterialPalette.blue.shadeDefault, + domainFn: (TimeSeriesSales sales, _) => sales.time, + measureFn: (TimeSeriesSales sales, _) => sales.sales, + data: data, + ) + ]; + } +} + +/// Sample time series data type. +class TimeSeriesSales { + TimeSeriesSales(this.time, this.sales); + + final DateTime time; + final int sales; +} diff --git a/lib/common/widgets/custom_checkbox.dart b/lib/common/widgets/custom_checkbox.dart new file mode 100644 index 0000000..27645d5 --- /dev/null +++ b/lib/common/widgets/custom_checkbox.dart @@ -0,0 +1,403 @@ +// Copyright 2015 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:math' as math; + +import 'package:flutter/rendering.dart'; +import 'package:flutter/widgets.dart'; + +import 'package:flutter/material.dart'; + +/// A material design checkbox. +/// +/// The checkbox itself does not maintain any state. Instead, when the state of +/// the checkbox changes, the widget calls the [onChanged] callback. Most +/// widgets that use a checkbox will listen for the [onChanged] callback and +/// rebuild the checkbox with a new [value] to update the visual appearance of +/// the checkbox. +/// +/// The checkbox can optionally display three values - true, false, and null - +/// if [tristate] is true. When [value] is null a dash is displayed. By default +/// [tristate] is false and the checkbox's [value] must be true or false. +/// +/// Requires one of its ancestors to be a [Material] widget. +/// +/// See also: +/// +/// * [CheckboxListTile], which combines this widget with a [ListTile] so that +/// you can give the checkbox a label. +/// * [Switch], a widget with semantics similar to [CustomCheckbox]. +/// * [Radio], for selecting among a set of explicit values. +/// * [Slider], for selecting a value in a range. +/// * +/// * +class CustomCheckbox extends StatefulWidget { + /// Creates a material design checkbox. + /// + /// The checkbox itself does not maintain any state. Instead, when the state of + /// the checkbox changes, the widget calls the [onChanged] callback. Most + /// widgets that use a checkbox will listen for the [onChanged] callback and + /// rebuild the checkbox with a new [value] to update the visual appearance of + /// the checkbox. + /// + /// The following arguments are required: + /// + /// * [value], which determines whether the checkbox is checked. The [value] + /// can only be null if [tristate] is true. + /// * [onChanged], which is called when the value of the checkbox should + /// change. It can be set to null to disable the checkbox. + /// + /// The value of [tristate] must not be null. + const CustomCheckbox({ + Key key, + @required this.value, + this.tristate = false, + @required this.onChanged, + this.activeColor, + this.checkColor, + this.materialTapTargetSize, + this.useTapTarget = true, + }) : assert(tristate != null), + assert(tristate || value != null), + super(key: key); + + final bool useTapTarget; + + /// Whether this checkbox is checked. + /// + /// This property must not be null. + final bool value; + + /// Called when the value of the checkbox should change. + /// + /// The checkbox passes the new value to the callback but does not actually + /// change state until the parent widget rebuilds the checkbox with the new + /// value. + /// + /// If this callback is null, the checkbox will be displayed as disabled + /// and will not respond to input gestures. + /// + /// When the checkbox is tapped, if [tristate] is false (the default) then + /// the [onChanged] callback will be applied to `!value`. If [tristate] is + /// true this callback cycle from false to true to null. + /// + /// The callback provided to [onChanged] should update the state of the parent + /// [StatefulWidget] using the [State.setState] method, so that the parent + /// gets rebuilt; for example: + /// + /// ```dart + /// Checkbox( + /// value: _throwShotAway, + /// onChanged: (bool newValue) { + /// setState(() { + /// _throwShotAway = newValue; + /// }); + /// }, + /// ) + /// ``` + final ValueChanged onChanged; + + /// The color to use when this checkbox is checked. + /// + /// Defaults to [ThemeData.toggleableActiveColor]. + final Color activeColor; + + /// The color to use for the check icon when this checkbox is checked + /// + /// Defaults to Color(0xFFFFFFFF) + final Color checkColor; + + /// If true the checkbox's [value] can be true, false, or null. + /// + /// Checkbox displays a dash when its value is null. + /// + /// When a tri-state checkbox is tapped its [onChanged] callback will be + /// applied to true if the current value is null or false, false otherwise. + /// Typically tri-state checkboxes are disabled (the onChanged callback is + /// null) so they don't respond to taps. + /// + /// If tristate is false (the default), [value] must not be null. + final bool tristate; + + /// Configures the minimum size of the tap target. + /// + /// Defaults to [ThemeData.materialTapTargetSize]. + /// + /// See also: + /// + /// * [MaterialTapTargetSize], for a description of how this affects tap targets. + final MaterialTapTargetSize materialTapTargetSize; + + /// The width of a checkbox widget. + static const double width = 18.0; + + @override + _CustomCheckboxState createState() => _CustomCheckboxState(); +} + +class _CustomCheckboxState extends State + with TickerProviderStateMixin { + @override + Widget build(BuildContext context) { + assert(debugCheckHasMaterial(context)); + final ThemeData themeData = Theme.of(context); + + Size size; + switch (widget.materialTapTargetSize ?? themeData.materialTapTargetSize) { + case MaterialTapTargetSize.padded: + size = const Size( + 2 * kRadialReactionRadius + 8.0, 2 * kRadialReactionRadius + 8.0); + break; + case MaterialTapTargetSize.shrinkWrap: + size = const Size(2 * kRadialReactionRadius, 2 * kRadialReactionRadius); + break; + } + + Size noTapTargetSize = Size(CustomCheckbox.width, CustomCheckbox.width); + final BoxConstraints additionalConstraints = + BoxConstraints.tight(widget.useTapTarget ? size : noTapTargetSize); + + return _CheckboxRenderObjectWidget( + value: widget.value, + tristate: widget.tristate, + activeColor: widget.activeColor ?? themeData.toggleableActiveColor, + checkColor: widget.checkColor ?? const Color(0xFFFFFFFF), + inactiveColor: widget.onChanged != null + ? themeData.unselectedWidgetColor + : themeData.disabledColor, + onChanged: widget.onChanged, + additionalConstraints: additionalConstraints, + vsync: this, + ); + } +} + +class _CheckboxRenderObjectWidget extends LeafRenderObjectWidget { + const _CheckboxRenderObjectWidget( + {Key key, + @required this.value, + @required this.tristate, + @required this.activeColor, + @required this.checkColor, + @required this.inactiveColor, + @required this.onChanged, + @required this.vsync, + @required this.additionalConstraints, + this.useTapTarget = true}) + : assert(tristate != null), + assert(tristate || value != null), + assert(activeColor != null), + assert(inactiveColor != null), + assert(vsync != null), + super(key: key); + + final bool value; + final bool tristate; + final Color activeColor; + final Color checkColor; + final Color inactiveColor; + final ValueChanged onChanged; + final TickerProvider vsync; + final BoxConstraints additionalConstraints; + final bool useTapTarget; + + @override + _RenderCheckbox createRenderObject(BuildContext context) => _RenderCheckbox( + value: value, + tristate: tristate, + activeColor: activeColor, + checkColor: checkColor, + inactiveColor: inactiveColor, + onChanged: onChanged, + vsync: vsync, + additionalConstraints: additionalConstraints, + ); + + @override + void updateRenderObject(BuildContext context, _RenderCheckbox renderObject) { + renderObject + ..value = value + ..tristate = tristate + ..activeColor = activeColor + ..checkColor = checkColor + ..inactiveColor = inactiveColor + ..onChanged = onChanged + ..additionalConstraints = additionalConstraints + ..vsync = vsync; + } +} + +const double _kEdgeSize = CustomCheckbox.width; +const Radius _kEdgeRadius = Radius.circular(1.0); +const double _kStrokeWidth = 2.0; + +class _RenderCheckbox extends RenderToggleable { + _RenderCheckbox({ + bool value, + bool tristate, + Color activeColor, + this.checkColor, + Color inactiveColor, + BoxConstraints additionalConstraints, + ValueChanged onChanged, + @required TickerProvider vsync, + }) : _oldValue = value, + super( + value: value, + tristate: tristate, + activeColor: activeColor, + inactiveColor: inactiveColor, + onChanged: onChanged, + additionalConstraints: additionalConstraints, + vsync: vsync, + ); + + bool _oldValue; + Color checkColor; + + @override + set value(bool newValue) { + if (newValue == value) return; + _oldValue = value; + super.value = newValue; + } + + @override + void describeSemanticsConfiguration(SemanticsConfiguration config) { + super.describeSemanticsConfiguration(config); + config.isChecked = value == true; + } + + // The square outer bounds of the checkbox at t, with the specified origin. + // At t == 0.0, the outer rect's size is _kEdgeSize (Checkbox.width) + // At t == 0.5, .. is _kEdgeSize - _kStrokeWidth + // At t == 1.0, .. is _kEdgeSize + RRect _outerRectAt(Offset origin, double t) { + final double inset = 1.0 - (t - 0.5).abs() * 2.0; + final double size = _kEdgeSize - inset * _kStrokeWidth; + final Rect rect = + Rect.fromLTWH(origin.dx + inset, origin.dy + inset, size, size); + return RRect.fromRectAndRadius(rect, _kEdgeRadius); + } + + // The checkbox's border color if value == false, or its fill color when + // value == true or null. + Color _colorAt(double t) { + // As t goes from 0.0 to 0.25, animate from the inactiveColor to activeColor. + return onChanged == null + ? inactiveColor + : (t >= 0.25 + ? activeColor + : Color.lerp(inactiveColor, activeColor, t * 4.0)); + } + + // White stroke used to paint the check and dash. + void _initStrokePaint(Paint paint) { + paint + ..color = checkColor + ..style = PaintingStyle.stroke + ..strokeWidth = _kStrokeWidth; + } + + void _drawBorder(Canvas canvas, RRect outer, double t, Paint paint) { + assert(t >= 0.0 && t <= 0.5); + final double size = outer.width; + // As t goes from 0.0 to 1.0, gradually fill the outer RRect. + final RRect inner = + outer.deflate(math.min(size / 2.0, _kStrokeWidth + size * t)); + canvas.drawDRRect(outer, inner, paint); + } + + void _drawCheck(Canvas canvas, Offset origin, double t, Paint paint) { + assert(t >= 0.0 && t <= 1.0); + // As t goes from 0.0 to 1.0, animate the two check mark strokes from the + // short side to the long side. + final Path path = Path(); + const Offset start = Offset(_kEdgeSize * 0.15, _kEdgeSize * 0.45); + const Offset mid = Offset(_kEdgeSize * 0.4, _kEdgeSize * 0.7); + const Offset end = Offset(_kEdgeSize * 0.85, _kEdgeSize * 0.25); + if (t < 0.5) { + final double strokeT = t * 2.0; + final Offset drawMid = Offset.lerp(start, mid, strokeT); + path.moveTo(origin.dx + start.dx, origin.dy + start.dy); + path.lineTo(origin.dx + drawMid.dx, origin.dy + drawMid.dy); + } else { + final double strokeT = (t - 0.5) * 2.0; + final Offset drawEnd = Offset.lerp(mid, end, strokeT); + path.moveTo(origin.dx + start.dx, origin.dy + start.dy); + path.lineTo(origin.dx + mid.dx, origin.dy + mid.dy); + path.lineTo(origin.dx + drawEnd.dx, origin.dy + drawEnd.dy); + } + canvas.drawPath(path, paint); + } + + void _drawDash(Canvas canvas, Offset origin, double t, Paint paint) { + assert(t >= 0.0 && t <= 1.0); + // As t goes from 0.0 to 1.0, animate the horizontal line from the + // mid point outwards. + const Offset start = Offset(_kEdgeSize * 0.2, _kEdgeSize * 0.5); + const Offset mid = Offset(_kEdgeSize * 0.5, _kEdgeSize * 0.5); + const Offset end = Offset(_kEdgeSize * 0.8, _kEdgeSize * 0.5); + final Offset drawStart = Offset.lerp(start, mid, 1.0 - t); + final Offset drawEnd = Offset.lerp(mid, end, t); + canvas.drawLine(origin + drawStart, origin + drawEnd, paint); + } + + @override + void paint(PaintingContext context, Offset offset) { + final Canvas canvas = context.canvas; + paintRadialReaction(canvas, offset, size.center(Offset.zero)); + + final Offset origin = + offset + (size / 2.0 - const Size.square(_kEdgeSize) / 2.0); + final AnimationStatus status = position.status; + final double tNormalized = + status == AnimationStatus.forward || status == AnimationStatus.completed + ? position.value + : 1.0 - position.value; + + // Four cases: false to null, false to true, null to false, true to false + if (_oldValue == false || value == false) { + final double t = value == false ? 1.0 - tNormalized : tNormalized; + final RRect outer = _outerRectAt(origin, t); + final Paint paint = Paint()..color = _colorAt(t); + + if (t <= 0.5) { + _drawBorder(canvas, outer, t, paint); + } else { + canvas.drawRRect(outer, paint); + + _initStrokePaint(paint); + final double tShrink = (t - 0.5) * 2.0; + if (_oldValue == null || value == null) { + _drawDash(canvas, origin, tShrink, paint); + } else { + _drawCheck(canvas, origin, tShrink, paint); + } + } + } else { + // Two cases: null to true, true to null + final RRect outer = _outerRectAt(origin, 1.0); + final Paint paint = Paint()..color = _colorAt(1.0); + canvas.drawRRect(outer, paint); + + _initStrokePaint(paint); + if (tNormalized <= 0.5) { + final double tShrink = 1.0 - tNormalized * 2.0; + if (_oldValue == true) { + _drawCheck(canvas, origin, tShrink, paint); + } else { + _drawDash(canvas, origin, tShrink, paint); + } + } else { + final double tExpand = (tNormalized - 0.5) * 2.0; + if (value == true) { + _drawCheck(canvas, origin, tExpand, paint); + } else { + _drawDash(canvas, origin, tExpand, paint); + } + } + } + } +} diff --git a/lib/common/widgets/labeled_checkbox.dart b/lib/common/widgets/labeled_checkbox.dart new file mode 100644 index 0000000..36aa4f8 --- /dev/null +++ b/lib/common/widgets/labeled_checkbox.dart @@ -0,0 +1,133 @@ +import 'package:flutter/material.dart'; +import 'package:local_spend/common/widgets/custom_checkbox.dart'; + +class LabeledCheckboxWithIcon extends StatelessWidget { + const LabeledCheckboxWithIcon({ + this.label, + this.textStyle, + this.icon, + this.iconSize, + this.iconColor, + this.padding, + this.value, + this.onChanged, + }); + + final String label; + final TextStyle textStyle; + final IconData icon; + final double iconSize; + final Color iconColor; + final EdgeInsets padding; + final bool value; + final Function onChanged; + + @override + Widget build(BuildContext context) { + return InkWell( + onTap: () { + onChanged(!value); + }, + child: Padding( + padding: padding, + child: Row( +// crossAxisAlignment: CrossAxisAlignment.center, //doesn't do anything + + children: [ + Container( + padding: EdgeInsets.all(0), + width: iconSize, + child: Icon( + icon, + // size: iconSize, + color: iconColor, + ), + ), + Expanded( + child: Text( + label, + style: textStyle, + textAlign: TextAlign.center, + )), + CustomCheckbox( + //custom checkbox removes padding so the form looks nice + + value: value, + useTapTarget: false, + onChanged: (bool newValue) { + onChanged(newValue); + }, + ), + ], + ), + ), + ); + } +} + +class LabeledCheckbox extends StatelessWidget { + const LabeledCheckbox({ + this.label, + this.textStyle, + this.padding, + this.value, + this.onChanged, + }); + + final String label; + final TextStyle textStyle; + final EdgeInsets padding; + final bool value; + final Function onChanged; + + @override + Widget build(BuildContext context) { + return InkWell( + onTap: () { + onChanged(!value); + }, + child: Padding( + padding: padding, + child: Row( + children: [ + Expanded(child: Text(label, style: textStyle)), + CustomCheckbox( + //custom checkbox removes padding so the form looks nice + + value: value, + useTapTarget: false, + onChanged: (bool newValue) { + onChanged(newValue); + }, + ), + ], + ), + ), + ); + } +} + +/* +//USAGE: + +bool _isSelected = false; + +@override +Widget build(BuildContext context) { + return Scaffold( + body: Center( + child: LabeledCheckbox( + label: 'Label Text Here', + padding: const EdgeInsets.fromLTRB(0, 0, 0, 0), + value: _isSelected, + + onChanged: (bool newValue) { + setState(() { + _isSelected = newValue; + }); + }, + ), + ), + ); +} +*/ diff --git a/lib/common/widgets/organisations_dialog.dart b/lib/common/widgets/organisations_dialog.dart new file mode 100644 index 0000000..cb3d16b --- /dev/null +++ b/lib/common/widgets/organisations_dialog.dart @@ -0,0 +1,241 @@ +import 'package:flutter/material.dart'; +import 'dart:async'; +import 'package:flutter/services.dart'; +import 'package:local_spend/common/apifunctions/find_organisations.dart'; + +class FindOrganisations { + TextField getSearchBar(TextEditingController controller, String hintText) { + return TextField( + controller: controller, + decoration: InputDecoration( + hintText: hintText, + icon: Icon(Icons.search), + ), + ); + } + + // todo: get all organisations, favourites and all data from one 'organisations' class or similar + // eg items: organisations.getFavourites().orderBy(name), + + Future _moreInfoDialog(context, Organisation organisation) { + TextStyle informationTitleStyle = new TextStyle(fontSize: 16); + TextStyle informationStyle = + new TextStyle(fontSize: 16, fontWeight: FontWeight.bold); + + return showDialog( + context: context, + barrierDismissible: true, + builder: (BuildContext context) { + return StatefulBuilder( + builder: (context, setState) { + return SimpleDialog( + children: [ + Container( + padding: EdgeInsets.fromLTRB(10, 0, 10, 0), + child: Text( + organisation.name, + style: new TextStyle( + fontSize: 21, fontWeight: FontWeight.bold), + ), + ), + Container( + padding: EdgeInsets.symmetric(horizontal: 10), + child: Divider(), + ), + Container( + width: MediaQuery.of(context).size.width, + padding: EdgeInsets.symmetric(horizontal: 10), + child: Table( +// defaultColumnWidth: FixedColumnWidth(100), + children: [ + TableRow( + children: [ + Text("Street:", style: informationTitleStyle), + Text(organisation.streetName, + style: informationStyle), + ], + ), + TableRow( + children: [ + Text("Postcode:", style: informationTitleStyle), + Text(organisation.postcode.toUpperCase(), + style: informationStyle), + ], + ), + TableRow( + children: [ + Text("Town:", style: informationTitleStyle), + Text(organisation.town, style: informationStyle), + ], + ), + ], + ), + ), + ], + ); + }, + ); + }, + ); + } + + Future dialog(context) { + bool _searchEnabled = false; + bool _orgsFetched = false; + TextEditingController searchBarText = new TextEditingController(); + var organisations = new Organisations(); + var listTitle = "All Organisations"; + var organisationsList = List(); + + Future _submitSearch(String search) async { + _searchEnabled = false; + listTitle = "Results for \'" + search + "\'"; + + var futureOrgs = await organisations.findOrganisations(search); + organisationsList = futureOrgs; + _searchEnabled = true; + return futureOrgs.length; + } + + return showDialog( + context: context, + barrierDismissible: true, + builder: (BuildContext context) { + return StatefulBuilder( + builder: (context, setState) { + return SimpleDialog( + children: [ + Column( + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Container( + padding: EdgeInsets.fromLTRB(20, 0, 0, 0), + width: 150, + height: 50, + child: TextField( + autofocus: true, + controller: searchBarText, + decoration: InputDecoration( + hintText: "Payee Name", + ), + onChanged: (value) { + if (value.isNotEmpty) { + _searchEnabled = true; + } else { + _searchEnabled = false; + } + setState(() => {_searchEnabled}); + }, + onSubmitted: _searchEnabled + ? ((_) { + SystemChannels.textInput + .invokeMethod('TextInput.hide'); + var result = + _submitSearch(searchBarText.text); + result.then((_) { + setState(() { + _orgsFetched = true; + }); + }); + }) + : null, + ), + ), + Container( + width: 80, + padding: EdgeInsets.fromLTRB(20, 0, 0, 0), + child: RaisedButton( + onPressed: _searchEnabled + ? (() { + SystemChannels.textInput + .invokeMethod('TextInput.hide'); + var result = + _submitSearch(searchBarText.text); + result.then((_) { + setState(() { + _orgsFetched = true; + }); + }); + }) + : null, + child: Icon(Icons.search, color: Colors.white), + color: Colors.blue, + ), + ), + ], + ), + ], + ), + + Column( + children: _orgsFetched + ? [ + Container( + padding: EdgeInsets.fromLTRB(20, 20, 20, 0), + child: Text( + listTitle, + style: new TextStyle( + fontSize: 23, fontWeight: FontWeight.bold), + ), + ), + Container( + padding: EdgeInsets.fromLTRB(10, 10, 10, 0), + width: MediaQuery.of(context).size.width, + height: MediaQuery.of(context).size.height * 0.67, + child: Material( + shadowColor: Colors.transparent, + color: Colors.transparent, + child: ListView.builder( + itemCount: organisationsList.length, + itemBuilder: (context, index) { + return Card( + child: ListTile( + leading: Icon(Icons.person), + title: Text(organisationsList[index].name, + style: new TextStyle(fontSize: 18)), + subtitle: Text(organisationsList[index] + .postcode + .toUpperCase()), + // trailing: Icon(Icons.arrow_forward_ios), + // onTap: _chosenOrg(organisationsList[index]), + onTap: () { + Navigator.of(context) + .pop(organisationsList[index]); + }, + onLongPress: () { + // show more details about the organisation in a new dialog + var moreInfo = _moreInfoDialog( + context, organisationsList[index]); + moreInfo.whenComplete(null); + }, + ), + ); + }, + ), + ), + ), + Center( + child: Container( + padding: EdgeInsets.fromLTRB(0, 10, 0, 0), + child: Text("Long press a payee for more info", + style: + TextStyle(fontStyle: FontStyle.italic)), + ), + ), + ] + : [Container()], + ), + + // help button for if org not listed + // cancel and ok buttons + ], +// ), + ); + }, + ); + }, + ); + } +} diff --git a/lib/common/widgets/popupListView.dart b/lib/common/widgets/popupListView.dart new file mode 100644 index 0000000..e1f2c10 --- /dev/null +++ b/lib/common/widgets/popupListView.dart @@ -0,0 +1,36 @@ +import 'package:flutter/material.dart'; +import 'dart:async'; + +class PopupListView { + Future dialog(context, List options, String title) { + return showDialog( + context: context, + barrierDismissible: true, + builder: (BuildContext context) { + return SimpleDialog( + title: Text(title), + children: getDialogOptions(context, options), + ); + }, + ); + } + + List getDialogOptions( + context, List options /*, Function onPressed*/) { + var dialogOptionsList = new List(); + + for (var i = 0; i < options.length; i++) { + dialogOptionsList.add( + new SimpleDialogOption( + // print each iteration to see if any are null + child: Text(options[i]), + onPressed: () { + Navigator.of(context).pop(options[i]); + }, + ), + ); + } + + return dialogOptionsList; + } +} diff --git a/lib/config.dart b/lib/config.dart new file mode 100644 index 0000000..c01ea03 --- /dev/null +++ b/lib/config.dart @@ -0,0 +1,46 @@ +import 'package:flutter/material.dart'; +import 'package:json_annotation/json_annotation.dart'; + +part 'config.g.dart'; + +@JsonSerializable(createToJson: false) +class Config { + Config({this.env, this.production, this.apiKey}); + + factory Config.fromJson(Map json) => _$ConfigFromJson(json); + + final String env; + final bool production; + final String apiKey; +} + +class ConfigWrapper extends StatelessWidget { + ConfigWrapper({Key key, this.config, this.child}); + + @override + Widget build(BuildContext context) { + return new _InheritedConfig(config: this.config, child: this.child); + } + + static Config of(BuildContext context) { + final _InheritedConfig inheritedConfig = + context.inheritFromWidgetOfExactType(_InheritedConfig); + return inheritedConfig.config; + } + + final Config config; + final Widget child; +} + +class _InheritedConfig extends InheritedWidget { + const _InheritedConfig( + {Key key, @required this.config, @required Widget child}) + : assert(config != null), + assert(child != null), + super(key: key, child: child); + final Config config; + + @override + bool updateShouldNotify(_InheritedConfig oldWidget) => + config != oldWidget.config; +} diff --git a/lib/config.g.dart b/lib/config.g.dart new file mode 100644 index 0000000..445b22e --- /dev/null +++ b/lib/config.g.dart @@ -0,0 +1,14 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'config.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +Config _$ConfigFromJson(Map json) { + return Config( + env: json['env'] as String, + production: json['production'] as bool, + apiKey: json['apiKey'] as String); +} diff --git a/lib/env/dev.dart b/lib/env/dev.dart new file mode 100644 index 0000000..386a36b --- /dev/null +++ b/lib/env/dev.dart @@ -0,0 +1,6 @@ +import 'package:json_annotation/json_annotation.dart'; + +part 'dev.g.dart'; + +@JsonLiteral('dev.json', asConst: true) +Map get config => _$configJsonLiteral; diff --git a/lib/env/dev.g.dart b/lib/env/dev.g.dart new file mode 100644 index 0000000..2ab2ce9 --- /dev/null +++ b/lib/env/dev.g.dart @@ -0,0 +1,13 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'dev.dart'; + +// ************************************************************************** +// JsonLiteralGenerator +// ************************************************************************** + +const _$configJsonLiteral = { + 'env': 'DEV', + 'production': false, + 'apiUrl': 'https://dev.localspend.co.uk/api' +}; diff --git a/lib/env/dev.json b/lib/env/dev.json new file mode 100644 index 0000000..b2a9044 --- /dev/null +++ b/lib/env/dev.json @@ -0,0 +1,5 @@ +{ + "env": "DEV", + "production": false, + "apiUrl": "https://dev.localspend.co.uk/api" +} \ No newline at end of file diff --git a/lib/env/prod.dart b/lib/env/prod.dart new file mode 100644 index 0000000..cd369b3 --- /dev/null +++ b/lib/env/prod.dart @@ -0,0 +1,6 @@ +import 'package:json_annotation/json_annotation.dart'; + +part 'prod.g.dart'; + +@JsonLiteral('prod.json', asConst: true) +Map get config => _$configJsonLiteral; diff --git a/lib/env/prod.g.dart b/lib/env/prod.g.dart new file mode 100644 index 0000000..0d1854f --- /dev/null +++ b/lib/env/prod.g.dart @@ -0,0 +1,13 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'prod.dart'; + +// ************************************************************************** +// JsonLiteralGenerator +// ************************************************************************** + +const _$configJsonLiteral = { + 'env': 'PROD', + 'production': true, + 'apiUrl': 'https://www.localspend.co.uk/api' +}; diff --git a/lib/env/prod.json b/lib/env/prod.json new file mode 100644 index 0000000..fe2f989 --- /dev/null +++ b/lib/env/prod.json @@ -0,0 +1,5 @@ +{ + "env": "PROD", + "production": true, + "apiUrl": "https://www.localspend.co.uk/api" +} \ No newline at end of file diff --git a/lib/main.dart b/lib/main.dart index bd5398b..309d5ea 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,111 +1,50 @@ import 'package:flutter/material.dart'; +import 'package:local_spend/pages/home_page.dart'; +import 'package:local_spend/pages/login_page.dart'; +import 'package:local_spend/pages/map_page.dart'; +import 'package:local_spend/pages/receipt_page_2.dart'; +import 'package:local_spend/pages/spash_screen.dart'; +import 'package:local_spend/pages/more_page.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; +import 'package:local_spend/common/apifunctions/get_graph_data.dart'; -void main() => runApp(MyApp()); +void main() { + runApp(MyApp()); +} + +void loadGraphs() {} + +class GraphWithTitle { + GraphWithTitle({this.graph, this.title}); + + GraphData graph; + String title; +} class MyApp extends StatelessWidget { - // This widget is the root of your application. @override Widget build(BuildContext context) { - return MaterialApp( - title: 'Flutter Demo', - 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.grey, + // TODO: load graphs on app login and send to graph widgets + + return new MaterialApp( + debugShowCheckedModeBanner: false, + localizationsDelegates: [ + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + ], + supportedLocales: [Locale("en")], + title: "Local Spend Tracker", + theme: new ThemeData( + primarySwatch: Colors.blueGrey, ), - home: MyHomePage(title: 'Flutter Demo Home Page'), - ); - } -} - -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 { - int _counter = 0; - - void _incrementCounter() { - setState(() { - // This call to setState tells the Flutter framework that something has - // changed in this State, which causes it to rerun the build method below - // so that the display can reflect the updated values. If we changed - // _counter without calling setState(), then the build method would not be - // called again, and so nothing would appear to happen. - _counter++; - }); - } - - @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), - ), - body: Center( - // Center is a layout widget. It takes a single child and positions it - // in the middle of the parent. - child: Column( - // Column is also layout widget. It takes a list of children and - // arranges them vertically. By default, it sizes itself to fit its - // children horizontally, and tries to be as tall as its parent. - // - // Invoke "debug painting" (press "p" in the console, choose the - // "Toggle Debug Paint" action from the Flutter Inspector in Android - // Studio, or the "Toggle Debug Paint" command in Visual Studio Code) - // to see the wireframe for each widget. - // - // Column has various properties to control how it sizes itself and - // how it positions its children. Here we use mainAxisAlignment to - // center the children vertically; the main axis here is the vertical - // axis because Columns are vertical (the cross axis would be - // horizontal). - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - 'You have pushed the button this many times:', - ), - Text( - '$_counter', - style: Theme.of(context).textTheme.display1, - ), - ], - ), - ), - floatingActionButton: FloatingActionButton( - onPressed: _incrementCounter, - tooltip: 'Increment', - child: Icon(Icons.add), - ), // This trailing comma makes auto-formatting nicer for build methods. + routes: { + "/HomePage": (BuildContext context) => HomePage(), + "/LoginPage": (BuildContext context) => LoginPage(), + '/MapPage': (BuildContext context) => MapPage(), + "/ReceiptPage": (BuildContext context) => ReceiptPage2(), + "/MorePage": (BuildContext context) => MorePage(), + }, + home: SplashScreen(), ); } } diff --git a/lib/main_dev.dart b/lib/main_dev.dart new file mode 100644 index 0000000..289940e --- /dev/null +++ b/lib/main_dev.dart @@ -0,0 +1,7 @@ +import 'package:flutter/material.dart'; +import 'package:local_spend/config.dart'; +import 'package:local_spend/env/dev.dart'; +import 'package:local_spend/main.dart'; + +void main() => runApp( + new ConfigWrapper(config: Config.fromJson(config), child: new MyApp())); diff --git a/lib/model/json/login_model.dart b/lib/model/json/login_model.dart new file mode 100644 index 0000000..6fbfc20 --- /dev/null +++ b/lib/model/json/login_model.dart @@ -0,0 +1,18 @@ +class LoginModel { + LoginModel(this.userName, this.token, this.userType); + + LoginModel.fromJson(Map json) + : userName = json['display_name'], + userType = json['user_type'], + token = json['session_key']; + + final String userName; + final String token; + final String userType; + + Map toJson() => { + 'name': userName, + 'user_type': userType, + 'token': token, + }; +} diff --git a/lib/pages/customerGraphs.dart b/lib/pages/customerGraphs.dart new file mode 100644 index 0000000..341b399 --- /dev/null +++ b/lib/pages/customerGraphs.dart @@ -0,0 +1,163 @@ +import 'package:flutter/material.dart'; +import 'package:local_spend/common/apifunctions/get_graph_data.dart'; +import 'package:charts_flutter/flutter.dart' as charts; + +class CustomerGraphs extends StatefulWidget { + CustomerGraphs({Key key}) : super(key: key); + + @override + _CustomerGraphsState createState() { + return _CustomerGraphsState(); + } +} + +class _CustomerGraphsState extends State { + GraphData totalLastWeekGraph = new GraphData("total_last_week"); + GraphData avgSpendLastWeekGraph = new GraphData("avg_spend_last_week"); + GraphData totalLastMonthGraph = new GraphData("total_last_month"); + GraphData avgSpendLastMonth = new GraphData("avg_spend_last_month"); + + @override + void initState() { + super.initState(); + } + + @override + void dispose() { + super.dispose(); + } + + @override + Widget build(BuildContext context) { + // Initializing graphs: + if (!totalLastWeekGraph.loaded) { + totalLastWeekGraph.setGraphData().then((_) { + setState(() {}); + }); + } + + if (!avgSpendLastWeekGraph.loaded) { + avgSpendLastWeekGraph.setGraphData().then((_) { + setState(() {}); + }); + } + + if (!totalLastMonthGraph.loaded) { + totalLastMonthGraph.setGraphData().then((_) { + setState(() {}); + }); + } + + if (!avgSpendLastMonth.loaded) { + avgSpendLastMonth.setGraphData().then((_) { + setState(() {}); + }); + } + + return ListView( + children: [ + Container( + padding: EdgeInsets.fromLTRB(0.0, 17, 0.0, 0.0), + child: Text( + "Last Week's Total Spend", + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 22.0, + color: Colors.black, + fontWeight: FontWeight.bold, + ), + ), + ), + Tooltip( + message: "Graph of total spend last week", + child: Container( + padding: EdgeInsets.symmetric(horizontal: 10), + height: 200, + child: totalLastWeekGraph.graph != null + ? new charts.TimeSeriesChart(totalLastWeekGraph.graph) + : Center( + child: CircularProgressIndicator( + valueColor: + new AlwaysStoppedAnimation(Colors.orange), + )), //List>es> + ), + ), + Container( + padding: EdgeInsets.fromLTRB(0.0, 17, 0.0, 0.0), + child: Text( + "Last Week's Average Spend", + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 22.0, + color: Colors.black, + fontWeight: FontWeight.bold, + ), + ), + ), + Tooltip( + message: "Graph of average spend last week", + child: Container( + padding: EdgeInsets.symmetric(horizontal: 10), + height: 200, + child: avgSpendLastWeekGraph.graph != null + ? new charts.TimeSeriesChart(avgSpendLastWeekGraph.graph) + : Center( + child: CircularProgressIndicator( + valueColor: new AlwaysStoppedAnimation(Colors.blue), + )), //List>es> + ), + ), + Container( + padding: EdgeInsets.fromLTRB(0.0, 17, 0.0, 0.0), + child: Text( + "Last Month's Total Spend", + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 22.0, + color: Colors.black, + fontWeight: FontWeight.bold, + ), + ), + ), + Tooltip( + message: "Graph of total spend last month", + child: Container( + padding: EdgeInsets.symmetric(horizontal: 10), + height: 200, + child: totalLastMonthGraph.graph != null + ? new charts.TimeSeriesChart(totalLastMonthGraph.graph) + : Center( + child: CircularProgressIndicator( + valueColor: new AlwaysStoppedAnimation(Colors.green), + )), //List>es> + ), + ), + Container( + padding: EdgeInsets.fromLTRB(0.0, 17, 0.0, 0.0), + child: Text( + "Last Month's Average Spend", + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 22.0, + color: Colors.black, + fontWeight: FontWeight.bold, + ), + ), + ), + Tooltip( + message: "Graph of average spend last month", + child: Container( + padding: EdgeInsets.symmetric(horizontal: 10), + height: 200, + child: avgSpendLastMonth.graph != null + ? new charts.TimeSeriesChart(avgSpendLastMonth.graph) + : Center( + child: CircularProgressIndicator( + valueColor: new AlwaysStoppedAnimation(Colors.red), + )), //List>es> + ), + ), + ], + ); + } +} diff --git a/lib/pages/home_page.dart b/lib/pages/home_page.dart new file mode 100644 index 0000000..4e5ad30 --- /dev/null +++ b/lib/pages/home_page.dart @@ -0,0 +1,76 @@ +import 'package:flutter/material.dart'; +import 'package:local_spend/pages/receipt_page_2.dart'; +import 'package:local_spend/pages/more_page.dart'; +import 'package:local_spend/pages/stats_page.dart'; +import 'package:local_spend/pages/map_page.dart'; + +class HomePage extends StatelessWidget { + static String _title = 'SpendTracker'; + + @override + Widget build(BuildContext context) { + return MaterialApp( + title: _title, + home: HomePageWidget(), + ); + } +} + +class HomePageWidget extends StatefulWidget { + HomePageWidget({Key key}) : super(key: key); + + @override + _HomePageState createState() => _HomePageState(); +} + +class _HomePageState extends State { + int _selectedIndex = 0; + + static const TextStyle optionStyle = + TextStyle(fontSize: 30, fontWeight: FontWeight.bold); + static List _widgetOptions = [ + ReceiptPage2(), + StatsPage(), + MapPage(), + MorePage() + ]; + + void _onItemTapped(int index) { + setState(() { + _selectedIndex = index; + }); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Center( + child: _widgetOptions.elementAt(_selectedIndex), + ), + bottomNavigationBar: BottomNavigationBar( + items: const [ + BottomNavigationBarItem( + icon: Icon(Icons.receipt), + title: Text('Submit Receipt'), + ), + BottomNavigationBarItem( + icon: Icon(Icons.show_chart), + title: Text('Statistics'), + ), + BottomNavigationBarItem( + icon: Icon(Icons.map), + title: Text('Locations'), + ), + BottomNavigationBarItem( + icon: Icon(Icons.more_horiz), + title: Text('More'), + ), + ], + currentIndex: _selectedIndex, + unselectedItemColor: Colors.grey[400], + selectedItemColor: Colors.blue[400], + onTap: _onItemTapped, + ), + ); + } +} diff --git a/lib/pages/login_page.dart b/lib/pages/login_page.dart new file mode 100644 index 0000000..a4ebd90 --- /dev/null +++ b/lib/pages/login_page.dart @@ -0,0 +1,226 @@ +import 'dart:async'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:local_spend/common/apifunctions/request_login_api.dart'; +import 'package:local_spend/common/functions/show_dialog_single_button.dart'; +import 'package:local_spend/common/platform/platform_scaffold.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:url_launcher/url_launcher.dart'; +import 'package:local_spend/common/widgets/labeled_checkbox.dart'; +import 'package:local_spend/common/widgets/animatedGradientButton.dart'; + +const url = "https://flutter.io/"; + +class LoginPage extends StatefulWidget { + @override + State createState() { + return new LoginPageState(); + } +} + +class LoginPageState extends State { + bool _isLoggingIn = false; + final TextEditingController _emailController = TextEditingController(); + final TextEditingController _passwordController = TextEditingController(); + bool _saveLoginDetails = true; + + FocusNode focusNode; // added so focus can move automatically + + Future launchURL(String url) async { + if (await canLaunch(url)) { + await launch(url, forceSafariVC: true, forceWebView: true); + } else { + showDialogSingleButton( + context, + "Unable to reach your website.", + "Currently unable to reach the website $url. Please try again at a later time.", + "OK"); + } + } + + @override + void initState() { + super.initState(); + _saveCurrentRoute("/LoginPage"); + + focusNode = FocusNode(); + + _fillLoginDetails(); + } + + @override + void dispose() { + focusNode.dispose(); //disposes focus node when form disposed + super.dispose(); + } + + void _fillLoginDetails() async { + SharedPreferences preferences = await SharedPreferences.getInstance(); + + var username = await preferences.get('username'); + var password = await preferences.get('password'); + + _emailController.text = await username; + _passwordController.text = await password; + } + + void _saveCurrentRoute(String lastRoute) async { + SharedPreferences preferences = await SharedPreferences.getInstance(); + await preferences.setString('LastPageRoute', lastRoute); + } + + void login(String username, String password) async { + _isLoggingIn = true; + await SystemChannels.textInput.invokeMethod('TextInput.hide'); + SharedPreferences preferences = await SharedPreferences.getInstance(); + + if (_saveLoginDetails) { + await preferences.setString('username', username); + await preferences.setString('password', password); + } else { + await preferences.setString('username', ""); + await preferences.setString('password', ""); + } + + await requestLoginAPI(context, username, password).then((value) { + _isLoggingIn = false; + }); + } + + @override + Widget build(BuildContext context) { + return WillPopScope( + onWillPop: () { + if (Navigator.canPop(context)) { + Navigator.of(context).pushNamedAndRemoveUntil( + '/HomePage', (Route route) => false); + } else { + Navigator.of(context).pushReplacementNamed('/HomePage'); + } + return null; + }, + child: PlatformScaffold( + body: Stack( + children: [ + AnimatedBackground([Colors.lightBlue[50], Colors.lightBlue[50]], + Colors.white, Alignment.topRight, Alignment.bottomLeft, 3), + Container( + margin: EdgeInsets.fromLTRB(60, 30, 60, 0), + child: Column( + children: [ + Expanded( + child: AnimatedContainer( + duration: Duration(seconds: 2), + margin: EdgeInsets.fromLTRB(15, 0, 15, 0), + decoration: BoxDecoration( + image: DecorationImage( + image: + AssetImage('assets/images/launch_image.png')), + ), + ), + ), + Padding( + padding: EdgeInsets.fromLTRB(0.0, 0.0, 0.0, 0.0), + child: TextField( + autocorrect: false, + textAlign: TextAlign.center, + controller: _emailController, + decoration: InputDecoration( + hintText: "EMAIL", + hintStyle: TextStyle(fontSize: 15), + ), + style: TextStyle( + fontSize: 18.0, + color: Colors.grey[800], + fontWeight: FontWeight.bold, + ), + onSubmitted: (_) { + FocusScope.of(context).requestFocus(focusNode); + }, + ), + ), + Padding( + padding: EdgeInsets.fromLTRB(0.0, 15.0, 0.0, 0.0), + child: TextField( + autocorrect: false, + textAlign: TextAlign.center, + controller: _passwordController, + focusNode: focusNode, + decoration: InputDecoration( + hintText: 'PASSWORD', + hintStyle: TextStyle(fontSize: 15), + ), + obscureText: true, + style: TextStyle( + fontSize: 18.0, + color: Colors.grey[800], + fontWeight: FontWeight.bold, + ), + onSubmitted: (_) { + login(_emailController.text, _passwordController.text); + }, + ), + ), + Container( + margin: EdgeInsets.fromLTRB(0.0, 40.0, 0.0, 30.0), + width: 100, + height: 50, + child: Opacity( + opacity: _isLoggingIn ? 0.5 : 1, + child: ClipRRect( + borderRadius: BorderRadius.circular(2), + child: Stack( + children: [ + AnimatedBackground( + [Colors.blue, Colors.lightBlue[300]], + Colors.lightBlue, + Alignment.bottomRight, + Alignment.topLeft, + 3), + Material( + type: MaterialType.transparency, + child: InkWell( + onTap: _isLoggingIn + ? null + : () => login(_emailController.text, + _passwordController.text), + child: new Center( + child: new Text( + 'GO', + style: new TextStyle( + fontSize: 18, color: Colors.white), + ), + ), + ), + ), + ], + ), + ), + ), + ), + Padding( + padding: EdgeInsets.fromLTRB(0, 10, 0, 50), + child: LabeledCheckbox( + label: "SAVE LOGIN", + textStyle: TextStyle( + fontSize: 18, + color: Colors.black54, + fontWeight: FontWeight.bold), + padding: const EdgeInsets.fromLTRB(0, 0, 0, 0), + value: _saveLoginDetails, + onChanged: (bool newValue) { + setState(() { + _saveLoginDetails = newValue; + }); + }, + ), + ), + ], + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/pages/map_page.dart b/lib/pages/map_page.dart new file mode 100644 index 0000000..f2c36ba --- /dev/null +++ b/lib/pages/map_page.dart @@ -0,0 +1,62 @@ +import 'package:flutter/material.dart'; +import 'dart:async'; +import 'package:google_maps_flutter/google_maps_flutter.dart' as gmaps; +import 'package:local_spend/common/apifunctions/get_map_data.dart' as mapData; +import 'package:local_spend/common/platform/platform_scaffold.dart'; + +class MapPage extends StatefulWidget { + MapPage({Key key}) : super(key: key); + + @override + _MapPageState createState() { + return _MapPageState(); + } +} + +class _MapPageState extends State { + final Map _markers = {}; + Future _onMapCreated(gmaps.GoogleMapController controller) async { + final region = await controller.getVisibleRegion(); + final locations = await mapData.getLocations(region.northeast, region.southwest); + setState(() { + _markers.clear(); + for (final location in locations.locations) { + final marker = gmaps.Marker( + markerId: gmaps.MarkerId(location.organisation.name), + position: gmaps.LatLng(location.lat, location.lng), + infoWindow: gmaps.InfoWindow( + title: location.organisation.name, + snippet: location.organisation.postcode, + ), + ); + _markers[location.organisation.name] = marker; + } + }); + } + @override + Widget build(BuildContext context) { + return PlatformScaffold( + appBar: AppBar( + backgroundColor: Colors.blue[400], + title: Text( + "Map", + style: TextStyle( + fontSize: 20, + color: Colors.white, + ), + ), + centerTitle: true, + iconTheme: IconThemeData(color: Colors.black), + ), + body: gmaps.GoogleMap( + myLocationButtonEnabled: false, + mapType: gmaps.MapType.hybrid, + onMapCreated: _onMapCreated, + initialCameraPosition: gmaps.CameraPosition( + target: gmaps.LatLng(54.0411301, -2.8104042), + zoom: 15, + ), + ), + ); + } +} diff --git a/lib/pages/more_page.dart b/lib/pages/more_page.dart new file mode 100644 index 0000000..1619faa --- /dev/null +++ b/lib/pages/more_page.dart @@ -0,0 +1,191 @@ +import 'package:flutter/material.dart'; +import 'package:local_spend/common/platform/platform_scaffold.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:local_spend/common/functions/logout.dart'; +import 'package:url_launcher/url_launcher.dart'; +import 'package:local_spend/common/functions/customAbout.dart' as custom; +import 'package:local_spend/common/functions/showDialogTwoButtons.dart'; + +const url = "https://flutter.io/"; +const demonstration = false; + +class MorePage extends StatefulWidget { + @override + State createState() { + return new MorePageState(); + } +} + +class MorePageState extends State { + FocusNode focusNode; // added so focus can move automatically + + DateTime date; + + @override + void initState() { + super.initState(); + _saveCurrentRoute("/MorePageState"); + + focusNode = FocusNode(); + } + + @override + void dispose() { + super.dispose(); + + focusNode.dispose(); //disposes focus node when form disposed + } + + void _saveCurrentRoute(String lastRoute) async { + SharedPreferences preferences = await SharedPreferences.getInstance(); + await preferences.setString('LastPageRoute', lastRoute); + } + + @override + Widget build(BuildContext context) { + return WillPopScope( + onWillPop: () { + if (Navigator.canPop(context)) { + Navigator.of(context).pushNamedAndRemoveUntil( + '/LoginPage', (Route route) => false); + } else { + Navigator.of(context).pushReplacementNamed('/LoginPage'); + } + return null; + }, + child: PlatformScaffold( + appBar: AppBar( + backgroundColor: Colors.blue[400], + title: Text( + "More", + style: TextStyle( + fontSize: 20, + color: Colors.white, + ), + ), +// leading: BackButton(), + centerTitle: true, + iconTheme: IconThemeData(color: Colors.black), + ), + body: Container( + child: ListView( + children: [ + Container( + padding: EdgeInsets.fromLTRB(30.0, 25, 30.0, 0.0), + child: Text( + "Local Spend Tracker", + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 22.0, + color: Colors.black, + fontWeight: FontWeight.bold, + ), + ), + ), + + Padding( + padding: EdgeInsets.fromLTRB(30.0, 25.0, 30.0, 0.0), + child: Container( + height: 65.0, + child: RaisedButton( + onPressed: () { + custom.showAboutDialog( + context: context, + applicationIcon: new Icon(Icons.receipt), + applicationName: "Local Spend Tracker", + children: [ + Text("Pear Trading is a commerce company designed to register and monitor money circulating in the local economy.\n"), + Container( + margin: EdgeInsets.symmetric(horizontal: 10), + height: 35, + child: RaisedButton( + onPressed: () => launch('http://www.peartrade.org'), + child: Text("Pear Trading", + style: TextStyle( + color: Colors.white, fontSize: 18.0)), + color: Colors.green, + ), + ), + + Container( + margin: EdgeInsets.fromLTRB(10.0, 10.0, 10.0, 0.0), + height: 40.0, + child: Material( + color: Colors.transparent, + child: InkWell( + borderRadius: BorderRadius.circular(3), + onTap: () => launch('https://shadow.cat'), + child: Column( + children: [ + Align( + child: Text("Developed by"), + alignment: Alignment.centerLeft), + Container( + margin: EdgeInsets.all(0), + child : Text( + "Shadowcat Systems", + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold + ), + ), + ), + ], + ), + ), + ), + ), + + ], + ); + }, + child: Text("ABOUT", + style: TextStyle(color: Colors.white, fontSize: 22.0)), + color: Colors.blue, + ), + ), + ), + + Padding( + padding: EdgeInsets.fromLTRB(30.0, 20.0, 30.0, 0.0), + child: Container( + height: 65.0, + child: RaisedButton( + onPressed: () { + showDialogTwoButtons( + context, + "Logout", + "Are you sure you want to log out?", + "Cancel", + "Logout", + logout); + }, + child: Text("LOGOUT", + style: TextStyle(color: Colors.white, fontSize: 22.0)), + color: Colors.red, + ), + ), + ), + +// Padding( +// padding: EdgeInsets.fromLTRB(30.0, 20.0, 30.0, 0.0), +// child: Container( +// height: 65.0, +// child: RaisedButton( +// onPressed: () { +// feedback(context); +// }, +// child: Text("FEEDBACK", +// style: +// TextStyle(color: Colors.white, fontSize: 22.0)), +// color: Colors.green, +// ), +// ), +// ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/pages/new_stats_page.dart b/lib/pages/new_stats_page.dart new file mode 100644 index 0000000..4dca8a4 --- /dev/null +++ b/lib/pages/new_stats_page.dart @@ -0,0 +1,57 @@ +import 'package:flutter/material.dart'; +import 'package:local_spend/common/platform/platform_scaffold.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +const url = "https://flutter.io/"; +const demonstration = false; + +class NewStatsPage extends StatefulWidget { + @override + State createState() { + return new NewStatsPageState(); + } +} + +class NewStatsPageState extends State { + /// Graph types: + /// - total_last_week + /// - avg_spend_last_week + /// - total_last_month + /// - avg_spend_last_month + + @override + void initState() { + super.initState(); + _saveCurrentRoute("/StatsPageState"); + } + + @override + void dispose() { + super.dispose(); + } + + void _saveCurrentRoute(String lastRoute) async { + SharedPreferences preferences = await SharedPreferences.getInstance(); + await preferences.setString('LastPageRoute', lastRoute); + } + + @override + Widget build(BuildContext context) { + return PlatformScaffold( + appBar: AppBar( + backgroundColor: Colors.blue[400], + title: Text( + "Statistics", + style: TextStyle( + fontSize: 20, + color: Colors.white, + ), + ), +// leading: BackButton(), + centerTitle: true, + iconTheme: IconThemeData(color: Colors.black), + ), + body: Container(), + ); + } +} diff --git a/lib/pages/orgGraphs.dart b/lib/pages/orgGraphs.dart new file mode 100644 index 0000000..444f3ce --- /dev/null +++ b/lib/pages/orgGraphs.dart @@ -0,0 +1,178 @@ +import 'package:flutter/material.dart'; +import 'package:local_spend/common/apifunctions/get_graph_data.dart'; +import 'package:charts_flutter/flutter.dart' as charts; + +class OrgGraphs extends StatefulWidget { + OrgGraphs({Key key}) : super(key: key); + + @override + _OrgGraphsState createState() { + return _OrgGraphsState(); + } +} + +class _OrgGraphsState extends State { + /// Organisations' graphs types: to fetch, POST to https://dev.localspend.co.uk/api/stats/[graph_type] as {"session_key":"[boop beep]"} + /// - organisations_all : organisation + /// - pies : organisation/pies + /// - snippets : organisation/snippets + /// - graphs : organisation/graphs + /// - {"graph":"customers_last_7_days","session_key":"[bleep]"} + /// - {"graph":"customers_last_30_days","session_key":"[blah]"} + /// - {"graph":"sales_last_7_days","session_key":"[bloop]"} + /// - {"graph":"sales_last_7_days","session_key":"[reee]"} + /// - {"graph":"purchases_last_7_days","session_key":"[yee]"} + /// - {"graph":"purchases_last_30_days","session_key":"[yah]"} + /// - {"graph":"purchases_all;","session_key":"[kappa]"} // I don't think this one works + /// + /// HTTP POST request sample: + /// {"graph":"total_last_week","session_key":"blahblahblah"} + +// OrganisationGraph customersLastWeek = new OrganisationGraph("graphs", graphsType: "customers_last_7_days"); + OrganisationGraph customersLastMonth = + new OrganisationGraph("graphs", graphsType: "customers_last_30_days"); + OrganisationGraph salesLastMonth = + new OrganisationGraph("graphs", graphsType: "sales_last_30_days"); + OrganisationGraph purchasesLastMonth = new OrganisationGraph("graphs", + graphsType: "purchases_last_30_days"); //purchases_last_30_days + + @override + void initState() { + super.initState(); + } + + @override + void dispose() { + super.dispose(); + } + + @override + Widget build(BuildContext context) { +// if (!customersLastWeek.loaded) { +// customersLastWeek.getGraphData().then((_) { +// setState(() {}); +// }); +// } + if (!customersLastMonth.loaded) { + customersLastMonth.getGraphData().then((_) { + setState(() {}); + }); + } + if (!salesLastMonth.loaded) { + salesLastMonth.getGraphData().then((_) { + setState(() {}); + }); + } + if (!purchasesLastMonth.loaded) { + purchasesLastMonth.getGraphData().then((_) { + setState(() {}); + }); + } + + return ListView( + children: [ +// Container( +// padding: EdgeInsets.fromLTRB(0.0, 17, 0.0, 0.0), +// child: Text( +// "Last Week's Customers", +// textAlign: TextAlign.center, +// style: TextStyle( +// fontSize: 22.0, +// color: Colors.black, +// fontWeight: FontWeight.bold, +// ), +// ), +// ), +// Tooltip( +// message: "Graph of customers last week", +// child: Container( +// padding: EdgeInsets.symmetric(horizontal: 10), +// height: 200, +// child: customersLastWeek.graph != null +// ? new charts.TimeSeriesChart(customersLastWeek.graph) +// : Center( +// child: Text( +// "Loading graph...")), //List> +// ), +// ), + + Container( + padding: EdgeInsets.fromLTRB(0.0, 17, 0.0, 0.0), + child: Text( + "This Month's Customers", + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 22.0, + color: Colors.black, + fontWeight: FontWeight.bold, + ), + ), + ), + Tooltip( + message: "Customers this month", // this needs to be better + child: Container( + padding: EdgeInsets.symmetric(horizontal: 10), + height: 200, + child: customersLastMonth.graph != null + ? new charts.TimeSeriesChart(customersLastMonth.graph) + : Center( + child: + CircularProgressIndicator()), //List> + ), + ), + + Container( + padding: EdgeInsets.fromLTRB(0.0, 17, 0.0, 0.0), + child: Text( + "This Month's Revenue", + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 22.0, + color: Colors.black, + fontWeight: FontWeight.bold, + ), + ), + ), + Tooltip( + message: "Revenue from sales this month", + child: Container( + padding: EdgeInsets.symmetric(horizontal: 10), + height: 200, + child: salesLastMonth.graph != null + ? new charts.TimeSeriesChart(salesLastMonth.graph) + : Center( + child: CircularProgressIndicator( + valueColor: new AlwaysStoppedAnimation(Colors.green), + )), //List> + ), + ), + + Container( + padding: EdgeInsets.fromLTRB(0.0, 17, 0.0, 0.0), + child: Text( + "This Month's Sales", + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 22.0, + color: Colors.black, + fontWeight: FontWeight.bold, + ), + ), + ), + Tooltip( + message: "Number of sales this month", + child: Container( + padding: EdgeInsets.symmetric(horizontal: 10), + height: 200, + child: purchasesLastMonth.graph != null + ? new charts.TimeSeriesChart(purchasesLastMonth.graph) + : Center( + child: CircularProgressIndicator( + valueColor: new AlwaysStoppedAnimation(Colors.red), + )), //List> + ), + ), + ], + ); + } +} diff --git a/lib/pages/receipt_page_2.dart b/lib/pages/receipt_page_2.dart new file mode 100644 index 0000000..4456016 --- /dev/null +++ b/lib/pages/receipt_page_2.dart @@ -0,0 +1,606 @@ +import 'package:flutter/material.dart'; +import 'package:local_spend/common/platform/platform_scaffold.dart'; +import 'package:intl/intl.dart'; +import 'dart:async'; +import 'package:flutter/cupertino.dart'; +import 'package:local_spend/common/widgets/animatedGradientButton.dart'; +import 'package:local_spend/common/apifunctions/find_organisations.dart'; +import 'package:local_spend/common/widgets/organisations_dialog.dart'; +import 'package:local_spend/common/apifunctions/submit_receipt_api.dart'; +import 'package:local_spend/common/apifunctions/categories.dart'; + +class Transaction { + Transaction( + this.date, + this.amount, + this.organisation, + this.recurring, + this.isEssential, + this.category, + ); + + DateTime date; + TextEditingController amount; + Organisation organisation; + String recurring; + bool isEssential; + String category; +} + +class ReceiptPage2 extends StatefulWidget { + @override + State createState() { + return new ReceiptPage2State(); + } +} + +class ReceiptPage2State extends State { + Transaction transaction = new Transaction( + DateTime.now(), + new TextEditingController(), + new Organisation(null, null, null, null, null), + "None", + false, + "Uncategorised", + ); + + AlertDialog _invalidDialog(context) { + return AlertDialog( + title: new Text("Invalid data"), + content: new Text( + "We couldn't process your request because some of the data entered is invalid."), + actions: [ + new FlatButton( + child: new Text("OK"), + onPressed: () { + Navigator.of(context).pop(); + }, + ), + ], + ); + } + + Future> getCats() async { + return await getCategories(); + } + + void _submitReceipt(Transaction transaction) { + Receipt receipt = new Receipt(); + receipt.organisationName = transaction.organisation.name; + receipt.street = transaction.organisation.streetName; + receipt.postcode = transaction.organisation.postcode; + receipt.town = transaction.organisation.town; + + receipt.recurring = transaction.recurring; + + if (transaction.recurring == "None") { + receipt.recurring = ""; + } + + receipt.category = transaction.category; + + if (transaction.category == "Uncategorised") { + receipt.category = ""; + } + + receipt.amount = transaction.amount.text.toString(); + receipt.time = DateFormat("yyyy-MM-dd'T'hh:mm':00.000+01:00'") + .format(transaction.date) + .toString(); + receipt.essential = transaction.isEssential.toString(); + + submitReceiptAPI(context, receipt); + } + + List _recurringOptions = new List(7); + List _categories = new List(); + + @override + void initState() { + super.initState(); + } + + @override + Widget build(BuildContext context) { + var _widgetHeight = MediaQuery.of(context).size.height * 0.06 < 40.0 + ? 40.0 + : MediaQuery.of(context).size.height * 0.06; + + var _fontSize = _widgetHeight * 0.45; + var _fontSizeButton = _widgetHeight * 0.5; + + if (_categories.isEmpty) { + Future> _futureCats = getCats(); + _categories.add("Fetching categories..."); + _futureCats.then((value) { + _categories = value; + _categories.insert(0, "Uncategorised"); + setState(() {}); + }); + } + + _recurringOptions[0] = "None"; + _recurringOptions[1] = "Daily"; + _recurringOptions[2] = "Weekly"; + _recurringOptions[3] = "Fortnightly"; + _recurringOptions[4] = "Monthly"; + _recurringOptions[5] = "Quarterly"; + _recurringOptions[6] = "Yearly"; + // these will be difficult to fetch from server as they are coded into the site's HTML rather than fetched + + return PlatformScaffold( + appBar: AppBar( + backgroundColor: Colors.blue[400], + title: Text( + "Submit Receipt", + style: TextStyle( + fontSize: 20, + color: Colors.white, + ), + ), + centerTitle: true, + iconTheme: IconThemeData(color: Colors.black), + ), + body: ListView( + children: [ + // each CHILD has its own horizontal padding because if the listView + // has padding, Android's end-of-scroll animation + // doesn't fit the screen properly and looks weird + + Container( + padding: EdgeInsets.fromLTRB( + MediaQuery.of(context).size.width * 0.025, + MediaQuery.of(context).size.height * 0.025, + 0, + 0.0), + child: Text( + "Receipt Details", + style: TextStyle( + fontSize: 26, + color: Colors.grey[700], + fontWeight: FontWeight.bold, + ), + ), + ), // "Receipt Details" title + + Container( + padding: EdgeInsets.fromLTRB( + MediaQuery.of(context).size.width * 0.05, + 15, + MediaQuery.of(context).size.width * 0.05, + 0.0), + child: Tooltip( + message: "Date and time of transaction", + child: Row( + children: [ + Container( + child: Text( + "Date/Time", + style: TextStyle( + fontSize: _fontSize, + color: Colors.black, + fontWeight: FontWeight.bold, + ), + ), + width: MediaQuery.of(context).size.width * 0.3, + ), + Container( + padding: const EdgeInsets.fromLTRB(0, 0, 0, 0), + height: _widgetHeight, + width: MediaQuery.of(context).size.width * 0.6, + child: RaisedButton( + onPressed: () { + showModalBottomSheet( + context: context, + builder: (BuildContext builder) { + return Container( + height: MediaQuery.of(context) + .copyWith() + .size + .height / + 3, + child: CupertinoDatePicker( + initialDateTime: + transaction.date.isAfter(DateTime.now()) + ? DateTime.now() + : transaction.date, + onDateTimeChanged: (DateTime newDate) { + setState(() => { + newDate.isAfter(DateTime.now()) + ? transaction.date = + DateTime.now() + : transaction.date = newDate, + }); + }, + use24hFormat: true, + maximumDate: DateTime.now(), + ), + ); + }); + }, + child: Text( + transaction.date == null + ? 'None set.' + : transaction.date.year != DateTime.now().year + ? '${new DateFormat.MMMd().format(transaction.date)}' + + " " + + transaction.date.year.toString() + + " at " + + '${new DateFormat.Hm().format(transaction.date)}' + : transaction.date.day == DateTime.now().day && + transaction.date.month == + DateTime.now().month + ? "Today at " + + '${new DateFormat.Hm().format(transaction.date)}' + : '${new DateFormat.MMMd().format(transaction.date)}' + + " at " + + '${new DateFormat.Hm().format(transaction.date)}', + style: TextStyle( + color: Colors.white, fontSize: _fontSizeButton), + ), + color: Colors.blue, + ), + ), + ], + ), + ), + ), // Date/Time picker + + Container( + padding: EdgeInsets.fromLTRB( + MediaQuery.of(context).size.width * 0.05, + 15, + MediaQuery.of(context).size.width * 0.05, + 0.0), + child: Tooltip( + message: "Transaction payee", + child: Row( + children: [ + Container( + child: Text( + "Payee", + style: TextStyle( + fontSize: _fontSize, + color: Colors.black, + fontWeight: FontWeight.bold, + ), + ), + width: MediaQuery.of(context).size.width * 0.3, + ), + Container( + padding: const EdgeInsets.fromLTRB(0, 0, 0, 0), + height: _widgetHeight, + width: MediaQuery.of(context).size.width * 0.6, + child: RaisedButton( + onPressed: () { + var organisations = new FindOrganisations(); + var orgDialog = organisations.dialog(context); + orgDialog.then((organisation) { + try { + organisation.name.length; + transaction.organisation = organisation; + // debugPrint(organisation.name); + setState(() {}); + } catch (_) { + debugPrint("No organisation chosen."); + } + }); + }, + child: Text( + transaction.organisation.name == null + ? 'Find' + : transaction.organisation.name.length > 14 + ? transaction.organisation.name + .substring(0, 12) + + "..." + : transaction.organisation.name, + style: TextStyle( + color: Colors.white, fontSize: _fontSizeButton), + ), + color: Colors.blue, + ), + ), + ], + ), + ), + ), // Organisation picker + + Container( + padding: EdgeInsets.fromLTRB( + MediaQuery.of(context).size.width * 0.05, + 15, + MediaQuery.of(context).size.width * 0.05, + 0.0), + child: Tooltip( + message: "Repeating?", + child: Row( + children: [ + Container( + child: Text( + "Recurring", + style: TextStyle( + fontSize: _fontSize, + color: Colors.black, + fontWeight: FontWeight.bold, + ), + ), + width: MediaQuery.of(context).size.width * 0.3, + ), + Container( + padding: const EdgeInsets.fromLTRB(0, 0, 0, 0), + height: _widgetHeight, + width: MediaQuery.of(context).size.width * 0.6, + child: RaisedButton( + onPressed: () { + transaction.recurring = _recurringOptions[0]; + setState(() {}); + showModalBottomSheet( + context: context, + builder: (BuildContext builder) { + return Container( + height: MediaQuery.of(context) + .copyWith() + .size + .height / + 3, + child: CupertinoPicker( + backgroundColor: Colors.white, + children: _recurringOptions + .map((thisOption) => Text(thisOption, + style: TextStyle(fontSize: 30))) + .toList(), + onSelectedItemChanged: ((newValue) { + transaction.recurring = + _recurringOptions[newValue]; + setState(() {}); + }), + magnification: 1.1, + useMagnifier: true, + itemExtent: 36, + ), + ); + }); + }, + child: Text( + transaction.recurring == null + ? 'None' + : transaction.recurring, + style: TextStyle( + color: Colors.white, fontSize: _fontSizeButton), + ), + color: Colors.blue, + ), + ), + ], + ), + ), + ), // Recurring picker + + Container( + padding: EdgeInsets.fromLTRB( + MediaQuery.of(context).size.width * 0.05, + 15, + MediaQuery.of(context).size.width * 0.05, + 0.0), + child: Row( + children: [ + Container( + child: Text( + "Category", + style: TextStyle( + fontSize: _fontSize, + color: Colors.black, + fontWeight: FontWeight.bold, + ), + ), + width: MediaQuery.of(context).size.width * 0.3, + ), + Container( + padding: const EdgeInsets.fromLTRB(0, 0, 0, 0), + height: _widgetHeight, + width: MediaQuery.of(context).size.width * 0.6, + child: Tooltip( + message: "Category of transaction", + child: RaisedButton( + onPressed: () { + transaction.category = _categories[0]; + setState(() {}); + showModalBottomSheet( + context: context, + builder: (BuildContext builder) { + return Container( + height: MediaQuery.of(context) + .copyWith() + .size + .height / + 3, + child: CupertinoPicker( + backgroundColor: Colors.white, + children: _categories + .map((thisOption) => Text( + thisOption, + style: TextStyle(fontSize: 30), + )) + .toList(), + onSelectedItemChanged: ((newValue) { + transaction.category = + _categories[newValue]; + setState(() {}); + }), + magnification: 1.1, + useMagnifier: true, + itemExtent: 36, + ), + ); + }); + }, + child: Text( + transaction.category == null + ? 'None' + : transaction.category, + style: TextStyle( + color: Colors.white, fontSize: _fontSizeButton), + ), + color: Colors.blue, + ), + ), + ), + ], + ), + ), // Category picker + + Container( + padding: EdgeInsets.fromLTRB( + MediaQuery.of(context).size.width * 0.05, + 15, + MediaQuery.of(context).size.width * 0.05, + 0.0), + child: Tooltip( + message: "Essential or not", + child: Row( + children: [ + Container( + child: Text( + "Essential", + style: TextStyle( + fontSize: _fontSize, + color: Colors.black, + fontWeight: FontWeight.bold, + ), + ), + width: MediaQuery.of(context).size.width * 0.3, + ), + Container( + height: _widgetHeight, + width: MediaQuery.of(context).size.width * 0.6, + child: Checkbox( + value: transaction.isEssential, + onChanged: ((value) { + setState(() => transaction.isEssential = value); + }), + ), + ), + ], + ), + ), + ), // Essential + + Container( + padding: EdgeInsets.fromLTRB( + MediaQuery.of(context).size.width * 0.05, + 15, + MediaQuery.of(context).size.width * 0.05, + 0.0), + child: Tooltip( + message: "Transaction amount", + child: Row( + children: [ + Container( + child: Text( + "Amount", + style: TextStyle( + fontSize: _fontSize, + color: Colors.black, + fontWeight: FontWeight.bold, + ), + ), + width: MediaQuery.of(context).size.width * 0.3, + ), + Container( + padding: const EdgeInsets.fromLTRB(0, 0, 0, 0), + height: _widgetHeight, + width: MediaQuery.of(context).size.width * 0.6, + child: TextField( + style: TextStyle( + fontSize: _fontSize, + ), + textAlign: TextAlign.center, + controller: transaction.amount, + decoration: InputDecoration(hintText: "0.00"), + keyboardType: TextInputType.numberWithOptions( + decimal: true, signed: true), + ), + ), + ], + ), + ), + ), // Amount picker + + Padding( + padding: EdgeInsets.fromLTRB( + MediaQuery.of(context).size.width * 0.05, + MediaQuery.of(context).size.height * 0.03, + MediaQuery.of(context).size.width * 0.05, + 15.0), + child: Tooltip( + message: "Submit receipt", + child: Container( + height: _widgetHeight * 1.7, + child: ClipRRect( + borderRadius: BorderRadius.circular(2), + child: Opacity( + opacity: 1, + child: Stack( + children: [ + AnimatedBackground( + [Colors.blue, Colors.lightBlue[300]], + Colors.lightBlue, + Alignment.topLeft, + Alignment.bottomRight, + 4), + Material( + type: MaterialType.transparency, + child: InkWell( + child: Center( + child: Text( + "GO", + style: TextStyle( + color: Colors.white, fontSize: 30.0), + ), + ), + onTap: () { + try { + if (transaction.amount.text == "" || + transaction.organisation.name == null) { + showDialog( + context: context, + builder: (BuildContext context) { + return _invalidDialog(context); + }); + } else { + if (double.tryParse( + transaction.amount.text) != + null && + double.tryParse(transaction.amount.text) > + 0) { + _submitReceipt(transaction); + } else { + showDialog( + context: context, + builder: (BuildContext context) { + return _invalidDialog(context); + }); + } + } + } catch (_) { + showDialog( + context: context, + builder: (BuildContext context) { + return _invalidDialog(context); + }); + } + }, + ), + ), + ], + ), + ), + ), + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/pages/spash_screen.dart b/lib/pages/spash_screen.dart new file mode 100644 index 0000000..26df3db --- /dev/null +++ b/lib/pages/spash_screen.dart @@ -0,0 +1,58 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:local_spend/common/platform/platform_scaffold.dart'; + +class SplashScreen extends StatefulWidget { + @override + _SplashScreenState createState() => _SplashScreenState(); +} + +class _SplashScreenState extends State { + final int splashDuration = 1; + + Future startTime() async { + return Timer(Duration(seconds: splashDuration), () { + SystemChannels.textInput.invokeMethod('TextInput.hide'); + Navigator.of(context).pushReplacementNamed('/LoginPage'); + }); + } + + @override + void initState() { + super.initState(); + startTime(); + } + + @override + Widget build(BuildContext context) { + return PlatformScaffold( + body: Container( + decoration: BoxDecoration(color: Colors.white), + child: Column( + children: [ + Expanded( + child: Container( + margin: EdgeInsets.all(15), + decoration: BoxDecoration( + image: DecorationImage( + image: AssetImage('assets/images/launch_image.png'), + ), + ), + ), + ), + Container( + margin: EdgeInsets.fromLTRB(0.0, 0.0, 0.0, 30.0), + child: Text( + "© Copyright Pear Trading 2019", + style: TextStyle( + fontSize: 16.0, + color: Colors.black, + ), + ), + ), + ], + ))); + } +} diff --git a/lib/pages/stats_page.dart b/lib/pages/stats_page.dart new file mode 100644 index 0000000..2446884 --- /dev/null +++ b/lib/pages/stats_page.dart @@ -0,0 +1,93 @@ +import 'package:flutter/material.dart'; +import 'package:local_spend/common/platform/platform_scaffold.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:local_spend/pages/customerGraphs.dart'; +import 'package:local_spend/pages/orgGraphs.dart'; + +const url = "https://flutter.io/"; +const demonstration = false; + +class StatsPage extends StatefulWidget { + @override + State createState() { + print( + "TODO: The 'stats' page should be loaded on login and cached rather than reloading on every opening of the page."); + print( + "Create new List in instantiated MyApp() and pass that or load it from this class' child with (graphs = super.graphList) or something."); + return new StatsPageState(); + } +} + +class StatsPageState extends State { + String userType = "-"; + + @override + void initState() { + super.initState(); + _saveCurrentRoute("/StatsPageState"); + } + + @override + void dispose() { + super.dispose(); + } + + void _saveCurrentRoute(String lastRoute) async { + SharedPreferences preferences = await SharedPreferences.getInstance(); + await preferences.setString('LastPageRoute', lastRoute); + } + + Future _getUserType() async { + SharedPreferences preferences = await SharedPreferences.getInstance(); + return await preferences.get('LastUserType'); + } + + @override + Widget build(BuildContext context) { + if (userType == "-") { + _getUserType().then((value) { + print(value); + userType = + '${value[0].toUpperCase()}${value.substring(1)}'; // capitalises first letter + setState(() {}); + }); + } + + return PlatformScaffold( + appBar: AppBar( + backgroundColor: Colors.blue[400], + title: Row( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + "Statistics", + style: TextStyle( + fontSize: 20, + color: Colors.white, + ), + ), + Padding(padding: EdgeInsets.symmetric(horizontal: 4)), + Text( + userType, + style: TextStyle( + fontSize: 20, + color: Colors.white70, + ), + ), + ], + ), + centerTitle: true, + iconTheme: IconThemeData(color: Colors.black), + ), + body: Container( + padding: EdgeInsets.fromLTRB(0, 0, 0, 0), + child: (userType == "-" + ? null + : (userType.toLowerCase() == "customer" + ? CustomerGraphs() + : OrgGraphs())), + ), + ); + } +} diff --git a/pubspec.lock b/pubspec.lock index 7eebb6b..0b7ae06 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1,133 +1,696 @@ # Generated by pub -# See https://www.dartlang.org/tools/pub/glossary#lockfile +# See https://dart.dev/tools/pub/glossary#lockfile packages: + _fe_analyzer_shared: + dependency: transitive + description: + name: _fe_analyzer_shared + url: "https://pub.dartlang.org" + source: hosted + version: "6.0.0" + analyzer: + dependency: transitive + description: + name: analyzer + url: "https://pub.dartlang.org" + source: hosted + version: "0.39.15" + args: + dependency: transitive + description: + name: args + url: "https://pub.dartlang.org" + source: hosted + version: "1.6.0" async: dependency: transitive description: name: async url: "https://pub.dartlang.org" source: hosted - version: "2.0.8" + version: "2.4.2" boolean_selector: dependency: transitive description: name: boolean_selector url: "https://pub.dartlang.org" source: hosted - version: "1.0.4" + version: "2.0.0" + build: + dependency: transitive + description: + name: build + url: "https://pub.dartlang.org" + source: hosted + version: "1.3.0" + build_config: + dependency: transitive + description: + name: build_config + url: "https://pub.dartlang.org" + source: hosted + version: "0.4.2" + build_daemon: + dependency: transitive + description: + name: build_daemon + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.4" + build_resolvers: + dependency: transitive + description: + name: build_resolvers + url: "https://pub.dartlang.org" + source: hosted + version: "1.3.10" + build_runner: + dependency: "direct dev" + description: + name: build_runner + url: "https://pub.dartlang.org" + source: hosted + version: "1.10.0" + build_runner_core: + dependency: transitive + description: + name: build_runner_core + url: "https://pub.dartlang.org" + source: hosted + version: "5.2.0" + built_collection: + dependency: transitive + description: + name: built_collection + url: "https://pub.dartlang.org" + source: hosted + version: "4.3.2" + built_value: + dependency: transitive + description: + name: built_value + url: "https://pub.dartlang.org" + source: hosted + version: "7.1.0" + characters: + dependency: transitive + description: + name: characters + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.0" charcode: dependency: transitive description: name: charcode url: "https://pub.dartlang.org" source: hosted - version: "1.1.2" + version: "1.1.3" + charts_common: + dependency: transitive + description: + name: charts_common + url: "https://pub.dartlang.org" + source: hosted + version: "0.9.0" + charts_flutter: + dependency: "direct main" + description: + name: charts_flutter + url: "https://pub.dartlang.org" + source: hosted + version: "0.9.0" + checked_yaml: + dependency: transitive + description: + name: checked_yaml + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.2" + cli_util: + dependency: transitive + description: + name: cli_util + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.4" + clock: + dependency: transitive + description: + name: clock + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.1" + code_builder: + dependency: transitive + description: + name: code_builder + url: "https://pub.dartlang.org" + source: hosted + version: "3.4.0" collection: dependency: transitive description: name: collection url: "https://pub.dartlang.org" source: hosted - version: "1.14.11" + version: "1.14.13" + 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.5" + csslib: + dependency: transitive + description: + name: csslib + url: "https://pub.dartlang.org" + source: hosted + version: "0.16.2" cupertino_icons: dependency: "direct main" description: name: cupertino_icons url: "https://pub.dartlang.org" source: hosted - version: "0.1.2" + version: "0.1.3" + dart_style: + dependency: transitive + description: + name: dart_style + url: "https://pub.dartlang.org" + source: hosted + version: "1.3.6" + datetime_picker_formfield: + dependency: "direct main" + description: + name: datetime_picker_formfield + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.0" + fake_async: + dependency: transitive + description: + name: fake_async + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.0" + file: + dependency: transitive + description: + name: file + url: "https://pub.dartlang.org" + source: hosted + version: "5.2.1" + fixnum: + dependency: transitive + description: + name: fixnum + url: "https://pub.dartlang.org" + source: hosted + version: "0.10.11" flutter: dependency: "direct main" description: flutter source: sdk version: "0.0.0" + flutter_fadein: + dependency: "direct main" + description: + name: flutter_fadein + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.1" + flutter_linkify: + dependency: "direct main" + description: + name: flutter_linkify + url: "https://pub.dartlang.org" + source: hosted + version: "3.1.3" + flutter_localizations: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_plugin_android_lifecycle: + dependency: transitive + description: + name: flutter_plugin_android_lifecycle + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.8" 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" + glob: + dependency: transitive + description: + name: glob + url: "https://pub.dartlang.org" + source: hosted + version: "1.2.0" + google_maps_flutter: + dependency: "direct main" + description: + name: google_maps_flutter + url: "https://pub.dartlang.org" + source: hosted + version: "0.5.28+1" + google_maps_flutter_platform_interface: + dependency: transitive + description: + name: google_maps_flutter_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.2" + graphs: + dependency: transitive + description: + name: graphs + url: "https://pub.dartlang.org" + source: hosted + version: "0.2.0" + html: + dependency: transitive + description: + name: html + url: "https://pub.dartlang.org" + source: hosted + version: "0.14.0+3" + http: + dependency: "direct main" + description: + name: http + url: "https://pub.dartlang.org" + source: hosted + version: "0.12.2" + http_multi_server: + dependency: transitive + description: + name: http_multi_server + url: "https://pub.dartlang.org" + source: hosted + version: "2.2.0" + http_parser: + dependency: transitive + description: + name: http_parser + url: "https://pub.dartlang.org" + source: hosted + version: "3.1.4" + intl: + dependency: transitive + description: + name: intl + url: "https://pub.dartlang.org" + source: hosted + version: "0.16.1" + io: + dependency: transitive + description: + name: io + url: "https://pub.dartlang.org" + source: hosted + version: "0.3.4" + js: + dependency: transitive + description: + name: js + url: "https://pub.dartlang.org" + source: hosted + version: "0.6.2" + json_annotation: + dependency: "direct main" + description: + name: json_annotation + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.1" + json_serializable: + dependency: "direct main" + description: + name: json_serializable + url: "https://pub.dartlang.org" + source: hosted + version: "3.3.0" + linkify: + dependency: transitive + description: + name: linkify + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.0" + logging: + dependency: transitive + description: + name: logging + url: "https://pub.dartlang.org" + source: hosted + version: "0.11.4" matcher: dependency: transitive description: name: matcher url: "https://pub.dartlang.org" source: hosted - version: "0.12.3+1" + version: "0.12.8" meta: dependency: transitive description: name: meta url: "https://pub.dartlang.org" source: hosted - version: "1.1.6" + version: "1.1.8" + mime: + dependency: transitive + description: + name: mime + url: "https://pub.dartlang.org" + source: hosted + version: "0.9.6+3" + node_interop: + dependency: transitive + description: + name: node_interop + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.1" + node_io: + dependency: transitive + description: + name: node_io + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.1" + package_config: + dependency: transitive + description: + name: package_config + url: "https://pub.dartlang.org" + source: hosted + version: "1.9.3" path: dependency: transitive description: name: path url: "https://pub.dartlang.org" source: hosted - version: "1.6.2" + version: "1.7.0" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + url: "https://pub.dartlang.org" + source: hosted + version: "0.0.1+2" + path_provider_platform_interface: + dependency: transitive + description: + name: path_provider_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.2" + pedantic: + dependency: "direct dev" + description: + name: pedantic + url: "https://pub.dartlang.org" + source: hosted + version: "1.9.0" + platform: + dependency: transitive + description: + name: platform + url: "https://pub.dartlang.org" + source: hosted + version: "2.2.1" + platform_detect: + dependency: transitive + description: + name: platform_detect + url: "https://pub.dartlang.org" + source: hosted + version: "1.4.0" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.2" + pool: + dependency: transitive + description: + name: pool + url: "https://pub.dartlang.org" + source: hosted + version: "1.4.0" + process: + dependency: transitive + description: + name: process + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.13" + pub_semver: + dependency: transitive + description: + name: pub_semver + url: "https://pub.dartlang.org" + source: hosted + version: "1.4.4" + pubspec_parse: + dependency: transitive + description: + name: pubspec_parse + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.5" quiver: dependency: transitive description: name: quiver url: "https://pub.dartlang.org" source: hosted - version: "2.0.1" + version: "2.1.3" + sa_anicoto: + dependency: transitive + description: + name: sa_anicoto + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.2" + sa_multi_tween: + dependency: transitive + description: + name: sa_multi_tween + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.1" + sa_stateless_animation: + dependency: transitive + description: + name: sa_stateless_animation + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.1" + sa_v1_migration: + dependency: transitive + description: + name: sa_v1_migration + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.2" + shared_preferences: + dependency: "direct main" + description: + name: shared_preferences + url: "https://pub.dartlang.org" + source: hosted + version: "0.5.8" + shared_preferences_linux: + dependency: transitive + description: + name: shared_preferences_linux + url: "https://pub.dartlang.org" + source: hosted + version: "0.0.2+1" + shared_preferences_macos: + dependency: transitive + description: + name: shared_preferences_macos + url: "https://pub.dartlang.org" + source: hosted + version: "0.0.1+10" + shared_preferences_platform_interface: + dependency: transitive + description: + name: shared_preferences_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.4" + shared_preferences_web: + dependency: transitive + description: + name: shared_preferences_web + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.2+7" + shelf: + dependency: transitive + description: + name: shelf + url: "https://pub.dartlang.org" + source: hosted + version: "0.7.7" + shelf_web_socket: + dependency: transitive + description: + name: shelf_web_socket + url: "https://pub.dartlang.org" + source: hosted + version: "0.2.3" + simple_animations: + dependency: "direct main" + description: + name: simple_animations + url: "https://pub.dartlang.org" + source: hosted + version: "2.2.1" sky_engine: dependency: transitive description: flutter source: sdk version: "0.0.99" + source_gen: + dependency: transitive + description: + name: source_gen + url: "https://pub.dartlang.org" + source: hosted + version: "0.9.6" source_span: dependency: transitive description: name: source_span url: "https://pub.dartlang.org" source: hosted - version: "1.4.1" + version: "1.7.0" stack_trace: dependency: transitive description: name: stack_trace url: "https://pub.dartlang.org" source: hosted - version: "1.9.3" + version: "1.9.5" stream_channel: dependency: transitive description: name: stream_channel url: "https://pub.dartlang.org" source: hosted - version: "1.6.8" + version: "2.0.0" + stream_transform: + dependency: transitive + description: + name: stream_transform + url: "https://pub.dartlang.org" + source: hosted + version: "1.2.0" string_scanner: dependency: transitive description: name: string_scanner url: "https://pub.dartlang.org" source: hosted - version: "1.0.4" + version: "1.0.5" + supercharged: + dependency: transitive + description: + name: supercharged + url: "https://pub.dartlang.org" + source: hosted + version: "1.7.0" term_glyph: dependency: transitive description: name: term_glyph url: "https://pub.dartlang.org" source: hosted - version: "1.0.1" + version: "1.1.0" test_api: dependency: transitive description: name: test_api url: "https://pub.dartlang.org" source: hosted - version: "0.2.1" + version: "0.2.17" + timing: + dependency: transitive + description: + name: timing + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.1+2" typed_data: dependency: transitive description: name: typed_data url: "https://pub.dartlang.org" source: hosted - version: "1.1.6" + version: "1.2.0" + url_launcher: + dependency: "direct main" + description: + name: url_launcher + url: "https://pub.dartlang.org" + source: hosted + version: "5.5.0" + url_launcher_linux: + dependency: transitive + description: + name: url_launcher_linux + url: "https://pub.dartlang.org" + source: hosted + version: "0.0.1+1" + url_launcher_macos: + dependency: transitive + description: + name: url_launcher_macos + url: "https://pub.dartlang.org" + source: hosted + version: "0.0.1+7" + url_launcher_platform_interface: + dependency: transitive + description: + name: url_launcher_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.7" + url_launcher_web: + dependency: transitive + description: + name: url_launcher_web + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.2" vector_math: dependency: transitive description: @@ -135,5 +698,34 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.0.8" + watcher: + dependency: transitive + description: + name: watcher + url: "https://pub.dartlang.org" + source: hosted + version: "0.9.7+15" + web_socket_channel: + dependency: transitive + description: + name: web_socket_channel + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.0" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.0" + yaml: + dependency: transitive + description: + name: yaml + url: "https://pub.dartlang.org" + source: hosted + version: "2.2.1" sdks: - dart: ">=2.0.0 <3.0.0" + dart: ">=2.9.0-14.0.dev <3.0.0" + flutter: ">=1.16.3 <2.0.0" diff --git a/pubspec.yaml b/pubspec.yaml index f9339a5..fb53942 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -10,12 +10,24 @@ description: Local Spend Tracker version: 1.0.0+1 environment: - sdk: ">=2.0.0-dev.68.0 <3.0.0" + sdk: ">=2.2.2 <3.0.0" dependencies: flutter: sdk: flutter - + flutter_localizations: + sdk: flutter + shared_preferences: ^0.5.8 + url_launcher: ^5.5.0 + json_annotation : ^3.0.1 + http: ^0.12.0+2 + datetime_picker_formfield: ^1.0.0 + flutter_linkify: ^3.1.3 + flutter_fadein: ^1.1.1 + charts_flutter: ^0.9.0 + simple_animations: ^2.2.1 + google_maps_flutter: ^0.5.20+5 + json_serializable: ^3.3.0 # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons. cupertino_icons: ^0.1.2 @@ -23,7 +35,8 @@ dependencies: dev_dependencies: flutter_test: sdk: flutter - + pedantic: ^1.4.0 + build_runner: ^1.10.0 # For information on the generic Dart part of this file, see the # following page: https://www.dartlang.org/tools/pub/pubspec @@ -40,7 +53,15 @@ flutter: # assets: # - images/a_dot_burr.jpeg # - images/a_dot_ham.jpeg + assets: + - assets/ + - assets/images/ + - assets/images/launch_image.png + fonts: + - family: Consolas + fonts: + - asset: assets/Consolas.ttf # An image asset can refer to one or more resolution-specific "variants", see # https://flutter.io/assets-and-images/#resolution-aware. diff --git a/test/widget_test.dart b/test/widget_test.dart index ea7f4d6..5beab46 100644 --- a/test/widget_test.dart +++ b/test/widget_test.dart @@ -11,20 +11,16 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:local_spend/main.dart'; void main() { - testWidgets('Counter increments smoke test', (WidgetTester tester) async { + testWidgets('GO button repetition 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)); + // Tap the GO button and trigger a frame. + await tester.tap(find.byKey(Key("goButton"))); await tester.pump(); - // Verify that our counter has incremented. - expect(find.text('0'), findsNothing); - expect(find.text('1'), findsOneWidget); + // Verify that the dialog has shown +// expect(find.text('GO'), findsNothing); +// expect(find.text('Invalid data'), findsOneWidget); }); }