├── .fvm ├── flutter_sdk └── fvm_config.json ├── .gitignore ├── .metadata ├── LICENSE ├── README.md ├── android ├── .gitignore ├── app │ ├── build.gradle │ └── src │ │ ├── debug │ │ └── AndroidManifest.xml │ │ ├── main │ │ ├── AndroidManifest.xml │ │ ├── kotlin │ │ │ └── com │ │ │ │ └── ouahiddev │ │ │ │ └── Blog │ │ │ │ └── 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 └── images │ ├── identity.svg │ ├── shield.svg │ ├── space-man.svg │ └── woman-laugh.svg ├── ios ├── .gitignore ├── Flutter │ ├── AppFrameworkInfo.plist │ ├── Debug.xcconfig │ └── Release.xcconfig ├── Runner.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ ├── IDEWorkspaceChecks.plist │ │ │ └── WorkspaceSettings.xcsettings │ └── xcshareddata │ │ └── xcschemes │ │ └── Runner.xcscheme ├── Runner.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ └── WorkspaceSettings.xcsettings └── 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 ├── main.dart └── src │ ├── application │ ├── article_create │ │ ├── bloc.dart │ │ ├── bloc.freezed.dart │ │ └── state.dart │ ├── article_details │ │ ├── bloc.dart │ │ ├── bloc.freezed.dart │ │ └── state.dart │ ├── articles │ │ ├── bloc.dart │ │ ├── bloc.freezed.dart │ │ └── state.dart │ ├── favorites │ │ ├── bloc.dart │ │ ├── bloc.freezed.dart │ │ └── state.dart │ ├── profile │ │ ├── bloc.dart │ │ ├── bloc.freezed.dart │ │ └── state.dart │ ├── search │ │ ├── bloc.dart │ │ ├── bloc.freezed.dart │ │ └── state.dart │ ├── sign_in │ │ ├── bloc.dart │ │ ├── bloc.freezed.dart │ │ └── state.dart │ └── sign_up │ │ ├── bloc.dart │ │ ├── bloc.freezed.dart │ │ └── state.dart │ ├── domain │ ├── article_create │ │ └── i_article_create_facade.dart │ ├── article_details │ │ └── i_article_details_facade.dart │ ├── articles │ │ └── i_articles_facade.dart │ ├── entities │ │ ├── api_error │ │ │ ├── api_error.dart │ │ │ └── api_error.g.dart │ │ ├── api_response │ │ │ ├── api_response.dart │ │ │ └── api_response.g.dart │ │ ├── article │ │ │ ├── article.dart │ │ │ └── article.g.dart │ │ ├── category │ │ │ ├── category.dart │ │ │ └── category.g.dart │ │ ├── favorite │ │ │ ├── favorite.dart │ │ │ └── favorite.g.dart │ │ └── user │ │ │ ├── user.dart │ │ │ └── user.g.dart │ ├── favorites │ │ └── i_favorites_facade.dart │ ├── profile │ │ └── i_profile_facade.dart │ ├── search │ │ └── i_search_facade.dart │ ├── sign_in │ │ └── i_sign_in_facade.dart │ └── sign_up │ │ └── i_sign_up_facade.dart │ ├── infrastructure │ ├── article_create │ │ └── article_create_facade.dart │ ├── article_details │ │ └── article_details_facade.dart │ ├── articles │ │ └── articles_facade.dart │ ├── core │ │ ├── network │ │ │ ├── interceptor.dart │ │ │ ├── network.dart │ │ │ └── network.g.dart │ │ └── preferences.dart │ ├── favorites │ │ └── favorites_facade.dart │ ├── profile │ │ └── profile_facade.dart │ ├── search │ │ └── search_facade.dart │ ├── sign_in │ │ └── sign_in_facade.dart │ └── sign_up │ │ └── sign_up_facade.dart │ ├── injection.config.dart │ ├── injection.dart │ └── presentation │ ├── routes │ ├── routes.dart │ └── routes_generator.dart │ ├── ui │ ├── components │ │ ├── article_item.dart │ │ ├── buttons │ │ │ ├── clear_button.dart │ │ │ ├── rounded_button.dart │ │ │ └── rounded_outline_button.dart │ │ ├── dialogs │ │ │ ├── question.dart │ │ │ └── waiting.dart │ │ ├── loading.dart │ │ ├── login.dart │ │ ├── my_article_item.dart │ │ └── text_fields │ │ │ ├── rounded_outline_text_field.dart │ │ │ └── search_field.dart │ └── pages │ │ ├── article_create │ │ ├── article_create.dart │ │ └── widgets │ │ │ ├── categories.dart │ │ │ ├── image.dart │ │ │ ├── images.dart │ │ │ └── widgets.dart │ │ ├── article_details │ │ └── article_details.dart │ │ ├── articles │ │ ├── articles.dart │ │ └── widgets │ │ │ ├── articles.dart │ │ │ ├── categories.dart │ │ │ └── widgets.dart │ │ ├── favorites │ │ └── favorites.dart │ │ ├── home │ │ ├── home.dart │ │ └── widgets │ │ │ ├── bottom_tab_bar.dart │ │ │ └── widgets.dart │ │ ├── on_boarding │ │ ├── on_boarding.dart │ │ └── widgets │ │ │ ├── dote.dart │ │ │ ├── indicator.dart │ │ │ ├── page.dart │ │ │ └── widgets.dart │ │ ├── profile │ │ ├── profile.dart │ │ └── widgets │ │ │ ├── header.dart │ │ │ └── widgets.dart │ │ ├── search │ │ └── search.dart │ │ ├── sign_in │ │ └── sign_in.dart │ │ └── sign_up │ │ └── sign_up.dart │ └── utils │ ├── extensions.dart │ └── validators.dart ├── pubspec.lock ├── pubspec.yaml ├── screenshot ├── flutter_01.png ├── flutter_02.png ├── flutter_03.png ├── flutter_04.png ├── flutter_05.png ├── flutter_06.png ├── flutter_07.png └── flutter_08.png ├── test ├── extensions_test.dart ├── test.dart └── widget_test.dart └── web ├── favicon.png ├── icons ├── Icon-192.png └── Icon-512.png ├── index.html └── manifest.json /.fvm/flutter_sdk: -------------------------------------------------------------------------------- 1 | C:/Users/Abdelouahed/fvm/versions/stable -------------------------------------------------------------------------------- /.fvm/fvm_config.json: -------------------------------------------------------------------------------- 1 | { 2 | "flutterSdkVersion": "stable", 3 | "flavors": {} 4 | } -------------------------------------------------------------------------------- /.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 | **/ios/Flutter/.last_build_id 26 | .dart_tool/ 27 | .flutter-plugins 28 | .flutter-plugins-dependencies 29 | .packages 30 | .pub-cache/ 31 | .pub/ 32 | /build/ 33 | 34 | # Web related 35 | lib/generated_plugin_registrant.dart 36 | 37 | # Symbolication related 38 | app.*.symbols 39 | 40 | # Obfuscation related 41 | app.*.map.json 42 | -------------------------------------------------------------------------------- /.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: d408d302e22179d598f467e11da5dd968dbdc9ec 8 | channel: beta 9 | 10 | project_type: app 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Flutter Blog 2 | a flutter project based on Backend built by Dart + Aqueduct + MongoDB 3 | ### [Backend code](https://github.com/GeekAbdelouahed/Aqueduct-MongoDB) 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | #### Developer [Abdelouahed Medjoudja](https://www.facebook.com/AbdelouahedMedjoudja) 16 | -------------------------------------------------------------------------------- /android/.gitignore: -------------------------------------------------------------------------------- 1 | gradle-wrapper.jar 2 | /.gradle 3 | /captures/ 4 | /gradlew 5 | /gradlew.bat 6 | /local.properties 7 | GeneratedPluginRegistrant.java 8 | 9 | # Remember to never publicly share your keystore. 10 | # See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app 11 | key.properties 12 | -------------------------------------------------------------------------------- /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 29 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.ouahiddev.Blog" 42 | minSdkVersion 16 43 | targetSdkVersion 29 44 | versionCode flutterVersionCode.toInteger() 45 | versionName flutterVersionName 46 | } 47 | 48 | buildTypes { 49 | release { 50 | // TODO: Add your own signing config for the release build. 51 | // Signing with the debug keys for now, so `flutter run --release` works. 52 | signingConfig signingConfigs.debug 53 | } 54 | } 55 | } 56 | 57 | flutter { 58 | source '../..' 59 | } 60 | 61 | dependencies { 62 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 63 | } 64 | -------------------------------------------------------------------------------- /android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 8 | 9 | 10 | 15 | 22 | 26 | 30 | 35 | 39 | 40 | 41 | 42 | 43 | 44 | 46 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /android/app/src/main/kotlin/com/ouahiddev/Blog/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.ouahiddev.Blog 2 | 3 | import io.flutter.embedding.android.FlutterActivity 4 | 5 | class MainActivity: FlutterActivity() { 6 | } 7 | -------------------------------------------------------------------------------- /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/GeekAbdelouahed/Flutter-Blog/1f1d9ea881992dc867fa79eed40c6496c405c2fa/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GeekAbdelouahed/Flutter-Blog/1f1d9ea881992dc867fa79eed40c6496c405c2fa/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GeekAbdelouahed/Flutter-Blog/1f1d9ea881992dc867fa79eed40c6496c405c2fa/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GeekAbdelouahed/Flutter-Blog/1f1d9ea881992dc867fa79eed40c6496c405c2fa/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GeekAbdelouahed/Flutter-Blog/1f1d9ea881992dc867fa79eed40c6496c405c2fa/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /android/app/src/profile/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext.kotlin_version = '1.3.50' 3 | repositories { 4 | google() 5 | jcenter() 6 | } 7 | 8 | dependencies { 9 | classpath 'com.android.tools.build:gradle:3.5.0' 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 | android.useAndroidX=true 3 | android.enableJetifier=true 4 | android.enableR8=true 5 | -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Fri Jun 23 08:50:38 CEST 2017 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.2-all.zip 7 | -------------------------------------------------------------------------------- /android/settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | 3 | def localPropertiesFile = new File(rootProject.projectDir, "local.properties") 4 | def properties = new Properties() 5 | 6 | assert localPropertiesFile.exists() 7 | localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) } 8 | 9 | def flutterSdkPath = properties.getProperty("flutter.sdk") 10 | assert flutterSdkPath != null, "flutter.sdk not set in local.properties" 11 | apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle" 12 | -------------------------------------------------------------------------------- /ios/.gitignore: -------------------------------------------------------------------------------- 1 | *.mode1v3 2 | *.mode2v3 3 | *.moved-aside 4 | *.pbxuser 5 | *.perspectivev3 6 | **/*sync/ 7 | .sconsign.dblite 8 | .tags* 9 | **/.vagrant/ 10 | **/DerivedData/ 11 | Icon? 12 | **/Pods/ 13 | **/.symlinks/ 14 | profile 15 | xcuserdata 16 | **/.generated/ 17 | Flutter/App.framework 18 | Flutter/Flutter.framework 19 | Flutter/Flutter.podspec 20 | Flutter/Generated.xcconfig 21 | Flutter/app.flx 22 | Flutter/app.zip 23 | Flutter/flutter_assets/ 24 | Flutter/flutter_export_environment.sh 25 | ServiceDefinitions.json 26 | Runner/GeneratedPluginRegistrant.* 27 | 28 | # Exceptions to above rules. 29 | !default.mode1v3 30 | !default.mode2v3 31 | !default.pbxuser 32 | !default.perspectivev3 33 | -------------------------------------------------------------------------------- /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 | 9.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/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /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.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /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/GeekAbdelouahed/Flutter-Blog/1f1d9ea881992dc867fa79eed40c6496c405c2fa/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/GeekAbdelouahed/Flutter-Blog/1f1d9ea881992dc867fa79eed40c6496c405c2fa/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/GeekAbdelouahed/Flutter-Blog/1f1d9ea881992dc867fa79eed40c6496c405c2fa/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/GeekAbdelouahed/Flutter-Blog/1f1d9ea881992dc867fa79eed40c6496c405c2fa/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/GeekAbdelouahed/Flutter-Blog/1f1d9ea881992dc867fa79eed40c6496c405c2fa/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/GeekAbdelouahed/Flutter-Blog/1f1d9ea881992dc867fa79eed40c6496c405c2fa/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/GeekAbdelouahed/Flutter-Blog/1f1d9ea881992dc867fa79eed40c6496c405c2fa/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/GeekAbdelouahed/Flutter-Blog/1f1d9ea881992dc867fa79eed40c6496c405c2fa/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/GeekAbdelouahed/Flutter-Blog/1f1d9ea881992dc867fa79eed40c6496c405c2fa/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/GeekAbdelouahed/Flutter-Blog/1f1d9ea881992dc867fa79eed40c6496c405c2fa/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/GeekAbdelouahed/Flutter-Blog/1f1d9ea881992dc867fa79eed40c6496c405c2fa/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/GeekAbdelouahed/Flutter-Blog/1f1d9ea881992dc867fa79eed40c6496c405c2fa/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/GeekAbdelouahed/Flutter-Blog/1f1d9ea881992dc867fa79eed40c6496c405c2fa/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/GeekAbdelouahed/Flutter-Blog/1f1d9ea881992dc867fa79eed40c6496c405c2fa/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/GeekAbdelouahed/Flutter-Blog/1f1d9ea881992dc867fa79eed40c6496c405c2fa/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/GeekAbdelouahed/Flutter-Blog/1f1d9ea881992dc867fa79eed40c6496c405c2fa/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GeekAbdelouahed/Flutter-Blog/1f1d9ea881992dc867fa79eed40c6496c405c2fa/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GeekAbdelouahed/Flutter-Blog/1f1d9ea881992dc867fa79eed40c6496c405c2fa/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 | NSPhotoLibraryUsageDescription 6 | Allow Select images for Article 7 | NSCameraUsageDescription 8 | Allow Select images for Article 9 | CFBundleDevelopmentRegion 10 | $(DEVELOPMENT_LANGUAGE) 11 | CFBundleExecutable 12 | $(EXECUTABLE_NAME) 13 | CFBundleIdentifier 14 | $(PRODUCT_BUNDLE_IDENTIFIER) 15 | CFBundleInfoDictionaryVersion 16 | 6.0 17 | CFBundleName 18 | Blog 19 | CFBundlePackageType 20 | APPL 21 | CFBundleShortVersionString 22 | $(FLUTTER_BUILD_NAME) 23 | CFBundleSignature 24 | ???? 25 | CFBundleVersion 26 | $(FLUTTER_BUILD_NUMBER) 27 | LSRequiresIPhoneOS 28 | 29 | UILaunchStoryboardName 30 | LaunchScreen 31 | UIMainStoryboardFile 32 | Main 33 | UISupportedInterfaceOrientations 34 | 35 | UIInterfaceOrientationPortrait 36 | UIInterfaceOrientationLandscapeLeft 37 | UIInterfaceOrientationLandscapeRight 38 | 39 | UISupportedInterfaceOrientations~ipad 40 | 41 | UIInterfaceOrientationPortrait 42 | UIInterfaceOrientationPortraitUpsideDown 43 | UIInterfaceOrientationLandscapeLeft 44 | UIInterfaceOrientationLandscapeRight 45 | 46 | UIViewControllerBasedStatusBarAppearance 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /ios/Runner/Runner-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | #import "GeneratedPluginRegistrant.h" 2 | -------------------------------------------------------------------------------- /lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter/services.dart'; 3 | import 'package:get_it/get_it.dart'; 4 | import 'package:injectable/injectable.dart'; 5 | 6 | import 'src/infrastructure/core/preferences.dart'; 7 | import 'src/injection.dart'; 8 | import 'src/presentation/routes/routes.dart'; 9 | import 'src/presentation/routes/routes_generator.dart'; 10 | 11 | void main() async { 12 | WidgetsFlutterBinding.ensureInitialized(); 13 | 14 | configureInjection(Environment.prod); 15 | 16 | await GetIt.I.isReady(); 17 | 18 | SystemChrome.setSystemUIOverlayStyle( 19 | SystemUiOverlayStyle(statusBarColor: Colors.indigo), 20 | ); 21 | 22 | runApp(MyApp()); 23 | } 24 | 25 | class MyApp extends StatelessWidget { 26 | @override 27 | Widget build(BuildContext context) { 28 | return MaterialApp( 29 | debugShowCheckedModeBanner: false, 30 | title: 'Flutter Blog', 31 | theme: ThemeData( 32 | primarySwatch: Colors.indigo, 33 | visualDensity: VisualDensity.adaptivePlatformDensity, 34 | ), 35 | onGenerateRoute: AppRoutesGenerator.generateRoute, 36 | initialRoute: getIt().isLoggedIn 37 | ? AppRoutes.home 38 | : AppRoutes.onBoarding, 39 | ); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /lib/src/application/article_create/bloc.dart: -------------------------------------------------------------------------------- 1 | import 'package:dartz/dartz.dart'; 2 | import 'package:dio/dio.dart'; 3 | import 'package:flutter_bloc/flutter_bloc.dart'; 4 | import 'package:freezed_annotation/freezed_annotation.dart'; 5 | import 'package:injectable/injectable.dart'; 6 | 7 | import '../../domain/article_create/i_article_create_facade.dart'; 8 | import '../../domain/entities/api_error/api_error.dart'; 9 | import '../../domain/entities/api_response/api_response.dart'; 10 | 11 | part 'bloc.freezed.dart'; 12 | part 'state.dart'; 13 | 14 | @injectable 15 | class ArticleCreateBloc extends Cubit { 16 | final IArticleCreateFacade _articleFacade; 17 | 18 | ArticleCreateBloc(this._articleFacade) : super(ArticleCreateState.initial()); 19 | 20 | void createArticle(FormData data) async { 21 | emit(state.copyWith(articleCreateState: none())); 22 | final result = await _articleFacade.createArticle(data); 23 | emit(state.copyWith(articleCreateState: optionOf(result))); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /lib/src/application/article_create/bloc.freezed.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | // ignore_for_file: deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies 3 | 4 | part of 'bloc.dart'; 5 | 6 | // ************************************************************************** 7 | // FreezedGenerator 8 | // ************************************************************************** 9 | 10 | T _$identity(T value) => value; 11 | 12 | /// @nodoc 13 | class _$ArticleCreateStateTearOff { 14 | const _$ArticleCreateStateTearOff(); 15 | 16 | // ignore: unused_element 17 | _ArticleCreateState call( 18 | {@required Option> articleCreateState}) { 19 | return _ArticleCreateState( 20 | articleCreateState: articleCreateState, 21 | ); 22 | } 23 | } 24 | 25 | /// @nodoc 26 | // ignore: unused_element 27 | const $ArticleCreateState = _$ArticleCreateStateTearOff(); 28 | 29 | /// @nodoc 30 | mixin _$ArticleCreateState { 31 | Option> get articleCreateState; 32 | 33 | @JsonKey(ignore: true) 34 | $ArticleCreateStateCopyWith get copyWith; 35 | } 36 | 37 | /// @nodoc 38 | abstract class $ArticleCreateStateCopyWith<$Res> { 39 | factory $ArticleCreateStateCopyWith( 40 | ArticleCreateState value, $Res Function(ArticleCreateState) then) = 41 | _$ArticleCreateStateCopyWithImpl<$Res>; 42 | $Res call({Option> articleCreateState}); 43 | } 44 | 45 | /// @nodoc 46 | class _$ArticleCreateStateCopyWithImpl<$Res> 47 | implements $ArticleCreateStateCopyWith<$Res> { 48 | _$ArticleCreateStateCopyWithImpl(this._value, this._then); 49 | 50 | final ArticleCreateState _value; 51 | // ignore: unused_field 52 | final $Res Function(ArticleCreateState) _then; 53 | 54 | @override 55 | $Res call({ 56 | Object articleCreateState = freezed, 57 | }) { 58 | return _then(_value.copyWith( 59 | articleCreateState: articleCreateState == freezed 60 | ? _value.articleCreateState 61 | : articleCreateState as Option>, 62 | )); 63 | } 64 | } 65 | 66 | /// @nodoc 67 | abstract class _$ArticleCreateStateCopyWith<$Res> 68 | implements $ArticleCreateStateCopyWith<$Res> { 69 | factory _$ArticleCreateStateCopyWith( 70 | _ArticleCreateState value, $Res Function(_ArticleCreateState) then) = 71 | __$ArticleCreateStateCopyWithImpl<$Res>; 72 | @override 73 | $Res call({Option> articleCreateState}); 74 | } 75 | 76 | /// @nodoc 77 | class __$ArticleCreateStateCopyWithImpl<$Res> 78 | extends _$ArticleCreateStateCopyWithImpl<$Res> 79 | implements _$ArticleCreateStateCopyWith<$Res> { 80 | __$ArticleCreateStateCopyWithImpl( 81 | _ArticleCreateState _value, $Res Function(_ArticleCreateState) _then) 82 | : super(_value, (v) => _then(v as _ArticleCreateState)); 83 | 84 | @override 85 | _ArticleCreateState get _value => super._value as _ArticleCreateState; 86 | 87 | @override 88 | $Res call({ 89 | Object articleCreateState = freezed, 90 | }) { 91 | return _then(_ArticleCreateState( 92 | articleCreateState: articleCreateState == freezed 93 | ? _value.articleCreateState 94 | : articleCreateState as Option>, 95 | )); 96 | } 97 | } 98 | 99 | /// @nodoc 100 | class _$_ArticleCreateState implements _ArticleCreateState { 101 | const _$_ArticleCreateState({@required this.articleCreateState}) 102 | : assert(articleCreateState != null); 103 | 104 | @override 105 | final Option> articleCreateState; 106 | 107 | @override 108 | String toString() { 109 | return 'ArticleCreateState(articleCreateState: $articleCreateState)'; 110 | } 111 | 112 | @override 113 | bool operator ==(dynamic other) { 114 | return identical(this, other) || 115 | (other is _ArticleCreateState && 116 | (identical(other.articleCreateState, articleCreateState) || 117 | const DeepCollectionEquality() 118 | .equals(other.articleCreateState, articleCreateState))); 119 | } 120 | 121 | @override 122 | int get hashCode => 123 | runtimeType.hashCode ^ 124 | const DeepCollectionEquality().hash(articleCreateState); 125 | 126 | @JsonKey(ignore: true) 127 | @override 128 | _$ArticleCreateStateCopyWith<_ArticleCreateState> get copyWith => 129 | __$ArticleCreateStateCopyWithImpl<_ArticleCreateState>(this, _$identity); 130 | } 131 | 132 | abstract class _ArticleCreateState implements ArticleCreateState { 133 | const factory _ArticleCreateState( 134 | {@required 135 | Option> articleCreateState}) = 136 | _$_ArticleCreateState; 137 | 138 | @override 139 | Option> get articleCreateState; 140 | @override 141 | @JsonKey(ignore: true) 142 | _$ArticleCreateStateCopyWith<_ArticleCreateState> get copyWith; 143 | } 144 | -------------------------------------------------------------------------------- /lib/src/application/article_create/state.dart: -------------------------------------------------------------------------------- 1 | part of 'bloc.dart'; 2 | 3 | @freezed 4 | abstract class ArticleCreateState with _$ArticleCreateState { 5 | const factory ArticleCreateState({ 6 | @required Option> articleCreateState, 7 | }) = _ArticleCreateState; 8 | 9 | factory ArticleCreateState.initial() => ArticleCreateState( 10 | articleCreateState: none(), 11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /lib/src/application/article_details/bloc.dart: -------------------------------------------------------------------------------- 1 | import 'package:dartz/dartz.dart'; 2 | import 'package:flutter_bloc/flutter_bloc.dart'; 3 | import 'package:freezed_annotation/freezed_annotation.dart'; 4 | import 'package:injectable/injectable.dart'; 5 | 6 | import '../../domain/article_details/i_article_details_facade.dart'; 7 | import '../../domain/entities/api_error/api_error.dart'; 8 | import '../../domain/entities/api_response/api_response.dart'; 9 | import '../../domain/entities/article/article.dart'; 10 | 11 | part 'bloc.freezed.dart'; 12 | 13 | part 'state.dart'; 14 | 15 | @injectable 16 | class ArticleDetailsBloc extends Cubit { 17 | final IArticleDetailsFacade _articleFacade; 18 | 19 | ArticleDetailsBloc(this._articleFacade) 20 | : super(ArticleDetailsState.initial()); 21 | 22 | void getArticleDetails(String id, {String userId}) async { 23 | emit(state.copyWith(articleDetailsState: none())); 24 | final result = await _articleFacade.getArticleDetails(id, userId: userId); 25 | emit(state.copyWith(articleDetailsState: optionOf(result))); 26 | } 27 | 28 | void addFavorite(String articleId, String userId) async { 29 | await _articleFacade.addFavorite(articleId, userId); 30 | } 31 | 32 | void removeFavorite(String articleId, String userId) async { 33 | await _articleFacade.removeFavorite(articleId, userId); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /lib/src/application/article_details/bloc.freezed.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | // ignore_for_file: deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies 3 | 4 | part of 'bloc.dart'; 5 | 6 | // ************************************************************************** 7 | // FreezedGenerator 8 | // ************************************************************************** 9 | 10 | T _$identity(T value) => value; 11 | 12 | /// @nodoc 13 | class _$ArticleDetailsStateTearOff { 14 | const _$ArticleDetailsStateTearOff(); 15 | 16 | // ignore: unused_element 17 | _ArticleDetailsState call( 18 | {@required 19 | Option>> articleDetailsState}) { 20 | return _ArticleDetailsState( 21 | articleDetailsState: articleDetailsState, 22 | ); 23 | } 24 | } 25 | 26 | /// @nodoc 27 | // ignore: unused_element 28 | const $ArticleDetailsState = _$ArticleDetailsStateTearOff(); 29 | 30 | /// @nodoc 31 | mixin _$ArticleDetailsState { 32 | Option>> get articleDetailsState; 33 | 34 | @JsonKey(ignore: true) 35 | $ArticleDetailsStateCopyWith get copyWith; 36 | } 37 | 38 | /// @nodoc 39 | abstract class $ArticleDetailsStateCopyWith<$Res> { 40 | factory $ArticleDetailsStateCopyWith( 41 | ArticleDetailsState value, $Res Function(ArticleDetailsState) then) = 42 | _$ArticleDetailsStateCopyWithImpl<$Res>; 43 | $Res call( 44 | {Option>> articleDetailsState}); 45 | } 46 | 47 | /// @nodoc 48 | class _$ArticleDetailsStateCopyWithImpl<$Res> 49 | implements $ArticleDetailsStateCopyWith<$Res> { 50 | _$ArticleDetailsStateCopyWithImpl(this._value, this._then); 51 | 52 | final ArticleDetailsState _value; 53 | // ignore: unused_field 54 | final $Res Function(ArticleDetailsState) _then; 55 | 56 | @override 57 | $Res call({ 58 | Object articleDetailsState = freezed, 59 | }) { 60 | return _then(_value.copyWith( 61 | articleDetailsState: articleDetailsState == freezed 62 | ? _value.articleDetailsState 63 | : articleDetailsState 64 | as Option>>, 65 | )); 66 | } 67 | } 68 | 69 | /// @nodoc 70 | abstract class _$ArticleDetailsStateCopyWith<$Res> 71 | implements $ArticleDetailsStateCopyWith<$Res> { 72 | factory _$ArticleDetailsStateCopyWith(_ArticleDetailsState value, 73 | $Res Function(_ArticleDetailsState) then) = 74 | __$ArticleDetailsStateCopyWithImpl<$Res>; 75 | @override 76 | $Res call( 77 | {Option>> articleDetailsState}); 78 | } 79 | 80 | /// @nodoc 81 | class __$ArticleDetailsStateCopyWithImpl<$Res> 82 | extends _$ArticleDetailsStateCopyWithImpl<$Res> 83 | implements _$ArticleDetailsStateCopyWith<$Res> { 84 | __$ArticleDetailsStateCopyWithImpl( 85 | _ArticleDetailsState _value, $Res Function(_ArticleDetailsState) _then) 86 | : super(_value, (v) => _then(v as _ArticleDetailsState)); 87 | 88 | @override 89 | _ArticleDetailsState get _value => super._value as _ArticleDetailsState; 90 | 91 | @override 92 | $Res call({ 93 | Object articleDetailsState = freezed, 94 | }) { 95 | return _then(_ArticleDetailsState( 96 | articleDetailsState: articleDetailsState == freezed 97 | ? _value.articleDetailsState 98 | : articleDetailsState 99 | as Option>>, 100 | )); 101 | } 102 | } 103 | 104 | /// @nodoc 105 | class _$_ArticleDetailsState implements _ArticleDetailsState { 106 | const _$_ArticleDetailsState({@required this.articleDetailsState}) 107 | : assert(articleDetailsState != null); 108 | 109 | @override 110 | final Option>> articleDetailsState; 111 | 112 | @override 113 | String toString() { 114 | return 'ArticleDetailsState(articleDetailsState: $articleDetailsState)'; 115 | } 116 | 117 | @override 118 | bool operator ==(dynamic other) { 119 | return identical(this, other) || 120 | (other is _ArticleDetailsState && 121 | (identical(other.articleDetailsState, articleDetailsState) || 122 | const DeepCollectionEquality() 123 | .equals(other.articleDetailsState, articleDetailsState))); 124 | } 125 | 126 | @override 127 | int get hashCode => 128 | runtimeType.hashCode ^ 129 | const DeepCollectionEquality().hash(articleDetailsState); 130 | 131 | @JsonKey(ignore: true) 132 | @override 133 | _$ArticleDetailsStateCopyWith<_ArticleDetailsState> get copyWith => 134 | __$ArticleDetailsStateCopyWithImpl<_ArticleDetailsState>( 135 | this, _$identity); 136 | } 137 | 138 | abstract class _ArticleDetailsState implements ArticleDetailsState { 139 | const factory _ArticleDetailsState( 140 | {@required 141 | Option>> 142 | articleDetailsState}) = _$_ArticleDetailsState; 143 | 144 | @override 145 | Option>> get articleDetailsState; 146 | @override 147 | @JsonKey(ignore: true) 148 | _$ArticleDetailsStateCopyWith<_ArticleDetailsState> get copyWith; 149 | } 150 | -------------------------------------------------------------------------------- /lib/src/application/article_details/state.dart: -------------------------------------------------------------------------------- 1 | part of 'bloc.dart'; 2 | 3 | @freezed 4 | abstract class ArticleDetailsState with _$ArticleDetailsState { 5 | const factory ArticleDetailsState({ 6 | @required 7 | Option>> articleDetailsState, 8 | }) = _ArticleDetailsState; 9 | 10 | factory ArticleDetailsState.initial() => ArticleDetailsState( 11 | articleDetailsState: none(), 12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /lib/src/application/articles/bloc.dart: -------------------------------------------------------------------------------- 1 | import 'package:dartz/dartz.dart'; 2 | import 'package:flutter_bloc/flutter_bloc.dart'; 3 | import 'package:freezed_annotation/freezed_annotation.dart'; 4 | import 'package:injectable/injectable.dart'; 5 | 6 | import '../../domain/articles/i_articles_facade.dart'; 7 | import '../../domain/entities/api_error/api_error.dart'; 8 | import '../../domain/entities/api_response/api_response.dart'; 9 | import '../../domain/entities/article/article.dart'; 10 | import '../../domain/entities/category/category.dart'; 11 | 12 | part 'bloc.freezed.dart'; 13 | 14 | part 'state.dart'; 15 | 16 | @injectable 17 | class ArticlesBloc extends Cubit { 18 | final IArticlesFacade _articlesFacade; 19 | 20 | ArticlesBloc(this._articlesFacade) : super(ArticlesState.initial()); 21 | 22 | void getCategories() async { 23 | emit(state.copyWith(categoriesState: none())); 24 | final result = await _articlesFacade.getCategories(); 25 | emit(state.copyWith(categoriesState: optionOf(result))); 26 | } 27 | 28 | void getArticles() async { 29 | emit(state.copyWith(articlesState: none())); 30 | final result = await _articlesFacade.getArticles(); 31 | emit(state.copyWith(articlesState: optionOf(result))); 32 | } 33 | 34 | void getArticlesByCategory(String categoryId) async { 35 | emit(state.copyWith(articlesState: none())); 36 | final result = await _articlesFacade.getArticlesByCategory(categoryId); 37 | emit(state.copyWith(articlesState: optionOf(result))); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /lib/src/application/articles/state.dart: -------------------------------------------------------------------------------- 1 | part of 'bloc.dart'; 2 | 3 | @freezed 4 | abstract class ArticlesState with _$ArticlesState { 5 | const factory ArticlesState({ 6 | @required 7 | Option>>> categoriesState, 8 | @required 9 | Option>>> articlesState, 10 | }) = _ArticlesState; 11 | 12 | factory ArticlesState.initial() => ArticlesState( 13 | categoriesState: none(), 14 | articlesState: none(), 15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /lib/src/application/favorites/bloc.dart: -------------------------------------------------------------------------------- 1 | import 'package:dartz/dartz.dart'; 2 | import 'package:flutter_bloc/flutter_bloc.dart'; 3 | import 'package:freezed_annotation/freezed_annotation.dart'; 4 | import 'package:injectable/injectable.dart'; 5 | 6 | import '../../domain/entities/api_error/api_error.dart'; 7 | import '../../domain/entities/api_response/api_response.dart'; 8 | import '../../domain/entities/favorite/favorite.dart'; 9 | import '../../domain/favorites/i_favorites_facade.dart'; 10 | 11 | part 'bloc.freezed.dart'; 12 | 13 | part 'state.dart'; 14 | 15 | @injectable 16 | class FavoritesBloc extends Cubit { 17 | final IFavoritesFacade _favoritesFacade; 18 | 19 | FavoritesBloc(this._favoritesFacade) : super(FavoritesState.initial()); 20 | 21 | void getFavorites(String userId) async { 22 | emit(state.copyWith(favoritesState: none())); 23 | final result = await _favoritesFacade.getFavorites(userId); 24 | emit(state.copyWith(favoritesState: optionOf(result))); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /lib/src/application/favorites/bloc.freezed.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | // ignore_for_file: deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies 3 | 4 | part of 'bloc.dart'; 5 | 6 | // ************************************************************************** 7 | // FreezedGenerator 8 | // ************************************************************************** 9 | 10 | T _$identity(T value) => value; 11 | 12 | /// @nodoc 13 | class _$FavoritesStateTearOff { 14 | const _$FavoritesStateTearOff(); 15 | 16 | // ignore: unused_element 17 | _FavoritesState call( 18 | {@required 19 | Option>>> 20 | favoritesState}) { 21 | return _FavoritesState( 22 | favoritesState: favoritesState, 23 | ); 24 | } 25 | } 26 | 27 | /// @nodoc 28 | // ignore: unused_element 29 | const $FavoritesState = _$FavoritesStateTearOff(); 30 | 31 | /// @nodoc 32 | mixin _$FavoritesState { 33 | Option>>> get favoritesState; 34 | 35 | @JsonKey(ignore: true) 36 | $FavoritesStateCopyWith get copyWith; 37 | } 38 | 39 | /// @nodoc 40 | abstract class $FavoritesStateCopyWith<$Res> { 41 | factory $FavoritesStateCopyWith( 42 | FavoritesState value, $Res Function(FavoritesState) then) = 43 | _$FavoritesStateCopyWithImpl<$Res>; 44 | $Res call( 45 | {Option>>> favoritesState}); 46 | } 47 | 48 | /// @nodoc 49 | class _$FavoritesStateCopyWithImpl<$Res> 50 | implements $FavoritesStateCopyWith<$Res> { 51 | _$FavoritesStateCopyWithImpl(this._value, this._then); 52 | 53 | final FavoritesState _value; 54 | // ignore: unused_field 55 | final $Res Function(FavoritesState) _then; 56 | 57 | @override 58 | $Res call({ 59 | Object favoritesState = freezed, 60 | }) { 61 | return _then(_value.copyWith( 62 | favoritesState: favoritesState == freezed 63 | ? _value.favoritesState 64 | : favoritesState 65 | as Option>>>, 66 | )); 67 | } 68 | } 69 | 70 | /// @nodoc 71 | abstract class _$FavoritesStateCopyWith<$Res> 72 | implements $FavoritesStateCopyWith<$Res> { 73 | factory _$FavoritesStateCopyWith( 74 | _FavoritesState value, $Res Function(_FavoritesState) then) = 75 | __$FavoritesStateCopyWithImpl<$Res>; 76 | @override 77 | $Res call( 78 | {Option>>> favoritesState}); 79 | } 80 | 81 | /// @nodoc 82 | class __$FavoritesStateCopyWithImpl<$Res> 83 | extends _$FavoritesStateCopyWithImpl<$Res> 84 | implements _$FavoritesStateCopyWith<$Res> { 85 | __$FavoritesStateCopyWithImpl( 86 | _FavoritesState _value, $Res Function(_FavoritesState) _then) 87 | : super(_value, (v) => _then(v as _FavoritesState)); 88 | 89 | @override 90 | _FavoritesState get _value => super._value as _FavoritesState; 91 | 92 | @override 93 | $Res call({ 94 | Object favoritesState = freezed, 95 | }) { 96 | return _then(_FavoritesState( 97 | favoritesState: favoritesState == freezed 98 | ? _value.favoritesState 99 | : favoritesState 100 | as Option>>>, 101 | )); 102 | } 103 | } 104 | 105 | /// @nodoc 106 | class _$_FavoritesState implements _FavoritesState { 107 | const _$_FavoritesState({@required this.favoritesState}) 108 | : assert(favoritesState != null); 109 | 110 | @override 111 | final Option>>> favoritesState; 112 | 113 | @override 114 | String toString() { 115 | return 'FavoritesState(favoritesState: $favoritesState)'; 116 | } 117 | 118 | @override 119 | bool operator ==(dynamic other) { 120 | return identical(this, other) || 121 | (other is _FavoritesState && 122 | (identical(other.favoritesState, favoritesState) || 123 | const DeepCollectionEquality() 124 | .equals(other.favoritesState, favoritesState))); 125 | } 126 | 127 | @override 128 | int get hashCode => 129 | runtimeType.hashCode ^ 130 | const DeepCollectionEquality().hash(favoritesState); 131 | 132 | @JsonKey(ignore: true) 133 | @override 134 | _$FavoritesStateCopyWith<_FavoritesState> get copyWith => 135 | __$FavoritesStateCopyWithImpl<_FavoritesState>(this, _$identity); 136 | } 137 | 138 | abstract class _FavoritesState implements FavoritesState { 139 | const factory _FavoritesState( 140 | {@required 141 | Option>>> 142 | favoritesState}) = _$_FavoritesState; 143 | 144 | @override 145 | Option>>> get favoritesState; 146 | @override 147 | @JsonKey(ignore: true) 148 | _$FavoritesStateCopyWith<_FavoritesState> get copyWith; 149 | } 150 | -------------------------------------------------------------------------------- /lib/src/application/favorites/state.dart: -------------------------------------------------------------------------------- 1 | part of 'bloc.dart'; 2 | 3 | @freezed 4 | abstract class FavoritesState with _$FavoritesState { 5 | const factory FavoritesState({ 6 | @required 7 | Option>>> favoritesState, 8 | }) = _FavoritesState; 9 | 10 | factory FavoritesState.initial() => FavoritesState( 11 | favoritesState: none(), 12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /lib/src/application/profile/bloc.dart: -------------------------------------------------------------------------------- 1 | import 'package:dartz/dartz.dart'; 2 | import 'package:flutter_bloc/flutter_bloc.dart'; 3 | import 'package:freezed_annotation/freezed_annotation.dart'; 4 | import 'package:injectable/injectable.dart'; 5 | 6 | import '../../domain/entities/api_error/api_error.dart'; 7 | import '../../domain/entities/api_response/api_response.dart'; 8 | import '../../domain/entities/article/article.dart'; 9 | import '../../domain/entities/user/user.dart'; 10 | import '../../domain/profile/i_profile_facade.dart'; 11 | 12 | part 'bloc.freezed.dart'; 13 | 14 | part 'state.dart'; 15 | 16 | @injectable 17 | class ProfileBloc extends Cubit { 18 | final IProfileFacade _profileFacade; 19 | 20 | ProfileBloc(this._profileFacade) : super(ProfileState.initial()); 21 | 22 | void getUserInformation(String userId) async { 23 | emit(state.copyWith(userInformationState: none())); 24 | final result = await _profileFacade.getUserInformation(userId); 25 | emit(state.copyWith(userInformationState: optionOf(result))); 26 | } 27 | 28 | void getArticlesByUser(String userId) async { 29 | emit(state.copyWith(articlesState: none())); 30 | final result = await _profileFacade.getArticlesByUser(userId); 31 | emit(state.copyWith(articlesState: optionOf(result))); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /lib/src/application/profile/bloc.freezed.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | // ignore_for_file: deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies 3 | 4 | part of 'bloc.dart'; 5 | 6 | // ************************************************************************** 7 | // FreezedGenerator 8 | // ************************************************************************** 9 | 10 | T _$identity(T value) => value; 11 | 12 | /// @nodoc 13 | class _$ProfileStateTearOff { 14 | const _$ProfileStateTearOff(); 15 | 16 | // ignore: unused_element 17 | _ProfileState call( 18 | {@required 19 | Option>> userInformationState, 20 | @required 21 | Option>>> articlesState}) { 22 | return _ProfileState( 23 | userInformationState: userInformationState, 24 | articlesState: articlesState, 25 | ); 26 | } 27 | } 28 | 29 | /// @nodoc 30 | // ignore: unused_element 31 | const $ProfileState = _$ProfileStateTearOff(); 32 | 33 | /// @nodoc 34 | mixin _$ProfileState { 35 | Option>> get userInformationState; 36 | Option>>> get articlesState; 37 | 38 | @JsonKey(ignore: true) 39 | $ProfileStateCopyWith get copyWith; 40 | } 41 | 42 | /// @nodoc 43 | abstract class $ProfileStateCopyWith<$Res> { 44 | factory $ProfileStateCopyWith( 45 | ProfileState value, $Res Function(ProfileState) then) = 46 | _$ProfileStateCopyWithImpl<$Res>; 47 | $Res call( 48 | {Option>> userInformationState, 49 | Option>>> articlesState}); 50 | } 51 | 52 | /// @nodoc 53 | class _$ProfileStateCopyWithImpl<$Res> implements $ProfileStateCopyWith<$Res> { 54 | _$ProfileStateCopyWithImpl(this._value, this._then); 55 | 56 | final ProfileState _value; 57 | // ignore: unused_field 58 | final $Res Function(ProfileState) _then; 59 | 60 | @override 61 | $Res call({ 62 | Object userInformationState = freezed, 63 | Object articlesState = freezed, 64 | }) { 65 | return _then(_value.copyWith( 66 | userInformationState: userInformationState == freezed 67 | ? _value.userInformationState 68 | : userInformationState as Option>>, 69 | articlesState: articlesState == freezed 70 | ? _value.articlesState 71 | : articlesState 72 | as Option>>>, 73 | )); 74 | } 75 | } 76 | 77 | /// @nodoc 78 | abstract class _$ProfileStateCopyWith<$Res> 79 | implements $ProfileStateCopyWith<$Res> { 80 | factory _$ProfileStateCopyWith( 81 | _ProfileState value, $Res Function(_ProfileState) then) = 82 | __$ProfileStateCopyWithImpl<$Res>; 83 | @override 84 | $Res call( 85 | {Option>> userInformationState, 86 | Option>>> articlesState}); 87 | } 88 | 89 | /// @nodoc 90 | class __$ProfileStateCopyWithImpl<$Res> extends _$ProfileStateCopyWithImpl<$Res> 91 | implements _$ProfileStateCopyWith<$Res> { 92 | __$ProfileStateCopyWithImpl( 93 | _ProfileState _value, $Res Function(_ProfileState) _then) 94 | : super(_value, (v) => _then(v as _ProfileState)); 95 | 96 | @override 97 | _ProfileState get _value => super._value as _ProfileState; 98 | 99 | @override 100 | $Res call({ 101 | Object userInformationState = freezed, 102 | Object articlesState = freezed, 103 | }) { 104 | return _then(_ProfileState( 105 | userInformationState: userInformationState == freezed 106 | ? _value.userInformationState 107 | : userInformationState as Option>>, 108 | articlesState: articlesState == freezed 109 | ? _value.articlesState 110 | : articlesState 111 | as Option>>>, 112 | )); 113 | } 114 | } 115 | 116 | /// @nodoc 117 | class _$_ProfileState implements _ProfileState { 118 | const _$_ProfileState( 119 | {@required this.userInformationState, @required this.articlesState}) 120 | : assert(userInformationState != null), 121 | assert(articlesState != null); 122 | 123 | @override 124 | final Option>> userInformationState; 125 | @override 126 | final Option>>> articlesState; 127 | 128 | @override 129 | String toString() { 130 | return 'ProfileState(userInformationState: $userInformationState, articlesState: $articlesState)'; 131 | } 132 | 133 | @override 134 | bool operator ==(dynamic other) { 135 | return identical(this, other) || 136 | (other is _ProfileState && 137 | (identical(other.userInformationState, userInformationState) || 138 | const DeepCollectionEquality().equals( 139 | other.userInformationState, userInformationState)) && 140 | (identical(other.articlesState, articlesState) || 141 | const DeepCollectionEquality() 142 | .equals(other.articlesState, articlesState))); 143 | } 144 | 145 | @override 146 | int get hashCode => 147 | runtimeType.hashCode ^ 148 | const DeepCollectionEquality().hash(userInformationState) ^ 149 | const DeepCollectionEquality().hash(articlesState); 150 | 151 | @JsonKey(ignore: true) 152 | @override 153 | _$ProfileStateCopyWith<_ProfileState> get copyWith => 154 | __$ProfileStateCopyWithImpl<_ProfileState>(this, _$identity); 155 | } 156 | 157 | abstract class _ProfileState implements ProfileState { 158 | const factory _ProfileState( 159 | {@required 160 | Option>> userInformationState, 161 | @required 162 | Option>>> 163 | articlesState}) = _$_ProfileState; 164 | 165 | @override 166 | Option>> get userInformationState; 167 | @override 168 | Option>>> get articlesState; 169 | @override 170 | @JsonKey(ignore: true) 171 | _$ProfileStateCopyWith<_ProfileState> get copyWith; 172 | } 173 | -------------------------------------------------------------------------------- /lib/src/application/profile/state.dart: -------------------------------------------------------------------------------- 1 | part of 'bloc.dart'; 2 | 3 | @freezed 4 | abstract class ProfileState with _$ProfileState { 5 | const factory ProfileState({ 6 | @required Option>> userInformationState, 7 | @required 8 | Option>>> articlesState, 9 | }) = _ProfileState; 10 | 11 | factory ProfileState.initial() => ProfileState( 12 | userInformationState: none(), 13 | articlesState: none(), 14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /lib/src/application/search/bloc.dart: -------------------------------------------------------------------------------- 1 | import 'package:dartz/dartz.dart'; 2 | import 'package:flutter_bloc/flutter_bloc.dart'; 3 | import 'package:freezed_annotation/freezed_annotation.dart'; 4 | import 'package:injectable/injectable.dart'; 5 | 6 | import '../../domain/entities/api_error/api_error.dart'; 7 | import '../../domain/entities/api_response/api_response.dart'; 8 | import '../../domain/entities/article/article.dart'; 9 | import '../../domain/search/i_search_facade.dart'; 10 | 11 | part 'bloc.freezed.dart'; 12 | 13 | part 'state.dart'; 14 | 15 | @injectable 16 | class SearchBloc extends Cubit { 17 | final ISearchFacade _searchFacade; 18 | 19 | SearchBloc(this._searchFacade) : super(SearchState.initial()); 20 | 21 | void searchArticles(String query) async { 22 | emit(state.copyWith( 23 | searchState: none(), 24 | isSearching: true, 25 | )); 26 | final result = await _searchFacade.searchArticles(query); 27 | emit(state.copyWith( 28 | searchState: optionOf(result), 29 | isSearching: false, 30 | )); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /lib/src/application/search/bloc.freezed.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | // ignore_for_file: deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies 3 | 4 | part of 'bloc.dart'; 5 | 6 | // ************************************************************************** 7 | // FreezedGenerator 8 | // ************************************************************************** 9 | 10 | T _$identity(T value) => value; 11 | 12 | /// @nodoc 13 | class _$SearchStateTearOff { 14 | const _$SearchStateTearOff(); 15 | 16 | // ignore: unused_element 17 | _SearchState call( 18 | {@required 19 | Option>>> searchState, 20 | @required 21 | bool isSearching}) { 22 | return _SearchState( 23 | searchState: searchState, 24 | isSearching: isSearching, 25 | ); 26 | } 27 | } 28 | 29 | /// @nodoc 30 | // ignore: unused_element 31 | const $SearchState = _$SearchStateTearOff(); 32 | 33 | /// @nodoc 34 | mixin _$SearchState { 35 | Option>>> get searchState; 36 | bool get isSearching; 37 | 38 | @JsonKey(ignore: true) 39 | $SearchStateCopyWith get copyWith; 40 | } 41 | 42 | /// @nodoc 43 | abstract class $SearchStateCopyWith<$Res> { 44 | factory $SearchStateCopyWith( 45 | SearchState value, $Res Function(SearchState) then) = 46 | _$SearchStateCopyWithImpl<$Res>; 47 | $Res call( 48 | {Option>>> searchState, 49 | bool isSearching}); 50 | } 51 | 52 | /// @nodoc 53 | class _$SearchStateCopyWithImpl<$Res> implements $SearchStateCopyWith<$Res> { 54 | _$SearchStateCopyWithImpl(this._value, this._then); 55 | 56 | final SearchState _value; 57 | // ignore: unused_field 58 | final $Res Function(SearchState) _then; 59 | 60 | @override 61 | $Res call({ 62 | Object searchState = freezed, 63 | Object isSearching = freezed, 64 | }) { 65 | return _then(_value.copyWith( 66 | searchState: searchState == freezed 67 | ? _value.searchState 68 | : searchState as Option>>>, 69 | isSearching: 70 | isSearching == freezed ? _value.isSearching : isSearching as bool, 71 | )); 72 | } 73 | } 74 | 75 | /// @nodoc 76 | abstract class _$SearchStateCopyWith<$Res> 77 | implements $SearchStateCopyWith<$Res> { 78 | factory _$SearchStateCopyWith( 79 | _SearchState value, $Res Function(_SearchState) then) = 80 | __$SearchStateCopyWithImpl<$Res>; 81 | @override 82 | $Res call( 83 | {Option>>> searchState, 84 | bool isSearching}); 85 | } 86 | 87 | /// @nodoc 88 | class __$SearchStateCopyWithImpl<$Res> extends _$SearchStateCopyWithImpl<$Res> 89 | implements _$SearchStateCopyWith<$Res> { 90 | __$SearchStateCopyWithImpl( 91 | _SearchState _value, $Res Function(_SearchState) _then) 92 | : super(_value, (v) => _then(v as _SearchState)); 93 | 94 | @override 95 | _SearchState get _value => super._value as _SearchState; 96 | 97 | @override 98 | $Res call({ 99 | Object searchState = freezed, 100 | Object isSearching = freezed, 101 | }) { 102 | return _then(_SearchState( 103 | searchState: searchState == freezed 104 | ? _value.searchState 105 | : searchState as Option>>>, 106 | isSearching: 107 | isSearching == freezed ? _value.isSearching : isSearching as bool, 108 | )); 109 | } 110 | } 111 | 112 | /// @nodoc 113 | class _$_SearchState implements _SearchState { 114 | const _$_SearchState({@required this.searchState, @required this.isSearching}) 115 | : assert(searchState != null), 116 | assert(isSearching != null); 117 | 118 | @override 119 | final Option>>> searchState; 120 | @override 121 | final bool isSearching; 122 | 123 | @override 124 | String toString() { 125 | return 'SearchState(searchState: $searchState, isSearching: $isSearching)'; 126 | } 127 | 128 | @override 129 | bool operator ==(dynamic other) { 130 | return identical(this, other) || 131 | (other is _SearchState && 132 | (identical(other.searchState, searchState) || 133 | const DeepCollectionEquality() 134 | .equals(other.searchState, searchState)) && 135 | (identical(other.isSearching, isSearching) || 136 | const DeepCollectionEquality() 137 | .equals(other.isSearching, isSearching))); 138 | } 139 | 140 | @override 141 | int get hashCode => 142 | runtimeType.hashCode ^ 143 | const DeepCollectionEquality().hash(searchState) ^ 144 | const DeepCollectionEquality().hash(isSearching); 145 | 146 | @JsonKey(ignore: true) 147 | @override 148 | _$SearchStateCopyWith<_SearchState> get copyWith => 149 | __$SearchStateCopyWithImpl<_SearchState>(this, _$identity); 150 | } 151 | 152 | abstract class _SearchState implements SearchState { 153 | const factory _SearchState( 154 | {@required 155 | Option>>> searchState, 156 | @required 157 | bool isSearching}) = _$_SearchState; 158 | 159 | @override 160 | Option>>> get searchState; 161 | @override 162 | bool get isSearching; 163 | @override 164 | @JsonKey(ignore: true) 165 | _$SearchStateCopyWith<_SearchState> get copyWith; 166 | } 167 | -------------------------------------------------------------------------------- /lib/src/application/search/state.dart: -------------------------------------------------------------------------------- 1 | part of 'bloc.dart'; 2 | 3 | @freezed 4 | abstract class SearchState with _$SearchState { 5 | const factory SearchState({ 6 | @required Option>>> searchState, 7 | @required bool isSearching, 8 | }) = _SearchState; 9 | 10 | factory SearchState.initial() => SearchState( 11 | searchState: none(), 12 | isSearching: false, 13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /lib/src/application/sign_in/bloc.dart: -------------------------------------------------------------------------------- 1 | import 'package:dartz/dartz.dart'; 2 | import 'package:flutter_bloc/flutter_bloc.dart'; 3 | import 'package:freezed_annotation/freezed_annotation.dart'; 4 | import 'package:injectable/injectable.dart'; 5 | 6 | import '../../domain/entities/api_error/api_error.dart'; 7 | import '../../domain/entities/api_response/api_response.dart'; 8 | import '../../domain/entities/user/user.dart'; 9 | import '../../domain/sign_in/i_sign_in_facade.dart'; 10 | 11 | part 'bloc.freezed.dart'; 12 | 13 | part 'state.dart'; 14 | 15 | @injectable 16 | class SignInBloc extends Cubit { 17 | final ISignInFacade _signInFacade; 18 | 19 | SignInBloc(this._signInFacade) : super(SignInState.initial()); 20 | 21 | void signIn(User user) async { 22 | emit(SignInState(signInState: none())); 23 | final result = await _signInFacade.signIn(user: user); 24 | emit(SignInState(signInState: optionOf(result))); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /lib/src/application/sign_in/bloc.freezed.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | // ignore_for_file: deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies 3 | 4 | part of 'bloc.dart'; 5 | 6 | // ************************************************************************** 7 | // FreezedGenerator 8 | // ************************************************************************** 9 | 10 | T _$identity(T value) => value; 11 | 12 | /// @nodoc 13 | class _$SignInStateTearOff { 14 | const _$SignInStateTearOff(); 15 | 16 | // ignore: unused_element 17 | _SignInState call( 18 | {@required Option>> signInState}) { 19 | return _SignInState( 20 | signInState: signInState, 21 | ); 22 | } 23 | } 24 | 25 | /// @nodoc 26 | // ignore: unused_element 27 | const $SignInState = _$SignInStateTearOff(); 28 | 29 | /// @nodoc 30 | mixin _$SignInState { 31 | Option>> get signInState; 32 | 33 | @JsonKey(ignore: true) 34 | $SignInStateCopyWith get copyWith; 35 | } 36 | 37 | /// @nodoc 38 | abstract class $SignInStateCopyWith<$Res> { 39 | factory $SignInStateCopyWith( 40 | SignInState value, $Res Function(SignInState) then) = 41 | _$SignInStateCopyWithImpl<$Res>; 42 | $Res call({Option>> signInState}); 43 | } 44 | 45 | /// @nodoc 46 | class _$SignInStateCopyWithImpl<$Res> implements $SignInStateCopyWith<$Res> { 47 | _$SignInStateCopyWithImpl(this._value, this._then); 48 | 49 | final SignInState _value; 50 | // ignore: unused_field 51 | final $Res Function(SignInState) _then; 52 | 53 | @override 54 | $Res call({ 55 | Object signInState = freezed, 56 | }) { 57 | return _then(_value.copyWith( 58 | signInState: signInState == freezed 59 | ? _value.signInState 60 | : signInState as Option>>, 61 | )); 62 | } 63 | } 64 | 65 | /// @nodoc 66 | abstract class _$SignInStateCopyWith<$Res> 67 | implements $SignInStateCopyWith<$Res> { 68 | factory _$SignInStateCopyWith( 69 | _SignInState value, $Res Function(_SignInState) then) = 70 | __$SignInStateCopyWithImpl<$Res>; 71 | @override 72 | $Res call({Option>> signInState}); 73 | } 74 | 75 | /// @nodoc 76 | class __$SignInStateCopyWithImpl<$Res> extends _$SignInStateCopyWithImpl<$Res> 77 | implements _$SignInStateCopyWith<$Res> { 78 | __$SignInStateCopyWithImpl( 79 | _SignInState _value, $Res Function(_SignInState) _then) 80 | : super(_value, (v) => _then(v as _SignInState)); 81 | 82 | @override 83 | _SignInState get _value => super._value as _SignInState; 84 | 85 | @override 86 | $Res call({ 87 | Object signInState = freezed, 88 | }) { 89 | return _then(_SignInState( 90 | signInState: signInState == freezed 91 | ? _value.signInState 92 | : signInState as Option>>, 93 | )); 94 | } 95 | } 96 | 97 | /// @nodoc 98 | class _$_SignInState implements _SignInState { 99 | const _$_SignInState({@required this.signInState}) 100 | : assert(signInState != null); 101 | 102 | @override 103 | final Option>> signInState; 104 | 105 | @override 106 | String toString() { 107 | return 'SignInState(signInState: $signInState)'; 108 | } 109 | 110 | @override 111 | bool operator ==(dynamic other) { 112 | return identical(this, other) || 113 | (other is _SignInState && 114 | (identical(other.signInState, signInState) || 115 | const DeepCollectionEquality() 116 | .equals(other.signInState, signInState))); 117 | } 118 | 119 | @override 120 | int get hashCode => 121 | runtimeType.hashCode ^ const DeepCollectionEquality().hash(signInState); 122 | 123 | @JsonKey(ignore: true) 124 | @override 125 | _$SignInStateCopyWith<_SignInState> get copyWith => 126 | __$SignInStateCopyWithImpl<_SignInState>(this, _$identity); 127 | } 128 | 129 | abstract class _SignInState implements SignInState { 130 | const factory _SignInState( 131 | {@required Option>> signInState}) = 132 | _$_SignInState; 133 | 134 | @override 135 | Option>> get signInState; 136 | @override 137 | @JsonKey(ignore: true) 138 | _$SignInStateCopyWith<_SignInState> get copyWith; 139 | } 140 | -------------------------------------------------------------------------------- /lib/src/application/sign_in/state.dart: -------------------------------------------------------------------------------- 1 | part of 'bloc.dart'; 2 | 3 | @freezed 4 | abstract class SignInState with _$SignInState { 5 | const factory SignInState({ 6 | @required Option>> signInState, 7 | }) = _SignInState; 8 | 9 | factory SignInState.initial() => SignInState( 10 | signInState: none(), 11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /lib/src/application/sign_up/bloc.dart: -------------------------------------------------------------------------------- 1 | import 'package:dartz/dartz.dart'; 2 | import 'package:flutter_bloc/flutter_bloc.dart'; 3 | import 'package:freezed_annotation/freezed_annotation.dart'; 4 | import 'package:injectable/injectable.dart'; 5 | 6 | import '../../domain/entities/api_error/api_error.dart'; 7 | import '../../domain/entities/api_response/api_response.dart'; 8 | import '../../domain/entities/user/user.dart'; 9 | import '../../domain/sign_up/i_sign_up_facade.dart'; 10 | 11 | part 'bloc.freezed.dart'; 12 | 13 | part 'state.dart'; 14 | 15 | @injectable 16 | class SignUpBloc extends Cubit { 17 | final ISignUpFacade _signUpFacade; 18 | 19 | SignUpBloc(this._signUpFacade) : super(SignUpState.initial()); 20 | 21 | void signUp(User user) async { 22 | emit(SignUpState(signUpState: none())); 23 | final result = await _signUpFacade.signUp(user: user); 24 | emit(SignUpState(signUpState: optionOf(result))); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /lib/src/application/sign_up/bloc.freezed.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | // ignore_for_file: deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies 3 | 4 | part of 'bloc.dart'; 5 | 6 | // ************************************************************************** 7 | // FreezedGenerator 8 | // ************************************************************************** 9 | 10 | T _$identity(T value) => value; 11 | 12 | /// @nodoc 13 | class _$SignUpStateTearOff { 14 | const _$SignUpStateTearOff(); 15 | 16 | // ignore: unused_element 17 | _SignUpState call( 18 | {@required Option>> signUpState}) { 19 | return _SignUpState( 20 | signUpState: signUpState, 21 | ); 22 | } 23 | } 24 | 25 | /// @nodoc 26 | // ignore: unused_element 27 | const $SignUpState = _$SignUpStateTearOff(); 28 | 29 | /// @nodoc 30 | mixin _$SignUpState { 31 | Option>> get signUpState; 32 | 33 | @JsonKey(ignore: true) 34 | $SignUpStateCopyWith get copyWith; 35 | } 36 | 37 | /// @nodoc 38 | abstract class $SignUpStateCopyWith<$Res> { 39 | factory $SignUpStateCopyWith( 40 | SignUpState value, $Res Function(SignUpState) then) = 41 | _$SignUpStateCopyWithImpl<$Res>; 42 | $Res call({Option>> signUpState}); 43 | } 44 | 45 | /// @nodoc 46 | class _$SignUpStateCopyWithImpl<$Res> implements $SignUpStateCopyWith<$Res> { 47 | _$SignUpStateCopyWithImpl(this._value, this._then); 48 | 49 | final SignUpState _value; 50 | // ignore: unused_field 51 | final $Res Function(SignUpState) _then; 52 | 53 | @override 54 | $Res call({ 55 | Object signUpState = freezed, 56 | }) { 57 | return _then(_value.copyWith( 58 | signUpState: signUpState == freezed 59 | ? _value.signUpState 60 | : signUpState as Option>>, 61 | )); 62 | } 63 | } 64 | 65 | /// @nodoc 66 | abstract class _$SignUpStateCopyWith<$Res> 67 | implements $SignUpStateCopyWith<$Res> { 68 | factory _$SignUpStateCopyWith( 69 | _SignUpState value, $Res Function(_SignUpState) then) = 70 | __$SignUpStateCopyWithImpl<$Res>; 71 | @override 72 | $Res call({Option>> signUpState}); 73 | } 74 | 75 | /// @nodoc 76 | class __$SignUpStateCopyWithImpl<$Res> extends _$SignUpStateCopyWithImpl<$Res> 77 | implements _$SignUpStateCopyWith<$Res> { 78 | __$SignUpStateCopyWithImpl( 79 | _SignUpState _value, $Res Function(_SignUpState) _then) 80 | : super(_value, (v) => _then(v as _SignUpState)); 81 | 82 | @override 83 | _SignUpState get _value => super._value as _SignUpState; 84 | 85 | @override 86 | $Res call({ 87 | Object signUpState = freezed, 88 | }) { 89 | return _then(_SignUpState( 90 | signUpState: signUpState == freezed 91 | ? _value.signUpState 92 | : signUpState as Option>>, 93 | )); 94 | } 95 | } 96 | 97 | /// @nodoc 98 | class _$_SignUpState implements _SignUpState { 99 | const _$_SignUpState({@required this.signUpState}) 100 | : assert(signUpState != null); 101 | 102 | @override 103 | final Option>> signUpState; 104 | 105 | @override 106 | String toString() { 107 | return 'SignUpState(signUpState: $signUpState)'; 108 | } 109 | 110 | @override 111 | bool operator ==(dynamic other) { 112 | return identical(this, other) || 113 | (other is _SignUpState && 114 | (identical(other.signUpState, signUpState) || 115 | const DeepCollectionEquality() 116 | .equals(other.signUpState, signUpState))); 117 | } 118 | 119 | @override 120 | int get hashCode => 121 | runtimeType.hashCode ^ const DeepCollectionEquality().hash(signUpState); 122 | 123 | @JsonKey(ignore: true) 124 | @override 125 | _$SignUpStateCopyWith<_SignUpState> get copyWith => 126 | __$SignUpStateCopyWithImpl<_SignUpState>(this, _$identity); 127 | } 128 | 129 | abstract class _SignUpState implements SignUpState { 130 | const factory _SignUpState( 131 | {@required Option>> signUpState}) = 132 | _$_SignUpState; 133 | 134 | @override 135 | Option>> get signUpState; 136 | @override 137 | @JsonKey(ignore: true) 138 | _$SignUpStateCopyWith<_SignUpState> get copyWith; 139 | } 140 | -------------------------------------------------------------------------------- /lib/src/application/sign_up/state.dart: -------------------------------------------------------------------------------- 1 | part of 'bloc.dart'; 2 | 3 | @freezed 4 | abstract class SignUpState with _$SignUpState { 5 | const factory SignUpState({ 6 | @required Option>> signUpState, 7 | }) = _SignUpState; 8 | 9 | factory SignUpState.initial() => SignUpState( 10 | signUpState: none(), 11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /lib/src/domain/article_create/i_article_create_facade.dart: -------------------------------------------------------------------------------- 1 | import 'package:dartz/dartz.dart'; 2 | import 'package:dio/dio.dart'; 3 | 4 | import '../entities/api_error/api_error.dart'; 5 | import '../entities/api_response/api_response.dart'; 6 | 7 | abstract class IArticleCreateFacade { 8 | Future> createArticle(FormData data); 9 | } 10 | -------------------------------------------------------------------------------- /lib/src/domain/article_details/i_article_details_facade.dart: -------------------------------------------------------------------------------- 1 | import 'package:dartz/dartz.dart'; 2 | 3 | import '../entities/api_error/api_error.dart'; 4 | import '../entities/api_response/api_response.dart'; 5 | import '../entities/article/article.dart'; 6 | 7 | abstract class IArticleDetailsFacade { 8 | Future>> getArticleDetails( 9 | String articleId, { 10 | String userId, 11 | }); 12 | 13 | Future> addFavorite( 14 | String articleId, 15 | String userId, 16 | ); 17 | 18 | Future> removeFavorite( 19 | String articleId, 20 | String userId, 21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /lib/src/domain/articles/i_articles_facade.dart: -------------------------------------------------------------------------------- 1 | import 'package:dartz/dartz.dart'; 2 | 3 | import '../entities/api_error/api_error.dart'; 4 | import '../entities/api_response/api_response.dart'; 5 | import '../entities/article/article.dart'; 6 | import '../entities/category/category.dart'; 7 | 8 | abstract class IArticlesFacade { 9 | Future>>> getCategories(); 10 | 11 | Future>>> getArticles(); 12 | 13 | Future>>> getArticlesByCategory(String categoryId); 14 | } 15 | -------------------------------------------------------------------------------- /lib/src/domain/entities/api_error/api_error.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | 3 | part 'api_error.g.dart'; 4 | 5 | @JsonSerializable() 6 | class ApiError { 7 | final String message; 8 | 9 | ApiError({ 10 | this.message, 11 | }); 12 | 13 | factory ApiError.fromJson(Map json) => 14 | _$ApiErrorFromJson(json); 15 | 16 | Map toJson() => _$ApiErrorToJson(this); 17 | } 18 | -------------------------------------------------------------------------------- /lib/src/domain/entities/api_error/api_error.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'api_error.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | ApiError _$ApiErrorFromJson(Map json) { 10 | return ApiError( 11 | message: json['message'] as String, 12 | ); 13 | } 14 | 15 | Map _$ApiErrorToJson(ApiError instance) => { 16 | 'message': instance.message, 17 | }; 18 | -------------------------------------------------------------------------------- /lib/src/domain/entities/api_response/api_response.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | 3 | part 'api_response.g.dart'; 4 | 5 | @JsonSerializable(genericArgumentFactories: true) 6 | class ApiResponse { 7 | final bool status; 8 | final String message; 9 | final T data; 10 | 11 | ApiResponse({ 12 | this.status, 13 | this.message, 14 | this.data, 15 | }); 16 | 17 | factory ApiResponse.fromJson( 18 | Map json, 19 | T Function(Object json) fromJsonT, 20 | ) => 21 | _$ApiResponseFromJson(json, fromJsonT); 22 | 23 | Map toJson(Object Function(T value) toJsonT) => 24 | _$ApiResponseToJson(this, toJsonT); 25 | } 26 | -------------------------------------------------------------------------------- /lib/src/domain/entities/api_response/api_response.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'api_response.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | ApiResponse _$ApiResponseFromJson( 10 | Map json, 11 | T Function(Object json) fromJsonT, 12 | ) { 13 | return ApiResponse( 14 | status: json['status'] as bool, 15 | message: json['message'] as String, 16 | data: fromJsonT(json['data']), 17 | ); 18 | } 19 | 20 | Map _$ApiResponseToJson( 21 | ApiResponse instance, 22 | Object Function(T value) toJsonT, 23 | ) => 24 | { 25 | 'status': instance.status, 26 | 'message': instance.message, 27 | 'data': toJsonT(instance.data), 28 | }; 29 | -------------------------------------------------------------------------------- /lib/src/domain/entities/article/article.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | 3 | part 'article.g.dart'; 4 | 5 | @JsonSerializable() 6 | class Article { 7 | @JsonKey(name: '_id') 8 | final String id; 9 | final String title; 10 | final String content; 11 | @JsonKey(name: 'user_id') 12 | final String userId; 13 | @JsonKey(name: 'category_id') 14 | final String categoryId; 15 | @JsonKey(name: 'created_at') 16 | final String createdAt; 17 | final List images; 18 | final bool isFavorite; 19 | 20 | Article({ 21 | this.id, 22 | this.title, 23 | this.content, 24 | this.userId, 25 | this.categoryId, 26 | this.createdAt, 27 | this.images, 28 | this.isFavorite, 29 | }); 30 | 31 | factory Article.fromJson(Map json) => 32 | _$ArticleFromJson(json); 33 | 34 | Map toJson() => _$ArticleToJson(this); 35 | } 36 | -------------------------------------------------------------------------------- /lib/src/domain/entities/article/article.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'article.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | Article _$ArticleFromJson(Map json) { 10 | return Article( 11 | id: json['_id'] as String, 12 | title: json['title'] as String, 13 | content: json['content'] as String, 14 | userId: json['user_id'] as String, 15 | categoryId: json['category_id'] as String, 16 | createdAt: json['created_at'] as String, 17 | images: (json['images'] as List)?.map((e) => e as String)?.toList(), 18 | isFavorite: json['isFavorite'] as bool, 19 | ); 20 | } 21 | 22 | Map _$ArticleToJson(Article instance) => { 23 | '_id': instance.id, 24 | 'title': instance.title, 25 | 'content': instance.content, 26 | 'user_id': instance.userId, 27 | 'category_id': instance.categoryId, 28 | 'created_at': instance.createdAt, 29 | 'images': instance.images, 30 | 'isFavorite': instance.isFavorite, 31 | }; 32 | -------------------------------------------------------------------------------- /lib/src/domain/entities/category/category.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | 3 | part 'category.g.dart'; 4 | 5 | @JsonSerializable() 6 | class Category { 7 | @JsonKey(name: '_id') 8 | final String id; 9 | final String name; 10 | 11 | Category({ 12 | this.id, 13 | this.name, 14 | }); 15 | 16 | factory Category.fromJson(Map json) => 17 | _$CategoryFromJson(json); 18 | 19 | Map toJson() => _$CategoryToJson(this); 20 | } 21 | -------------------------------------------------------------------------------- /lib/src/domain/entities/category/category.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'category.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | Category _$CategoryFromJson(Map json) { 10 | return Category( 11 | id: json['_id'] as String, 12 | name: json['name'] as String, 13 | ); 14 | } 15 | 16 | Map _$CategoryToJson(Category instance) => { 17 | '_id': instance.id, 18 | 'name': instance.name, 19 | }; 20 | -------------------------------------------------------------------------------- /lib/src/domain/entities/favorite/favorite.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | 3 | import '../article/article.dart'; 4 | 5 | part 'favorite.g.dart'; 6 | 7 | @JsonSerializable() 8 | class Favorite { 9 | @JsonKey(name: '_id') 10 | final String id; 11 | @JsonKey(name: 'article_id') 12 | final String articleId; 13 | @JsonKey(name: 'user_id') 14 | final String userId; 15 | final Article article; 16 | 17 | Favorite({ 18 | this.id, 19 | this.articleId, 20 | this.userId, 21 | this.article, 22 | }); 23 | 24 | factory Favorite.fromJson(Map json) => 25 | _$FavoriteFromJson(json); 26 | 27 | Map toJson() => _$FavoriteToJson(this); 28 | } 29 | -------------------------------------------------------------------------------- /lib/src/domain/entities/favorite/favorite.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'favorite.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | Favorite _$FavoriteFromJson(Map json) { 10 | return Favorite( 11 | id: json['_id'] as String, 12 | articleId: json['article_id'] as String, 13 | userId: json['user_id'] as String, 14 | article: json['article'] == null 15 | ? null 16 | : Article.fromJson(json['article'] as Map), 17 | ); 18 | } 19 | 20 | Map _$FavoriteToJson(Favorite instance) => { 21 | '_id': instance.id, 22 | 'article_id': instance.articleId, 23 | 'user_id': instance.userId, 24 | 'article': instance.article, 25 | }; 26 | -------------------------------------------------------------------------------- /lib/src/domain/entities/user/user.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | 3 | part 'user.g.dart'; 4 | 5 | @JsonSerializable() 6 | class User { 7 | @JsonKey(name: '_id') 8 | final String id; 9 | @JsonKey(name: 'first_name') 10 | final String firstName; 11 | @JsonKey(name: 'last_name') 12 | final String lastName; 13 | final String email; 14 | final String password; 15 | final String token; 16 | 17 | User({ 18 | this.id, 19 | this.firstName, 20 | this.lastName, 21 | this.email, 22 | this.password, 23 | this.token, 24 | }); 25 | 26 | factory User.fromJson(Map json) => _$UserFromJson(json); 27 | 28 | Map toJson() => _$UserToJson(this); 29 | } 30 | -------------------------------------------------------------------------------- /lib/src/domain/entities/user/user.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'user.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | User _$UserFromJson(Map json) { 10 | return User( 11 | id: json['_id'] as String, 12 | firstName: json['first_name'] as String, 13 | lastName: json['last_name'] as String, 14 | email: json['email'] as String, 15 | password: json['password'] as String, 16 | token: json['token'] as String, 17 | ); 18 | } 19 | 20 | Map _$UserToJson(User instance) => { 21 | '_id': instance.id, 22 | 'first_name': instance.firstName, 23 | 'last_name': instance.lastName, 24 | 'email': instance.email, 25 | 'password': instance.password, 26 | 'token': instance.token, 27 | }; 28 | -------------------------------------------------------------------------------- /lib/src/domain/favorites/i_favorites_facade.dart: -------------------------------------------------------------------------------- 1 | import 'package:dartz/dartz.dart'; 2 | 3 | import '../entities/api_error/api_error.dart'; 4 | import '../entities/api_response/api_response.dart'; 5 | import '../entities/favorite/favorite.dart'; 6 | 7 | abstract class IFavoritesFacade { 8 | Future>>> getFavorites( 9 | String userId, 10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /lib/src/domain/profile/i_profile_facade.dart: -------------------------------------------------------------------------------- 1 | import 'package:dartz/dartz.dart'; 2 | 3 | import '../../domain/entities/user/user.dart'; 4 | import '../entities/api_error/api_error.dart'; 5 | import '../entities/api_response/api_response.dart'; 6 | import '../entities/article/article.dart'; 7 | 8 | abstract class IProfileFacade { 9 | Future>> getUserInformation( 10 | String userId, 11 | ); 12 | 13 | Future>>> getArticlesByUser( 14 | String userId, 15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /lib/src/domain/search/i_search_facade.dart: -------------------------------------------------------------------------------- 1 | import 'package:dartz/dartz.dart'; 2 | 3 | import '../entities/api_error/api_error.dart'; 4 | import '../entities/api_response/api_response.dart'; 5 | import '../entities/article/article.dart'; 6 | 7 | abstract class ISearchFacade { 8 | Future>>> searchArticles( 9 | String query); 10 | } 11 | -------------------------------------------------------------------------------- /lib/src/domain/sign_in/i_sign_in_facade.dart: -------------------------------------------------------------------------------- 1 | import 'package:dartz/dartz.dart'; 2 | import 'package:meta/meta.dart'; 3 | 4 | import '../entities/api_error/api_error.dart'; 5 | import '../entities/api_response/api_response.dart'; 6 | import '../entities/user/user.dart'; 7 | 8 | abstract class ISignInFacade { 9 | Future>> signIn({@required User user}); 10 | } 11 | -------------------------------------------------------------------------------- /lib/src/domain/sign_up/i_sign_up_facade.dart: -------------------------------------------------------------------------------- 1 | import 'package:dartz/dartz.dart'; 2 | import 'package:meta/meta.dart'; 3 | 4 | import '../entities/api_error/api_error.dart'; 5 | import '../entities/api_response/api_response.dart'; 6 | import '../entities/user/user.dart'; 7 | 8 | abstract class ISignUpFacade { 9 | Future>> signUp({@required User user}); 10 | } 11 | -------------------------------------------------------------------------------- /lib/src/infrastructure/article_create/article_create_facade.dart: -------------------------------------------------------------------------------- 1 | import 'package:dartz/dartz.dart'; 2 | import 'package:dio/dio.dart'; 3 | import 'package:injectable/injectable.dart'; 4 | 5 | import '../../domain/article_create/i_article_create_facade.dart'; 6 | import '../../domain/entities/api_error/api_error.dart'; 7 | import '../../domain/entities/api_response/api_response.dart'; 8 | import '../../injection.dart'; 9 | import '../core/network/network.dart'; 10 | 11 | @LazySingleton(as: IArticleCreateFacade) 12 | class ArticlesDetailsFacade implements IArticleCreateFacade { 13 | @override 14 | Future> createArticle(FormData data) async { 15 | try { 16 | final apiResponse = await getIt().createArticle(data); 17 | if (apiResponse.status) { 18 | return right(apiResponse); 19 | } else { 20 | return left( 21 | ApiError(message: apiResponse.message), 22 | ); 23 | } 24 | } catch (e) { 25 | return left( 26 | ApiError(message: '$e'), 27 | ); 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /lib/src/infrastructure/article_details/article_details_facade.dart: -------------------------------------------------------------------------------- 1 | import 'package:dartz/dartz.dart'; 2 | import 'package:injectable/injectable.dart'; 3 | 4 | import '../../domain/article_details/i_article_details_facade.dart'; 5 | import '../../domain/entities/api_error/api_error.dart'; 6 | import '../../domain/entities/api_response/api_response.dart'; 7 | import '../../domain/entities/article/article.dart'; 8 | import '../../domain/entities/favorite/favorite.dart'; 9 | import '../../injection.dart'; 10 | import '../core/network/network.dart'; 11 | 12 | @LazySingleton(as: IArticleDetailsFacade) 13 | class ArticlesDetailsFacade implements IArticleDetailsFacade { 14 | @override 15 | Future>> getArticleDetails( 16 | String id, { 17 | String userId, 18 | }) async { 19 | try { 20 | final apiResponse = 21 | await getIt().getArticlesById(id, userId: userId); 22 | if (apiResponse.status) { 23 | return right(apiResponse); 24 | } else { 25 | return left( 26 | ApiError(message: apiResponse.message), 27 | ); 28 | } 29 | } catch (e) { 30 | return left( 31 | ApiError(message: '$e'), 32 | ); 33 | } 34 | } 35 | 36 | @override 37 | Future> addFavorite( 38 | String articleId, 39 | String userId, 40 | ) async { 41 | try { 42 | final favorite = Favorite(articleId: articleId, userId: userId); 43 | final apiResponse = await getIt().addFavorite(favorite); 44 | if (apiResponse.status) { 45 | return right(apiResponse); 46 | } else { 47 | return left( 48 | ApiError(message: apiResponse.message), 49 | ); 50 | } 51 | } catch (e) { 52 | return left( 53 | ApiError(message: '$e'), 54 | ); 55 | } 56 | } 57 | 58 | @override 59 | Future> removeFavorite( 60 | String articleId, 61 | String userId, 62 | ) async { 63 | try { 64 | final favorite = Favorite(articleId: articleId, userId: userId); 65 | final apiResponse = 66 | await getIt().removeFavorite(favorite); 67 | if (apiResponse.status) { 68 | return right(apiResponse); 69 | } else { 70 | return left( 71 | ApiError(message: apiResponse.message), 72 | ); 73 | } 74 | } catch (e) { 75 | return left( 76 | ApiError(message: '$e'), 77 | ); 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /lib/src/infrastructure/articles/articles_facade.dart: -------------------------------------------------------------------------------- 1 | import 'package:dartz/dartz.dart'; 2 | import 'package:injectable/injectable.dart'; 3 | 4 | import '../../domain/articles/i_articles_facade.dart'; 5 | import '../../domain/entities/api_error/api_error.dart'; 6 | import '../../domain/entities/api_response/api_response.dart'; 7 | import '../../domain/entities/article/article.dart'; 8 | import '../../domain/entities/category/category.dart'; 9 | import '../../injection.dart'; 10 | import '../core/network/network.dart'; 11 | 12 | @LazySingleton(as: IArticlesFacade) 13 | class ArticlesFacade implements IArticlesFacade { 14 | @override 15 | Future>>> getCategories() async { 16 | try { 17 | final apiResponse = await getIt().getCategories(); 18 | if (apiResponse.status) { 19 | return right(apiResponse); 20 | } else { 21 | return left( 22 | ApiError(message: apiResponse.message), 23 | ); 24 | } 25 | } catch (e) { 26 | return left( 27 | ApiError(message: '$e'), 28 | ); 29 | } 30 | } 31 | 32 | @override 33 | Future>>> getArticles() async { 34 | try { 35 | final apiResponse = await getIt().getArticles(); 36 | if (apiResponse.status) { 37 | return right(apiResponse); 38 | } else { 39 | return left( 40 | ApiError(message: apiResponse.message), 41 | ); 42 | } 43 | } catch (e) { 44 | return left( 45 | ApiError(message: '$e'), 46 | ); 47 | } 48 | } 49 | 50 | @override 51 | Future>>> getArticlesByCategory( 52 | categoryId) async { 53 | try { 54 | final apiResponse = 55 | await getIt().getArticlesByCategory(categoryId); 56 | if (apiResponse.status) { 57 | return right(apiResponse); 58 | } else { 59 | return left( 60 | ApiError(message: apiResponse.message), 61 | ); 62 | } 63 | } catch (e) { 64 | return left( 65 | ApiError(message: '$e'), 66 | ); 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /lib/src/infrastructure/core/network/interceptor.dart: -------------------------------------------------------------------------------- 1 | import 'package:dio/dio.dart'; 2 | 3 | import '../../../injection.dart'; 4 | import '../preferences.dart'; 5 | 6 | class AppInterceptor extends Interceptor { 7 | final _preferences = getIt(); 8 | 9 | @override 10 | Future onRequest(RequestOptions options) async { 11 | final token = _preferences.token; 12 | options.headers.addAll({'Authorization': token}); 13 | return options; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /lib/src/infrastructure/core/network/network.dart: -------------------------------------------------------------------------------- 1 | import 'package:dio/dio.dart'; 2 | import 'package:retrofit/retrofit.dart'; 3 | 4 | import '../../../domain/entities/api_response/api_response.dart'; 5 | import '../../../domain/entities/article/article.dart'; 6 | import '../../../domain/entities/category/category.dart'; 7 | import '../../../domain/entities/favorite/favorite.dart'; 8 | import '../../../domain/entities/user/user.dart'; 9 | import '../../../presentation/routes/routes.dart'; 10 | import 'interceptor.dart'; 11 | 12 | part 'network.g.dart'; 13 | 14 | @RestApi(baseUrl: AppRoutes.host) 15 | abstract class AppNetworkClient { 16 | factory AppNetworkClient(Dio dio, {String baseUrl}) = _AppNetworkClient; 17 | 18 | @POST('/auth/login') 19 | Future> signIn( 20 | @Body() User user, 21 | ); 22 | 23 | @POST('/auth/register') 24 | Future> signUp( 25 | @Body() User user, 26 | ); 27 | 28 | @GET('/users/{userId}') 29 | Future> getUserInformation( 30 | @Path('userId') String userId, 31 | ); 32 | 33 | @GET('/categories') 34 | Future>> getCategories(); 35 | 36 | @POST('/articles') 37 | Future createArticle( 38 | @Body() FormData data, 39 | ); 40 | 41 | @GET('/articles') 42 | Future>> getArticles(); 43 | 44 | @GET('/articles/{id}') 45 | Future> getArticlesById( 46 | @Path('id') String id, { 47 | @Query('userId') String userId, 48 | }); 49 | 50 | @GET('/articles') 51 | Future>> searchArticles( 52 | @Query('query') String query, 53 | ); 54 | 55 | @GET('/articles/byCategory/{categoryId}') 56 | Future>> getArticlesByCategory( 57 | @Path('categoryId') String categoryId, 58 | ); 59 | 60 | @GET('/articles/byUser/{userId}') 61 | Future>> getArticlesByUser( 62 | @Path('userId') String userId, 63 | ); 64 | 65 | @GET('/favorites/{userId}') 66 | Future>> getFavorites( 67 | @Path('userId') String userId, 68 | ); 69 | 70 | @POST('/favorites') 71 | Future addFavorite( 72 | @Body() Favorite favorite, 73 | ); 74 | 75 | @DELETE('/favorites') 76 | Future removeFavorite( 77 | @Body() Favorite favorite, 78 | ); 79 | } 80 | 81 | class AppNetwork { 82 | AppNetwork._(); 83 | 84 | static AppNetworkClient _appNetworkClient; 85 | 86 | static AppNetworkClient get instance { 87 | if (_appNetworkClient == null) { 88 | final dio = Dio(); 89 | dio.interceptors.add(AppInterceptor()); 90 | _appNetworkClient = AppNetworkClient(dio); 91 | } 92 | return _appNetworkClient; 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /lib/src/infrastructure/core/preferences.dart: -------------------------------------------------------------------------------- 1 | import 'package:shared_preferences/shared_preferences.dart'; 2 | 3 | abstract class AppPreferencesKeys { 4 | static const String TOKEN = 'token'; 5 | static const String USER_ID = 'user_id'; 6 | } 7 | 8 | class AppPreferences { 9 | static Future get instance async { 10 | final sharedPreferences = await SharedPreferences.getInstance(); 11 | return AppPreferences(sharedPreferences); 12 | } 13 | 14 | AppPreferences(this.sharedPreferences); 15 | 16 | final SharedPreferences sharedPreferences; 17 | 18 | Future setString(String key, String value) => 19 | sharedPreferences.setString(key, value); 20 | 21 | String getString(String key, {String def}) => 22 | sharedPreferences.getString(key) ?? def; 23 | 24 | String get userId => getString(AppPreferencesKeys.USER_ID); 25 | 26 | String get token => getString(AppPreferencesKeys.TOKEN); 27 | 28 | bool get isLoggedIn => 29 | (getString(AppPreferencesKeys.TOKEN)?.isNotEmpty ?? false) && 30 | (getString(AppPreferencesKeys.USER_ID)?.isNotEmpty ?? false); 31 | 32 | Future clear() => sharedPreferences.clear(); 33 | } 34 | -------------------------------------------------------------------------------- /lib/src/infrastructure/favorites/favorites_facade.dart: -------------------------------------------------------------------------------- 1 | import 'package:dartz/dartz.dart'; 2 | import 'package:injectable/injectable.dart'; 3 | 4 | import '../../domain/entities/api_error/api_error.dart'; 5 | import '../../domain/entities/api_response/api_response.dart'; 6 | import '../../domain/entities/favorite/favorite.dart'; 7 | import '../../domain/favorites/i_favorites_facade.dart'; 8 | import '../../injection.dart'; 9 | import '../core/network/network.dart'; 10 | 11 | @LazySingleton(as: IFavoritesFacade) 12 | class FavoritesFacade implements IFavoritesFacade { 13 | @override 14 | Future>>> getFavorites( 15 | String userId) async { 16 | try { 17 | final apiResponse = await getIt().getFavorites(userId); 18 | if (apiResponse.status) { 19 | return right(apiResponse); 20 | } else { 21 | return left( 22 | ApiError(message: apiResponse.message), 23 | ); 24 | } 25 | } catch (e) { 26 | return left( 27 | ApiError(message: '$e'), 28 | ); 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /lib/src/infrastructure/profile/profile_facade.dart: -------------------------------------------------------------------------------- 1 | import 'package:dartz/dartz.dart'; 2 | import 'package:injectable/injectable.dart'; 3 | 4 | import '../../domain/entities/api_error/api_error.dart'; 5 | import '../../domain/entities/api_response/api_response.dart'; 6 | import '../../domain/entities/article/article.dart'; 7 | import '../../domain/entities/user/user.dart'; 8 | import '../../domain/profile/i_profile_facade.dart'; 9 | import '../../injection.dart'; 10 | import '../core/network/network.dart'; 11 | 12 | @LazySingleton(as: IProfileFacade) 13 | class ProfileFacade implements IProfileFacade { 14 | @override 15 | Future>> getUserInformation( 16 | String userId, 17 | ) async { 18 | try { 19 | final apiResponse = 20 | await getIt().getUserInformation(userId); 21 | if (apiResponse.status) { 22 | return right(apiResponse); 23 | } else { 24 | return left( 25 | ApiError(message: apiResponse.message), 26 | ); 27 | } 28 | } catch (e) { 29 | return left( 30 | ApiError(message: '$e'), 31 | ); 32 | } 33 | } 34 | 35 | @override 36 | Future>>> getArticlesByUser( 37 | String userId, 38 | ) async { 39 | try { 40 | final apiResponse = 41 | await getIt().getArticlesByUser(userId); 42 | if (apiResponse.status) { 43 | return right(apiResponse); 44 | } else { 45 | return left( 46 | ApiError(message: apiResponse.message), 47 | ); 48 | } 49 | } catch (e) { 50 | return left( 51 | ApiError(message: '$e'), 52 | ); 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /lib/src/infrastructure/search/search_facade.dart: -------------------------------------------------------------------------------- 1 | import 'package:dartz/dartz.dart'; 2 | import 'package:injectable/injectable.dart'; 3 | 4 | import '../../domain/entities/api_error/api_error.dart'; 5 | import '../../domain/entities/api_response/api_response.dart'; 6 | import '../../domain/entities/article/article.dart'; 7 | import '../../domain/search/i_search_facade.dart'; 8 | import '../../injection.dart'; 9 | import '../core/network/network.dart'; 10 | 11 | @LazySingleton(as: ISearchFacade) 12 | class SearchFacade implements ISearchFacade { 13 | @override 14 | Future>>> searchArticles( 15 | String query) async { 16 | try { 17 | final apiResponse = await getIt().searchArticles(query); 18 | if (apiResponse.status) { 19 | return right(apiResponse); 20 | } else { 21 | return left( 22 | ApiError(message: apiResponse.message), 23 | ); 24 | } 25 | } catch (e) { 26 | return left( 27 | ApiError(message: '$e'), 28 | ); 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /lib/src/infrastructure/sign_in/sign_in_facade.dart: -------------------------------------------------------------------------------- 1 | import 'package:dartz/dartz.dart'; 2 | import 'package:injectable/injectable.dart'; 3 | 4 | import '../../domain/entities/api_error/api_error.dart'; 5 | import '../../domain/entities/api_response/api_response.dart'; 6 | import '../../domain/entities/user/user.dart'; 7 | import '../../domain/sign_in/i_sign_in_facade.dart'; 8 | import '../../injection.dart'; 9 | import '../core/network/network.dart'; 10 | 11 | @LazySingleton(as: ISignInFacade) 12 | class SignInFacade implements ISignInFacade { 13 | @override 14 | Future>> signIn({User user}) async { 15 | try { 16 | final apiResponse = await getIt().signIn(user); 17 | if (apiResponse.status) { 18 | return right(apiResponse); 19 | } else { 20 | return left( 21 | ApiError(message: apiResponse.message), 22 | ); 23 | } 24 | } catch (e) { 25 | return left( 26 | ApiError(message: '$e'), 27 | ); 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /lib/src/infrastructure/sign_up/sign_up_facade.dart: -------------------------------------------------------------------------------- 1 | import 'package:dartz/dartz.dart'; 2 | import 'package:injectable/injectable.dart'; 3 | 4 | import '../../domain/entities/api_error/api_error.dart'; 5 | import '../../domain/entities/api_response/api_response.dart'; 6 | import '../../domain/entities/user/user.dart'; 7 | import '../../domain/sign_up/i_sign_up_facade.dart'; 8 | import '../../injection.dart'; 9 | import '../core/network/network.dart'; 10 | 11 | @LazySingleton(as: ISignUpFacade) 12 | class SignUpFacade implements ISignUpFacade { 13 | @override 14 | Future>> signUp({User user}) async { 15 | try { 16 | final apiResponse = await getIt().signUp(user); 17 | if (apiResponse.status) { 18 | return right(apiResponse); 19 | } else { 20 | return left( 21 | ApiError(message: apiResponse.message), 22 | ); 23 | } 24 | } catch (e) { 25 | return left( 26 | ApiError(message: '$e'), 27 | ); 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /lib/src/injection.config.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | // ************************************************************************** 4 | // InjectableConfigGenerator 5 | // ************************************************************************** 6 | 7 | import 'package:get_it/get_it.dart' as _i1; 8 | import 'package:injectable/injectable.dart' as _i2; 9 | 10 | import 'application/article_create/bloc.dart' as _i23; 11 | import 'application/article_details/bloc.dart' as _i24; 12 | import 'application/articles/bloc.dart' as _i25; 13 | import 'application/favorites/bloc.dart' as _i26; 14 | import 'application/profile/bloc.dart' as _i19; 15 | import 'application/search/bloc.dart' as _i20; 16 | import 'application/sign_in/bloc.dart' as _i21; 17 | import 'application/sign_up/bloc.dart' as _i22; 18 | import 'domain/article_create/i_article_create_facade.dart' as _i3; 19 | import 'domain/article_details/i_article_details_facade.dart' as _i5; 20 | import 'domain/articles/i_articles_facade.dart' as _i7; 21 | import 'domain/favorites/i_favorites_facade.dart' as _i9; 22 | import 'domain/profile/i_profile_facade.dart' as _i11; 23 | import 'domain/search/i_search_facade.dart' as _i13; 24 | import 'domain/sign_in/i_sign_in_facade.dart' as _i15; 25 | import 'domain/sign_up/i_sign_up_facade.dart' as _i17; 26 | import 'infrastructure/article_create/article_create_facade.dart' as _i4; 27 | import 'infrastructure/article_details/article_details_facade.dart' as _i6; 28 | import 'infrastructure/articles/articles_facade.dart' as _i8; 29 | import 'infrastructure/favorites/favorites_facade.dart' as _i10; 30 | import 'infrastructure/profile/profile_facade.dart' as _i12; 31 | import 'infrastructure/search/search_facade.dart' as _i14; 32 | import 'infrastructure/sign_in/sign_in_facade.dart' as _i16; 33 | import 'infrastructure/sign_up/sign_up_facade.dart' 34 | as _i18; // ignore_for_file: unnecessary_lambdas 35 | 36 | // ignore_for_file: lines_longer_than_80_chars 37 | /// initializes the registration of provided dependencies inside of [GetIt] 38 | _i1.GetIt $initGetIt(_i1.GetIt get, 39 | {String environment, _i2.EnvironmentFilter environmentFilter}) { 40 | final gh = _i2.GetItHelper(get, environment, environmentFilter); 41 | gh.lazySingleton<_i3.IArticleCreateFacade>(() => _i4.ArticlesDetailsFacade()); 42 | gh.lazySingleton<_i5.IArticleDetailsFacade>( 43 | () => _i6.ArticlesDetailsFacade()); 44 | gh.lazySingleton<_i7.IArticlesFacade>(() => _i8.ArticlesFacade()); 45 | gh.lazySingleton<_i9.IFavoritesFacade>(() => _i10.FavoritesFacade()); 46 | gh.lazySingleton<_i11.IProfileFacade>(() => _i12.ProfileFacade()); 47 | gh.lazySingleton<_i13.ISearchFacade>(() => _i14.SearchFacade()); 48 | gh.lazySingleton<_i15.ISignInFacade>(() => _i16.SignInFacade()); 49 | gh.lazySingleton<_i17.ISignUpFacade>(() => _i18.SignUpFacade()); 50 | gh.factory<_i19.ProfileBloc>( 51 | () => _i19.ProfileBloc(get<_i11.IProfileFacade>())); 52 | gh.factory<_i20.SearchBloc>(() => _i20.SearchBloc(get<_i13.ISearchFacade>())); 53 | gh.factory<_i21.SignInBloc>(() => _i21.SignInBloc(get<_i15.ISignInFacade>())); 54 | gh.factory<_i22.SignUpBloc>(() => _i22.SignUpBloc(get<_i17.ISignUpFacade>())); 55 | gh.factory<_i23.ArticleCreateBloc>( 56 | () => _i23.ArticleCreateBloc(get<_i3.IArticleCreateFacade>())); 57 | gh.factory<_i24.ArticleDetailsBloc>( 58 | () => _i24.ArticleDetailsBloc(get<_i5.IArticleDetailsFacade>())); 59 | gh.factory<_i25.ArticlesBloc>( 60 | () => _i25.ArticlesBloc(get<_i7.IArticlesFacade>())); 61 | gh.factory<_i26.FavoritesBloc>( 62 | () => _i26.FavoritesBloc(get<_i9.IFavoritesFacade>())); 63 | return get; 64 | } 65 | -------------------------------------------------------------------------------- /lib/src/injection.dart: -------------------------------------------------------------------------------- 1 | import 'package:get_it/get_it.dart'; 2 | import 'package:injectable/injectable.dart'; 3 | 4 | import 'infrastructure/core/network/network.dart'; 5 | import 'infrastructure/core/preferences.dart'; 6 | import 'injection.config.dart'; 7 | 8 | final getIt = GetIt.instance; 9 | 10 | @injectableInit 11 | void configureInjection(String env) { 12 | $initGetIt(getIt, environment: env); 13 | getIt.registerSingletonAsync( 14 | () async => await AppPreferences.instance, 15 | ); 16 | getIt.registerFactory(() => AppNetwork.instance); 17 | } 18 | -------------------------------------------------------------------------------- /lib/src/presentation/routes/routes.dart: -------------------------------------------------------------------------------- 1 | abstract class AppRoutes { 2 | static const String host = 'http://192.168.1.8:8888/'; 3 | 4 | static const String home = '/home'; 5 | static const String onBoarding = '/onBoarding'; 6 | static const String signIn = '/signIn'; 7 | static const String signUp = '/signUp'; 8 | static const String articleDetails = '/articleDetails'; 9 | static const String articleCreate = '/articleCreate'; 10 | } 11 | -------------------------------------------------------------------------------- /lib/src/presentation/routes/routes_generator.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/cupertino.dart'; 2 | import 'package:flutter/material.dart'; 3 | 4 | import '../../domain/entities/article/article.dart'; 5 | import '../ui/pages/article_create/article_create.dart'; 6 | import '../ui/pages/article_details/article_details.dart'; 7 | import '../ui/pages/home/home.dart'; 8 | import '../ui/pages/on_boarding/on_boarding.dart'; 9 | import '../ui/pages/sign_in/sign_in.dart'; 10 | import '../ui/pages/sign_up/sign_up.dart'; 11 | import 'routes.dart'; 12 | 13 | abstract class AppRoutesGenerator { 14 | static Route generateRoute(RouteSettings params) { 15 | var screen; 16 | var args = params.arguments; 17 | 18 | switch (params.name) { 19 | case AppRoutes.onBoarding: 20 | screen = OnBoardingPage(); 21 | break; 22 | case AppRoutes.signUp: 23 | screen = SignUpPage(); 24 | break; 25 | 26 | case AppRoutes.signIn: 27 | screen = SignInPage(); 28 | break; 29 | 30 | case AppRoutes.home: 31 | screen = HomePage(); 32 | break; 33 | 34 | case AppRoutes.articleDetails: 35 | if (args is Article) 36 | screen = ArticleDetails(article: args); 37 | else 38 | screen = _errorScreen(params.name); 39 | break; 40 | 41 | case AppRoutes.articleCreate: 42 | screen = ArticleCreatePage(); 43 | break; 44 | 45 | default: 46 | screen = _errorScreen(params.name); 47 | } 48 | 49 | return CupertinoPageRoute( 50 | builder: (_) => screen, 51 | ); 52 | } 53 | 54 | static Widget _errorScreen(String route) => Scaffold( 55 | body: Text('$route : Route Not Found'), 56 | ); 57 | } 58 | -------------------------------------------------------------------------------- /lib/src/presentation/ui/components/article_item.dart: -------------------------------------------------------------------------------- 1 | import 'package:cached_network_image/cached_network_image.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:intl/intl.dart'; 4 | 5 | import '../../../domain/entities/article/article.dart'; 6 | import '../../routes/routes.dart'; 7 | 8 | class ArticleItemWidget extends StatelessWidget { 9 | final Article article; 10 | 11 | const ArticleItemWidget({Key key, @required this.article}) : super(key: key); 12 | 13 | @override 14 | Widget build(BuildContext context) => Card( 15 | elevation: 3, 16 | clipBehavior: Clip.antiAlias, 17 | margin: const EdgeInsets.symmetric(horizontal: 15, vertical: 7.5), 18 | shape: RoundedRectangleBorder( 19 | borderRadius: BorderRadius.circular(10), 20 | ), 21 | child: InkWell( 22 | onTap: () { 23 | Navigator.of(context).pushNamed( 24 | AppRoutes.articleDetails, 25 | arguments: article, 26 | ); 27 | }, 28 | child: Padding( 29 | padding: const EdgeInsets.all(10), 30 | child: Column( 31 | crossAxisAlignment: CrossAxisAlignment.stretch, 32 | children: [ 33 | ClipRRect( 34 | borderRadius: BorderRadius.circular(10), 35 | child: AspectRatio( 36 | aspectRatio: 2, 37 | child: CachedNetworkImage( 38 | imageUrl: 39 | '${AppRoutes.host}files/articles/${article.id}/${article.images[0]}', 40 | fit: BoxFit.cover, 41 | ), 42 | ), 43 | ), 44 | const SizedBox( 45 | height: 20, 46 | ), 47 | Text( 48 | DateFormat('yyyy/MM/dd HH:mm').format( 49 | DateTime.parse(article.createdAt), 50 | ), 51 | style: TextStyle( 52 | color: Colors.grey, 53 | fontSize: 12, 54 | fontWeight: FontWeight.w300, 55 | fontStyle: FontStyle.italic, 56 | ), 57 | ), 58 | const SizedBox( 59 | height: 10, 60 | ), 61 | Text( 62 | article.title, 63 | style: TextStyle( 64 | color: Colors.black87, 65 | fontSize: 18, 66 | fontWeight: FontWeight.w600, 67 | ), 68 | ), 69 | const SizedBox( 70 | height: 10, 71 | ), 72 | ], 73 | ), 74 | ), 75 | ), 76 | ); 77 | } 78 | -------------------------------------------------------------------------------- /lib/src/presentation/ui/components/buttons/clear_button.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class ClearButtonWidget extends StatelessWidget { 4 | final VoidCallback onPressed; 5 | final Widget child; 6 | 7 | const ClearButtonWidget({Key key, @required this.onPressed, this.child}) 8 | : super(key: key); 9 | 10 | @override 11 | Widget build(BuildContext context) => InkWell( 12 | onTap: onPressed, 13 | child: child ?? 14 | Icon( 15 | Icons.close, 16 | color: Colors.white, 17 | ), 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /lib/src/presentation/ui/components/buttons/rounded_button.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class AppRoundedButton extends StatelessWidget { 4 | final VoidCallback onPressed; 5 | final String title; 6 | final EdgeInsetsGeometry padding; 7 | final double radius; 8 | final double elevation; 9 | 10 | const AppRoundedButton({ 11 | Key key, 12 | @required this.onPressed, 13 | @required this.title, 14 | this.padding = const EdgeInsets.symmetric(horizontal: 30, vertical: 17), 15 | this.radius = 34, 16 | this.elevation, 17 | }) : super(key: key); 18 | 19 | @override 20 | Widget build(BuildContext context) => RaisedButton( 21 | elevation: elevation, 22 | onPressed: onPressed, 23 | color: Theme.of(context).primaryColor, 24 | padding: padding, 25 | shape: RoundedRectangleBorder( 26 | borderRadius: BorderRadius.circular(radius), 27 | ), 28 | child: Text( 29 | title, 30 | style: Theme.of(context).textTheme.button.copyWith( 31 | color: Colors.white, 32 | ), 33 | ), 34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /lib/src/presentation/ui/components/buttons/rounded_outline_button.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class AppRoundedOutlineButton extends StatelessWidget { 4 | final VoidCallback onPressed; 5 | final String title; 6 | 7 | const AppRoundedOutlineButton({ 8 | Key key, 9 | @required this.onPressed, 10 | @required this.title, 11 | }) : super(key: key); 12 | 13 | @override 14 | Widget build(BuildContext context) => OutlineButton( 15 | onPressed: onPressed, 16 | color: Theme.of(context).primaryColor, 17 | padding: const EdgeInsets.symmetric(horizontal: 30, vertical: 17), 18 | borderSide: BorderSide( 19 | color: Theme.of(context).primaryColor, 20 | ), 21 | shape: RoundedRectangleBorder( 22 | borderRadius: BorderRadius.circular(34), 23 | ), 24 | child: Text( 25 | title, 26 | style: Theme.of(context).textTheme.button.copyWith( 27 | color: Theme.of(context).primaryColor, 28 | ), 29 | ), 30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /lib/src/presentation/ui/components/dialogs/question.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class AppDialogQuestion extends StatelessWidget { 4 | final String title; 5 | final String question; 6 | final List actions; 7 | 8 | const AppDialogQuestion({ 9 | Key key, 10 | this.title, 11 | @required this.question, 12 | this.actions, 13 | }) : super(key: key); 14 | 15 | @override 16 | Widget build(BuildContext context) => SimpleDialog( 17 | contentPadding: const EdgeInsets.all(15), 18 | shape: RoundedRectangleBorder( 19 | borderRadius: BorderRadius.circular(10), 20 | ), 21 | title: title != null ? Text(title) : const SizedBox(), 22 | children: [ 23 | Text( 24 | question, 25 | style: Theme.of(context).textTheme.bodyText2, 26 | ), 27 | const SizedBox( 28 | height: 20, 29 | ), 30 | if (actions != null) 31 | Row( 32 | mainAxisAlignment: MainAxisAlignment.end, 33 | children: actions, 34 | ), 35 | ], 36 | ); 37 | } 38 | -------------------------------------------------------------------------------- /lib/src/presentation/ui/components/dialogs/waiting.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class AppDialogWaiting extends StatelessWidget { 4 | @override 5 | Widget build(BuildContext context) => Dialog( 6 | //contentPadding: const EdgeInsets.all(15), 7 | shape: RoundedRectangleBorder( 8 | borderRadius: BorderRadius.circular(10), 9 | ), 10 | child: Padding( 11 | padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 30), 12 | child: Row( 13 | children: [ 14 | CircularProgressIndicator(), 15 | const SizedBox( 16 | width: 20, 17 | ), 18 | Text('Please Wait...'), 19 | ], 20 | ), 21 | ), 22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /lib/src/presentation/ui/components/loading.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class LoadingWidget extends StatelessWidget { 4 | @override 5 | Widget build(BuildContext context) => Center( 6 | child: SizedBox( 7 | height: 25, 8 | width: 25, 9 | child: CircularProgressIndicator( 10 | strokeWidth: 2.5, 11 | ), 12 | ), 13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /lib/src/presentation/ui/components/login.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_svg/flutter_svg.dart'; 3 | 4 | import '../../routes/routes.dart'; 5 | import 'buttons/rounded_button.dart'; 6 | import 'buttons/rounded_outline_button.dart'; 7 | 8 | class AskLoginWidget extends StatelessWidget { 9 | @override 10 | Widget build(BuildContext context) => Center( 11 | child: Column( 12 | mainAxisSize: MainAxisSize.min, 13 | children: [ 14 | SvgPicture.asset( 15 | 'assets/images/shield.svg', 16 | width: 200, 17 | ), 18 | const SizedBox( 19 | height: 50, 20 | ), 21 | SizedBox( 22 | width: 200, 23 | child: AppRoundedButton( 24 | onPressed: () { 25 | Navigator.of(context).pushNamed(AppRoutes.signIn); 26 | }, 27 | title: 'SIGN IN', 28 | ), 29 | ), 30 | const SizedBox( 31 | height: 20, 32 | ), 33 | SizedBox( 34 | width: 200, 35 | child: AppRoundedOutlineButton( 36 | onPressed: () { 37 | Navigator.of(context).pushNamed(AppRoutes.signUp); 38 | }, 39 | title: 'SIGN UP', 40 | ), 41 | ), 42 | ], 43 | ), 44 | ); 45 | } 46 | -------------------------------------------------------------------------------- /lib/src/presentation/ui/components/my_article_item.dart: -------------------------------------------------------------------------------- 1 | import 'package:cached_network_image/cached_network_image.dart'; 2 | import 'package:flutter/cupertino.dart'; 3 | import 'package:flutter/material.dart'; 4 | import 'package:intl/intl.dart'; 5 | 6 | import '../../../domain/entities/article/article.dart'; 7 | import '../../routes/routes.dart'; 8 | 9 | class MyArticleItemWidget extends StatelessWidget { 10 | final Article article; 11 | final VoidCallback onDelete; 12 | 13 | const MyArticleItemWidget({Key key, @required this.article, this.onDelete}) 14 | : super(key: key); 15 | 16 | @override 17 | Widget build(BuildContext context) => Card( 18 | elevation: 3, 19 | clipBehavior: Clip.antiAlias, 20 | margin: const EdgeInsets.symmetric(vertical: 7.5), 21 | shape: RoundedRectangleBorder( 22 | borderRadius: BorderRadius.circular(10), 23 | ), 24 | child: InkWell( 25 | onTap: () { 26 | Navigator.of(context).pushNamed( 27 | AppRoutes.articleDetails, 28 | arguments: article, 29 | ); 30 | }, 31 | child: Padding( 32 | padding: const EdgeInsets.all(10), 33 | child: Row( 34 | crossAxisAlignment: CrossAxisAlignment.start, 35 | children: [ 36 | ClipRRect( 37 | borderRadius: BorderRadius.circular(10), 38 | child: CachedNetworkImage( 39 | imageUrl: 40 | '${AppRoutes.host}files/articles/${article.id}/${article.images[0]}', 41 | fit: BoxFit.cover, 42 | height: 100, 43 | width: 100, 44 | ), 45 | ), 46 | const SizedBox( 47 | width: 10, 48 | ), 49 | Expanded( 50 | child: Column( 51 | crossAxisAlignment: CrossAxisAlignment.start, 52 | children: [ 53 | Text( 54 | article.title, 55 | style: TextStyle( 56 | color: Colors.black87, 57 | fontSize: 18, 58 | fontWeight: FontWeight.w600, 59 | ), 60 | ), 61 | const SizedBox( 62 | height: 5, 63 | ), 64 | Text( 65 | article.content, 66 | style: TextStyle( 67 | color: Colors.grey, 68 | fontSize: 12, 69 | fontWeight: FontWeight.w300, 70 | ), 71 | maxLines: 2, 72 | overflow: TextOverflow.ellipsis, 73 | ), 74 | const SizedBox( 75 | height: 10, 76 | ), 77 | Align( 78 | alignment: AlignmentDirectional.bottomEnd, 79 | child: Text( 80 | DateFormat('yyyy/MM/dd HH:mm').format( 81 | DateTime.parse(article.createdAt), 82 | ), 83 | style: TextStyle( 84 | color: Colors.grey, 85 | fontSize: 12, 86 | fontWeight: FontWeight.w300, 87 | fontStyle: FontStyle.italic, 88 | ), 89 | ), 90 | ), 91 | ], 92 | ), 93 | ), 94 | ], 95 | ), 96 | ), 97 | ), 98 | ); 99 | } 100 | -------------------------------------------------------------------------------- /lib/src/presentation/ui/components/text_fields/rounded_outline_text_field.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class AppRoundedOutlineTextFormField extends StatefulWidget { 4 | final TextEditingController controller; 5 | final IconData prefixIcon; 6 | final String hint; 7 | final bool obscureText; 8 | final TextInputAction textInputAction; 9 | final TextInputType keyboardType; 10 | final double borderRadius; 11 | final int maxLines; 12 | final String Function(String) validator; 13 | 14 | const AppRoundedOutlineTextFormField({ 15 | Key key, 16 | this.controller, 17 | this.prefixIcon, 18 | this.hint, 19 | this.obscureText = false, 20 | this.textInputAction, 21 | this.keyboardType, 22 | this.borderRadius = 20, 23 | this.maxLines = 1, 24 | this.validator, 25 | }) : super(key: key); 26 | 27 | @override 28 | _AppRoundedOutlineTextFormFieldState createState() => 29 | _AppRoundedOutlineTextFormFieldState(); 30 | } 31 | 32 | class _AppRoundedOutlineTextFormFieldState 33 | extends State { 34 | bool _isObscureText; 35 | 36 | @override 37 | void initState() { 38 | super.initState(); 39 | _isObscureText = widget.obscureText; 40 | } 41 | 42 | @override 43 | Widget build(BuildContext context) => TextFormField( 44 | validator: widget.validator, 45 | controller: widget.controller, 46 | obscureText: _isObscureText, 47 | textInputAction: widget.textInputAction, 48 | keyboardType: widget.keyboardType, 49 | maxLines: widget.maxLines, 50 | decoration: InputDecoration( 51 | prefixIcon: 52 | (widget.prefixIcon == null) ? null : Icon(widget.prefixIcon), 53 | suffixIcon: widget.obscureText ? _buildObscureIcon() : null, 54 | hintText: widget.hint, 55 | border: OutlineInputBorder( 56 | borderRadius: BorderRadius.circular( 57 | widget.borderRadius, 58 | ), 59 | ), 60 | ), 61 | ); 62 | 63 | Widget _buildObscureIcon() => IconButton( 64 | onPressed: () { 65 | setState(() { 66 | _isObscureText = !_isObscureText; 67 | }); 68 | }, 69 | icon: Icon( 70 | _isObscureText ? Icons.remove_red_eye : Icons.remove_red_eye_outlined, 71 | ), 72 | ); 73 | } 74 | -------------------------------------------------------------------------------- /lib/src/presentation/ui/components/text_fields/search_field.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:flutter/material.dart'; 4 | 5 | import '../buttons/clear_button.dart'; 6 | 7 | class SearchFieldWidget extends StatefulWidget { 8 | final Function(String) onChanged; 9 | final TextStyle textStyle; 10 | final InputDecoration inputDecoration; 11 | final bool autoFocus; 12 | final Duration textChangeDuration; 13 | final Widget clearButton; 14 | final bool showClearButton; 15 | final int minSearchCharacters; 16 | 17 | const SearchFieldWidget({ 18 | Key key, 19 | this.onChanged, 20 | this.inputDecoration, 21 | this.textStyle, 22 | this.autoFocus = true, 23 | this.textChangeDuration = const Duration(milliseconds: 500), 24 | this.clearButton, 25 | this.showClearButton = true, 26 | this.minSearchCharacters = 2, 27 | }) : super(key: key); 28 | 29 | @override 30 | _SearchFieldWidgetState createState() => _SearchFieldWidgetState(); 31 | } 32 | 33 | class _SearchFieldWidgetState extends State { 34 | TextEditingController _textEditingController = TextEditingController(); 35 | Timer _timer; 36 | 37 | bool _isClearButtonVisible = false; 38 | 39 | void _onChanged() { 40 | if (_textEditingController.text.length < widget.minSearchCharacters) return; 41 | 42 | if (_timer?.isActive ?? false) { 43 | _timer?.cancel(); 44 | _timer = null; 45 | } 46 | _timer = Timer(widget.textChangeDuration, () { 47 | widget.onChanged?.call(_textEditingController.text); 48 | }); 49 | } 50 | 51 | @override 52 | void initState() { 53 | super.initState(); 54 | 55 | _textEditingController.addListener(() { 56 | if (_textEditingController.text.isNotEmpty && !_isClearButtonVisible) 57 | setState(() { 58 | _isClearButtonVisible = true; 59 | }); 60 | else if (_textEditingController.text.isEmpty && _isClearButtonVisible) 61 | setState(() { 62 | _isClearButtonVisible = false; 63 | }); 64 | }); 65 | } 66 | 67 | @override 68 | Widget build(BuildContext context) => Row( 69 | children: [ 70 | Icon( 71 | Icons.search, 72 | color: Colors.grey[400], 73 | ), 74 | const SizedBox( 75 | width: 5, 76 | ), 77 | Expanded( 78 | child: TextField( 79 | onChanged: (_) { 80 | _onChanged(); 81 | }, 82 | style: widget.textStyle ?? 83 | TextStyle( 84 | color: Colors.white, 85 | fontSize: 18, 86 | ), 87 | decoration: widget.inputDecoration ?? 88 | InputDecoration( 89 | hintText: 'Search', 90 | hintStyle: TextStyle( 91 | color: Colors.white.withOpacity(.5), 92 | fontSize: 18, 93 | ), 94 | border: InputBorder.none, 95 | ), 96 | controller: _textEditingController, 97 | autofocus: widget.autoFocus ?? true, 98 | textInputAction: TextInputAction.search, 99 | ), 100 | ), 101 | if (widget.showClearButton) 102 | Visibility( 103 | visible: _isClearButtonVisible, 104 | child: ClearButtonWidget( 105 | onPressed: () { 106 | _textEditingController.clear(); 107 | }, 108 | child: widget.clearButton, 109 | ), 110 | ), 111 | ], 112 | ); 113 | 114 | @override 115 | void dispose() { 116 | _textEditingController.dispose(); 117 | _timer?.cancel(); 118 | super.dispose(); 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /lib/src/presentation/ui/pages/article_create/article_create.dart: -------------------------------------------------------------------------------- 1 | import 'package:dio/dio.dart' as dio; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter_bloc/flutter_bloc.dart'; 4 | import 'package:fluttertoast/fluttertoast.dart'; 5 | 6 | import '../../../../application/article_create/bloc.dart'; 7 | import '../../../../application/articles/bloc.dart'; 8 | import '../../../../domain/entities/category/category.dart'; 9 | import '../../../../infrastructure/core/preferences.dart'; 10 | import '../../../../injection.dart'; 11 | import '../../../utils/extensions.dart'; 12 | import '../../components/buttons/rounded_button.dart'; 13 | import '../../components/dialogs/waiting.dart'; 14 | import '../../components/text_fields/rounded_outline_text_field.dart'; 15 | import 'widgets/widgets.dart'; 16 | 17 | class ArticleCreatePage extends StatefulWidget { 18 | @override 19 | _ArticleCreatePageState createState() => _ArticleCreatePageState(); 20 | } 21 | 22 | class _ArticleCreatePageState extends State { 23 | final _bloc = getIt(); 24 | final _preferences = getIt(); 25 | 26 | final _formKey = GlobalKey(); 27 | 28 | final _titleController = TextEditingController(); 29 | final _contentController = TextEditingController(); 30 | List _selectedImages = []; 31 | Category _selectedCategory; 32 | 33 | void _createArticle() { 34 | print(_contentController.text); 35 | final data = dio.FormData.fromMap({ 36 | 'title': _titleController.text, 37 | 'content': _contentController.text, 38 | 'category_id': _selectedCategory.id, 39 | 'user_id': _preferences.userId, 40 | }); 41 | data.files.addAll( 42 | _selectedImages 43 | .map((path) => MapEntry(path, dio.MultipartFile.fromFileSync(path))) 44 | .toList(), 45 | ); 46 | 47 | _bloc.createArticle(data); 48 | 49 | FocusScope.of(context).unfocus(); 50 | 51 | context.showAppDialog( 52 | isDismissible: false, 53 | child: AppDialogWaiting(), 54 | ); 55 | } 56 | 57 | void _validForm() { 58 | if (_selectedImages?.isEmpty ?? true) { 59 | Fluttertoast.showToast(msg: 'Please select some images'); 60 | return; 61 | } 62 | 63 | if (_selectedCategory == null) { 64 | Fluttertoast.showToast(msg: 'Please select category'); 65 | return; 66 | } 67 | 68 | bool isFormValid = _formKey.currentState.validate(); 69 | if (!isFormValid) return; 70 | 71 | _createArticle(); 72 | } 73 | 74 | @override 75 | void initState() { 76 | super.initState(); 77 | _bloc.listen((state) { 78 | state.articleCreateState.fold( 79 | () => null, 80 | (either) => either.fold( 81 | (apiError) { 82 | Navigator.pop(context); 83 | Fluttertoast.showToast(msg: apiError.message); 84 | }, 85 | (result) { 86 | Navigator.pop(context); 87 | Fluttertoast.showToast(msg: result.message); 88 | Navigator.pop(context); 89 | }, 90 | ), 91 | ); 92 | }); 93 | } 94 | 95 | @override 96 | Widget build(BuildContext context) => Scaffold( 97 | appBar: AppBar( 98 | title: Text('Create Article'), 99 | ), 100 | body: BlocProvider( 101 | create: (_) => getIt()..getCategories(), 102 | child: Form( 103 | key: _formKey, 104 | child: ListView( 105 | padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10), 106 | children: [ 107 | ImagesWidget( 108 | onSelectImages: (images) { 109 | _selectedImages = images; 110 | }, 111 | ), 112 | const SizedBox( 113 | height: 30, 114 | ), 115 | CategoriesWidget( 116 | onSelectCategory: (category) { 117 | _selectedCategory = category; 118 | }, 119 | ), 120 | const SizedBox( 121 | height: 20, 122 | ), 123 | AppRoundedOutlineTextFormField( 124 | controller: _titleController, 125 | textInputAction: TextInputAction.next, 126 | validator: (text) { 127 | // TODO just for test purpose this is not the best way to validate a text 128 | if ((text?.isEmpty ?? true) || text.length < 4) 129 | return 'Please insert valid title!'; 130 | return null; 131 | }, 132 | hint: 'Title', 133 | borderRadius: 10, 134 | ), 135 | const SizedBox( 136 | height: 20, 137 | ), 138 | AppRoundedOutlineTextFormField( 139 | controller: _contentController, 140 | hint: 'Content', 141 | validator: (text) { 142 | // TODO just for test purpose this is not the best way to validate a text 143 | if ((text?.isEmpty ?? true) || text.length < 10) 144 | return 'Please insert valid content!'; 145 | return null; 146 | }, 147 | maxLines: 7, 148 | borderRadius: 10, 149 | ), 150 | const SizedBox( 151 | height: 30, 152 | ), 153 | AppRoundedButton( 154 | onPressed: _validForm, 155 | radius: 10, 156 | title: 'Create Article', 157 | ), 158 | ], 159 | ), 160 | ), 161 | ), 162 | ); 163 | 164 | @override 165 | void dispose() { 166 | _titleController.dispose(); 167 | _contentController.dispose(); 168 | super.dispose(); 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /lib/src/presentation/ui/pages/article_create/widgets/categories.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_bloc/flutter_bloc.dart'; 3 | 4 | import '../../../../../application/articles/bloc.dart'; 5 | import '../../../../../domain/entities/category/category.dart'; 6 | import '../../../components/loading.dart'; 7 | 8 | class CategoriesWidget extends StatefulWidget { 9 | final Function(Category) onSelectCategory; 10 | 11 | const CategoriesWidget({Key key, this.onSelectCategory}) : super(key: key); 12 | 13 | @override 14 | _CategoriesWidgetState createState() => _CategoriesWidgetState(); 15 | } 16 | 17 | class _CategoriesWidgetState extends State { 18 | Category _selectedCategory; 19 | 20 | void _selectCategory(Category category) { 21 | setState(() { 22 | if (_selectedCategory?.id == category?.id) 23 | _selectedCategory = null; 24 | else 25 | _selectedCategory = category; 26 | }); 27 | 28 | widget.onSelectCategory?.call(_selectedCategory); 29 | } 30 | 31 | @override 32 | Widget build(BuildContext context) => 33 | BlocBuilder( 34 | builder: (context, state) => state.categoriesState.fold( 35 | () { 36 | _selectedCategory = null; 37 | return LoadingWidget(); 38 | }, 39 | (either) => either.fold( 40 | (apiError) => Text(apiError.message ?? 'Unknown Error!'), 41 | (result) => Column( 42 | crossAxisAlignment: CrossAxisAlignment.start, 43 | children: [ 44 | Text( 45 | 'Select Category', 46 | style: TextStyle( 47 | fontSize: 20, 48 | fontWeight: FontWeight.w700, 49 | ), 50 | ), 51 | const SizedBox( 52 | height: 20, 53 | ), 54 | Wrap( 55 | spacing: 10, 56 | children: result.data.map((category) { 57 | bool isSelected = _selectedCategory?.id == category.id; 58 | return InkWell( 59 | onTap: () { 60 | _selectCategory(category); 61 | }, 62 | child: Chip( 63 | backgroundColor: 64 | isSelected ? Theme.of(context).primaryColor : null, 65 | label: Text( 66 | category.name, 67 | style: TextStyle( 68 | color: isSelected ? Colors.white : null, 69 | ), 70 | ), 71 | ), 72 | ); 73 | }).toList(), 74 | ), 75 | ], 76 | ), 77 | ), 78 | ), 79 | ); 80 | } 81 | -------------------------------------------------------------------------------- /lib/src/presentation/ui/pages/article_create/widgets/image.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:flutter/material.dart'; 4 | 5 | class ImageWidget extends StatelessWidget { 6 | final String path; 7 | final VoidCallback onDelete; 8 | 9 | const ImageWidget({Key key, @required this.path, this.onDelete}) 10 | : super(key: key); 11 | 12 | @override 13 | Widget build(BuildContext context) => AspectRatio( 14 | aspectRatio: 1.5, 15 | child: Stack( 16 | children: [ 17 | Positioned.fill( 18 | child: Container( 19 | height: 120, 20 | clipBehavior: Clip.antiAlias, 21 | decoration: BoxDecoration( 22 | color: Colors.grey[400], 23 | borderRadius: BorderRadius.circular(10), 24 | ), 25 | child: path != null 26 | ? Image.file( 27 | File(path), 28 | fit: BoxFit.cover, 29 | ) 30 | : Icon( 31 | Icons.image, 32 | size: 70, 33 | color: Colors.white, 34 | ), 35 | ), 36 | ), 37 | PositionedDirectional( 38 | top: 5, 39 | end: 5, 40 | child: InkWell( 41 | onTap: onDelete, 42 | child: SizedBox( 43 | height: 30, 44 | width: 30, 45 | child: CircleAvatar( 46 | backgroundColor: Colors.white, 47 | child: Icon(Icons.close), 48 | ), 49 | ), 50 | ), 51 | ), 52 | ], 53 | ), 54 | ); 55 | } 56 | -------------------------------------------------------------------------------- /lib/src/presentation/ui/pages/article_create/widgets/images.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:image_picker/image_picker.dart'; 3 | 4 | import 'image.dart'; 5 | 6 | class ImagesWidget extends StatefulWidget { 7 | final Function(List) onSelectImages; 8 | 9 | const ImagesWidget({Key key, this.onSelectImages}) : super(key: key); 10 | 11 | @override 12 | _ImagesWidgetState createState() => _ImagesWidgetState(); 13 | } 14 | 15 | class _ImagesWidgetState extends State { 16 | List _selectedImages = []; 17 | 18 | final _imagePicker = ImagePicker(); 19 | 20 | void _addImage() async { 21 | final pickedImage = await _imagePicker.getImage( 22 | source: ImageSource.gallery, 23 | ); 24 | if (pickedImage == null) return; 25 | 26 | setState(() { 27 | _selectedImages.add(pickedImage.path); 28 | }); 29 | 30 | widget.onSelectImages?.call(_selectedImages); 31 | } 32 | 33 | void _deleteImage(int index) async { 34 | setState(() { 35 | _selectedImages.removeAt(index); 36 | }); 37 | 38 | widget.onSelectImages?.call(_selectedImages); 39 | } 40 | 41 | @override 42 | Widget build(BuildContext context) => Column( 43 | mainAxisSize: MainAxisSize.min, 44 | crossAxisAlignment: CrossAxisAlignment.start, 45 | children: [ 46 | const SizedBox( 47 | height: 20, 48 | ), 49 | Text( 50 | 'Select Images', 51 | style: TextStyle( 52 | fontSize: 20, 53 | fontWeight: FontWeight.w700, 54 | ), 55 | ), 56 | const SizedBox( 57 | height: 20, 58 | ), 59 | Container( 60 | height: 150, 61 | padding: const EdgeInsets.all(10), 62 | decoration: BoxDecoration( 63 | border: Border.all( 64 | color: Colors.grey, 65 | ), 66 | borderRadius: BorderRadius.circular(10), 67 | ), 68 | child: ListView.separated( 69 | scrollDirection: Axis.horizontal, 70 | itemCount: _selectedImages.length + 1, 71 | itemBuilder: (_, index) => index < _selectedImages.length 72 | ? ImageWidget( 73 | path: _selectedImages[index], 74 | onDelete: () { 75 | _deleteImage(index); 76 | }, 77 | ) 78 | : InkWell( 79 | onTap: _addImage, 80 | child: Container( 81 | height: 120, 82 | width: 120, 83 | decoration: BoxDecoration( 84 | color: Colors.grey[300], 85 | borderRadius: BorderRadius.circular(10), 86 | ), 87 | child: Icon( 88 | Icons.add, 89 | color: Colors.grey, 90 | size: 50, 91 | ), 92 | ), 93 | ), 94 | separatorBuilder: (_, __) => const SizedBox( 95 | width: 10, 96 | ), 97 | ), 98 | ), 99 | ], 100 | ); 101 | } 102 | -------------------------------------------------------------------------------- /lib/src/presentation/ui/pages/article_create/widgets/widgets.dart: -------------------------------------------------------------------------------- 1 | export 'categories.dart'; 2 | export 'images.dart'; 3 | -------------------------------------------------------------------------------- /lib/src/presentation/ui/pages/articles/articles.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_bloc/flutter_bloc.dart'; 3 | 4 | import '../../../../application/articles/bloc.dart'; 5 | import '../../../../infrastructure/core/preferences.dart'; 6 | import '../../../../injection.dart'; 7 | import '../../../routes/routes.dart'; 8 | import 'widgets/widgets.dart'; 9 | 10 | class ArticlesPage extends StatefulWidget { 11 | @override 12 | _ArticlesPageState createState() => _ArticlesPageState(); 13 | } 14 | 15 | class _ArticlesPageState extends State 16 | with AutomaticKeepAliveClientMixin { 17 | final _bloc = getIt(); 18 | 19 | final _preferences = getIt(); 20 | 21 | void _loadData() { 22 | _bloc 23 | ..getCategories() 24 | ..getArticles(); 25 | } 26 | 27 | @override 28 | void initState() { 29 | super.initState(); 30 | 31 | _loadData(); 32 | } 33 | 34 | @override 35 | Widget build(BuildContext context) { 36 | super.build(context); 37 | return Scaffold( 38 | body: SafeArea( 39 | child: RefreshIndicator( 40 | onRefresh: () { 41 | _loadData(); 42 | return Future.value(); 43 | }, 44 | child: BlocProvider( 45 | create: (_) => _bloc, 46 | child: CustomScrollView( 47 | slivers: [ 48 | SliverPadding( 49 | padding: const EdgeInsets.only( 50 | top: 30, 51 | left: 20, 52 | right: 20, 53 | bottom: 50, 54 | ), 55 | sliver: SliverToBoxAdapter( 56 | child: Row( 57 | mainAxisAlignment: MainAxisAlignment.spaceBetween, 58 | children: [ 59 | Expanded( 60 | child: Text( 61 | 'Hi There 👋 📰', 62 | style: TextStyle( 63 | color: Colors.black, 64 | fontSize: 30, 65 | fontWeight: FontWeight.w900, 66 | ), 67 | ), 68 | ), 69 | const SizedBox( 70 | width: 5, 71 | ), 72 | if (_preferences.isLoggedIn) 73 | SizedBox( 74 | height: 50, 75 | width: 50, 76 | child: CircleAvatar( 77 | backgroundColor: Colors.grey[200], 78 | child: IconButton( 79 | tooltip: 'Add Article', 80 | onPressed: () { 81 | Navigator.of(context).pushNamed( 82 | AppRoutes.articleCreate, 83 | ); 84 | }, 85 | icon: Icon(Icons.post_add), 86 | ), 87 | ), 88 | ), 89 | ], 90 | ), 91 | ), 92 | ), 93 | SliverPadding( 94 | padding: const EdgeInsets.only(bottom: 10), 95 | sliver: SliverToBoxAdapter( 96 | child: CategoriesWidget( 97 | onSelectCategory: (category) { 98 | if (category != null) 99 | _bloc.getArticlesByCategory(category.id); 100 | else 101 | _bloc.getArticles(); 102 | }, 103 | ), 104 | ), 105 | ), 106 | SliverPadding( 107 | padding: const EdgeInsets.only(bottom: 20), 108 | sliver: ArticlesWidget(), 109 | ), 110 | SliverToBoxAdapter( 111 | child: const SizedBox( 112 | height: 100, 113 | ), 114 | ), 115 | ], 116 | ), 117 | ), 118 | ), 119 | ), 120 | ); 121 | } 122 | 123 | @override 124 | bool get wantKeepAlive => true; 125 | } 126 | -------------------------------------------------------------------------------- /lib/src/presentation/ui/pages/articles/widgets/articles.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_bloc/flutter_bloc.dart'; 3 | 4 | import '../../../../../application/articles/bloc.dart'; 5 | import '../../../components/article_item.dart'; 6 | import '../../../components/loading.dart'; 7 | 8 | class ArticlesWidget extends StatefulWidget { 9 | @override 10 | _ArticlesWidgetState createState() => _ArticlesWidgetState(); 11 | } 12 | 13 | class _ArticlesWidgetState extends State { 14 | @override 15 | Widget build(BuildContext context) => 16 | BlocBuilder( 17 | builder: (context, state) => state.articlesState.fold( 18 | () => SliverToBoxAdapter( 19 | child: LoadingWidget(), 20 | ), 21 | (either) => either.fold( 22 | (apiError) => SliverToBoxAdapter( 23 | child: Text(apiError.message ?? 'Unknown Error!'), 24 | ), 25 | (result) => SliverList( 26 | delegate: SliverChildBuilderDelegate( 27 | (_, index) => ArticleItemWidget( 28 | article: result.data[index], 29 | ), 30 | childCount: result.data.length, 31 | ), 32 | ), 33 | ), 34 | ), 35 | ); 36 | } 37 | -------------------------------------------------------------------------------- /lib/src/presentation/ui/pages/articles/widgets/categories.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_bloc/flutter_bloc.dart'; 3 | 4 | import '../../../../../application/articles/bloc.dart'; 5 | import '../../../../../domain/entities/category/category.dart'; 6 | import '../../../components/loading.dart'; 7 | 8 | class CategoriesWidget extends StatefulWidget { 9 | final Function(Category) onSelectCategory; 10 | 11 | const CategoriesWidget({Key key, this.onSelectCategory}) : super(key: key); 12 | 13 | @override 14 | _CategoriesWidgetState createState() => _CategoriesWidgetState(); 15 | } 16 | 17 | class _CategoriesWidgetState extends State { 18 | Category _selectedCategory; 19 | 20 | void _selectCategory(Category category) { 21 | setState(() { 22 | if (_selectedCategory?.id == category?.id) 23 | _selectedCategory = null; 24 | else 25 | _selectedCategory = category; 26 | }); 27 | 28 | widget.onSelectCategory?.call(_selectedCategory); 29 | } 30 | 31 | @override 32 | Widget build(BuildContext context) => 33 | BlocBuilder( 34 | builder: (context, state) => state.categoriesState.fold( 35 | () { 36 | _selectedCategory = null; 37 | return LoadingWidget(); 38 | }, 39 | (either) => either.fold( 40 | (apiError) => Text(apiError.message ?? 'Unknown Error!'), 41 | (result) => Column( 42 | crossAxisAlignment: CrossAxisAlignment.start, 43 | children: [ 44 | Padding( 45 | padding: const EdgeInsets.only( 46 | left: 20, 47 | right: 20, 48 | bottom: 10, 49 | ), 50 | child: Text( 51 | 'Categories', 52 | style: TextStyle( 53 | fontSize: 20, 54 | fontWeight: FontWeight.w700, 55 | ), 56 | ), 57 | ), 58 | SizedBox( 59 | height: 50, 60 | child: ListView.separated( 61 | itemCount: result.data.length, 62 | scrollDirection: Axis.horizontal, 63 | padding: const EdgeInsets.symmetric(horizontal: 20), 64 | itemBuilder: (_, index) { 65 | bool isSelected = 66 | _selectedCategory?.id == result.data[index].id; 67 | return InkWell( 68 | onTap: () { 69 | _selectCategory(result.data[index]); 70 | }, 71 | child: Chip( 72 | backgroundColor: isSelected 73 | ? Theme.of(context).primaryColor 74 | : null, 75 | label: Text( 76 | result.data[index].name, 77 | style: TextStyle( 78 | color: isSelected ? Colors.white : null, 79 | ), 80 | ), 81 | ), 82 | ); 83 | }, 84 | separatorBuilder: (_, __) => const SizedBox( 85 | width: 10, 86 | ), 87 | ), 88 | ), 89 | ], 90 | ), 91 | ), 92 | ), 93 | ); 94 | } 95 | -------------------------------------------------------------------------------- /lib/src/presentation/ui/pages/articles/widgets/widgets.dart: -------------------------------------------------------------------------------- 1 | export 'articles.dart'; 2 | export 'categories.dart'; 3 | -------------------------------------------------------------------------------- /lib/src/presentation/ui/pages/favorites/favorites.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_bloc/flutter_bloc.dart'; 3 | 4 | import '../../../../application/favorites/bloc.dart'; 5 | import '../../../../infrastructure/core/preferences.dart'; 6 | import '../../../../injection.dart'; 7 | import '../../components/article_item.dart'; 8 | import '../../components/loading.dart'; 9 | import '../../components/login.dart'; 10 | 11 | class FavoritesPage extends StatefulWidget { 12 | @override 13 | _FavoritesPageState createState() => _FavoritesPageState(); 14 | } 15 | 16 | class _FavoritesPageState extends State { 17 | final _bloc = getIt(); 18 | 19 | final _preferences = getIt(); 20 | 21 | String _userId; 22 | 23 | Future _loadData() async { 24 | _bloc.getFavorites(_userId); 25 | return Future.value(); 26 | } 27 | 28 | @override 29 | void initState() { 30 | super.initState(); 31 | 32 | if (_preferences.isLoggedIn) { 33 | _userId = _preferences.userId; 34 | _loadData(); 35 | } 36 | } 37 | 38 | @override 39 | Widget build(BuildContext context) => SafeArea( 40 | child: _preferences.isLoggedIn 41 | ? RefreshIndicator( 42 | onRefresh: _loadData, 43 | child: BlocBuilder( 44 | cubit: _bloc, 45 | builder: (_, state) => state.favoritesState.fold( 46 | () => LoadingWidget(), 47 | (either) => either.fold( 48 | (apiError) => Text(apiError.message ?? 'Unknown error!'), 49 | (result) => ListView.builder( 50 | padding: const EdgeInsets.only( 51 | top: 20, 52 | bottom: 100, 53 | ), 54 | itemCount: result.data.length, 55 | itemBuilder: (_, index) => ArticleItemWidget( 56 | article: result.data[index].article, 57 | ), 58 | ), 59 | ), 60 | ), 61 | ), 62 | ) 63 | : AskLoginWidget(), 64 | ); 65 | } 66 | -------------------------------------------------------------------------------- /lib/src/presentation/ui/pages/home/home.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import '../articles/articles.dart'; 4 | import '../favorites/favorites.dart'; 5 | import '../profile/profile.dart'; 6 | import '../search/search.dart'; 7 | import 'widgets/widgets.dart'; 8 | 9 | class HomePage extends StatefulWidget { 10 | @override 11 | _HomePageState createState() => _HomePageState(); 12 | } 13 | 14 | class _HomePageState extends State 15 | with AutomaticKeepAliveClientMixin { 16 | final _controller = PageController(); 17 | 18 | final _indexNotifier = ValueNotifier(0); 19 | 20 | List _screens = [ 21 | ArticlesPage(), 22 | SearchPage(), 23 | FavoritesPage(), 24 | ProfilePage(), 25 | ]; 26 | 27 | @override 28 | void initState() { 29 | super.initState(); 30 | _indexNotifier.addListener(() { 31 | _controller.jumpToPage(_indexNotifier.value); 32 | }); 33 | } 34 | 35 | @override 36 | Widget build(BuildContext context) { 37 | super.build(context); 38 | return Scaffold( 39 | body: Stack( 40 | children: [ 41 | Positioned.fill( 42 | child: PageView( 43 | controller: _controller, 44 | onPageChanged: (index) { 45 | _indexNotifier.value = index; 46 | }, 47 | children: _screens, 48 | ), 49 | ), 50 | Positioned( 51 | bottom: 0, 52 | left: 0, 53 | right: 0, 54 | child: BottomNavigationWidget( 55 | indexNotifier: _indexNotifier, 56 | ), 57 | ), 58 | ], 59 | ), 60 | ); 61 | } 62 | 63 | @override 64 | bool get wantKeepAlive => true; 65 | } 66 | -------------------------------------------------------------------------------- /lib/src/presentation/ui/pages/home/widgets/bottom_tab_bar.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:google_nav_bar/google_nav_bar.dart'; 3 | 4 | class BottomNavigationWidget extends StatelessWidget { 5 | final ValueNotifier indexNotifier; 6 | 7 | BottomNavigationWidget({Key key, @required this.indexNotifier}) 8 | : super(key: key); 9 | 10 | final List _tabs = [ 11 | _buildTab(title: 'Articles', icon: Icons.article_outlined), 12 | _buildTab(title: 'Search', icon: Icons.search), 13 | _buildTab(title: 'Favorite', icon: Icons.favorite_border), 14 | _buildTab(title: 'Profile', icon: Icons.account_circle_outlined), 15 | ]; 16 | 17 | @override 18 | Widget build(BuildContext context) => ValueListenableBuilder( 19 | valueListenable: indexNotifier, 20 | builder: (_, index, __) => Card( 21 | elevation: 10, 22 | margin: const EdgeInsets.all(10), 23 | shape: RoundedRectangleBorder( 24 | borderRadius: BorderRadius.circular(20), 25 | ), 26 | child: GNav( 27 | gap: 10, 28 | iconSize: 20, 29 | selectedIndex: index, 30 | tabBackgroundColor: Colors.grey[200], 31 | activeColor: Theme.of(context).primaryColor, 32 | padding: const EdgeInsets.symmetric( 33 | horizontal: 20, 34 | vertical: 10, 35 | ), 36 | onTabChange: (index) { 37 | indexNotifier.value = index; 38 | }, 39 | tabs: _tabs, 40 | ), 41 | ), 42 | ); 43 | 44 | static GButton _buildTab({String title, IconData icon}) => GButton( 45 | text: title, 46 | icon: icon, 47 | borderRadius: BorderRadius.circular(12.5), 48 | margin: const EdgeInsets.all(10), 49 | padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 3), 50 | ); 51 | } 52 | -------------------------------------------------------------------------------- /lib/src/presentation/ui/pages/home/widgets/widgets.dart: -------------------------------------------------------------------------------- 1 | export 'bottom_tab_bar.dart'; 2 | -------------------------------------------------------------------------------- /lib/src/presentation/ui/pages/on_boarding/on_boarding.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import '../../../routes/routes.dart'; 4 | import '../../components/buttons/rounded_button.dart'; 5 | import '../../components/buttons/rounded_outline_button.dart'; 6 | import 'widgets/widgets.dart'; 7 | 8 | class OnBoardingPage extends StatefulWidget { 9 | @override 10 | _OnBoardingPageState createState() => _OnBoardingPageState(); 11 | } 12 | 13 | class _OnBoardingPageState extends State { 14 | final ValueNotifier indexNotifier = ValueNotifier(0); 15 | 16 | final List _pages = [ 17 | PageWidget( 18 | image: 'assets/images/space-man.svg', 19 | title: 'Let\'s Get Started', 20 | subtitle: 21 | 'Lorem Ipsum is simply dummy text of the printing and typesetting industry.', 22 | ), 23 | PageWidget( 24 | image: 'assets/images/woman-laugh.svg', 25 | title: 'Let\'s Get Started', 26 | subtitle: 27 | 'Lorem Ipsum is simply dummy text of the printing and typesetting industry.', 28 | ), 29 | PageWidget( 30 | image: 'assets/images/identity.svg', 31 | title: 'Let\'s Get Started', 32 | subtitle: 33 | 'Lorem Ipsum is simply dummy text of the printing and typesetting industry.', 34 | ), 35 | ]; 36 | 37 | @override 38 | Widget build(BuildContext context) => Scaffold( 39 | appBar: AppBar( 40 | elevation: 0, 41 | backgroundColor: Colors.transparent, 42 | ), 43 | body: Column( 44 | children: [ 45 | SizedBox( 46 | height: MediaQuery.of(context).size.height / 2, 47 | child: Column( 48 | children: [ 49 | Expanded( 50 | flex: 10, 51 | child: PageView( 52 | onPageChanged: (index) => indexNotifier.value = index, 53 | children: _pages, 54 | ), 55 | ), 56 | const Spacer(), 57 | IndicatorWidget( 58 | length: _pages.length, 59 | indexNotifier: indexNotifier, 60 | ), 61 | ], 62 | ), 63 | ), 64 | const Spacer(), 65 | SizedBox( 66 | width: MediaQuery.of(context).size.width * .7, 67 | child: Column( 68 | crossAxisAlignment: CrossAxisAlignment.stretch, 69 | children: [ 70 | AppRoundedButton( 71 | onPressed: () { 72 | Navigator.of(context).pushNamed(AppRoutes.signIn); 73 | }, 74 | title: 'Sign in', 75 | ), 76 | const SizedBox( 77 | height: 15, 78 | ), 79 | AppRoundedOutlineButton( 80 | onPressed: () { 81 | Navigator.of(context).pushNamed(AppRoutes.signUp); 82 | }, 83 | title: 'Sign up', 84 | ), 85 | ], 86 | ), 87 | ), 88 | const Spacer(), 89 | MaterialButton( 90 | onPressed: () { 91 | Navigator.of(context).pushNamedAndRemoveUntil( 92 | AppRoutes.home, 93 | ModalRoute.withName('/'), 94 | ); 95 | }, 96 | child: Text( 97 | 'Getting Started', 98 | style: Theme.of(context).textTheme.button.copyWith( 99 | fontSize: 16, 100 | fontWeight: FontWeight.bold, 101 | decoration: TextDecoration.underline, 102 | ), 103 | ), 104 | ), 105 | const Spacer(), 106 | ], 107 | ), 108 | ); 109 | } 110 | -------------------------------------------------------------------------------- /lib/src/presentation/ui/pages/on_boarding/widgets/dote.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class DoteWidget extends StatelessWidget { 4 | final bool isSelected; 5 | 6 | const DoteWidget({Key key, this.isSelected = false}) : super(key: key); 7 | 8 | @override 9 | Widget build(BuildContext context) { 10 | return Container( 11 | height: isSelected ? 7 : 5, 12 | width: isSelected ? 7 : 5, 13 | margin: const EdgeInsets.symmetric(horizontal: 5), 14 | decoration: BoxDecoration( 15 | color: isSelected ? Theme.of(context).primaryColor : Colors.grey, 16 | shape: BoxShape.circle, 17 | ), 18 | ); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /lib/src/presentation/ui/pages/on_boarding/widgets/indicator.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import 'dote.dart'; 4 | 5 | class IndicatorWidget extends StatelessWidget { 6 | final int length; 7 | final ValueNotifier indexNotifier; 8 | 9 | const IndicatorWidget({ 10 | Key key, 11 | @required this.length, 12 | @required this.indexNotifier, 13 | }) : super(key: key); 14 | 15 | @override 16 | Widget build(BuildContext context) => ValueListenableBuilder( 17 | valueListenable: indexNotifier, 18 | builder: (_, value, __) => Row( 19 | mainAxisAlignment: MainAxisAlignment.center, 20 | children: List.generate( 21 | length, 22 | (index) => DoteWidget( 23 | isSelected: value == index, 24 | ), 25 | ), 26 | ), 27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /lib/src/presentation/ui/pages/on_boarding/widgets/page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_svg/flutter_svg.dart'; 3 | 4 | class PageWidget extends StatelessWidget { 5 | final String image; 6 | final String title; 7 | final String subtitle; 8 | 9 | const PageWidget({ 10 | Key key, 11 | @required this.image, 12 | @required this.title, 13 | @required this.subtitle, 14 | }) : super(key: key); 15 | 16 | @override 17 | Widget build(BuildContext context) => Padding( 18 | padding: EdgeInsets.symmetric( 19 | horizontal: MediaQuery.of(context).size.width * .2, 20 | ), 21 | child: Column( 22 | children: [ 23 | Expanded( 24 | child: SvgPicture.asset( 25 | image, 26 | ), 27 | ), 28 | const SizedBox( 29 | height: 25, 30 | ), 31 | Text( 32 | title, 33 | style: TextStyle( 34 | color: Colors.black, 35 | fontSize: 25, 36 | fontWeight: FontWeight.w900, 37 | ), 38 | ), 39 | const SizedBox( 40 | height: 15, 41 | ), 42 | Text( 43 | subtitle, 44 | style: TextStyle( 45 | color: Colors.grey[900], 46 | fontSize: 16, 47 | fontWeight: FontWeight.w300, 48 | ), 49 | textAlign: TextAlign.center, 50 | ) 51 | ], 52 | ), 53 | ); 54 | } 55 | -------------------------------------------------------------------------------- /lib/src/presentation/ui/pages/on_boarding/widgets/widgets.dart: -------------------------------------------------------------------------------- 1 | export 'indicator.dart'; 2 | export 'page.dart'; 3 | -------------------------------------------------------------------------------- /lib/src/presentation/ui/pages/profile/profile.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_bloc/flutter_bloc.dart'; 3 | 4 | import '../../../../application/profile/bloc.dart'; 5 | import '../../../../infrastructure/core/preferences.dart'; 6 | import '../../../../injection.dart'; 7 | import '../../../routes/routes.dart'; 8 | import '../../../utils/extensions.dart'; 9 | import '../../components/buttons/rounded_button.dart'; 10 | import '../../components/dialogs/question.dart'; 11 | import '../../components/dialogs/waiting.dart'; 12 | import '../../components/loading.dart'; 13 | import '../../components/login.dart'; 14 | import '../../components/my_article_item.dart'; 15 | import 'widgets/widgets.dart'; 16 | 17 | class ProfilePage extends StatefulWidget { 18 | @override 19 | _ProfilePageState createState() => _ProfilePageState(); 20 | } 21 | 22 | class _ProfilePageState extends State 23 | with AutomaticKeepAliveClientMixin { 24 | final _bloc = getIt(); 25 | 26 | final _preferences = getIt(); 27 | 28 | String _userId; 29 | 30 | void _logout() async { 31 | context.showAppDialog( 32 | isDismissible: false, 33 | child: AppDialogWaiting(), 34 | ); 35 | await _preferences.clear(); 36 | Navigator.of(context).pushNamedAndRemoveUntil( 37 | AppRoutes.onBoarding, 38 | ModalRoute.withName('/'), 39 | ); 40 | } 41 | 42 | void _askLogout() { 43 | context.showAppDialog( 44 | child: AppDialogQuestion( 45 | title: 'Logout', 46 | question: 'Are you sure you want to leave us?', 47 | actions: [ 48 | MaterialButton( 49 | onPressed: () { 50 | Navigator.pop(context); 51 | }, 52 | child: Text('No'), 53 | ), 54 | AppRoundedButton( 55 | onPressed: () { 56 | Navigator.pop(context); 57 | _logout(); 58 | }, 59 | padding: const EdgeInsets.symmetric(horizontal: 20), 60 | radius: 5, 61 | elevation: 0, 62 | title: 'Yes', 63 | ), 64 | ], 65 | ), 66 | ); 67 | } 68 | 69 | void _loadData() { 70 | _bloc 71 | ..getUserInformation(_userId) 72 | ..getArticlesByUser(_userId); 73 | } 74 | 75 | @override 76 | void initState() { 77 | super.initState(); 78 | 79 | if (_preferences.isLoggedIn) { 80 | _userId = _preferences.userId; 81 | _loadData(); 82 | } 83 | } 84 | 85 | @override 86 | Widget build(BuildContext context) { 87 | super.build(context); 88 | return SafeArea( 89 | child: _preferences.isLoggedIn 90 | ? BlocBuilder( 91 | cubit: _bloc, 92 | builder: (_, state) => RefreshIndicator( 93 | onRefresh: () { 94 | _loadData(); 95 | return Future.value(); 96 | }, 97 | child: CustomScrollView( 98 | slivers: [ 99 | state.userInformationState.fold( 100 | () => SliverToBoxAdapter( 101 | child: LoadingWidget(), 102 | ), 103 | (either) => either.fold( 104 | (apiError) => SliverToBoxAdapter(), 105 | (result) => SliverPadding( 106 | padding: const EdgeInsets.only(bottom: 30), 107 | sliver: SliverToBoxAdapter( 108 | child: HeaderWidget( 109 | onLogout: _askLogout, 110 | user: result.data, 111 | ), 112 | ), 113 | ), 114 | ), 115 | ), 116 | SliverPadding( 117 | padding: const EdgeInsets.symmetric(horizontal: 20), 118 | sliver: SliverToBoxAdapter( 119 | child: Text( 120 | 'My Articles', 121 | style: TextStyle( 122 | fontSize: 20, 123 | fontWeight: FontWeight.w700, 124 | ), 125 | ), 126 | ), 127 | ), 128 | state.articlesState.fold( 129 | () => SliverToBoxAdapter( 130 | child: LoadingWidget(), 131 | ), 132 | (either) => either.fold( 133 | (apiError) => SliverToBoxAdapter(), 134 | (result) => SliverList( 135 | delegate: SliverChildBuilderDelegate( 136 | (_, index) => Padding( 137 | padding: const EdgeInsets.symmetric( 138 | horizontal: 20, 139 | ), 140 | child: MyArticleItemWidget( 141 | article: result.data[index], 142 | ), 143 | ), 144 | childCount: result.data.length, 145 | ), 146 | ), 147 | ), 148 | ), 149 | SliverPadding( 150 | padding: const EdgeInsets.only(bottom: 100), 151 | ), 152 | ], 153 | ), 154 | ), 155 | ) 156 | : AskLoginWidget(), 157 | ); 158 | } 159 | 160 | @override 161 | bool get wantKeepAlive => true; 162 | } 163 | -------------------------------------------------------------------------------- /lib/src/presentation/ui/pages/profile/widgets/header.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import '../../../../../domain/entities/user/user.dart'; 4 | 5 | class HeaderWidget extends StatelessWidget { 6 | final User user; 7 | final VoidCallback onLogout; 8 | 9 | const HeaderWidget({Key key, @required this.user, this.onLogout}) 10 | : super(key: key); 11 | 12 | @override 13 | Widget build(BuildContext context) => Container( 14 | padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 30), 15 | decoration: BoxDecoration( 16 | color: Theme.of(context).primaryColor, 17 | borderRadius: BorderRadius.vertical( 18 | bottom: Radius.circular(20), 19 | ), 20 | ), 21 | child: ListTile( 22 | onTap: () {}, 23 | contentPadding: const EdgeInsets.all(0), 24 | leading: Icon( 25 | Icons.account_circle, 26 | size: 60, 27 | color: Colors.white, 28 | ), 29 | title: Text( 30 | '${user.firstName} ${user.lastName}', 31 | maxLines: 1, 32 | overflow: TextOverflow.ellipsis, 33 | style: TextStyle( 34 | color: Colors.white, 35 | fontWeight: FontWeight.w700, 36 | ), 37 | ), 38 | subtitle: Text( 39 | user.email, 40 | maxLines: 1, 41 | overflow: TextOverflow.ellipsis, 42 | style: TextStyle( 43 | color: Colors.grey[300], 44 | ), 45 | ), 46 | trailing: IconButton( 47 | onPressed: onLogout, 48 | tooltip: 'Logout', 49 | color: Colors.white, 50 | icon: Icon(Icons.login), 51 | ), 52 | ), 53 | ); 54 | } 55 | -------------------------------------------------------------------------------- /lib/src/presentation/ui/pages/profile/widgets/widgets.dart: -------------------------------------------------------------------------------- 1 | export 'header.dart'; 2 | -------------------------------------------------------------------------------- /lib/src/presentation/ui/pages/search/search.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/cupertino.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter_bloc/flutter_bloc.dart'; 4 | 5 | import '../../../../application/search/bloc.dart'; 6 | import '../../../../injection.dart'; 7 | import '../../components/article_item.dart'; 8 | import '../../components/loading.dart'; 9 | import '../../components/text_fields/search_field.dart'; 10 | 11 | class SearchPage extends StatefulWidget { 12 | @override 13 | _SearchPageState createState() => _SearchPageState(); 14 | } 15 | 16 | class _SearchPageState extends State 17 | with AutomaticKeepAliveClientMixin { 18 | final _bloc = getIt(); 19 | 20 | @override 21 | Widget build(BuildContext context) { 22 | super.build(context); 23 | return SafeArea( 24 | child: CustomScrollView( 25 | slivers: [ 26 | SliverAppBar( 27 | pinned: true, 28 | expandedHeight: 180, 29 | shape: RoundedRectangleBorder( 30 | borderRadius: BorderRadius.vertical( 31 | bottom: Radius.circular(20), 32 | ), 33 | ), 34 | bottom: PreferredSize( 35 | preferredSize: const Size(double.infinity, 40), 36 | child: Container( 37 | margin: const EdgeInsets.all(20), 38 | padding: const EdgeInsets.symmetric(horizontal: 10), 39 | decoration: BoxDecoration( 40 | color: Colors.black.withOpacity(.2), 41 | borderRadius: BorderRadius.circular(25), 42 | ), 43 | child: SearchFieldWidget( 44 | onChanged: _bloc.searchArticles, 45 | autoFocus: false, 46 | ), 47 | ), 48 | ), 49 | leading: const SizedBox(), 50 | ), 51 | BlocBuilder( 52 | cubit: _bloc, 53 | builder: (_, state) => state.searchState.fold( 54 | () => state.isSearching 55 | ? SliverFillRemaining( 56 | child: LoadingWidget(), 57 | ) 58 | : SliverFillRemaining( 59 | child: const SizedBox(), 60 | ), 61 | (either) => either.fold( 62 | (apiError) => const SizedBox(), 63 | (result) => SliverList( 64 | delegate: SliverChildBuilderDelegate( 65 | (_, index) => ArticleItemWidget( 66 | article: result.data[index], 67 | ), 68 | childCount: result.data.length, 69 | ), 70 | ), 71 | ), 72 | ), 73 | ), 74 | SliverPadding( 75 | padding: const EdgeInsets.only(bottom: 100), 76 | ), 77 | ], 78 | ), 79 | ); 80 | } 81 | 82 | @override 83 | bool get wantKeepAlive => true; 84 | } 85 | -------------------------------------------------------------------------------- /lib/src/presentation/ui/pages/sign_in/sign_in.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/cupertino.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:fluttertoast/fluttertoast.dart'; 4 | 5 | import '../../../../application/sign_in/bloc.dart'; 6 | import '../../../../domain/entities/user/user.dart'; 7 | import '../../../../infrastructure/core/preferences.dart'; 8 | import '../../../../injection.dart'; 9 | import '../../../routes/routes.dart'; 10 | import '../../../utils/extensions.dart'; 11 | import '../../components/buttons/rounded_button.dart'; 12 | import '../../components/dialogs/waiting.dart'; 13 | import '../../components/text_fields/rounded_outline_text_field.dart'; 14 | 15 | class SignInPage extends StatefulWidget { 16 | @override 17 | _SignInPageState createState() => _SignInPageState(); 18 | } 19 | 20 | class _SignInPageState extends State { 21 | final _bloc = getIt(); 22 | 23 | final _preferences = getIt(); 24 | 25 | final _formKey = GlobalKey(); 26 | 27 | final _emailController = TextEditingController(); 28 | final _passwordController = TextEditingController(); 29 | 30 | void _saveUserData(User user) async { 31 | await _preferences.setString(AppPreferencesKeys.USER_ID, user.id); 32 | await _preferences.setString(AppPreferencesKeys.TOKEN, user.token); 33 | Navigator.of(context).pushNamedAndRemoveUntil( 34 | AppRoutes.home, 35 | ModalRoute.withName(''), 36 | ); 37 | } 38 | 39 | void _validForm() { 40 | bool isFormValid = _formKey.currentState.validate(); 41 | if (!isFormValid) return; 42 | final user = User( 43 | email: _emailController.text, 44 | password: _passwordController.text, 45 | ); 46 | _bloc.signIn(user); 47 | } 48 | 49 | @override 50 | void initState() { 51 | super.initState(); 52 | _bloc.listen( 53 | (state) { 54 | state.signInState.fold( 55 | () => context.showAppDialog( 56 | isDismissible: false, 57 | child: AppDialogWaiting(), 58 | ), 59 | (either) => either.fold( 60 | (apiError) { 61 | Navigator.pop(context); 62 | Fluttertoast.showToast(msg: apiError.message); 63 | }, 64 | (result) { 65 | _saveUserData(result.data); 66 | }, 67 | ), 68 | ); 69 | }, 70 | ); 71 | } 72 | 73 | @override 74 | Widget build(BuildContext context) => Scaffold( 75 | appBar: AppBar( 76 | elevation: 0, 77 | backgroundColor: Colors.transparent, 78 | leading: IconButton( 79 | onPressed: () { 80 | Navigator.pop(context); 81 | }, 82 | icon: Icon( 83 | Icons.arrow_back_ios, 84 | color: Colors.black, 85 | ), 86 | ), 87 | ), 88 | body: ListView( 89 | padding: const EdgeInsets.symmetric(horizontal: 20), 90 | children: [ 91 | const SizedBox( 92 | height: 50, 93 | ), 94 | const Text( 95 | 'Sign in', 96 | style: TextStyle( 97 | color: Colors.black, 98 | fontSize: 30, 99 | fontWeight: FontWeight.w900, 100 | ), 101 | ), 102 | SizedBox( 103 | height: MediaQuery.of(context).size.height * .1, 104 | ), 105 | Card( 106 | elevation: 10, 107 | shadowColor: Colors.black.withOpacity(.3), 108 | shape: RoundedRectangleBorder( 109 | borderRadius: BorderRadius.circular(9), 110 | ), 111 | margin: const EdgeInsets.all(0), 112 | child: Form( 113 | key: _formKey, 114 | child: Padding( 115 | padding: const EdgeInsets.all(20), 116 | child: Column( 117 | mainAxisSize: MainAxisSize.min, 118 | crossAxisAlignment: CrossAxisAlignment.stretch, 119 | children: [ 120 | AppRoundedOutlineTextFormField( 121 | controller: _emailController, 122 | validator: (value) => value.isValidEmail 123 | ? null 124 | : 'Please insert valid email!', 125 | hint: 'Email', 126 | keyboardType: TextInputType.emailAddress, 127 | textInputAction: TextInputAction.next, 128 | prefixIcon: Icons.email_outlined, 129 | ), 130 | const SizedBox( 131 | height: 10, 132 | ), 133 | AppRoundedOutlineTextFormField( 134 | controller: _passwordController, 135 | validator: (value) => value.isValidPassword 136 | ? null 137 | : 'Please insert valid password!', 138 | hint: 'Password', 139 | obscureText: true, 140 | prefixIcon: Icons.lock_outline, 141 | ), 142 | const SizedBox( 143 | height: 50, 144 | ), 145 | AppRoundedButton( 146 | onPressed: _validForm, 147 | title: 'Sign in', 148 | ), 149 | ], 150 | ), 151 | ), 152 | ), 153 | ), 154 | ], 155 | ), 156 | ); 157 | 158 | @override 159 | void dispose() { 160 | _emailController.dispose(); 161 | _passwordController.dispose(); 162 | super.dispose(); 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /lib/src/presentation/utils/extensions.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import 'validators.dart' as validators; 4 | 5 | extension StringExtensions on String { 6 | bool get isValidEmail => validators.isValidEmail(this); 7 | 8 | bool get isValidPassword => validators.isValidPassword(this); 9 | } 10 | 11 | extension BuildContextExtensions on BuildContext { 12 | Future showAppDialog({ 13 | @required Widget child, 14 | bool isDismissible = true, 15 | }) => 16 | showDialog( 17 | context: this, 18 | barrierDismissible: isDismissible, 19 | builder: (_) => WillPopScope( 20 | onWillPop: () async { 21 | return isDismissible; 22 | }, 23 | child: child, 24 | ), 25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /lib/src/presentation/utils/validators.dart: -------------------------------------------------------------------------------- 1 | const _pattern = 2 | r'^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$'; 3 | 4 | final _regex = RegExp(_pattern); 5 | bool isValidEmail(String value) => _regex.hasMatch(value); 6 | 7 | bool isValidPassword(String value) => value.length >= 5; 8 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: Blog 2 | description: A Flutter blog. 3 | 4 | publish_to: 'none' # Remove this line if you wish to publish to pub.dev 5 | 6 | version: 1.0.0+1 7 | 8 | environment: 9 | sdk: ">=2.7.0 <3.0.0" 10 | 11 | dependencies: 12 | flutter: 13 | sdk: flutter 14 | cached_network_image: ^2.5.0 15 | cupertino_icons: ^1.0.0 16 | dartz: ^0.9.2 17 | flutter_bloc: ^6.1.1 18 | flutter_svg: any 19 | fluttertoast: ^7.1.6 20 | freezed_annotation: ^0.12.0 21 | get_it: ^5.0.3 22 | google_nav_bar: ^3.1.0 23 | image_picker: ^0.6.7+21 24 | injectable: ^1.0.7 25 | intl: ^0.16.1 26 | logger: ^0.9.4 27 | retrofit: ^1.3.4+1 28 | shared_preferences: ^0.5.12+4 29 | smooth_page_indicator: ^0.2.0 30 | 31 | dev_dependencies: 32 | flutter_test: 33 | sdk: flutter 34 | build_runner: ^1.10.11 35 | freezed: ^0.12.6 36 | json_serializable: ^3.5.1 37 | injectable_generator: ^1.0.7 38 | mocktail: 39 | retrofit_generator: ^1.4.1+1 40 | 41 | # For information on the generic Dart part of this file, see the 42 | # following page: https://dart.dev/tools/pub/pubspec 43 | 44 | # The following section is specific to Flutter. 45 | flutter: 46 | 47 | # The following line ensures that the Material Icons font is 48 | # included with your application, so that you can use the icons in 49 | # the material Icons class. 50 | uses-material-design: true 51 | 52 | # To add assets to your application, add an assets section, like this: 53 | assets: 54 | - assets/images/ 55 | # - images/a_dot_ham.jpeg 56 | 57 | # An image asset can refer to one or more resolution-specific "variants", see 58 | # https://flutter.dev/assets-and-images/#resolution-aware. 59 | 60 | # For details regarding adding assets from package dependencies, see 61 | # https://flutter.dev/assets-and-images/#from-packages 62 | 63 | # To add custom fonts to your application, add a fonts section here, 64 | # in this "flutter" section. Each entry in this list should have a 65 | # "family" key with the font family name, and a "fonts" key with a 66 | # list giving the asset and other descriptors for the font. For 67 | # example: 68 | # fonts: 69 | # - family: Schyler 70 | # fonts: 71 | # - asset: fonts/Schyler-Regular.ttf 72 | # - asset: fonts/Schyler-Italic.ttf 73 | # style: italic 74 | # - family: Trajan Pro 75 | # fonts: 76 | # - asset: fonts/TrajanPro.ttf 77 | # - asset: fonts/TrajanPro_Bold.ttf 78 | # weight: 700 79 | # 80 | # For details regarding fonts from package dependencies, 81 | # see https://flutter.dev/custom-fonts/#from-packages 82 | -------------------------------------------------------------------------------- /screenshot/flutter_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GeekAbdelouahed/Flutter-Blog/1f1d9ea881992dc867fa79eed40c6496c405c2fa/screenshot/flutter_01.png -------------------------------------------------------------------------------- /screenshot/flutter_02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GeekAbdelouahed/Flutter-Blog/1f1d9ea881992dc867fa79eed40c6496c405c2fa/screenshot/flutter_02.png -------------------------------------------------------------------------------- /screenshot/flutter_03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GeekAbdelouahed/Flutter-Blog/1f1d9ea881992dc867fa79eed40c6496c405c2fa/screenshot/flutter_03.png -------------------------------------------------------------------------------- /screenshot/flutter_04.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GeekAbdelouahed/Flutter-Blog/1f1d9ea881992dc867fa79eed40c6496c405c2fa/screenshot/flutter_04.png -------------------------------------------------------------------------------- /screenshot/flutter_05.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GeekAbdelouahed/Flutter-Blog/1f1d9ea881992dc867fa79eed40c6496c405c2fa/screenshot/flutter_05.png -------------------------------------------------------------------------------- /screenshot/flutter_06.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GeekAbdelouahed/Flutter-Blog/1f1d9ea881992dc867fa79eed40c6496c405c2fa/screenshot/flutter_06.png -------------------------------------------------------------------------------- /screenshot/flutter_07.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GeekAbdelouahed/Flutter-Blog/1f1d9ea881992dc867fa79eed40c6496c405c2fa/screenshot/flutter_07.png -------------------------------------------------------------------------------- /screenshot/flutter_08.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GeekAbdelouahed/Flutter-Blog/1f1d9ea881992dc867fa79eed40c6496c405c2fa/screenshot/flutter_08.png -------------------------------------------------------------------------------- /test/extensions_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_test/flutter_test.dart'; 2 | import '../lib/src/presentation/utils/extensions.dart'; 3 | 4 | void main() { 5 | group('String extensions Test', () { 6 | test('Email without username test', () async { 7 | String email = '@gmail.com'; 8 | 9 | bool isValid = email.isValidEmail; 10 | 11 | expect(isValid, false); 12 | }); 13 | 14 | test('Email without symbole @ test', () async { 15 | String email = 'hello.gmail'; 16 | 17 | bool isValid = email.isValidEmail; 18 | 19 | expect(isValid, false); 20 | }); 21 | 22 | test('Email without mail server test', () async { 23 | String email = 'hello@.com'; 24 | 25 | bool isValid = email.isValidEmail; 26 | 27 | expect(isValid, false); 28 | }); 29 | 30 | test('Email without domain test', () async { 31 | String email = 'hello@gmail'; 32 | 33 | bool isValid = email.isValidEmail; 34 | 35 | expect(isValid, false); 36 | }); 37 | 38 | test('Valid email test', () async { 39 | String email = 'hello@gmail.com'; 40 | 41 | bool isValid = email.isValidEmail; 42 | 43 | expect(isValid, true); 44 | }); 45 | 46 | test('Not valid password test', () async { 47 | String email = '123'; 48 | 49 | bool isValid = email.isValidPassword; 50 | 51 | expect(isValid, false); 52 | }); 53 | 54 | test('Valid password test', () async { 55 | String email = 'helloworld'; 56 | 57 | bool isValid = email.isValidPassword; 58 | 59 | expect(isValid, true); 60 | }); 61 | }); 62 | } 63 | -------------------------------------------------------------------------------- /test/test.dart: -------------------------------------------------------------------------------- 1 | import 'package:dartz/dartz.dart'; 2 | import 'package:flutter_test/flutter_test.dart'; 3 | import 'package:mocktail/mocktail.dart'; 4 | 5 | import '../lib/src/domain/entities/api_response/api_response.dart'; 6 | import '../lib/src/domain/entities/article/article.dart'; 7 | import '../lib/src/domain/articles/i_articles_facade.dart'; 8 | 9 | class MockArticlesFacade extends Mock implements IArticlesFacade {} 10 | 11 | void main() { 12 | MockArticlesFacade articlesFacade = MockArticlesFacade(); 13 | 14 | group('Articles Test', () { 15 | test('Not empty articles', () async { 16 | when(() => articlesFacade.getArticles()).thenAnswer( 17 | ((_) async { 18 | return right( 19 | ApiResponse>( 20 | status: true, 21 | data:
[], 22 | ), 23 | ); 24 | }), 25 | ); 26 | 27 | final articles = await articlesFacade.getArticles(); 28 | 29 | expect(articles.getOrElse(() => null).data, isNotEmpty); 30 | }); 31 | }); 32 | } 33 | -------------------------------------------------------------------------------- /test/widget_test.dart: -------------------------------------------------------------------------------- 1 | // This is a basic Flutter widget test. 2 | // 3 | // To perform an interaction with a widget in your test, use the WidgetTester 4 | // utility that Flutter provides. For example, you can send tap and scroll 5 | // gestures. You can also use WidgetTester to find child widgets in the widget 6 | // tree, read text, and verify that the values of widget properties are correct. 7 | 8 | import 'package:flutter/material.dart'; 9 | import 'package:flutter_test/flutter_test.dart'; 10 | 11 | import 'package:Blog/main.dart'; 12 | 13 | void main() { 14 | testWidgets('Counter increments smoke test', (WidgetTester tester) async { 15 | // Build our app and trigger a frame. 16 | await tester.pumpWidget(MyApp()); 17 | 18 | // Verify that our counter starts at 0. 19 | expect(find.text('0'), findsOneWidget); 20 | expect(find.text('1'), findsNothing); 21 | 22 | // Tap the '+' icon and trigger a frame. 23 | await tester.tap(find.byIcon(Icons.add)); 24 | await tester.pump(); 25 | 26 | // Verify that our counter has incremented. 27 | expect(find.text('0'), findsNothing); 28 | expect(find.text('1'), findsOneWidget); 29 | }); 30 | } 31 | -------------------------------------------------------------------------------- /web/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GeekAbdelouahed/Flutter-Blog/1f1d9ea881992dc867fa79eed40c6496c405c2fa/web/favicon.png -------------------------------------------------------------------------------- /web/icons/Icon-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GeekAbdelouahed/Flutter-Blog/1f1d9ea881992dc867fa79eed40c6496c405c2fa/web/icons/Icon-192.png -------------------------------------------------------------------------------- /web/icons/Icon-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GeekAbdelouahed/Flutter-Blog/1f1d9ea881992dc867fa79eed40c6496c405c2fa/web/icons/Icon-512.png -------------------------------------------------------------------------------- /web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | Blog 18 | 19 | 20 | 21 | 24 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /web/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Blog", 3 | "short_name": "Blog", 4 | "start_url": ".", 5 | "display": "standalone", 6 | "background_color": "#0175C2", 7 | "theme_color": "#0175C2", 8 | "description": "A Flutter blog.", 9 | "orientation": "portrait-primary", 10 | "prefer_related_applications": false, 11 | "icons": [ 12 | { 13 | "src": "icons/Icon-192.png", 14 | "sizes": "192x192", 15 | "type": "image/png" 16 | }, 17 | { 18 | "src": "icons/Icon-512.png", 19 | "sizes": "512x512", 20 | "type": "image/png" 21 | } 22 | ] 23 | } 24 | --------------------------------------------------------------------------------