git@h5.yoqi.me 2 years ago
commit
3dded475f5
100 changed files with 4569 additions and 0 deletions
  1. 46 0
      .gitignore
  2. 10 0
      .metadata
  3. 43 0
      README.md
  4. 29 0
      analysis_options.yaml
  5. 13 0
      android/.gitignore
  6. 66 0
      android/app/build.gradle
  7. 0 0
      android/app/google-services.json
  8. 7 0
      android/app/src/debug/AndroidManifest.xml
  9. 54 0
      android/app/src/main/AndroidManifest.xml
  10. 19 0
      android/app/src/main/kotlin/com/vidcall/tws/youtube/Application.kt
  11. 18 0
      android/app/src/main/kotlin/com/vidcall/tws/youtube/FirebaseCloudMessagingPlugin.kt
  12. 6 0
      android/app/src/main/kotlin/com/vidcall/tws/youtube/MainActivity.kt
  13. BIN
      android/app/src/main/res/drawable-v21/ic_notify.png
  14. 12 0
      android/app/src/main/res/drawable-v21/launch_background.xml
  15. BIN
      android/app/src/main/res/drawable/ic_notify.png
  16. 12 0
      android/app/src/main/res/drawable/launch_background.xml
  17. BIN
      android/app/src/main/res/mipmap-hdpi/ic_launcher.png
  18. BIN
      android/app/src/main/res/mipmap-mdpi/ic_launcher.png
  19. BIN
      android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
  20. BIN
      android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
  21. BIN
      android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
  22. 18 0
      android/app/src/main/res/values-night/styles.xml
  23. 4 0
      android/app/src/main/res/values/strings.xml
  24. 18 0
      android/app/src/main/res/values/styles.xml
  25. 7 0
      android/app/src/profile/AndroidManifest.xml
  26. 32 0
      android/build.gradle
  27. 3 0
      android/gradle.properties
  28. 6 0
      android/gradle/wrapper/gradle-wrapper.properties
  29. 11 0
      android/settings.gradle
  30. BIN
      assets/sounds/ringlong.mp3
  31. 34 0
      ios/.gitignore
  32. 26 0
      ios/Flutter/AppFrameworkInfo.plist
  33. 1 0
      ios/Flutter/Debug.xcconfig
  34. 1 0
      ios/Flutter/Release.xcconfig
  35. 481 0
      ios/Runner.xcodeproj/project.pbxproj
  36. 7 0
      ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata
  37. 8 0
      ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
  38. 8 0
      ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
  39. 87 0
      ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
  40. 7 0
      ios/Runner.xcworkspace/contents.xcworkspacedata
  41. 8 0
      ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
  42. 8 0
      ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
  43. 13 0
      ios/Runner/AppDelegate.swift
  44. 122 0
      ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json
  45. BIN
      ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png
  46. BIN
      ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png
  47. BIN
      ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png
  48. BIN
      ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png
  49. BIN
      ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png
  50. BIN
      ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png
  51. BIN
      ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png
  52. BIN
      ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png
  53. BIN
      ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png
  54. BIN
      ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png
  55. BIN
      ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png
  56. BIN
      ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png
  57. BIN
      ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png
  58. BIN
      ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png
  59. BIN
      ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png
  60. 23 0
      ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json
  61. BIN
      ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png
  62. BIN
      ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png
  63. BIN
      ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png
  64. 5 0
      ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md
  65. 37 0
      ios/Runner/Base.lproj/LaunchScreen.storyboard
  66. 26 0
      ios/Runner/Base.lproj/Main.storyboard
  67. 47 0
      ios/Runner/Info.plist
  68. 1 0
      ios/Runner/Runner-Bridging-Header.h
  69. 41 0
      lib/data/api/auth_api.dart
  70. 85 0
      lib/data/api/call_api.dart
  71. 24 0
      lib/data/api/home_api.dart
  72. 60 0
      lib/data/models/call_model.dart
  73. 20 0
      lib/data/models/fcm_payload_model.dart
  74. 47 0
      lib/data/models/user_model.dart
  75. 80 0
      lib/main.dart
  76. 84 0
      lib/presentaion/cubit/auth/auth_cubit.dart
  77. 38 0
      lib/presentaion/cubit/auth/auth_state.dart
  78. 196 0
      lib/presentaion/cubit/call/call_cubit.dart
  79. 55 0
      lib/presentaion/cubit/call/call_state.dart
  80. 238 0
      lib/presentaion/cubit/home/home_cubit.dart
  81. 67 0
      lib/presentaion/cubit/home/home_state.dart
  82. 261 0
      lib/presentaion/screens/auth_screen.dart
  83. 258 0
      lib/presentaion/screens/call_screen.dart
  84. 141 0
      lib/presentaion/screens/home_screen.dart
  85. 34 0
      lib/presentaion/views/home_views/call_item_view.dart
  86. 54 0
      lib/presentaion/views/home_views/home_screen_pageview.dart
  87. 40 0
      lib/presentaion/views/home_views/user_item_view.dart
  88. 32 0
      lib/presentaion/widgets/call_widgets/default_circle_image.dart
  89. 44 0
      lib/presentaion/widgets/call_widgets/user_info_header.dart
  90. 41 0
      lib/routes.dart
  91. 137 0
      lib/services/fcm/firebase_notification_handler.dart
  92. 40 0
      lib/services/fcm/notification_handler.dart
  93. 29 0
      lib/shared/bloc_observer.dart
  94. 36 0
      lib/shared/constats.dart
  95. 78 0
      lib/shared/dio_helper.dart
  96. 45 0
      lib/shared/network/cache_helper.dart
  97. 79 0
      lib/shared/network/dio_helper.dart
  98. 15 0
      lib/shared/shared_widgets.dart
  99. 60 0
      lib/shared/theme.dart
  100. 726 0
      pubspec.lock

+ 46 - 0
.gitignore

@@ -0,0 +1,46 @@
+# Miscellaneous
+*.class
+*.log
+*.pyc
+*.swp
+.DS_Store
+.atom/
+.buildlog/
+.history
+.svn/
+
+# IntelliJ related
+*.iml
+*.ipr
+*.iws
+.idea/
+
+# The .vscode folder contains launch configuration and tasks you configure in
+# VS Code which you may wish to be included in version control, so this line
+# is commented out by default.
+#.vscode/
+
+# Flutter/Dart/Pub related
+**/doc/api/
+**/ios/Flutter/.last_build_id
+.dart_tool/
+.flutter-plugins
+.flutter-plugins-dependencies
+.packages
+.pub-cache/
+.pub/
+/build/
+
+# Web related
+lib/generated_plugin_registrant.dart
+
+# Symbolication related
+app.*.symbols
+
+# Obfuscation related
+app.*.map.json
+
+# Android Studio will place build artifacts here
+/android/app/debug
+/android/app/profile
+/android/app/release

+ 10 - 0
.metadata

@@ -0,0 +1,10 @@
+# This file tracks properties of this Flutter project.
+# Used by Flutter tool to assess capabilities and perform upgrades etc.
+#
+# This file should be version controlled and should not be manually edited.
+
+version:
+  revision: 7e9793dee1b85a243edd0e06cb1658e98b077561
+  channel: stable
+
+project_type: app

+ 43 - 0
README.md

@@ -0,0 +1,43 @@
+# Flutter Agora Fully Functional Video Call module
+
+## Tech Stack
+
+**Client:** Dart, Flutter
+
+**Server:** Firebase firestore, Google cloud functions
+
+## Techniques:
+
+*BloC pattern with Cubit State management
+
+*Fully Real-time data-consuming for (Users-History-VideoCall status)
+
+*Real-time handling for all call status (calling - accept- reject - cancel - busy - unAnswer - end)
+
+*Clean code and arch
+
+*Ui=>Cubit=>Api data flow
+
+*Dio package for deals with (generate agora token - FCM) api
+
+*Fcm notification (handling incoming calls in terminated mode)
+
+**Build token generator using nodejs:** https://www.youtube.com/watch?v=KcLypppA2IQ&ab_channel=Agora
+
+**Demo Video :** https://www.youtube.com/watch?v=Ond-VhB11h4
+
+
+![App Screenshot](https://i.ibb.co/3kWWdVX/Untitled1.png)
+
+![App Screenshot](https://i.ibb.co/nbV41dV/new.png)
+
+![App Screenshot](https://i.ibb.co/8KdjTF1/Untitled2.png)
+
+
+## Getting Started
+
+*Create a new firebase project and setup firestore and enable email/password authentication
+
+*Copy FCM authorization key and past it on fcmKey var in constants.dart file
+
+*Create agora new project and general token, channel name for test purpose and past them with your appId on (agoraAppId-agoraTestChannelName-agoraTestToken) vars in constants.dart file

+ 29 - 0
analysis_options.yaml

@@ -0,0 +1,29 @@
+# This file configures the analyzer, which statically analyzes Dart code to
+# check for errors, warnings, and lints.
+#
+# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
+# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
+# invoked from the command line by running `flutter analyze`.
+
+# 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
+
+linter:
+  # The lint rules applied to this project can be customized in the
+  # section below to disable rules from the `package:flutter_lints/flutter.yaml`
+  # included above or to enable additional rules. A list of all available lints
+  # and their documentation is published at
+  # https://dart-lang.github.io/linter/lints/index.html.
+  #
+  # Instead of disabling a lint rule for the entire project in the
+  # section below, it can also be suppressed for a single line of code
+  # or a specific dart file by using the `// ignore: name_of_lint` and
+  # `// ignore_for_file: name_of_lint` syntax on the line or in the file
+  # producing the lint.
+  rules:
+    # avoid_print: false  # Uncomment to disable the `avoid_print` rule
+    # prefer_single_quotes: true  # Uncomment to enable the `prefer_single_quotes` rule
+
+# Additional information about this file can be found at
+# https://dart.dev/guides/language/analysis-options

+ 13 - 0
android/.gitignore

@@ -0,0 +1,13 @@
+gradle-wrapper.jar
+/.gradle
+/captures/
+/gradlew
+/gradlew.bat
+/local.properties
+GeneratedPluginRegistrant.java
+
+# Remember to never publicly share your keystore.
+# See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app
+key.properties
+**/*.keystore
+**/*.jks

+ 66 - 0
android/app/build.gradle

@@ -0,0 +1,66 @@
+def localProperties = new Properties()
+def localPropertiesFile = rootProject.file('local.properties')
+if (localPropertiesFile.exists()) {
+    localPropertiesFile.withReader('UTF-8') { reader ->
+        localProperties.load(reader)
+    }
+}
+
+def flutterRoot = localProperties.getProperty('flutter.sdk')
+if (flutterRoot == null) {
+    throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.")
+}
+
+def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
+if (flutterVersionCode == null) {
+    flutterVersionCode = '1'
+}
+
+def flutterVersionName = localProperties.getProperty('flutter.versionName')
+if (flutterVersionName == null) {
+    flutterVersionName = '1.0'
+}
+
+apply plugin: 'com.android.application'
+apply plugin: 'kotlin-android'
+apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
+apply plugin: 'com.google.gms.google-services'
+android {
+    compileSdkVersion flutter.compileSdkVersion
+
+    compileOptions {
+        sourceCompatibility JavaVersion.VERSION_1_8
+        targetCompatibility JavaVersion.VERSION_1_8
+    }
+
+    kotlinOptions {
+        jvmTarget = '1.8'
+    }
+
+    sourceSets {
+        main.java.srcDirs += 'src/main/kotlin'
+    }
+
+    defaultConfig {
+        applicationId "com.agora.videocall.youtube"
+        minSdkVersion 19
+        multiDexEnabled true
+        targetSdkVersion flutter.targetSdkVersion
+        versionCode flutterVersionCode.toInteger()
+        versionName flutterVersionName
+    }
+
+    buildTypes {
+        release {
+            signingConfig signingConfigs.debug
+        }
+    }
+}
+
+flutter {
+    source '../..'
+}
+
+dependencies {
+    implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
+}

+ 0 - 0
android/app/google-services.json


+ 7 - 0
android/app/src/debug/AndroidManifest.xml

@@ -0,0 +1,7 @@
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.vidcall.tws.youtube">
+    <!-- Flutter needs it to communicate with the running application
+         to allow setting breakpoints, to provide hot reload, etc.
+    -->
+    <uses-permission android:name="android.permission.INTERNET"/>
+</manifest>

+ 54 - 0
android/app/src/main/AndroidManifest.xml

@@ -0,0 +1,54 @@
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.vidcall.tws.youtube">
+
+    <uses-permission android:name="android.permission.READ_PHONE_STATE" />
+    <uses-permission android:name="android.permission.INTERNET" />
+    <uses-permission android:name="android.permission.RECORD_AUDIO" />
+    <uses-permission android:name="android.permission.CAMERA" />
+    <uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
+    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
+    <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
+    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
+    <uses-permission android:name="android.permission.WAKE_LOCK" />
+
+   <application
+       android:name=".Application"
+        android:label="Agora Video Call"
+        android:icon="@mipmap/ic_launcher"
+        android:usesCleartextTraffic="true">
+       <meta-data
+           android:name="com.google.firebase.messaging.default_notification_channel"
+           android:value="@string/default_notification_channel_id" />
+
+       <meta-data
+           android:name="com.google.firebase.messaging.default_notification_icon"
+           android:resource="@drawable/ic_notify" />
+
+       <activity
+            android:name=".MainActivity"
+            android:exported="true"
+            android:launchMode="singleTop"
+            android:theme="@style/LaunchTheme"
+            android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
+            android:hardwareAccelerated="true"
+            android:windowSoftInputMode="adjustResize">
+            <!-- Specifies an Android theme to apply to this Activity as soon as
+                 the Android process has started. This theme is visible to the user
+                 while the Flutter UI initializes. After that, this theme continues
+                 to determine the Window background behind the Flutter UI. -->
+            <meta-data
+              android:name="io.flutter.embedding.android.NormalTheme"
+              android:resource="@style/NormalTheme"
+              />
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
+            </intent-filter>
+        </activity>
+        <!-- Don't delete the meta-data below.
+             This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
+        <meta-data
+            android:name="flutterEmbedding"
+            android:value="2" />
+    </application>
+</manifest>

+ 19 - 0
android/app/src/main/kotlin/com/vidcall/tws/youtube/Application.kt

@@ -0,0 +1,19 @@
+package com.vidcall.tws.youtube
+
+import android.media.MediaCas
+import com.vidcall.tws.youtube.FirebaseCloudMessagingPlugin
+import io.flutter.app.FlutterApplication
+import io.flutter.plugin.common.PluginRegistry
+import io.flutter.plugins.firebase.messaging.FlutterFirebaseMessagingBackgroundService
+
+class Application : FlutterApplication(),PluginRegistry.PluginRegistrantCallback {
+
+    override fun onCreate() {
+        super.onCreate()
+    }
+
+    override fun registerWith(registry: PluginRegistry) {
+        FirebaseCloudMessagingPlugin.registerWith(registry!!)
+    }
+
+}

+ 18 - 0
android/app/src/main/kotlin/com/vidcall/tws/youtube/FirebaseCloudMessagingPlugin.kt

@@ -0,0 +1,18 @@
+package com.vidcall.tws.youtube
+
+import io.flutter.plugin.common.PluginRegistry
+
+object FirebaseCloudMessagingPlugin {
+    fun registerWith(pluginRegistry: PluginRegistry)
+    {
+        if(alreadyRegisterWith(pluginRegistry)) return
+        registerWith(pluginRegistry)
+    }
+
+    private fun alreadyRegisterWith(pluginRegistry: PluginRegistry): Boolean {
+        val key = FirebaseCloudMessagingPlugin::class.java.canonicalName
+        if(pluginRegistry.hasPlugin(key)) return  true;
+        pluginRegistry.registrarFor(key)
+        return  false;
+    }
+}

+ 6 - 0
android/app/src/main/kotlin/com/vidcall/tws/youtube/MainActivity.kt

@@ -0,0 +1,6 @@
+package com.vidcall.tws.youtube
+
+import io.flutter.embedding.android.FlutterActivity
+
+class MainActivity: FlutterActivity() {
+}

BIN
android/app/src/main/res/drawable-v21/ic_notify.png


+ 12 - 0
android/app/src/main/res/drawable-v21/launch_background.xml

@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Modify this file to customize your launch splash screen -->
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:drawable="?android:colorBackground" />
+
+    <!-- You can insert your own image assets here -->
+    <!-- <item>
+        <bitmap
+            android:gravity="center"
+            android:src="@mipmap/launch_image" />
+    </item> -->
+</layer-list>

BIN
android/app/src/main/res/drawable/ic_notify.png


+ 12 - 0
android/app/src/main/res/drawable/launch_background.xml

@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Modify this file to customize your launch splash screen -->
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:drawable="@android:color/white" />
+
+    <!-- You can insert your own image assets here -->
+    <!-- <item>
+        <bitmap
+            android:gravity="center"
+            android:src="@mipmap/launch_image" />
+    </item> -->
+</layer-list>

BIN
android/app/src/main/res/mipmap-hdpi/ic_launcher.png


BIN
android/app/src/main/res/mipmap-mdpi/ic_launcher.png


BIN
android/app/src/main/res/mipmap-xhdpi/ic_launcher.png


BIN
android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png


BIN
android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png


+ 18 - 0
android/app/src/main/res/values-night/styles.xml

@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
+    <style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
+        <!-- Show a splash screen on the activity. Automatically removed when
+             Flutter draws its first frame -->
+        <item name="android:windowBackground">@drawable/launch_background</item>
+    </style>
+    <!-- Theme applied to the Android Window as soon as the process has started.
+         This theme determines the color of the Android Window while your
+         Flutter UI initializes, as well as behind your Flutter UI while its
+         running.
+
+         This Theme is only used starting with V2 of Flutter's Android embedding. -->
+    <style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
+        <item name="android:windowBackground">?android:colorBackground</item>
+    </style>
+</resources>

+ 4 - 0
android/app/src/main/res/values/strings.xml

@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <string name="default_notification_channel_id" translatable="false">normal_channel</string>
+</resources>

+ 18 - 0
android/app/src/main/res/values/styles.xml

@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
+    <style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
+        <!-- Show a splash screen on the activity. Automatically removed when
+             Flutter draws its first frame -->
+        <item name="android:windowBackground">@drawable/launch_background</item>
+    </style>
+    <!-- Theme applied to the Android Window as soon as the process has started.
+         This theme determines the color of the Android Window while your
+         Flutter UI initializes, as well as behind your Flutter UI while its
+         running.
+
+         This Theme is only used starting with V2 of Flutter's Android embedding. -->
+    <style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
+        <item name="android:windowBackground">?android:colorBackground</item>
+    </style>
+</resources>

+ 7 - 0
android/app/src/profile/AndroidManifest.xml

@@ -0,0 +1,7 @@
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.vidcall.tws.youtube">
+    <!-- Flutter needs it to communicate with the running application
+         to allow setting breakpoints, to provide hot reload, etc.
+    -->
+    <uses-permission android:name="android.permission.INTERNET"/>
+</manifest>

+ 32 - 0
android/build.gradle

@@ -0,0 +1,32 @@
+buildscript {
+    ext.kotlin_version = '1.6.10'
+    repositories {
+        google()
+        mavenCentral()
+    }
+
+    dependencies {
+        classpath 'com.android.tools.build:gradle:7.2.2'
+        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
+        classpath 'com.google.gms:google-services:4.3.10'
+    }
+}
+
+allprojects {
+    repositories {
+        google()
+        mavenCentral()
+    }
+}
+
+rootProject.buildDir = '../build'
+subprojects {
+    project.buildDir = "${rootProject.buildDir}/${project.name}"
+}
+subprojects {
+    project.evaluationDependsOn(':app')
+}
+
+task clean(type: Delete) {
+    delete rootProject.buildDir
+}

+ 3 - 0
android/gradle.properties

@@ -0,0 +1,3 @@
+org.gradle.jvmargs=-Xmx1536M
+android.useAndroidX=true
+android.enableJetifier=true

+ 6 - 0
android/gradle/wrapper/gradle-wrapper.properties

@@ -0,0 +1,6 @@
+#Fri Jun 23 08:50:38 CEST 2017
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-bin.zip

+ 11 - 0
android/settings.gradle

@@ -0,0 +1,11 @@
+include ':app'
+
+def localPropertiesFile = new File(rootProject.projectDir, "local.properties")
+def properties = new Properties()
+
+assert localPropertiesFile.exists()
+localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) }
+
+def flutterSdkPath = properties.getProperty("flutter.sdk")
+assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
+apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle"

BIN
assets/sounds/ringlong.mp3


+ 34 - 0
ios/.gitignore

@@ -0,0 +1,34 @@
+**/dgph
+*.mode1v3
+*.mode2v3
+*.moved-aside
+*.pbxuser
+*.perspectivev3
+**/*sync/
+.sconsign.dblite
+.tags*
+**/.vagrant/
+**/DerivedData/
+Icon?
+**/Pods/
+**/.symlinks/
+profile
+xcuserdata
+**/.generated/
+Flutter/App.framework
+Flutter/Flutter.framework
+Flutter/Flutter.podspec
+Flutter/Generated.xcconfig
+Flutter/ephemeral/
+Flutter/app.flx
+Flutter/app.zip
+Flutter/flutter_assets/
+Flutter/flutter_export_environment.sh
+ServiceDefinitions.json
+Runner/GeneratedPluginRegistrant.*
+
+# Exceptions to above rules.
+!default.mode1v3
+!default.mode2v3
+!default.pbxuser
+!default.perspectivev3

+ 26 - 0
ios/Flutter/AppFrameworkInfo.plist

@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+  <key>CFBundleDevelopmentRegion</key>
+  <string>en</string>
+  <key>CFBundleExecutable</key>
+  <string>App</string>
+  <key>CFBundleIdentifier</key>
+  <string>io.flutter.flutter.app</string>
+  <key>CFBundleInfoDictionaryVersion</key>
+  <string>6.0</string>
+  <key>CFBundleName</key>
+  <string>App</string>
+  <key>CFBundlePackageType</key>
+  <string>FMWK</string>
+  <key>CFBundleShortVersionString</key>
+  <string>1.0</string>
+  <key>CFBundleSignature</key>
+  <string>????</string>
+  <key>CFBundleVersion</key>
+  <string>1.0</string>
+  <key>MinimumOSVersion</key>
+  <string>9.0</string>
+</dict>
+</plist>

+ 1 - 0
ios/Flutter/Debug.xcconfig

@@ -0,0 +1 @@
+#include "Generated.xcconfig"

+ 1 - 0
ios/Flutter/Release.xcconfig

@@ -0,0 +1 @@
+#include "Generated.xcconfig"

+ 481 - 0
ios/Runner.xcodeproj/project.pbxproj

@@ -0,0 +1,481 @@
+// !$*UTF8*$!
+{
+	archiveVersion = 1;
+	classes = {
+	};
+	objectVersion = 50;
+	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 */; };
+		74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
+		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 */; };
+/* End PBXBuildFile section */
+
+/* Begin PBXCopyFilesBuildPhase section */
+		9705A1C41CF9048500538489 /* Embed Frameworks */ = {
+			isa = PBXCopyFilesBuildPhase;
+			buildActionMask = 2147483647;
+			dstPath = "";
+			dstSubfolderSpec = 10;
+			files = (
+			);
+			name = "Embed Frameworks";
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+/* End PBXCopyFilesBuildPhase section */
+
+/* Begin PBXFileReference section */
+		1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
+		1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
+		3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
+		74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
+		74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
+		7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
+		9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
+		9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
+		97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
+		97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
+		97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
+		97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
+		97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
+/* End PBXFileReference section */
+
+/* Begin PBXFrameworksBuildPhase section */
+		97C146EB1CF9000F007C117D /* Frameworks */ = {
+			isa = PBXFrameworksBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+/* End PBXFrameworksBuildPhase section */
+
+/* Begin PBXGroup section */
+		9740EEB11CF90186004384FC /* Flutter */ = {
+			isa = PBXGroup;
+			children = (
+				3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
+				9740EEB21CF90195004384FC /* Debug.xcconfig */,
+				7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
+				9740EEB31CF90195004384FC /* Generated.xcconfig */,
+			);
+			name = Flutter;
+			sourceTree = "<group>";
+		};
+		97C146E51CF9000F007C117D = {
+			isa = PBXGroup;
+			children = (
+				9740EEB11CF90186004384FC /* Flutter */,
+				97C146F01CF9000F007C117D /* Runner */,
+				97C146EF1CF9000F007C117D /* Products */,
+			);
+			sourceTree = "<group>";
+		};
+		97C146EF1CF9000F007C117D /* Products */ = {
+			isa = PBXGroup;
+			children = (
+				97C146EE1CF9000F007C117D /* Runner.app */,
+			);
+			name = Products;
+			sourceTree = "<group>";
+		};
+		97C146F01CF9000F007C117D /* Runner */ = {
+			isa = PBXGroup;
+			children = (
+				97C146FA1CF9000F007C117D /* Main.storyboard */,
+				97C146FD1CF9000F007C117D /* Assets.xcassets */,
+				97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
+				97C147021CF9000F007C117D /* Info.plist */,
+				1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */,
+				1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,
+				74858FAE1ED2DC5600515810 /* AppDelegate.swift */,
+				74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */,
+			);
+			path = Runner;
+			sourceTree = "<group>";
+		};
+/* End PBXGroup section */
+
+/* Begin PBXNativeTarget section */
+		97C146ED1CF9000F007C117D /* Runner */ = {
+			isa = PBXNativeTarget;
+			buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
+			buildPhases = (
+				9740EEB61CF901F6004384FC /* Run Script */,
+				97C146EA1CF9000F007C117D /* Sources */,
+				97C146EB1CF9000F007C117D /* Frameworks */,
+				97C146EC1CF9000F007C117D /* Resources */,
+				9705A1C41CF9048500538489 /* Embed Frameworks */,
+				3B06AD1E1E4923F5004D2608 /* Thin Binary */,
+			);
+			buildRules = (
+			);
+			dependencies = (
+			);
+			name = Runner;
+			productName = Runner;
+			productReference = 97C146EE1CF9000F007C117D /* Runner.app */;
+			productType = "com.apple.product-type.application";
+		};
+/* End PBXNativeTarget section */
+
+/* Begin PBXProject section */
+		97C146E61CF9000F007C117D /* Project object */ = {
+			isa = PBXProject;
+			attributes = {
+				LastUpgradeCheck = 1300;
+				ORGANIZATIONNAME = "";
+				TargetAttributes = {
+					97C146ED1CF9000F007C117D = {
+						CreatedOnToolsVersion = 7.3.1;
+						LastSwiftMigration = 1100;
+					};
+				};
+			};
+			buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */;
+			compatibilityVersion = "Xcode 9.3";
+			developmentRegion = en;
+			hasScannedForEncodings = 0;
+			knownRegions = (
+				en,
+				Base,
+			);
+			mainGroup = 97C146E51CF9000F007C117D;
+			productRefGroup = 97C146EF1CF9000F007C117D /* Products */;
+			projectDirPath = "";
+			projectRoot = "";
+			targets = (
+				97C146ED1CF9000F007C117D /* Runner */,
+			);
+		};
+/* End PBXProject section */
+
+/* Begin PBXResourcesBuildPhase section */
+		97C146EC1CF9000F007C117D /* Resources */ = {
+			isa = PBXResourcesBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+				97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */,
+				3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,
+				97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,
+				97C146FC1CF9000F007C117D /* Main.storyboard in Resources */,
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+/* End PBXResourcesBuildPhase section */
+
+/* Begin PBXShellScriptBuildPhase section */
+		3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
+			isa = PBXShellScriptBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+			);
+			inputPaths = (
+			);
+			name = "Thin Binary";
+			outputPaths = (
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+			shellPath = /bin/sh;
+			shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
+		};
+		9740EEB61CF901F6004384FC /* Run Script */ = {
+			isa = PBXShellScriptBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+			);
+			inputPaths = (
+			);
+			name = "Run Script";
+			outputPaths = (
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+			shellPath = /bin/sh;
+			shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
+		};
+/* End PBXShellScriptBuildPhase section */
+
+/* Begin PBXSourcesBuildPhase section */
+		97C146EA1CF9000F007C117D /* Sources */ = {
+			isa = PBXSourcesBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+				74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,
+				1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+/* End PBXSourcesBuildPhase section */
+
+/* Begin PBXVariantGroup section */
+		97C146FA1CF9000F007C117D /* Main.storyboard */ = {
+			isa = PBXVariantGroup;
+			children = (
+				97C146FB1CF9000F007C117D /* Base */,
+			);
+			name = Main.storyboard;
+			sourceTree = "<group>";
+		};
+		97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = {
+			isa = PBXVariantGroup;
+			children = (
+				97C147001CF9000F007C117D /* Base */,
+			);
+			name = LaunchScreen.storyboard;
+			sourceTree = "<group>";
+		};
+/* End PBXVariantGroup section */
+
+/* Begin XCBuildConfiguration section */
+		249021D3217E4FDB00AE95B9 /* Profile */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				ALWAYS_SEARCH_USER_PATHS = NO;
+				CLANG_ANALYZER_NONNULL = YES;
+				CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
+				CLANG_CXX_LIBRARY = "libc++";
+				CLANG_ENABLE_MODULES = YES;
+				CLANG_ENABLE_OBJC_ARC = YES;
+				CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+				CLANG_WARN_BOOL_CONVERSION = YES;
+				CLANG_WARN_COMMA = YES;
+				CLANG_WARN_CONSTANT_CONVERSION = YES;
+				CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+				CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+				CLANG_WARN_EMPTY_BODY = YES;
+				CLANG_WARN_ENUM_CONVERSION = YES;
+				CLANG_WARN_INFINITE_RECURSION = YES;
+				CLANG_WARN_INT_CONVERSION = YES;
+				CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+				CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
+				CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+				CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+				CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+				CLANG_WARN_STRICT_PROTOTYPES = YES;
+				CLANG_WARN_SUSPICIOUS_MOVE = YES;
+				CLANG_WARN_UNREACHABLE_CODE = YES;
+				CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+				"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
+				COPY_PHASE_STRIP = NO;
+				DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
+				ENABLE_NS_ASSERTIONS = NO;
+				ENABLE_STRICT_OBJC_MSGSEND = YES;
+				GCC_C_LANGUAGE_STANDARD = gnu99;
+				GCC_NO_COMMON_BLOCKS = YES;
+				GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+				GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+				GCC_WARN_UNDECLARED_SELECTOR = YES;
+				GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+				GCC_WARN_UNUSED_FUNCTION = YES;
+				GCC_WARN_UNUSED_VARIABLE = YES;
+				IPHONEOS_DEPLOYMENT_TARGET = 9.0;
+				MTL_ENABLE_DEBUG_INFO = NO;
+				SDKROOT = iphoneos;
+				SUPPORTED_PLATFORMS = iphoneos;
+				TARGETED_DEVICE_FAMILY = "1,2";
+				VALIDATE_PRODUCT = YES;
+			};
+			name = Profile;
+		};
+		249021D4217E4FDB00AE95B9 /* Profile */ = {
+			isa = XCBuildConfiguration;
+			baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
+			buildSettings = {
+				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+				CLANG_ENABLE_MODULES = YES;
+				CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
+				ENABLE_BITCODE = NO;
+				INFOPLIST_FILE = Runner/Info.plist;
+				LD_RUNPATH_SEARCH_PATHS = (
+					"$(inherited)",
+					"@executable_path/Frameworks",
+				);
+				PRODUCT_BUNDLE_IDENTIFIER = com.vidcall.tws.youtube;
+				PRODUCT_NAME = "$(TARGET_NAME)";
+				SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
+				SWIFT_VERSION = 5.0;
+				VERSIONING_SYSTEM = "apple-generic";
+			};
+			name = Profile;
+		};
+		97C147031CF9000F007C117D /* Debug */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				ALWAYS_SEARCH_USER_PATHS = NO;
+				CLANG_ANALYZER_NONNULL = YES;
+				CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
+				CLANG_CXX_LIBRARY = "libc++";
+				CLANG_ENABLE_MODULES = YES;
+				CLANG_ENABLE_OBJC_ARC = YES;
+				CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+				CLANG_WARN_BOOL_CONVERSION = YES;
+				CLANG_WARN_COMMA = YES;
+				CLANG_WARN_CONSTANT_CONVERSION = YES;
+				CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+				CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+				CLANG_WARN_EMPTY_BODY = YES;
+				CLANG_WARN_ENUM_CONVERSION = YES;
+				CLANG_WARN_INFINITE_RECURSION = YES;
+				CLANG_WARN_INT_CONVERSION = YES;
+				CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+				CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
+				CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+				CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+				CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+				CLANG_WARN_STRICT_PROTOTYPES = YES;
+				CLANG_WARN_SUSPICIOUS_MOVE = YES;
+				CLANG_WARN_UNREACHABLE_CODE = YES;
+				CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+				"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
+				COPY_PHASE_STRIP = NO;
+				DEBUG_INFORMATION_FORMAT = dwarf;
+				ENABLE_STRICT_OBJC_MSGSEND = YES;
+				ENABLE_TESTABILITY = YES;
+				GCC_C_LANGUAGE_STANDARD = gnu99;
+				GCC_DYNAMIC_NO_PIC = NO;
+				GCC_NO_COMMON_BLOCKS = YES;
+				GCC_OPTIMIZATION_LEVEL = 0;
+				GCC_PREPROCESSOR_DEFINITIONS = (
+					"DEBUG=1",
+					"$(inherited)",
+				);
+				GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+				GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+				GCC_WARN_UNDECLARED_SELECTOR = YES;
+				GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+				GCC_WARN_UNUSED_FUNCTION = YES;
+				GCC_WARN_UNUSED_VARIABLE = YES;
+				IPHONEOS_DEPLOYMENT_TARGET = 9.0;
+				MTL_ENABLE_DEBUG_INFO = YES;
+				ONLY_ACTIVE_ARCH = YES;
+				SDKROOT = iphoneos;
+				TARGETED_DEVICE_FAMILY = "1,2";
+			};
+			name = Debug;
+		};
+		97C147041CF9000F007C117D /* Release */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				ALWAYS_SEARCH_USER_PATHS = NO;
+				CLANG_ANALYZER_NONNULL = YES;
+				CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
+				CLANG_CXX_LIBRARY = "libc++";
+				CLANG_ENABLE_MODULES = YES;
+				CLANG_ENABLE_OBJC_ARC = YES;
+				CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+				CLANG_WARN_BOOL_CONVERSION = YES;
+				CLANG_WARN_COMMA = YES;
+				CLANG_WARN_CONSTANT_CONVERSION = YES;
+				CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+				CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+				CLANG_WARN_EMPTY_BODY = YES;
+				CLANG_WARN_ENUM_CONVERSION = YES;
+				CLANG_WARN_INFINITE_RECURSION = YES;
+				CLANG_WARN_INT_CONVERSION = YES;
+				CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+				CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
+				CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+				CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+				CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+				CLANG_WARN_STRICT_PROTOTYPES = YES;
+				CLANG_WARN_SUSPICIOUS_MOVE = YES;
+				CLANG_WARN_UNREACHABLE_CODE = YES;
+				CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+				"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
+				COPY_PHASE_STRIP = NO;
+				DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
+				ENABLE_NS_ASSERTIONS = NO;
+				ENABLE_STRICT_OBJC_MSGSEND = YES;
+				GCC_C_LANGUAGE_STANDARD = gnu99;
+				GCC_NO_COMMON_BLOCKS = YES;
+				GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+				GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+				GCC_WARN_UNDECLARED_SELECTOR = YES;
+				GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+				GCC_WARN_UNUSED_FUNCTION = YES;
+				GCC_WARN_UNUSED_VARIABLE = YES;
+				IPHONEOS_DEPLOYMENT_TARGET = 9.0;
+				MTL_ENABLE_DEBUG_INFO = NO;
+				SDKROOT = iphoneos;
+				SUPPORTED_PLATFORMS = iphoneos;
+				SWIFT_COMPILATION_MODE = wholemodule;
+				SWIFT_OPTIMIZATION_LEVEL = "-O";
+				TARGETED_DEVICE_FAMILY = "1,2";
+				VALIDATE_PRODUCT = YES;
+			};
+			name = Release;
+		};
+		97C147061CF9000F007C117D /* Debug */ = {
+			isa = XCBuildConfiguration;
+			baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
+			buildSettings = {
+				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+				CLANG_ENABLE_MODULES = YES;
+				CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
+				ENABLE_BITCODE = NO;
+				INFOPLIST_FILE = Runner/Info.plist;
+				LD_RUNPATH_SEARCH_PATHS = (
+					"$(inherited)",
+					"@executable_path/Frameworks",
+				);
+				PRODUCT_BUNDLE_IDENTIFIER = com.vidcall.tws.youtube;
+				PRODUCT_NAME = "$(TARGET_NAME)";
+				SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
+				SWIFT_OPTIMIZATION_LEVEL = "-Onone";
+				SWIFT_VERSION = 5.0;
+				VERSIONING_SYSTEM = "apple-generic";
+			};
+			name = Debug;
+		};
+		97C147071CF9000F007C117D /* Release */ = {
+			isa = XCBuildConfiguration;
+			baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
+			buildSettings = {
+				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+				CLANG_ENABLE_MODULES = YES;
+				CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
+				ENABLE_BITCODE = NO;
+				INFOPLIST_FILE = Runner/Info.plist;
+				LD_RUNPATH_SEARCH_PATHS = (
+					"$(inherited)",
+					"@executable_path/Frameworks",
+				);
+				PRODUCT_BUNDLE_IDENTIFIER = com.vidcall.tws.youtube;
+				PRODUCT_NAME = "$(TARGET_NAME)";
+				SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
+				SWIFT_VERSION = 5.0;
+				VERSIONING_SYSTEM = "apple-generic";
+			};
+			name = Release;
+		};
+/* End XCBuildConfiguration section */
+
+/* Begin XCConfigurationList section */
+		97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = {
+			isa = XCConfigurationList;
+			buildConfigurations = (
+				97C147031CF9000F007C117D /* Debug */,
+				97C147041CF9000F007C117D /* Release */,
+				249021D3217E4FDB00AE95B9 /* Profile */,
+			);
+			defaultConfigurationIsVisible = 0;
+			defaultConfigurationName = Release;
+		};
+		97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = {
+			isa = XCConfigurationList;
+			buildConfigurations = (
+				97C147061CF9000F007C117D /* Debug */,
+				97C147071CF9000F007C117D /* Release */,
+				249021D4217E4FDB00AE95B9 /* Profile */,
+			);
+			defaultConfigurationIsVisible = 0;
+			defaultConfigurationName = Release;
+		};
+/* End XCConfigurationList section */
+	};
+	rootObject = 97C146E61CF9000F007C117D /* Project object */;
+}

+ 7 - 0
ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata

@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Workspace
+   version = "1.0">
+   <FileRef
+      location = "self:">
+   </FileRef>
+</Workspace>

+ 8 - 0
ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist

@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+	<key>IDEDidComputeMac32BitWarning</key>
+	<true/>
+</dict>
+</plist>

+ 8 - 0
ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings

@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+	<key>PreviewsEnabled</key>
+	<false/>
+</dict>
+</plist>

+ 87 - 0
ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme

@@ -0,0 +1,87 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Scheme
+   LastUpgradeVersion = "1300"
+   version = "1.3">
+   <BuildAction
+      parallelizeBuildables = "YES"
+      buildImplicitDependencies = "YES">
+      <BuildActionEntries>
+         <BuildActionEntry
+            buildForTesting = "YES"
+            buildForRunning = "YES"
+            buildForProfiling = "YES"
+            buildForArchiving = "YES"
+            buildForAnalyzing = "YES">
+            <BuildableReference
+               BuildableIdentifier = "primary"
+               BlueprintIdentifier = "97C146ED1CF9000F007C117D"
+               BuildableName = "Runner.app"
+               BlueprintName = "Runner"
+               ReferencedContainer = "container:Runner.xcodeproj">
+            </BuildableReference>
+         </BuildActionEntry>
+      </BuildActionEntries>
+   </BuildAction>
+   <TestAction
+      buildConfiguration = "Debug"
+      selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+      selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+      shouldUseLaunchSchemeArgsEnv = "YES">
+      <MacroExpansion>
+         <BuildableReference
+            BuildableIdentifier = "primary"
+            BlueprintIdentifier = "97C146ED1CF9000F007C117D"
+            BuildableName = "Runner.app"
+            BlueprintName = "Runner"
+            ReferencedContainer = "container:Runner.xcodeproj">
+         </BuildableReference>
+      </MacroExpansion>
+      <Testables>
+      </Testables>
+   </TestAction>
+   <LaunchAction
+      buildConfiguration = "Debug"
+      selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+      selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+      launchStyle = "0"
+      useCustomWorkingDirectory = "NO"
+      ignoresPersistentStateOnLaunch = "NO"
+      debugDocumentVersioning = "YES"
+      debugServiceExtension = "internal"
+      allowLocationSimulation = "YES">
+      <BuildableProductRunnable
+         runnableDebuggingMode = "0">
+         <BuildableReference
+            BuildableIdentifier = "primary"
+            BlueprintIdentifier = "97C146ED1CF9000F007C117D"
+            BuildableName = "Runner.app"
+            BlueprintName = "Runner"
+            ReferencedContainer = "container:Runner.xcodeproj">
+         </BuildableReference>
+      </BuildableProductRunnable>
+   </LaunchAction>
+   <ProfileAction
+      buildConfiguration = "Profile"
+      shouldUseLaunchSchemeArgsEnv = "YES"
+      savedToolIdentifier = ""
+      useCustomWorkingDirectory = "NO"
+      debugDocumentVersioning = "YES">
+      <BuildableProductRunnable
+         runnableDebuggingMode = "0">
+         <BuildableReference
+            BuildableIdentifier = "primary"
+            BlueprintIdentifier = "97C146ED1CF9000F007C117D"
+            BuildableName = "Runner.app"
+            BlueprintName = "Runner"
+            ReferencedContainer = "container:Runner.xcodeproj">
+         </BuildableReference>
+      </BuildableProductRunnable>
+   </ProfileAction>
+   <AnalyzeAction
+      buildConfiguration = "Debug">
+   </AnalyzeAction>
+   <ArchiveAction
+      buildConfiguration = "Release"
+      revealArchiveInOrganizer = "YES">
+   </ArchiveAction>
+</Scheme>

+ 7 - 0
ios/Runner.xcworkspace/contents.xcworkspacedata

@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Workspace
+   version = "1.0">
+   <FileRef
+      location = "group:Runner.xcodeproj">
+   </FileRef>
+</Workspace>

+ 8 - 0
ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist

@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+	<key>IDEDidComputeMac32BitWarning</key>
+	<true/>
+</dict>
+</plist>

+ 8 - 0
ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings

@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+	<key>PreviewsEnabled</key>
+	<false/>
+</dict>
+</plist>

+ 13 - 0
ios/Runner/AppDelegate.swift

@@ -0,0 +1,13 @@
+import UIKit
+import Flutter
+
+@UIApplicationMain
+@objc class AppDelegate: FlutterAppDelegate {
+  override func application(
+    _ application: UIApplication,
+    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
+  ) -> Bool {
+    GeneratedPluginRegistrant.register(with: self)
+    return super.application(application, didFinishLaunchingWithOptions: launchOptions)
+  }
+}

+ 122 - 0
ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json

@@ -0,0 +1,122 @@
+{
+  "images" : [
+    {
+      "size" : "20x20",
+      "idiom" : "iphone",
+      "filename" : "Icon-App-20x20@2x.png",
+      "scale" : "2x"
+    },
+    {
+      "size" : "20x20",
+      "idiom" : "iphone",
+      "filename" : "Icon-App-20x20@3x.png",
+      "scale" : "3x"
+    },
+    {
+      "size" : "29x29",
+      "idiom" : "iphone",
+      "filename" : "Icon-App-29x29@1x.png",
+      "scale" : "1x"
+    },
+    {
+      "size" : "29x29",
+      "idiom" : "iphone",
+      "filename" : "Icon-App-29x29@2x.png",
+      "scale" : "2x"
+    },
+    {
+      "size" : "29x29",
+      "idiom" : "iphone",
+      "filename" : "Icon-App-29x29@3x.png",
+      "scale" : "3x"
+    },
+    {
+      "size" : "40x40",
+      "idiom" : "iphone",
+      "filename" : "Icon-App-40x40@2x.png",
+      "scale" : "2x"
+    },
+    {
+      "size" : "40x40",
+      "idiom" : "iphone",
+      "filename" : "Icon-App-40x40@3x.png",
+      "scale" : "3x"
+    },
+    {
+      "size" : "60x60",
+      "idiom" : "iphone",
+      "filename" : "Icon-App-60x60@2x.png",
+      "scale" : "2x"
+    },
+    {
+      "size" : "60x60",
+      "idiom" : "iphone",
+      "filename" : "Icon-App-60x60@3x.png",
+      "scale" : "3x"
+    },
+    {
+      "size" : "20x20",
+      "idiom" : "ipad",
+      "filename" : "Icon-App-20x20@1x.png",
+      "scale" : "1x"
+    },
+    {
+      "size" : "20x20",
+      "idiom" : "ipad",
+      "filename" : "Icon-App-20x20@2x.png",
+      "scale" : "2x"
+    },
+    {
+      "size" : "29x29",
+      "idiom" : "ipad",
+      "filename" : "Icon-App-29x29@1x.png",
+      "scale" : "1x"
+    },
+    {
+      "size" : "29x29",
+      "idiom" : "ipad",
+      "filename" : "Icon-App-29x29@2x.png",
+      "scale" : "2x"
+    },
+    {
+      "size" : "40x40",
+      "idiom" : "ipad",
+      "filename" : "Icon-App-40x40@1x.png",
+      "scale" : "1x"
+    },
+    {
+      "size" : "40x40",
+      "idiom" : "ipad",
+      "filename" : "Icon-App-40x40@2x.png",
+      "scale" : "2x"
+    },
+    {
+      "size" : "76x76",
+      "idiom" : "ipad",
+      "filename" : "Icon-App-76x76@1x.png",
+      "scale" : "1x"
+    },
+    {
+      "size" : "76x76",
+      "idiom" : "ipad",
+      "filename" : "Icon-App-76x76@2x.png",
+      "scale" : "2x"
+    },
+    {
+      "size" : "83.5x83.5",
+      "idiom" : "ipad",
+      "filename" : "Icon-App-83.5x83.5@2x.png",
+      "scale" : "2x"
+    },
+    {
+      "size" : "1024x1024",
+      "idiom" : "ios-marketing",
+      "filename" : "Icon-App-1024x1024@1x.png",
+      "scale" : "1x"
+    }
+  ],
+  "info" : {
+    "version" : 1,
+    "author" : "xcode"
+  }
+}

BIN
ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png


BIN
ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png


BIN
ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png


BIN
ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png


BIN
ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png


BIN
ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png


BIN
ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png


BIN
ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png


BIN
ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png


BIN
ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png


BIN
ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png


BIN
ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png


BIN
ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png


BIN
ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png


BIN
ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png


+ 23 - 0
ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json

@@ -0,0 +1,23 @@
+{
+  "images" : [
+    {
+      "idiom" : "universal",
+      "filename" : "LaunchImage.png",
+      "scale" : "1x"
+    },
+    {
+      "idiom" : "universal",
+      "filename" : "LaunchImage@2x.png",
+      "scale" : "2x"
+    },
+    {
+      "idiom" : "universal",
+      "filename" : "LaunchImage@3x.png",
+      "scale" : "3x"
+    }
+  ],
+  "info" : {
+    "version" : 1,
+    "author" : "xcode"
+  }
+}

BIN
ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png


BIN
ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png


BIN
ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png


+ 5 - 0
ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md

@@ -0,0 +1,5 @@
+# Launch Screen Assets
+
+You can customize the launch screen with your own desired assets by replacing the image files in this directory.
+
+You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images.

+ 37 - 0
ios/Runner/Base.lproj/LaunchScreen.storyboard

@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="12121" systemVersion="16G29" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
+    <dependencies>
+        <deployment identifier="iOS"/>
+        <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="12089"/>
+    </dependencies>
+    <scenes>
+        <!--View Controller-->
+        <scene sceneID="EHf-IW-A2E">
+            <objects>
+                <viewController id="01J-lp-oVM" sceneMemberID="viewController">
+                    <layoutGuides>
+                        <viewControllerLayoutGuide type="top" id="Ydg-fD-yQy"/>
+                        <viewControllerLayoutGuide type="bottom" id="xbc-2k-c8Z"/>
+                    </layoutGuides>
+                    <view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
+                        <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
+                        <subviews>
+                            <imageView opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" image="LaunchImage" translatesAutoresizingMaskIntoConstraints="NO" id="YRO-k0-Ey4">
+                            </imageView>
+                        </subviews>
+                        <color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
+                        <constraints>
+                            <constraint firstItem="YRO-k0-Ey4" firstAttribute="centerX" secondItem="Ze5-6b-2t3" secondAttribute="centerX" id="1a2-6s-vTC"/>
+                            <constraint firstItem="YRO-k0-Ey4" firstAttribute="centerY" secondItem="Ze5-6b-2t3" secondAttribute="centerY" id="4X2-HB-R7a"/>
+                        </constraints>
+                    </view>
+                </viewController>
+                <placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
+            </objects>
+            <point key="canvasLocation" x="53" y="375"/>
+        </scene>
+    </scenes>
+    <resources>
+        <image name="LaunchImage" width="168" height="185"/>
+    </resources>
+</document>

+ 26 - 0
ios/Runner/Base.lproj/Main.storyboard

@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="10117" systemVersion="15F34" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" initialViewController="BYZ-38-t0r">
+    <dependencies>
+        <deployment identifier="iOS"/>
+        <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="10085"/>
+    </dependencies>
+    <scenes>
+        <!--Flutter View Controller-->
+        <scene sceneID="tne-QT-ifu">
+            <objects>
+                <viewController id="BYZ-38-t0r" customClass="FlutterViewController" sceneMemberID="viewController">
+                    <layoutGuides>
+                        <viewControllerLayoutGuide type="top" id="y3c-jy-aDJ"/>
+                        <viewControllerLayoutGuide type="bottom" id="wfy-db-euE"/>
+                    </layoutGuides>
+                    <view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
+                        <rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
+                        <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
+                        <color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/>
+                    </view>
+                </viewController>
+                <placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
+            </objects>
+        </scene>
+    </scenes>
+</document>

+ 47 - 0
ios/Runner/Info.plist

@@ -0,0 +1,47 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+	<key>CFBundleDevelopmentRegion</key>
+	<string>$(DEVELOPMENT_LANGUAGE)</string>
+	<key>CFBundleDisplayName</key>
+	<string>Youtube</string>
+	<key>CFBundleExecutable</key>
+	<string>$(EXECUTABLE_NAME)</string>
+	<key>CFBundleIdentifier</key>
+	<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
+	<key>CFBundleInfoDictionaryVersion</key>
+	<string>6.0</string>
+	<key>CFBundleName</key>
+	<string>youtube</string>
+	<key>CFBundlePackageType</key>
+	<string>APPL</string>
+	<key>CFBundleShortVersionString</key>
+	<string>$(FLUTTER_BUILD_NAME)</string>
+	<key>CFBundleSignature</key>
+	<string>????</string>
+	<key>CFBundleVersion</key>
+	<string>$(FLUTTER_BUILD_NUMBER)</string>
+	<key>LSRequiresIPhoneOS</key>
+	<true/>
+	<key>UILaunchStoryboardName</key>
+	<string>LaunchScreen</string>
+	<key>UIMainStoryboardFile</key>
+	<string>Main</string>
+	<key>UISupportedInterfaceOrientations</key>
+	<array>
+		<string>UIInterfaceOrientationPortrait</string>
+		<string>UIInterfaceOrientationLandscapeLeft</string>
+		<string>UIInterfaceOrientationLandscapeRight</string>
+	</array>
+	<key>UISupportedInterfaceOrientations~ipad</key>
+	<array>
+		<string>UIInterfaceOrientationPortrait</string>
+		<string>UIInterfaceOrientationPortraitUpsideDown</string>
+		<string>UIInterfaceOrientationLandscapeLeft</string>
+		<string>UIInterfaceOrientationLandscapeRight</string>
+	</array>
+	<key>UIViewControllerBasedStatusBarAppearance</key>
+	<false/>
+</dict>
+</plist>

+ 1 - 0
ios/Runner/Runner-Bridging-Header.h

@@ -0,0 +1 @@
+#import "GeneratedPluginRegistrant.h"

+ 41 - 0
lib/data/api/auth_api.dart

@@ -0,0 +1,41 @@
+import 'package:cloud_firestore/cloud_firestore.dart';
+import 'package:firebase_auth/firebase_auth.dart';
+
+import '../../shared/constats.dart';
+import '../models/user_model.dart';
+
+class AuthApi {
+  Future<UserCredential> login(
+      {required String email, required String password}) {
+    return FirebaseAuth.instance.signInWithEmailAndPassword(
+      email: email,
+      password: password,
+    );
+  }
+
+  Future<UserCredential> register(
+      {required String email, required String password, required String name}) {
+    return FirebaseAuth.instance.createUserWithEmailAndPassword(
+      email: email,
+      password: password,
+    );
+  }
+
+  Future<void> createUser(
+      {required UserModel user}) {
+   return FirebaseFirestore.instance
+        .collection(userCollection)
+        .doc(user.id)
+        .set(user.toMap());
+  }
+
+  Future<DocumentSnapshot<Map<String, dynamic>>> checkUserExistInFirebase(
+      {required String uId}) {
+    return FirebaseFirestore.instance.collection(userCollection).doc(uId).get();
+  }
+
+  Future<DocumentSnapshot<Map<String, dynamic>>> getUserData(
+      {required String uId}) {
+    return FirebaseFirestore.instance.collection(userCollection).doc(uId).get();
+  }
+}

+ 85 - 0
lib/data/api/call_api.dart

@@ -0,0 +1,85 @@
+import 'dart:async';
+
+import 'package:cloud_firestore/cloud_firestore.dart';
+import 'package:dio/dio.dart';
+import 'package:flutter/cupertino.dart';
+
+import '../../shared/constats.dart';
+import '../../shared/dio_helper.dart';
+import '../../shared/network/cache_helper.dart';
+import '../models/call_model.dart';
+
+class CallApi {
+  StreamSubscription<QuerySnapshot<Map<String, dynamic>>>
+      listenToInComingCall() {
+    return FirebaseFirestore.instance
+        .collection(callsCollection)
+        .where('receiverId', isEqualTo: CacheHelper.getString(key: 'uId'))
+        .snapshots()
+        .listen((event) {});
+  }
+
+  StreamSubscription<DocumentSnapshot<Map<String, dynamic>>>
+  listenToCallStatus({required String callId}) {
+    return FirebaseFirestore.instance
+        .collection(callsCollection)
+        .doc(callId)
+        .snapshots()
+        .listen((event) {});
+  }
+
+  Future<void> postCallToFirestore({required CallModel callModel}) {
+    return FirebaseFirestore.instance
+        .collection(callsCollection)
+        .doc(callModel.id)
+        .set(callModel.toMap());
+  }
+
+  Future<void> updateUserBusyStatusFirestore({required CallModel callModel,required bool busy}) {
+    Map<String, dynamic> busyMap = {'busy': busy};
+    return FirebaseFirestore.instance
+        .collection(userCollection)
+        .doc(callModel.callerId)
+        .update(busyMap)
+        .then((value) {
+      FirebaseFirestore.instance
+          .collection(userCollection)
+          .doc(callModel.receiverId)
+          .update(busyMap);
+    });
+  }
+
+  //Sender
+  Future<dynamic> generateCallToken(
+      {required Map<String, dynamic> queryMap}) async {
+    try {
+      var response = await DioHelper.getData(
+          endPoint: fireCallEndpoint,
+          query: queryMap,
+          baseUrl: cloudFunctionBaseUrl);
+      debugPrint('fireVideoCallResp: ${response.data}');
+      if (response.statusCode == 200) {
+        return response.data;
+      } else {
+        throw Exception(
+            'Error: ${response.data} Status Code: ${response.statusCode}');
+      }
+    } on DioError catch (error) {
+      debugPrint("fireVideoCallError: ${error.toString()}");
+    }
+  }
+
+
+  Future<void> updateCallStatus({required String callId,required String status}){
+    return FirebaseFirestore.instance
+        .collection(callsCollection)
+        .doc(callId)
+        .update({'status': status});
+  }
+  Future<void> endCurrentCall({required String callId}){
+    return FirebaseFirestore.instance
+        .collection(callsCollection)
+        .doc(callId)
+        .update({'current': false});
+  }
+}

+ 24 - 0
lib/data/api/home_api.dart

@@ -0,0 +1,24 @@
+import 'dart:async';
+
+import 'package:cloud_firestore/cloud_firestore.dart';
+
+import '../../shared/constats.dart';
+import '../../shared/network/cache_helper.dart';
+
+class HomeApi{
+
+ //Not Used
+ Future<QuerySnapshot<Map<String,dynamic>>> getUsers() async{
+   return FirebaseFirestore.instance.collection(userCollection).where('uId',isNotEqualTo: CacheHelper.getString(key: 'uId')).get();
+  }
+
+  StreamSubscription<QuerySnapshot<Map<String, dynamic>>> getUsersRealTime() {
+   return FirebaseFirestore.instance.collection(userCollection).where('uId',isNotEqualTo: CacheHelper.getString(key: 'uId')).snapshots().listen((event) {});
+ }
+
+ StreamSubscription<QuerySnapshot<Map<String, dynamic>>> getCallHistoryRealTime() {
+  return FirebaseFirestore.instance.collection(callsCollection).orderBy('createAt',descending: true).snapshots().listen((event) {});
+ }
+ 
+ 
+}

+ 60 - 0
lib/data/models/call_model.dart

@@ -0,0 +1,60 @@
+import 'package:youtube/data/models/user_model.dart';
+
+class CallModel {
+  late String id;
+  String? token;
+  String? channelName;
+  String? callerId;
+  String? callerName;
+  String? callerAvatar;
+  String? receiverId;
+  String? receiverName;
+  String? receiverAvatar;
+  String? status;
+  num? createAt;
+  bool? current;
+  UserModel? otherUser; //UI
+
+  CallModel(
+      {required this.id,
+      this.callerId,
+      this.callerName,
+      this.callerAvatar,
+      this.receiverId,
+      this.receiverName,
+      this.receiverAvatar,
+      this.status,
+      this.createAt,this.current});
+
+  CallModel.fromJson(Map<String, dynamic> json) {
+    id = json['id'];
+    token = json['token'];
+    channelName = json['channelName'];
+    callerId = json['callerId'];
+    callerName = json['callerName'];
+    callerAvatar = json['callerAvatar'];
+    receiverId = json['receiverId'];
+    receiverName = json['receiverName'];
+    receiverAvatar = json['receiverAvatar'];
+    status = json['status'];
+    createAt = json['createAt'];
+    current = json['current'];
+  }
+
+  Map<String, dynamic> toMap() {
+    return {
+      'id': id,
+      'token': token,
+      'channelName': channelName,
+      'callerId': callerId,
+      'callerName': callerName,
+      'callerAvatar': callerAvatar,
+      'receiverId': receiverId,
+      'receiverName': receiverName,
+      'receiverAvatar': receiverAvatar,
+      'status': status,
+      'createAt': createAt,
+      'current': current
+    };
+  }
+}

+ 20 - 0
lib/data/models/fcm_payload_model.dart

@@ -0,0 +1,20 @@
+class FcmPayloadModel{
+  late String to;
+  late Map<String, dynamic> data;
+
+  FcmPayloadModel({required this.to, required this.data});
+
+  FcmPayloadModel.fromJson(Map<String,dynamic> json)
+  {
+    to = json['to'];
+    data = json['data'];
+  }
+
+  Map<String,dynamic> toMap()
+  {
+    return {
+      'to':to,
+      'data':data,
+    };
+  }
+}

+ 47 - 0
lib/data/models/user_model.dart

@@ -0,0 +1,47 @@
+class UserModel{
+  late String id;
+  late String name;
+  late String email;
+  late String avatar;
+  bool? busy;
+
+
+  UserModel({required this.name, required this.avatar});
+
+  UserModel.resister({required this.name, required this.id,required this.email,required this.avatar,required this.busy});
+
+  UserModel.fromJsonMap({required Map<String, dynamic> map,required String uId}){
+    id = uId;
+    name = map["name"];
+    email = map["email"];
+    avatar = map["avatar"];
+    busy = map["busy"];
+  }
+
+  Map<String,dynamic> toMap(){
+    return {
+      "uId": id,
+      "name": name,
+      "email": email,
+      "avatar": avatar,
+    };
+  }
+}
+
+class UserFcmTokenModel{
+  late String uId, token;
+
+  UserFcmTokenModel({required this.uId, required this.token});
+
+  UserFcmTokenModel.fromJson(Map<String, dynamic> json) {
+    uId = json['uId'];
+    token = json['token'];
+  }
+
+  Map<String, dynamic> toMap() {
+    return {
+      'uId': uId,
+      'token': token,
+    };
+  }
+}

+ 80 - 0
lib/main.dart

@@ -0,0 +1,80 @@
+import 'dart:convert';
+
+import 'package:firebase_core/firebase_core.dart';
+import 'package:firebase_messaging/firebase_messaging.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+import 'package:youtube/presentaion/cubit/auth/auth_cubit.dart';
+import 'package:youtube/presentaion/cubit/home/home_cubit.dart';
+import 'package:youtube/routes.dart';
+import 'package:youtube/services/fcm/firebase_notification_handler.dart';
+import 'package:youtube/shared/dio_helper.dart';
+import 'package:youtube/shared/network/cache_helper.dart';
+import 'package:youtube/shared/theme.dart';
+
+import 'shared/bloc_observer.dart';
+
+Future<void> main() async {
+  WidgetsFlutterBinding.ensureInitialized();
+
+  await CacheHelper.init();
+
+  await Firebase.initializeApp();
+
+  DioHelper.init();
+
+  //Handle FCM background
+  FirebaseMessaging.onBackgroundMessage(_backgroundHandler);
+
+  BlocOverrides.runZoned(
+      () => runApp(MyApp(
+            appRouter: AppRouter(),
+          )),
+      blocObserver: AppBlocObserver());
+}
+
+class MyApp extends StatelessWidget {
+  final AppRouter appRouter;
+
+  const MyApp({Key? key, required this.appRouter}) : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    return MultiBlocProvider(
+      providers: [
+        BlocProvider(
+          create: (_) =>
+              AuthCubit()..getUserData(uId: CacheHelper.getString(key: 'uId')),
+        ),
+        BlocProvider(
+          create: (_) => HomeCubit()
+            ..listenToInComingCalls()
+            ..getUsersRealTime()
+            ..getCallHistoryRealTime()
+            ..initFcm(context)
+            ..updateFcmToken(uId: CacheHelper.getString(key: 'uId')),
+        ),
+      ],
+      child: MaterialApp(
+        title: 'Firebase agora videochat',
+        debugShowCheckedModeBanner: false,
+        theme: appTheme,
+        onGenerateRoute: appRouter.onGenerateRoute,
+      ),
+    );
+  }
+}
+
+Future<void> _backgroundHandler(RemoteMessage message) async {
+  await Firebase.initializeApp();
+  await CacheHelper.init();
+  if (message.data['type'] == 'call') {
+    Map<String, dynamic> bodyMap = jsonDecode(message.data['body']);
+    await CacheHelper.saveData(
+        key: 'terminateIncomingCallData', value: jsonEncode(bodyMap));
+  }
+  FirebaseNotifications.showNotification(
+      title: message.data['title'],
+      body: message.data['body'],
+      type: message.data['type']);
+}

+ 84 - 0
lib/presentaion/cubit/auth/auth_cubit.dart

@@ -0,0 +1,84 @@
+import 'package:flutter/cupertino.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+import 'package:youtube/data/models/user_model.dart';
+
+import '../../../data/api/auth_api.dart';
+import 'auth_state.dart';
+
+class AuthCubit extends Cubit<AuthState> {
+  AuthCubit() : super(AuthInitial());
+
+  static AuthCubit get(context) => BlocProvider.of(context);
+
+  late UserModel currentUser;
+
+  final _authApi = AuthApi();
+
+  void login({required String email, required String password}) async {
+    emit(LoadingLoginState());
+    _authApi.login(email: email, password: password).then((value){
+      debugPrint(value.user!.email);
+      checkUserExistInFirebase(uId: value.user!.uid);
+    }).catchError((onError){
+      emit(ErrorLoginState(onError.toString()));
+    });
+  }
+
+  void register(
+      {required String email,
+      required String password,
+      required String name}) async {
+    emit(LoadingRegisterState());
+    _authApi.register(email: email, password: password, name: name).then((value){
+      createUser(
+        name: name,
+        email: email,
+        uId: value.user!.uid,
+      );
+    }).catchError((onError){
+      emit(ErrorRegisterState(onError.toString()));
+    });
+  }
+
+  void checkUserExistInFirebase({required String uId}) {
+    _authApi.checkUserExistInFirebase(uId: uId).then((user) {
+      if (user.exists) {
+        currentUser = UserModel.fromJsonMap(map: user.data()!, uId: uId);
+        emit(SuccessLoginState(uId));
+      } else {
+        emit(ErrorLoginState('Account not exist'));
+      }
+    }).catchError((onError){
+      emit(ErrorLoginState(onError.toString()));
+    });
+  }
+
+  void getUserData({required String uId}) {
+    if (uId.isNotEmpty) {
+      emit(LoadingGetUserDataState());
+      _authApi.getUserData(uId: uId).then((value){
+        if (value.exists) {
+          currentUser =
+              UserModel.fromJsonMap(map: value.data()!, uId: value.id);
+        } else {
+          emit(ErrorGetUserDataState('User not found'));
+        }
+        emit(SuccessGetUserDataState());
+      }).catchError((onError){
+        emit(ErrorGetUserDataState(onError.toString()));
+      });
+    }
+  }
+
+  void createUser(
+      {required String name, required String email, required String uId}) {
+    UserModel user = UserModel.resister(
+        name: name, id: uId, email: email, avatar: 'https://i.pravatar.cc/300',busy: false);
+    _authApi.createUser(user: user).then((value) {
+      currentUser = user;
+      emit(SuccessRegisterState(uId));
+    }).catchError((onError){
+      emit(ErrorRegisterState(onError.toString()));
+    });
+  }
+}

+ 38 - 0
lib/presentaion/cubit/auth/auth_state.dart

@@ -0,0 +1,38 @@
+abstract class AuthState {}
+
+class AuthInitial extends AuthState {}
+
+
+class LoadingRegisterState extends AuthState {}
+class SuccessRegisterState extends AuthState {
+  final String uId;
+
+  SuccessRegisterState(this.uId);
+}
+class ErrorRegisterState extends AuthState {
+  final String errorMessage;
+
+  ErrorRegisterState(this.errorMessage);
+}
+
+
+class LoadingLoginState extends AuthState {}
+class SuccessLoginState extends AuthState {
+  final String uId;
+
+  SuccessLoginState(this.uId);
+}
+class ErrorLoginState extends AuthState {
+  final String errorMessage;
+
+  ErrorLoginState(this.errorMessage);
+}
+
+
+class LoadingGetUserDataState extends AuthState {}
+class SuccessGetUserDataState extends AuthState {}
+class ErrorGetUserDataState extends AuthState {
+  final String errorMessage;
+
+  ErrorGetUserDataState(this.errorMessage);
+}

+ 196 - 0
lib/presentaion/cubit/call/call_cubit.dart

@@ -0,0 +1,196 @@
+import 'dart:async';
+import 'dart:typed_data';
+
+import 'package:audioplayers/audioplayers.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+import 'package:agora_rtc_engine/rtc_engine.dart';
+import 'package:youtube/data/models/call_model.dart';
+import 'package:youtube/presentaion/cubit/home/home_cubit.dart';
+import 'package:youtube/shared/constats.dart';
+
+import '../../../data/api/call_api.dart';
+import 'call_state.dart';
+import 'package:quiver/async.dart';
+
+class CallCubit extends Cubit<CallState> {
+  CallCubit() : super(CallInitial());
+
+  static CallCubit get(context) => BlocProvider.of(context);
+
+
+  //Agora video room
+
+  int? remoteUid;
+  RtcEngine? engine;
+
+
+  Future<void> initAgoraAndJoinChannel({required String channelToken,required String channelName,required bool isCaller}) async {
+    //create the engine
+    engine = await RtcEngine.create(agoraAppId);
+    await engine!.enableVideo();
+    engine!.setEventHandler(
+      RtcEngineEventHandler(
+        joinChannelSuccess: (String channel, int uid, int elapsed) {
+          debugPrint("local user $uid joined");
+        },
+        userJoined: (int uid, int elapsed) {
+          debugPrint("remote user $uid joined");
+          remoteUid = uid;
+          emit(AgoraRemoteUserJoinedEvent());
+        },
+        userOffline: (int uid, UserOfflineReason reason) {
+          debugPrint("remote user $uid left channel");
+          remoteUid = null;
+          emit(AgoraUserLeftEvent());
+        },
+
+      ),
+    );
+
+    //join channel
+    await engine!.joinChannel(agoraTestToken, agoraTestChannelName, null, 0);
+    if(isCaller){
+      emit(AgoraInitForSenderSuccessState());
+      playContactingRing(isCaller: true);
+    }else{
+      emit(AgoraInitForReceiverSuccessState());
+    }
+
+    debugPrint('channelTokenIs $channelToken channelNameIs $channelName');
+  }
+
+  //Sender
+  AudioPlayer assetsAudioPlayer = AudioPlayer();
+
+  Future<void> playContactingRing({required bool isCaller}) async {
+    String audioAsset = "assets/sounds/ringlong.mp3";
+    ByteData bytes = await rootBundle.load(audioAsset);
+    Uint8List  soundBytes = bytes.buffer.asUint8List(bytes.offsetInBytes, bytes.lengthInBytes);
+    int result = await assetsAudioPlayer.playBytes(soundBytes);
+    if(result == 1){ //play success
+      debugPrint("Sound playing successful.");
+    }else{
+      debugPrint("Error while playing sound.");
+    }
+    if(isCaller){
+      startCountdownCallTimer();
+    }
+  }
+
+  int current = 0;
+  late CountdownTimer countDownTimer;
+  void startCountdownCallTimer() {
+    countDownTimer = CountdownTimer(
+      const Duration(seconds: callDurationInSec),
+      const Duration(seconds: 1),
+    );
+    var sub = countDownTimer.listen(null);
+    sub.onData((duration) {
+      current = callDurationInSec - duration.elapsed.inSeconds;
+      debugPrint("DownCount: $current");
+    });
+
+    sub.onDone(() {
+      debugPrint("CallTimeDone");
+      sub.cancel();
+      emit(DownCountCallTimerFinishState());
+    });
+  }
+
+  bool muted = false;
+  Widget muteIcon = const Icon(Icons.keyboard_voice_rounded,color: Colors.black,);
+
+  Future<void> toggleMuted() async {
+    muted = !muted;
+    muteIcon = muted
+        ? const Icon(Icons.mic_off_rounded,color: Colors.black,)
+        : const Icon(Icons.keyboard_voice_rounded,color: Colors.black,);
+    await engine!.muteLocalAudioStream(muted);
+    emit(AgoraToggleMutedState());
+  }
+
+  Future<void> switchCamera() async {
+    await engine!.switchCamera();
+    emit(AgoraSwitchCameraState());
+  }
+
+  //Update Call Status
+  final _callApi = CallApi();
+  void updateCallStatusToUnAnswered({required String callId}){
+    emit(LoadingUnAnsweredVideoChatState());
+    _callApi.updateCallStatus(callId: callId, status: CallStatus.unAnswer.name).then((value) {
+      emit(SuccessUnAnsweredVideoChatState());
+    }).catchError((onError){
+      emit(ErrorUnAnsweredVideoChatState(onError.toString()));
+    });
+  }
+  Future<void> updateCallStatusToCancel({required String callId})async {
+    await _callApi.updateCallStatus(callId: callId, status: CallStatus.cancel.name);
+  }
+  Future<void> updateCallStatusToReject({required String callId})async {
+    await _callApi.updateCallStatus(callId: callId, status: CallStatus.reject.name);
+  }
+  Future<void> updateCallStatusToAccept({required CallModel callModel})async {
+   await _callApi.updateCallStatus(callId: callModel.id, status: CallStatus.accept.name);
+   initAgoraAndJoinChannel(channelToken: agoraTestChannelName, channelName: agoraTestToken, isCaller: false);
+  }
+  Future<void> updateCallStatusToEnd({required String callId})async {
+    await _callApi.updateCallStatus(callId: callId, status: CallStatus.end.name);
+  }
+
+  Future<void> endCurrentCall({required String callId}) async {
+    await _callApi.endCurrentCall(callId: callId);
+  }
+  Future<void> updateUserBusyStatusFirestore({required CallModel callModel}) async{
+    await _callApi.updateUserBusyStatusFirestore(callModel: callModel, busy: false);
+  }
+
+  Future<void> performEndCall({required CallModel callModel}) async{
+    await endCurrentCall(callId: callModel.id);
+    await updateUserBusyStatusFirestore(callModel: callModel);
+
+  }
+
+  StreamSubscription? callStatusStreamSubscription;
+  void listenToCallStatus({required CallModel callModel,required BuildContext context,required bool isReceiver}){
+   var _homeCubit = HomeCubit.get(context);
+   callStatusStreamSubscription = _callApi.listenToCallStatus(callId: callModel.id);
+   callStatusStreamSubscription!.onData((data) {
+      if(data.exists){
+        String status = data.data()!['status'];
+        if(status == CallStatus.accept.name) {
+          _homeCubit.currentCallStatus = CallStatus.accept;
+          debugPrint('acceptStatus');
+          emit(CallAcceptState());
+        }
+        if(status == CallStatus.reject.name) {
+          _homeCubit.currentCallStatus = CallStatus.reject;
+          debugPrint('rejectStatus');
+          callStatusStreamSubscription!.cancel();
+          emit(CallRejectState());
+        }
+        if(status == CallStatus.unAnswer.name) {
+          _homeCubit.currentCallStatus = CallStatus.unAnswer;
+          debugPrint('unAnswerStatusHere');
+          callStatusStreamSubscription!.cancel();
+          emit(CallNoAnswerState());
+        }
+        if(status == CallStatus.cancel.name) {
+          _homeCubit.currentCallStatus = CallStatus.cancel;
+          debugPrint('cancelStatus');
+          callStatusStreamSubscription!.cancel();
+          emit(CallCancelState());
+        }
+        if(status == CallStatus.end.name){
+          _homeCubit.currentCallStatus = CallStatus.end;
+          debugPrint('endStatus');
+          callStatusStreamSubscription!.cancel();
+          emit(CallEndState());
+        }
+      }
+    });
+  }
+}
+

+ 55 - 0
lib/presentaion/cubit/call/call_state.dart

@@ -0,0 +1,55 @@
+
+abstract class CallState {}
+
+class CallInitial extends CallState {}
+
+class DownCountCallTimerFinishState extends CallState {}
+
+
+//Agora video room States
+class AgoraRemoteUserJoinedEvent extends CallState {}
+class AgoraUserLeftEvent extends CallState {}
+
+class AgoraInitAndJoinedSuccessState extends CallState {}
+
+class AgoraInitForSenderSuccessState extends CallState {}
+class AgoraInitForReceiverSuccessState extends CallState {}
+
+class AgoraSwitchCameraState extends CallState {}
+class AgoraToggleMutedState extends CallState {}
+
+
+//Update Call Status
+class LoadingCancelVideoChatState extends CallState {}
+class SuccessCancelVideoChatState extends CallState {}
+class ErrorCancelVideoChatState extends CallState {
+  final String error;
+  ErrorCancelVideoChatState(this.error);
+}
+
+class LoadingRejectVideoChatState extends CallState {}
+class SuccessRejectVideoChatState extends CallState {}
+class ErrorRejectVideoChatState extends CallState {
+  final String error;
+
+  ErrorRejectVideoChatState(this.error);
+}
+
+
+class LoadingUnAnsweredVideoChatState extends CallState {}
+class SuccessUnAnsweredVideoChatState extends CallState {}
+class ErrorUnAnsweredVideoChatState extends CallState {
+  final String error;
+
+  ErrorUnAnsweredVideoChatState(this.error);
+}
+
+
+
+
+//call States
+class CallAcceptState extends CallState {}
+class CallRejectState extends CallState {}
+class CallNoAnswerState extends CallState {}
+class CallCancelState extends CallState {}
+class CallEndState extends CallState {}

+ 238 - 0
lib/presentaion/cubit/home/home_cubit.dart

@@ -0,0 +1,238 @@
+import 'dart:convert';
+
+import 'package:cloud_firestore/cloud_firestore.dart';
+import 'package:firebase_messaging/firebase_messaging.dart';
+import 'package:flutter/cupertino.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+import 'package:youtube/data/api/call_api.dart';
+import 'package:youtube/data/api/home_api.dart';
+import 'package:youtube/data/models/user_model.dart';
+import 'package:youtube/shared/constats.dart';
+
+import '../../../data/models/call_model.dart';
+import '../../../data/models/fcm_payload_model.dart';
+import '../../../services/fcm/firebase_notification_handler.dart';
+import '../../../shared/dio_helper.dart';
+import '../../../shared/network/cache_helper.dart';
+import 'home_state.dart';
+
+enum HomeTypes { users, history }
+
+
+class HomeCubit extends Cubit<HomeState> {
+  HomeCubit() : super(HomeInitial());
+
+  static HomeCubit get(context) => BlocProvider.of(context);
+
+  final firebaseNotifications = FirebaseNotifications();
+
+  void initFcm(context){
+    firebaseNotifications.setUpFcm(context: context, onForegroundClickCallNotify: (String payload){
+      debugPrint('Foreground Click Call Notify: $payload');
+    });
+  }
+
+
+  void updateFcmToken({required String uId}) {
+    FirebaseMessaging.instance.getToken().then((token) {
+      UserFcmTokenModel tokenModel = UserFcmTokenModel(token: token!,uId: uId);
+      FirebaseFirestore.instance
+          .collection(tokensCollection)
+          .doc(CacheHelper.getString(key: 'uId'))
+          .set(tokenModel.toMap())
+          .then((value) {
+        debugPrint('User Fcm Token Updated $token');
+      }).catchError((onError) {
+        debugPrint(onError.toString());
+      });
+    });
+  }
+
+  List<UserModel> users = [];
+
+
+  final _homeApi = HomeApi();
+  void getUsersRealTime(){
+      emit(LoadingGetUsersState());
+      _homeApi.getUsersRealTime().onData((data) {
+        if(data.size!=0){
+          users = []; // for realtime update the list
+          for (var element in data.docs) {
+            if(!users.any((e) => e.id == element.id)){
+              users.add(UserModel.fromJsonMap(map: element.data(),uId: element.id));
+            }
+          }
+          emit(SuccessGetUsersState());
+        }else{
+          emit(ErrorGetUsersState('No users found'));
+        }
+      });
+  }
+
+  List<CallModel> calls = [];
+
+  void getCallHistoryRealTime(){
+      emit(LoadingGetCallHistoryState());
+      _homeApi.getCallHistoryRealTime().onData((data) {
+        if(data.size != 0){
+          calls = []; // for realtime update the list
+          for (var element in data.docs) {
+            if(element.data()['callerId'] == CacheHelper.getString(key: 'uId')
+                || element.data()['receiverId'] == CacheHelper.getString(key: 'uId')){ //As firebase not allow multi where query, so we get all calls and filter it
+              if(!calls.any((e) => e.id == element.id)){
+                var call = CallModel.fromJson(element.data());
+                if(call.callerId == CacheHelper.getString(key: 'uId')){
+                  call.otherUser = UserModel(name: call.receiverName!, avatar: call.receiverAvatar!);
+                }else{
+                  call.otherUser = UserModel(name: call.callerName!, avatar: call.callerAvatar!);
+                }
+                calls.add(call);
+              }
+            }
+          }
+          emit(SuccessGetCallHistoryState());
+        }else{
+          emit(ErrorGetCallHistoryState('No Call History'));
+        }
+      });
+    }
+
+  //#region Get Data by Normal Request
+   void getUser(){
+    FirebaseFirestore.instance
+          .collection(userCollection)
+      .get()
+      .then((value) {
+        if(value.size!=0){
+          for (var element in value.docs) {
+            if(!users.any((e) => e.id == element.id)){
+              users.add(UserModel.fromJsonMap(map: element.data(),uId: element.id));
+            }else{
+              users[users.indexWhere((e) => e.id == element.id)] = UserModel.fromJsonMap(map: element.data(),uId: element.id);
+            }
+          }
+          emit(SuccessGetUsersState());
+        }else{
+          emit(ErrorGetUsersState('No users found'));
+        }
+      });
+    }
+   void getCallHistory(){
+    FirebaseFirestore.instance
+          .collection(callsCollection)
+          .get()
+          .then((value) {
+        debugPrint('sizeVal: ${value.size}');
+        if(value.size != 0){
+          for (var element in value.docs) {
+            if(element.data()['callerId'] == CacheHelper.getString(key: 'uId') || element.data()['receiverId'] == CacheHelper.getString(key: 'uId')){
+              calls.add(CallModel.fromJson(element.data()));
+            }
+
+          }
+          emit(SuccessGetCallHistoryState());
+        }else{
+          emit(ErrorGetCallHistoryState('No Call History'));
+        }
+      }).catchError((onError){
+        emit(ErrorGetCallHistoryState(onError.toString()));
+      });
+    }
+ //#endregion
+
+
+  //Call Logic ________________________________
+  final _callApi = CallApi();
+  bool fireCallLoading = false;
+  Future<void> fireVideoCall({required CallModel callModel}) async {
+    fireCallLoading = true;
+    emit(LoadingFireVideoCallState());
+      //1-generate call token
+     Map<String,dynamic> queryMap = {
+       'channelName' : 'channel_${UniqueKey().hashCode.toString()}',
+       'uid' : callModel.callerId,
+     };
+     _callApi.generateCallToken(queryMap: queryMap).then((value){
+       callModel.token = value['token'];
+       callModel.channelName = value['channel_name'];
+       //2-post call in Firebase
+       postCallToFirestore(callModel: callModel);
+     }).catchError((onError){
+       fireCallLoading = false;
+       //For test
+       callModel.token = agoraTestToken;
+       callModel.channelName = agoraTestChannelName;
+        postCallToFirestore(callModel: callModel);
+       emit(ErrorFireVideoCallState(onError.toString()));
+     });
+
+  }
+
+  void postCallToFirestore({required CallModel callModel}) {
+    _callApi.postCallToFirestore(callModel: callModel).then((value){
+      //3-update user busy status in Firebase
+      _callApi.updateUserBusyStatusFirestore(callModel: callModel, busy: true).then((value) {
+        fireCallLoading = false;
+        //4-send notification to receiver
+        sendNotificationForIncomingCall(callModel: callModel);
+      }).catchError((onError){
+        fireCallLoading = false;
+        emit(ErrorUpdateUserBusyStatus(onError.toString()));
+      });
+    }).catchError((onError){
+      fireCallLoading = false;
+      emit(ErrorPostCallToFirestoreState(onError.toString()));
+    });
+  }
+
+  void sendNotificationForIncomingCall({required CallModel callModel}) {
+    FirebaseFirestore.instance
+    .collection(tokensCollection)
+    .doc(callModel.receiverId)
+    .get()
+    .then((value) {
+      if(value.exists){
+        Map<String, dynamic> bodyMap = {
+          'type': 'call',
+          'title': 'New call',
+          'body': jsonEncode(callModel.toMap())
+        };
+        FcmPayloadModel fcmSendData = FcmPayloadModel(to: value.data()!['token'],data: bodyMap);
+
+        DioHelper.postData(
+          data:  fcmSendData.toMap(), baseUrl: 'https://fcm.googleapis.com/', endPoint: 'fcm/send',
+        ).then((value) {
+          debugPrint('SendNotifySuccess ${value.data.toString()}');
+          emit(SuccessFireVideoCallState(callModel: callModel));
+        }).catchError((onError){
+          debugPrint('Error when send Notify: $onError');
+          fireCallLoading = false;
+          emit(ErrorSendNotification(onError.toString()));
+        });
+      }
+    }).catchError((onError){
+      debugPrint('Error when get user token: $onError');
+      fireCallLoading = false;
+      emit(ErrorSendNotification(onError.toString()));
+    });
+  }
+ // CallModel inComingCall;
+
+  CallStatus? currentCallStatus;
+  void listenToInComingCalls() {
+    _callApi.listenToInComingCall().onData((data) {
+      if(data.size!=0){
+        for (var element in data.docs) {
+          if(element.data()['current'] == true){
+            String status = element.data()['status'];
+            if(status == CallStatus.ringing.name){
+              currentCallStatus = CallStatus.ringing;
+              debugPrint('ringingStatus');
+              emit(SuccessInComingCallState(callModel: CallModel.fromJson(element.data())));
+            }
+          }
+        }
+      }
+    });
+  }
+}

+ 67 - 0
lib/presentaion/cubit/home/home_state.dart

@@ -0,0 +1,67 @@
+
+import 'package:youtube/data/models/call_model.dart';
+
+abstract class HomeState {}
+
+class HomeInitial extends HomeState {
+}
+
+class ChangeCurrentPageState extends HomeState {
+}
+
+class LoadingGetUsersState extends HomeState {
+}
+class SuccessGetUsersState extends HomeState {
+}
+class ErrorGetUsersState extends HomeState {
+  final String message;
+
+  ErrorGetUsersState(this.message);
+}
+
+
+class LoadingGetCallHistoryState extends HomeState {
+}
+class SuccessGetCallHistoryState extends HomeState {
+}
+class ErrorGetCallHistoryState extends HomeState {
+  final String message;
+
+   ErrorGetCallHistoryState(this.message);
+}
+
+//Sender
+class LoadingFireVideoCallState extends HomeState {}
+class SuccessFireVideoCallState extends HomeState {
+  final CallModel callModel;
+
+  SuccessFireVideoCallState({required this.callModel});
+}
+class ErrorFireVideoCallState extends HomeState {
+  final String message;
+  ErrorFireVideoCallState(this.message);
+}
+
+class ErrorPostCallToFirestoreState extends HomeState {
+  final String message;
+  ErrorPostCallToFirestoreState(this.message);
+}
+
+class ErrorUpdateUserBusyStatus extends HomeState {
+  final String message;
+  ErrorUpdateUserBusyStatus(this.message);
+}
+
+class ErrorSendNotification extends HomeState {
+  final String message;
+  ErrorSendNotification(this.message);
+}
+
+
+//Incoming Call
+class SuccessInComingCallState extends HomeState {
+  final CallModel callModel;
+
+  SuccessInComingCallState({required this.callModel});
+}
+

+ 261 - 0
lib/presentaion/screens/auth_screen.dart

@@ -0,0 +1,261 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+import 'package:youtube/shared/constats.dart';
+
+import '../../shared/network/cache_helper.dart';
+import '../../shared/shared_widgets.dart';
+import '../../shared/theme.dart';
+import '../cubit/auth/auth_cubit.dart';
+import '../cubit/auth/auth_state.dart';
+
+enum AuthOptions {
+  login('Login', LoginView()),
+  register('Register', RegisterView());
+
+  final String name;
+  final Widget view;
+
+  const AuthOptions(this.name, this.view);
+}
+
+class AuthScreen extends StatelessWidget {
+  const AuthScreen({Key? key}) : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    return Scaffold(
+      appBar: AppBar(),
+      body: BlocListener<AuthCubit, AuthState>(
+        listener: (BuildContext context, state) {
+          if (state is SuccessLoginState) {
+            CacheHelper.saveData(key: 'uId', value: state.uId).then((value) {
+              Navigator.pushNamedAndRemoveUntil(
+                  context, homeScreen, (route) => false);
+            });
+          }
+          if (state is SuccessRegisterState) {
+            CacheHelper.saveData(key: 'uId', value: state.uId).then((value) {
+              Navigator.pushNamedAndRemoveUntil(
+                  context, homeScreen, (route) => false);
+            });
+          }
+          if (state is ErrorRegisterState) {
+            showToast(
+              msg: state.errorMessage,
+            );
+          }
+          if (state is ErrorLoginState) {
+            showToast(
+              msg: state.errorMessage,
+            );
+          }
+          if (state is ErrorGetUserDataState) {
+            showToast(
+              msg: state.errorMessage,
+            );
+          }
+          if (state is SuccessGetUserDataState) {
+            Navigator.pushNamedAndRemoveUntil(
+                context, homeScreen, (route) => false);
+          }
+        },
+        child: GestureDetector(
+          onTap: () => FocusScope.of(context).requestFocus(FocusNode()),
+          child: Padding(
+            padding: const EdgeInsets.all(8.0),
+            child: DefaultTabController(
+              length: AuthOptions.values.length,
+              child: Column(
+                mainAxisAlignment: MainAxisAlignment.center,
+                children: [
+                  TabBar(
+                    isScrollable: true,
+                    unselectedLabelColor: Colors.grey[400],
+                    indicatorColor: defaultColor,
+                    labelColor: defaultColor,
+                    tabs: AuthOptions.values
+                        .map((e) => Tab(
+                              child: Text(
+                                e.name,
+                                style: const TextStyle(fontSize: 15),
+                              ),
+                            ))
+                        .toList(),
+                  ),
+                  Expanded(
+                    child: TabBarView(
+                      children: AuthOptions.values.map((e) => e.view).toList(),
+                    ),
+                  ),
+                ],
+              ),
+            ),
+          ),
+        ),
+      ),
+    );
+  }
+}
+
+class LoginView extends StatefulWidget {
+  const LoginView({Key? key}) : super(key: key);
+
+  @override
+  State<LoginView> createState() => _LoginViewState();
+}
+
+class _LoginViewState extends State<LoginView> {
+  final emailController = TextEditingController();
+
+  final passwordController = TextEditingController();
+
+  @override
+  void dispose() {
+    emailController.dispose();
+    passwordController.dispose();
+    super.dispose();
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return BlocBuilder<AuthCubit, AuthState>(
+      builder: (BuildContext context, state) {
+        var authCubit = AuthCubit.get(context);
+        return Padding(
+          padding: const EdgeInsets.all(8.0),
+          child: Column(
+            mainAxisAlignment: MainAxisAlignment.center,
+            children: [
+              TextField(
+                controller: emailController,
+                decoration: const InputDecoration(
+                  border: OutlineInputBorder(),
+                  labelText: 'Email',
+                  hintText: 'Enter your email address',
+                ),
+              ),
+              const SizedBox(
+                height: 10.0,
+              ),
+              TextField(
+                controller: passwordController,
+                decoration: const InputDecoration(
+                  border: OutlineInputBorder(),
+                  labelText: 'Password',
+                  hintText: 'Enter your password',
+                ),
+              ),
+              const SizedBox(
+                height: 20.0,
+              ),
+              state is! LoadingLoginState
+                  ? ElevatedButton.icon(
+                      onPressed: () {
+                        if (emailController.text.isNotEmpty &&
+                            passwordController.text.isNotEmpty) {
+                          authCubit.login(
+                              email: emailController.text,
+                              password: passwordController.text);
+                        } else {
+                          showToast(msg: 'Please fill req data');
+                        }
+                      },
+                      icon: const Icon(Icons.login),
+                      label: const Text('Login'))
+                  : const CircularProgressIndicator(),
+            ],
+          ),
+        );
+      },
+    );
+  }
+}
+
+class RegisterView extends StatefulWidget {
+  const RegisterView({Key? key}) : super(key: key);
+
+  @override
+  State<RegisterView> createState() => _RegisterViewState();
+}
+
+class _RegisterViewState extends State<RegisterView> {
+  final nameController = TextEditingController();
+
+  final emailController = TextEditingController();
+
+  final passwordController = TextEditingController();
+
+  @override
+  void dispose() {
+    nameController.dispose();
+    emailController.dispose();
+    passwordController.dispose();
+    super.dispose();
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return BlocBuilder<AuthCubit, AuthState>(
+      builder: (BuildContext context, state) {
+        var authCubit = AuthCubit.get(context);
+        return Padding(
+          padding: const EdgeInsets.all(8.0),
+          child: Column(
+            mainAxisAlignment: MainAxisAlignment.center,
+            children: [
+              TextField(
+                controller: nameController,
+                decoration: const InputDecoration(
+                  border: OutlineInputBorder(),
+                  labelText: 'Name',
+                  hintText: 'Enter your name',
+                ),
+              ),
+              const SizedBox(
+                height: 10.0,
+              ),
+              TextField(
+                controller: emailController,
+                decoration: const InputDecoration(
+                  border: OutlineInputBorder(),
+                  labelText: 'Email',
+                  hintText: 'Enter your email address',
+                ),
+              ),
+              const SizedBox(
+                height: 10.0,
+              ),
+              TextField(
+                  controller: passwordController,
+                  decoration: const InputDecoration(
+                    border: OutlineInputBorder(),
+                    labelText: 'Password',
+                    hintText: 'Enter your password',
+                  )),
+              const SizedBox(
+                height: 10.0,
+              ),
+              state is! LoadingRegisterState
+                  ? ElevatedButton.icon(
+                      onPressed: () {
+                        if (emailController.text.isNotEmpty &&
+                            passwordController.text.isNotEmpty &&
+                            nameController.text.isNotEmpty) {
+                          authCubit.register(
+                              email: emailController.text,
+                              password: passwordController.text,
+                              name: nameController.text);
+                        } else {
+                          showToast(msg: 'Please fill req data');
+                        }
+                      },
+                      icon: const Icon(Icons.login),
+                      label: const Text('Register'))
+                  : const CircularProgressIndicator(),
+            ],
+          ),
+        );
+      },
+    );
+  }
+}

+ 258 - 0
lib/presentaion/screens/call_screen.dart

@@ -0,0 +1,258 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+import 'package:modal_progress_hud_nsn/modal_progress_hud_nsn.dart';
+import 'package:youtube/data/models/call_model.dart';
+import 'package:youtube/presentaion/cubit/call/call_cubit.dart';
+
+import '../../shared/constats.dart';
+import '../../shared/shared_widgets.dart';
+import '../cubit/call/call_state.dart';
+import '../widgets/call_widgets/default_circle_image.dart';
+import '../widgets/call_widgets/user_info_header.dart';
+import 'package:agora_rtc_engine/rtc_local_view.dart' as rtc_local_view;
+import 'package:agora_rtc_engine/rtc_remote_view.dart' as rtc_remote_view;
+import 'package:permission_handler/permission_handler.dart';
+
+class CallScreen extends StatefulWidget {
+  final bool isReceiver;
+  final CallModel callModel;
+  const CallScreen({Key? key, required this.isReceiver, required this.callModel}) : super(key: key);
+
+  @override
+  State<CallScreen> createState() => _CallScreenState();
+}
+
+class _CallScreenState extends State<CallScreen> {
+  late CallCubit _callCubit;
+
+  @override
+  void initState() {
+    super.initState();
+    _callCubit = CallCubit.get(context);
+    rePermission();
+    _callCubit.listenToCallStatus(callModel: widget.callModel, context: context,isReceiver: widget.isReceiver);
+    if(!widget.isReceiver){ //Caller
+      _callCubit.initAgoraAndJoinChannel(channelToken: widget.callModel.token!,channelName: widget.callModel.channelName!,isCaller:true);
+    }else{ //Receiver
+      _callCubit.playContactingRing(isCaller: false);
+    }
+  }
+
+  @override
+  void dispose() {
+    if(_callCubit.engine!=null){
+      _callCubit.engine!.destroy();
+    }
+    _callCubit.assetsAudioPlayer.dispose();
+    if(!widget.isReceiver){ //Sender
+      _callCubit.countDownTimer.cancel();
+    }
+    _callCubit.performEndCall(callModel: widget.callModel);
+    super.dispose();
+  }
+
+  Future<void> rePermission() async {
+    // retrieve permissions
+    await [Permission.microphone, Permission.camera].request();
+  }
+  @override
+  Widget build(BuildContext context) {
+    return BlocConsumer<CallCubit,CallState>(
+      listener: (BuildContext context, Object? state) {
+        if(state is ErrorUnAnsweredVideoChatState){
+          showToast(msg: 'UnExpected Error!: ${state.error}');
+        }
+        if(state is DownCountCallTimerFinishState){
+          if(_callCubit.remoteUid==null){
+            _callCubit.updateCallStatusToUnAnswered(callId: widget.callModel.id);
+          }
+        }
+
+        if(state is AgoraRemoteUserJoinedEvent){
+          //remote user join channel
+          if(!widget.isReceiver){ //Caller
+            _callCubit.countDownTimer.cancel();
+          }
+          _callCubit.assetsAudioPlayer.stop(); //Sender, Receiver
+
+        }
+
+        //Call States
+        if(state is CallNoAnswerState){
+          if(!widget.isReceiver){ //Caller
+            showToast(msg: 'No response!');
+          }
+          Navigator.pop(context);
+        }
+        if(state is CallCancelState){
+          if(widget.isReceiver){ //Receiver
+            showToast(msg: 'Caller cancel the call!');
+          }
+          Navigator.pop(context);
+        }
+        if(state is CallRejectState){
+          if(!widget.isReceiver){ //Caller
+            showToast(msg: 'Receiver reject the call!');
+          }
+          Navigator.pop(context);
+        }
+        if(state is CallEndState){
+          showToast(msg: 'Call ended!');
+          Navigator.pop(context);
+        }
+      },
+      builder: (BuildContext context, state) {
+        var cubit = CallCubit.get(context);
+        return ModalProgressHUD(
+          inAsyncCall: false,
+          child: WillPopScope(
+            onWillPop: () async {  return false; },
+            child: Scaffold(
+              body: Stack(
+                alignment: AlignmentDirectional.center,
+                children: [
+                  cubit.remoteUid == null ? !widget.isReceiver ? Container(color: Colors.red,child: const rtc_local_view.SurfaceView()) : Container( //res
+                    decoration:  BoxDecoration(
+                      image: DecorationImage(
+                        image:  widget.callModel.callerAvatar!.isNotEmpty ? NetworkImage(
+                          widget.callModel.callerAvatar!,
+                        ) :  const NetworkImage(
+                          'https://picsum.photos/200/300',
+                        ),
+                        fit: BoxFit.cover,
+                      ),
+                    ),
+                  ) : Stack(
+                    children: [
+                      Center(
+                        child: _remoteVideo(remoteUserId: cubit.remoteUid!),
+                      ),
+                      const Align(
+                        alignment: Alignment.bottomRight,
+                        child: SizedBox(
+                          width: 122,
+                          height: 219.0,
+                          child: Center(
+                            child: rtc_local_view.SurfaceView(),
+                          ),
+                        ),
+                      ),
+                    ],
+                  ),
+                  Container(
+                    padding: const EdgeInsets.all(15.0),
+                    child: Column(
+                      crossAxisAlignment: CrossAxisAlignment.center,
+                      children: [
+                        const SizedBox(height: 50.0,),
+                        !widget.isReceiver ?  UserInfoHeader( //Caller -> Show Receiver INFO
+                          avatar: widget.callModel.receiverAvatar!,
+                          name: widget.callModel.receiverName!,
+                        ) : UserInfoHeader( //Receiver -> Show Caller INFO
+                          avatar: widget.callModel.callerAvatar!,
+                          name: widget.callModel.callerName!,
+                        ),
+                        const SizedBox(height: 30.0,),
+                        cubit.remoteUid ==null ? Expanded(
+                          child: widget.isReceiver ?  Text('${widget.callModel.callerName} is calling you..',style: const TextStyle(color: Colors.white,fontSize: 39.0),)
+                              :const Text('Contacting..',style: TextStyle(color: Colors.white,fontSize: 39.0),),
+                        ) : Expanded(child: Container()),
+
+                        cubit.remoteUid ==null ? Row(
+                          mainAxisAlignment: MainAxisAlignment.center,
+                          children: [
+                            widget.isReceiver ? Expanded(
+                              child: InkWell(
+                                onTap: (){
+                                  //receiverAcceptVideoChat
+                                  _callCubit.updateCallStatusToAccept(callModel: widget.callModel);
+                                },
+                                child: Container(
+                                  decoration: BoxDecoration(
+                                    borderRadius: BorderRadius.circular(15.0),
+                                    color: Colors.green,
+                                  ),
+                                  child: const Center(
+                                    child:  Padding(
+                                      padding: EdgeInsets.symmetric(horizontal: 30.0,vertical: 8.0),
+                                      child: Text('Acceptance',style: TextStyle(color: Colors.white,fontSize: 13.0),),
+                                    ),
+                                  ),
+                                ),
+                              ),
+                            ) : Container(),
+                            widget.isReceiver ? const SizedBox(width: 15.0,) : Container(),
+                            Expanded(
+                              child: InkWell(
+                                onTap: (){
+                                  if(widget.isReceiver){
+                                    //receiverRejectVideoChat
+                                    _callCubit.updateCallStatusToReject(callId: widget.callModel.id);
+                                  }else{
+                                    //callerCancelVideoChat
+                                    _callCubit.updateCallStatusToCancel(callId: widget.callModel.id);
+                                  }
+                                },
+                                child: Container(
+                                  decoration: BoxDecoration(
+                                    borderRadius: BorderRadius.circular(15.0),
+                                    color: Colors.red,
+                                  ),
+                                  child:  Center(
+                                    child:  Padding(
+                                      padding: const EdgeInsets.symmetric(horizontal: 30.0,vertical: 8.0),
+                                      child:  Text(widget.isReceiver ? 'Reject' : 'Cancel',style: const TextStyle(color: Colors.white,fontSize: 13.0),),
+                                    ),
+                                  ),
+                                ),
+                              ),
+                            )
+                          ],
+                        )
+                            : Row(
+                          mainAxisAlignment: MainAxisAlignment.center,
+                          children: [
+                            Expanded(
+                              child: GestureDetector(
+                                onTap: (){
+                                  cubit.switchCamera();
+                                },
+                                child: const DefaultCircleImage(bgColor: Colors.white ,image: Icon(Icons.switch_camera_outlined,color: Colors.black,),center: true,width: 42,height: 42,),
+                              ),
+                            ),
+                            Expanded(
+                              child: GestureDetector(
+                                  onTap: (){
+                                    cubit.updateCallStatusToEnd(callId: widget.callModel.id);
+                                  },
+                                  child: const DefaultCircleImage(bgColor: Colors.red ,image: Icon(Icons.call_end_rounded,color: Colors.white,),center: true,width: 55,height: 55,)
+                              ),
+                            ),
+                            Expanded(
+                              child: GestureDetector(
+                                onTap: (){
+                                  cubit.toggleMuted();
+                                },
+                                child: DefaultCircleImage(bgColor: Colors.white ,image: cubit.muteIcon ,center: true,width: 42,height: 42,),
+                              ),
+                            ),
+
+
+                          ],
+                        ),
+                      ],
+                    ),
+                  ),
+                ],
+              ) ,
+            ),
+          ),
+        );
+      },
+    );
+  }
+  // Display remote user's video
+  Widget _remoteVideo({required int remoteUserId}) {
+    return rtc_remote_view.SurfaceView(uid: remoteUserId,channelId: /*widget.callModel.channelName!*/ agoraTestChannelName,);
+  }
+}

+ 141 - 0
lib/presentaion/screens/home_screen.dart

@@ -0,0 +1,141 @@
+import 'dart:convert';
+
+import 'package:flutter/material.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+import 'package:modal_progress_hud_nsn/modal_progress_hud_nsn.dart';
+import 'package:youtube/data/models/call_model.dart';
+import 'package:youtube/presentaion/cubit/home/home_cubit.dart';
+import 'package:youtube/shared/shared_widgets.dart';
+
+import '../../shared/constats.dart';
+import '../../shared/network/cache_helper.dart';
+import '../../shared/theme.dart';
+import '../cubit/auth/auth_cubit.dart';
+import '../cubit/home/home_state.dart';
+import '../views/home_views/home_screen_pageview.dart';
+
+
+
+
+class HomeScreen extends StatefulWidget {
+  const HomeScreen({Key? key}) : super(key: key);
+
+  @override
+  State<HomeScreen> createState() => _HomeScreenState();
+}
+
+class _HomeScreenState extends State<HomeScreen> {
+  @override
+  void initState() {
+    super.initState();
+    debugPrint('UserIdIs: ${CacheHelper.getString(key: 'uId')}');
+    Future.delayed(const Duration(milliseconds: 1000), () {
+      checkInComingTerminatedCall();
+    });
+  }
+
+  checkInComingTerminatedCall() async {
+    if(CacheHelper.getString(key: 'terminateIncomingCallData').isNotEmpty){ //if there is a terminated call
+      Map<String, dynamic> callMap = jsonDecode(CacheHelper.getString(key: 'terminateIncomingCallData'));
+      await CacheHelper.removeData(key: 'terminateIncomingCallData');
+      Navigator.pushNamed(context, callScreen,arguments: [
+        true,
+        CallModel.fromJson(callMap),
+      ]);
+    }
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return Scaffold(
+        appBar: AppBar(
+          centerTitle: true,
+          title: Text('Hello, ${AuthCubit.get(context).currentUser.name}'),
+        ),
+        body: BlocConsumer<HomeCubit, HomeState>(
+          listener: (context, state) {
+            //GetUserData States
+            if (state is ErrorGetUsersState) {
+              showToast(msg: state.message);
+            }
+            if (state is ErrorGetCallHistoryState) {
+            //  showToast(msg: state.message);
+            }
+            //FireCall States
+            if (state is ErrorFireVideoCallState) {
+             // showToast(msg: state.message);
+            }
+            if (state is ErrorPostCallToFirestoreState) {
+              showToast(msg: state.message);
+            }
+            if(state is ErrorUpdateUserBusyStatus){
+              showToast(msg: state.message);
+            }
+            if (state is SuccessFireVideoCallState) {
+              Navigator.pushNamed(context, callScreen, arguments: [false,state.callModel]);
+            }
+
+            //Receiver Call States
+            if(state is SuccessInComingCallState){
+              Navigator.pushNamed(context, callScreen, arguments: [true,state.callModel]);
+            }
+          },
+          builder: (context, state) {
+            var homeCubit = HomeCubit.get(context);
+            return ModalProgressHUD(
+              inAsyncCall: homeCubit.fireCallLoading,
+              child: Padding(
+                padding: const EdgeInsets.all(8.0),
+                child: DefaultTabController(
+                  length: HomeTypes.values.length,
+                  child: Column(
+                    crossAxisAlignment: CrossAxisAlignment.center,
+                    children: [
+                      TabBar(
+                        isScrollable: true,
+                        unselectedLabelColor: Colors.grey[400],
+                        indicatorColor: defaultColor,
+                        labelColor: defaultColor,
+                        tabs: HomeTypes.values
+                            .map((e) => Tab(
+                                  child: Text(
+                                    e.name,
+                                    style: const TextStyle(fontSize: 15),
+                                  ),
+                                ))
+                            .toList(),
+                      ),
+                      (state is LoadingGetUsersState ||
+                              state is LoadingGetCallHistoryState)
+                          ? const Padding(
+                              padding: EdgeInsets.symmetric(
+                                  vertical: 8.0, horizontal: 2.0),
+                              child: LinearProgressIndicator(
+                                backgroundColor: Colors.grey,
+                              ),
+                            )
+                          : Container(),
+                      const SizedBox(
+                        height: 10.0,
+                      ),
+                      Expanded(
+                        child: TabBarView(
+                          physics: const BouncingScrollPhysics(),
+                          children: HomeTypes.values.map((e) {
+                            return HomeScreenPageView(
+                              users: homeCubit.users,
+                              calls: homeCubit.calls,
+                              isUsers: e == HomeTypes.users ? true : false,
+                            );
+                          }).toList(),
+                        ),
+                      ),
+                    ],
+                  ),
+                ),
+              ),
+            );
+          },
+        ));
+  }
+}

+ 34 - 0
lib/presentaion/views/home_views/call_item_view.dart

@@ -0,0 +1,34 @@
+import 'package:cached_network_image/cached_network_image.dart';
+import 'package:flutter/material.dart';
+import 'package:youtube/data/models/call_model.dart';
+import 'package:intl/intl.dart';
+
+import '../../../shared/theme.dart';
+
+class CallItemView extends StatelessWidget {
+  final CallModel callModel;
+  const CallItemView({Key? key, required this.callModel}) : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    return ListTile(
+      subtitle: Text('${callModel.status}'),
+      title: Row(
+        children: [
+          callModel.otherUser!.avatar.isNotEmpty ? CircleAvatar(
+            backgroundColor: defaultColor,
+            radius: 22.0,
+            backgroundImage: CachedNetworkImageProvider(
+              callModel.otherUser!.avatar,
+            ),
+          ) : const Icon(Icons.person),
+          const SizedBox(width: 10.0,),
+          Expanded(child: Text(callModel.otherUser!.name)),
+          Text(DateFormat('dd/MM/yyyy HH:mm:ss').format(
+              DateTime.fromMillisecondsSinceEpoch(
+                  callModel.createAt!.toInt())),style: Theme.of(context).textTheme.caption,)
+        ],
+      ),
+    );
+  }
+}

+ 54 - 0
lib/presentaion/views/home_views/home_screen_pageview.dart

@@ -0,0 +1,54 @@
+import 'package:flutter/material.dart';
+import 'package:youtube/presentaion/views/home_views/user_item_view.dart';
+
+import '../../../data/models/call_model.dart';
+import '../../../data/models/user_model.dart';
+import '../../../shared/constats.dart';
+import '../../../shared/network/cache_helper.dart';
+import '../../../shared/shared_widgets.dart';
+import '../../cubit/auth/auth_cubit.dart';
+import '../../cubit/home/home_cubit.dart';
+import 'call_item_view.dart';
+
+class HomeScreenPageView extends StatelessWidget {
+  final List<UserModel> users;
+  final List<CallModel> calls;
+  final bool isUsers;
+  const HomeScreenPageView({Key? key, required this.users, required this.calls, required this.isUsers}) : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    return ListView.separated(
+        itemBuilder: (context, index) {
+          if (isUsers) {
+            return UserItemView(
+              userModel: users[index],
+              onCallTap: () {
+                if(!users[index].busy!){
+                  HomeCubit.get(context).fireVideoCall(
+                      callModel: CallModel(
+                          id: 'call_${UniqueKey().hashCode.toString()}',
+                          callerId: CacheHelper.getString(key: 'uId'),
+                          callerAvatar: AuthCubit.get(context).currentUser.avatar,
+                          callerName: AuthCubit.get(context).currentUser.name,
+                          receiverId: users[index].id,
+                          receiverAvatar:users[index].avatar,
+                          receiverName:users[index].name,
+                          status: CallStatus.ringing.name,
+                          createAt: DateTime.now().millisecondsSinceEpoch,
+                        current: true
+                      ));
+                }else{
+                 showToast(msg: 'User is busy');
+                }
+
+              },
+            );
+          } else {
+            return CallItemView(callModel: calls[index]);
+          }
+        },
+        separatorBuilder: (context, index) => const Divider(),
+        itemCount: isUsers ? users.length : calls.length);
+  }
+}

+ 40 - 0
lib/presentaion/views/home_views/user_item_view.dart

@@ -0,0 +1,40 @@
+import 'package:cached_network_image/cached_network_image.dart';
+import 'package:flutter/material.dart';
+import 'package:youtube/data/models/user_model.dart';
+
+import '../../../shared/theme.dart';
+
+class UserItemView extends StatelessWidget {
+  final UserModel userModel;
+  final GestureTapCallback onCallTap;
+
+  const UserItemView(
+      {Key? key, required this.userModel, required this.onCallTap})
+      : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+
+    return ListTile(
+      title: Row(
+        children: [
+          userModel.avatar.isNotEmpty ? CircleAvatar(
+            backgroundColor: defaultColor,
+            radius: 22.0,
+            backgroundImage: CachedNetworkImageProvider(
+              userModel.avatar,
+            ),
+          ) : const Icon(Icons.person),
+          const SizedBox(width: 10.0,),
+          Expanded(child: Text(userModel.name)),
+          GestureDetector(
+              onTap: onCallTap,
+              child: const Icon(
+                Icons.call,
+                color: Colors.green,
+              )),
+        ],
+      ),
+    );
+  }
+}

+ 32 - 0
lib/presentaion/widgets/call_widgets/default_circle_image.dart

@@ -0,0 +1,32 @@
+import 'package:flutter/cupertino.dart';
+
+class DefaultCircleImage extends StatelessWidget {
+  final Widget image;
+  final Color bgColor;
+  final bool center;
+  final double width;
+  final double height;
+  const DefaultCircleImage({
+    Key? key, required this.image,
+    required this.bgColor,
+    this.center = false,
+    this.width = 45,
+    this.height = 45
+  }) : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    return Container(
+      width: width,
+      height: height,
+      child: center? Center(child: image) : Padding(
+        padding: const EdgeInsets.all(13.0),
+        child: image,
+      ),
+      decoration: BoxDecoration(
+        shape: BoxShape.circle,
+        color: bgColor,
+      ),
+    );
+  }
+}

+ 44 - 0
lib/presentaion/widgets/call_widgets/user_info_header.dart

@@ -0,0 +1,44 @@
+import 'package:cached_network_image/cached_network_image.dart';
+import 'package:flutter/material.dart';
+
+import '../../../shared/theme.dart';
+
+class UserInfoHeader extends StatelessWidget {
+  final String avatar;
+  final String name;
+
+
+  const UserInfoHeader(
+      {Key? key,
+      required this.avatar,
+      required this.name,})
+      : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    return Row(
+      children: [
+        CircleAvatar(
+          radius: 24.0,
+          backgroundColor: Colors.grey.withOpacity(0.2),
+          child: avatar.isNotEmpty
+              ? CircleAvatar(
+                  backgroundColor: defaultColor,
+                  radius: 25.0,
+                  backgroundImage: CachedNetworkImageProvider(
+                    avatar,
+                  ),
+                )
+              : const Icon(Icons.person),
+        ),
+        const SizedBox(
+          width: 10.0,
+        ),
+        Text(
+          name,
+          style: const TextStyle(color: Colors.white),
+        ),
+      ],
+    );
+  }
+}

+ 41 - 0
lib/routes.dart

@@ -0,0 +1,41 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+import 'package:youtube/data/models/call_model.dart';
+import 'package:youtube/presentaion/cubit/call/call_cubit.dart';
+import 'package:youtube/presentaion/screens/call_screen.dart';
+import 'package:youtube/presentaion/screens/home_screen.dart';
+import 'package:youtube/presentaion/screens/auth_screen.dart';
+import 'package:youtube/shared/constats.dart';
+
+class AppRouter {
+  Route? onGenerateRoute(RouteSettings routeSettings) {
+    switch (routeSettings.name) {
+      case loginScreen:
+        return MaterialPageRoute(
+          builder: (_) {
+            return const AuthScreen();
+          },
+        );
+      case homeScreen:
+        return MaterialPageRoute(
+          builder: (_) {
+            return const HomeScreen();
+          },
+        );
+
+      case callScreen:
+        List<dynamic> args = routeSettings.arguments as List<dynamic>;
+        final isReceiver = args[0] as bool;
+        final callModel = args[1] as CallModel;
+        return MaterialPageRoute(
+          builder: (_) {
+            return BlocProvider(
+                create: (_) => CallCubit(),
+                child:
+                    CallScreen(isReceiver: isReceiver, callModel: callModel));
+          },
+        );
+    }
+    return null;
+  }
+}

+ 137 - 0
lib/services/fcm/firebase_notification_handler.dart

@@ -0,0 +1,137 @@
+import 'dart:convert';
+import 'dart:io';
+import 'dart:math';
+
+import 'package:firebase_messaging/firebase_messaging.dart';
+import 'package:flutter/cupertino.dart';
+import 'package:flutter/material.dart';
+
+import 'package:flutter_local_notifications/flutter_local_notifications.dart';
+import 'package:youtube/data/models/call_model.dart';
+
+import '../../shared/constats.dart';
+import 'notification_handler.dart';
+
+
+class FirebaseNotifications{
+  late FirebaseMessaging _messaging;
+  late BuildContext myContext;
+
+  void setUpFcm({required BuildContext context,required Function onForegroundClickCallNotify})
+  {
+    _messaging = FirebaseMessaging.instance;
+    NotificationHandler.initNotification(context: context, selectNotificationCallback: (String? payload) {
+      if(payload!=null){
+        onForegroundClickCallNotify(payload);
+      }
+    });
+    firebaseCloudMessageListener(context);
+    myContext = context;
+
+  }
+
+  void firebaseCloudMessageListener(BuildContext context) async{
+
+    final bool? result = await NotificationHandler.flutterLocalNotificationPlugin
+        .resolvePlatformSpecificImplementation<
+        IOSFlutterLocalNotificationsPlugin>()
+        ?.requestPermissions(
+      alert: true,
+      badge: true,
+      sound: true,
+    );
+
+    NotificationSettings settings = await _messaging.requestPermission(
+      alert: true,
+      announcement: true,
+      badge: true,
+      carPlay: false,
+      criticalAlert: false,
+      provisional: false,
+      sound: true,
+    );
+    if (settings.authorizationStatus == AuthorizationStatus.authorized) {
+      debugPrint('User granted permission');
+    } else if (settings.authorizationStatus == AuthorizationStatus.provisional) {
+      debugPrint('User granted provisional permission');
+    } else {
+      debugPrint('User declined or has not accepted permission');
+    }
+    debugPrint('Setting ${settings.authorizationStatus} LocalPer ${result.toString()}');
+    //Get Token
+    //Handle message
+    FirebaseMessaging.onMessage.listen((remoteMessage) { //Foreground Msg
+      if(remoteMessage.data['type'] != 'call'){
+        showNotification(title: remoteMessage.data['title'],body: remoteMessage.data['body'],type: remoteMessage.data['type']);
+      }
+    });
+    FirebaseMessaging.onMessageOpenedApp.listen((remoteMessage) {
+      debugPrint('Receive open app: $remoteMessage ');
+      debugPrint('InOpenAppNotifyBody ${remoteMessage.data['body'].toString()}');
+      if(Platform.isIOS) {
+        showDialog(context: myContext, builder: (context)=> CupertinoAlertDialog(title: Text(remoteMessage.notification!.title??''),
+          content:  Text(remoteMessage.notification!.body??''),
+          actions: [
+            CupertinoDialogAction(
+              isDefaultAction: true,
+              child: const Text('OK'),
+              onPressed: ()=> Navigator.of(context,rootNavigator: true,).pop(),
+            )
+          ],));
+      }
+    });
+  }
+
+
+  static AndroidNotificationDetails callChannel = const AndroidNotificationDetails(
+      'com.agora.videocall.youtube', 'call_channel',
+      autoCancel: false,
+      ongoing: true,
+      importance: Importance.max,
+      priority: Priority.max);
+  static AndroidNotificationDetails normalChannel = const AndroidNotificationDetails(
+      'com.agora.videocall.youtube', 'normal_channel',
+      autoCancel: true,
+      ongoing: false,
+      importance: Importance.low,
+      priority: Priority.low);
+
+ static void showNotification({required String title,required String body,required String type}) async{
+   debugPrint('callDataFromNotify $body');
+   if(type == 'call'){
+     Map<String, dynamic> bodyJson = jsonDecode(body);
+
+     int notificationId =Random().nextInt(1000);
+     var ios = const IOSNotificationDetails();
+     var platform = NotificationDetails(
+         android: callChannel,iOS: ios);
+     await NotificationHandler.flutterLocalNotificationPlugin
+         .show(notificationId, '📞Ringing...', '${CallModel.fromJson(bodyJson).callerName} is calling you', platform,payload: body);
+     await Future.delayed(const Duration(seconds: callDurationInSec), () {
+       NotificationHandler.flutterLocalNotificationPlugin.cancel(notificationId).then((value) {
+         showMissedCallNotification(senderName: bodyJson['sender']['full_name']);
+       });
+     });
+   }
+   else{
+     int notificationId =Random().nextInt(1000);
+     var ios = const IOSNotificationDetails();
+     var platform = NotificationDetails(
+         android: normalChannel,iOS: ios);
+     await NotificationHandler.flutterLocalNotificationPlugin
+         .show(notificationId, title, body, platform,payload: body);
+   }
+  }
+
+  static void showMissedCallNotification({required String senderName}) async{
+    int notificationId =Random().nextInt(1000);
+    var ios = const IOSNotificationDetails();
+    var platform = NotificationDetails(
+        android: normalChannel,iOS: ios);
+    await NotificationHandler.flutterLocalNotificationPlugin
+        .show(notificationId, '📞Missed Call', 'You have missed call from $senderName', platform,payload: 'missedCall');
+
+  }
+
+
+}

+ 40 - 0
lib/services/fcm/notification_handler.dart

@@ -0,0 +1,40 @@
+import 'package:flutter/cupertino.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_local_notifications/flutter_local_notifications.dart';
+
+
+
+class NotificationHandler{
+  static final flutterLocalNotificationPlugin =
+      FlutterLocalNotificationsPlugin();
+  static late BuildContext myContext;
+
+  static void initNotification({required BuildContext context,required SelectNotificationCallback selectNotificationCallback}){ //customize
+    myContext = context;
+    var initAndroid = const AndroidInitializationSettings("@drawable/ic_notify");
+    const IOSInitializationSettings initializationSettingsIOS =
+     IOSInitializationSettings(
+      requestSoundPermission: false,
+      requestBadgePermission: false,
+      requestAlertPermission: false,
+      onDidReceiveLocalNotification: onDidReceiveLocalNotification,
+    );
+
+    var initSetting = InitializationSettings(android: initAndroid,iOS: initializationSettingsIOS);
+    flutterLocalNotificationPlugin.initialize(initSetting,onSelectNotification: selectNotificationCallback);
+
+  }
+
+
+  static Future onDidReceiveLocalNotification(int? id,String? title,String? body,String? payload) async{
+    showDialog(context: myContext, builder: (context)=> CupertinoAlertDialog(title: Text(title!),
+    content: Text(body!),
+    actions: [
+      CupertinoDialogAction(
+        isDefaultAction: true,
+        child: const Text('OK'),
+        onPressed: ()=> Navigator.of(context,rootNavigator: true,).pop(),
+      )
+    ],));
+  }
+}

+ 29 - 0
lib/shared/bloc_observer.dart

@@ -0,0 +1,29 @@
+
+import 'package:bloc/bloc.dart';
+import 'package:flutter/cupertino.dart';
+
+class AppBlocObserver extends BlocObserver {
+  @override
+  void onCreate(BlocBase bloc) {
+    super.onCreate(bloc);
+    debugPrint('onCreate -- ${bloc.runtimeType}');
+  }
+
+  @override
+  void onChange(BlocBase bloc, Change change) {
+    super.onChange(bloc, change);
+    debugPrint('onChange -- ${bloc.runtimeType}, $change');
+  }
+
+  @override
+  void onError(BlocBase bloc, Object error, StackTrace stackTrace) {
+    debugPrint('onError -- ${bloc.runtimeType}, $error');
+    super.onError(bloc, error, stackTrace);
+  }
+
+  @override
+  void onClose(BlocBase bloc) {
+    super.onClose(bloc);
+    debugPrint('onClose -- ${bloc.runtimeType}');
+  }
+}

+ 36 - 0
lib/shared/constats.dart

@@ -0,0 +1,36 @@
+//Firebase
+const userCollection = 'Users';
+const callsCollection = 'Calls';
+const tokensCollection = 'Tokens';
+
+const fcmKey = 'AAAA8hfeYZKXDOOmZ6OR8Cd70oZdKsD2Ao44YfuXBFgocNOH5gp2'; //replace with your Fcm key
+//Routes
+const loginScreen = '/';
+const homeScreen = '/homeScreen';
+const callScreen = '/callScreen';
+const testScreen = '/testScreen';
+
+
+
+//Agora
+const agoraAppId = 'e21dde29886b'; //replace with your agora app id
+const agoraTestChannelName = 'newChannel'; //replace with your agora channel name
+const agoraTestToken = '006effbIACQIrib0zw+jFMnUP0OBgJajy/o8utZ2Zg9CqRnJo7WwAAAAAEABUm4+syy+mYgEAAQDOL6Zi'; //replace with your agora token
+
+//EndPoints -- this is for generating call token programmatically for each call
+const cloudFunctionBaseUrl = 'https://us-central1-agora-409098655.cloudfunctions.net/'; //replace with your clouded api base url
+const fireCallEndpoint = 'app/access_token'; //replace with your clouded api endpoint
+
+
+const int callDurationInSec = 45;
+
+//Call Status
+enum CallStatus {
+  none,
+  ringing,
+  accept,
+  reject,
+  unAnswer,
+  cancel,
+  end,
+}

+ 78 - 0
lib/shared/dio_helper.dart

@@ -0,0 +1,78 @@
+import 'package:dio/dio.dart';
+
+import 'constats.dart';
+
+
+class DioHelper{
+
+  static late Dio dio;
+
+  static init()
+  {
+    dio = Dio(
+      BaseOptions(
+          receiveDataWhenStatusError: true,
+          connectTimeout: 30*1000, // 30 seconds
+          receiveTimeout: 30*1000 // 30 seconds
+      ),
+    );
+  }
+
+  static Future<Response> getData({required String endPoint,Map<String, dynamic>? query,
+    required String baseUrl}) async
+  {
+      dio.options.baseUrl = baseUrl;
+      dio.options.headers =
+      {
+        'Content-Type':'application/json',
+        'Accept': 'application/json',
+      };
+
+      try {
+        return await dio.get(endPoint,queryParameters: query,);
+      }on DioError catch (ex) {
+        if(ex.type == DioErrorType.connectTimeout){
+          throw Exception("Connection Timeout Exception");
+        }
+        if (ex.type == DioErrorType.receiveTimeout) {
+          throw Exception("Receive Timeout Exception");
+        }
+        throw Exception(ex.message);
+      }
+
+  }
+
+  static Future<Response> postData({
+    required String endPoint,
+    Map<String, dynamic>? query,
+    required Map<String, dynamic> data,
+    bool isRow = true,
+    bool isPut = false,
+    String lang = 'en',
+    String? token,
+    required String baseUrl,
+  }) async
+  {
+    dio.options.followRedirects = true;
+    dio.options.validateStatus = (status)  => true;
+
+    dio.options.baseUrl = baseUrl;
+    dio.options.headers = {
+      'Content-Type':'application/json',
+      'Accept': 'application/json',
+      'Authorization':'key= $fcmKey',
+    };
+
+    return !isPut ? await dio.post(
+      endPoint,
+      queryParameters: query,
+      data: isRow ? data : FormData.fromMap(data),
+    ) : await dio.put(
+      endPoint,
+      queryParameters: query,
+      data: isRow ? data : FormData.fromMap(data),
+    );
+  }
+
+
+}

+ 45 - 0
lib/shared/network/cache_helper.dart

@@ -0,0 +1,45 @@
+import 'package:shared_preferences/shared_preferences.dart';
+
+class CacheHelper
+{
+  static late SharedPreferences sharedPreferences;
+
+  static init() async
+  {
+    sharedPreferences = await SharedPreferences.getInstance();
+  }
+  static Future<bool> putBool({required String key, required bool value})async
+  {
+   return await sharedPreferences.setBool(key, value);
+  }
+  static String getString({required String key})
+  {
+    return  sharedPreferences.getString(key) ?? '';
+  }
+  static int getInteger({required String key})
+  {
+    return  sharedPreferences.getInt(key) ?? 0;
+  }
+  static bool getBool({required String key})
+  {
+    return  sharedPreferences.getBool(key) ?? false;
+  }
+  static Future<bool> saveData({required String key, required dynamic value}) async
+  {
+     if(value is String) {
+       return await sharedPreferences.setString(key, value);
+     } else if(value is bool) {
+       return await sharedPreferences.setBool(key, value);
+     } else if(value is int) {
+       return await sharedPreferences.setInt(key, value);
+     } else {
+       return await sharedPreferences.setDouble(key, value);
+     }
+
+  }
+
+  static Future<bool> removeData({required String key,}) async
+  {
+    return await sharedPreferences.remove(key);
+  }
+}

+ 79 - 0
lib/shared/network/dio_helper.dart

@@ -0,0 +1,79 @@
+import 'package:dio/dio.dart';
+
+
+class DioHelper{
+
+  static late Dio dio;
+
+  static init()
+  {
+    dio = Dio(
+      BaseOptions(
+          receiveDataWhenStatusError: true,
+          connectTimeout: 30*1000, // 30 seconds
+          receiveTimeout: 30*1000 // 30 seconds
+      ),
+    );
+  }
+
+  static Future<Response> getData({required String endPoint,Map<String, dynamic>? query,String lang = 'en',
+    String? token,required String baseUrl}) async
+  {
+      dio.options.baseUrl = baseUrl;
+      dio.options.headers =
+      {
+        'Content-Type':'application/json',
+        'Accept': 'application/json',
+        'Accept-Language':lang,
+        'Authorization':'Bearer $token',
+      };
+
+      try {
+        return await dio.get(endPoint,queryParameters: query,);
+      }on DioError catch (ex) {
+        if(ex.type == DioErrorType.connectTimeout){
+          throw Exception("Connection Timeout Exception");
+        }
+        if (ex.type == DioErrorType.receiveTimeout) {
+          throw Exception("Receive Timeout Exception");
+        }
+        throw Exception(ex.message);
+      }
+
+  }
+
+  static Future<Response> postData({
+    required String endPoint,
+    Map<String, dynamic>? query,
+    required Map<String, dynamic> data,
+    bool isRow = true,
+    bool isPut = false,
+    String lang = 'en',
+    String? token,
+    required String baseUrl,
+  }) async
+  {
+    dio.options.followRedirects = true;
+    dio.options.validateStatus = (status)  => true;
+
+    dio.options.baseUrl = baseUrl;
+    dio.options.headers = {
+      'Content-Type':'application/json',
+      'Accept': 'application/json',
+      'Accept-Language': lang,
+      'Authorization':'Bearer $token',
+    };
+
+    return !isPut ? await dio.post(
+      endPoint,
+      queryParameters: query,
+      data: isRow ? data : FormData.fromMap(data),
+    ) : await dio.put(
+      endPoint,
+      queryParameters: query,
+      data: isRow ? data : FormData.fromMap(data),
+    );
+  }
+
+
+}

+ 15 - 0
lib/shared/shared_widgets.dart

@@ -0,0 +1,15 @@
+import 'package:flutter/material.dart';
+import 'package:fluttertoast/fluttertoast.dart';
+
+
+showToast(
+    {required String msg}) {
+  Fluttertoast.showToast(
+      msg: msg,
+      toastLength: Toast.LENGTH_SHORT,
+      gravity: ToastGravity.BOTTOM,
+      timeInSecForIosWeb: 5,
+      backgroundColor: Colors.grey,
+      textColor: Colors.white,
+      fontSize: 16.0);
+}

+ 60 - 0
lib/shared/theme.dart

@@ -0,0 +1,60 @@
+import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
+
+//Colors
+const MaterialColor defaultColor = MaterialColor(
+  0xffE23D73,
+  <int, Color>{
+    50: Color(0xffE23D73),
+    100: Color(0xffE23D73),
+    200: Color(0xffE23D73),
+    300: Color(0xffE23D73),
+    400: Color(0xffE23D73),
+    500: Color(0xffE23D73),
+    600: Color(0xffE23D73),
+    700: Color(0xffE23D73),
+    800: Color(0xffE23D73),
+    900: Color(0xffE23D73),
+  },
+);
+
+ThemeData appTheme = ThemeData(
+  buttonTheme: const ButtonThemeData(
+    buttonColor: defaultColor,
+    textTheme: ButtonTextTheme.primary,
+  ),
+  primarySwatch: defaultColor,
+  scaffoldBackgroundColor: Colors.white,
+  canvasColor: Colors.transparent,
+  appBarTheme: const AppBarTheme(
+    titleTextStyle: TextStyle(
+      color: Colors.black,
+      fontSize: 19.0,
+      fontFamily: 'MonstMid',
+
+    ),
+    titleSpacing: 19.0,
+    systemOverlayStyle: SystemUiOverlayStyle(
+      statusBarColor: Colors.white,
+      statusBarIconBrightness: Brightness.dark,
+    ),
+    backgroundColor: Colors.white,
+    iconTheme: IconThemeData(
+      color: Colors.black,
+    ),
+    elevation: 0.0,
+  ),
+  textTheme: const TextTheme(
+    bodyText1: TextStyle(
+      fontSize: 17.0,
+      fontWeight: FontWeight.w600,
+      color: Colors.black,
+    ),
+    subtitle1: TextStyle(
+      fontSize: 13.0,
+      fontWeight: FontWeight.w600,
+      color: Colors.black,
+      height: 1.3,
+    ),
+  ),
+);

+ 726 - 0
pubspec.lock

@@ -0,0 +1,726 @@
+# Generated by pub
+# See https://dart.dev/tools/pub/glossary#lockfile
+packages:
+  agora_rtc_engine:
+    dependency: "direct main"
+    description:
+      name: agora_rtc_engine
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "5.2.0"
+  args:
+    dependency: transitive
+    description:
+      name: args
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "2.3.1"
+  async:
+    dependency: transitive
+    description:
+      name: async
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "2.8.2"
+  audioplayers:
+    dependency: "direct main"
+    description:
+      name: audioplayers
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "0.20.1"
+  bloc:
+    dependency: "direct main"
+    description:
+      name: bloc
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "8.0.3"
+  boolean_selector:
+    dependency: transitive
+    description:
+      name: boolean_selector
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "2.1.0"
+  cached_network_image:
+    dependency: "direct main"
+    description:
+      name: cached_network_image
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "3.2.1"
+  cached_network_image_platform_interface:
+    dependency: transitive
+    description:
+      name: cached_network_image_platform_interface
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "1.0.0"
+  cached_network_image_web:
+    dependency: transitive
+    description:
+      name: cached_network_image_web
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "1.0.1"
+  characters:
+    dependency: transitive
+    description:
+      name: characters
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "1.2.0"
+  charcode:
+    dependency: transitive
+    description:
+      name: charcode
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "1.3.1"
+  clock:
+    dependency: transitive
+    description:
+      name: clock
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "1.1.0"
+  cloud_firestore:
+    dependency: "direct main"
+    description:
+      name: cloud_firestore
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "3.1.11"
+  cloud_firestore_platform_interface:
+    dependency: transitive
+    description:
+      name: cloud_firestore_platform_interface
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "5.5.2"
+  cloud_firestore_web:
+    dependency: transitive
+    description:
+      name: cloud_firestore_web
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "2.6.11"
+  collection:
+    dependency: transitive
+    description:
+      name: collection
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "1.16.0"
+  conditional_builder_null_safety:
+    dependency: "direct main"
+    description:
+      name: conditional_builder_null_safety
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "0.0.6"
+  crypto:
+    dependency: transitive
+    description:
+      name: crypto
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "3.0.2"
+  cupertino_icons:
+    dependency: "direct main"
+    description:
+      name: cupertino_icons
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "1.0.4"
+  dbus:
+    dependency: transitive
+    description:
+      name: dbus
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "0.7.3"
+  dio:
+    dependency: "direct main"
+    description:
+      name: dio
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "4.0.6"
+  equatable:
+    dependency: "direct main"
+    description:
+      name: equatable
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "2.0.3"
+  fake_async:
+    dependency: transitive
+    description:
+      name: fake_async
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "1.3.0"
+  ffi:
+    dependency: transitive
+    description:
+      name: ffi
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "1.1.2"
+  file:
+    dependency: transitive
+    description:
+      name: file
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "6.1.2"
+  firebase_auth:
+    dependency: "direct main"
+    description:
+      name: firebase_auth
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "3.3.12"
+  firebase_auth_platform_interface:
+    dependency: transitive
+    description:
+      name: firebase_auth_platform_interface
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "6.2.2"
+  firebase_auth_web:
+    dependency: transitive
+    description:
+      name: firebase_auth_web
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "3.3.10"
+  firebase_core:
+    dependency: "direct main"
+    description:
+      name: firebase_core
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "1.14.0"
+  firebase_core_platform_interface:
+    dependency: transitive
+    description:
+      name: firebase_core_platform_interface
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "4.2.5"
+  firebase_core_web:
+    dependency: transitive
+    description:
+      name: firebase_core_web
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "1.6.1"
+  firebase_messaging:
+    dependency: "direct main"
+    description:
+      name: firebase_messaging
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "11.2.12"
+  firebase_messaging_platform_interface:
+    dependency: "direct overridden"
+    description:
+      name: firebase_messaging_platform_interface
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "3.2.1"
+  firebase_messaging_web:
+    dependency: transitive
+    description:
+      name: firebase_messaging_web
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "2.2.10"
+  flutter:
+    dependency: "direct main"
+    description: flutter
+    source: sdk
+    version: "0.0.0"
+  flutter_bloc:
+    dependency: "direct main"
+    description:
+      name: flutter_bloc
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "8.0.1"
+  flutter_blurhash:
+    dependency: transitive
+    description:
+      name: flutter_blurhash
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "0.6.8"
+  flutter_cache_manager:
+    dependency: transitive
+    description:
+      name: flutter_cache_manager
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "3.3.0"
+  flutter_lints:
+    dependency: "direct dev"
+    description:
+      name: flutter_lints
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "1.0.4"
+  flutter_local_notifications:
+    dependency: "direct main"
+    description:
+      name: flutter_local_notifications
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "9.5.3+1"
+  flutter_local_notifications_linux:
+    dependency: transitive
+    description:
+      name: flutter_local_notifications_linux
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "0.4.2"
+  flutter_local_notifications_platform_interface:
+    dependency: transitive
+    description:
+      name: flutter_local_notifications_platform_interface
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "5.0.0"
+  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"
+  fluttertoast:
+    dependency: "direct main"
+    description:
+      name: fluttertoast
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "8.0.9"
+  http:
+    dependency: transitive
+    description:
+      name: http
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "0.13.4"
+  http_parser:
+    dependency: transitive
+    description:
+      name: http_parser
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "4.0.0"
+  intl:
+    dependency: "direct main"
+    description:
+      name: intl
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "0.17.0"
+  js:
+    dependency: transitive
+    description:
+      name: js
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "0.6.4"
+  json_annotation:
+    dependency: transitive
+    description:
+      name: json_annotation
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "4.5.0"
+  lints:
+    dependency: transitive
+    description:
+      name: lints
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "1.0.1"
+  matcher:
+    dependency: transitive
+    description:
+      name: matcher
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "0.12.11"
+  material_color_utilities:
+    dependency: transitive
+    description:
+      name: material_color_utilities
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "0.1.4"
+  meta:
+    dependency: transitive
+    description:
+      name: meta
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "1.7.0"
+  modal_progress_hud_nsn:
+    dependency: "direct main"
+    description:
+      name: modal_progress_hud_nsn
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "0.1.0-nullsafety-1"
+  nested:
+    dependency: transitive
+    description:
+      name: nested
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "1.0.0"
+  octo_image:
+    dependency: transitive
+    description:
+      name: octo_image
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "1.0.1"
+  path:
+    dependency: transitive
+    description:
+      name: path
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "1.8.1"
+  path_provider:
+    dependency: transitive
+    description:
+      name: path_provider
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "2.0.10"
+  path_provider_android:
+    dependency: transitive
+    description:
+      name: path_provider_android
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "2.0.14"
+  path_provider_ios:
+    dependency: transitive
+    description:
+      name: path_provider_ios
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "2.0.9"
+  path_provider_linux:
+    dependency: transitive
+    description:
+      name: path_provider_linux
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "2.1.5"
+  path_provider_macos:
+    dependency: transitive
+    description:
+      name: path_provider_macos
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "2.0.6"
+  path_provider_platform_interface:
+    dependency: transitive
+    description:
+      name: path_provider_platform_interface
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "2.0.3"
+  path_provider_windows:
+    dependency: transitive
+    description:
+      name: path_provider_windows
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "2.0.5"
+  pedantic:
+    dependency: transitive
+    description:
+      name: pedantic
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "1.11.1"
+  permission_handler:
+    dependency: "direct main"
+    description:
+      name: permission_handler
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "9.2.0"
+  permission_handler_android:
+    dependency: transitive
+    description:
+      name: permission_handler_android
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "9.0.2+1"
+  permission_handler_apple:
+    dependency: transitive
+    description:
+      name: permission_handler_apple
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "9.0.4"
+  permission_handler_platform_interface:
+    dependency: transitive
+    description:
+      name: permission_handler_platform_interface
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "3.7.0"
+  permission_handler_windows:
+    dependency: transitive
+    description:
+      name: permission_handler_windows
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "0.1.0"
+  petitparser:
+    dependency: transitive
+    description:
+      name: petitparser
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "4.4.0"
+  pin_code_fields:
+    dependency: "direct main"
+    description:
+      name: pin_code_fields
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "7.3.0"
+  platform:
+    dependency: transitive
+    description:
+      name: platform
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "3.1.0"
+  plugin_platform_interface:
+    dependency: transitive
+    description:
+      name: plugin_platform_interface
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "2.1.2"
+  process:
+    dependency: transitive
+    description:
+      name: process
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "4.2.4"
+  provider:
+    dependency: transitive
+    description:
+      name: provider
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "6.0.2"
+  quiver:
+    dependency: "direct main"
+    description:
+      name: quiver
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "3.1.0"
+  rxdart:
+    dependency: transitive
+    description:
+      name: rxdart
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "0.27.3"
+  shared_preferences:
+    dependency: "direct main"
+    description:
+      name: shared_preferences
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "2.0.13"
+  shared_preferences_android:
+    dependency: "direct overridden"
+    description:
+      name: shared_preferences_android
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "2.0.10"
+  shared_preferences_ios:
+    dependency: transitive
+    description:
+      name: shared_preferences_ios
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "2.1.0"
+  shared_preferences_linux:
+    dependency: transitive
+    description:
+      name: shared_preferences_linux
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "2.1.0"
+  shared_preferences_macos:
+    dependency: transitive
+    description:
+      name: shared_preferences_macos
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "2.0.3"
+  shared_preferences_platform_interface:
+    dependency: transitive
+    description:
+      name: shared_preferences_platform_interface
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "2.0.0"
+  shared_preferences_web:
+    dependency: transitive
+    description:
+      name: shared_preferences_web
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "2.0.3"
+  shared_preferences_windows:
+    dependency: transitive
+    description:
+      name: shared_preferences_windows
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "2.1.0"
+  sky_engine:
+    dependency: transitive
+    description: flutter
+    source: sdk
+    version: "0.0.99"
+  source_span:
+    dependency: transitive
+    description:
+      name: source_span
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "1.8.2"
+  sqflite:
+    dependency: transitive
+    description:
+      name: sqflite
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "2.0.2+1"
+  sqflite_common:
+    dependency: transitive
+    description:
+      name: sqflite_common
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "2.2.1+1"
+  stack_trace:
+    dependency: transitive
+    description:
+      name: stack_trace
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "1.10.0"
+  stream_channel:
+    dependency: transitive
+    description:
+      name: stream_channel
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "2.1.0"
+  string_scanner:
+    dependency: transitive
+    description:
+      name: string_scanner
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "1.1.0"
+  synchronized:
+    dependency: transitive
+    description:
+      name: synchronized
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "3.0.0+2"
+  term_glyph:
+    dependency: transitive
+    description:
+      name: term_glyph
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "1.2.0"
+  test_api:
+    dependency: transitive
+    description:
+      name: test_api
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "0.4.9"
+  timezone:
+    dependency: transitive
+    description:
+      name: timezone
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "0.8.0"
+  typed_data:
+    dependency: transitive
+    description:
+      name: typed_data
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "1.3.0"
+  uuid:
+    dependency: transitive
+    description:
+      name: uuid
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "3.0.6"
+  vector_math:
+    dependency: transitive
+    description:
+      name: vector_math
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "2.1.2"
+  win32:
+    dependency: transitive
+    description:
+      name: win32
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "2.5.1"
+  xdg_directories:
+    dependency: transitive
+    description:
+      name: xdg_directories
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "0.2.0+1"
+  xml:
+    dependency: transitive
+    description:
+      name: xml
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "5.3.1"
+sdks:
+  dart: ">=2.17.0 <3.0.0"
+  flutter: ">=3.0.0"

Some files were not shown because too many files changed in this diff