Merge pull request #9 from Pear-Trading/fix/update

Merge `felix/main` branch into `development`, whilst also updating it
This commit is contained in:
Tom Bloor 2020-08-05 14:04:43 +01:00 committed by GitHub
commit 203d375aa4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
107 changed files with 6139 additions and 304 deletions

2
.gitignore vendored
View file

@ -235,3 +235,5 @@ fabric.properties
!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages
# End of https://www.gitignore.io/api/android,flutter,androidstudio
lib/common/felixApiCreds.dart
android/app/src/main/AndroidManifest.xml

View file

@ -1,29 +1,116 @@
<component name="ProjectCodeStyleConfiguration">
<code_scheme name="Project" version="173">
<Objective-C-extensions>
<file>
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Import" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Macro" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Typedef" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Enum" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Constant" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Global" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Struct" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="FunctionPredecl" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Function" />
</file>
<class>
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Property" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Synthesize" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="InitMethod" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="StaticMethod" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="InstanceMethod" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="DeallocMethod" />
</class>
<extensions>
<pair source="cpp" header="h" fileNamingConvention="NONE" />
<pair source="c" header="h" fileNamingConvention="NONE" />
</extensions>
</Objective-C-extensions>
<codeStyleSettings language="XML">
<indentOptions>
<option name="CONTINUATION_INDENT_SIZE" value="4" />
</indentOptions>
<arrangement>
<rules>
<section>
<rule>
<match>
<AND>
<NAME>xmlns:android</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>xmlns:.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:id</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:name</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>name</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>style</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
<order>ANDROID_ATTRIBUTE_ORDER</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>.*</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
</rules>
</arrangement>
</codeStyleSettings>
</code_scheme>
</component>

View file

@ -1,6 +1,6 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="main.dart" type="FlutterRunConfigurationType" factoryName="Flutter">
<configuration default="false" name="main.dart" type="FlutterRunConfigurationType" factoryName="Flutter" singleton="false">
<option name="filePath" value="$PROJECT_DIR$/lib/main.dart" />
<method />
<method v="2" />
</configuration>
</component>

View file

@ -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,6 +38,9 @@ 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)

18
analysis_options.yaml Normal file
View file

@ -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

View file

@ -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'

View file

@ -13,11 +13,18 @@
additional functionality it is fine to subclass or reimplement
FlutterApplication and put your custom class here. -->
<application
android:name="io.flutter.app.FlutterApplication"
android:label="local_spend"
android:label="SpendTracker"
android:icon="@mipmap/ic_launcher">
<activity
android:name=".MainActivity"
<meta-data
android:name="flutterEmbedding"
android:value="2" />
<meta-data android:name="com.google.android.geo.API_KEY"
android:value="AIzaSyD0vsoT6Omnn01hbUiCjAhiS47uFYWnEHE"/>
<!-- Please don't be stupid with the above key-->
<activity android:name=".MainActivity"
android:launchMode="singleTop"
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|layoutDirection|fontScale|screenLayout|density"
@ -27,9 +34,19 @@
until Flutter renders its first frame. It can be removed if
there is no splash screen (such as the default splash screen
defined in @style/LaunchTheme). -->
<!-- Specify that the launch screen should continue being displayed -->
<!-- until Flutter renders its first frame. -->
<meta-data
android:name="io.flutter.app.android.SplashScreenUntilFirstFrame"
android:value="true" />
android:name="io.flutter.embedding.android.SplashScreenDrawable"
android:resource="@drawable/launch_background" />
<!-- Theme to apply as soon as Flutter begins rendering frames -->
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme"
/>
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>

View file

@ -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);
}
}

View file

@ -4,9 +4,9 @@
<item android:drawable="@android:color/white" />
<!-- You can insert your own image assets here -->
<!-- <item>
<!--<item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</item>-->
</layer-list>

View file

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@android:color/white" />
<!-- You can insert your own image assets here -->
<!--<item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item>-->
</layer-list>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 544 B

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 442 B

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 721 B

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 131 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

View file

@ -5,4 +5,8 @@
Flutter draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
<item name="android:windowBackground">@drawable/normal_background</item>
</style>
</resources>

View file

@ -1 +1,4 @@
org.gradle.jvmargs=-Xmx1536M
android.useAndroidX=true
android.enableJetifier=true
android.enableR8=true

BIN
assets/Consolas.ttf Normal file

Binary file not shown.

BIN
assets/images/cat.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 131 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

BIN
assets/images/text.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 169 KiB

View file

@ -1 +1,2 @@
#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
#include "Generated.xcconfig"

View file

@ -1 +1,2 @@
#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
#include "Generated.xcconfig"

69
ios/Podfile Normal file
View file

@ -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

45
ios/Podfile.lock Normal file
View file

@ -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

View file

@ -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 = "<group>"; };
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
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 = "<group>"; };
3B80C3931E831B6300D905FE /* App.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = App.framework; path = Flutter/App.framework; sourceTree = "<group>"; };
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 = "<group>"; };
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = "<group>"; };
7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = "<group>"; };
@ -55,6 +56,8 @@
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
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 = "<group>"; };
/* 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 = "<group>";
};
55C1C6FB63DDD235539F7C5D /* Pods */ = {
isa = PBXGroup;
children = (
510C088F759CD31C97486879 /* Pods-Runner.debug.xcconfig */,
0C47C2DEBD545D4054D9940A /* Pods-Runner.release.xcconfig */,
C40D35B4079C018C62D759E0 /* Pods-Runner.profile.xcconfig */,
);
path = Pods;
sourceTree = "<group>";
};
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 = "<group>";
};
@ -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;

View file

@ -4,4 +4,7 @@
<FileRef
location = "group:Runner.xcodeproj">
</FileRef>
<FileRef
location = "group:Pods/Pods.xcodeproj">
</FileRef>
</Workspace>

View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>

View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>BuildSystemType</key>
<string>Original</string>
</dict>
</plist>

View file

@ -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];

View file

@ -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"
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 564 B

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 115 KiB

View file

@ -2,8 +2,12 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>io.flutter.embedded_views_preview</key>
<true/>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleDisplayName</key>
<string>Local Spend Tracker</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
@ -26,6 +30,10 @@
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
<string>Main</string>
<key>UIStatusBarHidden</key>
<false/>
<key>UIStatusBarStyle</key>
<string>UIStatusBarStyleDarkContent</string>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>

View file

@ -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<List<String>> getCategories() async {
const url = "https://dev.localspend.co.uk/api/search/category";
var token;
await getToken().then((result) {
token = result;
});
Map<String, String> body = {
"session_key": token,
};
final response = await http.post(
url,
body: json.encode(body),
);
// print(response.body);
if (response.statusCode == 200) {
//request successful
List<String> categories = new List<String>();
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>
// Category is defined at the top^^
} else {
// not successful
return null;
}
}

View file

@ -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<Organisation> getTestData() {
var numItems = 10;
var itemsList = new List<Organisation>();
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<Organisation> _jsonToOrganisations(String json) {
Map decoded = jsonDecode(json);
List<dynamic> validated = decoded['validated'];
List<Map> organisationsMaps = new List<Map>();
validated.forEach((element) => organisationsMaps.add(element));
List<Organisation> organisations = new List<Organisation>();
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<List<Organisation>> findOrganisations(String search) async {
final url = "https://dev.localspend.co.uk/api/search";
var token;
await getToken().then((result) {
token = result;
});
Map<String, String> 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;
}
}
}

View file

@ -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<charts.Series<TimeSeriesCustomersOrSales, DateTime>> graph;
List<TimeSeriesCustomersOrSales> cachedData;
bool loaded = false;
Future<void> getGraphData() async {
if (loaded) {
this.graph = [
new charts.Series<TimeSeriesCustomersOrSales, DateTime>(
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<String, String> 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<dynamic> labels = responseJson['graph']['labels'];
final List<dynamic> data = responseJson['graph']['data'];
List<TimeSeriesCustomersOrSales> graphDataList =
new List<TimeSeriesCustomersOrSales>();
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<TimeSeriesCustomersOrSales, DateTime>(
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<charts.Series<dynamic, DateTime>> graph;
List<TimeSeriesSpend> cachedData;
bool loaded = false;
Future<List<charts.Series<dynamic, DateTime>>> setGraphData() async {
if (loaded) {
this.graph = [
new charts.Series<TimeSeriesSpend, DateTime>(
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<String, String> 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<dynamic> labels = responseJson['graph']['labels'];
final List<dynamic> data = responseJson['graph']['data'];
List<TimeSeriesSpend> timeSeriesSpendList = new List<TimeSeriesSpend>();
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<TimeSeriesSpend, DateTime>(
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<List<charts.Series<dynamic, DateTime>>> getGraphData() async {
if (loaded == true) {
return [
new charts.Series<TimeSeriesSpend, DateTime>(
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<String, String> 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<dynamic> labels = responseJson['graph']['labels'];
final List<dynamic> data = responseJson['graph']['data'];
List<TimeSeriesSpend> timeSeriesSpendList = new List<TimeSeriesSpend>();
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<TimeSeriesSpend, DateTime>(
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;
}

View file

@ -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<Location> locations;
final List<Region> regions;
}
Future<Locations> 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<String, Map<String, double>> 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,
);
}
}

View file

@ -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<void> _incorrectDialog(BuildContext context, bool isLoginWrong) async {
return showDialog<void>(
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: <Widget>[
FlatButton(
child: Text('OK'),
onPressed: () {
Navigator.of(context).pop();
},
),
],
),
);
},
);
}
Future<LoginModel> requestLoginAPI(
BuildContext context, String email, String password) async {
final url = "https://dev.localspend.co.uk/api/login";
Map<String, String> 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;
}

View file

@ -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<bool> requestLogoutAPI() async {
saveLogout();
final url = "https://dev.localspend.co.uk/api/logout";
var token;
await getToken().then((result) {
token = result;
});
Map<String, String> body = {
"Token": token,
};
await http.post(
url,
body: json.encode(body),
);
return true;
}

View file

@ -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<LoginModel> 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<String, String> 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;
}
}

View file

@ -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<Widget> 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<Widget> children,
}) {
assert(context != null);
showDialog<void>(
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<void>(
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<Widget> 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<Widget> body = <Widget>[];
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: <Widget>[
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 = <Widget>[
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: body,
),
];
if (children != null) body.addAll(children);
return AlertDialog(
content: SingleChildScrollView(
child: ListBody(children: body),
),
actions: <Widget>[
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<LicensePage> {
@override
void initState() {
super.initState();
_initLicenses();
}
final List<Widget> _licenses = <Widget>[];
bool _loaded = false;
Future<void> _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<LicenseParagraph> paragraphs =
await SchedulerBinding.instance.scheduleTask<List<LicenseParagraph>>(
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<Widget> contents = <Widget>[
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;
}

View file

@ -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: <Widget>[
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);
}

View file

@ -0,0 +1,9 @@
import 'package:shared_preferences/shared_preferences.dart';
import 'dart:async';
Future<String> getToken() async {
SharedPreferences preferences = await SharedPreferences.getInstance();
String getToken = preferences.getString("LastToken");
return getToken;
}

View file

@ -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<void> _clearLoginDetails() async {
SharedPreferences preferences = await SharedPreferences.getInstance();
await preferences.setString('username', "");
await preferences.setString('password', "");
}

View file

@ -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 : "");
}

View file

@ -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', "");
}

View file

@ -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: <Widget>[
// 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);
},
),
],
);
},
);
}

View file

@ -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: <Widget>[
// usually buttons at the bottom of the dialog
new FlatButton(
child: new Text(buttonLabel),
onPressed: () {
Navigator.of(context).pop();
},
),
],
);
},
);
}

View file

@ -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<Widget> 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,
);
}
}

View file

@ -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<Color> 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])),
);
},
);
}
}

View file

@ -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<charts.Series> 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<charts.Series<LinearSales, String>> _createSampleData() {
final data = [
new LinearSales("Other", 17),
new LinearSales("Mars", 26),
new LinearSales("Morecambe", 35),
new LinearSales("Lancaster", 48),
];
return [
new charts.Series<LinearSales, String>(
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;
}

View file

@ -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(),
);
}
}

View file

@ -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<charts.Series> 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<charts.Series<LinearSales, int>> _createSampleData() {
final data = [
new LinearSales(0, 100),
new LinearSales(1, 75),
new LinearSales(2, 25),
new LinearSales(3, 5),
];
return [
new charts.Series<LinearSales, int>(
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;
}

View file

@ -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<charts.Series> 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<charts.Series<OrdinalSales, String>> _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<OrdinalSales, String>(
id: 'Desktop',
domainFn: (OrdinalSales sales, _) => sales.year,
measureFn: (OrdinalSales sales, _) => sales.sales,
data: desktopSalesData,
),
new charts.Series<OrdinalSales, String>(
id: 'Tablet',
domainFn: (OrdinalSales sales, _) => sales.year,
measureFn: (OrdinalSales sales, _) => sales.sales,
data: tabletSalesData,
),
new charts.Series<OrdinalSales, String>(
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;
}

View file

@ -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<charts.Series> 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<charts.Series<LinearSales, int>> _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<LinearSales, int>(
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<LinearSales, int>(
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<LinearSales, int>(
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;
}

View file

@ -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<charts.Series> 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<charts.Series<LinearSales, int>> _createSampleData() {
final data = [
new LinearSales(0, 23),
new LinearSales(1, 44),
new LinearSales(2, 25),
new LinearSales(3, 15),
];
return [
new charts.Series<LinearSales, int>(
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;
}

View file

@ -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<charts.Series> 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<charts.Series<LinearSales, int>> _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<LinearSales, int>(
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<LinearSales, int>(
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<LinearSales, int>(
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<LinearSales, int>(
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<LinearSales, int>(
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<LinearSales, int>(
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;
}

View file

@ -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<charts.Series> 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<charts.Series<OrdinalSales, String>> _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<OrdinalSales, String>(
id: 'Lancaster',
domainFn: (OrdinalSales sales, _) => sales.year,
measureFn: (OrdinalSales sales, _) => sales.sales,
data: desktopSalesData,
),
new charts.Series<OrdinalSales, String>(
id: 'Morecambe',
domainFn: (OrdinalSales sales, _) => sales.year,
measureFn: (OrdinalSales sales, _) => sales.sales,
data: tabletSalesData,
),
new charts.Series<OrdinalSales, String>(
id: 'Mars',
domainFn: (OrdinalSales sales, _) => sales.year,
measureFn: (OrdinalSales sales, _) => sales.sales,
data: mobileSalesData,
),
new charts.Series<OrdinalSales, String>(
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;
}

View file

@ -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<charts.Series> 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<charts.Series<TimeSeriesSales, DateTime>> _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<TimeSeriesSales, DateTime>(
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;
}

View file

@ -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.
/// * <https://material.io/design/components/selection-controls.html#checkboxes>
/// * <https://material.io/design/components/lists.html#types>
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<bool> 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<CustomCheckbox>
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<bool> 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<bool> 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);
}
}
}
}
}

View file

@ -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: <Widget>[
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: <Widget>[
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;
});
},
),
),
);
}
*/

View file

@ -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<dynamic> _moreInfoDialog(context, Organisation organisation) {
TextStyle informationTitleStyle = new TextStyle(fontSize: 16);
TextStyle informationStyle =
new TextStyle(fontSize: 16, fontWeight: FontWeight.bold);
return showDialog<Organisation>(
context: context,
barrierDismissible: true,
builder: (BuildContext context) {
return StatefulBuilder(
builder: (context, setState) {
return SimpleDialog(
children: <Widget>[
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<Organisation> dialog(context) {
bool _searchEnabled = false;
bool _orgsFetched = false;
TextEditingController searchBarText = new TextEditingController();
var organisations = new Organisations();
var listTitle = "All Organisations";
var organisationsList = List<Organisation>();
Future<int> _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<Organisation>(
context: context,
barrierDismissible: true,
builder: (BuildContext context) {
return StatefulBuilder(
builder: (context, setState) {
return SimpleDialog(
children: <Widget>[
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
],
// ),
);
},
);
},
);
}
}

View file

@ -0,0 +1,36 @@
import 'package:flutter/material.dart';
import 'dart:async';
class PopupListView {
Future<dynamic> dialog(context, List<String> options, String title) {
return showDialog<dynamic>(
context: context,
barrierDismissible: true,
builder: (BuildContext context) {
return SimpleDialog(
title: Text(title),
children: getDialogOptions(context, options),
);
},
);
}
List<Widget> getDialogOptions(
context, List<String> options /*, Function onPressed*/) {
var dialogOptionsList = new List<SimpleDialogOption>();
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;
}
}

46
lib/config.dart Normal file
View file

@ -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<String, dynamic> 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;
}

14
lib/config.g.dart Normal file
View file

@ -0,0 +1,14 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'config.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
Config _$ConfigFromJson(Map<String, dynamic> json) {
return Config(
env: json['env'] as String,
production: json['production'] as bool,
apiKey: json['apiKey'] as String);
}

6
lib/env/dev.dart vendored Normal file
View file

@ -0,0 +1,6 @@
import 'package:json_annotation/json_annotation.dart';
part 'dev.g.dart';
@JsonLiteral('dev.json', asConst: true)
Map<String, dynamic> get config => _$configJsonLiteral;

13
lib/env/dev.g.dart vendored Normal file
View file

@ -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'
};

5
lib/env/dev.json vendored Normal file
View file

@ -0,0 +1,5 @@
{
"env": "DEV",
"production": false,
"apiUrl": "https://dev.localspend.co.uk/api"
}

6
lib/env/prod.dart vendored Normal file
View file

@ -0,0 +1,6 @@
import 'package:json_annotation/json_annotation.dart';
part 'prod.g.dart';
@JsonLiteral('prod.json', asConst: true)
Map<String, dynamic> get config => _$configJsonLiteral;

13
lib/env/prod.g.dart vendored Normal file
View file

@ -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'
};

5
lib/env/prod.json vendored Normal file
View file

@ -0,0 +1,5 @@
{
"env": "PROD",
"production": true,
"apiUrl": "https://www.localspend.co.uk/api"
}

View file

@ -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<MyHomePage> {
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: <Widget>[
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: <String, WidgetBuilder>{
"/HomePage": (BuildContext context) => HomePage(),
"/LoginPage": (BuildContext context) => LoginPage(),
'/MapPage': (BuildContext context) => MapPage(),
"/ReceiptPage": (BuildContext context) => ReceiptPage2(),
"/MorePage": (BuildContext context) => MorePage(),
},
home: SplashScreen(),
);
}
}

7
lib/main_dev.dart Normal file
View file

@ -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()));

View file

@ -0,0 +1,18 @@
class LoginModel {
LoginModel(this.userName, this.token, this.userType);
LoginModel.fromJson(Map<String, dynamic> json)
: userName = json['display_name'],
userType = json['user_type'],
token = json['session_key'];
final String userName;
final String token;
final String userType;
Map<String, dynamic> toJson() => {
'name': userName,
'user_type': userType,
'token': token,
};
}

View file

@ -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<CustomerGraphs> {
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: <Widget>[
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<Color>(Colors.orange),
)), //List<Series<dynamic, DateTime>>es<dynamic, DateTime>>
),
),
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<Color>(Colors.blue),
)), //List<Series<dynamic, DateTime>>es<dynamic, DateTime>>
),
),
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<Color>(Colors.green),
)), //List<Series<dynamic, DateTime>>es<dynamic, DateTime>>
),
),
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<Color>(Colors.red),
)), //List<Series<dynamic, DateTime>>es<dynamic, DateTime>>
),
),
],
);
}
}

76
lib/pages/home_page.dart Normal file
View file

@ -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<HomePageWidget> {
int _selectedIndex = 0;
static const TextStyle optionStyle =
TextStyle(fontSize: 30, fontWeight: FontWeight.bold);
static List<Widget> _widgetOptions = <Widget>[
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>[
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,
),
);
}
}

226
lib/pages/login_page.dart Normal file
View file

@ -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<StatefulWidget> createState() {
return new LoginPageState();
}
}
class LoginPageState extends State<LoginPage> {
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<dynamic> 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: <Widget>[
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;
});
},
),
),
],
),
),
],
),
),
);
}
}

62
lib/pages/map_page.dart Normal file
View file

@ -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<MapPage> {
final Map<String, gmaps.Marker> _markers = {};
Future<void> _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,
),
),
);
}
}

191
lib/pages/more_page.dart Normal file
View file

@ -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<StatefulWidget> createState() {
return new MorePageState();
}
}
class MorePageState extends State<MorePage> {
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<dynamic> 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: <Widget>[
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: <Widget>[
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,
// ),
// ),
// ),
],
),
),
),
);
}
}

View file

@ -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<StatefulWidget> createState() {
return new NewStatsPageState();
}
}
class NewStatsPageState extends State<NewStatsPage> {
/// 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(),
);
}
}

178
lib/pages/orgGraphs.dart Normal file
View file

@ -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<OrgGraphs> {
/// 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: <Widget>[
// 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<Series<dynamic, DateTime>>
// ),
// ),
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<Series<dynamic, DateTime>>
),
),
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<Color>(Colors.green),
)), //List<Series<dynamic, DateTime>>
),
),
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<Color>(Colors.red),
)), //List<Series<dynamic, DateTime>>
),
),
],
);
}
}

Some files were not shown because too many files have changed in this diff Show more