├── .gitignore ├── .metadata ├── LICENSE ├── README.md ├── analysis_options.yaml ├── android ├── app │ ├── build.gradle │ └── src │ │ ├── debug │ │ └── AndroidManifest.xml │ │ ├── main │ │ ├── AndroidManifest.xml │ │ ├── java │ │ │ └── com │ │ │ │ └── lcc │ │ │ │ └── notebulk │ │ │ │ └── MainActivity.java │ │ └── res │ │ │ ├── drawable-hdpi │ │ │ ├── ic_launcher_background.png │ │ │ └── ic_launcher_foreground.png │ │ │ ├── drawable-mdpi │ │ │ ├── ic_launcher_background.png │ │ │ └── ic_launcher_foreground.png │ │ │ ├── drawable-xhdpi │ │ │ ├── ic_launcher_background.png │ │ │ └── ic_launcher_foreground.png │ │ │ ├── drawable-xxhdpi │ │ │ ├── ic_launcher_background.png │ │ │ └── ic_launcher_foreground.png │ │ │ ├── drawable-xxxhdpi │ │ │ ├── ic_launcher_background.png │ │ │ └── ic_launcher_foreground.png │ │ │ ├── drawable │ │ │ └── launch_background.xml │ │ │ ├── mipmap-anydpi-v26 │ │ │ ├── ic_launcher.xml │ │ │ └── launcher_icon.xml │ │ │ ├── mipmap-hdpi │ │ │ ├── ic_launcher.png │ │ │ └── launcher_icon.png │ │ │ ├── mipmap-mdpi │ │ │ ├── ic_launcher.png │ │ │ └── launcher_icon.png │ │ │ ├── mipmap-xhdpi │ │ │ ├── ic_launcher.png │ │ │ └── launcher_icon.png │ │ │ ├── mipmap-xxhdpi │ │ │ ├── ic_launcher.png │ │ │ └── launcher_icon.png │ │ │ ├── mipmap-xxxhdpi │ │ │ ├── ic_launcher.png │ │ │ └── launcher_icon.png │ │ │ └── values │ │ │ ├── colors.xml │ │ │ └── styles.xml │ │ └── profile │ │ └── AndroidManifest.xml ├── build.gradle ├── gradle.properties ├── gradle │ └── wrapper │ │ └── gradle-wrapper.properties └── settings.gradle ├── assets ├── bg-adaptive.png ├── icon-adaptive.png ├── icon-android.png └── screenshots │ ├── actions.png │ ├── actions_2.png │ ├── actions_3.png │ ├── actions_4.png │ ├── archive.png │ ├── dark_mode.png │ ├── notes.png │ ├── reminders.png │ └── search.png ├── fonts ├── AppIcons.ttf ├── OFL.txt ├── Palanquin-Bold.ttf ├── Palanquin-Light.ttf ├── Palanquin-Medium.ttf ├── Palanquin-Regular.ttf ├── Palanquin-Thin.ttf ├── PalanquinDark-Bold.ttf └── licenses.txt ├── ios ├── Flutter │ ├── AppFrameworkInfo.plist │ ├── Debug.xcconfig │ └── Release.xcconfig ├── Runner.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ └── contents.xcworkspacedata │ └── xcshareddata │ │ └── xcschemes │ │ └── Runner.xcscheme ├── Runner.xcworkspace │ └── contents.xcworkspacedata └── Runner │ ├── AppDelegate.h │ ├── AppDelegate.m │ ├── Assets.xcassets │ ├── AppIcon.appiconset │ │ ├── Contents.json │ │ ├── Icon-App-1024x1024@1x.png │ │ ├── Icon-App-20x20@1x.png │ │ ├── Icon-App-20x20@2x.png │ │ ├── Icon-App-20x20@3x.png │ │ ├── Icon-App-29x29@1x.png │ │ ├── Icon-App-29x29@2x.png │ │ ├── Icon-App-29x29@3x.png │ │ ├── Icon-App-40x40@1x.png │ │ ├── Icon-App-40x40@2x.png │ │ ├── Icon-App-40x40@3x.png │ │ ├── Icon-App-60x60@2x.png │ │ ├── Icon-App-60x60@3x.png │ │ ├── Icon-App-76x76@1x.png │ │ ├── Icon-App-76x76@2x.png │ │ └── Icon-App-83.5x83.5@2x.png │ └── LaunchImage.imageset │ │ ├── Contents.json │ │ ├── LaunchImage.png │ │ ├── LaunchImage@2x.png │ │ ├── LaunchImage@3x.png │ │ └── README.md │ ├── Base.lproj │ ├── LaunchScreen.storyboard │ └── Main.storyboard │ ├── Info.plist │ └── main.m ├── lib ├── ecs │ ├── components.dart │ ├── ecs.dart │ ├── matchers.dart │ ├── systems.dart │ └── util.dart ├── features │ ├── features.dart │ ├── noteFormFeature.dart │ └── reminderFormFeature.dart ├── icons.dart ├── main.dart ├── mainApp.dart ├── pages │ ├── archivePage.dart │ ├── noteListPage.dart │ ├── pages.dart │ ├── reminderListPage.dart │ ├── searchPage.dart │ ├── settingsPage.dart │ └── splashScreenPage.dart ├── theme.dart ├── util.dart └── widgets │ ├── cards.dart │ └── util.dart ├── linux ├── Makefile └── notebulk.cc ├── pubspec.lock ├── pubspec.yaml └── test └── entitas_basic_test.dart /.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | 12 | # IntelliJ related 13 | *.iml 14 | *.ipr 15 | *.iws 16 | .idea/ 17 | 18 | # The .vscode folder contains launch configuration and tasks you configure in 19 | # VS Code which you may wish to be included in version control, so this line 20 | # is commented out by default. 21 | #.vscode/ 22 | 23 | # Flutter/Dart/Pub related 24 | **/doc/api/ 25 | .dart_tool/ 26 | .flutter-plugins 27 | .packages 28 | .pub-cache/ 29 | .pub/ 30 | /build/ 31 | 32 | # Android related 33 | **/android/**/gradle-wrapper.jar 34 | **/android/.gradle 35 | **/android/captures/ 36 | **/android/gradlew 37 | **/android/gradlew.bat 38 | **/android/local.properties 39 | **/android/**/GeneratedPluginRegistrant.java 40 | 41 | # iOS/XCode related 42 | **/ios/**/*.mode1v3 43 | **/ios/**/*.mode2v3 44 | **/ios/**/*.moved-aside 45 | **/ios/**/*.pbxuser 46 | **/ios/**/*.perspectivev3 47 | **/ios/**/*sync/ 48 | **/ios/**/.sconsign.dblite 49 | **/ios/**/.tags* 50 | **/ios/**/.vagrant/ 51 | **/ios/**/DerivedData/ 52 | **/ios/**/Icon? 53 | **/ios/**/Pods/ 54 | **/ios/**/.symlinks/ 55 | **/ios/**/profile 56 | **/ios/**/xcuserdata 57 | **/ios/.generated/ 58 | **/ios/Flutter/App.framework 59 | **/ios/Flutter/Flutter.framework 60 | **/ios/Flutter/Generated.xcconfig 61 | **/ios/Flutter/app.flx 62 | **/ios/Flutter/app.zip 63 | **/ios/Flutter/flutter_assets/ 64 | **/ios/ServiceDefinitions.json 65 | **/ios/Runner/GeneratedPluginRegistrant.* 66 | 67 | # Exceptions to above rules. 68 | !**/ios/**/default.mode1v3 69 | !**/ios/**/default.mode2v3 70 | !**/ios/**/default.pbxuser 71 | !**/ios/**/default.perspectivev3 72 | !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages 73 | -------------------------------------------------------------------------------- /.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled and should not be manually edited. 5 | 6 | version: 7 | revision: 919dcf53f368c0aa5c559728d9d89d241617913c 8 | channel: master 9 | 10 | project_type: app 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 lucasccustodio 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Notebulk 2 | 3 | ![alt icon](https://raw.githubusercontent.com/lucasccustodio/notebulk/master/assets/icon-android.png) 4 | 5 | Open source note taking application made with Flutter using Entitas framework for state management and Hive as database. Features: Context-aware status bar that can apply actions in batch to a group (hence the name ;)), database backup/restoration, search system with support for multiple terms (tags), reminders as Smart list that knows when one is late and custom theme with support for Dark mode. 6 | 7 | # Screenshots 8 | 9 | ![alt notes](/assets/screenshots/notes.png) 10 | 11 | ![alt reminders](/assets/screenshots/reminders.png) 12 | 13 | ![alt searching](/assets/screenshots/search.png) 14 | 15 | ![alt archive](/assets/screenshots/archive.png) 16 | 17 | ![alt note actions](/assets/screenshots/actions.png) 18 | 19 | ![alt reminder actions 1](/assets/screenshots/actions_2.png) 20 | 21 | ![alt reminder actions 2](/assets/screenshots/actions_3.png) 22 | 23 | ![alt archive actions](/assets/screenshots/actions_4.png) 24 | 25 | ![alt dark_mode](/assets/screenshots/dark_mode.png) 26 | -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2017 The Fuchsia Authors. All rights reserved. 2 | # Use of this source code is governed by a BSD-style license that can be 3 | # found in the LICENSE file. 4 | 5 | # Root analysis options shared among all Dart code in the respository. Based 6 | # on the Fuchsia standard analysis options, with some changes. 7 | linter: 8 | # Full list available at http://dart-lang.github.io/linter/lints/options/options.html. 9 | rules: 10 | - always_declare_return_types 11 | - always_put_control_body_on_new_line 12 | - always_put_required_named_parameters_first 13 | - always_require_non_null_named_parameters 14 | - annotate_overrides 15 | - avoid_as 16 | - avoid_bool_literals_in_conditional_expressions 17 | - avoid_catches_without_on_clauses 18 | - avoid_catching_errors 19 | - avoid_classes_with_only_static_members 20 | - avoid_empty_else 21 | # Not compatible with VS Code yet. 22 | # - avoid_field_initializers_in_const_classes 23 | - avoid_function_literals_in_foreach_calls 24 | - avoid_init_to_null 25 | - avoid_null_checks_in_equality_operators 26 | - avoid_positional_boolean_parameters 27 | - avoid_private_typedef_functions 28 | # TODO: Change relative imports for package imports 29 | # - avoid_relative_lib_imports 30 | # This puts an unnecessary burden on API clients. 31 | # - avoid_renaming_method_parameters 32 | - avoid_return_types_on_setters 33 | - avoid_returning_null 34 | - avoid_returning_this 35 | - avoid_single_cascade_in_expression_statements 36 | - avoid_setters_without_getters 37 | - avoid_slow_async_io 38 | - avoid_types_as_parameter_names 39 | - avoid_types_on_closure_parameters 40 | - avoid_unused_constructor_parameters 41 | - await_only_futures 42 | - camel_case_types 43 | - cancel_subscriptions 44 | - cascade_invocations 45 | - close_sinks 46 | - comment_references 47 | - constant_identifier_names 48 | - control_flow_in_finally 49 | - directives_ordering 50 | - empty_catches 51 | - empty_constructor_bodies 52 | - empty_statements 53 | - hash_and_equals 54 | - implementation_imports 55 | - invariant_booleans 56 | - iterable_contains_unrelated_type 57 | - join_return_with_assignment 58 | - library_names 59 | - library_prefixes 60 | - list_remove_unrelated_type 61 | - literal_only_boolean_expressions 62 | - no_adjacent_strings_in_list 63 | - no_duplicate_case_values 64 | - non_constant_identifier_names 65 | - omit_local_variable_types 66 | - one_member_abstracts 67 | - only_throw_errors 68 | - overridden_fields 69 | - package_api_docs 70 | - package_names 71 | - package_prefixed_library_names 72 | - parameter_assignments 73 | - prefer_adjacent_string_concatenation 74 | - prefer_asserts_in_initializer_lists 75 | - prefer_collection_literals 76 | - prefer_conditional_assignment 77 | # Disabled until bug is fixed 78 | # https://github.com/dart-lang/linter/issues/995 79 | # - prefer_const_constructors 80 | - prefer_const_constructors_in_immutables 81 | - prefer_const_declarations 82 | - prefer_const_literals_to_create_immutables 83 | - prefer_constructors_over_static_methods 84 | - prefer_contains 85 | - prefer_equal_for_default_values 86 | # Add this when 'short' is better defined. 87 | # - prefer_expression_function_bodies 88 | - prefer_final_fields 89 | - prefer_final_locals 90 | # Seems to have false positive with await for. 91 | # - prefer_foreach 92 | - prefer_function_declarations_over_variables 93 | - prefer_generic_function_type_aliases 94 | - prefer_initializing_formals 95 | - prefer_interpolation_to_compose_strings 96 | - prefer_is_empty 97 | - prefer_is_not_empty 98 | - prefer_iterable_whereType 99 | - prefer_single_quotes 100 | - prefer_typing_uninitialized_variables 101 | #- public_member_api_docs 102 | - recursive_getters 103 | - slash_for_doc_comments 104 | - sort_constructors_first 105 | - sort_unnamed_constructors_first 106 | - test_types_in_equals 107 | - throw_in_finally 108 | - type_annotate_public_apis 109 | - type_init_formals 110 | - unawaited_futures 111 | - unnecessary_brace_in_string_interps 112 | - unnecessary_getters_setters 113 | - unnecessary_lambdas 114 | - unnecessary_null_aware_assignments 115 | - unnecessary_null_in_if_null_operators 116 | - unnecessary_overrides 117 | - unnecessary_parenthesis 118 | - unnecessary_statements 119 | - unnecessary_this 120 | - unrelated_type_equality_checks 121 | - use_rethrow_when_possible 122 | - use_setters_to_change_properties 123 | - use_string_buffers 124 | - use_to_and_as_if_applicable 125 | - valid_regexps 126 | # Not compatible with VS Code yet. 127 | # - void_checks 128 | -------------------------------------------------------------------------------- /android/app/build.gradle: -------------------------------------------------------------------------------- 1 | def localProperties = new Properties() 2 | def localPropertiesFile = rootProject.file('local.properties') 3 | if (localPropertiesFile.exists()) { 4 | localPropertiesFile.withReader('UTF-8') { reader -> 5 | localProperties.load(reader) 6 | } 7 | } 8 | 9 | def flutterRoot = localProperties.getProperty('flutter.sdk') 10 | if (flutterRoot == null) { 11 | throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") 12 | } 13 | 14 | def flutterVersionCode = localProperties.getProperty('flutter.versionCode') 15 | if (flutterVersionCode == null) { 16 | flutterVersionCode = '1' 17 | } 18 | 19 | def flutterVersionName = localProperties.getProperty('flutter.versionName') 20 | if (flutterVersionName == null) { 21 | flutterVersionName = '1.0' 22 | } 23 | 24 | apply plugin: 'com.android.application' 25 | apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" 26 | 27 | def keystoreProperties = new Properties() 28 | def keystorePropertiesFile = rootProject.file('keystore.properties') 29 | if (keystorePropertiesFile.exists()) { 30 | keystoreProperties.load(new FileInputStream(keystorePropertiesFile)) 31 | } 32 | 33 | android { 34 | compileSdkVersion 29 35 | 36 | lintOptions { 37 | disable 'InvalidPackage' 38 | } 39 | 40 | defaultConfig { 41 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). 42 | applicationId "com.lcc.notebulk" 43 | minSdkVersion 21 44 | targetSdkVersion 29 45 | versionCode flutterVersionCode.toInteger() 46 | versionName flutterVersionName 47 | //testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 48 | } 49 | 50 | signingConfigs { 51 | release { 52 | if (keystorePropertiesFile.exists()) { 53 | keyAlias keystoreProperties['keyAlias'] 54 | keyPassword keystoreProperties['keyPassword'] 55 | storeFile file(keystoreProperties['storeFile']) 56 | storePassword keystoreProperties['storePassword'] 57 | } 58 | } 59 | } 60 | 61 | buildTypes { 62 | release { 63 | if (keystorePropertiesFile.exists()) { 64 | signingConfig signingConfigs.release 65 | minifyEnabled true 66 | useProguard true 67 | 68 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 69 | } else { 70 | signingConfig signingConfigs.debug 71 | } 72 | } 73 | } 74 | } 75 | 76 | flutter { 77 | source '../..' 78 | } 79 | 80 | dependencies { 81 | //testImplementation 'junit:junit:4.12' 82 | //androidTestImplementation 'androidx.test:runner:1.1.1' 83 | //androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1' 84 | } 85 | -------------------------------------------------------------------------------- /android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 9 | 10 | 11 | 14 | 15 | 19 | 26 | 30 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /android/app/src/main/java/com/lcc/notebulk/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.lcc.notebulk; 2 | 3 | import android.os.Bundle; 4 | import io.flutter.app.FlutterActivity; 5 | import io.flutter.plugins.GeneratedPluginRegistrant; 6 | 7 | public class MainActivity extends FlutterActivity { 8 | @Override 9 | protected void onCreate(Bundle savedInstanceState) { 10 | super.onCreate(savedInstanceState); 11 | GeneratedPluginRegistrant.registerWith(this); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-hdpi/ic_launcher_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasccustodio/notebulk/761f3bd54f46fcf28be4b58767726e1ef32df007/android/app/src/main/res/drawable-hdpi/ic_launcher_background.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-hdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasccustodio/notebulk/761f3bd54f46fcf28be4b58767726e1ef32df007/android/app/src/main/res/drawable-hdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-mdpi/ic_launcher_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasccustodio/notebulk/761f3bd54f46fcf28be4b58767726e1ef32df007/android/app/src/main/res/drawable-mdpi/ic_launcher_background.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-mdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasccustodio/notebulk/761f3bd54f46fcf28be4b58767726e1ef32df007/android/app/src/main/res/drawable-mdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xhdpi/ic_launcher_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasccustodio/notebulk/761f3bd54f46fcf28be4b58767726e1ef32df007/android/app/src/main/res/drawable-xhdpi/ic_launcher_background.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasccustodio/notebulk/761f3bd54f46fcf28be4b58767726e1ef32df007/android/app/src/main/res/drawable-xhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xxhdpi/ic_launcher_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasccustodio/notebulk/761f3bd54f46fcf28be4b58767726e1ef32df007/android/app/src/main/res/drawable-xxhdpi/ic_launcher_background.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasccustodio/notebulk/761f3bd54f46fcf28be4b58767726e1ef32df007/android/app/src/main/res/drawable-xxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xxxhdpi/ic_launcher_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasccustodio/notebulk/761f3bd54f46fcf28be4b58767726e1ef32df007/android/app/src/main/res/drawable-xxxhdpi/ic_launcher_background.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xxxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasccustodio/notebulk/761f3bd54f46fcf28be4b58767726e1ef32df007/android/app/src/main/res/drawable-xxxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-anydpi-v26/launcher_icon.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasccustodio/notebulk/761f3bd54f46fcf28be4b58767726e1ef32df007/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/launcher_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasccustodio/notebulk/761f3bd54f46fcf28be4b58767726e1ef32df007/android/app/src/main/res/mipmap-hdpi/launcher_icon.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasccustodio/notebulk/761f3bd54f46fcf28be4b58767726e1ef32df007/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/launcher_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasccustodio/notebulk/761f3bd54f46fcf28be4b58767726e1ef32df007/android/app/src/main/res/mipmap-mdpi/launcher_icon.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasccustodio/notebulk/761f3bd54f46fcf28be4b58767726e1ef32df007/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/launcher_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasccustodio/notebulk/761f3bd54f46fcf28be4b58767726e1ef32df007/android/app/src/main/res/mipmap-xhdpi/launcher_icon.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasccustodio/notebulk/761f3bd54f46fcf28be4b58767726e1ef32df007/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/launcher_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasccustodio/notebulk/761f3bd54f46fcf28be4b58767726e1ef32df007/android/app/src/main/res/mipmap-xxhdpi/launcher_icon.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasccustodio/notebulk/761f3bd54f46fcf28be4b58767726e1ef32df007/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/launcher_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasccustodio/notebulk/761f3bd54f46fcf28be4b58767726e1ef32df007/android/app/src/main/res/mipmap-xxxhdpi/launcher_icon.png -------------------------------------------------------------------------------- /android/app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FFC83660 4 | -------------------------------------------------------------------------------- /android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | -------------------------------------------------------------------------------- /android/app/src/profile/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | repositories { 3 | google() 4 | jcenter() 5 | } 6 | 7 | dependencies { 8 | classpath 'com.android.tools.build:gradle:3.5.0' 9 | } 10 | } 11 | 12 | allprojects { 13 | repositories { 14 | google() 15 | jcenter() 16 | } 17 | } 18 | 19 | rootProject.buildDir = '../build' 20 | subprojects { 21 | project.buildDir = "${rootProject.buildDir}/${project.name}" 22 | } 23 | subprojects { 24 | project.evaluationDependsOn(':app') 25 | } 26 | 27 | task clean(type: Delete) { 28 | delete rootProject.buildDir 29 | } 30 | -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx1536M 2 | org.gradle.daemon=false 3 | 4 | android.useAndroidX=true 5 | android.enableJetifier=true 6 | android.enableR8=true 7 | -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Fri Jun 23 08:50:38 CEST 2017 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl = https\://services.gradle.org/distributions/gradle-5.4.1-all.zip 7 | 8 | -------------------------------------------------------------------------------- /android/settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | 3 | def flutterProjectRoot = rootProject.projectDir.parentFile.toPath() 4 | 5 | def plugins = new Properties() 6 | def pluginsFile = new File(flutterProjectRoot.toFile(), '.flutter-plugins') 7 | if (pluginsFile.exists()) { 8 | pluginsFile.withReader('UTF-8') { reader -> plugins.load(reader) } 9 | } 10 | 11 | plugins.each { name, path -> 12 | def pluginDirectory = flutterProjectRoot.resolve(path).resolve('android').toFile() 13 | include ":$name" 14 | project(":$name").projectDir = pluginDirectory 15 | } 16 | -------------------------------------------------------------------------------- /assets/bg-adaptive.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasccustodio/notebulk/761f3bd54f46fcf28be4b58767726e1ef32df007/assets/bg-adaptive.png -------------------------------------------------------------------------------- /assets/icon-adaptive.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasccustodio/notebulk/761f3bd54f46fcf28be4b58767726e1ef32df007/assets/icon-adaptive.png -------------------------------------------------------------------------------- /assets/icon-android.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasccustodio/notebulk/761f3bd54f46fcf28be4b58767726e1ef32df007/assets/icon-android.png -------------------------------------------------------------------------------- /assets/screenshots/actions.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasccustodio/notebulk/761f3bd54f46fcf28be4b58767726e1ef32df007/assets/screenshots/actions.png -------------------------------------------------------------------------------- /assets/screenshots/actions_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasccustodio/notebulk/761f3bd54f46fcf28be4b58767726e1ef32df007/assets/screenshots/actions_2.png -------------------------------------------------------------------------------- /assets/screenshots/actions_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasccustodio/notebulk/761f3bd54f46fcf28be4b58767726e1ef32df007/assets/screenshots/actions_3.png -------------------------------------------------------------------------------- /assets/screenshots/actions_4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasccustodio/notebulk/761f3bd54f46fcf28be4b58767726e1ef32df007/assets/screenshots/actions_4.png -------------------------------------------------------------------------------- /assets/screenshots/archive.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasccustodio/notebulk/761f3bd54f46fcf28be4b58767726e1ef32df007/assets/screenshots/archive.png -------------------------------------------------------------------------------- /assets/screenshots/dark_mode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasccustodio/notebulk/761f3bd54f46fcf28be4b58767726e1ef32df007/assets/screenshots/dark_mode.png -------------------------------------------------------------------------------- /assets/screenshots/notes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasccustodio/notebulk/761f3bd54f46fcf28be4b58767726e1ef32df007/assets/screenshots/notes.png -------------------------------------------------------------------------------- /assets/screenshots/reminders.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasccustodio/notebulk/761f3bd54f46fcf28be4b58767726e1ef32df007/assets/screenshots/reminders.png -------------------------------------------------------------------------------- /assets/screenshots/search.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasccustodio/notebulk/761f3bd54f46fcf28be4b58767726e1ef32df007/assets/screenshots/search.png -------------------------------------------------------------------------------- /fonts/AppIcons.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasccustodio/notebulk/761f3bd54f46fcf28be4b58767726e1ef32df007/fonts/AppIcons.ttf -------------------------------------------------------------------------------- /fonts/OFL.txt: -------------------------------------------------------------------------------- 1 | Copyright 2014 Pria Ravichandran (pria.ravichandran@gmail.com) 2 | 3 | This Font Software is licensed under the SIL Open Font License, Version 1.1. 4 | This license is copied below, and is also available with a FAQ at: 5 | http://scripts.sil.org/OFL 6 | 7 | 8 | ----------------------------------------------------------- 9 | SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 10 | ----------------------------------------------------------- 11 | 12 | PREAMBLE 13 | The goals of the Open Font License (OFL) are to stimulate worldwide 14 | development of collaborative font projects, to support the font creation 15 | efforts of academic and linguistic communities, and to provide a free and 16 | open framework in which fonts may be shared and improved in partnership 17 | with others. 18 | 19 | The OFL allows the licensed fonts to be used, studied, modified and 20 | redistributed freely as long as they are not sold by themselves. The 21 | fonts, including any derivative works, can be bundled, embedded, 22 | redistributed and/or sold with any software provided that any reserved 23 | names are not used by derivative works. The fonts and derivatives, 24 | however, cannot be released under any other type of license. The 25 | requirement for fonts to remain under this license does not apply 26 | to any document created using the fonts or their derivatives. 27 | 28 | DEFINITIONS 29 | "Font Software" refers to the set of files released by the Copyright 30 | Holder(s) under this license and clearly marked as such. This may 31 | include source files, build scripts and documentation. 32 | 33 | "Reserved Font Name" refers to any names specified as such after the 34 | copyright statement(s). 35 | 36 | "Original Version" refers to the collection of Font Software components as 37 | distributed by the Copyright Holder(s). 38 | 39 | "Modified Version" refers to any derivative made by adding to, deleting, 40 | or substituting -- in part or in whole -- any of the components of the 41 | Original Version, by changing formats or by porting the Font Software to a 42 | new environment. 43 | 44 | "Author" refers to any designer, engineer, programmer, technical 45 | writer or other person who contributed to the Font Software. 46 | 47 | PERMISSION & CONDITIONS 48 | Permission is hereby granted, free of charge, to any person obtaining 49 | a copy of the Font Software, to use, study, copy, merge, embed, modify, 50 | redistribute, and sell modified and unmodified copies of the Font 51 | Software, subject to the following conditions: 52 | 53 | 1) Neither the Font Software nor any of its individual components, 54 | in Original or Modified Versions, may be sold by itself. 55 | 56 | 2) Original or Modified Versions of the Font Software may be bundled, 57 | redistributed and/or sold with any software, provided that each copy 58 | contains the above copyright notice and this license. These can be 59 | included either as stand-alone text files, human-readable headers or 60 | in the appropriate machine-readable metadata fields within text or 61 | binary files as long as those fields can be easily viewed by the user. 62 | 63 | 3) No Modified Version of the Font Software may use the Reserved Font 64 | Name(s) unless explicit written permission is granted by the corresponding 65 | Copyright Holder. This restriction only applies to the primary font name as 66 | presented to the users. 67 | 68 | 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font 69 | Software shall not be used to promote, endorse or advertise any 70 | Modified Version, except to acknowledge the contribution(s) of the 71 | Copyright Holder(s) and the Author(s) or with their explicit written 72 | permission. 73 | 74 | 5) The Font Software, modified or unmodified, in part or in whole, 75 | must be distributed entirely under this license, and must not be 76 | distributed under any other license. The requirement for fonts to 77 | remain under this license does not apply to any document created 78 | using the Font Software. 79 | 80 | TERMINATION 81 | This license becomes null and void if any of the above conditions are 82 | not met. 83 | 84 | DISCLAIMER 85 | THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 86 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF 87 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT 88 | OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE 89 | COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 90 | INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL 91 | DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 92 | FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM 93 | OTHER DEALINGS IN THE FONT SOFTWARE. 94 | -------------------------------------------------------------------------------- /fonts/Palanquin-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasccustodio/notebulk/761f3bd54f46fcf28be4b58767726e1ef32df007/fonts/Palanquin-Bold.ttf -------------------------------------------------------------------------------- /fonts/Palanquin-Light.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasccustodio/notebulk/761f3bd54f46fcf28be4b58767726e1ef32df007/fonts/Palanquin-Light.ttf -------------------------------------------------------------------------------- /fonts/Palanquin-Medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasccustodio/notebulk/761f3bd54f46fcf28be4b58767726e1ef32df007/fonts/Palanquin-Medium.ttf -------------------------------------------------------------------------------- /fonts/Palanquin-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasccustodio/notebulk/761f3bd54f46fcf28be4b58767726e1ef32df007/fonts/Palanquin-Regular.ttf -------------------------------------------------------------------------------- /fonts/Palanquin-Thin.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasccustodio/notebulk/761f3bd54f46fcf28be4b58767726e1ef32df007/fonts/Palanquin-Thin.ttf -------------------------------------------------------------------------------- /fonts/PalanquinDark-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasccustodio/notebulk/761f3bd54f46fcf28be4b58767726e1ef32df007/fonts/PalanquinDark-Bold.ttf -------------------------------------------------------------------------------- /ios/Flutter/AppFrameworkInfo.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | App 9 | CFBundleIdentifier 10 | io.flutter.flutter.app 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | App 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1.0 23 | MinimumOSVersion 24 | 8.0 25 | 26 | 27 | -------------------------------------------------------------------------------- /ios/Flutter/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Generated.xcconfig" 2 | -------------------------------------------------------------------------------- /ios/Flutter/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Generated.xcconfig" 2 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 39 | 40 | 41 | 42 | 43 | 44 | 54 | 56 | 62 | 63 | 64 | 65 | 66 | 67 | 73 | 75 | 81 | 82 | 83 | 84 | 86 | 87 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /ios/Runner/AppDelegate.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | 4 | @interface AppDelegate : FlutterAppDelegate 5 | 6 | @end 7 | -------------------------------------------------------------------------------- /ios/Runner/AppDelegate.m: -------------------------------------------------------------------------------- 1 | #include "AppDelegate.h" 2 | #include "GeneratedPluginRegistrant.h" 3 | 4 | @implementation AppDelegate 5 | 6 | - (BOOL)application:(UIApplication *)application 7 | didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { 8 | [GeneratedPluginRegistrant registerWithRegistry:self]; 9 | // Override point for customization after application launch. 10 | return [super application:application didFinishLaunchingWithOptions:launchOptions]; 11 | } 12 | 13 | @end 14 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "20x20", 5 | "idiom" : "iphone", 6 | "filename" : "Icon-App-20x20@2x.png", 7 | "scale" : "2x" 8 | }, 9 | { 10 | "size" : "20x20", 11 | "idiom" : "iphone", 12 | "filename" : "Icon-App-20x20@3x.png", 13 | "scale" : "3x" 14 | }, 15 | { 16 | "size" : "29x29", 17 | "idiom" : "iphone", 18 | "filename" : "Icon-App-29x29@1x.png", 19 | "scale" : "1x" 20 | }, 21 | { 22 | "size" : "29x29", 23 | "idiom" : "iphone", 24 | "filename" : "Icon-App-29x29@2x.png", 25 | "scale" : "2x" 26 | }, 27 | { 28 | "size" : "29x29", 29 | "idiom" : "iphone", 30 | "filename" : "Icon-App-29x29@3x.png", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "size" : "40x40", 35 | "idiom" : "iphone", 36 | "filename" : "Icon-App-40x40@2x.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "40x40", 41 | "idiom" : "iphone", 42 | "filename" : "Icon-App-40x40@3x.png", 43 | "scale" : "3x" 44 | }, 45 | { 46 | "size" : "60x60", 47 | "idiom" : "iphone", 48 | "filename" : "Icon-App-60x60@2x.png", 49 | "scale" : "2x" 50 | }, 51 | { 52 | "size" : "60x60", 53 | "idiom" : "iphone", 54 | "filename" : "Icon-App-60x60@3x.png", 55 | "scale" : "3x" 56 | }, 57 | { 58 | "size" : "20x20", 59 | "idiom" : "ipad", 60 | "filename" : "Icon-App-20x20@1x.png", 61 | "scale" : "1x" 62 | }, 63 | { 64 | "size" : "20x20", 65 | "idiom" : "ipad", 66 | "filename" : "Icon-App-20x20@2x.png", 67 | "scale" : "2x" 68 | }, 69 | { 70 | "size" : "29x29", 71 | "idiom" : "ipad", 72 | "filename" : "Icon-App-29x29@1x.png", 73 | "scale" : "1x" 74 | }, 75 | { 76 | "size" : "29x29", 77 | "idiom" : "ipad", 78 | "filename" : "Icon-App-29x29@2x.png", 79 | "scale" : "2x" 80 | }, 81 | { 82 | "size" : "40x40", 83 | "idiom" : "ipad", 84 | "filename" : "Icon-App-40x40@1x.png", 85 | "scale" : "1x" 86 | }, 87 | { 88 | "size" : "40x40", 89 | "idiom" : "ipad", 90 | "filename" : "Icon-App-40x40@2x.png", 91 | "scale" : "2x" 92 | }, 93 | { 94 | "size" : "76x76", 95 | "idiom" : "ipad", 96 | "filename" : "Icon-App-76x76@1x.png", 97 | "scale" : "1x" 98 | }, 99 | { 100 | "size" : "76x76", 101 | "idiom" : "ipad", 102 | "filename" : "Icon-App-76x76@2x.png", 103 | "scale" : "2x" 104 | }, 105 | { 106 | "size" : "83.5x83.5", 107 | "idiom" : "ipad", 108 | "filename" : "Icon-App-83.5x83.5@2x.png", 109 | "scale" : "2x" 110 | }, 111 | { 112 | "size" : "1024x1024", 113 | "idiom" : "ios-marketing", 114 | "filename" : "Icon-App-1024x1024@1x.png", 115 | "scale" : "1x" 116 | } 117 | ], 118 | "info" : { 119 | "version" : 1, 120 | "author" : "xcode" 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasccustodio/notebulk/761f3bd54f46fcf28be4b58767726e1ef32df007/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasccustodio/notebulk/761f3bd54f46fcf28be4b58767726e1ef32df007/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasccustodio/notebulk/761f3bd54f46fcf28be4b58767726e1ef32df007/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasccustodio/notebulk/761f3bd54f46fcf28be4b58767726e1ef32df007/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasccustodio/notebulk/761f3bd54f46fcf28be4b58767726e1ef32df007/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasccustodio/notebulk/761f3bd54f46fcf28be4b58767726e1ef32df007/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasccustodio/notebulk/761f3bd54f46fcf28be4b58767726e1ef32df007/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasccustodio/notebulk/761f3bd54f46fcf28be4b58767726e1ef32df007/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasccustodio/notebulk/761f3bd54f46fcf28be4b58767726e1ef32df007/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasccustodio/notebulk/761f3bd54f46fcf28be4b58767726e1ef32df007/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasccustodio/notebulk/761f3bd54f46fcf28be4b58767726e1ef32df007/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasccustodio/notebulk/761f3bd54f46fcf28be4b58767726e1ef32df007/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasccustodio/notebulk/761f3bd54f46fcf28be4b58767726e1ef32df007/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasccustodio/notebulk/761f3bd54f46fcf28be4b58767726e1ef32df007/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasccustodio/notebulk/761f3bd54f46fcf28be4b58767726e1ef32df007/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "LaunchImage.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "LaunchImage@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "LaunchImage@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasccustodio/notebulk/761f3bd54f46fcf28be4b58767726e1ef32df007/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasccustodio/notebulk/761f3bd54f46fcf28be4b58767726e1ef32df007/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasccustodio/notebulk/761f3bd54f46fcf28be4b58767726e1ef32df007/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md: -------------------------------------------------------------------------------- 1 | # Launch Screen Assets 2 | 3 | You can customize the launch screen with your own desired assets by replacing the image files in this directory. 4 | 5 | You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. -------------------------------------------------------------------------------- /ios/Runner/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /ios/Runner/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /ios/Runner/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | notebulk 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | $(FLUTTER_BUILD_NAME) 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | $(FLUTTER_BUILD_NUMBER) 23 | LSRequiresIPhoneOS 24 | 25 | UILaunchStoryboardName 26 | LaunchScreen 27 | UIMainStoryboardFile 28 | Main 29 | UISupportedInterfaceOrientations 30 | 31 | UIInterfaceOrientationPortrait 32 | UIInterfaceOrientationLandscapeLeft 33 | UIInterfaceOrientationLandscapeRight 34 | 35 | UISupportedInterfaceOrientations~ipad 36 | 37 | UIInterfaceOrientationPortrait 38 | UIInterfaceOrientationPortraitUpsideDown 39 | UIInterfaceOrientationLandscapeLeft 40 | UIInterfaceOrientationLandscapeRight 41 | 42 | UIViewControllerBasedStatusBarAppearance 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /ios/Runner/main.m: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | #import "AppDelegate.h" 4 | 5 | int main(int argc, char* argv[]) { 6 | @autoreleasepool { 7 | return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /lib/ecs/components.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:entitas_ff/entitas_ff.dart'; 4 | import 'package:flutter/material.dart'; 5 | import 'package:notebulk/theme.dart'; 6 | 7 | /* 8 | Naming conventions for unique components 9 | 10 | * = Ends with 11 | 12 | *Tag -> Identifier for unique components that represent some aspect of the app structure and helps with grouping relevant information. 13 | *Event -> Indentifier for event-like components, their modification triggers a system function and they rarely contain any data in them. 14 | */ 15 | 16 | // Note/reminder/tag components 17 | 18 | // Text data present on notes and reminders 19 | class Contents extends Component { 20 | final String value; 21 | 22 | Contents(this.value); 23 | } 24 | 25 | // Whether a note is currently archived 26 | class Archived extends Component {} 27 | 28 | // Holds information for the database use 29 | class DatabaseKey extends Component { 30 | final int value; 31 | 32 | DatabaseKey(this.value); 33 | } 34 | 35 | // Note's Todo list 36 | class Todo extends Component { 37 | final List value; 38 | 39 | Todo({this.value = const []}); 40 | } 41 | 42 | // Todo list item data 43 | class ListItem { 44 | bool isChecked; 45 | 46 | String label; 47 | ListItem(this.label, {this.isChecked = false}); 48 | ListItem.fromJson(Map map) 49 | : label = map['label'] ?? '', 50 | isChecked = map['isChecked'] ?? false; 51 | 52 | @override 53 | int get hashCode => label.hashCode ^ isChecked.hashCode; 54 | 55 | @override 56 | bool operator ==(dynamic other) => 57 | other is ListItem && other.hashCode == hashCode; 58 | 59 | Map toJson() => {'label': label, 'isChecked': isChecked}; 60 | 61 | @override 62 | String toString() => toJson().toString(); 63 | } 64 | 65 | // Note's image 66 | class Picture extends Component { 67 | final File value; 68 | 69 | Picture(String path) : value = File(path); 70 | } 71 | 72 | // Reminder's priority 73 | class Priority extends Component { 74 | final ReminderPriority value; 75 | 76 | Priority(this.value); 77 | } 78 | 79 | enum ReminderPriority { none, low, medium, high, maximum } 80 | 81 | // Tag's data 82 | class TagData extends Component { 83 | final String value; 84 | 85 | TagData(this.value); 86 | } 87 | 88 | // Note's tag list 89 | class Tags extends Component { 90 | final List value; 91 | 92 | Tags(this.value); 93 | } 94 | 95 | // Note and reminder creation/edit date 96 | class Timestamp extends Component { 97 | final DateTime value; 98 | 99 | Timestamp(String _timestamp) : value = DateTime.parse(_timestamp); 100 | } 101 | 102 | // Multipurpose components 103 | 104 | // Used by forms to enable the save button 105 | class Changed extends Component {} 106 | 107 | // Allows operations on multiple notes and reminders simultaneously 108 | class Selected extends Component {} 109 | 110 | // Marks a reminder as completed and toggles visiblity of status bar 111 | class Toggle extends Component {} 112 | 113 | // Tags for unique entities 114 | class AppSettingsTag extends UniqueComponent {} 115 | 116 | class StatusBarTag extends UniqueComponent {} 117 | 118 | class FeatureEntityTag extends UniqueComponent {} 119 | 120 | class SearchBarTag extends UniqueComponent {} 121 | 122 | class MainTickTag extends UniqueComponent {} 123 | 124 | // System components 125 | 126 | // Currently applied theme 127 | class AppTheme extends Component { 128 | final BaseTheme value; 129 | 130 | AppTheme(this.value); 131 | } 132 | 133 | // Whether the user has given permission for the database to load/persist the app's data 134 | class StoragePermission extends UniqueComponent { 135 | StoragePermission(); 136 | } 137 | 138 | // Used to track how much time has passed since the user stopped typing in the search bar 139 | class Tick extends Component { 140 | final int value; 141 | 142 | Tick(this.value); 143 | } 144 | 145 | // Makes the statusbar dismissible only be user interaction 146 | class WaitForUser extends Component {} 147 | 148 | // Marks a note or reminder to be persisted 149 | class PersistMe extends Component { 150 | final int key; 151 | 152 | PersistMe([this.key]); 153 | } 154 | 155 | // Current visible page on main screen 156 | class PageIndex extends UniqueComponent { 157 | final int value; 158 | 159 | final int oldValue; 160 | PageIndex(this.value, {this.oldValue}); 161 | } 162 | 163 | // Latest search string input by the user 164 | class SearchTerm extends Component { 165 | final String value; 166 | 167 | SearchTerm(this.value); 168 | } 169 | 170 | // Marks a note as being result of a search 171 | class SearchResult extends Component {} 172 | 173 | // Events 174 | 175 | // Archive notes 176 | class ArchiveNotesEvent extends UniqueComponent {} 177 | 178 | // Locale changed so updated localization 179 | class ChangeLocaleEvent extends UniqueComponent { 180 | final String localeCode; 181 | 182 | ChangeLocaleEvent(this.localeCode); 183 | } 184 | 185 | // Complete reminders 186 | class CompleteRemindersEvent extends UniqueComponent {} 187 | 188 | // Delete notes or reminders 189 | class DiscardSelectedEvent extends UniqueComponent {} 190 | 191 | // Export backup data to SD card 192 | class ExportBackupEvent extends UniqueComponent {} 193 | 194 | // Restore a previous backup 195 | class ImportBackupEvent extends UniqueComponent {} 196 | 197 | // Load user settings 198 | class LoadUserSettingsEvent extends UniqueComponent {} 199 | 200 | // Navigate somewhere 201 | class NavigationEvent extends UniqueComponent { 202 | final String routeName; 203 | 204 | final NavigationOps routeOp; 205 | 206 | NavigationEvent( 207 | {@required this.routeName, this.routeOp = NavigationOps.push}); 208 | 209 | NavigationEvent.pop() 210 | : routeName = '', 211 | routeOp = NavigationOps.pop; 212 | 213 | NavigationEvent.push(this.routeName) : routeOp = NavigationOps.push; 214 | 215 | NavigationEvent.replace(this.routeName) : routeOp = NavigationOps.replace; 216 | NavigationEvent.showDialog(this.routeName) 217 | : routeOp = NavigationOps.showDialog; 218 | } 219 | 220 | enum NavigationOps { push, pop, replace, showDialog } 221 | 222 | // Perform a search 223 | class PerformSearchEvent extends UniqueComponent {} 224 | 225 | // Persist current user settings 226 | class PersistUserSettingsEvent extends UniqueComponent {} 227 | 228 | // Refresh notes and reminders 229 | class RefreshDatabaseEvent extends UniqueComponent {} 230 | 231 | // Restore notes from archive 232 | class RestoreNotesEvent extends UniqueComponent {} 233 | 234 | // Set up the database 235 | class SetupDatabaseEvent extends UniqueComponent {} 236 | -------------------------------------------------------------------------------- /lib/ecs/ecs.dart: -------------------------------------------------------------------------------- 1 | export 'package:entitas_ff/entitas_ff.dart'; 2 | export 'components.dart'; 3 | export 'matchers.dart'; 4 | export 'systems.dart'; 5 | export 'util.dart'; -------------------------------------------------------------------------------- /lib/ecs/matchers.dart: -------------------------------------------------------------------------------- 1 | import 'package:entitas_ff/entitas_ff.dart'; 2 | import 'package:notebulk/ecs/components.dart'; 3 | import 'package:notebulk/util.dart'; 4 | 5 | /* 6 | Since a Entity is a mere container for components there must be a way to classify and group them, that's what a matcher is for. 7 | */ 8 | 9 | mixin Matchers { 10 | static EntityMatcher note = EntityMatcher( 11 | all: [Timestamp, Contents, DatabaseKey], 12 | none: [Archived, Priority], 13 | maybe: [Tags, Todo, Selected]); 14 | 15 | static EntityMatcher archived = EntityMatcher( 16 | none: [Priority], 17 | all: [Contents, Timestamp, DatabaseKey, Archived], 18 | maybe: [Selected, Tags, Todo]); 19 | 20 | static EntityMatcher reminder = EntityMatcher( 21 | all: [Timestamp, Contents, Priority, DatabaseKey], maybe: [Selected]); 22 | 23 | static EntityMatcher tag = EntityMatcher(all: [TagData], maybe: [Toggle]); 24 | 25 | static EntityMatcher searchResult = note.extend(all: [SearchResult]); 26 | 27 | static EntityMatcher settings = 28 | EntityMatcher(all: [AppTheme, Localization], maybe: [Toggle]); 29 | } 30 | -------------------------------------------------------------------------------- /lib/ecs/util.dart: -------------------------------------------------------------------------------- 1 | import 'package:entitas_ff/entitas_ff.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart'; 4 | import 'package:notebulk/ecs/components.dart'; 5 | 6 | void toggleSelected(Entity target) { 7 | if (target.hasT()) 8 | target.remove(); 9 | else 10 | target.set(Selected()); 11 | } 12 | 13 | /* 14 | Experimental untested stuff 15 | 16 | enum ListItemAnimation { 17 | fadeIn, 18 | fadeOut, 19 | enterLeft, 20 | enterRight, 21 | expand, 22 | shrink 23 | } 24 | 25 | class GroupObservingListBuilder extends StatefulWidget { 26 | const GroupObservingListBuilder( 27 | {@required this.matcher, 28 | @required this.itemBuilder, 29 | this.curve = Curves.bounceIn, 30 | this.reversed = true, 31 | this.duration = const Duration(milliseconds: 300), 32 | this.onAdded, 33 | this.onUpdated, 34 | Key key}) 35 | : super(key: key); 36 | 37 | final Curve curve; 38 | final EntityMatcher matcher; 39 | final bool reversed; 40 | final EntityWidgetBuilder itemBuilder; 41 | final ListItemAnimation onAdded; 42 | final ListItemAnimation onUpdated; 43 | final Duration duration; 44 | 45 | @override 46 | _GroupObservingListBuilderState createState() => 47 | _GroupObservingListBuilderState(); 48 | } 49 | 50 | class _GroupObservingListBuilderState extends State 51 | implements GroupObserver { 52 | EntityGroup _group; 53 | int _added, _updated; 54 | 55 | @override 56 | void didChangeDependencies() { 57 | super.didChangeDependencies(); 58 | _group?.removeObserver(this); 59 | final entityManager = EntityManagerProvider.of(context).entityManager; 60 | assert(entityManager != null); 61 | _group = entityManager.groupMatching(widget.matcher); 62 | _group?.addObserver(this); 63 | } 64 | 65 | @override 66 | void dispose() { 67 | _group?.removeObserver(this); 68 | super.dispose(); 69 | } 70 | 71 | @override 72 | Widget build(BuildContext context) { 73 | final groupLength = _group?.entities?.length ?? 0; 74 | 75 | return ListView.builder( 76 | itemCount: groupLength, 77 | itemBuilder: (context, index) => AnimatableEntityObservingWidget( 78 | key: ValueKey('ListItem${_group.entities[index].creationIndex}'), 79 | provider: (_) => _group.entities[index], 80 | startAnimating: true, 81 | duration: widget.duration, 82 | curve: widget.curve, 83 | tweens: {'_default': Tween(begin: 0.0, end: 1.0)}, 84 | onAnimationEnd: (end) { 85 | if (!end) { 86 | _added = -1; 87 | _updated = -1; 88 | _update(); 89 | } 90 | }, 91 | builder: (itemEntity, anim, context) { 92 | final delta = anim['_default'].value; 93 | 94 | if (index == _added) { 95 | return _AnimatedListItem( 96 | builder: widget.itemBuilder, 97 | delta: delta, 98 | animation: widget.onAdded, 99 | entity: itemEntity, 100 | ); 101 | } else if (index == _updated) { 102 | return _AnimatedListItem( 103 | builder: widget.itemBuilder, 104 | delta: delta, 105 | animation: widget.onUpdated, 106 | entity: itemEntity, 107 | ); 108 | } else 109 | return widget.itemBuilder(itemEntity, context); 110 | }, 111 | ), 112 | reverse: widget.reversed, 113 | shrinkWrap: true, 114 | ); 115 | } 116 | 117 | @override 118 | void added(EntityGroup group, ObservableEntity entity) { 119 | _added = _group.entities.indexOf(entity); 120 | _update(); 121 | } 122 | 123 | @override 124 | void removed(EntityGroup group, ObservableEntity entity) {} 125 | 126 | @override 127 | void updated(EntityGroup group, ObservableEntity entity) { 128 | _updated = _group.entities.indexOf(entity); 129 | _update(); 130 | } 131 | 132 | void _update() { 133 | if (mounted) { 134 | setState(() {}); 135 | } 136 | } 137 | } 138 | 139 | class GroupObservingGridBuilder extends StatefulWidget { 140 | const GroupObservingGridBuilder( 141 | {@required this.matcher, 142 | @required this.itemBuilder, 143 | @required this.delegate, 144 | this.reversed = true, 145 | this.duration = const Duration(milliseconds: 300), 146 | this.onAdded, 147 | this.onUpdated, 148 | Key key}) 149 | : super(key: key); 150 | 151 | final EntityMatcher matcher; 152 | final bool reversed; 153 | final SliverGridDelegate delegate; 154 | final EntityWidgetBuilder itemBuilder; 155 | final ListItemAnimation onAdded; 156 | final ListItemAnimation onUpdated; 157 | final Duration duration; 158 | 159 | @override 160 | _GroupObservingGridBuilderState createState() => 161 | _GroupObservingGridBuilderState(); 162 | } 163 | 164 | class _GroupObservingGridBuilderState extends State 165 | implements GroupObserver { 166 | EntityGroup _group; 167 | int _added, _updated; 168 | 169 | @override 170 | void didChangeDependencies() { 171 | super.didChangeDependencies(); 172 | _group?.removeObserver(this); 173 | final entityManager = EntityManagerProvider.of(context).entityManager; 174 | assert(entityManager != null); 175 | _group = entityManager.groupMatching(widget.matcher); 176 | _group?.addObserver(this); 177 | } 178 | 179 | @override 180 | void dispose() { 181 | _group?.removeObserver(this); 182 | super.dispose(); 183 | } 184 | 185 | @override 186 | Widget build(BuildContext context) { 187 | final groupLength = _group?.entities?.length ?? 0; 188 | 189 | return StaggeredGridView.countBuilder( 190 | itemCount: groupLength, 191 | shrinkWrap: true, 192 | physics: NeverScrollableScrollPhysics(), 193 | padding: EdgeInsets.symmetric(horizontal: 8, vertical: 8), 194 | crossAxisCount: 2, 195 | crossAxisSpacing: 4, 196 | mainAxisSpacing: 4, 197 | staggeredTileBuilder: (_) => StaggeredTile.fit(1), 198 | itemBuilder: (context, index) => AnimatableEntityObservingWidget( 199 | key: Key('GridItem${_group.entities[index].creationIndex}'), 200 | provider: (_) => _group.entities[index], 201 | startAnimating: true, 202 | duration: widget.duration, 203 | tweens: {'_default': Tween(begin: 0.0, end: 1.0)}, 204 | onAnimationEnd: (end) { 205 | if (!end) { 206 | _updated = -1; 207 | _added = -1; 208 | } 209 | }, 210 | builder: (itemEntity, anim, context) { 211 | final delta = anim['_default'].value; 212 | 213 | if (index == _added) { 214 | return _AnimatedListItem( 215 | builder: widget.itemBuilder, 216 | delta: delta, 217 | animation: widget.onAdded, 218 | entity: itemEntity, 219 | ); 220 | } else if (index == _updated) { 221 | return _AnimatedListItem( 222 | builder: widget.itemBuilder, 223 | delta: delta, 224 | animation: widget.onUpdated, 225 | entity: itemEntity, 226 | ); 227 | } else 228 | return widget.itemBuilder(itemEntity, context); 229 | }, 230 | ), 231 | ); 232 | } 233 | 234 | @override 235 | void added(EntityGroup group, ObservableEntity entity) { 236 | _added = group.entities.indexOf(entity); 237 | _update(); 238 | } 239 | 240 | @override 241 | void removed(EntityGroup group, ObservableEntity entity) { 242 | _update(); 243 | } 244 | 245 | @override 246 | void updated(EntityGroup group, ObservableEntity entity) { 247 | _updated = group.entities.indexOf(entity); 248 | _update(); 249 | } 250 | 251 | void _update() { 252 | if (mounted) { 253 | setState(() {}); 254 | } 255 | } 256 | } 257 | 258 | class _AnimatedListItem extends StatelessWidget { 259 | const _AnimatedListItem({ 260 | @required this.entity, 261 | @required this.animation, 262 | @required this.builder, 263 | @required this.delta, 264 | Key key, 265 | }) : super(key: key); 266 | 267 | final EntityWidgetBuilder builder; 268 | final ListItemAnimation animation; 269 | final double delta; 270 | final Entity entity; 271 | 272 | @override 273 | Widget build(BuildContext context) { 274 | var itemWidget = builder(entity, context); 275 | 276 | switch (animation) { 277 | case ListItemAnimation.fadeIn: 278 | itemWidget = Opacity( 279 | opacity: delta, 280 | child: itemWidget, 281 | ); 282 | break; 283 | case ListItemAnimation.fadeOut: 284 | itemWidget = Opacity( 285 | opacity: 1.0 - delta, 286 | child: itemWidget, 287 | ); 288 | break; 289 | case ListItemAnimation.expand: 290 | itemWidget = Align( 291 | alignment: Alignment.centerLeft, 292 | child: itemWidget, 293 | widthFactor: delta, 294 | heightFactor: delta, 295 | ); 296 | break; 297 | case ListItemAnimation.shrink: 298 | itemWidget = ClipRect( 299 | child: Align( 300 | child: itemWidget, 301 | widthFactor: 1.0 - delta, 302 | heightFactor: 1.0 - delta, 303 | ), 304 | ); 305 | break; 306 | case ListItemAnimation.enterLeft: 307 | itemWidget = FractionalTranslation( 308 | child: itemWidget, 309 | translation: Offset(-1.0 + delta, 0), 310 | transformHitTests: true, 311 | ); 312 | break; 313 | case ListItemAnimation.enterRight: 314 | itemWidget = FractionalTranslation( 315 | child: itemWidget, 316 | translation: Offset(1.0 + delta, 0), 317 | transformHitTests: true, 318 | ); 319 | break; 320 | default: 321 | } 322 | 323 | return itemWidget; 324 | } 325 | } 326 | */ 327 | -------------------------------------------------------------------------------- /lib/features/features.dart: -------------------------------------------------------------------------------- 1 | /* 2 | Features 3 | 4 | Features are a variant of EntityManagerProvider that create and manage their own EntityManager instance. 5 | They allow the app to run logic isolated from the main execution context which includes having control over what systems will be run or entities copied from the main EntityManager with the lifecycle callback onCreate, and any changes made can be later transmitted, if needed, to the main EntityManager using the lifecycle callback onDestroy. 6 | 7 | For example, when creating or editing a note there's no need to have any system being active on the background, so a isolated context is ideal not only for performance but predictability. 8 | */ 9 | 10 | export 'noteFormFeature.dart'; 11 | export 'reminderFormFeature.dart'; 12 | -------------------------------------------------------------------------------- /lib/features/reminderFormFeature.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:notebulk/ecs/ecs.dart'; 3 | import 'package:notebulk/theme.dart'; 4 | import 'package:notebulk/util.dart'; 5 | import 'package:tinycolor/tinycolor.dart'; 6 | import 'package:notebulk/widgets/util.dart'; 7 | 8 | // Reminder form, will get intially populated if the reminder already exists or start blank if creating a new one 9 | class ReminderFormFeature extends StatefulWidget { 10 | const ReminderFormFeature({Key key, this.title}) : super(key: key); 11 | 12 | final String title; 13 | 14 | @override 15 | _ReminderFormFeatureState createState() => _ReminderFormFeatureState(); 16 | } 17 | 18 | class _ReminderFormFeatureState extends State { 19 | GlobalKey key = GlobalKey(); 20 | FocusNode contentsNode; 21 | 22 | @override 23 | void initState() { 24 | super.initState(); 25 | contentsNode = FocusNode(); 26 | } 27 | 28 | @override 29 | Widget build(BuildContext context) { 30 | final entityManager = EntityManagerProvider.of(context).entityManager; 31 | final localization = 32 | entityManager.getUniqueEntity().get(); 33 | 34 | final appTheme = 35 | entityManager.getUniqueEntity().get().value; 36 | 37 | void closeFeature() { 38 | Navigator.of(context).pop(true); 39 | } 40 | 41 | void closeDialog() { 42 | Navigator.of(context).pop(false); 43 | } 44 | 45 | return WillPopScope( 46 | onWillPop: () async { 47 | if (!entityManager 48 | .getUniqueEntity() 49 | .hasT()) { 50 | return true; 51 | } else { 52 | return await showDialog( 53 | context: context, 54 | builder: (context) => ShouldLeavePromptDialog( 55 | message: localization.promptLeaveUnsaved, 56 | yesLabel: localization.yes, 57 | noLabel: localization.no, 58 | appTheme: appTheme, 59 | onNo: closeDialog, 60 | onYes: () { 61 | // Exit without saving 62 | entityManager 63 | .getUniqueEntity() 64 | .remove(); 65 | closeFeature(); 66 | }, 67 | )); 68 | } 69 | }, 70 | child: Scaffold( 71 | resizeToAvoidBottomInset: true, 72 | backgroundColor: appTheme.appBarColor, 73 | body: Form( 74 | key: key, 75 | child: SafeArea( 76 | top: false, 77 | bottom: true, 78 | maintainBottomViewPadding: false, 79 | child: Theme( 80 | data: ThemeData(accentColor: appTheme.primaryColor), 81 | child: CustomScrollView( 82 | slivers: [ 83 | SliverAppBar( 84 | floating: true, 85 | pinned: true, 86 | leading: IconButton( 87 | icon: Icon( 88 | AppIcons.left, 89 | color: appTheme.baseStyle.color, 90 | ), 91 | onPressed: () { 92 | Navigator.of(context).pop(); 93 | }, 94 | ), 95 | backgroundColor: appTheme.appBarColor, 96 | actions: [ 97 | EntityObservingWidget( 98 | provider: (em) => 99 | em.getUniqueEntity(), 100 | builder: (noteEntity, context) => FlatButton( 101 | child: Text(localization.saveChangesFeatureLabel, 102 | style: appTheme.appTitleTextStyle.copyWith( 103 | fontSize: 24, 104 | color: noteEntity.hasT() 105 | ? appTheme.primaryButtonColor 106 | : BaseTheme.lightGrey)), 107 | onPressed: noteEntity.hasT() 108 | ? () { 109 | if (key.currentState.validate()) 110 | key.currentState.save(); 111 | else { 112 | contentsNode 113 | ..unfocus() 114 | ..requestFocus(); 115 | return; 116 | } 117 | 118 | entityManager 119 | .getUniqueEntity() 120 | .set(PersistMe()); 121 | 122 | closeFeature(); 123 | } 124 | : null, 125 | ), 126 | ), 127 | ], 128 | title: EntityObservingWidget( 129 | provider: (em) => em.getUniqueEntity(), 130 | builder: (noteEntity, context) => Text( 131 | "${widget.title}${noteEntity.hasT() ? '*' : ''}", 132 | style: appTheme.appTitleTextStyle 133 | .copyWith(fontFamily: 'Palanquin'), 134 | ), 135 | ), 136 | ), 137 | SliverPadding( 138 | padding: const EdgeInsets.all(16), 139 | sliver: SliverToBoxAdapter( 140 | child: EntityObservingWidget( 141 | provider: (em) => 142 | em.getUniqueEntity(), 143 | builder: (noteEntity, context) => 144 | buildFormBody(noteEntity, localization, appTheme), 145 | ), 146 | )), 147 | ], 148 | ), 149 | ), 150 | ), 151 | ), 152 | ), 153 | ); 154 | } 155 | 156 | Widget buildFormBody( 157 | Entity noteEntity, Localization localization, BaseTheme appTheme) { 158 | final priority = noteEntity.get()?.value; 159 | final cardColor = priority != null 160 | ? appTheme.reminderPriorityColors[priority.index] 161 | : appTheme.appBarColor; 162 | final style = appTheme.formLabelStyle; 163 | 164 | return Padding( 165 | padding: const EdgeInsets.all(16), 166 | child: Column( 167 | mainAxisAlignment: MainAxisAlignment.start, 168 | crossAxisAlignment: CrossAxisAlignment.start, 169 | children: [ 170 | buildTimestamp(noteEntity, localization, appTheme), 171 | Padding( 172 | padding: const EdgeInsets.only(top: 16), 173 | child: Text( 174 | localization.featurePriorityLabel, 175 | style: style, 176 | ), 177 | ), 178 | buildColorPicker(noteEntity, appTheme), 179 | Padding( 180 | padding: const EdgeInsets.only(top: 16.0), 181 | child: Text( 182 | localization.featureContentsLabel, 183 | style: style, 184 | ), 185 | ), 186 | buildContentsField(noteEntity, localization, appTheme), 187 | ], 188 | ), 189 | ); 190 | } 191 | 192 | Widget buildColorPicker(Entity noteEntity, BaseTheme appTheme) { 193 | final reminderPriority = noteEntity.get()?.value; 194 | final size = MediaQuery.of(context).size.width * 0.15; 195 | 196 | return Row( 197 | crossAxisAlignment: CrossAxisAlignment.center, 198 | mainAxisAlignment: MainAxisAlignment.spaceEvenly, 199 | mainAxisSize: MainAxisSize.max, 200 | children: [ 201 | for (final priority in ReminderPriority.values) 202 | InkWell( 203 | onTap: () { 204 | noteEntity..set(Priority(priority))..set(Changed()); 205 | }, 206 | child: Container( 207 | decoration: BoxDecoration( 208 | color: appTheme.reminderPriorityColors[priority.index], 209 | border: priority == reminderPriority 210 | ? Border.all(color: appTheme.baseStyle.color, width: 2) 211 | : null, 212 | ), 213 | width: size, 214 | height: size, 215 | ), 216 | ) 217 | ], 218 | ); 219 | } 220 | 221 | Widget buildTimestamp( 222 | Entity noteEntity, Localization localization, BaseTheme appTheme) { 223 | final timestamp = noteEntity.get()?.value ?? DateTime.now(); 224 | final priority = noteEntity.get()?.value; 225 | final cardColor = priority != null 226 | ? appTheme.reminderPriorityColors[priority.index] 227 | : appTheme.appBarColor; 228 | final textColor = 229 | (TinyColor(cardColor).isDark() ? Colors.white : Colors.black) 230 | .withOpacity(0.7); 231 | 232 | return InkWell( 233 | onTap: () => showCalendarDialog(noteEntity, appTheme), 234 | child: Text( 235 | formatTimestamp(timestamp, localization), 236 | style: appTheme.titleTextStyle, 237 | ), 238 | ); 239 | } 240 | 241 | void showCalendarDialog(Entity noteEntity, BaseTheme appTheme) async { 242 | final today = DateTime.now(); 243 | final date = await showDatePicker( 244 | context: context, 245 | builder: (context, calendar) => Theme( 246 | data: ThemeData( 247 | brightness: appTheme.brightness, 248 | primaryColor: appTheme.primaryColor, 249 | backgroundColor: appTheme.primaryColor, 250 | fontFamily: 'Palanquin', 251 | dialogTheme: DialogTheme( 252 | titleTextStyle: appTheme.titleTextStyle, 253 | contentTextStyle: appTheme.subtitleTextStyle, 254 | ), 255 | accentColor: appTheme.accentColor), 256 | child: calendar, 257 | ), 258 | initialDate: noteEntity.get()?.value ?? today, 259 | firstDate: DateTime.utc(today.year), 260 | lastDate: DateTime.utc(today.year + 100)); 261 | 262 | if (date != null) { 263 | noteEntity..set(Timestamp(date.toIso8601String()))..set(Changed()); 264 | } 265 | } 266 | 267 | Widget buildContentsField( 268 | Entity noteEntity, Localization localization, BaseTheme appTheme) { 269 | final priority = noteEntity.get()?.value?.index ?? 0; 270 | final cardColor = appTheme.reminderPriorityColors[priority]; 271 | final fillColor = cardColor; 272 | 273 | return Theme( 274 | data: ThemeData( 275 | fontFamily: 'Palanquin'), // Fixes desktop not showing error text 276 | child: TextFormField( 277 | focusNode: contentsNode, 278 | autofocus: true, 279 | initialValue: noteEntity.get()?.value ?? '', 280 | textInputAction: TextInputAction.newline, 281 | keyboardType: TextInputType.text, 282 | maxLines: null, 283 | minLines: 5, 284 | decoration: InputDecoration( 285 | filled: true, 286 | fillColor: fillColor, 287 | contentPadding: const EdgeInsets.all(12), 288 | border: OutlineInputBorder( 289 | borderSide: BorderSide.none, 290 | borderRadius: BorderRadius.circular(6), 291 | gapPadding: 0), 292 | hintText: localization.featureReminderHint, 293 | ), 294 | onSaved: (contents) { 295 | // Form validated so save modifications 296 | noteEntity.set(Contents(contents)); 297 | }, 298 | onChanged: (_) { 299 | // Inform that the note contents changed and enable save button 300 | noteEntity.set(Changed()); 301 | }, 302 | validator: (contents) => 303 | contents.isEmpty ? localization.featureContentsError : null, 304 | style: appTheme.biggerBodyTextStyle.copyWith( 305 | color: TinyColor(cardColor).isDark() ? Colors.white : Colors.black), 306 | textAlign: TextAlign.left, 307 | ), 308 | ); 309 | } 310 | } 311 | -------------------------------------------------------------------------------- /lib/icons.dart: -------------------------------------------------------------------------------- 1 | /// Flutter icons AppIcons 2 | /// Copyright (C) 2019 by original authors @ fluttericon.com, fontello.com 3 | /// This font was generated by FlutterIcon.com, which is derived from Fontello. 4 | /// 5 | /// To use this font, place it in your fonts/ directory and include the 6 | /// following in your pubspec.yaml 7 | /// 8 | /// flutter: 9 | /// fonts: 10 | /// - family: AppIcons 11 | /// fonts: 12 | /// - asset: fonts/AppIcons.ttf 13 | /// 14 | /// 15 | /// * MFG Labs, Copyright (C) 2012 by Daniel Bruce 16 | /// Author: MFG Labs 17 | /// License: SIL (http://scripts.sil.org/OFL) 18 | /// Homepage: http://www.mfglabs.com/ 19 | /// 20 | import 'package:flutter/widgets.dart'; 21 | 22 | class AppIcons { 23 | AppIcons._(); 24 | 25 | static const _kFontFam = 'AppIcons'; 26 | 27 | static const IconData star = const IconData(0xe800, fontFamily: _kFontFam); 28 | static const IconData starEmpty = 29 | const IconData(0xe801, fontFamily: _kFontFam); 30 | static const IconData ok = const IconData(0xe802, fontFamily: _kFontFam); 31 | static const IconData cancel = const IconData(0xe803, fontFamily: _kFontFam); 32 | static const IconData plus = const IconData(0xe804, fontFamily: _kFontFam); 33 | static const IconData pencil = const IconData(0xe805, fontFamily: _kFontFam); 34 | static const IconData left = const IconData(0xe806, fontFamily: _kFontFam); 35 | static const IconData right = const IconData(0xe807, fontFamily: _kFontFam); 36 | static const IconData menu = const IconData(0xf008, fontFamily: _kFontFam); 37 | static const IconData grid = const IconData(0xf00a, fontFamily: _kFontFam); 38 | static const IconData gridEmpty = 39 | const IconData(0xf00b, fontFamily: _kFontFam); 40 | static const IconData download = 41 | const IconData(0xf02e, fontFamily: _kFontFam); 42 | static const IconData upload = const IconData(0xf02f, fontFamily: _kFontFam); 43 | static const IconData eye = const IconData(0xf082, fontFamily: _kFontFam); 44 | static const IconData trash = const IconData(0xf083, fontFamily: _kFontFam); 45 | static const IconData calendar = 46 | const IconData(0xf4c5, fontFamily: _kFontFam); 47 | static const IconData search = const IconData(0xf50d, fontFamily: _kFontFam); 48 | static const IconData lock = const IconData(0xf512, fontFamily: _kFontFam); 49 | static const IconData lockOpen = 50 | const IconData(0xf513, fontFamily: _kFontFam); 51 | } 52 | -------------------------------------------------------------------------------- /lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:entitas_ff/entitas_ff.dart'; 4 | import 'package:flutter/foundation.dart'; 5 | import 'package:flutter/material.dart'; 6 | import 'package:flutter/rendering.dart'; 7 | import 'package:flutter_localizations/flutter_localizations.dart'; 8 | import 'package:notebulk/mainApp.dart'; 9 | import 'package:notebulk/theme.dart'; 10 | import 'package:notebulk/util.dart'; 11 | import 'package:notebulk/widgets/util.dart'; 12 | 13 | import 'ecs/ecs.dart'; 14 | import 'features/features.dart'; 15 | import 'pages/pages.dart'; 16 | 17 | void main() async { 18 | // Necessary to debug on Linux/Fucshia 19 | if (Platform.isLinux || Platform.isFuchsia) 20 | debugDefaultTargetPlatformOverride = TargetPlatform.fuchsia; 21 | 22 | // Main EntityManager instance 23 | final mainEntityManager = EntityManager(); 24 | 25 | // Set up the app initial configuration 26 | mainEntityManager.setUnique(AppSettingsTag()) 27 | ..set(Localization.en()) 28 | ..set(AppTheme(BlankTheme())); 29 | mainEntityManager 30 | ..setUnique(StatusBarTag()) 31 | ..setUnique(PageIndex(0, oldValue: 0)) 32 | ..setUnique(SearchBarTag()); 33 | 34 | if (Platform.isFuchsia || Platform.isLinux || Platform.isWindows) 35 | mainEntityManager.setUnique( 36 | StoragePermission()); // No need for external storage read/write permission 37 | 38 | // The app's Navigator instance 39 | final navigatorKey = GlobalKey(); 40 | 41 | // Provides the EntityManager instance and hosts the RootSystem 42 | runApp(EntityManagerProvider( 43 | entityManager: mainEntityManager, 44 | child: EntityObservingWidget( 45 | provider: (em) => em.getUniqueEntity(), 46 | builder: (settings, context) { 47 | final appTheme = settings.get().value; 48 | 49 | return GradientBackground( 50 | appTheme: appTheme, 51 | child: MaterialApp( 52 | debugShowCheckedModeBanner: false, 53 | debugShowMaterialGrid: false, 54 | // Needed to have navigation events 55 | // TODO: Make RootSystem a NavigatorObserver and implement navigation on Entitas side instead 56 | navigatorKey: navigatorKey, 57 | title: 'Notebulk', 58 | initialRoute: Routes.splashScreen, 59 | supportedLocales: const [Locale('en', 'US'), Locale('pt', 'BR')], 60 | localeResolutionCallback: (locale, _) { 61 | if (locale != null) { 62 | mainEntityManager 63 | .setUnique(ChangeLocaleEvent(locale.languageCode)); 64 | } 65 | return locale; 66 | }, 67 | localizationsDelegates: [ 68 | GlobalMaterialLocalizations.delegate, 69 | GlobalWidgetsLocalizations.delegate, 70 | ], 71 | // Handle navigation here 72 | // TODO: Consider changing to Fluro, Voyager or alike 73 | onGenerateRoute: (settings) { 74 | Widget pageWidget; 75 | final localization = mainEntityManager 76 | .getUniqueEntity() 77 | .get(); 78 | final appTheme = mainEntityManager 79 | .getUniqueEntity() 80 | .get(); 81 | 82 | // See features.dart for an explaination on Features 83 | 84 | switch (settings.name) { 85 | case Routes.splashScreen: 86 | pageWidget = SplashScreenPage(); 87 | break; 88 | // 89 | case Routes.mainScreen: 90 | pageWidget = MainApp( 91 | entityManager: mainEntityManager, 92 | ); 93 | break; 94 | case Routes.createNote: 95 | pageWidget = EntityManagerProvider.feature( 96 | child: NoteFormFeature( 97 | title: localization.createNoteFeatureTitle, 98 | ), 99 | system: FeatureSystem( 100 | rootEntityManager: mainEntityManager, 101 | onCreate: (em, root) { 102 | // Will hold the note data temporarily 103 | em.setUnique(FeatureEntityTag()); 104 | // Copy the settings to match locale and theming 105 | em.setUnique(AppSettingsTag()) 106 | ..set(localization) 107 | ..set(appTheme); 108 | }, 109 | onDestroy: (em, root) { 110 | final note = em.getUniqueEntity(); 111 | 112 | // User didn't exit without saving, so persist note 113 | if (note.hasT()) 114 | root.createEntity() 115 | ..set(note.get()) 116 | ..set(note.get()) 117 | ..set(note.get()) 118 | ..set(note.get()) 119 | ..set(PersistMe()); 120 | }, 121 | ), 122 | ); 123 | break; 124 | case Routes.editNote: 125 | pageWidget = EntityManagerProvider.feature( 126 | child: NoteFormFeature( 127 | title: localization.editNoteFeatureTitle, 128 | ), 129 | system: FeatureSystem( 130 | rootEntityManager: mainEntityManager, 131 | onCreate: (em, root) { 132 | final editNote = 133 | root.getUniqueEntity(); 134 | 135 | // Copy theme and locale 136 | em.setUnique(AppSettingsTag()) 137 | ..set(localization) 138 | ..set(appTheme); 139 | 140 | // Populate the temporary note entity with current data 141 | em.setUnique(FeatureEntityTag()) 142 | ..set(editNote.get()) 143 | ..set(editNote.get()) 144 | ..set(editNote.get()) 145 | ..set(editNote.get()) 146 | ..set(editNote.get()); 147 | 148 | // Failsafe to avoid conflicts 149 | root.removeUnique(); 150 | }, 151 | onDestroy: (em, root) { 152 | final note = em.getUniqueEntity(); 153 | 154 | // User didn't exit without saving, so persist changes 155 | if (note.hasT()) 156 | root.createEntity() 157 | ..set(note.get()) 158 | ..set(note.get()) 159 | ..set(note.get()) 160 | ..set(note.get()) 161 | ..set(PersistMe(note.get().value)); 162 | }, 163 | ), 164 | ); 165 | break; 166 | case Routes.createReminder: 167 | pageWidget = EntityManagerProvider.feature( 168 | system: FeatureSystem( 169 | rootEntityManager: mainEntityManager, 170 | onCreate: (em, root) { 171 | // Temporary reminder with placeholder values 172 | em.setUnique(FeatureEntityTag()) 173 | ..set(Priority(ReminderPriority.low)) 174 | ..set(Timestamp(DateTime.now().toIso8601String())); 175 | // Copy to match theming and locale 176 | em.setUnique(AppSettingsTag()) 177 | ..set(localization) 178 | ..set(appTheme); 179 | }, 180 | onDestroy: (em, root) { 181 | final reminder = 182 | em.getUniqueEntity(); 183 | 184 | // User didn't exit without saving, so persist reminder 185 | if (reminder.hasT()) 186 | root.createEntity() 187 | ..set(reminder.get()) 188 | ..set(reminder.get()) 189 | ..set(reminder.get()) 190 | ..set(PersistMe()); 191 | }), 192 | child: ReminderFormFeature( 193 | title: localization.createEventFeatureTitle, 194 | ), 195 | ); 196 | break; 197 | case Routes.editReminder: 198 | pageWidget = EntityManagerProvider.feature( 199 | child: ReminderFormFeature( 200 | title: localization.editEventFeatureTitle, 201 | ), 202 | system: FeatureSystem( 203 | rootEntityManager: mainEntityManager, 204 | onCreate: (em, root) { 205 | final editReminder = 206 | root.getUniqueEntity(); 207 | 208 | em.setUnique(AppSettingsTag()) 209 | ..set(localization) 210 | ..set(appTheme); 211 | 212 | // Populate with current data 213 | em.setUnique(FeatureEntityTag()) 214 | ..set(editReminder.get()) 215 | ..set(editReminder.get()) 216 | ..set(editReminder.get()) 217 | ..set(editReminder.get()); 218 | 219 | // Failsafe to avoid conflicts 220 | root.removeUnique(); 221 | }, 222 | onDestroy: (em, root) { 223 | final reminder = em.getUniqueEntity(); 224 | 225 | // User didn't exit without saving, so persist changes 226 | if (reminder.hasT()) 227 | root.createEntity() 228 | ..set(reminder.get()) 229 | ..set(reminder.get()) 230 | ..set(reminder.get()) 231 | ..set(PersistMe(reminder.get().value)); 232 | }, 233 | ), 234 | ); 235 | break; 236 | default: 237 | pageWidget = Container(); 238 | } 239 | return MaterialPageRoute( 240 | builder: (_) => pageWidget, fullscreenDialog: true); 241 | }, 242 | // Basic theming 243 | theme: ThemeData( 244 | brightness: appTheme.brightness, 245 | canvasColor: Colors.transparent, 246 | splashColor: appTheme.accentColor, 247 | scaffoldBackgroundColor: Colors.transparent, 248 | textSelectionHandleColor: appTheme.primaryButtonColor, 249 | textSelectionColor: appTheme.primaryButtonColor, 250 | fontFamily: 'Palanquin', 251 | typography: Typography( 252 | englishLike: Typography.englishLike2018, 253 | dense: Typography.dense2018, 254 | tall: Typography.tall2018, 255 | )), 256 | ), 257 | ); 258 | }, 259 | ), 260 | // Manages all the app systems lifecycle and execution 261 | system: RootSystem(entityManager: mainEntityManager, systems: [ 262 | DatabaseSystem(), 263 | UserSettingsSystem(), 264 | TickSystem(), 265 | BackupSystem(), 266 | PersistanceSystem(), 267 | DiscardSystem(), 268 | ReminderOperationsSystem(), 269 | NoteOperationsSystem(), 270 | NavigationSystem(navigatorKey), 271 | StatusBarSystem(), 272 | InBetweenNavigationSystem(), 273 | SearchSystem() 274 | ]), 275 | )); 276 | } 277 | -------------------------------------------------------------------------------- /lib/mainApp.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:entitas_ff/entitas_ff.dart'; 4 | import 'package:flutter/gestures.dart'; 5 | import 'package:flutter/material.dart' hide TabBar; 6 | import 'package:notebulk/ecs/components.dart'; 7 | import 'package:notebulk/pages/archivePage.dart'; 8 | import 'package:notebulk/pages/noteListPage.dart'; 9 | import 'package:notebulk/pages/reminderListPage.dart'; 10 | import 'package:notebulk/pages/searchPage.dart'; 11 | import 'package:notebulk/pages/settingsPage.dart'; 12 | import 'package:notebulk/theme.dart'; 13 | import 'package:notebulk/util.dart'; 14 | import 'package:notebulk/widgets/util.dart'; 15 | 16 | class MainApp extends StatefulWidget { 17 | const MainApp({Key key, this.entityManager}) : super(key: key); 18 | 19 | final EntityManager entityManager; 20 | 21 | @override 22 | _MainAppState createState() => _MainAppState(); 23 | } 24 | 25 | class _MainAppState extends State { 26 | final Curve curve = Curves.easeIn; 27 | 28 | final Duration duration = const Duration(milliseconds: 200); 29 | PageController pageController; 30 | GlobalKey scaffoldKey; 31 | 32 | @override 33 | void didChangeDependencies() { 34 | super.didChangeDependencies(); 35 | pageController = PageController( 36 | keepPage: true, 37 | initialPage: widget.entityManager.getUnique().value); 38 | } 39 | 40 | @override 41 | void initState() { 42 | super.initState(); 43 | scaffoldKey = GlobalKey(); 44 | } 45 | 46 | @override 47 | Widget build(BuildContext context) { 48 | final localization = widget.entityManager 49 | .getUniqueEntity() 50 | .get(); 51 | final appTheme = widget.entityManager 52 | .getUniqueEntity() 53 | .get() 54 | .value; 55 | 56 | return WillPopScope( 57 | onWillPop: () async { 58 | return showDialog( 59 | context: context, 60 | builder: (context) => ShouldLeavePromptDialog( 61 | appTheme: appTheme, 62 | noLabel: localization.no, 63 | yesLabel: localization.yes, 64 | message: localization.promptLeaveApp, 65 | onYes: () { 66 | Navigator.of(context).pop(true); 67 | }, 68 | onNo: () { 69 | Navigator.of(context).pop(false); 70 | })); 71 | }, 72 | child: EntityObservingWidget( 73 | provider: (em) => em.getUniqueEntity(), 74 | blacklist: const [Localization], 75 | builder: (_, __) { 76 | final appTheme = widget.entityManager 77 | .getUniqueEntity() 78 | .get() 79 | .value; 80 | 81 | final isLandspace = 82 | MediaQuery.of(context).orientation == Orientation.landscape && 83 | Platform.isAndroid; 84 | 85 | return GestureDetector( 86 | behavior: HitTestBehavior.translucent, 87 | onTap: () { 88 | final selected = 89 | widget.entityManager.group(any: [Selected]).entities; 90 | for (final e in selected) e.remove(); 91 | }, 92 | child: Scaffold( 93 | key: scaffoldKey, 94 | resizeToAvoidBottomInset: false, 95 | appBar: isLandspace 96 | ? null 97 | : AppBar( 98 | elevation: 4, 99 | brightness: appTheme.brightness, 100 | backgroundColor: appTheme.appBarColor, 101 | leading: Platform.isAndroid || Platform.isIOS 102 | ? IconButton( 103 | icon: Icon( 104 | AppIcons.menu, 105 | color: appTheme.tertiaryButtonColor, 106 | ), 107 | onPressed: () { 108 | scaffoldKey.currentState.openDrawer(); 109 | }, 110 | ) 111 | : SizedBox(), 112 | automaticallyImplyLeading: false, 113 | bottom: PreferredSize( 114 | child: buildTabBar(appTheme, localization), 115 | preferredSize: Size(MediaQuery.of(context).size.width, 116 | kTextTabBarHeight), 117 | )), 118 | drawer: !isLandspace && (Platform.isAndroid || Platform.isIOS) 119 | ? buildDrawer(appTheme) 120 | : null, 121 | drawerDragStartBehavior: DragStartBehavior.down, 122 | drawerScrimColor: Colors.black54, 123 | body: buildScaffoldBody(appTheme)), 124 | ); 125 | }, 126 | ), 127 | ); 128 | } 129 | 130 | Drawer buildDrawer(BaseTheme appTheme) { 131 | final localization = widget.entityManager 132 | .getUniqueEntity() 133 | .get(); 134 | 135 | return Drawer( 136 | key: ValueKey('NotebulkDrawer'), 137 | child: Container( 138 | decoration: BoxDecoration(gradient: appTheme.backgroundGradient), 139 | child: ListView( 140 | shrinkWrap: true, 141 | children: [ 142 | Container( 143 | height: MediaQuery.of(context).size.height * 0.25, 144 | width: MediaQuery.of(context).size.width, 145 | alignment: Alignment.center, 146 | child: Text( 147 | localization.settingsPageTitle, 148 | style: appTheme.appTitleTextStyle, 149 | ), 150 | ), 151 | SettingsPage( 152 | entityManager: widget.entityManager, 153 | ), 154 | /* Divider(), 155 | ListTile( 156 | title: Text( 157 | 'TAGS', 158 | style: appTheme.titleTextStyle, 159 | ), 160 | ), 161 | FlatButton.icon( 162 | materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, 163 | icon: Icon( 164 | AppIcons.plus, 165 | color: appTheme.primaryButtonColor, 166 | ), 167 | label: Text( 168 | 'Criar tag', 169 | style: appTheme.actionableLabelStyle 170 | .copyWith(color: appTheme.primaryButtonColor), 171 | ), 172 | onPressed: () { 173 | widget.entityManager 174 | .setUnique(NavigationEvent.push(Routes.manageTags)); 175 | }, 176 | ), 177 | GroupObservingWidget( 178 | matcher: Matchers.tag, 179 | builder: (group, context) { 180 | final tags = group.entities.toList() 181 | ..sort((s1, s2) { 182 | final tag1 = s1.get().value; 183 | final tag2 = s1.get().value; 184 | 185 | return tag2.compareTo(tag1); 186 | }); 187 | 188 | return ListView.builder( 189 | shrinkWrap: true, 190 | physics: NeverScrollableScrollPhysics(), 191 | itemCount: tags.length, 192 | itemBuilder: (context, index) => ListTile( 193 | title: Row( 194 | mainAxisSize: MainAxisSize.max, 195 | crossAxisAlignment: CrossAxisAlignment.start, 196 | children: [ 197 | Icon( 198 | Icons.label, 199 | color: appTheme.subtitleTextStyle.color, 200 | ), 201 | SizedBox( 202 | width: 8, 203 | ), 204 | Text( 205 | tags[index].get().value, 206 | style: appTheme.subtitleTextStyle, 207 | ), 208 | ], 209 | ), 210 | ), 211 | ); 212 | }, 213 | ) */ 214 | ], 215 | ), 216 | ), 217 | ); 218 | } 219 | 220 | Widget buildScaffoldBody(BaseTheme appTheme) { 221 | final localization = widget.entityManager 222 | .getUniqueEntity() 223 | .get(); 224 | 225 | return Stack( 226 | children: [ 227 | SafeArea( 228 | maintainBottomViewPadding: false, 229 | bottom: true, 230 | child: Theme( 231 | data: ThemeData(accentColor: appTheme.primaryColor), 232 | child: Platform.isAndroid || Platform.isIOS 233 | ? PageView( 234 | dragStartBehavior: DragStartBehavior.down, 235 | controller: pageController, 236 | onPageChanged: (page) => widget.entityManager.setUnique( 237 | PageIndex(page, 238 | oldValue: widget.entityManager 239 | .getUnique() 240 | .value)), 241 | children: [ 242 | NoteListPage( 243 | entityManager: widget.entityManager, 244 | key: ValueKey('NotesPage'), 245 | ), 246 | ReminderListPage( 247 | entityManager: widget.entityManager, 248 | key: ValueKey('EventsPage'), 249 | ), 250 | SearchPage( 251 | entityManager: widget.entityManager, 252 | key: ValueKey('SearchPage'), 253 | ), 254 | ArchivePage( 255 | entityManager: widget.entityManager, 256 | key: ValueKey('ArchivePage'), 257 | ) 258 | ], 259 | ) 260 | : EntityObservingWidget( 261 | provider: (em) => em.getUniqueEntity(), 262 | builder: (e, __) => [ 263 | NoteListPage( 264 | entityManager: widget.entityManager, 265 | key: ValueKey('NotesPage'), 266 | ), 267 | ReminderListPage( 268 | entityManager: widget.entityManager, 269 | key: ValueKey('EventsPage'), 270 | ), 271 | SearchPage( 272 | entityManager: widget.entityManager, 273 | key: ValueKey('SearchPage'), 274 | ), 275 | ArchivePage( 276 | entityManager: widget.entityManager, 277 | key: ValueKey('ArchivePage'), 278 | ), 279 | SettingsPage( 280 | entityManager: widget.entityManager, 281 | key: ValueKey('SettingsPage'), 282 | ) 283 | ][e.get().value], 284 | ), 285 | ), 286 | ), 287 | Positioned( 288 | bottom: 0, 289 | child: StatusBar( 290 | key: ValueKey('StatusBar'), 291 | actions: (index) => [ 292 | if (index == 0) 293 | FlatButton( 294 | child: Text( 295 | localization.archiveActionLabel, 296 | style: appTheme.actionableLabelStyle, 297 | ), 298 | onPressed: () { 299 | widget.entityManager.setUnique(ArchiveNotesEvent()); 300 | }, 301 | ) 302 | else if (index == 1 && 303 | !widget.entityManager 304 | .group(any: [Selected]) 305 | .entities 306 | .any((e) => e.hasT())) 307 | FlatButton( 308 | child: Text(localization.completeActionLabel), 309 | onPressed: () { 310 | widget.entityManager.setUnique(CompleteRemindersEvent()); 311 | }, 312 | ) 313 | else if (index == 3) 314 | FlatButton( 315 | child: Text(localization.restoreActionLabel), 316 | onPressed: () { 317 | widget.entityManager.setUnique(RestoreNotesEvent()); 318 | }), 319 | if (index != 3) 320 | FlatButton( 321 | child: Text( 322 | localization.deleteActionLabel, 323 | style: appTheme.actionableLabelStyle, 324 | ), 325 | onPressed: () { 326 | widget.entityManager.setUnique(DiscardSelectedEvent()); 327 | }, 328 | ) 329 | ], 330 | ), 331 | ) 332 | ], 333 | ); 334 | } 335 | 336 | Widget buildTabBar(BaseTheme appTheme, Localization localization) { 337 | return AnimatableEntityObservingWidget( 338 | duration: duration, 339 | curve: curve, 340 | provider: (em) => em.getUniqueEntity(), 341 | startAnimating: false, 342 | tweens: { 343 | 'iconScale': Tween(begin: 0.0, end: 1.0), 344 | 'iconColor': ColorTween( 345 | begin: appTheme.otherTabItemColor, 346 | end: appTheme.selectedTabItemColor) 347 | }, 348 | //animateUpdated: (_, __) => EntityAnimation.forward, 349 | builder: (pageEntity, animations, __) { 350 | final pageIndex = pageEntity.get().value; 351 | 352 | return TabBar( 353 | index: pageIndex, 354 | scaleIcon: animations['iconScale'], 355 | colorIcon: animations['iconColor'], 356 | appTheme: appTheme, 357 | prevIndex: pageEntity.get().oldValue, 358 | onTap: (index) async { 359 | if (index != pageIndex) { 360 | if (Platform.isAndroid || Platform.isIOS) 361 | await pageController.animateToPage(index, 362 | duration: duration, curve: curve); 363 | 364 | widget.entityManager 365 | .getUniqueEntity() 366 | .set(PageIndex(index, oldValue: pageIndex)); 367 | } 368 | }, 369 | items: [ 370 | for (final label in localization.pageLabels) TabItem(label: label), 371 | if (Platform.isWindows || Platform.isLinux || Platform.isFuchsia) 372 | TabItem(label: localization.settingsPageTitle) 373 | ], 374 | ); 375 | }, 376 | ); 377 | } 378 | } 379 | -------------------------------------------------------------------------------- /lib/pages/archivePage.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:notebulk/ecs/ecs.dart'; 3 | import 'package:notebulk/util.dart'; 4 | import 'package:notebulk/widgets/cards.dart'; 5 | import 'package:notebulk/widgets/util.dart'; 6 | 7 | class ArchivePage extends StatelessWidget { 8 | const ArchivePage({Key key, this.entityManager}) : super(key: key); 9 | 10 | final EntityManager entityManager; 11 | 12 | @override 13 | Widget build(BuildContext context) { 14 | final localization = 15 | entityManager.getUniqueEntity().get(); 16 | final appTheme = 17 | entityManager.getUniqueEntity().get().value; 18 | 19 | return GroupObservingWidget( 20 | matcher: Matchers.archived, 21 | builder: (group, context) { 22 | final notesList = group.entities; 23 | final notesByDate = >{}; 24 | 25 | for (final note in notesList) { 26 | final date = note.get().value; 27 | final simplifiedDate = DateTime.utc(date.year, date.month); 28 | if (notesByDate[simplifiedDate] == null) { 29 | notesByDate[simplifiedDate] = []; 30 | } 31 | notesByDate[simplifiedDate].add(note); 32 | } 33 | 34 | if (notesByDate.isEmpty) { 35 | notesByDate[DateTime.now()] = []; 36 | } 37 | 38 | return Stack( 39 | children: [ 40 | if (notesList.isEmpty) 41 | Center( 42 | child: RichText( 43 | textWidthBasis: TextWidthBasis.longestLine, 44 | text: TextSpan( 45 | text: '${localization.emptyArchiveHintTitle}\n', 46 | style: appTheme.titleTextStyle, 47 | children: [ 48 | TextSpan( 49 | text: localization.emptyArchiveHintSubtitle, 50 | style: appTheme.subtitleTextStyle) 51 | ]), 52 | ), 53 | ) 54 | else 55 | ListView( 56 | primary: true, 57 | physics: BouncingScrollPhysics(), 58 | key: PageStorageKey('archivedScroll'), 59 | children: [ 60 | for (final noteGroup in notesByDate.entries) ...[ 61 | if (noteGroup.value.isNotEmpty) 62 | CheckboxListTile( 63 | dense: true, 64 | activeColor: appTheme.primaryButtonColor, 65 | checkColor: appTheme.buttonIconColor, 66 | controlAffinity: ListTileControlAffinity.leading, 67 | title: Text( 68 | formatTimestamp(noteGroup.key, localization, 69 | includeDay: false, includeWeekDay: false), 70 | style: appTheme.titleTextStyle, 71 | ), 72 | value: noteGroup.value 73 | .where((e) => e.hasT()) 74 | .length == 75 | noteGroup.value.length, 76 | onChanged: (value) { 77 | if (value) { 78 | for (final note in noteGroup.value) 79 | note.set(Selected()); 80 | } else { 81 | for (final note in noteGroup.value) 82 | note.remove(); 83 | } 84 | }, 85 | ), 86 | buildNotesGridView( 87 | noteGroup.value, 88 | buildNoteCard, 89 | ), 90 | ] 91 | ], 92 | ), 93 | ], 94 | ); 95 | }); 96 | } 97 | 98 | Widget buildNoteCard(Entity note) { 99 | return InkWell( 100 | onLongPress: () => toggleSelected(note), 101 | onTap: () { 102 | // Handle only selection since archived notes can't be edited 103 | if (entityManager.getUniqueEntity().hasT()) 104 | toggleSelected(note); 105 | }, 106 | child: NoteCardWidget( 107 | noteEntity: note, 108 | ), 109 | ); 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /lib/pages/noteListPage.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:kt_dart/kt.dart'; 3 | import 'package:notebulk/ecs/ecs.dart'; 4 | import 'package:notebulk/theme.dart'; 5 | import 'package:notebulk/util.dart'; 6 | import 'package:notebulk/widgets/cards.dart'; 7 | import 'package:notebulk/widgets/util.dart'; 8 | 9 | class NoteListPage extends StatelessWidget { 10 | const NoteListPage({Key key, this.entityManager}) : super(key: key); 11 | 12 | final EntityManager entityManager; 13 | 14 | @override 15 | Widget build(BuildContext context) { 16 | final localization = 17 | entityManager.getUniqueEntity().get(); 18 | final appTheme = 19 | entityManager.getUniqueEntity().get().value; 20 | 21 | return GroupObservingWidget( 22 | matcher: Matchers.note, 23 | builder: (group, context) { 24 | // Sort notes by creation/edited date 25 | final notesList = KtList.from(group.entities) 26 | .sortedByDescending((e) => e.get().value) 27 | .asList(); 28 | // Group by date, only month and year 29 | final notesByDate = >{}; 30 | 31 | for (final note in notesList) { 32 | final date = note.get().value; 33 | final simplifiedDate = DateTime.utc(date.year, date.month); 34 | if (notesByDate[simplifiedDate] == null) { 35 | notesByDate[simplifiedDate] = []; 36 | } 37 | notesByDate[simplifiedDate].add(note); 38 | } 39 | 40 | return Stack( 41 | children: [ 42 | if (notesList.isEmpty) 43 | buildEmptyPage(localization, appTheme) // Render empty state 44 | else 45 | ListView( 46 | primary: true, 47 | shrinkWrap: false, 48 | physics: BouncingScrollPhysics(), 49 | children: [ 50 | if (notesList.isNotEmpty) 51 | for (final noteGroup in notesByDate.entries) ...[ 52 | if (noteGroup.value.isNotEmpty) 53 | Theme( 54 | data: ThemeData( 55 | brightness: appTheme 56 | .brightness), // Make checkbox match brightness 57 | child: CheckboxListTile( 58 | dense: true, 59 | checkColor: appTheme.buttonIconColor, 60 | activeColor: appTheme.primaryButtonColor, 61 | controlAffinity: 62 | ListTileControlAffinity.leading, 63 | title: Text( 64 | formatTimestamp(noteGroup.key, localization, 65 | includeDay: false, includeWeekDay: false), 66 | style: appTheme.titleTextStyle, 67 | ), 68 | value: noteGroup.value 69 | .where((e) => e.hasT()) 70 | .length == 71 | noteGroup.value 72 | .length, // Inform that all notes for that month are selected 73 | onChanged: (value) { 74 | // Shortcut to select or deselect all associated notes 75 | if (value) { 76 | for (final note in noteGroup.value) 77 | note.set(Selected()); 78 | } else { 79 | for (final note in noteGroup.value) 80 | note.remove(); 81 | } 82 | }, 83 | )), 84 | buildNotesGridView(noteGroup.value, buildNoteCard) 85 | ] 86 | ], 87 | ), 88 | Positioned( 89 | bottom: 8, 90 | right: 8, 91 | child: FloatingActionButton( 92 | child: Icon( 93 | AppIcons.pencil, 94 | color: appTheme.buttonIconColor, 95 | ), 96 | backgroundColor: appTheme.primaryButtonColor, 97 | onPressed: () { 98 | // Open note form in creation mode 99 | entityManager 100 | .setUnique(NavigationEvent.push(Routes.createNote)); 101 | }, 102 | ), 103 | ), 104 | ], 105 | ); 106 | }); 107 | } 108 | 109 | Widget buildEmptyPage(Localization localization, BaseTheme appTheme) { 110 | return Center( 111 | child: RichText( 112 | text: TextSpan( 113 | text: '${localization.emptyNoteHintTitle}\n', 114 | style: appTheme.titleTextStyle, 115 | children: [ 116 | TextSpan( 117 | text: localization.emptyNoteHintSubtitle, 118 | style: appTheme.subtitleTextStyle) 119 | ]), 120 | ), 121 | ); 122 | } 123 | 124 | Widget buildNoteCard(Entity note) { 125 | return InkWell( 126 | onLongPress: () => toggleSelected(note), 127 | onTap: () { 128 | // If there's already a selection a single tap should select the note 129 | if (entityManager.getUniqueEntity().hasT()) 130 | toggleSelected(note); 131 | else { 132 | // If nothing is selected open note form for editing instead 133 | entityManager 134 | ..setUniqueOnEntity(FeatureEntityTag(), 135 | note) // Mark this note as the one being edited 136 | ..setUnique(NavigationEvent.push(Routes.editNote)); 137 | } 138 | }, 139 | child: NoteCardWidget( 140 | noteEntity: note, 141 | ), 142 | ); 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /lib/pages/pages.dart: -------------------------------------------------------------------------------- 1 | export 'package:notebulk/pages/splashScreenPage.dart'; 2 | export 'package:notebulk/pages/noteListPage.dart'; 3 | export 'package:notebulk/pages/reminderListPage.dart'; 4 | export 'package:notebulk/pages/searchPage.dart'; 5 | export 'package:notebulk/pages/archivePage.dart'; 6 | export 'package:notebulk/pages/settingsPage.dart'; 7 | -------------------------------------------------------------------------------- /lib/pages/reminderListPage.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:kt_dart/kt.dart'; 3 | import 'package:notebulk/ecs/ecs.dart'; 4 | import 'package:notebulk/theme.dart'; 5 | import 'package:notebulk/util.dart'; 6 | import 'package:notebulk/widgets/cards.dart'; 7 | import 'package:notebulk/widgets/util.dart'; 8 | 9 | class ReminderListPage extends StatelessWidget { 10 | const ReminderListPage({Key key, this.entityManager}) : super(key: key); 11 | 12 | final EntityManager entityManager; 13 | 14 | @override 15 | Widget build(BuildContext context) { 16 | final localization = 17 | entityManager.getUniqueEntity().get(); 18 | final appTheme = 19 | entityManager.getUniqueEntity().get().value; 20 | 21 | return GroupObservingWidget( 22 | matcher: Matchers.reminder, 23 | builder: (group, context) { 24 | final reminderList = group.entities; 25 | var currentReminders = []; 26 | var lateReminders = []; 27 | var completedReminders = []; 28 | final today = DateTime.now(); 29 | final currentDay = today.day; 30 | final currentMonth = today.month; 31 | final currentYear = today.year; 32 | final currentDate = 33 | DateTime.utc(currentYear, currentMonth, currentDay); 34 | 35 | // Sort and group reminders into current, late and completed 36 | for (final reminder in reminderList) { 37 | final reminderDueDate = reminder.get().value; 38 | 39 | if (reminder.hasT()) { 40 | completedReminders.add(reminder); 41 | continue; 42 | } 43 | 44 | if (currentDate.isBefore(reminderDueDate)) { 45 | currentReminders.add(reminder); 46 | } else { 47 | lateReminders.add(reminder); 48 | } 49 | } 50 | 51 | currentReminders = KtList.from(currentReminders) 52 | .sortedByDescending((e) => e.get().value) 53 | .asList(); 54 | 55 | lateReminders = KtList.from(lateReminders) 56 | .sortedByDescending((e) => e.get().value) 57 | .asList(); 58 | 59 | completedReminders = KtList.from(completedReminders) 60 | .sortedByDescending((e) => e.get().value) 61 | .asList(); 62 | 63 | return Stack( 64 | children: [ 65 | ListView( 66 | primary: true, 67 | physics: BouncingScrollPhysics(), 68 | key: PageStorageKey('eventsScroll'), 69 | children: [ 70 | if (currentReminders.isNotEmpty) ...[ 71 | ListTile( 72 | title: Text( 73 | localization.currentRemindersLabel, 74 | style: appTheme.titleTextStyle, 75 | ), 76 | ), 77 | buildNotesGridView(currentReminders, buildReminderCard) 78 | ] else 79 | ListTile( 80 | title: Text( 81 | localization.currentRemindersLabel, 82 | style: appTheme.titleTextStyle, 83 | ), 84 | subtitle: Text(localization.currentRemindersEmpty, 85 | style: appTheme.subtitleTextStyle), 86 | ), 87 | Divider(), 88 | if (lateReminders.isNotEmpty) ...[ 89 | ListTile( 90 | title: Text( 91 | localization.lateRemindersLabel, 92 | style: appTheme.titleTextStyle, 93 | ), 94 | ), 95 | buildNotesGridView(lateReminders, buildReminderCard) 96 | ] else 97 | ListTile( 98 | title: Text( 99 | localization.lateRemindersLabel, 100 | style: appTheme.titleTextStyle, 101 | ), 102 | subtitle: Text(localization.lateRemindersEmpty, 103 | style: appTheme.subtitleTextStyle), 104 | ), 105 | Divider(), 106 | if (completedReminders.isNotEmpty) ...[ 107 | ListTile( 108 | title: Text( 109 | localization.completedRemindersLabel, 110 | style: appTheme.titleTextStyle, 111 | ), 112 | ), 113 | buildNotesGridView(completedReminders, buildReminderCard) 114 | ] else 115 | ListTile( 116 | title: Text( 117 | localization.completedRemindersLabel, 118 | style: appTheme.titleTextStyle, 119 | ), 120 | subtitle: Text(localization.completedRemindersEmpty, 121 | style: appTheme.subtitleTextStyle), 122 | ) 123 | ], 124 | ), 125 | Positioned( 126 | bottom: 8, 127 | right: 8, 128 | child: FloatingActionButton( 129 | child: Icon( 130 | AppIcons.calendar, 131 | color: appTheme.buttonIconColor, 132 | ), 133 | backgroundColor: appTheme.primaryButtonColor, 134 | onPressed: () { 135 | // Open form to create a reminder 136 | entityManager 137 | .setUnique(NavigationEvent.push(Routes.createReminder)); 138 | }, 139 | ), 140 | ) 141 | ], 142 | ); 143 | }); 144 | } 145 | 146 | Widget buildReminderCard(Entity reminder) { 147 | return InkWell( 148 | onLongPress: () { 149 | toggleSelected(reminder); 150 | }, 151 | onTap: () { 152 | if (entityManager.getUniqueEntity().hasT()) { 153 | toggleSelected(reminder); 154 | } else { 155 | // Completed reminders can't be edited, so check first 156 | if (!reminder.hasT()) 157 | entityManager 158 | ..setUniqueOnEntity(FeatureEntityTag(), reminder) 159 | ..setUnique(NavigationEvent.push(Routes.editReminder)); 160 | } 161 | }, 162 | child: ReminderCardWidget( 163 | reminderEntity: reminder, 164 | ), 165 | ); 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /lib/pages/searchPage.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:notebulk/ecs/ecs.dart'; 3 | import 'package:notebulk/theme.dart'; 4 | import 'package:notebulk/util.dart'; 5 | import 'package:notebulk/widgets/cards.dart'; 6 | import 'package:notebulk/widgets/util.dart'; 7 | 8 | class SearchPage extends StatelessWidget { 9 | const SearchPage({Key key, this.entityManager}) : super(key: key); 10 | 11 | final EntityManager entityManager; 12 | 13 | @override 14 | Widget build(BuildContext context) { 15 | final localization = 16 | entityManager.getUniqueEntity().get(); 17 | final appTheme = 18 | entityManager.getUniqueEntity().get().value; 19 | 20 | return Padding( 21 | padding: EdgeInsets.all(16), 22 | child: ListView( 23 | primary: true, 24 | physics: BouncingScrollPhysics(), 25 | key: PageStorageKey('searchScroll'), 26 | children: [ 27 | buildSearchField(localization, appTheme), 28 | SizedBox( 29 | height: 16, 30 | ), 31 | GroupObservingWidget( 32 | matcher: Matchers.searchResult, 33 | builder: (group, context) { 34 | final notesList = group.entities; 35 | final term = entityManager 36 | .getUniqueEntity() 37 | .get() 38 | ?.value; 39 | 40 | // Nothing to show 41 | if (term == null || term.isEmpty) return SizedBox(); 42 | 43 | // No results so show hint as empty state 44 | if (notesList.isEmpty) 45 | return Center( 46 | child: RichText( 47 | text: TextSpan( 48 | text: '${localization.emptySearchHintTitle}\n', 49 | style: appTheme.titleTextStyle, 50 | children: [ 51 | TextSpan( 52 | text: localization.emptySearchHintSubTitle, 53 | style: appTheme.subtitleTextStyle) 54 | ]), 55 | ), 56 | ); 57 | 58 | return buildNotesGridView(notesList, buildNoteCard); 59 | }, 60 | ), 61 | ], 62 | ), 63 | ); 64 | } 65 | 66 | TextFormField buildSearchField( 67 | Localization localization, BaseTheme appTheme) { 68 | return TextFormField( 69 | key: ValueKey('SearchBar'), 70 | cursorColor: appTheme.primaryButtonColor, 71 | style: appTheme.biggerBodyTextStyle, 72 | decoration: InputDecoration( 73 | filled: true, 74 | fillColor: appTheme.selectedTabItemColor.withOpacity(0.3), 75 | contentPadding: 76 | const EdgeInsets.symmetric(horizontal: 16, vertical: 4), 77 | border: OutlineInputBorder( 78 | borderSide: BorderSide.none, 79 | borderRadius: BorderRadius.circular(10), 80 | gapPadding: 0), 81 | hintText: localization.searchNotesHint, 82 | hintStyle: appTheme.subtitleTextStyle, 83 | prefixIcon: Icon( 84 | AppIcons.search, 85 | color: appTheme.primaryButtonColor, 86 | )), 87 | initialValue: entityManager 88 | .getUniqueEntity() 89 | .get() 90 | ?.value, 91 | onChanged: (value) { 92 | // Update search terms and tick but don't trigger a event yet 93 | entityManager.getUniqueEntity() 94 | ..set(SearchTerm(value)) 95 | ..set(entityManager.getUniqueEntity().get()); 96 | }, 97 | onFieldSubmitted: (_) { 98 | // User submitted, trigger event now 99 | return entityManager.setUnique(PerformSearchEvent()); 100 | }, 101 | ); 102 | } 103 | 104 | Widget buildNoteCard(Entity note) { 105 | return InkWell( 106 | onTap: () { 107 | // Open form to edit a found note 108 | entityManager 109 | ..setUniqueOnEntity(FeatureEntityTag(), note) 110 | ..setUnique(NavigationEvent.push(Routes.editNote)); 111 | }, 112 | child: NoteCardWidget( 113 | noteEntity: note, 114 | ), 115 | ); 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /lib/pages/settingsPage.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:notebulk/ecs/ecs.dart'; 3 | import 'package:notebulk/theme.dart'; 4 | import 'package:notebulk/util.dart'; 5 | 6 | class SettingsPage extends StatelessWidget { 7 | const SettingsPage({Key key, this.entityManager}) : super(key: key); 8 | 9 | final EntityManager entityManager; 10 | 11 | @override 12 | Widget build(BuildContext context) { 13 | final localization = 14 | entityManager.getUniqueEntity().get(); 15 | final appTheme = 16 | entityManager.getUniqueEntity().get().value; 17 | 18 | return SingleChildScrollView( 19 | child: Column( 20 | mainAxisAlignment: MainAxisAlignment.start, 21 | children: [ 22 | SwitchListTile( 23 | dense: true, 24 | activeColor: appTheme.primaryButtonColor, 25 | title: Text( 26 | localization.settingsDarkModeLabel, 27 | style: appTheme.titleTextStyle, 28 | ), 29 | value: appTheme.brightness == Brightness.dark, 30 | onChanged: (enabled) { 31 | // Toggle dark mode and persist the changes 32 | entityManager 33 | .getUniqueEntity() 34 | .set(AppTheme(enabled ? DarkTheme() : LightTheme())); 35 | entityManager.setUnique(PersistUserSettingsEvent()); 36 | }), 37 | Divider(), 38 | ListTile( 39 | dense: true, 40 | title: Text( 41 | 'Backup', 42 | style: appTheme.titleTextStyle, 43 | ), 44 | ), 45 | FlatButton.icon( 46 | icon: Icon( 47 | AppIcons.download, 48 | color: appTheme.buttonIconColor, 49 | ), 50 | color: appTheme.primaryButtonColor, 51 | textColor: appTheme.buttonIconColor, 52 | label: Text( 53 | localization.settingsExportLabel, 54 | style: appTheme.actionableLabelStyle 55 | .copyWith(color: appTheme.buttonLabelColor), 56 | ), 57 | onPressed: () { 58 | // Trigger the event to export database backup 59 | entityManager.setUnique(ExportBackupEvent()); 60 | }, 61 | ), 62 | Text('WIP: Doesn\'t handle duplicates'), 63 | FlatButton.icon( 64 | icon: Icon(AppIcons.upload, color: appTheme.buttonIconColor), 65 | color: appTheme.primaryButtonColor, 66 | textColor: appTheme.buttonLabelColor, 67 | label: Text( 68 | localization.settingsImportLabel, 69 | style: appTheme.actionableLabelStyle 70 | .copyWith(color: appTheme.buttonLabelColor), 71 | ), 72 | onPressed: () { 73 | // Trigger the event to import database backup 74 | entityManager.setUnique(ImportBackupEvent()); 75 | }, 76 | ), 77 | SizedBox( 78 | height: 16, 79 | ), 80 | ], 81 | ), 82 | ); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /lib/pages/splashScreenPage.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:notebulk/theme.dart'; 3 | import 'package:notebulk/widgets/util.dart'; 4 | 5 | // Shown while the app is loading it's data 6 | class SplashScreenPage extends StatelessWidget { 7 | @override 8 | Widget build(BuildContext context) { 9 | return GradientBackground( 10 | appTheme: DarkTheme(), 11 | child: SizedBox.expand( 12 | child: Column( 13 | mainAxisAlignment: MainAxisAlignment.center, 14 | crossAxisAlignment: CrossAxisAlignment.center, 15 | children: [ 16 | Image.asset( 17 | 'assets/icon-adaptive.png', 18 | fit: BoxFit.contain, 19 | height: MediaQuery.of(context).size.height * 0.3, 20 | width: MediaQuery.of(context).size.height * 0.3, 21 | ), 22 | Material( 23 | child: Text('Notebulk', 24 | style: TextStyle( 25 | fontSize: 56, 26 | fontWeight: FontWeight.w700, 27 | fontFamily: 'PalanquinDark', 28 | color: Colors.white)), 29 | ), 30 | ], 31 | ), 32 | ), 33 | ); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /lib/theme.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | export 'icons.dart'; 3 | 4 | mixin _FontWeight { 5 | static FontWeight normal = FontWeight.w400; 6 | static FontWeight medium = FontWeight.w500; 7 | static FontWeight bold = FontWeight.w700; 8 | } 9 | 10 | abstract class BaseTheme { 11 | Brightness get brightness; 12 | Color get primaryColor; 13 | Color get accentColor; 14 | Color get appTitleColor; 15 | Color get appBarColor; 16 | Color get selectedTabItemColor; 17 | Color get otherTabItemColor; 18 | Color get settingsContainerColor; 19 | Color get settingsLabelContainerColor; 20 | Color get primaryButtonColor; 21 | Color get secondaryButtonColor; 22 | Color get tertiaryButtonColor; 23 | Color get buttonIconColor; 24 | Color get buttonLabelColor; 25 | List reminderPriorityColors = [ 26 | Colors.blue, 27 | Colors.green, 28 | Colors.yellow, 29 | Colors.orange, 30 | Colors.red, 31 | Colors.black 32 | ]; 33 | LinearGradient get backgroundGradient; 34 | Color get textFieldFillColor; 35 | 36 | static Color grey = Colors.grey; 37 | static Color lightGrey = Colors.grey.shade600; 38 | static Color darkGrey = Colors.grey.shade400; 39 | static Color darkestGrey = Color.fromRGBO(15, 15, 15, 1.0); 40 | 41 | TextStyle get baseStyle; 42 | 43 | TextStyle get appTitleTextStyle => TextStyle( 44 | color: appTitleColor, 45 | fontSize: 24, 46 | fontFamily: 'PalanquinDark', 47 | fontWeight: _FontWeight.bold); 48 | 49 | TextStyle get bodyTextStyle => 50 | baseStyle.copyWith(fontSize: 12, fontWeight: _FontWeight.normal); 51 | 52 | TextStyle get biggerBodyTextStyle => 53 | baseStyle.copyWith(fontSize: 14, fontWeight: _FontWeight.medium); 54 | 55 | TextStyle get titleTextStyle => 56 | baseStyle.copyWith(fontSize: 16, fontWeight: _FontWeight.bold); 57 | 58 | TextStyle get subtitleTextStyle => baseStyle.copyWith( 59 | fontSize: 14, color: lightGrey, fontWeight: _FontWeight.medium); 60 | 61 | TextStyle get cardWidgetContentsTyle => bodyTextStyle; 62 | 63 | TextStyle get cardWidgetTodoItemStyle => bodyTextStyle; 64 | 65 | TextStyle get cardWidgetTagStyle => bodyTextStyle.copyWith( 66 | fontWeight: _FontWeight.medium, color: primaryButtonColor); 67 | 68 | TextStyle get cardWidgetTimestampStyle => bodyTextStyle.copyWith( 69 | color: brightness == Brightness.light ? lightGrey : darkGrey, 70 | fontWeight: _FontWeight.bold); 71 | 72 | TextStyle get formLabelStyle => biggerBodyTextStyle.copyWith( 73 | color: darkGrey, 74 | ); 75 | 76 | TextStyle get actionableLabelStyle => subtitleTextStyle.copyWith( 77 | fontWeight: _FontWeight.bold, color: baseStyle.color); 78 | } 79 | 80 | class LightTheme extends BaseTheme { 81 | @override 82 | Color get primaryColor => Color(0xFFF6D365); 83 | @override 84 | Color get accentColor => Color(0xFFC83660); 85 | @override 86 | Brightness get brightness => Brightness.light; 87 | @override 88 | Color get appBarColor => Colors.white; 89 | @override 90 | Color get appTitleColor => Colors.black; 91 | 92 | @override 93 | LinearGradient get backgroundGradient => LinearGradient( 94 | colors: [Colors.white, primaryColor], 95 | begin: Alignment(0, -0.4), 96 | end: Alignment.bottomCenter, 97 | ); 98 | 99 | @override 100 | Color get buttonIconColor => Colors.white; 101 | 102 | @override 103 | Color get buttonLabelColor => Colors.white; 104 | 105 | @override 106 | Color get otherTabItemColor => BaseTheme.lightGrey; 107 | 108 | @override 109 | Color get primaryButtonColor => accentColor; 110 | 111 | @override 112 | Color get secondaryButtonColor => BaseTheme.darkGrey; 113 | 114 | @override 115 | Color get selectedTabItemColor => accentColor; 116 | 117 | @override 118 | Color get settingsContainerColor => Colors.white; 119 | 120 | @override 121 | Color get settingsLabelContainerColor => Colors.blue; 122 | 123 | @override 124 | Color get tertiaryButtonColor => Colors.black; 125 | 126 | @override 127 | Color get textFieldFillColor => BaseTheme.lightGrey; 128 | 129 | @override 130 | TextStyle get formLabelStyle => 131 | super.formLabelStyle.copyWith(color: baseStyle.color.withOpacity(0.75)); 132 | 133 | @override 134 | TextStyle get baseStyle => 135 | TextStyle(fontFamily: 'Palanquin', color: Colors.black, height: 1.2); 136 | } 137 | 138 | class DarkTheme extends BaseTheme { 139 | @override 140 | Color get primaryColor => Color(0xFFC83660); 141 | @override 142 | Color get accentColor => Color(0xFFF6D365); 143 | @override 144 | Brightness get brightness => Brightness.dark; 145 | @override 146 | Color get appBarColor => BaseTheme.darkestGrey; 147 | @override 148 | Color get appTitleColor => Colors.white; 149 | 150 | @override 151 | LinearGradient get backgroundGradient => LinearGradient( 152 | colors: [BaseTheme.darkestGrey, primaryColor], 153 | begin: Alignment(0, -0.4), 154 | end: Alignment.bottomCenter, 155 | ); 156 | 157 | @override 158 | Color get buttonIconColor => Colors.black; 159 | 160 | @override 161 | Color get buttonLabelColor => Colors.black; 162 | 163 | @override 164 | Color get otherTabItemColor => BaseTheme.darkGrey; 165 | 166 | @override 167 | Color get primaryButtonColor => accentColor; 168 | 169 | @override 170 | Color get secondaryButtonColor => BaseTheme.darkGrey; 171 | 172 | @override 173 | Color get selectedTabItemColor => primaryColor; 174 | 175 | @override 176 | Color get settingsContainerColor => BaseTheme.darkestGrey; 177 | 178 | @override 179 | Color get settingsLabelContainerColor => primaryColor; 180 | 181 | @override 182 | Color get tertiaryButtonColor => Colors.white; 183 | 184 | @override 185 | Color get textFieldFillColor => BaseTheme.lightGrey; 186 | 187 | @override 188 | TextStyle get baseStyle => 189 | TextStyle(fontFamily: 'Palanquin', color: Colors.white, height: 1.2); 190 | 191 | @override 192 | TextStyle get subtitleTextStyle => 193 | super.subtitleTextStyle.copyWith(color: BaseTheme.darkGrey); 194 | } 195 | 196 | // Placeholder theme to properly display SplashScreen 197 | // TODO: Get rid of this hack 198 | class BlankTheme extends DarkTheme { 199 | @override 200 | LinearGradient get backgroundGradient => 201 | LinearGradient(colors: [Colors.black, Colors.black]); 202 | } 203 | -------------------------------------------------------------------------------- /lib/util.dart: -------------------------------------------------------------------------------- 1 | import 'package:entitas_ff/entitas_ff.dart'; 2 | import 'package:flutter/foundation.dart'; 3 | import 'package:flutter/material.dart'; 4 | 5 | // Format timestamp to match the app's aesthetic 6 | String formatTimestamp(DateTime timestamp, Localization localization, 7 | {bool includeDay = true, bool includeWeekDay = true}) { 8 | final day = timestamp.day; 9 | final weekDay = timestamp.weekday; 10 | final month = timestamp.month; 11 | final year = timestamp.year; 12 | final buffer = StringBuffer(); 13 | 14 | if (includeWeekDay) { 15 | buffer.write('${localization.dayNames[weekDay - 1]}. '); 16 | } 17 | 18 | buffer.write('${localization.monthNames[month - 1]} '); 19 | 20 | if (includeDay) { 21 | buffer.write('$day, '); 22 | } 23 | 24 | buffer.write(year); 25 | 26 | return buffer.toString(); 27 | } 28 | 29 | // Localization data, currently only Pt-Br and EN-US 30 | class Localization extends Component { 31 | final String emptyNoteHintTitle; 32 | final String emptyNoteHintSubtitle; 33 | final String emptyArchiveHintTitle; 34 | final String emptyArchiveHintSubtitle; 35 | final String searchNotesHint; 36 | final String emptySearchHintTitle; 37 | final String emptySearchHintSubTitle; 38 | final String settingsPageTitle; 39 | final String settingsColorLabel; 40 | final String settingsDarkModeLabel; 41 | final String completedRemindersLabel; 42 | final String settingsExportLabel; 43 | final String settingsImportLabel; 44 | final String createNoteFeatureTitle; 45 | final String createEventFeatureTitle; 46 | final String editNoteFeatureTitle; 47 | final String editEventFeatureTitle; 48 | final String saveChangesFeatureLabel; 49 | final String featureContentsLabel; 50 | final String featureContentsError; 51 | final String featureContentsHint; 52 | final String featureTodoHint; 53 | final String featureTagsHint; 54 | final String featureImageLabel; 55 | final String featureImageCamera; 56 | final String featureImageGallery; 57 | final String featureTodoLabel; 58 | final String featureReminderHint; 59 | final String featurePriorityLabel; 60 | final String lateRemindersLabel; 61 | final String featureTagsLabel; 62 | final String currentRemindersLabel; 63 | final String archiveActionLabel; 64 | final String restoreActionLabel; 65 | final String deleteActionLabel; 66 | final String completeActionLabel; 67 | final String hideActionLabel; 68 | final List dayNames; 69 | final List monthNames; 70 | final String importedAlert; 71 | final String exportedAlert; 72 | final List pageLabels; 73 | final String yes; 74 | final String no; 75 | final String cancel; 76 | final String accept; 77 | final String promptLeaveApp; 78 | final String promptLeaveUnsaved; 79 | final String currentRemindersEmpty, 80 | lateRemindersEmpty, 81 | completedRemindersEmpty; 82 | 83 | final String selectedLabel; 84 | 85 | factory Localization.en() => Localization._( 86 | yes: 'Yes', 87 | no: 'No', 88 | cancel: 'Cancel', 89 | accept: 'Accept', 90 | promptLeaveApp: 'Wanna close the app?', 91 | promptLeaveUnsaved: 92 | 'You have changes pending to be saved. Leave without makings changes?', 93 | settingsPageTitle: 'Settings', 94 | createNoteFeatureTitle: 'New note', 95 | createEventFeatureTitle: 'New reminder', 96 | editNoteFeatureTitle: 'Edit note', 97 | editEventFeatureTitle: 'Edit reminder', 98 | emptyNoteHintTitle: "You don't have any notes right now.", 99 | emptyNoteHintSubtitle: 'Create one using the button bellow.', 100 | emptyArchiveHintTitle: 'Currently empty', 101 | emptyArchiveHintSubtitle: 102 | 'Your archived notes will end up here and can be later restored.', 103 | emptySearchHintTitle: "Your search didn't find anything.", 104 | emptySearchHintSubTitle: 105 | 'Try other terms or fix spelling errors if any.', 106 | searchNotesHint: 'Search for multiple tags ie: Work important etc.', 107 | featureContentsHint: 'What is this note about?', 108 | featureContentsLabel: 'Contents', 109 | featureContentsError: "Can't be empty", 110 | currentRemindersEmpty: 'Nothing happening right now', 111 | lateRemindersEmpty: 'Everything\'s in order!', 112 | completedRemindersEmpty: 113 | 'Remember to complete or update your late reminders.', 114 | featureTagsHint: 'Ie: Work, Important, etc.', 115 | featureTodoHint: 116 | 'Make lists by breaking the line and mark as checked by inserting * in the end of an item ie: Buy rice*', 117 | featureImageCamera: 'Take a picture', 118 | featureImageGallery: 'Open gallery', 119 | featureImageLabel: 'Picture', 120 | currentRemindersLabel: 'Current', 121 | featureTagsLabel: 'Tags', 122 | lateRemindersLabel: 'Late', 123 | featureTodoLabel: 'Todo list', 124 | featurePriorityLabel: 'Priority', 125 | featureReminderHint: 'What you want to remember?', 126 | saveChangesFeatureLabel: 'Save', 127 | settingsColorLabel: 'Theme color', 128 | settingsDarkModeLabel: 'Dark mode', 129 | completedRemindersLabel: 'Completed', 130 | settingsExportLabel: 'Export notes', 131 | settingsImportLabel: 'Import notes', 132 | archiveActionLabel: 'Archive', 133 | restoreActionLabel: 'Restore', 134 | deleteActionLabel: 'Delete', 135 | hideActionLabel: 'Hide', 136 | completeActionLabel: 'Complete', 137 | importedAlert: 'Backup restored succesfuly!', 138 | exportedAlert: 'Backup exported to SD card', 139 | selectedLabel: 'Apply to selection:', 140 | pageLabels: const [ 141 | 'My notes', 142 | 'Reminders', 143 | 'Search', 144 | 'Archive' 145 | ], 146 | dayNames: const [ 147 | 'MON', 148 | 'TUE', 149 | 'WED', 150 | 'THU', 151 | 'FRI', 152 | 'SAT', 153 | 'SUN' 154 | ], 155 | monthNames: const [ 156 | 'JANUARY', 157 | 'FEBRUARY', 158 | 'MARCH', 159 | 'APRIL', 160 | 'MAY', 161 | 'JUNE', 162 | 'JULY', 163 | 'AUGUST', 164 | 'SEPTEMBER', 165 | 'OCTOBER', 166 | 'NOVEMBER', 167 | 'DECEMBER' 168 | ]); 169 | 170 | factory Localization.ptBR() => Localization._( 171 | yes: 'Sim', 172 | no: 'Não', 173 | cancel: 'Cancelar', 174 | accept: 'Aceitar', 175 | promptLeaveApp: 'Deseja sair do app?', 176 | promptLeaveUnsaved: 177 | 'Você tem alterações pendentes à salvar. Quer Sair sem salvar as alterações?', 178 | emptySearchHintSubTitle: 179 | 'Tente outros termos ou procure por erros de digitação.', 180 | emptyArchiveHintSubtitle: 'Suas notas arquivadas irão aparecer aqui.', 181 | emptyNoteHintSubtitle: 'Crie uma usando o botão no canto inferior.', 182 | settingsPageTitle: 'Configurações', 183 | createNoteFeatureTitle: 'Criar nota', 184 | createEventFeatureTitle: 'Criar lembrete', 185 | editNoteFeatureTitle: 'Editar nota', 186 | editEventFeatureTitle: 'Editar lembrete', 187 | emptyNoteHintTitle: 'Você não possui nenhuma nota no momento', 188 | emptyArchiveHintTitle: 'Nenhuma nota arquivada', 189 | emptySearchHintTitle: 'Sua pesquisa não retornou resultados', 190 | searchNotesHint: 'Ex: Trabalho importante etc.', 191 | featureContentsHint: 'Sobre o que é essa nota?', 192 | featureContentsLabel: 'Conteúdo da nota', 193 | featureContentsError: 'Não pode ficar vazio.', 194 | currentRemindersEmpty: 'Nada acontecendo no momento', 195 | lateRemindersEmpty: 'Tudo em ordem!', 196 | completedRemindersEmpty: 197 | 'Não esqueça de completar ou atualizar os lembretes que atrasarem.', 198 | featureTagsHint: 199 | 'Separe as tags por vírgulas quando houver mais de uma ex: Trabalho, Importante, etc.', 200 | featureTodoHint: 201 | 'Crie items da lista quebrando a linha e marque como finalizado colocando * no final ex: Comprar arroz*', 202 | featureImageCamera: 'Tirar uma foto', 203 | featureImageGallery: 'Selecionar da galeria', 204 | featureImageLabel: 'Imagem', 205 | currentRemindersLabel: 'Atuais', 206 | featureTagsLabel: 'Tags', 207 | lateRemindersLabel: 'Em atraso', 208 | featureTodoLabel: 'Coisas a fazer', 209 | featurePriorityLabel: 'Prioridade', 210 | featureReminderHint: 'O que gostaria de lembrar?', 211 | saveChangesFeatureLabel: 'Salvar', 212 | settingsColorLabel: 'Cor do tema', 213 | settingsDarkModeLabel: 'Modo escuro', 214 | completedRemindersLabel: 'Concluídos', 215 | settingsExportLabel: 'Exportar notas', 216 | settingsImportLabel: 'Importar notas', 217 | archiveActionLabel: 'Arquivar', 218 | restoreActionLabel: 'Restaurar', 219 | deleteActionLabel: 'Excluir', 220 | hideActionLabel: 'Fechar', 221 | completeActionLabel: 'Concluir', 222 | importedAlert: 'Backup restaurado com sucesso!', 223 | exportedAlert: 'Backup exportado para o cartão SD', 224 | selectedLabel: 'Aplicar à seleção:', 225 | pageLabels: const [ 226 | 'Notas', 227 | 'Lembretes', 228 | 'Pesquisa', 229 | 'Arquivo' 230 | ], 231 | dayNames: const [ 232 | 'SEG', 233 | 'TER', 234 | 'QUA', 235 | 'QUI', 236 | 'SEX', 237 | 'SAB', 238 | 'DOM' 239 | ], 240 | monthNames: const [ 241 | 'JANEIRO', 242 | 'FEVEREIRO', 243 | 'MARÇO', 244 | 'ABRIL', 245 | 'MAIO', 246 | 'JUNHO', 247 | 'JULHO', 248 | 'AGOSTO', 249 | 'SETEMBRO', 250 | 'OUTUBRO', 251 | 'NOVEMBRO', 252 | 'DEZEMBRO' 253 | ]); 254 | 255 | Localization._( 256 | {@required this.featureReminderHint, 257 | @required this.featurePriorityLabel, 258 | @required this.completeActionLabel, 259 | @required this.emptyNoteHintSubtitle, 260 | @required this.emptyArchiveHintSubtitle, 261 | @required this.emptySearchHintSubTitle, 262 | @required this.currentRemindersEmpty, 263 | @required this.lateRemindersEmpty, 264 | @required this.completedRemindersEmpty, 265 | @required this.yes, 266 | @required this.no, 267 | @required this.cancel, 268 | @required this.accept, 269 | @required this.promptLeaveApp, 270 | @required this.promptLeaveUnsaved, 271 | @required this.emptyNoteHintTitle, 272 | @required this.emptyArchiveHintTitle, 273 | @required this.searchNotesHint, 274 | @required this.emptySearchHintTitle, 275 | @required this.settingsPageTitle, 276 | @required this.settingsColorLabel, 277 | @required this.settingsDarkModeLabel, 278 | @required this.completedRemindersLabel, 279 | @required this.settingsExportLabel, 280 | @required this.settingsImportLabel, 281 | @required this.createNoteFeatureTitle, 282 | @required this.createEventFeatureTitle, 283 | @required this.editNoteFeatureTitle, 284 | @required this.editEventFeatureTitle, 285 | @required this.saveChangesFeatureLabel, 286 | @required this.featureContentsHint, 287 | @required this.featureContentsLabel, 288 | @required this.featureContentsError, 289 | @required this.featureTodoHint, 290 | @required this.featureTagsHint, 291 | @required this.featureImageLabel, 292 | @required this.featureImageCamera, 293 | @required this.featureImageGallery, 294 | @required this.featureTodoLabel, 295 | @required this.lateRemindersLabel, 296 | @required this.featureTagsLabel, 297 | @required this.currentRemindersLabel, 298 | @required this.archiveActionLabel, 299 | @required this.restoreActionLabel, 300 | @required this.deleteActionLabel, 301 | @required this.hideActionLabel, 302 | @required this.selectedLabel, 303 | @required this.importedAlert, 304 | @required this.exportedAlert, 305 | @required this.dayNames, 306 | @required this.monthNames, 307 | @required this.pageLabels}); 308 | } 309 | 310 | // Route names, each must be unique 311 | class Routes { 312 | static const String mainScreen = 'mainPage'; 313 | static const String createNote = 'createNote'; 314 | static const String editNote = 'editNote'; 315 | static const String splashScreen = 'splashScreen'; 316 | static const String createReminder = 'createReminder'; 317 | static const String editReminder = 'editReminder'; 318 | } 319 | -------------------------------------------------------------------------------- /lib/widgets/cards.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:entitas_ff/entitas_ff.dart'; 4 | import 'package:flutter/material.dart'; 5 | import 'package:notebulk/ecs/components.dart'; 6 | import 'package:notebulk/theme.dart'; 7 | import 'package:notebulk/util.dart'; 8 | import 'package:tinycolor/tinycolor.dart'; 9 | 10 | class NoteCardWidget extends StatelessWidget { 11 | const NoteCardWidget({Key key, this.noteEntity}) : super(key: key); 12 | 13 | final Entity noteEntity; 14 | 15 | @override 16 | Widget build(BuildContext context) { 17 | final contents = noteEntity.get().value; 18 | final timestamp = noteEntity.get()?.value ?? DateTime.now(); 19 | final tags = noteEntity.get()?.value ?? []; 20 | final listItems = noteEntity.get()?.value ?? []; 21 | final picFile = noteEntity.get()?.value; 22 | final isSelected = noteEntity.hasT(); 23 | final localization = EntityManagerProvider.of(context) 24 | .entityManager 25 | .getUniqueEntity() 26 | .get(); 27 | final appTheme = EntityManagerProvider.of(context) 28 | .entityManager 29 | .getUniqueEntity() 30 | .get() 31 | .value; 32 | 33 | return Container( 34 | foregroundDecoration: BoxDecoration( 35 | color: isSelected 36 | ? appTheme.primaryButtonColor.withOpacity(0.75) 37 | : Colors.transparent), 38 | child: Card( 39 | clipBehavior: Clip.antiAlias, 40 | elevation: 4, 41 | color: appTheme.appBarColor, 42 | margin: EdgeInsets.all(0), 43 | child: Stack( 44 | children: [ 45 | Padding( 46 | padding: EdgeInsets.all(12), 47 | child: Column( 48 | mainAxisAlignment: MainAxisAlignment.start, 49 | crossAxisAlignment: CrossAxisAlignment.start, 50 | children: [ 51 | buildTimestamp(context, timestamp, localization, appTheme), 52 | SizedBox( 53 | height: 8, 54 | ), 55 | if (picFile != null) ...[ 56 | buildPicField(picFile), 57 | SizedBox( 58 | height: 8, 59 | ), 60 | ], 61 | buildContentsField(context, contents, appTheme), 62 | if (listItems.isNotEmpty) ...[ 63 | SizedBox( 64 | height: 8, 65 | ), 66 | buildListField(listItems, context, appTheme) 67 | ], 68 | if (tags.isNotEmpty) ...[ 69 | Divider(), 70 | buildTags(tags, context, appTheme) 71 | ], 72 | ], 73 | ), 74 | ), 75 | ], 76 | ), 77 | ), 78 | ); 79 | } 80 | 81 | Widget buildPicField(File picFile) { 82 | final margin = EdgeInsets.symmetric(vertical: 8); 83 | 84 | return Card( 85 | margin: margin, 86 | elevation: 2, 87 | child: Hero( 88 | tag: picFile.path, 89 | child: Image.file( 90 | picFile, 91 | fit: BoxFit.cover, 92 | ), 93 | ), 94 | ); 95 | } 96 | 97 | Widget buildContentsField( 98 | BuildContext context, String contents, BaseTheme appTheme) { 99 | return Text( 100 | contents, 101 | textAlign: TextAlign.left, 102 | textWidthBasis: TextWidthBasis.longestLine, 103 | style: appTheme.cardWidgetContentsTyle, 104 | ); 105 | } 106 | 107 | Widget buildListField( 108 | List listItems, BuildContext context, BaseTheme appTheme) { 109 | return Padding( 110 | padding: EdgeInsets.all(4), 111 | child: Column( 112 | mainAxisAlignment: MainAxisAlignment.start, 113 | crossAxisAlignment: CrossAxisAlignment.start, 114 | children: [ 115 | for (int index = 0; index < listItems.length; index++) 116 | Padding( 117 | padding: EdgeInsets.all(4), 118 | child: Row( 119 | crossAxisAlignment: CrossAxisAlignment.start, 120 | children: [ 121 | Expanded( 122 | child: Text( 123 | '• ${listItems[index].label}', 124 | softWrap: true, 125 | textAlign: TextAlign.left, 126 | style: appTheme.cardWidgetTodoItemStyle.copyWith( 127 | fontWeight: listItems[index].isChecked 128 | ? FontWeight.w500 129 | : FontWeight.w400, 130 | color: listItems[index].isChecked 131 | ? Colors.grey.shade600 132 | : appTheme.cardWidgetTodoItemStyle.color, 133 | decoration: listItems[index].isChecked 134 | ? TextDecoration.lineThrough 135 | : TextDecoration.none), 136 | ), 137 | ), 138 | ], 139 | ), 140 | ), 141 | ]), 142 | ); 143 | } 144 | 145 | Widget buildTimestamp(BuildContext context, DateTime timestamp, 146 | Localization localization, BaseTheme appTheme) { 147 | return Text( 148 | formatTimestamp(timestamp, localization), 149 | style: appTheme.cardWidgetTimestampStyle, 150 | ); 151 | } 152 | 153 | Widget buildTags( 154 | List tags, BuildContext context, BaseTheme appTheme) { 155 | final padding = EdgeInsets.symmetric(vertical: 4); 156 | 157 | return Padding( 158 | padding: padding, 159 | child: Wrap( 160 | spacing: 4, 161 | runSpacing: 4, 162 | children: [ 163 | for (final tag in tags) 164 | Container( 165 | padding: const EdgeInsets.all(4), 166 | child: RichText( 167 | text: 168 | TextSpan(text: tag, style: appTheme.cardWidgetTagStyle)), 169 | ) 170 | ], 171 | ), 172 | ); 173 | } 174 | } 175 | 176 | class ReminderCardWidget extends StatelessWidget { 177 | const ReminderCardWidget({Key key, this.reminderEntity}) : super(key: key); 178 | 179 | final Entity reminderEntity; 180 | 181 | @override 182 | Widget build(BuildContext context) { 183 | final reminderPriority = reminderEntity.get().value.index; 184 | final contents = reminderEntity.get().value; 185 | final timestamp = reminderEntity.get()?.value ?? DateTime.now(); 186 | final isSelected = reminderEntity.hasT(); 187 | final localization = EntityManagerProvider.of(context) 188 | .entityManager 189 | .getUniqueEntity() 190 | .get(); 191 | final appTheme = EntityManagerProvider.of(context) 192 | .entityManager 193 | .getUniqueEntity() 194 | .get() 195 | .value; 196 | 197 | return Container( 198 | foregroundDecoration: isSelected 199 | ? BoxDecoration(color: appTheme.primaryButtonColor.withOpacity(0.75)) 200 | : null, 201 | child: Card( 202 | clipBehavior: Clip.antiAlias, 203 | elevation: 4, 204 | color: appTheme.reminderPriorityColors[reminderPriority], 205 | margin: EdgeInsets.all(0), 206 | child: Stack( 207 | children: [ 208 | Padding( 209 | padding: EdgeInsets.all(12), 210 | child: Column( 211 | mainAxisAlignment: MainAxisAlignment.start, 212 | crossAxisAlignment: CrossAxisAlignment.start, 213 | children: [ 214 | buildTimestamp(context, timestamp, localization, appTheme), 215 | SizedBox( 216 | height: 8, 217 | ), 218 | buildContentsField(context, contents, appTheme), 219 | ], 220 | ), 221 | ), 222 | ], 223 | ), 224 | ), 225 | ); 226 | } 227 | 228 | Widget buildContentsField( 229 | BuildContext context, String contents, BaseTheme appTheme) { 230 | final priority = reminderEntity.get()?.value; 231 | final cardColor = priority != null 232 | ? appTheme.reminderPriorityColors[priority.index] 233 | : appTheme.appBarColor; 234 | final textColor = 235 | TinyColor(cardColor).isDark() ? Colors.white : Colors.black; 236 | 237 | return Text( 238 | contents, 239 | textAlign: TextAlign.left, 240 | maxLines: null, 241 | textWidthBasis: TextWidthBasis.longestLine, 242 | style: appTheme.cardWidgetContentsTyle.copyWith(color: textColor), 243 | ); 244 | } 245 | 246 | Widget buildTimestamp(BuildContext context, DateTime date, 247 | Localization localization, BaseTheme appTheme) { 248 | final timestamp = formatTimestamp(date, localization); 249 | final priority = reminderEntity.get()?.value; 250 | final cardColor = priority != null 251 | ? appTheme.reminderPriorityColors[priority.index] 252 | : appTheme.appBarColor; 253 | final textColor = 254 | TinyColor(cardColor).isDark() ? Colors.white : Colors.black; 255 | 256 | return Text(timestamp, 257 | style: appTheme.cardWidgetTimestampStyle.copyWith(color: textColor)); 258 | } 259 | } 260 | -------------------------------------------------------------------------------- /lib/widgets/util.dart: -------------------------------------------------------------------------------- 1 | import 'dart:ui'; 2 | 3 | import 'package:entitas_ff/entitas_ff.dart'; 4 | import 'package:flutter/material.dart'; 5 | import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart'; 6 | import 'package:notebulk/ecs/components.dart'; 7 | import 'package:notebulk/theme.dart'; 8 | import 'package:notebulk/util.dart'; 9 | 10 | Widget buildNotesGridView( 11 | List notes, Widget Function(Entity) buildNoteCard, 12 | [int gridCount = 2]) { 13 | return StaggeredGridView.countBuilder( 14 | shrinkWrap: true, 15 | physics: NeverScrollableScrollPhysics(), 16 | padding: EdgeInsets.symmetric(horizontal: 8, vertical: 4), 17 | crossAxisCount: gridCount, 18 | itemCount: notes.length, 19 | mainAxisSpacing: 4, 20 | crossAxisSpacing: 4, 21 | itemBuilder: (context, index) => buildNoteCard(notes[index]), 22 | staggeredTileBuilder: (index) => StaggeredTile.fit(1), 23 | ); 24 | } 25 | 26 | Widget buildNotesSliverGridView( 27 | List notes, Widget Function(Entity) buildNoteCard, 28 | [int count = 2]) { 29 | assert(count > 0, 'GridView must have a cross axis count greater than zero'); 30 | return SliverPadding( 31 | padding: EdgeInsets.symmetric(horizontal: 8, vertical: 4), 32 | sliver: SliverStaggeredGrid.countBuilder( 33 | crossAxisCount: count, 34 | itemCount: notes.length, 35 | mainAxisSpacing: 4, 36 | crossAxisSpacing: 4, 37 | itemBuilder: (context, index) => buildNoteCard(notes[index]), 38 | staggeredTileBuilder: (index) => StaggeredTile.fit(1), 39 | ), 40 | ); 41 | } 42 | 43 | /* 44 | Widget buildNotesListView( 45 | List notes, Widget Function(Entity) buildNoteCard) { 46 | return ListView.builder( 47 | shrinkWrap: true, 48 | itemCount: notes.length, 49 | itemBuilder: (context, index) => buildNoteCard(notes[index]), 50 | ); 51 | } 52 | */ 53 | 54 | class TabBar extends StatelessWidget { 55 | const TabBar({ 56 | @required this.onTap, 57 | @required this.items, 58 | Key key, 59 | this.appTheme, 60 | this.index = 0, 61 | this.prevIndex = 0, 62 | this.scaleIcon, 63 | this.colorIcon, 64 | }) : super(key: key); 65 | 66 | final Function(int) onTap; 67 | final List items; 68 | final int index, prevIndex; 69 | final Animation scaleIcon; 70 | final Animation colorIcon; 71 | final BaseTheme appTheme; 72 | 73 | @override 74 | Widget build(BuildContext context) { 75 | final tabWidth = MediaQuery.of(context).size.width / items.length - 2; 76 | final tabHeight = kTextTabBarHeight * 0.75; 77 | 78 | return Container( 79 | decoration: BoxDecoration(color: appTheme.appBarColor), 80 | height: kTextTabBarHeight, 81 | alignment: Alignment.center, 82 | padding: const EdgeInsets.only(top: 4), 83 | width: MediaQuery.of(context).size.width, 84 | child: Stack( 85 | children: [ 86 | Row( 87 | mainAxisAlignment: MainAxisAlignment.center, 88 | children: [ 89 | for (int i = 0; i < items.length; i++) 90 | InkWell( 91 | onTap: () => onTap(i), 92 | child: Column( 93 | children: [ 94 | Container( 95 | height: tabHeight, 96 | width: tabWidth, 97 | alignment: Alignment.center, 98 | child: Text( 99 | items[i].label, 100 | textAlign: TextAlign.center, 101 | style: appTheme.biggerBodyTextStyle.copyWith( 102 | color: index == i 103 | ? colorIcon.value 104 | : appTheme.otherTabItemColor, 105 | ), 106 | ), 107 | ), 108 | ], 109 | ), 110 | ), 111 | ], 112 | ), 113 | Positioned( 114 | left: 4.0 + 115 | (lerpDouble( 116 | tabWidth * prevIndex, tabWidth * index, scaleIcon.value)), 117 | bottom: 0, 118 | child: Container( 119 | height: 2, 120 | color: appTheme.selectedTabItemColor, 121 | width: tabWidth, 122 | ), 123 | ) 124 | ], 125 | ), 126 | ); 127 | } 128 | } 129 | 130 | class TabItem { 131 | const TabItem({this.label}); 132 | 133 | final String label; 134 | } 135 | 136 | class StatusBar extends StatelessWidget { 137 | final List Function(int) actions; 138 | 139 | const StatusBar({Key key, this.actions}) : super(key: key); 140 | 141 | @override 142 | Widget build(BuildContext context) { 143 | final entityManager = EntityManagerProvider.of(context).entityManager; 144 | final appTheme = 145 | entityManager.getUniqueEntity().get().value; 146 | final localization = 147 | entityManager.getUniqueEntity().get(); 148 | 149 | return AnimatableEntityObservingWidget.extended( 150 | provider: (em) => em.getUniqueEntity(), 151 | startAnimating: false, 152 | curve: Curves.fastOutSlowIn, 153 | duration: Duration(milliseconds: 200), 154 | tweens: { 155 | 'size': Tween(begin: 0, end: kBottomNavigationBarHeight) 156 | }, 157 | animateAdded: (c) => 158 | c is Toggle ? EntityAnimation.forward : EntityAnimation.none, 159 | animateRemoved: (c) => 160 | c is Toggle ? EntityAnimation.reverse : EntityAnimation.none, 161 | animateUpdated: (_, __) => EntityAnimation.none, 162 | builder: (statusEntity, animations, context) { 163 | return Stack( 164 | children: [ 165 | SizedBox( 166 | height: kBottomNavigationBarHeight * animations['size'].value, 167 | width: MediaQuery.of(context).size.width, 168 | ), 169 | Positioned( 170 | bottom: 0, 171 | child: EntityObservingWidget( 172 | provider: (em) => em.getUniqueEntity(), 173 | builder: (e, _) => Container( 174 | color: appTheme.appBarColor, 175 | width: MediaQuery.of(context).size.width, 176 | height: animations['size'].value, 177 | child: statusEntity.hasT() 178 | ? Row( 179 | mainAxisAlignment: MainAxisAlignment.spaceEvenly, 180 | crossAxisAlignment: CrossAxisAlignment.center, 181 | children: [ 182 | if (statusEntity.hasT()) 183 | Text( 184 | statusEntity.get().value, 185 | style: appTheme.biggerBodyTextStyle, 186 | ), 187 | if (statusEntity.hasT()) ...[ 188 | FlatButton( 189 | child: Text(localization.hideActionLabel), 190 | onPressed: () { 191 | statusEntity 192 | ..remove() 193 | ..remove(); 194 | }, 195 | ) 196 | ] else 197 | ...actions?.call(e.get().value), 198 | ], 199 | ) 200 | : SizedBox( 201 | width: 0, 202 | height: 0, 203 | ), 204 | ), 205 | ), 206 | ), 207 | ], 208 | ); 209 | }, 210 | ); 211 | } 212 | } 213 | 214 | class ShouldLeavePromptDialog extends StatelessWidget { 215 | final String message, yesLabel, noLabel; 216 | final VoidCallback onYes, onNo; 217 | final BaseTheme appTheme; 218 | 219 | const ShouldLeavePromptDialog({ 220 | @required this.message, 221 | @required this.onYes, 222 | @required this.onNo, 223 | this.appTheme, 224 | this.yesLabel, 225 | this.noLabel, 226 | Key key, 227 | }) : super(key: key); 228 | 229 | @override 230 | Widget build(BuildContext context) { 231 | final appTheme = this.appTheme ?? LightTheme(); 232 | return AlertDialog( 233 | title: Text( 234 | message, 235 | style: appTheme.titleTextStyle, 236 | ), 237 | actions: [ 238 | FlatButton( 239 | splashColor: appTheme.accentColor, 240 | child: Text( 241 | noLabel, 242 | style: appTheme.actionableLabelStyle, 243 | ), 244 | onPressed: () => Navigator.of(context).pop(false), 245 | ), 246 | FlatButton( 247 | splashColor: appTheme.accentColor, 248 | child: Text( 249 | yesLabel, 250 | style: appTheme.actionableLabelStyle 251 | .copyWith(color: appTheme.primaryButtonColor, fontSize: 16), 252 | ), 253 | onPressed: () => Navigator.of(context).pop(true), 254 | ), 255 | ], 256 | ); 257 | } 258 | } 259 | 260 | class GradientBackground extends StatelessWidget { 261 | const GradientBackground({Key key, this.child, this.appTheme}) 262 | : super(key: key); 263 | 264 | final BaseTheme appTheme; 265 | final Widget child; 266 | 267 | @override 268 | Widget build(BuildContext context) { 269 | return DecoratedBox( 270 | decoration: BoxDecoration(gradient: appTheme.backgroundGradient), 271 | child: child); 272 | } 273 | } 274 | 275 | /* class ClippableShadowPainter extends CustomPainter { 276 | ClippableShadowPainter({@required this.shadow, @required this.clipper}); 277 | 278 | final Shadow shadow; 279 | final CustomClipper clipper; 280 | 281 | @override 282 | void paint(Canvas canvas, Size size) { 283 | final paint = shadow.toPaint(); 284 | final clipPath = clipper.getClip(size).shift(shadow.offset); 285 | canvas.drawPath(clipPath, paint); 286 | } 287 | 288 | @override 289 | bool shouldRepaint(CustomPainter oldDelegate) { 290 | return true; 291 | } 292 | } */ 293 | -------------------------------------------------------------------------------- /linux/Makefile: -------------------------------------------------------------------------------- 1 | # Copyright 2018 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | # Executable name. 16 | BINARY_NAME=notebulk 17 | # The location of the flutter-desktop-embedding repository. 18 | FDE_ROOT=$(HOME)/Dev/flutter-desktop-embedding 19 | # The C++ code for the embedder application. 20 | SOURCES=notebulk.cc 21 | 22 | # Plugins to include from the flutter-desktop-embedding plugins/ directory. 23 | PLUGIN_NAMES=file_chooser 24 | # Additional plugins to include from the plugins/flutter_plugins subdirectory. 25 | FLUTTER_PLUGIN_NAMES= 26 | 27 | 28 | # Default build type. For a release build, set BUILD=release. 29 | # Currently this only sets NDEBUG, which is used to control the flags passed 30 | # to the Flutter engine in the example shell, and not the complation settings 31 | # (e.g., optimization level) of the C++ code. 32 | BUILD=debug 33 | 34 | # Configuration provided via flutter tool. 35 | include flutter/generated_config 36 | 37 | # Dependency locations 38 | FLUTTER_APP_CACHE_DIR=flutter/ 39 | FLUTTER_APP_DIR=$(CURDIR)/.. 40 | FLUTTER_APP_BUILD_DIR=$(FLUTTER_APP_DIR)/build 41 | PLUGINS_DIR=$(FDE_ROOT)/plugins 42 | FLUTTER_PLUGINS_DIR=$(PLUGINS_DIR)/flutter_plugins 43 | 44 | OUT_DIR=$(FLUTTER_APP_BUILD_DIR)/linux 45 | 46 | # Libraries 47 | FLUTTER_LIB_NAME=flutter_linux 48 | FLUTTER_LIB=$(FLUTTER_APP_CACHE_DIR)/lib$(FLUTTER_LIB_NAME).so 49 | # The name of each plugin library is the name of its directory with _plugin 50 | # appended. The exception is example_plugin (to avoid confusion with the 51 | # top-level example/ directory), so it's added here separately. 52 | PLUGIN_LIB_NAMES=$(foreach plugin,$(PLUGIN_NAMES) $(FLUTTER_PLUGIN_NAMES),$(plugin)_plugin) 53 | PLUGIN_LIBS=$(foreach plugin,$(PLUGIN_LIB_NAMES),$(OUT_DIR)/lib$(plugin).so) 54 | ALL_LIBS=$(FLUTTER_LIB) $(PLUGIN_LIBS) 55 | 56 | # Tools 57 | FLUTTER_BIN=$(FLUTTER_ROOT)/bin/flutter 58 | 59 | # Resources 60 | ICU_DATA_NAME=icudtl.dat 61 | ICU_DATA_SOURCE=$(FLUTTER_APP_CACHE_DIR)/$(ICU_DATA_NAME) 62 | FLUTTER_ASSETS_NAME=flutter_assets 63 | FLUTTER_ASSETS_SOURCE=$(FLUTTER_APP_BUILD_DIR)/$(FLUTTER_ASSETS_NAME) 64 | 65 | # Bundle structure 66 | BUNDLE_OUT_DIR=$(OUT_DIR)/$(BUILD) 67 | BUNDLE_DATA_DIR=$(BUNDLE_OUT_DIR)/data 68 | BUNDLE_LIB_DIR=$(BUNDLE_OUT_DIR)/lib 69 | 70 | BIN_OUT=$(BUNDLE_OUT_DIR)/$(BINARY_NAME) 71 | ICU_DATA_OUT=$(BUNDLE_DATA_DIR)/$(ICU_DATA_NAME) 72 | FLUTTER_LIB_OUT=$(BUNDLE_LIB_DIR)/lib$(FLUTTER_LIB_NAME).so 73 | ALL_LIBS_OUT=$(foreach lib,$(ALL_LIBS),$(BUNDLE_LIB_DIR)/$(notdir $(lib))) 74 | 75 | # Add relevant code from the wrapper library, which is intended to be statically 76 | # built into the client. 77 | WRAPPER_ROOT=$(FLUTTER_APP_CACHE_DIR)/cpp_client_wrapper 78 | WRAPPER_SOURCES= \ 79 | $(WRAPPER_ROOT)/flutter_window_controller.cc \ 80 | $(WRAPPER_ROOT)/plugin_registrar.cc \ 81 | $(WRAPPER_ROOT)/engine_method_result.cc 82 | SOURCES+=$(WRAPPER_SOURCES) 83 | 84 | # Headers 85 | WRAPPER_INCLUDE_DIR=$(WRAPPER_ROOT)/include 86 | # The plugin builds place all published headers in a top-level include/. 87 | PLUGIN_INCLUDE_DIRS=$(OUT_DIR)/include 88 | INCLUDE_DIRS=$(FLUTTER_APP_CACHE_DIR) $(PLUGIN_INCLUDE_DIRS) \ 89 | $(WRAPPER_INCLUDE_DIR) 90 | 91 | # Build settings 92 | CXX=clang++ 93 | CXXFLAGS.release=-DNDEBUG 94 | CXXFLAGS=-std=c++14 -Wall -Werror $(CXXFLAGS.$(BUILD)) 95 | CPPFLAGS=$(patsubst %,-I%,$(INCLUDE_DIRS)) 96 | LDFLAGS=-L$(BUNDLE_LIB_DIR) \ 97 | -l$(FLUTTER_LIB_NAME) \ 98 | $(patsubst %,-l%,$(PLUGIN_LIB_NAMES)) \ 99 | -Wl,-rpath=\$$ORIGIN/lib 100 | 101 | # Targets 102 | 103 | .PHONY: all 104 | all: $(BIN_OUT) bundle 105 | 106 | # This is a phony target because the flutter tool cannot describe 107 | # its inputs and outputs yet. 108 | .PHONY: sync 109 | sync: flutter/generated_config 110 | $(FLUTTER_ROOT)/packages/flutter_tools/bin/tool_backend.sh linux-x64 $(BUILD) 111 | 112 | .PHONY: bundle 113 | bundle: $(ICU_DATA_OUT) $(ALL_LIBS_OUT) bundleflutterassets 114 | 115 | $(BIN_OUT): $(SOURCES) $(ALL_LIBS_OUT) 116 | mkdir -p $(@D) 117 | $(CXX) $(CXXFLAGS) $(CPPFLAGS) $(SOURCES) $(LDFLAGS) -o $@ 118 | 119 | $(WRAPPER_SOURCES) $(FLUTTER_LIB) $(ICU_DATA_SOURCE) $(FLUTTER_ASSETS_SOURCE): \ 120 | | sync 121 | 122 | # Implicit rules won't match phony targets, so list plugin builds explicitly. 123 | $(OUT_DIR)/libfile_chooser_plugin.so: | file_chooser 124 | 125 | .PHONY: $(PLUGIN_NAMES) example_plugin 126 | $(PLUGIN_NAMES) example_plugin: 127 | make -C $(PLUGINS_DIR)/$@/linux \ 128 | OUT_DIR=$(OUT_DIR) FLUTTER_ROOT=$(FLUTTER_ROOT) 129 | 130 | .PHONY: $(FLUTTER_PLUGIN_NAMES) 131 | $(FLUTTER_PLUGIN_NAMES): 132 | make -C $(FLUTTER_PLUGINS_DIR)/$@/linux \ 133 | OUT_DIR=$(OUT_DIR) FLUTTER_ROOT=$(FLUTTER_ROOT) 134 | 135 | # Plugin library bundling pattern. 136 | $(BUNDLE_LIB_DIR)/%: $(OUT_DIR)/% 137 | mkdir -p $(BUNDLE_LIB_DIR) 138 | cp $< $@ 139 | 140 | $(FLUTTER_LIB_OUT): $(FLUTTER_LIB) 141 | mkdir -p $(@D) 142 | cp $< $@ 143 | 144 | $(ICU_DATA_OUT): $(ICU_DATA_SOURCE) 145 | mkdir -p $(@D) 146 | cp $< $@ 147 | 148 | # Fully re-copy the assets directory on each build to avoid having to keep a 149 | # comprehensive list of all asset files here, which would be fragile to changes 150 | # in the Flutter example (e.g., adding a new font to pubspec.yaml would require 151 | # changes here). 152 | .PHONY: bundleflutterassets 153 | bundleflutterassets: $(FLUTTER_ASSETS_SOURCE) 154 | mkdir -p $(BUNDLE_DATA_DIR) 155 | rsync -rpu --delete $(FLUTTER_ASSETS_SOURCE) $(BUNDLE_DATA_DIR) 156 | 157 | .PHONY: clean 158 | clean: 159 | rm -rf $(OUT_DIR); \ 160 | cd $(FLUTTER_APP_DIR); \ 161 | $(FLUTTER_BIN) clean 162 | -------------------------------------------------------------------------------- /linux/notebulk.cc: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include 20 | 21 | #include 22 | #include 23 | 24 | namespace { 25 | 26 | // Returns the path of the directory containing this executable, or an empty 27 | // string if the directory cannot be found. 28 | std::string GetExecutableDirectory() { 29 | char buffer[PATH_MAX + 1]; 30 | ssize_t length = readlink("/proc/self/exe", buffer, sizeof(buffer)); 31 | if (length > PATH_MAX) { 32 | std::cerr << "Couldn't locate executable" << std::endl; 33 | return ""; 34 | } 35 | std::string executable_path(buffer, length); 36 | size_t last_separator_position = executable_path.find_last_of('/'); 37 | if (last_separator_position == std::string::npos) { 38 | std::cerr << "Unabled to find parent directory of " << executable_path 39 | << std::endl; 40 | return ""; 41 | } 42 | return executable_path.substr(0, last_separator_position); 43 | } 44 | 45 | } // namespace 46 | 47 | int main(int argc, char **argv) { 48 | // Resources are located relative to the executable. 49 | std::string base_directory = GetExecutableDirectory(); 50 | if (base_directory.empty()) { 51 | base_directory = "."; 52 | } 53 | std::string data_directory = base_directory + "/data"; 54 | std::string assets_path = data_directory + "/flutter_assets"; 55 | std::string icu_data_path = data_directory + "/icudtl.dat"; 56 | 57 | // Arguments for the Flutter Engine. 58 | std::vector arguments; 59 | #ifdef NDEBUG 60 | arguments.push_back("--disable-dart-asserts"); 61 | #endif 62 | 63 | flutter::FlutterWindowController flutter_controller(icu_data_path); 64 | 65 | // Start the engine. 66 | if (!flutter_controller.CreateWindow(800, 600, "Notebulk", assets_path, 67 | arguments)) { 68 | return EXIT_FAILURE; 69 | } 70 | 71 | // Register any native plugins. 72 | FileChooserRegisterWithRegistrar( 73 | flutter_controller.GetRegistrarForPlugin("FileChooser")); 74 | 75 | // Run until the window is closed. 76 | flutter_controller.RunEventLoop(); 77 | return EXIT_SUCCESS; 78 | } 79 | -------------------------------------------------------------------------------- /pubspec.lock: -------------------------------------------------------------------------------- 1 | # Generated by pub 2 | # See https://dart.dev/tools/pub/glossary#lockfile 3 | packages: 4 | analyzer: 5 | dependency: transitive 6 | description: 7 | name: analyzer 8 | url: "https://pub.dartlang.org" 9 | source: hosted 10 | version: "0.36.4" 11 | args: 12 | dependency: transitive 13 | description: 14 | name: args 15 | url: "https://pub.dartlang.org" 16 | source: hosted 17 | version: "1.5.2" 18 | async: 19 | dependency: transitive 20 | description: 21 | name: async 22 | url: "https://pub.dartlang.org" 23 | source: hosted 24 | version: "2.3.0" 25 | boolean_selector: 26 | dependency: transitive 27 | description: 28 | name: boolean_selector 29 | url: "https://pub.dartlang.org" 30 | source: hosted 31 | version: "1.0.5" 32 | charcode: 33 | dependency: transitive 34 | description: 35 | name: charcode 36 | url: "https://pub.dartlang.org" 37 | source: hosted 38 | version: "1.1.2" 39 | collection: 40 | dependency: transitive 41 | description: 42 | name: collection 43 | url: "https://pub.dartlang.org" 44 | source: hosted 45 | version: "1.14.11" 46 | convert: 47 | dependency: transitive 48 | description: 49 | name: convert 50 | url: "https://pub.dartlang.org" 51 | source: hosted 52 | version: "2.1.1" 53 | crypto: 54 | dependency: transitive 55 | description: 56 | name: crypto 57 | url: "https://pub.dartlang.org" 58 | source: hosted 59 | version: "2.0.6" 60 | csslib: 61 | dependency: transitive 62 | description: 63 | name: csslib 64 | url: "https://pub.dartlang.org" 65 | source: hosted 66 | version: "0.16.1" 67 | entitas_ff: 68 | dependency: "direct main" 69 | description: 70 | path: "." 71 | ref: HEAD 72 | resolved-ref: "76c36dc484b331ca02c114ba12d964dd0c6ea336" 73 | url: "https://github.com/lucasccustodio/entitas_ff.git" 74 | source: git 75 | version: "0.2.2+3" 76 | flutter: 77 | dependency: "direct main" 78 | description: flutter 79 | source: sdk 80 | version: "0.0.0" 81 | flutter_test: 82 | dependency: "direct dev" 83 | description: flutter 84 | source: sdk 85 | version: "0.0.0" 86 | front_end: 87 | dependency: transitive 88 | description: 89 | name: front_end 90 | url: "https://pub.dartlang.org" 91 | source: hosted 92 | version: "0.1.19" 93 | glob: 94 | dependency: transitive 95 | description: 96 | name: glob 97 | url: "https://pub.dartlang.org" 98 | source: hosted 99 | version: "1.1.7" 100 | html: 101 | dependency: transitive 102 | description: 103 | name: html 104 | url: "https://pub.dartlang.org" 105 | source: hosted 106 | version: "0.14.0+2" 107 | http: 108 | dependency: transitive 109 | description: 110 | name: http 111 | url: "https://pub.dartlang.org" 112 | source: hosted 113 | version: "0.12.0+2" 114 | http_multi_server: 115 | dependency: transitive 116 | description: 117 | name: http_multi_server 118 | url: "https://pub.dartlang.org" 119 | source: hosted 120 | version: "2.1.0" 121 | http_parser: 122 | dependency: transitive 123 | description: 124 | name: http_parser 125 | url: "https://pub.dartlang.org" 126 | source: hosted 127 | version: "3.1.3" 128 | image_picker: 129 | dependency: "direct main" 130 | description: 131 | name: image_picker 132 | url: "https://pub.dartlang.org" 133 | source: hosted 134 | version: "0.6.0+17" 135 | io: 136 | dependency: transitive 137 | description: 138 | name: io 139 | url: "https://pub.dartlang.org" 140 | source: hosted 141 | version: "0.3.3" 142 | js: 143 | dependency: transitive 144 | description: 145 | name: js 146 | url: "https://pub.dartlang.org" 147 | source: hosted 148 | version: "0.6.1+1" 149 | json_rpc_2: 150 | dependency: transitive 151 | description: 152 | name: json_rpc_2 153 | url: "https://pub.dartlang.org" 154 | source: hosted 155 | version: "2.1.0" 156 | kernel: 157 | dependency: transitive 158 | description: 159 | name: kernel 160 | url: "https://pub.dartlang.org" 161 | source: hosted 162 | version: "0.3.19" 163 | logging: 164 | dependency: transitive 165 | description: 166 | name: logging 167 | url: "https://pub.dartlang.org" 168 | source: hosted 169 | version: "0.11.3+2" 170 | matcher: 171 | dependency: transitive 172 | description: 173 | name: matcher 174 | url: "https://pub.dartlang.org" 175 | source: hosted 176 | version: "0.12.5" 177 | meta: 178 | dependency: transitive 179 | description: 180 | name: meta 181 | url: "https://pub.dartlang.org" 182 | source: hosted 183 | version: "1.1.7" 184 | mime: 185 | dependency: transitive 186 | description: 187 | name: mime 188 | url: "https://pub.dartlang.org" 189 | source: hosted 190 | version: "0.9.6+3" 191 | multi_server_socket: 192 | dependency: transitive 193 | description: 194 | name: multi_server_socket 195 | url: "https://pub.dartlang.org" 196 | source: hosted 197 | version: "1.0.2" 198 | node_preamble: 199 | dependency: transitive 200 | description: 201 | name: node_preamble 202 | url: "https://pub.dartlang.org" 203 | source: hosted 204 | version: "1.4.5" 205 | package_config: 206 | dependency: transitive 207 | description: 208 | name: package_config 209 | url: "https://pub.dartlang.org" 210 | source: hosted 211 | version: "1.0.5" 212 | package_resolver: 213 | dependency: transitive 214 | description: 215 | name: package_resolver 216 | url: "https://pub.dartlang.org" 217 | source: hosted 218 | version: "1.0.10" 219 | path: 220 | dependency: transitive 221 | description: 222 | name: path 223 | url: "https://pub.dartlang.org" 224 | source: hosted 225 | version: "1.6.2" 226 | path_provider: 227 | dependency: "direct main" 228 | description: 229 | name: path_provider 230 | url: "https://pub.dartlang.org" 231 | source: hosted 232 | version: "1.2.0" 233 | pedantic: 234 | dependency: transitive 235 | description: 236 | name: pedantic 237 | url: "https://pub.dartlang.org" 238 | source: hosted 239 | version: "1.8.0+1" 240 | permission: 241 | dependency: "direct main" 242 | description: 243 | name: permission 244 | url: "https://pub.dartlang.org" 245 | source: hosted 246 | version: "0.1.2" 247 | pigment: 248 | dependency: transitive 249 | description: 250 | name: pigment 251 | url: "https://pub.dartlang.org" 252 | source: hosted 253 | version: "1.0.3" 254 | pool: 255 | dependency: transitive 256 | description: 257 | name: pool 258 | url: "https://pub.dartlang.org" 259 | source: hosted 260 | version: "1.4.0" 261 | pub_semver: 262 | dependency: transitive 263 | description: 264 | name: pub_semver 265 | url: "https://pub.dartlang.org" 266 | source: hosted 267 | version: "1.4.2" 268 | quiver: 269 | dependency: transitive 270 | description: 271 | name: quiver 272 | url: "https://pub.dartlang.org" 273 | source: hosted 274 | version: "2.0.3" 275 | sembast: 276 | dependency: "direct main" 277 | description: 278 | name: sembast 279 | url: "https://pub.dartlang.org" 280 | source: hosted 281 | version: "2.0.0+1" 282 | shelf: 283 | dependency: transitive 284 | description: 285 | name: shelf 286 | url: "https://pub.dartlang.org" 287 | source: hosted 288 | version: "0.7.5" 289 | shelf_packages_handler: 290 | dependency: transitive 291 | description: 292 | name: shelf_packages_handler 293 | url: "https://pub.dartlang.org" 294 | source: hosted 295 | version: "1.0.4" 296 | shelf_static: 297 | dependency: transitive 298 | description: 299 | name: shelf_static 300 | url: "https://pub.dartlang.org" 301 | source: hosted 302 | version: "0.2.8" 303 | shelf_web_socket: 304 | dependency: transitive 305 | description: 306 | name: shelf_web_socket 307 | url: "https://pub.dartlang.org" 308 | source: hosted 309 | version: "0.2.3" 310 | sky_engine: 311 | dependency: transitive 312 | description: flutter 313 | source: sdk 314 | version: "0.0.99" 315 | source_map_stack_trace: 316 | dependency: transitive 317 | description: 318 | name: source_map_stack_trace 319 | url: "https://pub.dartlang.org" 320 | source: hosted 321 | version: "1.1.5" 322 | source_maps: 323 | dependency: transitive 324 | description: 325 | name: source_maps 326 | url: "https://pub.dartlang.org" 327 | source: hosted 328 | version: "0.10.8" 329 | source_span: 330 | dependency: transitive 331 | description: 332 | name: source_span 333 | url: "https://pub.dartlang.org" 334 | source: hosted 335 | version: "1.5.5" 336 | stack_trace: 337 | dependency: transitive 338 | description: 339 | name: stack_trace 340 | url: "https://pub.dartlang.org" 341 | source: hosted 342 | version: "1.9.3" 343 | stream_channel: 344 | dependency: transitive 345 | description: 346 | name: stream_channel 347 | url: "https://pub.dartlang.org" 348 | source: hosted 349 | version: "2.0.0" 350 | string_scanner: 351 | dependency: transitive 352 | description: 353 | name: string_scanner 354 | url: "https://pub.dartlang.org" 355 | source: hosted 356 | version: "1.0.4" 357 | synchronized: 358 | dependency: transitive 359 | description: 360 | name: synchronized 361 | url: "https://pub.dartlang.org" 362 | source: hosted 363 | version: "2.1.0+1" 364 | term_glyph: 365 | dependency: transitive 366 | description: 367 | name: term_glyph 368 | url: "https://pub.dartlang.org" 369 | source: hosted 370 | version: "1.1.0" 371 | test: 372 | dependency: "direct dev" 373 | description: 374 | name: test 375 | url: "https://pub.dartlang.org" 376 | source: hosted 377 | version: "1.6.3" 378 | test_api: 379 | dependency: transitive 380 | description: 381 | name: test_api 382 | url: "https://pub.dartlang.org" 383 | source: hosted 384 | version: "0.2.5" 385 | test_core: 386 | dependency: transitive 387 | description: 388 | name: test_core 389 | url: "https://pub.dartlang.org" 390 | source: hosted 391 | version: "0.2.5" 392 | tinycolor: 393 | dependency: "direct main" 394 | description: 395 | name: tinycolor 396 | url: "https://pub.dartlang.org" 397 | source: hosted 398 | version: "1.0.2" 399 | typed_data: 400 | dependency: transitive 401 | description: 402 | name: typed_data 403 | url: "https://pub.dartlang.org" 404 | source: hosted 405 | version: "1.1.6" 406 | vector_math: 407 | dependency: transitive 408 | description: 409 | name: vector_math 410 | url: "https://pub.dartlang.org" 411 | source: hosted 412 | version: "2.0.8" 413 | vm_service_client: 414 | dependency: transitive 415 | description: 416 | name: vm_service_client 417 | url: "https://pub.dartlang.org" 418 | source: hosted 419 | version: "0.2.6+2" 420 | watcher: 421 | dependency: transitive 422 | description: 423 | name: watcher 424 | url: "https://pub.dartlang.org" 425 | source: hosted 426 | version: "0.9.7+12" 427 | web_socket_channel: 428 | dependency: transitive 429 | description: 430 | name: web_socket_channel 431 | url: "https://pub.dartlang.org" 432 | source: hosted 433 | version: "1.0.14" 434 | yaml: 435 | dependency: transitive 436 | description: 437 | name: yaml 438 | url: "https://pub.dartlang.org" 439 | source: hosted 440 | version: "2.1.16" 441 | sdks: 442 | dart: ">=2.2.2 <3.0.0" 443 | flutter: ">=1.5.0 <2.0.0" 444 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: notebulk 2 | description: A note taking application for your daily needs 3 | 4 | # The following defines the version and build number for your application. 5 | # A version number is three numbers separated by dots, like 1.2.43 6 | # followed by an optional build number separated by a +. 7 | # Both the version and the builder number may be overridden in flutter 8 | # build by specifying --build-name and --build-number, respectively. 9 | # In Android, build-name is used as versionName while build-number used as versionCode. 10 | # Read more about Android versioning at https://developer.android.com/studio/publish/versioning 11 | # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. 12 | # Read more about iOS versioning at 13 | # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html 14 | version: 1.2.0+1 15 | 16 | environment: 17 | sdk: ">=2.2.2 <3.0.0" 18 | 19 | dependencies: 20 | flutter: 21 | sdk: flutter 22 | entitas_ff: 23 | git: 24 | url: https://github.com/lucasccustodio/entitas_ff.git 25 | ref: master 26 | 27 | #Uncomment if Desktop 28 | #file_chooser: 29 | #path: /home/lucascc/Dev/flutter-desktop-embedding/plugins/file_chooser 30 | 31 | #image: ^2.1.4 32 | kt_dart: ^0.6.2 33 | hive: ^0.4.1+1 34 | path_provider: ^1.1.2 35 | tinycolor: ^1.0.2 36 | permission: ^0.1.2 37 | image_picker: ^0.6.0+17 38 | flutter_staggered_grid_view: ^0.3.0 39 | #extended_text_field: ^0.4.3 40 | flutter_localizations: 41 | sdk: flutter 42 | 43 | dev_dependencies: 44 | flutter_test: 45 | sdk: flutter 46 | test: 47 | flutter_launcher_icons: "^0.7.2" 48 | 49 | flutter_icons: 50 | android: true 51 | ios: false 52 | image_path: "assets/icon-adaptive.png" 53 | adaptive_icon_background: "#FFC83660" 54 | adaptive_icon_foreground: "assets/icon-adaptive.png" 55 | 56 | 57 | # For information on the generic Dart part of this file, see the 58 | # following page: https://dart.dev/tools/pub/pubspec 59 | 60 | # The following section is specific to Flutter. 61 | flutter: 62 | 63 | # The following line ensures that the Material Icons font is 64 | # included with your application, so that you can use the icons in 65 | # the material Icons class. 66 | uses-material-design: true 67 | 68 | # To add assets to your application, add an assets section, like this: 69 | # assets: 70 | # - images/a_dot_burr.jpeg 71 | # - images/a_dot_ham.jpeg 72 | 73 | # An image asset can refer to one or more resolution-specific "variants", see 74 | # https://flutter.dev/assets-and-images/#resolution-aware. 75 | 76 | # For details regarding adding assets from package dependencies, see 77 | # https://flutter.dev/assets-and-images/#from-packages 78 | 79 | # To add custom fonts to your application, add a fonts section here, 80 | # in this "flutter" section. Each entry in this list should have a 81 | # "family" key with the font family name, and a "fonts" key with a 82 | # list giving the asset and other descriptors for the font. For 83 | # example: 84 | assets: 85 | - assets/icon-adaptive.png 86 | fonts: 87 | - family: AppIcons 88 | fonts: 89 | - asset: fonts/AppIcons.ttf 90 | - family: Palanquin 91 | fonts: 92 | - asset: fonts/Palanquin-Thin.ttf 93 | weight: 100 94 | - asset: fonts/Palanquin-Light.ttf 95 | weight: 300 96 | - asset: fonts/Palanquin-Regular.ttf 97 | weight: 400 98 | - asset: fonts/Palanquin-Medium.ttf 99 | weight: 500 100 | - asset: fonts/Palanquin-Bold.ttf 101 | weight: 700 102 | - family: PalanquinDark 103 | fonts: 104 | - asset: fonts/PalanquinDark-Bold.ttf 105 | weight: 700 106 | # 107 | # For details regarding fonts from package dependencies, 108 | # see https://flutter.dev/custom-fonts/#from-packages 109 | -------------------------------------------------------------------------------- /test/entitas_basic_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:entitas_ff/entitas_ff.dart'; 3 | import 'package:flutter_test/flutter_test.dart'; 4 | 5 | //An UniqueComponent can only be held by a single entity at a time 6 | class TestComponent extends UniqueComponent { 7 | TestComponent(this.counter); 8 | 9 | final int counter; 10 | } 11 | 12 | void main() async { 13 | testWidgets('Should update counter when fab is tapped', (widgetTester) async { 14 | //Instantiate our EntityManager 15 | final testEntityManager = EntityManager()..setUnique(TestComponent(0)); 16 | //Instantiate TestComponent and set counter to 0 17 | 18 | await widgetTester.pumpWidget( 19 | //InheritedWidget that will provide our EntityManager to the subtree 20 | EntityManagerProvider( 21 | entityManager: testEntityManager, 22 | child: TestApp(), 23 | )); 24 | 25 | //By default counter should be at 0 26 | expect(find.text('Counter: 0'), findsOneWidget); 27 | 28 | //Tap to increase counter 29 | await widgetTester.tap(find.text('Increase counter')); 30 | 31 | //Trigger a frame 32 | await widgetTester.pump(); 33 | 34 | //Now counter should be at 1 35 | expect(find.text('Counter: 1'), findsOneWidget); 36 | }); 37 | } 38 | 39 | class TestApp extends StatelessWidget { 40 | @override 41 | Widget build(BuildContext context) { 42 | return MaterialApp( 43 | title: 'Testing', 44 | theme: ThemeData( 45 | primarySwatch: Colors.blue, 46 | ), 47 | home: Scaffold( 48 | appBar: AppBar( 49 | title: Text('Testing'), 50 | ), 51 | body: Center( 52 | //An reactive widget that will rebuild when the provided Entity's components are modified 53 | child: EntityObservingWidget( 54 | //getUniqueEntity will retrieve the entity which is currently holding the corresponding UniqueComponent, not that component itself as EntityObservingWidget is expecting an Entity, if the UniqueComponent isn't currently set it will return null. 55 | provider: (entityManager) => 56 | entityManager.getUniqueEntity(), 57 | //The builder function must always return a Widget 58 | builder: (entity, context) => 59 | Text('Counter: ${entity.get().counter}'), 60 | ), 61 | ), 62 | floatingActionButton: FloatingActionButton( 63 | child: Text('Increase counter'), 64 | onPressed: () { 65 | //Retrieve the underlying EntityManager 66 | final entityManager = 67 | EntityManagerProvider.of(context).entityManager; 68 | //Retrieve the UniqueComponent, not it's owner Entity, and the current counter value 69 | final counter = entityManager.getUnique().counter; 70 | //Update the UniqueComponent by creating a new instance with counter incremented, which will rebuild all EntityObservingWidgets currently observing for changes on this UniqueComponent 71 | entityManager.setUnique(TestComponent(counter + 1)); 72 | }, 73 | ), 74 | ), 75 | ); 76 | } 77 | } 78 | --------------------------------------------------------------------------------