diff --git a/.fandogh/config.yml b/.fandogh/config.yml new file mode 100644 index 0000000..5231c24 --- /dev/null +++ b/.fandogh/config.yml @@ -0,0 +1 @@ +image.name: app-test diff --git a/.fandogh/fandogh.yaml b/.fandogh/fandogh.yaml new file mode 100644 index 0000000..05b34a4 --- /dev/null +++ b/.fandogh/fandogh.yaml @@ -0,0 +1,13 @@ +kind: ExternalService +name: app-test +spec: + allow_http: false + disable_default_domains: true + image: app-test:0.0.4 + image_pull_policy: IfNotPresent + path: / + replicas: 1 + resources: + memory: 50Mi + domains: + - name: dev.didvan.app \ No newline at end of file diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 3bca3c2..08c9397 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,35 +1,55 @@ -# This file is a template, and might need editing before it works on your project. -# To contribute improvements to CI/CD templates, please follow the Development guide at: -# https://docs.gitlab.com/ee/development/cicd/templates.html -# This specific template is located at: -# https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/ci/templates/Flutter.gitlab-ci.yml +cache: + paths: + - build/web -code_quality: - stage: test - image: "cirrusci/flutter:1.22.5" - before_script: - - pub global activate dart_code_metrics - - export PATH="$PATH":"$HOME/.pub-cache/bin" - script: - - metrics lib -r codeclimate > gl-code-quality-report.json - artifacts: - reports: - codequality: gl-code-quality-report.json +stages: + - build + - push + - deploy -test: - stage: test - image: "cirrusci/flutter:1.22.5" - before_script: - - pub global activate junitreport - - export PATH="$PATH":"$HOME/.pub-cache/bin" +build: + stage: build + image: cirrusci/flutter:latest script: - - flutter test --machine --coverage | tojunit -o report.xml - - lcov --summary coverage/lcov.info - - genhtml coverage/lcov.info --output=coverage - coverage: '/lines\.*: \d+\.\d+\%/' - artifacts: - name: coverage - paths: - - $CI_PROJECT_DIR/coverage - reports: - junit: report.xml + - flutter clean + - flutter build web --web-renderer html + only: + - test + - dev + +push: + stage: push + image: docker:latest + services: + - docker:dind + script: + - docker login -u $CI_DEPLOY_USER -p $CI_DEPLOY_PASSWORD $CI_REGISTRY + - docker build --pull -t registry.gitlab.com/didvan/didvan-app/api:latest . + - docker push registry.gitlab.com/didvan/didvan-app/api:latest + only: + - test + - dev + +deploy-test: + stage: deploy + image: python:3.10.2 + variables: + COLLECT_ERROR: 1 + script: + - pip install fandogh_cli --upgrade + - fandogh login --username=$FAN_USR --password=$FAN_PASS + - fandogh service apply -f ./deployment/test.yaml -p SEC_NAME + only: + - test + +deploy-dev: + stage: deploy + image: python:3.10.2 + variables: + COLLECT_ERROR: 1 + script: + - pip install fandogh_cli --upgrade + - fandogh login --username=$FAN_USR --password=$FAN_PASS + - fandogh service apply -f ./deployment/dev.yaml -p SEC_NAME + only: + - dev \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..f75af4b --- /dev/null +++ b/Dockerfile @@ -0,0 +1,3 @@ +FROM nginx:latest +COPY ./build/web /usr/share/nginx/html/ +ENTRYPOINT ["nginx", "-g", "daemon off;"] diff --git a/README.md b/README.md index ec1c2ed..7137d8f 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,3 @@ -# didvan +# Didvan -A new Flutter project. -## Getting Started - -This project is a starting point for a Flutter application. - -A few resources to get you started if this is your first Flutter project: - -- [Lab: Write your first Flutter app](https://flutter.dev/docs/get-started/codelab) -- [Cookbook: Useful Flutter samples](https://flutter.dev/docs/cookbook) - -For help getting started with Flutter, view our -[online documentation](https://flutter.dev/docs), which offers tutorials, -samples, guidance on mobile development, and a full API reference. diff --git a/android/app/build.gradle b/android/app/build.gradle index c424bad..efe0f78 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -26,7 +26,8 @@ apply plugin: 'kotlin-android' apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" android { - compileSdkVersion 30 + compileSdkVersion 31 + compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 @@ -43,8 +44,8 @@ android { defaultConfig { // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). - applicationId "com.example.didvan" - minSdkVersion 16 + applicationId "com.didvan.didvanapp" + minSdkVersion 21 targetSdkVersion 30 versionCode flutterVersionCode.toInteger() versionName flutterVersionName @@ -64,5 +65,6 @@ flutter { } dependencies { + implementation platform('com.google.firebase:firebase-bom:29.1.0') implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" } diff --git a/android/app/google-services.json b/android/app/google-services.json new file mode 100644 index 0000000..5cb370e --- /dev/null +++ b/android/app/google-services.json @@ -0,0 +1,39 @@ +{ + "project_info": { + "project_number": "935017686266", + "project_id": "didvan-9b7da", + "storage_bucket": "didvan-9b7da.appspot.com" + }, + "client": [ + { + "client_info": { + "mobilesdk_app_id": "1:935017686266:android:f9cbc9aba8e3d65ed2d543", + "android_client_info": { + "package_name": "com.didvan.didvanapp" + } + }, + "oauth_client": [ + { + "client_id": "935017686266-lebnol7rb05oi9h0mripb41c892d2gij.apps.googleusercontent.com", + "client_type": 3 + } + ], + "api_key": [ + { + "current_key": "AIzaSyBp-UHjWeM0H0UHtX5yguFKG-riMzvvCzw" + } + ], + "services": { + "appinvite_service": { + "other_platform_oauth_client": [ + { + "client_id": "935017686266-lebnol7rb05oi9h0mripb41c892d2gij.apps.googleusercontent.com", + "client_type": 3 + } + ] + } + } + } + ], + "configuration_version": "1" +} \ No newline at end of file diff --git a/android/app/src/debug/AndroidManifest.xml b/android/app/src/debug/AndroidManifest.xml index b4988dc..06824d8 100644 --- a/android/app/src/debug/AndroidManifest.xml +++ b/android/app/src/debug/AndroidManifest.xml @@ -1,5 +1,5 @@ + package="com.didvan.didvanapp"> diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 666f236..34d2e7f 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -1,9 +1,14 @@ + package="com.didvan.didvanapp"> + + + + android:label="Didvan" + android:icon="@mipmap/ic_launcher" + android:usesCleartextTraffic="true"> + - + - diff --git a/android/app/src/main/kotlin/com/example/didvan/MainActivity.kt b/android/app/src/main/kotlin/com/example/didvan/MainActivity.kt index 0f5fc61..c4aec60 100644 --- a/android/app/src/main/kotlin/com/example/didvan/MainActivity.kt +++ b/android/app/src/main/kotlin/com/example/didvan/MainActivity.kt @@ -1,4 +1,4 @@ -package com.example.didvan +package com.didvan.didvanapp import io.flutter.embedding.android.FlutterActivity diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png index db77bb4..549da66 100644 Binary files a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png index 17987b7..ad4a263 100644 Binary files a/android/app/src/main/res/mipmap-mdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png index 09d4391..48f0188 100644 Binary files a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png index d5f1c8d..966049e 100644 Binary files a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png index 4d6372e..d4f8ac8 100644 Binary files a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/android/app/src/profile/AndroidManifest.xml b/android/app/src/profile/AndroidManifest.xml index b4988dc..06824d8 100644 --- a/android/app/src/profile/AndroidManifest.xml +++ b/android/app/src/profile/AndroidManifest.xml @@ -1,5 +1,5 @@ + package="com.didvan.didvanapp"> diff --git a/android/build.gradle b/android/build.gradle index ed45c65..125594d 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -1,5 +1,5 @@ buildscript { - ext.kotlin_version = '1.3.50' + ext.kotlin_version = '1.6.10' repositories { google() mavenCentral() @@ -7,6 +7,7 @@ buildscript { dependencies { classpath 'com.android.tools.build:gradle:4.1.0' + classpath 'com.google.gms:google-services:4.3.10' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties index bc6a58a..cc5527d 100644 --- a/android/gradle/wrapper/gradle-wrapper.properties +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-all.zip diff --git a/deployment/dev.yaml b/deployment/dev.yaml new file mode 100644 index 0000000..6738cdd --- /dev/null +++ b/deployment/dev.yaml @@ -0,0 +1,14 @@ +kind: ExternalService +name: app-dev +spec: + allow_http: false + disable_default_domains: true + image: registry.gitlab.com/didvan/didvan-app/api:latest + image_pull_policy: Always + image_pull_secret: $SEC_NAME + path: / + replicas: 1 + resources: + memory: 100Mi + domains: + - name: dev.didvan.app \ No newline at end of file diff --git a/deployment/test.yaml b/deployment/test.yaml new file mode 100644 index 0000000..c49f51c --- /dev/null +++ b/deployment/test.yaml @@ -0,0 +1,14 @@ +kind: ExternalService +name: app-test +spec: + allow_http: false + disable_default_domains: true + image: registry.gitlab.com/didvan/didvan-app/api:latest + image_pull_policy: Always + image_pull_secret: $SEC_NAME + path: / + replicas: 1 + resources: + memory: 150Mi + domains: + - name: test.didvan.app \ No newline at end of file diff --git a/ios/Flutter/Debug.xcconfig b/ios/Flutter/Debug.xcconfig index 592ceee..ec97fc6 100644 --- a/ios/Flutter/Debug.xcconfig +++ b/ios/Flutter/Debug.xcconfig @@ -1 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" #include "Generated.xcconfig" diff --git a/ios/Flutter/Release.xcconfig b/ios/Flutter/Release.xcconfig index 592ceee..c4855bf 100644 --- a/ios/Flutter/Release.xcconfig +++ b/ios/Flutter/Release.xcconfig @@ -1 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" #include "Generated.xcconfig" diff --git a/ios/Podfile b/ios/Podfile new file mode 100644 index 0000000..9411102 --- /dev/null +++ b/ios/Podfile @@ -0,0 +1,41 @@ +# Uncomment this line to define a global platform for your project +platform :ios, '10.0' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_ios_podfile_setup + +target 'Runner' do + use_frameworks! + use_modular_headers! + + flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_ios_build_settings(target) + end +end diff --git a/ios/Podfile.lock b/ios/Podfile.lock new file mode 100644 index 0000000..55fa4cf --- /dev/null +++ b/ios/Podfile.lock @@ -0,0 +1,177 @@ +PODS: + - audio_session (0.0.1): + - Flutter + - Firebase/CoreOnly (8.11.0): + - FirebaseCore (= 8.11.0) + - Firebase/Messaging (8.11.0): + - Firebase/CoreOnly + - FirebaseMessaging (~> 8.11.0) + - firebase_core (1.13.1): + - Firebase/CoreOnly (= 8.11.0) + - Flutter + - firebase_messaging (11.2.8): + - Firebase/Messaging (= 8.11.0) + - firebase_core + - Flutter + - FirebaseCore (8.11.0): + - FirebaseCoreDiagnostics (~> 8.0) + - GoogleUtilities/Environment (~> 7.7) + - GoogleUtilities/Logger (~> 7.7) + - FirebaseCoreDiagnostics (8.12.0): + - GoogleDataTransport (~> 9.1) + - GoogleUtilities/Environment (~> 7.7) + - GoogleUtilities/Logger (~> 7.7) + - nanopb (~> 2.30908.0) + - FirebaseInstallations (8.12.0): + - FirebaseCore (~> 8.0) + - GoogleUtilities/Environment (~> 7.7) + - GoogleUtilities/UserDefaults (~> 7.7) + - PromisesObjC (< 3.0, >= 1.2) + - FirebaseMessaging (8.11.0): + - FirebaseCore (~> 8.0) + - FirebaseInstallations (~> 8.0) + - GoogleDataTransport (~> 9.1) + - GoogleUtilities/AppDelegateSwizzler (~> 7.7) + - GoogleUtilities/Environment (~> 7.7) + - GoogleUtilities/Reachability (~> 7.7) + - GoogleUtilities/UserDefaults (~> 7.7) + - nanopb (~> 2.30908.0) + - Flutter (1.0.0) + - flutter_secure_storage (3.3.1): + - Flutter + - flutter_vibrate (0.0.1): + - Flutter + - FMDB (2.7.5): + - FMDB/standard (= 2.7.5) + - FMDB/standard (2.7.5) + - GoogleDataTransport (9.1.2): + - GoogleUtilities/Environment (~> 7.2) + - nanopb (~> 2.30908.0) + - PromisesObjC (< 3.0, >= 1.2) + - GoogleUtilities/AppDelegateSwizzler (7.7.0): + - GoogleUtilities/Environment + - GoogleUtilities/Logger + - GoogleUtilities/Network + - GoogleUtilities/Environment (7.7.0): + - PromisesObjC (< 3.0, >= 1.2) + - GoogleUtilities/Logger (7.7.0): + - GoogleUtilities/Environment + - GoogleUtilities/Network (7.7.0): + - GoogleUtilities/Logger + - "GoogleUtilities/NSData+zlib" + - GoogleUtilities/Reachability + - "GoogleUtilities/NSData+zlib (7.7.0)" + - GoogleUtilities/Reachability (7.7.0): + - GoogleUtilities/Logger + - GoogleUtilities/UserDefaults (7.7.0): + - GoogleUtilities/Logger + - image_cropper (0.0.4): + - Flutter + - TOCropViewController (~> 2.6.1) + - image_picker (0.0.1): + - Flutter + - just_audio (0.0.1): + - Flutter + - nanopb (2.30908.0): + - nanopb/decode (= 2.30908.0) + - nanopb/encode (= 2.30908.0) + - nanopb/decode (2.30908.0) + - nanopb/encode (2.30908.0) + - path_provider_ios (0.0.1): + - Flutter + - PromisesObjC (2.0.0) + - record (0.0.1): + - Flutter + - sqflite (0.0.2): + - Flutter + - FMDB (>= 2.7.5) + - TOCropViewController (2.6.1) + - url_launcher_ios (0.0.1): + - Flutter + +DEPENDENCIES: + - audio_session (from `.symlinks/plugins/audio_session/ios`) + - firebase_core (from `.symlinks/plugins/firebase_core/ios`) + - firebase_messaging (from `.symlinks/plugins/firebase_messaging/ios`) + - Flutter (from `Flutter`) + - flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`) + - flutter_vibrate (from `.symlinks/plugins/flutter_vibrate/ios`) + - image_cropper (from `.symlinks/plugins/image_cropper/ios`) + - image_picker (from `.symlinks/plugins/image_picker/ios`) + - just_audio (from `.symlinks/plugins/just_audio/ios`) + - path_provider_ios (from `.symlinks/plugins/path_provider_ios/ios`) + - record (from `.symlinks/plugins/record/ios`) + - sqflite (from `.symlinks/plugins/sqflite/ios`) + - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) + +SPEC REPOS: + trunk: + - Firebase + - FirebaseCore + - FirebaseCoreDiagnostics + - FirebaseInstallations + - FirebaseMessaging + - FMDB + - GoogleDataTransport + - GoogleUtilities + - nanopb + - PromisesObjC + - TOCropViewController + +EXTERNAL SOURCES: + audio_session: + :path: ".symlinks/plugins/audio_session/ios" + firebase_core: + :path: ".symlinks/plugins/firebase_core/ios" + firebase_messaging: + :path: ".symlinks/plugins/firebase_messaging/ios" + Flutter: + :path: Flutter + flutter_secure_storage: + :path: ".symlinks/plugins/flutter_secure_storage/ios" + flutter_vibrate: + :path: ".symlinks/plugins/flutter_vibrate/ios" + image_cropper: + :path: ".symlinks/plugins/image_cropper/ios" + image_picker: + :path: ".symlinks/plugins/image_picker/ios" + just_audio: + :path: ".symlinks/plugins/just_audio/ios" + path_provider_ios: + :path: ".symlinks/plugins/path_provider_ios/ios" + record: + :path: ".symlinks/plugins/record/ios" + sqflite: + :path: ".symlinks/plugins/sqflite/ios" + url_launcher_ios: + :path: ".symlinks/plugins/url_launcher_ios/ios" + +SPEC CHECKSUMS: + audio_session: 4f3e461722055d21515cf3261b64c973c062f345 + Firebase: 44dd9724c84df18b486639e874f31436eaa9a20c + firebase_core: 08f6a85f62060111de5e98d6a214810d11365de9 + firebase_messaging: 36238f3d0b933af8c919aef608408aae06ba22e8 + FirebaseCore: 2f4f85b453cc8fea4bb2b37e370007d2bcafe3f0 + FirebaseCoreDiagnostics: 3b40dfadef5b90433a60ae01f01e90fe87aa76aa + FirebaseInstallations: 25764cf322e77f99449395870a65b2bef88e1545 + FirebaseMessaging: 02e248e8997f71fa8cc9d78e9d49ec1a701ba14a + Flutter: 50d75fe2f02b26cc09d224853bb45737f8b3214a + flutter_secure_storage: 7953c38a04c3fdbb00571bcd87d8e3b5ceb9daec + flutter_vibrate: 9f4c2ab57008965f78969472367c329dd77eb801 + FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a + GoogleDataTransport: 629c20a4d363167143f30ea78320d5a7eb8bd940 + GoogleUtilities: e0913149f6b0625b553d70dae12b49fc62914fd1 + image_cropper: 60c2789d1f1a78c873235d4319ca0c34a69f2d98 + image_picker: 9aa50e1d8cdacdbed739e925b7eea16d014367e6 + just_audio: baa7252489dbcf47a4c7cc9ca663e9661c99aafa + nanopb: a0ba3315591a9ae0a16a309ee504766e90db0c96 + path_provider_ios: 7d7ce634493af4477d156294792024ec3485acd5 + PromisesObjC: 68159ce6952d93e17b2dfe273b8c40907db5ba58 + record: 7ee2393532f8553bbb09fa19e95478323b7c0a99 + sqflite: 6d358c025f5b867b29ed92fc697fd34924e11904 + TOCropViewController: edfd4f25713d56905ad1e0b9f5be3fbe0f59c863 + url_launcher_ios: 02f1989d4e14e998335b02b67a7590fa34f971af + +PODFILE CHECKSUM: fe0e1ee7f3d1f7d00b11b474b62dd62134535aea + +COCOAPODS: 1.11.2 diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 16cc79e..f2eb3d9 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 46; + objectVersion = 51; objects = { /* Begin PBXBuildFile section */ @@ -13,6 +13,8 @@ 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; + D194CE3E27D4A4740049AFC7 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = D194CE3D27D4A4740049AFC7 /* GoogleService-Info.plist */; }; + E870A5F479A60D6704DD5DF2 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 75DBECA488F412614712FB74 /* Pods_Runner.framework */; }; /* End PBXBuildFile section */ /* Begin PBXCopyFilesBuildPhase section */ @@ -31,9 +33,11 @@ /* Begin PBXFileReference section */ 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 1CDA6531AC975E620DBA1134 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 75DBECA488F412614712FB74 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; @@ -42,6 +46,9 @@ 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 98ACB01D5FA5A78DB2686183 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; + C97DED20C4A171F16FB949CD /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + D194CE3D27D4A4740049AFC7 /* GoogleService-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -49,12 +56,31 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + E870A5F479A60D6704DD5DF2 /* Pods_Runner.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 650C3E9A238A953E4E8E6AED /* Frameworks */ = { + isa = PBXGroup; + children = ( + 75DBECA488F412614712FB74 /* Pods_Runner.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + 8170350417391B7E9B77985B /* Pods */ = { + isa = PBXGroup; + children = ( + C97DED20C4A171F16FB949CD /* Pods-Runner.debug.xcconfig */, + 98ACB01D5FA5A78DB2686183 /* Pods-Runner.release.xcconfig */, + 1CDA6531AC975E620DBA1134 /* Pods-Runner.profile.xcconfig */, + ); + path = Pods; + sourceTree = ""; + }; 9740EEB11CF90186004384FC /* Flutter */ = { isa = PBXGroup; children = ( @@ -72,6 +98,8 @@ 9740EEB11CF90186004384FC /* Flutter */, 97C146F01CF9000F007C117D /* Runner */, 97C146EF1CF9000F007C117D /* Products */, + 8170350417391B7E9B77985B /* Pods */, + 650C3E9A238A953E4E8E6AED /* Frameworks */, ); sourceTree = ""; }; @@ -86,6 +114,7 @@ 97C146F01CF9000F007C117D /* Runner */ = { isa = PBXGroup; children = ( + D194CE3D27D4A4740049AFC7 /* GoogleService-Info.plist */, 97C146FA1CF9000F007C117D /* Main.storyboard */, 97C146FD1CF9000F007C117D /* Assets.xcassets */, 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, @@ -105,12 +134,14 @@ isa = PBXNativeTarget; buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( + 9F9C99F59A6A1134B656560D /* [CP] Check Pods Manifest.lock */, 9740EEB61CF901F6004384FC /* Run Script */, 97C146EA1CF9000F007C117D /* Sources */, 97C146EB1CF9000F007C117D /* Frameworks */, 97C146EC1CF9000F007C117D /* Resources */, 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + AE20B55CEF8506DDEEBE3543 /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -127,7 +158,7 @@ 97C146E61CF9000F007C117D /* Project object */ = { isa = PBXProject; attributes = { - LastUpgradeCheck = 1020; + LastUpgradeCheck = 1300; ORGANIZATIONNAME = ""; TargetAttributes = { 97C146ED1CF9000F007C117D = { @@ -161,6 +192,7 @@ files = ( 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, + D194CE3E27D4A4740049AFC7 /* GoogleService-Info.plist in Resources */, 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, ); @@ -197,6 +229,45 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; }; + 9F9C99F59A6A1134B656560D /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + AE20B55CEF8506DDEEBE3543 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ @@ -287,12 +358,19 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = W2PAW454F9; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; - PRODUCT_BUNDLE_IDENTIFIER = com.example.didvan; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.didvan.didvanapp; PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; VERSIONING_SYSTEM = "apple-generic"; @@ -411,12 +489,19 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = W2PAW454F9; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; - PRODUCT_BUNDLE_IDENTIFIER = com.example.didvan; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.didvan.didvanapp; PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; @@ -430,12 +515,19 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = W2PAW454F9; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; - PRODUCT_BUNDLE_IDENTIFIER = com.example.didvan; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.didvan.didvanapp; PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; VERSIONING_SYSTEM = "apple-generic"; diff --git a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index a28140c..3db53b6 100644 --- a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ + + diff --git a/ios/Runner/AppDelegate.swift b/ios/Runner/AppDelegate.swift index 70693e4..507864d 100644 --- a/ios/Runner/AppDelegate.swift +++ b/ios/Runner/AppDelegate.swift @@ -1,13 +1,30 @@ import UIKit import Flutter +import Firebase + @UIApplicationMain -@objc class AppDelegate: FlutterAppDelegate { +@objc class AppDelegate: FlutterAppDelegate, MessagingDelegate { override func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? ) -> Bool { + FirebaseApp.configure() + Messaging.messaging().delegate = self GeneratedPluginRegistrant.register(with: self) + if #available(iOS 10.0, *) { + // For iOS 10 display notification (sent via APNS) + UNUserNotificationCenter.current().delegate = self + let authOptions: UNAuthorizationOptions = [.alert, .badge, .sound] + UNUserNotificationCenter.current().requestAuthorization( + options: authOptions, + completionHandler: {_, _ in }) + } else { + let settings: UIUserNotificationSettings = + UIUserNotificationSettings(types: [.alert, .badge, .sound], categories: nil) + application.registerUserNotificationSettings(settings) + } + application.registerForRemoteNotifications() return super.application(application, didFinishLaunchingWithOptions: launchOptions) } -} +} \ No newline at end of file diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/100.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/100.png new file mode 100644 index 0000000..c89e7ed Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/100.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/1024.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/1024.png new file mode 100644 index 0000000..55e86ba Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/1024.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/114.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/114.png new file mode 100644 index 0000000..0a984f2 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/114.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/120.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/120.png new file mode 100644 index 0000000..54ff853 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/120.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/128.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/128.png new file mode 100644 index 0000000..d525986 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/128.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/144.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/144.png new file mode 100644 index 0000000..f00cef9 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/144.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/152.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/152.png new file mode 100644 index 0000000..1cebf8d Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/152.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/16.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/16.png new file mode 100644 index 0000000..eb8dc68 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/16.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/167.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/167.png new file mode 100644 index 0000000..2692e97 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/167.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/172.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/172.png new file mode 100644 index 0000000..8836e48 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/172.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/180.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/180.png new file mode 100644 index 0000000..0d0a595 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/180.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/196.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/196.png new file mode 100644 index 0000000..f7aee80 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/196.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/20.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/20.png new file mode 100644 index 0000000..05a2b2c Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/20.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/216.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/216.png new file mode 100644 index 0000000..e60aeea Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/216.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/256.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/256.png new file mode 100644 index 0000000..090d005 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/256.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/29.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/29.png new file mode 100644 index 0000000..489b5c8 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/29.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/32.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/32.png new file mode 100644 index 0000000..c9bf87c Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/32.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/40.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/40.png new file mode 100644 index 0000000..05c32e8 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/40.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/48.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/48.png new file mode 100644 index 0000000..a246eff Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/48.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/50.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/50.png new file mode 100644 index 0000000..d30c1cc Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/50.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/512.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/512.png new file mode 100644 index 0000000..77f48a9 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/512.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/55.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/55.png new file mode 100644 index 0000000..d38ad5d Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/55.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/57.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/57.png new file mode 100644 index 0000000..9e534dc Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/57.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/58.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/58.png new file mode 100644 index 0000000..e1fc0c9 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/58.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/60.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/60.png new file mode 100644 index 0000000..615b8f4 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/60.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/64.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/64.png new file mode 100644 index 0000000..82cbc07 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/64.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/72.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/72.png new file mode 100644 index 0000000..7ff37bc Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/72.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/76.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/76.png new file mode 100644 index 0000000..f7388be Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/76.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/80.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/80.png new file mode 100644 index 0000000..6702119 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/80.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/87.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/87.png new file mode 100644 index 0000000..b1011a6 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/87.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/88.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/88.png new file mode 100644 index 0000000..6d433e1 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/88.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json index d36b1fa..e138c0b 100644 --- a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json +++ b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -1,122 +1 @@ -{ - "images" : [ - { - "size" : "20x20", - "idiom" : "iphone", - "filename" : "Icon-App-20x20@2x.png", - "scale" : "2x" - }, - { - "size" : "20x20", - "idiom" : "iphone", - "filename" : "Icon-App-20x20@3x.png", - "scale" : "3x" - }, - { - "size" : "29x29", - "idiom" : "iphone", - "filename" : "Icon-App-29x29@1x.png", - "scale" : "1x" - }, - { - "size" : "29x29", - "idiom" : "iphone", - "filename" : "Icon-App-29x29@2x.png", - "scale" : "2x" - }, - { - "size" : "29x29", - "idiom" : "iphone", - "filename" : "Icon-App-29x29@3x.png", - "scale" : "3x" - }, - { - "size" : "40x40", - "idiom" : "iphone", - "filename" : "Icon-App-40x40@2x.png", - "scale" : "2x" - }, - { - "size" : "40x40", - "idiom" : "iphone", - "filename" : "Icon-App-40x40@3x.png", - "scale" : "3x" - }, - { - "size" : "60x60", - "idiom" : "iphone", - "filename" : "Icon-App-60x60@2x.png", - "scale" : "2x" - }, - { - "size" : "60x60", - "idiom" : "iphone", - "filename" : "Icon-App-60x60@3x.png", - "scale" : "3x" - }, - { - "size" : "20x20", - "idiom" : "ipad", - "filename" : "Icon-App-20x20@1x.png", - "scale" : "1x" - }, - { - "size" : "20x20", - "idiom" : "ipad", - "filename" : "Icon-App-20x20@2x.png", - "scale" : "2x" - }, - { - "size" : "29x29", - "idiom" : "ipad", - "filename" : "Icon-App-29x29@1x.png", - "scale" : "1x" - }, - { - "size" : "29x29", - "idiom" : "ipad", - "filename" : "Icon-App-29x29@2x.png", - "scale" : "2x" - }, - { - "size" : "40x40", - "idiom" : "ipad", - "filename" : "Icon-App-40x40@1x.png", - "scale" : "1x" - }, - { - "size" : "40x40", - "idiom" : "ipad", - "filename" : "Icon-App-40x40@2x.png", - "scale" : "2x" - }, - { - "size" : "76x76", - "idiom" : "ipad", - "filename" : "Icon-App-76x76@1x.png", - "scale" : "1x" - }, - { - "size" : "76x76", - "idiom" : "ipad", - "filename" : "Icon-App-76x76@2x.png", - "scale" : "2x" - }, - { - "size" : "83.5x83.5", - "idiom" : "ipad", - "filename" : "Icon-App-83.5x83.5@2x.png", - "scale" : "2x" - }, - { - "size" : "1024x1024", - "idiom" : "ios-marketing", - "filename" : "Icon-App-1024x1024@1x.png", - "scale" : "1x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} +{"images":[{"size":"60x60","expected-size":"180","filename":"180.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"3x"},{"size":"40x40","expected-size":"80","filename":"80.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"40x40","expected-size":"120","filename":"120.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"3x"},{"size":"60x60","expected-size":"120","filename":"120.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"57x57","expected-size":"57","filename":"57.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"1x"},{"size":"29x29","expected-size":"58","filename":"58.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"29x29","expected-size":"29","filename":"29.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"1x"},{"size":"29x29","expected-size":"87","filename":"87.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"3x"},{"size":"57x57","expected-size":"114","filename":"114.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"20x20","expected-size":"40","filename":"40.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"20x20","expected-size":"60","filename":"60.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"3x"},{"size":"1024x1024","filename":"1024.png","expected-size":"1024","idiom":"ios-marketing","folder":"Assets.xcassets/AppIcon.appiconset/","scale":"1x"},{"size":"40x40","expected-size":"80","filename":"80.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"size":"72x72","expected-size":"72","filename":"72.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"1x"},{"size":"76x76","expected-size":"152","filename":"152.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"size":"50x50","expected-size":"100","filename":"100.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"size":"29x29","expected-size":"58","filename":"58.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"size":"76x76","expected-size":"76","filename":"76.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"1x"},{"size":"29x29","expected-size":"29","filename":"29.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"1x"},{"size":"50x50","expected-size":"50","filename":"50.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"1x"},{"size":"72x72","expected-size":"144","filename":"144.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"size":"40x40","expected-size":"40","filename":"40.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"1x"},{"size":"83.5x83.5","expected-size":"167","filename":"167.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"size":"20x20","expected-size":"20","filename":"20.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"1x"},{"size":"20x20","expected-size":"40","filename":"40.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"idiom":"watch","filename":"172.png","folder":"Assets.xcassets/AppIcon.appiconset/","subtype":"38mm","scale":"2x","size":"86x86","expected-size":"172","role":"quickLook"},{"idiom":"watch","filename":"80.png","folder":"Assets.xcassets/AppIcon.appiconset/","subtype":"38mm","scale":"2x","size":"40x40","expected-size":"80","role":"appLauncher"},{"idiom":"watch","filename":"88.png","folder":"Assets.xcassets/AppIcon.appiconset/","subtype":"40mm","scale":"2x","size":"44x44","expected-size":"88","role":"appLauncher"},{"idiom":"watch","filename":"100.png","folder":"Assets.xcassets/AppIcon.appiconset/","subtype":"44mm","scale":"2x","size":"50x50","expected-size":"100","role":"appLauncher"},{"idiom":"watch","filename":"196.png","folder":"Assets.xcassets/AppIcon.appiconset/","subtype":"42mm","scale":"2x","size":"98x98","expected-size":"196","role":"quickLook"},{"idiom":"watch","filename":"216.png","folder":"Assets.xcassets/AppIcon.appiconset/","subtype":"44mm","scale":"2x","size":"108x108","expected-size":"216","role":"quickLook"},{"idiom":"watch","filename":"48.png","folder":"Assets.xcassets/AppIcon.appiconset/","subtype":"38mm","scale":"2x","size":"24x24","expected-size":"48","role":"notificationCenter"},{"idiom":"watch","filename":"55.png","folder":"Assets.xcassets/AppIcon.appiconset/","subtype":"42mm","scale":"2x","size":"27.5x27.5","expected-size":"55","role":"notificationCenter"},{"size":"29x29","expected-size":"87","filename":"87.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"watch","role":"companionSettings","scale":"3x"},{"size":"29x29","expected-size":"58","filename":"58.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"watch","role":"companionSettings","scale":"2x"},{"size":"1024x1024","expected-size":"1024","filename":"1024.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"watch-marketing","scale":"1x"},{"size":"128x128","expected-size":"128","filename":"128.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"1x"},{"size":"256x256","expected-size":"256","filename":"256.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"1x"},{"size":"128x128","expected-size":"256","filename":"256.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"2x"},{"size":"256x256","expected-size":"512","filename":"512.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"2x"},{"size":"32x32","expected-size":"32","filename":"32.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"1x"},{"size":"512x512","expected-size":"512","filename":"512.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"1x"},{"size":"16x16","expected-size":"16","filename":"16.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"1x"},{"size":"16x16","expected-size":"32","filename":"32.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"2x"},{"size":"32x32","expected-size":"64","filename":"64.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"2x"},{"size":"512x512","expected-size":"1024","filename":"1024.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"2x"}]} \ No newline at end of file diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png deleted file mode 100644 index dc9ada4..0000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png deleted file mode 100644 index 28c6bf0..0000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png deleted file mode 100644 index 2ccbfd9..0000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png deleted file mode 100644 index f091b6b..0000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png deleted file mode 100644 index 4cde121..0000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png deleted file mode 100644 index d0ef06e..0000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png deleted file mode 100644 index dcdc230..0000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png deleted file mode 100644 index 2ccbfd9..0000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png deleted file mode 100644 index c8f9ed8..0000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png deleted file mode 100644 index a6d6b86..0000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png deleted file mode 100644 index a6d6b86..0000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png deleted file mode 100644 index 75b2d16..0000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png deleted file mode 100644 index c4df70d..0000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png deleted file mode 100644 index 6a84f41..0000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png deleted file mode 100644 index d0e1f58..0000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png and /dev/null differ diff --git a/ios/Runner/GoogleService-Info.plist b/ios/Runner/GoogleService-Info.plist new file mode 100644 index 0000000..4bc4228 --- /dev/null +++ b/ios/Runner/GoogleService-Info.plist @@ -0,0 +1,34 @@ + + + + + CLIENT_ID + 935017686266-54hu01v9cc5pqpgofo1gk2n3hegj4r2m.apps.googleusercontent.com + REVERSED_CLIENT_ID + com.googleusercontent.apps.935017686266-54hu01v9cc5pqpgofo1gk2n3hegj4r2m + API_KEY + AIzaSyCMa-zg_uVhOfTnea5Klz6aPZlgHwVGj7U + GCM_SENDER_ID + 935017686266 + PLIST_VERSION + 1 + BUNDLE_ID + com.didvan.didvanapp + PROJECT_ID + didvan-9b7da + STORAGE_BUCKET + didvan-9b7da.appspot.com + IS_ADS_ENABLED + + IS_ANALYTICS_ENABLED + + IS_APPINVITE_ENABLED + + IS_GCM_ENABLED + + IS_SIGNIN_ENABLED + + GOOGLE_APP_ID + 1:935017686266:ios:de47638bd662463fd2d543 + + \ No newline at end of file diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index 8a877dd..17f2a3a 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -26,6 +26,14 @@ LaunchScreen UIMainStoryboardFile Main + FirebaseAppDelegateProxyEnabled + + NSMicrophoneUsageDescription + We need to access to the microphone to record audio file + NSPhotoLibraryUsageDescription + We need to access to the user gallery to add user profile photo + NSCameraUsageDescription + We need to access to the user gallery to add user profile photo UISupportedInterfaceOrientations UIInterfaceOrientationPortrait diff --git a/lib/assets/animations/indicator-dark.riv b/lib/assets/animations/indicator-dark.riv new file mode 100644 index 0000000..d995f60 Binary files /dev/null and b/lib/assets/animations/indicator-dark.riv differ diff --git a/lib/assets/animations/indicator-light.riv b/lib/assets/animations/indicator-light.riv new file mode 100644 index 0000000..0190c3e Binary files /dev/null and b/lib/assets/animations/indicator-light.riv differ diff --git a/lib/assets/animations/loading.gif b/lib/assets/animations/loading.gif new file mode 100644 index 0000000..cf967e1 Binary files /dev/null and b/lib/assets/animations/loading.gif differ diff --git a/lib/assets/fonts/Dana-FaNum-Black.ttf b/lib/assets/fonts/Dana-FaNum-Black.ttf new file mode 100644 index 0000000..48e24bf Binary files /dev/null and b/lib/assets/fonts/Dana-FaNum-Black.ttf differ diff --git a/lib/assets/fonts/Dana-FaNum-Bold.ttf b/lib/assets/fonts/Dana-FaNum-Bold.ttf new file mode 100644 index 0000000..da19d40 Binary files /dev/null and b/lib/assets/fonts/Dana-FaNum-Bold.ttf differ diff --git a/lib/assets/fonts/Dana-FaNum-ExtraBold.ttf b/lib/assets/fonts/Dana-FaNum-ExtraBold.ttf new file mode 100644 index 0000000..f0a4c42 Binary files /dev/null and b/lib/assets/fonts/Dana-FaNum-ExtraBold.ttf differ diff --git a/lib/assets/fonts/Dana-FaNum-ExtraLight.ttf b/lib/assets/fonts/Dana-FaNum-ExtraLight.ttf new file mode 100644 index 0000000..25972f8 Binary files /dev/null and b/lib/assets/fonts/Dana-FaNum-ExtraLight.ttf differ diff --git a/lib/assets/fonts/Dana-FaNum-Light.ttf b/lib/assets/fonts/Dana-FaNum-Light.ttf new file mode 100644 index 0000000..f141014 Binary files /dev/null and b/lib/assets/fonts/Dana-FaNum-Light.ttf differ diff --git a/lib/assets/fonts/Dana-FaNum-Medium.ttf b/lib/assets/fonts/Dana-FaNum-Medium.ttf new file mode 100644 index 0000000..b8e44af Binary files /dev/null and b/lib/assets/fonts/Dana-FaNum-Medium.ttf differ diff --git a/lib/assets/fonts/Dana-FaNum-Regular.ttf b/lib/assets/fonts/Dana-FaNum-Regular.ttf new file mode 100644 index 0000000..3691b96 Binary files /dev/null and b/lib/assets/fonts/Dana-FaNum-Regular.ttf differ diff --git a/lib/assets/fonts/Dana-FaNum-SemiBold.ttf b/lib/assets/fonts/Dana-FaNum-SemiBold.ttf new file mode 100644 index 0000000..3e15e00 Binary files /dev/null and b/lib/assets/fonts/Dana-FaNum-SemiBold.ttf differ diff --git a/lib/assets/fonts/Dana-FaNum-Thin.ttf b/lib/assets/fonts/Dana-FaNum-Thin.ttf new file mode 100644 index 0000000..5a6e427 Binary files /dev/null and b/lib/assets/fonts/Dana-FaNum-Thin.ttf differ diff --git a/lib/assets/fonts/Dana-FaNum-UltraBold.ttf b/lib/assets/fonts/Dana-FaNum-UltraBold.ttf new file mode 100644 index 0000000..2ac62ca Binary files /dev/null and b/lib/assets/fonts/Dana-FaNum-UltraBold.ttf differ diff --git a/lib/assets/fonts/Dana.ttf b/lib/assets/fonts/Dana.ttf new file mode 100644 index 0000000..93200a1 Binary files /dev/null and b/lib/assets/fonts/Dana.ttf differ diff --git a/lib/assets/fonts/IRANSansMobile-FaNum-Black.ttf b/lib/assets/fonts/IRANSansMobile-FaNum-Black.ttf new file mode 100644 index 0000000..411ef9e Binary files /dev/null and b/lib/assets/fonts/IRANSansMobile-FaNum-Black.ttf differ diff --git a/lib/assets/fonts/IRANSansMobile-FaNum-Bold.ttf b/lib/assets/fonts/IRANSansMobile-FaNum-Bold.ttf new file mode 100644 index 0000000..09fab2b Binary files /dev/null and b/lib/assets/fonts/IRANSansMobile-FaNum-Bold.ttf differ diff --git a/lib/assets/fonts/IRANSansMobile-FaNum-Light.ttf b/lib/assets/fonts/IRANSansMobile-FaNum-Light.ttf new file mode 100644 index 0000000..ecdac1d Binary files /dev/null and b/lib/assets/fonts/IRANSansMobile-FaNum-Light.ttf differ diff --git a/lib/assets/fonts/IRANSansMobile-FaNum-Medium.ttf b/lib/assets/fonts/IRANSansMobile-FaNum-Medium.ttf new file mode 100644 index 0000000..3dc55d0 Binary files /dev/null and b/lib/assets/fonts/IRANSansMobile-FaNum-Medium.ttf differ diff --git a/lib/assets/fonts/IRANSansMobile-FaNum-UltraLight.ttf b/lib/assets/fonts/IRANSansMobile-FaNum-UltraLight.ttf new file mode 100644 index 0000000..6564159 Binary files /dev/null and b/lib/assets/fonts/IRANSansMobile-FaNum-UltraLight.ttf differ diff --git a/lib/assets/fonts/IRANSansMobile-FaNum.ttf b/lib/assets/fonts/IRANSansMobile-FaNum.ttf new file mode 100644 index 0000000..c60002a Binary files /dev/null and b/lib/assets/fonts/IRANSansMobile-FaNum.ttf differ diff --git a/lib/assets/fonts/IRANSansMobile.ttf b/lib/assets/fonts/IRANSansMobile.ttf new file mode 100644 index 0000000..3c9b756 Binary files /dev/null and b/lib/assets/fonts/IRANSansMobile.ttf differ diff --git a/lib/assets/icons/Dicon.ttf b/lib/assets/icons/Dicon.ttf new file mode 100644 index 0000000..ac60bdf Binary files /dev/null and b/lib/assets/icons/Dicon.ttf differ diff --git a/lib/assets/images/categories/business-dark.svg b/lib/assets/images/categories/business-dark.svg new file mode 100644 index 0000000..9e09cda --- /dev/null +++ b/lib/assets/images/categories/business-dark.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/lib/assets/images/categories/business-light.svg b/lib/assets/images/categories/business-light.svg new file mode 100644 index 0000000..4b64c42 --- /dev/null +++ b/lib/assets/images/categories/business-light.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/lib/assets/images/categories/economic-dark.svg b/lib/assets/images/categories/economic-dark.svg new file mode 100644 index 0000000..3700deb --- /dev/null +++ b/lib/assets/images/categories/economic-dark.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/lib/assets/images/categories/economic-light.svg b/lib/assets/images/categories/economic-light.svg new file mode 100644 index 0000000..0513930 --- /dev/null +++ b/lib/assets/images/categories/economic-light.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/lib/assets/images/categories/enviromental-dark.svg b/lib/assets/images/categories/enviromental-dark.svg new file mode 100644 index 0000000..b59ba84 --- /dev/null +++ b/lib/assets/images/categories/enviromental-dark.svg @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lib/assets/images/categories/enviromental-light.svg b/lib/assets/images/categories/enviromental-light.svg new file mode 100644 index 0000000..ce38dd6 --- /dev/null +++ b/lib/assets/images/categories/enviromental-light.svg @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lib/assets/images/categories/political-dark.svg b/lib/assets/images/categories/political-dark.svg new file mode 100644 index 0000000..ae8f3ad --- /dev/null +++ b/lib/assets/images/categories/political-dark.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/lib/assets/images/categories/political-light.svg b/lib/assets/images/categories/political-light.svg new file mode 100644 index 0000000..c8f71b4 --- /dev/null +++ b/lib/assets/images/categories/political-light.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/lib/assets/images/categories/social-dark.svg b/lib/assets/images/categories/social-dark.svg new file mode 100644 index 0000000..c4a0fb8 --- /dev/null +++ b/lib/assets/images/categories/social-dark.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/lib/assets/images/categories/social-light.svg b/lib/assets/images/categories/social-light.svg new file mode 100644 index 0000000..2b28dac --- /dev/null +++ b/lib/assets/images/categories/social-light.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/lib/assets/images/categories/tech-dark.svg b/lib/assets/images/categories/tech-dark.svg new file mode 100644 index 0000000..3fa3e80 --- /dev/null +++ b/lib/assets/images/categories/tech-dark.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/lib/assets/images/categories/tech-light.svg b/lib/assets/images/categories/tech-light.svg new file mode 100644 index 0000000..ec8743a --- /dev/null +++ b/lib/assets/images/categories/tech-light.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/lib/assets/images/empty_states/bookmark-dark.svg b/lib/assets/images/empty_states/bookmark-dark.svg new file mode 100644 index 0000000..effdb45 --- /dev/null +++ b/lib/assets/images/empty_states/bookmark-dark.svg @@ -0,0 +1,83 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lib/assets/images/empty_states/bookmark-light.svg b/lib/assets/images/empty_states/bookmark-light.svg new file mode 100644 index 0000000..8bb6d38 --- /dev/null +++ b/lib/assets/images/empty_states/bookmark-light.svg @@ -0,0 +1,83 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lib/assets/images/empty_states/chart-dark.svg b/lib/assets/images/empty_states/chart-dark.svg new file mode 100644 index 0000000..87e4b6c --- /dev/null +++ b/lib/assets/images/empty_states/chart-dark.svg @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lib/assets/images/empty_states/chart-light.svg b/lib/assets/images/empty_states/chart-light.svg new file mode 100644 index 0000000..e7287c0 --- /dev/null +++ b/lib/assets/images/empty_states/chart-light.svg @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lib/assets/images/empty_states/chat-dark.svg b/lib/assets/images/empty_states/chat-dark.svg new file mode 100644 index 0000000..0e79e3f --- /dev/null +++ b/lib/assets/images/empty_states/chat-dark.svg @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lib/assets/images/empty_states/chat-light.svg b/lib/assets/images/empty_states/chat-light.svg new file mode 100644 index 0000000..164cb68 --- /dev/null +++ b/lib/assets/images/empty_states/chat-light.svg @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lib/assets/images/empty_states/connection-dark.svg b/lib/assets/images/empty_states/connection-dark.svg new file mode 100644 index 0000000..87ad7a7 --- /dev/null +++ b/lib/assets/images/empty_states/connection-dark.svg @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lib/assets/images/empty_states/connection-light.svg b/lib/assets/images/empty_states/connection-light.svg new file mode 100644 index 0000000..f0a62b5 --- /dev/null +++ b/lib/assets/images/empty_states/connection-light.svg @@ -0,0 +1,97 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lib/assets/images/empty_states/result-dark.svg b/lib/assets/images/empty_states/result-dark.svg new file mode 100644 index 0000000..1a8515d --- /dev/null +++ b/lib/assets/images/empty_states/result-dark.svg @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lib/assets/images/empty_states/result-light.svg b/lib/assets/images/empty_states/result-light.svg new file mode 100644 index 0000000..f56538b --- /dev/null +++ b/lib/assets/images/empty_states/result-light.svg @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lib/assets/images/empty_states/studio-dark.svg b/lib/assets/images/empty_states/studio-dark.svg new file mode 100644 index 0000000..392039b --- /dev/null +++ b/lib/assets/images/empty_states/studio-dark.svg @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lib/assets/images/empty_states/studio-light.svg b/lib/assets/images/empty_states/studio-light.svg new file mode 100644 index 0000000..5808670 --- /dev/null +++ b/lib/assets/images/empty_states/studio-light.svg @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lib/assets/images/logos/logo-horizontal-dark.svg b/lib/assets/images/logos/logo-horizontal-dark.svg new file mode 100644 index 0000000..95d2d09 --- /dev/null +++ b/lib/assets/images/logos/logo-horizontal-dark.svg @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lib/assets/images/logos/logo-horizontal-light.svg b/lib/assets/images/logos/logo-horizontal-light.svg new file mode 100644 index 0000000..4657fce --- /dev/null +++ b/lib/assets/images/logos/logo-horizontal-light.svg @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lib/assets/images/logos/logo-vertical-dark.svg b/lib/assets/images/logos/logo-vertical-dark.svg new file mode 100644 index 0000000..db6df03 --- /dev/null +++ b/lib/assets/images/logos/logo-vertical-dark.svg @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lib/assets/images/logos/logo-vertical-light.svg b/lib/assets/images/logos/logo-vertical-light.svg new file mode 100644 index 0000000..b79519f --- /dev/null +++ b/lib/assets/images/logos/logo-vertical-light.svg @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lib/assets/images/logos/studio-dark.svg b/lib/assets/images/logos/studio-dark.svg new file mode 100644 index 0000000..3d0e67d --- /dev/null +++ b/lib/assets/images/logos/studio-dark.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/lib/assets/images/logos/studio-light.svg b/lib/assets/images/logos/studio-light.svg new file mode 100644 index 0000000..73d14b1 --- /dev/null +++ b/lib/assets/images/logos/studio-light.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lib/assets/images/records/record-dark.svg b/lib/assets/images/records/record-dark.svg new file mode 100644 index 0000000..a95f8e3 --- /dev/null +++ b/lib/assets/images/records/record-dark.svg @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lib/assets/images/records/record-light.svg b/lib/assets/images/records/record-light.svg new file mode 100644 index 0000000..375ee5b --- /dev/null +++ b/lib/assets/images/records/record-light.svg @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lib/assets/images/themes/theme-dark.svg b/lib/assets/images/themes/theme-dark.svg new file mode 100644 index 0000000..ce96210 --- /dev/null +++ b/lib/assets/images/themes/theme-dark.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/lib/assets/images/themes/theme-light.svg b/lib/assets/images/themes/theme-light.svg new file mode 100644 index 0000000..93ac8c4 --- /dev/null +++ b/lib/assets/images/themes/theme-light.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/lib/config/design_config.dart b/lib/config/design_config.dart new file mode 100644 index 0000000..13cf836 --- /dev/null +++ b/lib/config/design_config.dart @@ -0,0 +1,60 @@ +import 'package:didvan/config/theme_data.dart'; +import 'package:didvan/main.dart'; +import 'package:didvan/providers/theme_provider.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:provider/provider.dart'; + +class DesignConfig { + static BuildContext? get context => navigatorKey.currentContext; + + static const BorderRadius lowBorderRadius = BorderRadius.all( + Radius.circular(8), + ); + static const BorderRadius mediumBorderRadius = BorderRadius.all( + Radius.circular(10), + ); + static const BorderRadius highBorderRadius = BorderRadius.all( + Radius.circular(16), + ); + + static Border get cardBorder => Border.all( + color: Theme.of(context!).colorScheme.cardBorder, + width: 0.5, + ); + + static List get defaultShadow => [ + BoxShadow( + color: const Color(0XFF1B3C59).withOpacity(0.25), + blurRadius: 16, + spreadRadius: 0, + ) + ]; + + static String get fontFamily => context!.read().fontFamily; + static double get fontScale => context!.read().fontScale; + + static Brightness get brightness => Theme.of(context!).brightness; + static bool get isDark => brightness == Brightness.dark; + + static const Duration lowAnimationDuration = Duration(milliseconds: 300); + static const Duration mediumAnimationDuration = Duration(milliseconds: 350); + + static SystemUiOverlayStyle get systemUiOverlayStyle => SystemUiOverlayStyle( + statusBarIconBrightness: + brightness == Brightness.dark ? Brightness.light : Brightness.dark, + statusBarColor: + Theme.of(context!).colorScheme.background.withOpacity(0.5), + systemNavigationBarColor: Theme.of(context!).colorScheme.surface, + systemNavigationBarDividerColor: Colors.transparent, + systemNavigationBarIconBrightness: + brightness == Brightness.dark ? Brightness.light : Brightness.dark, + ); + + static void updateSystemUiOverlayStyle() { + Future.delayed( + DesignConfig.lowAnimationDuration, + () => SystemChrome.setSystemUIOverlayStyle(systemUiOverlayStyle), + ); + } +} diff --git a/lib/config/theme_data.dart b/lib/config/theme_data.dart new file mode 100644 index 0000000..b1e6db7 --- /dev/null +++ b/lib/config/theme_data.dart @@ -0,0 +1,206 @@ +import 'package:didvan/config/design_config.dart'; +import 'package:flutter/material.dart'; + +class LightThemeConfig { + static const Color _primary = Color(0xFF007EA7); + static const Color _white = Color(0xFFFFFFFF); + static const Color _black = Color(0xFF292929); + static const Color _background = Color(0xFFF8F8FA); + + static ThemeData get themeData => ThemeData( + backgroundColor: _background, + scaffoldBackgroundColor: _background, + colorScheme: _colorScheme, + textTheme: _TextThemeData.data, + cardColor: _colorScheme.surface, + checkboxTheme: CheckboxThemeData( + fillColor: MaterialStateProperty.all(_colorScheme.primary), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(4), + ), + ), + iconTheme: IconThemeData( + color: _colorScheme.text, + ), + ); + + static const ColorScheme _colorScheme = ColorScheme( + primary: _primary, + primaryContainer: _white, + secondary: Color(0xFFD61515), + secondaryContainer: _white, + surface: _white, + background: _background, + error: Color(0xFFF00505), + onPrimary: _white, + onSecondary: _white, + onSurface: _black, + onBackground: _white, + onError: _white, + brightness: Brightness.light, + ); +} + +class DarkThemeConfig { + static const Color _primary = Color(0xFF007EA7); + static const Color _white = Color(0xFFFFFFFF); + static const Color _background = Color(0xFF202224); + + static ThemeData get themeData => ThemeData( + backgroundColor: _background, + scaffoldBackgroundColor: _background, + colorScheme: _colorScheme, + textTheme: _TextThemeData.data, + iconTheme: IconThemeData( + color: _colorScheme.text, + ), + checkboxTheme: CheckboxThemeData( + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + fillColor: MaterialStateProperty.all(_colorScheme.primary), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(4), + ), + ), + cardColor: _colorScheme.surface, + ); + + static const ColorScheme _colorScheme = ColorScheme( + primary: _primary, + primaryContainer: _white, + secondary: Color(0xFFE53939), + secondaryContainer: _white, + surface: Color(0xFF181B1F), + background: _background, + error: Color(0xFFF53B3B), + onPrimary: _white, + onSecondary: _white, + onSurface: text, + onBackground: text, + onError: _white, + brightness: Brightness.dark, + ); + + // Grey colors + static const Color white = Color(0xFFFFFFFF); + static const Color title = Color(0xFFF5F5F5); + static const Color text = Color(0xFFD6D6D6); + static const Color hint = Color(0xFFBBBBBB); + static const Color border = Color(0xFF666666); + + // Error and success + static const Color errorLight = Color(0xFFF0C9CD); + static const Color error = Color(0xFFF53B3B); + static const Color successLight = Color(0xFFBBD6B4); + static const Color success = Color(0xFF32A64C); +} + +class _TextThemeData { + static TextTheme get data => DesignConfig.context == null + ? const TextTheme() + : TextTheme( + bodyText1: _body1Text, + bodyText2: _body2Text, + caption: _captionText, + subtitle2: _subtitle2Text, + subtitle1: _subtitle1Text, + headline3: _headline3Text, + overline: _overlineText, + ); + + static TextStyle get _headline3Text => TextStyle( + fontSize: 20 * DesignConfig.fontScale, + fontWeight: FontWeight.w600, + ); + static TextStyle get _subtitle1Text => TextStyle( + fontSize: 17 * DesignConfig.fontScale, + fontWeight: FontWeight.w700, + ); + static TextStyle get _subtitle2Text => TextStyle( + fontSize: 15 * DesignConfig.fontScale, + fontWeight: FontWeight.w700, + ); + static TextStyle get _body1Text => TextStyle( + fontSize: 15 * DesignConfig.fontScale, + fontWeight: FontWeight.w700, + ); + static TextStyle get _body2Text => TextStyle( + fontSize: 15 * DesignConfig.fontScale, + fontWeight: FontWeight.w400, + ); + static TextStyle get _captionText => TextStyle( + fontSize: 13 * DesignConfig.fontScale, + fontWeight: FontWeight.w400, + ); + static TextStyle get _overlineText => TextStyle( + fontSize: 12 * DesignConfig.fontScale, + fontWeight: FontWeight.w300, + ); +} + +extension DidvanColorScheme on ColorScheme { + // Secondary colors + Color get secondaryDisabled => brightness == Brightness.dark + ? const Color(0xFF703838) + : const Color(0xFFFFC8C8); + + Color get white => const Color(0xFFFFFFFF); + Color get focused => brightness == Brightness.dark + ? const Color(0xFF20272E) + : const Color(0xFFE6F3FA); + Color get navigation => brightness == Brightness.dark + ? const Color(0xFF181B1F) + : const Color(0xFF012348); + Color get focusedBorder => brightness == Brightness.dark + ? const Color(0xFF9FB2C2) + : const Color(0xFF195D80); + Color get title => brightness == Brightness.dark + ? const Color(0xFFD6D6D6) + : const Color(0xFF1B3C59); + Color get text => brightness == Brightness.dark + ? const Color(0xFFD6D6D6) + : const Color(0xFF292929); + Color get inputText => brightness == Brightness.dark + ? const Color(0xFFA3A3A3) + : const Color(0xFF3D3D3D); + Color get caption => brightness == Brightness.dark + ? const Color(0xFFBBBBBB) + : const Color(0xFF666666); + Color get hint => brightness == Brightness.dark + ? const Color(0xFF7A7A7A) + : const Color(0xFFBBBBBB); + Color get disabledText => brightness == Brightness.dark + ? const Color(0xFF5C5C5C) + : const Color(0xFFBBBBBB); + Color get border => brightness == Brightness.dark + ? const Color(0xFF5C5C5C) + : const Color(0xFFE0E0E0); + Color get cardBorder => brightness == Brightness.dark + ? const Color(0xFF5C5C5C) + : const Color(0xFFEBEBEB); + Color get disabledBackground => brightness == Brightness.dark + ? const Color(0xFF333333) + : const Color(0xFFE0E0E0); + Color get secondCTA => brightness == Brightness.dark + ? const Color(0xFF474747) + : const Color(0xFFF5F5F5); + Color get splash => brightness == Brightness.dark + ? const Color(0xFF333333) + : const Color(0xFFC8E0F4); + Color get black => brightness == Brightness.dark + ? const Color(0xFF1F1F1F) + : const Color(0xFF292929); + Color get overlay => brightness == Brightness.dark + ? const Color(0xFF0F1011) + : const Color(0xFF292929); + + // Error and success colors + Color get errorBack => brightness == Brightness.dark + ? const Color(0xFF2B2325) + : const Color(0xFFFFF8F8); + Color get successBack => brightness == Brightness.dark + ? const Color(0xFF2C332B) + : const Color(0xFFF5FFFC); + Color get success => brightness == Brightness.dark + ? const Color(0xFF2FC250) + : const Color(0xFF2BB24A); +} diff --git a/lib/constants/app_icons.dart b/lib/constants/app_icons.dart new file mode 100644 index 0000000..b1eb80a --- /dev/null +++ b/lib/constants/app_icons.dart @@ -0,0 +1,452 @@ +/// Flutter icons DidvanIcons +/// Copyright (C) 2022 by original authors @ fluttericon.com, fontello.com +/// This font was generated by FlutterIcon.com, which is derived from Fontello. +/// +/// To use this font, place it in your fonts/ directory and include the +/// following in your pubspec.yaml +/// +/// flutter: +/// fonts: +/// - family: DidvanIcons +/// fonts: +/// - asset: fonts/DidvanIcons.ttf +/// +/// +/// +// ignore_for_file: constant_identifier_names + +import 'package:flutter/widgets.dart'; + +class DidvanIcons { + DidvanIcons._(); + + static const _kFontFam = 'Dicon'; + static const String? _kFontPkg = null; + + static const IconData alert_light = + IconData(0xe800, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData alert_regular = + IconData(0xe801, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData alert_solid = + IconData(0xe802, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData angle_down_light = + IconData(0xe803, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData angle_down_regular = + IconData(0xe804, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData angle_down_solid = + IconData(0xe805, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData angle_left_light = + IconData(0xe806, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData angle_left_regular = + IconData(0xe807, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData angle_left_solid = + IconData(0xe808, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData angle_right_light = + IconData(0xe809, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData angle_right_regular = + IconData(0xe80a, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData angle_right_solid = + IconData(0xe80b, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData angle_up_light = + IconData(0xe80c, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData angle_up_regular = + IconData(0xe80d, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData angle_up_solid = + IconData(0xe80e, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData arrow_up_light = + IconData(0xe80f, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData arrow_up_regular = + IconData(0xe810, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData arrow_up_solid = + IconData(0xe811, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData avatar_light = + IconData(0xe812, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData avatar_regular = + IconData(0xe813, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData avatar_solid = + IconData(0xe814, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData back_light = + IconData(0xe815, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData back_regular = + IconData(0xe816, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData back_solid = + IconData(0xe817, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData bookmark_light = + IconData(0xe818, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData bookmark_regular = + IconData(0xe819, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData bookmark_solid = + IconData(0xe81a, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData calendar_day_light = + IconData(0xe81b, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData calendar_day_regular = + IconData(0xe81c, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData calendar_day_solid = + IconData(0xe81d, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData calendar_range_light = + IconData(0xe81e, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData calendar_range_regular = + IconData(0xe81f, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData camera_light = + IconData(0xe820, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData calendar_range_solid = + IconData(0xe821, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData camera_regular = + IconData(0xe822, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData camera_solid = + IconData(0xe823, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData caret_down_light = + IconData(0xe824, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData caret_down_regular = + IconData(0xe825, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData caret_down_solid = + IconData(0xe826, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData chart_light = + IconData(0xe827, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData chart_regular = + IconData(0xe828, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData chart_solid = + IconData(0xe829, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData chats_light = + IconData(0xe82a, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData chats_regular = + IconData(0xe82b, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData chats_solid = + IconData(0xe82c, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData check_light = + IconData(0xe82d, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData check_regular = + IconData(0xe82e, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData check_solid = + IconData(0xe82f, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData check_circle_light = + IconData(0xe830, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData check_circle_regular = + IconData(0xe831, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData check_circle_solid = + IconData(0xe832, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData check_double_light = + IconData(0xe833, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData check_double_regular = + IconData(0xe834, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData check_double_solid = + IconData(0xe835, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData close_light = + IconData(0xe836, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData close_regular = + IconData(0xe837, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData close_solid = + IconData(0xe838, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData close_circle_light = + IconData(0xe839, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData close_circle_regular = + IconData(0xe83a, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData close_circle_solid = + IconData(0xe83b, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData description_light = + IconData(0xe83c, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData description_regular = + IconData(0xe83d, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData description_solid = + IconData(0xe83e, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData didvan_light = + IconData(0xe83f, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData didvan_regular = + IconData(0xe840, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData didvan_solid = + IconData(0xe841, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData dislike_light = + IconData(0xe842, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData dislike_regular = + IconData(0xe843, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData dislike_solid = + IconData(0xe844, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData download_light = + IconData(0xe845, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData download_regular = + IconData(0xe846, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData download_solid = + IconData(0xe847, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData evaluation_light = + IconData(0xe848, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData evaluation_regular = + IconData(0xe849, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData evaluation_solid = + IconData(0xe84a, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData exclamation_triangle_light = + IconData(0xe84b, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData exclamation_triangle_regular = + IconData(0xe84c, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData exclamation_triangle_solid = + IconData(0xe84d, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData eye_light = + IconData(0xe84e, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData eye_regular = + IconData(0xe84f, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData eye_solid = + IconData(0xe850, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData eye_slash_light = + IconData(0xe851, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData eye_slash_regular = + IconData(0xe852, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData eye_slash_solid = + IconData(0xe853, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData filter_light = + IconData(0xe854, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData filter_regular = + IconData(0xe855, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData filter_solid = + IconData(0xe856, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData fingerprint_light = + IconData(0xe857, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData fingerprint_regular = + IconData(0xe858, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData fingerprint_solid = + IconData(0xe859, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData font_light = + IconData(0xe85a, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData font_regular = + IconData(0xe85b, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData font_solid = + IconData(0xe85c, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData gallery_file_light = + IconData(0xe85d, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData gallery_file_regular = + IconData(0xe85e, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData gallery_file_solid = + IconData(0xe85f, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData google_light = + IconData(0xe860, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData google_regular = + IconData(0xe861, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData google_solid = + IconData(0xe862, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData hashtag_light = + IconData(0xe863, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData hashtag_regular = + IconData(0xe864, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData hashtag_solid = + IconData(0xe865, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData info_circle_light = + IconData(0xe866, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData info_circle_regular = + IconData(0xe867, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData info_circle_solid = + IconData(0xe868, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData lightbulb_exclamation_light = + IconData(0xe869, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData lightbulb_exclamation_regular = + IconData(0xe86a, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData lightbulb_exclamation_solid = + IconData(0xe86b, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData like_light = + IconData(0xe86c, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData like_regular = + IconData(0xe86d, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData like_solid = + IconData(0xe86e, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData loading_light = + IconData(0xe86f, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData loading_regular = + IconData(0xe870, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData loading_solid = + IconData(0xe871, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData maximize_light = + IconData(0xe872, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData maximize_regular = + IconData(0xe873, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData maximize_solid = + IconData(0xe874, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData media_backward_light = + IconData(0xe875, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData media_backward_regular = + IconData(0xe876, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData media_backward_solid = + IconData(0xe877, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData media_forward_light = + IconData(0xe878, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData media_forward_regular = + IconData(0xe879, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData media_forward_solid = + IconData(0xe87a, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData menu_light = + IconData(0xe87b, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData menu_regular = + IconData(0xe87c, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData menu_solid = + IconData(0xe87d, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData message_light = + IconData(0xe87e, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData message_regular = + IconData(0xe87f, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData message_solid = + IconData(0xe880, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData mic_light = + IconData(0xe881, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData mic_regular = + IconData(0xe882, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData mic_solid = + IconData(0xe883, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData minimize_light = + IconData(0xe884, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData minimize_regular = + IconData(0xe885, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData minimize_solid = + IconData(0xe886, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData news_light = + IconData(0xe887, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData news_regular = + IconData(0xe888, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData news_solid = + IconData(0xe889, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData notification_light = + IconData(0xe88a, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData notification_regular = + IconData(0xe88b, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData notification_solid = + IconData(0xe88c, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData pause_light = + IconData(0xe88d, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData pause_regular = + IconData(0xe88e, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData pause_solid = + IconData(0xe88f, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData pause_circle_light = + IconData(0xe890, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData pause_circle_regular = + IconData(0xe891, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData pause_circle_solid = + IconData(0xe892, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData play_light = + IconData(0xe893, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData play_regular = + IconData(0xe894, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData play_solid = + IconData(0xe895, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData play_circle_light = + IconData(0xe896, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData play_circle_regular = + IconData(0xe897, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData play_circle_solid = + IconData(0xe898, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData podcast_light = + IconData(0xe899, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData podcast_regular = + IconData(0xe89a, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData podcast_solid = + IconData(0xe89b, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData profile_light = + IconData(0xe89c, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData profile_regular = + IconData(0xe89d, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData profile_solid = + IconData(0xe89e, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData puzzle_light = + IconData(0xe89f, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData puzzle_regular = + IconData(0xe8a0, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData puzzle_solid = + IconData(0xe8a1, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData radar_light = + IconData(0xe8a2, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData radar_regular = + IconData(0xe8a3, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData radar_solid = + IconData(0xe8a4, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData refresh_light = + IconData(0xe8a5, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData refresh_regular = + IconData(0xe8a6, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData refresh_solid = + IconData(0xe8a7, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData search_light = + IconData(0xe8a8, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData search_regular = + IconData(0xe8a9, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData search_solid = + IconData(0xe8aa, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData send_light = + IconData(0xe8ab, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData send_regular = + IconData(0xe8ac, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData send_solid = + IconData(0xe8ad, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData setting_light = + IconData(0xe8ae, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData setting_regular = + IconData(0xe8af, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData setting_solid = + IconData(0xe8b0, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData sign_out_light = + IconData(0xe8b1, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData sign_out_regular = + IconData(0xe8b2, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData sign_out_solid = + IconData(0xe8b3, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData sleep_enabled_light = + IconData(0xe8b4, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData sleep_enabled_regular = + IconData(0xe8b5, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData sleep_enabled_solid = + IconData(0xe8b6, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData sleep_timer_light = + IconData(0xe8b7, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData sleep_timer_regular = + IconData(0xe8b8, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData sleep_timer_solid = + IconData(0xe8b9, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData sort_light = + IconData(0xe8ba, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData sort_regular = + IconData(0xe8bb, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData sort_solid = + IconData(0xe8bc, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData stop_circle_light = + IconData(0xe8bd, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData stop_circle_regular = + IconData(0xe8be, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData stop_circle_solid = + IconData(0xe8bf, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData support_light = + IconData(0xe8c0, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData support_regular = + IconData(0xe8c1, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData support_solid = + IconData(0xe8c2, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData theme_light = + IconData(0xe8c3, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData theme_regular = + IconData(0xe8c4, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData theme_solid = + IconData(0xe8c5, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData timer_light = + IconData(0xe8c6, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData timer_regular = + IconData(0xe8c7, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData timer_solid = + IconData(0xe8c8, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData trash_light = + IconData(0xe8c9, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData trash_regular = + IconData(0xe8ca, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData trash_solid = + IconData(0xe8cb, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData user_edit_light = + IconData(0xe8cc, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData user_edit_regular = + IconData(0xe8cd, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData user_edit_solid = + IconData(0xe8ce, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData user_shield_light = + IconData(0xe8cf, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData user_shield_regular = + IconData(0xe8d0, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData user_shield_solid = + IconData(0xe8d1, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData video_light = + IconData(0xe8d2, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData video_regular = + IconData(0xe8d3, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData video_solid = + IconData(0xe8d4, fontFamily: _kFontFam, fontPackage: _kFontPkg); +} diff --git a/lib/constants/app_strings.dart b/lib/constants/app_strings.dart new file mode 100644 index 0000000..e8f4321 --- /dev/null +++ b/lib/constants/app_strings.dart @@ -0,0 +1 @@ +class AppStrings {} diff --git a/lib/constants/assets.dart b/lib/constants/assets.dart new file mode 100644 index 0000000..e19e386 --- /dev/null +++ b/lib/constants/assets.dart @@ -0,0 +1,55 @@ +import 'package:didvan/config/design_config.dart'; +import 'package:flutter/cupertino.dart'; + +class Assets { + static const String _basePath = 'lib/assets'; + static const String _baseImagesPath = _basePath + '/images'; + static const String _baseThemesPath = _basePath + '/images/themes'; + static const String _baseEmptyStatesPath = _basePath + '/images/empty_states'; + static const String _baseAnimationsPath = _basePath + '/animations'; + static const String _baseRecordsPath = _basePath + '/images/records'; + static const String _baseLogosPath = _basePath + '/images/logos'; + + static String get verticalLogoWithText => + _baseLogosPath + '/logo-vertical-$_themeSuffix.svg'; + static String get horizontalLogoWithText => + _baseLogosPath + '/logo-horizontal-$_themeSuffix.svg'; + static String get studioLogo => _baseLogosPath + '/studio-$_themeSuffix.svg'; + + static String get logoLoadingAnimation => + _baseAnimationsPath + '/indicator-$_themeSuffix.riv'; + + static String get businessCategoryIcon => + _baseImagesPath + '/categories/business-$_themeSuffix.svg'; + static String get economicCategoryIcon => + _baseImagesPath + '/categories/economic-$_themeSuffix.svg'; + static String get enviromentalCategoryIcon => + _baseImagesPath + '/categories/enviromental-$_themeSuffix.svg'; + static String get politicalCategoryIcon => + _baseImagesPath + '/categories/political-$_themeSuffix.svg'; + static String get socialCategoryIcon => + _baseImagesPath + '/categories/social-$_themeSuffix.svg'; + static String get techCategoryIcon => + _baseImagesPath + '/categories/tech-$_themeSuffix.svg'; + + static String get emptyBookmark => + _baseEmptyStatesPath + '/bookmark-$_themeSuffix.svg'; + static String get emptyChart => + _baseEmptyStatesPath + '/chart-$_themeSuffix.svg'; + static String get emptyChat => + _baseEmptyStatesPath + '/chat-$_themeSuffix.svg'; + static String get emptyConnection => + _baseEmptyStatesPath + '/connection-$_themeSuffix.svg'; + static String get emptyResult => + _baseEmptyStatesPath + '/result-$_themeSuffix.svg'; + static String get emptyStudio => + _baseEmptyStatesPath + '/studio-$_themeSuffix.svg'; + + static const String lightTheme = _baseThemesPath + '/theme-light.svg'; + static const String darkTheme = _baseThemesPath + '/theme-dark.svg'; + + static String get record => _baseRecordsPath + '/record-$_themeSuffix.svg'; + + static String get _themeSuffix => + DesignConfig.brightness == Brightness.dark ? 'dark' : 'light'; +} diff --git a/lib/main.dart b/lib/main.dart index 8c93413..3ed3728 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,19 +1,62 @@ +import 'package:bot_toast/bot_toast.dart'; +import 'package:didvan/config/theme_data.dart'; +import 'package:didvan/providers/theme_provider.dart'; +import 'package:didvan/providers/user_provider.dart'; +import 'package:didvan/routes/route_generator.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; +import 'package:provider/provider.dart'; void main() { - runApp(const MyApp()); + runApp(const Didvan()); } -class MyApp extends StatelessWidget { - const MyApp({Key? key}) : super(key: key); +final GlobalKey navigatorKey = GlobalKey(); +final GlobalKey materialKey = GlobalKey(); - // This widget is the root of your application. +class Didvan extends StatelessWidget { + const Didvan({Key? key}) : super(key: key); @override Widget build(BuildContext context) { - return MaterialApp( - title: 'Didvan', - theme: ThemeData( - primarySwatch: Colors.blue, + return MultiProvider( + providers: [ + ChangeNotifierProvider( + create: (context) => UserProvider(), + ), + ChangeNotifierProvider( + create: (context) => ThemeProvider(), + ), + ], + child: Consumer( + builder: (context, themeProvider, child) => MaterialApp( + key: materialKey, + navigatorKey: navigatorKey, + debugShowCheckedModeBanner: false, + title: 'Didvan', + theme: LightThemeConfig.themeData.copyWith( + textTheme: LightThemeConfig.themeData.textTheme.apply( + fontFamily: themeProvider.fontFamily, + )), + darkTheme: DarkThemeConfig.themeData.copyWith( + textTheme: DarkThemeConfig.themeData.textTheme.apply( + fontFamily: themeProvider.fontFamily, + )), + color: LightThemeConfig.themeData.primaryColor, + themeMode: themeProvider.themeMode, + onGenerateRoute: (settings) => RouteGenerator.generateRoute(settings), + builder: BotToastInit(), //1. call BotToastInit + navigatorObservers: [BotToastNavigatorObserver()], + initialRoute: '/', + localizationsDelegates: const [ + GlobalCupertinoLocalizations.delegate, + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + ], + supportedLocales: const [ + Locale("fa", "IR"), + ], + locale: const Locale("fa", "IR"), + ), ), ); } diff --git a/lib/models/category.dart b/lib/models/category.dart new file mode 100644 index 0000000..3faf75a --- /dev/null +++ b/lib/models/category.dart @@ -0,0 +1,16 @@ +class CategoryData { + final int id; + final String label; + + const CategoryData({required this.id, required this.label}); + + factory CategoryData.fromJson(Map json) => CategoryData( + id: json['id'], + label: json['label'], + ); + + Map toJson() => { + 'id': id, + 'label': label, + }; +} diff --git a/lib/models/chat_room/chat_room.dart b/lib/models/chat_room/chat_room.dart new file mode 100644 index 0000000..99457eb --- /dev/null +++ b/lib/models/chat_room/chat_room.dart @@ -0,0 +1,33 @@ +import 'last_message.dart'; + +class ChatRoom { + final int id; + final String type; + final String updatedAt; + int unread; + final LastMessage lastMessage; + + ChatRoom({ + required this.id, + required this.type, + required this.updatedAt, + required this.unread, + required this.lastMessage, + }); + + factory ChatRoom.fromJson(Map json) => ChatRoom( + id: json['id'], + type: json['type'], + updatedAt: json['updatedAt'], + unread: json['unread'], + lastMessage: LastMessage.fromJson(json['lastMessage']), + ); + + Map toJson() => { + 'id': id, + 'type': type, + 'updatedAt': updatedAt, + 'unread': unread, + 'lastMessage': lastMessage.toJson(), + }; +} diff --git a/lib/models/chat_room/last_message.dart b/lib/models/chat_room/last_message.dart new file mode 100644 index 0000000..23c9f3f --- /dev/null +++ b/lib/models/chat_room/last_message.dart @@ -0,0 +1,27 @@ +class LastMessage { + final String? text; + final String? audio; + final bool writedByAdmin; + final bool readed; + + const LastMessage({ + this.text, + this.audio, + required this.writedByAdmin, + required this.readed, + }); + + factory LastMessage.fromJson(Map json) => LastMessage( + text: json['text'], + audio: json['audio'], + writedByAdmin: json['writedByAdmin'], + readed: json['readed'], + ); + + Map toJson() => { + 'text': text, + 'audio': audio, + 'writedByAdmin': writedByAdmin, + 'readed': readed, + }; +} diff --git a/lib/models/comment/comment.dart b/lib/models/comment/comment.dart new file mode 100644 index 0000000..b4f43ea --- /dev/null +++ b/lib/models/comment/comment.dart @@ -0,0 +1,51 @@ +import 'feedback.dart'; +import 'reply.dart'; +import 'user.dart'; + +class CommentData { + int id; + final String text; + final String createdAt; + final bool liked; + final bool disliked; + FeedbackData feedback; + final UserOverview user; + final List replies; + + CommentData({ + required this.id, + required this.text, + required this.createdAt, + required this.liked, + required this.disliked, + required this.feedback, + required this.user, + required this.replies, + }); + + factory CommentData.fromJson(Map json) => CommentData( + id: json['id'], + text: json['text'], + createdAt: json['createdAt'], + liked: json['liked'], + disliked: json['disliked'], + feedback: FeedbackData.fromJson(json['feedback']), + user: UserOverview.fromJson(json['user']), + replies: List.from( + json['replies'].map( + (reply) => Reply.fromJson(reply), + ), + ), + ); + + Map toJson() => { + 'id': id, + 'text': text, + 'createdAt': createdAt, + 'liked': liked, + 'disliked': disliked, + 'feedback': feedback.toJson(), + 'user': user.toJson(), + 'replies': replies.map((e) => e.toJson()).toList(), + }; +} diff --git a/lib/models/comment/feedback.dart b/lib/models/comment/feedback.dart new file mode 100644 index 0000000..d51d67f --- /dev/null +++ b/lib/models/comment/feedback.dart @@ -0,0 +1,16 @@ +class FeedbackData { + final int like; + final int dislike; + + const FeedbackData({required this.like, required this.dislike}); + + factory FeedbackData.fromJson(Map json) => FeedbackData( + like: json['like'], + dislike: json['dislike'], + ); + + Map toJson() => { + 'like': like, + 'dislike': dislike, + }; +} diff --git a/lib/models/comment/reply.dart b/lib/models/comment/reply.dart new file mode 100644 index 0000000..e2fb35d --- /dev/null +++ b/lib/models/comment/reply.dart @@ -0,0 +1,46 @@ +import 'feedback.dart'; +import 'user.dart'; + +class Reply { + int id; + final String text; + final String createdAt; + final bool liked; + final bool disliked; + final FeedbackData feedback; + final UserOverview user; + final UserOverview toUser; + + Reply({ + required this.id, + required this.text, + required this.createdAt, + required this.liked, + required this.disliked, + required this.feedback, + required this.user, + required this.toUser, + }); + + factory Reply.fromJson(Map json) => Reply( + id: json['id'], + text: json['text'], + createdAt: json['createdAt'], + liked: json['liked'], + disliked: json['disliked'], + feedback: FeedbackData.fromJson(json['feedback']), + user: UserOverview.fromJson(json['user']), + toUser: UserOverview.fromJson(json['toUser']), + ); + + Map toJson() => { + 'id': id, + 'text': text, + 'createdAt': createdAt, + 'liked': liked, + 'disliked': disliked, + 'feedback': feedback.toJson(), + 'user': user.toJson(), + 'toUser': toUser.toJson(), + }; +} diff --git a/lib/models/comment/user.dart b/lib/models/comment/user.dart new file mode 100644 index 0000000..3483134 --- /dev/null +++ b/lib/models/comment/user.dart @@ -0,0 +1,20 @@ +class UserOverview { + final int id; + final String fullName; + final String? photo; + + const UserOverview( + {required this.id, required this.fullName, required this.photo}); + + factory UserOverview.fromJson(Map json) => UserOverview( + id: json['id'], + fullName: json['fullName'], + photo: json['photo'], + ); + + Map toJson() => { + 'id': id, + 'fullName': fullName, + 'photo': photo, + }; +} diff --git a/lib/models/content.dart b/lib/models/content.dart new file mode 100644 index 0000000..3ec25fe --- /dev/null +++ b/lib/models/content.dart @@ -0,0 +1,39 @@ +class Content { + final int order; + final String? text; + final String? audio; + final String? video; + final String? image; + final String? largeImage; + final String? caption; + + const Content({ + required this.order, + required this.text, + required this.audio, + required this.video, + required this.image, + required this.largeImage, + required this.caption, + }); + + factory Content.fromJson(Map json) => Content( + order: json['order'], + text: json['text'], + audio: json['audio'], + video: json['video'], + image: json['image'], + largeImage: json['largeImage'], + caption: json['caption'], + ); + + Map toJson() => { + 'order': order, + 'text': text, + 'audio': audio, + 'video': video, + 'image': image, + 'largeImage': largeImage, + 'caption': caption, + }; +} diff --git a/lib/models/enums.dart b/lib/models/enums.dart new file mode 100644 index 0000000..a0f16db --- /dev/null +++ b/lib/models/enums.dart @@ -0,0 +1,18 @@ +enum AppState { + idle, + busy, + isolatedBusy, + failed, +} + +enum ButtonStyleMode { + primary, + secondary, + flat, +} + +enum ALertType { + error, + info, + success, +} diff --git a/lib/models/message_data/message_data.dart b/lib/models/message_data/message_data.dart new file mode 100644 index 0000000..1778abc --- /dev/null +++ b/lib/models/message_data/message_data.dart @@ -0,0 +1,53 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'radar_attachment.dart'; + +class MessageData { + final int id; + final String? text; + final String? audio; + final bool writedByAdmin; + final bool readed; + final String createdAt; + final RadarAttachment? radar; + final File? audioFile; + final int? audioDuration; + + const MessageData({ + required this.id, + required this.writedByAdmin, + required this.readed, + required this.createdAt, + this.text, + this.audio, + this.radar, + this.audioFile, + this.audioDuration, + }); + + factory MessageData.fromJson(Map json) => MessageData( + id: json['id'], + text: json['text'], + audio: json['audio'], + writedByAdmin: json['writedByAdmin'], + readed: json['readed'], + createdAt: json['createdAt'], + audioDuration: json['waveform'] == null + ? null + : jsonDecode(json['waveform'])['duration'] ?? 0, + radar: json['radar'] == null + ? null + : RadarAttachment.fromJson(json['radar']), + ); + + Map toJson() => { + 'id': id, + 'text': text, + 'audio': audio, + 'writedByAdmin': writedByAdmin, + 'readed': readed, + 'createdAt': createdAt, + 'radar': radar?.toJson(), + }; +} diff --git a/lib/models/message_data/radar_attachment.dart b/lib/models/message_data/radar_attachment.dart new file mode 100644 index 0000000..6e55f2d --- /dev/null +++ b/lib/models/message_data/radar_attachment.dart @@ -0,0 +1,48 @@ +import 'package:didvan/models/category.dart'; + +class RadarAttachment { + final int id; + final String title; + final String description; + final int timeToRead; + final String image; + final bool forManagers; + final String createdAt; + final List categories; + + const RadarAttachment({ + required this.id, + required this.title, + required this.description, + required this.timeToRead, + required this.image, + required this.forManagers, + required this.categories, + required this.createdAt, + }); + + factory RadarAttachment.fromJson(Map json) => + RadarAttachment( + id: json['id'], + title: json['title'], + description: json['description'], + timeToRead: json['timeToRead'], + image: json['image'], + createdAt: json['createdAt'], + forManagers: json['forManagers'], + categories: List.from( + json['categories'].map( + (cat) => CategoryData.fromJson(cat), + ), + ), + ); + + Map toJson() => { + 'id': id, + 'title': title, + 'description': description, + 'timeToRead': timeToRead, + 'image': image, + 'forManagers': forManagers, + }; +} diff --git a/lib/models/news_data.dart b/lib/models/news_data.dart new file mode 100644 index 0000000..9b99788 --- /dev/null +++ b/lib/models/news_data.dart @@ -0,0 +1,38 @@ +import 'package:flutter/foundation.dart'; + +@immutable +class NewsData { + final int id; + final String title; + final String reference; + final String description; + final String createdAt; + final String updatedAt; + + const NewsData({ + required this.id, + required this.title, + required this.reference, + required this.description, + required this.createdAt, + required this.updatedAt, + }); + + factory NewsData.fromJson(Map json) => NewsData( + id: json['id'], + title: json['title'], + reference: json['reference'], + description: json['description'], + createdAt: json['createdAt'], + updatedAt: json['updatedAt'], + ); + + Map toJson() => { + 'id': id, + 'title': title, + 'reference': reference, + 'description': description, + 'createdAt': createdAt, + 'updatedAt': updatedAt, + }; +} diff --git a/lib/models/news_details_data.dart b/lib/models/news_details_data.dart new file mode 100644 index 0000000..0a1a68b --- /dev/null +++ b/lib/models/news_details_data.dart @@ -0,0 +1,60 @@ +import 'package:didvan/models/content.dart'; +import 'package:didvan/models/overview_data.dart'; +import 'package:didvan/models/tag.dart'; + +class NewsDetailsData { + final int id; + final String title; + final String reference; + final String image; + final String createdAt; + final bool marked; + int comments; + final int order; + final List tags; + final List contents; + final List relatedContents = []; + + NewsDetailsData({ + required this.id, + required this.title, + required this.reference, + required this.image, + required this.createdAt, + required this.marked, + required this.comments, + required this.tags, + required this.contents, + required this.order, + }); + + factory NewsDetailsData.fromJson(Map json) { + return NewsDetailsData( + id: json['id'], + title: json['title'], + reference: json['reference'], + image: json['image'], + createdAt: json['createdAt'], + marked: json['marked'], + comments: json['comments'], + order: json['order'], + tags: List.from(json['tags'].map((tag) => Tag.fromJson(tag))), + contents: List.from(json['contents'].map( + (content) => Content.fromJson(content), + )), + ); + } + + Map toJson() => { + 'id': id, + 'title': title, + 'order': order, + 'reference': reference, + 'image': image, + 'createdAt': createdAt, + 'marked': marked, + 'comments': comments, + 'tags': tags.map((e) => e.toJson()).toList(), + 'contents': contents.map((e) => e.toJson()).toList(), + }; +} diff --git a/lib/models/overview_data.dart b/lib/models/overview_data.dart new file mode 100644 index 0000000..4d16449 --- /dev/null +++ b/lib/models/overview_data.dart @@ -0,0 +1,61 @@ +import 'package:didvan/models/category.dart'; + +class OverviewData { + final int id; + final String title; + final String image; + final String description; + final int? timeToRead; + final String? reference; + final bool forManagers; + final String createdAt; + final String type; + int comments; + bool marked; + final List? categories; + + OverviewData({ + required this.id, + required this.title, + required this.image, + required this.description, + required this.createdAt, + required this.type, + required this.marked, + required this.comments, + required this.forManagers, + this.timeToRead, + this.reference, + this.categories, + }); + + factory OverviewData.fromJson(Map json) => OverviewData( + id: json['id'], + title: json['title'], + image: json['image'], + description: json['description'], + timeToRead: json['timeToRead'], + reference: json['reference'], + forManagers: json['forManagers'] ?? false, + comments: json['comments'] ?? 0, + createdAt: json['createdAt'], + type: json['type'] ?? '', + marked: json['marked'] ?? false, + categories: (json['categories'] as List?) + ?.map((e) => CategoryData.fromJson(e as Map)) + .toList(), + ); + + Map toJson() => { + 'id': id, + 'title': title, + 'image': image, + 'description': description, + 'timeToRead': timeToRead, + 'reference': reference, + 'forManagers': forManagers, + 'createdAt': createdAt, + 'type': type, + 'categories': categories?.map((e) => e.toJson()).toList(), + }; +} diff --git a/lib/models/radar_details_data.dart b/lib/models/radar_details_data.dart new file mode 100644 index 0000000..a41bb1e --- /dev/null +++ b/lib/models/radar_details_data.dart @@ -0,0 +1,80 @@ +import 'package:didvan/models/category.dart'; +import 'package:didvan/models/overview_data.dart'; + +import 'content.dart'; +import 'tag.dart'; + +class RadarDetailsData { + final int id; + final String title; + final String image; + final int timeToRead; + final String createdAt; + final String? podcast; + final bool forManagers; + final bool marked; + int comments; + final List tags; + final List contents; + final List categories; + final int order; + final List relatedContents = []; + + RadarDetailsData({ + required this.id, + required this.title, + required this.image, + required this.timeToRead, + required this.createdAt, + required this.podcast, + required this.forManagers, + required this.marked, + required this.comments, + required this.tags, + required this.contents, + required this.categories, + required this.order, + }); + + factory RadarDetailsData.fromJson(Map json) { + return RadarDetailsData( + id: json['id'], + title: json['title'], + image: json['image'], + timeToRead: json['timeToRead'], + createdAt: json['createdAt'], + podcast: json['podcast'], + forManagers: json['forManagers'], + marked: json['marked'], + order: json['order'], + comments: json['comments'], + tags: List.from(json['tags'].map((tag) => Tag.fromJson(tag))), + contents: List.from( + json['contents'].map( + (content) => Content.fromJson(content), + ), + ), + categories: List.from( + json['categories'].map( + (cat) => CategoryData.fromJson(cat), + ), + ), + ); + } + + Map toJson() => { + 'id': id, + 'title': title, + 'image': image, + 'timeToRead': timeToRead, + 'createdAt': createdAt, + 'podcast': podcast, + 'forManagers': forManagers, + 'marked': marked, + 'order': order, + 'comments': comments, + 'tags': tags.map((e) => e.toJson()).toList(), + 'contents': contents.map((e) => e.toJson()).toList(), + 'categories': categories.map((e) => e.toJson()).toList(), + }; +} diff --git a/lib/models/requests/news.dart b/lib/models/requests/news.dart new file mode 100644 index 0000000..1e17e56 --- /dev/null +++ b/lib/models/requests/news.dart @@ -0,0 +1,13 @@ +class NewsRequestArgs { + final int page; + final String? startDate; + final String? endDate; + final String? search; + + const NewsRequestArgs({ + required this.page, + this.startDate, + this.endDate, + this.search, + }); +} diff --git a/lib/models/requests/radar.dart b/lib/models/requests/radar.dart new file mode 100644 index 0000000..12e7e24 --- /dev/null +++ b/lib/models/requests/radar.dart @@ -0,0 +1,17 @@ +class RadarRequestArgs { + final int page; + final List categories; + final String? startDate; + final String? endDate; + final String? search; + final bool? isSingleItem; + + const RadarRequestArgs({ + required this.page, + this.categories = const [], + this.startDate, + this.endDate, + this.search, + this.isSingleItem, + }); +} diff --git a/lib/models/settings_data.dart b/lib/models/settings_data.dart new file mode 100644 index 0000000..67e2f26 --- /dev/null +++ b/lib/models/settings_data.dart @@ -0,0 +1,13 @@ +import 'package:flutter/material.dart'; + +class SettingsData { + final String fontFamily; + final double fontScale; + final ThemeMode themeMode; + + SettingsData({ + required this.fontFamily, + required this.fontScale, + required this.themeMode, + }); +} diff --git a/lib/models/tag.dart b/lib/models/tag.dart new file mode 100644 index 0000000..34e690a --- /dev/null +++ b/lib/models/tag.dart @@ -0,0 +1,16 @@ +class Tag { + final int id; + final String label; + + const Tag({required this.id, required this.label}); + + factory Tag.fromJson(Map json) => Tag( + id: json['id'], + label: json['label'], + ); + + Map toJson() => { + 'id': id, + 'label': label, + }; +} diff --git a/lib/models/user.dart b/lib/models/user.dart new file mode 100644 index 0000000..92011db --- /dev/null +++ b/lib/models/user.dart @@ -0,0 +1,53 @@ +class User { + final int id; + final String phoneNumber; + final String fullName; + final String? username; + final String? photo; + final String? email; + + const User({ + required this.id, + required this.username, + required this.phoneNumber, + required this.photo, + required this.fullName, + required this.email, + }); + + factory User.fromJson(Map json) => User( + id: json['id'], + username: json['username'], + phoneNumber: json['phoneNumber'], + photo: json['photo'], + fullName: json['fullName'], + email: json['email'], + ); + + Map toJson() => { + 'id': id, + 'username': username, + 'phoneNumber': phoneNumber, + 'photo': photo, + 'fullName': fullName, + 'email': email, + }; + + User copyWith({ + int? id, + String? username, + String? phoneNumber, + String? photo, + String? fullName, + String? email, + }) { + return User( + id: id ?? this.id, + username: this.username, + phoneNumber: phoneNumber ?? this.phoneNumber, + photo: photo, + fullName: fullName ?? this.fullName, + email: email, + ); + } +} diff --git a/lib/models/view/action_sheet_data.dart b/lib/models/view/action_sheet_data.dart new file mode 100644 index 0000000..6166bb6 --- /dev/null +++ b/lib/models/view/action_sheet_data.dart @@ -0,0 +1,31 @@ +import 'package:flutter/cupertino.dart'; + +class ActionSheetData { + final Widget content; + final String? confrimTitle; + final String? dismissTitle; + final VoidCallback? onConfirmed; + final VoidCallback? onDismissed; + final String title; + final IconData? titleIcon; + final Color? titleColor; + final bool hasDismissButton; + final bool hasConfirmButton; + final bool withoutButtonMode; + final bool smallDismissButton; + + const ActionSheetData({ + required this.content, + required this.title, + this.confrimTitle, + this.onConfirmed, + this.titleColor, + this.hasDismissButton = true, + this.hasConfirmButton = true, + this.titleIcon, + this.dismissTitle, + this.onDismissed, + this.smallDismissButton = false, + this.withoutButtonMode = false, + }); +} diff --git a/lib/models/view/alert_data.dart b/lib/models/view/alert_data.dart new file mode 100644 index 0000000..1d4edc4 --- /dev/null +++ b/lib/models/view/alert_data.dart @@ -0,0 +1,8 @@ +import 'package:didvan/models/enums.dart'; + +class AlertData { + final String message; + final ALertType aLertType; + + AlertData({required this.message, this.aLertType = ALertType.error}); +} diff --git a/lib/models/view/app_bar_data.dart b/lib/models/view/app_bar_data.dart new file mode 100644 index 0000000..9e00aa5 --- /dev/null +++ b/lib/models/view/app_bar_data.dart @@ -0,0 +1,10 @@ +import 'package:flutter/material.dart'; + +class AppBarData { + final String? title; + final String? subtitle; + final bool hasBack; + final Widget? trailing; + + AppBarData({this.title, this.subtitle, this.hasBack = false, this.trailing}); +} diff --git a/lib/models/view/radar_category.dart b/lib/models/view/radar_category.dart new file mode 100644 index 0000000..4312d46 --- /dev/null +++ b/lib/models/view/radar_category.dart @@ -0,0 +1,7 @@ +class RadarCategory { + final int id; + final String title; + final String asset; + + RadarCategory({required this.id, required this.title, required this.asset}); +} diff --git a/lib/providers/core_provider.dart b/lib/providers/core_provider.dart new file mode 100644 index 0000000..820387d --- /dev/null +++ b/lib/providers/core_provider.dart @@ -0,0 +1,22 @@ +import 'package:didvan/models/enums.dart'; +import 'package:didvan/utils/action_sheet.dart'; +import 'package:flutter/cupertino.dart'; + +class CoreProvier with ChangeNotifier { + AppState _appState = AppState.idle; + + set appState(AppState newState) { + if (newState == AppState.isolatedBusy) { + ActionSheetUtils.showLogoLoadingIndicator(); + } + if (_appState == AppState.isolatedBusy) { + ActionSheetUtils.pop(); + } + _appState = newState; + notifyListeners(); + } + + void update() => notifyListeners(); + + AppState get appState => _appState; +} diff --git a/lib/providers/server_data_provider.dart b/lib/providers/server_data_provider.dart new file mode 100644 index 0000000..3b76c8f --- /dev/null +++ b/lib/providers/server_data_provider.dart @@ -0,0 +1,27 @@ +import 'package:didvan/services/network/request.dart'; +import 'package:didvan/services/network/request_helper.dart'; + +class ServerDataProvider { + static final List directTypes = []; + + static Future getData() async { + await _getDirectTypes(); + } + + static int labelToTypeId(String? label) => label == null + ? 7 + : directTypes.firstWhere((element) => element.value.contains(label)).key; + + static Future _getDirectTypes() async { + final service = RequestService(RequestHelper.directTypes); + await service.httpGet(); + if (service.isSuccess) { + final types = service.result['types']; + for (var i = 0; i < types.length; i++) { + directTypes.add(MapEntry(types[i]['id'], types[i]['label'])); + } + } else { + throw 'Fetching direct types failed!'; + } + } +} diff --git a/lib/providers/settings_provider.dart b/lib/providers/settings_provider.dart new file mode 100644 index 0000000..d49f51f --- /dev/null +++ b/lib/providers/settings_provider.dart @@ -0,0 +1,13 @@ +import 'package:didvan/providers/core_provider.dart'; +import 'package:flutter/material.dart'; + +class SettingsProvider extends CoreProvier { + ThemeMode _themeMode = ThemeMode.light; + + set themeMode(ThemeMode themeMode) { + _themeMode = themeMode; + notifyListeners(); + } + + ThemeMode get themeMode => _themeMode; +} diff --git a/lib/providers/theme_provider.dart b/lib/providers/theme_provider.dart new file mode 100644 index 0000000..b994b5c --- /dev/null +++ b/lib/providers/theme_provider.dart @@ -0,0 +1,29 @@ +import 'package:didvan/providers/core_provider.dart'; +import 'package:flutter/material.dart'; + +class ThemeProvider extends CoreProvier { + ThemeMode _themeMode = ThemeMode.system; + String _fontFamily = 'Dana-FA'; + double _fontScale = 1; + + set themeMode(ThemeMode value) { + _themeMode = value; + notifyListeners(); + } + + ThemeMode get themeMode => _themeMode; + + set fontFamily(String value) { + _fontFamily = value; + notifyListeners(); + } + + String get fontFamily => _fontFamily; + + set fontScale(double value) { + _fontScale = value; + notifyListeners(); + } + + double get fontScale => _fontScale; +} diff --git a/lib/providers/user_provider.dart b/lib/providers/user_provider.dart new file mode 100644 index 0000000..65aeacf --- /dev/null +++ b/lib/providers/user_provider.dart @@ -0,0 +1,156 @@ +import 'package:collection/collection.dart'; +import 'package:didvan/models/enums.dart'; +import 'package:didvan/models/user.dart'; +import 'package:didvan/models/view/alert_data.dart'; +import 'package:didvan/providers/core_provider.dart'; +import 'package:didvan/services/network/request.dart'; +import 'package:didvan/services/network/request_helper.dart'; +import 'package:didvan/services/storage/storage.dart'; +import 'package:didvan/utils/action_sheet.dart'; + +class UserProvider extends CoreProvier { + late User user; + bool isAuthenticated = false; + + static final List _radarMarkQueue = []; + static final List _newsMarkQueue = []; + + Future setAndGetToken({String? newToken}) async { + if (newToken == null) { + final token = await StorageService.getValue(key: 'token'); + return token; + } + await StorageService.setValue(key: 'token', value: newToken); + return null; + } + + Future getUserInfo() async { + isAuthenticated = true; + final RequestService service = RequestService(RequestHelper.userInfo); + await service.httpGet(); + if (service.isSuccess) { + user = User.fromJson(service.result['user']); + return; + } + throw 'Getting user from API failed!'; + } + + Future setProfilePhoto(dynamic file) async { + appState = AppState.isolatedBusy; + final RequestService service = + RequestService(RequestHelper.updateProfilePhoto); + await service.multipart( + file: file, + method: 'PUT', + fileName: 'user-profile', + fieldName: 'photo', + mediaExtension: 'jpg', + mediaFormat: 'image', + ); + if (service.isSuccess) { + user = user.copyWith(photo: service.result['photo']); + appState = AppState.idle; + return true; + } + appState = AppState.failed; + return false; + } + + Future deleteProfilePhoto() async { + appState = AppState.isolatedBusy; + final RequestService service = + RequestService(RequestHelper.updateProfilePhoto); + await service.delete(); + if (service.isSuccess) { + user = user.copyWith(photo: null); + appState = AppState.idle; + return true; + } + appState = AppState.idle; + return false; + } + + Future checkUsername(String username) async { + if (user.username == username) return true; + final RequestService service = RequestService( + RequestHelper.checkUsername, + body: {'username': username}, + ); + await service.post(); + if (service.isSuccess) { + return service.result['available']; + } + return null; + } + + Future editProfile( + String fullName, + String? username, + String? email, + ) async { + appState = AppState.isolatedBusy; + final service = RequestService( + RequestHelper.updateProfile, + body: { + 'fullName': fullName, + 'email': email, + 'username': username, + }, + ); + await service.put(); + if (service.isSuccess) { + user = user.copyWith( + fullName: fullName, + email: email, + username: username, + ); + appState = AppState.idle; + ActionSheetUtils.pop(); + ActionSheetUtils.showAlert( + AlertData( + message: 'پروفایل با موفقیت ویرایش شد', + aLertType: ALertType.success, + ), + ); + return; + } + appState = AppState.idle; + ActionSheetUtils.showAlert( + AlertData( + message: service.errorMessage, + ), + ); + } + + static Future changeRadarMark(int id, bool value) async { + _radarMarkQueue.add(MapEntry(id, value)); + Future.delayed(const Duration(milliseconds: 500), () async { + final MapEntry? lastChange = + _radarMarkQueue.lastWhereOrNull((item) => item.key == id); + if (lastChange == null) return; + final service = RequestService(RequestHelper.markRadar(id)); + if (lastChange.value) { + await service.post(); + } else { + await service.delete(); + } + _radarMarkQueue.removeWhere((element) => element.key == id); + }); + } + + static Future changeNewsMark(int id, bool value) async { + _newsMarkQueue.add(MapEntry(id, value)); + Future.delayed(const Duration(milliseconds: 500), () async { + final MapEntry? lastChange = + _newsMarkQueue.lastWhereOrNull((item) => item.key == id); + if (lastChange == null) return; + final service = RequestService(RequestHelper.markNews(id)); + if (lastChange.value) { + await service.post(); + } else { + await service.delete(); + } + _newsMarkQueue.removeWhere((element) => element.key == id); + }); + } +} diff --git a/lib/routes/route_generator.dart b/lib/routes/route_generator.dart new file mode 100644 index 0000000..205d9b7 --- /dev/null +++ b/lib/routes/route_generator.dart @@ -0,0 +1,185 @@ +import 'package:didvan/models/tag.dart'; +import 'package:didvan/views/authentication/authentication.dart'; +import 'package:didvan/views/authentication/authentication_state.dart'; +import 'package:didvan/views/home/comments/comments.dart'; +import 'package:didvan/views/home/comments/comments_state.dart'; +import 'package:didvan/views/home/direct/direct.dart'; +import 'package:didvan/views/home/direct/direct_state.dart'; +import 'package:didvan/views/home/hashtag/hashtag.dart'; +import 'package:didvan/views/home/hashtag/hashtag_state.dart'; +import 'package:didvan/views/home/home.dart'; +import 'package:didvan/views/home/home_state.dart'; +import 'package:didvan/views/home/news/news_details/news_details.dart'; +import 'package:didvan/views/home/news/news_details/news_details_state.dart'; +import 'package:didvan/views/home/news/news_state.dart'; +import 'package:didvan/views/home/radar/radar_details/radar_details.dart'; +import 'package:didvan/views/home/radar/radar_details/radar_details_state.dart'; +import 'package:didvan/views/home/radar/radar_state.dart'; +import 'package:didvan/views/home/settings/about_us/about_us.dart'; +import 'package:didvan/views/home/settings/bookmarks/bookmarks.dart'; +import 'package:didvan/views/home/settings/bookmarks/bookmark_state.dart'; +import 'package:didvan/views/home/settings/bookmarks/filtered_bookmark/filtered_bookmark.dart'; +import 'package:didvan/views/home/settings/bookmarks/filtered_bookmark/filtered_bookmarks_state.dart'; +import 'package:didvan/views/home/settings/direct_list/direct_list.dart'; +import 'package:didvan/views/home/settings/direct_list/direct_list_state.dart'; +import 'package:didvan/views/home/settings/general_settings/settings.dart'; +import 'package:didvan/views/home/settings/general_settings/settings_state.dart'; +import 'package:didvan/views/home/settings/profile/profile.dart'; +import 'package:didvan/views/home/studio/studio_state.dart'; +import 'package:didvan/views/splash/splash.dart'; +import 'package:didvan/routes/routes.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +class RouteGenerator { + static Route generateRoute(RouteSettings settings) { + switch (settings.name) { + case Routes.splash: + return _createRoute( + const Splash(), + ); + case Routes.authenticaion: + return _createRoute( + ChangeNotifierProvider( + create: (context) => AuthenticationState(), + child: Authentication(isResetPassword: settings.arguments as bool), + ), + ); + case Routes.home: + return _createRoute( + MultiProvider( + providers: [ + ChangeNotifierProvider( + create: (context) => HomeState(), + ), + ChangeNotifierProvider( + create: (context) => RadarState(), + ), + ChangeNotifierProvider( + create: (context) => NewsState(), + ), + ChangeNotifierProvider( + create: (context) => StudioState(), + ), + ], + child: const Home(), + ), + ); + case Routes.profile: + return _createRoute( + const Profile(), + ); + case Routes.aboutUs: + return _createRoute( + const AboutUs(), + ); + case Routes.generalSettings: + return _createRoute( + ChangeNotifierProvider( + create: (context) => GeneralSettingsState(), + child: const GeneralSettings(), + ), + ); + case Routes.radarDetails: + return _createRoute( + ChangeNotifierProvider( + create: (context) => RadarDetailsState(), + child: RadarDetails( + pageData: settings.arguments as Map, + ), + ), + ); + case Routes.newsDetails: + return _createRoute( + ChangeNotifierProvider( + create: (context) => NewsDetailsState(), + child: NewsDetails( + pageData: settings.arguments as Map, + ), + ), + ); + case Routes.directList: + return _createRoute( + ChangeNotifierProvider( + create: (context) => DirectListState(), + child: const DirectList(), + ), + ); + case Routes.direct: + return _createRoute( + ChangeNotifierProvider( + create: (context) => DirectState(), + child: Direct(pageData: settings.arguments as Map), + ), + ); + case Routes.comments: + return _createRoute( + ChangeNotifierProvider( + create: (context) => CommentsState(), + child: Comments( + pageData: settings.arguments as Map, + ), + ), + ); + case Routes.bookmarks: + return _createRoute( + ChangeNotifierProvider( + create: (context) => BookmarksState(), + child: const Bookmarks(), + ), + ); + case Routes.hashtag: + return _createRoute( + ChangeNotifierProvider( + create: (context) => HashtagState(), + child: Hashtag(tag: settings.arguments as Tag), + ), + ); + + case Routes.filteredBookmarks: + return _createRoute( + ChangeNotifierProvider( + create: (context) => FilteredBookmarksState( + settings.arguments as String, + ), + child: const FilteredBookmarks(), + ), + ); + default: + return _errorRoute(); + } + } + + static Route _errorRoute() { + return MaterialPageRoute(builder: (_) { + return Scaffold( + appBar: AppBar( + title: const Text('Error'), + ), + body: const Center( + child: Text('ERROR'), + ), + ); + }); + } + + static Route _createRoute(page) { + return MaterialPageRoute( + builder: (context) { + final shortestSide = MediaQuery.of(context).size.shortestSide; + final bool useMobileLayout = shortestSide < 600; + if (kIsWeb && !useMobileLayout) { + return Center(child: AspectRatio(aspectRatio: 9 / 16, child: page)); + } + return Container( + color: Theme.of(context).colorScheme.surface, + child: SafeArea( + child: page, + top: false, + ), + ); + }, + ); + } +} diff --git a/lib/routes/routes.dart b/lib/routes/routes.dart new file mode 100644 index 0000000..7cbab35 --- /dev/null +++ b/lib/routes/routes.dart @@ -0,0 +1,18 @@ +class Routes { + static const String splash = '/'; + static const String home = '/home'; + static const String aboutUs = '/about-us'; + static const String authenticaion = '/authentication'; + static const String profile = '/profile'; + static const String settings = '/settings'; + static const String generalSettings = '/general-settings'; + static const String radarDetails = '/radar-details'; + static const String newsDetails = '/news-details'; + static const String directList = '/direct-list'; + static const String direct = '/direct'; + static const String comments = '/comments'; + static const String bookmarks = '/bookmarks'; + static const String filteredBookmarks = '/filtered-bookmarks'; + static const String imageCropper = '/image-cropper'; + static const String hashtag = '/hashtag'; +} diff --git a/lib/services/app_initalizer.dart b/lib/services/app_initalizer.dart new file mode 100644 index 0000000..56d5f3f --- /dev/null +++ b/lib/services/app_initalizer.dart @@ -0,0 +1,87 @@ +import 'package:didvan/models/settings_data.dart'; +import 'package:didvan/services/media/media.dart'; +import 'package:didvan/services/storage/storage.dart'; +import 'package:firebase_core/firebase_core.dart'; +import 'package:firebase_messaging/firebase_messaging.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:path_provider/path_provider.dart'; + +class AppInitializer { + static Future setupServices() async { + if (!kIsWeb) { + StorageService.appDocsDir = + (await getApplicationDocumentsDirectory()).path; + StorageService.appTempsDir = (await getTemporaryDirectory()).path; + await _initializeFirebase(); + MediaService.init(); + } + } + + static Future initilizeSettings() async { + final brightness = await StorageService.getValue(key: 'brightness'); + if (brightness != null) { + final themeMode = brightness == 'dark' ? ThemeMode.dark : ThemeMode.light; + final fontFamily = await StorageService.getValue(key: 'fontFamily'); + final fontScale = double.parse( + await StorageService.getValue(key: 'fontSizeScale'), + ); + return SettingsData( + fontFamily: fontFamily, + fontScale: fontScale, + themeMode: themeMode, + ); + } else { + await StorageService.setValue( + key: 'notificationTimeRangeStart', + value: '00:00', + ); + await StorageService.setValue( + key: 'notificationTimeRangeEnd', + value: '23:59', + ); + await StorageService.setValue( + key: 'fontFamily', + value: 'Dana-FA', + ); + await StorageService.setValue( + key: 'fontSizeScale', + value: '1', + ); + await StorageService.setValue( + key: 'brightness', + value: 'light', + ); + return SettingsData( + fontFamily: 'Dana-FA', + fontScale: 1, + themeMode: ThemeMode.light, + ); + } + } + + static Future _initializeFirebase() async { + try { + await Firebase.initializeApp( + options: const FirebaseOptions( + apiKey: 'AIzaSyBp-UHjWeM0H0UHtX5yguFKG-riMzvvCzw', + appId: '1:935017686266:android:f9cbc9aba8e3d65ed2d543', + messagingSenderId: '935017686266', + projectId: 'didvan-9b7da', + ), + ); + } catch (e) { + Firebase.app(); + } + final FirebaseMessaging fcm = FirebaseMessaging.instance; + await fcm.requestPermission( + alert: true, + announcement: false, + badge: true, + carPlay: false, + criticalAlert: false, + provisional: false, + sound: true, + ); + } +} diff --git a/lib/services/media/media.dart b/lib/services/media/media.dart new file mode 100644 index 0000000..fb0fe74 --- /dev/null +++ b/lib/services/media/media.dart @@ -0,0 +1,69 @@ +import 'package:didvan/services/network/request.dart'; +import 'package:didvan/services/network/request_helper.dart'; +import 'package:flutter/foundation.dart'; +import 'package:image_picker/image_picker.dart'; +import 'package:just_audio/just_audio.dart'; + +class MediaService { + static final AudioPlayer audioPlayer = AudioPlayer(); + static String? audioPlayerTag; + static String? audioPlayerTitle; + static String? audioPlayerCover; + + static void init() { + audioPlayer.positionStream.listen((event) { + if (audioPlayer.duration != null && audioPlayer.duration! < event) { + audioPlayer.stop(); + audioPlayer.seek(const Duration(seconds: 0)); + } + }); + } + + static Future handleAudioPlayback({ + required dynamic audioSource, + }) async { + bool isNetworkAudio = audioSource.runtimeType == String; + String tag; + if (isNetworkAudio) { + tag = audioSource; + } else { + tag = audioSource.path; + } + if (audioPlayerTag == tag) { + if (audioPlayer.playing) { + await audioPlayer.pause(); + } else { + await audioPlayer.play(); + } + } else { + await audioPlayer.stop(); + audioPlayerTag = tag; + if (isNetworkAudio) { + await audioPlayer.setUrl( + RequestHelper.baseUrl + + audioSource + + '?accessToken=${RequestService.token}', + ); + } else { + if (kIsWeb) { + await audioPlayer + .setUrl(audioSource!.uri.path.replaceAll('%3A', ':')); + } else { + await audioPlayer.setFilePath(audioSource.path); + } + } + audioPlayer.play(); + } + } + + static Future resetAudioPlayer() async { + audioPlayerTag = null; + MediaService.audioPlayer.stop(); + } + + static Future pickImage({required ImageSource source}) async { + final imagePicker = ImagePicker(); + final XFile? pickedFile = await imagePicker.pickImage(source: source); + return pickedFile; + } +} diff --git a/lib/services/network/request.dart b/lib/services/network/request.dart new file mode 100644 index 0000000..1614cbc --- /dev/null +++ b/lib/services/network/request.dart @@ -0,0 +1,213 @@ +import 'dart:convert'; +import 'dart:developer'; +import 'package:http/http.dart' as http; +import 'package:http_parser/http_parser.dart' as parser; + +class RequestService { + static late String token; + + Map get result => _body?['result'] ?? const {}; + Map get errors => _body?['errors'] ?? const {}; + + String errorMessage = + 'خطا! لطفا اتصال اینترنت خود را بررسی و مجددا تلاش نمایید.'; + + Map? _body; + + final Map _headers = { + "accept": "*/*", + "Content-Type": "application/json; charset=UTF-8", + }; + final String url; + Map? _requestBody; + + bool isSuccess = false; + + RequestService( + this.url, { + Map? requestHeaders, + body, + bool useAutherization = true, + }) { + if (body != null) _requestBody = body; + if (requestHeaders != null) _headers.addAll(requestHeaders); + if (useAutherization) _headers.addAll({'Authorization': 'Bearer $token'}); + if (body != null) _requestBody = body; + } + + String? get _generateBody => + _requestBody == null ? null : jsonEncode(_requestBody); + + Future httpGet() async { + try { + final response = await http + .get( + Uri.parse(url), + headers: _headers, + ) + .timeout( + const Duration(seconds: 10), + ) + .catchError( + (e) => throw e, + ); + _handleResponse(response); + } catch (e) { + _handleError(null); + } + } + + Future post() async { + try { + final response = await http + .post( + Uri.parse(url), + body: _generateBody, + headers: _headers, + ) + .timeout( + const Duration(seconds: 10), + ) + .catchError( + (e) { + throw e; + }, + ); + _handleResponse(response); + } catch (e) { + _handleError(null); + } + } + + Future delete() async { + try { + final response = await http + .delete( + Uri.parse(url), + body: _generateBody, + headers: _headers, + ) + .timeout( + const Duration(seconds: 10), + ) + .catchError( + (e) => throw e, + ); + _handleResponse(response); + } catch (e) { + _handleError(null); + } + } + + Future multipart({ + required dynamic file, + required String method, + required String fileName, + required String fieldName, + required String mediaFormat, + required String mediaExtension, + }) async { + try { + final request = http.MultipartRequest(method, Uri.parse(url)); + _headers.update('Content-Type', (_) => 'multipart/form-data'); + request.headers.addAll(_headers); + final length = await file.length(); + if (_requestBody != null) { + _requestBody!.forEach((key, value) { + request.fields.addAll({key.toString(): value.toString()}); + }); + } + request.files.add( + http.MultipartFile( + fieldName, + file.readAsBytes().asStream(), + length, + filename: fileName + '.' + mediaExtension, + contentType: parser.MediaType(mediaFormat, mediaExtension), + ), + ); + final streamedResponse = await request + .send() + .timeout( + const Duration(seconds: 30), + ) + .catchError( + (e) => throw e, + ); + final response = await http.Response.fromStream(streamedResponse); + _handleResponse(response); + } catch (e) { + _handleError(null); + } + } + + Future put() async { + try { + final response = await http + .put( + Uri.parse(url), + body: _generateBody, + headers: _headers, + ) + .timeout( + const Duration(seconds: 10), + ) + .catchError( + (e) => throw e, + ); + _handleResponse(response); + } catch (e) { + _handleError(null); + } + } + + void _handleResponse(http.Response? response) { + if (_handleError(response)) { + if (response!.body.isNotEmpty) { + _body = json.decode(response.body); + } + } + errorMessage = _errorMessageGenerator(response); + } + + bool _handleError(http.Response? response) { + if (response == null) { + log( + 'Connecion failed or request timedout! ("$url")', + name: 'Request fail error', + ); + isSuccess = false; + return false; + } + if (response.statusCode != 200) { + dynamic data; + if (response.body.isEmpty) { + data = 'No results!'; + } else if (response.body.contains('')) { + data = response.body; + } else { + data = jsonDecode(response.body); + } + log( + 'Request to [$url] failed with status code ${response.statusCode} & result : \n $data', + name: 'Request fail error', + ); + isSuccess = false; + return false; + } + isSuccess = true; + return true; + } + + String _errorMessageGenerator(http.Response? response) { + String? error; + if (response != null) { + if (_body != null) { + if (result.isNotEmpty) { + error = result['msg']; + } + } + } + return error ?? 'خطا! لطفا اتصال اینترنت خود را بررسی و مجددا تلاش نمایید.'; + } +} diff --git a/lib/services/network/request_helper.dart b/lib/services/network/request_helper.dart new file mode 100644 index 0000000..5e28117 --- /dev/null +++ b/lib/services/network/request_helper.dart @@ -0,0 +1,147 @@ +import 'package:didvan/models/requests/news.dart'; +import 'package:didvan/models/requests/radar.dart'; +import 'package:didvan/models/requests/studio.dart'; + +class RequestHelper { + static const String baseUrl = 'https://api.didvan.app'; + static const String _baseUserUrl = baseUrl + '/user'; + static const String _baseRadarUrl = baseUrl + '/radar'; + static const String _baseNewsUrl = baseUrl + '/news'; + static const String _baseStudioUrl = baseUrl + '/studio'; + static const String _baseDirectUrl = _baseUserUrl + '/direct'; + + static const String confirmUsername = _baseUserUrl + '/confirmUsername'; + static const String changePassword = _baseUserUrl + '/changePassword'; + static const String login = _baseUserUrl + '/login'; + static const String directs = _baseUserUrl + '/direct'; + static const String userInfo = _baseUserUrl + '/info'; + static const String updateProfilePhoto = _baseUserUrl + '/profile/photo'; + static const String checkUsername = _baseUserUrl + '/CheckUsername'; + static const String updateProfile = _baseUserUrl + '/profile/edit'; + static const String otp = _baseUserUrl + '/otp'; + static String bookmarks({String? type}) => + _baseUserUrl + '/marked/${type ?? ''}'; + + static const String directTypes = baseUrl + '/direct/types'; + static String direct(int id) => _baseDirectUrl + '/$id'; + static String sendDirectMessage(int id) => + _baseDirectUrl + '/$id/sendMessage'; + static String tag({ + required List ids, + String? type, + int? itemId, + int? page, + int? limit, + }) => + baseUrl + + '/tag' + + _urlConcatGenerator([ + MapEntry('limit', limit?.toString() ?? '3'), + MapEntry('type', type), + MapEntry('id', itemId?.toString() ?? '1'), + MapEntry('tags', _urlListConcatGenerator(ids)) + ]); + + static String markRadar(int id) => _baseRadarUrl + '/$id/mark'; + static String radarComments(int id) => _baseRadarUrl + '/$id/comments'; + static String addRadarComment(int id) => _baseRadarUrl + '/$id/comments/add'; + static String feedbackRadarComment(int radarId, int id) => + _baseRadarUrl + '/$radarId/comments/$id/feedback'; + static String radarDetails(int id, RadarRequestArgs args) => + _baseRadarUrl + + '/$id' + + _urlConcatGenerator([ + MapEntry('page', args.page.toString()), + MapEntry('start', args.startDate), + MapEntry('end', args.endDate), + MapEntry('search', args.search), + MapEntry('categories', _urlListConcatGenerator(args.categories)), + ]); + static String radarOverviews({required RadarRequestArgs args}) => + _baseRadarUrl + + _urlConcatGenerator([ + MapEntry('page', args.page.toString()), + MapEntry('start', args.startDate), + MapEntry('end', args.endDate), + MapEntry('search', args.search), + MapEntry('categories', _urlListConcatGenerator(args.categories)), + ]); + + static String markNews(int id) => _baseNewsUrl + '/$id/mark'; + static String newsComments(int id) => _baseNewsUrl + '/$id/comments'; + static String addNewsComment(int id) => _baseNewsUrl + '/$id/comments/add'; + static String feedbackNewsComment(int radarId, int id) => + _baseNewsUrl + '/$radarId/comments/$id/feedback'; + static String newsDetails(int id, NewsRequestArgs args) => + _baseNewsUrl + + '/$id' + + _urlConcatGenerator([ + MapEntry('page', args.page.toString()), + MapEntry('start', args.startDate), + MapEntry('end', args.endDate), + MapEntry('search', args.search), + ]); + static String newsOverviews({required NewsRequestArgs args}) => + _baseNewsUrl + + _urlConcatGenerator([ + MapEntry('page', args.page.toString()), + MapEntry('start', args.startDate), + MapEntry('end', args.endDate), + MapEntry('search', args.search), + ]); + + static String markStudioItem(int id) => _baseStudioUrl + '/$id/mark'; + static String studioItemComments(int id) => _baseStudioUrl + '/$id/comments'; + static String addStudioItemComment(int id) => + _baseStudioUrl + '/$id/comments/add'; + static String feedbackStudioItemComment(int radarId, int id) => + _baseStudioUrl + '/$radarId/comments/$id/feedback'; + static String studioItemDetails(int id, StudioRequestArgs args) => + _baseStudioUrl + + '/$id' + + _urlConcatGenerator([ + MapEntry('page', args.page.toString()), + MapEntry('search', args.search), + MapEntry('order', args.order), + MapEntry('type', args.type), + ]); + static String studioItemOverviews({required StudioRequestArgs args}) => + _baseStudioUrl + + _urlConcatGenerator([ + MapEntry('page', args.page.toString()), + MapEntry('search', args.search), + MapEntry('order', args.order), + MapEntry('type', args.type), + ]); + + static String _urlConcatGenerator(List> additions) { + String result = ''; + additions.removeWhere((element) => element.value == null); + if (additions.isNotEmpty) { + result += '?'; + for (var i = 0; i < additions.length; i++) { + result += (additions[i].key + '=' + additions[i].value!); + if (i != additions.length - 1) { + result += '&'; + } + } + } + return result; + } + + static String? _urlListConcatGenerator(List? input) { + String? result; + if (input == null) return null; + if (input.isNotEmpty) { + result = ''; + for (var i = 0; i < input.length; i++) { + result = result! + input[i].toString(); + if (i != input.length - 1) { + result += ','; + } + } + return result; + } + return null; + } +} diff --git a/lib/services/storage/storage.dart b/lib/services/storage/storage.dart new file mode 100644 index 0000000..012936c --- /dev/null +++ b/lib/services/storage/storage.dart @@ -0,0 +1,26 @@ +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import 'package:universal_html/html.dart'; + +class StorageService { + static late String appDocsDir; + static late String appTempsDir; + static const FlutterSecureStorage _storage = FlutterSecureStorage(); + + static Future setValue({ + required String key, + required dynamic value, + }) async { + await _storage.write(key: key, value: value.toString()); + } + + static Future getValue({required String key}) async { + final result = await _storage.read(key: key); + return result; + } + + static Future delete({required String key}) async { + _storage.delete(key: key); + } + + static Storage get webStorage => window.localStorage; +} diff --git a/lib/utils/action_sheet.dart b/lib/utils/action_sheet.dart new file mode 100644 index 0000000..0bdb571 --- /dev/null +++ b/lib/utils/action_sheet.dart @@ -0,0 +1,239 @@ +import 'dart:async'; + +import 'package:bot_toast/bot_toast.dart'; +import 'package:didvan/config/design_config.dart'; +import 'package:didvan/config/theme_data.dart'; +import 'package:didvan/constants/assets.dart'; +import 'package:didvan/models/enums.dart'; +import 'package:didvan/models/view/action_sheet_data.dart'; +import 'package:didvan/models/view/alert_data.dart'; +import 'package:didvan/views/widgets/didvan/button.dart'; +import 'package:didvan/views/widgets/didvan/text.dart'; +import 'package:flutter/material.dart'; +import 'package:rive/rive.dart'; + +class ActionSheetUtils { + static late BuildContext context; + + static Future showLogoLoadingIndicator() async { + await showDialog( + context: context, + builder: (context) => Padding( + padding: EdgeInsets.symmetric( + horizontal: MediaQuery.of(context).size.width / 3, + ), + child: RiveAnimation.asset(Assets.logoLoadingAnimation), + ), + ); + } + + static Future showAlert(AlertData alertData) async { + bool isInit = true; + Color backgroundColor; + Color foregroundColor; + switch (alertData.aLertType) { + case ALertType.info: + backgroundColor = Theme.of(context).colorScheme.focused; + foregroundColor = Theme.of(context).colorScheme.focusedBorder; + break; + case ALertType.success: + backgroundColor = Theme.of(context).colorScheme.successBack; + foregroundColor = Theme.of(context).colorScheme.success; + break; + case ALertType.error: + backgroundColor = Theme.of(context).colorScheme.errorBack; + foregroundColor = Theme.of(context).colorScheme.error; + break; + } + BotToast.showNotification( + backgroundColor: backgroundColor, + title: (cancelFunc) => StatefulBuilder( + builder: (context, setState) { + if (isInit) { + Future.delayed(Duration.zero, () { + setState(() {}); + isInit = false; + }); + } + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + DidvanText(alertData.message, color: foregroundColor), + AnimatedContainer( + duration: const Duration(seconds: 2), + width: isInit ? MediaQuery.of(context).size.width - 32 : 0, + height: 2, + color: foregroundColor, + ), + ], + ); + }, + ), + ); + } + + static Future showBottomSheet({required ActionSheetData data}) async { + await showModalBottomSheet( + backgroundColor: Colors.transparent, + isScrollControlled: true, + context: context, + builder: (context) => Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + borderRadius: const BorderRadius.vertical( + top: Radius.circular(10), + ), + ), + child: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Center( + child: Container( + height: 3, + width: 50, + color: Theme.of(context).colorScheme.hint, + ), + ), + const SizedBox(height: 8), + Row( + children: [ + if (data.titleIcon != null) + Icon( + data.titleIcon, + color: data.titleColor ?? + Theme.of(context).colorScheme.title, + ), + if (data.titleIcon != null) const SizedBox(width: 8), + DidvanText( + data.title, + style: Theme.of(context).textTheme.subtitle1, + color: + data.titleColor ?? Theme.of(context).colorScheme.title, + ) + ], + ), + const SizedBox(height: 28), + data.content, + const SizedBox(height: 28), + if (!data.withoutButtonMode) + Row( + children: [ + if (data.hasDismissButton) + Expanded( + child: DidvanButton( + onPressed: () { + Navigator.of(context).pop(); + data.onDismissed?.call(); + }, + title: data.dismissTitle ?? 'بازگشت', + style: ButtonStyleMode.secondary, + ), + ), + if (data.hasDismissButton) const SizedBox(width: 20), + if (data.hasConfirmButton) + Expanded( + flex: data.smallDismissButton ? 2 : 1, + child: DidvanButton( + style: ButtonStyleMode.primary, + onPressed: () { + Navigator.of(context).pop(); + data.onConfirmed?.call(); + }, + title: data.confrimTitle ?? 'تایید', + ), + ), + ], + ), + ], + ), + ), + ), + ); + } + + static Future openDialog({required ActionSheetData data}) async { + await showDialog( + context: context, + builder: (context) => Dialog( + backgroundColor: Theme.of(context).colorScheme.surface, + shape: const RoundedRectangleBorder( + borderRadius: DesignConfig.mediumBorderRadius, + ), + child: Padding( + padding: const EdgeInsets.all(24.0), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisSize: MainAxisSize.min, + children: [ + GestureDetector( + onTap: () => Navigator.of(context).pop(), + child: Icon( + data.titleIcon, + size: 20, + color: data.titleColor, + ), + ), + const SizedBox( + width: 8, + ), + Expanded( + child: DidvanText( + data.title, + style: Theme.of(context).textTheme.headline3, + color: data.titleColor, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + const SizedBox( + height: 12, + ), + data.content, + const SizedBox( + height: 12, + ), + Row( + children: [ + if (data.hasDismissButton) + Expanded( + child: DidvanButton( + onPressed: data.onDismissed ?? () => pop(), + title: data.dismissTitle ?? 'بازگشت', + style: ButtonStyleMode.flat, + ), + ), + if (data.hasDismissButton) + const SizedBox( + width: 20, + ), + Expanded( + child: DidvanButton( + onPressed: () { + pop(); + data.onConfirmed?.call(); + }, + title: data.confrimTitle ?? 'تایید', + ), + ), + ], + ), + ], + ), + ), + ), + ); + } + + static void pop() { + DesignConfig.updateSystemUiOverlayStyle(); + Navigator.of(context).pop(); + } +} diff --git a/lib/utils/date_time.dart b/lib/utils/date_time.dart new file mode 100644 index 0000000..41be0c6 --- /dev/null +++ b/lib/utils/date_time.dart @@ -0,0 +1,109 @@ +import 'package:didvan/config/design_config.dart'; +import 'package:flutter/material.dart'; +import 'package:persian_datetime_picker/persian_datetime_picker.dart'; + +class DateTimeUtils { + static TimeOfDay stringToTimeOfDay(String input) => TimeOfDay( + hour: int.parse(input.split(':')[0]), + minute: int.parse(input.split(':')[1]), + ); + + static String normalizeTimeDuration(Duration input) { + String minute; + String second; + + if (input.inMinutes < 10) { + minute = '0${input.inMinutes}'; + } else { + minute = input.inMinutes.toString(); + } + int realSeconds = input.inSeconds % 60; + if (realSeconds < 10) { + second = '0$realSeconds'; + } else { + second = (realSeconds).toString(); + } + return '$minute:$second'; + } + + static Future showDatePicker({ + String? initialDate, + String? startDate, + String? endDate, + }) async { + final initDate = initialDate == null ? null : DateTime.parse(initialDate); + final initialJalali = Jalali.fromDateTime(initDate ?? DateTime.now()); + + final firstDate = Jalali.fromDateTime( + startDate == null ? DateTime(2021) : DateTime.parse(startDate), + ); + + final lastDate = Jalali.fromDateTime( + endDate == null ? DateTime.now() : DateTime.parse(endDate), + ); + + final Jalali? result = await showPersianDatePicker( + context: DesignConfig.context!, + initialDate: initialJalali, + firstDate: firstDate, + lastDate: lastDate, + ); + + return result?.toDateTime().toString(); + } + + static String timeWithAmPm(String input) { + final dateTime = utcToLocalTime(input); + bool isAm = true; + int hour = 0; + int minute = 0; + if (dateTime.hour > 12) { + isAm = false; + hour = dateTime.hour - 12; + } else { + hour = dateTime.hour; + } + minute = dateTime.minute; + return '$hour:${_timeNormalizer(minute)} ${isAm ? 'ق.ظ' : 'ب.ظ'}'; + } + + static DateTime utcToLocalTime(String input) { + final dateTime = DateTime.parse(input); + return dateTime.add(const Duration(hours: 3, minutes: 30)); + } + + static String _timeNormalizer(int input) { + if (input < 10) return '0$input'; + return input.toString(); + } + + static String momentGenerator(String input) { + final date = DateTime.parse(input); + final int seconds = (DateTime.now().difference(date).inSeconds).floor(); + + double interval = seconds / 31536000; + + if (interval > 1) { + return interval.floor().toString() + " سال پیش"; + } + interval = seconds / 2592000; + if (interval > 1) { + return interval.floor().toString() + " ماه پیش"; + } + interval = seconds / 86400; + if (interval > 1) { + if (interval.floor() == 1) return 'دیروز'; + return interval.floor().toString() + " روز پیش"; + } + interval = seconds / 3600; + if (interval > 1) { + return interval.floor().toString() + " ساعت پیش"; + } + interval = seconds / 60; + if (interval > 1) { + return interval.floor().toString() + " دقیقه پیش"; + } + return 'هم اکنون'; + // return seconds.floor().toString() + " ثانیه پیش"; + } +} diff --git a/lib/views/authentication/authentication.dart b/lib/views/authentication/authentication.dart new file mode 100644 index 0000000..9119411 --- /dev/null +++ b/lib/views/authentication/authentication.dart @@ -0,0 +1,60 @@ +import 'package:didvan/config/design_config.dart'; +import 'package:didvan/views/authentication/authentication_state.dart'; +import 'package:didvan/views/authentication/screens/password.dart'; +import 'package:didvan/views/authentication/screens/reset_password.dart'; +import 'package:didvan/views/authentication/screens/username.dart'; +import 'package:didvan/views/authentication/screens/verification.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +class Authentication extends StatefulWidget { + final bool isResetPassword; + const Authentication({Key? key, required this.isResetPassword}) + : super(key: key); + + @override + State createState() => _AuthenticationState(); +} + +class _AuthenticationState extends State { + late final List _pages; + + @override + void initState() { + if (widget.isResetPassword) { + _pages = const [ + Verification(), + ResetPassword(), + ]; + } else { + _pages = const [ + UsernameInput(), + PasswordInput(), + Verification(), + ResetPassword(), + ]; + } + super.initState(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Consumer( + builder: (context, state, child) => WillPopScope( + onWillPop: () async { + if (state.currentPageIndex == 0) { + return true; + } + state.currentPageIndex--; + return false; + }, + child: AnimatedSwitcher( + duration: DesignConfig.mediumAnimationDuration, + child: _pages[state.currentPageIndex], + ), + ), + ), + ); + } +} diff --git a/lib/views/authentication/authentication_state.dart b/lib/views/authentication/authentication_state.dart new file mode 100644 index 0000000..ffda950 --- /dev/null +++ b/lib/views/authentication/authentication_state.dart @@ -0,0 +1,120 @@ +import 'package:didvan/models/enums.dart'; +import 'package:didvan/models/view/alert_data.dart'; +import 'package:didvan/providers/core_provider.dart'; +import 'package:didvan/providers/user_provider.dart'; +import 'package:didvan/services/network/request.dart'; +import 'package:didvan/services/network/request_helper.dart'; +import 'package:didvan/utils/action_sheet.dart'; + +class AuthenticationState extends CoreProvier { + int _currentPageIndex = 0; + String username = ''; + String password = ''; + String _verificationCode = ''; + + set currentPageIndex(int value) { + _currentPageIndex = value; + notifyListeners(); + } + + int get currentPageIndex => _currentPageIndex; + + Future confirmUsername() async { + appState = AppState.isolatedBusy; + final RequestService service = RequestService( + RequestHelper.confirmUsername, + useAutherization: false, + body: {'username': username}, + ); + await service.post(); + if (service.isSuccess && service.result['confirmed']) { + appState = AppState.idle; + currentPageIndex++; + } else { + appState = AppState.failed; + ActionSheetUtils.showAlert(AlertData(message: service.errorMessage)); + } + } + + Future login(UserProvider userProvider) async { + appState = AppState.isolatedBusy; + final RequestService service = RequestService( + RequestHelper.login, + useAutherization: false, + body: {'username': username, "password": password}, + ); + await service.post(); + if (service.isSuccess && service.result['loggedIn']) { + final token = service.result['token']; + appState = AppState.idle; + await userProvider.setAndGetToken(newToken: token); + RequestService.token = token; + await userProvider.getUserInfo(); + return token; + } else { + appState = AppState.failed; + ActionSheetUtils.showAlert(AlertData(message: service.errorMessage)); + } + return null; + } + + Future sendOtpToken() async { + final service = RequestService( + RequestHelper.otp + '?username=$username', + useAutherization: false, + ); + await service.httpGet(); + } + + Future verifyOtpToken(String token) async { + appState = AppState.isolatedBusy; + final service = RequestService( + RequestHelper.otp, + body: { + 'code': token, + 'username': username, + }, + useAutherization: false, + ); + await service.post(); + appState = AppState.idle; + if (service.isSuccess) { + _verificationCode = token; + currentPageIndex++; + return true; + } + ActionSheetUtils.showAlert( + AlertData( + message: service.isSuccess + ? service.result['message'] + : 'کد وارد شده صحیح نمی‌باشد', + ), + ); + return false; + } + + Future resetPassword(String password) async { + appState = AppState.isolatedBusy; + final service = RequestService( + RequestHelper.changePassword, + body: { + 'code': _verificationCode, + 'username': username, + 'password': password, + }, + useAutherization: false, + ); + await service.put(); + if (service.isSuccess) { + ActionSheetUtils.showAlert( + AlertData( + message: 'کلمه عبور با موفقیت تغییر کرد', + aLertType: ALertType.success, + ), + ); + return true; + } + ActionSheetUtils.showAlert(AlertData(message: service.errorMessage)); + return false; + } +} diff --git a/lib/views/authentication/screens/password.dart b/lib/views/authentication/screens/password.dart new file mode 100644 index 0000000..391556b --- /dev/null +++ b/lib/views/authentication/screens/password.dart @@ -0,0 +1,81 @@ +import 'dart:developer'; + +import 'package:didvan/providers/server_data_provider.dart'; +import 'package:didvan/providers/user_provider.dart'; +import 'package:didvan/routes/routes.dart'; +import 'package:didvan/views/authentication/authentication_state.dart'; +import 'package:didvan/views/authentication/widgets/authentication_layout.dart'; +import 'package:didvan/views/widgets/didvan/button.dart'; +import 'package:didvan/views/widgets/didvan/text.dart'; +import 'package:didvan/views/widgets/didvan/text_field.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +class PasswordInput extends StatefulWidget { + const PasswordInput({Key? key}) : super(key: key); + + @override + State createState() => _PasswordInputState(); +} + +class _PasswordInputState extends State { + final _formKey = GlobalKey(); + + @override + Widget build(BuildContext context) { + final AuthenticationState state = context.read(); + return AuthenticationLayout( + appBarTitle: + 'ورود با ${state.username.contains('09') ? 'شماره موبایل' : 'نام کاربری'} ${state.username}', + children: [ + Form( + key: _formKey, + child: DidvanTextField( + onChanged: (value) => state.password = value, + onSubmitted: (value) => _onLogin(context), + autoFocus: true, + title: 'کلمه عبور', + hintText: 'کلمه عبور', + obsecureText: true, + validator: (value) => value.length < 8 + ? 'کلمه عبور نمی‌تواند از 8 کاراکتر کمتر باشد' + : null, + ), + ), + const SizedBox( + height: 32, + ), + GestureDetector( + onTap: () => state.currentPageIndex++, + child: DidvanText( + 'فراموشی رمز عبور', + style: Theme.of(context).textTheme.subtitle2, + color: Theme.of(context).colorScheme.primary, + ), + ), + const Spacer(), + DidvanButton( + onPressed: () => _onLogin(context), + title: 'ورود', + ), + const SizedBox( + height: 48, + ), + ], + ); + } + + Future _onLogin(BuildContext context) async { + if (!_formKey.currentState!.validate()) { + return; + } + final state = context.read(); + final userProvider = context.read(); + final token = await state.login(userProvider); + if (token != null) { + log(token); + await ServerDataProvider.getData(); + Navigator.of(context).pushReplacementNamed(Routes.home); + } + } +} diff --git a/lib/views/authentication/screens/reset_password.dart b/lib/views/authentication/screens/reset_password.dart new file mode 100644 index 0000000..9985a95 --- /dev/null +++ b/lib/views/authentication/screens/reset_password.dart @@ -0,0 +1,75 @@ +import 'package:didvan/providers/user_provider.dart'; +import 'package:didvan/routes/routes.dart'; +import 'package:didvan/views/authentication/authentication_state.dart'; +import 'package:didvan/views/authentication/widgets/authentication_layout.dart'; +import 'package:didvan/views/widgets/didvan/button.dart'; +import 'package:didvan/views/widgets/didvan/text_field.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +class ResetPassword extends StatefulWidget { + const ResetPassword({Key? key}) : super(key: key); + + @override + State createState() => _ResetPasswordState(); +} + +class _ResetPasswordState extends State { + String _password = ''; + String _passwordConfirmation = ''; + final _formKey = GlobalKey(); + + @override + Widget build(BuildContext context) { + return Form( + key: _formKey, + child: AuthenticationLayout( + children: [ + DidvanTextField( + title: 'کلمه عبور جدید', + onChanged: (value) => _password = value, + validator: (value) => value.length < 8 + ? 'کلمه عبور نمی‌تواند کمتر از 8 کاراکتر داشته باشد' + : null, + hintText: 'کلمه عبور جدید', + obsecureText: true, + ), + const SizedBox( + height: 16, + ), + DidvanTextField( + title: 'تکرار کلمه عبور جدید', + onChanged: (value) => _passwordConfirmation = value, + validator: (value) => _passwordConfirmation != _password + ? 'تکرار کلمه عبور با کلمه عبور یکسان نیست' + : null, + hintText: 'تکرار کلمه عبور جدید', + obsecureText: true, + ), + const Spacer(), + DidvanButton( + onPressed: () async { + if (!_formKey.currentState!.validate()) return; + final result = await context + .read() + .resetPassword(_password); + if (result && context.read().isAuthenticated) { + Navigator.of(context).pop(); + Navigator.of(context).pop(); + return; + } + Navigator.of(context).pushNamed( + Routes.authenticaion, + arguments: false, + ); + }, + title: 'تغییر رمز عبور', + ), + const SizedBox( + height: 48, + ), + ], + ), + ); + } +} diff --git a/lib/views/authentication/screens/username.dart b/lib/views/authentication/screens/username.dart new file mode 100644 index 0000000..e5a9187 --- /dev/null +++ b/lib/views/authentication/screens/username.dart @@ -0,0 +1,100 @@ +import 'package:didvan/views/authentication/authentication_state.dart'; +import 'package:didvan/views/authentication/widgets/authentication_layout.dart'; +import 'package:didvan/views/widgets/didvan/button.dart'; +import 'package:didvan/views/widgets/didvan/text_field.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:url_launcher/url_launcher.dart'; + +class UsernameInput extends StatefulWidget { + const UsernameInput({ + Key? key, + }) : super(key: key); + + @override + State createState() => _UsernameInputState(); +} + +class _UsernameInputState extends State { + final _formKey = GlobalKey(); + @override + Widget build(BuildContext context) { + final AuthenticationState state = context.read(); + return AuthenticationLayout( + children: [ + Form( + key: _formKey, + child: DidvanTextField( + initialValue: state.username, + title: 'نام کاربری یا شماره موبایل', + hintText: 'نام کاربری یا شماره موبایل', + textAlign: TextAlign.center, + acceptSpace: false, + onSubmitted: (value) => state.confirmUsername(), + validator: (value) { + if (value.contains(' ')) { + return 'نام کاربری باید بدون فاصله باشد'; + } + if (value.length < 4) { + return 'نام کاربری نمی‌تواند از 4 کاراکتر کمتر باشد'; + } + }, + onChanged: (value) { + state.username = value; + }, + ), + ), + const SizedBox( + height: 20, + ), + DidvanButton( + title: 'ورود', + onPressed: () { + if (_formKey.currentState!.validate()) { + state.confirmUsername(); + } + }, + ), + const Spacer(), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 60), + child: RichText( + textAlign: TextAlign.center, + text: TextSpan( + style: Theme.of(context).textTheme.caption, + children: [ + const TextSpan(text: 'با و ورود به دیدوان،'), + TextSpan( + text: ' شرایط ', + style: Theme.of(context) + .textTheme + .caption! + .copyWith(color: Theme.of(context).colorScheme.primary), + recognizer: TapGestureRecognizer()..onTap = _openTermsOfUse, + ), + const TextSpan(text: 'و\n'), + TextSpan( + text: ' قوانین حریم خصوصی ', + style: Theme.of(context) + .textTheme + .caption! + .copyWith(color: Theme.of(context).colorScheme.primary), + recognizer: TapGestureRecognizer()..onTap = _openTermsOfUse, + ), + const TextSpan(text: 'را می‌پذیرم'), + ], + ), + ), + ), + const SizedBox( + height: 48, + ), + ], + ); + } + + void _openTermsOfUse() { + launch('https://didvan.app/termsOfUse.html'); + } +} diff --git a/lib/views/authentication/screens/verification.dart b/lib/views/authentication/screens/verification.dart new file mode 100644 index 0000000..e18e5c3 --- /dev/null +++ b/lib/views/authentication/screens/verification.dart @@ -0,0 +1,142 @@ +import 'dart:async'; + +import 'package:didvan/config/design_config.dart'; +import 'package:didvan/config/theme_data.dart'; +import 'package:didvan/providers/user_provider.dart'; +import 'package:didvan/views/authentication/authentication_state.dart'; +import 'package:didvan/views/authentication/widgets/authentication_layout.dart'; +import 'package:didvan/views/widgets/didvan/button.dart'; +import 'package:didvan/views/widgets/didvan/text.dart'; +import 'package:flutter/material.dart'; +import 'package:pin_code_fields/pin_code_fields.dart'; +import 'package:provider/provider.dart'; + +class Verification extends StatefulWidget { + const Verification({Key? key}) : super(key: key); + + @override + State createState() => _VerificationState(); +} + +class _VerificationState extends State { + Timer? _timer; + int _secondsRemaining = 119; + + bool _isResendButtonEnabled = false; + + @override + void initState() { + final state = context.read(); + if (state.username == '') { + final user = context.read().user; + state.username = user.phoneNumber; + } + Future.delayed( + Duration.zero, + state.sendOtpToken, + ); + _handleTimer(); + super.initState(); + } + + @override + Widget build(BuildContext context) { + final AuthenticationState state = context.read(); + return WillPopScope( + onWillPop: () async { + _timer?.cancel(); + return true; + }, + child: AuthenticationLayout( + appBarTitle: 'تغییر رمز عبور', + children: [ + DidvanText( + 'کد 6 رقمی ارسال شده به موبایل', + style: Theme.of(context).textTheme.subtitle2, + fontWeight: FontWeight.normal, + ), + const SizedBox( + height: 8, + ), + DidvanText( + state.username, + style: Theme.of(context).textTheme.subtitle1, + ), + const SizedBox( + height: 8, + ), + DidvanText( + 'را وارد کنید:', + style: Theme.of(context).textTheme.subtitle2, + fontWeight: FontWeight.normal, + ), + const SizedBox( + height: 24, + ), + Directionality( + textDirection: TextDirection.ltr, + child: PinCodeTextField( + keyboardType: TextInputType.number, + animationType: AnimationType.scale, + cursorColor: Theme.of(context).colorScheme.text, + pinTheme: PinTheme( + fieldHeight: 48, + fieldWidth: 48, + selectedColor: Theme.of(context).colorScheme.primary, + inactiveColor: Theme.of(context).colorScheme.border, + activeFillColor: Theme.of(context).colorScheme.primary, + activeColor: Theme.of(context).colorScheme.primary, + borderRadius: DesignConfig.lowBorderRadius, + borderWidth: 1, + shape: PinCodeFieldShape.box, + ), + appContext: context, + length: 6, + onCompleted: (value) async { + final result = await state.verifyOtpToken(value); + if (result) { + _timer?.cancel(); + } + }, + onChanged: (value) {}, + ), + ), + const Spacer(), + DidvanButton( + enabled: _isResendButtonEnabled, + onPressed: () { + _handleTimer(); + state.sendOtpToken(); + }, + title: !_isResendButtonEnabled + ? '$_secondsRemaining ثانیه دیگر' + : 'ارسال مجدد کد', + ), + const SizedBox( + height: 48, + ), + ], + ), + ); + } + + Future _handleTimer() async { + _timer?.cancel(); + _isResendButtonEnabled = false; + _timer = Timer.periodic(const Duration(seconds: 1), (timer) { + try { + setState(() { + _secondsRemaining -= 1; + if (_secondsRemaining == 0) { + _isResendButtonEnabled = true; + _secondsRemaining = 129; + _timer?.cancel(); + } + }); + } catch (e) { + _timer?.cancel(); + } + }); + setState(() {}); + } +} diff --git a/lib/views/authentication/widgets/authentication_app_bar.dart b/lib/views/authentication/widgets/authentication_app_bar.dart new file mode 100644 index 0000000..81e86f3 --- /dev/null +++ b/lib/views/authentication/widgets/authentication_app_bar.dart @@ -0,0 +1,39 @@ +import 'package:didvan/config/theme_data.dart'; +import 'package:didvan/constants/app_icons.dart'; +import 'package:didvan/views/authentication/authentication_state.dart'; +import 'package:didvan/views/widgets/didvan/icon_button.dart'; +import 'package:didvan/views/widgets/didvan/text.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +class AuthenticationAppBar extends StatelessWidget { + final String? title; + const AuthenticationAppBar({Key? key, this.title}) : super(key: key); + + @override + Widget build(BuildContext context) { + final state = context.read(); + return Row( + children: [ + DidvanIconButton( + icon: DidvanIcons.back_regular, + onPressed: () { + if (state.currentPageIndex == 0) { + Navigator.of(context).pop(); + return; + } + state.currentPageIndex--; + }), + const SizedBox( + width: 4, + ), + if (title != null) + DidvanText( + title!, + style: Theme.of(context).textTheme.subtitle2, + color: Theme.of(context).colorScheme.title, + ) + ], + ); + } +} diff --git a/lib/views/authentication/widgets/authentication_layout.dart b/lib/views/authentication/widgets/authentication_layout.dart new file mode 100644 index 0000000..e3fceca --- /dev/null +++ b/lib/views/authentication/widgets/authentication_layout.dart @@ -0,0 +1,61 @@ +import 'package:didvan/views/authentication/widgets/authentication_app_bar.dart'; +import 'package:didvan/views/widgets/logos/didvan_horizontal_logo.dart'; +import 'package:flutter/material.dart'; + +class AuthenticationLayout extends StatelessWidget { + final List children; + final String? appBarTitle; + + const AuthenticationLayout( + {Key? key, required this.children, this.appBarTitle}) + : super(key: key); + + @override + Widget build(BuildContext context) { + return CustomScrollView( + slivers: [ + if (appBarTitle != null) + SliverAppBar( + automaticallyImplyLeading: false, + pinned: true, + toolbarHeight: 56, + backgroundColor: Theme.of(context).colorScheme.background, + flexibleSpace: Padding( + padding: EdgeInsets.only( + left: 16, + right: 16, + top: MediaQuery.of(context).padding.top, + ), + child: AuthenticationAppBar( + title: appBarTitle, + ), + ), + ), + SliverPadding( + padding: EdgeInsets.only( + top: appBarTitle == null ? 100 : 44, + left: 100, + right: 100, + bottom: 40, + ), + sliver: const SliverToBoxAdapter( + child: DidvanVerticalLogo( + height: 200, + ), + ), + ), + SliverPadding( + padding: const EdgeInsets.symmetric(horizontal: 20), + sliver: SliverFillRemaining( + hasScrollBody: false, + child: Column( + children: [ + for (var i = 0; i < children.length; i++) children[i], + ], + ), + ), + ), + ], + ); + } +} diff --git a/lib/views/home/comments/comments.dart b/lib/views/home/comments/comments.dart new file mode 100644 index 0000000..0bf0c72 --- /dev/null +++ b/lib/views/home/comments/comments.dart @@ -0,0 +1,260 @@ +import 'package:didvan/config/design_config.dart'; +import 'package:didvan/config/theme_data.dart'; +import 'package:didvan/constants/app_icons.dart'; +import 'package:didvan/models/view/app_bar_data.dart'; +import 'package:didvan/views/home/comments/comments_state.dart'; +import 'package:didvan/views/home/comments/widgets/comment_item.dart'; +import 'package:didvan/views/widgets/animated_visibility.dart'; +import 'package:didvan/views/widgets/didvan/icon_button.dart'; +import 'package:didvan/views/widgets/didvan/scaffold.dart'; +import 'package:didvan/views/widgets/didvan/text.dart'; +import 'package:didvan/views/widgets/shimmer_placeholder.dart'; +import 'package:didvan/views/widgets/state_handlers/sliver_state_handler.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +class Comments extends StatefulWidget { + final Map pageData; + const Comments({ + Key? key, + required this.pageData, + }) : super(key: key); + + @override + _CommentsState createState() => _CommentsState(); +} + +class _CommentsState extends State { + final _focusNode = FocusNode(); + double _bottomPadding = 0; + + @override + void initState() { + final state = context.read(); + state.itemId = widget.pageData['id']; + state.isRadar = widget.pageData['isRadar']; + state.onCommentsChanged = widget.pageData['onCommentsChanged']; + Future.delayed( + Duration.zero, + () => state.getComments(), + ); + super.initState(); + } + + @override + Widget build(BuildContext context) { + final bottomViewInset = MediaQuery.of(context).viewInsets.bottom; + if (bottomViewInset == 0) { + if (_bottomPadding != 0) { + FocusScope.of(context).unfocus(); + _bottomPadding = 0; + } + } + _bottomPadding = bottomViewInset; + return Material( + child: Stack( + children: [ + DidvanScaffold( + backgroundColor: Theme.of(context).colorScheme.surface, + appBarData: AppBarData( + hasBack: true, + title: 'نظرات', + subtitle: widget.pageData['title'], + ), + padding: const EdgeInsets.only(left: 16, right: 16, bottom: 92), + slivers: [ + Consumer( + builder: (context, state, child) => + SliverStateHandler( + onRetry: state.getComments, + state: state, + itemPadding: const EdgeInsets.symmetric(vertical: 16), + childCount: state.comments.length, + placeholder: const _CommentPlaceholder(), + builder: (context, state, index) => Comment( + focusNode: _focusNode, + comment: state.comments[index], + ), + ), + ), + ], + ), + Positioned( + left: 0, + right: 0, + bottom: MediaQuery.of(context).viewInsets.bottom, + child: _MessageBox(focusNode: _focusNode), + ), + ], + ), + ); + } +} + +class _MessageBox extends StatefulWidget { + final FocusNode focusNode; + const _MessageBox({Key? key, required this.focusNode}) : super(key: key); + + @override + State<_MessageBox> createState() => _MessageBoxState(); +} + +class _MessageBoxState extends State<_MessageBox> { + final _controller = TextEditingController(); + + @override + Widget build(BuildContext context) { + final state = context.watch(); + return Column( + children: [ + AnimatedVisibility( + duration: DesignConfig.lowAnimationDuration, + isVisible: state.showReplyBox, + child: Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + border: Border( + top: BorderSide( + color: Theme.of(context).colorScheme.border, + ), + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + if (state.replyingTo != null) + DidvanText( + 'پاسخ به ${state.replyingTo!.fullName}:', + color: Theme.of(context).colorScheme.caption, + style: Theme.of(context).textTheme.caption, + ), + const Spacer(), + DidvanIconButton( + gestureSize: 24, + color: Theme.of(context).colorScheme.caption, + icon: DidvanIcons.close_regular, + onPressed: () { + state.commentId = null; + state.replyingTo = null; + state.showReplyBox = false; + state.update(); + }, + ), + ], + ), + ), + ), + Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + border: Border( + top: BorderSide( + color: Theme.of(context).colorScheme.border, + ), + ), + ), + child: Row( + children: [ + DidvanIconButton( + onPressed: () => _onSend(state), + icon: DidvanIcons.send_solid, + size: 24, + color: Theme.of(context).colorScheme.focusedBorder, + ), + Expanded( + child: TextField( + focusNode: widget.focusNode, + controller: _controller, + keyboardType: TextInputType.multiline, + textInputAction: TextInputAction.send, + style: Theme.of(context).textTheme.bodyText2, + onEditingComplete: () {}, + onSubmitted: (value) => _onSend(state), + decoration: InputDecoration( + border: InputBorder.none, + hintText: 'پیام خود را ارسال کنید', + hintStyle: Theme.of(context).textTheme.caption!.copyWith( + color: Theme.of(context).colorScheme.disabledText), + ), + onChanged: (value) => state.text = value, + ), + ), + ], + ), + ), + ], + ); + } + + void _onSend(CommentsState state) { + if (state.text.replaceAll(' ', '').isNotEmpty) { + state.addComment(); + _controller.text = ''; + } + } +} + +class _CommentPlaceholder extends StatelessWidget { + const _CommentPlaceholder({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const ShimmerPlaceholder( + height: 24, + width: 24, + borderRadius: DesignConfig.highBorderRadius, + ), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: const [ + ShimmerPlaceholder( + height: 20, + width: 100, + ), + ShimmerPlaceholder( + height: 14, + width: 100, + ), + ], + ), + const SizedBox(height: 12), + const ShimmerPlaceholder( + height: 16, + width: double.infinity, + ), + const SizedBox(height: 8), + const ShimmerPlaceholder( + height: 16, + width: 200, + ), + const SizedBox(height: 8), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: const [ + ShimmerPlaceholder( + height: 24, + width: 48, + ), + SizedBox(width: 8), + ShimmerPlaceholder( + height: 24, + width: 48, + ), + ], + ), + ], + ), + ), + ], + ); + } +} diff --git a/lib/views/home/comments/comments_state.dart b/lib/views/home/comments/comments_state.dart new file mode 100644 index 0000000..ead8409 --- /dev/null +++ b/lib/views/home/comments/comments_state.dart @@ -0,0 +1,143 @@ +import 'package:didvan/config/design_config.dart'; +import 'package:didvan/models/comment/comment.dart'; +import 'package:didvan/models/comment/feedback.dart'; +import 'package:didvan/models/comment/reply.dart'; +import 'package:didvan/models/comment/user.dart'; +import 'package:didvan/models/enums.dart'; +import 'package:didvan/providers/core_provider.dart'; +import 'package:didvan/providers/user_provider.dart'; +import 'package:didvan/services/network/request.dart'; +import 'package:didvan/services/network/request_helper.dart'; +import 'package:provider/provider.dart'; + +class CommentsState extends CoreProvier { + String text = ''; + int? commentId; + UserOverview? replyingTo; + bool showReplyBox = false; + late void Function(int count) onCommentsChanged; + int _count = 0; + + final List comments = []; + final Map> _feedbackQueue = {}; + + bool isRadar = true; + int itemId = 0; + + Future getComments() async { + appState = AppState.busy; + final service = RequestService( + isRadar + ? RequestHelper.radarComments(itemId) + : RequestHelper.newsComments(itemId), + ); + await service.httpGet(); + if (service.isSuccess) { + final messages = service.result['comments']; + for (var i = 0; i < messages.length; i++) { + comments.add(CommentData.fromJson(messages[i])); + _count++; + for (var j = 0; j < messages[i]['replies'].length; j++) { + _count++; + } + } + appState = AppState.idle; + return; + } + appState = AppState.failed; + } + + Future feedback(int id, bool like, bool dislike) async { + _feedbackQueue.addAll({id: MapEntry(like, dislike)}); + Future.delayed(const Duration(milliseconds: 500), () async { + if (!_feedbackQueue.containsKey(id)) return; + final service = RequestService( + isRadar + ? RequestHelper.feedbackRadarComment(itemId, id) + : RequestHelper.feedbackNewsComment(itemId, id), + body: { + 'like': _feedbackQueue[id]!.key, + 'dislike': _feedbackQueue[id]!.value, + }); + await service.put(); + _feedbackQueue.remove(id); + }); + } + + Future addComment() async { + final user = DesignConfig.context!.read().user; + if (replyingTo != null) { + comments.firstWhere((comment) => comment.id == commentId).replies.add( + Reply( + id: 0, + text: text, + createdAt: DateTime.now().toString(), + liked: false, + disliked: false, + feedback: const FeedbackData(like: 0, dislike: 0), + toUser: replyingTo!, + user: UserOverview( + id: user.id, + fullName: user.fullName, + photo: user.photo, + ), + ), + ); + } else { + comments.insert( + 0, + CommentData( + id: 0, + text: text, + createdAt: DateTime.now().toString(), + liked: false, + disliked: false, + feedback: const FeedbackData(like: 0, dislike: 0), + user: UserOverview( + id: user.id, + fullName: user.fullName, + photo: user.photo, + ), + replies: [], + ), + ); + } + + _count++; + onCommentsChanged(_count); + + final body = {}; + + if (commentId != null) { + body.addAll({'commentId': commentId}); + } + if (replyingTo != null) { + body.addAll({'replyUserId': replyingTo!.id}); + } + + showReplyBox = false; + update(); + body.addAll({'text': text}); + final service = RequestService( + isRadar + ? RequestHelper.addRadarComment(itemId) + : RequestHelper.addNewsComment(itemId), + body: body); + + await service.post(); + if (service.isSuccess) { + if (replyingTo != null) { + comments + .firstWhere((comment) => comment.id == commentId) + .replies + .firstWhere((reply) => reply.id == 0) + .id = service.result['comment']['id']; + } else { + comments.firstWhere((comment) => comment.id == 0).id = + service.result['comment']['id']; + } + commentId = null; + replyingTo = null; + } + } +} diff --git a/lib/views/home/comments/widgets/comment_item.dart b/lib/views/home/comments/widgets/comment_item.dart new file mode 100644 index 0000000..62fc0a2 --- /dev/null +++ b/lib/views/home/comments/widgets/comment_item.dart @@ -0,0 +1,266 @@ +import 'package:didvan/config/design_config.dart'; +import 'package:didvan/config/theme_data.dart'; +import 'package:didvan/constants/app_icons.dart'; +import 'package:didvan/models/comment/comment.dart'; +import 'package:didvan/utils/date_time.dart'; +import 'package:didvan/views/home/comments/comments_state.dart'; +import 'package:didvan/views/widgets/animated_visibility.dart'; +import 'package:didvan/views/widgets/didvan/icon_button.dart'; +import 'package:didvan/views/widgets/didvan/text.dart'; +import 'package:didvan/views/widgets/ink_wrapper.dart'; +import 'package:didvan/views/widgets/skeleton_image.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +class Comment extends StatefulWidget { + final FocusNode focusNode; + final CommentData comment; + const Comment({ + Key? key, + required this.focusNode, + required this.comment, + }) : super(key: key); + + @override + State createState() => CommentState(); +} + +class CommentState extends State { + late final CommentsState state; + + bool _showSubComments = false; + + CommentData get _comment => widget.comment; + + @override + void initState() { + state = context.read(); + super.initState(); + } + + @override + Widget build(BuildContext context) { + return Column( + children: [ + _commentBuilder(comment: _comment), + if (_comment.replies.isNotEmpty) const SizedBox(height: 16), + for (var i = 0; i < _comment.replies.length; i++) + AnimatedVisibility( + duration: DesignConfig.lowAnimationDuration, + isVisible: _showSubComments, + child: _commentBuilder( + isSubComment: true, + comment: _comment.replies[i], + ), + ), + ], + ); + } + + Widget _commentBuilder({required comment, bool isSubComment = false}) => + Container( + decoration: BoxDecoration( + border: Border( + right: isSubComment + ? BorderSide(color: Theme.of(context).colorScheme.caption) + : BorderSide.none, + ), + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (isSubComment) const SizedBox(width: 12), + if (comment.user.photo == null) + const Icon(DidvanIcons.avatar_light), + if (comment.user.photo != null) + SkeletonImage( + imageUrl: comment.user.photo, + height: 24, + width: 24, + borderRadius: DesignConfig.highBorderRadius, + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + DidvanText( + comment.user.fullName, + style: Theme.of(context).textTheme.bodyText1, + ), + DidvanText( + DateTimeUtils.momentGenerator(comment.createdAt), + style: Theme.of(context).textTheme.caption, + color: Theme.of(context).colorScheme.caption, + ), + ], + ), + const SizedBox(height: 8), + if (isSubComment) + DidvanText( + 'پاسخ به ${comment.toUser.fullName}', + style: Theme.of(context).textTheme.caption, + color: Theme.of(context).colorScheme.caption, + ), + const SizedBox(height: 8), + DidvanText(comment.text), + const SizedBox(height: 8), + Row( + children: [ + InkWrapper( + onPressed: () { + state.commentId = _comment.id; + state.replyingTo = comment.user; + state.showReplyBox = true; + state.update(); + widget.focusNode.requestFocus(); + }, + child: DidvanText( + 'پاسخ', + style: Theme.of(context).textTheme.bodyText1, + color: Theme.of(context).colorScheme.primary, + ), + ), + if (!isSubComment) const SizedBox(width: 20), + if (!isSubComment && comment.replies.isNotEmpty) + InkWrapper( + onPressed: () => setState( + () => _showSubComments = !_showSubComments, + ), + child: Row( + children: [ + DidvanText( + 'پاسخ‌ها(${comment.replies.length})', + style: Theme.of(context).textTheme.bodyText1, + color: Theme.of(context).colorScheme.primary, + ), + AnimatedRotation( + duration: DesignConfig.lowAnimationDuration, + turns: _showSubComments ? 0.5 : 0, + child: Icon( + DidvanIcons.angle_down_regular, + color: Theme.of(context).colorScheme.primary, + ), + ), + ], + ), + ), + const Spacer(), + _FeedbackButtons( + likeCount: comment.feedback.like, + dislikeCount: comment.feedback.dislike, + likeValue: comment.liked, + dislikeValue: comment.disliked, + onFeedback: (like, dislike) => + state.feedback(comment.id, like, dislike), + ), + ], + ), + ], + ), + ), + ], + ), + ); +} + +class _FeedbackButtons extends StatefulWidget { + final int likeCount; + final int dislikeCount; + final bool likeValue; + final bool dislikeValue; + final void Function(bool like, bool dislike) onFeedback; + const _FeedbackButtons({ + Key? key, + required this.onFeedback, + required this.likeCount, + required this.dislikeCount, + required this.likeValue, + required this.dislikeValue, + }) : super(key: key); + + @override + State<_FeedbackButtons> createState() => _FeedbackButtonsState(); +} + +class _FeedbackButtonsState extends State<_FeedbackButtons> { + late bool _likeValue; + late bool _dislikeValue; + late int _likeCount; + late int _dislikeCount; + + @override + void initState() { + _likeValue = widget.likeValue; + _dislikeValue = widget.dislikeValue; + _likeCount = widget.likeCount; + _dislikeCount = widget.dislikeCount; + super.initState(); + } + + @override + Widget build(BuildContext context) { + return Row( + children: [ + DidvanText( + _likeCount.toString(), + style: Theme.of(context).textTheme.caption, + color: Theme.of(context).colorScheme.caption, + ), + const SizedBox(width: 4), + DidvanIconButton( + icon: _likeValue ? DidvanIcons.like_solid : DidvanIcons.like_regular, + color: _likeValue ? Theme.of(context).colorScheme.primary : null, + gestureSize: 24, + onPressed: () { + setState(() { + if (_likeValue) { + _likeCount--; + } else { + _likeCount++; + if (_dislikeValue) { + _dislikeValue = false; + _dislikeCount--; + } + } + _likeValue = !_likeValue; + }); + widget.onFeedback(_likeValue, _dislikeValue); + }, + ), + const SizedBox(width: 16), + DidvanText( + _dislikeCount.toString(), + style: Theme.of(context).textTheme.caption, + color: Theme.of(context).colorScheme.caption, + ), + const SizedBox(width: 4), + DidvanIconButton( + icon: _dislikeValue + ? DidvanIcons.dislike_solid + : DidvanIcons.dislike_regular, + color: _dislikeValue ? Theme.of(context).colorScheme.secondary : null, + gestureSize: 24, + onPressed: () { + setState(() { + if (_dislikeValue) { + _dislikeCount--; + } else { + _dislikeCount++; + if (_likeValue) { + _likeValue = false; + _likeCount--; + } + } + _dislikeValue = !_dislikeValue; + }); + widget.onFeedback(_likeValue, _dislikeValue); + }, + ), + ], + ); + } +} diff --git a/lib/views/home/direct/direct.dart b/lib/views/home/direct/direct.dart new file mode 100644 index 0000000..2af96ec --- /dev/null +++ b/lib/views/home/direct/direct.dart @@ -0,0 +1,101 @@ +import 'package:didvan/models/enums.dart'; +import 'package:didvan/models/view/app_bar_data.dart'; +import 'package:didvan/providers/server_data_provider.dart'; +import 'package:didvan/services/media/media.dart'; +import 'package:didvan/views/home/direct/direct_state.dart'; +import 'package:didvan/views/home/direct/widgets/message.dart'; +import 'package:didvan/views/home/direct/widgets/message_box.dart'; +import 'package:didvan/views/widgets/didvan/scaffold.dart'; +import 'package:didvan/views/widgets/state_handlers/sliver_state_handler.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_spinkit/flutter_spinkit.dart'; +import 'package:provider/provider.dart'; + +class Direct extends StatefulWidget { + final Map pageData; + const Direct({Key? key, required this.pageData}) : super(key: key); + + @override + State createState() => _DirectState(); +} + +class _DirectState extends State { + @override + void initState() { + final state = context.read(); + state.replyRadar = widget.pageData['radarAttachment']; + final typeId = ServerDataProvider.labelToTypeId(widget.pageData['type']); + state.typeId = typeId; + Future.delayed(Duration.zero, () { + state.getMessages(); + }); + super.initState(); + } + + @override + Widget build(BuildContext context) { + final state = context.watch(); + final d = MediaQuery.of(context); + return WillPopScope( + onWillPop: () async { + MediaService.resetAudioPlayer(); + return true; + }, + child: Material( + child: Stack( + children: [ + Positioned( + top: 0, + bottom: 56, + left: 0, + right: 0, + child: DidvanScaffold( + reverse: true, + backgroundColor: Theme.of(context).colorScheme.surface, + appBarData: AppBarData( + hasBack: true, + subtitle: 'ارتباط با سردبیر', + title: widget.pageData['type'] ?? 'پشتیبانی اپلیکیشن', + ), + slivers: [ + if (state.appState != AppState.busy) + SliverPadding( + padding: state.replyRadar == null + ? EdgeInsets.zero + : const EdgeInsets.only(bottom: 68), + sliver: SliverStateHandler( + itemPadding: const EdgeInsets.only(bottom: 12), + state: state, + builder: (context, state, index) => Message( + message: state.messages[index], + ), + childCount: state.messages.length, + onRetry: state.getMessages, + ), + ), + ], + children: [ + if (state.appState == AppState.busy) + SizedBox( + height: d.size.height - kToolbarHeight - d.padding.top, + child: Center( + child: SpinKitSpinningLines( + color: Theme.of(context).colorScheme.primary, + ), + ), + ), + ], + ), + ), + Positioned( + bottom: d.viewInsets.bottom, + right: 0, + left: 0, + child: const MessageBox(), + ), + ], + ), + ), + ); + } +} diff --git a/lib/views/home/direct/direct_state.dart b/lib/views/home/direct/direct_state.dart new file mode 100644 index 0000000..a4a14aa --- /dev/null +++ b/lib/views/home/direct/direct_state.dart @@ -0,0 +1,134 @@ +import 'dart:io'; + +import 'package:didvan/models/enums.dart'; +import 'package:didvan/models/message_data/message_data.dart'; +import 'package:didvan/models/message_data/radar_attachment.dart'; +import 'package:didvan/providers/core_provider.dart'; +import 'package:didvan/services/network/request.dart'; +import 'package:didvan/services/network/request_helper.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter_vibrate/flutter_vibrate.dart'; +import 'package:record/record.dart'; + +class DirectState extends CoreProvier { + final _recorder = Record(); + final List messages = []; + late final int typeId; + final Map> dailyMessages = {}; + + String? text; + RadarAttachment? replyRadar; + File? recordedFile; + int? audioDuration; + + bool isRecording = false; + + Future getMessages() async { + appState = AppState.busy; + final RequestService service = RequestService(RequestHelper.direct(typeId)); + await service.httpGet(); + if (service.isSuccess) { + final messageDatas = service.result['messages']; + for (var i = 0; i < messageDatas.length; i++) { + messages.add(MessageData.fromJson(messageDatas[i])); + _addToDailyGrouped(messages.last); + } + appState = AppState.idle; + return; + } + appState = AppState.failed; + } + + void deleteRecordedFile() { + recordedFile!.delete(); + recordedFile = null; + notifyListeners(); + } + + Future startRecording() async { + await _recorder.hasPermission(); + if (!kIsWeb) { + Vibrate.feedback(FeedbackType.medium); + } + isRecording = true; + _recorder.start(); + notifyListeners(); + } + + Future stopRecording({required bool sendImidiately}) async { + final path = await _recorder.stop(); + isRecording = false; + if (path == null) { + notifyListeners(); + return; + } + if (kIsWeb) { + final uri = Uri.file(path); + recordedFile = File.fromUri(uri); + } else { + recordedFile = File(path); + } + if (sendImidiately) { + await sendMessage(); + } else { + notifyListeners(); + } + } + + void _addToDailyGrouped(MessageData message) { + String createdAt = message.createdAt.replaceAll('T', ' ').split(' ').first; + if (!dailyMessages.containsKey(createdAt)) { + dailyMessages.addAll({ + createdAt: [messages.last.id] + }); + } else { + dailyMessages[createdAt]!.add(messages.last.id); + } + } + + Future sendMessage() async { + if ((text == null || text!.isEmpty) && recordedFile == null) return; + messages.insert( + 0, + MessageData( + id: 0, + writedByAdmin: false, + readed: false, + createdAt: + DateTime.now().subtract(const Duration(minutes: 210)).toString(), + text: text, + audio: null, + audioFile: recordedFile, + radar: replyRadar, + audioDuration: audioDuration, + ), + ); + _addToDailyGrouped(messages.first); + final body = {}; + if (text != null) { + body.addAll({'text': text}); + } + if (replyRadar != null) { + body.addAll({'radarId': replyRadar!.id}); + } + final uploadFile = recordedFile; + text = null; + recordedFile = null; + replyRadar = null; + notifyListeners(); + final service = + RequestService(RequestHelper.sendDirectMessage(typeId), body: body); + if (uploadFile == null) { + service.post(); + } else { + service.multipart( + file: uploadFile, + method: 'POST', + fieldName: 'audio', + fileName: 'voice-message', + mediaExtension: 'm4a', + mediaFormat: 'audio', + ); + } + } +} diff --git a/lib/views/home/direct/widgets/audio_widget.dart b/lib/views/home/direct/widgets/audio_widget.dart new file mode 100644 index 0000000..5bb7ba7 --- /dev/null +++ b/lib/views/home/direct/widgets/audio_widget.dart @@ -0,0 +1,35 @@ +import 'dart:io'; + +import 'package:didvan/services/media/media.dart'; +import 'package:didvan/views/home/widgets/audio_slider.dart'; +import 'package:didvan/views/home/widgets/player_controller_button.dart'; +import 'package:flutter/material.dart'; + +class AudioWidget extends StatelessWidget { + final String? audioUrl; + final File? audioFile; + const AudioWidget({Key? key, this.audioUrl, this.audioFile}) + : super(key: key); + + @override + Widget build(BuildContext context) { + return StreamBuilder( + stream: MediaService.audioPlayer.playingStream, + builder: (context, snapshot) { + return Row( + children: [ + Expanded( + child: AudioSlider( + tag: audioUrl ?? audioFile!.path, + ), + ), + AudioControllerButton( + audioFile: audioFile, + audioUrl: audioUrl, + ), + ], + ); + }, + ); + } +} diff --git a/lib/views/home/direct/widgets/message.dart b/lib/views/home/direct/widgets/message.dart new file mode 100644 index 0000000..49ab63d --- /dev/null +++ b/lib/views/home/direct/widgets/message.dart @@ -0,0 +1,188 @@ +import 'package:didvan/config/design_config.dart'; +import 'package:didvan/config/theme_data.dart'; +import 'package:didvan/constants/app_icons.dart'; +import 'package:didvan/models/message_data/message_data.dart'; +import 'package:didvan/utils/date_time.dart'; +import 'package:didvan/views/home/direct/direct_state.dart'; +import 'package:didvan/views/home/direct/widgets/audio_widget.dart'; +import 'package:didvan/views/widgets/didvan/divider.dart'; +import 'package:didvan/views/widgets/didvan/text.dart'; +import 'package:didvan/views/widgets/skeleton_image.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:persian_number_utility/persian_number_utility.dart'; + +class Message extends StatelessWidget { + final MessageData message; + const Message({Key? key, required this.message}) : super(key: key); + + @override + Widget build(BuildContext context) { + final firstMessageOfGroupId = context + .read() + .dailyMessages[message.createdAt.replaceAll('T', ' ').split(' ').first]! + .last; + return Column( + crossAxisAlignment: message.writedByAdmin + ? CrossAxisAlignment.end + : CrossAxisAlignment.start, + children: [ + if (message.id == firstMessageOfGroupId) + Center( + child: Container( + margin: const EdgeInsets.only(bottom: 12), + padding: const EdgeInsets.all(4), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.splash, + borderRadius: DesignConfig.lowBorderRadius, + ), + child: DidvanText( + DateTime.parse(message.createdAt).toPersianDateStr(), + style: Theme.of(context).textTheme.overline, + color: DesignConfig.isDark + ? Theme.of(context).colorScheme.white + : Theme.of(context).colorScheme.black, + ), + ), + ), + Padding( + padding: EdgeInsets.only( + right: message.writedByAdmin ? 20 : 0, + left: !message.writedByAdmin ? 20 : 0, + ), + child: Column( + crossAxisAlignment: message.writedByAdmin + ? CrossAxisAlignment.start + : CrossAxisAlignment.end, + children: [ + _MessageContainer( + writedByAdmin: message.writedByAdmin, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (message.text != null) DidvanText(message.text!), + if (message.audio != null || message.audioFile != null) + AudioWidget( + audioFile: message.audioFile, + audioUrl: message.audio, + ), + if (message.radar != null) const DidvanDivider(), + if (message.radar != null) const SizedBox(height: 4), + if (message.radar != null) + _ReplyRadarOverview(message: message), + if (message.radar != null) const SizedBox(height: 4), + ], + ), + ), + const SizedBox(height: 4), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + DidvanText( + DateTimeUtils.timeWithAmPm(message.createdAt), + style: Theme.of(context).textTheme.overline, + color: Theme.of(context).colorScheme.caption, + ), + if (!message.writedByAdmin) + Icon( + message.readed + ? DidvanIcons.check_double_light + : DidvanIcons.check_light, + size: 16, + ) + ], + ), + ], + ), + ), + ], + ); + } +} + +class _ReplyRadarOverview extends StatelessWidget { + final MessageData message; + const _ReplyRadarOverview({Key? key, required this.message}) + : super(key: key); + + @override + Widget build(BuildContext context) { + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SkeletonImage( + imageUrl: message.radar!.image, + height: 52, + width: 52, + ), + const SizedBox(width: 8), + Expanded( + child: SizedBox( + height: 52, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + DidvanText( + message.radar!.title, + style: Theme.of(context).textTheme.bodyText1, + maxLines: 1, + overflow: TextOverflow.ellipsis, + color: Theme.of(context).colorScheme.focusedBorder, + ), + Row( + children: [ + DidvanText( + 'رادار ' + message.radar!.categories.first.label, + style: Theme.of(context).textTheme.overline, + color: Theme.of(context).colorScheme.focusedBorder, + ), + const Spacer(), + DidvanText( + DateTimeUtils.momentGenerator(message.radar!.createdAt) + + ' | خواندن در ' + + message.radar!.timeToRead.toString() + + ' دقیقه', + color: Theme.of(context).colorScheme.focusedBorder, + style: Theme.of(context).textTheme.overline, + ), + // DidvanText('text'), + ], + ), + ], + ), + ), + ), + ], + ); + } +} + +class _MessageContainer extends StatelessWidget { + final bool writedByAdmin; + final Widget child; + const _MessageContainer({ + Key? key, + required this.writedByAdmin, + required this.child, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16), + decoration: BoxDecoration( + borderRadius: DesignConfig.mediumBorderRadius.copyWith( + bottomLeft: writedByAdmin ? Radius.zero : null, + bottomRight: !writedByAdmin ? Radius.zero : null, + ), + color: writedByAdmin ? null : Theme.of(context).colorScheme.focused, + border: Border.all( + color: Theme.of(context).colorScheme.border, + width: 0.5, + ), + ), + child: child, + ); + } +} diff --git a/lib/views/home/direct/widgets/message_box.dart b/lib/views/home/direct/widgets/message_box.dart new file mode 100644 index 0000000..897cff5 --- /dev/null +++ b/lib/views/home/direct/widgets/message_box.dart @@ -0,0 +1,218 @@ +import 'package:didvan/config/design_config.dart'; +import 'package:didvan/config/theme_data.dart'; +import 'package:didvan/constants/app_icons.dart'; +import 'package:didvan/views/home/direct/direct_state.dart'; +import 'package:didvan/views/home/direct/widgets/audio_widget.dart'; +import 'package:didvan/views/widgets/didvan/icon_button.dart'; +import 'package:didvan/views/widgets/didvan/text.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +class MessageBox extends StatelessWidget { + const MessageBox({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Column( + children: [ + Consumer( + builder: (context, state, child) => state.replyRadar != null + ? _MessageBoxContainer( + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const DidvanText( + 'لینک به مطلب:', + ), + DidvanText( + state.replyRadar!.title, + overflow: TextOverflow.ellipsis, + maxLines: 1, + color: Theme.of(context).colorScheme.primary, + ), + ], + ), + ), + DidvanIconButton( + icon: DidvanIcons.close_regular, + gestureSize: 24, + onPressed: () { + state.replyRadar = null; + state.update(); + }, + ), + ], + ), + ), + ) + : const SizedBox(), + ), + _MessageBoxContainer( + child: Consumer( + builder: (context, state, child) { + if (state.isRecording) { + return const _Recording(); + } else if (!state.isRecording && state.recordedFile != null) { + return const _RecordChecking(); + } + return const _Typing(); + }, + ), + ), + ], + ); + } +} + +class _MessageBoxContainer extends StatelessWidget { + final Widget child; + const _MessageBoxContainer({Key? key, required this.child}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Container( + height: 68, + decoration: BoxDecoration( + border: Border( + top: BorderSide( + color: Theme.of(context).colorScheme.cardBorder, + ), + ), + color: Theme.of(context).colorScheme.surface, + ), + child: child, + ); + } +} + +class _Typing extends StatefulWidget { + const _Typing({Key? key}) : super(key: key); + + @override + State<_Typing> createState() => _TypingState(); +} + +class _TypingState extends State<_Typing> { + final _formKey = GlobalKey(); + + @override + Widget build(BuildContext context) { + final state = context.watch(); + return Row( + children: [ + Expanded( + flex: 2, + child: AnimatedSwitcher( + duration: DesignConfig.lowAnimationDuration, + transitionBuilder: (child, animation) => ScaleTransition( + scale: animation, + child: child, + ), + child: state.text != null && state.text!.isNotEmpty + ? DidvanIconButton( + icon: DidvanIcons.send_solid, + onPressed: () { + _formKey.currentState!.reset(); + state.sendMessage(); + }, + size: 32, + color: Theme.of(context).colorScheme.focusedBorder, + ) + : DidvanIconButton( + icon: DidvanIcons.mic_solid, + onPressed: state.startRecording, + size: 32, + color: Theme.of(context).colorScheme.focusedBorder, + ), + ), + ), + Expanded( + flex: 15, + child: Form( + key: _formKey, + child: TextFormField( + textInputAction: TextInputAction.send, + style: Theme.of(context).textTheme.bodyText2, + decoration: InputDecoration( + border: InputBorder.none, + hintText: 'بنویسید یا پیام صوتی بگذارید...', + hintStyle: Theme.of(context).textTheme.caption!.copyWith( + color: Theme.of(context).colorScheme.disabledText), + ), + onChanged: (value) { + if (value.length <= 1) { + setState(() {}); + } + state.text = value; + }, + ), + ), + ), + ], + ); + } +} + +class _Recording extends StatelessWidget { + const _Recording({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + final state = context.read(); + return Row( + children: [ + DidvanIconButton( + icon: DidvanIcons.send_solid, + onPressed: () => state.stopRecording(sendImidiately: true), + gestureSize: 52, + ), + Expanded( + child: DidvanText( + 'در حال ضبط صدا ...', + style: Theme.of(context).textTheme.caption, + ), + ), + DidvanIconButton( + icon: DidvanIcons.stop_circle_solid, + color: Theme.of(context).colorScheme.secondary, + onPressed: () => state.stopRecording(sendImidiately: false), + size: 32, + ), + ], + ); + } +} + +class _RecordChecking extends StatelessWidget { + const _RecordChecking({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + final state = context.read(); + return Row( + children: [ + DidvanIconButton( + icon: DidvanIcons.send_solid, + onPressed: state.sendMessage, + color: Theme.of(context).colorScheme.focusedBorder, + ), + Expanded( + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: AudioWidget(audioFile: state.recordedFile!), + ), + ), + DidvanIconButton( + icon: DidvanIcons.trash_solid, + color: Theme.of(context).colorScheme.secondary, + onPressed: state.deleteRecordedFile, + ), + ], + ); + } +} diff --git a/lib/views/home/hashtag/hashtag.dart b/lib/views/home/hashtag/hashtag.dart new file mode 100644 index 0000000..2e94ce0 --- /dev/null +++ b/lib/views/home/hashtag/hashtag.dart @@ -0,0 +1,67 @@ +import 'package:didvan/models/tag.dart'; +import 'package:didvan/models/view/app_bar_data.dart'; +import 'package:didvan/views/home/hashtag/hashtag_state.dart'; +import 'package:didvan/views/home/widgets/news_overview.dart'; +import 'package:didvan/views/home/widgets/radar_overview.dart'; +import 'package:didvan/views/widgets/didvan/scaffold.dart'; +import 'package:didvan/views/widgets/state_handlers/sliver_state_handler.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +class Hashtag extends StatefulWidget { + final Tag tag; + const Hashtag({Key? key, required this.tag}) : super(key: key); + + @override + _HashtagState createState() => _HashtagState(); +} + +class _HashtagState extends State { + @override + void initState() { + final state = context.read(); + state.id = widget.tag.id; + Future.delayed(Duration.zero, () => state.getTagItems(page: 1)); + super.initState(); + } + + @override + Widget build(BuildContext context) { + return DidvanScaffold( + appBarData: AppBarData(title: '#' + widget.tag.label, hasBack: true), + slivers: [ + Consumer( + builder: (context, state, child) => SliverStateHandler( + itemPadding: const EdgeInsets.only(bottom: 8), + state: state, + placeholder: RadarOverview.placeholder, + builder: (context, state, index) { + index++; + if (index % 15 == 0 && index / 15 >= state.page) { + state.getTagItems(page: index ~/ 15 + 1); + } + index--; + final item = state.items[index]; + final type = item.type; + if (type == 'radar') { + return RadarOverview( + radar: item, + onCommentsChanged: (id, count) => item.comments = count, + onMarkChanged: (id, value) => item.marked = value, + ); + } else if (type == 'news') { + return NewsOverview( + news: item, + onMarkChanged: (id, value) => item.marked = value, + ); + } + return Container(); + }, + childCount: state.items.length, + onRetry: () => state.getTagItems(page: 1), + ), + ) + ], + ); + } +} diff --git a/lib/views/home/hashtag/hashtag_state.dart b/lib/views/home/hashtag/hashtag_state.dart new file mode 100644 index 0000000..a94cdbe --- /dev/null +++ b/lib/views/home/hashtag/hashtag_state.dart @@ -0,0 +1,36 @@ +import 'package:didvan/models/enums.dart'; +import 'package:didvan/models/overview_data.dart'; +import 'package:didvan/providers/core_provider.dart'; +import 'package:didvan/services/network/request.dart'; +import 'package:didvan/services/network/request_helper.dart'; + +class HashtagState extends CoreProvier { + final List items = []; + + late final int id; + int page = 1; + + Future getTagItems({required int page}) async { + this.page = page; + if (this.page == 1) { + appState = AppState.busy; + } + final service = RequestService(RequestHelper.tag( + ids: [id], + itemId: 1, + type: 'radar', + limit: 15, + page: page, + )); + await service.httpGet(); + if (service.isSuccess) { + final contents = service.result['contents']; + for (var i = 0; i < contents.length; i++) { + items.add(OverviewData.fromJson(contents[i])); + } + appState = AppState.idle; + return; + } + appState = AppState.failed; + } +} diff --git a/lib/views/home/home.dart b/lib/views/home/home.dart new file mode 100644 index 0000000..0e1d5a8 --- /dev/null +++ b/lib/views/home/home.dart @@ -0,0 +1,58 @@ +import 'package:didvan/config/design_config.dart'; +import 'package:didvan/views/home/home_state.dart'; +import 'package:didvan/views/home/news/news.dart'; +import 'package:didvan/views/home/radar/radar.dart'; +import 'package:didvan/views/home/settings/settings.dart'; +import 'package:didvan/views/home/statistics/statistics.dart'; +import 'package:didvan/views/home/studio/studio.dart'; +import 'package:didvan/views/home/widgets/bnb.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +class Home extends StatefulWidget { + const Home({Key? key}) : super(key: key); + + @override + State createState() => _HomeState(); +} + +class _HomeState extends State with SingleTickerProviderStateMixin { + late final TabController _tabController; + + @override + void initState() { + DesignConfig.updateSystemUiOverlayStyle(); + _tabController = TabController(length: 5, vsync: this, initialIndex: 2); + _tabController.addListener(() { + context.read().currentPageIndex = _tabController.index; + }); + super.initState(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + body: TabBarView( + physics: const NeverScrollableScrollPhysics(), + controller: _tabController, + children: const [ + News(), + Statictics(), + Radar(), + Studio(), + Settings(), + ], + ), + bottomNavigationBar: Consumer( + builder: (context, state, child) => DidvanBNB( + currentTabIndex: state.currentPageIndex, + onTabChanged: (index) { + state.currentPageIndex = index; + FocusScope.of(context).unfocus(); + _tabController.animateTo(index); + }, + ), + ), + ); + } +} diff --git a/lib/views/home/home_state.dart b/lib/views/home/home_state.dart new file mode 100644 index 0000000..ed4ea99 --- /dev/null +++ b/lib/views/home/home_state.dart @@ -0,0 +1,12 @@ +import 'package:didvan/providers/core_provider.dart'; + +class HomeState extends CoreProvier { + int _currentPageIndex = 2; + + set currentPageIndex(int value) { + _currentPageIndex = value; + notifyListeners(); + } + + int get currentPageIndex => _currentPageIndex; +} diff --git a/lib/views/home/news/news.dart b/lib/views/home/news/news.dart new file mode 100644 index 0000000..2911204 --- /dev/null +++ b/lib/views/home/news/news.dart @@ -0,0 +1,149 @@ +import 'dart:async'; + +import 'package:didvan/constants/app_icons.dart'; +import 'package:didvan/models/enums.dart'; +import 'package:didvan/models/requests/news.dart'; +import 'package:didvan/models/view/action_sheet_data.dart'; +import 'package:didvan/utils/action_sheet.dart'; +import 'package:didvan/views/home/news/news_state.dart'; +import 'package:didvan/views/home/widgets/date_picker_button.dart'; +import 'package:didvan/views/home/widgets/logo_app_bar.dart'; +import 'package:didvan/views/home/widgets/news_overview.dart'; +import 'package:didvan/views/home/widgets/search_field.dart'; +import 'package:didvan/views/widgets/item_title.dart'; +import 'package:didvan/views/widgets/state_handlers/empty_result.dart'; +import 'package:didvan/views/widgets/state_handlers/sliver_state_handler.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +class News extends StatefulWidget { + const News({Key? key}) : super(key: key); + + @override + State createState() => _NewsState(); +} + +class _NewsState extends State { + Timer? _timer; + final _focusNode = FocusNode(); + @override + void initState() { + context.read().init(); + super.initState(); + } + + @override + Widget build(BuildContext context) { + final state = context.watch(); + return CustomScrollView( + slivers: [ + const SliverToBoxAdapter(child: LogoAppBar()), + if (state.appState != AppState.failed) + SliverPadding( + padding: const EdgeInsets.only(left: 16, right: 16, bottom: 16), + sliver: SliverToBoxAdapter( + child: SearchField( + focusNode: _focusNode, + title: 'اخبار', + onChanged: _onChanged, + onFilterButtonPressed: _showFilterBottomSheet, + isFiltered: state.isFiltering, + ), + ), + ), + SliverStateHandler( + onRetry: () => state.getNews(page: state.page), + state: state, + builder: (context, state, index) { + index += 2; + if (index % 15 == 0 && state.lastPage != state.page) { + state.getNews(page: index ~/ 15 + 1); + } + index -= 2; + if (index >= state.news.length) { + return NewsOverview.placeholder; + } + final news = state.news[index]; + return NewsOverview( + news: news, + onMarkChanged: (id, value) => state.onMarkChanged(id, value), + newsRequestArgs: NewsRequestArgs( + page: state.page, + endDate: state.endDate, + startDate: state.startDate, + search: state.search, + ), + ); + }, + enableEmptyState: state.news.isEmpty, + emptyState: EmptyResult( + onNewSearch: () => _focusNode.requestFocus(), + ), + childCount: + state.news.length + (state.lastPage == state.page ? 0 : 3), + itemPadding: const EdgeInsets.only(left: 16, right: 16, bottom: 16), + placeholder: NewsOverview.placeholder, + ), + ], + ); + } + + void _onChanged(String value) { + final state = context.read(); + if (value.length < 4 && value.isNotEmpty || state.lastSearch == value) { + return; + } + _timer?.cancel(); + _timer = Timer(const Duration(seconds: 1), () { + state.search = value; + state.getNews(page: 1); + }); + } + + Future _showFilterBottomSheet() async { + final state = context.read(); + await ActionSheetUtils.showBottomSheet( + data: ActionSheetData( + title: 'فیلتر جستجو', + smallDismissButton: true, + titleIcon: DidvanIcons.filter_regular, + dismissTitle: 'حذف فیلتر', + confrimTitle: 'نمایش نتایج', + onDismissed: state.resetFilters, + onConfirmed: () => state.getNews(page: 1), + content: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ItemTitle( + title: 'تاریخ خبر', + style: Theme.of(context).textTheme.bodyText2, + icon: DidvanIcons.calendar_range_regular, + ), + const SizedBox(height: 8), + StatefulBuilder( + builder: (context, setState) => Row( + children: [ + DatePickerButton( + initialValue: state.startDate, + emptyText: 'از تاریخ', + onPicked: (date) { + setState(() => state.startDate = date); + }, + lastDate: state.endDate, + ), + const SizedBox(width: 8), + DatePickerButton( + initialValue: state.endDate, + emptyText: 'تا تاریخ', + onPicked: (date) => setState(() => state.endDate = date), + firstDate: state.startDate, + ), + ], + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/views/home/news/news_details/news_details.dart b/lib/views/home/news/news_details/news_details.dart new file mode 100644 index 0000000..55d09d4 --- /dev/null +++ b/lib/views/home/news/news_details/news_details.dart @@ -0,0 +1,90 @@ +import 'package:didvan/models/requests/news.dart'; +import 'package:didvan/views/home/news/news_details/news_details_state.dart'; +import 'package:didvan/views/home/widgets/floating_navigation_bar.dart'; +import 'package:didvan/views/widgets/didvan/page_view.dart'; +import 'package:didvan/views/widgets/state_handlers/state_handler.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +class NewsDetails extends StatefulWidget { + final Map pageData; + const NewsDetails({Key? key, required this.pageData}) : super(key: key); + + @override + State createState() => _NewsDetailsState(); +} + +class _NewsDetailsState extends State { + final ScrollController _scrollController = ScrollController(); + + @override + void initState() { + final state = context.read(); + _scrollController.addListener(() { + if (_scrollController.position.pixels > + _scrollController.position.maxScrollExtent - 200 && + !state.relatedQueue.contains(state.currentNews.id)) { + state.getRelatedContents(); + } + }); + state.args = widget.pageData['args'] ?? const NewsRequestArgs(page: 0); + Future.delayed(Duration.zero, () { + state.getNewsDetails(widget.pageData['id']); + }); + super.initState(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Consumer( + builder: (context, state, child) => StateHandler( + onRetry: () => state.getNewsDetails(state.currentNews.id), + state: state, + builder: (context, state) => Stack( + children: [ + if (state.news.isNotEmpty) + IgnorePointer( + ignoring: state.isFetchingNewItem, + child: DidvanPageView( + isRadar: false, + initialIndex: state.initialIndex, + onPageChanged: _onPageChnaged, + scrollController: _scrollController, + items: state.news, + currentIndex: state.currentIndex, + ), + ), + if (state.news.isNotEmpty) + Positioned( + bottom: 0, + left: 0, + right: 0, + child: FloatingNavigationBar( + hasUnmarkConfirmation: + widget.pageData['hasUnmarkConfirmation'], + scrollController: _scrollController, + item: state.currentNews, + onCommentsChanged: state.onCommentsChanged, + onMarkChanged: (value) => widget.pageData['onMarkChanged']( + state.currentNews.id, + value, + ), + isRadar: false, + ), + ), + ], + ), + ), + ), + ); + } + + void _onPageChnaged(int index) { + final state = context.read(); + state.getNewsDetails( + state.news[index]!.id, + isForward: state.currentIndex < index, + ); + } +} diff --git a/lib/views/home/news/news_details/news_details_state.dart b/lib/views/home/news/news_details/news_details_state.dart new file mode 100644 index 0000000..879a178 --- /dev/null +++ b/lib/views/home/news/news_details/news_details_state.dart @@ -0,0 +1,129 @@ +import 'dart:async'; +import 'dart:math'; + +import 'package:didvan/models/enums.dart'; +import 'package:didvan/models/news_details_data.dart'; +import 'package:didvan/models/overview_data.dart'; +import 'package:didvan/models/requests/news.dart'; +import 'package:didvan/providers/core_provider.dart'; +import 'package:didvan/services/network/request.dart'; +import 'package:didvan/services/network/request_helper.dart'; + +class NewsDetailsState extends CoreProvier { + final List news = []; + late final int initialIndex; + late final NewsRequestArgs args; + late Timer _trackingTimer; + int _trackingTimerCounter = 0; + bool isFetchingNewItem = false; + final List relatedQueue = []; + + int _currentIndex = 0; + int get currentIndex => _currentIndex; + + NewsDetailsData get currentNews => news[_currentIndex]!; + + Future getNewsDetails(int id, {bool? isForward}) async { + if (isForward == null) { + appState = AppState.busy; + } else { + isFetchingNewItem = true; + notifyListeners(); + } + final service = RequestService(RequestHelper.newsDetails(id, args)); + await service.httpGet(); + _handleTracking(sendRequest: isForward != null); + if (service.isSuccess) { + final result = service.result; + final newsItem = NewsDetailsData.fromJson(result['news']); + if (args.page == 0) { + news.add(newsItem); + initialIndex = 0; + appState = AppState.idle; + return; + } + NewsDetailsData? prevNews; + if (result['prevNews'].isNotEmpty) { + prevNews = NewsDetailsData.fromJson(result['prevNews']); + } + NewsDetailsData? nextNews; + if (result['nextNews'].isNotEmpty) { + nextNews = NewsDetailsData.fromJson(result['nextNews']); + } + if (isForward == null) { + news.addAll(List.generate(max(newsItem.order - 2, 0), (index) => null)); + if (prevNews != null) { + news.add(prevNews); + } + news.add(newsItem); + if (nextNews != null) { + news.add(nextNews); + } + _currentIndex = initialIndex = newsItem.order - 1; + } else if (isForward) { + if (!exists(nextNews) && nextNews != null) { + news.add(nextNews); + } + _currentIndex++; + } else if (!isForward) { + if (!exists(prevNews) && prevNews != null) { + news[_currentIndex - 2] = prevNews; + } + _currentIndex--; + } + isFetchingNewItem = false; + appState = AppState.idle; + return; + } + if (isForward == null) { + appState = AppState.failed; + } + } + + bool exists(NewsDetailsData? newsItem) => + news.any((n) => newsItem != null && n != null && n.id == newsItem.id); + + void onCommentsChanged(int count) { + news.firstWhere((item) => item!.id == currentNews.id)!.comments = count; + notifyListeners(); + } + + Future _handleTracking({bool sendRequest = true}) async { + if (!sendRequest) { + _trackingTimer = Timer.periodic(const Duration(seconds: 1), (timer) { + _trackingTimerCounter++; + }); + return; + } + //send request + _trackingTimerCounter = 0; + } + + Future getRelatedContents() async { + if (currentNews.relatedContents.isNotEmpty) return; + relatedQueue.add(currentNews.id); + final service = RequestService(RequestHelper.tag( + ids: currentNews.tags.map((tag) => tag.id).toList(), + itemId: currentNews.id, + type: 'news', + )); + await service.httpGet(); + if (service.isSuccess) { + final relateds = service.result['contents']; + for (var i = 0; i < relateds.length; i++) { + news + .where((element) => element != null) + .firstWhere((element) => element!.id == currentNews.id)! + .relatedContents + .add(OverviewData.fromJson(relateds[i])); + } + notifyListeners(); + } + } + + @override + void dispose() { + _trackingTimer.cancel(); + super.dispose(); + } +} diff --git a/lib/views/home/news/news_state.dart b/lib/views/home/news/news_state.dart new file mode 100644 index 0000000..47f3366 --- /dev/null +++ b/lib/views/home/news/news_state.dart @@ -0,0 +1,79 @@ +import 'package:didvan/models/enums.dart'; +import 'package:didvan/models/overview_data.dart'; +import 'package:didvan/models/requests/news.dart'; +import 'package:didvan/providers/core_provider.dart'; +import 'package:didvan/providers/user_provider.dart'; +import 'package:didvan/services/network/request.dart'; +import 'package:didvan/services/network/request_helper.dart'; + +class NewsState extends CoreProvier { + String search = ''; + String lastSearch = ''; + String? startDate; + String? endDate; + int page = 1; + int lastPage = 0; + + final List news = []; + + void init() { + search = ''; + lastSearch = ''; + startDate = null; + endDate = null; + Future.delayed(Duration.zero, () { + getNews(page: 1); + }); + } + + void resetFilters() { + startDate = null; + endDate = null; + getNews(page: 1); + } + + Future getNews({ + required int page, + }) async { + this.page = page; + if (this.page == 1) { + news.clear(); + } + if (search != '') { + lastSearch = search; + } + lastSearch = search; + if (page == 1) { + appState = AppState.busy; + } + final service = RequestService( + RequestHelper.newsOverviews( + args: NewsRequestArgs( + page: page, + startDate: startDate?.split(' ').first, + endDate: endDate?.split(' ').first, + search: search == '' ? null : search, + ), + ), + ); + await service.httpGet(); + if (service.isSuccess) { + lastPage = service.result['lastPage']; + final newsList = service.result['news']; + for (var i = 0; i < newsList.length; i++) { + news.add(OverviewData.fromJson(newsList[i])); + } + appState = AppState.idle; + return; + } + appState = AppState.failed; + } + + Future onMarkChanged(int id, bool value) async { + news.firstWhere((element) => element.id == id).marked = value; + notifyListeners(); + UserProvider.changeNewsMark(id, value); + } + + bool get isFiltering => startDate != null || endDate != null; +} diff --git a/lib/views/home/radar/radar.dart b/lib/views/home/radar/radar.dart new file mode 100644 index 0000000..1a241b0 --- /dev/null +++ b/lib/views/home/radar/radar.dart @@ -0,0 +1,296 @@ +// ignore_for_file: prefer_const_constructors + +import 'dart:async'; +import 'dart:math'; + +import 'package:didvan/config/design_config.dart'; +import 'package:didvan/config/theme_data.dart'; +import 'package:didvan/constants/app_icons.dart'; +import 'package:didvan/models/enums.dart'; +import 'package:didvan/models/requests/radar.dart'; +import 'package:didvan/models/view/action_sheet_data.dart'; +import 'package:didvan/views/home/radar/radar_state.dart'; +import 'package:didvan/views/home/radar/widgets/categories_gird.dart'; +import 'package:didvan/views/home/radar/widgets/categories_list.dart'; +import 'package:didvan/views/home/widgets/date_picker_button.dart'; +import 'package:didvan/views/home/widgets/logo_app_bar.dart'; +import 'package:didvan/utils/action_sheet.dart'; +import 'package:didvan/views/home/widgets/radar_overview.dart'; +import 'package:didvan/views/home/widgets/search_field.dart'; +import 'package:didvan/views/widgets/animated_visibility.dart'; +import 'package:didvan/views/widgets/didvan/checkbox.dart'; +import 'package:didvan/views/widgets/didvan/text.dart'; +import 'package:didvan/views/widgets/item_title.dart'; +import 'package:didvan/views/widgets/state_handlers/empty_result.dart'; +import 'package:didvan/views/widgets/state_handlers/sliver_state_handler.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +class Radar extends StatefulWidget { + const Radar({Key? key}) : super(key: key); + + @override + State createState() => _RadarState(); +} + +class _RadarState extends State { + final ScrollController _scrollController = ScrollController(); + final _focusNode = FocusNode(); + + bool _isAnimating = false; + + Timer? _timer; + + @override + void initState() { + _scrollController.addListener(() { + _handleAnimations(); + }); + final state = context.read(); + state.addListener(() { + if (state.shouldColapse && mounted) { + _handleAnimations(true); + state.shouldColapse = false; + } + }); + state.init(); + super.initState(); + } + + @override + Widget build(BuildContext context) { + return Consumer( + builder: (context, state, child) => Stack( + children: [ + CustomScrollView( + physics: _isAnimating + ? const NeverScrollableScrollPhysics() + : const ClampingScrollPhysics(), + controller: _scrollController, + slivers: [ + const SliverToBoxAdapter(child: LogoAppBar()), + if (state.appState != AppState.failed) + SliverPadding( + padding: const EdgeInsets.only( + left: 16, + right: 16, + bottom: 16, + ), + sliver: SliverToBoxAdapter( + child: SearchField( + focusNode: _focusNode, + isFiltered: state.filtering, + title: 'رادار', + onChanged: _onChanged, + onFilterButtonPressed: _showFilterBottomSheet, + ), + ), + ), + if (!state.filtering && + !state.searching && + state.appState != AppState.failed) + const SliverToBoxAdapter( + child: SizedBox(height: 276), + ), + if (state.appState != AppState.failed) + SliverPadding( + padding: const EdgeInsets.only(right: 16, bottom: 20), + sliver: SliverToBoxAdapter( + child: Align( + alignment: Alignment.centerRight, + child: AnimatedVisibility( + isVisible: !state.isColapsed && + !state.searching && + !state.filtering, + duration: DesignConfig.lowAnimationDuration, + child: DidvanText( + 'آخرین رصد', + style: Theme.of(context).textTheme.subtitle1, + color: Theme.of(context).colorScheme.title, + ), + ), + ), + ), + ), + SliverStateHandler( + onRetry: () => state.getRadars(page: state.page), + state: state, + itemPadding: const EdgeInsets.only( + bottom: 20, + left: 16, + right: 16, + ), + enableEmptyState: state.radars.isEmpty, + emptyState: Padding( + padding: const EdgeInsets.only(bottom: 120), + child: + EmptyResult(onNewSearch: () => _focusNode.requestFocus()), + ), + placeholder: RadarOverview.placeholder, + builder: (context, state, index) { + index += 2; + if (index % 15 == 0 && state.lastPage != state.page) { + state.getRadars(page: index ~/ 15 + 1); + } + index -= 2; + if (index >= state.radars.length) { + return RadarOverview.placeholder; + } + final radar = state.radars[index]; + return RadarOverview( + radar: radar, + onMarkChanged: (id, value) => state.changeMark(id, value), + onCommentsChanged: (id, count) => + state.onCommentsChanged(id, count), + radarRequestArgs: RadarRequestArgs( + page: state.page, + categories: + List.from(state.selectedCats.map((cat) => cat.id)), + endDate: state.endDate, + isSingleItem: false, + search: state.search, + startDate: state.startDate, + ), + ); + }, + childCount: state.radars.length + + (state.lastPage == state.page ? 0 : 3), + ), + if (state.radars.length == 1) + const SliverToBoxAdapter( + child: SizedBox(height: 320), + ), + ], + ), + if (state.appState != AppState.failed) CategoriesRow1(), + if (state.appState != AppState.failed) CategoriesRow2(), + if (state.appState != AppState.failed && + !state.searching && + !state.filtering) + CategoriesList(), + ], + ), + ); + } + + void _onChanged(String value) { + final state = context.read(); + if (value.length < 4 && value.isNotEmpty || state.lastSearch == value) { + return; + } + _timer?.cancel(); + _timer = Timer(const Duration(seconds: 1), () { + state.search = value; + state.getRadars(page: 1); + }); + } + + void _handleAnimations([bool forceAnimate = false]) async { + final state = context.read(); + if (_isAnimating || state.filtering || state.searching) return; + final double position = _scrollController.offset; + if (position > 5 && !state.isColapsed || forceAnimate) { + state.isScrolled = true; + _isAnimating = true; + setState(() {}); + await _scrollController.animateTo( + 380, + duration: DesignConfig.mediumAnimationDuration, + curve: Curves.easeIn, + ); + _isAnimating = false; + setState(() {}); + } else if (position < + min(_scrollController.position.maxScrollExtent, 380) && + state.isColapsed) { + state.isScrolled = false; + _isAnimating = true; + setState(() {}); + await _scrollController.animateTo( + 0, + duration: DesignConfig.mediumAnimationDuration, + curve: Curves.easeIn, + ); + _isAnimating = false; + setState(() {}); + } + } + + Future _showFilterBottomSheet() async { + final state = context.read(); + await ActionSheetUtils.showBottomSheet( + data: ActionSheetData( + title: 'فیلتر جستجو', + smallDismissButton: true, + titleIcon: DidvanIcons.filter_regular, + dismissTitle: 'حذف فیلتر', + confrimTitle: 'نمایش نتایج', + onDismissed: () => state.resetFilters(false), + onConfirmed: () => state.getRadars(page: 1), + content: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ItemTitle( + title: 'تاریخ رادار', + style: Theme.of(context).textTheme.bodyText2, + icon: DidvanIcons.calendar_range_regular, + ), + const SizedBox(height: 8), + StatefulBuilder( + builder: (context, setState) => Row( + children: [ + DatePickerButton( + initialValue: state.startDate, + emptyText: 'از تاریخ', + onPicked: (date) => setState(() => state.startDate = date), + lastDate: state.endDate, + ), + const SizedBox(width: 8), + DatePickerButton( + initialValue: state.endDate, + emptyText: 'تا تاریخ', + onPicked: (date) => setState(() => state.endDate = date), + firstDate: state.startDate, + ), + ], + ), + ), + const SizedBox(height: 28), + ItemTitle( + title: 'دسته بندی', + icon: DidvanIcons.radar_regular, + style: Theme.of(context).textTheme.bodyText2, + ), + const SizedBox(height: 12), + Wrap( + children: [ + for (var i = 0; i < state.categories.length; i++) + SizedBox( + width: (MediaQuery.of(context).size.width - 40) / 2, + child: DidvanCheckbox( + title: state.categories[i].title, + value: state.selectedCats.contains(state.categories[i]), + onChanged: (value) { + if (value) { + state.selectedCats.add(state.categories[i]); + return; + } + state.selectedCats.remove(state.categories[i]); + }, + ), + ), + ], + ), + ], + ), + ), + ); + } + + @override + void dispose() { + _focusNode.dispose(); + _scrollController.dispose(); + super.dispose(); + } +} diff --git a/lib/views/home/radar/radar_details/radar_details.dart b/lib/views/home/radar/radar_details/radar_details.dart new file mode 100644 index 0000000..1c9eb11 --- /dev/null +++ b/lib/views/home/radar/radar_details/radar_details.dart @@ -0,0 +1,97 @@ +import 'package:didvan/models/requests/radar.dart'; +import 'package:didvan/views/home/radar/radar_details/radar_details_state.dart'; +import 'package:didvan/views/home/widgets/floating_navigation_bar.dart'; +import 'package:didvan/views/widgets/didvan/page_view.dart'; +import 'package:didvan/views/widgets/state_handlers/state_handler.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +class RadarDetails extends StatefulWidget { + final Map pageData; + const RadarDetails({Key? key, required this.pageData}) : super(key: key); + + @override + State createState() => _RadarDetailsState(); +} + +class _RadarDetailsState extends State { + final ScrollController _scrollController = ScrollController(); + + @override + void initState() { + final state = context.read(); + _scrollController.addListener(() { + if (_scrollController.position.pixels > + _scrollController.position.maxScrollExtent - 200 && + !state.relatedQueue.contains(state.currentRadar.id)) { + state.getRelatedContents(); + } + }); + state.args = widget.pageData['args'] ?? const RadarRequestArgs(page: 0); + Future.delayed(Duration.zero, () { + state.getRadarDetails(widget.pageData['id']); + }); + super.initState(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Consumer( + builder: (context, state, child) => StateHandler( + onRetry: () => state.getRadarDetails(widget.pageData['id']), + state: state, + builder: (context, state) => Stack( + children: [ + if (state.radars.isNotEmpty) + IgnorePointer( + ignoring: state.isFetchingNewItem, + child: DidvanPageView( + isRadar: true, + initialIndex: state.initialIndex, + onPageChanged: _onPageChanged, + scrollController: _scrollController, + items: state.radars, + currentIndex: state.currentIndex, + ), + ), + if (state.radars.isNotEmpty) + Positioned( + bottom: 0, + left: 0, + right: 0, + child: FloatingNavigationBar( + hasUnmarkConfirmation: + widget.pageData['hasUnmarkConfirmation'], + isRadar: true, + scrollController: _scrollController, + onMarkChanged: (value) => + widget.pageData['onMarkChanged']?.call( + state.currentRadar.id, + value, + ), + item: state.currentRadar, + onCommentsChanged: (count) { + state.onCommentsChanged(count); + widget.pageData['onCommentsChanged']?.call( + state.currentRadar.id, + count, + ); + }, + ), + ), + ], + ), + ), + ), + ); + } + + void _onPageChanged(int index) { + final state = context.read(); + state.getRadarDetails( + state.radars[index]!.id, + isForward: state.currentIndex < index, + ); + } +} diff --git a/lib/views/home/radar/radar_details/radar_details_state.dart b/lib/views/home/radar/radar_details/radar_details_state.dart new file mode 100644 index 0000000..2095cec --- /dev/null +++ b/lib/views/home/radar/radar_details/radar_details_state.dart @@ -0,0 +1,140 @@ +import 'dart:async'; +import 'dart:math'; + +import 'package:didvan/models/enums.dart'; +import 'package:didvan/models/overview_data.dart'; +import 'package:didvan/models/radar_details_data.dart'; +import 'package:didvan/models/requests/radar.dart'; +import 'package:didvan/providers/core_provider.dart'; +import 'package:didvan/services/network/request.dart'; +import 'package:didvan/services/network/request_helper.dart'; + +class RadarDetailsState extends CoreProvier { + final List radars = []; + late Timer _trackingTimer; + int _trackingTimerCounter = 0; + late final int initialIndex; + late final RadarRequestArgs args; + bool isFetchingNewItem = false; + final List relatedQueue = []; + + int _currentIndex = 0; + int get currentIndex => _currentIndex; + + RadarDetailsData get currentRadar { + try { + return radars[_currentIndex]!; + } catch (e) { + return radars[_currentIndex + 1]!; + } + } + + Future getRadarDetails(int id, {bool? isForward}) async { + if (isForward == null) { + appState = AppState.busy; + } else { + isFetchingNewItem = true; + notifyListeners(); + } + final service = RequestService(RequestHelper.radarDetails(id, args)); + await service.httpGet(); + _handleTracking(sendRequest: isForward != null); + if (service.isSuccess) { + final result = service.result; + final radar = RadarDetailsData.fromJson(result['radar']); + if (args.page == 0) { + radars.add(radar); + initialIndex = 0; + appState = AppState.idle; + return; + } + + RadarDetailsData? prevRadar; + if (result['prevRadar'].isNotEmpty) { + prevRadar = RadarDetailsData.fromJson(result['prevRadar']); + } + + RadarDetailsData? nextRadar; + if (result['nextRadar'].isNotEmpty) { + nextRadar = RadarDetailsData.fromJson(result['nextRadar']); + } + + if (isForward == null) { + radars.addAll(List.generate(max(radar.order - 2, 0), (index) => null)); + if (prevRadar != null) { + radars.add(prevRadar); + } + radars.add(radar); + if (nextRadar != null) { + radars.add(nextRadar); + } + _currentIndex = initialIndex = radar.order - 1; + } else if (isForward) { + if (!exists(nextRadar) && nextRadar != null) { + radars.add(nextRadar); + } + _currentIndex++; + } else if (!isForward) { + if (!exists(prevRadar) && prevRadar != null) { + radars[_currentIndex - 2] = prevRadar; + } + _currentIndex--; + } + isFetchingNewItem = false; + appState = AppState.idle; + return; + } + //why? total page state shouldn't die! + if (isForward == null) { + appState = AppState.failed; + } + } + + Future getRelatedContents() async { + if (currentRadar.relatedContents.isNotEmpty) return; + relatedQueue.add(currentRadar.id); + final service = RequestService(RequestHelper.tag( + ids: currentRadar.tags.map((tag) => tag.id).toList(), + itemId: currentRadar.id, + type: 'radar', + )); + await service.httpGet(); + if (service.isSuccess) { + final relateds = service.result['contents']; + for (var i = 0; i < relateds.length; i++) { + radars + .where((element) => element != null) + .firstWhere((element) => element!.id == currentRadar.id)! + .relatedContents + .add(OverviewData.fromJson(relateds[i])); + } + notifyListeners(); + } + } + + bool exists(RadarDetailsData? radar) => + radars.any((r) => radar != null && r != null && r.id == radar.id); + + void onCommentsChanged(int count) { + radars.firstWhere((radar) => radar!.id == currentRadar.id)!.comments = + count; + notifyListeners(); + } + + Future _handleTracking({bool sendRequest = true}) async { + if (!sendRequest) { + _trackingTimer = Timer.periodic(const Duration(seconds: 1), (timer) { + _trackingTimerCounter++; + }); + return; + } + //send request + _trackingTimerCounter = 0; + } + + @override + void dispose() { + _trackingTimer.cancel(); + super.dispose(); + } +} diff --git a/lib/views/home/radar/radar_state.dart b/lib/views/home/radar/radar_state.dart new file mode 100644 index 0000000..75318ee --- /dev/null +++ b/lib/views/home/radar/radar_state.dart @@ -0,0 +1,134 @@ +import 'package:didvan/constants/assets.dart'; +import 'package:didvan/models/enums.dart'; +import 'package:didvan/models/overview_data.dart'; +import 'package:didvan/models/requests/radar.dart'; +import 'package:didvan/models/view/radar_category.dart'; +import 'package:didvan/providers/core_provider.dart'; +import 'package:didvan/providers/user_provider.dart'; +import 'package:didvan/services/network/request.dart'; +import 'package:didvan/services/network/request_helper.dart'; + +class RadarState extends CoreProvier { + String search = ''; + String lastSearch = ''; + String? startDate; + String? endDate; + int page = 1; + int lastPage = 1; + bool isScrolled = false; + bool shouldColapse = false; + final List selectedCats = []; + List categories = []; + final List radars = []; + + bool get filtering => + selectedCats.length > 1 || startDate != null || endDate != null; + + bool get searching => search.isNotEmpty; + + bool get isColapsed => + (selectedCats.length == 1 && !filtering && isScrolled) || isScrolled; + + bool get isCategorySelected => selectedCats.length == 1 && !filtering; + + void resetFilters(bool isInit) { + startDate = null; + endDate = null; + selectedCats.clear(); + search = ''; + lastSearch = ''; + isScrolled = false; + if (!isInit) { + getRadars(page: 1); + } + } + + Future getRadars({ + required int page, + }) async { + this.page = page; + if (this.page == page) { + radars.clear(); + } + lastSearch = search; + if (page == 1) { + appState = AppState.busy; + } + final RequestService service = RequestService( + RequestHelper.radarOverviews( + args: RadarRequestArgs( + page: page, + startDate: startDate?.split(' ').first, + endDate: endDate?.split(' ').first, + search: search == '' ? null : search, + categories: selectedCats.map((e) => e.id).toList(), + ), + ), + ); + await service.httpGet(); + if (service.isSuccess) { + lastPage = service.result['lastPage']; + final radarsList = service.result['radars']; + for (var i = 0; i < radarsList.length; i++) { + radars.add(OverviewData.fromJson(radarsList[i])); + } + if (searching || filtering || isColapsed || isCategorySelected) { + shouldColapse = true; + } + appState = AppState.idle; + return; + } + + appState = AppState.failed; + } + + Future changeMark(int id, bool value) async { + radars.firstWhere((element) => element.id == id).marked = value; + notifyListeners(); + UserProvider.changeRadarMark(id, value); + } + + void onCommentsChanged(int id, int count) { + radars.firstWhere((radar) => radar.id == id).comments = count; + notifyListeners(); + } + + void init() { + resetFilters(true); + Future.delayed(Duration.zero, () { + getRadars(page: 1); + }); + categories = [ + RadarCategory( + id: 1, + title: 'اقتصادی', + asset: Assets.economicCategoryIcon, + ), + RadarCategory( + id: 2, + title: 'سیاسی', + asset: Assets.politicalCategoryIcon, + ), + RadarCategory( + id: 3, + title: 'فناوری', + asset: Assets.techCategoryIcon, + ), + RadarCategory( + id: 4, + title: 'کسب و کار', + asset: Assets.businessCategoryIcon, + ), + RadarCategory( + id: 5, + title: 'زیست محیطی', + asset: Assets.enviromentalCategoryIcon, + ), + RadarCategory( + id: 6, + title: 'اجتماعی', + asset: Assets.socialCategoryIcon, + ), + ]; + } +} diff --git a/lib/views/home/radar/widgets/categories_gird.dart b/lib/views/home/radar/widgets/categories_gird.dart new file mode 100644 index 0000000..abcc0e2 --- /dev/null +++ b/lib/views/home/radar/widgets/categories_gird.dart @@ -0,0 +1,76 @@ +import 'package:didvan/config/design_config.dart'; +import 'package:didvan/views/home/radar/radar_state.dart'; +import 'package:didvan/views/home/radar/widgets/category_item.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +class CategoriesRow1 extends StatelessWidget { + const CategoriesRow1({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + final state = context.read(); + final isColapsed = state.isColapsed || state.searching || state.filtering; + final MediaQueryData d = MediaQuery.of(context); + return AnimatedPositioned( + curve: Curves.easeIn, + duration: DesignConfig.mediumAnimationDuration, + top: isColapsed ? -60 : 300 + d.padding.top, + left: isColapsed ? -80 : 0, + right: isColapsed ? 124 : 0, + child: Row( + children: context + .read() + .categories + .sublist(0, 3) + .map( + (category) => Expanded( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 6), + child: CategoryItem( + category: category, + isColapsed: isColapsed, + ), + ), + ), + ) + .toList(), + ), + ); + } +} + +class CategoriesRow2 extends StatelessWidget { + const CategoriesRow2({ + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + final state = context.read(); + final isColapsed = state.isColapsed || state.searching || state.filtering; + final MediaQueryData d = MediaQuery.of(context); + return AnimatedPositioned( + curve: Curves.easeIn, + duration: DesignConfig.mediumAnimationDuration, + top: isColapsed ? 12 : 176 + d.padding.top, + left: isColapsed ? -d.size.width : 0, + right: isColapsed ? d.size.width : 0, + child: Row( + children: context + .read() + .categories + .sublist(3, 6) + .map( + (category) => Expanded( + child: CategoryItem( + category: category, + isColapsed: isColapsed, + ), + ), + ) + .toList(), + ), + ); + } +} diff --git a/lib/views/home/radar/widgets/categories_list.dart b/lib/views/home/radar/widgets/categories_list.dart new file mode 100644 index 0000000..bca0187 --- /dev/null +++ b/lib/views/home/radar/widgets/categories_list.dart @@ -0,0 +1,130 @@ +import 'package:didvan/config/design_config.dart'; +import 'package:didvan/config/theme_data.dart'; +import 'package:didvan/models/view/radar_category.dart'; +import 'package:didvan/views/home/radar/radar_state.dart'; +import 'package:didvan/views/widgets/animated_visibility.dart'; +import 'package:didvan/views/widgets/didvan/text.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +class CategoriesList extends StatefulWidget { + const CategoriesList({Key? key}) : super(key: key); + + @override + State createState() => _CategoriesListState(); +} + +class _CategoriesListState extends State { + final _scrollController = ScrollController(); + + int _lastSelectedCategoryId = 0; + + @override + void didUpdateWidget(covariant CategoriesList oldWidget) { + final RadarState state = context.read(); + if (state.selectedCats.isNotEmpty && + _lastSelectedCategoryId != state.selectedCats.first.id) { + _lastSelectedCategoryId = state.selectedCats.first.id; + _scrollController.animateTo( + state.selectedCats.first.id * 100, + duration: DesignConfig.lowAnimationDuration, + curve: Curves.easeIn, + ); + } + super.didUpdateWidget(oldWidget); + } + + @override + Widget build(BuildContext context) { + final MediaQueryData d = MediaQuery.of(context); + final state = context.read(); + final isColapsed = state.isColapsed || state.searching || state.filtering; + return Positioned( + top: 0, + left: 0, + right: 0, + child: AnimatedCrossFade( + crossFadeState: + isColapsed ? CrossFadeState.showSecond : CrossFadeState.showFirst, + duration: DesignConfig.mediumAnimationDuration, + reverseDuration: DesignConfig.lowAnimationDuration, + sizeCurve: Curves.easeIn, + firstChild: const SizedBox(), + secondChild: Container( + height: 60 + d.padding.top, + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + boxShadow: DesignConfig.defaultShadow, + ), + child: AnimatedVisibility( + isVisible: isColapsed, + duration: DesignConfig.mediumAnimationDuration, + child: SingleChildScrollView( + controller: _scrollController, + // physics: const BouncingScrollPhysics(), + scrollDirection: Axis.horizontal, + padding: EdgeInsets.only( + top: d.padding.top + 12, + bottom: 12, + right: 12, + ), + child: Row( + children: [ + _itemBuilder( + RadarCategory(title: 'همه', asset: '', id: 0), + context, + ), + for (var i = 0; i < state.categories.length; i++) + _itemBuilder(state.categories[i], context), + ], + ), + ), + ), + ), + ), + ); + } + + Widget _itemBuilder(RadarCategory category, BuildContext context) { + final state = context.read(); + return GestureDetector( + onTap: () async { + if (state.selectedCats.isNotEmpty && + state.selectedCats.first.id == category.id) return; + state.selectedCats.clear(); + if (category.id != 0) state.selectedCats.add(category); + await _scrollController.animateTo( + category.id * 100, + duration: DesignConfig.lowAnimationDuration, + curve: Curves.easeIn, + ); + state.getRadars(page: 1); + }, + child: Container( + margin: const EdgeInsets.only(left: 12), + width: 100, + padding: const EdgeInsets.all(4), + alignment: Alignment.center, + child: FittedBox( + fit: BoxFit.scaleDown, + child: DidvanText( + category.title, + fontWeight: FontWeight.w600, + color: Theme.of(context).colorScheme.focusedBorder, + ), + ), + decoration: BoxDecoration( + color: state.selectedCats.length == 1 && + state.selectedCats.contains(category) || + category.id == 0 && state.selectedCats.isEmpty + ? Theme.of(context).colorScheme.focused + : null, + border: Border.all( + color: Theme.of(context).colorScheme.focusedBorder, + ), + borderRadius: DesignConfig.lowBorderRadius, + ), + ), + ); + } +} diff --git a/lib/views/home/radar/widgets/category_item.dart b/lib/views/home/radar/widgets/category_item.dart new file mode 100644 index 0000000..081b152 --- /dev/null +++ b/lib/views/home/radar/widgets/category_item.dart @@ -0,0 +1,97 @@ +import 'package:didvan/config/design_config.dart'; +import 'package:didvan/config/theme_data.dart'; +import 'package:didvan/models/view/radar_category.dart'; +import 'package:didvan/views/home/radar/radar_state.dart'; +import 'package:didvan/views/widgets/animated_visibility.dart'; +import 'package:didvan/views/widgets/didvan/text.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:provider/provider.dart'; + +class CategoryItem extends StatelessWidget { + final RadarCategory category; + final bool isColapsed; + + const CategoryItem({ + Key? key, + required this.isColapsed, + required this.category, + }) : super(key: key); + + double _width(context) { + final Size ds = MediaQuery.of(context).size; + if (kIsWeb) { + if (!_useWebMobileLayout(context)) { + return ds.height / 16 * 9 / 3; + } + } + return ds.width / 3; + } + + bool _useWebMobileLayout(context) { + final shortestSide = MediaQuery.of(context).size.shortestSide; + final bool useMobileLayout = shortestSide < 600; + return kIsWeb && useMobileLayout; + } + + @override + Widget build(BuildContext context) { + final Size ds = MediaQuery.of(context).size; + return Center( + child: GestureDetector( + onTap: () { + final state = context.read(); + state.selectedCats.clear(); + if (category.id != 0) { + state.selectedCats.add(category); + } + state.getRadars(page: 1); + }, + child: AnimatedContainer( + duration: DesignConfig.mediumAnimationDuration, + padding: isColapsed ? const EdgeInsets.all(4) : EdgeInsets.zero, + width: isColapsed ? 100 : _width(context), + alignment: Alignment.center, + decoration: BoxDecoration( + borderRadius: DesignConfig.lowBorderRadius, + border: isColapsed + ? Border.all(color: Theme.of(context).colorScheme.focusedBorder) + : null, + ), + child: Column( + children: [ + AnimatedVisibility( + duration: DesignConfig.mediumAnimationDuration, + isVisible: !isColapsed, + child: Container( + width: !_useWebMobileLayout(context) + ? _width(context) / 1.5 + : ds.width / 5, + height: !_useWebMobileLayout(context) + ? _width(context) / 1.5 + : ds.width / 5, + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + boxShadow: DesignConfig.defaultShadow, + borderRadius: DesignConfig.mediumBorderRadius, + ), + padding: const EdgeInsets.all(8), + child: SvgPicture.asset(category.asset), + ), + ), + const SizedBox( + height: 8, + ), + DidvanText( + category.title, + style: Theme.of(context).textTheme.subtitle2, + color: Theme.of(context).colorScheme.title, + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/views/home/settings/about_us/about_us.dart b/lib/views/home/settings/about_us/about_us.dart new file mode 100644 index 0000000..6cc17f9 --- /dev/null +++ b/lib/views/home/settings/about_us/about_us.dart @@ -0,0 +1,25 @@ +import 'package:didvan/models/view/app_bar_data.dart'; +import 'package:didvan/views/widgets/didvan/scaffold.dart'; +import 'package:didvan/views/widgets/didvan/text.dart'; +import 'package:flutter/material.dart'; + +class AboutUs extends StatelessWidget { + const AboutUs({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return DidvanScaffold( + appBarData: AppBarData( + hasBack: true, + title: 'معرفی دیدوان', + ), + children: const [ + DidvanText( + '''برای سفر مطمئن در جغرافیای مکانی به ابزارهایی مانند نقشه، قطب نما، سیستم ناوبری، علائم کنار جاده و یا حتی علائم طبیعی نیاز داریم. اما برای سفر در جغرافیای زمانی چه؟ +همه ما آدمیان و همه‌ی سازمان‌ها و شرکت‌هایمان، لاجرم مسافران جغرافیای زمانی هستند. ما همواره در مسیری رو به مقصدی در آینده در حرکت هستیم. اما فراموش نکنیم که سفر زمانی یک تفاوت بنیادین با طی مسیر در جغرافیای مکانی دارد. جغرافیای زمانی هیچ‌گاه نمی‌ایستد و سفر به آینده یک سفر شگفت‌انگیز به جایی است که قسمت‌هایی از آن را قبلاً در گذشته دیده‌ایم، قسمت‌هایی از آن را طی مسیر به تدریج می‌بینیم و قسمت‌هایی از آن کاملاً بدیع هستند. +برای سفر مطمئن در جغرافیای زمانی ما به سیستم‌های پیش‌نگر نیاز داریم و یکی از ارکان این سیستم‌ها، سامانه‌های رصد راهبردی هستند. دیدوان با شعار "چشم همیشه باز مدیران" سامانه رصد راهبردی شرکت فولاد مبارکه است تا سفری مطمئن به آینده را برای این شرکت رقم بزند.\nدر پارسی کهن دیدوان به این شکل معنی شده است: شخصی را گویند که بر جای بلند مانند سر کوه و بالای کشتی نشیند و هرچه از دور بیند خبر دهد. دیدوان چشمی است که آینده را می‌بیند. ما در دیدوان تلاش داریم که با رویکردی آینده‌پژوهانه مسیر پیش‌روی صنایع را تحلیل کنیم و با تحویل آن به مدیران صنایع، راهنمایی باشیم برای اخذ تصمیمات درست.''', + ), + ], + ); + } +} diff --git a/lib/views/home/settings/bookmarks/bookmark_state.dart b/lib/views/home/settings/bookmarks/bookmark_state.dart new file mode 100644 index 0000000..1807179 --- /dev/null +++ b/lib/views/home/settings/bookmarks/bookmark_state.dart @@ -0,0 +1,50 @@ +import 'package:didvan/models/enums.dart'; +import 'package:didvan/models/overview_data.dart'; +import 'package:didvan/providers/core_provider.dart'; +import 'package:didvan/providers/user_provider.dart'; +import 'package:didvan/services/network/request.dart'; +import 'package:didvan/services/network/request_helper.dart'; + +class BookmarksState extends CoreProvier { + final List bookmarks = []; + String search = ''; + String lastSearch = ''; + + bool get searching => search != ''; + + Future getBookmarks() async { + if (search != '') { + lastSearch = search; + } + appState = AppState.busy; + final service = RequestService(RequestHelper.bookmarks()); + await service.httpGet(); + + if (service.isSuccess) { + final marks = service.result['contents']; + bookmarks.clear(); + for (var i = 0; i < marks.length; i++) { + bookmarks.add(OverviewData.fromJson(marks[i])); + } + appState = AppState.idle; + return; + } + appState = AppState.failed; + } + + void onMarkChanged(int id, bool value) { + if (value) return; + final type = bookmarks.firstWhere((element) => element.id == id).type; + switch (type) { + case 'radar': + UserProvider.changeRadarMark(id, value); + break; + case 'news': + UserProvider.changeNewsMark(id, value); + break; + default: + } + bookmarks.removeWhere((element) => element.id == id); + notifyListeners(); + } +} diff --git a/lib/views/home/settings/bookmarks/bookmarks.dart b/lib/views/home/settings/bookmarks/bookmarks.dart new file mode 100644 index 0000000..1cb736a --- /dev/null +++ b/lib/views/home/settings/bookmarks/bookmarks.dart @@ -0,0 +1,141 @@ +import 'dart:async'; + +import 'package:didvan/config/design_config.dart'; +import 'package:didvan/constants/app_icons.dart'; +import 'package:didvan/models/view/app_bar_data.dart'; +import 'package:didvan/routes/routes.dart'; +import 'package:didvan/views/home/settings/bookmarks/bookmark_state.dart'; +import 'package:didvan/views/home/widgets/menu_item.dart'; +import 'package:didvan/views/home/widgets/multitype_overview.dart'; +import 'package:didvan/views/home/widgets/search_field.dart'; +import 'package:didvan/views/widgets/animated_visibility.dart'; +import 'package:didvan/views/widgets/didvan/card.dart'; +import 'package:didvan/views/widgets/didvan/divider.dart'; +import 'package:didvan/views/widgets/didvan/scaffold.dart'; +import 'package:didvan/views/widgets/item_title.dart'; +import 'package:didvan/views/widgets/state_handlers/empty_list.dart'; +import 'package:didvan/views/widgets/state_handlers/empty_result.dart'; +import 'package:didvan/views/widgets/state_handlers/sliver_state_handler.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +class Bookmarks extends StatefulWidget { + const Bookmarks({Key? key}) : super(key: key); + + @override + State createState() => _BookmarksState(); +} + +class _BookmarksState extends State { + final _focuseNode = FocusNode(); + Timer? _timer; + + @override + void initState() { + Future.delayed(Duration.zero, () { + context.read().getBookmarks(); + }); + super.initState(); + } + + @override + Widget build(BuildContext context) { + final state = context.watch(); + return DidvanScaffold( + appBarData: AppBarData( + title: 'نشان شده‌ها', + hasBack: true, + ), + children: [ + SearchField( + title: 'نشان شده‌ها', + onChanged: _onChanged, + focusNode: _focuseNode, + ), + const SizedBox(height: 16), + AnimatedVisibility( + duration: DesignConfig.lowAnimationDuration, + isVisible: !state.searching, + child: DidvanCard( + child: Column( + children: [ + MenuItem( + onTap: () => _onCategorySelected('radar'), + title: 'تحلیل‌های رادار', + icon: DidvanIcons.radar_regular, + iconSize: 24, + ), + const DidvanDivider(), + MenuItem( + onTap: () => _onCategorySelected('news'), + title: 'اخبار', + icon: DidvanIcons.news_regular, + iconSize: 24, + ), + const DidvanDivider(), + MenuItem( + onTap: () => _onCategorySelected('videos'), + title: 'ویدئو‌ها', + icon: DidvanIcons.video_regular, + iconSize: 24, + ), + const DidvanDivider(), + MenuItem( + onTap: () => _onCategorySelected('podcasts'), + title: 'پادکست‌ها', + icon: DidvanIcons.podcast_regular, + iconSize: 24, + ), + ], + ), + ), + ), + Align( + alignment: Alignment.centerRight, + child: AnimatedVisibility( + duration: DesignConfig.lowAnimationDuration, + isVisible: !state.searching, + child: const ItemTitle(title: 'آخرین نشان شده‌ها'), + ), + ), + ], + slivers: [ + SliverStateHandler( + state: state, + centerEmptyState: state.searching, + builder: (context, state, index) => MultitypeOverview( + item: state.bookmarks[index], + onMarkChanged: state.onMarkChanged, + hasUnmarkConfirmation: true, + ), + placeholder: MultitypeOverview.placeholder, + itemPadding: const EdgeInsets.only(bottom: 8), + emptyState: state.searching + ? EmptyResult(onNewSearch: _focuseNode.requestFocus) + : const EmptyList(), + enableEmptyState: state.bookmarks.isEmpty, + childCount: state.bookmarks.length, + onRetry: state.getBookmarks, + ), + ], + ); + } + + void _onCategorySelected(String type) { + if (type != 'radar' && type != 'news') return; + FocusScope.of(context).unfocus(); + Navigator.of(context).pushNamed(Routes.filteredBookmarks, arguments: type); + } + + void _onChanged(String value) { + final state = context.read(); + if (value.length < 4 && value.isNotEmpty || state.lastSearch == value) { + return; + } + _timer?.cancel(); + _timer = Timer(const Duration(seconds: 1), () { + state.search = value; + state.getBookmarks(); + }); + } +} diff --git a/lib/views/home/settings/bookmarks/filtered_bookmark/filtered_bookmark.dart b/lib/views/home/settings/bookmarks/filtered_bookmark/filtered_bookmark.dart new file mode 100644 index 0000000..f9a5454 --- /dev/null +++ b/lib/views/home/settings/bookmarks/filtered_bookmark/filtered_bookmark.dart @@ -0,0 +1,84 @@ +import 'package:didvan/models/view/app_bar_data.dart'; +import 'package:didvan/views/home/settings/bookmarks/filtered_bookmark/filtered_bookmarks_state.dart'; +import 'package:didvan/views/home/widgets/news_overview.dart'; +import 'package:didvan/views/home/widgets/radar_overview.dart'; +import 'package:didvan/views/widgets/didvan/scaffold.dart'; +import 'package:didvan/views/widgets/state_handlers/empty_list.dart'; +import 'package:didvan/views/widgets/state_handlers/sliver_state_handler.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +class FilteredBookmarks extends StatefulWidget { + const FilteredBookmarks({Key? key}) : super(key: key); + + @override + _FilteredBookmarksState createState() => _FilteredBookmarksState(); +} + +class _FilteredBookmarksState extends State { + @override + void initState() { + Future.delayed( + Duration.zero, + context.read().getBookmarks, + ); + super.initState(); + } + + String get _appBarTitle { + switch (context.read().type) { + case 'radar': + return 'تحلیل‌های رادار'; + case 'news': + return 'اخبار'; + case 'video': + return 'ویدئو‌ها'; + case 'podcast': + return 'پادکست‌ها'; + default: + return 'پادکست‌ها'; + } + } + + @override + Widget build(BuildContext context) { + return DidvanScaffold( + appBarData: AppBarData(title: _appBarTitle), + slivers: [ + Consumer( + builder: (context, state, child) => + SliverStateHandler( + state: state, + enableEmptyState: state.bookmarks.isEmpty, + itemPadding: const EdgeInsets.only(bottom: 8), + placeholder: RadarOverview.placeholder, + emptyState: const EmptyList(), + builder: (context, state, index) { + if (state.type == 'radar') { + return RadarOverview( + radar: state.bookmarks[index], + onMarkChanged: _onBookmarkChanged, + onCommentsChanged: state.onCommentsChanged, + hasUnmarkConfirmation: true, + ); + } + return NewsOverview( + news: state.bookmarks[index], + onMarkChanged: _onBookmarkChanged, + hasUnmarkConfirmation: true, + ); + }, + childCount: state.bookmarks.length, + onRetry: () => state.getBookmarks(), + ), + ), + ], + ); + } + + Future _onBookmarkChanged(int id, bool value) async { + if (value) return; + final state = context.read(); + state.onMarkChanged(id, false); + } +} diff --git a/lib/views/home/settings/bookmarks/filtered_bookmark/filtered_bookmarks_state.dart b/lib/views/home/settings/bookmarks/filtered_bookmark/filtered_bookmarks_state.dart new file mode 100644 index 0000000..697ee3a --- /dev/null +++ b/lib/views/home/settings/bookmarks/filtered_bookmark/filtered_bookmarks_state.dart @@ -0,0 +1,58 @@ +import 'package:didvan/models/enums.dart'; +import 'package:didvan/models/overview_data.dart'; +import 'package:didvan/providers/core_provider.dart'; +import 'package:didvan/providers/user_provider.dart'; +import 'package:didvan/services/network/request.dart'; +import 'package:didvan/services/network/request_helper.dart'; + +class FilteredBookmarksState extends CoreProvier { + String search = ''; + String lastSearch = ''; + final List bookmarks = []; + final String type; + + FilteredBookmarksState(this.type); + + bool get searching => search != ''; + + Future getBookmarks() async { + if (search != '') { + lastSearch = search; + } + appState = AppState.busy; + final service = RequestService( + RequestHelper.bookmarks(type: type == 'news' ? type : type + 's'), + ); + await service.httpGet(); + + if (service.isSuccess) { + final marks = service.result[type != 'news' ? type + 's' : type]; + bookmarks.clear(); + for (var i = 0; i < marks.length; i++) { + bookmarks.add(OverviewData.fromJson(marks[i])); + } + appState = AppState.idle; + return; + } + appState = AppState.failed; + } + + void onMarkChanged(int id, bool value) { + switch (type) { + case 'radar': + UserProvider.changeRadarMark(id, value); + break; + case 'news': + UserProvider.changeNewsMark(id, value); + break; + default: + } + bookmarks.removeWhere((element) => element.id == id); + notifyListeners(); + } + + void onCommentsChanged(int id, int value) { + bookmarks.firstWhere((radar) => radar.id == id).comments = value; + notifyListeners(); + } +} diff --git a/lib/views/home/settings/direct_list/direct_list.dart b/lib/views/home/settings/direct_list/direct_list.dart new file mode 100644 index 0000000..7c8d264 --- /dev/null +++ b/lib/views/home/settings/direct_list/direct_list.dart @@ -0,0 +1,107 @@ +import 'package:didvan/constants/assets.dart'; +import 'package:didvan/models/view/app_bar_data.dart'; +import 'package:didvan/views/home/settings/direct_list/direct_list_state.dart'; +import 'package:didvan/views/home/settings/direct_list/widgets/direct_item.dart'; +import 'package:didvan/views/widgets/didvan/badge.dart'; +import 'package:didvan/views/widgets/didvan/divider.dart'; +import 'package:didvan/views/widgets/didvan/scaffold.dart'; +import 'package:didvan/views/widgets/shimmer_placeholder.dart'; +import 'package:didvan/views/widgets/state_handlers/empty_state.dart'; +import 'package:didvan/views/widgets/state_handlers/sliver_state_handler.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +class DirectList extends StatefulWidget { + const DirectList({Key? key}) : super(key: key); + + @override + State createState() => _DirectListState(); +} + +class _DirectListState extends State { + @override + void initState() { + Future.delayed(Duration.zero, () { + context.read().getDirectsList(); + }); + super.initState(); + } + + @override + Widget build(BuildContext context) { + return Consumer( + builder: (context, state, child) => DidvanScaffold( + appBarData: AppBarData( + hasBack: true, + title: 'پیام‌ها', + trailing: state.unreadCount == 0 + ? null + : DidvanBadge( + text: state.unreadCount.toString(), + ), + ), + slivers: [ + SliverStateHandler( + onRetry: state.getDirectsList, + itemPadding: const EdgeInsets.symmetric(horizontal: 16), + state: state, + placeholder: const _ChatRoomPlaceholder(), + builder: (context, state, index) => ChatRoomItem( + chatRoom: state.chatRooms[index], + ), + childCount: state.chatRooms.length, + enableEmptyState: state.chatRooms.isEmpty, + emptyState: EmptyState( + asset: Assets.emptyBookmark, + title: 'پیامی وجود ندارد', + ), + ), + ], + ), + ); + } +} + +class _ChatRoomPlaceholder extends StatelessWidget { + const _ChatRoomPlaceholder({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Column( + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const ShimmerPlaceholder( + height: 24, + width: 24, + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: const [ + ShimmerPlaceholder( + height: 24, + width: 100, + ), + SizedBox(height: 12), + ShimmerPlaceholder( + height: 16, + width: 300, + ), + SizedBox(height: 12), + ShimmerPlaceholder( + height: 12, + width: 80, + ), + ], + ), + ), + ], + ), + const DidvanDivider(), + ], + ); + } +} diff --git a/lib/views/home/settings/direct_list/direct_list_state.dart b/lib/views/home/settings/direct_list/direct_list_state.dart new file mode 100644 index 0000000..728305a --- /dev/null +++ b/lib/views/home/settings/direct_list/direct_list_state.dart @@ -0,0 +1,32 @@ +import 'package:didvan/models/chat_room/chat_room.dart'; +import 'package:didvan/models/enums.dart'; +import 'package:didvan/providers/core_provider.dart'; +import 'package:didvan/services/network/request.dart'; +import 'package:didvan/services/network/request_helper.dart'; + +class DirectListState extends CoreProvier { + final List chatRooms = []; + int _unreadCount = 0; + int get unreadCount => _unreadCount; + + set unreadCount(int value) { + _unreadCount = value; + notifyListeners(); + } + + Future getDirectsList() async { + appState = AppState.busy; + final RequestService service = RequestService(RequestHelper.directs); + await service.httpGet(); + if (service.isSuccess) { + _unreadCount = service.result['unread'] ?? 0; + final List _chatRooms = service.result['chatRooms'] ?? []; + for (var i = 0; i < _chatRooms.length; i++) { + chatRooms.add(ChatRoom.fromJson(_chatRooms[i])); + } + appState = AppState.idle; + return; + } + appState = AppState.failed; + } +} diff --git a/lib/views/home/settings/direct_list/widgets/direct_item.dart b/lib/views/home/settings/direct_list/widgets/direct_item.dart new file mode 100644 index 0000000..6aa9a45 --- /dev/null +++ b/lib/views/home/settings/direct_list/widgets/direct_item.dart @@ -0,0 +1,93 @@ +import 'package:didvan/config/theme_data.dart'; +import 'package:didvan/constants/app_icons.dart'; +import 'package:didvan/models/chat_room/chat_room.dart'; +import 'package:didvan/routes/routes.dart'; +import 'package:didvan/utils/date_time.dart'; +import 'package:didvan/views/widgets/didvan/badge.dart'; +import 'package:didvan/views/widgets/didvan/divider.dart'; +import 'package:didvan/views/widgets/didvan/text.dart'; +import 'package:flutter/material.dart'; + +class ChatRoomItem extends StatelessWidget { + final ChatRoom chatRoom; + const ChatRoomItem({Key? key, required this.chatRoom}) : super(key: key); + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: () { + Navigator.of(context).pushNamed( + Routes.direct, + arguments: {'type': chatRoom.type}, + ); + chatRoom.unread = 0; + }, + child: Container( + color: Colors.transparent, + child: Column( + children: [ + Row( + children: [ + const Icon( + DidvanIcons.avatar_light, + size: 32, + ), + const SizedBox(width: 12), + Expanded( + child: DidvanText( + chatRoom.type, + style: Theme.of(context).textTheme.bodyText1, + ), + ), + if (chatRoom.unread != 0) + DidvanBadge(text: chatRoom.unread.toString()), + ], + ), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(width: 40), + Icon( + chatRoom.lastMessage.readed + ? DidvanIcons.check_double_light + : DidvanIcons.check_light, + size: 16, + ), + const SizedBox(width: 4), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + if (chatRoom.lastMessage.text == null) + const Icon( + DidvanIcons.mic_light, + size: 18, + ), + Expanded( + child: DidvanText( + chatRoom.lastMessage.text ?? 'پیام صوتی', + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + DidvanText( + DateTimeUtils.momentGenerator(chatRoom.updatedAt), + style: Theme.of(context).textTheme.caption, + color: Theme.of(context).colorScheme.caption, + ) + ], + ), + ), + ], + ), + const DidvanDivider(), + ], + ), + ), + ); + } +} diff --git a/lib/views/home/settings/general_settings/settings.dart b/lib/views/home/settings/general_settings/settings.dart new file mode 100644 index 0000000..6dcfd2c --- /dev/null +++ b/lib/views/home/settings/general_settings/settings.dart @@ -0,0 +1,293 @@ +import 'package:day_night_time_picker/lib/constants.dart'; +import 'package:day_night_time_picker/lib/daynight_timepicker.dart'; +import 'package:didvan/config/design_config.dart'; +import 'package:didvan/config/theme_data.dart'; +import 'package:didvan/constants/app_icons.dart'; +import 'package:didvan/constants/assets.dart'; +import 'package:didvan/models/view/action_sheet_data.dart'; +import 'package:didvan/models/view/app_bar_data.dart'; +import 'package:didvan/providers/theme_provider.dart'; +import 'package:didvan/utils/action_sheet.dart'; +import 'package:didvan/views/home/settings/general_settings/settings_state.dart'; +import 'package:didvan/views/home/widgets/menu_item.dart'; +import 'package:didvan/views/widgets/didvan/card.dart'; +import 'package:didvan/views/widgets/didvan/divider.dart'; +import 'package:didvan/views/widgets/didvan/radial_button.dart'; +import 'package:didvan/views/widgets/didvan/scaffold.dart'; +import 'package:didvan/views/widgets/didvan/text.dart'; +import 'package:didvan/views/widgets/item_title.dart'; +import 'package:didvan/views/widgets/state_handlers/state_handler.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:provider/provider.dart'; + +class GeneralSettings extends StatefulWidget { + const GeneralSettings({Key? key}) : super(key: key); + + @override + State createState() => _GeneralSettingsState(); +} + +class _GeneralSettingsState extends State { + String get _fontScaleSuffix { + final fontScale = DesignConfig.fontScale; + if (fontScale == 1) return 'متوسط'; + if (fontScale == 1.15) return 'بزرگ'; + return 'کوچک'; + } + + @override + Widget build(BuildContext context) { + return Consumer( + builder: (context, state, child) => StateHandler( + onRetry: () {}, + state: context.read(), + builder: (context, state) => DidvanScaffold( + appBarData: AppBarData(hasBack: true, title: 'تنظیمات'), + children: [ + DidvanCard( + child: MenuItem( + title: 'زمان دریافت اعلان', + onTap: () => _pickTimeRange(context), + icon: DidvanIcons.notification_regular, + suffix: state.notificationTimeRange[0], + ), + ), + const ItemTitle( + title: 'انتخاب قلم', + icon: DidvanIcons.font_solid, + ), + DidvanCard( + child: Column( + children: [ + MenuItem( + suffix: DesignConfig.fontFamily == 'Dana-FA' + ? 'دانا' + : 'ایران سنس', + title: 'فونت برنامه', + onTap: _showFontFamilyBottomSheet, + ), + const DidvanDivider(), + MenuItem( + suffix: _fontScaleSuffix, + title: 'اندازه متن', + onTap: _showFontScaleBottomSheet, + ), + ], + ), + ), + const ItemTitle( + title: 'ظاهر برنامه', + icon: DidvanIcons.theme_solid, + ), + DidvanCard( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + _themeItem(context, state, 'light'), + _themeItem(context, state, 'dark'), + ], + ), + ), + ], + ), + ), + ); + } + + Future _showFontFamilyBottomSheet() async { + final themeProvider = context.read(); + final state = context.read(); + final family = state.fontFamily; + await ActionSheetUtils.showBottomSheet( + data: ActionSheetData( + content: StatefulBuilder( + builder: (context, setState) => Column( + children: [ + DidvanRadialButton( + title: 'دانا', + fontFamily: 'Dana-FA', + onSelected: () { + state.fontFamily = 'Dana-FA'; + setState(() {}); + }, + value: state.fontFamily == 'Dana-FA', + ), + const SizedBox(height: 24), + DidvanRadialButton( + title: 'ایران سنس', + fontFamily: 'Iransans-FA', + onSelected: () { + state.fontFamily = 'Iransans-FA'; + setState(() {}); + }, + value: state.fontFamily == 'Iransans-FA', + ), + ], + ), + ), + title: 'انتخاب فونت برنامه', + titleIcon: DidvanIcons.font_regular, + onDismissed: () => state.fontFamily = family, + onConfirmed: () => themeProvider.fontFamily = state.fontFamily, + ), + ); + } + + Future _showFontScaleBottomSheet() async { + final themeProvider = context.read(); + final state = context.read(); + final scale = state.fontSizeScale; + await ActionSheetUtils.showBottomSheet( + data: ActionSheetData( + content: StatefulBuilder( + builder: (context, setState) => Column( + children: [ + DidvanRadialButton( + title: 'بزرگ', + fontSize: 15 * 1.15, + onSelected: () { + state.fontSizeScale = 1.15; + setState(() {}); + }, + value: state.fontSizeScale == 1.15, + ), + const SizedBox(height: 24), + DidvanRadialButton( + title: 'متوسط', + onSelected: () { + state.fontSizeScale = 1; + setState(() {}); + }, + value: state.fontSizeScale == 1, + ), + const SizedBox(height: 24), + DidvanRadialButton( + title: 'کوچک', + fontSize: 15 * 0.85, + onSelected: () { + state.fontSizeScale = 0.85; + setState(() {}); + }, + value: state.fontSizeScale == 0.85, + ), + ], + ), + ), + title: 'انتخاب اندازه متن', + titleIcon: DidvanIcons.font_regular, + onDismissed: () => state.fontSizeScale = scale, + onConfirmed: () => themeProvider.fontScale = state.fontSizeScale, + ), + ); + } + + Future _pickTimeRange(BuildContext context) async { + ActionSheetUtils.showBottomSheet( + data: ActionSheetData( + content: Row( + children: [ + const SizedBox(width: 8), + Expanded(child: _timeFieldBuilder(context, 0)), + const SizedBox(width: 8), + const DidvanText('-'), + const SizedBox(width: 8), + Expanded(child: _timeFieldBuilder(context, 1)), + const SizedBox(width: 8), + ], + ), + title: 'زمان دریافت اعلان', + titleIcon: DidvanIcons.notification_regular, + ), + ); + } + + Widget _timeFieldBuilder(BuildContext context, int index) { + final GeneralSettingsState state = context.read(); + return ChangeNotifierProvider.value( + value: state, + child: StatefulBuilder( + builder: (context, setState) => GestureDetector( + onTap: () async { + await _openTimePicker(context, index); + setState(() {}); + }, + child: Container( + alignment: Alignment.center, + padding: const EdgeInsets.all(24), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.splash, + borderRadius: DesignConfig.mediumBorderRadius, + border: Border.all( + color: Theme.of(context).colorScheme.border, + ), + ), + child: DidvanText(state.notificationTimeRange[index]), + ), + ), + ), + ); + } + + Future _openTimePicker(BuildContext context, int index) async { + final GeneralSettingsState state = context.read(); + await Navigator.of(context).push( + showPicker( + okText: 'تایید', + cancelText: 'بازگشت', + accentColor: Theme.of(context).colorScheme.primary, + okStyle: Theme.of(context).textTheme.bodyText2!, + cancelStyle: Theme.of(context).textTheme.bodyText2!, + unselectedColor: Theme.of(context).colorScheme.text, + blurredBackground: true, + hourLabel: 'ساعت', + minuteLabel: 'دقیقه', + is24HrFormat: true, + iosStylePicker: true, + minuteInterval: MinuteInterval.FIFTEEN, + context: context, + value: const TimeOfDay(hour: 0, minute: 0), + themeData: Theme.of(context), + onChange: (time) { + state.notificationTimeRange = state.notificationTimeRange + ..replaceRange( + index, + index + 1, + ['${time.hour}:${time.minute}'], + ); + }, + ), + ); + } + + Widget _themeItem( + BuildContext context, GeneralSettingsState state, String brightness) { + final bool isDarkTheme = brightness == 'dark'; + return GestureDetector( + onTap: () { + state.brightness = brightness; + if (isDarkTheme && DesignConfig.isDark) { + return; + } else if (!isDarkTheme && !DesignConfig.isDark) { + return; + } else { + if (isDarkTheme) { + context.read().themeMode = ThemeMode.dark; + } else { + context.read().themeMode = ThemeMode.light; + } + DesignConfig.updateSystemUiOverlayStyle(); + } + }, + child: Column( + children: [ + SvgPicture.asset(isDarkTheme ? Assets.darkTheme : Assets.lightTheme), + const SizedBox(height: 8), + DidvanText( + isDarkTheme ? 'تیره' : 'روشن', + ), + ], + ), + ); + } +} diff --git a/lib/views/home/settings/general_settings/settings_state.dart b/lib/views/home/settings/general_settings/settings_state.dart new file mode 100644 index 0000000..19d5d5b --- /dev/null +++ b/lib/views/home/settings/general_settings/settings_state.dart @@ -0,0 +1,74 @@ +import 'package:didvan/models/enums.dart'; +import 'package:didvan/providers/core_provider.dart'; +import 'package:didvan/services/storage/storage.dart'; + +class GeneralSettingsState extends CoreProvier { + GeneralSettingsState() { + getSettingsFromStorage(); + } + + List _notificationTimeRange = ['00:00', '23:59']; + String _fontFamily = 'Dana-FA'; + double _fontSizeScale = 1; + String _brightness = 'light'; + + set notificationTimeRange(List value) { + _notificationTimeRange = value; + StorageService.setValue( + key: 'notificationTimeRangeStart', + value: value[0], + ); + StorageService.setValue( + key: 'notificationTimeRangeStart', + value: value[1], + ); + } + + List get notificationTimeRange => _notificationTimeRange; + + set fontFamily(String value) { + _fontFamily = value; + StorageService.setValue( + key: 'fontFamily', + value: value, + ); + notifyListeners(); + } + + String get fontFamily => _fontFamily; + + set fontSizeScale(double value) { + _fontSizeScale = value; + StorageService.setValue( + key: 'fontSizeScale', + value: value, + ); + notifyListeners(); + } + + double get fontSizeScale => _fontSizeScale; + + set brightness(String value) { + _brightness = value; + StorageService.setValue( + key: 'brightness', + value: value, + ); + notifyListeners(); + } + + String get brightness => _brightness; + + Future getSettingsFromStorage() async { + appState = AppState.busy; + _notificationTimeRange[0] = + await StorageService.getValue(key: 'notificationTimeRangeStart'); + _notificationTimeRange[1] = + await StorageService.getValue(key: 'notificationTimeRangeEnd'); + _fontFamily = await StorageService.getValue(key: 'fontFamily'); + _brightness = await StorageService.getValue(key: 'brightness'); + final scale = await StorageService.getValue(key: 'fontSizeScale'); + _fontSizeScale = double.parse(scale); + appState = AppState.idle; + } +} diff --git a/lib/views/home/settings/profile/profile.dart b/lib/views/home/settings/profile/profile.dart new file mode 100644 index 0000000..071d6ac --- /dev/null +++ b/lib/views/home/settings/profile/profile.dart @@ -0,0 +1,184 @@ +import 'dart:async'; + +import 'package:didvan/config/design_config.dart'; +import 'package:didvan/models/view/app_bar_data.dart'; +import 'package:didvan/providers/user_provider.dart'; +import 'package:didvan/routes/routes.dart'; +import 'package:didvan/views/home/settings/profile/widgets/profile_photo.dart'; +import 'package:didvan/views/home/widgets/menu_item.dart'; +import 'package:didvan/views/widgets/animated_visibility.dart'; +import 'package:didvan/views/widgets/didvan/button.dart'; +import 'package:didvan/views/widgets/didvan/card.dart'; +import 'package:didvan/views/widgets/didvan/divider.dart'; +import 'package:didvan/views/widgets/didvan/scaffold.dart'; +import 'package:didvan/views/widgets/didvan/text.dart'; +import 'package:didvan/views/widgets/didvan/text_field.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +class Profile extends StatefulWidget { + const Profile({Key? key}) : super(key: key); + + @override + State createState() => _ProfileState(); +} + +class _ProfileState extends State { + Timer? _timer; + + late String fullName; + String? username; + String? email; + bool _usernameIsAvailible = true; + + final _formKey = GlobalKey(); + + @override + void initState() { + final user = context.read().user; + fullName = user.fullName; + username = user.username; + email = user.email; + super.initState(); + } + + @override + Widget build(BuildContext context) { + return Consumer( + builder: (context, state, child) => Material( + child: Stack( + children: [ + Form( + key: _formKey, + child: DidvanScaffold( + padding: const EdgeInsets.only(left: 16, right: 16, bottom: 92), + appBarData: AppBarData(title: 'ویرایش پروفایل'), + children: [ + const ProfilePhoto(), + const SizedBox(height: 16), + DidvanCard( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + DidvanTextField( + title: 'نام و نام‌ خانوادگی', + hintText: 'نام و نام خانوادگی', + initialValue: state.user.fullName, + onChanged: (value) { + _setButtonState(); + fullName = value; + }, + ), + const SizedBox(height: 16), + DidvanTextField( + title: 'موبایل', + enabled: false, + hintText: state.user.phoneNumber, + ), + const SizedBox(height: 16), + DidvanTextField( + title: 'نام کاربری', + hintText: 'انتخاب نام کاربری (اختیاری)', + onChanged: _onUsernameChanged, + initialValue: state.user.username, + ), + AnimatedVisibility( + duration: DesignConfig.lowAnimationDuration, + isVisible: !_usernameIsAvailible, + child: DidvanText( + 'نام کاربری در دسترس نمی‌باشد', + style: Theme.of(context).textTheme.caption, + color: Theme.of(context).colorScheme.error, + ), + ), + const SizedBox(height: 16), + DidvanTextField( + title: 'ایمیل', + hintText: 'مثال: example@email.com', + onChanged: (value) { + _setButtonState(); + email = value; + }, + initialValue: state.user.email, + validator: (value) async { + bool emailValid = RegExp( + r"^[a-zA-Z0-9.a-zA-Z0-9.!#$%&'*+-/=?^_`{|}~]+@[a-zA-Z0-9]+\.[a-zA-Z]+") + .hasMatch(value); + if (!emailValid) { + return 'ایمیل وارد شده معتبر نمی‌باشد'; + } + }, + ), + // const SizedBox(height: 16), + // DidvanSwitch( + // value: true, + // title: 'ورود با اثر انگشت', + // onChanged: (value) => {}, + // ), + const DidvanDivider(), + MenuItem( + title: 'تغییر رمز عبور', + onTap: () => Navigator.of(context).pushNamed( + Routes.authenticaion, + arguments: true, + ), + ), + ], + ), + ), + ], + ), + ), + Positioned( + left: 20, + right: 20, + bottom: 20, + child: DidvanButton( + key: UniqueKey(), + title: 'ذخیره تغییرات', + onPressed: () { + if (_formKey.currentState!.validate()) { + state.editProfile(fullName, username, email); + } + }, + enabled: _buttonEnabled, + ), + ), + ], + ), + ), + ); + } + + void _setButtonState() { + if (!_buttonEnabled) { + setState(() {}); + } + } + + bool get _buttonEnabled { + final user = context.read().user; + return !(user.email == email && + user.fullName == fullName && + user.username == username); + } + + void _onUsernameChanged(String value) { + _usernameIsAvailible = true; + _setButtonState(); + username = value; + _timer?.cancel(); + if (value.length < 5) { + return; + } + _timer = Timer(const Duration(seconds: 1), () { + context.read().checkUsername(value).then((value) { + if (value == false) { + setState(() { + _usernameIsAvailible = false; + }); + } + }); + }); + } +} diff --git a/lib/views/home/settings/profile/widgets/profile_photo.dart b/lib/views/home/settings/profile/widgets/profile_photo.dart new file mode 100644 index 0000000..62f1018 --- /dev/null +++ b/lib/views/home/settings/profile/widgets/profile_photo.dart @@ -0,0 +1,168 @@ +import 'dart:io'; + +import 'package:didvan/config/theme_data.dart'; +import 'package:didvan/constants/app_icons.dart'; +import 'package:didvan/models/enums.dart'; +import 'package:didvan/models/view/action_sheet_data.dart'; +import 'package:didvan/models/view/alert_data.dart'; +import 'package:didvan/providers/user_provider.dart'; +import 'package:didvan/services/media/media.dart'; +import 'package:didvan/utils/action_sheet.dart'; +import 'package:didvan/views/home/widgets/menu_item.dart'; +import 'package:didvan/views/widgets/didvan/divider.dart'; +import 'package:didvan/views/widgets/didvan/text.dart'; +import 'package:didvan/views/widgets/skeleton_image.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:image_picker/image_picker.dart'; +import 'package:provider/provider.dart'; +import 'package:image_cropper/image_cropper.dart'; + +class ProfilePhoto extends StatefulWidget { + const ProfilePhoto({Key? key}) : super(key: key); + + @override + State createState() => _ProfilePhotoState(); +} + +class _ProfilePhotoState extends State { + @override + Widget build(BuildContext context) { + final userProvider = context.watch(); + return GestureDetector( + onTap: () => _openImagePickerSheet(), + child: Center( + child: Stack( + children: [ + if (userProvider.user.photo == null) + Container( + height: 96, + width: 96, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: Theme.of(context).colorScheme.focused, + ), + child: Icon( + DidvanIcons.profile_solid, + size: 60, + color: Theme.of(context).colorScheme.title, + ), + ), + if (userProvider.user.photo != null) + SkeletonImage( + key: GlobalKey(debugLabel: userProvider.user.photo), + imageUrl: userProvider.user.photo!, + width: 96, + height: 96, + borderRadius: BorderRadius.circular(200), + ), + Positioned( + bottom: 0, + right: 0, + child: Container( + padding: const EdgeInsets.all(4), + decoration: BoxDecoration( + border: Border.all( + color: Theme.of(context).colorScheme.cardBorder, + ), + color: Theme.of(context).colorScheme.surface, + borderRadius: BorderRadius.circular(100), + ), + child: const Icon( + DidvanIcons.camera_regular, + ), + ), + ), + ], + ), + ), + ); + } + + Future _openImagePickerSheet() async { + FocusScope.of(context).unfocus(); + await ActionSheetUtils.showBottomSheet( + data: ActionSheetData( + content: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const DidvanText('بارگذاری تصویر از:'), + const SizedBox(height: 16), + Padding( + padding: const EdgeInsets.only(right: 20), + child: Column( + children: [ + MenuItem( + title: 'دوربین', + onTap: () => _setProfilePhoto(ImageSource.camera), + icon: DidvanIcons.camera_regular, + ), + const DidvanDivider(), + MenuItem( + title: 'گالری', + onTap: () => _setProfilePhoto(ImageSource.gallery), + icon: DidvanIcons.gallery_file_regular, + ), + ], + ), + ), + const DidvanDivider(), + MenuItem( + title: 'حذف تصویر', + onTap: () => _setProfilePhoto(null), + icon: DidvanIcons.trash_solid, + color: Theme.of(context).colorScheme.secondary, + ), + ], + ), + hasConfirmButton: false, + hasDismissButton: false, + title: 'بارگذاری تصویر از:', + ), + ); + } + + Future _setProfilePhoto(ImageSource? source) async { + ActionSheetUtils.pop(); + final state = context.read(); + if (source == null) { + final result = await state.deleteProfilePhoto(); + ActionSheetUtils.showAlert( + AlertData( + message: + result ? 'تصویر پروفایل حذف شد.' : 'حذف تصویر موفقیت آمیز نبود.', + aLertType: result ? ALertType.success : ALertType.error, + ), + ); + return; + } + final pickedFile = await MediaService.pickImage(source: source); + File? file; + if (pickedFile != null && !kIsWeb) { + file = await ImageCropper().cropImage( + sourcePath: pickedFile.path, + aspectRatio: const CropAspectRatio(ratioX: 1, ratioY: 1), + iosUiSettings: const IOSUiSettings( + title: 'برش تصویر', + doneButtonTitle: 'تایید', + cancelButtonTitle: 'بازگشت', + ), + androidUiSettings: const AndroidUiSettings(toolbarTitle: 'برش تصویر'), + compressQuality: 70, + ); + if (file == null) return; + } + if (pickedFile == null) return; + final uploadFile = kIsWeb ? pickedFile : file; + final result = await state.setProfilePhoto(uploadFile); + ActionSheetUtils.showAlert( + AlertData( + message: result + ? 'تصویر پروفایل بارگذاری شد.' + : 'بارگذاری تصویر موفقیت آمیز نبود.', + aLertType: result ? ALertType.success : ALertType.error, + ), + ); + } +} diff --git a/lib/views/home/settings/settings.dart b/lib/views/home/settings/settings.dart new file mode 100644 index 0000000..7753d26 --- /dev/null +++ b/lib/views/home/settings/settings.dart @@ -0,0 +1,118 @@ +import 'package:didvan/constants/app_icons.dart'; +import 'package:didvan/providers/theme_provider.dart'; +import 'package:didvan/providers/user_provider.dart'; +import 'package:didvan/routes/routes.dart'; +import 'package:didvan/services/storage/storage.dart'; +import 'package:didvan/views/home/widgets/logo_app_bar.dart'; +import 'package:didvan/views/home/widgets/menu_item.dart'; +import 'package:didvan/views/widgets/didvan/card.dart'; +import 'package:didvan/views/widgets/didvan/divider.dart'; +import 'package:didvan/views/widgets/didvan/text.dart'; +import 'package:didvan/views/widgets/item_title.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:url_launcher/url_launcher.dart'; + +class Settings extends StatelessWidget { + const Settings({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return SingleChildScrollView( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Consumer( + builder: (context, themeProvider, child) => LogoAppBar( + hasExtraPadding: false, + key: UniqueKey(), + ), + ), + const SizedBox(height: 16), + DidvanCard( + child: Column( + children: [ + MenuItem( + title: 'پیام‌ها', + icon: DidvanIcons.message_regular, + onTap: () => + Navigator.of(context).pushNamed(Routes.directList), + ), + const DidvanDivider(), + MenuItem( + title: 'ویرایش پروفایل', + icon: DidvanIcons.user_edit_regular, + suffix: context.read().user.fullName, + onTap: () => Navigator.of(context).pushNamed(Routes.profile), + ), + const DidvanDivider(), + MenuItem( + title: 'تنظیمات', + icon: DidvanIcons.setting_regular, + onTap: () => + Navigator.of(context).pushNamed(Routes.generalSettings), + ), + const DidvanDivider(), + MenuItem( + title: 'نشان شده‌ها', + icon: DidvanIcons.bookmark_regular, + onTap: () => + Navigator.of(context).pushNamed(Routes.bookmarks), + ), + const DidvanDivider(), + MenuItem( + title: 'خروج از حساب کاربری', + icon: DidvanIcons.sign_out_regular, + color: Theme.of(context).colorScheme.secondary, + onTap: () async { + StorageService.delete(key: 'token'); + Navigator.of(context).pushNamedAndRemoveUntil( + Routes.splash, + (_) => false, + ); + }, + ), + ], + ), + ), + const ItemTitle( + title: 'درباره ما', + icon: DidvanIcons.info_circle_regular, + ), + DidvanCard( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + MenuItem( + icon: DidvanIcons.didvan_solid, + title: 'معرفی دیدوان', + onTap: () => Navigator.of(context).pushNamed(Routes.aboutUs), + ), + const DidvanDivider(), + MenuItem( + icon: DidvanIcons.support_regular, + title: 'پیام به پشتیبانی', + onTap: () { + launch('mailto:info@didvan.app'); + }, + ), + const DidvanDivider(), + MenuItem( + icon: DidvanIcons.alert_regular, + title: 'حریم خصوصی', + onTap: () => {}, + ), + ], + ), + ), + const SizedBox(height: 16), + DidvanText( + 'نسخه نرم‌افزار: آزمایشی', + style: Theme.of(context).textTheme.caption, + ), + ], + ), + ); + } +} diff --git a/lib/views/home/statistics/statistics.dart b/lib/views/home/statistics/statistics.dart new file mode 100644 index 0000000..eef8150 --- /dev/null +++ b/lib/views/home/statistics/statistics.dart @@ -0,0 +1,26 @@ +import 'package:didvan/config/theme_data.dart'; +import 'package:didvan/constants/assets.dart'; +import 'package:didvan/views/home/widgets/logo_app_bar.dart'; +import 'package:didvan/views/widgets/state_handlers/empty_state.dart'; +import 'package:flutter/material.dart'; + +class Statictics extends StatelessWidget { + const Statictics({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Column( + children: [ + const LogoAppBar(), + Expanded( + child: EmptyState( + asset: Assets.emptyChart, + title: 'قیمت‌ها و شاخص‌های اقتصادی', + subtitle: 'به زودی...', + titleColor: Theme.of(context).colorScheme.title, + ), + ), + ], + ); + } +} diff --git a/lib/views/home/studio/podcast_details/podcast_details.dart b/lib/views/home/studio/podcast_details/podcast_details.dart new file mode 100644 index 0000000..140d8d0 --- /dev/null +++ b/lib/views/home/studio/podcast_details/podcast_details.dart @@ -0,0 +1,17 @@ +import 'package:flutter/material.dart'; + +class PodcastDetails extends StatelessWidget { + const PodcastDetails({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Material( + child: Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + borderRadius: const BorderRadius.vertical(top: Radius.circular(8)), + ), + ), + ); + } +} diff --git a/lib/views/home/studio/studio.dart b/lib/views/home/studio/studio.dart new file mode 100644 index 0000000..08c8e37 --- /dev/null +++ b/lib/views/home/studio/studio.dart @@ -0,0 +1,130 @@ +import 'package:didvan/constants/app_icons.dart'; +import 'package:didvan/models/view/action_sheet_data.dart'; +import 'package:didvan/utils/action_sheet.dart'; +import 'package:didvan/views/home/studio/studio_state.dart'; +import 'package:didvan/views/home/studio/widgets/slider.dart'; +import 'package:didvan/views/home/studio/widgets/tab_bar.dart'; +import 'package:didvan/views/home/widgets/logo_app_bar.dart'; +import 'package:didvan/views/home/widgets/search_field.dart'; +import 'package:didvan/views/widgets/didvan/divider.dart'; +import 'package:didvan/views/widgets/didvan/icon_button.dart'; +import 'package:didvan/views/widgets/didvan/radial_button.dart'; +import 'package:didvan/views/widgets/item_title.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +class Studio extends StatefulWidget { + const Studio({Key? key}) : super(key: key); + + @override + State createState() => _StudioState(); +} + +class _StudioState extends State { + final FocusNode _focusNode = FocusNode(); + + @override + Widget build(BuildContext context) { + return CustomScrollView( + slivers: [ + SliverToBoxAdapter( + child: Row( + children: [ + const Expanded(child: LogoAppBar(type: 'studio')), + Padding( + padding: + EdgeInsets.only(top: MediaQuery.of(context).padding.top), + child: DidvanIconButton( + icon: DidvanIcons.bookmark_regular, + onPressed: () {}, + ), + ), + ], + ), + ), + const SliverToBoxAdapter( + child: StudioTabBar(), + ), + SliverToBoxAdapter( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: SearchField( + title: 'جستجو در استودیو', + onChanged: (value) {}, + focusNode: _focusNode, + ), + ), + ), + const SliverToBoxAdapter( + child: StudioSlider(), + ), + const SliverPadding( + padding: EdgeInsets.symmetric(horizontal: 16), + sliver: SliverToBoxAdapter( + child: DidvanDivider( + verticalPadding: 0, + ), + ), + ), + SliverPadding( + padding: const EdgeInsets.symmetric(horizontal: 16), + sliver: SliverToBoxAdapter( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const ItemTitle(title: 'تازه‌ترین‌ها'), + DidvanIconButton( + gestureSize: 36, + icon: DidvanIcons.sort_regular, + onPressed: _showSortDialog, + ), + ], + ), + ), + ), + ], + ); + } + + void _showSortDialog() { + final state = context.read(); + ActionSheetUtils.showBottomSheet( + data: ActionSheetData( + content: StatefulBuilder( + builder: (context, setState) => Column( + children: [ + DidvanRadialButton( + title: 'جدیدترین‌ها', + onSelected: () => setState( + () => state.selectedSortTypeIndex = 0, + ), + value: state.selectedSortTypeIndex == 0, + ), + const SizedBox(height: 24), + DidvanRadialButton( + title: 'پربازدیدترین‌ها', + onSelected: () => setState( + () => state.selectedSortTypeIndex = 1, + ), + value: state.selectedSortTypeIndex == 1, + ), + const SizedBox(height: 24), + DidvanRadialButton( + title: 'پربحث‌ترین‌ها', + onSelected: () => setState( + () => state.selectedSortTypeIndex = 2, + ), + value: state.selectedSortTypeIndex == 2, + ), + ], + ), + ), + title: 'مرتب‌‌سازی', + titleIcon: DidvanIcons.sort_regular, + hasDismissButton: false, + confrimTitle: 'مرتب سازی', + onConfirmed: () {}, + ), + ); + } +} diff --git a/lib/views/home/studio/studio_state.dart b/lib/views/home/studio/studio_state.dart new file mode 100644 index 0000000..28ebd1e --- /dev/null +++ b/lib/views/home/studio/studio_state.dart @@ -0,0 +1,7 @@ +import 'package:didvan/providers/core_provider.dart'; + +class StudioState extends CoreProvier { + int selectedSortTypeIndex = 0; + + bool videosSelected = true; +} diff --git a/lib/views/home/studio/video_details/video_details.dart b/lib/views/home/studio/video_details/video_details.dart new file mode 100644 index 0000000..e69de29 diff --git a/lib/views/home/studio/widgets/slider.dart b/lib/views/home/studio/widgets/slider.dart new file mode 100644 index 0000000..d312b2b --- /dev/null +++ b/lib/views/home/studio/widgets/slider.dart @@ -0,0 +1,51 @@ +import 'package:carousel_slider/carousel_slider.dart'; +import 'package:didvan/config/theme_data.dart'; +import 'package:flutter/material.dart'; + +class StudioSlider extends StatelessWidget { + const StudioSlider({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Column( + children: [ + CarouselSlider( + items: [ + Image.network('https://wallpapercave.com/wp/wp10731650.jpg'), + Image.network('https://wallpapercave.com/wp/wp10731650.jpg'), + Image.network('https://wallpapercave.com/wp/wp10731650.jpg'), + Image.network('https://wallpapercave.com/wp/wp10731650.jpg'), + ], + options: CarouselOptions( + viewportFraction: 0.94, + aspectRatio: 16 / 9, + autoPlay: true, + ), + ), + Row(), + ], + ); + } +} + +class _SliderIndicator extends StatelessWidget { + final bool isCurrentIndex; + const _SliderIndicator({Key? key, required this.isCurrentIndex}) + : super(key: key); + + @override + Widget build(BuildContext context) { + return Container( + height: 8, + width: 8, + decoration: BoxDecoration( + border: Border.all( + color: Theme.of(context).colorScheme.focusedBorder, + ), + shape: BoxShape.circle, + color: + isCurrentIndex ? Theme.of(context).colorScheme.focusedBorder : null, + ), + ); + } +} diff --git a/lib/views/home/studio/widgets/tab_bar.dart b/lib/views/home/studio/widgets/tab_bar.dart new file mode 100644 index 0000000..7e11744 --- /dev/null +++ b/lib/views/home/studio/widgets/tab_bar.dart @@ -0,0 +1,108 @@ +import 'package:didvan/config/design_config.dart'; +import 'package:didvan/config/theme_data.dart'; +import 'package:didvan/constants/app_icons.dart'; +import 'package:didvan/views/home/studio/studio_state.dart'; +import 'package:didvan/views/widgets/didvan/text.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +class StudioTabBar extends StatelessWidget { + const StudioTabBar({ + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + final state = context.watch(); + return Container( + margin: const EdgeInsets.symmetric(horizontal: 16), + padding: const EdgeInsets.all(4), + decoration: BoxDecoration( + border: Border.all( + color: state.videosSelected + ? Theme.of(context).colorScheme.secondary + : Theme.of(context).primaryColor, + ), + borderRadius: DesignConfig.lowBorderRadius, + ), + child: Row( + children: [ + Expanded( + child: _StudioTypeButton( + icon: DidvanIcons.video_solid, + selectedColor: Theme.of(context).colorScheme.secondary, + title: 'ویدئو', + onTap: () {}, + isSelected: state.videosSelected, + ), + ), + Container( + width: 1, + height: 32, + color: Theme.of(context).colorScheme.border, + ), + Expanded( + child: _StudioTypeButton( + icon: DidvanIcons.podcast_solid, + selectedColor: Theme.of(context).colorScheme.focusedBorder, + title: 'پادکست', + onTap: () {}, + isSelected: !state.videosSelected, + ), + ), + ], + ), + ); + } +} + +class _StudioTypeButton extends StatelessWidget { + final IconData icon; + final String title; + final VoidCallback onTap; + final bool isSelected; + final Color selectedColor; + const _StudioTypeButton({ + Key? key, + required this.icon, + required this.selectedColor, + required this.title, + required this.onTap, + required this.isSelected, + }) : super(key: key); + + Color? _color(context) => + isSelected ? selectedColor : Theme.of(context).colorScheme.hint; + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: onTap, + child: Container( + color: Colors.transparent, + child: Column( + children: [ + Icon( + icon, + size: 32, + color: _color(context), + ), + if (!isSelected) const SizedBox(height: 18), + if (isSelected) + Container( + width: 88, + height: 1, + color: _color(context), + ), + if (isSelected) + DidvanText( + title, + style: Theme.of(context).textTheme.overline, + color: _color(context), + ) + ], + ), + ), + ); + } +} diff --git a/lib/views/home/widgets/audio_slider.dart b/lib/views/home/widgets/audio_slider.dart new file mode 100644 index 0000000..b9edf70 --- /dev/null +++ b/lib/views/home/widgets/audio_slider.dart @@ -0,0 +1,39 @@ +import 'package:audio_video_progress_bar/audio_video_progress_bar.dart'; +import 'package:didvan/services/media/media.dart'; +import 'package:flutter/material.dart'; + +class AudioSlider extends StatelessWidget { + final String tag; + const AudioSlider({Key? key, required this.tag}) : super(key: key); + + bool get _isPlaying => MediaService.audioPlayerTag == tag; + + @override + Widget build(BuildContext context) { + return IgnorePointer( + ignoring: MediaService.audioPlayerTag != tag, + child: Directionality( + textDirection: TextDirection.ltr, + child: StreamBuilder( + stream: _isPlaying ? MediaService.audioPlayer.positionStream : null, + builder: (context, snapshot) { + return ProgressBar( + total: MediaService.audioPlayer.duration ?? Duration.zero, + progress: snapshot.data ?? Duration.zero, + buffered: + _isPlaying ? MediaService.audioPlayer.bufferedPosition : null, + thumbRadius: 6, + barHeight: 3, + timeLabelTextStyle: const TextStyle(fontSize: 0), + onSeek: (value) => _onSeek(value.inMilliseconds), + ); + }, + ), + ), + ); + } + + void _onSeek(int value) { + MediaService.audioPlayer.seek(Duration(milliseconds: value)); + } +} diff --git a/lib/views/home/widgets/audio_visualizer.dart b/lib/views/home/widgets/audio_visualizer.dart new file mode 100644 index 0000000..c39ca3d --- /dev/null +++ b/lib/views/home/widgets/audio_visualizer.dart @@ -0,0 +1,314 @@ +// import 'dart:io'; +// import 'dart:math'; + +// import 'package:didvan/config/design_config.dart'; +// import 'package:didvan/config/theme_data.dart'; +// import 'package:didvan/constants/app_icons.dart'; +// import 'package:didvan/constants/assets.dart'; +// import 'package:didvan/pages/home/direct/direct_state.dart'; +// import 'package:didvan/services/media/media.dart'; +// import 'package:didvan/services/storage/storage.dart'; +// import 'package:didvan/utils/date_time.dart'; +// import 'package:didvan/widgets/didvan/icon_button.dart'; +// import 'package:didvan/widgets/didvan/text.dart'; +// import 'package:flutter/foundation.dart'; +// import 'package:flutter/material.dart'; +// import 'package:flutter_svg/flutter_svg.dart'; +// import 'package:just_waveform/just_waveform.dart'; +// import 'package:provider/provider.dart'; + +// class AudioVisualizer extends StatefulWidget { +// final File? audioFile; +// final Waveform? waveform; +// final String? audioUrl; +// final int? duration; +// final Color? backgroundColor; + +// const AudioVisualizer({ +// Key? key, +// this.audioFile, +// this.waveform, +// this.audioUrl, +// this.duration, +// this.backgroundColor, +// }) : super(key: key); + +// @override +// State createState() => _AudioVisualizerState(); +// } + +// class _AudioVisualizerState extends State { +// Stream? waveDataStream; + +// @override +// void initState() { +// if (!kIsWeb && widget.audioFile != null) { +// waveDataStream = JustWaveform.extract( +// audioInFile: widget.audioFile!, +// waveOutFile: File(StorageService.appTempsDir + '/rec-wave.wave'), +// zoom: const WaveformZoom.pixelsPerSecond(100), +// ); +// } +// super.initState(); +// } + +// bool get _nowPlaying => +// MediaService.lastAudioPath == widget.audioFile || +// MediaService.lastAudioPath == widget.audioUrl; + +// @override +// Widget build(BuildContext context) { +// return Container( +// decoration: BoxDecoration( +// color: widget.backgroundColor ?? +// (DesignConfig.isDark +// ? Theme.of(context).colorScheme.black +// : Theme.of(context).colorScheme.background), +// borderRadius: DesignConfig.mediumBorderRadius, +// ), +// child: Row( +// children: [ +// const SizedBox(width: 12), +// StreamBuilder( +// stream: +// _nowPlaying ? MediaService.audioPlayer.positionStream : null, +// builder: (context, snapshot) { +// String text = ''; +// if (MediaService.audioPlayer.duration == null) { +// Future.delayed(Duration.zero, () { +// if (mounted) { +// setState(() {}); +// } +// }); +// } +// if (snapshot.data == null || snapshot.data == Duration.zero) { +// text = DateTimeUtils.normalizeTimeDuration( +// MediaService.audioPlayer.duration ?? +// widget.waveform?.duration ?? +// Duration.zero); +// } else { +// text = DateTimeUtils.normalizeTimeDuration(snapshot.data!); +// } +// return DidvanText( +// text, +// color: Theme.of(context).colorScheme.focusedBorder, +// isEnglishFont: true, +// ); +// }, +// ), +// const SizedBox(width: 12), +// Expanded( +// child: Builder( +// builder: (context) { +// if (kIsWeb) { +// return SvgPicture.asset(Assets.record); +// } +// if (widget.audioFile != null) { +// return StreamBuilder( +// stream: waveDataStream, +// builder: (context, snapshot) { +// if (snapshot.data == null || +// snapshot.data!.waveform == null) { +// return const SizedBox(); +// } +// final waveform = snapshot.data!.waveform!; +// context.read().waveform = waveform; +// return _waveWidget(waveform); +// }, +// ); +// } +// if (widget.waveform == null && waveDataStream == null) { +// return SvgPicture.asset(Assets.record); +// } +// return _waveWidget(widget.waveform!); +// }, +// ), +// ), +// StreamBuilder( +// stream: _nowPlaying ? MediaService.audioPlayer.playingStream : null, +// builder: (context, snapshot) { +// return DidvanIconButton( +// icon: snapshot.data == true +// ? DidvanIcons.pause_circle_solid +// : DidvanIcons.play_circle_solid, +// color: Theme.of(context).colorScheme.focusedBorder, +// onPressed: () { +// MediaService.handleAudioPlayback( +// audioSource: widget.audioFile ?? widget.audioUrl, +// isNetworkAudio: widget.audioFile == null, +// ); +// setState(() {}); +// }, +// ); +// }, +// ), +// ], +// ), +// ); +// } + +// Widget _waveWidget(Waveform waveform) => IgnorePointer( +// ignoring: !_nowPlaying, +// child: GestureDetector( +// onHorizontalDragUpdate: _changePosition, +// onTapDown: _changePosition, +// child: SizedBox( +// height: double.infinity, +// width: double.infinity, +// child: _AudioWaveformWidget( +// waveform: waveform, +// start: Duration.zero, +// scale: 2, +// strokeWidth: 3, +// nowPlaying: _nowPlaying, +// duration: waveform.duration, +// waveColor: Theme.of(context).colorScheme.focusedBorder, +// ), +// ), +// ), +// ); + +// void _changePosition(details) { +// if (MediaService.audioPlayer.audioSource == null) return; +// double posper = +// details.localPosition.dx / (MediaQuery.of(context).size.width - 200); +// if (posper >= 1 || posper < 0) return; +// final position = MediaService.audioPlayer.duration!.inMilliseconds; +// MediaService.audioPlayer.seek( +// Duration(milliseconds: (posper * position).toInt()), +// ); +// } +// } + +// class _AudioWaveformWidget extends StatelessWidget { +// final Color waveColor; +// final double scale; +// final double strokeWidth; +// final double pixelsPerStep; +// final Waveform waveform; +// final Duration start; +// final bool nowPlaying; +// final Duration duration; + +// const _AudioWaveformWidget({ +// Key? key, +// required this.waveform, +// required this.start, +// required this.duration, +// required this.nowPlaying, +// this.waveColor = Colors.blue, +// this.scale = 1.0, +// this.strokeWidth = 5.0, +// this.pixelsPerStep = 8.0, +// }) : super(key: key); + +// @override +// Widget build(BuildContext context) { +// return ClipRect( +// child: StreamBuilder( +// stream: nowPlaying ? MediaService.audioPlayer.positionStream : null, +// builder: (context, snapshot) { +// double progress = 0; +// if (snapshot.data == null || +// MediaService.audioPlayer.duration == null) { +// progress = 0; +// } else { +// progress = snapshot.data!.inMilliseconds / +// MediaService.audioPlayer.duration!.inMilliseconds * +// 100; +// } +// if (progress >= 100) { +// progress = 0; +// MediaService.audioPlayer.stop(); +// MediaService.audioPlayer.seek(Duration.zero); +// } +// return CustomPaint( +// painter: _AudioWaveformPainter( +// waveColor: waveColor, +// waveform: waveform, +// start: start, +// duration: duration, +// scale: scale, +// strokeWidth: strokeWidth, +// pixelsPerStep: pixelsPerStep, +// progressPercentage: progress, +// progressColor: Theme.of(context).colorScheme.focusedBorder, +// color: Theme.of(context).colorScheme.border, +// ), +// ); +// }), +// ); +// } +// } + +// class _AudioWaveformPainter extends CustomPainter { +// final double scale; +// final double strokeWidth; +// final double pixelsPerStep; +// final Waveform waveform; +// final Duration start; +// final Duration duration; +// final double progressPercentage; +// final Color progressColor; +// final Color color; + +// _AudioWaveformPainter({ +// required this.waveform, +// required this.start, +// required this.duration, +// required this.progressPercentage, +// required this.color, +// required this.progressColor, +// Color waveColor = Colors.blue, +// this.scale = 1.0, +// this.strokeWidth = 5.0, +// this.pixelsPerStep = 8.0, +// }); + +// @override +// void paint(Canvas canvas, Size size) { +// if (duration == Duration.zero) return; +// double width = size.width; +// double height = size.height; + +// final waveformPixelsPerWindow = waveform.positionToPixel(duration).toInt(); +// final waveformPixelsPerDevicePixel = waveformPixelsPerWindow / width; +// final waveformPixelsPerStep = waveformPixelsPerDevicePixel * pixelsPerStep; +// final sampleOffset = waveform.positionToPixel(start); +// final sampleStart = -sampleOffset % waveformPixelsPerStep; +// final totalLength = waveformPixelsPerWindow; +// final wavePaintB = Paint() +// ..style = PaintingStyle.stroke +// ..strokeWidth = strokeWidth +// ..strokeCap = StrokeCap.round +// ..color = progressColor; +// final wavePaintA = Paint() +// ..style = PaintingStyle.stroke +// ..strokeWidth = strokeWidth +// ..strokeCap = StrokeCap.round +// ..color = color; +// for (var i = sampleStart.toDouble(); +// i <= waveformPixelsPerWindow + 1.0; +// i += waveformPixelsPerStep) { +// final sampleIdx = (sampleOffset + i).toInt(); +// final x = i / waveformPixelsPerDevicePixel; +// final minY = normalise(waveform.getPixelMin(sampleIdx), height); +// final maxY = normalise(waveform.getPixelMax(sampleIdx), height); +// canvas.drawLine( +// Offset(x + strokeWidth / 2, max(strokeWidth * 0.75, minY)), +// Offset(x + strokeWidth / 2, min(height - strokeWidth * 0.75, maxY)), +// i / totalLength < progressPercentage / 100 ? wavePaintB : wavePaintA, +// ); +// } +// } + +// @override +// bool shouldRepaint(covariant _AudioWaveformPainter oldDelegate) { +// return oldDelegate.progressPercentage != progressPercentage; +// } + +// double normalise(int s, double height) { +// final y = 32768 + (scale * s).clamp(-32768.0, 32767.0).toDouble(); +// return height - 1 - y * height / 65536; +// } +// } diff --git a/lib/views/home/widgets/bnb.dart b/lib/views/home/widgets/bnb.dart new file mode 100644 index 0000000..4c78de0 --- /dev/null +++ b/lib/views/home/widgets/bnb.dart @@ -0,0 +1,174 @@ +import 'package:didvan/config/design_config.dart'; +import 'package:didvan/config/theme_data.dart'; +import 'package:didvan/constants/app_icons.dart'; +import 'package:didvan/services/media/media.dart'; +import 'package:didvan/views/widgets/didvan/icon_button.dart'; +import 'package:didvan/views/widgets/didvan/text.dart'; +import 'package:didvan/views/widgets/skeleton_image.dart'; +import 'package:flutter/material.dart'; + +class DidvanBNB extends StatelessWidget { + final int currentTabIndex; + final void Function(int index) onTabChanged; + + const DidvanBNB( + {Key? key, required this.currentTabIndex, required this.onTabChanged}) + : super(key: key); + + @override + Widget build(BuildContext context) { + return StreamBuilder( + stream: MediaService.audioPlayer.playingStream, + builder: (context, snapshot) { + return Stack( + children: [ + AnimatedContainer( + duration: DesignConfig.lowAnimationDuration, + height: snapshot.data == true ? 120 : 72, + decoration: BoxDecoration( + color: DesignConfig.isDark + ? Theme.of(context).colorScheme.focused + : Theme.of(context).colorScheme.navigation, + borderRadius: const BorderRadius.vertical( + top: Radius.circular(16), + ), + ), + child: Row( + children: [ + const DidvanIconButton( + icon: DidvanIcons.close_regular, + gestureSize: 24, + onPressed: MediaService.resetAudioPlayer, + ), + const SizedBox(width: 16), + if (MediaService.audioPlayerCover != null) + SkeletonImage(imageUrl: MediaService.audioPlayerCover!), + const SizedBox(width: 16), + ], + ), + ), + Positioned( + bottom: 0, + left: 0, + right: 0, + child: Container( + height: 72, + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + borderRadius: + const BorderRadius.vertical(top: Radius.circular(16)), + boxShadow: DesignConfig.defaultShadow, + ), + padding: const EdgeInsets.symmetric(horizontal: 12), + child: Row( + children: [ + _NavBarItem( + isSelected: currentTabIndex == 0, + title: 'اخبار', + selectedIcon: DidvanIcons.news_solid, + unselectedIcon: DidvanIcons.news_light, + onTap: () => onTabChanged(0), + ), + _NavBarItem( + isSelected: currentTabIndex == 1, + title: 'آمار', + selectedIcon: DidvanIcons.chart_solid, + unselectedIcon: DidvanIcons.chart_light, + onTap: () => onTabChanged(1), + ), + _NavBarItem( + isSelected: currentTabIndex == 2, + title: 'رادار', + selectedIcon: DidvanIcons.radar_solid, + unselectedIcon: DidvanIcons.radar_light, + onTap: () => onTabChanged(2), + ), + _NavBarItem( + isSelected: currentTabIndex == 3, + title: 'استودیو', + selectedIcon: DidvanIcons.play_circle_solid, + unselectedIcon: DidvanIcons.play_circle_light, + onTap: () => onTabChanged(3), + ), + _NavBarItem( + isSelected: currentTabIndex == 4, + title: 'تنظیمات', + selectedIcon: DidvanIcons.setting_solid, + unselectedIcon: DidvanIcons.setting_light, + onTap: () => onTabChanged(4), + ), + ], + ), + ), + ), + ], + ); + }); + } +} + +class _NavBarItem extends StatelessWidget { + final VoidCallback onTap; + final bool isSelected; + final String title; + final IconData selectedIcon; + final IconData unselectedIcon; + const _NavBarItem({ + Key? key, + required this.isSelected, + required this.title, + required this.selectedIcon, + required this.unselectedIcon, + required this.onTap, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Expanded( + child: Tooltip( + message: title, + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.title, + borderRadius: DesignConfig.highBorderRadius, + boxShadow: DesignConfig.defaultShadow, + ), + child: GestureDetector( + onTap: onTap, + child: Container( + color: Colors.transparent, + child: Column( + children: [ + const SizedBox( + height: 4, + ), + AnimatedContainer( + padding: const EdgeInsets.all(4), + duration: DesignConfig.lowAnimationDuration, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: isSelected + ? Theme.of(context).colorScheme.focused + : Theme.of(context).colorScheme.surface, + ), + child: Icon( + isSelected ? selectedIcon : unselectedIcon, + size: 32, + color: DesignConfig.isDark + ? Theme.of(context).colorScheme.text + : Theme.of(context).colorScheme.title, + ), + ), + DidvanText( + title, + style: Theme.of(context).textTheme.caption, + color: Theme.of(context).colorScheme.title, + ), + const Spacer(), + ], + ), + ), + ), + ), + ); + } +} diff --git a/lib/views/home/widgets/bookmark_button.dart b/lib/views/home/widgets/bookmark_button.dart new file mode 100644 index 0000000..4de2621 --- /dev/null +++ b/lib/views/home/widgets/bookmark_button.dart @@ -0,0 +1,69 @@ +import 'package:didvan/constants/app_icons.dart'; +import 'package:didvan/models/view/action_sheet_data.dart'; +import 'package:didvan/utils/action_sheet.dart'; +import 'package:didvan/views/widgets/didvan/icon_button.dart'; +import 'package:didvan/views/widgets/didvan/text.dart'; +import 'package:flutter/material.dart'; + +class BookmarkButton extends StatefulWidget { + final bool value; + final void Function(bool value) onMarkChanged; + final bool bigGestureSize; + final bool askForConfirmation; + const BookmarkButton({ + Key? key, + required this.value, + this.bigGestureSize = false, + required this.onMarkChanged, + this.askForConfirmation = false, + }) : super(key: key); + + @override + State createState() => _BookmarkButtonState(); +} + +class _BookmarkButtonState extends State { + bool _value = false; + + @override + void didUpdateWidget(covariant BookmarkButton oldWidget) { + _value = widget.value; + super.didUpdateWidget(oldWidget); + } + + @override + void initState() { + _value = widget.value; + super.initState(); + } + + @override + Widget build(BuildContext context) { + return DidvanIconButton( + gestureSize: widget.bigGestureSize ? 32 : 24, + icon: _value ? DidvanIcons.bookmark_solid : DidvanIcons.bookmark_regular, + onPressed: () async { + bool confirm = false; + if (widget.askForConfirmation) { + await ActionSheetUtils.openDialog( + data: ActionSheetData( + content: const DidvanText( + 'آیا می‌خواهید این محتوا از نشان‌ شده‌ها حذف شود؟', + ), + titleIcon: DidvanIcons.bookmark_regular, + titleColor: Theme.of(context).colorScheme.secondary, + title: 'تایید عملیات', + onConfirmed: () => confirm = true, + ), + ); + } + if (!widget.askForConfirmation || confirm) { + setState(() { + _value = !_value; + }); + widget.onMarkChanged(_value); + } + }, + ); + } +} diff --git a/lib/views/home/widgets/date_picker_button.dart b/lib/views/home/widgets/date_picker_button.dart new file mode 100644 index 0000000..ac5164c --- /dev/null +++ b/lib/views/home/widgets/date_picker_button.dart @@ -0,0 +1,70 @@ +import 'package:didvan/config/design_config.dart'; +import 'package:didvan/config/theme_data.dart'; +import 'package:didvan/utils/date_time.dart'; +import 'package:didvan/views/widgets/didvan/text.dart'; +import 'package:flutter/material.dart'; +import 'package:persian_number_utility/persian_number_utility.dart'; + +class DatePickerButton extends StatefulWidget { + final String? initialValue; + final String? emptyText; + final void Function(String? date) onPicked; + final String? firstDate; + final String? lastDate; + const DatePickerButton({ + Key? key, + this.initialValue, + required this.onPicked, + this.firstDate, + this.lastDate, + this.emptyText, + }) : super(key: key); + + @override + State createState() => _DatePickerButtonState(); +} + +class _DatePickerButtonState extends State { + String? pickedDate; + + @override + void initState() { + pickedDate = widget.initialValue; + super.initState(); + } + + @override + Widget build(BuildContext context) { + return Expanded( + child: GestureDetector( + onTap: () async { + pickedDate = await DateTimeUtils.showDatePicker( + initialDate: pickedDate, + startDate: widget.firstDate, + endDate: widget.lastDate, + ); + if (pickedDate == null) { + return; + } + setState(() {}); + widget.onPicked(pickedDate); + }, + child: Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: DesignConfig.brightness == Brightness.dark + ? Theme.of(context).colorScheme.splash + : Theme.of(context).colorScheme.surface, + borderRadius: DesignConfig.lowBorderRadius, + border: Border.all(color: Theme.of(context).colorScheme.border), + ), + child: DidvanText( + pickedDate == null + ? widget.emptyText ?? 'تاریخ' + : pickedDate!.toPersianDate(), + ), + ), + ), + ); + } +} diff --git a/lib/views/home/widgets/floating_navigation_bar.dart b/lib/views/home/widgets/floating_navigation_bar.dart new file mode 100644 index 0000000..3cc0323 --- /dev/null +++ b/lib/views/home/widgets/floating_navigation_bar.dart @@ -0,0 +1,248 @@ +import 'package:didvan/config/design_config.dart'; +import 'package:didvan/config/theme_data.dart'; +import 'package:didvan/constants/app_icons.dart'; +import 'package:didvan/models/message_data/radar_attachment.dart'; +import 'package:didvan/models/view/action_sheet_data.dart'; +import 'package:didvan/routes/routes.dart'; +import 'package:didvan/utils/action_sheet.dart'; +import 'package:didvan/views/home/widgets/bookmark_button.dart'; +import 'package:didvan/views/home/widgets/menu_item.dart'; +import 'package:didvan/views/widgets/didvan/chip.dart'; +import 'package:didvan/views/widgets/didvan/divider.dart'; +import 'package:didvan/views/widgets/didvan/icon_button.dart'; +import 'package:didvan/views/widgets/didvan/text.dart'; +import 'package:didvan/views/widgets/item_title.dart'; +import 'package:flutter/material.dart'; + +class FloatingNavigationBar extends StatefulWidget { + final ScrollController scrollController; + final dynamic item; + final bool hasUnmarkConfirmation; + final void Function(int count) onCommentsChanged; + final bool isRadar; + final void Function(bool value) onMarkChanged; + + const FloatingNavigationBar({ + Key? key, + required this.scrollController, + required this.onCommentsChanged, + required this.onMarkChanged, + required this.isRadar, + required this.item, + this.hasUnmarkConfirmation = false, + }) : super(key: key); + + @override + State createState() => _FloatingNavigationBarState(); +} + +class _FloatingNavigationBarState extends State { + bool _isScrolled = false; + int _comments = 0; + + @override + void didUpdateWidget(covariant FloatingNavigationBar oldWidget) { + _comments = widget.item.comments; + _isScrolled = false; + _handleScroll(); + super.didUpdateWidget(oldWidget); + } + + @override + void initState() { + _handleScroll(); + _isScrolled = false; + _comments = widget.item.comments; + super.initState(); + } + + @override + Widget build(BuildContext context) { + final Color foregroundColor = DesignConfig.isDark + ? Theme.of(context).colorScheme.focusedBorder + : Theme.of(context).colorScheme.focused; + return Container( + margin: const EdgeInsets.only(left: 32, right: 32, bottom: 20), + padding: const EdgeInsets.symmetric(horizontal: 12), + width: double.infinity, + height: 48, + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.navigation, + borderRadius: BorderRadius.circular(24), + border: DesignConfig.isDark + ? Border.all( + color: Theme.of(context).colorScheme.cardBorder, + ) + : null, + ), + child: Theme( + data: Theme.of(context).copyWith( + iconTheme: IconThemeData( + color: foregroundColor, + ), + ), + child: Row( + children: [ + AnimatedRotation( + duration: DesignConfig.lowAnimationDuration, + turns: _isScrolled ? -0.25 : 0, + child: DidvanIconButton( + onPressed: () { + if (_isScrolled) { + widget.scrollController.animateTo( + 0, + duration: DesignConfig.lowAnimationDuration, + curve: Curves.easeIn, + ); + return; + } + Navigator.of(context).pop(); + }, + icon: DidvanIcons.back_regular, + ), + ), + const Spacer(), + if (widget.isRadar) + BookmarkButton( + askForConfirmation: widget.hasUnmarkConfirmation, + value: widget.item.marked, + onMarkChanged: (value) { + widget.onMarkChanged(value); + if (widget.hasUnmarkConfirmation && !value) { + Navigator.of(context).pop(); + } + }, + bigGestureSize: true, + ), + SizedBox( + width: 60, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + if (_comments != 0) + DidvanText( + _comments.toString(), + color: foregroundColor, + ), + DidvanIconButton( + gestureSize: 32, + onPressed: () => Navigator.of(context).pushNamed( + Routes.comments, + arguments: { + 'id': widget.item.id, + 'isRadar': widget.isRadar, + 'title': widget.item.title, + 'onCommentsChanged': widget.onCommentsChanged, + }, + ), + icon: DidvanIcons.chats_regular, + ), + ], + ), + ), + if (!widget.isRadar) const SizedBox(width: 12), + if (!widget.isRadar) + BookmarkButton( + askForConfirmation: widget.hasUnmarkConfirmation, + value: widget.item.marked, + onMarkChanged: (value) { + widget.onMarkChanged(value); + if (widget.hasUnmarkConfirmation && !value) { + Navigator.of(context).pop(); + } + }, + bigGestureSize: true, + ), + if (widget.isRadar) + DidvanIconButton( + gestureSize: 32, + onPressed: _showMoreOptions, + icon: DidvanIcons.menu_regular, + ), + ], + ), + ), + ); + } + + void _handleScroll() { + widget.scrollController.addListener(() { + final position = widget.scrollController.position.pixels; + final size = MediaQuery.of(context).size.height * 0.3; + if (position > size && _isScrolled == false) { + setState(() { + _isScrolled = true; + }); + } + if (position < size && _isScrolled == true) { + setState(() { + _isScrolled = false; + }); + } + }); + } + + void _showMoreOptions() { + final categories = widget.item.categories!; + ActionSheetUtils.showBottomSheet( + data: ActionSheetData( + content: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const ItemTitle( + title: 'ارتباط با سردبیر', + icon: DidvanIcons.profile_regular, + ), + const SizedBox(height: 16), + for (var i = 0; i < categories.length; i++) ...[ + Padding( + padding: const EdgeInsets.only(right: 20), + child: MenuItem( + titleWidget: DidvanChip(label: categories[i].label), + onTap: () { + Navigator.of(context).pop(); + Navigator.of(context).pushNamed( + Routes.direct, + arguments: { + 'radarAttachment': RadarAttachment( + id: widget.item.id, + title: widget.item.title, + description: widget.item.contents.first.text, + timeToRead: widget.item.timeToRead, + image: widget.item.image, + forManagers: widget.item.forManagers, + categories: widget.item.categories, + createdAt: widget.item.createdAt, + ), + 'type': categories[i].label, + }, + ); + }, + ), + ), + if (i != categories.length - 1) + const Padding( + padding: EdgeInsets.only(right: 20), + child: DidvanDivider(verticalPadding: 8), + ), + ], + const DidvanDivider(), + MenuItem( + title: 'گزارش اشکال', + onTap: () { + Navigator.of(context).pop(); + Navigator.of(context).pushNamed( + Routes.direct, + arguments: {}, + ); + }, + icon: DidvanIcons.description_regular, + ), + ], + ), + title: 'موارد بیشتر', + withoutButtonMode: true, + ), + ); + } +} diff --git a/lib/views/home/widgets/logo_app_bar.dart b/lib/views/home/widgets/logo_app_bar.dart new file mode 100644 index 0000000..50f0bfc --- /dev/null +++ b/lib/views/home/widgets/logo_app_bar.dart @@ -0,0 +1,26 @@ +import 'package:didvan/views/widgets/logos/didvan_vertical_logo.dart'; +import 'package:flutter/material.dart'; + +class LogoAppBar extends StatelessWidget { + final String? type; + final bool hasExtraPadding; + const LogoAppBar({Key? key, this.hasExtraPadding = true, this.type}) + : super(key: key); + + @override + Widget build(BuildContext context) { + final MediaQueryData d = MediaQuery.of(context); + final double extraPadding = hasExtraPadding ? 0 : 16; + return Container( + margin: EdgeInsets.only(top: d.padding.top), + padding: EdgeInsets.only( + left: (type == null ? 140 : 0) - extraPadding, + top: 16 - extraPadding, + bottom: 16 - extraPadding, + right: 16 - extraPadding, + ), + alignment: Alignment.centerRight, + child: DidvanHorizontalLogo(type: type), + ); + } +} diff --git a/lib/views/home/widgets/menu_item.dart b/lib/views/home/widgets/menu_item.dart new file mode 100644 index 0000000..d0612c7 --- /dev/null +++ b/lib/views/home/widgets/menu_item.dart @@ -0,0 +1,73 @@ +import 'package:didvan/config/theme_data.dart'; +import 'package:didvan/constants/app_icons.dart'; +import 'package:didvan/views/widgets/didvan/text.dart'; +import 'package:flutter/material.dart'; + +class MenuItem extends StatelessWidget { + final String? title; + final Widget? titleWidget; + final IconData? icon; + final double iconSize; + final String? suffix; + final VoidCallback onTap; + final Widget? trailing; + final Color? color; + MenuItem({ + Key? key, + required this.onTap, + this.title, + this.icon, + this.suffix, + this.color, + this.trailing, + this.titleWidget, + this.iconSize = 18, + }) : super(key: key) { + if (title == null && titleWidget == null) { + throw Exception; + } + } + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: onTap, + child: Container( + color: Colors.transparent, + child: Row( + children: [ + if (icon != null) + Icon( + icon, + size: iconSize, + color: color ?? Theme.of(context).colorScheme.title, + ), + if (icon != null) const SizedBox(width: 4), + if (titleWidget != null) titleWidget!, + if (titleWidget == null) + DidvanText( + title!, + color: color ?? Theme.of(context).colorScheme.title, + ), + const Spacer(), + if (suffix != null) + DidvanText( + suffix!, + style: Theme.of(context) + .textTheme + .subtitle2! + .copyWith(fontWeight: FontWeight.w400), + color: Theme.of(context).colorScheme.primary, + ), + trailing ?? + Icon( + DidvanIcons.angle_left_regular, + size: 18, + color: color ?? Theme.of(context).colorScheme.title, + ), + ], + ), + ), + ); + } +} diff --git a/lib/views/home/widgets/multitype_overview.dart b/lib/views/home/widgets/multitype_overview.dart new file mode 100644 index 0000000..0076a05 --- /dev/null +++ b/lib/views/home/widgets/multitype_overview.dart @@ -0,0 +1,123 @@ +import 'package:didvan/config/theme_data.dart'; +import 'package:didvan/constants/app_icons.dart'; +import 'package:didvan/models/overview_data.dart'; +import 'package:didvan/models/requests/news.dart'; +import 'package:didvan/models/requests/radar.dart'; +import 'package:didvan/routes/routes.dart'; +import 'package:didvan/views/widgets/didvan/card.dart'; +import 'package:didvan/views/widgets/didvan/text.dart'; +import 'package:didvan/views/widgets/shimmer_placeholder.dart'; +import 'package:didvan/views/widgets/skeleton_image.dart'; +import 'package:flutter/material.dart'; +import 'package:persian_number_utility/persian_number_utility.dart'; + +class MultitypeOverview extends StatelessWidget { + final OverviewData item; + final bool hasUnmarkConfirmation; + final void Function(int id, bool value) onMarkChanged; + + const MultitypeOverview({ + Key? key, + required this.item, + required this.onMarkChanged, + this.hasUnmarkConfirmation = false, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return DidvanCard( + onTap: () => Navigator.of(context).pushNamed( + item.type == 'radar' ? Routes.radarDetails : Routes.newsDetails, + arguments: { + 'onMarkChanged': onMarkChanged, + 'id': item.id, + 'args': item.type == 'radar' + ? const RadarRequestArgs(page: 0) + : const NewsRequestArgs(page: 0), + 'hasUnmarkConfirmation': hasUnmarkConfirmation, + }, + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Stack( + children: [ + SkeletonImage(imageUrl: item.image, height: 80, width: 80), + Container( + padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 8), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.secondary, + borderRadius: const BorderRadius.horizontal( + left: Radius.circular(10), + ), + ), + child: Icon( + item.type == 'radar' + ? DidvanIcons.radar_light + : DidvanIcons.news_light, + color: Theme.of(context).colorScheme.white, + size: 18, + ), + ), + ], + ), + const SizedBox(width: 8), + Expanded( + child: SizedBox( + height: 80, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + DidvanText( + item.title, + style: Theme.of(context).textTheme.bodyText1, + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + Row( + children: [ + const Icon( + DidvanIcons.calendar_day_light, + size: 18, + ), + const SizedBox(width: 4), + DidvanText( + DateTime.parse(item.createdAt).toPersianDateStr(), + style: Theme.of(context).textTheme.overline, + ), + // DidvanText('text'), + ], + ), + ], + ), + ), + ), + ], + ), + ); + } + + static Widget get placeholder => DidvanCard( + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const ShimmerPlaceholder(height: 80, width: 80), + const SizedBox(width: 8), + SizedBox( + height: 80, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: const [ + ShimmerPlaceholder(height: 18, width: 200), + SizedBox(height: 8), + ShimmerPlaceholder(height: 18, width: 100), + Spacer(), + ShimmerPlaceholder(height: 14, width: 80), + ], + ), + ), + ], + ), + ); +} diff --git a/lib/views/home/widgets/news_overview.dart b/lib/views/home/widgets/news_overview.dart new file mode 100644 index 0000000..e998acf --- /dev/null +++ b/lib/views/home/widgets/news_overview.dart @@ -0,0 +1,138 @@ +import 'package:didvan/models/overview_data.dart'; +import 'package:didvan/models/requests/news.dart'; +import 'package:didvan/routes/routes.dart'; +import 'package:didvan/utils/date_time.dart'; +import 'package:didvan/views/home/widgets/bookmark_button.dart'; +import 'package:didvan/views/widgets/didvan/card.dart'; +import 'package:didvan/views/widgets/didvan/divider.dart'; +import 'package:didvan/views/widgets/didvan/text.dart'; +import 'package:didvan/views/widgets/shimmer_placeholder.dart'; +import 'package:didvan/views/widgets/skeleton_image.dart'; +import 'package:flutter/material.dart'; + +class NewsOverview extends StatelessWidget { + final OverviewData news; + final NewsRequestArgs? newsRequestArgs; + final void Function(int id, bool value) onMarkChanged; + final bool hasUnmarkConfirmation; + const NewsOverview({ + Key? key, + required this.news, + required this.onMarkChanged, + this.newsRequestArgs, + this.hasUnmarkConfirmation = false, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return DidvanCard( + onTap: () => Navigator.of(context).pushNamed( + Routes.newsDetails, + arguments: { + 'onMarkChanged': onMarkChanged, + 'id': news.id, + 'args': newsRequestArgs, + 'hasUnmarkConfirmation': hasUnmarkConfirmation, + }, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + SkeletonImage( + imageUrl: news.image, + width: 64, + height: 64, + ), + const SizedBox(width: 8), + Expanded( + child: SizedBox( + height: 64, + child: DidvanText( + news.title, + style: Theme.of(context).textTheme.bodyText1, + ), + ), + ), + ], + ), + const SizedBox(height: 8), + DidvanText( + news.description, + maxLines: 3, + ), + const DidvanDivider(verticalPadding: 8), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + DidvanText( + news.reference!, + style: Theme.of(context).textTheme.caption, + ), + DidvanText( + ' - ' + DateTimeUtils.momentGenerator(news.createdAt), + style: Theme.of(context).textTheme.caption, + ), + ], + ), + BookmarkButton( + value: news.marked, + onMarkChanged: (value) => onMarkChanged(news.id, value), + askForConfirmation: hasUnmarkConfirmation, + ), + ], + ), + ], + ), + ); + } + + static Widget get placeholder => DidvanCard( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const ShimmerPlaceholder(height: 64, width: 64), + const SizedBox(width: 8), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: const [ + ShimmerPlaceholder(height: 18, width: 200), + SizedBox(height: 8), + ShimmerPlaceholder(height: 18, width: 100), + ], + ), + ], + ), + const SizedBox(height: 12), + const ShimmerPlaceholder( + height: 16, + width: double.infinity, + ), + const SizedBox(height: 8), + const ShimmerPlaceholder( + height: 16, + width: double.infinity, + ), + const SizedBox(height: 8), + const ShimmerPlaceholder( + height: 16, + width: 100, + ), + const DidvanDivider(verticalPadding: 8), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: const [ + ShimmerPlaceholder(height: 12, width: 150), + ShimmerPlaceholder(height: 24, width: 24), + ], + ), + ], + ), + ); +} diff --git a/lib/views/home/widgets/player_controller_button.dart b/lib/views/home/widgets/player_controller_button.dart new file mode 100644 index 0000000..7e2cc14 --- /dev/null +++ b/lib/views/home/widgets/player_controller_button.dart @@ -0,0 +1,34 @@ +import 'dart:io'; + +import 'package:didvan/config/theme_data.dart'; +import 'package:didvan/constants/app_icons.dart'; +import 'package:didvan/services/media/media.dart'; +import 'package:didvan/views/widgets/didvan/icon_button.dart'; +import 'package:flutter/material.dart'; + +class AudioControllerButton extends StatelessWidget { + final String? audioUrl; + final File? audioFile; + + const AudioControllerButton({Key? key, this.audioUrl, this.audioFile}) + : super(key: key); + + bool get _nowPlaying => + MediaService.audioPlayerTag == audioUrl || + audioFile != null && MediaService.audioPlayerTag == audioFile!.path; + + @override + Widget build(BuildContext context) { + return DidvanIconButton( + icon: MediaService.audioPlayer.playing == true && _nowPlaying + ? DidvanIcons.pause_circle_solid + : DidvanIcons.play_circle_solid, + color: Theme.of(context).colorScheme.focusedBorder, + onPressed: () { + MediaService.handleAudioPlayback( + audioSource: audioFile ?? audioUrl, + ); + }, + ); + } +} diff --git a/lib/views/home/widgets/podcast_overview.dart b/lib/views/home/widgets/podcast_overview.dart new file mode 100644 index 0000000..fa626fb --- /dev/null +++ b/lib/views/home/widgets/podcast_overview.dart @@ -0,0 +1,138 @@ +import 'package:didvan/models/overview_data.dart'; +import 'package:didvan/models/requests/news.dart'; +import 'package:didvan/routes/routes.dart'; +import 'package:didvan/utils/date_time.dart'; +import 'package:didvan/views/home/widgets/bookmark_button.dart'; +import 'package:didvan/views/widgets/didvan/card.dart'; +import 'package:didvan/views/widgets/didvan/divider.dart'; +import 'package:didvan/views/widgets/didvan/text.dart'; +import 'package:didvan/views/widgets/shimmer_placeholder.dart'; +import 'package:didvan/views/widgets/skeleton_image.dart'; +import 'package:flutter/material.dart'; + +class PodcastOverview extends StatelessWidget { + final OverviewData news; + final NewsRequestArgs? newsRequestArgs; + final void Function(int id, bool value) onMarkChanged; + final bool hasUnmarkConfirmation; + const PodcastOverview({ + Key? key, + required this.news, + required this.onMarkChanged, + this.newsRequestArgs, + this.hasUnmarkConfirmation = false, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return DidvanCard( + onTap: () => Navigator.of(context).pushNamed( + Routes.newsDetails, + arguments: { + 'onMarkChanged': onMarkChanged, + 'id': news.id, + 'args': newsRequestArgs, + 'hasUnmarkConfirmation': hasUnmarkConfirmation, + }, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + SkeletonImage( + imageUrl: news.image, + width: 64, + height: 64, + ), + const SizedBox(width: 8), + Expanded( + child: SizedBox( + height: 64, + child: DidvanText( + news.title, + style: Theme.of(context).textTheme.bodyText1, + ), + ), + ), + ], + ), + const SizedBox(height: 8), + DidvanText( + news.description, + maxLines: 3, + ), + const DidvanDivider(verticalPadding: 8), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + DidvanText( + news.reference!, + style: Theme.of(context).textTheme.caption, + ), + DidvanText( + ' - ' + DateTimeUtils.momentGenerator(news.createdAt), + style: Theme.of(context).textTheme.caption, + ), + ], + ), + BookmarkButton( + value: news.marked, + onMarkChanged: (value) => onMarkChanged(news.id, value), + askForConfirmation: hasUnmarkConfirmation, + ), + ], + ), + ], + ), + ); + } + + static Widget get placeholder => DidvanCard( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const ShimmerPlaceholder(height: 64, width: 64), + const SizedBox(width: 8), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: const [ + ShimmerPlaceholder(height: 18, width: 200), + SizedBox(height: 8), + ShimmerPlaceholder(height: 18, width: 100), + ], + ), + ], + ), + const SizedBox(height: 12), + const ShimmerPlaceholder( + height: 16, + width: double.infinity, + ), + const SizedBox(height: 8), + const ShimmerPlaceholder( + height: 16, + width: double.infinity, + ), + const SizedBox(height: 8), + const ShimmerPlaceholder( + height: 16, + width: 100, + ), + const DidvanDivider(verticalPadding: 8), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: const [ + ShimmerPlaceholder(height: 12, width: 150), + ShimmerPlaceholder(height: 24, width: 24), + ], + ), + ], + ), + ); +} diff --git a/lib/views/home/widgets/radar_overview.dart b/lib/views/home/widgets/radar_overview.dart new file mode 100644 index 0000000..5616994 --- /dev/null +++ b/lib/views/home/widgets/radar_overview.dart @@ -0,0 +1,197 @@ +import 'package:didvan/config/theme_data.dart'; +import 'package:didvan/constants/app_icons.dart'; +import 'package:didvan/models/overview_data.dart'; +import 'package:didvan/models/requests/radar.dart'; +import 'package:didvan/routes/routes.dart'; +import 'package:didvan/utils/date_time.dart'; +import 'package:didvan/views/home/widgets/bookmark_button.dart'; +import 'package:didvan/views/widgets/didvan/card.dart'; +import 'package:didvan/views/widgets/didvan/divider.dart'; +import 'package:didvan/views/widgets/didvan/icon_button.dart'; +import 'package:didvan/views/widgets/didvan/text.dart'; +import 'package:didvan/views/widgets/shimmer_placeholder.dart'; +import 'package:didvan/views/widgets/skeleton_image.dart'; +import 'package:flutter/material.dart'; + +class RadarOverview extends StatelessWidget { + final OverviewData radar; + final void Function(int id, int count) onCommentsChanged; + final void Function(int id, bool value) onMarkChanged; + final bool hasUnmarkConfirmation; + final RadarRequestArgs? radarRequestArgs; + const RadarOverview({ + Key? key, + required this.radar, + required this.onCommentsChanged, + required this.onMarkChanged, + this.radarRequestArgs, + this.hasUnmarkConfirmation = false, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return DidvanCard( + onTap: () => Navigator.of(context).pushNamed( + Routes.radarDetails, + arguments: { + 'onMarkChanged': onMarkChanged, + 'onCommentsChanged': onCommentsChanged, + 'id': radar.id, + 'args': radarRequestArgs, + 'hasUnmarkConfirmation': hasUnmarkConfirmation, + }, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + DidvanText( + radar.title, + fontWeight: FontWeight.w600, + ), + const SizedBox(height: 8), + Stack( + children: [ + SkeletonImage( + imageUrl: radar.image, + aspectRatio: 16 / 9, + ), + if (radar.forManagers) + Positioned( + top: 0, + right: 0, + child: Container( + padding: const EdgeInsets.all(4), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.secondary, + borderRadius: const BorderRadius.only( + bottomLeft: Radius.circular(8), + topRight: Radius.circular(8), + ), + ), + child: DidvanText( + 'برای مدیران', + style: Theme.of(context).textTheme.overline, + color: Theme.of(context).colorScheme.white, + ), + ), + ), + ], + ), + const SizedBox(height: 8), + Row( + children: [ + DidvanText( + radar.categories!.first.label, + style: Theme.of(context).textTheme.overline, + color: Theme.of(context).colorScheme.caption, + ), + const Spacer(), + DidvanText( + '${DateTimeUtils.momentGenerator(radar.createdAt)} | خواندن در ${radar.timeToRead} دقیقه', + style: Theme.of(context).textTheme.overline, + color: Theme.of(context).colorScheme.caption, + ), + ], + ), + const SizedBox(height: 8), + DidvanText( + radar.description, + maxLines: 3, + ), + const DidvanDivider(), + Row( + children: [ + BookmarkButton( + value: radar.marked, + onMarkChanged: (value) => onMarkChanged(radar.id, value), + askForConfirmation: hasUnmarkConfirmation, + ), + const Spacer(), + if (radar.comments != 0) DidvanText(radar.comments.toString()), + const SizedBox(width: 4), + DidvanIconButton( + icon: DidvanIcons.chats_regular, + gestureSize: 24, + onPressed: () => Navigator.of(context).pushNamed( + Routes.comments, + arguments: { + 'isRadar': true, + 'title': radar.title, + 'id': radar.id, + 'onCommentsChanged': (count) => + onCommentsChanged(radar.id, count) + }, + ), + ), + const SizedBox(width: 16), + // const DidvanText('10'), + // const SizedBox(width: 4), + // const Icon(DidvanIcons.evaluation_regular), + ], + ), + ], + ), + ); + } + + static Widget get placeholder => DidvanCard( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const ShimmerPlaceholder(height: 16), + const SizedBox(height: 8), + const ShimmerPlaceholder( + width: 200, + height: 16, + ), + const SizedBox(height: 16), + const AspectRatio(aspectRatio: 16 / 9, child: ShimmerPlaceholder()), + const SizedBox(height: 8), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: const [ + ShimmerPlaceholder( + height: 12, + width: 70, + ), + ShimmerPlaceholder( + height: 12, + width: 70, + ), + ], + ), + const SizedBox(height: 8), + const ShimmerPlaceholder( + height: 16, + ), + const SizedBox(height: 8), + const ShimmerPlaceholder( + height: 16, + ), + const SizedBox(height: 8), + const ShimmerPlaceholder( + height: 16, + ), + const DidvanDivider(), + Row( + children: const [ + ShimmerPlaceholder( + height: 32, + width: 32, + ), + Spacer(), + ShimmerPlaceholder( + height: 32, + width: 32, + ), + SizedBox(width: 16), + ShimmerPlaceholder( + height: 32, + width: 32, + ), + ], + ), + ], + ), + ); +} diff --git a/lib/views/home/widgets/search_field.dart b/lib/views/home/widgets/search_field.dart new file mode 100644 index 0000000..dea0aa1 --- /dev/null +++ b/lib/views/home/widgets/search_field.dart @@ -0,0 +1,130 @@ +import 'package:didvan/config/theme_data.dart'; +import 'package:didvan/constants/app_icons.dart'; +import 'package:didvan/views/widgets/didvan/icon_button.dart'; +import 'package:flutter/material.dart'; + +class SearchField extends StatefulWidget { + final String title; + final FocusNode focusNode; + final bool? isFiltered; + final void Function(String value) onChanged; + final VoidCallback? onFilterButtonPressed; + + const SearchField({ + Key? key, + required this.title, + required this.onChanged, + required this.focusNode, + this.onFilterButtonPressed, + this.isFiltered, + }) : super(key: key); + + @override + State createState() => _SearchFieldState(); +} + +class _SearchFieldState extends State { + @override + void initState() { + widget.focusNode.addListener(() { + setState(() {}); + }); + super.initState(); + } + + @override + Widget build(BuildContext context) { + return SizedBox( + height: 40, + child: Row( + children: [ + Expanded( + child: Container( + decoration: BoxDecoration( + color: _fillColor(), + ), + child: TextFormField( + focusNode: widget.focusNode, + style: Theme.of(context).textTheme.bodyText2, + textAlignVertical: TextAlignVertical.center, + onChanged: widget.onChanged, + keyboardType: TextInputType.text, + textInputAction: TextInputAction.search, + decoration: InputDecoration( + focusedBorder: OutlineInputBorder( + borderRadius: const BorderRadius.all( + Radius.circular(4), + ), + borderSide: BorderSide( + color: Theme.of(context).colorScheme.primary, + ), + ), + prefixIcon: Icon( + DidvanIcons.search_regular, + color: Theme.of(context).colorScheme.text, + ), + prefixIconColor: Theme.of(context).colorScheme.inputText, + enabledBorder: OutlineInputBorder( + borderRadius: const BorderRadius.all( + Radius.circular(4), + ), + borderSide: BorderSide( + color: Theme.of(context).colorScheme.border, + ), + ), + fillColor: Colors.red, + contentPadding: const EdgeInsets.only( + left: 12, + right: 12, + ), + border: InputBorder.none, + hintText: 'جستجو مطلب در ${widget.title}', + hintStyle: TextStyle( + color: Theme.of(context).colorScheme.disabledText, + ), + ), + ), + ), + ), + if (widget.onFilterButtonPressed != null) const SizedBox(width: 8), + if (widget.onFilterButtonPressed != null) + Stack( + children: [ + DidvanIconButton( + onPressed: widget.onFilterButtonPressed!, + icon: widget.isFiltered! + ? DidvanIcons.filter_solid + : DidvanIcons.filter_regular, + size: 32, + gestureSize: 32, + ), + if (widget.isFiltered!) + Positioned( + child: Container( + width: 10, + height: 10, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: Theme.of(context).colorScheme.secondary, + ), + ), + ), + ], + ), + ], + ), + ); + } + + Color _fillColor() { + if (widget.focusNode.hasFocus) { + return Theme.of(context).colorScheme.surface; + } + return Theme.of(context).colorScheme.surface; + } + + @override + void dispose() { + super.dispose(); + } +} diff --git a/lib/views/home/widgets/tag_item.dart b/lib/views/home/widgets/tag_item.dart new file mode 100644 index 0000000..1265361 --- /dev/null +++ b/lib/views/home/widgets/tag_item.dart @@ -0,0 +1,52 @@ +import 'package:didvan/config/design_config.dart'; +import 'package:didvan/config/theme_data.dart'; +import 'package:didvan/constants/app_icons.dart'; +import 'package:didvan/models/tag.dart'; +import 'package:didvan/routes/routes.dart'; +import 'package:didvan/views/widgets/didvan/text.dart'; +import 'package:didvan/views/widgets/ink_wrapper.dart'; +import 'package:flutter/material.dart'; + +class TagItem extends StatelessWidget { + final Tag tag; + + const TagItem({ + Key? key, + required this.tag, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return InkWrapper( + borderRadius: DesignConfig.lowBorderRadius, + onPressed: () => + Navigator.of(context).pushNamed(Routes.hashtag, arguments: tag), + child: Container( + padding: const EdgeInsets.symmetric( + vertical: 4, + horizontal: 8, + ), + decoration: BoxDecoration( + borderRadius: DesignConfig.lowBorderRadius, + border: Border.all( + color: Theme.of(context).colorScheme.focusedBorder, + ), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + DidvanIcons.hashtag_regular, + color: Theme.of(context).colorScheme.focusedBorder, + ), + DidvanText( + tag.label, + color: Theme.of(context).colorScheme.focusedBorder, + style: Theme.of(context).textTheme.bodyText1, + ), + ], + ), + ), + ); + } +} diff --git a/lib/views/splash/splash.dart b/lib/views/splash/splash.dart new file mode 100644 index 0000000..c0d05cd --- /dev/null +++ b/lib/views/splash/splash.dart @@ -0,0 +1,126 @@ +import 'dart:developer'; + +import 'package:didvan/config/design_config.dart'; +import 'package:didvan/main.dart'; +import 'package:didvan/providers/server_data_provider.dart'; +import 'package:didvan/providers/theme_provider.dart'; +import 'package:didvan/providers/user_provider.dart'; +import 'package:didvan/routes/routes.dart'; +import 'package:didvan/services/app_initalizer.dart'; +import 'package:didvan/services/network/request.dart'; +import 'package:didvan/services/storage/storage.dart'; +import 'package:didvan/utils/action_sheet.dart'; +import 'package:didvan/views/widgets/didvan/button.dart'; +import 'package:didvan/views/widgets/logos/didvan_horizontal_logo.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_spinkit/flutter_spinkit.dart'; +import 'package:provider/provider.dart'; +import 'package:universal_html/html.dart' as html; + +class Splash extends StatefulWidget { + const Splash({Key? key}) : super(key: key); + + @override + _SplashState createState() => _SplashState(); +} + +class _SplashState extends State { + bool _isGettingThemeData = true; + bool _errorOccured = false; + + @override + void initState() { + _initialize(); + super.initState(); + } + + @override + Widget build(BuildContext context) { + if (_isGettingThemeData) { + return Container( + color: Colors.black, + height: double.infinity, + width: double.infinity, + ); + } + return AnnotatedRegion( + value: DesignConfig.systemUiOverlayStyle.copyWith( + systemNavigationBarColor: Theme.of(context).colorScheme.background, + ), + child: Scaffold( + body: Container( + alignment: Alignment.center, + padding: const EdgeInsets.all(60), + color: Theme.of(context).colorScheme.background, + child: Column( + children: [ + const SizedBox(height: 70), + const Expanded( + child: DidvanVerticalLogo(), + ), + if (!_errorOccured) + SpinKitSpinningLines( + color: Theme.of(context).colorScheme.primary, + ), + if (_errorOccured) const SizedBox(height: 30), + if (_errorOccured) + DidvanButton( + width: 120, + height: 40, + title: 'تلاش مجدد', + onPressed: () { + setState(() { + _errorOccured = false; + }); + _initialize(); + }, + ), + ], + ), + ), + ), + ); + } + + Future _initialize() async { + try { + ActionSheetUtils.context = navigatorKey.currentContext!; + if (kIsWeb) { + html.window.onBeforeUnload.listen((event) { + StorageService.webStorage + .removeWhere((key, value) => key == 'image-cache'); + }); + } + await AppInitializer.setupServices(); + final settingsData = await AppInitializer.initilizeSettings(); + final themeProvider = context.read(); + themeProvider.themeMode = settingsData.themeMode; + themeProvider.fontFamily = settingsData.fontFamily; + themeProvider.fontScale = settingsData.fontScale; + await Future.delayed( + const Duration(milliseconds: 200), + () => setState(() { + _isGettingThemeData = false; + }), + ); + final userProvider = context.read(); + final String? token = await userProvider.setAndGetToken(); + if (token != null) { + log(token); + RequestService.token = token; + await userProvider.getUserInfo(); + await ServerDataProvider.getData(); + } + Navigator.of(context).pushReplacementNamed( + token == null ? Routes.authenticaion : Routes.home, + arguments: token == null ? false : null, + ); + } catch (e) { + setState(() { + _errorOccured = true; + }); + } + } +} diff --git a/lib/views/widgets/animated_visibility.dart b/lib/views/widgets/animated_visibility.dart new file mode 100644 index 0000000..0c058e8 --- /dev/null +++ b/lib/views/widgets/animated_visibility.dart @@ -0,0 +1,93 @@ +library animated_visibility; + +import 'package:flutter/material.dart'; + +enum FadeMode { + vertical, + horizontal, + both, +} + +class AnimatedVisibility extends StatefulWidget { + final bool isVisible; + final Duration duration; + final Widget child; + final Curve curve; + final FadeMode fadeMode; + + const AnimatedVisibility({ + Key? key, + required this.isVisible, + required this.duration, + required this.child, + this.fadeMode = FadeMode.both, + this.curve = Curves.easeIn, + }) : super(key: key); + + @override + _AnimatedVisibilityState createState() => _AnimatedVisibilityState(); +} + +class _AnimatedVisibilityState extends State + with SingleTickerProviderStateMixin { + late final AnimationController _sizeController; + late final Animation _animation; + + @override + void initState() { + super.initState(); + _setupAnimation(); + _runSizeCheck(true); + } + + void _setupAnimation() { + _sizeController = + AnimationController(vsync: this, duration: widget.duration); + _animation = CurvedAnimation( + parent: _sizeController, + curve: widget.curve, + ); + } + + @override + void didUpdateWidget(covariant AnimatedVisibility oldWidget) { + _runSizeCheck(false); + super.didUpdateWidget(oldWidget); + } + + void _runSizeCheck(bool isInit) async { + if (widget.isVisible) { + if (isInit) { + _sizeController.value = 1; + } + _sizeController.forward(); + } else { + _sizeController.reverse(); + } + } + + @override + void dispose() { + _sizeController.dispose(); + super.dispose(); + } + + bool get _isVertical => + widget.fadeMode == FadeMode.vertical || widget.fadeMode == FadeMode.both; + bool get _isHorizontal => + widget.fadeMode == FadeMode.horizontal || + widget.fadeMode == FadeMode.both; + + @override + Widget build(BuildContext context) { + return AnimatedBuilder( + animation: _animation, + child: widget.child, + builder: (context, child) => Align( + heightFactor: _isVertical ? _animation.value : null, + widthFactor: _isHorizontal ? _animation.value : null, + child: Opacity(opacity: _animation.value, child: child), + ), + ); + } +} diff --git a/lib/views/widgets/didvan/app_bar.dart b/lib/views/widgets/didvan/app_bar.dart new file mode 100644 index 0000000..1de49fe --- /dev/null +++ b/lib/views/widgets/didvan/app_bar.dart @@ -0,0 +1,74 @@ +import 'package:didvan/config/theme_data.dart'; +import 'package:didvan/constants/app_icons.dart'; +import 'package:didvan/models/view/app_bar_data.dart'; +import 'package:didvan/views/widgets/didvan/text.dart'; +import 'package:flutter/material.dart'; + +class DidvanAppBar extends StatelessWidget { + final AppBarData appBarData; + final bool hasBorder; + final Color? backgroundColor; + const DidvanAppBar({ + Key? key, + required this.appBarData, + this.hasBorder = false, + this.backgroundColor = Colors.transparent, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Container( + height: kToolbarHeight + MediaQuery.of(context).padding.top, + width: MediaQuery.of(context).size.width, + padding: const EdgeInsets.only(right: 4, left: 20), + decoration: BoxDecoration( + color: backgroundColor, + border: hasBorder + ? Border( + bottom: BorderSide( + color: Theme.of(context).colorScheme.cardBorder, + ), + ) + : null, + ), + child: Row( + children: [ + IconButton( + onPressed: () => Navigator.of(context).pop(), + color: Theme.of(context).colorScheme.title, + icon: const Icon( + DidvanIcons.back_regular, + ), + ), + const SizedBox(width: 16), + Expanded( + child: Align( + alignment: Alignment.centerRight, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + if (appBarData.title != null) + DidvanText( + appBarData.title!, + style: Theme.of(context).textTheme.headline3, + color: Theme.of(context).colorScheme.title, + ), + if (appBarData.subtitle != null) + DidvanText( + appBarData.subtitle!, + maxLines: 1, + overflow: TextOverflow.clip, + style: Theme.of(context).textTheme.overline, + color: Theme.of(context).colorScheme.caption, + ), + ], + ), + ), + ), + if (appBarData.trailing != null) appBarData.trailing!, + ], + ), + ); + } +} diff --git a/lib/views/widgets/didvan/badge.dart b/lib/views/widgets/didvan/badge.dart new file mode 100644 index 0000000..c88c0fd --- /dev/null +++ b/lib/views/widgets/didvan/badge.dart @@ -0,0 +1,35 @@ +import 'package:didvan/config/theme_data.dart'; +import 'package:didvan/views/widgets/didvan/text.dart'; +import 'package:flutter/material.dart'; + +class DidvanBadge extends StatelessWidget { + final String text; + const DidvanBadge({ + Key? key, + required this.text, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Container( + height: 24, + width: 24, + padding: const EdgeInsets.only(top: 2), + alignment: Alignment.center, + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.secondary, + shape: BoxShape.circle, + ), + child: FittedBox( + fit: BoxFit.scaleDown, + child: DidvanText( + text, + isEnglishFont: true, + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.overline, + color: Theme.of(context).colorScheme.white, + ), + ), + ); + } +} diff --git a/lib/views/widgets/didvan/button.dart b/lib/views/widgets/didvan/button.dart new file mode 100644 index 0000000..de7498b --- /dev/null +++ b/lib/views/widgets/didvan/button.dart @@ -0,0 +1,80 @@ +import 'package:didvan/config/design_config.dart'; +import 'package:didvan/config/theme_data.dart'; +import 'package:didvan/models/enums.dart'; +import 'package:didvan/views/widgets/didvan/text.dart'; +import 'package:flutter/material.dart'; + +class DidvanButton extends StatelessWidget { + final VoidCallback? onPressed; + final String? title; + final double? width; + final ButtonStyleMode style; + final bool enabled; + final double? height; + const DidvanButton({ + Key? key, + this.onPressed, + this.title, + this.style = ButtonStyleMode.primary, + this.enabled = true, + this.width, + this.height = 48, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + Color foregroundColor = Colors.black; + Color backgroundColor = Colors.black; + switch (style) { + case ButtonStyleMode.primary: + backgroundColor = Theme.of(context).colorScheme.primary; + foregroundColor = Theme.of(context).colorScheme.white; + break; + case ButtonStyleMode.secondary: + backgroundColor = Theme.of(context).colorScheme.secondCTA; + foregroundColor = Theme.of(context).colorScheme.text; + break; + case ButtonStyleMode.flat: + if (DesignConfig.isDark) { + backgroundColor = Theme.of(context).colorScheme.surface; + foregroundColor = Theme.of(context).colorScheme.white; + } else { + backgroundColor = Theme.of(context).colorScheme.surface; + foregroundColor = Theme.of(context).colorScheme.primary; + } + break; + default: + } + return SizedBox( + height: height, + width: width ?? double.infinity, + child: MaterialButton( + shape: RoundedRectangleBorder( + borderRadius: DesignConfig.lowBorderRadius, + side: style == ButtonStyleMode.flat + ? BorderSide(color: foregroundColor) + : BorderSide.none, + ), + color: backgroundColor, + disabledColor: Theme.of(context).colorScheme.disabledBackground, + onPressed: !enabled + ? null + : () { + FocusScope.of(context).unfocus(); + onPressed?.call(); + }, + child: _childBuilder(foregroundColor, context), + ), + ); + } + + Widget? _childBuilder(Color color, context) { + if (title != null) { + return DidvanText( + title!, + color: enabled ? color : Theme.of(context).colorScheme.disabledText, + ); + } + return null; + } +} diff --git a/lib/views/widgets/didvan/card.dart b/lib/views/widgets/didvan/card.dart new file mode 100644 index 0000000..5be4aa4 --- /dev/null +++ b/lib/views/widgets/didvan/card.dart @@ -0,0 +1,35 @@ +import 'package:didvan/config/design_config.dart'; +import 'package:flutter/material.dart'; + +class DidvanCard extends StatelessWidget { + final Widget? child; + final EdgeInsets? padding; + final EdgeInsets? margin; + final bool enableBorder; + final VoidCallback? onTap; + const DidvanCard({ + Key? key, + this.child, + this.padding = const EdgeInsets.all(16), + this.margin, + this.enableBorder = true, + this.onTap, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: onTap, + child: Container( + padding: padding, + margin: margin, + decoration: BoxDecoration( + borderRadius: DesignConfig.mediumBorderRadius, + color: Theme.of(context).colorScheme.surface, + border: enableBorder ? DesignConfig.cardBorder : null, + ), + child: child, + ), + ); + } +} diff --git a/lib/views/widgets/didvan/checkbox.dart b/lib/views/widgets/didvan/checkbox.dart new file mode 100644 index 0000000..dfeb454 --- /dev/null +++ b/lib/views/widgets/didvan/checkbox.dart @@ -0,0 +1,59 @@ +import 'package:didvan/views/widgets/didvan/text.dart'; +import 'package:flutter/material.dart'; + +class DidvanCheckbox extends StatefulWidget { + final String title; + final bool? value; + final void Function(bool value) onChanged; + const DidvanCheckbox({ + Key? key, + required this.title, + required this.value, + required this.onChanged, + }) : super(key: key); + + @override + State createState() => _DidvanCheckboxState(); +} + +class _DidvanCheckboxState extends State { + bool _value = false; + + @override + void initState() { + if (widget.value != null) { + _value = widget.value!; + } + super.initState(); + } + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: () { + setState(() { + _value = !_value; + }); + widget.onChanged(_value); + }, + child: Container( + color: Colors.transparent, + child: Row( + children: [ + Checkbox( + value: _value, + onChanged: (value) { + setState(() { + _value = value ?? false; + }); + widget.onChanged(_value); + }, + ), + const SizedBox(width: 8), + DidvanText(widget.title), + ], + ), + ), + ); + } +} diff --git a/lib/views/widgets/didvan/chip.dart b/lib/views/widgets/didvan/chip.dart new file mode 100644 index 0000000..a1a7e73 --- /dev/null +++ b/lib/views/widgets/didvan/chip.dart @@ -0,0 +1,35 @@ +import 'package:didvan/config/design_config.dart'; +import 'package:didvan/config/theme_data.dart'; +import 'package:didvan/views/widgets/didvan/text.dart'; +import 'package:didvan/views/widgets/ink_wrapper.dart'; +import 'package:flutter/material.dart'; + +class DidvanChip extends StatelessWidget { + final String label; + final VoidCallback? onTap; + const DidvanChip({ + Key? key, + required this.label, + this.onTap, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return InkWrapper( + borderRadius: DesignConfig.highBorderRadius, + onPressed: onTap, + child: Container( + padding: const EdgeInsets.symmetric(vertical: 2, horizontal: 8), + decoration: BoxDecoration( + borderRadius: DesignConfig.highBorderRadius, + color: Theme.of(context).colorScheme.primary, + ), + child: DidvanText( + label, + style: Theme.of(context).textTheme.caption, + color: Theme.of(context).colorScheme.white, + ), + ), + ); + } +} diff --git a/lib/views/widgets/didvan/divider.dart b/lib/views/widgets/didvan/divider.dart new file mode 100644 index 0000000..3b6b382 --- /dev/null +++ b/lib/views/widgets/didvan/divider.dart @@ -0,0 +1,18 @@ +import 'package:didvan/config/theme_data.dart'; +import 'package:flutter/material.dart'; + +class DidvanDivider extends StatelessWidget { + final double? verticalPadding; + + const DidvanDivider({Key? key, this.verticalPadding}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Container( + height: 1, + width: double.infinity, + margin: EdgeInsets.symmetric(vertical: verticalPadding ?? 16), + color: Theme.of(context).colorScheme.border, + ); + } +} diff --git a/lib/views/widgets/didvan/icon_button.dart b/lib/views/widgets/didvan/icon_button.dart new file mode 100644 index 0000000..3046757 --- /dev/null +++ b/lib/views/widgets/didvan/icon_button.dart @@ -0,0 +1,41 @@ +import 'package:didvan/views/widgets/ink_wrapper.dart'; +import 'package:flutter/material.dart'; + +class DidvanIconButton extends StatelessWidget { + final IconData icon; + final Color? color; + final Color? backgroundColor; + final double? size; + final double? gestureSize; + final VoidCallback onPressed; + const DidvanIconButton({ + Key? key, + required this.icon, + required this.onPressed, + this.color, + this.size, + this.gestureSize, + this.backgroundColor, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return InkWrapper( + onPressed: onPressed, + borderRadius: BorderRadius.circular(200), + child: Container( + decoration: BoxDecoration( + color: backgroundColor, + shape: BoxShape.circle, + ), + height: gestureSize ?? 48, + width: gestureSize ?? 48, + child: Icon( + icon, + size: size, + color: color, + ), + ), + ); + } +} diff --git a/lib/views/widgets/didvan/page_view.dart b/lib/views/widgets/didvan/page_view.dart new file mode 100644 index 0000000..f0a83d3 --- /dev/null +++ b/lib/views/widgets/didvan/page_view.dart @@ -0,0 +1,351 @@ +import 'package:carousel_slider/carousel_slider.dart'; +import 'package:didvan/config/design_config.dart'; +import 'package:didvan/config/theme_data.dart'; +import 'package:didvan/constants/app_icons.dart'; +import 'package:didvan/utils/date_time.dart'; +import 'package:didvan/views/home/widgets/multitype_overview.dart'; +import 'package:didvan/views/home/widgets/tag_item.dart'; +import 'package:didvan/views/widgets/animated_visibility.dart'; +import 'package:didvan/views/widgets/didvan/card.dart'; +import 'package:didvan/views/widgets/didvan/divider.dart'; +import 'package:didvan/views/widgets/didvan/text.dart'; +import 'package:didvan/views/widgets/ink_wrapper.dart'; +import 'package:didvan/views/widgets/item_title.dart'; +import 'package:didvan/views/widgets/skeleton_image.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_html/flutter_html.dart'; + +class DidvanPageView extends StatefulWidget { + final List items; + final int initialIndex; + final int currentIndex; + final bool isRadar; + final ScrollController scrollController; + + final void Function(int index) onPageChanged; + const DidvanPageView({ + Key? key, + required this.initialIndex, + required this.items, + required this.scrollController, + required this.onPageChanged, + required this.isRadar, + required this.currentIndex, + }) : super(key: key); + + @override + State createState() => _DidvanPageViewState(); +} + +class _DidvanPageViewState extends State { + @override + Widget build(BuildContext context) { + final double deviceTopPadding = MediaQuery.of(context).padding.top; + return Stack( + children: [ + Directionality( + textDirection: TextDirection.ltr, + child: CarouselSlider.builder( + itemCount: widget.items.length, + options: CarouselOptions( + onPageChanged: (index, reason) => widget.onPageChanged(index), + height: double.infinity, + initialPage: widget.initialIndex, + viewportFraction: 0.94, + enableInfiniteScroll: false, + ), + itemBuilder: (context, index, realIndex) => Directionality( + textDirection: TextDirection.rtl, + child: SizedBox( + height: MediaQuery.of(context).size.height, + child: SingleChildScrollView( + controller: index == widget.currentIndex + ? widget.scrollController + : null, + physics: const BouncingScrollPhysics(), + padding: EdgeInsets.only( + left: 4, + right: 4, + top: 16 + deviceTopPadding, + bottom: 92, + ), + child: DidvanCard( + padding: EdgeInsets.zero, + enableBorder: false, + child: Builder( + builder: (context) { + final item = widget.items[index]; + if (item == null) { + return const SizedBox(); + } + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SkeletonImage( + imageUrl: item.image, + aspectRatio: 16 / 9, + ), + const SizedBox(height: 20), + Padding( + padding: + const EdgeInsets.symmetric(horizontal: 16), + child: DidvanText( + item.title, + style: Theme.of(context).textTheme.subtitle1, + ), + ), + const SizedBox(height: 8), + Padding( + padding: + const EdgeInsets.symmetric(horizontal: 16), + child: _subtitle(item), + ), + for (var i = 0; i < item.contents.length; i++) + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 4, + ), + child: _contentBuilder(item, i), + ), + if (item.tags.isNotEmpty) + const SizedBox(height: 20), + Padding( + padding: + const EdgeInsets.symmetric(horizontal: 16), + child: Wrap( + spacing: 8, + runSpacing: 8, + children: [ + for (var i = 0; i < item.tags.length; i++) + TagItem(tag: item.tags[i]), + ], + ), + ), + const Padding( + padding: EdgeInsets.only( + left: 16, + right: 16, + top: 16, + ), + child: DidvanDivider(verticalPadding: 0), + ), + const Padding( + padding: EdgeInsets.symmetric(horizontal: 16.0), + child: ItemTitle(title: 'مطالب مشابه'), + ), + if (item.relatedContents.isEmpty) + for (var i = 0; i < 3; i++) + Padding( + padding: const EdgeInsets.only( + bottom: 8, + left: 16, + right: 16, + ), + child: MultitypeOverview.placeholder, + ), + for (var i = 0; + i < item.relatedContents.length; + i++) + Padding( + padding: const EdgeInsets.only( + bottom: 8, + left: 16, + right: 16, + ), + child: MultitypeOverview( + item: item.relatedContents[i], + onMarkChanged: (id, value) {}, + ), + ), + const SizedBox(height: 8), + ], + ); + }, + ), + ), + ), + ), + ), + ), + ), + Positioned( + child: _BackButton(scrollController: widget.scrollController), + right: 24, + top: 24 + deviceTopPadding, + ), + ], + ); + } + + Widget _contentBuilder(dynamic item, int index) { + final content = item.contents[index]; + if (content.text != null) { + return Html( + data: content.text, + style: { + '*': Style( + direction: TextDirection.rtl, + lineHeight: LineHeight.percent(135), + margin: EdgeInsets.zero, + padding: EdgeInsets.zero, + ), + }, + ); + } + + if (content.image != null) { + return Column( + children: [ + GestureDetector( + onTap: () { + showDialog( + context: context, + builder: (context) => Stack( + children: [ + Positioned.fill( + child: InteractiveViewer( + child: Center( + child: SkeletonImage( + width: MediaQuery.of(context).size.width, + imageUrl: content.largeImage ?? content.image, + ), + ), + ), + ), + const Positioned( + right: 24, + top: 24, + child: _BackButton(), + ), + ], + ), + ); + }, + child: SkeletonImage( + imageUrl: content.image!, + width: double.infinity, + ), + ), + if (content.caption != null) + Center( + child: DidvanText( + content.caption, + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.caption, + ), + ), + ], + ); + } + return const SizedBox(); + } + + Widget _subtitle(dynamic item) { + if (widget.isRadar) { + return Wrap( + crossAxisAlignment: WrapCrossAlignment.center, + children: [ + DidvanText( + 'رادار ', + style: Theme.of(context).textTheme.caption, + ), + for (var i = 0; i < item.categories.length; i++) + DidvanText( + item.categories[i].label + + '${i != item.categories.length - 1 ? '،' : ''} ', + style: Theme.of(context).textTheme.caption, + ), + DidvanText( + ' - ' + DateTimeUtils.momentGenerator(item.createdAt), + style: Theme.of(context).textTheme.caption, + ), + ], + ); + } else { + return Row( + children: [ + DidvanText( + item.reference, + style: Theme.of(context).textTheme.caption, + ), + DidvanText( + ' - ' + DateTimeUtils.momentGenerator(item.createdAt), + style: Theme.of(context).textTheme.caption, + ), + ], + ); + } + } +} + +class _BackButton extends StatefulWidget { + final ScrollController? scrollController; + const _BackButton({Key? key, this.scrollController}) : super(key: key); + + @override + __BackButtonState createState() => __BackButtonState(); +} + +class __BackButtonState extends State<_BackButton> { + bool _isVisible = false; + + @override + void didUpdateWidget(covariant _BackButton oldWidget) { + if (widget.scrollController != null) { + _isVisible = false; + _handleScroll(); + } + super.didUpdateWidget(oldWidget); + } + + @override + void initState() { + if (widget.scrollController != null) { + _handleScroll(); + } else { + _isVisible = true; + } + super.initState(); + } + + void _handleScroll() { + widget.scrollController!.addListener(() { + final position = widget.scrollController!.position.pixels; + final size = MediaQuery.of(context).size.height * 0.3; + if (position > size && _isVisible == false) { + setState(() { + _isVisible = true; + }); + } + if (position < size && _isVisible == true) { + setState(() { + _isVisible = false; + }); + } + }); + } + + @override + Widget build(BuildContext context) { + return AnimatedVisibility( + duration: DesignConfig.lowAnimationDuration, + isVisible: _isVisible, + child: InkWrapper( + borderRadius: DesignConfig.lowBorderRadius, + onPressed: Navigator.of(context).pop, + child: Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.splash, + border: Border.all(color: Theme.of(context).colorScheme.border), + borderRadius: DesignConfig.lowBorderRadius, + ), + child: const Icon( + DidvanIcons.back_regular, + size: 32, + ), + ), + ), + ); + } +} diff --git a/lib/views/widgets/didvan/radial_button.dart b/lib/views/widgets/didvan/radial_button.dart new file mode 100644 index 0000000..4d05007 --- /dev/null +++ b/lib/views/widgets/didvan/radial_button.dart @@ -0,0 +1,61 @@ +import 'package:didvan/config/theme_data.dart'; +import 'package:didvan/views/widgets/didvan/text.dart'; +import 'package:flutter/material.dart'; + +class DidvanRadialButton extends StatelessWidget { + final String title; + final VoidCallback onSelected; + final bool value; + final String? fontFamily; + const DidvanRadialButton({ + Key? key, + required this.title, + required this.onSelected, + required this.value, + this.fontFamily, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: value ? null : onSelected, + child: Container( + color: Colors.transparent, + child: Row( + children: [ + Container( + padding: const EdgeInsets.all(4), + height: 24, + width: 24, + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all( + color: value + ? Theme.of(context).colorScheme.secondary + : Theme.of(context).colorScheme.text, + ), + ), + child: Container( + width: double.infinity, + height: double.infinity, + decoration: BoxDecoration( + color: value + ? Theme.of(context).colorScheme.secondary + : Colors.transparent, + shape: BoxShape.circle, + ), + ), + ), + const SizedBox(width: 8), + DidvanText( + title, + style: Theme.of(context).textTheme.bodyText2!.copyWith( + fontFamily: fontFamily, + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/views/widgets/didvan/scaffold.dart b/lib/views/widgets/didvan/scaffold.dart new file mode 100644 index 0000000..db75ac9 --- /dev/null +++ b/lib/views/widgets/didvan/scaffold.dart @@ -0,0 +1,136 @@ +import 'package:didvan/models/view/app_bar_data.dart'; +import 'package:didvan/views/widgets/didvan/app_bar.dart'; +import 'package:flutter/material.dart'; + +class DidvanScaffold extends StatefulWidget { + final List? slivers; + final List? children; + final AppBarData appBarData; + final EdgeInsets padding; + final Color? backgroundColor; + final bool reverse; + + const DidvanScaffold({ + Key? key, + this.slivers, + required this.appBarData, + this.children, + this.padding = const EdgeInsets.symmetric(horizontal: 16), + this.backgroundColor, + this.reverse = false, + }) : super(key: key); + + @override + State createState() => _DidvanScaffoldState(); +} + +class _DidvanScaffoldState extends State { + final _scrollController = ScrollController(); + + @override + Widget build(BuildContext context) { + final double statusBarHeight = MediaQuery.of(context).padding.top; + return Scaffold( + backgroundColor: widget.backgroundColor, + body: Padding( + padding: EdgeInsets.only(top: statusBarHeight), + child: Stack( + children: [ + CustomScrollView( + controller: _scrollController, + reverse: widget.reverse, + slivers: [ + if (!widget.reverse) + SliverAppBar( + toolbarHeight: kToolbarHeight, + backgroundColor: widget.backgroundColor ?? + Theme.of(context).colorScheme.background, + automaticallyImplyLeading: false, + pinned: true, + flexibleSpace: DidvanAppBar(appBarData: widget.appBarData), + ), + if (!widget.reverse) + const SliverToBoxAdapter( + child: SizedBox(height: 16), + ), + if (widget.children != null) + SliverPadding( + padding: widget.padding, + sliver: SliverList( + delegate: SliverChildBuilderDelegate( + (context, index) => widget.children![index], + childCount: widget.children!.length, + ), + ), + ), + if (widget.slivers != null) + for (var i = 0; i < widget.slivers!.length; i++) + SliverPadding( + padding: widget.padding, + sliver: widget.slivers![i], + ), + if (widget.reverse) + SliverToBoxAdapter( + child: SizedBox( + height: kToolbarHeight + + MediaQuery.of(context).padding.top + + 12, + ), + ), + ], + ), + if (widget.reverse) + _AppBar( + appBarData: widget.appBarData, + scrollController: _scrollController, + ), + ], + ), + ), + ); + } +} + +class _AppBar extends StatefulWidget { + final AppBarData appBarData; + final ScrollController scrollController; + const _AppBar({ + Key? key, + required this.appBarData, + required this.scrollController, + }) : super(key: key); + + @override + __AppBarState createState() => __AppBarState(); +} + +class __AppBarState extends State<_AppBar> { + bool _isScrolled = false; + + @override + void initState() { + widget.scrollController.addListener(() { + final position = widget.scrollController.position.pixels; + if (position > 10 && _isScrolled == false) { + setState(() { + _isScrolled = true; + }); + } + if (position < 10 && _isScrolled == true) { + setState(() { + _isScrolled = false; + }); + } + }); + super.initState(); + } + + @override + Widget build(BuildContext context) { + return DidvanAppBar( + backgroundColor: Theme.of(context).colorScheme.surface, + appBarData: widget.appBarData, + hasBorder: _isScrolled, + ); + } +} diff --git a/lib/views/widgets/didvan/switch.dart b/lib/views/widgets/didvan/switch.dart new file mode 100644 index 0000000..558a2f7 --- /dev/null +++ b/lib/views/widgets/didvan/switch.dart @@ -0,0 +1,54 @@ +import 'package:didvan/views/home/widgets/menu_item.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; + +class DidvanSwitch extends StatefulWidget { + final bool value; + final String title; + final IconData? icon; + final void Function(bool value) onChanged; + const DidvanSwitch({ + Key? key, + required this.value, + required this.title, + this.icon, + required this.onChanged, + }) : super(key: key); + + @override + _DidvanSwitchState createState() => _DidvanSwitchState(); +} + +class _DidvanSwitchState extends State { + bool _value = false; + + @override + void initState() { + _value = widget.value; + super.initState(); + } + + @override + Widget build(BuildContext context) { + return MenuItem( + title: widget.title, + onTap: () { + setState( + () => _value = !_value, + ); + widget.onChanged(_value); + }, + icon: widget.icon, + trailing: CupertinoSwitch( + activeColor: Theme.of(context).colorScheme.primary, + value: _value, + onChanged: (value) { + setState(() { + _value = value; + }); + widget.onChanged(value); + }, + ), + ); + } +} diff --git a/lib/views/widgets/didvan/text.dart b/lib/views/widgets/didvan/text.dart new file mode 100644 index 0000000..1ea5ec8 --- /dev/null +++ b/lib/views/widgets/didvan/text.dart @@ -0,0 +1,45 @@ +import 'package:didvan/config/design_config.dart'; +import 'package:flutter/material.dart'; + +class DidvanText extends StatelessWidget { + final TextStyle? style; + final String text; + final Color? color; + final FontWeight? fontWeight; + final double? fontSize; + final TextAlign textAlign; + final int? maxLines; + final bool isEnglishFont; + final TextOverflow? overflow; + + const DidvanText( + this.text, { + Key? key, + this.style, + this.color, + this.fontSize, + this.fontWeight, + this.textAlign = TextAlign.right, + this.maxLines, + this.isEnglishFont = false, + this.overflow, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Text( + text, + style: ((style ?? Theme.of(context).textTheme.bodyText2)!.copyWith( + color: color, + fontWeight: fontWeight, + fontSize: fontSize, + )).copyWith( + fontFamily: isEnglishFont ? DesignConfig.fontFamily : null, + height: 1.7, + ), + overflow: overflow, + textAlign: textAlign, + maxLines: maxLines, + ); + } +} diff --git a/lib/views/widgets/didvan/text_field.dart b/lib/views/widgets/didvan/text_field.dart new file mode 100644 index 0000000..065b6d7 --- /dev/null +++ b/lib/views/widgets/didvan/text_field.dart @@ -0,0 +1,222 @@ +import 'package:didvan/config/design_config.dart'; +import 'package:didvan/config/theme_data.dart'; +import 'package:didvan/constants/app_icons.dart'; +import 'package:didvan/views/widgets/animated_visibility.dart'; +import 'package:didvan/views/widgets/didvan/text.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:persian_number_utility/persian_number_utility.dart'; + +class DidvanTextField extends StatefulWidget { + final void Function(String value)? onChanged; + final void Function(String value)? onSubmitted; + final bool enabled; + final bool autoFocus; + final TextAlign textAlign; + final String? title; + final String? hintText; + final dynamic initialValue; + final bool obsecureText; + final bool acceptSpace; + final Function(String value)? validator; + final TextInputType? textInputType; + const DidvanTextField({ + Key? key, + this.onChanged, + this.enabled = true, + this.title, + this.hintText, + this.initialValue, + this.validator, + this.textInputType, + this.textAlign = TextAlign.right, + this.obsecureText = false, + this.autoFocus = false, + this.onSubmitted, + this.acceptSpace = true, + }) : super(key: key); + + @override + State createState() => _DidvanTextFieldState(); +} + +class _DidvanTextFieldState extends State { + final FocusNode _focusNode = FocusNode(); + final TextEditingController _controller = TextEditingController(); + + bool _hideContent = false; + String? _error; + + @override + void initState() { + if (widget.initialValue != null) { + _controller.text = widget.initialValue; + } + _hideContent = widget.obsecureText; + _focusNode.addListener(() { + setState(() {}); + }); + super.initState(); + } + + @override + Widget build(BuildContext context) { + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (widget.title != null) + DidvanText( + widget.title!, + color: _titleColor(), + ), + if (widget.title != null) const SizedBox(height: 8), + Container( + height: 48, + padding: const EdgeInsets.symmetric(horizontal: 12).copyWith( + left: widget.obsecureText ? 0 : 12, + ), + decoration: BoxDecoration( + color: _fillColor(), + borderRadius: DesignConfig.lowBorderRadius, + border: Border.all(color: _borderColor()), + ), + child: TextFormField( + inputFormatters: [ + if (!widget.acceptSpace) + FilteringTextInputFormatter.allow(RegExp("[0-9a-zA-Z]")), + ], + autofocus: widget.autoFocus, + obscureText: _hideContent, + textAlign: widget.textAlign, + keyboardType: widget.textInputType, + focusNode: _focusNode, + controller: _controller, + onFieldSubmitted: widget.onSubmitted, + onChanged: _onChanged, + validator: _validator, + obscuringCharacter: '*', + style: Theme.of(context) + .textTheme + .bodyText2! + .copyWith(fontFamily: DesignConfig.fontFamily.padRight(3)), + decoration: InputDecoration( + suffixIcon: _suffixBuilder(), + enabled: widget.enabled, + border: InputBorder.none, + hintText: widget.hintText, + errorStyle: const TextStyle(height: 0.01), + hintStyle: Theme.of(context) + .textTheme + .bodyText2! + .copyWith(color: Theme.of(context).colorScheme.hint), + ), + ), + ), + const SizedBox(height: 8), + AnimatedVisibility( + isVisible: _error != null, + duration: DesignConfig.lowAnimationDuration, + child: Row( + children: [ + Icon( + DidvanIcons.lightbulb_exclamation_regular, + color: Theme.of(context).colorScheme.error, + size: 14, + ), + DidvanText( + _error ?? '', + style: Theme.of(context).textTheme.caption, + color: Theme.of(context).colorScheme.error, + ), + ], + ), + ) + ], + ); + } + + Color _borderColor() { + if (_focusNode.hasFocus) { + return Theme.of(context).colorScheme.focusedBorder; + } else if (_error != null) { + return Theme.of(context).colorScheme.error; + } + return Theme.of(context).colorScheme.border; + } + + Color? _titleColor() { + if (!widget.enabled) { + return Theme.of(context).colorScheme.hint; + } + if (_error != null) { + return Theme.of(context).colorScheme.error; + } + return null; + } + + Color _fillColor() { + if (!widget.enabled) { + return DesignConfig.isDark + ? Theme.of(context).colorScheme.disabledBackground + : Theme.of(context).colorScheme.secondCTA; + } + if (_focusNode.hasFocus) { + return Theme.of(context).colorScheme.focused; + } + if (_error != null) { + return Theme.of(context).colorScheme.errorBack; + } + return Theme.of(context).colorScheme.surface; + } + + Widget? _suffixBuilder() { + if (widget.obsecureText) { + return FittedBox( + fit: BoxFit.scaleDown, + child: GestureDetector( + onTap: () { + setState(() { + _hideContent = !_hideContent; + }); + }, + child: Icon( + _hideContent ? DidvanIcons.eye_solid : DidvanIcons.eye_slash_solid, + ), + ), + ); + } + return null; + } + + void _onChanged(String value) { + setState(() { + _error = null; + }); + value = value.toEnglishDigit(); + widget.onChanged?.call(value); + } + + String? _validator(String? value) { + if (widget.validator != null) { + final String? error = widget.validator!(value!); + if (error != null) { + setState(() { + _error = error; + }); + return ''; + } else { + setState(() { + _error = null; + }); + } + } + return null; + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } +} diff --git a/lib/views/widgets/ink_wrapper.dart b/lib/views/widgets/ink_wrapper.dart new file mode 100644 index 0000000..f63a044 --- /dev/null +++ b/lib/views/widgets/ink_wrapper.dart @@ -0,0 +1,39 @@ +import 'package:didvan/config/design_config.dart'; +import 'package:flutter/material.dart'; + +class InkWrapper extends StatelessWidget { + final Color? splashColor; + final Color? highlightColor; + final Widget child; + final VoidCallback? onPressed; + final BorderRadius? borderRadius; + + const InkWrapper({ + Key? key, + this.splashColor, + this.highlightColor, + required this.child, + this.onPressed, + this.borderRadius = DesignConfig.lowBorderRadius, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Stack( + children: [ + child, + Positioned.fill( + child: Material( + color: Colors.transparent, + child: InkWell( + borderRadius: borderRadius, + splashColor: splashColor, + highlightColor: highlightColor, + onTap: onPressed, + ), + ), + ), + ], + ); + } +} diff --git a/lib/views/widgets/item_title.dart b/lib/views/widgets/item_title.dart new file mode 100644 index 0000000..0477d87 --- /dev/null +++ b/lib/views/widgets/item_title.dart @@ -0,0 +1,38 @@ +import 'package:didvan/config/theme_data.dart'; +import 'package:didvan/views/widgets/didvan/text.dart'; +import 'package:flutter/material.dart'; + +class ItemTitle extends StatelessWidget { + final String title; + final TextStyle? style; + final IconData? icon; + final Color? color; + + const ItemTitle({ + Key? key, + required this.title, + this.icon, + this.color, + this.style, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.only(top: 16, bottom: 12), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (icon != null) + Icon(icon, color: color ?? Theme.of(context).colorScheme.title), + if (icon != null) const SizedBox(width: 4), + DidvanText( + title, + style: style ?? Theme.of(context).textTheme.subtitle1, + color: color ?? Theme.of(context).colorScheme.title, + ) + ], + ), + ); + } +} diff --git a/lib/views/widgets/logos/didvan_horizontal_logo.dart b/lib/views/widgets/logos/didvan_horizontal_logo.dart new file mode 100644 index 0000000..13437e0 --- /dev/null +++ b/lib/views/widgets/logos/didvan_horizontal_logo.dart @@ -0,0 +1,13 @@ +import 'package:didvan/constants/assets.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; + +class DidvanVerticalLogo extends StatelessWidget { + final double? height; + const DidvanVerticalLogo({Key? key, this.height}) : super(key: key); + + @override + Widget build(BuildContext context) { + return SvgPicture.asset(Assets.verticalLogoWithText, height: height); + } +} diff --git a/lib/views/widgets/logos/didvan_vertical_logo.dart b/lib/views/widgets/logos/didvan_vertical_logo.dart new file mode 100644 index 0000000..75cf9d5 --- /dev/null +++ b/lib/views/widgets/logos/didvan_vertical_logo.dart @@ -0,0 +1,21 @@ +import 'package:didvan/constants/assets.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; + +class DidvanHorizontalLogo extends StatelessWidget { + final String? type; + const DidvanHorizontalLogo({Key? key, this.type}) : super(key: key); + + @override + Widget build(BuildContext context) { + return SvgPicture.asset(_asset); + } + + String get _asset { + if (type == 'studio') { + return Assets.studioLogo; + } else { + return Assets.horizontalLogoWithText; + } + } +} diff --git a/lib/views/widgets/process_indicators.dart b/lib/views/widgets/process_indicators.dart new file mode 100644 index 0000000..8c2b6a5 --- /dev/null +++ b/lib/views/widgets/process_indicators.dart @@ -0,0 +1,10 @@ +import 'package:flutter/material.dart'; + +class DidvanProgressIndicator extends StatelessWidget { + const DidvanProgressIndicator({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Container(); + } +} diff --git a/lib/views/widgets/shimmer_placeholder.dart b/lib/views/widgets/shimmer_placeholder.dart new file mode 100644 index 0000000..fd39851 --- /dev/null +++ b/lib/views/widgets/shimmer_placeholder.dart @@ -0,0 +1,36 @@ +import 'package:didvan/config/design_config.dart'; +import 'package:didvan/config/theme_data.dart'; +import 'package:flutter/material.dart'; +import 'package:skeleton_text/skeleton_text.dart'; + +class ShimmerPlaceholder extends StatelessWidget { + final double? height; + final double? width; + final BorderRadius? borderRadius; + + const ShimmerPlaceholder({ + Key? key, + this.height, + this.width, + this.borderRadius = DesignConfig.lowBorderRadius, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return SkeletonAnimation( + borderRadius: borderRadius!, + shimmerColor: Theme.of(context).colorScheme.secondCTA, + gradientColor: Theme.of(context).colorScheme.cardBorder, + child: Container( + height: height, + width: width, + decoration: BoxDecoration( + color: DesignConfig.isDark + ? Theme.of(context).colorScheme.focused + : Theme.of(context).colorScheme.disabledBackground, + borderRadius: borderRadius, + ), + ), + ); + } +} diff --git a/lib/views/widgets/skeleton_image.dart b/lib/views/widgets/skeleton_image.dart new file mode 100644 index 0000000..c59f431 --- /dev/null +++ b/lib/views/widgets/skeleton_image.dart @@ -0,0 +1,55 @@ +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:didvan/config/design_config.dart'; +import 'package:didvan/services/network/request.dart'; +import 'package:didvan/services/network/request_helper.dart'; +import 'package:didvan/views/widgets/shimmer_placeholder.dart'; +import 'package:flutter/material.dart'; +import 'package:cached_network_image_platform_interface/cached_network_image_platform_interface.dart'; + +class SkeletonImage extends StatelessWidget { + final String imageUrl; + final double? width; + final double? height; + final BorderRadius? borderRadius; + final double? aspectRatio; + const SkeletonImage({ + Key? key, + required this.imageUrl, + this.borderRadius = DesignConfig.lowBorderRadius, + this.aspectRatio, + this.width, + this.height, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return _aspectRatioGenerator( + child: CachedNetworkImage( + imageRenderMethodForWeb: ImageRenderMethodForWeb.HttpGet, + httpHeaders: {'Authorization': 'Bearer ${RequestService.token}'}, + width: width, + height: height, + imageUrl: RequestHelper.baseUrl + imageUrl, + imageBuilder: (context, imageProvider) => ClipRRect( + borderRadius: borderRadius ?? DesignConfig.lowBorderRadius, + child: Image( + image: imageProvider, + fit: BoxFit.cover, + ), + ), + progressIndicatorBuilder: (context, url, progress) => + ShimmerPlaceholder( + borderRadius: borderRadius, + ), + ), + ); + } + + Widget _aspectRatioGenerator({required Widget child}) => aspectRatio == null + ? SizedBox(key: ValueKey(imageUrl), child: child) + : AspectRatio( + key: ValueKey(imageUrl), + aspectRatio: aspectRatio!, + child: child, + ); +} diff --git a/lib/views/widgets/state_handlers/empty_connection.dart b/lib/views/widgets/state_handlers/empty_connection.dart new file mode 100644 index 0000000..c6543bb --- /dev/null +++ b/lib/views/widgets/state_handlers/empty_connection.dart @@ -0,0 +1,18 @@ +import 'package:didvan/constants/assets.dart'; +import 'package:didvan/views/widgets/state_handlers/empty_state.dart'; +import 'package:flutter/material.dart'; + +class EmptyConnection extends StatelessWidget { + final VoidCallback onRetry; + const EmptyConnection({Key? key, required this.onRetry}) : super(key: key); + + @override + Widget build(BuildContext context) { + return EmptyState( + asset: Assets.emptyConnection, + title: 'ارتباط با اینترنت قطع شد...', + action: onRetry, + buttonTitle: 'تلاش دوباره', + ); + } +} diff --git a/lib/views/widgets/state_handlers/empty_list.dart b/lib/views/widgets/state_handlers/empty_list.dart new file mode 100644 index 0000000..9be66b6 --- /dev/null +++ b/lib/views/widgets/state_handlers/empty_list.dart @@ -0,0 +1,15 @@ +import 'package:didvan/constants/assets.dart'; +import 'package:didvan/views/widgets/state_handlers/empty_state.dart'; +import 'package:flutter/material.dart'; + +class EmptyList extends StatelessWidget { + const EmptyList({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return EmptyState( + asset: Assets.emptyBookmark, + title: 'لیست خالی است', + ); + } +} diff --git a/lib/views/widgets/state_handlers/empty_result.dart b/lib/views/widgets/state_handlers/empty_result.dart new file mode 100644 index 0000000..1ae6b8f --- /dev/null +++ b/lib/views/widgets/state_handlers/empty_result.dart @@ -0,0 +1,18 @@ +import 'package:didvan/constants/assets.dart'; +import 'package:didvan/views/widgets/state_handlers/empty_state.dart'; +import 'package:flutter/material.dart'; + +class EmptyResult extends StatelessWidget { + final VoidCallback onNewSearch; + const EmptyResult({Key? key, required this.onNewSearch}) : super(key: key); + + @override + Widget build(BuildContext context) { + return EmptyState( + asset: Assets.emptyResult, + title: 'نتیجه‌ای پیدا نشد', + buttonTitle: 'تغییر جستجو', + action: onNewSearch, + ); + } +} diff --git a/lib/views/widgets/state_handlers/empty_state.dart b/lib/views/widgets/state_handlers/empty_state.dart new file mode 100644 index 0000000..abfe420 --- /dev/null +++ b/lib/views/widgets/state_handlers/empty_state.dart @@ -0,0 +1,54 @@ +import 'package:didvan/config/theme_data.dart'; +import 'package:didvan/views/widgets/didvan/button.dart'; +import 'package:didvan/views/widgets/didvan/text.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; + +class EmptyState extends StatelessWidget { + final String asset; + final String title; + final String? subtitle; + final String? buttonTitle; + final VoidCallback? action; + final Color? titleColor; + + const EmptyState({ + Key? key, + required this.asset, + required this.title, + this.action, + this.buttonTitle, + this.subtitle, + this.titleColor, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SizedBox(height: 210, child: SvgPicture.asset(asset)), + const SizedBox(height: 16), + DidvanText( + title, + style: Theme.of(context).textTheme.headline3, + color: titleColor ?? Theme.of(context).colorScheme.caption, + ), + if (subtitle != null) const SizedBox(height: 8), + if (subtitle != null) + DidvanText( + subtitle!, + color: Theme.of(context).colorScheme.caption, + ), + if (action != null) const SizedBox(height: 16), + if (action != null) + DidvanButton( + height: 40, + onPressed: action, + title: buttonTitle, + width: 112, + ), + ], + ); + } +} diff --git a/lib/views/widgets/state_handlers/sliver_state_handler.dart b/lib/views/widgets/state_handlers/sliver_state_handler.dart new file mode 100644 index 0000000..cb762f5 --- /dev/null +++ b/lib/views/widgets/state_handlers/sliver_state_handler.dart @@ -0,0 +1,69 @@ +import 'package:didvan/models/enums.dart'; +import 'package:didvan/providers/core_provider.dart'; +import 'package:didvan/views/widgets/state_handlers/empty_connection.dart'; +import 'package:flutter/material.dart'; + +class SliverStateHandler extends SliverList { + final T state; + final Widget Function(BuildContext context, T state, int index) builder; + final int childCount; + final VoidCallback onRetry; + final bool enableEmptyState; + final Widget? emptyState; + final Widget? placeholder; + final EdgeInsets? itemPadding; + final bool centerEmptyState; + SliverStateHandler({ + Key? key, + required this.state, + required this.builder, + required this.childCount, + required this.onRetry, + this.itemPadding, + this.placeholder, + this.emptyState, + this.enableEmptyState = false, + this.centerEmptyState = true, + }) : super( + key: key, + delegate: SliverChildBuilderDelegate( + (context, index) { + if (state.appState == AppState.failed) { + return Padding( + padding: EdgeInsets.only( + top: centerEmptyState ? 120 : 20, + bottom: 20, + ), + child: EmptyConnection(onRetry: onRetry), + ); + } + if (enableEmptyState && state.appState == AppState.idle) { + return Padding( + padding: EdgeInsets.only( + top: centerEmptyState ? 120 : 20, + bottom: 20, + ), + child: emptyState, + ); + } + if (state.appState == AppState.busy) { + return Padding( + padding: itemPadding ?? const EdgeInsets.all(0), + child: placeholder, + ); + } + return Padding( + padding: itemPadding ?? const EdgeInsets.all(0), + child: builder(context, state, index), + ); + }, + childCount: state.appState == AppState.idle + ? enableEmptyState + ? 1 + : childCount + : state.appState == AppState.busy + ? 3 + : 1, + ), + ); +} diff --git a/lib/views/widgets/state_handlers/state_handler.dart b/lib/views/widgets/state_handlers/state_handler.dart new file mode 100644 index 0000000..513ac3f --- /dev/null +++ b/lib/views/widgets/state_handlers/state_handler.dart @@ -0,0 +1,51 @@ +import 'package:didvan/models/enums.dart'; +import 'package:didvan/providers/core_provider.dart'; +import 'package:didvan/views/widgets/state_handlers/empty_connection.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_spinkit/flutter_spinkit.dart'; + +class StateHandler extends StatelessWidget { + final T state; + final Widget Function(BuildContext context, T state) builder; + final VoidCallback onRetry; + final bool enableEmptyState; + final Widget? placeholder; + final Widget? emptyState; + final double topPadding; + const StateHandler({ + Key? key, + required this.builder, + required this.onRetry, + required this.state, + this.emptyState, + this.enableEmptyState = false, + this.topPadding = 0, + this.placeholder, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + if (enableEmptyState && state.appState == AppState.idle) { + return Container( + padding: EdgeInsets.only(top: topPadding), + alignment: Alignment.center, + child: SingleChildScrollView( + child: emptyState ?? const Text('Custom Widget Required!'), + ), + ); + } + switch (state.appState) { + case AppState.idle: + return builder(context, state); + case AppState.busy: + return placeholder ?? + SpinKitSpinningLines( + color: Theme.of(context).colorScheme.primary, + ); + case AppState.failed: + return Center(child: EmptyConnection(onRetry: onRetry)); + default: + return Container(); + } + } +} diff --git a/pubspec.lock b/pubspec.lock index 18fcc0f..1ae7618 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -7,7 +7,21 @@ packages: name: async url: "https://pub.dartlang.org" source: hosted - version: "2.8.1" + version: "2.8.2" + audio_session: + dependency: transitive + description: + name: audio_session + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.6+1" + audio_video_progress_bar: + dependency: "direct main" + description: + name: audio_video_progress_bar + url: "https://pub.dartlang.org" + source: hosted + version: "0.10.0" boolean_selector: dependency: transitive description: @@ -15,13 +29,48 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.1.0" + bot_toast: + dependency: "direct main" + description: + name: bot_toast + url: "https://pub.dartlang.org" + source: hosted + version: "4.0.1" + cached_network_image: + dependency: "direct main" + description: + name: cached_network_image + url: "https://pub.dartlang.org" + source: hosted + version: "3.2.0" + cached_network_image_platform_interface: + dependency: transitive + description: + name: cached_network_image_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.0" + cached_network_image_web: + dependency: transitive + description: + name: cached_network_image_web + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.1" + carousel_slider: + dependency: "direct main" + description: + name: carousel_slider + url: "https://pub.dartlang.org" + source: hosted + version: "4.0.0" characters: dependency: transitive description: name: characters url: "https://pub.dartlang.org" source: hosted - version: "1.1.0" + version: "1.2.0" charcode: dependency: transitive description: @@ -43,6 +92,27 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.15.0" + cross_file: + dependency: transitive + description: + name: cross_file + url: "https://pub.dartlang.org" + source: hosted + version: "0.3.2" + crypto: + dependency: transitive + description: + name: crypto + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.1" + csslib: + dependency: transitive + description: + name: csslib + url: "https://pub.dartlang.org" + source: hosted + version: "0.17.1" cupertino_icons: dependency: "direct main" description: @@ -50,6 +120,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.0.4" + day_night_time_picker: + dependency: "direct main" + description: + name: day_night_time_picker + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.4+1" fake_async: dependency: transitive description: @@ -57,11 +134,88 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.2.0" + ffi: + dependency: transitive + description: + name: ffi + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.2" + file: + dependency: transitive + description: + name: file + url: "https://pub.dartlang.org" + source: hosted + version: "6.1.2" + firebase_core: + dependency: "direct main" + description: + name: firebase_core + url: "https://pub.dartlang.org" + source: hosted + version: "1.13.1" + firebase_core_platform_interface: + dependency: transitive + description: + name: firebase_core_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "4.2.5" + firebase_core_web: + dependency: transitive + description: + name: firebase_core_web + url: "https://pub.dartlang.org" + source: hosted + version: "1.6.1" + firebase_messaging: + dependency: "direct main" + description: + name: firebase_messaging + url: "https://pub.dartlang.org" + source: hosted + version: "11.2.8" + firebase_messaging_platform_interface: + dependency: transitive + description: + name: firebase_messaging_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "3.2.1" + firebase_messaging_web: + dependency: transitive + description: + name: firebase_messaging_web + url: "https://pub.dartlang.org" + source: hosted + version: "2.2.9" flutter: dependency: "direct main" description: flutter source: sdk version: "0.0.0" + flutter_blurhash: + dependency: transitive + description: + name: flutter_blurhash + url: "https://pub.dartlang.org" + source: hosted + version: "0.6.0" + flutter_cache_manager: + dependency: transitive + description: + name: flutter_cache_manager + url: "https://pub.dartlang.org" + source: hosted + version: "3.3.0" + flutter_html: + dependency: "direct main" + description: + name: flutter_html + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.0-alpha.2" flutter_lints: dependency: "direct dev" description: @@ -69,11 +223,182 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.0.4" + flutter_localizations: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_plugin_android_lifecycle: + dependency: transitive + description: + name: flutter_plugin_android_lifecycle + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.5" + flutter_secure_storage: + dependency: "direct main" + description: + name: flutter_secure_storage + url: "https://pub.dartlang.org" + source: hosted + version: "5.0.2" + flutter_secure_storage_linux: + dependency: transitive + description: + name: flutter_secure_storage_linux + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.0" + flutter_secure_storage_macos: + dependency: transitive + description: + name: flutter_secure_storage_macos + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.0" + flutter_secure_storage_platform_interface: + dependency: transitive + description: + name: flutter_secure_storage_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.0" + flutter_secure_storage_web: + dependency: transitive + description: + name: flutter_secure_storage_web + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.2" + flutter_secure_storage_windows: + dependency: transitive + description: + name: flutter_secure_storage_windows + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.2" + flutter_spinkit: + dependency: "direct main" + description: + name: flutter_spinkit + url: "https://pub.dartlang.org" + source: hosted + version: "5.1.0" + flutter_svg: + dependency: "direct main" + description: + name: flutter_svg + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.0" flutter_test: dependency: "direct dev" description: flutter source: sdk version: "0.0.0" + flutter_vibrate: + dependency: "direct main" + description: + name: flutter_vibrate + url: "https://pub.dartlang.org" + source: hosted + version: "1.3.0" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + graphs: + dependency: transitive + description: + name: graphs + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.0" + html: + dependency: transitive + description: + name: html + url: "https://pub.dartlang.org" + source: hosted + version: "0.15.0" + http: + dependency: transitive + description: + name: http + url: "https://pub.dartlang.org" + source: hosted + version: "0.13.4" + http_parser: + dependency: transitive + description: + name: http_parser + url: "https://pub.dartlang.org" + source: hosted + version: "4.0.0" + image_cropper: + dependency: "direct main" + description: + name: image_cropper + url: "https://pub.dartlang.org" + source: hosted + version: "1.5.0" + image_picker: + dependency: "direct main" + description: + name: image_picker + url: "https://pub.dartlang.org" + source: hosted + version: "0.8.4+4" + image_picker_for_web: + dependency: transitive + description: + name: image_picker_for_web + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.4" + image_picker_platform_interface: + dependency: transitive + description: + name: image_picker_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "2.4.1" + intl: + dependency: transitive + description: + name: intl + url: "https://pub.dartlang.org" + source: hosted + version: "0.17.0" + js: + dependency: transitive + description: + name: js + url: "https://pub.dartlang.org" + source: hosted + version: "0.6.3" + just_audio: + dependency: "direct main" + description: + name: just_audio + url: "https://pub.dartlang.org" + source: hosted + version: "0.9.18" + just_audio_platform_interface: + dependency: transitive + description: + name: just_audio_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "4.0.0" + just_audio_web: + dependency: transitive + description: + name: just_audio_web + url: "https://pub.dartlang.org" + source: hosted + version: "0.4.2" lints: dependency: transitive description: @@ -87,7 +412,14 @@ packages: name: matcher url: "https://pub.dartlang.org" source: hosted - version: "0.12.10" + version: "0.12.11" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.3" meta: dependency: transitive description: @@ -95,6 +427,27 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.7.0" + nested: + dependency: transitive + description: + name: nested + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.0" + numerus: + dependency: transitive + description: + name: numerus + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.1" + octo_image: + dependency: transitive + description: + name: octo_image + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.1" path: dependency: transitive description: @@ -102,6 +455,174 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.8.0" + path_drawing: + dependency: transitive + description: + name: path_drawing + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.0" + path_parsing: + dependency: transitive + description: + name: path_parsing + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.0" + path_provider: + dependency: "direct main" + description: + name: path_provider + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.8" + path_provider_android: + dependency: transitive + description: + name: path_provider_android + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.11" + path_provider_ios: + dependency: transitive + description: + name: path_provider_ios + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.7" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.4" + path_provider_macos: + dependency: transitive + description: + name: path_provider_macos + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.4" + path_provider_platform_interface: + dependency: transitive + description: + name: path_provider_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.1" + path_provider_windows: + dependency: transitive + description: + name: path_provider_windows + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.4" + pedantic: + dependency: transitive + description: + name: pedantic + url: "https://pub.dartlang.org" + source: hosted + version: "1.11.1" + persian_datetime_picker: + dependency: "direct main" + description: + name: persian_datetime_picker + url: "https://pub.dartlang.org" + source: hosted + version: "2.4.0" + persian_number_utility: + dependency: "direct main" + description: + name: persian_number_utility + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.1" + petitparser: + dependency: transitive + description: + name: petitparser + url: "https://pub.dartlang.org" + source: hosted + version: "4.4.0" + pin_code_fields: + dependency: "direct main" + description: + name: pin_code_fields + url: "https://pub.dartlang.org" + source: hosted + version: "7.3.0" + platform: + dependency: transitive + description: + name: platform + url: "https://pub.dartlang.org" + source: hosted + version: "3.1.0" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.2" + process: + dependency: transitive + description: + name: process + url: "https://pub.dartlang.org" + source: hosted + version: "4.2.4" + provider: + dependency: "direct main" + description: + name: provider + url: "https://pub.dartlang.org" + source: hosted + version: "6.0.2" + record: + dependency: "direct main" + description: + name: record + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.2" + record_platform_interface: + dependency: transitive + description: + name: record_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "0.2.0" + record_web: + dependency: "direct main" + description: + name: record_web + url: "https://pub.dartlang.org" + source: hosted + version: "0.2.1" + rive: + dependency: "direct main" + description: + name: rive + url: "https://pub.dartlang.org" + source: hosted + version: "0.7.33" + rxdart: + dependency: transitive + description: + name: rxdart + url: "https://pub.dartlang.org" + source: hosted + version: "0.27.3" + skeleton_text: + dependency: "direct main" + description: + name: skeleton_text + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.0" sky_engine: dependency: transitive description: flutter @@ -114,6 +635,20 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.8.1" + sqflite: + dependency: transitive + description: + name: sqflite + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.1" + sqflite_common: + dependency: transitive + description: + name: sqflite_common + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.0" stack_trace: dependency: transitive description: @@ -135,6 +670,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.1.0" + synchronized: + dependency: transitive + description: + name: synchronized + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.0" term_glyph: dependency: transitive description: @@ -148,7 +690,7 @@ packages: name: test_api url: "https://pub.dartlang.org" source: hosted - version: "0.4.2" + version: "0.4.8" typed_data: dependency: transitive description: @@ -156,12 +698,111 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.3.0" + universal_html: + dependency: "direct main" + description: + name: universal_html + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.8" + universal_io: + dependency: transitive + description: + name: universal_io + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.4" + url_launcher: + dependency: "direct main" + description: + name: url_launcher + url: "https://pub.dartlang.org" + source: hosted + version: "6.0.18" + url_launcher_android: + dependency: transitive + description: + name: url_launcher_android + url: "https://pub.dartlang.org" + source: hosted + version: "6.0.14" + url_launcher_ios: + dependency: transitive + description: + name: url_launcher_ios + url: "https://pub.dartlang.org" + source: hosted + version: "6.0.14" + url_launcher_linux: + dependency: transitive + description: + name: url_launcher_linux + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.3" + url_launcher_macos: + dependency: transitive + description: + name: url_launcher_macos + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.3" + url_launcher_platform_interface: + dependency: transitive + description: + name: url_launcher_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.4" + url_launcher_web: + dependency: transitive + description: + name: url_launcher_web + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.6" + url_launcher_windows: + dependency: transitive + description: + name: url_launcher_windows + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.2" + uuid: + dependency: transitive + description: + name: uuid + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.5" vector_math: dependency: transitive description: name: vector_math url: "https://pub.dartlang.org" source: hosted - version: "2.1.0" + version: "2.1.1" + win32: + dependency: transitive + description: + name: win32 + url: "https://pub.dartlang.org" + source: hosted + version: "2.3.3" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + url: "https://pub.dartlang.org" + source: hosted + version: "0.2.0" + xml: + dependency: transitive + description: + name: xml + url: "https://pub.dartlang.org" + source: hosted + version: "5.3.1" sdks: - dart: ">=2.12.0 <3.0.0" + dart: ">=2.16.0 <3.0.0" + flutter: ">=2.5.0" diff --git a/pubspec.yaml b/pubspec.yaml index 18c9bf7..8b76c42 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,5 +1,5 @@ name: didvan -description: A new Flutter project. +description: چشم همیشه باز مدیران # The following line prevents the package from being accidentally published to # pub.dev using `flutter pub publish`. This is preferred for private packages. @@ -15,7 +15,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. # Read more about iOS versioning at # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -version: 1.0.0+1 +version: 1.0.5+5 environment: sdk: ">=2.12.0 <3.0.0" @@ -29,11 +29,40 @@ environment: dependencies: flutter: sdk: flutter + flutter_localizations: + sdk: flutter # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons. cupertino_icons: ^1.0.2 + provider: ^6.0.1 + pin_code_fields: ^7.3.0 + rive: ^0.7.33 + image_picker: ^0.8.4+4 + day_night_time_picker: ^1.0.3+1 + path_provider: ^2.0.8 + flutter_spinkit: ^5.1.0 + flutter_svg: ^1.0.0 + cached_network_image: ^3.2.0 + skeleton_text: ^3.0.0 + carousel_slider: ^4.0.0 + flutter_vibrate: ^1.3.0 + universal_html: ^2.0.8 + record: ^3.0.2 + just_audio: ^0.9.18 + record_web: ^0.2.1 + persian_datetime_picker: ^2.4.0 + persian_number_utility: ^1.1.1 + bot_toast: ^4.0.1 + flutter_secure_storage: ^5.0.2 + flutter_html: ^3.0.0-alpha.2 + url_launcher: ^6.0.18 + audio_video_progress_bar: ^0.10.0 + image_cropper: ^1.5.0 + firebase_messaging: ^11.2.8 + firebase_core: ^1.13.1 + dev_dependencies: flutter_test: @@ -58,9 +87,46 @@ flutter: uses-material-design: true # To add assets to your application, add an assets section, like this: - # assets: - # - images/a_dot_burr.jpeg - # - images/a_dot_ham.jpeg + assets: + - lib/assets/images/logos/logo-vertical-dark.svg + - lib/assets/images/logos/logo-vertical-light.svg + - lib/assets/images/logos/logo-horizontal-dark.svg + - lib/assets/images/logos/logo-horizontal-light.svg + - lib/assets/images/logos/studio-dark.svg + - lib/assets/images/logos/studio-light.svg + - lib/assets/images/categories/business-light.svg + - lib/assets/images/categories/economic-light.svg + - lib/assets/images/categories/enviromental-light.svg + - lib/assets/images/categories/political-light.svg + - lib/assets/images/categories/social-light.svg + - lib/assets/images/categories/tech-light.svg + - lib/assets/images/categories/business-dark.svg + - lib/assets/images/categories/economic-dark.svg + - lib/assets/images/categories/enviromental-dark.svg + - lib/assets/images/categories/political-dark.svg + - lib/assets/images/categories/social-dark.svg + - lib/assets/images/categories/tech-dark.svg + - lib/assets/images/themes/theme-light.svg + - lib/assets/images/themes/theme-dark.svg + - lib/assets/images/records/record-dark.svg + - lib/assets/images/records/record-light.svg + - lib/assets/images/empty_states/bookmark-light.svg + - lib/assets/images/empty_states/chart-light.svg + - lib/assets/images/empty_states/chat-light.svg + - lib/assets/images/empty_states/connection-light.svg + - lib/assets/images/empty_states/result-light.svg + - lib/assets/images/empty_states/studio-light.svg + - lib/assets/images/empty_states/bookmark-dark.svg + - lib/assets/images/empty_states/chart-dark.svg + - lib/assets/images/empty_states/chat-dark.svg + - lib/assets/images/empty_states/connection-dark.svg + - lib/assets/images/empty_states/result-dark.svg + - lib/assets/images/empty_states/studio-dark.svg + - lib/assets/animations/indicator-light.riv + - lib/assets/animations/indicator-dark.riv + - lib/assets/loading.gif + + # An image asset can refer to one or more resolution-specific "variants", see # https://flutter.dev/assets-and-images/#resolution-aware. @@ -73,7 +139,37 @@ flutter: # "family" key with the font family name, and a "fonts" key with a # list giving the asset and other descriptors for the font. For # example: - # fonts: + fonts: + - family: Dana-FA + fonts: + - asset: lib/assets/fonts/Dana-FaNum-Black.ttf + - asset: lib/assets/fonts/Dana-FaNum-Bold.ttf + - asset: lib/assets/fonts/Dana-FaNum-SemiBold.ttf + - asset: lib/assets/fonts/Dana-FaNum-ExtraBold.ttf + - asset: lib/assets/fonts/Dana-FaNum-Light.ttf + - asset: lib/assets/fonts/Dana-FaNum-Medium.ttf + - asset: lib/assets/fonts/Dana-FaNum-Regular.ttf + - asset: lib/assets/fonts/Dana-FaNum-Thin.ttf + - asset: lib/assets/fonts/Dana-FaNum-UltraBold.ttf + - family: Iransans-FA + fonts: + - asset: lib/assets/fonts/IRANSansMobile-FaNum-Black.ttf + - asset: lib/assets/fonts/IRANSansMobile-FaNum-Bold.ttf + - asset: lib/assets/fonts/IRANSansMobile-FaNum-Light.ttf + - asset: lib/assets/fonts/IRANSansMobile-FaNum-Medium.ttf + - asset: lib/assets/fonts/IRANSansMobile-FaNum-UltraLight.ttf + - asset: lib/assets/fonts/IRANSansMobile-FaNum.ttf + + - family: Dana + fonts: + - asset: lib/assets/fonts/Dana.ttf + - family: Iransans + fonts: + - asset: lib/assets/fonts/IRANSansMobile.ttf + - family: Dicon + fonts: + - asset: lib/assets/icons/Dicon.ttf + # fonts: # - family: Schyler # fonts: # - asset: fonts/Schyler-Regular.ttf diff --git a/test/widget_test.dart b/test/widget_test.dart index f3e669a..ae40864 100644 --- a/test/widget_test.dart +++ b/test/widget_test.dart @@ -5,26 +5,8 @@ // gestures. You can also use WidgetTester to find child widgets in the widget // tree, read text, and verify that the values of widget properties are correct. -import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:didvan/main.dart'; - void main() { - testWidgets('Counter increments smoke test', (WidgetTester tester) async { - // Build our app and trigger a frame. - await tester.pumpWidget(const MyApp()); - - // Verify that our counter starts at 0. - expect(find.text('0'), findsOneWidget); - expect(find.text('1'), findsNothing); - - // Tap the '+' icon and trigger a frame. - await tester.tap(find.byIcon(Icons.add)); - await tester.pump(); - - // Verify that our counter has incremented. - expect(find.text('0'), findsNothing); - expect(find.text('1'), findsOneWidget); - }); + testWidgets('Counter increments smoke test', (WidgetTester tester) async {}); } diff --git a/web/favicon.png b/web/favicon.png new file mode 100644 index 0000000..1c17c66 Binary files /dev/null and b/web/favicon.png differ diff --git a/web/icons/icon.png b/web/icons/icon.png new file mode 100644 index 0000000..966049e Binary files /dev/null and b/web/icons/icon.png differ diff --git a/web/index.html b/web/index.html new file mode 100644 index 0000000..a2dfb65 --- /dev/null +++ b/web/index.html @@ -0,0 +1,127 @@ + + + + + + + + + + + + + + + + + + + + Didvan + + + + +
+ +
+ + + + diff --git a/web/manifest.json b/web/manifest.json new file mode 100644 index 0000000..450b2c2 --- /dev/null +++ b/web/manifest.json @@ -0,0 +1,35 @@ +{ + "name": "Didvan", + "short_name": "Didvan", + "start_url": ".", + "display": "standalone", + "background_color": "#0175C2", + "theme_color": "#0175C2", + "description": "چشم همیشه باز مدیران", + "orientation": "portrait-primary", + "prefer_related_applications": false, + "icons": [ + { + "src": "icons/icon.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "icons/icon.png", + "sizes": "512x512", + "type": "image/png" + }, + { + "src": "icons/icon.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "maskable" + }, + { + "src": "icons/icon.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "maskable" + } + ] +}