├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── analysis_options.yaml ├── coverage └── lcov.info ├── example ├── README.md ├── analysis_options.yaml ├── lib │ ├── main.dart │ └── src │ │ ├── app │ │ ├── controller.dart │ │ ├── controller │ │ │ ├── theme_controller.dart │ │ │ ├── translations_controller.dart │ │ │ └── word_pair_timer.dart │ │ ├── model.dart │ │ ├── model │ │ │ └── translations │ │ │ │ ├── translations_chinese.dart │ │ │ │ ├── translations_french.dart │ │ │ │ ├── translations_german.dart │ │ │ │ ├── translations_hebrew.dart │ │ │ │ ├── translations_russian.dart │ │ │ │ └── translations_spanish.dart │ │ ├── view.dart │ │ └── view │ │ │ ├── app.dart │ │ │ ├── color_picker.dart │ │ │ └── menu │ │ │ ├── app_menu.dart │ │ │ └── iso_spinner.dart │ │ ├── controller.dart │ │ ├── home │ │ ├── controller.dart │ │ ├── controller │ │ │ ├── contacts_controller.dart │ │ │ ├── counter_controller.dart │ │ │ ├── template_controller.dart │ │ │ └── wordpairs_controller.dart │ │ ├── model.dart │ │ ├── model │ │ │ ├── contacts │ │ │ │ ├── contact.dart │ │ │ │ ├── contact_fields.dart │ │ │ │ └── contacts_db.dart │ │ │ ├── counter │ │ │ │ └── counter_model.dart │ │ │ ├── settings.dart │ │ │ └── words │ │ │ │ └── wordpairs_model.dart │ │ ├── view.dart │ │ └── view │ │ │ ├── contacts │ │ │ ├── add_contact.dart │ │ │ └── contact_details.dart │ │ │ ├── contacts_view.dart │ │ │ ├── counter_view.dart │ │ │ └── wordpairs_view.dart │ │ ├── model.dart │ │ └── view.dart ├── pubspec.lock ├── pubspec.yaml └── test │ ├── src │ ├── test_utils.dart │ ├── tests │ │ ├── contacts_test.dart │ │ ├── counter_test.dart │ │ ├── menu │ │ │ ├── about_menu.dart │ │ │ ├── app_menu.dart │ │ │ ├── interface_menu.dart │ │ │ ├── locale_menu.dart │ │ │ └── open_menu.dart │ │ ├── unit │ │ │ ├── controller_test.dart │ │ │ └── wordpairs_model.dart │ │ └── words_test.dart │ └── view.dart │ └── widget_test.dart ├── lib ├── controller.dart ├── model.dart ├── prefs.dart ├── run_app.dart ├── settings.dart ├── src │ ├── conditional_export.dart │ ├── controller │ │ ├── alarm_manager.dart │ │ ├── app.dart │ │ ├── assets │ │ │ └── assets.dart │ │ ├── device_info.dart │ │ ├── get_utils │ │ │ └── get_utils.dart │ │ ├── schedule_notificaitons.dart │ │ └── util │ │ │ └── handle_error.dart │ ├── model │ │ ├── fileutils │ │ │ ├── files.dart │ │ │ └── installfile.dart │ │ ├── mvc.dart │ │ └── utils │ │ │ └── string_encryption.dart │ └── view │ │ ├── app.dart │ │ ├── app_menu.dart │ │ ├── app_navigator.dart │ │ ├── app_state.dart │ │ ├── app_statefulwidget.dart │ │ ├── extensions │ │ ├── _extensions_view.dart │ │ ├── context_extensions.dart │ │ ├── double_extensions.dart │ │ ├── duration_extensions.dart │ │ ├── dynamic_extensions.dart │ │ ├── num_extensions.dart │ │ ├── string_extensions.dart │ │ └── widget_extensions.dart │ │ ├── mvc.dart │ │ ├── platforms │ │ ├── run_app.dart │ │ └── run_webapp.dart │ │ ├── utils │ │ ├── app_settings.dart │ │ ├── error_handler.dart │ │ ├── field_widgets.dart │ │ ├── inherited_state.dart │ │ ├── loading_screen.dart │ │ └── timezone.dart │ │ └── uxutils │ │ ├── controller.dart │ │ ├── model.dart │ │ ├── src │ │ └── view │ │ │ ├── common_widgets │ │ │ └── custom_raised_button.dart │ │ │ ├── custom_scroll_physics.dart │ │ │ ├── dialog_box.dart │ │ │ ├── iso_spinner.dart │ │ │ ├── nav_bottom_bar.dart │ │ │ ├── show_cupertino_date_picker.dart │ │ │ ├── simple_bottom_appbar.dart │ │ │ ├── tab_buttons.dart │ │ │ ├── utils │ │ │ └── preferred_orientation_mixin.dart │ │ │ └── variable_string.dart │ │ ├── ux.dart │ │ └── view.dart └── view.dart ├── pubspec.yaml └── test └── widget_test.dart /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | ## Github Action Workflow for Continuous Integration Testing 2 | # https://medium.com/mobile-development-group/github-actions-for-flutter-cf02923d7b5d 3 | # 4 | # 5 | # 6 | name: Flutter CI 7 | 8 | # Github Actions will execute the workflow following the events under on key. 9 | # This workflow is triggered on pushes to the repository. 10 | on: 11 | push: 12 | branches: 13 | - master 14 | pull_request: 15 | branches: 16 | - master 17 | 18 | # A workflow run is made up of one or more jobs. 19 | jobs: 20 | build: 21 | # This job will run on ubuntu virtual machine 22 | runs-on: ubuntu-latest 23 | 24 | # A job contains a sequence of tasks called steps. 25 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it 26 | steps: 27 | - uses: actions/checkout@v2 28 | # Setup Java environment in order to build the Android app. 29 | - uses: actions/setup-java@v1 30 | with: 31 | java-version: '12.x' 32 | # Setup the flutter environment. 33 | - uses: subosito/flutter-action@v1 34 | with: 35 | channel: 'stable' # 'beta' 'dev', 'alpha', default to: 'stable' 36 | 37 | # Write the command to get the Flutter dependencies. 38 | - run: flutter pub get 39 | 40 | # Check for any formatting issues in the code. 41 | - run: flutter format . 42 | 43 | # Statically analyze the Dart code for any errors, but don't fail if any. 44 | - run: flutter analyze . --preamble --no-fatal-infos --no-fatal-warnings 45 | 46 | # Run widget tests for our flutter project. 47 | - run: flutter test --coverage 48 | 49 | # Parse a tag from the commit message 50 | - id: get-tag 51 | run: | 52 | id=$(echo ${{github.event.head_commit.message}} | cut -d' ' -f1) 53 | echo "::set-output name=tag::$id" 54 | 55 | # Create a Release up on Github 56 | if: !contains('${{ steps.get-tag.outputs.TAG }}', '+') 57 | - uses: actions/create-release@v1 58 | env: 59 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # This token is provided by Actions, you do not need to create your own token 60 | with: 61 | tag_name: ${{ steps.get-tag.outputs.tag }} 62 | release_name: Release ${{ steps.get-tag.outputs.tag }} 63 | # body_path: CHANGELOG.md 64 | body: | 65 | See CHANGELOG.md 66 | draft: false 67 | prerelease: false -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Good to keep this file with the code. 2 | #.gitignore 3 | 4 | ## Github Action Workflow for Continuous Integration Testing 5 | # https://medium.com/mobile-development-group/github-actions-for-flutter-cf02923d7b5d 6 | # .github 7 | 8 | # Ignore except for application packages 9 | pubspec.lock 10 | 11 | # Flutter IDE files 12 | .packages 13 | .flutter-plugins 14 | .flutter-plugins-dependencies 15 | flutter_export_environment.sh 16 | .pub/ 17 | out/ 18 | .gradle/ 19 | .dart_tool/ 20 | .pub-cache/ 21 | # Ignore API documentation directory created by dartdoc: 22 | doc/api/ 23 | 24 | # Android Studio 25 | /*.iml 26 | 27 | # Ignore App signing files 28 | *.jks 29 | 30 | # Avoid committing generated JavaScript files: 31 | *.dart.js 32 | *.info.json # Produced by the --dump-info flag. 33 | *.js # When generated by dart2js. Don't specify *.js if your 34 | # project includes source files written in JavaScript. 35 | *.js_ 36 | *.js.deps 37 | *.js.map 38 | 39 | # Explicitly include the example app 40 | !example/** 41 | example/.* 42 | example/ios/Flutter/flutter_export_environment.sh 43 | 44 | ### Eclipse ### 45 | *.pydevproject 46 | .project 47 | .metadata 48 | 49 | /bin/** 50 | /tmp/** 51 | /tmp/**/* 52 | /*.tmp 53 | /*.bak 54 | /*.swp 55 | /*~.nib 56 | 57 | /.classpath 58 | /.settings/ 59 | /.loadpath 60 | 61 | 62 | # Miscellaneous 63 | #*.lock 64 | *.class 65 | *.log 66 | *.pyc 67 | *.swp 68 | .DS_Store 69 | .atom/ 70 | .buildlog/ 71 | .history 72 | .svn/ 73 | 74 | # IntelliJ related 75 | *.iml 76 | *.ipr 77 | *.iws 78 | .idea/ 79 | 80 | # Visual Studio Code related 81 | .vscode/ 82 | 83 | # Flutter/Dart/Pub related 84 | build/ 85 | 86 | # Web related 87 | lib/generated_plugin_registrant.dart 88 | 89 | # Symbolication related 90 | app.*.symbolscd 91 | 92 | # Obfuscation related 93 | app.*.map.json 94 | mapping.txt 95 | seeds.txt 96 | unused.txt 97 | obfuscate/ 98 | 99 | # Android related 100 | #google-services.json 101 | **/android/**/gradle-wrapper.jar 102 | **/android/.gradle 103 | **/android/captures/ 104 | **/android/gradlew 105 | **/android/gradlew.bat 106 | **/android/local.properties 107 | **/android/key.properties 108 | **/android/**/GeneratedPluginRegistrant.java 109 | 110 | # iOS/XCode related 111 | #GoogleService-Info.plist 112 | **/ios/**/*.mode1v3 113 | **/ios/**/*.mode2v3 114 | **/ios/**/*.moved-aside 115 | **/ios/**/*.pbxuser 116 | **/ios/**/*.perspectivev3 117 | **/ios/**/*sync/ 118 | **/ios/**/.sconsign.dblite 119 | **/ios/**/.tags* 120 | **/ios/**/.vagrant/ 121 | **/ios/**/DerivedData/ 122 | **/ios/**/Icon? 123 | **/ios/**/Pods/ 124 | **/ios/**/.symlinks/ 125 | **/ios/**/profile 126 | **/ios/**/xcuserdata 127 | **/ios/.generated/ 128 | 129 | App.framework 130 | Flutter.framework 131 | Generated.xcconfig 132 | app.flx 133 | app.zip 134 | flutter_assets/ 135 | ServiceDefinitions.json 136 | GeneratedPluginRegistrant.* 137 | Info.plist 138 | **/ios/Flutter/.last_build_id 139 | 140 | # Exceptions to above rules. 141 | !**/ios/**/default.mode1v3 142 | !**/ios/**/default.mode2v3 143 | !**/ios/**/default.pbxuser 144 | !**/ios/**/default.perspectivev3 145 | !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages 146 | 147 | ################################## original .gitignore 148 | ##Root Files & Folders 149 | #/.gradle 150 | #/backup 151 | #build/ 152 | # 153 | ##Arbitary Files 154 | #*.txt 155 | #*.doc 156 | #*.docx 157 | # 158 | ## Windows image file caches 159 | #Thumbs.db 160 | #ehthumbs.db 161 | # 162 | ## Folder config file 163 | #Desktop.ini 164 | # 165 | ## Recycle Bin used on file shares 166 | #$RECYCLE.BIN/ 167 | # 168 | ## Windows Installer files 169 | #*.cab 170 | #*.msi 171 | #*.msm 172 | #*.msp 173 | # 174 | ## Windows shortcuts 175 | #*.lnk 176 | # 177 | ## ========================= 178 | ## Operating System Files 179 | ## ========================= 180 | # 181 | ## OSX 182 | ## ========================= 183 | # 184 | #.DS_Store 185 | #.AppleDouble 186 | #.LSOverride 187 | # 188 | ## Thumbnails 189 | #/._* 190 | # 191 | ## Files that might appear on external disk 192 | #/.Spotlight-V100 193 | #/.Trashes 194 | # 195 | ## Directories potentially created on remote AFP share 196 | #.AppleDB 197 | #.AppleDesktop 198 | #Network Trash Folder 199 | #Temporary Items 200 | #.apdisk 201 | # 202 | #/captures 203 | # 204 | ## Built application files 205 | #/*.apk 206 | #/*.ap_ 207 | # 208 | ## Files for the ART/Dalvik VM 209 | #/*.dex 210 | # 211 | ## Java class files 212 | #/*.class 213 | # 214 | ## Generated files 215 | #bin/ 216 | #gen/ 217 | #out/ 218 | # 219 | ## Local configuration file (sdk path, etc) 220 | #/local.properties 221 | # 222 | ## Proguard folder generated by Eclipse 223 | #proguard/ 224 | # 225 | ## Log Files 226 | #*.log 227 | # 228 | ## Android Studio Navigation editor temp files 229 | #.navigation/ 230 | # 231 | ## Android Studio captures folder 232 | #captures/ 233 | # 234 | ## Intellij 235 | #*.iml 236 | ##.idea/tasks.xml 237 | ##.idea/gradle.xml 238 | ## Except for application packages 239 | #pubspec.lock 240 | # 241 | ## Keystore files 242 | #*.jks 243 | # 244 | ## External native build folder generated in Android Studio 2.2 and later 245 | #.externalNativeBuild 246 | # 247 | ##.gitignore 248 | #/.DS_Store 249 | #.atom/ 250 | #/.idea/ 251 | #.vscode/ 252 | #.packages 253 | #.pub/ 254 | # 255 | #.flutter-plugins 256 | #doc/api/ 257 | #.dart_tool/ 258 | ## test/ 259 | # 260 | ## Don't need the Android stuff 261 | #android/ 262 | # 263 | ## Don't need the iOS stuff 264 | #ios/ 265 | # 266 | ## Avoid committing generated JavaScript files: 267 | #/*.dart.js 268 | #/*.info.json # Produced by the --dump-info flag. 269 | #/*.js # When generated by dart2js. Don't specify *.js if your 270 | # # project includes source files written in JavaScript. 271 | #/*.js_ 272 | #/*.js.deps 273 | #/*.js.map 274 | # 275 | #### Eclipse ### 276 | #*.pydevproject 277 | #.project 278 | #.metadata 279 | #bin/** 280 | #tmp/** 281 | #tmp/**/* 282 | #/*.tmp 283 | #/*.bak 284 | #/*.swp 285 | #/*~.nib 286 | # 287 | #/.classpath 288 | #/.settings/ 289 | #/.loadpath 290 | # 291 | ## Google Services (e.g. APIs or Firebase) 292 | #google-services.json 293 | # 294 | #/example/.flutter-plugins-dependencies 295 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | notifications: 2 | email: support@andrioussolutions.com 3 | 4 | language: dart 5 | 6 | dart: 7 | - stable 8 | 9 | jobs: 10 | include: 11 | - name: Flutter Test Stable Channel 12 | language: dart 13 | os: linux 14 | script: 15 | - ./flutter/bin/flutter test 16 | env: FLUTTER_VERSION=stable 17 | 18 | - name: Flutter Test Stable Channel 19 | language: dart 20 | os: linux 21 | script: 22 | - ./flutter/bin/flutter test 23 | env: FLUTTER_VERSION=beta 24 | 25 | allow_failures: 26 | - env: FLUTTER_VERSION=beta 27 | 28 | before_script: 29 | - git clone https://github.com/flutter/flutter.git -b $FLUTTER_VERSION 30 | - "./flutter/bin/flutter doctor" -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | # Example App 2 | 3 | #### Extensive example app showcasing the underlying MVC framework. 4 | -------------------------------------------------------------------------------- /example/analysis_options.yaml: -------------------------------------------------------------------------------- 1 | # This file configures the analyzer, which statically analyzes Dart code to 2 | # check for errors, warnings, and lints. 3 | # 4 | # The issues identified by the analyzer are surfaced in the UI of Dart-enabled 5 | # IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be 6 | # invoked from the command line by running `flutter analyze`. 7 | 8 | # The following line activates a set of recommended lints for Flutter apps, 9 | # packages, and plugins designed to encourage good coding practices. 10 | include: package:flutter_lints/flutter.yaml 11 | 12 | linter: 13 | # The lint rules applied to this project can be customized in the 14 | # section below to disable rules from the `package:flutter_lints/flutter.yaml` 15 | # included above or to enable additional rules. A list of all available lints 16 | # and their documentation is published at 17 | # https://dart-lang.github.io/linter/lints/index.html. 18 | # 19 | # Instead of disabling a lint rule for the entire project in the 20 | # section below, it can also be suppressed for a single line of code 21 | # or a specific dart file by using the `// ignore: name_of_lint` and 22 | # `// ignore_for_file: name_of_lint` syntax on the line or in the file 23 | # producing the lint. 24 | rules: 25 | # avoid_print: false # Uncomment to disable the `avoid_print` rule 26 | # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule 27 | 28 | # Additional information about this file can be found at 29 | # https://dart.dev/guides/language/analysis-options 30 | -------------------------------------------------------------------------------- /example/lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'package:mvc_application_example/src/view.dart'; 2 | 3 | void main() => runApp(TemplateApp()); 4 | -------------------------------------------------------------------------------- /example/lib/src/app/controller.dart: -------------------------------------------------------------------------------- 1 | /// The Theme Controller at the application level 2 | export 'package:mvc_application_example/src/app/controller/theme_controller.dart'; 3 | 4 | /// Controller of the App's Language Translations 5 | export 'package:mvc_application_example/src/app/controller/translations_controller.dart'; 6 | 7 | /// The Word Pair Timer Controller at the application level 8 | export 'package:mvc_application_example/src/app/controller/word_pair_timer.dart'; 9 | -------------------------------------------------------------------------------- /example/lib/src/app/controller/theme_controller.dart: -------------------------------------------------------------------------------- 1 | import 'package:mvc_application_example/src/view.dart' show ThemeData; 2 | 3 | import 'package:mvc_application_example/src/controller.dart' 4 | show ControllerMVC, Prefs; 5 | 6 | /// The App's theme controller 7 | class ThemeController extends ControllerMVC { 8 | factory ThemeController() => _this ??= ThemeController._(); 9 | ThemeController._() { 10 | _isDarkmode = Prefs.getBool('darkmode', false); 11 | } 12 | static ThemeController? _this; 13 | late bool _isDarkmode; 14 | 15 | /// Indicate if in 'dark mode' or not 16 | bool get isDarkMode => _isDarkmode; 17 | 18 | /// Record if the App's in dark mode or not. 19 | set isDarkMode(bool? set) { 20 | if (set == null) { 21 | return; 22 | } 23 | _isDarkmode = set; 24 | Prefs.setBool('darkmode', _isDarkmode); 25 | } 26 | 27 | /// Explicitly return the 'dark theme.' 28 | ThemeData setDarkMode() { 29 | isDarkMode = true; 30 | return ThemeData.dark(); 31 | } 32 | 33 | /// Returns 'dark theme' only if specified. 34 | /// Otherwise, it returns null. 35 | ThemeData? setIfDarkMode() => _isDarkmode ? setDarkMode() : null; 36 | } 37 | -------------------------------------------------------------------------------- /example/lib/src/app/controller/translations_controller.dart: -------------------------------------------------------------------------------- 1 | import 'package:mvc_application_example/src/model.dart'; 2 | 3 | import 'package:mvc_application_example/src/view.dart'; 4 | 5 | //ignore: non_constant_identifier_names 6 | final AppTrs = AppTranslations(); 7 | 8 | class AppTranslations extends L10n { 9 | factory AppTranslations() => _this ??= AppTranslations._(); 10 | AppTranslations._(); 11 | static AppTranslations? _this; 12 | 13 | /// The text's original Locale 14 | @override 15 | Locale get textLocale => const Locale('en', 'US'); 16 | 17 | /// The app's translations 18 | @override 19 | Map> get l10nMap => { 20 | const Locale('zh', 'CN'): zhCN, 21 | const Locale('fr', 'FR'): frFR, 22 | const Locale('de', 'DE'): deDE, 23 | const Locale('he', 'IL'): heIL, 24 | const Locale('ru', 'RU'): ruRU, 25 | const Locale('es', 'AR'): esAR, 26 | }; 27 | } 28 | -------------------------------------------------------------------------------- /example/lib/src/app/model.dart: -------------------------------------------------------------------------------- 1 | /// The Language Translations files 2 | export 'package:mvc_application_example/src/app/model/translations/translations_chinese.dart'; 3 | export 'package:mvc_application_example/src/app/model/translations/translations_french.dart'; 4 | export 'package:mvc_application_example/src/app/model/translations/translations_german.dart'; 5 | export 'package:mvc_application_example/src/app/model/translations/translations_hebrew.dart'; 6 | export 'package:mvc_application_example/src/app/model/translations/translations_russian.dart'; 7 | export 'package:mvc_application_example/src/app/model/translations/translations_spanish.dart'; 8 | -------------------------------------------------------------------------------- /example/lib/src/app/model/translations/translations_chinese.dart: -------------------------------------------------------------------------------- 1 | Map zhCN = { 2 | 'Demo App': '演示应用', 3 | 'Counter Page Demo': '计数器页面演示', 4 | 'You have pushed the button this many times:': '您已经多次按下按钮:', 5 | 'Add': '添加', 6 | 'Interface:': '界面', 7 | 'Application:': '应用', 8 | 'Locale:': '语言环境', 9 | 'Colour Theme': '颜色主题', 10 | 'About': '关于', 11 | 'Cancel': '取消', 12 | 'Save': '保存', 13 | 'You deleted an item.': '您删除了一个项目。', 14 | 'You archived an item.': '您归档了一个项目。', 15 | 'id': 'ID', 16 | 'Display Name': '显示名称', 17 | 'First Name': '名', 18 | 'Middle Name': '中间名字', 19 | 'Last Name': '姓', 20 | 'Email': '电子邮件', 21 | 'Phone': '电话', 22 | 'Company': '公司', 23 | 'Job': '工作', 24 | 'home': '家', 25 | 'work': '工作', 26 | 'landline': '座机', 27 | 'mobile': '移动的', 28 | 'other': '其他', 29 | 'Cannot be empty': '不能为空', 30 | 'Add a contact': '添加联系人', 31 | 'Edit a contact': '编辑联系人', 32 | 'Delete this contact?': '删除此联系人?', 33 | 'Startup Name Generator': '启动名称生成器', 34 | 'Saved Suggestions': '保存的建议', 35 | 'Current Language': '当前语言', 36 | }; 37 | -------------------------------------------------------------------------------- /example/lib/src/app/model/translations/translations_french.dart: -------------------------------------------------------------------------------- 1 | Map frFR = { 2 | 'Demo App': 'Application de démonstration', 3 | 'Counter Page Demo': 'Démo de la page de compteur', 4 | 'You have pushed the button this many times:': 5 | 'Vous avez appuyé sur le bouton autant de fois:', 6 | 'Add': 'Ajouter', 7 | 'Interface:': 'Interface:', 8 | 'Application:': 'Application:', 9 | 'Locale:': 'Lieu', 10 | 'Colour Theme': 'Thème de couleur', 11 | 'About': 'Sur', 12 | 'Cancel': 'Annuler', 13 | 'Save': 'Enregistrer', 14 | 'You deleted an item.': 'Vous avez supprimé un élément.', 15 | 'You archived an item.': 'Vous avez archivé un élément.', 16 | 'id': 'identifiant', 17 | 'Display Name': 'Afficher un nom', 18 | 'First Name': 'Prénom', 19 | 'Middle Name': 'Deuxième nom', 20 | 'Last Name': 'Nom de famille', 21 | 'Email': 'E-mail', 22 | 'Phone': 'Téléphoner', 23 | 'Company': 'Société', 24 | 'Job': 'Travail', 25 | 'home': 'domicile', 26 | 'work': 'travail', 27 | 'landline': 'ligne fixe', 28 | 'mobile': 'mobile', 29 | 'other': 'autre', 30 | 'Cannot be empty': 'Ne peux pas être vide', 31 | 'Add a contact': 'Ajouter un contact', 32 | 'Edit a contact': 'Modifier un contact', 33 | 'Delete this contact?': 'Supprimer ce contact?', 34 | 'Startup Name Generator': 'Générateur de nom de démarrage', 35 | 'Saved Suggestions': 'Suggestions enregistrées', 36 | 'Current Language': 'Langue courante', 37 | }; 38 | -------------------------------------------------------------------------------- /example/lib/src/app/model/translations/translations_german.dart: -------------------------------------------------------------------------------- 1 | Map deDE = { 2 | 'Demo App': 'Demo-App', 3 | 'Counter Page Demo': 'Zählerseiten-Demo', 4 | 'You have pushed the button this many times:': 5 | 'Sie haben den Knopf so oft gedrückt:', 6 | 'Add': 'Hinzufügen', 7 | 'Interface:': 'Schnittstelle', 8 | 'Application:': 'Anwendung:', 9 | 'Locale:': 'Gebietsschema', 10 | 'Colour Theme': 'Farbthema', 11 | 'About': 'Etwa', 12 | 'Cancel': 'Abbrechen', 13 | 'Save': 'Speichern', 14 | 'You deleted an item.': 'Sie haben ein Element gelöscht.', 15 | 'You archived an item.': 'Sie haben einen Artikel archiviert.', 16 | 'id': 'Ich würde', 17 | 'Display Name': 'Anzeigename', 18 | 'First Name': 'Vorname', 19 | 'Middle Name': 'Zweiter Vorname', 20 | 'Last Name': 'Nachname', 21 | 'Email': 'Email', 22 | 'Phone': 'Telefon', 23 | 'Company': 'Begleitung', 24 | 'Job': 'Arbeit', 25 | 'home': 'Heimat', 26 | 'work': 'arbeiten', 27 | 'landline': 'Festnetz', 28 | 'mobile': 'Handy, Mobiltelefon', 29 | 'other': 'andere', 30 | 'Cannot be empty': 'Kann nicht leer sein', 31 | 'Add a contact': 'Einen Kontakt hinzufügen', 32 | 'Edit a contact': 'Bearbeiten Sie einen Kontakt', 33 | 'Delete this contact?': 'Diesen Kontakt löschen?', 34 | 'Startup Name Generator': 'Startup-Namensgenerator', 35 | 'Saved Suggestions': 'Gespeicherte Vorschläge', 36 | 'Current Language': 'Aktuelle Sprache', 37 | }; 38 | -------------------------------------------------------------------------------- /example/lib/src/app/model/translations/translations_hebrew.dart: -------------------------------------------------------------------------------- 1 | Map heIL = { 2 | 'Demo App': 'אפליקציית הדגמה', 3 | 'Counter Page Demo': 'הדגמת עמוד נגדי', 4 | 'You have pushed the button this many times:': 5 | 'לחצת על הכפתור כל כך הרבה פעמים:', 6 | 'Add': 'לְהוֹסִיף', 7 | 'Interface:': 'מִמְשָׁק', 8 | 'Application:': 'יישום:', 9 | 'Locale:': 'מקום', 10 | 'Colour Theme': 'נושא צבע', 11 | 'About': 'על אודות', 12 | 'Cancel': 'לְבַטֵל', 13 | 'Save': 'להציל', 14 | 'You deleted an item.': 'מחקת פריט.', 15 | 'You archived an item.': 'העברת פריט לארכיון.', 16 | 'id': 'תְעוּדַת זֶהוּת', 17 | 'Display Name': 'הצג שם', 18 | 'First Name': 'שם פרטי', 19 | 'Middle Name': 'שם אמצעי', 20 | 'Last Name': 'שם משפחה', 21 | 'Email': 'אימייל', 22 | 'Phone': 'טלפון', 23 | 'Company': 'חֶברָה', 24 | 'Job': 'עבודה', 25 | 'home': 'בית', 26 | 'work': 'עֲבוֹדָה', 27 | 'landline': 'טלפון נייח', 28 | 'mobile': 'נייד', 29 | 'other': 'אַחֵר', 30 | 'Cannot be empty': 'לא יכול להיות ריק', 31 | 'Add a contact': 'הוסף איש קשר', 32 | 'Edit a contact': 'ערוך איש קשר', 33 | 'Delete this contact?': 'למחוק איש קשר זה?', 34 | 'Startup Name Generator': 'מחולל שמות אתחול', 35 | 'Saved Suggestions': 'הצעות שמורות', 36 | 'Current Language': 'שפה נוכחית', 37 | }; 38 | -------------------------------------------------------------------------------- /example/lib/src/app/model/translations/translations_russian.dart: -------------------------------------------------------------------------------- 1 | Map ruRU = { 2 | 'Demo App': 'Демонстрационное приложение', 3 | 'Counter Page Demo': 'Демонстрационная страница счетчика', 4 | 'You have pushed the button this many times:': 5 | 'Вы нажимали кнопку много раз:', 6 | 'Add': 'Добавлять', 7 | 'Interface:': 'Интерфейс', 8 | 'Application:': 'Применение:', 9 | 'Locale:': 'Регион', 10 | 'Colour Theme': 'Цветовая тема', 11 | 'About': 'О', 12 | 'Cancel': 'Отмена', 13 | 'Save': 'Сохранять', 14 | 'You deleted an item.': 'Вы удалили элемент.', 15 | 'You archived an item.': 'Вы заархивировали элемент.', 16 | 'id': 'я бы', 17 | 'Display Name': 'Показать имя', 18 | 'First Name': 'Имя', 19 | 'Middle Name': 'Второе имя', 20 | 'Last Name': 'Фамилия', 21 | 'Email': 'Электронное письмо', 22 | 'Phone': 'Телефон', 23 | 'Company': 'Компания', 24 | 'Job': 'Работа', 25 | 'home': 'домой', 26 | 'work': 'Работа', 27 | 'landline': 'стационарный', 28 | 'mobile': 'мобильный', 29 | 'other': 'разное', 30 | 'Cannot be empty': 'Не может быть пустым', 31 | 'Add a contact': 'Добавить контакт', 32 | 'Edit a contact': 'Изменить контакт', 33 | 'Delete this contact?': 'Удалить этот контакт?', 34 | 'Startup Name Generator': 'Генератор имени запуска', 35 | 'Saved Suggestions': 'Сохраненные предложения', 36 | 'Current Language': 'Текущий язык', 37 | }; 38 | -------------------------------------------------------------------------------- /example/lib/src/app/model/translations/translations_spanish.dart: -------------------------------------------------------------------------------- 1 | Map esAR = { 2 | 'Demo App': 'Aplicación de demostración', 3 | 'Counter Page Demo': 'Demostración de página de contador', 4 | 'You have pushed the button this many times:': 5 | 'Has pulsado el botón tantas veces:', 6 | 'Add': 'Agregar', 7 | 'Interface:': 'Interfaz', 8 | 'Application:': 'Solicitud:', 9 | 'Locale:': 'Lugar', 10 | 'Colour Theme': 'Tema de color', 11 | 'About': 'Acerca de', 12 | 'Cancel': 'Cancelar', 13 | 'Save': 'Ahorrar', 14 | 'You deleted an item.': 'Has eliminado un elemento.', 15 | 'You archived an item.': 'Has archivado un artículo.', 16 | 'id': 'identificación', 17 | 'Display Name': 'Nombre para mostrar', 18 | 'First Name': 'Primer nombre', 19 | 'Middle Name': 'Segundo nombre', 20 | 'Last Name': 'Apellido', 21 | 'Email': 'Correo electrónico', 22 | 'Phone': 'Teléfono', 23 | 'Company': 'Compañía', 24 | 'Job': 'Trabajo', 25 | 'home': 'casa', 26 | 'work': 'trabajo', 27 | 'landline': 'telefono fijo', 28 | 'mobile': 'móvil', 29 | 'other': 'otro', 30 | 'Cannot be empty': 'No puede estar vacío', 31 | 'Add a contact': 'Añade un contacto', 32 | 'Edit a contact': 'Editar un contacto', 33 | 'Delete this contact?': '¿Eliminar este contacto?', 34 | 'Startup Name Generator': 'Generador de nombres de inicio', 35 | 'Saved Suggestions': 'Sugerencias guardadas', 36 | 'Current Language': 'Idioma actual', 37 | }; 38 | -------------------------------------------------------------------------------- /example/lib/src/app/view.dart: -------------------------------------------------------------------------------- 1 | // application's main app 2 | export 'package:mvc_application_example/src/app/view/app.dart'; 3 | 4 | // application's menu bar 5 | export 'package:mvc_application_example/src/app/view/menu/app_menu.dart'; 6 | 7 | // Language Spinner 8 | export 'package:mvc_application_example/src/app/view/menu/iso_spinner.dart'; 9 | 10 | // Color picker routine 11 | export 'package:mvc_application_example/src/app/view/color_picker.dart'; 12 | -------------------------------------------------------------------------------- /example/lib/src/app/view/app.dart: -------------------------------------------------------------------------------- 1 | import 'package:mvc_application_example/src/controller.dart'; 2 | 3 | import 'package:mvc_application_example/src/view.dart'; 4 | 5 | /// App 6 | class TemplateApp extends AppStatefulWidget { 7 | TemplateApp({Key? key}) : super(key: key); 8 | 9 | // This is the 'View' of the application. 10 | @override 11 | AppState createAppState() => TemplateView(); 12 | } 13 | 14 | // This is the 'View' of the application. The 'look and feel' of the app. 15 | class TemplateView extends AppState { 16 | TemplateView() 17 | : super( 18 | con: TemplateController(), 19 | controllers: [ContactsController()], 20 | inTitle: () => 'Demo App'.tr, 21 | debugShowCheckedModeBanner: false, 22 | switchUI: Prefs.getBool('switchUI'), 23 | locale: AppTrs.textLocale, 24 | supportedLocales: AppTrs.supportedLocales, 25 | localizationsDelegates: [ 26 | AppTrs.delegate!, 27 | GlobalWidgetsLocalizations.delegate, 28 | GlobalCupertinoLocalizations.delegate, 29 | GlobalMaterialLocalizations.delegate, 30 | ], 31 | ); 32 | @override 33 | Widget onHome() => (con as TemplateController).onHome(); 34 | } 35 | -------------------------------------------------------------------------------- /example/lib/src/app/view/color_picker.dart: -------------------------------------------------------------------------------- 1 | /// 2 | /// Created 09 Feb 2019 3 | /// Andrious Solutions 4 | /// 5 | 6 | /// Import the interface 7 | import 'package:mvc_application_example/src/view.dart'; 8 | 9 | import 'package:flutter_material_color_picker/flutter_material_color_picker.dart'; 10 | 11 | class ColorPicker { 12 | static Color get color => _color; 13 | static set color(Color? color) { 14 | if (color != null) { 15 | _color = color; 16 | } 17 | } 18 | 19 | static Color _color = Colors.red; 20 | 21 | static ColorSwatch get colorSwatch => _colorSwatch; 22 | static set colorSwatch(ColorSwatch swatch) { 23 | _color = swatch; 24 | _colorSwatch = swatch; 25 | } 26 | 27 | static ColorSwatch _colorSwatch = Colors.red; 28 | 29 | static bool allowShades = false; 30 | static double get circleSize => _circleSize; 31 | static set circleSize(double size) { 32 | if (size > 1.0) { 33 | _circleSize = size; 34 | } 35 | } 36 | 37 | static double _circleSize = 60; 38 | 39 | static IconData iconSelected = Icons.check; 40 | 41 | // ignore: avoid_setters_without_getters 42 | static set onColorChange(ValueChanged func) => _onColorChange = func; 43 | static ValueChanged? _onColorChange; 44 | 45 | // ignore: avoid_setters_without_getters 46 | static set onChange(ValueChanged> func) => _onChange = func; 47 | static ValueChanged>? _onChange; 48 | 49 | static List> get colors => Colors.primaries; 50 | 51 | // static Text title = const Text('Colour Theme'); 52 | 53 | static Future?> showColorPicker({ 54 | required BuildContext context, 55 | ValueChanged? onColorChange, 56 | ValueChanged>? onChange, 57 | bool shrinkWrap = false, 58 | }) { 59 | return showDialog>( 60 | context: context, 61 | builder: (BuildContext context) => SimpleDialog(children: [ 62 | MaterialColorPicker( 63 | selectedColor: _color, 64 | onColorChange: (Color color) { 65 | _color = color; 66 | if (onColorChange != null) { 67 | onColorChange(color); 68 | } 69 | if (_onColorChange != null) { 70 | _onColorChange!(color); 71 | } 72 | Navigator.pop(context, color); 73 | }, 74 | onMainColorChange: (ColorSwatch? color) { 75 | _color = color!; 76 | _colorSwatch = color as ColorSwatch; 77 | if (onChange != null) { 78 | onChange(color); 79 | } 80 | if (_onChange != null) { 81 | _onChange!(color); 82 | } 83 | Navigator.pop(context, color); 84 | }, 85 | colors: colors, 86 | allowShades: allowShades, // default true 87 | iconSelected: iconSelected, 88 | circleSize: circleSize, 89 | shrinkWrap: shrinkWrap, 90 | ), 91 | ]), 92 | ); 93 | } 94 | 95 | @override 96 | // ignore: avoid_equals_and_hash_code_on_mutable_classes 97 | bool operator ==(Object other) => 98 | identical(this, other) || 99 | other is ColorPicker && runtimeType == other.runtimeType; 100 | 101 | @override 102 | // ignore: avoid_equals_and_hash_code_on_mutable_classes 103 | int get hashCode => 0; 104 | } 105 | 106 | enum DialogDemoAction { 107 | cancel, 108 | discard, 109 | disagree, 110 | agree, 111 | } 112 | -------------------------------------------------------------------------------- /example/lib/src/app/view/menu/app_menu.dart: -------------------------------------------------------------------------------- 1 | import 'package:mvc_application_example/src/model.dart'; 2 | 3 | import 'package:mvc_application_example/src/view.dart'; 4 | 5 | import 'package:mvc_application_example/src/controller.dart'; 6 | 7 | class PopMenu extends AppPopupMenu { 8 | // 9 | PopMenu({ 10 | Key? key, 11 | List? items, 12 | PopupMenuItemBuilder? itemBuilder, 13 | String? initialValue, 14 | PopupMenuItemSelected? onSelected, 15 | PopupMenuCanceled? onCanceled, 16 | String? tooltip, 17 | double? elevation, 18 | EdgeInsetsGeometry? padding, 19 | Widget? child, 20 | Widget? icon, 21 | Offset? offset, 22 | bool? enabled, 23 | ShapeBorder? shape, 24 | Color? color, 25 | bool? captureInheritedThemes, 26 | }) : _con = TemplateController(), 27 | super( 28 | key: key ?? const Key('appMenuButton'), 29 | items: items, 30 | itemBuilder: itemBuilder, 31 | initialValue: initialValue, 32 | onSelected: onSelected, 33 | onCanceled: onCanceled, 34 | tooltip: tooltip, 35 | elevation: elevation, 36 | padding: padding, 37 | child: child, 38 | icon: icon, 39 | offset: offset ?? const Offset(0, 45), 40 | enabled: enabled, 41 | shape: shape ?? 42 | RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), 43 | color: color, 44 | // false so to prevent the error, 45 | // "Looking up a deactivated widget's ancestor is unsafe." 46 | captureInheritedThemes: captureInheritedThemes ?? false, 47 | ); 48 | 49 | final TemplateController _con; 50 | 51 | // Supply what the interface 52 | String get application => _con.application; 53 | 54 | String get interface => App.useMaterial ? 'Material' : 'Cupertino'; 55 | 56 | @override 57 | List> get menuItems => [ 58 | PopupMenuItem( 59 | key: const Key('interfaceMenuItem'), 60 | value: 'interface', 61 | child: Text('${L10n.s('Interface:')} $interface'), 62 | ), 63 | PopupMenuItem( 64 | key: const Key('applicationMenuItem'), 65 | value: 'application', 66 | child: Text('${L10n.s('Application:')} $application'), 67 | ), 68 | PopupMenuItem( 69 | key: const Key('localeMenuItem'), 70 | value: 'locale', 71 | child: Text('${L10n.s('Locale:')} ${App.locale!.toLanguageTag()}'), 72 | ), 73 | if (App.useMaterial) 74 | PopupMenuItem( 75 | key: const Key('colorMenuItem'), 76 | value: 'color', 77 | child: L10n.t('Colour Theme'), 78 | ), 79 | PopupMenuItem( 80 | key: const Key('aboutMenuItem'), 81 | value: 'about', 82 | child: L10n.t('About'), 83 | ), 84 | ]; 85 | 86 | @override 87 | Future onSelection(String value) async { 88 | final appContext = App.context!; 89 | switch (value) { 90 | case 'interface': 91 | _con.changeUI(); 92 | break; 93 | case 'application': 94 | _con.changeApp(); 95 | break; 96 | case 'locale': 97 | final locales = App.supportedLocales!; 98 | 99 | final initialItem = locales.indexOf(App.locale!); 100 | 101 | final spinner = ISOSpinner( 102 | initialItem: initialItem, 103 | supportedLocales: locales, 104 | onSelectedItemChanged: (int index) async { 105 | // Retrieve the available locales. 106 | final locale = AppTrs.getLocale(index); 107 | if (locale != null) { 108 | App.locale = locale; 109 | App.refresh(); 110 | } 111 | }); 112 | 113 | await DialogBox( 114 | title: 'Current Language'.tr, 115 | body: [spinner], 116 | press01: () { 117 | spinner.onSelectedItemChanged(initialItem); 118 | }, 119 | press02: () {}, 120 | switchButtons: Settings.getLeftHanded(), 121 | ).show(); 122 | 123 | // If the current app is the 'counter' app 124 | if (_con.counterApp) { 125 | // Has to be initialized again for some reason?? 126 | _con.initTimer(); 127 | } 128 | 129 | break; 130 | case 'color': 131 | // Set the current colour. 132 | ColorPicker.color = App.themeData!.primaryColor; 133 | 134 | await ColorPicker.showColorPicker( 135 | context: appContext, 136 | onColorChange: _onColorChange, 137 | onChange: _onChange, 138 | shrinkWrap: true); 139 | break; 140 | case 'about': 141 | showAboutDialog( 142 | context: appContext, 143 | applicationName: App.vw?.title ?? '', 144 | applicationVersion: 145 | 'version: ${App.version} build: ${App.buildNumber}', 146 | ); 147 | break; 148 | default: 149 | } 150 | } 151 | 152 | void _onColorChange(Color value) { 153 | /// Implement to take in a color change. 154 | } 155 | 156 | /// Of course, the controller is to response to such user events. 157 | void _onChange([ColorSwatch? value]) => _con.onColorPicker(value); 158 | } 159 | -------------------------------------------------------------------------------- /example/lib/src/app/view/menu/iso_spinner.dart: -------------------------------------------------------------------------------- 1 | import 'package:mvc_application_example/src/controller.dart'; 2 | 3 | import 'package:mvc_application/view.dart' as s; 4 | 5 | /// A Spinner listing the available Locales. 6 | class ISOSpinner extends StatefulWidget { 7 | const ISOSpinner({ 8 | this.initialItem, 9 | Key? key, 10 | }) : super(key: key); 11 | final int? initialItem; 12 | 13 | /// Retrieve the available locales. 14 | List locales() => AppTrs.supportedLocales; 15 | 16 | /// Assign the specified Locale. 17 | Future onSelectedItemChanged(int index) async { 18 | final List? localesList = locales(); 19 | if (localesList != null) { 20 | s.App.locale = localesList[index]; 21 | await s.Prefs.setString('locale', localesList[index].toLanguageTag()); 22 | s.App.refresh(); 23 | } 24 | } 25 | 26 | @override 27 | State createState() => _SpinnerState(); 28 | } 29 | 30 | class _SpinnerState extends State { 31 | @override 32 | void initState() { 33 | super.initState(); 34 | locales = widget.locales(); 35 | int? index; 36 | if (widget.initialItem != null && widget.initialItem! > -1) { 37 | index = widget.initialItem!; 38 | } else { 39 | index = locales?.indexOf(s.App.locale!); 40 | if (index == null || index < 0) { 41 | index = 0; 42 | } 43 | } 44 | controller = FixedExtentScrollController(initialItem: index); 45 | } 46 | 47 | List? locales; 48 | late FixedExtentScrollController controller; 49 | 50 | @override 51 | Widget build(BuildContext context) => SizedBox( 52 | height: 100, 53 | child: s.CupertinoPicker.builder( 54 | itemExtent: 25, //height of each item 55 | childCount: locales?.length, 56 | scrollController: controller, 57 | onSelectedItemChanged: widget.onSelectedItemChanged, 58 | itemBuilder: (BuildContext context, int index) => Text( 59 | locales?[index].countryCode == null 60 | ? '${locales?[index].languageCode}' 61 | : '${locales?[index].languageCode}-${locales?[index].countryCode}', 62 | style: const TextStyle(fontSize: 20), 63 | ), 64 | )); 65 | } 66 | -------------------------------------------------------------------------------- /example/lib/src/controller.dart: -------------------------------------------------------------------------------- 1 | // The MVC application framework. 2 | export 'package:mvc_application/controller.dart'; 3 | 4 | // The controller for the app as a whole. 5 | export 'package:mvc_application_example/src/app/controller.dart'; 6 | 7 | // The controller for the home screen. 8 | export 'package:mvc_application_example/src/home/controller.dart'; 9 | -------------------------------------------------------------------------------- /example/lib/src/home/controller.dart: -------------------------------------------------------------------------------- 1 | /// 2 | /// The controllers involved for the home screen. 3 | /// 4 | 5 | // The App Controller 6 | export 'package:mvc_application_example/src/home/controller/template_controller.dart'; 7 | 8 | // The Contacts app 9 | export 'package:mvc_application_example/src/home/controller/contacts_controller.dart'; 10 | 11 | // The Counter app 12 | export 'package:mvc_application_example/src/home/controller/counter_controller.dart'; 13 | 14 | // The Word Pairs app 15 | export 'package:mvc_application_example/src/home/controller/wordpairs_controller.dart'; 16 | -------------------------------------------------------------------------------- /example/lib/src/home/controller/contacts_controller.dart: -------------------------------------------------------------------------------- 1 | import 'package:mvc_application_example/src/view.dart'; 2 | 3 | import 'package:mvc_application_example/src/controller.dart'; 4 | 5 | import 'package:mvc_application_example/src/home/model/contacts/contact.dart'; 6 | 7 | import 'package:mvc_application_example/src/home/model/contacts/contacts_db.dart'; 8 | 9 | class ContactsController extends AppController { 10 | // 11 | factory ContactsController([StateMVC? state]) => 12 | _this ??= ContactsController._(state); 13 | 14 | ContactsController._([StateMVC? state]) 15 | : model = ContactsDB(), 16 | super(state); 17 | final ContactsDB model; 18 | static ContactsController? _this; 19 | 20 | @override 21 | Future initAsync() async { 22 | _sortedAlpha = Prefs.getBool(sortKEY, false); 23 | final init = await model.initState(); 24 | if (init) { 25 | await getContacts(); 26 | } 27 | return init; 28 | } 29 | 30 | static late bool _sortedAlpha; 31 | static const String sortKEY = 'sort_by_alpha'; 32 | 33 | @override 34 | bool onAsyncError(FlutterErrorDetails details) { 35 | /// Supply an 'error handler' routine if something goes wrong 36 | /// in the corresponding initAsync() routine. 37 | /// Returns true if the error was properly handled. 38 | return false; 39 | } 40 | 41 | // Merely for demonstration purposes. Erase if not using. 42 | /// The framework calls this method whenever it removes this [State] object 43 | /// from the tree. 44 | @override 45 | void deactivate() { 46 | super.deactivate(); 47 | } 48 | 49 | @override 50 | void dispose() { 51 | model.dispose(); 52 | super.dispose(); 53 | } 54 | 55 | Future> getContacts() async { 56 | _contacts = await model.getContacts(); 57 | if (_sortedAlpha) { 58 | _contacts!.sort(); 59 | } 60 | return _contacts!; 61 | } 62 | 63 | @override 64 | Future refresh() async { 65 | await getContacts(); 66 | super.refresh(); 67 | } 68 | 69 | /// Called by menu option 70 | Future> sort() async { 71 | _sortedAlpha = !_sortedAlpha; 72 | await Prefs.setBool(sortKEY, _sortedAlpha); 73 | await refresh(); 74 | return _contacts!; 75 | } 76 | 77 | final GlobalKey scaffoldKey = GlobalKey(); 78 | 79 | List? get items => _contacts; 80 | List? _contacts; 81 | 82 | Contact? itemAt(int index) => items?.elementAt(index); 83 | 84 | Future deleteItem(int index) async { 85 | final Contact? contact = items?.elementAt(index); 86 | var delete = contact != null; 87 | if (delete) { 88 | delete = await contact.delete(); 89 | } 90 | await refresh(); 91 | return delete; 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /example/lib/src/home/controller/counter_controller.dart: -------------------------------------------------------------------------------- 1 | import 'package:mvc_application_example/src/controller.dart' 2 | show App, AppController, State, TemplateController, Widget, WordPairsTimer; 3 | 4 | import 'package:mvc_application_example/src/model.dart' 5 | show CounterModel, State, Widget; 6 | 7 | class CounterController extends AppController { 8 | factory CounterController() => _this ??= CounterController._(); 9 | CounterController._() : super() { 10 | // 11 | _model = CounterModel(); // CounterModel(useDouble: true); 12 | 13 | /// Provide the 'timer' controller to the interface. 14 | wordPairsTimer = WordPairsTimer(); 15 | } 16 | static CounterController? _this; 17 | late final CounterModel _model; 18 | late final WordPairsTimer wordPairsTimer; 19 | 20 | @override 21 | void initState() { 22 | super.initState(); 23 | // Add this controller to the State object's lifecycle. 24 | wordPairsTimer.addState(state); 25 | } 26 | 27 | // Merely for demonstration purposes. Erase if not using. 28 | /// The framework calls this method whenever it removes this [State] object 29 | /// from the tree. 30 | @override 31 | void deactivate() { 32 | super.deactivate(); 33 | } 34 | 35 | // Merely for demonstration purposes. Erase if not using. 36 | /// The framework calls this method when this [State] object will never 37 | /// build again. 38 | /// Note: THERE IS NO GUARANTEE THIS METHOD WILL RUN in the Framework. 39 | @override 40 | void dispose() { 41 | super.dispose(); 42 | } 43 | 44 | /// the 'counter' value. 45 | String get data => _model.data; 46 | 47 | /// The 'View' is calling setState() 48 | void onPressed() => _model.onPressed(); 49 | 50 | /// Supply the word pair 51 | Widget get wordPair => wordPairsTimer.wordPair; 52 | 53 | /// Access to the timer 54 | WordPairsTimer get timer => wordPairsTimer; 55 | 56 | /// The 'Controller' is calling the 'View' to call setState() 57 | // void onPressed() => setState(() => _model.onPressed()); 58 | 59 | /// Retrieve the app's own controller. 60 | TemplateController get appController => 61 | _appController ??= App.vw!.con as TemplateController; 62 | TemplateController? _appController; 63 | } 64 | -------------------------------------------------------------------------------- /example/lib/src/home/controller/template_controller.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async' show unawaited; 2 | 3 | import 'package:mvc_application_example/src/controller.dart'; 4 | 5 | // You can see 'at a glance' this Controller also 'talks to' the interface (View). 6 | import 'package:mvc_application_example/src/view.dart'; 7 | 8 | class TemplateController extends AppController { 9 | factory TemplateController() => _this ??= TemplateController._(); 10 | TemplateController._() 11 | : wordPairsTimer = WordPairsController(), 12 | super(); 13 | static TemplateController? _this; 14 | 15 | final WordPairsController wordPairsTimer; 16 | 17 | // Assign to the 'leading' widget on the interface. 18 | void leading() => changeUI(); 19 | 20 | /// Switch to the other User Interface. 21 | void changeUI() { 22 | // 23 | Navigator.popUntil(App.context!, ModalRoute.withName('/')); 24 | 25 | // This has to be called first. 26 | App.changeUI(App.useMaterial ? 'Cupertino' : 'Material'); 27 | 28 | bool switchUI; 29 | if (App.useMaterial) { 30 | if (UniversalPlatform.isAndroid) { 31 | switchUI = false; 32 | } else { 33 | switchUI = true; 34 | } 35 | } else { 36 | if (UniversalPlatform.isAndroid) { 37 | switchUI = true; 38 | } else { 39 | switchUI = false; 40 | } 41 | } 42 | Prefs.setBool('switchUI', switchUI); 43 | } 44 | 45 | /// Indicate if the Counter app is to run. 46 | bool get counterApp => _appNames[_appCount] == 'Counter'; 47 | 48 | /// Indicate if the Words app is to run. 49 | bool get wordsApp => _appNames[_appCount] == 'Word Pairs'; 50 | 51 | /// Indicate if the Contacts app is to run. 52 | bool get contactsApp => _appNames[_appCount] == 'Contacts'; 53 | 54 | int _appCount = 0; 55 | final _appNames = ['Counter', 'Word Pairs', 'Contacts']; 56 | 57 | Widget onHome() { 58 | // 59 | _appCount = Prefs.getInt('appRun'); 60 | 61 | final Key key = UniqueKey(); 62 | 63 | Widget? widget; 64 | 65 | switch (_appNames[_appCount]) { 66 | case 'Word Pairs': 67 | widget = WordPairs(key: key); 68 | break; 69 | case 'Counter': 70 | widget = CounterPage(key: key); 71 | break; 72 | case 'Contacts': 73 | widget = ContactsList(key: key); 74 | break; 75 | default: 76 | widget = const SizedBox(); 77 | } 78 | return widget; 79 | } 80 | 81 | // Supply what the interface 82 | String get application => _appNames[_appCount]; 83 | 84 | /// Switch to the other application. 85 | void changeApp([String? appName = '']) { 86 | // 87 | if (appName == null || 88 | appName.isEmpty || 89 | !_appNames.contains(appName.trim())) { 90 | // 91 | _appCount++; 92 | if (_appCount == _appNames.length) { 93 | _appCount = 0; 94 | } 95 | } else { 96 | _appCount = _appNames.indexOf(appName.trim()); 97 | } 98 | 99 | unawaited(Prefs.setBool('words', _appNames[_appCount] == 'Word')); 100 | 101 | Prefs.setInt('appRun', _appCount).then((value) => App.refresh()); 102 | } 103 | 104 | /// Working with the ColorPicker to change the app's color theme 105 | void onColorPicker([ColorSwatch? value]) { 106 | // 107 | App.setThemeData(value); 108 | App.refresh(); 109 | } 110 | 111 | // /// Retrieve the app's own controller. 112 | // TemplateController get appController => 113 | // _appController ??= App.vw!.con as TemplateController; 114 | // TemplateController? _appController; 115 | 116 | /// Supply the app's popupmenu 117 | Widget popupMenu({ 118 | Key? key, 119 | List? items, 120 | PopupMenuItemBuilder? itemBuilder, 121 | String? initialValue, 122 | PopupMenuItemSelected? onSelected, 123 | PopupMenuCanceled? onCanceled, 124 | String? tooltip, 125 | double? elevation, 126 | EdgeInsetsGeometry? padding, 127 | Widget? child, 128 | Widget? icon, 129 | Offset? offset, 130 | bool? enabled, 131 | ShapeBorder? shape, 132 | Color? color, 133 | bool? captureInheritedThemes, 134 | }) => 135 | PopMenu( 136 | key: key, 137 | items: items, 138 | itemBuilder: itemBuilder, 139 | initialValue: initialValue, 140 | onSelected: onSelected, 141 | onCanceled: onCanceled, 142 | tooltip: tooltip, 143 | elevation: elevation, 144 | padding: padding, 145 | child: child, 146 | icon: icon, 147 | offset: offset, 148 | enabled: enabled, 149 | shape: shape, 150 | color: color, 151 | captureInheritedThemes: captureInheritedThemes, 152 | ).popupMenuButton; 153 | 154 | /// Start up the timer 155 | void initTimer() => wordPairsTimer.initTimer(); 156 | 157 | /// Cancel the timer 158 | void cancelTimer() => wordPairsTimer.cancelTimer(); 159 | } 160 | -------------------------------------------------------------------------------- /example/lib/src/home/controller/wordpairs_controller.dart: -------------------------------------------------------------------------------- 1 | import 'package:mvc_application_example/src/controller.dart'; 2 | 3 | import 'package:mvc_application_example/src/model.dart'; 4 | 5 | // You can see 'at a glance' this Controller also 'talks to' the interface (View). 6 | import 'package:mvc_application_example/src/view.dart'; 7 | 8 | class WordPairsController extends ControllerMVC { 9 | factory WordPairsController([StateMVC? state]) => 10 | _this ??= WordPairsController._(state); 11 | WordPairsController._(StateMVC? state) 12 | : timer = WordPairsTimer(seconds: 2), 13 | model = WordPairsModel(), 14 | super(state); 15 | static WordPairsController? _this; 16 | final WordPairsTimer timer; 17 | final WordPairsModel model; 18 | 19 | @override 20 | void initState() { 21 | super.initState(); 22 | model.addState(state); 23 | } 24 | 25 | /// Start up the timer. 26 | void initTimer() => timer.initTimer(); 27 | 28 | /// Cancel the timer 29 | void cancelTimer() => timer.timer?.cancel(); 30 | 31 | Widget get wordPair => timer.wordPair; 32 | 33 | void build(int i) => model.build(i); 34 | 35 | String get data => model.data; 36 | 37 | Widget get trailing => model.trailing; 38 | 39 | void onTap(int i) => model.onTap(i); 40 | 41 | Iterable tiles() => model.tiles(); 42 | } 43 | -------------------------------------------------------------------------------- /example/lib/src/home/model.dart: -------------------------------------------------------------------------------- 1 | /// 2 | /// All the data involved for the home screen. 3 | /// 4 | 5 | // Counter app data 6 | export 'package:mvc_application_example/src/home/model/counter/counter_model.dart'; 7 | 8 | export 'package:mvc_application_example/src/home/model/settings.dart'; 9 | 10 | // Word pairs 11 | export 'package:mvc_application_example/src/home/model/words/wordpairs_model.dart'; 12 | -------------------------------------------------------------------------------- /example/lib/src/home/model/contacts/contact.dart: -------------------------------------------------------------------------------- 1 | import 'package:mvc_application_example/src/view.dart'; 2 | 3 | import 'contact_fields.dart'; 4 | 5 | import 'contacts_db.dart'; 6 | 7 | class Contact extends ContactEdit implements Comparable { 8 | // 9 | Contact() { 10 | populate(); 11 | } 12 | 13 | Contact.fromMap(Map map) { 14 | populate(map); 15 | } 16 | 17 | @override 18 | int compareTo(Contact other) => 19 | _givenName.value.toString().compareTo(other._givenName.value.toString()); 20 | } 21 | 22 | class ContactEdit extends ContactList { 23 | ContactEdit() { 24 | model = ContactsDB(); 25 | } 26 | late ContactsDB model; 27 | 28 | GlobalKey get formKey { 29 | if (!_inForm) { 30 | _inForm = true; 31 | } 32 | return _formKey; 33 | } 34 | 35 | bool _inForm = false; 36 | 37 | final GlobalKey _formKey = GlobalKey(); 38 | 39 | Future onPressed([BuildContext? context]) async { 40 | if (!_formKey.currentState!.validate()) { 41 | return false; 42 | } 43 | _formKey.currentState!.save(); 44 | _inForm = false; 45 | final added = await add(); 46 | return added; 47 | } 48 | 49 | Future add() => model.addContact(this as Contact); 50 | 51 | Future delete() => model.deleteContact(this as Contact); 52 | 53 | Future undelete() => model.undeleteContact(this as Contact); 54 | 55 | bool isChanged() => _givenName.changedFields 56 | .where((field) => field is! Phone && field is! Email) 57 | .isNotEmpty; 58 | 59 | bool phoneChange() => _givenName.changeIn(); 60 | 61 | bool emailChange() => _givenName.changeIn(); 62 | } 63 | 64 | class ContactList extends ContactFields { 65 | // 66 | List? _emails, _phones; 67 | 68 | void populate([Map? map]) { 69 | // 70 | final ma = MapClass(map); 71 | 72 | _id = Id(ma.p('id')); 73 | _givenName = GivenName(ma.p('givenName')); 74 | _middleName = MiddleName(ma.p('middleName')); 75 | _familyName = FamilyName(ma.p('familyName')); 76 | 77 | _displayName = displayName(this as Contact); 78 | _company = Company(ma.p('company')); 79 | _jobTitle = JobTitle(ma.p('jobTitle')); 80 | _phone = Phone(ma.p('phones')); 81 | _email = Email(ma.p('emails')); 82 | } 83 | 84 | Map get toMap { 85 | // 86 | final emailList = email.mapItems( 87 | 'email', 88 | _emails, 89 | (data) => Email.init(data), 90 | ); 91 | 92 | final phoneList = phone.mapItems( 93 | 'phone', 94 | _phones, 95 | (data) => Phone.init(data), 96 | ); 97 | 98 | return { 99 | 'id': _id.value, 100 | 'displayName': _displayName.value, 101 | 'givenName': _givenName.value, 102 | 'middleName': _middleName.value, 103 | 'familyName': _familyName.value, 104 | 'emails': emailList, 105 | 'phones': phoneList, 106 | 'company': _company.value, 107 | 'jobTitle': _jobTitle.value, 108 | }; 109 | } 110 | } 111 | 112 | class ContactFields with StateGetter { 113 | // 114 | late FormFields _id, 115 | _displayName, 116 | _givenName, 117 | _middleName, 118 | _familyName, 119 | _phone, 120 | _email, 121 | _company, 122 | _jobTitle; 123 | 124 | /// Attach to a State object 125 | @override 126 | bool pushState([StateMVC? state]) { 127 | _id.pushState(state); 128 | _displayName.pushState(state); 129 | _givenName.pushState(state); 130 | _middleName.pushState(state); 131 | _familyName.pushState(state); 132 | _phone.pushState(state); 133 | _email.pushState(state); 134 | _company.pushState(state); 135 | _jobTitle.pushState(state); 136 | return super.pushState(state); 137 | } 138 | 139 | /// Detach from the State object 140 | @override 141 | bool popState() { 142 | _id.popState(); 143 | _displayName.popState(); 144 | _givenName.popState(); 145 | _middleName.popState(); 146 | _familyName.popState(); 147 | _phone.popState(); 148 | _email.popState(); 149 | _company.popState(); 150 | _jobTitle.popState(); 151 | return super.popState(); 152 | } 153 | 154 | Id get id => _id as Id; 155 | set id(Id id) => _id = id; 156 | 157 | DisplayName get displayName => _displayName as DisplayName; 158 | set displayName(DisplayName name) => _displayName = name; 159 | 160 | GivenName get givenName => _givenName as GivenName; 161 | set givenName(GivenName name) => _givenName = name; 162 | 163 | MiddleName get middleName => _middleName as MiddleName; 164 | set middleName(MiddleName name) => _middleName = name; 165 | 166 | FamilyName get familyName => _familyName as FamilyName; 167 | set familyName(FamilyName name) => _familyName = name; 168 | 169 | Company get company => _company as Company; 170 | set company(Company company) => _company = company; 171 | 172 | JobTitle get jobTitle => _jobTitle as JobTitle; 173 | set jobTitle(JobTitle job) => _jobTitle = job; 174 | 175 | Phone get phone => _phone as Phone; 176 | set phone(Phone phone) => _phone = phone; 177 | 178 | Email get email => _email as Email; 179 | set email(Email email) => _email = email; 180 | } 181 | -------------------------------------------------------------------------------- /example/lib/src/home/model/contacts/contact_fields.dart: -------------------------------------------------------------------------------- 1 | import 'package:mvc_application_example/src/view.dart'; 2 | 3 | import 'contact.dart' show Contact; 4 | 5 | /// Add to the class this: 6 | /// `extends FieldWidgets with FieldChange` 7 | mixin FormFields on FieldWidgets { 8 | // 9 | Set> get changedFields => _changedFields; 10 | static final Set> _changedFields = {}; 11 | 12 | /// If the field's value changed, that field is added to a Set. 13 | @override 14 | void onSaved(dynamic v) { 15 | super.onSaved(v); 16 | if (isChanged()) { 17 | _changedFields.add(this); 18 | } 19 | } 20 | 21 | bool changeIn() => changedFields.whereType().isNotEmpty; 22 | } 23 | 24 | class Id extends FieldWidgets with FormFields { 25 | Id(dynamic value) : super(label: 'Identifier', value: value); 26 | } 27 | 28 | String? notEmpty(String? v) => 29 | v != null && v.isEmpty ? 'Cannot be empty' : null; 30 | 31 | FormFields displayName(Contact contact) { 32 | String? display; 33 | 34 | if (contact.givenName.value != null) { 35 | display = contact.givenName.value ?? ''; 36 | display = '$display ${contact.familyName.value}'; 37 | } 38 | display ??= ''; 39 | return DisplayName(display, Text(display)); 40 | } 41 | 42 | class DisplayName extends FieldWidgets with FormFields { 43 | DisplayName(String value, Widget child) 44 | : super( 45 | label: 'Display Name'.tr, 46 | value: value, 47 | child: child, 48 | ); 49 | } 50 | 51 | class GivenName extends FieldWidgets with FormFields { 52 | GivenName([dynamic value]) 53 | : super( 54 | label: 'First Name'.tr, 55 | value: value, 56 | validator: notEmpty, 57 | keyboardType: TextInputType.name, 58 | ); 59 | } 60 | 61 | class MiddleName extends FieldWidgets with FormFields { 62 | MiddleName([dynamic value]) 63 | : super( 64 | label: 'Middle Name'.tr, 65 | value: value, 66 | keyboardType: TextInputType.name, 67 | ); 68 | } 69 | 70 | class FamilyName extends FieldWidgets with FormFields { 71 | FamilyName([dynamic value]) 72 | : super( 73 | label: 'Last Name'.tr, 74 | value: value, 75 | validator: notEmpty, 76 | keyboardType: TextInputType.name, 77 | ); 78 | } 79 | 80 | class Company extends FieldWidgets with FormFields { 81 | Company([dynamic value]) 82 | : super( 83 | label: 'Company'.tr, 84 | value: value, 85 | keyboardType: TextInputType.name, 86 | ); 87 | } 88 | 89 | class JobTitle extends FieldWidgets with FormFields { 90 | JobTitle([dynamic value]) 91 | : super( 92 | label: 'Job'.tr, 93 | value: value, 94 | keyboardType: TextInputType.name, 95 | ); 96 | } 97 | 98 | class Phone extends FieldWidgets with FormFields { 99 | // 100 | Phone([dynamic value]) 101 | : super( 102 | label: 'Phone'.tr, 103 | value: value, 104 | inputDecoration: const InputDecoration(labelText: 'Phone'), 105 | keyboardType: TextInputType.phone, 106 | ) { 107 | // Change the name of the map's key fields. 108 | keys(value: 'phone'); 109 | // There may be more than one phone number 110 | one2Many(() => Phone()); 111 | } 112 | 113 | Phone.init(DataFieldItem dataItem) 114 | : super( 115 | label: dataItem.label, 116 | value: dataItem.value, 117 | type: dataItem.type, 118 | ); 119 | 120 | @override 121 | ListItems onListItems({ 122 | String? title, 123 | List>? items, 124 | MapItemFunction? mapItem, 125 | GestureTapCallback? onTap, 126 | ValueChanged? onChanged, 127 | List? dropItems, 128 | }) => 129 | super.onListItems( 130 | title: title, 131 | items: items, 132 | mapItem: mapItem, 133 | onTap: onTap, 134 | onChanged: onChanged ?? (String? value) => state!.setState(() {}), 135 | dropItems: dropItems ?? 136 | ['home'.tr, 'work'.tr, 'landline'.tr, 'mobile'.tr, 'other'.tr], 137 | ); 138 | 139 | @override 140 | List> mapItems>( 141 | String key, 142 | List? items, 143 | U Function(DataFieldItem dataItem) create, 144 | [U? itemsObj]) { 145 | // 146 | final list = super.mapItems(key, items, create, itemsObj); 147 | 148 | //ignore: unnecessary_cast 149 | for (int cnt = 0; cnt <= this.items!.length - 1; cnt++) { 150 | // 151 | final phone = this.items!.elementAt(cnt) as Phone; 152 | 153 | list[cnt]['initValue'] = phone.initialValue; 154 | } 155 | return list; 156 | } 157 | } 158 | 159 | class Email extends FieldWidgets with FormFields { 160 | Email([dynamic value]) 161 | : super( 162 | label: 'Email'.tr, 163 | value: value, 164 | inputDecoration: const InputDecoration(labelText: 'Email'), 165 | keyboardType: TextInputType.emailAddress, 166 | ) { 167 | // There may be more than one email address. 168 | one2Many(() => Email()); 169 | } 170 | 171 | Email.init(DataFieldItem dataItem) 172 | : super( 173 | label: dataItem.label, 174 | value: dataItem.value, 175 | type: dataItem.type, 176 | ); 177 | 178 | @override 179 | ListItems onListItems({ 180 | String? title, 181 | List>? items, 182 | MapItemFunction? mapItem, 183 | GestureTapCallback? onTap, 184 | ValueChanged? onChanged, 185 | List? dropItems, 186 | }) => 187 | super.onListItems( 188 | title: title, 189 | items: items, 190 | mapItem: mapItem, 191 | onTap: onTap, 192 | dropItems: dropItems ?? ['home'.tr, 'work'.tr, 'other'.tr], 193 | onChanged: onChanged ?? 194 | (String? value) { 195 | state!.setState(() {}); 196 | }, 197 | ); 198 | } 199 | -------------------------------------------------------------------------------- /example/lib/src/home/model/contacts/contacts_db.dart: -------------------------------------------------------------------------------- 1 | import 'dart:core'; 2 | 3 | import 'package:dbutils/sqlite_db.dart' show Database, SQLiteDB, Transaction; 4 | 5 | import 'package:mvc_application_example/src/view.dart' 6 | show DataFieldItem, showBox; 7 | 8 | import 'contact.dart' show Contact; 9 | 10 | class ContactsDB extends SQLiteDB { 11 | factory ContactsDB() => _this ??= ContactsDB._(); 12 | ContactsDB._() : super(); 13 | 14 | /// Make only one instance of this class. 15 | static ContactsDB? _this; 16 | 17 | @override 18 | String get name => 'Contacts'; 19 | 20 | @override 21 | int get version => 1; 22 | 23 | Future initState() => init(); 24 | 25 | void dispose() => disposed(); 26 | 27 | @override 28 | Future onConfigure(Database db) { 29 | return db.execute('PRAGMA foreign_keys=ON;'); 30 | } 31 | 32 | @override 33 | Future onCreate(Database db, int version) async { 34 | await db.transaction((Transaction txn) async { 35 | await txn.execute(''' 36 | CREATE TABLE Contacts( 37 | id INTEGER PRIMARY KEY 38 | ,givenName TEXT 39 | ,middleName TEXT 40 | ,familyName TEXT 41 | ,company TEXT 42 | ,jobTitle TEXT 43 | ,deleted INTEGER DEFAULT 0 44 | ) 45 | '''); 46 | 47 | await txn.execute(''' 48 | CREATE TABLE Emails( 49 | id INTEGER PRIMARY KEY 50 | ,userid INTEGER 51 | ,type TEXT 52 | ,email TEXT 53 | ,deleted INTEGER DEFAULT 0 54 | ,FOREIGN KEY (userid) REFERENCES Contacts (id) 55 | ) 56 | '''); 57 | 58 | await txn.execute(''' 59 | CREATE TABLE Phones( 60 | id INTEGER PRIMARY KEY 61 | ,userid INTEGER 62 | ,type TEXT 63 | ,phone TEXT 64 | ,deleted INTEGER DEFAULT 0 65 | ) 66 | '''); 67 | }, exclusive: true); 68 | } 69 | 70 | Future> getContacts() async { 71 | return listContacts( 72 | await _this!.rawQuery('SELECT * FROM Contacts WHERE deleted = 0')); 73 | } 74 | 75 | Future> listContacts(List> query) async { 76 | // 77 | final contactList = []; 78 | 79 | for (final contact in query) { 80 | // 81 | final map = contact.map((key, value) { 82 | return MapEntry(key, value is int ? value.toString() : value); 83 | }); 84 | 85 | final phones = await _this!.rawQuery( 86 | 'SELECT * FROM Phones WHERE userid = ${contact['id']} AND deleted = 0'); 87 | 88 | map['phones'] = 89 | phones.map((m) => DataFieldItem.fromMap(m, value: 'phone')).toList(); 90 | 91 | final emails = await _this!.rawQuery( 92 | 'SELECT * FROM Emails WHERE userid = ${contact['id']} AND deleted = 0'); 93 | 94 | map['emails'] = 95 | emails.map((m) => DataFieldItem.fromMap(m, value: 'email')).toList(); 96 | 97 | final _contact = Contact.fromMap(map); 98 | 99 | contactList.add(_contact); 100 | } 101 | 102 | return contactList; 103 | } 104 | 105 | Future addContact(Contact contact) async { 106 | // 107 | var add = true; 108 | 109 | final map = contact.toMap; 110 | 111 | // The Contact's unique id 112 | dynamic id = map['id']; 113 | 114 | if (contact.isChanged()) { 115 | // 116 | final newContact = await _this!.saveMap('Contacts', map); 117 | 118 | id ??= newContact['id']; 119 | 120 | add = newContact.isNotEmpty; 121 | } 122 | 123 | // Save Phone Numbers 124 | if (add && contact.phoneChange()) { 125 | // 126 | for (final Map phone in map['phones']) { 127 | // 128 | if (phone.isEmpty) { 129 | continue; 130 | } 131 | 132 | phone.addAll({'userid': id}); 133 | 134 | final phoneNumber = phone['phone'] as String; 135 | 136 | if (phoneNumber.isEmpty) { 137 | // 138 | phone['phone'] = phone['initValue']; 139 | 140 | final delete = await showBox( 141 | text: 'Deleting this Phone number?', 142 | context: contact.state!.context); 143 | 144 | if (delete) { 145 | phone.addAll({'deleted': 1}); 146 | } 147 | } 148 | 149 | await _this!.saveMap('Phones', phone); 150 | } 151 | } 152 | 153 | if (add && contact.emailChange()) { 154 | // Save Emails. 155 | for (final Map email in map['emails']) { 156 | // 157 | if (email.isEmpty) { 158 | continue; 159 | } 160 | 161 | email.addAll({'userid': id}); 162 | 163 | final emailAddress = email['email'] as String; 164 | 165 | if (emailAddress.isEmpty) { 166 | // 167 | email['phone'] = email['initValue']; 168 | 169 | final delete = await showBox( 170 | text: 'Deleting this email?', context: contact.state!.context); 171 | 172 | if (delete) { 173 | email.addAll({'deleted': 1}); 174 | } 175 | } 176 | 177 | await _this!.saveMap('Emails', email); 178 | } 179 | } 180 | return add; 181 | } 182 | 183 | void func(key, value) {} 184 | 185 | Future deleteContact(Contact contact) async { 186 | // 187 | final map = contact.toMap; 188 | // 189 | final id = map['id']; 190 | 191 | if (id == null) { 192 | return Future.value(false); 193 | } 194 | 195 | Map rec; 196 | 197 | rec = _this!.newRec('Contacts', map); 198 | 199 | rec['deleted'] = 1; 200 | 201 | rec = await _this!.saveMap('Contacts', rec); 202 | 203 | if (rec.isNotEmpty) { 204 | // 205 | for (final Map phone in map['phones']) { 206 | // 207 | rec = _this!.newRec('Phones', phone); 208 | 209 | rec['deleted'] = 1; 210 | 211 | await _this!.saveMap('Phones', rec); 212 | } 213 | 214 | for (final Map email in map['emails']) { 215 | // 216 | rec = _this!.newRec('Emails', email); 217 | 218 | rec['deleted'] = 1; 219 | 220 | await _this!.saveMap('Emails', rec); 221 | } 222 | } 223 | 224 | return rec.isNotEmpty; 225 | } 226 | 227 | Future undeleteContact(Contact contact) async { 228 | // 229 | final map = contact.toMap; 230 | 231 | var id = map['id']; 232 | 233 | if (id == null) { 234 | return Future.value(0); 235 | } 236 | 237 | if (id is String) { 238 | id = int.parse(id); 239 | } 240 | 241 | var query = 242 | await _this!.rawQuery('UPDATE Contacts SET deleted = 0 WHERE id = $id'); 243 | final rec = query.length; 244 | 245 | if (rec > 0) { 246 | query = 247 | await _this!.rawQuery('UPDATE Phones SET deleted = 0 WHERE id = $id'); 248 | 249 | query = 250 | await _this!.rawQuery('UPDATE Emails SET deleted = 0 WHERE id = $id'); 251 | } 252 | return rec; 253 | } 254 | } 255 | -------------------------------------------------------------------------------- /example/lib/src/home/model/counter/counter_model.dart: -------------------------------------------------------------------------------- 1 | /// Model 2 | 3 | class CounterModel { 4 | factory CounterModel({bool? useDouble}) => 5 | _this ??= CounterModel._(useDouble); 6 | CounterModel._(bool? useDouble) { 7 | _useDouble = useDouble ?? false; 8 | _integer = 0; 9 | _double = 0; 10 | } 11 | static CounterModel? _this; 12 | bool _useDouble = false; 13 | late int _integer; 14 | late double _double; 15 | 16 | String get data => _useDouble ? _double.toStringAsFixed(2) : '$_integer'; 17 | 18 | void onPressed() => _useDouble ? _double = _double + 0.01 : _integer++; 19 | } 20 | 21 | // class CounterModel { 22 | // int _counter = 0; 23 | // 24 | // String get data => '$_counter'; 25 | // 26 | // void onPressed() => _counter++; 27 | // } 28 | 29 | // class CounterModel { 30 | // double _counter = 6.00; 31 | // 32 | // String get data => _counter.toStringAsFixed(2); 33 | // 34 | // void onPressed() => _counter = _counter + 0.01; 35 | // } 36 | -------------------------------------------------------------------------------- /example/lib/src/home/model/settings.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async' show Future; 2 | 3 | import 'package:flutter/material.dart' 4 | show StatelessWidget, TextStyle, VoidCallback; 5 | 6 | import 'package:mvc_application_example/src/view.dart' show AppSettings, Prefs; 7 | 8 | class Settings { 9 | // 10 | static bool get(String? setting) { 11 | if (setting == null || setting.trim().isEmpty) { 12 | return false; 13 | } 14 | return Prefs.getBool(setting, false); 15 | } 16 | 17 | static Future set(String? setting, bool value) { 18 | if (setting == null || setting.trim().isEmpty) { 19 | return Future.value(false); 20 | } 21 | return Prefs.setBool(setting, value); 22 | } 23 | 24 | static bool getOrder() { 25 | return Prefs.getBool('order_of_items', false); 26 | } 27 | 28 | static Future setOrder(bool value) { 29 | return Prefs.setBool('order_of_items', value); 30 | } 31 | 32 | static bool getLeftHanded() { 33 | return Prefs.getBool('left_handed', false); 34 | } 35 | 36 | static Future setLeftHanded(bool value) { 37 | return Prefs.setBool('left_handed', value); 38 | } 39 | 40 | static StatelessWidget tapText(String text, VoidCallback onTap, 41 | {TextStyle? style}) { 42 | return AppSettings.tapText(text, onTap, style: style); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /example/lib/src/home/model/words/wordpairs_model.dart: -------------------------------------------------------------------------------- 1 | import 'package:mvc_application_example/src/view.dart'; 2 | 3 | import 'package:mvc_application_example/src/controller.dart'; 4 | 5 | import 'package:english_words/english_words.dart'; 6 | 7 | /// Representing the data source (the model) of this App's design pattern. 8 | class WordPairsModel extends ControllerMVC { 9 | factory WordPairsModel([StateMVC? state]) => 10 | _this ??= WordPairsModel._(state); 11 | WordPairsModel._(StateMVC? state) : super(state) { 12 | words = _EnglishWords(); 13 | _counter = 0; 14 | onPressed(); 15 | } 16 | static WordPairsModel? _this; 17 | late _EnglishWords words; 18 | late int _counter; 19 | 20 | int _index = 0; 21 | 22 | // data for the name generator app. 23 | String get data => words.current.asPascalCase; 24 | 25 | void onPressed() => words.build(_counter++); 26 | 27 | List get suggestions => words.suggestions; 28 | 29 | Set get saved => words.saved; 30 | 31 | WordPair get current => words.current; 32 | 33 | String get title => current.asPascalCase; 34 | 35 | Icon get icon => words.icon; 36 | 37 | Widget get trailing => icon; 38 | 39 | void build(int i) => words.build(i); 40 | 41 | Iterable tiles({TextStyle style = const TextStyle(fontSize: 25)}) => 42 | words.saved.map( 43 | (WordPair pair) { 44 | Widget widget; 45 | if (App.useCupertino) { 46 | widget = CupertinoListTile(title: Text(pair.asPascalCase)); 47 | } else { 48 | widget = ListTile( 49 | title: Text( 50 | pair.asPascalCase, 51 | style: style, 52 | ), 53 | ); 54 | } 55 | return widget; 56 | }, 57 | ); 58 | 59 | void onTap(int i) => setState(() { 60 | words.onTap(i); 61 | }); 62 | 63 | /// Supply one of the saved word pairs 64 | WordPair getWordPair() { 65 | final list = saved.toList(); 66 | _index++; 67 | if (_index >= list.length) { 68 | _index = 0; 69 | } 70 | return list.isEmpty ? WordPair('', '') : list[_index]; 71 | } 72 | 73 | /// Return an example of 'saved' word pair 74 | Widget get wordPair { 75 | // Iterate through the saved word pairs 76 | final twoWords = getWordPair(); 77 | return Text( 78 | twoWords.asString, 79 | style: TextStyle( 80 | color: Colors.red, 81 | fontSize: Theme.of(state!.context).textTheme.headline4!.fontSize, 82 | ), 83 | ); 84 | } 85 | } 86 | 87 | class _EnglishWords { 88 | // 89 | List get suggestions => _suggestions; 90 | final _suggestions = []; 91 | 92 | Set get saved => _saved; 93 | final Set _saved = {}; 94 | 95 | int get index => _index; 96 | late int _index; 97 | 98 | void build(int i) { 99 | _index = i ~/ 2; 100 | if (_index >= _suggestions.length) { 101 | _suggestions.addAll(generateWordPairs().take(10)); 102 | } 103 | } 104 | 105 | WordPair get current => _suggestions[_index]; 106 | 107 | Icon get icon { 108 | final bool alreadySaved = _saved.contains(_suggestions[_index]); 109 | return Icon( 110 | alreadySaved ? Icons.favorite : Icons.favorite_border, 111 | color: alreadySaved ? Colors.red : null, 112 | ); 113 | } 114 | 115 | void onTap(int i) { 116 | final int index = i ~/ 2; 117 | final WordPair? pair = _suggestions[index]; 118 | if (pair == null) { 119 | return; 120 | } 121 | if (_saved.contains(_suggestions[index])) { 122 | _saved.remove(pair); 123 | } else { 124 | _saved.add(pair); 125 | } 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /example/lib/src/home/view.dart: -------------------------------------------------------------------------------- 1 | /// 2 | /// All the UI involved in the home screen. 3 | /// 4 | 5 | // Counter app UI 6 | export 'package:mvc_application_example/src/home/view/counter_view.dart'; 7 | // Startup Name Generator UI 8 | export 'package:mvc_application_example/src/home/view/wordpairs_view.dart'; 9 | // Contacts app 10 | export 'package:mvc_application_example/src/home/view/contacts_view.dart'; 11 | -------------------------------------------------------------------------------- /example/lib/src/home/view/contacts/add_contact.dart: -------------------------------------------------------------------------------- 1 | import 'package:mvc_application_example/src/view.dart'; 2 | 3 | import 'package:mvc_application_example/src/home/model/contacts/contact.dart'; 4 | 5 | class AddContact extends StatefulWidget { 6 | const AddContact({this.contact, this.title, Key? key}) : super(key: key); 7 | final Contact? contact; 8 | final String? title; 9 | @override 10 | State createState() => _AddContactState(); 11 | } 12 | 13 | /// Should always keep your State class 'hidden' with the leading underscore 14 | class _AddContactState extends StateMVC { 15 | @override 16 | void initState() { 17 | super.initState(); 18 | contact = widget.contact ?? Contact(); 19 | //Link to this State object 20 | contact.pushState(this); 21 | } 22 | 23 | // Either an 'empty' contact or a contact passed to class, AddContact 24 | late Contact contact; 25 | 26 | @override 27 | void dispose() { 28 | // Detach from the State object. 29 | contact.popState(); 30 | super.dispose(); 31 | } 32 | 33 | // Use the appropriate interface depending on the platform. 34 | // Called everytime the setState() function is called. 35 | @override 36 | Widget build(BuildContext context) => 37 | App.useMaterial ? _BuildAndroid(state: this) : _BuildiOS(state: this); 38 | } 39 | 40 | /// The Android interface. 41 | class _BuildAndroid extends StatelessWidget { 42 | const _BuildAndroid({Key? key, required this.state}) : super(key: key); 43 | final _AddContactState state; 44 | 45 | @override 46 | Widget build(BuildContext context) { 47 | final widget = state.widget; 48 | final contact = state.contact; 49 | return Scaffold( 50 | appBar: AppBar( 51 | title: Text(widget.title ?? 'Add a contact'.tr), 52 | actions: [ 53 | TextButton( 54 | onPressed: () async { 55 | final pop = await contact.onPressed(); 56 | if (pop) { 57 | await contact.model.getContacts(); 58 | Navigator.of(context).pop(); 59 | } 60 | }, 61 | child: const Icon(Icons.save, color: Colors.white), 62 | ) 63 | ], 64 | ), 65 | body: Container( 66 | padding: const EdgeInsets.all(12), 67 | child: Form( 68 | key: contact.formKey, 69 | child: ListView( 70 | children: [ 71 | contact.givenName.textFormField, 72 | contact.middleName.textFormField, 73 | contact.familyName.textFormField, 74 | contact.phone.onListItems(), 75 | contact.email.onListItems(), 76 | contact.company.textFormField, 77 | contact.jobTitle.textFormField, 78 | ], 79 | ), 80 | ), 81 | ), 82 | ); 83 | } 84 | } 85 | 86 | /// The iOS interface 87 | class _BuildiOS extends StatelessWidget { 88 | const _BuildiOS({Key? key, required this.state}) : super(key: key); 89 | final _AddContactState state; 90 | 91 | @override 92 | Widget build(BuildContext context) { 93 | final widget = state.widget; 94 | final contact = state.contact; 95 | return CupertinoPageScaffold( 96 | navigationBar: CupertinoNavigationBar( 97 | leading: CupertinoNavigationBarBackButton( 98 | previousPageTitle: 'Home', 99 | onPressed: () { 100 | Navigator.of(context).maybePop(); 101 | }, 102 | ), 103 | middle: Text(widget.title ?? 'Add a contact'.tr), 104 | trailing: Material( 105 | child: TextButton( 106 | onPressed: () async { 107 | final pop = await contact.onPressed(); 108 | if (pop) { 109 | await contact.model.getContacts(); 110 | Navigator.of(context).pop(); 111 | } 112 | }, 113 | child: const Icon(Icons.save), 114 | ), 115 | ), 116 | ), 117 | child: Container( 118 | padding: const EdgeInsets.all(12), 119 | child: Form( 120 | key: contact.formKey, 121 | child: ListView( 122 | children: [ 123 | contact.givenName.textFormField, 124 | contact.middleName.textFormField, 125 | contact.familyName.textFormField, 126 | contact.phone.onListItems(), 127 | contact.email.onListItems(), 128 | contact.company.textFormField, 129 | contact.jobTitle.textFormField, 130 | ], 131 | ), 132 | ), 133 | ), 134 | ); 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /example/lib/src/home/view/contacts/contact_details.dart: -------------------------------------------------------------------------------- 1 | import 'package:mvc_application_example/src/view.dart'; 2 | 3 | import 'package:mvc_application_example/src/home/model/contacts/contact.dart' 4 | show Contact; 5 | 6 | import 'add_contact.dart' show AddContact; 7 | 8 | enum AppBarBehavior { normal, pinned, floating, snapping } 9 | 10 | class ContactDetails extends StatefulWidget { 11 | const ContactDetails({required this.contact, Key? key}) : super(key: key); 12 | final Contact contact; 13 | @override 14 | State createState() => _DetailsState(); 15 | } 16 | 17 | class _DetailsState extends StateMVC { 18 | @override 19 | void initState() { 20 | super.initState(); 21 | contact = widget.contact; 22 | } 23 | 24 | late Contact contact; 25 | 26 | @override 27 | Widget build(BuildContext context) => 28 | App.useMaterial ? _BuildAndroid(state: this) : _BuildiOS(state: this); 29 | 30 | // Provide a means to 'edit' the details 31 | Future editContact(Contact? contact, BuildContext context) async { 32 | final widget = AddContact(contact: contact, title: 'Edit a contact'.tr); 33 | PageRoute route; 34 | if (App.useMaterial) { 35 | route = 36 | MaterialPageRoute(builder: (BuildContext context) => widget); 37 | } else { 38 | route = 39 | CupertinoPageRoute(builder: (BuildContext context) => widget); 40 | } 41 | await Navigator.of(context).push(route); 42 | final contacts = await contact!.model.getContacts(); 43 | this.contact = contacts 44 | .firstWhere((contact) => contact.id.value == this.contact.id.value); 45 | setState(() {}); 46 | } 47 | } 48 | 49 | // Android interface 50 | class _BuildAndroid extends StatelessWidget { 51 | const _BuildAndroid({Key? key, required this.state}) : super(key: key); 52 | final _DetailsState state; 53 | 54 | @override 55 | Widget build(BuildContext context) { 56 | final contact = state.contact; 57 | // Dart allows for local function declarations 58 | onTap() => state.editContact(contact, context); 59 | return Scaffold( 60 | appBar: AppBar(title: contact.displayName.text, actions: [ 61 | TextButton( 62 | onPressed: () async { 63 | // Confirm the deletion 64 | final delete = await showBox( 65 | text: 'Delete this contact?'.tr, context: context); 66 | 67 | if (delete) { 68 | // 69 | await contact.delete(); 70 | 71 | Navigator.of(context).pop(); 72 | } 73 | // // A 'then' clause implementation. 74 | // showBox(text: 'Delete this contact?', context: context) 75 | // .then((bool delete) { 76 | // if (delete) { 77 | // contact.delete().then((_) { 78 | // Navigator.of(context).pop(); 79 | // }); 80 | // } 81 | // }); 82 | }, 83 | child: const Icon(Icons.delete, color: Colors.white), 84 | ), 85 | ]), 86 | bottomNavigationBar: SimpleBottomAppBar( 87 | button01: HomeBarButton(onPressed: () { 88 | Navigator.of(context).pop(); 89 | }), 90 | button03: EditBarButton(onPressed: onTap), 91 | ), 92 | body: CustomScrollView(slivers: [ 93 | SliverList( 94 | delegate: SliverChildListDelegate([ 95 | contact.givenName.onListTile(tap: onTap), 96 | contact.middleName.onListTile(tap: onTap), 97 | contact.familyName.onListTile(tap: onTap), 98 | contact.phone.onListItems(onTap: onTap), 99 | contact.email.onListItems(onTap: onTap), 100 | contact.company.onListTile(tap: onTap), 101 | contact.jobTitle.onListTile(tap: onTap), 102 | ]), 103 | ) 104 | ]), 105 | ); 106 | } 107 | } 108 | 109 | // iOS interface 110 | class _BuildiOS extends StatelessWidget { 111 | const _BuildiOS({Key? key, required this.state}) : super(key: key); 112 | final _DetailsState state; 113 | 114 | @override 115 | Widget build(BuildContext context) { 116 | final contact = state.contact; 117 | // Dart allows for local function declarations 118 | onTap() => state.editContact(contact, context); 119 | return CupertinoPageScaffold( 120 | child: CustomScrollView(slivers: [ 121 | CupertinoSliverNavigationBar( 122 | leading: CupertinoNavigationBarBackButton( 123 | previousPageTitle: 'Home', 124 | onPressed: () { 125 | Navigator.of(context).maybePop(); 126 | }, 127 | ), 128 | largeTitle: Text( 129 | '${contact.givenName.value ?? ''} ${contact.familyName.value ?? ''}'), 130 | trailing: Material( 131 | child: IconButton( 132 | icon: const Icon(Icons.delete), 133 | onPressed: () { 134 | showBox(text: 'Delete this contact?'.tr, context: context) 135 | .then((bool delete) { 136 | if (delete) { 137 | contact.delete().then((_) { 138 | Navigator.of(context).maybePop(); 139 | }); 140 | } 141 | }); 142 | }, 143 | ), 144 | ), 145 | ), 146 | SliverList( 147 | delegate: SliverChildListDelegate([ 148 | contact.givenName.onListTile(tap: onTap), 149 | contact.middleName.onListTile(tap: onTap), 150 | contact.familyName.onListTile(tap: onTap), 151 | contact.phone.onListItems(onTap: onTap), 152 | contact.email.onListItems(onTap: onTap), 153 | contact.company.onListTile(tap: onTap), 154 | contact.jobTitle.onListTile(tap: onTap), 155 | ]), 156 | ), 157 | ]), 158 | ); 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /example/lib/src/home/view/wordpairs_view.dart: -------------------------------------------------------------------------------- 1 | import 'package:mvc_application_example/src/view.dart'; 2 | 3 | import 'package:mvc_application_example/src/controller.dart'; 4 | 5 | class WordPairs extends StatefulWidget { 6 | const WordPairs({Key? key}) : super(key: key); 7 | 8 | @override 9 | State createState() => _WordPairsState(); 10 | } 11 | 12 | /// Should always keep your State class 'hidden' with the leading underscore 13 | class _WordPairsState extends StateMVC { 14 | _WordPairsState() : super(WordPairsController()) { 15 | con = controller as WordPairsController; 16 | } 17 | late WordPairsController con; 18 | 19 | @override 20 | void initState() { 21 | super.initState(); 22 | appCon = TemplateController(); 23 | } 24 | 25 | late TemplateController appCon; 26 | 27 | /// Depending on the platform, run an 'Android' or 'iOS' style of Widget. 28 | @override 29 | Widget build(BuildContext context) => App.useMaterial 30 | ? _RandomWordsAndroid(state: this) 31 | : _RandomWordsiOS(state: this); 32 | } 33 | 34 | /// The Android-style of interface 35 | class _RandomWordsAndroid extends StatelessWidget { 36 | // 37 | const _RandomWordsAndroid({Key? key, required this.state}) : super(key: key); 38 | final _WordPairsState state; 39 | 40 | @override 41 | Widget build(BuildContext context) { 42 | // final widget = state.widget; 43 | final con = state.con; 44 | final appCon = state.appCon; 45 | return Scaffold( 46 | appBar: AppBar( 47 | title: Text('Startup Name Generator'.tr), 48 | actions: [ 49 | IconButton( 50 | key: const Key('listSaved'), 51 | icon: const Icon(Icons.list), 52 | onPressed: _pushSaved, 53 | ), 54 | appCon.popupMenu(), 55 | ], 56 | ), 57 | body: ListView.builder( 58 | padding: const EdgeInsets.all(16), 59 | itemBuilder: (context, i) { 60 | if (i.isOdd) { 61 | return const Divider(); 62 | } 63 | con.build(i); 64 | return ListTile( 65 | title: Text( 66 | con.data, 67 | style: const TextStyle(fontSize: 25), 68 | ), 69 | trailing: con.trailing, 70 | onTap: () { 71 | con.onTap(i); 72 | }, 73 | ); 74 | }), 75 | ); 76 | } 77 | 78 | void _pushSaved() { 79 | Navigator.of(state.context).push( 80 | MaterialPageRoute( 81 | builder: (BuildContext context) { 82 | final tiles = state.con.tiles(); 83 | final divided = ListTile.divideTiles( 84 | context: context, 85 | tiles: tiles, 86 | ).toList(); 87 | 88 | return Scaffold( 89 | appBar: AppBar( 90 | title: Text('Saved Suggestions'.tr), 91 | ), 92 | body: ListView(children: divided), 93 | ); 94 | }, 95 | ), 96 | ); 97 | } 98 | } 99 | 100 | /// The iOS-style of interface 101 | class _RandomWordsiOS extends StatelessWidget { 102 | // 103 | const _RandomWordsiOS({Key? key, required this.state}) : super(key: key); 104 | final _WordPairsState state; 105 | @override 106 | Widget build(BuildContext context) { 107 | // final widget = state.widget; 108 | final con = state.con; 109 | final appCon = state.appCon; 110 | return CupertinoPageScaffold( 111 | child: CustomScrollView( 112 | slivers: [ 113 | CupertinoSliverNavigationBar( 114 | largeTitle: Text('Startup Name Generator'.tr), 115 | trailing: Row( 116 | mainAxisSize: MainAxisSize.min, 117 | mainAxisAlignment: MainAxisAlignment.end, 118 | children: [ 119 | CupertinoButton( 120 | key: const Key('listSaved'), 121 | onPressed: _pushSaved, 122 | child: const Icon(Icons.list), 123 | ), 124 | appCon.popupMenu(), 125 | ], 126 | ), 127 | ), 128 | SliverSafeArea( 129 | top: false, 130 | minimum: const EdgeInsets.only(top: 8), 131 | sliver: SliverList( 132 | delegate: SliverChildBuilderDelegate( 133 | (context, i) { 134 | if (i.isOdd) { 135 | return const Divider(); 136 | } 137 | con.build(i); 138 | return CupertinoListTile( 139 | title: Text(con.data), 140 | trailing: con.trailing, 141 | onTap: () { 142 | con.onTap(i); 143 | }, 144 | ); 145 | }, 146 | ), 147 | ), 148 | ) 149 | ], 150 | ), 151 | ); 152 | } 153 | 154 | void _pushSaved() { 155 | Navigator.of(state.context).push( 156 | CupertinoPageRoute( 157 | builder: (BuildContext context) { 158 | final Iterable tiles = state.con.tiles(); 159 | final Iterator it = tiles.iterator; 160 | it.moveNext(); 161 | return CupertinoPageScaffold( 162 | child: CustomScrollView( 163 | slivers: [ 164 | CupertinoSliverNavigationBar( 165 | largeTitle: Text('Saved Suggestions'.tr), 166 | ), 167 | SliverSafeArea( 168 | top: false, 169 | minimum: const EdgeInsets.only(top: 8), 170 | sliver: SliverList( 171 | delegate: SliverChildBuilderDelegate( 172 | (context, i) { 173 | final tile = it.current; 174 | it.moveNext(); 175 | return tile; 176 | }, 177 | childCount: tiles.length, 178 | ), 179 | ), 180 | ) 181 | ], 182 | ), 183 | ); 184 | }, 185 | ), 186 | ); 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /example/lib/src/model.dart: -------------------------------------------------------------------------------- 1 | // The MVC application framework. 2 | export 'package:mvc_application/model.dart'; 3 | 4 | /// Data to be used throughout the app. 5 | export 'package:mvc_application_example/src/app/model.dart'; 6 | 7 | /// The data for the home screen. 8 | export 'package:mvc_application_example/src/home/model.dart'; 9 | -------------------------------------------------------------------------------- /example/lib/src/view.dart: -------------------------------------------------------------------------------- 1 | // The mvc application framework. 2 | export 'package:mvc_application/view.dart'; 3 | 4 | // The UI at the app level. 5 | export 'package:mvc_application_example/src/app/view.dart' hide ISOSpinner; 6 | 7 | // The UI for the home screen. 8 | export 'package:mvc_application_example/src/home/view.dart'; 9 | -------------------------------------------------------------------------------- /example/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: mvc_application_example 2 | description: Create a Flutter App based on a standard design pattern using third-party packages for functions and features. 3 | homepage: https://github.com/AndriousSolutions/app_template 4 | repository: https://github.com/AndriousSolutions/app_template 5 | 6 | version: 2.5.2 7 | 8 | environment: 9 | sdk: ">=2.16.0 <3.0.0" 10 | 11 | # Dependencies specify other packages that your package needs in order to work. 12 | # To automatically upgrade your package dependencies to the latest versions 13 | # consider running `flutter pub upgrade --major-versions`. Alternatively, 14 | # dependencies can be manually updated by changing the version numbers below to 15 | # the latest version available on pub.dev. To see which dependencies have newer 16 | # versions available, run `flutter pub outdated`. 17 | dependencies: 18 | flutter: 19 | sdk: flutter 20 | 21 | # The following adds the Cupertino Icons font to your application. 22 | # Use with the CupertinoIcons class for iOS style icons. 23 | cupertino_icons: ^1.0.0 24 | 25 | # https://pub.dev/packages/dbutils 26 | dbutils: ^5.0.0 27 | 28 | # Generates English word pairs 29 | # https://pub.dev/packages/english_words 30 | english_words: ^4.0.0 31 | 32 | # https://pub.dev/packages/flutter_material_color_picker/ 33 | flutter_material_color_picker: ^1.0.0 34 | 35 | # https://pub.dev/packages/mvc_application 36 | mvc_application: #^8.0.0 37 | path: ../ 38 | 39 | dev_dependencies: 40 | flutter_test: 41 | sdk: flutter 42 | 43 | # The "flutter_lints" package below contains a set of recommended lints to 44 | # encourage good coding practices. The lint set provided by the package is 45 | # activated in the `analysis_options.yaml` file located at the root of your 46 | # package. See that file for information about deactivating specific lint 47 | # rules and activating additional ones. 48 | flutter_lints: ^1.0.0 49 | 50 | # For information on the generic Dart part of this file, see the 51 | # following page: https://dart.dev/tools/pub/pubspec 52 | 53 | # The following section is specific to Flutter. 54 | flutter: 55 | 56 | # The following line ensures that the Material Icons font is 57 | # included with your application, so that you can use the icons in 58 | # the material Icons class. 59 | uses-material-design: true 60 | 61 | # To add assets to your application, add an assets section, like this: 62 | # assets: 63 | # - images/a_dot_burr.jpeg 64 | # - images/a_dot_ham.jpeg 65 | 66 | # An image asset can refer to one or more resolution-specific "variants", see 67 | # https://flutter.dev/assets-and-images/#resolution-aware. 68 | 69 | # For details regarding adding assets from package dependencies, see 70 | # https://flutter.dev/assets-and-images/#from-packages 71 | 72 | # To add custom fonts to your application, add a fonts section here, 73 | # in this "flutter" section. Each entry in this list should have a 74 | # "family" key with the font family name, and a "fonts" key with a 75 | # list giving the asset and other descriptors for the font. For 76 | # example: 77 | # fonts: 78 | # - family: Schyler 79 | # fonts: 80 | # - asset: fonts/Schyler-Regular.ttf 81 | # - asset: fonts/Schyler-Italic.ttf 82 | # style: italic 83 | # - family: Trajan Pro 84 | # fonts: 85 | # - asset: fonts/TrajanPro.ttf 86 | # - asset: fonts/TrajanPro_Bold.ttf 87 | # weight: 700 88 | # 89 | # For details regarding fonts from package dependencies, 90 | # see https://flutter.dev/custom-fonts/#from-packages 91 | -------------------------------------------------------------------------------- /example/test/src/test_utils.dart: -------------------------------------------------------------------------------- 1 | String _errorMessage = ''; 2 | 3 | void collectError(Object error) { 4 | // 5 | _errorMessage = '$_errorMessage${error.toString()}'; 6 | } 7 | 8 | /// Throw an Exception if there are a collection of errors. 9 | void reportTestErrors() { 10 | // 11 | if (_errorMessage.isNotEmpty) { 12 | // 13 | throw Exception(_errorMessage); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /example/test/src/tests/contacts_test.dart: -------------------------------------------------------------------------------- 1 | /// 2 | import '../view.dart'; 3 | 4 | String _location = '========================== contacts_test.dart'; 5 | 6 | /// Testing the Contacts app 7 | Future contactsTest(WidgetTester tester) async { 8 | // 9 | // Delete the last contact entered 10 | await _deleteContact(tester); 11 | 12 | // Tap the '+' icon and trigger a frame. 13 | await tester.tap(find.byIcon(Icons.add)); 14 | 15 | await tester.pumpAndSettle(); 16 | 17 | // Find a list of word pairs 18 | Finder finder = find.byType(TextFormField); 19 | 20 | // The text form fields should be available. 21 | expect(finder, findsWidgets, reason: _location); 22 | 23 | for (var cnt = 0; cnt < 7; cnt++) { 24 | // 25 | final field = finder.at(cnt); 26 | 27 | await tester.tap(field); 28 | await tester.pump(); 29 | 30 | await tester.pumpAndSettle(); 31 | // await tester.showKeyboard(field); 32 | String text = ''; 33 | switch (cnt) { 34 | case 0: 35 | text = 'Greg'; 36 | break; 37 | case 1: 38 | text = ''; 39 | break; 40 | case 2: 41 | text = 'Perry'; 42 | break; 43 | case 3: 44 | text = '123 456-7890'; 45 | break; 46 | case 4: 47 | text = 'greg.perry@somewhere.com'; 48 | break; 49 | case 5: 50 | text = 'Andrious Solutions Ltd.'; 51 | break; 52 | case 6: 53 | text = 'Founder'; 54 | break; 55 | } 56 | await tester.enterText(field, text); 57 | } 58 | 59 | finder = find.widgetWithIcon(TextButton, Icons.save); 60 | 61 | expect(finder, findsOneWidget, reason: _location); 62 | 63 | await tester.tap(finder); 64 | await tester.pump(); 65 | 66 | await tester.pumpAndSettle(); 67 | await tester.pumpAndSettle(); 68 | await tester.pumpAndSettle(); 69 | } 70 | 71 | /// Delete a contact if any 72 | Future _deleteContact(WidgetTester tester) async { 73 | // 74 | final con = ContactsController(); 75 | 76 | // If there are no contacts 77 | if (con.items == null || con.items!.isEmpty) { 78 | return; 79 | } 80 | 81 | Finder finder; 82 | 83 | if (App.useMaterial) { 84 | // 85 | finder = find.byType(ListTile, skipOffstage: false); 86 | 87 | expect(finder, findsWidgets, reason: _location); 88 | 89 | await tester.tap(finder.first); 90 | await tester.pump(); 91 | } else { 92 | // 93 | finder = find.byWidgetPredicate( 94 | (Widget widget) => widget is GestureDetector && widget.child is Row, 95 | description: 'a CupertinoListTile widget', 96 | skipOffstage: false); 97 | 98 | expect(finder, findsWidgets, reason: _location); 99 | 100 | // Retrieve the widget 101 | final tile = tester.firstWidget(finder); 102 | 103 | tile.onTap!(); 104 | } 105 | await tester.pumpAndSettle(); 106 | 107 | final deleteButton = find.widgetWithIcon( 108 | App.useMaterial ? TextButton : IconButton, Icons.delete, 109 | skipOffstage: false); 110 | 111 | expect(deleteButton, findsOneWidget, reason: _location); 112 | 113 | await tester.tap(deleteButton); 114 | await tester.pump(); 115 | await tester.pumpAndSettle(); 116 | 117 | // Find the appropriate button even if translated. 118 | final button = find.widgetWithText( 119 | App.useMaterial ? TextButton : CupertinoDialogAction, 'OK'); 120 | expect(button, findsOneWidget, reason: _location); 121 | await tester.tap(button); 122 | await tester.pump(); 123 | await tester.pumpAndSettle(); 124 | } 125 | -------------------------------------------------------------------------------- /example/test/src/tests/counter_test.dart: -------------------------------------------------------------------------------- 1 | /// 2 | import '../view.dart'; 3 | 4 | String _location = '========================== counter_test.dart'; 5 | 6 | /// Testing the counter app 7 | Future counterTest(WidgetTester tester) async { 8 | // Retrieve the current count. 9 | String start = CounterController().data; 10 | // Verify that our counter starts at 0. 11 | expect(find.text(start), findsOneWidget, reason: _location); 12 | expect(find.text('1'), findsNothing, reason: _location); 13 | 14 | // 9 counts 15 | for (int cnt = 1; cnt <= 9; cnt++) { 16 | // Tap the '+' icon and trigger a frame. 17 | await tester.tap(find.byIcon(Icons.add)); 18 | await tester.pump(); 19 | } 20 | 21 | final end = int.parse(start) + 9; 22 | start = end.toString(); 23 | 24 | // Verify that our counter has incremented. 25 | expect(find.text('0'), findsNothing, reason: _location); 26 | expect(find.text(start), findsOneWidget, reason: _location); 27 | } 28 | -------------------------------------------------------------------------------- /example/test/src/tests/menu/about_menu.dart: -------------------------------------------------------------------------------- 1 | import '../../view.dart'; 2 | 3 | String _location = '========================== about_menu.dart'; 4 | 5 | /// Open the About menu 6 | Future openAboutMenu(WidgetTester tester) async { 7 | /// Open popup menu 8 | await openPopupMenu(tester); 9 | 10 | /// Open the About window 11 | final about = find.byKey(const Key('aboutMenuItem')); 12 | expect(about, findsOneWidget, reason: _location); 13 | await tester.tap(about); 14 | await tester.pumpAndSettle(); 15 | await tester.pumpAndSettle(); 16 | 17 | /// Close window 18 | // Find the appropriate button even if translated. 19 | final button = find.widgetWithText(TextButton, L10n.s('CLOSE')); 20 | expect(button, findsOneWidget, reason: _location); 21 | await tester.tap(button); 22 | await tester.pumpAndSettle(); 23 | await tester.pumpAndSettle(); 24 | } 25 | -------------------------------------------------------------------------------- /example/test/src/tests/menu/app_menu.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart' show Key; 2 | 3 | import 'package:flutter_test/flutter_test.dart'; 4 | 5 | import 'open_menu.dart'; 6 | 7 | String _location = '========================== app_menu.dart'; 8 | 9 | /// Switch the app through the popupmenu 10 | Future openApplicationMenu(WidgetTester tester) async { 11 | /// Open popup menu 12 | final open = await openPopupMenu(tester); 13 | 14 | // 15 | if (!open) { 16 | return; 17 | } 18 | 19 | /// Wait for the transition in the Interface 20 | await tester.pumpAndSettle(); 21 | 22 | /// Switch the application 23 | final application = 24 | find.byKey(const Key('applicationMenuItem'), skipOffstage: false); 25 | 26 | expect(application, findsOneWidget, reason: _location); 27 | 28 | await tester.tap(application); 29 | 30 | await tester.pumpAndSettle(); 31 | } 32 | -------------------------------------------------------------------------------- /example/test/src/tests/menu/interface_menu.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart' show Key; 2 | 3 | import 'package:flutter_test/flutter_test.dart'; 4 | 5 | import 'open_menu.dart'; 6 | 7 | String _location = '========================== interface_menu.dart'; 8 | 9 | /// Switch the Interface through the popupmenu 10 | Future openInterfaceMenu(WidgetTester tester) async { 11 | /// Open popup menu 12 | await openPopupMenu(tester); 13 | 14 | /// Switch the Interface 15 | final interface = find.byKey(const Key('interfaceMenuItem')); 16 | 17 | expect(interface, findsOneWidget, reason: _location); 18 | 19 | await tester.tap(interface); 20 | await tester.pump(); 21 | 22 | /// Wait for the transition of the Interface 23 | await tester.pumpAndSettle(); 24 | await tester.pumpAndSettle(); 25 | await tester.pumpAndSettle(); 26 | } 27 | -------------------------------------------------------------------------------- /example/test/src/tests/menu/locale_menu.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart' show Key, Scrollable, SimpleDialogOption; 2 | 3 | import 'package:flutter_test/flutter_test.dart'; 4 | 5 | import 'open_menu.dart'; 6 | 7 | String _location = '========================== locale_menu.dart'; 8 | 9 | /// Open the Locale menu 10 | Future openLocaleMenu(WidgetTester tester) async { 11 | /// Open the popupmenu 12 | await openPopupMenu(tester); 13 | 14 | /// Open the Locale window 15 | final locale = find.byKey(const Key('localeMenuItem')); 16 | expect(locale, findsOneWidget, reason: _location); 17 | await tester.tap(locale); 18 | await tester.pumpAndSettle(); 19 | 20 | /// Select a language 21 | await selectLanguage(tester); 22 | 23 | /// Close window 24 | final button = find.widgetWithText(SimpleDialogOption, 'Cancel'); 25 | expect(button, findsOneWidget, reason: _location); 26 | await tester.tap(button); 27 | await tester.pumpAndSettle(); 28 | } 29 | 30 | Future selectLanguage(WidgetTester tester) async { 31 | // 32 | final listFinder = find.byType(Scrollable, skipOffstage: false); 33 | 34 | expect(listFinder, findsWidgets, reason: _location); 35 | 36 | // Scroll until the item to be found appears. 37 | await tester.scrollUntilVisible( 38 | find.text('fr-FR'), 39 | 500.0, 40 | scrollable: listFinder.last, 41 | ); 42 | 43 | await tester.tap(listFinder.last); 44 | await tester.pump(); 45 | await tester.pumpAndSettle(); 46 | } 47 | -------------------------------------------------------------------------------- /example/test/src/tests/menu/open_menu.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart' show Key; 2 | 3 | import 'package:flutter_test/flutter_test.dart'; 4 | 5 | import '../../test_utils.dart'; 6 | 7 | String _location = '========================== open_menu.dart'; 8 | 9 | ///Open the PopupMenu 10 | Future openPopupMenu(WidgetTester tester, 11 | {bool throwError = true}) async { 12 | bool opened = true; 13 | try { 14 | final popup = find.byKey(const Key('appMenuButton'), skipOffstage: false); 15 | expect(popup, findsOneWidget, reason: _location); 16 | await tester.tap(popup); 17 | 18 | /// Wait for the transition in the Interface 19 | await tester.pumpAndSettle(); 20 | await tester.pumpAndSettle(); 21 | } catch (err) { 22 | opened = false; 23 | collectError(err); 24 | if (throwError) { 25 | rethrow; 26 | } 27 | } 28 | return opened; 29 | } 30 | -------------------------------------------------------------------------------- /example/test/src/tests/unit/controller_test.dart: -------------------------------------------------------------------------------- 1 | /// 2 | import '../../view.dart'; 3 | 4 | String _location = '========================== controller_test.dart'; 5 | 6 | Future testTemplateController(WidgetTester tester) async { 7 | //ignore: avoid_print 8 | print('====================== Unit Testing Controller '); 9 | 10 | final con = TemplateController(); 11 | 12 | final app = con.application; 13 | 14 | expect(app, isInstanceOf(), reason: _location); 15 | //ignore: avoid_print 16 | print('con.application: $app $_location'); 17 | 18 | con.changeApp('Counter'); 19 | 20 | await tester.pumpAndSettle(); 21 | 22 | if (!con.counterApp) { 23 | fail('Failed to switch app. $_location'); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /example/test/src/tests/unit/wordpairs_model.dart: -------------------------------------------------------------------------------- 1 | /// 2 | import '../../view.dart'; 3 | 4 | import 'package:english_words/english_words.dart'; 5 | 6 | String _location = '========================== wordpairs_model.dart'; 7 | 8 | Future wordPairsModelTest(WidgetTester tester) async { 9 | //ignore: avoid_print 10 | print('====================== Unit Testing WordPairsModel '); 11 | final model = WordPairsModel(); 12 | final data = model.data; 13 | expect(data, isInstanceOf(), reason: _location); 14 | //ignore: avoid_print 15 | print('data: $data $_location'); 16 | final wordPair = model.current; 17 | expect(wordPair, isInstanceOf(), reason: _location); 18 | //ignore: avoid_print 19 | print('wordPair.asString: ${wordPair.asString} $_location'); 20 | final suggestions = model.suggestions; 21 | expect(suggestions, isInstanceOf>(), reason: _location); 22 | } 23 | -------------------------------------------------------------------------------- /example/test/src/tests/words_test.dart: -------------------------------------------------------------------------------- 1 | /// 2 | import '../view.dart'; 3 | 4 | String _location = '========================== words_test.dart'; 5 | 6 | /// Testing Random Word Pairs App 7 | Future wordsTest(WidgetTester tester) async { 8 | // 9 | Finder finder; 10 | 11 | if (App.useMaterial) { 12 | // Find a list of word pairs 13 | finder = find.byType(ListTile); 14 | } else { 15 | finder = find.byType(CupertinoListTile); 16 | } 17 | 18 | expect(finder, findsWidgets, reason: _location); 19 | 20 | // Tap the first three words 21 | if (App.useMaterial) { 22 | // 23 | await tester.tap(finder.first); 24 | await tester.pump(); 25 | 26 | await tester.tap(finder.at(1)); 27 | await tester.pump(); 28 | 29 | await tester.tap(finder.at(2)); 30 | await tester.pump(); 31 | } else { 32 | // 33 | // Retrieve the widget 34 | var tile = tester.widget(finder.first); 35 | tile.onTap!(); 36 | await tester.pump(); 37 | 38 | tile = tester.widget(finder.at(1)); 39 | tile.onTap!(); 40 | await tester.pump(); 41 | 42 | tile = tester.widget(finder.at(2)); 43 | tile.onTap!(); 44 | await tester.pump(); 45 | } 46 | 47 | if (App.useCupertino) { 48 | return; 49 | } 50 | 51 | /// Go to the 'Saved Suggestions' page 52 | finder = find.byKey(const Key('listSaved')); // find.bytype(IconButton); 53 | 54 | expect(finder, findsWidgets, reason: _location); 55 | 56 | await tester.tap(finder.first); 57 | await tester.pump(); 58 | 59 | /// Rebuild the Widget after the state has changed 60 | await tester.pumpAndSettle(); 61 | 62 | final model = WordPairsModel(); 63 | 64 | /// Successfully saved the selected word-pairs. 65 | if (model.saved.isEmpty) { 66 | // fail('Failed to list saved suggestions'); 67 | } else { 68 | expect(model.saved.length, equals(3), reason: _location); 69 | } 70 | 71 | if (App.useMaterial) { 72 | finder = find.byType(IconButton); 73 | 74 | expect(finder, findsOneWidget, reason: _location); 75 | 76 | /// Find the 'back button' and return 77 | await tester.tap(finder.first); 78 | } else { 79 | final con = WordPairsController(); 80 | final state = con.state; 81 | Navigator.of(state!.context).pop(); 82 | } 83 | 84 | /// Wait a frame after the state has changed; 85 | await tester.pump(); 86 | } 87 | -------------------------------------------------------------------------------- /example/test/src/view.dart: -------------------------------------------------------------------------------- 1 | /// 2 | export 'package:flutter/material.dart' show Key; 3 | 4 | export 'package:flutter_test/flutter_test.dart'; 5 | 6 | /// Helpful utilities for testing 7 | export 'test_utils.dart'; 8 | 9 | /// App's very source code 10 | export 'package:mvc_application_example/src/model.dart'; 11 | 12 | export 'package:mvc_application_example/src/view.dart'; 13 | 14 | export 'package:mvc_application_example/src/controller.dart'; 15 | 16 | /// Unit Tests 17 | export 'tests/unit/wordpairs_model.dart'; 18 | 19 | export 'tests/unit/controller_test.dart'; 20 | 21 | /// Individual Tests 22 | export 'tests/counter_test.dart'; 23 | 24 | export 'tests/words_test.dart'; 25 | 26 | export 'tests/contacts_test.dart'; 27 | 28 | /// Menu Tests 29 | export 'tests/menu/open_menu.dart'; 30 | 31 | export 'tests/menu/about_menu.dart'; 32 | 33 | export 'tests/menu/app_menu.dart'; 34 | 35 | export 'tests/menu/interface_menu.dart'; 36 | 37 | export 'tests/menu/locale_menu.dart'; 38 | -------------------------------------------------------------------------------- /example/test/widget_test.dart: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | import 'src/view.dart'; 4 | 5 | void main() { 6 | /// Define a test. The TestWidgets function also provides a WidgetTester 7 | /// to work with. The WidgetTester allows you to build and interact 8 | /// with widgets in the test environment. 9 | testWidgets('app_template testing', (WidgetTester tester) async { 10 | // 11 | await tester.pumpWidget(TemplateApp()); 12 | 13 | /// Flutter won’t automatically rebuild your widget in the test environment. 14 | /// Use pump() or pumpAndSettle() to ask Flutter to rebuild the widget. 15 | 16 | /// pumpAndSettle() waits for all animations to complete. 17 | await tester.pumpAndSettle(); 18 | 19 | final con = TemplateController(); 20 | 21 | // for (var interface = 1; interface <= 2; interface++) { 22 | // 23 | int cnt = 1; 24 | 25 | while (cnt <= 3) { 26 | switch (con.application) { 27 | case 'Counter': 28 | 29 | /// Counter app testing 30 | await counterTest(tester); 31 | break; 32 | case 'Word Pairs': 33 | 34 | /// Random Word Pairs app 35 | await wordsTest(tester); 36 | break; 37 | case 'Contacts': 38 | 39 | /// Contacts app 40 | await contactsTest(tester); 41 | break; 42 | } 43 | 44 | /// Switch the app programmatically. 45 | // con.changeApp(); 46 | /// Switch the app through the popupmenu 47 | await openApplicationMenu(tester); 48 | 49 | /// Wait for the transition in the Interface 50 | await tester.pumpAndSettle(); 51 | 52 | cnt++; 53 | } 54 | 55 | /// Open the Locale window 56 | await openLocaleMenu(tester); 57 | 58 | /// Open About menu 59 | await openAboutMenu(tester); 60 | 61 | /// Switch the Interface 62 | await openInterfaceMenu(tester); 63 | // } 64 | 65 | /// Unit testing does not involve integration or widget testing. 66 | 67 | /// WordPairs App Model Unit Testing 68 | await wordPairsModelTest(tester); 69 | 70 | /// Unit testing the App's controller object 71 | await testTemplateController(tester); 72 | 73 | reportTestErrors(); 74 | }); 75 | } 76 | -------------------------------------------------------------------------------- /lib/controller.dart: -------------------------------------------------------------------------------- 1 | /// 2 | /// Copyright (C) 2018 Andrious Solutions 3 | /// 4 | /// Licensed under the Apache License, Version 2.0 (the "License"); 5 | /// you may not use this file except in compliance with the License. 6 | /// You may obtain a copy of the License at 7 | /// 8 | /// http://www.apache.org/licenses/LICENSE-2.0 9 | /// 10 | /// Unless required by applicable law or agreed to in writing, software 11 | /// distributed under the License is distributed on an "AS IS" BASIS, 12 | /// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | /// See the License for the specific language governing permissions and 14 | /// limitations under the License. 15 | /// 16 | /// Created 25 Dec 2018 17 | /// 18 | /// 19 | 20 | // Material 21 | export 'package:flutter/material.dart' hide runApp; 22 | 23 | // Cupertino 24 | export 'package:flutter/cupertino.dart' hide RefreshCallback, runApp; 25 | 26 | // App 27 | export 'package:mvc_pattern/mvc_pattern.dart' show StateListener; 28 | 29 | // App's View 30 | export 'package:mvc_application/src/view/app.dart' show App, AppDrawer; 31 | 32 | //App's Controller 33 | export 'package:mvc_application/src/controller/app.dart' 34 | show AppController, ControllerMVC; 35 | 36 | // Error Handler 37 | export 'package:mvc_application/src/controller/util/handle_error.dart' 38 | show HandleError; 39 | 40 | // Notifications 41 | export 'package:mvc_application/src/controller/schedule_notificaitons.dart'; 42 | 43 | // Device Info 44 | export 'package:mvc_application/src/controller/device_info.dart' 45 | show DeviceInfo; 46 | 47 | // Assets 48 | export 'package:mvc_application/src/controller/assets/assets.dart'; 49 | 50 | // Get Utils 51 | export 'package:mvc_application/src/controller/get_utils/get_utils.dart'; 52 | 53 | // Preferences 54 | export 'package:prefs/prefs.dart' show Prefs, SharedPreferences; 55 | -------------------------------------------------------------------------------- /lib/model.dart: -------------------------------------------------------------------------------- 1 | /// 2 | /// Copyright (C) 2018 Andrious Solutions 3 | /// 4 | /// Licensed under the Apache License, Version 2.0 (the "License"); 5 | /// you may not use this file except in compliance with the License. 6 | /// You may obtain a copy of the License at 7 | /// 8 | /// http://www.apache.org/licenses/LICENSE-2.0 9 | /// 10 | /// Unless required by applicable law or agreed to in writing, software 11 | /// distributed under the License is distributed on an "AS IS" BASIS, 12 | /// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | /// See the License for the specific language governing permissions and 14 | /// limitations under the License. 15 | /// 16 | /// Created 25 Dec 2018 17 | /// 18 | /// 19 | 20 | /// Model classes 21 | export 'package:mvc_application/src/model/mvc.dart'; 22 | 23 | /// Material 24 | export 'package:flutter/material.dart' hide runApp; 25 | 26 | /// Cupertino 27 | export 'package:flutter/cupertino.dart' hide RefreshCallback, runApp; 28 | 29 | /// file utils 30 | export 'package:mvc_application/src/model/fileutils/files.dart'; 31 | 32 | /// Install file 33 | export 'package:mvc_application/src/model/fileutils/installfile.dart'; 34 | 35 | /// Preferences 36 | export 'package:prefs/prefs.dart' show Prefs; 37 | -------------------------------------------------------------------------------- /lib/prefs.dart: -------------------------------------------------------------------------------- 1 | /// 2 | /// Copyright (C) 2018 Andrious Solutions 3 | /// 4 | /// Licensed under the Apache License, Version 2.0 (the "License"); 5 | /// you may not use this file except in compliance with the License. 6 | /// You may obtain a copy of the License at 7 | /// 8 | /// http://www.apache.org/licenses/LICENSE-2.0 9 | /// 10 | /// Unless required by applicable law or agreed to in writing, software 11 | /// distributed under the License is distributed on an "AS IS" BASIS, 12 | /// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | /// See the License for the specific language governing permissions and 14 | /// limitations under the License. 15 | /// 16 | /// Created 26 Dec 2018 17 | /// 18 | /// 19 | 20 | export 'package:prefs/prefs.dart'; 21 | -------------------------------------------------------------------------------- /lib/run_app.dart: -------------------------------------------------------------------------------- 1 | /// 2 | /// Copyright (C) 2021 Andrious Solutions 3 | /// 4 | /// Licensed under the Apache License, Version 2.0 (the "License"); 5 | /// you may not use this file except in compliance with the License. 6 | /// You may obtain a copy of the License at 7 | /// 8 | /// http://www.apache.org/licenses/LICENSE-2.0 9 | /// 10 | /// Unless required by applicable law or agreed to in writing, software 11 | /// distributed under the License is distributed on an "AS IS" BASIS, 12 | /// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | /// See the License for the specific language governing permissions and 14 | /// limitations under the License. 15 | /// 16 | /// Created 28 Sep 2021 17 | /// 18 | 19 | /// At times, you may need to explicitly supply the custom runApp function: 20 | /// 21 | /// import 'package:mvc_application/run_app.dart'; 22 | /// 23 | /// Otherwise, it's supplied by the view.dart export file. 24 | /// 25 | export 'package:mvc_application/src/conditional_export.dart' 26 | if (dart.library.html) 'package:mvc_application/src/view/platforms/run_webapp.dart' 27 | if (dart.library.io) 'package:mvc_application/src/view/platforms/run_app.dart' 28 | show runApp; 29 | -------------------------------------------------------------------------------- /lib/settings.dart: -------------------------------------------------------------------------------- 1 | /// 2 | /// Copyright (C) 2018 Andrious Solutions 3 | /// 4 | /// Licensed under the Apache License, Version 2.0 (the "License"); 5 | /// you may not use this file except in compliance with the License. 6 | /// You may obtain a copy of the License at 7 | /// 8 | /// http://www.apache.org/licenses/LICENSE-2.0 9 | /// 10 | /// Unless required by applicable law or agreed to in writing, software 11 | /// distributed under the License is distributed on an "AS IS" BASIS, 12 | /// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | /// See the License for the specific language governing permissions and 14 | /// limitations under the License. 15 | /// 16 | /// Created 24 Dec 2018 17 | /// 18 | /// 19 | 20 | export 'package:mvc_application/src/view/utils/app_settings.dart'; 21 | 22 | export 'package:prefs/prefs.dart'; 23 | -------------------------------------------------------------------------------- /lib/src/conditional_export.dart: -------------------------------------------------------------------------------- 1 | /// 2 | /// Copyright (C) 2020 Andrious Solutions 3 | /// 4 | /// Licensed under the Apache License, Version 2.0 (the "License"); 5 | /// you may not use this file except in compliance with the License. 6 | /// You may obtain a copy of the License at 7 | /// 8 | /// http://www.apache.org/licenses/LICENSE-2.0 9 | /// 10 | /// Unless required by applicable law or agreed to in writing, software 11 | /// distributed under the License is distributed on an "AS IS" BASIS, 12 | /// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | /// See the License for the specific language governing permissions and 14 | /// limitations under the License. 15 | /// 16 | /// Created 02 Oct 2020 17 | /// 18 | /// 19 | 20 | /// Merely a 'stub' used by conditional import and export statements. 21 | 22 | import 'package:flutter/material.dart' show ErrorWidgetBuilder, Widget; 23 | 24 | import 'package:flutter/foundation.dart' show FlutterExceptionHandler; 25 | 26 | import 'package:mvc_application/view.dart' show ReportErrorHandler; 27 | 28 | /// Used in the conditional export statement: 29 | /// Found in 'package:mvc_application/view.dart' 30 | /// For example: 31 | /// export 'package:mvc_application/src/conditional_export.dart' 32 | /// if (dart.library.html) 'package:flutter/material.dart' 33 | /// if (dart.library.io) 'package:mvc_application/src/controller/app.dart' show runApp; 34 | 35 | /// This of course is fake. Merely to satisfy the Dart Analysis tool. 36 | /// if (dart.library.html) 'package:mvc_application/src/view/platforms/run_webapp.dart' 37 | /// if (dart.library.io) 'package:mvc_application/src/view/platforms/run_app.dart' 38 | void runApp( 39 | Widget app, { 40 | FlutterExceptionHandler? errorHandler, 41 | ErrorWidgetBuilder? errorScreen, 42 | ReportErrorHandler? errorReport, 43 | bool allowNewHandlers = false, 44 | }) {} 45 | -------------------------------------------------------------------------------- /lib/src/controller/app.dart: -------------------------------------------------------------------------------- 1 | /// 2 | /// Copyright (C) 2018 Andrious Solutions 3 | /// 4 | /// Licensed under the Apache License, Version 2.0 (the "License"); 5 | /// you may not use this file except in compliance with the License. 6 | /// You may obtain a copy of the License at 7 | /// 8 | /// http://www.apache.org/licenses/LICENSE-2.0 9 | /// 10 | /// Unless required by applicable law or agreed to in writing, software 11 | /// distributed under the License is distributed on an "AS IS" BASIS, 12 | /// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | /// See the License for the specific language governing permissions and 14 | /// limitations under the License. 15 | /// 16 | /// Created 24 Dec 2018 17 | /// 18 | 19 | import 'package:flutter/foundation.dart' show FlutterErrorDetails; 20 | 21 | import 'package:mvc_application/controller.dart' show HandleError; 22 | 23 | import 'package:mvc_application/view.dart' as v 24 | show App, ConnectivityListener, ConnectivityResult, StateMVC; 25 | 26 | import 'package:mvc_pattern/mvc_pattern.dart' as mvc; 27 | 28 | /// A Controller for the 'app level'. 29 | class AppController extends ControllerMVC implements v.ConnectivityListener { 30 | /// Optionally supply a 'State' object to be linked to this State Controller. 31 | AppController([v.StateMVC? state]) : super(state); 32 | 33 | /// Initialize any immediate 'none time-consuming' operations 34 | /// at the very beginning. 35 | @Deprecated('No need. Use initState()') 36 | void initApp() {} 37 | 38 | @override 39 | Future initAsync() async { 40 | /// Initialize any 'time-consuming' operations at the beginning. 41 | /// Initialize items essential to the Mobile Applications. 42 | /// Implement any asynchronous operations needed done at start up. 43 | return true; 44 | } 45 | 46 | @override 47 | bool onAsyncError(FlutterErrorDetails details) { 48 | /// Supply an 'error handler' routine if something goes wrong 49 | /// in the corresponding initAsync() routine. 50 | /// Returns true if the error was properly handled. 51 | return false; 52 | } 53 | 54 | /// The 'App Level' Error Handler. 55 | /// Override if you like to customize your error handling. 56 | void onError(FlutterErrorDetails details) { 57 | // Call the App's 'current' error handler. 58 | v.App?.onError(details); 59 | } 60 | 61 | /// If the device's connectivity changes. 62 | @override 63 | void onConnectivityChanged(v.ConnectivityResult result) {} 64 | } 65 | 66 | /// Your 'working' class most concerned with the app's functionality. 67 | /// Incorporates an Error Handler. 68 | class ControllerMVC extends mvc.ControllerMVC with HandleError { 69 | /// Optionally supply a 'State' object to be linked to this State Controller. 70 | ControllerMVC([v.StateMVC? state]) : super(state); 71 | 72 | /// The current StateMVC object from mvc_application/view.dart 73 | v.StateMVC? get stateMVC => state as v.StateMVC?; 74 | } 75 | -------------------------------------------------------------------------------- /lib/src/controller/assets/assets.dart: -------------------------------------------------------------------------------- 1 | /// 2 | /// Copyright (C) 2019 Andrious Solutions 3 | /// 4 | /// Licensed under the Apache License, Version 2.0 (the "License"); 5 | /// you may not use this file except in compliance with the License. 6 | /// You may obtain a copy of the License at 7 | /// 8 | /// http://www.apache.org/licenses/LICENSE-2.0 9 | /// 10 | /// Unless required by applicable law or agreed to in writing, software 11 | /// distributed under the License is distributed on an "AS IS" BASIS, 12 | /// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | /// See the License for the specific language governing permissions and 14 | /// limitations under the License. 15 | /// 16 | /// Created 09 Mar 2019 17 | /// 18 | /// 19 | 20 | import 'dart:async' show Future; 21 | 22 | import 'package:flutter/material.dart' 23 | show AssetImage, BuildContext, DefaultAssetBundle; 24 | 25 | import 'package:flutter/services.dart' show AssetBundle, ByteData; 26 | 27 | /// The Assets manager for this custom framework. 28 | class Assets { 29 | /// Initialize the App's Assets Manager. 30 | static Future init(BuildContext context, {String? dir}) { 31 | if (_assets == null) { 32 | _assets = DefaultAssetBundle.of(context); 33 | _dir = dir ?? 'assets'; 34 | } 35 | return Future.value(true); 36 | } 37 | 38 | static AssetBundle? _assets; 39 | static String? _dir; 40 | 41 | /// Clean up after the Assets Manager 42 | static void dispose() { 43 | _assets = null; 44 | } 45 | 46 | /// Retrieve a 'ByteData' object by its Key value. 47 | static Future getStreamF(String key) async { 48 | assert(Assets._assets != null, 'Assets.init() must be called first.'); 49 | ByteData data; 50 | try { 51 | data = await Assets._assets!.load('$setPath(key)$key'); 52 | } catch (ex) { 53 | data = ByteData(0); 54 | } 55 | return data; 56 | } 57 | 58 | /// Retrieve a String value by its Key value. 59 | static Future getStringF(String key, {bool cache = true}) async { 60 | assert(Assets._assets != null, 'Assets.init() must be called first.'); 61 | String asset; 62 | try { 63 | asset = 64 | await Assets._assets!.loadString('$setPath(key)$key', cache: cache); 65 | } catch (ex) { 66 | asset = ''; 67 | } 68 | return asset; 69 | } 70 | 71 | /// Retrieve an object of type, T, by its String value. 72 | /// Supply a parser function to process the operation. 73 | /// (See. Flutter's [AssetBundle.loadStructuredData] 74 | Future? getData( 75 | String key, Future Function(String value) parser) async { 76 | assert(Assets._assets != null, 'Assets.init() must be called first.'); 77 | Future? data; 78 | try { 79 | data = Assets._assets!.loadStructuredData('$setPath(key)$key', parser); 80 | } catch (ex) { 81 | data = null; 82 | } 83 | return data!; 84 | } 85 | 86 | /// Retrieve a String by its Key value. 87 | /// Supply a parser function to process the operation. 88 | /// (See. Flutter's [AssetBundle.loadStructuredData] 89 | Future getStringData( 90 | String key, Future Function(String value) parser) async { 91 | assert(Assets._assets != null, 'Assets.init() must be called first.'); 92 | String? data; 93 | try { 94 | data = 95 | await Assets._assets!.loadStructuredData('$setPath(key)$key', parser); 96 | } catch (ex) { 97 | data = null; 98 | } 99 | return data; 100 | } 101 | 102 | /// Retrieve a boolean value by its Key value. 103 | /// Supply a parser function to process the operation. 104 | /// (See. Flutter's [AssetBundle.loadStructuredData] 105 | Future getBoolData( 106 | String key, Future Function(String value) parser) async { 107 | assert(Assets._assets != null, 'Assets.init() must be called first.'); 108 | bool data; 109 | try { 110 | data = 111 | await Assets._assets!.loadStructuredData('$setPath(key)$key', parser); 112 | } catch (ex) { 113 | data = false; 114 | } 115 | return data; 116 | } 117 | 118 | /// Retrieve an AssetImage by its Key value. 119 | /// Supply a parser function to process the operation. 120 | /// (See. Flutter's [AssetBundle.loadStructuredData] 121 | AssetImage getImage(String key, {AssetBundle? bundle, String? package}) { 122 | return AssetImage(key, bundle: bundle, package: package); 123 | } 124 | 125 | /// Determine the appropriate path for the asset. 126 | static String? setPath(String key) { 127 | /// In case 'assets' begins the key or if '/' begins the key. 128 | final path = key.indexOf(_dir!) == 0 129 | ? '' 130 | : key.substring(0, 0) == '/' 131 | ? _dir 132 | : '$_dir/'; 133 | return path; 134 | } 135 | } 136 | 137 | /// Saved for the Testing code to work. 138 | /// A Calculator. 139 | class Calculator { 140 | /// Returns [value] plus 1. 141 | int addOne(int value) => value + 1; 142 | } 143 | -------------------------------------------------------------------------------- /lib/src/controller/util/handle_error.dart: -------------------------------------------------------------------------------- 1 | /// 2 | /// Copyright (C) 2020 Andrious Solutions 3 | /// 4 | /// Licensed under the Apache License, Version 2.0 (the "License"); 5 | /// you may not use this file except in compliance with the License. 6 | /// You may obtain a copy of the License at 7 | /// 8 | /// http://www.apache.org/licenses/LICENSE-2.0 9 | /// 10 | /// Unless required by applicable law or agreed to in writing, software 11 | /// distributed under the License is distributed on an "AS IS" BASIS, 12 | /// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | /// See the License for the specific language governing permissions and 14 | /// limitations under the License. 15 | /// 16 | /// Created 09 Apr 2020 17 | /// 18 | /// 19 | mixin HandleError { 20 | /// Return the 'last' error if any. 21 | Exception? getError([dynamic error]) { 22 | var ex = _error; 23 | if (error == null) { 24 | _error = null; 25 | } else { 26 | if (error is! Exception) { 27 | _error = Exception(error.toString()); 28 | } else { 29 | _error = error; 30 | } 31 | ex ??= _error; 32 | } 33 | return ex; 34 | } 35 | 36 | /// Simply display the error. 37 | String get errorMsg => _error == null ? '' : _error.toString(); 38 | 39 | /// Indicate if app is 'in error.' 40 | bool get inError => _error != null; 41 | 42 | /// Indicate if the app is 'in error.' 43 | bool get hasError => _error != null; 44 | Exception? _error; 45 | } 46 | -------------------------------------------------------------------------------- /lib/src/model/fileutils/files.dart: -------------------------------------------------------------------------------- 1 | /// 2 | /// Copyright (C) 2018 Andrious Solutions 3 | /// 4 | /// Licensed under the Apache License, Version 2.0 (the "License"); 5 | /// you may not use this file except in compliance with the License. 6 | /// You may obtain a copy of the License at 7 | /// 8 | /// http://www.apache.org/licenses/LICENSE-2.0 9 | /// 10 | /// Unless required by applicable law or agreed to in writing, software 11 | /// distributed under the License is distributed on an "AS IS" BASIS, 12 | /// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | /// See the License for the specific language governing permissions and 14 | /// limitations under the License. 15 | /// 16 | /// Created 11 May 2018 17 | /// 18 | import 'dart:async' show Future; 19 | import 'dart:io' show File; 20 | 21 | import 'package:path_provider/path_provider.dart' 22 | show getApplicationDocumentsDirectory; 23 | 24 | // ignore: avoid_classes_with_only_static_members 25 | /// Utility class to manipulate all files. 26 | class Files { 27 | static String? _path; 28 | 29 | /// Return the local path location. 30 | static Future get localPath async { 31 | if (_path == null) { 32 | final directory = await getApplicationDocumentsDirectory(); 33 | _path = directory.path; 34 | } 35 | return _path; 36 | } 37 | 38 | /// Return the contents of the specified file. 39 | /// Pass the name of the file. 40 | static Future read(String fileName) async { 41 | final file = await get(fileName); 42 | return readFile(file); 43 | } 44 | 45 | /// Return the contents of the specified file. 46 | /// Pass the [File] object 47 | static Future readFile(File file) async { 48 | String contents; 49 | try { 50 | // Read the file 51 | contents = await file.readAsString(); 52 | } catch (e) { 53 | // If we encounter an error 54 | contents = ''; 55 | } 56 | return contents; 57 | } 58 | 59 | /// Write the file 60 | static Future write(String fileName, String content) async { 61 | final file = await get(fileName); 62 | return writeFile(file, content); 63 | } 64 | 65 | /// Write the file 66 | static Future writeFile(File file, String content) => 67 | file.writeAsString(content, flush: true); 68 | 69 | /// Return a boolean indicating if a file exists or not. 70 | /// Pass in the name of the file. 71 | static Future exists(String fileName) async { 72 | final file = await get(fileName); 73 | // ignore: avoid_slow_async_io 74 | return file.exists(); 75 | } 76 | 77 | /// Return the File object of the specified file. 78 | /// Pass in the name of the file. 79 | static Future get(String fileName) async { 80 | final path = await localPath; 81 | return File('$path/$fileName'); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /lib/src/model/fileutils/installfile.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async' show Future; 2 | 3 | /// 4 | /// Copyright (C) 2018 Andrious Solutions 5 | /// 6 | /// Licensed under the Apache License, Version 2.0 (the "License"); 7 | /// you may not use this file except in compliance with the License. 8 | /// You may obtain a copy of the License at 9 | /// 10 | /// http://www.apache.org/licenses/LICENSE-2.0 11 | /// 12 | /// Unless required by applicable law or agreed to in writing, software 13 | /// distributed under the License is distributed on an "AS IS" BASIS, 14 | /// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | /// See the License for the specific language governing permissions and 16 | /// limitations under the License. 17 | /// 18 | /// Created 11 May 2018 19 | /// 20 | import 'dart:io' show File; 21 | 22 | import 'package:uuid/uuid.dart' show Uuid; 23 | 24 | import 'files.dart' show Files; 25 | 26 | /// Introduces a 'install file' unique to the app. 27 | class InstallFile { 28 | /// The name of the 'install file.' 29 | static const String FILE_NAME = '.install'; 30 | 31 | /// The unique Id cotained with the 'install file.' 32 | static String? sID; 33 | 34 | static bool _justInstalled = false; 35 | 36 | /// Indicate if this is the 'first' install of the app. 37 | bool get justInstalled => _justInstalled; 38 | 39 | /// Return the unique identifier for this app installation. 40 | static Future id() async { 41 | if (sID != null) { 42 | return sID; 43 | } 44 | 45 | final installFile = await Files.get(FILE_NAME); 46 | 47 | try { 48 | // ignore: avoid_slow_async_io 49 | final exists = await installFile.exists(); 50 | 51 | if (!exists) { 52 | _justInstalled = true; 53 | 54 | sID = writeInstallationFile(installFile); 55 | } else { 56 | sID = await readInstallationFile(installFile); 57 | } 58 | } catch (ex) { 59 | sID = ''; 60 | } 61 | 62 | return sID; 63 | } 64 | 65 | /// Returns the content of the 'install file.' 66 | /// Pass in a File object of the install file. 67 | static Future readInstallationFile(File installFile) async { 68 | final file = await Files.get(FILE_NAME); 69 | 70 | final content = await Files.readFile(file); 71 | 72 | return content; 73 | } 74 | 75 | /// Write to the 'install file.' 76 | /// Pass in a File object representing the install file. 77 | static String writeInstallationFile(File file) { 78 | const uuid = Uuid(); 79 | // Generate a v4 (random) id 80 | final id = uuid.v4(); // -> '110ec58a-a0f2-4ac4-8393-c866d813b8d1' 81 | Files.writeFile(file, id); 82 | return id; 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /lib/src/model/mvc.dart: -------------------------------------------------------------------------------- 1 | /// 2 | /// Copyright (C) 2019 Andrious Solutions 3 | /// 4 | /// Licensed under the Apache License, Version 2.0 (the "License"); 5 | /// you may not use this file except in compliance with the License. 6 | /// You may obtain a copy of the License at 7 | /// 8 | /// http://www.apache.org/licenses/LICENSE-2.0 9 | /// 10 | /// Unless required by applicable law or agreed to in writing, software 11 | /// distributed under the License is distributed on an "AS IS" BASIS, 12 | /// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | /// See the License for the specific language governing permissions and 14 | /// limitations under the License. 15 | /// 16 | /// Created 12 Mar 2019 17 | /// 18 | /// 19 | 20 | import 'package:mvc_pattern/mvc_pattern.dart' as mvc; 21 | 22 | import 'package:mvc_application/view.dart' as v show StateMVC; 23 | 24 | import 'package:mvc_application/controller.dart' show HandleError; 25 | 26 | /// The Model for a simple app. 27 | /// Incorporates an Error Handler. 28 | class ModelMVC extends mvc.ModelMVC with HandleError { 29 | /// The Singleton Pattern is used with only one stance instantiated. 30 | factory ModelMVC([v.StateMVC? state]) => _firstMod ??= ModelMVC._(state); 31 | ModelMVC._(v.StateMVC? state) : super(state); 32 | static ModelMVC? _firstMod; 33 | 34 | /// Allow for easy access to 'the first Model' throughout the application. 35 | // ignore: prefer_constructors_over_static_methods 36 | static ModelMVC get mod => _firstMod ?? ModelMVC(); 37 | } 38 | -------------------------------------------------------------------------------- /lib/src/model/utils/string_encryption.dart: -------------------------------------------------------------------------------- 1 | // /// 2 | // /// Copyright (C) 2020 Andrious Solutions 3 | // /// 4 | // /// Licensed under the Apache License, Version 2.0 (the "License"); 5 | // /// you may not use this file except in compliance with the License. 6 | // /// You may obtain a copy of the License at 7 | // /// 8 | // /// http://www.apache.org/licenses/LICENSE-2.0 9 | // /// 10 | // /// Unless required by applicable law or agreed to in writing, software 11 | // /// distributed under the License is distributed on an "AS IS" BASIS, 12 | // /// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // /// See the License for the specific language governing permissions and 14 | // /// limitations under the License. 15 | // /// 16 | // /// Created 31 Mar 2020 17 | // /// 18 | // /// 19 | // 20 | // import 'package:flutter_string_encryption/flutter_string_encryption.dart' 21 | // show PlatformStringCryptor; 22 | // 23 | // /// Encryption os String values. 24 | // class StringCrypt { 25 | // StringCrypt({ 26 | // String? key, 27 | // String? password, 28 | // String? salt, 29 | // }) { 30 | // if (key != null && key.trim().isNotEmpty) { 31 | // _key = key; 32 | // } else { 33 | // _keyFromPassword(password, salt).then((String key) { 34 | // _key = key; 35 | // }); 36 | // } 37 | // } 38 | // String? _key; 39 | // static final PlatformStringCryptor _crypto = PlatformStringCryptor(); 40 | // 41 | // Future en(String data, [String? key]) => encrypt(data, key); 42 | // 43 | // Future encrypt(String data, [String? key]) async { 44 | // if (key != null) { 45 | // key = key.trim(); 46 | // if (key.isEmpty) { 47 | // key = null; 48 | // } 49 | // } 50 | // String encrypt; 51 | // try { 52 | // encrypt = await _crypto.encrypt(data, (key ??= _key)!); 53 | // } catch (ex) { 54 | // encrypt = ''; 55 | // getError(ex); 56 | // } 57 | // return encrypt; 58 | // } 59 | // 60 | // Future de(String data, [String? key]) => decrypt(data, key); 61 | // 62 | // Future decrypt(String data, [String? key]) async { 63 | // if (key != null) { 64 | // key = key.trim(); 65 | // if (key.isEmpty) { 66 | // key = null; 67 | // } 68 | // } 69 | // String decrypt; 70 | // try { 71 | // decrypt = await _crypto.decrypt(data, (key ??= _key)!); 72 | // } catch (ex) { 73 | // decrypt = ''; 74 | // getError(ex); 75 | // } 76 | // return decrypt; 77 | // } 78 | // 79 | // // You will need a key to decrypt things and so on. 80 | // static Future generateRandomKey() => _crypto.generateRandomKey(); 81 | // 82 | // // Generates a salt to use with [generateKeyFromPassword] 83 | // static Future generateSalt() => _crypto.generateSalt(); 84 | // 85 | // // Gets a key from the given [password] and [salt]. 86 | // static Future generateKeyFromPassword(String password, String salt) => 87 | // _crypto.generateKeyFromPassword(password, salt); 88 | // 89 | // Future _keyFromPassword(String? password, String? salt) async { 90 | // if (password == null || password.trim().isEmpty) { 91 | // return ''; 92 | // } 93 | // String _salt; 94 | // if (salt == null || salt.trim().isEmpty) { 95 | // _salt = await generateSalt(); 96 | // } else { 97 | // _salt = salt.trim(); 98 | // } 99 | // return generateKeyFromPassword(password, _salt); 100 | // } 101 | // 102 | // bool get hasError => _error != null; 103 | // 104 | // bool get inError => _error != null; 105 | // Object? _error; 106 | // 107 | // Exception? getError([Object? error]) { 108 | // // Return the stored exception 109 | // var ex = _error as Exception?; 110 | // // Empty the stored exception 111 | // if (error == null) { 112 | // _error = null; 113 | // } else { 114 | // if (error is! Exception) { 115 | // error = Exception(error.toString()); 116 | // } 117 | // _error = error; 118 | // } 119 | // // Return the exception just past if any. 120 | // return ex ??= error as Exception?; 121 | // } 122 | // } 123 | -------------------------------------------------------------------------------- /lib/src/view/extensions/_extensions_view.dart: -------------------------------------------------------------------------------- 1 | /// 2 | /// Copyright (C) 2022 Andrious Solutions 3 | /// 4 | /// Licensed under the Apache License, Version 2.0 (the "License"); 5 | /// you may not use this file except in compliance with the License. 6 | /// You may obtain a copy of the License at 7 | /// 8 | /// http://www.apache.org/licenses/LICENSE-2.0 9 | /// 10 | /// Unless required by applicable law or agreed to in writing, software 11 | /// distributed under the License is distributed on an "AS IS" BASIS, 12 | /// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | /// See the License for the specific language governing permissions and 14 | /// limitations under the License. 15 | /// 16 | /// Created 15 May 2022 17 | /// 18 | 19 | export 'package:mvc_application/src/view/extensions/context_extensions.dart'; 20 | 21 | export 'package:mvc_application/src/view/extensions/double_extensions.dart'; 22 | 23 | export 'package:mvc_application/src/view/extensions/duration_extensions.dart'; 24 | 25 | export 'package:mvc_application/src/view/extensions/dynamic_extensions.dart'; 26 | 27 | export 'package:mvc_application/src/view/extensions/num_extensions.dart'; 28 | 29 | export 'package:mvc_application/src/view/extensions/string_extensions.dart'; 30 | 31 | export 'package:mvc_application/src/view/extensions/widget_extensions.dart'; 32 | -------------------------------------------------------------------------------- /lib/src/view/extensions/context_extensions.dart: -------------------------------------------------------------------------------- 1 | /// MIT License 2 | /// 3 | /// Copyright (c) 2019 Jonny Borges 4 | /// 5 | /// Permission is hereby granted, free of charge, to any person obtaining a copy 6 | /// of this software and associated documentation files (the "Software"), to deal 7 | /// in the Software without restriction, including without limitation the rights 8 | /// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | /// copies of the Software, and to permit persons to whom the Software is 10 | /// furnished to do so, subject to the following conditions: 11 | /// 12 | /// The above copyright notice and this permission notice shall be included in all 13 | /// copies or substantial portions of the Software. 14 | /// 15 | /// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | /// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | /// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | /// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | /// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | /// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | /// SOFTWARE. 22 | /// 23 | /// Original source: get 4.6.1 24 | 25 | import 'package:mvc_application/view.dart'; 26 | 27 | /// An extension on BuildContext 28 | extension ContextExtensionss on BuildContext { 29 | /// The same of [MediaQuery.of(context).size] 30 | Size get mediaQuerySize => MediaQuery.of(this).size; 31 | 32 | /// The same of [MediaQuery.of(context).size.height] 33 | /// Note: updates when you rezise your screen (like on a browser or 34 | /// desktop window) 35 | double get height => mediaQuerySize.height; 36 | 37 | /// The same of [MediaQuery.of(context).size.width] 38 | /// Note: updates when you rezise your screen (like on a browser or 39 | /// desktop window) 40 | double get width => mediaQuerySize.width; 41 | 42 | /// Gives you the power to get a portion of the height. 43 | /// Useful for responsive applications. 44 | /// 45 | /// [dividedBy] is for when you want to have a portion of the value you 46 | /// would get like for example: if you want a value that represents a third 47 | /// of the screen you can set it to 3, and you will get a third of the height 48 | /// 49 | /// [reducedBy] is a percentage value of how much of the height you want 50 | /// if you for example want 46% of the height, then you reduce it by 56%. 51 | double heightTransformer({double dividedBy = 1, double reducedBy = 0.0}) { 52 | return (mediaQuerySize.height - 53 | ((mediaQuerySize.height / 100) * reducedBy)) / 54 | dividedBy; 55 | } 56 | 57 | /// Gives you the power to get a portion of the width. 58 | /// Useful for responsive applications. 59 | /// 60 | /// [dividedBy] is for when you want to have a portion of the value you 61 | /// would get like for example: if you want a value that represents a third 62 | /// of the screen you can set it to 3, and you will get a third of the width 63 | /// 64 | /// [reducedBy] is a percentage value of how much of the width you want 65 | /// if you for example want 46% of the width, then you reduce it by 56%. 66 | double widthTransformer({double dividedBy = 1, double reducedBy = 0.0}) { 67 | return (mediaQuerySize.width - ((mediaQuerySize.width / 100) * reducedBy)) / 68 | dividedBy; 69 | } 70 | 71 | /// Divide the height proportionally by the given value 72 | double ratio({ 73 | double dividedBy = 1, 74 | double reducedByW = 0.0, 75 | double reducedByH = 0.0, 76 | }) { 77 | return heightTransformer(dividedBy: dividedBy, reducedBy: reducedByH) / 78 | widthTransformer(dividedBy: dividedBy, reducedBy: reducedByW); 79 | } 80 | 81 | /// similar to [MediaQuery.of(context).padding] 82 | ThemeData get theme => Theme.of(this); 83 | 84 | /// Check if dark mode theme is enable 85 | bool get isDarkMode => theme.brightness == Brightness.dark; 86 | 87 | /// give access to Theme.of(context).iconTheme.color 88 | Color? get iconColor => theme.iconTheme.color; 89 | 90 | /// similar to [MediaQuery.of(context).padding] 91 | TextTheme get textTheme => Theme.of(this).textTheme; 92 | 93 | /// similar to [MediaQuery.of(context).padding] 94 | EdgeInsets get mediaQueryPadding => MediaQuery.of(this).padding; 95 | 96 | /// similar to [MediaQuery.of(context).padding] 97 | MediaQueryData get mediaQuery => MediaQuery.of(this); 98 | 99 | /// similar to [MediaQuery.of(context).viewPadding] 100 | EdgeInsets get mediaQueryViewPadding => MediaQuery.of(this).viewPadding; 101 | 102 | /// similar to [MediaQuery.of(context).viewInsets] 103 | EdgeInsets get mediaQueryViewInsets => MediaQuery.of(this).viewInsets; 104 | 105 | /// similar to [MediaQuery.of(context).orientation] 106 | Orientation get orientation => MediaQuery.of(this).orientation; 107 | 108 | /// check if device is on landscape mode 109 | bool get isLandscape => orientation == Orientation.landscape; 110 | 111 | /// check if device is on portrait mode 112 | bool get isPortrait => orientation == Orientation.portrait; 113 | 114 | /// similar to [MediaQuery.of(this).devicePixelRatio] 115 | double get devicePixelRatio => MediaQuery.of(this).devicePixelRatio; 116 | 117 | /// similar to [MediaQuery.of(this).textScaleFactor] 118 | double get textScaleFactor => MediaQuery.of(this).textScaleFactor; 119 | 120 | /// get the shortestSide from screen 121 | double get mediaQueryShortestSide => mediaQuerySize.shortestSide; 122 | 123 | /// True if width be larger than 800 124 | bool get showNavbar => width > 800; 125 | 126 | /// True if the shortestSide is smaller than 600p 127 | bool get isPhone => mediaQueryShortestSide < 600; 128 | 129 | /// True if the shortestSide is largest than 600p 130 | bool get isSmallTablet => mediaQueryShortestSide >= 600; 131 | 132 | /// True if the shortestSide is largest than 720p 133 | bool get isLargeTablet => mediaQueryShortestSide >= 720; 134 | 135 | /// True if the current device is Tablet 136 | bool get isTablet => isSmallTablet || isLargeTablet; 137 | 138 | /// Returns a specific value according to the screen size 139 | /// if the device width is higher than or equal to 1200 return 140 | /// [desktop] value. if the device width is higher than or equal to 600 141 | /// and less than 1200 return [tablet] value. 142 | /// if the device width is less than 300 return [watch] value. 143 | /// in other cases return [mobile] value. 144 | T responsiveValue({ 145 | T? mobile, 146 | T? tablet, 147 | T? desktop, 148 | T? watch, 149 | }) { 150 | var deviceWidth = mediaQuerySize.shortestSide; 151 | if (UniversalPlatform.isDesktop) { 152 | deviceWidth = mediaQuerySize.width; 153 | } 154 | if (deviceWidth >= 1200 && desktop != null) { 155 | return desktop; 156 | } else if (deviceWidth >= 600 && tablet != null) { 157 | return tablet; 158 | } else if (deviceWidth < 300 && watch != null) { 159 | return watch; 160 | } else { 161 | return mobile!; 162 | } 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /lib/src/view/extensions/double_extensions.dart: -------------------------------------------------------------------------------- 1 | /// MIT License 2 | /// 3 | /// Copyright (c) 2019 Jonny Borges 4 | /// 5 | /// Permission is hereby granted, free of charge, to any person obtaining a copy 6 | /// of this software and associated documentation files (the "Software"), to deal 7 | /// in the Software without restriction, including without limitation the rights 8 | /// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | /// copies of the Software, and to permit persons to whom the Software is 10 | /// furnished to do so, subject to the following conditions: 11 | /// 12 | /// The above copyright notice and this permission notice shall be included in all 13 | /// copies or substantial portions of the Software. 14 | /// 15 | /// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | /// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | /// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | /// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | /// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | /// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | /// SOFTWARE. 22 | /// 23 | /// Original source: get 4.6.1 24 | 25 | import 'dart:math'; 26 | 27 | /// Provide the precision to the double object 28 | extension Precision on double { 29 | /// 30 | double toPrecision(int fractionDigits) { 31 | final mod = pow(10, fractionDigits.toDouble()).toDouble(); 32 | return (this * mod).round().toDouble() / mod; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /lib/src/view/extensions/duration_extensions.dart: -------------------------------------------------------------------------------- 1 | /// MIT License 2 | /// 3 | /// Copyright (c) 2019 Jonny Borges 4 | /// 5 | /// Permission is hereby granted, free of charge, to any person obtaining a copy 6 | /// of this software and associated documentation files (the "Software"), to deal 7 | /// in the Software without restriction, including without limitation the rights 8 | /// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | /// copies of the Software, and to permit persons to whom the Software is 10 | /// furnished to do so, subject to the following conditions: 11 | /// 12 | /// The above copyright notice and this permission notice shall be included in all 13 | /// copies or substantial portions of the Software. 14 | /// 15 | /// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | /// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | /// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | /// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | /// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | /// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | /// SOFTWARE. 22 | /// 23 | /// Original source: get 4.6.1 24 | 25 | import 'dart:async'; 26 | 27 | /// Duration utilities. 28 | extension GetDurationUtils on Duration { 29 | /// Utility to delay some callback (or code execution). 30 | /// 31 | /// Sample: 32 | /// ``` 33 | /// void main() async { 34 | /// final _delay = 3.seconds; 35 | /// print('+ wait $_delay'); 36 | /// await _delay.delay(); 37 | /// print('- finish wait $_delay'); 38 | /// print('+ callback in 700ms'); 39 | /// await 0.7.seconds.delay(() { 40 | /// } 41 | ///``` 42 | Future delay([FutureOr Function()? callback]) async => 43 | Future.delayed(this, callback); 44 | } 45 | -------------------------------------------------------------------------------- /lib/src/view/extensions/dynamic_extensions.dart: -------------------------------------------------------------------------------- 1 | /// MIT License 2 | /// 3 | /// Copyright (c) 2019 Jonny Borges 4 | /// 5 | /// Permission is hereby granted, free of charge, to any person obtaining a copy 6 | /// of this software and associated documentation files (the "Software"), to deal 7 | /// in the Software without restriction, including without limitation the rights 8 | /// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | /// copies of the Software, and to permit persons to whom the Software is 10 | /// furnished to do so, subject to the following conditions: 11 | /// 12 | /// The above copyright notice and this permission notice shall be included in all 13 | /// copies or substantial portions of the Software. 14 | /// 15 | /// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | /// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | /// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | /// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | /// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | /// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | /// SOFTWARE. 22 | /// 23 | /// Original source: get 4.6.1 24 | 25 | import 'package:mvc_application/src/controller/get_utils/get_utils.dart'; 26 | 27 | /// 28 | extension GetDynamicUtils on dynamic { 29 | /// 30 | bool? get isBlank => GetUtils.isBlank(this); 31 | } 32 | -------------------------------------------------------------------------------- /lib/src/view/extensions/num_extensions.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:mvc_application/src/controller/get_utils/get_utils.dart'; 4 | 5 | /// 6 | extension GetNumUtils on num { 7 | /// True if this number is lower than num 8 | bool isLowerThan(num b) => GetUtils.isLowerThan(this, b); 9 | 10 | /// True if this number is greater than num 11 | bool isGreaterThan(num b) => GetUtils.isGreaterThan(this, b); 12 | 13 | /// True if this number is equal to num 14 | bool isEqual(num b) => GetUtils.isEqual(this, b); 15 | 16 | /// Utility to delay some callback (or code execution). 17 | /// to stop it. 18 | /// 19 | /// Sample: 20 | /// ``` 21 | /// void main() async { 22 | /// print('+ wait for 2 seconds'); 23 | /// await 2.delay(); 24 | /// print('- 2 seconds completed'); 25 | /// print('+ callback in 1.2sec'); 26 | /// 1.delay(() => print('- 1.2sec callback called')); 27 | /// print('currently running callback 1.2sec'); 28 | /// } 29 | ///``` 30 | Future delay([FutureOr Function()? callback]) async => 31 | Future.delayed( 32 | Duration(milliseconds: (this * 1000).round()), 33 | callback, 34 | ); 35 | 36 | /// Easy way to make Durations from numbers. 37 | /// 38 | /// Sample: 39 | /// ``` 40 | /// print(1.seconds + 200.milliseconds); 41 | /// print(1.hours + 30.minutes); 42 | /// print(1.5.hours); 43 | ///``` 44 | Duration get milliseconds => Duration(microseconds: (this * 1000).round()); 45 | 46 | /// This number in seconds. 47 | Duration get seconds => Duration(milliseconds: (this * 1000).round()); 48 | 49 | /// This number in minutes. 50 | Duration get minutes => 51 | Duration(seconds: (this * Duration.secondsPerMinute).round()); 52 | 53 | /// This number in hours. 54 | Duration get hours => 55 | Duration(minutes: (this * Duration.minutesPerHour).round()); 56 | 57 | /// This number in days. 58 | Duration get days => Duration(hours: (this * Duration.hoursPerDay).round()); 59 | } 60 | -------------------------------------------------------------------------------- /lib/src/view/extensions/string_extensions.dart: -------------------------------------------------------------------------------- 1 | /// MIT License 2 | /// 3 | /// Copyright (c) 2019 Jonny Borges 4 | /// 5 | /// Permission is hereby granted, free of charge, to any person obtaining a copy 6 | /// of this software and associated documentation files (the "Software"), to deal 7 | /// in the Software without restriction, including without limitation the rights 8 | /// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | /// copies of the Software, and to permit persons to whom the Software is 10 | /// furnished to do so, subject to the following conditions: 11 | /// 12 | /// The above copyright notice and this permission notice shall be included in all 13 | /// copies or substantial portions of the Software. 14 | /// 15 | /// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | /// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | /// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | /// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | /// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | /// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | /// SOFTWARE. 22 | /// 23 | /// Original source: get 4.6.1 24 | 25 | import 'package:mvc_application/src/controller/get_utils/get_utils.dart'; 26 | 27 | /// 28 | extension GetStringUtils on String { 29 | /// Is this String a number? 30 | bool get isNum => GetUtils.isNum(this); 31 | 32 | /// Is this String all numeric characters? 33 | bool get isNumericOnly => GetUtils.isNumericOnly(this); 34 | 35 | /// Is this String all alphabetical characters? 36 | bool get isAlphabetOnly => GetUtils.isAlphabetOnly(this); 37 | 38 | /// Is this String a boolean value? 39 | bool get isBool => GetUtils.isBool(this); 40 | 41 | /// Is this String a Vector? 42 | bool get isVectorFileName => GetUtils.isVector(this); 43 | 44 | /// Is this String an Image file? 45 | bool get isImageFileName => GetUtils.isImage(this); 46 | 47 | /// Is this String an Audio file? 48 | bool get isAudioFileName => GetUtils.isAudio(this); 49 | 50 | /// Is this String a Video file? 51 | bool get isVideoFileName => GetUtils.isVideo(this); 52 | 53 | /// Is this String a txt file? 54 | bool get isTxtFileName => GetUtils.isTxt(this); 55 | 56 | /// Is this String a Word file? 57 | bool get isDocumentFileName => GetUtils.isWord(this); 58 | 59 | /// Is this String an Excel file? 60 | bool get isExcelFileName => GetUtils.isExcel(this); 61 | 62 | /// Is this String an PPT file? 63 | bool get isPPTFileName => GetUtils.isPPT(this); 64 | 65 | /// Is this String is an APK file? 66 | bool get isAPKFileName => GetUtils.isAPK(this); 67 | 68 | /// Is this String is an PDF file? 69 | bool get isPDFFileName => GetUtils.isPDF(this); 70 | 71 | /// Is this String an HTML file? 72 | bool get isHTMLFileName => GetUtils.isHTML(this); 73 | 74 | /// Is this String a URL? 75 | bool get isURL => GetUtils.isURL(this); 76 | 77 | /// Is this String an Email? 78 | bool get isEmail => GetUtils.isEmail(this); 79 | 80 | /// Is this String a phone number? 81 | bool get isPhoneNumber => GetUtils.isPhoneNumber(this); 82 | 83 | /// Is this String a DateTime value? 84 | bool get isDateTime => GetUtils.isDateTime(this); 85 | 86 | /// Is this String 87 | bool get isMD5 => GetUtils.isMD5(this); 88 | 89 | /// Is this String a SHA1 character string? 90 | bool get isSHA1 => GetUtils.isSHA1(this); 91 | 92 | /// Is this String a SHA256 character string? 93 | bool get isSHA256 => GetUtils.isSHA256(this); 94 | 95 | /// Is this String a Binary character string? 96 | bool get isBinary => GetUtils.isBinary(this); 97 | 98 | /// Is this String a IPv4 character string? 99 | bool get isIPv4 => GetUtils.isIPv4(this); 100 | 101 | /// Is this String a IPv6 character string? 102 | bool get isIPv6 => GetUtils.isIPv6(this); 103 | 104 | /// Is this String an Hexadecimal character string? 105 | bool get isHexadecimal => GetUtils.isHexadecimal(this); 106 | 107 | /// Is this String a Palindrom? 108 | bool get isPalindrom => GetUtils.isPalindrom(this); 109 | 110 | /// This string is a Passport value? 111 | bool get isPassport => GetUtils.isPassport(this); 112 | 113 | /// Is this String a currency? 114 | bool get isCurrency => GetUtils.isCurrency(this); 115 | 116 | /// Is this String is a Cpf character string? 117 | bool get isCpf => GetUtils.isCpf(this); 118 | 119 | /// Is this String is Cnpj character string? 120 | bool get isCnpj => GetUtils.isCnpj(this); 121 | 122 | /// Does the provided string contained in this string? 123 | bool isCaseInsensitiveContains(String b) => 124 | GetUtils.isCaseInsensitiveContains(this, b); 125 | 126 | /// Does any of the string value contained in this string? 127 | bool isCaseInsensitiveContainsAny(String b) => 128 | GetUtils.isCaseInsensitiveContainsAny(this, b); 129 | 130 | /// Return this String all capitalized. 131 | String? get capitalize => GetUtils.capitalize(this); 132 | 133 | /// Return this String starting with a capital letter. 134 | String? get capitalizeFirst => GetUtils.capitalizeFirst(this); 135 | 136 | /// Return this String with no blanks. 137 | String get removeAllWhitespace => GetUtils.removeAllWhitespace(this); 138 | 139 | /// Return this String in camelcase format. 140 | String? get camelCase => GetUtils.camelCase(this); 141 | 142 | /// Return this String to param case format. 143 | String? get paramCase => GetUtils.paramCase(this); 144 | 145 | /// Return this String with only numbers. 146 | String numericOnly({bool firstWordOnly = false}) => 147 | GetUtils.numericOnly(this, firstWordOnly: firstWordOnly); 148 | 149 | /// Return this String as a path with optional segments inserted. 150 | String createPath([Iterable? segments]) { 151 | final path = startsWith('/') ? this : '/$this'; 152 | return GetUtils.createPath(path, segments); 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /lib/src/view/extensions/widget_extensions.dart: -------------------------------------------------------------------------------- 1 | /// MIT License 2 | /// 3 | /// Copyright (c) 2019 Jonny Borges 4 | /// 5 | /// Permission is hereby granted, free of charge, to any person obtaining a copy 6 | /// of this software and associated documentation files (the "Software"), to deal 7 | /// in the Software without restriction, including without limitation the rights 8 | /// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | /// copies of the Software, and to permit persons to whom the Software is 10 | /// furnished to do so, subject to the following conditions: 11 | /// 12 | /// The above copyright notice and this permission notice shall be included in all 13 | /// copies or substantial portions of the Software. 14 | /// 15 | /// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | /// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | /// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | /// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | /// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | /// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | /// SOFTWARE. 22 | /// 23 | /// Original source: get 4.6.1 24 | 25 | import 'package:flutter/widgets.dart'; 26 | 27 | /// add Padding Property to widget 28 | extension WidgetPaddingX on Widget { 29 | Widget paddingAll(double padding) => 30 | Padding(padding: EdgeInsets.all(padding), child: this); 31 | 32 | Widget paddingSymmetric({double horizontal = 0.0, double vertical = 0.0}) => 33 | Padding( 34 | padding: 35 | EdgeInsets.symmetric(horizontal: horizontal, vertical: vertical), 36 | child: this); 37 | 38 | Widget paddingOnly({ 39 | double left = 0.0, 40 | double top = 0.0, 41 | double right = 0.0, 42 | double bottom = 0.0, 43 | }) => 44 | Padding( 45 | padding: EdgeInsets.only( 46 | top: top, left: left, right: right, bottom: bottom), 47 | child: this); 48 | 49 | Widget get paddingZero => Padding(padding: EdgeInsets.zero, child: this); 50 | } 51 | 52 | /// Add margin property to widget 53 | extension WidgetMarginX on Widget { 54 | Widget marginAll(double margin) => 55 | Container(margin: EdgeInsets.all(margin), child: this); 56 | 57 | Widget marginSymmetric({double horizontal = 0.0, double vertical = 0.0}) => 58 | Container( 59 | margin: 60 | EdgeInsets.symmetric(horizontal: horizontal, vertical: vertical), 61 | child: this); 62 | 63 | Widget marginOnly({ 64 | double left = 0.0, 65 | double top = 0.0, 66 | double right = 0.0, 67 | double bottom = 0.0, 68 | }) => 69 | Container( 70 | margin: EdgeInsets.only( 71 | top: top, left: left, right: right, bottom: bottom), 72 | child: this); 73 | 74 | Widget get marginZero => Container(margin: EdgeInsets.zero, child: this); 75 | } 76 | 77 | /// Allows you to insert widgets inside a CustomScrollView 78 | extension WidgetSliverBoxX on Widget { 79 | Widget get sliverBox => SliverToBoxAdapter(child: this); 80 | } 81 | -------------------------------------------------------------------------------- /lib/src/view/mvc.dart: -------------------------------------------------------------------------------- 1 | /// 2 | /// Copyright (C) 2019 Andrious Solutions 3 | /// 4 | /// Licensed under the Apache License, Version 2.0 (the "License"); 5 | /// you may not use this file except in compliance with the License. 6 | /// You may obtain a copy of the License at 7 | /// 8 | /// http://www.apache.org/licenses/LICENSE-2.0 9 | /// 10 | /// Unless required by applicable law or agreed to in writing, software 11 | /// distributed under the License is distributed on an "AS IS" BASIS, 12 | /// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | /// See the License for the specific language governing permissions and 14 | /// limitations under the License. 15 | /// 16 | /// Created 12 Mar 2019 17 | /// 18 | /// 19 | 20 | import 'package:flutter/material.dart' 21 | show 22 | Widget, 23 | Color, 24 | FlutterErrorDetails, 25 | GenerateAppTitle, 26 | GlobalKey, 27 | Locale, 28 | LocaleResolutionCallback, 29 | LocalizationsDelegate, 30 | NavigatorObserver, 31 | NavigatorState, 32 | RouteFactory, 33 | ThemeData, 34 | TransitionBuilder, 35 | WidgetBuilder; 36 | 37 | import 'package:mvc_application/view.dart' show AppState; 38 | 39 | import 'package:mvc_application/controller.dart' show AppController; 40 | 41 | /// Passed as 'View' to MVC class for a simple app. 42 | class ViewMVC extends AppState { 43 | /// Supply the interface properties for this simple App 44 | ViewMVC({ 45 | Widget? home, 46 | AppController? con, 47 | GlobalKey? navigatorKey, 48 | Map? routes, 49 | String? initialRoute, 50 | RouteFactory? onGenerateRoute, 51 | RouteFactory? onUnknownRoute, 52 | List? navigatorObservers, 53 | TransitionBuilder? builder, 54 | String? title, 55 | GenerateAppTitle? onGenerateTitle, 56 | ThemeData? theme, 57 | Color? color, 58 | Locale? locale, 59 | Iterable>? localizationsDelegates, 60 | LocaleResolutionCallback? localeResolutionCallback, 61 | List? supportedLocales, 62 | bool? debugShowMaterialGrid, 63 | bool? showPerformanceOverlay, 64 | bool? checkerboardRasterCacheImages, 65 | bool? checkerboardOffscreenLayers, 66 | bool? showSemanticsDebugger, 67 | bool? debugShowCheckedModeBanner, 68 | }) : super( 69 | home: home, 70 | con: con ?? AppController(), 71 | navigatorKey: navigatorKey, 72 | routes: routes, 73 | initialRoute: initialRoute, 74 | onGenerateRoute: onGenerateRoute, 75 | onUnknownRoute: onUnknownRoute, 76 | navigatorObservers: navigatorObservers, 77 | builder: builder, 78 | title: title, 79 | onGenerateTitle: onGenerateTitle, 80 | theme: theme, 81 | color: color, 82 | locale: locale, 83 | localizationsDelegates: localizationsDelegates, 84 | localeResolutionCallback: localeResolutionCallback, 85 | supportedLocales: supportedLocales, 86 | debugShowMaterialGrid: debugShowMaterialGrid, 87 | showPerformanceOverlay: showPerformanceOverlay, 88 | checkerboardRasterCacheImages: checkerboardRasterCacheImages, 89 | checkerboardOffscreenLayers: checkerboardOffscreenLayers, 90 | showSemanticsDebugger: showSemanticsDebugger, 91 | debugShowCheckedModeBanner: debugShowCheckedModeBanner, 92 | ); 93 | 94 | @override 95 | Future initAsync() => super.initAsync(); 96 | 97 | @override 98 | bool onAsyncError(FlutterErrorDetails details) => super.onAsyncError(details); 99 | } 100 | -------------------------------------------------------------------------------- /lib/src/view/platforms/run_app.dart: -------------------------------------------------------------------------------- 1 | /// 2 | /// Copyright (C) 2021 Andrious Solutions 3 | /// 4 | /// Licensed under the Apache License, Version 2.0 (the "License"); 5 | /// you may not use this file except in compliance with the License. 6 | /// You may obtain a copy of the License at 7 | /// 8 | /// http://www.apache.org/licenses/LICENSE-2.0 9 | /// 10 | /// Unless required by applicable law or agreed to in writing, software 11 | /// distributed under the License is distributed on an "AS IS" BASIS, 12 | /// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | /// See the License for the specific language governing permissions and 14 | /// limitations under the License. 15 | /// 16 | /// Created 28 Sep 2021 17 | /// 18 | import 'dart:async' show runZonedGuarded; 19 | 20 | import 'dart:isolate' show Isolate, RawReceivePort; 21 | 22 | import 'package:flutter/foundation.dart' show FlutterExceptionHandler; 23 | 24 | import 'package:flutter/material.dart' as m 25 | show ErrorWidgetBuilder, Widget, runApp; 26 | 27 | import 'package:mvc_application/view.dart' as v 28 | show AppErrorHandler, ReportErrorHandler; 29 | 30 | /// Add an Error Handler right at the start. 31 | void runApp( 32 | m.Widget app, { 33 | FlutterExceptionHandler? errorHandler, 34 | m.ErrorWidgetBuilder? errorScreen, 35 | v.ReportErrorHandler? errorReport, 36 | bool allowNewHandlers = false, 37 | }) { 38 | // Instantiate the app's error handler. 39 | final handler = v.AppErrorHandler( 40 | handler: errorHandler, 41 | builder: errorScreen, 42 | report: errorReport, 43 | allowNewHandlers: allowNewHandlers); 44 | 45 | Isolate.current.addErrorListener(RawReceivePort((dynamic pair) { 46 | // 47 | if (pair is List) { 48 | final isolateError = pair; 49 | handler.isolateError( 50 | isolateError.first.toString(), 51 | StackTrace.fromString(isolateError.last.toString()), 52 | ); 53 | } 54 | }).sendPort); 55 | 56 | // Catch any errors attempting to execute runApp(); 57 | runZonedGuarded(() { 58 | m.runApp(app); 59 | }, handler.runZonedError); 60 | } 61 | -------------------------------------------------------------------------------- /lib/src/view/platforms/run_webapp.dart: -------------------------------------------------------------------------------- 1 | /// 2 | /// Copyright (C) 2021 Andrious Solutions 3 | /// 4 | /// Licensed under the Apache License, Version 2.0 (the "License"); 5 | /// you may not use this file except in compliance with the License. 6 | /// You may obtain a copy of the License at 7 | /// 8 | /// http://www.apache.org/licenses/LICENSE-2.0 9 | /// 10 | /// Unless required by applicable law or agreed to in writing, software 11 | /// distributed under the License is distributed on an "AS IS" BASIS, 12 | /// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | /// See the License for the specific language governing permissions and 14 | /// limitations under the License. 15 | /// 16 | /// Created 28 Sep 2021 17 | /// 18 | 19 | import 'dart:async' show runZonedGuarded; 20 | 21 | import 'package:flutter/foundation.dart' show FlutterExceptionHandler; 22 | 23 | import 'package:flutter/material.dart' as m 24 | show ErrorWidgetBuilder, Widget, runApp; 25 | 26 | import 'package:mvc_application/view.dart' as v 27 | show AppErrorHandler, ReportErrorHandler; 28 | 29 | /// To change the URL strategy from hash to path 30 | /// and remove that annoying # sign from the website's url. 31 | import 'package:url_strategy/url_strategy.dart'; 32 | 33 | /// Add an Error Handler right at the start. 34 | void runApp( 35 | m.Widget app, { 36 | FlutterExceptionHandler? errorHandler, 37 | m.ErrorWidgetBuilder? errorScreen, 38 | v.ReportErrorHandler? errorReport, 39 | bool allowNewHandlers = false, 40 | }) { 41 | // Instantiate the app's error handler. 42 | final handler = v.AppErrorHandler( 43 | handler: errorHandler, 44 | builder: errorScreen, 45 | report: errorReport, 46 | allowNewHandlers: allowNewHandlers); 47 | 48 | // Here we set the URL strategy for our web app. 49 | // It is safe to call this function when running on mobile or desktop as well. 50 | setPathUrlStrategy(); 51 | 52 | // Catch any errors attempting to execute runApp(); 53 | runZonedGuarded(() { 54 | m.runApp(app); 55 | }, handler.runZonedError); 56 | } 57 | -------------------------------------------------------------------------------- /lib/src/view/utils/app_settings.dart: -------------------------------------------------------------------------------- 1 | /// 2 | /// Copyright (C) 2018 Andrious Solutions 3 | /// 4 | /// Licensed under the Apache License, Version 2.0 (the "License"); 5 | /// you may not use this file except in compliance with the License. 6 | /// You may obtain a copy of the License at 7 | /// 8 | /// http://www.apache.org/licenses/LICENSE-2.0 9 | /// 10 | /// Unless required by applicable law or agreed to in writing, software 11 | /// distributed under the License is distributed on an "AS IS" BASIS, 12 | /// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | /// See the License for the specific language governing permissions and 14 | /// limitations under the License. 15 | /// 16 | /// Created 11 Sep 2018 17 | /// 18 | 19 | import 'package:flutter/material.dart' 20 | show 21 | AlignmentDirectional, 22 | BoxConstraints, 23 | BuildContext, 24 | Container, 25 | DefaultTextStyle, 26 | EdgeInsetsDirectional, 27 | IconTheme, 28 | Key, 29 | MediaQuery, 30 | MergeSemantics, 31 | StatelessWidget, 32 | Text, 33 | TextButton, 34 | TextOverflow, 35 | TextSpan, 36 | TextStyle, 37 | Theme, 38 | VoidCallback, 39 | Widget, 40 | showAboutDialog; 41 | 42 | import 'package:flutter/gestures.dart' show TapGestureRecognizer; 43 | 44 | import 'package:url_launcher/url_launcher.dart' show launch; 45 | 46 | import 'package:flutter/foundation.dart' as p 47 | show defaultTargetPlatform, TargetPlatform; 48 | 49 | // ignore: avoid_classes_with_only_static_members 50 | /// Readily supply the app's settings in an about window. 51 | class AppSettings { 52 | /// Return the 'default' [p.TargetPlatform] object. 53 | static p.TargetPlatform get defaultTargetPlatform => p.defaultTargetPlatform; 54 | 55 | /// A simple Widget of Text to 'tap' on. 56 | static StatelessWidget tapText(String text, VoidCallback onTap, 57 | {TextStyle? style}) { 58 | return _TapText(text, onTap, style: style); 59 | } 60 | 61 | /// A simple URL link Widget. 62 | static _LinkTextSpan linkTextSpan( 63 | {TextStyle? style, String? url, String? text}) { 64 | return _LinkTextSpan(style: style, url: url, text: text); 65 | } 66 | 67 | /// Show a simple 'About' Screen displaying information about the App. 68 | static void showAbout({ 69 | required BuildContext context, 70 | String? applicationName, 71 | String? applicationVersion, 72 | Widget? applicationIcon, 73 | String? applicationLegalese, 74 | List? children, 75 | }) { 76 | showAboutDialog( 77 | context: context, 78 | applicationName: applicationName, 79 | applicationVersion: applicationVersion, 80 | applicationIcon: applicationIcon, 81 | applicationLegalese: applicationLegalese, 82 | children: children, 83 | ); 84 | } 85 | } 86 | 87 | class _TapText extends StatelessWidget { 88 | const _TapText(this.text, this.onTap, {this.style}); 89 | 90 | final String text; 91 | final VoidCallback onTap; 92 | final TextStyle? style; 93 | 94 | @override 95 | Widget build(BuildContext context) { 96 | return _OptionsItem( 97 | child: _FlatButton( 98 | onPressed: onTap, 99 | style: style, 100 | child: Text(text), 101 | ), 102 | ); 103 | } 104 | } 105 | 106 | class _OptionsItem extends StatelessWidget { 107 | const _OptionsItem({Key? key, this.child}) : super(key: key); 108 | 109 | final Widget? child; 110 | 111 | @override 112 | Widget build(BuildContext context) { 113 | return MergeSemantics( 114 | child: Container( 115 | constraints: BoxConstraints( 116 | minHeight: 48.0 * MediaQuery.textScaleFactorOf(context)), 117 | padding: const EdgeInsetsDirectional.only(start: 56), 118 | alignment: AlignmentDirectional.centerStart, 119 | child: DefaultTextStyle( 120 | style: DefaultTextStyle.of(context).style, 121 | maxLines: 2, 122 | overflow: TextOverflow.fade, 123 | child: IconTheme( 124 | data: Theme.of(context).primaryIconTheme, 125 | child: child!, 126 | ), 127 | ), 128 | ), 129 | ); 130 | } 131 | } 132 | 133 | class _FlatButton extends StatelessWidget { 134 | const _FlatButton({ 135 | Key? key, 136 | this.onPressed, 137 | this.child, 138 | this.style, 139 | }) : super(key: key); 140 | 141 | final VoidCallback? onPressed; 142 | final Widget? child; 143 | final TextStyle? style; 144 | 145 | @override 146 | Widget build(BuildContext context) { 147 | final child = style == null 148 | ? this.child! 149 | : DefaultTextStyle( 150 | style: style!, 151 | child: this.child!, 152 | ); 153 | return TextButton( 154 | onPressed: onPressed, 155 | child: child, 156 | ); 157 | } 158 | } 159 | 160 | class _LinkTextSpan extends TextSpan { 161 | // Beware! 162 | // 163 | // This class is only safe because the TapGestureRecognizer is not 164 | // given a deadline and therefore never allocates any resources. 165 | // 166 | // In any other situation -- setting a deadline, using any of the less trivial 167 | // recognizers, etc -- you would have to manage the gesture recognizer's 168 | // lifetime and call dispose() when the TextSpan was no longer being rendered. 169 | // 170 | // Since TextSpan itself is @immutable, this means that you would have to 171 | // manage the recognizer from outside the TextSpan, e.g. in the State of a 172 | // stateful widget that then hands the recognizer to the TextSpan. 173 | 174 | _LinkTextSpan({TextStyle? style, String? url, String? text}) 175 | : super( 176 | style: style, 177 | text: text ?? url, 178 | recognizer: TapGestureRecognizer() 179 | ..onTap = () { 180 | launch(url!, forceSafariVC: false); 181 | }); 182 | } 183 | -------------------------------------------------------------------------------- /lib/src/view/utils/inherited_state.dart: -------------------------------------------------------------------------------- 1 | /// 2 | /// Copyright (C) 2021 Andrious Solutions 3 | /// 4 | /// Licensed under the Apache License, Version 2.0 (the "License"); 5 | /// you may not use this file except in compliance with the License. 6 | /// You may obtain a copy of the License at 7 | /// 8 | /// http://www.apache.org/licenses/LICENSE-2.0 9 | /// 10 | /// Unless required by applicable law or agreed to in writing, software 11 | /// distributed under the License is distributed on an "AS IS" BASIS, 12 | /// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | /// See the License for the specific language governing permissions and 14 | /// limitations under the License. 15 | /// 16 | /// Created 11 Dec 2021 17 | /// 18 | /// 19 | 20 | import 'package:mvc_application/controller.dart'; 21 | import 'package:mvc_application/view.dart'; 22 | 23 | // ignore: avoid_classes_with_only_static_members 24 | /// Builds a [InheritedWidget]. 25 | /// 26 | /// It's instantiated in a standalone widget 27 | /// so its setState() call will **only** rebuild 28 | /// [InheritedWidget] and consequently any of its dependents, 29 | /// instead of rebuilding the app's entire widget tree. 30 | class InheritedStates { 31 | // 32 | static final Map _states = {}; 33 | 34 | /// Returns an Object containing InheritedWidget and its build function 35 | static InheritedStateWidget add(InheritedWidget? Function() func, 36 | {Key? key}) => 37 | InheritedStateWidget(func, key: key); 38 | 39 | /// Link a widget to an InheritedWidget of type T 40 | static bool inheritWidget(BuildContext context, 41 | {Object? aspect}) => 42 | context.dependOnInheritedWidgetOfExactType(aspect: aspect) != null; 43 | 44 | /// If the widget has been mounted with its State object 45 | static bool has() => _states.containsKey(T); 46 | 47 | /// Calls the build() function in this Widget's State object. 48 | static void rebuild() { 49 | // 50 | final inheritedWidget = _states[_type()]; 51 | 52 | inheritedWidget?.setState(() {}); 53 | } 54 | } 55 | 56 | /// Explicitly returns a Type 57 | Type _type() => U; 58 | 59 | /// Provides the build() function to be rebuilt 60 | class InheritedStateWidget extends StatefulWidget { 61 | /// Supply a Callback function returning an InheritedWidget 62 | InheritedStateWidget(this._func, {Key? key}) : super(key: key); 63 | final InheritedWidget? Function() _func; 64 | 65 | // Retains a reference to its State object 66 | final Set<_InheritedStateWidget> _state = {_InheritedStateWidget()}; 67 | 68 | @override 69 | //ignore: no_logic_in_create_state 70 | State createState() => _state.first; 71 | 72 | /// Calls the build() function in this Widget's State object. 73 | void rebuild() => _state.first.setState(() {}); 74 | } 75 | 76 | class _InheritedStateWidget extends State { 77 | @override 78 | void initState() { 79 | super.initState(); 80 | // Record this State object for later reference. 81 | final InheritedWidget? inheritedWidget = widget._func(); 82 | if (inheritedWidget != null) { 83 | _type = inheritedWidget.runtimeType; 84 | InheritedStates._states[_type] = this; 85 | } 86 | } 87 | 88 | /// Record the runtimeType of the InheritedWidget 89 | late Type _type; 90 | 91 | /// Supply a reference to this State object back to its Widget 92 | @override 93 | void didChangeDependencies() { 94 | widget._state.add(this); 95 | super.didChangeDependencies(); 96 | } 97 | 98 | /// Remove the reference 99 | @override 100 | void dispose() { 101 | // Any reference to this State object should be removed. 102 | InheritedStates._states.remove(_type); 103 | widget._state.clear(); 104 | super.dispose(); 105 | } 106 | 107 | /// If its widget is re-created update the its State Set. 108 | @override 109 | void didUpdateWidget(covariant InheritedStateWidget oldWidget) { 110 | // Don't want a memory leak 111 | oldWidget._state.clear(); 112 | 113 | widget._state.add(this); 114 | 115 | super.didUpdateWidget(oldWidget); 116 | } 117 | 118 | /// Don't if the widget is not in the widget tree. 119 | @override 120 | void setState(VoidCallback fn) { 121 | if (mounted) { 122 | super.setState(fn); 123 | } 124 | } 125 | 126 | @override 127 | Widget build(BuildContext context) => widget._func() ?? const SizedBox(); 128 | } 129 | -------------------------------------------------------------------------------- /lib/src/view/utils/loading_screen.dart: -------------------------------------------------------------------------------- 1 | /// 2 | /// Copyright (C) 2018 Andrious Solutions 3 | /// 4 | /// Licensed under the Apache License, Version 2.0 (the "License"); 5 | /// you may not use this file except in compliance with the License. 6 | /// You may obtain a copy of the License at 7 | /// 8 | /// http://www.apache.org/licenses/LICENSE-2.0 9 | /// 10 | /// Unless required by applicable law or agreed to in writing, software 11 | /// distributed under the License is distributed on an "AS IS" BASIS, 12 | /// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | /// See the License for the specific language governing permissions and 14 | /// limitations under the License. 15 | /// 16 | /// Created 21 Jun 2018 17 | /// 18 | 19 | import 'package:flutter/material.dart' 20 | show 21 | AnimatedBuilder, 22 | Animation, 23 | AnimationController, 24 | AnimationStatus, 25 | AppBar, 26 | BuildContext, 27 | Center, 28 | CircularProgressIndicator, 29 | CurvedAnimation, 30 | Curves, 31 | Interval, 32 | Key, 33 | MaterialApp, 34 | Scaffold, 35 | SingleTickerProviderStateMixin, 36 | State, 37 | StatefulWidget, 38 | Text, 39 | Widget; 40 | 41 | /// 42 | /// This is just a basic `Scaffold` with a centered `CircularProgressIndicator` 43 | /// class right in the middle of the screen. 44 | /// 45 | /// It's copied from the `flutter_gallery` example project in flutter/flutter 46 | /// 47 | class LoadingScreen extends StatefulWidget { 48 | /// Basic `Scaffold` with a centered `CircularProgressIndicator` 49 | const LoadingScreen({Key? key}) : super(key: key); 50 | @override 51 | _LoadingScreenState createState() => _LoadingScreenState(); 52 | } 53 | 54 | class _LoadingScreenState extends State 55 | with SingleTickerProviderStateMixin { 56 | late AnimationController _controller; 57 | late Animation _animation; 58 | 59 | @override 60 | void initState() { 61 | super.initState(); 62 | _controller = AnimationController( 63 | duration: const Duration(milliseconds: 1500), vsync: this) 64 | ..forward(); 65 | _animation = CurvedAnimation( 66 | parent: _controller, 67 | curve: const Interval(0, 0.9, curve: Curves.fastOutSlowIn), 68 | reverseCurve: Curves.fastOutSlowIn) 69 | ..addStatusListener((AnimationStatus status) { 70 | if (status == AnimationStatus.dismissed) { 71 | _controller.forward(); 72 | } else if (status == AnimationStatus.completed) { 73 | _controller.reverse(); 74 | } 75 | }); 76 | } 77 | 78 | @override 79 | void dispose() { 80 | _controller.stop(); 81 | super.dispose(); 82 | } 83 | 84 | @override 85 | Widget build(BuildContext context) { 86 | return MaterialApp( 87 | home: Scaffold( 88 | appBar: AppBar(title: const Text('Loading...')), 89 | body: AnimatedBuilder( 90 | animation: _animation, 91 | builder: (BuildContext context, Widget? child) { 92 | return const Center(child: CircularProgressIndicator()); 93 | }))); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /lib/src/view/utils/timezone.dart: -------------------------------------------------------------------------------- 1 | import 'package:timezone/data/latest.dart'; 2 | import 'package:timezone/timezone.dart' as t; 3 | import 'package:flutter_native_timezone/flutter_native_timezone.dart'; 4 | 5 | /// Supply timezone information. 6 | class TimeZone { 7 | /// A factory constructor for only one single instance. 8 | factory TimeZone() => _this ?? TimeZone._(); 9 | 10 | TimeZone._() { 11 | initializeTimeZones(); 12 | } 13 | static TimeZone? _this; 14 | 15 | /// Supply a String describing the current timezone. 16 | Future getTimeZoneName() async => 17 | FlutterNativeTimezone.getLocalTimezone(); 18 | 19 | /// Returns a Location object from the specified timezone. 20 | Future getLocation([String? timeZoneName]) async { 21 | if (timeZoneName == null || timeZoneName.isEmpty) { 22 | timeZoneName = await getTimeZoneName(); 23 | } 24 | return t.getLocation(timeZoneName); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /lib/src/view/uxutils/controller.dart: -------------------------------------------------------------------------------- 1 | /// 2 | /// Copyright (C) 2019 Andrious Solutions 3 | /// 4 | /// Licensed under the Apache License, Version 2.0 (the "License"); 5 | /// you may not use this file except in compliance with the License. 6 | /// You may obtain a copy of the License at 7 | /// 8 | /// http://www.apache.org/licenses/LICENSE-2.0 9 | /// 10 | /// Unless required by applicable law or agreed to in writing, software 11 | /// distributed under the License is distributed on an "AS IS" BASIS, 12 | /// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | /// See the License for the specific language governing permissions and 14 | /// limitations under the License. 15 | /// 16 | /// Created 05 Feb 2019 17 | /// 18 | /// 19 | 20 | // export 'package:mxc_application/src/view/uxutils/src/controller'; 21 | -------------------------------------------------------------------------------- /lib/src/view/uxutils/model.dart: -------------------------------------------------------------------------------- 1 | /// 2 | /// Copyright (C) 2019 Andrious Solutions 3 | /// 4 | /// Licensed under the Apache License, Version 2.0 (the "License"); 5 | /// you may not use this file except in compliance with the License. 6 | /// You may obtain a copy of the License at 7 | /// 8 | /// http://www.apache.org/licenses/LICENSE-2.0 9 | /// 10 | /// Unless required by applicable law or agreed to in writing, software 11 | /// distributed under the License is distributed on an "AS IS" BASIS, 12 | /// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | /// See the License for the specific language governing permissions and 14 | /// limitations under the License. 15 | /// 16 | /// Created 05 Feb 2019 17 | /// 18 | /// 19 | 20 | // export 'package:mxc_application/src/view/uxutils/src/controller'; 21 | -------------------------------------------------------------------------------- /lib/src/view/uxutils/src/view/common_widgets/custom_raised_button.dart: -------------------------------------------------------------------------------- 1 | /// 2 | /// Copyright (C) 2019 Andrious Solutions 3 | /// 4 | /// Licensed under the Apache License, Version 2.0 (the "License"); 5 | /// you may not use this file except in compliance with the License. 6 | /// You may obtain a copy of the License at 7 | /// 8 | /// http://www.apache.org/licenses/LICENSE-2.0 9 | /// 10 | /// Unless required by applicable law or agreed to in writing, software 11 | /// distributed under the License is distributed on an "AS IS" BASIS, 12 | /// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | /// See the License for the specific language governing permissions and 14 | /// limitations under the License. 15 | /// 16 | /// 17 | /// 18 | import 'package:flutter/material.dart'; 19 | 20 | /// A Elevated button with a built-in spinner 21 | /// Used to convey an on-going process that completes with an enabled button. 22 | @immutable 23 | class CustomRaisedButton extends StatelessWidget { 24 | /// A constructor that takes in Elevated buttons properties. 25 | const CustomRaisedButton({ 26 | Key? key, 27 | this.loading, 28 | required this.onPressed, 29 | this.onLongPress, 30 | this.onHover, 31 | this.onFocusChange, 32 | this.style, 33 | this.focusNode, 34 | this.autofocus, 35 | this.clipBehavior, 36 | required this.child, 37 | }) : super(key: key); 38 | 39 | /// A flag when True will enable the button. 40 | final bool? loading; 41 | 42 | /// Optional Callback function 43 | final VoidCallback? onPressed; 44 | 45 | /// Optional 'Long Press' Callback function 46 | final VoidCallback? onLongPress; 47 | 48 | /// Optional. Called when a pointer enters or exits the button response area. 49 | final ValueChanged? onHover; 50 | 51 | /// Optional. Called when the focus changes. 52 | final ValueChanged? onFocusChange; 53 | 54 | /// Customizes this button's appearance. 55 | final ButtonStyle? style; 56 | 57 | /// To obtain the keyboard focus and to handle keyboard events. 58 | final FocusNode? focusNode; 59 | 60 | /// If True, this widget will be selected as the initial focus when no other 61 | /// node in its scope is currently focused. 62 | final bool? autofocus; 63 | 64 | /// Different ways to clip a widget's content. 65 | final Clip? clipBehavior; 66 | 67 | /// Typically the button's label. 68 | final Widget? child; 69 | 70 | @override 71 | Widget build(BuildContext context) => ElevatedButton( 72 | onPressed: loading ?? false ? null : onPressed, 73 | onLongPress: onLongPress, 74 | onHover: onHover, 75 | onFocusChange: onFocusChange, 76 | style: style, 77 | focusNode: focusNode, 78 | autofocus: autofocus ?? false, 79 | clipBehavior: clipBehavior ?? Clip.none, 80 | child: loading ?? false ? buttonSpinner(context) : child, 81 | ); 82 | 83 | /// Displays a Processing Indicator. 84 | Widget buttonSpinner(BuildContext context) { 85 | var data = Theme.of(context); 86 | data = data.copyWith( 87 | colorScheme: data.colorScheme.copyWith(secondary: Colors.white70)); 88 | return Theme( 89 | data: data, 90 | child: const SizedBox( 91 | width: 28, 92 | height: 28, 93 | child: CircularProgressIndicator( 94 | strokeWidth: 3, 95 | ), 96 | ), 97 | ); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /lib/src/view/uxutils/src/view/custom_scroll_physics.dart: -------------------------------------------------------------------------------- 1 | /// 2 | /// Author Tim Rijckaert https://github.com/timrijckaert 3 | /// 4 | /// Licensed under the Apache License, Version 2.0 (the "License"); 5 | /// you may not use this file except in compliance with the License. 6 | /// You may obtain a copy of the License at 7 | /// 8 | /// http://www.apache.org/licenses/LICENSE-2.0 9 | /// 10 | /// Unless required by applicable law or agreed to in writing, software 11 | /// distributed under the License is distributed on an "AS IS" BASIS, 12 | /// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | /// See the License for the specific language governing permissions and 14 | /// limitations under the License. 15 | /// 16 | /// Created Sep 27, 2019 17 | /// 18 | /// 19 | 20 | import 'dart:math' show min; 21 | 22 | import 'package:flutter/material.dart' 23 | show 24 | ScrollMetrics, 25 | ScrollPhysics, 26 | ScrollPosition, 27 | ScrollSpringSimulation, 28 | Simulation, 29 | Tolerance; 30 | 31 | /// A 'Snapping' Scrolling Physics 32 | class SnappingListScrollPhysics extends ScrollPhysics { 33 | /// Supply the intended width of the items scrolled. 34 | /// Optionally supply a 'parent' Physics object to encompass. 35 | const SnappingListScrollPhysics({ 36 | required this.itemWidth, 37 | ScrollPhysics? parent, 38 | }) : super(parent: parent); 39 | 40 | /// Supply the intended width of the items scrolled. 41 | final double itemWidth; 42 | 43 | @override 44 | SnappingListScrollPhysics applyTo(ScrollPhysics? ancestor) => 45 | SnappingListScrollPhysics( 46 | parent: buildParent(ancestor), 47 | itemWidth: itemWidth, 48 | ); 49 | 50 | double _getItem(ScrollPosition position) => (position.pixels) / itemWidth; 51 | 52 | double _getPixels(ScrollPosition position, double item) => 53 | min(item * itemWidth, position.maxScrollExtent); 54 | 55 | double _getTargetPixels( 56 | ScrollPosition position, Tolerance tolerance, double velocity) { 57 | var item = _getItem(position); 58 | if (velocity < -tolerance.velocity) { 59 | item -= 0.5; 60 | } else if (velocity > tolerance.velocity) { 61 | item += 0.5; 62 | } 63 | return _getPixels(position, item.roundToDouble()); 64 | } 65 | 66 | @override 67 | Simulation? createBallisticSimulation( 68 | ScrollMetrics position, double velocity) { 69 | // If we're out of range and not headed back in range, defer to the parent 70 | // ballistics, which should put us back in range at a page boundary. 71 | if ((velocity <= 0.0 && position.pixels <= position.minScrollExtent) || 72 | (velocity >= 0.0 && position.pixels >= position.maxScrollExtent)) { 73 | return super.createBallisticSimulation(position, velocity); 74 | } 75 | final tolerance = this.tolerance; 76 | final target = 77 | _getTargetPixels(position as ScrollPosition, tolerance, velocity); 78 | if (target != position.pixels) { 79 | return ScrollSpringSimulation(spring, position.pixels, target, velocity, 80 | tolerance: tolerance); 81 | } 82 | return null; 83 | } 84 | 85 | @override 86 | bool get allowImplicitScrolling => false; 87 | } 88 | 89 | ///// 90 | ///// 91 | ///// Author: Norris Duncan https://github.com/norrisduncan 92 | ///// 93 | ///// 94 | //class CustomSnappingScrollPhysicsForTheControlPanelHousing extends ScrollPhysics { 95 | // final List stoppingPoints; 96 | // final SpringDescription springDescription = SpringDescription(mass: 100.0, damping: .2, stiffness: 50.0); 97 | // 98 | // @override 99 | // CustomSnappingScrollPhysicsForTheControlPanelHousing({@required this.stoppingPoints, ScrollPhysics parent}) 100 | // : super(parent: parent) { 101 | // stoppingPoints.sort(); 102 | // } 103 | // 104 | // @override 105 | // CustomSnappingScrollPhysicsForTheControlPanelHousing applyTo(ScrollPhysics ancestor) { 106 | // return new CustomSnappingScrollPhysicsForTheControlPanelHousing( 107 | // stoppingPoints: stoppingPoints, parent: buildParent(ancestor)); 108 | // } 109 | // 110 | // @override 111 | // Simulation createBallisticSimulation(ScrollMetrics scrollMetrics, double velocity) { 112 | // double targetStoppingPoint = _getTargetStoppingPointPixels(scrollMetrics.pixels, velocity, 0.0003, stoppingPoints); 113 | // 114 | // return ScrollSpringSimulation(springDescription, scrollMetrics.pixels, targetStoppingPoint, velocity, 115 | // tolerance: Tolerance(velocity: .00003, distance: .003)); 116 | // } 117 | // 118 | // double _getTargetStoppingPointPixels( 119 | // double initialPosition, double velocity, double drag, List stoppingPoints) { 120 | // double endPointBeforeSnappingIsCalculated = 121 | // initialPosition + (-velocity / math.log(drag)).clamp(stoppingPoints[0], stoppingPoints.last); 122 | // if (stoppingPoints.contains(endPointBeforeSnappingIsCalculated)) { 123 | // return endPointBeforeSnappingIsCalculated; 124 | // } 125 | // if (endPointBeforeSnappingIsCalculated > stoppingPoints.last) { 126 | // return stoppingPoints.last; 127 | // } 128 | // for (int i = 0; i < stoppingPoints.length; i++) { 129 | // if (endPointBeforeSnappingIsCalculated < stoppingPoints[i] && 130 | // endPointBeforeSnappingIsCalculated < stoppingPoints[i] - (stoppingPoints[i] - stoppingPoints[i - 1]) / 2) { 131 | // double stoppingPoint = stoppingPoints[i - 1]; 132 | // debugPrint(stoppingPoint.toString()); 133 | // return stoppingPoint; 134 | // } else if (endPointBeforeSnappingIsCalculated < stoppingPoints[i] && 135 | // endPointBeforeSnappingIsCalculated > stoppingPoints[i] - (stoppingPoints[i] - stoppingPoints[i - 1]) / 2) { 136 | // double stoppingPoint = stoppingPoints[i]; 137 | // debugPrint(stoppingPoint.toString()); 138 | // return stoppingPoint; 139 | // } 140 | // } 141 | // throw Error.safeToString('Failed finding a new scroll simulation endpoint for this scroll animation'); 142 | // } 143 | //} 144 | -------------------------------------------------------------------------------- /lib/src/view/uxutils/src/view/iso_spinner.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Andrious Solutions Ltd. All rights reserved. 2 | // Use of this source code is governed by a Apache License, Version 2.0. 3 | // The main directory contains that LICENSE file. 4 | 5 | import 'package:mvc_application/view.dart'; 6 | 7 | import 'package:flutter/gestures.dart' show PointerDeviceKind; 8 | 9 | /// A Spinner listing the available Locales. 10 | class ISOSpinner extends StatefulWidget { 11 | /// Supply the supported Locales and Item Changed Routine. 12 | const ISOSpinner({ 13 | Key? key, 14 | this.initialItem, 15 | this.locale, 16 | required this.supportedLocales, 17 | required this.onSelectedItemChanged, 18 | }) : super(key: key); 19 | 20 | /// The 'current' Locale. 21 | final Locale? locale; 22 | 23 | /// The currently 'selected' Locale item on the Spinner. 24 | final int? initialItem; 25 | 26 | /// The List of supported Locales. 27 | final List supportedLocales; 28 | 29 | /// The routine called when a new Locale is selected. 30 | final Future Function(int index) onSelectedItemChanged; 31 | 32 | @override 33 | State createState() => _SpinnerState(); 34 | } 35 | 36 | class _SpinnerState extends State { 37 | @override 38 | void initState() { 39 | super.initState(); 40 | 41 | locales = widget.supportedLocales; 42 | 43 | int? index; 44 | 45 | if (widget.initialItem != null && widget.initialItem! > -1) { 46 | index = widget.initialItem!; 47 | } else if (widget.locale != null) { 48 | index = locales.indexOf(widget.locale!); 49 | } 50 | 51 | if (index == null || index < 0) { 52 | index = 0; 53 | } 54 | controller = FixedExtentScrollController(initialItem: index); 55 | } 56 | 57 | late List locales; 58 | FixedExtentScrollController? controller; 59 | 60 | @override 61 | Widget build(BuildContext context) { 62 | // 63 | Widget widget = CupertinoPicker.builder( 64 | itemExtent: 25, //height of each item 65 | childCount: locales.length, 66 | scrollController: controller, 67 | onSelectedItemChanged: this.widget.onSelectedItemChanged, 68 | itemBuilder: (BuildContext context, int index) => Text( 69 | locales[index].countryCode == null 70 | ? locales[index].languageCode 71 | : '${locales[index].languageCode}-${locales[index].countryCode}', 72 | style: const TextStyle(fontSize: 20), 73 | ), 74 | ); 75 | 76 | // By design, gestures are turned off on browser screens 77 | if (UniversalPlatform.isWeb && !App.inSmallScreen) { 78 | // 79 | widget = ScrollConfiguration( 80 | behavior: ScrollConfiguration.of(context).copyWith( 81 | dragDevices: { 82 | PointerDeviceKind.touch, 83 | PointerDeviceKind.mouse, 84 | }, 85 | ), 86 | child: widget, 87 | ); 88 | } 89 | 90 | return SizedBox( 91 | height: 100, 92 | child: widget, 93 | ); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /lib/src/view/uxutils/src/view/show_cupertino_date_picker.dart: -------------------------------------------------------------------------------- 1 | /// 2 | /// Copyright (C) 2019 Andrious Solutions Ltd. 3 | /// adaptation from Miguel Ruivo https://github.com/miguelpruivo 4 | /// 5 | /// Licensed under the Apache License, Version 2.0 (the "License"); 6 | /// you may not use this file except in compliance with the License. 7 | /// You may obtain a copy of the License at 8 | /// 9 | /// http://www.apache.org/licenses/LICENSE-2.0 10 | /// 11 | /// Unless required by applicable law or agreed to in writing, software 12 | /// distributed under the License is distributed on an "AS IS" BASIS, 13 | /// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | /// See the License for the specific language governing permissions and 15 | /// limitations under the License. 16 | /// 17 | /// Created 03 Mar 2020 18 | /// 19 | /// 20 | 21 | import 'dart:ui' show ImageFilter; 22 | 23 | import 'package:flutter/cupertino.dart' 24 | show 25 | Alignment, 26 | Border, 27 | BorderSide, 28 | BoxDecoration, 29 | BuildContext, 30 | Color, 31 | Column, 32 | Container, 33 | CrossAxisAlignment, 34 | CupertinoButton, 35 | CupertinoDatePicker, 36 | CupertinoDatePickerMode, 37 | CupertinoIcons, 38 | CupertinoTheme, 39 | EdgeInsets, 40 | Expanded, 41 | FontWeight, 42 | Icon, 43 | Key, 44 | MainAxisAlignment, 45 | Navigator, 46 | Row, 47 | SizedBox, 48 | Text, 49 | Widget, 50 | showCupertinoModalPopup; 51 | import 'package:flutter/material.dart' show Color, Colors; 52 | 53 | export 'dart:ui' show ImageFilter; 54 | 55 | export 'package:flutter/material.dart' show Color, Colors; 56 | 57 | /// Display the Cupertino Date Picker 58 | void showCupertinoDatePicker( 59 | BuildContext context, { 60 | Key? key, 61 | CupertinoDatePickerMode mode = CupertinoDatePickerMode.dateAndTime, 62 | required void Function(DateTime value) onDateTimeChanged, 63 | DateTime? initialDateTime, 64 | DateTime? minimumDate, 65 | DateTime? maximumDate, 66 | int minimumYear = 1, 67 | int? maximumYear, 68 | int minuteInterval = 1, 69 | bool use24hFormat = false, 70 | Color? backgroundColor, 71 | ImageFilter? filter, 72 | bool useRootNavigator = true, 73 | bool? semanticsDismissible, 74 | Widget? cancelText, 75 | Widget? doneText, 76 | bool useText = false, 77 | bool leftHanded = false, 78 | }) { 79 | // Default to right now. 80 | initialDateTime ??= DateTime.now(); 81 | 82 | // Retrieve the current 'theme' 83 | final theme = CupertinoTheme.of(context); 84 | 85 | // Assign the spinner's background colour. 86 | backgroundColor ??= theme.scaffoldBackgroundColor; 87 | 88 | if (!useText) { 89 | cancelText = const Icon(CupertinoIcons.clear_circled); 90 | } else { 91 | cancelText ??= Text( 92 | 'Cancel', 93 | style: theme.textTheme.actionTextStyle.copyWith( 94 | fontWeight: FontWeight.w600, 95 | color: Colors.red, 96 | ), 97 | ); 98 | } 99 | 100 | if (!useText) { 101 | doneText = const Icon(CupertinoIcons.check_mark_circled); 102 | } else { 103 | doneText ??= Text( 104 | 'Save', 105 | style: 106 | theme.textTheme.actionTextStyle.copyWith(fontWeight: FontWeight.w600), 107 | ); 108 | } 109 | 110 | final cancelButton = CupertinoButton( 111 | padding: const EdgeInsets.symmetric(horizontal: 15), 112 | onPressed: () { 113 | onDateTimeChanged(DateTime(0000)); 114 | Navigator.of(context).pop(); 115 | }, 116 | child: cancelText, 117 | ); 118 | 119 | final doneButton = CupertinoButton( 120 | padding: const EdgeInsets.symmetric(horizontal: 15), 121 | onPressed: () => Navigator.of(context).pop(), 122 | child: doneText, 123 | ); 124 | 125 | // 126 | showCupertinoModalPopup( 127 | context: context, 128 | builder: (context) => SizedBox( 129 | height: 240, 130 | child: Column( 131 | crossAxisAlignment: CrossAxisAlignment.stretch, 132 | children: [ 133 | Container( 134 | alignment: Alignment.centerRight, 135 | decoration: const BoxDecoration( 136 | color: Color.fromRGBO(249, 249, 247, 1), 137 | border: Border( 138 | bottom: BorderSide(width: 0.5, color: Colors.black38), 139 | ), 140 | ), 141 | child: Row( 142 | mainAxisAlignment: MainAxisAlignment.spaceBetween, 143 | children: [ 144 | if (leftHanded) doneButton else cancelButton, 145 | if (leftHanded) cancelButton else doneButton, 146 | ], 147 | ), 148 | ), 149 | Expanded( 150 | child: CupertinoDatePicker( 151 | key: key, 152 | mode: mode, 153 | onDateTimeChanged: (DateTime value) { 154 | if (onDateTimeChanged == null) { 155 | return; 156 | } 157 | if (mode == CupertinoDatePickerMode.time) { 158 | onDateTimeChanged( 159 | DateTime(0000, 01, 01, value.hour, value.minute)); 160 | } else { 161 | onDateTimeChanged(value); 162 | } 163 | }, 164 | initialDateTime: initialDateTime, 165 | minimumDate: minimumDate, 166 | maximumDate: maximumDate, 167 | minimumYear: minimumYear, 168 | maximumYear: maximumYear, 169 | minuteInterval: minuteInterval, 170 | use24hFormat: use24hFormat, 171 | backgroundColor: backgroundColor, 172 | )), 173 | ], 174 | ), 175 | ), 176 | filter: filter, 177 | useRootNavigator: useRootNavigator, 178 | semanticsDismissible: semanticsDismissible, 179 | ); 180 | } 181 | -------------------------------------------------------------------------------- /lib/src/view/uxutils/src/view/utils/preferred_orientation_mixin.dart: -------------------------------------------------------------------------------- 1 | /// 2 | /// Copyright (C) 2021 Andrious Solutions 3 | /// 4 | /// Licensed under the Apache License, Version 2.0 (the "License"); 5 | /// you may not use this file except in compliance with the License. 6 | /// You may obtain a copy of the License at 7 | /// 8 | /// http://www.apache.org/licenses/LICENSE-2.0 9 | /// 10 | /// Unless required by applicable law or agreed to in writing, software 11 | /// distributed under the License is distributed on an "AS IS" BASIS, 12 | /// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | /// See the License for the specific language governing permissions and 14 | /// limitations under the License. 15 | /// 16 | /// Created 20 Oct 2021 17 | /// 18 | /// 19 | 20 | import 'package:flutter/services.dart'; 21 | 22 | import 'package:mvc_application/view.dart'; 23 | 24 | /// Landscape-only State object. 25 | mixin SetOrientationLandscapeOnly on State { 26 | @override 27 | void initState() { 28 | super.initState(); 29 | SystemChrome.setPreferredOrientations([ 30 | DeviceOrientation.landscapeRight, 31 | DeviceOrientation.landscapeLeft, 32 | ]); 33 | } 34 | 35 | @override 36 | void dispose() { 37 | /// The empty list causes the application to defer to the system default. 38 | SystemChrome.setPreferredOrientations([]); 39 | super.dispose(); 40 | } 41 | } 42 | 43 | /// Portrait-only State object. 44 | mixin SetOrientationPortraitOnly on State { 45 | @override 46 | void initState() { 47 | super.initState(); 48 | SystemChrome.setPreferredOrientations([ 49 | DeviceOrientation.portraitUp, 50 | DeviceOrientation.portraitDown, 51 | ]); 52 | } 53 | 54 | @override 55 | void dispose() { 56 | /// The empty list causes the application to defer to the system default. 57 | SystemChrome.setPreferredOrientations([]); 58 | super.dispose(); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /lib/src/view/uxutils/src/view/variable_string.dart: -------------------------------------------------------------------------------- 1 | // ignore: avoid_classes_with_only_static_members 2 | /// 3 | /// Copyright (C) 2019 Andrious Solutions 4 | /// 5 | /// Licensed under the Apache License, Version 2.0 (the "License"); 6 | /// you may not use this file except in compliance with the License. 7 | /// You may obtain a copy of the License at 8 | /// 9 | /// http://www.apache.org/licenses/LICENSE-2.0 10 | /// 11 | /// Unless required by applicable law or agreed to in writing, software 12 | /// distributed under the License is distributed on an "AS IS" BASIS, 13 | /// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | /// See the License for the specific language governing permissions and 15 | /// limitations under the License. 16 | /// 17 | /// Created 14 Nov 2019 18 | /// 19 | /// 20 | 21 | class VarStr { 22 | static final VariableString _varStr = VariableString(); 23 | 24 | /// Set a String value to the Variable String object. 25 | static String set(String str) => _varStr.value = str; 26 | 27 | /// Return the Variable String Object's value. 28 | static String get get => _varStr.value; 29 | } 30 | 31 | /// Variable String Clas 32 | class VariableString { 33 | /// Variable String Constructor. 34 | VariableString() { 35 | regExp = RegExp("'(.*?)'"); 36 | } 37 | 38 | /// Reg Expression. 39 | late RegExp regExp; 40 | 41 | String _value = ''; 42 | 43 | set value(String? str) { 44 | _value = ''; 45 | if (str != null && str.isNotEmpty) { 46 | final match = regExp.firstMatch(str); 47 | if (match != null) { 48 | _value = match.group(0)!.replaceAll("'", ''); 49 | } 50 | } 51 | } 52 | 53 | /// Return the Variable String Object's value. 54 | String get value => _value; 55 | } 56 | -------------------------------------------------------------------------------- /lib/src/view/uxutils/ux.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Andrious Solutions Ltd. All rights reserved. 2 | // Use of this source code is governed by a Apache License, Version 2.0. 3 | // The main directory contains that LICENSE file. 4 | 5 | export 'package:mvc_application/src/view/uxutils/model.dart'; 6 | 7 | export 'package:mvc_application/src/view/uxutils/view.dart'; 8 | 9 | export 'package:mvc_application/src/view/uxutils/controller.dart'; 10 | -------------------------------------------------------------------------------- /lib/src/view/uxutils/view.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Andrious Solutions Ltd. All rights reserved. 2 | // Use of this source code is governed by a Apache License, Version 2.0. 3 | // The main directory contains that LICENSE file. 4 | 5 | export 'package:mvc_application/src/view/uxutils/src/view/simple_bottom_appbar.dart'; 6 | 7 | export 'package:mvc_application/src/view/uxutils/src/view/dialog_box.dart'; 8 | 9 | export 'package:mvc_application/src/view/uxutils/src/view/custom_scroll_physics.dart'; 10 | 11 | export 'package:mvc_application/src/view/uxutils/src/view/variable_string.dart'; 12 | 13 | export 'package:mvc_application/src/view/uxutils/src/view/common_widgets/custom_raised_button.dart'; 14 | 15 | export 'package:mvc_application/src/view/uxutils/src/view/nav_bottom_bar.dart'; 16 | 17 | export 'package:mvc_application/src/view/uxutils/src/view/show_cupertino_date_picker.dart'; 18 | 19 | export 'package:mvc_application/src/view/uxutils/src/view/tab_buttons.dart'; 20 | 21 | export 'package:mvc_application/src/view/uxutils/src/view/iso_spinner.dart'; 22 | -------------------------------------------------------------------------------- /lib/view.dart: -------------------------------------------------------------------------------- 1 | /// 2 | /// Copyright (C) 2018 Andrious Solutions 3 | /// 4 | /// Licensed under the Apache License, Version 2.0 (the "License"); 5 | /// you may not use this file except in compliance with the License. 6 | /// You may obtain a copy of the License at 7 | /// 8 | /// http://www.apache.org/licenses/LICENSE-2.0 9 | /// 10 | /// Unless required by applicable law or agreed to in writing, software 11 | /// distributed under the License is distributed on an "AS IS" BASIS, 12 | /// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | /// See the License for the specific language governing permissions and 14 | /// limitations under the License. 15 | /// 16 | /// Created 25 Dec 2018 17 | /// 18 | /// 19 | 20 | /// Material 21 | export 'package:flutter/material.dart' hide runApp; 22 | 23 | /// Cupertino 24 | export 'package:flutter/cupertino.dart' hide RefreshCallback, runApp; 25 | 26 | /// Supply the custom runApp function 27 | export 'package:mvc_application/src/conditional_export.dart' 28 | if (dart.library.html) 'package:mvc_application/src/view/platforms/run_webapp.dart' 29 | if (dart.library.io) 'package:mvc_application/src/view/platforms/run_app.dart' 30 | show runApp; 31 | 32 | /// Replace 'dart:io' for Web applications 33 | export 'package:universal_platform/universal_platform.dart'; 34 | 35 | /// Flutter Framework's Foundation 36 | export 'package:flutter/foundation.dart' show kIsWeb, mustCallSuper, protected; 37 | 38 | /// MVC 39 | export 'package:mvc_pattern/mvc_pattern.dart' 40 | show AppStatefulWidgetMVC, InheritedStateMixin, InheritedStateMVC, SetState; 41 | 42 | /// App 43 | export 'package:mvc_application/src/view/app.dart'; 44 | 45 | /// App StatefulWidget 46 | export 'package:mvc_application/src/view/app_statefulwidget.dart' 47 | hide ErrorWidgetBuilder; 48 | 49 | /// App State Object 50 | export 'package:mvc_application/src/view/app_state.dart'; 51 | 52 | /// Settings 53 | export 'package:mvc_application/src/view/utils/app_settings.dart'; 54 | 55 | /// Error Handling 56 | export 'package:mvc_application/src/view/utils/error_handler.dart' 57 | show AppErrorHandler, ReportErrorHandler; 58 | 59 | /// Screens 60 | export 'package:mvc_application/src/view/utils/loading_screen.dart'; 61 | 62 | /// Fields 63 | export 'package:mvc_application/src/view/utils/field_widgets.dart'; 64 | 65 | /// Localiztions 66 | export 'package:flutter_localizations/flutter_localizations.dart' 67 | show 68 | GlobalCupertinoLocalizations, 69 | GlobalMaterialLocalizations, 70 | GlobalWidgetsLocalizations; 71 | 72 | /// TimeZone 73 | export 'package:mvc_application/src/view/utils/timezone.dart'; 74 | 75 | /// InheritedWidget Widget 76 | export 'package:mvc_application/src/view/utils/inherited_state.dart' 77 | show InheritedStates, InheritedStateWidget; 78 | 79 | /// Menus 80 | export 'package:mvc_application/src/view/app_menu.dart' 81 | show AppMenu, AppPopupMenu, Menu; 82 | 83 | /// Extensions 84 | export 'package:mvc_application/src/view/extensions/_extensions_view.dart'; 85 | 86 | /// Router Navigation 87 | export 'package:mvc_application/src/view/app_navigator.dart'; 88 | 89 | /// UX Utils 90 | export 'package:mvc_application/src/view/uxutils/view.dart'; 91 | 92 | /// Preferences 93 | export 'package:prefs/prefs.dart' show Prefs; 94 | 95 | /// Translations 96 | export 'package:l10n_translator/l10n.dart'; 97 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: mvc_application 2 | description: Flutter Framework for Applications using the MVC Design Pattern 3 | homepage: https://www.andrioussolutions.com 4 | repository: https://github.com/AndriousSolutions/mvc_application 5 | 6 | version: 8.13.1 7 | 8 | environment: 9 | sdk: '>=2.17.1 <3.0.0' 10 | 11 | dependencies: 12 | # https://pub.dartlang.org/packages/connectivity_plus 13 | connectivity_plus: ^2.0.0 14 | 15 | # https://pub.dev/packages/device_info_plus 16 | device_info_plus: ^3.0.0 17 | 18 | flutter: 19 | sdk: flutter 20 | 21 | # https://pub.dev/packages/flutter_local_notifications 22 | flutter_local_notifications: ^9.0.0 23 | 24 | # To support for other languages other than US English localizations 25 | flutter_localizations: 26 | sdk: flutter 27 | 28 | # https://pub.dev/packages/flutter_material_color_picker/ 29 | flutter_material_color_picker: ^1.0.0 30 | 31 | # https://pub.dev/packages/flutter_native_timezone/ 32 | flutter_native_timezone: ^2.0.0 33 | 34 | # https://pub.dev/packages/i10n_translator/ 35 | l10n_translator: ^3.0.0 36 | # path: ../../packages/l10n_translator 37 | 38 | # https://pub.dartlang.org/packages/mvc_pattern 39 | mvc_pattern: ^8.0.0 40 | # path: ../../packages/mvc_pattern 41 | 42 | # https://pub.dev/packages/package_info_plus 43 | package_info_plus: ^1.0.0 44 | 45 | # https://pub.dev/packages/path_provider 46 | path_provider: ^2.0.0 47 | 48 | # https://pub.dartlang.org/packages/prefs 49 | prefs: ^3.0.0 50 | # path: ../../packages/prefs 51 | 52 | # https://pub.dev/packages/timezone 53 | timezone: ^0.8.0 54 | 55 | # https://pub.dev/packages/universal_html/ 56 | universal_html: ^2.0.0 57 | 58 | # https://pub.dev/packages/universal_platform 59 | universal_platform: ^1.0.0 60 | 61 | # https://pub.dartlang.org/packages/url_launcher 62 | # used in AppSettings.dart 63 | url_launcher: ^6.0.0 64 | url_launcher_web: ^2.0.0 65 | 66 | # https://pub.dev/packages/url_strategy 67 | url_strategy: ^0.2.0 68 | 69 | # https://pub.dartlang.org/packages/uuid 70 | uuid: ^3.0.0 71 | 72 | dev_dependencies: 73 | flutter_test: 74 | sdk: flutter 75 | 76 | # Supply the example app for testing. 77 | mvc_application_example: 78 | path: example 79 | 80 | # # Setting up Lint Rules 81 | # # Most of the recommended lints directly implement the guidelines set out in Effective Dart: 82 | # # https://dart.dev/guides/language/effective-dart 83 | # # https://pub.dev/packages/pedantic 84 | # pedantic: ^1.10.0-nullsafety.3 85 | 86 | # For information on the generic Dart part of this file, see the 87 | # following page: https://www.dartlang.org/tools/pub/pubspec 88 | 89 | # The following section is specific to Flutter. 90 | flutter: 91 | 92 | # The following line ensures that the Material Icons font is 93 | # included with your application, so that you can use the icons in 94 | # the material Icons class. 95 | uses-material-design: true 96 | 97 | # To add assets to your package, add an assets section, like this: 98 | # assets: 99 | # - images/a_dot_burr.jpeg 100 | # - images/a_dot_ham.jpeg 101 | # 102 | # For details regarding assets in packages, see 103 | # https://flutter.io/assets-and-images/#from-packages 104 | # 105 | # An image asset can refer to one or more resolution-specific "variants", see 106 | # https://flutter.io/assets-and-images/#resolution-aware. 107 | 108 | # To add custom fonts to your package, add a fonts section here, 109 | # in this "flutter" section. Each entry in this list should have a 110 | # "family" key with the font family name, and a "fonts" key with a 111 | # list giving the asset and other descriptors for the font. For 112 | # example: 113 | # fonts: 114 | # - family: Schyler 115 | # fonts: 116 | # - asset: fonts/Schyler-Regular.ttf 117 | # - asset: fonts/Schyler-Italic.ttf 118 | # style: italic 119 | # - family: Trajan Pro 120 | # fonts: 121 | # - asset: fonts/TrajanPro.ttf 122 | # - asset: fonts/TrajanPro_Bold.ttf 123 | # weight: 700 124 | # 125 | # For details regarding fonts in packages, see 126 | # https://flutter.io/custom-fonts/#from-packages 127 | 128 | 129 | ## http://127.0.0.1:60500/C:/Programs/Tools/Projects/Flutter/packages/mvc_application?authToken=Y0q218m717o%3D 130 | -------------------------------------------------------------------------------- /test/widget_test.dart: -------------------------------------------------------------------------------- 1 | import '../example/test/src/view.dart'; 2 | 3 | void main() { 4 | /// Define a test. The TestWidgets function also provides a WidgetTester 5 | /// to work with. The WidgetTester allows you to build and interact 6 | /// with widgets in the test environment. 7 | testWidgets('app_template testing', (WidgetTester tester) async { 8 | // 9 | await tester.pumpWidget(TemplateApp()); 10 | 11 | /// Flutter won’t automatically rebuild your widget in the test environment. 12 | /// Use pump() or pumpAndSettle() to ask Flutter to rebuild the widget. 13 | 14 | /// pumpAndSettle() waits for all animations to complete. 15 | await tester.pumpAndSettle(); 16 | 17 | final con = TemplateController(); 18 | 19 | // for (var interface = 1; interface <= 2; interface++) { 20 | // 21 | int cnt = 1; 22 | 23 | while (cnt <= 3) { 24 | switch (con.application) { 25 | case 'Counter': 26 | 27 | /// Counter app testing 28 | await counterTest(tester); 29 | break; 30 | case 'Word Pairs': 31 | 32 | /// Random Word Pairs app 33 | await wordsTest(tester); 34 | break; 35 | case 'Contacts': 36 | 37 | /// Contacts app 38 | await contactsTest(tester); 39 | break; 40 | } 41 | 42 | /// Switch the app programmatically. 43 | // con.changeApp(); 44 | /// Switch the app through the popupmenu 45 | await openApplicationMenu(tester); 46 | 47 | /// Wait for the transition in the Interface 48 | await tester.pumpAndSettle(); 49 | 50 | cnt++; 51 | } 52 | 53 | /// Open the Locale window 54 | await openLocaleMenu(tester); 55 | 56 | /// Open About menu 57 | await openAboutMenu(tester); 58 | 59 | /// Switch the Interface 60 | await openInterfaceMenu(tester); 61 | // } 62 | 63 | /// Unit testing does not involve integration or widget testing. 64 | 65 | /// WordPairs App Model Unit Testing 66 | await wordPairsModelTest(tester); 67 | 68 | /// Unit testing the App's controller object 69 | await testTemplateController(tester); 70 | 71 | reportTestErrors(); 72 | }); 73 | } 74 | --------------------------------------------------------------------------------