diff --git a/.githooks/pre-commit b/.githooks/pre-commit index 6013a1b..93fb26c 100755 --- a/.githooks/pre-commit +++ b/.githooks/pre-commit @@ -3,6 +3,9 @@ printf "\e[33;1m%s\e[0m\n" 'Running the Flutter formatter' flutter format . printf "\e[33;1m%s\e[0m\n" 'Finished running the Flutter formatter' +printf "\e[33;1m%s\e[0m\n" 'Running the import_sorter' +flutter pub run import_sorter:main +printf "\e[33;1m%s\e[0m\n" 'Finished running the import_sorter' branch="$(git rev-parse --abbrev-ref HEAD)" diff --git a/.githooks/pre-push b/.githooks/pre-push index a6645ac..82d2f01 100755 --- a/.githooks/pre-push +++ b/.githooks/pre-push @@ -10,4 +10,9 @@ if [ $? -ne 0 ]; then printf "\e[31;1m%s\e[0m\n" 'Flutter analyzer error' exit 1 fi +flutter test +if [ $? -ne 0 ]; then + printf "\e[31;1m%s\e[0m\n" 'Flutter test error' + exit 1 +fi printf "\e[33;1m%s\e[0m\n" 'Finished running the Flutter analyzer' diff --git a/.github/workflows/check_issues.yaml b/.github/workflows/check_issues.yaml index bfb7ee5..c179913 100644 --- a/.github/workflows/check_issues.yaml +++ b/.github/workflows/check_issues.yaml @@ -7,7 +7,7 @@ on: jobs: test: - name: Analyze + name: Analyze & Test runs-on: ubuntu-latest steps: - uses: actions/checkout@v1 @@ -16,6 +16,7 @@ jobs: java-version: "12.x" - uses: subosito/flutter-action@v1 with: - flutter-version: "3.3.2" + flutter-version: "3.3.4" - run: flutter pub get - run: flutter analyze + - run: flutter test diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index 8dc2d7f..14a4481 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -16,7 +16,7 @@ jobs: java-version: "12.x" - uses: subosito/flutter-action@v1 with: - flutter-version: "3.3.2" + flutter-version: "3.3.3" - run: flutter pub get - run: flutter analyze @@ -31,7 +31,7 @@ jobs: java-version: "12.x" - uses: subosito/flutter-action@v1 with: - flutter-version: "3.3.2" + flutter-version: "3.3.3" - run: flutter pub get - run: flutter build apk --target-platform android-arm,android-arm64 --dart-define="lambiengcode=PRODUCTION" --release -v - name: Create a Release APK diff --git a/.vscode/settings.json b/.vscode/settings.json index abb3215..3dcda23 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,3 +1,4 @@ { - "dart.flutterSdkPath": "/Users/lambiengcode/Documents/askany_mobile/F:\\flutter_windows_2.8.1-stable\\flutter" + "dart.flutterSdkPath": "/Users/lambiengcode/Documents/askany_mobile/F:\\flutter_windows_2.8.1-stable\\flutter", + "java.configuration.updateBuildConfiguration": "interactive" } \ No newline at end of file diff --git a/README.md b/README.md index ac3fcf4..ae5eaf5 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,44 @@ -# streamskit_mobile +# StreamOS - UI Live Streaming App using Flutter 🛠️ 🎮 -A new Flutter project. +Computador + +StreamOS UI is a Flutter application with a sleek and intuitive user interface. The app features a robust login system, which allows users to sign in using their Google or Apple accounts via Firebase. + +The app has been built using Test-Driven Development (TDD) approach, ensuring that the login functionality is thoroughly tested through unit tests. + +## Screenshots + +

+ + + + +

+

+ + + + +

+ +## Features + +- Intuitive user interface +- Login with Google/Apple using Firebase ## Getting Started -This project is a starting point for a Flutter application. +To get started with StreamOS UI, follow the instructions below. + +### Prerequisites +- Flutter +- Firebase account -A few resources to get you started if this is your first Flutter project: +### Installation +1. Clone the repository: `git clone https://github.com/lambiengcode/flutter-live-stream-ui.git` +2. Install dependencies: `flutter pub get` +3. Run the app: `flutter run` -- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab) -- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook) +## Conclusion -For help getting started with Flutter development, view the -[online documentation](https://docs.flutter.dev/), which offers tutorials, -samples, guidance on mobile development, and a full API reference. +StreamOS UI is a sleek and intuitive Flutter application with a robust login system. The app's login functionality has been implemented using Firebase, ensuring a secure and seamless user experience. diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..034e848 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,21 @@ +# Security Policy + +## Supported Versions + +Use this section to tell people about which versions of your project are +currently being supported with security updates. + +| Version | Supported | +| ------- | ------------------ | +| 5.1.x | :white_check_mark: | +| 5.0.x | :x: | +| 4.0.x | :white_check_mark: | +| < 4.0 | :x: | + +## Reporting a Vulnerability + +Use this section to tell people how to report a vulnerability. + +Tell them where to go, how often they can expect to get an update on a +reported vulnerability, what to expect if the vulnerability is accepted or +declined, etc. diff --git a/analysis_options.yaml b/analysis_options.yaml index 1411db2..61b6c4d 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -7,7 +7,7 @@ # The following line activates a set of recommended lints for Flutter apps, # packages, and plugins designed to encourage good coding practices. -# include: package:flutter_lints/flutter.yaml +include: package:flutter_lints/flutter.yaml linter: # The lint rules applied to this project can be customized in the diff --git a/android/app/build.gradle b/android/app/build.gradle index fb3f944..0870c73 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -26,7 +26,7 @@ apply plugin: 'kotlin-android' apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" android { - compileSdkVersion flutter.compileSdkVersion + compileSdkVersion 33 ndkVersion flutter.ndkVersion compileOptions { @@ -47,8 +47,8 @@ android { applicationId "com.streams.kit" // You can update the following values to match your application needs. // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-build-configuration. - minSdkVersion flutter.minSdkVersion - targetSdkVersion flutter.targetSdkVersion + minSdkVersion 24 + targetSdkVersion 30 versionCode flutterVersionCode.toInteger() versionName flutterVersionName } diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 1032cef..624074c 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -1,7 +1,18 @@ + + + + + + + + + + + CFBundleVersion 1.0 MinimumOSVersion - 11.0 + 12.0 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..f6d3c81 --- /dev/null +++ b/ios/Podfile @@ -0,0 +1,44 @@ +# Uncomment this line to define a global platform for your project +platform :ios, '13.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) + target.build_configurations.each do |config| + config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '13.0' + end + end +end diff --git a/ios/Podfile.lock b/ios/Podfile.lock new file mode 100644 index 0000000..bb595c8 --- /dev/null +++ b/ios/Podfile.lock @@ -0,0 +1,266 @@ +PODS: + - AppAuth (1.7.5): + - AppAuth/Core (= 1.7.5) + - AppAuth/ExternalUserAgent (= 1.7.5) + - AppAuth/Core (1.7.5) + - AppAuth/ExternalUserAgent (1.7.5): + - AppAuth/Core + - FBAEMKit (14.1.0): + - FBSDKCoreKit_Basics (= 14.1.0) + - FBSDKCoreKit (14.1.0): + - FBAEMKit (= 14.1.0) + - FBSDKCoreKit_Basics (= 14.1.0) + - FBSDKCoreKit_Basics (14.1.0) + - FBSDKLoginKit (14.1.0): + - FBSDKCoreKit (= 14.1.0) + - Firebase/Auth (10.25.0): + - Firebase/CoreOnly + - FirebaseAuth (~> 10.25.0) + - Firebase/CoreOnly (10.25.0): + - FirebaseCore (= 10.25.0) + - Firebase/Crashlytics (10.25.0): + - Firebase/CoreOnly + - FirebaseCrashlytics (~> 10.25.0) + - Firebase/Messaging (10.25.0): + - Firebase/CoreOnly + - FirebaseMessaging (~> 10.25.0) + - firebase_auth (4.19.5): + - Firebase/Auth (= 10.25.0) + - firebase_core + - Flutter + - firebase_core (2.31.0): + - Firebase/CoreOnly (= 10.25.0) + - Flutter + - firebase_crashlytics (3.5.5): + - Firebase/Crashlytics (= 10.25.0) + - firebase_core + - Flutter + - firebase_messaging (14.9.2): + - Firebase/Messaging (= 10.25.0) + - firebase_core + - Flutter + - FirebaseAppCheckInterop (10.25.0) + - FirebaseAuth (10.25.0): + - FirebaseAppCheckInterop (~> 10.17) + - FirebaseCore (~> 10.0) + - GoogleUtilities/AppDelegateSwizzler (~> 7.8) + - GoogleUtilities/Environment (~> 7.8) + - GTMSessionFetcher/Core (< 4.0, >= 2.1) + - RecaptchaInterop (~> 100.0) + - FirebaseCore (10.25.0): + - FirebaseCoreInternal (~> 10.0) + - GoogleUtilities/Environment (~> 7.12) + - GoogleUtilities/Logger (~> 7.12) + - FirebaseCoreExtension (10.25.0): + - FirebaseCore (~> 10.0) + - FirebaseCoreInternal (10.25.0): + - "GoogleUtilities/NSData+zlib (~> 7.8)" + - FirebaseCrashlytics (10.25.0): + - FirebaseCore (~> 10.5) + - FirebaseInstallations (~> 10.0) + - FirebaseRemoteConfigInterop (~> 10.23) + - FirebaseSessions (~> 10.5) + - GoogleDataTransport (~> 9.2) + - GoogleUtilities/Environment (~> 7.8) + - nanopb (< 2.30911.0, >= 2.30908.0) + - PromisesObjC (~> 2.1) + - FirebaseInstallations (10.25.0): + - FirebaseCore (~> 10.0) + - GoogleUtilities/Environment (~> 7.8) + - GoogleUtilities/UserDefaults (~> 7.8) + - PromisesObjC (~> 2.1) + - FirebaseMessaging (10.25.0): + - FirebaseCore (~> 10.0) + - FirebaseInstallations (~> 10.0) + - GoogleDataTransport (~> 9.3) + - GoogleUtilities/AppDelegateSwizzler (~> 7.8) + - GoogleUtilities/Environment (~> 7.8) + - GoogleUtilities/Reachability (~> 7.8) + - GoogleUtilities/UserDefaults (~> 7.8) + - nanopb (< 2.30911.0, >= 2.30908.0) + - FirebaseRemoteConfigInterop (10.25.0) + - FirebaseSessions (10.25.0): + - FirebaseCore (~> 10.5) + - FirebaseCoreExtension (~> 10.0) + - FirebaseInstallations (~> 10.0) + - GoogleDataTransport (~> 9.2) + - GoogleUtilities/Environment (~> 7.13) + - GoogleUtilities/UserDefaults (~> 7.13) + - nanopb (< 2.30911.0, >= 2.30908.0) + - PromisesSwift (~> 2.1) + - Flutter (1.0.0) + - flutter_facebook_auth (4.4.1): + - FBSDKLoginKit (= 14.1.0) + - Flutter + - google_sign_in_ios (0.0.1): + - Flutter + - GoogleSignIn (~> 6.2) + - GoogleDataTransport (9.4.1): + - GoogleUtilities/Environment (~> 7.7) + - nanopb (< 2.30911.0, >= 2.30908.0) + - PromisesObjC (< 3.0, >= 1.2) + - GoogleSignIn (6.2.4): + - AppAuth (~> 1.5) + - GTMAppAuth (~> 1.3) + - GTMSessionFetcher/Core (< 3.0, >= 1.1) + - GoogleUtilities/AppDelegateSwizzler (7.13.3): + - GoogleUtilities/Environment + - GoogleUtilities/Logger + - GoogleUtilities/Network + - GoogleUtilities/Privacy + - GoogleUtilities/Environment (7.13.3): + - GoogleUtilities/Privacy + - PromisesObjC (< 3.0, >= 1.2) + - GoogleUtilities/Logger (7.13.3): + - GoogleUtilities/Environment + - GoogleUtilities/Privacy + - GoogleUtilities/Network (7.13.3): + - GoogleUtilities/Logger + - "GoogleUtilities/NSData+zlib" + - GoogleUtilities/Privacy + - GoogleUtilities/Reachability + - "GoogleUtilities/NSData+zlib (7.13.3)": + - GoogleUtilities/Privacy + - GoogleUtilities/Privacy (7.13.3) + - GoogleUtilities/Reachability (7.13.3): + - GoogleUtilities/Logger + - GoogleUtilities/Privacy + - GoogleUtilities/UserDefaults (7.13.3): + - GoogleUtilities/Logger + - GoogleUtilities/Privacy + - GTMAppAuth (1.3.1): + - AppAuth/Core (~> 1.6) + - GTMSessionFetcher/Core (< 3.0, >= 1.5) + - GTMSessionFetcher/Core (2.3.0) + - image_picker_ios (0.0.1): + - Flutter + - nanopb (2.30910.0): + - nanopb/decode (= 2.30910.0) + - nanopb/encode (= 2.30910.0) + - nanopb/decode (2.30910.0) + - nanopb/encode (2.30910.0) + - path_provider_foundation (0.0.1): + - Flutter + - FlutterMacOS + - PromisesObjC (2.4.0) + - PromisesSwift (2.4.0): + - PromisesObjC (= 2.4.0) + - RecaptchaInterop (100.0.0) + - sign_in_with_apple (0.0.1): + - Flutter + - sqflite (0.0.3): + - Flutter + - FlutterMacOS + - url_launcher_ios (0.0.1): + - Flutter + +DEPENDENCIES: + - firebase_auth (from `.symlinks/plugins/firebase_auth/ios`) + - firebase_core (from `.symlinks/plugins/firebase_core/ios`) + - firebase_crashlytics (from `.symlinks/plugins/firebase_crashlytics/ios`) + - firebase_messaging (from `.symlinks/plugins/firebase_messaging/ios`) + - Flutter (from `Flutter`) + - flutter_facebook_auth (from `.symlinks/plugins/flutter_facebook_auth/ios`) + - google_sign_in_ios (from `.symlinks/plugins/google_sign_in_ios/ios`) + - image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`) + - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) + - sign_in_with_apple (from `.symlinks/plugins/sign_in_with_apple/ios`) + - sqflite (from `.symlinks/plugins/sqflite/darwin`) + - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) + +SPEC REPOS: + trunk: + - AppAuth + - FBAEMKit + - FBSDKCoreKit + - FBSDKCoreKit_Basics + - FBSDKLoginKit + - Firebase + - FirebaseAppCheckInterop + - FirebaseAuth + - FirebaseCore + - FirebaseCoreExtension + - FirebaseCoreInternal + - FirebaseCrashlytics + - FirebaseInstallations + - FirebaseMessaging + - FirebaseRemoteConfigInterop + - FirebaseSessions + - GoogleDataTransport + - GoogleSignIn + - GoogleUtilities + - GTMAppAuth + - GTMSessionFetcher + - nanopb + - PromisesObjC + - PromisesSwift + - RecaptchaInterop + +EXTERNAL SOURCES: + firebase_auth: + :path: ".symlinks/plugins/firebase_auth/ios" + firebase_core: + :path: ".symlinks/plugins/firebase_core/ios" + firebase_crashlytics: + :path: ".symlinks/plugins/firebase_crashlytics/ios" + firebase_messaging: + :path: ".symlinks/plugins/firebase_messaging/ios" + Flutter: + :path: Flutter + flutter_facebook_auth: + :path: ".symlinks/plugins/flutter_facebook_auth/ios" + google_sign_in_ios: + :path: ".symlinks/plugins/google_sign_in_ios/ios" + image_picker_ios: + :path: ".symlinks/plugins/image_picker_ios/ios" + path_provider_foundation: + :path: ".symlinks/plugins/path_provider_foundation/darwin" + sign_in_with_apple: + :path: ".symlinks/plugins/sign_in_with_apple/ios" + sqflite: + :path: ".symlinks/plugins/sqflite/darwin" + url_launcher_ios: + :path: ".symlinks/plugins/url_launcher_ios/ios" + +SPEC CHECKSUMS: + AppAuth: 501c04eda8a8d11f179dbe8637b7a91bb7e5d2fa + FBAEMKit: a899515e45476027f73aef377b5cffadcd56ca3a + FBSDKCoreKit: 24f8bc8d3b5b2a8c5c656a1329492a12e8efa792 + FBSDKCoreKit_Basics: 6e578c9bdc7aa1365dbbbde633c9ebb536bcaa98 + FBSDKLoginKit: 787de205d524c3a4b17d527916f1d066e4361660 + Firebase: 0312a2352584f782ea56f66d91606891d4607f06 + firebase_auth: 76ea642e91a9e914b3af751a416046ce1a965cf4 + firebase_core: 0b39f4f424e02eecabb2356ddf331fa07b772af8 + firebase_crashlytics: 5adb9a5ac7858811cef7a9447a011bb4dcb540c3 + firebase_messaging: 8999827b6efc9c3ab4b1f9dc246deaa7f13dbf88 + FirebaseAppCheckInterop: 5da5ce93e8797a215e3f677fb0654b74e736c8b8 + FirebaseAuth: c0f93dcc570c9da2bffb576969d793e95c344fbb + FirebaseCore: 7ec4d0484817f12c3373955bc87762d96842d483 + FirebaseCoreExtension: 8a47811d0b155501559ef05d089518152a0a1677 + FirebaseCoreInternal: 910a81992c33715fec9263ca7381d59ab3a750b7 + FirebaseCrashlytics: 4b96efb0ce73b38b2a85e8b8bd1bd8f63f09d015 + FirebaseInstallations: 91950fe859846fff0fbd296180909dd273103b09 + FirebaseMessaging: 88950ba9485052891ebe26f6c43a52bb62248952 + FirebaseRemoteConfigInterop: b25018791b204c0d78a90e394d6c62d9b1f22da8 + FirebaseSessions: c0939656253a1fa0e94ecc266ccf770cc8b33732 + Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 + flutter_facebook_auth: 361ac7a57263ebf327f26089507ead0d66558ee8 + google_sign_in_ios: 4f85eb9f937450765c8573bb85fd8cd6a5af675c + GoogleDataTransport: 6c09b596d841063d76d4288cc2d2f42cc36e1e2a + GoogleSignIn: 5651ce3a61e56ca864160e79b484cd9ed3f49b7a + GoogleUtilities: ea963c370a38a8069cc5f7ba4ca849a60b6d7d15 + GTMAppAuth: 0ff230db599948a9ad7470ca667337803b3fc4dd + GTMSessionFetcher: 3a63d75eecd6aa32c2fc79f578064e1214dfdec2 + image_picker_ios: 99dfe1854b4fa34d0364e74a78448a0151025425 + nanopb: 438bc412db1928dac798aa6fd75726007be04262 + path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 + PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 + PromisesSwift: 9d77319bbe72ebf6d872900551f7eeba9bce2851 + RecaptchaInterop: 7d1a4a01a6b2cb1610a47ef3f85f0c411434cb21 + sign_in_with_apple: f3bf75217ea4c2c8b91823f225d70230119b8440 + sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec + url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe + +PODFILE CHECKSUM: bd295fc198b245bb51189c8140d55e735c8dd822 + +COCOAPODS: 1.15.2 diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 82919bf..9cf0c37 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -3,13 +3,15 @@ archiveVersion = 1; classes = { }; - objectVersion = 50; + objectVersion = 54; objects = { /* Begin PBXBuildFile section */ 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 40684CBA5EA54C8A7F2CC14E /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A5C642D84F9B2C90C6C478BF /* Pods_Runner.framework */; }; 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; + 8D0A88E028FDA96E0070E94C /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 8D0A88DF28FDA96E0070E94C /* GoogleService-Info.plist */; }; 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 */; }; @@ -32,9 +34,13 @@ 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 = ""; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 610335755246C6F60FE9FD14 /* 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 = ""; }; 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 = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; + 8AF245831FD34D37CF37DDB9 /* 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 = ""; }; + 8D0A88DF28FDA96E0070E94C /* GoogleService-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = ""; }; + 8D0A88E128FDAA0F0070E94C /* Runner.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Runner.entitlements; 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 = ""; }; 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -42,6 +48,8 @@ 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 = ""; }; + A5C642D84F9B2C90C6C478BF /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + B02922E201C5089328B22D94 /* 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 = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -49,12 +57,23 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 40684CBA5EA54C8A7F2CC14E /* Pods_Runner.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 21574D27796729176B267C49 /* Pods */ = { + isa = PBXGroup; + children = ( + 8AF245831FD34D37CF37DDB9 /* Pods-Runner.debug.xcconfig */, + B02922E201C5089328B22D94 /* Pods-Runner.release.xcconfig */, + 610335755246C6F60FE9FD14 /* Pods-Runner.profile.xcconfig */, + ); + path = Pods; + sourceTree = ""; + }; 9740EEB11CF90186004384FC /* Flutter */ = { isa = PBXGroup; children = ( @@ -72,6 +91,8 @@ 9740EEB11CF90186004384FC /* Flutter */, 97C146F01CF9000F007C117D /* Runner */, 97C146EF1CF9000F007C117D /* Products */, + 21574D27796729176B267C49 /* Pods */, + DEB8ACB4F7A29354A9F868CC /* Frameworks */, ); sourceTree = ""; }; @@ -86,10 +107,12 @@ 97C146F01CF9000F007C117D /* Runner */ = { isa = PBXGroup; children = ( + 8D0A88E128FDAA0F0070E94C /* Runner.entitlements */, 97C146FA1CF9000F007C117D /* Main.storyboard */, 97C146FD1CF9000F007C117D /* Assets.xcassets */, 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, 97C147021CF9000F007C117D /* Info.plist */, + 8D0A88DF28FDA96E0070E94C /* GoogleService-Info.plist */, 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, @@ -98,6 +121,14 @@ path = Runner; sourceTree = ""; }; + DEB8ACB4F7A29354A9F868CC /* Frameworks */ = { + isa = PBXGroup; + children = ( + A5C642D84F9B2C90C6C478BF /* Pods_Runner.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -105,12 +136,15 @@ isa = PBXNativeTarget; buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( + 3216120C9048560070B84E61 /* [CP] Check Pods Manifest.lock */, 9740EEB61CF901F6004384FC /* Run Script */, 97C146EA1CF9000F007C117D /* Sources */, 97C146EB1CF9000F007C117D /* Frameworks */, 97C146EC1CF9000F007C117D /* Resources */, 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + CC8680EB32E496F890A253D7 /* [CP] Embed Pods Frameworks */, + 1F65992E5291859F00F78227 /* [CP] Copy Pods Resources */, ); buildRules = ( ); @@ -127,7 +161,7 @@ 97C146E61CF9000F007C117D /* Project object */ = { isa = PBXProject; attributes = { - LastUpgradeCheck = 1300; + LastUpgradeCheck = 1510; ORGANIZATIONNAME = ""; TargetAttributes = { 97C146ED1CF9000F007C117D = { @@ -161,6 +195,7 @@ files = ( 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, + 8D0A88E028FDA96E0070E94C /* GoogleService-Info.plist in Resources */, 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, ); @@ -169,12 +204,53 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ + 1F65992E5291859F00F78227 /* [CP] Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Copy Pods Resources"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; + 3216120C9048560070B84E61 /* [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; + }; 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); inputPaths = ( + "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", ); name = "Thin Binary"; outputPaths = ( @@ -185,6 +261,7 @@ }; 9740EEB61CF901F6004384FC /* Run Script */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); @@ -197,6 +274,23 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; }; + CC8680EB32E496F890A253D7 /* [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 */ @@ -272,7 +366,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; @@ -287,12 +381,16 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; DEVELOPMENT_TEAM = 3SHAJNAYM4; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = "Streams Kit"; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.video"; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -300,6 +398,7 @@ MARKETING_VERSION = 1.0.0; PRODUCT_BUNDLE_IDENTIFIER = com.streams.kit; PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; VERSIONING_SYSTEM = "apple-generic"; @@ -353,7 +452,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -402,7 +501,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; @@ -419,12 +518,16 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; DEVELOPMENT_TEAM = 3SHAJNAYM4; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = "Streams Kit"; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.video"; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -432,6 +535,7 @@ MARKETING_VERSION = 1.0.0; PRODUCT_BUNDLE_IDENTIFIER = com.streams.kit; PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; @@ -445,12 +549,16 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; DEVELOPMENT_TEAM = 3SHAJNAYM4; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = "Streams Kit"; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.video"; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -458,6 +566,7 @@ MARKETING_VERSION = 1.0.0; PRODUCT_BUNDLE_IDENTIFIER = com.streams.kit; 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 f3d88ac..498aa0d 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..e9b4ca7 100644 --- a/ios/Runner/AppDelegate.swift +++ b/ios/Runner/AppDelegate.swift @@ -1,5 +1,6 @@ import UIKit import Flutter +import Firebase @UIApplicationMain @objc class AppDelegate: FlutterAppDelegate { @@ -7,7 +8,10 @@ import Flutter _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? ) -> Bool { + /// Configuration + FirebaseApp.configure() GeneratedPluginRegistrant.register(with: self) + application.registerForRemoteNotifications() return super.application(application, didFinishLaunchingWithOptions: launchOptions) } } diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-1024.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-1024.png new file mode 100644 index 0000000..c52923b Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-1024.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/STREAM KIT-20.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-20.png similarity index 60% rename from ios/Runner/Assets.xcassets/AppIcon.appiconset/STREAM KIT-20.png rename to ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-20.png index f8c9c56..0090472 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/STREAM KIT-20.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-20.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-20@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-20@2x.png new file mode 100644 index 0000000..ab94901 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-20@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-20@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-20@3x.png new file mode 100644 index 0000000..e0da407 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-20@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/STREAM KIT-29.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-29.png similarity index 51% rename from ios/Runner/Assets.xcassets/AppIcon.appiconset/STREAM KIT-29.png rename to ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-29.png index 7d1d7ae..1e072c4 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/STREAM KIT-29.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-29.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-29@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-29@2x.png new file mode 100644 index 0000000..0743f87 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-29@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-29@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-29@3x.png new file mode 100644 index 0000000..8899db3 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-29@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-40.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-40.png new file mode 100644 index 0000000..ab94901 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-40.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-40@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-40@2x.png new file mode 100644 index 0000000..07caa6b Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-40@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-40@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-40@3x.png new file mode 100644 index 0000000..53a9ab0 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-40@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-60@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-60@2x.png new file mode 100644 index 0000000..53a9ab0 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-60@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-60@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-60@3x.png new file mode 100644 index 0000000..2324c6f Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-60@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-76.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-76.png new file mode 100644 index 0000000..ebefe80 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-76.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-76@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-76@2x.png new file mode 100644 index 0000000..979c111 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-76@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-83.5@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-83.5@2x.png new file mode 100644 index 0000000..3f4ab91 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-83.5@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json index 451038c..69c9e36 100644 --- a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json +++ b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -1,109 +1,109 @@ { "images" : [ { - "filename" : "STREAM KIT-20@2x.png", + "filename" : "AppIcon-20@2x.png", "idiom" : "iphone", "scale" : "2x", "size" : "20x20" }, { - "filename" : "STREAM KIT-20@3x.png", + "filename" : "AppIcon-20@3x.png", "idiom" : "iphone", "scale" : "3x", "size" : "20x20" }, { - "filename" : "STREAM KIT-29@2x.png", + "filename" : "AppIcon-29@2x.png", "idiom" : "iphone", "scale" : "2x", "size" : "29x29" }, { - "filename" : "STREAM KIT-29@3x.png", + "filename" : "AppIcon-29@3x.png", "idiom" : "iphone", "scale" : "3x", "size" : "29x29" }, { - "filename" : "STREAM KIT-40@2x.png", + "filename" : "AppIcon-40@2x.png", "idiom" : "iphone", "scale" : "2x", "size" : "40x40" }, { - "filename" : "STREAM KIT-40@3x.png", + "filename" : "AppIcon-40@3x.png", "idiom" : "iphone", "scale" : "3x", "size" : "40x40" }, { - "filename" : "STREAM KIT-60@2x.png", + "filename" : "AppIcon-60@2x.png", "idiom" : "iphone", "scale" : "2x", "size" : "60x60" }, { - "filename" : "STREAM KIT-60@3x.png", + "filename" : "AppIcon-60@3x.png", "idiom" : "iphone", "scale" : "3x", "size" : "60x60" }, { - "filename" : "STREAM KIT-20.png", + "filename" : "AppIcon-20.png", "idiom" : "ipad", "scale" : "1x", "size" : "20x20" }, { - "filename" : "STREAM KIT-20@2x.png", + "filename" : "AppIcon-20@2x.png", "idiom" : "ipad", "scale" : "2x", "size" : "20x20" }, { - "filename" : "STREAM KIT-29.png", + "filename" : "AppIcon-29.png", "idiom" : "ipad", "scale" : "1x", "size" : "29x29" }, { - "filename" : "STREAM KIT-29@2x.png", + "filename" : "AppIcon-29@2x.png", "idiom" : "ipad", "scale" : "2x", "size" : "29x29" }, { - "filename" : "STREAM KIT-40.png", + "filename" : "AppIcon-40.png", "idiom" : "ipad", "scale" : "1x", "size" : "40x40" }, { - "filename" : "STREAM KIT-40@2x.png", + "filename" : "AppIcon-40@2x.png", "idiom" : "ipad", "scale" : "2x", "size" : "40x40" }, { - "filename" : "STREAM KIT-76.png", + "filename" : "AppIcon-76.png", "idiom" : "ipad", "scale" : "1x", "size" : "76x76" }, { - "filename" : "STREAM KIT-76@2x.png", + "filename" : "AppIcon-76@2x.png", "idiom" : "ipad", "scale" : "2x", "size" : "76x76" }, { - "filename" : "STREAM KIT-83.5@2x.png", + "filename" : "AppIcon-83.5@2x.png", "idiom" : "ipad", "scale" : "2x", "size" : "83.5x83.5" }, { - "filename" : "STREAM KIT-1024.png", + "filename" : "AppIcon-1024.png", "idiom" : "ios-marketing", "scale" : "1x", "size" : "1024x1024" diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/STREAM KIT-1024.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/STREAM KIT-1024.png deleted file mode 100644 index 6183b3e..0000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/STREAM KIT-1024.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/STREAM KIT-20@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/STREAM KIT-20@2x.png deleted file mode 100644 index d348989..0000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/STREAM KIT-20@2x.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/STREAM KIT-20@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/STREAM KIT-20@3x.png deleted file mode 100644 index 12392e1..0000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/STREAM KIT-20@3x.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/STREAM KIT-29@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/STREAM KIT-29@2x.png deleted file mode 100644 index f79966b..0000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/STREAM KIT-29@2x.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/STREAM KIT-29@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/STREAM KIT-29@3x.png deleted file mode 100644 index fad87d5..0000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/STREAM KIT-29@3x.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/STREAM KIT-40.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/STREAM KIT-40.png deleted file mode 100644 index d348989..0000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/STREAM KIT-40.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/STREAM KIT-40@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/STREAM KIT-40@2x.png deleted file mode 100644 index 1445dc4..0000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/STREAM KIT-40@2x.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/STREAM KIT-40@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/STREAM KIT-40@3x.png deleted file mode 100644 index ae0f456..0000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/STREAM KIT-40@3x.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/STREAM KIT-60@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/STREAM KIT-60@2x.png deleted file mode 100644 index ae0f456..0000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/STREAM KIT-60@2x.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/STREAM KIT-60@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/STREAM KIT-60@3x.png deleted file mode 100644 index 377b91f..0000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/STREAM KIT-60@3x.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/STREAM KIT-76.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/STREAM KIT-76.png deleted file mode 100644 index e85976f..0000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/STREAM KIT-76.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/STREAM KIT-76@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/STREAM KIT-76@2x.png deleted file mode 100644 index 0ef8d73..0000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/STREAM KIT-76@2x.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/STREAM KIT-83.5@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/STREAM KIT-83.5@2x.png deleted file mode 100644 index cb28cad..0000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/STREAM KIT-83.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..feddb9d --- /dev/null +++ b/ios/Runner/GoogleService-Info.plist @@ -0,0 +1,34 @@ + + + + + CLIENT_ID + 38681732543-a1c15bbp5qra3vdf4c26f8h4qj4mp8hm.apps.googleusercontent.com + REVERSED_CLIENT_ID + com.googleusercontent.apps.38681732543-a1c15bbp5qra3vdf4c26f8h4qj4mp8hm + API_KEY + AIzaSyBvr4N6_c5AtQfWH44EU8QHDH-sPgGtr9A + GCM_SENDER_ID + 38681732543 + PLIST_VERSION + 1 + BUNDLE_ID + com.streams.kit + PROJECT_ID + streamos-2c5cd + STORAGE_BUCKET + streamos-2c5cd.appspot.com + IS_ADS_ENABLED + + IS_ANALYTICS_ENABLED + + IS_APPINVITE_ENABLED + + IS_GCM_ENABLED + + IS_SIGNIN_ENABLED + + GOOGLE_APP_ID + 1:38681732543:ios:d0c2f5a9f52dd059a5ad0a + + \ No newline at end of file diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index 3865557..759156d 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -7,7 +7,7 @@ CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleDisplayName - StreamKit + StreamOS CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier @@ -22,6 +22,21 @@ $(FLUTTER_BUILD_NAME) CFBundleSignature ???? + CFBundleURLTypes + + + CFBundleTypeRole + Editor + CFBundleURLName + com.streams.kit + CFBundleURLSchemes + + + com.googleusercontent.apps.38681732543-a1c15bbp5qra3vdf4c26f8h4qj4mp8hm + https + + + CFBundleVersion $(FLUTTER_BUILD_NUMBER) LSRequiresIPhoneOS diff --git a/ios/Runner/Runner.entitlements b/ios/Runner/Runner.entitlements new file mode 100644 index 0000000..0c67376 --- /dev/null +++ b/ios/Runner/Runner.entitlements @@ -0,0 +1,5 @@ + + + + + diff --git a/ios/clean-pods.sh b/ios/clean-pods.sh old mode 100644 new mode 100755 diff --git a/lib/core/app/application.dart b/lib/core/app/application.dart new file mode 100644 index 0000000..b0e2ca1 --- /dev/null +++ b/lib/core/app/application.dart @@ -0,0 +1,38 @@ +// Flutter imports: +import 'package:flutter/material.dart'; + +// Project imports: +import 'package:streamskit_mobile/core/app/config/base_local_data.dart'; +import 'package:streamskit_mobile/core/app/config/base_remote_data.dart'; +import 'package:streamskit_mobile/core/injection/injection_container.dart'; +import 'package:streamskit_mobile/core/util/path_helper.dart'; + +class Application { + /// [Production - Dev] + static Future initialAppLication() async { + try { + // Init dependency injection + configureDependencies(); + + // Prepare path cache + await PathHelper.createDirStreamOS(); + + // Init Hive + await BaseLocalData.initialBox(); + + // Configure Dio [Cookie, Retry, Transformer] + await BaseRemoteData.configureDio(); + } catch (error) { + debugPrint(error.toString()); + } + } + + ///Singleton factory + static final Application _instance = Application._internal(); + + factory Application() { + return _instance; + } + + Application._internal(); +} diff --git a/lib/features/auth/data/.gitkeep b/lib/core/app/colors/.gitkeep similarity index 100% rename from lib/features/auth/data/.gitkeep rename to lib/core/app/colors/.gitkeep diff --git a/lib/core/app/colors/app_color.dart b/lib/core/app/colors/app_color.dart new file mode 100644 index 0000000..33b5a64 --- /dev/null +++ b/lib/core/app/colors/app_color.dart @@ -0,0 +1,135 @@ +// Flutter imports: +import 'package:flutter/material.dart'; + +var colorBlack = const Color(0xFF121212); +var colorPrimaryBlack = const Color(0xFF121212); +var colorDarkGrey = const Color(0xFF657786); +var colorPrimary = const Color(0xFF1DA1F2); +var colorTitle = const Color(0xFF2C3D50); +var colorReds400 = Colors.red.shade400; +var colorBorderAvatar = const Color(0xff3e455b); + +var colorBlack1 = const Color(0xFF191414); +var colorPink = const Color(0xFFcd32d7); +var colorPurple2 = const Color(0xFFa9ade6); +var colorBlack2 = const Color(0xFF303234); +var colorRed = const Color(0xFFf11a42); +var colorBlue = const Color(0xFF1d51fe); +var colorPurple = const Color(0xFF7d60e5); +var colorHintText = const Color(0xFFa0a0a0); + +var colorHigh = Colors.redAccent; +var colorMedium = Colors.amber.shade700; +var colorLow = colorPrimary; +var colorCompleted = Colors.green; +var colorFailed = colorDarkGrey; +var colorActive = const Color(0xFF00D72F); +var colorGreenLight = const Color(0xFF009E60); +var colorAttendance = const Color(0xFF0CCF4C); + +var colorBlueGrey = const Color(0xFF455A64); +var colorBlueGreyIos = const Color(0xFF1C1F2E); + +var colorGreyWhite = const Color(0x4dE3E3E3); +var colorGreyWhite2 = const Color(0xFFE3E3E3); +var colorCaptionSearch = const Color(0xFFA3A3A3); + +Color colorDividerTimeline = const Color(0xFFC5D0CF); + +Color mC = Colors.grey.shade100; +Color mCL = Colors.white; +Color mCM = Colors.grey.shade200; +Color mCU = Colors.grey.shade300; +Color mCH = Colors.grey.shade400; +Color mGB = Colors.grey.shade500; +Color mGM = Colors.grey.shade700; +Color mGE = Colors.grey.shade800; +Color mGD = Colors.grey.shade900; +Color mCD = Colors.black.withOpacity(0.075); +Color mCC = Colors.green.withOpacity(0.65); +Color fCD = Colors.grey.shade700; +Color fCL = Colors.grey; + +class AppColors { + final Color primary; + final Color primaryLight; + final Color primaryDark; + final Color background; + final Color focusColor; + final Color unFocusColor; + final Color accent; + final Color disabled; + final Color error; + final Color divider; + final Color dividerBackgroundColor; + final Color header; + final Color button; + final Color contentText1; + final Color contentText2; + final Color subText1; + final Color subText2; + + const AppColors({ + required this.header, + required this.primary, + required this.primaryLight, + required this.primaryDark, + required this.background, + required this.focusColor, + required this.unFocusColor, + required this.accent, + required this.disabled, + required this.error, + required this.divider, + required this.dividerBackgroundColor, + required this.button, + required this.contentText1, + required this.contentText2, + required this.subText1, + required this.subText2, + }); + + factory AppColors.light() { + return AppColors( + header: colorBlack, + primary: colorPrimary, + primaryLight: mCL, + primaryDark: colorBlack, + background: Colors.white, + focusColor: Colors.green, + unFocusColor: Colors.grey, + accent: const Color(0xFF17c063), + disabled: Colors.black12, + error: const Color(0xFFFF7466), + divider: Colors.black26, + dividerBackgroundColor: Colors.black54, + button: const Color(0xFF657786), + contentText1: colorBlack, + contentText2: colorBlack, + subText1: mGD, + subText2: mGB, + ); + } + + factory AppColors.dark() { + return AppColors( + header: colorBlack, + primary: colorPrimary, + primaryLight: mCL, + primaryDark: colorBlack, + background: colorPrimaryBlack, + focusColor: Colors.green, + unFocusColor: Colors.grey, + accent: const Color(0xFF17c063), + disabled: Colors.black12, + error: const Color(0xFFFF7466), + divider: Colors.white24, + dividerBackgroundColor: Colors.black54, + button: const Color(0xFF657786), + contentText1: mCL, + contentText2: mC, + subText1: mCH, + subText2: mGB, + ); + } +} diff --git a/lib/core/app/config/base_local_data.dart b/lib/core/app/config/base_local_data.dart new file mode 100644 index 0000000..2b34dcb --- /dev/null +++ b/lib/core/app/config/base_local_data.dart @@ -0,0 +1,21 @@ +// Package imports: +import 'package:hive/hive.dart'; + +// Project imports: +import 'package:streamskit_mobile/core/app/constant/storage_keys.dart'; +import 'package:streamskit_mobile/core/util/path_helper.dart'; + +class BaseLocalData { + static Future initialBox() async { + String path = await PathHelper.localStoreDirStreamOS; + Hive.init(path); + await Hive.openBox(StorageKeys.boxSystem); + await openBoxApp(); + } + + static Future openBoxApp() async { + await Hive.openBox(StorageKeys.boxAuth); + await Hive.openBox(StorageKeys.boxUser); + await Hive.openBox(StorageKeys.boxLiveStreams); + } +} diff --git a/lib/core/app/config/base_remote_data.dart b/lib/core/app/config/base_remote_data.dart new file mode 100644 index 0000000..3ad7793 --- /dev/null +++ b/lib/core/app/config/base_remote_data.dart @@ -0,0 +1,295 @@ +// Dart imports: +import 'dart:async'; +import 'dart:convert' as convert; + +// Flutter imports: +import 'package:flutter/foundation.dart'; + +// Package imports: +import 'package:dio/dio.dart' as diox; + +// Project imports: +import 'package:streamskit_mobile/core/app/constant/constants.dart'; +import 'package:streamskit_mobile/core/types/http_status_code.dart'; +import 'package:streamskit_mobile/core/types/service_method.dart'; +import 'package:streamskit_mobile/core/util/dio_transformer.dart'; +import 'package:streamskit_mobile/core/util/logger.dart'; +import 'package:streamskit_mobile/core/util/stop_watch_api.dart'; + +class BaseRemoteData { + static diox.Dio dio = diox.Dio(diox.BaseOptions( + baseUrl: serviceBaseEndpoint, + connectTimeout: const Duration(milliseconds: connectTimeOut), + receiveTimeout: const Duration(milliseconds: receiveTimeOut), + sendTimeout: const Duration(milliseconds: receiveTimeOut), + )); // with default Options + + Future> downloadFile( + String url, String path, Function onReceive) async { + var response = await dio.download( + url, + path, + options: getOptions(), + onReceiveProgress: (received, total) { + onReceive(received, total); + }, + ); + return response; + } + + Future> postFormData( + String gateway, + diox.FormData formData, + ) async { + try { + var response = await dio.post( + gateway, + data: formData, + options: getOptions(), + onSendProgress: (send, total) {}, + onReceiveProgress: (received, total) {}, + ); + + return response; + } on diox.DioError catch (exception) { + return catchDioError(exception: exception, gateway: gateway); + } + } + + Future> putFormData( + String gateway, + diox.FormData formData, + ) async { + try { + var response = await dio.put( + gateway, + data: formData, + options: getOptions(), + onSendProgress: (send, total) {}, + onReceiveProgress: (received, total) {}, + ); + return response; + } on diox.DioError catch (exception) { + return catchDioError(exception: exception, gateway: gateway); + } + } + + Future> postRoute( + String gateway, Map body, + {String? query}) async { + try { + Map paramsObject = {}; + if (query != null) { + query.split('&').forEach((element) { + paramsObject[element.split('=')[0].toString()] = + element.split('=')[1].toString(); + }); + } + var response = kDebugMode + ? await StopWatch.stopWatchApi( + () => dio.post( + gateway, + data: convert.jsonEncode(body), + options: getOptions(), + queryParameters: query == null ? null : paramsObject, + ), + ServiceMethod.post.methodName, + gateway) + : await dio.post( + gateway, + data: convert.jsonEncode(body), + options: getOptions(), + queryParameters: query == null ? null : paramsObject, + ); + return response; + } on diox.DioError catch (exception) { + return catchDioError(exception: exception, gateway: gateway); + } + } + + Future> putRoute( + String gateway, + Map body, + ) async { + try { + var response = kDebugMode + ? await StopWatch.stopWatchApi( + () => dio.put( + gateway, + data: convert.jsonEncode(body), + options: getOptions(), + ), + ServiceMethod.put.methodName, + gateway) + : await dio.put( + gateway, + data: convert.jsonEncode(body), + options: getOptions(), + ); + return response; + } on diox.DioError catch (exception) { + return catchDioError(exception: exception, gateway: gateway); + } + } + + Future> patchRoute( + String gateway, { + String? query, + Map? body, + }) async { + try { + Map paramsObject = {}; + if (query != null) { + query.split('&').forEach((element) { + paramsObject[element.split('=')[0].toString()] = + element.split('=')[1].toString(); + }); + } + + var response = kDebugMode + ? await StopWatch.stopWatchApi( + () => dio.patch( + gateway, + data: body == null ? null : convert.jsonEncode(body), + options: getOptions(), + queryParameters: query == null ? null : paramsObject, + ), + ServiceMethod.patch.methodName, + gateway) + : await dio.patch( + gateway, + data: body == null ? null : convert.jsonEncode(body), + options: getOptions(), + queryParameters: query == null ? null : paramsObject, + ); + return response; + } on diox.DioError catch (exception) { + return catchDioError(exception: exception, gateway: gateway); + } + } + + Future> getRoute( + String gateway, { + String? params, + String? query, + }) async { + try { + Map paramsObject = {}; + if (query != null) { + query.split('&').forEach((element) { + paramsObject[element.split('=')[0].toString()] = + element.split('=')[1].toString(); + }); + } + + var response = kDebugMode + ? await StopWatch.stopWatchApi( + () => dio.get( + gateway, + options: getOptions(), + queryParameters: query == null ? null : paramsObject, + ), + ServiceMethod.get.methodName, + gateway) + : await dio.get( + gateway, + options: getOptions(), + queryParameters: query == null ? null : paramsObject, + ); + return response; + } on diox.DioError catch (exception) { + return catchDioError(exception: exception, gateway: gateway); + } + } + + Future> deleteRoute( + String gateway, { + String? params, + String? query, + Map? body, + diox.FormData? formData, + }) async { + try { + Map paramsObject = {}; + if (query != null) { + query.split('&').forEach((element) { + paramsObject[element.split('=')[0].toString()] = + element.split('=')[1].toString(); + }); + } + + var response = kDebugMode + ? await StopWatch.stopWatchApi( + () => dio.delete( + gateway, + data: formData ?? + (body == null ? null : convert.jsonEncode(body)), + options: getOptions(), + queryParameters: query == null ? null : paramsObject, + ), + ServiceMethod.delete.methodName, + gateway) + : await dio.delete( + gateway, + data: + formData ?? (body == null ? null : convert.jsonEncode(body)), + options: getOptions(), + queryParameters: query == null ? null : paramsObject, + ); + return response; + } on diox.DioError catch (exception) { + return catchDioError(exception: exception, gateway: gateway); + } + } + + diox.Response catchDioError({ + required diox.DioError exception, + required String gateway, + }) { + return diox.Response( + requestOptions: diox.RequestOptions(path: gateway), + data: null, + statusCode: StatusCode.badGateway, + statusMessage: "CATCH EXCEPTION DIO", + ); + } + + diox.Options getOptions() { + return diox.Options( + validateStatus: (status) { + // if (status == StatusCode.unauthorized && + // UserLocal().getAccessToken().isNotEmpty) { + // UserLocal().clearAccessToken(); + // showDialogLoading(); + // AppBloc.authBloc.add(LogOutEvent()); + // } + return true; + }, + headers: getHeaders(), + ); + } + + getHeaders() { + return { + 'Authorization': 'Bearer ', + 'Content-Type': 'application/json; charset=UTF-8', + 'Connection': 'keep-alive', + 'Accept': '*/*', + 'Accept-Encoding': 'gzip, deflate, br', + }; + } + + static Future configureDio() async { + // Transform + dio.transformer = DioTransformer(); // replace dio default transformer + } + + printEndpoint(String method, String endpoint) { + UtilLogger.log('${method.toUpperCase()}: $endpoint'); + } + + printResponse(diox.Response response) { + UtilLogger.log('StatusCode: ${response.statusCode}'); + UtilLogger.log('Body: ${response.data.toString()}'); + } +} diff --git a/lib/core/app/constant/constants.dart b/lib/core/app/constant/constants.dart new file mode 100644 index 0000000..7281ff8 --- /dev/null +++ b/lib/core/app/constant/constants.dart @@ -0,0 +1,14 @@ +// Remote Data +const String serviceBaseEndpoint = 'https://streamos.tk/'; + +// Utils +const double inchToDP = 160; + +// Delay times - miliseconds +const int delay100ms = 100; +const int delay200ms = 200; +const int durationDefaultAnimation = 300; +const int delay500ms = 500; +const int delayASecond = 1000; +const int connectTimeOut = 5000; +const int receiveTimeOut = 5000; diff --git a/test/features/auth/data/repositories/auth_repository_test.dart b/lib/core/app/constant/data_channel_events.dart similarity index 100% rename from test/features/auth/data/repositories/auth_repository_test.dart rename to lib/core/app/constant/data_channel_events.dart diff --git a/lib/core/app/constant/endpoints.dart b/lib/core/app/constant/endpoints.dart new file mode 100644 index 0000000..25dda54 --- /dev/null +++ b/lib/core/app/constant/endpoints.dart @@ -0,0 +1,4 @@ +class Endpoints { + // Auth + static const String signIn = 'auth/signIn'; +} diff --git a/test/features/auth/domain/usecases/login_with_email_test.dart b/lib/core/app/constant/socket_events.dart similarity index 100% rename from test/features/auth/domain/usecases/login_with_email_test.dart rename to lib/core/app/constant/socket_events.dart diff --git a/lib/core/app/constant/storage_keys.dart b/lib/core/app/constant/storage_keys.dart new file mode 100644 index 0000000..9e376e1 --- /dev/null +++ b/lib/core/app/constant/storage_keys.dart @@ -0,0 +1,13 @@ +class StorageKeys { + // Box Variables + static const String boxSystem = 'boxSystem'; + static const String boxAuth = 'boxAuth'; + static const String boxUser = 'boxUser'; + static const String boxLiveStreams = 'boxLiveStreams'; + + // Key in Box - Auth + static const String accessToken = 'accessToken'; + + // Key in Box - Live Streams + static const String liveStreamsKey = 'liveStreamsKey'; +} diff --git a/lib/core/app/constants.dart b/lib/core/app/constants.dart new file mode 100644 index 0000000..7c374c8 --- /dev/null +++ b/lib/core/app/constants.dart @@ -0,0 +1,7 @@ +const double inchToDP = 160; + +// Delay times - miliseconds +const int delay200ms = 200; +const int delay500ms = 500; +const int delayASecond = 1000; +const int delayHalfSecond = 500; diff --git a/lib/features/auth/domain/.gitkeep b/lib/core/app/lang/.gitkeep similarity index 100% rename from lib/features/auth/domain/.gitkeep rename to lib/core/app/lang/.gitkeep diff --git a/lib/features/auth/presentation/.gitkeep b/lib/core/app/themes/.gitkeep similarity index 100% rename from lib/features/auth/presentation/.gitkeep rename to lib/core/app/themes/.gitkeep diff --git a/lib/core/app/themes/box_shadow.dart b/lib/core/app/themes/box_shadow.dart new file mode 100644 index 0000000..657e06d --- /dev/null +++ b/lib/core/app/themes/box_shadow.dart @@ -0,0 +1,57 @@ +// Flutter imports: +import 'package:flutter/material.dart'; + +// Project imports: +import 'package:streamskit_mobile/core/app/colors/app_color.dart'; + +class BoxShadowStatic { + static List? boxShadow(BuildContext context) { + if (Theme.of(context).brightness == Brightness.dark) { + return [ + BoxShadow( + color: const Color(0xFF14171A).withOpacity(.65), + offset: const Offset(.75, .75), + blurRadius: .4, + ), + BoxShadow( + color: colorBlack.withOpacity(.25), + offset: const Offset(-.4, -.4), + blurRadius: .4, + ), + ]; + } else { + return [ + BoxShadow( + color: Colors.black.withOpacity(0.08), + blurRadius: 4, + offset: const Offset(1, 1), + ), + ]; + } + } + + static List? boxShadowActive(BuildContext context) { + if (Theme.of(context).brightness == Brightness.dark) { + return [ + BoxShadow( + color: Colors.black.withOpacity(.8), + offset: const Offset(1, 1), + blurRadius: 1, + ), + BoxShadow( + color: colorBlack.withOpacity(.35), + offset: const Offset(-1, -1), + blurRadius: 1, + ), + ]; + } else { + return [ + BoxShadow( + color: Colors.black.withOpacity(0.08), + blurRadius: 4, + offset: const Offset(1, 1), + ), + ]; + } + } +} diff --git a/lib/core/app/themes/themes.dart b/lib/core/app/themes/themes.dart new file mode 100644 index 0000000..ec0abe4 --- /dev/null +++ b/lib/core/app/themes/themes.dart @@ -0,0 +1,139 @@ +// Dart imports: +import 'dart:io'; + +// Flutter imports: +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +// Project imports: +import 'package:streamskit_mobile/core/app/colors/app_color.dart'; + +class AppTheme { + AppTheme({ + required this.data, + }); + + factory AppTheme.light({bool isChild = false}) { + final appColors = AppColors.light(); + final themeData = ThemeData( + useMaterial3: true, + pageTransitionsTheme: isChild + ? const PageTransitionsTheme(builders: { + TargetPlatform.iOS: ZoomPageTransitionsBuilder(), + TargetPlatform.android: ZoomPageTransitionsBuilder(), + }) + : const PageTransitionsTheme(builders: { + TargetPlatform.iOS: CupertinoPageTransitionsBuilder(), + TargetPlatform.android: CupertinoPageTransitionsBuilder(), + }), + brightness: Brightness.light, + primaryColor: appColors.primary, + primaryColorLight: appColors.primaryLight, + primaryColorDark: appColors.primaryDark, + focusColor: appColors.focusColor, + disabledColor: appColors.unFocusColor, + scaffoldBackgroundColor: appColors.background, + snackBarTheme: SnackBarThemeData( + backgroundColor: appColors.error, + behavior: SnackBarBehavior.floating, + ), + bottomNavigationBarTheme: BottomNavigationBarThemeData( + backgroundColor: appColors.background, + selectedItemColor: colorPrimary, + ), + appBarTheme: AppBarTheme( + scrolledUnderElevation: 0, + surfaceTintColor: appColors.background, + backgroundColor: appColors.background, + systemOverlayStyle: SystemUiOverlayStyle( + statusBarColor: Colors.transparent, + statusBarBrightness: Brightness.light == + (Platform.isAndroid ? Brightness.dark : Brightness.light) + ? Brightness.light + : Brightness.dark, + statusBarIconBrightness: Brightness.light == + (Platform.isAndroid ? Brightness.dark : Brightness.light) + ? Brightness.light + : Brightness.dark, + ), + iconTheme: IconThemeData( + color: appColors.contentText1, + ), + ), + dividerColor: appColors.divider, + colorScheme: const ColorScheme.light().copyWith( + surface: appColors.dividerBackgroundColor, + ), + ); + return AppTheme( + data: themeData, + ); + } + + factory AppTheme.dark({bool isChild = false}) { + final appColors = AppColors.dark(); + final themeData = ThemeData( + useMaterial3: true, + pageTransitionsTheme: isChild + ? const PageTransitionsTheme(builders: { + TargetPlatform.iOS: ZoomPageTransitionsBuilder(), + TargetPlatform.android: ZoomPageTransitionsBuilder(), + }) + : const PageTransitionsTheme(builders: { + TargetPlatform.iOS: CupertinoPageTransitionsBuilder(), + TargetPlatform.android: CupertinoPageTransitionsBuilder(), + }), + brightness: Brightness.dark, + primaryColor: appColors.primary, + primaryColorLight: appColors.primaryLight, + primaryColorDark: appColors.primaryDark, + focusColor: appColors.focusColor, + disabledColor: appColors.unFocusColor, + scaffoldBackgroundColor: appColors.background, + snackBarTheme: SnackBarThemeData( + backgroundColor: appColors.error, + behavior: SnackBarBehavior.floating, + ), + bottomNavigationBarTheme: BottomNavigationBarThemeData( + backgroundColor: appColors.background, + selectedItemColor: colorPrimary, + ), + appBarTheme: AppBarTheme( + scrolledUnderElevation: 0, + surfaceTintColor: appColors.background, + backgroundColor: appColors.background, + systemOverlayStyle: SystemUiOverlayStyle( + statusBarColor: Colors.transparent, + statusBarBrightness: Brightness.dark == + (Platform.isAndroid ? Brightness.dark : Brightness.light) + ? Brightness.light + : Brightness.dark, + statusBarIconBrightness: Brightness.dark == + (Platform.isAndroid ? Brightness.dark : Brightness.light) + ? Brightness.light + : Brightness.dark, + ), + iconTheme: IconThemeData( + color: appColors.contentText1, + ), + ), + textTheme: TextTheme( + displayLarge: TextStyle(color: appColors.header), + displayMedium: TextStyle(color: appColors.header), + bodyLarge: TextStyle(color: appColors.contentText1), + bodyMedium: TextStyle(color: appColors.contentText2), + titleMedium: TextStyle(color: appColors.subText1), + titleSmall: TextStyle(color: appColors.subText2), + ), + dividerColor: appColors.divider, + colorScheme: const ColorScheme.dark().copyWith( + surface: appColors.dividerBackgroundColor, + ), + ); + return AppTheme( + data: themeData, + ); + } + + final ThemeData data; +} diff --git a/lib/core/error/failure.dart b/lib/core/error/failure.dart new file mode 100644 index 0000000..22ea587 --- /dev/null +++ b/lib/core/error/failure.dart @@ -0,0 +1,14 @@ +// Package imports: +import 'package:equatable/equatable.dart'; + +abstract class Failure extends Equatable { + @override + List get props => []; +} + +// General failures +class CannotFoundItem extends Failure {} + +class CannotParseItem extends Failure {} + +class NullValue extends Failure {} diff --git a/lib/core/injection/injection_container.config.dart b/lib/core/injection/injection_container.config.dart new file mode 100644 index 0000000..c23357f --- /dev/null +++ b/lib/core/injection/injection_container.config.dart @@ -0,0 +1,74 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +// ************************************************************************** +// InjectableConfigGenerator +// ************************************************************************** + +// ignore_for_file: no_leading_underscores_for_library_prefixes + +// Package imports: +import 'package:get_it/get_it.dart' as _i1; +import 'package:injectable/injectable.dart' as _i2; + +// Project imports: +import '../../features/app/bloc/app_bloc.dart' as _i3; +import '../../features/auth/data/datasources/auth_local_datasource.dart' as _i4; +import '../../features/auth/data/repositories/auth_repository_impl.dart' as _i7; +import '../../features/auth/domain/repositories/auth_repository.dart' as _i6; +import '../../features/auth/domain/usecases/check_logined.dart' as _i8; +import '../../features/auth/domain/usecases/sign_in_with_social.dart' as _i10; +import '../../features/auth/domain/usecases/sign_out.dart' as _i11; +import '../../features/auth/presentation/bloc/auth_bloc.dart' as _i12; + +import '../../features/auth/data/datasources/auth_remote_datasource.dart' + as _i5; +import '../../features/home/data/datasources/local_live_stream_source.dart' + as _i9; +import '../../features/home/data/repositories/live_stream_repository_impl.dart' + as _i14; +import '../../features/home/domain/repositories/live_stream_repository.dart' + as _i13; +import '../../features/home/domain/usecases/get_list_live_streaming.dart' + as _i15; // ignore_for_file: unnecessary_lambdas + +// ignore_for_file: lines_longer_than_80_chars +/// initializes the registration of provided dependencies inside of [GetIt] +_i1.GetIt $initGetIt( + _i1.GetIt get, { + String? environment, + _i2.EnvironmentFilter? environmentFilter, +}) { + final gh = _i2.GetItHelper( + get, + environment, + environmentFilter, + ); + gh.factory<_i3.AppBloc>(() => _i3.AppBloc()); + gh.lazySingleton<_i4.AuthLocalDataSource>( + () => _i4.AuthLocalDataSourceImpl()); + gh.lazySingleton<_i5.AuthRemoteDataSource>( + () => _i5.AuthRemoteDataSourceImpl()); + gh.lazySingleton<_i6.AuthRepository>(() => _i7.AuthRepositoryImpl( + localData: get<_i4.AuthLocalDataSource>(), + remoteData: get<_i5.AuthRemoteDataSource>(), + )); + gh.lazySingleton<_i8.CheckLogined>( + () => _i8.CheckLogined(repository: get<_i6.AuthRepository>())); + gh.lazySingleton<_i9.LocalLiveStreamSource>( + () => _i9.LocalLiveStreamSourceImpl()); + gh.lazySingleton<_i10.SignInWithSocial>( + () => _i10.SignInWithSocial(repository: get<_i6.AuthRepository>())); + gh.lazySingleton<_i11.SignOut>( + () => _i11.SignOut(repository: get<_i6.AuthRepository>())); + gh.factory<_i12.AuthBloc>(() => _i12.AuthBloc( + get<_i10.SignInWithSocial>(), + get<_i8.CheckLogined>(), + get<_i11.SignOut>(), + )); + gh.lazySingleton<_i13.LiveStreamRepository>(() => + _i14.LiveStreamRepositoryImpl( + localData: get<_i9.LocalLiveStreamSource>())); + gh.lazySingleton<_i15.GetListLiveStreaming>(() => + _i15.GetListLiveStreaming(repository: get<_i13.LiveStreamRepository>())); + return get; +} diff --git a/lib/core/injection/injection_container.dart b/lib/core/injection/injection_container.dart new file mode 100644 index 0000000..57759d1 --- /dev/null +++ b/lib/core/injection/injection_container.dart @@ -0,0 +1,15 @@ +// Package imports: +import 'package:get_it/get_it.dart'; +import 'package:injectable/injectable.dart'; + +// Project imports: +import 'package:streamskit_mobile/core/injection/injection_container.config.dart'; + +final getIt = GetIt.instance; + +@InjectableInit( + initializerName: r'$initGetIt', // default + preferRelativeImports: true, // default + asExtension: false, // default +) +void configureDependencies() => $initGetIt(getIt); diff --git a/lib/core/navigator/app_navigator_observer.dart b/lib/core/navigator/app_navigator_observer.dart new file mode 100644 index 0000000..26bde16 --- /dev/null +++ b/lib/core/navigator/app_navigator_observer.dart @@ -0,0 +1,54 @@ +// Flutter imports: +import 'package:flutter/material.dart'; + +class AppNavigatorObserver extends NavigatorObserver { + static List routeNames = []; + + @override + void didPush(Route route, Route? previousRoute) { + super.didPush(route, previousRoute); + routeNames.add(route.settings.name); + + updateCurrentRouteToBloc(route.settings.name ?? ''); + } + + @override + void didPop(Route route, Route? previousRoute) { + super.didPop(route, previousRoute); + if (routeNames.length > 1) { + routeNames.removeLast(); + } + + updateCurrentRouteToBloc(route.settings.name ?? ''); + } + + @override + void didReplace({Route? newRoute, Route? oldRoute}) { + super.didReplace(); + routeNames[routeNames.length - 1] = newRoute?.settings.name ?? ''; + + updateCurrentRouteToBloc(newRoute?.settings.name ?? ''); + } + + @override + void didRemove(Route route, Route? previousRoute) { + super.didRemove(route, previousRoute); + int indexOfRoute = routeNames.indexOf(route.settings.name ?? ''); + if (indexOfRoute != -1) { + routeNames.removeRange(indexOfRoute, routeNames.length); + } + + updateCurrentRouteToBloc(route.settings.name ?? ''); + } + + void updateCurrentRouteToBloc(String routeName) {} + + // Static + static String? get currentRouteName => + routeNames.lastWhere((route) => route != null && route.isNotEmpty, + orElse: () => null); + + static void resetRoutes() { + routeNames = []; + } +} diff --git a/lib/core/navigator/app_pages.dart b/lib/core/navigator/app_pages.dart new file mode 100644 index 0000000..afc34c1 --- /dev/null +++ b/lib/core/navigator/app_pages.dart @@ -0,0 +1,196 @@ +// Flutter imports: +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; + +// Project imports: +import 'package:streamskit_mobile/core/navigator/app_navigator_observer.dart'; +import 'package:streamskit_mobile/core/navigator/app_routes.dart'; +import 'package:streamskit_mobile/core/navigator/scaffold_wrapper.dart'; +import 'package:streamskit_mobile/core/navigator/transition_route.dart'; +import 'package:streamskit_mobile/core/util/sizer_custom/sizer.dart'; +import 'package:streamskit_mobile/features/auth/presentation/screens/sign_in_screen.dart'; +import 'package:streamskit_mobile/features/home.dart'; +import 'package:streamskit_mobile/features/home/presentation/splash_screen.dart'; +import 'package:streamskit_mobile/features/profile/presentation/screens/edit_description_screen.dart'; +import 'package:streamskit_mobile/features/profile/presentation/screens/edit_phone_number_screen.dart'; +import 'package:streamskit_mobile/features/profile/presentation/screens/edit_profile_screen.dart'; +import 'package:streamskit_mobile/features/profile/presentation/screens/edit_username_screen.dart'; +import 'package:streamskit_mobile/features/profile/presentation/screens/setting_screen.dart'; + +class AppNavigator extends RouteObserver> { + static GlobalKey navigatorKey = GlobalKey(); + static GlobalKey navigatorAccountKey = GlobalKey(); + + Route getRoute(RouteSettings settings) { + Map? arguments = _getArguments(settings); + switch (settings.name) { + case Routes.rootRoute: + return _buildRoute( + settings, + const Home(), + ); + + case Routes.splashRoute: + return _buildRoute( + settings, + const SplashScreen(), + ); + + case Routes.authenticationRoute: + return _buildRoute( + settings, + const SignInScreen(), + ); + + case Routes.editDescriptionRoute: + return _buildRoute( + settings, + EditDescriptionScreen( + description: arguments?['description'], + ), + ); + + case Routes.editPhoneNumberRoute: + return _buildRoute( + settings, + EditPhoneNumberScreen( + phoneNumber: arguments?['phoneNumber'], + ), + ); + + case Routes.editUsernameRoute: + return _buildRoute( + settings, + EditUserNameScreen( + username: arguments?['username'], + ), + ); + + case Routes.editProfileRoute: + return _buildRoute( + settings, + const EditProfileScreen(), + ); + + case Routes.settingRoute: + return _buildRoute( + settings, + const SettingScreen(), + ); + + default: + return _buildRoute( + const RouteSettings(name: Routes.rootRoute), + const Home(), + ); + } + } + + _buildRoute( + RouteSettings routeSettings, + Widget builder, + ) { + return AppMaterialPageRoute( + builder: (context) => ScaffoldWrapper( + child: builder, + ), + settings: routeSettings, + ); + } + + static Future? push( + String route, { + Object? arguments, + }) { + late NavigatorState stateByContext; + + stateByContext = state; + + return stateByContext.pushNamed(route, arguments: arguments); + } + + static Future pushNamedAndRemoveUntil( + String route, { + Object? arguments, + }) { + if (route == Routes.rootRoute) { + AppNavigatorObserver.resetRoutes(); + } + + return state.pushNamedAndRemoveUntil( + route, + (route) => false, + arguments: arguments, + ); + } + + static Future? replaceWith( + String route, { + Map? arguments, + }) { + return state.pushReplacementNamed(route, arguments: arguments); + } + + static void popUntil(String routeName) { + state.popUntil((route) { + if (route.isFirst) return true; + + return route.settings.name == routeName; + }); + } + + static void pop({BuildContext? context}) { + if (SizerUtil.isTablet) { + if (context != null && + getRouteTablet(ModalRoute.of(context)?.settings.name ?? '')) { + if (!canAccountPop) return; + + accountState?.pop(); + + return; + } + } + + if (!canPop) return; + + state.pop(); + } + + static void navigatorAccountPop() { + if (!canAccountPop) return; + + accountState?.pop(); + } + + _getArguments(RouteSettings settings) { + return settings.arguments; + } + + static void navigatorAccountPopToRoot() { + accountState?.popUntil((route) => route.isFirst); + } + + static bool get canPop => state.canPop(); + + static bool get canAccountPop => accountState?.canPop() ?? true; + + static String? currentRoute() => AppNavigatorObserver.currentRouteName; + + static BuildContext? get context => navigatorKey.currentContext; + + static BuildContext? get accountContext => navigatorAccountKey.currentContext; + + static NavigatorState get state => navigatorKey.currentState!; + + static NavigatorState? get accountState => navigatorAccountKey.currentState; + + static bool getRouteTablet(String route) => [].contains(route); + + static bool shouldBeShowPopupInstrealOfScreen({required String route}) { + if (!SizerUtil.isTablet) return false; + + return popupInstrealOfScreen.contains(route); + } + + static List popupInstrealOfScreen = []; +} diff --git a/lib/core/navigator/app_routes.dart b/lib/core/navigator/app_routes.dart new file mode 100644 index 0000000..3065f1b --- /dev/null +++ b/lib/core/navigator/app_routes.dart @@ -0,0 +1,24 @@ +class Routes { + static const rootRoute = '/'; + static const splashRoute = '/splash'; + static const authenticationRoute = '/authentication'; + + // Chat + static const chatRoute = '/chat'; + + // Stream + static const streamRoute = '/stream'; + + //Profile + static const profileRoute = '/profile'; + static const editProfileRoute = '/editProfile'; + static const editUsernameRoute = '/editUsernameProfile'; + static const editDescriptionRoute = '/editDescriptionProfile'; + static const editPhoneNumberRoute = '/editPhoneNumber'; + + //Setting + static const settingRoute = '/setting'; + + //Search + static const searchRoute = '/search'; +} diff --git a/lib/core/navigator/bot_toast_navigator_observer.dart b/lib/core/navigator/bot_toast_navigator_observer.dart new file mode 100644 index 0000000..7c1629a --- /dev/null +++ b/lib/core/navigator/bot_toast_navigator_observer.dart @@ -0,0 +1,65 @@ +// Flutter imports: +import 'package:flutter/material.dart'; + +class BotToastNavigatorObserverProxy { + void Function(Route route, Route? previousRoute)? didPush; + void Function(Route? newRoute, Route? oldRoute)? didReplace; + void Function(Route route, Route? previousRoute)? didRemove; + void Function(Route route, Route? previousRoute)? didPop; + + BotToastNavigatorObserverProxy( + {this.didPush, this.didReplace, this.didRemove, this.didPop}); + + BotToastNavigatorObserverProxy.all(VoidCallback leavePageCallback) { + didPush = (_, __) => leavePageCallback(); + didReplace = (_, __) => leavePageCallback(); + didRemove = (_, __) => leavePageCallback(); + didPop = (_, __) => leavePageCallback(); + } +} + +class BotToastNavigatorObserver extends NavigatorObserver { + static final List _leavePageCallbacks = []; + + static void register( + BotToastNavigatorObserverProxy botToastNavigatorObserverProxy) { + _leavePageCallbacks.add(botToastNavigatorObserverProxy); + } + + static void unregister( + BotToastNavigatorObserverProxy botToastNavigatorObserverProxy) { + _leavePageCallbacks.remove(botToastNavigatorObserverProxy); + } + + @override + void didPush(Route route, Route? previousRoute) { + final copy = _leavePageCallbacks.toList(growable: false); + for (BotToastNavigatorObserverProxy observerProxy in copy) { + observerProxy.didPush?.call(route, previousRoute); + } + } + + @override + void didReplace({Route? newRoute, Route? oldRoute}) { + final copy = _leavePageCallbacks.toList(growable: false); + for (BotToastNavigatorObserverProxy observerProxy in copy) { + observerProxy.didReplace?.call(newRoute, oldRoute); + } + } + + @override + void didRemove(Route route, Route? previousRoute) { + final copy = _leavePageCallbacks.toList(growable: false); + for (BotToastNavigatorObserverProxy observerProxy in copy) { + observerProxy.didRemove?.call(route, previousRoute); + } + } + + @override + void didPop(Route route, Route? previousRoute) { + final copy = _leavePageCallbacks.toList(growable: false); + for (BotToastNavigatorObserverProxy observerProxy in copy) { + observerProxy.didPop?.call(route, previousRoute); + } + } +} diff --git a/lib/core/navigator/scaffold_wrapper.dart b/lib/core/navigator/scaffold_wrapper.dart new file mode 100644 index 0000000..3c34eae --- /dev/null +++ b/lib/core/navigator/scaffold_wrapper.dart @@ -0,0 +1,106 @@ +// Dart imports: +// ignore_for_file: deprecated_member_use + +import 'dart:async'; +import 'dart:io'; + +// Flutter imports: +import 'package:flutter/material.dart'; + +// Project imports: +import 'package:streamskit_mobile/core/navigator/app_pages.dart'; + +class ScaffoldWrapper extends StatefulWidget { + final Widget child; + + const ScaffoldWrapper({ + super.key, + required this.child, + }); + + @override + State createState() => _ScaffoldWrapperState(); +} + +class _ScaffoldWrapperState extends State + with WidgetsBindingObserver { + final List _ignoreRotateEvent = []; + + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addObserver(this); + } + + @override + void dispose() { + WidgetsBinding.instance.removeObserver(this); + super.dispose(); + } + + @override + void didChangeMetrics() { + if (_ignoreRotateEvent.contains(AppNavigator.currentRoute())) return; + } + + _hideKeyboard() { + if (FocusScope.of(context).hasFocus) { + FocusScope.of(context).unfocus(); + } + } + + @override + Widget build(BuildContext context) { + return OrientationBuilder(builder: (context, orientation) { + return SafeArea( + top: false, + bottom: false, + child: Platform.isIOS + ? _child + : WillPopScope( + onWillPop: _goBackward, + child: _child, + ), + ); + }); + } + + Widget get _child { + return Scaffold( + resizeToAvoidBottomInset: false, + extendBodyBehindAppBar: true, + extendBody: true, + body: _getBody, + ); + } + + Widget get _getBody { + return GestureDetector( + onHorizontalDragUpdate: Platform.isAndroid || + [].contains(AppNavigator.currentRoute()) + ? null + : (details) async { + // Cannot back when at ROOT, EDIT_PHOTO and Connecting Call + if (Platform.isIOS && ![].contains(AppNavigator.currentRoute())) { + //set the sensitivity for your ios gesture anywhere between 10-50 is good + + int sensitivity = 15; + + if (details.delta.dx > sensitivity) { + //SWIPE FROM RIGHT DETECTION + bool canBackward = await _goBackward(); + if (canBackward) { + AppNavigator.pop(); + } + } + } + }, + onTap: () => _hideKeyboard(), + child: widget.child, + ); + } + + Future _goBackward() async { + return true; + } +} diff --git a/lib/core/navigator/transition_route.dart b/lib/core/navigator/transition_route.dart new file mode 100644 index 0000000..2e08bde --- /dev/null +++ b/lib/core/navigator/transition_route.dart @@ -0,0 +1,39 @@ +// Dart imports: +import 'dart:io'; + +// Flutter imports: +import 'package:flutter/material.dart'; + +// Project imports: +import 'package:streamskit_mobile/core/app/constant/constants.dart'; + +class AppMaterialPageRoute extends MaterialPageRoute { + AppMaterialPageRoute({ + required super.builder, + required RouteSettings super.settings, + }); + + @override + Duration get transitionDuration => const Duration( + milliseconds: durationDefaultAnimation, + ); + + @override + @protected + bool get hasScopedWillPopCallback { + return Platform.isIOS ? true : false; + } + + @override + Widget buildTransitions(BuildContext context, Animation animation, + Animation secondaryAnimation, Widget child) { + final PageTransitionsTheme theme = Theme.of(context).pageTransitionsTheme; + return theme.buildTransitions( + this, + context, + animation, + secondaryAnimation, + child, + ); + } +} diff --git a/lib/core/network/url_launcher_helper.dart b/lib/core/network/url_launcher_helper.dart new file mode 100644 index 0000000..c9b823e --- /dev/null +++ b/lib/core/network/url_launcher_helper.dart @@ -0,0 +1,10 @@ +// Package imports: +import 'package:url_launcher/url_launcher.dart'; + +class UrlLauncherHelper { + static Future launchUrlString(Uri uri) async { + if (await canLaunchUrl(uri)) { + await launchUrl(uri, mode: LaunchMode.inAppWebView); + } else {} + } +} diff --git a/lib/core/types/http_status_code.dart b/lib/core/types/http_status_code.dart new file mode 100644 index 0000000..d9e66c4 --- /dev/null +++ b/lib/core/types/http_status_code.dart @@ -0,0 +1,42 @@ +class StatusCode { + static const int continueCode = 100; + static const int switchingProtocols = 101; + static const int ok = 200; + static const int created = 201; + static const int accepted = 202; + static const int notiAuthoritativeInfomation = 203; + static const int noContent = 204; + static const int resetContent = 205; + static const int partialContent = 206; + static const int multipleChoices = 300; + static const int movedPermanently = 301; + static const int found = 302; + static const int seeOther = 303; + static const int notModified = 304; + static const int useProxy = 305; + static const int temporaryRedirect = 307; + static const int badRequest = 400; + static const int unauthorized = 401; + static const int paymentRequired = 402; + static const int forbiden = 403; + static const int notFound = 404; + static const int methodNotAllowed = 405; + static const int notAcceptable = 406; + static const int proxyAuthenticationRequired = 407; + static const int requestTimeout = 408; + static const int conflict = 409; + static const int gone = 410; + static const int lengthRequired = 411; + static const int preconditionFailed = 412; + static const int requestEntityTooLarge = 413; + static const int requestUriTooLong = 414; + static const int unsupportedMediaType = 415; + static const int requestedRangeNotSatisfiable = 416; + static const int expectationFailed = 417; + static const int internalServerError = 500; + static const int notImplemented = 501; + static const int badGateway = 502; + + static List get validateStatus => + [unauthorized, notFound, internalServerError, badGateway]; +} diff --git a/lib/core/types/service_method.dart b/lib/core/types/service_method.dart new file mode 100644 index 0000000..75b1d64 --- /dev/null +++ b/lib/core/types/service_method.dart @@ -0,0 +1,11 @@ +enum ServiceMethod { + get, + post, + put, + patch, + delete, +} + +extension ServiceDescription on ServiceMethod { + String get methodName => name.toUpperCase(); +} diff --git a/lib/core/usecase/usecase.dart b/lib/core/usecase/usecase.dart new file mode 100644 index 0000000..58903fa --- /dev/null +++ b/lib/core/usecase/usecase.dart @@ -0,0 +1,28 @@ +// Package imports: +import 'package:dartz/dartz.dart'; +import 'package:equatable/equatable.dart'; + +// Project imports: +import 'package:streamskit_mobile/core/error/failure.dart'; + +abstract class UseCase { + Either call(Params params); +} + +abstract class UseCaseFuture { + Future> call(Params params); +} + +class NoParams extends Equatable { + @override + List get props => []; +} + +class Params extends Equatable { + final Object object; + + const Params({required this.object}); + + @override + List get props => [object]; +} diff --git a/lib/core/util/after_layout_mixin.dart b/lib/core/util/after_layout_mixin.dart new file mode 100644 index 0000000..88a0df6 --- /dev/null +++ b/lib/core/util/after_layout_mixin.dart @@ -0,0 +1,19 @@ +// Dart imports: +import 'dart:async'; + +// Flutter imports: +import 'package:flutter/material.dart'; + +mixin AfterLayoutMixin on State { + @override + void initState() { + super.initState(); + WidgetsBinding.instance.endOfFrame.then( + (_) { + if (mounted) afterFirstLayout(context); + }, + ); + } + + FutureOr afterFirstLayout(BuildContext context); +} diff --git a/lib/core/util/app_bars/appbar_none.dart b/lib/core/util/app_bars/appbar_none.dart new file mode 100644 index 0000000..52353c5 --- /dev/null +++ b/lib/core/util/app_bars/appbar_none.dart @@ -0,0 +1,33 @@ +// Dart imports: +import 'dart:io'; + +// Flutter imports: +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +AppBar appBarBrighnessDark({ + required BuildContext context, + Brightness? brightness, + Color? backgroundColor, +}) { + return AppBar( + toolbarHeight: 0.0, + systemOverlayStyle: SystemUiOverlayStyle( + statusBarColor: Colors.transparent, + statusBarBrightness: (brightness ?? Theme.of(context).brightness) == + (Platform.isAndroid ? Brightness.dark : Brightness.light) + ? Brightness.light + : Brightness.dark, + statusBarIconBrightness: (brightness ?? Theme.of(context).brightness) == + (Platform.isAndroid ? Brightness.dark : Brightness.light) + ? Brightness.light + : Brightness.dark, + ), + backgroundColor: backgroundColor ?? Colors.transparent, + surfaceTintColor: + backgroundColor ?? Theme.of(context).scaffoldBackgroundColor, + elevation: 0.0, + automaticallyImplyLeading: false, + centerTitle: true, + ); +} diff --git a/lib/core/util/common/pagination_list_view.dart b/lib/core/util/common/pagination_list_view.dart new file mode 100644 index 0000000..5b21999 --- /dev/null +++ b/lib/core/util/common/pagination_list_view.dart @@ -0,0 +1,108 @@ +// Flutter imports: +import 'package:flutter/cupertino.dart'; + +// Project imports: +import 'package:streamskit_mobile/core/app/constant/constants.dart'; +import 'package:streamskit_mobile/core/util/common/pull_to_refresh/pull_to_refresh.dart'; +import 'package:streamskit_mobile/core/util/sizer_custom/sizer.dart'; + +class PaginationListView extends StatefulWidget { + final int itemCount; + final Widget Function(BuildContext, int) itemBuilder; + final Widget childShimmer; + final Axis scrollDirection; + final bool isLoadMore; + final Function? callBackLoadMore; + final Function(Function)? callBackRefresh; + final EdgeInsetsGeometry? padding; + final ScrollPhysics? physics; + const PaginationListView({ + super.key, + required this.itemCount, + required this.itemBuilder, + required this.childShimmer, + this.callBackLoadMore, + this.callBackRefresh, + this.padding = EdgeInsets.zero, + this.scrollDirection = Axis.vertical, + this.isLoadMore = false, + this.physics, + }); + @override + State createState() => _PaginationListViewState(); +} + +class _PaginationListViewState extends State { + late final ScrollController _scrollController = ScrollController(); + final RefreshController _refreshController = + RefreshController(initialRefresh: false); + final Key linkKey = GlobalKey(); + + @override + void initState() { + super.initState(); + _scrollController.addListener( + () { + if (_scrollController.position.pixels >= + _scrollController.position.maxScrollExtent - 2.sp) { + if (widget.callBackLoadMore != null) { + widget.callBackLoadMore!(); + } + } + }, + ); + } + + @override + void dispose() { + _refreshController.dispose(); + _scrollController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return SmartRefresher( + controller: _refreshController, + enablePullUp: false, + enablePullDown: widget.callBackRefresh != null, + header: const WaterDropHeader( + refresh: CupertinoActivityIndicator(), + complete: SizedBox(), + completeDuration: Duration(milliseconds: delay200ms ~/ 2), + ), + onRefresh: () async { + if (widget.callBackRefresh != null) { + widget.callBackRefresh!(() => _refreshController.refreshCompleted()); + } else { + await Future.delayed(const Duration(milliseconds: delay500ms)); + _refreshController.refreshCompleted(); + } + }, + onLoading: () {}, + child: SizerUtil.isTablet + ? RawScrollbar( + controller: _scrollController, + thumbVisibility: false, + radius: Radius.circular(30.sp), + thickness: 3.sp, + child: _buildListView(), + ) + : _buildListView(), + ); + } + + ListView _buildListView() { + return ListView.builder( + controller: _scrollController, + padding: widget.padding!, + scrollDirection: widget.scrollDirection, + physics: widget.physics ?? const BouncingScrollPhysics(), + itemCount: widget.itemCount + (widget.isLoadMore ? 1 : 0), + itemBuilder: (context, index) => + widget.isLoadMore && index == widget.itemCount + ? widget.childShimmer + : widget.itemBuilder(context, index), + ); + } +} diff --git a/lib/core/util/common/pull_to_refresh/pull_to_refresh.dart b/lib/core/util/common/pull_to_refresh/pull_to_refresh.dart new file mode 100644 index 0000000..acca5ee --- /dev/null +++ b/lib/core/util/common/pull_to_refresh/pull_to_refresh.dart @@ -0,0 +1,11 @@ +export 'src/smart_refresher.dart'; +export 'src/indicator/classic_indicator.dart'; +export 'src/indicator/waterdrop_header.dart'; +export 'src/indicator/custom_indicator.dart'; +export 'src/internals/refresh_physics.dart'; +export "src/internals/indicator_wrap.dart"; +export 'src/indicator/link_indicator.dart'; +export 'src/indicator/material_indicator.dart'; +export 'src/indicator/bezier_indicator.dart'; +export 'src/indicator/twolevel_indicator.dart'; +export 'src/internals/refresh_localizations.dart'; diff --git a/lib/core/util/common/pull_to_refresh/src/indicator/bezier_indicator.dart b/lib/core/util/common/pull_to_refresh/src/indicator/bezier_indicator.dart new file mode 100644 index 0000000..ae5811d --- /dev/null +++ b/lib/core/util/common/pull_to_refresh/src/indicator/bezier_indicator.dart @@ -0,0 +1,487 @@ +// ignore_for_file: constant_identifier_names + +import 'package:flutter/material.dart' + hide RefreshIndicator, RefreshIndicatorState; + +// Dart imports: +import 'dart:math' as math; + +// Flutter imports: +import 'package:flutter/physics.dart'; + +// Project imports: +import 'package:streamskit_mobile/core/util/common/pull_to_refresh/pull_to_refresh.dart'; + +enum BezierDismissType { None, RectSpread, ScaleToCenter } + +enum BezierCircleType { Raidal, Progress } + +/// bezier container,if you need to implements indicator with bezier ,you can use consider about use this +/// this will add the bezier container effect +/// +/// See also: +/// +/// [BezierCircleHeader], bezier container +circle progress indicator +class BezierHeader extends RefreshIndicator { + final OffsetCallBack? onOffsetChange; + final ModeChangeCallBack? onModeChange; + final VoidFutureCallBack? readyRefresh, endRefresh; + final VoidCallback? onResetValue; + final Color? bezierColor; + // decide how to behave when bezier is ready to dismiss + final BezierDismissType dismissType; + final bool enableChildOverflow; + final Widget child; + // container height(not contain bezier) + final double rectHeight; + + const BezierHeader( + {super.key, + this.child = const Text(""), + this.onOffsetChange, + this.onModeChange, + this.readyRefresh, + this.enableChildOverflow = false, + this.endRefresh, + this.onResetValue, + this.dismissType = BezierDismissType.RectSpread, + this.rectHeight = 70, + this.bezierColor}) + : super( + refreshStyle: RefreshStyle.UnFollow, height: rectHeight); + + @override + State createState() { + return _BezierHeaderState(); + } +} + +class _BezierHeaderState extends RefreshIndicatorState + with TickerProviderStateMixin { + late AnimationController _beizerBounceCtl, _bezierDismissCtl; + + @override + void initState() { + _beizerBounceCtl = AnimationController( + vsync: this, lowerBound: -10, upperBound: 50, value: 0); + _bezierDismissCtl = AnimationController(vsync: this); + super.initState(); + } + + @override + void onOffsetChange(double offset) { + if (widget.onOffsetChange != null) { + widget.onOffsetChange!(offset); + } + if (!_beizerBounceCtl.isAnimating || (!floating)) { + _beizerBounceCtl.value = math.max(0, offset - widget.rectHeight); + } + } + + @override + void onModeChange(RefreshStatus? mode) { + if (widget.onModeChange != null) { + widget.onModeChange!(mode); + } + super.onModeChange(mode); + } + + @override + void dispose() { + _bezierDismissCtl.dispose(); + _beizerBounceCtl.dispose(); + super.dispose(); + } + + @override + Future readyToRefresh() { + final Simulation simulation = SpringSimulation( + const SpringDescription( + mass: 3.4, + stiffness: 10000.5, + damping: 6, + ), + _beizerBounceCtl.value, + 0, + 1000); + _beizerBounceCtl.animateWith(simulation); + if (widget.readyRefresh != null) { + return widget.readyRefresh!(); + } + return super.readyToRefresh(); + } + + @override + Future endRefresh() async { + if (widget.endRefresh != null) { + await widget.endRefresh!(); + } + return _bezierDismissCtl.animateTo(1.0, + duration: const Duration(milliseconds: 200)); + } + + @override + void resetValue() { + _bezierDismissCtl.reset(); + _beizerBounceCtl.value = 0; + if (widget.onResetValue != null) { + widget.onResetValue!(); + } + super.resetValue(); + } + + @override + Widget buildContent(BuildContext context, RefreshStatus? mode) { + return AnimatedBuilder( + builder: (_, __) { + return Stack( + children: [ + Positioned( + // ignore: sort_child_properties_last + child: AnimatedBuilder( + builder: (_, __) { + return ClipPath( + clipper: _BezierDismissPainter( + value: _bezierDismissCtl.value, + dismissType: widget.dismissType), + child: ClipPath( + clipper: _BezierPainter( + value: _beizerBounceCtl.value, + startOffsetY: widget.rectHeight), + child: Container( + height: widget.rectHeight + 30, + color: widget.bezierColor ?? + Theme.of(context).primaryColor, + ), + ), + ); + }, + animation: _bezierDismissCtl, + ), + bottom: -50, + top: 0, + left: 0, + right: 0, + ), + !widget.enableChildOverflow + ? ClipRect( + child: SizedBox( + height: (_beizerBounceCtl.isAnimating || + mode == RefreshStatus.refreshing + ? 0 + : math.max(0, _beizerBounceCtl.value)) + + widget.rectHeight, + child: widget.child, + ), + ) + : SizedBox( + height: (_beizerBounceCtl.isAnimating || + mode == RefreshStatus.refreshing + ? 0 + : math.max(0, _beizerBounceCtl.value)) + + widget.rectHeight, + child: widget.child, + ), + ], + ); + }, + animation: _beizerBounceCtl, + ); + } +} + +class _BezierDismissPainter extends CustomClipper { + final BezierDismissType? dismissType; + + final double? value; + + _BezierDismissPainter({this.dismissType, this.value}); + + @override + getClip(Size size) { + Path path = Path(); + if (dismissType == BezierDismissType.None || value == 0) { + path.moveTo(0, 0); + path.lineTo(size.width, 0); + path.lineTo(size.width, size.height); + path.lineTo(0, size.height); + path.lineTo(0, 0); + } else if (dismissType == BezierDismissType.RectSpread) { + Path path1 = Path(); + Path path2 = Path(); + double halfWidth = size.width / 2; + path1.moveTo(0, 0); + path1.lineTo(halfWidth - value! * halfWidth, 0); + path1.lineTo(halfWidth - value! * halfWidth, size.height); + path1.lineTo(0, size.height); + path1.lineTo(0, 0); + + path2.moveTo(size.width, 0); + path2.lineTo(halfWidth + value! * halfWidth, 0); + path2.lineTo(halfWidth + value! * halfWidth, size.height); + path2.lineTo(size.width, size.height); + path2.lineTo(size.width, 0); + path.addPath(path1, const Offset(0, 0)); + path.addPath(path2, const Offset(0, 0)); + } else { + final double maxExtent = + math.max(size.width, size.height) * (1.0 - value!); + final double centerX = size.width / 2; + final double centerY = size.height / 2; + path.addOval(Rect.fromCircle( + center: Offset(centerX, centerY), radius: maxExtent / 2)); + } + return path; + } + + @override + bool shouldReclip(_BezierDismissPainter oldClipper) { + return dismissType != oldClipper.dismissType || value != oldClipper.value; + } +} + +class _BezierPainter extends CustomClipper { + final double? startOffsetY; + + final double? value; + + _BezierPainter({this.value, this.startOffsetY}); + + @override + getClip(Size size) { + Path path = Path(); + path.lineTo(0, startOffsetY!); + path.quadraticBezierTo( + size.width / 2, startOffsetY! + value! * 2, size.width, startOffsetY!); + path.moveTo(size.width, startOffsetY!); + path.lineTo(size.width, 0); + path.lineTo(0, 0); + + return path; + } + + @override + bool shouldReclip(_BezierPainter oldClipper) { + return value != oldClipper.value; + } +} + +/// bezier + circle indicator,you can use this directly +/// +///simple usage +///```dart +///header: BezierCircleHeader( +///bezierColor: Colors.red, +///circleColor: Colors.amber, +///dismissType: BezierDismissType.ScaleToCenter, +///circleType: BezierCircleType.Raidal, +///) +///``` +class BezierCircleHeader extends StatefulWidget { + final Color? bezierColor; + // two style:radial or progress + final BezierCircleType circleType; + + final double rectHeight; + + final Color circleColor; + + final double circleRadius; + + final bool enableChildOverflow; + + final BezierDismissType dismissType; + + const BezierCircleHeader( + {super.key, + this.bezierColor, + this.rectHeight = 70, + this.circleColor = Colors.white, + this.enableChildOverflow = false, + this.dismissType = BezierDismissType.RectSpread, + this.circleType = BezierCircleType.Progress, + this.circleRadius = 12}); + + @override + State createState() { + return _BezierCircleHeaderState(); + } +} + +class _BezierCircleHeaderState extends State + with TickerProviderStateMixin { + RefreshStatus mode = RefreshStatus.idle; + late AnimationController _childMoveCtl; + late Tween _childMoveTween; + late AnimationController _dismissCtrl; + late Tween _disMissTween; + late AnimationController _radialCtrl; + + @override + void initState() { + _dismissCtrl = AnimationController(vsync: this); + _childMoveCtl = AnimationController(vsync: this); + _radialCtrl = AnimationController( + vsync: this, duration: const Duration(milliseconds: 500)); + _childMoveTween = AlignmentGeometryTween( + begin: Alignment.bottomCenter, end: Alignment.center); + _disMissTween = Tween( + begin: const Offset(0.0, 0.0), end: const Offset(0.0, 1.5)); + super.initState(); + } + + @override + void dispose() { + _dismissCtrl.dispose(); + _childMoveCtl.dispose(); + _radialCtrl.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return BezierHeader( + bezierColor: widget.bezierColor, + rectHeight: widget.rectHeight, + dismissType: widget.dismissType, + enableChildOverflow: widget.enableChildOverflow, + readyRefresh: () async { + await _childMoveCtl.animateTo(1.0, + duration: const Duration(milliseconds: 300)); + }, + onResetValue: () { + _dismissCtrl.value = 0; + _childMoveCtl.reset(); + }, + onModeChange: (m) { + mode = m; + if (m == RefreshStatus.refreshing) { + _radialCtrl.repeat(period: const Duration(milliseconds: 500)); + } + setState(() {}); + }, + endRefresh: () async { + _radialCtrl.reset(); + await _dismissCtrl.animateTo(1, + duration: const Duration(milliseconds: 550)); + }, + child: SlideTransition( + position: _disMissTween.animate(_dismissCtrl), + child: AlignTransition( + alignment: _childMoveCtl + .drive(_childMoveTween as Animatable), + child: widget.circleType == BezierCircleType.Progress + ? SizedBox( + height: widget.circleRadius * 2 + 5, + child: Stack( + children: [ + Center( + child: Container( + height: widget.circleRadius * 2, + decoration: BoxDecoration( + color: widget.circleColor, + shape: BoxShape.circle), + ), + ), + Center( + child: SizedBox( + height: widget.circleRadius * 2 + 5, + width: widget.circleRadius * 2 + 5, + child: CircularProgressIndicator( + valueColor: mode == RefreshStatus.refreshing + ? AlwaysStoppedAnimation(widget.circleColor) + : const AlwaysStoppedAnimation( + Colors.transparent), + strokeWidth: 2, + ), + ), + ) + ], + ), + ) + : AnimatedBuilder( + builder: (_, __) { + return SizedBox( + height: widget.circleRadius * 2, + child: CustomPaint( + painter: _RaidalPainter( + value: _radialCtrl.value, + circleColor: widget.circleColor, + circleRadius: widget.circleRadius, + refreshing: mode == RefreshStatus.refreshing), + ), + ); + }, + animation: _radialCtrl, + ), + ), + ), + ); + } +} + +class _RaidalPainter extends CustomPainter { + final double? value; + + final Color? circleColor; + + final double? circleRadius; + + final bool? refreshing; + + _RaidalPainter( + {this.value, this.circleColor, this.circleRadius, this.refreshing}); + + @override + void paint(Canvas canvas, Size size) { + Paint paint = Paint(); + paint.color = circleColor!; + paint.strokeWidth = 2; + paint.strokeCap = StrokeCap.round; + paint.style = PaintingStyle.stroke; + if (refreshing!) { + canvas.drawArc( + Rect.fromCircle( + center: Offset(size.width / 2, size.height / 2), + radius: circleRadius! + 3), + -math.pi / 2, + math.pi * 4, + false, + paint); + } + paint.style = PaintingStyle.fill; + canvas.drawArc( + Rect.fromCircle( + center: Offset(size.width / 2, size.height / 2), + radius: circleRadius!), + -math.pi / 2, + math.pi * 4, + true, + paint); + paint.color = const Color.fromRGBO(233, 233, 233, 0.8); + canvas.drawArc( + Rect.fromCircle( + center: Offset(size.width / 2, size.height / 2), + radius: circleRadius!), + -math.pi / 2, + math.pi * 4 * value!, + true, + paint); + paint.style = PaintingStyle.stroke; + if (refreshing!) { + canvas.drawArc( + Rect.fromCircle( + center: Offset(size.width / 2, size.height / 2), + radius: circleRadius! + 3), + -math.pi / 2, + math.pi * 4 * value!, + false, + paint); + } + } + + @override + bool shouldRepaint(_RaidalPainter oldDelegate) { + return value != oldDelegate.value; + } +} diff --git a/lib/core/util/common/pull_to_refresh/src/indicator/classic_indicator.dart b/lib/core/util/common/pull_to_refresh/src/indicator/classic_indicator.dart new file mode 100644 index 0000000..a4105cc --- /dev/null +++ b/lib/core/util/common/pull_to_refresh/src/indicator/classic_indicator.dart @@ -0,0 +1,311 @@ +/* + * Author: Jpeng + * Email: peng8350@gmail.com + * createTime:2018-05-14 17:39 + */ + +import 'package:flutter/material.dart' + hide RefreshIndicator, RefreshIndicatorState; + +// Flutter imports: +import 'package:flutter/cupertino.dart'; +import 'package:flutter/foundation.dart'; + +// Project imports: +import 'package:streamskit_mobile/core/util/common/pull_to_refresh/pull_to_refresh.dart'; + +/// direction that icon should place to the text +enum IconPosition { left, right, top, bottom } + +/// wrap child in outside,mostly use in add background color and padding +typedef OuterBuilder = Widget Function(Widget child); + +///the most common indicator,combine with a text and a icon +/// +/// See also: +/// +/// [ClassicFooter] +class ClassicHeader extends RefreshIndicator { + /// a builder for re wrap child,If you need to change the boxExtent or background,padding etc.you need outerBuilder to reWrap child + /// example: + /// ```dart + /// outerBuilder:(child){ + /// return Container( + /// color:Colors.red, + /// child:child + /// ); + /// } + /// ```` + /// In this example,it will help to add backgroundColor in indicator + final OuterBuilder? outerBuilder; + final String? releaseText, + idleText, + refreshingText, + completeText, + failedText, + canTwoLevelText; + final Widget? releaseIcon, + idleIcon, + refreshingIcon, + completeIcon, + failedIcon, + canTwoLevelIcon, + twoLevelView; + + /// icon and text middle margin + final double spacing; + final IconPosition iconPos; + + final TextStyle textStyle; + + const ClassicHeader({ + super.key, + RefreshStyle super.refreshStyle, + super.height, + super.completeDuration = const Duration(milliseconds: 600), + this.outerBuilder, + this.textStyle = const TextStyle(color: Colors.grey), + this.releaseText, + this.refreshingText, + this.canTwoLevelIcon, + this.twoLevelView, + this.canTwoLevelText, + this.completeText, + this.failedText, + this.idleText, + this.iconPos = IconPosition.left, + this.spacing = 15.0, + this.refreshingIcon, + this.failedIcon = const Icon(Icons.error, color: Colors.grey), + this.completeIcon = const Icon(Icons.done, color: Colors.grey), + this.idleIcon = const Icon(Icons.arrow_downward, color: Colors.grey), + this.releaseIcon = const Icon(Icons.refresh, color: Colors.grey), + }); + + @override + State createState() { + return _ClassicHeaderState(); + } +} + +class _ClassicHeaderState extends RefreshIndicatorState { + Widget _buildText(mode) { + RefreshString strings = + RefreshLocalizations.of(context)?.currentLocalization ?? + EnRefreshString(); + return Text( + mode == RefreshStatus.canRefresh + ? widget.releaseText ?? strings.canRefreshText! + : mode == RefreshStatus.completed + ? widget.completeText ?? strings.refreshCompleteText! + : mode == RefreshStatus.failed + ? widget.failedText ?? strings.refreshFailedText! + : mode == RefreshStatus.refreshing + ? widget.refreshingText ?? strings.refreshingText! + : mode == RefreshStatus.idle + ? widget.idleText ?? strings.idleRefreshText! + : mode == RefreshStatus.canTwoLevel + ? widget.canTwoLevelText ?? + strings.canTwoLevelText! + : "", + style: widget.textStyle); + } + + Widget _buildIcon(mode) { + Widget? icon = mode == RefreshStatus.canRefresh + ? widget.releaseIcon + : mode == RefreshStatus.idle + ? widget.idleIcon + : mode == RefreshStatus.completed + ? widget.completeIcon + : mode == RefreshStatus.failed + ? widget.failedIcon + : mode == RefreshStatus.canTwoLevel + ? widget.canTwoLevelIcon + : mode == RefreshStatus.canTwoLevel + ? widget.canTwoLevelIcon + : mode == RefreshStatus.refreshing + ? widget.refreshingIcon ?? + SizedBox( + width: 25.0, + height: 25.0, + child: defaultTargetPlatform == + TargetPlatform.iOS + ? const CupertinoActivityIndicator() + : const CircularProgressIndicator( + strokeWidth: 2.0), + ) + : widget.twoLevelView; + return icon ?? Container(); + } + + @override + bool needReverseAll() { + return false; + } + + @override + Widget buildContent(BuildContext context, RefreshStatus? mode) { + Widget textWidget = _buildText(mode); + Widget iconWidget = _buildIcon(mode); + List children = [iconWidget, textWidget]; + final Widget container = Wrap( + spacing: widget.spacing, + textDirection: widget.iconPos == IconPosition.left + ? TextDirection.ltr + : TextDirection.rtl, + direction: widget.iconPos == IconPosition.bottom || + widget.iconPos == IconPosition.top + ? Axis.vertical + : Axis.horizontal, + crossAxisAlignment: WrapCrossAlignment.center, + verticalDirection: widget.iconPos == IconPosition.bottom + ? VerticalDirection.up + : VerticalDirection.down, + alignment: WrapAlignment.center, + children: children, + ); + return widget.outerBuilder != null + ? widget.outerBuilder!(container) + : SizedBox( + height: widget.height, + child: Center(child: container), + ); + } +} + +///the most common indicator,combine with a text and a icon +/// +// See also: +// +// [ClassicHeader] +class ClassicFooter extends LoadIndicator { + final String? idleText, loadingText, noDataText, failedText, canLoadingText; + + /// a builder for re wrap child,If you need to change the boxExtent or background,padding etc.you need outerBuilder to reWrap child + /// example: + /// ```dart + /// outerBuilder:(child){ + /// return Container( + /// color:Colors.red, + /// child:child + /// ); + /// } + /// ```` + /// In this example,it will help to add backgroundColor in indicator + final OuterBuilder? outerBuilder; + + final Widget? idleIcon, loadingIcon, noMoreIcon, failedIcon, canLoadingIcon; + + /// icon and text middle margin + final double spacing; + + final IconPosition iconPos; + + final TextStyle textStyle; + + /// notice that ,this attrs only works for LoadStyle.ShowWhenLoading + final Duration completeDuration; + + const ClassicFooter({ + super.key, + super.onClick, + super.loadStyle, + super.height, + this.outerBuilder, + this.textStyle = const TextStyle(color: Colors.grey), + this.loadingText, + this.noDataText, + this.noMoreIcon, + this.idleText, + this.failedText, + this.canLoadingText, + this.failedIcon = const Icon(Icons.error, color: Colors.grey), + this.iconPos = IconPosition.left, + this.spacing = 15.0, + this.completeDuration = const Duration(milliseconds: 300), + this.loadingIcon, + this.canLoadingIcon = const Icon(Icons.autorenew, color: Colors.grey), + this.idleIcon = const Icon(Icons.arrow_upward, color: Colors.grey), + }); + + @override + State createState() { + return _ClassicFooterState(); + } +} + +class _ClassicFooterState extends LoadIndicatorState { + Widget _buildText(LoadStatus? mode) { + RefreshString strings = + RefreshLocalizations.of(context)?.currentLocalization ?? + EnRefreshString(); + return Text( + mode == LoadStatus.loading + ? widget.loadingText ?? strings.loadingText! + : LoadStatus.noMore == mode + ? widget.noDataText ?? strings.noMoreText! + : LoadStatus.failed == mode + ? widget.failedText ?? strings.loadFailedText! + : LoadStatus.canLoading == mode + ? widget.canLoadingText ?? strings.canLoadingText! + : widget.idleText ?? strings.idleLoadingText!, + style: widget.textStyle); + } + + Widget _buildIcon(LoadStatus? mode) { + Widget? icon = mode == LoadStatus.loading + ? widget.loadingIcon ?? + SizedBox( + width: 25.0, + height: 25.0, + child: defaultTargetPlatform == TargetPlatform.iOS + ? const CupertinoActivityIndicator() + : const CircularProgressIndicator(strokeWidth: 2.0), + ) + : mode == LoadStatus.noMore + ? widget.noMoreIcon + : mode == LoadStatus.failed + ? widget.failedIcon + : mode == LoadStatus.canLoading + ? widget.canLoadingIcon + : widget.idleIcon; + return icon ?? Container(); + } + + @override + Future endLoading() { + return Future.delayed(widget.completeDuration); + } + + @override + Widget buildContent(BuildContext context, LoadStatus? mode) { + Widget textWidget = _buildText(mode); + Widget iconWidget = _buildIcon(mode); + List children = [iconWidget, textWidget]; + final Widget container = Wrap( + spacing: widget.spacing, + textDirection: widget.iconPos == IconPosition.left + ? TextDirection.ltr + : TextDirection.rtl, + direction: widget.iconPos == IconPosition.bottom || + widget.iconPos == IconPosition.top + ? Axis.vertical + : Axis.horizontal, + crossAxisAlignment: WrapCrossAlignment.center, + verticalDirection: widget.iconPos == IconPosition.bottom + ? VerticalDirection.up + : VerticalDirection.down, + alignment: WrapAlignment.center, + children: children, + ); + return widget.outerBuilder != null + ? widget.outerBuilder!(container) + : SizedBox( + height: widget.height, + child: Center( + child: container, + ), + ); + } +} diff --git a/lib/core/util/common/pull_to_refresh/src/indicator/custom_indicator.dart b/lib/core/util/common/pull_to_refresh/src/indicator/custom_indicator.dart new file mode 100644 index 0000000..74ace90 --- /dev/null +++ b/lib/core/util/common/pull_to_refresh/src/indicator/custom_indicator.dart @@ -0,0 +1,192 @@ +// Flutter imports: +import 'package:flutter/widgets.dart'; + +// Project imports: +import '../internals/indicator_wrap.dart'; +import '../smart_refresher.dart'; + +/// custom header builder,you can use second paramter to know what header state is +typedef HeaderBuilder = Widget Function( + BuildContext context, RefreshStatus? mode); + +/// custom footer builder,you can use second paramter to know what footerr state is +typedef FooterBuilder = Widget Function(BuildContext context, LoadStatus? mode); + +/// a custom Indicator for header +/// +/// here is the very simple usage +/// +/// ```dart +/// CustomHeader( +/// builder: (context,mode){ +/// Widget body; +/// if(mode==RefreshStatus.idle){ +/// body = Text("pull down refresh"); +/// } +/// else if(mode==RefreshStatus.refreshing){ +/// body = CupertinoActivityIndicator(); +/// } +/// else if(mode==RefreshStatus.canRefresh){ +/// body = Text("release to refresh"); +/// } +/// else if(mode==RefreshStatus.completed){ +/// body = Text("refreshCompleted!"); +/// } +/// return Container( +/// height: 60.0, +/// child: Center( +/// child: body, +/// ), +/// ); +/// }, +/// ) +/// ``` +/// If you need to listen overScroll event do some animate,you should use [OnOffsetChange] callback in [SmartRefresher] +/// finally,If your indicator contain more complex animation and need to update frequently ,I suggest you extends [RefreshIndicator] to implements +/// +/// See also +/// +/// [CustomFooter], a custom Indicator for footer +class CustomHeader extends RefreshIndicator { + final HeaderBuilder builder; + + final VoidFutureCallBack? readyToRefresh; + + final VoidFutureCallBack? endRefresh; + + final OffsetCallBack? onOffsetChange; + + final ModeChangeCallBack? onModeChange; + + final VoidCallback? onResetValue; + + const CustomHeader({ + super.key, + required this.builder, + this.readyToRefresh, + this.endRefresh, + this.onOffsetChange, + this.onModeChange, + this.onResetValue, + super.height, + super.completeDuration = const Duration(milliseconds: 600), + RefreshStyle super.refreshStyle, + }); + + @override + State createState() { + return _CustomHeaderState(); + } +} + +class _CustomHeaderState extends RefreshIndicatorState { + @override + void onOffsetChange(double offset) { + if (widget.onOffsetChange != null) { + widget.onOffsetChange!(offset); + } + super.onOffsetChange(offset); + } + + @override + void onModeChange(RefreshStatus? mode) { + if (widget.onModeChange != null) { + widget.onModeChange!(mode); + } + super.onModeChange(mode); + } + + @override + Future readyToRefresh() { + if (widget.readyToRefresh != null) { + return widget.readyToRefresh!(); + } + return super.readyToRefresh(); + } + + @override + Future endRefresh() { + if (widget.endRefresh != null) { + return widget.endRefresh!(); + } + return super.endRefresh(); + } + + @override + Widget buildContent(BuildContext context, RefreshStatus? mode) { + return widget.builder(context, mode); + } +} + +/// a custom Indicator for footer,the usage I have put in [CustomHeader],same with that +/// See also +/// +/// [CustomHeader], a custom Indicator for header +class CustomFooter extends LoadIndicator { + final FooterBuilder builder; + + final OffsetCallBack? onOffsetChange; + + final ModeChangeCallBack? onModeChange; + + final VoidFutureCallBack? readyLoading; + + final VoidFutureCallBack? endLoading; + + const CustomFooter({ + super.key, + super.height, + this.onModeChange, + this.onOffsetChange, + this.readyLoading, + this.endLoading, + super.loadStyle, + required this.builder, + Function? onClick, + }) : super( + onClick: onClick as void Function()?); + + @override + State createState() { + return _CustomFooterState(); + } +} + +class _CustomFooterState extends LoadIndicatorState { + @override + void onOffsetChange(double offset) { + if (widget.onOffsetChange != null) { + widget.onOffsetChange!(offset); + } + super.onOffsetChange(offset); + } + + @override + void onModeChange(LoadStatus? mode) { + if (widget.onModeChange != null) { + widget.onModeChange!(mode); + } + super.onModeChange(mode); + } + + @override + Future readyToLoad() { + if (widget.readyLoading != null) { + return widget.readyLoading!(); + } + return super.readyToLoad(); + } + + @override + Future endLoading() { + if (widget.endLoading != null) { + return widget.endLoading!(); + } + return super.endLoading(); + } + + @override + Widget buildContent(BuildContext context, LoadStatus? mode) { + return widget.builder(context, mode); + } +} diff --git a/lib/core/util/common/pull_to_refresh/src/indicator/link_indicator.dart b/lib/core/util/common/pull_to_refresh/src/indicator/link_indicator.dart new file mode 100644 index 0000000..34ae953 --- /dev/null +++ b/lib/core/util/common/pull_to_refresh/src/indicator/link_indicator.dart @@ -0,0 +1,96 @@ +// Flutter imports: +import 'package:flutter/widgets.dart'; + +// Project imports: +import 'package:streamskit_mobile/core/util/common/pull_to_refresh/pull_to_refresh.dart'; + +/// enable header link other header place outside the viewport +class LinkHeader extends RefreshIndicator { + /// the key that widget outside viewport indicator + final Key linkKey; + + const LinkHeader( + {super.key, + required this.linkKey, + super.height = 0.0, + super.refreshStyle = null, + super.completeDuration = const Duration(milliseconds: 200)}); + + @override + State createState() { + return _LinkHeaderState(); + } +} + +class _LinkHeaderState extends RefreshIndicatorState { + @override + void resetValue() { + ((widget.linkKey as GlobalKey).currentState as RefreshProcessor) + .resetValue(); + } + + @override + Future endRefresh() { + return ((widget.linkKey as GlobalKey).currentState as RefreshProcessor) + .endRefresh(); + } + + @override + void onModeChange(RefreshStatus? mode) { + ((widget.linkKey as GlobalKey).currentState as RefreshProcessor) + .onModeChange(mode); + } + + @override + void onOffsetChange(double offset) { + ((widget.linkKey as GlobalKey).currentState as RefreshProcessor) + .onOffsetChange(offset); + } + + @override + Future readyToRefresh() { + return ((widget.linkKey as GlobalKey).currentState as RefreshProcessor) + .readyToRefresh(); + } + + @override + Widget buildContent(BuildContext context, RefreshStatus? mode) { + return Container(); + } +} + +/// enable footer link other footer place outside the viewport +class LinkFooter extends LoadIndicator { + /// the key that widget outside viewport indicator + final Key linkKey; + + const LinkFooter( + {super.key, + required this.linkKey, + super.height = 0.0, + super.loadStyle}); + + @override + State createState() { + return _LinkFooterState(); + } +} + +class _LinkFooterState extends LoadIndicatorState { + @override + void onModeChange(LoadStatus? mode) { + ((widget.linkKey as GlobalKey).currentState as LoadingProcessor) + .onModeChange(mode); + } + + @override + void onOffsetChange(double offset) { + ((widget.linkKey as GlobalKey).currentState as LoadingProcessor) + .onOffsetChange(offset); + } + + @override + Widget buildContent(BuildContext context, LoadStatus? mode) { + return Container(); + } +} diff --git a/lib/core/util/common/pull_to_refresh/src/indicator/material_indicator.dart b/lib/core/util/common/pull_to_refresh/src/indicator/material_indicator.dart new file mode 100644 index 0000000..d150efd --- /dev/null +++ b/lib/core/util/common/pull_to_refresh/src/indicator/material_indicator.dart @@ -0,0 +1,399 @@ +import 'package:flutter/material.dart' + hide RefreshIndicator, RefreshIndicatorState; + +// Project imports: +import 'package:streamskit_mobile/core/util/common/pull_to_refresh/pull_to_refresh.dart'; + +// How much the scroll's drag gesture can overshoot the RefreshIndicator's +// displacement; max displacement = _kDragSizeFactorLimit * displacement. +const double _kDragSizeFactorLimit = 1.5; + +/// mostly use flutter inner's RefreshIndicator +class MaterialClassicHeader extends RefreshIndicator { + /// see flutter RefreshIndicator documents,the meaning same with that + final String? semanticsLabel; + + /// see flutter RefreshIndicator documents,the meaning same with that + final String? semanticsValue; + + /// see flutter RefreshIndicator documents,the meaning same with that + final Color? color; + + /// Distance from the top when refreshing + final double distance; + + /// see flutter RefreshIndicator documents,the meaning same with that + final Color? backgroundColor; + + const MaterialClassicHeader({ + super.key, + super.height = 80.0, + this.semanticsLabel, + this.semanticsValue, + this.color, + super.offset, + this.distance = 50.0, + this.backgroundColor, + }) : super( + refreshStyle: RefreshStyle.Front, + ); + + @override + State createState() { + return _MaterialClassicHeaderState(); + } +} + +class _MaterialClassicHeaderState + extends RefreshIndicatorState + with TickerProviderStateMixin { + ScrollPosition? _position; + Animation? _positionFactor; + Animation? _valueColor; + late AnimationController _scaleFactor; + late AnimationController _positionController; + late AnimationController _valueAni; + + @override + void initState() { + _valueAni = AnimationController( + vsync: this, + value: 0.0, + lowerBound: 0.0, + upperBound: 1.0, + duration: const Duration(milliseconds: 500)); + _valueAni.addListener(() { + // frequently setState will decline the performance + if (mounted && _position!.pixels <= 0) setState(() {}); + }); + _positionController = AnimationController( + vsync: this, duration: const Duration(milliseconds: 300)); + _scaleFactor = AnimationController( + vsync: this, + value: 1.0, + lowerBound: 0.0, + upperBound: 1.0, + duration: const Duration(milliseconds: 300)); + _positionFactor = _positionController.drive(Tween( + begin: const Offset(0.0, -1.0), + end: Offset(0.0, widget.height / 44.0))); + super.initState(); + } + + @override + void didUpdateWidget(covariant MaterialClassicHeader oldWidget) { + // lambiengcode: implement didUpdateWidget + _position = Scrollable.of(context).position; + super.didUpdateWidget(oldWidget); + } + + @override + Widget buildContent(BuildContext context, RefreshStatus? mode) { + // lambiengcode: implement buildContent + return _buildIndicator(widget.backgroundColor ?? Colors.white); + } + + Widget _buildIndicator(Color outerColor) { + return SlideTransition( + position: _positionFactor!, + child: ScaleTransition( + scale: _scaleFactor, + child: Align( + alignment: Alignment.topCenter, + child: RefreshProgressIndicator( + semanticsLabel: widget.semanticsLabel ?? + MaterialLocalizations.of(context).refreshIndicatorSemanticLabel, + semanticsValue: widget.semanticsValue, + value: floating ? null : _valueAni.value, + valueColor: _valueColor, + backgroundColor: outerColor, + strokeWidth: 2.0, + ), + ), + ), + ); + } + + @override + void onOffsetChange(double offset) { + // lambiengcode: implement onOffsetChange + if (!floating) { + _valueAni.value = offset / configuration!.headerTriggerDistance; + _positionController.value = offset / configuration!.headerTriggerDistance; + } + } + + @override + void onModeChange(RefreshStatus? mode) { + // lambiengcode: implement onModeChange + if (mode == RefreshStatus.refreshing) { + _positionController.value = widget.distance / widget.height; + _scaleFactor.value = 1; + } + super.onModeChange(mode); + } + + @override + void resetValue() { + // lambiengcode: implement resetValue + _scaleFactor.value = 1.0; + _positionController.value = 0.0; + _valueAni.value = 0.0; + super.resetValue(); + } + + @override + void didChangeDependencies() { + final ThemeData theme = Theme.of(context); + _position = Scrollable.of(context).position; + _valueColor = _positionController.drive( + ColorTween( + begin: (widget.color ?? theme.primaryColor).withOpacity(0.0), + end: (widget.color ?? theme.primaryColor).withOpacity(1.0), + ).chain( + CurveTween(curve: const Interval(0.0, 1.0 / _kDragSizeFactorLimit))), + ); + super.didChangeDependencies(); + } + + @override + Future readyToRefresh() { + // lambiengcode: implement readyToRefresh + return _positionController.animateTo(widget.distance / widget.height); + } + + @override + Future endRefresh() { + // lambiengcode: implement endRefresh + return _scaleFactor.animateTo(0.0); + } + + @override + void dispose() { + // lambiengcode: implement dispose + _valueAni.dispose(); + _scaleFactor.dispose(); + _positionController.dispose(); + super.dispose(); + } +} + +/// attach the waterdrop effect to [MaterialClassicHeader] +class WaterDropMaterialHeader extends MaterialClassicHeader { + const WaterDropMaterialHeader({ + super.key, + super.semanticsLabel, + super.distance = 60.0, + super.offset, + super.semanticsValue, + Color super.color = Colors.white, + super.backgroundColor, + }) : super( + height: 80.0); + + @override + State createState() { + // lambiengcode: implement createState + return _WaterDropMaterialHeaderState(); + } +} + +class _WaterDropMaterialHeaderState extends _MaterialClassicHeaderState { + AnimationController? _bezierController; + bool _showWater = false; + + @override + void initState() { + // lambiengcode: implement initState + super.initState(); + _bezierController = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 500), + upperBound: 1.5, + lowerBound: 0.0, + value: 0.0); + _positionController = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 300), + upperBound: 1.0, + lowerBound: 0.0, + value: 0.0); + _positionFactor = _positionController.drive(Tween( + begin: const Offset(0.0, -0.5), end: const Offset(0.0, 1.5))); + } + + @override + void didChangeDependencies() { + // lambiengcode: implement didChangeDependencies + super.didChangeDependencies(); + final ThemeData theme = Theme.of(context); + _valueColor = _positionController.drive( + ColorTween( + begin: (widget.color ?? theme.primaryColor).withOpacity(0.0), + end: (widget.color ?? theme.primaryColor).withOpacity(1.0), + ).chain( + CurveTween(curve: const Interval(0.0, 1.0 / _kDragSizeFactorLimit))), + ); + } + + @override + Future readyToRefresh() { + // lambiengcode: implement readyToRefresh + _bezierController!.value = 1.01; + _showWater = true; + _bezierController!.animateTo(1.5, + curve: Curves.bounceOut, duration: const Duration(milliseconds: 550)); + return _positionController + .animateTo(widget.distance / widget.height, + curve: Curves.bounceOut, + duration: const Duration(milliseconds: 550)) + .then((_) { + _showWater = false; + }); + } + + @override + Future endRefresh() { + // lambiengcode: implement endRefresh + _showWater = false; + return super.endRefresh(); + } + + @override + void resetValue() { + // lambiengcode: implement resetValue + _bezierController!.reset(); + super.resetValue(); + } + + @override + void dispose() { + // lambiengcode: implement dispose + _bezierController!.dispose(); + super.dispose(); + } + + @override + void onOffsetChange(double offset) { + // lambiengcode: implement onOffsetChange + offset = offset > 80.0 ? 80.0 : offset; + + if (!floating) { + _bezierController!.value = + (offset / configuration!.headerTriggerDistance); + _valueAni.value = _bezierController!.value; + _positionController.value = _bezierController!.value * 0.3; + _scaleFactor.value = + offset < 40.0 ? 0.0 : (_bezierController!.value - 0.5) * 2 + 0.5; + } + } + + @override + Widget buildContent(BuildContext context, RefreshStatus? mode) { + // lambiengcode: implement buildContent + return SizedBox( + height: 100.0, + child: Stack( + children: [ + CustomPaint( + painter: _BezierPainter( + listener: _bezierController, + color: + widget.backgroundColor ?? Theme.of(context).primaryColor), + child: Container(), + ), + CustomPaint( + painter: _showWater + ? _WaterPainter( + ratio: widget.distance / widget.height, + color: widget.backgroundColor ?? + Theme.of(context).primaryColor, + listener: _positionFactor) + : null, + child: _buildIndicator( + widget.backgroundColor ?? Theme.of(context).primaryColor), + ) + ], + ), + ); + } +} + +class _WaterPainter extends CustomPainter { + final Color? color; + final Animation? listener; + + Offset get offset => listener!.value; + final double? ratio; + + _WaterPainter({this.color, this.listener, this.ratio}) + : super(repaint: listener); + + @override + void paint(Canvas canvas, Size size) { + // lambiengcode: implement paint + final Paint paint = Paint(); + paint.color = color!; + final Path path = Path(); + path.moveTo(size.width / 2 - 20.0, offset.dy * 100.0 + 20.0); + path.conicTo( + size.width / 2, + offset.dy * 100.0 - 70.0 * (ratio! - offset.dy), + size.width / 2 + 20.0, + offset.dy * 100.0 + 20.0, + 10.0 * (ratio! - offset.dy)); + canvas.drawPath(path, paint); + } + + @override + bool shouldRepaint(_WaterPainter oldDelegate) { + // lambiengcode: implement shouldRepaint + return this != oldDelegate || offset != oldDelegate.offset; + } +} + +class _BezierPainter extends CustomPainter { + final AnimationController? listener; + final Color? color; + + double get value => listener!.value; + + _BezierPainter({this.listener, this.color}) : super(repaint: listener); + + @override + void paint(Canvas canvas, Size size) { + // lambiengcode: implement paint + final double middleX = size.width / 2; + final Paint paint = Paint(); + paint.color = color!; + if (value < 0.5) { + final Path path = Path(); + path.moveTo(0.0, 0.0); + path.quadraticBezierTo(middleX, 70.0 * value, size.width, 0.0); + canvas.drawPath(path, paint); + } else if (value <= 1.0) { + final Path path = Path(); + final double offsetY = 60.0 * (value - 0.5) + 20.0; + path.moveTo(0.0, 0.0); + path.quadraticBezierTo(middleX + 40.0 * (value - 0.5), + 40.0 - 40.0 * value, middleX - 10.0, offsetY); + path.lineTo(middleX + 10.0, offsetY); + path.quadraticBezierTo( + middleX - 40.0 * (value - 0.5), 40.0 - 40.0 * value, size.width, 0.0); + path.moveTo(size.width, 0.0); + path.lineTo(0.0, 0.0); + canvas.drawPath(path, paint); + } else { + final Path path = Path(); + path.moveTo(0.0, 0.0); + path.conicTo(middleX, 60.0 * (1.5 - value), size.width, 0.0, 5.0); + canvas.drawPath(path, paint); + } + } + + @override + bool shouldRepaint(_BezierPainter oldDelegate) { + // lambiengcode: implement shouldRepaint + return this != oldDelegate || oldDelegate.value != value; + } +} diff --git a/lib/core/util/common/pull_to_refresh/src/indicator/twolevel_indicator.dart b/lib/core/util/common/pull_to_refresh/src/indicator/twolevel_indicator.dart new file mode 100644 index 0000000..1fd51b1 --- /dev/null +++ b/lib/core/util/common/pull_to_refresh/src/indicator/twolevel_indicator.dart @@ -0,0 +1,169 @@ +// Flutter imports: +import 'package:flutter/material.dart'; + +// Project imports: +import 'package:streamskit_mobile/core/util/common/pull_to_refresh/pull_to_refresh.dart'; + +enum TwoLevelDisplayAlignment { fromTop, fromCenter, fromBottom } + +/// this header help you implements twoLevel function easyily, +/// the behaviour just like TaoBao,XieCheng(携程) App TwoLevel +/// +/// just a example +/// +/// ```dart +/// +///TwoLevelHeader( +/// textStyle: TextStyle(color: Colors.white), +/// displayAlignment: TwoLevelDisplayAlignment.fromTop, +/// decoration: BoxDecoration( +/// image: DecorationImage( +/// image: AssetImage("images/secondfloor.jpg"), +/// fit: BoxFit.cover, +/// // 很重要的属性,这会影响你打开二楼和关闭二楼的动画效果 +/// alignment: Alignment.topCenter), +///), +///twoLevelWidget: Container( +/// decoration: BoxDecoration( +/// image: DecorationImage( +/// image: AssetImage("images/secondfloor.jpg"), +// 很重要的属性,这会影响你打开二楼和关闭二楼的动画效果,关联到TwoLevelHeader,如果背景一致的情况,请设置相同 +/// alignment: Alignment.topCenter, +/// fit: BoxFit.cover), +/// ), +/// Container( +/// height: 60.0, +/// child: GestureDetector( +/// child: Icon( +/// Icons.arrow_back_ios, +/// color: Colors.white, +/// ), +/// onTap: () { +/// SmartRefresher.of(context).controller.twoLevelComplete(); +/// }, +/// ), +/// alignment: Alignment.bottomLeft, +///), +///), +///); +/// +/// ``` +class TwoLevelHeader extends StatelessWidget { + /// this attr mostly put image or color + final BoxDecoration? decoration; + + /// the content in TwoLevel,display in (twoLevelOpening,closing,TwoLeveling state) + final Widget? twoLevelWidget; + + /// fromTop use with RefreshStyle.Behind,from bottom use with Follow Style + final TwoLevelDisplayAlignment displayAlignment; + // the following is the same with ClassicHeader + final String? releaseText, + idleText, + refreshingText, + completeText, + failedText, + canTwoLevelText; + + final Widget? releaseIcon, + idleIcon, + refreshingIcon, + completeIcon, + failedIcon, + canTwoLevelIcon; + + /// icon and text middle margin + final double spacing; + final IconPosition iconPos; + + final TextStyle textStyle; + + final double height; + final Duration completeDuration; + + const TwoLevelHeader( + {super.key, + this.height = 80.0, + this.decoration, + this.displayAlignment = TwoLevelDisplayAlignment.fromBottom, + this.completeDuration = const Duration(milliseconds: 600), + this.textStyle = const TextStyle(color: Color(0xff555555)), + this.releaseText, + this.refreshingText, + this.canTwoLevelIcon, + this.canTwoLevelText, + this.completeText, + this.failedText, + this.idleText, + this.iconPos = IconPosition.left, + this.spacing = 15.0, + this.refreshingIcon, + this.failedIcon = const Icon(Icons.error, color: Colors.grey), + this.completeIcon = const Icon(Icons.done, color: Colors.grey), + this.idleIcon = const Icon(Icons.arrow_downward, color: Colors.grey), + this.releaseIcon = const Icon(Icons.refresh, color: Colors.grey), + this.twoLevelWidget}); + + @override + Widget build(BuildContext context) { + // lambiengcode: implement build + return ClassicHeader( + refreshStyle: displayAlignment == TwoLevelDisplayAlignment.fromBottom + ? RefreshStyle.Follow + : RefreshStyle.Behind, + height: height, + refreshingIcon: refreshingIcon, + refreshingText: refreshingText, + releaseIcon: releaseIcon, + releaseText: releaseText, + completeDuration: completeDuration, + canTwoLevelIcon: canTwoLevelIcon, + canTwoLevelText: canTwoLevelText, + failedIcon: failedIcon, + failedText: failedText, + idleIcon: idleIcon, + idleText: idleText, + completeIcon: completeIcon, + completeText: completeText, + spacing: spacing, + textStyle: textStyle, + iconPos: iconPos, + outerBuilder: (child) { + final RefreshStatus? mode = + SmartRefresher.of(context)!.controller.headerStatus; + final bool isTwoLevel = (mode == RefreshStatus.twoLevelClosing || + mode == RefreshStatus.twoLeveling || + mode == RefreshStatus.twoLevelOpening); + if (displayAlignment == TwoLevelDisplayAlignment.fromBottom) { + return Container( + decoration: !isTwoLevel + ? (decoration ?? const BoxDecoration(color: Colors.redAccent)) + : null, + height: SmartRefresher.ofState(context)!.viewportExtent, + alignment: isTwoLevel ? null : Alignment.bottomCenter, + child: isTwoLevel + ? twoLevelWidget + : Padding( + padding: const EdgeInsets.only(bottom: 15), + child: child, + ), + ); + } else { + return Container( + child: isTwoLevel + ? twoLevelWidget + : Container( + decoration: !isTwoLevel + ? (decoration ?? + const BoxDecoration(color: Colors.redAccent)) + : null, + alignment: Alignment.bottomCenter, + padding: const EdgeInsets.only(bottom: 15), + child: child, + ), + ); + } + }, + ); + } +} diff --git a/lib/core/util/common/pull_to_refresh/src/indicator/waterdrop_header.dart b/lib/core/util/common/pull_to_refresh/src/indicator/waterdrop_header.dart new file mode 100644 index 0000000..ee76314 --- /dev/null +++ b/lib/core/util/common/pull_to_refresh/src/indicator/waterdrop_header.dart @@ -0,0 +1,278 @@ +/* + * Author: Jpeng + * Email: peng8350@gmail.com + * Time: 2019/5/5 下午2:37 + */ + +// Dart imports: +import 'dart:async'; + +// Flutter imports: +import 'package:flutter/cupertino.dart'; +import 'package:flutter/foundation.dart'; + +// Project imports: +import 'package:streamskit_mobile/core/util/common/pull_to_refresh/pull_to_refresh.dart'; + +import 'package:flutter/material.dart' + hide RefreshIndicatorState, RefreshIndicator; + +/// QQ ios refresh header effect +class WaterDropHeader extends RefreshIndicator { + /// refreshing content + final Widget? refresh; + + /// complete content + final Widget? complete; + + /// failed content + final Widget? failed; + + /// idle Icon center in waterCircle + final Widget idleIcon; + + /// waterDrop color,default grey + final Color waterDropColor; + + const WaterDropHeader({ + super.key, + this.refresh, + this.complete, + super.completeDuration = const Duration(milliseconds: 600), + this.failed, + this.waterDropColor = Colors.grey, + this.idleIcon = const Icon( + Icons.autorenew, + size: 15, + color: Colors.white, + ), + }) : super( + height: 60.0, + refreshStyle: RefreshStyle.UnFollow); + + @override + State createState() { + // lambiengcode: implement createState + return _WaterDropHeaderState(); + } +} + +class _WaterDropHeaderState extends RefreshIndicatorState + with TickerProviderStateMixin { + AnimationController? _animationController; + late AnimationController _dismissCtl; + + @override + void onOffsetChange(double offset) { + // lambiengcode: implement onOffsetChange + final double realOffset = + offset - 44.0; //55.0 mean circleHeight(24) + originH (20) in Painter + // when readyTorefresh + if (!_animationController!.isAnimating) { + _animationController!.value = realOffset; + } + } + + @override + Future readyToRefresh() { + // lambiengcode: implement readyToRefresh + _dismissCtl.animateTo(0.0); + return _animationController!.animateTo(0.0); + } + + @override + void initState() { + // lambiengcode: implement initState + _dismissCtl = AnimationController( + vsync: this, duration: const Duration(milliseconds: 400), value: 1.0); + _animationController = AnimationController( + vsync: this, + lowerBound: 0.0, + upperBound: 50.0, + duration: const Duration(milliseconds: 400)); + super.initState(); + } + + @override + bool needReverseAll() { + // lambiengcode: implement needReverseAll + return false; + } + + @override + Widget buildContent(BuildContext context, RefreshStatus? mode) { + // lambiengcode: implement buildContent + Widget? child; + if (mode == RefreshStatus.refreshing) { + child = widget.refresh ?? + SizedBox( + width: 25.0, + height: 25.0, + child: defaultTargetPlatform == TargetPlatform.iOS + ? const CupertinoActivityIndicator() + : const CircularProgressIndicator(strokeWidth: 2.0), + ); + } else if (mode == RefreshStatus.completed) { + child = widget.complete ?? + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon( + Icons.done, + color: Colors.grey, + ), + Container( + width: 15.0, + ), + Text( + (RefreshLocalizations.of(context)?.currentLocalization ?? + EnRefreshString()) + .refreshCompleteText!, + style: const TextStyle(color: Colors.grey), + ) + ], + ); + } else if (mode == RefreshStatus.failed) { + child = widget.failed ?? + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon( + Icons.close, + color: Colors.grey, + ), + Container( + width: 15.0, + ), + Text( + (RefreshLocalizations.of(context)?.currentLocalization ?? + EnRefreshString()) + .refreshFailedText!, + style: const TextStyle(color: Colors.grey)) + ], + ); + } else if (mode == RefreshStatus.idle || mode == RefreshStatus.canRefresh) { + return FadeTransition( + opacity: _dismissCtl, + child: SizedBox( + height: 60.0, + child: Stack( + children: [ + RotatedBox( + quarterTurns: + Scrollable.of(context).axisDirection == AxisDirection.up + ? 10 + : 0, + child: CustomPaint( + painter: _QqPainter( + color: widget.waterDropColor, + listener: _animationController, + ), + child: Container( + height: 60.0, + ), + ), + ), + Container( + alignment: + Scrollable.of(context).axisDirection == AxisDirection.up + ? Alignment.bottomCenter + : Alignment.topCenter, + margin: + Scrollable.of(context).axisDirection == AxisDirection.up + ? const EdgeInsets.only(bottom: 12.0) + : const EdgeInsets.only(top: 12.0), + child: widget.idleIcon, + ) + ], + ), + )); + } + return SizedBox( + height: 60.0, + child: Center( + child: child, + ), + ); + } + + @override + void resetValue() { + // lambiengcode: implement resetValue + _animationController!.reset(); + _dismissCtl.value = 1.0; + } + + @override + void dispose() { + // lambiengcode: implement dispose + _dismissCtl.dispose(); + _animationController!.dispose(); + super.dispose(); + } +} + +class _QqPainter extends CustomPainter { + final Color? color; + final Animation? listener; + + double get value => listener!.value; + final Paint painter = Paint(); + + _QqPainter({this.color, this.listener}) : super(repaint: listener); + + @override + void paint(Canvas canvas, Size size) { + const double originH = 20.0; + final double middleW = size.width / 2; + + const double circleSize = 12.0; + + const double scaleRatio = 0.1; + + final double offset = value; + + painter.color = color!; + canvas.drawCircle(Offset(middleW, originH), circleSize, painter); + Path path = Path(); + path.moveTo(middleW - circleSize, originH); + + //drawleft + path.cubicTo( + middleW - circleSize, + originH, + middleW - circleSize + value * scaleRatio, + originH + offset / 5, + middleW - circleSize + value * scaleRatio * 2, + originH + offset); + path.lineTo( + middleW + circleSize - value * scaleRatio * 2, originH + offset); + //draw right + path.cubicTo( + middleW + circleSize - value * scaleRatio * 2, + originH + offset, + middleW + circleSize - value * scaleRatio, + originH + offset / 5, + middleW + circleSize, + originH); + //draw upper circle + path.moveTo(middleW - circleSize, originH); + path.arcToPoint(Offset(middleW + circleSize, originH), + radius: const Radius.circular(circleSize)); + + //draw lowwer circle + path.moveTo( + middleW + circleSize - value * scaleRatio * 2, originH + offset); + path.arcToPoint( + Offset(middleW - circleSize + value * scaleRatio * 2, originH + offset), + radius: Radius.circular(value * scaleRatio)); + path.close(); + canvas.drawPath(path, painter); + } + + @override + bool shouldRepaint(CustomPainter oldDelegate) { + // lambiengcode: implement shouldRepaint + return oldDelegate != this; + } +} diff --git a/lib/core/util/common/pull_to_refresh/src/internals/indicator_wrap.dart b/lib/core/util/common/pull_to_refresh/src/internals/indicator_wrap.dart new file mode 100644 index 0000000..f010e53 --- /dev/null +++ b/lib/core/util/common/pull_to_refresh/src/internals/indicator_wrap.dart @@ -0,0 +1,757 @@ +// ignore_for_file: invalid_use_of_protected_member, invalid_use_of_visible_for_testing_member + +// Dart imports: +import 'dart:math' as math; + +// Flutter imports: +import 'package:flutter/rendering.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter/widgets.dart'; +import 'package:streamskit_mobile/core/util/common/pull_to_refresh/src/internals/slivers.dart'; + +// Project imports: +import 'package:streamskit_mobile/core/util/common/pull_to_refresh/src/smart_refresher.dart'; + +typedef VoidFutureCallBack = Future Function(); + +typedef OffsetCallBack = void Function(double offset); + +typedef ModeChangeCallBack = void Function(T? mode); + +/// a widget implements ios pull down refresh effect and Android material RefreshIndicator overScroll effect +abstract class RefreshIndicator extends StatefulWidget { + /// refresh display style + final RefreshStyle? refreshStyle; + + /// the visual extent indicator + final double height; + + //layout offset + final double offset; + + /// the stopped time when refresh complete or fail + final Duration completeDuration; + + const RefreshIndicator({ + super.key, + this.height = 60.0, + this.offset = 0.0, + this.completeDuration = const Duration(milliseconds: 500), + this.refreshStyle = RefreshStyle.Follow, + }); +} + +/// a widget implements pull up load +abstract class LoadIndicator extends StatefulWidget { + /// load more display style + final LoadStyle loadStyle; + + /// the visual extent indicator + final double height; + + /// callback when user click footer + final VoidCallback? onClick; + + const LoadIndicator({ + super.key, + this.onClick, + this.loadStyle = LoadStyle.ShowAlways, + this.height = 60.0, + }); +} + +/// Internal Implementation of Head Indicator +/// +/// you can extends RefreshIndicatorState for custom header,if you want to active complex animation effect +/// +/// here is the most simple example +/// +/// ```dart +/// +/// class RunningHeaderState extends RefreshIndicatorState +/// with TickerProviderStateMixin { +/// AnimationController _scaleAnimation; +/// AnimationController _offsetController; +/// Tween offsetTween; +/// +/// @override +/// void initState() { +/// // lambiengcode: implement initState +/// _scaleAnimation = AnimationController(vsync: this); +/// _offsetController = AnimationController( +/// vsync: this, duration: Duration(milliseconds: 1000)); +/// offsetTween = Tween(end: Offset(0.6, 0.0), begin: Offset(0.0, 0.0)); +/// super.initState(); +/// } +/// +/// @override +/// void onOffsetChange(double offset) { +/// // lambiengcode: implement onOffsetChange +/// if (!floating) { +/// _scaleAnimation.value = offset / 80.0; +/// } +/// super.onOffsetChange(offset); +/// } +/// +/// @override +/// void resetValue() { +/// // lambiengcode: implement handleModeChange +/// _scaleAnimation.value = 0.0; +/// _offsetController.value = 0.0; +/// } +/// +/// @override +/// void dispose() { +/// // lambiengcode: implement dispose +/// _scaleAnimation.dispose(); +/// _offsetController.dispose(); +/// super.dispose(); +/// } +/// +/// @override +/// Future endRefresh() { +/// // lambiengcode: implement endRefresh +/// return _offsetController.animateTo(1.0).whenComplete(() {}); +/// } +/// +/// @override +/// Widget buildContent(BuildContext context, RefreshStatus mode) { +/// // lambiengcode: implement buildContent +/// return SlideTransition( +/// child: ScaleTransition( +/// child: (mode != RefreshStatus.idle || mode != RefreshStatus.canRefresh) +/// ? Image.asset("images/custom_2.gif") +/// : Image.asset("images/custom_1.jpg"), +/// scale: _scaleAnimation, +/// ), +/// position: offsetTween.animate(_offsetController), +/// ); +/// } +/// } +/// ``` +abstract class RefreshIndicatorState + extends State + with IndicatorStateMixin, RefreshProcessor { + bool _inVisual() { + return _position!.pixels < 0.0; + } + + @override + double _calculateScrollOffset() { + return (floating + ? (mode == RefreshStatus.twoLeveling || + mode == RefreshStatus.twoLevelOpening || + mode == RefreshStatus.twoLevelClosing + ? refresherState!.viewportExtent + : widget.height) + : 0.0) - + (_position?.pixels as num); + } + + @override + void _handleOffsetChange() { + // lambiengcode: implement _handleOffsetChange + super._handleOffsetChange(); + final double overscrollPast = _calculateScrollOffset(); + onOffsetChange(overscrollPast); + } + + // handle the state change between canRefresh and idle canRefresh before refreshing + @override + void _dispatchModeByOffset(double offset) { + if (mode == RefreshStatus.twoLeveling) { + if (_position!.pixels > configuration!.closeTwoLevelDistance && + activity is BallisticScrollActivity) { + refresher!.controller.twoLevelComplete(); + + return; + } + } + if (RefreshStatus.twoLevelOpening == mode || + mode == RefreshStatus.twoLevelClosing) { + return; + } + if (floating) return; + // no matter what activity is done, when offset ==0.0 and !floating,it should be set to idle for setting ifCanDrag + if (offset == 0.0) { + mode = RefreshStatus.idle; + } + + // If FrontStyle overScroll,it shouldn't disable gesture in scrollable + if (_position!.extentBefore == 0.0 && + widget.refreshStyle == RefreshStyle.Front) { + _position!.context.setIgnorePointer(false); + } + // Sometimes different devices return velocity differently, so it's impossible to judge from velocity whether the user + // has invoked animateTo (0.0) or the user is dragging the view.Sometimes animateTo (0.0) does not return velocity = 0.0 + // velocity < 0.0 may be spring up,>0.0 spring down + if ((configuration!.enableBallisticRefresh && activity!.velocity < 0.0) || + activity is DragScrollActivity || + activity is DrivenScrollActivity) { + if (refresher!.enablePullDown && + offset >= configuration!.headerTriggerDistance) { + if (!configuration!.skipCanRefresh) { + mode = RefreshStatus.canRefresh; + } else { + floating = true; + update(); + readyToRefresh().then((_) { + if (!mounted) return; + mode = RefreshStatus.refreshing; + }); + } + } else if (refresher!.enablePullDown) { + mode = RefreshStatus.idle; + } + if (refresher!.enableTwoLevel && + offset >= configuration!.twiceTriggerDistance) { + mode = RefreshStatus.canTwoLevel; + } else if (refresher!.enableTwoLevel && !refresher!.enablePullDown) { + mode = RefreshStatus.idle; + } + } + //mostly for spring back + else if (activity is BallisticScrollActivity) { + if (RefreshStatus.canRefresh == mode) { + // refreshing + floating = true; + update(); + readyToRefresh().then((_) { + if (!mounted) return; + mode = RefreshStatus.refreshing; + }); + } + if (mode == RefreshStatus.canTwoLevel) { + // enter twoLevel + floating = true; + update(); + if (!mounted) return; + + mode = RefreshStatus.twoLevelOpening; + } + } + } + + @override + void _handleModeChange() { + if (!mounted) { + return; + } + update(); + if (mode == RefreshStatus.idle || mode == RefreshStatus.canRefresh) { + floating = false; + + resetValue(); + + if (mode == RefreshStatus.idle) refresherState!.setCanDrag(true); + } + if (mode == RefreshStatus.completed || mode == RefreshStatus.failed) { + endRefresh().then((_) { + if (!mounted) return; + floating = false; + if (mode == RefreshStatus.completed || mode == RefreshStatus.failed) { + refresherState! + .setCanDrag(configuration!.enableScrollWhenRefreshCompleted); + } + update(); + /* + handle two Situation: + 1.when user dragging to refreshing, then user scroll down not to see the indicator,then it will not spring back, + the _onOffsetChange didn't callback,it will keep failed or success state. + 2. As FrontStyle,when user dragging in 0~100 in refreshing state,it should be reset after the state change + */ + WidgetsBinding.instance.addPostFrameCallback((_) { + if (!mounted) { + return; + } + if (widget.refreshStyle == RefreshStyle.Front) { + if (_inVisual()) { + _position!.jumpTo(0.0); + } + mode = RefreshStatus.idle; + } else { + if (!_inVisual()) { + mode = RefreshStatus.idle; + } else { + activity!.delegate.goBallistic(0.0); + } + } + }); + }); + } else if (mode == RefreshStatus.refreshing) { + if (!floating) { + floating = true; + readyToRefresh(); + } + if (configuration!.enableRefreshVibrate) { + HapticFeedback.vibrate(); + } + refresher?.onRefresh?.call(); + } else if (mode == RefreshStatus.twoLevelOpening) { + floating = true; + refresherState!.setCanDrag(false); + WidgetsBinding.instance.addPostFrameCallback((_) { + if (!mounted) return; + activity!.resetActivity(); + _position! + .animateTo( + 0.0, + duration: const Duration(milliseconds: 500), + curve: Curves.linear, + ) + .whenComplete(() { + mode = RefreshStatus.twoLeveling; + }); + refresher?.onTwoLevel?.call(true); + }); + } else if (mode == RefreshStatus.twoLevelClosing) { + floating = false; + refresherState!.setCanDrag(false); + update(); + refresher?.onTwoLevel?.call(false); + } else if (mode == RefreshStatus.twoLeveling) { + refresherState!.setCanDrag(configuration!.enableScrollWhenTwoLevel); + } + onModeChange(mode); + } + + // the method can provide a callback to implements some animation + @override + Future readyToRefresh() { + return Future.value(); + } + + // it mean the state will enter success or fail + @override + Future endRefresh() { + return Future.delayed(widget.completeDuration); + } + + bool needReverseAll() { + return true; + } + + @override + void resetValue() {} + + @override + Widget build(BuildContext context) { + return SliverRefresh( + paintOffsetY: widget.offset, + floating: floating, + refreshIndicatorLayoutExtent: mode == RefreshStatus.twoLeveling || + mode == RefreshStatus.twoLevelOpening || + mode == RefreshStatus.twoLevelClosing + ? refresherState!.viewportExtent + : widget.height, + refreshStyle: widget.refreshStyle, + child: RotatedBox( + quarterTurns: needReverseAll() && + Scrollable.of(context).axisDirection == AxisDirection.up + ? 10 + : 0, + child: buildContent(context, mode), + ), + ); + } +} + +abstract class LoadIndicatorState extends State + with IndicatorStateMixin, LoadingProcessor { + // use to update between one page and above one page + bool _isHide = false; + bool _enableLoading = false; + LoadStatus? _lastMode = LoadStatus.idle; + + @override + double _calculateScrollOffset() { + final double overScrollPastEnd = + math.max(_position!.pixels - _position!.maxScrollExtent, 0.0); + + return overScrollPastEnd; + } + + void enterLoading() { + setState(() { + floating = true; + }); + _enableLoading = false; + readyToLoad().then((_) { + if (!mounted) { + return; + } + mode = LoadStatus.loading; + }); + } + + @override + Future endLoading() { + // lambiengcode: implement endLoading + return Future.delayed(Duration.zero); + } + + void finishLoading() { + if (!floating) { + return; + } + endLoading().then((_) { + if (!mounted) { + return; + } + + // this line for patch bug temporary:indicator disappears fastly when load more complete + if (mounted) Scrollable.of(context).position.correctBy(0.00001); + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted && (_position?.outOfRange ?? false)) { + activity!.delegate.goBallistic(0); + } + }); + setState(() { + floating = false; + }); + }); + } + + bool _checkIfCanLoading() { + if (_position!.maxScrollExtent - _position!.pixels <= + configuration!.footerTriggerDistance && + _position!.extentBefore > 2.0 && + _enableLoading) { + if (!configuration!.enableLoadingWhenFailed && + mode == LoadStatus.failed) { + return false; + } + if (!configuration!.enableLoadingWhenNoData && + mode == LoadStatus.noMore) { + return false; + } + if (mode != LoadStatus.canLoading && + _position!.userScrollDirection == ScrollDirection.forward) { + return false; + } + + return true; + } + + return false; + } + + @override + void _handleModeChange() { + if (!mounted || _isHide) { + return; + } + + update(); + if (mode == LoadStatus.idle || + mode == LoadStatus.failed || + mode == LoadStatus.noMore) { + // #292,#265,#208 + // stop the slow bouncing when load more too fast + if (_position!.activity!.velocity < 0 && + _lastMode == LoadStatus.loading && + !_position!.outOfRange && + _position is ScrollActivityDelegate) { + _position!.beginActivity( + IdleScrollActivity(_position as ScrollActivityDelegate), + ); + } + + finishLoading(); + } + if (mode == LoadStatus.loading) { + if (!floating) { + enterLoading(); + } + if (configuration!.enableLoadMoreVibrate) { + HapticFeedback.vibrate(); + } + refresher?.onLoading?.call(); + if (widget.loadStyle == LoadStyle.ShowWhenLoading) { + floating = true; + } + } else { + if (activity is! DragScrollActivity) _enableLoading = false; + } + _lastMode = mode; + onModeChange(mode); + } + + @override + void _dispatchModeByOffset(double offset) { + if (!mounted || _isHide || LoadStatus.loading == mode || floating) { + return; + } + if (activity is DragScrollActivity) { + if (_checkIfCanLoading()) { + mode = LoadStatus.canLoading; + } else { + mode = _lastMode; + } + } + if (activity is BallisticScrollActivity) { + if (configuration!.enableBallisticLoad) { + if (_checkIfCanLoading()) enterLoading(); + } else if (mode == LoadStatus.canLoading) { + enterLoading(); + } + } + } + + @override + void _handleOffsetChange() { + if (_isHide) { + return; + } + super._handleOffsetChange(); + final double overscrollPast = _calculateScrollOffset(); + onOffsetChange(overscrollPast); + } + + void _listenScrollEnd() { + if (!_position!.isScrollingNotifier.value) { + // when user release gesture from screen + if (_isHide || mode == LoadStatus.loading || mode == LoadStatus.noMore) { + return; + } + + if (_checkIfCanLoading()) { + if (activity is IdleScrollActivity) { + if ((configuration!.enableBallisticLoad) || + ((!configuration!.enableBallisticLoad) && + mode == LoadStatus.canLoading)) enterLoading(); + } + } + } else { + if (activity is DragScrollActivity || activity is DrivenScrollActivity) { + _enableLoading = true; + } + } + } + + @override + void _onPositionUpdated(ScrollPosition newPosition) { + _position?.isScrollingNotifier.removeListener(_listenScrollEnd); + newPosition.isScrollingNotifier.addListener(_listenScrollEnd); + super._onPositionUpdated(newPosition); + } + + @override + void didChangeDependencies() { + // lambiengcode: implement didChangeDependencies + super.didChangeDependencies(); + _lastMode = mode; + } + + @override + void dispose() { + // lambiengcode: implement dispose + _position?.isScrollingNotifier.removeListener(_listenScrollEnd); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + // lambiengcode: implement build + return SliverLoading( + hideWhenNotFull: configuration!.hideFooterWhenNotFull, + floating: widget.loadStyle == LoadStyle.ShowAlways + ? true + : widget.loadStyle == LoadStyle.HideAlways + ? false + : floating, + shouldFollowContent: configuration!.shouldFooterFollowWhenNotFull != null + ? configuration!.shouldFooterFollowWhenNotFull!(mode) + : mode == LoadStatus.noMore, + layoutExtent: widget.height, + mode: mode, + child: LayoutBuilder( + builder: (context, cons) { + _isHide = cons.biggest.height == 0.0; + + return GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () { + widget.onClick?.call(); + }, + child: buildContent(context, mode), + ); + }, + ), + ); + } +} + +/// mixin in IndicatorState,it will get position and remove when dispose,init mode state +/// +/// help to finish the work that the header indicator and footer indicator need to do +mixin IndicatorStateMixin on State { + SmartRefresher? refresher; + + RefreshConfiguration? configuration; + SmartRefresherState? refresherState; + + bool _floating = false; + + set floating(floating) => _floating = floating; + + get floating => _floating; + + set mode(mode) => _mode?.value = mode; + + get mode => _mode?.value; + + RefreshNotifier? _mode; + + ScrollActivity? get activity => _position!.activity; + + // it doesn't support get the ScrollController as the listener, because it will cause "multiple scrollview use one ScrollController" + // error,only replace the ScrollPosition to listen the offset + ScrollPosition? _position; + + // update ui + void update() { + if (mounted) { + setState(() { + return; + }); + } + } + + void _handleOffsetChange() { + if (!mounted) { + return; + } + final double overscrollPast = _calculateScrollOffset(); + if (overscrollPast < 0.0) { + return; + } + _dispatchModeByOffset(overscrollPast); + } + + void disposeListener() { + _mode?.removeListener(_handleModeChange); + _position?.removeListener(_handleOffsetChange); + _position = null; + _mode = null; + } + + void _updateListener() { + configuration = RefreshConfiguration.of(context); + refresher = SmartRefresher.of(context); + refresherState = SmartRefresher.ofState(context); + final RefreshNotifier? newMode = V == RefreshStatus + ? refresher!.controller.headerMode as RefreshNotifier? + : refresher!.controller.footerMode as RefreshNotifier?; + final ScrollPosition newPosition = Scrollable.of(context).position; + if (newMode != _mode) { + _mode?.removeListener(_handleModeChange); + _mode = newMode; + _mode?.addListener(_handleModeChange); + } + if (newPosition != _position) { + _position?.removeListener(_handleOffsetChange); + _onPositionUpdated(newPosition); + _position = newPosition; + _position?.addListener(_handleOffsetChange); + } + } + + @override + void initState() { + // lambiengcode: implement initState + if (V == RefreshStatus) { + SmartRefresher.of(context)?.controller.headerMode?.value = + RefreshStatus.idle; + } + super.initState(); + } + + @override + void dispose() { + // lambiengcode: implement dispose + //1.3.7: here need to careful after add asSliver builder + disposeListener(); + super.dispose(); + } + + @override + void didChangeDependencies() { + // lambiengcode: implement didChangeDependencies + _updateListener(); + super.didChangeDependencies(); + } + + @override + void didUpdateWidget(T oldWidget) { + // lambiengcode: implement didUpdateWidget + // needn't to update _headerMode,because it's state will never change + // 1.3.7: here need to careful after add asSliver builder + _updateListener(); + super.didUpdateWidget(oldWidget); + } + + void _onPositionUpdated(ScrollPosition newPosition) { + refresher!.controller.onPositionUpdated(newPosition); + } + + void _handleModeChange(); + + double _calculateScrollOffset(); + + void _dispatchModeByOffset(double offset); + + Widget buildContent(BuildContext context, V mode); +} + +/// head Indicator exposure interface +mixin RefreshProcessor { + /// out of edge offset callback + void onOffsetChange(double offset) { + return; + } + + /// mode change callback + void onModeChange(RefreshStatus? mode) { + return; + } + + /// when indicator is ready into refresh,it will call back and waiting for this function finish,then callback onRefresh + Future readyToRefresh() { + return Future.value(); + } + + // when indicator is ready to dismiss layout ,it will callback and then spring back after finish + Future endRefresh() { + return Future.value(); + } + + // when indicator has been spring back,it need to reset value + void resetValue() {} +} + +/// footer Indicator exposure interface +mixin LoadingProcessor { + void onOffsetChange(double offset) { + return; + } + + void onModeChange(LoadStatus? mode) { + return; + } + + /// when indicator is ready into refresh,it will call back and waiting for this function finish,then callback onRefresh + Future readyToLoad() { + return Future.value(); + } + + // when indicator is ready to dismiss layout ,it will callback and then spring back after finish + Future endLoading() { + return Future.value(); + } + + // when indicator has been spring back,it need to reset value + void resetValue() { + return; + } +} diff --git a/lib/core/util/common/pull_to_refresh/src/internals/refresh_localizations.dart b/lib/core/util/common/pull_to_refresh/src/internals/refresh_localizations.dart new file mode 100644 index 0000000..3eb855e --- /dev/null +++ b/lib/core/util/common/pull_to_refresh/src/internals/refresh_localizations.dart @@ -0,0 +1,631 @@ +// Flutter imports: +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + +/// Implementation of localized strings for the [ClassicHeader],[ClassicFooter],[TwoLevelHeader] +/// +/// +/// Supported languages:now only add Chinese and English +/// If you need to add other languages,please give me a pr +/// +/// ## Sample code +/// +/// To include the localizations provided by this class in a [MaterialApp], +/// add [RefreshLocalizations.delegates] to +/// [MaterialApp.localizationsDelegates], and specify the locales your +/// app supports with [MaterialApp.supportedLocales]: +/// +/// ```dart +/// new MaterialApp( +/// localizationsDelegates: RefreshLocalizations.delegates, +/// supportedLocales: [ +/// const Locale('en'), // American English +/// const Locale('zh'), // Israeli Hebrew +/// // ... +/// ], +/// // ... +/// ) +/// +/// If you don't have the language you need here and you want to add it, you can give me a pr. +/// +/// Steps: +/// 1. custom a class XXRefreshString implements RefreshString ,and then translate them +/// 2. add it into values +/// ```dart +/// Map values = { +/// 'en': EnRefreshString(), +/// 'zh': ChRefreshString(), +/// 'fr': FrRefreshString(), +/// 'ru': RuRefreshString(), +/// 'uk': UkRefreshString(), +/// 'xx':XXRefreshString(), // xx indicate your country code +/// }; +/// 3. update delegate a method "isSupported" +/// ```dart +/// @override +// bool isSupported(Locale locale) { +// return ['en', 'zh', 'fr', 'ru', 'uk','xx'].contains(locale.languageCode); +// } +/// ``` +/// +/// see #175 to find more details +/// +/// +/// ``` +/// +/// +/// ``` +class RefreshLocalizations { + final Locale locale; + + RefreshLocalizations(this.locale); + + Map values = { + 'en': EnRefreshString(), + 'zh': ChRefreshString(), + 'fr': FrRefreshString(), + 'ru': RuRefreshString(), + 'uk': UkRefreshString(), + 'it': ItRefreshString(), + 'ja': JpRefreshString(), + 'de': DeRefreshString(), + 'es': EsRefreshString(), + 'nl': NlRefreshString(), + 'sv': SvRefreshString(), + 'pt': PtRefreshString(), + 'ko': KrRefreshString(), + }; + + RefreshString? get currentLocalization { + if (values.containsKey(locale.languageCode)) { + return values[locale.languageCode]; + } + return values["en"]; + } + + static const RefreshLocalizationsDelegate delegate = + RefreshLocalizationsDelegate(); + + static RefreshLocalizations? of(BuildContext context) { + return Localizations.of(context, RefreshLocalizations); + } +} + +class RefreshLocalizationsDelegate + extends LocalizationsDelegate { + const RefreshLocalizationsDelegate(); + + @override + bool isSupported(Locale locale) { + return [ + 'en', + 'zh', + 'fr', + 'ru', + 'uk', + 'ja', + 'it', + 'de', + 'ko', + 'pt', + 'sv', + 'nl', + 'es' + ].contains(locale.languageCode); + } + + @override + Future load(Locale locale) { + return SynchronousFuture( + RefreshLocalizations(locale)); + } + + @override + bool shouldReload(LocalizationsDelegate old) { + return false; + } +} + +/// interface implements different language +abstract class RefreshString { + /// pull down refresh idle text + String? idleRefreshText; + + /// tips user to release gesture to refresh at time + String? canRefreshText; + + /// refreshing state text + String? refreshingText; + + /// refresh completed text + String? refreshCompleteText; + + /// refresh failed text + String? refreshFailedText; + + /// enable open twoLevel and tips user to release gesture to enter two level + String? canTwoLevelText; + + /// pull down load idle text + String? idleLoadingText; + + /// tips user to release gesture to load more at time + String? canLoadingText; + + /// loading state text + String? loadingText; + + /// load failed text + String? loadFailedText; + + /// no more data text + String? noMoreText; +} + +/// Chinese +class ChRefreshString implements RefreshString { + @override + String? canLoadingText = "松手开始加载数据"; + + @override + String? canRefreshText = "松开开始刷新数据"; + + @override + String? canTwoLevelText = "释放手势,进入二楼"; + + @override + String? idleLoadingText = "上拉加载"; + + @override + String? idleRefreshText = "下拉刷新"; + + @override + String? loadFailedText = "加载失败"; + + @override + String? loadingText = "加载中…"; + + @override + String? noMoreText = "没有更多数据了"; + + @override + String? refreshCompleteText = "刷新成功"; + + @override + String? refreshFailedText = "刷新失败"; + + @override + String? refreshingText = "刷新中…"; +} + +/// English +class EnRefreshString implements RefreshString { + @override + String? canLoadingText = "Release to load more"; + + @override + String? canRefreshText = "Release to refresh"; + + @override + String? canTwoLevelText = "Release to enter secondfloor"; + + @override + String? idleLoadingText = "Pull up Load more"; + + @override + String? idleRefreshText = "Pull down Refresh"; + + @override + String? loadFailedText = "Load Failed"; + + @override + String? loadingText = "Loading…"; + + @override + String? noMoreText = "No more data"; + + @override + String? refreshCompleteText = "Refresh completed"; + + @override + String? refreshFailedText = "Refresh failed"; + + @override + String? refreshingText = "Refreshing…"; +} + +/// French +class FrRefreshString implements RefreshString { + @override + String? canLoadingText = "Relâchez pour charger davantage"; + + @override + String? canRefreshText = "Relâchez pour rafraîchir"; + + @override + String? canTwoLevelText = "Relâchez pour entrer secondfloor"; + + @override + String? idleLoadingText = "Tirez pour charger davantage"; + + @override + String? idleRefreshText = "Tirez pour rafraîchir"; + + @override + String? loadFailedText = "Chargement échoué"; + + @override + String? loadingText = "Chargement…"; + + @override + String? noMoreText = "Aucune autre donnée"; + + @override + String? refreshCompleteText = "Rafraîchissement terminé"; + + @override + String? refreshFailedText = "Rafraîchissement échoué"; + + @override + String? refreshingText = "Rafraîchissement…"; +} + +/// Russian +class RuRefreshString implements RefreshString { + @override + String? canLoadingText = "Отпустите, чтобы загрузить больше"; + + @override + String? canRefreshText = "Отпустите, чтобы обновить"; + + @override + String? canTwoLevelText = "Отпустите, чтобы войти на второй уровень"; + + @override + String? idleLoadingText = "Тянуть вверх, чтобы загрузить больше"; + + @override + String? idleRefreshText = "Тянуть вниз, чтобы обновить"; + + @override + String? loadFailedText = "Ошибка загрузки"; + + @override + String? loadingText = "Загрузка…"; + + @override + String? noMoreText = "Больше данных нет"; + + @override + String? refreshCompleteText = "Обновление завершено"; + + @override + String? refreshFailedText = "Не удалось обновить"; + + @override + String? refreshingText = "Обновление…"; +} + +// Ukrainian +class UkRefreshString implements RefreshString { + @override + String? canLoadingText = "Відпустіть, щоб завантажити більше"; + + @override + String? canRefreshText = "Відпустіть, щоб оновити"; + + @override + String? canTwoLevelText = "Відпустіть, щоб увійти на другий рівень"; + + @override + String? idleLoadingText = "Тягнути вгору, щоб завантажити більше"; + + @override + String? idleRefreshText = "Тягнути вниз, щоб оновити"; + + @override + String? loadFailedText = "Помилка завантаження"; + + @override + String? loadingText = "Завантаження…"; + + @override + String? noMoreText = "Більше даних немає"; + + @override + String? refreshCompleteText = "Оновлення завершено"; + + @override + String? refreshFailedText = "Не вдалося оновити"; + + @override + String? refreshingText = "Оновлення…"; +} + +/// Italian +class ItRefreshString implements RefreshString { + @override + String? canLoadingText = "Rilascia per caricare altro"; + + @override + String? canRefreshText = "Rilascia per aggiornare"; + + @override + String? canTwoLevelText = "Rilascia per accedere a secondfloor"; + + @override + String? idleLoadingText = "Tira per caricare altro"; + + @override + String? idleRefreshText = "Tira giù per aggiornare"; + + @override + String? loadFailedText = "Caricamento fallito"; + + @override + String? loadingText = "Caricamento…"; + + @override + String? noMoreText = "Nessun altro elemento"; + + @override + String? refreshCompleteText = "Aggiornamento completato"; + + @override + String? refreshFailedText = "Aggiornamento fallito"; + + @override + String? refreshingText = "Aggiornamento…"; +} + +/// Japanese +class JpRefreshString implements RefreshString { + @override + String? canLoadingText = "指を離して更に読み込む"; + + @override + String? canRefreshText = "指を離して更新"; + + @override + String? canTwoLevelText = "指を離して2段目を表示"; + + @override + String? idleLoadingText = "上方スワイプで更に読み込む"; + + @override + String? idleRefreshText = "下方スワイプでデータを更新"; + + @override + String? loadFailedText = "読み込みが失敗しました"; + + @override + String? loadingText = "読み込み中…"; + + @override + String? noMoreText = "データはありません"; + + @override + String? refreshCompleteText = "更新完了"; + + @override + String? refreshFailedText = "更新が失敗しました"; + + @override + String? refreshingText = "更新中…"; +} + +/// German +class DeRefreshString implements RefreshString { + @override + String? canLoadingText = "Loslassen, um mehr zu laden"; + + @override + String? canRefreshText = "Zum Aktualisieren loslassen"; + + @override + String? canTwoLevelText = "Lassen Sie los, um den zweiten Stock zu betreten"; + + @override + String? idleLoadingText = "Hochziehen, mehr laden"; + + @override + String? idleRefreshText = "Ziehen für Aktualisierung"; + + @override + String? loadFailedText = "Laden ist fehlgeschlagen"; + + @override + String? loadingText = "Lade…"; + + @override + String? noMoreText = "Keine weitere Daten"; + + @override + String? refreshCompleteText = "Aktualisierung fertig"; + + @override + String? refreshFailedText = "Aktualisierung fehlgeschlagen"; + + @override + String? refreshingText = "Aktualisiere…"; +} + +/// Spanish +class EsRefreshString implements RefreshString { + @override + String? canLoadingText = "Suelte para cargar más"; + + @override + String? canRefreshText = "Suelte para actualizar"; + + @override + String? canTwoLevelText = "Suelte para entrar al segundo nivel"; + + @override + String? idleLoadingText = "Tire hacia arriba para cargar más"; + + @override + String? idleRefreshText = "Tire hacia abajo para refrescar"; + + @override + String? loadFailedText = "Error de carga"; + + @override + String? loadingText = "Cargando…"; + + @override + String? noMoreText = "No hay más datos disponibles"; + + @override + String? refreshCompleteText = "Actualización completada"; + + @override + String? refreshFailedText = "Error al actualizar"; + + @override + String? refreshingText = "Actualizando…"; +} + +/// Dutch +class NlRefreshString implements RefreshString { + @override + String? canLoadingText = "Laat los om meer te laden"; + + @override + String? canRefreshText = "Laat los om te vernieuwen"; + + @override + String? canTwoLevelText = "Laat los om naar tweede verdieping te gaan"; + + @override + String? idleLoadingText = "Trek omhoog om meer te laden"; + + @override + String? idleRefreshText = "Trek omlaag om te vernieuwen"; + + @override + String? loadFailedText = "Laden mislukt"; + + @override + String? loadingText = "Laden…"; + + @override + String? noMoreText = "Geen data meer"; + + @override + String? refreshCompleteText = "Vernieuwen voltooid"; + + @override + String? refreshFailedText = "Vernieuwen mislukt"; + + @override + String? refreshingText = "Vernieuwen…"; +} + +/// Swedish +class SvRefreshString implements RefreshString { + @override + String? canLoadingText = "Släpp för att ladda mer"; + + @override + String? canRefreshText = "Släpp för att uppdatera"; + + @override + String? canTwoLevelText = "Släpp för att gå till andra våningen"; + + @override + String? idleLoadingText = "Dra upp för att ladda mer"; + + @override + String? idleRefreshText = "Dra ner för att uppdatera"; + + @override + String? loadFailedText = "Hämtningen misslyckades"; + + @override + String? loadingText = "Laddar…"; + + @override + String? noMoreText = "Ingen mer data"; + + @override + String? refreshCompleteText = "Uppdaterad"; + + @override + String? refreshFailedText = "Kunde inte uppdatera"; + + @override + String? refreshingText = "Uppdaterar…"; +} + +// Portuguese - Brazil +class PtRefreshString implements RefreshString { + @override + String? canLoadingText = "Solte para carregar mais"; + + @override + String? canRefreshText = "Solte para atualizar"; + + @override + String? canTwoLevelText = "Solte para entrar no segundo andar"; + + @override + String? idleLoadingText = "Puxe para cima para carregar mais"; + + @override + String? idleRefreshText = "Puxe para baixo para atualizar"; + + @override + String? loadFailedText = "Falha ao carregar"; + + @override + String? loadingText = "Carregando…"; + + @override + String? noMoreText = "Não há mais dados"; + + @override + String? refreshCompleteText = "Atualização completada"; + + @override + String? refreshFailedText = "Falha ao atualizar"; + + @override + String? refreshingText = "Atualizando…"; +} + +/// Korean +class KrRefreshString implements RefreshString { + @override + String? canLoadingText = "당겨서 불러오기"; + + @override + String? canRefreshText = "당겨서 새로 고침"; + + @override + String? canTwoLevelText = "두 번째 레벨로 이동"; + + @override + String? idleLoadingText = "위로 당겨서 불러오기"; + + @override + String? idleRefreshText = "아래로 당겨서 새로 고침"; + + @override + String? loadFailedText = "로딩에 실패했습니다."; + + @override + String? loadingText = "로딩 중…"; + + @override + String? noMoreText = "데이터가 더 이상 없습니다."; + + @override + String? refreshCompleteText = "새로 고침 완료"; + + @override + String? refreshFailedText = "새로 고침에 실패했습니다."; + + @override + String? refreshingText = "새로 고침 중…"; +} diff --git a/lib/core/util/common/pull_to_refresh/src/internals/refresh_physics.dart b/lib/core/util/common/pull_to_refresh/src/internals/refresh_physics.dart new file mode 100644 index 0000000..6ae1132 --- /dev/null +++ b/lib/core/util/common/pull_to_refresh/src/internals/refresh_physics.dart @@ -0,0 +1,305 @@ +// ignore_for_file: invalid_use_of_visible_for_testing_member, invalid_use_of_protected_member, deprecated_member_use + +// Dart imports: +import 'dart:math' as math; + +// Flutter imports: +import 'package:flutter/rendering.dart'; +import 'package:flutter/widgets.dart'; + +// Project imports: +import 'package:streamskit_mobile/core/util/common/pull_to_refresh/pull_to_refresh.dart'; +import 'package:streamskit_mobile/core/util/common/pull_to_refresh/src/internals/slivers.dart'; + +/// a scrollPhysics for config refresh scroll effect,enable viewport out of edge whatever physics it is +/// in [ClampingScrollPhysics], it doesn't allow to flip out of edge,but in RefreshPhysics,it will allow to do that, +/// by parent physics passing,it also can attach the different of iOS and Android different scroll effect +/// it also handles interception scrolling when refreshed, or when the second floor is open and closed. +/// with [SpringDescription] passing,you can custom spring back animate,the more paramter can be setting in [RefreshConfiguration] +/// +/// see also: +/// +/// * [RefreshConfiguration], a configuration for Controlling how SmartRefresher widgets behave in a subtree +// ignore: MUST_BE_IMMUTABLE +class RefreshPhysics extends ScrollPhysics { + final double? maxOverScrollExtent, maxUnderScrollExtent; + final double? topHitBoundary, bottomHitBoundary; + final SpringDescription? springDescription; + final double? dragSpeedRatio; + final bool? enableScrollWhenTwoLevel, enableScrollWhenRefreshCompleted; + final RefreshController? controller; + final int? updateFlag; + + /// find out the viewport when bouncing,for compute the layoutExtent in header and footer + /// This does not have any impact on performance. it only execute once + RenderViewport? viewportRender; + + /// Creates scroll physics that bounce back from the edge. + RefreshPhysics( + {super.parent, + this.updateFlag, + this.maxUnderScrollExtent, + this.springDescription, + this.controller, + this.dragSpeedRatio, + this.topHitBoundary, + this.bottomHitBoundary, + this.enableScrollWhenRefreshCompleted, + this.enableScrollWhenTwoLevel, + this.maxOverScrollExtent}); + + @override + RefreshPhysics applyTo(ScrollPhysics? ancestor) { + return RefreshPhysics( + parent: buildParent(ancestor), + updateFlag: updateFlag, + springDescription: springDescription, + dragSpeedRatio: dragSpeedRatio, + enableScrollWhenTwoLevel: enableScrollWhenTwoLevel, + topHitBoundary: topHitBoundary, + bottomHitBoundary: bottomHitBoundary, + controller: controller, + enableScrollWhenRefreshCompleted: enableScrollWhenRefreshCompleted, + maxUnderScrollExtent: maxUnderScrollExtent, + maxOverScrollExtent: maxOverScrollExtent); + } + + RenderViewport? findViewport(BuildContext? context) { + if (context == null) { + return null; + } + RenderViewport? result; + context.visitChildElements((Element e) { + final RenderObject? renderObject = e.findRenderObject(); + if (renderObject is RenderViewport) { + assert(result == null); + result = renderObject; + } else { + result = findViewport(e); + } + }); + return result; + } + + @override + bool shouldAcceptUserOffset(ScrollMetrics position) { + // lambiengcode: implement shouldAcceptUserOffset + if (parent is NeverScrollableScrollPhysics) { + return false; + } + return true; + } + + // It seem that it was odd to do so,but I have no choose to do this for updating the state value(enablePullDown and enablePullUp), + // in Scrollable.dart _shouldUpdatePosition method,it use physics.runtimeType to check if the two physics is the same,this + // will lead to whether the newPhysics should replace oldPhysics,If flutter can provide a method such as "shouldUpdate", + // It can work perfectly. + @override + // lambiengcode: implement runtimeType + Type get runtimeType { + if (updateFlag == 0) { + return RefreshPhysics; + } else { + return BouncingScrollPhysics; + } + } + + @override + double applyPhysicsToUserOffset(ScrollMetrics position, double offset) { + // lambiengcode: implement applyPhysicsToUserOffset + viewportRender ??= + findViewport(controller!.position?.context.storageContext); + if (controller!.headerMode!.value == RefreshStatus.twoLeveling) { + if (offset > 0.0) { + return parent!.applyPhysicsToUserOffset(position, offset); + } + } else { + if ((offset > 0.0 && + viewportRender?.firstChild is! RenderSliverRefresh) || + (offset < 0 && viewportRender?.lastChild is! RenderSliverLoading)) { + return parent!.applyPhysicsToUserOffset(position, offset); + } + } + if (position.outOfRange || + controller!.headerMode!.value == RefreshStatus.twoLeveling) { + final double overscrollPastStart = + math.max(position.minScrollExtent - position.pixels, 0.0); + final double overscrollPastEnd = math.max( + position.pixels - + (controller!.headerMode!.value == RefreshStatus.twoLeveling + ? 0.0 + : position.maxScrollExtent), + 0.0); + final double overscrollPast = + math.max(overscrollPastStart, overscrollPastEnd); + final bool easing = (overscrollPastStart > 0.0 && offset < 0.0) || + (overscrollPastEnd > 0.0 && offset > 0.0); + + final double friction = easing + // Apply less resistance when easing the overscroll vs tensioning. + ? frictionFactor( + (overscrollPast - offset.abs()) / position.viewportDimension) + : frictionFactor(overscrollPast / position.viewportDimension); + final double direction = offset.sign; + return direction * + _applyFriction(overscrollPast, offset.abs(), friction) * + (dragSpeedRatio ?? 1.0); + } + return super.applyPhysicsToUserOffset(position, offset); + } + + static double _applyFriction( + double extentOutside, double absDelta, double gamma) { + assert(absDelta > 0); + double total = 0.0; + if (extentOutside > 0) { + final double deltaToLimit = extentOutside / gamma; + if (absDelta < deltaToLimit) return absDelta * gamma; + total += extentOutside; + absDelta -= deltaToLimit; + } + return total + absDelta; + } + + double frictionFactor(double overscrollFraction) => + 0.52 * math.pow(1 - overscrollFraction, 2); + + @override + double applyBoundaryConditions(ScrollMetrics position, double value) { + final ScrollPosition scrollPosition = position as ScrollPosition; + viewportRender ??= + findViewport(controller!.position?.context.storageContext); + bool notFull = position.minScrollExtent == position.maxScrollExtent; + final bool enablePullDown = viewportRender == null + ? false + : viewportRender!.firstChild is RenderSliverRefresh; + final bool enablePullUp = viewportRender == null + ? false + : viewportRender!.lastChild is RenderSliverLoading; + if (controller!.headerMode!.value == RefreshStatus.twoLeveling) { + if (position.pixels - value > 0.0) { + return parent!.applyBoundaryConditions(position, value); + } + } else { + if ((position.pixels - value > 0.0 && !enablePullDown) || + (position.pixels - value < 0 && !enablePullUp)) { + return parent!.applyBoundaryConditions(position, value); + } + } + double topExtra = 0.0; + double? bottomExtra = 0.0; + if (enablePullDown) { + final RenderSliverRefresh sliverHeader = + viewportRender!.firstChild as RenderSliverRefresh; + topExtra = sliverHeader.hasLayoutExtent + ? 0.0 + : sliverHeader.refreshIndicatorLayoutExtent; + } + if (enablePullUp) { + final RenderSliverLoading? sliverFooter = + viewportRender!.lastChild as RenderSliverLoading?; + bottomExtra = (!notFull && sliverFooter!.geometry!.scrollExtent != 0) || + (notFull && + controller!.footerStatus == LoadStatus.noMore && + !RefreshConfiguration.of( + controller!.position!.context.storageContext)! + .enableLoadingWhenNoData) || + (notFull && + (RefreshConfiguration.of( + controller!.position!.context.storageContext) + ?.hideFooterWhenNotFull ?? + false)) + ? 0.0 + : sliverFooter!.layoutExtent; + } + final double topBoundary = + position.minScrollExtent - maxOverScrollExtent! - topExtra; + final double bottomBoundary = + position.maxScrollExtent + maxUnderScrollExtent! + bottomExtra!; + + if (scrollPosition.activity is BallisticScrollActivity) { + if (topHitBoundary != double.infinity) { + if (value < -topHitBoundary! && -topHitBoundary! <= position.pixels) { + // hit top edge + return value + topHitBoundary!; + } + } + if (bottomHitBoundary != double.infinity) { + if (position.pixels < bottomHitBoundary! + position.maxScrollExtent && + bottomHitBoundary! + position.maxScrollExtent < value) { + // hit bottom edge + return value - bottomHitBoundary! - position.maxScrollExtent; + } + } + } + if (maxOverScrollExtent != double.infinity && + value < topBoundary && + topBoundary < position.pixels) { + return value - topBoundary; + } + if (maxUnderScrollExtent != double.infinity && + position.pixels < bottomBoundary && + bottomBoundary < value) { + // hit bottom edge + return value - bottomBoundary; + } + + // check user is dragging,it is import,some devices may not bounce with different frame and time,bouncing return the different velocity + if (scrollPosition.activity is DragScrollActivity) { + if (maxOverScrollExtent != double.infinity && + value < position.pixels && + position.pixels <= topBoundary) { + return value - position.pixels; + } + if (maxUnderScrollExtent != double.infinity && + bottomBoundary <= position.pixels && + position.pixels < value) { + return value - position.pixels; + } + } + return 0.0; + } + + @override + Simulation? createBallisticSimulation( + ScrollMetrics position, double velocity) { + // lambiengcode: implement createBallisticSimulation + viewportRender ??= + findViewport(controller!.position?.context.storageContext); + + final bool enablePullDown = viewportRender == null + ? false + : viewportRender!.firstChild is RenderSliverRefresh; + final bool enablePullUp = viewportRender == null + ? false + : viewportRender!.lastChild is RenderSliverLoading; + if (controller!.headerMode!.value == RefreshStatus.twoLeveling) { + if (velocity < 0.0) { + return parent!.createBallisticSimulation(position, velocity); + } + } else if (!position.outOfRange) { + if ((velocity < 0.0 && !enablePullDown) || + (velocity > 0 && !enablePullUp)) { + return parent!.createBallisticSimulation(position, velocity); + } + } + if ((position.pixels > 0 && + controller!.headerMode!.value == RefreshStatus.twoLeveling) || + position.outOfRange) { + return BouncingScrollSimulation( + spring: springDescription ?? spring, + position: position.pixels, + // -1.0 avoid stop springing back ,and release gesture + velocity: velocity * 0.91, + // lambiengcode:(abarth): We should move this constant closer to the drag end. + leadingExtent: position.minScrollExtent, + trailingExtent: + controller!.headerMode!.value == RefreshStatus.twoLeveling + ? 0.0 + : position.maxScrollExtent, + tolerance: tolerance, + ); + } + return super.createBallisticSimulation(position, velocity); + } +} diff --git a/lib/core/util/common/pull_to_refresh/src/internals/slivers.dart b/lib/core/util/common/pull_to_refresh/src/internals/slivers.dart new file mode 100644 index 0000000..51a2cd1 --- /dev/null +++ b/lib/core/util/common/pull_to_refresh/src/internals/slivers.dart @@ -0,0 +1,568 @@ +// ignore_for_file: avoid_renaming_method_parameters + +// Dart imports: +import 'dart:math' as math; + +// Flutter imports: +import 'package:flutter/rendering.dart'; +import 'package:flutter/widgets.dart'; + +// Project imports: +import 'package:streamskit_mobile/core/util/common/pull_to_refresh/pull_to_refresh.dart'; + +/// Render header sliver widget +class SliverRefresh extends SingleChildRenderObjectWidget { + const SliverRefresh({ + super.key, + this.paintOffsetY, + this.refreshIndicatorLayoutExtent = 0.0, + this.floating = false, + super.child, + this.refreshStyle, + }) : assert(refreshIndicatorLayoutExtent >= 0.0); + + /// The amount of space the indicator should occupy in the sliver in a + /// resting state when in the refreshing mode. + final double refreshIndicatorLayoutExtent; + + /// _RenderSliverRefresh will paint the child in the available + /// space either way but this instructs the _RenderSliverRefresh + /// on whether to also occupy any layoutExtent space or not. + final bool floating; + + /// header indicator display style + final RefreshStyle? refreshStyle; + + /// headerOffset Head indicator layout deviation Y coordinates, mostly for FrontStyle + final double? paintOffsetY; + + @override + RenderSliverRefresh createRenderObject(BuildContext context) { + return RenderSliverRefresh( + refreshIndicatorExtent: refreshIndicatorLayoutExtent, + hasLayoutExtent: floating, + paintOffsetY: paintOffsetY, + refreshStyle: refreshStyle, + ); + } + + @override + void updateRenderObject( + BuildContext context, covariant RenderSliverRefresh renderObject) { + final RefreshStatus mode = + SmartRefresher.of(context)!.controller.headerMode!.value; + renderObject + ..refreshIndicatorLayoutExtent = refreshIndicatorLayoutExtent + ..hasLayoutExtent = floating + ..context = context + ..refreshStyle = refreshStyle + ..updateFlag = mode == RefreshStatus.twoLevelOpening || + mode == RefreshStatus.twoLeveling || + mode == RefreshStatus.idle + ..paintOffsetY = paintOffsetY; + } +} + +class RenderSliverRefresh extends RenderSliverSingleBoxAdapter { + RenderSliverRefresh( + {required double refreshIndicatorExtent, + required bool hasLayoutExtent, + RenderBox? child, + this.paintOffsetY, + this.refreshStyle}) + : assert(refreshIndicatorExtent >= 0.0), + _refreshIndicatorExtent = refreshIndicatorExtent, + _hasLayoutExtent = hasLayoutExtent { + this.child = child; + } + + RefreshStyle? refreshStyle; + late BuildContext context; + + // The amount of layout space the indicator should occupy in the sliver in a + // resting state when in the refreshing mode. + double get refreshIndicatorLayoutExtent => _refreshIndicatorExtent; + double _refreshIndicatorExtent; + double? paintOffsetY; + // need to trigger shouldAceppty user offset ,else it will not limit scroll when enter twolevel or exit + // also it will crash if you call applyNewDimession when the state change + // I don't know why flutter limit it, no choice + bool _updateFlag = false; + + set refreshIndicatorLayoutExtent(double value) { + assert(value >= 0.0); + if (value == _refreshIndicatorExtent) return; + _refreshIndicatorExtent = value; + markNeedsLayout(); + } + + // The child box will be laid out and painted in the available space either + // way but this determines whether to also occupy any + // [SliverGeometry.layoutExtent] space or not. + bool get hasLayoutExtent => _hasLayoutExtent; + bool _hasLayoutExtent; + + set hasLayoutExtent(bool value) { + if (value == _hasLayoutExtent) return; + if (!value) { + _updateFlag = true; + } + _hasLayoutExtent = value; + markNeedsLayout(); + } + + // This keeps track of the previously applied scroll offsets to the scrollable + // so that when [refreshIndicatorLayoutExtent] or [hasLayoutExtent] changes, + // the appropriate delta can be applied to keep everything in the same place + // visually. + double layoutExtentOffsetCompensation = 0.0; + + @override + // lambiengcode: implement centerOffsetAdjustment + double get centerOffsetAdjustment { + if (refreshStyle == RefreshStyle.Front) { + final RenderViewportBase renderViewport = + parent as RenderViewportBase>; + return math.max(0.0, -renderViewport.offset.pixels); + } + return 0.0; + } + + @override + void layout(Constraints constraints, {bool parentUsesSize = false}) { + // lambiengcode: implement layout + if (refreshStyle == RefreshStyle.Front) { + final RenderViewportBase renderViewport = + parent as RenderViewportBase>; + super.layout( + (constraints as SliverConstraints) + .copyWith(overlap: math.min(0.0, renderViewport.offset.pixels)), + parentUsesSize: true); + } else { + super.layout(constraints, parentUsesSize: parentUsesSize); + } + } + + set updateFlag(u) { + _updateFlag = u; + markNeedsLayout(); + } + + @override + void debugAssertDoesMeetConstraints() { + assert(geometry!.debugAssertIsValid(informationCollector: () sync* { + yield describeForError( + 'The RenderSliver that returned the offending geometry was'); + })); + assert(() { + if (geometry!.paintExtent > constraints.remainingPaintExtent) { + throw FlutterError.fromParts([ + ErrorSummary( + 'SliverGeometry has a paintOffset that exceeds the remainingPaintExtent from the constraints.'), + describeForError( + 'The render object whose geometry violates the constraints is the following'), + ErrorDescription( + 'The paintExtent must cause the child sliver to paint within the viewport, and so ' + 'cannot exceed the remainingPaintExtent.', + ), + ]); + } + return true; + }()); + } + + @override + void performLayout() { + if (_updateFlag) { + // ignore_for_file: INVALID_USE_OF_PROTECTED_MEMBER + // ignore_for_file: INVALID_USE_OF_VISIBLE_FOR_TESTING_MEMBER + Scrollable.of(context).position.activity!.applyNewDimensions(); + _updateFlag = false; + } + // The new layout extent this sliver should now have. + final double layoutExtent = + (_hasLayoutExtent ? 1.0 : 0.0) * _refreshIndicatorExtent; + // If the new layoutExtent instructive changed, the SliverGeometry's + // layoutExtent will take that value (on the next performLayout run). Shift + // the scroll offset first so it doesn't make the scroll position suddenly jump. + if (refreshStyle != RefreshStyle.Front) { + if (layoutExtent != layoutExtentOffsetCompensation) { + geometry = SliverGeometry( + scrollOffsetCorrection: layoutExtent - layoutExtentOffsetCompensation, + ); + + layoutExtentOffsetCompensation = layoutExtent; + return; + } + } + bool active = constraints.overlap < 0.0 || layoutExtent > 0.0; + final double overscrolledExtent = + -(parent as RenderViewportBase).offset.pixels; + if (refreshStyle == RefreshStyle.Behind) { + child!.layout( + constraints.asBoxConstraints( + maxExtent: math.max(0, overscrolledExtent + layoutExtent)), + parentUsesSize: true, + ); + } else { + child!.layout( + constraints.asBoxConstraints(), + parentUsesSize: true, + ); + } + final double boxExtent = (constraints.axisDirection == AxisDirection.up || + constraints.axisDirection == AxisDirection.down) + ? child!.size.height + : child!.size.width; + + if (active) { + final double needPaintExtent = math.min( + math.max( + math.max( + (constraints.axisDirection == AxisDirection.up || + constraints.axisDirection == AxisDirection.down) + ? child!.size.height + : child!.size.width, + layoutExtent) - + constraints.scrollOffset, + 0.0, + ), + constraints.remainingPaintExtent); + switch (refreshStyle) { + case RefreshStyle.Follow: + geometry = SliverGeometry( + scrollExtent: layoutExtent, + paintOrigin: -boxExtent - constraints.scrollOffset + layoutExtent, + paintExtent: needPaintExtent, + hitTestExtent: needPaintExtent, + hasVisualOverflow: overscrolledExtent < boxExtent, + maxPaintExtent: needPaintExtent, + layoutExtent: math.min(needPaintExtent, + math.max(layoutExtent - constraints.scrollOffset, 0.0)), + ); + + break; + case RefreshStyle.Behind: + geometry = SliverGeometry( + scrollExtent: layoutExtent, + paintOrigin: -overscrolledExtent - constraints.scrollOffset, + paintExtent: needPaintExtent, + maxPaintExtent: needPaintExtent, + layoutExtent: + math.max(layoutExtent - constraints.scrollOffset, 0.0), + ); + break; + case RefreshStyle.UnFollow: + geometry = SliverGeometry( + scrollExtent: layoutExtent, + paintOrigin: math.min( + -overscrolledExtent - constraints.scrollOffset, + -boxExtent - constraints.scrollOffset + layoutExtent), + paintExtent: needPaintExtent, + hasVisualOverflow: overscrolledExtent < boxExtent, + maxPaintExtent: needPaintExtent, + layoutExtent: math.min(needPaintExtent, + math.max(layoutExtent - constraints.scrollOffset, 0.0)), + ); + + break; + case RefreshStyle.Front: + geometry = SliverGeometry( + paintOrigin: constraints.axisDirection == AxisDirection.up || + constraints.crossAxisDirection == AxisDirection.left + ? boxExtent + : 0.0, + visible: true, + hasVisualOverflow: true, + ); + break; + case null: + break; + } + setChildParentData(child!, constraints, geometry!); + } else { + geometry = SliverGeometry.zero; + } + } + + @override + void paint(PaintingContext paintContext, Offset offset) { + paintContext.paintChild( + child!, Offset(offset.dx, offset.dy + paintOffsetY!)); + } + + @override + void applyPaintTransform(RenderObject child, Matrix4 transform) {} +} + +/// Render footer sliver widget +class SliverLoading extends SingleChildRenderObjectWidget { + /// when not full one page,whether it should be hide and disable loading + final bool? hideWhenNotFull; + final bool? floating; + + /// load state + final LoadStatus? mode; + final double? layoutExtent; + + /// when not full one page,whether it should follow content + final bool? shouldFollowContent; + + const SliverLoading({ + super.key, + this.mode, + this.floating, + this.shouldFollowContent, + this.layoutExtent, + this.hideWhenNotFull, + super.child, + }); + + @override + RenderSliverLoading createRenderObject(BuildContext context) { + return RenderSliverLoading( + hideWhenNotFull: hideWhenNotFull, + mode: mode, + hasLayoutExtent: floating, + shouldFollowContent: shouldFollowContent, + layoutExtent: layoutExtent); + } + + @override + void updateRenderObject( + BuildContext context, covariant RenderSliverLoading renderObject) { + renderObject + ..mode = mode + ..hasLayoutExtent = floating! + ..layoutExtent = layoutExtent + ..shouldFollowContent = shouldFollowContent + ..hideWhenNotFull = hideWhenNotFull; + } +} + +class RenderSliverLoading extends RenderSliverSingleBoxAdapter { + RenderSliverLoading({ + RenderBox? child, + this.mode, + double? layoutExtent, + bool? hasLayoutExtent, + this.shouldFollowContent, + this.hideWhenNotFull, + }) { + _hasLayoutExtent = hasLayoutExtent; + this.layoutExtent = layoutExtent; + this.child = child; + } + + bool? shouldFollowContent; + bool? hideWhenNotFull; + + LoadStatus? mode; + + double? _layoutExtent; + + set layoutExtent(extent) { + if (extent == _layoutExtent) return; + _layoutExtent = extent; + markNeedsLayout(); + } + + get layoutExtent => _layoutExtent; + + bool get hasLayoutExtent => _hasLayoutExtent!; + bool? _hasLayoutExtent; + + set hasLayoutExtent(bool value) { + if (value == _hasLayoutExtent) return; + _hasLayoutExtent = value; + markNeedsLayout(); + } + + bool _computeIfFull(SliverConstraints cons) { + final RenderViewport viewport = parent as RenderViewport; + RenderSliver? sliverP = viewport.firstChild; + double totalScrollExtent = cons.precedingScrollExtent; + while (sliverP != this) { + if (sliverP is RenderSliverRefresh) { + totalScrollExtent -= sliverP.geometry!.scrollExtent; + break; + } + sliverP = viewport.childAfter(sliverP!); + } + // consider about footer layoutExtent,it should be subtracted it's height + return totalScrollExtent > cons.viewportMainAxisExtent; + } + + // many sitiuation: 1. reverse 2. not reverse + // 3. follow content 4. unfollow content + //5. not full 6. full + double? computePaintOrigin(double? layoutExtent, bool reverse, bool follow) { + if (follow) { + if (reverse) { + return layoutExtent; + } + return 0.0; + } else { + if (reverse) { + return math.max( + constraints.viewportMainAxisExtent - + constraints.precedingScrollExtent, + 0.0) + + layoutExtent!; + } else { + return math.max( + constraints.viewportMainAxisExtent - + constraints.precedingScrollExtent, + 0.0); + } + } + } + + @override + void debugAssertDoesMeetConstraints() { + assert(geometry!.debugAssertIsValid(informationCollector: () sync* { + yield describeForError( + 'The RenderSliver that returned the offending geometry was'); + })); + assert(() { + if (geometry!.paintExtent > constraints.remainingPaintExtent) { + throw FlutterError.fromParts([ + ErrorSummary( + 'SliverGeometry has a paintOffset that exceeds the remainingPaintExtent from the constraints.'), + describeForError( + 'The render object whose geometry violates the constraints is the following'), + ErrorDescription( + 'The paintExtent must cause the child sliver to paint within the viewport, and so ' + 'cannot exceed the remainingPaintExtent.', + ), + ]); + } + return true; + }()); + } + + @override + void performLayout() { + assert(constraints.growthDirection == GrowthDirection.forward); + if (child == null) { + geometry = SliverGeometry.zero; + return; + } + bool active; + if (hideWhenNotFull! && mode != LoadStatus.noMore) { + active = _computeIfFull(constraints); + } else { + active = true; + } + if (active) { + child!.layout(constraints.asBoxConstraints(), parentUsesSize: true); + } else { + child!.layout( + constraints.asBoxConstraints(maxExtent: 0.0, minExtent: 0.0), + parentUsesSize: true); + } + double childExtent = constraints.axis == Axis.vertical + ? child!.size.height + : child!.size.width; + final double paintedChildSize = + calculatePaintOffset(constraints, from: 0.0, to: childExtent); + final double cacheExtent = + calculateCacheOffset(constraints, from: 0.0, to: childExtent); + assert(paintedChildSize.isFinite); + assert(paintedChildSize >= 0.0); + if (active) { + // consider reverse loading and HideAlways==loadStyle + geometry = SliverGeometry( + scrollExtent: !_hasLayoutExtent! || !_computeIfFull(constraints) + ? 0 + : layoutExtent, + paintExtent: paintedChildSize, + // this need to fix later + paintOrigin: computePaintOrigin( + !_hasLayoutExtent! || !_computeIfFull(constraints) + ? layoutExtent + : 0.0, + constraints.axisDirection == AxisDirection.up || + constraints.axisDirection == AxisDirection.left, + _computeIfFull(constraints) || shouldFollowContent!)!, + cacheExtent: cacheExtent, + maxPaintExtent: childExtent, + hitTestExtent: paintedChildSize, + visible: true, + hasVisualOverflow: true, + ); + setChildParentData(child!, constraints, geometry!); + } else { + geometry = SliverGeometry.zero; + } + } +} + +class SliverRefreshBody extends SingleChildRenderObjectWidget { + /// Creates a sliver that contains a single box widget. + const SliverRefreshBody({ + super.key, + super.child, + }); + + @override + RenderSliverRefreshBody createRenderObject(BuildContext context) => + RenderSliverRefreshBody(); +} + +class RenderSliverRefreshBody extends RenderSliverSingleBoxAdapter { + /// Creates a [RenderSliver] that wraps a [RenderBox]. + RenderSliverRefreshBody({ + super.child, + }); + + @override + void performLayout() { + if (child == null) { + geometry = SliverGeometry.zero; + return; + } + child!.layout(constraints.asBoxConstraints(maxExtent: 1111111), + parentUsesSize: true); + double? childExtent; + switch (constraints.axis) { + case Axis.horizontal: + childExtent = child!.size.width; + break; + case Axis.vertical: + childExtent = child!.size.height; + break; + } + if (childExtent == 1111111) { + child!.layout( + constraints.asBoxConstraints( + maxExtent: constraints.viewportMainAxisExtent), + parentUsesSize: true); + } + switch (constraints.axis) { + case Axis.horizontal: + childExtent = child!.size.width; + break; + case Axis.vertical: + childExtent = child!.size.height; + break; + } + final double paintedChildSize = + calculatePaintOffset(constraints, from: 0.0, to: childExtent); + final double cacheExtent = + calculateCacheOffset(constraints, from: 0.0, to: childExtent); + + assert(paintedChildSize.isFinite); + assert(paintedChildSize >= 0.0); + geometry = SliverGeometry( + scrollExtent: childExtent, + paintExtent: paintedChildSize, + cacheExtent: cacheExtent, + maxPaintExtent: childExtent, + hitTestExtent: paintedChildSize, + hasVisualOverflow: childExtent > constraints.remainingPaintExtent || + constraints.scrollOffset > 0.0, + ); + setChildParentData(child!, constraints, geometry!); + } +} diff --git a/lib/core/util/common/pull_to_refresh/src/smart_refresher.dart b/lib/core/util/common/pull_to_refresh/src/smart_refresher.dart new file mode 100644 index 0000000..a8f310f --- /dev/null +++ b/lib/core/util/common/pull_to_refresh/src/smart_refresher.dart @@ -0,0 +1,1065 @@ +// ignore_for_file: overridden_fields, annotate_overrides, constant_identifier_names + +// Flutter imports: +import 'package:flutter/foundation.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/widgets.dart'; + +// Project imports: +import 'package:streamskit_mobile/core/util/common/pull_to_refresh/src/internals/slivers.dart'; +import 'indicator/classic_indicator.dart'; +import 'indicator/material_indicator.dart'; +import 'internals/indicator_wrap.dart'; +import 'internals/refresh_physics.dart'; + +// ignore_for_file: INVALID_USE_OF_PROTECTED_MEMBER +// ignore_for_file: INVALID_USE_OF_VISIBLE_FOR_TESTING_MEMBER +// ignore_for_file: DEPRECATED_MEMBER_USE + +/// when viewport not full one page, for different state,whether it should follow the content +typedef OnTwoLevel = void Function(bool isOpen); + +/// when viewport not full one page, for different state,whether it should follow the content +typedef ShouldFollowContent = bool Function(LoadStatus? status); + +/// global default indicator builder +typedef IndicatorBuilder = Widget Function(); + +/// a builder for attaching refresh function with the physics +typedef RefresherBuilder = Widget Function( + BuildContext context, RefreshPhysics physics); + +/// header state +enum RefreshStatus { + /// Initial state, when not being overscrolled into, or after the overscroll + /// is canceled or after done and the sliver retracted away. + idle, + + /// Dragged far enough that the onRefresh callback will callback + canRefresh, + + /// the indicator is refreshing,waiting for the finish callback + refreshing, + + /// the indicator refresh completed + completed, + + /// the indicator refresh failed + failed, + + /// Dragged far enough that the onTwoLevel callback will callback + canTwoLevel, + + /// indicator is opening twoLevel + twoLevelOpening, + + /// indicator is in twoLevel + twoLeveling, + + /// indicator is closing twoLevel + twoLevelClosing +} + +/// footer state +enum LoadStatus { + /// Initial state, which can be triggered loading more by gesture pull up + idle, + + canLoading, + + /// indicator is loading more data + loading, + + /// indicator is no more data to loading,this state doesn't allow to load more whatever + noMore, + + /// indicator load failed,Initial state, which can be click retry,If you need to pull up trigger load more,you should set enableLoadingWhenFailed = true in RefreshConfiguration + failed +} + +/// header indicator display style +enum RefreshStyle { + // indicator box always follow content + Follow, + // indicator box follow content,When the box reaches the top and is fully visible, it does not follow content. + UnFollow, + + /// Let the indicator size zoom in with the boundary distance,look like showing behind the content + Behind, + + /// this style just like flutter RefreshIndicator,showing above the content + Front +} + +/// footer indicator display style +enum LoadStyle { + /// indicator always own layoutExtent whatever the state + ShowAlways, + + /// indicator always own 0.0 layoutExtent whatever the state + HideAlways, + + /// indicator always own layoutExtent when loading state, the other state is 0.0 layoutExtent + ShowWhenLoading +} + +/// This is the most important component that provides drop-down refresh and up loading. +/// [RefreshController] must not be null,Only one controller to one SmartRefresher +/// +/// header,I have finished a lot indicators,you can checkout [ClassicHeader],[WaterDropMaterialHeader],[MaterialClassicHeader],[WaterDropHeader],[BezierCircleHeader] +/// footer,[ClassicFooter] +///If you need to custom header or footer,You should check out [CustomHeader] or [CustomFooter] +/// +/// See also: +/// +/// * [RefreshConfiguration], A global configuration for all SmartRefresher in subtrees +/// +/// * [RefreshController], A controller controll header and footer indicators state +class SmartRefresher extends StatefulWidget { + /// Refresh Content + /// + /// notice that: If child is extends ScrollView,It will help you get the internal slivers and add footer and header in it. + /// else it will put child into SliverToBoxAdapter and add footer and header + final Widget? child; + + /// header indicator displace before content + /// + /// If reverse is false,header displace at the top of content. + /// If reverse is true,header displace at the bottom of content. + /// if scrollDirection = Axis.horizontal,it will display at left or right + /// + /// from 1.5.2,it has been change RefreshIndicator to Widget,but remember only pass sliver widget, + /// if you pass not a sliver,it will throw error + final Widget? header; + + /// footer indicator display after content + /// + /// If reverse is true,header displace at the top of content. + /// If reverse is false,header displace at the bottom of content. + /// if scrollDirection = Axis.horizontal,it will display at left or right + /// + /// from 1.5.2,it has been change LoadIndicator to Widget,but remember only pass sliver widget, + // if you pass not a sliver,it will throw error + final Widget? footer; + // This bool will affect whether or not to have the function of drop-up load. + final bool enablePullUp; + + /// controll whether open the second floor function + final bool enableTwoLevel; + + /// This bool will affect whether or not to have the function of drop-down refresh. + final bool enablePullDown; + + /// callback when header refresh + /// + /// when the callback is happening,you should use [RefreshController] + /// to end refreshing state,else it will keep refreshing state + final VoidCallback? onRefresh; + + /// callback when footer loading more data + /// + /// when the callback is happening,you should use [RefreshController] + /// to end loading state,else it will keep loading state + final VoidCallback? onLoading; + + /// callback when header ready to twoLevel + /// + /// If you want to close twoLevel,you should use [RefreshController.closeTwoLevel] + final OnTwoLevel? onTwoLevel; + + /// Controll inner state + final RefreshController controller; + + /// child content builder + final RefresherBuilder? builder; + + /// copy from ScrollView,for setting in SingleChildView,not ScrollView + final Axis? scrollDirection; + + /// copy from ScrollView,for setting in SingleChildView,not ScrollView + final bool? reverse; + + /// copy from ScrollView,for setting in SingleChildView,not ScrollView + final ScrollController? scrollController; + + /// copy from ScrollView,for setting in SingleChildView,not ScrollView + final bool? primary; + + /// copy from ScrollView,for setting in SingleChildView,not ScrollView + final ScrollPhysics? physics; + + /// copy from ScrollView,for setting in SingleChildView,not ScrollView + final double? cacheExtent; + + /// copy from ScrollView,for setting in SingleChildView,not ScrollView + final int? semanticChildCount; + + /// copy from ScrollView,for setting in SingleChildView,not ScrollView + final DragStartBehavior? dragStartBehavior; + + /// creates a widget help attach the refresh and load more function + /// controller must not be null, + /// child is your refresh content,Note that there's a big difference between children inheriting from ScrollView or not. + /// If child is extends ScrollView,inner will get the slivers from ScrollView,if not,inner will wrap child into SliverToBoxAdapter. + /// If your child inner container Scrollable,please consider about converting to Sliver,and use CustomScrollView,or use [builder] constructor + /// such as AnimatedList,RecordableList,doesn't allow to put into child,it will wrap it into SliverToBoxAdapter + /// If you don't need pull down refresh ,just enablePullDown = false, + /// If you need pull up load ,just enablePullUp = true + const SmartRefresher( + {super.key, + required this.controller, + this.child, + this.header, + this.footer, + this.enablePullDown = true, + this.enablePullUp = false, + this.enableTwoLevel = false, + this.onRefresh, + this.onLoading, + this.onTwoLevel, + this.dragStartBehavior, + this.primary, + this.cacheExtent, + this.semanticChildCount, + this.reverse, + this.physics, + this.scrollDirection, + this.scrollController}) + : builder = null; + + /// creates a widget help attach the refresh and load more function + /// controller must not be null,builder must not be null + /// this constructor use to handle some special third party widgets,this widget need to pass slivers ,but they are + /// not extends ScrollView,so my widget inner will wrap child to SliverToBoxAdapter,which cause scrollable wrapping scrollable. + /// for example,NestedScrollView is a StalessWidget,it's headerSliversbuilder can return a slivers array,So if we want to do + /// refresh above NestedScrollVIew,we must use this constrctor to implements refresh above NestedScrollView,but for now,NestedScrollView + /// can not support overscroll out of edge + const SmartRefresher.builder({ + super.key, + required this.controller, + required this.builder, + this.enablePullDown = true, + this.enablePullUp = false, + this.enableTwoLevel = false, + this.onRefresh, + this.onLoading, + this.onTwoLevel, + }) : header = null, + footer = null, + child = null, + scrollController = null, + scrollDirection = null, + physics = null, + reverse = null, + semanticChildCount = null, + dragStartBehavior = null, + cacheExtent = null, + primary = null; + + static SmartRefresher? of(BuildContext? context) { + return context!.findAncestorWidgetOfExactType(); + } + + static SmartRefresherState? ofState(BuildContext? context) { + return context!.findAncestorStateOfType(); + } + + @override + State createState() { + return SmartRefresherState(); + } +} + +class SmartRefresherState extends State { + RefreshPhysics? _physics; + bool _updatePhysics = false; + double viewportExtent = 0; + bool _canDrag = true; + + final RefreshIndicator defaultHeader = + defaultTargetPlatform == TargetPlatform.iOS + ? const ClassicHeader() + : const MaterialClassicHeader(); + + final LoadIndicator defaultFooter = const ClassicFooter(); + + //build slivers from child Widget + List? _buildSliversByChild(BuildContext context, Widget? child, + RefreshConfiguration? configuration) { + List? slivers; + if (child is ScrollView) { + if (child is BoxScrollView) { + //avoid system inject padding when own indicator top or bottom + Widget sliver = child.buildChildLayout(context); + if (child.padding != null) { + slivers = [SliverPadding(sliver: sliver, padding: child.padding!)]; + } else { + slivers = [sliver]; + } + } else { + slivers = List.from(child.buildSlivers(context), growable: true); + } + } else if (child is! Scrollable) { + slivers = [ + SliverRefreshBody( + child: child ?? Container(), + ) + ]; + } + if (widget.enablePullDown || widget.enableTwoLevel) { + slivers?.insert( + 0, + widget.header ?? + (configuration?.headerBuilder != null + ? configuration?.headerBuilder!() + : null) ?? + defaultHeader); + } + //insert header or footer + if (widget.enablePullUp) { + slivers?.add(widget.footer ?? + (configuration?.footerBuilder != null + ? configuration?.footerBuilder!() + : null) ?? + defaultFooter); + } + + return slivers; + } + + ScrollPhysics _getScrollPhysics( + RefreshConfiguration? conf, ScrollPhysics physics) { + final bool isBouncingPhysics = physics is BouncingScrollPhysics || + (physics is AlwaysScrollableScrollPhysics && + ScrollConfiguration.of(context) + .getScrollPhysics(context) + .runtimeType == + BouncingScrollPhysics); + return _physics = RefreshPhysics( + dragSpeedRatio: conf?.dragSpeedRatio ?? 1, + springDescription: conf?.springDescription ?? + const SpringDescription( + mass: 2.2, + stiffness: 150, + damping: 16, + ), + controller: widget.controller, + enableScrollWhenTwoLevel: conf?.enableScrollWhenTwoLevel ?? true, + updateFlag: _updatePhysics ? 0 : 1, + enableScrollWhenRefreshCompleted: + conf?.enableScrollWhenRefreshCompleted ?? false, + maxUnderScrollExtent: conf?.maxUnderScrollExtent ?? + (isBouncingPhysics ? double.infinity : 0.0), + maxOverScrollExtent: conf?.maxOverScrollExtent ?? + (isBouncingPhysics ? double.infinity : 60.0), + topHitBoundary: conf?.topHitBoundary ?? + (isBouncingPhysics + ? double.infinity + : 0.0), // need to fix default value by ios or android later + bottomHitBoundary: conf?.bottomHitBoundary ?? + (isBouncingPhysics ? double.infinity : 0.0)) + .applyTo(!_canDrag ? const NeverScrollableScrollPhysics() : physics); + } + + // build the customScrollView + Widget? _buildBodyBySlivers( + Widget? childView, List? slivers, RefreshConfiguration? conf) { + Widget? body; + if (childView is! Scrollable) { + bool? primary = widget.primary; + Key? key; + double? cacheExtent = widget.cacheExtent; + + Axis? scrollDirection = widget.scrollDirection; + int? semanticChildCount = widget.semanticChildCount; + bool? reverse = widget.reverse; + ScrollController? scrollController = widget.scrollController; + DragStartBehavior? dragStartBehavior = widget.dragStartBehavior; + ScrollPhysics? physics = widget.physics; + Key? center; + double? anchor; + ScrollViewKeyboardDismissBehavior? keyboardDismissBehavior; + String? restorationId; + Clip? clipBehavior; + + if (childView is ScrollView) { + primary = primary ?? childView.primary; + cacheExtent = cacheExtent ?? childView.cacheExtent; + key = key ?? childView.key; + semanticChildCount = semanticChildCount ?? childView.semanticChildCount; + reverse = reverse ?? childView.reverse; + dragStartBehavior = dragStartBehavior ?? childView.dragStartBehavior; + scrollDirection = scrollDirection ?? childView.scrollDirection; + physics = physics ?? childView.physics; + center = center ?? childView.center; + anchor = anchor ?? childView.anchor; + keyboardDismissBehavior = + keyboardDismissBehavior ?? childView.keyboardDismissBehavior; + restorationId = restorationId ?? childView.restorationId; + clipBehavior = clipBehavior ?? childView.clipBehavior; + scrollController = scrollController ?? childView.controller; + } + body = CustomScrollView( + // ignore: DEPRECATED_MEMBER_USE_FROM_SAME_PACKAGE + controller: scrollController, + cacheExtent: cacheExtent, + key: key, + scrollDirection: scrollDirection ?? Axis.vertical, + semanticChildCount: semanticChildCount, + primary: primary, + clipBehavior: clipBehavior ?? Clip.hardEdge, + keyboardDismissBehavior: + keyboardDismissBehavior ?? ScrollViewKeyboardDismissBehavior.manual, + anchor: anchor ?? 0.0, + restorationId: restorationId, + center: center, + physics: _getScrollPhysics( + conf, physics ?? const AlwaysScrollableScrollPhysics()), + slivers: slivers!, + dragStartBehavior: dragStartBehavior ?? DragStartBehavior.start, + reverse: reverse ?? false, + ); + } else { + body = Scrollable( + physics: _getScrollPhysics( + conf, childView.physics ?? const AlwaysScrollableScrollPhysics()), + controller: childView.controller, + axisDirection: childView.axisDirection, + semanticChildCount: childView.semanticChildCount, + dragStartBehavior: childView.dragStartBehavior, + viewportBuilder: (context, offset) { + Viewport viewport = + childView.viewportBuilder(context, offset) as Viewport; + if (widget.enablePullDown) { + viewport.children.insert( + 0, + widget.header ?? + (conf?.headerBuilder != null + ? conf?.headerBuilder!() + : null) ?? + defaultHeader); + } + //insert header or footer + if (widget.enablePullUp) { + viewport.children.add(widget.footer ?? + (conf?.footerBuilder != null ? conf?.footerBuilder!() : null) ?? + defaultFooter); + } + return viewport; + }, + ); + } + return body; + } + + bool _ifNeedUpdatePhysics() { + RefreshConfiguration? conf = RefreshConfiguration.of(context); + if (conf == null || _physics == null) { + return false; + } + + if (conf.topHitBoundary != _physics!.topHitBoundary || + _physics!.bottomHitBoundary != conf.bottomHitBoundary || + conf.maxOverScrollExtent != _physics!.maxOverScrollExtent || + _physics!.maxUnderScrollExtent != conf.maxUnderScrollExtent || + _physics!.dragSpeedRatio != conf.dragSpeedRatio || + _physics!.enableScrollWhenTwoLevel != conf.enableScrollWhenTwoLevel || + _physics!.enableScrollWhenRefreshCompleted != + conf.enableScrollWhenRefreshCompleted) { + return true; + } + return false; + } + + void setCanDrag(bool canDrag) { + if (_canDrag == canDrag) { + return; + } + setState(() { + _canDrag = canDrag; + }); + } + + @override + void didUpdateWidget(SmartRefresher oldWidget) { + if (widget.controller != oldWidget.controller) { + widget.controller.headerMode!.value = + oldWidget.controller.headerMode!.value; + widget.controller.footerMode!.value = + oldWidget.controller.footerMode!.value; + } + super.didUpdateWidget(oldWidget); + } + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + if (_ifNeedUpdatePhysics()) { + _updatePhysics = !_updatePhysics; + } + } + + @override + void initState() { + if (widget.controller.initialRefresh) { + WidgetsBinding.instance.addPostFrameCallback((_) { + // if mounted,it avoid one situation: when init done,then dispose the widget before build. + // this situation mostly TabBarView + if (mounted) widget.controller.requestRefresh(); + }); + } + widget.controller._bindState(this); + super.initState(); + } + + @override + void dispose() { + widget.controller._detachPosition(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final RefreshConfiguration? configuration = + RefreshConfiguration.of(context); + Widget? body; + if (widget.builder != null) { + body = widget.builder!( + context, + _getScrollPhysics( + configuration, const AlwaysScrollableScrollPhysics()) + as RefreshPhysics); + } else { + List? slivers = + _buildSliversByChild(context, widget.child, configuration); + body = _buildBodyBySlivers(widget.child, slivers, configuration); + } + if (configuration == null) { + body = RefreshConfiguration(child: body!); + } + return LayoutBuilder( + builder: (c2, cons) { + viewportExtent = cons.biggest.height; + return body!; + }, + ); + } +} + +/// A controller controll header and footer state, +/// it can trigger driving request Refresh ,set the initalRefresh,status if needed +/// +/// See also: +/// +/// * [SmartRefresher],a widget help you attach refresh and load more function easily +class RefreshController { + SmartRefresherState? _refresherState; + + /// header status mode controll + RefreshNotifier? headerMode; + + /// footer status mode controll + RefreshNotifier? footerMode; + + /// the scrollable inner's position + /// + /// notice that: position is null before build, + /// the value is get when the header or footer callback onPositionUpdated + ScrollPosition? position; + + RefreshStatus? get headerStatus => headerMode?.value; + + LoadStatus? get footerStatus => footerMode?.value; + + bool get isRefresh => headerMode?.value == RefreshStatus.refreshing; + + bool get isTwoLevel => + headerMode?.value == RefreshStatus.twoLeveling || + headerMode?.value == RefreshStatus.twoLevelOpening || + headerMode?.value == RefreshStatus.twoLevelClosing; + + bool get isLoading => footerMode?.value == LoadStatus.loading; + + final bool initialRefresh; + + /// initialRefresh:When SmartRefresher is init,it will call requestRefresh at once + /// + /// initialRefreshStatus: headerMode default value + /// + /// initialLoadStatus: footerMode default value + RefreshController( + {this.initialRefresh = false, + RefreshStatus? initialRefreshStatus, + LoadStatus? initialLoadStatus}) { + headerMode = RefreshNotifier(initialRefreshStatus ?? RefreshStatus.idle); + footerMode = RefreshNotifier(initialLoadStatus ?? LoadStatus.idle); + } + + void _bindState(SmartRefresherState state) { + assert(_refresherState == null, + "Don't use one refreshController to multiple SmartRefresher,It will cause some unexpected bugs mostly in TabBarView"); + _refresherState = state; + } + + /// callback when the indicator is builded,and catch the scrollable's inner position + void onPositionUpdated(ScrollPosition newPosition) { + position?.isScrollingNotifier.removeListener(_listenScrollEnd); + position = newPosition; + position!.isScrollingNotifier.addListener(_listenScrollEnd); + } + + void _detachPosition() { + _refresherState = null; + position?.isScrollingNotifier.removeListener(_listenScrollEnd); + } + + StatefulElement? _findIndicator(BuildContext context, Type elementType) { + StatefulElement? result; + context.visitChildElements((Element e) { + if (elementType == RefreshIndicator) { + if (e.widget is RefreshIndicator) { + result = e as StatefulElement?; + } + } else { + if (e.widget is LoadIndicator) { + result = e as StatefulElement?; + } + } + + result ??= _findIndicator(e, elementType); + }); + return result; + } + + /// when bounce out of edge and stopped by overScroll or underScroll, it should be SpringBack to 0.0 + /// but ScrollPhysics didn't provide one way to spring back when outOfEdge(stopped by applyBouncingCondition return != 0.0) + /// so for making it spring back, it should be trigger goBallistic make it spring back + void _listenScrollEnd() { + if (position != null && position!.outOfRange) { + position?.activity?.applyNewDimensions(); + } + } + + /// make the header enter refreshing state,and callback onRefresh + Future? requestRefresh( + {bool needMove = true, + bool needCallback = true, + Duration duration = const Duration(milliseconds: 500), + Curve curve = Curves.linear}) { + assert(position != null, + 'Try not to call requestRefresh() before build,please call after the ui was rendered'); + if (isRefresh) return Future.value(); + StatefulElement? indicatorElement = + _findIndicator(position!.context.storageContext, RefreshIndicator); + + if (indicatorElement == null || _refresherState == null) return null; + (indicatorElement.state as RefreshIndicatorState).floating = true; + + if (needMove && _refresherState!.mounted) { + _refresherState!.setCanDrag(false); + } + if (needMove) { + return Future.delayed(const Duration(milliseconds: 50)).then((_) async { + // - 0.0001 is for NestedScrollView. + await position + ?.animateTo(position!.minScrollExtent - 0.0001, + duration: duration, curve: curve) + .then((_) { + if (_refresherState != null && _refresherState!.mounted) { + _refresherState!.setCanDrag(true); + if (needCallback) { + headerMode!.value = RefreshStatus.refreshing; + } else { + headerMode!.setValueWithNoNotify(RefreshStatus.refreshing); + if (indicatorElement.state.mounted) { + (indicatorElement.state as RefreshIndicatorState) + .setState(() {}); + } + } + } + }); + }); + } else { + Future.value().then((_) { + headerMode!.value = RefreshStatus.refreshing; + }); + } + return null; + } + + /// make the header enter refreshing state,and callback onRefresh + Future requestTwoLevel( + {Duration duration = const Duration(milliseconds: 300), + Curve curve = Curves.linear}) { + assert(position != null, + 'Try not to call requestRefresh() before build,please call after the ui was rendered'); + headerMode!.value = RefreshStatus.twoLevelOpening; + return Future.delayed(const Duration(milliseconds: 50)).then((_) async { + await position?.animateTo(position!.minScrollExtent, + duration: duration, curve: curve); + }); + } + + /// make the footer enter loading state,and callback onLoading + Future? requestLoading( + {bool needMove = true, + bool needCallback = true, + Duration duration = const Duration(milliseconds: 300), + Curve curve = Curves.linear}) { + assert(position != null, + 'Try not to call requestLoading() before build,please call after the ui was rendered'); + if (isLoading) return Future.value(); + StatefulElement? indicatorElement = + _findIndicator(position!.context.storageContext, LoadIndicator); + + if (indicatorElement == null || _refresherState == null) return null; + (indicatorElement.state as LoadIndicatorState).floating = true; + if (needMove && _refresherState!.mounted) { + _refresherState!.setCanDrag(false); + } + if (needMove) { + return Future.delayed(const Duration(milliseconds: 50)).then((_) async { + await position + ?.animateTo(position!.maxScrollExtent, + duration: duration, curve: curve) + .then((_) { + if (_refresherState != null && _refresherState!.mounted) { + _refresherState!.setCanDrag(true); + if (needCallback) { + footerMode!.value = LoadStatus.loading; + } else { + footerMode!.setValueWithNoNotify(LoadStatus.loading); + if (indicatorElement.state.mounted) { + (indicatorElement.state as LoadIndicatorState).setState(() {}); + } + } + } + }); + }); + } else { + return Future.value().then((_) { + footerMode!.value = LoadStatus.loading; + }); + } + } + + /// request complete,the header will enter complete state, + /// + /// resetFooterState : it will set the footer state from noData to idle + void refreshCompleted({bool resetFooterState = false}) { + headerMode?.value = RefreshStatus.completed; + if (resetFooterState) { + resetNoData(); + } + } + + /// end twoLeveling,will return back first floor + Future? twoLevelComplete( + {Duration duration = const Duration(milliseconds: 500), + Curve curve = Curves.linear}) { + headerMode?.value = RefreshStatus.twoLevelClosing; + WidgetsBinding.instance.addPostFrameCallback((_) { + position! + .animateTo(0.0, duration: duration, curve: curve) + .whenComplete(() { + headerMode!.value = RefreshStatus.idle; + }); + }); + return null; + } + + /// request failed,the header display failed state + void refreshFailed() { + headerMode?.value = RefreshStatus.failed; + } + + /// not show success or failed, it will set header state to idle and spring back at once + void refreshToIdle() { + headerMode?.value = RefreshStatus.idle; + } + + /// after data returned,set the footer state to idle + void loadComplete() { + // change state after ui update,else it will have a bug:twice loading + WidgetsBinding.instance.addPostFrameCallback((_) { + footerMode?.value = LoadStatus.idle; + }); + } + + /// If catchError happen,you may call loadFailed indicate fetch data from network failed + void loadFailed() { + // change state after ui update,else it will have a bug:twice loading + WidgetsBinding.instance.addPostFrameCallback((_) { + footerMode?.value = LoadStatus.failed; + }); + } + + /// load more success without error,but no data returned + void loadNoData() { + WidgetsBinding.instance.addPostFrameCallback((_) { + footerMode?.value = LoadStatus.noMore; + }); + } + + /// reset footer noData state to idle + void resetNoData() { + if (footerMode?.value == LoadStatus.noMore) { + footerMode!.value = LoadStatus.idle; + } + } + + /// for some special situation, you should call dispose() for safe,it may throw errors after parent widget dispose + void dispose() { + headerMode!.dispose(); + footerMode!.dispose(); + headerMode = null; + footerMode = null; + } +} + +/// Controls how SmartRefresher widgets behave in a subtree.the usage just like [ScrollConfiguration] +/// +/// The refresh configuration determines smartRefresher some behaviours,global setting default indicator +/// +/// see also: +/// +/// * [SmartRefresher], a widget help attach the refresh and load more function +class RefreshConfiguration extends InheritedWidget { + final Widget child; + + /// global default header builder + final IndicatorBuilder? headerBuilder; + + /// global default footer builder + final IndicatorBuilder? footerBuilder; + + /// custom spring animate + final SpringDescription springDescription; + + /// If need to refreshing now when reaching triggerDistance + final bool skipCanRefresh; + + /// if it should follow content for different state + final ShouldFollowContent? shouldFooterFollowWhenNotFull; + + /// when listView data small(not enough one page) , it should be hide + final bool hideFooterWhenNotFull; + + /// whether user can drag viewport when twoLeveling + final bool enableScrollWhenTwoLevel; + + /// whether user can drag viewport when refresh complete and spring back + final bool enableScrollWhenRefreshCompleted; + + /// whether trigger refresh by BallisticScrollActivity + final bool enableBallisticRefresh; + + /// whether trigger loading by BallisticScrollActivity + final bool enableBallisticLoad; + + /// whether footer can trigger load by reaching footerDistance when failed state + final bool enableLoadingWhenFailed; + + /// whether footer can trigger load by reaching footerDistance when inNoMore state + final bool enableLoadingWhenNoData; + + /// overScroll distance of trigger refresh + final double headerTriggerDistance; + + /// the overScroll distance of trigger twoLevel + final double twiceTriggerDistance; + + /// Close the bottom crossing distance on the second floor, premise:enableScrollWhenTwoLevel is true + final double closeTwoLevelDistance; + + /// the extentAfter distance of trigger loading + final double footerTriggerDistance; + + /// the speed ratio when dragging overscroll ,compute=origin physics dragging speed *dragSpeedRatio + final double dragSpeedRatio; + + /// max overScroll distance when out of edge + final double? maxOverScrollExtent; + + /// max underScroll distance when out of edge + final double? maxUnderScrollExtent; + + /// The boundary is located at the top edge and stops when inertia rolls over the boundary distance + final double? topHitBoundary; + + /// The boundary is located at the bottom edge and stops when inertia rolls under the boundary distance + final double? bottomHitBoundary; + + /// toggle of refresh vibrate + final bool enableRefreshVibrate; + + /// toggle of loadmore vibrate + final bool enableLoadMoreVibrate; + + const RefreshConfiguration( + {super.key, + required this.child, + this.headerBuilder, + this.footerBuilder, + this.dragSpeedRatio = 1.0, + this.shouldFooterFollowWhenNotFull, + this.enableScrollWhenTwoLevel = true, + this.enableLoadingWhenNoData = false, + this.enableBallisticRefresh = false, + this.springDescription = const SpringDescription( + mass: 2.2, + stiffness: 150, + damping: 16, + ), + this.enableScrollWhenRefreshCompleted = false, + this.enableLoadingWhenFailed = true, + this.twiceTriggerDistance = 150.0, + this.closeTwoLevelDistance = 80.0, + this.skipCanRefresh = false, + this.maxOverScrollExtent, + this.enableBallisticLoad = true, + this.maxUnderScrollExtent, + this.headerTriggerDistance = 80.0, + this.footerTriggerDistance = 15.0, + this.hideFooterWhenNotFull = false, + this.enableRefreshVibrate = false, + this.enableLoadMoreVibrate = false, + this.topHitBoundary, + this.bottomHitBoundary}) + : assert(headerTriggerDistance > 0), + assert(twiceTriggerDistance > 0), + assert(closeTwoLevelDistance > 0), + assert(dragSpeedRatio > 0), + super(child: child); + + /// Construct RefreshConfiguration to copy attributes from ancestor nodes + /// If the parameter is null, it will automatically help you to absorb the attributes of your ancestor Refresh Configuration, instead of having to copy them manually by yourself. + /// + /// it mostly use in some stiuation is different the other SmartRefresher in App + RefreshConfiguration.copyAncestor({ + super.key, + required BuildContext context, + required this.child, + IndicatorBuilder? headerBuilder, + IndicatorBuilder? footerBuilder, + double? dragSpeedRatio, + ShouldFollowContent? shouldFooterFollowWhenNotFull, + bool? enableScrollWhenTwoLevel, + bool? enableBallisticRefresh, + bool? enableBallisticLoad, + bool? enableLoadingWhenNoData, + SpringDescription? springDescription, + bool? enableScrollWhenRefreshCompleted, + bool? enableLoadingWhenFailed, + double? twiceTriggerDistance, + double? closeTwoLevelDistance, + bool? skipCanRefresh, + double? maxOverScrollExtent, + double? maxUnderScrollExtent, + double? topHitBoundary, + double? bottomHitBoundary, + double? headerTriggerDistance, + double? footerTriggerDistance, + bool? enableRefreshVibrate, + bool? enableLoadMoreVibrate, + bool? hideFooterWhenNotFull, + }) : assert(RefreshConfiguration.of(context) != null, + "search RefreshConfiguration anscestor return null,please Make sure that RefreshConfiguration is the ancestor of that element"), + headerBuilder = + headerBuilder ?? RefreshConfiguration.of(context)!.headerBuilder, + footerBuilder = + footerBuilder ?? RefreshConfiguration.of(context)!.footerBuilder, + dragSpeedRatio = + dragSpeedRatio ?? RefreshConfiguration.of(context)!.dragSpeedRatio, + twiceTriggerDistance = twiceTriggerDistance ?? + RefreshConfiguration.of(context)!.twiceTriggerDistance, + headerTriggerDistance = headerTriggerDistance ?? + RefreshConfiguration.of(context)!.headerTriggerDistance, + footerTriggerDistance = footerTriggerDistance ?? + RefreshConfiguration.of(context)!.footerTriggerDistance, + springDescription = springDescription ?? + RefreshConfiguration.of(context)!.springDescription, + hideFooterWhenNotFull = hideFooterWhenNotFull ?? + RefreshConfiguration.of(context)!.hideFooterWhenNotFull, + maxOverScrollExtent = maxOverScrollExtent ?? + RefreshConfiguration.of(context)!.maxOverScrollExtent, + maxUnderScrollExtent = maxUnderScrollExtent ?? + RefreshConfiguration.of(context)!.maxUnderScrollExtent, + topHitBoundary = + topHitBoundary ?? RefreshConfiguration.of(context)!.topHitBoundary, + bottomHitBoundary = bottomHitBoundary ?? + RefreshConfiguration.of(context)!.bottomHitBoundary, + skipCanRefresh = + skipCanRefresh ?? RefreshConfiguration.of(context)!.skipCanRefresh, + enableScrollWhenRefreshCompleted = enableScrollWhenRefreshCompleted ?? + RefreshConfiguration.of(context)!.enableScrollWhenRefreshCompleted, + enableScrollWhenTwoLevel = enableScrollWhenTwoLevel ?? + RefreshConfiguration.of(context)!.enableScrollWhenTwoLevel, + enableBallisticRefresh = enableBallisticRefresh ?? + RefreshConfiguration.of(context)!.enableBallisticRefresh, + enableBallisticLoad = enableBallisticLoad ?? + RefreshConfiguration.of(context)!.enableBallisticLoad, + enableLoadingWhenNoData = enableLoadingWhenNoData ?? + RefreshConfiguration.of(context)!.enableLoadingWhenNoData, + enableLoadingWhenFailed = enableLoadingWhenFailed ?? + RefreshConfiguration.of(context)!.enableLoadingWhenFailed, + closeTwoLevelDistance = closeTwoLevelDistance ?? + RefreshConfiguration.of(context)!.closeTwoLevelDistance, + enableRefreshVibrate = enableRefreshVibrate ?? + RefreshConfiguration.of(context)!.enableRefreshVibrate, + enableLoadMoreVibrate = enableLoadMoreVibrate ?? + RefreshConfiguration.of(context)!.enableLoadMoreVibrate, + shouldFooterFollowWhenNotFull = shouldFooterFollowWhenNotFull ?? + RefreshConfiguration.of(context)!.shouldFooterFollowWhenNotFull, + super(child: child); + + static RefreshConfiguration? of(BuildContext context) { + return context.dependOnInheritedWidgetOfExactType(); + } + + @override + bool updateShouldNotify(RefreshConfiguration oldWidget) { + return skipCanRefresh != oldWidget.skipCanRefresh || + hideFooterWhenNotFull != oldWidget.hideFooterWhenNotFull || + dragSpeedRatio != oldWidget.dragSpeedRatio || + enableScrollWhenRefreshCompleted != + oldWidget.enableScrollWhenRefreshCompleted || + enableBallisticRefresh != oldWidget.enableBallisticRefresh || + enableScrollWhenTwoLevel != oldWidget.enableScrollWhenTwoLevel || + closeTwoLevelDistance != oldWidget.closeTwoLevelDistance || + footerTriggerDistance != oldWidget.footerTriggerDistance || + headerTriggerDistance != oldWidget.headerTriggerDistance || + twiceTriggerDistance != oldWidget.twiceTriggerDistance || + maxUnderScrollExtent != oldWidget.maxUnderScrollExtent || + oldWidget.maxOverScrollExtent != maxOverScrollExtent || + enableBallisticRefresh != oldWidget.enableBallisticRefresh || + enableLoadingWhenFailed != oldWidget.enableLoadingWhenFailed || + topHitBoundary != oldWidget.topHitBoundary || + enableRefreshVibrate != oldWidget.enableRefreshVibrate || + enableLoadMoreVibrate != oldWidget.enableLoadMoreVibrate || + bottomHitBoundary != oldWidget.bottomHitBoundary; + } +} + +class RefreshNotifier extends ChangeNotifier implements ValueListenable { + /// Creates a [ChangeNotifier] that wraps this value. + RefreshNotifier(this._value); + T _value; + + @override + T get value => _value; + + set value(T newValue) { + if (_value == newValue) return; + _value = newValue; + notifyListeners(); + } + + void setValueWithNoNotify(T newValue) { + if (_value == newValue) return; + _value = newValue; + } + + @override + String toString() => '${describeIdentity(this)}($value)'; +} diff --git a/lib/core/util/common/touchable_opacity.dart b/lib/core/util/common/touchable_opacity.dart new file mode 100644 index 0000000..6443d09 --- /dev/null +++ b/lib/core/util/common/touchable_opacity.dart @@ -0,0 +1,53 @@ +// Flutter imports: +import 'package:flutter/material.dart'; + +class TouchableOpacity extends StatefulWidget { + final Function? onTap; + final Function? onLongPress; + final Widget child; + + const TouchableOpacity({ + super.key, + required this.child, + this.onTap, + this.onLongPress, + }); + + @override + State createState() => _TouchableOpacityState(); +} + +class _TouchableOpacityState extends State { + bool enable = false; + @override + Widget build(BuildContext context) { + return GestureDetector( + onLongPress: () { + if (!enable && widget.onLongPress != null) { + widget.onLongPress!(); + } + }, + onTap: () { + if (!enable && widget.onTap != null) { + widget.onTap!(); + } + }, + onTapDown: (a) { + setState(() { + enable = true; + }); + }, + onTapUp: (a) { + setState(() { + enable = false; + }); + }, + onTapCancel: () { + setState(() { + enable = false; + }); + }, + child: Opacity(opacity: enable ? 0.5 : 1, child: widget.child), + ); + } +} diff --git a/lib/core/util/custom_image/custom_netword_image.dart b/lib/core/util/custom_image/custom_netword_image.dart new file mode 100644 index 0000000..9bdd40c --- /dev/null +++ b/lib/core/util/custom_image/custom_netword_image.dart @@ -0,0 +1,99 @@ +// Flutter imports: +import 'package:flutter/material.dart'; + +// Package imports: +import 'package:cached_network_image/cached_network_image.dart'; + +// Project imports: +import 'package:streamskit_mobile/core/app/constant/constants.dart'; +import 'package:streamskit_mobile/core/util/custom_image/default_image.dart'; +import 'package:streamskit_mobile/core/util/custom_image/place_holder_image.dart'; + +class CustomNetworkImage extends StatelessWidget { + final String? urlToImage; + final double? height; + final double? width; + final BoxShape shape; + final BoxFit? fit; + final EdgeInsetsGeometry? margin; + final BorderRadiusGeometry? borderRadius; + final Widget? placeholderWidget; + final ColorFilter? colorFilter; + final Widget? childInAvatar; + // ignore: use_key_in_widget_constructors + const CustomNetworkImage({ + Key? key, + required this.urlToImage, + this.height, + this.width, + this.shape = BoxShape.circle, + this.fit = BoxFit.cover, + this.margin, + this.borderRadius, + this.placeholderWidget, + this.colorFilter, + this.childInAvatar, + }) : assert(height != null || width != null); + @override + Widget build(BuildContext context) { + return urlToImage == null + ? placeholderWidget ?? + DefaultImage( + height: height ?? width!, + width: width ?? height!, + margin: margin, + shape: shape, + borderRadius: borderRadius, + childInAvatar: childInAvatar, + ) + : CachedNetworkImage( + cacheKey: urlToImage, + memCacheHeight: 1024 * 200, + memCacheWidth: 1024 * 200, + maxWidthDiskCache: 1024 * 1024, + maxHeightDiskCache: 1024 * 1024, + placeholderFadeInDuration: const Duration(milliseconds: delay200ms), + fadeInDuration: const Duration(milliseconds: delay200ms), + fadeOutDuration: const Duration(milliseconds: delay200ms), + imageUrl: urlToImage!, + imageBuilder: (context, imageProvider) => Container( + height: height ?? width!, + width: width ?? height!, + margin: margin, + decoration: BoxDecoration( + shape: shape, + borderRadius: borderRadius, + image: DecorationImage( + colorFilter: colorFilter, + image: imageProvider, + fit: fit, + filterQuality: FilterQuality.low, + ), + ), + alignment: Alignment.bottomRight, + child: childInAvatar, + ), + placeholder: (context, url) => + placeholderWidget ?? + Container( + margin: margin, + child: PlaceHolderImage( + height: height ?? width!, + width: width ?? height!, + shape: shape, + borderRadius: borderRadius, + ), + ), + errorWidget: (context, url, error) => + placeholderWidget ?? + DefaultImage( + height: height ?? width!, + width: width ?? height!, + margin: margin, + shape: shape, + borderRadius: borderRadius, + childInAvatar: childInAvatar, + ), + ); + } +} diff --git a/lib/core/util/custom_image/default_image.dart b/lib/core/util/custom_image/default_image.dart new file mode 100644 index 0000000..eb05eb8 --- /dev/null +++ b/lib/core/util/custom_image/default_image.dart @@ -0,0 +1,45 @@ +// Flutter imports: +import 'package:flutter/material.dart'; + +// Project imports: +import 'package:streamskit_mobile/core/util/styles/style.dart'; + +// Project imports: + +class DefaultImage extends StatelessWidget { + final double height; + final double width; + final EdgeInsetsGeometry? margin; + final BoxShape shape; + final BorderRadiusGeometry? borderRadius; + final Widget? childInAvatar; + const DefaultImage({ + super.key, + required this.height, + required this.width, + required this.margin, + required this.shape, + this.borderRadius, + this.childInAvatar, + }); + @override + Widget build(BuildContext context) { + return Container( + height: height, + width: width, + margin: margin, + decoration: BoxDecoration( + shape: shape, + borderRadius: borderRadius, + image: DecorationImage( + image: const AssetImage( + launcherIcon, + ), + fit: shape == BoxShape.circle ? BoxFit.fitHeight : BoxFit.contain, + ), + ), + alignment: Alignment.bottomRight, + child: childInAvatar, + ); + } +} diff --git a/lib/core/util/custom_image/place_holder_image.dart b/lib/core/util/custom_image/place_holder_image.dart new file mode 100644 index 0000000..83be93a --- /dev/null +++ b/lib/core/util/custom_image/place_holder_image.dart @@ -0,0 +1,36 @@ +// Flutter imports: +import 'package:flutter/material.dart'; + +// Project imports: +import 'package:streamskit_mobile/core/app/colors/app_color.dart'; +import 'package:streamskit_mobile/core/util/shimmer/fade_shimmer.dart'; + +class PlaceHolderImage extends StatelessWidget { + final double height; + final double width; + final BoxShape shape; + final BorderRadiusGeometry? borderRadius; + const PlaceHolderImage({ + super.key, + required this.height, + required this.width, + required this.shape, + required this.borderRadius, + }); + @override + Widget build(BuildContext context) { + return shape == BoxShape.circle + ? FadeShimmer.round( + size: height, + highlightColor: colorPink, + baseColor: mCM, + ) + : FadeShimmer( + width: width, + height: height, + highlightColor: colorPink, + baseColor: mCM, + borderRadius: borderRadius, + ); + } +} diff --git a/lib/core/util/dio_transformer.dart b/lib/core/util/dio_transformer.dart new file mode 100644 index 0000000..163e2e2 --- /dev/null +++ b/lib/core/util/dio_transformer.dart @@ -0,0 +1,32 @@ +// Dart imports: +// ignore_for_file: deprecated_member_use + +import 'dart:convert'; + +// Flutter imports: +import 'package:flutter/foundation.dart'; + +// Package imports: +import 'package:dio/dio.dart'; + +/// Dio has already implemented a [DefaultTransformer], and as the default +/// [Transformer]. If you want to custom the transformation of +/// request/response data, you can provide a [Transformer] by your self, and +/// replace the [DefaultTransformer] by setting the [dio.Transformer]. +/// +/// [DioTransformer] is especially for flutter, by which the json decoding +/// will be in background with [compute] function. + +/// FlutterTransformer +class DioTransformer extends DefaultTransformer { + DioTransformer() : super(jsonDecodeCallback: _parseJson); +} + +// Must be top-level function +_parseAndDecode(String response) { + return jsonDecode(response); +} + +_parseJson(String text) { + return compute(_parseAndDecode, text); +} diff --git a/lib/core/util/firebase/firebase_auth.dart b/lib/core/util/firebase/firebase_auth.dart new file mode 100644 index 0000000..0dc8446 --- /dev/null +++ b/lib/core/util/firebase/firebase_auth.dart @@ -0,0 +1,122 @@ +// Dart imports: +import 'dart:convert'; +import 'dart:math'; + +// Package imports: +import 'package:crypto/crypto.dart'; +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:flutter_facebook_auth/flutter_facebook_auth.dart'; +import 'package:google_sign_in/google_sign_in.dart'; +import 'package:sign_in_with_apple/sign_in_with_apple.dart'; + +// Project imports: +import 'package:streamskit_mobile/features/auth/domain/entities/social.dart'; + +// Project imports: + +Future signInWithGoogle() async { + try { + await GoogleSignIn().signOut(); + final GoogleSignInAccount? googleUser = await GoogleSignIn().signIn(); + final GoogleSignInAuthentication googleAuth = + await googleUser!.authentication; + final OAuthCredential googleCredential = GoogleAuthProvider.credential( + accessToken: googleAuth.accessToken, + idToken: googleAuth.idToken, + ); + final UserCredential firebaseUserCredential = + await FirebaseAuth.instance.signInWithCredential(googleCredential); + + if (firebaseUserCredential.user == null) { + return null; + } + + return SocialValue( + fullName: googleUser.displayName ?? 'user.askany.google', + googleId: firebaseUserCredential.user!.uid, + ); + } catch (e) { + return null; + } +} + +Future signInWithFacebook({ + LoginBehavior behavior = LoginBehavior.nativeOnly, +}) async { + try { + await FacebookAuth.instance.logOut(); + // FacebookAuth.instance.expressLogin(); + final result = await FacebookAuth.instance.login(loginBehavior: behavior); + + switch (result.status) { + case LoginStatus.success: + final facebookAuthCredential = + FacebookAuthProvider.credential(result.accessToken!.token); + final firebaseUserCredential = await FirebaseAuth.instance + .signInWithCredential(facebookAuthCredential); + return SocialValue( + fullName: + firebaseUserCredential.user!.displayName ?? 'user.askany.fb', + facebookId: firebaseUserCredential.user!.uid, + ); + case LoginStatus.cancelled: + return null; + case LoginStatus.failed: + break; + default: + break; + } + + return signInWithFacebook(behavior: LoginBehavior.webOnly); + } catch (error) { + return null; + } +} + +Future signInWithApple() async { + try { + final rawNonce = generateNonce(); + final nonce = sha256ofString(rawNonce); + + final appleCredential = await SignInWithApple.getAppleIDCredential( + scopes: [ + AppleIDAuthorizationScopes.email, + AppleIDAuthorizationScopes.fullName, + ], + nonce: nonce, + ); + + final oauthCredential = OAuthProvider("apple.com").credential( + idToken: appleCredential.identityToken, + rawNonce: rawNonce, + ); + + final UserCredential firebaseUserCredential = + await FirebaseAuth.instance.signInWithCredential(oauthCredential); + + if (firebaseUserCredential.user == null) { + return null; + } + + return SocialValue( + fullName: appleCredential.givenName ?? 'user.askany.apple', + appleId: firebaseUserCredential.user!.uid, + ); + } catch (e) { + return null; + } +} + +String generateNonce([int length = 32]) { + const charset = + '0123456789ABCDEFGHIJKLMNOPQRSTUVXYZabcdefghijklmnopqrstuvwxyz-._'; + final random = Random.secure(); + return List.generate(length, (_) => charset[random.nextInt(charset.length)]) + .join(); +} + +String sha256ofString(String input) { + final bytes = utf8.encode(input); + final digest = sha256.convert(bytes); + return digest.toString(); +} diff --git a/lib/core/util/logger.dart b/lib/core/util/logger.dart new file mode 100644 index 0000000..0fe8fef --- /dev/null +++ b/lib/core/util/logger.dart @@ -0,0 +1,24 @@ +// Dart imports: +import 'dart:developer' as developer; + +// Flutter imports: +import 'package:flutter/foundation.dart'; + +class UtilLogger { + static const String tag = "SALEBOLT"; + + static log([String tag = tag, dynamic msg]) { + if (kDebugMode) { + developer.log('$msg', name: tag); + } + } + + ///Singleton factory + static final UtilLogger _instance = UtilLogger._internal(); + + factory UtilLogger() { + return _instance; + } + + UtilLogger._internal(); +} diff --git a/lib/core/util/numeral/numeral.dart b/lib/core/util/numeral/numeral.dart new file mode 100644 index 0000000..72fc708 --- /dev/null +++ b/lib/core/util/numeral/numeral.dart @@ -0,0 +1,73 @@ +// Project imports: +import 'parser.dart'; + +/// Default fraction digits value. +const int defaultFractionDigits = 1; + +/// Numeral formatter +/// +/// This class is used to format numbers into strings. +/// +/// Example usage: +/// ```dart +/// const numeral = Numeral(1314); +/// print(numeral.format()); // => "1.314K" +/// ``` +class Numeral { + /// Create [Numeral] class. + /// + /// The Factory create a [Numeral] class instance. + /// + /// [numeral] is num [Type]. + /// + /// return [Numeral] instance. + const Numeral(this.numeral); + + /// Original numeral. + final num numeral; + + /// Format [number] to beautiful [String]. + /// + /// E.g: + /// ```dart + /// Numeral(1000).value(); // -> 1K + /// ``` + /// + /// return a [String] type. + String format({int fractionDigits = defaultFractionDigits}) { + final NumeralParsedValue parsed = numeralParser(numeral); + + return _removeEndsZero(parsed.value.toStringAsFixed(fractionDigits)) + + parsed.suffix; + } + + /// Remove value ends with zero. + /// + /// Remove formated value ends with zero, + /// replace to zero string. + /// + /// [value] type is [String]. + /// + /// return a [String] type. + String _removeEndsZero(String value) { + if (!value.contains('.')) { + return value; + } + + if (value.endsWith('.')) { + return value.substring(0, value.length - 1); + } else if (value.endsWith('0')) { + return _removeEndsZero(value.substring(0, value.length - 1)); + } + + return value; + } + + /// Get formated value. + /// + /// Get the [value] function value. + /// + /// return a [String] type. + @override + String toString() => format(); +} diff --git a/lib/core/util/numeral/parser.dart b/lib/core/util/numeral/parser.dart new file mode 100644 index 0000000..3b4b880 --- /dev/null +++ b/lib/core/util/numeral/parser.dart @@ -0,0 +1,59 @@ +/// Numeral parsed value. +class NumeralParsedValue { + /// String suffix. + final String suffix; + + /// number parsed value. + final num value; + + /// Create the [NumeralParsedValue] + /// + /// [value] Is the parsed value. + /// [suffix] The last string after the connection. + NumeralParsedValue._(this.value, this.suffix); + + /// [NumeralParsedValue] Factory. + /// + /// [value] Is the parsed value. + /// [suffix] The last string after the connection. + factory NumeralParsedValue({ + required String suffix, + required num value, + }) { + return NumeralParsedValue._(value, suffix); + } + + /// To display string. + @override + String toString() { + return '$runtimeType(String suffix = "$suffix", num value = $value);'; + } +} + +/// Numeral value parser. +/// +/// [value] Is need parse value. +/// +/// return a [NumeralParsedValue]. +NumeralParsedValue numeralParser(num value) { + final num abs = value.abs(); + + // If number > 1 trillion. + if (abs >= 1000000000000) { + return NumeralParsedValue(value: value / 1000000000000, suffix: 'T'); + + // If number > 1 billion. + } else if (abs >= 1000000000) { + return NumeralParsedValue(value: value / 1000000000, suffix: 'B'); + + // If number > 1 million. + } else if (abs >= 1000000) { + return NumeralParsedValue(value: value / 1000000, suffix: 'M'); + + // If number > 1 thousand. + } else if (abs >= 1000) { + return NumeralParsedValue(value: value / 1000, suffix: 'K'); + } + + return NumeralParsedValue(value: value, suffix: ''); +} diff --git a/lib/core/util/path_helper.dart b/lib/core/util/path_helper.dart new file mode 100644 index 0000000..ecd4d10 --- /dev/null +++ b/lib/core/util/path_helper.dart @@ -0,0 +1,80 @@ +// ignore_for_file: depend_on_referenced_packages + +// Dart imports: +import 'dart:io'; + +// Flutter imports: +import 'package:flutter/services.dart'; + +// Package imports: +import 'package:path_provider/path_provider.dart'; + +// Project imports: +import 'package:streamskit_mobile/core/util/logger.dart'; + +class PathHelper { + static Future deleteCacheImageDir(String path) async { + final cacheDir = Directory(path); + if (cacheDir.existsSync()) { + cacheDir.deleteSync(recursive: true); + } + } + + static Future createDirStreamOS() async { + String tempAskanyDir = await tempDirStreamOS; + String localStoreAskanyDir = await localStoreDirStreamOS; + Directory myDir = Directory(tempAskanyDir); + Directory localDir = Directory(localStoreAskanyDir); + if (!myDir.existsSync()) { + await myDir.create(); + } + + if (!localDir.existsSync()) { + await localDir.create(); + } + } + + static Future get tempDirStreamOS async => + '${(await getTemporaryDirectory()).path}/streamOS'; + + static Future get localStoreDirStreamOS async => + '${(await getTemporaryDirectory()).path}/hive'; + + static Future get appDir async => + await getApplicationDocumentsDirectory(); + + static Future get downloadsDir async { + Directory downloadsDirectory; + try { + if (Platform.isIOS) { + downloadsDirectory = await getLibraryDirectory(); + } else { + downloadsDirectory = await getApplicationSupportDirectory(); + } + + return downloadsDirectory; + } on PlatformException { + UtilLogger.log('Could not get the downloads directory'); + return null; + } + } + + static Future getTempSize() async { + String tempAskanyDir = await tempDirStreamOS; + Directory myDir = Directory(tempAskanyDir); + + if (!myDir.existsSync()) return 0; + + return myDir.listSync().isEmpty ? 0 : ((myDir.statSync().size - 64) * 1024); + } + + static Future clearTempDir() async { + String tempAskanyDir = await tempDirStreamOS; + Directory myDir = Directory(tempAskanyDir); + + if (!myDir.existsSync()) return; + + myDir.deleteSync(recursive: true); + myDir.create(); + } +} diff --git a/lib/core/util/shimmer/fade_shimmer.dart b/lib/core/util/shimmer/fade_shimmer.dart new file mode 100644 index 0000000..be87643 --- /dev/null +++ b/lib/core/util/shimmer/fade_shimmer.dart @@ -0,0 +1,143 @@ +// Dart imports: +import 'dart:async'; + +// Flutter imports: +import 'package:flutter/material.dart'; + +enum FadeTheme { light, dark, lightReverse } + +class FadeShimmer extends StatefulWidget { + final Color? highlightColor; + final Color? baseColor; + final double radius; + final double? width; + final double? height; + final BorderRadiusGeometry? borderRadius; + + /// light or dark with predefined highlightColor and baseColor + /// need to pass this or highlightColor and baseColor + final FadeTheme? fadeTheme; + + /// delay time before update the color, use this to make loading items animate follow each other instead of parallel, check the example for demo. + final int millisecondsDelay; + + const FadeShimmer( + {super.key, + this.millisecondsDelay = 0, + this.radius = 0, + this.fadeTheme, + this.highlightColor, + this.baseColor, + this.borderRadius, + this.width, + this.height}) + : assert( + (highlightColor != null && baseColor != null) || fadeTheme != null); + + /// use this to create a round loading widget + factory FadeShimmer.round( + {required double size, + Color? highlightColor, + int millisecondsDelay = 0, + Color? baseColor, + FadeTheme? fadeTheme}) => + FadeShimmer( + height: size, + width: size, + radius: size / 2, + baseColor: baseColor, + highlightColor: highlightColor, + fadeTheme: fadeTheme, + millisecondsDelay: millisecondsDelay, + ); + + @override + State createState() => _FadeShimmerState(); +} + +class _FadeShimmerState extends State { + static final isHighLightStream = + Stream.periodic(const Duration(seconds: 1), (x) => x % 2 == 0) + .asBroadcastStream(); + bool isHighLight = true; + late StreamSubscription sub; + + Color get highLightColor { + if (widget.fadeTheme != null) { + switch (widget.fadeTheme) { + case FadeTheme.light: + return const Color(0xffF9F9FB); + case FadeTheme.dark: + return const Color(0xff3A3E3F); + case FadeTheme.lightReverse: + return Theme.of(context).brightness == Brightness.dark + ? const Color(0xff393e47) + : const Color(0xffE6E8EB); + default: + return const Color(0xff3A3E3F); + } + } + return widget.highlightColor!; + } + + Color get baseColor { + if (widget.fadeTheme != null) { + switch (widget.fadeTheme) { + case FadeTheme.light: + return const Color(0xffE6E8EB); + case FadeTheme.dark: + return const Color(0xff2A2C2E); + case FadeTheme.lightReverse: + return Theme.of(context).brightness == Brightness.dark + ? const Color(0xff3d3d5c) + : const Color(0xffF9F9FB); + default: + return const Color(0xff2A2C2E); + } + } + return widget.baseColor!; + } + + @override + void dispose() { + sub.cancel(); + super.dispose(); + } + + void safeSetState() { + if (mounted) { + setState(() {}); + } + } + + @override + void initState() { + super.initState(); + sub = isHighLightStream.listen((isHighLight) { + if (widget.millisecondsDelay != 0) { + Future.delayed(Duration(milliseconds: widget.millisecondsDelay), () { + isHighLight = isHighLight; + safeSetState(); + }); + } else { + isHighLight = isHighLight; + safeSetState(); + } + }); + } + + @override + Widget build(BuildContext context) { + return AnimatedContainer( + curve: Curves.easeInOut, + duration: const Duration(milliseconds: 1200), + width: widget.width, + height: widget.height, + decoration: BoxDecoration( + color: isHighLight ? highLightColor : baseColor, + borderRadius: widget.radius == 0 + ? widget.borderRadius + : BorderRadius.circular(widget.radius)), + ); + } +} diff --git a/lib/core/util/sizer_custom/extension.dart b/lib/core/util/sizer_custom/extension.dart new file mode 100644 index 0000000..cfa8e93 --- /dev/null +++ b/lib/core/util/sizer_custom/extension.dart @@ -0,0 +1,68 @@ +part of 'sizer.dart'; + +extension SizerExt on num { + /// Calculates the height depending on the device's screen size + /// + /// Eg: 20.h -> will take 20% of the screen's height + double get h => this * SizerUtil.height / 100; + + /// Calculates the width depending on the device's screen size + /// + /// Eg: 20.w -> will take 20% of the screen's width + double get w => this * SizerUtil.width / 100; + + /// Calculates the sp (Scalable Pixel) depending on the device's screen size + // double get sp => this * (SizerUtil.width / 3) / 100; + + double get width { + // DEVICE INCH + double deviceSize = math.sqrt(100.h * 100.h + 100.w * 100.w) / inchToDP; + if (deviceSize > 6.5) { + return 65.w * (6.5 / deviceSize); + } else if (deviceSize > 5.5) { + return 100.w; + } else if (deviceSize > 5.0) { + return 90.w; + } else { + return 85.w; + } + } + + bool get isTablet { + // DEVICE INCH + double deviceSize = math.sqrt(100.h * 100.h + 100.w * 100.w) / inchToDP; + if (deviceSize > 6.5) { + return true; + } + + return false; + } + + double get sp => this * (width / 3) / 100; + + int get itemCountGridViewCalendar { + return (100.w / (150.sp)).round(); + } + + int get itemCountGridViewMoney { + return (65.w / (100.sp)).round(); + } + + int get itemCountGridViewPhoto { + // DEVICE INCH + double deviceSize = math.sqrt(100.h * 100.h + 100.w * 100.w) / inchToDP; + if (deviceSize > 7.5) { + return 5; + } else if (deviceSize > 6) { + return 4; + } else if (deviceSize > 5.5) { + return 3; + } else { + return 3; + } + } + + double get heightDialogListTablet { + return 52.h; + } +} diff --git a/lib/core/util/sizer_custom/sizer.dart b/lib/core/util/sizer_custom/sizer.dart new file mode 100644 index 0000000..9a32b34 --- /dev/null +++ b/lib/core/util/sizer_custom/sizer.dart @@ -0,0 +1,22 @@ +/* + * Created by Urmish patel on 2018/9/29. + * email: urmishpatel9@gmail.com +*/ +library sizer; + +// Dart imports: +import 'dart:io'; +import 'dart:math' as math; + +// Flutter imports: +import 'package:flutter/foundation.dart' show kIsWeb; +import 'package:flutter/widgets.dart'; + +// Project imports: +import 'package:streamskit_mobile/core/app/constant/constants.dart'; + +part 'extension.dart'; + +part 'util.dart'; + +part 'widget.dart'; diff --git a/lib/core/util/sizer_custom/util.dart b/lib/core/util/sizer_custom/util.dart new file mode 100644 index 0000000..a653d69 --- /dev/null +++ b/lib/core/util/sizer_custom/util.dart @@ -0,0 +1,74 @@ +part of 'sizer.dart'; + +class SizerUtil { + /// Device's BoxConstraints + static late BoxConstraints boxConstraints; + + /// Device's Orientation + static late Orientation orientation; + + /// Type of Device + /// + /// This can either be mobile or tablet + static late DeviceType deviceType; + + /// Device's Height + static late double height; + + /// Device's Width + static late double width; + + /// Sets the Screen's size and Device's Orientation, + /// BoxConstraints, Height, and Width + static void setScreenSize( + BoxConstraints constraints, Orientation currentOrientation) { + // Sets boxconstraints and orientation + boxConstraints = constraints; + orientation = currentOrientation; + + // Sets screen width and height + if (orientation == Orientation.portrait) { + width = boxConstraints.maxWidth; + height = boxConstraints.maxHeight; + } else { + width = boxConstraints.maxHeight; + height = boxConstraints.maxWidth; + } + + // Sets ScreenType + if (kIsWeb) { + deviceType = DeviceType.web; + } else if (Platform.isAndroid || Platform.isIOS) { + if ((orientation == Orientation.portrait && width < 600) || + (orientation == Orientation.landscape && height < 600)) { + deviceType = DeviceType.mobile; + } else { + deviceType = DeviceType.tablet; + } + } else if (Platform.isMacOS) { + deviceType = DeviceType.mac; + } else if (Platform.isWindows) { + deviceType = DeviceType.windows; + } else if (Platform.isLinux) { + deviceType = DeviceType.linux; + } else { + deviceType = DeviceType.fuchsia; + } + } + + // for responsive web + static getWebResponsiveSize({smallSize, mediumSize, largeSize}) { + return width < 600 + ? smallSize //'phone' + : width >= 600 && width <= 1024 + ? mediumSize //'tablet' + : largeSize; //'desktop'; + } + + static bool get isTablet => deviceType == DeviceType.tablet; +} + +/// Type of Device +/// +/// This can be either mobile or tablet +enum DeviceType { mobile, tablet, web, mac, windows, linux, fuchsia } diff --git a/lib/core/util/sizer_custom/widget.dart b/lib/core/util/sizer_custom/widget.dart new file mode 100644 index 0000000..c1f30e5 --- /dev/null +++ b/lib/core/util/sizer_custom/widget.dart @@ -0,0 +1,28 @@ +part of 'sizer.dart'; + +/// Provides `Context`, `Orientation`, and `DeviceType` parameters to the builder function +typedef ResponsiveBuild = Widget Function( + BuildContext context, + Orientation orientation, + DeviceType deviceType, +); + +/// A widget that gets the device's details like orientation and constraints +/// +/// Usage: Wrap MaterialApp with this widget +class Sizer extends StatelessWidget { + const Sizer({super.key, required this.builder}); + + /// Builds the widget whenever the orientation changes + final ResponsiveBuild builder; + + @override + Widget build(BuildContext context) { + return LayoutBuilder(builder: (context, constraints) { + return OrientationBuilder(builder: (context, orientation) { + SizerUtil.setScreenSize(constraints, orientation); + return builder(context, orientation, SizerUtil.deviceType); + }); + }); + } +} diff --git a/lib/core/util/stop_watch_api.dart b/lib/core/util/stop_watch_api.dart new file mode 100644 index 0000000..6af0812 --- /dev/null +++ b/lib/core/util/stop_watch_api.dart @@ -0,0 +1,25 @@ +// Package imports: +import 'package:dio/dio.dart'; + +// Project imports: +import 'package:streamskit_mobile/core/app/constant/constants.dart'; +import 'package:streamskit_mobile/core/util/logger.dart'; + +class StopWatch { + static Future stopWatchApi( + Future Function() next, String method, String endpoint) async { + DateTime startTime = DateTime.now(); + var result = await next(); + DateTime endTime = DateTime.now(); + int duration = endTime.difference(startTime).inMilliseconds; + if (duration >= delayASecond) { + UtilLogger.log( + 'WARNING RESPONSE TIME', '**********************************'); + UtilLogger.log( + 'WARNING RESPONSE TIME', '$method: $endpoint - ${duration}ms\n'); + UtilLogger.log( + 'WARNING RESPONSE TIME', '**********************************'); + } + return result; + } +} diff --git a/lib/core/util/styles/auth_style.dart b/lib/core/util/styles/auth_style.dart new file mode 100644 index 0000000..8368fdb --- /dev/null +++ b/lib/core/util/styles/auth_style.dart @@ -0,0 +1,3 @@ +const String logoGoogle = 'assets/icons/ic_google.png'; +const String logoFacebook = 'assets/icons/ic_facebook.png'; +const String logoApple = 'assets/icons/ic_apple.png'; diff --git a/lib/core/util/styles/chat_style.dart b/lib/core/util/styles/chat_style.dart new file mode 100644 index 0000000..6e2735b --- /dev/null +++ b/lib/core/util/styles/chat_style.dart @@ -0,0 +1,24 @@ +// Flutter imports: +import 'package:flutter/material.dart'; + +// Project imports: +import 'package:streamskit_mobile/core/app/colors/app_color.dart'; +import 'package:streamskit_mobile/core/util/sizer_custom/sizer.dart'; + +Divider dividerChat(BuildContext context) { + return Divider( + color: colorDividerTimeline, + thickness: 0.2, + height: 0.2, + ); +} + +Divider dividerChatWithPadding(context) { + return Divider( + color: colorDividerTimeline, + thickness: 0.2, + height: 0.2, + indent: 12.sp, + endIndent: 12.sp, + ); +} diff --git a/lib/core/util/styles/home_style.dart b/lib/core/util/styles/home_style.dart new file mode 100644 index 0000000..e8b8331 --- /dev/null +++ b/lib/core/util/styles/home_style.dart @@ -0,0 +1,8 @@ +const String launcherIcon = 'assets/icons/launcher_icon.png'; +const String imageStartStream = 'assets/images/img_start_stream.png'; + +const String iconFire = 'assets/icons/ic_fire.png'; +const String iconNearby = 'assets/icons/ic_nearby.png'; +const String iconGame = 'assets/icons/ic_game.png'; +const String iconSharing = 'assets/icons/ic_sharing.png'; +const String iconEye = 'assets/icons/ic_eye.png'; diff --git a/lib/core/util/styles/profile_style.dart b/lib/core/util/styles/profile_style.dart new file mode 100644 index 0000000..294fcec --- /dev/null +++ b/lib/core/util/styles/profile_style.dart @@ -0,0 +1,83 @@ +// Flutter imports: +import 'package:flutter/material.dart'; + +// Project imports: +import 'package:streamskit_mobile/core/app/colors/app_color.dart'; +import 'package:streamskit_mobile/core/util/sizer_custom/sizer.dart'; + +const Color colorDividerBottomSheet = Color(0xFFC4C4C4); +const Color colorDividerBottomSheetDark = Color(0xFF595959); +const Color colorBorderTextField = Color(0xFFC5D0CF); + +/// Text size: 13 color: mCL weight: 700 +TextStyle text13w700mCL = TextStyle( + color: mCL, + fontSize: 13.sp, + fontWeight: FontWeight.w700, + overflow: TextOverflow.ellipsis, +); + +/// Text size: 13 color: mCL +TextStyle text13mCL = TextStyle( + color: mCL, + fontSize: 13.sp, + overflow: TextOverflow.ellipsis, +); + +/// Text size: 11 color: mCL +TextStyle text11mCL = TextStyle( + color: mCL, + fontSize: 11.sp, + overflow: TextOverflow.ellipsis, +); + +/// Text size: 11 color: colorBlack1 +TextStyle text11cB1 = TextStyle( + color: colorBlack1, + fontSize: 11.sp, + overflow: TextOverflow.ellipsis, +); + +///Text size: 11 color : mGB +TextStyle text11mGB = TextStyle( + color: mGB, + fontSize: 11.sp, + overflow: TextOverflow.ellipsis, +); + +///Text size: 11 color : mGM +TextStyle text11mGM = TextStyle( + color: mGM, + fontSize: 11.sp, + overflow: TextOverflow.ellipsis, +); + +///Text size: 11 color : mGD +TextStyle text11mGD = TextStyle( + color: mGD, + fontSize: 11.sp, + overflow: TextOverflow.ellipsis, +); + +///Text size: 9 color: mCL +TextStyle text9mCL = TextStyle( + color: mCL, + fontSize: 9.sp, + overflow: TextOverflow.ellipsis, + height: 1.4, +); + +///Text size: 9 color: mGM +TextStyle text9mGM = TextStyle( + color: mGM, + fontSize: 9.sp, + overflow: TextOverflow.ellipsis, +); + +///Text size: 7 color: mCL +TextStyle text7mCL = TextStyle( + color: mCL, + fontSize: 7.sp, + overflow: TextOverflow.ellipsis, + height: 1.4, +); diff --git a/lib/core/util/styles/search_style.dart b/lib/core/util/styles/search_style.dart new file mode 100644 index 0000000..2ef4d95 --- /dev/null +++ b/lib/core/util/styles/search_style.dart @@ -0,0 +1,22 @@ +// Flutter imports: +import 'package:flutter/material.dart'; + +// Project imports: +import 'package:streamskit_mobile/core/app/colors/app_color.dart'; +import 'package:streamskit_mobile/core/util/sizer_custom/sizer.dart'; + +/// Text size: 11 color: mGB weight: 700 +TextStyle text11w700mGB = TextStyle( + color: mGB, + fontSize: 11.sp, + fontWeight: FontWeight.w700, + overflow: TextOverflow.ellipsis, +); + +///Text size: 9 color: mGB +TextStyle text9mGB = TextStyle( + color: mGB, + fontSize: 9.sp, + overflow: TextOverflow.ellipsis, + height: 1.4, +); diff --git a/lib/core/util/styles/style.dart b/lib/core/util/styles/style.dart new file mode 100644 index 0000000..5543aab --- /dev/null +++ b/lib/core/util/styles/style.dart @@ -0,0 +1,2 @@ +export 'home_style.dart'; +export 'auth_style.dart'; diff --git a/lib/features/app/app.dart b/lib/features/app/app.dart new file mode 100644 index 0000000..6b7061f --- /dev/null +++ b/lib/features/app/app.dart @@ -0,0 +1,74 @@ +// Dart imports: +import 'dart:async'; + +// Flutter imports: +import 'package:flutter/material.dart'; + +// Package imports: +import 'package:flutter_bloc/flutter_bloc.dart'; + +// Project imports: +import 'package:streamskit_mobile/core/app/constant/constants.dart'; +import 'package:streamskit_mobile/core/app/themes/themes.dart'; +import 'package:streamskit_mobile/core/navigator/app_pages.dart'; +import 'package:streamskit_mobile/core/util/after_layout_mixin.dart'; +import 'package:streamskit_mobile/core/util/sizer_custom/sizer.dart'; +import 'package:streamskit_mobile/features/auth/presentation/bloc/auth_bloc.dart'; +import 'package:streamskit_mobile/features/auth/presentation/screens/sign_in_screen.dart'; +import 'package:streamskit_mobile/features/home.dart'; +import 'package:streamskit_mobile/features/home/presentation/splash_screen.dart'; + +class App extends StatefulWidget { + const App({super.key}); + + @override + State createState() { + return _AppState(); + } +} + +class _AppState extends State with AfterLayoutMixin { + bool _isInitial = true; + + @override + Widget build(BuildContext context) { + return Sizer( + builder: ((context, orientation, deviceType) { + return MaterialApp( + navigatorKey: AppNavigator.navigatorKey, + debugShowCheckedModeBanner: false, + theme: AppTheme.light().data, + darkTheme: AppTheme.dark().data, + themeMode: ThemeMode.dark, + navigatorObservers: [NavigatorObserver()], + onGenerateRoute: (settings) { + return AppNavigator().getRoute(settings); + }, + home: BlocBuilder( + builder: (context, auth) { + if (_isInitial) { + return const SplashScreen(); + } + + bool isLogined = auth is AuthSuccess; + if (isLogined) { + return const Home(); + } + + return const SignInScreen(); + }, + ), + ); + }), + ); + } + + @override + FutureOr afterFirstLayout(BuildContext context) { + Future.delayed(const Duration(milliseconds: delayASecond), () { + setState(() { + _isInitial = false; + }); + }); + } +} diff --git a/lib/features/app/bloc/app_bloc.dart b/lib/features/app/bloc/app_bloc.dart new file mode 100644 index 0000000..b129568 --- /dev/null +++ b/lib/features/app/bloc/app_bloc.dart @@ -0,0 +1,24 @@ +// Package imports: +import 'package:flutter_bloc/flutter_bloc.dart'; + +// Project imports: +import 'package:streamskit_mobile/core/injection/injection_container.dart'; +import 'package:streamskit_mobile/features/auth/presentation/bloc/auth_bloc.dart'; + +class AppBloc { + static final AuthBloc authBloc = getIt(); + static final List providers = [ + BlocProvider( + create: (context) => authBloc..add(OnAuthCheckEvent()), + ), + ]; + + ///Singleton factory + static final AppBloc _instance = AppBloc._internal(); + + factory AppBloc() { + return _instance; + } + + AppBloc._internal(); +} diff --git a/lib/features/app/screens/scaffold_wrapper.dart b/lib/features/app/screens/scaffold_wrapper.dart new file mode 100644 index 0000000..b960845 --- /dev/null +++ b/lib/features/app/screens/scaffold_wrapper.dart @@ -0,0 +1,37 @@ +// Flutter imports: +import 'package:flutter/cupertino.dart'; + +// Project imports: +import 'package:streamskit_mobile/core/util/sizer_custom/sizer.dart'; + +class ScaffoldWrapper extends StatelessWidget { + final bool isLoading; + final Widget child; + const ScaffoldWrapper({ + super.key, + this.isLoading = false, + required this.child, + }); + + @override + Widget build(BuildContext context) { + return Stack( + children: [ + IgnorePointer( + ignoring: isLoading, + child: child, + ), + Visibility( + visible: isLoading, + child: Container( + alignment: Alignment.center, + color: const Color(0x80000000), + child: CupertinoActivityIndicator( + radius: 15.sp, + ), + ), + ), + ], + ); + } +} diff --git a/lib/features/auth/data/datasources/auth_local_datasource.dart b/lib/features/auth/data/datasources/auth_local_datasource.dart new file mode 100644 index 0000000..6c41f5f --- /dev/null +++ b/lib/features/auth/data/datasources/auth_local_datasource.dart @@ -0,0 +1,32 @@ +// Package imports: +import 'package:hive/hive.dart'; +import 'package:injectable/injectable.dart'; + +// Project imports: +import 'package:streamskit_mobile/core/app/constant/storage_keys.dart'; + +abstract class AuthLocalDataSource { + String? getAccessToken(); + void saveAccessToken(String accessToken); + void clearAccessToken(); +} + +@LazySingleton(as: AuthLocalDataSource) +class AuthLocalDataSourceImpl implements AuthLocalDataSource { + final Box hiveBox = Hive.box(StorageKeys.boxAuth); + + @override + void clearAccessToken() { + hiveBox.delete(StorageKeys.accessToken); + } + + @override + String? getAccessToken() { + return hiveBox.get(StorageKeys.accessToken); + } + + @override + void saveAccessToken(String accessToken) { + hiveBox.put(StorageKeys.accessToken, accessToken); + } +} diff --git a/lib/features/auth/data/datasources/auth_remote_datasource.dart b/lib/features/auth/data/datasources/auth_remote_datasource.dart new file mode 100644 index 0000000..d9a352e --- /dev/null +++ b/lib/features/auth/data/datasources/auth_remote_datasource.dart @@ -0,0 +1,31 @@ +// Package imports: +import 'package:dio/dio.dart'; +import 'package:injectable/injectable.dart'; + +// Project imports: +import 'package:streamskit_mobile/core/app/config/base_remote_data.dart'; +import 'package:streamskit_mobile/core/app/constant/endpoints.dart'; +import 'package:streamskit_mobile/core/types/http_status_code.dart'; +import 'package:streamskit_mobile/features/auth/domain/entities/social.dart'; + +abstract class AuthRemoteDataSource { + Future signInWithSocial(SocialValue socialValue); +} + +@LazySingleton(as: AuthRemoteDataSource) +class AuthRemoteDataSourceImpl extends AuthRemoteDataSource { + @override + Future signInWithSocial(SocialValue socialValue) async { + Map body = socialValue.toMap(); + Response response = await BaseRemoteData().postRoute( + Endpoints.signIn, + body, + ); + + if (response.statusCode == StatusCode.ok) { + return response.data['data']; + } + + return null; + } +} diff --git a/lib/features/auth/data/repositories/auth_repository_impl.dart b/lib/features/auth/data/repositories/auth_repository_impl.dart new file mode 100644 index 0000000..cca6e95 --- /dev/null +++ b/lib/features/auth/data/repositories/auth_repository_impl.dart @@ -0,0 +1,53 @@ +// Package imports: +import 'package:dartz/dartz.dart'; +import 'package:injectable/injectable.dart'; + +// Project imports: +import 'package:streamskit_mobile/core/error/failure.dart'; +import 'package:streamskit_mobile/core/usecase/usecase.dart'; +import 'package:streamskit_mobile/features/auth/data/datasources/auth_local_datasource.dart'; +import 'package:streamskit_mobile/features/auth/data/datasources/auth_remote_datasource.dart'; +import 'package:streamskit_mobile/features/auth/domain/entities/social.dart'; +import 'package:streamskit_mobile/features/auth/domain/repositories/auth_repository.dart'; + +@LazySingleton(as: AuthRepository) +class AuthRepositoryImpl implements AuthRepository { + final AuthLocalDataSource localData; + final AuthRemoteDataSource remoteData; + + const AuthRepositoryImpl({ + required this.localData, + required this.remoteData, + }); + + @override + Either checkSignined() { + String? token = localData.getAccessToken(); + + if (token == null) { + return Left(NullValue()); + } + + return const Right(true); + } + + @override + Future> signIn(Params params) async { + String? accessToken = await remoteData.signInWithSocial( + params.object as SocialValue, + ); + + if (accessToken != null) { + localData.saveAccessToken(accessToken); + return const Right(true); + } + + return const Right(false); + } + + @override + Either signOut() { + localData.clearAccessToken(); + return const Right(true); + } +} diff --git a/lib/features/auth/domain/entities/auth_type.dart b/lib/features/auth/domain/entities/auth_type.dart new file mode 100644 index 0000000..e795edd --- /dev/null +++ b/lib/features/auth/domain/entities/auth_type.dart @@ -0,0 +1,5 @@ +enum AuthType { + google, + facebook, + apple, +} diff --git a/lib/features/auth/domain/entities/social.dart b/lib/features/auth/domain/entities/social.dart new file mode 100644 index 0000000..d3c0b8a --- /dev/null +++ b/lib/features/auth/domain/entities/social.dart @@ -0,0 +1,91 @@ +// Dart imports: +import 'dart:convert'; + +class SocialValue { + final String fullName; + final String? facebookId; + final String? googleId; + final String? appleId; + SocialValue({ + required this.fullName, + this.facebookId, + this.googleId, + this.appleId, + }); + + SocialValue copyWith({ + String? fullName, + String? email, + String? facebookId, + String? googleId, + String? appleId, + }) { + return SocialValue( + fullName: fullName ?? this.fullName, + facebookId: facebookId ?? this.facebookId, + googleId: googleId ?? this.googleId, + appleId: appleId ?? this.appleId, + ); + } + + Map toMap() { + Map result = { + 'fullName': fullName, + }; + + if (googleId != null) { + result['credentialId'] = googleId!; + result['socialType'] = 0; + } + + if (appleId != null) { + result['credentialId'] = appleId!; + result['socialType'] = 2; + } + + if (facebookId != null) { + result['credentialId'] = facebookId!; + result['socialType'] = 1; + } + + return result; + } + + factory SocialValue.fromMap(Map map) { + return SocialValue( + fullName: map['fullName'] ?? '', + facebookId: map['facebookId'], + googleId: map['googleId'], + appleId: map['appleId'], + ); + } + + String toJson() => json.encode(toMap()); + + factory SocialValue.fromJson(String source) => + SocialValue.fromMap(json.decode(source)); + + @override + String toString() { + return 'SocialModel(fullName: $fullName, facebookId: $facebookId, googleId: $googleId, appleId: $appleId)'; + } + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + + return other is SocialValue && + other.fullName == fullName && + other.facebookId == facebookId && + other.googleId == googleId && + other.appleId == appleId; + } + + @override + int get hashCode { + return fullName.hashCode ^ + facebookId.hashCode ^ + googleId.hashCode ^ + appleId.hashCode; + } +} diff --git a/lib/features/auth/domain/repositories/auth_repository.dart b/lib/features/auth/domain/repositories/auth_repository.dart new file mode 100644 index 0000000..e1ffc46 --- /dev/null +++ b/lib/features/auth/domain/repositories/auth_repository.dart @@ -0,0 +1,14 @@ +// Package imports: +import 'package:dartz/dartz.dart'; + +// Project imports: +import 'package:streamskit_mobile/core/error/failure.dart'; +import 'package:streamskit_mobile/core/usecase/usecase.dart'; + +abstract class AuthRepository { + Future> signIn(Params params); + + Either checkSignined(); + + Either signOut(); +} diff --git a/lib/features/auth/domain/usecases/check_logined.dart b/lib/features/auth/domain/usecases/check_logined.dart new file mode 100644 index 0000000..cccf33e --- /dev/null +++ b/lib/features/auth/domain/usecases/check_logined.dart @@ -0,0 +1,20 @@ +// Package imports: +import 'package:dartz/dartz.dart'; +import 'package:injectable/injectable.dart'; + +// Project imports: +import 'package:streamskit_mobile/core/error/failure.dart'; +import 'package:streamskit_mobile/core/usecase/usecase.dart'; +import 'package:streamskit_mobile/features/auth/domain/repositories/auth_repository.dart'; + +@lazySingleton +class CheckLogined implements UseCase { + final AuthRepository repository; + + const CheckLogined({required this.repository}); + + @override + Either call(NoParams noParams) { + return repository.checkSignined(); + } +} diff --git a/lib/features/auth/domain/usecases/sign_in_with_social.dart b/lib/features/auth/domain/usecases/sign_in_with_social.dart new file mode 100644 index 0000000..cf98e1a --- /dev/null +++ b/lib/features/auth/domain/usecases/sign_in_with_social.dart @@ -0,0 +1,25 @@ +// Package imports: +import 'package:dartz/dartz.dart'; +import 'package:injectable/injectable.dart'; + +// Project imports: +import 'package:streamskit_mobile/core/error/failure.dart'; +import 'package:streamskit_mobile/core/usecase/usecase.dart'; +import 'package:streamskit_mobile/features/auth/domain/entities/social.dart'; +import 'package:streamskit_mobile/features/auth/domain/repositories/auth_repository.dart'; + +@lazySingleton +class SignInWithSocial implements UseCaseFuture { + final AuthRepository repository; + + const SignInWithSocial({required this.repository}); + + @override + Future> call(Params params) async { + if (params.object is! SocialValue) { + return Left(CannotParseItem()); + } + + return repository.signIn(params); + } +} diff --git a/lib/features/auth/domain/usecases/sign_out.dart b/lib/features/auth/domain/usecases/sign_out.dart new file mode 100644 index 0000000..8d6d540 --- /dev/null +++ b/lib/features/auth/domain/usecases/sign_out.dart @@ -0,0 +1,20 @@ +// Package imports: +import 'package:dartz/dartz.dart'; +import 'package:injectable/injectable.dart'; + +// Project imports: +import 'package:streamskit_mobile/core/error/failure.dart'; +import 'package:streamskit_mobile/core/usecase/usecase.dart'; +import 'package:streamskit_mobile/features/auth/domain/repositories/auth_repository.dart'; + +@lazySingleton +class SignOut implements UseCase { + final AuthRepository repository; + + const SignOut({required this.repository}); + + @override + Either call(NoParams noParams) { + return repository.signOut(); + } +} diff --git a/lib/features/auth/presentation/bloc/auth_bloc.dart b/lib/features/auth/presentation/bloc/auth_bloc.dart new file mode 100644 index 0000000..fbc3534 --- /dev/null +++ b/lib/features/auth/presentation/bloc/auth_bloc.dart @@ -0,0 +1,90 @@ +// Package imports: +import 'package:dartz/dartz.dart'; +import 'package:equatable/equatable.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +// Project imports: +import 'package:streamskit_mobile/core/error/failure.dart'; +import 'package:streamskit_mobile/core/navigator/app_pages.dart'; +import 'package:streamskit_mobile/core/navigator/app_routes.dart'; +import 'package:streamskit_mobile/core/usecase/usecase.dart'; +import 'package:streamskit_mobile/core/util/firebase/firebase_auth.dart'; +import 'package:streamskit_mobile/features/auth/domain/entities/auth_type.dart'; +import 'package:streamskit_mobile/features/auth/domain/entities/social.dart'; +import 'package:streamskit_mobile/features/auth/domain/usecases/check_logined.dart'; +import 'package:streamskit_mobile/features/auth/domain/usecases/sign_in_with_social.dart'; +import 'package:streamskit_mobile/features/auth/domain/usecases/sign_out.dart'; + +part 'auth_event.dart'; +part 'auth_state.dart'; + +class AuthBloc extends Bloc { + final CheckLogined _checkLogined; + final SignInWithSocial _signInWithSocial; + final SignOut _signOut; + AuthBloc( + this._signInWithSocial, + this._checkLogined, + this._signOut, + ) : super(AuthInitial()) { + on((event, emit) async { + if (event is OnAuthCheckEvent) { + checkLogined(emit); + } + + if (event is SignInEvent) { + await signIn(emit, authType: event.authType); + } + + if (event is SignOutEvent) { + signOut(emit); + } + }); + } + + // MARK: Private methods + void checkLogined(Emitter emit) { + Either hasLogined = _checkLogined.call(NoParams()); + hasLogined.fold((l) => emit(AuthFailure()), (r) => emit(AuthSuccess())); + } + + Future signIn( + Emitter emit, { + required AuthType authType, + }) async { + emit(Authenticating()); + late final SocialValue? socialValue; + switch (authType) { + case AuthType.google: + socialValue = await signInWithGoogle(); + break; + case AuthType.apple: + socialValue = await signInWithApple(); + break; + default: + socialValue = await signInWithFacebook(); + break; + } + if (socialValue == null) { + // Get credential id failure + emit(AuthFailure()); + } else { + Either signInSucceed = await _signInWithSocial.call( + Params(object: socialValue), + ); + + signInSucceed.fold( + (l) => emit(AuthFailure()), + (r) => emit(AuthSuccess()), + ); + } + } + + void signOut(Emitter emit) async { + Either signOutSucceed = _signOut.call(NoParams()); + signOutSucceed.fold((l) => l, (r) { + emit(AuthFailure()); + AppNavigator.popUntil(Routes.rootRoute); + }); + } +} diff --git a/lib/features/auth/presentation/bloc/auth_event.dart b/lib/features/auth/presentation/bloc/auth_event.dart new file mode 100644 index 0000000..f43fb1d --- /dev/null +++ b/lib/features/auth/presentation/bloc/auth_event.dart @@ -0,0 +1,17 @@ +part of 'auth_bloc.dart'; + +abstract class AuthEvent extends Equatable { + const AuthEvent(); + + @override + List get props => []; +} + +class OnAuthCheckEvent extends AuthEvent {} + +class SignInEvent extends AuthEvent { + final AuthType authType; + const SignInEvent({required this.authType}); +} + +class SignOutEvent extends AuthEvent {} diff --git a/lib/features/auth/presentation/bloc/auth_state.dart b/lib/features/auth/presentation/bloc/auth_state.dart new file mode 100644 index 0000000..f5cf022 --- /dev/null +++ b/lib/features/auth/presentation/bloc/auth_state.dart @@ -0,0 +1,16 @@ +part of 'auth_bloc.dart'; + +abstract class AuthState extends Equatable { + const AuthState(); + + @override + List get props => []; +} + +class AuthInitial extends AuthState {} + +class AuthSuccess extends AuthState {} + +class AuthFailure extends AuthState {} + +class Authenticating extends AuthState {} diff --git a/lib/features/auth/presentation/screens/sign_in_screen.dart b/lib/features/auth/presentation/screens/sign_in_screen.dart new file mode 100644 index 0000000..c0529e8 --- /dev/null +++ b/lib/features/auth/presentation/screens/sign_in_screen.dart @@ -0,0 +1,192 @@ +// Flutter imports: +import 'package:flutter/material.dart'; + +// Package imports: +import 'package:flutter_bloc/flutter_bloc.dart'; + +// Project imports: +import 'package:streamskit_mobile/core/app/colors/app_color.dart'; +import 'package:streamskit_mobile/core/util/sizer_custom/sizer.dart'; +import 'package:streamskit_mobile/core/util/styles/style.dart'; +import 'package:streamskit_mobile/features/app/bloc/app_bloc.dart'; +import 'package:streamskit_mobile/features/app/screens/scaffold_wrapper.dart'; +import 'package:streamskit_mobile/features/auth/domain/entities/auth_type.dart'; +import 'package:streamskit_mobile/features/auth/presentation/bloc/auth_bloc.dart'; +import 'package:streamskit_mobile/features/auth/presentation/widgets/sign_in_button.dart'; + +class SignInScreen extends StatefulWidget { + const SignInScreen({super.key}); + + @override + State createState() => _SignInScreenState(); +} + +class _SignInScreenState extends State { + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + return ScaffoldWrapper( + isLoading: state is Authenticating, + child: Scaffold( + body: SafeArea( + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + SizedBox(height: 10.sp), + Image.asset( + launcherIcon, + height: 40.sp, + width: 40.sp, + ), + SizedBox(height: 48.sp), + Expanded( + child: Container( + padding: EdgeInsets.symmetric( + horizontal: 20.sp, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + RichText( + text: TextSpan( + style: Theme.of(context) + .textTheme + .titleMedium + ?.copyWith( + fontSize: 19.sp, + fontWeight: FontWeight.w700, + height: 1.46, + ), + children: [ + const TextSpan( + text: 'Sign In for\nstarting become\n', + ), + TextSpan( + text: 'Streamer', + style: TextStyle(color: colorPurple), + ), + ], + ), + ), + SizedBox(height: 16.sp), + Text( + 'Creative, entertaining application just for you. Come to StreamOS today,\nwe got you.', + softWrap: true, + strutStyle: StrutStyle.disabled, + style: Theme.of(context) + .textTheme + .labelMedium + ?.copyWith( + fontSize: 12.sp, + fontWeight: FontWeight.w300, + height: 1.4, + ), + ), + const Spacer(), + SignInButton( + title: 'Continue with Google', + iconAsset: logoGoogle, + onPressed: () { + AppBloc.authBloc.add( + const SignInEvent( + authType: AuthType.google, + ), + ); + }, + ), + SizedBox(height: 12.sp), + SignInButton( + title: 'Continue with Facebook', + iconAsset: logoFacebook, + onPressed: () { + AppBloc.authBloc.add( + const SignInEvent( + authType: AuthType.facebook, + ), + ); + }, + ), + Padding( + padding: EdgeInsets.symmetric( + vertical: 12.sp, + ), + child: Row( + children: [ + const Expanded( + child: Divider( + height: .25, + thickness: .25, + ), + ), + Padding( + padding: + EdgeInsets.symmetric(horizontal: 12.sp), + child: Text( + 'or', + style: + Theme.of(context).textTheme.labelMedium, + ), + ), + const Expanded( + child: Divider( + height: .25, + thickness: .25, + ), + ), + ], + ), + ), + SignInButton( + title: 'Continue with Apple', + iconAsset: logoApple, + onPressed: () { + AppBloc.authBloc.add( + const SignInEvent( + authType: AuthType.apple, + ), + ); + }, + ), + SizedBox(height: 20.sp), + RichText( + text: TextSpan( + style: Theme.of(context) + .textTheme + .labelSmall + ?.copyWith( + fontSize: 11.5.sp, + fontWeight: FontWeight.w300, + ), + children: [ + const TextSpan( + text: 'By signing up, you agree to our ', + ), + TextSpan( + text: 'Terms, Privacy Policy', + style: TextStyle(color: colorPink), + ), + const TextSpan( + text: ' and ', + ), + TextSpan( + text: 'Cookie Use.', + style: TextStyle(color: colorPink), + ), + ], + ), + ), + SizedBox(height: 30.sp), + ], + ), + ), + ), + ], + ), + ), + ), + ); + }, + ); + } +} diff --git a/lib/features/auth/presentation/widgets/sign_in_button.dart b/lib/features/auth/presentation/widgets/sign_in_button.dart new file mode 100644 index 0000000..4e06199 --- /dev/null +++ b/lib/features/auth/presentation/widgets/sign_in_button.dart @@ -0,0 +1,53 @@ +// Flutter imports: +import 'package:flutter/material.dart'; + +// Project imports: +import 'package:streamskit_mobile/core/util/sizer_custom/sizer.dart'; + +class SignInButton extends StatelessWidget { + final String title; + final String iconAsset; + final Function() onPressed; + + const SignInButton({ + super.key, + required this.iconAsset, + required this.title, + required this.onPressed, + }); + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: onPressed, + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(30.sp), + color: Colors.white, + ), + padding: EdgeInsets.symmetric(vertical: 11.25.sp), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + SizedBox(width: 14.5.w), + Image.asset( + iconAsset, + height: 16.sp, + width: 16.sp, + fit: BoxFit.contain, + ), + SizedBox(width: 10.sp), + Text( + title, + style: TextStyle( + color: Theme.of(context).scaffoldBackgroundColor, + fontWeight: FontWeight.w700, + fontSize: 11.5.sp, + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/features/chat/.gitkeep b/lib/features/chat/data/.gitkeep similarity index 100% rename from lib/features/chat/.gitkeep rename to lib/features/chat/data/.gitkeep diff --git a/lib/features/chat/data/conversation_model.dart b/lib/features/chat/data/conversation_model.dart new file mode 100644 index 0000000..e28980b --- /dev/null +++ b/lib/features/chat/data/conversation_model.dart @@ -0,0 +1,157 @@ +// Dart imports: +import 'dart:convert'; + +class ConversationModel { + final String fullName; + final String lastMessage; + final String urlUserImage; + final DateTime createdAt; + final int countUnreadMessage; + final bool isSeen; + ConversationModel({ + required this.fullName, + required this.lastMessage, + required this.urlUserImage, + required this.createdAt, + required this.countUnreadMessage, + required this.isSeen, + }); + + ConversationModel copyWith( + {String? fullName, + String? lastMessage, + String? urlUserImage, + DateTime? createdAt, + int? countUnreadMessage, + bool? isSeen}) { + return ConversationModel( + fullName: fullName ?? this.fullName, + lastMessage: lastMessage ?? this.lastMessage, + urlUserImage: urlUserImage ?? this.urlUserImage, + createdAt: createdAt ?? this.createdAt, + countUnreadMessage: countUnreadMessage ?? this.countUnreadMessage, + isSeen: isSeen ?? this.isSeen, + ); + } + + Map toMap() { + return { + 'fullName': fullName, + 'lastMessage': lastMessage, + 'urlUserImage': urlUserImage, + 'createdAt': createdAt.millisecondsSinceEpoch, + 'countUnreadMessage': countUnreadMessage, + 'isSeen': isSeen, + }; + } + + factory ConversationModel.fromMap(Map map) { + return ConversationModel( + fullName: map['fullName'] ?? '', + lastMessage: map['lastMessage'] ?? '', + urlUserImage: map['urlUserImage'] ?? '', + createdAt: DateTime.fromMillisecondsSinceEpoch(map['createdAt']), + countUnreadMessage: map['countUnreadMessage']?.toInt() ?? 0, + isSeen: map['isSeen'] ?? false); + } + + String toJson() => json.encode(toMap()); + + factory ConversationModel.fromJson(String source) => + ConversationModel.fromMap(json.decode(source)); + + @override + String toString() { + return 'ConversationModel(fullName: $fullName, lastMessage: $lastMessage, urlUserImage: $urlUserImage, createdAt: $createdAt, countUnreadMessage: $countUnreadMessage)'; + } + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + + return other is ConversationModel && + other.fullName == fullName && + other.lastMessage == lastMessage && + other.urlUserImage == urlUserImage && + other.createdAt == createdAt && + other.countUnreadMessage == countUnreadMessage && + other.isSeen == isSeen; + } + + @override + int get hashCode { + return fullName.hashCode ^ + lastMessage.hashCode ^ + urlUserImage.hashCode ^ + createdAt.hashCode ^ + countUnreadMessage.hashCode ^ + isSeen.hashCode; + } +} + +List conversationFake = [ + ConversationModel( + fullName: 'Long Tứ', + lastMessage: 'Đánh bài không :))', + urlUserImage: + 'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcShD1rBYj3o7UoD5vBdtlgAVcLh3bq-kAjwFw&usqp=CAU', + createdAt: DateTime.now(), + countUnreadMessage: 2, + isSeen: true), + ConversationModel( + fullName: 'Thánh Đỗ', + lastMessage: 'Live Stream game đi anh ei!!!', + urlUserImage: + 'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcSvpUqkQzykHU3Jr8v9yhGkSbhL18lGd3ObzQ&usqp=CAU', + createdAt: DateTime.now(), + countUnreadMessage: 3, + isSeen: true), + ConversationModel( + fullName: 'Kẻ bí Ẩn', + lastMessage: 'Live Stream lẹ đi', + urlUserImage: + 'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcRfaZBAHVPJ6xRNWZqOH_zxfa5YpSqdcdxiVw&usqp=CAU', + createdAt: DateTime.now(), + countUnreadMessage: 6, + isSeen: false), + ConversationModel( + fullName: 'Kẻ bí Ẩn', + lastMessage: 'Live Stream lẹ đi', + urlUserImage: + 'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcRfaZBAHVPJ6xRNWZqOH_zxfa5YpSqdcdxiVw&usqp=CAU', + createdAt: DateTime.now(), + countUnreadMessage: 6, + isSeen: false), + ConversationModel( + fullName: 'Kẻ bí Ẩn', + lastMessage: 'Live Stream lẹ đi', + urlUserImage: + 'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcRfaZBAHVPJ6xRNWZqOH_zxfa5YpSqdcdxiVw&usqp=CAU', + createdAt: DateTime.now(), + countUnreadMessage: 6, + isSeen: false), + ConversationModel( + fullName: 'Kẻ bí Ẩn', + lastMessage: 'Live Stream lẹ đi', + urlUserImage: + 'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcRfaZBAHVPJ6xRNWZqOH_zxfa5YpSqdcdxiVw&usqp=CAU', + createdAt: DateTime.now(), + countUnreadMessage: 6, + isSeen: true), + ConversationModel( + fullName: 'Kẻ bí Ẩn', + lastMessage: 'Live Stream lẹ đi', + urlUserImage: + 'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcRfaZBAHVPJ6xRNWZqOH_zxfa5YpSqdcdxiVw&usqp=CAU', + createdAt: DateTime.now(), + countUnreadMessage: 6, + isSeen: true), + ConversationModel( + fullName: 'Kẻ bí Ẩn', + lastMessage: 'Live Stream lẹ đi', + urlUserImage: + 'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcRfaZBAHVPJ6xRNWZqOH_zxfa5YpSqdcdxiVw&usqp=CAU', + createdAt: DateTime.now(), + countUnreadMessage: 6, + isSeen: true), +]; diff --git a/lib/features/home/.gitkeep b/lib/features/chat/domain/.gitkeep similarity index 100% rename from lib/features/home/.gitkeep rename to lib/features/chat/domain/.gitkeep diff --git a/lib/features/chat/presentation/screens/chat_screen.dart b/lib/features/chat/presentation/screens/chat_screen.dart new file mode 100644 index 0000000..f75a2ca --- /dev/null +++ b/lib/features/chat/presentation/screens/chat_screen.dart @@ -0,0 +1,103 @@ +// Flutter imports: +import 'package:flutter/material.dart'; + +// Project imports: +import 'package:streamskit_mobile/core/util/common/touchable_opacity.dart'; +import 'package:streamskit_mobile/core/util/sizer_custom/sizer.dart'; +import 'package:streamskit_mobile/core/util/styles/chat_style.dart'; +import 'package:streamskit_mobile/features/chat/data/conversation_model.dart'; +import 'package:streamskit_mobile/features/chat/presentation/widgets/bottom_chat_options.dart'; +import 'package:streamskit_mobile/features/chat/presentation/widgets/chat_card.dart'; +import 'package:streamskit_mobile/features/chat/presentation/widgets/search_box.dart'; +import 'package:streamskit_mobile/features/chat/presentation/widgets/user_connect_widget.dart'; +import 'package:streamskit_mobile/features/home/data/model/user_model.dart'; + +class ChatScreen extends StatefulWidget { + const ChatScreen({super.key}); + + @override + State createState() => _ChatScreenState(); +} + +class _ChatScreenState extends State { + _showBottomSheetOptions() { + showModalBottomSheet( + context: context, + backgroundColor: Colors.transparent, + isDismissible: true, + isScrollControlled: true, + barrierColor: Colors.black38, + builder: (context) { + return const BottomChatOptions(); + }, + ); + } + + @override + Widget build(BuildContext context) { + return Container( + padding: EdgeInsets.only(top: 36.sp), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SearchBox(), + Padding( + padding: EdgeInsets.symmetric(horizontal: 16.sp, vertical: 12.sp), + child: Text( + "Recent", + style: TextStyle( + color: Colors.white, + fontWeight: FontWeight.w600, + fontSize: 13.sp), + ), + ), + SizedBox( + height: 88.sp, + width: double.infinity, + child: ListView.builder( + padding: EdgeInsets.symmetric(horizontal: 16.sp), + itemCount: listUserFake.length, + shrinkWrap: true, + scrollDirection: Axis.horizontal, + itemBuilder: ((context, index) { + if (index == 0) { + return UserConnectWidget( + userModel: listUserFake[index], + isAuthor: true, + ); + } + return UserConnectWidget( + userModel: listUserFake[index], + isAuthor: false, + ); + }), + ), + ), + dividerChatWithPadding(context), + SizedBox( + height: 20.sp, + ), + Expanded( + child: ListView.builder( + physics: const BouncingScrollPhysics(), + padding: EdgeInsets.symmetric(horizontal: 16.sp).add( + EdgeInsets.only(bottom: 78.sp), + ), + itemBuilder: (context, index) { + return TouchableOpacity( + onLongPress: () { + _showBottomSheetOptions(); + }, + child: ChatCard( + conversationModel: conversationFake[index], + ), + ); + }, + itemCount: conversationFake.length, + ), + ), + ], + ), + ); + } +} diff --git a/lib/features/chat/presentation/widgets/bottom_chat_options.dart b/lib/features/chat/presentation/widgets/bottom_chat_options.dart new file mode 100644 index 0000000..6ab0583 --- /dev/null +++ b/lib/features/chat/presentation/widgets/bottom_chat_options.dart @@ -0,0 +1,87 @@ +// Flutter imports: +import 'package:flutter/material.dart'; + +// Project imports: +import 'package:streamskit_mobile/core/app/colors/app_color.dart'; +import 'package:streamskit_mobile/core/app/themes/box_shadow.dart'; +import 'package:streamskit_mobile/core/util/sizer_custom/sizer.dart'; +import 'package:streamskit_mobile/core/util/styles/chat_style.dart'; +import 'package:streamskit_mobile/features/chat/presentation/widgets/button_option_widget.dart'; + +class BottomChatOptions extends StatefulWidget { + const BottomChatOptions({ + super.key, + }); + @override + State createState() => _BottomChatOptionsState(); +} + +class _BottomChatOptionsState extends State { + late final List chatOptions; + + @override + void initState() { + super.initState(); + chatOptions = [ + 'Tắt thông báo', + 'Xóa cuộc trò chuyện', + ]; + } + + @override + Widget build(BuildContext context) { + return Container( + margin: EdgeInsets.symmetric( + vertical: 12.sp, + horizontal: 10.sp, + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.end, + mainAxisSize: MainAxisSize.min, + children: [ + Container( + decoration: BoxDecoration( + color: + Theme.of(context).brightness == Brightness.dark ? mGD : mC, + borderRadius: BorderRadius.circular(8.sp), + boxShadow: BoxShadowStatic.boxShadow(context)), + child: ListView.builder( + physics: const NeverScrollableScrollPhysics(), + padding: EdgeInsets.zero, + shrinkWrap: true, + itemCount: chatOptions.length, + itemBuilder: (context, index) { + return Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + index > 0 ? dividerChat(context) : const SizedBox(), + ButtonOptionWidget( + text: chatOptions[index], + isDanger: index == chatOptions.length - 1, + handlePressed: () {}, + ), + ], + ); + }, + ), + ), + SizedBox(height: 4.sp), + Container( + height: 40.sp, + decoration: BoxDecoration( + color: + Theme.of(context).brightness == Brightness.dark ? mGD : mC, + borderRadius: BorderRadius.circular(8.sp), + boxShadow: BoxShadowStatic.boxShadow(context)), + child: ButtonOptionWidget( + text: 'Hủy', + isCancel: true, + handlePressed: () {}, + ), + ), + SizedBox(height: 4.sp), + ], + ), + ); + } +} diff --git a/lib/features/chat/presentation/widgets/button_option_widget.dart b/lib/features/chat/presentation/widgets/button_option_widget.dart new file mode 100644 index 0000000..f768c81 --- /dev/null +++ b/lib/features/chat/presentation/widgets/button_option_widget.dart @@ -0,0 +1,48 @@ +// Flutter imports: +import 'package:flutter/material.dart'; + +// Project imports: +import 'package:streamskit_mobile/core/navigator/app_pages.dart'; +import 'package:streamskit_mobile/core/util/common/touchable_opacity.dart'; +import 'package:streamskit_mobile/core/util/sizer_custom/sizer.dart'; + +class ButtonOptionWidget extends StatelessWidget { + final String text; + final Function handlePressed; + final bool isDanger; + final bool isCancel; + const ButtonOptionWidget({ + super.key, + required this.text, + required this.handlePressed, + this.isDanger = false, + this.isCancel = false, + }); + + @override + Widget build(BuildContext context) { + return TouchableOpacity( + onTap: () { + AppNavigator.pop(); + handlePressed(); + }, + child: Container( + height: 42.sp, + alignment: Alignment.center, + color: Colors.transparent, + child: Text( + text, + style: TextStyle( + color: isDanger + ? Theme.of(context).brightness == Brightness.dark + ? const Color(0xffdb5757) + : Colors.red.shade700 + : Colors.white, //Theme.of(context).textTheme.bodyText2!.color, + fontSize: 13.sp, + fontWeight: isCancel ? FontWeight.w700 : FontWeight.w600, + ), + ), + ), + ); + } +} diff --git a/lib/features/chat/presentation/widgets/chat_card.dart b/lib/features/chat/presentation/widgets/chat_card.dart new file mode 100644 index 0000000..336c07f --- /dev/null +++ b/lib/features/chat/presentation/widgets/chat_card.dart @@ -0,0 +1,111 @@ +// Flutter imports: +import 'package:flutter/material.dart'; + +// Project imports: +import 'package:streamskit_mobile/core/app/colors/app_color.dart'; +import 'package:streamskit_mobile/core/util/custom_image/custom_netword_image.dart'; +import 'package:streamskit_mobile/core/util/sizer_custom/sizer.dart'; +import 'package:streamskit_mobile/features/chat/data/conversation_model.dart'; + +class ChatCard extends StatefulWidget { + final ConversationModel conversationModel; + const ChatCard({ + super.key, + required this.conversationModel, + }); + + @override + State createState() => _ChatCardState(); +} + +class _ChatCardState extends State { + @override + Widget build(BuildContext context) { + return Container( + padding: EdgeInsets.only(bottom: 6.sp), + height: 50.sp, + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Container( + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all( + width: 1.2.sp, + color: colorBlack2, + ), + ), + child: CustomNetworkImage( + height: 45.sp, + width: 45.sp, + urlToImage: widget.conversationModel.urlUserImage, + shape: BoxShape.circle, + ), + ), + SizedBox( + width: 8.sp, + ), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Text( + widget.conversationModel.fullName, + style: TextStyle( + fontSize: 13.sp, + color: Colors.white, + fontWeight: FontWeight.w500), + ), + ), + Text( + "04:01", + style: TextStyle(fontSize: 10.sp, color: Colors.grey), + ), + ], + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Text( + widget.conversationModel.lastMessage, + style: TextStyle( + fontSize: 10.sp, + color: widget.conversationModel.isSeen + ? Colors.grey + : Colors.white), + ), + ), + widget.conversationModel.isSeen == true + ? const SizedBox() + : Container( + padding: EdgeInsets.all(4.sp), + width: 14.sp, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: colorPink, + ), + child: Center( + child: Text( + widget.conversationModel.countUnreadMessage + .toString(), + style: TextStyle( + fontSize: 8.sp, color: Colors.white), + ), + ), + ) + ], + ), + ], + ), + ), + ], + ), + ); + } +} diff --git a/lib/features/chat/presentation/widgets/search_box.dart b/lib/features/chat/presentation/widgets/search_box.dart new file mode 100644 index 0000000..28e9b4f --- /dev/null +++ b/lib/features/chat/presentation/widgets/search_box.dart @@ -0,0 +1,111 @@ +// Dart imports: +import 'dart:async'; + +// Flutter imports: +import 'package:flutter/material.dart'; + +// Project imports: +import 'package:streamskit_mobile/core/app/colors/app_color.dart'; +import 'package:streamskit_mobile/core/app/constants.dart'; +import 'package:streamskit_mobile/core/util/sizer_custom/sizer.dart'; + +class SearchBox extends StatefulWidget { + final EdgeInsetsGeometry? margin; + final Function(String)? onChanged; + final Function()? handleClear; + const SearchBox({ + super.key, + this.margin, + this.onChanged, + this.handleClear, + }); + + @override + State createState() => _SearchBoxState(); +} + +class _SearchBoxState extends State { + TextEditingController searchKey = TextEditingController(); + Timer? _debounce; + + @override + void dispose() { + _debounce?.cancel(); + super.dispose(); + + searchKey.text = ''; + } + + @override + Widget build(BuildContext context) { + return Container( + height: 35.sp, + width: 100.w, + margin: widget.margin ?? EdgeInsets.symmetric(horizontal: 16.sp), + decoration: BoxDecoration( + border: + Border.all(color: Theme.of(context).dividerColor, width: .5.sp), + borderRadius: BorderRadius.circular(8.sp), + color: colorGreyWhite), + child: TextFormField( + controller: searchKey, + style: TextStyle( + color: Theme.of(context).brightness == Brightness.light + ? colorCaptionSearch + : mC, + fontSize: 12.sp, + ), + keyboardType: TextInputType.multiline, + maxLines: 1, + decoration: InputDecoration( + contentPadding: EdgeInsets.only( + top: 0.sp, + right: 10.sp, + ), + hintText: 'Tìm kiếm', + hintStyle: TextStyle( + color: colorCaptionSearch, + fontSize: 12.sp, + fontWeight: FontWeight.w400, + ), + filled: true, + fillColor: Colors.transparent, + border: const OutlineInputBorder( + borderSide: BorderSide.none, + ), + prefixIcon: Container( + margin: EdgeInsets.all(10.sp), + child: Icon( + Icons.search, + size: 16.sp, + color: const Color(0xFFA4A4A4), + ), + ), + suffixIcon: searchKey.text.isEmpty + ? const SizedBox() + : IconButton( + onPressed: () { + if (widget.handleClear == null) { + } else { + widget.handleClear!(); + } + searchKey.text = ''; + }, + icon: const Icon( + Icons.close, + color: Color(0xFFA4A4A4), + ), + ), + ), + onChanged: widget.onChanged ?? + (val) { + if (_debounce?.isActive ?? false) _debounce?.cancel(); + _debounce = + Timer(const Duration(milliseconds: delayHalfSecond), () {}); + + setState(() {}); + }, + ), + ); + } +} diff --git a/lib/features/chat/presentation/widgets/user_connect_widget.dart b/lib/features/chat/presentation/widgets/user_connect_widget.dart new file mode 100644 index 0000000..4b69399 --- /dev/null +++ b/lib/features/chat/presentation/widgets/user_connect_widget.dart @@ -0,0 +1,74 @@ +// Flutter imports: +import 'package:flutter/material.dart'; + +// Project imports: +import 'package:streamskit_mobile/core/app/colors/app_color.dart'; +import 'package:streamskit_mobile/core/util/custom_image/custom_netword_image.dart'; +import 'package:streamskit_mobile/core/util/sizer_custom/sizer.dart'; +import 'package:streamskit_mobile/features/home/data/model/user_model.dart'; + +class UserConnectWidget extends StatelessWidget { + final UserModel userModel; + final bool isAuthor; + const UserConnectWidget( + {super.key, required this.userModel, required this.isAuthor}); + + @override + Widget build(BuildContext context) { + return Container( + margin: EdgeInsets.only(right: 18.sp), + child: Column( + children: [ + Stack( + children: [ + Container( + padding: EdgeInsets.all(3.5.sp), + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all( + width: 1.2.sp, + color: userModel.isLiveStream ? colorPink : colorBlack2, + ), + ), + child: CustomNetworkImage( + height: 45.sp, + width: 45.sp, + urlToImage: userModel.urlToImage, + shape: BoxShape.circle, + ), + ), + Visibility( + visible: isAuthor, + child: Positioned( + bottom: 2.sp, + right: 5.sp, + child: Container( + decoration: const BoxDecoration( + color: Colors.blue, shape: BoxShape.circle), + height: 16.sp, + width: 16.sp, + child: Center( + child: Icon( + Icons.add, + size: 12.sp, + ), + ), + ), + ), + ), + ], + ), + SizedBox(height: 9.sp), + Text( + userModel.fullName, + style: TextStyle( + color: userModel.isLiveStream ? mCL : fCL, + fontSize: 10.sp, + fontWeight: userModel.isLiveStream ? FontWeight.w500 : null, + ), + ) + ], + ), + ); + } +} diff --git a/lib/features/home.dart b/lib/features/home.dart new file mode 100644 index 0000000..4d08708 --- /dev/null +++ b/lib/features/home.dart @@ -0,0 +1,184 @@ +// Dart imports: +import 'dart:ui'; + +// Flutter imports: +import 'package:flutter/material.dart'; + +// Package imports: +import 'package:phosphor_flutter/phosphor_flutter.dart'; + +// Project imports: +import 'package:streamskit_mobile/core/app/colors/app_color.dart'; +import 'package:streamskit_mobile/core/util/common/touchable_opacity.dart'; +import 'package:streamskit_mobile/core/util/sizer_custom/sizer.dart'; +import 'package:streamskit_mobile/features/chat/presentation/screens/chat_screen.dart'; +import 'package:streamskit_mobile/features/home/presentation/screens/home_screen.dart'; +import 'package:streamskit_mobile/features/profile/presentation/screens/profile_screen.dart'; +import 'package:streamskit_mobile/features/search/presentation/screens/search_screen.dart'; +import 'package:streamskit_mobile/features/stream/presentation/screens/stream_screen.dart'; + +class Home extends StatefulWidget { + const Home({super.key}); + + @override + State createState() => _HomeState(); +} + +class _HomeState extends State { + final List _tabs = [ + const HomeScreen(), + const SearchScreen(), + const StreamScreen(), + const ChatScreen(), + const ProfileScreen(), + ]; + int _currentIndex = 0; + + @override + void dispose() { + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Stack( + children: [ + _tabs[_currentIndex], + Align( + alignment: Alignment.bottomCenter, + child: Container( + margin: EdgeInsets.only(bottom: 30.sp), + child: ClipRRect( + borderRadius: BorderRadius.vertical( + top: Radius.circular(100.sp), + ), + child: BackdropFilter( + filter: ImageFilter.blur(sigmaX: 5, sigmaY: 10), + child: Container( + padding: EdgeInsets.all(8.sp), + margin: EdgeInsets.all(8.sp), + decoration: BoxDecoration( + shape: BoxShape.circle, + color: Colors.black.withOpacity(0.3), + ), + child: SizedBox( + height: 30.sp, + width: 30.sp, + ), + ), + ), + ), + ), + ), + Align( + alignment: Alignment.bottomCenter, + child: ClipRRect( + borderRadius: BorderRadius.vertical( + top: Radius.circular(20.sp), + ), + child: BackdropFilter( + filter: ImageFilter.blur(sigmaX: 5, sigmaY: 10), + child: Container( + padding: EdgeInsets.symmetric(horizontal: 16.sp), + height: 70.sp, + color: Colors.black.withOpacity(0.3), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + _buildItemBottomBar( + inActiveIcon: PhosphorIcons.houseLight, + activeIcon: PhosphorIcons.houseFill, + index: 0, + ), + _buildItemBottomBar( + inActiveIcon: PhosphorIcons.magnifyingGlassLight, + activeIcon: PhosphorIcons.magnifyingGlassBold, + index: 1, + ), + const Expanded(child: SizedBox()), + _buildItemBottomBar( + inActiveIcon: PhosphorIcons.chatTeardropDotsLight, + activeIcon: PhosphorIcons.chatTeardropDotsFill, + index: 3, + ), + _buildItemBottomBar( + inActiveIcon: PhosphorIcons.userCircleLight, + activeIcon: PhosphorIcons.userCircleFill, + index: 4, + ), + ], + ), + ), + ), + ), + ), + Align( + alignment: Alignment.bottomCenter, + child: TouchableOpacity( + onTap: () {}, + child: Container( + padding: EdgeInsets.all(13.sp), + margin: EdgeInsets.only(bottom: 38.sp), + decoration: BoxDecoration( + color: colorPink, + shape: BoxShape.circle, + ), + child: Icon( + PhosphorIcons.plusBold, + size: 20.sp, + color: mCL, + ), + ), + ), + ) + ], + ), + ); + } + + Widget _buildItemBottomBar({ + required IconData inActiveIcon, + required IconData activeIcon, + required int index, + }) { + return Expanded( + child: TouchableOpacity( + onTap: () { + setState(() { + _currentIndex = index; + }); + }, + child: Container( + color: Colors.transparent, + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Container( + decoration: BoxDecoration( + color: Colors.transparent, + borderRadius: BorderRadius.circular(15.sp), + ), + child: Icon( + index == _currentIndex ? activeIcon : inActiveIcon, + size: 21.sp, + color: index == _currentIndex ? mCL : fCL, + ), + ), + SizedBox(height: 4.sp), + Container( + height: 3.sp, + width: 3.sp, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: index == _currentIndex ? mCH : Colors.transparent, + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/features/profile/.gitkeep b/lib/features/home/data/.gitkeep similarity index 100% rename from lib/features/profile/.gitkeep rename to lib/features/home/data/.gitkeep diff --git a/lib/features/home/data/datasources/local_live_stream_source.dart b/lib/features/home/data/datasources/local_live_stream_source.dart new file mode 100644 index 0000000..8d10981 --- /dev/null +++ b/lib/features/home/data/datasources/local_live_stream_source.dart @@ -0,0 +1,39 @@ +// Package imports: +import 'package:hive/hive.dart'; +import 'package:injectable/injectable.dart'; + +// Project imports: +import 'package:streamskit_mobile/core/app/constant/storage_keys.dart'; +import 'package:streamskit_mobile/features/home/data/model/live_stream_model.dart'; + +abstract class LocalLiveStreamSource { + List getLiveStreams(); + void saveLiveStreams(List liveStreams); + void clearLiveStreams(); +} + +@LazySingleton(as: LocalLiveStreamSource) +class LocalLiveStreamSourceImpl implements LocalLiveStreamSource { + final Box hiveBox = Hive.box(StorageKeys.boxLiveStreams); + + @override + List getLiveStreams() { + List liveStreamsRaw = hiveBox.get(StorageKeys.liveStreamsKey) ?? []; + return liveStreamsRaw + .map((liveStream) => LiveStreamModel.fromJson(liveStream)) + .toList(); + } + + @override + void saveLiveStreams(List liveStreams) { + hiveBox.put( + StorageKeys.liveStreamsKey, + liveStreams.map((liveStream) => liveStream.toJson()).toList(), + ); + } + + @override + void clearLiveStreams() { + hiveBox.delete(StorageKeys.liveStreamsKey); + } +} diff --git a/test/features/auth/domain/usecases/login_with_social_test.dart b/lib/features/home/data/datasources/remote_live_stream_source.dart similarity index 100% rename from test/features/auth/domain/usecases/login_with_social_test.dart rename to lib/features/home/data/datasources/remote_live_stream_source.dart diff --git a/lib/features/home/data/model/category_model.dart b/lib/features/home/data/model/category_model.dart new file mode 100644 index 0000000..c48ff5e --- /dev/null +++ b/lib/features/home/data/model/category_model.dart @@ -0,0 +1,65 @@ +// Dart imports: +import 'dart:convert'; + +// Project imports: +import 'package:streamskit_mobile/core/util/styles/style.dart'; + +class CategoryModel { + String title; + String image; + CategoryModel({ + required this.title, + required this.image, + }); + + CategoryModel copyWith({ + String? title, + String? image, + }) { + return CategoryModel( + title: title ?? this.title, + image: image ?? this.image, + ); + } + + Map toMap() { + return { + 'title': title, + 'image': image, + }; + } + + factory CategoryModel.fromMap(Map map) { + return CategoryModel( + title: map['title'] ?? '', + image: map['image'] ?? '', + ); + } + + String toJson() => json.encode(toMap()); + + factory CategoryModel.fromJson(String source) => + CategoryModel.fromMap(json.decode(source)); + + @override + String toString() => 'CategoryModel(title: $title, image: $image)'; + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + + return other is CategoryModel && + other.title == title && + other.image == image; + } + + @override + int get hashCode => title.hashCode ^ image.hashCode; +} + +List listCategoryFake = [ + CategoryModel(title: 'Popular', image: iconFire), + CategoryModel(title: 'Nearby', image: iconNearby), + CategoryModel(title: 'Games', image: iconGame), + CategoryModel(title: 'Sharing', image: iconSharing), +]; diff --git a/lib/features/home/data/model/live_stream_model.dart b/lib/features/home/data/model/live_stream_model.dart new file mode 100644 index 0000000..e25e4f8 --- /dev/null +++ b/lib/features/home/data/model/live_stream_model.dart @@ -0,0 +1,113 @@ +// ignore_for_file: public_member_api_docs, sort_constructors_first + +// Dart imports: +import 'dart:convert'; + +// Flutter imports: +import 'package:flutter/material.dart'; + +class LiveStreamModel { + final int peopleParticipant; + final int type; + final String urlToImage; + LiveStreamModel({ + required this.peopleParticipant, + required this.type, + required this.urlToImage, + }); + + LiveStreamModel copyWith({ + int? peopleParticipant, + int? type, + String? urlToImage, + }) { + return LiveStreamModel( + peopleParticipant: peopleParticipant ?? this.peopleParticipant, + type: type ?? this.type, + urlToImage: urlToImage ?? this.urlToImage, + ); + } + + Map toMap() { + return { + 'peopleParticipant': peopleParticipant, + 'type': type, + 'urlToImage': urlToImage, + }; + } + + factory LiveStreamModel.fromMap(Map map) { + return LiveStreamModel( + peopleParticipant: map['peopleParticipant'] as int, + type: map['type'] as int, + urlToImage: map['urlToImage'] as String, + ); + } + + String toJson() => json.encode(toMap()); + + factory LiveStreamModel.fromJson(String source) => + LiveStreamModel.fromMap(json.decode(source) as Map); + + @override + String toString() => + 'LiveStreamModel(peopleParticipant: $peopleParticipant, type: $type, urlToImage: $urlToImage)'; + + @override + bool operator ==(covariant LiveStreamModel other) { + if (identical(this, other)) return true; + + return other.peopleParticipant == peopleParticipant && + other.type == type && + other.urlToImage == urlToImage; + } + + @override + int get hashCode => + peopleParticipant.hashCode ^ type.hashCode ^ urlToImage.hashCode; + + String get getTitleType { + switch (type) { + case 1: + return 'Game'; + case 2: + return 'Review'; + case 3: + return 'Music'; + } + return ''; + } + + Color get getColorType { + switch (type) { + case 1: + return Colors.redAccent; + case 2: + return Colors.purpleAccent; + case 3: + return Colors.blueAccent; + } + return Colors.redAccent; + } +} + +List listLiveStreamFake = [ + LiveStreamModel(peopleParticipant: 910, type: 1, urlToImage: urlImageGame), + LiveStreamModel(peopleParticipant: 910, type: 2, urlToImage: urlImageReview), + LiveStreamModel(peopleParticipant: 910, type: 3, urlToImage: urlImageMusic), + LiveStreamModel(peopleParticipant: 910, type: 2, urlToImage: urlImageReview), + LiveStreamModel(peopleParticipant: 910, type: 3, urlToImage: urlImageMusic), + LiveStreamModel(peopleParticipant: 910, type: 2, urlToImage: urlImageReview), + LiveStreamModel(peopleParticipant: 910, type: 3, urlToImage: urlImageMusic), + LiveStreamModel(peopleParticipant: 910, type: 1, urlToImage: urlImageGame), + LiveStreamModel(peopleParticipant: 910, type: 2, urlToImage: urlImageReview), + LiveStreamModel(peopleParticipant: 910, type: 1, urlToImage: urlImageGame), + LiveStreamModel(peopleParticipant: 910, type: 2, urlToImage: urlImageReview), +]; + +String urlImageGame = + 'https://kenh14cdn.com/2020/10/30/photo-1-1604044340274364328631.png'; +String urlImageReview = + 'https://media.istockphoto.com/photos/cybersport-gamer-have-live-stream-picture-id1306424929?k=20&m=1306424929&s=612x612&w=0&h=tN9CafElP0EZHQG4s14zO_Ko0OriOzCt4fOc2q9lQz4='; +String urlImageMusic = + 'https://images.unsplash.com/photo-1653469894816-c517bc3427a7?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxzZWFyY2h8N3x8bGl2ZXN0cmVhbSUyMG11c2ljfGVufDB8fDB8fA%3D%3D&auto=format&fit=crop&w=500&q=60'; diff --git a/lib/features/home/data/model/user_model.dart b/lib/features/home/data/model/user_model.dart new file mode 100644 index 0000000..156910f --- /dev/null +++ b/lib/features/home/data/model/user_model.dart @@ -0,0 +1,163 @@ +// ignore_for_file: public_member_api_docs, sort_constructors_first + +// Dart imports: +import 'dart:convert'; + +// Flutter imports: +import 'package:flutter/foundation.dart'; + +class UserModel { + String? id; + String fullName; + String urlToImage; + String? description; + String? phoneNumber; + bool gender; + DateTime? birthday; + int? posts; + int? followings; + int? followers; + List? listFields; + bool isLiveStream; + UserModel({ + this.id, + required this.fullName, + required this.urlToImage, + this.description, + this.phoneNumber, + this.gender = false, + this.birthday, + this.posts, + this.followings, + this.followers, + this.listFields, + this.isLiveStream = false, + }); + + UserModel copyWith({ + String? id, + String? fullName, + String? urlToImage, + String? description, + String? phoneNumber, + bool? gender, + DateTime? birthday, + int? posts, + int? followings, + int? followers, + List? listFields, + bool? isLiveStream, + }) { + return UserModel( + id: id ?? this.id, + fullName: fullName ?? this.fullName, + urlToImage: urlToImage ?? this.urlToImage, + description: description ?? this.description, + phoneNumber: phoneNumber ?? this.phoneNumber, + gender: gender ?? this.gender, + birthday: birthday ?? this.birthday, + posts: posts ?? this.posts, + followings: followings ?? this.followings, + followers: followers ?? this.followers, + listFields: listFields ?? this.listFields, + isLiveStream: isLiveStream ?? this.isLiveStream, + ); + } + + Map toMap() { + return { + 'id': id, + 'fullName': fullName, + 'urlToImage': urlToImage, + 'description': description, + 'phoneNumber': phoneNumber, + 'gender': gender, + 'birthday': birthday?.millisecondsSinceEpoch, + 'posts': posts, + 'followings': followings, + 'followers': followers, + 'listFields': listFields, + 'isLiveStream': isLiveStream, + }; + } + + factory UserModel.fromMap(Map map) { + return UserModel( + id: map['id'], + fullName: map['fullName'] ?? '', + urlToImage: map['urlToImage'] ?? '', + description: map['description'], + phoneNumber: map['phoneNumber'], + gender: map['gender'] ?? false, + birthday: map['birthday'] != null + ? DateTime.fromMillisecondsSinceEpoch(map['birthday']) + : null, + posts: map['posts']?.toInt(), + followings: map['followings']?.toInt(), + followers: map['followers']?.toInt(), + listFields: List.from(map['listFields']), + isLiveStream: map['isLiveStream'] ?? false, + ); + } + + String toJson() => json.encode(toMap()); + + factory UserModel.fromJson(String source) => + UserModel.fromMap(json.decode(source)); + + @override + String toString() { + return 'UserModel(id: $id, fullName: $fullName, urlToImage: $urlToImage, description: $description, phoneNumber: $phoneNumber, gender: $gender, birthday: $birthday, posts: $posts, followings: $followings, followers: $followers, listFields: $listFields, isLiveStream: $isLiveStream)'; + } + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + + return other is UserModel && + other.id == id && + other.fullName == fullName && + other.urlToImage == urlToImage && + other.description == description && + other.phoneNumber == phoneNumber && + other.gender == gender && + other.birthday == birthday && + other.posts == posts && + other.followings == followings && + other.followers == followers && + listEquals(other.listFields, listFields) && + other.isLiveStream == isLiveStream; + } + + @override + int get hashCode { + return id.hashCode ^ + fullName.hashCode ^ + urlToImage.hashCode ^ + description.hashCode ^ + phoneNumber.hashCode ^ + gender.hashCode ^ + birthday.hashCode ^ + posts.hashCode ^ + followings.hashCode ^ + followers.hashCode ^ + listFields.hashCode ^ + isLiveStream.hashCode; + } +} + +List listUserFake = [ + UserModel(fullName: 'Brody', urlToImage: urlUserFake3, isLiveStream: true), + UserModel(fullName: 'Johnny', urlToImage: urlUserFake2), + UserModel(fullName: 'Caroline', urlToImage: urlUserFake), + UserModel(fullName: 'Jerry', urlToImage: urlUserFake2), + UserModel(fullName: 'Tommy', urlToImage: urlUserFake), + UserModel(fullName: 'Cris', urlToImage: urlUserFake2), +]; + +const urlUserFake = + 'https://images.unsplash.com/photo-1559969143-b2defc6419fd?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxzZWFyY2h8M3x8Z2FtZXJ8ZW58MHx8MHx8&auto=format&fit=crop&w=500&q=60'; +const urlUserFake2 = + 'https://images.unsplash.com/photo-1534488972407-5a4aa1e47d83?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxzZWFyY2h8N3x8Z2FtZXJ8ZW58MHx8MHx8&auto=format&fit=crop&w=500&q=60'; +const urlUserFake3 = + 'https://images.unsplash.com/photo-1610041321420-a596dd14ebc9?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxzZWFyY2h8MTR8fGdhbWVyfGVufDB8fDB8fA%3D%3D&auto=format&fit=crop&w=500&q=60'; diff --git a/lib/features/home/data/repositories/live_stream_repository_impl.dart b/lib/features/home/data/repositories/live_stream_repository_impl.dart new file mode 100644 index 0000000..e368739 --- /dev/null +++ b/lib/features/home/data/repositories/live_stream_repository_impl.dart @@ -0,0 +1,21 @@ +// Package imports: +import 'package:dartz/dartz.dart'; +import 'package:injectable/injectable.dart'; + +// Project imports: +import 'package:streamskit_mobile/core/error/failure.dart'; +import 'package:streamskit_mobile/features/home/data/datasources/local_live_stream_source.dart'; +import 'package:streamskit_mobile/features/home/data/model/live_stream_model.dart'; +import 'package:streamskit_mobile/features/home/domain/repositories/live_stream_repository.dart'; + +@LazySingleton(as: LiveStreamRepository) +class LiveStreamRepositoryImpl implements LiveStreamRepository { + final LocalLiveStreamSource localData; + + const LiveStreamRepositoryImpl({required this.localData}); + + @override + Either> getLiveStreams() { + return Right(localData.getLiveStreams()); + } +} diff --git a/lib/features/home/domain/repositories/live_stream_repository.dart b/lib/features/home/domain/repositories/live_stream_repository.dart new file mode 100644 index 0000000..938db50 --- /dev/null +++ b/lib/features/home/domain/repositories/live_stream_repository.dart @@ -0,0 +1,10 @@ +// Package imports: +import 'package:dartz/dartz.dart'; + +// Project imports: +import 'package:streamskit_mobile/core/error/failure.dart'; +import 'package:streamskit_mobile/features/home/data/model/live_stream_model.dart'; + +abstract class LiveStreamRepository { + Either> getLiveStreams(); +} diff --git a/lib/features/home/domain/usecases/get_list_live_streaming.dart b/lib/features/home/domain/usecases/get_list_live_streaming.dart new file mode 100644 index 0000000..323da84 --- /dev/null +++ b/lib/features/home/domain/usecases/get_list_live_streaming.dart @@ -0,0 +1,21 @@ +// Package imports: +import 'package:dartz/dartz.dart'; +import 'package:injectable/injectable.dart'; + +// Project imports: +import 'package:streamskit_mobile/core/error/failure.dart'; +import 'package:streamskit_mobile/core/usecase/usecase.dart'; +import 'package:streamskit_mobile/features/home/data/model/live_stream_model.dart'; +import 'package:streamskit_mobile/features/home/domain/repositories/live_stream_repository.dart'; + +@lazySingleton +class GetListLiveStreaming implements UseCase, NoParams> { + final LiveStreamRepository repository; + + const GetListLiveStreaming({required this.repository}); + + @override + Either> call(NoParams params) { + return repository.getLiveStreams(); + } +} diff --git a/lib/features/home/presentation/screens/home_screen.dart b/lib/features/home/presentation/screens/home_screen.dart new file mode 100644 index 0000000..fb09ef9 --- /dev/null +++ b/lib/features/home/presentation/screens/home_screen.dart @@ -0,0 +1,91 @@ +// Flutter imports: +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +// Package imports: +import 'package:phosphor_flutter/phosphor_flutter.dart'; + +// Project imports: +import 'package:streamskit_mobile/core/util/sizer_custom/sizer.dart'; +import 'package:streamskit_mobile/core/util/styles/style.dart'; +import 'package:streamskit_mobile/features/home/presentation/widgets/button_circle.dart'; +import 'package:streamskit_mobile/features/home/presentation/widgets/list_category_home.dart'; +import 'package:streamskit_mobile/features/home/presentation/widgets/list_live_stream.dart'; +import 'package:streamskit_mobile/features/home/presentation/widgets/list_user_follow.dart'; + +class HomeScreen extends StatefulWidget { + const HomeScreen({super.key}); + + @override + State createState() => _HomeScreenState(); +} + +class _HomeScreenState extends State { + @override + Widget build(BuildContext context) { + return Scaffold( + extendBodyBehindAppBar: true, + appBar: AppBar( + // toolbarHeight: 40.sp, + systemOverlayStyle: const SystemUiOverlayStyle( + statusBarColor: Colors.transparent, + statusBarBrightness: Brightness.dark, + statusBarIconBrightness: Brightness.light, + ), + backgroundColor: Colors.transparent, + surfaceTintColor: Theme.of(context).scaffoldBackgroundColor, + elevation: 0.0, + automaticallyImplyLeading: false, + centerTitle: true, + actions: [ + Container( + width: 100.w, + padding: EdgeInsets.symmetric(horizontal: 16.sp), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Image.asset( + launcherIcon, + height: 44.sp, + width: 44.sp, + fit: BoxFit.cover, + ), + Row( + children: [ + ButtonCircle( + icon: PhosphorIcons.moonLight, + onTap: () {}, + ), + SizedBox(width: 8.sp), + ButtonCircle( + icon: PhosphorIcons.bellLight, + onTap: () {}, + ), + SizedBox(width: 8.sp), + ButtonCircle( + icon: PhosphorIcons.magnifyingGlassLight, + onTap: () {}, + ), + ], + ) + ], + ), + ), + ], + ), + body: SafeArea( + bottom: false, + child: Column( + children: [ + SizedBox(height: 12.sp), + const ListUserFollow(), + SizedBox(height: 12.sp), + const ListCategoryHome(), + SizedBox(height: 12.sp), + const ListLiveStream(), + ], + ), + ), + ); + } +} diff --git a/lib/features/home/presentation/splash_screen.dart b/lib/features/home/presentation/splash_screen.dart new file mode 100644 index 0000000..6d98042 --- /dev/null +++ b/lib/features/home/presentation/splash_screen.dart @@ -0,0 +1,30 @@ +// Flutter imports: +import 'package:flutter/material.dart'; + +// Project imports: +import 'package:streamskit_mobile/core/util/sizer_custom/sizer.dart'; +import 'package:streamskit_mobile/core/util/styles/style.dart'; + +class SplashScreen extends StatelessWidget { + const SplashScreen({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Column( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Align( + alignment: Alignment.center, + child: Image.asset( + launcherIcon, + height: 240.sp, + width: 240.sp, + ), + ), + ], + ), + ); + } +} diff --git a/lib/features/home/presentation/widgets/button_circle.dart b/lib/features/home/presentation/widgets/button_circle.dart new file mode 100644 index 0000000..265c386 --- /dev/null +++ b/lib/features/home/presentation/widgets/button_circle.dart @@ -0,0 +1,35 @@ +// Flutter imports: +import 'package:flutter/material.dart'; + +// Package imports: +import 'package:phosphor_flutter/phosphor_flutter.dart'; + +// Project imports: +import 'package:streamskit_mobile/core/app/colors/app_color.dart'; +import 'package:streamskit_mobile/core/util/common/touchable_opacity.dart'; +import 'package:streamskit_mobile/core/util/sizer_custom/sizer.dart'; + +class ButtonCircle extends StatelessWidget { + final IconData? icon; + final Function() onTap; + const ButtonCircle({super.key, required this.icon, required this.onTap}); + + @override + Widget build(BuildContext context) { + return TouchableOpacity( + onTap: onTap, + child: Container( + decoration: BoxDecoration( + color: fCD, + shape: BoxShape.circle, + ), + padding: EdgeInsets.all(7.sp), + child: Icon( + icon ?? PhosphorIcons.moonLight, + color: Colors.white, + size: 18.sp, + ), + ), + ); + } +} diff --git a/lib/features/home/presentation/widgets/category_card.dart b/lib/features/home/presentation/widgets/category_card.dart new file mode 100644 index 0000000..364977e --- /dev/null +++ b/lib/features/home/presentation/widgets/category_card.dart @@ -0,0 +1,61 @@ +// Flutter imports: +import 'package:flutter/material.dart'; + +// Project imports: +import 'package:streamskit_mobile/core/app/colors/app_color.dart'; +import 'package:streamskit_mobile/core/util/common/touchable_opacity.dart'; +import 'package:streamskit_mobile/core/util/sizer_custom/sizer.dart'; +import 'package:streamskit_mobile/features/home/data/model/category_model.dart'; + +class CategoryCard extends StatelessWidget { + final CategoryModel categoryModel; + final Function() onTap; + final bool isCheck; + const CategoryCard({ + super.key, + required this.categoryModel, + required this.onTap, + required this.isCheck, + }); + + @override + Widget build(BuildContext context) { + return TouchableOpacity( + onTap: onTap, + child: Container( + margin: EdgeInsets.only(right: 10.sp), + padding: EdgeInsets.symmetric( + horizontal: 16.sp, + vertical: 4.sp, + ), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(30.sp), + color: isCheck ? colorPink : fCD, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Container( + padding: EdgeInsets.only(bottom: 2.sp), + child: Image.asset( + categoryModel.image, + height: 12.sp, + width: 12.sp, + fit: BoxFit.cover, + ), + ), + SizedBox(width: 3.sp), + Text( + categoryModel.title, + style: TextStyle( + color: mCL, + fontSize: 10.sp, + ), + ) + ], + ), + ), + ); + } +} diff --git a/lib/features/home/presentation/widgets/list_category_home.dart b/lib/features/home/presentation/widgets/list_category_home.dart new file mode 100644 index 0000000..7d77297 --- /dev/null +++ b/lib/features/home/presentation/widgets/list_category_home.dart @@ -0,0 +1,73 @@ +// Flutter imports: +import 'package:flutter/material.dart'; + +// Project imports: +import 'package:streamskit_mobile/core/app/colors/app_color.dart'; +import 'package:streamskit_mobile/core/util/sizer_custom/sizer.dart'; +import 'package:streamskit_mobile/features/home/data/model/category_model.dart'; +import 'package:streamskit_mobile/features/home/presentation/widgets/category_card.dart'; + +class ListCategoryHome extends StatefulWidget { + const ListCategoryHome({super.key}); + + @override + State createState() => _ListCategoryHomeState(); +} + +class _ListCategoryHomeState extends State { + int _currentIndex = 0; + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: EdgeInsets.symmetric(horizontal: 16.sp), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'Categories', + style: TextStyle( + color: Colors.white, + fontSize: 13.sp, + fontWeight: FontWeight.w700, + ), + ), + Text( + 'View all', + style: TextStyle( + color: colorPink, + fontSize: 11.sp, + ), + ), + ], + ), + ), + SizedBox(height: 8.sp), + SizedBox( + height: 32.5.sp, + width: double.infinity, + child: ListView.builder( + padding: EdgeInsets.symmetric(horizontal: 16.sp), + itemCount: listCategoryFake.length, + shrinkWrap: true, + scrollDirection: Axis.horizontal, + itemBuilder: ((context, index) { + return CategoryCard( + categoryModel: listCategoryFake[index], + isCheck: _currentIndex == index, + onTap: () { + setState(() { + _currentIndex = index; + }); + }, + ); + }), + ), + ), + ], + ); + } +} diff --git a/lib/features/home/presentation/widgets/list_live_stream.dart b/lib/features/home/presentation/widgets/list_live_stream.dart new file mode 100644 index 0000000..44ad95f --- /dev/null +++ b/lib/features/home/presentation/widgets/list_live_stream.dart @@ -0,0 +1,44 @@ +// Flutter imports: +import 'package:flutter/material.dart'; + +// Project imports: +import 'package:streamskit_mobile/core/util/sizer_custom/sizer.dart'; +import 'package:streamskit_mobile/features/home/data/model/live_stream_model.dart'; +import 'package:streamskit_mobile/features/home/presentation/widgets/live_stream_card.dart'; +import 'package:streamskit_mobile/features/stream/presentation/screens/stream_screen.dart'; + +class ListLiveStream extends StatelessWidget { + const ListLiveStream({super.key}); + + @override + Widget build(BuildContext context) { + return Expanded( + child: GridView.builder( + padding: EdgeInsets.symmetric( + horizontal: 16.sp, + ).add( + EdgeInsets.only( + bottom: 80.sp, + ), + ), + physics: const BouncingScrollPhysics(), + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 0.itemCountGridViewCalendar, + mainAxisSpacing: 18.sp, + crossAxisSpacing: 10.sp, + childAspectRatio: 0.58, + ), + itemCount: listLiveStreamFake.length, + itemBuilder: (context, index) { + return LiveStreamCard( + liveStreamModel: listLiveStreamFake[index], + onTap: (() { + Navigator.of(context).push(MaterialPageRoute( + builder: (context) => const StreamScreen())); + }), + ); + }, + ), + ); + } +} diff --git a/lib/features/home/presentation/widgets/list_user_follow.dart b/lib/features/home/presentation/widgets/list_user_follow.dart new file mode 100644 index 0000000..db3b446 --- /dev/null +++ b/lib/features/home/presentation/widgets/list_user_follow.dart @@ -0,0 +1,30 @@ +// Flutter imports: +import 'package:flutter/material.dart'; + +// Project imports: +import 'package:streamskit_mobile/core/util/sizer_custom/sizer.dart'; +import 'package:streamskit_mobile/features/home/data/model/user_model.dart'; +import 'package:streamskit_mobile/features/home/presentation/widgets/user_widget.dart'; + +class ListUserFollow extends StatelessWidget { + const ListUserFollow({super.key}); + + @override + Widget build(BuildContext context) { + return SizedBox( + height: 74.sp, + width: double.infinity, + child: ListView.builder( + padding: EdgeInsets.symmetric(horizontal: 16.sp), + itemCount: listUserFake.length, + shrinkWrap: true, + scrollDirection: Axis.horizontal, + itemBuilder: ((context, index) { + return UserWidget( + userModel: listUserFake[index], + ); + }), + ), + ); + } +} diff --git a/lib/features/home/presentation/widgets/live_stream_card.dart b/lib/features/home/presentation/widgets/live_stream_card.dart new file mode 100644 index 0000000..fcd1569 --- /dev/null +++ b/lib/features/home/presentation/widgets/live_stream_card.dart @@ -0,0 +1,189 @@ +// Dart imports: +import 'dart:ui'; + +// Flutter imports: +import 'package:flutter/material.dart'; + +// Package imports: +import 'package:phosphor_flutter/phosphor_flutter.dart'; + +// Project imports: +import 'package:streamskit_mobile/core/app/colors/app_color.dart'; +import 'package:streamskit_mobile/core/util/common/touchable_opacity.dart'; +import 'package:streamskit_mobile/core/util/custom_image/custom_netword_image.dart'; +import 'package:streamskit_mobile/core/util/sizer_custom/sizer.dart'; +import 'package:streamskit_mobile/core/util/styles/style.dart'; +import 'package:streamskit_mobile/features/home/data/model/live_stream_model.dart'; + +class LiveStreamCard extends StatelessWidget { + final LiveStreamModel liveStreamModel; + final Function() onTap; + const LiveStreamCard( + {super.key, required this.liveStreamModel, required this.onTap}); + + @override + Widget build(BuildContext context) { + return TouchableOpacity( + onTap: onTap, + child: Stack( + children: [ + CustomNetworkImage( + urlToImage: liveStreamModel.urlToImage, + height: 180.sp, + shape: BoxShape.rectangle, + borderRadius: BorderRadius.circular( + 13.sp, + ), + // fit: BoxFit.cover, + ), + Column( + children: [ + Container( + height: 180.sp, + padding: EdgeInsets.all(8.sp), + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + Colors.transparent, + Colors.black.withOpacity(0.89) + ], + end: Alignment.bottomCenter, + begin: Alignment.topCenter, + ), + borderRadius: BorderRadius.circular( + 13.sp, + ), + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + mainAxisSize: MainAxisSize.max, + children: [ + ClipRRect( + borderRadius: BorderRadius.circular(10.sp), + child: BackdropFilter( + filter: ImageFilter.blur( + sigmaX: 5, + sigmaY: 10, + ), + child: Container( + padding: EdgeInsets.symmetric( + horizontal: 6.sp, + vertical: 2.sp, + ), + decoration: BoxDecoration( + color: mCH.withOpacity(0.45), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Image.asset( + iconEye, + height: 13.sp, + width: 13.sp, + fit: BoxFit.cover, + color: Colors.white, + ), + SizedBox(width: 5.sp), + Text( + '${liveStreamModel.peopleParticipant}', + style: TextStyle( + color: Colors.white, + fontSize: 9.sp, + fontWeight: FontWeight.w500, + ), + ) + ], + ), + ), + ), + ), + Container( + padding: EdgeInsets.symmetric( + horizontal: 8.sp, + vertical: 2.sp, + ), + decoration: BoxDecoration( + color: liveStreamModel.getColorType, + borderRadius: BorderRadius.circular( + 9.sp, + ), + ), + child: Text( + liveStreamModel.getTitleType, + style: TextStyle( + color: mCL, + fontSize: 9.sp, + ), + ), + ), + ], + ), + Text( + 'You update Ep Ep', + style: TextStyle( + color: Colors.white, + fontSize: 11.sp, + ), + ), + ], + ), + ), + SizedBox(height: 8.sp), + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + CustomNetworkImage( + urlToImage: + 'https://cdn.dribbble.com/users/3245638/screenshots/15628559/media/21f20574f74b6d6f8e74f92bde7de2fd.png?compress=1&resize=400x300&vertical=top', + height: 30.sp, + ), + SizedBox(width: 5.sp), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Randy Rangers', + overflow: TextOverflow.ellipsis, + style: TextStyle( + color: mCL, + fontSize: 11.sp, + fontWeight: FontWeight.w700, + ), + ), + Text( + '159K Followers', + overflow: TextOverflow.ellipsis, + style: TextStyle( + color: fCL, + fontSize: 9.sp, + fontWeight: FontWeight.w500, + ), + ) + ], + ), + ), + TouchableOpacity( + onTap: () {}, + child: Container( + color: Colors.transparent, + child: Icon( + PhosphorIcons.dotsThreeVerticalFill, + size: 20.sp, + color: fCL, + ), + ), + ), + ], + ) + ], + ), + ], + ), + ); + } +} diff --git a/lib/features/home/presentation/widgets/user_widget.dart b/lib/features/home/presentation/widgets/user_widget.dart new file mode 100644 index 0000000..72d8e7c --- /dev/null +++ b/lib/features/home/presentation/widgets/user_widget.dart @@ -0,0 +1,84 @@ +// Flutter imports: +import 'package:flutter/material.dart'; + +// Project imports: +import 'package:streamskit_mobile/core/app/colors/app_color.dart'; +import 'package:streamskit_mobile/core/util/custom_image/custom_netword_image.dart'; +import 'package:streamskit_mobile/core/util/sizer_custom/sizer.dart'; +import 'package:streamskit_mobile/features/home/data/model/user_model.dart'; + +class UserWidget extends StatelessWidget { + final UserModel userModel; + + const UserWidget({super.key, required this.userModel}); + + @override + Widget build(BuildContext context) { + return Stack( + children: [ + Container( + margin: EdgeInsets.only(right: 8.sp), + child: Column( + children: [ + Container( + padding: EdgeInsets.all(3.5.sp), + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all( + width: 1.2.sp, + color: userModel.isLiveStream ? colorPink : colorBlack2, + ), + ), + child: CustomNetworkImage( + height: 42.sp, + width: 42.sp, + urlToImage: userModel.urlToImage, + shape: BoxShape.circle, + ), + ), + SizedBox(height: 6.sp), + Text( + userModel.fullName, + style: TextStyle( + color: userModel.isLiveStream ? mCL : fCL, + fontSize: 10.sp, + fontWeight: userModel.isLiveStream ? FontWeight.w500 : null, + ), + ) + ], + ), + ), + Visibility( + visible: userModel.isLiveStream, + child: Positioned( + right: 15, + child: Container( + padding: const EdgeInsets.only( + left: 1, + bottom: 1, + right: 1, + ), + alignment: Alignment.center, + color: Theme.of(context).scaffoldBackgroundColor, + child: Container( + padding: EdgeInsets.symmetric(vertical: 2.sp, horizontal: 9.sp), + decoration: BoxDecoration( + color: Colors.redAccent, + borderRadius: BorderRadius.circular(10.sp), + ), + child: Text( + 'Live', + style: TextStyle( + color: Colors.white, + fontSize: 7.sp, + fontWeight: FontWeight.w500, + ), + ), + ), + ), + ), + ) + ], + ); + } +} diff --git a/lib/features/search/.gitkeep b/lib/features/profile/data/.gitkeep similarity index 100% rename from lib/features/search/.gitkeep rename to lib/features/profile/data/.gitkeep diff --git a/lib/features/profile/data/list_live_card_model.dart b/lib/features/profile/data/list_live_card_model.dart new file mode 100644 index 0000000..ffb1729 --- /dev/null +++ b/lib/features/profile/data/list_live_card_model.dart @@ -0,0 +1,63 @@ +// Dart imports: +import 'dart:convert'; + +// Flutter imports: +import 'package:flutter/foundation.dart'; + +// Project imports: +import 'package:streamskit_mobile/features/profile/data/live_card_model.dart'; + +class ListLiveCardModel { + final int type; // 1: Live Stream // 2: LastLive // 3: STar // 4 Posts + final List listLiveCardModel; + ListLiveCardModel({ + required this.type, + required this.listLiveCardModel, + }); + + ListLiveCardModel copyWith({ + int? type, + List? listLiveCardModel, + }) { + return ListLiveCardModel( + type: type ?? this.type, + listLiveCardModel: listLiveCardModel ?? this.listLiveCardModel, + ); + } + + Map toMap() { + return { + 'type': type, + 'listLiveCardModel': listLiveCardModel.map((x) => x.toMap()).toList(), + }; + } + + factory ListLiveCardModel.fromMap(Map map) { + return ListLiveCardModel( + type: map['type']?.toInt() ?? 0, + listLiveCardModel: List.from( + map['listLiveCardModel']?.map((x) => LiveCardModel.fromMap(x))), + ); + } + + String toJson() => json.encode(toMap()); + + factory ListLiveCardModel.fromJson(String source) => + ListLiveCardModel.fromMap(json.decode(source)); + + @override + String toString() => + 'ListLiveCardModel(type: $type, listLiveCardModel: $listLiveCardModel)'; + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + + return other is ListLiveCardModel && + other.type == type && + listEquals(other.listLiveCardModel, listLiveCardModel); + } + + @override + int get hashCode => type.hashCode ^ listLiveCardModel.hashCode; +} diff --git a/lib/features/profile/data/live_card_model.dart b/lib/features/profile/data/live_card_model.dart new file mode 100644 index 0000000..fbe4f15 --- /dev/null +++ b/lib/features/profile/data/live_card_model.dart @@ -0,0 +1,123 @@ +// Dart imports: +import 'dart:convert'; + +// Flutter imports: +import 'package:flutter/material.dart'; + +class LiveCardModel { + final String id; + final String idAccount; + final String image; + final bool statusLive; + final int numberViewer; + final int categoryLive; + LiveCardModel({ + required this.id, + required this.idAccount, + required this.image, + required this.statusLive, + required this.numberViewer, + required this.categoryLive, + }); + + String get getTitleType { + switch (categoryLive) { + case 1: + return 'Game'; + case 2: + return 'Review'; + case 3: + return 'Music'; + case 4: + return 'Talk'; + } + return ''; + } + + Color get getColorType { + switch (categoryLive) { + case 1: + return Colors.redAccent; + case 2: + return Colors.purpleAccent; + case 3: + return Colors.blueAccent; + case 4: + return Colors.orangeAccent; + } + return Colors.redAccent; + } + + LiveCardModel copyWith({ + String? id, + String? idAccount, + String? image, + bool? statusLive, + int? numberViewer, + int? categoryLive, + }) { + return LiveCardModel( + id: id ?? this.id, + idAccount: idAccount ?? this.idAccount, + image: image ?? this.image, + statusLive: statusLive ?? this.statusLive, + numberViewer: numberViewer ?? this.numberViewer, + categoryLive: categoryLive ?? this.categoryLive, + ); + } + + Map toMap() { + return { + 'id': id, + 'idAccount': idAccount, + 'image': image, + 'statusLive': statusLive, + 'numberViewer': numberViewer, + 'categoryLive': categoryLive, + }; + } + + factory LiveCardModel.fromMap(Map map) { + return LiveCardModel( + id: map['id'] ?? '', + idAccount: map['idAccount'] ?? '', + image: map['image'] ?? '', + statusLive: map['statusLive'] ?? false, + numberViewer: map['numberViewer']?.toInt() ?? 0, + categoryLive: map['categoryLive']?.toInt() ?? 0, + ); + } + + String toJson() => json.encode(toMap()); + + factory LiveCardModel.fromJson(String source) => + LiveCardModel.fromMap(json.decode(source)); + + @override + String toString() { + return 'LiveCardModel(id: $id, idAccount: $idAccount, image: $image, statusLive: $statusLive, numberViewer: $numberViewer, categoryLive: $categoryLive)'; + } + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + + return other is LiveCardModel && + other.id == id && + other.idAccount == idAccount && + other.image == image && + other.statusLive == statusLive && + other.numberViewer == numberViewer && + other.categoryLive == categoryLive; + } + + @override + int get hashCode { + return id.hashCode ^ + idAccount.hashCode ^ + image.hashCode ^ + statusLive.hashCode ^ + numberViewer.hashCode ^ + categoryLive.hashCode; + } +} diff --git a/lib/features/profile/domain/.gitkeep b/lib/features/profile/domain/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/lib/features/profile/presentation/screens/edit_description_screen.dart b/lib/features/profile/presentation/screens/edit_description_screen.dart new file mode 100644 index 0000000..a869fcb --- /dev/null +++ b/lib/features/profile/presentation/screens/edit_description_screen.dart @@ -0,0 +1,131 @@ +// Flutter imports: +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +// Package imports: +import 'package:flutter_phosphor_icons/flutter_phosphor_icons.dart'; + +// Project imports: +import 'package:streamskit_mobile/core/app/colors/app_color.dart'; +import 'package:streamskit_mobile/core/navigator/app_pages.dart'; +import 'package:streamskit_mobile/core/util/common/touchable_opacity.dart'; +import 'package:streamskit_mobile/core/util/sizer_custom/sizer.dart'; +import 'package:streamskit_mobile/core/util/styles/profile_style.dart'; +import 'package:streamskit_mobile/features/profile/presentation/widgets/text_form_filed_request.dart'; + +class EditDescriptionScreen extends StatefulWidget { + final String? description; + const EditDescriptionScreen({super.key, this.description}); + + @override + State createState() => _EditDescriptionScreenState(); +} + +class _EditDescriptionScreenState extends State { + final TextEditingController _descriptionController = TextEditingController(); + String description = ""; + + @override + void initState() { + if (widget.description != null) { + _descriptionController.text = widget.description!; + } else { + _descriptionController.text = ""; + } + description = _descriptionController.text; + super.initState(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Theme.of(context).colorScheme.surface, + appBar: AppBar( + elevation: 0, + leadingWidth: 64.sp, + backgroundColor: Colors.transparent, + titleSpacing: 0.sp, + title: Text( + "Update bio", + style: text13w700mCL, + ), + leading: Padding( + padding: EdgeInsets.symmetric(horizontal: 16.sp), + child: TouchableOpacity( + onTap: () { + AppNavigator.pop(); + }, + child: Icon( + PhosphorIcons.arrow_left, + color: mCL, + ), + ), + ), + ), + body: Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Container( + margin: EdgeInsets.symmetric(horizontal: 16.sp), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8.sp), + color: mGD, + ), + child: Column( + children: [ + SizedBox(height: 4.sp), + TextFieldFormRequest( + textStyle: text9mCL, + textStyleHint: text9mGM, + controller: _descriptionController, + maxLines: 7, + onChanged: (val) { + setState(() { + description = val; + }); + }, + maxLength: 300, + hintText: + "Chỉnh sửa tiểu sử của bạn, để nhiều người biết đến", + validatorForm: null, + inputFormatters: [ + LengthLimitingTextInputFormatter(300), + ], + ), + Container( + padding: EdgeInsets.only( + right: 12.sp, + bottom: 8.sp, + ), + alignment: Alignment.bottomRight, + child: Text( + '${description.length.toString()}/300', + style: text11mGM, + ), + ), + ], + ), + ), + SizedBox(height: 10.sp), + TouchableOpacity( + onTap: () { + AppNavigator.pop(); + }, + child: Container( + margin: EdgeInsets.symmetric(horizontal: 16.sp), + padding: EdgeInsets.symmetric(horizontal: 8.sp, vertical: 5.sp), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(3.sp), + color: colorPink, + ), + child: Text( + "Xác nhận", + style: text9mCL, + ), + ), + ) + ], + ), + ); + } +} diff --git a/lib/features/profile/presentation/screens/edit_phone_number_screen.dart b/lib/features/profile/presentation/screens/edit_phone_number_screen.dart new file mode 100644 index 0000000..4699eb6 --- /dev/null +++ b/lib/features/profile/presentation/screens/edit_phone_number_screen.dart @@ -0,0 +1,235 @@ +// Flutter imports: +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +// Package imports: +import 'package:flutter_phosphor_icons/flutter_phosphor_icons.dart'; + +// Project imports: +import 'package:streamskit_mobile/core/app/colors/app_color.dart'; +import 'package:streamskit_mobile/core/navigator/app_pages.dart'; +import 'package:streamskit_mobile/core/util/common/touchable_opacity.dart'; +import 'package:streamskit_mobile/core/util/sizer_custom/sizer.dart'; +import 'package:streamskit_mobile/core/util/styles/profile_style.dart'; +import 'package:streamskit_mobile/features/profile/presentation/widgets/text_form_field_profile.dart'; + +class EditPhoneNumberScreen extends StatefulWidget { + final String? phoneNumber; + const EditPhoneNumberScreen({super.key, this.phoneNumber}); + + @override + State createState() => _EditPhoneNumberScreenState(); +} + +class _EditPhoneNumberScreenState extends State { + final GlobalKey _formKey = GlobalKey(); + final TextEditingController phoneNumberController = TextEditingController(); + final TextEditingController verifyController = TextEditingController(); + final TextEditingController newPasswordController = TextEditingController(); + bool isVisibility = false; + String _code = ""; + FocusNode focusNode = FocusNode(); + + @override + void initState() { + super.initState(); + if (widget.phoneNumber != null) { + phoneNumberController.text = widget.phoneNumber!; + } else { + phoneNumberController.text = ""; + } + verifyController.text = ""; + newPasswordController.text = ""; + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Theme.of(context).colorScheme.surface, + appBar: AppBar( + elevation: 0, + leadingWidth: 64.sp, + backgroundColor: Colors.transparent, + titleSpacing: 0.sp, + title: Text( + "Link to phone number", + style: text13w700mCL, + ), + leading: Padding( + padding: EdgeInsets.symmetric(horizontal: 16.sp), + child: TouchableOpacity( + onTap: () { + AppNavigator.pop(); + }, + child: Icon( + PhosphorIcons.arrow_left, + color: mCL, + ), + ), + ), + ), + body: Form( + key: _formKey, + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + SizedBox(height: 5.sp), + Padding( + padding: EdgeInsets.symmetric(horizontal: 16.0.sp), + child: Text( + "Nhập số điện thoại di động để nhận mã xác nhận miễn phí", + style: TextStyle( + color: mCL, + fontSize: 9.5.sp, + overflow: TextOverflow.ellipsis, + height: 1.4, + ), + ), + ), + SizedBox(height: 8.sp), + TextFormFieldProfile( + validator: (val) { + if (val != "") { + if (val!.length < 9) { + return "Số điện thoại không đúng"; + } else { + return null; + } + } else { + return "Vui lòng nhập số điện thoại"; + } + }, + icon: Icons.phone, + inputFormatters: [ + LengthLimitingTextInputFormatter(10), + FilteringTextInputFormatter.digitsOnly, + ], + hintText: "Nhập số điện thoại di động", + controller: phoneNumberController, + ), + SizedBox(height: 5.sp), + TextFormFieldProfile( + validator: (val) { + if (val != "") { + if (val!.length != 6) { + return "Mã xác nhận phải có đủ 6 kí tự"; + } else { + return null; + } + } else { + if (phoneNumberController.text != "" && + phoneNumberController.text.length >= 9 && + newPasswordController.text != "" && + newPasswordController.text.length >= 6) { + return "Vui lòng nhập mã xác nhận"; + } else { + return null; + } + } + }, + onChanged: (val) { + setState(() { + _code = val; + }); + }, + focusNode: focusNode, + icon: Icons.verified_user_outlined, + inputFormatters: [ + LengthLimitingTextInputFormatter(6), + FilteringTextInputFormatter.digitsOnly, + ], + hintText: "Nhập mã xác nhận", + controller: verifyController, + suffixIcon: TouchableOpacity( + onTap: () async { + if (phoneNumberController.text.isNotEmpty) { + // var appSignalId = await SmsAutoFill().getAppSignature; + // Map sendOTP = { + // "mobile_phone": phoneNumberController.text.toString(), + // "app_signal_id": appSignalId + // }; + // DEV: 0rF7BmP/yzk . PD: + // print(sendOTP.toString()); + } + }, + child: Container( + margin: EdgeInsets.only(top: 20.sp), + padding: + EdgeInsets.symmetric(horizontal: 4.sp, vertical: 5.sp), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(5.sp), + color: colorPink, + ), + child: Text( + 'Gửi mã xác nhận', + style: text7mCL, + ), + ), + ), + ), + SizedBox(height: 5.sp), + TextFormFieldProfile( + validator: (val) { + if (val != "") { + if (val!.length < 6) { + return "Mật khẩu phải có ít nhất 6 kí tự"; + } else { + return null; + } + } else { + return "Vui lòng nhập mật khẩu"; + } + }, + isVisibility: isVisibility, + icon: Icons.lock_outline, + hintText: "Nhập mật khẩu", + suffixIcon: TouchableOpacity( + child: Icon( + isVisibility ? Icons.visibility : Icons.visibility_off, + size: 16.sp, + color: mCU, + ), + onTap: () { + setState(() { + isVisibility = !isVisibility; + }); + }, + ), + inputFormatters: [ + LengthLimitingTextInputFormatter(32), + ], + controller: newPasswordController, + ), + SizedBox(height: 30.sp), + TouchableOpacity( + onTap: () { + if (_formKey.currentState!.validate()) { + // ignore: avoid_print + print(_code); + AppNavigator.pop(); + } + }, + child: Container( + width: double.infinity, + margin: EdgeInsets.symmetric(horizontal: 20.sp), + padding: + EdgeInsets.symmetric(horizontal: 4.sp, vertical: 12.sp), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(2.sp), + color: colorPink, + ), + child: Align( + alignment: Alignment.center, + child: Text( + 'Xong', + style: text11mCL, + ), + ), + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/features/profile/presentation/screens/edit_profile_screen.dart b/lib/features/profile/presentation/screens/edit_profile_screen.dart new file mode 100644 index 0000000..3a53050 --- /dev/null +++ b/lib/features/profile/presentation/screens/edit_profile_screen.dart @@ -0,0 +1,230 @@ +// Flutter imports: +import 'package:flutter/material.dart'; + +// Package imports: +import 'package:flutter_phosphor_icons/flutter_phosphor_icons.dart'; +import 'package:intl/intl.dart'; + +// Project imports: +import 'package:streamskit_mobile/core/app/colors/app_color.dart'; +import 'package:streamskit_mobile/core/navigator/app_pages.dart'; +import 'package:streamskit_mobile/core/navigator/app_routes.dart'; +import 'package:streamskit_mobile/core/util/common/touchable_opacity.dart'; +import 'package:streamskit_mobile/core/util/custom_image/custom_netword_image.dart'; +import 'package:streamskit_mobile/core/util/sizer_custom/sizer.dart'; +import 'package:streamskit_mobile/core/util/styles/profile_style.dart'; +import 'package:streamskit_mobile/features/home/data/model/user_model.dart'; +import 'package:streamskit_mobile/features/profile/presentation/widgets/bottom_sheet_birthday.dart'; +import 'package:streamskit_mobile/features/profile/presentation/widgets/bottom_sheet_gender.dart'; +import 'package:streamskit_mobile/features/profile/presentation/widgets/bottom_sheet_image.dart'; +import 'package:streamskit_mobile/features/profile/presentation/widgets/edit_profile_widget.dart'; + +class EditProfileScreen extends StatefulWidget { + const EditProfileScreen({super.key}); + + @override + State createState() => _EditProfileScreenState(); +} + +class _EditProfileScreenState extends State { + final UserModel user = UserModel( + id: "", + urlToImage: + "https://donoithatdanang.com/wp-content/uploads/2021/11/mang-hinh-khoa-cute-08.jpg", + fullName: "Tony Tony Chopper", + description: + "Hành trình leo thách đấu mùa 12 cùng top lane!\nhttps://www.facebook.com/chopper189 \n11PM-12PM", + posts: 1000, + followers: 9400, + followings: 8543337121, + listFields: null); + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Theme.of(context).colorScheme.surface, + appBar: AppBar( + elevation: 0, + leadingWidth: 64.sp, + titleSpacing: 0.sp, + backgroundColor: Colors.transparent, + title: Text( + "Update profile", + style: text13w700mCL, + ), + leading: Padding( + padding: EdgeInsets.symmetric(horizontal: 16.sp), + child: TouchableOpacity( + onTap: () { + AppNavigator.pop(); + }, + child: Icon( + PhosphorIcons.arrow_left, + color: mCL, + ), + ), + ), + ), + body: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + TouchableOpacity( + onTap: () { + showModalBottomSheet( + context: context, + backgroundColor: Colors.transparent, + builder: (context) => BottomSheetImage( + handleFinish: (val) { + setState(() { + user.urlToImage = val; + }); + }, + ), + ); + }, + child: Container( + padding: EdgeInsets.symmetric(horizontal: 8.sp, vertical: 8.sp), + decoration: BoxDecoration( + color: Colors.grey.shade900, + ), + child: Row( + children: [ + Padding( + padding: EdgeInsets.only(left: 8.sp), + child: Text( + "Ảnh đại diện", + style: text11mGB, + ), + ), + const Spacer(), + CustomNetworkImage( + urlToImage: user.urlToImage, + height: 36.sp, + width: 36.sp, + ), + SizedBox( + width: 1.sp, + ), + Icon( + Icons.navigate_next, + size: 18.sp, + color: mGB, + ) + ], + ), + ), + ), + SizedBox(height: 12.sp), + ProfileEditWidget( + onTap: () { + AppNavigator.push( + Routes.editUsernameRoute, + arguments: {"username": "Tony Tony Chopper"}, + ); + }, + title: "Nickname/Tên hiển thị", + value: "Tony Tony Chopper", + ), + SizedBox(height: 4.sp), + ProfileEditWidget( + onTap: () {}, + title: "ID", + value: "58965874", + style: text11mGB, + ), + SizedBox(height: 12.sp), + ProfileEditWidget( + onTap: () { + AppNavigator.push( + Routes.editDescriptionRoute, + arguments: { + "description": + "Hành trình leo thách đấu mùa 12 cùng top lane!\nhttps://www.facebook.com/chopper189 \n11PM-12PM" + }, + ); + }, + title: "Giới thiệu cá nhân", + value: + "Hành trình leo thách đấu mùa 12 cùng top lane!\nhttps://www.facebook.com/chopper189 \n11PM-12PM", + ), + SizedBox(height: 12.sp), + Padding( + padding: EdgeInsets.symmetric(horizontal: 16.0.sp), + child: Text( + "Personal Information", + style: text9mGM, + ), + ), + SizedBox(height: 12.sp), + ProfileEditWidget( + onTap: () { + AppNavigator.push( + Routes.editPhoneNumberRoute, + arguments: { + "phoneNumber": '0338671454', + }, + ); + }, + title: "Phone Number", + value: user.phoneNumber ?? "Phone Number", + style: user.phoneNumber == null ? text11mGM : null, + ), + SizedBox(height: 2.sp), + ProfileEditWidget( + onTap: () { + showModalBottomSheet( + context: context, + backgroundColor: Colors.transparent, + builder: (context) => BottomSheetGender( + handleSelectGender: (value) { + if (value != null) { + setState(() { + user.gender = value == "Man" ? true : false; + }); + } + }, + ), + ); + }, + title: "Gender", + value: user.gender ? "Man" : "Woman", + ), + SizedBox(height: 2.sp), + ProfileEditWidget( + onTap: () { + showModalBottomSheet( + context: context, + backgroundColor: Colors.transparent, + builder: (context) => BottomSheetBirthday( + onDateChanged: (date) { + if (date != null) { + setState( + () { + user.birthday = date.toLocal(); + }, + ); + } + AppNavigator.pop(); + }, + dateInit: user.birthday ?? + DateTime( + DateTime.now().year - 18, + ), + ), + ); + }, + title: "Birthday", + value: DateFormat("dd-MM-yyyy").format( + user.birthday ?? + DateTime( + DateTime.now().year - 18, + ), + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/features/profile/presentation/screens/edit_username_screen.dart b/lib/features/profile/presentation/screens/edit_username_screen.dart new file mode 100644 index 0000000..9d07001 --- /dev/null +++ b/lib/features/profile/presentation/screens/edit_username_screen.dart @@ -0,0 +1,133 @@ +// Flutter imports: +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +// Package imports: +import 'package:flutter_phosphor_icons/flutter_phosphor_icons.dart'; + +// Project imports: +import 'package:streamskit_mobile/core/app/colors/app_color.dart'; +import 'package:streamskit_mobile/core/navigator/app_pages.dart'; +import 'package:streamskit_mobile/core/util/common/touchable_opacity.dart'; +import 'package:streamskit_mobile/core/util/sizer_custom/sizer.dart'; +import 'package:streamskit_mobile/core/util/styles/profile_style.dart'; +import 'package:streamskit_mobile/features/profile/presentation/widgets/text_form_filed_request.dart'; + +class EditUserNameScreen extends StatefulWidget { + final String? username; + const EditUserNameScreen({super.key, this.username}); + + @override + State createState() => _EditUserNameScreenState(); +} + +class _EditUserNameScreenState extends State { + final TextEditingController usernameController = TextEditingController(); + final GlobalKey _formKey = GlobalKey(); + + @override + void initState() { + if (widget.username != null) { + usernameController.text = widget.username!; + } else { + usernameController.text = ""; + } + super.initState(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Theme.of(context).colorScheme.surface, + appBar: AppBar( + elevation: 0, + leadingWidth: 64.sp, + backgroundColor: Colors.transparent, + titleSpacing: 0.sp, + title: Text( + "Update user name", + style: text13w700mCL, + ), + leading: Padding( + padding: EdgeInsets.symmetric(horizontal: 16.sp), + child: TouchableOpacity( + onTap: () { + AppNavigator.pop(); + }, + child: Icon( + PhosphorIcons.arrow_left, + color: mCL, + ), + ), + ), + ), + body: Form( + key: _formKey, + child: Column( + children: [ + Padding( + padding: EdgeInsets.symmetric(horizontal: 16.0.sp), + child: TextFieldFormRequest( + underLine: mGM, + suffixIcon: TouchableOpacity( + child: Icon( + Icons.cancel, + size: 16.sp, + color: mGB, + ), + onTap: () { + setState(() { + usernameController.clear(); + }); + }, + ), + inputFormatters: [ + FilteringTextInputFormatter.allow(RegExp( + "[ aAàÀảẢãÃáÁạẠăĂằẰẳẲẵẴắẮặẶâÂầẦẩẨẫẪấẤậẬbBcCdDđĐeEèÈẻẺẽẼéÉẹẸêÊềỀểỂễỄếẾệỆfFgGhHiIìÌỉỈĩĨíÍịỊjJkKlLmMnNoOòÒỏỎõÕóÓọỌôÔồỒổỔỗỖốỐộỘơƠờỜởỞỡỠớỚợỢpPqQrRsStTuUùÙủỦũŨúÚụỤưƯừỪửỬữỮứỨựỰvVwWxXyYỳỲỷỶỹỸýÝỵỴzZ]")), + ], + validatorForm: (val) { + if (val != "") { + if (val!.length < 3) { + return "Tên hiển thị phải lớn hơn bằng 3"; + } else { + return null; + } + } else { + return "Vui lòng nhập tên hiển thị"; + } + }, + hintText: "Tên hiển thị", + controller: usernameController, + ), + ), + SizedBox(height: 30.sp), + TouchableOpacity( + onTap: () { + if (_formKey.currentState!.validate()) { + AppNavigator.pop(); + } + }, + child: Container( + width: double.infinity, + margin: EdgeInsets.symmetric(horizontal: 20.sp), + padding: + EdgeInsets.symmetric(horizontal: 4.sp, vertical: 12.sp), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(2.sp), + color: colorPink, + ), + child: Align( + alignment: Alignment.center, + child: Text( + 'Xong', + style: text11mCL, + ), + ), + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/features/profile/presentation/screens/profile_screen.dart b/lib/features/profile/presentation/screens/profile_screen.dart new file mode 100644 index 0000000..fffd46e --- /dev/null +++ b/lib/features/profile/presentation/screens/profile_screen.dart @@ -0,0 +1,317 @@ +// Flutter imports: +import 'package:flutter/material.dart'; + +// Package imports: +import 'package:flutter_phosphor_icons/flutter_phosphor_icons.dart'; + +// Project imports: +import 'package:streamskit_mobile/core/app/colors/app_color.dart'; +import 'package:streamskit_mobile/core/navigator/app_pages.dart'; +import 'package:streamskit_mobile/core/util/custom_image/custom_netword_image.dart'; +import 'package:streamskit_mobile/core/util/sizer_custom/sizer.dart'; +import 'package:streamskit_mobile/core/util/styles/profile_style.dart'; +import 'package:streamskit_mobile/features/home/data/model/user_model.dart'; +import 'package:streamskit_mobile/features/profile/data/list_live_card_model.dart'; +import 'package:streamskit_mobile/features/profile/data/live_card_model.dart'; +import 'package:streamskit_mobile/features/profile/presentation/widgets/bottom_sheet_choose_option.dart'; +import 'package:streamskit_mobile/features/profile/presentation/widgets/circle_icon.dart'; +import 'package:streamskit_mobile/features/profile/presentation/widgets/details_info_live_user.dart'; +import 'package:streamskit_mobile/features/profile/presentation/widgets/gribview_live_card.dart'; + +class ProfileScreen extends StatefulWidget { + const ProfileScreen({super.key}); + + @override + State createState() => _ProfileScreenState(); +} + +class _ProfileScreenState extends State + with TickerProviderStateMixin { + late TabController _tabController; + late ScrollController _scrollController; + late AnimationController _colorAnimationController; + late Animation colorTween, iconColorTween; + late Animation colorTitleTween; + bool isMe = true; + + @override + void initState() { + // Initial controller + _tabController = TabController(length: 4, vsync: this); + _scrollController = ScrollController(); + // Initial animation + _colorAnimationController = + AnimationController(vsync: this, duration: const Duration(seconds: 0)); + colorTween = + ColorTween(begin: Colors.transparent, end: const Color(0xff0d0d0d)) + .animate(_colorAnimationController); + iconColorTween = + ColorTween(begin: const Color(0xff5a5959), end: const Color(0xff0d0d0d)) + .animate(_colorAnimationController); + colorTitleTween = + Tween(begin: 0, end: 1).animate(_colorAnimationController); + // Controller listen + _scrollController.addListener( + () { + _colorAnimationController + .animateTo(_scrollController.position.pixels / 160.sp); + }, + ); + super.initState(); + } + + @override + void dispose() { + _colorAnimationController.dispose(); + _scrollController.dispose(); + _tabController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return DefaultTabController( + initialIndex: 1, + length: 4, + child: Scaffold( + extendBodyBehindAppBar: true, + backgroundColor: Theme.of(context).colorScheme.surface, + appBar: PreferredSize( + preferredSize: const Size(double.infinity, kToolbarHeight), + child: AnimatedBuilder( + animation: _colorAnimationController, + builder: (context, child) => AppBar( + elevation: 0, + centerTitle: true, + title: Opacity( + opacity: colorTitleTween.value, + child: Row( + children: [ + CustomNetworkImage( + urlToImage: user.urlToImage, + height: 25.sp, + width: 25.sp, + ), + SizedBox( + width: 10.sp, + ), + Text( + "Tony Tony Chopper", + style: text13mCL, + ), + ], + ), + ), + leading: !isMe + ? Padding( + padding: EdgeInsets.symmetric(horizontal: 16.sp), + child: CircleIcon( + onTap: () { + AppNavigator.pop(); + }, + icon: PhosphorIcons.arrow_left, + backgroundIcon: iconColorTween.value, + ), + ) + : null, + leadingWidth: 64.sp, + backgroundColor: colorTween.value, + actions: [ + Padding( + padding: EdgeInsets.symmetric(horizontal: 16.sp), + child: CircleIcon( + onTap: () { + showModalBottomSheet( + backgroundColor: Colors.transparent, + context: context, + builder: (context) => + const BottomSheetChooseOption()); + }, + icon: PhosphorIcons.dots_three, + backgroundIcon: iconColorTween.value, + ), + ), + ], + ), + ), + ), + body: NestedScrollView( + controller: _scrollController, + headerSliverBuilder: (context, value) { + return [ + SliverToBoxAdapter( + child: Container( + margin: EdgeInsets.only(top: 12.sp), + padding: EdgeInsets.symmetric(horizontal: 8.sp), + child: DetailInfoLiveUserWidget(user: user), + ), + ), + SliverToBoxAdapter( + child: Padding( + padding: EdgeInsets.symmetric(horizontal: 16.sp), + child: Column( + children: [ + const Divider( + thickness: 0.5, + color: Colors.white24, + ), + TabBar( + onTap: (_) {}, + overlayColor: WidgetStateProperty.all( + Colors.transparent), + indicatorSize: TabBarIndicatorSize.label, + automaticIndicatorColorAdjustment: false, + indicatorColor: mGB, + unselectedLabelStyle: TextStyle( + color: mGB, + fontSize: 10.sp, + fontWeight: FontWeight.w500, + ), + labelStyle: TextStyle( + color: mGB, + fontSize: 11.sp, + fontWeight: FontWeight.w500, + ), + isScrollable: true, + controller: _tabController, + tabs: [ + Tab( + child: SizedBox( + width: 64.sp, + child: const Text("Live Stream"), + ), + ), + Tab( + child: SizedBox( + width: 48.sp, + child: const Text("Last Live"), + ), + ), + Tab( + child: SizedBox( + width: 30.sp, + child: const Text("Star"), + ), + ), + Tab( + child: SizedBox( + width: 30.sp, + child: const Text("Posts"), + ), + ), + ], + ), + ], + ), + ), + ), + ]; + }, + body: TabBarView( + physics: const BouncingScrollPhysics(), + controller: _tabController, + children: [ + GridviewLiveCard( + liveModel: listLiveStream.listLiveCardModel, + type: listLiveStream.type, + ), + GridviewLiveCard( + liveModel: [listLiveStream.listLiveCardModel[1]], + type: listLiveStream.type, + ), + GridviewLiveCard( + liveModel: [listLiveStream.listLiveCardModel[2]], + type: listLiveStream.type, + ), + GridviewLiveCard( + liveModel: [listLiveStream.listLiveCardModel[3]], + type: listLiveStream.type, + ), + ], + ), + ), + ), + ); + } + + ListLiveCardModel listLiveStream = ListLiveCardModel( + type: 1, + listLiveCardModel: [ + LiveCardModel( + id: "", + idAccount: "", + categoryLive: 4, + image: + "https://cyberxanh.vn/wp-content/uploads/2021/11/top-20-mau-thiet-ke-phong-livestream-game-cuc-dinh-33-1038x800.jpg", + numberViewer: 950000001, + statusLive: true), + LiveCardModel( + id: "", + idAccount: "", + categoryLive: 4, + image: + "https://cyberxanh.vn/wp-content/uploads/2021/11/top-20-mau-thiet-ke-phong-livestream-game-cuc-dinh-33-1038x800.jpg", + numberViewer: 950000001, + statusLive: true), + LiveCardModel( + id: "", + idAccount: "", + categoryLive: 4, + image: + "https://cyberxanh.vn/wp-content/uploads/2021/11/top-20-mau-thiet-ke-phong-livestream-game-cuc-dinh-33-1038x800.jpg", + numberViewer: 950000001, + statusLive: true), + LiveCardModel( + id: "", + idAccount: "", + categoryLive: 4, + image: + "https://cyberxanh.vn/wp-content/uploads/2021/11/top-20-mau-thiet-ke-phong-livestream-game-cuc-dinh-33-1038x800.jpg", + numberViewer: 950000001, + statusLive: true), + LiveCardModel( + id: "", + idAccount: "", + categoryLive: 3, + image: + "https://st.nhipcaudautu.vn/staticFile/Subject/2019/01/03/livestream-game_31517638.jpg", + numberViewer: 78421, + statusLive: true), + LiveCardModel( + id: "", + idAccount: "", + categoryLive: 2, + image: + "https://photo-cms-tinnhanhchungkhoan.zadn.vn/w660/Uploaded/2022/gtnwae/2022_03_14/z-a-5520.jpg", + numberViewer: 78421, + statusLive: true), + LiveCardModel( + id: "", + idAccount: "", + categoryLive: 1, + image: + "https://ecdn.game4v.com/g4v-content/uploads/2021/05/stream-1.jpg", + numberViewer: 950000001, + statusLive: true) + ], + ); + static const listFieldLive = [ + "Dota 2", + "Mobile Legend", + "LOL", + "PUBG", + "Rise of Kingdom", + "Genshin Impact" + ]; + final UserModel user = UserModel( + id: "", + urlToImage: + "https://donoithatdanang.com/wp-content/uploads/2021/11/mang-hinh-khoa-cute-08.jpg", + fullName: "Tony Tony Chopper", + description: + "Hành trình leo thách đấu mùa 12 cùng top lane!\nhttps://www.facebook.com/chopper189 \n11PM-12PM", + posts: 1000, + followers: 9400, + followings: 8543337121, + listFields: listFieldLive); +} diff --git a/lib/features/profile/presentation/screens/setting_screen.dart b/lib/features/profile/presentation/screens/setting_screen.dart new file mode 100644 index 0000000..e352abc --- /dev/null +++ b/lib/features/profile/presentation/screens/setting_screen.dart @@ -0,0 +1,94 @@ +// Flutter imports: +import 'package:flutter/material.dart'; + +// Package imports: +import 'package:flutter_phosphor_icons/flutter_phosphor_icons.dart'; + +// Project imports: +import 'package:streamskit_mobile/core/app/colors/app_color.dart'; +import 'package:streamskit_mobile/core/navigator/app_pages.dart'; +import 'package:streamskit_mobile/core/util/common/touchable_opacity.dart'; +import 'package:streamskit_mobile/core/util/sizer_custom/sizer.dart'; +import 'package:streamskit_mobile/core/util/styles/profile_style.dart'; +import 'package:streamskit_mobile/features/profile/presentation/widgets/account_setting.dart'; +import 'package:streamskit_mobile/features/profile/presentation/widgets/content_setting.dart'; +import 'package:streamskit_mobile/features/profile/presentation/widgets/login_setting.dart'; + +class SettingScreen extends StatefulWidget { + const SettingScreen({super.key}); + + @override + State createState() => _SettingScreenState(); +} + +class _SettingScreenState extends State { + String version = "1.0.0"; + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Theme.of(context).colorScheme.surface, + appBar: AppBar( + elevation: 0, + leadingWidth: 64.sp, + centerTitle: true, + backgroundColor: Colors.transparent, + title: Text( + "Settings and privacy", + style: text13w700mCL, + ), + leading: Padding( + padding: EdgeInsets.symmetric(horizontal: 16.sp), + child: TouchableOpacity( + onTap: () { + AppNavigator.pop(); + }, + child: Icon( + PhosphorIcons.arrow_left, + color: mCL, + ), + ), + ), + ), + body: Container( + padding: EdgeInsets.symmetric( + horizontal: 16.sp, + ), + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox(height: 8.sp), + const AccountSetting(), + SizedBox(height: 8.sp), + Divider( + thickness: 0.2.sp, + color: mCH, + ), + SizedBox(height: 12.sp), + const ContentSetting(), + SizedBox(height: 8.sp), + Divider( + thickness: 0.2.sp, + color: mCH, + ), + SizedBox(height: 12.sp), + const LoginSetting(), + SizedBox(height: 50.sp), + Align( + alignment: Alignment.center, + child: Text( + "Version app $version", + style: TextStyle( + color: fCD, + fontSize: 9.sp, + ), + ), + ), + SizedBox(height: 12.sp), + ], + ), + ), + ), + ); + } +} diff --git a/lib/features/profile/presentation/widgets/account_setting.dart b/lib/features/profile/presentation/widgets/account_setting.dart new file mode 100644 index 0000000..7825a15 --- /dev/null +++ b/lib/features/profile/presentation/widgets/account_setting.dart @@ -0,0 +1,84 @@ +// Flutter imports: +import 'package:flutter/material.dart'; + +// Package imports: +import 'package:flutter_phosphor_icons/flutter_phosphor_icons.dart'; + +// Project imports: +import 'package:streamskit_mobile/core/app/colors/app_color.dart'; +import 'package:streamskit_mobile/core/navigator/app_pages.dart'; +import 'package:streamskit_mobile/core/navigator/app_routes.dart'; +import 'package:streamskit_mobile/core/util/sizer_custom/sizer.dart'; +import 'package:streamskit_mobile/features/profile/presentation/widgets/row_icon_text.dart'; + +class AccountSetting extends StatelessWidget { + const AccountSetting({ + super.key, + }); + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "ACCOUNT", + style: TextStyle( + fontSize: 10.sp, + color: Colors.grey, + fontWeight: FontWeight.w700, + ), + ), + SizedBox(height: 8.sp), + RowIconText( + title: "Manage account", + colorLeading: mCL, + sizeLeading: 15.sp, + textStyle: TextStyle(color: mCL, fontSize: 11.sp), + iconLeading: PhosphorIcons.user_thin, + onTap: () { + AppNavigator.push( + Routes.editProfileRoute, + ); + }, + ), + SizedBox(height: 8.sp), + RowIconText( + title: "Privacy", + sizeLeading: 15.sp, + colorLeading: mCL, + textStyle: TextStyle(color: mCL, fontSize: 11.sp), + iconLeading: PhosphorIcons.lock_key_thin, + onTap: () {}, + ), + SizedBox(height: 8.sp), + RowIconText( + title: "Balance", + sizeLeading: 15.sp, + colorLeading: mCL, + textStyle: TextStyle(color: mCL, fontSize: 11.sp), + iconLeading: PhosphorIcons.wallet_thin, + onTap: () {}, + ), + SizedBox(height: 8.sp), + RowIconText( + title: "QR code", + sizeLeading: 15.sp, + colorLeading: mCL, + textStyle: TextStyle(color: mCL, fontSize: 11.sp), + iconLeading: PhosphorIcons.qr_code_thin, + onTap: () {}, + ), + SizedBox(height: 8.sp), + RowIconText( + title: "Share profile", + sizeLeading: 15.sp, + colorLeading: mCL, + textStyle: TextStyle(color: mCL, fontSize: 11.sp), + iconLeading: PhosphorIcons.share_thin, + onTap: () {}, + ), + ], + ); + } +} diff --git a/lib/features/profile/presentation/widgets/bottom_sheet_birthday.dart b/lib/features/profile/presentation/widgets/bottom_sheet_birthday.dart new file mode 100644 index 0000000..2e0d614 --- /dev/null +++ b/lib/features/profile/presentation/widgets/bottom_sheet_birthday.dart @@ -0,0 +1,103 @@ +// Flutter imports: +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; + +// Package imports: +import 'package:intl/intl.dart'; + +// Project imports: +import 'package:streamskit_mobile/core/app/colors/app_color.dart'; +import 'package:streamskit_mobile/core/util/common/touchable_opacity.dart'; +import 'package:streamskit_mobile/core/util/sizer_custom/sizer.dart'; +import 'package:streamskit_mobile/core/util/styles/profile_style.dart'; +import 'package:streamskit_mobile/features/profile/presentation/widgets/bottom_sheet_choose_option.dart'; + +class BottomSheetBirthday extends StatefulWidget { + final Function(DateTime?) onDateChanged; + final DateTime? dateInit; + + const BottomSheetBirthday({ + super.key, + required this.onDateChanged, + required this.dateInit, + }); + + @override + State createState() => _BottomSheetBirthdayState(); +} + +class _BottomSheetBirthdayState extends State { + DateTime dateSelect = DateTime.now(); + @override + void dispose() { + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.vertical( + top: Radius.circular(20.sp), + ), + color: Colors.grey.shade900, + ), + child: Column( + children: [ + const DividerBottomSheet(), + Padding( + padding: EdgeInsets.symmetric(horizontal: 16.0.sp, vertical: 12.sp), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + SizedBox( + width: 18.sp, + ), + Text( + DateFormat("dd-MM-yyyy").format(dateSelect), + style: text11mCL, + ), + TouchableOpacity( + onTap: () { + if (dateSelect != widget.dateInit) { + widget.onDateChanged(dateSelect); + } + }, + child: Icon( + Icons.check_outlined, + color: mCU, + size: 18.sp, + ), + ), + ], + ), + ), + Expanded( + child: CupertinoTheme( + data: CupertinoThemeData( + textTheme: CupertinoTextThemeData( + dateTimePickerTextStyle: text11mCL, + ), + ), + child: CupertinoDatePicker( + mode: CupertinoDatePickerMode.date, + initialDateTime: widget.dateInit ?? + DateTime( + DateTime.now().year - 1, + ), + maximumDate: DateTime.now().toLocal(), + minimumDate: DateTime(DateTime.now().year - 100), + onDateTimeChanged: (date) { + setState(() { + dateSelect = date; + }); + }, + dateOrder: DatePickerDateOrder.dmy, + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/features/profile/presentation/widgets/bottom_sheet_choose_option.dart b/lib/features/profile/presentation/widgets/bottom_sheet_choose_option.dart new file mode 100644 index 0000000..bc1b1ff --- /dev/null +++ b/lib/features/profile/presentation/widgets/bottom_sheet_choose_option.dart @@ -0,0 +1,78 @@ +// Flutter imports: +import 'package:flutter/material.dart'; + +// Project imports: +import 'package:streamskit_mobile/core/app/colors/app_color.dart'; +import 'package:streamskit_mobile/core/navigator/app_pages.dart'; +import 'package:streamskit_mobile/core/navigator/app_routes.dart'; +import 'package:streamskit_mobile/core/util/sizer_custom/sizer.dart'; +import 'package:streamskit_mobile/core/util/styles/profile_style.dart'; +import 'package:streamskit_mobile/features/profile/presentation/widgets/row_icon_text.dart'; + +class BottomSheetChooseOption extends StatelessWidget { + const BottomSheetChooseOption({super.key}); + + @override + Widget build(BuildContext context) { + return Container( + padding: EdgeInsets.symmetric(horizontal: 16.sp).add( + EdgeInsets.only(bottom: 10.sp), + ), + decoration: BoxDecoration( + borderRadius: BorderRadius.vertical( + top: Radius.circular(20.sp), + ), + color: Colors.grey.shade900, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const DividerBottomSheet(), + SizedBox( + height: 8.sp, + ), + const RowIconText( + title: "Creator tools", + iconLeading: Icons.manage_accounts_outlined, + iconSuffix: Icons.circle, + colorSuffix: Colors.red, + ), + Divider( + thickness: 0.2, + color: mCU, + ), + RowIconText( + title: "Settings and privacy", + iconLeading: Icons.settings_outlined, + onTap: () { + AppNavigator.pop(); + AppNavigator.push( + Routes.settingRoute, + ); + }, + ), + ], + ), + ); + } +} + +class DividerBottomSheet extends StatelessWidget { + const DividerBottomSheet({super.key}); + + @override + Widget build(BuildContext context) { + return Align( + alignment: Alignment.center, + child: Container( + height: 3.sp, + width: 60.sp, + decoration: BoxDecoration( + color: colorDividerBottomSheetDark, + borderRadius: BorderRadius.circular(30), + ), + ), + ); + } +} diff --git a/lib/features/profile/presentation/widgets/bottom_sheet_gender.dart b/lib/features/profile/presentation/widgets/bottom_sheet_gender.dart new file mode 100644 index 0000000..9aaea5e --- /dev/null +++ b/lib/features/profile/presentation/widgets/bottom_sheet_gender.dart @@ -0,0 +1,57 @@ +// Flutter imports: +import 'package:flutter/material.dart'; + +// Project imports: +import 'package:streamskit_mobile/core/app/colors/app_color.dart'; +import 'package:streamskit_mobile/core/navigator/app_pages.dart'; +import 'package:streamskit_mobile/core/util/sizer_custom/sizer.dart'; +import 'package:streamskit_mobile/features/profile/presentation/widgets/bottom_sheet_choose_option.dart'; +import 'package:streamskit_mobile/features/profile/presentation/widgets/row_icon_text.dart'; + +class BottomSheetGender extends StatelessWidget { + final Function handleSelectGender; + const BottomSheetGender({super.key, required this.handleSelectGender}); + + @override + Widget build(BuildContext context) { + return Container( + padding: EdgeInsets.symmetric(horizontal: 16.sp).add( + EdgeInsets.only(bottom: 10.sp), + ), + decoration: BoxDecoration( + borderRadius: BorderRadius.vertical( + top: Radius.circular(20.sp), + ), + color: Colors.grey.shade900, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const DividerBottomSheet(), + SizedBox( + height: 8.sp, + ), + RowIconText( + title: "Man", + onTap: () { + AppNavigator.pop(); + handleSelectGender("Man"); + }, + ), + Divider( + thickness: 0.2, + color: mCU, + ), + RowIconText( + title: "Woman", + onTap: () { + AppNavigator.pop(); + handleSelectGender("Woman"); + }, + ), + ], + ), + ); + } +} diff --git a/lib/features/profile/presentation/widgets/bottom_sheet_image.dart b/lib/features/profile/presentation/widgets/bottom_sheet_image.dart new file mode 100644 index 0000000..85664d4 --- /dev/null +++ b/lib/features/profile/presentation/widgets/bottom_sheet_image.dart @@ -0,0 +1,103 @@ +// Flutter imports: +import 'package:flutter/material.dart'; + +// Package imports: +import 'package:image_picker/image_picker.dart'; + +// Project imports: +import 'package:streamskit_mobile/core/app/colors/app_color.dart'; +import 'package:streamskit_mobile/core/navigator/app_pages.dart'; +import 'package:streamskit_mobile/core/util/sizer_custom/sizer.dart'; +import 'package:streamskit_mobile/core/util/styles/profile_style.dart'; +import 'package:streamskit_mobile/features/profile/presentation/widgets/bottom_sheet_choose_option.dart'; +import 'package:streamskit_mobile/features/profile/presentation/widgets/row_icon_text.dart'; + +class BottomSheetImage extends StatefulWidget { + final Function(String)? handleFinish; + const BottomSheetImage({super.key, this.handleFinish}); + + @override + State createState() => _BottomSheetImageState(); +} + +class _BottomSheetImageState extends State { + final _picker = ImagePicker(); + + Future getImage({context, source = ImageSource.gallery}) async { + return await _picker.pickImage(source: source, imageQuality: 100); + } + + Future handleImagePicker( + BuildContext context, { + ImageSource source = ImageSource.gallery, + Function? handleFinish, + }) async { + try { + AppNavigator.pop(); + XFile? image = await getImage( + context: context, + source: source, + ); + if (image != null && handleFinish != null) { + handleFinish(image.path); + } + } catch (exception) { + // ignore: avoid_print + print(exception); + } + } + + @override + Widget build(BuildContext context) { + return Container( + padding: EdgeInsets.symmetric(horizontal: 16.sp).add( + EdgeInsets.only(bottom: 10.sp), + ), + decoration: BoxDecoration( + borderRadius: BorderRadius.vertical( + top: Radius.circular(20.sp), + ), + color: Colors.grey.shade900, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const DividerBottomSheet(), + SizedBox( + height: 8.sp, + ), + Text( + 'New Avatar', + style: text13w700mCL, + ), + SizedBox( + height: 8.sp, + ), + RowIconText( + iconLeading: Icons.image_outlined, + title: "Gallery", + onTap: () async { + await handleImagePicker(context, + source: ImageSource.gallery, + handleFinish: widget.handleFinish); + }, + ), + Divider( + thickness: 0.2, + color: mCU, + ), + RowIconText( + iconLeading: Icons.camera, + title: "Camera", + onTap: () async { + await handleImagePicker(context, + source: ImageSource.camera, + handleFinish: widget.handleFinish); + }, + ), + ], + ), + ); + } +} diff --git a/lib/features/profile/presentation/widgets/circle_icon.dart b/lib/features/profile/presentation/widgets/circle_icon.dart new file mode 100644 index 0000000..42cbe0f --- /dev/null +++ b/lib/features/profile/presentation/widgets/circle_icon.dart @@ -0,0 +1,40 @@ +// Flutter imports: +import 'package:flutter/material.dart'; + +// Project imports: +import 'package:streamskit_mobile/core/util/common/touchable_opacity.dart'; + +class CircleIcon extends StatelessWidget { + final IconData icon; + final Function()? onTap; + final Color? colorIcon; + final Color? backgroundIcon; + final double? sizeIcon; + const CircleIcon({ + super.key, + required this.icon, + this.onTap, + this.colorIcon, + this.backgroundIcon, + this.sizeIcon, + }); + + @override + Widget build(BuildContext context) { + return TouchableOpacity( + onTap: onTap ?? () {}, + child: Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + shape: BoxShape.circle, + color: backgroundIcon ?? const Color(0xff5a5959), + ), + child: Icon( + icon, + color: colorIcon ?? Colors.white, + size: sizeIcon, + ), + ), + ); + } +} diff --git a/lib/features/profile/presentation/widgets/content_setting.dart b/lib/features/profile/presentation/widgets/content_setting.dart new file mode 100644 index 0000000..353f784 --- /dev/null +++ b/lib/features/profile/presentation/widgets/content_setting.dart @@ -0,0 +1,60 @@ +// Flutter imports: +import 'package:flutter/material.dart'; + +// Package imports: +import 'package:flutter_phosphor_icons/flutter_phosphor_icons.dart'; + +// Project imports: +import 'package:streamskit_mobile/core/app/colors/app_color.dart'; +import 'package:streamskit_mobile/core/util/sizer_custom/sizer.dart'; +import 'package:streamskit_mobile/features/profile/presentation/widgets/row_icon_text.dart'; + +class ContentSetting extends StatelessWidget { + const ContentSetting({ + super.key, + }); + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "CONTENT & ACTIVITY", + style: TextStyle( + fontSize: 10.sp, + color: Colors.grey, + fontWeight: FontWeight.w700, + ), + ), + SizedBox(height: 8.sp), + RowIconText( + title: "Push notifications", + colorLeading: mCL, + sizeLeading: 15.sp, + textStyle: TextStyle(color: mCL, fontSize: 11.sp), + iconLeading: PhosphorIcons.bell, + onTap: () {}, + ), + SizedBox(height: 8.sp), + RowIconText( + title: "Language", + colorLeading: mCL, + sizeLeading: 15.sp, + textStyle: TextStyle(color: mCL, fontSize: 11.sp), + iconLeading: PhosphorIcons.translate_thin, + onTap: () {}, + ), + SizedBox(height: 8.sp), + RowIconText( + title: "Watch history", + colorLeading: mCL, + sizeLeading: 15.sp, + textStyle: TextStyle(color: mCL, fontSize: 11.sp), + iconLeading: PhosphorIcons.clock_counter_clockwise_thin, + onTap: () {}, + ), + ], + ); + } +} diff --git a/lib/features/profile/presentation/widgets/details_info_live_user.dart b/lib/features/profile/presentation/widgets/details_info_live_user.dart new file mode 100644 index 0000000..1362377 --- /dev/null +++ b/lib/features/profile/presentation/widgets/details_info_live_user.dart @@ -0,0 +1,212 @@ +// Flutter imports: +import 'package:flutter/material.dart'; + +// Package imports: +import 'package:flutter_linkify/flutter_linkify.dart'; +import 'package:flutter_phosphor_icons/flutter_phosphor_icons.dart'; + +// Project imports: +import 'package:streamskit_mobile/core/network/url_launcher_helper.dart'; +import 'package:streamskit_mobile/core/util/common/touchable_opacity.dart'; +import 'package:streamskit_mobile/core/util/custom_image/custom_netword_image.dart'; +import 'package:streamskit_mobile/core/util/sizer_custom/sizer.dart'; +import 'package:streamskit_mobile/core/util/styles/profile_style.dart'; +import 'package:streamskit_mobile/features/home/data/model/user_model.dart'; +import 'package:streamskit_mobile/features/profile/presentation/widgets/index_info_user.dart'; + +class DetailInfoLiveUserWidget extends StatefulWidget { + final UserModel user; + const DetailInfoLiveUserWidget({ + super.key, + required this.user, + }); + + @override + State createState() => + _DetailInfoLiveUserWidgetState(); +} + +class _DetailInfoLiveUserWidgetState extends State { + bool isFollowing = false; + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + SizedBox(height: 25.sp), + Stack( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + GestureDetector( + onTap: () {}, + child: CustomNetworkImage( + urlToImage: + "https://i.pinimg.com/originals/ca/33/57/ca335747b1f2b5b8611be00eb1307105.jpg", + shape: BoxShape.rectangle, + borderRadius: BorderRadius.circular(16.sp), + width: 150.w, + height: 150.sp, + ), + ), + Row( + children: [ + SizedBox( + width: 80.sp, + ), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: EdgeInsets.only( + top: 10.sp, + bottom: 2.sp, + ), + child: Row( + children: [ + Container( + constraints: BoxConstraints(maxWidth: 80.w), + child: Text( + widget.user.fullName, + style: text13mCL, + ), + ), + SizedBox(width: 2.sp), + Icon( + PhosphorIcons.circle_wavy_check_fill, + size: 13.sp, + color: Theme.of(context).primaryColor, + ), + ], + ), + ), + Text( + "@tonytonychopper", + style: text9mCL, + ), + ], + ), + const Spacer(), + TouchableOpacity( + onTap: () { + setState(() { + isFollowing = !isFollowing; + }); + }, + child: Container( + padding: EdgeInsets.symmetric( + horizontal: 8.sp, + vertical: 4.sp, + ), + decoration: BoxDecoration( + color: Colors.blueAccent, + shape: isFollowing + ? BoxShape.circle + : BoxShape.rectangle, + borderRadius: isFollowing + ? null + : BorderRadius.circular( + 15.sp, + ), + ), + child: isFollowing + ? const Icon(Icons.how_to_reg_outlined) + : Text( + "Follow", + style: text11mCL, + ), + ), + ), + ], + ), + SizedBox(height: 5.sp), + ], + ), + Positioned( + left: 5.sp, + bottom: 0.sp, + child: Container( + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all( + width: 8, + color: Colors.black, + ), + ), + child: CustomNetworkImage( + urlToImage: widget.user.urlToImage, + height: 60.sp, + width: 60.sp, + ), + ), + ), + ], + ), + SizedBox(height: 10.sp), + Padding( + padding: EdgeInsets.symmetric(horizontal: 16.0.sp), + child: Linkify( + onOpen: (link) async { + UrlLauncherHelper.launchUrlString(Uri.parse(link.url)); + }, + text: widget.user.description!, + maxLines: 3, + overflow: TextOverflow.ellipsis, + textAlign: TextAlign.center, + style: text9mCL, + linkStyle: TextStyle( + color: Theme.of(context).primaryColor, + decoration: TextDecoration.underline, + ), + ), + ), + SizedBox(height: 5.sp), + Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + IndexInfoUser( + titleIndex: "Posts", + numberIndex: widget.user.posts!, + ), + Container( + margin: EdgeInsets.symmetric(horizontal: 20.sp), + width: 1.sp, + height: 80.sp, + decoration: const BoxDecoration( + gradient: LinearGradient( + colors: [Colors.black26, Colors.white, Colors.black26], + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + ), + ), + ), + IndexInfoUser( + titleIndex: "Following", + numberIndex: widget.user.followings!, + ), + Container( + margin: EdgeInsets.symmetric(horizontal: 20.sp), + width: 0.8.sp, + height: 80.sp, + decoration: const BoxDecoration( + gradient: LinearGradient( + colors: [Colors.black26, Colors.white, Colors.black26], + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + ), + ), + ), + IndexInfoUser( + titleIndex: "Followers", + numberIndex: widget.user.followers!, + ), + ], + ), + SizedBox(height: 5.sp), + ], + ); + } +} diff --git a/lib/features/profile/presentation/widgets/edit_profile_widget.dart b/lib/features/profile/presentation/widgets/edit_profile_widget.dart new file mode 100644 index 0000000..89abff3 --- /dev/null +++ b/lib/features/profile/presentation/widgets/edit_profile_widget.dart @@ -0,0 +1,53 @@ +// Flutter imports: +import 'package:flutter/material.dart'; + +// Project imports: +import 'package:streamskit_mobile/core/util/common/touchable_opacity.dart'; +import 'package:streamskit_mobile/core/util/sizer_custom/sizer.dart'; +import 'package:streamskit_mobile/core/util/styles/profile_style.dart'; + +class ProfileEditWidget extends StatelessWidget { + final String title; + final String value; + final Function()? onTap; + final TextStyle? style; + const ProfileEditWidget({ + super.key, + required this.title, + required this.value, + this.style, + this.onTap, + }); + + @override + Widget build(BuildContext context) { + return TouchableOpacity( + onTap: onTap ?? () {}, + child: Container( + padding: EdgeInsets.symmetric(horizontal: 8.sp, vertical: 12.sp), + decoration: BoxDecoration( + color: Colors.grey.shade900, + ), + child: Row( + children: [ + Padding( + padding: EdgeInsets.only(left: 8.sp), + child: Text( + title, + style: text11mGB, + ), + ), + const Spacer(), + Container( + constraints: BoxConstraints(maxWidth: 150.sp), + child: Text( + value, + style: style ?? text11mCL, + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/features/profile/presentation/widgets/gribview_live_card.dart b/lib/features/profile/presentation/widgets/gribview_live_card.dart new file mode 100644 index 0000000..dc0d9ef --- /dev/null +++ b/lib/features/profile/presentation/widgets/gribview_live_card.dart @@ -0,0 +1,43 @@ +// Flutter imports: +import 'package:flutter/material.dart'; + +// Project imports: +import 'package:streamskit_mobile/core/util/sizer_custom/sizer.dart'; +import 'package:streamskit_mobile/features/profile/data/live_card_model.dart'; +import 'package:streamskit_mobile/features/profile/presentation/widgets/live_card_profile.dart'; + +class GridviewLiveCard extends StatelessWidget { + final List liveModel; + final int type; + final ScrollPhysics? physics; + final bool shrinkWrap; + final EdgeInsetsGeometry? padding; + const GridviewLiveCard({ + super.key, + required this.liveModel, + required this.type, + this.physics, + this.shrinkWrap = true, + this.padding, + }); + + @override + Widget build(BuildContext context) { + return Padding( + padding: padding ?? EdgeInsets.symmetric(horizontal: 16.sp), + child: GridView.count( + shrinkWrap: shrinkWrap, + padding: EdgeInsets.only(top: 8.sp, bottom: 72.sp), + crossAxisCount: 2, + crossAxisSpacing: 15.sp, + childAspectRatio: 0.7, + physics: physics ?? const NeverScrollableScrollPhysics(), + mainAxisSpacing: 20.sp, + children: List.generate( + liveModel.length, + (index) => LiveCardProflie(liveModel: liveModel[index]), + ), + ), + ); + } +} diff --git a/lib/features/profile/presentation/widgets/index_info_user.dart b/lib/features/profile/presentation/widgets/index_info_user.dart new file mode 100644 index 0000000..13020f6 --- /dev/null +++ b/lib/features/profile/presentation/widgets/index_info_user.dart @@ -0,0 +1,34 @@ +// Flutter imports: +import 'package:flutter/material.dart'; + +// Project imports: +import 'package:streamskit_mobile/core/util/numeral/numeral.dart'; +import 'package:streamskit_mobile/core/util/sizer_custom/sizer.dart'; +import 'package:streamskit_mobile/core/util/styles/profile_style.dart'; + +class IndexInfoUser extends StatelessWidget { + final String titleIndex; + final int numberIndex; + const IndexInfoUser({ + super.key, + required this.titleIndex, + required this.numberIndex, + }); + + @override + Widget build(BuildContext context) { + return Column( + children: [ + Text( + Numeral(numberIndex).format(), + style: text13w700mCL, + ), + SizedBox(height: 2.sp), + Text( + titleIndex, + style: text11mCL, + ), + ], + ); + } +} diff --git a/lib/features/profile/presentation/widgets/live_card_profile.dart b/lib/features/profile/presentation/widgets/live_card_profile.dart new file mode 100644 index 0000000..24a28ac --- /dev/null +++ b/lib/features/profile/presentation/widgets/live_card_profile.dart @@ -0,0 +1,122 @@ +// Dart imports: +import 'dart:ui'; + +// Flutter imports: +import 'package:flutter/material.dart'; + +// Project imports: +import 'package:streamskit_mobile/core/app/colors/app_color.dart'; +import 'package:streamskit_mobile/core/util/custom_image/custom_netword_image.dart'; +import 'package:streamskit_mobile/core/util/numeral/numeral.dart'; +import 'package:streamskit_mobile/core/util/sizer_custom/sizer.dart'; +import 'package:streamskit_mobile/core/util/styles/profile_style.dart'; +import 'package:streamskit_mobile/core/util/styles/style.dart'; +import 'package:streamskit_mobile/features/profile/data/live_card_model.dart'; + +class LiveCardProflie extends StatelessWidget { + final LiveCardModel liveModel; + final Function()? onTap; + const LiveCardProflie({ + super.key, + required this.liveModel, + this.onTap, + }); + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: onTap ?? () {}, + child: Stack( + children: [ + CustomNetworkImage( + urlToImage: liveModel.image, + shape: BoxShape.rectangle, + borderRadius: BorderRadius.circular(16.sp), + height: double.infinity, + ), + liveModel.statusLive + ? Positioned( + right: 5.sp, + top: 8.sp, + child: Container( + padding: EdgeInsets.symmetric( + horizontal: 8.sp, + vertical: 4.sp, + ), + decoration: BoxDecoration( + color: liveModel.getColorType, + borderRadius: BorderRadius.circular( + 9.sp, + ), + ), + child: Text( + liveModel.getTitleType, + style: text9mCL, + ), + ), + ) + : const SizedBox(), + liveModel.statusLive + ? Positioned( + left: 5.sp, + top: 8.sp, + child: ClipRRect( + borderRadius: BorderRadius.circular(10.sp), + child: BackdropFilter( + filter: ImageFilter.blur( + sigmaX: 4, + sigmaY: 4, + ), + child: Container( + padding: EdgeInsets.symmetric( + horizontal: 6.sp, + vertical: 4.sp, + ), + decoration: BoxDecoration( + color: mCH.withOpacity(0.45), + borderRadius: BorderRadius.circular(10.sp), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Image.asset( + iconEye, + height: 13.sp, + width: 13.sp, + fit: BoxFit.cover, + color: Theme.of(context).primaryColorLight, + ), + SizedBox(width: 5.sp), + Text( + Numeral(liveModel.numberViewer).format(), + style: text9mCL, + ) + ], + ), + ), + ), + ), + ) + : const SizedBox(), + Container( + alignment: Alignment.bottomLeft, + height: double.infinity, + width: double.infinity, + padding: EdgeInsets.only(bottom: 8.sp, left: 8.sp), + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [Colors.transparent, Colors.black.withOpacity(0.89)], + end: Alignment.bottomCenter, + begin: Alignment.topCenter, + ), + borderRadius: BorderRadius.circular( + 13.sp, + ), + ), + child: Text('You update Ep Ep', style: text11mCL), + ) + ], + ), + ); + } +} diff --git a/lib/features/profile/presentation/widgets/login_setting.dart b/lib/features/profile/presentation/widgets/login_setting.dart new file mode 100644 index 0000000..c6f0e6c --- /dev/null +++ b/lib/features/profile/presentation/widgets/login_setting.dart @@ -0,0 +1,55 @@ +// Flutter imports: +import 'package:flutter/material.dart'; + +// Package imports: +import 'package:flutter_phosphor_icons/flutter_phosphor_icons.dart'; + +// Project imports: +import 'package:streamskit_mobile/core/app/colors/app_color.dart'; +import 'package:streamskit_mobile/core/util/sizer_custom/sizer.dart'; +import 'package:streamskit_mobile/features/app/bloc/app_bloc.dart'; +import 'package:streamskit_mobile/features/auth/presentation/bloc/auth_bloc.dart'; +import 'package:streamskit_mobile/features/profile/presentation/widgets/row_icon_text.dart'; + +class LoginSetting extends StatelessWidget { + const LoginSetting({ + super.key, + }); + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "LOGIN", + style: TextStyle( + fontSize: 10.sp, + color: Colors.grey, + fontWeight: FontWeight.w700, + ), + ), + SizedBox(height: 8.sp), + RowIconText( + title: "Switch account", + colorLeading: mCL, + sizeLeading: 15.sp, + textStyle: TextStyle(color: mCL, fontSize: 11.sp), + iconLeading: PhosphorIcons.user_switch_thin, + onTap: () {}, + ), + SizedBox(height: 8.sp), + RowIconText( + title: "Log out", + colorLeading: mCL, + sizeLeading: 15.sp, + textStyle: TextStyle(color: mCL, fontSize: 11.sp), + iconLeading: PhosphorIcons.sign_out, + onTap: () { + AppBloc.authBloc.add(SignOutEvent()); + }, + ), + ], + ); + } +} diff --git a/lib/features/profile/presentation/widgets/row_icon_text.dart b/lib/features/profile/presentation/widgets/row_icon_text.dart new file mode 100644 index 0000000..a22b4f7 --- /dev/null +++ b/lib/features/profile/presentation/widgets/row_icon_text.dart @@ -0,0 +1,76 @@ +// Flutter imports: +import 'package:flutter/material.dart'; + +// Project imports: +import 'package:streamskit_mobile/core/util/common/touchable_opacity.dart'; +import 'package:streamskit_mobile/core/util/sizer_custom/sizer.dart'; +import 'package:streamskit_mobile/core/util/styles/profile_style.dart'; + +class RowIconText extends StatelessWidget { + final String title; + final IconData? iconLeading; + final Color? colorLeading; + final double? sizeLeading; + final IconData? iconSuffix; + final Color? colorSuffix; + final double? sizeSuffix; + final TextStyle? textStyle; + final Function()? onTap; + const RowIconText({ + super.key, + required this.title, + this.iconLeading, + this.onTap, + this.colorLeading, + this.iconSuffix, + this.colorSuffix, + this.sizeLeading, + this.sizeSuffix, + this.textStyle, + }); + + @override + Widget build(BuildContext context) { + return TouchableOpacity( + onTap: onTap ?? () {}, + child: Padding( + padding: EdgeInsets.symmetric(vertical: 8.sp), + child: iconLeading == null + ? Align( + alignment: Alignment.center, + child: Text( + title, + style: textStyle ?? text13mCL, + ), + ) + : Row( + children: [ + Icon( + iconLeading, + size: sizeLeading ?? 18.sp, + color: colorLeading, + ), + SizedBox( + width: 10.sp, + ), + Text( + title, + style: textStyle ?? text13mCL, + ), + const Spacer(), + iconSuffix != null + ? Padding( + padding: EdgeInsets.only(right: 8.0.sp), + child: Icon( + iconSuffix, + size: sizeSuffix ?? 9.sp, + color: colorSuffix, + ), + ) + : const SizedBox() + ], + ), + ), + ); + } +} diff --git a/lib/features/profile/presentation/widgets/text_form_field_profile.dart b/lib/features/profile/presentation/widgets/text_form_field_profile.dart new file mode 100644 index 0000000..5b47d74 --- /dev/null +++ b/lib/features/profile/presentation/widgets/text_form_field_profile.dart @@ -0,0 +1,87 @@ +// Flutter imports: +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +// Project imports: +import 'package:streamskit_mobile/core/app/colors/app_color.dart'; +import 'package:streamskit_mobile/core/util/sizer_custom/sizer.dart'; +import 'package:streamskit_mobile/core/util/styles/profile_style.dart'; + +class TextFormFieldProfile extends StatelessWidget { + final String hintText; + final IconData icon; + final double? iconSize; + final Function(String)? onChanged; + final Color? iconColor; + final Widget? suffixWidget; + final Widget? suffixIcon; + final bool isVisibility; + final int? maxLines; + final List? inputFormatters; + final TextEditingController controller; + final FocusNode? focusNode; + final String? Function(String?)? validator; + const TextFormFieldProfile({ + super.key, + required this.controller, + required this.hintText, + required this.icon, + this.iconSize, + this.iconColor, + this.suffixWidget, + this.inputFormatters, + this.suffixIcon, + this.isVisibility = true, + this.focusNode, + this.onChanged, + this.validator, + this.maxLines, + }); + + @override + Widget build(BuildContext context) { + return Padding( + padding: EdgeInsets.symmetric(horizontal: 16.0.sp), + child: Row( + children: [ + Icon( + icon, + size: iconSize ?? 18.sp, + color: iconColor ?? mCU, + ), + SizedBox(width: 16.sp), + Expanded( + child: TextFormField( + validator: validator, + onChanged: onChanged, + obscureText: !isVisibility, + focusNode: focusNode, + keyboardType: TextInputType.number, + inputFormatters: inputFormatters, + style: text11mCL, + cursorColor: colorPink, + controller: controller, + decoration: InputDecoration( + contentPadding: EdgeInsets.only(top: 10.sp, bottom: 10.sp), + suffix: suffixWidget, + suffixIcon: suffixIcon, + isDense: maxLines == 1, + enabledBorder: UnderlineInputBorder( + borderSide: BorderSide(color: mGD), + ), + focusedBorder: UnderlineInputBorder( + borderSide: BorderSide(color: mGD), + ), + border: UnderlineInputBorder( + borderSide: BorderSide(color: mGD), + ), + hintText: hintText, + hintStyle: text11mGM, + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/features/profile/presentation/widgets/text_form_filed_request.dart b/lib/features/profile/presentation/widgets/text_form_filed_request.dart new file mode 100644 index 0000000..8bf1aed --- /dev/null +++ b/lib/features/profile/presentation/widgets/text_form_filed_request.dart @@ -0,0 +1,104 @@ +// Flutter imports: +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +// Project imports: +import 'package:streamskit_mobile/core/app/colors/app_color.dart'; +import 'package:streamskit_mobile/core/util/sizer_custom/sizer.dart'; +import 'package:streamskit_mobile/core/util/styles/profile_style.dart'; + +class TextFieldFormRequest extends StatelessWidget { + final String? Function(String?)? validatorForm; + final void Function(String)? onChanged; + final String hintText; + final int maxLines; + final int? maxLength; + final bool isAvailable; + final bool isActive; + final TextStyle? textStyle; + final TextStyle? textStyleHint; + final TextEditingController? controller; + final List? inputFormatters; + final Widget? suffixIcon; + final TextInputType? textInputType; + final FocusNode? focusNode; + final Color? colorTextField; + final void Function()? onTap; + final double? height; + final AutovalidateMode? autovalidateMode; + final Color? underLine; + + const TextFieldFormRequest({ + super.key, + required this.validatorForm, + required this.hintText, + this.textInputType, + this.maxLines = 1, + this.maxLength, + this.onChanged, + this.isAvailable = true, + this.isActive = true, + this.controller, + this.suffixIcon, + this.inputFormatters, + this.focusNode, + this.colorTextField, + this.onTap, + this.height, + this.autovalidateMode, + this.textStyle, + this.textStyleHint, + this.underLine, + }); + @override + Widget build(BuildContext context) { + return Container( + margin: EdgeInsets.only( + top: maxLength != null ? 0.0 : 10.sp, + bottom: maxLength != null ? 4.sp : 0.0), + width: double.infinity, + child: TextFormField( + onTap: onTap ?? () {}, + validator: validatorForm, + focusNode: focusNode, + controller: controller, + style: textStyle ?? text11mCL, + cursorColor: colorPink, + keyboardType: textInputType ?? TextInputType.multiline, + onChanged: onChanged, + maxLines: maxLines == 1 ? null : maxLines, + inputFormatters: inputFormatters ?? + [ + LengthLimitingTextInputFormatter(maxLength), + ], + decoration: InputDecoration( + fillColor: mGM, + hintText: hintText, + hintStyle: textStyleHint ?? text11mGM, + isDense: maxLines == 1, + enabledBorder: UnderlineInputBorder( + borderSide: BorderSide(color: underLine ?? mGD), + ), + focusedBorder: UnderlineInputBorder( + borderSide: BorderSide(color: underLine ?? mGD), + ), + border: UnderlineInputBorder( + borderSide: BorderSide(color: underLine ?? mGD), + ), + contentPadding: maxLines == 1 + ? EdgeInsets.symmetric( + vertical: 11.sp, + horizontal: 10.sp, + ) + : EdgeInsets.symmetric( + vertical: 8.sp, + horizontal: 10.sp, + ), + suffix: suffixIcon, + ), + autovalidateMode: + autovalidateMode ?? AutovalidateMode.onUserInteraction, + ), + ); + } +} diff --git a/lib/features/search/data/.gitkeep b/lib/features/search/data/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/lib/features/search/data/top_search_model.dart b/lib/features/search/data/top_search_model.dart new file mode 100644 index 0000000..60d54bc --- /dev/null +++ b/lib/features/search/data/top_search_model.dart @@ -0,0 +1,55 @@ +// Dart imports: +import 'dart:convert'; + +class TopSearch { + String nameStreamer; + int stt; + TopSearch({ + required this.nameStreamer, + required this.stt, + }); + + TopSearch copyWith({ + String? nameStreamer, + int? stt, + }) { + return TopSearch( + nameStreamer: nameStreamer ?? this.nameStreamer, + stt: stt ?? this.stt, + ); + } + + Map toMap() { + return { + 'nameStreamer': nameStreamer, + 'stt': stt, + }; + } + + factory TopSearch.fromMap(Map map) { + return TopSearch( + nameStreamer: map['nameStreamer'] ?? '', + stt: map['stt']?.toInt() ?? 0, + ); + } + + String toJson() => json.encode(toMap()); + + factory TopSearch.fromJson(String source) => + TopSearch.fromMap(json.decode(source)); + + @override + String toString() => 'TopSearch(nameStreamer: $nameStreamer, stt: $stt)'; + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + + return other is TopSearch && + other.nameStreamer == nameStreamer && + other.stt == stt; + } + + @override + int get hashCode => nameStreamer.hashCode ^ stt.hashCode; +} diff --git a/lib/features/search/domain/.gitkeep b/lib/features/search/domain/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/lib/features/search/presentation/screens/search_screen.dart b/lib/features/search/presentation/screens/search_screen.dart new file mode 100644 index 0000000..aa10ae8 --- /dev/null +++ b/lib/features/search/presentation/screens/search_screen.dart @@ -0,0 +1,312 @@ +// Flutter imports: +import 'package:flutter/material.dart'; + +// Project imports: +import 'package:streamskit_mobile/core/app/colors/app_color.dart'; +import 'package:streamskit_mobile/core/util/common/touchable_opacity.dart'; +import 'package:streamskit_mobile/core/util/sizer_custom/sizer.dart'; +import 'package:streamskit_mobile/core/util/styles/profile_style.dart'; +import 'package:streamskit_mobile/core/util/styles/search_style.dart'; +import 'package:streamskit_mobile/features/profile/data/list_live_card_model.dart'; +import 'package:streamskit_mobile/features/profile/data/live_card_model.dart'; +import 'package:streamskit_mobile/features/profile/presentation/widgets/gribview_live_card.dart'; +import 'package:streamskit_mobile/features/search/data/top_search_model.dart'; +import 'package:streamskit_mobile/features/search/presentation/widgets/search_more_widget.dart'; + +class SearchScreen extends StatefulWidget { + const SearchScreen({super.key}); + + @override + State createState() => _SearchScreenState(); +} + +class _SearchScreenState extends State { + final TextEditingController _keySearch = TextEditingController(); + final ScrollController _scrollController = ScrollController(); + String keyword = ""; + + List listHistory = ["Ba rọi", "Mixi", "DCT", "Anime"]; + + List listMoreSearch = [ + TopSearch(stt: 1, nameStreamer: "Chopper"), + TopSearch(stt: 2, nameStreamer: "Thầy giáo ba"), + TopSearch(stt: 3, nameStreamer: "Mixi gaming"), + TopSearch(stt: 4, nameStreamer: "Trực tiếp game"), + TopSearch(stt: 5, nameStreamer: "MiGame"), + TopSearch(stt: 6, nameStreamer: "Man united"), + TopSearch(stt: 7, nameStreamer: "Tokyo revenger"), + TopSearch(stt: 8, nameStreamer: "Naruto"), + TopSearch(stt: 9, nameStreamer: "One Piece"), + TopSearch(stt: 10, nameStreamer: "Kimetsu no yaiba"), + ]; + + ListLiveCardModel listLiveStream = ListLiveCardModel( + type: 1, + listLiveCardModel: [ + LiveCardModel( + id: "", + idAccount: "", + categoryLive: 4, + image: + "https://cyberxanh.vn/wp-content/uploads/2021/11/top-20-mau-thiet-ke-phong-livestream-game-cuc-dinh-33-1038x800.jpg", + numberViewer: 950000001, + statusLive: true), + LiveCardModel( + id: "", + idAccount: "", + categoryLive: 4, + image: + "https://cyberxanh.vn/wp-content/uploads/2021/11/top-20-mau-thiet-ke-phong-livestream-game-cuc-dinh-33-1038x800.jpg", + numberViewer: 950000001, + statusLive: true), + LiveCardModel( + id: "", + idAccount: "", + categoryLive: 4, + image: + "https://cyberxanh.vn/wp-content/uploads/2021/11/top-20-mau-thiet-ke-phong-livestream-game-cuc-dinh-33-1038x800.jpg", + numberViewer: 950000001, + statusLive: true), + LiveCardModel( + id: "", + idAccount: "", + categoryLive: 4, + image: + "https://cyberxanh.vn/wp-content/uploads/2021/11/top-20-mau-thiet-ke-phong-livestream-game-cuc-dinh-33-1038x800.jpg", + numberViewer: 950000001, + statusLive: true), + LiveCardModel( + id: "", + idAccount: "", + categoryLive: 3, + image: + "https://st.nhipcaudautu.vn/staticFile/Subject/2019/01/03/livestream-game_31517638.jpg", + numberViewer: 78421, + statusLive: true), + LiveCardModel( + id: "", + idAccount: "", + categoryLive: 2, + image: + "https://photo-cms-tinnhanhchungkhoan.zadn.vn/w660/Uploaded/2022/gtnwae/2022_03_14/z-a-5520.jpg", + numberViewer: 78421, + statusLive: true), + LiveCardModel( + id: "", + idAccount: "", + categoryLive: 1, + image: + "https://ecdn.game4v.com/g4v-content/uploads/2021/05/stream-1.jpg", + numberViewer: 950000001, + statusLive: true) + ], + ); + + @override + void initState() { + _keySearch.text = ""; + keyword = ""; + super.initState(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Column( + children: [ + SizedBox( + height: 5.sp, + ), + SizedBox( + height: 30.sp, + child: TextFormField( + controller: _keySearch, + cursorColor: colorPink, + style: text9mCL, + onChanged: (val) { + setState(() { + keyword = val; + }); + }, + decoration: InputDecoration( + filled: true, + fillColor: mGE, + prefixIcon: TouchableOpacity( + child: Icon( + Icons.search, + size: 15.sp, + color: mGB, + ), + ), + suffixIcon: keyword.isNotEmpty + ? TouchableOpacity( + onTap: () { + setState(() { + _keySearch.clear(); + keyword = ""; + }); + }, + child: Icon( + Icons.cancel, + size: 15.sp, + color: mGB, + ), + ) + : const SizedBox(), + hintText: "Search streamer | Game", + hintStyle: text9mGB, + contentPadding: EdgeInsets.only(top: 5.sp), + enabledBorder: UnderlineInputBorder( + borderRadius: BorderRadius.circular(16.sp), + borderSide: const BorderSide(color: Colors.transparent), + ), + focusedBorder: UnderlineInputBorder( + borderRadius: BorderRadius.circular(16.sp), + borderSide: const BorderSide(color: Colors.transparent), + ), + border: UnderlineInputBorder( + borderRadius: BorderRadius.circular(16.sp), + borderSide: const BorderSide(color: Colors.transparent), + ), + ), + ), + ), + Divider(color: mGE, thickness: 1), + ], + ), + ), + body: NestedScrollView( + controller: _scrollController, + headerSliverBuilder: (context, value) { + return [ + SliverToBoxAdapter( + child: Padding( + padding: + EdgeInsets.symmetric(horizontal: 16.0.sp, vertical: 5.sp), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Text( + "History Search", + style: text11w700mGB, + ), + const Spacer(), + TouchableOpacity( + child: Icon( + Icons.delete_outline_rounded, + color: mCL, + size: 15, + ), + onTap: () { + setState(() { + listHistory.clear(); + }); + }, + ), + ], + ), + listHistory.isNotEmpty + ? Wrap( + spacing: 5.sp, + runSpacing: -5.sp, + children: List.generate( + listHistory.length, + (index) => TouchableOpacity( + onTap: () { + setState(() { + _keySearch.text = listHistory[index]; + keyword = listHistory[index]; + }); + }, + child: Chip( + backgroundColor: mGD, + label: Text( + listHistory[index], + style: text9mCL, + ), + ), + ), + ), + ) + : const SizedBox(), + ], + ), + ), + ), + SliverToBoxAdapter( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: + EdgeInsets.symmetric(horizontal: 16.sp, vertical: 5.sp), + child: Text( + 'Top Search', + style: text11w700mGB, + ), + ), + Container( + padding: EdgeInsets.symmetric( + horizontal: 16.sp, + ), + height: 160.sp, + width: double.infinity, + child: GridView.builder( + padding: EdgeInsets.zero, + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + childAspectRatio: 4.5, + crossAxisCount: 2, + crossAxisSpacing: 2.sp, + mainAxisSpacing: 1.sp, + ), + itemCount: 10, + itemBuilder: (BuildContext context, int index) => + SearchMoreWidget( + sttColor: listMoreSearch[index].stt == 1 + ? Colors.redAccent + : listMoreSearch[index].stt == 2 + ? Colors.orangeAccent + : listMoreSearch[index].stt == 3 + ? colorPink + : Colors.transparent, + stt: listMoreSearch[index].stt.toString(), + nameStreamer: listMoreSearch[index].nameStreamer, + ), + ), + ), + ], + ), + ), + ]; + }, + body: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: EdgeInsets.symmetric(horizontal: 16.sp), + child: Text( + 'Living', + style: text11w700mGB, + ), + ), + SizedBox( + height: 8.sp, + ), + GridviewLiveCard( + physics: const BouncingScrollPhysics(), + liveModel: listLiveStream.listLiveCardModel, + type: listLiveStream.type, + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/features/search/presentation/widgets/search_more_widget.dart b/lib/features/search/presentation/widgets/search_more_widget.dart new file mode 100644 index 0000000..d0f2944 --- /dev/null +++ b/lib/features/search/presentation/widgets/search_more_widget.dart @@ -0,0 +1,52 @@ +// Flutter imports: +import 'package:flutter/material.dart'; + +// Project imports: +import 'package:streamskit_mobile/core/app/colors/app_color.dart'; +import 'package:streamskit_mobile/core/util/sizer_custom/sizer.dart'; +import 'package:streamskit_mobile/core/util/styles/profile_style.dart'; + +class SearchMoreWidget extends StatelessWidget { + final String nameStreamer; + final String stt; + final Color sttColor; + const SearchMoreWidget({ + super.key, + required this.nameStreamer, + required this.stt, + required this.sttColor, + }); + + @override + Widget build(BuildContext context) { + return Row( + children: [ + Container( + width: 15.sp, + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [sttColor.withOpacity(0.6), sttColor], + stops: const [0.0, 1.0], + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + ), + shape: BoxShape.circle, + border: Border.all( + width: 0.5, + color: mCL, + ), + ), + child: Align( + alignment: Alignment.center, child: Text(stt, style: text9mCL)), + ), + SizedBox( + width: 8.sp, + ), + Text( + nameStreamer, + style: text11mCL, + ), + ], + ); + } +} diff --git a/lib/features/stream/data/models/message_model.dart b/lib/features/stream/data/models/message_model.dart new file mode 100644 index 0000000..2003a5d --- /dev/null +++ b/lib/features/stream/data/models/message_model.dart @@ -0,0 +1,101 @@ +// Dart imports: +import 'dart:convert'; + +class MessageModel { + final String imageUrl; + final String fullName; + final String message; + MessageModel({ + required this.imageUrl, + required this.fullName, + required this.message, + }); + + MessageModel copyWith({ + String? imageUrl, + String? fullName, + String? message, + }) { + return MessageModel( + imageUrl: imageUrl ?? this.imageUrl, + fullName: fullName ?? this.fullName, + message: message ?? this.message, + ); + } + + Map toMap() { + return { + 'imageUrl': imageUrl, + 'fullName': fullName, + 'message': message, + }; + } + + factory MessageModel.fromMap(Map map) { + return MessageModel( + imageUrl: map['imageUrl'] ?? '', + fullName: map['fullName'] ?? '', + message: map['message'] ?? '', + ); + } + + String toJson() => json.encode(toMap()); + + factory MessageModel.fromJson(String source) => + MessageModel.fromMap(json.decode(source)); + + @override + String toString() => + 'MessageModel(imageUrl: $imageUrl, fullName: $fullName, message: $message)'; + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + + return other is MessageModel && + other.imageUrl == imageUrl && + other.fullName == fullName && + other.message == message; + } + + @override + int get hashCode => imageUrl.hashCode ^ fullName.hashCode ^ message.hashCode; +} + +MessageModel pinMessageFake = MessageModel( + imageUrl: + 'https://my-test-11.slatic.net/p/96b9cce35f664d67479547587686742a.jpg', + fullName: 'Lord Busuz', + message: 'Các bạn xem stream vui vẻ'); +List listMessageFake = [ + MessageModel( + imageUrl: + 'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcRGdhbus9QU3FSl_cwnCX6tCcxpYN-Wj5NVLg&usqp=CAU', + fullName: 'Hà Anh Tuấn', + message: 'Hát gì đi bạn ei :>'), + MessageModel( + imageUrl: + 'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcSTywaXYb5-6bjevxgw_cD3bu0vcyW3J45g_w&usqp=CAU', + fullName: 'Tuấn 5 củ', + message: 'Liên minh ko em!!! :))'), + MessageModel( + imageUrl: + 'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcRFjg_69eVjIeli08uXE09Z2ddWue-GINy2qg&usqp=CAU', + fullName: 'Trung Ly Đeng', + message: 'Đấm nhau khum'), + MessageModel( + imageUrl: + 'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcSTywaXYb5-6bjevxgw_cD3bu0vcyW3J45g_w&usqp=CAU', + fullName: 'Tuấn 5 củ', + message: 'Liên minh ko em!!! :))'), + MessageModel( + imageUrl: + 'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcRFjg_69eVjIeli08uXE09Z2ddWue-GINy2qg&usqp=CAU', + fullName: 'Trung Ly Muội', + message: 'Đấm nhau khum'), + MessageModel( + imageUrl: + 'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcSTywaXYb5-6bjevxgw_cD3bu0vcyW3J45g_w&usqp=CAU', + fullName: 'Jack 5 củ', + message: 'Bỏ vợ là nghề của em!!!. mọi người cứ tin em'), +]; diff --git a/lib/features/stream/presentation/screens/stream_screen.dart b/lib/features/stream/presentation/screens/stream_screen.dart new file mode 100644 index 0000000..b19cb93 --- /dev/null +++ b/lib/features/stream/presentation/screens/stream_screen.dart @@ -0,0 +1,67 @@ +// Flutter imports: +// ignore_for_file: depend_on_referenced_packages + +// Flutter imports: +import 'package:flutter/material.dart'; + +// Package imports: +import 'package:provider/provider.dart'; + +// Project imports: +import 'package:streamskit_mobile/core/util/sizer_custom/sizer.dart'; +import 'package:streamskit_mobile/features/stream/presentation/widgets/app_bar_stream.dart'; +import 'package:streamskit_mobile/features/stream/presentation/widgets/comment_widget.dart'; +import 'package:streamskit_mobile/features/stream/provider/hearts_provider.dart'; + +class StreamScreen extends StatefulWidget { + const StreamScreen({super.key}); + + @override + State createState() => _StreamScreenState(); +} + +class _StreamScreenState extends State { + @override + Widget build(BuildContext context) { + return MultiProvider( + providers: [ + ChangeNotifierProvider(create: (context) => FloatingHeartsProvider()), + ], + child: Scaffold( + resizeToAvoidBottomInset: false, + body: Container( + width: double.infinity, + height: double.infinity, + color: Colors.purple.withOpacity(0.2), + child: Stack( + children: [ + Image.asset( + 'assets/images/stream_image.jpg', + fit: BoxFit.fitHeight, + height: double.infinity, + ), + Padding( + padding: + EdgeInsets.symmetric(vertical: 16.sp, horizontal: 16.sp), + child: const AppBarStream(), + ), + Padding( + padding: EdgeInsets.symmetric(horizontal: 20.sp) + .add(EdgeInsets.only(bottom: 10.sp)), + child: const Align( + alignment: Alignment.bottomCenter, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + CommentWidgets(), + ], + ), + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/features/stream/presentation/widgets/app_bar_stream.dart b/lib/features/stream/presentation/widgets/app_bar_stream.dart new file mode 100644 index 0000000..a098f8b --- /dev/null +++ b/lib/features/stream/presentation/widgets/app_bar_stream.dart @@ -0,0 +1,30 @@ +// Flutter imports: +import 'package:flutter/material.dart'; + +// Project imports: +import 'package:streamskit_mobile/core/util/sizer_custom/sizer.dart'; +import 'package:streamskit_mobile/features/stream/presentation/widgets/name_live_widget.dart'; +import 'package:streamskit_mobile/features/stream/presentation/widgets/viewer_widget.dart'; + +class AppBarStream extends StatelessWidget { + const AppBarStream({super.key}); + + @override + Widget build(BuildContext context) { + return Container( + padding: EdgeInsets.symmetric(vertical: 16.sp), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Expanded( + child: FullnameLiveWidget(), + ), + SizedBox( + width: 6.sp, + ), + const ViewerWidget(), + ], + ), + ); + } +} diff --git a/lib/features/stream/presentation/widgets/author_message_card.dart b/lib/features/stream/presentation/widgets/author_message_card.dart new file mode 100644 index 0000000..36d33d7 --- /dev/null +++ b/lib/features/stream/presentation/widgets/author_message_card.dart @@ -0,0 +1,89 @@ +// Flutter imports: +import 'package:flutter/material.dart'; + +// Package imports: +import 'package:phosphor_flutter/phosphor_flutter.dart'; + +// Project imports: +import 'package:streamskit_mobile/core/util/custom_image/custom_netword_image.dart'; +import 'package:streamskit_mobile/core/util/sizer_custom/sizer.dart'; +import 'package:streamskit_mobile/features/stream/data/models/message_model.dart'; + +class AuthorMessageCard extends StatefulWidget { + final MessageModel messageModel; + final bool pin; + const AuthorMessageCard( + {super.key, required this.messageModel, required this.pin}); + + @override + State createState() => _AuthorMessageCardState(); +} + +class _AuthorMessageCardState extends State { + @override + Widget build(BuildContext context) { + return Container( + padding: EdgeInsets.all(6.sp), + margin: EdgeInsets.only(bottom: 5.sp), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(100.sp), + color: Colors.white.withOpacity(0.10), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + CustomNetworkImage( + height: 32.sp, + width: 32.sp, + urlToImage: widget.messageModel.imageUrl, + shape: BoxShape.circle, + ), + SizedBox( + width: 10.sp, + ), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + widget.messageModel.fullName, + style: TextStyle( + fontSize: 10.sp, + fontWeight: FontWeight.w700, + color: Colors.white), + ), + SizedBox( + height: 2.sp, + ), + Text( + widget.messageModel.message, + maxLines: 2, + textAlign: TextAlign.justify, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: 10.sp, + fontWeight: FontWeight.w500, + color: Colors.white), + ), + ], + ), + ), + widget.pin + ? Container( + margin: EdgeInsets.only(left: 4.sp), + height: 30.sp, + width: 30.sp, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(100.sp), + color: Colors.grey.withOpacity(0.28), + ), + child: Icon( + PhosphorIcons.pushPinFill, + size: 16.0.sp, + )) + : const SizedBox(), + ], + ), + ); + } +} diff --git a/lib/features/stream/presentation/widgets/comment_widget.dart b/lib/features/stream/presentation/widgets/comment_widget.dart new file mode 100644 index 0000000..bd980c0 --- /dev/null +++ b/lib/features/stream/presentation/widgets/comment_widget.dart @@ -0,0 +1,168 @@ +// Flutter imports: +// ignore_for_file: depend_on_referenced_packages + +// Flutter imports: +import 'package:flutter/material.dart'; + +// Package imports: +import 'package:phosphor_flutter/phosphor_flutter.dart'; +import 'package:provider/provider.dart'; + +// Project imports: +import 'package:streamskit_mobile/core/util/common/touchable_opacity.dart'; +import 'package:streamskit_mobile/core/util/sizer_custom/sizer.dart'; +import 'package:streamskit_mobile/features/stream/data/models/message_model.dart'; +import 'package:streamskit_mobile/features/stream/presentation/widgets/author_message_card.dart'; +import 'package:streamskit_mobile/features/stream/presentation/widgets/floating_hearts.dart'; +import 'package:streamskit_mobile/features/stream/provider/hearts_provider.dart'; + +class CommentWidgets extends StatefulWidget { + const CommentWidgets({super.key}); + + @override + State createState() => _CommentWidgetsState(); +} + +class _CommentWidgetsState extends State { + @override + Widget build(BuildContext context) { + return SizedBox( + height: 350.sp, + child: Row( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Expanded( + child: Column( + children: [ + Expanded( + child: ShaderMask( + shaderCallback: (Rect bounds) { + return const LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + Colors.transparent, + Colors.white, + Colors.white, + ], + ).createShader(bounds); + }, + blendMode: BlendMode.dstIn, + child: Container( + padding: EdgeInsets.symmetric(vertical: 8.sp), + height: 280.sp, + child: ListView.builder( + physics: const BouncingScrollPhysics(), + itemCount: listMessageFake.length, + itemBuilder: (context, index) { + return AuthorMessageCard( + messageModel: listMessageFake[index], + pin: false, + ); + }, + ), + ), + ), + ), + AuthorMessageCard( + messageModel: pinMessageFake, + pin: true, + ), + SizedBox( + height: 6.sp, + ), + Row( + children: [ + Expanded( + child: Container( + padding: EdgeInsets.only(right: 4.sp), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(100.sp), + color: Colors.grey.withOpacity(0.28), + ), + child: TextField( + decoration: InputDecoration( + border: InputBorder.none, + hintStyle: + TextStyle(fontSize: 11.sp, color: Colors.grey), + hintText: 'Comment...', + contentPadding: EdgeInsets.all(12.sp), + suffixIcon: Container( + // padding: EdgeInsets.all(4.sp), + margin: EdgeInsets.all(4.sp), + width: 12.sp, + height: 12.sp, + decoration: BoxDecoration( + color: Colors.purple.shade400, + borderRadius: BorderRadius.circular(100.sp)), + child: Icon( + PhosphorIcons.paperPlaneTiltBold, + size: 16.0.sp, + color: Colors.white, + ), + ), + ), + ), + ), + ), + SizedBox( + width: 2.sp, + ), + TouchableOpacity( + onTap: () {}, + child: Container( + margin: EdgeInsets.only(left: 4.sp), + height: 40.sp, + width: 40.sp, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(100.sp), + color: Colors.grey.withOpacity(0.28), + ), + child: Icon( + Icons.share_outlined, + color: Colors.white, + size: 20.0.sp, + ), + ), + ), + ], + ), + ], + ), + ), + SizedBox( + width: 6.sp, + ), + Column( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + const Expanded( + child: FloatingHeartsWidget(), + ), + TouchableOpacity( + onTap: () { + FloatingHeartsProvider floatingHeartsProvider = + context.read(); + floatingHeartsProvider.addHeart(); + }, + child: Container( + height: 40.sp, + width: 40.sp, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(100.sp), + color: Colors.grey.withOpacity(0.28), + ), + child: Icon( + Icons.favorite, + color: Colors.red, + size: 24.0.sp, + ), + ), + ), + ], + ), + ], + ), + ); + } +} diff --git a/lib/features/stream/presentation/widgets/floating_hearts.dart b/lib/features/stream/presentation/widgets/floating_hearts.dart new file mode 100644 index 0000000..5d03ba9 --- /dev/null +++ b/lib/features/stream/presentation/widgets/floating_hearts.dart @@ -0,0 +1,32 @@ +// Flutter imports: +// ignore_for_file: depend_on_referenced_packages + +// Flutter imports: +import 'package:flutter/material.dart'; + +// Package imports: +import 'package:provider/provider.dart'; + +// Project imports: +import 'package:streamskit_mobile/core/util/sizer_custom/sizer.dart'; +import 'package:streamskit_mobile/features/stream/provider/hearts_provider.dart'; + +class FloatingHeartsWidget extends StatelessWidget { + const FloatingHeartsWidget({super.key}); + @override + Widget build(BuildContext context) { + FloatingHeartsProvider floatingHeartsProvider = + context.watch(); + + return Padding( + padding: EdgeInsets.only(bottom: 8.sp), + child: Align( + alignment: Alignment.bottomRight, + child: SizedBox( + width: 40.sp, + child: Stack(children: floatingHeartsProvider.hearts), + ), + ), + ); + } +} diff --git a/lib/features/stream/presentation/widgets/hearts_animation.dart b/lib/features/stream/presentation/widgets/hearts_animation.dart new file mode 100644 index 0000000..71eb285 --- /dev/null +++ b/lib/features/stream/presentation/widgets/hearts_animation.dart @@ -0,0 +1,81 @@ +// Dart imports: +// ignore_for_file: depend_on_referenced_packages + +// Dart imports: +import 'dart:math' as math; +import 'dart:ui'; + +// Flutter imports: +import 'package:flutter/material.dart'; + +// Package imports: +import 'package:provider/provider.dart'; + +// Project imports: +import 'package:streamskit_mobile/core/util/sizer_custom/sizer.dart'; +import 'package:streamskit_mobile/features/stream/provider/hearts_provider.dart'; + +class HeartAnimation extends StatefulWidget { + const HeartAnimation({super.key}); + + @override + HeartStateAnimation createState() => HeartStateAnimation(); +} + +class HeartStateAnimation extends State + with SingleTickerProviderStateMixin { + late AnimationController controller; + + final Color _color = Color((math.Random().nextDouble() * 0xFFFFFF).toInt()); + final double right = math.Random().nextInt(20).toDouble(); + final int randomSize = math.Random().nextInt(18); + double opacity = 0.7; + + @override + void dispose() { + controller.dispose(); + super.dispose(); + } + + @override + void initState() { + controller = AnimationController( + vsync: this, + duration: const Duration(seconds: 3), + ); + _startAnimation(controller); + super.initState(); + } + + void _startAnimation(AnimationController controller) { + controller.forward().whenComplete(() { + FloatingHeartsProvider floatingHeartsProvider = + context.read(); + floatingHeartsProvider.removeHeart(widget.key); + // controller.dispose(); + }); + } + + @override + Widget build(BuildContext context) { + Size size = Size(30.sp, 150.sp); + + return AnimatedBuilder( + animation: controller, + builder: (context, snapshot) { + final value = controller.value; + double? bottom = lerpDouble(10.sp, 30.sp + size.height * 0.25, value); + opacity = lerpDouble(0.7, 0, value)!; + + return Positioned( + bottom: bottom, + right: right, + child: Icon(Icons.favorite, + color: _color.withOpacity(opacity), + size: 20.sp //(size.width * 0.05 + randomSize).toDouble(), + ), + ); + }, + ); + } +} diff --git a/lib/features/stream/presentation/widgets/name_live_widget.dart b/lib/features/stream/presentation/widgets/name_live_widget.dart new file mode 100644 index 0000000..14e24a8 --- /dev/null +++ b/lib/features/stream/presentation/widgets/name_live_widget.dart @@ -0,0 +1,71 @@ +// Flutter imports: +import 'package:flutter/material.dart'; + +// Project imports: +import 'package:streamskit_mobile/core/util/common/touchable_opacity.dart'; +import 'package:streamskit_mobile/core/util/custom_image/custom_netword_image.dart'; +import 'package:streamskit_mobile/core/util/sizer_custom/sizer.dart'; + +class FullnameLiveWidget extends StatelessWidget { + const FullnameLiveWidget({super.key}); + + @override + Widget build(BuildContext context) { + return Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + CustomNetworkImage( + height: 32.sp, + width: 32.sp, + urlToImage: + 'https://my-test-11.slatic.net/p/96b9cce35f664d67479547587686742a.jpg', + shape: BoxShape.circle, + ), + SizedBox( + width: 4.sp, + ), + Expanded( + child: SizedBox( + height: 48.sp, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Lord Busuz', + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: 13.sp, + fontWeight: FontWeight.w700, + color: Colors.white), + ), + Text( + '159K Followers', + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: 10.sp, + fontWeight: FontWeight.w400, + color: Colors.white), + ), + ], + ), + ), + ), + TouchableOpacity( + child: Container( + margin: EdgeInsets.only(right: 8.sp), + padding: EdgeInsets.symmetric(horizontal: 8.sp, vertical: 4.8.sp), + decoration: BoxDecoration( + color: Colors.blue.shade700, + borderRadius: BorderRadius.circular(20.sp), + ), + child: Text( + 'Follow', + style: TextStyle(fontSize: 10.sp, color: Colors.white), + ), + ), + ), + ], + ); + } +} diff --git a/lib/features/stream/presentation/widgets/viewer_widget.dart b/lib/features/stream/presentation/widgets/viewer_widget.dart new file mode 100644 index 0000000..741bbac --- /dev/null +++ b/lib/features/stream/presentation/widgets/viewer_widget.dart @@ -0,0 +1,63 @@ +// Flutter imports: +import 'package:flutter/material.dart'; + +// Project imports: +import 'package:streamskit_mobile/core/util/numeral/numeral.dart'; +import 'package:streamskit_mobile/core/util/sizer_custom/sizer.dart'; + +class ViewerWidget extends StatefulWidget { + const ViewerWidget({super.key}); + + @override + State createState() => _ViewerWidgetState(); +} + +class _ViewerWidgetState extends State { + @override + Widget build(BuildContext context) { + return Container( + padding: EdgeInsets.symmetric(horizontal: 4.sp, vertical: 4), + height: 30.sp, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(100.sp), + color: Colors.white.withOpacity(0.25), + ), + child: Center( + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.remove_red_eye_outlined, + color: Colors.white, + size: 14.sp, + ), + SizedBox( + width: 2.sp, + ), + Text( + const Numeral(55411).format(), + style: TextStyle(fontSize: 11.sp, color: Colors.white), + ), + SizedBox( + width: 2.6.sp, + ), + Container( + height: 16.sp, + padding: EdgeInsets.symmetric(horizontal: 4.8.sp), + decoration: BoxDecoration( + color: Colors.red.shade700, + borderRadius: BorderRadius.circular(18.sp), + ), + child: Center( + child: Text( + 'Live', + style: TextStyle(fontSize: 10.sp, color: Colors.white), + ), + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/features/stream/provider/hearts_provider.dart b/lib/features/stream/provider/hearts_provider.dart new file mode 100644 index 0000000..0ae1223 --- /dev/null +++ b/lib/features/stream/provider/hearts_provider.dart @@ -0,0 +1,26 @@ +// Flutter imports: +import 'package:flutter/material.dart'; + +// Project imports: +import 'package:streamskit_mobile/features/stream/presentation/widgets/hearts_animation.dart'; + +class FloatingHeartsProvider with ChangeNotifier { + final List _hearts = []; + int _key = 0; + + List get hearts => _hearts; + + void addHeart() { + _hearts.addAll([ + HeartAnimation(key: Key((_key + 1).toString())), + HeartAnimation(key: Key((_key + 2).toString())), + HeartAnimation(key: Key((_key + 3).toString())) + ]); + _key += 3; + notifyListeners(); + } + + void removeHeart(Key? key) { + _hearts.clear(); + } +} diff --git a/lib/main.dart b/lib/main.dart index e016029..0850aec 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,115 +1,36 @@ -import 'package:flutter/material.dart'; - -void main() { - runApp(const MyApp()); -} - -class MyApp extends StatelessWidget { - const MyApp({super.key}); - - // This widget is the root of your application. - @override - Widget build(BuildContext context) { - return MaterialApp( - title: 'Flutter Demo', - theme: ThemeData( - // This is the theme of your application. - // - // Try running your application with "flutter run". You'll see the - // application has a blue toolbar. Then, without quitting the app, try - // changing the primarySwatch below to Colors.green and then invoke - // "hot reload" (press "r" in the console where you ran "flutter run", - // or simply save your changes to "hot reload" in a Flutter IDE). - // Notice that the counter didn't reset back to zero; the application - // is not restarted. - primarySwatch: Colors.blue, - ), - home: const MyHomePage(title: 'Flutter Demo Home Page'), - ); - } -} - -class MyHomePage extends StatefulWidget { - const MyHomePage({super.key, required this.title}); +// Dart imports: +import 'dart:async'; - // This widget is the home page of your application. It is stateful, meaning - // that it has a State object (defined below) that contains fields that affect - // how it looks. - - // This class is the configuration for the state. It holds the values (in this - // case the title) provided by the parent (in this case the App widget) and - // used by the build method of the State. Fields in a Widget subclass are - // always marked "final". - - final String title; - - @override - State createState() => _MyHomePageState(); -} - -class _MyHomePageState extends State { - int _counter = 0; - - void _incrementCounter() { - setState(() { - // This call to setState tells the Flutter framework that something has - // changed in this State, which causes it to rerun the build method below - // so that the display can reflect the updated values. If we changed - // _counter without calling setState(), then the build method would not be - // called again, and so nothing would appear to happen. - _counter++; - }); - } +// Flutter imports: +import 'package:flutter/material.dart'; - @override - Widget build(BuildContext context) { - // This method is rerun every time setState is called, for instance as done - // by the _incrementCounter method above. - // - // The Flutter framework has been optimized to make rerunning build methods - // fast, so that you can just rebuild anything that needs updating rather - // than having to individually change instances of widgets. - return Scaffold( - appBar: AppBar( - // Here we take the value from the MyHomePage object that was created by - // the App.build method, and use it to set our appbar title. - title: Text(widget.title), - ), - body: Center( - // Center is a layout widget. It takes a single child and positions it - // in the middle of the parent. - child: Column( - // Column is also a layout widget. It takes a list of children and - // arranges them vertically. By default, it sizes itself to fit its - // children horizontally, and tries to be as tall as its parent. - // - // Invoke "debug painting" (press "p" in the console, choose the - // "Toggle Debug Paint" action from the Flutter Inspector in Android - // Studio, or the "Toggle Debug Paint" command in Visual Studio Code) - // to see the wireframe for each widget. - // - // Column has various properties to control how it sizes itself and - // how it positions its children. Here we use mainAxisAlignment to - // center the children vertically; the main axis here is the vertical - // axis because Columns are vertical (the cross axis would be - // horizontal). - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Text( - 'You have pushed the button this many times:', - ), - Text( - '$_counter', - style: Theme.of(context).textTheme.headline4, - ), - ], - ), +// Package imports: +import 'package:firebase_core/firebase_core.dart'; +import 'package:firebase_crashlytics/firebase_crashlytics.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +// Project imports: +import 'package:streamskit_mobile/core/app/application.dart'; +import 'package:streamskit_mobile/features/app/app.dart'; +import 'package:streamskit_mobile/features/app/bloc/app_bloc.dart'; + +void main(List args) async { + await runZonedGuarded(() async { + WidgetsFlutterBinding.ensureInitialized(); + PaintingBinding.instance.imageCache.maximumSizeBytes = + 1024 * 1024 * 300; // 300 MB + + await Application.initialAppLication(); + + await Firebase.initializeApp(); + FlutterError.onError = FirebaseCrashlytics.instance.recordFlutterError; + runApp( + MultiBlocProvider( + providers: AppBloc.providers, + child: const App(), ), - floatingActionButton: FloatingActionButton( - onPressed: _incrementCounter, - tooltip: 'Increment', - child: const Icon(Icons.add), - ), // This trailing comma makes auto-formatting nicer for build methods. ); - } + }, (error, stackTrace) { + FirebaseCrashlytics.instance.recordError(error, stackTrace); + }); } diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index e71a16d..7299b5c 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -6,6 +6,14 @@ #include "generated_plugin_registrant.h" +#include +#include void fl_register_plugins(FlPluginRegistry* registry) { + g_autoptr(FlPluginRegistrar) file_selector_linux_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin"); + file_selector_plugin_register_with_registrar(file_selector_linux_registrar); + g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); + url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); } diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index 2e1de87..786ff5c 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -3,6 +3,8 @@ # list(APPEND FLUTTER_PLUGIN_LIST + file_selector_linux + url_launcher_linux ) list(APPEND FLUTTER_FFI_PLUGIN_LIST diff --git a/macos/Flutter/Flutter-Debug.xcconfig b/macos/Flutter/Flutter-Debug.xcconfig index c2efd0b..4b81f9b 100644 --- a/macos/Flutter/Flutter-Debug.xcconfig +++ b/macos/Flutter/Flutter-Debug.xcconfig @@ -1 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" #include "ephemeral/Flutter-Generated.xcconfig" diff --git a/macos/Flutter/Flutter-Release.xcconfig b/macos/Flutter/Flutter-Release.xcconfig index c2efd0b..5caa9d1 100644 --- a/macos/Flutter/Flutter-Release.xcconfig +++ b/macos/Flutter/Flutter-Release.xcconfig @@ -1 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" #include "ephemeral/Flutter-Generated.xcconfig" diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index cccf817..d28bb2e 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -5,6 +5,24 @@ import FlutterMacOS import Foundation +import file_selector_macos +import firebase_auth +import firebase_core +import firebase_crashlytics +import firebase_messaging +import path_provider_foundation +import sign_in_with_apple +import sqflite +import url_launcher_macos func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin")) + FLTFirebaseAuthPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseAuthPlugin")) + FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin")) + FLTFirebaseCrashlyticsPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCrashlyticsPlugin")) + FLTFirebaseMessagingPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseMessagingPlugin")) + PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) + SignInWithApplePlugin.register(with: registry.registrar(forPlugin: "SignInWithApplePlugin")) + SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin")) + UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) } diff --git a/macos/Podfile b/macos/Podfile new file mode 100644 index 0000000..dade8df --- /dev/null +++ b/macos/Podfile @@ -0,0 +1,40 @@ +platform :osx, '10.11' + +# 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', 'ephemeral', '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 Flutter-Generated.xcconfig, then run \"flutter pub get\"" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_macos_podfile_setup + +target 'Runner' do + use_frameworks! + use_modular_headers! + + flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__)) +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_macos_build_settings(target) + end +end diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png index 82b6f9d..9b4d8e8 100644 Binary files a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png index 13b35eb..ff96dae 100644 Binary files a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png index bdb5722..506c1d7 100644 Binary files a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png index 326c0e7..6a64e2c 100644 Binary files a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png index 2f1632c..6d95f52 100644 Binary files a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png differ diff --git a/pubspec.lock b/pubspec.lock index 4dccc06..71a58c8 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1,230 +1,1314 @@ # Generated by pub # See https://dart.dev/tools/pub/glossary#lockfile packages: + _fe_analyzer_shared: + dependency: transitive + description: + name: _fe_analyzer_shared + sha256: "0b2f2bd91ba804e53a61d757b986f89f1f9eaed5b11e4b2f5a2468d86d6c9fc7" + url: "https://pub.dev" + source: hosted + version: "67.0.0" + _flutterfire_internals: + dependency: transitive + description: + name: _flutterfire_internals + sha256: "2350805d7afefb0efe7acd325cb19d3ae8ba4039b906eade3807ffb69938a01f" + url: "https://pub.dev" + source: hosted + version: "1.3.33" + analyzer: + dependency: transitive + description: + name: analyzer + sha256: "37577842a27e4338429a1cbc32679d508836510b056f1eedf0c8d20e39c1383d" + url: "https://pub.dev" + source: hosted + version: "6.4.1" archive: dependency: transitive description: name: archive - url: "https://pub.dartlang.org" + sha256: "793964beb8e297995714326628881437d4211f10fc8843534bab54129cd896ee" + url: "https://pub.dev" source: hosted version: "3.3.1" args: dependency: transitive description: name: args - url: "https://pub.dartlang.org" + sha256: b003c3098049a51720352d219b0bb5f219b60fbfb68e7a4748139a06a5676515 + url: "https://pub.dev" source: hosted version: "2.3.1" async: dependency: transitive description: name: async - url: "https://pub.dartlang.org" + sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" + url: "https://pub.dev" source: hosted - version: "2.9.0" + version: "2.11.0" + bloc: + dependency: transitive + description: + name: bloc + sha256: bd4f8027bfa60d96c8046dec5ce74c463b2c918dce1b0d36593575995344534a + url: "https://pub.dev" + source: hosted + version: "8.1.0" boolean_selector: dependency: transitive description: name: boolean_selector - url: "https://pub.dartlang.org" + sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" + url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.1" + build: + dependency: transitive + description: + name: build + sha256: "80184af8b6cb3e5c1c4ec6d8544d27711700bc3e6d2efad04238c7b5290889f0" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + build_config: + dependency: transitive + description: + name: build_config + sha256: "5b7355c14258f5e7df24bad1566f7b991de3e54aeacfb94e1a65e5233d9739c1" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + build_daemon: + dependency: transitive + description: + name: build_daemon + sha256: "5f02d73eb2ba16483e693f80bee4f088563a820e47d1027d4cdfe62b5bb43e65" + url: "https://pub.dev" + source: hosted + version: "4.0.0" + build_resolvers: + dependency: transitive + description: + name: build_resolvers + sha256: "6c4dd11d05d056e76320b828a1db0fc01ccd376922526f8e9d6c796a5adbac20" + url: "https://pub.dev" + source: hosted + version: "2.2.1" + build_runner: + dependency: "direct dev" + description: + name: build_runner + sha256: "1414d6d733a85d8ad2f1dfcb3ea7945759e35a123cb99ccfac75d0758f75edfa" + url: "https://pub.dev" + source: hosted + version: "2.4.10" + build_runner_core: + dependency: transitive + description: + name: build_runner_core + sha256: "409c20ff6b6a9c9f4152fc9fcbf16440fedf02fcacc0fb26ea3b8eab9a860a40" + url: "https://pub.dev" + source: hosted + version: "7.2.4" + built_collection: + dependency: transitive + description: + name: built_collection + sha256: "376e3dd27b51ea877c28d525560790aee2e6fbb5f20e2f85d5081027d94e2100" + url: "https://pub.dev" + source: hosted + version: "5.1.1" + built_value: + dependency: transitive + description: + name: built_value + sha256: d7a9cd57c215bdf8d502772447aa6b52a8ab3f956d25d5fdea6ef1df2d2dad60 + url: "https://pub.dev" + source: hosted + version: "8.4.1" + cached_network_image: + dependency: "direct main" + description: + name: cached_network_image + sha256: "28ea9690a8207179c319965c13cd8df184d5ee721ae2ce60f398ced1219cea1f" + url: "https://pub.dev" + source: hosted + version: "3.3.1" + cached_network_image_platform_interface: + dependency: transitive + description: + name: cached_network_image_platform_interface + sha256: "9e90e78ae72caa874a323d78fa6301b3fb8fa7ea76a8f96dc5b5bf79f283bf2f" + url: "https://pub.dev" + source: hosted + version: "4.0.0" + cached_network_image_web: + dependency: transitive + description: + name: cached_network_image_web + sha256: "205d6a9f1862de34b93184f22b9d2d94586b2f05c581d546695e3d8f6a805cd7" + url: "https://pub.dev" + source: hosted + version: "1.2.0" characters: dependency: transitive description: name: characters - url: "https://pub.dartlang.org" + sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" + url: "https://pub.dev" source: hosted - version: "1.2.1" + version: "1.3.0" checked_yaml: dependency: transitive description: name: checked_yaml - url: "https://pub.dartlang.org" + sha256: dd007e4fb8270916820a0d66e24f619266b60773cddd082c6439341645af2659 + url: "https://pub.dev" source: hosted version: "2.0.1" cli_util: dependency: transitive description: name: cli_util - url: "https://pub.dartlang.org" + sha256: "66f86e916d285c1a93d3b79587d94bd71984a66aac4ff74e524cfa7877f1395c" + url: "https://pub.dev" source: hosted version: "0.3.5" clock: dependency: transitive description: name: clock - url: "https://pub.dartlang.org" + sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf + url: "https://pub.dev" source: hosted version: "1.1.1" + code_builder: + dependency: transitive + description: + name: code_builder + sha256: f692079e25e7869c14132d39f223f8eec9830eb76131925143b2129c4bb01b37 + url: "https://pub.dev" + source: hosted + version: "4.10.0" collection: dependency: transitive description: name: collection - url: "https://pub.dartlang.org" + sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a + url: "https://pub.dev" source: hosted - version: "1.16.0" - crypto: + version: "1.18.0" + convert: + dependency: transitive + description: + name: convert + sha256: "196284f26f69444b7f5c50692b55ec25da86d9e500451dc09333bf2e3ad69259" + url: "https://pub.dev" + source: hosted + version: "3.0.2" + cross_file: dependency: transitive + description: + name: cross_file + sha256: f71079978789bc2fe78d79227f1f8cfe195b31bbd8db2399b0d15a4b96fb843b + url: "https://pub.dev" + source: hosted + version: "0.3.3+2" + crypto: + dependency: "direct main" description: name: crypto - url: "https://pub.dartlang.org" + sha256: aa274aa7774f8964e4f4f38cc994db7b6158dd36e9187aaceaddc994b35c6c67 + url: "https://pub.dev" source: hosted version: "3.0.2" cupertino_icons: dependency: "direct main" description: name: cupertino_icons - url: "https://pub.dartlang.org" + sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6 + url: "https://pub.dev" source: hosted - version: "1.0.5" + version: "1.0.8" + dart_style: + dependency: transitive + description: + name: dart_style + sha256: "99e066ce75c89d6b29903d788a7bb9369cf754f7b24bf70bf4b6d6d6b26853b9" + url: "https://pub.dev" + source: hosted + version: "2.3.6" + dartz: + dependency: "direct main" + description: + name: dartz + sha256: e6acf34ad2e31b1eb00948692468c30ab48ac8250e0f0df661e29f12dd252168 + url: "https://pub.dev" + source: hosted + version: "0.10.1" + dio: + dependency: "direct main" + description: + name: dio + sha256: "3e5c4a94d112540d0c9a6b7f3969832e1604eb8cde0f88d0808382f9f632100b" + url: "https://pub.dev" + source: hosted + version: "5.0.3" + equatable: + dependency: "direct main" + description: + name: equatable + sha256: c2b87cb7756efdf69892005af546c56c0b5037f54d2a88269b4f347a505e3ca2 + url: "https://pub.dev" + source: hosted + version: "2.0.5" fake_async: dependency: transitive description: name: fake_async - url: "https://pub.dartlang.org" + sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" + url: "https://pub.dev" source: hosted version: "1.3.1" + ffi: + dependency: transitive + description: + name: ffi + sha256: a38574032c5f1dd06c4aee541789906c12ccaab8ba01446e800d9c5b79c4a978 + url: "https://pub.dev" + source: hosted + version: "2.0.1" + file: + dependency: transitive + description: + name: file + sha256: "1b92bec4fc2a72f59a8e15af5f52cd441e4a7860b49499d69dfa817af20e925d" + url: "https://pub.dev" + source: hosted + version: "6.1.4" + file_selector_linux: + dependency: transitive + description: + name: file_selector_linux + sha256: "045d372bf19b02aeb69cacf8b4009555fb5f6f0b7ad8016e5f46dd1387ddd492" + url: "https://pub.dev" + source: hosted + version: "0.9.2+1" + file_selector_macos: + dependency: transitive + description: + name: file_selector_macos + sha256: f42eacb83b318e183b1ae24eead1373ab1334084404c8c16e0354f9a3e55d385 + url: "https://pub.dev" + source: hosted + version: "0.9.4" + file_selector_platform_interface: + dependency: transitive + description: + name: file_selector_platform_interface + sha256: a3994c26f10378a039faa11de174d7b78eb8f79e4dd0af2a451410c1a5c3f66b + url: "https://pub.dev" + source: hosted + version: "2.6.2" + file_selector_windows: + dependency: transitive + description: + name: file_selector_windows + sha256: d3547240c20cabf205c7c7f01a50ecdbc413755814d6677f3cb366f04abcead0 + url: "https://pub.dev" + source: hosted + version: "0.9.3+1" + firebase_auth: + dependency: "direct main" + description: + name: firebase_auth + sha256: f0a75f61992d036e4c46ad0e9febd364d98aa2c092690a5475cb1421a8243cfe + url: "https://pub.dev" + source: hosted + version: "4.19.5" + firebase_auth_platform_interface: + dependency: transitive + description: + name: firebase_auth_platform_interface + sha256: feb77258404309ffc7761c78e1c0ad2ed5e4fdc378e035619e2cc13be4397b62 + url: "https://pub.dev" + source: hosted + version: "7.2.6" + firebase_auth_web: + dependency: transitive + description: + name: firebase_auth_web + sha256: "6d527f357da2bf93a67a42b423aa92943104a0c290d1d72ad9a42c779d501cd2" + url: "https://pub.dev" + source: hosted + version: "5.11.5" + firebase_core: + dependency: "direct main" + description: + name: firebase_core + sha256: "372d94ced114b9c40cb85e18c50ac94a7e998c8eec630c50d7aec047847d27bf" + url: "https://pub.dev" + source: hosted + version: "2.31.0" + firebase_core_platform_interface: + dependency: transitive + description: + name: firebase_core_platform_interface + sha256: c437ae5d17e6b5cc7981cf6fd458a5db4d12979905f9aafd1fea930428a9fe63 + url: "https://pub.dev" + source: hosted + version: "5.0.0" + firebase_core_web: + dependency: transitive + description: + name: firebase_core_web + sha256: "43d9e951ac52b87ae9cc38ecdcca1e8fa7b52a1dd26a96085ba41ce5108db8e9" + url: "https://pub.dev" + source: hosted + version: "2.17.0" + firebase_crashlytics: + dependency: "direct main" + description: + name: firebase_crashlytics + sha256: "8954a6493e05f73c87da1b15bab4970a5b325f138e254a0e1c18b4f97554720c" + url: "https://pub.dev" + source: hosted + version: "3.5.5" + firebase_crashlytics_platform_interface: + dependency: transitive + description: + name: firebase_crashlytics_platform_interface + sha256: "82f5e203cb0a83ccfce7d9b02de63c94e2f56fae96788df4b7446ca2cc81dac7" + url: "https://pub.dev" + source: hosted + version: "3.6.33" + firebase_messaging: + dependency: "direct main" + description: + name: firebase_messaging + sha256: e0882a7426821f7caccaabfc15a535155cd15b4daa73a5a7b3af701a552d73ab + url: "https://pub.dev" + source: hosted + version: "14.9.2" + firebase_messaging_platform_interface: + dependency: transitive + description: + name: firebase_messaging_platform_interface + sha256: "52e12cc50e1395ad7ea3552dcbe9958fb1994b5afcf58ee4c0db053932a6fce5" + url: "https://pub.dev" + source: hosted + version: "4.5.35" + firebase_messaging_web: + dependency: transitive + description: + name: firebase_messaging_web + sha256: "8812cc5929380b783f92290d934bf32e2fea06701583f47cdccd5f13f4f24522" + url: "https://pub.dev" + source: hosted + version: "3.8.5" + fixnum: + dependency: transitive + description: + name: fixnum + sha256: "04be3e934c52e082558cc9ee21f42f5c1cd7a1262f4c63cd0357c08d5bba81ec" + url: "https://pub.dev" + source: hosted + version: "1.0.1" flutter: dependency: "direct main" description: flutter source: sdk version: "0.0.0" - flutter_launcher_icons: + flutter_bloc: + dependency: "direct main" + description: + name: flutter_bloc + sha256: "890c51c8007f0182360e523518a0c732efb89876cb4669307af7efada5b55557" + url: "https://pub.dev" + source: hosted + version: "8.1.1" + flutter_cache_manager: + dependency: transitive + description: + name: flutter_cache_manager + sha256: "8207f27539deb83732fdda03e259349046a39a4c767269285f449ade355d54ba" + url: "https://pub.dev" + source: hosted + version: "3.3.1" + flutter_facebook_auth: dependency: "direct main" + description: + name: flutter_facebook_auth + sha256: "50dc3eef562acbe1e4cfad478053c9c16f9eaac49ad14ec48f00ed9dae1ba0cd" + url: "https://pub.dev" + source: hosted + version: "4.4.1+1" + flutter_facebook_auth_platform_interface: + dependency: transitive + description: + name: flutter_facebook_auth_platform_interface + sha256: "7950f5f8a6f2270c5d29af2a514733987db1191f70838fa777b282e47365f8c8" + url: "https://pub.dev" + source: hosted + version: "3.2.0" + flutter_facebook_auth_web: + dependency: transitive + description: + name: flutter_facebook_auth_web + sha256: "0f732e968c929a3c11a215ded802557576230ff0a0794c88941a8e92ff07b2eb" + url: "https://pub.dev" + source: hosted + version: "3.2.0" + flutter_launcher_icons: + dependency: "direct dev" description: name: flutter_launcher_icons - url: "https://pub.dartlang.org" + sha256: a9de6706cd844668beac27c0aed5910fa0534832b3c2cad61a5fd977fce82a5d + url: "https://pub.dev" source: hosted version: "0.10.0" + flutter_linkify: + dependency: "direct main" + description: + name: flutter_linkify + sha256: "74669e06a8f358fee4512b4320c0b80e51cffc496607931de68d28f099254073" + url: "https://pub.dev" + source: hosted + version: "6.0.0" + flutter_lints: + dependency: "direct dev" + description: + name: flutter_lints + sha256: "3f41d009ba7172d5ff9be5f6e6e6abb4300e263aab8866d2a0842ed2a70f8f0c" + url: "https://pub.dev" + source: hosted + version: "4.0.0" + flutter_phosphor_icons: + dependency: "direct main" + description: + name: flutter_phosphor_icons + sha256: "60a970f5fb5b22c93cea61e96c2cd922612dd2d4a359fa441e4d31c2e3b3c664" + url: "https://pub.dev" + source: hosted + version: "0.0.1+6" + flutter_plugin_android_lifecycle: + dependency: transitive + description: + name: flutter_plugin_android_lifecycle + sha256: "60fc7b78455b94e6de2333d2f95196d32cf5c22f4b0b0520a628804cb463503b" + url: "https://pub.dev" + source: hosted + version: "2.0.7" flutter_test: dependency: "direct dev" description: flutter source: sdk version: "0.0.0" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + frontend_server_client: + dependency: transitive + description: + name: frontend_server_client + sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694 + url: "https://pub.dev" + source: hosted + version: "4.0.0" + get_it: + dependency: "direct main" + description: + name: get_it + sha256: d85128a5dae4ea777324730dc65edd9c9f43155c109d5cc0a69cab74139fbac1 + url: "https://pub.dev" + source: hosted + version: "7.7.0" + glob: + dependency: transitive + description: + name: glob + sha256: c51b4fdfee4d281f49b8c957f1add91b815473597f76bcf07377987f66a55729 + url: "https://pub.dev" + source: hosted + version: "2.1.0" + google_sign_in: + dependency: "direct main" + description: + name: google_sign_in + sha256: d3e31052ce068d0fe2eb9975ee4409266a044592cf67737ec230be07343c6bdf + url: "https://pub.dev" + source: hosted + version: "5.4.2" + google_sign_in_android: + dependency: transitive + description: + name: google_sign_in_android + sha256: "25449f21c7c8c28520576cab82e58fbb33b11055ebf69189cf255e2611cac6da" + url: "https://pub.dev" + source: hosted + version: "6.1.1" + google_sign_in_ios: + dependency: transitive + description: + name: google_sign_in_ios + sha256: "673c0b07eb512ea7097df316a4706e6fe9ccef7c7c258761005e8aa420fe955f" + url: "https://pub.dev" + source: hosted + version: "5.5.0" + google_sign_in_platform_interface: + dependency: transitive + description: + name: google_sign_in_platform_interface + sha256: "61306213c76bb8170c3aa20017df296c0131c24d7f6c0cc7e2eeaeac34c9f457" + url: "https://pub.dev" + source: hosted + version: "2.3.0" + google_sign_in_web: + dependency: transitive + description: + name: google_sign_in_web + sha256: "5b35c221169a7b3e0fc15043ac09102ef542300ef92f2e1f05d5d8efde263af5" + url: "https://pub.dev" + source: hosted + version: "0.10.2" + graphs: + dependency: transitive + description: + name: graphs + sha256: aedc5a15e78fc65a6e23bcd927f24c64dd995062bcd1ca6eda65a3cff92a4d19 + url: "https://pub.dev" + source: hosted + version: "2.3.1" + hive: + dependency: "direct main" + description: + name: hive + sha256: "8dcf6db979d7933da8217edcec84e9df1bdb4e4edc7fc77dbd5aa74356d6d941" + url: "https://pub.dev" + source: hosted + version: "2.2.3" + http: + dependency: transitive + description: + name: http + sha256: "6aa2946395183537c8b880962d935877325d6a09a2867c3970c05c0fed6ac482" + url: "https://pub.dev" + source: hosted + version: "0.13.5" + http_multi_server: + dependency: transitive + description: + name: http_multi_server + sha256: "97486f20f9c2f7be8f514851703d0119c3596d14ea63227af6f7a481ef2b2f8b" + url: "https://pub.dev" + source: hosted + version: "3.2.1" + http_parser: + dependency: transitive + description: + name: http_parser + sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" + url: "https://pub.dev" + source: hosted + version: "4.0.2" image: dependency: transitive description: name: image - url: "https://pub.dartlang.org" + sha256: "9d2c5f73435a70a936d317769ee8e7ef480e37674b9f2bce95ea98969a307855" + url: "https://pub.dev" source: hosted version: "3.2.0" + image_picker: + dependency: "direct main" + description: + name: image_picker + sha256: "26222b01a0c9a2c8fe02fc90b8208bd3325da5ed1f4a2acabf75939031ac0bdd" + url: "https://pub.dev" + source: hosted + version: "1.0.7" + image_picker_android: + dependency: "direct main" + description: + name: image_picker_android + sha256: "42c098e7fb6334746be37cdc30369ade356ed4f14d48b7a0313f95a9159f4321" + url: "https://pub.dev" + source: hosted + version: "0.8.9+5" + image_picker_for_web: + dependency: transitive + description: + name: image_picker_for_web + sha256: e2423c53a68b579a7c37a1eda967b8ae536c3d98518e5db95ca1fe5719a730a3 + url: "https://pub.dev" + source: hosted + version: "3.0.2" + image_picker_ios: + dependency: transitive + description: + name: image_picker_ios + sha256: fadafce49e8569257a0cad56d24438a6fa1f0cbd7ee0af9b631f7492818a4ca3 + url: "https://pub.dev" + source: hosted + version: "0.8.9+1" + image_picker_linux: + dependency: transitive + description: + name: image_picker_linux + sha256: "4ed1d9bb36f7cd60aa6e6cd479779cc56a4cb4e4de8f49d487b1aaad831300fa" + url: "https://pub.dev" + source: hosted + version: "0.2.1+1" + image_picker_macos: + dependency: transitive + description: + name: image_picker_macos + sha256: "3f5ad1e8112a9a6111c46d0b57a7be2286a9a07fc6e1976fdf5be2bd31d4ff62" + url: "https://pub.dev" + source: hosted + version: "0.2.1+1" + image_picker_platform_interface: + dependency: "direct main" + description: + name: image_picker_platform_interface + sha256: "9ec26d410ff46f483c5519c29c02ef0e02e13a543f882b152d4bfd2f06802f80" + url: "https://pub.dev" + source: hosted + version: "2.10.0" + image_picker_windows: + dependency: transitive + description: + name: image_picker_windows + sha256: "6ad07afc4eb1bc25f3a01084d28520496c4a3bb0cb13685435838167c9dcedeb" + url: "https://pub.dev" + source: hosted + version: "0.2.1+1" + import_sorter: + dependency: "direct dev" + description: + name: import_sorter + sha256: eb15738ccead84e62c31e0208ea4e3104415efcd4972b86906ca64a1187d0836 + url: "https://pub.dev" + source: hosted + version: "4.6.0" + injectable: + dependency: "direct main" + description: + name: injectable + sha256: "3c8355a29d11ff28c0311bed754649761f345ef7a13ff66a714380954af51226" + url: "https://pub.dev" + source: hosted + version: "2.4.2" + injectable_generator: + dependency: "direct dev" + description: + name: injectable_generator + sha256: "2ca3ada337eac0ef6b82f8049c970ddb63947738fdf32ac6cbef8d1567d7ba05" + url: "https://pub.dev" + source: hosted + version: "2.6.1" + intl: + dependency: "direct main" + description: + name: intl + sha256: "910f85bce16fb5c6f614e117efa303e85a1731bb0081edf3604a2ae6e9a3cc91" + url: "https://pub.dev" + source: hosted + version: "0.17.0" + io: + dependency: transitive + description: + name: io + sha256: "0d4c73c3653ab85bf696d51a9657604c900a370549196a91f33e4c39af760852" + url: "https://pub.dev" + source: hosted + version: "1.0.3" + js: + dependency: transitive + description: + name: js + sha256: "5528c2f391ededb7775ec1daa69e65a2d61276f7552de2b5f7b8d34ee9fd4ab7" + url: "https://pub.dev" + source: hosted + version: "0.6.5" json_annotation: dependency: transitive description: name: json_annotation - url: "https://pub.dartlang.org" + sha256: "3520fa844009431b5d4491a5a778603520cdc399ab3406332dcc50f93547258c" + url: "https://pub.dev" source: hosted version: "4.7.0" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: "7f0df31977cb2c0b88585095d168e689669a2cc9b97c309665e3386f3e9d341a" + url: "https://pub.dev" + source: hosted + version: "10.0.4" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: "06e98f569d004c1315b991ded39924b21af84cf14cc94791b8aea337d25b57f8" + url: "https://pub.dev" + source: hosted + version: "3.0.3" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" + url: "https://pub.dev" + source: hosted + version: "3.0.1" + linkify: + dependency: transitive + description: + name: linkify + sha256: "4139ea77f4651ab9c315b577da2dd108d9aa0bd84b5d03d33323f1970c645832" + url: "https://pub.dev" + source: hosted + version: "5.0.0" + lints: + dependency: transitive + description: + name: lints + sha256: "976c774dd944a42e83e2467f4cc670daef7eed6295b10b36ae8c85bcbf828235" + url: "https://pub.dev" + source: hosted + version: "4.0.0" + logging: + dependency: transitive + description: + name: logging + sha256: c0bbfe94d46aedf9b8b3e695cf3bd48c8e14b35e3b2c639e0aa7755d589ba946 + url: "https://pub.dev" + source: hosted + version: "1.1.0" matcher: dependency: transitive description: name: matcher - url: "https://pub.dartlang.org" + sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb + url: "https://pub.dev" source: hosted - version: "0.12.12" + version: "0.12.16+1" material_color_utilities: dependency: transitive description: name: material_color_utilities - url: "https://pub.dartlang.org" + sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a" + url: "https://pub.dev" source: hosted - version: "0.1.5" + version: "0.8.0" meta: dependency: transitive description: name: meta - url: "https://pub.dartlang.org" + sha256: "7687075e408b093f36e6bbf6c91878cc0d4cd10f409506f7bc996f68220b9136" + url: "https://pub.dev" + source: hosted + version: "1.12.0" + mime: + dependency: transitive + description: + name: mime + sha256: "2e123074287cc9fd6c09de8336dae606d1ddb88d9ac47358826db698c176a1f2" + url: "https://pub.dev" + source: hosted + version: "1.0.5" + mockito: + dependency: "direct dev" + description: + name: mockito + sha256: "6841eed20a7befac0ce07df8116c8b8233ed1f4486a7647c7fc5a02ae6163917" + url: "https://pub.dev" + source: hosted + version: "5.4.4" + nested: + dependency: transitive + description: + name: nested + sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20" + url: "https://pub.dev" + source: hosted + version: "1.0.0" + octo_image: + dependency: transitive + description: + name: octo_image + sha256: "45b40f99622f11901238e18d48f5f12ea36426d8eced9f4cbf58479c7aa2430d" + url: "https://pub.dev" + source: hosted + version: "2.0.0" + package_config: + dependency: transitive + description: + name: package_config + sha256: "1c5b77ccc91e4823a5af61ee74e6b972db1ef98c2ff5a18d3161c982a55448bd" + url: "https://pub.dev" source: hosted - version: "1.8.0" + version: "2.1.0" path: dependency: transitive description: name: path - url: "https://pub.dartlang.org" + sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" + url: "https://pub.dev" + source: hosted + version: "1.9.0" + path_provider: + dependency: "direct main" + description: + name: path_provider + sha256: b27217933eeeba8ff24845c34003b003b2b22151de3c908d0e679e8fe1aa078b + url: "https://pub.dev" + source: hosted + version: "2.1.2" + path_provider_android: + dependency: transitive + description: + name: path_provider_android + sha256: a248d8146ee5983446bf03ed5ea8f6533129a12b11f12057ad1b4a67a2b3b41d + url: "https://pub.dev" + source: hosted + version: "2.2.4" + path_provider_foundation: + dependency: transitive + description: + name: path_provider_foundation + sha256: f234384a3fdd67f989b4d54a5d73ca2a6c422fa55ae694381ae0f4375cd1ea16 + url: "https://pub.dev" + source: hosted + version: "2.4.0" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 + url: "https://pub.dev" + source: hosted + version: "2.2.1" + path_provider_platform_interface: + dependency: transitive + description: + name: path_provider_platform_interface + sha256: "94b1e0dd80970c1ce43d5d4e050a9918fce4f4a775e6142424c30a29a363265c" + url: "https://pub.dev" + source: hosted + version: "2.1.1" + path_provider_windows: + dependency: transitive + description: + name: path_provider_windows + sha256: "8bc9f22eee8690981c22aa7fc602f5c85b497a6fb2ceb35ee5a5e5ed85ad8170" + url: "https://pub.dev" source: hosted - version: "1.8.2" + version: "2.2.1" petitparser: dependency: transitive description: name: petitparser - url: "https://pub.dartlang.org" + sha256: "2ebb289dc4764ec397f5cd3ca9881c6d17196130a7d646ed022a0dd9c2e25a71" + url: "https://pub.dev" source: hosted version: "5.0.0" + phosphor_flutter: + dependency: "direct main" + description: + name: phosphor_flutter + sha256: "82aee69109d55518abbc5d77aeb893bfd2d39796aa51b871cadf2e56d03fa8e3" + url: "https://pub.dev" + source: hosted + version: "1.4.0" + platform: + dependency: transitive + description: + name: platform + sha256: "4a451831508d7d6ca779f7ac6e212b4023dd5a7d08a27a63da33756410e32b76" + url: "https://pub.dev" + source: hosted + version: "3.1.0" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" + url: "https://pub.dev" + source: hosted + version: "2.1.8" + pool: + dependency: transitive + description: + name: pool + sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a" + url: "https://pub.dev" + source: hosted + version: "1.5.1" + process: + dependency: transitive + description: + name: process + sha256: "53fd8db9cec1d37b0574e12f07520d582019cb6c44abf5479a01505099a34a09" + url: "https://pub.dev" + source: hosted + version: "4.2.4" + provider: + dependency: transitive + description: + name: provider + sha256: e1e7413d70444ea3096815a60fe5da1b11bda8a9dc4769252cc82c53536f8bcc + url: "https://pub.dev" + source: hosted + version: "6.0.4" + pub_semver: + dependency: transitive + description: + name: pub_semver + sha256: "40d3ab1bbd474c4c2328c91e3a7df8c6dd629b79ece4c4bd04bee496a224fb0c" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + pubspec_parse: + dependency: transitive + description: + name: pubspec_parse + sha256: "75f6614d6dde2dc68948dffbaa4fe5dae32cd700eb9fb763fe11dfb45a3c4d0a" + url: "https://pub.dev" + source: hosted + version: "1.2.1" + quiver: + dependency: transitive + description: + name: quiver + sha256: "93982981971e812c94d4a6fa3a57b89f9ec12b38b6380cd3c1370c3b01e4580e" + url: "https://pub.dev" + source: hosted + version: "3.1.0" + recase: + dependency: transitive + description: + name: recase + sha256: e4eb4ec2dcdee52dcf99cb4ceabaffc631d7424ee55e56f280bc039737f89213 + url: "https://pub.dev" + source: hosted + version: "4.1.0" + rxdart: + dependency: transitive + description: + name: rxdart + sha256: "5d22055fd443806c03ef24a02000637cf51eae49c2a0168d38a43fc166b0209c" + url: "https://pub.dev" + source: hosted + version: "0.27.5" + shelf: + dependency: transitive + description: + name: shelf + sha256: c24a96135a2ccd62c64b69315a14adc5c3419df63b4d7c05832a346fdb73682c + url: "https://pub.dev" + source: hosted + version: "1.4.0" + shelf_web_socket: + dependency: transitive + description: + name: shelf_web_socket + sha256: "6db16374bc3497d21aa0eebe674d3db9fdf82082aac0f04dc7b44e4af5b08afc" + url: "https://pub.dev" + source: hosted + version: "1.0.2" + sign_in_with_apple: + dependency: "direct main" + description: + name: sign_in_with_apple + sha256: "425cf29f6c8730314d08d62a3193319b84f40940f1f6f0d9cea703aaf3205a03" + url: "https://pub.dev" + source: hosted + version: "4.1.0" + sign_in_with_apple_platform_interface: + dependency: transitive + description: + name: sign_in_with_apple_platform_interface + sha256: a5883edee09ed6be19de19e7d9f618a617fe41a6fa03f76d082dfb787e9ea18d + url: "https://pub.dev" + source: hosted + version: "1.0.0" + sign_in_with_apple_web: + dependency: transitive + description: + name: sign_in_with_apple_web + sha256: "44b66528f576e77847c14999d5e881e17e7223b7b0625a185417829e5306f47a" + url: "https://pub.dev" + source: hosted + version: "1.0.1" sky_engine: dependency: transitive description: flutter source: sdk version: "0.0.99" + source_gen: + dependency: transitive + description: + name: source_gen + sha256: "14658ba5f669685cd3d63701d01b31ea748310f7ab854e471962670abcf57832" + url: "https://pub.dev" + source: hosted + version: "1.5.0" source_span: dependency: transitive description: name: source_span - url: "https://pub.dartlang.org" + sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" + url: "https://pub.dev" source: hosted - version: "1.9.0" + version: "1.10.0" + sqflite: + dependency: transitive + description: + name: sqflite + sha256: a9016f495c927cb90557c909ff26a6d92d9bd54fc42ba92e19d4e79d61e798c6 + url: "https://pub.dev" + source: hosted + version: "2.3.2" + sqflite_common: + dependency: transitive + description: + name: sqflite_common + sha256: "28d8c66baee4968519fb8bd6cdbedad982d6e53359091f0b74544a9f32ec72d5" + url: "https://pub.dev" + source: hosted + version: "2.5.3" stack_trace: dependency: transitive description: name: stack_trace - url: "https://pub.dartlang.org" + sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" + url: "https://pub.dev" source: hosted - version: "1.10.0" + version: "1.11.1" stream_channel: dependency: transitive description: name: stream_channel - url: "https://pub.dartlang.org" + sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 + url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.2" + stream_transform: + dependency: transitive + description: + name: stream_transform + sha256: f1d172e22a5432c042b5adfa9aff621372e4292231d9d73ad00f486ad01c2395 + url: "https://pub.dev" + source: hosted + version: "2.0.1" string_scanner: dependency: transitive description: name: string_scanner - url: "https://pub.dartlang.org" + sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" + url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "1.2.0" + synchronized: + dependency: transitive + description: + name: synchronized + sha256: "7b530acd9cb7c71b0019a1e7fa22c4105e675557a4400b6a401c71c5e0ade1ac" + url: "https://pub.dev" + source: hosted + version: "3.0.0+3" term_glyph: dependency: transitive description: name: term_glyph - url: "https://pub.dartlang.org" + sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 + url: "https://pub.dev" source: hosted version: "1.2.1" test_api: dependency: transitive description: name: test_api - url: "https://pub.dartlang.org" + sha256: "9955ae474176f7ac8ee4e989dadfb411a58c30415bcfb648fa04b2b8a03afa7f" + url: "https://pub.dev" + source: hosted + version: "0.7.0" + timing: + dependency: transitive + description: + name: timing + sha256: c386d07d7f5efc613479a7c4d9d64b03710b03cfaa7e8ad5f2bfb295a1f0dfad + url: "https://pub.dev" + source: hosted + version: "1.0.0" + tint: + dependency: transitive + description: + name: tint + sha256: d856019547532d4ea24171f554b319081c004c37741e7946eae30cb09f24e1c7 + url: "https://pub.dev" source: hosted - version: "0.4.12" + version: "2.0.0" typed_data: dependency: transitive description: name: typed_data - url: "https://pub.dartlang.org" + sha256: "26f87ade979c47a150c9eaab93ccd2bebe70a27dc0b4b29517f2904f04eb11a5" + url: "https://pub.dev" source: hosted version: "1.3.1" + url_launcher: + dependency: "direct main" + description: + name: url_launcher + sha256: "6ce1e04375be4eed30548f10a315826fd933c1e493206eab82eed01f438c8d2e" + url: "https://pub.dev" + source: hosted + version: "6.2.6" + url_launcher_android: + dependency: transitive + description: + name: url_launcher_android + sha256: "507dc655b1d9cb5ebc756032eb785f114e415f91557b73bf60b7e201dfedeb2f" + url: "https://pub.dev" + source: hosted + version: "6.2.2" + url_launcher_ios: + dependency: transitive + description: + name: url_launcher_ios + sha256: "7068716403343f6ba4969b4173cbf3b84fc768042124bc2c011e5d782b24fe89" + url: "https://pub.dev" + source: hosted + version: "6.3.0" + url_launcher_linux: + dependency: transitive + description: + name: url_launcher_linux + sha256: ab360eb661f8879369acac07b6bb3ff09d9471155357da8443fd5d3cf7363811 + url: "https://pub.dev" + source: hosted + version: "3.1.1" + url_launcher_macos: + dependency: transitive + description: + name: url_launcher_macos + sha256: "9a1a42d5d2d95400c795b2914c36fdcb525870c752569438e4ebb09a2b5d90de" + url: "https://pub.dev" + source: hosted + version: "3.2.0" + url_launcher_platform_interface: + dependency: transitive + description: + name: url_launcher_platform_interface + sha256: "4aca1e060978e19b2998ee28503f40b5ba6226819c2b5e3e4d1821e8ccd92198" + url: "https://pub.dev" + source: hosted + version: "2.3.0" + url_launcher_web: + dependency: transitive + description: + name: url_launcher_web + sha256: "8d9e750d8c9338601e709cd0885f95825086bd8b642547f26bda435aade95d8a" + url: "https://pub.dev" + source: hosted + version: "2.3.1" + url_launcher_windows: + dependency: transitive + description: + name: url_launcher_windows + sha256: ecf9725510600aa2bb6d7ddabe16357691b6d2805f66216a97d1b881e21beff7 + url: "https://pub.dev" + source: hosted + version: "3.1.1" + uuid: + dependency: transitive + description: + name: uuid + sha256: "2469694ad079893e3b434a627970c33f2fa5adc46dfe03c9617546969a9a8afc" + url: "https://pub.dev" + source: hosted + version: "3.0.6" vector_math: dependency: transitive description: name: vector_math - url: "https://pub.dartlang.org" + sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" + url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.4" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: "3923c89304b715fb1eb6423f017651664a03bf5f4b29983627c4da791f74a4ec" + url: "https://pub.dev" + source: hosted + version: "14.2.1" + watcher: + dependency: transitive + description: + name: watcher + sha256: "3d2ad6751b3c16cf07c7fca317a1413b3f26530319181b37e3b9039b84fc01d8" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + web: + dependency: transitive + description: + name: web + sha256: "97da13628db363c635202ad97068d47c5b8aa555808e7a9411963c533b449b27" + url: "https://pub.dev" + source: hosted + version: "0.5.1" + web_socket_channel: + dependency: transitive + description: + name: web_socket_channel + sha256: "3a969ddcc204a3e34e863d204b29c0752716f78b6f9cc8235083208d268a4ccd" + url: "https://pub.dev" + source: hosted + version: "2.2.0" + win32: + dependency: transitive + description: + name: win32 + sha256: "1952a663c0e34fbde55916010d54bbb249bf5f2583113c497602f0ee01c6faa4" + url: "https://pub.dev" + source: hosted + version: "3.0.0" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + sha256: "11541eedefbcaec9de35aa82650b695297ce668662bbd6e3911a7fabdbde589f" + url: "https://pub.dev" + source: hosted + version: "0.2.0+2" xml: dependency: transitive description: name: xml - url: "https://pub.dartlang.org" + sha256: ac0e3f4bf00ba2708c33fbabbbe766300e509f8c82dbd4ab6525039813f7e2fb + url: "https://pub.dev" source: hosted version: "6.1.0" yaml: dependency: transitive description: name: yaml - url: "https://pub.dartlang.org" + sha256: "23812a9b125b48d4007117254bca50abb6c712352927eece9e155207b1db2370" + url: "https://pub.dev" source: hosted version: "3.1.1" sdks: - dart: ">=2.18.1 <3.0.0" + dart: ">=3.4.0 <4.0.0" + flutter: ">=3.19.0" diff --git a/pubspec.yaml b/pubspec.yaml index 1784fd3..55cb571 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,9 +1,9 @@ name: streamskit_mobile -description: A new Flutter project. +description: Live Studio - Open Source # The following line prevents the package from being accidentally published to # pub.dev using `flutter pub publish`. This is preferred for private packages. -publish_to: 'none' # Remove this line if you wish to publish to pub.dev +publish_to: "none" # Remove this line if you wish to publish to pub.dev # The following defines the version and build number for your application. # A version number is three numbers separated by dots, like 1.2.43 @@ -20,7 +20,8 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev version: 1.0.0+1 environment: - sdk: '>=2.18.1 <3.0.0' + sdk: '>=3.1.5 <4.0.0' + flutter: ">=3.19.0" # Dependencies specify other packages that your package needs in order to work. # To automatically upgrade your package dependencies to the latest versions @@ -32,16 +33,71 @@ dependencies: flutter: 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 - flutter_launcher_icons: ^0.10.0 + + # State Management + flutter_bloc: ^8.1.1 + + # Network + dio: ^5.0.3 + + # Icon + cupertino_icons: ^1.0.8 + phosphor_flutter: ^1.4.0 + flutter_phosphor_icons: ^0.0.1+6 + + # UI Helper + flutter_linkify: ^6.0.0 + url_launcher: ^6.2.5 + intl: ^0.17.0 + + # Image + cached_network_image: ^3.3.1 + image_picker: ^1.0.7 + image_picker_android: ^0.8.9+5 + image_picker_platform_interface: ^2.10.0 + + # Dependency Injection + get_it: ^7.6.8 + injectable: ^2.4.1 + + # Caching + hive: ^2.2.3 + + # Mics + equatable: ^2.0.5 + dartz: ^0.10.1 + path_provider: ^2.0.11 + + # Firebase + firebase_core: ^2.25.5 + firebase_messaging: ^14.7.17 + firebase_auth: ^4.17.6 + firebase_crashlytics: ^3.4.16 + + # Authenticate + google_sign_in: ^5.4.0 + flutter_facebook_auth: ^4.4.0+1 + sign_in_with_apple: ^4.1.0 + crypto: ^3.0.2 dev_dependencies: flutter_test: sdk: flutter + # Code generators + import_sorter: ^4.6.0 + flutter_launcher_icons: ^0.10.0 + injectable_generator: ^2.6.1 + build_runner: ^2.4.9 + + # Lint + flutter_lints: ^4.0.0 + + # Test + mockito: ^5.4.4 + flutter_icons: android: "launcher_icon" ios: false @@ -60,16 +116,15 @@ flutter_icons: # The following section is specific to Flutter packages. flutter: - # The following line ensures that the Material Icons font is # included with your application, so that you can use the icons in # the material Icons class. 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: + - assets/ + - assets/icons/ + - assets/images/ # An image asset can refer to one or more resolution-specific "variants", see # https://flutter.dev/assets-and-images/#resolution-aware diff --git a/run.sh b/run.sh index 62fce9b..15e7a66 100755 --- a/run.sh +++ b/run.sh @@ -1,4 +1,4 @@ -echo "0. Run release without environment" +echo "0. Unit Test" echo "1. Debug with: environment is DEV" echo "2. Debug with: environment is STAGING" echo "3. Debug with: environment is PRODUCTION" @@ -10,6 +10,10 @@ while : do read -p "Run with: " input case $input in + 0) + flutter test -r expanded + break + ;; 1) flutter run --dart-define="lambiengcode=DEV" break diff --git a/screenshots/launcher_icon_rounded.png b/screenshots/launcher_icon_rounded.png new file mode 100644 index 0000000..1d636b4 Binary files /dev/null and b/screenshots/launcher_icon_rounded.png differ diff --git a/screenshots/photo_0.jpg b/screenshots/photo_0.jpg new file mode 100644 index 0000000..915eb8b Binary files /dev/null and b/screenshots/photo_0.jpg differ diff --git a/screenshots/photo_1.jpeg b/screenshots/photo_1.jpeg new file mode 100644 index 0000000..ad7fc52 Binary files /dev/null and b/screenshots/photo_1.jpeg differ diff --git a/screenshots/photo_2.jpeg b/screenshots/photo_2.jpeg new file mode 100644 index 0000000..cbe48f9 Binary files /dev/null and b/screenshots/photo_2.jpeg differ diff --git a/screenshots/photo_3.jpeg b/screenshots/photo_3.jpeg new file mode 100644 index 0000000..8cccafb Binary files /dev/null and b/screenshots/photo_3.jpeg differ diff --git a/screenshots/photo_4.jpeg b/screenshots/photo_4.jpeg new file mode 100644 index 0000000..0dc425c Binary files /dev/null and b/screenshots/photo_4.jpeg differ diff --git a/screenshots/photo_5.jpeg b/screenshots/photo_5.jpeg new file mode 100644 index 0000000..6434ffa Binary files /dev/null and b/screenshots/photo_5.jpeg differ diff --git a/screenshots/photo_6.jpeg b/screenshots/photo_6.jpeg new file mode 100644 index 0000000..9ed481d Binary files /dev/null and b/screenshots/photo_6.jpeg differ diff --git a/screenshots/photo_7.jpeg b/screenshots/photo_7.jpeg new file mode 100644 index 0000000..9422dea Binary files /dev/null and b/screenshots/photo_7.jpeg differ diff --git a/test/features/auth/domain/usecases/register_with_email_test.dart b/test/features/auth/domain/usecases/register_with_email_test.dart deleted file mode 100644 index 8b13789..0000000 --- a/test/features/auth/domain/usecases/register_with_email_test.dart +++ /dev/null @@ -1 +0,0 @@ - diff --git a/test/features/auth/domain/usecases/sign_in_with_social_test.dart b/test/features/auth/domain/usecases/sign_in_with_social_test.dart new file mode 100644 index 0000000..c9e140a --- /dev/null +++ b/test/features/auth/domain/usecases/sign_in_with_social_test.dart @@ -0,0 +1,97 @@ +// Package imports: +import 'package:dartz/dartz.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; + +// Project imports: +import 'package:streamskit_mobile/core/error/failure.dart'; +import 'package:streamskit_mobile/core/usecase/usecase.dart'; +import 'package:streamskit_mobile/features/auth/domain/entities/social.dart'; +import 'package:streamskit_mobile/features/auth/domain/repositories/auth_repository.dart'; +import 'package:streamskit_mobile/features/auth/domain/usecases/sign_in_with_social.dart'; +import 'sign_in_with_social_test.mocks.dart'; + +@GenerateNiceMocks([MockSpec()]) +SocialValue fakeSocialValue = SocialValue( + fullName: 'lambiengcode', + googleId: 'lambiengcodeId', +); +SocialValue fakeWrongSocialValue = SocialValue( + fullName: 'lambiengcode', + googleId: 'lambiengcodeId1', +); + +void main() { + late SignInWithSocial usecase; + late MockAuthRepository mockAuthRepository; + + setUp(() { + mockAuthRepository = MockAuthRepository(); + usecase = SignInWithSocial(repository: mockAuthRepository); + }); + + test( + 'should sign in successful', + () async { + when( + mockAuthRepository.signIn( + Params(object: fakeSocialValue), + ), + ).thenAnswer((realInvocation) => Future.value(const Right(true))); + // act + final Either result = await usecase( + Params(object: fakeSocialValue), + ); + // assert + expect( + result.isRight(), + const Right(true).isRight(), + ); + verify(mockAuthRepository.signIn(Params(object: fakeSocialValue))); + verifyNoMoreInteractions(mockAuthRepository); + }, + ); + + test( + 'should sign in failure - wrong email/password', + () async { + when( + mockAuthRepository.signIn( + Params(object: fakeWrongSocialValue), + ), + ).thenAnswer((realInvocation) => Future.value(const Right(false))); + // act + final Either result = await usecase( + Params(object: fakeWrongSocialValue), + ); + // assert + expect( + result.isRight(), + const Right(false).isRight(), + ); + verify(mockAuthRepository.signIn(Params(object: fakeWrongSocialValue))); + verifyNoMoreInteractions(mockAuthRepository); + }, + ); + + test( + 'should sign in failure - param is not SocialValue', + () async { + // act + final Either result = await usecase( + const Params(object: {}), + ); + // assert + expect( + result.isLeft(), + true, + ); + verifyNever( + mockAuthRepository.signIn( + Params(object: fakeWrongSocialValue), + ), + ); + }, + ); +} diff --git a/test/features/auth/domain/usecases/sign_in_with_social_test.mocks.dart b/test/features/auth/domain/usecases/sign_in_with_social_test.mocks.dart new file mode 100644 index 0000000..3262313 --- /dev/null +++ b/test/features/auth/domain/usecases/sign_in_with_social_test.mocks.dart @@ -0,0 +1,113 @@ +// Mocks generated by Mockito 5.3.0 from annotations +// in streamskit_mobile/test/features/auth/domain/usecases/sign_in_with_social_test.dart. +// Do not manually edit this file. + +// ignore_for_file: no_leading_underscores_for_library_prefixes + +// Dart imports: +import 'dart:async' as _i4; + +// Package imports: +import 'package:dartz/dartz.dart' as _i2; +import 'package:mockito/mockito.dart' as _i1; + +// Project imports: +import 'package:streamskit_mobile/core/error/failure.dart' as _i5; +import 'package:streamskit_mobile/core/usecase/usecase.dart' as _i6; + +import 'package:streamskit_mobile/features/auth/domain/repositories/auth_repository.dart' + as _i3; + +// ignore_for_file: type=lint +// ignore_for_file: avoid_redundant_argument_values +// ignore_for_file: avoid_setters_without_getters +// ignore_for_file: comment_references +// ignore_for_file: implementation_imports +// ignore_for_file: invalid_use_of_visible_for_testing_member +// ignore_for_file: prefer_const_constructors +// ignore_for_file: unnecessary_parenthesis +// ignore_for_file: camel_case_types +// ignore_for_file: subtype_of_sealed_class + +class _FakeEither_0 extends _i1.SmartFake implements _i2.Either { + _FakeEither_0( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +/// A class which mocks [AuthRepository]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockAuthRepository extends _i1.Mock implements _i3.AuthRepository { + @override + _i4.Future<_i2.Either<_i5.Failure, bool>> signIn(_i6.Params? params) => + (super.noSuchMethod( + Invocation.method( + #signIn, + [params], + ), + returnValue: _i4.Future<_i2.Either<_i5.Failure, bool>>.value( + _FakeEither_0<_i5.Failure, bool>( + this, + Invocation.method( + #signIn, + [params], + ), + )), + returnValueForMissingStub: + _i4.Future<_i2.Either<_i5.Failure, bool>>.value( + _FakeEither_0<_i5.Failure, bool>( + this, + Invocation.method( + #signIn, + [params], + ), + )), + ) as _i4.Future<_i2.Either<_i5.Failure, bool>>); + @override + _i2.Either<_i5.Failure, bool> checkSignined() => (super.noSuchMethod( + Invocation.method( + #checkSignined, + [], + ), + returnValue: _FakeEither_0<_i5.Failure, bool>( + this, + Invocation.method( + #checkSignined, + [], + ), + ), + returnValueForMissingStub: _FakeEither_0<_i5.Failure, bool>( + this, + Invocation.method( + #checkSignined, + [], + ), + ), + ) as _i2.Either<_i5.Failure, bool>); + @override + _i2.Either<_i5.Failure, bool> signOut() => (super.noSuchMethod( + Invocation.method( + #signOut, + [], + ), + returnValue: _FakeEither_0<_i5.Failure, bool>( + this, + Invocation.method( + #signOut, + [], + ), + ), + returnValueForMissingStub: _FakeEither_0<_i5.Failure, bool>( + this, + Invocation.method( + #signOut, + [], + ), + ), + ) as _i2.Either<_i5.Failure, bool>); +} diff --git a/test/features/home/data/models/live_stream_model_test.dart b/test/features/home/data/models/live_stream_model_test.dart new file mode 100644 index 0000000..3e0409f --- /dev/null +++ b/test/features/home/data/models/live_stream_model_test.dart @@ -0,0 +1,73 @@ +// Dart imports: +import 'dart:convert'; + +// Package imports: +import 'package:flutter_test/flutter_test.dart'; + +// Project imports: +import 'package:streamskit_mobile/features/home/data/model/live_stream_model.dart'; +import '../../../../fixtures/fixture_reader.dart'; + +void main() { + final LiveStreamModel liveStreamModel = listLiveStreamFake[0]; + + test( + 'should be a subclass of LiveStream entity', + () async { + // assert + expect(liveStreamModel, isA()); + }, + ); + + group('fromMap', () { + test( + 'should return a valid model when the JSON', + () async { + // arrange + final List arrayRaw = + jsonDecode(fixture('live_stream_model.json')); + // act + final List result = arrayRaw + .map( + (liveStreamJson) => LiveStreamModel.fromMap(liveStreamJson)) + .toList(); + // assert + expect(result.length, arrayRaw.length); + }, + ); + }); + + group('toMap', () { + test( + 'should return a MAP map containing the proper data', + () async { + // act + final result = liveStreamModel.toMap(); + // assert + final expectedMap = { + 'peopleParticipant': 910, + 'type': 1, + 'urlToImage': urlImageGame, + }; + expect(result, expectedMap); + }, + ); + }); + + group('toJson', () { + test( + 'should return a JSON map containing the proper data', + () async { + // act + final result = liveStreamModel.toJson(); + // assert + final expectedMap = { + 'peopleParticipant': 910, + 'type': 1, + 'urlToImage': urlImageGame, + }; + expect(result, jsonEncode(expectedMap)); + }, + ); + }); +} diff --git a/test/features/home/data/repositories/live_stream_repository_test.dart b/test/features/home/data/repositories/live_stream_repository_test.dart new file mode 100644 index 0000000..3d14746 --- /dev/null +++ b/test/features/home/data/repositories/live_stream_repository_test.dart @@ -0,0 +1,53 @@ +// Dart imports: +import 'dart:convert'; + +// Package imports: +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/mockito.dart'; + +// Project imports: +import 'package:streamskit_mobile/features/home/data/datasources/local_live_stream_source.dart'; +import 'package:streamskit_mobile/features/home/data/model/live_stream_model.dart'; +import 'package:streamskit_mobile/features/home/data/repositories/live_stream_repository_impl.dart'; +import '../../../../fixtures/fixture_reader.dart'; + +class MockLocalDataSource extends Mock implements LocalLiveStreamSource { + @override + List getLiveStreams() { + return super.noSuchMethod(Invocation.method(#getLiveStreams, [])) ?? []; + } +} + +void main() { + late LiveStreamRepositoryImpl repository; + late MockLocalDataSource mockLocalDataSource; + + setUp(() { + mockLocalDataSource = MockLocalDataSource(); + repository = LiveStreamRepositoryImpl(localData: mockLocalDataSource); + }); + + group('get live streams', () { + final List arrayRaw = + jsonDecode(fixture('live_stream_model.json')); + final List liveStreams = arrayRaw + .map( + (liveStream) => LiveStreamModel.fromMap(liveStream), + ) + .toList(); + + test( + 'should return list live stream model', + () async { + // arrange + when(mockLocalDataSource.getLiveStreams()).thenAnswer( + (_) => liveStreams, + ); + // act + repository.getLiveStreams(); + // assert + verifyNever(mockLocalDataSource.saveLiveStreams(liveStreams)); + }, + ); + }); +} diff --git a/test/features/home/domain/usecases/get_list_live_stream_test.dart b/test/features/home/domain/usecases/get_list_live_stream_test.dart new file mode 100644 index 0000000..e3aedcc --- /dev/null +++ b/test/features/home/domain/usecases/get_list_live_stream_test.dart @@ -0,0 +1,41 @@ +// Package imports: +import 'package:dartz/dartz.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; + +// Project imports: +import 'package:streamskit_mobile/core/error/failure.dart'; +import 'package:streamskit_mobile/core/usecase/usecase.dart'; +import 'package:streamskit_mobile/features/home/data/model/live_stream_model.dart'; +import 'package:streamskit_mobile/features/home/domain/repositories/live_stream_repository.dart'; +import 'package:streamskit_mobile/features/home/domain/usecases/get_list_live_streaming.dart'; +import 'get_list_live_stream_test.mocks.dart'; + +@GenerateNiceMocks([MockSpec()]) +List liveStreams = listLiveStreamFake; + +void main() { + late GetListLiveStreaming usecase; + late MockLiveStreamRepository mockLiveStreamRepository; + + setUp(() { + mockLiveStreamRepository = MockLiveStreamRepository(); + usecase = GetListLiveStreaming(repository: mockLiveStreamRepository); + }); + + test( + 'should get list live streams from repository', + () async { + when(mockLiveStreamRepository.getLiveStreams()) + .thenAnswer((_) => Right(liveStreams)); + // act + final result = usecase(NoParams()); + // assert + expect(result.isRight(), + Right>(liveStreams).isRight()); + verify(mockLiveStreamRepository.getLiveStreams()); + verifyNoMoreInteractions(mockLiveStreamRepository); + }, + ); +} diff --git a/test/features/home/domain/usecases/get_list_live_stream_test.mocks.dart b/test/features/home/domain/usecases/get_list_live_stream_test.mocks.dart new file mode 100644 index 0000000..f2e7b95 --- /dev/null +++ b/test/features/home/domain/usecases/get_list_live_stream_test.mocks.dart @@ -0,0 +1,68 @@ +// Mocks generated by Mockito 5.3.0 from annotations +// in streamskit_mobile/test/features/home/domain/usecases/get_list_live_stream_test.dart. +// Do not manually edit this file. + +// ignore_for_file: no_leading_underscores_for_library_prefixes + +// Package imports: +import 'package:dartz/dartz.dart' as _i2; +import 'package:mockito/mockito.dart' as _i1; + +// Project imports: +import 'package:streamskit_mobile/core/error/failure.dart' as _i4; + +import 'package:streamskit_mobile/features/home/data/model/live_stream_model.dart' + as _i5; +import 'package:streamskit_mobile/features/home/domain/repositories/live_stream_repository.dart' + as _i3; + +// ignore_for_file: type=lint +// ignore_for_file: avoid_redundant_argument_values +// ignore_for_file: avoid_setters_without_getters +// ignore_for_file: comment_references +// ignore_for_file: implementation_imports +// ignore_for_file: invalid_use_of_visible_for_testing_member +// ignore_for_file: prefer_const_constructors +// ignore_for_file: unnecessary_parenthesis +// ignore_for_file: camel_case_types +// ignore_for_file: subtype_of_sealed_class + +class _FakeEither_0 extends _i1.SmartFake implements _i2.Either { + _FakeEither_0( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +/// A class which mocks [LiveStreamRepository]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockLiveStreamRepository extends _i1.Mock + implements _i3.LiveStreamRepository { + @override + _i2.Either<_i4.Failure, List<_i5.LiveStreamModel>> getLiveStreams() => + (super.noSuchMethod( + Invocation.method( + #getLiveStreams, + [], + ), + returnValue: _FakeEither_0<_i4.Failure, List<_i5.LiveStreamModel>>( + this, + Invocation.method( + #getLiveStreams, + [], + ), + ), + returnValueForMissingStub: + _FakeEither_0<_i4.Failure, List<_i5.LiveStreamModel>>( + this, + Invocation.method( + #getLiveStreams, + [], + ), + ), + ) as _i2.Either<_i4.Failure, List<_i5.LiveStreamModel>>); +} diff --git a/test/fixtures/fixture_reader.dart b/test/fixtures/fixture_reader.dart new file mode 100644 index 0000000..fcda8ea --- /dev/null +++ b/test/fixtures/fixture_reader.dart @@ -0,0 +1,4 @@ +// Dart imports: +import 'dart:io'; + +String fixture(String name) => File('test/fixtures/$name').readAsStringSync(); diff --git a/test/fixtures/live_stream_model.json b/test/fixtures/live_stream_model.json new file mode 100644 index 0000000..002fab7 --- /dev/null +++ b/test/fixtures/live_stream_model.json @@ -0,0 +1,22 @@ +[ + { + "peopleParticipant": 910, + "type": 1, + "urlToImage": "" + }, + { + "peopleParticipant": 910, + "type": 2, + "urlToImage": "" + }, + { + "peopleParticipant": 910, + "type": 3, + "urlToImage": "" + }, + { + "peopleParticipant": 910, + "type": 4, + "urlToImage": "" + } +] \ No newline at end of file diff --git a/tools-helper.sh b/tools-helper.sh new file mode 100755 index 0000000..c9087d1 --- /dev/null +++ b/tools-helper.sh @@ -0,0 +1,24 @@ +echo "1. import_sorter" +echo "2. dart_code_metrics" +echo "3. injectable" + +while : +do + read -p "Run with: " input + case $input in + 1) + flutter pub run import_sorter:main + break + ;; + 2) + flutter pub run dart_code_metrics:metrics analyze lib + break + ;; + 3) + flutter packages pub run build_runner build + break + ;; + *) + ;; + esac +done \ No newline at end of file diff --git a/web/icons/Icon-maskable-192.png b/web/icons/Icon-maskable-192.png index eb9b4d7..270f93a 100644 Binary files a/web/icons/Icon-maskable-192.png and b/web/icons/Icon-maskable-192.png differ diff --git a/web/icons/Icon-maskable-512.png b/web/icons/Icon-maskable-512.png index d69c566..13c61d5 100644 Binary files a/web/icons/Icon-maskable-512.png and b/web/icons/Icon-maskable-512.png differ diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index 8b6d468..3a3369d 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -6,6 +6,18 @@ #include "generated_plugin_registrant.h" +#include +#include +#include +#include void RegisterPlugins(flutter::PluginRegistry* registry) { + FileSelectorWindowsRegisterWithRegistrar( + registry->GetRegistrarForPlugin("FileSelectorWindows")); + FirebaseAuthPluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("FirebaseAuthPluginCApi")); + FirebaseCorePluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("FirebaseCorePluginCApi")); + UrlLauncherWindowsRegisterWithRegistrar( + registry->GetRegistrarForPlugin("UrlLauncherWindows")); } diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index b93c4c3..b9b24c8 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -3,6 +3,10 @@ # list(APPEND FLUTTER_PLUGIN_LIST + file_selector_windows + firebase_auth + firebase_core + url_launcher_windows ) list(APPEND FLUTTER_FFI_PLUGIN_LIST pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy