From 43b43cd2d06f4244a64c44cac0e7c50219121dcd Mon Sep 17 00:00:00 2001 From: Winfried Kappeler Date: Mon, 28 Sep 2020 09:25:24 +0200 Subject: [PATCH] Start --- .gitignore | 47 +++++++++++ .metadata | 10 +++ LICENSE | 121 +++++++++++++++++++++++++++ README.md | 16 ++++ lib/app.dart | 43 ++++++++++ lib/flutter_bones.dart | 14 ++++ lib/main.dart | 7 ++ lib/src/helper/settings.dart | 26 ++++++ lib/src/helper/validators.dart | 69 +++++++++++++++ lib/src/model/field_model.dart | 85 +++++++++++++++++++ lib/src/model/model_base.dart | 90 ++++++++++++++++++++ lib/src/model/model_types.dart | 4 + lib/src/model/module/user_model.dart | 43 ++++++++++ lib/src/model/module_model.dart | 24 ++++++ lib/src/model/page_model.dart | 41 +++++++++ lib/src/model/section_model.dart | 41 +++++++++ lib/src/model/widget_model.dart | 75 +++++++++++++++++ lib/src/page/login_page.dart | 67 +++++++++++++++ lib/src/page/page_data.dart | 15 ++++ lib/src/page/role_page.dart | 64 ++++++++++++++ lib/src/page/user_page.dart | 69 +++++++++++++++ lib/src/private/bappbar.dart | 6 ++ lib/src/private/bdrawer.dart | 76 +++++++++++++++++ lib/src/private/bsettings.dart | 30 +++++++ lib/src/widget/simpleform.dart | 23 +++++ pubspec.yaml | 83 ++++++++++++++++++ test/helpers/settings_test.dart | 23 +++++ test/helpers/validators_test.dart | 53 ++++++++++++ test/widget_test.dart | 33 ++++++++ 29 files changed, 1298 insertions(+) create mode 100644 .gitignore create mode 100644 .metadata create mode 100644 LICENSE create mode 100644 README.md create mode 100644 lib/app.dart create mode 100644 lib/flutter_bones.dart create mode 100644 lib/main.dart create mode 100644 lib/src/helper/settings.dart create mode 100644 lib/src/helper/validators.dart create mode 100644 lib/src/model/field_model.dart create mode 100644 lib/src/model/model_base.dart create mode 100644 lib/src/model/model_types.dart create mode 100644 lib/src/model/module/user_model.dart create mode 100644 lib/src/model/module_model.dart create mode 100644 lib/src/model/page_model.dart create mode 100644 lib/src/model/section_model.dart create mode 100644 lib/src/model/widget_model.dart create mode 100644 lib/src/page/login_page.dart create mode 100644 lib/src/page/page_data.dart create mode 100644 lib/src/page/role_page.dart create mode 100644 lib/src/page/user_page.dart create mode 100644 lib/src/private/bappbar.dart create mode 100644 lib/src/private/bdrawer.dart create mode 100644 lib/src/private/bsettings.dart create mode 100644 lib/src/widget/simpleform.dart create mode 100644 pubspec.yaml create mode 100644 test/helpers/settings_test.dart create mode 100644 test/helpers/validators_test.dart create mode 100644 test/widget_test.dart diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..95b551c --- /dev/null +++ b/.gitignore @@ -0,0 +1,47 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +pubspec.lock + +# wk: @ToDo: nicht sicher +android/ +ios/ +web/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.packages +.pub-cache/ +.pub/ +/build/ + +# Web related +lib/generated_plugin_registrant.dart + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json diff --git a/.metadata b/.metadata new file mode 100644 index 0000000..837b938 --- /dev/null +++ b/.metadata @@ -0,0 +1,10 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: cba91c97d03ac8aae66317097f9e8aeade86257f + channel: master + +project_type: app diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..0e259d4 --- /dev/null +++ b/LICENSE @@ -0,0 +1,121 @@ +Creative Commons Legal Code + +CC0 1.0 Universal + + CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE + LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN + ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS + INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES + REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS + PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM + THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED + HEREUNDER. + +Statement of Purpose + +The laws of most jurisdictions throughout the world automatically confer +exclusive Copyright and Related Rights (defined below) upon the creator +and subsequent owner(s) (each and all, an "owner") of an original work of +authorship and/or a database (each, a "Work"). + +Certain owners wish to permanently relinquish those rights to a Work for +the purpose of contributing to a commons of creative, cultural and +scientific works ("Commons") that the public can reliably and without fear +of later claims of infringement build upon, modify, incorporate in other +works, reuse and redistribute as freely as possible in any form whatsoever +and for any purposes, including without limitation commercial purposes. +These owners may contribute to the Commons to promote the ideal of a free +culture and the further production of creative, cultural and scientific +works, or to gain reputation or greater distribution for their Work in +part through the use and efforts of others. + +For these and/or other purposes and motivations, and without any +expectation of additional consideration or compensation, the person +associating CC0 with a Work (the "Affirmer"), to the extent that he or she +is an owner of Copyright and Related Rights in the Work, voluntarily +elects to apply CC0 to the Work and publicly distribute the Work under its +terms, with knowledge of his or her Copyright and Related Rights in the +Work and the meaning and intended legal effect of CC0 on those rights. + +1. Copyright and Related Rights. A Work made available under CC0 may be +protected by copyright and related or neighboring rights ("Copyright and +Related Rights"). Copyright and Related Rights include, but are not +limited to, the following: + + i. the right to reproduce, adapt, distribute, perform, display, + communicate, and translate a Work; + ii. moral rights retained by the original author(s) and/or performer(s); +iii. publicity and privacy rights pertaining to a person's image or + likeness depicted in a Work; + iv. rights protecting against unfair competition in regards to a Work, + subject to the limitations in paragraph 4(a), below; + v. rights protecting the extraction, dissemination, use and reuse of data + in a Work; + vi. database rights (such as those arising under Directive 96/9/EC of the + European Parliament and of the Council of 11 March 1996 on the legal + protection of databases, and under any national implementation + thereof, including any amended or successor version of such + directive); and +vii. other similar, equivalent or corresponding rights throughout the + world based on applicable law or treaty, and any national + implementations thereof. + +2. Waiver. To the greatest extent permitted by, but not in contravention +of, applicable law, Affirmer hereby overtly, fully, permanently, +irrevocably and unconditionally waives, abandons, and surrenders all of +Affirmer's Copyright and Related Rights and associated claims and causes +of action, whether now known or unknown (including existing as well as +future claims and causes of action), in the Work (i) in all territories +worldwide, (ii) for the maximum duration provided by applicable law or +treaty (including future time extensions), (iii) in any current or future +medium and for any number of copies, and (iv) for any purpose whatsoever, +including without limitation commercial, advertising or promotional +purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each +member of the public at large and to the detriment of Affirmer's heirs and +successors, fully intending that such Waiver shall not be subject to +revocation, rescission, cancellation, termination, or any other legal or +equitable action to disrupt the quiet enjoyment of the Work by the public +as contemplated by Affirmer's express Statement of Purpose. + +3. Public License Fallback. Should any part of the Waiver for any reason +be judged legally invalid or ineffective under applicable law, then the +Waiver shall be preserved to the maximum extent permitted taking into +account Affirmer's express Statement of Purpose. In addition, to the +extent the Waiver is so judged Affirmer hereby grants to each affected +person a royalty-free, non transferable, non sublicensable, non exclusive, +irrevocable and unconditional license to exercise Affirmer's Copyright and +Related Rights in the Work (i) in all territories worldwide, (ii) for the +maximum duration provided by applicable law or treaty (including future +time extensions), (iii) in any current or future medium and for any number +of copies, and (iv) for any purpose whatsoever, including without +limitation commercial, advertising or promotional purposes (the +"License"). The License shall be deemed effective as of the date CC0 was +applied by Affirmer to the Work. Should any part of the License for any +reason be judged legally invalid or ineffective under applicable law, such +partial invalidity or ineffectiveness shall not invalidate the remainder +of the License, and in such case Affirmer hereby affirms that he or she +will not (i) exercise any of his or her remaining Copyright and Related +Rights in the Work or (ii) assert any associated claims and causes of +action with respect to the Work, in either case contrary to Affirmer's +express Statement of Purpose. + +4. Limitations and Disclaimers. + + a. No trademark or patent rights held by Affirmer are waived, abandoned, + surrendered, licensed or otherwise affected by this document. + b. Affirmer offers the Work as-is and makes no representations or + warranties of any kind concerning the Work, express, implied, + statutory or otherwise, including without limitation warranties of + title, merchantability, fitness for a particular purpose, non + infringement, or the absence of latent or other defects, accuracy, or + the present or absence of errors, whether or not discoverable, all to + the greatest extent permissible under applicable law. + c. Affirmer disclaims responsibility for clearing rights of other persons + that may apply to the Work or any use thereof, including without + limitation any person's Copyright and Related Rights in the Work. + Further, Affirmer disclaims responsibility for obtaining any necessary + consents, permissions or other rights required for any use of the + Work. + d. Affirmer understands and acknowledges that Creative Commons is not a + party to this document and has no duty or obligation with respect to + this CC0 or use of the Work. diff --git a/README.md b/README.md new file mode 100644 index 0000000..56cfb39 --- /dev/null +++ b/README.md @@ -0,0 +1,16 @@ +# bones + +Sammlung von Widgets und Bibliotheksfunktionen als Basis für andere Projekte + +## Getting Started + +This project is a starting point for a Flutter application. + +A few resources to get you started if this is your first Flutter project: + +- [Lab: Write your first Flutter app](https://flutter.dev/docs/get-started/codelab) +- [Cookbook: Useful Flutter samples](https://flutter.dev/docs/cookbook) + +For help getting started with Flutter, view our +[online documentation](https://flutter.dev/docs), which offers tutorials, +samples, guidance on mobile development, and a full API reference. diff --git a/lib/app.dart b/lib/app.dart new file mode 100644 index 0000000..d947f36 --- /dev/null +++ b/lib/app.dart @@ -0,0 +1,43 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bones/flutter_bones.dart'; +import 'package:flutter_bones/src/private/bsettings.dart'; + +class BoneApp extends StatefulWidget { + @override + BoneAppState createState() => BoneAppState(); +} + +class BoneAppState extends State { + + @override + Widget build(BuildContext context) { + return MaterialApp( + title: 'Virtuelle Fragestunde', + theme: ThemeData( + primarySwatch: Colors.blue, + visualDensity: VisualDensity.adaptivePlatformDensity, + ), + initialRoute: '/login', + onGenerateRoute: _getRoute, + ); + } + +} + +Route _getRoute(RouteSettings settings) { + MaterialPageRoute route; + if (settings.name == '/login') { + route = MaterialPageRoute( + settings: settings, + builder: (BuildContext context) => LoginPage(BSettings.instance.pageData), + fullscreenDialog: false, + ); + } else { + route = MaterialPageRoute( + settings: settings, + builder: (BuildContext context) => LoginPage(BSettings.instance.pageData), + fullscreenDialog: false, + ); + } + return route; +} diff --git a/lib/flutter_bones.dart b/lib/flutter_bones.dart new file mode 100644 index 0000000..82ea4cd --- /dev/null +++ b/lib/flutter_bones.dart @@ -0,0 +1,14 @@ +export 'src/widget/simpleform.dart'; +export 'src/helper/settings.dart'; +export 'src/helper/validators.dart'; +export 'src/page/page_data.dart'; +export 'src/page/login_page.dart'; +export 'src/page/role_page.dart'; +export 'src/page/user_page.dart'; +export 'src/model/model_types.dart'; +export 'src/model/model_base.dart'; +export 'src/model/widget_model.dart'; +export 'src/model/field_model.dart'; +export 'src/model/module_model.dart'; +export 'src/model/section_model.dart'; +export 'src/model/page_model.dart'; \ No newline at end of file diff --git a/lib/main.dart b/lib/main.dart new file mode 100644 index 0000000..7f57697 --- /dev/null +++ b/lib/main.dart @@ -0,0 +1,7 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bones/app.dart'; + +void main() { + runApp(BoneApp()); +} + diff --git a/lib/src/helper/settings.dart b/lib/src/helper/settings.dart new file mode 100644 index 0000000..59b75ca --- /dev/null +++ b/lib/src/helper/settings.dart @@ -0,0 +1,26 @@ +import 'package:flutter/material.dart'; + +class Settings { + static Locale locale = Locale('US', 'en'); + + /// Sets the locale code. + /// [locale] the info about localisation + /// Finding [locale] of an app: @see https://github.com/flutter/website/blob/master/examples/internationalization/minimal/lib/main.dart + static setLocale(locale) => Settings.locale = locale; + static setLocaleByNames({String country='US', String language='en'}) => Settings.locale = Locale(country, language); + /// Translates a [text] with a given translation [map]. + /// Structure of the [map]: { : { : translation } } + /// return: [text] if no translation has been found, otherwise: the translation from the [map] + static String translate(String text, Map> map, {Map placeholders}){ + var rc = text; + if (map.containsKey(text) && map[text].containsKey(locale.languageCode)){ + rc = map[text][locale.languageCode]; + } + if (placeholders != null){ + placeholders.keys.forEach((key) { + rc = rc.replaceAll('%{$key}', placeholders[key]); + }); + } + return rc; + } +} \ No newline at end of file diff --git a/lib/src/helper/validators.dart b/lib/src/helper/validators.dart new file mode 100644 index 0000000..fee37c3 --- /dev/null +++ b/lib/src/helper/validators.dart @@ -0,0 +1,69 @@ +import 'package:dart_bones/dart_bones.dart'; +import 'package:flutter_bones/flutter_bones.dart'; +import 'package:meta/meta.dart'; + +/// Tests whether [input] is an valid email address. +/// returns null if not empty, otherwise an error message +String checkEmail(String input) => Validation.isEmail(input) + ? null + : _vt('Not an email address: %{0} Example: joe@example.com', {'0': input}); + +/// returns null if not empty, otherwise an error message +String checkInt(String input) => + Validation.isInt(input) ? null : _vt('Not a number: %{0}', {'0': input}); + +/// Validates an [input] with many [validators]. +String checkMany(String input, List validators) { + String rc; + for (var item in validators) { + final rc2 = item(input); + if (rc2 != null) { + rc = rc2; + break; + } + } + return rc; +} + +/// returns null if not empty, otherwise an error message +String checkNat(String input) => Validation.isNat(input) + ? null + : _vt('Not a not negative number: %{0}', {'0': input}); + +// Tests whether [input] is a natural number (>= 0). +/// Tests whether [input] is not empty. +/// returns null if not empty, otherwise an error message +String checkNotEmpty(String input) { + return input.isEmpty ? _vt('Please fill in') : null; +} + +// Tests whether [input] is an integer (>= 0). +/// Tests whether [input] is an valid phone number. +/// returns null if not empty, otherwise an error message +String checkPhoneNumber(String input) => Validation.isPhoneNumber(input) + ? null + : _vt('Not a phone number: %{0} Examples: "089-123452 "+49-89-12345"', + {'0': input}); + +String _vt(String key, [Map placeholders]) => + Settings.translate(key, ValidatorTranslations.translations, + placeholders: placeholders); + +@protected +class ValidatorTranslations { + static final translations = { + 'Please fill in': { + 'de': 'Bitte ausfüllen', + }, + 'Not a number: %{0}': { + 'de': 'Keine Zahl: %{0}', + }, + 'Not an email address: %{0} Example: joe@example.com': { + 'de': 'Keine gültige EMailadresse: %{0} Beispiel: joe@example.com', + }, + 'Not a phone number: %{0} Examples: "089-123452 "+49-89-12345"': { + 'de': + 'Keine gültige Telefonnummer: %{0} Beispiele: "089-123452 "+49-89-12345"', + }, + }; +} diff --git a/lib/src/model/field_model.dart b/lib/src/model/field_model.dart new file mode 100644 index 0000000..adb0092 --- /dev/null +++ b/lib/src/model/field_model.dart @@ -0,0 +1,85 @@ +import 'package:flutter/material.dart'; +import 'package:dart_bones/dart_bones.dart'; +import 'package:flutter_bones/flutter_bones.dart'; +import 'package:meta/meta.dart'; + +class FieldModel extends ModelBase { + static final regExprOptions = RegExp(r'^(undef|readonly|disabled|password|required}'); + final SectionModel parent; + String name; + String label; + String toolTip; + FieldModelType fieldModelType; + DataType dataType; + int maxSize; + int rows; + List options; + FormFieldValidator validator; + FormFieldSetter onSaved; + + final Map map; + + FieldModel(this.parent, this.map, BaseLogger logger) : super(logger); + + /// Returns the name including the names of the parent + @override + String fullName() => '${parent.fullName()}.$name'; + + /// Parses the map and stores the data in the instance. + void parse() { + checkSuperfluous(map, 'name label toolTip fieldModelType dataType maxSize rows options'.split(' ')); + name = parseString('name', map, required: true); + label = parseString('label', map, required: false); + toolTip = parseString('toolTip', map, required: false); + fieldModelType = parseEnum( + 'fieldModelType', map, FieldModelType.values); + dataType = parseEnum( + 'dataType', map, DataType.values); + maxSize = parseInt('maxSize', map, required: false); + rows = parseInt('rows', map, required: false); + options = parseOptions('options', map); + } + + /// Tests the validity of the entries in [optionList] + /// Errors will be logged. + bool checkOptions() { + bool rc = false; + options.forEach((element) { + if (regExprOptions.firstMatch(element) == null) { + logger.error('unbekannte Feldoption $element in ${fullName()}'); + rc = true; + } + }); + return rc; + } + /// Returns the widget representing the field. + Widget widget(Key formKey){ + Widget rc; + switch(fieldModelType){ + case FieldModelType.text: + rc = TextFormField(key: formKey, + validator: validator, + decoration: InputDecoration(labelText: label), + onSaved: onSaved, + maxLength: maxSize, + maxLines: rows, + readOnly: options.contains('readonly'), + obscureText: options.contains('password'), + ); + break; + case FieldModelType.checkbox: + rc = Checkbox(key: formKey, + decoration: InputDecoration(labelText: label), + onSaved: onSaved, + maxLength: maxSize, + maxLines: rows, + readOnly: options.contains('readonly'), + obscureText: options.contains('password'), + ); + break; + } + return rc; + } +} + +enum FieldModelType { checkbox, combobox, image, text, } diff --git a/lib/src/model/model_base.dart b/lib/src/model/model_base.dart new file mode 100644 index 0000000..ff06252 --- /dev/null +++ b/lib/src/model/model_base.dart @@ -0,0 +1,90 @@ +import 'package:flutter/material.dart'; +import 'package:dart_bones/dart_bones.dart'; +import 'package:flutter_bones/flutter_bones.dart'; +import 'package:meta/meta.dart'; + +/// Base class of all models. +abstract class ModelBase { + BaseLogger logger; + ModelBase(this.logger); + + /// Tests a [map] for superfluous [keys]. + /// Keys of the [map] that are not listed in [keys] will be logged as errors. + /// Keys that do not have the type String are logged as errors. + void checkSuperfluous(Map map, List keys) { + map.keys.forEach((element) { + if (element.runtimeType != String) { + logger.error('wrong key type ${element.runtimeType} in ${fullName()}'); + } else if (!keys.contains(map[element])) { + logger.error('wrong key ${element.runtimeType} in ${fullName()}'); + } + }); + } + + /// Returns the name including the names of the parent + String fullName(); + + /// Fetches an entry from a map addressed by a [key]. + /// An error is logged if [required] is true and the map does not contain the key. + T parseEnum(String key, Map map, List values, + {required: bool}) { + T rc; + if (!map.containsKey(key)) { + if (required) { + logger.error('missing $key in ${fullName()}'); + } + } else { + rc = StringUtils.stringToEnum(map[key], values); + } + return rc; + } + + /// Fetches an entry from a map addressed by a [key]. + /// An error is logged if [required] is true and the map does not contain the key. + int parseInt(String key, Map map, {required: bool}) { + int rc; + if (!map.containsKey(key)) { + if (required) { + logger.error('missing $key in ${fullName()}'); + } else { + if (map[key].runtimeType == int) { + rc = map[key]; + } else if (map[key].runtimeType == String) { + if (Validation.isInt(map[key])) { + rc = StringUtils.asInt(map[key]); + } else { + logger + .error('not an integer: ${map[key]} map[$key] in {fullName()}'); + } + } else { + logger.error('not an integer: ${map[key]} map[$key] in {fullName()}'); + } + } + } + return rc; + } + + /// Fetches an entry from a map addressed by a [key]. + /// An error is logged if [required] is true and the map does not contain the key. + List parseOptions(String key, Map map) { + List rc = []; + if (map.containsKey(key)) { + rc = map[key].split(';'); + } + return rc; + } + + /// Fetches an entry from a map addressed by a [key]. + /// An error is logged if [required] is true and the map does not contain the key. + String parseString(String key, Map map, {required: bool}) { + String rc; + if (!map.containsKey(key)) { + if (required) { + logger.error('missing $key in ${fullName()}'); + } else { + rc = map[key] as String; + } + } + return rc; + } +} diff --git a/lib/src/model/model_types.dart b/lib/src/model/model_types.dart new file mode 100644 index 0000000..d441b53 --- /dev/null +++ b/lib/src/model/model_types.dart @@ -0,0 +1,4 @@ +import 'package:flutter_bones/flutter_bones.dart'; +enum DataType { + bool, currency, date, dateTime, float, int, reference, string, +} diff --git a/lib/src/model/module/user_model.dart b/lib/src/model/module/user_model.dart new file mode 100644 index 0000000..6416fcd --- /dev/null +++ b/lib/src/model/module/user_model.dart @@ -0,0 +1,43 @@ +import 'package:flutter_bones/flutter_bones.dart'; +import 'package:dart_bones/dart_bones.dart'; + +class UserModel extends ModuleModel{ + + static final model = { + "module": "user", + "pages": [ + { + "name": "create", + "pageModeType": "create", + "sections": [ + { + "sectionModeType": "simpleForm", + "fields": [ + { + "name": "user", + "fieldModelType": "text", + "label": "Benutzer", + "options": "required;unique", + }, + { + "name": "displayname", + "label": "Anzeigename", + "fieldModelType": "text", + "options": "required", + }, + { + "name": "role", + "label": "Rolle", + "fieldModelType": "combobox", + "dataType": "reference", + "options": "required;undef", + }, + ] + } + ] + }, + ], + }; + UserModel(BaseLogger logger): super(model, logger); + +} \ No newline at end of file diff --git a/lib/src/model/module_model.dart b/lib/src/model/module_model.dart new file mode 100644 index 0000000..dfacd9f --- /dev/null +++ b/lib/src/model/module_model.dart @@ -0,0 +1,24 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bones/flutter_bones.dart'; +import 'package:meta/meta.dart'; +import 'package:dart_bones/dart_bones.dart'; + +class ModuleModel extends ModelBase { + final Map map; + String name; + List options; + List pages; + + ModuleModel(this.map, BaseLogger logger): super(logger); + /// Returns the name including the names of the parent + @override + String fullName() => name; + + /// Parses the map and stores the data in the instance. + void parse(){ + checkSuperfluous(map, 'module pages options'.split(' ')); + name = parseString('module', map, required: true); + pages = PageModel.parseList(this, map['pages'], logger); + options = parseOptions('options', map); + } +} diff --git a/lib/src/model/page_model.dart b/lib/src/model/page_model.dart new file mode 100644 index 0000000..1281b73 --- /dev/null +++ b/lib/src/model/page_model.dart @@ -0,0 +1,41 @@ +import 'package:dart_bones/dart_bones.dart'; +import 'package:flutter_bones/flutter_bones.dart'; +import 'package:meta/meta.dart'; + +/// Represents one screen of the application. +class PageModel extends ModelBase { + static final regExprOptions = RegExp(r'^(unknown)'); + final ModuleModel module; + final Mapmap; + String name; + PageModelType pageModelType; + final Listsections = []; + List options; + + PageModel(this.module, this.map, BaseLogger logger): super(logger); + + /// Returns the name including the names of the parent + @override + String fullName() => '${module.fullName()}.$name'; + + /// Parses the map and stores the data in the instance. + void parse(){ + checkSuperfluous(map, 'name pageModelType sections options'.split(' ')); + name = parseString('name', map, required: true); + pageModelType = parseEnum('fieldTypeInfo', map, PageModelType.values); + options = parseOptions('options', map); + } + /// Returns a list of Pages constructed by the Json like [map]. + static List parseList( + ModuleModel parent, List> map, BaseLogger logger) { + final rc = map.map((item) => PageModel(parent, item, logger)); + return rc; + } +} + +enum PageModelType { + change, + create, + delete, + overview, +} diff --git a/lib/src/model/section_model.dart b/lib/src/model/section_model.dart new file mode 100644 index 0000000..70ec50b --- /dev/null +++ b/lib/src/model/section_model.dart @@ -0,0 +1,41 @@ +import 'package:flutter/material.dart'; +import 'package:dart_bones/dart_bones.dart'; +import 'package:flutter_bones/flutter_bones.dart'; +import 'package:meta/meta.dart'; + +/// A part of a page represented by one widget. +class SectionModel extends ModelBase { + static final regExprOptions = RegExp(r'^(unknown)'); + SectionModelType sectionModelType; + final PageModel page; + String name; + List fields; + List options; + final Map map; + SectionModel(this.page, this.map, BaseLogger logger) : super(logger); + + /// Returns the name including the names of the parent + @override + String fullName() => '${page.fullName()}.$name'; + + /// Parses the map and stores the data in the instance. + void parse() { + checkSuperfluous(map, 'name sectionModelType fields options'.split(' ')); + name = parseString('name', map, required: true); + sectionModelType = parseEnum( + 'sectionModelType', map, PageModelType.values); + options = parseOptions('options', map); + } + + /// Returns a list of Pages constructed by the Json like [map]. + static List parseList( + PageModel parent, List> map, BaseLogger logger) { + final rc = map.map((item) => SectionModel(parent, item, logger)); + return rc; + } +} + +enum SectionModelType { + simpleForm, + query, +} diff --git a/lib/src/model/widget_model.dart b/lib/src/model/widget_model.dart new file mode 100644 index 0000000..fe688cd --- /dev/null +++ b/lib/src/model/widget_model.dart @@ -0,0 +1,75 @@ +import 'package:flutter/material.dart'; +import 'package:dart_bones/dart_bones.dart'; +import 'package:flutter_bones/flutter_bones.dart'; +import 'package:meta/meta.dart'; + +/// A base class for items inside a page: SectionModel FieldModel TextModel... +abstract class WidgetModel extends ModelBase { + final SectionModel section; + final PageModel page; + final Map map; + + WidgetModel(this.section, this.page, BaseLogger logger) : super(logger); + + /// Returns the name including the names of the parent + @override + String fullName() => '${parent.fullName()}.$name'; + + /// Parses the map and stores the data in the instance. + void parse() { + checkSuperfluous(map, 'name label toolTip fieldModelType dataType maxSize rows options'.split(' ')); + name = parseString('name', map, required: true); + label = parseString('label', map, required: false); + toolTip = parseString('toolTip', map, required: false); + fieldModelType = parseEnum( + 'fieldModelType', map, WidgetModelType.values); + dataType = parseEnum( + 'dataType', map, DataType.values); + maxSize = parseInt('maxSize', map, required: false); + rows = parseInt('rows', map, required: false); + options = parseOptions('options', map); + } + + /// Tests the validity of the entries in [optionList] + /// Errors will be logged. + bool checkOptions() { + bool rc = false; + options.forEach((element) { + if (regExprOptions.firstMatch(element) == null) { + logger.error('unbekannte Feldoption $element in ${fullName()}'); + rc = true; + } + }); + return rc; + } + /// Returns the widget representing the field. + Widget widget(Key formKey){ + Widget rc; + switch(fieldModelType){ + case WidgetModelType.text: + rc = TextFormField(key: formKey, + validator: validator, + decoration: InputDecoration(labelText: label), + onSaved: onSaved, + maxLength: maxSize, + maxLines: rows, + readOnly: options.contains('readonly'), + obscureText: options.contains('password'), + ); + break; + case WidgetModelType.checkbox: + rc = Checkbox(key: formKey, + decoration: InputDecoration(labelText: label), + onSaved: onSaved, + maxLength: maxSize, + maxLines: rows, + readOnly: options.contains('readonly'), + obscureText: options.contains('password'), + ); + break; + } + return rc; + } +} + +enum WidgetModelType { checkbox, combobox, image, text, } diff --git a/lib/src/page/login_page.dart b/lib/src/page/login_page.dart new file mode 100644 index 0000000..d03a477 --- /dev/null +++ b/lib/src/page/login_page.dart @@ -0,0 +1,67 @@ +import 'package:flutter/material.dart'; +import 'package:dart_bones/dart_bones.dart'; +import 'package:flutter_bones/flutter_bones.dart'; + +class LoginPage extends StatefulWidget { + final PageData pageData; + LoginPage(this.pageData, { Key key }) : super(key: key); + @override + LoginPageState createState() { + // LoginPageState.setPageData(pageData); + final rc = LoginPageState(pageData); + + return rc; + } +} + +class LoginPageState extends State{ + LoginPageState(this.pageData); + final PageData pageData; + + final GlobalKey _formKey = GlobalKey(); + static User currentUser = User(); + + @override + Widget build(BuildContext context) { + final user = LoginUser(); + return Scaffold( + appBar: pageData.appBarBuilder('login'), + drawer: pageData.drawerBuilder(context), + body: SimpleForm.simpleForm( + key: _formKey, + configuration: pageData.configuration, + fields: [ + Text('Bitte loggen Sie sich ein.'), + TextFormField( + validator: checkNotEmpty, + decoration: InputDecoration(labelText: 'Name'), + onSaved: (input) => user.name = input, + ), + TextFormField( + validator: (input) => Validation.isEmail(input) || Validation.isPhoneNumber(input) ? null : 'keine Emailadresse und keine Telefonnummer: $input', + decoration: InputDecoration(labelText: 'Passwort'), + onSaved: (input) => user.password = input, + obscureText: true, + ), + ], + buttons: [ + RaisedButton( + onPressed: () => login(context), + child: Text('Anmelden'), + ), + ], + )); + } + + void login(context) async { + if (_formKey.currentState.validate()) { + _formKey.currentState.save(); + //@ToDo: store in database + } + } +} + +class LoginUser { + String name; + String password; +} \ No newline at end of file diff --git a/lib/src/page/page_data.dart b/lib/src/page/page_data.dart new file mode 100644 index 0000000..70ad597 --- /dev/null +++ b/lib/src/page/page_data.dart @@ -0,0 +1,15 @@ +import 'package:dart_bones/dart_bones.dart'; +import 'package:flutter/material.dart'; + +/// Data class for storing parameter to build a page. +class PageData { + BaseConfiguration configuration; + /// Signature: AppBar func(String title) + AppBar Function(String title) appBarBuilder; + Drawer Function(dynamic context) drawerBuilder; + /// Constructor. + /// [configuration] is a map with the widget data (e.g. padding) + /// [appBarBuilder] is a factory of a function returning a outside designed AppBar + /// [drawerBuilder] is a factory of a function returning a Drawer which handles the "Hamburger menu" + PageData(this.configuration, this.appBarBuilder, this.drawerBuilder); +} \ No newline at end of file diff --git a/lib/src/page/role_page.dart b/lib/src/page/role_page.dart new file mode 100644 index 0000000..d37fea1 --- /dev/null +++ b/lib/src/page/role_page.dart @@ -0,0 +1,64 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bones/flutter_bones.dart'; + +class RolePage extends StatefulWidget { + final PageData pageData; + RolePage(this.pageData, { Key key }) : super(key: key); + @override + RolePageState createState() { + // RolePageState.setPageData(pageData); + final rc = RolePageState(pageData); + + return rc; + } +} + +class RolePageState extends State{ + RolePageState(this.pageData); + final PageData pageData; + + final GlobalKey _formKey = GlobalKey(); + static Role currentRole = Role(); + + @override + Widget build(BuildContext context) { + final role = Role(); + return Scaffold( + appBar: pageData.appBarBuilder('Rollen'), + drawer: pageData.drawerBuilder(context), + body: SimpleForm.simpleForm( + key: _formKey, + configuration: pageData.configuration, + fields: [ + TextFormField( + validator: checkNotEmpty, + decoration: InputDecoration(labelText: 'Name'), + onSaved: (input) => role.name = input, + ), + TextFormField( + validator: checkNat, + decoration: InputDecoration(labelText: 'Priorität'), + onSaved: (input) => role.priority = input, + ), + ], + buttons: [ + RaisedButton( + onPressed: () => login(context), + child: Text('Anmelden'), + ), + ], + )); + } + + void login(context) async { + if (_formKey.currentState.validate()) { + _formKey.currentState.save(); + //@ToDo: store in database + } + } +} + +class Role { + String priority; + String name; +} \ No newline at end of file diff --git a/lib/src/page/user_page.dart b/lib/src/page/user_page.dart new file mode 100644 index 0000000..85166d4 --- /dev/null +++ b/lib/src/page/user_page.dart @@ -0,0 +1,69 @@ +import 'package:flutter/material.dart'; +import 'package:dart_bones/dart_bones.dart'; +import 'package:flutter_bones/flutter_bones.dart'; + +class UserPage extends StatefulWidget { + final PageData pageData; + UserPage(this.pageData, { Key key }) : super(key: key); + @override + UserPageState createState() { + // UserPageState.setPageData(pageData); + final rc = UserPageState(pageData); + + return rc; + } +} + +class UserPageState extends State{ + UserPageState(this.pageData); + final PageData pageData; + + final GlobalKey _formKey = GlobalKey(); + static User currentUser = User(); + + @override + Widget build(BuildContext context) { + final user = User(); + return Scaffold( + appBar: pageData.appBarBuilder('Benutzer'), + drawer: pageData.drawerBuilder(context), + body: SimpleForm.simpleForm( + key: _formKey, + configuration: pageData.configuration, + fields: [ + TextFormField( + validator: checkNotEmpty, + decoration: InputDecoration(labelText: 'Name'), + onSaved: (input) => user.name = input, + ), + TextFormField( + validator: (input) => Validation.isEmail(input) || Validation.isPhoneNumber(input) ? null : 'keine Emailadresse und keine Telefonnummer: $input', + decoration: InputDecoration(labelText: 'Passwort'), + onSaved: (input) => user.password = input, + obscureText: true, + ), + ], + buttons: [ + RaisedButton( + onPressed: () => store(context), + child: Text('Speichern'), + ), + ], + )); + } + + void store(context) async { + if (_formKey.currentState.validate()) { + _formKey.currentState.save(); + //@ToDo: store in database + } + } +} + +class User { + String name; + String email; + String displayName; + String password; + int role; +} \ No newline at end of file diff --git a/lib/src/private/bappbar.dart b/lib/src/private/bappbar.dart new file mode 100644 index 0000000..0ec5ff2 --- /dev/null +++ b/lib/src/private/bappbar.dart @@ -0,0 +1,6 @@ +import 'package:flutter/material.dart'; + +class BAppBar extends AppBar { + BAppBar({String title}) : super(title: Text(title)); + static BAppBar builder(String title) => BAppBar(title: title); +} diff --git a/lib/src/private/bdrawer.dart b/lib/src/private/bdrawer.dart new file mode 100644 index 0000000..544dd48 --- /dev/null +++ b/lib/src/private/bdrawer.dart @@ -0,0 +1,76 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bones/flutter_bones.dart'; +import 'package:flutter_bones/src/private/bsettings.dart'; + +class MenuItem { + final String title; + final dynamic page; + final IconData icon; + MenuItem(this.title, this.page, this.icon); + + static List menuItems() { + final settings = BSettings.instance; + return [ + MenuItem('Login', () => LoginPage(settings.pageData), + Icons.account_box_outlined), + ]; + } +} + +class VFrageDrawer extends Drawer { + VFrageDrawer(context) : super(child: buildGrid(context)); + + /// Returns a method creating a drawer. + static VFrageDrawer builder(dynamic context) => VFrageDrawer(context); + + static Widget buildGrid(context) { + final list = MenuItem.menuItems(); + final rc = Card( + child: GridView.count( + crossAxisCount: 2, + crossAxisSpacing: 16.0, + children: list + .map((item) => GridTile( + child: new InkResponse( + enableFeedback: true, + child: Card( + child: Container( + alignment: Alignment.center, + child: Column( + children: [ + SizedBox(width: 10.0, height: 40.0), + Icon(item.icon), + Text(item.title) + ], + ), + ), + ), + onTap: () => Navigator.push(context, + MaterialPageRoute(builder: (context) => item.page())), + ), + )) + .toList(), + )); + return rc; + } + static Widget buildListView(context) { + final list = MenuItem.menuItems(); + final rc = Card( + child: ListView( + shrinkWrap: true, + physics: ClampingScrollPhysics(), + children: list + .map((item) => ListTile( + title: Text(item.title), + onTap: () { + // What happens after you tap the navigation item + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => item.page())); + }, + )) + .toList())); + return rc; + } +} diff --git a/lib/src/private/bsettings.dart b/lib/src/private/bsettings.dart new file mode 100644 index 0000000..c0b8b5d --- /dev/null +++ b/lib/src/private/bsettings.dart @@ -0,0 +1,30 @@ +import 'package:dart_bones/dart_bones.dart'; +import 'package:flutter_bones/flutter_bones.dart'; +import 'package:flutter_bones/src/private/bdrawer.dart'; +import 'package:flutter_bones/src/private/bappbar.dart'; + +class BSettings { + static BSettings _instance; + + /// Returns the singleton of VSetting. + static BSettings get instance { + if (_instance == null) { + final map = { + 'form.card.padding': '16.0', + 'form.gap.field_button.height': '16.0', + }; + final logger = MemoryLogger(LEVEL_FINE); + final pageData = PageData(BaseConfiguration(map, logger), BAppBar.builder, + VFrageDrawer.builder); + _instance = BSettings(BaseConfiguration(map, logger), pageData, logger); + } + return _instance; + } + + final BaseConfiguration configuration; + + final BaseLogger logger; + + final PageData pageData; + BSettings(this.configuration, this.pageData, this.logger); +} diff --git a/lib/src/widget/simpleform.dart b/lib/src/widget/simpleform.dart new file mode 100644 index 0000000..3bc5269 --- /dev/null +++ b/lib/src/widget/simpleform.dart @@ -0,0 +1,23 @@ +import 'package:flutter/material.dart'; +import 'package:dart_bones/dart_bones.dart'; + +class SimpleForm { + static Form simpleForm({@required Key key, @required List fields, + @required List buttons, @required BaseConfiguration configuration} ) { + final padding = configuration.asFloat('form.card.padding', defaultValue: 16.0); + return Form( + key: key, + child: Card( + margin: EdgeInsets.symmetric(vertical: padding, horizontal: padding), + child: Padding( + padding: + EdgeInsets.symmetric(vertical: 16.0, horizontal: 16.0), + child: ListView( + children: + [...fields, + SizedBox(height: configuration.asFloat('form.gap.field_button.height', defaultValue: 16.0)), + ...buttons] + )), + )); + } +} \ No newline at end of file diff --git a/pubspec.yaml b/pubspec.yaml new file mode 100644 index 0000000..e1d0a53 --- /dev/null +++ b/pubspec.yaml @@ -0,0 +1,83 @@ +name: flutter_bones +description: Helpers for a quick building of a Flutter app. + +# The following line prevents the package from being accidentally published to +# pub.dev using `pub publish`. This is preferred for private packages. +publish_to: 'none' # Remove this line if you wish to publish to pub.dev + +# The following defines the version and build number for your application. +# A version number is three numbers separated by dots, like 1.2.43 +# followed by an optional build number separated by a +. +# Both the version and the builder number may be overridden in flutter +# build by specifying --build-name and --build-number, respectively. +# In Android, build-name is used as versionName while build-number used as versionCode. +# Read more about Android versioning at https://developer.android.com/studio/publish/versioning +# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. +# Read more about iOS versioning at +# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html +version: 1.0.0+1 + +environment: + sdk: ">=2.7.0 <3.0.0" + +dependencies: + flutter: + sdk: flutter + flutter_localizations: + sdk: flutter + dart_bones: "^0.2.2" + + + # The following adds the Cupertino Icons font to your application. + # Use with the CupertinoIcons class for iOS style icons. + cupertino_icons: ^1.0.0 + +dev_dependencies: + flutter_test: + sdk: flutter + pedantic: ^1.8.0 + test: ^1.6.0 + mockito: ^4.1.1 + test_coverage: ^0.4.2 + +# For information on the generic Dart part of this file, see the +# following page: https://dart.dev/tools/pub/pubspec + +# The following section is specific to Flutter. +flutter: + + # The following line ensures that the Material Icons font is + # included with your application, so that you can use the icons in + # the material Icons class. + uses-material-design: true + + # To add assets to your application, add an assets section, like this: + # assets: + # - images/a_dot_burr.jpeg + # - images/a_dot_ham.jpeg + + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.dev/assets-and-images/#resolution-aware. + + # For details regarding adding assets from package dependencies, see + # https://flutter.dev/assets-and-images/#from-packages + + # To add custom fonts to your application, add a fonts section here, + # in this "flutter" section. Each entry in this list should have a + # "family" key with the font family name, and a "fonts" key with a + # list giving the asset and other descriptors for the font. For + # example: + # fonts: + # - family: Schyler + # fonts: + # - asset: fonts/Schyler-Regular.ttf + # - asset: fonts/Schyler-Italic.ttf + # style: italic + # - family: Trajan Pro + # fonts: + # - asset: fonts/TrajanPro.ttf + # - asset: fonts/TrajanPro_Bold.ttf + # weight: 700 + # + # For details regarding fonts from package dependencies, + # see https://flutter.dev/custom-fonts/#from-packages diff --git a/test/helpers/settings_test.dart b/test/helpers/settings_test.dart new file mode 100644 index 0000000..cebeaa2 --- /dev/null +++ b/test/helpers/settings_test.dart @@ -0,0 +1,23 @@ +import 'package:flutter_bones/flutter_bones.dart'; +import 'package:test/test.dart'; + +void main() { + final map = { 'help' : { 'de' : 'Hilfe'}, + 'wrong name %{0}' : { 'de' : 'falscher name %{0}' } + }; + setUpAll(() => Settings.setLocaleByNames(language: 'en')); + group('Settings', () { + test('translate-en', () { + Settings.setLocaleByNames(language: 'en'); + expect(Settings.translate('abc', map), equals('abc')); + expect(Settings.translate('help', map), equals('help')); + expect(Settings.translate('wrong name %{0}', map, placeholders: {'0' : 'Joe'}), equals('wrong name Joe')); + }); + test('translate-de', () { + Settings.setLocaleByNames(language: 'de'); + expect(Settings.translate('abc', map), equals('abc')); + expect(Settings.translate('help', map), equals('Hilfe')); + expect(Settings.translate('wrong name %{0}', map, placeholders: {'0' : 'Joe'}), equals('falscher name Joe')); + }); +}); +} diff --git a/test/helpers/validators_test.dart b/test/helpers/validators_test.dart new file mode 100644 index 0000000..f682a9d --- /dev/null +++ b/test/helpers/validators_test.dart @@ -0,0 +1,53 @@ +import 'package:flutter_bones/flutter_bones.dart'; +import 'package:test/test.dart'; + +void main() { + group('Validators', () { + test('checkEmail', () { + expect(checkEmail('joe@example.com'), isNull); + expect( + checkEmail('joe@example@com'), + equals( + 'Not an email address: joe@example@com Example: joe@example.com')); + }); + test('checkPhoneNumber', () { + expect(checkPhoneNumber('089-12345'), isNull); + expect( + checkPhoneNumber('handy'), + equals( + 'Not a phone number: handy Examples: "089-123452 "+49-89-12345"')); + }); + test('checkNotEmpty', () { + expect(checkNotEmpty('a'), isNull); + expect( + checkNotEmpty(''), + equals( + 'Please fill in')); + }); + test('checkMany', () { + expect(checkMany('089-1234', [checkNotEmpty, checkPhoneNumber]), isNull); + expect( + checkMany('', [checkNotEmpty, checkPhoneNumber]), + equals( + 'Please fill in')); + expect( + checkMany('handy', [checkNotEmpty, checkPhoneNumber]), + equals( + 'Not a phone number: handy Examples: "089-123452 "+49-89-12345"')); + }); + test('checkNat', () { + expect(checkNat('123'), isNull); + expect( + checkNat('-1'), + equals( + 'Not a not negative number: -1')); + }); + test('checkInt', () { + expect(checkNat('123'), isNull); + expect( + checkNat('-a'), + equals( + 'Not a not negative number: -a')); + }); +}); +} diff --git a/test/widget_test.dart b/test/widget_test.dart new file mode 100644 index 0000000..49ab1a7 --- /dev/null +++ b/test/widget_test.dart @@ -0,0 +1,33 @@ +// This is a basic Flutter widget test. +// +// To perform an interaction with a widget in your test, use the WidgetTester +// utility that Flutter provides. For example, you can send tap and scroll +// gestures. You can also use WidgetTester to find child widgets in the widget +// tree, read text, and verify that the values of widget properties are correct. + +/* +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'package:flutter_bones/flutter_bones.dart'; +*/ +void main() { + /* + testWidgets('Counter increments smoke test', (WidgetTester tester) async { + // Build our app and trigger a frame. + await tester.pumpWidget(MyApp()); + + // Verify that our counter starts at 0. + expect(find.text('0'), findsOneWidget); + expect(find.text('1'), findsNothing); + + // Tap the '+' icon and trigger a frame. + await tester.tap(find.byIcon(Icons.add)); + await tester.pump(); + + // Verify that our counter has incremented. + expect(find.text('0'), findsNothing); + expect(find.text('1'), findsOneWidget); + }); + */ +} -- 2.39.5