diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 81f2036..f5bffe1 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -6,13 +6,13 @@ PODS: - Flutter DEPENDENCIES: - - Flutter (from `.symlinks/flutter/ios-profile`) + - Flutter (from `.symlinks/flutter/ios`) - shared_preferences (from `.symlinks/plugins/shared_preferences/ios`) - url_launcher (from `.symlinks/plugins/url_launcher/ios`) EXTERNAL SOURCES: Flutter: - :path: ".symlinks/flutter/ios-profile" + :path: ".symlinks/flutter/ios" shared_preferences: :path: ".symlinks/plugins/shared_preferences/ios" url_launcher: diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index dff0209..a83a4d3 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -281,7 +281,7 @@ ); inputPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh", - "${PODS_ROOT}/../.symlinks/flutter/ios-profile/Flutter.framework", + "${PODS_ROOT}/../.symlinks/flutter/ios/Flutter.framework", ); name = "[CP] Embed Pods Frameworks"; outputPaths = ( diff --git a/lib/common/apifunctions/request_login_api.dart b/lib/common/apifunctions/request_login_api.dart index 36ae376..2e31f8e 100644 --- a/lib/common/apifunctions/request_login_api.dart +++ b/lib/common/apifunctions/request_login_api.dart @@ -38,6 +38,9 @@ Future requestLoginAPI( return LoginModel.fromJson(responseJson); } else { + debugPrint("Invalid, either creds are wrong or server is down"); + Navigator.of(context).pushReplacementNamed('/HomePage'); // just here temporarily while server is down + final responseJson = json.decode(response.body); saveCurrentLogin(responseJson, body["email"]); diff --git a/lib/common/functions/customAbout.dart b/lib/common/functions/customAbout.dart new file mode 100644 index 0000000..28f8732 --- /dev/null +++ b/lib/common/functions/customAbout.dart @@ -0,0 +1,509 @@ +// Copyright 2016 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; +//import 'dart:developer' show Timeline, Flow; +import 'dart:developer' as dev; +import 'dart:io' show Platform; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/scheduler.dart'; +import 'package:flutter/widgets.dart' hide Flow; +import 'package:flutter/material.dart'; + +/// A [ListTile] that shows an about box. +/// +/// This widget is often added to an app's [Drawer]. When tapped it shows +/// an about box dialog with [showAboutDialog]. +/// +/// The about box will include a button that shows licenses for software used by +/// the application. The licenses shown are those returned by the +/// [LicenseRegistry] API, which can be used to add more licenses to the list. +/// +/// If your application does not have a [Drawer], you should provide an +/// affordance to call [showAboutDialog] or (at least) [showLicensePage]. +class AboutListTile extends StatelessWidget { + /// Creates a list tile for showing an about box. + /// + /// The arguments are all optional. The application name, if omitted, will be + /// derived from the nearest [Title] widget. The version, icon, and legalese + /// values default to the empty string. + const AboutListTile({ + Key key, + this.icon = const Icon(null), + this.child, + this.applicationName, + this.applicationVersion, + this.applicationIcon, + this.applicationLegalese, + this.aboutBoxChildren, + }) : super(key: key); + + /// The icon to show for this drawer item. + /// + /// By default no icon is shown. + /// + /// This is not necessarily the same as the image shown in the dialog box + /// itself; which is controlled by the [applicationIcon] property. + final Widget icon; + + /// The label to show on this drawer item. + /// + /// Defaults to a text widget that says "About Foo" where "Foo" is the + /// application name specified by [applicationName]. + final Widget child; + + /// The name of the application. + /// + /// This string is used in the default label for this drawer item (see + /// [child]) and as the caption of the [AboutDialog] that is shown. + /// + /// Defaults to the value of [Title.title], if a [Title] widget can be found. + /// Otherwise, defaults to [Platform.resolvedExecutable]. + final String applicationName; + + /// The version of this build of the application. + /// + /// This string is shown under the application name in the [AboutDialog]. + /// + /// Defaults to the empty string. + final String applicationVersion; + + /// The icon to show next to the application name in the [AboutDialog]. + /// + /// By default no icon is shown. + /// + /// Typically this will be an [ImageIcon] widget. It should honor the + /// [IconTheme]'s [IconThemeData.size]. + /// + /// This is not necessarily the same as the icon shown on the drawer item + /// itself, which is controlled by the [icon] property. + final Widget applicationIcon; + + /// A string to show in small print in the [AboutDialog]. + /// + /// Typically this is a copyright notice. + /// + /// Defaults to the empty string. + final String applicationLegalese; + + /// Widgets to add to the [AboutDialog] after the name, version, and legalese. + /// + /// This could include a link to a Web site, some descriptive text, credits, + /// or other information to show in the about box. + /// + /// Defaults to nothing. + final List aboutBoxChildren; + + @override + Widget build(BuildContext context) { + assert(debugCheckHasMaterial(context)); + assert(debugCheckHasMaterialLocalizations(context)); + return ListTile( + leading: icon, + title: child ?? + Text(MaterialLocalizations.of(context).aboutListTileTitle(applicationName ?? _defaultApplicationName(context))), + onTap: () { + showAboutDialog( + context: context, + applicationName: applicationName, + applicationVersion: applicationVersion, + applicationIcon: applicationIcon, + applicationLegalese: applicationLegalese, + children: aboutBoxChildren, + ); + }, + ); + } +} + +/// Displays an [AboutDialog], which describes the application and provides a +/// button to show licenses for software used by the application. +/// +/// The arguments correspond to the properties on [AboutDialog]. +/// +/// If the application has a [Drawer], consider using [AboutListTile] instead +/// of calling this directly. +/// +/// If you do not need an about box in your application, you should at least +/// provide an affordance to call [showLicensePage]. +/// +/// The licenses shown on the [LicensePage] are those returned by the +/// [LicenseRegistry] API, which can be used to add more licenses to the list. +/// +/// The `context` argument is passed to [showDialog], the documentation for +/// which discusses how it is used. +void showAboutDialog({ + @required BuildContext context, + String applicationName, + String applicationVersion, + Widget applicationIcon, + String applicationLegalese, + List children, +}) { + assert(context != null); + showDialog( + context: context, + builder: (BuildContext context) { + return AboutDialog( + applicationName: applicationName, + applicationVersion: applicationVersion, + applicationIcon: applicationIcon, + applicationLegalese: applicationLegalese, + children: children, + ); + }, + ); +} + +/// Displays a [LicensePage], which shows licenses for software used by the +/// application. +/// +/// The arguments correspond to the properties on [LicensePage]. +/// +/// If the application has a [Drawer], consider using [AboutListTile] instead +/// of calling this directly. +/// +/// The [AboutDialog] shown by [showAboutDialog] includes a button that calls +/// [showLicensePage]. +/// +/// The licenses shown on the [LicensePage] are those returned by the +/// [LicenseRegistry] API, which can be used to add more licenses to the list. +void showLicensePage({ + @required BuildContext context, + String applicationName, + String applicationVersion, + Widget applicationIcon, + String applicationLegalese, +}) { + assert(context != null); + Navigator.push(context, MaterialPageRoute( + builder: (BuildContext context) => LicensePage( + applicationName: applicationName, + applicationVersion: applicationVersion, + applicationLegalese: applicationLegalese, + ) + )); +} + +/// An about box. This is a dialog box with the application's icon, name, +/// version number, and copyright, plus a button to show licenses for software +/// used by the application. +/// +/// To show an [AboutDialog], use [showAboutDialog]. +/// +/// If the application has a [Drawer], the [AboutListTile] widget can make the +/// process of showing an about dialog simpler. +/// +/// The [AboutDialog] shown by [showAboutDialog] includes a button that calls +/// [showLicensePage]. +/// +/// The licenses shown on the [LicensePage] are those returned by the +/// [LicenseRegistry] API, which can be used to add more licenses to the list. +class AboutDialog extends StatelessWidget { + /// Creates an about box. + /// + /// The arguments are all optional. The application name, if omitted, will be + /// derived from the nearest [Title] widget. The version, icon, and legalese + /// values default to the empty string. + const AboutDialog({ + Key key, + this.applicationName, + this.applicationVersion, + this.applicationIcon, + this.applicationLegalese, + this.children, + }) : super(key: key); + + /// The name of the application. + /// + /// Defaults to the value of [Title.title], if a [Title] widget can be found. + /// Otherwise, defaults to [Platform.resolvedExecutable]. + final String applicationName; + + /// The version of this build of the application. + /// + /// This string is shown under the application name. + /// + /// Defaults to the empty string. + final String applicationVersion; + + /// The icon to show next to the application name. + /// + /// By default no icon is shown. + /// + /// Typically this will be an [ImageIcon] widget. It should honor the + /// [IconTheme]'s [IconThemeData.size]. + final Widget applicationIcon; + + /// A string to show in small print. + /// + /// Typically this is a copyright notice. + /// + /// Defaults to the empty string. + final String applicationLegalese; + + /// Widgets to add to the dialog box after the name, version, and legalese. + /// + /// This could include a link to a Web site, some descriptive text, credits, + /// or other information to show in the about box. + /// + /// Defaults to nothing. + final List children; + + @override + Widget build(BuildContext context) { + assert(debugCheckHasMaterialLocalizations(context)); + final String name = applicationName ?? _defaultApplicationName(context); + final String version = applicationVersion ?? _defaultApplicationVersion(context); + final Widget icon = applicationIcon ?? _defaultApplicationIcon(context); + List body = []; + if (icon != null) + body.add(IconTheme(data: const IconThemeData(size: 45.0), child: icon)); + body.add(Expanded( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 24.0), + child: ListBody( + children: [ + Text(name, style: Theme.of(context).textTheme.title), + Text(version, style: Theme.of(context).textTheme.body1), + Text(applicationLegalese ?? '', style: Theme.of(context).textTheme.caption), + ], + ), + ), + )); + body = [ + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: body, + ), + ]; + if (children != null) + body.addAll(children); + return AlertDialog( + content: SingleChildScrollView( + child: ListBody(children: body), + ), + actions: [ + FlatButton( + child: Text(MaterialLocalizations.of(context).viewLicensesButtonLabel), + onPressed: () { + showLicensePage( + context: context, + applicationName: applicationName, + applicationVersion: applicationVersion, + applicationIcon: applicationIcon, + applicationLegalese: applicationLegalese, + ); + }, + ), + FlatButton( + child: Text(MaterialLocalizations.of(context).closeButtonLabel), + onPressed: () { + Navigator.pop(context); + }, + ), + ], + ); + } +} + +/// A page that shows licenses for software used by the application. +/// +/// To show a [LicensePage], use [showLicensePage]. +/// +/// The [AboutDialog] shown by [showAboutDialog] and [AboutListTile] includes +/// a button that calls [showLicensePage]. +/// +/// The licenses shown on the [LicensePage] are those returned by the +/// [LicenseRegistry] API, which can be used to add more licenses to the list. +class LicensePage extends StatefulWidget { + /// Creates a page that shows licenses for software used by the application. + /// + /// The arguments are all optional. The application name, if omitted, will be + /// derived from the nearest [Title] widget. The version and legalese values + /// default to the empty string. + /// + /// The licenses shown on the [LicensePage] are those returned by the + /// [LicenseRegistry] API, which can be used to add more licenses to the list. + const LicensePage({ + Key key, + this.applicationName, + this.applicationVersion, + this.applicationLegalese, + }) : super(key: key); + + /// The name of the application. + /// + /// Defaults to the value of [Title.title], if a [Title] widget can be found. + /// Otherwise, defaults to [Platform.resolvedExecutable]. + final String applicationName; + + /// The version of this build of the application. + /// + /// This string is shown under the application name. + /// + /// Defaults to the empty string. + final String applicationVersion; + + /// A string to show in small print. + /// + /// Typically this is a copyright notice. + /// + /// Defaults to the empty string. + final String applicationLegalese; + + @override + _LicensePageState createState() => _LicensePageState(); +} + +class _LicensePageState extends State { + @override + void initState() { + super.initState(); + _initLicenses(); + } + + final List _licenses = []; + bool _loaded = false; + + Future _initLicenses() async { + int debugFlowId = -1; + assert(() { + final dev.Flow flow = dev.Flow.begin(); + dev.Timeline.timeSync('_initLicenses()', () { }, flow: flow); + debugFlowId = flow.id; + return true; + }()); + await for (LicenseEntry license in LicenseRegistry.licenses) { + if (!mounted) { + return; + } + assert(() { + dev.Timeline.timeSync('_initLicenses()', () { }, flow: dev.Flow.step(debugFlowId)); + return true; + }()); + final List paragraphs = + await SchedulerBinding.instance.scheduleTask>( + license.paragraphs.toList, + Priority.animation, + debugLabel: 'License', + ); + setState(() { + _licenses.add(const Padding( + padding: EdgeInsets.symmetric(vertical: 18.0), + child: Text( + '🍀‬', // That's U+1F340. Could also use U+2766 (❦) if U+1F340 doesn't work everywhere. + textAlign: TextAlign.center, + ), + )); + _licenses.add(Container( + decoration: const BoxDecoration( + border: Border(bottom: BorderSide(width: 0.0)) + ), + child: Text( + license.packages.join(', '), + style: const TextStyle(fontWeight: FontWeight.bold), + textAlign: TextAlign.center, + ), + )); + for (LicenseParagraph paragraph in paragraphs) { + if (paragraph.indent == LicenseParagraph.centeredIndent) { + _licenses.add(Padding( + padding: const EdgeInsets.only(top: 16.0), + child: Text( + paragraph.text, + style: const TextStyle(fontWeight: FontWeight.bold), + textAlign: TextAlign.center, + ), + )); + } else { + assert(paragraph.indent >= 0); + _licenses.add(Padding( + padding: EdgeInsetsDirectional.only(top: 8.0, start: 16.0 * paragraph.indent), + child: Text(paragraph.text), + )); + } + } + }); + } + setState(() { + _loaded = true; + }); + assert(() { + dev.Timeline.timeSync('Build scheduled', () { }, flow: dev.Flow.end(debugFlowId)); + return true; + }()); + } + + @override + Widget build(BuildContext context) { + assert(debugCheckHasMaterialLocalizations(context)); + final String name = widget.applicationName ?? _defaultApplicationName(context); + final String version = widget.applicationVersion ?? _defaultApplicationVersion(context); + final MaterialLocalizations localizations = MaterialLocalizations.of(context); + final List contents = [ + Text(name, style: Theme.of(context).textTheme.headline, textAlign: TextAlign.center), + Text(version, style: Theme.of(context).textTheme.body1, textAlign: TextAlign.center), + Container(height: 18.0), + Text(widget.applicationLegalese ?? '', style: Theme.of(context).textTheme.caption, textAlign: TextAlign.center), + Container(height: 18.0), + Text('Powered by Flutter', style: Theme.of(context).textTheme.body1, textAlign: TextAlign.center), + Container(height: 24.0), + ]; + contents.addAll(_licenses); + if (!_loaded) { + contents.add(const Padding( + padding: EdgeInsets.symmetric(vertical: 24.0), + child: Center( + child: CircularProgressIndicator(), + ), + )); + } + return Scaffold( + appBar: AppBar( + title: Text(localizations.licensesPageTitle), + ), + // All of the licenses page text is English. We don't want localized text + // or text direction. + body: Localizations.override( + locale: const Locale('en', 'US'), + context: context, + child: DefaultTextStyle( + style: Theme.of(context).textTheme.caption, + child: SafeArea( + bottom: false, + child: Scrollbar( + child: ListView( + padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 12.0), + children: contents, + ), + ), + ), + ), + ), + ); + } +} + +String _defaultApplicationName(BuildContext context) { + // This doesn't handle the case of the application's title dynamically + // changing. In theory, we should make Title expose the current application + // title using an InheritedWidget, and so forth. However, in practice, if + // someone really wants their application title to change dynamically, they + // can provide an explicit applicationName to the widgets defined in this + // file, instead of relying on the default. + final Title ancestorTitle = context.ancestorWidgetOfExactType(Title); + return ancestorTitle?.title ?? Platform.resolvedExecutable.split(Platform.pathSeparator).last; +} + +String _defaultApplicationVersion(BuildContext context) { + // TODO(ianh): Get this from the embedder somehow. + return ''; +} + +Widget _defaultApplicationIcon(BuildContext context) { + // TODO(ianh): Get this from the embedder somehow. + return null; +} diff --git a/lib/common/functions/feedback.dart b/lib/common/functions/feedback.dart new file mode 100644 index 0000000..9156358 --- /dev/null +++ b/lib/common/functions/feedback.dart @@ -0,0 +1,61 @@ +import 'package:flutter/material.dart'; + +final TextEditingController _feedbackController = TextEditingController(); + +void feedback( + BuildContext context) { + // flutter defined function + showDialog( + context: context, + builder: (BuildContext context) { + // return object of type Dialog + return AlertDialog( + title: new Text("Feedback"), + content: Column( + children: [ + Text("We would love to hear your thoughts."), + Text("\nThis section needs to be fixed."), + + Container( + margin: const EdgeInsets.fromLTRB(0, 20, 0, 0), + width: 200, + height: 70, + + child : new TextField( + controller: _feedbackController, + decoration: InputDecoration( + hintText: 'Enter your feedback here', + ), + // obscureText: true, + autocorrect: true, + maxLines: 20, + style: TextStyle( + fontSize: 18.0, + color: Colors.grey[800], + fontWeight: FontWeight.bold, + ), + onSubmitted: (_) { + submit(_feedbackController.text); + }, + ), + ), + new Container( + padding: const EdgeInsets.fromLTRB(0, 10, 0, 0), + child : new FlatButton( + child: new Text("Submit"), + onPressed: () { + submit(_feedbackController.text); + Navigator.of(context).pop(); + }, + ), + ), + ], + ), + ); + }, + ); +} + +void submit(String feedback) { + debugPrint(feedback); +} \ No newline at end of file diff --git a/lib/common/functions/showDialogTwoButtons.dart b/lib/common/functions/showDialogTwoButtons.dart new file mode 100644 index 0000000..03ab0d7 --- /dev/null +++ b/lib/common/functions/showDialogTwoButtons.dart @@ -0,0 +1,34 @@ +import 'package:flutter/material.dart'; + +void showDialogTwoButtons( + BuildContext context, String title, String message, String buttonLabel1, String buttonLabel2, Function action) { + // flutter defined function + showDialog( + context: context, + builder: (BuildContext context) { + // return object of type Dialog + return AlertDialog( + title: new Text(title), + content: new Text(message), + actions: [ + // usually buttons at the bottom of the dialog + + new FlatButton( + child: new Text(buttonLabel1), + onPressed: () { + Navigator.of(context).pop(); + }, + ), + + new FlatButton( + child: new Text(buttonLabel2), + onPressed: () { + action(context); + Navigator.of(context).pop(); + }, + ), + ], + ); + }, + ); +} diff --git a/lib/common/widgets/popupListView.dart b/lib/common/widgets/popupListView.dart index e4f0128..c0784c0 100644 --- a/lib/common/widgets/popupListView.dart +++ b/lib/common/widgets/popupListView.dart @@ -20,6 +20,7 @@ class PopupListView { 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(this.context).pop(); @@ -33,8 +34,6 @@ class PopupListView { return dialogOptionsList; } - - Widget dialog() { return new SimpleDialog( title: Text(listTitle), diff --git a/lib/main.dart b/lib/main.dart index 408b8b3..d98746d 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -4,6 +4,7 @@ import 'package:local_spend/pages/login_page.dart'; import 'package:local_spend/pages/receipt_page.dart'; import 'package:local_spend/pages/spash_screen.dart'; import 'package:local_spend/pages/about_screen.dart'; +import 'package:local_spend/pages/settings.dart'; import 'package:local_spend/config.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; @@ -25,7 +26,7 @@ class MyApp extends StatelessWidget { Locale("en") ], - title: "Splash and Token Authentication", + title: "Local Spend Tracker", theme: new ThemeData( primarySwatch: Colors.blueGrey, ), @@ -34,6 +35,7 @@ class MyApp extends StatelessWidget { "/LoginPage": (BuildContext context) => LoginPage(), "/ReceiptPage": (BuildContext context) => ReceiptPage(), "/AboutPage": (BuildContext context) => AboutPage(), + "/SettingsPage": (BuildContext context) => SettingsPage(), }, home: SplashScreen(), ); diff --git a/lib/pages/about_screen.dart b/lib/pages/about_screen.dart index 2c20bcd..ad64060 100644 --- a/lib/pages/about_screen.dart +++ b/lib/pages/about_screen.dart @@ -7,7 +7,10 @@ import 'package:flutter_linkify/flutter_linkify.dart'; class AboutPage extends StatefulWidget { @override - _HomePageState createState() => _HomePageState(); +// _HomePageState createState() => _HomePageState(); + State createState() { + return new _HomePageState(); + } } class _HomePageState extends State { @@ -17,6 +20,11 @@ class _HomePageState extends State { _saveCurrentRoute("/AboutPage"); } + @override + void dispose() { + super.dispose(); + } + _saveCurrentRoute(String lastRoute) async { SharedPreferences preferences = await SharedPreferences.getInstance(); await preferences.setString('LastScreenRoute', lastRoute); @@ -24,27 +32,36 @@ class _HomePageState extends State { @override Widget build(BuildContext context) { - return PlatformScaffold( - appBar: AppBar( - title: Text( - "About Page", - style: TextStyle( - fontSize: 30, - color: Colors.black), + return WillPopScope( + onWillPop: () { + if (Navigator.canPop(context)) { + Navigator.of(context).pushNamedAndRemoveUntil( + '/HomePage', (Route route) => true); + } else { + Navigator.of(context).pushReplacementNamed('/HomePage'); + } + }, + child: PlatformScaffold( + appBar: AppBar( + title: Text( + "About Page", + style: TextStyle( + fontSize: 20, + color: Colors.black, + ), + ), + leading: BackButton(), + centerTitle: true, + iconTheme: IconThemeData(color: Colors.black), ), - centerTitle: true, - iconTheme: IconThemeData(color: Colors.black), - elevation: Theme.of(context).platform == TargetPlatform.iOS ? 0.0 : 6.0, - ), - drawer: BasicDrawer(), - body: Container( - padding: EdgeInsets.all(32.0), - child: ListView( + body: Container( + padding: EdgeInsets.all(32.0), + child: ListView( children: [ InkWell( child: const Center(child: Text ('Pear Trading', - style: TextStyle( + style: TextStyle( fontSize: 20, color: Colors.blue, ), @@ -113,6 +130,7 @@ class _HomePageState extends State { ), ), ], + ), ), ), ); diff --git a/lib/pages/home_page.dart b/lib/pages/home_page.dart index 35dc251..18faacf 100644 --- a/lib/pages/home_page.dart +++ b/lib/pages/home_page.dart @@ -1,95 +1,62 @@ import 'package:flutter/material.dart'; -import 'package:local_spend/common/platform/platform_scaffold.dart'; -import 'package:local_spend/common/widgets/basic_drawer.dart'; -import 'package:shared_preferences/shared_preferences.dart'; -import 'package:flutter/services.dart'; -import 'package:local_spend/common/apifunctions/request_logout_api.dart'; -import 'package:local_spend/common/functions/get_token.dart'; -import 'package:flutter_fadein/flutter_fadein.dart'; -import 'package:local_spend/common/functions/logout.dart'; +import 'package:local_spend/pages/receipt_page.dart'; +import 'package:local_spend/pages/settings.dart'; + +class HomePage extends StatelessWidget { + static String _title = 'Text here'; + + @override + Widget build(BuildContext context) { + return MaterialApp( + title: _title, + home: HomePageWidget(), + ); + } +} + +class HomePageWidget extends StatefulWidget { + HomePageWidget({Key key}) : super(key: key); -class HomePage extends StatefulWidget { @override _HomePageState createState() => _HomePageState(); } -class _HomePageState extends State { - @override +class _HomePageState extends State { + int _selectedIndex = 0; + static const TextStyle optionStyle = + TextStyle(fontSize: 30, fontWeight: FontWeight.bold); + static List _widgetOptions = [ + ReceiptPage(), + SettingsPage() + ]; - void initState() { - super.initState(); - _saveCurrentRoute("/HomePage"); - } - - _saveCurrentRoute(String lastRoute) async { - SharedPreferences preferences = await SharedPreferences.getInstance(); - await preferences.setString('LastScreenRoute', lastRoute); + void _onItemTapped(int index) { + setState(() { + _selectedIndex = index; + }); } @override Widget build(BuildContext context) { - return PlatformScaffold( - appBar: AppBar( - title: Text( - "Navigation", - style: TextStyle(color: Colors.black), - ), - iconTheme: IconThemeData(color: Colors.black), - elevation: Theme.of(context).platform == TargetPlatform.iOS ? 0.0 : 6.0, + return Scaffold( + body: Center( + child: _widgetOptions.elementAt(_selectedIndex), ), - drawer: BasicDrawer(), - body: Container( - padding: EdgeInsets.fromLTRB(0, 15, 0, 0), - child: FadeIn( - duration: Duration(milliseconds: 500), - curve: Curves.easeIn, - - child: Column( - children: [ - - ListTile( - title: new Center( - child: new Text( - "Submit Receipt", - style: TextStyle(color: Colors.black, fontSize: 20.0), - textAlign: TextAlign.center, - ), - ), - onTap: () { - // debugPrint('$token'); - Navigator.of(context).pushNamed('/ReceiptPage'); - }, - ), - - ListTile( - title: new Center( - child: new Text( - "About", - style: TextStyle(color: Colors.black, fontSize: 20.0), - ), - ), - onTap: () { - SystemChannels.textInput.invokeMethod('TextInput.hide'); - Navigator.of(context).pushReplacementNamed('/AboutPage'); - }, - ), - - ListTile( - title: new Center( - child: new Text( - "Logout", - style: TextStyle(color: Colors.black, fontSize: 20.0), - ), - ), - onTap: () { - logout(context); - }, - - ), - ], + bottomNavigationBar: BottomNavigationBar( + items: const [ + BottomNavigationBarItem( + icon: Icon(Icons.receipt), + title: Text('Submit Receipt'), ), - ), + BottomNavigationBarItem( + icon: Icon(Icons.settings), + title: Text('Settings'), + ), + ], + currentIndex: _selectedIndex, + selectedItemColor: Colors.blue[400], + onTap: _onItemTapped, ), ); } -} +} \ No newline at end of file diff --git a/lib/pages/login_page.dart b/lib/pages/login_page.dart index e50c9f8..f0bfc48 100644 --- a/lib/pages/login_page.dart +++ b/lib/pages/login_page.dart @@ -19,8 +19,8 @@ class LoginPage extends StatefulWidget { } class LoginPageState extends State { - final TextEditingController _emailController = TextEditingController(/*text: 'test@example.com'*/); // remove - final TextEditingController _passwordController = TextEditingController(/*text: 'abc123'*/); // remove + final TextEditingController _emailController = TextEditingController(text: 'test@example.com'); // remove + final TextEditingController _passwordController = TextEditingController(text: 'abc123'); // remove bool _saveLoginDetails = true; // I am extremely sorry for the placement of this variable // it will be fixed soon I promise diff --git a/lib/pages/receipt_page.dart b/lib/pages/receipt_page.dart index 7b13c4c..3715904 100644 --- a/lib/pages/receipt_page.dart +++ b/lib/pages/receipt_page.dart @@ -9,9 +9,11 @@ import 'package:local_spend/common/widgets/basic_drawer.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:url_launcher/url_launcher.dart'; import 'package:intl/intl.dart'; +import 'package:local_spend/pages/settings.dart'; import 'package:datetime_picker_formfield/datetime_picker_formfield.dart'; import 'package:local_spend/common/apifunctions/find_organisations.dart'; import 'package:local_spend/common/widgets/popupListView.dart'; +import 'package:local_spend/common/widgets/labeled_checkbox.dart'; const URL = "https://flutter.io/"; const demonstration = false; @@ -30,6 +32,7 @@ class ReceiptPageState extends State { final TextEditingController _recurringController = TextEditingController(); final TextEditingController _typeController = TextEditingController(); final TextEditingController _orgController = TextEditingController(); + bool _recurringCheckbox = false; // have mercy, this will be removed. sorry for this variable's placement... FocusNode focusNode; // added so focus can move automatically @@ -174,223 +177,310 @@ class ReceiptPageState extends State { @override Widget build(BuildContext context) { - var drawer = Drawer(); return WillPopScope( onWillPop: () { if (Navigator.canPop(context)) { Navigator.of(context).pushNamedAndRemoveUntil( - '/HomePage', (Route route) => false); + '/LoginPage', (Route route) => false); } else { - Navigator.of(context).pushReplacementNamed('/HomePage'); + Navigator.of(context).pushReplacementNamed('/LoginPage'); } }, child: PlatformScaffold( - drawer: BasicDrawer(), + appBar: AppBar( + backgroundColor: Colors.blue[400], title: Text( "Submit Receipt", style: TextStyle( - fontSize: 30.0, + fontSize: 20, color: Colors.black, ), ), +// leading: BackButton(), centerTitle: true, iconTheme: IconThemeData(color: Colors.black), ), + body: Container( - child: Padding( - padding: EdgeInsets.fromLTRB(30.0, 0.0, 30.0, 0.0), - child: ListView( - children: [ -// Container( -// alignment: Alignment.topCenter, -// child: Padding( -// padding: EdgeInsets.fromLTRB(0.0, 40.0, 0.0, 15.0), -// child: Text( -// "Required fields are in bold", -// style: TextStyle(fontSize: 20.0, color: Colors.black), -// ), -// )), - Padding( - padding: EdgeInsets.fromLTRB(0.0,25,0.0,0.0), - child : Text( - "Time of Transaction", - style: TextStyle( - fontSize: 18.0, - color: Colors.black, - fontWeight: FontWeight.bold, - ), + padding: EdgeInsets.fromLTRB(30.0, 0.0, 30.0, 0.0), + child: ListView( + children: [ + Padding( + padding: EdgeInsets.fromLTRB(0.0,25,0.0,0.0), + child : Text( + "Time of Transaction", + style: TextStyle( + fontSize: 18.0, + color: Colors.black, + fontWeight: FontWeight.bold, ), ), - DateTimePickerFormField( - inputType: InputType.both, - format: DateFormat("dd/MM/yyyy 'at' hh:mm"), - editable: true, - controller: _timeController, + ), + DateTimePickerFormField( + inputType: InputType.both, + format: DateFormat("dd/MM/yyyy 'at' hh:mm"), + editable: true, + controller: _timeController, + decoration: InputDecoration( + labelText: 'Date/Time of Transaction', hasFloatingPlaceholder: false), + onChanged: (dt) => setState(() => date = dt), + ), + Padding( + padding: EdgeInsets.fromLTRB(0.0,25,0.0,0.0), + child: Text( + "Amount", + style: TextStyle( + fontSize: 18.0, + color: Colors.black, + fontWeight: FontWeight.bold, + ), + ), + ), + Padding( + padding: EdgeInsets.fromLTRB(0.0, 5.0, 0.0, 0.0), + child: TextField( + controller: _amountController, decoration: InputDecoration( - labelText: 'Date/Time of Transaction', hasFloatingPlaceholder: false), - onChanged: (dt) => setState(() => date = dt), - ), - Padding( - padding: EdgeInsets.fromLTRB(0.0,25,0.0,0.0), - child: Text( - "Amount", - style: TextStyle( - fontSize: 18.0, - color: Colors.black, - fontWeight: FontWeight.bold, - ), + hintText: 'Value in £', ), - ), - Padding( - padding: EdgeInsets.fromLTRB(0.0, 5.0, 0.0, 0.0), - child: TextField( - controller: _amountController, - decoration: InputDecoration( - hintText: 'Value in £', - ), // obscureText: true, - autocorrect: false, - keyboardType: TextInputType.number, - style: TextStyle( - fontSize: 18.0, - color: Colors.grey[800], - fontWeight: FontWeight.bold, - ), - onSubmitted: (_) { - FocusScope.of(context).requestFocus(focusNode); + autocorrect: false, + keyboardType: TextInputType.number, + style: TextStyle( + fontSize: 18.0, + color: Colors.grey[800], + fontWeight: FontWeight.bold, + ), + onSubmitted: (_) { + FocusScope.of(context).requestFocus(focusNode); // submitReceipt(_amountController.text, _timeController.text); - }, - ), + }, ), + ), - Padding( - padding: EdgeInsets.fromLTRB(0.0,25,0.0,0.0), + Padding( + padding: EdgeInsets.fromLTRB(0.0,25,0.0,0.0), - child : Container ( - height: 22, // this should be the same height as text + child : Container ( + height: 22, // this should be the same height as text - child : ListView( - scrollDirection: Axis.horizontal, - children: [ + child : ListView( + scrollDirection: Axis.horizontal, + children: [ - Container( - child: Text( - "Organization Name", - style: TextStyle( - fontSize: 18.0, - color: Colors.black, - fontWeight: FontWeight.bold, - ), + Container( + child: Text( + "Organization Name", + style: TextStyle( + fontSize: 18.0, + color: Colors.black, + fontWeight: FontWeight.bold, ), ), + ), - Container( - child : Padding( - padding: EdgeInsets.fromLTRB(5,0,0,4), // sorry about hardcoded constraints - child: FlatButton( - onPressed: () { - var organisations = findOrganisations(_orgController.text); - // some tasty async stuff here yum yum - // and a pretty little dialog too yay - var choice = organisations.then((data) => listOrganisations(data, context)); + Container( + child : Padding( + padding: EdgeInsets.fromLTRB(5,0,0,4), // sorry about hardcoded constraints + child: FlatButton( + onPressed: () { + var organisations = findOrganisations(_orgController.text); + // some tasty async stuff here yum yum + // and a pretty little dialog too yay (doesn't work) + var choice = organisations.then((data) => listOrganisations(data, context)); - // choice is a Future - }, - child: Text("Find", - style: - TextStyle(color: Colors.blue, fontSize: 18.0)), - ), - ), - ) + // choice is a Future + }, + child: Text("Find", + style: + TextStyle(color: Colors.blue, fontSize: 18.0)), + ), + ), + ) - ], - ), + ], ), ), + ), - Padding( - padding: EdgeInsets.fromLTRB(0.0, 5.0, 0.0, 0.0), - child: TextField( - controller: _orgController, - focusNode: focusNode, - decoration: InputDecoration( - hintText: 'Eg. Pear Trading', - ), + Padding( + padding: EdgeInsets.fromLTRB(0.0, 5.0, 0.0, 0.0), + child: TextField( + controller: _orgController, + focusNode: focusNode, + decoration: InputDecoration( + hintText: 'Eg. Pear Trading', + ), // obscureText: true, - autocorrect: true, - style: TextStyle( - fontSize: 18.0, - color: Colors.grey[800], - fontWeight: FontWeight.bold, - ), - onSubmitted: (_) { - submitReceipt(_amountController.text, - _timeController.text, _orgController.text); - // TODO: make sure organisation is valid - // TODO: Add 'find organisation' button which displays a dialog to, well, find the organisation's address or manual entry + autocorrect: true, + style: TextStyle( + fontSize: 18.0, + color: Colors.grey[800], + fontWeight: FontWeight.bold, + ), + onSubmitted: (_) { + submitReceipt(_amountController.text, + _timeController.text, _orgController.text); + // TODO: make sure organisation is valid + // TODO: Add 'find organisation' button which displays a dialog to, well, find the organisation's address or manual entry + }, + ), + ), + + Padding( + padding: EdgeInsets.fromLTRB(0.0,25,0.0,0.0), + + child : Container ( + height: 18, + + child : ListView( + scrollDirection: Axis.horizontal, + + children: [ + Container( + child: Text( + "Essential", + style: TextStyle( + fontSize: 18.0, + color: Colors.black, + ), + ), + ), + + Container( + child : Padding( + padding: EdgeInsets.fromLTRB(20.0, 0.0, 0, 0), + + child: Checkbox(value: + _essentialController.text.toLowerCase() == 'true', + onChanged: (bool newValue) { + setState(() { + _essentialController.text = + convertBoolToString(newValue); + }); + }), + ), + ), + + ], + ), + ), + ), + + Padding( + padding: EdgeInsets.fromLTRB(0.0,25,0.0,0.0), + + child : Container ( + height: 27, +// width: 400, + + child : ListView( + scrollDirection: Axis.horizontal, + + children: [ + Container( + child: Text( + "Recurring", + style: TextStyle( + fontSize: 18.0, + color: Colors.black, + ), + ), + ), + + Container( + child : Padding( + padding: EdgeInsets.fromLTRB(15.0, 0.0, 0, 4), + + child: Checkbox(value: + _essentialController.text.toLowerCase() != "none" || + _essentialController.text.toLowerCase() != 'false', + onChanged: (bool newValue) { + setState(() { + var options = new List(7); + options[0] = "Daily"; + options[1] = "Weekly"; + options[2] = "Fortnightly"; + options[3] = "Monthly"; + options[5] = "Quarterly"; + options[6] = "Yearly"; + + var popupListView = new PopupListView( + context, options, "Recurring..."); + + var dialog = popupListView.dialog(); + + showDialog( + context: context, + builder: (BuildContext context) { + return dialog; + }, + ); + + print(popupListView.result); +// _recurringController.text = +// popupListView.result; + }); + }), + ), + ), + + Container( + padding: EdgeInsets.fromLTRB(10, 2, 0, 0), + child: Text( + convertBoolToString(_essentialController.text.toLowerCase() != "none" || + _essentialController.text.toLowerCase() != 'false'), + style: TextStyle( + fontSize: 16.0, + color: Colors.grey, + ), + ), + ), + ], + ), + ), + ), + + +// var options = new List(1); +// options[0] = "Weekly"; +// +// var popupListView = new PopupListView(context, options, "Recurring..."); +// +// var dialog = popupListView.dialog(); +// +// showDialog( +// context: context, +// builder: (BuildContext context) { +// return dialog; +// }, +// ); +// +// print(popupListView.result); +// _recurringController.text = popupListView.result; +// +// setState(() { +// _recurringController.text = +// convertBoolToString(newValue); + + + Padding( + padding: EdgeInsets.fromLTRB(0.0, 40.0, 0.0, 0.0), + child: Container( + height: 65.0, + child: RaisedButton( + onPressed: () { + submitReceipt(_amountController.text, _timeController.text, _orgController.text); }, + child: Text("SUBMIT", + style: + TextStyle(color: Colors.white, fontSize: 22.0)), + color: Colors.blue, ), ), - - Padding( - padding: EdgeInsets.fromLTRB(0.0,25,0.0,0.0), - - child : Container ( - height: 18, - - child : ListView( - scrollDirection: Axis.horizontal, - - children: [ - Container( - child: Text( - "Essential Purchase", - style: TextStyle( - fontSize: 18.0, - color: Colors.black, - ), - ), - ), - - Container( - child : Padding( - padding: EdgeInsets.fromLTRB(20.0, 0.0, 0, 0), - - child: Checkbox(value: - _essentialController.text.toLowerCase() == 'true', - onChanged: (bool newValue) { - setState(() { - _essentialController.text = - convertBoolToString(newValue); - }); - }), - ), - ), - - ], - ), - ), - ), - - - Padding( - padding: EdgeInsets.fromLTRB(0.0, 70.0, 0.0, 0.0), - child: Container( - height: 65.0, - child: RaisedButton( - onPressed: () { - submitReceipt(_amountController.text, _timeController.text, _orgController.text); - }, - child: Text("SUBMIT", - style: - TextStyle(color: Colors.white, fontSize: 22.0)), - color: Colors.blue, - ), - ), - ), - ], - ), + ), + ], ), ), ), diff --git a/lib/pages/settings.dart b/lib/pages/settings.dart new file mode 100644 index 0000000..ebde997 --- /dev/null +++ b/lib/pages/settings.dart @@ -0,0 +1,180 @@ +import 'dart:async'; + +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'; +import 'package:local_spend/common/functions/feedback.dart'; + +const URL = "https://flutter.io/"; +const demonstration = false; + +class SettingsPage extends StatefulWidget { + @override + State createState() { + return new SettingsPageState(); + } +} + +class SettingsPageState extends State { + FocusNode focusNode; // added so focus can move automatically + + DateTime date; + + @override + void initState() { + super.initState(); + _saveCurrentRoute("/SettingsPageState"); + + focusNode = FocusNode(); + } + + @override + void dispose() { + super.dispose(); + + focusNode.dispose(); //disposes focus node when form disposed + } + + _saveCurrentRoute(String lastRoute) async { + SharedPreferences preferences = await SharedPreferences.getInstance(); + await preferences.setString('LastPageRoute', lastRoute); + } + + @override + Widget build(BuildContext context) { + return WillPopScope( + onWillPop: () { + if (Navigator.canPop(context)) { + Navigator.of(context).pushNamedAndRemoveUntil( + '/LoginPage', (Route route) => false); + } else { + Navigator.of(context).pushReplacementNamed('/LoginPage'); + } + }, + child: PlatformScaffold( + + appBar: AppBar( + backgroundColor: Colors.blue[400], + title: Text( + "Settings", + style: TextStyle( + fontSize: 20, + color: Colors.black, + ), + ), +// leading: BackButton(), + centerTitle: true, + iconTheme: IconThemeData(color: Colors.black), + ), + + body: Container( + padding: EdgeInsets.fromLTRB(30.0, 0.0, 30.0, 0.0), + child: ListView( + children: [ + + Container( + padding: EdgeInsets.fromLTRB(0.0,25,0.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(10, 15, 0, 0), +// child: Text("helloooo"), +// ), + + Padding( + padding: EdgeInsets.fromLTRB(0.0, 25.0, 0.0, 0.0), + child: Container( + height: 65.0, + child: RaisedButton( + onPressed: () { + + custom.showAboutDialog( + context: context, + applicationIcon: new Icon(Icons.receipt), + applicationName: "Local Spend Tracker", + children: [ + Text("Pear Trading is a commerce company designed to register and monitor money circulating in the local economy."), + Text("\nContact at test@example.com or +44(0)1524 64544"), + + Padding( + padding: EdgeInsets.fromLTRB(0,20,0,0), + child: InkWell( + child: Text + ('Developed by Shadowcat Systems', + style: TextStyle( + color: Colors.blue, + ), + ), + onTap: () => launch('https://shadow.cat/') + ), + ), + ], + ); + + }, + child: Text("ABOUT", + style: + TextStyle(color: Colors.white, fontSize: 22.0)), + color: Colors.blue, + ), + ), + ), + + Padding( + padding: EdgeInsets.fromLTRB(0.0, 20.0, 0.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(0.0, 20.0, 0.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, + ), + ), + ), + ], + ), + ), + ), + ); + } +}