--- /dev/null
+# 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
--- /dev/null
+# 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
--- /dev/null
+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.
--- /dev/null
+# 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.
--- /dev/null
+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<BoneApp> {
+
+ @override
+ Widget build(BuildContext context) {
+ return MaterialApp(
+ title: 'Virtuelle Fragestunde',
+ theme: ThemeData(
+ primarySwatch: Colors.blue,
+ visualDensity: VisualDensity.adaptivePlatformDensity,
+ ),
+ initialRoute: '/login',
+ onGenerateRoute: _getRoute,
+ );
+ }
+
+}
+
+Route<dynamic> _getRoute(RouteSettings settings) {
+ MaterialPageRoute route;
+ if (settings.name == '/login') {
+ route = MaterialPageRoute<void>(
+ settings: settings,
+ builder: (BuildContext context) => LoginPage(BSettings.instance.pageData),
+ fullscreenDialog: false,
+ );
+ } else {
+ route = MaterialPageRoute<void>(
+ settings: settings,
+ builder: (BuildContext context) => LoginPage(BSettings.instance.pageData),
+ fullscreenDialog: false,
+ );
+ }
+ return route;
+}
--- /dev/null
+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
--- /dev/null
+import 'package:flutter/material.dart';
+import 'package:flutter_bones/app.dart';
+
+void main() {
+ runApp(BoneApp());
+}
+
--- /dev/null
+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]: { <English text> : { <language code> : translation } }
+ /// return: [text] if no translation has been found, otherwise: the translation from the [map]
+ static String translate(String text, Map<String, Map<String, String>> map, {Map<String, String> 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
--- /dev/null
+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<Function> 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<String, String> 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"',
+ },
+ };
+}
--- /dev/null
+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<String> options;
+ FormFieldValidator<String> validator;
+ FormFieldSetter onSaved;
+
+ final Map<String, dynamic> 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>(
+ 'fieldModelType', map, FieldModelType.values);
+ dataType = parseEnum<DataType>(
+ '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, }
--- /dev/null
+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<String, dynamic> map, List<String> 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<T>(String key, Map<String, dynamic> 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<String, dynamic> 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<String> parseOptions(String key, Map<String, dynamic> map) {
+ List<String> 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<String, dynamic> 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;
+ }
+}
--- /dev/null
+import 'package:flutter_bones/flutter_bones.dart';
+enum DataType {
+ bool, currency, date, dateTime, float, int, reference, string,
+}
--- /dev/null
+import 'package:flutter_bones/flutter_bones.dart';
+import 'package:dart_bones/dart_bones.dart';
+
+class UserModel extends ModuleModel{
+
+ static final model = <String, dynamic>{
+ "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
--- /dev/null
+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<String, dynamic> map;
+ String name;
+ List<String> options;
+ List<PageModel> 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);
+ }
+}
--- /dev/null
+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 Map<String, dynamic>map;
+ String name;
+ PageModelType pageModelType;
+ final List<SectionModel>sections = [];
+ List<String> 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<PageModelType>('fieldTypeInfo', map, PageModelType.values);
+ options = parseOptions('options', map);
+ }
+ /// Returns a list of Pages constructed by the Json like [map].
+ static List<PageModel> parseList(
+ ModuleModel parent, List<Map<String, dynamic>> map, BaseLogger logger) {
+ final rc = map.map((item) => PageModel(parent, item, logger));
+ return rc;
+ }
+}
+
+enum PageModelType {
+ change,
+ create,
+ delete,
+ overview,
+}
--- /dev/null
+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<FieldModel> fields;
+ List<String> options;
+ final Map<String, dynamic> 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>(
+ 'sectionModelType', map, PageModelType.values);
+ options = parseOptions('options', map);
+ }
+
+ /// Returns a list of Pages constructed by the Json like [map].
+ static List<SectionModel> parseList(
+ PageModel parent, List<Map<String, dynamic>> map, BaseLogger logger) {
+ final rc = map.map((item) => SectionModel(parent, item, logger));
+ return rc;
+ }
+}
+
+enum SectionModelType {
+ simpleForm,
+ query,
+}
--- /dev/null
+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<String, dynamic> 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<WidgetModelType>(
+ 'fieldModelType', map, WidgetModelType.values);
+ dataType = parseEnum<DataType>(
+ '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, }
--- /dev/null
+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<LoginPage>{
+ LoginPageState(this.pageData);
+ final PageData pageData;
+
+ final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
+ 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: <Widget>[
+ 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: <Widget>[
+ 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
--- /dev/null
+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
--- /dev/null
+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<RolePage>{
+ RolePageState(this.pageData);
+ final PageData pageData;
+
+ final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
+ 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: <Widget>[
+ 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: <Widget>[
+ 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
--- /dev/null
+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<UserPage>{
+ UserPageState(this.pageData);
+ final PageData pageData;
+
+ final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
+ 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: <Widget>[
+ 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: <Widget>[
+ 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
--- /dev/null
+import 'package:flutter/material.dart';
+
+class BAppBar extends AppBar {
+ BAppBar({String title}) : super(title: Text(title));
+ static BAppBar builder(String title) => BAppBar(title: title);
+}
--- /dev/null
+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<MenuItem> menuItems() {
+ final settings = BSettings.instance;
+ return <MenuItem>[
+ 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;
+ }
+}
--- /dev/null
+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);
+}
--- /dev/null
+import 'package:flutter/material.dart';
+import 'package:dart_bones/dart_bones.dart';
+
+class SimpleForm {
+ static Form simpleForm({@required Key key, @required List<Widget> fields,
+ @required List<Widget> 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:
+ <Widget>[...fields,
+ SizedBox(height: configuration.asFloat('form.gap.field_button.height', defaultValue: 16.0)),
+ ...buttons]
+ )),
+ ));
+ }
+}
\ No newline at end of file
--- /dev/null
+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
--- /dev/null
+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'));
+ });
+});
+}
--- /dev/null
+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'));
+ });
+});
+}
--- /dev/null
+// 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);
+ });
+ */
+}