├── .firebase └── hosting.YnVpbGRcd2Vi.cache ├── .firebaserc ├── .github └── workflows │ └── dart.yml ├── .gitignore ├── .metadata ├── .vscode └── launch.json ├── README.md ├── analysis_options.yaml ├── android ├── app │ ├── build.gradle │ └── src │ │ ├── debug │ │ └── AndroidManifest.xml │ │ ├── main │ │ ├── AndroidManifest.xml │ │ ├── kotlin │ │ │ └── com │ │ │ │ └── example │ │ │ │ └── fun_with_flutter_website │ │ │ │ └── MainActivity.kt │ │ └── res │ │ │ ├── drawable │ │ │ └── launch_background.xml │ │ │ ├── mipmap-hdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-mdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xhdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xxhdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xxxhdpi │ │ │ └── ic_launcher.png │ │ │ └── values │ │ │ └── styles.xml │ │ └── profile │ │ └── AndroidManifest.xml ├── build.gradle ├── gradle.properties ├── gradle │ └── wrapper │ │ └── gradle-wrapper.properties └── settings.gradle ├── assets ├── icons │ └── CustomIcons.ttf └── images │ ├── course_background.png │ └── funwith_favicon.png ├── firebase.json ├── 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.swift │ ├── 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 │ └── Runner-Bridging-Header.h ├── lib ├── application │ ├── auth │ │ ├── auth_bloc.dart │ │ ├── auth_bloc.freezed.dart │ │ ├── auth_event.dart │ │ ├── auth_state.dart │ │ └── sign_in_form │ │ │ ├── sign_in_form_bloc.dart │ │ │ ├── sign_in_form_bloc.freezed.dart │ │ │ ├── sign_in_form_event.dart │ │ │ └── sign_in_form_state.dart │ ├── blog │ │ ├── blog_bloc.dart │ │ ├── blog_bloc.freezed.dart │ │ ├── blog_event.dart │ │ └── blog_state.dart │ ├── contact_form │ │ └── bloc │ │ │ ├── contact_form_bloc.dart │ │ │ ├── contact_form_bloc.freezed.dart │ │ │ ├── contact_form_event.dart │ │ │ └── contact_form_state.dart │ ├── filtered_blog │ │ ├── filtered_blog_bloc.dart │ │ ├── filtered_blog_bloc.freezed.dart │ │ ├── filtered_blog_event.dart │ │ └── filtered_blog_state.dart │ ├── page │ │ ├── page_bloc.dart │ │ ├── page_bloc.freezed.dart │ │ ├── page_event.dart │ │ └── page_state.dart │ ├── simple_bloc_delegate.dart │ └── theme │ │ └── bloc │ │ ├── theme_bloc.dart │ │ ├── theme_bloc.freezed.dart │ │ ├── theme_event.dart │ │ └── theme_state.dart ├── domain │ ├── auth │ │ ├── auth_failure.dart │ │ ├── auth_failure.freezed.dart │ │ ├── i_auth_facade.dart │ │ ├── user.dart │ │ ├── user.freezed.dart │ │ └── value_objects.dart │ ├── blog │ │ ├── blog.dart │ │ ├── blog.freezed.dart │ │ ├── blog.g.dart │ │ ├── blog_failure.dart │ │ ├── blog_failure.freezed.dart │ │ ├── i_blog_repository.dart │ │ ├── post_data.dart │ │ ├── post_data.freezed.dart │ │ ├── post_data.g.dart │ │ ├── tag.dart │ │ ├── tag.freezed.dart │ │ └── tag.g.dart │ ├── contact_form │ │ ├── contact_form.dart │ │ ├── contact_form.freezed.dart │ │ ├── contact_form_failure.dart │ │ ├── contact_form_failure.freezed.dart │ │ ├── i_contact_form_repository.dart │ │ └── value_objects.dart │ └── core │ │ ├── errors.dart │ │ ├── failures.dart │ │ ├── failures.freezed.dart │ │ ├── value_objects.dart │ │ ├── value_transformers.dart │ │ └── value_validators.dart ├── infrastructure │ ├── auth │ │ ├── firebase_auth_facade.dart │ │ ├── firebase_auth_service.dart │ │ └── firebase_user_mapper.dart │ ├── blog │ │ ├── blog_api.dart │ │ ├── blog_repository.dart │ │ ├── dev_blog_repository.dart │ │ └── register_module.dart │ ├── contact_form │ │ ├── contact_form_api.dart │ │ └── contact_form_repository.dart │ ├── core │ │ ├── firebase_injectable_module.dart │ │ └── urls.dart │ └── url_repository.dart ├── injection.dart ├── injection.iconfig.dart ├── main.dart └── presentation │ ├── app │ ├── app.dart │ └── components │ │ ├── app_page.dart │ │ └── error_listener.dart │ ├── common │ ├── accent_button.dart │ ├── error_widget.dart │ ├── header.dart │ ├── info_bar.dart │ └── loading_indicator.dart │ ├── core │ ├── adaptive_dialog.dart │ ├── adaptive_scaffold.dart │ ├── app_widget.dart │ ├── constants.dart │ ├── extensions.dart │ ├── html_element_widget.dart │ ├── iframe_widget.dart │ ├── notification_helper.dart │ ├── themes.dart │ └── utils │ │ ├── custom_icons_icons.dart │ │ ├── tag_name_generator.dart │ │ └── url_handler.dart │ ├── pages │ ├── blog │ │ ├── blog_page.dart │ │ └── widgets │ │ │ ├── blog_post_card.dart │ │ │ ├── blog_posts.dart │ │ │ └── tag_widgets.dart │ ├── contact │ │ ├── contact_page.dart │ │ └── widgets │ │ │ └── contact_us_form.dart │ └── home │ │ ├── components │ │ └── sliver_course_header.dart │ │ ├── home_page.dart │ │ └── widgets │ │ └── sliver_course_header.dart │ ├── routes │ ├── router.dart │ └── router.gr.dart │ └── sign_in │ ├── sign_in_page.dart │ └── widgets │ └── sign_in_form.dart ├── pubspec.lock ├── pubspec.yaml └── web ├── assets ├── CustomIcons.ttf ├── DMSerifDisplay-Regular.ttf ├── FontManifest.json ├── MaterialIcons-Regular.ttf ├── OpenSans-Regular.ttf ├── WorkSans-Regular.ttf └── images │ ├── course_background.png │ └── funwith_favicon.png └── index.html /.firebase/hosting.YnVpbGRcd2Vi.cache: -------------------------------------------------------------------------------- 1 | 404.html,1593751626256,daa499dd96d8229e73235345702ba32f0793f0c8e5c0d30e40e37a5872be57aa 2 | assets/CustomIcons.ttf,1589042644000,ca4c0d29da7794a9d333f56daede6dc00cb6cead7a00d17f8375ac9d8868aa40 3 | assets/DMSerifDisplay-Regular.ttf,1589042644000,fe46e71d87a9c180fbe917ce28eeb66845adeb0546187a69d02f82c9d80d767b 4 | assets/FontManifest.json,1589042644000,f324383d06cc87dd3f3af0d031f55242fb62b14b45b789511836196da6eef86a 5 | assets/MaterialIcons-Regular.ttf,1589042644000,f33d9640ccc280d7d0a2ab55f2a4e25ebf548c3c7027b97964168231c101c2bb 6 | assets/OpenSans-Regular.ttf,1589042644000,629804cdd416aa0ef22aaece8d5399d7fdc1b621b4d6450eaa5b036482d69b6d 7 | assets/WorkSans-Regular.ttf,1589042644000,884acedb1c889e60633f6c873556aae07ada0011a487752381d21c88d22dce72 8 | assets/assets/icons/CustomIcons.ttf,1589042644000,ca4c0d29da7794a9d333f56daede6dc00cb6cead7a00d17f8375ac9d8868aa40 9 | assets/assets/images/course_background.png,1593364023322,4c1a11af341434552f30f8c381204dabc167a8dec0c37cfa54a309eec2ecbe78 10 | assets/assets/images/funwith_favicon.png,1589042644000,df93db2263f38f11c2fe13cf864eccfa9fb6704dbdb6a2ce8a7a2c02556db089 11 | assets/fonts/MaterialIcons-Regular.ttf,1589869440288,91d9d27a9faaaa741c2bb316421210a98dce9d7ee6c59f323e223c82216e2c3b 12 | assets/images/course_background.png,1593623056926,4c1a11af341434552f30f8c381204dabc167a8dec0c37cfa54a309eec2ecbe78 13 | assets/images/funwith_favicon.png,1589042644000,df93db2263f38f11c2fe13cf864eccfa9fb6704dbdb6a2ce8a7a2c02556db089 14 | assets/packages/cupertino_icons/assets/CupertinoIcons.ttf,1589869306342,894da28a77592818df91b2299df5c1005b8ab18adcd6798113667e58eaaa5d10 15 | flutter_service_worker.js,1593753908207,38c6086d3c204cf9a49b6cee0b2f24d26bdb83ed097bc889c01a93582e486915 16 | index.html,1593750463565,5898eb1266a6764b15cb476e5687d8877f9877d92663d8065eedfa62e7fec181 17 | assets/AssetManifest.json,1593753907323,d027569ad0ce27b9cb5a90c7560f3b8ca595cb7bcfa197adfa651ce1432105bf 18 | assets/NOTICES,1593753907324,02dbe4a4df9d39c96f8a6a7645852a1fb1ab741687755a85838f076eed99fa56 19 | main.dart.js,1593753905704,267797e23cc49a0c8bf7d86ded0bd148475da8270fe9f64ae2612877741f1f70 20 | main.dart.js.map,1593753906800,18b200f683c6be9929d1dbe1c3033436ec4032a482a51c2b81cc3f3bcde2be7b 21 | -------------------------------------------------------------------------------- /.firebaserc: -------------------------------------------------------------------------------- 1 | { 2 | "projects": { 3 | "default": "fun-with" 4 | }, 5 | "targets": { 6 | "fun-with": { 7 | "hosting": { 8 | "app": [ 9 | "fun-with" 10 | ] 11 | } 12 | } 13 | } 14 | } -------------------------------------------------------------------------------- /.github/workflows/dart.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | push: 4 | branches: 5 | - master 6 | pull_request: 7 | branches: 8 | - master 9 | jobs: 10 | build_web: 11 | name: Build Flutter (Web) 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v1 15 | - uses: subosito/flutter-action@v1 16 | with: 17 | channel: 'master' 18 | - run: flutter pub get 19 | - run: flutter config --enable-web 20 | - run: flutter build web 21 | - name: Archive Production Artifact 22 | uses: actions/upload-artifact@master 23 | with: 24 | name: web-build 25 | path: build/web 26 | deploy_web: 27 | name: Deploy Web to Firebase Hosting 28 | needs: build_web 29 | runs-on: ubuntu-latest 30 | steps: 31 | - name: Checkout Repo 32 | uses: actions/checkout@master 33 | - name: Download Artifact 34 | uses: actions/download-artifact@master 35 | with: 36 | name: web-build 37 | - name: Deploy to Firebase 38 | uses: w9jds/firebase-action@master 39 | with: 40 | args: deploy --only hosting:app 41 | env: 42 | FIREBASE_TOKEN: ${{ secrets.FIREBASE_TOKEN }} 43 | PROJECT_ID: default 44 | -------------------------------------------------------------------------------- /.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 | .flutter-plugins-dependencies 28 | .packages 29 | .pub-cache/ 30 | .pub/ 31 | /build/ 32 | 33 | # Android related 34 | **/android/**/gradle-wrapper.jar 35 | **/android/.gradle 36 | **/android/captures/ 37 | **/android/gradlew 38 | **/android/gradlew.bat 39 | **/android/local.properties 40 | **/android/**/GeneratedPluginRegistrant.java 41 | 42 | # iOS/XCode related 43 | **/ios/**/*.mode1v3 44 | **/ios/**/*.mode2v3 45 | **/ios/**/*.moved-aside 46 | **/ios/**/*.pbxuser 47 | **/ios/**/*.perspectivev3 48 | **/ios/**/*sync/ 49 | **/ios/**/.sconsign.dblite 50 | **/ios/**/.tags* 51 | **/ios/**/.vagrant/ 52 | **/ios/**/DerivedData/ 53 | **/ios/**/Icon? 54 | **/ios/**/Pods/ 55 | **/ios/**/.symlinks/ 56 | **/ios/**/profile 57 | **/ios/**/xcuserdata 58 | **/ios/.generated/ 59 | **/ios/Flutter/App.framework 60 | **/ios/Flutter/Flutter.framework 61 | **/ios/Flutter/Generated.xcconfig 62 | **/ios/Flutter/app.flx 63 | **/ios/Flutter/app.zip 64 | **/ios/Flutter/flutter_assets/ 65 | **/ios/Flutter/flutter_export_environment.sh 66 | **/ios/ServiceDefinitions.json 67 | **/ios/Runner/GeneratedPluginRegistrant.* 68 | 69 | # Web related 70 | lib/generated_plugin_registrant.dart 71 | 72 | # Exceptions to above rules. 73 | !**/ios/**/default.mode1v3 74 | !**/ios/**/default.mode2v3 75 | !**/ios/**/default.pbxuser 76 | !**/ios/**/default.perspectivev3 77 | !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages 78 | -------------------------------------------------------------------------------- /.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: af07bb50faecbfe7d576113058c94cae0e21b91e 8 | channel: master 9 | 10 | project_type: app 11 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Flutter", 9 | "request": "launch", 10 | "type": "dart", 11 | "args": [ 12 | "--web-hostname", 13 | "localhost", 14 | "--web-port", 15 | "5000" 16 | ] 17 | } 18 | ] 19 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Fun with Flutter Website 2 | 3 | Source code to the [Fun with Flutter website](https://funwith.app), a companion application to the [Fun with Flutter YouTube Channel](https://www.youtube.com/funwithflutter) 4 | 5 | To serve as a learning opportunity for myself, as well as other. Cheers. 6 | -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | include: package:lint/analysis_options.yaml 2 | 3 | # Specify analysis options. 4 | # 5 | # For a list of lints, see: http://dart-lang.github.io/linter/lints/ 6 | # See the configuration guide for more 7 | # https://github.com/dart-lang/sdk/tree/master/pkg/analyzer#configuring-the-analyzer 8 | 9 | analyzer: 10 | exclude: 11 | - "**/*.g.dart" 12 | - "**/*.gr.dart" 13 | - "**/*.iconfig.dart" 14 | - "**/*.freezed.dart" 15 | - "**/firebase_auth_facade.dart" 16 | # TODO: remove firebase_auth_facade from excludes once Firebase throws 17 | # [PlatformException] or some other exception that can be caught 18 | # - 'bin/cache/**' 19 | # - "lib/**/*.g.dart" 20 | # - "lib/**/*.gr.dart" 21 | # - "lib/**/*.freezed.dart" 22 | # - "lib/injection.iconfig.dart" 23 | strong-mode: 24 | implicit-casts: false 25 | implicit-dynamic: false 26 | errors: 27 | # TODO: enable these two lint once firebase_auth throws 28 | # [PlatformException] or some other exception that can be caught 29 | # argument_type_not_assignable: ignore 30 | # switch_expression_not_assignable: ignore 31 | 32 | # treat missing required parameters as a warning (not a hint) 33 | missing_required_param: error 34 | # treat missing returns as a warning (not a hint) 35 | missing_return: error 36 | # allow having TODOs in the code 37 | todo: ignore 38 | # Ignore analyzer hints for updating pubspecs when using Future or 39 | # Stream and not importing dart:async 40 | # Please see https://github.com/flutter/flutter/pull/24528 for details. 41 | sdk_version_async_exported_from_core: ignore 42 | 43 | linter: 44 | rules: 45 | - prefer_relative_imports 46 | -------------------------------------------------------------------------------- /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 plugin: 'kotlin-android' 26 | apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" 27 | 28 | android { 29 | compileSdkVersion 28 30 | 31 | sourceSets { 32 | main.java.srcDirs += 'src/main/kotlin' 33 | } 34 | 35 | lintOptions { 36 | disable 'InvalidPackage' 37 | } 38 | 39 | defaultConfig { 40 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). 41 | applicationId "com.example.fun_with_flutter_website" 42 | minSdkVersion 16 43 | targetSdkVersion 28 44 | versionCode flutterVersionCode.toInteger() 45 | versionName flutterVersionName 46 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" 47 | } 48 | 49 | buildTypes { 50 | release { 51 | // TODO: Add your own signing config for the release build. 52 | // Signing with the debug keys for now, so `flutter run --release` works. 53 | signingConfig signingConfigs.debug 54 | } 55 | } 56 | } 57 | 58 | flutter { 59 | source '../..' 60 | } 61 | 62 | dependencies { 63 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 64 | testImplementation 'junit:junit:4.12' 65 | androidTestImplementation 'com.android.support.test:runner:1.0.2' 66 | androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' 67 | } 68 | -------------------------------------------------------------------------------- /android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 9 | 13 | 20 | 24 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /android/app/src/main/kotlin/com/example/fun_with_flutter_website/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.example.fun_with_flutter_website 2 | 3 | import android.os.Bundle 4 | 5 | import io.flutter.app.FlutterActivity 6 | import io.flutter.plugins.GeneratedPluginRegistrant 7 | 8 | class MainActivity: FlutterActivity() { 9 | override fun onCreate(savedInstanceState: Bundle?) { 10 | super.onCreate(savedInstanceState) 11 | GeneratedPluginRegistrant.registerWith(this) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/funwithflutter/fun-with-flutter-website/b163e23345901b1937c29da1d7bc18ad7283b879/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/funwithflutter/fun-with-flutter-website/b163e23345901b1937c29da1d7bc18ad7283b879/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/funwithflutter/fun-with-flutter-website/b163e23345901b1937c29da1d7bc18ad7283b879/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/funwithflutter/fun-with-flutter-website/b163e23345901b1937c29da1d7bc18ad7283b879/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/funwithflutter/fun-with-flutter-website/b163e23345901b1937c29da1d7bc18ad7283b879/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /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 | ext.kotlin_version = '1.2.71' 3 | repositories { 4 | google() 5 | jcenter() 6 | } 7 | 8 | dependencies { 9 | classpath 'com.android.tools.build:gradle:3.2.1' 10 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 11 | } 12 | } 13 | 14 | allprojects { 15 | repositories { 16 | google() 17 | jcenter() 18 | } 19 | } 20 | 21 | rootProject.buildDir = '../build' 22 | subprojects { 23 | project.buildDir = "${rootProject.buildDir}/${project.name}" 24 | } 25 | subprojects { 26 | project.evaluationDependsOn(':app') 27 | } 28 | 29 | task clean(type: Delete) { 30 | delete rootProject.buildDir 31 | } 32 | -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx1536M 2 | 3 | -------------------------------------------------------------------------------- /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-4.10.2-all.zip 7 | -------------------------------------------------------------------------------- /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/icons/CustomIcons.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/funwithflutter/fun-with-flutter-website/b163e23345901b1937c29da1d7bc18ad7283b879/assets/icons/CustomIcons.ttf -------------------------------------------------------------------------------- /assets/images/course_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/funwithflutter/fun-with-flutter-website/b163e23345901b1937c29da1d7bc18ad7283b879/assets/images/course_background.png -------------------------------------------------------------------------------- /assets/images/funwith_favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/funwithflutter/fun-with-flutter-website/b163e23345901b1937c29da1d7bc18ad7283b879/assets/images/funwith_favicon.png -------------------------------------------------------------------------------- /firebase.json: -------------------------------------------------------------------------------- 1 | { 2 | "hosting": { 3 | "target": "app", 4 | "public": "build/web", 5 | "ignore": [ 6 | "firebase.json", 7 | "**/.*", 8 | "**/node_modules/**" 9 | ], 10 | "headers": [ 11 | { 12 | "source": "**", 13 | "headers": [ 14 | { 15 | "key": "Cache-Control", 16 | "value": "public, no-cache, max-age=31536000" 17 | } 18 | ] 19 | } 20 | ] 21 | } 22 | } -------------------------------------------------------------------------------- /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.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import Flutter 3 | 4 | @UIApplicationMain 5 | @objc class AppDelegate: FlutterAppDelegate { 6 | override func application( 7 | _ application: UIApplication, 8 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? 9 | ) -> Bool { 10 | GeneratedPluginRegistrant.register(with: self) 11 | return super.application(application, didFinishLaunchingWithOptions: launchOptions) 12 | } 13 | } 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/funwithflutter/fun-with-flutter-website/b163e23345901b1937c29da1d7bc18ad7283b879/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/funwithflutter/fun-with-flutter-website/b163e23345901b1937c29da1d7bc18ad7283b879/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/funwithflutter/fun-with-flutter-website/b163e23345901b1937c29da1d7bc18ad7283b879/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/funwithflutter/fun-with-flutter-website/b163e23345901b1937c29da1d7bc18ad7283b879/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/funwithflutter/fun-with-flutter-website/b163e23345901b1937c29da1d7bc18ad7283b879/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/funwithflutter/fun-with-flutter-website/b163e23345901b1937c29da1d7bc18ad7283b879/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/funwithflutter/fun-with-flutter-website/b163e23345901b1937c29da1d7bc18ad7283b879/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/funwithflutter/fun-with-flutter-website/b163e23345901b1937c29da1d7bc18ad7283b879/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/funwithflutter/fun-with-flutter-website/b163e23345901b1937c29da1d7bc18ad7283b879/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/funwithflutter/fun-with-flutter-website/b163e23345901b1937c29da1d7bc18ad7283b879/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/funwithflutter/fun-with-flutter-website/b163e23345901b1937c29da1d7bc18ad7283b879/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/funwithflutter/fun-with-flutter-website/b163e23345901b1937c29da1d7bc18ad7283b879/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/funwithflutter/fun-with-flutter-website/b163e23345901b1937c29da1d7bc18ad7283b879/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/funwithflutter/fun-with-flutter-website/b163e23345901b1937c29da1d7bc18ad7283b879/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/funwithflutter/fun-with-flutter-website/b163e23345901b1937c29da1d7bc18ad7283b879/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/funwithflutter/fun-with-flutter-website/b163e23345901b1937c29da1d7bc18ad7283b879/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/funwithflutter/fun-with-flutter-website/b163e23345901b1937c29da1d7bc18ad7283b879/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/funwithflutter/fun-with-flutter-website/b163e23345901b1937c29da1d7bc18ad7283b879/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 | fun_with_flutter_website 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/Runner-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | #import "GeneratedPluginRegistrant.h" -------------------------------------------------------------------------------- /lib/application/auth/auth_bloc.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:bloc/bloc.dart'; 4 | import 'package:dartz/dartz.dart'; 5 | import 'package:flutter/foundation.dart'; 6 | import 'package:freezed_annotation/freezed_annotation.dart'; 7 | import 'package:injectable/injectable.dart'; 8 | import 'package:meta/meta.dart'; 9 | 10 | import '../../domain/auth/i_auth_facade.dart'; 11 | import '../../domain/auth/user.dart'; 12 | 13 | part 'auth_bloc.freezed.dart'; 14 | part 'auth_event.dart'; 15 | part 'auth_state.dart'; 16 | 17 | @injectable 18 | class AuthBloc extends Bloc { 19 | final IAuthFacade _authFacade; 20 | 21 | AuthBloc(this._authFacade); 22 | 23 | @override 24 | AuthState get initialState { 25 | _subscribeToAuthStateChanged(); 26 | return const AuthState.initial(); 27 | } 28 | 29 | StreamSubscription> _authStateStreamSubscription; 30 | 31 | void _subscribeToAuthStateChanged() { 32 | debugPrint('Subscribing to auth state changes'); 33 | _authStateStreamSubscription = 34 | _authFacade.onAuthStateChanged.listen(_onAuthStateChangedListener); 35 | } 36 | 37 | void _unsubscribeFromAuthStateChanged() { 38 | debugPrint('Unsubscribing from AuthState changes'); 39 | _authStateStreamSubscription.cancel(); 40 | } 41 | 42 | void _onAuthStateChangedListener(Option userOption) { 43 | debugPrint('AuthState changed'); 44 | final isSignedIn = userOption.fold( 45 | () => false, 46 | (user) => true, 47 | ); 48 | 49 | /// Only add onAuthStateChanged event if [isSignedIn] does not 50 | /// match the current auth state 51 | if ((isSignedIn && state is Authenticated) || 52 | (!isSignedIn && state is Unauthenticated)) { 53 | return; 54 | } 55 | add(AuthEvent.onAuthStateChanged(isSignedIn: isSignedIn)); 56 | } 57 | 58 | @override 59 | Future close() { 60 | _unsubscribeFromAuthStateChanged(); 61 | return super.close(); 62 | } 63 | 64 | @override 65 | Stream mapEventToState( 66 | AuthEvent event, 67 | ) async* { 68 | yield* event.map( 69 | authCheckRequested: (e) async* { 70 | final userOption = await _authFacade.getSignedInUser(); 71 | yield userOption.fold( 72 | () => const AuthState.unauthenticated(), 73 | (_) => const AuthState.authenticated(), 74 | ); 75 | }, 76 | signOut: (e) async* { 77 | await _authFacade.signOut(); 78 | yield const AuthState.unauthenticated(); 79 | }, 80 | onAuthStateChanged: (e) async* { 81 | if (e.isSignedIn) { 82 | yield const AuthState.authenticated(); 83 | } else { 84 | yield const AuthState.unauthenticated(); 85 | } 86 | }, 87 | ); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /lib/application/auth/auth_event.dart: -------------------------------------------------------------------------------- 1 | part of 'auth_bloc.dart'; 2 | 3 | @freezed 4 | abstract class AuthEvent with _$AuthEvent { 5 | const factory AuthEvent.authCheckRequested() = AuthCheckRequested; 6 | const factory AuthEvent.signOut() = SignOut; 7 | const factory AuthEvent.onAuthStateChanged({bool isSignedIn}) = OnAuthStateChanged; 8 | } 9 | -------------------------------------------------------------------------------- /lib/application/auth/auth_state.dart: -------------------------------------------------------------------------------- 1 | part of 'auth_bloc.dart'; 2 | 3 | @freezed 4 | abstract class AuthState with _$AuthState { 5 | const factory AuthState.initial() = Initial; 6 | const factory AuthState.authenticated() = Authenticated; 7 | const factory AuthState.unauthenticated() = Unauthenticated; 8 | } 9 | -------------------------------------------------------------------------------- /lib/application/auth/sign_in_form/sign_in_form_bloc.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:bloc/bloc.dart'; 4 | import 'package:dartz/dartz.dart'; 5 | import 'package:flutter/foundation.dart'; 6 | import 'package:freezed_annotation/freezed_annotation.dart'; 7 | import 'package:injectable/injectable.dart'; 8 | import 'package:meta/meta.dart'; 9 | 10 | import '../../../domain/auth/auth_failure.dart'; 11 | import '../../../domain/auth/i_auth_facade.dart'; 12 | import '../../../domain/auth/value_objects.dart'; 13 | 14 | part 'sign_in_form_bloc.freezed.dart'; 15 | part 'sign_in_form_event.dart'; 16 | part 'sign_in_form_state.dart'; 17 | 18 | @injectable 19 | class SignInFormBloc extends Bloc { 20 | final IAuthFacade _authFacade; 21 | 22 | SignInFormBloc(this._authFacade); 23 | 24 | @override 25 | SignInFormState get initialState => SignInFormState.initial(); 26 | 27 | @override 28 | Stream mapEventToState( 29 | SignInFormEvent event, 30 | ) async* { 31 | yield* event.map( 32 | emailChanged: (e) async* { 33 | yield state.copyWith( 34 | emailAddress: EmailAddress(e.emailStr), 35 | authFailureOrSuccessOption: none(), 36 | ); 37 | }, 38 | passwordChanged: (e) async* { 39 | yield state.copyWith( 40 | password: Password(e.passwordStr), 41 | authFailureOrSuccessOption: none(), 42 | ); 43 | }, 44 | registerWithEmailAndPasswordPressed: (e) async* { 45 | yield* _performActionOnAuthFacadeWithEmailAndPassword( 46 | _authFacade.registerWithEmailAndPassword, 47 | ); 48 | }, 49 | signInWithEmailAndPasswordPressed: (e) async* { 50 | yield* _performActionOnAuthFacadeWithEmailAndPassword( 51 | _authFacade.signInWithEmailAndPassword, 52 | ); 53 | }, 54 | signInWithGooglePressed: (e) async* { 55 | yield state.copyWith( 56 | isSubmitting: true, 57 | authFailureOrSuccessOption: none(), 58 | ); 59 | final failureOrSuccess = await _authFacade.signInWithGoogle(); 60 | yield state.copyWith( 61 | isSubmitting: false, 62 | authFailureOrSuccessOption: some(failureOrSuccess), 63 | ); 64 | }, 65 | ); 66 | } 67 | 68 | Stream _performActionOnAuthFacadeWithEmailAndPassword( 69 | Future> Function({ 70 | @required EmailAddress emailAddress, 71 | @required Password password, 72 | }) 73 | forwardedCall, 74 | ) async* { 75 | Either failureOrSuccess; 76 | 77 | final isEmailValid = state.emailAddress.isValid(); 78 | final isPasswordValid = state.password.isValid(); 79 | 80 | if (isEmailValid && isPasswordValid) { 81 | yield state.copyWith( 82 | isSubmitting: true, 83 | authFailureOrSuccessOption: none(), 84 | ); 85 | 86 | failureOrSuccess = await forwardedCall( 87 | emailAddress: state.emailAddress, 88 | password: state.password, 89 | ); 90 | } 91 | 92 | yield state.copyWith( 93 | isSubmitting: false, 94 | showErrorMessages: true, 95 | authFailureOrSuccessOption: optionOf(failureOrSuccess), 96 | ); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /lib/application/auth/sign_in_form/sign_in_form_event.dart: -------------------------------------------------------------------------------- 1 | part of 'sign_in_form_bloc.dart'; 2 | 3 | @freezed 4 | abstract class SignInFormEvent with _$SignInFormEvent { 5 | const factory SignInFormEvent.emailChanged(String emailStr) = EmailChanged; 6 | const factory SignInFormEvent.passwordChanged(String passwordStr) = 7 | PasswordChanged; 8 | const factory SignInFormEvent.registerWithEmailAndPasswordPressed() = 9 | RegisterWithEmailAndPasswordPressed; 10 | const factory SignInFormEvent.signInWithEmailAndPasswordPressed() = 11 | SignInWithEmailAndPasswordPressed; 12 | const factory SignInFormEvent.signInWithGooglePressed() = 13 | SignInWithGooglePressed; 14 | } 15 | -------------------------------------------------------------------------------- /lib/application/auth/sign_in_form/sign_in_form_state.dart: -------------------------------------------------------------------------------- 1 | part of 'sign_in_form_bloc.dart'; 2 | 3 | @freezed 4 | abstract class SignInFormState with _$SignInFormState { 5 | const factory SignInFormState({ 6 | @required EmailAddress emailAddress, 7 | @required Password password, 8 | @required bool showErrorMessages, 9 | @required bool isSubmitting, 10 | @required Option> authFailureOrSuccessOption, 11 | }) = _SignInFormState; 12 | 13 | factory SignInFormState.initial() => SignInFormState( 14 | emailAddress: EmailAddress(''), 15 | password: Password(''), 16 | showErrorMessages: false, 17 | isSubmitting: false, 18 | authFailureOrSuccessOption: none(), 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /lib/application/blog/blog_bloc.dart: -------------------------------------------------------------------------------- 1 | import 'package:bloc/bloc.dart'; 2 | import 'package:freezed_annotation/freezed_annotation.dart'; 3 | import 'package:injectable/injectable.dart'; 4 | 5 | import '../../domain/blog/blog.dart'; 6 | import '../../domain/blog/i_blog_repository.dart'; 7 | import '../../domain/blog/tag.dart'; 8 | 9 | part 'blog_bloc.freezed.dart'; 10 | part 'blog_event.dart'; 11 | part 'blog_state.dart'; 12 | 13 | @injectable 14 | class BlogBloc extends Bloc { 15 | BlogBloc(this._blogRepository); 16 | 17 | final IBlogRepository _blogRepository; 18 | 19 | @override 20 | BlogState get initialState { 21 | return const BlogState.initial(); 22 | } 23 | 24 | @override 25 | Stream mapEventToState( 26 | BlogEvent event, 27 | ) async* { 28 | yield const BlogState.loading(); 29 | yield* event.map(fetch: (e) async* { 30 | final blogOption = await _blogRepository.getBlogData(); 31 | yield blogOption.fold( 32 | (e) => const BlogState.error(), 33 | (blog) { 34 | final List tags = 35 | blog.tags.map((Tag tag) => tag.name).toList(); 36 | return BlogState.loaded(blog, tags); 37 | }, 38 | ); 39 | }); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /lib/application/blog/blog_event.dart: -------------------------------------------------------------------------------- 1 | part of 'blog_bloc.dart'; 2 | 3 | @freezed 4 | abstract class BlogEvent with _$BlogEvent { 5 | const factory BlogEvent.fetch() = _Fetch; 6 | } 7 | -------------------------------------------------------------------------------- /lib/application/blog/blog_state.dart: -------------------------------------------------------------------------------- 1 | part of 'blog_bloc.dart'; 2 | 3 | @freezed 4 | abstract class BlogState with _$BlogState { 5 | const factory BlogState.initial() = Initial; 6 | const factory BlogState.loading() = Loading; 7 | const factory BlogState.error() = Error; 8 | const factory BlogState.loaded(Blog blog, List tags) = Loaded; 9 | } 10 | 11 | // @immutable 12 | // abstract class BlogState { 13 | // const BlogState(); 14 | // } 15 | 16 | // /// This is the default state 17 | // class BlogLoading extends BlogState { 18 | // @override 19 | // String toString() => 'BlogLoading'; 20 | // } 21 | 22 | // class BlogError extends BlogState { 23 | // @override 24 | // String toString() => 'BlogError'; 25 | // } 26 | 27 | // class BlogLoaded extends BlogState { 28 | // const BlogLoaded(this.blog, this.tags) : super(); 29 | 30 | // final Blog blog; 31 | // final List tags; 32 | 33 | // @override 34 | // String toString() => 'BlogLoaded'; 35 | // } 36 | -------------------------------------------------------------------------------- /lib/application/contact_form/bloc/contact_form_bloc.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:bloc/bloc.dart'; 4 | import 'package:dartz/dartz.dart'; 5 | import 'package:freezed_annotation/freezed_annotation.dart'; 6 | import 'package:injectable/injectable.dart'; 7 | import 'package:meta/meta.dart'; 8 | 9 | import '../../../domain/contact_form/contact_form.dart'; 10 | import '../../../domain/contact_form/contact_form_failure.dart'; 11 | import '../../../domain/contact_form/i_contact_form_repository.dart'; 12 | import '../../../domain/contact_form/value_objects.dart'; 13 | 14 | part 'contact_form_bloc.freezed.dart'; 15 | part 'contact_form_event.dart'; 16 | part 'contact_form_state.dart'; 17 | 18 | @injectable 19 | class ContactFormBloc extends Bloc { 20 | final IContactFormRepository _contactFormRepository; 21 | 22 | ContactFormBloc(this._contactFormRepository); 23 | 24 | @override 25 | ContactFormState get initialState => ContactFormState.initial(); 26 | 27 | @override 28 | Stream mapEventToState( 29 | ContactFormEvent event, 30 | ) async* { 31 | yield* event.map(nameChanged: (e) async* { 32 | yield state.copyWith( 33 | name: Name(e.name), 34 | submitFailureOrSuccessOption: none(), 35 | ); 36 | }, emailChanged: (e) async* { 37 | yield state.copyWith( 38 | replyTo: ReplyTo(e.email), 39 | submitFailureOrSuccessOption: none(), 40 | ); 41 | }, messageChanged: (e) async* { 42 | yield state.copyWith( 43 | message: Message(e.message), 44 | submitFailureOrSuccessOption: none(), 45 | ); 46 | }, submit: (_) async* { 47 | Either failureOrSuccess; 48 | 49 | final isReplyToValid = state.replyTo.isValid(); 50 | final isNameValid = state.name.isValid(); 51 | final messageValid = state.message.isValid(); 52 | 53 | if (isReplyToValid && isNameValid && messageValid) { 54 | yield state.copyWith( 55 | isSubmitting: true, 56 | submitFailureOrSuccessOption: none(), 57 | ); 58 | 59 | failureOrSuccess = await _contactFormRepository.submitForm( 60 | ContactForm( 61 | state.name, 62 | state.replyTo, 63 | state.message, 64 | ), 65 | ); 66 | } 67 | yield state.copyWith( 68 | isSubmitting: false, 69 | showErrorMessages: true, 70 | submitFailureOrSuccessOption: optionOf(failureOrSuccess), 71 | ); 72 | }); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /lib/application/contact_form/bloc/contact_form_event.dart: -------------------------------------------------------------------------------- 1 | part of 'contact_form_bloc.dart'; 2 | 3 | @freezed 4 | abstract class ContactFormEvent with _$ContactFormEvent { 5 | const factory ContactFormEvent.nameChanged(String name) = _NameChanged; 6 | const factory ContactFormEvent.emailChanged(String email) = _EmailChanged; 7 | const factory ContactFormEvent.messageChanged(String message) = 8 | _MessageChanged; 9 | const factory ContactFormEvent.submit() = _Submit; 10 | } 11 | -------------------------------------------------------------------------------- /lib/application/contact_form/bloc/contact_form_state.dart: -------------------------------------------------------------------------------- 1 | part of 'contact_form_bloc.dart'; 2 | 3 | @freezed 4 | abstract class ContactFormState with _$ContactFormState { 5 | const factory ContactFormState({ 6 | @required Name name, 7 | @required ReplyTo replyTo, 8 | @required Message message, 9 | @required bool showErrorMessages, 10 | @required bool isSubmitting, 11 | @required 12 | Option> submitFailureOrSuccessOption, 13 | }) = _ContactFormState; 14 | 15 | factory ContactFormState.initial() => ContactFormState( 16 | name: Name(''), 17 | replyTo: ReplyTo(''), 18 | message: Message(''), 19 | showErrorMessages: false, 20 | isSubmitting: false, 21 | submitFailureOrSuccessOption: none(), 22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /lib/application/filtered_blog/filtered_blog_bloc.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:bloc/bloc.dart'; 4 | import 'package:flutter/foundation.dart'; 5 | import 'package:freezed_annotation/freezed_annotation.dart'; 6 | 7 | import '../../domain/blog/blog.dart'; 8 | import '../../domain/blog/tag.dart'; 9 | import '../blog/blog_bloc.dart'; 10 | 11 | part 'filtered_blog_bloc.freezed.dart'; 12 | part 'filtered_blog_event.dart'; 13 | part 'filtered_blog_state.dart'; 14 | 15 | class FilterBlogBloc extends Bloc { 16 | FilterBlogBloc({@required this.blogBloc}) { 17 | _blogSubscription = blogBloc.listen((blogState) { 18 | blogState.maybeMap( 19 | loaded: (blogState) { 20 | add(FilterBlogEvent.update(blogState.blog)); 21 | }, 22 | orElse: () { 23 | add(const FilterBlogEvent.errorFromBlog()); 24 | }, 25 | ); 26 | }); 27 | } 28 | 29 | final BlogBloc blogBloc; 30 | StreamSubscription _blogSubscription; 31 | 32 | @override 33 | FilterBlogState get initialState { 34 | return blogBloc.state.map( 35 | initial: (_) => const FilterBlogState.loading(), 36 | loading: (_) => const FilterBlogState.loading(), 37 | error: (_) => const FilterBlogState.error(), 38 | loaded: (s) => FilterBlogState.loaded(s.blog, ''), 39 | ); 40 | } 41 | 42 | @override 43 | Stream mapEventToState( 44 | FilterBlogEvent event, 45 | ) async* { 46 | yield* event.map( 47 | update: (_) async* { 48 | yield* _mapUpdateFilterToState(); 49 | }, 50 | filterByTag: (e) async* { 51 | yield* _mapTagFilterToState(e); 52 | }, 53 | clearFilters: (_) async* { 54 | yield* blogBloc.state.maybeMap(loaded: (s) async* { 55 | yield FilterBlogState.loaded(s.blog, ''); 56 | }, orElse: () async* { 57 | yield const FilterBlogState.error(); 58 | }); 59 | }, 60 | errorFromBlog: (_) async* { 61 | yield const FilterBlogState.error(); 62 | }, 63 | ); 64 | } 65 | 66 | Stream _mapUpdateFilterToState() async* { 67 | yield* blogBloc.state.maybeMap( 68 | loaded: (s) async* { 69 | yield FilterBlogState.loaded(s.blog, 'tagFilter'); 70 | }, 71 | orElse: () async* { 72 | yield const FilterBlogState.error(); 73 | }, 74 | ); 75 | } 76 | 77 | Stream _mapTagFilterToState(_FilterByTag event) async* { 78 | yield* blogBloc.state.maybeMap( 79 | loaded: (blogState) async* { 80 | yield* state.maybeMap( 81 | loaded: (s) async* { 82 | final currentTag = s.tagFilter; 83 | if (currentTag == event.tag) { 84 | yield* _mapUpdateFilterToState(); 85 | return; 86 | } 87 | try { 88 | final filteredBlog = 89 | _mapTagFilterToFilteredBlog(blogState.blog, event.tag); 90 | yield FilterBlogState.loaded(filteredBlog, event.tag); 91 | } catch (_) { 92 | yield const FilterBlogState.error(); 93 | } 94 | }, 95 | orElse: () async* { 96 | yield const FilterBlogState.error(); 97 | }, 98 | ); 99 | }, 100 | orElse: () async* { 101 | yield const FilterBlogState.error(); 102 | }, 103 | ); 104 | } 105 | 106 | Blog _mapTagFilterToFilteredBlog(Blog blog, String tagFilter) { 107 | final Tag filteredTag = blog.tags.firstWhere((tag) { 108 | return tag.name == tagFilter; 109 | }); 110 | return blog.copyWith(pages: filteredTag.pages); 111 | } 112 | 113 | @override 114 | Future close() { 115 | _blogSubscription.cancel(); 116 | return super.close(); 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /lib/application/filtered_blog/filtered_blog_event.dart: -------------------------------------------------------------------------------- 1 | part of 'filtered_blog_bloc.dart'; 2 | 3 | @freezed 4 | abstract class FilterBlogEvent with _$FilterBlogEvent { 5 | const factory FilterBlogEvent.update(Blog blog) = _Update; 6 | const factory FilterBlogEvent.filterByTag(String tag) = _FilterByTag; 7 | const factory FilterBlogEvent.clearFilters() = _ClearFilters; 8 | const factory FilterBlogEvent.errorFromBlog() = _ErrorFromBlog; 9 | } 10 | 11 | // @immutable 12 | // abstract class FilteredBlogEvent { 13 | // const FilteredBlogEvent(); 14 | // } 15 | 16 | // class UpdateFilteredBlog extends FilteredBlogEvent { 17 | // const UpdateFilteredBlog(this.blog); 18 | 19 | // final Blog blog; 20 | 21 | // @override 22 | // String toString() => 'UpdateFilteredBlog'; 23 | // } 24 | 25 | // class FilterByTag extends FilteredBlogEvent { 26 | // const FilterByTag(this.tagFilter); 27 | 28 | // final String tagFilter; 29 | 30 | // @override 31 | // String toString() => 'FilterByTag'; 32 | // } 33 | 34 | // class ClearFilters extends FilteredBlogEvent { 35 | // @override 36 | // String toString() => 'ClearFilters'; 37 | // } 38 | -------------------------------------------------------------------------------- /lib/application/filtered_blog/filtered_blog_state.dart: -------------------------------------------------------------------------------- 1 | part of 'filtered_blog_bloc.dart'; 2 | 3 | @freezed 4 | abstract class FilterBlogState with _$FilterBlogState { 5 | const factory FilterBlogState.error() = _Error; 6 | const factory FilterBlogState.loading() = _Loading; 7 | const factory FilterBlogState.loaded(Blog filteredBlog, String tagFilter) = 8 | _Loaded; 9 | } 10 | -------------------------------------------------------------------------------- /lib/application/page/page_bloc.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'package:bloc/bloc.dart'; 3 | import 'package:freezed_annotation/freezed_annotation.dart'; 4 | 5 | part 'page_bloc.freezed.dart'; 6 | part 'page_event.dart'; 7 | part 'page_state.dart'; 8 | 9 | class PageBloc extends Bloc { 10 | @override 11 | PageState get initialState => PageState.home; 12 | 13 | @override 14 | Stream mapEventToState( 15 | PageEvent event, 16 | ) async* { 17 | yield* event.map( 18 | update: (e) async* { 19 | yield e.page; 20 | }, 21 | ); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /lib/application/page/page_bloc.freezed.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | // ignore_for_file: deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named 3 | 4 | part of 'page_bloc.dart'; 5 | 6 | // ************************************************************************** 7 | // FreezedGenerator 8 | // ************************************************************************** 9 | 10 | T _$identity(T value) => value; 11 | 12 | class _$PageEventTearOff { 13 | const _$PageEventTearOff(); 14 | 15 | _Update update(PageState page) { 16 | return _Update( 17 | page, 18 | ); 19 | } 20 | } 21 | 22 | // ignore: unused_element 23 | const $PageEvent = _$PageEventTearOff(); 24 | 25 | mixin _$PageEvent { 26 | PageState get page; 27 | 28 | @optionalTypeArgs 29 | Result when({ 30 | @required Result update(PageState page), 31 | }); 32 | @optionalTypeArgs 33 | Result maybeWhen({ 34 | Result update(PageState page), 35 | @required Result orElse(), 36 | }); 37 | @optionalTypeArgs 38 | Result map({ 39 | @required Result update(_Update value), 40 | }); 41 | @optionalTypeArgs 42 | Result maybeMap({ 43 | Result update(_Update value), 44 | @required Result orElse(), 45 | }); 46 | 47 | $PageEventCopyWith get copyWith; 48 | } 49 | 50 | abstract class $PageEventCopyWith<$Res> { 51 | factory $PageEventCopyWith(PageEvent value, $Res Function(PageEvent) then) = 52 | _$PageEventCopyWithImpl<$Res>; 53 | $Res call({PageState page}); 54 | } 55 | 56 | class _$PageEventCopyWithImpl<$Res> implements $PageEventCopyWith<$Res> { 57 | _$PageEventCopyWithImpl(this._value, this._then); 58 | 59 | final PageEvent _value; 60 | // ignore: unused_field 61 | final $Res Function(PageEvent) _then; 62 | 63 | @override 64 | $Res call({ 65 | Object page = freezed, 66 | }) { 67 | return _then(_value.copyWith( 68 | page: page == freezed ? _value.page : page as PageState, 69 | )); 70 | } 71 | } 72 | 73 | abstract class _$UpdateCopyWith<$Res> implements $PageEventCopyWith<$Res> { 74 | factory _$UpdateCopyWith(_Update value, $Res Function(_Update) then) = 75 | __$UpdateCopyWithImpl<$Res>; 76 | @override 77 | $Res call({PageState page}); 78 | } 79 | 80 | class __$UpdateCopyWithImpl<$Res> extends _$PageEventCopyWithImpl<$Res> 81 | implements _$UpdateCopyWith<$Res> { 82 | __$UpdateCopyWithImpl(_Update _value, $Res Function(_Update) _then) 83 | : super(_value, (v) => _then(v as _Update)); 84 | 85 | @override 86 | _Update get _value => super._value as _Update; 87 | 88 | @override 89 | $Res call({ 90 | Object page = freezed, 91 | }) { 92 | return _then(_Update( 93 | page == freezed ? _value.page : page as PageState, 94 | )); 95 | } 96 | } 97 | 98 | class _$_Update implements _Update { 99 | const _$_Update(this.page) : assert(page != null); 100 | 101 | @override 102 | final PageState page; 103 | 104 | @override 105 | String toString() { 106 | return 'PageEvent.update(page: $page)'; 107 | } 108 | 109 | @override 110 | bool operator ==(dynamic other) { 111 | return identical(this, other) || 112 | (other is _Update && 113 | (identical(other.page, page) || 114 | const DeepCollectionEquality().equals(other.page, page))); 115 | } 116 | 117 | @override 118 | int get hashCode => 119 | runtimeType.hashCode ^ const DeepCollectionEquality().hash(page); 120 | 121 | @override 122 | _$UpdateCopyWith<_Update> get copyWith => 123 | __$UpdateCopyWithImpl<_Update>(this, _$identity); 124 | 125 | @override 126 | @optionalTypeArgs 127 | Result when({ 128 | @required Result update(PageState page), 129 | }) { 130 | assert(update != null); 131 | return update(page); 132 | } 133 | 134 | @override 135 | @optionalTypeArgs 136 | Result maybeWhen({ 137 | Result update(PageState page), 138 | @required Result orElse(), 139 | }) { 140 | assert(orElse != null); 141 | if (update != null) { 142 | return update(page); 143 | } 144 | return orElse(); 145 | } 146 | 147 | @override 148 | @optionalTypeArgs 149 | Result map({ 150 | @required Result update(_Update value), 151 | }) { 152 | assert(update != null); 153 | return update(this); 154 | } 155 | 156 | @override 157 | @optionalTypeArgs 158 | Result maybeMap({ 159 | Result update(_Update value), 160 | @required Result orElse(), 161 | }) { 162 | assert(orElse != null); 163 | if (update != null) { 164 | return update(this); 165 | } 166 | return orElse(); 167 | } 168 | } 169 | 170 | abstract class _Update implements PageEvent { 171 | const factory _Update(PageState page) = _$_Update; 172 | 173 | @override 174 | PageState get page; 175 | @override 176 | _$UpdateCopyWith<_Update> get copyWith; 177 | } 178 | -------------------------------------------------------------------------------- /lib/application/page/page_event.dart: -------------------------------------------------------------------------------- 1 | part of 'page_bloc.dart'; 2 | 3 | @freezed 4 | abstract class PageEvent with _$PageEvent { 5 | const factory PageEvent.update(PageState page) = _Update; 6 | } 7 | -------------------------------------------------------------------------------- /lib/application/page/page_state.dart: -------------------------------------------------------------------------------- 1 | part of 'page_bloc.dart'; 2 | 3 | enum PageState { contact, home, blog, courses, videos } 4 | -------------------------------------------------------------------------------- /lib/application/simple_bloc_delegate.dart: -------------------------------------------------------------------------------- 1 | import 'package:bloc/bloc.dart'; 2 | import 'package:flutter/foundation.dart'; 3 | 4 | class SimpleBlocDelegate extends BlocDelegate { 5 | @override 6 | void onEvent(Bloc bloc, Object event) { 7 | super.onEvent(bloc, event); 8 | debugPrint(event.toString()); 9 | } 10 | 11 | @override 12 | void onError(Bloc bloc, Object error, StackTrace stacktrace) { 13 | super.onError(bloc, error, stacktrace); 14 | debugPrint(error.toString()); 15 | } 16 | 17 | @override 18 | void onTransition(Bloc bloc, Transition transition) { 19 | super.onTransition(bloc, transition); 20 | debugPrint(transition.toString()); 21 | } 22 | } -------------------------------------------------------------------------------- /lib/application/theme/bloc/theme_bloc.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/foundation.dart'; 2 | import 'package:bloc/bloc.dart'; 3 | import 'package:flutter/material.dart'; 4 | import 'package:freezed_annotation/freezed_annotation.dart'; 5 | import 'package:meta/meta.dart'; 6 | 7 | part 'theme_event.dart'; 8 | part 'theme_state.dart'; 9 | 10 | part 'theme_bloc.freezed.dart'; 11 | 12 | class ThemeBloc extends Bloc { 13 | @override 14 | ThemeState get initialState => const ThemeState.light(); 15 | 16 | @override 17 | Stream mapEventToState( 18 | ThemeEvent event, 19 | ) async* { 20 | yield* event.map(switchTheme: (_) async* { 21 | if (state is Light) { 22 | yield const ThemeState.dark(); 23 | } else { 24 | yield const ThemeState.light(); 25 | } 26 | }); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /lib/application/theme/bloc/theme_event.dart: -------------------------------------------------------------------------------- 1 | part of 'theme_bloc.dart'; 2 | 3 | @freezed 4 | abstract class ThemeEvent with _$ThemeEvent { 5 | const factory ThemeEvent.switchTheme() = SwitchTheme; 6 | // const factory ThemeEvent.switchToLight() = _SwitchToLight; 7 | } 8 | -------------------------------------------------------------------------------- /lib/application/theme/bloc/theme_state.dart: -------------------------------------------------------------------------------- 1 | part of 'theme_bloc.dart'; 2 | 3 | @freezed 4 | abstract class ThemeState with _$ThemeState { 5 | const factory ThemeState.light() = Light; 6 | const factory ThemeState.dark() = Dark; 7 | } 8 | -------------------------------------------------------------------------------- /lib/domain/auth/auth_failure.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | 3 | part 'auth_failure.freezed.dart'; 4 | 5 | @freezed 6 | abstract class AuthFailure with _$AuthFailure { 7 | const factory AuthFailure.cancelledByUser() = CancelledByUser; 8 | const factory AuthFailure.serverError() = ServerError; 9 | const factory AuthFailure.emailAlreadyInUse() = EmailAlreadyInUse; 10 | const factory AuthFailure.invalidEmailAndPasswordCombination() = 11 | InvalidEmailAndPasswordCombination; 12 | const factory AuthFailure.userDisabled() = UserDisabled; 13 | } 14 | -------------------------------------------------------------------------------- /lib/domain/auth/i_auth_facade.dart: -------------------------------------------------------------------------------- 1 | import 'package:dartz/dartz.dart'; 2 | import 'package:flutter/foundation.dart'; 3 | 4 | import 'auth_failure.dart'; 5 | import 'user.dart'; 6 | import 'value_objects.dart'; 7 | 8 | 9 | abstract class IAuthFacade { 10 | Future> getSignedInUser(); 11 | Stream> get onAuthStateChanged; 12 | Future> registerWithEmailAndPassword({ 13 | @required EmailAddress emailAddress, 14 | @required Password password, 15 | }); 16 | Future> signInWithEmailAndPassword({ 17 | @required EmailAddress emailAddress, 18 | @required Password password, 19 | }); 20 | Future> signInWithGoogle(); 21 | Future signOut(); 22 | } 23 | -------------------------------------------------------------------------------- /lib/domain/auth/user.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | 3 | import '../core/value_objects.dart'; 4 | 5 | part 'user.freezed.dart'; 6 | 7 | @freezed 8 | abstract class User with _$User { 9 | const factory User({ 10 | @required UniqueId id, 11 | }) = _User; 12 | } 13 | -------------------------------------------------------------------------------- /lib/domain/auth/user.freezed.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | // ignore_for_file: deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named 3 | 4 | part of 'user.dart'; 5 | 6 | // ************************************************************************** 7 | // FreezedGenerator 8 | // ************************************************************************** 9 | 10 | T _$identity(T value) => value; 11 | 12 | class _$UserTearOff { 13 | const _$UserTearOff(); 14 | 15 | _User call({@required UniqueId id}) { 16 | return _User( 17 | id: id, 18 | ); 19 | } 20 | } 21 | 22 | // ignore: unused_element 23 | const $User = _$UserTearOff(); 24 | 25 | mixin _$User { 26 | UniqueId get id; 27 | 28 | $UserCopyWith get copyWith; 29 | } 30 | 31 | abstract class $UserCopyWith<$Res> { 32 | factory $UserCopyWith(User value, $Res Function(User) then) = 33 | _$UserCopyWithImpl<$Res>; 34 | $Res call({UniqueId id}); 35 | } 36 | 37 | class _$UserCopyWithImpl<$Res> implements $UserCopyWith<$Res> { 38 | _$UserCopyWithImpl(this._value, this._then); 39 | 40 | final User _value; 41 | // ignore: unused_field 42 | final $Res Function(User) _then; 43 | 44 | @override 45 | $Res call({ 46 | Object id = freezed, 47 | }) { 48 | return _then(_value.copyWith( 49 | id: id == freezed ? _value.id : id as UniqueId, 50 | )); 51 | } 52 | } 53 | 54 | abstract class _$UserCopyWith<$Res> implements $UserCopyWith<$Res> { 55 | factory _$UserCopyWith(_User value, $Res Function(_User) then) = 56 | __$UserCopyWithImpl<$Res>; 57 | @override 58 | $Res call({UniqueId id}); 59 | } 60 | 61 | class __$UserCopyWithImpl<$Res> extends _$UserCopyWithImpl<$Res> 62 | implements _$UserCopyWith<$Res> { 63 | __$UserCopyWithImpl(_User _value, $Res Function(_User) _then) 64 | : super(_value, (v) => _then(v as _User)); 65 | 66 | @override 67 | _User get _value => super._value as _User; 68 | 69 | @override 70 | $Res call({ 71 | Object id = freezed, 72 | }) { 73 | return _then(_User( 74 | id: id == freezed ? _value.id : id as UniqueId, 75 | )); 76 | } 77 | } 78 | 79 | class _$_User implements _User { 80 | const _$_User({@required this.id}) : assert(id != null); 81 | 82 | @override 83 | final UniqueId id; 84 | 85 | @override 86 | String toString() { 87 | return 'User(id: $id)'; 88 | } 89 | 90 | @override 91 | bool operator ==(dynamic other) { 92 | return identical(this, other) || 93 | (other is _User && 94 | (identical(other.id, id) || 95 | const DeepCollectionEquality().equals(other.id, id))); 96 | } 97 | 98 | @override 99 | int get hashCode => 100 | runtimeType.hashCode ^ const DeepCollectionEquality().hash(id); 101 | 102 | @override 103 | _$UserCopyWith<_User> get copyWith => 104 | __$UserCopyWithImpl<_User>(this, _$identity); 105 | } 106 | 107 | abstract class _User implements User { 108 | const factory _User({@required UniqueId id}) = _$_User; 109 | 110 | @override 111 | UniqueId get id; 112 | @override 113 | _$UserCopyWith<_User> get copyWith; 114 | } 115 | -------------------------------------------------------------------------------- /lib/domain/auth/value_objects.dart: -------------------------------------------------------------------------------- 1 | import 'package:dartz/dartz.dart'; 2 | 3 | import '../core/failures.dart'; 4 | import '../core/value_objects.dart'; 5 | import '../core/value_validators.dart'; 6 | 7 | class EmailAddress extends ValueObject { 8 | @override 9 | final Either, String> value; 10 | 11 | factory EmailAddress(String input) { 12 | assert(input != null); 13 | return EmailAddress._( 14 | validateEmailAddress(input), 15 | ); 16 | } 17 | 18 | const EmailAddress._(this.value); 19 | } 20 | 21 | class Password extends ValueObject { 22 | @override 23 | final Either, String> value; 24 | 25 | factory Password(String input) { 26 | assert(input != null); 27 | return Password._( 28 | validatePassword(input), 29 | ); 30 | } 31 | 32 | const Password._(this.value); 33 | } 34 | -------------------------------------------------------------------------------- /lib/domain/blog/blog.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | 3 | import 'post_data.dart'; 4 | import 'tag.dart'; 5 | 6 | part 'blog.freezed.dart'; 7 | part 'blog.g.dart'; 8 | 9 | @freezed 10 | abstract class Blog with _$Blog{ 11 | const factory Blog(List tags, List pages) = _Blog; 12 | 13 | factory Blog.fromJson(Map json) => _$BlogFromJson(json); 14 | } -------------------------------------------------------------------------------- /lib/domain/blog/blog.freezed.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | // ignore_for_file: deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named 3 | 4 | part of 'blog.dart'; 5 | 6 | // ************************************************************************** 7 | // FreezedGenerator 8 | // ************************************************************************** 9 | 10 | T _$identity(T value) => value; 11 | Blog _$BlogFromJson(Map json) { 12 | return _Blog.fromJson(json); 13 | } 14 | 15 | class _$BlogTearOff { 16 | const _$BlogTearOff(); 17 | 18 | _Blog call(List tags, List pages) { 19 | return _Blog( 20 | tags, 21 | pages, 22 | ); 23 | } 24 | } 25 | 26 | // ignore: unused_element 27 | const $Blog = _$BlogTearOff(); 28 | 29 | mixin _$Blog { 30 | List get tags; 31 | List get pages; 32 | 33 | Map toJson(); 34 | $BlogCopyWith get copyWith; 35 | } 36 | 37 | abstract class $BlogCopyWith<$Res> { 38 | factory $BlogCopyWith(Blog value, $Res Function(Blog) then) = 39 | _$BlogCopyWithImpl<$Res>; 40 | $Res call({List tags, List pages}); 41 | } 42 | 43 | class _$BlogCopyWithImpl<$Res> implements $BlogCopyWith<$Res> { 44 | _$BlogCopyWithImpl(this._value, this._then); 45 | 46 | final Blog _value; 47 | // ignore: unused_field 48 | final $Res Function(Blog) _then; 49 | 50 | @override 51 | $Res call({ 52 | Object tags = freezed, 53 | Object pages = freezed, 54 | }) { 55 | return _then(_value.copyWith( 56 | tags: tags == freezed ? _value.tags : tags as List, 57 | pages: pages == freezed ? _value.pages : pages as List, 58 | )); 59 | } 60 | } 61 | 62 | abstract class _$BlogCopyWith<$Res> implements $BlogCopyWith<$Res> { 63 | factory _$BlogCopyWith(_Blog value, $Res Function(_Blog) then) = 64 | __$BlogCopyWithImpl<$Res>; 65 | @override 66 | $Res call({List tags, List pages}); 67 | } 68 | 69 | class __$BlogCopyWithImpl<$Res> extends _$BlogCopyWithImpl<$Res> 70 | implements _$BlogCopyWith<$Res> { 71 | __$BlogCopyWithImpl(_Blog _value, $Res Function(_Blog) _then) 72 | : super(_value, (v) => _then(v as _Blog)); 73 | 74 | @override 75 | _Blog get _value => super._value as _Blog; 76 | 77 | @override 78 | $Res call({ 79 | Object tags = freezed, 80 | Object pages = freezed, 81 | }) { 82 | return _then(_Blog( 83 | tags == freezed ? _value.tags : tags as List, 84 | pages == freezed ? _value.pages : pages as List, 85 | )); 86 | } 87 | } 88 | 89 | @JsonSerializable() 90 | class _$_Blog implements _Blog { 91 | const _$_Blog(this.tags, this.pages) 92 | : assert(tags != null), 93 | assert(pages != null); 94 | 95 | factory _$_Blog.fromJson(Map json) => 96 | _$_$_BlogFromJson(json); 97 | 98 | @override 99 | final List tags; 100 | @override 101 | final List pages; 102 | 103 | @override 104 | String toString() { 105 | return 'Blog(tags: $tags, pages: $pages)'; 106 | } 107 | 108 | @override 109 | bool operator ==(dynamic other) { 110 | return identical(this, other) || 111 | (other is _Blog && 112 | (identical(other.tags, tags) || 113 | const DeepCollectionEquality().equals(other.tags, tags)) && 114 | (identical(other.pages, pages) || 115 | const DeepCollectionEquality().equals(other.pages, pages))); 116 | } 117 | 118 | @override 119 | int get hashCode => 120 | runtimeType.hashCode ^ 121 | const DeepCollectionEquality().hash(tags) ^ 122 | const DeepCollectionEquality().hash(pages); 123 | 124 | @override 125 | _$BlogCopyWith<_Blog> get copyWith => 126 | __$BlogCopyWithImpl<_Blog>(this, _$identity); 127 | 128 | @override 129 | Map toJson() { 130 | return _$_$_BlogToJson(this); 131 | } 132 | } 133 | 134 | abstract class _Blog implements Blog { 135 | const factory _Blog(List tags, List pages) = _$_Blog; 136 | 137 | factory _Blog.fromJson(Map json) = _$_Blog.fromJson; 138 | 139 | @override 140 | List get tags; 141 | @override 142 | List get pages; 143 | @override 144 | _$BlogCopyWith<_Blog> get copyWith; 145 | } 146 | -------------------------------------------------------------------------------- /lib/domain/blog/blog.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'blog.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | _$_Blog _$_$_BlogFromJson(Map json) { 10 | return _$_Blog( 11 | (json['tags'] as List) 12 | ?.map((e) => e == null ? null : Tag.fromJson(e as Map)) 13 | ?.toList(), 14 | (json['pages'] as List) 15 | ?.map((e) => 16 | e == null ? null : PostData.fromJson(e as Map)) 17 | ?.toList(), 18 | ); 19 | } 20 | 21 | Map _$_$_BlogToJson(_$_Blog instance) => { 22 | 'tags': instance.tags, 23 | 'pages': instance.pages, 24 | }; 25 | -------------------------------------------------------------------------------- /lib/domain/blog/blog_failure.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | 3 | part 'blog_failure.freezed.dart'; 4 | 5 | @freezed 6 | abstract class BlogFailure with _$BlogFailure { 7 | const factory BlogFailure.serverError() = ServerError; 8 | } 9 | -------------------------------------------------------------------------------- /lib/domain/blog/blog_failure.freezed.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | // ignore_for_file: deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named 3 | 4 | part of 'blog_failure.dart'; 5 | 6 | // ************************************************************************** 7 | // FreezedGenerator 8 | // ************************************************************************** 9 | 10 | T _$identity(T value) => value; 11 | 12 | class _$BlogFailureTearOff { 13 | const _$BlogFailureTearOff(); 14 | 15 | ServerError serverError() { 16 | return const ServerError(); 17 | } 18 | } 19 | 20 | // ignore: unused_element 21 | const $BlogFailure = _$BlogFailureTearOff(); 22 | 23 | mixin _$BlogFailure { 24 | @optionalTypeArgs 25 | Result when({ 26 | @required Result serverError(), 27 | }); 28 | @optionalTypeArgs 29 | Result maybeWhen({ 30 | Result serverError(), 31 | @required Result orElse(), 32 | }); 33 | @optionalTypeArgs 34 | Result map({ 35 | @required Result serverError(ServerError value), 36 | }); 37 | @optionalTypeArgs 38 | Result maybeMap({ 39 | Result serverError(ServerError value), 40 | @required Result orElse(), 41 | }); 42 | } 43 | 44 | abstract class $BlogFailureCopyWith<$Res> { 45 | factory $BlogFailureCopyWith( 46 | BlogFailure value, $Res Function(BlogFailure) then) = 47 | _$BlogFailureCopyWithImpl<$Res>; 48 | } 49 | 50 | class _$BlogFailureCopyWithImpl<$Res> implements $BlogFailureCopyWith<$Res> { 51 | _$BlogFailureCopyWithImpl(this._value, this._then); 52 | 53 | final BlogFailure _value; 54 | // ignore: unused_field 55 | final $Res Function(BlogFailure) _then; 56 | } 57 | 58 | abstract class $ServerErrorCopyWith<$Res> { 59 | factory $ServerErrorCopyWith( 60 | ServerError value, $Res Function(ServerError) then) = 61 | _$ServerErrorCopyWithImpl<$Res>; 62 | } 63 | 64 | class _$ServerErrorCopyWithImpl<$Res> extends _$BlogFailureCopyWithImpl<$Res> 65 | implements $ServerErrorCopyWith<$Res> { 66 | _$ServerErrorCopyWithImpl( 67 | ServerError _value, $Res Function(ServerError) _then) 68 | : super(_value, (v) => _then(v as ServerError)); 69 | 70 | @override 71 | ServerError get _value => super._value as ServerError; 72 | } 73 | 74 | class _$ServerError implements ServerError { 75 | const _$ServerError(); 76 | 77 | @override 78 | String toString() { 79 | return 'BlogFailure.serverError()'; 80 | } 81 | 82 | @override 83 | bool operator ==(dynamic other) { 84 | return identical(this, other) || (other is ServerError); 85 | } 86 | 87 | @override 88 | int get hashCode => runtimeType.hashCode; 89 | 90 | @override 91 | @optionalTypeArgs 92 | Result when({ 93 | @required Result serverError(), 94 | }) { 95 | assert(serverError != null); 96 | return serverError(); 97 | } 98 | 99 | @override 100 | @optionalTypeArgs 101 | Result maybeWhen({ 102 | Result serverError(), 103 | @required Result orElse(), 104 | }) { 105 | assert(orElse != null); 106 | if (serverError != null) { 107 | return serverError(); 108 | } 109 | return orElse(); 110 | } 111 | 112 | @override 113 | @optionalTypeArgs 114 | Result map({ 115 | @required Result serverError(ServerError value), 116 | }) { 117 | assert(serverError != null); 118 | return serverError(this); 119 | } 120 | 121 | @override 122 | @optionalTypeArgs 123 | Result maybeMap({ 124 | Result serverError(ServerError value), 125 | @required Result orElse(), 126 | }) { 127 | assert(orElse != null); 128 | if (serverError != null) { 129 | return serverError(this); 130 | } 131 | return orElse(); 132 | } 133 | } 134 | 135 | abstract class ServerError implements BlogFailure { 136 | const factory ServerError() = _$ServerError; 137 | } 138 | -------------------------------------------------------------------------------- /lib/domain/blog/i_blog_repository.dart: -------------------------------------------------------------------------------- 1 | import 'package:dartz/dartz.dart'; 2 | 3 | import 'blog.dart'; 4 | import 'blog_failure.dart'; 5 | 6 | abstract class IBlogRepository { 7 | Future> getBlogData(); 8 | } 9 | -------------------------------------------------------------------------------- /lib/domain/blog/post_data.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | 3 | part 'post_data.freezed.dart'; 4 | part 'post_data.g.dart'; 5 | 6 | @freezed 7 | abstract class PostData with _$PostData { 8 | const factory PostData( 9 | String description, String title, String uri, String thumbnail, 10 | [@Default(['']) List tags]) = _PostData; 11 | 12 | factory PostData.fromJson(Map json) => 13 | _$PostDataFromJson(json); 14 | } 15 | -------------------------------------------------------------------------------- /lib/domain/blog/post_data.freezed.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | // ignore_for_file: deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named 3 | 4 | part of 'post_data.dart'; 5 | 6 | // ************************************************************************** 7 | // FreezedGenerator 8 | // ************************************************************************** 9 | 10 | T _$identity(T value) => value; 11 | PostData _$PostDataFromJson(Map json) { 12 | return _PostData.fromJson(json); 13 | } 14 | 15 | class _$PostDataTearOff { 16 | const _$PostDataTearOff(); 17 | 18 | _PostData call(String description, String title, String uri, String thumbnail, 19 | [List tags = const ['']]) { 20 | return _PostData( 21 | description, 22 | title, 23 | uri, 24 | thumbnail, 25 | tags, 26 | ); 27 | } 28 | } 29 | 30 | // ignore: unused_element 31 | const $PostData = _$PostDataTearOff(); 32 | 33 | mixin _$PostData { 34 | String get description; 35 | String get title; 36 | String get uri; 37 | String get thumbnail; 38 | List get tags; 39 | 40 | Map toJson(); 41 | $PostDataCopyWith get copyWith; 42 | } 43 | 44 | abstract class $PostDataCopyWith<$Res> { 45 | factory $PostDataCopyWith(PostData value, $Res Function(PostData) then) = 46 | _$PostDataCopyWithImpl<$Res>; 47 | $Res call( 48 | {String description, 49 | String title, 50 | String uri, 51 | String thumbnail, 52 | List tags}); 53 | } 54 | 55 | class _$PostDataCopyWithImpl<$Res> implements $PostDataCopyWith<$Res> { 56 | _$PostDataCopyWithImpl(this._value, this._then); 57 | 58 | final PostData _value; 59 | // ignore: unused_field 60 | final $Res Function(PostData) _then; 61 | 62 | @override 63 | $Res call({ 64 | Object description = freezed, 65 | Object title = freezed, 66 | Object uri = freezed, 67 | Object thumbnail = freezed, 68 | Object tags = freezed, 69 | }) { 70 | return _then(_value.copyWith( 71 | description: 72 | description == freezed ? _value.description : description as String, 73 | title: title == freezed ? _value.title : title as String, 74 | uri: uri == freezed ? _value.uri : uri as String, 75 | thumbnail: thumbnail == freezed ? _value.thumbnail : thumbnail as String, 76 | tags: tags == freezed ? _value.tags : tags as List, 77 | )); 78 | } 79 | } 80 | 81 | abstract class _$PostDataCopyWith<$Res> implements $PostDataCopyWith<$Res> { 82 | factory _$PostDataCopyWith(_PostData value, $Res Function(_PostData) then) = 83 | __$PostDataCopyWithImpl<$Res>; 84 | @override 85 | $Res call( 86 | {String description, 87 | String title, 88 | String uri, 89 | String thumbnail, 90 | List tags}); 91 | } 92 | 93 | class __$PostDataCopyWithImpl<$Res> extends _$PostDataCopyWithImpl<$Res> 94 | implements _$PostDataCopyWith<$Res> { 95 | __$PostDataCopyWithImpl(_PostData _value, $Res Function(_PostData) _then) 96 | : super(_value, (v) => _then(v as _PostData)); 97 | 98 | @override 99 | _PostData get _value => super._value as _PostData; 100 | 101 | @override 102 | $Res call({ 103 | Object description = freezed, 104 | Object title = freezed, 105 | Object uri = freezed, 106 | Object thumbnail = freezed, 107 | Object tags = freezed, 108 | }) { 109 | return _then(_PostData( 110 | description == freezed ? _value.description : description as String, 111 | title == freezed ? _value.title : title as String, 112 | uri == freezed ? _value.uri : uri as String, 113 | thumbnail == freezed ? _value.thumbnail : thumbnail as String, 114 | tags == freezed ? _value.tags : tags as List, 115 | )); 116 | } 117 | } 118 | 119 | @JsonSerializable() 120 | class _$_PostData implements _PostData { 121 | const _$_PostData(this.description, this.title, this.uri, this.thumbnail, 122 | [this.tags = const ['']]) 123 | : assert(description != null), 124 | assert(title != null), 125 | assert(uri != null), 126 | assert(thumbnail != null), 127 | assert(tags != null); 128 | 129 | factory _$_PostData.fromJson(Map json) => 130 | _$_$_PostDataFromJson(json); 131 | 132 | @override 133 | final String description; 134 | @override 135 | final String title; 136 | @override 137 | final String uri; 138 | @override 139 | final String thumbnail; 140 | @JsonKey(defaultValue: const ['']) 141 | @override 142 | final List tags; 143 | 144 | @override 145 | String toString() { 146 | return 'PostData(description: $description, title: $title, uri: $uri, thumbnail: $thumbnail, tags: $tags)'; 147 | } 148 | 149 | @override 150 | bool operator ==(dynamic other) { 151 | return identical(this, other) || 152 | (other is _PostData && 153 | (identical(other.description, description) || 154 | const DeepCollectionEquality() 155 | .equals(other.description, description)) && 156 | (identical(other.title, title) || 157 | const DeepCollectionEquality().equals(other.title, title)) && 158 | (identical(other.uri, uri) || 159 | const DeepCollectionEquality().equals(other.uri, uri)) && 160 | (identical(other.thumbnail, thumbnail) || 161 | const DeepCollectionEquality() 162 | .equals(other.thumbnail, thumbnail)) && 163 | (identical(other.tags, tags) || 164 | const DeepCollectionEquality().equals(other.tags, tags))); 165 | } 166 | 167 | @override 168 | int get hashCode => 169 | runtimeType.hashCode ^ 170 | const DeepCollectionEquality().hash(description) ^ 171 | const DeepCollectionEquality().hash(title) ^ 172 | const DeepCollectionEquality().hash(uri) ^ 173 | const DeepCollectionEquality().hash(thumbnail) ^ 174 | const DeepCollectionEquality().hash(tags); 175 | 176 | @override 177 | _$PostDataCopyWith<_PostData> get copyWith => 178 | __$PostDataCopyWithImpl<_PostData>(this, _$identity); 179 | 180 | @override 181 | Map toJson() { 182 | return _$_$_PostDataToJson(this); 183 | } 184 | } 185 | 186 | abstract class _PostData implements PostData { 187 | const factory _PostData( 188 | String description, String title, String uri, String thumbnail, 189 | [List tags]) = _$_PostData; 190 | 191 | factory _PostData.fromJson(Map json) = _$_PostData.fromJson; 192 | 193 | @override 194 | String get description; 195 | @override 196 | String get title; 197 | @override 198 | String get uri; 199 | @override 200 | String get thumbnail; 201 | @override 202 | List get tags; 203 | @override 204 | _$PostDataCopyWith<_PostData> get copyWith; 205 | } 206 | -------------------------------------------------------------------------------- /lib/domain/blog/post_data.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'post_data.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | _$_PostData _$_$_PostDataFromJson(Map json) { 10 | return _$_PostData( 11 | json['description'] as String, 12 | json['title'] as String, 13 | json['uri'] as String, 14 | json['thumbnail'] as String, 15 | (json['tags'] as List)?.map((e) => e as String)?.toList() ?? [''], 16 | ); 17 | } 18 | 19 | Map _$_$_PostDataToJson(_$_PostData instance) => 20 | { 21 | 'description': instance.description, 22 | 'title': instance.title, 23 | 'uri': instance.uri, 24 | 'thumbnail': instance.thumbnail, 25 | 'tags': instance.tags, 26 | }; 27 | -------------------------------------------------------------------------------- /lib/domain/blog/tag.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | 3 | import 'post_data.dart'; 4 | 5 | part 'tag.freezed.dart'; 6 | part 'tag.g.dart'; 7 | 8 | @freezed 9 | abstract class Tag with _$Tag{ 10 | const factory Tag(String name, List pages) = _Tag; 11 | 12 | factory Tag.fromJson(Map json) => _$TagFromJson(json); 13 | } -------------------------------------------------------------------------------- /lib/domain/blog/tag.freezed.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | // ignore_for_file: deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named 3 | 4 | part of 'tag.dart'; 5 | 6 | // ************************************************************************** 7 | // FreezedGenerator 8 | // ************************************************************************** 9 | 10 | T _$identity(T value) => value; 11 | Tag _$TagFromJson(Map json) { 12 | return _Tag.fromJson(json); 13 | } 14 | 15 | class _$TagTearOff { 16 | const _$TagTearOff(); 17 | 18 | _Tag call(String name, List pages) { 19 | return _Tag( 20 | name, 21 | pages, 22 | ); 23 | } 24 | } 25 | 26 | // ignore: unused_element 27 | const $Tag = _$TagTearOff(); 28 | 29 | mixin _$Tag { 30 | String get name; 31 | List get pages; 32 | 33 | Map toJson(); 34 | $TagCopyWith get copyWith; 35 | } 36 | 37 | abstract class $TagCopyWith<$Res> { 38 | factory $TagCopyWith(Tag value, $Res Function(Tag) then) = 39 | _$TagCopyWithImpl<$Res>; 40 | $Res call({String name, List pages}); 41 | } 42 | 43 | class _$TagCopyWithImpl<$Res> implements $TagCopyWith<$Res> { 44 | _$TagCopyWithImpl(this._value, this._then); 45 | 46 | final Tag _value; 47 | // ignore: unused_field 48 | final $Res Function(Tag) _then; 49 | 50 | @override 51 | $Res call({ 52 | Object name = freezed, 53 | Object pages = freezed, 54 | }) { 55 | return _then(_value.copyWith( 56 | name: name == freezed ? _value.name : name as String, 57 | pages: pages == freezed ? _value.pages : pages as List, 58 | )); 59 | } 60 | } 61 | 62 | abstract class _$TagCopyWith<$Res> implements $TagCopyWith<$Res> { 63 | factory _$TagCopyWith(_Tag value, $Res Function(_Tag) then) = 64 | __$TagCopyWithImpl<$Res>; 65 | @override 66 | $Res call({String name, List pages}); 67 | } 68 | 69 | class __$TagCopyWithImpl<$Res> extends _$TagCopyWithImpl<$Res> 70 | implements _$TagCopyWith<$Res> { 71 | __$TagCopyWithImpl(_Tag _value, $Res Function(_Tag) _then) 72 | : super(_value, (v) => _then(v as _Tag)); 73 | 74 | @override 75 | _Tag get _value => super._value as _Tag; 76 | 77 | @override 78 | $Res call({ 79 | Object name = freezed, 80 | Object pages = freezed, 81 | }) { 82 | return _then(_Tag( 83 | name == freezed ? _value.name : name as String, 84 | pages == freezed ? _value.pages : pages as List, 85 | )); 86 | } 87 | } 88 | 89 | @JsonSerializable() 90 | class _$_Tag implements _Tag { 91 | const _$_Tag(this.name, this.pages) 92 | : assert(name != null), 93 | assert(pages != null); 94 | 95 | factory _$_Tag.fromJson(Map json) => _$_$_TagFromJson(json); 96 | 97 | @override 98 | final String name; 99 | @override 100 | final List pages; 101 | 102 | @override 103 | String toString() { 104 | return 'Tag(name: $name, pages: $pages)'; 105 | } 106 | 107 | @override 108 | bool operator ==(dynamic other) { 109 | return identical(this, other) || 110 | (other is _Tag && 111 | (identical(other.name, name) || 112 | const DeepCollectionEquality().equals(other.name, name)) && 113 | (identical(other.pages, pages) || 114 | const DeepCollectionEquality().equals(other.pages, pages))); 115 | } 116 | 117 | @override 118 | int get hashCode => 119 | runtimeType.hashCode ^ 120 | const DeepCollectionEquality().hash(name) ^ 121 | const DeepCollectionEquality().hash(pages); 122 | 123 | @override 124 | _$TagCopyWith<_Tag> get copyWith => 125 | __$TagCopyWithImpl<_Tag>(this, _$identity); 126 | 127 | @override 128 | Map toJson() { 129 | return _$_$_TagToJson(this); 130 | } 131 | } 132 | 133 | abstract class _Tag implements Tag { 134 | const factory _Tag(String name, List pages) = _$_Tag; 135 | 136 | factory _Tag.fromJson(Map json) = _$_Tag.fromJson; 137 | 138 | @override 139 | String get name; 140 | @override 141 | List get pages; 142 | @override 143 | _$TagCopyWith<_Tag> get copyWith; 144 | } 145 | -------------------------------------------------------------------------------- /lib/domain/blog/tag.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'tag.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | _$_Tag _$_$_TagFromJson(Map json) { 10 | return _$_Tag( 11 | json['name'] as String, 12 | (json['pages'] as List) 13 | ?.map((e) => 14 | e == null ? null : PostData.fromJson(e as Map)) 15 | ?.toList(), 16 | ); 17 | } 18 | 19 | Map _$_$_TagToJson(_$_Tag instance) => { 20 | 'name': instance.name, 21 | 'pages': instance.pages, 22 | }; 23 | -------------------------------------------------------------------------------- /lib/domain/contact_form/contact_form.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | 3 | import 'value_objects.dart'; 4 | 5 | part 'contact_form.freezed.dart'; 6 | 7 | @freezed 8 | abstract class ContactForm with _$ContactForm { 9 | const factory ContactForm(Name name, ReplyTo replyTo, Message message) = 10 | _ContactForm; 11 | } 12 | -------------------------------------------------------------------------------- /lib/domain/contact_form/contact_form.freezed.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | // ignore_for_file: deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named 3 | 4 | part of 'contact_form.dart'; 5 | 6 | // ************************************************************************** 7 | // FreezedGenerator 8 | // ************************************************************************** 9 | 10 | T _$identity(T value) => value; 11 | 12 | class _$ContactFormTearOff { 13 | const _$ContactFormTearOff(); 14 | 15 | _ContactForm call(Name name, ReplyTo replyTo, Message message) { 16 | return _ContactForm( 17 | name, 18 | replyTo, 19 | message, 20 | ); 21 | } 22 | } 23 | 24 | // ignore: unused_element 25 | const $ContactForm = _$ContactFormTearOff(); 26 | 27 | mixin _$ContactForm { 28 | Name get name; 29 | ReplyTo get replyTo; 30 | Message get message; 31 | 32 | $ContactFormCopyWith get copyWith; 33 | } 34 | 35 | abstract class $ContactFormCopyWith<$Res> { 36 | factory $ContactFormCopyWith( 37 | ContactForm value, $Res Function(ContactForm) then) = 38 | _$ContactFormCopyWithImpl<$Res>; 39 | $Res call({Name name, ReplyTo replyTo, Message message}); 40 | } 41 | 42 | class _$ContactFormCopyWithImpl<$Res> implements $ContactFormCopyWith<$Res> { 43 | _$ContactFormCopyWithImpl(this._value, this._then); 44 | 45 | final ContactForm _value; 46 | // ignore: unused_field 47 | final $Res Function(ContactForm) _then; 48 | 49 | @override 50 | $Res call({ 51 | Object name = freezed, 52 | Object replyTo = freezed, 53 | Object message = freezed, 54 | }) { 55 | return _then(_value.copyWith( 56 | name: name == freezed ? _value.name : name as Name, 57 | replyTo: replyTo == freezed ? _value.replyTo : replyTo as ReplyTo, 58 | message: message == freezed ? _value.message : message as Message, 59 | )); 60 | } 61 | } 62 | 63 | abstract class _$ContactFormCopyWith<$Res> 64 | implements $ContactFormCopyWith<$Res> { 65 | factory _$ContactFormCopyWith( 66 | _ContactForm value, $Res Function(_ContactForm) then) = 67 | __$ContactFormCopyWithImpl<$Res>; 68 | @override 69 | $Res call({Name name, ReplyTo replyTo, Message message}); 70 | } 71 | 72 | class __$ContactFormCopyWithImpl<$Res> extends _$ContactFormCopyWithImpl<$Res> 73 | implements _$ContactFormCopyWith<$Res> { 74 | __$ContactFormCopyWithImpl( 75 | _ContactForm _value, $Res Function(_ContactForm) _then) 76 | : super(_value, (v) => _then(v as _ContactForm)); 77 | 78 | @override 79 | _ContactForm get _value => super._value as _ContactForm; 80 | 81 | @override 82 | $Res call({ 83 | Object name = freezed, 84 | Object replyTo = freezed, 85 | Object message = freezed, 86 | }) { 87 | return _then(_ContactForm( 88 | name == freezed ? _value.name : name as Name, 89 | replyTo == freezed ? _value.replyTo : replyTo as ReplyTo, 90 | message == freezed ? _value.message : message as Message, 91 | )); 92 | } 93 | } 94 | 95 | class _$_ContactForm implements _ContactForm { 96 | const _$_ContactForm(this.name, this.replyTo, this.message) 97 | : assert(name != null), 98 | assert(replyTo != null), 99 | assert(message != null); 100 | 101 | @override 102 | final Name name; 103 | @override 104 | final ReplyTo replyTo; 105 | @override 106 | final Message message; 107 | 108 | @override 109 | String toString() { 110 | return 'ContactForm(name: $name, replyTo: $replyTo, message: $message)'; 111 | } 112 | 113 | @override 114 | bool operator ==(dynamic other) { 115 | return identical(this, other) || 116 | (other is _ContactForm && 117 | (identical(other.name, name) || 118 | const DeepCollectionEquality().equals(other.name, name)) && 119 | (identical(other.replyTo, replyTo) || 120 | const DeepCollectionEquality() 121 | .equals(other.replyTo, replyTo)) && 122 | (identical(other.message, message) || 123 | const DeepCollectionEquality().equals(other.message, message))); 124 | } 125 | 126 | @override 127 | int get hashCode => 128 | runtimeType.hashCode ^ 129 | const DeepCollectionEquality().hash(name) ^ 130 | const DeepCollectionEquality().hash(replyTo) ^ 131 | const DeepCollectionEquality().hash(message); 132 | 133 | @override 134 | _$ContactFormCopyWith<_ContactForm> get copyWith => 135 | __$ContactFormCopyWithImpl<_ContactForm>(this, _$identity); 136 | } 137 | 138 | abstract class _ContactForm implements ContactForm { 139 | const factory _ContactForm(Name name, ReplyTo replyTo, Message message) = 140 | _$_ContactForm; 141 | 142 | @override 143 | Name get name; 144 | @override 145 | ReplyTo get replyTo; 146 | @override 147 | Message get message; 148 | @override 149 | _$ContactFormCopyWith<_ContactForm> get copyWith; 150 | } 151 | -------------------------------------------------------------------------------- /lib/domain/contact_form/contact_form_failure.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | 3 | part 'contact_form_failure.freezed.dart'; 4 | 5 | @freezed 6 | abstract class ContactFormFailure with _$ContactFormFailure { 7 | const factory ContactFormFailure.serverError() = ServerError; 8 | } 9 | -------------------------------------------------------------------------------- /lib/domain/contact_form/contact_form_failure.freezed.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | // ignore_for_file: deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named 3 | 4 | part of 'contact_form_failure.dart'; 5 | 6 | // ************************************************************************** 7 | // FreezedGenerator 8 | // ************************************************************************** 9 | 10 | T _$identity(T value) => value; 11 | 12 | class _$ContactFormFailureTearOff { 13 | const _$ContactFormFailureTearOff(); 14 | 15 | ServerError serverError() { 16 | return const ServerError(); 17 | } 18 | } 19 | 20 | // ignore: unused_element 21 | const $ContactFormFailure = _$ContactFormFailureTearOff(); 22 | 23 | mixin _$ContactFormFailure { 24 | @optionalTypeArgs 25 | Result when({ 26 | @required Result serverError(), 27 | }); 28 | @optionalTypeArgs 29 | Result maybeWhen({ 30 | Result serverError(), 31 | @required Result orElse(), 32 | }); 33 | @optionalTypeArgs 34 | Result map({ 35 | @required Result serverError(ServerError value), 36 | }); 37 | @optionalTypeArgs 38 | Result maybeMap({ 39 | Result serverError(ServerError value), 40 | @required Result orElse(), 41 | }); 42 | } 43 | 44 | abstract class $ContactFormFailureCopyWith<$Res> { 45 | factory $ContactFormFailureCopyWith( 46 | ContactFormFailure value, $Res Function(ContactFormFailure) then) = 47 | _$ContactFormFailureCopyWithImpl<$Res>; 48 | } 49 | 50 | class _$ContactFormFailureCopyWithImpl<$Res> 51 | implements $ContactFormFailureCopyWith<$Res> { 52 | _$ContactFormFailureCopyWithImpl(this._value, this._then); 53 | 54 | final ContactFormFailure _value; 55 | // ignore: unused_field 56 | final $Res Function(ContactFormFailure) _then; 57 | } 58 | 59 | abstract class $ServerErrorCopyWith<$Res> { 60 | factory $ServerErrorCopyWith( 61 | ServerError value, $Res Function(ServerError) then) = 62 | _$ServerErrorCopyWithImpl<$Res>; 63 | } 64 | 65 | class _$ServerErrorCopyWithImpl<$Res> 66 | extends _$ContactFormFailureCopyWithImpl<$Res> 67 | implements $ServerErrorCopyWith<$Res> { 68 | _$ServerErrorCopyWithImpl( 69 | ServerError _value, $Res Function(ServerError) _then) 70 | : super(_value, (v) => _then(v as ServerError)); 71 | 72 | @override 73 | ServerError get _value => super._value as ServerError; 74 | } 75 | 76 | class _$ServerError implements ServerError { 77 | const _$ServerError(); 78 | 79 | @override 80 | String toString() { 81 | return 'ContactFormFailure.serverError()'; 82 | } 83 | 84 | @override 85 | bool operator ==(dynamic other) { 86 | return identical(this, other) || (other is ServerError); 87 | } 88 | 89 | @override 90 | int get hashCode => runtimeType.hashCode; 91 | 92 | @override 93 | @optionalTypeArgs 94 | Result when({ 95 | @required Result serverError(), 96 | }) { 97 | assert(serverError != null); 98 | return serverError(); 99 | } 100 | 101 | @override 102 | @optionalTypeArgs 103 | Result maybeWhen({ 104 | Result serverError(), 105 | @required Result orElse(), 106 | }) { 107 | assert(orElse != null); 108 | if (serverError != null) { 109 | return serverError(); 110 | } 111 | return orElse(); 112 | } 113 | 114 | @override 115 | @optionalTypeArgs 116 | Result map({ 117 | @required Result serverError(ServerError value), 118 | }) { 119 | assert(serverError != null); 120 | return serverError(this); 121 | } 122 | 123 | @override 124 | @optionalTypeArgs 125 | Result maybeMap({ 126 | Result serverError(ServerError value), 127 | @required Result orElse(), 128 | }) { 129 | assert(orElse != null); 130 | if (serverError != null) { 131 | return serverError(this); 132 | } 133 | return orElse(); 134 | } 135 | } 136 | 137 | abstract class ServerError implements ContactFormFailure { 138 | const factory ServerError() = _$ServerError; 139 | } 140 | -------------------------------------------------------------------------------- /lib/domain/contact_form/i_contact_form_repository.dart: -------------------------------------------------------------------------------- 1 | import 'package:dartz/dartz.dart'; 2 | 3 | import 'contact_form.dart'; 4 | import 'contact_form_failure.dart'; 5 | 6 | abstract class IContactFormRepository { 7 | Future> submitForm(ContactForm contact); 8 | } 9 | -------------------------------------------------------------------------------- /lib/domain/contact_form/value_objects.dart: -------------------------------------------------------------------------------- 1 | import 'package:dartz/dartz.dart'; 2 | 3 | import '../core/failures.dart'; 4 | import '../core/value_objects.dart'; 5 | import '../core/value_validators.dart'; 6 | 7 | class Message extends ValueObject { 8 | @override 9 | final Either, String> value; 10 | 11 | static const maxLength = 1000; 12 | 13 | factory Message(String input) { 14 | assert(input != null); 15 | return Message._( 16 | validateMaxStringLength(input, maxLength).flatMap(validateStringNotEmpty), 17 | ); 18 | } 19 | 20 | const Message._(this.value); 21 | } 22 | 23 | class ReplyTo extends ValueObject { 24 | @override 25 | final Either, String> value; 26 | 27 | factory ReplyTo(String input) { 28 | assert(input != null); 29 | return ReplyTo._( 30 | validateEmailAddress(input), 31 | ); 32 | } 33 | 34 | const ReplyTo._(this.value); 35 | } 36 | 37 | class Name extends ValueObject { 38 | @override 39 | final Either, String> value; 40 | 41 | static const maxLength = 30; 42 | 43 | factory Name(String input) { 44 | assert(input != null); 45 | return Name._( 46 | validateMaxStringLength(input, maxLength).flatMap(validateStringNotEmpty), 47 | ); 48 | } 49 | 50 | const Name._(this.value); 51 | } 52 | -------------------------------------------------------------------------------- /lib/domain/core/errors.dart: -------------------------------------------------------------------------------- 1 | import 'failures.dart'; 2 | 3 | class NotAuthenticatedError extends Error {} 4 | 5 | class UnexpectedValueError extends Error { 6 | final ValueFailure valueFailure; 7 | 8 | UnexpectedValueError(this.valueFailure); 9 | 10 | @override 11 | String toString() { 12 | const explanation = 13 | 'Encountered a ValueFailure at an unrecoverable point. Terminating.'; 14 | return Error.safeToString('$explanation Failure was: $valueFailure'); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /lib/domain/core/failures.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | 3 | part 'failures.freezed.dart'; 4 | 5 | @freezed 6 | abstract class ValueFailure with _$ValueFailure { 7 | const factory ValueFailure.invalidEmail({ 8 | @required T failedValue, 9 | }) = InvalidEmail; 10 | const factory ValueFailure.shortPassword({ 11 | @required T failedValue, 12 | }) = ShortPassword; 13 | const factory ValueFailure.exceedingLength({ 14 | @required T failedValue, 15 | @required int max, 16 | }) = ExceedingLength; 17 | const factory ValueFailure.empty({ 18 | @required T failedValue, 19 | }) = Empty; 20 | const factory ValueFailure.multiline({ 21 | @required T failedValue, 22 | }) = Multiline; 23 | } 24 | -------------------------------------------------------------------------------- /lib/domain/core/value_objects.dart: -------------------------------------------------------------------------------- 1 | import 'package:dartz/dartz.dart'; 2 | import 'package:flutter/foundation.dart'; 3 | import 'package:freezed_annotation/freezed_annotation.dart'; 4 | import 'package:uuid/uuid.dart'; 5 | 6 | import 'errors.dart'; 7 | import 'failures.dart'; 8 | 9 | @immutable 10 | abstract class ValueObject { 11 | const ValueObject(); 12 | Either, T> get value; 13 | 14 | /// Throws [UnexpectedValueError] containing the [ValueFailure] 15 | T getOrCrash() { 16 | // id = identity - same as writing (right) => right 17 | return value.fold((f) => throw UnexpectedValueError(f), id); 18 | } 19 | 20 | Either, Unit> get failureOrUnit { 21 | return value.fold( 22 | (l) => left(l), 23 | (r) => right(unit), 24 | ); 25 | } 26 | 27 | bool isValid() => value.isRight(); 28 | 29 | @override 30 | bool operator ==(Object o) { 31 | if (identical(this, o)) return true; 32 | 33 | return o is ValueObject && o.value == value; 34 | } 35 | 36 | @override 37 | int get hashCode => value.hashCode; 38 | 39 | @override 40 | String toString() => 'Value($value)'; 41 | } 42 | 43 | class UniqueId extends ValueObject { 44 | @override 45 | final Either, String> value; 46 | 47 | factory UniqueId() { 48 | return UniqueId._( 49 | right(Uuid().v1()), 50 | ); 51 | } 52 | 53 | factory UniqueId.fromUniqueString(String uniqueId) { 54 | assert(uniqueId != null); 55 | return UniqueId._( 56 | right(uniqueId), 57 | ); 58 | } 59 | 60 | const UniqueId._(this.value); 61 | } 62 | -------------------------------------------------------------------------------- /lib/domain/core/value_transformers.dart: -------------------------------------------------------------------------------- 1 | import 'dart:ui'; 2 | 3 | Color makeColorOpaque(Color color) { 4 | return color.withOpacity(1); 5 | } 6 | -------------------------------------------------------------------------------- /lib/domain/core/value_validators.dart: -------------------------------------------------------------------------------- 1 | import 'package:dartz/dartz.dart'; 2 | 3 | import 'failures.dart'; 4 | 5 | Either, String> validateEmailAddress(String input) { 6 | const emailRegex = 7 | r"""^[a-zA-Z0-9.a-zA-Z0-9.!#$%&'*+-/=?^_`{|}~]+@[a-zA-Z0-9]+\.[a-zA-Z]+"""; 8 | if (RegExp(emailRegex).hasMatch(input)) { 9 | return right(input); 10 | } else { 11 | return left(ValueFailure.invalidEmail(failedValue: input)); 12 | } 13 | } 14 | 15 | Either, String> validatePassword(String input) { 16 | if (input.length >= 6) { 17 | return right(input); 18 | } else { 19 | return left(ValueFailure.shortPassword(failedValue: input)); 20 | } 21 | } 22 | 23 | Either, String> validateMaxStringLength( 24 | String input, 25 | int maxLength, 26 | ) { 27 | if (input.length <= maxLength) { 28 | return right(input); 29 | } else { 30 | return left( 31 | ValueFailure.exceedingLength(failedValue: input, max: maxLength), 32 | ); 33 | } 34 | } 35 | 36 | Either, String> validateStringNotEmpty(String input) { 37 | if (input.isNotEmpty) { 38 | return right(input); 39 | } else { 40 | return left(ValueFailure.empty(failedValue: input)); 41 | } 42 | } 43 | 44 | Either, String> validateSingleLine(String input) { 45 | if (input.contains('\n')) { 46 | return left(ValueFailure.multiline(failedValue: input)); 47 | } else { 48 | return right(input); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /lib/infrastructure/auth/firebase_auth_facade.dart: -------------------------------------------------------------------------------- 1 | import 'package:dartz/dartz.dart'; 2 | import 'package:firebase_auth/firebase_auth.dart'; 3 | import 'package:flutter/foundation.dart'; 4 | import 'package:flutter/services.dart'; 5 | import 'package:google_sign_in/google_sign_in.dart'; 6 | import 'package:injectable/injectable.dart'; 7 | 8 | import '../../domain/auth/auth_failure.dart'; 9 | import '../../domain/auth/i_auth_facade.dart'; 10 | import '../../domain/auth/user.dart'; 11 | import '../../domain/auth/value_objects.dart'; 12 | import 'firebase_user_mapper.dart'; 13 | 14 | @LazySingleton(as: IAuthFacade) 15 | class FirebaseAuthFacade implements IAuthFacade { 16 | final FirebaseAuth _firebaseAuth; 17 | final GoogleSignIn _googleSignIn; 18 | 19 | FirebaseAuthFacade( 20 | this._firebaseAuth, 21 | this._googleSignIn, 22 | ); 23 | 24 | @override 25 | Future> getSignedInUser() => _firebaseAuth 26 | .currentUser() 27 | .then((firebaseUser) => optionOf(firebaseUser?.toDomain())); 28 | 29 | @override 30 | Stream> get onAuthStateChanged { 31 | return _firebaseAuth.onAuthStateChanged 32 | .map((firebaseUser) => optionOf(firebaseUser?.toDomain())); 33 | } 34 | 35 | @override 36 | Future> registerWithEmailAndPassword({ 37 | @required EmailAddress emailAddress, 38 | @required Password password, 39 | }) async { 40 | final emailAddressStr = emailAddress.getOrCrash(); 41 | final passwordStr = password.getOrCrash(); 42 | try { 43 | await _firebaseAuth.createUserWithEmailAndPassword( 44 | email: emailAddressStr, 45 | password: passwordStr, 46 | ); 47 | return right(unit); 48 | } catch (e) { 49 | debugPrint(e.code); 50 | switch (e.code) { 51 | case "auth/email-already-in-use": 52 | return left(const AuthFailure.emailAlreadyInUse()); 53 | break; 54 | default: 55 | return left(const AuthFailure.serverError()); 56 | } 57 | } 58 | 59 | /// The below works on mobile. Not on Web 60 | // on PlatformException catch (e) { 61 | // if (e.code == 'ERROR_EMAIL_ALREADY_IN_USE') { 62 | // return left(const AuthFailure.emailAlreadyInUse()); 63 | // } else { 64 | // return left(const AuthFailure.serverError()); 65 | // } 66 | // } 67 | } 68 | 69 | @override 70 | Future> signInWithEmailAndPassword({ 71 | @required EmailAddress emailAddress, 72 | @required Password password, 73 | }) async { 74 | final emailAddressStr = emailAddress.getOrCrash(); 75 | final passwordStr = password.getOrCrash(); 76 | try { 77 | await _firebaseAuth.signInWithEmailAndPassword( 78 | email: emailAddressStr, 79 | password: passwordStr, 80 | ); 81 | return right(unit); 82 | } catch (e) { 83 | switch (e.code) { 84 | case "auth/wrong-password": 85 | case "auth/user-not-found": 86 | return left(const AuthFailure.invalidEmailAndPasswordCombination()); 87 | break; 88 | case "auth/user-disabled": 89 | return left(const AuthFailure.userDisabled()); 90 | break; 91 | default: 92 | return left(const AuthFailure.serverError()); 93 | } 94 | } 95 | 96 | /// The below works on mobile. Not on web 97 | // on PlatformException catch (e) { 98 | // if (e.code == 'ERROR_WRONG_PASSWORD' || 99 | // e.code == 'ERROR_USER_NOT_FOUND') { 100 | // return left(const AuthFailure.invalidEmailAndPasswordCombination()); 101 | // } else { 102 | // return left(const AuthFailure.serverError()); 103 | // } 104 | // } 105 | } 106 | 107 | @override 108 | Future> signInWithGoogle() async { 109 | GoogleSignInAccount googleUser; 110 | try { 111 | googleUser = await _googleSignIn.signIn(); 112 | 113 | final googleAuthentication = await googleUser.authentication; 114 | 115 | final authCredential = GoogleAuthProvider.getCredential( 116 | idToken: googleAuthentication.idToken, 117 | accessToken: googleAuthentication.accessToken, 118 | ); 119 | 120 | await _firebaseAuth.signInWithCredential(authCredential); 121 | return right(unit); 122 | } catch (_) { 123 | if (googleUser == null) { 124 | return left(const AuthFailure.cancelledByUser()); 125 | } 126 | return left(const AuthFailure.serverError()); 127 | } 128 | } 129 | 130 | @override 131 | Future signOut() => Future.wait([ 132 | // TODO investigate when Google sign out should be called 133 | // _googleSignIn.signOut(), 134 | _firebaseAuth.signOut(), 135 | ]); 136 | } 137 | -------------------------------------------------------------------------------- /lib/infrastructure/auth/firebase_auth_service.dart: -------------------------------------------------------------------------------- 1 | import 'package:firebase_auth/firebase_auth.dart'; 2 | 3 | import 'package:meta/meta.dart'; 4 | 5 | @immutable 6 | class User { 7 | const User({ 8 | @required this.uid, 9 | this.email, 10 | this.photoUrl, 11 | this.displayName, 12 | }); 13 | 14 | final String uid; 15 | final String email; 16 | final String photoUrl; 17 | final String displayName; 18 | } 19 | 20 | class FirebaseAuthService { 21 | final FirebaseAuth _firebaseAuth = FirebaseAuth.instance; 22 | 23 | User _userFromFirebase(FirebaseUser user) { 24 | if (user == null) { 25 | return null; 26 | } 27 | return User( 28 | uid: user.uid, 29 | email: user.email, 30 | displayName: user.displayName, 31 | photoUrl: user.photoUrl, 32 | ); 33 | } 34 | 35 | Stream get onAuthStateChanged { 36 | return _firebaseAuth.onAuthStateChanged.map(_userFromFirebase); 37 | } 38 | 39 | Future signInAnonymously() async { 40 | final authResult = await _firebaseAuth.signInAnonymously(); 41 | return _userFromFirebase(authResult.user); 42 | } 43 | 44 | Future signOut() async { 45 | return _firebaseAuth.signOut(); 46 | } 47 | 48 | /// Future implementation 49 | 50 | Future signInWithEmailAndPassword(String email, String password) async { 51 | final AuthResult authResult = await _firebaseAuth 52 | .signInWithCredential(EmailAuthProvider.getCredential( 53 | email: email, 54 | password: password, 55 | )); 56 | return _userFromFirebase(authResult.user); 57 | } 58 | 59 | Future createUserWithEmailAndPassword( 60 | String email, String password) async { 61 | final AuthResult authResult = await _firebaseAuth 62 | .createUserWithEmailAndPassword(email: email, password: password); 63 | return _userFromFirebase(authResult.user); 64 | } 65 | 66 | Future sendPasswordResetEmail(String email) async { 67 | await _firebaseAuth.sendPasswordResetEmail(email: email); 68 | } 69 | 70 | Future currentUser() async { 71 | final FirebaseUser user = await _firebaseAuth.currentUser(); 72 | return _userFromFirebase(user); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /lib/infrastructure/auth/firebase_user_mapper.dart: -------------------------------------------------------------------------------- 1 | import 'package:firebase_auth/firebase_auth.dart'; 2 | 3 | import '../../domain/auth/user.dart'; 4 | import '../../domain/core/value_objects.dart'; 5 | 6 | extension FirebaseUserDomainX on FirebaseUser { 7 | User toDomain() { 8 | return User( 9 | id: UniqueId.fromUniqueString(uid), 10 | ); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /lib/infrastructure/blog/blog_api.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:http/http.dart' as http; 4 | 5 | class BlogApi { 6 | BlogApi(this.uri); 7 | final String uri; 8 | 9 | Future> fetchData() async { 10 | final http.Response response = await http.get( 11 | Uri.encodeFull(uri), 12 | ); 13 | if (response.statusCode == 200) { 14 | final Map result = 15 | json.decode(response.body) as Map; 16 | return result; 17 | } else { 18 | throw Exception('Error fetching blog posts: ${response.statusCode}'); 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /lib/infrastructure/blog/blog_repository.dart: -------------------------------------------------------------------------------- 1 | import 'package:dartz/dartz.dart'; 2 | import 'package:flutter/cupertino.dart'; 3 | import 'package:injectable/injectable.dart'; 4 | 5 | import '../../domain/blog/blog.dart'; 6 | import '../../domain/blog/blog_failure.dart'; 7 | import '../../domain/blog/i_blog_repository.dart'; 8 | import '../core/urls.dart'; 9 | import 'blog_api.dart'; 10 | 11 | @prod 12 | @Injectable(as: IBlogRepository) 13 | class BlogRepository implements IBlogRepository { 14 | final BlogApi _blogApi = BlogApi('$blogProductionUrl/index.json'); 15 | 16 | @override 17 | Future> getBlogData() async { 18 | try { 19 | final data = await _blogApi.fetchData(); 20 | final blog = Blog.fromJson(data); 21 | return right(blog); 22 | } on Exception catch (e) { 23 | debugPrint(e.toString()); 24 | return left(const BlogFailure.serverError()); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /lib/infrastructure/blog/dev_blog_repository.dart: -------------------------------------------------------------------------------- 1 | import 'package:dartz/dartz.dart'; 2 | import 'package:flutter/cupertino.dart'; 3 | import 'package:injectable/injectable.dart'; 4 | 5 | import '../../domain/blog/blog.dart'; 6 | import '../../domain/blog/blog_failure.dart'; 7 | import '../../domain/blog/i_blog_repository.dart'; 8 | import '../core/urls.dart'; 9 | import 'blog_api.dart'; 10 | 11 | @dev 12 | @Injectable(as: IBlogRepository) 13 | class DevBlogRepository implements IBlogRepository { 14 | final BlogApi _blogApi = BlogApi('$blogTestingUrl/index.json'); 15 | 16 | @override 17 | Future> getBlogData() async { 18 | try { 19 | final data = await _blogApi.fetchData(); 20 | final blog = Blog.fromJson(data); 21 | return right(blog); 22 | } on Exception catch (e) { 23 | debugPrint(e.toString()); 24 | return left(const BlogFailure.serverError()); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /lib/infrastructure/blog/register_module.dart: -------------------------------------------------------------------------------- 1 | // import 'package:injectable/injectable.dart'; 2 | 3 | // @module 4 | // abstract class RegisterModule { 5 | // Client apiClient( 6 | // @factoryParam String url, 7 | // ) => 8 | // ApiClient(url); 9 | // } 10 | 11 | // @Injectable(as: Client) 12 | // class ApiClient extends Client { 13 | // final String baseUrl; 14 | 15 | // ApiClient(@Named('baseUrl') this.baseUrl); 16 | 17 | // @override 18 | // String get url => baseUrl; 19 | // } 20 | 21 | // abstract class Client { 22 | // String get url; 23 | // } 24 | -------------------------------------------------------------------------------- /lib/infrastructure/contact_form/contact_form_api.dart: -------------------------------------------------------------------------------- 1 | import 'package:dio/dio.dart'; 2 | import 'package:flutter/foundation.dart'; 3 | 4 | class ContactFormApi { 5 | ContactFormApi(this.uri); 6 | final String uri; 7 | 8 | Future submitForm(String name, String replyTo, String message) async { 9 | final map = {}; 10 | map['name'] = name; 11 | map['_replyto'] = replyTo; 12 | map['message'] = message; 13 | try { 14 | await Dio().post( 15 | uri, 16 | data: map, 17 | options: Options( 18 | followRedirects: false, 19 | validateStatus: (status) { 20 | return status < 500; 21 | }), 22 | ); 23 | } catch (e) { 24 | debugPrint('erorr: $e'); 25 | rethrow; 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /lib/infrastructure/contact_form/contact_form_repository.dart: -------------------------------------------------------------------------------- 1 | import 'package:dartz/dartz.dart'; 2 | import 'package:injectable/injectable.dart'; 3 | 4 | import '../../domain/contact_form/contact_form.dart'; 5 | import '../../domain/contact_form/contact_form_failure.dart'; 6 | import '../../domain/contact_form/i_contact_form_repository.dart'; 7 | import 'contact_form_api.dart'; 8 | 9 | @Injectable(as: IContactFormRepository) 10 | class ContactFormRepository implements IContactFormRepository { 11 | final ContactFormApi _api = ContactFormApi('https://formspree.io/xyynnggq'); 12 | 13 | @override 14 | Future> submitForm( 15 | ContactForm contact) async { 16 | final String name = contact.name.getOrCrash(); 17 | final String replyTo = contact.replyTo.getOrCrash(); 18 | final String message = contact.message.getOrCrash(); 19 | 20 | try { 21 | await _api.submitForm(name, replyTo, message); 22 | return right(unit); 23 | } catch (e) { 24 | return left(const ContactFormFailure.serverError()); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /lib/infrastructure/core/firebase_injectable_module.dart: -------------------------------------------------------------------------------- 1 | import 'package:firebase_auth/firebase_auth.dart'; 2 | import 'package:google_sign_in/google_sign_in.dart'; 3 | import 'package:injectable/injectable.dart'; 4 | 5 | @module 6 | abstract class FirebaseInjectableModule { 7 | @lazySingleton 8 | GoogleSignIn get googleSignIn => GoogleSignIn(); 9 | @lazySingleton 10 | FirebaseAuth get firebaseAuth => FirebaseAuth.instance; 11 | // @lazySingleton 12 | // Firestore get firestore => Firestore.instance; 13 | } 14 | -------------------------------------------------------------------------------- /lib/infrastructure/core/urls.dart: -------------------------------------------------------------------------------- 1 | import 'package:meta/meta.dart'; 2 | 3 | const blogTestingUrl = 'http://localhost:8000'; 4 | const blogProductionUrl = 'https://blog.funwith.app'; 5 | 6 | const funWithYouTubeUrl = 'https://www.youtube.com/funwithflutter'; 7 | const funWithYouTubeSubscribeUrl = 8 | 'https://www.youtube.com/funwithflutter/?sub_confirmation=1'; 9 | 10 | const courseAnimationPerf = 11 | 'https://courses.funwith.app/p/mastering-animation-in-flutter/?product_id=1679475&coupon_code=FUN'; 12 | 13 | const funWithGithubUrl = 'https://github.com/funwithflutter'; 14 | const funWithPatreon = 'https://www.patreon.com/funwithflutter'; 15 | const funWithTwitter = 'https://twitter.com/FunFlutter'; 16 | const funWithMedium = ''; // TODO(Gordon): medium 17 | 18 | const flutterDev = 'https://flutter.dev'; 19 | 20 | const PackageUrl waveSliderPackage = PackageUrl( 21 | pubUrl: 'https://pub.dev/packages/wave_slider', 22 | youtubeUrl: 23 | 'https://www.youtube.com/playlist?list=PLjr4ufdmNA4J2-KwMutexAjjf_VmjL1eH'); 24 | const PackageUrl splashTapPackage = PackageUrl( 25 | pubUrl: 'https://pub.dev/packages/splash_tap', 26 | youtubeUrl: 'https://www.youtube.com/watch?v=7qkhpeZdD7U'); 27 | const PackageUrl confettiPackage = PackageUrl( 28 | pubUrl: 'https://pub.dev/packages/confetti', 29 | youtubeUrl: 'https://www.youtube.com/watch?v=jvhw3cfj2rk'); 30 | 31 | @immutable 32 | class PackageUrl { 33 | const PackageUrl({this.pubUrl = '', this.youtubeUrl = ''}) 34 | : assert(pubUrl != null && youtubeUrl != null); 35 | 36 | final String pubUrl; 37 | final String youtubeUrl; 38 | } 39 | -------------------------------------------------------------------------------- /lib/infrastructure/url_repository.dart: -------------------------------------------------------------------------------- 1 | import 'core/urls.dart' as url; 2 | 3 | const _thumbnailUrlPath = 'thumbnails'; 4 | 5 | 6 | String get blogBaseUrl { 7 | if (_isReleaseBuild()) { 8 | return url.blogProductionUrl; 9 | } 10 | return url.blogTestingUrl; 11 | } 12 | 13 | String get blogDataUrl { 14 | return '$blogBaseUrl/index.json'; 15 | } 16 | 17 | String blogThumbnailUrl(String filename) { 18 | return '$blogBaseUrl/$_thumbnailUrlPath/$filename'; 19 | } 20 | 21 | bool _isReleaseBuild() { 22 | var isProd = true; 23 | assert(() { 24 | isProd = false; 25 | return true; 26 | }()); 27 | return isProd; 28 | } 29 | -------------------------------------------------------------------------------- /lib/injection.dart: -------------------------------------------------------------------------------- 1 | import 'package:get_it/get_it.dart'; 2 | import 'package:injectable/injectable.dart'; 3 | 4 | import 'injection.iconfig.dart'; 5 | 6 | final GetIt getIt = GetIt.instance; 7 | 8 | @injectableInit 9 | void configureInjection(String env) { 10 | $initGetIt(getIt, environment: env); 11 | } 12 | -------------------------------------------------------------------------------- /lib/injection.iconfig.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | // ************************************************************************** 4 | // InjectableConfigGenerator 5 | // ************************************************************************** 6 | 7 | import 'package:fun_with_flutter/infrastructure/core/firebase_injectable_module.dart'; 8 | import 'package:firebase_auth/firebase_auth.dart'; 9 | import 'package:google_sign_in/google_sign_in.dart'; 10 | import 'package:fun_with_flutter/infrastructure/auth/firebase_auth_facade.dart'; 11 | import 'package:fun_with_flutter/domain/auth/i_auth_facade.dart'; 12 | import 'package:fun_with_flutter/infrastructure/blog/blog_repository.dart'; 13 | import 'package:fun_with_flutter/domain/blog/i_blog_repository.dart'; 14 | import 'package:fun_with_flutter/infrastructure/blog/dev_blog_repository.dart'; 15 | import 'package:fun_with_flutter/infrastructure/contact_form/contact_form_repository.dart'; 16 | import 'package:fun_with_flutter/domain/contact_form/i_contact_form_repository.dart'; 17 | import 'package:fun_with_flutter/application/auth/sign_in_form/sign_in_form_bloc.dart'; 18 | import 'package:fun_with_flutter/application/auth/auth_bloc.dart'; 19 | import 'package:fun_with_flutter/application/blog/blog_bloc.dart'; 20 | import 'package:fun_with_flutter/application/contact_form/bloc/contact_form_bloc.dart'; 21 | import 'package:get_it/get_it.dart'; 22 | 23 | void $initGetIt(GetIt g, {String environment}) { 24 | final firebaseInjectableModule = _$FirebaseInjectableModule(); 25 | g.registerLazySingleton( 26 | () => firebaseInjectableModule.firebaseAuth); 27 | g.registerLazySingleton( 28 | () => firebaseInjectableModule.googleSignIn); 29 | g.registerLazySingleton( 30 | () => FirebaseAuthFacade(g(), g())); 31 | g.registerFactory(() => ContactFormRepository()); 32 | g.registerFactory(() => SignInFormBloc(g())); 33 | g.registerFactory(() => AuthBloc(g())); 34 | g.registerFactory(() => BlogBloc(g())); 35 | g.registerFactory( 36 | () => ContactFormBloc(g())); 37 | 38 | //Register prod Dependencies -------- 39 | if (environment == 'prod') { 40 | g.registerFactory(() => BlogRepository()); 41 | } 42 | 43 | //Register dev Dependencies -------- 44 | if (environment == 'dev') { 45 | g.registerFactory(() => DevBlogRepository()); 46 | } 47 | } 48 | 49 | class _$FirebaseInjectableModule extends FirebaseInjectableModule {} 50 | -------------------------------------------------------------------------------- /lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'package:bloc/bloc.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter_bloc/flutter_bloc.dart'; 4 | import 'package:injectable/injectable.dart'; 5 | 6 | import 'application/auth/auth_bloc.dart'; 7 | import 'application/blog/blog_bloc.dart'; 8 | import 'application/contact_form/bloc/contact_form_bloc.dart'; 9 | import 'application/filtered_blog/filtered_blog_bloc.dart'; 10 | import 'application/page/page_bloc.dart'; 11 | import 'application/simple_bloc_delegate.dart'; 12 | import 'application/theme/bloc/theme_bloc.dart'; 13 | import 'injection.dart'; 14 | import 'presentation/core/app_widget.dart'; 15 | 16 | void main() { 17 | WidgetsFlutterBinding.ensureInitialized(); 18 | String env = Environment.prod; 19 | assert(() { 20 | env = Environment.dev; 21 | BlocSupervisor.delegate = SimpleBlocDelegate(); 22 | return true; 23 | }()); 24 | configureInjection(env); 25 | 26 | runApp(MultiBlocProvider( 27 | providers: [ 28 | BlocProvider( 29 | create: (context) => 30 | getIt()..add(const AuthEvent.authCheckRequested()), 31 | ), 32 | BlocProvider( 33 | create: (context) => getIt()..add(const BlogEvent.fetch()), 34 | ), 35 | BlocProvider( 36 | create: (context) => getIt(), 37 | ), 38 | BlocProvider( 39 | create: (context) { 40 | return FilterBlogBloc(blogBloc: BlocProvider.of(context)); 41 | }, 42 | // lazy loading off to allow blog filtering before 43 | // the blog page has been accessed 44 | lazy: false, 45 | ), 46 | BlocProvider(create: (context) { 47 | return PageBloc(); 48 | }), 49 | BlocProvider(create: (context) { 50 | return ThemeBloc(); 51 | }), 52 | ], 53 | child: const MyApp(), 54 | )); 55 | } 56 | -------------------------------------------------------------------------------- /lib/presentation/app/components/app_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_bloc/flutter_bloc.dart'; 3 | 4 | import '../../../application/page/page_bloc.dart'; 5 | import '../../common/error_widget.dart'; 6 | import '../../pages/blog/blog_page.dart'; 7 | import '../../pages/contact/contact_page.dart'; 8 | import '../../pages/home/home_page.dart'; 9 | 10 | class AppPage extends StatelessWidget { 11 | const AppPage(); 12 | 13 | @override 14 | Widget build(BuildContext context) { 15 | return BlocBuilder( 16 | builder: (BuildContext context, PageState state) { 17 | switch (state) { 18 | case PageState.home: 19 | return const HomePage(); 20 | break; 21 | case PageState.blog: 22 | return const FilteredBlogPage(); 23 | break; 24 | case PageState.contact: 25 | return const ContactUsPage(); 26 | break; 27 | default: 28 | return const CustomError( 29 | errorMessage: ''' 30 | Whoops, you found something that should not exist. Or maybe it should and it's an error. Who knows? 31 | ''', 32 | ); 33 | break; 34 | } 35 | }, 36 | ); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /lib/presentation/app/components/error_listener.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_bloc/flutter_bloc.dart'; 3 | 4 | import '../../../application/blog/blog_bloc.dart'; 5 | import '../../core/constants.dart'; 6 | import '../../core/extensions.dart'; 7 | import '../../core/notification_helper.dart'; 8 | 9 | class ErrorListener extends StatelessWidget { 10 | const ErrorListener({Key key, this.child}) : super(key: key); 11 | 12 | final Widget child; 13 | 14 | @override 15 | Widget build(BuildContext context) { 16 | return MultiBlocListener( 17 | listeners: [ 18 | BlocListener( 19 | listener: (context, state) { 20 | state.maybeMap( 21 | error: (error) { 22 | NotificationHelper.error( 23 | message: 'Could not load blog data', 24 | isPhone: isPhoneSize(context.mediaSize), 25 | ).show(context); 26 | }, 27 | orElse: () {}, 28 | ); 29 | }, 30 | ), 31 | ], 32 | child: child, 33 | ); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /lib/presentation/common/accent_button.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:google_fonts/google_fonts.dart'; 3 | 4 | class AccentButton extends StatelessWidget { 5 | const AccentButton({ 6 | Key key, 7 | @required this.onPressed, 8 | @required this.label, 9 | }) : assert(onPressed != null), 10 | assert(label != null), 11 | super(key: key); 12 | 13 | final VoidCallback onPressed; 14 | final String label; 15 | 16 | @override 17 | Widget build(BuildContext context) { 18 | return FlatButton( 19 | onPressed: onPressed, 20 | color: Theme.of(context).accentColor, 21 | shape: RoundedRectangleBorder( 22 | borderRadius: BorderRadius.circular(8), 23 | ), 24 | child: Text( 25 | label, 26 | style: GoogleFonts.firaCode( 27 | textStyle: const TextStyle(fontSize: 18, color: Colors.white), 28 | ), 29 | ), 30 | ); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /lib/presentation/common/error_widget.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import '../core/themes.dart'; 4 | 5 | @immutable 6 | class CustomError extends StatelessWidget { 7 | const CustomError({Key key, @required this.errorMessage}) 8 | : assert(errorMessage != null), 9 | super(key: key); 10 | 11 | final String errorMessage; 12 | 13 | @override 14 | Widget build(BuildContext context) { 15 | return Center( 16 | child: Padding( 17 | padding: const EdgeInsets.all(8), 18 | child: Text( 19 | errorMessage, 20 | overflow: TextOverflow.fade, 21 | textAlign: TextAlign.center, 22 | style: Theme.of(context).textTheme.headline6.copyWith( 23 | color: AppTheme.errorColor, 24 | ), 25 | ), // TODO(Gordon): expand this 26 | ), 27 | ); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /lib/presentation/common/header.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:google_fonts/google_fonts.dart'; 3 | 4 | import '../core/constants.dart'; 5 | 6 | class SliverHeader extends StatelessWidget { 7 | const SliverHeader({ 8 | Key key, 9 | @required this.label, 10 | }) : super(key: key); 11 | 12 | final String label; 13 | 14 | @override 15 | Widget build(BuildContext context) { 16 | return SliverToBoxAdapter( 17 | child: Header(label: label), 18 | ); 19 | } 20 | } 21 | 22 | class Header extends StatelessWidget { 23 | const Header({ 24 | Key key, 25 | @required this.label, 26 | }) : super(key: key); 27 | 28 | final String label; 29 | 30 | @override 31 | Widget build(BuildContext context) { 32 | return Center( 33 | child: ConstrainedBox( 34 | constraints: const BoxConstraints(maxWidth: kMaxBodyWidth), 35 | child: Align( 36 | alignment: Alignment.centerLeft, 37 | child: Padding( 38 | padding: const EdgeInsets.symmetric(vertical: 16.0), 39 | child: Text( 40 | label, 41 | style: GoogleFonts.firaCode(fontSize: 32), 42 | ), 43 | ), 44 | ), 45 | ), 46 | ); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /lib/presentation/common/info_bar.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/gestures.dart'; 2 | import 'package:flutter/material.dart'; 3 | 4 | import '../../infrastructure/core/urls.dart' as url; 5 | import '../core/utils/custom_icons_icons.dart'; 6 | import '../core/utils/url_handler.dart'; 7 | 8 | class InfoBar extends StatelessWidget { 9 | const InfoBar({Key key}) : super(key: key); 10 | 11 | @override 12 | Widget build(BuildContext context) { 13 | return Padding( 14 | padding: const EdgeInsets.all(8.0), 15 | child: Center( 16 | child: Column( 17 | children: [ 18 | RichText( 19 | text: TextSpan( 20 | children: [ 21 | const TextSpan(text: 'Made in '), 22 | TextSpan( 23 | text: 'Flutter', 24 | recognizer: TapGestureRecognizer() 25 | ..onTap = () { 26 | launchURL(url.flutterDev); 27 | }, 28 | style: TextStyle(color: Theme.of(context).accentColor), 29 | ), 30 | const TextSpan(text: ' by '), 31 | TextSpan( 32 | text: 'Gordon Hayes', 33 | recognizer: TapGestureRecognizer() 34 | ..onTap = () { 35 | launchURL(url.funWithTwitter); 36 | }, 37 | style: TextStyle(color: Theme.of(context).accentColor), 38 | ), 39 | ], 40 | style: Theme.of(context).textTheme.caption, 41 | ), 42 | ), 43 | const _IconBar(), 44 | Text('Copyright FunWithFlutter © 2020', 45 | style: Theme.of(context).textTheme.overline), 46 | ], 47 | ), 48 | ), 49 | ); 50 | } 51 | } 52 | 53 | class SliverBottomInfoBar extends StatelessWidget { 54 | const SliverBottomInfoBar({ 55 | Key key, 56 | }) : super(key: key); 57 | 58 | @override 59 | Widget build(BuildContext context) { 60 | return const SliverToBoxAdapter( 61 | child: Padding( 62 | padding: EdgeInsets.symmetric(vertical: 16.0), 63 | child: InfoBar(), 64 | ), 65 | ); 66 | } 67 | } 68 | 69 | class _IconBar extends StatelessWidget { 70 | const _IconBar({ 71 | Key key, 72 | }) : super(key: key); 73 | 74 | @override 75 | Widget build(BuildContext context) { 76 | return Wrap( 77 | alignment: WrapAlignment.center, 78 | children: const [ 79 | SizedBox( 80 | width: 16, 81 | ), 82 | _IconBarButton( 83 | iconData: CustomIcons.youtube, 84 | url: url.funWithYouTubeUrl, 85 | ), 86 | _IconBarButton( 87 | iconData: CustomIcons.githubCircled, 88 | url: url.funWithGithubUrl, 89 | ), 90 | _IconBarButton( 91 | iconData: CustomIcons.twitterSquared, 92 | url: url.funWithTwitter, 93 | ), 94 | // _IconBarButton( 95 | // iconData: CustomIcons.coffee, 96 | // url: url.funWithPatreon, 97 | // ), 98 | ], 99 | ); 100 | } 101 | } 102 | 103 | class _IconBarButton extends StatefulWidget { 104 | const _IconBarButton({Key key, this.url, this.iconData}) : super(key: key); 105 | 106 | final String url; 107 | final IconData iconData; 108 | 109 | @override 110 | _IconBarButtonState createState() => _IconBarButtonState(); 111 | } 112 | 113 | class _IconBarButtonState extends State<_IconBarButton> { 114 | // static const Color _stationaryColor = Colors.white; 115 | // static const Color _hoverColor = AppTheme.accentColor; 116 | // Color _color = _stationaryColor; 117 | bool _onHover = false; 118 | 119 | void _onTap() { 120 | launchURL(widget.url); 121 | } 122 | 123 | // void _changeButtonColor(Color color) { 124 | // setState(() { 125 | // _color = color; 126 | // }); 127 | // } 128 | 129 | void _onPointerEnter(PointerEnterEvent event) { 130 | // _changeButtonColor(_hoverColor); 131 | setState(() { 132 | _onHover = true; 133 | }); 134 | } 135 | 136 | void _onPointerExit(PointerExitEvent event) { 137 | // _changeButtonColor(_stationaryColor); 138 | setState(() { 139 | _onHover = false; 140 | }); 141 | } 142 | 143 | @override 144 | Widget build(BuildContext context) { 145 | return MouseRegion( 146 | onEnter: _onPointerEnter, 147 | onExit: _onPointerExit, 148 | child: GestureDetector( 149 | onTap: _onTap, 150 | child: Padding( 151 | padding: const EdgeInsets.all(8.0), 152 | child: Icon( 153 | widget.iconData, 154 | color: _onHover ? Theme.of(context).accentColor : null, 155 | size: 32, 156 | ), 157 | ), 158 | ), 159 | ); 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /lib/presentation/common/loading_indicator.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class LoadingIndicator extends StatelessWidget { 4 | const LoadingIndicator({Key key}) : super(key: key); 5 | 6 | @override 7 | Widget build(BuildContext context) { 8 | return const Center( 9 | child: Padding( 10 | padding: EdgeInsets.symmetric(horizontal: 8.0, vertical: 16), 11 | child: CircularProgressIndicator(), 12 | ), 13 | ); 14 | } 15 | } 16 | 17 | class SliverLoadingIndicator extends StatelessWidget { 18 | const SliverLoadingIndicator({ 19 | Key key, 20 | }) : super(key: key); 21 | 22 | @override 23 | Widget build(BuildContext context) { 24 | return const SliverToBoxAdapter( 25 | child: LoadingIndicator(), 26 | ); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /lib/presentation/core/adaptive_dialog.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | const kDialogSize = Size(450, 450); 4 | 5 | class AdaptiveDialog extends StatelessWidget { 6 | final Widget child; 7 | 8 | const AdaptiveDialog({ 9 | Key key, 10 | @required this.child, 11 | }) : super(key: key); 12 | 13 | @override 14 | Widget build(BuildContext context) { 15 | return LayoutBuilder( 16 | builder: (context, dimens) { 17 | if (dimens.maxWidth < kDialogSize.width || 18 | dimens.maxHeight < kDialogSize.height) { 19 | return Center(child: child); 20 | } 21 | return Center( 22 | child: SizedBox.fromSize( 23 | size: kDialogSize, 24 | child: child, 25 | ), 26 | ); 27 | }, 28 | ); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /lib/presentation/core/app_widget.dart: -------------------------------------------------------------------------------- 1 | import 'package:auto_route/auto_route.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter_bloc/flutter_bloc.dart'; 4 | 5 | import '../../application/theme/bloc/theme_bloc.dart'; 6 | import '../routes/router.gr.dart'; 7 | import 'themes.dart'; 8 | 9 | class MyApp extends StatelessWidget { 10 | const MyApp({Key key}) : super(key: key); 11 | 12 | @override 13 | Widget build(BuildContext context) { 14 | return BlocBuilder( 15 | builder: (context, state) { 16 | return MaterialApp( 17 | title: 'Fun with Flutter', 18 | theme: AppTheme.lightTheme(), 19 | darkTheme: AppTheme.darkTheme(), 20 | themeMode: state.map( 21 | light: (_) => ThemeMode.light, 22 | dark: (_) => ThemeMode.dark, 23 | ), 24 | builder: ExtendedNavigator(router: Router()), 25 | debugShowCheckedModeBanner: false, 26 | ); 27 | }, 28 | ); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /lib/presentation/core/constants.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/widgets.dart'; 2 | 3 | const kTabletBreakpoint = 720.0; 4 | const kDesktopBreakpoint = 1400.0; 5 | const kSideMenuWidth = 200.0; 6 | 7 | const kMaxBodyWidth = 850.0; 8 | 9 | bool isPhoneSize(Size screenSize) { 10 | return screenSize.width < kTabletBreakpoint; 11 | } 12 | -------------------------------------------------------------------------------- /lib/presentation/core/extensions.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/widgets.dart'; 2 | 3 | extension SizeExtension on BuildContext { 4 | /// Performs a lookup using the `BuildContext` to obtain 5 | /// the MediaQuery size 6 | /// 7 | /// Calling this method is equivalent to calling: 8 | /// 9 | /// ```dart 10 | /// MediaQuery.of(context).size 11 | /// ``` 12 | Size get mediaSize => MediaQuery.of(this).size; 13 | } 14 | -------------------------------------------------------------------------------- /lib/presentation/core/html_element_widget.dart: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // 3 | // Copyright (c) 2020 Simon Lightfoot 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in all 13 | // copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | // SOFTWARE. 22 | // 23 | import 'dart:html' as html; 24 | import 'dart:ui' as ui; 25 | 26 | import 'package:flutter/foundation.dart'; 27 | import 'package:flutter/gestures.dart' show OneSequenceGestureRecognizer; 28 | import 'package:flutter/material.dart'; 29 | import 'package:flutter/rendering.dart'; 30 | import 'package:flutter/services.dart' 31 | show PlatformViewController, SystemChannels; 32 | 33 | final _widgetRef = {}; 34 | 35 | abstract class HtmlElementWidget extends StatefulWidget { 36 | const HtmlElementWidget({ 37 | Key key, 38 | }) : super(key: key); 39 | 40 | html.HtmlElement createHtmlElement(BuildContext context); 41 | 42 | @override 43 | _HtmlElementWidgetState createState() => _HtmlElementWidgetState(); 44 | } 45 | 46 | class _HtmlElementWidgetState extends State { 47 | static bool _registered = false; 48 | 49 | @override 50 | void initState() { 51 | super.initState(); 52 | if (!_registered) { 53 | registerPlatformView(); 54 | _registered = true; 55 | } 56 | } 57 | 58 | void registerPlatformView() { 59 | if (kIsWeb) { 60 | // ignore: undefined_prefixed_name 61 | ui.platformViewRegistry.registerViewFactory( 62 | 'HtmlElementWidget', 63 | (int viewId) { 64 | final widget = _widgetRef[viewId]; 65 | assert(widget != null); 66 | return widget.createHtmlElement(context); 67 | }, 68 | ); 69 | } 70 | } 71 | 72 | @override 73 | Widget build(BuildContext context) { 74 | return PlatformViewLink( 75 | viewType: 'HtmlElementWidget', 76 | onCreatePlatformView: (PlatformViewCreationParams params) { 77 | _widgetRef[params.id] = widget; 78 | final controller = _HtmlElementViewController(params); 79 | controller._initialize().then((_) { 80 | params.onPlatformViewCreated(params.id); 81 | }); 82 | return controller; 83 | }, 84 | surfaceFactory: (_, PlatformViewController controller) { 85 | return PlatformViewSurface( 86 | controller: controller, 87 | gestureRecognizers: const >{}, 88 | hitTestBehavior: PlatformViewHitTestBehavior.opaque, 89 | ); 90 | }, 91 | ); 92 | } 93 | } 94 | 95 | class _HtmlElementViewController extends PlatformViewController { 96 | _HtmlElementViewController(this.params); 97 | 98 | final PlatformViewCreationParams params; 99 | 100 | @override 101 | int get viewId => params.id; 102 | 103 | bool _initialized = false; 104 | 105 | Future _initialize() async { 106 | await SystemChannels.platform_views.invokeMethod( 107 | 'create', 108 | {'id': viewId, 'viewType': params.viewType}, 109 | ); 110 | _initialized = true; 111 | } 112 | 113 | @override 114 | void clearFocus() { 115 | // Currently this does nothing on Flutter Web. 116 | // TODO(het): Implement this. See https://github.com/flutter/flutter/issues/39496 117 | } 118 | 119 | @override 120 | void dispatchPointerEvent(PointerEvent event) { 121 | // We do not dispatch pointer events to HTML views because they may contain 122 | // cross-origin iframes, which only accept user-generated events. 123 | } 124 | 125 | @override 126 | void dispose() { 127 | if (_initialized) { 128 | // Asynchronously dispose this view. 129 | SystemChannels.platform_views.invokeMethod('dispose', viewId); 130 | } 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /lib/presentation/core/iframe_widget.dart: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // 3 | // Copyright (c) 2020 Simon Lightfoot 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in all 13 | // copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | // SOFTWARE. 22 | // 23 | import 'dart:html'; 24 | 25 | import 'package:flutter/foundation.dart'; 26 | import 'package:flutter/material.dart'; 27 | import 'html_element_widget.dart'; 28 | 29 | class IFrameWidget extends HtmlElementWidget { 30 | const IFrameWidget({ 31 | Key key, 32 | this.width, 33 | this.height, 34 | this.src, 35 | this.style, 36 | this.allow, 37 | this.allowFullscreen, 38 | }) : super(key: key); 39 | 40 | final double width; 41 | final double height; 42 | final String src; 43 | final String style; 44 | final String allow; 45 | final bool allowFullscreen; 46 | 47 | @override 48 | HtmlElement createHtmlElement(BuildContext context) { 49 | return IFrameElement() 50 | ..width = width.toString() 51 | ..height = height.toString() 52 | ..src = src 53 | ..style.cssText = style 54 | ..allow = allow 55 | ..allowFullscreen = allowFullscreen; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /lib/presentation/core/notification_helper.dart: -------------------------------------------------------------------------------- 1 | import 'package:flushbar/flushbar.dart'; 2 | import 'package:flutter/material.dart'; 3 | 4 | import 'themes.dart'; 5 | 6 | class NotificationHelper { 7 | static Flushbar error({ 8 | @required String message, 9 | String title, 10 | Duration duration = const Duration(seconds: 3), 11 | bool isPhone = false, 12 | }) { 13 | return Flushbar( 14 | title: title, 15 | message: message, 16 | duration: duration, 17 | maxWidth: isPhone ? null : 300, 18 | flushbarPosition: 19 | isPhone ? FlushbarPosition.BOTTOM : FlushbarPosition.TOP, 20 | flushbarStyle: FlushbarStyle.GROUNDED, 21 | icon: const Icon( 22 | Icons.warning, 23 | size: 28.0, 24 | color: AppTheme.errorColor, 25 | ), 26 | ); 27 | } 28 | 29 | static Flushbar success({ 30 | @required String message, 31 | String title, 32 | Duration duration = const Duration(seconds: 3), 33 | bool isPhone = false, 34 | }) { 35 | return Flushbar( 36 | title: title, 37 | message: message, 38 | duration: duration, 39 | maxWidth: isPhone ? null : 300, 40 | flushbarPosition: 41 | isPhone ? FlushbarPosition.BOTTOM : FlushbarPosition.TOP, 42 | flushbarStyle: FlushbarStyle.GROUNDED, 43 | icon: Icon( 44 | Icons.check_circle, 45 | color: Colors.green[300], 46 | ), 47 | ); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /lib/presentation/core/themes.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class AppTheme { 4 | static const Color primaryColor = Color(0xFF284B63); 5 | // TODO these two are not used 6 | // static const Color secondaryColor = Color.fromRGBO(1, 87, 155, 1); 7 | // static const Color secondaryColor = Colors.yellow; 8 | static const Color accentColor = Color(0xFFFF6B6B); 9 | 10 | static const Color surfaceColor = Color(0xFFF2F2F2); 11 | static const Color backgroundColor = Color(0xFFFAFAFA); 12 | 13 | static const Color errorColor = Color(0xFFe63946); 14 | 15 | static const double fontSizeMedium = 14; 16 | 17 | static ThemeData darkTheme() { 18 | return ThemeData.dark().copyWith( 19 | primaryColor: primaryColor, 20 | accentColor: accentColor, 21 | errorColor: errorColor, 22 | colorScheme: ThemeData.dark().colorScheme.copyWith( 23 | secondary: surfaceColor, 24 | ), 25 | navigationRailTheme: const NavigationRailThemeData( 26 | selectedIconTheme: IconThemeData(color: accentColor), 27 | unselectedIconTheme: IconThemeData(color: Colors.black54), 28 | ), 29 | bottomNavigationBarTheme: const BottomNavigationBarThemeData( 30 | selectedIconTheme: IconThemeData(color: accentColor), 31 | unselectedIconTheme: IconThemeData( 32 | color: surfaceColor, 33 | ), 34 | ), 35 | primaryIconTheme: const IconThemeData( 36 | size: 30, 37 | color: surfaceColor, 38 | ), 39 | iconTheme: const IconThemeData( 40 | size: 30, 41 | color: surfaceColor, 42 | ), 43 | ); 44 | } 45 | 46 | static ThemeData lightTheme() { 47 | return ThemeData.light().copyWith( 48 | primaryColor: primaryColor, 49 | accentColor: accentColor, 50 | dividerColor: accentColor, 51 | errorColor: errorColor, 52 | colorScheme: ThemeData.light().colorScheme.copyWith( 53 | primary: primaryColor, 54 | secondary: Colors.black54, 55 | surface: surfaceColor, 56 | onSurface: surfaceColor, 57 | ), 58 | navigationRailTheme: const NavigationRailThemeData( 59 | backgroundColor: surfaceColor, 60 | selectedIconTheme: IconThemeData(color: accentColor), 61 | unselectedIconTheme: IconThemeData(color: Colors.black54), 62 | ), 63 | bottomNavigationBarTheme: const BottomNavigationBarThemeData( 64 | selectedIconTheme: IconThemeData(color: accentColor), 65 | unselectedIconTheme: IconThemeData( 66 | color: Colors.black54, 67 | ), 68 | backgroundColor: surfaceColor, 69 | ), 70 | primaryIconTheme: const IconThemeData( 71 | color: primaryColor, 72 | size: 30, 73 | ), 74 | iconTheme: const IconThemeData( 75 | color: primaryColor, 76 | size: 30, 77 | ), 78 | ); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /lib/presentation/core/utils/custom_icons_icons.dart: -------------------------------------------------------------------------------- 1 | /// Flutter icons CustomIcons 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: CustomIcons 11 | /// fonts: 12 | /// - asset: fonts/CustomIcons.ttf 13 | /// 14 | /// 15 | /// * Typicons, (c) Stephen Hutchings 2012 16 | /// Author: Stephen Hutchings 17 | /// License: SIL (http://scripts.sil.org/OFL) 18 | /// Homepage: http://typicons.com/ 19 | /// * Font Awesome, Copyright (C) 2016 by Dave Gandy 20 | /// Author: Dave Gandy 21 | /// License: SIL () 22 | /// Homepage: http://fortawesome.github.com/Font-Awesome/ 23 | /// 24 | import 'package:flutter/widgets.dart'; 25 | 26 | class CustomIcons { 27 | CustomIcons._(); 28 | 29 | static const _kFontFam = 'CustomIcons'; 30 | 31 | static const IconData coffee = IconData(0xe800, fontFamily: _kFontFam); 32 | static const IconData githubCircled = 33 | IconData(0xf09b, fontFamily: _kFontFam); 34 | static const IconData youtube = IconData(0xf167, fontFamily: _kFontFam); 35 | static const IconData google = IconData(0xf1a0, fontFamily: _kFontFam); 36 | static const IconData medium = IconData(0xf23a, fontFamily: _kFontFam); 37 | static const IconData twitterSquared = 38 | IconData(0xf304, fontFamily: _kFontFam); 39 | } 40 | -------------------------------------------------------------------------------- /lib/presentation/core/utils/tag_name_generator.dart: -------------------------------------------------------------------------------- 1 | 2 | // Todo(Gordon): does not need to be a class 3 | class TagDisplayNameGenerator { 4 | static const tagNames = { 5 | 'flutter': 'Flutter', 6 | 'architecture': 'Architecture', 7 | 'dependency-injection': 'Dependency Injection', 8 | 'animation': 'Animation', 9 | 'ui-layout': 'UI Layout', 10 | 'dart': 'Dart', 11 | 'cheat-sheet': 'Cheat Sheet', 12 | 'flutter-web': 'Flutter Web' 13 | }; 14 | 15 | static String mapTagToDisplayName(String tag) { 16 | if (tagNames.containsKey(tag)) { 17 | return tagNames[tag]; 18 | } 19 | return tag; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /lib/presentation/core/utils/url_handler.dart: -------------------------------------------------------------------------------- 1 | import 'package:url_launcher/url_launcher.dart'; 2 | 3 | Future launchURL(String url) async { 4 | if (await canLaunch(url)) { 5 | await launch(url); 6 | } else { 7 | throw 'Could not launch $url'; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /lib/presentation/pages/blog/blog_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_bloc/flutter_bloc.dart'; 3 | 4 | import '../../../application/filtered_blog/filtered_blog_bloc.dart'; 5 | import '../../../domain/blog/tag.dart'; 6 | import '../../common/header.dart'; 7 | import '../../common/info_bar.dart'; 8 | import '../../common/loading_indicator.dart'; 9 | import '../../core/constants.dart'; 10 | import 'widgets/blog_posts.dart'; 11 | import 'widgets/tag_widgets.dart'; 12 | 13 | class FilteredBlogPage extends StatelessWidget { 14 | const FilteredBlogPage(); 15 | @override 16 | Widget build(BuildContext context) { 17 | return BlocBuilder( 18 | builder: (BuildContext context, FilterBlogState state) { 19 | return Center( 20 | child: LayoutBuilder( 21 | builder: (context, constraints) { 22 | return Scrollbar( 23 | child: CustomScrollView( 24 | slivers: [ 25 | const SliverSafeArea( 26 | sliver: SliverHeader(label: 'Blog Posts'), 27 | ), 28 | _displayTags(state), 29 | _displayPosts( 30 | context, 31 | state, 32 | constraints.maxWidth > kMaxBodyWidth 33 | ? kMaxBodyWidth 34 | : constraints.maxWidth, 35 | constraints.maxWidth, 36 | ), 37 | const SliverBottomInfoBar() 38 | ], 39 | ), 40 | ); 41 | }, 42 | ), 43 | ); 44 | }, 45 | ); 46 | } 47 | 48 | Widget _displayTags( 49 | FilterBlogState state, 50 | ) { 51 | return state.map( 52 | error: (_) => const SliverToBoxAdapter(), 53 | loading: (_) => const SliverToBoxAdapter(), 54 | loaded: (s) => SliverToBoxAdapter( 55 | child: TagsSelection( 56 | tags: s.filteredBlog.tags, 57 | currentTag: s.tagFilter, 58 | ), 59 | ), 60 | ); 61 | } 62 | 63 | Widget _displayPosts(BuildContext context, FilterBlogState state, 64 | double width, double maxWidth) { 65 | return state.map( 66 | loading: (_) { 67 | return const SliverLoadingIndicator(); 68 | }, 69 | error: (_) { 70 | return const NoBlogPosts(); 71 | }, 72 | loaded: (state) { 73 | return BlogPosts( 74 | blog: state.filteredBlog, 75 | width: width, 76 | maxWidth: maxWidth, 77 | onTagTap: (tag) { 78 | context.bloc().add( 79 | FilterBlogEvent.filterByTag(tag), 80 | ); 81 | }, 82 | ); 83 | }, 84 | ); 85 | } 86 | } 87 | 88 | class TagsSelection extends StatelessWidget { 89 | const TagsSelection({ 90 | Key key, 91 | @required this.tags, 92 | @required this.currentTag, 93 | }) : super(key: key); 94 | 95 | final List tags; 96 | final String currentTag; 97 | 98 | @override 99 | Widget build(BuildContext context) { 100 | return Padding( 101 | padding: const EdgeInsets.all(8.0), 102 | child: Column( 103 | children: [ 104 | Wrap( 105 | alignment: WrapAlignment.center, 106 | children: [ 107 | ...tags?.map( 108 | (tag) { 109 | TagConfiguartion tagConfig; 110 | if (tag.name == currentTag) { 111 | tagConfig = 112 | TagConfiguartion.bigTag(tag.name, isSelected: true); 113 | } else { 114 | tagConfig = TagConfiguartion.bigTag(tag.name); 115 | } 116 | return TagWidget( 117 | tagConfig: tagConfig, 118 | onTap: (tag) { 119 | BlocProvider.of(context).add( 120 | FilterBlogEvent.filterByTag(tagConfig.tag), 121 | ); 122 | }, 123 | ); 124 | }, 125 | )?.toList() 126 | ], 127 | ), 128 | ], 129 | ), 130 | ); 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /lib/presentation/pages/blog/widgets/blog_post_card.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import '../../../../domain/blog/post_data.dart'; 4 | import '../../../../infrastructure/url_repository.dart' as url_repository; 5 | import '../../../core/utils/url_handler.dart'; 6 | import 'tag_widgets.dart'; 7 | 8 | class BlogPostCard extends StatelessWidget { 9 | const BlogPostCard({ 10 | Key key, 11 | @required this.post, 12 | @required this.onTagTap, 13 | }) : super(key: key); 14 | final PostData post; 15 | 16 | static const double _cardWidth = 400; 17 | static double get cardWidth => _cardWidth; 18 | static const double _cardHeight = 430; 19 | static double get cardHeight => _cardHeight; 20 | 21 | final OnTagTap onTagTap; 22 | 23 | @override 24 | Widget build(BuildContext context) { 25 | return Center( 26 | child: ConstrainedBox( 27 | constraints: const BoxConstraints( 28 | maxWidth: _cardWidth, 29 | maxHeight: _cardHeight, 30 | minHeight: _cardHeight, 31 | ), 32 | child: InkWell( 33 | onTap: () { 34 | launchURL(post.uri); 35 | }, 36 | child: Card( 37 | color: Theme.of(context).colorScheme.surface, 38 | child: Column( 39 | mainAxisAlignment: MainAxisAlignment.spaceBetween, 40 | children: [ 41 | Align( 42 | alignment: Alignment.topCenter, 43 | child: Image.network( 44 | url_repository.blogThumbnailUrl(post.thumbnail), 45 | fit: BoxFit.contain, 46 | ), 47 | ), 48 | Padding( 49 | padding: const EdgeInsets.symmetric( 50 | horizontal: 16, 51 | ), 52 | child: Text( 53 | post?.title, 54 | overflow: TextOverflow.ellipsis, 55 | style: Theme.of(context).textTheme.headline5, 56 | ), 57 | ), 58 | Padding( 59 | padding: const EdgeInsets.symmetric( 60 | horizontal: 16, 61 | ), 62 | child: Text( 63 | post?.description, 64 | maxLines: 2, 65 | textAlign: TextAlign.center, 66 | ), 67 | ), 68 | Row( 69 | children: [ 70 | ...post.tags 71 | .map( 72 | (e) => TagWidget( 73 | tagConfig: TagConfiguartion.smallTag(e), 74 | onTap: onTagTap, 75 | ), 76 | ) 77 | .toList(), 78 | ], 79 | ) 80 | ], 81 | ), 82 | ), 83 | ), 84 | ), 85 | ); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /lib/presentation/pages/blog/widgets/blog_posts.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter/rendering.dart'; 3 | 4 | import '../../../../domain/blog/blog.dart'; 5 | import 'blog_post_card.dart'; 6 | import 'tag_widgets.dart'; 7 | 8 | class BlogPosts extends StatelessWidget { 9 | const BlogPosts({ 10 | Key key, 11 | @required this.blog, 12 | @required this.width, 13 | @required this.maxWidth, 14 | @required this.onTagTap, 15 | this.limitNumberOfBlogs, 16 | }) : super(key: key); 17 | 18 | final Blog blog; 19 | final double width; 20 | final double maxWidth; 21 | final int limitNumberOfBlogs; 22 | final OnTagTap onTagTap; 23 | 24 | int _numberOfBlogsToShow(int numberOfBlogsAvailable) { 25 | if (limitNumberOfBlogs == null) { 26 | return numberOfBlogsAvailable; 27 | } 28 | return (numberOfBlogsAvailable >= limitNumberOfBlogs) 29 | ? limitNumberOfBlogs 30 | : numberOfBlogsAvailable; 31 | } 32 | 33 | @override 34 | Widget build(BuildContext context) { 35 | int crossAxisCount; 36 | double childAspectRation; 37 | 38 | final padding = (maxWidth - width) / 2; 39 | 40 | if (maxWidth >= BlogPostCard.cardWidth * 2) { 41 | crossAxisCount = 2; 42 | childAspectRation = (width / 2) / BlogPostCard.cardHeight; 43 | } else { 44 | crossAxisCount = 1; 45 | childAspectRation = width / BlogPostCard.cardHeight; 46 | } 47 | return SliverPadding( 48 | padding: EdgeInsets.symmetric(horizontal: padding), 49 | sliver: SliverGrid( 50 | gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( 51 | crossAxisSpacing: 8, 52 | mainAxisSpacing: 16, 53 | crossAxisCount: crossAxisCount, 54 | childAspectRatio: childAspectRation, 55 | ), 56 | delegate: SliverChildBuilderDelegate( 57 | (context, index) { 58 | return BlogPostCard( 59 | key: ValueKey(blog.pages[index].title), 60 | post: blog.pages[index], 61 | onTagTap: onTagTap, 62 | ); 63 | }, 64 | childCount: _numberOfBlogsToShow(blog.pages.length), 65 | ), 66 | ), 67 | ); 68 | } 69 | } 70 | 71 | class NoBlogPosts extends StatelessWidget { 72 | const NoBlogPosts({ 73 | Key key, 74 | }) : super(key: key); 75 | 76 | @override 77 | Widget build(BuildContext context) { 78 | return const SliverToBoxAdapter( 79 | child: Center( 80 | child: Padding( 81 | padding: EdgeInsets.symmetric(horizontal: 8.0, vertical: 16), 82 | child: Text( 83 | 'No blog content to show :(', 84 | style: TextStyle(fontSize: 18), 85 | ), 86 | ), 87 | ), 88 | ); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /lib/presentation/pages/blog/widgets/tag_widgets.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import '../../../core/themes.dart'; 4 | 5 | class TagConfiguartion { 6 | final String tag; 7 | final double height; 8 | final double fontSize; 9 | final bool isSelected; 10 | final Color color = Colors.red[500]; 11 | 12 | TagConfiguartion._(this.tag, this.height, this.fontSize, this.isSelected); 13 | 14 | TagConfiguartion.bigTag(String tag, {bool isSelected = false}) 15 | : this._(tag, 40, 18, isSelected); 16 | TagConfiguartion.smallTag(String tag, {bool isSelected = false}) 17 | : this._(tag, 30, 12, isSelected); 18 | 19 | Color get getColor { 20 | if (!isSelected) { 21 | return AppTheme.accentColor; 22 | } 23 | return AppTheme.primaryColor; 24 | } 25 | } 26 | 27 | typedef OnTagTap = void Function(String tag); 28 | 29 | class TagWidget extends StatelessWidget { 30 | const TagWidget({ 31 | Key key, 32 | @required this.tagConfig, 33 | @required this.onTap, 34 | }) : super(key: key); 35 | 36 | final TagConfiguartion tagConfig; 37 | final OnTagTap onTap; 38 | 39 | @override 40 | Widget build(BuildContext context) { 41 | List shadow; 42 | final translate = Matrix4.identity(); 43 | if (tagConfig.isSelected) { 44 | translate.translate(0, -5); 45 | shadow = [ 46 | const BoxShadow( 47 | color: Colors.black26, 48 | spreadRadius: 3, 49 | blurRadius: 7, 50 | offset: Offset(0, 3), 51 | ), 52 | ]; 53 | } 54 | return Padding( 55 | padding: const EdgeInsets.all(8.0), 56 | child: InkWell( 57 | onTap: () { 58 | onTap(tagConfig.tag); 59 | }, 60 | child: AnimatedContainer( 61 | duration: const Duration(milliseconds: 200), 62 | padding: const EdgeInsets.all(8), 63 | transform: translate, 64 | decoration: BoxDecoration( 65 | borderRadius: BorderRadius.circular(5), 66 | boxShadow: shadow, 67 | color: tagConfig.getColor, 68 | ), 69 | height: tagConfig.height, 70 | child: Text( 71 | tagConfig.tag, 72 | style: TextStyle(fontSize: tagConfig.fontSize, color: Colors.white), 73 | ), 74 | ), 75 | ), 76 | ); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /lib/presentation/pages/contact/contact_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import '../../common/info_bar.dart'; 4 | import 'widgets/contact_us_form.dart'; 5 | 6 | class ContactUsPage extends StatelessWidget { 7 | const ContactUsPage(); 8 | @override 9 | Widget build(BuildContext context) { 10 | return SingleChildScrollView( 11 | child: SafeArea( 12 | child: Center( 13 | child: ConstrainedBox( 14 | constraints: const BoxConstraints(maxWidth: 800, minWidth: 300), 15 | child: Padding( 16 | padding: const EdgeInsets.all(8.0), 17 | child: Column( 18 | crossAxisAlignment: CrossAxisAlignment.start, 19 | children: [ 20 | Wrap( 21 | children: [ 22 | Column( 23 | crossAxisAlignment: CrossAxisAlignment.start, 24 | children: [ 25 | Text('Building something in Flutter?', 26 | style: Theme.of(context).textTheme.headline4), 27 | Text('Contact us', 28 | style: Theme.of(context).textTheme.headline5), 29 | ], 30 | ), 31 | ], 32 | ), 33 | const SizedBox( 34 | height: 32, 35 | ), 36 | const ContactUsForm(), 37 | // const IFrameWidget( 38 | // width: 640, 39 | // height: 360, 40 | // src: 'https://www.youtube.com/embed/2zwEdDoPvnc', 41 | // style: 'border: 0;', 42 | // allow: 43 | // 'accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture', 44 | // allowFullscreen: true, 45 | // ), 46 | const InfoBar(), 47 | ], 48 | ), 49 | ), 50 | ), 51 | ), 52 | ), 53 | ); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /lib/presentation/pages/contact/widgets/contact_us_form.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_bloc/flutter_bloc.dart'; 3 | import 'package:fun_with_flutter/presentation/common/accent_button.dart'; 4 | 5 | import '../../../../application/contact_form/bloc/contact_form_bloc.dart'; 6 | import '../../../core/constants.dart'; 7 | import '../../../core/extensions.dart'; 8 | import '../../../core/notification_helper.dart'; 9 | 10 | class ContactUsForm extends StatelessWidget { 11 | const ContactUsForm({Key key}) : super(key: key); 12 | 13 | @override 14 | Widget build(BuildContext context) { 15 | return BlocConsumer( 16 | listener: (context, state) => state.submitFailureOrSuccessOption.fold( 17 | () => () {}, 18 | (either) => either.fold( 19 | (failure) => NotificationHelper.error( 20 | message: failure.map(serverError: (_) => 'Server error'), 21 | isPhone: isPhoneSize(context.mediaSize), 22 | ).show(context), 23 | (_) => NotificationHelper.success( 24 | message: 'Form submitted successfully', 25 | isPhone: isPhoneSize(context.mediaSize), 26 | ).show(context), 27 | ), 28 | ), 29 | builder: (context, state) { 30 | final submitted = 31 | state.submitFailureOrSuccessOption.fold(() => false, (a) => true); 32 | if (submitted) { 33 | return const _FormSubmittedSuccess(); 34 | } 35 | return Container( 36 | padding: const EdgeInsets.all(8), 37 | color: Theme.of(context).colorScheme.surface, 38 | child: Form( 39 | autovalidate: state.showErrorMessages, 40 | child: Column( 41 | mainAxisAlignment: MainAxisAlignment.center, 42 | children: [ 43 | TextFormField( 44 | decoration: const InputDecoration( 45 | prefixIcon: Icon(Icons.email), 46 | labelText: 'Reply to', 47 | border: OutlineInputBorder(), 48 | enabledBorder: UnderlineInputBorder(), 49 | ), 50 | autocorrect: false, 51 | onChanged: (value) => context 52 | .bloc() 53 | .add(ContactFormEvent.emailChanged(value)), 54 | validator: (_) => 55 | context.bloc().state.replyTo.value.fold( 56 | (f) => f.maybeMap( 57 | invalidEmail: (_) => 'Invalid Email', 58 | orElse: () => null, 59 | ), 60 | (_) => null, 61 | ), 62 | ), 63 | const SizedBox(height: 8), 64 | TextFormField( 65 | decoration: const InputDecoration( 66 | prefixIcon: Icon(Icons.face), 67 | labelText: 'Name', 68 | border: OutlineInputBorder(), 69 | enabledBorder: UnderlineInputBorder(), 70 | ), 71 | onChanged: (value) => context 72 | .bloc() 73 | .add(ContactFormEvent.nameChanged(value)), 74 | validator: (_) => context 75 | .bloc() 76 | .state 77 | .name 78 | .value 79 | .fold( 80 | (f) => f.maybeMap( 81 | exceedingLength: (_) => 'Input too long', 82 | empty: (_) => 'Input required', 83 | multiline: (_) => 'Input cannot be multiple lines', 84 | orElse: () => null, 85 | ), 86 | (_) => null, 87 | ), 88 | ), 89 | const SizedBox(height: 12), 90 | TextFormField( 91 | maxLines: 5, 92 | decoration: const InputDecoration( 93 | prefixIcon: Icon(Icons.message), 94 | labelText: 'Message', 95 | border: OutlineInputBorder(), 96 | enabledBorder: UnderlineInputBorder(), 97 | ), 98 | onChanged: (value) => context 99 | .bloc() 100 | .add(ContactFormEvent.messageChanged(value)), 101 | validator: (_) => 102 | context.bloc().state.message.value.fold( 103 | (f) => f.maybeMap( 104 | exceedingLength: (_) => 'Input too long', 105 | empty: (_) => 'Input required', 106 | orElse: () => null, 107 | ), 108 | (_) => null, 109 | ), 110 | ), 111 | const SizedBox(height: 8), 112 | AccentButton( 113 | onPressed: () { 114 | context 115 | .bloc() 116 | .add(const ContactFormEvent.submit()); 117 | }, 118 | label: 'SUBMIT', 119 | ), 120 | if (state.isSubmitting) ...[ 121 | const SizedBox(height: 8), 122 | const LinearProgressIndicator(), 123 | ] 124 | ], 125 | ), 126 | ), 127 | ); 128 | }, 129 | ); 130 | } 131 | } 132 | 133 | class _FormSubmittedSuccess extends StatelessWidget { 134 | const _FormSubmittedSuccess({Key key}) : super(key: key); 135 | 136 | @override 137 | Widget build(BuildContext context) { 138 | return Column( 139 | children: const [ 140 | Text('Thank you! We will get back to you as soon as possible.'), 141 | Text('Alternatively, contact us directly at funwithflutter@gmail.com'), 142 | ], 143 | ); 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /lib/presentation/pages/home/components/sliver_course_header.dart: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/funwithflutter/fun-with-flutter-website/b163e23345901b1937c29da1d7bc18ad7283b879/lib/presentation/pages/home/components/sliver_course_header.dart -------------------------------------------------------------------------------- /lib/presentation/pages/home/home_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_bloc/flutter_bloc.dart'; 3 | 4 | import '../../../application/blog/blog_bloc.dart'; 5 | import '../../../application/filtered_blog/filtered_blog_bloc.dart'; 6 | import '../../../application/page/page_bloc.dart'; 7 | import '../../common/header.dart'; 8 | import '../../common/info_bar.dart'; 9 | import '../../common/loading_indicator.dart'; 10 | import '../../core/constants.dart'; 11 | import '../blog/widgets/blog_posts.dart'; 12 | import 'widgets/sliver_course_header.dart'; 13 | 14 | class HomePage extends StatelessWidget { 15 | const HomePage(); 16 | @override 17 | Widget build(BuildContext context) { 18 | // TODO create a standard implementation for all of the different pages with scrolling 19 | // and make it in such a way that the entire page is withing the scrollable area 20 | return BlocBuilder( 21 | builder: (BuildContext context, BlogState state) { 22 | return Center( 23 | child: LayoutBuilder( 24 | builder: (context, constraints) { 25 | return Scrollbar( 26 | child: CustomScrollView( 27 | slivers: [ 28 | const SliverSafeArea( 29 | sliver: SliverHeader(label: 'Courses'), 30 | ), 31 | const SliverCourseHeader(), 32 | const SliverHeader(label: 'Recent blog posts'), 33 | _displayPosts( 34 | context, 35 | state, 36 | constraints.maxWidth > kMaxBodyWidth 37 | ? kMaxBodyWidth 38 | : constraints.maxWidth, 39 | constraints.maxWidth, 40 | ), 41 | const SliverBottomInfoBar() 42 | ], 43 | ), 44 | ); 45 | }, 46 | ), 47 | ); 48 | }, 49 | ); 50 | } 51 | 52 | Widget _displayPosts( 53 | BuildContext context, BlogState state, double width, double maxWidth) { 54 | return state.map( 55 | initial: (_) { 56 | return const SliverLoadingIndicator(); 57 | }, 58 | loading: (_) { 59 | return const SliverLoadingIndicator(); 60 | }, 61 | error: (_) { 62 | return const NoBlogPosts(); 63 | }, 64 | loaded: (state) { 65 | return BlogPosts( 66 | blog: state.blog, 67 | width: width, 68 | maxWidth: maxWidth, 69 | limitNumberOfBlogs: 2, 70 | onTagTap: (tag) { 71 | context.bloc().add( 72 | FilterBlogEvent.filterByTag(tag), 73 | ); 74 | context.bloc().add( 75 | const PageEvent.update(PageState.blog), 76 | ); 77 | }, 78 | ); 79 | }, 80 | ); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /lib/presentation/pages/home/widgets/sliver_course_header.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import '../../../../infrastructure/core/urls.dart' as url; 4 | import '../../../common/accent_button.dart'; 5 | import '../../../core/constants.dart'; 6 | import '../../../core/utils/url_handler.dart'; 7 | 8 | class SliverCourseHeader extends StatelessWidget { 9 | const SliverCourseHeader({ 10 | Key key, 11 | }) : super(key: key); 12 | 13 | @override 14 | Widget build(BuildContext context) { 15 | return SliverToBoxAdapter( 16 | child: Center( 17 | child: ConstrainedBox( 18 | constraints: const BoxConstraints(maxWidth: kMaxBodyWidth), 19 | child: Column( 20 | children: [ 21 | Container( 22 | height: 200, 23 | decoration: const BoxDecoration( 24 | image: DecorationImage( 25 | fit: BoxFit.cover, 26 | image: ExactAssetImage('images/course_background.png'), 27 | ), 28 | ), 29 | ), 30 | Container( 31 | padding: const EdgeInsets.all(16), 32 | color: Theme.of(context).colorScheme.surface, 33 | child: Center( 34 | child: Column( 35 | children: [ 36 | Padding( 37 | padding: const EdgeInsets.all(8.0), 38 | child: Text( 39 | 'Flutter Animation and Performance Course', 40 | style: Theme.of(context) 41 | .textTheme 42 | .headline5 43 | .copyWith(fontWeight: FontWeight.bold), 44 | ), 45 | ), 46 | ConstrainedBox( 47 | constraints: const BoxConstraints(maxWidth: 500), 48 | child: const Padding( 49 | padding: EdgeInsets.all(8.0), 50 | child: Text( 51 | 'Sart animating in Flutter with ease and then learn to prevent common performance problems.', 52 | style: TextStyle( 53 | fontSize: 16, 54 | ), 55 | textAlign: TextAlign.center, 56 | ), 57 | ), 58 | ), 59 | AccentButton( 60 | label: 'Start', 61 | onPressed: () { 62 | launchURL(url.courseAnimationPerf); 63 | }, 64 | ) 65 | ], 66 | ), 67 | ), 68 | ) 69 | ], 70 | ), 71 | ), 72 | ), 73 | ); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /lib/presentation/routes/router.dart: -------------------------------------------------------------------------------- 1 | import 'package:auto_route/auto_route_annotations.dart'; 2 | 3 | import '../app/app.dart'; 4 | import '../sign_in/sign_in_page.dart'; 5 | 6 | @MaterialAutoRouter(generateNavigationHelperExtension: true) 7 | class $Router { 8 | @initial 9 | App app; 10 | // HomePage homePage; 11 | SignInPage signInPage; 12 | } 13 | -------------------------------------------------------------------------------- /lib/presentation/routes/router.gr.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | // ************************************************************************** 4 | // AutoRouteGenerator 5 | // ************************************************************************** 6 | 7 | import 'package:flutter/material.dart'; 8 | import 'package:flutter/cupertino.dart'; 9 | import 'package:auto_route/auto_route.dart'; 10 | import 'package:fun_with_flutter/presentation/app/app.dart'; 11 | import 'package:fun_with_flutter/presentation/sign_in/sign_in_page.dart'; 12 | 13 | abstract class Routes { 14 | static const app = '/'; 15 | static const signInPage = '/sign-in-page'; 16 | static const all = { 17 | app, 18 | signInPage, 19 | }; 20 | } 21 | 22 | class Router extends RouterBase { 23 | @override 24 | Set get allRoutes => Routes.all; 25 | 26 | @Deprecated('call ExtendedNavigator.ofRouter() directly') 27 | static ExtendedNavigatorState get navigator => 28 | ExtendedNavigator.ofRouter(); 29 | 30 | @override 31 | Route onGenerateRoute(RouteSettings settings) { 32 | final args = settings.arguments; 33 | switch (settings.name) { 34 | case Routes.app: 35 | if (hasInvalidArgs(args)) { 36 | return misTypedArgsRoute(args); 37 | } 38 | final typedArgs = args as AppArguments ?? AppArguments(); 39 | return MaterialPageRoute( 40 | builder: (context) => App(key: typedArgs.key), 41 | settings: settings, 42 | ); 43 | case Routes.signInPage: 44 | return MaterialPageRoute( 45 | builder: (context) => SignInPage(), 46 | settings: settings, 47 | ); 48 | default: 49 | return unknownRoutePage(settings.name); 50 | } 51 | } 52 | } 53 | 54 | // ************************************************************************* 55 | // Arguments holder classes 56 | // ************************************************************************** 57 | 58 | //App arguments holder class 59 | class AppArguments { 60 | final Key key; 61 | AppArguments({this.key}); 62 | } 63 | 64 | // ************************************************************************* 65 | // Navigation helper methods extension 66 | // ************************************************************************** 67 | 68 | extension RouterNavigationHelperMethods on ExtendedNavigatorState { 69 | Future pushApp({ 70 | Key key, 71 | }) => 72 | pushNamed( 73 | Routes.app, 74 | arguments: AppArguments(key: key), 75 | ); 76 | 77 | Future pushSignInPage() => pushNamed(Routes.signInPage); 78 | } 79 | -------------------------------------------------------------------------------- /lib/presentation/sign_in/sign_in_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_bloc/flutter_bloc.dart'; 3 | 4 | import '../../application/auth/sign_in_form/sign_in_form_bloc.dart'; 5 | import '../../injection.dart'; 6 | import 'widgets/sign_in_form.dart'; 7 | 8 | class SignInPage extends StatelessWidget { 9 | const SignInPage(); 10 | 11 | @override 12 | Widget build(BuildContext context) { 13 | return Scaffold( 14 | appBar: AppBar( 15 | title: Text( 16 | 'Sign In / Register', 17 | style: TextStyle(color: Theme.of(context).primaryColor), 18 | ), 19 | backgroundColor: Colors.transparent, 20 | elevation: 0, 21 | ), 22 | body: BlocProvider( 23 | create: (context) => getIt(), 24 | child: const SignInForm(), 25 | ), 26 | ); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /lib/presentation/sign_in/widgets/sign_in_form.dart: -------------------------------------------------------------------------------- 1 | import 'package:auto_route/auto_route.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter_bloc/flutter_bloc.dart'; 4 | 5 | import '../../../application/auth/sign_in_form/sign_in_form_bloc.dart'; 6 | import '../../core/constants.dart'; 7 | import '../../core/extensions.dart'; 8 | import '../../core/notification_helper.dart'; 9 | 10 | class SignInForm extends StatelessWidget { 11 | const SignInForm(); 12 | 13 | @override 14 | Widget build(BuildContext context) { 15 | return BlocConsumer( 16 | listener: (context, state) { 17 | state.authFailureOrSuccessOption.fold( 18 | () {}, 19 | (either) => either.fold( 20 | (failure) { 21 | NotificationHelper.error( 22 | message: failure.map( 23 | cancelledByUser: (_) => 'Sign in cancelled', 24 | serverError: (_) => 'Server error', 25 | emailAlreadyInUse: (_) => 'Email already in use', 26 | invalidEmailAndPasswordCombination: (_) => 27 | 'Invalid email and password combination', 28 | userDisabled: (_) => 29 | 'User disabled. Please contact the site administrator for assistance', 30 | ), 31 | isPhone: isPhoneSize(context.mediaSize), 32 | ).show(context); 33 | }, 34 | (_) { 35 | ExtendedNavigator.of(context).pop(); 36 | }, 37 | ), 38 | ); 39 | }, 40 | builder: (context, state) { 41 | return Padding( 42 | padding: const EdgeInsets.all(8.0), 43 | child: Form( 44 | autovalidate: state.showErrorMessages, 45 | child: Column( 46 | mainAxisAlignment: MainAxisAlignment.center, 47 | children: [ 48 | TextFormField( 49 | decoration: const InputDecoration( 50 | prefixIcon: Icon(Icons.email), 51 | labelText: 'Email', 52 | ), 53 | autocorrect: false, 54 | onChanged: (value) => context 55 | .bloc() 56 | .add(SignInFormEvent.emailChanged(value)), 57 | validator: (_) => context 58 | .bloc() 59 | .state 60 | .emailAddress 61 | .value 62 | .fold( 63 | (f) => f.maybeMap( 64 | invalidEmail: (_) => 'Invalid Email', 65 | orElse: () => null, 66 | ), 67 | (_) => null, 68 | ), 69 | ), 70 | const SizedBox(height: 8), 71 | TextFormField( 72 | decoration: const InputDecoration( 73 | prefixIcon: Icon(Icons.lock), 74 | labelText: 'Password', 75 | ), 76 | autocorrect: false, 77 | obscureText: true, 78 | onChanged: (value) => context 79 | .bloc() 80 | .add(SignInFormEvent.passwordChanged(value)), 81 | validator: (_) => 82 | context.bloc().state.password.value.fold( 83 | (f) => f.maybeMap( 84 | shortPassword: (_) => 'Short Password', 85 | orElse: () => null, 86 | ), 87 | (_) => null, 88 | ), 89 | ), 90 | const SizedBox(height: 8), 91 | Row( 92 | children: [ 93 | Expanded( 94 | child: FlatButton( 95 | onPressed: () { 96 | context.bloc().add( 97 | const SignInFormEvent 98 | .signInWithEmailAndPasswordPressed(), 99 | ); 100 | }, 101 | child: const Text('SIGN IN'), 102 | ), 103 | ), 104 | Expanded( 105 | child: FlatButton( 106 | onPressed: () { 107 | context.bloc().add( 108 | const SignInFormEvent 109 | .registerWithEmailAndPasswordPressed(), 110 | ); 111 | }, 112 | child: const Text('REGISTER'), 113 | ), 114 | ), 115 | ], 116 | ), 117 | RaisedButton( 118 | onPressed: () { 119 | context 120 | .bloc() 121 | .add(const SignInFormEvent.signInWithGooglePressed()); 122 | }, 123 | color: Theme.of(context).primaryColor, 124 | child: const Text( 125 | 'SIGN IN WITH GOOGLE', 126 | style: TextStyle( 127 | color: Colors.white, 128 | fontWeight: FontWeight.bold, 129 | ), 130 | ), 131 | ), 132 | if (state.isSubmitting) ...[ 133 | const SizedBox(height: 8), 134 | const LinearProgressIndicator(), 135 | ] 136 | ], 137 | ), 138 | ), 139 | ); 140 | }, 141 | ); 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: fun_with_flutter 2 | description: A new Flutter project. 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.0.0+1 15 | 16 | environment: 17 | sdk: '>=2.8.0 <3.0.0' 18 | 19 | dependencies: 20 | flutter: 21 | sdk: flutter 22 | # State management 23 | flutter_bloc: ^4.0.1 24 | bloc: ^4.0.0 25 | freezed_annotation: ^0.11.0 26 | uuid: ^2.1.0 27 | # UI and Nav 28 | universal_html: ^1.2.3 29 | confetti: ^0.5.2 30 | # Network 31 | http: ^0.12.1 32 | firebase_auth: ^0.16.1 33 | after_layout: ^1.0.7+2 34 | google_sign_in: ^4.5.1 35 | dartz: ^0.9.1 36 | kt_dart: ^0.7.0+1 37 | get_it: ^4.0.2 38 | injectable: ^0.4.0+1 39 | auto_route: ^0.5.0 40 | flushbar: ^1.10.4 41 | animations: ^1.1.1 42 | google_fonts: ^1.1.0 43 | url_launcher: ^5.4.11 44 | dio: ^3.0.9 45 | 46 | # The following adds the Cupertino Icons font to your application. 47 | # Use with the CupertinoIcons class for iOS style icons. 48 | cupertino_icons: ^0.1.3 49 | 50 | dev_dependencies: 51 | flutter_test: 52 | sdk: flutter 53 | lint: ^1.2.0 54 | build_runner: ^1.10.0 55 | freezed: ^0.11.2 56 | json_serializable: ^3.3.0 57 | injectable_generator: ^0.4.1 58 | auto_route_generator: ^0.5.0 59 | 60 | 61 | 62 | # For information on the generic Dart part of this file, see the 63 | # following page: https://dart.dev/tools/pub/pubspec 64 | 65 | # The following section is specific to Flutter. 66 | flutter: 67 | 68 | # The following line ensures that the Material Icons font is 69 | # included with your application, so that you can use the icons in 70 | # the material Icons class. 71 | uses-material-design: true 72 | 73 | # To add assets to your application, add an assets section, like this: 74 | assets: 75 | - assets/images/funwith_favicon.png 76 | - assets/images/course_background.png 77 | # - images/a_dot_ham.jpeg 78 | 79 | # An image asset can refer to one or more resolution-specific "variants", see 80 | # https://flutter.dev/assets-and-images/#resolution-aware. 81 | 82 | # For details regarding adding assets from package dependencies, see 83 | # https://flutter.dev/assets-and-images/#from-packages 84 | 85 | # To add custom fonts to your application, add a fonts section here, 86 | # in this "flutter" section. Each entry in this list should have a 87 | # "family" key with the font family name, and a "fonts" key with a 88 | # list giving the asset and other descriptors for the font. For 89 | # example: 90 | fonts: 91 | - family: CustomIcons 92 | fonts: 93 | - asset: assets/icons/CustomIcons.ttf 94 | # - family: Trajan Pro 95 | # fonts: 96 | # - asset: fonts/TrajanPro.ttf 97 | # - asset: fonts/TrajanPro_Bold.ttf 98 | # weight: 700 99 | # 100 | # For details regarding fonts from package dependencies, 101 | # see https://flutter.dev/custom-fonts/#from-packages 102 | -------------------------------------------------------------------------------- /web/assets/CustomIcons.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/funwithflutter/fun-with-flutter-website/b163e23345901b1937c29da1d7bc18ad7283b879/web/assets/CustomIcons.ttf -------------------------------------------------------------------------------- /web/assets/DMSerifDisplay-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/funwithflutter/fun-with-flutter-website/b163e23345901b1937c29da1d7bc18ad7283b879/web/assets/DMSerifDisplay-Regular.ttf -------------------------------------------------------------------------------- /web/assets/FontManifest.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "family": "MaterialIcons", 4 | "fonts": [ 5 | { 6 | "asset": "MaterialIcons-Regular.ttf" 7 | } 8 | ] 9 | }, 10 | { 11 | "family": "CustomIcons", 12 | "fonts": [ 13 | { 14 | "asset": "CustomIcons.ttf" 15 | } 16 | ] 17 | }, 18 | { 19 | "family": "WorkSans", 20 | "fonts": [ 21 | { 22 | "asset": "WorkSans-Regular.ttf" 23 | } 24 | ] 25 | }, 26 | { 27 | "family": "OpenSans", 28 | "fonts": [ 29 | { 30 | "asset": "OpenSans-Regular.ttf" 31 | } 32 | ] 33 | }, 34 | { 35 | "family": "DMSerifDisplay", 36 | "fonts": [ 37 | { 38 | "asset": "DMSerifDisplay-Regular.ttf" 39 | } 40 | ] 41 | } 42 | ] -------------------------------------------------------------------------------- /web/assets/MaterialIcons-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/funwithflutter/fun-with-flutter-website/b163e23345901b1937c29da1d7bc18ad7283b879/web/assets/MaterialIcons-Regular.ttf -------------------------------------------------------------------------------- /web/assets/OpenSans-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/funwithflutter/fun-with-flutter-website/b163e23345901b1937c29da1d7bc18ad7283b879/web/assets/OpenSans-Regular.ttf -------------------------------------------------------------------------------- /web/assets/WorkSans-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/funwithflutter/fun-with-flutter-website/b163e23345901b1937c29da1d7bc18ad7283b879/web/assets/WorkSans-Regular.ttf -------------------------------------------------------------------------------- /web/assets/images/course_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/funwithflutter/fun-with-flutter-website/b163e23345901b1937c29da1d7bc18ad7283b879/web/assets/images/course_background.png -------------------------------------------------------------------------------- /web/assets/images/funwith_favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/funwithflutter/fun-with-flutter-website/b163e23345901b1937c29da1d7bc18ad7283b879/web/assets/images/funwith_favicon.png -------------------------------------------------------------------------------- /web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 51 | 52 | 53 | 73 | 74 | 75 | 76 | 77 | --------------------------------------------------------------------------------